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/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 && ( +
+ Preview ảnh món +
+ )} + + {uploadError && ( +

+ + {uploadError} +

+ )} +
+