feat: Add dashboard for managing portfolio content (blog, experience, projects) and restructure public portfolio routes.

This commit is contained in:
Moh Dzulfikri Maulana
2026-03-09 05:37:58 +07:00
parent dca848666e
commit e25416f3db
31 changed files with 2591 additions and 35 deletions

View File

@@ -0,0 +1,55 @@
"use client";
import { useState } from "react";
import { Trash2, Loader2 } from "lucide-react";
import { cn } from "@/lib/utils";
interface DeleteButtonProps {
id: string;
onDelete: (id: string) => Promise<any>;
className?: string;
}
export function DeleteButton({ id, onDelete, className }: DeleteButtonProps) {
const [isDeleting, setIsDeleting] = useState(false);
const [confirm, setConfirm] = useState(false);
const handleDelete = async () => {
if (!confirm) {
setConfirm(true);
setTimeout(() => setConfirm(false), 3000);
return;
}
setIsDeleting(true);
try {
await onDelete(id);
} catch (error) {
console.error(error);
alert("Failed to delete.");
setIsDeleting(false);
}
};
return (
<button
onClick={handleDelete}
disabled={isDeleting}
className={cn(
"p-2 rounded-lg transition-all flex items-center gap-2",
confirm
? "bg-destructive text-destructive-foreground hover:bg-destructive/90"
: "text-foreground-muted hover:text-destructive hover:bg-destructive/10",
className
)}
title={confirm ? "Click again to confirm" : "Delete"}
>
{isDeleting ? (
<Loader2 size={16} className="animate-spin" />
) : (
<Trash2 size={16} />
)}
{confirm && <span className="text-[10px] font-bold uppercase">Confirm?</span>}
</button>
);
}

View File

@@ -0,0 +1,252 @@
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { BlogStatus } from "@prisma/client";
import { createBlog, updateBlog } from "@/lib/actions/blog";
import {
Save,
X,
Plus,
Trash2,
ChevronLeft,
Layout,
Type,
FileText,
Tag as TagIcon,
Globe,
Eye,
} from "lucide-react";
import Link from "next/link";
import { cn } from "@/lib/utils";
interface BlogFormProps {
initialData?: any;
}
export function BlogForm({ initialData }: BlogFormProps) {
const router = useRouter();
const [loading, setLoading] = useState(false);
const [tags, setTags] = useState<string[]>(
initialData?.tags?.map((t: any) => t.name) || [],
);
const [tagInput, setTagInput] = useState("");
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
setLoading(true);
try {
const formData = new FormData(e.currentTarget);
const data = {
title: formData.get("title") as string,
slug: formData.get("slug") as string,
content: formData.get("content") as string,
excerpt: formData.get("excerpt") as string,
status: formData.get("status") as BlogStatus,
tags: tags,
};
if (initialData) {
await updateBlog(initialData.id, data);
} else {
await createBlog(data);
}
router.push("/dashboard/blog");
router.refresh();
} catch (error) {
console.error(error);
alert("Something went wrong. Check the console.");
} finally {
setLoading(false);
}
}
const addTag = () => {
if (tagInput && !tags.includes(tagInput)) {
setTags([...tags, tagInput]);
setTagInput("");
}
};
const removeTag = (tagToRemove: string) => {
setTags(tags.filter((t) => t !== tagToRemove));
};
return (
<form onSubmit={handleSubmit} className="space-y-8 max-w-5xl mx-auto pb-20">
<div className="flex items-center justify-between gap-4 border-b border-border pb-6">
<div className="flex items-center gap-4">
<Link
href="/dashboard/blog"
className="p-2 hover:bg-surface-elevated rounded-lg text-foreground-muted transition-colors"
>
<ChevronLeft size={20} />
</Link>
<div>
<h1 className="text-2xl font-display font-bold text-foreground">
{initialData ? "Edit Post" : "Create New Post"}
</h1>
<p className="text-sm text-foreground-muted">
{initialData ? "Make changes to your article." : "Draft a new masterpiece."}
</p>
</div>
</div>
<div className="flex items-center gap-3">
<button
type="button"
onClick={() => router.back()}
className="px-4 py-2 border border-border bg-surface text-foreground font-medium rounded-lg hover:bg-surface-elevated transition-all text-sm"
>
Cancel
</button>
<button
type="submit"
disabled={loading}
className="inline-flex items-center gap-2 px-6 py-2 bg-primary text-primary-foreground font-semibold rounded-lg hover:shadow-lg hover:shadow-primary/20 transition-all disabled:opacity-50 text-sm"
>
<Save size={18} />
{loading ? "Saving..." : initialData ? "Update Changes" : "Publish Post"}
</button>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
{/* Main Content */}
<div className="lg:col-span-2 space-y-6">
<div className="bg-surface border border-border/50 rounded-2xl p-6 space-y-6 card-shadow">
<div className="space-y-2">
<label className="text-sm font-semibold text-foreground flex items-center gap-2">
<Type size={16} className="text-primary/60" />
Post Title
</label>
<input
name="title"
defaultValue={initialData?.title}
placeholder="Enter a catchy title..."
required
className="w-full px-4 py-3 bg-background border border-border/50 rounded-xl focus:ring-2 focus:ring-primary/20 focus:border-primary outline-none transition-all text-lg font-medium"
/>
</div>
<div className="space-y-2">
<label className="text-sm font-semibold text-foreground flex items-center gap-2">
<Globe size={16} className="text-primary/60" />
Slug URL
</label>
<div className="flex">
<span className="inline-flex items-center px-3 rounded-l-xl border border-r-0 border-border/50 bg-surface-elevated text-foreground-muted text-sm italic">
/blog/
</span>
<input
name="slug"
defaultValue={initialData?.slug}
placeholder="post-slug-here"
required
className="flex-1 px-4 py-2.5 bg-background border border-border/50 rounded-r-xl focus:ring-2 focus:ring-primary/20 focus:border-primary outline-none transition-all text-sm"
/>
</div>
</div>
<div className="space-y-2">
<label className="text-sm font-semibold text-foreground flex items-center gap-2">
<FileText size={16} className="text-primary/60" />
Content (Markdown supported)
</label>
<textarea
name="content"
defaultValue={initialData?.content}
placeholder="Write your story here..."
required
rows={15}
className="w-full px-4 py-3 bg-background border border-border/50 rounded-xl focus:ring-2 focus:ring-primary/20 focus:border-primary outline-none transition-all text-sm font-mono leading-relaxed"
/>
</div>
</div>
</div>
{/* Sidebar / Metadata */}
<div className="space-y-6">
{/* Status & Settings */}
<div className="bg-surface border border-border/50 rounded-2xl p-6 space-y-6 card-shadow">
<h3 className="text-sm font-bold uppercase tracking-widest text-foreground-muted">Publishing</h3>
<div className="space-y-2">
<label className="text-sm font-semibold text-foreground">Status</label>
<select
name="status"
defaultValue={initialData?.status || BlogStatus.DRAFT}
className="w-full px-4 py-2.5 bg-background border border-border/50 rounded-xl focus:ring-2 focus:ring-primary/20 focus:border-primary outline-none transition-all text-sm"
>
<option value={BlogStatus.DRAFT}>Draft</option>
<option value={BlogStatus.PUBLISHED}>Published</option>
<option value={BlogStatus.SCHEDULED}>Scheduled</option>
</select>
</div>
<div className="space-y-2 pt-2 border-t border-border/50">
<label className="text-sm font-semibold text-foreground flex items-center gap-2">
<Layout size={16} className="text-primary/60" />
Excerpt
</label>
<textarea
name="excerpt"
defaultValue={initialData?.excerpt}
placeholder="Brief summary of the post..."
rows={4}
className="w-full px-4 py-3 bg-background border border-border/50 rounded-xl focus:ring-2 focus:ring-primary/20 focus:border-primary outline-none transition-all text-sm leading-snug"
/>
</div>
</div>
{/* Tags */}
<div className="bg-surface border border-border/50 rounded-2xl p-6 space-y-4 card-shadow">
<h3 className="text-sm font-bold uppercase tracking-widest text-foreground-muted flex items-center gap-2">
<TagIcon size={14} />
Tags
</h3>
<div className="flex gap-2">
<input
value={tagInput}
onChange={(e) => setTagInput(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && (e.preventDefault(), addTag())}
placeholder="Add tag..."
className="flex-1 px-3 py-2 bg-background border border-border/50 rounded-lg text-xs outline-none focus:border-primary"
/>
<button
type="button"
onClick={addTag}
className="p-2 bg-primary/10 text-primary rounded-lg hover:bg-primary/20 transition-colors"
>
<Plus size={16} />
</button>
</div>
<div className="flex flex-wrap gap-2 pt-2">
{tags.map((tag) => (
<span
key={tag}
className="inline-flex items-center gap-1 px-2 py-1 bg-surface-elevated border border-border/50 rounded-md text-[10px] font-bold text-foreground"
>
{tag}
<button
type="button"
onClick={() => removeTag(tag)}
className="p-0.5 hover:text-destructive transition-colors"
>
<X size={10} />
</button>
</span>
))}
{tags.length === 0 && (
<p className="text-xs text-foreground-muted italic">No tags added yet.</p>
)}
</div>
</div>
</div>
</div>
</form>
);
}

View File

@@ -0,0 +1,327 @@
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { createExperience, updateExperience } from "@/lib/actions/experience";
import {
Save,
ChevronLeft,
Briefcase,
Layers,
Building2,
Calendar,
Layers as TechIcon,
Trash2,
Plus,
X,
ListIcon,
} from "lucide-react";
import Link from "next/link";
import { cn } from "@/lib/utils";
interface ExperienceFormProps {
initialData?: any;
}
export function ExperienceForm({ initialData }: ExperienceFormProps) {
const router = useRouter();
const [loading, setLoading] = useState(false);
const [highlights, setHighlights] = useState<string[]>(
initialData?.highlights || [],
);
const [highlightInput, setHighlightInput] = useState("");
const [techStack, setTechStack] = useState<string[]>(
initialData?.techStack || [],
);
const [techInput, setTechInput] = useState("");
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
setLoading(true);
try {
const formData = new FormData(e.currentTarget);
const data = {
company: formData.get("company") as string,
role: formData.get("role") as string,
startDate: new Date(formData.get("startDate") as string),
endDate: formData.get("endDate") ? new Date(formData.get("endDate") as string) : null,
current: formData.get("current") === "on",
description: formData.get("description") as string,
highlights: highlights,
techStack: techStack,
order: parseInt(formData.get("order") as string) || 0,
};
if (initialData) {
await updateExperience(initialData.id, data);
} else {
await createExperience(data);
}
router.push("/dashboard/experience");
router.refresh();
} catch (error) {
console.error(error);
alert("Something went wrong.");
} finally {
setLoading(false);
}
}
const addHighlight = () => {
if (highlightInput && !highlights.includes(highlightInput)) {
setHighlights([...highlights, highlightInput]);
setHighlightInput("");
}
};
const removeHighlight = (h: string) => {
setHighlights(highlights.filter((t) => t !== h));
};
const addTech = () => {
if (techInput && !techStack.includes(techInput)) {
setTechStack([...techStack, techInput]);
setTechInput("");
}
};
const removeTech = (t: string) => {
setTechStack(techStack.filter((tech) => tech !== t));
};
return (
<form onSubmit={handleSubmit} className="space-y-8 max-w-5xl mx-auto pb-20">
<div className="flex items-center justify-between gap-4 border-b border-border pb-6">
<div className="flex items-center gap-4">
<Link
href="/dashboard/experience"
className="p-2 hover:bg-surface-elevated rounded-lg text-foreground-muted transition-colors"
>
<ChevronLeft size={20} />
</Link>
<div>
<h1 className="text-2xl font-display font-bold text-foreground">
{initialData ? "Edit Position" : "Add Work Experience"}
</h1>
<p className="text-sm text-foreground-muted">
{initialData ? "Updating your career details." : "Showcase your professional journey."}
</p>
</div>
</div>
<div className="flex items-center gap-3">
<button
type="button"
onClick={() => router.back()}
className="px-4 py-2 border border-border bg-surface text-foreground font-medium rounded-lg hover:bg-surface-elevated transition-all text-sm"
>
Cancel
</button>
<button
type="submit"
disabled={loading}
className="inline-flex items-center gap-2 px-6 py-2 bg-primary text-primary-foreground font-semibold rounded-lg hover:shadow-lg hover:shadow-primary/20 transition-all disabled:opacity-50 text-sm"
>
<Save size={18} />
{loading ? "Saving..." : initialData ? "Update Record" : "Add Experience"}
</button>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
<div className="lg:col-span-2 space-y-6">
<div className="bg-surface border border-border/50 rounded-2xl p-6 space-y-6 card-shadow">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<label className="text-sm font-semibold text-foreground flex items-center gap-2">
<Building2 size={16} className="text-primary/60" />
Company Name
</label>
<input
name="company"
defaultValue={initialData?.company}
placeholder="e.g. Google"
required
className="w-full px-4 py-2.5 bg-background border border-border/50 rounded-xl focus:ring-2 focus:ring-primary/20 focus:border-primary outline-none transition-all text-sm font-medium"
/>
</div>
<div className="space-y-2">
<label className="text-sm font-semibold text-foreground flex items-center gap-2">
<Briefcase size={16} className="text-primary/60" />
Job Role
</label>
<input
name="role"
defaultValue={initialData?.role}
placeholder="e.g. Senior Frontend Developer"
required
className="w-full px-4 py-2.5 bg-background border border-border/50 rounded-xl focus:ring-2 focus:ring-primary/20 focus:border-primary outline-none transition-all text-sm font-medium"
/>
</div>
</div>
<div className="space-y-2 pt-4">
<label className="text-sm font-semibold text-foreground">Position Overview</label>
<textarea
name="description"
defaultValue={initialData?.description}
placeholder="Briefly summarize your key responsibilities and impact..."
required
rows={4}
className="w-full px-4 py-3 bg-background border border-border/50 rounded-xl focus:ring-2 focus:ring-primary/20 focus:border-primary outline-none transition-all text-sm leading-relaxed"
/>
</div>
</div>
<div className="bg-surface border border-border/50 rounded-2xl p-6 space-y-4 card-shadow">
<h3 className="text-sm font-bold uppercase tracking-widest text-foreground-muted flex items-center gap-2">
<ListIcon size={14} />
Key Highlights & Impact
</h3>
<div className="flex gap-2">
<input
value={highlightInput}
onChange={(e) => setHighlightInput(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && (e.preventDefault(), addHighlight())}
placeholder="Add high-impact achievement..."
className="flex-1 px-3 py-2 bg-background border border-border/50 rounded-lg text-xs outline-none focus:border-primary"
/>
<button
type="button"
onClick={addHighlight}
className="p-2 bg-primary/10 text-primary rounded-lg hover:bg-primary/20 transition-colors"
>
<Plus size={16} />
</button>
</div>
<div className="space-y-2 pt-2">
{highlights.map((h) => (
<div
key={h}
className="flex items-start gap-3 p-3 bg-surface-elevated/50 border border-border/40 rounded-xl group"
>
<div className="w-1.5 h-1.5 rounded-full bg-primary shrink-0 mt-1.5" />
<span className="text-xs text-foreground flex-1 leading-normal">{h}</span>
<button
type="button"
onClick={() => removeHighlight(h)}
className="p-1 text-foreground-muted hover:text-destructive transition-colors opacity-0 group-hover:opacity-100"
>
<Trash2 size={12} />
</button>
</div>
))}
{highlights.length === 0 && (
<p className="text-xs text-foreground-muted italic text-center py-4 bg-background/30 rounded-xl border border-dashed border-border/50">Describe your biggest wins in this role.</p>
)}
</div>
</div>
</div>
<div className="space-y-6">
<div className="bg-surface border border-border/50 rounded-2xl p-6 space-y-6 card-shadow">
<h3 className="text-sm font-bold uppercase tracking-widest text-foreground-muted">Timeline</h3>
<div className="space-y-4">
<div className="space-y-2">
<label className="text-xs font-semibold text-foreground flex items-center gap-2">
<Calendar size={14} />
Start Date
</label>
<input
name="startDate"
type="date"
defaultValue={initialData?.startDate ? new Date(initialData.startDate).toISOString().split('T')[0] : ""}
required
className="w-full px-3 py-2 bg-background border border-border/50 rounded-lg outline-none text-xs"
/>
</div>
<div className="space-y-2">
<label className="text-xs font-semibold text-foreground flex items-center gap-2">
<Calendar size={14} />
End Date
</label>
<input
name="endDate"
type="date"
defaultValue={initialData?.endDate ? new Date(initialData.endDate).toISOString().split('T')[0] : ""}
disabled={initialData?.current}
className="w-full px-3 py-2 bg-background border border-border/50 rounded-lg outline-none text-xs disabled:opacity-40"
/>
</div>
<div className="flex items-center gap-2 pt-2">
<input
name="current"
type="checkbox"
id="current"
defaultChecked={initialData?.current}
className="w-3.5 h-3.5 rounded border-border text-primary focus:ring-primary outline-none"
/>
<label htmlFor="current" className="text-xs font-medium text-foreground cursor-pointer">
Ongoing (Currently active)
</label>
</div>
</div>
<div className="pt-4 border-t border-border/50">
<label className="text-xs font-semibold text-foreground">Listing Priority Order</label>
<input
name="order"
type="number"
defaultValue={initialData?.order || 0}
className="w-full mt-2 px-3 py-2 bg-background border border-border/50 rounded-lg outline-none text-xs"
/>
</div>
</div>
<div className="bg-surface border border-border/50 rounded-2xl p-6 space-y-4 card-shadow">
<h3 className="text-sm font-bold uppercase tracking-widest text-foreground-muted flex items-center gap-2">
<TechIcon size={14} />
Skills & Tech
</h3>
<div className="flex gap-2">
<input
value={techInput}
onChange={(e) => setTechInput(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && (e.preventDefault(), addTech())}
placeholder="Add tech..."
className="flex-1 px-3 py-2 bg-background border border-border/50 rounded-lg text-xs outline-none focus:border-primary"
/>
<button
type="button"
onClick={addTech}
className="p-2 bg-primary/10 text-primary rounded-lg hover:bg-primary/20 transition-colors"
>
<Plus size={16} />
</button>
</div>
<div className="flex flex-wrap gap-2 pt-2">
{techStack.map((tech) => (
<span
key={tech}
className="inline-flex items-center gap-1 px-2 py-1 bg-surface-elevated border border-border/50 rounded-md text-[10px] font-bold text-foreground"
>
{tech}
<button
type="button"
onClick={() => removeTech(tech)}
className="p-0.5 hover:text-destructive transition-colors"
>
<X size={10} />
</button>
</span>
))}
</div>
</div>
</div>
</div>
</form>
);
}

View File

@@ -0,0 +1,270 @@
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { createProject, updateProject } from "@/lib/actions/project";
import {
Save,
ChevronLeft,
Briefcase,
Layers,
Link as LinkIcon,
Github,
Award,
Trash2,
Plus,
X,
} from "lucide-react";
import Link from "next/link";
import { cn } from "@/lib/utils";
interface ProjectFormProps {
initialData?: any;
}
export function ProjectForm({ initialData }: ProjectFormProps) {
const router = useRouter();
const [loading, setLoading] = useState(false);
const [techStack, setTechStack] = useState<string[]>(
initialData?.techStack || [],
);
const [techInput, setTechInput] = useState("");
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
setLoading(true);
try {
const formData = new FormData(e.currentTarget);
const data = {
title: formData.get("title") as string,
description: formData.get("description") as string,
imageUrl: formData.get("imageUrl") as string || null,
liveUrl: formData.get("liveUrl") as string || null,
githubUrl: formData.get("githubUrl") as string || null,
techStack: techStack,
featured: formData.get("featured") === "on",
order: parseInt(formData.get("order") as string) || 0,
};
if (initialData) {
await updateProject(initialData.id, data);
} else {
await createProject(data);
}
router.push("/dashboard/projects");
router.refresh();
} catch (error) {
console.error(error);
alert("Something went wrong.");
} finally {
setLoading(false);
}
}
const addTech = () => {
if (techInput && !techStack.includes(techInput)) {
setTechStack([...techStack, techInput]);
setTechInput("");
}
};
const removeTech = (tech: string) => {
setTechStack(techStack.filter((t) => t !== tech));
};
return (
<form onSubmit={handleSubmit} className="space-y-8 max-w-5xl mx-auto pb-20">
<div className="flex items-center justify-between gap-4 border-b border-border pb-6">
<div className="flex items-center gap-4">
<Link
href="/dashboard/projects"
className="p-2 hover:bg-surface-elevated rounded-lg text-foreground-muted transition-colors"
>
<ChevronLeft size={20} />
</Link>
<div>
<h1 className="text-2xl font-display font-bold text-foreground">
{initialData ? "Edit Project" : "New Portfolio Project"}
</h1>
<p className="text-sm text-foreground-muted">
{initialData ? "Refine your project details." : "Add a new achievement to your collection."}
</p>
</div>
</div>
<div className="flex items-center gap-3">
<button
type="button"
onClick={() => router.back()}
className="px-4 py-2 border border-border bg-surface text-foreground font-medium rounded-lg hover:bg-surface-elevated transition-all text-sm"
>
Cancel
</button>
<button
type="submit"
disabled={loading}
className="inline-flex items-center gap-2 px-6 py-2 bg-primary text-primary-foreground font-semibold rounded-lg hover:shadow-lg hover:shadow-primary/20 transition-all disabled:opacity-50 text-sm"
>
<Save size={18} />
{loading ? "Saving..." : initialData ? "Update Project" : "Create Project"}
</button>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
<div className="lg:col-span-2 space-y-6">
<div className="bg-surface border border-border/50 rounded-2xl p-6 space-y-6 card-shadow">
<div className="space-y-4">
<div className="space-y-2">
<label className="text-sm font-semibold text-foreground flex items-center gap-2">
<Briefcase size={16} className="text-primary/60" />
Project Title
</label>
<input
name="title"
defaultValue={initialData?.title}
placeholder="e.g. Finance Hub Pro"
required
className="w-full px-4 py-2.5 bg-background border border-border/50 rounded-xl focus:ring-2 focus:ring-primary/20 focus:border-primary outline-none transition-all text-sm font-medium"
/>
</div>
<div className="space-y-2">
<label className="text-sm font-semibold text-foreground">Detailed Description</label>
<textarea
name="description"
defaultValue={initialData?.description}
placeholder="Describe your project, your role, and the value it provided..."
required
rows={8}
className="w-full px-4 py-3 bg-background border border-border/50 rounded-xl focus:ring-2 focus:ring-primary/20 focus:border-primary outline-none transition-all text-sm leading-relaxed"
/>
</div>
</div>
</div>
<div className="bg-surface border border-border/50 rounded-2xl p-6 space-y-6 card-shadow">
<h3 className="text-sm font-bold uppercase tracking-widest text-foreground-muted">Assets & Visibility</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<label className="text-sm font-semibold text-foreground">Thumbnail Image URL</label>
<input
name="imageUrl"
defaultValue={initialData?.imageUrl}
placeholder="https://..."
className="w-full px-4 py-2 bg-background border border-border/50 rounded-xl focus:ring-2 focus:ring-primary/20 outline-none transition-all text-sm"
/>
</div>
<div className="space-y-2">
<label className="text-sm font-semibold text-foreground">Display Order</label>
<input
name="order"
type="number"
defaultValue={initialData?.order || 0}
className="w-full px-4 py-2 bg-background border border-border/50 rounded-xl focus:ring-2 focus:ring-primary/20 outline-none transition-all text-sm"
/>
</div>
</div>
<div className="flex items-center gap-2 pt-4 border-t border-border/50">
<input
name="featured"
type="checkbox"
id="featured"
defaultChecked={initialData?.featured}
className="w-4 h-4 rounded border-border text-primary focus:ring-primary"
/>
<label htmlFor="featured" className="text-sm font-medium text-foreground flex items-center gap-1.5 cursor-pointer">
<Award size={14} className="text-yellow-500" />
Feature this project on homepage
</label>
</div>
</div>
</div>
<div className="space-y-6">
{/* Links Section */}
<div className="bg-surface border border-border/50 rounded-2xl p-6 space-y-4 card-shadow">
<h3 className="text-sm font-bold uppercase tracking-widest text-foreground-muted">Project Links</h3>
<div className="space-y-4">
<div className="space-y-2">
<label className="text-xs font-semibold text-foreground flex items-center gap-2">
<LinkIcon size={14} />
Live Preview URL
</label>
<input
name="liveUrl"
defaultValue={initialData?.liveUrl}
placeholder="https://example.com"
className="w-full px-3 py-2 bg-background border border-border/50 rounded-lg focus:ring-2 focus:ring-primary/20 outline-none transition-all text-xs"
/>
</div>
<div className="space-y-2">
<label className="text-xs font-semibold text-foreground flex items-center gap-2">
<Github size={14} />
Github Repository
</label>
<input
name="githubUrl"
defaultValue={initialData?.githubUrl}
placeholder="https://github.com/..."
className="w-full px-3 py-2 bg-background border border-border/50 rounded-lg focus:ring-2 focus:ring-primary/20 outline-none transition-all text-xs"
/>
</div>
</div>
</div>
{/* Tech Stack */}
<div className="bg-surface border border-border/50 rounded-2xl p-6 space-y-4 card-shadow">
<h3 className="text-sm font-bold uppercase tracking-widest text-foreground-muted flex items-center gap-2">
<Layers size={14} />
Tech Stack
</h3>
<div className="flex gap-2">
<input
value={techInput}
onChange={(e) => setTechInput(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && (e.preventDefault(), addTech())}
placeholder="Add tech..."
className="flex-1 px-3 py-2 bg-background border border-border/50 rounded-lg text-xs outline-none focus:border-primary"
/>
<button
type="button"
onClick={addTech}
className="p-2 bg-primary/10 text-primary rounded-lg hover:bg-primary/20 transition-colors"
>
<Plus size={16} />
</button>
</div>
<div className="flex flex-wrap gap-2 pt-2">
{techStack.map((tech) => (
<span
key={tech}
className="inline-flex items-center gap-1 px-2 py-1 bg-surface-elevated border border-border/50 rounded-md text-[10px] font-bold text-foreground"
>
{tech}
<button
type="button"
onClick={() => removeTech(tech)}
className="p-0.5 hover:text-destructive transition-colors"
>
<X size={10} />
</button>
</span>
))}
{techStack.length === 0 && (
<p className="text-xs text-foreground-muted italic text-center w-full">No technologies listed.</p>
)}
</div>
</div>
</div>
</div>
</form>
);
}