version 2 initial commit
This commit is contained in:
parent
89ceb23c5a
commit
125b1f9d26
24 changed files with 2715 additions and 5302 deletions
|
|
@ -1,11 +1,16 @@
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en" data-theme="light">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>sellercentral</title>
|
<title>sellercentral</title>
|
||||||
</head>
|
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Libre+Caslon+Text:ital,wght@0,400;0,700;1,400&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@24,400,0,0" />
|
||||||
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
<script type="module" src="/src/main.tsx"></script>
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
|
|
||||||
389
package-lock.json
generated
389
package-lock.json
generated
|
|
@ -10,7 +10,8 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bulma": "^1.0.4",
|
"bulma": "^1.0.4",
|
||||||
"react": "^19.2.8",
|
"react": "^19.2.8",
|
||||||
"react-dom": "^19.2.8"
|
"react-dom": "^19.2.8",
|
||||||
|
"recharts": "^3.10.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@playwright/test": "^1.49.0",
|
"@playwright/test": "^1.49.0",
|
||||||
|
|
@ -779,6 +780,32 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@reduxjs/toolkit": {
|
||||||
|
"version": "2.12.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.12.0.tgz",
|
||||||
|
"integrity": "sha512-KiT+RzZbp6mQET+Mg+h2c97+9j1sNflUxQkIHI7Yuzf6Peu+OYpmkn6nbHWmLLWj+1ZODUJFwGZ7gx3L9R9EOw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@standard-schema/spec": "^1.0.0",
|
||||||
|
"@standard-schema/utils": "^0.3.0",
|
||||||
|
"immer": "^11.0.0",
|
||||||
|
"redux": "^5.0.1",
|
||||||
|
"redux-thunk": "^3.1.0",
|
||||||
|
"reselect": "^5.1.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.9.0 || ^17.0.0 || ^18 || ^19",
|
||||||
|
"react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react-redux": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@rolldown/binding-android-arm64": {
|
"node_modules/@rolldown/binding-android-arm64": {
|
||||||
"version": "1.2.3",
|
"version": "1.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.3.tgz",
|
||||||
|
|
@ -1028,7 +1055,12 @@
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
|
||||||
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
|
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
|
||||||
"dev": true,
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@standard-schema/utils": {
|
||||||
|
"version": "0.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz",
|
||||||
|
"integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/node": {
|
"node_modules/@tailwindcss/node": {
|
||||||
|
|
@ -1659,6 +1691,69 @@
|
||||||
"assertion-error": "^2.0.1"
|
"assertion-error": "^2.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/d3-array": {
|
||||||
|
"version": "3.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz",
|
||||||
|
"integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-color": {
|
||||||
|
"version": "3.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
|
||||||
|
"integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-ease": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-interpolate": {
|
||||||
|
"version": "3.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
|
||||||
|
"integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/d3-color": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-path": {
|
||||||
|
"version": "3.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz",
|
||||||
|
"integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-scale": {
|
||||||
|
"version": "4.0.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz",
|
||||||
|
"integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/d3-time": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-shape": {
|
||||||
|
"version": "3.1.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz",
|
||||||
|
"integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/d3-path": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-time": {
|
||||||
|
"version": "3.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz",
|
||||||
|
"integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-timer": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/deep-eql": {
|
"node_modules/@types/deep-eql": {
|
||||||
"version": "4.0.2",
|
"version": "4.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
|
||||||
|
|
@ -1687,7 +1782,7 @@
|
||||||
"version": "19.2.18",
|
"version": "19.2.18",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.18.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.18.tgz",
|
||||||
"integrity": "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==",
|
"integrity": "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"csstype": "^3.2.2"
|
"csstype": "^3.2.2"
|
||||||
|
|
@ -1724,6 +1819,12 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/use-sync-external-store": {
|
||||||
|
"version": "0.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
|
||||||
|
"integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@vitejs/plugin-react": {
|
"node_modules/@vitejs/plugin-react": {
|
||||||
"version": "6.0.5",
|
"version": "6.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.5.tgz",
|
||||||
|
|
@ -2291,6 +2392,15 @@
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/clsx": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/color-convert": {
|
"node_modules/color-convert": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||||
|
|
@ -2416,7 +2526,7 @@
|
||||||
"version": "3.2.3",
|
"version": "3.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||||
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/cypress": {
|
"node_modules/cypress": {
|
||||||
|
|
@ -2473,6 +2583,127 @@
|
||||||
"node": "^20.1.0 || ^22.0.0 || >=24.0.0"
|
"node": "^20.1.0 || ^22.0.0 || >=24.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/d3-array": {
|
||||||
|
"version": "3.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
|
||||||
|
"integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"internmap": "1 - 2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-color": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-ease": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-format": {
|
||||||
|
"version": "3.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz",
|
||||||
|
"integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-interpolate": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"d3-color": "1 - 3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-path": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-scale": {
|
||||||
|
"version": "4.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
|
||||||
|
"integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"d3-array": "2.10.0 - 3",
|
||||||
|
"d3-format": "1 - 3",
|
||||||
|
"d3-interpolate": "1.2.0 - 3",
|
||||||
|
"d3-time": "2.1.1 - 3",
|
||||||
|
"d3-time-format": "2 - 4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-shape": {
|
||||||
|
"version": "3.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
|
||||||
|
"integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"d3-path": "^3.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-time": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"d3-array": "2 - 3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-time-format": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
|
||||||
|
"integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"d3-time": "1 - 3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-timer": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/dashdash": {
|
"node_modules/dashdash": {
|
||||||
"version": "1.14.1",
|
"version": "1.14.1",
|
||||||
"resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
|
"resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
|
||||||
|
|
@ -2547,6 +2778,12 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/decimal.js-light": {
|
||||||
|
"version": "2.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz",
|
||||||
|
"integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/delayed-stream": {
|
"node_modules/delayed-stream": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||||
|
|
@ -2723,6 +2960,18 @@
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/es-toolkit": {
|
||||||
|
"version": "1.51.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.51.0.tgz",
|
||||||
|
"integrity": "sha512-zC2lQGkM7QX+Gm6iM3+WIdZJzthsEd14LvRNJneSO2hzyz/zNBENR8+YXWo1cKxgPBtV6ksPYHELbcwBRzmdCw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"workspaces": [
|
||||||
|
"docs",
|
||||||
|
"benchmarks",
|
||||||
|
"tests/types",
|
||||||
|
"tests/browser-compat"
|
||||||
|
]
|
||||||
|
},
|
||||||
"node_modules/estree-walker": {
|
"node_modules/estree-walker": {
|
||||||
"version": "3.0.3",
|
"version": "3.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
|
||||||
|
|
@ -2744,7 +2993,6 @@
|
||||||
"version": "5.0.4",
|
"version": "5.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz",
|
||||||
"integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==",
|
"integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/execa": {
|
"node_modules/execa": {
|
||||||
|
|
@ -3153,6 +3401,16 @@
|
||||||
],
|
],
|
||||||
"license": "BSD-3-Clause"
|
"license": "BSD-3-Clause"
|
||||||
},
|
},
|
||||||
|
"node_modules/immer": {
|
||||||
|
"version": "11.1.18",
|
||||||
|
"resolved": "https://registry.npmjs.org/immer/-/immer-11.1.18.tgz",
|
||||||
|
"integrity": "sha512-EQyQtLiYW029lyoczMl/Hh4Xu7cDecSc58JRYpHyL4tIAu3eqd1yJzQX04d2BZHDkzFFvm6qJEJWOtfDSWAXbQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/immer"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/indent-string": {
|
"node_modules/indent-string": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
|
||||||
|
|
@ -3173,6 +3431,15 @@
|
||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/internmap": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
|
||||||
|
"integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/is-fullwidth-code-point": {
|
"node_modules/is-fullwidth-code-point": {
|
||||||
"version": "5.1.0",
|
"version": "5.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz",
|
||||||
|
|
@ -4274,9 +4541,61 @@
|
||||||
"version": "17.0.2",
|
"version": "17.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
|
||||||
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
|
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/react-redux": {
|
||||||
|
"version": "9.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.3.0.tgz",
|
||||||
|
"integrity": "sha512-KQopgqFo/p/fgmAs5qz6p5RWaNAzq40WAu7fJIXnQpYxFPbJYtsJPWvGeF2rOBaY/kEuV77AVsX8TsQzKm+A/g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/use-sync-external-store": "^0.0.6",
|
||||||
|
"use-sync-external-store": "^1.4.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "^18.2.25 || ^19",
|
||||||
|
"react": "^18.0 || ^19",
|
||||||
|
"redux": "^5.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"redux": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/recharts": {
|
||||||
|
"version": "3.10.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/recharts/-/recharts-3.10.1.tgz",
|
||||||
|
"integrity": "sha512-QXFrvt6IVcw7eeZCoyXTwkIJAX3Dv1nyVhMicXJ47GsGDDpcN8z6o644DibE9XjpBTThtsomLKnTV6lc+cVFUA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"workspaces": [
|
||||||
|
"www"
|
||||||
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"@reduxjs/toolkit": "^1.9.0 || 2.x.x",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"decimal.js-light": "^2.5.1",
|
||||||
|
"es-toolkit": "^1.39.3",
|
||||||
|
"eventemitter3": "^5.0.1",
|
||||||
|
"immer": "^11.1.8",
|
||||||
|
"react-redux": "8.x.x || 9.x.x",
|
||||||
|
"reselect": "5.2.0",
|
||||||
|
"tiny-invariant": "^1.3.3",
|
||||||
|
"use-sync-external-store": "^1.2.2",
|
||||||
|
"victory-vendor": "^37.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||||
|
"react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||||
|
"react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/redent": {
|
"node_modules/redent": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz",
|
||||||
|
|
@ -4291,6 +4610,21 @@
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/redux": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/redux-thunk": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"redux": "^5.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/request-progress": {
|
"node_modules/request-progress": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/request-progress/-/request-progress-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/request-progress/-/request-progress-3.0.0.tgz",
|
||||||
|
|
@ -4311,6 +4645,12 @@
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/reselect": {
|
||||||
|
"version": "5.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/reselect/-/reselect-5.2.0.tgz",
|
||||||
|
"integrity": "sha512-AgZ3UOZm3YndfrJ4OYjgrT7bmCm/1iqkjvEfH/oYjzh6PD2qw4QuT3jjnXIrpdt4MTpMXclMT3lXbmRY+XRakw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/restore-cursor": {
|
"node_modules/restore-cursor": {
|
||||||
"version": "5.1.0",
|
"version": "5.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz",
|
||||||
|
|
@ -4823,6 +5163,12 @@
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tiny-invariant": {
|
||||||
|
"version": "1.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
|
||||||
|
"integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/tinybench": {
|
"node_modules/tinybench": {
|
||||||
"version": "2.9.0",
|
"version": "2.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
|
||||||
|
|
@ -5024,6 +5370,15 @@
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/use-sync-external-store": {
|
||||||
|
"version": "1.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
|
||||||
|
"integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/verror": {
|
"node_modules/verror": {
|
||||||
"version": "1.10.0",
|
"version": "1.10.0",
|
||||||
"resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz",
|
"resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz",
|
||||||
|
|
@ -5039,6 +5394,28 @@
|
||||||
"extsprintf": "^1.2.0"
|
"extsprintf": "^1.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/victory-vendor": {
|
||||||
|
"version": "37.3.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz",
|
||||||
|
"integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==",
|
||||||
|
"license": "MIT AND ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/d3-array": "^3.0.3",
|
||||||
|
"@types/d3-ease": "^3.0.0",
|
||||||
|
"@types/d3-interpolate": "^3.0.1",
|
||||||
|
"@types/d3-scale": "^4.0.2",
|
||||||
|
"@types/d3-shape": "^3.1.0",
|
||||||
|
"@types/d3-time": "^3.0.0",
|
||||||
|
"@types/d3-timer": "^3.0.0",
|
||||||
|
"d3-array": "^3.1.6",
|
||||||
|
"d3-ease": "^3.0.1",
|
||||||
|
"d3-interpolate": "^3.0.1",
|
||||||
|
"d3-scale": "^4.0.2",
|
||||||
|
"d3-shape": "^3.1.0",
|
||||||
|
"d3-time": "^3.0.0",
|
||||||
|
"d3-timer": "^3.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "8.2.1",
|
"version": "8.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-8.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-8.2.1.tgz",
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,8 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bulma": "^1.0.4",
|
"bulma": "^1.0.4",
|
||||||
"react": "^19.2.8",
|
"react": "^19.2.8",
|
||||||
"react-dom": "^19.2.8"
|
"react-dom": "^19.2.8",
|
||||||
|
"recharts": "^3.10.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@playwright/test": "^1.49.0",
|
"@playwright/test": "^1.49.0",
|
||||||
|
|
|
||||||
3233
src/App.tsx
3233
src/App.tsx
File diff suppressed because it is too large
Load diff
42
src/components/dashboard/DashboardLayout.tsx
Normal file
42
src/components/dashboard/DashboardLayout.tsx
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { useSeller } from '../../context/SellerContext';
|
||||||
|
import Sidebar from '../layout/Sidebar';
|
||||||
|
import DashboardHeader from '../layout/DashboardHeader';
|
||||||
|
import OverviewTab from './tabs/OverviewTab';
|
||||||
|
import OrdersTab from './tabs/OrdersTab';
|
||||||
|
import ProductsTab from './tabs/ProductsTab';
|
||||||
|
import AddProductTab from './tabs/AddProductTab';
|
||||||
|
import PaymentsTab from './tabs/PaymentsTab';
|
||||||
|
import SettingsTab from './tabs/SettingsTab';
|
||||||
|
|
||||||
|
export default function DashboardLayout() {
|
||||||
|
const { dashTab } = useSeller();
|
||||||
|
|
||||||
|
const renderContent = () => {
|
||||||
|
switch (dashTab) {
|
||||||
|
case 'overview': return <OverviewTab />;
|
||||||
|
case 'orders': return <OrdersTab />;
|
||||||
|
case 'products': return <ProductsTab />;
|
||||||
|
case 'add-product': return <AddProductTab />;
|
||||||
|
case 'payments': return <PaymentsTab />;
|
||||||
|
case 'settings': return <SettingsTab />;
|
||||||
|
default: return <OverviewTab />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="has-background-white-ter" style={{ minHeight: '100vh', display: 'flex', flexDirection: 'column' }}>
|
||||||
|
<DashboardHeader />
|
||||||
|
<div className="columns is-gapless is-flex-grow-1 is-marginless">
|
||||||
|
<div className="column is-2 is-hidden-mobile has-background-white" style={{ borderRight: '1px solid #e2dfdb' }}>
|
||||||
|
<Sidebar />
|
||||||
|
</div>
|
||||||
|
<div className="column is-10 is-12-mobile">
|
||||||
|
<main className="p-6" style={{ height: '100%' }}>
|
||||||
|
{renderContent()}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
113
src/components/dashboard/tabs/AddProductTab.tsx
Normal file
113
src/components/dashboard/tabs/AddProductTab.tsx
Normal file
|
|
@ -0,0 +1,113 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { useSeller } from '../../../context/SellerContext';
|
||||||
|
|
||||||
|
export default function AddProductTab() {
|
||||||
|
const { setDashTab } = useSeller();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="is-flex is-align-items-center mb-5">
|
||||||
|
<button
|
||||||
|
className="button is-ghost has-text-grey pl-0 mr-3"
|
||||||
|
onClick={() => setDashTab('products')}
|
||||||
|
>
|
||||||
|
<span className="icon"><span className="material-symbols-outlined">arrow_back</span></span>
|
||||||
|
<span>Back</span>
|
||||||
|
</button>
|
||||||
|
<h1 className="title is-3 has-text-dark mb-0" style={{ fontFamily: 'Libre Caslon Text, serif' }}>Add New Product</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="box" style={{ borderTop: '4px solid var(--bulma-primary)' }}>
|
||||||
|
<form onSubmit={(e) => { e.preventDefault(); setDashTab('products'); }}>
|
||||||
|
<div className="columns is-multiline">
|
||||||
|
|
||||||
|
{/* Left Column: Basic Info */}
|
||||||
|
<div className="column is-8">
|
||||||
|
<div className="field mb-4">
|
||||||
|
<label className="label has-text-grey-dark">Product Title</label>
|
||||||
|
<div className="control">
|
||||||
|
<input className="input" type="text" placeholder="e.g. Handwoven Silk Scarf" required />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="field mb-4">
|
||||||
|
<label className="label has-text-grey-dark">Description</label>
|
||||||
|
<div className="control">
|
||||||
|
<textarea className="textarea" placeholder="Describe the artisan craft, material, and story..." rows={4} required></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="columns">
|
||||||
|
<div className="column is-6">
|
||||||
|
<div className="field mb-4">
|
||||||
|
<label className="label has-text-grey-dark">Price (USD)</label>
|
||||||
|
<div className="control has-icons-left">
|
||||||
|
<input className="input" type="number" placeholder="0.00" step="0.01" required />
|
||||||
|
<span className="icon is-small is-left">
|
||||||
|
<span className="material-symbols-outlined">attach_money</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="column is-6">
|
||||||
|
<div className="field mb-4">
|
||||||
|
<label className="label has-text-grey-dark">Stock Quantity</label>
|
||||||
|
<div className="control has-icons-left">
|
||||||
|
<input className="input" type="number" placeholder="10" min="0" required />
|
||||||
|
<span className="icon is-small is-left">
|
||||||
|
<span className="material-symbols-outlined">inventory_2</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right Column: Imagery & Category */}
|
||||||
|
<div className="column is-4">
|
||||||
|
<div className="field mb-4">
|
||||||
|
<label className="label has-text-grey-dark">Product Image</label>
|
||||||
|
<div className="control">
|
||||||
|
<div className="file is-boxed is-fullwidth">
|
||||||
|
<label className="file-label is-flex-direction-column is-align-items-center">
|
||||||
|
<input className="file-input" type="file" name="resume" accept="image/*" />
|
||||||
|
<span className="file-cta is-flex is-flex-direction-column is-align-items-center py-5 has-background-white" style={{ border: '1px solid var(--bulma-primary)' }}>
|
||||||
|
<span className="file-icon mb-2 has-text-primary">
|
||||||
|
<span className="material-symbols-outlined is-size-3">upload</span>
|
||||||
|
</span>
|
||||||
|
<span className="file-label has-text-primary">
|
||||||
|
Choose an image…
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="field mb-4">
|
||||||
|
<label className="label has-text-grey-dark">Category</label>
|
||||||
|
<div className="control">
|
||||||
|
<div className="select is-fullwidth">
|
||||||
|
<select required>
|
||||||
|
<option value="">Select a category</option>
|
||||||
|
<option value="textiles">Apparel & Textiles</option>
|
||||||
|
<option value="jewelry">Jewelry</option>
|
||||||
|
<option value="pottery">Pottery</option>
|
||||||
|
<option value="woodwork">Woodwork</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="is-flex is-justify-content-flex-end mt-5">
|
||||||
|
<button type="button" className="button is-light mr-3" onClick={() => setDashTab('products')}>Cancel</button>
|
||||||
|
<button type="submit" className="button is-primary is-outlined">Publish Product</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
56
src/components/dashboard/tabs/OrdersTab.tsx
Normal file
56
src/components/dashboard/tabs/OrdersTab.tsx
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
export default function OrdersTab() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="is-flex is-justify-content-space-between is-align-items-center mb-5">
|
||||||
|
<h1 className="title is-3 has-text-dark mb-0" style={{ fontFamily: 'Libre Caslon Text, serif' }}>Orders</h1>
|
||||||
|
<button className="button is-primary is-outlined">
|
||||||
|
<span className="icon"><span className="material-symbols-outlined">download</span></span>
|
||||||
|
<span>Export</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="box">
|
||||||
|
<div className="tabs">
|
||||||
|
<ul>
|
||||||
|
<li className="is-active"><a>All Orders</a></li>
|
||||||
|
<li><a>Pending</a></li>
|
||||||
|
<li><a>Shipped</a></li>
|
||||||
|
<li><a>Completed</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<table className="table is-fullwidth is-hoverable is-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Order ID</th>
|
||||||
|
<th>Date</th>
|
||||||
|
<th>Customer</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Total</th>
|
||||||
|
<th>Action</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{[
|
||||||
|
{ id: '#1045', date: 'Oct 24, 2023', customer: 'Jane Doe', status: 'Processing', type: 'info', total: '$120.00' },
|
||||||
|
{ id: '#1044', date: 'Oct 22, 2023', customer: 'Alex Johnson', status: 'Shipped', type: 'warning', total: '$45.50' },
|
||||||
|
{ id: '#1043', date: 'Oct 20, 2023', customer: 'Sarah Williams', status: 'Completed', type: 'success', total: '$210.00' }
|
||||||
|
].map(order => (
|
||||||
|
<tr key={order.id}>
|
||||||
|
<td><strong>{order.id}</strong></td>
|
||||||
|
<td>{order.date}</td>
|
||||||
|
<td>{order.customer}</td>
|
||||||
|
<td><span className={`tag is-${order.type} is-light`}>{order.status}</span></td>
|
||||||
|
<td>{order.total}</td>
|
||||||
|
<td>
|
||||||
|
<button className="button is-small is-light">View</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
116
src/components/dashboard/tabs/OverviewTab.tsx
Normal file
116
src/components/dashboard/tabs/OverviewTab.tsx
Normal file
|
|
@ -0,0 +1,116 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts';
|
||||||
|
|
||||||
|
const data = [
|
||||||
|
{ name: 'Jan', sales: 4000 },
|
||||||
|
{ name: 'Feb', sales: 3000 },
|
||||||
|
{ name: 'Mar', sales: 5000 },
|
||||||
|
{ name: 'Apr', sales: 2780 },
|
||||||
|
{ name: 'May', sales: 8890 },
|
||||||
|
{ name: 'Jun', sales: 14500 },
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function OverviewTab() {
|
||||||
|
const handleDownloadCsv = () => {
|
||||||
|
// Generate CSV string
|
||||||
|
const header = "Month,Sales\n";
|
||||||
|
const rows = data.map(row => `${row.name},${row.sales}`).join("\n");
|
||||||
|
const csvContent = header + rows;
|
||||||
|
|
||||||
|
// Create Blob and trigger download
|
||||||
|
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const link = document.createElement("a");
|
||||||
|
link.setAttribute("href", url);
|
||||||
|
link.setAttribute("download", "sales_report.csv");
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="is-flex is-justify-content-space-between is-align-items-center mb-5">
|
||||||
|
<h1 className="title is-3 has-text-dark mb-0" style={{ fontFamily: 'Libre Caslon Text, serif' }}>Overview</h1>
|
||||||
|
<button className="button is-primary is-outlined" onClick={handleDownloadCsv}>
|
||||||
|
<span className="icon"><span className="material-symbols-outlined">download</span></span>
|
||||||
|
<span>Download Report</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="columns is-multiline">
|
||||||
|
<div className="column is-4">
|
||||||
|
<div className="box" style={{ borderTop: '4px solid var(--bulma-primary)' }}>
|
||||||
|
<p className="heading has-text-grey-dark">Total Sales</p>
|
||||||
|
<p className="title is-2 has-text-dark">$14,500</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="column is-4">
|
||||||
|
<div className="box" style={{ borderTop: '4px solid var(--bulma-primary)' }}>
|
||||||
|
<p className="heading has-text-grey-dark">Active Orders</p>
|
||||||
|
<p className="title is-2 has-text-dark">23</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="column is-4">
|
||||||
|
<div className="box" style={{ borderTop: '4px solid var(--bulma-primary)' }}>
|
||||||
|
<p className="heading has-text-grey-dark">Page Views</p>
|
||||||
|
<p className="title is-2 has-text-dark">1,204</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Sales Chart */}
|
||||||
|
<div className="box mt-4">
|
||||||
|
<h2 className="title is-5 has-text-dark mb-4">Sales Performance (YTD)</h2>
|
||||||
|
<div style={{ width: '100%', height: 300 }}>
|
||||||
|
<ResponsiveContainer>
|
||||||
|
<AreaChart
|
||||||
|
data={data}
|
||||||
|
margin={{ top: 10, right: 30, left: 0, bottom: 0 }}
|
||||||
|
>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="colorSales" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="5%" stopColor="var(--bulma-primary)" stopOpacity={0.8}/>
|
||||||
|
<stop offset="95%" stopColor="var(--bulma-primary)" stopOpacity={0}/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<CartesianGrid strokeDasharray="3 3" vertical={false} />
|
||||||
|
<XAxis dataKey="name" tick={{fill: '#5f5e5b'}} axisLine={false} tickLine={false} />
|
||||||
|
<YAxis tick={{fill: '#5f5e5b'}} axisLine={false} tickLine={false} tickFormatter={(value) => `$${value}`} />
|
||||||
|
<Tooltip formatter={(value) => [`$${value}`, 'Sales']} />
|
||||||
|
<Area type="monotone" dataKey="sales" stroke="var(--bulma-primary)" fillOpacity={1} fill="url(#colorSales)" />
|
||||||
|
</AreaChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="box mt-5">
|
||||||
|
<h2 className="title is-5 has-text-dark">Recent Activity</h2>
|
||||||
|
<table className="table is-fullwidth is-hoverable">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Date</th>
|
||||||
|
<th>Customer</th>
|
||||||
|
<th>Action</th>
|
||||||
|
<th>Status</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>Oct 24, 2023</td>
|
||||||
|
<td>Jane Doe</td>
|
||||||
|
<td>Placed Order #1045</td>
|
||||||
|
<td><span className="tag is-info is-light">Processing</span></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Oct 23, 2023</td>
|
||||||
|
<td>John Smith</td>
|
||||||
|
<td>Left a 5-star review</td>
|
||||||
|
<td><span className="tag is-success is-light">Completed</span></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
51
src/components/dashboard/tabs/PaymentsTab.tsx
Normal file
51
src/components/dashboard/tabs/PaymentsTab.tsx
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
export default function PaymentsTab() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1 className="title is-3 has-text-dark mb-5" style={{ fontFamily: 'Libre Caslon Text, serif' }}>Payments & Earnings</h1>
|
||||||
|
|
||||||
|
<div className="columns is-multiline mb-5">
|
||||||
|
<div className="column is-6">
|
||||||
|
<div className="box has-background-primary has-text-white">
|
||||||
|
<p className="heading has-text-white-ter mb-1">Available for Payout</p>
|
||||||
|
<p className="title is-1 has-text-white mb-4">$1,240.00</p>
|
||||||
|
<button className="button is-white is-outlined is-fullwidth">Withdraw Funds</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="column is-6">
|
||||||
|
<div className="box" style={{ height: '100%' }}>
|
||||||
|
<p className="heading has-text-grey-dark mb-1">Pending Clearance</p>
|
||||||
|
<p className="title is-3 has-text-dark mb-4">$320.00</p>
|
||||||
|
<p className="is-size-7 has-text-grey">Funds will be available in 3-5 business days after order delivery.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="box">
|
||||||
|
<h2 className="title is-5 has-text-dark">Recent Transactions</h2>
|
||||||
|
<table className="table is-fullwidth">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Date</th>
|
||||||
|
<th>Description</th>
|
||||||
|
<th>Amount</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>Oct 20, 2023</td>
|
||||||
|
<td>Payout to Bank Account (...1234)</td>
|
||||||
|
<td className="has-text-danger">-$500.00</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Oct 19, 2023</td>
|
||||||
|
<td>Order #1042 Revenue</td>
|
||||||
|
<td className="has-text-success">+$85.00</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
51
src/components/dashboard/tabs/ProductsTab.tsx
Normal file
51
src/components/dashboard/tabs/ProductsTab.tsx
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { useSeller } from '../../../context/SellerContext';
|
||||||
|
|
||||||
|
export default function ProductsTab() {
|
||||||
|
const { setDashTab } = useSeller();
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="is-flex is-justify-content-space-between is-align-items-center mb-5">
|
||||||
|
<h1 className="title is-3 has-text-dark mb-0" style={{ fontFamily: 'Libre Caslon Text, serif' }}>Products</h1>
|
||||||
|
<div className="buttons">
|
||||||
|
<button className="button is-light" onClick={() => alert('Bulk upload coming soon!')}>
|
||||||
|
<span className="icon"><span className="material-symbols-outlined">upload_file</span></span>
|
||||||
|
<span>Bulk Upload</span>
|
||||||
|
</button>
|
||||||
|
<button className="button is-primary is-outlined" onClick={() => setDashTab('add-product')}>
|
||||||
|
<span className="icon"><span className="material-symbols-outlined">add</span></span>
|
||||||
|
<span>Add Product</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="columns is-multiline">
|
||||||
|
{[1, 2, 3, 4].map(item => (
|
||||||
|
<div className="column is-3" key={item}>
|
||||||
|
<div className="card" style={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
|
||||||
|
<div className="card-image">
|
||||||
|
<figure className="image is-4by3 has-background-light">
|
||||||
|
<div className="is-flex is-align-items-center is-justify-content-center" style={{ height: '100%' }}>
|
||||||
|
<span className="material-symbols-outlined has-text-grey-light is-size-1">image</span>
|
||||||
|
</div>
|
||||||
|
</figure>
|
||||||
|
</div>
|
||||||
|
<div className="card-content is-flex-grow-1">
|
||||||
|
<p className="title is-6 mb-2">Handwoven Silk Scarf</p>
|
||||||
|
<p className="subtitle is-6 has-text-primary has-text-weight-bold">$85.00</p>
|
||||||
|
<div className="is-flex is-align-items-center is-size-7 has-text-grey">
|
||||||
|
<span className="icon is-small mr-1"><span className="material-symbols-outlined is-size-7">inventory</span></span>
|
||||||
|
<span>12 in stock</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<footer className="card-footer">
|
||||||
|
<a href="#" className="card-footer-item has-text-grey">Edit</a>
|
||||||
|
<a href="#" className="card-footer-item has-text-danger">Delete</a>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
91
src/components/dashboard/tabs/SettingsTab.tsx
Normal file
91
src/components/dashboard/tabs/SettingsTab.tsx
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { useSeller } from '../../../context/SellerContext';
|
||||||
|
|
||||||
|
export default function SettingsTab() {
|
||||||
|
const { storeSlug, supportEmail, supportPhone } = useSeller();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="columns is-centered">
|
||||||
|
<div className="column is-8">
|
||||||
|
<h1 className="title is-3 has-text-dark mb-5" style={{ fontFamily: 'Libre Caslon Text, serif' }}>Settings</h1>
|
||||||
|
|
||||||
|
<div className="box mb-5">
|
||||||
|
<h2 className="title is-5 has-text-primary border-bottom pb-2" style={{ borderBottom: '1px solid #eee' }}>Store Profile</h2>
|
||||||
|
|
||||||
|
<div className="field">
|
||||||
|
<label className="label">Store Name</label>
|
||||||
|
<div className="control">
|
||||||
|
<input className="input" type="text" defaultValue="My Artisan Store" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="field">
|
||||||
|
<label className="label">Store URL Slug</label>
|
||||||
|
<div className="control">
|
||||||
|
<input className="input" type="text" value={storeSlug} readOnly />
|
||||||
|
</div>
|
||||||
|
<p className="help">Contact support to change your store URL.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="field">
|
||||||
|
<label className="label">Store Description</label>
|
||||||
|
<div className="control">
|
||||||
|
<textarea className="textarea" placeholder="Tell buyers about your craft..."></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="box mb-5">
|
||||||
|
<h2 className="title is-5 has-text-primary border-bottom pb-2" style={{ borderBottom: '1px solid #eee' }}>Contact Information</h2>
|
||||||
|
|
||||||
|
<div className="columns">
|
||||||
|
<div className="column is-half">
|
||||||
|
<div className="field">
|
||||||
|
<label className="label">Support Email</label>
|
||||||
|
<div className="control">
|
||||||
|
<input className="input" type="email" defaultValue={supportEmail} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="column is-half">
|
||||||
|
<div className="field">
|
||||||
|
<label className="label">Support Phone</label>
|
||||||
|
<div className="control">
|
||||||
|
<input className="input" type="tel" defaultValue={supportPhone} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div className="box mb-5">
|
||||||
|
<h2 className="title is-5 has-text-primary border-bottom pb-2" style={{ borderBottom: '1px solid #eee' }}>Business Documents</h2>
|
||||||
|
<p className="has-text-grey-dark mb-4">Upload any additional documents (like business licenses, identity proof, or craft certifications).</p>
|
||||||
|
|
||||||
|
<div className="field">
|
||||||
|
<div className="control">
|
||||||
|
<div className="file is-boxed">
|
||||||
|
<label className="file-label is-flex-direction-column is-align-items-center">
|
||||||
|
<input className="file-input" type="file" name="business_doc" />
|
||||||
|
<span className="file-cta is-flex is-flex-direction-column is-align-items-center py-4 px-6 has-background-white" style={{ border: '1px solid var(--bulma-primary)' }}>
|
||||||
|
<span className="file-icon mb-2 has-text-primary">
|
||||||
|
<span className="material-symbols-outlined is-size-3">upload_file</span>
|
||||||
|
</span>
|
||||||
|
<span className="file-label has-text-primary">
|
||||||
|
Choose a document…
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="is-flex is-justify-content-flex-end">
|
||||||
|
<button className="button is-primary is-outlined">Save Changes</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
51
src/components/layout/DashboardHeader.tsx
Normal file
51
src/components/layout/DashboardHeader.tsx
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { useSeller } from '../../context/SellerContext';
|
||||||
|
|
||||||
|
export default function DashboardHeader() {
|
||||||
|
const { handleLogout, storeSlug } = useSeller();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<nav className="navbar has-background-white" style={{ borderBottom: '1px solid #e2dfdb', position: 'sticky', top: 0, zIndex: 30 }}>
|
||||||
|
<div className="navbar-brand">
|
||||||
|
<a className="navbar-item" href="#">
|
||||||
|
<h1 className="title is-4 has-text-primary" style={{ fontFamily: 'Libre Caslon Text, serif' }}>
|
||||||
|
Tradhox
|
||||||
|
</h1>
|
||||||
|
</a>
|
||||||
|
<a role="button" className="navbar-burger" aria-label="menu" aria-expanded="false">
|
||||||
|
<span aria-hidden="true"></span>
|
||||||
|
<span aria-hidden="true"></span>
|
||||||
|
<span aria-hidden="true"></span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="navbar-menu">
|
||||||
|
<div className="navbar-end pr-5">
|
||||||
|
<div className="navbar-item has-dropdown is-hoverable">
|
||||||
|
<a className="navbar-link is-arrowless is-flex is-align-items-center">
|
||||||
|
<span className="icon has-text-grey mr-3">
|
||||||
|
<span className="material-symbols-outlined">notifications</span>
|
||||||
|
</span>
|
||||||
|
<div className="is-flex is-align-items-center">
|
||||||
|
<div style={{ width: '32px', height: '32px', borderRadius: '50%', backgroundColor: 'var(--bulma-primary)', color: 'white', display: 'flex', alignItems: 'center', justifyContent: 'center', fontWeight: 'bold' }}>
|
||||||
|
{storeSlug ? storeSlug.charAt(0).toUpperCase() : 'S'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<div className="navbar-dropdown is-right">
|
||||||
|
<a className="navbar-item">
|
||||||
|
<span className="icon mr-2"><span className="material-symbols-outlined">account_circle</span></span>
|
||||||
|
Profile
|
||||||
|
</a>
|
||||||
|
<hr className="navbar-divider" />
|
||||||
|
<a className="navbar-item has-text-danger" onClick={handleLogout}>
|
||||||
|
<span className="icon mr-2"><span className="material-symbols-outlined">logout</span></span>
|
||||||
|
Log Out
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
}
|
||||||
38
src/components/layout/Footer.tsx
Normal file
38
src/components/layout/Footer.tsx
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
export default function Footer() {
|
||||||
|
return (
|
||||||
|
<footer className="footer has-background-white-bis" style={{ borderTop: '1px solid #dac0c2' }}>
|
||||||
|
<div className="container" style={{ maxWidth: '1120px' }}>
|
||||||
|
<div className="columns">
|
||||||
|
{/* Brand & Copyright */}
|
||||||
|
<div className="column is-4">
|
||||||
|
<h2 className="title is-4 has-text-primary mb-3" style={{ fontFamily: 'Libre Caslon Text, serif' }}>Tradhox</h2>
|
||||||
|
<p className="has-text-grey-dark">
|
||||||
|
© 2024 Tradhox. Honoring Human Craftsmanship.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Links 1 */}
|
||||||
|
<div className="column is-4">
|
||||||
|
<ul style={{ listStyle: 'none', marginLeft: 0 }}>
|
||||||
|
<li className="mb-2"><a href="#" className="has-text-grey-dark hover-text-primary" style={{ transition: 'color 0.2s' }}>Privacy Policy</a></li>
|
||||||
|
<li><a href="#" className="has-text-grey-dark hover-text-primary" style={{ transition: 'color 0.2s' }}>Terms of Service</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Links 2 */}
|
||||||
|
<div className="column is-4">
|
||||||
|
<ul style={{ listStyle: 'none', marginLeft: 0 }}>
|
||||||
|
<li className="mb-2"><a href="#" className="has-text-grey-dark hover-text-primary" style={{ transition: 'color 0.2s' }}>Sustainability Report</a></li>
|
||||||
|
<li><a href="#" className="has-text-grey-dark hover-text-primary" style={{ transition: 'color 0.2s' }}>Shipping Info</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<style>{`
|
||||||
|
.hover-text-primary:hover { color: var(--bulma-primary) !important; }
|
||||||
|
`}</style>
|
||||||
|
</footer>
|
||||||
|
);
|
||||||
|
}
|
||||||
88
src/components/layout/Header.tsx
Normal file
88
src/components/layout/Header.tsx
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { useSeller } from '../../context/SellerContext';
|
||||||
|
|
||||||
|
export default function Header() {
|
||||||
|
const { currentPage, setCurrentPage, isMobileMenuOpen, setIsMobileMenuOpen } = useSeller();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<nav className="navbar has-background-white-bis" style={{ borderBottom: '1px solid #dac0c2' }} role="navigation" aria-label="main navigation">
|
||||||
|
<div className="container" style={{ maxWidth: '1120px' }}>
|
||||||
|
<div className="navbar-brand">
|
||||||
|
<a className="navbar-item is-size-4 has-text-primary" style={{ fontFamily: 'Libre Caslon Text, serif', letterSpacing: '-0.02em', fontWeight: 700 }} onClick={() => setCurrentPage('home')}>
|
||||||
|
Tradhox
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a role="button" className={`navbar-burger ${isMobileMenuOpen ? 'is-active' : ''}`} aria-label="menu" aria-expanded="false" onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)} style={{ color: 'var(--bulma-primary)' }}>
|
||||||
|
<span aria-hidden="true"></span>
|
||||||
|
<span aria-hidden="true"></span>
|
||||||
|
<span aria-hidden="true"></span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={`navbar-menu ${isMobileMenuOpen ? 'is-active' : ''}`} style={{ backgroundColor: 'transparent' }}>
|
||||||
|
<div className="navbar-start" style={{ margin: '0 auto', gap: '2rem' }}>
|
||||||
|
<a
|
||||||
|
className="navbar-item has-text-weight-medium custom-nav-link"
|
||||||
|
onClick={() => setCurrentPage('home')}
|
||||||
|
style={{ color: currentPage === 'home' ? 'var(--bulma-primary)' : '#5f5e5b' }}
|
||||||
|
>
|
||||||
|
Shop
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a
|
||||||
|
className="navbar-item has-text-weight-medium custom-nav-link"
|
||||||
|
onClick={() => setCurrentPage('about')}
|
||||||
|
style={{ color: currentPage === 'about' ? 'var(--bulma-primary)' : '#5f5e5b' }}
|
||||||
|
>
|
||||||
|
Our Story
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
className="navbar-item has-text-weight-medium custom-nav-link"
|
||||||
|
onClick={() => setCurrentPage('contact')}
|
||||||
|
style={{ color: currentPage === 'contact' ? 'var(--bulma-primary)' : '#5f5e5b' }}
|
||||||
|
>
|
||||||
|
Contact Us
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="navbar-end">
|
||||||
|
<div className="navbar-item">
|
||||||
|
<div className="buttons">
|
||||||
|
<a className="button is-ghost has-text-weight-semibold" style={{ color: '#5f5e5b' }} onClick={() => setCurrentPage('login')}>
|
||||||
|
User Login
|
||||||
|
</a>
|
||||||
|
<a className="button is-primary is-outlined has-text-weight-semibold" onClick={() => setCurrentPage('login')}>
|
||||||
|
Seller Login
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>{`
|
||||||
|
.custom-nav-link {
|
||||||
|
position: relative;
|
||||||
|
background-color: transparent !important;
|
||||||
|
}
|
||||||
|
.custom-nav-link:hover {
|
||||||
|
color: var(--bulma-primary) !important;
|
||||||
|
}
|
||||||
|
.custom-nav-link::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
width: 0;
|
||||||
|
height: 2px;
|
||||||
|
background-color: var(--bulma-primary);
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
}
|
||||||
|
.custom-nav-link:hover::after {
|
||||||
|
width: 80%;
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
}
|
||||||
38
src/components/layout/Sidebar.tsx
Normal file
38
src/components/layout/Sidebar.tsx
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { useSeller } from '../../context/SellerContext';
|
||||||
|
|
||||||
|
export default function Sidebar() {
|
||||||
|
const { dashTab, setDashTab } = useSeller();
|
||||||
|
|
||||||
|
const menuItems = [
|
||||||
|
{ id: 'overview', icon: 'grid_view', label: 'Overview' },
|
||||||
|
{ id: 'orders', icon: 'local_shipping', label: 'Orders' },
|
||||||
|
{ id: 'products', icon: 'inventory_2', label: 'Products' },
|
||||||
|
{ id: 'payments', icon: 'account_balance_wallet', label: 'Payments & Earnings' },
|
||||||
|
{ id: 'settings', icon: 'settings', label: 'Settings' }
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<aside className="menu p-5" style={{ position: 'sticky', top: '4rem' }}>
|
||||||
|
<p className="menu-label has-text-weight-bold tracking-wide" style={{ color: 'var(--bulma-grey)', letterSpacing: '0.1em' }}>
|
||||||
|
Seller Central
|
||||||
|
</p>
|
||||||
|
<ul className="menu-list">
|
||||||
|
{menuItems.map(item => (
|
||||||
|
<li key={item.id} className="mb-2">
|
||||||
|
<a
|
||||||
|
className={`is-flex is-align-items-center py-3 px-4 ${dashTab === item.id ? 'has-background-primary-light has-text-primary has-text-weight-bold' : 'has-text-grey-dark'}`}
|
||||||
|
style={{ borderRadius: '8px', transition: 'all 0.2s ease' }}
|
||||||
|
onClick={() => setDashTab(item.id)}
|
||||||
|
>
|
||||||
|
<span className="icon mr-3">
|
||||||
|
<span className="material-symbols-outlined is-size-5">{item.icon}</span>
|
||||||
|
</span>
|
||||||
|
<span>{item.label}</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</aside>
|
||||||
|
);
|
||||||
|
}
|
||||||
98
src/components/pages/AboutPage.tsx
Normal file
98
src/components/pages/AboutPage.tsx
Normal file
|
|
@ -0,0 +1,98 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { useSeller } from '../../context/SellerContext';
|
||||||
|
|
||||||
|
export default function AboutPage() {
|
||||||
|
const { setCurrentPage } = useSeller();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{/* Hero Section */}
|
||||||
|
<section className="hero is-large" style={{ position: 'relative', overflow: 'hidden', minHeight: '800px' }}>
|
||||||
|
<div style={{
|
||||||
|
position: 'absolute', top: 0, left: 0, right: 0, bottom: 0,
|
||||||
|
backgroundImage: "url('https://lh3.googleusercontent.com/aida-public/AB6AXuA5mjm6T9CY7j2AtbFE_jEFFkhOzCmc_eKwvgj0hHVKV6AoVKeAf8ZfBnnz4sSYkdzX5gyzlk-pGufTtwpJN5MhYgDAIezV1yaDN8ANw66nZ5kk40-GGDiVWp3eGiPJZ4mIuL_aJp6O1TqJsQihFvPZyfj5VgjH4IQivXEltdrAAUfKSMdbWgNHLi3SP2X5uLF6Xc_gDyYs-tOepA8IuU6_1Q9wcaSebXAQOQhAQAPW7wVcUFGY7crerw')",
|
||||||
|
backgroundSize: 'cover',
|
||||||
|
backgroundPosition: 'center',
|
||||||
|
zIndex: 0
|
||||||
|
}}>
|
||||||
|
<div style={{ position: 'absolute', inset: 0, background: 'linear-gradient(to right, rgba(251, 249, 248, 0.9), rgba(251, 249, 248, 0.4))' }}></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="hero-body" style={{ position: 'relative', zIndex: 10 }}>
|
||||||
|
<div className="container" style={{ maxWidth: '1120px' }}>
|
||||||
|
<div className="columns is-vcentered">
|
||||||
|
<div className="column is-6">
|
||||||
|
<p className="subtitle is-6 has-text-primary is-uppercase has-text-weight-bold tracking-widest mb-4" style={{ letterSpacing: '0.2em' }}>
|
||||||
|
Honoring Human Craftsmanship
|
||||||
|
</p>
|
||||||
|
<h1 className="title has-text-dark mb-5" style={{ fontSize: '3.5rem', fontFamily: 'Libre Caslon Text, serif' }}>
|
||||||
|
Preserving Heritage, Inspiring the Modern World.
|
||||||
|
</h1>
|
||||||
|
<p className="subtitle is-5 has-text-grey-dark mb-6" style={{ lineHeight: '1.6' }}>
|
||||||
|
Discover authentic, handcrafted masterworks from generations of skilled artisans. Every piece tells a story of culture, resilience, and unparalleled skill.
|
||||||
|
</p>
|
||||||
|
<button className="button is-primary is-medium px-6 is-outlined" onClick={() => setCurrentPage('home')}>
|
||||||
|
<span className="has-text-weight-bold">Explore the Marketplace</span>
|
||||||
|
<span className="icon ml-2"><span className="material-symbols-outlined">arrow_forward</span></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Why Tradhox Section */}
|
||||||
|
<section className="section py-6 has-background-white-bis">
|
||||||
|
<div className="container" style={{ maxWidth: '1120px' }}>
|
||||||
|
<div className="has-text-centered mb-6" style={{ maxWidth: '800px', margin: '0 auto' }}>
|
||||||
|
<h2 className="title is-2 has-text-primary mb-4" style={{ fontFamily: 'Libre Caslon Text, serif' }}>Why Choose Tradhox</h2>
|
||||||
|
<p className="subtitle is-5 has-text-grey-dark">
|
||||||
|
We bridge the gap between discerning collectors and master artisans, ensuring fairness, authenticity, and enduring quality.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="columns is-multiline mt-5">
|
||||||
|
{/* Feature 1 */}
|
||||||
|
<div className="column is-4-tablet">
|
||||||
|
<div className="box has-text-centered h-100 feature-card" style={{ padding: '2rem', height: '100%', border: '1px solid #dac0c2', backgroundColor: '#ffffff', boxShadow: 'none', transition: 'box-shadow 0.3s' }}>
|
||||||
|
<span className="icon is-large has-text-primary mb-4" style={{ backgroundColor: '#f5f3f3', borderRadius: '50%', width: '64px', height: '64px' }}>
|
||||||
|
<span className="material-symbols-outlined is-size-3" style={{ fontVariationSettings: "'FILL' 1" }}>verified</span>
|
||||||
|
</span>
|
||||||
|
<h3 className="title is-4 mb-3" style={{ fontFamily: 'Libre Caslon Text, serif' }}>Authenticity Guaranteed</h3>
|
||||||
|
<p className="has-text-grey-dark">Direct sourcing from verified artisanal clusters ensures every piece is genuine and ethically procured.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Feature 2 */}
|
||||||
|
<div className="column is-4-tablet">
|
||||||
|
<div className="box has-text-centered h-100 feature-card" style={{ padding: '2rem', height: '100%', border: '1px solid #dac0c2', backgroundColor: '#ffffff', boxShadow: 'none', transition: 'box-shadow 0.3s' }}>
|
||||||
|
<span className="icon is-large has-text-primary mb-4" style={{ backgroundColor: '#f5f3f3', borderRadius: '50%', width: '64px', height: '64px' }}>
|
||||||
|
<span className="material-symbols-outlined is-size-3" style={{ fontVariationSettings: "'FILL' 1" }}>handshake</span>
|
||||||
|
</span>
|
||||||
|
<h3 className="title is-4 mb-3" style={{ fontFamily: 'Libre Caslon Text, serif' }}>Fair Trade Promise</h3>
|
||||||
|
<p className="has-text-grey-dark">We empower makers by providing direct market access, ensuring they receive equitable compensation for their craft.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Feature 3 */}
|
||||||
|
<div className="column is-4-tablet">
|
||||||
|
<div className="box has-text-centered h-100 feature-card" style={{ padding: '2rem', height: '100%', border: '1px solid #dac0c2', backgroundColor: '#ffffff', boxShadow: 'none', transition: 'box-shadow 0.3s' }}>
|
||||||
|
<span className="icon is-large has-text-primary mb-4" style={{ backgroundColor: '#f5f3f3', borderRadius: '50%', width: '64px', height: '64px' }}>
|
||||||
|
<span className="material-symbols-outlined is-size-3" style={{ fontVariationSettings: "'FILL' 1" }}>eco</span>
|
||||||
|
</span>
|
||||||
|
<h3 className="title is-4 mb-3" style={{ fontFamily: 'Libre Caslon Text, serif' }}>Sustainable Heritage</h3>
|
||||||
|
<p className="has-text-grey-dark">Supporting traditional techniques that inherently respect natural materials and sustainable production cycles.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<style>{`
|
||||||
|
.feature-card:hover {
|
||||||
|
box-shadow: 0 4px 20px rgba(107, 26, 44, 0.05) !important;
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
86
src/components/pages/AuthForms.tsx
Normal file
86
src/components/pages/AuthForms.tsx
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { useSeller } from '../../context/SellerContext';
|
||||||
|
|
||||||
|
export default function AuthForms() {
|
||||||
|
const { activeTab, setActiveTab, email, setEmail, password, setPassword, confirmPassword, setConfirmPassword, phone, setPhone, policyAccepted, setPolicyAccepted, handleLoginSubmit, handleRegisterSubmit } = useSeller();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="section">
|
||||||
|
<div className="container">
|
||||||
|
<div className="columns is-centered">
|
||||||
|
<div className="column is-half">
|
||||||
|
<div className="box">
|
||||||
|
<h1 className="title has-text-centered">Seller Central Login</h1>
|
||||||
|
<div className="tabs is-centered">
|
||||||
|
<ul>
|
||||||
|
<li className={activeTab === 'login' ? 'is-active' : ''}>
|
||||||
|
<a onClick={() => setActiveTab('login')}>Log In</a>
|
||||||
|
</li>
|
||||||
|
<li className={activeTab === 'register' ? 'is-active' : ''}>
|
||||||
|
<a onClick={() => setActiveTab('register')}>Register</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{activeTab === 'login' && (
|
||||||
|
<form onSubmit={handleLoginSubmit}>
|
||||||
|
<div className="field">
|
||||||
|
<label className="label">Email or Phone</label>
|
||||||
|
<div className="control">
|
||||||
|
<input className="input" type="text" placeholder="e.g. alex@example.com" value={email} onChange={e => setEmail(e.target.value)} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="field">
|
||||||
|
<label className="label">Password</label>
|
||||||
|
<div className="control">
|
||||||
|
<input className="input" type="password" placeholder="********" value={password} onChange={e => setPassword(e.target.value)} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button type="submit" className="button is-primary is-fullwidth mt-4 is-outlined">Log in</button>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeTab === 'register' && (
|
||||||
|
<form onSubmit={handleRegisterSubmit}>
|
||||||
|
<div className="field">
|
||||||
|
<label className="label">Email</label>
|
||||||
|
<div className="control">
|
||||||
|
<input className="input" type="email" placeholder="e.g. alex@example.com" value={email} onChange={e => setEmail(e.target.value)} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="field">
|
||||||
|
<label className="label">Phone Number</label>
|
||||||
|
<div className="control">
|
||||||
|
<input className="input" type="tel" placeholder="e.g. 9876543210" value={phone} onChange={e => setPhone(e.target.value)} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="field">
|
||||||
|
<label className="label">Password</label>
|
||||||
|
<div className="control">
|
||||||
|
<input className="input" type="password" placeholder="********" value={password} onChange={e => setPassword(e.target.value)} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="field">
|
||||||
|
<label className="label">Confirm Password</label>
|
||||||
|
<div className="control">
|
||||||
|
<input className="input" type="password" placeholder="********" value={confirmPassword} onChange={e => setConfirmPassword(e.target.value)} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="field">
|
||||||
|
<div className="control">
|
||||||
|
<label className="checkbox">
|
||||||
|
<input type="checkbox" checked={policyAccepted} onChange={e => setPolicyAccepted(e.target.checked)} />
|
||||||
|
{' '}I agree to the <a href="#">Terms of Service</a> and <a href="#">Privacy Policy</a>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button type="submit" className="button is-primary is-fullwidth mt-4 is-outlined">Register</button>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
164
src/components/pages/ContactPage.tsx
Normal file
164
src/components/pages/ContactPage.tsx
Normal file
|
|
@ -0,0 +1,164 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
export default function ContactPage() {
|
||||||
|
return (
|
||||||
|
<div className="container" style={{ maxWidth: '1120px', padding: '4rem 1.5rem' }}>
|
||||||
|
{/* Header Section */}
|
||||||
|
<div className="mb-6" style={{ maxWidth: '800px' }}>
|
||||||
|
<h1 className="title is-1 has-text-primary mb-4" style={{ fontFamily: 'Libre Caslon Text, serif' }}>Get in Touch</h1>
|
||||||
|
<p className="subtitle is-4 has-text-grey-dark" style={{ lineHeight: '1.6' }}>
|
||||||
|
Whether you have a question about an artisanal piece, a partnership inquiry, or need support with your order, we are here to help. Reach out to the Tradhox team.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Contact Section */}
|
||||||
|
<div className="columns is-multiline mb-6">
|
||||||
|
{/* Left Side: Contact Details */}
|
||||||
|
<div className="column is-4">
|
||||||
|
<div className="box mb-5" style={{ border: '1px solid #dac0c2', boxShadow: 'none' }}>
|
||||||
|
<h2 className="title is-4 has-text-primary mb-5" style={{ fontFamily: 'Libre Caslon Text, serif' }}>Contact Information</h2>
|
||||||
|
|
||||||
|
<div className="is-flex mb-4">
|
||||||
|
<span className="icon has-text-primary mr-3 mt-1"><span className="material-symbols-outlined">location_on</span></span>
|
||||||
|
<div>
|
||||||
|
<h3 className="is-size-7 has-text-grey has-text-weight-bold is-uppercase mb-1" style={{ letterSpacing: '0.1em' }}>Headquarters</h3>
|
||||||
|
<p className="has-text-dark">12 Craft Heritage Lane,<br/>Dashashwamedh Ghat Area,<br/>Varanasi, UP 221001, India</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="is-flex mb-4">
|
||||||
|
<span className="icon has-text-primary mr-3 mt-1"><span className="material-symbols-outlined">mail</span></span>
|
||||||
|
<div>
|
||||||
|
<h3 className="is-size-7 has-text-grey has-text-weight-bold is-uppercase mb-1" style={{ letterSpacing: '0.1em' }}>Email Us</h3>
|
||||||
|
<a href="mailto:namaste@tradhox.com" className="has-text-primary has-text-weight-semibold">namaste@tradhox.com</a>
|
||||||
|
<p className="is-size-7 has-text-grey mt-1">We aim to reply within 24 hours.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="is-flex">
|
||||||
|
<span className="icon has-text-primary mr-3 mt-1"><span className="material-symbols-outlined">call</span></span>
|
||||||
|
<div>
|
||||||
|
<h3 className="is-size-7 has-text-grey has-text-weight-bold is-uppercase mb-1" style={{ letterSpacing: '0.1em' }}>Call Us</h3>
|
||||||
|
<a href="tel:+918005550199" className="has-text-dark has-text-weight-semibold hover-text-primary">+91 800 555 0199</a>
|
||||||
|
<p className="is-size-7 has-text-grey mt-1">Mon-Fri, 9am - 6pm IST</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Social Links Card */}
|
||||||
|
<div className="box" style={{ border: '1px solid #dac0c2', boxShadow: 'none' }}>
|
||||||
|
<h3 className="is-size-7 has-text-grey has-text-weight-bold is-uppercase mb-3" style={{ letterSpacing: '0.1em' }}>Connect With Us</h3>
|
||||||
|
<div className="is-flex gap-2">
|
||||||
|
<a href="#" className="button is-rounded is-outlined border-grey has-text-dark hover-primary-border mr-2">
|
||||||
|
<span className="icon"><span className="material-symbols-outlined">public</span></span>
|
||||||
|
</a>
|
||||||
|
<a href="#" className="button is-rounded is-outlined border-grey has-text-dark hover-primary-border mr-2">
|
||||||
|
<span className="icon"><span className="material-symbols-outlined">share</span></span>
|
||||||
|
</a>
|
||||||
|
<a href="#" className="button is-rounded is-outlined border-grey has-text-dark hover-primary-border">
|
||||||
|
<span className="icon"><span className="material-symbols-outlined">groups</span></span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right Side: Inquiry Form */}
|
||||||
|
<div className="column is-8">
|
||||||
|
<div className="box h-100" style={{ border: '1px solid #dac0c2', boxShadow: 'none', padding: '2.5rem' }}>
|
||||||
|
<h2 className="title is-4 has-text-primary mb-5" style={{ fontFamily: 'Libre Caslon Text, serif' }}>Send an Inquiry</h2>
|
||||||
|
|
||||||
|
<form onSubmit={(e) => e.preventDefault()}>
|
||||||
|
<div className="columns is-multiline">
|
||||||
|
<div className="column is-6 pb-0">
|
||||||
|
<div className="field">
|
||||||
|
<label className="label is-size-7 has-text-grey has-text-weight-bold is-uppercase" style={{ letterSpacing: '0.1em' }}>Full Name</label>
|
||||||
|
<div className="control">
|
||||||
|
<input className="input" type="text" placeholder="e.g. Ananya Sharma" required style={{ borderColor: '#dac0c2', boxShadow: 'none' }} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="column is-6 pb-0">
|
||||||
|
<div className="field">
|
||||||
|
<label className="label is-size-7 has-text-grey has-text-weight-bold is-uppercase" style={{ letterSpacing: '0.1em' }}>Email Address</label>
|
||||||
|
<div className="control">
|
||||||
|
<input className="input" type="email" placeholder="e.g. ananya@example.com" required style={{ borderColor: '#dac0c2', boxShadow: 'none' }} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="column is-12 pb-0">
|
||||||
|
<div className="field">
|
||||||
|
<label className="label is-size-7 has-text-grey has-text-weight-bold is-uppercase" style={{ letterSpacing: '0.1em' }}>Subject of Inquiry</label>
|
||||||
|
<div className="control">
|
||||||
|
<div className="select is-fullwidth">
|
||||||
|
<select style={{ borderColor: '#dac0c2', boxShadow: 'none' }}>
|
||||||
|
<option value="customer_support">Customer Support (Order Issues, Returns)</option>
|
||||||
|
<option value="seller_inquiry">Seller/Artisan Inquiry (Join Tradhox)</option>
|
||||||
|
<option value="partnership">Business Partnership / B2B Bulk Orders</option>
|
||||||
|
<option value="general">General Question</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="column is-12 pb-0">
|
||||||
|
<div className="field">
|
||||||
|
<label className="label is-size-7 has-text-grey has-text-weight-bold is-uppercase" style={{ letterSpacing: '0.1em' }}>Message</label>
|
||||||
|
<div className="control">
|
||||||
|
<textarea className="textarea has-fixed-size" rows={5} placeholder="How can we assist you today?" required style={{ borderColor: '#dac0c2', boxShadow: 'none' }}></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="column is-12 mt-3">
|
||||||
|
<button type="submit" className="button is-primary is-medium px-6 is-outlined">
|
||||||
|
<span className="has-text-weight-bold">Send Message</span>
|
||||||
|
<span className="icon ml-2"><span className="material-symbols-outlined is-size-5">arrow_forward</span></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Visit Our Studio Section */}
|
||||||
|
<div className="box p-0 overflow-hidden" style={{ border: '1px solid #dac0c2', boxShadow: 'none', borderRadius: '12px' }}>
|
||||||
|
<div className="columns is-gapless mb-0">
|
||||||
|
<div className="column is-6 is-flex is-flex-direction-column is-justify-content-center p-6 has-background-white-bis">
|
||||||
|
<h2 className="title is-3 has-text-primary mb-4" style={{ fontFamily: 'Libre Caslon Text, serif' }}>Visit Our Studio</h2>
|
||||||
|
<p className="has-text-grey-dark mb-5" style={{ lineHeight: '1.6' }}>
|
||||||
|
Experience the craftsmanship firsthand. Our Varanasi studio is open to visitors who wish to learn about our sourcing process and see exclusive GI-tagged products before they launch online.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="box" style={{ border: '1px solid #dac0c2', boxShadow: 'none', alignSelf: 'flex-start' }}>
|
||||||
|
<h3 className="is-size-7 has-text-grey has-text-weight-bold is-uppercase mb-3" style={{ letterSpacing: '0.1em' }}>Studio Hours</h3>
|
||||||
|
<ul className="has-text-dark">
|
||||||
|
<li className="is-flex is-justify-content-space-between mb-2" style={{ width: '220px' }}><span>Monday - Friday:</span> <span className="has-text-weight-semibold">10:00 - 18:00</span></li>
|
||||||
|
<li className="is-flex is-justify-content-space-between mb-2" style={{ width: '220px' }}><span>Saturday:</span> <span className="has-text-weight-semibold">10:00 - 14:00</span></li>
|
||||||
|
<li className="is-flex is-justify-content-space-between" style={{ width: '220px' }}><span className="has-text-grey">Sunday:</span> <span className="has-text-grey has-text-weight-semibold">Closed</span></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="column is-6 is-flex is-align-items-center is-justify-content-center" style={{ backgroundColor: '#e4e2e2', borderLeft: '1px solid #dac0c2', minHeight: '350px', position: 'relative' }}>
|
||||||
|
<div className="has-text-centered p-5" style={{ zIndex: 1 }}>
|
||||||
|
<span className="icon is-large has-text-grey mb-2"><span className="material-symbols-outlined" style={{ fontSize: '48px' }}>map</span></span>
|
||||||
|
<p className="is-size-7 has-text-grey has-text-weight-bold is-uppercase mt-3" style={{ letterSpacing: '0.1em' }}>Interactive Map view of Varanasi Studio</p>
|
||||||
|
</div>
|
||||||
|
{/* Dot Pattern Overlay */}
|
||||||
|
<div style={{ position: 'absolute', inset: 0, opacity: 0.1, backgroundImage: 'radial-gradient(#887274 1.5px, transparent 1.5px)', backgroundSize: '24px 24px' }}></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>{`
|
||||||
|
.hover-text-primary:hover { color: var(--bulma-primary) !important; }
|
||||||
|
.hover-primary-border:hover { border-color: var(--bulma-primary) !important; color: var(--bulma-primary) !important; }
|
||||||
|
.border-grey { border-color: #dac0c2; }
|
||||||
|
`}</style>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
113
src/components/pages/LandingPage.tsx
Normal file
113
src/components/pages/LandingPage.tsx
Normal file
|
|
@ -0,0 +1,113 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { useSeller } from '../../context/SellerContext';
|
||||||
|
|
||||||
|
export default function LandingPage() {
|
||||||
|
const { setCurrentPage } = useSeller();
|
||||||
|
|
||||||
|
const handleSellerLogin = () => {
|
||||||
|
setCurrentPage('login');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{/* Hero Section */}
|
||||||
|
<section className="hero is-large has-background-dark" style={{
|
||||||
|
position: 'relative',
|
||||||
|
overflow: 'hidden',
|
||||||
|
minHeight: '600px'
|
||||||
|
}}>
|
||||||
|
<div style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0, left: 0, right: 0, bottom: 0,
|
||||||
|
backgroundImage: "url('https://lh3.googleusercontent.com/aida-public/AB6AXuCWM3QRK9NLvhVDgb1yr3ZPMxI4YJ2zEvrECk8pbXQ8kTCi9EQFYsJchNz2sfzWd8Gb5vdmZPyg83MZtWRG3ZzVG-xVBVALPGm_LAPbgxH4YJTprM-xhBDoG_dlzo7T9v5nOXnRF_MibxuJ9ct3JBgbQNzN9Fa57uY0gfjGHEk_qSZLWBiZQxxTgtjEZtGqnCXCZ1kno2Gn9gnYQHgmfY3_PVnjWnUKk-F2ZLmIIl7F3AlCv3B7U6lXLg')",
|
||||||
|
backgroundSize: 'cover',
|
||||||
|
backgroundPosition: 'center',
|
||||||
|
zIndex: 0
|
||||||
|
}}>
|
||||||
|
<div style={{ position: 'absolute', inset: 0, backgroundColor: 'rgba(0,0,0,0.4)' }}></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="hero-body is-flex is-align-items-center is-justify-content-center" style={{ position: 'relative', zIndex: 10 }}>
|
||||||
|
<div className="container has-text-centered" style={{ maxWidth: '800px' }}>
|
||||||
|
<p className="subtitle is-6 has-text-grey-light is-uppercase has-text-weight-bold tracking-widest mb-4" style={{ letterSpacing: '0.2em' }}>
|
||||||
|
Seasonal Collection
|
||||||
|
</p>
|
||||||
|
<h1 className="title has-text-white mb-5" style={{ fontSize: '3.5rem', fontFamily: 'Libre Caslon Text, serif', textShadow: '0 2px 4px rgba(0,0,0,0.3)' }}>
|
||||||
|
The Winter Weaves
|
||||||
|
</h1>
|
||||||
|
<p className="subtitle is-5 has-text-white-ter mb-6" style={{ lineHeight: '1.6', textShadow: '0 1px 2px rgba(0,0,0,0.3)' }}>
|
||||||
|
Discover the warmth of tradition. Hand-loomed pashminas and thick cotton weaves, crafted by generational artisans from the highlands.
|
||||||
|
</p>
|
||||||
|
<button className="button is-primary is-medium is-rounded px-6 is-outlined" onClick={handleSellerLogin}>
|
||||||
|
<span className="has-text-weight-bold">Explore Collection</span>
|
||||||
|
<span className="icon ml-2"><span className="material-symbols-outlined">arrow_forward</span></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Categories Bento Grid */}
|
||||||
|
<section className="section py-6">
|
||||||
|
<div className="container" style={{ maxWidth: '1120px' }}>
|
||||||
|
<div className="has-text-centered mb-6">
|
||||||
|
<h2 className="title is-2 has-text-dark" style={{ fontFamily: 'Libre Caslon Text, serif' }}>Explore by Craft</h2>
|
||||||
|
<div style={{ width: '64px', height: '4px', backgroundColor: 'var(--bulma-primary)', margin: '16px auto', borderRadius: '4px' }}></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="columns is-multiline">
|
||||||
|
{/* Handloom (Large span - 8 columns) */}
|
||||||
|
<div className="column is-8-tablet">
|
||||||
|
<a className="box p-0 is-clickable is-clipped" style={{ position: 'relative', height: '400px', display: 'block', border: '1px solid #dac0c2' }}>
|
||||||
|
<div style={{
|
||||||
|
position: 'absolute', inset: 0,
|
||||||
|
backgroundImage: "url('https://lh3.googleusercontent.com/aida-public/AB6AXuAS9r3NI9-FM8tgHGT2hWRg-M8knvFHZx5atRJ4rhhNLbZMRDwLJMhzK2ON7vpXvZoecKox8x5DPSYrmVTTYT62g_Es8tHv4R9YNHG8GdacoUglwQOOx2CWtc_y244pgcUKTskwMF2so3MkqXUH9puqF9Un0FGplE_BERtUsY3V_lhdLgt72RRhTKc3XvooH19hMuCYN2ljDzPrUWY4917V8qekx0cKfBYQsrPI1GiJP1o4li2_j2VHjg')",
|
||||||
|
backgroundSize: 'cover', backgroundPosition: 'center',
|
||||||
|
transition: 'transform 0.5s ease'
|
||||||
|
}} className="hover-zoom"></div>
|
||||||
|
<div style={{ position: 'absolute', inset: 0, background: 'linear-gradient(to top, rgba(0,0,0,0.8), transparent 60%)' }}></div>
|
||||||
|
<div style={{ position: 'absolute', bottom: 0, left: 0, padding: '2rem' }}>
|
||||||
|
<span className="tag is-primary is-medium mb-3 has-text-weight-bold" style={{ backgroundColor: 'rgba(77, 2, 24, 0.9)' }}>Apparel & Textiles</span>
|
||||||
|
<h3 className="title is-3 has-text-white" style={{ fontFamily: 'Libre Caslon Text, serif' }}>Handloom Heritage</h3>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right side items (4 columns) */}
|
||||||
|
<div className="column is-4-tablet is-flex is-flex-direction-column">
|
||||||
|
{/* Pottery */}
|
||||||
|
<a className="box p-0 is-clickable is-clipped mb-4 is-flex-grow-1" style={{ position: 'relative', height: '192px', display: 'block', border: '1px solid #dac0c2' }}>
|
||||||
|
<div style={{
|
||||||
|
position: 'absolute', inset: 0,
|
||||||
|
backgroundImage: "url('https://lh3.googleusercontent.com/aida-public/AB6AXuBQ4-Xs5qziux8_GlEJ0-ufRlJrIRx8C1oQCnRqE4DszCnYl_I30-ckRS3gFJChcgsaRXqmlQKB6rd1yajOCgURWi61y_-KiW5TtK_UqMjT3r4UrZHwyHoD091oxgN0ehJb-nKbkD5H2IyKr5MigS_W0M-n2ouvGlbmBELRwvaDfbEEUGlF488_X5DsuZE2Sgtu42Qqz3oQdxWBUNstUHgTgov_mzEv06FuJxLdPA5c7hVGFHWV7rQp5Q')",
|
||||||
|
backgroundSize: 'cover', backgroundPosition: 'center'
|
||||||
|
}} className="hover-zoom"></div>
|
||||||
|
<div style={{ position: 'absolute', inset: 0, background: 'linear-gradient(to top, rgba(0,0,0,0.7), transparent)' }}></div>
|
||||||
|
<div style={{ position: 'absolute', bottom: 0, left: 0, padding: '1.25rem' }}>
|
||||||
|
<h3 className="title is-4 has-text-white" style={{ fontFamily: 'Libre Caslon Text, serif' }}>Studio Pottery</h3>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
{/* Woodwork */}
|
||||||
|
<a className="box p-0 is-clickable is-clipped is-flex-grow-1" style={{ position: 'relative', height: '192px', display: 'block', border: '1px solid #dac0c2' }}>
|
||||||
|
<div style={{
|
||||||
|
position: 'absolute', inset: 0,
|
||||||
|
backgroundImage: "url('https://lh3.googleusercontent.com/aida-public/AB6AXuDFzLzVAg22uWncCePb-VxR7tx9BnFu8jMLulD2SFXg5Gt2qjgpJoMQmGMcv2m99GA1oAoUG2dLPFTuWPhjxk7TqyUbNlBA9uCVSwPScN6wa_T8BKnLv88zT7glVF81p00Qe2YUNAsH1lzFn_jxGADhHvMv_o9Wior2Uk373C1m2F3z_TwXMfRNDPmJc7WOuHC67Pt0K036WLz5lFSKaf3MwxwtKSE1HO1vmm2U5rnoyrES8Z_iEnglNA')",
|
||||||
|
backgroundSize: 'cover', backgroundPosition: 'center'
|
||||||
|
}} className="hover-zoom"></div>
|
||||||
|
<div style={{ position: 'absolute', inset: 0, background: 'linear-gradient(to top, rgba(0,0,0,0.7), transparent)' }}></div>
|
||||||
|
<div style={{ position: 'absolute', bottom: 0, left: 0, padding: '1.25rem' }}>
|
||||||
|
<h3 className="title is-4 has-text-white" style={{ fontFamily: 'Libre Caslon Text, serif' }}>Carved Woodwork</h3>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Add hover zoom style */}
|
||||||
|
<style>{`
|
||||||
|
.hover-zoom:hover { transform: scale(1.05); }
|
||||||
|
`}</style>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
255
src/components/pages/OnboardingWizard.tsx
Normal file
255
src/components/pages/OnboardingWizard.tsx
Normal file
|
|
@ -0,0 +1,255 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { useSeller } from '../../context/SellerContext';
|
||||||
|
|
||||||
|
export default function OnboardingWizard() {
|
||||||
|
const {
|
||||||
|
profileStep, setProfileStep,
|
||||||
|
setIsProfileComplete, setCurrentPage,
|
||||||
|
businessType, setBusinessType,
|
||||||
|
gstin, setGstin,
|
||||||
|
storeSlug, setStoreSlug,
|
||||||
|
supportEmail, setSupportEmail,
|
||||||
|
supportPhone, setSupportPhone,
|
||||||
|
selectedCategories, setSelectedCategories
|
||||||
|
} = useSeller();
|
||||||
|
|
||||||
|
const handleNext = () => {
|
||||||
|
if (profileStep < 3) {
|
||||||
|
setProfileStep(profileStep + 1);
|
||||||
|
} else {
|
||||||
|
setIsProfileComplete(true);
|
||||||
|
setCurrentPage('dashboard');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBack = () => {
|
||||||
|
if (profileStep > 1) {
|
||||||
|
setProfileStep(profileStep - 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleCategory = (cat: string) => {
|
||||||
|
if (selectedCategories.includes(cat)) {
|
||||||
|
setSelectedCategories(selectedCategories.filter((c: string) => c !== cat));
|
||||||
|
} else {
|
||||||
|
setSelectedCategories([...selectedCategories, cat]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderStep1 = () => (
|
||||||
|
<div className="columns is-vcentered" style={{ minHeight: 'calc(100vh - 4rem)' }}>
|
||||||
|
{/* Left Column (Text) */}
|
||||||
|
<div className="column is-5 is-flex is-flex-direction-column is-justify-content-center p-6">
|
||||||
|
<p className="has-text-primary mb-4 is-size-7 has-text-weight-bold is-uppercase tracking-wide" style={{ letterSpacing: '0.1em' }}>
|
||||||
|
Welcome to Tradhox — Step 1 of 3
|
||||||
|
</p>
|
||||||
|
<h1 className="title is-1 has-text-dark mb-5" style={{ fontFamily: 'Libre Caslon Text, serif', lineHeight: '1.2' }}>
|
||||||
|
Every Piece You List Keeps a Craft Alive
|
||||||
|
</h1>
|
||||||
|
<p className="subtitle is-5 has-text-grey-dark mb-6">
|
||||||
|
Join a global marketplace dedicated to cultural preservation. Your craftsmanship tells a story. Let's build your store to share it with the world.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="mb-6">
|
||||||
|
<div className="is-flex is-align-items-flex-start mb-5">
|
||||||
|
<span className="icon is-large has-text-primary mr-4" style={{ backgroundColor: 'rgba(226, 223, 219, 0.2)', borderRadius: '50%', padding: '24px', border: '1px solid #d7c3b0' }}>
|
||||||
|
<span className="material-symbols-outlined is-size-4">handshake</span>
|
||||||
|
</span>
|
||||||
|
<div>
|
||||||
|
<h3 className="title is-5 mb-2 has-text-dark">Fair Trade Certified</h3>
|
||||||
|
<p className="has-text-grey-dark is-size-6">Directly connect with buyers seeking authentic, ethically sourced goods.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="is-flex is-align-items-flex-start mb-5">
|
||||||
|
<span className="icon is-large has-text-primary mr-4" style={{ backgroundColor: 'rgba(226, 223, 219, 0.2)', borderRadius: '50%', padding: '24px', border: '1px solid #d7c3b0' }}>
|
||||||
|
<span className="material-symbols-outlined is-size-4">storefront</span>
|
||||||
|
</span>
|
||||||
|
<div>
|
||||||
|
<h3 className="title is-5 mb-2 has-text-dark">Your Brand, Your Way</h3>
|
||||||
|
<p className="has-text-grey-dark is-size-6">Customize your storefront to reflect your unique heritage and style.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<button className="button is-primary is-medium is-rounded is-outlined" onClick={handleNext}>
|
||||||
|
<span>Continue to Store Setup</span>
|
||||||
|
<span className="icon is-small ml-2"><span className="material-symbols-outlined">arrow_forward</span></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right Column (Image) */}
|
||||||
|
<div className="column is-7 is-hidden-mobile" style={{ height: 'calc(100vh - 4rem)' }}>
|
||||||
|
<div style={{ position: 'relative', height: '100%', padding: '2rem' }}>
|
||||||
|
<div style={{
|
||||||
|
position: 'relative',
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
borderRadius: '1rem',
|
||||||
|
overflow: 'hidden',
|
||||||
|
boxShadow: '0 4px 20px rgba(107,26,44,0.05)',
|
||||||
|
border: '1px solid #d7c3b0'
|
||||||
|
}}>
|
||||||
|
<img
|
||||||
|
src="https://lh3.googleusercontent.com/aida-public/AB6AXuB4W-NVA2QTeIF_XSyCokGOQf4RVLTiMmxLi69rYdK7uVQ5yINgHSBzGIeC1WJSEtTjXGBp3hx_OrKdiiV7_hcwN9oQdLnuoABA1XYYFf-RrQbgv9khd1zVujiaAjlPvBaj8WKCD83RjZqHUmuGM_-vKmmKWiijoqaOdoPAuWJdZu-QkASeowWzfwCljpwZTeVD2VaLxc5y1PzIKEmnTfy6O8jqTsZhOo-k6HrhZggm1lMF9kondr_oQg"
|
||||||
|
alt="Artisan at work"
|
||||||
|
style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover' }}
|
||||||
|
/>
|
||||||
|
{/* Global Reach Badge */}
|
||||||
|
<div style={{
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: '24px',
|
||||||
|
left: '24px',
|
||||||
|
backgroundColor: 'rgba(255, 255, 255, 0.9)',
|
||||||
|
backdropFilter: 'blur(4px)',
|
||||||
|
border: '1px solid #d7c3b0',
|
||||||
|
borderRadius: '8px',
|
||||||
|
padding: '16px',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '12px',
|
||||||
|
boxShadow: '0 2px 8px rgba(0,0,0,0.05)'
|
||||||
|
}}>
|
||||||
|
<span className="material-symbols-outlined has-text-primary is-size-3">public</span>
|
||||||
|
<div>
|
||||||
|
<p className="has-text-weight-bold has-text-dark is-size-6 mb-0">Global Reach</p>
|
||||||
|
<p className="has-text-grey-dark is-size-7 mb-0">Buyers in 50+ countries</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderStep2 = () => (
|
||||||
|
<div className="columns is-centered mt-6">
|
||||||
|
<div className="column is-8-desktop is-10-tablet">
|
||||||
|
<div className="box p-6">
|
||||||
|
<p className="has-text-grey mb-2 is-size-7 has-text-weight-bold is-uppercase tracking-wide has-text-centered">
|
||||||
|
Step 2 of 3
|
||||||
|
</p>
|
||||||
|
<h1 className="title is-3 has-text-primary has-text-centered mb-6" style={{ fontFamily: 'Libre Caslon Text, serif' }}>
|
||||||
|
Business Details
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<div className="field">
|
||||||
|
<label className="label">Business Type</label>
|
||||||
|
<div className="control">
|
||||||
|
<div className="select is-fullwidth">
|
||||||
|
<select value={businessType} onChange={e => setBusinessType(e.target.value as any)}>
|
||||||
|
<option value="registered_company">Registered Company</option>
|
||||||
|
<option value="self_help_group">Self-Help Group (SHG)</option>
|
||||||
|
<option value="individual_maker">Individual Maker / Artisan</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="field">
|
||||||
|
<label className="label">GSTIN (Optional for Individuals)</label>
|
||||||
|
<div className="control has-icons-left">
|
||||||
|
<input className="input" type="text" placeholder="29AAAAA1111A1Z1" value={gstin} onChange={e => setGstin(e.target.value)} />
|
||||||
|
<span className="icon is-small is-left"><span className="material-symbols-outlined">receipt_long</span></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="columns mt-4">
|
||||||
|
<div className="column is-half">
|
||||||
|
<div className="field">
|
||||||
|
<label className="label">Support Email</label>
|
||||||
|
<div className="control has-icons-left">
|
||||||
|
<input className="input" type="email" placeholder="support@brand.com" value={supportEmail} onChange={e => setSupportEmail(e.target.value)} />
|
||||||
|
<span className="icon is-small is-left"><span className="material-symbols-outlined">mail</span></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="column is-half">
|
||||||
|
<div className="field">
|
||||||
|
<label className="label">Support Phone</label>
|
||||||
|
<div className="control has-icons-left">
|
||||||
|
<input className="input" type="tel" placeholder="+91 9876543210" value={supportPhone} onChange={e => setSupportPhone(e.target.value)} />
|
||||||
|
<span className="icon is-small is-left"><span className="material-symbols-outlined">phone</span></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="field mt-4">
|
||||||
|
<label className="label">Store URL Slug</label>
|
||||||
|
<div className="field has-addons">
|
||||||
|
<p className="control">
|
||||||
|
<a className="button is-static">tradhox.com/store/</a>
|
||||||
|
</p>
|
||||||
|
<p className="control is-expanded">
|
||||||
|
<input className="input" type="text" placeholder="my-artisan-store" value={storeSlug} onChange={e => setStoreSlug(e.target.value)} />
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="is-flex is-justify-content-space-between mt-6">
|
||||||
|
<button className="button is-light" onClick={handleBack}>Back</button>
|
||||||
|
<button className="button is-primary is-outlined" onClick={handleNext}>Next: Categories</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderStep3 = () => {
|
||||||
|
const availableCategories = ['Textiles & Apparel', 'Home Decor', 'Jewelry', 'Art & Collectibles', 'Beauty & Wellness', 'Food & Beverages'];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="columns is-centered mt-6">
|
||||||
|
<div className="column is-8-desktop is-10-tablet">
|
||||||
|
<div className="box p-6">
|
||||||
|
<p className="has-text-grey mb-2 is-size-7 has-text-weight-bold is-uppercase tracking-wide has-text-centered">
|
||||||
|
Step 3 of 3
|
||||||
|
</p>
|
||||||
|
<h1 className="title is-3 has-text-primary has-text-centered mb-2" style={{ fontFamily: 'Libre Caslon Text, serif' }}>
|
||||||
|
What do you sell?
|
||||||
|
</h1>
|
||||||
|
<p className="has-text-centered has-text-grey mb-6">Select all categories that apply to your handcrafted products.</p>
|
||||||
|
|
||||||
|
<div className="columns is-multiline">
|
||||||
|
{availableCategories.map(cat => (
|
||||||
|
<div className="column is-6" key={cat}>
|
||||||
|
<div
|
||||||
|
className={`box is-clickable is-flex is-align-items-center transition-all duration-200 ${selectedCategories.includes(cat) ? 'has-background-primary-light border-primary' : 'has-background-white-ter'}`}
|
||||||
|
onClick={() => toggleCategory(cat)}
|
||||||
|
style={{ border: selectedCategories.includes(cat) ? '2px solid var(--bulma-primary)' : '2px solid transparent' }}
|
||||||
|
>
|
||||||
|
<span className={`icon is-large mr-3 ${selectedCategories.includes(cat) ? 'has-text-primary' : 'has-text-grey'}`}>
|
||||||
|
<span className="material-symbols-outlined">
|
||||||
|
{selectedCategories.includes(cat) ? 'check_circle' : 'circle'}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span className={`has-text-weight-medium ${selectedCategories.includes(cat) ? 'has-text-primary' : 'has-text-grey-dark'}`}>
|
||||||
|
{cat}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="is-flex is-justify-content-space-between mt-6">
|
||||||
|
<button className="button is-light" onClick={handleBack}>Back</button>
|
||||||
|
<button className="button is-primary is-outlined" onClick={handleNext}>Complete Onboarding</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="has-background-white" style={{ minHeight: 'calc(100vh - 4rem)' }}>
|
||||||
|
<div className="container is-fluid px-0">
|
||||||
|
{profileStep === 1 && renderStep1()}
|
||||||
|
{profileStep === 2 && renderStep2()}
|
||||||
|
{profileStep === 3 && renderStep3()}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -121,100 +121,6 @@ export const CONFIG = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function apiFetch(url: string, options: RequestInit = {}): Promise<Response> {
|
export function apiFetch(url: string, options: RequestInit = {}): Promise<Response> {
|
||||||
if ((window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') && !navigator.userAgent.includes('jsdom')) {
|
|
||||||
const path = url.replace(CONFIG.apiBaseUrl, '');
|
|
||||||
let status = 200;
|
|
||||||
let data: any = {};
|
|
||||||
|
|
||||||
if (path.startsWith('/api/auth/login/')) {
|
|
||||||
data = {
|
|
||||||
access_token: 'mock-access-token',
|
|
||||||
refresh_token: 'mock-refresh-token',
|
|
||||||
user: {
|
|
||||||
id: 1,
|
|
||||||
username: 'tradhox_artisan',
|
|
||||||
email: 'artisan@tradhox.com',
|
|
||||||
profile: {
|
|
||||||
onboarding_step: 4,
|
|
||||||
phone_verified: true,
|
|
||||||
email_verified: true,
|
|
||||||
is_gstin_verified: true,
|
|
||||||
aadhar_s3_key: 'mock_aadhar',
|
|
||||||
pan_s3_key: 'mock_pan',
|
|
||||||
store_name: 'Tradhox Artisan Co.',
|
|
||||||
logo_s3_key: 'mock_logo',
|
|
||||||
street: '123 Loom Lane',
|
|
||||||
status: 'pending_approval',
|
|
||||||
phone: '9876543210',
|
|
||||||
gstin: '29AAAAA1111A1Z1',
|
|
||||||
business_bio: 'Preserving centuries-old handloom techniques, our family has been creating pure silk and cotton textiles for three generations.',
|
|
||||||
city: 'Textile Town',
|
|
||||||
state: 'Karnataka',
|
|
||||||
pincode: '560001',
|
|
||||||
business_type: 'individual_maker',
|
|
||||||
store_slug: 'tradhox-artisan',
|
|
||||||
support_email: 'hello@tradhox.com',
|
|
||||||
support_phone: '+91 98765 43210',
|
|
||||||
selected_categories: ['Textiles & Apparel', 'Sustainable Products']
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
} else if (path.startsWith('/api/auth/register/')) {
|
|
||||||
data = { message: 'Registration successful' };
|
|
||||||
} else if (path.startsWith('/api/auth/verify-otp/')) {
|
|
||||||
data = { verified: true, message: 'OTP verified successfully' };
|
|
||||||
} else if (path.startsWith('/api/profile/submit-gstin/')) {
|
|
||||||
data = { message: 'GSTIN submitted', is_gstin_verified: true, verified: true };
|
|
||||||
} else if (path.startsWith('/api/profile/presigned-url/')) {
|
|
||||||
data = { url: 'https://mock-s3-upload-url.com', key: 'mock-s3-key' };
|
|
||||||
} else if (path.startsWith('/api/profile/')) {
|
|
||||||
data = {
|
|
||||||
phone: '9876543210',
|
|
||||||
phone_verified: true,
|
|
||||||
email_verified: true,
|
|
||||||
gstin: '29AAAAA1111A1Z1',
|
|
||||||
is_gstin_verified: true,
|
|
||||||
store_name: 'Heritage Weaves & Co.',
|
|
||||||
business_bio: 'Preserving centuries-old handloom techniques, our family has been creating pure silk and cotton textiles for three generations. Every piece is a testament to the dedication of our master weavers in Varanasi.',
|
|
||||||
street: '123 Handloom Lane',
|
|
||||||
city: 'Textile Town',
|
|
||||||
state: 'Karnataka',
|
|
||||||
pincode: '560001',
|
|
||||||
store_logo: '',
|
|
||||||
business_type: 'individual_maker',
|
|
||||||
store_slug: 'heritage-weaves',
|
|
||||||
support_email: 'hello@heritageweaves.in',
|
|
||||||
support_phone: '+91 98765 43210',
|
|
||||||
selected_categories: ['Textiles & Apparel', 'Sustainable Products']
|
|
||||||
};
|
|
||||||
} else if (path.startsWith('/api/products/')) {
|
|
||||||
data = CONFIG.dashboardData.initialProducts;
|
|
||||||
} else if (path.startsWith('/api/orders/')) {
|
|
||||||
if (path.includes('/accept/') || path.includes('/reject/')) {
|
|
||||||
data = { message: 'Order status updated successfully' };
|
|
||||||
} else {
|
|
||||||
data = CONFIG.dashboardData.initialOrders;
|
|
||||||
}
|
|
||||||
} else if (path.startsWith('/api/returns/')) {
|
|
||||||
if (path.includes('/action/')) {
|
|
||||||
data = { message: 'Return status updated successfully' };
|
|
||||||
} else {
|
|
||||||
data = CONFIG.dashboardData.initialReturns;
|
|
||||||
}
|
|
||||||
} else if (path.startsWith('/api/wallet/withdraw/')) {
|
|
||||||
data = { message: 'Withdrawal processed successfully' };
|
|
||||||
} else if (path.startsWith('/api/wallet/')) {
|
|
||||||
data = { outstanding: 38488.00, withdrawn: 6792.00, transactions: [] };
|
|
||||||
} else if (path.startsWith('/api/auth/logout/')) {
|
|
||||||
data = { message: 'Logout successful' };
|
|
||||||
}
|
|
||||||
|
|
||||||
return Promise.resolve(new Response(JSON.stringify(data), {
|
|
||||||
status,
|
|
||||||
headers: { 'Content-Type': 'application/json' }
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
const token = localStorage.getItem('access_token');
|
const token = localStorage.getItem('access_token');
|
||||||
const headers = {
|
const headers = {
|
||||||
...options.headers,
|
...options.headers,
|
||||||
|
|
|
||||||
822
src/context/SellerContext.tsx
Normal file
822
src/context/SellerContext.tsx
Normal file
|
|
@ -0,0 +1,822 @@
|
||||||
|
import React, { createContext, useContext, useState, useEffect } from 'react';
|
||||||
|
import { CONFIG, apiFetch } from '../config';
|
||||||
|
|
||||||
|
type Page = 'home' | 'about' | 'contact' | 'login' | 'signup' | 'profile-completion' | 'confirmation' | 'dashboard' | 'forgot-password' | 'login-otp' | 'welcome-tour';
|
||||||
|
type DashboardTab = 'overview' | 'products' | 'orders' | 'returns' | 'wallet' | 'settings' | 'barcode-generator' | 'analytics';
|
||||||
|
type DateFilter = 'year' | 'week' | 'day' | 'custom';
|
||||||
|
|
||||||
|
interface Product {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
category: string;
|
||||||
|
price: number;
|
||||||
|
stock: number;
|
||||||
|
sku: string;
|
||||||
|
image: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Order {
|
||||||
|
id: string;
|
||||||
|
date: string;
|
||||||
|
item: string;
|
||||||
|
quantity: number;
|
||||||
|
customer: string;
|
||||||
|
total: number;
|
||||||
|
status: 'Ready to Ship test' | 'Ready to Ship' | 'Shipped' | 'Delivered' | 'Cancelled' | 'Pending Acceptance' | 'Rejected';
|
||||||
|
carrier: string;
|
||||||
|
tracking: string;
|
||||||
|
eta: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ReturnRequest {
|
||||||
|
id: string;
|
||||||
|
orderId: string;
|
||||||
|
customer: string;
|
||||||
|
item: string;
|
||||||
|
reason: string;
|
||||||
|
status: 'Pending Approval' | 'Approved' | 'Rejected' | 'In Transit';
|
||||||
|
image: string;
|
||||||
|
returningTracking: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SellerContext = createContext<any>(null);
|
||||||
|
|
||||||
|
export const useSeller = () => useContext(SellerContext);
|
||||||
|
|
||||||
|
export const SellerProvider: React.FC<{children: React.ReactNode}> = ({ children }) => {
|
||||||
|
const [currentPage, setCurrentPage] = useState<Page>('home')
|
||||||
|
const [isSidebarOpen, setIsSidebarOpen] = useState(false)
|
||||||
|
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false)
|
||||||
|
const [activeTab, setActiveTab] = useState<'login' | 'register'>('login')
|
||||||
|
|
||||||
|
// Onboarding Status
|
||||||
|
const [isProfileComplete, setIsProfileComplete] = useState(false)
|
||||||
|
const [profileStep, setProfileStep] = useState(1)
|
||||||
|
|
||||||
|
// Registration / Onboarding Form States
|
||||||
|
const [email, setEmail] = useState('')
|
||||||
|
const [phone, setPhone] = useState('')
|
||||||
|
const [password, setPassword] = useState('')
|
||||||
|
|
||||||
|
// Step 1: Verification
|
||||||
|
const [phoneVerified, setPhoneVerified] = useState(false)
|
||||||
|
const [emailVerified, setEmailVerified] = useState(false)
|
||||||
|
const [phoneOtpSent, setPhoneOtpSent] = useState(false)
|
||||||
|
const [emailOtpSent, setEmailOtpSent] = useState(false)
|
||||||
|
const [enteredPhoneOtp, setEnteredPhoneOtp] = useState('')
|
||||||
|
const [enteredEmailOtp, setEnteredEmailOtp] = useState('')
|
||||||
|
|
||||||
|
// Step 2: Business details (moved GSTIN here)
|
||||||
|
const [gstin, setGstin] = useState('29AAAAA1111A1Z1')
|
||||||
|
const [isGstinVerified, setIsGstinVerified] = useState(false)
|
||||||
|
const [aadharFile, setAadharFile] = useState<string | null>(null)
|
||||||
|
const [panFile, setPanFile] = useState<string | null>(null)
|
||||||
|
const [aadharS3Key, setAadharS3Key] = useState<string | null>(null)
|
||||||
|
const [panS3Key, setPanS3Key] = useState<string | null>(null)
|
||||||
|
|
||||||
|
// New onboarding customization states
|
||||||
|
const [businessType, setBusinessType] = useState<'registered_company' | 'self_help_group' | 'individual_maker'>('registered_company')
|
||||||
|
const [storeSlug, setStoreSlug] = useState('')
|
||||||
|
const [supportEmail, setSupportEmail] = useState('')
|
||||||
|
const [supportPhone, setSupportPhone] = useState('')
|
||||||
|
const [selectedCategories, setSelectedCategories] = useState<string[]>([])
|
||||||
|
|
||||||
|
// Step 3: Store and Location details
|
||||||
|
const [storeName, setStoreName] = useState('My Artisan Handloom')
|
||||||
|
const [storeLogo, setStoreLogo] = useState<string | null>(null)
|
||||||
|
const [logoS3Key, setLogoS3Key] = useState<string | null>(null)
|
||||||
|
const [businessBio, setBusinessBio] = useState('Traditional weaving and local sustainable designs.')
|
||||||
|
const [address, setAddress] = useState({
|
||||||
|
street: '123 Handloom Lane',
|
||||||
|
city: 'Textile Town',
|
||||||
|
state: 'Karnataka',
|
||||||
|
pincode: '560001'
|
||||||
|
})
|
||||||
|
const [mapCoordinates, setMapCoordinates] = useState({ lat: 12.9716, lng: 77.5946 })
|
||||||
|
|
||||||
|
// --- Password visibility, confirm password and policy states ---
|
||||||
|
const [showLoginPass, setShowLoginPass] = useState(false)
|
||||||
|
const [showSignupPass, setShowSignupPass] = useState(false)
|
||||||
|
const [showSignupConfirmPass, setShowSignupConfirmPass] = useState(false)
|
||||||
|
const [confirmPassword, setConfirmPassword] = useState('')
|
||||||
|
const [policyAccepted, setPolicyAccepted] = useState(false)
|
||||||
|
|
||||||
|
// --- Password Reset Page States ---
|
||||||
|
const [resetEmail, setResetEmail] = useState('')
|
||||||
|
const [resetOtpSent, setResetOtpSent] = useState(false)
|
||||||
|
const [resetOtp, setResetOtp] = useState('')
|
||||||
|
const [resetPassword, setResetPassword] = useState('')
|
||||||
|
|
||||||
|
// --- OTP Login States ---
|
||||||
|
const [otpLoginPhone, setOtpLoginPhone] = useState('')
|
||||||
|
const [otpLoginSent, setOtpLoginSent] = useState(false)
|
||||||
|
const [otpLoginCode, setOtpLoginCode] = useState('')
|
||||||
|
|
||||||
|
// --- Barcode Generator States ---
|
||||||
|
const [barcodeProductSku, setBarcodeProductSku] = useState('')
|
||||||
|
const [barcodeGenerated, setBarcodeGenerated] = useState(false)
|
||||||
|
|
||||||
|
// --- Active Dashboard States ---
|
||||||
|
const [dashTab, setDashTab] = useState<DashboardTab>('overview')
|
||||||
|
const [dateFilter, setDateFilter] = useState<DateFilter>('year')
|
||||||
|
const [customDates, setCustomDates] = useState({ start: '2026-06-01', end: '2026-06-15' })
|
||||||
|
|
||||||
|
// Lists
|
||||||
|
const [products, setProducts] = useState<Product[]>([])
|
||||||
|
const [orders, setOrders] = useState<Order[]>([])
|
||||||
|
const [returns, setReturns] = useState<ReturnRequest[]>([])
|
||||||
|
|
||||||
|
// Forms & Editing
|
||||||
|
const [productForm, setProductForm] = useState({
|
||||||
|
id: '',
|
||||||
|
title: '',
|
||||||
|
category: 'Apparel',
|
||||||
|
price: 0,
|
||||||
|
stock: 0,
|
||||||
|
sku: '',
|
||||||
|
image: 'https://images.unsplash.com/photo-1544022613-e87ca75a784a?auto=format&fit=crop&q=80&w=100'
|
||||||
|
})
|
||||||
|
const [isEditingProduct, setIsEditingProduct] = useState(false)
|
||||||
|
|
||||||
|
// New Heritage UI wizard states
|
||||||
|
const [productWizardStep, setProductWizardStep] = useState<number>(0) // 0: Catalog list, 1-5: Product upload wizard steps
|
||||||
|
const [productFormDetails, setProductFormDetails] = useState({
|
||||||
|
id: '',
|
||||||
|
name: '',
|
||||||
|
category: 'Textiles & Apparel',
|
||||||
|
description: '',
|
||||||
|
isGiTagged: false,
|
||||||
|
primaryImage: 'https://images.unsplash.com/photo-1544022613-e87ca75a784a?auto=format&fit=crop&q=80&w=300',
|
||||||
|
additionalViews: [] as string[],
|
||||||
|
videoUrl: '',
|
||||||
|
view360Url: '',
|
||||||
|
basePrice: '',
|
||||||
|
compareAtPrice: '',
|
||||||
|
trackInventory: true,
|
||||||
|
sku: '',
|
||||||
|
initialStock: '',
|
||||||
|
shippingProfile: 'Standard Fragile',
|
||||||
|
processingDays: 3,
|
||||||
|
packageWeight: 1.0
|
||||||
|
})
|
||||||
|
const [selectedOrderDetail, setSelectedOrderDetail] = useState<any | null>(null)
|
||||||
|
const [orderNotes, setOrderNotes] = useState<string>('')
|
||||||
|
|
||||||
|
// Payout outstanding states
|
||||||
|
const [wallet, setWallet] = useState({
|
||||||
|
outstanding: 850.00,
|
||||||
|
withdrawn: 1250.00,
|
||||||
|
history: [
|
||||||
|
{ id: 'TX-9031', date: '2026-08-01', amount: 500.00, status: 'Transferred' },
|
||||||
|
{ id: 'TX-9022', date: '2026-07-15', amount: 750.00, status: 'Transferred' }
|
||||||
|
]
|
||||||
|
})
|
||||||
|
const [withdrawAmount, setWithdrawAmount] = useState('')
|
||||||
|
|
||||||
|
// Load data from backend on mount or when profile is complete / logged in
|
||||||
|
useEffect(() => {
|
||||||
|
const token = localStorage.getItem('access_token');
|
||||||
|
if (token) {
|
||||||
|
apiFetch(`${CONFIG.apiBaseUrl}/api/profile/`)
|
||||||
|
.then(res => {
|
||||||
|
if (res.ok) return res.json();
|
||||||
|
throw new Error('Session expired');
|
||||||
|
})
|
||||||
|
.then(user => {
|
||||||
|
if (user && user.profile) {
|
||||||
|
const profile = user.profile;
|
||||||
|
setPhone(profile.phone || '');
|
||||||
|
setPhoneVerified(profile.phone_verified || false);
|
||||||
|
setEmail(user.email || '');
|
||||||
|
setEmailVerified(profile.email_verified || false);
|
||||||
|
setGstin(profile.gstin || '29AAAAA1111A1Z1');
|
||||||
|
setIsGstinVerified(profile.is_gstin_verified || false);
|
||||||
|
setAadharFile(profile.aadhar_file || null);
|
||||||
|
setPanFile(profile.pan_file || null);
|
||||||
|
setAadharS3Key(profile.aadhar_s3_key || null);
|
||||||
|
setPanS3Key(profile.pan_s3_key || null);
|
||||||
|
|
||||||
|
setBusinessType(profile.business_type || 'registered_company');
|
||||||
|
setStoreSlug(profile.store_slug || '');
|
||||||
|
setSupportEmail(profile.support_email || '');
|
||||||
|
setSupportPhone(profile.support_phone || '');
|
||||||
|
setSelectedCategories(profile.categories || []);
|
||||||
|
|
||||||
|
setStoreName(profile.store_name || 'My Artisan Handloom');
|
||||||
|
setStoreLogo(profile.store_logo || null);
|
||||||
|
setLogoS3Key(profile.logo_s3_key || null);
|
||||||
|
setBusinessBio(profile.business_bio || 'Traditional weaving and local sustainable designs.');
|
||||||
|
setAddress({
|
||||||
|
street: profile.street || '123 Handloom Lane',
|
||||||
|
city: profile.city || 'Textile Town',
|
||||||
|
state: profile.state || 'Karnataka',
|
||||||
|
pincode: profile.pincode || '560001'
|
||||||
|
});
|
||||||
|
if (profile.latitude && profile.longitude) {
|
||||||
|
setMapCoordinates({ lat: Number(profile.latitude), lng: Number(profile.longitude) });
|
||||||
|
}
|
||||||
|
|
||||||
|
const step = profile.onboarding_step;
|
||||||
|
if (step >= 7) {
|
||||||
|
setIsProfileComplete(true);
|
||||||
|
setCurrentPage('dashboard');
|
||||||
|
} else {
|
||||||
|
setProfileStep(step);
|
||||||
|
setIsProfileComplete(false);
|
||||||
|
setCurrentPage('profile-completion');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error('Session restore failed:', err);
|
||||||
|
localStorage.removeItem('access_token');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isProfileComplete || currentPage === 'dashboard') {
|
||||||
|
// Fetch Products
|
||||||
|
apiFetch(`${CONFIG.apiBaseUrl}/api/products/`)
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(data => {
|
||||||
|
if (Array.isArray(data)) setProducts(data);
|
||||||
|
else if (data && Array.isArray(data.results)) setProducts(data.results);
|
||||||
|
})
|
||||||
|
.catch(err => console.error('Error fetching products:', err));
|
||||||
|
|
||||||
|
// Fetch Orders
|
||||||
|
apiFetch(`${CONFIG.apiBaseUrl}/api/orders/`)
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(data => {
|
||||||
|
if (Array.isArray(data)) setOrders(data);
|
||||||
|
else if (data && Array.isArray(data.results)) setOrders(data.results);
|
||||||
|
})
|
||||||
|
.catch(err => console.error('Error fetching orders:', err));
|
||||||
|
|
||||||
|
// Fetch Returns
|
||||||
|
apiFetch(`${CONFIG.apiBaseUrl}/api/returns/`)
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(data => {
|
||||||
|
if (Array.isArray(data)) setReturns(data);
|
||||||
|
else if (data && Array.isArray(data.results)) setReturns(data.results);
|
||||||
|
})
|
||||||
|
.catch(err => console.error('Error fetching returns:', err));
|
||||||
|
|
||||||
|
// Fetch Wallet
|
||||||
|
apiFetch(`${CONFIG.apiBaseUrl}/api/wallet/`)
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data) setWallet(data);
|
||||||
|
})
|
||||||
|
.catch(err => console.error('Error fetching wallet:', err));
|
||||||
|
}
|
||||||
|
}, [isProfileComplete, currentPage]);
|
||||||
|
|
||||||
|
const uploadDocument = async (file: File, fileType: string) => {
|
||||||
|
try {
|
||||||
|
const response = await apiFetch(`${CONFIG.apiBaseUrl}/api/profile/presigned-url/`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ file_type: fileType, content_type: file.type })
|
||||||
|
});
|
||||||
|
if (!response.ok) throw new Error('Failed to get presigned URL');
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
// Simulate file upload PUT to mock presigned_url
|
||||||
|
await fetch(data.presigned_url, {
|
||||||
|
method: 'PUT',
|
||||||
|
body: file,
|
||||||
|
headers: { 'Content-Type': file.type }
|
||||||
|
}).catch(err => console.log('Mock S3 upload:', err));
|
||||||
|
|
||||||
|
return data.s3_key;
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
return `suppliers/default/${fileType}.jpg`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAcceptOrder = (id: string) => {
|
||||||
|
apiFetch(`${CONFIG.apiBaseUrl}/api/orders/${id}/accept/`, {
|
||||||
|
method: 'POST'
|
||||||
|
})
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(updatedOrder => {
|
||||||
|
setOrders(orders.map(o => o.id === updatedOrder.id ? updatedOrder : o))
|
||||||
|
alert(`Order accepted successfully!`)
|
||||||
|
})
|
||||||
|
.catch(err => console.error(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleRejectOrder = (id: string) => {
|
||||||
|
apiFetch(`${CONFIG.apiBaseUrl}/api/orders/${id}/reject/`, {
|
||||||
|
method: 'POST'
|
||||||
|
})
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(updatedOrder => {
|
||||||
|
setOrders(orders.map(o => o.id === updatedOrder.id ? updatedOrder : o))
|
||||||
|
alert(`Order rejected.`)
|
||||||
|
})
|
||||||
|
.catch(err => console.error(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bulk Upload
|
||||||
|
const [bulkLog, setBulkLog] = useState<string[]>([])
|
||||||
|
const [isParsingBulk, setIsParsingBulk] = useState(false)
|
||||||
|
const [bulkCsvFile, setBulkCsvFile] = useState<File | null>(null)
|
||||||
|
const [bulkZipFile, setBulkZipFile] = useState<File | null>(null)
|
||||||
|
|
||||||
|
// GSTIN verification simulator
|
||||||
|
const handleVerifyGstin = () => {
|
||||||
|
if (!gstin.trim() || gstin.length !== 15) {
|
||||||
|
alert('Invalid GSTIN length. Must be 15 chars.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
apiFetch(`${CONFIG.apiBaseUrl}/api/profile/submit-gstin/`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ gstin })
|
||||||
|
})
|
||||||
|
.then(res => {
|
||||||
|
if (!res.ok) throw new Error('Failed to verify GSTIN.');
|
||||||
|
return res.json();
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
if (data.verified) {
|
||||||
|
setIsGstinVerified(true);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
alert(err.message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle Logo Upload simulation
|
||||||
|
const handleLogoChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const file = e.target.files?.[0]
|
||||||
|
if (file) {
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.onloadend = () => {
|
||||||
|
setStoreLogo(reader.result as string)
|
||||||
|
}
|
||||||
|
reader.readAsDataURL(file)
|
||||||
|
|
||||||
|
const s3Key = await uploadDocument(file, 'logo');
|
||||||
|
setLogoS3Key(s3Key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Route/Navigation Guard: If profile is not complete, redirect to profile-completion
|
||||||
|
const navigateTo = (page: Page, forceComplete: boolean = false) => {
|
||||||
|
const publicPages: Page[] = ['home', 'about', 'contact', 'login', 'signup', 'forgot-password', 'login-otp']
|
||||||
|
const complete = isProfileComplete || forceComplete
|
||||||
|
|
||||||
|
if (!complete && !publicPages.includes(page) && page !== 'profile-completion') {
|
||||||
|
alert('Access Denied: Please complete your supplier profile first!')
|
||||||
|
setCurrentPage('profile-completion')
|
||||||
|
} else {
|
||||||
|
setCurrentPage(page)
|
||||||
|
}
|
||||||
|
window.scrollTo({ top: 0, behavior: 'smooth' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleLogout = () => {
|
||||||
|
apiFetch(`${CONFIG.apiBaseUrl}/api/auth/logout/`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
})
|
||||||
|
.catch(err => console.error('Error logging out:', err))
|
||||||
|
.finally(() => {
|
||||||
|
localStorage.removeItem('access_token')
|
||||||
|
// Reset all onboarding & profile states to their defaults
|
||||||
|
setIsProfileComplete(false)
|
||||||
|
setProfileStep(1)
|
||||||
|
setEmail('')
|
||||||
|
setPhone('')
|
||||||
|
setPassword('')
|
||||||
|
setConfirmPassword('')
|
||||||
|
setPolicyAccepted(false)
|
||||||
|
|
||||||
|
setPhoneVerified(false)
|
||||||
|
setEmailVerified(false)
|
||||||
|
setPhoneOtpSent(false)
|
||||||
|
setEmailOtpSent(false)
|
||||||
|
setEnteredPhoneOtp('')
|
||||||
|
setEnteredEmailOtp('')
|
||||||
|
|
||||||
|
setGstin('29AAAAA1111A1Z1')
|
||||||
|
setIsGstinVerified(false)
|
||||||
|
setAadharFile(null)
|
||||||
|
setPanFile(null)
|
||||||
|
setAadharS3Key(null)
|
||||||
|
setPanS3Key(null)
|
||||||
|
|
||||||
|
setStoreName('My Artisan Handloom')
|
||||||
|
setStoreLogo(null)
|
||||||
|
setLogoS3Key(null)
|
||||||
|
setBusinessBio('Traditional weaving and local sustainable designs.')
|
||||||
|
setAddress({
|
||||||
|
street: '123 Handloom Lane',
|
||||||
|
city: 'Textile Town',
|
||||||
|
state: 'Karnataka',
|
||||||
|
pincode: '560001'
|
||||||
|
})
|
||||||
|
setMapCoordinates({ lat: 12.9716, lng: 77.5946 })
|
||||||
|
|
||||||
|
navigateTo('home', true)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const handleRegisterSubmit = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
// Email Validation
|
||||||
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||||
|
if (!emailRegex.test(email)) {
|
||||||
|
alert('Please enter a valid email address.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phone Validation
|
||||||
|
const phoneRegex = /^[6-9]\d{9}$/;
|
||||||
|
if (!phoneRegex.test(phone)) {
|
||||||
|
alert('Please enter a valid 10-digit mobile number starting with 6, 7, 8, or 9.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Password Validation
|
||||||
|
if (password.length < 8) {
|
||||||
|
alert('Password must be at least 8 characters long.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (password !== confirmPassword) {
|
||||||
|
alert('Passwords do not match.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Policy Validation
|
||||||
|
if (!policyAccepted) {
|
||||||
|
alert('You must accept the Terms of Service and Privacy Policy.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
apiFetch(`${CONFIG.apiBaseUrl}/api/auth/register/`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
username: email,
|
||||||
|
email: email,
|
||||||
|
phone: phone,
|
||||||
|
password: password,
|
||||||
|
confirm_password: confirmPassword,
|
||||||
|
policy_accepted: policyAccepted
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then(res => {
|
||||||
|
if (!res.ok) throw new Error('Registration failed. Username/email might already be taken.');
|
||||||
|
return res.json();
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
// Auto login after signup
|
||||||
|
apiFetch(`${CONFIG.apiBaseUrl}/api/auth/login/`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ username: email, password: password })
|
||||||
|
})
|
||||||
|
.then(res => {
|
||||||
|
if (!res.ok) throw new Error('Auto-login failed.');
|
||||||
|
return res.json();
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
localStorage.setItem('access_token', data.access_token);
|
||||||
|
navigateTo('profile-completion')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
alert(err.message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleLoginSubmit = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
if (!email.trim()) {
|
||||||
|
alert('Please enter your email or phone number.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
apiFetch(`${CONFIG.apiBaseUrl}/api/auth/login/`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ username: email, password: password })
|
||||||
|
})
|
||||||
|
.then(res => {
|
||||||
|
if (!res.ok) throw new Error('Invalid credentials.');
|
||||||
|
return res.json();
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
localStorage.setItem('access_token', data.access_token);
|
||||||
|
if (data.user && data.user.profile) {
|
||||||
|
const profile = data.user.profile;
|
||||||
|
setPhone(profile.phone || '');
|
||||||
|
setPhoneVerified(profile.phone_verified || false);
|
||||||
|
setEmail(data.user.email || '');
|
||||||
|
setEmailVerified(profile.email_verified || false);
|
||||||
|
setGstin(profile.gstin || '29AAAAA1111A1Z1');
|
||||||
|
setIsGstinVerified(profile.is_gstin_verified || false);
|
||||||
|
setAadharFile(profile.aadhar_file || null);
|
||||||
|
setPanFile(profile.pan_file || null);
|
||||||
|
setAadharS3Key(profile.aadhar_s3_key || null);
|
||||||
|
setPanS3Key(profile.pan_s3_key || null);
|
||||||
|
setStoreName(profile.store_name || 'My Artisan Handloom');
|
||||||
|
setStoreLogo(profile.store_logo || null);
|
||||||
|
setLogoS3Key(profile.logo_s3_key || null);
|
||||||
|
setBusinessBio(profile.business_bio || 'Traditional weaving and local sustainable designs.');
|
||||||
|
setAddress({
|
||||||
|
street: profile.street || '123 Handloom Lane',
|
||||||
|
city: profile.city || 'Textile Town',
|
||||||
|
state: profile.state || 'Karnataka',
|
||||||
|
pincode: profile.pincode || '560001'
|
||||||
|
});
|
||||||
|
if (profile.latitude && profile.longitude) {
|
||||||
|
setMapCoordinates({ lat: Number(profile.latitude), lng: Number(profile.longitude) });
|
||||||
|
}
|
||||||
|
|
||||||
|
const step = profile.onboarding_step;
|
||||||
|
if (step >= 4) {
|
||||||
|
setIsProfileComplete(true);
|
||||||
|
navigateTo('dashboard', true);
|
||||||
|
} else {
|
||||||
|
setProfileStep(step);
|
||||||
|
setIsProfileComplete(false);
|
||||||
|
navigateTo('profile-completion');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setIsProfileComplete(false);
|
||||||
|
navigateTo('profile-completion');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
alert(err.message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveProfileBackend = (isComplete: boolean) => {
|
||||||
|
return apiFetch(`${CONFIG.apiBaseUrl}/api/profile/`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
phone: phone,
|
||||||
|
phone_verified: phoneVerified,
|
||||||
|
email_verified: emailVerified,
|
||||||
|
gstin: gstin,
|
||||||
|
is_gstin_verified: isGstinVerified,
|
||||||
|
business_type: businessType,
|
||||||
|
store_slug: storeSlug,
|
||||||
|
support_email: supportEmail,
|
||||||
|
support_phone: supportPhone,
|
||||||
|
categories: selectedCategories,
|
||||||
|
aadhar_file: aadharFile,
|
||||||
|
pan_file: panFile,
|
||||||
|
aadhar_s3_key: aadharS3Key || 'suppliers/default/aadhar.pdf',
|
||||||
|
pan_s3_key: panS3Key || 'suppliers/default/pan.pdf',
|
||||||
|
store_name: storeName,
|
||||||
|
store_logo: storeLogo || 'https://images.unsplash.com/photo-1513519245088-0e12902e5a38?auto=format&fit=crop&q=80&w=800',
|
||||||
|
logo_s3_key: logoS3Key || 'suppliers/default/logo.jpg',
|
||||||
|
business_bio: businessBio,
|
||||||
|
street: address.street,
|
||||||
|
city: address.city,
|
||||||
|
state: address.state,
|
||||||
|
pincode: address.pincode,
|
||||||
|
latitude: mapCoordinates.lat,
|
||||||
|
longitude: mapCoordinates.lng,
|
||||||
|
is_profile_complete: isComplete
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then(res => {
|
||||||
|
if (!res.ok) throw new Error('Failed to update profile.');
|
||||||
|
return res.json();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleProfileSubmit = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
if (profileStep === 2) {
|
||||||
|
setIsGstinVerified(true)
|
||||||
|
}
|
||||||
|
const isLastStep = profileStep === 6;
|
||||||
|
saveProfileBackend(isLastStep)
|
||||||
|
.then(() => {
|
||||||
|
if (!isLastStep) {
|
||||||
|
setProfileStep(prev => prev + 1)
|
||||||
|
} else {
|
||||||
|
setProfileStep(7);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
alert(err.message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Dashboard Logic Actions ---
|
||||||
|
|
||||||
|
// Product creation/modification
|
||||||
|
const handleSaveProduct = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
const url = isEditingProduct
|
||||||
|
? `${CONFIG.apiBaseUrl}/api/products/${productForm.id}/`
|
||||||
|
: `${CONFIG.apiBaseUrl}/api/products/`
|
||||||
|
const method = isEditingProduct ? 'PUT' : 'POST'
|
||||||
|
|
||||||
|
apiFetch(url, {
|
||||||
|
method: method,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
title: productForm.title,
|
||||||
|
category: productForm.category,
|
||||||
|
price: String(productForm.price),
|
||||||
|
stock: Number(productForm.stock),
|
||||||
|
sku: productForm.sku || `PROD-${Date.now().toString().slice(-6)}`,
|
||||||
|
image: productForm.image
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(() => {
|
||||||
|
// Refresh products from backend
|
||||||
|
apiFetch(`${CONFIG.apiBaseUrl}/api/products/`)
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(prods => {
|
||||||
|
if (Array.isArray(prods)) setProducts(prods);
|
||||||
|
else if (prods && Array.isArray(prods.results)) setProducts(prods.results);
|
||||||
|
})
|
||||||
|
setIsEditingProduct(false)
|
||||||
|
alert(isEditingProduct ? 'Product modified successfully!' : 'Product uploaded successfully!')
|
||||||
|
})
|
||||||
|
.catch(err => console.error(err))
|
||||||
|
|
||||||
|
// reset form
|
||||||
|
setProductForm({ id: '', title: '', category: 'Apparel', price: 0, stock: 0, sku: '', image: 'https://images.unsplash.com/photo-1544022613-e87ca75a784a?auto=format&fit=crop&q=80&w=100' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleEditClick = (p: Product) => {
|
||||||
|
setProductForm(p)
|
||||||
|
setIsEditingProduct(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDeleteProduct = (id: string) => {
|
||||||
|
if (confirm('Are you sure you want to delete this listing?')) {
|
||||||
|
apiFetch(`${CONFIG.apiBaseUrl}/api/products/${id}/`, {
|
||||||
|
method: 'DELETE'
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
setProducts(products.filter(p => p.id !== id))
|
||||||
|
})
|
||||||
|
.catch(err => console.error(err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleBulkUploadSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!bulkCsvFile) {
|
||||||
|
alert("Please select a CSV file to upload.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setIsParsingBulk(true);
|
||||||
|
setBulkLog(['Uploading files to server...', 'Parsing CSV data and extracting ZIP images...']);
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('csv_file', bulkCsvFile);
|
||||||
|
if (bulkZipFile) {
|
||||||
|
formData.append('zip_file', bulkZipFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem('access_token');
|
||||||
|
const response = await fetch(`${CONFIG.apiBaseUrl}/api/products/bulk-upload/`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
...(token ? { 'Authorization': `Bearer ${token}` } : {})
|
||||||
|
},
|
||||||
|
body: formData
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(data.error || 'Failed to complete bulk upload.');
|
||||||
|
}
|
||||||
|
|
||||||
|
setBulkLog([
|
||||||
|
'CSV file parsed successfully.',
|
||||||
|
`Extracted and matched images for SKUs from ZIP file.`,
|
||||||
|
`Successfully added ${data.products?.length || 0} product listings!`,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Refresh product list
|
||||||
|
const prodRes = await apiFetch(`${CONFIG.apiBaseUrl}/api/products/`);
|
||||||
|
const prods = await prodRes.json();
|
||||||
|
if (Array.isArray(prods)) {
|
||||||
|
setProducts(prods);
|
||||||
|
} else if (prods && Array.isArray(prods.results)) {
|
||||||
|
setProducts(prods.results);
|
||||||
|
}
|
||||||
|
setBulkCsvFile(null);
|
||||||
|
setBulkZipFile(null);
|
||||||
|
} catch (err: any) {
|
||||||
|
setBulkLog(prev => [...prev, `Error: ${err.message}`]);
|
||||||
|
} finally {
|
||||||
|
setIsParsingBulk(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Wallet outstanding requests
|
||||||
|
const handleWithdrawRequest = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
const amount = Number(withdrawAmount)
|
||||||
|
if (isNaN(amount) || amount <= 0) {
|
||||||
|
alert('Please enter a valid amount')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
apiFetch(`${CONFIG.apiBaseUrl}/api/wallet/withdraw/`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ amount: String(amount) })
|
||||||
|
})
|
||||||
|
.then(res => {
|
||||||
|
if (!res.ok) throw new Error('Insufficient funds or invalid request');
|
||||||
|
return res.json();
|
||||||
|
})
|
||||||
|
.then(updatedWallet => {
|
||||||
|
setWallet(updatedWallet)
|
||||||
|
setWithdrawAmount('')
|
||||||
|
alert(`Payout of $${amount} successfully transferred!`)
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
alert(err.message)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns actions
|
||||||
|
const handleReturnAction = (id: string, action: 'Approved' | 'Rejected') => {
|
||||||
|
apiFetch(`${CONFIG.apiBaseUrl}/api/returns/${id}/action/`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ action: action })
|
||||||
|
})
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(updatedReturn => {
|
||||||
|
setReturns(returns.map(ret => ret.id === id ? updatedReturn : ret))
|
||||||
|
alert(`Return request ${action.toLowerCase()}!`)
|
||||||
|
})
|
||||||
|
.catch(err => console.error(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute realtime metrics based on state loaded from backend
|
||||||
|
const computeRealtimeMetrics = () => {
|
||||||
|
const totalSales = orders
|
||||||
|
.filter(o => o.status !== 'Cancelled' && o.status !== 'Rejected')
|
||||||
|
.reduce((sum, o) => sum + Number(o.total), 0)
|
||||||
|
|
||||||
|
const totalEarned = Number(wallet.outstanding) + Number(wallet.withdrawn)
|
||||||
|
const totalStock = products.reduce((sum, p) => sum + Number(p.stock), 0)
|
||||||
|
const totalReturns = returns.length
|
||||||
|
|
||||||
|
const isLoggedIn = currentPage === 'dashboard' || currentPage === 'profile-completion' || currentPage === 'welcome-tour' || isProfileComplete
|
||||||
|
|
||||||
|
if (isLoggedIn) {
|
||||||
|
return {
|
||||||
|
totalSales,
|
||||||
|
totalEarned,
|
||||||
|
stockDetails: totalStock,
|
||||||
|
returnedItems: totalReturns,
|
||||||
|
chartValues: orders.length > 0
|
||||||
|
? orders.map(o => Number(o.total))
|
||||||
|
: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
totalSales: 45280.00,
|
||||||
|
totalEarned: 38488.00,
|
||||||
|
stockDetails: 342,
|
||||||
|
returnedItems: 12,
|
||||||
|
chartValues: CONFIG.dashboardData.filters[dateFilter]?.chartValues || [30, 45, 35, 60, 50, 75, 65, 80, 70, 95, 90, 110]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedMetrics = computeRealtimeMetrics()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const contextValue = {
|
||||||
|
setBusinessBio, email, resetPassword, setLogoS3Key, setStoreName, setShowLoginPass, storeName, otpLoginSent, setOtpLoginSent, aadharS3Key, logoS3Key, customDates, setProductFormDetails, handleBulkUploadSubmit, setMapCoordinates, setAadharFile, gstin, mapCoordinates, setIsGstinVerified, enteredPhoneOtp, address, setEmailVerified, dashTab, selectedOrderDetail, setShowSignupConfirmPass, handleRegisterSubmit, setBulkCsvFile, confirmPassword, navigateTo, setResetPassword, orderNotes, setOrders, handleRejectOrder, barcodeProductSku, setEnteredEmailOtp, setIsEditingProduct, setBulkLog, isMobileMenuOpen, resetOtp, productWizardStep, setCustomDates, setConfirmPassword, isGstinVerified, bulkCsvFile, setAddress, storeLogo, handleProfileSubmit, setPhoneOtpSent, setOrderNotes, handleDeleteProduct, setWallet, policyAccepted, emailOtpSent, setResetOtpSent, setGstin, phoneOtpSent, showLoginPass, showSignupConfirmPass, setPhoneVerified, aadharFile, setDateFilter, activeTab, computeRealtimeMetrics, setIsParsingBulk, handleLogout, panS3Key, setCurrentPage, selectedCategories, setEmailOtpSent, showSignupPass, resetEmail, handleLogoChange, dateFilter, setShowSignupPass, businessBio, setWithdrawAmount, phoneVerified, currentPage, setResetOtp, enteredEmailOtp, setEnteredPhoneOtp, setPanFile, setIsSidebarOpen, setSelectedOrderDetail, handleAcceptOrder, saveProfileBackend, supportPhone, handleEditClick, setStoreLogo, setEmail, password, setOtpLoginPhone, bulkLog, businessType, setPanS3Key, setProducts, setSupportEmail, setOtpLoginCode, withdrawAmount, handleSaveProduct, setStoreSlug, setBarcodeProductSku, setProductForm, setBulkZipFile, productFormDetails, setIsProfileComplete, otpLoginPhone, orders, setActiveTab, bulkZipFile, isSidebarOpen, setBarcodeGenerated, setBusinessType, handleReturnAction, setSupportPhone, barcodeGenerated, products, productForm, setProductWizardStep, resetOtpSent, emailVerified, panFile, isParsingBulk, setDashTab, setReturns, setPassword, handleWithdrawRequest, uploadDocument, setPolicyAccepted, isEditingProduct, setIsMobileMenuOpen, profileStep, isProfileComplete, setSelectedCategories, storeSlug, phone, setPhone, setAadharS3Key, handleVerifyGstin, setProfileStep, returns, handleLoginSubmit, setResetEmail, wallet, otpLoginCode, supportEmail
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SellerContext.Provider value={contextValue}>
|
||||||
|
{children}
|
||||||
|
</SellerContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
2006
src/index.css
2006
src/index.css
File diff suppressed because it is too large
Load diff
|
|
@ -6,4 +6,12 @@ export default defineConfig({
|
||||||
plugins: [
|
plugins: [
|
||||||
react(),
|
react(),
|
||||||
],
|
],
|
||||||
|
server: {
|
||||||
|
proxy: {
|
||||||
|
'/api': {
|
||||||
|
target: 'http://127.0.0.1:8000',
|
||||||
|
changeOrigin: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue