structure du projet + docker, back: mise en place BD et apps, front: début de dev pour le header et mise en place du thème et css global (override des variables bootstrap)
@@ -0,0 +1,24 @@
|
||||
# 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?
|
||||
@@ -0,0 +1,68 @@
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
|
||||
server_name _;
|
||||
|
||||
location /static/ {
|
||||
alias /var/www/html/static/;
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
access_log off;
|
||||
}
|
||||
|
||||
location /media/ {
|
||||
alias /var/www/html/media/;
|
||||
expires 30d;
|
||||
add_header Cache-Control "public";
|
||||
}
|
||||
|
||||
location /assets {
|
||||
root /usr/share/nginx/html;
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
}
|
||||
|
||||
location /api/ {
|
||||
proxy_pass http://backend:8000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection 'upgrade';
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_cache_bypass $http_upgrade;
|
||||
proxy_connect_timeout 60s;
|
||||
proxy_send_timeout 60s;
|
||||
proxy_read_timeout 60s;
|
||||
}
|
||||
|
||||
location / {
|
||||
root /usr/share/nginx/html;
|
||||
index index.html
|
||||
try_files $uri $uri/ /index.html;
|
||||
|
||||
# Cache busting pour index.html
|
||||
add_header Cache-Control "public, max-age=0, must-revalidate";
|
||||
}
|
||||
|
||||
location ~ /\. {
|
||||
deny all;
|
||||
access_log off;
|
||||
log_not_found off;
|
||||
}
|
||||
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header X-XSS-Protection "1; mode=block" always;
|
||||
add_header Referrer-Policy "no-referrer-when-downgrade" always;
|
||||
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
|
||||
|
||||
gzip on;
|
||||
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss;
|
||||
gzip_min_length 1000;
|
||||
|
||||
error_page 404 /index.html;
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
user nginx;
|
||||
worker_processes auto;
|
||||
error_log /var/log/nginx/error.log warn;
|
||||
pid /var/run/nginx.pid;
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
use epoll;
|
||||
}
|
||||
|
||||
http {
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||
'$status $body_bytes_sent "$http_referer" '
|
||||
'"$http_user_agent" "$http_x_forwarded_for"';
|
||||
|
||||
access_log /var/log/nginx/access.log main;
|
||||
|
||||
sendfile on;
|
||||
tcp_nopush on;
|
||||
tcp_nodelay on;
|
||||
keepalive_timeout 65;
|
||||
types_hash_max_size 2048;
|
||||
client_max_body_size 20M;
|
||||
|
||||
# Gzip compression
|
||||
gzip on;
|
||||
gzip_vary on;
|
||||
gzip_proxied any;
|
||||
gzip_comp_level 6;
|
||||
gzip_types text/plain text/css text/xml text/javascript
|
||||
application/json application/javascript application/xml+rss
|
||||
application/rss+xml font/truetype font/opentype
|
||||
application/vnd.ms-fontobject image/svg+xml;
|
||||
|
||||
include /etc/nginx/conf.d/*.conf;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
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.flat.recommended,
|
||||
reactRefresh.configs.vite,
|
||||
],
|
||||
languageOptions: {
|
||||
globals: globals.browser,
|
||||
parserOptions: { ecmaFeatures: { jsx: true } },
|
||||
},
|
||||
},
|
||||
])
|
||||
@@ -0,0 +1,17 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="theme-color" content="#5d6367">
|
||||
<title>Eiro</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"bootstrap": "^5.3.8",
|
||||
"react": "^19.2.6",
|
||||
"react-bootstrap": "^2.10.10",
|
||||
"react-dom": "^19.2.6",
|
||||
"react-icons": "^5.6.0",
|
||||
"sass": "^1.100.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^6.0.1",
|
||||
"eslint": "^10.3.0",
|
||||
"eslint-plugin-react-hooks": "^7.1.1",
|
||||
"eslint-plugin-react-refresh": "^0.5.2",
|
||||
"globals": "^17.6.0",
|
||||
"vite": "^8.0.12"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 74 KiB |
@@ -0,0 +1,12 @@
|
||||
import { useState } from 'react'
|
||||
import Header from './components/Header/Header'
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
@@ -0,0 +1,182 @@
|
||||
:root {
|
||||
/* ============================
|
||||
Base
|
||||
============================ */
|
||||
|
||||
--bs-body-bg: #FEF6F4;
|
||||
--bs-body-color: #34344A;
|
||||
|
||||
--bs-light: #FEF6F4;
|
||||
--bs-light-rgb: 254, 246, 244;
|
||||
|
||||
--bs-dark: #34344A;
|
||||
--bs-dark-rgb: 52, 52, 74;
|
||||
|
||||
|
||||
/* ============================
|
||||
Semantic colors
|
||||
============================ */
|
||||
|
||||
--bs-primary: #845A6D;
|
||||
--bs-primary-rgb: 132, 90, 109;
|
||||
|
||||
--bs-secondary: #C89B7B;
|
||||
--bs-secondary-rgb: 200, 155, 123;
|
||||
|
||||
--bs-success: #32746D;
|
||||
--bs-success-rgb: 50, 116, 109;
|
||||
|
||||
--bs-info: #9A7284;
|
||||
--bs-info-rgb: 154, 114, 132;
|
||||
|
||||
--bs-warning: #C89B7B;
|
||||
--bs-warning-rgb: 200, 155, 123;
|
||||
|
||||
--bs-danger: #D47386;
|
||||
--bs-danger-rgb: 212, 115, 134;
|
||||
|
||||
|
||||
/* ============================
|
||||
Links
|
||||
============================ */
|
||||
|
||||
--bs-link-color: var(--bs-primary);
|
||||
--bs-link-color-rgb: 132, 90, 109;
|
||||
|
||||
/* Hover = primary + dark */
|
||||
--bs-link-hover-color: color-mix(in srgb, var(--bs-primary) 80%, var(--bs-dark));
|
||||
--bs-link-hover-color-rgb: 118, 86, 102;
|
||||
|
||||
|
||||
/* ============================
|
||||
Subtle UI backgrounds
|
||||
============================ */
|
||||
|
||||
--bs-border-color: color-mix(in srgb, var(--bs-dark) 15%, var(--bs-light));
|
||||
--bs-border-color-rgb: 223, 214, 214;
|
||||
|
||||
--bs-primary-bg-subtle: color-mix(in srgb, var(--bs-primary) 12%, var(--bs-light));
|
||||
--bs-primary-bg-subtle-rgb: 243, 229, 233;
|
||||
|
||||
--bs-success-bg-subtle: color-mix(in srgb, var(--bs-success) 12%, var(--bs-light));
|
||||
--bs-success-bg-subtle-rgb: 229, 238, 237;
|
||||
|
||||
--bs-danger-bg-subtle: color-mix(in srgb, var(--bs-danger) 12%, var(--bs-light));
|
||||
--bs-danger-bg-subtle-rgb: 245, 224, 229;
|
||||
|
||||
--bs-secondary-bg: color-mix(in srgb, var(--bs-secondary) 10%, var(--bs-light));
|
||||
--bs-secondary-bg-rgb: 246, 236, 230;
|
||||
|
||||
|
||||
/* ============================
|
||||
Focus ring
|
||||
============================ */
|
||||
|
||||
--bs-focus-ring-color: rgba(132, 90, 109, 0.25);
|
||||
|
||||
|
||||
/* ============================
|
||||
Typography polish
|
||||
============================ */
|
||||
|
||||
--bs-heading-color: var(--bs-dark);
|
||||
|
||||
--bs-secondary-color: color-mix(in srgb, var(--bs-dark) 65%, var(--bs-light));
|
||||
--bs-secondary-color-rgb: 120, 120, 135;
|
||||
}
|
||||
|
||||
|
||||
/* =========================================
|
||||
DARK MODE
|
||||
Trigger: <html data-bs-theme="dark">
|
||||
========================================= */
|
||||
|
||||
[data-bs-theme="dark"] {
|
||||
|
||||
/* ============================
|
||||
Base background + text
|
||||
============================ */
|
||||
|
||||
--bs-body-bg: #1B1B24;
|
||||
--bs-body-color: #FEF6F4;
|
||||
|
||||
--bs-light: #2A2A36;
|
||||
--bs-light-rgb: 42, 42, 54;
|
||||
|
||||
--bs-dark: #FEF6F4;
|
||||
--bs-dark-rgb: 254, 246, 244;
|
||||
|
||||
|
||||
/* ============================
|
||||
Semantic colors
|
||||
============================ */
|
||||
|
||||
--bs-primary: #B08A9C;
|
||||
--bs-primary-rgb: 176, 138, 156;
|
||||
|
||||
--bs-secondary: #C89B7B;
|
||||
--bs-secondary-rgb: 200, 155, 123;
|
||||
|
||||
--bs-success: #4AA89E;
|
||||
--bs-success-rgb: 74, 168, 158;
|
||||
|
||||
--bs-info: #9A7284;
|
||||
--bs-info-rgb: 154, 114, 132;
|
||||
|
||||
--bs-warning: #E2B48E;
|
||||
--bs-warning-rgb: 226, 180, 142;
|
||||
|
||||
--bs-danger: #E08A9C;
|
||||
--bs-danger-rgb: 224, 138, 156;
|
||||
|
||||
|
||||
/* ============================
|
||||
Links
|
||||
============================ */
|
||||
|
||||
--bs-link-color: rgb(var(--bs-primary-rgb));
|
||||
--bs-link-hover-color: color-mix(in srgb, var(--bs-primary) 85%, white);
|
||||
|
||||
|
||||
/* ============================
|
||||
Borders + surfaces
|
||||
============================ */
|
||||
|
||||
--bs-border-color: rgba(255, 255, 255, 0.10);
|
||||
|
||||
--bs-secondary-bg: rgba(var(--bs-primary-rgb), 0.08);
|
||||
--bs-tertiary-bg: rgba(255, 255, 255, 0.04);
|
||||
|
||||
--bs-primary-bg-subtle: rgba(var(--bs-primary-rgb), 0.12);
|
||||
--bs-success-bg-subtle: rgba(var(--bs-success-rgb), 0.12);
|
||||
--bs-danger-bg-subtle: rgba(var(--bs-danger-rgb), 0.12);
|
||||
|
||||
|
||||
/* ============================
|
||||
Focus ring
|
||||
============================ */
|
||||
|
||||
--bs-focus-ring-color: rgba(var(--bs-primary-rgb), 0.35);
|
||||
|
||||
|
||||
/* ============================
|
||||
Typography polish
|
||||
============================ */
|
||||
|
||||
--bs-heading-color: #FEF6F4;
|
||||
--bs-secondary-color: rgba(254, 246, 244, 0.70);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* =========================================
|
||||
OTHER VARIABLES
|
||||
========================================= */
|
||||
|
||||
:root {
|
||||
--bs-border-radius: 0.6rem;
|
||||
|
||||
.toast-container>:not(:last-child) {
|
||||
--bs-toast-spacing: 0.6rem;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,354 @@
|
||||
@font-face {
|
||||
font-family: "Salin";
|
||||
src: url("../fonts/salin.otf") format("opentype");
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5 {
|
||||
color: var(--bs-heading-color);
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration-thickness: 1px;
|
||||
text-underline-offset: 0.15em;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration-thickness: 2px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--bs-link-color);
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: var(--bs-link-hover-color);
|
||||
}
|
||||
|
||||
/* ---------------------------------------
|
||||
Navbar
|
||||
---------------------------------------- */
|
||||
|
||||
.navbar {
|
||||
border-bottom: 1px solid rgba(var(--bs-border-color-rgb), 1);
|
||||
background: color-mix(in srgb, var(--bs-light) 92%, var(--bs-primary));
|
||||
}
|
||||
|
||||
.navbar .navbar-brand {
|
||||
letter-spacing: 0.03em;
|
||||
color: var(--bs-dark);
|
||||
font-family: "Salin";
|
||||
}
|
||||
|
||||
.logo-text {
|
||||
letter-spacing: 0.03em;
|
||||
color: var(--bs-dark);
|
||||
font-family: "Salin";
|
||||
}
|
||||
|
||||
.logo-img {
|
||||
max-height: 10vh;
|
||||
}
|
||||
|
||||
.navbar .nav-link {
|
||||
color: rgba(var(--bs-dark-rgb), 0.78);
|
||||
border-radius: 999px;
|
||||
padding: 0.4rem 0.75rem;
|
||||
}
|
||||
|
||||
.navbar .nav-link:hover,
|
||||
.navbar .nav-link:focus {
|
||||
color: var(--bs-dark);
|
||||
background: rgba(var(--bs-primary-rgb), 0.08);
|
||||
}
|
||||
|
||||
.navbar .nav-link.active {
|
||||
color: var(--bs-dark);
|
||||
background: rgba(var(--bs-primary-rgb), 0.14);
|
||||
}
|
||||
|
||||
/* ---------------------------------------
|
||||
Buttons
|
||||
---------------------------------------- */
|
||||
|
||||
.btn {
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.005em;
|
||||
box-shadow: 0 1px 0 rgba(var(--bs-dark-rgb), 0.04);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
border-color: rgba(var(--bs-primary-rgb), 0.65);
|
||||
--bs-btn-bg: var(--bs-primary);
|
||||
--bs-btn-border-color: var(--bs-primary);
|
||||
|
||||
--bs-btn-hover-bg: color-mix(in srgb, var(--bs-primary) 85%, black);
|
||||
--bs-btn-hover-border-color: color-mix(in srgb, var(--bs-primary) 85%, black);
|
||||
|
||||
--bs-btn-active-bg: color-mix(in srgb, var(--bs-primary) 75%, black);
|
||||
--bs-btn-active-border-color: color-mix(in srgb, var(--bs-primary) 75%, black);
|
||||
|
||||
--bs-btn-focus-shadow-rgb: var(--bs-primary-rgb);
|
||||
color: rgb(var(--bs-light-rgb)) !important;
|
||||
}
|
||||
|
||||
.btn-primary:hover,
|
||||
.btn-primary:focus {
|
||||
filter: brightness(0.98);
|
||||
box-shadow:
|
||||
0 0 0 0.2rem rgba(var(--bs-primary-rgb), 0.18),
|
||||
0 6px 16px rgba(var(--bs-dark-rgb), 0.08);
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
--bs-btn-bg: var(--bs-success);
|
||||
--bs-btn-border-color: var(--bs-success);
|
||||
--bs-btn-focus-shadow-rgb: var(--bs-success-rgb);
|
||||
}
|
||||
|
||||
.btn-success:hover,
|
||||
.btn-success:focus {
|
||||
box-shadow:
|
||||
0 0 0 0.2rem rgba(var(--bs-success-rgb), 0.18),
|
||||
0 6px 16px rgba(var(--bs-dark-rgb), 0.08);
|
||||
}
|
||||
|
||||
.btn-outline-primary {
|
||||
--bs-btn-color: var(--bs-primary);
|
||||
--bs-btn-border-color: rgba(var(--bs-primary-rgb), .45);
|
||||
--bs-btn-hover-bg: rgba(var(--bs-primary-rgb), .10);
|
||||
--bs-btn-hover-border-color: rgba(var(--bs-primary-rgb), .45);
|
||||
--bs-btn-active-bg: rgba(var(--bs-primary-rgb), .16);
|
||||
--bs-btn-active-border-color: rgba(var(--bs-primary-rgb), .45);
|
||||
--bs-btn-focus-shadow-rgb: var(--bs-primary-rgb);
|
||||
}
|
||||
|
||||
.btn-outline-primary:hover,
|
||||
.btn-outline-primary:focus {
|
||||
background: rgba(var(--bs-primary-rgb), 0.10);
|
||||
color: rgb(var(--bs-primary-rgb));
|
||||
box-shadow:
|
||||
0 0 0 0.2rem rgba(var(--bs-primary-rgb), 0.12);
|
||||
}
|
||||
|
||||
/* Icon buttons */
|
||||
.btn.btn-icon {
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
padding: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 999px;
|
||||
box-shadow: 0 0 transparent;
|
||||
}
|
||||
|
||||
.btn.btn-icon:hover,
|
||||
.btn.btn-icon:focus {
|
||||
color: var(--bs-dark);
|
||||
background: rgba(var(--bs-primary-rgb), 0.08);
|
||||
}
|
||||
|
||||
|
||||
/* ---------------------------------------
|
||||
Dropdowns
|
||||
---------------------------------------- */
|
||||
|
||||
.dropdown-menu .dropdown-item:hover {
|
||||
background: rgba(var(--bs-primary-rgb), .08);
|
||||
}
|
||||
|
||||
/* ---------------------------------------
|
||||
Cards / containers
|
||||
---------------------------------------- */
|
||||
|
||||
.card {
|
||||
border-radius: 1.25rem;
|
||||
border: 1px solid rgba(var(--bs-border-color-rgb), 1);
|
||||
background: rgba(var(--bs-light-rgb), 0.85);
|
||||
box-shadow: 0 2px 10px rgba(var(--bs-dark-rgb), 0.04);
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
border-bottom: 1px solid rgba(var(--bs-border-color-rgb), 1);
|
||||
background: rgba(var(--bs-light-rgb), 0.94);
|
||||
}
|
||||
|
||||
.card-body {
|
||||
--bs-card-spacer-y: 0.5rem;
|
||||
--bs-card-spacer-x: 0.5rem;
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
/* border-top: 1px solid rgba(var(--bs-border-color-rgb), 1); */
|
||||
border-top: none;
|
||||
background: rgba(var(--bs-light-rgb), 0.94);
|
||||
}
|
||||
|
||||
/* ---------------------------------------
|
||||
Badges
|
||||
---------------------------------------- */
|
||||
|
||||
.badge {
|
||||
border-radius: 999px;
|
||||
font-weight: 600;
|
||||
padding: 0.4em 0.7em;
|
||||
}
|
||||
|
||||
.badge.text-bg-primary {
|
||||
background: var(--bs-primary) !important;
|
||||
color: rgb(var(--bs-light-rgb)) !important;
|
||||
border: 1px solid rgba(var(--bs-primary-rgb), 0.20);
|
||||
}
|
||||
|
||||
.badge.text-bg-success {
|
||||
background: rgba(var(--bs-success-rgb), 0.14) !important;
|
||||
color: rgb(var(--bs-success-rgb)) !important;
|
||||
border: 1px solid rgba(var(--bs-success-rgb), 0.20);
|
||||
}
|
||||
|
||||
.badge.text-bg-danger {
|
||||
background: rgba(var(--bs-danger-rgb), 0.14) !important;
|
||||
color: rgb(var(--bs-danger-rgb)) !important;
|
||||
border: 1px solid rgba(var(--bs-danger-rgb), 0.20);
|
||||
}
|
||||
|
||||
.badge.text-bg-warning {
|
||||
background: rgba(var(--bs-warning-rgb), 0.18) !important;
|
||||
color: rgb(var(--bs-dark-rgb)) !important;
|
||||
border: 1px solid rgba(var(--bs-warning-rgb), 0.22);
|
||||
}
|
||||
|
||||
/* ---------------------------------------
|
||||
Forms
|
||||
---------------------------------------- */
|
||||
|
||||
.form-control,
|
||||
.form-select {
|
||||
border-color: rgba(var(--bs-border-color-rgb), 1);
|
||||
background: rgba(var(--bs-light-rgb), 0.95);
|
||||
}
|
||||
|
||||
.form-control:focus,
|
||||
.form-select:focus {
|
||||
border-color: rgba(var(--bs-primary-rgb), 0.45);
|
||||
box-shadow: 0 0 0 0.2rem rgba(var(--bs-primary-rgb), 0.18);
|
||||
}
|
||||
|
||||
/* Checkboxes/switches */
|
||||
.form-check-input:focus {
|
||||
box-shadow: 0 0 0 0.2rem rgba(var(--bs-primary-rgb), 0.18);
|
||||
}
|
||||
|
||||
.form-check-input:checked {
|
||||
background-color: rgb(var(--bs-primary-rgb));
|
||||
border-color: rgb(var(--bs-primary-rgb));
|
||||
}
|
||||
|
||||
/* ---------------------------------------
|
||||
Alerts (soft)
|
||||
---------------------------------------- */
|
||||
|
||||
.alert {
|
||||
border-radius: 1rem;
|
||||
border: 1px solid rgba(var(--bs-border-color-rgb), 1);
|
||||
background: rgba(var(--bs-light-rgb), 0.90);
|
||||
}
|
||||
|
||||
.alert-primary {
|
||||
--bs-toast-max-width: 33vw;
|
||||
border-color: rgba(var(--bs-primary-rgb), 0.25);
|
||||
background: rgba(var(--bs-primary-rgb), 0.20);
|
||||
color: rgb(var(--bs-dark-rgb));
|
||||
}
|
||||
|
||||
.alert-success {
|
||||
--bs-toast-max-width: 33vw;
|
||||
border-color: rgba(var(--bs-success-rgb), 0.25);
|
||||
background: rgba(var(--bs-success-rgb), 0.20);
|
||||
color: rgb(var(--bs-dark-rgb));
|
||||
}
|
||||
|
||||
.alert-danger {
|
||||
--bs-toast-max-width: 33vw;
|
||||
border-color: rgba(var(--bs-danger-rgb), 0.25);
|
||||
background: rgba(var(--bs-danger-rgb), 0.30);
|
||||
color: rgb(var(--bs-dark-rgb));
|
||||
}
|
||||
|
||||
.alert-warning {
|
||||
--bs-toast-max-width: 33vw;
|
||||
border-color: rgba(var(--bs-warning-rgb), 0.28);
|
||||
background: rgba(var(--bs-warning-rgb), 0.2);
|
||||
color: rgb(var(--bs-dark-rgb));
|
||||
}
|
||||
|
||||
.alert-icon {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
/* ---------------------------------------
|
||||
Tables (soft rows)
|
||||
---------------------------------------- */
|
||||
|
||||
.table {
|
||||
--bs-table-bg: transparent;
|
||||
}
|
||||
|
||||
.table thead th {
|
||||
color: rgba(var(--bs-dark-rgb), 0.75);
|
||||
font-weight: 700;
|
||||
border-bottom: 1px solid rgba(var(--bs-border-color-rgb), 1);
|
||||
}
|
||||
|
||||
.table tbody tr {
|
||||
border-bottom: 1px solid rgba(var(--bs-border-color-rgb), 1);
|
||||
}
|
||||
|
||||
.table-hover tbody tr:hover {
|
||||
background: rgba(var(--bs-primary-rgb), 0.06);
|
||||
}
|
||||
|
||||
/* ---------------------------------------
|
||||
Toasts / Modals
|
||||
---------------------------------------- */
|
||||
|
||||
.modal-content {
|
||||
border-radius: 1.25rem;
|
||||
border: 1px solid rgba(var(--bs-border-color-rgb), 1);
|
||||
box-shadow: 0 18px 50px rgba(var(--bs-dark-rgb), 0.18);
|
||||
}
|
||||
|
||||
/* ---------------------------------------
|
||||
Utility: “pill” chips (nice for filters)
|
||||
---------------------------------------- */
|
||||
|
||||
.chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
padding: 0.35rem 0.75rem;
|
||||
border-radius: 999px;
|
||||
border: 1px solid rgba(var(--bs-border-color-rgb), 1);
|
||||
background: rgba(var(--bs-light-rgb), 0.85);
|
||||
color: rgba(var(--bs-dark-rgb), 0.8);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.chip:hover {
|
||||
background: rgba(var(--bs-primary-rgb), 0.08);
|
||||
color: var(--bs-dark);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
<?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 16 16" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="m 8 1 c -1.65625 0 -3 1.34375 -3 3 s 1.34375 3 3 3 s 3 -1.34375 3 -3 s -1.34375 -3 -3 -3 z m -1.5 7 c -2.492188 0 -4.5 2.007812 -4.5 4.5 v 0.5 c 0 1.109375 0.890625 2 2 2 h 8 c 1.109375 0 2 -0.890625 2 -2 v -0.5 c 0 -2.492188 -2.007812 -4.5 -4.5 -4.5 z m 0 0" fill="#2e3436"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 509 B |
|
After Width: | Height: | Size: 74 KiB |
|
After Width: | Height: | Size: 65 KiB |
|
After Width: | Height: | Size: 55 KiB |
|
After Width: | Height: | Size: 97 KiB |
|
After Width: | Height: | Size: 95 KiB |
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 170 KiB |
@@ -0,0 +1,3 @@
|
||||
.pointer-icon {
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import Container from 'react-bootstrap/Container';
|
||||
import Nav from 'react-bootstrap/Nav';
|
||||
import Navbar from 'react-bootstrap/Navbar';
|
||||
import NavDropdown from 'react-bootstrap/NavDropdown';
|
||||
import Button from "react-bootstrap/Button"
|
||||
|
||||
import { useThemeContext } from '../../context/ThemeContext'
|
||||
import { useTheme } from '../../hooks/useTheme'
|
||||
|
||||
import './Header.css'
|
||||
import appIcon from "../../assets/images/icon/icon-removebg.png"
|
||||
import { BsFillSunFill, BsFillMoonFill } from 'react-icons/bs'
|
||||
|
||||
export default function Header(props) {
|
||||
const { theme, toggleTheme } = useTheme()
|
||||
|
||||
return (
|
||||
<Navbar expand="lg" className="align-middle px-3 py-2">
|
||||
<Container fluid className="g-3">
|
||||
<Navbar.Brand href="#home">
|
||||
<img
|
||||
alt="Icon"
|
||||
src={appIcon}
|
||||
width="30"
|
||||
height="30"
|
||||
className="d-inline-block"
|
||||
/>{' '}
|
||||
{import.meta.env.VITE_APP_NAME}
|
||||
</Navbar.Brand>
|
||||
</Container>
|
||||
|
||||
{theme === 'dark' ?
|
||||
<BsFillSunFill className="pointer-icon" onClick={toggleTheme} /> :
|
||||
<BsFillMoonFill className="pointer-icon" onClick={toggleTheme} />
|
||||
}
|
||||
</Navbar>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { createContext, useContext } from 'react'
|
||||
import { useTheme } from '../hooks/useTheme'
|
||||
|
||||
const ThemeContext = createContext(null)
|
||||
|
||||
export function ThemeProvider({ children }) {
|
||||
const theme = useTheme()
|
||||
return (
|
||||
<ThemeContext.Provider value={theme}>
|
||||
{children}
|
||||
</ThemeContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export function useThemeContext() {
|
||||
const ctx = useContext(ThemeContext)
|
||||
if (!ctx) throw new Error('useThemeContext must be used within ThemeProvider')
|
||||
return ctx
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
|
||||
export function useTheme() {
|
||||
const [theme, setTheme] = useState(() => {
|
||||
const saved = localStorage.getItem('theme')
|
||||
if (saved) return saved
|
||||
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
document.documentElement.setAttribute('data-bs-theme', theme)
|
||||
localStorage.setItem('theme', theme)
|
||||
}, [theme])
|
||||
|
||||
const toggleTheme = () => setTheme(t => t === 'dark' ? 'light' : 'dark')
|
||||
|
||||
return { theme, setTheme, toggleTheme }
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import App from './App.jsx'
|
||||
|
||||
// import 'bootstrap/dist/css/bootstrap.css'
|
||||
import './assets/css/bootstrap.min.css'
|
||||
import './assets/css/bootstrap-override.css'
|
||||
import './assets/css/theme.css'
|
||||
import { ThemeProvider } from './context/ThemeContext.jsx'
|
||||
|
||||
createRoot(document.getElementById('root')).render(
|
||||
<StrictMode>
|
||||
<ThemeProvider>
|
||||
<App />
|
||||
</ThemeProvider>
|
||||
</StrictMode>,
|
||||
)
|
||||
@@ -0,0 +1,12 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
envDir: "../",
|
||||
server: {
|
||||
port: 3000,
|
||||
'/api': "http://localhost:8000/api/v1"
|
||||
}
|
||||
})
|
||||