Skip to main content

Component structure

Wrkks components are organized by functionality:
components/
├── buttons/          # Action buttons
├── resume/           # Resume editing & preview
├── ui/               # Base UI components (Shadcn)
├── FileUpload.tsx    # PDF upload component
├── Hero.tsx          # Landing page hero
├── SyncUser.tsx      # User sync component
└── ThemeToggle.tsx   # Dark mode toggle

Core components

FileUpload

Handles PDF resume uploads with drag-and-drop support.
"use client";

import { useFileUpload } from "@/hooks/use-file-upload";
import GenerateBtn from "./buttons/GenerateBtn";

export default function FileUpload() {
  const maxSize = 10 * 1024 * 1024; // 10MB

  const [
    { files, isDragging, errors },
    {
      handleDragEnter,
      handleDragLeave,
      handleDragOver,
      handleDrop,
      openFileDialog,
      removeFile,
      getInputProps,
    },
  ] = useFileUpload({
    maxSize,
    accept: "application/pdf",
  });

  const file = files[0];

  return (
    <div className="flex flex-col gap-2 w-100 cursor-pointer">
      <div
        className="flex min-h-40 flex-col items-center justify-center rounded-xl border-2 border-dashed"
        data-dragging={isDragging || undefined}
        onClick={openFileDialog}
        onDragEnter={handleDragEnter}
        onDragLeave={handleDragLeave}
        onDragOver={handleDragOver}
        onDrop={handleDrop}
      >
        <input {...getInputProps()} className="sr-only" />
        <UploadIcon className="size-4" />
        <p>Drag & drop pdf or click to browse</p>
      </div>

      {file && (
        <div className="flex items-center justify-between">
          <p>{file.file.name}</p>
          <Button onClick={() => removeFile(file.id)}>Remove</Button>
        </div>
      )}

      <GenerateBtn file={file?.file as File} />
    </div>
  );
}
Features:
  • Drag-and-drop file upload
  • File size validation (10MB limit)
  • PDF-only accept filter
  • Error handling and display
  • Visual drag state feedback
Usage:
import FileUpload from "@/components/FileUpload";

export default function UploadPage() {
  return (
    <div>
      <h1>Upload Your Resume</h1>
      <FileUpload />
    </div>
  );
}

Hero

Landing page hero section with gradient effects and CTAs.
import HomeActionBtn from "./buttons/BuildMyWebsiteBtn";
import ShareBtn from "./buttons/ShareBtn";
import Timeline from "./timeline";

const Hero = () => (
  <div className="relative flex flex-col gap-16 items-center justify-center px-6 py-12">
    {/* Dynamic Background Blur */}
    <div className="absolute inset-0 pointer-events-none -z-10">
      <div className="absolute top-1/4 left-1/2 -translate-x-1/2 w-75 h-75 md:w-150 md:h-150 bg-blue-500/10 dark:bg-white/5 blur-[120px] rounded-full" />
    </div>

    <div className="relative z-10 text-center max-w-4xl mx-auto">
      {/* Badge */}
      <div className="inline-flex items-center backdrop-blur-md bg-black/3 dark:bg-white/5 border rounded-full px-4 py-1.5">
        <Sparkles className="mr-2 size-3.5 text-blue-500" />
        LinkedIn to Website With Wrkks
      </div>

      {/* Headline */}
      <h1 className="mt-8 text-4xl sm:text-6xl md:text-7xl font-bold">
        Turn your Resume into a{" "}
        <span className="text-blue-600 dark:text-blue-400">
          Stunning Website
        </span>{" "}
        in seconds.
      </h1>

      {/* Description */}
      <p className="mt-6 text-base md:text-lg text-slate-600 dark:text-white/70">
        Stop wrestling with website builders. Drop your LinkedIn PDF or upload a
        resume PDF, and we'll instantly generate a professional site.
      </p>

      {/* CTA Buttons */}
      <div className="flex flex-col sm:flex-row items-center justify-center gap-4 mt-10">
        <HomeActionBtn />
        <ShareBtn />
      </div>
    </div>

    <Timeline />
  </div>
);
Features:
  • Responsive typography (4xl → 6xl → 7xl)
  • Animated background blur effect
  • Glass morphism badge
  • Dual CTA buttons
  • Timeline component integration

ResumeEditor

Comprehensive resume editing interface with sections for all resume data.
"use client";

import { useResumeStore } from "@/hooks/stores/useResumeStore";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { Button } from "@/components/ui/button";

export const ResumeEditor = () => {
  const {
    resume,
    updatePersonalInfo,
    updateSummary,
    updateExperience,
  } = useResumeStore();

  if (!resume) {
    return <div>No resume data found.</div>;
  }

  return (
    <aside className="w-full h-full bg-background flex flex-col max-w-3xl mx-auto border rounded-lg">
      <ScrollArea className="flex-1">
        <div className="p-8 space-y-10">
          {/* Personal Info */}
          <section>
            <h2 className="text-2xl font-semibold mb-4">Personal Info</h2>
            <div className="flex gap-4 mb-4">
              <div>
                <label className="font-medium mb-1 block">Name</label>
                <Input
                  value={resume.personalInfo.name}
                  onChange={(e) => updatePersonalInfo({ name: e.target.value })}
                />
              </div>
              <div>
                <label className="font-medium mb-1 block">Title</label>
                <Input
                  value={resume.personalInfo.title}
                  onChange={(e) => updatePersonalInfo({ title: e.target.value })}
                />
              </div>
            </div>
          </section>

          {/* Work Experience */}
          <section>
            <h2 className="text-2xl font-semibold mb-4">Work Experience</h2>
            {resume.experience.map((exp, idx) => (
              <div key={idx} className="border rounded-lg p-5 space-y-4 mb-4">
                <Input
                  value={exp.position}
                  onChange={(e) => {
                    const newExp = [...resume.experience];
                    newExp[idx].position = e.target.value;
                    updateExperience(newExp);
                  }}
                />
                {/* More fields... */}
              </div>
            ))}
            <Button onClick={addExperience}>Add Experience</Button>
          </section>
        </div>
      </ScrollArea>
    </aside>
  );
};
Features:
  • Zustand state integration
  • Dynamic section management (add/remove)
  • Real-time updates
  • Sections: Personal Info, Experience, Projects, Education, Skills, Custom
  • Scroll area for long content
State management:
const { resume, updatePersonalInfo } = useResumeStore();

// Update personal info
updatePersonalInfo({ name: "John Doe" });

// Add new experience
const newExp = { company: "", position: "", /* ... */ };
updateExperience([...resume.experience, newExp]);

SyncUser

Server component that syncs Clerk users to Supabase on first visit.
"use server";

import { createClient } from "@/lib/supabase/server";
import { currentUser } from "@clerk/nextjs/server";

export default async function SyncUser() {
  const clerkUser = await currentUser();
  const supabase = await createClient();

  if (!clerkUser) return null;

  const { data: existingUser } = await supabase
    .from("users")
    .select("id")
    .eq("clerk_user_id", clerkUser.id)
    .single();

  if (!existingUser) {
    const username = clerkUser.emailAddresses[0].emailAddress.split("@")[0];

    await supabase.from("users").insert({
      clerk_user_id: clerkUser.id,
      username,
      email: clerkUser.emailAddresses[0].emailAddress,
      resume: null,
    });
  }

  return null;
}
Features:
  • Server-side execution
  • Automatic user creation
  • Username generation from email
  • Idempotent (checks for existing user)
Usage in layout:
app/layout.tsx
import SyncUser from "@/components/SyncUser";

export default function RootLayout({ children }) {
  return (
    <ClerkProvider>
      <html>
        <body>
          <SyncUser />
          <main>{children}</main>
        </body>
      </html>
    </ClerkProvider>
  );
}

Button components

All action buttons are in components/buttons/:

GenerateBtn

Triggers resume parsing from uploaded PDF.
import { useState } from "react";
import { useRouter } from "next/navigation";
import { Button } from "@/components/ui/button";

export default function GenerateBtn({ file }: { file: File }) {
  const [loading, setLoading] = useState(false);
  const router = useRouter();

  const handleGenerate = async () => {
    if (!file) return;
    
    setLoading(true);
    const formData = new FormData();
    formData.append("file", file);

    // Parse PDF
    const parseRes = await fetch("/api/parse-resume", {
      method: "POST",
      body: formData,
    });
    const { text } = await parseRes.json();

    // Extract structured data
    const extractRes = await fetch("/api/extract-info", {
      method: "POST",
      body: JSON.stringify({ text }),
    });
    const resume = await extractRes.json();

    // Save to store and navigate
    useResumeStore.getState().setResume(resume);
    router.push("/website");
  };

  return (
    <Button onClick={handleGenerate} disabled={!file || loading}>
      {loading ? "Generating..." : "Generate Website"}
    </Button>
  );
}

PublishBtn

Publishes user’s website and makes it live.
import { Button } from "@/components/ui/button";

export default function PublishBtn({ data }) {
  const handlePublish = async () => {
    await fetch("/api/user/publish-resume", {
      method: "POST",
      body: JSON.stringify({ islive: true }),
    });
  };

  return (
    <Button onClick={handlePublish}>
      {data?.islive ? "Update" : "Publish"}
    </Button>
  );
}

UI components

Base components from Shadcn/ui in components/ui/:
import { Button } from "@/components/ui/button";

<Button variant="default">Click me</Button>
<Button variant="outline">Outline</Button>
<Button variant="ghost">Ghost</Button>
<Button size="sm">Small</Button>
<Button size="lg">Large</Button>

Hooks

useFileUpload

Custom hook for file upload functionality:
hooks/use-file-upload.ts
const [
  { files, isDragging, errors },
  { handleDrop, removeFile, openFileDialog, getInputProps }
] = useFileUpload({
  maxSize: 10 * 1024 * 1024,
  accept: "application/pdf",
  multiple: false,
});
Options:
  • maxSize - Maximum file size in bytes
  • accept - Accepted MIME types
  • multiple - Allow multiple files
  • onFilesChange - Callback when files change

useResumeStore

Zustand store for resume state:
import { useResumeStore } from "@/hooks/stores/useResumeStore";

function MyComponent() {
  const { resume, setResume, updatePersonalInfo } = useResumeStore();
  
  return (
    <div>
      <h1>{resume?.personalInfo.name}</h1>
      <button onClick={() => updatePersonalInfo({ name: "New Name" })}>
        Update Name
      </button>
    </div>
  );
}

Component patterns

Server vs Client Components

// No "use client" directive
import { getUserData } from "@/lib/supabase/user/getUserData";

export default async function Profile() {
  const user = await getUserData();
  return <div>{user.name}</div>;
}

Composition

// Compose smaller components
export default function WebsitePage() {
  return (
    <div>
      <NavBar />
      <ResumeEditor />
      <ResumePreview />
      <Footer />
    </div>
  );
}

Type safety

import { Resume } from "@/lib/types";

interface ResumeEditorProps {
  resume: Resume;
  onUpdate: (resume: Resume) => void;
}

export function ResumeEditor({ resume, onUpdate }: ResumeEditorProps) {
  // TypeScript ensures type safety
}

Build docs developers (and LLMs) love