From 78a0f6c64608dad3ec9f7966d0581cff25a3bde6 Mon Sep 17 00:00:00 2001 From: atticl Date: Mon, 1 Dec 2025 02:29:08 +0100 Subject: [PATCH] project init --- backend/.gitignore | 2 + backend/main.py | 122 + backend/requirements.txt | 9 + frontend/.gitignore | 2 + frontend/app/admin/layout.tsx | 18 + frontend/app/admin/login/page.tsx | 109 + frontend/app/admin/page.tsx | 115 + frontend/app/admin/projects/page.tsx | 115 + frontend/app/admin/stack/page.tsx | 158 + frontend/app/globals.css | 130 + frontend/app/layout.tsx | 55 + frontend/app/page.tsx | 25 + frontend/components.json | 21 + frontend/components/about.tsx | 63 + frontend/components/admin/admin-shell.tsx | 39 + frontend/components/admin/admin-sidebar.tsx | 74 + frontend/components/admin/project-form.tsx | 257 ++ frontend/components/admin/stack-form.tsx | 109 + frontend/components/contact.tsx | 80 + frontend/components/footer.tsx | 13 + frontend/components/header.tsx | 71 + frontend/components/hero.tsx | 66 + frontend/components/philosophy.tsx | 72 + frontend/components/projects.tsx | 128 + frontend/components/techstack.tsx | 72 + frontend/components/ui/accordion.tsx | 66 + frontend/components/ui/alert-dialog.tsx | 157 + frontend/components/ui/alert.tsx | 66 + frontend/components/ui/aspect-ratio.tsx | 11 + frontend/components/ui/avatar.tsx | 53 + frontend/components/ui/badge.tsx | 46 + frontend/components/ui/breadcrumb.tsx | 109 + frontend/components/ui/button-group.tsx | 83 + frontend/components/ui/button.tsx | 60 + frontend/components/ui/calendar.tsx | 213 + frontend/components/ui/card.tsx | 92 + frontend/components/ui/carousel.tsx | 241 ++ frontend/components/ui/chart.tsx | 353 ++ frontend/components/ui/checkbox.tsx | 32 + frontend/components/ui/collapsible.tsx | 33 + frontend/components/ui/command.tsx | 184 + frontend/components/ui/context-menu.tsx | 252 ++ frontend/components/ui/dialog.tsx | 143 + frontend/components/ui/drawer.tsx | 135 + frontend/components/ui/dropdown-menu.tsx | 257 ++ frontend/components/ui/empty.tsx | 104 + frontend/components/ui/field.tsx | 244 ++ frontend/components/ui/form.tsx | 167 + frontend/components/ui/hover-card.tsx | 44 + frontend/components/ui/input-group.tsx | 169 + frontend/components/ui/input-otp.tsx | 77 + frontend/components/ui/input.tsx | 21 + frontend/components/ui/item.tsx | 193 + frontend/components/ui/kbd.tsx | 28 + frontend/components/ui/label.tsx | 24 + frontend/components/ui/menubar.tsx | 276 ++ frontend/components/ui/navigation-menu.tsx | 166 + frontend/components/ui/pagination.tsx | 127 + frontend/components/ui/popover.tsx | 48 + frontend/components/ui/progress.tsx | 31 + frontend/components/ui/radio-group.tsx | 45 + frontend/components/ui/resizable.tsx | 56 + frontend/components/ui/scroll-area.tsx | 58 + frontend/components/ui/select.tsx | 185 + frontend/components/ui/separator.tsx | 28 + frontend/components/ui/sheet.tsx | 139 + frontend/components/ui/sidebar.tsx | 726 ++++ frontend/components/ui/skeleton.tsx | 13 + frontend/components/ui/slider.tsx | 63 + frontend/components/ui/sonner.tsx | 25 + frontend/components/ui/spinner.tsx | 16 + frontend/components/ui/switch.tsx | 31 + frontend/components/ui/table.tsx | 116 + frontend/components/ui/tabs.tsx | 66 + frontend/components/ui/textarea.tsx | 18 + frontend/components/ui/toast.tsx | 129 + frontend/components/ui/toaster.tsx | 35 + frontend/components/ui/toggle-group.tsx | 73 + frontend/components/ui/toggle.tsx | 47 + frontend/components/ui/tooltip.tsx | 61 + frontend/components/ui/use-mobile.tsx | 19 + frontend/components/ui/use-toast.ts | 191 + frontend/hooks/use-mobile.ts | 19 + frontend/hooks/use-toast.ts | 191 + frontend/lib/auth-context.tsx | 62 + frontend/lib/data-store.tsx | 76 + frontend/lib/mock-data.ts | 91 + frontend/lib/types.ts | 23 + frontend/lib/utils.ts | 6 + frontend/next-env.d.ts | 6 + frontend/next.config.mjs | 11 + frontend/package-lock.json | 3997 +++++++++++++++++++ frontend/package.json | 73 + frontend/pnpm-lock.yaml | 5 + frontend/postcss.config.mjs | 8 + frontend/public/apple-icon.png | Bin 0 -> 156303 bytes frontend/public/icon-dark-32x32.png | Bin 0 -> 731446 bytes frontend/public/icon-light-32x32.png | Bin 0 -> 731446 bytes frontend/public/icon.svg | 26 + frontend/public/placeholder-logo.png | Bin 0 -> 568 bytes frontend/public/placeholder-logo.svg | 1 + frontend/public/placeholder-user.jpg | Bin 0 -> 1635 bytes frontend/public/placeholder.jpg | Bin 0 -> 1064 bytes frontend/public/placeholder.svg | 1 + frontend/styles/globals.css | 125 + frontend/tsconfig.json | 41 + 106 files changed, 13132 insertions(+) create mode 100644 backend/.gitignore create mode 100644 backend/main.py create mode 100644 backend/requirements.txt create mode 100644 frontend/.gitignore create mode 100644 frontend/app/admin/layout.tsx create mode 100644 frontend/app/admin/login/page.tsx create mode 100644 frontend/app/admin/page.tsx create mode 100644 frontend/app/admin/projects/page.tsx create mode 100644 frontend/app/admin/stack/page.tsx create mode 100644 frontend/app/globals.css create mode 100644 frontend/app/layout.tsx create mode 100644 frontend/app/page.tsx create mode 100644 frontend/components.json create mode 100644 frontend/components/about.tsx create mode 100644 frontend/components/admin/admin-shell.tsx create mode 100644 frontend/components/admin/admin-sidebar.tsx create mode 100644 frontend/components/admin/project-form.tsx create mode 100644 frontend/components/admin/stack-form.tsx create mode 100644 frontend/components/contact.tsx create mode 100644 frontend/components/footer.tsx create mode 100644 frontend/components/header.tsx create mode 100644 frontend/components/hero.tsx create mode 100644 frontend/components/philosophy.tsx create mode 100644 frontend/components/projects.tsx create mode 100644 frontend/components/techstack.tsx create mode 100644 frontend/components/ui/accordion.tsx create mode 100644 frontend/components/ui/alert-dialog.tsx create mode 100644 frontend/components/ui/alert.tsx create mode 100644 frontend/components/ui/aspect-ratio.tsx create mode 100644 frontend/components/ui/avatar.tsx create mode 100644 frontend/components/ui/badge.tsx create mode 100644 frontend/components/ui/breadcrumb.tsx create mode 100644 frontend/components/ui/button-group.tsx create mode 100644 frontend/components/ui/button.tsx create mode 100644 frontend/components/ui/calendar.tsx create mode 100644 frontend/components/ui/card.tsx create mode 100644 frontend/components/ui/carousel.tsx create mode 100644 frontend/components/ui/chart.tsx create mode 100644 frontend/components/ui/checkbox.tsx create mode 100644 frontend/components/ui/collapsible.tsx create mode 100644 frontend/components/ui/command.tsx create mode 100644 frontend/components/ui/context-menu.tsx create mode 100644 frontend/components/ui/dialog.tsx create mode 100644 frontend/components/ui/drawer.tsx create mode 100644 frontend/components/ui/dropdown-menu.tsx create mode 100644 frontend/components/ui/empty.tsx create mode 100644 frontend/components/ui/field.tsx create mode 100644 frontend/components/ui/form.tsx create mode 100644 frontend/components/ui/hover-card.tsx create mode 100644 frontend/components/ui/input-group.tsx create mode 100644 frontend/components/ui/input-otp.tsx create mode 100644 frontend/components/ui/input.tsx create mode 100644 frontend/components/ui/item.tsx create mode 100644 frontend/components/ui/kbd.tsx create mode 100644 frontend/components/ui/label.tsx create mode 100644 frontend/components/ui/menubar.tsx create mode 100644 frontend/components/ui/navigation-menu.tsx create mode 100644 frontend/components/ui/pagination.tsx create mode 100644 frontend/components/ui/popover.tsx create mode 100644 frontend/components/ui/progress.tsx create mode 100644 frontend/components/ui/radio-group.tsx create mode 100644 frontend/components/ui/resizable.tsx create mode 100644 frontend/components/ui/scroll-area.tsx create mode 100644 frontend/components/ui/select.tsx create mode 100644 frontend/components/ui/separator.tsx create mode 100644 frontend/components/ui/sheet.tsx create mode 100644 frontend/components/ui/sidebar.tsx create mode 100644 frontend/components/ui/skeleton.tsx create mode 100644 frontend/components/ui/slider.tsx create mode 100644 frontend/components/ui/sonner.tsx create mode 100644 frontend/components/ui/spinner.tsx create mode 100644 frontend/components/ui/switch.tsx create mode 100644 frontend/components/ui/table.tsx create mode 100644 frontend/components/ui/tabs.tsx create mode 100644 frontend/components/ui/textarea.tsx create mode 100644 frontend/components/ui/toast.tsx create mode 100644 frontend/components/ui/toaster.tsx create mode 100644 frontend/components/ui/toggle-group.tsx create mode 100644 frontend/components/ui/toggle.tsx create mode 100644 frontend/components/ui/tooltip.tsx create mode 100644 frontend/components/ui/use-mobile.tsx create mode 100644 frontend/components/ui/use-toast.ts create mode 100644 frontend/hooks/use-mobile.ts create mode 100644 frontend/hooks/use-toast.ts create mode 100644 frontend/lib/auth-context.tsx create mode 100644 frontend/lib/data-store.tsx create mode 100644 frontend/lib/mock-data.ts create mode 100644 frontend/lib/types.ts create mode 100644 frontend/lib/utils.ts create mode 100644 frontend/next-env.d.ts create mode 100644 frontend/next.config.mjs create mode 100644 frontend/package-lock.json create mode 100644 frontend/package.json create mode 100644 frontend/pnpm-lock.yaml create mode 100644 frontend/postcss.config.mjs create mode 100644 frontend/public/apple-icon.png create mode 100644 frontend/public/icon-dark-32x32.png create mode 100644 frontend/public/icon-light-32x32.png create mode 100644 frontend/public/icon.svg create mode 100644 frontend/public/placeholder-logo.png create mode 100644 frontend/public/placeholder-logo.svg create mode 100644 frontend/public/placeholder-user.jpg create mode 100644 frontend/public/placeholder.jpg create mode 100644 frontend/public/placeholder.svg create mode 100644 frontend/styles/globals.css create mode 100644 frontend/tsconfig.json diff --git a/backend/.gitignore b/backend/.gitignore new file mode 100644 index 0000000..bec91e8 --- /dev/null +++ b/backend/.gitignore @@ -0,0 +1,2 @@ +__pycache__/* +.venv/* \ No newline at end of file diff --git a/backend/main.py b/backend/main.py new file mode 100644 index 0000000..4037a06 --- /dev/null +++ b/backend/main.py @@ -0,0 +1,122 @@ +from fastapi import FastAPI +from pydantic import BaseModel, EmailStr +from typing import List, Optional +from contextlib import asynccontextmanager +import sqlalchemy +from sqlalchemy import Column, String, Boolean, Text +from sqlalchemy.ext.declarative import declarative_base +import databases + +DATABASE_URL = "mysql+pymysql://username:password@localhost:3306/mydatabase" + +database = databases.Database(DATABASE_URL) +metadata = sqlalchemy.MetaData() +Base = declarative_base() + +# -------------------- SQLAlchemy Models -------------------- +class ProjectModel(Base): + __tablename__ = "projects" + id = Column(String(36), primary_key=True) + name = Column(String(255), nullable=False) + role = Column(String(255), nullable=False) + description = Column(Text, nullable=False) + highlights = Column(Text) # store as JSON string + stack = Column(Text) # store as JSON string + website = Column(String(255), nullable=True) + github = Column(String(255), nullable=True) + featured = Column(Boolean, default=False) + +class StackCategoryModel(Base): + __tablename__ = "stack_categories" + id = Column(String(36), primary_key=True) + title = Column(String(255), nullable=False) + items = Column(Text) # JSON string + +class UserModel(Base): + __tablename__ = "users" + id = Column(String(36), primary_key=True) + email = Column(String(255), nullable=False, unique=True) + name = Column(String(255), nullable=False) + +# -------------------- Pydantic Schemas -------------------- + +class Project(BaseModel): + id: Optional[str] + name: str + role: str + description: str + highlights: List[str] + stack: List[str] + website: Optional[str] = None + github: Optional[str] = None + featured: bool + +class StackCategory(BaseModel): + id: Optional[str] + title: str + items: List[str] + +class User(BaseModel): + id: Optional[str] + email: EmailStr + name: str + + +# -------------------- FastAPI App -------------------- + +@asynccontextmanager +async def lifespan(app: FastAPI): + await database.connect() + yield + await database.disconnect() + +app = FastAPI(title="atticl.com API", version="1.0", docs_url=None, redoc_url=None, openapi_url=None, lifespan=lifespan) + +# -------------------- Helper Functions -------------------- + +import json +import uuid + +def to_json_str(lst: List[str]) -> str: + return json.dumps(lst) + +def from_json_str(s: str) -> List[str]: + return json.loads(s) + +# -------------------- Project Endpoints -------------------- + +@app.post("/projects/", response_model=Project) +async def create_project(project: Project): + project.id = str(uuid.uuid4()) + query = ProjectModel.__table__.insert().values( + id=project.id, + name=project.name, + role=project.role, + description=project.description, + highlights=to_json_str(project.highlights), + stack=to_json_str(project.stack), + website=project.website, + github=project.github, + featured=project.featured + ) + await database.execute(query) + return project + +@app.get("/projects/", response_model=List[Project]) +async def list_projects(): + query = ProjectModel.__table__.select() + rows = await database.fetch_all(query) + result = [] + for row in rows: + result.append(Project( + id=row["id"], + name=row["name"], + role=row["role"], + description=row["description"], + highlights=from_json_str(row["highlights"]), + stack=from_json_str(row["stack"]), + website=row["website"], + github=row["github"], + featured=row["featured"] + )) + return result \ No newline at end of file diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..39fc6fd --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,9 @@ +fastapi +uvicorn[standard] +SQLAlchemy +databases +pymysql +pydantic +pydantic[email] +python-dotenv +aiomysql \ No newline at end of file diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..346ee43 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,2 @@ +.next/* +node_modules/* \ No newline at end of file diff --git a/frontend/app/admin/layout.tsx b/frontend/app/admin/layout.tsx new file mode 100644 index 0000000..012df4c --- /dev/null +++ b/frontend/app/admin/layout.tsx @@ -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 ( + + {children} + + ) +} diff --git a/frontend/app/admin/login/page.tsx b/frontend/app/admin/login/page.tsx new file mode 100644 index 0000000..8a0f5ac --- /dev/null +++ b/frontend/app/admin/login/page.tsx @@ -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 ( +
+
+ {/* Header */} +
+
+ + atticl +
+

Admin Login

+

$ authenticate --session

+
+ + {/* Login form */} +
+
+
+ + setEmail(e.target.value)} + placeholder="admin@atticl.com" + className="font-mono" + required + /> +
+ +
+ + setPassword(e.target.value)} + placeholder="••••••••" + className="font-mono" + required + /> +
+ + {error && ( +
+ + {error} +
+ )} +
+ + +
+ + {/* Dev hint */} +
+

Demo: admin@atticl.com / admin123

+
+
+
+ ) +} diff --git a/frontend/app/admin/page.tsx b/frontend/app/admin/page.tsx new file mode 100644 index 0000000..16d2456 --- /dev/null +++ b/frontend/app/admin/page.tsx @@ -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 ( + +
+ {/* Header */} +
+

Dashboard

+

+ $ atticl admin --status +

+
+ + {/* Stats grid */} +
+ {stats.map((stat) => ( + +
+ + {stat.value} +
+

{stat.label}

+ + ))} +
+ + {/* Quick actions */} +
+

Quick Actions

+
+ +
+ +
+
+

Manage Projects

+

Add, edit, or remove projects

+
+ + +
+ +
+
+

Manage Tech Stack

+

Update your technology categories

+
+ +
+
+ + {/* API placeholder */} +
+
+
+ +
+
+

API Integration

+

+ Currently using mock data. Replace the data store functions with your API calls when ready. +

+ lib/data-store.tsx +
+
+
+
+
+ ) +} diff --git a/frontend/app/admin/projects/page.tsx b/frontend/app/admin/projects/page.tsx new file mode 100644 index 0000000..16d2456 --- /dev/null +++ b/frontend/app/admin/projects/page.tsx @@ -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 ( + +
+ {/* Header */} +
+

Dashboard

+

+ $ atticl admin --status +

+
+ + {/* Stats grid */} +
+ {stats.map((stat) => ( + +
+ + {stat.value} +
+

{stat.label}

+ + ))} +
+ + {/* Quick actions */} +
+

Quick Actions

+
+ +
+ +
+
+

Manage Projects

+

Add, edit, or remove projects

+
+ + +
+ +
+
+

Manage Tech Stack

+

Update your technology categories

+
+ +
+
+ + {/* API placeholder */} +
+
+
+ +
+
+

API Integration

+

+ Currently using mock data. Replace the data store functions with your API calls when ready. +

+ lib/data-store.tsx +
+
+
+
+
+ ) +} diff --git a/frontend/app/admin/stack/page.tsx b/frontend/app/admin/stack/page.tsx new file mode 100644 index 0000000..0825b82 --- /dev/null +++ b/frontend/app/admin/stack/page.tsx @@ -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(null) + const [deletingCategory, setDeletingCategory] = useState(null) + + const handleCreate = () => { + setEditingCategory(null) + setIsFormOpen(true) + } + + const handleEdit = (category: StackCategory) => { + setEditingCategory(category) + setIsFormOpen(true) + } + + const handleSubmit = (data: Omit) => { + 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 ( + +
+ {/* Header */} +
+
+

Tech Stack

+

+ $ atticl stack --list ({stackCategories.length} categories,{" "} + {totalItems} items) +

+
+ +
+ + {/* Categories grid */} +
+ {stackCategories.map((category) => ( +
+
+
+ +

{category.title}

+
+
+ + +
+
+ +
    + {category.items.map((item) => ( +
  • + + {item} +
  • + ))} +
+ +

+ {category.items.length} {category.items.length === 1 ? "item" : "items"} +

+
+ ))} +
+ + {stackCategories.length === 0 && ( +
+

No tech stack categories yet.

+ +
+ )} + + {/* Form dialog */} + + + + {editingCategory ? "Edit Category" : "New Category"} + + setIsFormOpen(false)} /> + + + + {/* Delete confirmation */} + setDeletingCategory(null)}> + + + Delete Category + + Are you sure you want to delete the "{deletingCategory?.title}" category? This will remove all{" "} + {deletingCategory?.items.length} technologies in it. + + + + Cancel + + Delete + + + + +
+
+ ) +} diff --git a/frontend/app/globals.css b/frontend/app/globals.css new file mode 100644 index 0000000..71a334c --- /dev/null +++ b/frontend/app/globals.css @@ -0,0 +1,130 @@ +@import "tailwindcss"; +@import "tw-animate-css"; + +@custom-variant dark (&:is(.dark *)); + +:root { + --background: oklch(1 0 0); + --foreground: oklch(0.145 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0.145 0 0); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.145 0 0); + --primary: oklch(0.205 0 0); + --primary-foreground: oklch(0.985 0 0); + --secondary: oklch(0.97 0 0); + --secondary-foreground: oklch(0.205 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --accent: oklch(0.97 0 0); + --accent-foreground: oklch(0.205 0 0); + --destructive: oklch(0.577 0.245 27.325); + --destructive-foreground: oklch(0.577 0.245 27.325); + --border: oklch(0.922 0 0); + --input: oklch(0.922 0 0); + --ring: oklch(0.708 0 0); + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --radius: 0.625rem; + --sidebar: oklch(0.985 0 0); + --sidebar-foreground: oklch(0.145 0 0); + --sidebar-primary: oklch(0.205 0 0); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.97 0 0); + --sidebar-accent-foreground: oklch(0.205 0 0); + --sidebar-border: oklch(0.922 0 0); + --sidebar-ring: oklch(0.708 0 0); +} + +/* Updated to black and purple color scheme */ +.dark { + --background: oklch(0.08 0 0); + --foreground: oklch(0.95 0 0); + --card: oklch(0.12 0 0); + --card-foreground: oklch(0.95 0 0); + --popover: oklch(0.12 0 0); + --popover-foreground: oklch(0.95 0 0); + --primary: oklch(0.7 0.18 290); + --primary-foreground: oklch(0.98 0 0); + --secondary: oklch(0.16 0 0); + --secondary-foreground: oklch(0.85 0 0); + --muted: oklch(0.2 0 0); + --muted-foreground: oklch(0.6 0 0); + --accent: oklch(0.7 0.18 290); + --accent-foreground: oklch(0.98 0 0); + --destructive: oklch(0.65 0.2 25); + --destructive-foreground: oklch(0.95 0 0); + --border: oklch(0.22 0.02 290); + --input: oklch(0.16 0 0); + --ring: oklch(0.7 0.18 290); + --chart-1: oklch(0.7 0.18 290); + --chart-2: oklch(0.65 0.2 310); + --chart-3: oklch(0.6 0.15 270); + --chart-4: oklch(0.75 0.12 320); + --chart-5: oklch(0.55 0.18 280); + --sidebar: oklch(0.12 0 0); + --sidebar-foreground: oklch(0.95 0 0); + --sidebar-primary: oklch(0.7 0.18 290); + --sidebar-primary-foreground: oklch(0.98 0 0); + --sidebar-accent: oklch(0.16 0 0); + --sidebar-accent-foreground: oklch(0.95 0 0); + --sidebar-border: oklch(0.22 0.02 290); + --sidebar-ring: oklch(0.7 0.18 290); + --terminal: oklch(0.75 0.15 300); +} + +@theme inline { + /* Added Inter and JetBrains Mono fonts for backend aesthetic */ + --font-sans: "Inter", "Geist", "Geist Fallback"; + --font-mono: "JetBrains Mono", "Geist Mono", "Geist Mono Fallback"; + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-destructive-foreground: var(--destructive-foreground); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --color-chart-1: var(--chart-1); + --color-chart-2: var(--chart-2); + --color-chart-3: var(--chart-3); + --color-chart-4: var(--chart-4); + --color-chart-5: var(--chart-5); + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + --color-sidebar: var(--sidebar); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-ring: var(--sidebar-ring); + /* Added terminal color for green accents */ + --color-terminal: var(--terminal); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } +} diff --git a/frontend/app/layout.tsx b/frontend/app/layout.tsx new file mode 100644 index 0000000..095808a --- /dev/null +++ b/frontend/app/layout.tsx @@ -0,0 +1,55 @@ +import type React from "react" +import type { Metadata, Viewport } from "next" +import { Inter, JetBrains_Mono } from "next/font/google" +import { Analytics } from "@vercel/analytics/next" +import { DataStoreProvider } from "@/lib/data-store" +import "./globals.css" + +const inter = Inter({ subsets: ["latin"] }) +const jetbrainsMono = JetBrains_Mono({ + subsets: ["latin"], + variable: "--font-mono", +}) + +export const metadata: Metadata = { + title: "atticl — Backend Developer & Co-founder", + description: + "Backend developer focused on architecture, APIs, databases, performance, and security. Co-founder of crowware.com.", + generator: "v0.app", + icons: { + icon: [ + { + url: "/icon-light-32x32.png", + media: "(prefers-color-scheme: light)", + }, + { + url: "/icon-dark-32x32.png", + media: "(prefers-color-scheme: dark)", + }, + { + url: "/icon.svg", + type: "image/svg+xml", + }, + ], + apple: "/apple-icon.png", + }, +} + +export const viewport: Viewport = { + themeColor: "#1a1a1f", +} + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode +}>) { + return ( + + + {children} + + + + ) +} diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx new file mode 100644 index 0000000..4c34d00 --- /dev/null +++ b/frontend/app/page.tsx @@ -0,0 +1,25 @@ +import { Hero } from "@/components/hero"; +import { About } from "@/components/about"; +import { Header } from "@/components/header"; +import { Footer } from "@/components/footer"; +import { Contact } from "@/components/contact"; +import { Projects } from "@/components/projects"; +import { TechStack } from "@/components/techstack"; +import { Philosophy } from "@/components/philosophy"; + +export default function Home() { + return ( +
+
+
+ + + + + + +
+
+
+ ) +} \ No newline at end of file diff --git a/frontend/components.json b/frontend/components.json new file mode 100644 index 0000000..4ee62ee --- /dev/null +++ b/frontend/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "", + "css": "app/globals.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "iconLibrary": "lucide" +} diff --git a/frontend/components/about.tsx b/frontend/components/about.tsx new file mode 100644 index 0000000..460f238 --- /dev/null +++ b/frontend/components/about.tsx @@ -0,0 +1,63 @@ +import { Server, Database, Shield, Gauge } from "lucide-react" + +const highlights = [ + { icon: Server, label: "Backend Architecture" }, + { icon: Database, label: "Database Design" }, + { icon: Shield, label: "Security" }, + { icon: Gauge, label: "Performance" }, +] + +export function About() { + return ( +
+
+
+ {/* Section label */} +
+

About

+
+
+ + {/* Content */} +
+

+ I'm a backend developer who cares about what happens after the request hits the server. My work is focused + on building reliable, maintainable systems. APIs that don't break, databases that stay fast, infrastructure + that handles real-world load. +

+ +

+ As co-founder of{" "} + + crowware.com + + , I've learnt that good engineering isn't about using the newest tools. + It's about making pragmatic choices that serve the product and the team. + I prefer boring technology that works over shiny frameworks that don't. +

+ +

+ When I write code, I think about the person who has to maintain it at 2am. I optimize for clarity, + readability and the ability to debug production issues without losing sleep. (also for my own sanity) +

+ + {/* Highlight badges */} +
+ {highlights.map((item) => ( +
+ + {item.label} +
+ ))} +
+
+
+
+
+ ) +} \ No newline at end of file diff --git a/frontend/components/admin/admin-shell.tsx b/frontend/components/admin/admin-shell.tsx new file mode 100644 index 0000000..4485e41 --- /dev/null +++ b/frontend/components/admin/admin-shell.tsx @@ -0,0 +1,39 @@ +"use client" + +import type React from "react" + +import { useEffect } from "react" +import { useRouter } from "next/navigation" +import { useAuth } from "@/lib/auth-context" +import { AdminSidebar } from "./admin-sidebar" +import { Loader2 } from "lucide-react" + +export function AdminShell({ children }: { children: React.ReactNode }) { + const { isAuthenticated, isLoading } = useAuth() + const router = useRouter() + + useEffect(() => { + if (!isLoading && !isAuthenticated) { + router.push("/admin/login") + } + }, [isAuthenticated, isLoading, router]) + + if (isLoading) { + return ( +
+ +
+ ) + } + + if (!isAuthenticated) { + return null + } + + return ( +
+ +
{children}
+
+ ) +} diff --git a/frontend/components/admin/admin-sidebar.tsx b/frontend/components/admin/admin-sidebar.tsx new file mode 100644 index 0000000..d780c97 --- /dev/null +++ b/frontend/components/admin/admin-sidebar.tsx @@ -0,0 +1,74 @@ +"use client" + +import Link from "next/link" +import { usePathname } from "next/navigation" +import { cn } from "@/lib/utils" +import { useAuth } from "@/lib/auth-context" +import { LayoutDashboard, FolderKanban, Layers, LogOut, Terminal, ExternalLink } from "lucide-react" +import { Button } from "@/components/ui/button" + +const navItems = [ + { href: "/admin", label: "Dashboard", icon: LayoutDashboard }, + { href: "/admin/projects", label: "Projects", icon: FolderKanban }, + { href: "/admin/stack", label: "Tech Stack", icon: Layers }, +] + +export function AdminSidebar() { + const pathname = usePathname() + const { logout } = useAuth() + + return ( + + ) +} diff --git a/frontend/components/admin/project-form.tsx b/frontend/components/admin/project-form.tsx new file mode 100644 index 0000000..4633b36 --- /dev/null +++ b/frontend/components/admin/project-form.tsx @@ -0,0 +1,257 @@ +"use client" + +import type React from "react" + +import { useState, useEffect } from "react" +import type { Project } from "@/lib/types" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { Textarea } from "@/components/ui/textarea" +import { Switch } from "@/components/ui/switch" +import { X, Plus } from "lucide-react" + +interface ProjectFormProps { + project?: Project | null + onSubmit: (data: Omit) => void + onCancel: () => void +} + +export function ProjectForm({ project, onSubmit, onCancel }: ProjectFormProps) { + const [formData, setFormData] = useState({ + name: "", + role: "", + description: "", + highlights: [] as string[], + stack: [] as string[], + website: "", + github: "", + featured: false, + }) + const [newHighlight, setNewHighlight] = useState("") + const [newTech, setNewTech] = useState("") + + useEffect(() => { + if (project) { + setFormData({ + name: project.name, + role: project.role, + description: project.description, + highlights: project.highlights, + stack: project.stack, + website: project.website || "", + github: project.github || "", + featured: project.featured, + }) + } + }, [project]) + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault() + onSubmit({ + ...formData, + website: formData.website || null, + github: formData.github || null, + }) + } + + const addHighlight = () => { + if (newHighlight.trim()) { + setFormData((prev) => ({ + ...prev, + highlights: [...prev.highlights, newHighlight.trim()], + })) + setNewHighlight("") + } + } + + const removeHighlight = (index: number) => { + setFormData((prev) => ({ + ...prev, + highlights: prev.highlights.filter((_, i) => i !== index), + })) + } + + const addTech = () => { + if (newTech.trim()) { + setFormData((prev) => ({ + ...prev, + stack: [...prev.stack, newTech.trim()], + })) + setNewTech("") + } + } + + const removeTech = (index: number) => { + setFormData((prev) => ({ + ...prev, + stack: prev.stack.filter((_, i) => i !== index), + })) + } + + return ( +
+
+
+ + setFormData((prev) => ({ ...prev, name: e.target.value }))} + placeholder="My Project" + required + /> +
+
+ + setFormData((prev) => ({ ...prev, role: e.target.value }))} + placeholder="Creator / Lead / Contributor" + required + /> +
+
+ +
+ +