Merge pull request 'Created User Review' (#25) from develop into main
Release package / release (push) Has started running

Reviewed-on: #25
This commit was merged in pull request #25.
This commit is contained in:
2026-03-31 11:03:53 +00:00
6 changed files with 9666 additions and 865 deletions
+38 -2
View File
@@ -1,7 +1,10 @@
"use client";
import ReviewModal from "@/components/ReviewModal";
import { useAuth } from "@/lib/auth-context";
import { useCart } from "@/lib/cart-context";
import Link from "next/link";
import { useState } from "react";
const formatPrice = (value: number) =>
value.toLocaleString("vi-VN", { style: "currency", currency: "VND" });
@@ -15,8 +18,20 @@ export default function PaymentPage() {
removeFromCart,
setQuantity,
} = useCart();
const { user } = useAuth();
const [isReviewOpen, setIsReviewOpen] = useState(false);
const isCustomer = user?.role === "customer";
const handlePayment = () => {
// UI-only: open review modal after "payment"
if (isCustomer) {
setIsReviewOpen(true);
}
};
return (
<>
<div className="mx-auto w-full max-w-screen-2xl px-4 py-6 md:px-6 md:py-8 lg:px-8">
<div className="flex flex-col gap-6 xl:flex-row">
<section className="min-w-0 flex-1">
@@ -36,7 +51,9 @@ export default function PaymentPage() {
<table className="w-full min-w-190 text-sm">
<thead>
<tr className="bg-(--color-border-light)/40 text-left">
<th className="px-4 py-3 font-semibold">Tên sản phẩm</th>
<th className="px-4 py-3 font-semibold">
Tên sản phẩm
</th>
<th className="px-4 py-3 font-semibold">Giá tiền</th>
<th className="px-4 py-3 font-semibold"> tả</th>
<th className="px-4 py-3 font-semibold">Số lượng</th>
@@ -121,6 +138,7 @@ export default function PaymentPage() {
<div className="mt-4 grid grid-cols-2 gap-3">
<button
onClick={handlePayment}
className="inline-flex cursor-pointer items-center justify-center gap-2 rounded-xl bg-(--color-primary) px-3 py-2.5 text-white transition-colors hover:bg-(--color-primary-dark)"
type="button"
>
@@ -129,6 +147,7 @@ export default function PaymentPage() {
</button>
<button
onClick={handlePayment}
className="text-foreground inline-flex cursor-pointer items-center justify-center gap-2 rounded-xl border border-(--color-border) px-3 py-2.5 transition-colors hover:bg-(--color-border-light)"
type="button"
>
@@ -136,10 +155,21 @@ export default function PaymentPage() {
<span className="hidden lg:inline">QR Code</span>
</button>
<Link href="/">
{isCustomer && (
<button
onClick={() => setIsReviewOpen(true)}
className="text-foreground inline-flex cursor-pointer items-center justify-center gap-2 rounded-xl border border-(--color-border) px-3 py-2.5 transition-colors hover:bg-(--color-border-light)"
type="button"
>
<i className="fa-solid fa-star"></i>
<span className="hidden lg:inline">Đánh giá</span>
</button>
)}
<Link href="/" className={isCustomer ? "" : "col-span-2"}>
<button
className="text-foreground inline-flex w-full cursor-pointer items-center justify-center gap-2 rounded-xl border border-(--color-border) px-3 py-2.5 transition-colors hover:bg-(--color-border-light)"
type="button"
>
<i className="fa-solid fa-arrow-rotate-left"></i>
<span className="hidden lg:inline">Quay về</span>
@@ -150,5 +180,11 @@ export default function PaymentPage() {
</aside>
</div>
</div>
<ReviewModal
isOpen={isReviewOpen}
onClose={() => setIsReviewOpen(false)}
/>
</>
);
}
+43 -4
View File
@@ -233,9 +233,47 @@ registration flow. Two-step process: phone verification → account creation.
### 4. Payment Page (app/(main)/payment/page.tsx)
**Route:** `/payment` **Type:** Client component (TBD) **Description:**
Payment/checkout page. Currently a placeholder; implement when cart and order
system are ready.
**Route:** `/payment`
**Type:** Client component
**Description:** Payment/checkout page. Shows cart items in a table with quantity controls and a summary aside with payment actions.
#### Key Features
- **Cart Table:**
- Lists all cart items (name, price, description, quantity controls, delete)
- Quantity input with +/- buttons and direct number input
- Empty state message when cart is empty
- Horizontal scroll on small screens (min-w-190)
- **Invoice Aside:**
- Sticky on desktop (top offset = header height + 1rem)
- Shows total price
- Payment buttons: Tiền mặt, QR Code (UI-only; trigger review modal for customers)
- **"Đánh giá" button** — only visible when `user.role === "customer"`; opens ReviewModal
- **"Quay về" button** — links back to `/`; spans full width when review button is absent
- **Review Modal (ReviewModal):**
- Opened when a customer clicks "Đánh giá" OR clicks a payment button (Tiền mặt/QR)
- Closed via "Quay lại" button, backdrop click, or after submitting and closing
- See `components/ReviewModal.tsx` for full documentation
#### Context Usage
- **useCart()** — items, totalPrice, increaseQty, decreaseQty, removeFromCart, setQuantity
- **useAuth()** — user (to check `user.role === "customer"` for review button visibility)
#### State Management
```tsx
isReviewOpen: boolean // Controls ReviewModal visibility
isCustomer: boolean // Derived from user.role === "customer"
```
#### Responsive Behavior
- **Mobile:** Single column layout, table scrolls horizontally, button labels hidden (icon only)
- **Desktop (lg+):** Button labels visible, aside becomes sticky sidebar (xl: w-85)
- **Review button grid:** When customer is logged in, 3 buttons in 2-column grid; "Quay về" occupies remaining space. Otherwise 2-column grid with "Quay về" spanning full width.
---
@@ -387,7 +425,8 @@ Defined at `:root` for light mode, with dark mode variants:
## Future Enhancements
- [ ] Payment page implementation
- [x] Payment page implementation
- [x] Customer review modal (ReviewModal) with 5-star rating + textarea
- [ ] Order history/tracking page
- [ ] Manager dashboard (menu management, order tracking)
- [ ] User profile page
+69
View File
@@ -198,6 +198,75 @@ hover.
---
## ReviewModal
**File:** components/ReviewModal.tsx
**Description:** Modal for customer reviews. Shows a 5-star rating selector and a textarea for written feedback. After submission, displays a thank-you message. Appears on the payment page for customers (via a dedicated "Đánh giá" button) and also opens automatically after a successful payment action.
### Props
| Prop | Type | Required | Default | Description |
| ------- | ---------- | -------- | ------- | ------------------------------------------ |
| isOpen | boolean | yes | - | Controls modal visibility |
| onClose | () => void | yes | - | Callback to close the modal & reset state |
### Behavior
- **Star rating:** 5 interactive stars; hover highlights up to hovered star, click locks selection. Disabled submit button until at least 1 star selected.
- **Star labels:** 1 = Rất tệ, 2 = Tệ, 3 = Bình thường, 4 = Tốt, 5 = Xuất sắc
- **Textarea:** Optional free-text review (placeholder in Vietnamese).
- **Footer buttons:**
- "Quay lại" — closes modal without submitting; resets all state.
- "Xác nhận" — submits (UI-only); transitions to thank-you state.
- **Thank-you state:** Replaces form with "Cảm ơn quý khách" message + close button.
- **Backdrop click:** Also closes the modal (same as "Quay lại").
- **State reset:** All state (rating, hover, review text, submitted) resets on close.
### Styling
| Element | Key classes |
| --------------- | ----------------------------------------------------------------------- |
| Backdrop | fixed inset-0, bg-black/50 backdrop-blur-sm, z-50 |
| Modal panel | max-w-md, rounded-2xl, border --color-border-light, white bg, shadow-xl |
| Stars | text-3xl sm:text-4xl, hover:scale-110, active:scale-95 |
| Active star | fa-solid fa-star text-yellow-400 |
| Inactive star | fa-regular fa-star text-(--color-border) |
| Textarea | rounded-xl, focus:ring with --color-primary/20 |
| Quay lại button | border --color-border, hover --color-border-light |
| Xác nhận button | bg --color-primary, disabled:opacity-50 |
| Thank-you icon | fa-solid fa-heart text-(--color-accent), --color-accent-light bg |
### Responsive
- Padding: `p-6 sm:p-8` — larger on sm+ screens
- Stars: `text-3xl sm:text-4xl`
- Modal width: `w-full max-w-md` with `p-4` page padding — works on all screen sizes
### Dependencies
- React useState
- FontAwesome icons (fa-star, fa-regular fa-star, fa-heart, fa-arrow-left, fa-check)
- Tailwind CSS + CSS custom properties from globals.css
### Usage in Payment Page
```tsx
// Only shown for customers (user.role === "customer")
const [isReviewOpen, setIsReviewOpen] = useState(false);
// Button in payment aside (only visible to customers)
<button onClick={() => setIsReviewOpen(true)}>Đánh giá</button>
// Payment buttons (Tiền mặt / QR Code) also open the modal for customers
const handlePayment = () => {
if (isCustomer) setIsReviewOpen(true);
};
<ReviewModal isOpen={isReviewOpen} onClose={() => setIsReviewOpen(false)} />
```
---
# Contexts Documentation
## AuthContext (lib/auth-context.tsx)
+170
View File
@@ -0,0 +1,170 @@
"use client";
import { useState } from "react";
interface ReviewModalProps {
isOpen: boolean;
onClose: () => void;
}
export default function ReviewModal({ isOpen, onClose }: ReviewModalProps) {
const [rating, setRating] = useState(0);
const [hovered, setHovered] = useState(0);
const [review, setReview] = useState("");
const [submitted, setSubmitted] = useState(false);
if (!isOpen) return null;
const handleSubmit = () => {
setSubmitted(true);
};
const handleClose = () => {
// Reset state when closing
setRating(0);
setHovered(0);
setReview("");
setSubmitted(false);
onClose();
};
return (
<div
className="fixed inset-0 z-50 flex items-center justify-center p-4"
role="dialog"
aria-modal="true"
aria-labelledby="review-modal-title"
>
{/* Backdrop */}
<div
className="absolute inset-0 bg-black/50 backdrop-blur-sm"
onClick={handleClose}
aria-hidden="true"
/>
{/* Modal */}
<div className="relative w-full max-w-md rounded-2xl border border-(--color-border-light) bg-white p-6 shadow-xl sm:p-8">
{submitted ? (
/* Thank you state */
<div className="flex flex-col items-center gap-4 py-4 text-center">
<div className="flex h-16 w-16 items-center justify-center rounded-full bg-(--color-accent-light) text-3xl">
<i className="fa-solid fa-heart text-(--color-accent)"></i>
</div>
<h2
id="review-modal-title"
className="text-xl font-bold text-foreground"
>
Cảm ơn quý khách
</h2>
<p className="text-(--color-text-muted)">
Chúng tôi trân trọng đánh giá của bạn!
</p>
<button
onClick={handleClose}
className="mt-2 inline-flex items-center justify-center gap-2 rounded-xl bg-(--color-primary) px-6 py-2.5 text-white transition-colors hover:bg-(--color-primary-dark)"
>
Đóng
</button>
</div>
) : (
/* Review form */
<>
<h2
id="review-modal-title"
className="mb-1 text-xl font-bold text-foreground"
>
Đánh giá của bạn
</h2>
<p className="mb-5 text-sm text-(--color-text-muted)">
Hãy cho chúng tôi biết trải nghiệm của bạn hôm nay
</p>
{/* Star rating */}
<div className="mb-5">
<p className="mb-2 text-sm font-medium text-(--color-text-secondary)">
Mức đ hài lòng
</p>
<div
className="flex gap-2"
role="radiogroup"
aria-label="Xếp hạng sao"
>
{[1, 2, 3, 4, 5].map((star) => {
const isActive = star <= (hovered || rating);
return (
<button
key={star}
type="button"
onClick={() => setRating(star)}
onMouseEnter={() => setHovered(star)}
onMouseLeave={() => setHovered(0)}
aria-label={`${star} sao`}
aria-pressed={rating === star}
className="text-3xl transition-transform hover:scale-110 active:scale-95 sm:text-4xl"
>
<i
className={
isActive
? "fa-solid fa-star text-yellow-400"
: "fa-regular fa-star text-(--color-border)"
}
></i>
</button>
);
})}
</div>
{rating > 0 && (
<p className="mt-1.5 text-xs text-(--color-text-muted)">
{
["", "Rất tệ", "Tệ", "Bình thường", "Tốt", "Xuất sắc"][
rating
]
}
</p>
)}
</div>
{/* Review textarea */}
<div className="mb-6">
<label
htmlFor="review-text"
className="mb-2 block text-sm font-medium text-(--color-text-secondary)"
>
Nhận xét (tùy chọn)
</label>
<textarea
id="review-text"
value={review}
onChange={(e) => setReview(e.target.value)}
placeholder="Chia sẻ cảm nhận của bạn về đồ uống, dịch vụ..."
rows={4}
className="w-full resize-none rounded-xl border border-(--color-border) bg-transparent px-3 py-2.5 text-sm text-foreground placeholder:text-(--color-text-muted) focus:border-(--color-primary) focus:outline-none focus:ring-2 focus:ring-(--color-primary)/20 transition-colors"
/>
</div>
{/* Footer buttons */}
<div className="flex gap-3">
<button
type="button"
onClick={handleClose}
className="flex-1 inline-flex items-center justify-center gap-2 rounded-xl border border-(--color-border) px-4 py-2.5 text-sm font-medium text-foreground transition-colors hover:bg-(--color-border-light)"
>
<i className="fa-solid fa-arrow-left"></i>
Quay lại
</button>
<button
type="button"
onClick={handleSubmit}
disabled={rating === 0}
className="flex-1 inline-flex items-center justify-center gap-2 rounded-xl bg-(--color-primary) px-4 py-2.5 text-sm font-medium text-white transition-colors hover:bg-(--color-primary-dark) disabled:cursor-not-allowed disabled:opacity-50"
>
<i className="fa-solid fa-check"></i>
Xác nhận
</button>
</div>
</>
)}
</div>
</div>
);
}
+9226 -738
View File
File diff suppressed because it is too large Load Diff
-1
View File
@@ -15,7 +15,6 @@
"@types/node": "^20.19.37",
"@types/react": "^19.2.14",
"next": "16.1.7",
"php": "^1.1.0",
"react": "19.2.3",
"react-dom": "19.2.3",
"tailwind": "^4.0.0",