resolve conflict

This commit is contained in:
p2405951 committed 2025-12-10 14:59:58 +01:00
commit f2492c253c
84 files changed
+2825 -972

No files matched your search

+1
View File
@@ -0,0 +1 @@
VITE_API_URL=
+2
View File
@@ -25,3 +25,5 @@ dist-ssr
*storybook.log *storybook.log
storybook-static storybook-static
.env
+21 -739
View File
@@ -18,7 +18,8 @@
"react-bootstrap": "^2.10.10", "react-bootstrap": "^2.10.10",
"react-chartjs-2": "^5.3.1", "react-chartjs-2": "^5.3.1",
"react-dom": "^19.1.1", "react-dom": "^19.1.1",
"react-router": "^7.9.4" "react-router": "^7.9.4",
"react-router-dom": "^7.10.1"
}, },
"devDependencies": { "devDependencies": {
"@chromatic-com/storybook": "^4.1.2", "@chromatic-com/storybook": "^4.1.2",
@@ -524,278 +525,6 @@
"integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==", "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@esbuild/aix-ppc64": {
"version": "0.25.10",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.10.tgz",
"integrity": "sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"aix"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.25.10",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.10.tgz",
"integrity": "sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.25.10",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.10.tgz",
"integrity": "sha512-LSQa7eDahypv/VO6WKohZGPSJDq5OVOo3UoFR1E4t4Gj1W7zEQMUhI+lo81H+DtB+kP+tDgBp+M4oNCwp6kffg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.25.10",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.10.tgz",
"integrity": "sha512-MiC9CWdPrfhibcXwr39p9ha1x0lZJ9KaVfvzA0Wxwz9ETX4v5CHfF09bx935nHlhi+MxhA63dKRRQLiVgSUtEg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.25.10",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.10.tgz",
"integrity": "sha512-JC74bdXcQEpW9KkV326WpZZjLguSZ3DfS8wrrvPMHgQOIEIG/sPXEN/V8IssoJhbefLRcRqw6RQH2NnpdprtMA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.25.10",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.10.tgz",
"integrity": "sha512-tguWg1olF6DGqzws97pKZ8G2L7Ig1vjDmGTwcTuYHbuU6TTjJe5FXbgs5C1BBzHbJ2bo1m3WkQDbWO2PvamRcg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.25.10",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.10.tgz",
"integrity": "sha512-3ZioSQSg1HT2N05YxeJWYR+Libe3bREVSdWhEEgExWaDtyFbbXWb49QgPvFH8u03vUPX10JhJPcz7s9t9+boWg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.25.10",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.10.tgz",
"integrity": "sha512-LLgJfHJk014Aa4anGDbh8bmI5Lk+QidDmGzuC2D+vP7mv/GeSN+H39zOf7pN5N8p059FcOfs2bVlrRr4SK9WxA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.25.10",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.10.tgz",
"integrity": "sha512-oR31GtBTFYCqEBALI9r6WxoU/ZofZl962pouZRTEYECvNF/dtXKku8YXcJkhgK/beU+zedXfIzHijSRapJY3vg==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.25.10",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.10.tgz",
"integrity": "sha512-5luJWN6YKBsawd5f9i4+c+geYiVEw20FVW5x0v1kEMWNq8UctFjDiMATBxLvmmHA4bf7F6hTRaJgtghFr9iziQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.25.10",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.10.tgz",
"integrity": "sha512-NrSCx2Kim3EnnWgS4Txn0QGt0Xipoumb6z6sUtl5bOEZIVKhzfyp/Lyw4C1DIYvzeW/5mWYPBFJU3a/8Yr75DQ==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.25.10",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.10.tgz",
"integrity": "sha512-xoSphrd4AZda8+rUDDfD9J6FUMjrkTz8itpTITM4/xgerAZZcFW7Dv+sun7333IfKxGG8gAq+3NbfEMJfiY+Eg==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.25.10",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.10.tgz",
"integrity": "sha512-ab6eiuCwoMmYDyTnyptoKkVS3k8fy/1Uvq7Dj5czXI6DF2GqD2ToInBI0SHOp5/X1BdZ26RKc5+qjQNGRBelRA==",
"cpu": [
"mips64el"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.25.10",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.10.tgz",
"integrity": "sha512-NLinzzOgZQsGpsTkEbdJTCanwA5/wozN9dSgEl12haXJBzMTpssebuXR42bthOF3z7zXFWH1AmvWunUCkBE4EA==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.25.10",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.10.tgz",
"integrity": "sha512-FE557XdZDrtX8NMIeA8LBJX3dC2M8VGXwfrQWU7LB5SLOajfJIxmSdyL/gU1m64Zs9CBKvm4UAuBp5aJ8OgnrA==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.25.10",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.10.tgz",
"integrity": "sha512-3BBSbgzuB9ajLoVZk0mGu+EHlBwkusRmeNYdqmznmMc9zGASFjSsxgkNsqmXugpPk00gJ0JNKh/97nxmjctdew==",
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-x64": { "node_modules/@esbuild/linux-x64": {
"version": "0.25.10", "version": "0.25.10",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.10.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.10.tgz",
@@ -813,159 +542,6 @@
"node": ">=18" "node": ">=18"
} }
}, },
"node_modules/@esbuild/netbsd-arm64": {
"version": "0.25.10",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.10.tgz",
"integrity": "sha512-AKQM3gfYfSW8XRk8DdMCzaLUFB15dTrZfnX8WXQoOUpUBQ+NaAFCP1kPS/ykbbGYz7rxn0WS48/81l9hFl3u4A==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.25.10",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.10.tgz",
"integrity": "sha512-7RTytDPGU6fek/hWuN9qQpeGPBZFfB4zZgcz2VK2Z5VpdUxEI8JKYsg3JfO0n/Z1E/6l05n0unDCNc4HnhQGig==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-arm64": {
"version": "0.25.10",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.10.tgz",
"integrity": "sha512-5Se0VM9Wtq797YFn+dLimf2Zx6McttsH2olUBsDml+lm0GOCRVebRWUvDtkY4BWYv/3NgzS8b/UM3jQNh5hYyw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.25.10",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.10.tgz",
"integrity": "sha512-XkA4frq1TLj4bEMB+2HnI0+4RnjbuGZfet2gs/LNs5Hc7D89ZQBHQ0gL2ND6Lzu1+QVkjp3x1gIcPKzRNP8bXw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openharmony-arm64": {
"version": "0.25.10",
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.10.tgz",
"integrity": "sha512-AVTSBhTX8Y/Fz6OmIVBip9tJzZEUcY8WLh7I59+upa5/GPhh2/aM6bvOMQySspnCCHvFi79kMtdJS1w0DXAeag==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openharmony"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.25.10",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.10.tgz",
"integrity": "sha512-fswk3XT0Uf2pGJmOpDB7yknqhVkJQkAQOcW/ccVOtfx05LkbWOaRAtn5SaqXypeKQra1QaEa841PgrSL9ubSPQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.25.10",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.10.tgz",
"integrity": "sha512-ah+9b59KDTSfpaCg6VdJoOQvKjI33nTaQr4UluQwW7aEwZQsbMCfTmfEO4VyewOxx4RaDT/xCy9ra2GPWmO7Kw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.25.10",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.10.tgz",
"integrity": "sha512-QHPDbKkrGO8/cz9LKVnJU22HOi4pxZnZhhA2HYHez5Pz4JeffhDjf85E57Oyco163GnzNCVkZK0b/n4Y0UHcSw==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.25.10",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.10.tgz",
"integrity": "sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@eslint-community/eslint-utils": { "node_modules/@eslint-community/eslint-utils": {
"version": "4.9.0", "version": "4.9.0",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz",
@@ -1728,216 +1304,6 @@
} }
} }
}, },
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.52.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.4.tgz",
"integrity": "sha512-BTm2qKNnWIQ5auf4deoetINJm2JzvihvGb9R6K/ETwKLql/Bb3Eg2H1FBp1gUb4YGbydMA3jcmQTR73q7J+GAA==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
]
},
"node_modules/@rollup/rollup-android-arm64": {
"version": "4.52.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.4.tgz",
"integrity": "sha512-P9LDQiC5vpgGFgz7GSM6dKPCiqR3XYN1WwJKA4/BUVDjHpYsf3iBEmVz62uyq20NGYbiGPR5cNHI7T1HqxNs2w==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.52.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.4.tgz",
"integrity": "sha512-QRWSW+bVccAvZF6cbNZBJwAehmvG9NwfWHwMy4GbWi/BQIA/laTIktebT2ipVjNncqE6GLPxOok5hsECgAxGZg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@rollup/rollup-darwin-x64": {
"version": "4.52.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.4.tgz",
"integrity": "sha512-hZgP05pResAkRJxL1b+7yxCnXPGsXU0fG9Yfd6dUaoGk+FhdPKCJ5L1Sumyxn8kvw8Qi5PvQ8ulenUbRjzeCTw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@rollup/rollup-freebsd-arm64": {
"version": "4.52.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.4.tgz",
"integrity": "sha512-xmc30VshuBNUd58Xk4TKAEcRZHaXlV+tCxIXELiE9sQuK3kG8ZFgSPi57UBJt8/ogfhAF5Oz4ZSUBN77weM+mQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
]
},
"node_modules/@rollup/rollup-freebsd-x64": {
"version": "4.52.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.4.tgz",
"integrity": "sha512-WdSLpZFjOEqNZGmHflxyifolwAiZmDQzuOzIq9L27ButpCVpD7KzTRtEG1I0wMPFyiyUdOO+4t8GvrnBLQSwpw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.52.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.4.tgz",
"integrity": "sha512-xRiOu9Of1FZ4SxVbB0iEDXc4ddIcjCv2aj03dmW8UrZIW7aIQ9jVJdLBIhxBI+MaTnGAKyvMwPwQnoOEvP7FgQ==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.52.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.4.tgz",
"integrity": "sha512-FbhM2p9TJAmEIEhIgzR4soUcsW49e9veAQCziwbR+XWB2zqJ12b4i/+hel9yLiD8pLncDH4fKIPIbt5238341Q==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.52.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.4.tgz",
"integrity": "sha512-4n4gVwhPHR9q/g8lKCyz0yuaD0MvDf7dV4f9tHt0C73Mp8h38UCtSCSE6R9iBlTbXlmA8CjpsZoujhszefqueg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.52.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.4.tgz",
"integrity": "sha512-u0n17nGA0nvi/11gcZKsjkLj1QIpAuPFQbR48Subo7SmZJnGxDpspyw2kbpuoQnyK+9pwf3pAoEXerJs/8Mi9g==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-loong64-gnu": {
"version": "4.52.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.4.tgz",
"integrity": "sha512-0G2c2lpYtbTuXo8KEJkDkClE/+/2AFPdPAbmaHoE870foRFs4pBrDehilMcrSScrN/fB/1HTaWO4bqw+ewBzMQ==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
"version": "4.52.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.4.tgz",
"integrity": "sha512-teSACug1GyZHmPDv14VNbvZFX779UqWTsd7KtTM9JIZRDI5NUwYSIS30kzI8m06gOPB//jtpqlhmraQ68b5X2g==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.52.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.4.tgz",
"integrity": "sha512-/MOEW3aHjjs1p4Pw1Xk4+3egRevx8Ji9N6HUIA1Ifh8Q+cg9dremvFCUbOX2Zebz80BwJIgCBUemjqhU5XI5Eg==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-riscv64-musl": {
"version": "4.52.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.4.tgz",
"integrity": "sha512-1HHmsRyh845QDpEWzOFtMCph5Ts+9+yllCrREuBR/vg2RogAQGGBRC8lDPrPOMnrdOJ+mt1WLMOC2Kao/UwcvA==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.52.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.4.tgz",
"integrity": "sha512-seoeZp4L/6D1MUyjWkOMRU6/iLmCU2EjbMTyAG4oIOs1/I82Y5lTeaxW0KBfkUdHAWN7j25bpkt0rjnOgAcQcA==",
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": { "node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.52.4", "version": "4.52.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.4.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.4.tgz",
@@ -1966,76 +1332,6 @@
"linux" "linux"
] ]
}, },
"node_modules/@rollup/rollup-openharmony-arm64": {
"version": "4.52.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.4.tgz",
"integrity": "sha512-1ox+GqgRWqaB1RnyZXL8PD6E5f7YyRUJYnCqKpNzxzP0TkaUh112NDrR9Tt+C8rJ4x5G9Mk8PQR3o7Ku2RKqKA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openharmony"
]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.52.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.4.tgz",
"integrity": "sha512-8GKr640PdFNXwzIE0IrkMWUNUomILLkfeHjXBi/nUvFlpZP+FA8BKGKpacjW6OUUHaNI6sUURxR2U2g78FOHWQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.52.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.4.tgz",
"integrity": "sha512-AIy/jdJ7WtJ/F6EcfOb2GjR9UweO0n43jNObQMb6oGxkYTfLcnN7vYYpG+CN3lLxrQkzWnMOoNSHTW54pgbVxw==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@rollup/rollup-win32-x64-gnu": {
"version": "4.52.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.4.tgz",
"integrity": "sha512-UF9KfsH9yEam0UjTwAgdK0anlQ7c8/pWPU2yVjyWcF1I1thABt6WXE47cI71pGiZ8wGvxohBoLnxM04L/wj8mQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.52.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.4.tgz",
"integrity": "sha512-bf9PtUa0u8IXDVxzRToFQKsNCRz9qLYfR/MpECxl4mRoWYjAeFjgxj1XdZr2M/GNVpT05p+LgQOHopYDlUu6/w==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@standard-schema/spec": { "node_modules/@standard-schema/spec": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz",
@@ -4001,21 +3297,6 @@
"url": "https://github.com/sponsors/isaacs" "url": "https://github.com/sponsors/isaacs"
} }
}, },
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/function-bind": { "node_modules/function-bind": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
@@ -4953,21 +4234,6 @@
"node": ">=18" "node": ">=18"
} }
}, },
"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/pngjs": { "node_modules/pngjs": {
"version": "7.0.0", "version": "7.0.0",
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz", "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz",
@@ -5241,9 +4507,9 @@
} }
}, },
"node_modules/react-router": { "node_modules/react-router": {
"version": "7.9.4", "version": "7.10.1",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.9.4.tgz", "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.10.1.tgz",
"integrity": "sha512-SD3G8HKviFHg9xj7dNODUKDFgpG4xqD5nhyd0mYoB5iISepuZAvzSr8ywxgxKJ52yRzf/HWtVHc9AWwoTbljvA==", "integrity": "sha512-gHL89dRa3kwlUYtRQ+m8NmxGI6CgqN+k4XyGjwcFoQwwCWF6xXpOCUlDovkXClS0d0XJN/5q7kc5W3kiFEd0Yw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"cookie": "^1.0.1", "cookie": "^1.0.1",
@@ -5262,6 +4528,22 @@
} }
} }
}, },
"node_modules/react-router-dom": {
"version": "7.10.1",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.10.1.tgz",
"integrity": "sha512-JNBANI6ChGVjA5bwsUIwJk7LHKmqB4JYnYfzFwyp2t12Izva11elds2jx7Yfoup2zssedntwU0oZ5DEmk5Sdaw==",
"license": "MIT",
"dependencies": {
"react-router": "7.10.1"
},
"engines": {
"node": ">=20.0.0"
},
"peerDependencies": {
"react": ">=18",
"react-dom": ">=18"
}
},
"node_modules/react-transition-group": { "node_modules/react-transition-group": {
"version": "4.4.5", "version": "4.4.5",
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
+4 -2
View File
@@ -9,7 +9,8 @@
"lint": "eslint .", "lint": "eslint .",
"preview": "vite preview", "preview": "vite preview",
"storybook": "storybook dev -p 6006", "storybook": "storybook dev -p 6006",
"build-storybook": "storybook build" "build-storybook": "storybook build",
"test": "vitest"
}, },
"dependencies": { "dependencies": {
"@emotion/react": "^11.14.0", "@emotion/react": "^11.14.0",
@@ -22,7 +23,8 @@
"react-bootstrap": "^2.10.10", "react-bootstrap": "^2.10.10",
"react-chartjs-2": "^5.3.1", "react-chartjs-2": "^5.3.1",
"react-dom": "^19.1.1", "react-dom": "^19.1.1",
"react-router": "^7.9.4" "react-router": "^7.9.4",
"react-router-dom": "^7.10.1"
}, },
"devDependencies": { "devDependencies": {
"@chromatic-com/storybook": "^4.1.2", "@chromatic-com/storybook": "^4.1.2",
+30
View File
@@ -8,9 +8,15 @@ importers:
.: .:
dependencies: dependencies:
chart.js:
specifier: ^4.5.1
version: 4.5.1
react: react:
specifier: ^19.1.1 specifier: ^19.1.1
version: 19.2.0 version: 19.2.0
react-chartjs-2:
specifier: ^5.3.1
version: 5.3.1(chart.js@4.5.1)(react@19.2.0)
react-dom: react-dom:
specifier: ^19.1.1 specifier: ^19.1.1
version: 19.2.0(react@19.2.0) version: 19.2.0(react@19.2.0)
@@ -436,6 +442,9 @@ packages:
'@jridgewell/trace-mapping@0.3.31': '@jridgewell/trace-mapping@0.3.31':
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
'@kurkle/color@0.3.4':
resolution: {integrity: sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==}
'@mdx-js/react@3.1.1': '@mdx-js/react@3.1.1':
resolution: {integrity: sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw==} resolution: {integrity: sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw==}
peerDependencies: peerDependencies:
@@ -949,6 +958,10 @@ packages:
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
engines: {node: '>=10'} engines: {node: '>=10'}
chart.js@4.5.1:
resolution: {integrity: sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==}
engines: {pnpm: '>=8'}
check-error@2.1.1: check-error@2.1.1:
resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==}
engines: {node: '>= 16'} engines: {node: '>= 16'}
@@ -1510,6 +1523,12 @@ packages:
queue-microtask@1.2.3: queue-microtask@1.2.3:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
react-chartjs-2@5.3.1:
resolution: {integrity: sha512-h5IPXKg9EXpjoBzUfyWJvllMjG2mQ4EiuHQFhms/AjUm0XSZHhyRy2xVmLXHKrtcdrPO4mnGqRtYoD0vp95A0A==}
peerDependencies:
chart.js: ^4.1.1
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
react-docgen-typescript@2.4.0: react-docgen-typescript@2.4.0:
resolution: {integrity: sha512-ZtAp5XTO5HRzQctjPU0ybY0RRCQO19X/8fxn3w7y2VVTUbGHDKULPTL4ky3vB05euSgG5NpALhEhDPvQ56wvXg==} resolution: {integrity: sha512-ZtAp5XTO5HRzQctjPU0ybY0RRCQO19X/8fxn3w7y2VVTUbGHDKULPTL4ky3vB05euSgG5NpALhEhDPvQ56wvXg==}
peerDependencies: peerDependencies:
@@ -2186,6 +2205,8 @@ snapshots:
'@jridgewell/resolve-uri': 3.1.2 '@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.5 '@jridgewell/sourcemap-codec': 1.5.5
'@kurkle/color@0.3.4': {}
'@mdx-js/react@3.1.1(@types/react@19.2.2)(react@19.2.0)': '@mdx-js/react@3.1.1(@types/react@19.2.2)(react@19.2.0)':
dependencies: dependencies:
'@types/mdx': 2.0.13 '@types/mdx': 2.0.13
@@ -2738,6 +2759,10 @@ snapshots:
ansi-styles: 4.3.0 ansi-styles: 4.3.0
supports-color: 7.2.0 supports-color: 7.2.0
chart.js@4.5.1:
dependencies:
'@kurkle/color': 0.3.4
check-error@2.1.1: {} check-error@2.1.1: {}
chromatic@12.2.0: {} chromatic@12.2.0: {}
@@ -3255,6 +3280,11 @@ snapshots:
queue-microtask@1.2.3: {} queue-microtask@1.2.3: {}
react-chartjs-2@5.3.1(chart.js@4.5.1)(react@19.2.0):
dependencies:
chart.js: 4.5.1
react: 19.2.0
react-docgen-typescript@2.4.0(typescript@5.9.3): react-docgen-typescript@2.4.0(typescript@5.9.3):
dependencies: dependencies:
typescript: 5.9.3 typescript: 5.9.3
File renamed without changes.

Before

Width:  |  Height:  |  Size: 605 B

After

Width:  |  Height:  |  Size: 605 B

File renamed without changes.

Before

Width:  |  Height:  |  Size: 222 B

After

Width:  |  Height:  |  Size: 222 B

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

+4
View File
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11 4H7.2C6.0799 4 5.51984 4 5.09202 4.21799C4.71569 4.40974 4.40973 4.7157 4.21799 5.09202C4 5.51985 4 6.0799 4 7.2V16.8C4 17.9201 4 18.4802 4.21799 18.908C4.40973 19.2843 4.71569 19.5903 5.09202 19.782C5.51984 20 6.0799 20 7.2 20H16.8C17.9201 20 18.4802 20 18.908 19.782C19.2843 19.5903 19.5903 19.2843 19.782 18.908C20 18.4802 20 17.9201 20 16.8V12.5M15.5 5.5L18.3284 8.32843M10.7627 10.2373L17.411 3.58902C18.192 2.80797 19.4584 2.80797 20.2394 3.58902C21.0205 4.37007 21.0205 5.6364 20.2394 6.41745L13.3774 13.2794C12.6158 14.0411 12.235 14.4219 11.8012 14.7247C11.4162 14.9936 11.0009 15.2162 10.564 15.3882C10.0717 15.582 9.54378 15.6885 8.48793 15.9016L8 16L8.04745 15.6678C8.21536 14.4925 8.29932 13.9048 8.49029 13.3561C8.65975 12.8692 8.89125 12.4063 9.17906 11.9786C9.50341 11.4966 9.92319 11.0768 10.7627 10.2373Z" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

+4
View File
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg fill="#000000" width="800px" height="800px" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg">
<path d="M30.015 12.97l-2.567-0.569c-0.2-0.64-0.462-1.252-0.762-1.841l1.389-2.313c0.518-0.829 0.78-2.047 0-2.829l-1.415-1.414c-0.78-0.781-2.098-0.64-2.894-0.088l-2.251 1.434c-0.584-0.303-1.195-0.563-1.829-0.768l-0.576-2.598c-0.172-0.953-1.005-1.984-2.11-1.984h-2c-1.104 0-1.781 1.047-2 2l-0.642 2.567c-0.678 0.216-1.328 0.492-1.948 0.819l-2.308-1.47c-0.795-0.552-2.114-0.692-2.894 0.088l-1.415 1.414c-0.781 0.782-0.519 2 0 2.828l1.461 2.435c-0.274 0.552-0.517 1.123-0.705 1.72l-2.566 0.569c-0.953 0.171-1.984 1.005-1.984 2.109v2c0 1.105 1.047 1.782 2 2l2.598 0.649c0.179 0.551 0.404 1.080 0.658 1.593l-1.462 2.438c-0.518 0.828-0.78 2.047 0 2.828l1.415 1.414c0.78 0.782 2.098 0.64 2.894 0.089l2.313-1.474c0.623 0.329 1.277 0.608 1.96 0.823l0.64 2.559c0.219 0.953 0.896 2 2 2h2c1.105 0 1.938-1.032 2.11-1.985l0.577-2.604c0.628-0.203 1.23-0.459 1.808-0.758l2.256 1.438c0.796 0.552 2.114 0.692 2.895-0.089l1.415-1.414c0.78-0.782 0.518-2 0-2.828l-1.39-2.317c0.279-0.549 0.521-1.12 0.716-1.714l2.599-0.649c0.953-0.219 2-0.895 2-2v-2c0-1.104-1.031-1.938-1.985-2.11zM30.001 16.939c-0.085 0.061-0.245 0.145-0.448 0.192l-3.708 0.926-0.344 1.051c-0.155 0.474-0.356 0.954-0.597 1.428l-0.502 0.986 1.959 3.267c0.125 0.2 0.183 0.379 0.201 0.485l-1.316 1.314c-0.127-0.040-0.271-0.092-0.341-0.14l-3.292-2.099-1.023 0.529c-0.493 0.256-0.999 0.468-1.503 0.631l-1.090 0.352-0.824 3.723c-0.038 0.199-0.145 0.36-0.218 0.417h-1.8c-0.061-0.085-0.145-0.245-0.191-0.448l-0.921-3.681-1.066-0.338c-0.549-0.173-1.097-0.404-1.63-0.684l-1.028-0.543-3.293 2.099c-0.135 0.091-0.279 0.143-0.409 0.143l-1.311-1.276c0.018-0.104 0.072-0.274 0.181-0.449l2.045-3.408-0.487-0.98c-0.227-0.462-0.407-0.895-0.547-1.325l-0.343-1.052-3.671-0.918c-0.231-0.052-0.398-0.139-0.485-0.2v-1.86c0.001 0.001 0.002 0.001 0.005 0.001 0.034 0 0.198-0.117 0.335-0.142l3.772-0.835 0.346-1.103c0.141-0.449 0.333-0.917 0.588-1.43l0.487-0.98-2.024-3.373c-0.125-0.201-0.184-0.38-0.201-0.485l1.315-1.314c0.128 0.041 0.271 0.093 0.34 0.14l3.354 2.138 1.027-0.542c0.527-0.278 1.073-0.507 1.622-0.682l1.063-0.338 0.912-3.649c0.053-0.231 0.138-0.398 0.2-0.485h1.859c-0.014 0.020 0.115 0.195 0.142 0.339l0.84 3.794 1.089 0.352c0.511 0.165 1.023 0.38 1.523 0.639l1.023 0.532 3.224-2.053c0.135-0.092 0.279-0.143 0.409-0.143l1.313 1.276c-0.017 0.104-0.072 0.276-0.181 0.45l-1.98 3.296 0.505 0.988c0.273 0.533 0.48 1.033 0.635 1.529l0.346 1.104 3.697 0.82c0.224 0.041 0.398 0.171 0.434 0.241zM16.013 9.99c-3.321 0-6.023 2.697-6.023 6.010s2.702 6.010 6.023 6.010 6.023-2.697 6.023-6.009c0-3.313-2.702-6.010-6.023-6.010zM16 20c-2.205 0-4-1.794-4-4s1.794-4 4-4c2.206 0 4 1.794 4 4s-1.794 4-4 4z"></path>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

+33
View File
@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg
fill="#000000"
width="800"
height="800"
viewBox="0 0 24 24"
version="1.1"
id="svg1"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<g
id="Dark">
<path
d="m 12.741,20.917 a 9.389,9.389 0 0 1 -1.395,-0.105 9.141,9.141 0 0 1 -1.465,-17.7 1.177,1.177 0 0 1 1.21,0.281 1.273,1.273 0 0 1 0.325,1.293 8.112,8.112 0 0 0 -0.353,2.68 8.266,8.266 0 0 0 4.366,6.857 7.628,7.628 0 0 0 3.711,0.993 1.242,1.242 0 0 1 0.994,1.963 v 0 a 9.148,9.148 0 0 1 -7.393,3.738 z M 10.261,4.05 a 0.211,0.211 0 0 0 -0.065,0.011 8.137,8.137 0 1 0 9.131,12.526 v 0 A 0.224,0.224 0 0 0 19.34,16.352 0.232,0.232 0 0 0 19.134,16.216 8.619,8.619 0 0 1 14.946,15.1 9.274,9.274 0 0 1 10.063,7.4 9.123,9.123 0 0 1 10.463,4.392 0.286,0.286 0 0 0 10.394,4.107 0.184,0.184 0 0 0 10.261,4.05 Z"
id="path1" />
</g>
<path
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:0.6;stroke-opacity:1"
d="M 12.27,19.887864 C 8.6252997,19.678981 5.5285722,16.983616 4.802205,13.387962 4.6751742,12.759136 4.6511961,12.497206 4.6531749,11.76 c 0.00153,-0.569372 0.01461,-0.787852 0.068001,-1.135667 C 4.9722048,8.989005 5.6646891,7.5303876 6.7700292,6.3087222 7.5904449,5.4019665 8.7542199,4.6113468 9.8911899,4.1883372 10.240576,4.0583481 10.339583,4.0517409 10.40655,4.1539446 10.47235,4.2543711 10.46876,4.3012971 10.3656,4.6890069 10.138096,5.5440534 10.057589,6.1522074 10.055041,7.035 c -0.002,0.7022856 0.03507,1.0781595 0.174729,1.7704386 0.441048,2.1862074 1.740566,4.2313134 3.533802,5.5612914 1.522152,1.128924 3.239127,1.76416 5.001426,1.850398 0.426916,0.02089 0.522077,0.04974 0.567784,0.172121 0.01351,0.03616 0.01272,0.09717 -0.0018,0.135571 -0.0463,0.122854 -0.480151,0.639299 -0.836032,0.99518 -1.646591,1.64659 -3.893995,2.501458 -6.225,2.367864 z"
id="path2" />
<path
style="fill:#000000;stroke-width:0.6"
d="M 12.24,20.891972 C 11.360122,20.823621 10.700414,20.69763 9.9480678,20.454259 8.4650988,19.974545 7.068363,19.071092 6.0273591,17.91822 3.5681787,15.194772 2.9603488,11.317696 4.4732148,8.0050236 5.4683391,5.8260321 7.3075485,4.1019471 9.54,3.2553909 c 0.474857,-0.1800684 0.671021,-0.2157843 0.918134,-0.1671678 0.388517,0.076436 0.689105,0.3111453 0.870584,0.6797817 0.07976,0.1620108 0.09971,0.2428032 0.110439,0.4473105 0.01165,0.2220426 -0.0021,0.3102909 -0.125027,0.7996359 -0.186386,0.7422312 -0.231182,1.0670649 -0.250567,1.816968 -0.01825,0.7060722 0.01143,1.0908765 0.130201,1.6880808 0.513779,2.583326 2.113703,4.659009 4.525866,5.871694 1.023273,0.514437 1.887784,0.742874 3.138603,0.829338 0.686858,0.04748 0.813587,0.08854 1.095191,0.354845 0.396466,0.374925 0.503868,0.948025 0.265728,1.417923 -0.188868,0.372671 -1.032844,1.304647 -1.622363,1.791523 -1.361429,1.124387 -3.007202,1.834185 -4.749821,2.048528 -0.372347,0.0458 -1.32298,0.08018 -1.606968,0.05812 z m 1.812099,-1.059112 c 2.09671,-0.362295 3.901569,-1.432794 5.155126,-3.05761 0.138666,-0.179734 0.168974,-0.239673 0.166437,-0.329157 -0.0049,-0.17327 -0.112259,-0.246087 -0.362835,-0.246118 -0.638294,-9e-5 -1.608859,-0.157002 -2.343213,-0.378853 -0.845921,-0.255557 -1.94952,-0.811781 -2.743228,-1.382616 C 12.717736,13.570686 11.728623,12.407926 11.05893,11.07 10.402835,9.7592388 10.076992,8.4125052 10.082658,7.035 10.086058,6.2084796 10.19082,5.4236802 10.394064,4.7021844 10.501518,4.3207323 10.50584,4.2345858 10.422823,4.1290458 10.333346,4.0152951 10.211226,4.0262211 9.8074461,4.1841018 8.7208542,4.6089669 7.7283636,5.269389 6.9148926,6.1088616 6.4866426,6.5508003 6.2969187,6.7845894 5.9734524,7.2689589 5.2794414,8.3081952 4.8329166,9.5184891 4.659645,10.83 4.602754,11.260615 4.6119,12.415262 4.675686,12.855 c 0.1862775,1.284197 0.6107841,2.410878 1.2942264,3.435 0.3498402,0.524227 0.5965434,0.824106 1.0489137,1.275 1.2796647,1.275489 2.8952232,2.063697 4.7361019,2.310675 0.483003,0.0648 1.818404,0.03991 2.297171,-0.04282 z"
id="path3" />
<path
style="fill:#000000;stroke-width:2.75694"
d="m 4.285738,11.874144 c 0.031414,-0.01988 0.3856,-0.02656 0.7854538,-0.01483 0.3998535,0.01172 0.3734269,0.02799 -0.058508,0.03615 -0.4319739,0.0082 -0.7591449,-0.0014 -0.7269853,-0.02132 z"
id="path4" />
</svg>

After

Width:  |  Height:  |  Size: 4.3 KiB

+29
View File
@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg version="1.0" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
width="800px" height="800px" viewBox="0 0 64 64" enable-background="new 0 0 64 64" xml:space="preserve">
<g>
<circle fill-rule="evenodd" clip-rule="evenodd" fill="#231F20" cx="32.003" cy="32.005" r="16.001"/>
<path fill-rule="evenodd" clip-rule="evenodd" fill="#231F20" d="M12.001,31.997c0-2.211-1.789-4-4-4H4c-2.211,0-4,1.789-4,4
s1.789,4,4,4h4C10.212,35.997,12.001,34.208,12.001,31.997z"/>
<path fill-rule="evenodd" clip-rule="evenodd" fill="#231F20" d="M12.204,46.139l-2.832,2.833c-1.563,1.562-1.563,4.094,0,5.656
c1.562,1.562,4.094,1.562,5.657,0l2.833-2.832c1.562-1.562,1.562-4.095,0-5.657C16.298,44.576,13.767,44.576,12.204,46.139z"/>
<path fill-rule="evenodd" clip-rule="evenodd" fill="#231F20" d="M32.003,51.999c-2.211,0-4,1.789-4,4V60c0,2.211,1.789,4,4,4
s4-1.789,4-4l-0.004-4.001C36.003,53.788,34.21,51.999,32.003,51.999z"/>
<path fill-rule="evenodd" clip-rule="evenodd" fill="#231F20" d="M51.798,46.143c-1.559-1.566-4.091-1.566-5.653-0.004
s-1.562,4.095,0,5.657l2.829,2.828c1.562,1.57,4.094,1.562,5.656,0s1.566-4.09,0-5.656L51.798,46.143z"/>
<path fill-rule="evenodd" clip-rule="evenodd" fill="#231F20" d="M60.006,27.997l-4.009,0.008
c-2.203-0.008-3.992,1.781-3.992,3.992c-0.008,2.211,1.789,4,3.992,4h4.001c2.219,0.008,4-1.789,4-4
C64.002,29.79,62.217,27.997,60.006,27.997z"/>
<path fill-rule="evenodd" clip-rule="evenodd" fill="#231F20" d="M51.798,17.859l2.828-2.829c1.574-1.566,1.562-4.094,0-5.657
c-1.559-1.567-4.09-1.567-5.652-0.004l-2.829,2.836c-1.562,1.555-1.562,4.086,0,5.649C47.699,19.426,50.239,19.418,51.798,17.859z"
/>
<path fill-rule="evenodd" clip-rule="evenodd" fill="#231F20" d="M32.003,11.995c2.207,0.016,4-1.789,4-3.992v-4
c0-2.219-1.789-4-4-4c-2.211-0.008-4,1.781-4,3.993l0.008,4.008C28.003,10.206,29.792,11.995,32.003,11.995z"/>
<path fill-rule="evenodd" clip-rule="evenodd" fill="#231F20" d="M12.212,17.855c1.555,1.562,4.079,1.562,5.646-0.004
c1.574-1.551,1.566-4.09,0.008-5.649l-2.829-2.828c-1.57-1.571-4.094-1.559-5.657,0c-1.575,1.559-1.575,4.09-0.012,5.653
L12.212,17.855z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 98 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 844 B

After

Width:  |  Height:  |  Size: 844 B

File renamed without changes.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#000000"><path d="M784-120 532-372q-30 24-69 38t-83 14q-109 0-184.5-75.5T120-580q0-109 75.5-184.5T380-840q109 0 184.5 75.5T640-580q0 44-14 83t-38 69l252 252-56 56ZM380-400q75 0 127.5-52.5T560-580q0-75-52.5-127.5T380-760q-75 0-127.5 52.5T200-580q0 75 52.5 127.5T380-400Z"/></svg>

After

Width:  |  Height:  |  Size: 376 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#000000"><path d="m80-280 150-400h86l150 400h-82l-34-96H196l-32 96H80Zm140-164h104l-48-150h-6l-50 150Zm328 164v-76l202-252H556v-72h282v76L638-352h202v72H548ZM360-760l120-120 120 120H360ZM480-80 360-200h240L480-80Z"/></svg>

After

Width:  |  Height:  |  Size: 320 B

+5
View File
@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="currentColor">
<path d="M 480,-880 360,-760 H 600 Z M 360,-200 480,-80 600,-200 Z" />
<path d="M 71.679924,-674.40094 H 398.50264 v 60.67705 l -208.59362,252.3436 h 214.5832 v 75.7812 H 65.690344 v -60.67705 L 274.28397,-598.61973 H 71.679924 Z" />
<path d="M 769.04932,-356.43238 H 612.27858 l -24.73957,70.83329 H 486.75782 l 144.01033,-388.80185 h 119.53118 l 144.01033,388.80185 H 793.52847 Z m -131.77076,-72.13537 h 106.51035 l -53.12496,-154.68741 z" />
</svg>

After

Width:  |  Height:  |  Size: 545 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#000000"><path d="M340-160q-125 0-212.5-87.5T40-460q0-125 87.5-212.5T340-760q52 0 98 16.5t84 45.5l42-42 56 56-42 42q29 38 45.5 84.5T640-460q0 125-87.5 212.5T340-160Zm440 0L640-300l56-56 44 44v-488h80v487l43-43 57 56-140 140ZM240-800v-80h200v80H240Zm100 560q92 0 156-64t64-156q0-92-64-156t-156-64q-92 0-156 64t-64 156q0 92 64 156t156 64Zm-40-180h80v-200h-80v200Zm40-40Z"/></svg>

After

Width:  |  Height:  |  Size: 475 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#000000"><path d="M340-160q-125 0-212.5-87.5T40-460q0-125 87.5-212.5T340-760q52 0 98 16.5t84 45.5l42-42 56 56-42 42q29 38 45.5 84.5T640-460q0 125-87.5 212.5T340-160Zm400 0v-488l-44 44-56-56 140-140 140 140-57 56-43-43v487h-80ZM240-800v-80h200v80H240Zm100 560q92 0 156-64t64-156q0-92-64-156t-156-64q-92 0-156 64t-64 156q0 92 64 156t156 64Zm-40-180h80v-200h-80v200Zm40-40Z"/></svg>

After

Width:  |  Height:  |  Size: 477 B

+84
View File
@@ -0,0 +1,84 @@
import styles from "./Event.module.css";
import { useState, useEffect } from "react";
import { useNavigate } from "react-router";
import Button from "/src/components/ui/button/button.jsx"
function Event({event}){
const navigate = useNavigate();
const [eventMenu, seteventMenu] = useState(false);
const [windowSize, setWindowSize] = useState({
width: null,
height: null,
});
useEffect(() => {
function handleResize() {
setWindowSize({
width: window.innerWidth,
height: window.innerHeight,
});
}
window.addEventListener("resize", handleResize);
handleResize();
return () => window.removeEventListener("resize", handleResize);
}, []);
return (
<div className={`${styles.eventContainer} glassCard`}>
<div className={styles.eventContainerTop}>
<div className={styles.eventContent}>
<div className={`${styles.eventImageDiv}`}>
{event.image == null ? (
<img src="/empty.png" className={styles.eventImage} alt="Pas de photo de l'événement" />
) : (
<img src={event.image + `.png`} alt="Photo de l'événement" />
)}
</div>
<div className={styles.eventInfos}>
<p className={styles.eventTitle}>{event.name}</p>
<p>{event.description}</p>
{event.tasks != null ? (
<div className={eventMenu ? styles.eventMenuOpen : styles.eventMenuClose}>
<ul>
{event.tasks
.slice(0,5)
.map((task, index) => (
<li key={index}> - {task.name}</li>
))}
{event.tasks.length > 5 ? (
<li>...</li>
) : null}
</ul>
</div>
) : null}
</div>
</div>
{windowSize.width > 768 ? (
<div className={styles.eventButtons}>
<Button
variant={"transparent"}
onClick={()=>{seteventMenu(!eventMenu)}}
className={`${eventMenu ? styles.moreButtonClicked : ""}`}
>
{">"}
</Button>
</div>
) : null}
</div>
<div className={styles.eventContainerBottom}>
{(windowSize.width <= 768 || eventMenu) ? (
<Button onClick={() => navigate(`/event/` + event.id)}>Voir plus</Button>
) : null}
</div>
</div>
)
}
export default Event;
+127
View File
@@ -0,0 +1,127 @@
.eventContainer {
display: flex;
flex-direction: column;
margin: 1rem 0;
width: 80%;
padding: 1rem 0.5rem;
}
.eventContainerTop {
display: flex;
justify-content: space-between;
}
.eventContainerBottom{
display: flex;
align-items: center;
justify-content: end;
}
.moreLink {
width: fit-content;
}
.eventImageDiv {
display: flex;
align-items: center;
justify-content: center;
margin-right: 1rem;
width: auto;
height: 6rem;
flex-shrink: 0;
}
.eventImage {
max-width: 100%;
max-height: 100%;
object-fit: contain;
border-radius: 20px;
}
.eventContent {
display: flex;
width: 100%;
}
.eventTitle {
font-size: 2rem;
}
.eventButtons {
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: flex-end;
width: fit-content;
overflow: visible;
}
.eventButtons a {
white-space: nowrap;
}
.moreButton {
background: none;
border: none;
color: #019AFF;
font-size: 1.5rem;
transition: all 0.2s ease;
cursor: pointer;
border-radius: 50%;
width: 55px;
height: 55px;
}
.moreButton:hover {
font-size: 2rem;
background: rgba(191, 191, 191, 0.5);
}
.moreButton:focus {
outline: none;
}
.moreButton:focus-visible {
outline: 2px solid black;
}
.moreButtonClicked {
rotate: 90deg;
}
.eventMenuOpen {
display: flex;
flex-direction: column;
margin-left: 1.5em;
}
.eventMenuClose {
display: none;
}
@media screen and (max-width: 768px) {
.eventImage {
width: 100%;
height: auto;
display: block;
object-fit: cover;
border-bottom-right-radius: 0;
border-bottom-left-radius: 0;
}
.eventContainer{
padding: 0;
}
.eventImageDiv {
margin: 0;
}
.eventContent {
flex-direction: column;
}
.eventInfos, .eventContainerBottom {
padding: 10px;
}
}
+61
View File
@@ -0,0 +1,61 @@
import styles from "./Filter.module.css";
function Filter({isFilterVisible, filters, setFilters}){
const setAlphabeticalOrder = (order) => {
setFilters(prev => ({
...prev,
alphabetical : prev.alphabetical === order ? null : order
}));
};
const setYearOrder = (order) => {
setFilters(prev => ({
...prev,
yearOrder: prev.yearOrder === order ? null : order
}));
};
return (
<div className={`${styles.filterContainer} ${isFilterVisible ? styles.filterOpen : styles.filterClose}`}>
<abbr title="Tri alphabétique croissant (A à Z)">
<button className={`${styles.filterTag} glassCard ${filters.alphabetical !== "asc" ? "" : styles.active}`}
onClick={() => setAlphabeticalOrder("asc")}>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="currentColor">
<path d="m80-280 150-400h86l150 400h-82l-34-96H196l-32 96H80Zm140-164h104l-48-150h-6l-50 150Zm328 164v-76l202-252H556v-72h282v76L638-352h202v72H548ZM360-760l120-120 120 120H360ZM480-80 360-200h240L480-80Z"/>
</svg>
</button>
</abbr>
<abbr title="Tri alphabétique décroissant (Z à A)">
<button className={`${styles.filterTag} glassCard ${filters.alphabetical !== "desc" ? "" : styles.active}`}
onClick={() => setAlphabeticalOrder("desc")}>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="currentColor">
<path d="M 480,-880 360,-760 H 600 Z M 360,-200 480,-80 600,-200 Z" />
<path d="M 71.679924,-674.40094 H 398.50264 v 60.67705 l -208.59362,252.3436 h 214.5832 v 75.7812 H 65.690344 v -60.67705 L 274.28397,-598.61973 H 71.679924 Z" />
<path d="M 769.04932,-356.43238 H 612.27858 l -24.73957,70.83329 H 486.75782 l 144.01033,-388.80185 h 119.53118 l 144.01033,388.80185 H 793.52847 Z m -131.77076,-72.13537 h 106.51035 l -53.12496,-154.68741 z" />
</svg>
</button>
</abbr>
<abbr title="Tri chronologique décroissant (du plus récent au plus ancien)">
<button className={`${styles.filterTag} glassCard ${filters.yearOrder !== "desc" ? "" : styles.active}`}
onClick={() => setYearOrder("desc")}>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="currentColor">
<path d="M340-160q-125 0-212.5-87.5T40-460q0-125 87.5-212.5T340-760q52 0 98 16.5t84 45.5l42-42 56 56-42 42q29 38 45.5 84.5T640-460q0 125-87.5 212.5T340-160Zm400 0v-488l-44 44-56-56 140-140 140 140-57 56-43-43v487h-80ZM240-800v-80h200v80H240Zm100 560q92 0 156-64t64-156q0-92-64-156t-156-64q-92 0-156 64t-64 156q0 92 64 156t156 64Zm-40-180h80v-200h-80v200Zm40-40Z"/>
</svg>
</button>
</abbr>
<abbr title="Tri chronologique croissant (du plus ancien au plus récent)">
<button className={`${styles.filterTag} glassCard ${filters.yearOrder !== "asc" ? "" : styles.active}`}
onClick={() => setYearOrder("asc")}>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="currentColor">
<path d="M340-160q-125 0-212.5-87.5T40-460q0-125 87.5-212.5T340-760q52 0 98 16.5t84 45.5l42-42 56 56-42 42q29 38 45.5 84.5T640-460q0 125-87.5 212.5T340-160Zm440 0L640-300l56-56 44 44v-488h80v487l43-43 57 56-140 140ZM240-800v-80h200v80H240Zm100 560q92 0 156-64t64-156q0-92-64-156t-156-64q-92 0-156 64t-64 156q0 92 64 156t156 64Zm-40-180h80v-200h-80v200Zm40-40Z"/>
</svg>
</button>
</abbr>
</div>
)
}
export default Filter;
+85
View File
@@ -0,0 +1,85 @@
.filterContainer {
display: flex;
flex-direction: column;
gap: 0.4rem;
overflow: hidden;
max-height: 0;
opacity: 0;
transition: max-height 0.5s ease, opacity 0.8s ease, transform 0.5s ease;
}
.filterOpen {
max-height: 200px;
opacity: 1;
transform: translateY(0);
}
.filterClose{
max-height: 200px;
opacity: 0;
}
.filterTag {
border: none;
background: none;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
width: 45px;
height: 45px;
}
.filterTag:focus {
outline: none;
}
.filterTag:focus-visible {
outline: 2px solid black;
}
.filterTag:hover {
cursor: pointer;
svg{
fill: #019AFF;
}
}
.filterTag svg {
transform: scale(0.7);
color: black;
}
.active {
border: 1px solid #019AFF;
}
.active svg {
fill: #019AFF;
transform: scale(0.8);
}
@media screen and (max-width: 768px){
.filterContainer {
flex-direction: row;
flex-wrap: wrap;
}
.filterOpen{
display: flex;
}
.filterClose{
display: none;
}
.filterMenu {
width: 100%;
}
abbr {
width: fit-content;
}
}
+3 -3
View File
@@ -18,7 +18,7 @@ function Header() {
setNotifications(notifData || []); setNotifications(notifData || []);
} }
loadNotifications(); //loadNotifications();
const handleClickOutside = (event) => { const handleClickOutside = (event) => {
if ( if (
notificationRef.current && notificationRef.current &&
@@ -41,7 +41,7 @@ function Header() {
<nav className={`${mobileMenu ? styles.inactiveHeaderContent : styles.activeHeaderContent}`}> <nav className={`${mobileMenu ? styles.inactiveHeaderContent : styles.activeHeaderContent}`}>
<div className={styles.headerLeftContent}> <div className={styles.headerLeftContent}>
<NavLink className={styles.logoLink} to="/"> <NavLink className={styles.logoLink} to="/">
<img src="/src/assets/logo.png" alt="logo" className={styles.logo} /> <img src="logo.png" alt="logo" className={styles.logo} />
</NavLink> </NavLink>
<div className={styles.navLinks}> <div className={styles.navLinks}>
<NavLink to="/" className={({ isActive }) => isActive ? styles.activeLink : styles.link}> <NavLink to="/" className={({ isActive }) => isActive ? styles.activeLink : styles.link}>
@@ -70,7 +70,7 @@ function Header() {
{unreadNotification && <span className={styles.notificationBadge}></span>} {unreadNotification && <span className={styles.notificationBadge}></span>}
</button> </button>
<Link to="" className={styles.profileLink}> <Link to="/profile" className={styles.profileLink}>
<svg xmlns="http://www.w3.org/2000/svg" height="30px" viewBox="0 -960 960 960" width="auto" fill="currentFill"> <svg xmlns="http://www.w3.org/2000/svg" height="30px" viewBox="0 -960 960 960" width="auto" fill="currentFill">
<path d="M480-492.31q-57.75 0-98.87-41.12Q340-574.56 340-632.31q0-57.75 41.13-98.87 41.12-41.13 98.87-41.13 57.75 0 98.87 41.13Q620-690.06 620-632.31q0 57.75-41.13 98.88-41.12 41.12-98.87 41.12ZM180-187.69v-88.93q0-29.38 15.96-54.42 15.96-25.04 42.66-38.5 59.3-29.07 119.65-43.61 60.35-14.54 121.73-14.54t121.73 14.54q60.35 14.54 119.65 43.61 26.7 13.46 42.66 38.5Q780-306 780-276.62v88.93H180Z" /> <path d="M480-492.31q-57.75 0-98.87-41.12Q340-574.56 340-632.31q0-57.75 41.13-98.87 41.12-41.13 98.87-41.13 57.75 0 98.87 41.13Q620-690.06 620-632.31q0 57.75-41.13 98.88-41.12 41.12-98.87 41.12ZM180-187.69v-88.93q0-29.38 15.96-54.42 15.96-25.04 42.66-38.5 59.3-29.07 119.65-43.61 60.35-14.54 121.73-14.54t121.73 14.54q60.35 14.54 119.65 43.61 26.7 13.46 42.66 38.5Q780-306 780-276.62v88.93H180Z" />
</svg> </svg>
+1 -1
View File
@@ -179,7 +179,7 @@ svg {
opacity: 0; opacity: 0;
transform: translateY(-10px); transform: translateY(-10px);
transition: opacity 0.3s ease, transform 0.3s ease; transition: opacity 0.3s ease, transform 0.3s ease;
z-index: 1; z-index: 0;
} }
.activeNotificationDiv { .activeNotificationDiv {
+103
View File
@@ -0,0 +1,103 @@
import styles from "./SearchBar.module.css"
import {Link} from "react-router";
import {useEffect, useState} from "react";
import searchEvents from "../../utils/searchEvents.js";
import searchUsers from "../../utils/searchUsers.js";
import { useLocation } from "react-router";
function SearchBar() {
const [searchQuery, setSearchQuery] = useState('');
const [searchResults, setSearchResults] = useState([]);
const location = useLocation();
useEffect(() => {
const performSearch = async (query) => {
if (query.trim() === '') {
setSearchResults([]);
return;
}
let data;
if (location.pathname.includes("events")) {
data = await searchEvents(query);
} else if (location.pathname.includes("volunteers")) {
data = await searchUsers(query);
}
if (data) {
setSearchResults(data);
} else {
setSearchResults([]);
}
};
const handler = setTimeout(() => {
performSearch(searchQuery);
}, 300);
return () => {
clearTimeout(handler);
};
}, [searchQuery]);
return (
<div className={`${styles.searchBarContainer} glassCard`}>
<form className={styles.searchBarForm} onSubmit={(element) => element.preventDefault()}>
<svg xmlns="http://www.w3.org/2000/svg" width="24px" viewBox="0 -960 960 960" fill="#currentColor">
<path
d="M784-120 532-372q-30 24-69 38t-83 14q-109 0-184.5-75.5T120-580q0-109 75.5-184.5T380-840q109 0 184.5 75.5T640-580q0 44-14 83t-38 69l252 252-56 56ZM380-400q75 0 127.5-52.5T560-580q0-75-52.5-127.5T380-760q-75 0-127.5 52.5T200-580q0 75 52.5 127.5T380-400Z"/>
</svg>
<input
type="text"
placeholder="Rechercher un événement"
value={searchQuery}
onChange={(element) => setSearchQuery(element.target.value)}
/>
</form>
<div className={styles.lineContainer}>
{searchResults.length > 0 ? (
<div className={styles.line}></div>
) : null}
</div>
<div className={styles.elementList}>
{searchResults.map((result, i) => (
(location.pathname.includes("events")) ? (
<Link to={`/event/` + result.id}>
<div className={styles.searchedEvent} key={i}>
<div className={`${styles.eventImageDiv}`}>
{result.image == null ? (
<img src="empty.png" className={styles.eventImage} alt="Pas de photo de l'événement" />
) : (
<img src={result.image + `.png`} alt="Photo de l'événement" />
)}
</div>
<p>{result.name}</p>
</div>
</Link>
) : location.pathname.includes("volunteers") ? (
<Link to={`/profile/` + result.id}>
<div className={styles.searchedEvent} key={i}>
<div className={`${styles.eventImageDiv}`}>
{result.image == null ? (
<img src="empty.png" className={styles.eventImage} alt="Pas de photo de l'événement" />
) : (
<img src={result.image + `.png`} alt="Photo de l'événement" />
)}
</div>
<p>{result.name} {result.lastname}</p>
</div>
</Link>
) : null
))}
</div>
</div>
);
}
export default SearchBar;
@@ -0,0 +1,86 @@
.searchBarContainer{
position: fixed;
top: 4rem;
z-index: 100;
width: 60%;
background: #fff;
padding: 0.5rem;
}
.searchBarForm{
display: flex;
}
.elementList {
max-height: 60vh;
overflow-y: auto;
}
.eventImageDiv {
display: flex;
align-items: center;
justify-content: center;
margin-right: 1rem;
width: auto;
height: 2rem;
flex-shrink: 0;
}
.searchedEvent{
display: flex;
padding: 0.5rem;
border-radius: 15px;
align-items: center;
}
.searchedEvent:hover {
background: #afafaf;
cursor: pointer;
}
.searchedEvent p {
display: flex;
align-items: center;
text-decoration: none;
color: black;
}
.lineContainer{
display: flex;
justify-content: center;
align-items: center;
}
.line{
height: 0;
width: 96%;
border: #606060 solid 1px;
margin: 0.5rem 0;
}
.eventImage {
max-width: 100%;
max-height: 100%;
object-fit: contain;
border-radius: 20px;
}
.searchBarForm input{
background: none;
box-shadow: none;
height: 2em;
}
.searchBarForm input:focus{
outline: none;
}
@media screen and (max-width: 480px) {
.searchBarContainer{
width: 80%;
}
}
@@ -0,0 +1,70 @@
import Modal from "../ui/modal/modal.jsx";
import Button from "../ui/button/button.jsx";
import styles from "./SettingsModal.module.css"
import { useContext, useState } from "react";
import ThemeSwitcher from "../ui/themeSwitcher/ThemeSwitcher.jsx";
import { AuthContext } from "../../contexts/auth/AuthContext.js";
import TextInput from "../ui/input/input.jsx";
import updateUser from "../../utils/users/updateUser.js";
export default function SettingsModal() {
const { user, logout, update } = useContext(AuthContext);
const [open, setOpen] = useState(false);
const [name, setName] = useState(user.name);
const [lastname, setLastName] = useState(user.lastname);
const [phone, setPhone] = useState(user.phone);
const handleLogout = () => {
logout();
}
const handleSubmit = async () => {
await updateUser(name, lastname, phone);
update();
}
return (
<>
<Button variant={"transparent"} onClick={() => setOpen(!open)}>
<img src={"/icons/settings-wheel.svg"} alt={"Modify btn"} className={styles.modifyIcon}/>
</Button>
<Modal title={"Paramètres"} open={open} onClose={() => setOpen(false)}>
<div className={styles.content}>
<div className={styles.section}>
<h3>Compte</h3>
<Button variant={"danger"} onClick={handleLogout}> Déconnexion </Button>
<div className={styles.section}>
<h4>Changer de Prénom</h4>
<TextInput value={name} onChange={e => setName(e.target.value)} type={"text"} />
</div>
<div className={styles.section}>
<h4>Changer de nom</h4>
<TextInput value={lastname} onChange={e => setLastName(e.target.value)} type={"text"} />
</div>
<div className={styles.section}>
<h4>Changer de numéro de téléphone</h4>
<TextInput value={phone} onChange={e => setPhone(e.target.value)} type={"text"} />
</div>
<Button className={styles.submitBtn} onClick={handleSubmit}>Confirmer</Button>
</div>
<div className={styles.section}>
<h3>Apparence</h3>
<ThemeSwitcher />
</div>
</div>
</Modal>
</>
)
}
@@ -0,0 +1,23 @@
.content {
width: 100%;
padding: 20px;
display: flex;
flex-direction: column;
}
.modifyIcon {
height: 30px;
width: 30px;
}
.section {
display: flex;
flex-direction: column;
gap: 10px;
margin-top: 15px;
margin-bottom: 15px;
}
.submitBtn {
align-self: center;
}
+29
View File
@@ -0,0 +1,29 @@
import React, {useEffect, useState} from "react";
import getEventById from "../../utils/event/getEventById.js";
import styles from "./Task.module.css";
import formatDate from "../../utils/date/formatDate.js";
const Task = ({ task }) => {
const [event, setEvent] = useState(null);
useEffect(() => {
(async () => {
const e = await getEventById(task.id);
setEvent(e);
}) ()
}, [])
return (
<div className={`${styles.task} glassCard`}>
<div className={styles.content}>
<div>
<h2> {event && event.name} - {task.name}</h2>
<p className={styles.desc}> {task.description} </p>
<p className={styles.date}> {formatDate(task.start)} </p>
</div>
</div>
</div>
);
}
export default Task;
+22
View File
@@ -0,0 +1,22 @@
.task {
width: 100%;
padding: 12px;
margin-bottom: 10px;
border-radius: 8px;
}
.content {
display: flex;
justify-content: space-between;
flex-direction: column;
}
.desc {
font-size: 0.8rem;
color: #666;
margin: 0 0 0 0;
}
.date {
font-size: 0.8rem;
}
+66
View File
@@ -0,0 +1,66 @@
import styles from "./ToolBar.module.css"
import {Link} from "react-router";
import Filter from "../Filter/Filter.jsx";
import {useEffect, useState} from "react";
import SearchBar from "../SearchBar/SearchBar.jsx";
function ToolBar({setFilters, filters}) {
const [isFilterVisible, setIsFilterVisible] = useState(false);
const [isSearchVisible, setIsSearchVisible] = useState(false);
const toggleFilters = () => {
setIsFilterVisible(prev => !prev);
};
useEffect(() => {
if (isSearchVisible) {
document.documentElement.style.overflow = 'hidden';
} else {
document.documentElement.style.overflow = '';
}
return () => {
document.documentElement.style.overflow = '';
};
}, [isSearchVisible]);
return (
<div>
{isSearchVisible ? (
<div className={styles.searchBarContainer}>
<div className={styles.searchBarOverlay} onClick={() => setIsSearchVisible(false)}></div>
<SearchBar/>
</div>
) : null}
<div className={styles.eventsButtons}>
<Link
to="/event/create"
className={`${styles.createButton} glassCard`}>
+
</Link>
<button className={`${styles.searchButton} glassCard`} onClick={() => setIsSearchVisible(true)}>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="#currentColor">
<path d="M784-120 532-372q-30 24-69 38t-83 14q-109 0-184.5-75.5T120-580q0-109 75.5-184.5T380-840q109 0 184.5 75.5T640-580q0 44-14 83t-38 69l252 252-56 56ZM380-400q75 0 127.5-52.5T560-580q0-75-52.5-127.5T380-760q-75 0-127.5 52.5T200-580q0 75 52.5 127.5T380-400Z"/>
</svg>
</button>
<div className={`${styles.filterContainer} ${isFilterVisible ? "glassCard" : ""}`}>
<button className={`${isFilterVisible ? "" : styles.mobileFilter} ${styles.sortButton} glassCard`} onClick={toggleFilters}>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="currentColor">
<path d="M440-160q-17 0-28.5-11.5T400-200v-240L168-736q-15-20-4.5-42t36.5-22h560q26 0 36.5 22t-4.5 42L560-440v240q0 17-11.5 28.5T520-160h-80Zm40-308 198-252H282l198 252Zm0 0Z"/>
</svg>
</button>
<div className={styles.filterMenu}>
<Filter isFilterVisible={isFilterVisible} filters={filters} setFilters={setFilters}/>
</div>
</div>
</div>
</div>
);
}
export default ToolBar;
+107
View File
@@ -0,0 +1,107 @@
.eventsButtons {
overflow: visible;
position: fixed;
top: 10rem;
right: 2rem;
display: flex;
flex-direction: column;
align-items: flex-end;
width: fit-content;
height: fit-content;
gap: 1rem;
}
.filterContainer{
padding: 0;
border-radius: 30px;
overflow: visible;
}
.filterMenu{
padding: 5px 5px 5px 5px;
width: fit-content;
}
.createButton, .searchButton, .sortButton {
display: flex;
align-items: center;
justify-content: center;
border: none;
border-radius: 50% !important;
width: 55px !important;
height: 55px !important;
transition: all 0.5s ease;
}
.createButton {
font-size: 2rem;
color: #019AFF;
}
.createButton:hover, .searchButton:hover, .mobileFilter:hover {
transform: translateY(-5px);
cursor: pointer;
}
.searchBarOverlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 100;
background-color: rgba(0, 0, 0, 0.3);
backdrop-filter: blur(4px);
}
.searchBarContainer{
display: flex;
justify-content: center;
}
.sortButton svg, .searchButton svg {
transform: scale(0.5);
fill: #019AFF;
}
.sortButton:focus, .searchButton:focus {
outline: none;
}
@media screen and (max-width: 768px) {
.createButton,
.searchButton,
.sortButton {
flex: 0 0 55px;
width: 55px !important;
height: 55px !important;
}
.eventsButtons {
flex-direction: row;
position: relative;
top: 0;
right: 0;
align-items: flex-start;
width: 100%;
margin-top: 16px;
}
.filterContainer{
display: flex;
width: fit-content;
}
.filterMenu{
width: fit-content;
}
.createButton:hover, .searchButton:hover, .mobileFilter:hover {
transform: none !important;
}
}
+21 -4
View File
@@ -1,8 +1,10 @@
import styles from "./background.module.css"; import styles from "./background.module.css";
import { useEffect, useRef } from "react"; import { useEffect, useRef, useState } from "react";
const Background = ({ children, className }) => { const Background = ({ children, className }) => {
const circlesRef = useRef([]);
const circlesRef = useRef([]);
const [theme, setTheme] = useState("light");
const getRandomTranslation = () => { const getRandomTranslation = () => {
const randomX = Math.floor(Math.random() * 100) - 30; const randomX = Math.floor(Math.random() * 100) - 30;
@@ -20,8 +22,23 @@ const Background = ({ children, className }) => {
}); });
}, []); }, []);
return ( useEffect(() => {
<div className={`${styles["background-container"]} ${className || ''}`}> const updateTheme = () => {
setTheme(localStorage.getItem("theme") || "light");
};
updateTheme(); // Initial load
window.addEventListener("theme-changed", updateTheme);
return () => {
window.removeEventListener("theme-changed", updateTheme);
};
}, []);
return (
<div className={`${styles.backgroundContainer} ${className || ''} ${theme === "light" ? styles.backgroundLight : styles.backgroundDark}`}>
<div <div
ref={(el) => (circlesRef.current[0] = el)} ref={(el) => (circlesRef.current[0] = el)}
className={`${styles["circle"]} ${styles["circle-1"]}`} className={`${styles["circle"]} ${styles["circle-1"]}`}
@@ -1,10 +1,9 @@
/* BACKGROUND ////////////////////////////////////////////////////////*/ /* BACKGROUND ////////////////////////////////////////////////////////*/
.background-container { .backgroundContainer {
z-index: 0; z-index: 0;
margin: 0; margin: 0;
padding: 0; padding: 0;
min-height: 100vh; min-height: 100vh;
background: #00F0FF;
overflow: hidden; overflow: hidden;
position: relative; position: relative;
width: 100vw; width: 100vw;
@@ -13,6 +12,14 @@
align-items: center; align-items: center;
} }
.backgroundLight {
background: #00F0FF;
}
.backgroundDark {
background: #001C83;
}
.circle { .circle {
position: absolute; position: absolute;
border-radius: 50%; border-radius: 50%;
@@ -82,7 +89,6 @@
.circle-1 { .circle-1 {
width: 300px; width: 300px;
height: 300px; height: 300px;
background-color: #019AFF;
top: -5%; top: -5%;
left: -5%; left: -5%;
} }
@@ -90,7 +96,6 @@
.circle-2 { .circle-2 {
width: 175px; width: 175px;
height: 175px; height: 175px;
background-color: #91FCFF;
top: 58%; top: 58%;
left: 10%; left: 10%;
} }
@@ -98,7 +103,6 @@
.circle-3 { .circle-3 {
width: 140px; width: 140px;
height: 140px; height: 140px;
background-color: #91FCFF;
top: 29%; top: 29%;
right: 9%; right: 9%;
} }
@@ -106,7 +110,6 @@
.circle-4 { .circle-4 {
width: 250px; width: 250px;
height: 250px; height: 250px;
background-color: #019AFF;
bottom: 0; bottom: 0;
right: 0; right: 0;
} }
@@ -114,7 +117,6 @@
.circle-5 { .circle-5 {
width: 240px; width: 240px;
height: 240px; height: 240px;
background-color: #015AFF;
top: 2%; top: 2%;
right: 10%; right: 10%;
} }
-103
View File
@@ -1,103 +0,0 @@
/*import { useState } from "react";
import login from "../../utils/login.js";
import getUser from "../../utils/getUser.js";
import styles from "./form.module.css";
function LoginForm({ className }) {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
async function handleSubmit(e) {
e.preventDefault();
await login(email, password);
getUser();
}
return (
<div className={`${className} ${"glassCard"}`}>
<div className={styles.formgroup}>
<input type="email" id="email" value={email} placeholder="Adresse mail" onChange={e => setEmail(e.target.value)} required/>
</div>
<div className={styles.formgroup}>
<input type="password" id="password" value={password} placeholder="Mot de passe" onChange={e => setPassword(e.target.value)} required/>
<br/>
</div>
<div className={styles.logButton} >
<button className={styles.loginButton} onClick={e => handleSubmit(e)}>Se connecter</button>
</div>
</div>
);
}
export default LoginForm;*/
import { useState } from "react";
import login from "../../utils/login.js";
import getUser from "../../utils/getUser.js";
import styles from "./form.module.css";
function LoginForm({ className }) {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [showPassword, setShowPassword] = useState(false);
async function handleSubmit(e) {
e.preventDefault();
await login(email, password);
getUser();
}
const toggleShowPassword = () => {
setShowPassword(!showPassword);
};
return (
<div className={`${className} ${"glassCard"}`}>
<div className={styles.formgroup}>
<input
type="email"
id="email"
className={styles.input}
value={email}
placeholder="Adresse mail"
onChange={e => setEmail(e.target.value)}
required
/>
</div>
<div className={styles.formgroup}>
<div style={{ position: 'relative', width: '100%' }}>
<input
type={showPassword ? "text" : "password"}
id="password"
className={styles.input}
value={password}
placeholder="Mot de passe"
onChange={e => setPassword(e.target.value)}
required
style={{ width: '100%', paddingRight: '40px' }}
/>
<button
type="button"
onClick={toggleShowPassword}
style={{
position: 'absolute',
width: '30px',
right: '10px',
top: '50%',
transform: 'translateY(-50%)',
background: 'none',
border: 'none',
cursor: 'pointer',
fontSize: '16px',
}}
>
{showPassword ? '👁️' : '🔒'}
</button>
</div>
</div>
<div className={styles.logButton}>
<button className={styles.loginButton} onClick={handleSubmit}>Se connecter</button>
</div>
</div>
);
}
export default LoginForm;
+45
View File
@@ -0,0 +1,45 @@
import { useState, useContext } from "react";
import styles from "./loginForm.module.css";
import Button from "../ui/button/button.jsx";
import Input from "../ui/input/input.jsx";
import { useNavigate } from "react-router";
import { AuthContext } from "../../contexts/auth/AuthContext.js";
function LoginForm({ className }) {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
let navigate = useNavigate();
const { login } = useContext(AuthContext);
async function handleSubmit(e) {
e.preventDefault();
await login(email, password);
navigate("/");
}
return (
<div className={`${className} glassCard ${styles.inputContainer}`}>
<div className={styles.formgroup}>
<Input
type="email"
value={email}
placeholder="Adresse mail"
onChange={e => setEmail(e.target.value)}
style={{ width: '100%' }}
/>
</div>
<div className={styles.formgroup}>
<Input password={true} onChange={e => setPassword(e.target.value)} value={password} placeholder={"Mot de passe"} />
</div>
<div className={styles.logButton}>
<Button variant={"transparent"} onClick={() => navigate("/register")}>Vous n'avez pas de compte ?</Button>
<Button onClick={handleSubmit} variant={"default"}> Se connecter </Button>
</div>
</div>
);
}
export default LoginForm;
@@ -18,14 +18,21 @@ input::placeholder{
font-size: 20px; font-size: 20px;
color: rgba(0,0,0,0.45); color: rgba(0,0,0,0.45);
} }
.formgroup{
margin-top:23px;
}
.logButton{ .logButton{
display: flex; display: flex;
justify-content: center; justify-content: center;
flex-direction: column;
align-items: center; align-items: center;
margin-top: 12px; margin-top: 12px;
} }
.formgroup{
margin-top:23px; .inputContainer {
padding: 15px;
} }
/* style scoped (module) */ /* style scoped (module) */
+2 -2
View File
@@ -1,6 +1,6 @@
import styles from "./button.module.css"; import styles from "./button.module.css";
const Button = ({ children, variant = "primary", onClick }) => { const Button = ({ children, variant = "primary", onClick, className }) => {
let btnStyle; let btnStyle;
if (variant === "primary") btnStyle = styles.primary; if (variant === "primary") btnStyle = styles.primary;
@@ -8,7 +8,7 @@ const Button = ({ children, variant = "primary", onClick }) => {
else if(variant === "transparent") btnStyle = styles.transparent; else if(variant === "transparent") btnStyle = styles.transparent;
else btnStyle = styles.default; else btnStyle = styles.default;
return <div className={`${styles.btn} ${btnStyle}`} onClick={onClick}> return <div className={`${styles.btn} ${btnStyle} ${className}`} onClick={onClick}>
{children} {children}
</div>; </div>;
}; };
+27 -2
View File
@@ -1,13 +1,38 @@
import styles from "./input.module.css"; import styles from "./input.module.css";
import { useState } from "react";
const TextInput = ({ placeholder, onChange, value, borderStyle, ...props }) => { const TextInput = ({ placeholder, onChange, value, borderStyle, password, className, ...props }) => {
const [showPassword, setShowPassword] = useState(false);
let inputBorderStyle = styles.square; let inputBorderStyle = styles.square;
if (borderStyle === "square") inputBorderStyle = styles.square; if (borderStyle === "square") inputBorderStyle = styles.square;
if(borderStyle === "rounded") inputBorderStyle = styles.rounded; if(borderStyle === "rounded") inputBorderStyle = styles.rounded;
return <input className={`${styles.input} ${inputBorderStyle}`} onChange={onChange} placeholder={placeholder} value={value} {...props} /> const toggleShowPassword = () => {
setShowPassword(!showPassword);
};
if(password) return (
<div className={styles.passwordContainer}>
<TextInput
type={showPassword ? "text" : "password"}
value={value}
placeholder={placeholder}
onChange={onChange}
className={styles.passwordInput}
/>
<button
type="button"
onClick={toggleShowPassword}
className={styles.passwordBtn}
>
{showPassword ? '👁️' : '🔒'}
</button>
</div>
);
return <input className={`${styles.input} ${inputBorderStyle} ${className}`} onChange={onChange} placeholder={placeholder} value={value} {...props} />
}; };
export default TextInput; export default TextInput;
+20
View File
@@ -30,3 +30,23 @@
border-radius: 12px; border-radius: 12px;
} }
.passwordContainer {
position: relative;
width: 100%;
}
.passwordInput {
width: 100%;
}
.passwordBtn {
position: absolute;
width: 30px;
right: 10px;
top: 50%;
transform: translateY(-50%);
background: none;
border: none;
cursor: pointer;
font-size: 16px;
}
+31
View File
@@ -0,0 +1,31 @@
import { createPortal } from "react-dom";
import styles from "./modal.module.css";
import Button from "../button/button.jsx";
const Modal = ({ open, onClose, children, title }) => {
if (!open) return null;
const handleOverlayClick = () => {
onClose();
};
const handleModalClick = (e) => {
e.stopPropagation();
};
return createPortal(
<div className={styles.overlay} onClick={handleOverlayClick}>
<div className={styles.modal} onClick={handleModalClick}>
{title && <h2>{title}</h2>}
{children}
<div className={styles.modalFooter}>
<Button onClick={onClose}>Fermer</Button>
</div>
</div>
</div>,
document.body
);
};
export default Modal;
+38
View File
@@ -0,0 +1,38 @@
.overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.25);
display: flex;
justify-content: center;
z-index: 1000;
align-items: flex-start;
overflow-y: auto;
padding: 40px 0;
}
.modal {
padding: 20px 20px 20px 20px;
border-radius: 8px;
min-width: 300px;
max-width: 70%;
max-height: none;
position: relative;
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
width: clamp(300px, 50vw, 600px);
border: 1px solid rgba(200, 200, 200, 0.3);
background: rgba(255, 255, 255, 0.5);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
color: #000;
outline: none;
}
.modalFooter {
display: flex;
align-items: center;
justify-content: center;
margin-top: 20px;
}
@@ -0,0 +1,32 @@
import Button from "../button/button.jsx";
import { useState, useEffect } from "react";
import styles from "./ThemeSwitcher.module.css";
export default function ThemeSwitcher() {
const [theme, setTheme] = useState("");
useEffect(() => {
const t = localStorage.getItem("theme");
if(t) setTheme(t);
else setTheme("light");
}, []);
const switchTheme = () => {
let newTheme = theme === "light" ? "dark" : "light";
if(theme === null || theme === "") newTheme = "dark";
localStorage.setItem("theme", newTheme);
setTheme(newTheme);
window.dispatchEvent(new Event("theme-changed"));
};
return <Button variant={"default"} onClick={switchTheme} className={styles.themeBtn}>
<img src={`/icons/theme/${theme}.svg`} alt={`${theme} icon`} className={styles.themeIcon} />
<p>Changer de thème </p>
</Button>
}
@@ -0,0 +1,12 @@
.themeBtn {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
gap: 5px;
}
.themeIcon {
height: 20px;
width: 20px;
}
+3
View File
@@ -0,0 +1,3 @@
import { createContext } from "react";
export const AuthContext = createContext(null);
+43
View File
@@ -0,0 +1,43 @@
import { useState, useEffect } from "react";
import loginFunction from "../../utils/login.js";
import getUser from "../../utils/getUser.js";
import { AuthContext } from "./AuthContext";
import logoutFunction from "../../utils/auth/logout.js"
export function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
(async () => {
const u = await getUser();
setUser(u);
setLoading(false);
})();
}, []);
const login = async (email, password) => {
const res = await loginFunction(email, password);
if (!res || res.status !== 200) throw new Error("Login error");
const u = await getUser();
setUser(u);
return res;
};
const logout = async () => {
await logoutFunction();
setUser(null);
};
const update = async () => {
const u = await getUser();
setUser(u);
}
return (
<AuthContext.Provider value={{ user, loading, login, logout, update }}>
{children}
</AuthContext.Provider>
);
}
+9 -5
View File
@@ -1,8 +1,10 @@
* { * {
margin: 0; margin: 0;
padding: 0; padding: 0;
width: 100%;
box-sizing: border-box; box-sizing: border-box;
}
html {
overflow-x: hidden; overflow-x: hidden;
} }
@@ -17,16 +19,19 @@
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }
@media screen and (max-width: 768px) {
.content{
padding: 0.7rem;
}
}
.content { .content {
padding: 1.5rem; padding: 1.5rem;
z-index: 1; z-index: 1;
width: 100%;
} }
a { a {
font-weight: 500; font-weight: 500;
color: #646cff; color: #646cff;
@@ -45,7 +50,6 @@ body {
/*.glassCard { /*.glassCard {
width: 100%; width: 100%;
padding: 10px;
height: fit-content; height: fit-content;
background: rgba(255, 255, 255, 0.65); background: rgba(255, 255, 255, 0.65);
backdrop-filter: blur(3px); backdrop-filter: blur(3px);
+8
View File
@@ -2,10 +2,18 @@ import { Outlet } from "react-router";
import Footer from "./components/Footer/Footer.jsx" import Footer from "./components/Footer/Footer.jsx"
import Header from "./components/Header/Header.jsx"; import Header from "./components/Header/Header.jsx";
import Background from "./components/background/background.jsx"; import Background from "./components/background/background.jsx";
import { Navigate } from "react-router";
import { useContext} from "react";
import { AuthContext } from "./contexts/auth/AuthContext.js";
function Layout(){ function Layout(){
const { user, loading } = useContext(AuthContext);
if (loading) return <p>Loading</p>;
if (!user) return <Navigate to="/login" />;
return ( return (
<div> <div>
<Background> <Background>
+4 -1
View File
@@ -3,10 +3,13 @@ import { createRoot } from 'react-dom/client'
import './index.css' import './index.css'
import { RouterProvider } from "react-router"; import { RouterProvider } from "react-router";
import router from './router'; import router from './router';
import { AuthProvider } from "./contexts/auth/authProvider.jsx";
createRoot(document.getElementById('root')).render( createRoot(document.getElementById('root')).render(
<StrictMode> <StrictMode>
<RouterProvider router={router} /> <AuthProvider>
<RouterProvider router={router} />
</AuthProvider>
</StrictMode>, </StrictMode>,
) )
+40
View File
@@ -0,0 +1,40 @@
import { useEffect, useState } from "react";
import Event from "../../components/Event/Event.jsx";
import styles from "./EventsPage.module.css";
import ToolBar from "../../components/ToolBar/ToolBar.jsx";
import getAllEvents from "../../utils/event/getAllEvents.js";
import filter from "../../utils/filter.js";
function EventsPage(){
const [filters, setFilters] = useState({
alphabetical: "asc",
yearOrder: "desc",
});
const [events, setEvents] = useState([]);
useEffect(() => {
(async () => {
const data = await getAllEvents();
setEvents(data.data);
}) ()
}, []);
return (
<div className={styles.eventsContainer}>
<ToolBar setFilters={setFilters} filters={filters}/>
<div className={styles.eventsList}>
{filter(events, filters, "event").map((event, index) => (
<Event key={index} event={event} />
))}
</div>
</div>
)
}
export default EventsPage;
+11
View File
@@ -0,0 +1,11 @@
.eventsContainer {
width: 100%;
position: relative;
}
.eventsList{
display: flex;
align-items: center;
flex-direction: column;
}
+173 -3
View File
@@ -1,11 +1,181 @@
import { useState } from "react";
import { useMemo } from "react";
import styles from "./HomePage.module.css";
function HomePage() {
const events = {
"2025-11-07": ["Réunion de projet", "Anniversaire de Marie"],
"2025-11-10": ["Cours de React", "Rendez-vous médecin"],
"2025-11-14": ["Sortie cinéma"],
};
function HomePage(){ const monthNames = [
"Janvier", "Février", "Mars", "Avril", "Mai", "Juin",
"Juillet", "Août", "Septembre", "Octobre", "Novembre", "Décembre"
];
const today = new Date();
const [currentMonth, setCurrentMonth] = useState(today.getMonth());
const [currentYear, setCurrentYear] = useState(today.getFullYear());
const [selectedDay, setSelectedDay] = useState(() => {
const year = today.getFullYear();
const month = String(today.getMonth() + 1).padStart(2, "0");
const day = String(today.getDate()).padStart(2, "0");
const key = `${year}-${month}-${day}`;
return { date: key, events: events[key] || [] };
});
const daysInMonth = new Date(currentYear, currentMonth + 1, 0).getDate();
const firstDay = new Date(currentYear, currentMonth, 1).getDay();
const startDay = (firstDay + 6) % 7;
const days = [];
for (let i = 0; i < startDay; i++) days.push(null);
for (let i = 1; i <= daysInMonth; i++) days.push(i);
const handlePrevMonth = () => {
let newMonth = currentMonth - 1;
let newYear = currentYear;
if (newMonth < 0) {
newMonth = 11;
newYear--;
}
setCurrentMonth(newMonth);
setCurrentYear(newYear);
const key = `${newYear}-${String(newMonth + 1).padStart(2, "0")}-01`;
setSelectedDay({ date: key, events: events[key] || [] });
};
const handleNextMonth = () => {
let newMonth = currentMonth + 1;
let newYear = currentYear;
if (newMonth > 11) {
newMonth = 0;
newYear++;
}
setCurrentMonth(newMonth);
setCurrentYear(newYear);
const key = `${newYear}-${String(newMonth + 1).padStart(2, "0")}-01`;
setSelectedDay({ date: key, events: events[key] || [] });
};
const handleDayClick = (day) => {
if (!day) return;
const key = `${currentYear}-${String(currentMonth + 1).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
setSelectedDay({ date: key, events: events[key] || [] });
};
const sortedEvents = useMemo(() => {
return Object.entries(events)
.map(([dateKey, eventList]) => ({ date: dateKey, events: eventList }))
.sort((a, b) => new Date(a.date) - new Date(b.date));
}, [events]);
const formatDate = (dateString) => {
const [year, month, day] = dateString.split('-');
return `${day} ${monthNames[parseInt(month) - 1]} ${year}`;
};
return ( return (
<div> <div className={styles.allContent}>
<div className={styles.glassCard}>
<div className={styles.calendarContainer}>
<div className={styles.calendarHeader}>
<button onClick={handlePrevMonth}>◀</button>
<p>{monthNames[currentMonth]} {currentYear}</p>
<button onClick={handleNextMonth}>▶</button>
</div>
<table className={styles.calendar}>
<thead>
<tr>
<th>Lun</th>
<th>Mar</th>
<th>Mer</th>
<th>Jeu</th>
<th>Ven</th>
<th>Sam</th>
<th>Dim</th>
</tr>
</thead>
<tbody>
{Array.from({ length: Math.ceil(days.length / 7) }).map((_, weekIndex) => (
<tr key={weekIndex}>
{days.slice(weekIndex * 7, weekIndex * 7 + 7).map((day, index) => {
const key = `${currentYear}-${String(currentMonth + 1).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
const hasEvent = events[key];
return (
<td
key={index}
className={[
day === today.getDate() &&
currentMonth === today.getMonth() &&
currentYear === today.getFullYear()
? styles.today : "",
hasEvent ? styles.hasEvent : "",
selectedDay?.date === key ? styles.selected : ""
].join(" ")}
onClick={() => handleDayClick(day)}
>
{day || ""}
</td>
);
})}
</tr>
))}
</tbody>
</table>
{selectedDay && (
<div className={styles.eventBox}>
<h3>Événements du {formatDate(selectedDay.date)}</h3>
{selectedDay.events.length > 0 ? (
<ul>
{selectedDay.events.map((ev, i) => (
<li key={i}>{ev}</li>
))}
</ul>
) : (
<p>Aucun événement ce jour-là.</p>
)}
</div>
)}
</div>
</div>
<div className={styles.glassCard}>
<div className={styles.eventListContainer}>
<h2>Liste des Événements à Venir</h2>
{sortedEvents.length > 0 ? (
<ul className={styles.eventList}>
{sortedEvents.map((eventGroup) => (
<li key={eventGroup.date} className={styles.eventGroup}>
<div className={styles.eventDate}>
<strong>{formatDate(eventGroup.date)}</strong>
</div>
<ul className={styles.eventDetails}>
{eventGroup.events.map((event, i) => (
<li key={i}>- {event}</li>
))}
</ul>
</li>
))}
</ul>
) : (
<p>Aucun événement planifié pour l'instant.</p>
)}
</div>
</div>
</div> </div>
) );
} }
export default HomePage; export default HomePage;
+172 -2
View File
@@ -1,3 +1,173 @@
.title { .calendarContainer {
color: red; padding: 20px;
font-family: Arial, sans-serif;
display: flex;
flex-direction: column;
align-items: center;
}
.calendarHeader {
display: flex;
width: 100%;
flex-direction: row;
justify-content: space-between;
align-items: center;
gap: 10px;
}
.calendarHeader p {
width: auto;
text-align: center;
font-weight: bold;
font-size: 40px;
}
.calendarHeader button {
top: 50%;
left: 50%;
width: 40px;
height: 40px;
border-radius: 50%;
border: none;
background: transparent;
box-shadow:
inset 1.25px 1.25px 1px rgba(255, 255, 255, 1),
inset -1.25px -1.25px 1px rgba(255, 255, 255, 1),
inset -0.25px 0.25px 1px rgba(0, 0, 0, 0.2),
inset 0.25px -0.25px 1px rgba(0, 0, 0, 0.2);
font-size: 15px;
color: #019AFF;
cursor: pointer;
}
.calendar {
border-collapse: collapse;
width: 80%;
max-width: 500px;
}
.calendar th, .calendar td {
border: none;
padding: 10px;
width: 14.2%;
line-height: 130%;
text-align: center;
cursor: pointer;
}
.today {
color: #007bff;
}
.hasEvent {
position: relative;
}
.hasEvent::before {
content: "";
position: absolute;
left: 50%;
bottom: 10px;
width: 20px;
height: 2px;
background-color: #007bff;
transform: translateX(-50%);
border-radius: 10px;
}
.hasEvent.selected::before {
width: 25px;
height: 2px;
bottom: 10px;
}
.selected {
position: relative;
font-size: 18px;
font-weight: 600;
}
.selected::after {
content: "";
position: absolute;
top: 50%;
left: 50%;
width: 40px;
height: 40px;
transform: translate(-50%, -50%);
border-radius: 38%;
pointer-events: none;
background: transparent;
box-shadow:
inset 1.25px 1.25px 1px rgba(255, 255, 255, 1),
inset -1.25px -1.25px 1px rgba(255, 255, 255, 1),
inset -0.25px 0.25px 1px rgba(0, 0, 0, 0.2),
inset 0.25px -0.25px 1px rgba(0, 0, 0, 0.2);
}
.eventBox {
border-radius: 20px;
padding: 15px;
margin-top: 16px;
border: none;
background: rgb(255, 255, 255, 0.25);
box-shadow:
inset 1.25px 1.25px 1px rgba(255, 255, 255, 1),
inset -1.25px -1.25px 1px rgba(255, 255, 255, 1),
inset -0.25px 0.25px 1px rgba(0, 0, 0, 0.2),
inset 0.25px -0.25px 1px rgba(0, 0, 0, 0.2);
}
.eventBox ul {
list-style-type: disc;
}
.eventGroup{
border-radius: 20px;
padding: 15px;
margin-top: 16px;
border: none;
background: rgb(255, 255, 255, 0.25);
box-shadow:
inset 1.25px 1.25px 1px rgba(255, 255, 255, 1),
inset -1.25px -1.25px 1px rgba(255, 255, 255, 1),
inset -0.25px 0.25px 1px rgba(0, 0, 0, 0.2),
inset 0.25px -0.25px 1px rgba(0, 0, 0, 0.2);
}
.glassCard {
width: 45%;
margin: 16px;
padding: 10px 20px 10px 20px;
height: fit-content;
background: rgba(255, 255, 255, 0.65);
backdrop-filter: blur(3px);
-webkit-backdrop-filter: blur(3px);
border-radius: 20px;
border: 1px solid rgba(255, 255, 255, 0.3);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1),
inset 0 1px 0 rgba(255, 255, 255, 0.5),
inset 0 -1px 0 rgba(255, 255, 255, 0.1),
inset 0 0 8px 4px rgba(255, 255, 255, 0.4);
overflow: hidden;
}
.allContent{
display: flex;
flex-direction: row;
}
@media (max-width: 768px) {
.glassCard {
width: 100%;
margin: 0px;
margin-top: 16px;
margin-bottom: 16px;
display: flex;
}
.allContent{
display: flex;
flex-direction: column;
}
} }
+23 -8
View File
@@ -1,14 +1,29 @@
import styles from "./LoginPage.module.css"; import styles from "./LoginPage.module.css";
import LoginForm from "../../components/form/form.jsx"; import LoginForm from "../../components/loginform/loginForm.jsx";
import Background from "../../components/background/background.jsx";
import { useEffect, useContext } from "react";
import { AuthContext } from "../../contexts/auth/AuthContext.js";
import { useNavigate } from "react-router";
function LoginPage() { function LoginPage() {
return (
<div className={styles.container}> const { user, loading } = useContext(AuthContext)
<LoginForm /> const navigate = useNavigate();
</div>
); useEffect(() => {
if (!loading && user) {
navigate("/");
}
}, [loading, user]);
return (
<Background>
<div className={styles.container}>
<h1 className={styles.title}>Connexion</h1>
<LoginForm />
</div>
</Background>
);
} }
export default LoginPage; export default LoginPage;
+13 -5
View File
@@ -1,15 +1,23 @@
.container { .container {
position: relative; position: absolute;
z-index: 2; z-index: 2;
display: flex; display: flex;
flex-direction: column;
width: 600px; width: 600px;
overflow: hidden;
max-height: 80%; max-height: 80%;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
padding: 20px; padding: 20px;
border-radius:12px; border-radius: 12px;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
} }
.title {
font-size: 36px;
color: white;
text-align: center;
margin-bottom: 20px;
text-shadow: 0 2px 8px rgba(0,0,0,0.3);
}
+55
View File
@@ -0,0 +1,55 @@
import React, { useContext } from "react";
import styles from "./ProfilePage.module.css";
import { AuthContext } from "../../contexts/auth/AuthContext.js";
import formatDate from "../../utils/date/formatDate.js";
import Task from "../../components/Task/Task";
import SettingsModal from "../../components/SettingsModal/SettingsModal.jsx";
function ProfilePage() {
const { user } = useContext(AuthContext);
return (
<div className={styles.container}>
<div className={`${styles.profileboard} glassCard`}>
<div className={styles.contentWrapper}>
<div className={styles.profilePictureContainer}>
<img
src="/react.svg"
alt="Photo de profil"
className={styles.profilePicture}
/>
</div>
<div className={styles.description}>
<div className={styles.headerProfile}>
<h2> {user.name} {user.lastname} </h2>
</div>
<div className={styles.topDescription}>
<div className={styles.infoBlock}>
<p><strong>Role :</strong> {user.role}</p>
<p><strong>Membre depuis :</strong> {formatDate(user.created_at)}</p>
</div>
<div className={styles.infoBlock}>
<p><strong>Mail :</strong> {user.email} </p>
<p><strong>Téléphone :</strong> {user.phone}</p>
</div>
<SettingsModal />
</div>
<h2>Vos Événements :</h2>
{user.tasks && user.tasks.length > 0 ? (
user.tasks.map((task, index) => (
<Task key={index} task={task} />
))
) : (
<p>Aucune tâche pour le moment.</p>
)}
</div>
</div>
</div>
</div>
);
}
export default ProfilePage;
+164
View File
@@ -0,0 +1,164 @@
.container {
width: 100%;
padding: 10px;
box-sizing: border-box;
}
.profileboard {
width: 100%;
padding: 15px;
border-radius: 20px;
box-sizing: border-box;
}
.contentWrapper {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
}
.profilePictureContainer {
width: 120px;
height: 120px;
margin-bottom: 15px;
}
.profilePicture {
width: 100%;
height: 100%;
border-radius: 50%;
object-fit: cover;
}
.description {
width: 100%;
text-align: left;
margin-left:4%;
}
.topDescription {
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
margin-bottom: 15px;
}
.infoBlock {
width: 100%;
background: rgba(255, 255, 255, 0.1);
padding: 10px;
border-radius: 5px;
}
.eventTasks {
max-height: 0;
overflow: hidden;
transition: max-height 0.3s ease-out, opacity 0.2s ease;
opacity: 0;
padding-left: 20px;
}
.eventTasks.expanded {
max-height: 500px;
opacity: 1;
margin-top: 10px;
}
.description h2{
font-size:30px;
}
.profilePicture{
width:90%;
}
.settingsBtns {
display: flex;
gap: 5px;
justify-content: right;
}
@media screen and (min-width: 768px) {
.contentWrapper {
flex-direction: row;
justify-content: flex-start;
align-items: flex-start;
}
.profilePictureContainer {
width: 150px;
height: 150px;
margin-right: 20px;
margin-bottom: 0;
}
.description {
width: calc(100% - 170px);
margin-left:4%;
}
.topDescription {
flex-direction: row;
justify-content: space-between;
}
.infoBlock {
width: 48%;
}
.event {
padding: 15px;
}
.description h2{
font-size:30px;
}
.profilePicture{
width:90%;
}
}
.description button{
width:50px;
height:50px;
}
.headerProfile {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
/* Desktop (1024px et plus) */
@media screen and (min-width: 1024px) {
.container {
max-width: 100%;
margin: 0 auto;
padding: 20px;
}
.profileboard {
padding: 25px;
}
.profilePictureContainer {
width: 30%;
height:auto;
}
.profilePicture{
width:90%;
}
.description {
width: calc(100% - 200px);
margin-left:4%;
}
.event {
padding: 18px;
}
.description h2{
font-size:30px;
}
}
+134
View File
@@ -0,0 +1,134 @@
import styles from "./RegisterPage.module.css";
import Background from "../../components/background/background.jsx";
import Input from "../../components/ui/input/input.jsx";
import Button from "../../components/ui/button/button.jsx";
import {useContext, useEffect, useState} from "react";
import register from "../../utils/register.js";
import { useNavigate } from "react-router";
import { AuthContext } from "../../contexts/auth/AuthContext.js";
import Modal from "../../components/ui/modal/modal.jsx";
function RegisterPage() {
const navigate = useNavigate();
const [email, setEmail] = useState("");
const [phone, setPhone] = useState("");
const [password, setPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState("");
const [name, setName] = useState("");
const [lastName, setLastName] = useState("");
const [open, setOpen] = useState(false);
const [title, setTitle] = useState("");
const [message, setMessage] = useState("");
const { user, loading } = useContext(AuthContext)
useEffect(() => {
if (!loading && user) {
navigate("/");
}
}, [loading, user]);
const handleModal = (t, m) => {
setTitle(t)
setMessage(m)
setOpen(true);
}
async function handleSubmit(e) {
e.preventDefault();
if(!(password === confirmPassword)) handleModal("Mot de passe invalide", "Les mots de passes ne conrrespondent pas");
else if(password.length < 8) handleModal("Mot de passe invalide", "Le mot de passe doit faire plus de 8 charactères");
else {
const res = await register(email, password, name, lastName, phone);
if (res.status === 422) {
if (res.errors.email) handleModal("Adresse Email invalide", "Le formet de votre adresse Email n'est pas valide");
if (res.errors.phone) handleModal("Numéro de téléphone invalide", "Le formet de votre numéro de téléphone n'est pas valide");
} else navigate("/login");
}
}
const handlePhone = (e) => {
const value = e.target.value;
if (value === "" || /^[0-9 +]+$/.test(value)) {
setPhone(value);
}
};
return <Background>
<div className={styles.container}>
<h1 className={styles.title}>Créer un compte</h1>
<div className={`glassCard ${styles.inputContainer}`}>
<div className={styles.formGroup}>
<Input
value={name}
placeholder="Prénom"
onChange={e => setName(e.target.value)}
style={{ width: '100%' }}
/>
</div>
<div className={styles.formGroup}>
<Input
value={lastName}
placeholder="Nom"
onChange={e => setLastName(e.target.value)}
style={{ width: '100%' }}
/>
</div>
<div className={styles.formGroup}>
<Input
type="email"
value={email}
placeholder="Adresse mail"
onChange={e => setEmail(e.target.value)}
style={{ width: '100%' }}
/>
</div>
<div className={styles.formGroup}>
<Input
value={phone}
placeholder="Numéro de téléphone"
onChange={handlePhone}
style={{ width: '100%' }}
/>
</div>
<div className={styles.formGroup}>
<Input password={true} onChange={e => setPassword(e.target.value)} value={password} placeholder={"Mot de passe"}/>
</div>
<div className={styles.formGroup}>
<Input password={true} onChange={e => setConfirmPassword(e.target.value)} value={confirmPassword} placeholder={"Confirmer le mot de passe"} />
</div>
<div className={styles.logButton}>
<Button variant={"transparent"} onClick={() => navigate("/login")}>Vous avez déjà un compte ?</Button>
<Button variant={"default"} onClick={handleSubmit}> Se connecter </Button>
</div>
</div>
</div>
<Modal open={open} onClose={() => setOpen(false)} title={title}>
<p>{message}</p>
</Modal>
</Background>
}
export default RegisterPage;
@@ -0,0 +1,43 @@
.container {
position: relative;
z-index: 2;
display: flex;
flex-direction: column;
width: 30%;
justify-content: center;
align-items: center;
padding: 20px;
border-radius:12px;
}
.title {
font-size: 36px;
color: white;
text-align: center;
margin-bottom: 20px;
text-shadow: 0 2px 8px rgba(0,0,0,0.3);
}
:global(.glassCard) {
--glass-bg: #ffffff9f;
}
.logButton{
display: flex;
justify-content: center;
flex-direction: column;
align-items: center;
margin-top: 12px;
}
.formGroup{
margin-top:23px;
}
h1 {
overflow: visible;
text-align: center;
}
.inputContainer {
padding: 15px;
}
-1
View File
@@ -1,6 +1,5 @@
import React, { useState } from "react"; import React, { useState } from "react";
import styles from "./StatPage.module.css"; import styles from "./StatPage.module.css";
// Importez les composants avec une majuscule
import ParticipationChart from "../../components/chart/ParticipationChart.jsx"; import ParticipationChart from "../../components/chart/ParticipationChart.jsx";
import IncompleteEvents from "../../components/IncompleteEvent/IncompleteEvent.jsx"; import IncompleteEvents from "../../components/IncompleteEvent/IncompleteEvent.jsx";
import PendingMembers from "../../components/PendingMembers/PendingMembers.jsx"; import PendingMembers from "../../components/PendingMembers/PendingMembers.jsx";
+74
View File
@@ -0,0 +1,74 @@
import React, {useState} from "react";
import styles from "./VolunteersPage.module.css";
import formatDate from "../../utils/date/formatDate.js";
import { useNavigate } from "react-router";
import Button from "../../components/ui/button/button.jsx";
export default function VolunteerCard({ user }) {
const [isExpanded, setIsExpanded] = useState(false);
const toggleExpand = () => {
setIsExpanded(!isExpanded);
};
const navigate = useNavigate();
return (
<div className={`${styles.profileboard} glassCard`}>
<div className={styles.header}>
<h2>{user?.name} {user?.lastname}</h2>
<Button
variant={"transparent"}
onClick={toggleExpand}
className={`${isExpanded ? styles.moreButtonClicked : ""}`}
>
{">"}
</Button>
</div>
<div className={styles.mainContent}>
{!isExpanded ? (
<div className={styles.basicInfo}>
<p><strong>Rôle :</strong> {user?.role}</p>
<p><strong>Membre depuis :</strong> {user?.created_at && formatDate(user.created_at)}</p>
</div>
) : (
<div className={styles.expandedContent}>
<div className={styles.profilePictureContainer}>
<img src={"/react.svg"} alt="profile" className={styles.profilePicture} />
</div>
<div className={styles.textInfo}>
<div className={styles.basicInfo}>
<p><strong>Rôle :</strong> {user?.role}</p>
<p><strong>Membre depuis :</strong> {user?.created_at && formatDate(user.created_at)}</p>
</div>
<div className={styles.contactInfo}>
<p><strong>Email :</strong> {user?.email}</p>
{user?.phone && <p><strong>Téléphone :</strong> {user.phone}</p>}
</div>
</div>
</div>
)}
</div>
{isExpanded && (
<div className={styles.bottomBar}>
<Button
onClick={() => navigate(`/profile/${user.id}`)}
variant="primary"
>
Voir plus
</Button>
</div>
)}
</div>
);
}
+37
View File
@@ -0,0 +1,37 @@
import { useState, useEffect } from "react";
import styles from "./VolunteersPage.module.css";
import VolunteerCard from "./VolunteerCard.jsx";
import getAllUser from "../../utils/users/getAllUsers.js";
import ToolBar from "../../components/ToolBar/ToolBar.jsx";
import filter from "../../utils/filter.js";
import getUserById from "../../utils/users/getUserById.js";
function VolunteerPage() {
const [filters, setFilters] = useState({
alphabetical: "asc",
yearOrder: "desc",
});
const [users, setUsers] = useState([]);
useEffect(() => {
(async () => {
const ids = await getAllUser();
const usersData = await Promise.all(ids.map(id => getUserById(id)));
setUsers(usersData);
})()
}, [])
return (
<div className={styles.container}>
<ToolBar setFilters={setFilters} filters={filters}/>
<div className={styles.usersList}>
{filter(users, filters, "user").map((user, index) => (
<VolunteerCard key={index} user={user} />
))}
</div>
</div>
);
}
export default VolunteerPage;
@@ -0,0 +1,119 @@
.container {
width: 100%;
position: relative;
}
.profileboard {
width: 80%;
display: flex;
flex-direction: column;
gap: 10px;
box-sizing: border-box;
margin: 1rem 0;
padding: 1rem 0.5rem;
}
.usersList{
display: flex;
flex-direction: column;
align-items: center;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.header h2 {
margin: 0;
}
.mainContent {
display: flex;
flex-direction: row;
width: 100%;
flex: 1;
}
.basicInfo {
flex: 1;
}
.expandedContent {
display: flex;
flex-direction: row;
width: 100%;
}
.textInfo {
display: flex;
flex-direction: column;
flex: 1;
}
.contactInfo {
margin-top: 10px;
}
.contactInfo p {
margin: 5px 0;
}
.bottomBar {
display: flex;
justify-content: flex-end;
}
.profilePictureContainer {
width: 120px;
height: 120px;
margin-right: 20px;
}
.profilePicture {
width: 100%;
height: 100%;
border-radius: 50%;
object-fit: cover;
}
.expandButton {
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
background: transparent;
border: none;
font-size: 16px;
}
.moreButtonClicked {
rotate: 90deg;
}
@media screen and (min-width: 768px) {
.profilePictureContainer {
width: 150px;
height: 150px;
}
}
@media screen and (min-width: 1024px) {
.container {
padding: 20px;
}
.profileboard {
padding: 25px;
}
.profilePictureContainer {
width: 200px;
height: auto;
}
}
+27 -7
View File
@@ -3,9 +3,21 @@ import Layout from "./layout.jsx";
import HomePage from "./pages/Home/HomePage"; import HomePage from "./pages/Home/HomePage";
import LoginPage from "./pages/Login/LoginPage"; import LoginPage from "./pages/Login/LoginPage";
import StatPage from "./pages/Stat/StatPage"; import StatPage from "./pages/Stat/StatPage";
import testPage from "./pages/Stat/test.jsx" import RegisterPage from "./pages/Register/RegisterPage.jsx";
import VolunteersPage from "./pages/Volunteers/VolunteersPage";
import ProfilePage from "./pages/Profile/ProfilePage";
import EventsPage from "./pages/Events/EventsPage.jsx";
const router = createBrowserRouter([ const router = createBrowserRouter([
{
path: "/login",
Component: LoginPage,
},
{
path: "/register",
Component: RegisterPage,
},
{ {
path: "/", path: "/",
Component: Layout, Component: Layout,
@@ -15,17 +27,25 @@ const router = createBrowserRouter([
Component: HomePage, Component: HomePage,
}, },
{ {
path: "/login", path: "/admin",
Component: LoginPage, Component:StatPage,
},
{
path:"/profile",
Component:ProfilePage,
},
{
path: "/events",
Component: EventsPage,
},
{
path: "/volunteers",
Component: VolunteersPage,
}, },
{ {
path: "/stat", path: "/stat",
Component:StatPage, Component:StatPage,
}, },
{
path: "/test",
Component:testPage,
}
] ]
} }
]) ])
+49
View File
@@ -0,0 +1,49 @@
import Modal from "../components/ui/modal/modal.jsx";
import Button from "../components/ui/button/button.jsx";
import { useState } from "react";
export default {
title: 'UI/Modal',
component: Modal,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
argTypes: {
open: {
control: 'boolean',
description: 'Variable qui gère si la modal est ouverte ou fermée.',
},
children: {
control: Object,
description: 'Contenu de la modal.',
},
title: {
control: 'text',
description: 'Titre de la modal',
},
onClose: {
action: 'changed',
description: 'Fonction appelée pour fermer la modal.',
},
},
};
const Template = (args) => {
const [isOpen, setIsOpen] = useState(false);
return (
<div>
<Button onClick={() => setIsOpen(true)}>Ouvrir</Button>
<Modal {...args} open={isOpen} onClose={() => setIsOpen(false)}>
{args.children}
</Modal>
</div>
);
};
export const Default = Template.bind({});
Default.args = {
title: "Hello",
children: <p>Lorem Ipsum...</p>,
};
+28
View File
@@ -0,0 +1,28 @@
import getXSRFToken from "../getXSRF.js";
export default async function logout() {
const csrfToken = await getXSRFToken();
try {
const response = await fetch(`http://${import.meta.env.VITE_API_URL}/api/users/logout`, {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'X-XSRF-TOKEN': csrfToken
},
});
console.log('Code HTTP :', response.status);
if (!response.ok) {
throw new Error(`Erreur HTTP ${response.status}`);
}
return { status: response.status };
} catch (error) {
return error;
}
}
+9
View File
@@ -0,0 +1,9 @@
export default function formatDate(d) {
const date = new Date(d);
const jj = String(date.getUTCDate()).padStart(2, '0');
const mm = String(date.getUTCMonth() + 1).padStart(2, '0');
const aaaa = date.getUTCFullYear();
return `${jj}/${mm}/${aaaa}`;
}
+9
View File
@@ -0,0 +1,9 @@
import { expect, test } from 'vitest';
import formatDate from './formatDate';
const date = "2025-11-28T11:26:45.000000Z"
const res = "28/11/2025"
test("Format Date", () => {
expect(formatDate(date)).toBe(res);
})
+23
View File
@@ -0,0 +1,23 @@
export default async function getAllEvents() {
try {
const res = await fetch(`http://${import.meta.env.VITE_API_URL}/api/events/`, {
method: 'GET',
credentials: 'include',
headers: {
'Accept': 'application/json'
}
});
if (!res.ok) {
throw new Error(`Erreur serveur : ${res.status}`);
}
const data = await res.json();
return data;
} catch (err) {
console.error('Erreur :', err);
return null;
}
}
+23
View File
@@ -0,0 +1,23 @@
export default async function GetEventById(id) {
try {
const res = await fetch(`http://${import.meta.env.VITE_API_URL}/api/events/${id}`, {
method: 'GET',
credentials: 'include',
headers: {
'Accept': 'application/json'
}
});
if (!res.ok) {
throw new Error(`Erreur serveur : ${res.status}`);
}
const data = await res.json();
return data;
} catch (err) {
console.error('Erreur :', err);
return null;
}
}
+40
View File
@@ -0,0 +1,40 @@
function filter(elements, filters, type) {
const compareDate = (dateA, dateB) => {
const dA = new Date(dateA);
const dB = new Date(dateB);
if (filters.yearOrder === "asc") {
return dA - dB; // plus ancien -> plus récent
} else {
return dB - dA; // plus récent -> plus ancien
}
};
return [...elements].sort((a, b) => {
let timeComparison;
if (filters.yearOrder !== null) {
if (type === "event") {
timeComparison = compareDate(a.start, b.start);
}
else if (type === "user") {
timeComparison = compareDate(a.created_at, b.created_at);
}
if (timeComparison !== 0 && timeComparison !== undefined) {
return timeComparison;
}
}
if (filters.alphabetical !== null) {
return filters.alphabetical === "asc"
? a.name.localeCompare(b.name)
: b.name.localeCompare(a.name);
}
return 0;
});
}
export default filter;
+19 -10
View File
@@ -1,12 +1,21 @@
export default async function getUser() { export default async function getUser() {
await fetch('http://localhost:8000/api/me', { try {
method: 'GET', const res = await fetch(`http://${import.meta.env.VITE_API_URL}/api/users/me`, {
credentials: 'include', method: 'GET',
headers: { credentials: 'include',
'Accept': 'application/json' headers: {
'Accept': 'application/json'
}
});
if (!res.ok) {
throw new Error(`Erreur serveur : ${res.status}`);
} }
})
.then(res => res.json()) const data = await res.json();
.then(data => console.log('Utilisateur connecté :', data)) return data;
.catch(err => console.error('Erreur :', err)); } catch (err) {
} console.error('Erreur :', err);
return null;
}
}
+1 -1
View File
@@ -1,7 +1,7 @@
export default async function getUserNotifications() { export default async function getUserNotifications() {
try { try {
const response = await fetch( const response = await fetch(
'http://localhost:8000/api/users/1/notifications', 'http://localhost:80/api/users/1/notifications',
{ {
method: 'GET', method: 'GET',
credentials: 'include', credentials: 'include',
+1 -1
View File
@@ -1,5 +1,5 @@
export default async function getXSRFToken() { export default async function getXSRFToken() {
const response = await fetch('http://localhost:8000/sanctum/csrf-cookie', { const response = await fetch(`http://${import.meta.env.VITE_API_URL}/sanctum/csrf-cookie`, {
method: 'GET', method: 'GET',
credentials: 'include' credentials: 'include'
}); });
+25 -27
View File
@@ -3,31 +3,29 @@ import getXSRFToken from "./getXSRF.js";
export default async function login(email, password) { export default async function login(email, password) {
const csrfToken = await getXSRFToken(); const csrfToken = await getXSRFToken();
try {
await fetch('http://localhost:8000/api/users/login', { const response = await fetch(`http://${import.meta.env.VITE_API_URL}/api/users/login`, {
method: 'POST', method: 'POST',
credentials: 'include', credentials: 'include',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'X-XSRF-TOKEN': csrfToken, 'X-XSRF-TOKEN': csrfToken,
'Accept': 'application/json' 'Accept': 'application/json'
}, },
body: JSON.stringify({ body: JSON.stringify({ email, password })
email: email,
password: password
})
})
.then(response => {
console.log('Code HTTP :', response.status);
if (!response.ok) {
throw new Error(`Erreur HTTP ${response.status}`);
}
return response.json();
})
.then(data => {
console.log('Connexion réussie ! Données utilisateur :', data);
})
.catch(error => {
console.error('Erreur lors de la connexion :', error.message);
}); });
}
console.log('Code HTTP :', response.status);
if (!response.ok) {
throw new Error(`Erreur HTTP ${response.status}`);
}
const data = await response.json();
console.log('Connexion réussie ! Données utilisateur :', data);
return { status: response.status, data };
} catch (error) {
return error;
}
}
+36
View File
@@ -0,0 +1,36 @@
import getXSRFToken from "./getXSRF.js";
export default async function register(email, password, name, lastname, phone) {
const csrfToken = await getXSRFToken();
try {
const response = await fetch(`http://${import.meta.env.VITE_API_URL}/api/users/register`, {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
'X-XSRF-TOKEN': csrfToken,
'Accept': 'application/json'
},
body: JSON.stringify({ email, password, name, lastname, phone }),
});
const data = await response.json();
if (!response.ok) {
if (response.status === 422) {
return {
status: 422,
errors: data.errors
};
}
throw new Error(`Erreur HTTP ${response.status}`);
}
return { status: response.status, data };
} catch (error) {
return { status: "error", error };
}
}
+22
View File
@@ -0,0 +1,22 @@
export default async function searchEvents(query) {
try {
const encodedQuery = encodeURIComponent(query);
const response = await fetch(
`http://${import.meta.env.VITE_API_URL}/api/events/search?query=${encodedQuery}`,
{
method: 'GET',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
}
}
);
const data = await response.json();
return data.hits.map(hit => hit.document);
} catch (err) {
console.error("Erreur :", err);
return null;
}
}
+22
View File
@@ -0,0 +1,22 @@
export default async function searchUsers(query) {
try {
const encodedQuery = encodeURIComponent(query);
const response = await fetch(
`http://localhost:80/api/users/search?query=${encodedQuery}`,
{
method: 'GET',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
}
}
);
const data = await response.json();
return data.hits.map(hit => hit.document);
} catch (err) {
console.error("Erreur :", err);
return null;
}
}
+21
View File
@@ -0,0 +1,21 @@
export default async function getAllUser() {
try {
const res = await fetch(`http://${import.meta.env.VITE_API_URL}/api/users`, {
method: 'GET',
credentials: 'include',
headers: {
'Accept': 'application/json'
}
});
if (!res.ok) {
throw new Error(`Erreur serveur : ${res.status}`);
}
const data = await res.json();
return data;
} catch (err) {
console.error('Erreur :', err);
return null;
}
}
+20
View File
@@ -0,0 +1,20 @@
export default async function getUserById (id) {
try {
const res = await fetch(`http://${import.meta.env.VITE_API_URL}/api/users/${id}`, {
method: 'GET',
credentials: 'include',
headers: {
'Accept': 'application/json'
}
});
if (!res.ok) {
throw new Error(`Erreur serveur : ${res.status}`);
}
return await res.json();
} catch (err) {
console.error('Erreur :', err);
return null;
}
}
+32
View File
@@ -0,0 +1,32 @@
import getXSRFToken from "../getXSRF.js";
export default async function updateUser(name, lastname, phone) {
const csrfToken = await getXSRFToken();
try {
const res = await fetch(`http://${import.meta.env.VITE_API_URL}/api/users/update`, {
method: 'POST',
credentials: 'include',
headers: {
'Accept': 'application/json',
'X-XSRF-TOKEN': csrfToken,
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: name,
phone: phone,
lastname: lastname,
})
});
if (!res.ok) {
throw new Error(`Erreur serveur : ${res.status}`);
}
return await res.json();
} catch (err) {
console.error('Erreur :', err);
return null;
}
}
+43 -36
View File
@@ -1,41 +1,48 @@
// vite.config.js
/// <reference types="vitest/config" /> /// <reference types="vitest/config" />
import { defineConfig } from 'vite'; import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'; import react from '@vitejs/plugin-react'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'
import { playwright } from '@vitest/browser-playwright'
// https://vite.dev/config/
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { storybookTest } from '@storybook/addon-vitest/vitest-plugin';
import { playwright } from '@vitest/browser-playwright';
const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); const dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url));
// More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon
export default defineConfig({ export default defineConfig({
plugins: [react()], plugins: [react()],
test: { base: '/',
projects: [{ test: {
extends: true, projects: [
plugins: [ {
// The plugin will run tests for the stories defined in your Storybook config extends: true,
// See options at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon#storybooktest plugins: [
storybookTest({ storybookTest({
configDir: path.join(dirname, '.storybook') configDir: path.join(dirname, '.storybook'),
})], }),
test: { ],
name: 'storybook', test: {
browser: { name: 'storybook',
enabled: true, browser: {
headless: true, enabled: true,
provider: playwright({}), headless: true,
instances: [{ provider: playwright({}),
browser: 'chromium' instances: [{ browser: 'chromium' }],
}] },
}, setupFiles: ['.storybook/vitest.setup.js'],
setupFiles: ['.storybook/vitest.setup.js'] },
} },
}] {
}, test: {
server: { name: 'unit',
host: true, include: [
} 'src/**/*.test.js',
}); 'src/**/*.test.ts',
'utils/**/*.test.js',
],
environment: 'node',
},
},
],
},
})