refactor: move frontend in apps/frontend
No files matched your search
@@ -0,0 +1,11 @@
|
||||
VITE_API_URL=
|
||||
|
||||
BROADCASTER = reverb
|
||||
REVERB_KEY = local
|
||||
REVERB_HOST = 127.0.0.1
|
||||
REVERB_PORT = 8080
|
||||
FORCE_TLS = false
|
||||
DISABLE_STATS = true
|
||||
ENCRYPTED = false
|
||||
CLUSTER = mt1
|
||||
ENABLED_TRANSPORTS = [ws, wss]
|
||||
@@ -0,0 +1,29 @@
|
||||
.env
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
../../.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
*storybook.log
|
||||
storybook-static
|
||||
@@ -0,0 +1,20 @@
|
||||
|
||||
|
||||
/** @type { import('@storybook/react-vite').StorybookConfig } */
|
||||
const config = {
|
||||
"stories": [
|
||||
"../src/**/*.mdx",
|
||||
"../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"
|
||||
],
|
||||
"addons": [
|
||||
"@chromatic-com/storybook",
|
||||
"@storybook/addon-docs",
|
||||
"@storybook/addon-a11y",
|
||||
"@storybook/addon-vitest"
|
||||
],
|
||||
"framework": {
|
||||
"name": "@storybook/react-vite",
|
||||
"options": {}
|
||||
}
|
||||
};
|
||||
export default config;
|
||||
@@ -0,0 +1,20 @@
|
||||
/** @type { import('@storybook/react-vite').Preview } */
|
||||
const preview = {
|
||||
parameters: {
|
||||
controls: {
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
date: /Date$/i,
|
||||
},
|
||||
},
|
||||
|
||||
a11y: {
|
||||
// 'todo' - show a11y violations in the test UI only
|
||||
// 'error' - fail CI on a11y violations
|
||||
// 'off' - skip a11y checks entirely
|
||||
test: "todo"
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export default preview;
|
||||
@@ -0,0 +1,7 @@
|
||||
import * as a11yAddonAnnotations from "@storybook/addon-a11y/preview";
|
||||
import { setProjectAnnotations } from '@storybook/react-vite';
|
||||
import * as projectAnnotations from './preview.js';
|
||||
|
||||
// This is an important step to apply the right configuration when testing your stories.
|
||||
// More info at: https://storybook.js.org/docs/api/portable-stories/portable-stories-vitest#setprojectannotations
|
||||
setProjectAnnotations([a11yAddonAnnotations, projectAnnotations]);
|
||||
@@ -0,0 +1,16 @@
|
||||
# React + Vite
|
||||
|
||||
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||
|
||||
Currently, two official plugins are available:
|
||||
|
||||
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
|
||||
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
||||
|
||||
## React Compiler
|
||||
|
||||
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
|
||||
|
||||
## Expanding the ESLint configuration
|
||||
|
||||
If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project.
|
||||
@@ -0,0 +1,32 @@
|
||||
// For more info, see https://github.com/storybookjs/eslint-plugin-storybook#configuration-flat-config-format
|
||||
import storybook from "eslint-plugin-storybook";
|
||||
|
||||
import js from '@eslint/js'
|
||||
import globals from 'globals'
|
||||
import reactHooks from 'eslint-plugin-react-hooks'
|
||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||
import { defineConfig, globalIgnores } from 'eslint/config'
|
||||
|
||||
export default defineConfig([
|
||||
globalIgnores(['dist']),
|
||||
{
|
||||
files: ['**/*.{js,jsx}'],
|
||||
extends: [
|
||||
js.configs.recommended,
|
||||
reactHooks.configs['recommended-latest'],
|
||||
reactRefresh.configs.vite,
|
||||
],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
ecmaFeatures: { jsx: true },
|
||||
sourceType: 'module',
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
|
||||
},
|
||||
},
|
||||
])
|
||||
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="logoIcone.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Comité des fêtes de Beaupont</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/apps/frontend/srcs/frontend/src/main.jsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,57 @@
|
||||
{
|
||||
"name": "sae-front",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"lint": "eslint ../..",
|
||||
"preview": "vite preview",
|
||||
"storybook": "storybook dev -p 6006",
|
||||
"build-storybook": "storybook build",
|
||||
"test": "vitest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@emotion/styled": "^11.14.1",
|
||||
"@floating-ui/react": "^0.27.16",
|
||||
"@mui/material": "^7.3.6",
|
||||
"bootstrap": "^5.3.8",
|
||||
"chart.js": "^4.5.1",
|
||||
"laravel-echo": "^2.2.7",
|
||||
"pusher-js": "^8.4.0",
|
||||
"react": "^19.1.1",
|
||||
"react-bootstrap": "^2.10.10",
|
||||
"react-chartjs-2": "^5.3.1",
|
||||
"react-dom": "^19.1.1",
|
||||
"react-router": "^7.9.4",
|
||||
"react-router-dom": "^7.10.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@chromatic-com/storybook": "^4.1.2",
|
||||
"@eslint/js": "^9.36.0",
|
||||
"@storybook/addon-a11y": "^10.0.6",
|
||||
"@storybook/addon-docs": "^10.0.6",
|
||||
"@storybook/addon-vitest": "^10.0.6",
|
||||
"@storybook/react-vite": "^10.0.6",
|
||||
"@types/node": "^25.0.10",
|
||||
"@types/react": "^19.2.10",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^5.0.4",
|
||||
"@vitest/browser-playwright": "^4.0.8",
|
||||
"@vitest/coverage-v8": "^4.0.8",
|
||||
"baseline-browser-mapping": "^2.9.7",
|
||||
"eslint": "^9.36.0",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.22",
|
||||
"eslint-plugin-storybook": "^10.0.6",
|
||||
"globals": "^16.4.0",
|
||||
"playwright": "^1.56.1",
|
||||
"prop-types": "^15.8.1",
|
||||
"storybook": "^10.0.6",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^7.1.7",
|
||||
"vitest": "^4.0.8"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
<svg viewBox="0 0 60 60" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M34.325 52.5C33.8855 53.2577 33.2546 53.8866 32.4956 54.3238C31.7365 54.761 30.8759 54.9911 30 54.9911C29.1241 54.9911 28.2635 54.761 27.5044 54.3238C26.7454 53.8866 26.1145 53.2577 25.675 52.5M45 20C45 16.0218 43.4196 12.2064 40.6066 9.3934C37.7936 6.58035 33.9782 5 30 5C26.0218 5 22.2064 6.58035 19.3934 9.3934C16.5804 12.2064 15 16.0218 15 20C15 37.5 7.5 42.5 7.5 42.5H52.5C52.5 42.5 45 37.5 45 20Z" stroke="#1E1E1E" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 578 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#000000"><path d="m256-200-56-56 224-224-224-224 56-56 224 224 224-224 56 56-224 224 224 224-56 56-224-224-224 224Z"/></svg>
|
||||
|
After Width: | Height: | Size: 222 B |
|
After Width: | Height: | Size: 79 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#000000"><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>
|
||||
|
After Width: | Height: | Size: 290 B |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -0,0 +1,2 @@
|
||||
<?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 -8 72 72" id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg"><defs><style>.cls-1{fill:#010002;}</style></defs><title>tools</title><path class="cls-1" d="M13.32,44.31a4.5,4.5,0,0,0,6.36,6.36L34.44,35.91l-6.36-6.36Z"/><polygon class="cls-1" points="50.2 16.96 56.63 13.63 60 7.13 56.86 3.99 50.36 7.36 47.03 13.78 39.21 21.61 42.38 24.79 50.2 16.96"/><path class="cls-1" d="M51.61,34,51,34a8.9,8.9,0,0,0-3.11.58L29.45,16.09A8.9,8.9,0,0,0,30,13l-.06-.6A8.9,8.9,0,0,0,17.19,4.89l6,6a3,3,0,0,1,.68,1.08A3,3,0,0,1,21,16a2.92,2.92,0,0,1-1-.21,3.15,3.15,0,0,1-1.08-.67l-6-6a8.9,8.9,0,0,0,7.49,12.78L21,22a8.9,8.9,0,0,0,3.11-.58L42.6,39.84A8.9,8.9,0,0,0,42,43l.06.6A9,9,0,0,0,51,51.94a8.72,8.72,0,0,0,3.85-.9l-6-6A3.08,3.08,0,0,1,48.21,44,3,3,0,0,1,51,40a2.92,2.92,0,0,1,1,.21,2.72,2.72,0,0,1,1.08.67l6,6A8.9,8.9,0,0,0,51.61,34Z"/></svg>
|
||||
|
After Width: | Height: | Size: 1009 B |
|
After Width: | Height: | Size: 98 KiB |
|
After Width: | Height: | Size: 50 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#000000"><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.93H180Zm60-60h480v-28.93q0-12.15-7.04-22.5-7.04-10.34-19.11-16.88-51.7-25.46-105.42-38.58Q534.7-367.69 480-367.69q-54.7 0-108.43 13.11-53.72 13.12-105.42 38.58-12.07 6.54-19.11 16.88-7.04 10.35-7.04 22.5v28.93Zm240-304.62q33 0 56.5-23.5t23.5-56.5q0-33-23.5-56.5t-56.5-23.5q-33 0-56.5 23.5t-23.5 56.5q0 33 23.5 56.5t56.5 23.5Zm0-80Zm0 384.62Z"/></svg>
|
||||
|
After Width: | Height: | Size: 844 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 4.0 KiB |
@@ -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 |
@@ -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 |
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" 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: 559 B |
@@ -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 |
@@ -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 |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1,123 @@
|
||||
.backgroundContainer {
|
||||
z-index: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
min-height: 100vh;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
width: 100vw;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.backgroundLight {
|
||||
background: #00F0FF;
|
||||
}
|
||||
|
||||
.backgroundDark {
|
||||
background: #001C83;
|
||||
}
|
||||
|
||||
.circle {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
opacity: 0.8;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.circle-1 {
|
||||
width: 600px;
|
||||
height: 600px;
|
||||
background-color: #019AFF;
|
||||
top: -10%;
|
||||
left: -10%;
|
||||
}
|
||||
|
||||
.circle-2 {
|
||||
width: 250px;
|
||||
height: 250px;
|
||||
background-color: #91FCFF;
|
||||
top: 51%;
|
||||
left: 10%;
|
||||
}
|
||||
|
||||
.circle-3 {
|
||||
width: 280px;
|
||||
height: 280px;
|
||||
background-color: #91FCFF;
|
||||
top: 28%;
|
||||
right: 9%;
|
||||
}
|
||||
|
||||
.circle-4 {
|
||||
width: 500px;
|
||||
height: 500px;
|
||||
background-color: #019AFF;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.circle-5 {
|
||||
width: 240px;
|
||||
height: 240px;
|
||||
background-color: #015AFF;
|
||||
top: 2%;
|
||||
right: 2%;
|
||||
}
|
||||
|
||||
.circle-1,.circle-2, .circle-3, .circle-4, .circle-5 {
|
||||
animation: float 6s ease-in-out infinite;
|
||||
}
|
||||
.circle {
|
||||
animation: float 6s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0%, 100% {
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
50% {
|
||||
transform: translate(var(--random-x), var(--random-y));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width:600px) {
|
||||
|
||||
|
||||
.circle-1 {
|
||||
width: 300px;
|
||||
height: 300px;
|
||||
top: -5%;
|
||||
left: -5%;
|
||||
}
|
||||
|
||||
.circle-2 {
|
||||
width: 175px;
|
||||
height: 175px;
|
||||
top: 58%;
|
||||
left: 10%;
|
||||
}
|
||||
|
||||
.circle-3 {
|
||||
width: 140px;
|
||||
height: 140px;
|
||||
top: 29%;
|
||||
right: 9%;
|
||||
}
|
||||
|
||||
.circle-4 {
|
||||
width: 250px;
|
||||
height: 250px;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.circle-5 {
|
||||
width: 240px;
|
||||
height: 240px;
|
||||
top: 2%;
|
||||
right: 10%;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import styles from "./Background.module.css";
|
||||
import { useEffect, useRef, useState, ReactNode } from "react";
|
||||
|
||||
interface BackgroundProps {
|
||||
children?: ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const Background = ({ children, className }: BackgroundProps) => {
|
||||
|
||||
const circlesRef = useRef<(HTMLDivElement | null)[]>([]);
|
||||
const [theme, setTheme] = useState("light");
|
||||
|
||||
const getRandomTranslation = (): { x: number; y: number } => {
|
||||
const randomX = Math.floor(Math.random() * 100) - 30;
|
||||
const randomY = Math.floor(Math.random() * 100) - 30;
|
||||
return { x: randomX, y: randomY };
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
circlesRef.current.forEach((circle) => {
|
||||
if (circle) {
|
||||
const { x, y } = getRandomTranslation();
|
||||
circle.style.setProperty('--random-x', `${x}px`);
|
||||
circle.style.setProperty('--random-y', `${y}px`);
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const updateTheme = () => {
|
||||
const t = localStorage.getItem("theme") || "light";
|
||||
setTheme(t);
|
||||
document.body.classList.toggle("dark", t === "dark");
|
||||
};
|
||||
|
||||
updateTheme();
|
||||
|
||||
window.addEventListener("theme-changed", updateTheme);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("theme-changed", updateTheme);
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
||||
return (
|
||||
<div className={`${styles.backgroundContainer} ${className || ''} ${theme === "light" ? styles.backgroundLight : styles.backgroundDark}`}>
|
||||
<div
|
||||
ref={(el) => { circlesRef.current[0] = el; }}
|
||||
className={`${styles["circle"]} ${styles["circle-1"]}`}
|
||||
style={{ "--random-x": "0px", "--random-y": "0px" } as React.CSSProperties}
|
||||
></div>
|
||||
<div
|
||||
ref={(el) => { circlesRef.current[1] = el; }}
|
||||
className={`${styles["circle"]} ${styles["circle-2"]}`}
|
||||
style={{ "--random-x": "0px", "--random-y": "0px" } as React.CSSProperties}
|
||||
></div>
|
||||
<div
|
||||
ref={(el) => { circlesRef.current[2] = el; }}
|
||||
className={`${styles["circle"]} ${styles["circle-3"]}`}
|
||||
style={{ "--random-x": "0px", "--random-y": "0px" } as React.CSSProperties}
|
||||
></div>
|
||||
<div
|
||||
ref={(el) => { circlesRef.current[3] = el; }}
|
||||
className={`${styles["circle"]} ${styles["circle-4"]}`}
|
||||
style={{ "--random-x": "0px", "--random-y": "0px" } as React.CSSProperties}
|
||||
></div>
|
||||
<div
|
||||
ref={(el) => { circlesRef.current[4] = el; }}
|
||||
className={`${styles["circle"]} ${styles["circle-5"]}`}
|
||||
style={{ "--random-x": "0px", "--random-y": "0px" } as React.CSSProperties}
|
||||
></div>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Background;
|
||||
@@ -0,0 +1,21 @@
|
||||
import Button from "../ui/Button/Button";
|
||||
import { useContext } from "react";
|
||||
import { AuthContext, AuthContextType } from "../../contexts/Auth/AuthContext";
|
||||
import { APP_CONFIG } from "../../config/appConfig";
|
||||
|
||||
export default function ExportCalendarBtn() {
|
||||
|
||||
const { user } = useContext(AuthContext) as AuthContextType;
|
||||
const calendarUrl = `${import.meta.env.VITE_API_URL}/api/export/ics/${user?.id}`;
|
||||
|
||||
return <Button
|
||||
variant="default"
|
||||
onClick={() => {
|
||||
window.open(
|
||||
`${APP_CONFIG.newCalendarURL}${calendarUrl}`
|
||||
);
|
||||
}}
|
||||
>
|
||||
Exporter le calendrier
|
||||
</Button>
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
.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;
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
.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:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
|
||||
.active {
|
||||
border: 2px solid #019AFF;
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import styles from "./Filter.module.css";
|
||||
import Button from "../ui/Button/Button";
|
||||
import React from "react";
|
||||
|
||||
type FiltersType = {
|
||||
alphabetical: "asc" | "desc" | null;
|
||||
yearOrder: "asc" | "desc" | null;
|
||||
[key: string]: any;
|
||||
};
|
||||
|
||||
type FilterProps = {
|
||||
isFilterVisible: boolean;
|
||||
filters: FiltersType;
|
||||
setFilters: React.Dispatch<React.SetStateAction<FiltersType>>;
|
||||
};
|
||||
|
||||
function Filter({ isFilterVisible, filters, setFilters }: FilterProps): any {
|
||||
|
||||
const setAlphabeticalOrder = (order: "asc" | "desc"): void => {
|
||||
setFilters((prev) => ({
|
||||
...prev,
|
||||
alphabetical: prev.alphabetical === order ? null : order
|
||||
}));
|
||||
};
|
||||
|
||||
const setYearOrder = (order: "asc" | "desc"): void => {
|
||||
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}`}
|
||||
variant={"default"}
|
||||
onClick={() => setAlphabeticalOrder("asc")}
|
||||
>
|
||||
<img src={"sortByAlpha.svg"} alt="Tri alphabétique croissant (A à Z)" />
|
||||
</Button>
|
||||
</abbr>
|
||||
|
||||
<abbr title="Tri alphabétique décroissant (Z à A)">
|
||||
<Button
|
||||
className={`${styles.filterTag} glassCard ${filters.alphabetical !== "desc" ? "" : styles.active}`}
|
||||
variant={"default"}
|
||||
onClick={() => setAlphabeticalOrder("desc")}
|
||||
>
|
||||
<img src={"sortByAntiAlpha.svg"} alt="Tri alphabétique décroissant (Z à A)" />
|
||||
</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}`}
|
||||
variant={"default"}
|
||||
onClick={() => setYearOrder("desc")}
|
||||
>
|
||||
<img src={"timerArrowUp.svg"} alt="Tri chronologique décroissant (du plus récent au plus ancien)" />
|
||||
</Button>
|
||||
</abbr>
|
||||
|
||||
<abbr title="Tri chronologique croissant (du plus ancien au plus récent)">
|
||||
<Button
|
||||
className={`${styles.filterTag} glassCard ${filters.yearOrder !== "asc" ? "" : styles.active}`}
|
||||
variant={"default"}
|
||||
onClick={() => setYearOrder("asc")}
|
||||
>
|
||||
<img src={"timerArrowDown.svg"} alt="Tri chronologique croissant (du plus ancien au plus récent)" />
|
||||
</Button>
|
||||
</abbr>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Filter;
|
||||
@@ -0,0 +1,76 @@
|
||||
.footer {
|
||||
width: 100%;
|
||||
padding: 1.5rem 2rem;
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.footerContent {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.footerNav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.link {
|
||||
color: black !important;
|
||||
text-decoration: none;
|
||||
font-size: 0.95rem;
|
||||
transition: color 0.2s ease;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.link:hover {
|
||||
color: #019AFF !important;
|
||||
}
|
||||
|
||||
.separator {
|
||||
width: 1px;
|
||||
height: 16px;
|
||||
background-color: black;
|
||||
margin: 0 1rem;
|
||||
}
|
||||
|
||||
.copyright {
|
||||
color: black;
|
||||
font-size: 0.9rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.section{
|
||||
margin-top: 2rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
.footerContent {
|
||||
flex-direction: column-reverse;
|
||||
gap: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.footerNav {
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.link {
|
||||
margin: 0.5rem 1rem;
|
||||
}
|
||||
|
||||
.separator {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import styles from "./Footer.module.css"
|
||||
import { Link } from "react-router";
|
||||
import { ReactNode, useState } from "react";
|
||||
import Modal from "../ui/Modal/Modal";
|
||||
import Button from "../ui/Button/Button";
|
||||
import scrollToTop from "../../utils/scrollToTop.js";
|
||||
|
||||
export default function Footer(): ReactNode{
|
||||
const currentYear = new Date().getFullYear();
|
||||
const [isCopyrightModal, setIsCopyrightModal] = useState<boolean>(false);
|
||||
|
||||
|
||||
return <>
|
||||
<footer className={`${styles.footer} glassCard`}>
|
||||
<div className={styles.footerContent}>
|
||||
<Button
|
||||
variant={"transparent"}
|
||||
className={styles.link}
|
||||
onClick={() => setIsCopyrightModal(true)}
|
||||
>
|
||||
Copyright © {currentYear}. Tous droits réservés.
|
||||
</Button>
|
||||
|
||||
<div className={styles.copyright}>
|
||||
|
||||
</div>
|
||||
|
||||
<nav className={styles.footerNav}>
|
||||
<Link to="/rgpd" className={styles.link} onClick={scrollToTop}>
|
||||
Fiche RGPD
|
||||
</Link>
|
||||
<span className={styles.separator}></span>
|
||||
<Link to="/legalNotices" className={styles.link} onClick={scrollToTop}>
|
||||
Mentions légales
|
||||
</Link>
|
||||
<span className={styles.separator}></span>
|
||||
<Link to={"https://comite.beaupont.fr/contactez-nous"} className={styles.link}>
|
||||
Contact
|
||||
</Link>
|
||||
<span className={styles.separator}></span>
|
||||
<Link to="/" className={styles.link} onClick={scrollToTop}>
|
||||
Accueil
|
||||
</Link>
|
||||
</nav>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<Modal title={"Réalisé par"} open={isCopyrightModal} onClose={() => setIsCopyrightModal(false)}>
|
||||
<section className={styles.section}>
|
||||
<div className={styles.inputContainer}>
|
||||
<p>Quentin T'JAMPENS</p>
|
||||
<p>Antoine ORDONNEAU</p>
|
||||
<p>Malo LEBLOND</p>
|
||||
<p>
|
||||
Giovanni JOSSERAND (
|
||||
<Link to={"https://linktree.josserand.ovh/"}>linktree</Link>
|
||||
)
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
</Modal>
|
||||
</>
|
||||
}
|
||||
@@ -0,0 +1,248 @@
|
||||
.headerContainer {
|
||||
position: relative;
|
||||
height: fit-content;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
|
||||
.header{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.8rem;
|
||||
height: fit-content;
|
||||
z-index: 2;
|
||||
position: relative;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.profileImg{
|
||||
width:40px;
|
||||
height:40px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
|
||||
.activeHeaderContent {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
opacity: 1;
|
||||
transition: opacity 0.3s ease;
|
||||
pointer-events: auto;
|
||||
}
|
||||
.inactiveHeaderContent{
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
.inactiveMobileMenu{
|
||||
display: none;
|
||||
}
|
||||
|
||||
.activeMobileMenu{
|
||||
height: fit-content;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: start;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
.headerLeftContent {
|
||||
display: flex;
|
||||
width: fit-content;
|
||||
align-items: center;
|
||||
gap: 1.5rem;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.headerRightContent {
|
||||
width: fit-content;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 4rem;
|
||||
width: auto;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.logoLink {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: fit-content;
|
||||
height: fit-content;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
|
||||
.link {
|
||||
color: #000000;
|
||||
width: fit-content;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
border-bottom: 2px solid transparent;
|
||||
}
|
||||
|
||||
.link:hover{
|
||||
border-bottom: 2px solid #019AFF;
|
||||
|
||||
}
|
||||
|
||||
.activeLink {
|
||||
width: fit-content;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
color: #019AFF;
|
||||
border-bottom: 2px solid #019AFF;
|
||||
}
|
||||
|
||||
.bellBtn, .profileLink {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: transform 0.5s ease;
|
||||
background: none;
|
||||
padding: 7px;
|
||||
border-radius: 50%;
|
||||
width: 55px;
|
||||
height: 55px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.notificationsImg {
|
||||
stroke: black;
|
||||
stroke-width: 3.5;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
.bellBtn:focus {
|
||||
outline: none;
|
||||
}
|
||||
.bellBtn:focus-visible {
|
||||
outline: 2px solid black;
|
||||
}
|
||||
|
||||
.activeBellBtn {
|
||||
transform: translateY(-4px);
|
||||
background: rgba(191, 191, 191, 0.5);
|
||||
}
|
||||
|
||||
|
||||
.bellBtn:hover {
|
||||
transform: translateY(-4px);
|
||||
background: rgba(191, 191, 191, 0.5);
|
||||
}
|
||||
|
||||
|
||||
.profileLink:hover {
|
||||
transform: translateY(-4px);
|
||||
background: rgba(191, 191, 191, 0.5);
|
||||
}
|
||||
|
||||
|
||||
.profileImg {
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.notificationDiv {
|
||||
position: absolute;
|
||||
width: auto;
|
||||
top: 105%;
|
||||
right: 2rem;
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
transition: opacity 0.3s ease, transform 0.3s ease;
|
||||
z-index: 0;
|
||||
max-height: 60vh;
|
||||
overflow: scroll;
|
||||
overflow-x: hidden;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.activeNotificationDiv {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.notification{
|
||||
padding: 0.5em 1em;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-radius: 15px;
|
||||
}
|
||||
|
||||
.notificationBadge {
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
right: 9px;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
background-color: red;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
|
||||
.closeBtn:focus {
|
||||
outline: none;
|
||||
}
|
||||
.closeBtn:focus-visible {
|
||||
outline: 2px solid black;
|
||||
}
|
||||
|
||||
.burgerBtn {
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
width: 30px;
|
||||
height: 22px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
display: none;
|
||||
z-index: 10;
|
||||
background: none;
|
||||
}
|
||||
|
||||
.burgerBar {
|
||||
width: 100%;
|
||||
height: 3px;
|
||||
background-color: #000000;
|
||||
border-radius: 2px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.activeBurgerBtn .burgerBar:nth-child(1) {
|
||||
transform: translateY(10px) rotate(45deg);
|
||||
}
|
||||
.activeBurgerBtn .burgerBar:nth-child(2) {
|
||||
opacity: 0;
|
||||
}
|
||||
.activeBurgerBtn .burgerBar:nth-child(3) {
|
||||
transform: translateY(-9px) rotate(-45deg);
|
||||
}
|
||||
|
||||
|
||||
.navLinks{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
.navLinks {
|
||||
display: none;
|
||||
}
|
||||
.burgerBtn {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
import { Link, NavLink } from "react-router";
|
||||
import { useContext, useEffect, useRef, useState } from "react";
|
||||
import styles from "./Header.module.css"
|
||||
import getUserNotifications from "../../utils/notifications/getUserNotifications.js";
|
||||
import deleteNotificationUser from "../../utils/notifications/deleteNotificationUser.js";
|
||||
import initEcho from "../../utils/echo/initEcho.js"
|
||||
import readNotifications from "../../utils/notifications/readNotifications.js";
|
||||
import { userNotificationsListener } from "../../utils/echo/listeners/userNotificationsListener.js";
|
||||
import NotificationCard from "../NotificationCard/NotificationCard";
|
||||
import { EventContext } from "../../contexts/Events/EventContext.js";
|
||||
import { AuthContext } from "../../contexts/Auth/AuthContext.js";
|
||||
import { PendingMembersContext } from "../../contexts/PendingMembers/PendingMembersContext";
|
||||
import Notification from "../../interfaces/notification.interface";
|
||||
|
||||
|
||||
function Header() {
|
||||
|
||||
const { update, user } = useContext(AuthContext)!;
|
||||
const { updateEvent } = useContext(EventContext)!;
|
||||
const { updatePendingMembers } = useContext(PendingMembersContext) as unknown as { updatePendingMembers: () => void };
|
||||
const [notificationMenu, setnotificationMenu] = useState<boolean>(false);
|
||||
const [unreadNotification, setunreadNotification] = useState<boolean>(false);
|
||||
const [mobileMenu, setMobileMenu] = useState<boolean>(false);
|
||||
const [notifications, setNotifications] = useState<Notification[]>([]);
|
||||
const notificationRef = useRef<HTMLDivElement>(null);
|
||||
const profileImageUrl = user?.profile_photo_path
|
||||
? `${import.meta.env.VITE_API_URL}/storage/${user.profile_photo_path}`
|
||||
: "/person.svg";
|
||||
useEffect(() => {
|
||||
if (!user) return;
|
||||
|
||||
updateEvent();
|
||||
|
||||
let echoInstance: ReturnType<typeof initEcho> | null = null;
|
||||
|
||||
async function initializeHeader() {
|
||||
try {
|
||||
const echo = initEcho();
|
||||
echoInstance = echo;
|
||||
userNotificationsListener(echo, user, setNotifications, setunreadNotification, updatePendingMembers);
|
||||
|
||||
const notifData = await getUserNotifications();
|
||||
|
||||
const data: Notification[] = notifData || [];
|
||||
setNotifications(data);
|
||||
const hasUnread = data.some(n => n.read_at === null);
|
||||
setunreadNotification(hasUnread);
|
||||
} catch (err) {
|
||||
console.error("Erreur initialisation Header:", err);
|
||||
}
|
||||
}
|
||||
|
||||
initializeHeader();
|
||||
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (
|
||||
notificationRef.current &&
|
||||
!notificationRef.current.contains(event.target as Node) &&
|
||||
!(event.target as Element).closest(`.${styles.bellBtn}`)
|
||||
) {
|
||||
setnotificationMenu(false);
|
||||
}
|
||||
};
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", handleClickOutside);
|
||||
if (echoInstance) {
|
||||
echoInstance.disconnect();
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
||||
|
||||
const toggleNotificationMenu = async () => {
|
||||
try {
|
||||
setnotificationMenu(!notificationMenu);
|
||||
if (unreadNotification) {
|
||||
const result = await readNotifications();
|
||||
if (result) {
|
||||
setunreadNotification(false);
|
||||
update();
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Erreur pour ouvrir/fermer le menu de notifications :', err);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async function deleteNotification(notificationId: string) {
|
||||
try {
|
||||
const result = await deleteNotificationUser(notificationId);
|
||||
if (result) {
|
||||
setNotifications(prev => prev.filter(n => n.id !== notificationId));
|
||||
update();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Erreur suppression notification :', err);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<div className={styles.headerContainer}>
|
||||
<header className={`${styles.header} glassCard`}>
|
||||
<nav className={`${mobileMenu ? styles.inactiveHeaderContent : styles.activeHeaderContent}`}>
|
||||
<div className={styles.headerLeftContent}>
|
||||
<NavLink className={styles.logoLink} to="/">
|
||||
<img src="/logo.png" alt="logo" className={styles.logo} />
|
||||
</NavLink>
|
||||
<div className={styles.navLinks}>
|
||||
<NavLink to="/" className={({ isActive }) => isActive ? styles.activeLink : styles.link}>
|
||||
Accueil
|
||||
</NavLink>
|
||||
<NavLink to="/events" className={({ isActive }) => isActive ? styles.activeLink : styles.link}>
|
||||
Evènements
|
||||
</NavLink>
|
||||
<NavLink to="/volunteers" className={({ isActive }) => isActive ? styles.activeLink : styles.link}>
|
||||
Bénévoles
|
||||
</NavLink>
|
||||
{user?.isAdmin && (
|
||||
<NavLink to="/admin" className={({ isActive }) => isActive ? styles.activeLink : styles.link}>
|
||||
Administration
|
||||
</NavLink>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.headerRightContent}>
|
||||
<button
|
||||
className={`${styles.bellBtn} ${notificationMenu ? styles.activeBellBtn : ""}`}
|
||||
onClick={() => { toggleNotificationMenu(); }}
|
||||
>
|
||||
<img className={styles.notificationsImg} src="/bell.svg" alt="notifications" />
|
||||
{unreadNotification && <span className={styles.notificationBadge}></span>}
|
||||
</button>
|
||||
|
||||
<Link to="/profile" className={styles.profileLink}>
|
||||
<img
|
||||
className={styles.profileImg}
|
||||
src={profileImageUrl}
|
||||
alt="profile"
|
||||
/>
|
||||
</Link>
|
||||
|
||||
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<nav className={`${mobileMenu ? styles.activeMobileMenu : styles.inactiveMobileMenu}`}>
|
||||
<NavLink to="/" className={({ isActive }) => isActive ? styles.activeLink : styles.link}>
|
||||
Accueil
|
||||
</NavLink>
|
||||
<NavLink to="/events" className={({ isActive }) => isActive ? styles.activeLink : styles.link}>
|
||||
Evènements
|
||||
</NavLink>
|
||||
<NavLink to="/volunteers" className={({ isActive }) => isActive ? styles.activeLink : styles.link}>
|
||||
Bénévoles
|
||||
</NavLink>
|
||||
{user?.isAdmin && (
|
||||
<NavLink to="/admin" className={({ isActive }) => isActive ? styles.activeLink : styles.link}>
|
||||
Administration
|
||||
</NavLink>
|
||||
)}
|
||||
</nav>
|
||||
|
||||
|
||||
<button
|
||||
className={`${styles.burgerBtn} ${mobileMenu ? styles.activeBurgerBtn : ""}`}
|
||||
onClick={() => setMobileMenu(!mobileMenu)}
|
||||
>
|
||||
<div className={styles.burgerBar}></div>
|
||||
<div className={styles.burgerBar}></div>
|
||||
<div className={styles.burgerBar}></div>
|
||||
</button>
|
||||
</header>
|
||||
|
||||
|
||||
|
||||
|
||||
<div
|
||||
ref={notificationRef}
|
||||
className={`${styles.notificationDiv} glassCard ${notificationMenu ? styles.activeNotificationDiv : ""}`}
|
||||
>
|
||||
{notifications.length === 0 ? (
|
||||
<p className={styles.notification}>Vous n'avez aucune notification !</p>
|
||||
) : (
|
||||
<NotificationCard notifications={notifications} deleteNotification={deleteNotification} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Header;
|
||||
@@ -0,0 +1,45 @@
|
||||
.notificationCard {
|
||||
padding: 1.25rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.08);
|
||||
transition: background 0.2s ease;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.cardHeader {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.timeTag {
|
||||
color: #019AFF;
|
||||
letter-spacing: 0.2px;
|
||||
}
|
||||
|
||||
.message {
|
||||
margin: 0;
|
||||
font-size: 1.1rem;
|
||||
line-height: 1.5;
|
||||
color: #2c3e50;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.closeIconButton {
|
||||
background: none;
|
||||
border: none;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
font-size: 1.1rem;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.closeIconButton:hover {
|
||||
color: #cc0000;
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import formatDate from "../../utils/date/formatDate.js";
|
||||
import formatTime from "../../utils/date/formatTime.js";
|
||||
import styles from "./NotificationCard.module.css"
|
||||
import Notification from "../../interfaces/notification.interface";
|
||||
|
||||
interface NotificationCardProps {
|
||||
notifications: Notification[];
|
||||
deleteNotification: (id: string) => void;
|
||||
}
|
||||
|
||||
function NotificationCard({ notifications, deleteNotification }: NotificationCardProps) {
|
||||
return (
|
||||
notifications.map((notification, index) => (
|
||||
<div className={`${styles.notificationCard} glassBorder`} key={index}>
|
||||
<div className={styles.cardHeader}>
|
||||
<span className={styles.timeTag}>
|
||||
Le {formatDate(notification.created_at)} à {formatTime(notification.created_at)}
|
||||
</span>
|
||||
<button
|
||||
className={styles.closeIconButton}
|
||||
onClick={() => deleteNotification(notification.id)}
|
||||
title="Supprimer"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className={styles.message}>
|
||||
{notification.data.message}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
export default NotificationCard;
|
||||
@@ -0,0 +1,87 @@
|
||||
.searchBarContainer{
|
||||
position: fixed;
|
||||
top: 20vh;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
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;
|
||||
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,120 @@
|
||||
import styles from "./SearchBar.module.css";
|
||||
import { Link } from "react-router";
|
||||
import { useEffect, useState } from "react";
|
||||
import searchEvents from "../../utils/events/searchEvents";
|
||||
import searchUsers from "../../utils/users/searchUsers";
|
||||
import { useLocation } from "react-router";
|
||||
|
||||
function SearchBar(): any {
|
||||
const [searchQuery, setSearchQuery] = useState<string>("");
|
||||
const [searchResults, setSearchResults] = useState<any[]>([]);
|
||||
const location: any = useLocation();
|
||||
|
||||
useEffect(() => {
|
||||
const performSearch = async (query: string): Promise<void> => {
|
||||
if (query.trim() === "") {
|
||||
setSearchResults([]);
|
||||
return;
|
||||
}
|
||||
|
||||
let data: any;
|
||||
|
||||
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: ReturnType<typeof setTimeout> = setTimeout(() => {
|
||||
performSearch(searchQuery);
|
||||
}, 300);
|
||||
|
||||
return () => {
|
||||
clearTimeout(handler);
|
||||
};
|
||||
}, [searchQuery, location.pathname]);
|
||||
|
||||
return (
|
||||
<div className={`${styles.searchBarContainer} glassCard`}>
|
||||
<form
|
||||
className={styles.searchBarForm}
|
||||
onSubmit={(element: React.FormEvent<HTMLFormElement>) =>
|
||||
element.preventDefault()
|
||||
}
|
||||
>
|
||||
<img src="search.svg" alt="Rechercher" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Rechercher un événement"
|
||||
value={searchQuery}
|
||||
onChange={(element: React.ChangeEvent<HTMLInputElement>) =>
|
||||
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: any, i: number) =>
|
||||
location.pathname.includes("events") ? (
|
||||
<Link to={`/events/` + result.id} key={i}>
|
||||
<div className={styles.searchedEvent}>
|
||||
<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} key={i}>
|
||||
<div className={styles.searchedEvent}>
|
||||
<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,21 @@
|
||||
.task {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import getEventById from "../../utils/events/getEventById";
|
||||
import styles from "./Task.module.css";
|
||||
import formatDate from "../../utils/date/formatDate";
|
||||
|
||||
interface TaskType {
|
||||
id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
location: string;
|
||||
start: string;
|
||||
end: string;
|
||||
max_participants: number;
|
||||
events_id: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
pivot: {
|
||||
user_id: number;
|
||||
task_id: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface EventType {
|
||||
id: number;
|
||||
name: string;
|
||||
description?: string;
|
||||
location?: string;
|
||||
start?: string;
|
||||
end?: string;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
interface TaskProps {
|
||||
task: TaskType;
|
||||
}
|
||||
|
||||
const Task: React.FC<TaskProps> = ({ task }) => {
|
||||
const [event, setEvent] = useState<EventType | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchEvent = async (): Promise<void> => {
|
||||
try {
|
||||
const e = await getEventById(task.events_id);
|
||||
setEvent(e.data as EventType);
|
||||
} catch (error) {
|
||||
console.error("Erreur lors du chargement de l'événement", error);
|
||||
}
|
||||
};
|
||||
|
||||
fetchEvent();
|
||||
}, [task.events_id]);
|
||||
|
||||
return (
|
||||
<div className={`${styles.task} glassBorder`}>
|
||||
<div className={styles.content}>
|
||||
<div>
|
||||
<h2>
|
||||
{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;
|
||||
@@ -0,0 +1,101 @@
|
||||
.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;
|
||||
z-index: 4;
|
||||
}
|
||||
|
||||
|
||||
.filterContainer{
|
||||
padding: 0;
|
||||
border-radius: 30px;
|
||||
overflow: visible;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.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: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: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: 6px 0 16px 0;
|
||||
}
|
||||
|
||||
.filterContainer{
|
||||
width: fit-content;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.filterMenu{
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.createButton:hover, .searchButton:hover, .mobileFilter:hover {
|
||||
transform: none !important;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import styles from "./ToolBar.module.css"
|
||||
import CreateEventBtn from "./tools/CreateEventBtn/CreateEventBtn";
|
||||
import SearchBtn from "./tools/SearchBtn";
|
||||
import FilterBtn from "./tools/FilterBtn";
|
||||
|
||||
interface Filters {
|
||||
alphabetical: "asc" | "desc";
|
||||
yearOrder: "asc" | "desc";
|
||||
}
|
||||
|
||||
interface ToolBarProps {
|
||||
setFilters: React.Dispatch<React.SetStateAction<Filters>>;
|
||||
filters: Filters;
|
||||
showCreate?: boolean;
|
||||
}
|
||||
|
||||
function ToolBar({ setFilters, filters, showCreate = true }: ToolBarProps) {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.eventsButtons}>
|
||||
{showCreate && <CreateEventBtn />}
|
||||
<SearchBtn />
|
||||
<FilterBtn setFilters={setFilters} filters={filters} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ToolBar;
|
||||
@@ -0,0 +1,62 @@
|
||||
.createButton {
|
||||
font-size: 1.5rem;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.createButton {
|
||||
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:hover {
|
||||
transform: translateY(-5px);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.inputContainer {
|
||||
margin: 0.5em;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.input {
|
||||
width: 70%;
|
||||
}
|
||||
|
||||
.create {
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
margin-top: 15px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.dateInputContainer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 40px;
|
||||
}
|
||||
|
||||
.inputContainer h3{
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
|
||||
@media screen and (max-width: 900px) {
|
||||
|
||||
.dateInputContainer {
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import styles from "./CreateEventBtn.module.css"
|
||||
import { useState, useContext } from "react";
|
||||
import Modal from "../../../ui/Modal/Modal";
|
||||
import Button from "../../../ui/Button/Button";
|
||||
import TextInput from "../../../ui/Input/Input";
|
||||
import { EventContext } from "../../../../contexts/Events/EventContext.js";
|
||||
import { AuthContext } from "../../../../contexts/Auth/AuthContext";
|
||||
|
||||
|
||||
export default function CreateEventBtn() {
|
||||
|
||||
const { addEvent } = useContext(EventContext)!;
|
||||
const { user } = useContext(AuthContext)!;
|
||||
|
||||
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||
const [name, setName] = useState<string>("");
|
||||
const [description, setDescription] = useState<string>("");
|
||||
const [start, setStart] = useState<string>("");
|
||||
const [end, setEnd] = useState<string>("");
|
||||
|
||||
const handleCreateEvent = async () => {
|
||||
|
||||
if (name !== "" && description !== "" && start !== "" && end !== "") {
|
||||
await addEvent(name, description, start, end);
|
||||
}
|
||||
setIsOpen(false);
|
||||
}
|
||||
|
||||
if(!user?.isAdmin) return null;
|
||||
|
||||
return <>
|
||||
<Button className={`${styles.createButton} glassCard`} variant={"default"} onClick={() => setIsOpen(true)}> + </Button>
|
||||
|
||||
<Modal title={"Créer un événement"} open={isOpen} onClose={() => setIsOpen(false)}>
|
||||
|
||||
<section className={styles.section}>
|
||||
<div className={styles.inputContainer}>
|
||||
<h3>Titre</h3>
|
||||
<TextInput className={styles.input} placeholder={"Nom de l'événement"} value={name} onChange={e => setName(e.target.value)} />
|
||||
</div>
|
||||
|
||||
<div className={styles.inputContainer}>
|
||||
<h3>Description</h3>
|
||||
<TextInput className={styles.input} placeholder={"Description de l'événement"} value={description} onChange={e => setDescription(e.target.value)} />
|
||||
</div>
|
||||
|
||||
<div className={styles.inputContainer}>
|
||||
<h3>Date</h3>
|
||||
<div className={styles.dateInputContainer}>
|
||||
<div className={styles.dateInput}>
|
||||
<p>Début</p>
|
||||
<TextInput type={"datetime-local"} value={start} onChange={e => setStart(e.target.value)} />
|
||||
</div>
|
||||
<div className={styles.dateInput}>
|
||||
<p>Fin</p>
|
||||
<TextInput type={"datetime-local"} value={end} onChange={e => setEnd(e.target.value)} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button onClick={handleCreateEvent} className={styles.create}>Créer</Button>
|
||||
</section>
|
||||
</Modal>
|
||||
</>
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import styles from "../ToolBar.module.css"
|
||||
import Button from "../../ui/Button/Button";
|
||||
import { useState } from "react";
|
||||
import Filter from "../../Filter/Filter";
|
||||
|
||||
interface Filters {
|
||||
alphabetical: "asc" | "desc";
|
||||
yearOrder: "asc" | "desc";
|
||||
}
|
||||
|
||||
interface FilterBtnProps {
|
||||
setFilters: any
|
||||
filters: Filters
|
||||
}
|
||||
|
||||
export default function FilterBtn({ setFilters, filters }: FilterBtnProps) {
|
||||
|
||||
const [isFilterVisible, setIsFilterVisible] = useState(false);
|
||||
|
||||
const toggleFilters = () => {
|
||||
setIsFilterVisible(prev => !prev);
|
||||
};
|
||||
|
||||
return <div className={`${styles.filterContainer} ${isFilterVisible ? "glassCard" : ""}`}>
|
||||
<Button className={`${isFilterVisible ? "" : styles.mobileFilter} ${styles.sortButton} glassCard`} variant={"default"} onClick={toggleFilters}>
|
||||
<img src="filter.svg" alt="Filtrer"/>
|
||||
</Button>
|
||||
<div className={styles.filterMenu}>
|
||||
<Filter isFilterVisible={isFilterVisible} filters={filters} setFilters={setFilters}/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import styles from "../ToolBar.module.css"
|
||||
import Button from "../../ui/Button/Button";
|
||||
import { useEffect, useState } from "react";
|
||||
import SearchBar from "../../SearchBar/SearchBar";
|
||||
|
||||
export default function SearchBtn() {
|
||||
|
||||
const [isSearchVisible, setIsSearchVisible] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (isSearchVisible) {
|
||||
document.documentElement.style.overflow = 'hidden';
|
||||
} else {
|
||||
document.documentElement.style.overflow = '';
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.documentElement.style.overflow = '';
|
||||
};
|
||||
}, [isSearchVisible]);
|
||||
|
||||
return <>
|
||||
|
||||
{isSearchVisible ? (
|
||||
<div className={styles.searchBarContainer}>
|
||||
<div className={styles.searchBarOverlay} onClick={() => setIsSearchVisible(false)}></div>
|
||||
<SearchBar/>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div>
|
||||
<Button className={`${styles.searchButton} glassCard`} variant={"default"} onClick={() => setIsSearchVisible(true)}>
|
||||
<img src="search.svg" alt="Rechercher"/>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
</>
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
.btn {
|
||||
padding: 0.2rem 0.8rem 0.2rem 0.8rem;
|
||||
width: fit-content;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
cursor: pointer;
|
||||
filter: brightness(0.95);
|
||||
}
|
||||
|
||||
.primary {
|
||||
color: white;
|
||||
background-color: #019AFF;
|
||||
}
|
||||
|
||||
.danger {
|
||||
color: white;
|
||||
background-color: #FF073A;
|
||||
}
|
||||
|
||||
.transparent {
|
||||
color: #019AFF;
|
||||
}
|
||||
|
||||
.default {
|
||||
color: black;
|
||||
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);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { ReactNode, MouseEventHandler } from "react";
|
||||
import styles from "./Button.module.css";
|
||||
|
||||
type ButtonVariant = "primary" | "danger" | "transparent" | "default";
|
||||
|
||||
interface ButtonProps {
|
||||
children: ReactNode;
|
||||
variant?: ButtonVariant;
|
||||
onClick?: MouseEventHandler<HTMLDivElement>;
|
||||
className?: string | undefined;
|
||||
}
|
||||
|
||||
const Button = ({
|
||||
children,
|
||||
variant = "primary",
|
||||
onClick,
|
||||
className = "",
|
||||
}: ButtonProps) => {
|
||||
let btnStyle: string;
|
||||
|
||||
if (variant === "primary") { // @ts-ignore
|
||||
btnStyle = styles.primary;
|
||||
}
|
||||
else if (variant === "danger") { // @ts-ignore
|
||||
btnStyle = styles.danger;
|
||||
}
|
||||
else if (variant === "transparent") { // @ts-ignore
|
||||
btnStyle = styles.transparent;
|
||||
}
|
||||
else { // @ts-ignore
|
||||
btnStyle = styles.default;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`${styles.btn} ${btnStyle} ${className}`}
|
||||
onClick={onClick}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Button;
|
||||
@@ -0,0 +1,52 @@
|
||||
.input {
|
||||
border: 1px solid rgba(200, 200, 200, 0.3);
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
color: #000;
|
||||
font-size: 16px;
|
||||
outline: none;
|
||||
transition: all 0.3s ease;
|
||||
width: fit-content;
|
||||
padding: 8px 10px 8px 10px;
|
||||
height: fit-content;
|
||||
}
|
||||
|
||||
.input::placeholder {
|
||||
color: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.input:focus {
|
||||
border-color: rgba(200, 200, 200, 0.6);
|
||||
box-shadow: 0 4px 30px rgba(255, 255, 255, 0.2);
|
||||
background: rgba(255, 255, 255, 0.38);
|
||||
}
|
||||
|
||||
.rounded {
|
||||
border-radius: 25px;
|
||||
}
|
||||
|
||||
.square {
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import styles from "./Input.module.css";
|
||||
import { useState, ChangeEvent, InputHTMLAttributes } from "react";
|
||||
|
||||
interface TextInputProps extends InputHTMLAttributes<HTMLInputElement> {
|
||||
placeholder?: string;
|
||||
value?: string;
|
||||
onChange?: (e: ChangeEvent<HTMLInputElement>) => void;
|
||||
borderStyle?: "square" | "rounded";
|
||||
password?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const TextInput = ({
|
||||
placeholder = "",
|
||||
onChange,
|
||||
value = "",
|
||||
borderStyle = "square",
|
||||
password = false,
|
||||
className = "",
|
||||
...props
|
||||
}: TextInputProps) => {
|
||||
const [showPassword, setShowPassword] = useState<boolean>(false);
|
||||
|
||||
let inputBorderStyle = styles.square;
|
||||
if (borderStyle === "rounded") inputBorderStyle = styles.rounded;
|
||||
|
||||
const toggleShowPassword = () => {
|
||||
setShowPassword((prev) => !prev);
|
||||
};
|
||||
|
||||
if (password) {
|
||||
return (
|
||||
<div className={styles.passwordContainer}>
|
||||
<input
|
||||
type={showPassword ? "text" : "password"}
|
||||
value={value}
|
||||
placeholder={placeholder}
|
||||
onChange={onChange}
|
||||
className={`${styles.input} ${styles.passwordInput} ${inputBorderStyle} ${className}`}
|
||||
{...props}
|
||||
/>
|
||||
<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;
|
||||
@@ -0,0 +1,25 @@
|
||||
.wrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
width: 100%;
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
|
||||
border: 5px solid #e0e0e0;
|
||||
border-top: 5px solid #3498db;
|
||||
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import React from "react";
|
||||
import styles from "./Loading.module.css";
|
||||
|
||||
const Loading: React.FC = () => {
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
<div className={styles.spinner} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Loading;
|
||||
@@ -0,0 +1,33 @@
|
||||
.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 {
|
||||
border-radius: 20px;
|
||||
min-width: 300px;
|
||||
max-width: 70%;
|
||||
max-height: none;
|
||||
position: relative;
|
||||
width: clamp(300px, 50vw, 600px);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.modalFooter {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top: 10px;
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { createPortal } from "react-dom";
|
||||
import styles from "./Modal.module.css";
|
||||
import Button from "../Button/Button";
|
||||
import { ReactNode, MouseEvent } from "react";
|
||||
|
||||
interface ModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
children: ReactNode;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
const Modal = ({ open, onClose, children, title }: ModalProps) => {
|
||||
if (!open) return null;
|
||||
|
||||
const handleOverlayClick = () => {
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleModalClick = (e: MouseEvent<HTMLDivElement>) => {
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
return createPortal(
|
||||
<div className={styles.overlay} onClick={handleOverlayClick}>
|
||||
<div className={`glassCard ${styles.modal}`} onClick={handleModalClick}>
|
||||
{title && <h2>{title}</h2>}
|
||||
{children}
|
||||
<div className={styles.modalFooter}>
|
||||
<Button variant={"default"} onClick={onClose}>Fermer</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
document.body
|
||||
);
|
||||
};
|
||||
|
||||
export default Modal;
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
.themeBtn {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.themeIcon {
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
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>
|
||||
}
|
||||
}*/
|
||||
|
||||
import Button from "../Button/Button";
|
||||
import { useState, useEffect } from "react";
|
||||
import styles from "./ThemeSwitcher.module.css";
|
||||
|
||||
type Theme = "light" | "dark";
|
||||
|
||||
export default function ThemeSwitcher() {
|
||||
const [theme, setTheme] = useState<Theme | "">("");
|
||||
|
||||
useEffect(() => {
|
||||
const savedTheme = localStorage.getItem("theme") as Theme | null;
|
||||
if (savedTheme) {
|
||||
setTheme(savedTheme);
|
||||
} else {
|
||||
setTheme("light");
|
||||
}
|
||||
}, []);
|
||||
|
||||
const switchTheme = () => {
|
||||
const newTheme: Theme = theme === "light" ? "dark" : "light";
|
||||
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,5 @@
|
||||
export const APP_CONFIG = {
|
||||
contactEmail: "contact@example.com",
|
||||
contactPhone: "+33 01 02 03 04 05",
|
||||
newCalendarURL: "https://calendar.google.com/calendar/u/0/r/settings/addbyurl?cid="
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { createContext } from "react";
|
||||
import User from "../../interfaces/user.interface"
|
||||
|
||||
export interface AuthContextType {
|
||||
user: User | null;
|
||||
loading: boolean;
|
||||
update: () => void;
|
||||
login: (email: string, password: string) => Promise<{ status: number }>;
|
||||
logout: () => Promise<void>;
|
||||
}
|
||||
|
||||
|
||||
export const AuthContext = createContext<AuthContextType | null>(null);
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import loginFunction from "../../utils/auth/login.ts";
|
||||
import getUser from "../../utils/getUser.ts";
|
||||
import { AuthContext } from "./AuthContext.ts";
|
||||
import logoutFunction from "../../utils/auth/logout.ts"
|
||||
|
||||
|
||||
export function AuthProvider({ children }) {
|
||||
const [user, setUser] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const u = await getUser();
|
||||
if(u.status === 200) setUser(u.data);
|
||||
setLoading(false);
|
||||
})();
|
||||
}, []);
|
||||
|
||||
const login = async (email, password) => {
|
||||
const res = await loginFunction(email, password);
|
||||
const u = res.status === 200 ? await getUser() : null;
|
||||
if (u) setUser(u.data);
|
||||
return res;
|
||||
};
|
||||
|
||||
|
||||
const logout = async () => {
|
||||
await logoutFunction();
|
||||
setUser(null);
|
||||
};
|
||||
|
||||
const update = async () => {
|
||||
const u = await getUser();
|
||||
setUser(u.data);
|
||||
}
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={{ user, loading, login, logout, update }}>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import { createContext } from "react";
|
||||
|
||||
export const EventDetailContext = createContext(null);
|
||||
@@ -0,0 +1,28 @@
|
||||
import {useState} from 'react';
|
||||
import { EventDetailContext } from "./EventDetailContext.js"
|
||||
import getEventById from "../../utils/events/getEventById.ts";
|
||||
import deleteTaskFunction from "../../utils/tasks/deleteTask.ts";
|
||||
|
||||
export function EventDetailProvider({ children }) {
|
||||
|
||||
const [tasks, setTasks] = useState([]);
|
||||
const [eventInfo, setEventInfo] = useState([]);
|
||||
|
||||
|
||||
const updateEventInfo = async (id) => {
|
||||
const e = await getEventById(id);
|
||||
setEventInfo(e.data);
|
||||
setTasks(e.data.tasks)
|
||||
}
|
||||
|
||||
const deleteTask = async (id, eventId) => {
|
||||
await deleteTaskFunction(id);
|
||||
await updateEventInfo(eventId);
|
||||
}
|
||||
|
||||
return (
|
||||
<EventDetailContext.Provider value={{ tasks, eventInfo, updateEventInfo, deleteTask }}>
|
||||
{children}
|
||||
</EventDetailContext.Provider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { createContext } from "react";
|
||||
|
||||
export type Task = {
|
||||
name: string;
|
||||
description: string;
|
||||
start: string | Date;
|
||||
end: string | Date;
|
||||
};
|
||||
|
||||
export type EventGroup = {
|
||||
date: string;
|
||||
name: string;
|
||||
start: string | Date;
|
||||
tasks: Task[];
|
||||
};
|
||||
|
||||
export type EventContextType = {
|
||||
events: EventGroup[];
|
||||
updateEvent: () => void;
|
||||
addEvent: (name: string, description: string, start: string, end: string) => Promise<void>;
|
||||
removeEvent:(eventId: number) => void;
|
||||
};
|
||||
|
||||
export const EventContext = createContext<EventContextType | null>(null);
|
||||
@@ -0,0 +1,41 @@
|
||||
import { useState, ReactNode } from 'react';
|
||||
import { EventContext, EventGroup, EventContextType } from "./EventContext";
|
||||
import getAllEvents from "../../utils/events/getAllEvents";
|
||||
import createEvent from "../../utils/events/createEvent";
|
||||
import deleteEvent from "../../utils/events/deleteEvent";
|
||||
|
||||
interface EventProviderProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function EventProvider({ children }: EventProviderProps) {
|
||||
const [events, setEvents] = useState<EventGroup[]>([]);
|
||||
|
||||
const updateEvent = async () => {
|
||||
const result = await getAllEvents();
|
||||
setEvents(result.data.data);
|
||||
};
|
||||
|
||||
const addEvent = async (name: string, description: string, start: string, end: string) => {
|
||||
await createEvent(name, description, start, end);
|
||||
await updateEvent();
|
||||
};
|
||||
|
||||
const removeEvent = async (eventId: number) => {
|
||||
await deleteEvent(eventId);
|
||||
await updateEvent();
|
||||
};
|
||||
|
||||
const contextValue: EventContextType = {
|
||||
events,
|
||||
updateEvent,
|
||||
addEvent,
|
||||
removeEvent,
|
||||
};
|
||||
|
||||
return (
|
||||
<EventContext.Provider value={contextValue}>
|
||||
{children}
|
||||
</EventContext.Provider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import { createContext } from "react";
|
||||
|
||||
export const PendingMembersContext = createContext(null);
|
||||
@@ -0,0 +1,37 @@
|
||||
import { useState } from "react";
|
||||
import { PendingMembersContext } from "./PendingMembersContext.js";
|
||||
|
||||
import getUserToValidate from "../../utils/users/getUserToValidate.ts";
|
||||
import getUserById from "../../utils/users/getUserById.ts";
|
||||
|
||||
export function PendingMembersProvider({ children }) {
|
||||
|
||||
const [pendingMembers, setPendingMembers] = useState([]);
|
||||
|
||||
const updatePendingMembers = async () => {
|
||||
const result = await getUserToValidate();
|
||||
const userIds = result.data;
|
||||
|
||||
if (!userIds || userIds.length === 0) {
|
||||
setPendingMembers([]);
|
||||
return;
|
||||
}
|
||||
|
||||
const users = await Promise.all(
|
||||
userIds.map(id => getUserById(id))
|
||||
);
|
||||
|
||||
setPendingMembers(users.map(u => u.data));
|
||||
};
|
||||
|
||||
return (
|
||||
<PendingMembersContext.Provider
|
||||
value={{
|
||||
pendingMembers,
|
||||
updatePendingMembers
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</PendingMembersContext.Provider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
scrollbar-color: #b5b5b5 transparent;
|
||||
}
|
||||
|
||||
html {
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
:root {
|
||||
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
|
||||
--glass-back: rgba(255, 255, 255, 0.54);
|
||||
--glass-border: rgba(255, 255, 255, 0.2);
|
||||
--glass-shadow: 0 8px 32px rgba(0, 0, 0, 0.1),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.3),
|
||||
inset 0 -1px 0 rgba(255, 255, 255, 0.05),
|
||||
inset 0 0 8px 4px rgba(255, 255, 255, 0.2);
|
||||
|
||||
--glass-border-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);
|
||||
--glass-border-back: rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
place-items: center;
|
||||
width:100%;
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
body.dark {
|
||||
--glass-back: rgba(160, 160, 160, 0.54);
|
||||
--glass-border: rgba(160, 160, 160, 0.2);
|
||||
--glass-shadow: 0 8px 32px rgba(0, 0, 0, 0.1),
|
||||
inset 0 1px 0 rgba(160, 160, 160, 0.3),
|
||||
inset 0 -1px 0 rgba(160, 160, 160, 0.05),
|
||||
inset 0 0 8px 4px rgba(160, 160, 160, 0.2);
|
||||
|
||||
--glass-border-shadow:
|
||||
inset 1.25px 1.25px 1px rgba(180, 180, 180, 0.6),
|
||||
inset -1.25px -1.25px 1px rgba(180, 180, 180, 0.6),
|
||||
inset -0.25px 0.25px 1px rgba(0, 0, 0, 0.4),
|
||||
inset 0.25px -0.25px 1px rgba(0, 0, 0, 0.4);
|
||||
--glass-border-back: rgba(160, 160, 160, 0.25);
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 1.5rem;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: #646cff;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
|
||||
.glassCard {
|
||||
padding: 10px 20px 10px 20px;
|
||||
background: var(--glass-back);
|
||||
backdrop-filter: blur(3px);
|
||||
-webkit-backdrop-filter: blur(3px);
|
||||
border-radius: 20px;
|
||||
border: 1px solid var(--glass-border);
|
||||
box-shadow: var(--glass-shadow);
|
||||
}
|
||||
|
||||
.glassBorder{
|
||||
padding: 10px 20px 10px 20px;
|
||||
border-radius: 20px;
|
||||
border: none;
|
||||
background: var(--glass-border-back);
|
||||
box-shadow: var(--glass-border-shadow);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3.2em;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
|
||||
button:hover {
|
||||
border-color: #646cff;
|
||||
}
|
||||
button:focus,
|
||||
button:focus-visible {
|
||||
outline: 4px auto -webkit-focus-ring-color;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
.content{
|
||||
padding: 0.7rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
|
||||
import type User from "./user.interface";
|
||||
export interface AuthContextType {
|
||||
user: User | null;
|
||||
update: () => void;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
export default interface Notification {
|
||||
id: string;
|
||||
data: {
|
||||
message: string;
|
||||
};
|
||||
read_at: string | null;
|
||||
created_at: string;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export default interface Response<T> {
|
||||
status: number;
|
||||
data: T;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
export default interface TaskType {
|
||||
id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
location: string;
|
||||
start: string;
|
||||
end: string;
|
||||
max_participants: number;
|
||||
events_id: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
pivot: {
|
||||
user_id: number;
|
||||
task_id: number;
|
||||
};
|
||||
}
|
||||
export default interface Task {
|
||||
id: number;
|
||||
title?: string;
|
||||
completed?: boolean;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import TaskType from "./task.interface";
|
||||
|
||||
export default interface User {
|
||||
id: number;
|
||||
name: string;
|
||||
lastname: string;
|
||||
role: string;
|
||||
email: string;
|
||||
phone: string | null;
|
||||
created_at: string;
|
||||
isAdmin: boolean;
|
||||
validate: number;
|
||||
tasks: TaskType[];
|
||||
profile_photo_path?: string | null,
|
||||
email_notifications: number,
|
||||
web_notifications: number;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { Outlet } from "react-router";
|
||||
import Footer from "./components/Footer/Footer.tsx"
|
||||
import Header from "./components/Header/Header.tsx";
|
||||
import Background from "./components/Background/Background.tsx";
|
||||
import { Navigate } from "react-router";
|
||||
import { useContext} from "react";
|
||||
import { AuthContext } from "./contexts/Auth/AuthContext.ts";
|
||||
import Loading from "./components/ui/Loading/Loading.tsx";
|
||||
|
||||
|
||||
function Layout(){
|
||||
|
||||
const { user, loading } = useContext(AuthContext);
|
||||
|
||||
if (loading) return <Background>
|
||||
<Loading />
|
||||
</Background>;
|
||||
if (!user) return <Navigate to="/login" />;
|
||||
if(user && user.validate === 0) return <Navigate to="/error/validation" />;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Background>
|
||||
<div className="content">
|
||||
<Header />
|
||||
<Outlet />
|
||||
<Footer />
|
||||
</div>
|
||||
</Background>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Layout;
|
||||
@@ -0,0 +1,24 @@
|
||||
import {StrictMode} from 'react'
|
||||
import {createRoot} from 'react-dom/client'
|
||||
import './index.css'
|
||||
import {RouterProvider} from "react-router";
|
||||
import router from './router.js';
|
||||
import {AuthProvider} from "./contexts/Auth/AuthProvider.jsx";
|
||||
import {EventProvider} from "./contexts/Events/EventProvider.tsx";
|
||||
import {EventDetailProvider} from "./contexts/EventDetail/EventDetailProvider.jsx";
|
||||
import {PendingMembersProvider} from "./contexts/PendingMembers/PendingMembersProvider.jsx";
|
||||
|
||||
|
||||
createRoot(document.getElementById('root')).render(
|
||||
<StrictMode>
|
||||
<AuthProvider>
|
||||
<PendingMembersProvider>
|
||||
<EventProvider>
|
||||
<EventDetailProvider>
|
||||
<RouterProvider router={router}/>
|
||||
</EventDetailProvider>
|
||||
</EventProvider>
|
||||
</PendingMembersProvider>
|
||||
</AuthProvider>
|
||||
</StrictMode>,
|
||||
)
|
||||
@@ -0,0 +1,30 @@
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: 100%;
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
gap: 20px;
|
||||
}
|
||||
.rightSection {
|
||||
width: 60%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 760px) {
|
||||
.container {
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
margin-top:25px;
|
||||
}
|
||||
|
||||
.rightSection {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
margin-top:0;
|
||||
margin-bottom:25px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { useContext } from "react";
|
||||
import styles from "./AdminPage.module.css";
|
||||
import IncompleteEvents from "./components/IncompleteEvent/IncompleteEvent";
|
||||
import PendingMembers from "./components/PendingMembers/PendingMembers";
|
||||
import { AuthContext } from "../../contexts/Auth/AuthContext";
|
||||
import user from "../../interfaces/user.interface"
|
||||
import { useNavigate } from "react-router";
|
||||
import RegisterMember from "./components/RegisterMember/RegisterMember";
|
||||
|
||||
|
||||
|
||||
function AdminPage() {
|
||||
const authContext = useContext(AuthContext);
|
||||
const navigate = useNavigate();
|
||||
|
||||
if (!authContext?.user?.isAdmin) {
|
||||
navigate("/");
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<RegisterMember />
|
||||
<div className={styles.rightSection}>
|
||||
<IncompleteEvents />
|
||||
<PendingMembers />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default AdminPage;
|
||||
@@ -0,0 +1,40 @@
|
||||
import React, { useContext } from "react";
|
||||
import styles from "./IncompleteEvent.module.css";
|
||||
import { EventContext } from "../../../../contexts/Events/EventContext.ts";
|
||||
|
||||
function IncompleteEvent() {
|
||||
|
||||
const { events } = useContext(EventContext);
|
||||
|
||||
return (
|
||||
<div className={`${styles.rightBlock} glassCard`}>
|
||||
<h2>Derniers événements</h2>
|
||||
<div className={styles.eventsContainer}>
|
||||
{Array.isArray(events) && events.length > 0 ? (
|
||||
events.map((event) => (
|
||||
<div key={event.id} className={`glassBorder ${styles.eventCard}`}>
|
||||
<h3>{event.name}</h3>
|
||||
<p className={styles.eventDate}>
|
||||
{new Date(event.start).toLocaleDateString("fr-FR")} - {new Date(event.end).toLocaleDateString("fr-FR")}
|
||||
</p>
|
||||
<p className={styles.eventDescription}>{event.description}</p>
|
||||
<h4>Tâches associées :</h4>
|
||||
<ul className={styles.tasksList}>
|
||||
{event.tasks?.length ? (
|
||||
event.tasks.map((task, taskIndex) => (
|
||||
<li key={taskIndex}>{task.name}</li>
|
||||
))
|
||||
) : (
|
||||
<li>Aucune tâche associée.</li>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
))) : (
|
||||
<p>Aucun événement n'existe pour le moment</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default IncompleteEvent;
|
||||
@@ -0,0 +1,114 @@
|
||||
.rightBlock {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.rightBlock > h2 {
|
||||
font-size: 29px;
|
||||
}
|
||||
|
||||
.eventsContainer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 10px;
|
||||
margin-top: 5px;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
padding-bottom: 10px;
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
.eventCard {
|
||||
min-width: calc((100% - 20px) / 3);
|
||||
max-width: calc((100% - 20px) / 3);
|
||||
min-height: 250px;
|
||||
max-height: 250px;
|
||||
overflow-y: auto;
|
||||
text-align: center;
|
||||
box-sizing: border-box;
|
||||
opacity: 0;
|
||||
transform: translateX(20px);
|
||||
animation: slideInEvent 1s ease forwards;
|
||||
flex-shrink: 0;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.eventCard ul {
|
||||
align-items: center;
|
||||
margin-left: 5%;
|
||||
}
|
||||
|
||||
.eventCard ul li {
|
||||
text-align: left;
|
||||
list-style-type: circle;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.eventCard > h3 {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
/* Animation des cartes avec délais */
|
||||
.eventCard:nth-child(1) { animation-delay: 0s; }
|
||||
.eventCard:nth-child(2) { animation-delay: 0.1s; }
|
||||
.eventCard:nth-child(3) { animation-delay: 0.2s; }
|
||||
.eventCard:nth-child(4) { animation-delay: 0.3s; }
|
||||
.eventCard:nth-child(5) { animation-delay: 0.4s; }
|
||||
.eventCard:nth-child(6) { animation-delay: 0.5s; }
|
||||
|
||||
.emptyEvent {
|
||||
min-width: calc((100% - 20px) / 3);
|
||||
max-width: calc((100% - 20px) / 3);
|
||||
min-height: 250px;
|
||||
max-height: 250px;
|
||||
padding: 10px;
|
||||
text-align: center;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
opacity: 1;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@keyframes slideInEvent {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 760px) {
|
||||
.eventCard {
|
||||
min-width: 100%;
|
||||
min-height: 200px;
|
||||
max-height: fit-content;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.eventCard h3 {
|
||||
font-size: 17px;
|
||||
}
|
||||
|
||||
.eventsContainer {
|
||||
flex-direction: column;
|
||||
max-height: 300px;
|
||||
margin-top: 15px;
|
||||
height: fit-content;
|
||||
overflow-y: auto;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.rightBlock > h2 {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.emptyEvent {
|
||||
min-width: 100%;
|
||||
min-height: 200px;
|
||||
max-height: fit-content;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import React, {useState, useEffect, useContext} from "react";
|
||||
import Button from "../../../../components/ui/Button/Button.tsx";
|
||||
import styles from "./PendingMembers.module.css";
|
||||
import Modal from "../../../../components/ui/Modal/Modal.tsx";
|
||||
import validateUser from "../../../../utils/users/validateUser.ts";
|
||||
import deleteOtherUser from "../../../../utils/users/deleteOtherUser.ts";
|
||||
import { PendingMembersContext } from "../../../../contexts/PendingMembers/PendingMembersContext.js";
|
||||
|
||||
function PendingMembers() {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [message, setMessage] = useState("");
|
||||
const [title, setTitle] = useState("");
|
||||
const { pendingMembers, updatePendingMembers } = useContext(PendingMembersContext);
|
||||
|
||||
useEffect(() => {
|
||||
updatePendingMembers();
|
||||
}, []);
|
||||
|
||||
const handleValidate = async (id, name) => {
|
||||
const result = await validateUser(id);
|
||||
|
||||
if(result.status === 200) {
|
||||
setTitle("Utilisateur accepté avec succès")
|
||||
setMessage(`L'utilisateur ${name} a été accepté avec succès`)
|
||||
} else {
|
||||
setTitle("Erreur")
|
||||
setMessage(`Une erreur est survenue durant l'acceptation de l'utilisateur ${name}`)
|
||||
}
|
||||
setOpen(true);
|
||||
updatePendingMembers();
|
||||
}
|
||||
|
||||
const handleRefuse = async (id, name) => {
|
||||
const result = await deleteOtherUser(id);
|
||||
|
||||
if(result.status === 200) {
|
||||
setTitle("Utilisateur refusé avec succès")
|
||||
setMessage(`L'utilisateur ${name} a été refusé avec succès`)
|
||||
setOpen(true);
|
||||
} else {
|
||||
setTitle("Erreur")
|
||||
setMessage(`Une erreur est survenue durant le refus de l'utilisateur ${name}`)
|
||||
setOpen(true);
|
||||
}
|
||||
|
||||
updatePendingMembers();
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`${styles.rightBlock} glassCard`}>
|
||||
<h2>Membres en attente de validation</h2>
|
||||
|
||||
<div className={styles.membersContainer}>
|
||||
|
||||
{pendingMembers.length === 0 && <p>Aucun membre en attente</p>}
|
||||
|
||||
{pendingMembers.map((member, index) => (
|
||||
|
||||
<div key={index} className={`glassBorder ${styles.memberCard}`}>
|
||||
|
||||
<p>{member.name} {member.lastname}</p>
|
||||
|
||||
<div className={styles.buttonGroup}>
|
||||
<Button variant="primary" onClick={() => handleValidate(member.id, `${member.name} ${member.lastname}`)}>Valider</Button>
|
||||
<Button variant="danger" onClick={() => handleRefuse(member.id, `${member.name} ${member.lastname}`)}>Refuser</Button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
))}
|
||||
|
||||
</div>
|
||||
|
||||
<Modal open={open} onClose={() => setOpen(false)} title={title}>
|
||||
{message}
|
||||
</Modal>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default PendingMembers;
|
||||
@@ -0,0 +1,76 @@
|
||||
.rightBlock {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
||||
.rightBlock > h2 {
|
||||
font-size: 29px;
|
||||
}
|
||||
|
||||
.membersContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
margin-top: 20px;
|
||||
max-height: 185px;
|
||||
overflow-y: auto;
|
||||
padding-right: 5px;
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
.memberCard {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
flex-shrink: 0;
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
animation: slideInMember 0.5s ease forwards;
|
||||
}
|
||||
|
||||
.memberCard:nth-child(1) { animation-delay: 0s; }
|
||||
.memberCard:nth-child(2) { animation-delay: 0.1s; }
|
||||
.memberCard:nth-child(3) { animation-delay: 0.2s; }
|
||||
|
||||
.memberCard p {
|
||||
font-weight: bold;
|
||||
font-size: 20px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 250px;
|
||||
}
|
||||
|
||||
.buttonGroup {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
justify-content: right;
|
||||
}
|
||||
|
||||
@keyframes slideInMember {
|
||||
from { opacity: 0; transform: translateY(20px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
@media screen and (max-width: 760px) {
|
||||
.membersContainer {
|
||||
max-height: 200px;
|
||||
}
|
||||
|
||||
.rightBlock > h2{
|
||||
font-size: 18px;
|
||||
}
|
||||
.memberCard p{
|
||||
font-size: 15px;
|
||||
max-width: 150px;
|
||||
}
|
||||
}
|
||||
@media screen and (min-width: 761px) {
|
||||
.memberCard p{
|
||||
white-space: normal;
|
||||
overflow: visible;
|
||||
text-overflow: clip;
|
||||
max-width: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
.leftBlock {
|
||||
width: 100%;
|
||||
max-width: 720px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
min-height: calc(100vh - 260px);
|
||||
box-sizing: border-box;
|
||||
padding: 20px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.registerMemberHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.registerForm {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.registerForm input {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.registerForm button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.modalContent {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.leftBlock {
|
||||
width: 70%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.leftBlock {
|
||||
width: 40%;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import React, { useState } from "react"
|
||||
import styles from "./RegisterMember.module.css"
|
||||
import TextInput from "../../../../components/ui/Input/Input"
|
||||
import Button from "../../../../components/ui/Button/Button";
|
||||
import Response from "../../../../interfaces/response.interface";
|
||||
import ResponseData from "./responseData.inteface";
|
||||
import adminCreateUser from "../../../../utils/users/adminCreateUser";
|
||||
import Modal from "../../../../components/ui/Modal/Modal";
|
||||
|
||||
|
||||
const RegisterMember: React.FC = () => {
|
||||
|
||||
const [name, setName] = useState<string>("");
|
||||
const [lastname, setLastName] = useState<string>("");
|
||||
const [email, setEmail] = useState<string>("");
|
||||
const [phone, setPhone] = useState<string>("");
|
||||
const [password, setPassword] = useState<string>("");
|
||||
|
||||
const [open, setOpen] = useState<boolean>(false);
|
||||
const [title, setTitle] = useState<string>("");
|
||||
const [message, setMessage] = useState<string>("");
|
||||
|
||||
|
||||
const handleCreate = async (): Promise<void> => {
|
||||
|
||||
if(name !== "" && lastname !== "" && email !== "" && phone !== "" ) {
|
||||
const response: Response<ResponseData> = await adminCreateUser(email, name, lastname, phone);
|
||||
if(response.status === 200) {
|
||||
setPassword(response.data.password);
|
||||
setTitle(`Utilisateur enregistré avec succès`);
|
||||
setMessage(`L'utilisateur ${name} ${lastname} a été enregistré avec le mot de passe: ${response.data.password}`);
|
||||
setOpen(true);
|
||||
} else {
|
||||
setTitle(`Erreur`);
|
||||
setMessage(`Une erreur est survenue lors de l'enregistrement de ${name} ${lastname}`);
|
||||
setOpen(true);
|
||||
}
|
||||
} else {
|
||||
setTitle("Erreur");
|
||||
setMessage("Un des champs n'a pas été rempli");
|
||||
setOpen(true);
|
||||
}
|
||||
}
|
||||
|
||||
const showPassword = (): void => {
|
||||
setTitle(`Mot de passe de ${name} ${lastname}`);
|
||||
setMessage(password);
|
||||
setOpen(true);
|
||||
}
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter') {
|
||||
handleCreate();
|
||||
}
|
||||
};
|
||||
|
||||
return <div className={`${styles.leftBlock} glassCard`}>
|
||||
<div className={styles.registerMemberHeader}>
|
||||
<h2>Enregister un bénévole</h2>
|
||||
</div>
|
||||
|
||||
<div className={styles.registerForm} onKeyDown={handleKeyDown}>
|
||||
<TextInput placeholder={"Prenom"} type={"text"} value={name} onChange={e => setName(e.target.value)} />
|
||||
<TextInput placeholder={"Nom"} type={"text"} value={lastname} onChange={e => setLastName(e.target.value)} />
|
||||
<TextInput placeholder={"Adresse Email"} type={"email"} value={email} onChange={e => setEmail(e.target.value)} />
|
||||
<TextInput placeholder={"Numéro de téléphone"} type={"tel"} value={phone} onChange={e => setPhone(e.target.value)} />
|
||||
|
||||
{password !== "" && <Button onClick={showPassword}> Afficher le mot de passe </Button>}
|
||||
|
||||
<Button variant={"default"} onClick={handleCreate}>Enregistrer</Button>
|
||||
</div>
|
||||
|
||||
<Modal open={open} onClose={() => setOpen(false)} title={title}>
|
||||
<p className={styles.modalContent}>{message}</p>
|
||||
</Modal>
|
||||
|
||||
</div>
|
||||
}
|
||||
|
||||
export default RegisterMember
|
||||
@@ -0,0 +1,4 @@
|
||||
export default interface ResponseData {
|
||||
message: string;
|
||||
password: string;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
.title {
|
||||
color: white;
|
||||
font-size: xx-large;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
z-index: 2;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
|
||||
p {
|
||||
font-size: large;
|
||||
white-space: pre-line;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useSearchParams, useNavigate } from "react-router";
|
||||
import confirmEmailChange from "../../utils/users/confirmEmailChange";
|
||||
import styles from "./ConfirmEmailChangePage.module.css";
|
||||
import Background from "../../components/Background/Background";
|
||||
|
||||
export default function ConfirmEmailChange() {
|
||||
const [searchParams] = useSearchParams();
|
||||
const navigate = useNavigate();
|
||||
const [message, setMessage] = useState<string>('Validation en cours...');
|
||||
const [success, setSuccess] = useState<boolean | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const token = searchParams.get('token');
|
||||
if (!token) {
|
||||
setMessage('Token manquant.');
|
||||
setSuccess(false);
|
||||
return;
|
||||
}
|
||||
|
||||
confirmEmailChange(token)
|
||||
.then(() => {
|
||||
setMessage('Votre adresse email a bien été mise à jour.\nVous allez être redirigé vers la page de connexion...');
|
||||
setSuccess(true);
|
||||
setTimeout(() => navigate('/login'), 3000);
|
||||
})
|
||||
.catch(() => {
|
||||
setMessage('Token invalide ou expiré.');
|
||||
setSuccess(false);
|
||||
});
|
||||
}, []);
|
||||
|
||||
return <Background>
|
||||
<div className={styles.content}>
|
||||
<h1 className={styles.title}> Validation de votre adresse email </h1>
|
||||
<div className={`${styles.container} glassCard`}>
|
||||
<p>{message}</p>
|
||||
</div>
|
||||
</div>
|
||||
</Background>
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
.allContent{
|
||||
margin: 16px 0 16px 0;
|
||||
align-self: center;
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
|
||||
.textImage {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.eventImage {
|
||||
width: 40%;
|
||||
aspect-ratio: 16 / 9;
|
||||
background-color: #e0e0e0;
|
||||
border-radius: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.eventInfo {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.eventInfo p {
|
||||
white-space: normal;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.eventImage {
|
||||
width: 40%;
|
||||
height: 100%;
|
||||
aspect-ratio: 16 / 9;
|
||||
background-color: #e0e0e0;
|
||||
border-radius: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.registerButton:hover {
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.tasks {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.eventContent {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.manageBtn {
|
||||
height: fit-content;
|
||||
}
|
||||
|
||||
.modalContent {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.manageEvent {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
|
||||
.eventContent {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.textImage {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.manageEvent {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.eventImage {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.manageEvent{
|
||||
margin: 16px 0 16px 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
import {useEffect, useContext, useRef} from "react";
|
||||
import styles from "./EventDetailPage.module.css";
|
||||
import { useParams } from "react-router";
|
||||
import { formatDateLetter } from "../../utils/date/formatDateLetter";
|
||||
import EventTask from "./components/EventTask/EventTask";
|
||||
import { AuthContext } from "../../contexts/Auth/AuthContext";
|
||||
import { EventDetailContext } from "../../contexts/EventDetail/EventDetailContext";
|
||||
import CreateTaskBtn from "./components/CreateTaskBtn/CreateTaskBtn";
|
||||
import DeleteEventBtn from "./components/DeleteEventBtn/DeleteEventBtn";
|
||||
import EditEventBtn from "./components/EditEventBtn/EditEventBtn";
|
||||
import uploadEventPhoto from "../../utils/events/uploadEventPhoto";
|
||||
|
||||
function EventDetailPage() {
|
||||
const params = useParams();
|
||||
const id = params.id as string;
|
||||
|
||||
const { user } = useContext(AuthContext) as any;
|
||||
const { tasks, updateEventInfo, eventInfo } = useContext(EventDetailContext) as any;
|
||||
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
||||
const imageUrl = eventInfo?.event_photo_path
|
||||
? `${import.meta.env.VITE_API_URL}/storage/${eventInfo.event_photo_path}`
|
||||
: "/empty.png";
|
||||
|
||||
console.log(eventInfo);
|
||||
useEffect(() => {
|
||||
if (id) {
|
||||
updateEventInfo(id);
|
||||
}
|
||||
}, []);
|
||||
const handleImageClick = () => {
|
||||
if (user?.isAdmin && fileInputRef.current) {
|
||||
fileInputRef.current?.click();
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileChange = async (e: any) => {
|
||||
const file = e.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
await uploadEventPhoto(Number(id), file);
|
||||
updateEventInfo(id);
|
||||
};
|
||||
return (
|
||||
<div className={styles.allContent}>
|
||||
<div className={`glassCard`}>
|
||||
<div className={styles.textImage}>
|
||||
<img
|
||||
src={imageUrl}
|
||||
alt="Photo événement"
|
||||
className={styles.eventImage}
|
||||
onClick={handleImageClick}
|
||||
style={{ cursor: user?.isAdmin ? "pointer" : "default" }}
|
||||
/>
|
||||
<input
|
||||
type="file"
|
||||
ref={fileInputRef}
|
||||
style={{ display: "none" }}
|
||||
onChange={handleFileChange}
|
||||
/>
|
||||
|
||||
<div className={styles.eventInfo}>
|
||||
<h2>{eventInfo?.name}</h2>
|
||||
|
||||
<div className={styles.eventContent}>
|
||||
<div>
|
||||
<p>{eventInfo && formatDateLetter(eventInfo.start)}</p>
|
||||
<p>{eventInfo?.description}</p>
|
||||
</div>
|
||||
|
||||
{user?.isAdmin && (
|
||||
<div className={styles.manageEvent}>
|
||||
<EditEventBtn id={id} />
|
||||
<DeleteEventBtn id={id} />
|
||||
<CreateTaskBtn id={id} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>Tâches</h2>
|
||||
|
||||
<div className={styles.tasks}>
|
||||
{tasks?.map((task: any, index: number) => (
|
||||
<EventTask key={task.id} taskId={task.id} index={index} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default EventDetailPage;
|
||||
@@ -0,0 +1,19 @@
|
||||
.usersList {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.userCard {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.userName {
|
||||
max-width: 200px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
import {ReactNode, useContext, useEffect, useState} from "react";
|
||||
import Button from "../../../../components/ui/Button/Button";
|
||||
import Modal from "../../../../components/ui/Modal/Modal";
|
||||
import User from "../../../Volunteers/interfaces/user.interface";
|
||||
import getAllUser from "../../../../utils/users/getAllUsers";
|
||||
import Loading from "../../../../components/ui/Loading/Loading";
|
||||
import getUserById from "../../../../utils/users/getUserById";
|
||||
import styles from "./AssignUserBtn.module.css";
|
||||
import {AuthContext} from "../../../../contexts/Auth/AuthContext";
|
||||
import assignOtherUser from "../../../../utils/tasks/assignOtherUser";
|
||||
import unAssignOtherUser from "../../../../utils/tasks/unAssignOtherUser";
|
||||
import {EventDetailContext} from "../../../../contexts/EventDetail/EventDetailContext";
|
||||
|
||||
interface AssignUserBtnProps {
|
||||
taskId: string | number;
|
||||
eventId: string | number;
|
||||
}
|
||||
|
||||
export default function AssignUserBtn({ taskId, eventId }: AssignUserBtnProps): ReactNode {
|
||||
|
||||
const [open, setOpen] = useState<boolean>(false);
|
||||
const [users, setUsers] = useState<Array<User>>([]);
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
|
||||
const { user } = useContext(AuthContext) as any;
|
||||
const { updateEventInfo } = useContext(EventDetailContext) as any;
|
||||
|
||||
if (!user?.isAdmin) return null;
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
setLoading(true);
|
||||
|
||||
const result = await getAllUser();
|
||||
|
||||
if (result.status !== 200 || !Array.isArray(result.data)) {
|
||||
console.error("Erreur lors du chargement des utilisateurs", result);
|
||||
return;
|
||||
}
|
||||
|
||||
const usersData = await Promise.all(
|
||||
result.data.map(async (id: number): Promise<User | null> => {
|
||||
const res = await getUserById(id);
|
||||
|
||||
if (res.status === 200) {
|
||||
return res.data as User;
|
||||
}
|
||||
|
||||
return null;
|
||||
})
|
||||
);
|
||||
|
||||
setUsers(usersData.filter((user): user is User => user !== null));
|
||||
setLoading(false);
|
||||
})();
|
||||
}, []);
|
||||
|
||||
const [assignedIds, setAssignedIds] = useState<Set<number>>(new Set());
|
||||
|
||||
useEffect(() => {
|
||||
setAssignedIds(new Set(
|
||||
users.filter(u => u.tasks.some(t => t.id === taskId)).map(u => u.id)
|
||||
));
|
||||
}, [users, taskId]);
|
||||
|
||||
const handleAssign = async (selectedUser: User) => {
|
||||
setAssignedIds(prev => new Set(prev).add(selectedUser.id));
|
||||
await assignOtherUser(taskId as number, selectedUser.id);
|
||||
updateEventInfo(eventId);
|
||||
};
|
||||
|
||||
const handleUnassign = async (selectedUser: User) => {
|
||||
setAssignedIds(prev => { const next = new Set(prev); next.delete(selectedUser.id); return next; });
|
||||
await unAssignOtherUser(taskId as number, selectedUser.id);
|
||||
updateEventInfo(eventId);
|
||||
};
|
||||
|
||||
return <>
|
||||
<Button variant={"default"} onClick={() => setOpen(true)}>Assigner</Button>
|
||||
|
||||
<Modal open={open} onClose={() => setOpen(false)} title={"Assigner un utilisateur à cette tâche"}>
|
||||
<div className={styles.usersList}>
|
||||
{loading ? <Loading /> : users.map((u: User) => (
|
||||
<div key={u.id} className={`${styles.userCard} glassBorder`}>
|
||||
<p className={styles.userName}>{u.name} {u.lastname}</p>
|
||||
{assignedIds.has(u.id) ? (
|
||||
<Button variant={"danger"} onClick={() => handleUnassign(u)}>
|
||||
Désassigner
|
||||
</Button>
|
||||
) : (
|
||||
<Button onClick={() => handleAssign(u)}>
|
||||
Assigner
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Modal>
|
||||
</>
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
import styles from "../../EventDetailPage.module.css";
|
||||
import Button from "../../../../components/ui/Button/Button.tsx";
|
||||
import {useContext, useState} from "react";
|
||||
import TextInput from "../../../../components/ui/Input/Input.tsx";
|
||||
import Modal from "../../../../components/ui/Modal/Modal.tsx";
|
||||
import createTaskApi from "../../../../utils/tasks/createTask.ts";
|
||||
import {EventDetailContext} from "../../../../contexts/EventDetail/EventDetailContext.js";
|
||||
|
||||
export default function CreateTaskBtn({ id }) {
|
||||
|
||||
const [openTaskModal, setOpenTaskModal] = useState(false);
|
||||
|
||||
const [taskName, setTaskName] = useState("");
|
||||
const [taskDescription, setTaskDescription] = useState("");
|
||||
const [taskLocation, setTaskLocation] = useState("");
|
||||
const [maxParticipants, setMaxParticipants] = useState("");
|
||||
const [startTime, setStartTime] = useState("");
|
||||
const [endTime, setEndTime] = useState("");
|
||||
|
||||
const { updateEventInfo } = useContext(EventDetailContext)
|
||||
|
||||
const createTask = async () => {
|
||||
const data = await createTaskApi({
|
||||
eventId: id,
|
||||
name: taskName,
|
||||
description: taskDescription,
|
||||
start: startTime,
|
||||
end: endTime,
|
||||
location: taskLocation,
|
||||
maxParticipants,
|
||||
});
|
||||
|
||||
if (data?.status === 200) {
|
||||
setOpenTaskModal(false);
|
||||
setTaskName("");
|
||||
setTaskDescription("");
|
||||
setTaskLocation("");
|
||||
setMaxParticipants("");
|
||||
setStartTime("");
|
||||
setEndTime("");
|
||||
|
||||
updateEventInfo(id)
|
||||
} else {
|
||||
console.log("⚠️ Échec de la création de la task");
|
||||
}
|
||||
};
|
||||
|
||||
return <>
|
||||
|
||||
<Button variant={"default"} className={styles.manageBtn} onClick={() => setOpenTaskModal(true)}>Créer tâche</Button>
|
||||
|
||||
<Modal open={openTaskModal} onClose={() => setOpenTaskModal(false)} title={"Créer tâche"}>
|
||||
<div className={styles.modalContent}>
|
||||
|
||||
<div>
|
||||
<p>Nom de la tâche</p>
|
||||
<TextInput
|
||||
value={taskName}
|
||||
onChange={(e) => setTaskName(e.target.value)}
|
||||
placeholder="Nom de la tâche"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p>Description</p>
|
||||
<TextInput
|
||||
value={taskDescription}
|
||||
onChange={(e) => setTaskDescription(e.target.value)}
|
||||
placeholder="Description de la tâche"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p>Lieu</p>
|
||||
<TextInput
|
||||
value={taskLocation}
|
||||
onChange={(e) => setTaskLocation(e.target.value)}
|
||||
placeholder="Lieu"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p>Nombre de participants max</p>
|
||||
<TextInput
|
||||
type="number"
|
||||
value={maxParticipants}
|
||||
onChange={(e) => setMaxParticipants(e.target.value)}
|
||||
placeholder="Participants max"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p>Heure de début</p>
|
||||
<TextInput
|
||||
type="datetime-local"
|
||||
value={startTime}
|
||||
onChange={(e) => setStartTime(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p>Heure de fin</p>
|
||||
<TextInput
|
||||
type="datetime-local"
|
||||
value={endTime}
|
||||
onChange={(e) => setEndTime(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button variant="primary" onClick={createTask}> Créer la tâche </Button>
|
||||
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
</>
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
import styles from "../../EventDetailPage.module.css";
|
||||
import Button from "../../../../components/ui/Button/Button";
|
||||
import { useContext, useState } from "react";
|
||||
import TextInput from "../../../../components/ui/Input/Input";
|
||||
import Modal from "../../../../components/ui/Modal/Modal";
|
||||
import createTaskApi from "../../../../utils/tasks/createTask";
|
||||
import { EventDetailContext } from "../../../../contexts/EventDetail/EventDetailContext";
|
||||
|
||||
type CreateTaskBtnProps = {
|
||||
id: string | number;
|
||||
};
|
||||
|
||||
export default function CreateTaskBtn({ id }: CreateTaskBtnProps) {
|
||||
const [openTaskModal, setOpenTaskModal] = useState(false);
|
||||
|
||||
const [taskName, setTaskName] = useState<string>("");
|
||||
const [taskDescription, setTaskDescription] = useState<string>("");
|
||||
const [taskLocation, setTaskLocation] = useState<string>("");
|
||||
const [maxParticipants, setMaxParticipants] = useState<string>("");
|
||||
const [startTime, setStartTime] = useState<string>("");
|
||||
const [endTime, setEndTime] = useState<string>("");
|
||||
|
||||
const { updateEventInfo } = useContext(EventDetailContext) as any;
|
||||
|
||||
const createTask = async () => {
|
||||
const data = await createTaskApi({
|
||||
eventId: id,
|
||||
name: taskName,
|
||||
description: taskDescription,
|
||||
start: startTime,
|
||||
end: endTime,
|
||||
location: taskLocation,
|
||||
maxParticipants,
|
||||
});
|
||||
|
||||
if (data?.status === 200) {
|
||||
setOpenTaskModal(false);
|
||||
setTaskName("");
|
||||
setTaskDescription("");
|
||||
setTaskLocation("");
|
||||
setMaxParticipants("");
|
||||
setStartTime("");
|
||||
setEndTime("");
|
||||
|
||||
updateEventInfo(id);
|
||||
} else {
|
||||
console.log("⚠️ Échec de la création de la task");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
variant={"default"}
|
||||
className={styles.manageBtn}
|
||||
onClick={() => setOpenTaskModal(true)}
|
||||
>
|
||||
Créer tâche
|
||||
</Button>
|
||||
|
||||
<Modal
|
||||
open={openTaskModal}
|
||||
onClose={() => setOpenTaskModal(false)}
|
||||
title={"Créer tâche"}
|
||||
>
|
||||
<div className={styles.modalContent}>
|
||||
<div>
|
||||
<p>Nom de la tâche</p>
|
||||
<TextInput
|
||||
value={taskName}
|
||||
onChange={(e: any) => setTaskName(e.target.value)}
|
||||
placeholder="Nom de la tâche"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p>Description</p>
|
||||
<TextInput
|
||||
value={taskDescription}
|
||||
onChange={(e: any) => setTaskDescription(e.target.value)}
|
||||
placeholder="Description de la tâche"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p>Lieu</p>
|
||||
<TextInput
|
||||
value={taskLocation}
|
||||
onChange={(e: any) => setTaskLocation(e.target.value)}
|
||||
placeholder="Lieu"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p>Nombre maximum de participants</p>
|
||||
<TextInput
|
||||
type="number"
|
||||
value={maxParticipants}
|
||||
onChange={(e: any) => setMaxParticipants(e.target.value)}
|
||||
placeholder="Participants max"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p>Heure de début</p>
|
||||
<TextInput
|
||||
type="datetime-local"
|
||||
value={startTime}
|
||||
onChange={(e: any) => setStartTime(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p>Heure de fin</p>
|
||||
<TextInput
|
||||
type="datetime-local"
|
||||
value={endTime}
|
||||
onChange={(e: any) => setEndTime(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button variant="default" onClick={createTask}>
|
||||
Créer la tâche
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import styles from "../../EventDetailPage.module.css";
|
||||
import Button from "../../../../components/ui/Button/Button";
|
||||
import Modal from "../../../../components/ui/Modal/Modal";
|
||||
import { useContext, useState } from "react";
|
||||
import { EventDetailContext } from "../../../../contexts/EventDetail/EventDetailContext";
|
||||
import { EventContext } from "../../../../contexts/Events/EventContext";
|
||||
import { useNavigate } from "react-router";
|
||||
|
||||
type DeleteEventBtnProps = {
|
||||
id: string | number;
|
||||
};
|
||||
|
||||
export default function DeleteEventBtn({ id }: DeleteEventBtnProps) {
|
||||
const { eventInfo } = useContext(EventDetailContext) as any;
|
||||
const { removeEvent } = useContext(EventContext) as any;
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleDelete = () => {
|
||||
setOpen(true);
|
||||
};
|
||||
|
||||
const delEvent = async () => {
|
||||
const result = await removeEvent(id);
|
||||
|
||||
if (result.status === 200 || result.status === 204) {
|
||||
setOpen(false);
|
||||
navigate("/events");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
variant={"danger"}
|
||||
className={styles.manageBtn}
|
||||
onClick={handleDelete}
|
||||
>
|
||||
Supprimer
|
||||
</Button>
|
||||
|
||||
<Modal open={open} onClose={() => setOpen(false)} title={"Suppression"}>
|
||||
<div className={styles.modalContent}>
|
||||
<p>
|
||||
Etes-vous sur de vouloir supprimer l'événement {eventInfo?.name} ?
|
||||
</p>
|
||||
|
||||
<Button variant={"primary"} onClick={delEvent}>
|
||||
Confirmer
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { useContext, useState } from "react";
|
||||
import { AuthContext } from "../../../../contexts/Auth/AuthContext";
|
||||
import { EventDetailContext } from "../../../../contexts/EventDetail/EventDetailContext";
|
||||
import Button from "../../../../components/ui/Button/Button";
|
||||
import Modal from "../../../../components/ui/Modal/Modal";
|
||||
import styles from "../../EventDetailPage.module.css";
|
||||
|
||||
type DeleteTaskBtnProps = {
|
||||
taskId: string | number;
|
||||
eventId: string | number;
|
||||
taskName: string;
|
||||
};
|
||||
|
||||
export default function DeleteTaskBtn({ taskId, eventId, taskName }: DeleteTaskBtnProps) {
|
||||
|
||||
const { user } = useContext(AuthContext) as any;
|
||||
const { deleteTask } = useContext(EventDetailContext) as any;
|
||||
|
||||
const [openModal, setOpenModal] = useState<boolean>(false);
|
||||
|
||||
if (!user?.isAdmin) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button variant={"danger"} onClick={() => setOpenModal(true)}>
|
||||
Supprimer
|
||||
</Button>
|
||||
|
||||
<Modal open={openModal} onClose={() => setOpenModal(false)} title={"Suppression"}>
|
||||
<div className={styles.modalContent}>
|
||||
<p className={styles.modalContent}>
|
||||
Etes-vous sur de vouloir supprimer la tâche {taskName} ?
|
||||
</p>
|
||||
|
||||
<Button
|
||||
variant={"danger"}
|
||||
onClick={() => {
|
||||
deleteTask(taskId, eventId);
|
||||
setOpenModal(false);
|
||||
}}
|
||||
>
|
||||
Confirmer
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import styles from "../../EventDetailPage.module.css";
|
||||
import Button from "../../../../components/ui/Button/Button";
|
||||
import Modal from "../../../../components/ui/Modal/Modal";
|
||||
import TextInput from "../../../../components/ui/Input/Input";
|
||||
import { useContext, useState, useEffect } from "react";
|
||||
import { EventDetailContext } from "../../../../contexts/EventDetail/EventDetailContext";
|
||||
import updateEvent from "../../../../utils/events/updateEvent";
|
||||
|
||||
type EditEventBtnProps = {
|
||||
id: string | number;
|
||||
};
|
||||
|
||||
export default function EditEventBtn({ id }: EditEventBtnProps) {
|
||||
const { eventInfo, updateEventInfo } = useContext(EventDetailContext) as any;
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
const [name, setName] = useState<string>("");
|
||||
const [description, setDescription] = useState<string>("");
|
||||
const [start, setStart] = useState<string>("");
|
||||
const [end, setEnd] = useState<string>("");
|
||||
|
||||
useEffect(() => {
|
||||
if (open && eventInfo) {
|
||||
setName(eventInfo.name ?? "");
|
||||
setDescription(eventInfo.description ?? "");
|
||||
setStart(eventInfo.start ? eventInfo.start.slice(0, 16) : "");
|
||||
setEnd(eventInfo.end ? eventInfo.end.slice(0, 16) : "");
|
||||
}
|
||||
}, [open, eventInfo]);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
const res = await updateEvent(Number(id), name, description, start, end);
|
||||
await updateEventInfo(id);
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
variant={"default"}
|
||||
className={styles.manageBtn}
|
||||
onClick={() => setOpen(true)}
|
||||
>
|
||||
Modifier
|
||||
</Button>
|
||||
|
||||
<Modal open={open} onClose={() => setOpen(false)} title={"Modifier l'événement"}>
|
||||
<div className={styles.modalContent}>
|
||||
<div>
|
||||
<p>Nom</p>
|
||||
<TextInput
|
||||
value={name}
|
||||
onChange={(e: any) => setName(e.target.value)}
|
||||
placeholder="Nom de l'événement"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p>Description</p>
|
||||
<TextInput
|
||||
value={description}
|
||||
onChange={(e: any) => setDescription(e.target.value)}
|
||||
placeholder="Description"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p>Début</p>
|
||||
<TextInput
|
||||
type="datetime-local"
|
||||
value={start}
|
||||
onChange={(e: any) => setStart(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p>Fin</p>
|
||||
<TextInput
|
||||
type="datetime-local"
|
||||
value={end}
|
||||
onChange={(e: any) => setEnd(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button variant="default" onClick={handleSubmit}>
|
||||
Confirmer
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import { AuthContext } from "../../../../contexts/Auth/AuthContext";
|
||||
import { EventDetailContext } from "../../../../contexts/EventDetail/EventDetailContext";
|
||||
import Button from "../../../../components/ui/Button/Button";
|
||||
import Modal from "../../../../components/ui/Modal/Modal";
|
||||
import TextInput from "../../../../components/ui/Input/Input";
|
||||
import styles from "../../EventDetailPage.module.css";
|
||||
import updateTask from "../../../../utils/tasks/updateTask";
|
||||
|
||||
type EditTaskBtnProps = {
|
||||
taskId: number;
|
||||
eventId: number | string;
|
||||
};
|
||||
|
||||
export default function EditTaskBtn({ taskId, eventId }: EditTaskBtnProps) {
|
||||
const { user } = useContext(AuthContext) as any;
|
||||
const { tasks, updateEventInfo } = useContext(EventDetailContext) as any;
|
||||
const task = tasks?.find((t: any) => t.id === taskId);
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
const [name, setName] = useState<string>("");
|
||||
const [description, setDescription] = useState<string>("");
|
||||
const [start, setStart] = useState<string>("");
|
||||
const [end, setEnd] = useState<string>("");
|
||||
const [location, setLocation] = useState<string>("");
|
||||
const [maxParticipants, setMaxParticipants] = useState<string>("");
|
||||
|
||||
useEffect(() => {
|
||||
if (open && task) {
|
||||
setName(task.name ?? "");
|
||||
setDescription(task.description ?? "");
|
||||
setStart(task.start ? task.start.slice(0, 16) : "");
|
||||
setEnd(task.end ? task.end.slice(0, 16) : "");
|
||||
setLocation(task.location ?? "");
|
||||
setMaxParticipants(task.max_participants?.toString() ?? "");
|
||||
}
|
||||
}, [open, task]);
|
||||
|
||||
if (!user?.isAdmin) return null;
|
||||
|
||||
const handleSubmit = async () => {
|
||||
await updateTask(taskId, {
|
||||
name,
|
||||
description,
|
||||
start,
|
||||
end,
|
||||
location,
|
||||
max_participants: Number(maxParticipants),
|
||||
});
|
||||
await updateEventInfo(eventId);
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button variant={"default"} onClick={() => setOpen(true)}>
|
||||
Modifier
|
||||
</Button>
|
||||
|
||||
<Modal open={open} onClose={() => setOpen(false)} title={"Modifier la tâche"}>
|
||||
<div className={styles.modalContent}>
|
||||
<div>
|
||||
<p>Nom</p>
|
||||
<TextInput
|
||||
value={name}
|
||||
onChange={(e: any) => setName(e.target.value)}
|
||||
placeholder="Nom de la tâche"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p>Description</p>
|
||||
<TextInput
|
||||
value={description}
|
||||
onChange={(e: any) => setDescription(e.target.value)}
|
||||
placeholder="Description"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p>Lieu</p>
|
||||
<TextInput
|
||||
value={location}
|
||||
onChange={(e: any) => setLocation(e.target.value)}
|
||||
placeholder="Lieu"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p>Participants max</p>
|
||||
<TextInput
|
||||
type="number"
|
||||
value={maxParticipants}
|
||||
onChange={(e: any) => setMaxParticipants(e.target.value)}
|
||||
placeholder="Participants max"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p>Début</p>
|
||||
<TextInput
|
||||
type="datetime-local"
|
||||
value={start}
|
||||
onChange={(e: any) => setStart(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p>Fin</p>
|
||||
<TextInput
|
||||
type="datetime-local"
|
||||
value={end}
|
||||
onChange={(e: any) => setEnd(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button variant="default" onClick={handleSubmit}>
|
||||
Confirmer
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
.task {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.taskDate {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.manageTaskBtn {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.content {
|
||||
margin: 16px 0 16px 0;
|
||||
animation: fadeInUp 0.6s ease-out forwards;
|
||||
animation-delay: calc(var(--i) * 0.1s);
|
||||
}
|
||||
|
||||
.participants {
|
||||
margin-top: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.participantsHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.participantsLabel {
|
||||
font-size: 0.8rem;
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
.participantsBadge {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
padding: 2px 10px;
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.badgeOpen {
|
||||
background: var(--glass-back);
|
||||
color: #00F0FF;
|
||||
}
|
||||
|
||||
.badgeAlmost {
|
||||
background: var(--glass-back);
|
||||
color: #019AFF;
|
||||
}
|
||||
|
||||
.badgeFull {
|
||||
background: var(--glass-back);
|
||||
color: #001C83;
|
||||
}
|
||||
|
||||
.progressBar {
|
||||
width: 100%;
|
||||
max-width: 200px;
|
||||
height: 5px;
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progressFill {
|
||||
height: 100%;
|
||||
border-radius: 999px;
|
||||
transition: width 0.4s ease;
|
||||
}
|
||||
|
||||
.fillOpen { background: #00F0FF; }
|
||||
.fillAlmost { background: #019AFF; }
|
||||
.fillFull { background: #001C83; }
|
||||
|
||||
.modalContent {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
|
||||
.task {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||