project init
This commit is contained in:
18
frontend/app/admin/layout.tsx
Normal file
18
frontend/app/admin/layout.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
"use client"
|
||||
|
||||
import type React from "react"
|
||||
|
||||
import { AuthProvider } from "@/lib/auth-context"
|
||||
import { DataStoreProvider } from "@/lib/data-store"
|
||||
|
||||
export default function AdminLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<AuthProvider>
|
||||
<DataStoreProvider>{children}</DataStoreProvider>
|
||||
</AuthProvider>
|
||||
)
|
||||
}
|
||||
109
frontend/app/admin/login/page.tsx
Normal file
109
frontend/app/admin/login/page.tsx
Normal file
@@ -0,0 +1,109 @@
|
||||
"use client"
|
||||
|
||||
import type React from "react"
|
||||
|
||||
import { useState } from "react"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { useAuth } from "@/lib/auth-context"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Terminal, AlertCircle, Loader2 } from "lucide-react"
|
||||
|
||||
export default function LoginPage() {
|
||||
const [email, setEmail] = useState("")
|
||||
const [password, setPassword] = useState("")
|
||||
const [error, setError] = useState("")
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
const { login } = useAuth()
|
||||
const router = useRouter()
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
setError("")
|
||||
setIsSubmitting(true)
|
||||
|
||||
const success = await login(email, password)
|
||||
|
||||
if (success) {
|
||||
router.push("/admin")
|
||||
} else {
|
||||
setError("Invalid credentials")
|
||||
}
|
||||
setIsSubmitting(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background flex items-center justify-center p-6">
|
||||
<div className="w-full max-w-sm space-y-8">
|
||||
{/* Header */}
|
||||
<div className="text-center space-y-2">
|
||||
<div className="flex items-center justify-center gap-2 text-primary">
|
||||
<Terminal size={24} />
|
||||
<span className="font-mono text-lg font-semibold">atticl</span>
|
||||
</div>
|
||||
<h1 className="text-2xl font-semibold text-foreground">Admin Login</h1>
|
||||
<p className="text-sm text-muted-foreground font-mono">$ authenticate --session</p>
|
||||
</div>
|
||||
|
||||
{/* Login form */}
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
<div className="bg-card border border-border rounded-lg p-6 space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email" className="text-sm font-mono text-muted-foreground">
|
||||
Email
|
||||
</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder="admin@atticl.com"
|
||||
className="font-mono"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="password" className="text-sm font-mono text-muted-foreground">
|
||||
Password
|
||||
</Label>
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder="••••••••"
|
||||
className="font-mono"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="flex items-center gap-2 text-destructive text-sm">
|
||||
<AlertCircle size={14} />
|
||||
<span className="font-mono">{error}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Button type="submit" className="w-full font-mono" disabled={isSubmitting}>
|
||||
{isSubmitting ? (
|
||||
<>
|
||||
<Loader2 size={16} className="animate-spin mr-2" />
|
||||
Authenticating...
|
||||
</>
|
||||
) : (
|
||||
"Login"
|
||||
)}
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
{/* Dev hint */}
|
||||
<div className="text-center">
|
||||
<p className="text-xs text-muted-foreground font-mono">Demo: admin@atticl.com / admin123</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
115
frontend/app/admin/page.tsx
Normal file
115
frontend/app/admin/page.tsx
Normal file
@@ -0,0 +1,115 @@
|
||||
"use client"
|
||||
|
||||
import { AdminShell } from "@/components/admin/admin-shell"
|
||||
import { useDataStore } from "@/lib/data-store"
|
||||
import { FolderKanban, Layers, Terminal } from "lucide-react"
|
||||
import Link from "next/link"
|
||||
|
||||
export default function AdminDashboard() {
|
||||
const { projects, stackCategories } = useDataStore()
|
||||
|
||||
const stats = [
|
||||
{
|
||||
label: "Total Projects",
|
||||
value: projects.length,
|
||||
icon: FolderKanban,
|
||||
href: "/admin/projects",
|
||||
},
|
||||
{
|
||||
label: "Featured Projects",
|
||||
value: projects.filter((p) => p.featured).length,
|
||||
icon: FolderKanban,
|
||||
href: "/admin/projects",
|
||||
},
|
||||
{
|
||||
label: "Stack Categories",
|
||||
value: stackCategories.length,
|
||||
icon: Layers,
|
||||
href: "/admin/stack",
|
||||
},
|
||||
{
|
||||
label: "Total Tech Items",
|
||||
value: stackCategories.reduce((acc, cat) => acc + cat.items.length, 0),
|
||||
icon: Layers,
|
||||
href: "/admin/stack",
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<AdminShell>
|
||||
<div className="p-8 space-y-8">
|
||||
{/* Header */}
|
||||
<div className="space-y-2">
|
||||
<h1 className="text-2xl font-semibold text-foreground">Dashboard</h1>
|
||||
<p className="text-sm text-muted-foreground font-mono">
|
||||
<Terminal size={12} className="inline mr-1" />$ atticl admin --status
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Stats grid */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{stats.map((stat) => (
|
||||
<Link
|
||||
key={stat.label}
|
||||
href={stat.href}
|
||||
className="bg-card border border-border rounded-lg p-6 hover:border-primary/50 transition-colors group"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<stat.icon size={20} className="text-muted-foreground group-hover:text-primary transition-colors" />
|
||||
<span className="text-3xl font-semibold text-foreground">{stat.value}</span>
|
||||
</div>
|
||||
<p className="mt-2 text-sm font-mono text-muted-foreground">{stat.label}</p>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Quick actions */}
|
||||
<div className="space-y-4">
|
||||
<h2 className="font-mono text-sm text-muted-foreground uppercase tracking-wider">Quick Actions</h2>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<Link
|
||||
href="/admin/projects"
|
||||
className="bg-card border border-border rounded-lg p-4 hover:border-primary/50 transition-colors flex items-center gap-4"
|
||||
>
|
||||
<div className="w-10 h-10 bg-primary/10 rounded-md flex items-center justify-center">
|
||||
<FolderKanban size={18} className="text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-foreground">Manage Projects</p>
|
||||
<p className="text-sm text-muted-foreground">Add, edit, or remove projects</p>
|
||||
</div>
|
||||
</Link>
|
||||
<Link
|
||||
href="/admin/stack"
|
||||
className="bg-card border border-border rounded-lg p-4 hover:border-primary/50 transition-colors flex items-center gap-4"
|
||||
>
|
||||
<div className="w-10 h-10 bg-primary/10 rounded-md flex items-center justify-center">
|
||||
<Layers size={18} className="text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-foreground">Manage Tech Stack</p>
|
||||
<p className="text-sm text-muted-foreground">Update your technology categories</p>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* API placeholder */}
|
||||
<div className="bg-card border border-dashed border-border rounded-lg p-6">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="w-10 h-10 bg-secondary rounded-md flex items-center justify-center flex-shrink-0">
|
||||
<Terminal size={18} className="text-muted-foreground" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<p className="font-medium text-foreground">API Integration</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Currently using mock data. Replace the data store functions with your API calls when ready.
|
||||
</p>
|
||||
<code className="text-xs font-mono text-primary bg-secondary px-2 py-1 rounded">lib/data-store.tsx</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AdminShell>
|
||||
)
|
||||
}
|
||||
115
frontend/app/admin/projects/page.tsx
Normal file
115
frontend/app/admin/projects/page.tsx
Normal file
@@ -0,0 +1,115 @@
|
||||
"use client"
|
||||
|
||||
import { AdminShell } from "@/components/admin/admin-shell"
|
||||
import { useDataStore } from "@/lib/data-store"
|
||||
import { FolderKanban, Layers, Terminal } from "lucide-react"
|
||||
import Link from "next/link"
|
||||
|
||||
export default function AdminDashboard() {
|
||||
const { projects, stackCategories } = useDataStore()
|
||||
|
||||
const stats = [
|
||||
{
|
||||
label: "Total Projects",
|
||||
value: projects.length,
|
||||
icon: FolderKanban,
|
||||
href: "/admin/projects",
|
||||
},
|
||||
{
|
||||
label: "Featured Projects",
|
||||
value: projects.filter((p) => p.featured).length,
|
||||
icon: FolderKanban,
|
||||
href: "/admin/projects",
|
||||
},
|
||||
{
|
||||
label: "Stack Categories",
|
||||
value: stackCategories.length,
|
||||
icon: Layers,
|
||||
href: "/admin/stack",
|
||||
},
|
||||
{
|
||||
label: "Total Tech Items",
|
||||
value: stackCategories.reduce((acc, cat) => acc + cat.items.length, 0),
|
||||
icon: Layers,
|
||||
href: "/admin/stack",
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<AdminShell>
|
||||
<div className="p-8 space-y-8">
|
||||
{/* Header */}
|
||||
<div className="space-y-2">
|
||||
<h1 className="text-2xl font-semibold text-foreground">Dashboard</h1>
|
||||
<p className="text-sm text-muted-foreground font-mono">
|
||||
<Terminal size={12} className="inline mr-1" />$ atticl admin --status
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Stats grid */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{stats.map((stat) => (
|
||||
<Link
|
||||
key={stat.label}
|
||||
href={stat.href}
|
||||
className="bg-card border border-border rounded-lg p-6 hover:border-primary/50 transition-colors group"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<stat.icon size={20} className="text-muted-foreground group-hover:text-primary transition-colors" />
|
||||
<span className="text-3xl font-semibold text-foreground">{stat.value}</span>
|
||||
</div>
|
||||
<p className="mt-2 text-sm font-mono text-muted-foreground">{stat.label}</p>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Quick actions */}
|
||||
<div className="space-y-4">
|
||||
<h2 className="font-mono text-sm text-muted-foreground uppercase tracking-wider">Quick Actions</h2>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<Link
|
||||
href="/admin/projects"
|
||||
className="bg-card border border-border rounded-lg p-4 hover:border-primary/50 transition-colors flex items-center gap-4"
|
||||
>
|
||||
<div className="w-10 h-10 bg-primary/10 rounded-md flex items-center justify-center">
|
||||
<FolderKanban size={18} className="text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-foreground">Manage Projects</p>
|
||||
<p className="text-sm text-muted-foreground">Add, edit, or remove projects</p>
|
||||
</div>
|
||||
</Link>
|
||||
<Link
|
||||
href="/admin/stack"
|
||||
className="bg-card border border-border rounded-lg p-4 hover:border-primary/50 transition-colors flex items-center gap-4"
|
||||
>
|
||||
<div className="w-10 h-10 bg-primary/10 rounded-md flex items-center justify-center">
|
||||
<Layers size={18} className="text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-foreground">Manage Tech Stack</p>
|
||||
<p className="text-sm text-muted-foreground">Update your technology categories</p>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* API placeholder */}
|
||||
<div className="bg-card border border-dashed border-border rounded-lg p-6">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="w-10 h-10 bg-secondary rounded-md flex items-center justify-center flex-shrink-0">
|
||||
<Terminal size={18} className="text-muted-foreground" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<p className="font-medium text-foreground">API Integration</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Currently using mock data. Replace the data store functions with your API calls when ready.
|
||||
</p>
|
||||
<code className="text-xs font-mono text-primary bg-secondary px-2 py-1 rounded">lib/data-store.tsx</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AdminShell>
|
||||
)
|
||||
}
|
||||
158
frontend/app/admin/stack/page.tsx
Normal file
158
frontend/app/admin/stack/page.tsx
Normal file
@@ -0,0 +1,158 @@
|
||||
"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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user