Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5dd8af91b2 | |||
| 418489b4fd | |||
| aa83f7488b | |||
| 5c880bd7c6 | |||
| 32b152d40c | |||
| db2e92a90c | |||
| 2913fa0d73 | |||
| 423d4e3436 | |||
| bae229bdf9 | |||
| a21bb8add6 |
@@ -9,6 +9,10 @@ on:
|
|||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
release:
|
release:
|
||||||
permissions:
|
permissions:
|
||||||
@@ -84,24 +88,6 @@ jobs:
|
|||||||
VERSION=$(node -p "require('./package.json').version")
|
VERSION=$(node -p "require('./package.json').version")
|
||||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Commit & Push Changes
|
|
||||||
if: gitea.ref == 'refs/heads/main'
|
|
||||||
run: |
|
|
||||||
if [ -n "$(git status --porcelain)" ]; then
|
|
||||||
git config --global user.name "gitea-actions"
|
|
||||||
git config --global user.email "actions-user@noreply.git.demonkernel.io.vn"
|
|
||||||
|
|
||||||
REPO_NAME="${{ gitea.repository }}"
|
|
||||||
|
|
||||||
git remote set-url origin ssh://git@$TARGET_IP:222/${REPO_NAME}.git
|
|
||||||
|
|
||||||
git rm -r --cached .
|
|
||||||
git add .
|
|
||||||
git commit -m "chore: release [ci skip]"
|
|
||||||
|
|
||||||
git push
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Log in to Gitea Container Registry
|
- name: Log in to Gitea Container Registry
|
||||||
if: gitea.ref == 'refs/heads/main'
|
if: gitea.ref == 'refs/heads/main'
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
@@ -133,3 +119,21 @@ jobs:
|
|||||||
git.demonkernel.io.vn/${{ steps.meta.outputs.repo }}:latest
|
git.demonkernel.io.vn/${{ steps.meta.outputs.repo }}:latest
|
||||||
git.demonkernel.io.vn/${{ steps.meta.outputs.repo }}:${{ steps.meta.outputs.version }}
|
git.demonkernel.io.vn/${{ steps.meta.outputs.repo }}:${{ steps.meta.outputs.version }}
|
||||||
|
|
||||||
|
- name: Commit & Push Changes
|
||||||
|
if: gitea.ref == 'refs/heads/main'
|
||||||
|
run: |
|
||||||
|
if [ -n "$(git status --porcelain)" ]; then
|
||||||
|
git config --global user.name "gitea-actions"
|
||||||
|
git config --global user.email "actions-user@noreply.git.demonkernel.io.vn"
|
||||||
|
|
||||||
|
REPO_NAME="${{ gitea.repository }}"
|
||||||
|
|
||||||
|
git remote set-url origin ssh://git@$TARGET_IP:222/${REPO_NAME}.git
|
||||||
|
|
||||||
|
git rm -r --cached .
|
||||||
|
git add .
|
||||||
|
git commit -m "chore: release [ci skip]"
|
||||||
|
|
||||||
|
git push
|
||||||
|
fi
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -20,7 +20,7 @@
|
|||||||
|
|
||||||
# production
|
# production
|
||||||
/build
|
/build
|
||||||
/release.zip
|
/release*.zip
|
||||||
|
|
||||||
# misc
|
# misc
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|||||||
+156
-120
@@ -1,7 +1,10 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import ReviewModal from "@/components/ReviewModal";
|
||||||
|
import { useAuth } from "@/lib/auth-context";
|
||||||
import { useCart } from "@/lib/cart-context";
|
import { useCart } from "@/lib/cart-context";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
const formatPrice = (value: number) =>
|
const formatPrice = (value: number) =>
|
||||||
value.toLocaleString("vi-VN", { style: "currency", currency: "VND" });
|
value.toLocaleString("vi-VN", { style: "currency", currency: "VND" });
|
||||||
@@ -15,140 +18,173 @@ export default function PaymentPage() {
|
|||||||
removeFromCart,
|
removeFromCart,
|
||||||
setQuantity,
|
setQuantity,
|
||||||
} = useCart();
|
} = 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 (
|
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">
|
<div className="mx-auto w-full max-w-screen-2xl px-4 py-6 md:px-6 md:py-8 lg:px-8">
|
||||||
<section className="min-w-0 flex-1">
|
<div className="flex flex-col gap-6 xl:flex-row">
|
||||||
<div className="bg-card overflow-hidden rounded-2xl border border-(--color-border-light)">
|
<section className="min-w-0 flex-1">
|
||||||
<div className="border-b border-(--color-border-light) px-4 py-3">
|
<div className="bg-card overflow-hidden rounded-2xl border border-(--color-border-light)">
|
||||||
<h1 className="text-foreground text-lg font-bold md:text-xl">
|
<div className="border-b border-(--color-border-light) px-4 py-3">
|
||||||
Trang thanh toán
|
<h1 className="text-foreground text-lg font-bold md:text-xl">
|
||||||
</h1>
|
Trang thanh toán
|
||||||
</div>
|
</h1>
|
||||||
|
|
||||||
{items.length === 0 ? (
|
|
||||||
<div className="px-4 py-10 text-center text-(--color-text-muted)">
|
|
||||||
Chưa có sản phẩm nào trong giỏ hàng.
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
|
||||||
<div className="overflow-x-auto">
|
{items.length === 0 ? (
|
||||||
<table className="w-full min-w-190 text-sm">
|
<div className="px-4 py-10 text-center text-(--color-text-muted)">
|
||||||
<thead>
|
Chưa có sản phẩm nào trong giỏ hàng.
|
||||||
<tr className="bg-(--color-border-light)/40 text-left">
|
</div>
|
||||||
<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>
|
<div className="overflow-x-auto">
|
||||||
<th className="px-4 py-3 font-semibold">Mô tả</th>
|
<table className="w-full min-w-190 text-sm">
|
||||||
<th className="px-4 py-3 font-semibold">Số lượng</th>
|
<thead>
|
||||||
<th className="px-4 py-3 text-right font-semibold">
|
<tr className="bg-(--color-border-light)/40 text-left">
|
||||||
Xóa
|
<th className="px-4 py-3 font-semibold">
|
||||||
</th>
|
Tên sản phẩm
|
||||||
</tr>
|
</th>
|
||||||
</thead>
|
<th className="px-4 py-3 font-semibold">Giá tiền</th>
|
||||||
<tbody>
|
<th className="px-4 py-3 font-semibold">Mô tả</th>
|
||||||
{items.map((item) => (
|
<th className="px-4 py-3 font-semibold">Số lượng</th>
|
||||||
<tr
|
<th className="px-4 py-3 text-right font-semibold">
|
||||||
key={item.id}
|
Xóa
|
||||||
className="border-t border-(--color-border-light)"
|
</th>
|
||||||
>
|
|
||||||
<td className="text-foreground px-4 py-3 font-medium">
|
|
||||||
{item.name}
|
|
||||||
</td>
|
|
||||||
<td className="px-4 py-3 font-semibold text-(--color-primary)">
|
|
||||||
{formatPrice(item.price)}
|
|
||||||
</td>
|
|
||||||
<td className="max-w-70 px-4 py-3 text-(--color-text-muted)">
|
|
||||||
<p className="line-clamp-2">{item.description}</p>
|
|
||||||
</td>
|
|
||||||
<td className="px-4 py-3">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<button
|
|
||||||
onClick={() => decreaseQty(item.id)}
|
|
||||||
className="h-8 w-8 rounded-lg border border-(--color-border) hover:bg-(--color-border-light)"
|
|
||||||
aria-label={`Giảm số lượng ${item.name}`}
|
|
||||||
>
|
|
||||||
-
|
|
||||||
</button>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
min={1}
|
|
||||||
value={item.quantity}
|
|
||||||
onChange={(e) =>
|
|
||||||
setQuantity(item.id, Number(e.target.value))
|
|
||||||
}
|
|
||||||
className="h-8 w-16 rounded-lg border border-(--color-border) bg-transparent text-center"
|
|
||||||
title="Nhập số lượng"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
onClick={() => increaseQty(item.id)}
|
|
||||||
className="h-8 w-8 rounded-lg border border-(--color-border) hover:bg-(--color-border-light)"
|
|
||||||
aria-label={`Tăng số lượng ${item.name}`}
|
|
||||||
>
|
|
||||||
+
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td className="px-4 py-3 text-right">
|
|
||||||
<button
|
|
||||||
onClick={() => removeFromCart(item.id)}
|
|
||||||
className="inline-flex items-center justify-center gap-2 rounded-lg bg-red-500 px-3 py-2 text-white transition-colors hover:bg-red-600"
|
|
||||||
>
|
|
||||||
<i className="fa-solid fa-trash"></i>
|
|
||||||
<span className="hidden lg:inline">
|
|
||||||
Xóa sản phẩm
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
</thead>
|
||||||
</tbody>
|
<tbody>
|
||||||
</table>
|
{items.map((item) => (
|
||||||
</div>
|
<tr
|
||||||
)}
|
key={item.id}
|
||||||
</div>
|
className="border-t border-(--color-border-light)"
|
||||||
</section>
|
>
|
||||||
|
<td className="text-foreground px-4 py-3 font-medium">
|
||||||
<aside className="shrink-0 xl:w-85">
|
{item.name}
|
||||||
<div className="bg-card sticky top-[calc(var(--spacing-header-height)+1rem)] rounded-2xl border border-(--color-border-light) p-4 md:p-5">
|
</td>
|
||||||
<h2 className="mb-4 text-lg font-bold">Hóa đơn</h2>
|
<td className="px-4 py-3 font-semibold text-(--color-primary)">
|
||||||
|
{formatPrice(item.price)}
|
||||||
<div className="flex items-center justify-between border-b border-(--color-border-light) pb-4">
|
</td>
|
||||||
<span className="text-(--color-text-muted)">Tổng cộng</span>
|
<td className="max-w-70 px-4 py-3 text-(--color-text-muted)">
|
||||||
<span className="text-xl font-bold text-(--color-primary)">
|
<p className="line-clamp-2">{item.description}</p>
|
||||||
{formatPrice(totalPrice)}
|
</td>
|
||||||
</span>
|
<td className="px-4 py-3">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() => decreaseQty(item.id)}
|
||||||
|
className="h-8 w-8 rounded-lg border border-(--color-border) hover:bg-(--color-border-light)"
|
||||||
|
aria-label={`Giảm số lượng ${item.name}`}
|
||||||
|
>
|
||||||
|
-
|
||||||
|
</button>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min={1}
|
||||||
|
value={item.quantity}
|
||||||
|
onChange={(e) =>
|
||||||
|
setQuantity(item.id, Number(e.target.value))
|
||||||
|
}
|
||||||
|
className="h-8 w-16 rounded-lg border border-(--color-border) bg-transparent text-center"
|
||||||
|
title="Nhập số lượng"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={() => increaseQty(item.id)}
|
||||||
|
className="h-8 w-8 rounded-lg border border-(--color-border) hover:bg-(--color-border-light)"
|
||||||
|
aria-label={`Tăng số lượng ${item.name}`}
|
||||||
|
>
|
||||||
|
+
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3 text-right">
|
||||||
|
<button
|
||||||
|
onClick={() => removeFromCart(item.id)}
|
||||||
|
className="inline-flex items-center justify-center gap-2 rounded-lg bg-red-500 px-3 py-2 text-white transition-colors hover:bg-red-600"
|
||||||
|
>
|
||||||
|
<i className="fa-solid fa-trash"></i>
|
||||||
|
<span className="hidden lg:inline">
|
||||||
|
Xóa sản phẩm
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<div className="mt-4 grid grid-cols-2 gap-3">
|
<aside className="shrink-0 xl:w-85">
|
||||||
<button
|
<div className="bg-card sticky top-[calc(var(--spacing-header-height)+1rem)] rounded-2xl border border-(--color-border-light) p-4 md:p-5">
|
||||||
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)"
|
<h2 className="mb-4 text-lg font-bold">Hóa đơn</h2>
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
<i className="fa-solid fa-money-bill-wave"></i>
|
|
||||||
<span className="hidden lg:inline">Tiền mặt</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
<div className="flex items-center justify-between border-b border-(--color-border-light) pb-4">
|
||||||
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)"
|
<span className="text-(--color-text-muted)">Tổng cộng</span>
|
||||||
type="button"
|
<span className="text-xl font-bold text-(--color-primary)">
|
||||||
>
|
{formatPrice(totalPrice)}
|
||||||
<i className="fa-solid fa-qrcode"></i>
|
</span>
|
||||||
<span className="hidden lg:inline">QR Code</span>
|
</div>
|
||||||
</button>
|
|
||||||
|
|
||||||
<Link href="/">
|
<div className="mt-4 grid grid-cols-2 gap-3">
|
||||||
<button
|
<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"
|
||||||
|
>
|
||||||
|
<i className="fa-solid fa-money-bill-wave"></i>
|
||||||
|
<span className="hidden lg:inline">Tiền mặt</span>
|
||||||
|
</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)"
|
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"
|
type="button"
|
||||||
>
|
>
|
||||||
<i className="fa-solid fa-arrow-rotate-left"></i>
|
<i className="fa-solid fa-qrcode"></i>
|
||||||
<span className="hidden lg:inline">Quay về</span>
|
<span className="hidden lg:inline">QR Code</span>
|
||||||
</button>
|
</button>
|
||||||
</Link>
|
|
||||||
|
{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>
|
||||||
|
</button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</aside>
|
||||||
</aside>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
<ReviewModal
|
||||||
|
isOpen={isReviewOpen}
|
||||||
|
onClose={() => setIsReviewOpen(false)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+43
-4
@@ -233,9 +233,47 @@ registration flow. Two-step process: phone verification → account creation.
|
|||||||
|
|
||||||
### 4. Payment Page (app/(main)/payment/page.tsx)
|
### 4. Payment Page (app/(main)/payment/page.tsx)
|
||||||
|
|
||||||
**Route:** `/payment` **Type:** Client component (TBD) **Description:**
|
**Route:** `/payment`
|
||||||
Payment/checkout page. Currently a placeholder; implement when cart and order
|
**Type:** Client component
|
||||||
system are ready.
|
**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
|
## Future Enhancements
|
||||||
|
|
||||||
- [ ] Payment page implementation
|
- [x] Payment page implementation
|
||||||
|
- [x] Customer review modal (ReviewModal) with 5-star rating + textarea
|
||||||
- [ ] Order history/tracking page
|
- [ ] Order history/tracking page
|
||||||
- [ ] Manager dashboard (menu management, order tracking)
|
- [ ] Manager dashboard (menu management, order tracking)
|
||||||
- [ ] User profile page
|
- [ ] User profile page
|
||||||
|
|||||||
@@ -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
|
# Contexts Documentation
|
||||||
|
|
||||||
## AuthContext (lib/auth-context.tsx)
|
## AuthContext (lib/auth-context.tsx)
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
+1
-2
@@ -19,8 +19,7 @@ COPY . .
|
|||||||
# Tắt dữ liệu thu thập của Next.js trong quá trình build
|
# Tắt dữ liệu thu thập của Next.js trong quá trình build
|
||||||
ENV NEXT_TELEMETRY_DISABLED 1
|
ENV NEXT_TELEMETRY_DISABLED 1
|
||||||
|
|
||||||
RUN npm install -g pnpm
|
RUN npm run build
|
||||||
RUN pnpm run build
|
|
||||||
|
|
||||||
# --- Giai đoạn 3: Runner (Sản phẩm cuối) ---
|
# --- Giai đoạn 3: Runner (Sản phẩm cuối) ---
|
||||||
FROM node:25-alpine AS runner
|
FROM node:25-alpine AS runner
|
||||||
|
|||||||
+1
-1
@@ -10,7 +10,7 @@ import type { NextConfig } from "next";
|
|||||||
* Docs: https://nextjs.org/docs/app/api-reference/next-config-js
|
* Docs: https://nextjs.org/docs/app/api-reference/next-config-js
|
||||||
*/
|
*/
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
output: 'standalone',
|
output: "standalone",
|
||||||
images: {
|
images: {
|
||||||
remotePatterns: [
|
remotePatterns: [
|
||||||
{
|
{
|
||||||
|
|||||||
Generated
+9230
-742
File diff suppressed because it is too large
Load Diff
+1
-2
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "temp",
|
"name": "temp",
|
||||||
"version": "1.0.7",
|
"version": "1.0.9",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
@@ -15,7 +15,6 @@
|
|||||||
"@types/node": "^20.19.37",
|
"@types/node": "^20.19.37",
|
||||||
"@types/react": "^19.2.14",
|
"@types/react": "^19.2.14",
|
||||||
"next": "16.1.7",
|
"next": "16.1.7",
|
||||||
"php": "^1.1.0",
|
|
||||||
"react": "19.2.3",
|
"react": "19.2.3",
|
||||||
"react-dom": "19.2.3",
|
"react-dom": "19.2.3",
|
||||||
"tailwind": "^4.0.0",
|
"tailwind": "^4.0.0",
|
||||||
|
|||||||
+2
-1
@@ -18,7 +18,8 @@ const config: GlobalConfig = {
|
|||||||
[
|
[
|
||||||
"@semantic-release/exec",
|
"@semantic-release/exec",
|
||||||
{
|
{
|
||||||
prepareCmd: "node scripts/release.ts ${nextRelease.version}",
|
prepareCmd:
|
||||||
|
"node scripts/release.ts ${nextRelease.version} && sed -i 's|image: git.demonkernel.io.vn/foodsurf/frontend:.*|image: git.demonkernel.io.vn/foodsurf/frontend:${nextRelease.version}|g' k8s.yaml",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
|
|||||||
+11
-7
@@ -7,13 +7,17 @@ if (!version) {
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const zipPath = `release-v${version}.zip`; // Thêm version vào tên file cho dễ quản lý
|
const zipPath = `release.zip`;
|
||||||
|
|
||||||
console.log(`📦 Đang chuẩn bị đóng gói App Router Standalone cho version: ${version}...`);
|
console.log(
|
||||||
|
`📦 Đang chuẩn bị đóng gói App Router Standalone cho version: ${version}...`,
|
||||||
|
);
|
||||||
|
|
||||||
// Kiểm tra xem đã build chưa
|
// Kiểm tra xem đã build chưa
|
||||||
if (!fs.existsSync(".next/standalone")) {
|
if (!fs.existsSync(".next/standalone")) {
|
||||||
console.error("❌ Thư mục .next/standalone không tồn tại. Hãy chạy 'pnpm build' trước!");
|
console.error(
|
||||||
|
"❌ Thư mục .next/standalone không tồn tại. Hãy chạy 'pnpm build' trước!",
|
||||||
|
);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,10 +31,10 @@ if (!fs.existsSync(".next/standalone")) {
|
|||||||
try {
|
try {
|
||||||
// Tạo thư mục tạm để cấu trúc lại file trước khi nén
|
// Tạo thư mục tạm để cấu trúc lại file trước khi nén
|
||||||
execSync("rm -rf temp_release && mkdir -p temp_release");
|
execSync("rm -rf temp_release && mkdir -p temp_release");
|
||||||
|
|
||||||
console.log("🚚 Đang sao chép các file standalone...");
|
console.log("🚚 Đang sao chép các file standalone...");
|
||||||
execSync("cp -r .next/standalone/. temp_release/");
|
execSync("cp -r .next/standalone/. temp_release/");
|
||||||
|
|
||||||
console.log("🚚 Đang sao chép static và public...");
|
console.log("🚚 Đang sao chép static và public...");
|
||||||
execSync("mkdir -p temp_release/.next/static");
|
execSync("mkdir -p temp_release/.next/static");
|
||||||
execSync("cp -r .next/static/. temp_release/.next/static/");
|
execSync("cp -r .next/static/. temp_release/.next/static/");
|
||||||
@@ -44,9 +48,9 @@ try {
|
|||||||
execSync("rm -rf temp_release");
|
execSync("rm -rf temp_release");
|
||||||
|
|
||||||
console.log(`✅ Đã tạo file thành công: ${zipPath}`);
|
console.log(`✅ Đã tạo file thành công: ${zipPath}`);
|
||||||
|
|
||||||
execSync("pnpm format");
|
execSync("pnpm format");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("❌ Có lỗi xảy ra trong quá trình nén:", error.message);
|
console.error("❌ Có lỗi xảy ra trong quá trình nén:", error.message);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user