diff --git a/admin/package-lock.json b/admin/package-lock.json index c0d3444..b958af2 100644 --- a/admin/package-lock.json +++ b/admin/package-lock.json @@ -14,6 +14,7 @@ "@types/bcryptjs": "^2.4.6", "afrohub": "file:..", "bcryptjs": "^3.0.3", + "d3-scale": "^4.0.2", "dotenv": "^17.4.1", "lucide-react": "^1.8.0", "next": "16.2.3", @@ -22,14 +23,20 @@ "react": "19.2.4", "react-dom": "19.2.4", "react-hot-toast": "^2.6.0", - "react-quill-new": "^3.8.3" + "react-quill-new": "^3.8.3", + "react-simple-maps": "^3.0.0", + "recharts": "^3.8.1", + "topojson-client": "^3.1.0" }, "devDependencies": { "@tailwindcss/postcss": "^4", + "@types/d3-scale": "^4.0.9", "@types/node": "^20", "@types/pg": "^8.20.0", "@types/react": "^19", "@types/react-dom": "^19", + "@types/react-simple-maps": "^3.0.6", + "@types/topojson-client": "^3.1.5", "eslint": "^9", "eslint-config-next": "16.2.3", "tailwindcss": "^4", @@ -1379,6 +1386,42 @@ "@prisma/debug": "6.19.3" } }, + "node_modules/@reduxjs/toolkit": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.2.tgz", + "integrity": "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==", + "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/@reduxjs/toolkit/node_modules/immer": { + "version": "11.1.8", + "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.8.tgz", + "integrity": "sha512-/tbkHMW7y10Lx6i1crLjD4/OhNkRG+Fo7byZHtah0547nIeXYcpIXaUh0IAQY6gO5459qpGGYapcEOHtFXkIuA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", @@ -1392,6 +1435,12 @@ "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", "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" + }, "node_modules/@swc/helpers": { "version": "0.5.15", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", @@ -1689,6 +1738,98 @@ "integrity": "sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==", "license": "MIT" }, + "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": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-2.0.6.tgz", + "integrity": "sha512-tbaFGDmJWHqnenvk3QGSvD3RVwr631BjKRD7Sc7VLRgrdX5mk5hTyoeBL6rXZaeoXzmZwIl1D2HPogEdt1rHBg==", + "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-geo": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-2.0.7.tgz", + "integrity": "sha512-RIXlxPdxvX+LAZFv+t78CuYpxYag4zuw9mZc+AwfB8tZpKU90rMEn2il2ADncmeZlb7nER9dDsJpRisA3lRvjA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-interpolate": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-2.0.5.tgz", + "integrity": "sha512-UINE41RDaUMbulp+bxQMDnhOi51rh5lA2dG+dWZU0UY/IwQiG/u2x8TfnWYU9+xwGdXsJoAvrBYUEQl0r91atg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-color": "^2" + } + }, + "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-selection": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-2.0.5.tgz", + "integrity": "sha512-71BorcY0yXl12S7lvb01JdaN9TpeUHBDb4RRhSq8U8BEkX/nIk5p7Byho+ZRTsx5nYLMpAbY3qt5EhqFzfGJlw==", + "dev": true, + "license": "MIT" + }, + "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/d3-zoom": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-2.0.7.tgz", + "integrity": "sha512-JWke4E8ZyrKUQ68ESTWSK16fVb0OYnaiJ+WXJRYxKLn4aXU0o4CLYxMWBEiouUfO3TTCoyroOrGPcBG6u1aAxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-interpolate": "^2", + "@types/d3-selection": "^2" + } + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -1696,6 +1837,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -1752,6 +1900,46 @@ "@types/react": "^19.2.0" } }, + "node_modules/@types/react-simple-maps": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/react-simple-maps/-/react-simple-maps-3.0.6.tgz", + "integrity": "sha512-hR01RXt6VvsE41FxDd+Bqm1PPGdKbYjCYVtCgh38YeBPt46z3SwmWPWu2L3EdCAP6bd6VYEgztucihRw1C0Klg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-geo": "^2", + "@types/d3-zoom": "^2", + "@types/geojson": "*", + "@types/react": "*" + } + }, + "node_modules/@types/topojson-client": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/@types/topojson-client/-/topojson-client-3.1.5.tgz", + "integrity": "sha512-C79rySTyPxnQNNguTZNI1Ct4D7IXgvyAs3p9HPecnl6mNrJ5+UhvGNYcZfpROYV2lMHI48kJPxwR+F9C6c7nmw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/geojson": "*", + "@types/topojson-specification": "*" + } + }, + "node_modules/@types/topojson-specification": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/topojson-specification/-/topojson-specification-1.0.5.tgz", + "integrity": "sha512-C7KvcQh+C2nr6Y2Ub4YfgvWvWCgP2nOQMtfhlnwsRL4pYmmwzBS7HclGiS87eQfDOU/DLQpX6GEscviaz4yLIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, + "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/@typescript-eslint/eslint-plugin": { "version": "8.58.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.58.1.tgz", @@ -2859,6 +3047,15 @@ "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", "license": "MIT" }, + "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": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -2879,6 +3076,12 @@ "dev": true, "license": "MIT" }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "license": "MIT" + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -2929,6 +3132,226 @@ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "license": "MIT" }, + "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-dispatch": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-2.0.0.tgz", + "integrity": "sha512-S/m2VsXI7gAti2pBoLClFFTMOO1HTtT0j99AuXLoGFKO6deHDdnv6ZGTxSTTUTgO1zVcv82fCOtDjYK4EECmWA==", + "license": "BSD-3-Clause" + }, + "node_modules/d3-drag": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-2.0.0.tgz", + "integrity": "sha512-g9y9WbMnF5uqB9qKqwIIa/921RYWzlUDv9Jl1/yONQwxbOfszAWTCm8u7HOTgJgRDXiRZN56cHT9pd24dmXs8w==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-dispatch": "1 - 2", + "d3-selection": "2" + } + }, + "node_modules/d3-ease": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-2.0.0.tgz", + "integrity": "sha512-68/n9JWarxXkOWMshcT5IcjbB+agblQUaIsbnXmrzejn2O82n3p2A9R2zEB9HIEFWKFwPAEDDN8gR0VdSAyyAQ==", + "license": "BSD-3-Clause" + }, + "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-geo": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-2.0.2.tgz", + "integrity": "sha512-8pM1WGMLGFuhq9S+FpPURxic+gKzjluCD/CHTuUF3mXMeiCo0i6R0tO1s4+GArRFde96SLcW/kOFRjoAosPsFA==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "^2.5.0" + } + }, + "node_modules/d3-geo/node_modules/d3-array": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz", + "integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==", + "license": "BSD-3-Clause", + "dependencies": { + "internmap": "^1.0.0" + } + }, + "node_modules/d3-geo/node_modules/internmap": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz", + "integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==", + "license": "ISC" + }, + "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-selection": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-2.0.0.tgz", + "integrity": "sha512-XoGGqhLUN/W14NmaqcO/bb1nqjDAw5WtSYb2X8wiuQWvSZUsUVYsOSkOybUrNvcBjaywBdYPy03eXHMXjk9nZA==", + "license": "BSD-3-Clause" + }, + "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": "2.0.0", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-2.0.0.tgz", + "integrity": "sha512-TO4VLh0/420Y/9dO3+f9abDEFYeCUr2WZRlxJvbp4HPTQcSylXNiL6yZa9FIUvV1yRiFufl1bszTCLDqv9PWNA==", + "license": "BSD-3-Clause" + }, + "node_modules/d3-transition": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-2.0.0.tgz", + "integrity": "sha512-42ltAGgJesfQE3u9LuuBHNbGrI/AJjNL2OAUdclE70UE6Vy239GCBEYD38uBPoLeNsOhFStGpPI0BAOV+HMxog==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-color": "1 - 2", + "d3-dispatch": "1 - 2", + "d3-ease": "1 - 2", + "d3-interpolate": "1 - 2", + "d3-timer": "1 - 2" + }, + "peerDependencies": { + "d3-selection": "2" + } + }, + "node_modules/d3-transition/node_modules/d3-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-2.0.0.tgz", + "integrity": "sha512-SPXi0TSKPD4g9tw0NMZFnR95XVgUZiBH+uUTqQuDu1OsE2zomHU7ho0FISciaPvosimixwHFl3WHLGabv6dDgQ==", + "license": "BSD-3-Clause" + }, + "node_modules/d3-transition/node_modules/d3-interpolate": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-2.0.1.tgz", + "integrity": "sha512-c5UhwwTs/yybcmTpAVqwSFl6vrQ8JZJoT5F7xNFK9pymv5C0Ymcc9/LIJHtYIggg/yS9YHw8i8O8tgb9pupjeQ==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-color": "1 - 2" + } + }, + "node_modules/d3-zoom": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-2.0.0.tgz", + "integrity": "sha512-fFg7aoaEm9/jf+qfstak0IYpnesZLiMX6GZvXtUSdv8RH2o4E2qeelgdU09eKS6wGuiGMfcnMI0nTIqWzRHGpw==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-dispatch": "1 - 2", + "d3-drag": "2", + "d3-interpolate": "1 - 2", + "d3-selection": "2", + "d3-transition": "2" + } + }, + "node_modules/d3-zoom/node_modules/d3-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-2.0.0.tgz", + "integrity": "sha512-SPXi0TSKPD4g9tw0NMZFnR95XVgUZiBH+uUTqQuDu1OsE2zomHU7ho0FISciaPvosimixwHFl3WHLGabv6dDgQ==", + "license": "BSD-3-Clause" + }, + "node_modules/d3-zoom/node_modules/d3-interpolate": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-2.0.1.tgz", + "integrity": "sha512-c5UhwwTs/yybcmTpAVqwSFl6vrQ8JZJoT5F7xNFK9pymv5C0Ymcc9/LIJHtYIggg/yS9YHw8i8O8tgb9pupjeQ==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-color": "1 - 2" + } + }, "node_modules/damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", @@ -3008,6 +3431,12 @@ } } }, + "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/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -3346,6 +3775,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es-toolkit": { + "version": "1.46.1", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.46.1.tgz", + "integrity": "sha512-5eNtXOs3tbfxXOj04tjjseeWkRWaoCjdEI+96DgwzZoe6c9juL49pXlzAFTI72aWC9Y8p7168g6XIKjh7k6pyQ==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks" + ] + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -4297,6 +4736,16 @@ "node": ">= 4" } }, + "node_modules/immer": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz", + "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/import-fresh": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", @@ -4339,6 +4788,15 @@ "node": ">= 0.4" } }, + "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-array-buffer": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", @@ -6162,6 +6620,46 @@ "react-dom": "^16 || ^17 || ^18 || ^19" } }, + "node_modules/react-redux": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", + "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+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/react-simple-maps": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/react-simple-maps/-/react-simple-maps-3.0.0.tgz", + "integrity": "sha512-vKNFrvpPG8Vyfdjnz5Ne1N56rZlDfHXv5THNXOVZMqbX1rWZA48zQuYT03mx6PAKanqarJu/PDLgshIZAfHHqw==", + "license": "MIT", + "dependencies": { + "d3-geo": "^2.0.2", + "d3-selection": "^2.0.0", + "d3-zoom": "^2.0.0", + "topojson-client": "^3.1.0" + }, + "peerDependencies": { + "prop-types": "^15.7.2", + "react": "^16.8.0 || 17.x || 18.x", + "react-dom": "^16.8.0 || 17.x || 18.x" + } + }, "node_modules/readdirp": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", @@ -6175,6 +6673,51 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/recharts": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.8.1.tgz", + "integrity": "sha512-mwzmO1s9sFL0TduUpwndxCUNoXsBw3u3E/0+A+cLcrSfQitSG62L32N69GhqUrrT5qKcAE3pCGVINC6pqkBBQg==", + "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": "^10.1.1", + "react-redux": "8.x.x || 9.x.x", + "reselect": "5.1.1", + "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/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/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -6219,6 +6762,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/reselect": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", + "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", + "license": "MIT" + }, "node_modules/resolve": { "version": "2.0.0-next.6", "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.6.tgz", @@ -6820,6 +7369,12 @@ "url": "https://opencollective.com/webpack" } }, + "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/tinyexec": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.1.1.tgz", @@ -6890,6 +7445,20 @@ "node": ">=8.0" } }, + "node_modules/topojson-client": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/topojson-client/-/topojson-client-3.1.0.tgz", + "integrity": "sha512-605uxS6bcYxGXw9qi62XyrV6Q3xwbndjachmNxu8HWTtVPxZfEJN9fd/SZS1Q54Sn2y0TMyMxFj/cJINqGHrKw==", + "license": "ISC", + "dependencies": { + "commander": "2" + }, + "bin": { + "topo2geo": "bin/topo2geo", + "topomerge": "bin/topomerge", + "topoquantize": "bin/topoquantize" + } + }, "node_modules/ts-api-utils": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", @@ -7030,7 +7599,7 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "devOptional": true, + "dev": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -7166,6 +7735,64 @@ "punycode": "^2.1.0" } }, + "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/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/victory-vendor/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/victory-vendor/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/victory-vendor/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/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/admin/package.json b/admin/package.json index fe1bb42..05e45ad 100644 --- a/admin/package.json +++ b/admin/package.json @@ -15,6 +15,7 @@ "@types/bcryptjs": "^2.4.6", "afrohub": "file:..", "bcryptjs": "^3.0.3", + "d3-scale": "^4.0.2", "dotenv": "^17.4.1", "lucide-react": "^1.8.0", "next": "16.2.3", @@ -23,14 +24,20 @@ "react": "19.2.4", "react-dom": "19.2.4", "react-hot-toast": "^2.6.0", - "react-quill-new": "^3.8.3" + "react-quill-new": "^3.8.3", + "react-simple-maps": "^3.0.0", + "recharts": "^3.8.1", + "topojson-client": "^3.1.0" }, "devDependencies": { "@tailwindcss/postcss": "^4", + "@types/d3-scale": "^4.0.9", "@types/node": "^20", "@types/pg": "^8.20.0", "@types/react": "^19", "@types/react-dom": "^19", + "@types/react-simple-maps": "^3.0.6", + "@types/topojson-client": "^3.1.5", "eslint": "^9", "eslint-config-next": "16.2.3", "tailwindcss": "^4", diff --git a/admin/prisma/schema.prisma b/admin/prisma/schema.prisma index abb5f64..37a4205 100644 --- a/admin/prisma/schema.prisma +++ b/admin/prisma/schema.prisma @@ -160,6 +160,8 @@ model BlogPost { tags String[] @default([]) publishedAt DateTime @default(now()) status ContentStatus @default(PUBLISHED) + coverPosition String? @default("50% 50%") + coverZoom Float? @default(1.0) } model HomeSlide { @@ -174,6 +176,8 @@ model HomeSlide { isActive Boolean @default(true) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt + coverPosition String? @default("50% 50%") + coverZoom Float? @default(1.0) business Business? @relation(fields: [businessId], references: [id]) } @@ -198,6 +202,8 @@ model Interview { tags String[] @default([]) publishedAt DateTime @default(now()) status ContentStatus @default(PUBLISHED) + coverPosition String? @default("50% 50%") + coverZoom Float? @default(1.0) } model Comment { @@ -358,6 +364,8 @@ model Event { tags String[] @default([]) publishedAt DateTime @default(now()) status ContentStatus @default(PUBLISHED) + coverPosition String? @default("50% 50%") + coverZoom Float? @default(1.0) } model Favorite { diff --git a/admin/src/app/actions/actualites.ts b/admin/src/app/actions/actualites.ts index 53ca9d0..88540d3 100644 --- a/admin/src/app/actions/actualites.ts +++ b/admin/src/app/actions/actualites.ts @@ -17,6 +17,9 @@ export async function createBlogPost(data: { metaDescription?: string; publishedAt?: Date; status?: ContentStatus; + coverPosition?: string; + coverZoom?: number; + sources?: any; }) { try { const slug = data.slug || generateSlug(data.title); @@ -51,6 +54,9 @@ export async function updateBlogPost(id: string, data: { metaDescription?: string; publishedAt?: Date; status?: ContentStatus; + coverPosition?: string; + coverZoom?: number; + sources?: any; }) { try { const slug = data.slug || generateSlug(data.title); diff --git a/admin/src/app/actions/event.ts b/admin/src/app/actions/event.ts index a2598cd..66a8c7a 100644 --- a/admin/src/app/actions/event.ts +++ b/admin/src/app/actions/event.ts @@ -18,6 +18,8 @@ export async function createEvent(data: { metaDescription?: string; publishedAt?: Date; status?: ContentStatus; + coverPosition?: string; + coverZoom?: number; }) { try { const slug = data.slug || generateSlug(data.title); @@ -53,6 +55,8 @@ export async function updateEvent(id: string, data: { metaDescription?: string; publishedAt?: Date; status?: ContentStatus; + coverPosition?: string; + coverZoom?: number; }) { try { const slug = data.slug || generateSlug(data.title); diff --git a/admin/src/app/actions/slides.ts b/admin/src/app/actions/slides.ts index 5a8dd51..7644668 100644 --- a/admin/src/app/actions/slides.ts +++ b/admin/src/app/actions/slides.ts @@ -27,6 +27,8 @@ export async function createSlide(data: any) { linkUrl: data.linkUrl || null, order: data.order || 0, isActive: data.isActive ?? true, + coverPosition: data.coverPosition || "50% 50%", + coverZoom: data.coverZoom || 1, } }); revalidatePath("/slides"); @@ -50,6 +52,8 @@ export async function updateSlide(id: string, data: any) { linkUrl: data.linkUrl || null, order: data.order || 0, isActive: data.isActive ?? true, + coverPosition: data.coverPosition || "50% 50%", + coverZoom: data.coverZoom || 1, } }); revalidatePath("/slides"); diff --git a/admin/src/app/actions/upload.ts b/admin/src/app/actions/upload.ts index 838f798..c651cee 100644 --- a/admin/src/app/actions/upload.ts +++ b/admin/src/app/actions/upload.ts @@ -14,7 +14,9 @@ export async function uploadImage(formData: FormData) { const buffer = Buffer.from(bytes); // Create a unique filename - const fileExtension = file.name.split('.').pop(); + let fileExtension = file.name.split('.').pop()?.toLowerCase() || 'jpg'; + if (fileExtension === 'jpeg') fileExtension = 'jpg'; + const fileName = `${Date.now()}-${Math.random().toString(36).substring(2, 9)}.${fileExtension}`; // Path for the ROOT public/uploads (where the main site will look) diff --git a/admin/src/app/globals.css b/admin/src/app/globals.css index 2761b25..7951e51 100644 --- a/admin/src/app/globals.css +++ b/admin/src/app/globals.css @@ -299,3 +299,23 @@ body { .quill-dark-wrapper .ql-snow.ql-toolbar button.ql-divider:hover::before { background-color: var(--primary); } + +/* Styles pour les tableaux dans l'éditeur */ +.quill-dark-wrapper .ql-editor table { + width: 100%; + border-collapse: collapse; + margin: 1em 0; + border: 1px solid var(--border); +} + +.quill-dark-wrapper .ql-editor th, +.quill-dark-wrapper .ql-editor td { + border: 1px solid var(--border); + padding: 0.5rem; + text-align: left; +} + +.quill-dark-wrapper .ql-editor th { + background-color: var(--secondary); + font-weight: 600; +} diff --git a/admin/src/app/metrics/page.tsx b/admin/src/app/metrics/page.tsx index cc8f1e6..395f5ab 100644 --- a/admin/src/app/metrics/page.tsx +++ b/admin/src/app/metrics/page.tsx @@ -13,8 +13,11 @@ import { Search, ArrowUp, ArrowDown, - Calendar + Calendar, + Map as MapIcon } from 'lucide-react'; +import AnalyticsMap from '@/components/AnalyticsMap'; +import CountryHistogram from '@/components/CountryHistogram'; function ComparisonBadge({ value, isPercent = true }: { value: number | null, isPercent?: boolean }) { if (value === null) return null; @@ -76,12 +79,28 @@ async function getMetrics(range: string = 'week', from?: string, to?: string) { const topCountries = await prisma.analyticsEvent.groupBy({ by: ['country'], - where: currentWhere, + where: { type: 'PAGE_VIEW', ...currentWhere }, _count: { id: true }, orderBy: { _count: { id: 'desc' } }, - take: 10 + take: 50 // Increased to get more map data }); + // Unique visitors per country + const uniqueVisitorsPerCountryRaw = await prisma.analyticsEvent.groupBy({ + by: ['country', 'ip'], + where: { type: 'PAGE_VIEW', ...currentWhere } + }); + + const uniqueVisitorsPerCountryMap: Record = {}; + uniqueVisitorsPerCountryRaw.forEach(item => { + const country = item.country || 'Unknown'; + uniqueVisitorsPerCountryMap[country] = (uniqueVisitorsPerCountryMap[country] || 0) + 1; + }); + + const uniqueVisitorsPerCountry = Object.entries(uniqueVisitorsPerCountryMap) + .map(([country, count]) => ({ country, count })) + .sort((a, b) => b.count - a.count); + const devices = await prisma.analyticsEvent.groupBy({ by: ['device'], where: currentWhere, @@ -156,6 +175,7 @@ async function getMetrics(range: string = 'week', from?: string, to?: string) { totalEvents, uniqueVisitors, recentIPs, + uniqueVisitorsPerCountry, deltas: { events: prevTotalEvents > 0 ? ((totalEvents - prevTotalEvents) / prevTotalEvents) * 100 : null, visitors: prevUniqueVisitors > 0 ? ((uniqueVisitors - prevUniqueVisitors) / prevUniqueVisitors) * 100 : null, @@ -169,7 +189,7 @@ async function getMetrics(range: string = 'week', from?: string, to?: string) { export default async function MetricsPage({ searchParams: searchParamsPromise }: { searchParams: Promise<{ range?: string, from?: string, to?: string }> }) { const searchParams = await searchParamsPromise; const range = searchParams.range || 'week'; - const { topPages, topCountries, devices, browsers, totalEvents, uniqueVisitors, recentIPs, deltas } = await getMetrics(range, searchParams.from, searchParams.to); + const { topPages, topCountries, devices, browsers, totalEvents, uniqueVisitors, recentIPs, deltas, uniqueVisitorsPerCountry } = await getMetrics(range, searchParams.from, searchParams.to); const rangeLabels: Record = { day: 'Dernières 24h', @@ -237,13 +257,13 @@ export default async function MetricsPage({ searchParams: searchParamsPromise }:
-
{uniqueVisitors}
+
{uniqueVisitors.toLocaleString()}

Visiteurs uniques (IP)

- {searchParams.from ? `Du ${searchParams.from} au ${searchParams.to}` : rangeLabels[range]} + Personnes réelles distinctes

@@ -253,13 +273,13 @@ export default async function MetricsPage({ searchParams: searchParamsPromise }:
-
{totalEvents}
+
{totalEvents.toLocaleString()}
-

Événements enregistrés

+

Impressions (Pages vues)

- {searchParams.from ? `Du ${searchParams.from} au ${searchParams.to}` : rangeLabels[range]} + Nombre total de fois où les pages ont été vues

@@ -275,13 +295,29 @@ export default async function MetricsPage({ searchParams: searchParamsPromise }: -

Part du trafic Mobile

+

Trafic Mobile

- {searchParams.from ? `Du ${searchParams.from} au ${searchParams.to}` : rangeLabels[range]} + Part des appareils mobiles

+ {/* World Map Section */} + + + {/* Histogram Section */} + ({ + country: c.country || 'Inconnu', + impressions: c._count.id, + visitors: uniqueVisitorsPerCountry.find(uv => uv.country === c.country)?.count || 0 + }))} + /> +
{/* Top Countries Table */}
@@ -294,18 +330,28 @@ export default async function MetricsPage({ searchParams: searchParamsPromise }: Pays - Visites + Impressions + V. Uniques - {topCountries.map((c) => ( - - {c.country || 'Inconnu'} - - {c._count.id} - - - ))} + {topCountries.map((c) => { + const uniqueCount = uniqueVisitorsPerCountry.find(uv => uv.country === c.country)?.count || 0; + return ( + + + + {c.country || 'Inconnu'} + + + {c._count.id.toLocaleString()} + + + {uniqueCount.toLocaleString()} + + + ); + })}
diff --git a/admin/src/components/ActualitesForm.tsx b/admin/src/components/ActualitesForm.tsx index 1c686c3..18166ae 100644 --- a/admin/src/components/ActualitesForm.tsx +++ b/admin/src/components/ActualitesForm.tsx @@ -3,7 +3,7 @@ import { useTransition, useState, useEffect } from 'react'; import { useRouter } from 'next/navigation'; import { createBlogPost, updateBlogPost } from '@/app/actions/actualites'; -import { Loader2, ArrowLeft, Save, Calendar, CheckCircle2, FileText, Archive } from 'lucide-react'; +import { Loader2, ArrowLeft, Save, Calendar, CheckCircle2, FileText, Archive, Link as LinkIcon, Plus, Trash2 } from 'lucide-react'; import Link from 'next/link'; import { toast } from 'react-hot-toast'; import RichTextEditor from './RichTextEditor'; @@ -26,6 +26,9 @@ interface Props { metaDescription?: string | null; publishedAt?: Date | string | null; status?: ContentStatus; + coverPosition?: string | null; + coverZoom?: number | null; + sources?: any; }; } @@ -34,6 +37,8 @@ export default function BlogForm({ initialData }: Props) { const [isPending, startTransition] = useTransition(); const [content, setContent] = useState(initialData?.content || ''); const [imageUrl, setImageUrl] = useState(initialData?.imageUrl || ''); + const [coverPosition, setCoverPosition] = useState(initialData?.coverPosition || '50% 50%'); + const [coverZoom, setCoverZoom] = useState(initialData?.coverZoom || 1); const [tags, setTags] = useState(initialData?.tags || []); const [allExistingTags, setAllExistingTags] = useState([]); const [publishedAtValue, setPublishedAtValue] = useState( @@ -41,6 +46,9 @@ export default function BlogForm({ initialData }: Props) { ? new Date(initialData.publishedAt).toISOString().slice(0, 16) : new Date().toISOString().slice(0, 16) ); + const [sources, setSources] = useState<{ title: string; url: string }[]>( + Array.isArray(initialData?.sources) ? initialData.sources : [] + ); const isPublished = new Date(publishedAtValue) <= new Date(); @@ -65,6 +73,9 @@ export default function BlogForm({ initialData }: Props) { metaDescription: formData.get('metaDescription') as string, publishedAt: publishedAtStr ? new Date(publishedAtStr) : undefined, status: formData.get('status') as ContentStatus, + coverPosition: coverPosition, + coverZoom: coverZoom, + sources: sources, }; if (!data.imageUrl) { @@ -148,6 +159,11 @@ export default function BlogForm({ initialData }: Props) { value={imageUrl} onChange={setImageUrl} name="imageUrl" + showPositionControls={true} + position={coverPosition} + zoom={coverZoom} + onPositionChange={setCoverPosition} + onZoomChange={setCoverZoom} />
@@ -187,6 +203,61 @@ export default function BlogForm({ initialData }: Props) { placeholder="Rédigez votre article ici..." />
+ + {/* SOURCES SECTION */} +
+
+ + +
+ +
+ {sources.map((source, index) => ( +
+ { + const newSources = [...sources]; + newSources[index].title = e.target.value; + setSources(newSources); + }} + className="flex-1 bg-slate-900 border border-slate-700 rounded-lg p-2.5 text-sm text-white focus:outline-none focus:border-indigo-500" + /> + { + const newSources = [...sources]; + newSources[index].url = e.target.value; + setSources(newSources); + }} + className="flex-2 bg-slate-900 border border-slate-700 rounded-lg p-2.5 text-sm text-white focus:outline-none focus:border-indigo-500" + /> + +
+ ))} + {sources.length === 0 && ( +

Aucune source ajoutée pour cet article.

+ )} +
+
{/* SEO SECTION */} diff --git a/admin/src/components/AnalyticsMap.tsx b/admin/src/components/AnalyticsMap.tsx new file mode 100644 index 0000000..0459ea6 --- /dev/null +++ b/admin/src/components/AnalyticsMap.tsx @@ -0,0 +1,197 @@ +"use client"; + +import React, { useMemo } from "react"; +import { + ComposableMap, + Geographies, + Geography, + Sphere, + Graticule, + ZoomableGroup +} from "react-simple-maps"; +import { scaleLinear } from "d3-scale"; +import { ZoomIn, ZoomOut, RotateCcw } from "lucide-react"; + +// URL for the world map TopoJSON +const geoUrl = "https://cdn.jsdelivr.net/npm/world-atlas@2/countries-110m.json"; + +interface AnalyticsMapProps { + data: { country: string; count: number }[]; + title: string; +} + +// Simple mapping for common countries to ISO Alpha-3 codes used by world-atlas +// In a real app, this should be handled at the tracking level or via a robust library +const countryToISO: Record = { + "France": "FRA", + "United States": "USA", + "Canada": "CAN", + "United Kingdom": "GBR", + "Germany": "DEU", + "Ivory Coast": "CIV", + "Côte d'Ivoire": "CIV", + "Senegal": "SEN", + "Sénégal": "SEN", + "Cameroon": "CMR", + "Cameroun": "CMR", + "Mali": "MLI", + "Benin": "BEN", + "Bénin": "BEN", + "Togo": "TGO", + "Gabon": "GAB", + "Congo": "COG", + "DR Congo": "COD", + "Nigeria": "NGA", + "Ghana": "GHA", + "Morocco": "MAR", + "Maroc": "MAR", + "Algeria": "DZA", + "Algérie": "DZA", + "Tunisia": "TUN", + "Tunisie": "TUN", + "Belgium": "BEL", + "Belgique": "BEL", + "Switzerland": "CHE", + "Suisse": "CHE", + "Spain": "ESP", + "Espagne": "ESP", + "Italy": "ITA", + "Italie": "ITA", +}; + +export default function AnalyticsMap({ data, title }: AnalyticsMapProps) { + const maxCount = useMemo(() => { + if (data.length === 0) return 1; + return Math.max(...data.map((d) => d.count)); + }, [data]); + + const colorScale = scaleLinear() + .domain([0, maxCount]) + .range(["#1e293b", "#6366f1"]); + + const mappedData = useMemo(() => { + const map: Record = {}; + data.forEach((d) => { + const iso = countryToISO[d.country] || d.country; // Fallback to name if not in map + map[iso] = d.count; + }); + return map; + }, [data]); + + const [position, setPosition] = React.useState({ coordinates: [0, 0], zoom: 1 }); + + function handleZoomIn() { + if (position.zoom >= 4) return; + setPosition((pos) => ({ ...pos, zoom: pos.zoom * 1.5 })); + } + + function handleZoomOut() { + if (position.zoom <= 1) return; + setPosition((pos) => ({ ...pos, zoom: pos.zoom / 1.5 })); + } + + function handleReset() { + setPosition({ coordinates: [0, 0], zoom: 1 }); + } + + function handleMoveEnd(position: { coordinates: [number, number]; zoom: number }) { + setPosition(position); + } + + return ( +
+
+

+ {title} +

+ + {/* Zoom Controls */} +
+ + +
+ +
+
+ +
+ + + + + {data.length > 0 && ( + + {({ geographies }) => + geographies.map((geo) => { + const countryName = geo.properties.name; + const countryId = geo.id; + const countryISO = geo.properties.iso_a3; + + const count = mappedData[countryName] || mappedData[countryISO] || mappedData[countryId] || 0; + + return ( + 0 ? colorScale(count) : "#0f172a"} + stroke="#1e293b" + strokeWidth={0.5} + style={{ + default: { outline: "none" }, + hover: { fill: "#4f46e5", outline: "none", cursor: "pointer" }, + pressed: { outline: "none" } + }} + /> + ); + }) + } + + )} + + +
+ + {/* Legend / Instructions */} +
+
+ + Pas de trafic +
+
+ + Trafic identifié +
+
+
+ ); +} diff --git a/admin/src/components/CountryHistogram.tsx b/admin/src/components/CountryHistogram.tsx new file mode 100644 index 0000000..7c65681 --- /dev/null +++ b/admin/src/components/CountryHistogram.tsx @@ -0,0 +1,97 @@ +"use client"; + +import React from 'react'; +import { + BarChart, + Bar, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + Legend, + ResponsiveContainer, + Cell +} from 'recharts'; + +interface CountryHistogramProps { + data: { country: string; impressions: number; visitors: number }[]; + title: string; +} + +export default function CountryHistogram({ data, title }: CountryHistogramProps) { + // Take top 10 for readability + const chartData = data.slice(0, 10).map(item => ({ + name: item.country || 'Inconnu', + "Impressions": item.impressions, + "V. Uniques": item.visitors + })); + + return ( +
+

+ {title} +

+ +
+ + + + + value.toLocaleString()} + /> + + + + + + +
+
+ ); +} diff --git a/admin/src/components/EventForm.tsx b/admin/src/components/EventForm.tsx index c443028..2c19af2 100644 --- a/admin/src/components/EventForm.tsx +++ b/admin/src/components/EventForm.tsx @@ -21,6 +21,8 @@ export default function EventForm({ initialData }: Props) { const [isPending, startTransition] = useTransition(); const [description, setDescription] = useState(initialData?.description || ''); const [thumbnailUrl, setThumbnailUrl] = useState(initialData?.thumbnailUrl || ''); + const [coverPosition, setCoverPosition] = useState(initialData?.coverPosition || '50% 50%'); + const [coverZoom, setCoverZoom] = useState(initialData?.coverZoom || 1); const [tags, setTags] = useState(initialData?.tags || []); const [allExistingTags, setAllExistingTags] = useState([]); const [publishedAtValue, setPublishedAtValue] = useState( @@ -53,6 +55,8 @@ export default function EventForm({ initialData }: Props) { metaDescription: formData.get('metaDescription') as string, publishedAt: publishedAtStr ? new Date(publishedAtStr) : undefined, status: formData.get('status') as ContentStatus, + coverPosition: coverPosition, + coverZoom: coverZoom, }; if (!data.thumbnailUrl) { @@ -170,6 +174,11 @@ export default function EventForm({ initialData }: Props) { value={thumbnailUrl} onChange={setThumbnailUrl} name="thumbnailUrl" + showPositionControls={true} + position={coverPosition} + zoom={coverZoom} + onPositionChange={setCoverPosition} + onZoomChange={setCoverZoom} />
diff --git a/admin/src/components/ImageUploader.tsx b/admin/src/components/ImageUploader.tsx index bf8b42c..e74fa4b 100644 --- a/admin/src/components/ImageUploader.tsx +++ b/admin/src/components/ImageUploader.tsx @@ -11,9 +11,25 @@ interface Props { label?: string; placeholder?: string; name?: string; + showPositionControls?: boolean; + position?: string; + zoom?: number; + onPositionChange?: (value: string) => void; + onZoomChange?: (value: number) => void; } -export default function ImageUploader({ value, onChange, label, placeholder, name }: Props) { +export default function ImageUploader({ + value, + onChange, + label, + placeholder, + name, + showPositionControls = false, + position = "50% 50%", + zoom = 1, + onPositionChange, + onZoomChange +}: Props) { const [mode, setMode] = useState<'url' | 'upload'>(value?.startsWith('/uploads/') ? 'upload' : 'url'); const [isUploading, setIsUploading] = useState(false); const fileInputRef = useRef(null); @@ -23,8 +39,9 @@ export default function ImageUploader({ value, onChange, label, placeholder, nam if (!file) return; // Basic validation - if (!file.type.startsWith('image/')) { - toast.error("Le fichier doit être une image"); + const allowedTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp', 'image/pjpeg']; + if (!allowedTypes.includes(file.type) && !file.type.startsWith('image/')) { + toast.error("Le fichier doit être une image (JPG, PNG ou WEBP)"); return; } @@ -120,37 +137,111 @@ export default function ImageUploader({ value, onChange, label, placeholder, nam
) : ( -
- Prévisualisation -
- - +
+
+ Prévisualisation +
+ + +
+ {/* Hidden inputs for values to be sent via form if needed */} + + {showPositionControls && ( + <> + + + + )}
- {/* Hidden input for value to be sent via form if needed */} - + + {showPositionControls && ( +
+
+
+
+ + +
+ onZoomChange?.(parseFloat(e.target.value))} + className="w-full h-1.5 bg-slate-800 rounded-lg appearance-none cursor-pointer accent-indigo-500" + /> +
+
+ +
+
+ + { + const y = position.split(' ')[1]; + onPositionChange?.(`${e.target.value}% ${y}`); + }} + className="w-full h-1.5 bg-slate-800 rounded-lg appearance-none cursor-pointer accent-indigo-500" + /> +
+
+ + { + const x = position.split(' ')[0]; + onPositionChange?.(`${x} ${e.target.value}%`); + }} + className="w-full h-1.5 bg-slate-800 rounded-lg appearance-none cursor-pointer accent-indigo-500" + /> +
+
+
+ )}
)}
diff --git a/admin/src/components/RichTextEditor.tsx b/admin/src/components/RichTextEditor.tsx index 6cdc980..96287bd 100644 --- a/admin/src/components/RichTextEditor.tsx +++ b/admin/src/components/RichTextEditor.tsx @@ -34,12 +34,13 @@ export default function RichTextEditor({ value, onChange, placeholder }: RichTex const quillRef = useRef(null); const modules = useMemo(() => ({ + table: true, toolbar: { container: [ [{ 'header': [1, 2, 3, 4, 5, 6, false] }], ['bold', 'italic', 'underline', 'strike'], [{ 'list': 'ordered' }, { 'list': 'bullet' }], - ['divider', 'link', 'clean'], + ['table', 'divider', 'link', 'clean'], ], handlers: { divider: function() { @@ -58,7 +59,7 @@ export default function RichTextEditor({ value, onChange, placeholder }: RichTex 'header', 'bold', 'italic', 'underline', 'strike', 'list', - 'divider', 'link', + 'divider', 'link', 'table' ]; return ( diff --git a/admin/src/components/SlideForm.tsx b/admin/src/components/SlideForm.tsx index 1d58c29..76375d1 100644 --- a/admin/src/components/SlideForm.tsx +++ b/admin/src/components/SlideForm.tsx @@ -19,6 +19,8 @@ export default function SlideForm({ initialData }: Props) { // Form state const [type, setType] = useState(initialData?.type || 'CUSTOM'); const [imageUrl, setImageUrl] = useState(initialData?.imageUrl || ''); + const [coverPosition, setCoverPosition] = useState(initialData?.coverPosition || '50% 50%'); + const [coverZoom, setCoverZoom] = useState(initialData?.coverZoom || 1); const [isActive, setIsActive] = useState(initialData?.isActive ?? true); // Business search state @@ -55,6 +57,8 @@ export default function SlideForm({ initialData }: Props) { linkUrl: type === 'CUSTOM' ? formData.get('linkUrl') as string : `/annuaire/${selectedBusiness?.id}`, order: parseInt(formData.get('order') as string) || 0, isActive, + coverPosition: coverPosition, + coverZoom: coverZoom, }; if (type === 'BUSINESS' && !data.businessId) { @@ -232,6 +236,11 @@ export default function SlideForm({ initialData }: Props) { value={imageUrl} onChange={setImageUrl} name="imageUrl" + showPositionControls={true} + position={coverPosition} + zoom={coverZoom} + onPositionChange={setCoverPosition} + onZoomChange={setCoverZoom} />
)} diff --git a/app/actions/events.ts b/app/actions/events.ts index 9d323c9..92c8e57 100644 --- a/app/actions/events.ts +++ b/app/actions/events.ts @@ -28,7 +28,7 @@ export async function createEvent(data: { thumbnailUrl: data.thumbnailUrl, link: data.link || null, tags: data.tags || [], - status: ContentStatus.PENDING, // Always pending for user submission + status: 'PENDING' as ContentStatus, // Always pending for user submission slug: `${slug}-${Math.random().toString(36).substring(2, 7)}` } }); diff --git a/app/actions/news.ts b/app/actions/news.ts index 3ee2038..3798185 100644 --- a/app/actions/news.ts +++ b/app/actions/news.ts @@ -11,6 +11,7 @@ export async function createNews(data: { author: string; imageUrl: string; tags?: string[]; + sources?: any; }) { try { const slug = data.title @@ -26,12 +27,13 @@ export async function createNews(data: { author: data.author, imageUrl: data.imageUrl, tags: data.tags || [], - status: ContentStatus.PENDING, // Always pending for user submission + sources: data.sources || [], + status: 'PENDING' as ContentStatus, // Always pending for user submission slug: `${slug}-${Math.random().toString(36).substring(2, 7)}` } }); - revalidatePath("/blog"); + revalidatePath("/actualites"); return { success: true, post }; } catch (error) { console.error("Failed to create news:", error); diff --git a/app/actions/upload.ts b/app/actions/upload.ts index edd7ed1..a3960c9 100644 --- a/app/actions/upload.ts +++ b/app/actions/upload.ts @@ -13,7 +13,13 @@ export async function uploadImage(formData: FormData) { // Convert to Base64 const base64 = buffer.toString('base64'); - const mimeType = file.type || 'image/jpeg'; + let mimeType = file.type || 'image/jpeg'; + + // Normalize JPEG mime types + if (mimeType === 'image/jpg' || mimeType === 'image/pjpeg') { + mimeType = 'image/jpeg'; + } + const dataUrl = `data:${mimeType};base64,${base64}`; // Return the data URL directly (it will be saved in the database) diff --git a/app/actualites/[id]/page.tsx b/app/actualites/[id]/page.tsx index eded85c..f9a51bc 100644 --- a/app/actualites/[id]/page.tsx +++ b/app/actualites/[id]/page.tsx @@ -5,6 +5,7 @@ import { ArrowLeft, ArrowRight, User, Calendar, Share2 } from 'lucide-react'; import { sanitizeHTML } from '../../../lib/sanitize'; import { prisma } from '../../../lib/prisma'; import BlogShareButtons from '../../../components/BlogShareButtons'; +import LightboxImage from '@/components/LightboxImage'; import { Metadata } from 'next'; interface Props { @@ -117,11 +118,12 @@ export default async function BlogPostPage({ params }: Props) {
{/* Image d'en-tête */} -
- {post.title} +
@@ -160,7 +162,7 @@ export default async function BlogPostPage({ params }: Props) { {post.excerpt}

- {/* Corps du texte avec ID pour le contrôle CSS */} + {/* Corps du texte avec ID pour le contrôle CSS */}
+ {/* Sources Section */} + {Array.isArray(post.sources) && post.sources.length > 0 && ( +
+

+ + Sources & Références +

+ +
+ )} + {/* Partage / Footer */}

Vous avez aimé cet article ?

diff --git a/app/actualites/page.tsx b/app/actualites/page.tsx index 2eed1b7..a95cf6a 100644 --- a/app/actualites/page.tsx +++ b/app/actualites/page.tsx @@ -42,7 +42,16 @@ export default async function BlogPage() { {posts.map(post => (
- {post.title} + {post.title}
diff --git a/app/afrolife/[id]/page.tsx b/app/afrolife/[id]/page.tsx index e348bef..3522c88 100644 --- a/app/afrolife/[id]/page.tsx +++ b/app/afrolife/[id]/page.tsx @@ -5,6 +5,7 @@ import { ArrowLeft, Share2, Play, Calendar, User, Building2, MapPin, ArrowRight import { sanitizeHTML } from '../../../lib/sanitize'; import { prisma } from '../../../lib/prisma'; import { InterviewType } from '@prisma/client'; +import LightboxImage from '@/components/LightboxImage'; import { Metadata } from 'next'; @@ -161,8 +162,12 @@ export default async function AfroLifeDetailPage({ params }: Props) {
) : (
- {item!.title} -
+
)} diff --git a/app/afrolife/page.tsx b/app/afrolife/page.tsx index d9392d9..f5509c9 100644 --- a/app/afrolife/page.tsx +++ b/app/afrolife/page.tsx @@ -149,7 +149,12 @@ export default async function AfroLifePage({ searchParams }: Props) { {item.title}
diff --git a/app/globals.css b/app/globals.css index 5ba484e..7924e27 100644 --- a/app/globals.css +++ b/app/globals.css @@ -57,4 +57,34 @@ h4 { .prose p { margin-bottom: 1.25em; text-align: left; +} + +/* Styles pour les tableaux dans le contenu */ +#blog-content table, +.prose table { + width: 100%; + border-collapse: collapse; + margin: 1.5em 0; + font-size: 0.9em; + border: 1px solid #e2e8f0; +} + +#blog-content th, +#blog-content td, +.prose th, +.prose td { + border: 1px solid #e2e8f0; + padding: 0.75rem; + text-align: left; +} + +#blog-content th, +.prose th { + background-color: #f8fafc; + font-weight: 600; +} + +#blog-content tr:nth-child(even), +.prose tr:nth-child(even) { + background-color: #fcfcfc; } \ No newline at end of file diff --git a/components/HomeSlider.tsx b/components/HomeSlider.tsx index faf2e6a..bbd0701 100644 --- a/components/HomeSlider.tsx +++ b/components/HomeSlider.tsx @@ -13,6 +13,8 @@ interface HomeSlide { subtitle?: string | null; imageUrl?: string | null; linkUrl?: string | null; + coverPosition?: string | null; + coverZoom?: number | null; } interface Props { @@ -85,10 +87,15 @@ const HomeSlider = ({ slides, onSearch }: Props) => {
diff --git a/components/LightboxImage.tsx b/components/LightboxImage.tsx new file mode 100644 index 0000000..ff4d5da --- /dev/null +++ b/components/LightboxImage.tsx @@ -0,0 +1,99 @@ +'use client'; + +import React, { useState, useEffect } from 'react'; +import { X, Maximize2 } from 'lucide-react'; + +interface LightboxImageProps { + src: string; + alt: string; + position?: string; + zoom?: number; + className?: string; +} + +export default function LightboxImage({ + src, + alt, + position = "50% 50%", + zoom = 1, + className = "w-full h-full object-cover" +}: LightboxImageProps) { + const [isOpen, setIsOpen] = useState(false); + + // Close on Escape key + useEffect(() => { + const handleEsc = (e: KeyboardEvent) => { + if (e.key === 'Escape') setIsOpen(false); + }; + window.addEventListener('keydown', handleEsc); + return () => window.removeEventListener('keydown', handleEsc); + }, []); + + // Prevent scroll when open + useEffect(() => { + if (isOpen) { + document.body.style.overflow = 'hidden'; + } else { + document.body.style.overflow = 'unset'; + } + return () => { document.body.style.overflow = 'unset'; }; + }, [isOpen]); + + return ( + <> +
setIsOpen(true)} + > + {alt} +
+
+ +
+
+
+ + {/* Lightbox Overlay */} + {isOpen && ( +
setIsOpen(false)} + > + + +
+ {alt} + + {/* Legend / Caption */} +
+

{alt}

+
+
+
+ )} + + ); +} diff --git a/components/dashboard/CreateEventModal.tsx b/components/dashboard/CreateEventModal.tsx index 8c73bb0..5733427 100644 --- a/components/dashboard/CreateEventModal.tsx +++ b/components/dashboard/CreateEventModal.tsx @@ -171,10 +171,10 @@ export default function CreateEventModal({ isOpen, onClose }: CreateEventModalPr )}

Cliquez pour ajouter une image

-

PNG, JPG jusqu'à 2Mo

+

PNG, JPG, JPEG jusqu'à 5Mo

diff --git a/components/dashboard/CreateNewsModal.tsx b/components/dashboard/CreateNewsModal.tsx index d1eef9b..ea4b4bb 100644 --- a/components/dashboard/CreateNewsModal.tsx +++ b/components/dashboard/CreateNewsModal.tsx @@ -170,10 +170,10 @@ export default function CreateNewsModal({ isOpen, onClose }: CreateNewsModalProp )}

Cliquez pour ajouter une image

-

PNG, JPG jusqu'à 2Mo

+

PNG, JPG, JPEG jusqu'à 5Mo

diff --git a/prisma/schema.prisma b/prisma/schema.prisma index abb5f64..9c40989 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -160,6 +160,9 @@ model BlogPost { tags String[] @default([]) publishedAt DateTime @default(now()) status ContentStatus @default(PUBLISHED) + coverPosition String? @default("50% 50%") + coverZoom Float? @default(1.0) + sources Json? @default("[]") } model HomeSlide { @@ -174,6 +177,8 @@ model HomeSlide { isActive Boolean @default(true) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt + coverPosition String? @default("50% 50%") + coverZoom Float? @default(1.0) business Business? @relation(fields: [businessId], references: [id]) } @@ -198,6 +203,8 @@ model Interview { tags String[] @default([]) publishedAt DateTime @default(now()) status ContentStatus @default(PUBLISHED) + coverPosition String? @default("50% 50%") + coverZoom Float? @default(1.0) } model Comment { @@ -358,6 +365,8 @@ model Event { tags String[] @default([]) publishedAt DateTime @default(now()) status ContentStatus @default(PUBLISHED) + coverPosition String? @default("50% 50%") + coverZoom Float? @default(1.0) } model Favorite { diff --git a/scratch/debug-enum.ts b/scratch/debug-enum.ts new file mode 100644 index 0000000..d26d194 --- /dev/null +++ b/scratch/debug-enum.ts @@ -0,0 +1,4 @@ +import { ContentStatus } from '@prisma/client'; + +console.log('Available ContentStatus:', Object.values(ContentStatus)); +console.log('PENDING value:', ContentStatus.PENDING); diff --git a/scratch/debug-prisma.ts b/scratch/debug-prisma.ts new file mode 100644 index 0000000..18c075d --- /dev/null +++ b/scratch/debug-prisma.ts @@ -0,0 +1,3 @@ +import { prisma } from './lib/prisma'; + +console.log('Prisma models:', Object.keys(prisma).filter(k => !k.startsWith('_') && !k.startsWith('$')));