diff --git a/frontend/app/(auth)/admin/menu/[id]/page.tsx b/frontend/app/(auth)/admin/menu/[id]/page.tsx new file mode 100644 index 0000000..8e33724 --- /dev/null +++ b/frontend/app/(auth)/admin/menu/[id]/page.tsx @@ -0,0 +1,38 @@ +"use client"; + +import { useState } from "react"; +import { useParams, useRouter } from "next/navigation"; +import { useQuery } from "@tanstack/react-query"; +import { adminApi } from "@/lib/api/admin"; +import { MenuForm } from "@/app/components/admin/menu/MenuForm"; + +export default function EditMenuItemPage() { + const { id } = useParams<{ id: string }>(); + const router = useRouter(); + const [isSubmitting, setIsSubmitting] = useState(false); + + const { data: item, isLoading, isError } = useQuery({ + queryKey: ["menu-item", id], + queryFn: () => adminApi.getMenuItem(id), + }); + + const handleSubmit = async (data: any) => { + setIsSubmitting(true); + try { + await adminApi.updateMenuItem(id, data); + router.push("/admin/menu"); + } finally { + setIsSubmitting(false); + } + }; + + if (isLoading) return

Loading...

; + if (isError || !item) return

Failed to load menu item.

; + + return ( +
+

Edit Menu Item

+ +
+ ); +} diff --git a/frontend/app/(auth)/admin/menu/new/page.tsx b/frontend/app/(auth)/admin/menu/new/page.tsx new file mode 100644 index 0000000..25fc550 --- /dev/null +++ b/frontend/app/(auth)/admin/menu/new/page.tsx @@ -0,0 +1,28 @@ +"use client"; + +import { useState } from "react"; +import { useRouter } from "next/navigation"; +import { adminApi } from "@/lib/api/admin"; +import { MenuForm } from "@/app/components/admin/menu/MenuForm"; + +export default function NewMenuItemPage() { + const router = useRouter(); + const [isSubmitting, setIsSubmitting] = useState(false); + + const handleSubmit = async (data: any) => { + setIsSubmitting(true); + try { + await adminApi.createMenuItem(data); + router.push("/admin/menu"); + } finally { + setIsSubmitting(false); + } + }; + + return ( +
+

Add Menu Item

+ +
+ ); +} diff --git a/frontend/app/(auth)/admin/not-found.tsx b/frontend/app/(auth)/admin/not-found.tsx new file mode 100644 index 0000000..d494e4d --- /dev/null +++ b/frontend/app/(auth)/admin/not-found.tsx @@ -0,0 +1,44 @@ +"use client"; + +import { useRouter } from "next/navigation"; +import { motion } from "framer-motion"; + +export default function AdminNotFound() { + const router = useRouter(); + + return ( + +

+ 404 🔒 +

+

+ This page doesn't exist in the admin panel. +

+
+ + + +
+
+ ); +} diff --git a/frontend/app/(auth)/admin/profile/ProfileClient.tsx b/frontend/app/(auth)/admin/profile/ProfileClient.tsx new file mode 100644 index 0000000..2c8ce7b --- /dev/null +++ b/frontend/app/(auth)/admin/profile/ProfileClient.tsx @@ -0,0 +1,88 @@ +"use client"; + +import { useState } from "react"; +import { format } from "date-fns"; +import { useAuthStore } from "@/lib/store/authStore"; +import { ProfileForm } from "@/app/components/admin/profile/ProfileForm"; +import { ChangePasswordForm } from "@/app/components/admin/profile/ChangePasswordForm"; +import { RoleBadge } from "@/app/components/admin/users/RoleBadge"; + +type Tab = "profile" | "security"; + +export default function ProfileClient() { + const user = useAuthStore((s) => s.user); + const [tab, setTab] = useState("profile"); + const [isSubmitting, setIsSubmitting] = useState(false); + + if (!user) return null; + + const initials = user.username.slice(0, 2).toUpperCase(); + + const handleProfileSubmit = async (data: { username: string; email: string }) => { + setIsSubmitting(true); + try { + // TODO: call update profile API + console.log("Profile update", data); + } finally { + setIsSubmitting(false); + } + }; + + const handlePasswordSubmit = async (data: { currentPassword: string; newPassword: string; confirmPassword: string }) => { + setIsSubmitting(true); + try { + // TODO: call change password API + console.log("Password change", data); + } finally { + setIsSubmitting(false); + } + }; + + return ( +
+ {/* Left: Avatar card */} +
+
+ {initials} +
+
+

{user.username}

+

{user.email}

+
+ + {user.lastLoginAt && ( +

+ Last login: {format(new Date(user.lastLoginAt), "PPP")} +

+ )} +
+ + {/* Right: Tabs */} +
+
+ {(["profile", "security"] as Tab[]).map((t) => ( + + ))} +
+ + {tab === "profile" ? ( + + ) : ( + + )} +
+
+ ); +} diff --git a/frontend/app/(auth)/admin/profile/page.tsx b/frontend/app/(auth)/admin/profile/page.tsx new file mode 100644 index 0000000..9bd0c6b --- /dev/null +++ b/frontend/app/(auth)/admin/profile/page.tsx @@ -0,0 +1,8 @@ +import type { Metadata } from "next"; +import ProfileClient from "./ProfileClient"; + +export const metadata: Metadata = { title: "Profile" }; + +export default function ProfilePage() { + return ; +} diff --git a/frontend/app/(auth)/layout.tsx b/frontend/app/(auth)/layout.tsx new file mode 100644 index 0000000..cf4c5bb --- /dev/null +++ b/frontend/app/(auth)/layout.tsx @@ -0,0 +1,5 @@ +import React from "react"; + +export default function AuthLayout({ children }: { children: React.ReactNode }) { + return <>{children}; +} diff --git a/frontend/app/components/admin/profile/ChangePasswordForm.tsx b/frontend/app/components/admin/profile/ChangePasswordForm.tsx new file mode 100644 index 0000000..a316310 --- /dev/null +++ b/frontend/app/components/admin/profile/ChangePasswordForm.tsx @@ -0,0 +1,67 @@ +"use client"; + +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; +import { Loader2 } from "lucide-react"; +import { cn } from "@/lib/utils"; + +const schema = z.object({ + currentPassword: z.string().min(1, "Required"), + newPassword: z.string().min(8, "At least 8 characters"), + confirmPassword: z.string().min(1, "Required"), +}).refine((d) => d.newPassword === d.confirmPassword, { + message: "Passwords do not match", + path: ["confirmPassword"], +}); + +type FormData = z.infer; + +interface ChangePasswordFormProps { + onSubmit: (data: FormData) => Promise; + isSubmitting: boolean; +} + +export function ChangePasswordForm({ onSubmit, isSubmitting }: ChangePasswordFormProps) { + const { register, handleSubmit, formState: { errors }, reset } = useForm({ + resolver: zodResolver(schema), + }); + + const handleFormSubmit = async (data: FormData) => { + await onSubmit(data); + reset(); + }; + + const fields: { name: keyof FormData; label: string }[] = [ + { name: "currentPassword", label: "Current Password" }, + { name: "newPassword", label: "New Password" }, + { name: "confirmPassword", label: "Confirm New Password" }, + ]; + + return ( +
+ {fields.map(({ name, label }) => ( +
+ + + {errors[name] &&

{errors[name]?.message}

} +
+ ))} + +
+ ); +} diff --git a/frontend/app/components/admin/users/RoleBadge.tsx b/frontend/app/components/admin/users/RoleBadge.tsx new file mode 100644 index 0000000..4fc9813 --- /dev/null +++ b/frontend/app/components/admin/users/RoleBadge.tsx @@ -0,0 +1,29 @@ +import { AdminRole } from "@/lib/types/user"; +import { cn } from "@/lib/utils"; + +const roleStyles: Record = { + [AdminRole.SUPER_ADMIN]: "bg-purple-900 text-purple-200", + [AdminRole.ADMIN]: "bg-blue-900 text-blue-200", + [AdminRole.MANAGER]: "bg-green-900 text-green-200", + [AdminRole.STAFF]: "bg-gray-700 text-gray-200", +}; + +const roleLabels: Record = { + [AdminRole.SUPER_ADMIN]: "Super Admin", + [AdminRole.ADMIN]: "Admin", + [AdminRole.MANAGER]: "Manager", + [AdminRole.STAFF]: "Staff", +}; + +interface RoleBadgeProps { + role: AdminRole; + className?: string; +} + +export function RoleBadge({ role, className }: RoleBadgeProps) { + return ( + + {roleLabels[role]} + + ); +}