Compare commits

...

13 Commits

Author SHA1 Message Date
TakahashiNguyen 5dd8af91b2 fix: better k8s yaml replacement (#26)
Release package / release (push) Successful in 1h2m3s
Reviewed-on: #26
2026-03-31 11:33:35 +00:00
TaNguyenThanhQuy 418489b4fd Merge pull request 'Created User Review' (#25) from develop into main
Release package / release (push) Has started running
Reviewed-on: #25
2026-03-31 11:03:53 +00:00
Thanh Quy - wolf aa83f7488b Fix conflict develop
Release package / release (pull_request) Successful in 12m42s
2026-03-31 17:37:35 +07:00
gitea-actions 5c880bd7c6 chore: release [ci skip] 2026-03-31 10:15:34 +00:00
TakahashiNguyen 32b152d40c chore: update release script (#21)
Release package / release (push) Successful in 26m17s
Reviewed-on: #21
2026-03-31 09:50:29 +00:00
gitea-actions db2e92a90c chore: release [ci skip] 2026-03-31 08:16:50 +00:00
TakahashiNguyen 2913fa0d73 fix: better ci and docker build (#20)
Release package / release (push) Successful in 28m17s
Co-authored-by: TakahashiNg <83152264+TakahashiNguyen@users.noreply.github.com>
Reviewed-on: #20
2026-03-31 07:52:42 +00:00
gitea-actions 423d4e3436 chore: release [ci skip] 2026-03-31 07:21:05 +00:00
TakahashiNguyen ddaa1ad873 fix: change from static execute to dynamic execute (#19)
Release package / release (push) Failing after 24m2s
Reviewed-on: #19
2026-03-31 07:16:53 +00:00
gitea-actions ee137f66f0 chore: release [ci skip] 2026-03-31 05:22:38 +00:00
TakahashiNguyen 9a488df385 fix: better release config (#18)
Release package / release (push) Successful in 10m28s
Reviewed-on: #18
2026-03-31 05:19:37 +00:00
Thanh Quy - wolf bae229bdf9 Add feauture review with only user is customer 2026-03-30 12:32:29 +07:00
Thanh Quy - wolf a21bb8add6 Delete package php 2026-03-30 12:09:21 +07:00
15 changed files with 9879 additions and 1014 deletions
+26 -19
View File
@@ -4,7 +4,14 @@ on:
push:
branches:
- main
- dev-*
pull_request:
branches:
- main
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
release:
@@ -81,24 +88,6 @@ jobs:
VERSION=$(node -p "require('./package.json').version")
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
if: gitea.ref == 'refs/heads/main'
uses: docker/login-action@v3
@@ -130,3 +119,21 @@ jobs:
git.demonkernel.io.vn/${{ steps.meta.outputs.repo }}:latest
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
View File
@@ -20,7 +20,7 @@
# production
/build
/release.zip
/release*.zip
# misc
.DS_Store
+156 -120
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,140 +18,173 @@ 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">
<div className="bg-card overflow-hidden rounded-2xl border border-(--color-border-light)">
<div className="border-b border-(--color-border-light) px-4 py-3">
<h1 className="text-foreground text-lg font-bold md:text-xl">
Trang thanh toán
</h1>
</div>
{items.length === 0 ? (
<div className="px-4 py-10 text-center text-(--color-text-muted)">
Chưa sản phẩm nào trong giỏ hàng.
<>
<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">
<div className="bg-card overflow-hidden rounded-2xl border border-(--color-border-light)">
<div className="border-b border-(--color-border-light) px-4 py-3">
<h1 className="text-foreground text-lg font-bold md:text-xl">
Trang thanh toán
</h1>
</div>
) : (
<div className="overflow-x-auto">
<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">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>
<th className="px-4 py-3 text-right font-semibold">
Xóa
</th>
</tr>
</thead>
<tbody>
{items.map((item) => (
<tr
key={item.id}
className="border-t border-(--color-border-light)"
>
<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>
{items.length === 0 ? (
<div className="px-4 py-10 text-center text-(--color-text-muted)">
Chưa sản phẩm nào trong giỏ hàng.
</div>
) : (
<div className="overflow-x-auto">
<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">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>
<th className="px-4 py-3 text-right font-semibold">
Xóa
</th>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
</section>
<aside className="shrink-0 xl:w-85">
<div className="bg-card sticky top-[calc(var(--spacing-header-height)+1rem)] rounded-2xl border border-(--color-border-light) p-4 md:p-5">
<h2 className="mb-4 text-lg font-bold">Hóa đơn</h2>
<div className="flex items-center justify-between border-b border-(--color-border-light) pb-4">
<span className="text-(--color-text-muted)">Tổng cộng</span>
<span className="text-xl font-bold text-(--color-primary)">
{formatPrice(totalPrice)}
</span>
</thead>
<tbody>
{items.map((item) => (
<tr
key={item.id}
className="border-t border-(--color-border-light)"
>
<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>
))}
</tbody>
</table>
</div>
)}
</div>
</section>
<div className="mt-4 grid grid-cols-2 gap-3">
<button
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>
<aside className="shrink-0 xl:w-85">
<div className="bg-card sticky top-[calc(var(--spacing-header-height)+1rem)] rounded-2xl border border-(--color-border-light) p-4 md:p-5">
<h2 className="mb-4 text-lg font-bold">Hóa đơn</h2>
<button
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-qrcode"></i>
<span className="hidden lg:inline">QR Code</span>
</button>
<div className="flex items-center justify-between border-b border-(--color-border-light) pb-4">
<span className="text-(--color-text-muted)">Tổng cộng</span>
<span className="text-xl font-bold text-(--color-primary)">
{formatPrice(totalPrice)}
</span>
</div>
<Link href="/">
<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"
>
<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)"
type="button"
>
<i className="fa-solid fa-arrow-rotate-left"></i>
<span className="hidden lg:inline">Quay về</span>
<i className="fa-solid fa-qrcode"></i>
<span className="hidden lg:inline">QR Code</span>
</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>
</aside>
</aside>
</div>
</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>
);
}
+34 -19
View File
@@ -1,32 +1,47 @@
# --- Giai đoạn 1: Build ---
FROM node:25-alpine AS builder
# --- Giai đoạn 1: Dependencies ---
FROM node:25-alpine AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app
# Cài đặt pnpm
RUN npm install -g pnpm
WORKDIR /app
# Copy file định nghĩa package
# Copy file định nghĩa package để tận dụng cache của Docker
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
# Cài đặt dependencies (sử dụng --frozen-lockfile để đảm bảo đúng phiên bản)
RUN pnpm install --prod --frozen-lockfile
# Copy toàn bộ code
# --- Giai đoạn 2: Builder ---
FROM node:25-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# Build Next.js (Yêu cầu next.config.js có output: 'export')
RUN pnpm run build
# Tắt dữ liệu thu thập của Next.js trong quá trình build
ENV NEXT_TELEMETRY_DISABLED 1
# --- Giai đoạn 2: Run (Sản phẩm cuối) ---
FROM nginx:alpine
RUN npm run build
# Xóa file mặc định của nginx (tùy chọn nhưng nên làm)
RUN rm -rf /usr/share/nginx/html/*
# --- Giai đoạn 3: Runner (Sản phẩm cuối) ---
FROM node:25-alpine AS runner
WORKDIR /app
# Copy folder out từ giai đoạn builder
COPY --from=builder /app/out /usr/share/nginx/html
ENV NODE_ENV production
ENV NEXT_TELEMETRY_DISABLED 1
EXPOSE 80
# Tạo user để chạy app (bảo mật hơn chạy root)
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
CMD ["nginx", "-g", "daemon off;"]
# Copy các file cần thiết từ builder
# Standalone chỉ copy code cần chạy, bỏ qua devDependencies và file thừa
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT 3000
# Chạy bằng server.js được sinh ra bởi output: 'standalone'
CMD ["node", "server.js"]
+13 -8
View File
@@ -16,14 +16,19 @@ spec:
spec:
containers:
- name: frontend-container
image: git.demonkernel.io.vn/foodsurf/frontend:1.0.6
image: git.demonkernel.io.vn/foodsurf/frontend:latest
ports:
- containerPort: 3000
resources:
limits:
cpu: "50m"
memory: "32Mi"
cpu: "200m"
memory: "256Mi"
requests:
cpu: "10m"
memory: "16Mi"
cpu: "50m"
memory: "128Mi"
env:
- name: NODE_ENV
value: "production"
---
apiVersion: v1
kind: Service
@@ -35,6 +40,6 @@ spec:
app: web
ports:
- protocol: TCP
port: 80
targetPort: 80
nodePort: 30080
port: 80
targetPort: 3000
nodePort: 30080
+1 -1
View File
@@ -10,7 +10,7 @@ import type { NextConfig } from "next";
* Docs: https://nextjs.org/docs/app/api-reference/next-config-js
*/
const nextConfig: NextConfig = {
output: "export",
output: "standalone",
images: {
remotePatterns: [
{
+9230 -742
View File
File diff suppressed because it is too large Load Diff
+1 -2
View File
@@ -1,6 +1,6 @@
{
"name": "temp",
"version": "1.0.6",
"version": "1.0.9",
"private": true,
"scripts": {
"dev": "next dev",
@@ -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",
+89 -89
View File
@@ -59,7 +59,7 @@ importers:
version: 9.39.4(jiti@2.6.1)
eslint-config-next:
specifier: 16.1.7
version: 16.1.7(@typescript-eslint/parser@8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)
version: 16.1.7(@typescript-eslint/parser@8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)
prettier:
specifier: ^3.8.1
version: 3.8.1
@@ -769,63 +769,63 @@ packages:
'@types/responselike@1.0.3':
resolution: {integrity: sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==}
'@typescript-eslint/eslint-plugin@8.57.2':
resolution: {integrity: sha512-NZZgp0Fm2IkD+La5PR81sd+g+8oS6JwJje+aRWsDocxHkjyRw0J5L5ZTlN3LI1LlOcGL7ph3eaIUmTXMIjLk0w==}
'@typescript-eslint/eslint-plugin@8.58.0':
resolution: {integrity: sha512-RLkVSiNuUP1C2ROIWfqX+YcUfLaSnxGE/8M+Y57lopVwg9VTYYfhuz15Yf1IzCKgZj6/rIbYTmJCUSqr76r0Wg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
'@typescript-eslint/parser': ^8.57.2
'@typescript-eslint/parser': ^8.58.0
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: '>=4.8.4 <6.0.0'
typescript: '>=4.8.4 <6.1.0'
'@typescript-eslint/parser@8.57.2':
resolution: {integrity: sha512-30ScMRHIAD33JJQkgfGW1t8CURZtjc2JpTrq5n2HFhOefbAhb7ucc7xJwdWcrEtqUIYJ73Nybpsggii6GtAHjA==}
'@typescript-eslint/parser@8.58.0':
resolution: {integrity: sha512-rLoGZIf9afaRBYsPUMtvkDWykwXwUPL60HebR4JgTI8mxfFe2cQTu3AGitANp4b9B2QlVru6WzjgB2IzJKiCSA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: '>=4.8.4 <6.0.0'
typescript: '>=4.8.4 <6.1.0'
'@typescript-eslint/project-service@8.57.2':
resolution: {integrity: sha512-FuH0wipFywXRTHf+bTTjNyuNQQsQC3qh/dYzaM4I4W0jrCqjCVuUh99+xd9KamUfmCGPvbO8NDngo/vsnNVqgw==}
'@typescript-eslint/project-service@8.58.0':
resolution: {integrity: sha512-8Q/wBPWLQP1j16NxoPNIKpDZFMaxl7yWIoqXWYeWO+Bbd2mjgvoF0dxP2jKZg5+x49rgKdf7Ck473M8PC3V9lg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.0.0'
typescript: '>=4.8.4 <6.1.0'
'@typescript-eslint/scope-manager@8.57.2':
resolution: {integrity: sha512-snZKH+W4WbWkrBqj4gUNRIGb/jipDW3qMqVJ4C9rzdFc+wLwruxk+2a5D+uoFcKPAqyqEnSb4l2ULuZf95eSkw==}
'@typescript-eslint/scope-manager@8.58.0':
resolution: {integrity: sha512-W1Lur1oF50FxSnNdGp3Vs6P+yBRSmZiw4IIjEeYxd8UQJwhUF0gDgDD/W/Tgmh73mxgEU3qX0Bzdl/NGuSPEpQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@typescript-eslint/tsconfig-utils@8.57.2':
resolution: {integrity: sha512-3Lm5DSM+DCowsUOJC+YqHHnKEfFh5CoGkj5Z31NQSNF4l5wdOwqGn99wmwN/LImhfY3KJnmordBq/4+VDe2eKw==}
'@typescript-eslint/tsconfig-utils@8.58.0':
resolution: {integrity: sha512-doNSZEVJsWEu4htiVC+PR6NpM+pa+a4ClH9INRWOWCUzMst/VA9c4gXq92F8GUD1rwhNvRLkgjfYtFXegXQF7A==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.0.0'
typescript: '>=4.8.4 <6.1.0'
'@typescript-eslint/type-utils@8.57.2':
resolution: {integrity: sha512-Co6ZCShm6kIbAM/s+oYVpKFfW7LBc6FXoPXjTRQ449PPNBY8U0KZXuevz5IFuuUj2H9ss40atTaf9dlGLzbWZg==}
'@typescript-eslint/type-utils@8.58.0':
resolution: {integrity: sha512-aGsCQImkDIqMyx1u4PrVlbi/krmDsQUs4zAcCV6M7yPcPev+RqVlndsJy9kJ8TLihW9TZ0kbDAzctpLn5o+lOg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: '>=4.8.4 <6.0.0'
typescript: '>=4.8.4 <6.1.0'
'@typescript-eslint/types@8.57.2':
resolution: {integrity: sha512-/iZM6FnM4tnx9csuTxspMW4BOSegshwX5oBDznJ7S4WggL7Vczz5d2W11ecc4vRrQMQHXRSxzrCsyG5EsPPTbA==}
'@typescript-eslint/types@8.58.0':
resolution: {integrity: sha512-O9CjxypDT89fbHxRfETNoAnHj/i6IpRK0CvbVN3qibxlLdo5p5hcLmUuCCrHMpxiWSwKyI8mCP7qRNYuOJ0Uww==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@typescript-eslint/typescript-estree@8.57.2':
resolution: {integrity: sha512-2MKM+I6g8tJxfSmFKOnHv2t8Sk3T6rF20A1Puk0svLK+uVapDZB/4pfAeB7nE83uAZrU6OxW+HmOd5wHVdXwXA==}
'@typescript-eslint/typescript-estree@8.58.0':
resolution: {integrity: sha512-7vv5UWbHqew/dvs+D3e1RvLv1v2eeZ9txRHPnEEBUgSNLx5ghdzjHa0sgLWYVKssH+lYmV0JaWdoubo0ncGYLA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.0.0'
typescript: '>=4.8.4 <6.1.0'
'@typescript-eslint/utils@8.57.2':
resolution: {integrity: sha512-krRIbvPK1ju1WBKIefiX+bngPs+odIQUtR7kymzPfo1POVw3jlF+nLkmexdSSd4UCbDcQn+wMBATOOmpBbqgKg==}
'@typescript-eslint/utils@8.58.0':
resolution: {integrity: sha512-RfeSqcFeHMHlAWzt4TBjWOAtoW9lnsAGiP3GbaX9uVgTYYrMbVnGONEfUCiSss+xMHFl+eHZiipmA8WkQ7FuNA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: '>=4.8.4 <6.0.0'
typescript: '>=4.8.4 <6.1.0'
'@typescript-eslint/visitor-keys@8.57.2':
resolution: {integrity: sha512-zhahknjobV2FiD6Ee9iLbS7OV9zi10rG26odsQdfBO/hjSzUQbkIYgda+iNKK1zNiW2ey+Lf8MU5btN17V3dUw==}
'@typescript-eslint/visitor-keys@8.58.0':
resolution: {integrity: sha512-XJ9UD9+bbDo4a4epraTwG3TsNPeiB9aShrUneAVXy8q4LuwowN+qu89/6ByLMINqvIMeI9H9hOHQtg/ijrYXzQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@unrs/resolver-binding-android-arm-eabi@1.11.1':
@@ -1466,8 +1466,8 @@ packages:
ee-first@1.1.1:
resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
electron-to-chromium@1.5.328:
resolution: {integrity: sha512-QNQ5l45DzYytThO21403XN3FvK0hOkWDG8viNf6jqS42msJ8I4tGDSpBCgvDRRPnkffafiwAym2X2eHeGD2V0w==}
electron-to-chromium@1.5.329:
resolution: {integrity: sha512-/4t+AS1l4S3ZC0Ja7PHFIWeBIxGA3QGqV8/yKsP36v7NcyUCl+bIcmw6s5zVuMIECWwBrAK/6QLzTmbJChBboQ==}
emoji-regex@10.6.0:
resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==}
@@ -2578,8 +2578,8 @@ packages:
resolution: {integrity: sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==}
engines: {node: '>=8'}
minimatch@10.2.4:
resolution: {integrity: sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==}
minimatch@10.2.5:
resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==}
engines: {node: 18 || 20 || >=22}
minimatch@3.1.5:
@@ -3622,12 +3622,12 @@ packages:
resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==}
engines: {node: '>= 0.4'}
typescript-eslint@8.57.2:
resolution: {integrity: sha512-VEPQ0iPgWO/sBaZOU1xo4nuNdODVOajPnTIbog2GKYr31nIlZ0fWPoCQgGfF3ETyBl1vn63F/p50Um9Z4J8O8A==}
typescript-eslint@8.58.0:
resolution: {integrity: sha512-e2TQzKfaI85fO+F3QywtX+tCTsu/D3WW5LVU6nz8hTFKFZ8yBJ6mSYRpXqdR3mFjPWmO0eWsTa5f+UpAOe/FMA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: '>=4.8.4 <6.0.0'
typescript: '>=4.8.4 <6.1.0'
typescript@5.9.3:
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
@@ -4528,7 +4528,7 @@ snapshots:
'@types/minimatch@6.0.0':
dependencies:
minimatch: 10.2.4
minimatch: 10.2.5
'@types/node@20.19.37':
dependencies:
@@ -4548,14 +4548,14 @@ snapshots:
dependencies:
'@types/node': 20.19.37
'@typescript-eslint/eslint-plugin@8.57.2(@typescript-eslint/parser@8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)':
'@typescript-eslint/eslint-plugin@8.58.0(@typescript-eslint/parser@8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)':
dependencies:
'@eslint-community/regexpp': 4.12.2
'@typescript-eslint/parser': 8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)
'@typescript-eslint/scope-manager': 8.57.2
'@typescript-eslint/type-utils': 8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)
'@typescript-eslint/utils': 8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)
'@typescript-eslint/visitor-keys': 8.57.2
'@typescript-eslint/parser': 8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)
'@typescript-eslint/scope-manager': 8.58.0
'@typescript-eslint/type-utils': 8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)
'@typescript-eslint/utils': 8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)
'@typescript-eslint/visitor-keys': 8.58.0
eslint: 9.39.4(jiti@2.6.1)
ignore: 7.0.5
natural-compare: 1.4.0
@@ -4564,41 +4564,41 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@typescript-eslint/parser@8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)':
'@typescript-eslint/parser@8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)':
dependencies:
'@typescript-eslint/scope-manager': 8.57.2
'@typescript-eslint/types': 8.57.2
'@typescript-eslint/typescript-estree': 8.57.2(typescript@5.9.3)
'@typescript-eslint/visitor-keys': 8.57.2
'@typescript-eslint/scope-manager': 8.58.0
'@typescript-eslint/types': 8.58.0
'@typescript-eslint/typescript-estree': 8.58.0(typescript@5.9.3)
'@typescript-eslint/visitor-keys': 8.58.0
debug: 4.4.3
eslint: 9.39.4(jiti@2.6.1)
typescript: 5.9.3
transitivePeerDependencies:
- supports-color
'@typescript-eslint/project-service@8.57.2(typescript@5.9.3)':
'@typescript-eslint/project-service@8.58.0(typescript@5.9.3)':
dependencies:
'@typescript-eslint/tsconfig-utils': 8.57.2(typescript@5.9.3)
'@typescript-eslint/types': 8.57.2
'@typescript-eslint/tsconfig-utils': 8.58.0(typescript@5.9.3)
'@typescript-eslint/types': 8.58.0
debug: 4.4.3
typescript: 5.9.3
transitivePeerDependencies:
- supports-color
'@typescript-eslint/scope-manager@8.57.2':
'@typescript-eslint/scope-manager@8.58.0':
dependencies:
'@typescript-eslint/types': 8.57.2
'@typescript-eslint/visitor-keys': 8.57.2
'@typescript-eslint/types': 8.58.0
'@typescript-eslint/visitor-keys': 8.58.0
'@typescript-eslint/tsconfig-utils@8.57.2(typescript@5.9.3)':
'@typescript-eslint/tsconfig-utils@8.58.0(typescript@5.9.3)':
dependencies:
typescript: 5.9.3
'@typescript-eslint/type-utils@8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)':
'@typescript-eslint/type-utils@8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)':
dependencies:
'@typescript-eslint/types': 8.57.2
'@typescript-eslint/typescript-estree': 8.57.2(typescript@5.9.3)
'@typescript-eslint/utils': 8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)
'@typescript-eslint/types': 8.58.0
'@typescript-eslint/typescript-estree': 8.58.0(typescript@5.9.3)
'@typescript-eslint/utils': 8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)
debug: 4.4.3
eslint: 9.39.4(jiti@2.6.1)
ts-api-utils: 2.5.0(typescript@5.9.3)
@@ -4606,16 +4606,16 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@typescript-eslint/types@8.57.2': {}
'@typescript-eslint/types@8.58.0': {}
'@typescript-eslint/typescript-estree@8.57.2(typescript@5.9.3)':
'@typescript-eslint/typescript-estree@8.58.0(typescript@5.9.3)':
dependencies:
'@typescript-eslint/project-service': 8.57.2(typescript@5.9.3)
'@typescript-eslint/tsconfig-utils': 8.57.2(typescript@5.9.3)
'@typescript-eslint/types': 8.57.2
'@typescript-eslint/visitor-keys': 8.57.2
'@typescript-eslint/project-service': 8.58.0(typescript@5.9.3)
'@typescript-eslint/tsconfig-utils': 8.58.0(typescript@5.9.3)
'@typescript-eslint/types': 8.58.0
'@typescript-eslint/visitor-keys': 8.58.0
debug: 4.4.3
minimatch: 10.2.4
minimatch: 10.2.5
semver: 7.7.4
tinyglobby: 0.2.15
ts-api-utils: 2.5.0(typescript@5.9.3)
@@ -4623,20 +4623,20 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@typescript-eslint/utils@8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)':
'@typescript-eslint/utils@8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)':
dependencies:
'@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.6.1))
'@typescript-eslint/scope-manager': 8.57.2
'@typescript-eslint/types': 8.57.2
'@typescript-eslint/typescript-estree': 8.57.2(typescript@5.9.3)
'@typescript-eslint/scope-manager': 8.58.0
'@typescript-eslint/types': 8.58.0
'@typescript-eslint/typescript-estree': 8.58.0(typescript@5.9.3)
eslint: 9.39.4(jiti@2.6.1)
typescript: 5.9.3
transitivePeerDependencies:
- supports-color
'@typescript-eslint/visitor-keys@8.57.2':
'@typescript-eslint/visitor-keys@8.58.0':
dependencies:
'@typescript-eslint/types': 8.57.2
'@typescript-eslint/types': 8.58.0
eslint-visitor-keys: 5.0.1
'@unrs/resolver-binding-android-arm-eabi@1.11.1':
@@ -4938,7 +4938,7 @@ snapshots:
dependencies:
baseline-browser-mapping: 2.10.12
caniuse-lite: 1.0.30001782
electron-to-chromium: 1.5.328
electron-to-chromium: 1.5.329
node-releases: 2.0.36
update-browserslist-db: 1.2.3(browserslist@4.28.1)
@@ -5267,7 +5267,7 @@ snapshots:
ee-first@1.1.1: {}
electron-to-chromium@1.5.328: {}
electron-to-chromium@1.5.329: {}
emoji-regex@10.6.0: {}
@@ -5415,18 +5415,18 @@ snapshots:
escape-string-regexp@5.0.0: {}
eslint-config-next@16.1.7(@typescript-eslint/parser@8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3):
eslint-config-next@16.1.7(@typescript-eslint/parser@8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3):
dependencies:
'@next/eslint-plugin-next': 16.1.7
eslint: 9.39.4(jiti@2.6.1)
eslint-import-resolver-node: 0.3.9
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.6.1))
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1))
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1))
eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.4(jiti@2.6.1))
eslint-plugin-react: 7.37.5(eslint@9.39.4(jiti@2.6.1))
eslint-plugin-react-hooks: 7.0.1(eslint@9.39.4(jiti@2.6.1))
globals: 16.4.0
typescript-eslint: 8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)
typescript-eslint: 8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)
optionalDependencies:
typescript: 5.9.3
transitivePeerDependencies:
@@ -5454,22 +5454,22 @@ snapshots:
tinyglobby: 0.2.15
unrs-resolver: 1.11.1
optionalDependencies:
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1))
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1))
transitivePeerDependencies:
- supports-color
eslint-module-utils@2.12.1(@typescript-eslint/parser@8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1)):
eslint-module-utils@2.12.1(@typescript-eslint/parser@8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1)):
dependencies:
debug: 3.2.7
optionalDependencies:
'@typescript-eslint/parser': 8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)
'@typescript-eslint/parser': 8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)
eslint: 9.39.4(jiti@2.6.1)
eslint-import-resolver-node: 0.3.9
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.6.1))
transitivePeerDependencies:
- supports-color
eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1)):
eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1)):
dependencies:
'@rtsao/scc': 1.1.0
array-includes: 3.1.9
@@ -5480,7 +5480,7 @@ snapshots:
doctrine: 2.1.0
eslint: 9.39.4(jiti@2.6.1)
eslint-import-resolver-node: 0.3.9
eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1))
eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1))
hasown: 2.0.2
is-core-module: 2.16.1
is-glob: 4.0.3
@@ -5492,7 +5492,7 @@ snapshots:
string.prototype.trimend: 1.0.9
tsconfig-paths: 3.15.0
optionalDependencies:
'@typescript-eslint/parser': 8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)
'@typescript-eslint/parser': 8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)
transitivePeerDependencies:
- eslint-import-resolver-typescript
- eslint-import-resolver-webpack
@@ -6540,7 +6540,7 @@ snapshots:
mimic-response@2.1.0: {}
minimatch@10.2.4:
minimatch@10.2.5:
dependencies:
brace-expansion: 5.0.5
@@ -7613,12 +7613,12 @@ snapshots:
possible-typed-array-names: 1.1.0
reflect.getprototypeof: 1.0.10
typescript-eslint@8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3):
typescript-eslint@8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3):
dependencies:
'@typescript-eslint/eslint-plugin': 8.57.2(@typescript-eslint/parser@8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)
'@typescript-eslint/parser': 8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)
'@typescript-eslint/typescript-estree': 8.57.2(typescript@5.9.3)
'@typescript-eslint/utils': 8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)
'@typescript-eslint/eslint-plugin': 8.58.0(@typescript-eslint/parser@8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)
'@typescript-eslint/parser': 8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)
'@typescript-eslint/typescript-estree': 8.58.0(typescript@5.9.3)
'@typescript-eslint/utils': 8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)
eslint: 9.39.4(jiti@2.6.1)
typescript: 5.9.3
transitivePeerDependencies:
+1 -1
View File
@@ -19,7 +19,7 @@ const config: GlobalConfig = {
"@semantic-release/exec",
{
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/k8s.yaml",
"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",
},
],
[
+44 -7
View File
@@ -1,4 +1,5 @@
import { execSync } from "node:child_process";
import fs from "node:fs";
const version = process.argv[2];
if (!version) {
@@ -6,14 +7,50 @@ if (!version) {
process.exit(1);
}
const zipPath = "release.zip";
const zipPath = `release.zip`;
console.log(`📦 Đang nén file cho version: ${version}...`);
console.log(
`📦 Đang chuẩn bị đóng gói App Router Standalone cho version: ${version}...`,
);
execSync(`cd out && zip -r ../${zipPath} .`, {
stdio: "inherit",
});
// Kiểm tra xem đã build chưa
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!",
);
process.exit(1);
}
console.log(`✅ Đã tạo file zip: ${zipPath}`);
/**
* Với Next.js Standalone, ta cần:
* 1. Nội dung trong .next/standalone (copy ra ngoài hoặc nén trực tiếp)
* 2. .next/static (phải nằm trong .next/static của gói release)
* 3. public (phải nằm trong public của gói release)
*/
execSync("pnpm format");
try {
// 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");
console.log("🚚 Đang sao chép các file standalone...");
execSync("cp -r .next/standalone/. temp_release/");
console.log("🚚 Đang sao chép static và public...");
execSync("mkdir -p temp_release/.next/static");
execSync("cp -r .next/static/. temp_release/.next/static/");
execSync("cp -r public temp_release/ 2>/dev/null || : "); // Bỏ qua nếu không có thư mục public
console.log("🗜️ Đang nén thành file zip...");
// Di chuyển vào temp_release để nén các file bên trong nó
execSync(`cd temp_release && zip -r ../${zipPath} .`, { stdio: "inherit" });
// Xóa thư mục tạm
execSync("rm -rf temp_release");
console.log(`✅ Đã tạo file thành công: ${zipPath}`);
execSync("pnpm format");
} catch (error) {
console.error("❌ Có lỗi xảy ra trong quá trình nén:", error.message);
process.exit(1);
}
+1 -1
View File
@@ -31,5 +31,5 @@
".next/dev/types/**/*.ts",
"types/**/*.d.ts"
],
"exclude": ["node_modules", "script", "*.ts"]
"exclude": ["node_modules", "scripts", "*.ts"]
}