Ajouter SSR prerending, espace employe, formulaire inscription employee depuis admin
This commit is contained in:
@@ -36,7 +36,7 @@ const router = express.Router();
|
|||||||
router.get(
|
router.get(
|
||||||
"/ticket/:code",
|
"/ticket/:code",
|
||||||
authMiddleware,
|
authMiddleware,
|
||||||
roleMiddleware("EMPLOYEE", "ADMIN"),
|
roleMiddleware("EMPLOYEE"),
|
||||||
getGainByTicketCode
|
getGainByTicketCode
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -69,7 +69,7 @@ router.get(
|
|||||||
router.patch(
|
router.patch(
|
||||||
"/claim/:id/deliver",
|
"/claim/:id/deliver",
|
||||||
authMiddleware,
|
authMiddleware,
|
||||||
roleMiddleware("EMPLOYEE", "ADMIN"),
|
roleMiddleware("EMPLOYEE"),
|
||||||
deliverClaim
|
deliverClaim
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
FROM node:22-alpine AS build
|
FROM node:22-slim AS build
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
RUN npm ci
|
RUN npm ci
|
||||||
|
RUN npx playwright install --with-deps chromium
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export default defineConfig([
|
|||||||
globalIgnores(['dist']),
|
globalIgnores(['dist']),
|
||||||
{
|
{
|
||||||
files: ['**/*.{js,jsx}'],
|
files: ['**/*.{js,jsx}'],
|
||||||
|
ignores: ['scripts/**'],
|
||||||
extends: [
|
extends: [
|
||||||
js.configs.recommended,
|
js.configs.recommended,
|
||||||
reactHooks.configs.flat.recommended,
|
reactHooks.configs.flat.recommended,
|
||||||
@@ -18,4 +19,13 @@ export default defineConfig([
|
|||||||
parserOptions: { ecmaFeatures: { jsx: true } },
|
parserOptions: { ecmaFeatures: { jsx: true } },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
// Scripts Node exécutés à la compilation (ex. prerender.js), jamais
|
||||||
|
// dans le navigateur : globals Node, pas de règles React.
|
||||||
|
files: ['scripts/**/*.js'],
|
||||||
|
extends: [js.configs.recommended],
|
||||||
|
languageOptions: {
|
||||||
|
globals: globals.node,
|
||||||
|
},
|
||||||
|
},
|
||||||
])
|
])
|
||||||
|
|||||||
@@ -6,6 +6,12 @@ server {
|
|||||||
index index.html;
|
index index.html;
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
try_files $uri $uri/ /index.html;
|
# $uri/index.html (et non $uri/) : sert directement le fichier des
|
||||||
|
# pages pré-rendues (ex. /accueil -> dist/accueil/index.html) sans
|
||||||
|
# passer par la redirection 301 vers l'URL à slash final que nginx
|
||||||
|
# déclenche pour une résolution de type répertoire ($uri/). Les
|
||||||
|
# canonical/sitemap pointent vers les URLs sans slash : elles
|
||||||
|
# doivent répondre 200 directement, pas via un redirect.
|
||||||
|
try_files $uri $uri/index.html /index.html;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
97
frontend/package-lock.json
generated
97
frontend/package-lock.json
generated
@@ -26,6 +26,7 @@
|
|||||||
"eslint-plugin-react-refresh": "^0.5.2",
|
"eslint-plugin-react-refresh": "^0.5.2",
|
||||||
"globals": "^17.6.0",
|
"globals": "^17.6.0",
|
||||||
"jsdom": "^29.1.1",
|
"jsdom": "^29.1.1",
|
||||||
|
"playwright": "^1.62.1",
|
||||||
"tailwindcss": "^4.3.0",
|
"tailwindcss": "^4.3.0",
|
||||||
"vite": "^8.0.12",
|
"vite": "^8.0.12",
|
||||||
"vitest": "^4.1.10"
|
"vitest": "^4.1.10"
|
||||||
@@ -120,7 +121,6 @@
|
|||||||
"integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==",
|
"integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.29.7",
|
"@babel/code-frame": "^7.29.7",
|
||||||
"@babel/generator": "^7.29.7",
|
"@babel/generator": "^7.29.7",
|
||||||
@@ -441,7 +441,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20.19.0"
|
"node": ">=20.19.0"
|
||||||
},
|
},
|
||||||
@@ -490,11 +489,33 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20.19.0"
|
"node": ">=20.19.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@emnapi/core": {
|
||||||
|
"version": "1.11.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.3.tgz",
|
||||||
|
"integrity": "sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@emnapi/wasi-threads": "1.2.3",
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@emnapi/runtime": {
|
||||||
|
"version": "1.11.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz",
|
||||||
|
"integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@emnapi/wasi-threads": {
|
"node_modules/@emnapi/wasi-threads": {
|
||||||
"version": "1.2.3",
|
"version": "1.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.3.tgz",
|
||||||
@@ -1442,7 +1463,8 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
|
||||||
"integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
|
"integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/@types/chai": {
|
"node_modules/@types/chai": {
|
||||||
"version": "5.2.3",
|
"version": "5.2.3",
|
||||||
@@ -1489,7 +1511,6 @@
|
|||||||
"integrity": "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==",
|
"integrity": "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"csstype": "^3.2.2"
|
"csstype": "^3.2.2"
|
||||||
}
|
}
|
||||||
@@ -1500,7 +1521,6 @@
|
|||||||
"integrity": "sha512-Bsc+QHgp+P/F02XDzNCY9jnZNCUuLki36KT7VKrTXXLdHf+vHMNZnW1rVu5DNW/rCK+fya3DATySbLM4yhtKUw==",
|
"integrity": "sha512-Bsc+QHgp+P/F02XDzNCY9jnZNCUuLki36KT7VKrTXXLdHf+vHMNZnW1rVu5DNW/rCK+fya3DATySbLM4yhtKUw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "^19.2.0"
|
"@types/react": "^19.2.0"
|
||||||
}
|
}
|
||||||
@@ -1650,7 +1670,6 @@
|
|||||||
"integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==",
|
"integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"acorn": "bin/acorn"
|
"acorn": "bin/acorn"
|
||||||
},
|
},
|
||||||
@@ -1691,6 +1710,7 @@
|
|||||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
@@ -1701,6 +1721,7 @@
|
|||||||
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
|
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
},
|
},
|
||||||
@@ -1794,7 +1815,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"baseline-browser-mapping": "^2.11.12",
|
"baseline-browser-mapping": "^2.11.12",
|
||||||
"caniuse-lite": "^1.0.30001809",
|
"caniuse-lite": "^1.0.30001809",
|
||||||
@@ -1974,7 +1994,8 @@
|
|||||||
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
|
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
|
||||||
"integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
|
"integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/electron-to-chromium": {
|
"node_modules/electron-to-chromium": {
|
||||||
"version": "1.5.408",
|
"version": "1.5.408",
|
||||||
@@ -2046,7 +2067,6 @@
|
|||||||
"integrity": "sha512-wqA7W2jbsC/BnV9Iv1UZpKVFkO1AdNoSmYW8NWG4HNOBbkAMvIqDZ27pI2f07dqn583NcIC44ckjAcOXDL1QbQ==",
|
"integrity": "sha512-wqA7W2jbsC/BnV9Iv1UZpKVFkO1AdNoSmYW8NWG4HNOBbkAMvIqDZ27pI2f07dqn583NcIC44ckjAcOXDL1QbQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"packages/*"
|
"packages/*"
|
||||||
],
|
],
|
||||||
@@ -2923,6 +2943,7 @@
|
|||||||
"integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
|
"integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"lz-string": "bin/bin.js"
|
"lz-string": "bin/bin.js"
|
||||||
}
|
}
|
||||||
@@ -3130,7 +3151,6 @@
|
|||||||
"integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==",
|
"integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
@@ -3138,6 +3158,53 @@
|
|||||||
"url": "https://github.com/sponsors/jonschlinkert"
|
"url": "https://github.com/sponsors/jonschlinkert"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/playwright": {
|
||||||
|
"version": "1.62.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.62.1.tgz",
|
||||||
|
"integrity": "sha512-0M+L3LAD8/nm554LOla9Ayx0j0tmFZ0FBcoQ7F1VuVHpM/XpiC8RcDzBQB8W5+hA8L22THxELzeF+2WcUzvcLg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"playwright-core": "1.62.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"playwright": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"fsevents": "2.3.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/playwright-core": {
|
||||||
|
"version": "1.62.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.62.1.tgz",
|
||||||
|
"integrity": "sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"playwright-core": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/playwright/node_modules/fsevents": {
|
||||||
|
"version": "2.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||||
|
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||||
|
"dev": true,
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/postcss": {
|
"node_modules/postcss": {
|
||||||
"version": "8.5.26",
|
"version": "8.5.26",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz",
|
||||||
@@ -3183,6 +3250,7 @@
|
|||||||
"integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
|
"integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ansi-regex": "^5.0.1",
|
"ansi-regex": "^5.0.1",
|
||||||
"ansi-styles": "^5.0.0",
|
"ansi-styles": "^5.0.0",
|
||||||
@@ -3207,7 +3275,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz",
|
||||||
"integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==",
|
"integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
@@ -3217,7 +3284,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz",
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz",
|
||||||
"integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==",
|
"integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"scheduler": "^0.27.0"
|
"scheduler": "^0.27.0"
|
||||||
},
|
},
|
||||||
@@ -3230,7 +3296,8 @@
|
|||||||
"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,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/react-router": {
|
"node_modules/react-router": {
|
||||||
"version": "7.18.2",
|
"version": "7.18.2",
|
||||||
@@ -3625,7 +3692,6 @@
|
|||||||
"integrity": "sha512-EU/eS7BH3XROHh2YnBefjM6DBKA6ZeMZEYQbj7NLWg5wHYlhB8B/Mayd5XsgWq+NFYccDOTemRpdETWR6Ka/lw==",
|
"integrity": "sha512-EU/eS7BH3XROHh2YnBefjM6DBKA6ZeMZEYQbj7NLWg5wHYlhB8B/Mayd5XsgWq+NFYccDOTemRpdETWR6Ka/lw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"lightningcss": "^1.33.0",
|
"lightningcss": "^1.33.0",
|
||||||
"picomatch": "^4.0.5",
|
"picomatch": "^4.0.5",
|
||||||
@@ -4183,7 +4249,6 @@
|
|||||||
"integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==",
|
"integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/colinhacks"
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,8 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build && node scripts/prerender.js",
|
||||||
|
"prerender": "node scripts/prerender.js",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"test": "vitest run",
|
"test": "vitest run",
|
||||||
@@ -30,6 +31,7 @@
|
|||||||
"eslint-plugin-react-refresh": "^0.5.2",
|
"eslint-plugin-react-refresh": "^0.5.2",
|
||||||
"globals": "^17.6.0",
|
"globals": "^17.6.0",
|
||||||
"jsdom": "^29.1.1",
|
"jsdom": "^29.1.1",
|
||||||
|
"playwright": "^1.62.1",
|
||||||
"tailwindcss": "^4.3.0",
|
"tailwindcss": "^4.3.0",
|
||||||
"vite": "^8.0.12",
|
"vite": "^8.0.12",
|
||||||
"vitest": "^4.1.10"
|
"vitest": "^4.1.10"
|
||||||
|
|||||||
54
frontend/scripts/prerender.js
Normal file
54
frontend/scripts/prerender.js
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import { mkdirSync, writeFileSync } from 'node:fs'
|
||||||
|
import { dirname, join } from 'node:path'
|
||||||
|
import { fileURLToPath } from 'node:url'
|
||||||
|
import { preview } from 'vite'
|
||||||
|
import { chromium } from 'playwright'
|
||||||
|
|
||||||
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||||
|
const root = join(__dirname, '..')
|
||||||
|
const distDir = join(root, 'dist')
|
||||||
|
|
||||||
|
// Pages publiques, indexables (voir public/robots.txt) : ce sont les seules
|
||||||
|
// qui ont besoin d'un HTML pré-rendu pour le SEO et les aperçus sociaux. Les
|
||||||
|
// pages privées (profil, mes gains, admin...) restent en rendu 100% client,
|
||||||
|
// servies par le index.html générique via le fallback SPA de nginx.
|
||||||
|
const ROUTES = [
|
||||||
|
'/accueil',
|
||||||
|
'/notre-concept',
|
||||||
|
'/regles-jeu',
|
||||||
|
'/contact',
|
||||||
|
'/mentions-legales',
|
||||||
|
'/confidentialite',
|
||||||
|
'/accessibilite',
|
||||||
|
'/cgu',
|
||||||
|
'/sitemap',
|
||||||
|
'/inscription',
|
||||||
|
'/connexion',
|
||||||
|
]
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const server = await preview({ root, preview: { port: 4173, strictPort: false } })
|
||||||
|
const base = server.resolvedUrls.local[0].replace(/\/$/, '')
|
||||||
|
|
||||||
|
const browser = await chromium.launch()
|
||||||
|
const page = await browser.newPage()
|
||||||
|
|
||||||
|
for (const route of ROUTES) {
|
||||||
|
await page.goto(`${base}${route}`, { waitUntil: 'networkidle' })
|
||||||
|
await page.waitForSelector('h1')
|
||||||
|
|
||||||
|
const html = await page.content()
|
||||||
|
const outDir = join(distDir, route.replace(/^\//, ''))
|
||||||
|
mkdirSync(outDir, { recursive: true })
|
||||||
|
writeFileSync(join(outDir, 'index.html'), `<!doctype html>\n${html}`)
|
||||||
|
console.log(`✓ pré-rendu ${route} -> dist${route}/index.html`)
|
||||||
|
}
|
||||||
|
|
||||||
|
await browser.close()
|
||||||
|
await server.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((error) => {
|
||||||
|
console.error('Erreur de pré-rendu :', error)
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { getStatsApi, getUsersApi, drawWinnerApi, getWinnerApi, getEmailingContactsApi, setGameSettingsApi } from '../services/admin'
|
import { getStatsApi, getUsersApi, drawWinnerApi, getWinnerApi, getEmailingContactsApi, setGameSettingsApi, createEmployeeApi } from '../services/admin'
|
||||||
import { searchTicketApi, deliverClaimApi } from '../services/employee'
|
|
||||||
import { useHandleAuthError } from '../hooks/useHandleAuthError'
|
import { useHandleAuthError } from '../hooks/useHandleAuthError'
|
||||||
|
|
||||||
function StatCard({ label, value, sub }) {
|
function StatCard({ label, value, sub }) {
|
||||||
@@ -63,20 +62,17 @@ export default function Admin() {
|
|||||||
const [settingsError, setSettingsError] = useState('')
|
const [settingsError, setSettingsError] = useState('')
|
||||||
const [settingsOk, setSettingsOk] = useState('')
|
const [settingsOk, setSettingsOk] = useState('')
|
||||||
|
|
||||||
// Remise de lot
|
|
||||||
const [ticketCode, setTicketCode] = useState('')
|
|
||||||
const [ticketResult, setTicketResult] = useState(null)
|
|
||||||
const [ticketSearching, setTicketSearching] = useState(false)
|
|
||||||
const [ticketDelivering, setTicketDelivering] = useState(false)
|
|
||||||
const [ticketSearchErr, setTicketSearchErr] = useState('')
|
|
||||||
const [ticketDeliverErr, setTicketDeliverErr] = useState('')
|
|
||||||
const [ticketSuccess, setTicketSuccess] = useState(false)
|
|
||||||
|
|
||||||
// Contacts emailing
|
// Contacts emailing
|
||||||
const [emailingContacts, setEmailingContacts] = useState(null)
|
const [emailingContacts, setEmailingContacts] = useState(null)
|
||||||
const [emailingLoading, setEmailingLoading] = useState(false)
|
const [emailingLoading, setEmailingLoading] = useState(false)
|
||||||
const [emailingError, setEmailingError] = useState('')
|
const [emailingError, setEmailingError] = useState('')
|
||||||
|
|
||||||
|
// Création d'un compte employé
|
||||||
|
const [employeeForm, setEmployeeForm] = useState({ firstname: '', lastname: '', email: '', password: '' })
|
||||||
|
const [employeeSaving, setEmployeeSaving] = useState(false)
|
||||||
|
const [employeeError, setEmployeeError] = useState('')
|
||||||
|
const [employeeOk, setEmployeeOk] = useState('')
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
Promise.all([getStatsApi(), getUsersApi(), getWinnerApi()])
|
Promise.all([getStatsApi(), getUsersApi(), getWinnerApi()])
|
||||||
.then(([s, u, w]) => {
|
.then(([s, u, w]) => {
|
||||||
@@ -154,43 +150,19 @@ export default function Admin() {
|
|||||||
URL.revokeObjectURL(url)
|
URL.revokeObjectURL(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleTicketSearch = async (e) => {
|
const handleCreateEmployee = async (e) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
setTicketResult(null)
|
setEmployeeError('')
|
||||||
setTicketSearchErr('')
|
setEmployeeOk('')
|
||||||
setTicketDeliverErr('')
|
setEmployeeSaving(true)
|
||||||
setTicketSuccess(false)
|
|
||||||
setTicketSearching(true)
|
|
||||||
try {
|
try {
|
||||||
const data = await searchTicketApi(ticketCode.trim())
|
const data = await createEmployeeApi(employeeForm)
|
||||||
setTicketResult(data)
|
setEmployeeOk(`Compte employé créé pour ${data.employee.firstname} ${data.employee.lastname} (${data.employee.email}).`)
|
||||||
|
setEmployeeForm({ firstname: '', lastname: '', email: '', password: '' })
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!handleAuthError(err)) setTicketSearchErr(err.message)
|
if (!handleAuthError(err)) setEmployeeError(err.message)
|
||||||
} finally {
|
} finally {
|
||||||
setTicketSearching(false)
|
setEmployeeSaving(false)
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleTicketDeliver = async () => {
|
|
||||||
if (!ticketResult?.claim?.id) return
|
|
||||||
setTicketDeliverErr('')
|
|
||||||
setTicketDelivering(true)
|
|
||||||
try {
|
|
||||||
await deliverClaimApi(ticketResult.claim.id)
|
|
||||||
setTicketResult((prev) => ({
|
|
||||||
...prev,
|
|
||||||
claim: { ...prev.claim, status: 'DELIVERED', claimedAt: new Date().toISOString() },
|
|
||||||
}))
|
|
||||||
setTicketSuccess(true)
|
|
||||||
setStats((prev) => prev ? {
|
|
||||||
...prev,
|
|
||||||
claimsPending: Math.max(0, (prev.claimsPending ?? 1) - 1),
|
|
||||||
claimsDelivered: (prev.claimsDelivered ?? 0) + 1,
|
|
||||||
} : prev)
|
|
||||||
} catch (err) {
|
|
||||||
if (!handleAuthError(err)) setTicketDeliverErr(err.message)
|
|
||||||
} finally {
|
|
||||||
setTicketDelivering(false)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -415,93 +387,73 @@ export default function Admin() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ── Remise de lot ── */}
|
{/* ── Création d'un compte employé ── */}
|
||||||
<div className="bg-white rounded-xl shadow p-4 sm:p-6">
|
<div className="bg-white rounded-xl shadow p-4 sm:p-6">
|
||||||
<h2 className="text-base sm:text-lg font-bold text-gray-800 mb-4">Remise de lot</h2>
|
<h2 className="text-base sm:text-lg font-bold text-gray-800 mb-1">Créer un compte employé</h2>
|
||||||
|
<p className="text-xs sm:text-sm text-gray-500 mb-4">
|
||||||
|
Donne accès à l'espace employé (remise des lots en boutique). L'admin ne gère pas les remises lui-même.
|
||||||
|
</p>
|
||||||
|
|
||||||
<form onSubmit={handleTicketSearch} className="flex gap-2 mb-4">
|
<form onSubmit={handleCreateEmployee} className="grid sm:grid-cols-2 gap-3">
|
||||||
<label htmlFor="admin-ticket-code" className="sr-only">Code ticket</label>
|
<div>
|
||||||
|
<label htmlFor="employee-firstname" className="block text-xs font-medium text-gray-700 mb-1">Prénom</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
id="admin-ticket-code"
|
id="employee-firstname"
|
||||||
value={ticketCode}
|
value={employeeForm.firstname}
|
||||||
onChange={(e) => setTicketCode(e.target.value.toUpperCase())}
|
onChange={(e) => setEmployeeForm({ ...employeeForm, firstname: e.target.value })}
|
||||||
placeholder="Code ticket (10 caractères)"
|
required
|
||||||
maxLength={10}
|
className="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm text-gray-700 focus:outline-none focus:ring-1 focus:ring-[#006B6B] focus:border-[#006B6B]"
|
||||||
className="flex-1 min-w-0 border border-gray-200 rounded-lg px-4 py-2.5 text-sm font-mono uppercase tracking-widest focus:outline-none focus:ring-2 focus:ring-[#006B6B] focus:border-[#006B6B]"
|
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label htmlFor="employee-lastname" className="block text-xs font-medium text-gray-700 mb-1">Nom</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="employee-lastname"
|
||||||
|
value={employeeForm.lastname}
|
||||||
|
onChange={(e) => setEmployeeForm({ ...employeeForm, lastname: e.target.value })}
|
||||||
|
required
|
||||||
|
className="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm text-gray-700 focus:outline-none focus:ring-1 focus:ring-[#006B6B] focus:border-[#006B6B]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label htmlFor="employee-email" className="block text-xs font-medium text-gray-700 mb-1">Email</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
id="employee-email"
|
||||||
|
value={employeeForm.email}
|
||||||
|
onChange={(e) => setEmployeeForm({ ...employeeForm, email: e.target.value })}
|
||||||
|
required
|
||||||
|
className="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm text-gray-700 focus:outline-none focus:ring-1 focus:ring-[#006B6B] focus:border-[#006B6B]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label htmlFor="employee-password" className="block text-xs font-medium text-gray-700 mb-1">Mot de passe</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
id="employee-password"
|
||||||
|
value={employeeForm.password}
|
||||||
|
onChange={(e) => setEmployeeForm({ ...employeeForm, password: e.target.value })}
|
||||||
|
required
|
||||||
|
minLength={8}
|
||||||
|
placeholder="8 caractères minimum"
|
||||||
|
className="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm text-gray-700 focus:outline-none focus:ring-1 focus:ring-[#006B6B] focus:border-[#006B6B]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="sm:col-span-2 flex items-center gap-3 pt-1">
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={ticketSearching || ticketCode.trim().length === 0}
|
disabled={employeeSaving}
|
||||||
className="bg-[#006B6B] hover:bg-[#005959] disabled:opacity-50 text-white font-semibold px-4 py-2.5 rounded-lg transition-colors text-sm whitespace-nowrap"
|
className="bg-[#006B6B] hover:bg-[#005959] disabled:opacity-50 text-white font-semibold px-4 py-2 rounded-lg transition-colors text-xs sm:text-sm"
|
||||||
>
|
>
|
||||||
{ticketSearching ? 'Recherche…' : 'Rechercher'}
|
{employeeSaving ? 'Création…' : 'Créer le compte'}
|
||||||
</button>
|
</button>
|
||||||
|
{employeeError && <p className="text-red-500 text-xs sm:text-sm">{employeeError}</p>}
|
||||||
|
{employeeOk && <p className="text-green-600 text-xs sm:text-sm">{employeeOk}</p>}
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
{ticketSearchErr && (
|
|
||||||
<p className="text-red-500 text-sm mb-3">{ticketSearchErr}</p>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{ticketResult && (
|
|
||||||
<div className="border border-gray-100 rounded-xl overflow-hidden">
|
|
||||||
{ticketSuccess && (
|
|
||||||
<div className="bg-green-500 px-4 py-2.5 flex items-center gap-2">
|
|
||||||
<svg className="w-4 h-4 text-white flex-shrink-0" fill="none" stroke="currentColor" strokeWidth={2.5} viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
|
|
||||||
</svg>
|
|
||||||
<p className="text-white text-sm font-medium">Lot remis avec succès.</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="px-4 py-3 flex items-start justify-between gap-3 border-b border-gray-100">
|
|
||||||
<div>
|
|
||||||
<p className="font-semibold text-gray-900 text-sm">{ticketResult.lot.name}</p>
|
|
||||||
<p className="text-gray-500 text-xs mt-0.5">{ticketResult.lot.description}</p>
|
|
||||||
{ticketResult.lot.value && <p className="text-[#006B6B] text-xs font-semibold mt-1">{ticketResult.lot.value} €</p>}
|
|
||||||
</div>
|
|
||||||
<span className={`flex-shrink-0 text-xs font-semibold px-2.5 py-1 rounded-full ${
|
|
||||||
ticketResult.claim?.status === 'DELIVERED' ? 'bg-green-100 text-green-700'
|
|
||||||
: ticketResult.claim?.status === 'CANCELLED' ? 'bg-gray-100 text-gray-500'
|
|
||||||
: 'bg-yellow-100 text-yellow-700'
|
|
||||||
}`}>
|
|
||||||
{ticketResult.claim?.status === 'DELIVERED' ? 'Remis'
|
|
||||||
: ticketResult.claim?.status === 'CANCELLED' ? 'Expiré'
|
|
||||||
: 'À remettre'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="px-4 py-3 flex items-center gap-3 border-b border-gray-100">
|
|
||||||
<div className="w-8 h-8 rounded-full bg-[#006B6B] flex items-center justify-center text-white text-xs font-bold flex-shrink-0">
|
|
||||||
{(ticketResult.client.firstname?.[0] ?? '?').toUpperCase()}{(ticketResult.client.lastname?.[0] ?? '?').toUpperCase()}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="text-sm font-medium text-gray-800">{ticketResult.client.firstname} {ticketResult.client.lastname}</p>
|
|
||||||
<p className="text-xs text-gray-500">{ticketResult.client.email}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="px-4 py-3 space-y-2">
|
|
||||||
{ticketDeliverErr && <p className="text-red-500 text-xs">{ticketDeliverErr}</p>}
|
|
||||||
|
|
||||||
{ticketResult.claim?.status === 'PENDING' && !ticketSuccess && (
|
|
||||||
<button
|
|
||||||
onClick={handleTicketDeliver}
|
|
||||||
disabled={ticketDelivering}
|
|
||||||
className="w-full bg-[#006B6B] hover:bg-[#005959] disabled:opacity-50 text-white font-semibold py-2.5 rounded-lg transition-colors text-sm"
|
|
||||||
>
|
|
||||||
{ticketDelivering ? 'Confirmation…' : 'Confirmer la remise du lot'}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
<button
|
|
||||||
onClick={() => { setTicketCode(''); setTicketResult(null); setTicketSuccess(false); setTicketSearchErr(''); setTicketDeliverErr('') }}
|
|
||||||
className="w-full border border-gray-200 text-gray-500 hover:bg-gray-50 py-2 rounded-lg transition-colors text-sm"
|
|
||||||
>
|
|
||||||
Rechercher un autre code
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ── Tirage au sort ── */}
|
{/* ── Tirage au sort ── */}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useRef, useState } from 'react'
|
import { useEffect, useRef } from 'react'
|
||||||
import { useNavigate, useSearchParams, Link } from 'react-router-dom'
|
import { useNavigate, useSearchParams, Link } from 'react-router-dom'
|
||||||
import { useAuth } from '../context/AuthContext'
|
import { useAuth } from '../context/AuthContext'
|
||||||
|
|
||||||
@@ -8,39 +8,40 @@ export default function AuthCallback() {
|
|||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const [searchParams] = useSearchParams()
|
const [searchParams] = useSearchParams()
|
||||||
const { login } = useAuth()
|
const { login } = useAuth()
|
||||||
const [error, setError] = useState('')
|
|
||||||
// StrictMode monte deux fois en dev : on ne traite le token qu'une seule fois.
|
// StrictMode monte deux fois en dev : on ne traite le token qu'une seule fois.
|
||||||
const handled = useRef(false)
|
const handled = useRef(false)
|
||||||
|
|
||||||
useEffect(() => {
|
// L'erreur (s'il y en a une) est entièrement dérivée de l'URL : pas besoin
|
||||||
if (handled.current) return
|
// d'un state ni d'un effet pour la calculer, juste pour l'action à usage
|
||||||
handled.current = true
|
// unique (login + redirection) une fois l'utilisateur décodé avec succès.
|
||||||
|
|
||||||
const errorParam = searchParams.get('error')
|
const errorParam = searchParams.get('error')
|
||||||
if (errorParam) {
|
|
||||||
setError(errorParam)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const token = searchParams.get('token')
|
const token = searchParams.get('token')
|
||||||
const encodedUser = searchParams.get('user')
|
const encodedUser = searchParams.get('user')
|
||||||
|
|
||||||
if (!token || !encodedUser) {
|
let error = ''
|
||||||
setError('Réponse de connexion incomplète.')
|
let user = null
|
||||||
return
|
if (errorParam) {
|
||||||
|
error = errorParam
|
||||||
|
} else if (!token || !encodedUser) {
|
||||||
|
error = 'Réponse de connexion incomplète.'
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
user = JSON.parse(atob(encodedUser))
|
||||||
|
} catch {
|
||||||
|
error = 'Réponse de connexion illisible.'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
useEffect(() => {
|
||||||
const user = JSON.parse(atob(encodedUser))
|
if (handled.current || !user) return
|
||||||
|
handled.current = true
|
||||||
|
|
||||||
login(user, token)
|
login(user, token)
|
||||||
|
|
||||||
if (user.role === 'ADMIN') navigate('/admin', { replace: true })
|
if (user.role === 'ADMIN') navigate('/admin', { replace: true })
|
||||||
else if (user.role === 'EMPLOYEE') navigate('/employe', { replace: true })
|
else if (user.role === 'EMPLOYEE') navigate('/employe', { replace: true })
|
||||||
else navigate('/mes-gains', { replace: true })
|
else navigate('/mes-gains', { replace: true })
|
||||||
} catch {
|
}, [user, token, login, navigate])
|
||||||
setError('Réponse de connexion illisible.')
|
|
||||||
}
|
|
||||||
}, [searchParams, login, navigate])
|
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export default function Profile() {
|
|||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
if (!handleAuthError(e)) setHasPassword(true)
|
if (!handleAuthError(e)) setHasPassword(true)
|
||||||
})
|
})
|
||||||
}, [])
|
}, [handleAuthError])
|
||||||
|
|
||||||
const handleSendSetupLink = async () => {
|
const handleSendSetupLink = async () => {
|
||||||
setLinkError('')
|
setLinkError('')
|
||||||
|
|||||||
@@ -60,3 +60,14 @@ export async function getWinnerApi() {
|
|||||||
if (!res.ok) throw apiError(data.message || 'Erreur gagnant.', res.status)
|
if (!res.ok) throw apiError(data.message || 'Erreur gagnant.', res.status)
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function createEmployeeApi({ firstname, lastname, email, password }) {
|
||||||
|
const res = await fetch(`${BASE}/employees`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: authHeaders(),
|
||||||
|
body: JSON.stringify({ firstname, lastname, email, password }),
|
||||||
|
})
|
||||||
|
const data = await res.json()
|
||||||
|
if (!res.ok) throw apiError(data.message || "Erreur lors de la création de l'employé.", res.status)
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user