feat: add reviews tab and review entity for enhanced manager functionality
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { ProductsTab } from "@/components/organisms/manager";
|
||||
import { ProductsTab, ReviewsTab } from "@/components/organisms/manager";
|
||||
import { useAuth } from "@/lib/auth-context";
|
||||
import { useManager } from "@/lib/manager-context";
|
||||
import Link from "next/link";
|
||||
@@ -19,6 +19,12 @@ export default function ManagerPage() {
|
||||
icon: "fa-solid fa-utensils",
|
||||
count: products.length,
|
||||
},
|
||||
{
|
||||
id: "reviews" as const,
|
||||
label: "Reviews",
|
||||
icon: "fa-solid fa-star",
|
||||
count: null,
|
||||
},
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
@@ -156,15 +162,9 @@ export default function ManagerPage() {
|
||||
{tabs.find((t) => t.id === activeTab)?.label ?? "Manager"}
|
||||
</h1>
|
||||
<p className="truncate text-xs text-(--color-text-muted)">
|
||||
Manage{" "}
|
||||
{activeTab === "products"
|
||||
? "menu items"
|
||||
: activeTab === "combos"
|
||||
? "combos"
|
||||
: activeTab === "categories"
|
||||
? "categories"
|
||||
: "menu"}{" "}
|
||||
for your store
|
||||
? "Manage menu items for your store"
|
||||
: "Customer reviews for your eatery"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -352,6 +352,7 @@ export default function ManagerPage() {
|
||||
|
||||
<main className="flex-1 p-5 md:p-8">
|
||||
{activeTab === "products" && <ProductsTab />}
|
||||
{activeTab === "reviews" && <ReviewsTab />}
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
import type { MenuItemEntity } from "@/lib/types";
|
||||
|
||||
export interface ReviewEntity {
|
||||
id?: string;
|
||||
reviewerId: string;
|
||||
eateryId: string;
|
||||
rating: number;
|
||||
comment?: string;
|
||||
createdAt?: string;
|
||||
}
|
||||
|
||||
export interface ProductModalProps {
|
||||
product: MenuItemEntity | null;
|
||||
onSave: (p: MenuItemEntity) => void;
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
"use client";
|
||||
|
||||
import { useManager } from "@/lib/manager-context";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import type { ReviewEntity } from "./Manager.types";
|
||||
|
||||
interface FetchState {
|
||||
reviews: ReviewEntity[];
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
function StarRating({ rating }: { rating: number }) {
|
||||
return (
|
||||
<div className="flex gap-0.5">
|
||||
{[1, 2, 3, 4, 5].map((star) => (
|
||||
<i
|
||||
key={star}
|
||||
className={
|
||||
star <= rating
|
||||
? "fa-solid fa-star text-sm text-yellow-400"
|
||||
: "fa-regular fa-star text-sm text-(--color-border)"
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ReviewsTab() {
|
||||
const { eateryId } = useManager();
|
||||
const [{ reviews, loading, error }, setState] = useState<FetchState>({
|
||||
reviews: [],
|
||||
loading: true,
|
||||
error: null,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!eateryId) return;
|
||||
|
||||
const controller = new AbortController();
|
||||
|
||||
fetch(`/api/eatery/review/${eateryId}`, { signal: controller.signal })
|
||||
.then((res) => {
|
||||
if (!res.ok) throw new Error(`Failed to load reviews: ${res.status}`);
|
||||
return res.json() as Promise<ReviewEntity[]>;
|
||||
})
|
||||
.then((data) => setState({ reviews: data, loading: false, error: null }))
|
||||
.catch((err: unknown) => {
|
||||
if (err instanceof Error && err.name === "AbortError") return;
|
||||
setState({
|
||||
reviews: [],
|
||||
loading: false,
|
||||
error: err instanceof Error ? err.message : "Failed to load reviews.",
|
||||
});
|
||||
});
|
||||
|
||||
return () => controller.abort();
|
||||
}, [eateryId]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-20 text-(--color-text-muted)">
|
||||
<i className="fa-solid fa-spinner fa-spin mr-2"></i>
|
||||
Loading reviews...
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-20 text-red-500">
|
||||
<i className="fa-solid fa-triangle-exclamation mr-2"></i>
|
||||
{error}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (reviews.length === 0) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center py-20 text-center text-(--color-text-muted)">
|
||||
<i className="fa-regular fa-star mb-3 text-4xl"></i>
|
||||
<p className="text-sm font-medium">No reviews yet</p>
|
||||
<p className="mt-1 text-xs">Reviews from customers will appear here.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const avgRating =
|
||||
reviews.reduce((sum, r) => sum + r.rating, 0) / reviews.length;
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Summary header */}
|
||||
<div className="flex items-center gap-4 rounded-2xl border border-(--color-border-light) bg-white p-5">
|
||||
<div className="flex h-14 w-14 shrink-0 items-center justify-center rounded-2xl bg-(--color-accent-light)">
|
||||
<i className="fa-solid fa-star text-2xl text-yellow-400"></i>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-foreground text-2xl font-bold">
|
||||
{avgRating.toFixed(1)}
|
||||
<span className="ml-1 text-sm font-normal text-(--color-text-muted)">
|
||||
/ 5
|
||||
</span>
|
||||
</p>
|
||||
<StarRating rating={Math.round(avgRating)} />
|
||||
<p className="mt-0.5 text-xs text-(--color-text-muted)">
|
||||
{reviews.length} review{reviews.length !== 1 ? "s" : ""}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Review list */}
|
||||
<div className="space-y-3">
|
||||
{reviews.map((review, idx) => (
|
||||
<div
|
||||
key={review.id ?? idx}
|
||||
className="rounded-2xl border border-(--color-border-light) bg-white p-4"
|
||||
>
|
||||
<div className="mb-2 flex items-start justify-between gap-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-(--color-accent-light)">
|
||||
<i className="fa-solid fa-user text-xs text-(--color-primary)"></i>
|
||||
</div>
|
||||
<span className="text-sm font-medium text-(--color-text-secondary)">
|
||||
{review.reviewerId}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex shrink-0 flex-col items-end gap-1">
|
||||
<StarRating rating={review.rating} />
|
||||
{review.createdAt && (
|
||||
<span className="text-xs text-(--color-text-muted)">
|
||||
{new Date(review.createdAt).toLocaleDateString("vi-VN")}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{review.comment ? (
|
||||
<p className="text-sm leading-relaxed text-(--color-text-secondary)">
|
||||
{review.comment}
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-sm italic text-(--color-text-muted)">
|
||||
No comment provided.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -3,8 +3,10 @@ export { default as DeleteConfirm } from "./DeleteConfirm";
|
||||
export { default as ProductModal } from "./ProductModal";
|
||||
export { default as ProductsTab } from "./ProductsTab";
|
||||
export { default as MenuItemsTab } from "./MenuItemsTab";
|
||||
export { default as ReviewsTab } from "./ReviewsTab";
|
||||
export type {
|
||||
ProductModalProps,
|
||||
DeleteConfirmProps,
|
||||
StatusBadgeProps,
|
||||
ReviewEntity,
|
||||
} from "./Manager.types";
|
||||
|
||||
@@ -21,11 +21,12 @@ import {
|
||||
|
||||
// ─── Types ────────────────────────────────────────────────────────────────────
|
||||
|
||||
export type ManagerTab = "products";
|
||||
export type ManagerTab = "products" | "reviews";
|
||||
|
||||
interface ManagerContextType {
|
||||
// Data
|
||||
products: MenuItemEntity[];
|
||||
eateryId: string | null;
|
||||
|
||||
// Active tab
|
||||
activeTab: ManagerTab;
|
||||
@@ -103,6 +104,8 @@ export function ManagerProvider({ children }: { children: ReactNode }) {
|
||||
fetchPolicy: "network-only",
|
||||
});
|
||||
|
||||
const eateryId = data?.allEateries?.[0]?.id ?? null;
|
||||
|
||||
const [mutateAddMenuItem] = useMutation<addMenuItemMutation>(ADD_MENU_ITEM, {
|
||||
client: eateryClient,
|
||||
});
|
||||
@@ -191,6 +194,7 @@ export function ManagerProvider({ children }: { children: ReactNode }) {
|
||||
<ManagerContext.Provider
|
||||
value={{
|
||||
products,
|
||||
eateryId,
|
||||
activeTab,
|
||||
setActiveTab,
|
||||
addProduct,
|
||||
|
||||
Reference in New Issue
Block a user