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 (
+
+ );
+}
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]}
+
+ );
+}