159 lines
5.9 KiB
TypeScript
159 lines
5.9 KiB
TypeScript
"use client"
|
|
|
|
import { useState } from "react"
|
|
import { AdminShell } from "@/components/admin/admin-shell"
|
|
import { StackForm } from "@/components/admin/stack-form"
|
|
import { useDataStore } from "@/lib/data-store"
|
|
import type { StackCategory } from "@/lib/types"
|
|
import { Button } from "@/components/ui/button"
|
|
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"
|
|
import {
|
|
AlertDialog,
|
|
AlertDialogAction,
|
|
AlertDialogCancel,
|
|
AlertDialogContent,
|
|
AlertDialogDescription,
|
|
AlertDialogFooter,
|
|
AlertDialogHeader,
|
|
AlertDialogTitle,
|
|
} from "@/components/ui/alert-dialog"
|
|
import { Plus, Pencil, Trash2, Terminal, Layers } from "lucide-react"
|
|
|
|
export default function StackPage() {
|
|
const { stackCategories, addStackCategory, updateStackCategory, deleteStackCategory } = useDataStore()
|
|
const [isFormOpen, setIsFormOpen] = useState(false)
|
|
const [editingCategory, setEditingCategory] = useState<StackCategory | null>(null)
|
|
const [deletingCategory, setDeletingCategory] = useState<StackCategory | null>(null)
|
|
|
|
const handleCreate = () => {
|
|
setEditingCategory(null)
|
|
setIsFormOpen(true)
|
|
}
|
|
|
|
const handleEdit = (category: StackCategory) => {
|
|
setEditingCategory(category)
|
|
setIsFormOpen(true)
|
|
}
|
|
|
|
const handleSubmit = (data: Omit<StackCategory, "id">) => {
|
|
if (editingCategory) {
|
|
updateStackCategory(editingCategory.id, data)
|
|
} else {
|
|
addStackCategory(data)
|
|
}
|
|
setIsFormOpen(false)
|
|
setEditingCategory(null)
|
|
}
|
|
|
|
const handleDelete = () => {
|
|
if (deletingCategory) {
|
|
deleteStackCategory(deletingCategory.id)
|
|
setDeletingCategory(null)
|
|
}
|
|
}
|
|
|
|
const totalItems = stackCategories.reduce((acc, cat) => acc + cat.items.length, 0)
|
|
|
|
return (
|
|
<AdminShell>
|
|
<div className="p-8 space-y-8">
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between">
|
|
<div className="space-y-2">
|
|
<h1 className="text-2xl font-semibold text-foreground">Tech Stack</h1>
|
|
<p className="text-sm text-muted-foreground font-mono">
|
|
<Terminal size={12} className="inline mr-1" />$ atticl stack --list ({stackCategories.length} categories,{" "}
|
|
{totalItems} items)
|
|
</p>
|
|
</div>
|
|
<Button onClick={handleCreate} className="gap-2">
|
|
<Plus size={16} />
|
|
Add Category
|
|
</Button>
|
|
</div>
|
|
|
|
{/* Categories grid */}
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
{stackCategories.map((category) => (
|
|
<div
|
|
key={category.id}
|
|
className="bg-card border border-border rounded-lg p-5 hover:border-primary/30 transition-colors"
|
|
>
|
|
<div className="flex items-start justify-between mb-4">
|
|
<div className="flex items-center gap-2">
|
|
<Layers size={16} className="text-primary" />
|
|
<h3 className="font-mono text-sm text-primary uppercase tracking-wider">{category.title}</h3>
|
|
</div>
|
|
<div className="flex items-center gap-1">
|
|
<Button variant="ghost" size="icon" className="h-8 w-8" onClick={() => handleEdit(category)}>
|
|
<Pencil size={14} />
|
|
</Button>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="h-8 w-8 hover:text-destructive"
|
|
onClick={() => setDeletingCategory(category)}
|
|
>
|
|
<Trash2 size={14} />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
<ul className="space-y-1.5">
|
|
{category.items.map((item) => (
|
|
<li key={item} className="text-sm text-muted-foreground font-mono flex items-center gap-2">
|
|
<span className="w-1 h-1 bg-muted-foreground rounded-full" />
|
|
{item}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
|
|
<p className="mt-4 text-xs text-muted-foreground">
|
|
{category.items.length} {category.items.length === 1 ? "item" : "items"}
|
|
</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
{stackCategories.length === 0 && (
|
|
<div className="text-center py-12 text-muted-foreground">
|
|
<p className="font-mono">No tech stack categories yet.</p>
|
|
<Button onClick={handleCreate} variant="link" className="mt-2">
|
|
Add your first category
|
|
</Button>
|
|
</div>
|
|
)}
|
|
|
|
{/* Form dialog */}
|
|
<Dialog open={isFormOpen} onOpenChange={setIsFormOpen}>
|
|
<DialogContent className="max-w-md">
|
|
<DialogHeader>
|
|
<DialogTitle className="font-mono">{editingCategory ? "Edit Category" : "New Category"}</DialogTitle>
|
|
</DialogHeader>
|
|
<StackForm category={editingCategory} onSubmit={handleSubmit} onCancel={() => setIsFormOpen(false)} />
|
|
</DialogContent>
|
|
</Dialog>
|
|
|
|
{/* Delete confirmation */}
|
|
<AlertDialog open={!!deletingCategory} onOpenChange={() => setDeletingCategory(null)}>
|
|
<AlertDialogContent>
|
|
<AlertDialogHeader>
|
|
<AlertDialogTitle>Delete Category</AlertDialogTitle>
|
|
<AlertDialogDescription>
|
|
Are you sure you want to delete the "{deletingCategory?.title}" category? This will remove all{" "}
|
|
{deletingCategory?.items.length} technologies in it.
|
|
</AlertDialogDescription>
|
|
</AlertDialogHeader>
|
|
<AlertDialogFooter>
|
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
|
<AlertDialogAction onClick={handleDelete} className="bg-destructive text-destructive-foreground">
|
|
Delete
|
|
</AlertDialogAction>
|
|
</AlertDialogFooter>
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
</div>
|
|
</AdminShell>
|
|
)
|
|
}
|