From e08f2f9286a283d6543b196b78327a3f355ba2ae Mon Sep 17 00:00:00 2001 From: Wira Basalamah Date: Fri, 24 Apr 2026 05:19:05 +0700 Subject: [PATCH] feat: add Ina Trading portal flows and API integration --- next.config.ts | 17 +- package-lock.json | 231 ++- package.json | 4 + public/favicon.svg | 7 + public/ina_logo.png | Bin 0 -> 62180 bytes public/logo.svg | 15 + src/app/(auth)/account-not-found/page.tsx | 128 ++ src/app/(auth)/forgot-password/page.tsx | 152 ++ src/app/(auth)/login/page.tsx | 231 +++ src/app/(auth)/register/complete/page.tsx | 103 + src/app/(auth)/register/page.tsx | 337 ++++ src/app/(auth)/register/verify/page.tsx | 321 +++ src/app/(auth)/reset-password/page.tsx | 261 +++ src/app/(dashboard)/dashboard/help/page.tsx | 173 ++ src/app/(dashboard)/dashboard/page.tsx | 232 +++ .../dashboard/warehouse/WarehouseForm.tsx | 381 ++++ .../warehouse/[warehouseId]/edit/page.tsx | 78 + .../dashboard/warehouse/new/page.tsx | 17 + .../(dashboard)/dashboard/warehouse/page.tsx | 311 +++ src/app/(dashboard)/layout.tsx | 155 ++ .../products/[productId]/detail/page.tsx | 516 +++++ .../products/[productId]/edit/page.tsx | 1744 +++++++++++++++++ .../products/new/category/page.tsx | 224 +++ .../(dashboard)/products/new/details/page.tsx | 432 ++++ src/app/(dashboard)/products/new/layout.tsx | 141 ++ .../(dashboard)/products/new/pricing/page.tsx | 1085 ++++++++++ .../(dashboard)/products/new/review/page.tsx | 407 ++++ .../products/new/specifications/page.tsx | 541 +++++ .../products/new/submitted/page.tsx | 44 + src/app/(dashboard)/products/page.tsx | 527 +++++ .../settings/change-password/page.tsx | 295 +++ src/app/(dashboard)/settings/layout.tsx | 56 + src/app/(dashboard)/settings/page.tsx | 571 ++++++ src/app/(onboarding)/layout.tsx | 164 ++ .../(onboarding)/onboarding/business/page.tsx | 1152 +++++++++++ src/app/(onboarding)/onboarding/plan/page.tsx | 371 ++++ .../(onboarding)/onboarding/security/page.tsx | 55 + .../onboarding/store-detail/page.tsx | 446 +++++ .../(onboarding)/onboarding/success/page.tsx | 109 ++ src/app/admin/dashboard/page.tsx | 212 ++ src/app/admin/layout.tsx | 108 + src/app/admin/news/[newsId]/edit/page.tsx | 377 ++++ src/app/admin/news/new/page.tsx | 290 +++ src/app/admin/news/page.tsx | 274 +++ src/app/admin/places/PlaceForm.tsx | 600 ++++++ src/app/admin/places/[placeId]/edit/page.tsx | 81 + src/app/admin/places/new/page.tsx | 18 + src/app/admin/places/page.tsx | 283 +++ src/app/admin/review/[productId]/page.tsx | 429 ++++ src/app/admin/review/page.tsx | 278 +++ src/app/api/admin/news/[newsId]/route.ts | 26 + src/app/api/admin/news/route.ts | 29 + src/app/api/admin/places/[placeId]/route.ts | 27 + src/app/api/admin/places/route.ts | 29 + .../admin/review/[productId]/compare/route.ts | 17 + src/app/api/admin/review/[productId]/route.ts | 54 + src/app/api/admin/review/route.ts | 16 + src/app/api/auth/finalize-register/route.ts | 261 +++ src/app/api/auth/login/route.ts | 71 + src/app/api/auth/send-otp/route.ts | 28 + src/app/api/auth/verify-otp/route.ts | 45 + src/app/api/locations/all/route.ts | 43 + src/app/api/locations/cities/route.ts | 50 + src/app/api/locations/provinces/route.ts | 41 + src/app/api/products/[productId]/route.ts | 68 + src/app/api/products/categories/route.ts | 20 + src/app/api/products/create/route.ts | 16 + src/app/api/products/route.ts | 31 + .../subcategories/[categoryId]/route.ts | 24 + .../submit-review/[productId]/route.ts | 18 + src/app/api/products/warehouses/route.ts | 20 + src/app/api/profile/change-password/route.ts | 21 + src/app/api/seller/profile/route.ts | 34 + src/app/api/seller/route.ts | 39 + src/app/api/seller/store/route.ts | 22 + src/app/api/upload/route.ts | 46 + src/app/api/warehouses/[warehouseId]/route.ts | 39 + src/app/api/warehouses/route.ts | 22 + src/app/favicon.ico | Bin 25931 -> 362 bytes src/app/globals.css | 109 +- src/app/icon.svg | 5 + src/app/layout.tsx | 58 +- src/app/page.tsx | 64 +- src/components/app-icon.tsx | 197 ++ src/components/language-toggle.tsx | 22 + src/components/product-submenu-nav.tsx | 59 + src/components/upload-field.tsx | 137 ++ src/lib/api.ts | 24 + src/lib/countries.ts | 130 ++ src/lib/i18n-context.tsx | 51 + src/lib/mailer.ts | 43 + src/lib/product-draft.tsx | 244 +++ src/lib/product-options.ts | 88 + src/lib/redis.ts | 5 + src/lib/translations/en.ts | 743 +++++++ src/lib/translations/id.ts | 744 +++++++ src/lib/use-product-submit.ts | 135 ++ 97 files changed, 18889 insertions(+), 110 deletions(-) create mode 100644 public/favicon.svg create mode 100755 public/ina_logo.png create mode 100644 public/logo.svg create mode 100644 src/app/(auth)/account-not-found/page.tsx create mode 100644 src/app/(auth)/forgot-password/page.tsx create mode 100644 src/app/(auth)/login/page.tsx create mode 100644 src/app/(auth)/register/complete/page.tsx create mode 100644 src/app/(auth)/register/page.tsx create mode 100644 src/app/(auth)/register/verify/page.tsx create mode 100644 src/app/(auth)/reset-password/page.tsx create mode 100644 src/app/(dashboard)/dashboard/help/page.tsx create mode 100644 src/app/(dashboard)/dashboard/page.tsx create mode 100644 src/app/(dashboard)/dashboard/warehouse/WarehouseForm.tsx create mode 100644 src/app/(dashboard)/dashboard/warehouse/[warehouseId]/edit/page.tsx create mode 100644 src/app/(dashboard)/dashboard/warehouse/new/page.tsx create mode 100644 src/app/(dashboard)/dashboard/warehouse/page.tsx create mode 100644 src/app/(dashboard)/layout.tsx create mode 100644 src/app/(dashboard)/products/[productId]/detail/page.tsx create mode 100644 src/app/(dashboard)/products/[productId]/edit/page.tsx create mode 100644 src/app/(dashboard)/products/new/category/page.tsx create mode 100644 src/app/(dashboard)/products/new/details/page.tsx create mode 100644 src/app/(dashboard)/products/new/layout.tsx create mode 100644 src/app/(dashboard)/products/new/pricing/page.tsx create mode 100644 src/app/(dashboard)/products/new/review/page.tsx create mode 100644 src/app/(dashboard)/products/new/specifications/page.tsx create mode 100644 src/app/(dashboard)/products/new/submitted/page.tsx create mode 100644 src/app/(dashboard)/products/page.tsx create mode 100644 src/app/(dashboard)/settings/change-password/page.tsx create mode 100644 src/app/(dashboard)/settings/layout.tsx create mode 100644 src/app/(dashboard)/settings/page.tsx create mode 100644 src/app/(onboarding)/layout.tsx create mode 100644 src/app/(onboarding)/onboarding/business/page.tsx create mode 100644 src/app/(onboarding)/onboarding/plan/page.tsx create mode 100644 src/app/(onboarding)/onboarding/security/page.tsx create mode 100644 src/app/(onboarding)/onboarding/store-detail/page.tsx create mode 100644 src/app/(onboarding)/onboarding/success/page.tsx create mode 100644 src/app/admin/dashboard/page.tsx create mode 100644 src/app/admin/layout.tsx create mode 100644 src/app/admin/news/[newsId]/edit/page.tsx create mode 100644 src/app/admin/news/new/page.tsx create mode 100644 src/app/admin/news/page.tsx create mode 100644 src/app/admin/places/PlaceForm.tsx create mode 100644 src/app/admin/places/[placeId]/edit/page.tsx create mode 100644 src/app/admin/places/new/page.tsx create mode 100644 src/app/admin/places/page.tsx create mode 100644 src/app/admin/review/[productId]/page.tsx create mode 100644 src/app/admin/review/page.tsx create mode 100644 src/app/api/admin/news/[newsId]/route.ts create mode 100644 src/app/api/admin/news/route.ts create mode 100644 src/app/api/admin/places/[placeId]/route.ts create mode 100644 src/app/api/admin/places/route.ts create mode 100644 src/app/api/admin/review/[productId]/compare/route.ts create mode 100644 src/app/api/admin/review/[productId]/route.ts create mode 100644 src/app/api/admin/review/route.ts create mode 100644 src/app/api/auth/finalize-register/route.ts create mode 100644 src/app/api/auth/login/route.ts create mode 100644 src/app/api/auth/send-otp/route.ts create mode 100644 src/app/api/auth/verify-otp/route.ts create mode 100644 src/app/api/locations/all/route.ts create mode 100644 src/app/api/locations/cities/route.ts create mode 100644 src/app/api/locations/provinces/route.ts create mode 100644 src/app/api/products/[productId]/route.ts create mode 100644 src/app/api/products/categories/route.ts create mode 100644 src/app/api/products/create/route.ts create mode 100644 src/app/api/products/route.ts create mode 100644 src/app/api/products/subcategories/[categoryId]/route.ts create mode 100644 src/app/api/products/submit-review/[productId]/route.ts create mode 100644 src/app/api/products/warehouses/route.ts create mode 100644 src/app/api/profile/change-password/route.ts create mode 100644 src/app/api/seller/profile/route.ts create mode 100644 src/app/api/seller/route.ts create mode 100644 src/app/api/seller/store/route.ts create mode 100644 src/app/api/upload/route.ts create mode 100644 src/app/api/warehouses/[warehouseId]/route.ts create mode 100644 src/app/api/warehouses/route.ts create mode 100644 src/app/icon.svg create mode 100644 src/components/app-icon.tsx create mode 100644 src/components/language-toggle.tsx create mode 100644 src/components/product-submenu-nav.tsx create mode 100644 src/components/upload-field.tsx create mode 100644 src/lib/api.ts create mode 100644 src/lib/countries.ts create mode 100644 src/lib/i18n-context.tsx create mode 100644 src/lib/mailer.ts create mode 100644 src/lib/product-draft.tsx create mode 100644 src/lib/product-options.ts create mode 100644 src/lib/redis.ts create mode 100644 src/lib/translations/en.ts create mode 100644 src/lib/translations/id.ts create mode 100644 src/lib/use-product-submit.ts diff --git a/next.config.ts b/next.config.ts index e9ffa30..2cdcdcd 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,7 +1,22 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { - /* config options here */ + images: { + unoptimized: true, + remotePatterns: [ + { + protocol: "https", + hostname: "be.inatrading.co.id", + }, + { + protocol: "http", + hostname: "203.175.11.191", + }, + ], + dangerouslyAllowSVG: true, + contentDispositionType: "attachment", + contentSecurityPolicy: "default-src 'self'; script-src 'none'; sandbox;", + }, }; export default nextConfig; diff --git a/package-lock.json b/package-lock.json index 42aa5af..bb9ed18 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,11 @@ "name": "ina-trading-web", "version": "0.1.0", "dependencies": { + "@types/nodemailer": "^8.0.0", + "axios": "^1.15.0", + "ioredis": "^5.10.1", "next": "16.2.3", + "nodemailer": "^8.0.5", "react": "19.2.4", "react-dom": "19.2.4" }, @@ -1019,6 +1023,12 @@ "url": "https://opencollective.com/libvips" } }, + "node_modules/@ioredis/commands": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.5.1.tgz", + "integrity": "sha512-JH8ZL/ywcJyR9MmJ5BNqZllXNZQqQbnVZOqpPQqE1vHiFgAw4NHbvE0FOduNU8IX9babitBT46571OnPTT0Zcw==", + "license": "MIT" + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -1621,12 +1631,20 @@ "version": "20.19.39", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.39.tgz", "integrity": "sha512-orrrD74MBUyK8jOAD/r0+lfa1I2MO6I+vAkmAWzMYbCcgrN4lCrmK52gRFQq/JRxfYPfonkr4b0jcY7Olqdqbw==", - "dev": true, "license": "MIT", "dependencies": { "undici-types": "~6.21.0" } }, + "node_modules/@types/nodemailer": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-8.0.0.tgz", + "integrity": "sha512-fyf8jWULsCo0d0BuoQ75i6IeoHs47qcqxWc7yUdUcV0pOZGjUTTOvwdG1PRXUDqN/8A64yQdQdnA2pZgcdi+cA==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/react": { "version": "19.2.14", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", @@ -2485,6 +2503,12 @@ "node": ">= 0.4" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, "node_modules/available-typed-arrays": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", @@ -2511,6 +2535,17 @@ "node": ">=4" } }, + "node_modules/axios": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.0.tgz", + "integrity": "sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^2.1.0" + } + }, "node_modules/axobject-query": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", @@ -2621,7 +2656,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -2701,6 +2735,15 @@ "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", "license": "MIT" }, + "node_modules/cluster-key-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", + "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -2721,6 +2764,18 @@ "dev": true, "license": "MIT" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -2822,7 +2877,6 @@ "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -2879,6 +2933,24 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -2906,7 +2978,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", @@ -3018,7 +3089,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -3028,7 +3098,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -3066,7 +3135,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -3079,7 +3147,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -3676,6 +3743,26 @@ "dev": true, "license": "ISC" }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/for-each": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", @@ -3692,11 +3779,26 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -3757,7 +3859,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -3782,7 +3883,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", @@ -3870,7 +3970,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -3942,7 +4041,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -3955,7 +4053,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, "license": "MIT", "dependencies": { "has-symbols": "^1.0.3" @@ -3971,7 +4068,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -4049,6 +4145,30 @@ "node": ">= 0.4" } }, + "node_modules/ioredis": { + "version": "5.10.1", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.10.1.tgz", + "integrity": "sha512-HuEDBTI70aYdx1v6U97SbNx9F1+svQKBDo30o0b9fw055LMepzpOOd0Ccg9Q6tbqmBSJaMuY0fB7yw9/vjBYCA==", + "license": "MIT", + "dependencies": { + "@ioredis/commands": "1.5.1", + "cluster-key-slot": "^1.1.0", + "debug": "^4.3.4", + "denque": "^2.1.0", + "lodash.defaults": "^4.2.0", + "lodash.isarguments": "^3.1.0", + "redis-errors": "^1.2.0", + "redis-parser": "^3.0.0", + "standard-as-callback": "^2.1.0" + }, + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ioredis" + } + }, "node_modules/is-array-buffer": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", @@ -4922,6 +5042,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", + "license": "MIT" + }, + "node_modules/lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==", + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -4966,7 +5098,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -4996,6 +5127,27 @@ "node": ">=8.6" } }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/minimatch": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", @@ -5023,7 +5175,6 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, "node_modules/nanoid": { @@ -5174,6 +5325,15 @@ "dev": true, "license": "MIT" }, + "node_modules/nodemailer": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.5.tgz", + "integrity": "sha512-0PF8Yb1yZuQfQbq+5/pZJrtF6WQcjTd5/S4JOHs9PGFxuTqoB/icwuB44pOdURHJbRKX1PPoJZtY7R4VUoCC8w==", + "license": "MIT-0", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -5485,6 +5645,15 @@ "react-is": "^16.13.1" } }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -5544,6 +5713,27 @@ "dev": true, "license": "MIT" }, + "node_modules/redis-errors": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", + "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/redis-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", + "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", + "license": "MIT", + "dependencies": { + "redis-errors": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -5960,6 +6150,12 @@ "dev": true, "license": "MIT" }, + "node_modules/standard-as-callback": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", + "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==", + "license": "MIT" + }, "node_modules/stop-iteration-iterator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", @@ -6438,7 +6634,6 @@ "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, "license": "MIT" }, "node_modules/unrs-resolver": { diff --git a/package.json b/package.json index 19128ed..3f13821 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,11 @@ "lint": "eslint" }, "dependencies": { + "@types/nodemailer": "^8.0.0", + "axios": "^1.15.0", + "ioredis": "^5.10.1", "next": "16.2.3", + "nodemailer": "^8.0.5", "react": "19.2.4", "react-dom": "19.2.4" }, diff --git a/public/favicon.svg b/public/favicon.svg new file mode 100644 index 0000000..0de12f3 --- /dev/null +++ b/public/favicon.svg @@ -0,0 +1,7 @@ + + + + INA + + TRADING + diff --git a/public/ina_logo.png b/public/ina_logo.png new file mode 100755 index 0000000000000000000000000000000000000000..688de2faf8aae6d048a8c869756ae0865c3943b4 GIT binary patch literal 62180 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 + Ina Trading + INA TRADING logo with subtitle Digital Security Technology by PERURI. + + + INA + TRADING + + Digital Security Technology by + + + + + + diff --git a/src/app/(auth)/account-not-found/page.tsx b/src/app/(auth)/account-not-found/page.tsx new file mode 100644 index 0000000..28b1ccd --- /dev/null +++ b/src/app/(auth)/account-not-found/page.tsx @@ -0,0 +1,128 @@ +"use client"; + +import Image from "next/image"; +import Link from "next/link"; +import { useSearchParams } from "next/navigation"; +import { Suspense } from "react"; +import { LanguageToggle } from "@/components/language-toggle"; +import { useLanguage } from "@/lib/i18n-context"; + +function AccountNotFoundContent() { + const searchParams = useSearchParams(); + const email = searchParams.get("email") || ""; + const { t } = useLanguage(); + const a = t.auth.accountNotFound; + + return ( +
+ {/* Left Side */} +
+
+
+
+ + + {a.editorialIntelligence} + +
+

+ {a.tradePrecision} +

+

+ {a.tradeSubtitle} +

+
+
+ + {/* Right Side */} +
+
+ Ina Trading +
+ + + help_outline + {a.helpLink} + +
+
+ +
+
+ person_off +
+ +
+

+ {a.title} +

+
+

{email}

+ + {a.change} + +
+
+ +
+ + {a.createAccount} + + +
+ + + + + login + + {a.loginOther} + +
+
+ + +
+
+ ); +} + +export default function AccountNotFoundPage() { + return ( + + + + ); +} diff --git a/src/app/(auth)/forgot-password/page.tsx b/src/app/(auth)/forgot-password/page.tsx new file mode 100644 index 0000000..c78ce68 --- /dev/null +++ b/src/app/(auth)/forgot-password/page.tsx @@ -0,0 +1,152 @@ +"use client"; + +import Image from "next/image"; +import Link from "next/link"; +import { FormEvent, useState } from "react"; +import { LanguageToggle } from "@/components/language-toggle"; +import { useLanguage } from "@/lib/i18n-context"; + +export default function ForgotPasswordPage() { + const { t } = useLanguage(); + const f = t.auth.forgotPassword; + + const [contact, setContact] = useState(""); + const [submitted, setSubmitted] = useState(false); + + function handleSubmit(e: FormEvent) { + e.preventDefault(); + if (!contact.trim()) return; + setSubmitted(true); + } + + return ( + <> +
+
+
+
+
+
+ +
+
+
+ + {f.securityFirst} + +
+

+ {f.heroTitle} +

+

+ {f.heroSubtitle} +

+
+ +
+
+ 99.9% + {f.uptime} +
+
+ 24/7 + {f.fraudMonitoring} +
+
+
+ +
+
+ Ina Trading +
+ +
+ +
+ +
+
+

+ {f.title} +

+

{f.subtitle}

+
+ +
+ {submitted && ( +
+ {f.apiNotReady} {contact}. +
+ )} + +
+ +
+ + alternate_email + + setContact(e.target.value)} + placeholder="name@company.com" + required + className="w-full bg-transparent border-none p-0 text-lg font-medium text-on-surface placeholder:text-outline/50 focus:ring-0" + /> +
+

+ {/* privacy note kept short */} +

+
+ +
+ +
+
+ +
+ + + chevron_left + + {f.backToLogin} + + +
+ contact_support +

+ {f.havingTrouble}{" "} + {f.supportLink} +

+
+
+
+ +
+
+
+ +
+ +
+ + ); +} diff --git a/src/app/(auth)/login/page.tsx b/src/app/(auth)/login/page.tsx new file mode 100644 index 0000000..d111064 --- /dev/null +++ b/src/app/(auth)/login/page.tsx @@ -0,0 +1,231 @@ +"use client"; + +import Image from "next/image"; +import Link from "next/link"; +import { useState } from "react"; +import { useRouter } from "next/navigation"; +import { LanguageToggle } from "@/components/language-toggle"; +import { useLanguage } from "@/lib/i18n-context"; + +const authFieldWrapperClass = + "relative rounded-xl border border-outline-variant/60 bg-surface-container-high px-0 transition-all duration-300 focus-within:border-primary focus-within:bg-surface-container-lowest"; + +export default function LoginPage() { + const router = useRouter(); + const { t } = useLanguage(); + const l = t.auth.login; + + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [showPassword, setShowPassword] = useState(false); + const [remember, setRemember] = useState(false); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(""); + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault(); + setError(""); + setLoading(true); + + try { + const res = await fetch("/api/auth/login", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ email, password }), + }); + const data = await res.json(); + + if (res.status === 404 && data.error === "ACCOUNT_NOT_FOUND") { + router.push(`/account-not-found?email=${encodeURIComponent(email)}`); + return; + } + + if (!res.ok) { + setError(data.error || l.errorGeneric); + return; + } + + if (remember) { + localStorage.setItem("token", data.token); + localStorage.setItem("role", data.role); + } else { + sessionStorage.setItem("token", data.token); + sessionStorage.setItem("role", data.role); + } + + if (data.role === "admin") { + router.push("/admin/dashboard"); + return; + } + + if (data.role === "seller") { + // Check seller profile completeness + try { + const profileRes = await fetch("/api/seller/profile", { + headers: { "x-auth-token": data.token }, + }); + const profileData = await profileRes.json(); + const profile = profileData?.data || profileData; + + const isIncomplete = + !profile?.storeName || + !profile?.biography || + !profile?.sellerImageUrl; + + if (isIncomplete || data.onboardingRequired) { + router.push("/onboarding/business"); + return; + } + } catch { + // If profile check fails, still proceed to dashboard + } + + router.push("/dashboard"); + return; + } + + router.push("/dashboard"); + } catch { + setError(l.errorConnection); + } finally { + setLoading(false); + } + } + + return ( +
+ {/* Left Side */} +
+
+
+
+
+
+

+ {l.heroTitle} +

+

+ {l.heroSubtitle} +

+
+
+ + {/* Right Side */} +
+
+ {/* Mobile Logo + Language Toggle */} +
+ Ina Trading + +
+ + {/* Desktop language toggle */} +
+ Ina Trading + +
+ +
+

+ {l.title} +

+

{l.subtitle}

+
+ +
+ {error && ( +
+ {error} +
+ )} + +
+ +
+ + person + + setEmail(e.target.value)} + placeholder="name@business.com" + required + className="w-full rounded-xl border-none bg-transparent py-4 pl-12 pr-4 font-medium placeholder:text-outline-variant focus:outline-none" + /> +
+
+ +
+
+ + + {l.forgotPassword} + +
+
+ + lock + + setPassword(e.target.value)} + placeholder="••••••••" + required + className="w-full rounded-xl border-none bg-transparent py-4 pl-12 pr-12 font-medium placeholder:text-outline-variant focus:outline-none" + /> + +
+
+ +
+ setRemember(e.target.checked)} + className="w-5 h-5 rounded border-outline-variant text-primary focus:ring-primary" + /> + +
+ +
+ +
+
+ +
+

+ {l.noAccount}{" "} + + {l.registerFree} + +

+
+
+
+ +
+ ); +} diff --git a/src/app/(auth)/register/complete/page.tsx b/src/app/(auth)/register/complete/page.tsx new file mode 100644 index 0000000..2d7a994 --- /dev/null +++ b/src/app/(auth)/register/complete/page.tsx @@ -0,0 +1,103 @@ +"use client"; + +import Link from "next/link"; +import { useRouter } from "next/navigation"; +import { useEffect, useState } from "react"; +import { LanguageToggle } from "@/components/language-toggle"; +import { useLanguage } from "@/lib/i18n-context"; + +export default function CompleteRegisterPage() { + const router = useRouter(); + const { t } = useLanguage(); + const c = t.auth.complete; + + const [loading, setLoading] = useState(false); + const [error, setError] = useState(""); + + useEffect(() => { + const raw = sessionStorage.getItem("registerData"); + const otpVerified = sessionStorage.getItem("otpVerified"); + if (!raw || otpVerified !== "true") { + router.replace("/register"); + } + }, [router]); + + async function handleComplete() { + const raw = sessionStorage.getItem("registerData"); + if (!raw) { + setError(c.noData); + return; + } + + setLoading(true); + setError(""); + + try { + const registerData = JSON.parse(raw); + const res = await fetch("/api/auth/finalize-register", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ role: registerData.role, registerData }), + }); + + const data = await res.json(); + + if (!res.ok) { + setError(data?.error || c.registerFail); + return; + } + + sessionStorage.removeItem("registerData"); + sessionStorage.removeItem("otpVerified"); + sessionStorage.removeItem("otpVerifiedEmail"); + router.push("/login"); + } catch { + setError(t.common.connectionError); + } finally { + setLoading(false); + } + } + + return ( +
+
+
+ +
+
+
+ {c.finalStep} +
+

+ {c.title} +

+

{c.subtitle}

+
+ + {error ? ( +
+ {error} +
+ ) : null} + +
+ + + + {c.backToRegister} + +
+
+
+ ); +} diff --git a/src/app/(auth)/register/page.tsx b/src/app/(auth)/register/page.tsx new file mode 100644 index 0000000..c3fa10a --- /dev/null +++ b/src/app/(auth)/register/page.tsx @@ -0,0 +1,337 @@ +"use client"; + +import Image from "next/image"; +import Link from "next/link"; +import { useState, Suspense } from "react"; +import { useRouter, useSearchParams } from "next/navigation"; +import { LanguageToggle } from "@/components/language-toggle"; +import { useLanguage } from "@/lib/i18n-context"; + +type Role = "seller" | "buyer"; + +const authInputClass = + "w-full rounded-xl border border-outline-variant/60 bg-surface-container-highest px-4 py-3 text-on-surface placeholder:text-surface-dim focus:border-primary focus:bg-surface-container-lowest focus:outline-none transition-all duration-300"; + +const authInputWithIconClass = + "w-full rounded-xl border border-outline-variant/60 bg-surface-container-highest px-4 py-3 pr-10 text-on-surface placeholder:text-surface-dim focus:border-primary focus:bg-surface-container-lowest focus:outline-none transition-all duration-300"; + +function RegisterContent() { + const router = useRouter(); + const searchParams = useSearchParams(); + const emailFromQuery = searchParams.get("email") || ""; + const { t } = useLanguage(); + const r = t.auth.register; + + const [role, setRole] = useState("seller"); + const [email, setEmail] = useState(emailFromQuery); + const [name, setName] = useState(""); + const [mobile, setMobile] = useState(""); + const [password, setPassword] = useState(""); + const [confirmPassword, setConfirmPassword] = useState(""); + const [showPassword, setShowPassword] = useState(false); + const [showConfirm, setShowConfirm] = useState(false); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(""); + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault(); + setError(""); + + if (password !== confirmPassword) { + setError(r.passwordMismatch); + return; + } + + if (password.length < 6) { + setError(r.passwordTooShort); + return; + } + + setLoading(true); + + try { + const res = await fetch("/api/auth/send-otp", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ email }), + }); + + const data = await res.json(); + + if (!res.ok) { + setError(data.error || r.otpError); + return; + } + + sessionStorage.setItem( + "registerData", + JSON.stringify({ name, mobile, password, role, email }) + ); + + router.push(`/register/verify?email=${encodeURIComponent(email)}`); + } catch { + setError(t.common.connectionError); + } finally { + setLoading(false); + } + } + + return ( +
+ {/* Left Side */} +
+
+
+
+
+
+

+ {r.joinNetwork} +

+

+ {r.joinSubtitle} +

+
+
+
98%
+
{r.successRate}
+
+
+
24/7
+
{r.marketPulse}
+
+
+
+
+ + {/* Right Side */} +
+
+
+
+
+ Ina Trading + +
+

+ {r.title} +

+

{r.subtitle}

+
+ +
+ {error && ( +
+ {error} +
+ )} + + {/* Role Selector */} +
+ +
+ + +
+
+ + {/* Email */} +
+ + setEmail(e.target.value)} + placeholder="name@business.com" + required + readOnly={!!emailFromQuery} + className={`${authInputClass} ${ + emailFromQuery ? "bg-surface-container text-on-surface-variant cursor-not-allowed" : "" + }`} + /> + {emailFromQuery && ( +

{r.emailFromPrevious}

+ )} +
+ + {/* Name */} +
+ + setName(e.target.value)} + placeholder="John Doe" + required + className={authInputClass} + /> +
+ + {/* Mobile */} +
+ + setMobile(e.target.value)} + placeholder="+62 812 3456 789" + required + className={authInputClass} + /> +
+ + {/* Password */} +
+ +
+ setPassword(e.target.value)} + placeholder={r.passwordPlaceholder} + required + className={authInputWithIconClass} + /> + +
+
+ + {/* Confirm Password */} +
+ +
+ setConfirmPassword(e.target.value)} + placeholder={r.confirmPasswordPlaceholder} + required + className={authInputWithIconClass} + /> + +
+
+ +
+ + +
+

+ {r.haveAccount}{" "} + + {r.signIn} + +

+
+
+
+ + {/* Step Progress */} +
+
+
+
+
+
+ + {r.stepOf} + +
+
+ +
+

+ {r.termsAgreement}{" "} + {t.common.terms} {r.and}{" "} + {t.common.privacy} {r.inaTrading} +

+
+
+
+ ); +} + +export default function RegisterPage() { + return ( + + + + ); +} diff --git a/src/app/(auth)/register/verify/page.tsx b/src/app/(auth)/register/verify/page.tsx new file mode 100644 index 0000000..950fe53 --- /dev/null +++ b/src/app/(auth)/register/verify/page.tsx @@ -0,0 +1,321 @@ +"use client"; + +import Image from "next/image"; +import Link from "next/link"; +import { useState, useRef, Suspense, useEffect } from "react"; +import { useRouter, useSearchParams } from "next/navigation"; +import { LanguageToggle } from "@/components/language-toggle"; +import { useLanguage } from "@/lib/i18n-context"; + +function VerifyContent() { + const router = useRouter(); + const searchParams = useSearchParams(); + const email = searchParams.get("email") || ""; + const { t } = useLanguage(); + const v = t.auth.verify; + + const [otp, setOtp] = useState(["", "", "", "", "", ""]); + const [loading, setLoading] = useState(false); + const [resending, setResending] = useState(false); + const [error, setError] = useState(""); + const [errorStep, setErrorStep] = useState(""); + const [success, setSuccess] = useState(""); + const [countdown, setCountdown] = useState(0); + + const inputRefs = useRef<(HTMLInputElement | null)[]>([]); + + useEffect(() => { + inputRefs.current[0]?.focus(); + }, []); + + useEffect(() => { + if (countdown <= 0) return; + const timer = setTimeout(() => setCountdown((c) => c - 1), 1000); + return () => clearTimeout(timer); + }, [countdown]); + + function handleOtpChange(index: number, value: string) { + if (!/^\d*$/.test(value)) return; + const newOtp = [...otp]; + newOtp[index] = value.slice(-1); + setOtp(newOtp); + if (value && index < 5) { + inputRefs.current[index + 1]?.focus(); + } + } + + function handleKeyDown(index: number, e: React.KeyboardEvent) { + if (e.key === "Backspace" && !otp[index] && index > 0) { + inputRefs.current[index - 1]?.focus(); + } + } + + function handlePaste(e: React.ClipboardEvent) { + e.preventDefault(); + const pasted = e.clipboardData.getData("text").replace(/\D/g, "").slice(0, 6); + if (!pasted) return; + const newOtp = [...otp]; + pasted.split("").forEach((char, i) => { if (i < 6) newOtp[i] = char; }); + setOtp(newOtp); + const nextIndex = Math.min(pasted.length, 5); + inputRefs.current[nextIndex]?.focus(); + } + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault(); + setError(""); + setErrorStep(""); + const otpCode = otp.join(""); + + if (otpCode.length < 6) { + setError(v.otpTooShort); + return; + } + + const rawData = sessionStorage.getItem("registerData"); + if (!rawData) { + setError(v.noData); + return; + } + + const { role } = JSON.parse(rawData); + setLoading(true); + + try { + const res = await fetch("/api/auth/verify-otp", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ email, otp: otpCode }), + }); + + const data = await res.json(); + + if (!res.ok) { + const backendMessage = data?.error || data?.responseDesc || data?.message || v.verifyFail; + setError(backendMessage); + setErrorStep(data?.step || ""); + return; + } + + const parsedData = JSON.parse(rawData); + + if (role === "seller") { + const registerData = { ...parsedData, email, otpVerified: true }; + + const registerRes = await fetch("/api/auth/finalize-register", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ role: "seller", registerData }), + }); + + const registerResult = await registerRes.json(); + + if (!registerRes.ok) { + setError(registerResult?.error || v.registerFail); + setErrorStep(registerResult?.step || ""); + return; + } + + if (registerResult?.token) { + sessionStorage.setItem("token", registerResult.token); + sessionStorage.setItem("role", "seller"); + } + + sessionStorage.removeItem("registerData"); + sessionStorage.removeItem("otpVerified"); + sessionStorage.removeItem("otpVerifiedEmail"); + setSuccess(v.successSeller); + setTimeout(() => { router.push("/onboarding/business"); }, 1000); + return; + } + + sessionStorage.setItem("registerData", JSON.stringify({ ...parsedData, email, otpVerified: true })); + sessionStorage.setItem("otpVerified", "true"); + sessionStorage.setItem("otpVerifiedEmail", email); + setSuccess(v.successBuyer); + setTimeout(() => { router.push("/register/complete"); }, 1000); + } catch { + setError(t.common.connectionError); + } finally { + setLoading(false); + } + } + + async function handleResend() { + if (countdown > 0) return; + setError(""); + setErrorStep(""); + setResending(true); + + try { + const res = await fetch("/api/auth/send-otp", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ email }), + }); + + if (res.ok) { + setOtp(["", "", "", "", "", ""]); + inputRefs.current[0]?.focus(); + setCountdown(60); + } else { + const data = await res.json(); + setError(data.error || t.common.connectionError); + } + } catch { + setError(t.common.connectionError); + } finally { + setResending(false); + } + } + + return ( +
+ {/* Left Side */} +
+
+
+
+

+ {v.secureYourFuture} +

+
+

+ {v.verifyIdentity} +

+
+
+

99.9%

+

{v.transactionSecurity}

+
+
+

256-bit

+

{v.bankLevelEncryption}

+
+
+
+
+ + {/* Right Side */} +
+ {/* Logo */} +
+ Ina Trading +
+ +
+
+
+ + mark_email_read + +
+

+ {v.title} +

+

+ {v.subtitle}{" "} + {email} + {v.subtitleSuffix} +

+
+ +
+ {error && ( +
+
{error}
+ {errorStep ? ( +
+ Step: {errorStep} +
+ ) : null} +
+ )} + {success && ( +
+ {success} +
+ )} + + {/* OTP Grid */} +
+ {otp.map((digit, index) => ( + { inputRefs.current[index] = el; }} + type="text" + inputMode="numeric" + maxLength={1} + value={digit} + onChange={(e) => handleOtpChange(index, e.target.value)} + onKeyDown={(e) => handleKeyDown(index, e)} + placeholder="•" + className="otp-input w-14 h-16 text-center text-2xl font-bold rounded-xl border-none bg-surface-container-highest text-on-surface transition-all focus:bg-surface-container-lowest focus:ring-0" + /> + ))} +
+ +
+ {v.noCode} + +
+ +
+ +
+
+ +
+
+
+ shield + + {v.securityTitle} + +
+

{v.securityDesc}

+
+
+
+ + + close + {t.common.cancel} + + +
+ +
+
+
+ ); +} + +export default function VerifyPage() { + return ( + + + + ); +} diff --git a/src/app/(auth)/reset-password/page.tsx b/src/app/(auth)/reset-password/page.tsx new file mode 100644 index 0000000..113a568 --- /dev/null +++ b/src/app/(auth)/reset-password/page.tsx @@ -0,0 +1,261 @@ +"use client"; + +import Image from "next/image"; +import Link from "next/link"; +import { FormEvent, useState } from "react"; +import { LanguageToggle } from "@/components/language-toggle"; + +const passwordFieldWrapperClass = + "relative rounded-xl border border-outline-variant/60 bg-surface-container-highest transition-all duration-300 focus-within:border-primary focus-within:bg-surface-container-lowest"; + +function getPasswordChecks(password: string) { + return { + minLength: password.length >= 12, + uppercase: /[A-Z]/.test(password), + number: /\d/.test(password), + special: /[^A-Za-z0-9]/.test(password), + }; +} + +export default function ResetPasswordPage() { + const [newPassword, setNewPassword] = useState(""); + const [confirmPassword, setConfirmPassword] = useState(""); + const [showNewPassword, setShowNewPassword] = useState(false); + const [showConfirmPassword, setShowConfirmPassword] = useState(false); + const [error, setError] = useState(""); + const [submitted, setSubmitted] = useState(false); + + const checks = getPasswordChecks(newPassword); + const isStrongPassword = Object.values(checks).every(Boolean); + const passwordsMatch = newPassword === confirmPassword; + + function handleSubmit(e: FormEvent) { + e.preventDefault(); + setError(""); + setSubmitted(false); + + if (!isStrongPassword) { + setError("Password baru belum memenuhi requirement minimum."); + return; + } + + if (!passwordsMatch) { + setError("Konfirmasi password tidak sama."); + return; + } + + // API reset password belum tersedia. + setSubmitted(true); + } + + return ( + <> +
+
+
+ Ina Trading +
+ +
+

+ Secure.
+ Refined.
+ Absolute. +

+

+ Protecting your financial ecosystem with world-class encryption + and institutional-grade security protocols. +

+
+ +
+
+
+ Security Status +
+
+ 99.9% Up +
+
+
+
+ Data Protection +
+
+ AES-256 +
+
+
+ +
+
+
+
+
+
+ +
+
+
+ Ina Trading + +
+
+ +
+ +
+
+

+ Reset password +

+

+ Enter your new credentials to regain access to your trading + dashboard. +

+
+ +
+ {error && ( +
+ {error} +
+ )} + + {submitted && ( +
+ Reset password API belum tersedia. Screen ini sudah siap dan + submit reset password berhasil disimulasikan. +
+ )} + +
+ +
+ setNewPassword(e.target.value)} + placeholder="••••••••" + required + className="w-full rounded-xl bg-transparent border-none py-4 px-4 pr-12 text-on-surface font-medium placeholder:text-outline-variant focus:ring-0" + /> + +
+
+ +
+ +
+ setConfirmPassword(e.target.value)} + placeholder="••••••••" + required + className="w-full rounded-xl bg-transparent border-none py-4 px-4 pr-12 text-on-surface font-medium placeholder:text-outline-variant focus:ring-0" + /> + +
+
+ +
+

+ Requirement:{" "} + Minimum 12 characters, including one uppercase letter, one + special character, and one numeric value. +

+
+ + 12+ characters + + + Uppercase letter + + + Numeric value + + + Special character + +
+
+ +
+ +
+
+ +
+

+ Remember your password? + + Go back to Login + +

+
+
+ +
+ +
+
+
+ + ); +} diff --git a/src/app/(dashboard)/dashboard/help/page.tsx b/src/app/(dashboard)/dashboard/help/page.tsx new file mode 100644 index 0000000..1bb0d85 --- /dev/null +++ b/src/app/(dashboard)/dashboard/help/page.tsx @@ -0,0 +1,173 @@ +"use client"; + +import { useState } from "react"; + +const categories = [ + { + icon: "payments", + title: "Payment Protocol", + desc: "Financial clearing cycles, escrow rules, and institutional wire frameworks.", + }, + { + icon: "local_shipping", + title: "Shipping & Logistics", + desc: "Global supply chain tracking, customs documentation, and freight insurance.", + }, + { + icon: "verified_user", + title: "Account & Compliance", + desc: "KYC requirements, identity verification, and regional trading licenses.", + }, + { + icon: "language", + title: "Global Trading Rules", + desc: "International trade laws, market restrictions, and tariff updates.", + }, +]; + +const faqs = [ + "How to verify my business identity?", + "What are the shipping limits for Europe?", + "How to contact a trade curator?", +]; + +export default function HelpPage() { + const [search, setSearch] = useState(""); + + return ( +
+ {/* Hero */} +
+ {/* Dot grid */} +
+ {/* Decorative accent */} +
+
+
+ +
+

+ How can we help
+ you today? +

+
+ + search + + setSearch(e.target.value)} + className="w-full h-14 pl-14 pr-6 bg-white shadow-xl text-base font-medium placeholder:text-slate-400 focus:ring-2 focus:ring-primary/20 focus:outline-none rounded-xl border-none transition-all" + placeholder="Search for documentation, protocols, or support articles..." + type="text" + /> +
+
+
+ + {/* Popular Categories */} +
+
+
+

Resource Infrastructure

+

Popular Categories

+
+
+
+ +
+ {categories.map((cat) => ( +
+
+ {cat.icon} +

{cat.title}

+

{cat.desc}

+
+ ))} +
+
+ + {/* FAQ + Contact Support */} +
+ {/* FAQ */} +
+

+ Top Frequently Asked Questions +

+
+ {faqs.map((q) => ( +
+ {q} + + arrow_forward + +
+ ))} +
+
+ + {/* Contact Support */} +
+
+
+

+ Can't find what you're looking for? +

+

+ Our expert trade curators are available 24/7 for institutional grade support and technical troubleshooting. +

+
+
+ + +
+
+
+ + {/* Footer */} +
+
+
Ina Trading
+

+ © 2024 Ina Trading Marketplace. All Rights Reserved. +

+
+
+ {["Privacy Policy", "Compliance Framework", "Terms of Service", "Operational Status"].map((l) => ( + + {l} + + ))} +
+
+
+ ); +} diff --git a/src/app/(dashboard)/dashboard/page.tsx b/src/app/(dashboard)/dashboard/page.tsx new file mode 100644 index 0000000..6714245 --- /dev/null +++ b/src/app/(dashboard)/dashboard/page.tsx @@ -0,0 +1,232 @@ +"use client"; + +import { useLanguage } from "@/lib/i18n-context"; + +const recentOrders = [ + { + id: "ORD-001", + product: "Titan Minimalist V2", + sku: "TM-9920", + customer: "Sarah Jenkins", + location: "London, UK", + date: "Oct 24, 2023", + amount: "$249.00", + status: "Processing", + statusColor: "text-primary bg-primary/10", + }, + { + id: "ORD-002", + product: "Sonic Wave Pro", + sku: "SW-1021", + customer: "Marcus Thorne", + location: "Berlin, DE", + date: "Oct 23, 2023", + amount: "$499.00", + status: "Shipped", + statusColor: "text-secondary bg-secondary/10", + }, + { + id: "ORD-003", + product: "Pulse Runner X", + sku: "PR-8821", + customer: "Elena Rodriguez", + location: "Madrid, ES", + date: "Oct 23, 2023", + amount: "$125.00", + status: "Delivered", + statusColor: "text-tertiary bg-tertiary/10", + }, +]; + +const barHeights = [40, 65, 50, 85, 70, 60, 95, 45, 55, 80]; + +export default function DashboardPage() { + const { t } = useLanguage(); + const d = t.dashboard.overview; + + return ( +
+ {/* Hero Title */} +
+

+ {d.title} +

+

{d.subtitle}

+
+ + {/* Stats Grid */} +
+ {/* Total Products */} +
+
+

+ {d.totalProducts} +

+

1,284

+
+ trending_up + +12.5% {d.vsLastMonth} +
+
+ + {/* Total Buyers */} +
+
+

+ {d.totalBuyers} +

+

42,502

+
+ group + {d.globalReach} +
+
+ + {/* Refunds */} +
+
+

+ {d.refunds} +

+

142

+
+ history + 0.3% {d.returnRate} +
+
+
+ + {/* Analytics Row */} +
+ {/* Orders Analytics Chart */} +
+
+
+

{d.ordersAnalytics}

+

{d.ordersSubtitle}

+
+ +
+ + {/* Bar Chart */} +
+ {barHeights.map((height, i) => ( +
+ ))} +
+
+ {d.wk} 1 + {d.wk} 2 + {d.wk} 3 + {d.wk} 4 +
+
+ + {/* Earnings */} +
+

{d.earnings}

+ + {/* Donut Chart Placeholder */} +
+
+
+
+ $84.2k + + {d.grossRevenue} + +
+
+
+ + {/* Legend */} +
+ {[ + { color: "bg-primary", label: d.directSales, pct: "65%" }, + { color: "bg-secondary", label: d.retailPartners, pct: "25%" }, + { color: "bg-tertiary", label: d.affiliates, pct: "10%" }, + ].map((item) => ( +
+
+ + {item.label} +
+ {item.pct} +
+ ))} +
+
+
+ + {/* Recent Orders Table */} +
+
+

{d.recentOrders}

+ +
+
+ + + + + + + + + + + + + {recentOrders.map((order) => ( + + + + + + + + + ))} + +
{d.productDetails}{d.customer}{d.transactionDate}{d.amount}{d.status}{d.action}
+
+
+ inventory_2 +
+
+

{order.product}

+

SKU: {order.sku}

+
+
+
+

{order.customer}

+

{order.location}

+
{order.date}{order.amount} + + {order.status} + + + +
+
+
+
+ ); +} diff --git a/src/app/(dashboard)/dashboard/warehouse/WarehouseForm.tsx b/src/app/(dashboard)/dashboard/warehouse/WarehouseForm.tsx new file mode 100644 index 0000000..0ae4260 --- /dev/null +++ b/src/app/(dashboard)/dashboard/warehouse/WarehouseForm.tsx @@ -0,0 +1,381 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { useRouter } from "next/navigation"; +import { COUNTRIES } from "@/lib/countries"; + +export interface WarehouseFormState { + name: string; + address: string; + country: string; + province: string; + provinceId: string; + city: string; + cityId: string; + postalCode: string; + latitude: string; + longitude: string; + warehouseType: string; +} + +export const defaultWarehouseForm: WarehouseFormState = { + name: "", + address: "", + country: "Indonesia", + province: "", + provinceId: "", + city: "", + cityId: "", + postalCode: "", + latitude: "", + longitude: "", + warehouseType: "INA", +}; + +interface Province { id: string; name: string; } +interface City { id: string; name: string; } + +function getToken() { + if (typeof window === "undefined") return ""; + return sessionStorage.getItem("token") || localStorage.getItem("token") || ""; +} + +function normalizeToken(t: string) { + if (!t) return ""; + return t.startsWith("Bearer ") ? t : `Bearer ${t}`; +} + +const inputCls = + "w-full bg-surface-container-low border-b-2 border-outline/30 focus:border-primary border-t-0 border-x-0 rounded-t-lg px-4 py-3 text-sm font-medium text-on-surface placeholder:text-slate-400 focus:ring-0 focus:outline-none transition-all"; + +const labelCls = "block text-xs font-black uppercase tracking-[0.15em] text-slate-500 mb-2"; + +interface WarehouseFormProps { + initialData?: WarehouseFormState; + pageTitle: string; + pageSubtitle: string; + submitLabel: string; + submittingLabel: string; + successMessage: string; + apiMethod: "POST" | "PUT"; + apiUrl: string; +} + +export function WarehouseForm({ + initialData, + pageTitle, + pageSubtitle, + submitLabel, + submittingLabel, + successMessage, + apiMethod, + apiUrl, +}: WarehouseFormProps) { + const router = useRouter(); + const [form, setForm] = useState(initialData ?? defaultWarehouseForm); + const [provinces, setProvinces] = useState([]); + const [cities, setCities] = useState([]); + const [loadingProvinces, setLoadingProvinces] = useState(false); + const [loadingCities, setLoadingCities] = useState(false); + const [saving, setSaving] = useState(false); + const [savingPhase, setSavingPhase] = useState(""); + const [error, setError] = useState(""); + const [success, setSuccess] = useState(false); + + const isIndonesia = form.country === "Indonesia"; + + useEffect(() => { + if (initialData) setForm(initialData); + }, [initialData]); + + useEffect(() => { + if (!isIndonesia) { + setProvinces([]); + setCities([]); + return; + } + setLoadingProvinces(true); + fetch("/api/locations/provinces", { headers: { "x-auth-token": normalizeToken(getToken()) } }) + .then((r) => r.json()) + .then((d) => setProvinces(Array.isArray(d?.rows) ? d.rows : [])) + .catch(() => setProvinces([])) + .finally(() => setLoadingProvinces(false)); + }, [isIndonesia]); + + useEffect(() => { + if (!isIndonesia || !form.provinceId) { + setCities([]); + return; + } + setLoadingCities(true); + fetch(`/api/locations/cities?provinceId=${form.provinceId}`, { + headers: { "x-auth-token": normalizeToken(getToken()) }, + }) + .then((r) => r.json()) + .then((d) => setCities(Array.isArray(d?.rows) ? d.rows : [])) + .catch(() => setCities([])) + .finally(() => setLoadingCities(false)); + }, [isIndonesia, form.provinceId]); + + function update(patch: Partial) { + setForm((prev) => ({ ...prev, ...patch })); + } + + async function handleSave() { + if (!form.address.trim()) { setError("Alamat wajib diisi"); return; } + + setSaving(true); + setSavingPhase("Menyimpan perubahan..."); + setError(""); + + const payload: Record = { + name: form.name || null, + address: form.address, + country: form.country || null, + province: form.province || null, + city: form.city || null, + postalCode: form.postalCode || null, + latitude: form.latitude ? parseFloat(form.latitude) : null, + longitude: form.longitude ? parseFloat(form.longitude) : null, + warehouseType: form.warehouseType || null, + }; + + try { + const token = normalizeToken(getToken()); + const res = await fetch(apiUrl, { + method: apiMethod, + headers: { "Content-Type": "application/json", "x-auth-token": token }, + body: JSON.stringify(payload), + }); + const data = await res.json(); + if (!res.ok) { + setError(data?.responseDesc || data?.error || "Gagal menyimpan warehouse"); + return; + } + setSuccess(true); + setTimeout(() => router.push("/dashboard/warehouse"), 1500); + } catch { + setError("Gagal terhubung ke server"); + } finally { + setSaving(false); + setSavingPhase(""); + } + } + + return ( +
+ {/* Page Header */} +
+
+ Management Dashboard +

{pageTitle}

+

{pageSubtitle}

+
+ +
+ + {/* Form Card */} +
+

Facility Details

+ +
+ {/* Name */} +
+ + update({ name: e.target.value })} + placeholder="Contoh: Gudang Utama Jakarta..." + className={inputCls} + /> +
+ + {/* Address */} +
+ + update({ address: e.target.value })} + placeholder="Jl. Nama Jalan No. ..." + className={inputCls} + /> +
+ + {/* Country */} +
+ + +
+ + {/* Province */} +
+ + {isIndonesia ? ( + + ) : ( + update({ province: e.target.value })} + placeholder="Nama provinsi / state..." + className={inputCls} + /> + )} +
+ + {/* City */} +
+ + {isIndonesia ? ( + + ) : ( + update({ city: e.target.value })} + placeholder="Nama kota..." + className={inputCls} + /> + )} +
+ + {/* Postal Code */} +
+ + update({ postalCode: e.target.value })} + placeholder="Contoh: 13920" + className={inputCls} + /> +
+ + {/* Warehouse Type */} +
+ + +
+ + {/* Lat / Lng */} +
+ + update({ latitude: e.target.value })} + placeholder="Contoh: -6.1891" + className={inputCls} + /> +
+
+ + update({ longitude: e.target.value })} + placeholder="Contoh: 106.9247" + className={inputCls} + /> +
+
+ + {/* Status messages + buttons */} +
+ {success && ( +
+ check_circle + {successMessage} +
+ )} + {error && ( +
+ error + {error} +
+ )} +
+ {savingPhase && ( +

+ progress_activity + {savingPhase} +

+ )} +
+ + +
+
+
+
+
+ ); +} diff --git a/src/app/(dashboard)/dashboard/warehouse/[warehouseId]/edit/page.tsx b/src/app/(dashboard)/dashboard/warehouse/[warehouseId]/edit/page.tsx new file mode 100644 index 0000000..7ecb343 --- /dev/null +++ b/src/app/(dashboard)/dashboard/warehouse/[warehouseId]/edit/page.tsx @@ -0,0 +1,78 @@ +"use client"; + +import { useParams } from "next/navigation"; +import { useState } from "react"; +import { useRouter } from "next/navigation"; +import { WarehouseForm, WarehouseFormState } from "../../WarehouseForm"; + +function getCachedWarehouse(warehouseId: string): WarehouseFormState | null { + if (typeof window === "undefined" || !warehouseId) return null; + const cached = sessionStorage.getItem("editWarehouseCache"); + if (!cached) return null; + + try { + const raw = JSON.parse(cached) as Record; + if (raw.id !== warehouseId) return null; + sessionStorage.removeItem("editWarehouseCache"); + + return { + name: typeof raw.name === "string" ? raw.name : "", + address: typeof raw.address === "string" ? raw.address : "", + country: typeof raw.country === "string" ? raw.country : "Indonesia", + province: typeof raw.province === "string" ? raw.province : "", + provinceId: "", + city: typeof raw.city === "string" ? raw.city : "", + cityId: "", + postalCode: typeof raw.postalCode === "string" ? raw.postalCode : "", + latitude: raw.latitude != null ? String(raw.latitude) : "", + longitude: raw.longitude != null ? String(raw.longitude) : "", + warehouseType: + typeof raw.warehouseType === "string" ? raw.warehouseType : "INA", + }; + } catch { + return null; + } +} + +export default function EditWarehousePage() { + const params = useParams<{ warehouseId: string }>(); + const router = useRouter(); + const [initialData] = useState(() => + getCachedWarehouse(params.warehouseId) + ); + + const loadError = initialData + ? "" + : "Data tidak tersedia. Kembali ke daftar warehouse dan klik Edit lagi."; + + if (loadError) { + return ( +
+ error +

{loadError}

+ +
+ ); + } + + if (!initialData) { + return ( +
+ progress_activity +
+ ); + } + + return ( + + ); +} diff --git a/src/app/(dashboard)/dashboard/warehouse/new/page.tsx b/src/app/(dashboard)/dashboard/warehouse/new/page.tsx new file mode 100644 index 0000000..bd282f8 --- /dev/null +++ b/src/app/(dashboard)/dashboard/warehouse/new/page.tsx @@ -0,0 +1,17 @@ +"use client"; + +import { WarehouseForm } from "../WarehouseForm"; + +export default function NewWarehousePage() { + return ( + + ); +} diff --git a/src/app/(dashboard)/dashboard/warehouse/page.tsx b/src/app/(dashboard)/dashboard/warehouse/page.tsx new file mode 100644 index 0000000..7b847fc --- /dev/null +++ b/src/app/(dashboard)/dashboard/warehouse/page.tsx @@ -0,0 +1,311 @@ +"use client"; + +import Link from "next/link"; +import { useEffect, useState } from "react"; +import { useRouter } from "next/navigation"; + +interface WarehouseRow { + id: string; + name: string | null; + address: string | null; + city: string | null; + province: string | null; + country: string | null; + postalCode: string | null; + latitude: number | null; + longitude: number | null; + warehouseType: string | null; +} + +function getToken() { + if (typeof window === "undefined") return ""; + return sessionStorage.getItem("token") || localStorage.getItem("token") || ""; +} + +function normalizeToken(t: string) { + if (!t) return ""; + return t.startsWith("Bearer ") ? t : `Bearer ${t}`; +} + +function typeBadge(type: string | null) { + if (type === "INA") return "bg-primary-fixed text-on-primary-fixed-variant"; + if (type === "Other") return "bg-secondary-fixed text-on-secondary-fixed-variant"; + return "bg-slate-100 text-slate-500"; +} + +export default function WarehousePage() { + const router = useRouter(); + const [rows, setRows] = useState([]); + const [filtered, setFiltered] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(""); + const [search, setSearch] = useState(""); + const [page, setPage] = useState(0); + const [totalItem, setTotalItem] = useState(0); + const [totalPage, setTotalPage] = useState(1); + const [deleteId, setDeleteId] = useState(null); + const [deleting, setDeleting] = useState(false); + const pageSize = 20; + + useEffect(() => { + load(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [page]); + + async function load() { + setLoading(true); + setError(""); + try { + const token = normalizeToken(getToken()); + const res = await fetch(`/api/products/warehouses?page=${page + 1}&size=${pageSize}`, { + headers: { "x-auth-token": token }, + }); + const data = await res.json(); + const list: WarehouseRow[] = Array.isArray(data?.rows) ? data.rows : []; + setRows(list); + setFiltered(list); + setTotalItem(data?.totalItem ?? 0); + setTotalPage(data?.totalPage ?? 1); + } catch { + setError("Gagal memuat data warehouse"); + } finally { + setLoading(false); + } + } + + useEffect(() => { + const q = search.toLowerCase(); + setFiltered( + rows.filter( + (r) => + (r.name || "").toLowerCase().includes(q) || + (r.address || "").toLowerCase().includes(q) || + (r.city || "").toLowerCase().includes(q) || + (r.province || "").toLowerCase().includes(q) || + (r.country || "").toLowerCase().includes(q) + ) + ); + }, [search, rows]); + + async function handleDelete(id: string) { + setDeleting(true); + try { + const token = normalizeToken(getToken()); + const res = await fetch(`/api/warehouses/${id}`, { + method: "DELETE", + headers: { "x-auth-token": token }, + }); + if (!res.ok) { + const d = await res.json().catch(() => ({})); + alert(d?.responseDesc || "Gagal menghapus warehouse"); + return; + } + setDeleteId(null); + load(); + } catch { + alert("Gagal terhubung ke server"); + } finally { + setDeleting(false); + } + } + + const startEntry = page * pageSize + 1; + const endEntry = Math.min((page + 1) * pageSize, totalItem); + + return ( +
+ {/* Delete Confirmation Modal */} + {deleteId && ( +
+
+
+
+ delete +
+

Hapus Warehouse?

+
+

+ Tindakan ini tidak bisa dibatalkan. Warehouse akan dihapus secara permanen. +

+
+ + +
+
+
+ )} + + {/* Header */} +
+
+ Management Dashboard +

Warehouse

+

Kelola gudang dan lokasi penyimpanan produk

+
+
+
+ search + setSearch(e.target.value)} + /> +
+ + add + Tambah Warehouse + +
+
+ + {/* Table */} +
+ {loading ? ( +
+ progress_activity +

Memuat data...

+
+ ) : error ? ( +
+ error +

{error}

+
+ ) : filtered.length === 0 ? ( +
+ warehouse +

Belum ada warehouse

+
+ ) : ( +
+ + + + + + + + + + + + {filtered.map((item) => ( + + {/* Warehouse name */} + + + {/* Address */} + + + {/* Postal code */} + + + {/* Type */} + + + {/* Actions */} + + + ))} + +
WarehouseAlamatKode PosTipeAksi
+
+
+ warehouse +
+
+

+ {item.name || Tanpa nama} +

+

+ {item.city && item.province ? `${item.city}, ${item.province}` : item.city || item.province || "—"} +

+
+
+
+

{item.address || "—"}

+

{item.country || ""}

+
+ {item.postalCode || "—"} + + + {item.warehouseType || "—"} + + +
+ + +
+
+
+ )} + + {/* Pagination */} + {!loading && totalItem > 0 && ( +
+ + Menampilkan {startEntry}–{endEntry} dari {totalItem} warehouse + +
+ + {Array.from({ length: Math.min(totalPage, 5) }, (_, i) => i).map((i) => ( + + ))} + +
+
+ )} +
+
+ ); +} diff --git a/src/app/(dashboard)/layout.tsx b/src/app/(dashboard)/layout.tsx new file mode 100644 index 0000000..04b3eff --- /dev/null +++ b/src/app/(dashboard)/layout.tsx @@ -0,0 +1,155 @@ +"use client"; + +import { usePathname, useRouter } from "next/navigation"; +import Image from "next/image"; +import Link from "next/link"; +import { AppIcon } from "@/components/app-icon"; +import { LanguageToggle } from "@/components/language-toggle"; +import { useLanguage } from "@/lib/i18n-context"; +import { ProductSubmenuNav } from "@/components/product-submenu-nav"; + +const navItems = [ + { href: "/dashboard", icon: "dashboard", label: "Dashboard" }, + { href: "/products", icon: "inventory_2", label: "Product" }, + { href: "/dashboard/inventory", icon: "inventory", label: "Inventory" }, + { href: "/dashboard/warehouse", icon: "warehouse", label: "Warehouse" }, + { href: "/dashboard/orders", icon: "shopping_cart", label: "Orders" }, + { href: "/dashboard/invoice", icon: "receipt", label: "Invoice" }, + { href: "/dashboard/customers", icon: "groups", label: "Customers" }, + { href: "/dashboard/marketing", icon: "campaign", label: "Marketing" }, + { href: "/dashboard/finance", icon: "payments", label: "Finance" }, + { href: "/dashboard/reports", icon: "assessment", label: "Reports" }, + { href: "/dashboard/analytics", icon: "analytics", label: "Analytics" }, + { href: "/dashboard/shop", icon: "storefront", label: "Shop" }, + { href: "/dashboard/reviews", icon: "reviews", label: "Reviews" }, + { href: "/settings", icon: "storefront", label: "Settings" }, +]; + +const visibleNavLabels = new Set([ + "Dashboard", + "Product", + "Warehouse", + // "Orders", // hidden temporarily + // "Invoice", // hidden temporarily + "Settings", +]); + + +export default function DashboardLayout({ + children, +}: { + children: React.ReactNode; +}) { + const pathname = usePathname(); + const router = useRouter(); + const { t } = useLanguage(); + + const handleLogout = () => { + localStorage.removeItem("token"); + localStorage.removeItem("role"); + sessionStorage.removeItem("token"); + sessionStorage.removeItem("role"); + router.push("/login"); + }; + + return ( +
+ {/* Top Nav */} +
+
+ Ina Trading +
+ + +
+
+ +
+ + + +
+
+ + {/* Sidebar */} + + + {/* Main Content */} +
+ {children} +
+
+ ); +} diff --git a/src/app/(dashboard)/products/[productId]/detail/page.tsx b/src/app/(dashboard)/products/[productId]/detail/page.tsx new file mode 100644 index 0000000..590c014 --- /dev/null +++ b/src/app/(dashboard)/products/[productId]/detail/page.tsx @@ -0,0 +1,516 @@ +"use client"; + +import Link from "next/link"; +import { useParams, useSearchParams } from "next/navigation"; +import { Suspense, useEffect, useState } from "react"; +import { useLanguage } from "@/lib/i18n-context"; + +const API_BASE = process.env.NEXT_PUBLIC_API_URL || ""; + +interface ProductWarehouse { + id?: string; + stock?: number; +} + +interface ProductMeasurement { + measurementType?: string; + measurementValue?: string; + price?: string | number; + currency?: string; + weight?: string | number; + weightType?: string; + length?: string | number; + width?: string | number; + height?: string | number; + dimensionType?: string; + isConfigurePromotionPrice?: boolean; + promotionPrice?: string | number; + promotionCurrency?: string; + warehouses?: ProductWarehouse[]; +} + +interface ProductModel { + warehouses?: ProductWarehouse[]; + productMeasurements?: ProductMeasurement[]; +} + +interface ProductImage { + sequence?: number; + imageId?: string; +} + +interface ProductInfoItem { + paramName: string; + paramValue: string; +} + +interface ProductCategory { + name?: string; +} + +interface ProductSubCategory { + id?: string; + name?: string; + category?: ProductCategory; +} + +interface ProductDetail { + name?: string; + state?: string; + subCategory?: ProductSubCategory; + isPreOrder?: boolean; + isNew?: boolean; + isEligibleToExport?: boolean; + preOrderDay?: string | number; + description?: string; + imageId?: string; + productImages?: ProductImage[]; + productModels?: ProductModel[]; + productKeyWords?: string[]; + productFeatures?: string[]; + productInformations?: ProductInfoItem[]; + categoryInformations?: ProductInfoItem[]; + complianceInformation?: { + countryOfOrigin?: string; + safetyWarning?: string; + isDangerousGoodRegulation?: boolean; + }; + warrantyInformation?: { + type?: string; + duration?: string | number; + durationType?: string; + }; +} + +function getToken() { + if (typeof window === "undefined") return ""; + return sessionStorage.getItem("token") || localStorage.getItem("token") || ""; +} + +function SectionHeader({ step, title }: { step: string; title: string }) { + return ( +
+
+ {step} +
+

{title}

+
+ ); +} + +function Row({ label, value }: { label: string; value?: string | number | boolean | null }) { + if (value === "" || value === undefined || value === null) return null; + const display = typeof value === "boolean" ? (value ? "Ya" : "Tidak") : String(value); + return ( +
+ {label} + {display} +
+ ); +} + +function isNonEmptyString(value: string | undefined): value is string { + return typeof value === "string" && value.length > 0; +} + +function ToggleBadge({ label, value }: { label: string; value: boolean }) { + return ( +
+ {label} + + {value ? "Ya" : "Tidak"} + +
+ ); +} + +function ProductDetailPageInner() { + const { t } = useLanguage(); + const d = t.dashboard.productDetail; + const params = useParams<{ productId: string }>(); + const searchParams = useSearchParams(); + const isDraft = searchParams.get("draft") === "1"; + const [product, setProduct] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(""); + const errorLoadText = d.errorLoad; + + useEffect(() => { + if (!params.productId) return; + const url = `/api/products/${params.productId}${isDraft ? "?draft=1" : ""}`; + fetch(url, { headers: { "x-auth-token": getToken() } }) + .then((r) => r.json()) + .then((j) => { + if (!j) throw new Error("No data"); + setProduct(j?.data || j); + }) + .catch(() => setError(errorLoadText)) + .finally(() => setLoading(false)); + }, [errorLoadText, params.productId, isDraft]); + + if (loading) { + return ( +
+

{d.loading}

+
+ ); + } + + if (error || !product) { + return ( +
+
+ {error || d.notFound} +
+
+ ); + } + + const models: ProductModel[] = Array.isArray(product.productModels) + ? product.productModels + : []; + const keywords = Array.isArray(product.productKeyWords) ? product.productKeyWords.filter(Boolean) : []; + const features = Array.isArray(product.productFeatures) ? product.productFeatures.filter(Boolean) : []; + const productInfos = Array.isArray(product.productInformations) ? product.productInformations.filter((i: { paramName: string; paramValue: string }) => i.paramName && i.paramValue) : []; + const categoryInfos = Array.isArray(product.categoryInformations) ? product.categoryInformations.filter((i: { paramName: string; paramValue: string }) => i.paramName && i.paramValue) : []; + const allImages: string[] = [ + ...(product.imageId ? [product.imageId] : []), + ...(Array.isArray(product.productImages) + ? product.productImages + .sort( + (a: ProductImage, b: ProductImage) => + (a.sequence ?? 0) - (b.sequence ?? 0) + ) + .map((img: ProductImage) => img.imageId) + .filter(isNonEmptyString) + : []), + ]; + + return ( +
+ {/* Page Header */} +
+ +
+
+

{product.name || d.title}

+

{product.state || "DRAFT"}

+
+ + edit + {d.editProduct} + +
+
+ + {/* ── Section 01: Basic Details (Category) ───────────────────────────── */} +
+ +
+
+

{d.mainCategory}

+

{product.subCategory?.category?.name || "—"}

+
+
+

{d.subCategory}

+

{product.subCategory?.name || product.subCategory?.id || "—"}

+
+
+
+ + {/* ── Section 02: Description ────────────────────────────────────────── */} +
+ {/* Left */} +
+ + + {/* Name */} +
+

{d.officialName}

+

{product.name || "—"}

+
+ + {/* Toggles */} +
+ + +
+ + {product.isPreOrder && ( +
+

{d.preOrderDay}

+

{product.preOrderDay || "—"}

+
+ )} + + {/* Keywords */} + {keywords.length > 0 && ( +
+

{d.keywords}

+
+ {keywords.map((k: string) => ( + {k} + ))} +
+
+ )} + + {/* Description */} +
+

{d.narrative}

+

{product.description || "—"}

+
+ + {/* Features */} + {features.length > 0 && ( +
+

{d.features}

+
+ {features.map((f: string, i: number) => ( +
+ drag_indicator +

{f}

+
+ ))} +
+
+ )} +
+ + {/* Right: Visual Identity */} +
+

{d.visualIdentity}

+ {allImages.length === 0 ? ( +
+ image +
+ ) : ( +
+ {allImages.map((imgId, i) => ( +
+
+ {/* eslint-disable-next-line @next/next/no-img-element */} + {i { (e.target as HTMLImageElement).style.display = "none"; }} + /> +
+
+

+ {i === 0 ? d.mainImage : `${d.gallery} ${i}`} +

+

{t.common.uploaded}

+
+
+ ))} +
+ )} +
+ info +

+ {allImages.length} {d.imagesAvailable} +

+
+
+
+ + {/* ── Section 03: Pricing & Model ───────────────────────────────────── */} + {models.length > 0 && ( +
+
+
03
+

{d.section03} ({models.length})

+
+ {/* eslint-disable-next-line @typescript-eslint/no-explicit-any */} + {models.map((m: any, i: number) => { + const weightUnit = m.weightType || "G"; + const dimUnit = m.dimensionType || "CM"; + const pkgWeightUnit = m.packagingWeightType || "G"; + const pkgDimUnit = m.packagingDimensionType || "CM"; + const measurements = Array.isArray(m.productMeasurements) ? m.productMeasurements : []; + return ( +
+
+
{i + 1}
+

{m.name || `Model ${i + 1}`}

+ {m.sku && SKU: {m.sku}} + {measurements.length > 0 && ( + {measurements.length} measurement(s) + )} +
+
+ + + + {m.isConfigurePromotionPrice && } + {m.isConfigurePromotionPrice && m.promotionStartDate && ( + + )} + + +
+ {/* Warehouses */} + {Array.isArray(m.warehouses) && m.warehouses.length > 0 && ( +
+

{d.warehouseStock}

+
+ {/* eslint-disable-next-line @typescript-eslint/no-explicit-any */} + {m.warehouses.filter((w: any) => w.id).map((w: any, wi: number) => ( +
+ {w.id?.slice(0, 8)}... + {w.stock ?? 0} unit +
+ ))} +
+
+ )} + {/* Measurements */} + {measurements.length > 0 && ( +
+

Measurements / Variants

+
+ {measurements.map((ms: ProductMeasurement, mi: number) => { + const msWeightUnit = ms.weightType || "G"; + const msDimUnit = ms.dimensionType || "CM"; + return ( +
+
+ + {String(mi + 1).padStart(2, "0")} + + {ms.measurementType && {ms.measurementType}} + {ms.measurementValue && — {ms.measurementValue}} +
+
+ + + + {ms.isConfigurePromotionPrice && } +
+ {Array.isArray(ms.warehouses) && + ms.warehouses.filter((w: ProductWarehouse) => w.id).length > 0 && ( +
+

Stock

+ {ms.warehouses + .filter((w: ProductWarehouse) => w.id) + .map((w: ProductWarehouse, wi: number) => ( +
+ {w.id?.slice(0, 8)}... + {w.stock ?? 0} unit +
+ ))} +
+ )} +
+ ); + })} +
+
+ )} +
+ ); + })} +
+ )} + + {/* ── Section 04: General Info ──────────────────────────────────────── */} +
+ + + {productInfos.length > 0 && ( +
+

{d.productInfo}

+ {productInfos.map((item: { paramName: string; paramValue: string }, i: number) => ( + + ))} +
+ )} + + {categoryInfos.length > 0 && ( + <> + {productInfos.length > 0 &&
} +
+

{d.categoryInfo}

+ {categoryInfos.map((item: { paramName: string; paramValue: string }, i: number) => ( + + ))} +
+ + )} + + {product.complianceInformation && ( + <> +
+
+

{d.compliance}

+ + + +
+ + )} + + {product.warrantyInformation && ( + <> +
+
+

{d.warranty}

+ + +
+ + )} + +
+
+

{d.export}

+ +
+
+ + {/* ── Fixed Bottom Footer ───────────────────────────────────────────── */} +
+
+
+ {d.modeReadOnly} +
+
+ + arrow_back + Kembali + + + edit + Edit Produk + +
+
+
+
+ ); +} + +export default function ProductDetailPage() { + return ( + + + + ); +} diff --git a/src/app/(dashboard)/products/[productId]/edit/page.tsx b/src/app/(dashboard)/products/[productId]/edit/page.tsx new file mode 100644 index 0000000..128446b --- /dev/null +++ b/src/app/(dashboard)/products/[productId]/edit/page.tsx @@ -0,0 +1,1744 @@ +"use client"; + +import { useParams, useRouter, useSearchParams } from "next/navigation"; +import { Suspense, useEffect, useRef, useState } from "react"; +import { WEIGHT_TYPES, DIMENSION_TYPES, WORLD_CURRENCIES } from "@/lib/product-options"; +import { useLanguage } from "@/lib/i18n-context"; + +// ─── Types ──────────────────────────────────────────────────────────────────── + +interface CategoryOption { + id: string; + name: string; +} + +interface WarehouseOption { + id: string; + name: string; + address: string; + city: string; +} + +interface EditWarehouse { + id: string; + stock: number; +} + +interface EditMeasurement { + _key: string; + measurementType: string; + measurementValue: string; + price: string; + currency: string; + weight: string; + weightType: string; + length: string; + width: string; + height: string; + dimensionType: string; + packagingWeight: string; + packagingWeightType: string; + packagingLength: string; + packagingWidth: string; + packagingHeight: string; + packagingDimensionType: string; + hasPromotion: boolean; + promotionPrice: string; + promotionCurrency: string; + promotionStartDate: string; + promotionEndDate: string; + warehouses: EditWarehouse[]; +} + +interface EditModel { + _key: string; + name: string; + sku: string; + imageId: string; + price: string; + currency: string; + weight: string; + weightType: string; + length: string; + width: string; + height: string; + dimensionType: string; + packagingWeight: string; + packagingWeightType: string; + packagingLength: string; + packagingWidth: string; + packagingHeight: string; + packagingDimensionType: string; + hasPromotion: boolean; + promotionPrice: string; + promotionCurrency: string; + promotionStartDate: string; + promotionEndDate: string; + warehouses: EditWarehouse[]; + measurements: EditMeasurement[]; +} + +interface EditState { + categoryId: string; + categoryName: string; + subCategoryId: string; + subCategoryName: string; + name: string; + description: string; + isPreOrder: boolean; + preOrderDay: string; + isNew: boolean; + isEligibleToExport: boolean; + imageId: string; + productImages: string[]; + keywords: string[]; + features: string[]; + models: EditModel[]; + productInformations: { paramName: string; paramValue: string }[]; + categoryInformations: { paramName: string; paramValue: string }[]; + complianceInformation: { + safetyWarning: string; + countryOfOrigin: string; + isDangerousGoodRegulation: boolean; + fileId: string; + }; + warrantyInformation: { + type: string; + duration: string; + durationType: string; + }; +} + +interface ApiWarehouse { + id?: string | number | null; + stock?: string | number | null; +} + +interface ApiMeasurement { + measurementType?: string | number | null; + measurementValue?: string | number | null; + price?: string | number | null; + currency?: string | number | null; + weight?: string | number | null; + weightType?: string | number | null; + length?: string | number | null; + width?: string | number | null; + height?: string | number | null; + dimensionType?: string | number | null; + packagingWeight?: string | number | null; + packagingWeightType?: string | number | null; + packagingLength?: string | number | null; + packagingWidth?: string | number | null; + packagingHeight?: string | number | null; + packagingDimensionType?: string | number | null; + isConfigurePromotionPrice?: boolean | null; + promotionPrice?: string | number | null; + promotionCurrency?: string | number | null; + promotionStartDate?: string | number | null; + promotionEndDate?: string | number | null; + warehouses?: ApiWarehouse[]; +} + +interface ApiModel extends ApiMeasurement { + name?: string | number | null; + sku?: string | number | null; + imageId?: string | number | null; + image?: string | number | null; + warehouses?: ApiWarehouse[]; + productMeasurements?: ApiMeasurement[]; +} + +interface ApiProductImage { + sequence?: number | null; + imageId?: string | number | null; +} + +interface ApiParamItem { + paramName?: string; + paramValue?: string; +} + +function toParamItems(items: ApiParamItem[] | undefined) { + if (!Array.isArray(items)) return []; + return items.map((item: ApiParamItem) => ({ + paramName: toStr(item.paramName), + paramValue: toStr(item.paramValue), + })); +} + +interface ApiProduct { + subCategory?: { + id?: string | number | null; + name?: string | number | null; + } | null; + name?: string | number | null; + description?: string | number | null; + isPreOrder?: boolean | null; + preOrderDay?: string | number | null; + isNew?: boolean | null; + isEligibleToExport?: boolean | null; + imageId?: string | number | null; + productImages?: ApiProductImage[]; + productKeyWords?: string[]; + productFeatures?: string[]; + productModels?: ApiModel[]; + productInformations?: ApiParamItem[]; + categoryInformations?: ApiParamItem[]; + complianceInformation?: { + safetyWarning?: string | number | null; + countryOfOrigin?: string | number | null; + isDangerousGoodRegulation?: boolean | null; + fileId?: string | number | null; + file?: string | number | null; + } | null; + warrantyInformation?: { + type?: string | number | null; + duration?: string | number | null; + durationType?: string | number | null; + } | null; +} + +// ─── Helpers ────────────────────────────────────────────────────────────────── + +function getToken() { + if (typeof window === "undefined") return ""; + return sessionStorage.getItem("token") || localStorage.getItem("token") || ""; +} + +function toNum(v: string | number | null | undefined): number { + const n = Number(String(v ?? "").replace(/,/g, "")); + return Number.isFinite(n) ? n : 0; +} + +function toStr(v: string | number | null | undefined): string { + if (v === null || v === undefined) return ""; + return String(v); +} + +function newMeasurement(): EditMeasurement { + return { + _key: `meas-${Date.now()}-${Math.random().toString(36).slice(2)}`, + measurementType: "", + measurementValue: "", + price: "", + currency: "IDR", + weight: "", + weightType: "G", + length: "", + width: "", + height: "", + dimensionType: "CM", + packagingWeight: "", + packagingWeightType: "G", + packagingLength: "", + packagingWidth: "", + packagingHeight: "", + packagingDimensionType: "CM", + hasPromotion: false, + promotionPrice: "", + promotionCurrency: "IDR", + promotionStartDate: "", + promotionEndDate: "", + warehouses: [{ id: "", stock: 0 }], + }; +} + +function newModel(index: number): EditModel { + return { + _key: `model-${Date.now()}-${index}`, + name: "", + sku: "", + imageId: "", + price: "", + currency: "IDR", + weight: "", + weightType: "G", + length: "", + width: "", + height: "", + dimensionType: "CM", + packagingWeight: "", + packagingWeightType: "G", + packagingLength: "", + packagingWidth: "", + packagingHeight: "", + packagingDimensionType: "CM", + hasPromotion: false, + promotionPrice: "", + promotionCurrency: "IDR", + promotionStartDate: "", + promotionEndDate: "", + warehouses: [{ id: "", stock: 0 }], + measurements: [], + }; +} + +function apiToEditState(data: ApiProduct): EditState { + const rawModels = Array.isArray(data?.productModels) ? data.productModels : []; + + const models: EditModel[] = rawModels.length > 0 + ? rawModels.map((m: ApiModel, i: number) => ({ + _key: `loaded-${i}`, + name: toStr(m.name), + sku: toStr(m.sku), + imageId: toStr(m.imageId ?? m.image), + price: toStr(m.price), + currency: toStr(m.currency) || "IDR", + weight: toStr(m.weight), + weightType: toStr(m.weightType) || "G", + length: toStr(m.length), + width: toStr(m.width), + height: toStr(m.height), + dimensionType: toStr(m.dimensionType) || "CM", + packagingWeight: toStr(m.packagingWeight), + packagingWeightType: toStr(m.packagingWeightType) || "G", + packagingLength: toStr(m.packagingLength), + packagingWidth: toStr(m.packagingWidth), + packagingHeight: toStr(m.packagingHeight), + packagingDimensionType: toStr(m.packagingDimensionType) || "CM", + hasPromotion: !!m.isConfigurePromotionPrice, + promotionPrice: toStr(m.promotionPrice), + promotionCurrency: toStr(m.promotionCurrency) || toStr(m.currency) || "IDR", + promotionStartDate: toStr(m.promotionStartDate), + promotionEndDate: toStr(m.promotionEndDate), + warehouses: Array.isArray(m.warehouses) && m.warehouses.length > 0 + ? m.warehouses.map((w: ApiWarehouse) => ({ + id: toStr(w.id), + stock: Number(w.stock ?? 0), + })) + : [{ id: "", stock: 0 }], + measurements: Array.isArray(m.productMeasurements) + ? m.productMeasurements.map((ms: ApiMeasurement, mi: number) => ({ + _key: `loaded-meas-${i}-${mi}`, + measurementType: toStr(ms.measurementType), + measurementValue: toStr(ms.measurementValue), + price: toStr(ms.price), + currency: toStr(ms.currency) || "IDR", + weight: toStr(ms.weight), + weightType: toStr(ms.weightType) || "G", + length: toStr(ms.length), + width: toStr(ms.width), + height: toStr(ms.height), + dimensionType: toStr(ms.dimensionType) || "CM", + packagingWeight: toStr(ms.packagingWeight), + packagingWeightType: toStr(ms.packagingWeightType) || "G", + packagingLength: toStr(ms.packagingLength), + packagingWidth: toStr(ms.packagingWidth), + packagingHeight: toStr(ms.packagingHeight), + packagingDimensionType: toStr(ms.packagingDimensionType) || "CM", + hasPromotion: !!ms.isConfigurePromotionPrice, + promotionPrice: toStr(ms.promotionPrice), + promotionCurrency: toStr(ms.promotionCurrency) || toStr(ms.currency) || "IDR", + promotionStartDate: toStr(ms.promotionStartDate), + promotionEndDate: toStr(ms.promotionEndDate), + warehouses: Array.isArray(ms.warehouses) && ms.warehouses.length > 0 + ? ms.warehouses.map((w: ApiWarehouse) => ({ + id: toStr(w.id), + stock: Number(w.stock ?? 0), + })) + : [{ id: "", stock: 0 }], + })) + : [], + })) + : [newModel(0)]; + + return { + categoryId: "", + categoryName: "", + subCategoryId: toStr(data?.subCategory?.id), + subCategoryName: toStr(data?.subCategory?.name), + name: toStr(data?.name), + description: toStr(data?.description), + isPreOrder: !!data?.isPreOrder, + preOrderDay: toStr(data?.preOrderDay), + isNew: data?.isNew !== false, + isEligibleToExport: !!data?.isEligibleToExport, + imageId: toStr(data?.imageId), + productImages: Array.isArray(data?.productImages) + ? data.productImages + .sort( + (a: ApiProductImage, b: ApiProductImage) => + (a.sequence ?? 0) - (b.sequence ?? 0) + ) + .map((img: ApiProductImage) => toStr(img.imageId)) + : [], + keywords: Array.isArray(data?.productKeyWords) ? data.productKeyWords.filter(Boolean) : [], + features: Array.isArray(data?.productFeatures) ? data.productFeatures.filter(Boolean) : [], + models, + productInformations: toParamItems(data?.productInformations), + categoryInformations: toParamItems(data?.categoryInformations), + complianceInformation: { + safetyWarning: toStr(data?.complianceInformation?.safetyWarning), + countryOfOrigin: toStr(data?.complianceInformation?.countryOfOrigin), + isDangerousGoodRegulation: !!data?.complianceInformation?.isDangerousGoodRegulation, + fileId: toStr(data?.complianceInformation?.fileId ?? data?.complianceInformation?.file), + }, + warrantyInformation: { + type: toStr(data?.warrantyInformation?.type), + duration: toStr(data?.warrantyInformation?.duration), + durationType: toStr(data?.warrantyInformation?.durationType) || "MONTH", + }, + }; +} + +const MAX_IMAGES = 8; +const inputCls = "w-full bg-surface-container-low rounded-xl border border-outline-variant/10 p-3.5 text-sm font-semibold focus:ring-2 focus:ring-primary/10 focus:outline-none"; + +// ─── Sub-components ─────────────────────────────────────────────────────────── + +function SectionHeader({ step, title }: { step: string; title: string }) { + return ( +
+
+ {step} +
+

{title}

+
+ ); +} + +function ImageSlotUpload({ + fileId, + label, + onUploaded, + onRemove, +}: { + fileId: string; + label: string; + onUploaded: (id: string) => void; + onRemove: () => void; +}) { + const { t } = useLanguage(); + const e = t.dashboard.productEdit; + const inputRef = useRef(null); + const [uploading, setUploading] = useState(false); + const [error, setError] = useState(""); + + async function handleChange(e: React.ChangeEvent) { + const file = e.target.files?.[0]; + if (!file) return; + setUploading(true); + setError(""); + try { + const fd = new FormData(); + fd.append("file", file); + const res = await fetch("/api/upload", { + method: "POST", + headers: { "x-auth-token": getToken() }, + body: fd, + }); + const data = await res.json(); + if (!res.ok) throw new Error(data?.responseDesc || data?.error || "Upload gagal"); + const id = data?.data?.id || data?.data?.fileId || data?.fileId || ""; + if (!id) throw new Error("File id tidak ditemukan"); + onUploaded(id); + } catch (err) { + setError(err instanceof Error ? err.message : "Upload gagal"); + } finally { + setUploading(false); + if (inputRef.current) inputRef.current.value = ""; + } + } + + return ( +
+
+ + {fileId ? "image" : "add_photo_alternate"} + +
+
+

{label}

+

{fileId ? e.uploaded : e.noImage}

+ {error &&

{error}

} +
+
+ + +
+ +
+ ); +} + +function ModelImageUpload({ value, onUploaded }: { value: string; onUploaded: (id: string) => void }) { + const { t } = useLanguage(); + const e = t.dashboard.productEdit; + const inputRef = useRef(null); + const [uploading, setUploading] = useState(false); + const [error, setError] = useState(""); + const [previewUrl, setPreviewUrl] = useState(""); + + async function handleChange(e: React.ChangeEvent) { + const file = e.target.files?.[0]; + if (!file) return; + const objectUrl = URL.createObjectURL(file); + setPreviewUrl(objectUrl); + setUploading(true); + setError(""); + try { + const fd = new FormData(); + fd.append("file", file); + const res = await fetch("/api/upload", { + method: "POST", + headers: { "x-auth-token": getToken() }, + body: fd, + }); + const data = await res.json(); + if (!res.ok) throw new Error(data?.responseDesc || data?.error || "Upload gagal"); + const id = data?.data?.id || data?.data?.fileId || data?.fileId || ""; + if (!id) throw new Error("File id tidak ditemukan"); + onUploaded(id); + } catch (err) { + setError(err instanceof Error ? err.message : "Upload gagal"); + setPreviewUrl(""); + } finally { + setUploading(false); + if (inputRef.current) inputRef.current.value = ""; + } + } + + const hasImage = value || previewUrl; + + return ( +
!uploading && inputRef.current?.click()} + > + {hasImage ? ( + <> +
+ {previewUrl ? ( + Model preview + ) : ( +
+ image +

{e.uploaded}

+
+ )} +
+
+ + {uploading ? e.uploading : e.changeImage} + +
+ + ) : ( +
+ {uploading ? ( +
+
+

{e.uploading}

+
+ ) : ( + <> + add_photo_alternate +

{e.uploadModelImage}

+ + )} +
+ )} + {error && ( +
{error}
+ )} + +
+ ); +} + +function MeasurementCard({ + measurement, + index, + warehouses, + onChange, + onRemove, +}: { + measurement: EditMeasurement; + index: number; + warehouses: WarehouseOption[]; + onChange: (updated: EditMeasurement) => void; + onRemove: () => void; +}) { + const { t } = useLanguage(); + const e = t.dashboard.productEdit; + + function set(field: keyof EditMeasurement, value: unknown) { + onChange({ ...measurement, [field]: value }); + } + + function updateWh(wi: number, field: "id" | "stock", value: string | number) { + onChange({ ...measurement, warehouses: measurement.warehouses.map((w, i) => i === wi ? { ...w, [field]: value } : w) }); + } + + const inp = "w-full bg-white border border-neutral-200 rounded-lg px-3 py-2 text-xs font-bold focus:ring-1 focus:ring-primary/20 focus:outline-none"; + const sel = "w-full bg-white border border-neutral-200 rounded-lg px-3 py-2 text-[10px] font-bold focus:ring-1 focus:ring-primary/20 focus:outline-none"; + + return ( +
+
+ {e.measurementLabel} {String(index + 1).padStart(2, "0")} +
+ + +
+ {/* Core Information */} +
+

{e.coreInformation}

+
+
+ + set("measurementType", ev.target.value)} /> +
+
+ + set("measurementValue", ev.target.value)} /> +
+
+
+ + {/* Pricing & Physical Specs */} +
+

{e.pricingSpecs}

+
+
+ + set("price", ev.target.value)} /> +
+
+ + +
+
+ + +
+
+ + set("weight", ev.target.value)} /> +
+
+ +
+ + set("length", ev.target.value)} /> + set("width", ev.target.value)} /> + set("height", ev.target.value)} /> +
+
+
+
+ + {/* Promotion & Packaging */} +
+ {/* Promotion */} +
+
+ {e.promotion} + +
+
+
+ + set("promotionPrice", ev.target.value)} /> +
+
+ + +
+
+
+ + set("promotionStartDate", ev.target.value)} /> +
+
+ + set("promotionEndDate", ev.target.value)} /> +
+
+
+
+ + {/* Packaging */} +
+ {e.packaging} +
+ +
+ + set("packagingWeight", ev.target.value)} /> +
+
+
+ +
+ + set("packagingLength", ev.target.value)} /> + set("packagingWidth", ev.target.value)} /> + set("packagingHeight", ev.target.value)} /> +
+
+
+
+ + {/* Measurement Warehouses */} +
+
+ {e.stock} — {e.measurementLabel} {String(index + 1).padStart(2, "0")} + +
+
+ {measurement.warehouses.map((wh, wi) => ( +
+ + updateWh(wi, "stock", Number(ev.target.value || 0))} + placeholder={e.stock} type="number" min="0" + className="w-24 bg-white border border-neutral-200 rounded-xl p-3 text-sm font-semibold text-center focus:ring-2 focus:ring-primary/10 focus:outline-none" /> + {measurement.warehouses.length > 1 && ( + + )} +
+ ))} +
+
+
+
+ ); +} + +function ModelCard({ + model, + index, + total, + warehouses, + onChange, + onRemove, +}: { + model: EditModel; + index: number; + total: number; + warehouses: WarehouseOption[]; + onChange: (m: EditModel) => void; + onRemove: () => void; +}) { + const { t } = useLanguage(); + const e = t.dashboard.productEdit; + + function set(field: keyof EditModel, value: unknown) { + onChange({ ...model, [field]: value }); + } + + function updateWh(wi: number, field: "id" | "stock", value: string | number) { + onChange({ ...model, warehouses: model.warehouses.map((w, i) => i === wi ? { ...w, [field]: value } : w) }); + } + + const hasMeasurements = model.measurements.length > 0; + + const inp = "w-full bg-surface-container-low border border-outline-variant/10 rounded-xl px-3 py-2.5 text-sm font-semibold focus:ring-2 focus:ring-primary/10 focus:outline-none"; + const sel = "w-full bg-surface-container-low border border-outline-variant/10 rounded-xl px-3 py-2.5 text-xs font-bold focus:ring-2 focus:ring-primary/10 focus:outline-none"; + + return ( +
+ {/* Header */} +
+
+
{index + 1}
+
+ set("name", ev.target.value)} + placeholder={`Model ${String(index + 1).padStart(2, "0")} Name`} + className="bg-transparent border-none text-sm font-black tracking-wide text-on-surface focus:ring-0 p-0 placeholder-outline/40 w-48" + /> + set("sku", ev.target.value)} + placeholder="SKU" + className="bg-transparent border-none text-xs font-bold text-outline focus:ring-0 p-0 placeholder-outline/30 w-32" + /> +
+
+ {total > 1 && ( + + )} +
+ +
+ {/* Image + Pricing Grid */} +
+ {/* Model Image */} +
+ + set("imageId", id)} /> +
+ + {/* Pricing & Specs */} +
+ {/* Price row */} +
+
+ + set("price", ev.target.value)} placeholder="0" type="number" min="0" className={inp} /> +
+
+ + +
+
+ + {/* Weight row */} +
+ +
+ + set("weight", ev.target.value)} placeholder="0" type="number" min="0" className={inp} /> +
+
+ + {/* Dimensions row */} +
+ +
+ + set("length", ev.target.value)} placeholder="L" type="number" min="0" className={inp + " text-center"} /> + set("width", ev.target.value)} placeholder="W" type="number" min="0" className={inp + " text-center"} /> + set("height", ev.target.value)} placeholder="H" type="number" min="0" className={inp + " text-center"} /> +
+
+
+
+ + {/* Promotion & Packaging */} +
+ {/* Promotion */} +
+
+ {e.promotion} + +
+
+
+
+ + set("promotionPrice", ev.target.value)} placeholder="0" type="number" min="0" className={inp} /> +
+
+ + +
+
+
+
+ + set("promotionStartDate", ev.target.value)} className={inp} /> +
+
+ + set("promotionEndDate", ev.target.value)} className={inp} /> +
+
+
+
+ + {/* Packaging */} +
+ {e.packagingFootprint} +
+ +
+ + set("packagingWeight", ev.target.value)} placeholder="0" type="number" min="0" className={inp} /> +
+
+
+ +
+ + set("packagingLength", ev.target.value)} placeholder="L" type="number" min="0" className={inp + " text-center"} /> + set("packagingWidth", ev.target.value)} placeholder="W" type="number" min="0" className={inp + " text-center"} /> + set("packagingHeight", ev.target.value)} placeholder="H" type="number" min="0" className={inp + " text-center"} /> +
+
+
+
+ + {/* Model Warehouses */} +
+
+ + +
+
+ {model.warehouses.map((wh, wi) => ( +
+ + updateWh(wi, "stock", Number(ev.target.value || 0))} placeholder={e.stock} type="number" min="0" + className="w-24 bg-surface-container-low rounded-xl border border-outline-variant/10 p-3.5 text-sm font-semibold text-center focus:ring-2 focus:ring-primary/10 focus:outline-none" /> + {model.warehouses.length > 1 && ( + + )} +
+ ))} +
+
+ + {/* Measurements */} +
+
+
+ +

{hasMeasurements ? `${model.measurements.length} ${e.variantAdded}` : e.addSizeVariant}

+
+ +
+ {model.measurements.length > 0 && ( +
+ {model.measurements.map((ms, mi) => ( + onChange({ ...model, measurements: model.measurements.map((m, i) => i === mi ? updated : m) })} + onRemove={() => onChange({ ...model, measurements: model.measurements.filter((_, i) => i !== mi) })} + /> + ))} +
+ )} +
+
+
+ ); +} + +// ─── Main Page ──────────────────────────────────────────────────────────────── + +export default function EditProductPage() { + return ( + + + + ); +} + +function EditProductPageInner() { + const params = useParams<{ productId: string }>(); + const router = useRouter(); + const searchParams = useSearchParams(); + const isDraftParam = searchParams.get("draft") === "1"; + const { t } = useLanguage(); + const e = t.dashboard.productEdit; + + const [form, setForm] = useState(null); + const [loading, setLoading] = useState(true); + const [loadError, setLoadError] = useState(""); + const [saving, setSaving] = useState(false); + const [saveError, setSaveError] = useState(""); + const [saveSuccess, setSaveSuccess] = useState(false); + const [errorLog, setErrorLog] = useState<{ request: unknown; response: unknown } | null>(null); + const [errorLogCopied, setErrorLogCopied] = useState(false); + + const [productState, setProductState] = useState(isDraftParam ? "DRAFT" : ""); + const [categories, setCategories] = useState([]); + const [subCategories, setSubCategories] = useState([]); + const [loadingSubcategories, setLoadingSubcategories] = useState(false); + const [warehouses, setWarehouses] = useState([]); + const [keywordInput, setKeywordInput] = useState(""); + const [debugPayload, setDebugPayload] = useState(""); + const [publishing, setPublishing] = useState(false); + const [publishSuccess, setPublishSuccess] = useState(false); + + // Load product, categories (with subcategory resolution), and warehouses + useEffect(() => { + if (!params.productId) return; + + async function loadAll() { + setLoading(true); + setLoadError(""); + try { + const token = getToken(); + const [productRes, catRes, whRes] = await Promise.all([ + fetch(`/api/products/${params.productId}${isDraftParam ? "?draft=1" : ""}`, { headers: { "x-auth-token": token } }), + fetch("/api/products/categories?size=100", { headers: { "x-auth-token": token } }), + fetch("/api/products/warehouses?size=100", { headers: { "x-auth-token": token } }), + ]); + + const [productJson, catJson, whJson] = await Promise.all([ + productRes.json(), + catRes.json(), + whRes.json(), + ]); + + if (!productRes.ok) throw new Error(productJson?.responseDesc || "Gagal memuat produk"); + + const rawProduct = productJson?.data || productJson; + const editState = apiToEditState(rawProduct); + const subCategoryId = editState.subCategoryId; + + const cats: CategoryOption[] = Array.isArray(catJson?.rows) ? catJson.rows : []; + + // Resolve which main category owns this subcategory + if (subCategoryId && cats.length > 0) { + const subCatResults = await Promise.all( + cats.map((cat) => + fetch(`/api/products/subcategories/${cat.id}?size=100`, { headers: { "x-auth-token": token } }) + .then((r) => r.json()) + .then((j) => ({ cat, rows: Array.isArray(j?.rows) ? j.rows as CategoryOption[] : [] })) + .catch(() => ({ cat, rows: [] as CategoryOption[] })) + ) + ); + for (const { cat, rows } of subCatResults) { + const match = rows.find((s) => s.id === subCategoryId); + if (match) { + editState.categoryId = cat.id; + editState.categoryName = cat.name; + editState.subCategoryName = match.name; + break; + } + } + } + + setProductState(isDraftParam ? "DRAFT" : toStr(rawProduct?.state)); + setCategories(cats); + setForm(editState); + setWarehouses( + Array.isArray(whJson?.rows) ? whJson.rows : + Array.isArray(whJson?.data) ? whJson.data : [] + ); + } catch (err) { + setLoadError(err instanceof Error ? err.message : "Gagal memuat data"); + } finally { + setLoading(false); + } + } + + loadAll(); + }, [params.productId]); + + // Load subcategories when category changes (for draft editing) + useEffect(() => { + if (!form?.categoryId || productState !== "DRAFT") { + setSubCategories([]); + return; + } + setLoadingSubcategories(true); + fetch(`/api/products/subcategories/${form.categoryId}?size=100`, { + headers: { "x-auth-token": getToken() }, + }) + .then((r) => r.json()) + .then((j) => setSubCategories(Array.isArray(j?.rows) ? j.rows : [])) + .catch(() => setSubCategories([])) + .finally(() => setLoadingSubcategories(false)); + }, [form?.categoryId, productState]); + + function update(patch: Partial) { + setForm((prev) => prev ? { ...prev, ...patch } : prev); + } + + function updateModel(index: number, model: EditModel) { + if (!form) return; + setForm({ ...form, models: form.models.map((m, i) => i === index ? model : m) }); + } + + function addKeyword() { + if (!form) return; + const v = keywordInput.trim(); + if (!v) return; + if (!form.keywords.includes(v)) update({ keywords: [...form.keywords, v] }); + setKeywordInput(""); + } + + function buildPayload(state?: "DRAFT" | "PUBLISHED") { + if (!form) return null; + const base = { + subCategory: form.subCategoryId ? { id: form.subCategoryId } : undefined, + name: form.name, + description: form.description, + isPreOrder: form.isPreOrder, + preOrderDay: form.isPreOrder ? toNum(form.preOrderDay) : 0, + isNew: form.isNew, + isEligibleToExport: form.isEligibleToExport, + imageId: form.imageId || undefined, + productImages: form.productImages.filter(Boolean).map((imageId, i) => ({ imageId, sequence: i + 1 })), + productKeyWords: form.keywords.filter(Boolean), + productFeatures: form.features.filter(Boolean), + productModels: form.models.map((m) => ({ + name: m.name, + sku: m.sku, + imageId: m.imageId || undefined, + price: toNum(m.price), + currency: m.currency, + weight: toNum(m.weight), + weightType: m.weightType || "G", + length: toNum(m.length), + width: toNum(m.width), + height: toNum(m.height), + dimensionType: m.dimensionType || "CM", + isMeasurement: m.measurements.length > 0, + isConfigurePromotionPrice: m.hasPromotion, + promotionPrice: m.hasPromotion ? toNum(m.promotionPrice) : 0, + promotionCurrency: m.promotionCurrency || m.currency, + promotionStartDate: m.promotionStartDate || undefined, + promotionEndDate: m.promotionEndDate || undefined, + packagingWeight: toNum(m.packagingWeight), + packagingWeightType: m.packagingWeightType || "G", + packagingLength: toNum(m.packagingLength), + packagingWidth: toNum(m.packagingWidth), + packagingHeight: toNum(m.packagingHeight), + packagingDimensionType: m.packagingDimensionType || "CM", + warehouses: m.warehouses.filter((w) => w.id).map((w) => ({ id: w.id, stock: Number(w.stock || 0) })), + productMeasurements: m.measurements.map((ms) => ({ + measurementType: ms.measurementType, + measurementValue: ms.measurementValue, + price: toNum(ms.price), + currency: ms.currency || "IDR", + weight: toNum(ms.weight), + weightType: ms.weightType || "G", + length: toNum(ms.length), + width: toNum(ms.width), + height: toNum(ms.height), + dimensionType: ms.dimensionType || "CM", + packagingWeight: toNum(ms.packagingWeight), + packagingWeightType: ms.packagingWeightType || "G", + packagingLength: toNum(ms.packagingLength), + packagingWidth: toNum(ms.packagingWidth), + packagingHeight: toNum(ms.packagingHeight), + packagingDimensionType: ms.packagingDimensionType || "CM", + isConfigurePromotionPrice: ms.hasPromotion, + promotionPrice: ms.hasPromotion ? toNum(ms.promotionPrice) : 0, + promotionCurrency: ms.promotionCurrency || ms.currency || "IDR", + promotionStartDate: ms.promotionStartDate || undefined, + promotionEndDate: ms.promotionEndDate || undefined, + warehouses: ms.warehouses.filter((w) => w.id).map((w) => ({ id: w.id, stock: Number(w.stock || 0) })), + })), + })), + productInformations: form.productInformations.filter((i) => i.paramName && i.paramValue), + categoryInformations: form.categoryInformations.filter((i) => i.paramName && i.paramValue), + complianceInformation: { ...form.complianceInformation }, + warrantyInformation: { ...form.warrantyInformation, duration: toNum(form.warrantyInformation.duration) }, + }; + return state ? { ...base, state } : base; + } + + async function handleSaveDraft() { + if (!form) return; + setSaving(true); + setSaveError(""); + setSaveSuccess(false); + setErrorLog(null); + setErrorLogCopied(false); + + try { + const payload = buildPayload(); + setDebugPayload(JSON.stringify(payload, null, 2)); + + const res = await fetch(`/api/products/${params.productId}?draft=1`, { + method: "PUT", + headers: { "Content-Type": "application/json", "x-auth-token": getToken() }, + body: JSON.stringify(payload), + }); + const result = await res.json(); + if (!res.ok) { + setErrorLog({ request: payload, response: result }); + throw new Error(result?.responseDesc || "Gagal menyimpan draft"); + } + + setSaveSuccess(true); + setTimeout(() => setSaveSuccess(false), 3000); + } catch (err) { + setSaveError(err instanceof Error ? err.message : "Gagal menyimpan draft"); + } finally { + setSaving(false); + } + } + + async function handlePublish() { + if (!form) return; + setPublishing(true); + setSaveError(""); + setPublishSuccess(false); + setErrorLog(null); + setErrorLogCopied(false); + + try { + const payload = buildPayload("PUBLISHED"); + setDebugPayload(JSON.stringify(payload, null, 2)); + + const res = await fetch("/api/products/create", { + method: "POST", + headers: { "Content-Type": "application/json", "x-auth-token": getToken() }, + body: JSON.stringify(payload), + }); + const result = await res.json(); + if (!res.ok) { + setErrorLog({ request: payload, response: result }); + throw new Error(result?.responseDesc || "Gagal mempublikasikan produk"); + } + + setPublishSuccess(true); + setTimeout(() => router.push("/products"), 1500); + } catch (err) { + setSaveError(err instanceof Error ? err.message : "Gagal mempublikasikan produk"); + } finally { + setPublishing(false); + } + } + + async function handleSave() { + if (!form) return; + setSaving(true); + setSaveError(""); + setSaveSuccess(false); + setErrorLog(null); + setErrorLogCopied(false); + + try { + const payload = buildPayload(); + const payloadJson = JSON.stringify(payload, null, 2); + console.log("[EditProduct] PUT payload:", payloadJson); + setDebugPayload(payloadJson); + + const res = await fetch(`/api/products/${params.productId}`, { + method: "PUT", + headers: { "Content-Type": "application/json", "x-auth-token": getToken() }, + body: JSON.stringify(payload), + }); + const result = await res.json(); + if (!res.ok) { + setErrorLog({ request: payload, response: result }); + throw new Error(result?.responseDesc || "Gagal menyimpan produk"); + } + + const reviewRes = await fetch(`/api/products/submit-review/${params.productId}`, { + method: "POST", + headers: { "x-auth-token": getToken() }, + }); + const reviewResult = await reviewRes.json(); + if (!reviewRes.ok) { + setErrorLog({ request: { submitReview: params.productId }, response: reviewResult }); + throw new Error(reviewResult?.responseDesc || "Gagal mengirim produk ke review"); + } + + setSaveSuccess(true); + setTimeout(() => router.back(), 1500); + } catch (err) { + setSaveError(err instanceof Error ? err.message : "Gagal menyimpan produk"); + } finally { + setSaving(false); + } + } + + // ── Loading / Error states ── + if (loading) { + return ( +
+

{e.loading}

+
+ ); + } + + if (loadError || !form) { + return ( +
+
+ {loadError || e.productNotFound} +
+
+ ); + } + + // ── Render ── + return ( +
+ {/* Page header */} +
+ +
+
+

{e.title}

+

{form.name || "—"}

+
+ +
+
+ + {/* ── Section 01: Basic Details ──────────────────────────────────────── */} +
+ + + {productState === "DRAFT" ? ( +
+
+ + +
+
+ + +
+
+ ) : ( +
+
+ +
+ lock + + {form.categoryName || (form.categoryId ? form.categoryId : )} + +
+
+
+ +
+ lock + + {form.subCategoryName || (form.subCategoryId ? form.subCategoryId : )} + +
+

{e.categoryLocked}

+
+
+ )} +
+ + {/* ── Section 02: Description ────────────────────────────────────────── */} +
+ {/* Left: text fields */} +
+ + + {/* Name */} +
+ + update({ name: ev.target.value })} + placeholder="e.g. Vintage Italian Leather Executive Chair" className={inputCls} /> +
+ + {/* Toggles */} +
+ + +
+ + {form.isPreOrder && ( +
+ + update({ preOrderDay: ev.target.value })} + placeholder="14" type="number" min="0" className={inputCls} /> +
+ )} + + {/* Keywords */} +
+ +
+ setKeywordInput(ev.target.value)} + onKeyDown={(ev) => { if (ev.key === "Enter") { ev.preventDefault(); addKeyword(); } }} + placeholder={e.addKeyword} className={inputCls + " flex-1"} /> + +
+
+ {form.keywords.map((k) => ( + + ))} +
+
+ + {/* Description */} +
+ +