diff --git a/frontend/app/(auth)/admin/gallery/[id]/page.tsx b/frontend/app/(auth)/admin/gallery/[id]/page.tsx
new file mode 100644
index 0000000..53261d7
--- /dev/null
+++ b/frontend/app/(auth)/admin/gallery/[id]/page.tsx
@@ -0,0 +1,17 @@
+"use client";
+
+import { useParams } from "next/navigation";
+import GalleryFormWrapper from "@/components/admin/gallery/GalleryFormWrapper";
+import { RoleProtectedPage } from "@/components/admin/RoleProtectedPage";
+import { AdminRole } from "@/lib/types/user";
+
+export default function EditGalleryImagePage() {
+ const params = useParams();
+ const id = params.id as string;
+
+ return (
+
+
+
+ );
+}
diff --git a/frontend/app/(auth)/admin/gallery/new/page.tsx b/frontend/app/(auth)/admin/gallery/new/page.tsx
new file mode 100644
index 0000000..e173465
--- /dev/null
+++ b/frontend/app/(auth)/admin/gallery/new/page.tsx
@@ -0,0 +1,16 @@
+import { Metadata } from "next";
+import GalleryFormWrapper from "@/components/admin/gallery/GalleryFormWrapper";
+import { RoleProtectedPage } from "@/components/admin/RoleProtectedPage";
+import { AdminRole } from "@/lib/types/user";
+
+export const metadata: Metadata = {
+ title: "Add Gallery Image",
+};
+
+export default function NewGalleryImagePage() {
+ return (
+
+
+
+ );
+}
diff --git a/frontend/app/(auth)/admin/instagram/InstagramClient.tsx b/frontend/app/(auth)/admin/instagram/InstagramClient.tsx
new file mode 100644
index 0000000..b3513fd
--- /dev/null
+++ b/frontend/app/(auth)/admin/instagram/InstagramClient.tsx
@@ -0,0 +1,246 @@
+"use client";
+
+import { useState } from "react";
+import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
+import { useRouter } from "next/navigation";
+import { Plus, Instagram as InstagramIcon, Eye, EyeOff, Loader2 } from "lucide-react";
+import { motion } from "framer-motion";
+import toast from "react-hot-toast";
+import { adminApi } from "@/lib/api/admin";
+import { InstagramTable } from "@/components/admin/instagram/InstagramTable";
+import { Button } from "@/components/ui/Button";
+import type { InstagramPost } from "@/lib/api/admin";
+
+export default function InstagramClient() {
+ const router = useRouter();
+ const queryClient = useQueryClient();
+
+ // Fetch Instagram posts
+ const {
+ data: posts = [],
+ isLoading,
+ error,
+ } = useQuery({
+ queryKey: ["instagram-posts"],
+ queryFn: () => adminApi.getAllInstagramPosts(),
+ });
+
+ // Toggle visibility mutation
+ const toggleVisibilityMutation = useMutation({
+ mutationFn: adminApi.toggleInstagramPostVisibility,
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ["instagram-posts"] });
+ toast.success("Post visibility updated successfully");
+ },
+ onError: (error: any) => {
+ toast.error(error.message || "Failed to update post visibility");
+ },
+ });
+
+ // Delete mutation
+ const deleteMutation = useMutation({
+ mutationFn: adminApi.deleteInstagramPost,
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ["instagram-posts"] });
+ toast.success("Instagram post deleted successfully");
+ },
+ onError: (error: any) => {
+ toast.error(error.message || "Failed to delete post");
+ },
+ });
+
+ // Calculate stats
+ const stats = {
+ total: posts.length,
+ visible: posts.filter((post: InstagramPost) => post.isVisible).length,
+ hidden: posts.filter((post: InstagramPost) => !post.isVisible).length,
+ };
+
+ const handleToggleVisibility = (id: string) => {
+ toggleVisibilityMutation.mutate(id);
+ };
+
+ const handleDelete = async (id: string) => {
+ await deleteMutation.mutateAsync(id);
+ };
+
+ // Loading skeleton
+ if (isLoading) {
+ return (
+
+
+
+
Instagram Posts
+
Manage your Instagram feed
+
+
+
+
+ {/* Stats skeletons */}
+
+ {[1, 2, 3].map((i) => (
+
+ ))}
+
+
+ {/* Table skeleton */}
+
+
+ {[1, 2, 3, 4, 5].map((i) => (
+
+ ))}
+
+
+
+ );
+ }
+
+ // Error state
+ if (error) {
+ return (
+
+
+
+
+
+
+ Failed to load Instagram posts
+
+
+ Please check your connection and try again.
+
+
+
+
+ );
+ }
+
+ return (
+
+ {/* Header */}
+
+
+
Instagram Posts
+
Manage your Instagram feed
+
+
+
+
+ {/* Stats Cards */}
+
+
+
+
+
+
Total Posts
+
{stats.total}
+
+
+
+
+
+
+
+
+
Visible
+
{stats.visible}
+
+
+
+
+
+
+
+
+
Hidden
+
{stats.hidden}
+
+
+
+
+
+ {/* Table */}
+
+
+
+
+ {/* Empty state */}
+ {posts.length === 0 && (
+
+
+
+
+
+ No Instagram posts yet
+
+
+ Get started by adding your first Instagram post to the feed.
+
+
+
+ )}
+
+ );
+}
diff --git a/frontend/app/(auth)/admin/instagram/[id]/page.tsx b/frontend/app/(auth)/admin/instagram/[id]/page.tsx
new file mode 100644
index 0000000..2c18eb3
--- /dev/null
+++ b/frontend/app/(auth)/admin/instagram/[id]/page.tsx
@@ -0,0 +1,17 @@
+"use client";
+
+import { useParams } from "next/navigation";
+import InstagramFormWrapper from "../../../../components/admin/instagram/InstagramFormWrapper";
+import { RoleProtectedPage } from "../../../../components/admin/RoleProtectedPage";
+import { AdminRole } from "@/lib/types/user";
+
+export default function EditInstagramPostPage() {
+ const params = useParams();
+ const id = params.id as string;
+
+ return (
+
+
+
+ );
+}
diff --git a/frontend/app/(auth)/admin/instagram/new/page.tsx b/frontend/app/(auth)/admin/instagram/new/page.tsx
new file mode 100644
index 0000000..339a2e0
--- /dev/null
+++ b/frontend/app/(auth)/admin/instagram/new/page.tsx
@@ -0,0 +1,16 @@
+import { Metadata } from "next";
+import InstagramFormWrapper from "../../../../components/admin/instagram/InstagramFormWrapper";
+import { RoleProtectedPage } from "../../../../components/admin/RoleProtectedPage";
+import { AdminRole } from "@/lib/types/user";
+
+export const metadata: Metadata = {
+ title: "Add Instagram Post",
+};
+
+export default function NewInstagramPostPage() {
+ return (
+
+
+
+ );
+}
diff --git a/frontend/app/(auth)/admin/instagram/page.tsx b/frontend/app/(auth)/admin/instagram/page.tsx
new file mode 100644
index 0000000..da65278
--- /dev/null
+++ b/frontend/app/(auth)/admin/instagram/page.tsx
@@ -0,0 +1,16 @@
+import { Metadata } from "next";
+import InstagramClient from "./InstagramClient";
+import { RoleProtectedPage } from "@/components/admin/RoleProtectedPage";
+import { AdminRole } from "@/lib/types/user";
+
+export const metadata: Metadata = {
+ title: "Instagram",
+};
+
+export default function InstagramManagementPage() {
+ return (
+
+
+
+ );
+}
diff --git a/frontend/app/(auth)/login/LoginClient.tsx b/frontend/app/(auth)/login/LoginClient.tsx
new file mode 100644
index 0000000..d068215
--- /dev/null
+++ b/frontend/app/(auth)/login/LoginClient.tsx
@@ -0,0 +1,162 @@
+"use client";
+
+import { useEffect, useState } from "react";
+import { useRouter } from "next/navigation";
+import { useForm } from "react-hook-form";
+import { zodResolver } from "@hookform/resolvers/zod";
+import { z } from "zod";
+import { Eye, EyeOff, Loader2 } from "lucide-react";
+import { adminApi } from "@/lib/api/api";
+import { useAuthStore } from "@/lib/store/authStore";
+import Link from "next/link";
+
+const loginSchema = z.object({
+ username: z.string().min(1, "Username is required"),
+ password: z.string().min(1, "Password is required"),
+});
+
+type LoginFormData = z.infer;
+
+export default function LoginClient() {
+ const router = useRouter();
+ const { isAuthenticated, setAuth } = useAuthStore();
+ const [showPassword, setShowPassword] = useState(false);
+ const [isLoading, setIsLoading] = useState(false);
+ const [error, setError] = useState(null);
+
+ const {
+ register,
+ handleSubmit,
+ formState: { errors },
+ } = useForm({
+ resolver: zodResolver(loginSchema),
+ });
+
+ useEffect(() => {
+ if (isAuthenticated) {
+ router.push("/admin/dashboard");
+ }
+ }, [isAuthenticated, router]);
+
+ const onSubmit = async (data: LoginFormData) => {
+ setIsLoading(true);
+ setError(null);
+
+ try {
+ const response = await adminApi.login(data);
+ setAuth(response.user, response.token);
+ router.push("/admin/dashboard");
+ } catch (err) {
+ setError(err instanceof Error ? err.message : "Login failed");
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ const allowRegistration = process.env.NEXT_PUBLIC_ALLOW_REGISTRATION === "true";
+
+ return (
+
+
+
+
+
+ Admin Login
+
+
+ Sign in to your admin account
+
+
+
+
+
+
+ );
+}
diff --git a/frontend/app/(auth)/login/page.tsx b/frontend/app/(auth)/login/page.tsx
new file mode 100644
index 0000000..5b56c65
--- /dev/null
+++ b/frontend/app/(auth)/login/page.tsx
@@ -0,0 +1,10 @@
+import { Metadata } from "next";
+import LoginClient from "./LoginClient";
+
+export const metadata: Metadata = {
+ title: "Admin Login",
+};
+
+export default function LoginPage() {
+ return ;
+}
diff --git a/frontend/app/components/admin/gallery/GalleryForm.tsx b/frontend/app/components/admin/gallery/GalleryForm.tsx
new file mode 100644
index 0000000..d169084
--- /dev/null
+++ b/frontend/app/components/admin/gallery/GalleryForm.tsx
@@ -0,0 +1,205 @@
+"use client";
+
+import React from "react";
+import { useForm, Controller } from "react-hook-form";
+import { zodResolver } from "@hookform/resolvers/zod";
+import { z } from "zod";
+import { Button } from "../../ui/Button";
+import { Input } from "../../ui/Input";
+import { ImageUpload } from "../../ui/ImageUpload";
+import { cn } from "@/lib/utils";
+import type { GalleryCategory } from "@/lib/api/admin";
+
+const galleryImageSchema = z.object({
+ imageUrl: z.string().min(1, "Image is required"),
+ alt: z.string().min(1, "Alt text is required"),
+ category: z.enum(["food", "ambiance", "kitchen", "events", "drinks"]),
+ isVisible: z.boolean().optional(),
+ displayOrder: z.number().min(0).optional(),
+});
+
+export type GalleryFormData = z.infer;
+
+interface GalleryFormProps {
+ initialData?: Partial;
+ onSubmit: (data: GalleryFormData) => Promise;
+ isSubmitting: boolean;
+ submitLabel?: string;
+}
+
+const categories: { value: GalleryCategory; label: string }[] = [
+ { value: "food", label: "Food" },
+ { value: "ambiance", label: "Ambiance" },
+ { value: "kitchen", label: "Kitchen" },
+ { value: "events", label: "Events" },
+ { value: "drinks", label: "Drinks" },
+];
+
+export function GalleryForm({
+ initialData,
+ onSubmit,
+ isSubmitting,
+ submitLabel = "Save Image",
+}: GalleryFormProps) {
+ const {
+ register,
+ handleSubmit,
+ control,
+ formState: { errors },
+ } = useForm({
+ resolver: zodResolver(galleryImageSchema),
+ defaultValues: {
+ isVisible: true,
+ displayOrder: 0,
+ imageUrl: "",
+ alt: "",
+ category: "food",
+ ...initialData,
+ },
+ });
+
+ return (
+
+ );
+}
diff --git a/frontend/app/components/admin/gallery/GalleryFormWrapper.tsx b/frontend/app/components/admin/gallery/GalleryFormWrapper.tsx
new file mode 100644
index 0000000..011d58d
--- /dev/null
+++ b/frontend/app/components/admin/gallery/GalleryFormWrapper.tsx
@@ -0,0 +1,115 @@
+"use client";
+
+import { useState } from "react";
+import { useRouter } from "next/navigation";
+import { useMutation, useQuery } from "@tanstack/react-query";
+import { GalleryForm, GalleryFormData } from "./GalleryForm";
+import { adminApi } from "@/lib/api/admin";
+import type { GalleryImage } from "@/lib/api/admin";
+
+interface GalleryFormWrapperProps {
+ mode: "create" | "edit";
+ initialData?: Partial;
+ imageId?: string;
+}
+
+export default function GalleryFormWrapper({
+ mode,
+ initialData,
+ imageId
+}: GalleryFormWrapperProps) {
+ const router = useRouter();
+ const [isSubmitting, setIsSubmitting] = useState(false);
+
+ // For edit mode, fetch the image data
+ const { data: imageResponse, isLoading, error } = useQuery({
+ queryKey: ["gallery-image", imageId],
+ queryFn: () => adminApi.getGalleryImage(imageId!),
+ enabled: mode === "edit" && !!imageId,
+ });
+
+ const image = imageResponse;
+
+ const createMutation = useMutation({
+ mutationFn: adminApi.createGalleryImage,
+ onSuccess: () => {
+ router.push("/admin/gallery");
+ },
+ onError: (error) => {
+ console.error("Failed to create gallery image:", error);
+ },
+ });
+
+ const updateMutation = useMutation({
+ mutationFn: ({ id, data }: { id: string; data: Partial }) =>
+ adminApi.updateGalleryImage(id, data),
+ onSuccess: () => {
+ router.push("/admin/gallery");
+ },
+ onError: (error) => {
+ console.error("Failed to update gallery image:", error);
+ },
+ });
+
+ const handleSubmit = async (data: GalleryFormData) => {
+ setIsSubmitting(true);
+ try {
+ if (mode === "create") {
+ await createMutation.mutateAsync(data);
+ } else if (mode === "edit" && imageId) {
+ await updateMutation.mutateAsync({ id: imageId, data });
+ }
+ } finally {
+ setIsSubmitting(false);
+ }
+ };
+
+ if (mode === "edit" && isLoading) {
+ return (
+
+
+
+
Loading gallery image...
+
+
+ );
+ }
+
+ if (mode === "edit" && error) {
+ return (
+
+
+
+ Failed to load gallery image. Please try again.
+
+
+
+ );
+ }
+
+ const submitLabel = mode === "create" ? "Add to Gallery" : "Update Image";
+ const formData = mode === "edit" && image ? image : initialData;
+
+ return (
+
+
+
+ {mode === "create" ? "Add to Gallery" : "Update Image"}
+
+
+ {mode === "create"
+ ? "Add a new image to your gallery collection."
+ : "Update the gallery image details."
+ }
+
+
+
+
+
+ );
+}
diff --git a/frontend/app/components/admin/instagram/InstagramFormWrapper.tsx b/frontend/app/components/admin/instagram/InstagramFormWrapper.tsx
new file mode 100644
index 0000000..8324131
--- /dev/null
+++ b/frontend/app/components/admin/instagram/InstagramFormWrapper.tsx
@@ -0,0 +1,115 @@
+"use client";
+
+import { useState } from "react";
+import { useRouter } from "next/navigation";
+import { useMutation, useQuery } from "@tanstack/react-query";
+import { InstagramForm, InstagramFormData } from "./InstagramForm";
+import { instagramApi } from "@/lib/api/api";
+import { InstagramPost } from "@/lib/types";
+
+interface InstagramFormWrapperProps {
+ mode: "create" | "edit";
+ initialData?: Partial;
+ postId?: string;
+}
+
+export default function InstagramFormWrapper({
+ mode,
+ initialData,
+ postId
+}: InstagramFormWrapperProps) {
+ const router = useRouter();
+ const [isSubmitting, setIsSubmitting] = useState(false);
+
+ // For edit mode, fetch the post data
+ const { data: postResponse, isLoading, error } = useQuery({
+ queryKey: ["instagram-post", postId],
+ queryFn: () => instagramApi.getById(postId!),
+ enabled: mode === "edit" && !!postId,
+ });
+
+ const post = postResponse?.data;
+
+ const createMutation = useMutation({
+ mutationFn: (data: InstagramFormData) => instagramApi.create(data),
+ onSuccess: () => {
+ router.push("/admin/instagram");
+ },
+ onError: (error) => {
+ console.error("Failed to create Instagram post:", error);
+ },
+ });
+
+ const updateMutation = useMutation({
+ mutationFn: ({ id, data }: { id: string; data: Partial }) =>
+ instagramApi.update(id, data),
+ onSuccess: () => {
+ router.push("/admin/instagram");
+ },
+ onError: (error) => {
+ console.error("Failed to update Instagram post:", error);
+ },
+ });
+
+ const handleSubmit = async (data: InstagramFormData) => {
+ setIsSubmitting(true);
+ try {
+ if (mode === "create") {
+ await createMutation.mutateAsync(data);
+ } else if (mode === "edit" && postId) {
+ await updateMutation.mutateAsync({ id: postId, data });
+ }
+ } finally {
+ setIsSubmitting(false);
+ }
+ };
+
+ if (mode === "edit" && isLoading) {
+ return (
+
+
+
+
Loading Instagram post...
+
+
+ );
+ }
+
+ if (mode === "edit" && error) {
+ return (
+
+
+
+ Failed to load Instagram post. Please try again.
+
+
+
+ );
+ }
+
+ const submitLabel = mode === "create" ? "Create Post" : "Update Post";
+ const formData = mode === "edit" && post ? post : initialData;
+
+ return (
+
+
+
+ {mode === "create" ? "Add Instagram Post" : "Edit Instagram Post"}
+
+
+ {mode === "create"
+ ? "Add a new Instagram post to display on your website."
+ : "Update the Instagram post details."
+ }
+
+
+
+
+
+ );
+}
diff --git a/frontend/lib/api/api.ts b/frontend/lib/api/api.ts
index c771196..986b4f6 100644
--- a/frontend/lib/api/api.ts
+++ b/frontend/lib/api/api.ts
@@ -6,6 +6,7 @@ import type {
InstagramPost,
} from "@/lib/types";
import { GalleryImage } from "./admin";
+import { AdminUser } from "@/lib/types/user";
const API_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:9001/api";
const RESTAURANT_ID = process.env.NEXT_PUBLIC_RESTAURANT_ID;
@@ -114,6 +115,43 @@ export const contactApi = {
export const instagramApi = {
getPosts: (limit: number = 9) =>
fetchApi(addRestaurantId(`/instagram?limit=${limit}`)),
+
+ getById: (id: string) =>
+ fetchApi(addRestaurantId(`/instagram/${id}`)),
+
+ create: async (data: Omit) => {
+ const response = await fetch(`${API_URL}${addRestaurantId('/instagram')}`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(data),
+ });
+
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({}));
+ throw new Error(errorData.message || 'Failed to create Instagram post');
+ }
+
+ return response.json();
+ },
+
+ update: async (id: string, data: Partial) => {
+ const response = await fetch(`${API_URL}${addRestaurantId(`/instagram/${id}`)}`, {
+ method: 'PUT',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(data),
+ });
+
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({}));
+ throw new Error(errorData.message || 'Failed to update Instagram post');
+ }
+
+ return response.json();
+ },
};
// Gallery API
@@ -149,6 +187,30 @@ export const qrApi = {
}),
};
+// Admin API
+export const adminApi = {
+ login: async (credentials: { username: string; password: string }) => {
+ const response = await fetch(`${API_URL}/admin/login`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(credentials),
+ });
+
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({}));
+ throw new Error(errorData.message || "Login failed");
+ }
+
+ const data = await response.json();
+ return {
+ token: data.token,
+ user: data.user as AdminUser,
+ };
+ },
+};
+
const apiClient = {
menu: menuApi,
trending: trendingApi,
@@ -157,6 +219,7 @@ const apiClient = {
gallery: galleryApi,
analytics: analyticsApi,
qr: qrApi,
+ admin: adminApi,
};
export default apiClient;