Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions frontend/app/(auth)/admin/menu/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -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 <p className="p-8 text-center">Loading...</p>;
if (isError || !item) return <p className="p-8 text-center text-red-500">Failed to load menu item.</p>;

return (
<div className="p-6">
<h1 className="text-2xl font-bold mb-6">Edit Menu Item</h1>
<MenuForm initialData={item} onSubmit={handleSubmit} isSubmitting={isSubmitting} submitLabel="Update Menu Item" />
</div>
);
}
28 changes: 28 additions & 0 deletions frontend/app/(auth)/admin/menu/new/page.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="p-6">
<h1 className="text-2xl font-bold mb-6">Add Menu Item</h1>
<MenuForm onSubmit={handleSubmit} isSubmitting={isSubmitting} submitLabel="Create Menu Item" />
</div>
);
}
44 changes: 44 additions & 0 deletions frontend/app/(auth)/admin/not-found.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
"use client";

import { useRouter } from "next/navigation";
import { motion } from "framer-motion";

export default function AdminNotFound() {
const router = useRouter();

return (
<motion.div
className="min-h-screen flex flex-col items-center justify-center gap-6 bg-gray-950 text-white px-4"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4 }}
>
<h1 className="text-8xl font-black bg-gradient-to-r from-purple-400 to-pink-500 bg-clip-text text-transparent">
404 🔒
</h1>
<p className="text-xl text-gray-400 text-center max-w-md">
This page doesn&apos;t exist in the admin panel.
</p>
<div className="flex flex-wrap gap-3 justify-center">
<button
onClick={() => router.push("/admin/dashboard")}
className="px-5 py-2 rounded-lg bg-purple-600 hover:bg-purple-700 transition"
>
Go to Dashboard
</button>
<button
onClick={() => router.push("/")}
className="px-5 py-2 rounded-lg bg-gray-700 hover:bg-gray-600 transition"
>
Main Site
</button>
<button
onClick={() => router.back()}
className="px-5 py-2 rounded-lg border border-gray-600 hover:bg-gray-800 transition"
>
Go Back
</button>
</div>
</motion.div>
);
}
88 changes: 88 additions & 0 deletions frontend/app/(auth)/admin/profile/ProfileClient.tsx
Original file line number Diff line number Diff line change
@@ -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<Tab>("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 (
<div className="flex flex-col md:flex-row gap-6 p-6">
{/* Left: Avatar card */}
<div className="md:w-64 shrink-0 bg-gray-900 rounded-xl p-6 flex flex-col items-center gap-3 text-center">
<div className="w-16 h-16 rounded-full bg-purple-600 flex items-center justify-center text-2xl font-bold text-white">
{initials}
</div>
<div>
<p className="font-semibold text-white">{user.username}</p>
<p className="text-sm text-gray-400">{user.email}</p>
</div>
<RoleBadge role={user.role} />
{user.lastLoginAt && (
<p className="text-xs text-gray-500">
Last login: {format(new Date(user.lastLoginAt), "PPP")}
</p>
)}
</div>

{/* Right: Tabs */}
<div className="flex-1 bg-gray-900 rounded-xl p-6">
<div className="flex gap-4 border-b border-gray-700 mb-6">
{(["profile", "security"] as Tab[]).map((t) => (
<button
key={t}
onClick={() => setTab(t)}
className={`pb-2 text-sm font-medium capitalize border-b-2 transition ${
tab === t ? "border-purple-500 text-white" : "border-transparent text-gray-400 hover:text-white"
}`}
>
{t === "profile" ? "Profile Info" : "Security"}
</button>
))}
</div>

{tab === "profile" ? (
<ProfileForm
initialData={{ username: user.username, email: user.email }}
onSubmit={handleProfileSubmit}
isSubmitting={isSubmitting}
/>
) : (
<ChangePasswordForm onSubmit={handlePasswordSubmit} isSubmitting={isSubmitting} />
)}
</div>
</div>
);
}
8 changes: 8 additions & 0 deletions frontend/app/(auth)/admin/profile/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type { Metadata } from "next";
import ProfileClient from "./ProfileClient";

export const metadata: Metadata = { title: "Profile" };

export default function ProfilePage() {
return <ProfileClient />;
}
5 changes: 5 additions & 0 deletions frontend/app/(auth)/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import React from "react";

export default function AuthLayout({ children }: { children: React.ReactNode }) {
return <>{children}</>;
}
67 changes: 67 additions & 0 deletions frontend/app/components/admin/profile/ChangePasswordForm.tsx
Original file line number Diff line number Diff line change
@@ -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<typeof schema>;

interface ChangePasswordFormProps {
onSubmit: (data: FormData) => Promise<void>;
isSubmitting: boolean;
}

export function ChangePasswordForm({ onSubmit, isSubmitting }: ChangePasswordFormProps) {
const { register, handleSubmit, formState: { errors }, reset } = useForm<FormData>({
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 (
<form onSubmit={handleSubmit(handleFormSubmit)} className="space-y-4">
{fields.map(({ name, label }) => (
<div key={name}>
<label className="block text-sm font-medium text-gray-300 mb-1">{label}</label>
<input
type="password"
{...register(name)}
className={cn(
"w-full px-3 py-2 rounded-lg bg-gray-800 border text-white focus:outline-none focus:ring-2 focus:ring-purple-500",
errors[name] ? "border-red-500" : "border-gray-600"
)}
/>
{errors[name] && <p className="mt-1 text-xs text-red-400">{errors[name]?.message}</p>}
</div>
))}
<button
type="submit"
disabled={isSubmitting}
className="flex items-center gap-2 px-4 py-2 bg-purple-600 hover:bg-purple-700 text-white rounded-lg transition disabled:opacity-50"
>
{isSubmitting && <Loader2 className="w-4 h-4 animate-spin" />}
Change Password
</button>
</form>
);
}
29 changes: 29 additions & 0 deletions frontend/app/components/admin/users/RoleBadge.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { AdminRole } from "@/lib/types/user";
import { cn } from "@/lib/utils";

const roleStyles: Record<AdminRole, string> = {
[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, string> = {
[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 (
<span className={cn("px-2 py-0.5 rounded-full text-xs font-semibold", roleStyles[role], className)}>
{roleLabels[role]}
</span>
);
}
Loading