diff --git a/TODO.md b/TODO.md
index cb9d54c..63862fc 100644
--- a/TODO.md
+++ b/TODO.md
@@ -1,103 +1,10 @@
-# Coffee Shop Frontend - TODO
+# TODO - Add image upload field for manager add/update menu item
-## Completed Features & Implementations
-
-### A. Dead Code Removed
-
-- [x] lib/constants.ts - Removed unused NAV_LINKS export
-- [x] lib/types.ts - Removed unused NavLink interface
-- [x] components/Navbar.tsx - Removed trivial handleClick wrapper; inlined
- onCategoryChange call
-- [x] components/Navbar.tsx - Removed unused Link import
-
-### B. Bugs / Inaccuracies Fixed
-
-- [x] layouts/header.tsx - Fixed JSDoc: 3-column -> 2-column layout (no center
- section exists)
-- [x] app/page.tsx - Added available !== false filter to product list
-- [x] app/page.tsx - Fixed setState-in-effect lint error: moved initial sidebar
- state to lazy useState initializer
-- [x] next.config.ts - Added explanatory JSDoc comment
-
-### C. Documentation Updated
-
-- [x] README.md - Fixed file structure tree, removed SCSS, fixed dark mode note,
- updated tech table
-- [x] components/COMPONENTS.md - Fixed CartProduct styling (was outdated
- w-64/text-red-500/bg-blue-600); added Navbar, Header, Footer sections
-
-### D. New Documentation Created
-
-- [x] WORKFLOW.md - Architecture, data flow, design token system, how-to guides,
- dev workflow
-
-### E. Mini-test Results
-
-- [x] npm run lint - PASSED (0 errors, 0 warnings)
-- [x] npm run build - PASSED (Compiled successfully, TypeScript clean, static
- pages generated)
-
----
-
-## Pending Features (Future Work)
-
-### Cart & Ordering
-
-- [ ] Implement cart checkout flow (app/(main)/cart or modal)
-- [ ] Cart sidebar/modal with item list and total
-- [ ] Order submission API integration
-- [ ] Payment page implementation (app/(main)/payment)
-- [ ] Order history/tracking page
-- [ ] Toast notifications for cart actions
-
-### Authentication & User Management
-
-- [ ] Real backend authentication (replace MOCK_AUTH_DB)
-- [ ] Real OTP delivery service (SMS integration)
-- [ ] User profile page with edit capability
-- [ ] Password reset/recovery flow
-- [ ] Session management and token refresh
-
-### Manager Features
-
-- [ ] Manager dashboard page (app/(manager)/page.tsx)
-- [ ] Product management (add/edit/delete)
-- [ ] Category management
-- [ ] Order management & tracking
-- [ ] Sales analytics/dashboard
-- [ ] Inventory management
-
-### Backend Integration
-
-- [ ] Replace MOCK_PRODUCTS with API calls (GET /api/products)
-- [ ] Replace MOCK_SHOPS with API calls (GET /api/shops)
-- [ ] Replace MOCK_USERS with real authentication (POST /api/auth/login)
-- [ ] Real product images (replace placeholder.jpg)
-- [ ] Image upload for products
-
-### UX Improvements
-
-- [ ] Dark mode toggle (CSS variables prepared, toggle UI needed)
-- [ ] Loading skeletons for product grid
-- [ ] Product detail modal/page with full description
-- [ ] Wishlist/favorites feature
-- [ ] Sort products (price, rating, etc.)
-- [ ] Filter by price range
-- [ ] Quantity selector in product card
-- [ ] Related products suggestions
-
-### Performance & SEO
-
-- [ ] Dynamic route generation for products (app/(main)/product/[id]/page.tsx)
-- [ ] Dynamic route generation for shops (app/(feed)/shop/[id]/page.tsx)
-- [ ] Meta tags and Open Graph for SEO
-- [ ] Image optimization and lazy loading
-- [ ] Code splitting and dynamic imports
-
-### Accessibility & Testing
-
-- [ ] Keyboard navigation testing
-- [ ] ARIA labels audit
-- [ ] Unit tests for contexts
-- [ ] E2E tests for user flows
-- [ ] Accessibility audit (WCAG 2.1 AA)
+- [x] Update GraphQL queries/mutations in `lib/manager-context.tsx` to include `imageUrl`
+- [x] Update `components/organisms/manager/ProductModal.tsx`
+ - [x] Add file input for image selection
+ - [x] Add upload button to call `POST /api/file`
+ - [x] Store returned URL string into `form.imageUrl`
+ - [x] Show upload/loading/error states and image preview
+- [x] Update `components/organisms/manager/ProductsTab.tsx` to show image thumbnail in table
+- [x] Mark TODO progress after each step completed
diff --git a/app/(main)/payment/page.tsx b/app/(main)/payment/page.tsx
index 82b1634..13e38d1 100644
--- a/app/(main)/payment/page.tsx
+++ b/app/(main)/payment/page.tsx
@@ -25,7 +25,11 @@ export default function PaymentPage() {
const { products } = useManager();
const findProduct = (id: string): MenuItemEntity =>
- products.find((i) => i.id == id)!;
+ products.find((i) => i.id == id) ?? ({
+ name: "Unknown product",
+ description: "",
+ price: 0,
+ } as MenuItemEntity);
const [isReviewOpen, setIsReviewOpen] = useState(false);
const isCustomer = user?.role === "customer";
@@ -83,7 +87,7 @@ export default function PaymentPage() {
return (
{name}
diff --git a/components/organisms/manager/ProductModal.tsx b/components/organisms/manager/ProductModal.tsx
index c1b8c89..e552a44 100644
--- a/components/organisms/manager/ProductModal.tsx
+++ b/components/organisms/manager/ProductModal.tsx
@@ -14,13 +14,60 @@ export default function ProductModal({
const [form, setForm] = useState({
name: product?.name ?? "",
price: product?.price ?? 0,
- imageUrl: product?.imageUrl ?? "/imgs/products/placeholder.jpg",
+ imageUrl: product?.imageUrl ?? "",
description: product?.description ?? "",
available: product?.available ?? true,
});
+ const [uploading, setUploading] = useState(false);
+ const [uploadError, setUploadError] = useState(null);
+
+ const inputCls =
+ "text-foreground w-full rounded-xl border border-(--color-border) bg-white px-3 py-2 text-sm transition outline-none focus:border-(--color-primary) focus:ring-2 focus:ring-(--color-primary)/20";
+
+ const uploadImage = async (file: File) => {
+ setUploading(true);
+ setUploadError(null);
+
+ try {
+ const body = new FormData();
+ body.append("file", file);
+
+ const res = await fetch("/api/file", {
+ method: "POST",
+ body,
+ });
+
+ if (!res.ok) {
+ throw new Error("Upload ảnh thất bại.");
+ }
+
+ const raw = await res.text();
+
+ let filename = "";
+ try {
+ const parsed = JSON.parse(raw.trim());
+ filename = parsed.filename || "";
+ } catch {
+ const match = raw.match(/"filename"\s*:\s*"([^"]+)"/i);
+ filename = match?.[1] || "";
+ }
+
+ if (!filename) {
+ throw new Error("Không nhận được filename ảnh từ server.");
+ }
+
+ setForm((prev) => ({ ...prev, imageUrl: filename }));
+ } catch (error: any) {
+ setUploadError(error?.message || "Không thể upload ảnh.");
+ } finally {
+ setUploading(false);
+ }
+ };
+
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
+
if (isEdit && product) {
onSave({ ...form, id: product.id });
} else {
@@ -28,14 +75,21 @@ export default function ProductModal({
}
};
- const inputCls =
- "text-foreground w-full rounded-xl border border-(--color-border) bg-white px-3 py-2 text-sm transition outline-none focus:border-(--color-primary) focus:ring-2 focus:ring-(--color-primary)/20";
+ const handleFileChange = async (e: React.ChangeEvent) => {
+ const file = e.target.files?.[0];
+ if (!file) return;
+ await uploadImage(file);
+ };
+
+ const toDisplayUrl = (filename: string) =>
+ filename && !filename.startsWith("/") && !filename.startsWith("http")
+ ? `/api/file/${filename}`
+ : filename;
+
+ const previewUrl = toDisplayUrl(form.imageUrl);
return (
- e.target === e.currentTarget && onClose()}
- >
+
@@ -83,6 +137,46 @@ export default function ProductModal({
/>
+
+
+
+
+
+
+ {uploading && (
+
+
+ Đang upload ảnh...
+
+ )}
+
+
+ {previewUrl && (
+
+ 
+
+ )}
+
+ {uploadError && (
+
+
+ {uploadError}
+
+ )}
+
+
|