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>
);
}
- Drag-and-drop file upload
- File size validation (10MB limit)
- PDF-only accept filter
- Error handling and display
- Visual drag state feedback
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>
);
- 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>
);
};
- Zustand state integration
- Dynamic section management (add/remove)
- Real-time updates
- Sections: Personal Info, Experience, Projects, Education, Skills, Custom
- Scroll area for long content
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;
}
- Server-side execution
- Automatic user creation
- Username generation from email
- Idempotent (checks for existing user)
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 incomponents/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 incomponents/ui/:
- Button
- Input
- Dialog
- Card
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>
import { Input } from "@/components/ui/input";
<Input
type="text"
placeholder="Enter your name"
value={value}
onChange={(e) => setValue(e.target.value)}
/>
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>Edit Profile</DialogTitle>
</DialogHeader>
{/* Content */}
</DialogContent>
</Dialog>
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
<Card>
<CardHeader>
<CardTitle>Resume</CardTitle>
</CardHeader>
<CardContent>
<p>Your resume content</p>
</CardContent>
</Card>
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,
});
maxSize- Maximum file size in bytesaccept- Accepted MIME typesmultiple- Allow multiple filesonFilesChange- 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
}