diff --git a/.DS_Store b/.DS_Store
index e47155a..9da6b58 100644
Binary files a/.DS_Store and b/.DS_Store differ
diff --git a/.gitignore b/.gitignore
index 485dee6..6143b15 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,44 @@
-.idea
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+/.pnp
+.pnp.*
+.yarn/*
+!.yarn/patches
+!.yarn/plugins
+!.yarn/releases
+!.yarn/versions
+
+# testing
+/coverage
+
+# next.js
+/.next/
+/out/
+
+# production
+/build
+
+# misc
+.DS_Store
+*.pem
+
+# debug
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+.pnpm-debug.log*
+
+# env files (can opt-in for committing if needed)
+.env*
+
+# vercel
+.vercel
+
+# typescript
+*.tsbuildinfo
+next-env.d.ts
+
+# python
+/backend/.venv
diff --git a/Static/.DS_Store b/Static/.DS_Store
deleted file mode 100644
index 998e1a1..0000000
Binary files a/Static/.DS_Store and /dev/null differ
diff --git a/Static/css/admin.css b/Static/css/admin.css
deleted file mode 100644
index 32c9cff..0000000
--- a/Static/css/admin.css
+++ /dev/null
@@ -1,1130 +0,0 @@
-:root {
- --primary-color: #3a86ff;
- --secondary-color: #8338ec;
- --accent-color: #ff006e;
- --dark-color: #1a1a2e;
- --light-color: #f8f9fa;
- --gray-color: #6c757d;
- --success-color: #06d6a0;
- --header-color: #09092d;
- --header-color-variant: #6576a3;
-
- --gradient-header: linear-gradient(135deg, #4158D0, #C850C0, #FFCC70);
- --font-main: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
- --font-heading: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
- --transition-speed: 0.3s;
- --border-radius: 8px;
-}
-
-/* ===== RESET & BASE STYLES ===== */
-* {
- margin: 0;
- padding: 0;
- box-sizing: border-box;
-}
-
-body {
- font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
- min-height: 100vh;
- color: #333;
- line-height: 1.6;
-}
-
-/* ===== LOGIN PAGE STYLES (PRESERVED) ===== */
-body.login-page {
- display: flex;
- justify-content: center;
- align-items: center;
- min-height: 100vh;
- background-image: url("../img/bg-login.jpg");
- background-size: cover;
- background-position: center;
- margin: 0;
-}
-
-section.login {
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- width: 300px;
- padding: 20px;
- background-color: rgba(255, 255, 255, 0.4);
- backdrop-filter: blur(10px);
- border: 1px solid lightslategrey;
- border-radius: 15px;
- margin: auto;
-}
-
-section.login form {
- display: flex;
- flex-direction: column;
- align-items: center;
- width: 100%;
- gap: 1rem;
-}
-
-section.login input[type="text"],
-section.login input[type="password"] {
- border: 1px solid lightslategrey;
- border-radius: 5px;
- width: 100%;
- padding: 0.75rem;
- font-family: inherit;
- font-size: 1rem;
- resize: vertical;
- text-align: center;
-}
-
-section.login .btn {
- width: 100%;
- padding: 0.75rem;
- font-family: inherit;
- font-size: 1rem;
- resize: vertical;
- cursor: pointer;
- background-color: #06d6a0;
- border: 1px solid #10b981;
- color: #185742;
- border-radius: 5px;
- transition: background-color 0.3s ease;
-}
-
-section.login .btn:hover {
- background-color: #10b981;
-}
-
-/* ===== MODERN ADMIN LAYOUT ===== */
-.admin-container {
- display: flex;
- min-height: 100vh;
-}
-
-/* ===== NAVIGATION SIDEBAR ===== */
-nav {
- width: 280px;
- background: linear-gradient(180deg, #1e293b 0%, #334155 100%);
- box-shadow: 4px 0 20px rgba(0, 0, 0, 0.1);
- position: fixed;
- height: 100vh;
- left: 0;
- top: 0;
- z-index: 1000;
- transition: transform 0.3s ease;
-}
-
-nav ul {
- list-style: none;
- padding: 0;
- margin: 0;
- display: flex;
- flex-direction: column;
- height: 100%;
-}
-
-nav ul li.title {
- background: linear-gradient(135deg, #3b82f6, #8b5cf6);
- color: white;
- padding: 1.5rem;
- font-size: 1.4rem;
- font-weight: 700;
- text-align: center;
- border-bottom: 2px solid rgba(255, 255, 255, 0.1);
- margin-bottom: 1rem;
-}
-
-nav ul li.nav-item {
- margin: 0.25rem 0.75rem;
- border-radius: 12px;
- transition: all 0.3s ease;
-}
-
-nav ul li.nav-item:hover {
- background: rgba(255, 255, 255, 0.1);
- transform: translateX(5px);
-}
-
-nav ul li.nav-item a {
- color: #e2e8f0;
- text-decoration: none;
- padding: 1rem 1.25rem;
- display: block;
- border-radius: 12px;
- font-weight: 500;
- transition: all 0.3s ease;
- position: relative;
- overflow: hidden;
-}
-
-nav ul li.nav-item a:hover {
- color: #60a5fa;
- background: rgba(96, 165, 250, 0.1);
-}
-
-nav ul li.nav-item a::before {
- content: '';
- position: absolute;
- left: 0;
- top: 0;
- height: 100%;
- width: 3px;
- background: #60a5fa;
- transform: scaleY(0);
- transition: transform 0.3s ease;
-}
-
-nav ul li.nav-item a:hover::before {
- transform: scaleY(1);
-}
-
-/* ===== MAIN CONTENT AREA ===== */
-section:not(.login) {
- margin-left: 280px;
- padding: 2rem;
- flex: 1;
- background: rgba(255, 255, 255, 0.05);
- backdrop-filter: blur(10px);
- border-radius: 20px 0 0 0;
- margin-top: 0;
-}
-
-/* ===== DASHBOARD STYLES ===== */
-.dashboard {
- max-width: 1400px;
- margin: 0 auto;
-}
-
-.dashboard h1 {
- font-size: 3rem;
- font-weight: 800;
- background: #c8e7ff;
- -webkit-background-clip: text;
- -webkit-text-fill-color: transparent;
- background-clip: text;
- margin-bottom: 0.5rem;
- text-align: center;
-}
-
-.welcome-message {
- font-size: 1.2rem;
- color: rgba(255, 255, 255, 0.8);
- text-align: center;
- margin-bottom: 3rem;
- font-weight: 300;
-}
-
-/* ===== DASHBOARD GRID ===== */
-.dashboard-grid {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
- gap: 2rem;
- margin-bottom: 3rem;
-}
-
-.dashboard-card {
- background: rgba(255, 255, 255, 0.95);
- backdrop-filter: blur(20px);
- border-radius: 20px;
- padding: 2rem;
- border: 1px solid rgba(255, 255, 255, 0.2);
- box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
- transition: transform 0.3s ease, box-shadow 0.3s ease;
- position: relative;
- overflow: hidden;
- will-change: transform;
-}
-
-.dashboard-card::before {
- content: '';
- position: absolute;
- top: 0;
- left: 0;
- right: 0;
- height: 4px;
- background: linear-gradient(90deg, #667eea, #764ba2);
- transform: scaleX(0);
- transition: transform 0.3s ease;
- transform-origin: left;
-}
-
-.dashboard-card:hover::before {
- transform: scaleX(1);
-}
-
-.dashboard-card:hover {
- transform: translateY(-8px);
- box-shadow: 0 25px 50px rgba(0, 0, 0, 0.15);
-}
-
-.dashboard-card h3 {
- font-size: 1.5rem;
- margin-bottom: 1rem;
- color: #1e293b;
- font-weight: 700;
- display: flex;
- align-items: center;
- gap: 0.5rem;
-}
-
-.dashboard-card p {
- color: #64748b;
- margin-bottom: 1.5rem;
- line-height: 1.7;
- font-size: 1rem;
-}
-
-/* ===== MODERN BUTTONS ===== */
-.btn-primary, .btn-success, .btn-secondary, .btn-warning, .btn-danger {
- padding: 1rem 2rem;
- border-radius: 5px;
- text-decoration: none;
- font-weight: 600;
- text-align: center;
- display: inline-block;
- transition: transform 0.2s ease, box-shadow 0.2s ease;
- cursor: pointer;
- border: none;
- font-size: 1rem;
- position: relative;
- overflow: hidden;
- text-transform: uppercase;
- letter-spacing: 0.5px;
- will-change: transform;
-}
-
-.btn-primary {
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
- color: white;
- box-shadow: 0 10px 30px rgba(102, 126, 234, 0.4);
-}
-
-.btn-primary:hover {
- transform: translateY(-3px);
- box-shadow: 0 15px 40px rgba(102, 126, 234, 0.6);
-}
-
-.btn-secondary {
- background: linear-gradient(135deg, #6b7280 0%, #4b5563 100%);
- color: white;
- box-shadow: 0 10px 30px rgba(107, 114, 128, 0.4);
-}
-
-.btn-secondary:hover {
- transform: translateY(-3px);
- box-shadow: 0 15px 40px rgba(107, 114, 128, 0.6);
-}
-
-.btn-success {
- background: linear-gradient(135deg, #85f887 0%, #1daf4c 100%);
- color: white;
- box-shadow: 0 10px 30px rgba(126, 234, 102, 0.4);
-}
-
-.btn-success:hover {
- transform: translateY(-3px);
- box-shadow: 0 15px 40px rgba(126, 234, 102, 0.4);
-}
-
-.btn-warning {
- background: linear-gradient(135deg, #fbbf24 0%, #f59e0b 100%);
- color: white;
- box-shadow: 0 10px 30px rgba(251, 191, 36, 0.4);
-}
-
-.btn-warning:hover {
- transform: translateY(-3px);
- box-shadow: 0 15px 40px rgba(251, 191, 36, 0.6);
-}
-
-.btn-danger {
- background: linear-gradient(135deg, #f43f5e 0%, #e11d48 100%);
- color: white;
- box-shadow: 0 10px 30px rgba(244, 63, 94, 0.4);
-}
-
-.btn-danger:hover {
- transform: translateY(-3px);
- box-shadow: 0 15px 40px rgba(244, 63, 94, 0.6);
-}
-
-.btn-small {
- padding: 0.5rem 1rem;
- font-size: 0.9rem;
-}
-
-.btn-small:hover {
- transform: translateY(-2px);
- box-shadow: 0 10px 25px rgba(102, 126, 234, 0.3);
-}
-
-
-
-/* ===== STATISTICS SECTION ===== */
-.stats {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
- gap: 1.5rem;
-}
-
-.stat-item {
- background: rgba(102, 126, 234, 0.1);
- border-radius: 16px;
- padding: 1.5rem;
- text-align: center;
- border: 2px solid rgba(102, 126, 234, 0.2);
- transition: background-color 0.2s ease, border-color 0.2s ease, transform 0.2s ease;
- will-change: transform;
-}
-
-.stat-item:hover {
- background: rgba(102, 126, 234, 0.2);
- border-color: rgba(102, 126, 234, 0.4);
- transform: scale(1.03);
-}
-
-.stat-number {
- font-size: 2.5rem;
- font-weight: 900;
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
- -webkit-background-clip: text;
- -webkit-text-fill-color: transparent;
- background-clip: text;
- display: block;
- margin-bottom: 0.5rem;
-}
-
-.stat-label {
- font-size: 0.9rem;
- color: #64748b;
- font-weight: 600;
- text-transform: uppercase;
- letter-spacing: 0.5px;
-}
-
-/* ===== QUICK ACTIONS ===== */
-.quick-actions {
- display: flex;
- flex-direction: column;
- gap: 1rem;
-}
-
-/* ===== RECENT ACTIVITY ===== */
-.recent-activity {
- background: rgba(255, 255, 255, 0.95);
- backdrop-filter: blur(20px);
- border-radius: 20px;
- padding: 2rem;
- border: 1px solid rgba(255, 255, 255, 0.2);
- box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
-}
-
-.recent-activity h3 {
- font-size: 1.5rem;
- margin-bottom: 1.5rem;
- color: #1e293b;
- font-weight: 700;
- text-align: center;
-}
-
-.activity-list {
- display: flex;
- flex-direction: column;
- gap: 1rem;
-}
-
-.activity-item {
- background: linear-gradient(135deg, rgba(102, 126, 234, 0.05), rgba(118, 75, 162, 0.05));
- border-radius: 16px;
- padding: 1.5rem;
- border-left: 4px solid transparent;
- border-image: linear-gradient(135deg, #667eea, #764ba2) 1;
- transition: background 0.3s ease, transform 0.3s ease;
- position: relative;
- overflow: hidden;
- will-change: transform;
-}
-
-.activity-item::before {
- content: '';
- position: absolute;
- left: 0;
- top: 0;
- bottom: 0;
- width: 4px;
- background: linear-gradient(135deg, #667eea, #764ba2);
-}
-
-.activity-item:hover {
- background: linear-gradient(135deg, rgba(102, 126, 234, 0.1), rgba(118, 75, 162, 0.1));
- transform: translateX(8px);
-}
-
-.activity-date {
- font-size: 0.9rem;
- color: #667eea;
- font-weight: 700;
- text-transform: uppercase;
- letter-spacing: 0.5px;
- margin-bottom: 0.5rem;
- display: block;
-}
-
-.activity-desc {
- color: #1e293b;
- font-weight: 500;
- font-size: 1rem;
-}
-
-/* ===== CARDS PROJECTS ===== */
-.projects-grid {
- display: grid;
- grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
- gap: 2rem;
- margin: 2rem 0;
-}
-
-.card-projects {
- background: rgba(255, 255, 255, 0.95);
- backdrop-filter: blur(20px);
- border-radius: 20px;
- padding: 0;
- border: 1px solid rgba(255, 255, 255, 0.2);
- box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
- transition: transform 0.3s ease, box-shadow 0.3s ease;
- overflow: hidden;
- will-change: transform;
- position: relative;
-}
-
-.card-projects::before {
- content: '';
- position: absolute;
- top: 0;
- left: 0;
- right: 0;
- height: 4px;
- background: linear-gradient(90deg, #667eea, #764ba2);
- transform: scaleX(0);
- transition: transform 0.3s ease;
- transform-origin: left;
- z-index: 2;
-}
-
-.card-projects:hover::before {
- transform: scaleX(1);
-}
-
-.card-projects:hover {
- transform: translateY(-8px);
- box-shadow: 0 25px 50px rgba(0, 0, 0, 0.15);
-}
-
-.card-title {
- height: 150px;
- background: linear-gradient(135deg, #b3ea66 0%, #3193a4 100%);
- margin: 0;
- display: flex;
- align-items: center;
- justify-content: center;
- color: white;
- text-align: center;
- position: relative;
- overflow: hidden;
-}
-
-.card-title::after {
- content: '';
- position: absolute;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- background: rgba(0, 0, 0, 0.1);
- opacity: 0;
- transition: opacity 0.3s ease;
-}
-
-.card-projects:hover .card-title::after {
- opacity: 1;
-}
-
-.card-title h3 {
- font-size: 1.4rem;
- font-weight: 700;
- margin: 0;
- padding: 1rem;
- z-index: 1;
- position: relative;
-}
-
-.card-projects > h3 {
- font-size: 1.3rem;
- color: #1e293b;
- margin: 1.5rem 1.5rem 1rem 1.5rem;
- font-weight: 700;
-}
-
-.card-projects > p {
- color: #64748b;
- line-height: 1.6;
- margin: 0 1.5rem 1rem 1.5rem;
- font-size: 0.95rem;
-}
-
-.card-projects > p:last-of-type {
- margin-bottom: 1.5rem;
-}
-
-.card-projects > p strong {
- color: #1e293b;
- font-weight: 600;
-}
-
-.card-projects .btn {
- margin: 0 1.5rem 1.5rem 1.5rem;
- padding: 0.8rem 1.5rem;
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
- color: white;
- text-decoration: none;
- border-radius: 50px;
- font-weight: 600;
- text-transform: uppercase;
- letter-spacing: 0.5px;
- font-size: 0.9rem;
- transition: transform 0.2s ease, box-shadow 0.2s ease;
- display: inline-block;
- border: none;
- cursor: pointer;
-}
-
-.card-projects .btn:hover {
- transform: translateY(-2px);
- box-shadow: 0 10px 25px rgba(102, 126, 234, 0.4);
-}
-
-.card-tags {
- display: flex;
- flex-wrap: wrap;
- gap: 0.5rem;
- margin: 1.5rem;
-}
-
-.card-projects .action-btn {
- margin: 0.5rem;
-}
-
-span.tag {
- background: rgba(102, 126, 234, 0.1);
- color: #667eea;
- padding: 0.5rem 1rem;
- border-radius: 12px;
- font-size: 0.9rem;
- font-weight: 600;
- transition: background-color 0.3s ease, transform 0.3s ease;
-}
-
-/* ===== PROJECT ACTIONS ===== */
-.projects-actions {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 2rem;
- flex-wrap: wrap;
- gap: 1rem;
-}
-
-.projects-actions h1 {
- margin: 0;
- flex: 1;
- min-width: 200px;
-}
-
-.projects-actions .btn-success {
- flex-shrink: 0;
-}
-
-/* ===== PROJECT FORM STYLES ===== */
-.form-project {
- background: rgba(255, 255, 255, 0.95);
- backdrop-filter: blur(20px);
- border-radius: 20px;
- padding: 2rem;
- margin-bottom: 2rem;
- border: 1px solid rgba(255, 255, 255, 0.2);
- box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
- position: relative;
- overflow: hidden;
-}
-
-.form-project::before {
- content: '';
- position: absolute;
- top: 0;
- left: 0;
- right: 0;
- height: 4px;
- background: linear-gradient(90deg, #667eea, #764ba2);
-}
-
-.form-project.hidden {
- display: none;
-}
-
-.form-project form {
- display: grid;
- gap: 1.5rem;
- max-width: 800px;
- margin: 0 auto;
-}
-
-.form-group {
- display: flex;
- flex-direction: column;
- gap: 0.5rem;
-}
-
-.form-group label {
- font-weight: 600;
- color: #1e293b;
- font-size: 1rem;
- margin-bottom: 0.25rem;
- position: relative;
-}
-
-.form-group label::after {
- content: '';
- position: absolute;
- bottom: -2px;
- left: 0;
- width: 30px;
- height: 2px;
- background: linear-gradient(90deg, #667eea, #764ba2);
- border-radius: 1px;
-}
-
-.form-group input,
-.form-group textarea {
- padding: 1rem 1.25rem;
- border: 2px solid rgba(102, 126, 234, 0.1);
- border-radius: 12px;
- font-size: 1rem;
- font-family: inherit;
- background: rgba(255, 255, 255, 0.8);
- backdrop-filter: blur(10px);
- transition: all 0.3s ease;
- color: #1e293b;
- resize: vertical;
-}
-
-.form-group input:focus,
-.form-group textarea:focus {
- outline: none;
- border-color: #667eea;
- background: rgba(255, 255, 255, 0.95);
- box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
- transform: translateY(-1px);
-}
-
-.form-group input::placeholder,
-.form-group textarea::placeholder {
- color: #94a3b8;
- font-style: italic;
-}
-
-.form-group textarea {
- min-height: 120px;
- max-height: 300px;
- line-height: 1.6;
-}
-
-/* Styles pour les formulaires */
-.form-help {
- font-size: 0.8rem;
- color: #6b7280;
- margin-top: 0.25rem;
- font-style: italic;
- display: block;
-}
-
-/* Form responsive design */
-@media (max-width: 768px) {
- .form-project {
- padding: 1.5rem;
- margin: 1rem;
- border-radius: 16px;
- }
-
- .form-project form {
- gap: 1.25rem;
- }
-
- .form-group input,
- .form-group textarea {
- padding: 0.875rem 1rem;
- font-size: 0.95rem;
- }
-}
-
-/* ===== RESPONSIVE DESIGN ===== */
-@media (max-width: 1024px) {
- nav {
- transform: translateX(-100%);
- }
-
- nav.open {
- transform: translateX(0);
- }
-
- section {
- margin-left: 0;
- padding: 1rem;
- }
-
- .dashboard h1 {
- font-size: 2.5rem;
- }
-}
-
-@media (max-width: 768px) {
- .dashboard-grid {
- grid-template-columns: 1fr;
- gap: 1.5rem;
- }
-
- .dashboard h1 {
- font-size: 2rem;
- }
-
- .stats {
- grid-template-columns: repeat(2, 1fr);
- }
-
- .activity-item {
- padding: 1rem;
- }
-
- .activity-item:hover {
- transform: translateX(5px);
- }
-}
-
-@media (max-width: 480px) {
- section {
- padding: 0.5rem;
- }
-
- .dashboard-card {
- padding: 1.5rem;
- }
-
- .stats {
- grid-template-columns: 1fr;
- }
-
- .btn-primary, btn-success, .btn-secondary, .btn-danger {
- padding: 0.8rem 1.5rem;
- font-size: 0.9rem;
- }
-}
-
-.hidden {
- display: none;
-}
-
-/* ===== TECHNOLOGIES MANAGER STYLES ===== */
-.technologies-grid {
- background: #f8f9fa;
- border-radius: var(--border-radius);
- padding: 1.5rem;
- border: 1px solid #dee2e6;
-}
-
-.tech-controls {
- margin-bottom: 1.5rem;
- padding: 1rem;
- background: white;
- border-radius: var(--border-radius);
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
-}
-
-.tech-search {
- position: relative;
- margin-bottom: 1rem;
-}
-
-.tech-search input {
- width: 100%;
- padding: 0.75rem 2.5rem 0.75rem 1rem;
- border: 2px solid #e9ecef;
- border-radius: 25px;
- font-size: 0.9rem;
- transition: all var(--transition-speed);
-}
-
-.tech-search input:focus {
- border-color: var(--primary-color);
- box-shadow: 0 0 0 3px rgba(58, 134, 255, 0.1);
- outline: none;
-}
-
-.search-icon {
- position: absolute;
- right: 1rem;
- top: 50%;
- transform: translateY(-50%);
- color: var(--gray-color);
- font-size: 1.1rem;
-}
-
-.tech-quick-actions {
- display: flex;
- gap: 0.5rem;
- margin-bottom: 1rem;
- flex-wrap: wrap;
-}
-
-.btn-quick {
- padding: 0.5rem 1rem;
- border: 1px solid var(--primary-color);
- background: white;
- color: var(--primary-color);
- border-radius: 20px;
- font-size: 0.8rem;
- cursor: pointer;
- transition: all var(--transition-speed);
-}
-
-.btn-quick:hover {
- background: var(--primary-color);
- color: white;
- transform: translateY(-2px);
-}
-
-.selected-count {
- text-align: center;
- font-weight: 600;
- padding: 0.5rem;
- border-radius: var(--border-radius);
-}
-
-.count-empty {
- color: var(--gray-color);
- background: #f8f9fa;
-}
-
-.count-low {
- color: #ffc107;
- background: #fff3cd;
-}
-
-.count-medium {
- color: #28a745;
- background: #d1edda;
-}
-
-.count-high {
- color: #dc3545;
- background: #f8d7da;
-}
-
-.tech-categories {
- display: grid;
- gap: 1rem;
-}
-
-.tech-category {
- background: white;
- border-radius: var(--border-radius);
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
- overflow: hidden;
- transition: all var(--transition-speed);
-}
-
-.tech-category:hover {
- box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
-}
-
-.category-header {
- background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
- color: white;
- padding: 1rem;
- cursor: pointer;
- display: flex;
- align-items: center;
- justify-content: space-between;
- user-select: none;
- transition: all var(--transition-speed);
-}
-
-.category-header:hover {
- background: linear-gradient(135deg, var(--secondary-color), var(--primary-color));
-}
-
-.category-header h4 {
- margin: 0;
- font-size: 1rem;
- font-weight: 600;
-}
-
-.category-toggle {
- font-size: 1.2rem;
- transition: transform var(--transition-speed);
-}
-
-.category-count {
- font-size: 0.8rem;
- opacity: 0.8;
-}
-
-.category-items {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
- gap: 0.5rem;
- padding: 1rem;
- background: #fafbfc;
-}
-
-.tech-item {
- display: flex;
- align-items: center;
- padding: 0.5rem;
- background: white;
- border-radius: 6px;
- transition: all var(--transition-speed);
- cursor: pointer;
- border: 2px solid transparent;
-}
-
-.tech-item:hover {
- background: #e3f2fd;
- transform: translateY(-1px);
-}
-
-.tech-checkbox {
- margin-right: 0.5rem;
- transform: scale(1.2);
- cursor: pointer;
-}
-
-.tech-checkbox:checked + .tech-label {
- color: var(--primary-color);
- font-weight: 600;
-}
-
-.tech-item:has(.tech-checkbox:checked) {
- background: linear-gradient(135deg, #e3f2fd, #f3e5f5);
- border-color: var(--primary-color);
- box-shadow: 0 2px 4px rgba(58, 134, 255, 0.2);
-}
-
-.tech-label {
- font-size: 0.9rem;
- cursor: pointer;
- transition: all var(--transition-speed);
- flex: 1;
-}
-
-/* ===== RESPONSIVE DESIGN ===== */
-@media (max-width: 768px) {
- .tech-categories {
- grid-template-columns: 1fr;
- }
-
- .category-items {
- grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
- gap: 0.3rem;
- padding: 0.5rem;
- }
-
- .tech-item {
- padding: 0.3rem;
- }
-
- .tech-quick-actions {
- justify-content: center;
- }
-
- .btn-quick {
- flex: 1;
- min-width: 0;
- }
-}
-
-@media (max-width: 480px) {
- .category-items {
- grid-template-columns: 1fr;
- }
-
- .tech-quick-actions {
- flex-direction: column;
- }
-
- .category-header {
- padding: 0.75rem;
- }
-
- .category-header h4 {
- font-size: 0.9rem;
- }
-}
-
-/* ===== ANIMATIONS ===== */
-.tech-item {
- animation: fadeInUp 0.3s ease-out;
-}
-
-@keyframes fadeInUp {
- from {
- opacity: 0;
- transform: translateY(10px);
- }
- to {
- opacity: 1;
- transform: translateY(0);
- }
-}
-
-.tech-category {
- animation: slideIn 0.5s ease-out;
-}
-
-@keyframes slideIn {
- from {
- opacity: 0;
- transform: translateX(-20px);
- }
- to {
- opacity: 1;
- transform: translateX(0);
- }
-}
-
-/* ===== DARK THEME SUPPORT ===== */
-.dark-theme .technologies-grid {
- background: #2d2d2d;
- border-color: #444;
-}
-
-.dark-theme .tech-controls {
- background: #3a3a3a;
-}
-
-.dark-theme .tech-search input {
- background: #2d2d2d;
- border-color: #555;
- color: #e0e0e0;
-}
-
-.dark-theme .tech-item {
- background: #3a3a3a;
- color: #e0e0e0;
-}
-
-.dark-theme .tech-item:hover {
- background: #4a4a4a;
-}
-
-.dark-theme .category-items {
- background: #2d2d2d;
-}
\ No newline at end of file
diff --git a/Static/css/style-secondaire.css b/Static/css/style-secondaire.css
deleted file mode 100644
index 91d6510..0000000
--- a/Static/css/style-secondaire.css
+++ /dev/null
@@ -1,710 +0,0 @@
-/* Réinitialisation de base */
-*, *::before, *::after {
- margin: 0;
- padding: 0;
- box-sizing: border-box;
-}
-
-/* Variables pour palette de couleurs et typographie */
-:root {
- /* Palette principale - Couleurs professionnelles et tech */
- --primary-color: #2563eb; /* Bleu royal - Couleur principale */
- --secondary-color: #0e84a3; /* Bleu-vert - Couleur secondaire */
- --accent-color: #0ea5e9; /* Cyan vif - Pour accents et CTA */
- --dark-color: #0f172a; /* Bleu très foncé - Fond et texte */
- --light-color: #f8fafc; /* Blanc cassé - Fond et contraste */
- --gray-color: #64748b; /* Gris slate - Texte secondaire */
- --success-color: #10b981; /* Vert émeraude - Succès/validation */
- --header-color: #0f172a; /* Bleu foncé pour le texte du header */
- --header-color-variant: #38bdf8; /* Bleu ciel pour accentuation */
-
- /* Dégradés professionnels */
- --gradient-header: linear-gradient(135deg, #1e3a8a, #0369a1, #0284c7);
- --gradient-card: linear-gradient(to bottom right, rgba(15, 23, 42, 0.03), rgba(14, 165, 233, 0.05));
- --gradient-accent: linear-gradient(90deg, #0ea5e9, #2563eb);
-
- /* Typographie améliorée */
- --font-main: 'Inter', 'Segoe UI', system-ui, sans-serif;
- --font-heading: 'Montserrat', 'Segoe UI', system-ui, sans-serif;
- --font-code: 'JetBrains Mono', 'Cascadia Code', monospace;
-
- /* Variables d'interface */
- --transition-speed: 0.3s;
- --border-radius: 8px;
- --card-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
- --box-shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
- --box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
-}
-
-/* Style général du corps */
-html {
- scroll-behavior: smooth;
-}
-
-body {
- font-family: var(--font-main);
- line-height: 1.6;
- color: var(--dark-color);
- background-color: var(--light-color);
- overflow-x: hidden;
-}
-
-/* En-tête */
-header {
- background: var(--gradient-header);
- background-size: 200% 200%;
- animation: gradientAnimation 15s ease infinite;
- color: white;
- padding: 5rem 1rem;
- text-align: center;
- min-height: 280px;
- display: flex;
- flex-direction: column;
- justify-content: center;
- position: relative;
- overflow: hidden;
- box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
- perspective: 1000px;
-}
-
-@keyframes gradientAnimation {
- 0% { background-position: 0% 50%; }
- 50% { background-position: 100% 50%; }
- 100% { background-position: 0% 50%; }
-}
-
-/* Animations et éléments pour l'en-tête */
-header::before {
- content: '';
- position: absolute;
- top: -50%;
- left: -50%;
- width: 200%;
- height: 200%;
- background: radial-gradient(circle, rgba(255, 255, 255, 0.1) 10%, transparent 70%);
- transform: rotate(30deg);
- pointer-events: none;
- animation: shimmer 15s infinite linear;
- z-index: 1;
-}
-
-/* Vagues animées */
-header::after {
- content: '';
- position: absolute;
- bottom: 0;
- left: 0;
- width: 100%;
- height: 70px;
- background: url('data:image/svg+xml;utf8, ');
- background-size: cover;
- background-repeat: no-repeat;
- filter: drop-shadow(0px -5px 5px rgba(0,0,0,0.1));
- z-index: 2;
-}
-
-/* Effet de superposition en dégradé */
-header .overlay {
- position: absolute;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- background: radial-gradient(circle at center, transparent 0%, rgba(0,0,0,0.2) 100%);
- z-index: 1;
-}
-
-/* Particules animées */
-@keyframes float {
- 0% { transform: translateY(0) rotate(0deg) scale(1); opacity: 0.8; }
- 25% { transform: translateY(-15px) rotate(5deg) scale(0.9); opacity: 0.6; }
- 50% { transform: translateY(-25px) rotate(10deg) scale(0.8); opacity: 0.4; }
- 75% { transform: translateY(-15px) rotate(5deg) scale(0.9); opacity: 0.6; }
- 100% { transform: translateY(0) rotate(0deg) scale(1); opacity: 0.8; }
-}
-
-@keyframes shimmer {
- 0% { transform: rotate(30deg) translateY(0) scale(1); opacity: 0.5; }
- 50% { transform: rotate(25deg) translateY(-15px) scale(1.05); opacity: 0.7; }
- 100% { transform: rotate(30deg) translateY(0) scale(1); opacity: 0.5; }
-}
-
-@keyframes pulse {
- 0% { transform: scale(1); opacity: 0.8; }
- 50% { transform: scale(1.05); opacity: 1; }
- 100% { transform: scale(1); opacity: 0.8; }
-}
-
-.particles {
- position: absolute;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- overflow: hidden;
- pointer-events: none;
- z-index: 1;
-}
-
-.particle {
- position: absolute;
- display: block;
- border-radius: 50%;
- background-color: rgba(255, 255, 255, 0.4);
- box-shadow: 0 0 20px 5px rgba(255, 255, 255, 0.3);
- pointer-events: none;
- z-index: 1;
- backdrop-filter: blur(1px);
- filter: blur(1px);
-}
-
-.particle.star {
- clip-path: polygon(50% 0%, 61% 35%, 98% 35%, 68% 57%, 79% 91%, 50% 70%, 21% 91%, 32% 57%, 2% 35%, 39% 35%);
- background-color: rgba(255, 255, 255, 0.9);
- box-shadow: 0 0 20px 10px rgba(255, 255, 255, 0.4);
- filter: blur(0);
-}
-
-/* Animation des éléments du header */
-header h1 {
- position: relative;
- z-index: 2;
- font-size: 3.8rem;
- font-weight: 800;
- margin-bottom: 0.5rem;
- text-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
- animation: fadeInDown 1s both, pulse 5s infinite ease-in-out 1s;
- letter-spacing: 1px;
- transform: translateZ(50px);
-}
-
-header h1::after {
- content: '';
- position: absolute;
- background: white;
- left: 50%;
- transform: translateX(-50%);
- width: 100px;
- height: 4px;
- bottom: -10px;
- animation: expandWidth 1.5s ease forwards;
- border-radius: var(--border-radius);
- box-shadow: 0 2px 10px rgba(255, 255, 255, 0.5);
-}
-
-header p {
- position: relative;
- z-index: 2;
- font-size: 1.6rem;
- color: rgba(255, 255, 255, 0.95);
- font-weight: 300;
- margin-top: 1.2rem;
- animation: fadeInUp 1s 0.5s both;
- text-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
- transform: translateZ(30px);
-}
-
-@keyframes fadeInDown {
- from {
- opacity: 0;
- transform: translateY(-30px);
- }
- to {
- opacity: 1;
- transform: translateY(0);
- }
-}
-
-@keyframes fadeInUp {
- from {
- opacity: 0;
- transform: translateY(30px);
- }
- to {
- opacity: 1;
- transform: translateY(0);
- }
-}
-
-@keyframes expandWidth {
- from { width: 0; }
- to { width: 100px; }
-}
-
-/* Styles du contenu du header */
-.header-content {
- position: relative;
- z-index: 10;
- max-width: 800px;
- margin: 0 auto;
- background: rgba(255, 255, 255, 0.05);
- backdrop-filter: blur(10px);
- border-radius: 16px;
- padding: 2rem;
- border: 1px solid rgba(255, 255, 255, 0.12);
- box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15);
- transform-style: preserve-3d;
-}
-
-/* Effet de frappe au clavier */
-.typing-text {
- display: inline-block;
- white-space: nowrap;
- border-right: none;
- font-family: var(--font-main);
- background: linear-gradient(to right, var(--accent-color), var(--header-color-variant));
- -webkit-background-clip: text;
- background-clip: text;
- -webkit-text-fill-color: transparent;
- font-weight: 600;
- letter-spacing: 0.5px;
-}
-
-.cursor {
- display: inline-block;
- width: 3px;
- height: 1.6em;
- background-color: white;
- margin-left: 3px;
- animation: blink 1s step-end infinite;
- box-shadow: 0 0 10px rgba(255, 255, 255, 0.8);
- vertical-align: middle;
- position: relative;
- top: -2px;
-}
-
-@keyframes blink {
- from, to { opacity: 1; box-shadow: 0 0 15px rgba(255, 255, 255, 0.8); }
- 50% { opacity: 0; box-shadow: none; }
-}
-
-/* Navigation */
-nav {
- background-color: var(--dark-color);
- position: sticky;
- top: 0;
- z-index: 1000;
- box-shadow: 0 2px 4px rgba(0,0,0,0.1);
-}
-
-nav ul {
- display: flex;
- justify-content: center;
- list-style-type: none;
- padding: 0;
-}
-
-nav ul li {
- position: relative;
-}
-
-nav ul li a {
- display: inline-block;
- padding: 1rem 1.5rem;
- text-decoration: none;
- font-weight: 500;
- transition: all var(--transition-speed) ease;
- cursor: pointer;
-}
-
-nav ul li::after {
- content: '';
- position: absolute;
- bottom: 0;
- left: 50%;
- width: 0;
- height: 2px;
- background-color: var(--accent-color);
- transition: all var(--transition-speed) ease;
- transform: translateX(-50%);
-}
-
-nav ul li:hover::after {
- width: 80%;
-}
-
-nav ul li:hover a {
- color: var(--accent-color);
-}
-
-/* Styles pour les pages actives et scrolled navbar */
-nav.scrolled {
- background-color: rgba(26, 26, 46, 0.95);
- box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
- padding: 5px 10px;
- transition: all 0.3s ease;
-}
-
-nav ul li.active::after {
- width: 80%;
-}
-
-nav ul li.active a {
- color: var(--accent-color);
-}
-
-/* Contenu principal */
-main {
- max-width: 1200px;
- margin: 2rem auto;
- padding: 0 1rem;
-}
-
-.main-content {
- background-color: white;
- border-radius: var(--border-radius);
- padding: 2rem;
- box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
- transition: transform var(--transition-speed);
-}
-
-/* Titres */
-h1, h2, h3, h4, h5, h6 {
- font-family: var(--font-heading);
- margin-bottom: 1rem;
- color: var(--dark-color);
- font-weight: 700;
- line-height: 1.2;
-}
-
-h1 {
- font-size: 2.5rem;
- margin-bottom: 1.5rem;
- position: relative;
- display: inline-block;
-}
-
-h1::after {
- content: '';
- position: absolute;
- bottom: -10px;
- left: 0;
- width: 80px;
- height: 4px;
- background: var(--accent-color);
-}
-
-h2 {
- font-size: 2rem;
-}
-
-h3 {
- font-size: 1.75rem;
-}
-
-/* Paragraphes */
-p {
- margin-bottom: 1.5rem;
- color: var(--gray-color);
-}
-
-/* Liens */
-a {
- color: var(--primary-color);
- text-decoration: none;
- transition: color var(--transition-speed);
-}
-
-a:hover {
- color: var(--secondary-color);
-}
-
-/* Pied de page */
-footer {
- background-color: var(--dark-color);
- color: var(--light-color);
- padding: 2rem 1rem;
- text-align: center;
- margin-top: 3rem;
-}
-
-footer p {
- color: var(--light-color);
- opacity: 0.8;
- margin-bottom: 0;
-}
-
-/* Styles pour la visualisation des compétences */
-.skill-card {
- background-color: var(--light-color);
- padding: 1.2rem;
- border-radius: var(--border-radius);
- box-shadow: 0 2px 4px rgba(0,0,0,0.05);
- transition: transform 0.3s ease, box-shadow 0.3s ease;
-}
-
-.skill-card:hover {
- transform: translateY(-5px);
- box-shadow: 0 8px 15px rgba(0,0,0,0.1);
-}
-
-.skill-bar {
- height: 6px;
- background-color: #e0e0e0;
- margin-top: 1rem;
- border-radius: 3px;
- overflow: hidden;
-}
-
-.skill-bar-fill {
- height: 100%;
- border-radius: 3px;
-}
-
-.lang-circle {
- position: relative;
- width: 100px;
- height: 100px;
- border-radius: 50%;
- margin: 10px auto;
-}
-
-.lang-circle-inner {
- position: absolute;
- top: 10px;
- left: 10px;
- width: 80px;
- height: 80px;
- border-radius: 50%;
- background-color: var(--light-color);
- display: flex;
- align-items: center;
- justify-content: center;
-}
-
-.service-icon {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- width: 40px;
- height: 40px;
- color: white;
- border-radius: 8px;
- margin-right: 10px;
-}
-
-.experience-timeline {
- position: relative;
- padding-left: 30px;
- margin-left: 20px;
- border-left: 2px solid var(--primary-color);
-}
-
-.timeline-dot {
- position: absolute;
- width: 20px;
- height: 20px;
- background-color: var(--primary-color);
- border-radius: 50%;
- left: -41px;
- top: 0;
-}
-
-/* Media queries pour le responsive */
-@media (max-width: 768px) {
- nav ul {
- flex-direction: column;
- align-items: center;
- }
-
- nav ul li {
- width: 100%;
- text-align: center;
- }
-
- nav ul li a {
- padding: 0.75rem 1rem;
- }
-
- h1 {
- font-size: 2rem;
- }
-
- h2 {
- font-size: 1.75rem;
- }
-
- h3 {
- font-size: 1.5rem;
- }
-}
-
-/* Animations et effets */
-@keyframes fadeIn {
- from { opacity: 0; }
- to { opacity: 1; }
-}
-
-.main-content {
- animation: fadeIn 0.5s ease-in-out;
-}
-
-/* Animations supplémentaires */
-@keyframes slideInUp {
- from {
- transform: translateY(20px);
- opacity: 0;
- }
- to {
- transform: translateY(0);
- opacity: 1;
- }
-}
-
-.section {
- animation: slideInUp 0.5s ease-out forwards;
-}
-
-/* Style pour les sections de contenu */
-.section {
- margin-bottom: 3rem;
- padding: 1.8rem;
- border-radius: var(--border-radius);
- background-color: white;
- box-shadow: var(--card-shadow);
- transition: all var(--transition-speed);
- border: 1px solid rgba(0, 0, 0, 0.03);
- position: relative;
- overflow: hidden;
-}
-
-.section:hover {
- transform: translateY(-5px);
- box-shadow: 0 12px 28px rgba(0, 0, 0, 0.12);
-}
-
-.section::before {
- content: '';
- position: absolute;
- top: 0;
- left: 0;
- width: 4px;
- height: 0;
- background: var(--gradient-accent);
- transition: height 0.5s ease;
-}
-
-.section:hover::before {
- height: 100%;
-}
-
-/* Styles pour des cartes technologiques */
-.tech-card {
- background: var(--gradient-card);
- padding: 1.5rem;
- border-radius: var(--border-radius);
- border: 1px solid rgba(0, 0, 0, 0.05);
- display: flex;
- align-items: center;
- gap: 1.5rem;
- margin-bottom: 1.5rem;
- transition: all var(--transition-speed);
-}
-
-.tech-card:hover {
- transform: translateY(-3px);
- box-shadow: var(--box-shadow);
-}
-
-.tech-icon {
- width: 50px;
- height: 50px;
- display: flex;
- align-items: center;
- justify-content: center;
- border-radius: 12px;
- font-size: 1.5rem;
- color: white;
- background: var(--gradient-accent);
-}
-
-/* Boutons */
-.btn {
- display: inline-block;
- padding: 0.75rem 1.5rem;
- background: var(--gradient-accent);
- color: white;
- border-radius: 8px;
- border: none;
- cursor: pointer;
- transition: all var(--transition-speed);
- font-weight: 600;
- font-size: 0.9rem;
- letter-spacing: 0.5px;
- box-shadow: var(--box-shadow);
- position: relative;
- overflow: hidden;
- z-index: 1;
-}
-
-.btn::before {
- content: '';
- position: absolute;
- top: 0;
- left: -100%;
- width: 100%;
- height: 100%;
- background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
- transition: left 0.7s ease;
- z-index: -1;
-}
-
-.btn:hover {
- transform: translateY(-3px);
- box-shadow: 0 6px 15px rgba(37, 99, 235, 0.3);
-}
-
-.btn:hover::before {
- left: 100%;
-}
-
-/* Style des formulaires */
-input, textarea {
- transition: border-color var(--transition-speed);
-}
-
-input:focus, textarea:focus {
- outline: none;
- border-color: var(--primary-color) !important;
- box-shadow: 0 0 0 2px rgba(58, 134, 255, 0.2);
-}
-
-/* Mode sombre */
-.dark-theme {
- --light-color: #1a1a2e;
- --dark-color: #f8f9fa;
- --gray-color: #e0e0e0;
- background-color: #121212;
-}
-
-.dark-theme .main-content {
- background-color: #252525;
-}
-
-.dark-theme .section,
-.dark-theme [style*="background-color: var(--light-color)"] {
- background-color: #2d2d2d !important;
- color: var(--dark-color);
-}
-
-.dark-theme p {
- color: #e0e0e0;
-}
-
-/* Custom scrollbar */
-::-webkit-scrollbar {
- width: 8px;
-}
-
-::-webkit-scrollbar-track {
- background: #f1f1f1;
-}
-
-::-webkit-scrollbar-thumb {
- background: var(--primary-color);
- border-radius: 10px;
-}
-
-::-webkit-scrollbar-thumb:hover {
- background: var(--secondary-color);
-}
\ No newline at end of file
diff --git a/Static/css/style.css b/Static/css/style.css
deleted file mode 100644
index 18fa1d4..0000000
--- a/Static/css/style.css
+++ /dev/null
@@ -1,837 +0,0 @@
-/* Réinitialisation de base */
-*, *::before, *::after {
- margin: 0;
- padding: 0;
- box-sizing: border-box;
-}
-
-/* Variables pour palette de couleurs et typographie */
-:root {
- --primary-color: #3a86ff;
- --secondary-color: #8338ec;
- --accent-color: #ff006e;
- --dark-color: #2f2f2f;
- --light-color: #f8f9fa;
- --gray-color: #6c757d;
- --success-color: #06d6a0;
- --header-color: #09092d;
- --header-color-variant: #6576a3;
-
- --gradient-header: linear-gradient(135deg, #4158D0, #C850C0, #FFCC70);
- --font-main: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
- --font-heading: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
- --transition-speed: 0.3s;
- --border-radius: 8px;
-}
-
-/* Style général du corps */
-html {
- scroll-behavior: smooth;
-}
-
-body {
- font-family: var(--font-main);
- line-height: 1.6;
- color: var(--dark-color);
- background-color: var(--light-color);
- overflow-x: hidden;
-}
-
-/* En-tête */
-header {
- background: var(--gradient-header);
- background-size: 200% 200%;
- animation: gradientAnimation 15s ease infinite;
- color: white;
- padding: 5rem 1rem;
- text-align: center;
- min-height: 280px;
- display: flex;
- flex-direction: column;
- justify-content: center;
- position: relative;
- overflow: hidden;
- box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
- perspective: 1000px;
-}
-
-@keyframes gradientAnimation {
- 0% { background-position: 0% 50%; }
- 50% { background-position: 100% 50%; }
- 100% { background-position: 0% 50%; }
-}
-
-/* Animations et éléments pour l'en-tête */
-header::before {
- content: '';
- position: absolute;
- top: -50%;
- left: -50%;
- width: 200%;
- height: 200%;
- background: radial-gradient(circle, rgba(255, 255, 255, 0.1) 10%, transparent 70%);
- transform: rotate(30deg);
- pointer-events: none;
- animation: shimmer 15s infinite linear;
- z-index: 1;
-}
-
-/* Vagues animées */
-header::after {
- content: '';
- position: absolute;
- bottom: 0;
- left: 0;
- width: 100%;
- height: 70px;
- background: url('data:image/svg+xml;utf8, ');
- background-size: cover;
- background-repeat: no-repeat;
- filter: drop-shadow(0px -5px 5px rgba(0,0,0,0.1));
- z-index: 2;
-}
-
-/* Effet de superposition en dégradé */
-header .overlay {
- position: absolute;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- background: radial-gradient(circle at center, transparent 0%, rgba(0,0,0,0.2) 100%);
- z-index: 1;
-}
-
-/* Particules animées */
-@keyframes float {
- 0% { transform: translateY(0) rotate(0deg) scale(1); opacity: 0.8; }
- 25% { transform: translateY(-15px) rotate(5deg) scale(0.9); opacity: 0.6; }
- 50% { transform: translateY(-25px) rotate(10deg) scale(0.8); opacity: 0.4; }
- 75% { transform: translateY(-15px) rotate(5deg) scale(0.9); opacity: 0.6; }
- 100% { transform: translateY(0) rotate(0deg) scale(1); opacity: 0.8; }
-}
-
-@keyframes shimmer {
- 0% { transform: rotate(30deg) translateY(0) scale(1); opacity: 0.5; }
- 50% { transform: rotate(25deg) translateY(-15px) scale(1.05); opacity: 0.7; }
- 100% { transform: rotate(30deg) translateY(0) scale(1); opacity: 0.5; }
-}
-
-@keyframes pulse {
- 0% { transform: scale(1); opacity: 0.8; }
- 50% { transform: scale(1.05); opacity: 1; }
- 100% { transform: scale(1); opacity: 0.8; }
-}
-
-.particles {
- position: absolute;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- overflow: hidden;
- pointer-events: none;
- z-index: 1;
-}
-
-.particle {
- position: absolute;
- display: block;
- border-radius: 50%;
- background-color: rgba(255, 255, 255, 0.4);
- box-shadow: 0 0 20px 5px rgba(255, 255, 255, 0.3);
- pointer-events: none;
- z-index: 1;
- backdrop-filter: blur(1px);
- filter: blur(1px);
-}
-
-.particle.star {
- clip-path: polygon(50% 0%, 61% 35%, 98% 35%, 68% 57%, 79% 91%, 50% 70%, 21% 91%, 32% 57%, 2% 35%, 39% 35%);
- background-color: rgba(255, 255, 255, 0.9);
- box-shadow: 0 0 20px 10px rgba(255, 255, 255, 0.4);
- filter: blur(0);
-}
-
-/* Animation des éléments du header */
-header h1 {
- position: relative;
- z-index: 2;
- font-size: 3.8rem;
- font-weight: 800;
- margin-bottom: 0.5rem;
- text-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
- animation: fadeInDown 1s both, pulse 5s infinite ease-in-out 1s;
- letter-spacing: 1px;
- transform: translateZ(50px);
-}
-
-header h1::after {
- content: '';
- position: absolute;
- background: white;
- left: 50%;
- transform: translateX(-50%);
- width: 100px;
- height: 4px;
- bottom: -10px;
- animation: expandWidth 1.5s ease forwards;
- border-radius: var(--border-radius);
- box-shadow: 0 2px 10px rgba(255, 255, 255, 0.5);
-}
-
-header p {
- position: relative;
- z-index: 2;
- font-size: 1.6rem;
- color: rgba(255, 255, 255, 0.95);
- font-weight: 300;
- margin-top: 1.2rem;
- animation: fadeInUp 1s 0.5s both;
- text-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
- transform: translateZ(30px);
-}
-
-@keyframes fadeInDown {
- from {
- opacity: 0;
- transform: translateY(-30px);
- }
- to {
- opacity: 1;
- transform: translateY(0);
- }
-}
-
-@keyframes fadeInUp {
- from {
- opacity: 0;
- transform: translateY(30px);
- }
- to {
- opacity: 1;
- transform: translateY(0);
- }
-}
-
-@keyframes expandWidth {
- from { width: 0; }
- to { width: 100px; }
-}
-
-/* Styles du contenu du header */
-.header-content {
- position: relative;
- z-index: 10;
- max-width: 800px;
- margin: 0 auto;
-}
-
-/* Effet de frappe au clavier */
-.typing-text {
- display: inline-block;
- white-space: nowrap;
- border-right: none;
- font-family: var(--font-main);
- background: linear-gradient(to right, var(--header-color), var(--header-color-variant));
- -webkit-background-clip: text;
- background-clip: text;
- -webkit-text-fill-color: transparent;
- font-weight: 500;
-}
-
-.cursor {
- display: inline-block;
- width: 3px;
- height: 1.6em;
- background-color: white;
- margin-left: 3px;
- animation: blink 1s step-end infinite;
- box-shadow: 0 0 10px rgba(255, 255, 255, 0.8);
- vertical-align: middle;
- position: relative;
- top: -2px;
-}
-
-@keyframes blink {
- from, to { opacity: 1; box-shadow: 0 0 15px rgba(255, 255, 255, 0.8); }
- 50% { opacity: 0; box-shadow: none; }
-}
-
-/* Navigation */
-nav {
- background-color: var(--dark-color);
- position: sticky;
- top: 0;
- z-index: 1000;
- box-shadow: 0 2px 4px rgba(0,0,0,0.1);
-}
-
-nav ul {
- display: flex;
- justify-content: center;
- list-style-type: none;
- padding: 0;
-}
-
-nav ul li {
- position: relative;
-}
-
-nav ul li a {
- display: inline-block;
- padding: 1rem 1.5rem;
- text-decoration: none;
- font-weight: 500;
- transition: all var(--transition-speed) ease;
- cursor: pointer;
-}
-
-nav ul li::after {
- content: '';
- position: absolute;
- bottom: 0;
- left: 50%;
- width: 0;
- height: 2px;
- background-color: var(--accent-color);
- transition: all var(--transition-speed) ease;
- transform: translateX(-50%);
-}
-
-nav ul li:hover::after {
- width: 80%;
-}
-
-nav ul li:hover a {
- color: var(--accent-color);
-}
-
-/* Styles pour les pages actives et scrolled navbar */
-nav.scrolled {
- background-color: rgba(26, 26, 46, 0.95);
- box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
- padding: 5px 10px;
- transition: all 0.3s ease;
-}
-
-nav ul li.active::after {
- width: 80%;
-}
-
-nav ul li.active a {
- color: var(--accent-color);
-}
-
-/* Contenu principal */
-main {
- max-width: 1200px;
- margin: 2rem auto;
- padding: 0 1rem;
-}
-
-.main-content {
- background-color: white;
- border-radius: var(--border-radius);
- padding: 2rem;
- box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
- transition: transform var(--transition-speed);
-}
-
-/* Titres */
-h1, h2, h3, h4, h5, h6 {
- font-family: var(--font-heading);
- margin-bottom: 1rem;
- color: var(--dark-color);
- font-weight: 700;
- line-height: 1.2;
-}
-
-h1 {
- font-size: 2.5rem;
- margin-bottom: 1.5rem;
- position: relative;
- display: inline-block;
-}
-
-h1::after {
- content: '';
- position: absolute;
- bottom: -10px;
- left: 0;
- width: 80px;
- height: 4px;
- background: var(--accent-color);
-}
-
-h2 {
- font-size: 2rem;
-}
-
-h3 {
- font-size: 1.75rem;
-}
-
-/* Paragraphes */
-p {
- margin-bottom: 1.5rem;
-}
-
-/* Liens */
-a {
- color: var(--primary-color);
- text-decoration: none;
- transition: color var(--transition-speed);
-}
-
-a:hover {
- color: var(--secondary-color);
-}
-
-/* Pied de page */
-footer {
- background-color: #2d2d2d;
- color: var(--light-color);
- padding: 2rem 1rem;
- text-align: center;
- margin-top: 3rem;
-}
-
-footer p {
- color: var(--dark-color);
- opacity: 0.8;
- margin-bottom: 0;
-}
-
-/* Styles pour la visualisation des compétences */
-.skill-card {
- background-color: var(--light-color);
- padding: 1.2rem;
- border-radius: var(--border-radius);
- box-shadow: 0 2px 4px rgba(0,0,0,0.05);
- transition: transform 0.3s ease, box-shadow 0.3s ease;
-}
-
-.skill-card:hover {
- transform: translateY(-5px);
- box-shadow: 0 8px 15px rgba(0,0,0,0.1);
-}
-
-.skill-bar {
- height: 6px;
- background-color: #e0e0e0;
- margin-top: 1rem;
- border-radius: 3px;
- overflow: hidden;
-}
-
-.skill-bar-fill {
- height: 100%;
- border-radius: 3px;
-}
-
-.lang-circle {
- position: relative;
- width: 100px;
- height: 100px;
- border-radius: 50%;
- margin: 10px auto;
-}
-
-.lang-circle-inner {
- position: absolute;
- top: 10px;
- left: 10px;
- width: 80px;
- height: 80px;
- border-radius: 50%;
- background-color: var(--light-color);
- display: flex;
- align-items: center;
- justify-content: center;
-}
-
-.service-icon {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- width: 40px;
- height: 40px;
- color: white;
- border-radius: 8px;
- margin-right: 10px;
-}
-
-.experience-timeline {
- position: relative;
- padding-left: 30px;
- margin-left: 20px;
- border-left: 2px solid var(--primary-color);
-}
-
-.timeline-dot {
- position: absolute;
- width: 20px;
- height: 20px;
- background-color: var(--primary-color);
- border-radius: 50%;
- left: -41px;
- top: 0;
-}
-
-
-/* Media queries pour le responsive */
-@media (max-width: 768px) {
- nav ul {
- flex-direction: column;
- align-items: center;
- }
-
- nav ul li {
- width: 100%;
- text-align: center;
- }
-
- nav ul li a {
- padding: 0.75rem 1rem;
- }
-
- h1 {
- font-size: 2rem;
- }
-
- h2 {
- font-size: 1.75rem;
- }
-
- h3 {
- font-size: 1.5rem;
- }
-}
-
-/* Animations et effets */
-@keyframes fadeIn {
- from { opacity: 0; }
- to { opacity: 1; }
-}
-
-.main-content {
- animation: fadeIn 0.5s ease-in-out;
-}
-
-/* Animations supplémentaires */
-@keyframes slideInUp {
- from {
- transform: translateY(20px);
- opacity: 0;
- }
- to {
- transform: translateY(0);
- opacity: 1;
- }
-}
-
-.section {
- animation: slideInUp 0.5s ease-out forwards;
-}
-
-/* Style pour les sections de contenu */
-.section {
- margin-bottom: 3rem;
- padding: 1.5rem;
- border-radius: var(--border-radius);
- background-color: white;
- box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
- transition: transform var(--transition-speed);
-}
-
-.section:hover {
- transform: translateY(-5px);
-}
-
-/* Boutons */
-.btn {
- display: inline-block;
- padding: 0.75rem 1.5rem;
- background-color: var(--primary-color);
- color: white;
- border-radius: 30px;
- border: none;
- cursor: pointer;
- transition: all var(--transition-speed);
- font-weight: 600;
- text-transform: uppercase;
- font-size: 0.9rem;
- letter-spacing: 0.5px;
- box-shadow: 0 4px 6px rgba(58, 134, 255, 0.2);
-}
-
-.btn:hover {
- background-color: var(--secondary-color);
- color: white;
- transform: translateY(-3px);
- box-shadow: 0 6px 8px rgba(131, 56, 236, 0.3);
-}
-
-/* Style des formulaires */
-input, textarea {
- transition: border-color var(--transition-speed);
-}
-
-input:focus, textarea:focus {
- outline: none;
- border-color: var(--primary-color) !important;
- box-shadow: 0 0 0 2px rgba(58, 134, 255, 0.2);
-}
-
-/* Mode sombre */
-.dark-theme {
- --light-color: #1a1a2e;
- --dark-color: #f8f9fa;
- --gray-color: #e0e0e0;
- background-color: #121212;
-}
-
-.dark-theme .main-content {
- background-color: #252525;
-}
-
-.dark-theme .section,
-.dark-theme [style*="background-color: var(--light-color)"] {
- background-color: #2d2d2d !important;
- color: var(--dark-color);
-}
-
-.dark-theme p {
- color: #e0e0e0;
-}
-
-/* Custom scrollbar */
-::-webkit-scrollbar {
- width: 8px;
-}
-
-::-webkit-scrollbar-track {
- background: #f1f1f1;
-}
-
-::-webkit-scrollbar-thumb {
- background: var(--primary-color);
- border-radius: 10px;
-}
-
-::-webkit-scrollbar-thumb:hover {
- background: var(--secondary-color);
-}
-
-/* ===== PROJECTS SECTION STYLES ===== */
-.projects-grid {
- display: grid;
- grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
- align-items: start;
- gap: 2rem;
- margin: 2rem 0;
- padding: 0 1rem;
-}
-
-.project-card {
- display: flex;
- flex-direction: column;
- justify-content: space-between;
- background: white;
- border-radius: var(--border-radius);
- box-shadow: 0 4px 15px rgba(0, 0, 0, 0.08);
- overflow: hidden;
- transition: all var(--transition-speed) ease;
- border: 1px solid rgba(0, 0, 0, 0.05);
- position: relative;
-}
-
-.project-card::before {
- content: '';
- position: absolute;
- top: 0;
- left: 0;
- right: 0;
- height: 3px;
- background: var(--gradient-header);
- transform: scaleX(0);
- transition: transform var(--transition-speed) ease;
- transform-origin: left;
-}
-
-.project-card:hover::before {
- transform: scaleX(1);
-}
-
-.project-card:hover {
- transform: translateY(-8px);
- box-shadow: 0 12px 30px rgba(0, 0, 0, 0.15);
-}
-
-.project-card-header {
- background: var(--gradient-header);
- background-size: 200% 200%;
- animation: gradientAnimation 15s ease infinite;
- color: white;
- padding: 1.5rem;
- text-align: center;
- min-height: 120px;
- display: flex;
- align-items: center;
- justify-content: center;
- position: relative;
- overflow: hidden;
-}
-
-.project-card-header::after {
- content: '';
- position: absolute;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- background: rgba(0, 0, 0, 0.1);
- opacity: 0;
- transition: opacity var(--transition-speed) ease;
-}
-
-.project-card:hover .project-card-header::after {
- opacity: 1;
-}
-
-.project-card-header h3 {
- font-size: 1.25rem;
- font-weight: 600;
- margin: 0;
- z-index: 1;
- position: relative;
-}
-
-.project-card-content {
- padding: 1.5rem;
-}
-
-.project-card-content h4 {
- color: var(--dark-color);
- font-size: 1.1rem;
- font-weight: 600;
- margin-bottom: 0.75rem;
- line-height: 1.3;
-}
-
-.project-card-content p {
- color: var(--gray-color);
- font-size: 0.9rem;
- line-height: 1.6;
- margin-bottom: 0.75rem;
-}
-
-.project-card-content p:last-of-type {
- margin-bottom: 1.25rem;
-}
-
-.project-card-content strong {
- color: var(--dark-color);
- font-weight: 600;
-}
-
-.project-card-content .btn {
- background: var(--primary-color);
- color: white;
- padding: 0.75rem 1.5rem;
- text-decoration: none;
- border-radius: var(--border-radius);
- font-weight: 500;
- font-size: 0.9rem;
- display: inline-block;
- transition: all var(--transition-speed) ease;
- border: none;
- cursor: pointer;
- text-transform: uppercase;
- letter-spacing: 0.5px;
-}
-
-.project-card-content .btn:hover {
- background: var(--secondary-color);
- transform: translateY(-2px);
- box-shadow: 0 4px 12px rgba(58, 134, 255, 0.3);
-}
-
-.card-tags {
- display: flex;
- flex-wrap: wrap;
- gap: 0.5rem;
- margin: 2rem;
-}
-
-span.tag {
- background: rgba(102, 126, 234, 0.1);
- color: #667eea;
- padding: 0.5rem 1rem;
- border-radius: 12px;
- font-size: 0.9rem;
- font-weight: 600;
- transition: background-color 0.3s ease, transform 0.3s ease;
-}
-
-/* Dark theme support for project cards */
-.dark-theme .project-card {
- background: #2d2d2d;
- border-color: rgba(255, 255, 255, 0.1);
- box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
-}
-
-.dark-theme .project-card:hover {
- box-shadow: 0 12px 30px rgba(0, 0, 0, 0.4);
-}
-
-.dark-theme .project-card-content h4 {
- color: #e0e0e0;
-}
-
-.dark-theme .project-card-content p {
- color: #b0b0b0;
-}
-
-.dark-theme .project-card-content strong {
- color: #e0e0e0;
-}
-
-/* Responsive design for project cards */
-@media (max-width: 768px) {
- .projects-grid {
- grid-template-columns: 1fr;
- gap: 1.5rem;
- margin: 1.5rem 0;
- }
-
- .project-card-header {
- padding: 1.25rem;
- min-height: 100px;
- }
-
- .project-card-header h3 {
- font-size: 1.1rem;
- }
-
- .project-card-content {
- padding: 1.25rem;
- }
-
- .project-card-content .btn {
- width: 100%;
- text-align: center;
- }
-}
-
-@media (max-width: 480px) {
- .projects-grid {
- padding: 0 0.5rem;
- gap: 1rem;
- }
-
- .project-card-content {
- padding: 1rem;
- }
-}
\ No newline at end of file
diff --git a/Static/img/bg-login.jpg b/Static/img/bg-login.jpg
deleted file mode 100644
index af09156..0000000
Binary files a/Static/img/bg-login.jpg and /dev/null differ
diff --git a/Static/js/admin.js b/Static/js/admin.js
deleted file mode 100644
index bf28554..0000000
--- a/Static/js/admin.js
+++ /dev/null
@@ -1,135 +0,0 @@
-import { readJson } from './readJson.js';
-
-document.addEventListener('DOMContentLoaded', async function() {
- const loadProjects = async () => {
- try {
- const data = await readJson('../data/projects.json');
- return data;
- } catch (error) {
- console.error('Erreur lors du chargement des projets :', error);
- return [];
- }
- }
-
- // ===* ACTIONS SUR LA PAGE *===
- const section = document.querySelector('section');
- const divProjectsGrid = document.querySelector('.projects-grid');
- const createProjectButton = document.querySelector('[data-id="creation-project-btn"]');
- const divCreateProject = document.querySelector('.form-project');
- const sectionProjects = document.querySelector('[data-id="projects"]');
-
- if (divProjectsGrid) {
- const data = await loadProjects();
-
- if (data) {
- const projects = Object.values(data);
-
- console.log('Projets :', projects);
- projects.forEach(project => {
- let state = 'Actif';
- if (!project.active) {
- state = 'Inactif';
- } else {
- state = 'Actif';
- }
-
- const projectDiv = document.createElement('div');
-
- projectDiv.className = 'card-projects';
- projectDiv.innerHTML = `
-
-
${project.type}
-
-
- Supprimer
- Modifier
-
- Etat : ${state}
- ${project.name}
- ${project.description}
- ${project.technologies && project.technologies.length > 0 ? `
-
- ${project.technologies.map(tech => `${tech} `).join('')}
-
` : ''}
- ${project.link ? `Voir le projet ` : ''}
- `;
- divProjectsGrid.appendChild(projectDiv);
-
- const deleteButton = projectDiv.querySelector('[data-id="delete-project"]');
- const editButton = projectDiv.querySelector('[data-id="edit-project"]');
-
- deleteButton.addEventListener('click', async () => {
- const confirmDelete = confirm(`Êtes-vous sûr de vouloir supprimer le projet "${project.name}" ?`);
- if (confirmDelete) {
- try {
- window.location.href = `./?page=delete-project&id=${project.id}`;
- } catch (error) {
- console.error('Erreur lors de la suppression du projet :', error);
- }
- }
- });
-
- editButton.addEventListener('click', () => {
- const projectId = editButton.getAttribute('data-project-id');
- const projectToEdit = projects.find(p => p.id === projectId);
-
- if (projectToEdit) {
- // Remplir le formulaire avec les données du projet à modifier
- document.getElementById('project-type').value = projectToEdit.type || '';
- document.getElementById('project-name').value = projectToEdit.name || '';
- document.getElementById('project-description').value = projectToEdit.description || '';
- document.getElementById('project-image').value = projectToEdit.image || '';
- document.getElementById('project-start-date').value = projectToEdit.start_date || '';
- document.getElementById('project-end-date').value = projectToEdit.end_date || '';
-
- // Pré-sélectionner les technologies si disponibles
- if (projectToEdit.technologies && window.technologiesManager) {
- window.technologiesManager.preselectTechnologies(projectToEdit.technologies);
- }
-
- // Afficher le formulaire et faire défiler vers celui-ci
- divCreateProject.classList.remove('hidden');
- createProjectButton.textContent = 'Fermer le formulaire';
- createProjectButton.classList.remove('btn-success');
- createProjectButton.classList.add('btn-danger');
-
- // Scroll vers le formulaire
- divCreateProject.scrollIntoView({ behavior: 'smooth' });
- }
- });
- });
- }
-
-
- }
-
- if(createProjectButton) {
- console.log("Create project button found");
- createProjectButton.addEventListener('click', () => {
- divCreateProject.classList.toggle('hidden');
- if( divCreateProject.classList.contains('hidden')) {
- createProjectButton.textContent = 'Ajouter projet';
- createProjectButton.classList.remove('btn-danger');
- createProjectButton.classList.add('btn-success');
-
- // Réinitialiser le formulaire
- const form = divCreateProject.querySelector('form');
- if (form) {
- form.reset();
- // Réinitialiser aussi les technologies
- if (window.technologiesManager) {
- window.technologiesManager.clearAllSelections();
- }
- }
- }
- else {
- createProjectButton.textContent = 'Fermer le formulaire';
- createProjectButton.classList.remove('btn-success');
- createProjectButton.classList.add('btn-danger');
-
- // Scroll vers le formulaire
- divCreateProject.scrollIntoView({ behavior: 'smooth' });
- }
- })
- }
-});
\ No newline at end of file
diff --git a/Static/js/main.js b/Static/js/main.js
deleted file mode 100644
index 8d9e101..0000000
--- a/Static/js/main.js
+++ /dev/null
@@ -1,301 +0,0 @@
-import { readJson } from './readJson.js';
-
-document.addEventListener('DOMContentLoaded', async function() {
- const navbar = document.querySelector("nav");
- const header = document.querySelector("header");
- let pagePath = "";
-
- const linkInterceptor = () => {
- const a = document.querySelectorAll('a');
-
- a.forEach(function(link) {
- if( link.getAttribute('data-page') != "externe") {
- link.addEventListener('click', function(event) {
- event.preventDefault();
- const href = link.getAttribute('data-page');
- const id = link.getAttribute('id');
- if (!href) {
- console.warn('Attribut data-page introuvable sur la balise:', link);
- return;
- }
- if(id != "admin") {
- loadPage(`./Views/${href}.html`);
- } else {
- window.location.href = href;
- }
- });
- }
- });
- }
-
- const loadPage = async (page) => {
- const mainContent = document.querySelector('.main-content');
-
- const loadProjects = async () => {
- try {
- const data = await readJson('./data/projects.json');
- return data;
- } catch (error) {
- console.error('Erreur lors du chargement des projets :', error);
- return [];
- }
- }
-
- const loadDataContacts = async () => {
- try {
- const data = await readJson('./data/contacts.json');
- return data;
- } catch (error) {
- console.error('Erreur lors du chargement des projets :', error);
- return [];
- }
- }
-
- if (!mainContent) {
- console.error('Élément .main-content introuvable dans le document.');
- return;
- }
- if(mainContent) {
- fetch(page)
- .then(response => {
- if(!response.ok) {
- throw new Error(`Erreur lors du chargement de la page : ${response.statusText}`);
- }
- pagePath = page;
- return response.text();
- })
- .then(async (html) => {
- mainContent.innerHTML = html;
- linkInterceptor();
-
- const divProjectsGrid = document.querySelector('.projects-grid');
- if (divProjectsGrid) {
- console.log("Projects grid found, loading projects...");
- const data = await loadProjects();
-
- if (data) {
- const projects = Object.values(data);
-
- console.log('Projets :', projects);
- projects.forEach(project => {
- if (project.active) {
- const projectDiv = document.createElement('div');
- projectDiv.className = 'project-card';
- projectDiv.innerHTML = `
-
-
-
${project.name}
-
${project.description}
- ${project.link ? `
Voir le projet ` : ''}
-
-
- ${project.technologies && project.technologies.length > 0 ?
- project.technologies.map(tech => `${tech} `).join('') : ''}
-
-
- `;
- divProjectsGrid.appendChild(projectDiv);
- }
- });
- }
- }
-
- const contactPage = document.querySelector('.contact-page');
- if (contactPage) {
- console.log("Contact page found, loading contact data...");
- const data = await loadDataContacts();
-
- if (data) {
- const email = document.querySelector('[data-id="email"]');
- const phone = document.querySelector('[data-id="phone"]');
- const github = document.querySelector('[data-id="github"]');
- const linkedin = document.querySelector('[data-id="linkedin"]');
- const twitter = document.querySelector('[data-id="twitter"]');
-
- email.textContent = data.email;
- email.href = `mailto:${data.email}`;
-
- phone.textContent = data.gsm;
- phone.href = `tel:${data.gsm}`;
- github.href = data.github;
- linkedin.href = data.linkedin;
- twitter.href = data.twitter;
- }
- }
- });
- }
- }
-
- // Animation pour l'en-tête au défilement
- window.addEventListener('scroll', () => {
- if (window.scrollY > 50) {
- navbar.classList.add('scrolled');
- } else {
- navbar.classList.remove('scrolled');
- }
- });
-
- // Ajout de la détection du mode sombre
- const prefersDarkScheme = window.matchMedia("(prefers-color-scheme: dark)");
- if (prefersDarkScheme.matches) {
- document.body.classList.add("dark-theme");
- }
-
- // Gestion des formulaires
- document.addEventListener('submit', (e) => {
- const form = e.target.closest('form');
- if (form) {
- e.preventDefault();
- // Simuler l'envoi du formulaire
- const submitBtn = form.querySelector('button[type="submit"]');
- if (submitBtn) {
- const originalText = submitBtn.textContent;
- submitBtn.textContent = 'Envoi en cours...';
- submitBtn.disabled = true;
-
- setTimeout(() => {
- form.innerHTML = `
-
Message envoyé avec succès!
-
Merci pour votre message. Je vous répondrai dans les plus brefs délais.
-
`;
- }, 1500);
- }
- }
- });
-
- // Animation des particules dans le header
- const createParticles = () => {
- const particlesContainer = document.getElementById('particles');
- if (!particlesContainer) return;
-
- // Nombre de particules
- const particleCount = 30;
-
- // Supprimer les particules existantes
- particlesContainer.innerHTML = '';
-
- // Créer de nouvelles particules
- for (let i = 0; i < particleCount; i++) {
- const particle = document.createElement('span');
- particle.classList.add('particle');
-
- // Attributs aléatoires pour chaque particule
- const size = Math.random() * 15 + 5;
- const posX = Math.random() * 100;
- const posY = Math.random() * 100;
- const delay = Math.random() * 5;
- const duration = Math.random() * 5 + 5;
-
- particle.style.width = `${size}px`;
- particle.style.height = `${size}px`;
- particle.style.left = `${posX}%`;
- particle.style.top = `${posY}%`;
- particle.style.animation = `float ${duration}s infinite ${delay}s`;
- particle.style.opacity = Math.random() * 0.5 + 0.3;
-
- particlesContainer.appendChild(particle);
- }
- };
-
- // Effet d'écriture au clavier
- const initTypeWriter = () => {
- const textElement = document.getElementById('typing-text');
- if (!textElement) return;
-
- const phrases = [
- "Développeur Web & Applications",
- "Développeur Python",
- "Développeur JavaScript",
- "Développeur C# & Unity",
- "Développeur EmberJS",
- "Développeur Typescript",
- "Développeur Angular",
- ];
-
- let phraseIndex = 0;
- let charIndex = 0;
- let isDeleting = false;
- let typingSpeed = 100;
- let isWaiting = false;
-
- function typeWriter() {
- if (isWaiting) {
- setTimeout(typeWriter, typingSpeed);
- isWaiting = false;
- return;
- }
-
- const currentPhrase = phrases[phraseIndex];
-
- if (isDeleting) {
- // Effacer le texte
- textElement.textContent = currentPhrase.substring(0, charIndex - 1);
- charIndex--;
- typingSpeed = 30; // Plus rapide pour effacer
- } else {
- // Écrire le texte
- textElement.textContent = currentPhrase.substring(0, charIndex + 1);
- charIndex++;
-
- // Variation aléatoire de la vitesse pour un effet plus naturel
- typingSpeed = Math.random() * 50 + 80;
- }
-
- // Si toute la phrase est écrite
- if (!isDeleting && charIndex === currentPhrase.length) {
- // Pause avant d'effacer
- isDeleting = true;
- typingSpeed = 2000; // Pause plus longue
- isWaiting = true;
- }
-
- // Si la phrase est effacée
- if (isDeleting && charIndex === 0) {
- isDeleting = false;
- phraseIndex = (phraseIndex + 1) % phrases.length;
- typingSpeed = 700; // Pause avant la prochaine phrase
- isWaiting = true;
- }
-
- setTimeout(typeWriter, typingSpeed);
- }
-
- typeWriter();
-
- // Effet d'apparition du curseur
- const cursor = document.querySelector('.cursor');
- if (cursor) {
- cursor.style.animation = 'blink 1s step-end infinite';
- }
- };
-
- // Effet de défilement doux lorsqu'on clique sur le header
- if (header) {
- header.addEventListener('click', () => {
- // Défilement vers la section suivante
- const nextSection = document.querySelector('nav');
- if (nextSection) {
- window.scrollTo({
- top: nextSection.offsetTop,
- behavior: 'smooth'
- });
- }
- });
-
- // Effet visuel au survol pour indiquer que le header est cliquable
- header.style.cursor = 'pointer';
- header.addEventListener('mouseenter', () => {
- header.style.transform = 'scale(1.01)';
- });
- header.addEventListener('mouseleave', () => {
- header.style.transform = 'scale(1)';
- });
- }
-
- // Initialiser les particules et l'effet d'écriture
- createParticles();
- initTypeWriter();
- loadPage('Views/home.html');
-});
\ No newline at end of file
diff --git a/Static/js/readJson.js b/Static/js/readJson.js
deleted file mode 100644
index c8d54b7..0000000
--- a/Static/js/readJson.js
+++ /dev/null
@@ -1,11 +0,0 @@
-const readJson = (filePath) => {
- return fetch(filePath)
- .then(response => {
- if(!response.ok) {
- throw new Error(`Erreur lors du chargement du fichier JSON : ${response.statusText}`);
- }
- return response.json();
- })
-};
-
-export { readJson };
\ No newline at end of file
diff --git a/Static/js/technologies.js b/Static/js/technologies.js
deleted file mode 100644
index 3782129..0000000
--- a/Static/js/technologies.js
+++ /dev/null
@@ -1,315 +0,0 @@
-// Gestionnaire des technologies pour les projets
-class TechnologiesManager {
- constructor() {
- this.technologies = {
- 'frontend': {
- label: 'Frontend Web',
- items: [
- 'HTML', 'CSS', 'JavaScript', 'TypeScript', 'React', 'Vue.js',
- 'Angular', 'Svelte', 'Next.js', 'Nuxt.js', 'Sass', 'Tailwind CSS', 'Bootstrap'
- ]
- },
- 'backend': {
- label: 'Backend Web',
- items: [
- 'PHP', 'Laravel', 'Symfony', 'Node.js', 'Express.js', 'Python',
- 'Django', 'Flask', 'FastAPI', 'Ruby', 'Ruby on Rails', 'Go', 'Rust'
- ]
- },
- 'languages': {
- label: 'Langages de Programmation',
- items: [
- 'Java', 'Spring', 'C#', '.NET', 'C++', 'C', 'Kotlin', 'Swift',
- 'Dart', 'Scala', 'R', 'MATLAB'
- ]
- },
- 'databases': {
- label: 'Bases de Données',
- items: [
- 'MySQL', 'PostgreSQL', 'MongoDB', 'Redis', 'SQLite', 'Elasticsearch',
- 'MariaDB', 'Oracle', 'Firebase', 'Supabase'
- ]
- },
- 'mobile': {
- label: 'Développement Mobile',
- items: [
- 'React Native', 'Flutter', 'Ionic', 'Xamarin', 'Apache Cordova', 'Android', 'iOS'
- ]
- },
- 'games': {
- label: 'Développement de Jeux',
- items: [
- 'Unity', 'Unreal Engine', 'Godot', 'Phaser', 'Three.js', 'Babylon.js',
- 'PixiJS', 'Construct 3', 'GameMaker Studio', 'RPG Maker'
- ]
- },
- 'devops': {
- label: 'DevOps & Cloud',
- items: [
- 'Docker', 'Kubernetes', 'AWS', 'Azure', 'Google Cloud', 'Heroku',
- 'Vercel', 'Netlify', 'Jenkins', 'GitLab CI', 'GitHub Actions', 'Terraform'
- ]
- },
- 'tools': {
- label: 'Outils & Frameworks',
- items: [
- 'Git', 'GitHub', 'Webpack', 'Vite', 'Babel', 'ESLint', 'Prettier',
- 'Jest', 'Cypress', 'Storybook'
- ]
- },
- 'design': {
- label: 'Design & Création',
- items: [
- 'Figma', 'Sketch', 'Adobe XD', 'Photoshop', 'Illustrator', 'Canva',
- 'Blender', 'Maya', '3ds Max'
- ]
- }
- };
-
- this.selectedTechs = new Set();
- this.init();
- }
-
- init() {
- this.render();
- this.attachEvents();
- this.addSearchFunctionality();
- this.addQuickActions();
- }
-
- generateId(tech) {
- return 'tag-' + tech.toLowerCase().replace(/[^a-z0-9]/g, '-');
- }
-
- render() {
- const container = document.querySelector('.technologies-grid');
- if (!container) return;
-
- let html = `
-
-
-
- 🔍
-
-
- Tout sélectionner
- Tout désélectionner
- Replier/Déplier
-
-
- 0 technologie(s) sélectionnée(s)
-
-
-
- `;
-
- for (const [categoryKey, category] of Object.entries(this.technologies)) {
- html += `
-
-
-
- `;
-
- category.items.forEach(tech => {
- const id = this.generateId(tech);
- html += `
-
-
- ${tech}
-
- `;
- });
-
- html += `
-
-
- `;
- }
-
- html += '
';
- container.innerHTML = html;
- }
-
- attachEvents() {
- // Événements pour les checkboxes
- document.querySelectorAll('.tech-checkbox').forEach(checkbox => {
- checkbox.addEventListener('change', (e) => {
- if (e.target.checked) {
- this.selectedTechs.add(e.target.value);
- } else {
- this.selectedTechs.delete(e.target.value);
- }
- this.updateSelectedCount();
- this.updateHiddenField();
- });
- });
-
- // Événements pour replier/déplier les catégories
- document.querySelectorAll('.category-header').forEach(header => {
- header.addEventListener('click', (e) => {
- const categoryKey = e.currentTarget.dataset.toggle;
- const categoryItems = document.getElementById(`category-${categoryKey}`);
- const toggle = e.currentTarget.querySelector('.category-toggle');
-
- if (categoryItems.style.display === 'none') {
- categoryItems.style.display = 'grid';
- toggle.textContent = '▼';
- } else {
- categoryItems.style.display = 'none';
- toggle.textContent = '▶';
- }
- });
- });
- }
-
- addSearchFunctionality() {
- const searchInput = document.getElementById('tech-search');
- if (!searchInput) return;
-
- searchInput.addEventListener('input', (e) => {
- const query = e.target.value.toLowerCase();
- const techItems = document.querySelectorAll('.tech-item');
-
- techItems.forEach(item => {
- const label = item.querySelector('.tech-label').textContent.toLowerCase();
- if (label.includes(query)) {
- item.style.display = 'flex';
- } else {
- item.style.display = 'none';
- }
- });
-
- // Masquer les catégories vides
- document.querySelectorAll('.tech-category').forEach(category => {
- const visibleItems = category.querySelectorAll('.tech-item[style*="flex"]');
- if (query && visibleItems.length === 0) {
- category.style.display = 'none';
- } else {
- category.style.display = 'block';
- }
- });
- });
- }
-
- addQuickActions() {
- const selectAllBtn = document.getElementById('select-all');
- const deselectAllBtn = document.getElementById('deselect-all');
- const toggleCategoriesBtn = document.getElementById('toggle-categories');
-
- if (selectAllBtn) {
- selectAllBtn.addEventListener('click', () => {
- document.querySelectorAll('.tech-checkbox').forEach(checkbox => {
- checkbox.checked = true;
- this.selectedTechs.add(checkbox.value);
- });
- this.updateSelectedCount();
- this.updateHiddenField();
- });
- }
-
- if (deselectAllBtn) {
- deselectAllBtn.addEventListener('click', () => {
- document.querySelectorAll('.tech-checkbox').forEach(checkbox => {
- checkbox.checked = false;
- });
- this.selectedTechs.clear();
- this.updateSelectedCount();
- this.updateHiddenField();
- });
- }
-
- if (toggleCategoriesBtn) {
- toggleCategoriesBtn.addEventListener('click', () => {
- const allCategories = document.querySelectorAll('.category-items');
- const allToggles = document.querySelectorAll('.category-toggle');
- const firstCategory = allCategories[0];
- const isCollapsed = firstCategory.style.display === 'none';
-
- allCategories.forEach(category => {
- category.style.display = isCollapsed ? 'grid' : 'none';
- });
-
- allToggles.forEach(toggle => {
- toggle.textContent = isCollapsed ? '▼' : '▶';
- });
- });
- }
- }
-
- updateSelectedCount() {
- const countDisplay = document.getElementById('count-display');
- if (countDisplay) {
- const count = this.selectedTechs.size;
- countDisplay.textContent = `${count} technologie(s) sélectionnée(s)`;
-
- // Changer la couleur selon le nombre
- if (count === 0) {
- countDisplay.className = 'count-empty';
- } else if (count <= 3) {
- countDisplay.className = 'count-low';
- } else if (count <= 6) {
- countDisplay.className = 'count-medium';
- } else {
- countDisplay.className = 'count-high';
- }
- }
- }
-
- // Méthode pour pré-sélectionner des technologies (utile pour l'édition)
- selectTechnologies(techArray) {
- this.selectedTechs.clear();
-
- techArray.forEach(tech => {
- this.selectedTechs.add(tech);
- const checkbox = document.querySelector(`input[value="${tech}"]`);
- if (checkbox) {
- checkbox.checked = true;
- }
- });
-
- this.updateSelectedCount();
- this.updateHiddenField();
- }
-
- // Alias pour la compatibilité
- preselectTechnologies(techArray) {
- this.selectTechnologies(techArray);
- }
-
- // Méthode pour effacer toutes les sélections
- clearAllSelections() {
- document.querySelectorAll('.tech-checkbox').forEach(checkbox => {
- checkbox.checked = false;
- });
- this.selectedTechs.clear();
- this.updateSelectedCount();
- this.updateHiddenField();
- }
-
- // Méthode pour mettre à jour le champ caché
- updateHiddenField() {
- const hiddenField = document.getElementById('selected-technologies');
- if (hiddenField) {
- hiddenField.value = Array.from(this.selectedTechs).join(',');
- }
- }
-
- // Méthode pour obtenir les technologies sélectionnées
- getSelectedTechnologies() {
- return Array.from(this.selectedTechs);
- }
-}
-
-// Initialiser le gestionnaire au chargement de la page
-document.addEventListener('DOMContentLoaded', () => {
- window.techManager = new TechnologiesManager();
-});
-
-// Export pour utilisation dans d'autres fichiers
-if (typeof module !== 'undefined' && module.exports) {
- module.exports = TechnologiesManager;
-}
diff --git a/Views/about.html b/Views/about.html
deleted file mode 100644
index 391e517..0000000
--- a/Views/about.html
+++ /dev/null
@@ -1,312 +0,0 @@
-
- À propos de moi
-
-
-
Je suis Anthony VIOLET, développeur web et designer freelance avec une passion pour la création d'expériences numériques innovantes.
-
Avec plusieurs années d'expérience dans le développement web, je combine compétences techniques et sensibilité esthétique pour créer des sites et applications qui se démarquent.
-
Mon approche du développement est centrée sur l'utilisateur, avec un focus sur la performance, l'accessibilité et l'esthétique.
-
-
-
Mon parcours
-
Diplômé en développement web et multimédia, j'ai travaillé sur divers projets allant de sites vitrines à des applications web complexes.
-
Ma curiosité et ma volonté d'apprentissage me poussent à me tenir constamment informé des dernières tendances et technologies du web.
-
-
-
-
-
- Mes valeurs
-
-
-
Qualité
-
Je m'engage à livrer un travail soigné et de haute qualité, respectant les standards du web et les bonnes pratiques.
-
-
-
Innovation
-
Je cherche constamment à explorer de nouvelles solutions et approches pour résoudre les défis de conception et de développement.
-
-
-
Communication
-
Je privilégie une communication claire et transparente tout au long du processus de création.
-
-
-
-
-
- Mes compétences
-
-
-
Python
-
-
-
-
JavaScript
-
-
-
-
PHP
-
-
-
-
C#
-
-
-
-
-
-
Langues
-
-
-
-
-
Espagnol
-
-
Intermédiaire
-
-
-
-
-
- Technologies utilisées
-
-
HTML5/CSS3
-
JavaScript
-
Python
-
PHP
-
SQL
-
Node.js
-
React
-
Angular
-
Pandas
-
NumPy
-
Symfony
-
Laravel
-
SciKit-Learn
-
-
-
-
- Contactez-moi
- Vous souhaitez discuter d'un projet ou simplement en savoir plus sur mon travail?
- Envoyez-moi un message
-
-
-
- Dernières expériences professionnelles
-
-
-
-
-
Stagiaire chez Tryptik
-
11/2024 - 12/2024
-
Développement application, maintien ambiance SCRUM, développement web.
-
- SCRUM
- Développement web
- Application
-
-
-
-
-
-
-
Collecte vendanges
-
2023
-
Récolte, ramassage et transport de nature.
-
- Travail d'équipe
- Rigueur
-
-
-
-
-
-
-
Assistant de vie
-
2023
-
Fourni des assistances aux personnes âgées dans leurs activités quotidiennes, en assurant le service des repas et en proposant des activités.
-
- Assistance
- Service
- Conseil
-
-
-
-
-
-
-
Divers métiers
-
2010 - 2017
-
Barman, boucher, préparateur de commandes, vendeur en magasin de matériaux.
-
- Polyvalence
- Adaptabilité
- Service client
-
-
-
-
-
-
- Formation
-
-
-
Développeur Gaming
-
Technocité - 2025
-
Après avoir approfondi mes connaissances en web et applications, ma curiosité m'a poussé à découvrir le développement dans le gaming. J'ai pu apréhender et maitriser le C# et des outils comme Unity..
-
Formé en tant que développeur gaming, j'ai acquis à maîtriser le C# ainsi que le moteur Unity.
-
-
-
Développeur Web orientée data analyste
-
Technofutur TIC - 2024
-
Après environ 15 ans d'apprentissage en autodidacte, j'ai pu approfondir et expérimenter mes connaissances dans le développement web (PHP, POO, CSS, HTML, SQL...).
-
Formé en tant que développeur web en data analyse, j'ai acquis à maîtriser JS/TS, Python (Pandas, NumPy, Nest.JS, Express, Angular, et Python (Pandas, NumPy, MatPlotLib...)).
-
-
-
Préparateur de commande en entrepôt
-
AFPA Dijon - France - 2010
-
-
-
-
-
- À propos
-
-
Après environ 15 ans d'apprentissage en autodidacte, j'ai pu approfondir et expérimenter mes connaissances dans le développement web (PHP, POO, CSS, HTML, SQL...).
-
Formé en tant que développeur web en data analyse, j'ai acquis à maîtriser JS/TS, Python (Pandas, NumPy, Nest.JS, Express, Angular, et Python (Pandas, NumPy, MatPlotLib...)).
-
-
-
-
- Centres d'intérêt
-
-
-
- 💻
-
-
Développement et Programmation
-
-
-
-
-
- 📚
-
-
Lecture de romans
-
-
-
\ No newline at end of file
diff --git a/Views/contact.html b/Views/contact.html
deleted file mode 100644
index 3ee097b..0000000
--- a/Views/contact.html
+++ /dev/null
@@ -1,58 +0,0 @@
-
- Contactez-moi
- Vous avez un projet en tête ou une question? N'hésitez pas à me contacter en utilisant le formulaire ci-dessous.
-
-
-
-
-
- Autres moyens de contact
-
-
-
-
Téléphone
-
+33 6 XX XX XX XX
-
-
-
-
-
-
- Disponibilité
- Je suis actuellement disponible pour des projets freelance à temps partiel.
- N'hésitez pas à me contacter pour discuter de votre projet et obtenir un devis personnalisé.
-
diff --git a/Views/home.html b/Views/home.html
deleted file mode 100644
index afb5e6e..0000000
--- a/Views/home.html
+++ /dev/null
@@ -1,69 +0,0 @@
-
-
-
- Mes compétences
-
-
-
Python
-
NumPy, Pandas, SciKit-Learn
-
-
-
-
JavaScript
-
Node.js, React/JS, Angular
-
-
-
-
PHP
-
Symfony, Laravel, OOP
-
-
-
-
-
-
-
- Services proposés
-
-
-
- 🌐
- Création de site web
-
-
Des sites web modernes, responsives et optimisés pour tous les appareils.
-
-
-
- 📊
- Analyse de données
-
-
Traitement et visualisation de données avec Python (NumPy, Pandas, SciKit-Learn).
-
-
-
- 💻
- Développement web
-
-
Applications web performantes avec JavaScript (Node.js, React, Angular) et PHP (Symfony).
-
-
-
-
-
- Contactez-moi
- Vous avez un projet en tête ou une question? N'hésitez pas à me contacter!
- Me contacter
-
\ No newline at end of file
diff --git a/Views/index.html b/Views/index.html
deleted file mode 100644
index e54bc08..0000000
--- a/Views/index.html
+++ /dev/null
@@ -1,17 +0,0 @@
-
- Admin
- Ravi de te revoir, = $admin["username"]; ?> !
-
-
- Espace d'administration protégé.
-
-
-
- Réglages généraux
- Les projets
- Infos contact
-
-
-
\ No newline at end of file
diff --git a/Views/projects.html b/Views/projects.html
deleted file mode 100644
index ec40a43..0000000
--- a/Views/projects.html
+++ /dev/null
@@ -1,19 +0,0 @@
-
- Mes projets
- Découvrez une sélection de mes réalisations récentes.
-
-
-
-
-
-
- Vous avez un projet en tête? N'hésitez pas à me contacter pour en discuter!
- Me contacter
-
diff --git a/admin/.DS_Store b/admin/.DS_Store
deleted file mode 100644
index 5cfda11..0000000
Binary files a/admin/.DS_Store and /dev/null differ
diff --git a/admin/SECURITY_README.md b/admin/SECURITY_README.md
deleted file mode 100644
index 0c6802a..0000000
--- a/admin/SECURITY_README.md
+++ /dev/null
@@ -1,83 +0,0 @@
-# Améliorations de Sécurité - Administration
-
-## 🔒 Corrections Apportées
-
-### 1. Authentification Sécurisée
-- ✅ **Hash des mots de passe** : Utilisation de `password_hash()` et `password_verify()`
-- ✅ **Protection CSRF** : Tokens CSRF sur tous les formulaires
-- ✅ **Limitation des tentatives** : Max 5 tentatives, blocage de 15 minutes
-- ✅ **Sessions sécurisées** : Configuration stricte des cookies de session
-- ✅ **Timeout de session** : Déconnexion automatique après 1 heure d'inactivité
-
-### 2. Validation et Sanitization
-- ✅ **Échappement HTML** : Protection contre XSS
-- ✅ **Validation des données** : Vérification des formats et longueurs
-- ✅ **Sanitization** : Nettoyage des entrées utilisateur
-
-### 3. Protection des Fichiers
-- ✅ **Fichiers sensibles cachés** : `.htaccess` pour protéger config.php
-- ✅ **Headers de sécurité** : X-Frame-Options, CSP, etc.
-- ✅ **Logs de tentatives** : Enregistrement des tentatives de connexion
-
-## 🚀 Installation
-
-### Étape 1 : Générer le hash du mot de passe
-1. Accédez à `admin/generate_password_hash.php`
-2. Entrez votre mot de passe actuel
-3. Copiez le hash généré
-4. **SUPPRIMEZ le fichier** `generate_password_hash.php`
-
-### Étape 2 : Configuration
-1. Ouvrez `admin/config.php`
-2. Remplacez `ADMIN_PASSWORD_HASH` par le hash généré
-3. Ajustez les autres paramètres si nécessaire
-
-### Étape 3 : Test
-1. Testez la connexion avec vos identifiants
-2. Vérifiez que les tentatives incorrectes sont bloquées
-3. Testez la déconnexion sécurisée
-
-## ⚙️ Configuration
-
-### Paramètres dans config.php
-```php
-define('ADMIN_PASSWORD_HASH', 'votre_hash_ici');
-define('SESSION_TIMEOUT', 3600); // 1 heure
-define('MAX_LOGIN_ATTEMPTS', 5); // 5 tentatives
-define('LOGIN_LOCKOUT_TIME', 900); // 15 minutes
-```
-
-## 🔐 Fonctionnalités de Sécurité
-
-### Protection contre les attaques par force brute
-- Limitation du nombre de tentatives par IP
-- Blocage temporaire après échec
-- Délai artificiel sur les tentatives échouées
-
-### Protection CSRF
-- Token unique par session
-- Vérification sur tous les formulaires sensibles
-- Régénération automatique des tokens
-
-### Gestion des sessions
-- Configuration sécurisée des cookies
-- Régénération périodique des IDs de session
-- Nettoyage complet lors de la déconnexion
-
-## 📁 Fichiers Modifiés
-
-- `admin/config.php` - Configuration de sécurité
-- `admin/login.php` - Authentification sécurisée
-- `admin/logout.php` - Déconnexion sécurisée
-- `admin/index.php` - Gestion des sessions
-- `admin/projects.php` - Protection CSRF et validation
-- `admin/includes/nav.php` - Déconnexion sécurisée
-- `admin/.htaccess` - Protection des fichiers
-- `admin/generate_password_hash.php` - Générateur de hash (à supprimer)
-
-## ⚠️ Important
-
-1. **Supprimez** `generate_password_hash.php` après utilisation
-2. **Sauvegardez** vos données avant mise en production
-3. **Testez** toutes les fonctionnalités après déploiement
-4. **Surveillez** les logs de tentatives de connexion
diff --git a/admin/config.php b/admin/config.php
deleted file mode 100644
index cb84669..0000000
--- a/admin/config.php
+++ /dev/null
@@ -1,119 +0,0 @@
- 300) { // 5 minutes
- session_regenerate_id(true);
- $_SESSION['last_regeneration'] = time();
- }
-}
-
-// Fonction pour vérifier les tentatives de connexion
-function checkLoginAttempts($ip) {
- $attempts_file = __DIR__ . '/login_attempts.json';
-
- if (!file_exists($attempts_file)) {
- return true;
- }
-
- $attempts_data = file_get_contents($attempts_file);
- $attempts = $attempts_data ? json_decode($attempts_data, true) : [];
-
- if (isset($attempts[$ip])) {
- $last_attempt = $attempts[$ip]['last_attempt'];
- $attempt_count = $attempts[$ip]['count'];
-
- // Si le délai de blocage est écoulé, réinitialiser
- if (time() - $last_attempt > LOGIN_LOCKOUT_TIME) {
- unset($attempts[$ip]);
- file_put_contents($attempts_file, json_encode($attempts));
- return true;
- }
-
- // Si trop de tentatives
- if ($attempt_count >= MAX_LOGIN_ATTEMPTS) {
- return false;
- }
- }
-
- return true;
-}
-
-// Fonction pour enregistrer une tentative de connexion
-function recordLoginAttempt($ip, $success = false) {
- $attempts_file = __DIR__ . '/login_attempts.json';
- $attempts_data = file_exists($attempts_file) ? file_get_contents($attempts_file) : '{}';
- $attempts = json_decode($attempts_data, true) ?: [];
-
- if ($success) {
- // Supprimer les tentatives en cas de succès
- if (isset($attempts[$ip])) {
- unset($attempts[$ip]);
- }
- } else {
- // Enregistrer la tentative échouée
- $attempts[$ip] = [
- 'count' => ($attempts[$ip]['count'] ?? 0) + 1,
- 'last_attempt' => time()
- ];
- }
-
- file_put_contents($attempts_file, json_encode($attempts));
-}
-
-// Générer un token CSRF
-function generateCSRFToken() {
- if (empty($_SESSION['csrf_token'])) {
- $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
- }
- return $_SESSION['csrf_token'];
-}
-
-// Vérifier le token CSRF
-function verifyCSRFToken($token) {
- return isset($_SESSION['csrf_token']) && hash_equals($_SESSION['csrf_token'], $token);
-}
-
-// Vérifier si l'utilisateur est connecté et la session est valide
-function isAuthenticated() {
- if (!isset($_SESSION['connected']) || !$_SESSION['connected']) {
- return false;
- }
-
- // Vérifier le timeout de session
- if (isset($_SESSION['last_activity']) && (time() - $_SESSION['last_activity'] > SESSION_TIMEOUT)) {
- session_destroy();
- return false;
- }
-
- // Mettre à jour l'activité
- $_SESSION['last_activity'] = time();
-
- return true;
-}
-
-// Nettoyer et valider les données d'entrée
-function sanitizeInput($data) {
- return htmlspecialchars(trim($data), ENT_QUOTES, 'UTF-8');
-}
-?>
diff --git a/admin/contact.php b/admin/contact.php
deleted file mode 100644
index 8c21337..0000000
--- a/admin/contact.php
+++ /dev/null
@@ -1,111 +0,0 @@
-Token de sécurité invalide.";
- } else {
- $firstname = sanitizeInput($_POST['firstname']);
- $lastname = sanitizeInput($_POST['lastname']);
- $email = sanitizeInput($_POST['email']);
- $gsm = sanitizeInput($_POST['gsm']);
- $linkedin = sanitizeInput($_POST['linkedin']);
- $twitter = sanitizeInput($_POST['twitter']);
- $github = sanitizeInput($_POST['github']);
- $message = 'Formulaire soumis
';
-
- $jsonFile = '../data/contacts.json';
- if(file_exists($jsonFile)) {
- $content = file_get_contents($jsonFile);
- $contact = $content;
-
- if($contact) {
- $contact = json_decode($content, true);
- }
-
- $updatedContact = [
- 'firstname' => $firstname,
- 'lastname' => $lastname,
- 'email' => $email,
- 'gsm' => $gsm,
- 'linkedin' => $linkedin,
- 'twitter' => $twitter,
- 'github' => $github
- ];
-
- $contact = array_merge($contact, $updatedContact);
- if (file_put_contents($jsonFile, json_encode($contact, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE))) {
- $message = 'Données de contact mises à jour avec succès !
';
- } else {
- $message = 'Erreur lors de la mise à jour des données de contact.
';
- }
- }
- }
-} else {
- // Charger les données de contact existantes
- $jsonFile = '../data/contacts.json';
- if(file_exists($jsonFile)) {
- $content = file_get_contents($jsonFile);
- if($content) {
- $contact = json_decode($content, true);
- $firstname = $contact['firstname'] ?? '';
- $lastname = $contact['lastname'] ?? '';
- $email = $contact['email'] ?? '';
- $gsm = $contact['gsm'] ?? '';
- $linkedin = $contact['linkedin'] ?? '';
- $twitter = $contact['twitter'] ?? '';
- $github = $contact['github'] ?? '';
- }
- }
-}
-
-?>
-
-
-
Données de contacts
-
Les données de contacts affichées ici sont reprise sur le site dans la rubrique contact
-
-
-
\ No newline at end of file
diff --git a/admin/delete-project.php b/admin/delete-project.php
deleted file mode 100644
index e9bca4b..0000000
--- a/admin/delete-project.php
+++ /dev/null
@@ -1,34 +0,0 @@
-ID de projet manquant.";
- exit;
-}
-
-$jsonFile = '../data/projects.json';
-
-if(file_exists($jsonFile)) {
- $content = file_get_contents($jsonFile);
- $projects = $content ? json_decode($content, true) : [];
-} else {
- $projects = [];
-}
-
-// Check if project exists
-$projectKey = array_search($id, array_column($projects, 'id'));
-if ($projectKey === false) {
- echo "Projet non trouvé.
";
- exit;
-}
-// On supprime le projet
-unset($projects[$projectKey]);
-// Reindex the array
-$projects = array_values($projects);
-// Save updated projects back to the JSON file
-if (file_put_contents($jsonFile, json_encode($projects, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE))) {
- echo "Projet supprimé avec succès !
";
-} else {
- echo "Erreur lors de la suppression du projet.
";
-}
-?>
\ No newline at end of file
diff --git a/admin/generate_password_hash.php b/admin/generate_password_hash.php
deleted file mode 100644
index db61b58..0000000
--- a/admin/generate_password_hash.php
+++ /dev/null
@@ -1,60 +0,0 @@
-Générateur de hash pour mot de passe";
-
-if ($_POST && isset($_POST['password'])) {
- $password = $_POST['password'];
- $hash = password_hash($password, PASSWORD_DEFAULT);
-
- echo "";
- echo "Hash généré : ";
- echo "" . htmlspecialchars($hash) . " ";
- echo "Copiez ce hash dans config.php à la place de ADMIN_PASSWORD_HASH ";
- echo "
";
-
- // Vérification
- if (password_verify($password, $hash)) {
- echo "✓ Vérification réussie - Le hash fonctionne correctement
";
- } else {
- echo "✗ Erreur de vérification
";
- }
-}
-?>
-
-
-
-
- Générateur de Hash
-
-
-
-
- ⚠️ ATTENTION : Supprimez ce fichier après utilisation pour des raisons de sécurité !
-
-
-
- Entrez votre mot de passe :
-
- Générer le hash
-
-
-
-
Instructions :
-
- Entrez votre mot de passe ci-dessus
- Copiez le hash généré
- Remplacez la valeur de ADMIN_PASSWORD_HASH dans config.php
- Supprimez ce fichier (generate_password_hash.php)
-
-
-
-
diff --git a/admin/home.php b/admin/home.php
deleted file mode 100644
index 947a8d1..0000000
--- a/admin/home.php
+++ /dev/null
@@ -1,61 +0,0 @@
-
-
-
-
Tableau de bord
-
Bienvenue dans l'espace d'administration de votre site personnel.
-
-
-
-
🎨 Projets
-
Gérez vos projets et réalisations
-
Accéder
-
-
-
-
📧 Contacts
-
Consultez les données de contact
-
Accéder
-
-
-
-
📊 Statistiques
-
-
- 5
- Projets actifs
-
-
- 12
- Messages reçus
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/admin/htaccess b/admin/htaccess
deleted file mode 100644
index c597c3d..0000000
--- a/admin/htaccess
+++ /dev/null
@@ -1,37 +0,0 @@
-# Sécurisation du dossier d'administration
-
-# Cacher les fichiers sensibles
-
- Require all denied
-
-
-
- Require all denied
-
-
-
- Require all denied
-
-
-# Protection contre les attaques par force brute
-
- Require all granted
- # Limiter les requêtes POST (optionnel, à configurer selon vos besoins)
-
-
-# Headers de sécurité
-
- Header always set X-Content-Type-Options nosniff
- Header always set X-Frame-Options DENY
- Header always set X-XSS-Protection "1; mode=block"
- Header always set Referrer-Policy "strict-origin-when-cross-origin"
- Header always set Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'"
-
-
-# Désactiver l'affichage des erreurs PHP en production
-php_flag display_errors off
-php_flag log_errors on
-
-# Limiter la taille des uploads
-php_value upload_max_filesize 10M
-php_value post_max_size 10M
diff --git a/admin/includes/nav.php b/admin/includes/nav.php
deleted file mode 100644
index d4175c3..0000000
--- a/admin/includes/nav.php
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
-
\ No newline at end of file
diff --git a/admin/index.php b/admin/index.php
deleted file mode 100644
index e9237a4..0000000
--- a/admin/index.php
+++ /dev/null
@@ -1,36 +0,0 @@
- $_SESSION['username']
- );
- $connected = "true";
-} else {
- $page = "login";
-}
-
-?>
-
-
-
-
-
-
-
- Administration
-
->
-
-
-
\ No newline at end of file
diff --git a/admin/init.php b/admin/init.php
deleted file mode 100644
index c5aad49..0000000
--- a/admin/init.php
+++ /dev/null
@@ -1,22 +0,0 @@
-
diff --git a/admin/login.php b/admin/login.php
deleted file mode 100644
index 43a5daa..0000000
--- a/admin/login.php
+++ /dev/null
@@ -1,61 +0,0 @@
-Trop de tentatives de connexion. Réessayez dans ' . ceil($remaining_time / 60) . ' minutes.';
-} else {
- // Traitement du formulaire de connexion
- if ($_POST && isset($_POST['username']) && isset($_POST['userPassword']) && isset($_POST['csrf_token'])) {
-
- // Vérifier le token CSRF
- if (!verifyCSRFToken($_POST['csrf_token'])) {
- $msg = 'Token de sécurité invalide. Veuillez réessayer. ';
- } else {
- $username = sanitizeInput($_POST['username']);
- $password = $_POST['userPassword'];
-
- // Vérifier les identifiants
- if ($username === ADMIN_USERNAME && password_verify($password, ADMIN_PASSWORD_HASH)) {
- // Connexion réussie
- session_regenerate_id(true);
- $_SESSION['connected'] = true;
- $_SESSION['username'] = $username;
- $_SESSION['last_activity'] = time();
-
- // Supprimer les tentatives de connexion en cas de succès
- recordLoginAttempt($client_ip, true);
-
- $msg = 'Connexion réussie ! Accéder à l\'administration ';
- } else {
- // Connexion échouée
- recordLoginAttempt($client_ip, false);
- $msg = 'Identifiants incorrects. ';
-
- // Ajouter un délai pour ralentir les attaques par force brute
- sleep(2);
- }
- }
- }
-}
-?>
-
\ No newline at end of file
diff --git a/admin/login_attempts.json b/admin/login_attempts.json
deleted file mode 100644
index 0637a08..0000000
--- a/admin/login_attempts.json
+++ /dev/null
@@ -1 +0,0 @@
-[]
\ No newline at end of file
diff --git a/admin/logout.php b/admin/logout.php
deleted file mode 100644
index e9396db..0000000
--- a/admin/logout.php
+++ /dev/null
@@ -1,27 +0,0 @@
-
\ No newline at end of file
diff --git a/admin/projects.php b/admin/projects.php
deleted file mode 100644
index 3a0ec2d..0000000
--- a/admin/projects.php
+++ /dev/null
@@ -1,126 +0,0 @@
-Token de sécurité invalide.";
- } else {
- // Handle form submission for project creation
- if(isset($_POST['name']) && isset($_POST['description']) && isset($_POST['start_date'])) {
- $type = sanitizeInput($_POST['type']);
- $name = sanitizeInput($_POST['name']);
- $description = sanitizeInput($_POST['description']);
- $start_date = sanitizeInput($_POST['start_date']);
- $end_date = isset($_POST['end_date']) && !empty($_POST['end_date']) ? sanitizeInput($_POST['end_date']) : null;
- $link = isset($_POST['link']) && !empty($_POST['link']) ? sanitizeInput($_POST['link']) : null;
- $technologies = isset($_POST['technologies']) && !empty($_POST['technologies']) ?
- array_map('trim', explode(',', sanitizeInput($_POST['technologies']))) : [];
- $tags = isset($_POST['tags']) && !empty($_POST['tags']) ?
- array_map('trim', explode(',', sanitizeInput($_POST['tags']))) : [];
-
- // Validation des données
- if (empty($type) || empty($name) || empty($description) || empty($start_date)) {
- echo "Tous les champs obligatoires doivent être remplis.
";
- } elseif (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $start_date)) {
- echo "Format de date de début invalide.
";
- } elseif ($end_date && !preg_match('/^\d{4}-\d{2}-\d{2}$/', $end_date)) {
- echo "Format de date de fin invalide.
";
- } else {
- $jsonFile = '../data/projects.json';
- // Read existing projects
- if(file_exists($jsonFile)) {
- $content = file_get_contents($jsonFile);
- $projects = $content ? json_decode($content, true) : [];
- } else {
- $projects = [];
- }
-
- // Create new project entry
- $newProject = [
- 'id' => uniqid(),
- 'type' => $type,
- 'name' => $name,
- 'description' => $description,
- 'link' => $link,
- 'technologies' => $technologies,
- 'start_date' => $start_date,
- 'end_date' => $end_date,
- 'created_at' => date('Y-m-d H:i:s'),
- 'active' => true
- ];
-
- // Add new project to the list
- $projects[] = $newProject;
-
- // Save updated projects back to the JSON file
- if (file_put_contents($jsonFile, json_encode($projects, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE))) {
- echo "Projet '$name' créé avec succès !
";
- // Reset form fields
- unset($_POST);
- } else {
- echo "Erreur lors de la sauvegarde du projet.
";
- }
- }
- }
- }
-}
-?>
-
-
-
\ No newline at end of file
diff --git a/backend/.idea/.gitignore b/backend/.idea/.gitignore
new file mode 100644
index 0000000..13566b8
--- /dev/null
+++ b/backend/.idea/.gitignore
@@ -0,0 +1,8 @@
+# Default ignored files
+/shelf/
+/workspace.xml
+# Editor-based HTTP Client requests
+/httpRequests/
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml
diff --git a/backend/.idea/backend.iml b/backend/.idea/backend.iml
new file mode 100644
index 0000000..33b0145
--- /dev/null
+++ b/backend/.idea/backend.iml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/backend/.idea/encodings.xml b/backend/.idea/encodings.xml
new file mode 100644
index 0000000..df87cf9
--- /dev/null
+++ b/backend/.idea/encodings.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/backend/.idea/inspectionProfiles/profiles_settings.xml b/backend/.idea/inspectionProfiles/profiles_settings.xml
new file mode 100644
index 0000000..105ce2d
--- /dev/null
+++ b/backend/.idea/inspectionProfiles/profiles_settings.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/backend/.idea/misc.xml b/backend/.idea/misc.xml
new file mode 100644
index 0000000..512a53e
--- /dev/null
+++ b/backend/.idea/misc.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/backend/.idea/modules.xml b/backend/.idea/modules.xml
new file mode 100644
index 0000000..e066844
--- /dev/null
+++ b/backend/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/backend/.idea/vcs.xml b/backend/.idea/vcs.xml
new file mode 100644
index 0000000..6c0b863
--- /dev/null
+++ b/backend/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/backend/__init__.py b/backend/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/backend/__pycache__/app.cpython-313.pyc b/backend/__pycache__/app.cpython-313.pyc
new file mode 100644
index 0000000..6277630
Binary files /dev/null and b/backend/__pycache__/app.cpython-313.pyc differ
diff --git a/backend/app.py b/backend/app.py
new file mode 100644
index 0000000..671cdfa
--- /dev/null
+++ b/backend/app.py
@@ -0,0 +1,32 @@
+from flask import Flask, jsonify
+from flask_cors import CORS
+
+from routes.projects import projects_bp
+from routes.contact import contact_bp
+from routes.cv import cv_bp
+from routes.services import services_bp
+
+from dotenv import load_dotenv
+import os
+
+load_dotenv()
+
+app = Flask(__name__)
+CORS(app)
+
+app.config["API_KEY"] = os.getenv("API_KEY")
+
+@app.route('/api/')
+@app.route('/')
+def get_home():
+ return jsonify({
+ "return": "Welcome to API"
+ })
+
+app.register_blueprint(projects_bp)
+app.register_blueprint(contact_bp)
+app.register_blueprint(cv_bp)
+app.register_blueprint(services_bp)
+
+if __name__ == '__main__':
+ app.run(debug=True, port=5000)
\ No newline at end of file
diff --git a/backend/data/contact.json b/backend/data/contact.json
new file mode 100644
index 0000000..391e087
--- /dev/null
+++ b/backend/data/contact.json
@@ -0,0 +1,9 @@
+{
+ "first_name": "Anthony",
+ "last_name": "VIOLET",
+ "email": "tutu@tutu.com",
+ "phone_number": "bah nope",
+ "linkedin": "tjrs pas... quoique",
+ "git": "non",
+ "web": "ok"
+}
\ No newline at end of file
diff --git a/backend/data/cv.json b/backend/data/cv.json
new file mode 100644
index 0000000..2b06630
--- /dev/null
+++ b/backend/data/cv.json
@@ -0,0 +1,36 @@
+[
+ {
+ "about_text": "Après environ 15 ans d'apprentissage en autodidacte, j'ai pu approfondir et expérimenter mes connaissances dans le développement web (PHP, POO, CSS, HTML, SQL...).",
+ "my_values": "la montagne, la neige et le caca",
+ "my_skills": [
+ {
+ "Python": [
+ "Numpy",
+ "Pandas"
+ ],
+ "Javascript": [
+ "NodeJS",
+ "React"
+ ]
+ }
+ ],
+ "technologies": [
+ "PHP",
+ "Python"
+ ],
+ "work_experience": [
+ {
+ "Vendanges": "lol"
+ }
+ ],
+ "education": [
+ {
+ "Technofutur": "rien"
+ }
+ ],
+ "hobbies": [
+ "manger",
+ "bouger"
+ ]
+ }
+]
\ No newline at end of file
diff --git a/backend/data/projects.json b/backend/data/projects.json
new file mode 100644
index 0000000..c9d30f3
--- /dev/null
+++ b/backend/data/projects.json
@@ -0,0 +1,34 @@
+[
+ {
+ "id": 1,
+ "name": "test",
+ "description": "test",
+ "technologies": "test",
+ "image": "test",
+ "url": "test",
+ "source": "test",
+ "created_at": 1758087635.903851,
+ "updated_at": 1758087635.903851,
+ "status": [
+ "En cours",
+ "Terminé",
+ "Futur projet"
+ ]
+ },
+ {
+ "id": 2,
+ "name": "Maitre pokemon",
+ "description": "je suis le maitre Pokemon avec les 8 badges en ma possesion",
+ "technologies": "IT-PokeMaster",
+ "image": "Masterball.png",
+ "url": "Masterball.png",
+ "source": "Pokepedia",
+ "created_at": 1758089969.696575,
+ "updated_at": 1758089969.696576,
+ "status": [
+ "En cours",
+ "Terminé",
+ "Futur projet"
+ ]
+ }
+]
\ No newline at end of file
diff --git a/backend/data/services.json b/backend/data/services.json
new file mode 100644
index 0000000..c0196f8
--- /dev/null
+++ b/backend/data/services.json
@@ -0,0 +1,14 @@
+[
+ {
+ "id": 1,
+ "name" : "Création de site web",
+ "icon": "🌐",
+ "description": "Des sites web modernes, responsives et optimisés pour tous les appareils."
+ },
+ {
+ "id": 2,
+ "name" : "Création de site web",
+ "icon": "🌐",
+ "description": "Des sites web modernes, responsives et optimisés pour tous les appareils."
+ }
+]
\ No newline at end of file
diff --git a/backend/models/__init__.py b/backend/models/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/backend/models/__pycache__/__init__.cpython-313.pyc b/backend/models/__pycache__/__init__.cpython-313.pyc
new file mode 100644
index 0000000..b9652b7
Binary files /dev/null and b/backend/models/__pycache__/__init__.cpython-313.pyc differ
diff --git a/backend/models/__pycache__/contact_model.cpython-313.pyc b/backend/models/__pycache__/contact_model.cpython-313.pyc
new file mode 100644
index 0000000..8b37f4f
Binary files /dev/null and b/backend/models/__pycache__/contact_model.cpython-313.pyc differ
diff --git a/backend/models/__pycache__/cv_model.cpython-313.pyc b/backend/models/__pycache__/cv_model.cpython-313.pyc
new file mode 100644
index 0000000..9da51ac
Binary files /dev/null and b/backend/models/__pycache__/cv_model.cpython-313.pyc differ
diff --git a/backend/models/__pycache__/project_model.cpython-313.pyc b/backend/models/__pycache__/project_model.cpython-313.pyc
new file mode 100644
index 0000000..2400ca4
Binary files /dev/null and b/backend/models/__pycache__/project_model.cpython-313.pyc differ
diff --git a/backend/models/__pycache__/service_model.cpython-313.pyc b/backend/models/__pycache__/service_model.cpython-313.pyc
new file mode 100644
index 0000000..8a17ed0
Binary files /dev/null and b/backend/models/__pycache__/service_model.cpython-313.pyc differ
diff --git a/backend/models/contact_model.py b/backend/models/contact_model.py
new file mode 100644
index 0000000..e8604d4
--- /dev/null
+++ b/backend/models/contact_model.py
@@ -0,0 +1,24 @@
+class ContactModel:
+ def __init__(self, data):
+ self.first_name = data.get('first_name')
+ self.last_name = data.get('last_name')
+ self.email = data.get('email')
+ self.phonenumber = data.get('phone_number')
+ self.linkedin = data.get('linkedin')
+ self.git = data.get('git')
+ self.web = data.get('web')
+
+
+ def is_valid(self):
+ return self.first_name is not None and self.last_name is not None and self.email is not None and self.phonenumber is not None
+
+ def to_dict(self):
+ return {
+ "first_name": self.first_name,
+ "last_name": self.last_name,
+ "email": self.email,
+ "phone_number": self.phonenumber,
+ "linkedin": self.linkedin,
+ "git": self.git,
+ "web": self.web,
+ }
\ No newline at end of file
diff --git a/backend/models/cv_model.py b/backend/models/cv_model.py
new file mode 100644
index 0000000..dd7e526
--- /dev/null
+++ b/backend/models/cv_model.py
@@ -0,0 +1,20 @@
+class CVModel:
+ def __init__(self, data):
+ self.about_text = data.get("about_text")
+ self.my_values = data.get("my_values")
+ self.my_skills = data.get("my_skills")
+ self.technologies = data.get("technologies")
+ self.work_experience = data.get("work_experience")
+ self.education = data.get("education")
+ self.hobbies = data.get("hobbies")
+
+ def to_dict(self):
+ return {
+ "about_text": self.about_text,
+ "my_values": self.my_values,
+ "my_skills": self.my_skills,
+ "technologies": self.technologies,
+ "work_experience": self.work_experience,
+ "education": self.education,
+ "hobbies": self.hobbies
+ }
diff --git a/backend/models/project_model.py b/backend/models/project_model.py
new file mode 100644
index 0000000..3614dd3
--- /dev/null
+++ b/backend/models/project_model.py
@@ -0,0 +1,31 @@
+import time
+from utils.enums import enumerates
+class ProjectModel:
+ def __init__(self, data):
+ self.id = data.get('id')
+ self.name = data.get('name')
+ self.description = data.get('description')
+ self.technologies = data.get('technologies')
+ self.image = data.get('image')
+ self.url = data.get('url')
+ self.source = data.get('source')
+ self.created_at = data.get('created_at')
+ self.updated_at = data.get('updated_at')
+ self.status = data.get('status')
+
+ def is_valid(self):
+ return self.name is not None
+
+ def to_dict(self):
+ return {
+ "id": self.id,
+ "name": self.name,
+ "description": self.description,
+ "technologies": self.technologies,
+ "image": self.image,
+ "url": self.url,
+ "source": self.source,
+ "created_at": time.time() if self.created_at is None else self.created_at,
+ "updated_at": time.time() if self.updated_at is None else self.updated_at,
+ "status": enumerates["status"],
+ }
\ No newline at end of file
diff --git a/backend/models/service_model.py b/backend/models/service_model.py
new file mode 100644
index 0000000..ff002ac
--- /dev/null
+++ b/backend/models/service_model.py
@@ -0,0 +1,14 @@
+class ServiceModel:
+ def __init__(self, data):
+ self.service_id = data.get('id')
+ self.service_name = data.get('name')
+ self.service_icon = data.get('icon')
+ self.service_description = data.get('description')
+
+ def to_dict(self):
+ return {
+ "id": self.service_id,
+ "name": self.service_name,
+ "icon": self.service_icon,
+ "description": self.service_description
+ }
\ No newline at end of file
diff --git a/backend/requirements.txt b/backend/requirements.txt
new file mode 100644
index 0000000..3cc53e7
--- /dev/null
+++ b/backend/requirements.txt
@@ -0,0 +1,3 @@
+flask
+flask-cors
+dotenv
\ No newline at end of file
diff --git a/backend/routes/__init__.py b/backend/routes/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/backend/routes/__pycache__/__init__.cpython-313.pyc b/backend/routes/__pycache__/__init__.cpython-313.pyc
new file mode 100644
index 0000000..6b83457
Binary files /dev/null and b/backend/routes/__pycache__/__init__.cpython-313.pyc differ
diff --git a/backend/routes/__pycache__/contact.cpython-313.pyc b/backend/routes/__pycache__/contact.cpython-313.pyc
new file mode 100644
index 0000000..3fa46ef
Binary files /dev/null and b/backend/routes/__pycache__/contact.cpython-313.pyc differ
diff --git a/backend/routes/__pycache__/cv.cpython-313.pyc b/backend/routes/__pycache__/cv.cpython-313.pyc
new file mode 100644
index 0000000..4116027
Binary files /dev/null and b/backend/routes/__pycache__/cv.cpython-313.pyc differ
diff --git a/backend/routes/__pycache__/projects.cpython-313.pyc b/backend/routes/__pycache__/projects.cpython-313.pyc
new file mode 100644
index 0000000..e87a866
Binary files /dev/null and b/backend/routes/__pycache__/projects.cpython-313.pyc differ
diff --git a/backend/routes/__pycache__/services.cpython-313.pyc b/backend/routes/__pycache__/services.cpython-313.pyc
new file mode 100644
index 0000000..73fd6af
Binary files /dev/null and b/backend/routes/__pycache__/services.cpython-313.pyc differ
diff --git a/backend/routes/contact.py b/backend/routes/contact.py
new file mode 100644
index 0000000..bc5a0cd
--- /dev/null
+++ b/backend/routes/contact.py
@@ -0,0 +1,29 @@
+from flask import Blueprint, jsonify, request, current_app
+from utils.json_crud import load_json, save_json, add_entry, delete_entry, update_entry
+from utils.data_loader import load_data
+from models.contact_model import ContactModel
+import os
+
+DATA_DIR = os.path.join(os.path.dirname(__file__), '../data')
+CONTACTS_FILE = os.path.join(DATA_DIR, 'contact.json')
+
+contact_bp = Blueprint('contact', __name__, url_prefix='/api/contact')
+
+@contact_bp.route('/', methods=['GET'])
+def get_contact():
+ contacts = load_data('contact.json')
+ return jsonify(contacts)
+
+@contact_bp.route('/', methods=['POST'])
+def update_contact():
+ key = request.headers.get('x-api-key')
+ if key != current_app.config['API_KEY']:
+ return jsonify({
+ "error": "Unauthorized"
+ }), 401
+
+ data = request.json
+ new_entry = ContactModel(data).to_dict()
+ contact = [new_entry]
+ save_json(CONTACTS_FILE, contact)
+ return new_entry
\ No newline at end of file
diff --git a/backend/routes/cv.py b/backend/routes/cv.py
new file mode 100644
index 0000000..ea0e7a7
--- /dev/null
+++ b/backend/routes/cv.py
@@ -0,0 +1,41 @@
+from flask import Blueprint, jsonify, request, current_app
+from utils.data_loader import load_data
+from utils.json_crud import load_json, save_json, add_entry, delete_entry, update_entry
+from models.cv_model import CVModel
+import os
+
+DATA_DIR = os.path.join(os.path.dirname(__file__), '../data')
+CV_FILE = os.path.join(DATA_DIR, 'cv.json')
+
+cv_bp = Blueprint('cv', __name__, url_prefix='/api/cv')
+
+@cv_bp.route('/', methods=['GET'])
+def get_cv():
+ cv = load_data('cv.json')
+ return jsonify(cv)
+
+@cv_bp.route('/', methods=['POST'])
+def update_cv():
+ #key = request.headers.get('x-api-key')
+ #if key != current_app.config['API_KEY']:
+ # return jsonify({
+ # "error": "Unauthorized"
+ # }), 401
+
+ data = request.json
+ new_entry = CVModel(data).to_dict()
+ cv = [new_entry]
+ save_json(CV_FILE, cv)
+ return jsonify(new_entry), 201
+
+@cv_bp.route('/skills/', methods=['GET'])
+def get_skills():
+ cv = load_data('cv.json')
+ skills = cv[0].get('my_skills')
+ return jsonify(skills)
+
+@cv_bp.route('/about/', methods=['GET'])
+def get_about():
+ cv = load_data('cv.json')
+ about = cv[0].get('about_text')
+ return jsonify(about)
\ No newline at end of file
diff --git a/backend/routes/projects.py b/backend/routes/projects.py
new file mode 100644
index 0000000..016f8c1
--- /dev/null
+++ b/backend/routes/projects.py
@@ -0,0 +1,48 @@
+from flask import Blueprint, jsonify, request, current_app
+from utils.data_loader import load_data
+from utils.json_crud import load_json, save_json, add_entry, delete_entry, update_entry
+from models.project_model import ProjectModel
+import os
+
+DATA_DIR = os.path.join(os.path.dirname(__file__), '../data')
+PROJECTS_FILE = os.path.join(DATA_DIR, 'projects.json')
+
+projects_bp = Blueprint('projects', __name__, url_prefix='/api/projects')
+
+@projects_bp.route('/', methods=['GET'])
+def get_projects():
+ projects = load_data('projects.json')
+ return jsonify(projects)
+
+@projects_bp.route('/', methods=['GET'])
+def get_project(project_id):
+ projects = load_data('projects.json')
+ project = next((p for p in projects if p.get('id') == project_id), None)
+ print(project)
+ return jsonify(project)
+
+@projects_bp.route('/', methods=['POST'])
+def create_project():
+ key = request.headers.get('x-api-key')
+
+ if key != current_app.config['API_KEY']:
+ return jsonify({
+ "error": "Unauthorized"
+ }), 401
+ projects = load_data('projects.json')
+ entry = request.json
+
+ project = ProjectModel(entry)
+
+ if not project.is_valid():
+ return jsonify({
+ "error": "Invalid project data"
+ }), 400
+
+ new_id = max((p.get('id') for p in projects), default=0) + 1
+
+ data = project.to_dict()
+ data['id'] = new_id
+
+ added = add_entry(PROJECTS_FILE, data)
+ return jsonify(added), 201
\ No newline at end of file
diff --git a/backend/routes/services.py b/backend/routes/services.py
new file mode 100644
index 0000000..06d0687
--- /dev/null
+++ b/backend/routes/services.py
@@ -0,0 +1,15 @@
+from flask import Blueprint, jsonify, request, current_app
+from utils.data_loader import load_data
+from utils.json_crud import load_json, save_json, add_entry, delete_entry, update_entry
+from models.service_model import ServiceModel
+import os
+
+DATA_DIR = os.path.join(os.path.dirname(__file__), '../data')
+SERVICES_FILE = os.path.join(DATA_DIR, 'services.json')
+
+services_bp = Blueprint('services', __name__, url_prefix='/api/services')
+
+@services_bp.route('/', methods=['GET'])
+def get_services():
+ services = load_data('services.json')
+ return jsonify(services)
\ No newline at end of file
diff --git a/backend/utils/__init__.py b/backend/utils/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/backend/utils/__pycache__/__init__.cpython-313.pyc b/backend/utils/__pycache__/__init__.cpython-313.pyc
new file mode 100644
index 0000000..a3cd4a8
Binary files /dev/null and b/backend/utils/__pycache__/__init__.cpython-313.pyc differ
diff --git a/backend/utils/__pycache__/data_loader.cpython-313.pyc b/backend/utils/__pycache__/data_loader.cpython-313.pyc
new file mode 100644
index 0000000..9424f46
Binary files /dev/null and b/backend/utils/__pycache__/data_loader.cpython-313.pyc differ
diff --git a/backend/utils/__pycache__/enums.cpython-313.pyc b/backend/utils/__pycache__/enums.cpython-313.pyc
new file mode 100644
index 0000000..310b954
Binary files /dev/null and b/backend/utils/__pycache__/enums.cpython-313.pyc differ
diff --git a/backend/utils/__pycache__/json_crud.cpython-313.pyc b/backend/utils/__pycache__/json_crud.cpython-313.pyc
new file mode 100644
index 0000000..228174d
Binary files /dev/null and b/backend/utils/__pycache__/json_crud.cpython-313.pyc differ
diff --git a/backend/utils/data_loader.py b/backend/utils/data_loader.py
new file mode 100644
index 0000000..54f5698
--- /dev/null
+++ b/backend/utils/data_loader.py
@@ -0,0 +1,8 @@
+import json
+import os
+
+DATA_DIR = os.path.join(os.path.dirname(__file__), '../data')
+
+def load_data(filename):
+ with open(os.path.join(DATA_DIR, filename), 'r') as f:
+ return json.load(f)
\ No newline at end of file
diff --git a/backend/utils/enums.py b/backend/utils/enums.py
new file mode 100644
index 0000000..8c9b0c6
--- /dev/null
+++ b/backend/utils/enums.py
@@ -0,0 +1,21 @@
+enumerates = {
+ "status": [
+ "En cours",
+ "Terminé",
+ "Futur projet"
+ ],
+ "technologies": [
+ "HTML & CSS",
+ "Python",
+ "Django",
+ "React",
+ "NodeJS",
+ "Angular",
+ "C#",
+ "PHP",
+ "Javascript",
+ "TypeScript",
+ "Unity",
+ "Godot"
+ ],
+}
\ No newline at end of file
diff --git a/backend/utils/json_crud.py b/backend/utils/json_crud.py
new file mode 100644
index 0000000..c3bb779
--- /dev/null
+++ b/backend/utils/json_crud.py
@@ -0,0 +1,33 @@
+import json
+import os
+
+def load_json(filename):
+ if not os.path.exists(filename):
+ return []
+ with open(filename, 'r', encoding='utf-8') as f:
+ return json.load(f)
+
+def save_json(filename, data):
+ with open(filename, 'w', encoding='utf-8') as f:
+ json.dump(data, f, indent=4, ensure_ascii=False)
+
+def add_entry(filename, entry):
+ data = load_json(filename)
+ data.append(entry)
+ save_json(filename, data)
+ return entry
+
+def update_entry(filename, entry_id, new_entry):
+ data = load_json(filename)
+ for i, item in enumerate(data):
+ if item.get('id') == entry_id:
+ data[i] = new_entry
+ save_json(filename, data)
+ return new_entry
+ return None
+
+def delete_entry(filename, entry_id):
+ data = load_json(filename)
+ new_data = [item for item in data if item.get('id') != entry_id]
+ save_json(filename, new_data)
+ return len(data) != len(new_data)
\ No newline at end of file
diff --git a/data/contacts.json b/data/contacts.json
deleted file mode 100644
index 74ce6f1..0000000
--- a/data/contacts.json
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "firstname": "Anthony",
- "lastname": "VIOLET",
- "email": "violet.anthony90@gmail.com",
- "gsm": "",
- "social": {
- "linkedin": "",
- "twitter": "",
- "github": ""
- },
- "linkedin": "https:\/\/www.linkedin.com\/in\/anthony-violet\/",
- "twitter": "",
- "github": "https:\/\/github.com\/MrToine"
-}
\ No newline at end of file
diff --git a/data/projects.json b/data/projects.json
deleted file mode 100644
index 18c248b..0000000
--- a/data/projects.json
+++ /dev/null
@@ -1,53 +0,0 @@
-[
- {
- "id": "685711e89e84c",
- "type": "Site vitrine",
- "name": "Entreprise de construction",
- "description": "Un exemple de site vitrine pour une entreprise de construction. Ce projet concept démontre mes compétences en création d'interfaces professionnelles pour les indépendants et TPE Il illustre ma capacité à concevoir une stratégie digitale complète : galerie de réalisations, formulaire de devis, optimisation SEO locale et zone de témoignages clients.",
- "link": "https:\/\/violet-anthony.eu\/projects.examples\/batipro",
- "technologies": [
- "HTML",
- "CSS",
- "JavaScript",
- "PHP"
- ],
- "start_date": "2025-05-19",
- "end_date": "2025-05-23",
- "created_at": "2025-06-21 20:11:20",
- "active": true
- },
- {
- "id": "6857146e58bd6",
- "type": "CRM",
- "name": "Gestionnaire de prospects et de clients",
- "description": "J'ai réaliser à titre personnel, un CRM pour le solodev. Il permet de créer des prospect, suivre ces derniers, envoyer des emails, les transformer en client, créer des devis, contacter et associer les clients à des projets. Le CRM à était un réelle défi à mes yeux car c'est la première fois que je créer un logiciel interconnecté par des modules avec python.",
- "link": null,
- "technologies": [
- "HTML",
- "CSS",
- "Python",
- "Flask"
- ],
- "start_date": "2025-04-28",
- "end_date": "2025-05-12",
- "created_at": "2025-06-21 20:22:06",
- "active": true
- },
- {
- "id": "685714d667695",
- "type": "CV interactif",
- "name": "Site pro",
- "description": "J'ai développer ce site web pour permettre de mettre en avant mes compétences en tant que développeur web & applications. Il me permet de mettre en avant des projets réaliser pour des clients ou des projets fictif pour démontrer mon savoir faire.",
- "link": "https:\/\/violet-anthony.eu",
- "technologies": [
- "PHP",
- "JavaScript",
- "HTML",
- "CSS"
- ],
- "start_date": "2025-05-12",
- "end_date": "2025-05-16",
- "created_at": "2025-06-21 20:23:50",
- "active": true
- }
-]
\ No newline at end of file
diff --git a/frontend/.gitignore b/frontend/.gitignore
new file mode 100644
index 0000000..5ef6a52
--- /dev/null
+++ b/frontend/.gitignore
@@ -0,0 +1,41 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+/.pnp
+.pnp.*
+.yarn/*
+!.yarn/patches
+!.yarn/plugins
+!.yarn/releases
+!.yarn/versions
+
+# testing
+/coverage
+
+# next.js
+/.next/
+/out/
+
+# production
+/build
+
+# misc
+.DS_Store
+*.pem
+
+# debug
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+.pnpm-debug.log*
+
+# env files (can opt-in for committing if needed)
+.env*
+
+# vercel
+.vercel
+
+# typescript
+*.tsbuildinfo
+next-env.d.ts
diff --git a/frontend/.idea/.gitignore b/frontend/.idea/.gitignore
new file mode 100644
index 0000000..13566b8
--- /dev/null
+++ b/frontend/.idea/.gitignore
@@ -0,0 +1,8 @@
+# Default ignored files
+/shelf/
+/workspace.xml
+# Editor-based HTTP Client requests
+/httpRequests/
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml
diff --git a/frontend/.idea/dictionaries/project.xml b/frontend/.idea/dictionaries/project.xml
new file mode 100644
index 0000000..4787784
--- /dev/null
+++ b/frontend/.idea/dictionaries/project.xml
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/frontend/.idea/encodings.xml b/frontend/.idea/encodings.xml
new file mode 100644
index 0000000..df87cf9
--- /dev/null
+++ b/frontend/.idea/encodings.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/frontend/.idea/frontend.iml b/frontend/.idea/frontend.iml
new file mode 100644
index 0000000..24643cc
--- /dev/null
+++ b/frontend/.idea/frontend.iml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/frontend/.idea/modules.xml b/frontend/.idea/modules.xml
new file mode 100644
index 0000000..f3d93d7
--- /dev/null
+++ b/frontend/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/frontend/.idea/vcs.xml b/frontend/.idea/vcs.xml
new file mode 100644
index 0000000..6c0b863
--- /dev/null
+++ b/frontend/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/frontend/README.md b/frontend/README.md
new file mode 100644
index 0000000..e215bc4
--- /dev/null
+++ b/frontend/README.md
@@ -0,0 +1,36 @@
+This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
+
+## Getting Started
+
+First, run the development server:
+
+```bash
+npm run dev
+# or
+yarn dev
+# or
+pnpm dev
+# or
+bun dev
+```
+
+Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
+
+You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
+
+This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
+
+## Learn More
+
+To learn more about Next.js, take a look at the following resources:
+
+- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
+- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
+
+You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
+
+## Deploy on Vercel
+
+The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
+
+Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
diff --git a/frontend/app/api/projects/route.ts b/frontend/app/api/projects/route.ts
new file mode 100644
index 0000000..20b117d
--- /dev/null
+++ b/frontend/app/api/projects/route.ts
@@ -0,0 +1,17 @@
+import { NextResponse } from "next/server";
+
+export async function POST(req: Request) {
+ const body = await req.json();
+
+ const res = await fetch("http://127.0.0.1:5000/api/projects/", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ "x-api-key": process.env.API_KEY!,
+ },
+ body: JSON.stringify(body),
+ });
+
+ const data = await res.json();
+ return NextResponse.json(data, { status: res.status });
+}
\ No newline at end of file
diff --git a/frontend/app/components/About.tsx b/frontend/app/components/About.tsx
new file mode 100644
index 0000000..18cfaa2
--- /dev/null
+++ b/frontend/app/components/About.tsx
@@ -0,0 +1,24 @@
+async function GetAboutDatas(){
+ const res = await fetch(`${process.env.BACKEND_URL}/cv/about`, {
+ cache: process.env.CACHE
+ });
+
+ if(!res.ok){
+ throw new Error("Erreur lors de la lecture des données de contact.")
+ }
+ return res.json();
+}
+
+export default async function About(){
+ const about_text = await GetAboutDatas();
+
+ return (
+
+
+
Développeur web
+
Anthony Violet
+
{about_text}
+
+
+ );
+}
\ No newline at end of file
diff --git a/frontend/app/components/Contact.tsx b/frontend/app/components/Contact.tsx
new file mode 100644
index 0000000..7b0b596
--- /dev/null
+++ b/frontend/app/components/Contact.tsx
@@ -0,0 +1,104 @@
+"use client";
+
+import Link from "next/link";
+import { useEffect, useState } from "react";
+import { usePathname } from "next/navigation";
+
+type ContactData = {
+ email?: string;
+ linkedin?: string;
+ github?: string;
+};
+
+export default function ContactComponent() {
+ const pathname = usePathname();
+ const onContactPage = pathname === "/contact";
+
+ const [data, setData] = useState(null);
+
+ useEffect(() => {
+ if (!onContactPage) return;
+
+ // Essaye avec BACKEND_URL, sinon fallback vers une route locale hypothétique
+ const backend = (process.env.BACKEND_URL as string) || "http://127.0.0.1:5000/api";
+ const url = backend ? `${backend}/contact` : "http://127.0.0.1:5000/api/contact";
+
+ fetch(url, { cache: "no-store" })
+ .then((res) => (res.ok ? res.json() : Promise.reject(new Error("fetch contact failed"))))
+ .then((json) => setData(json))
+ .catch(() => setData(null));
+ }, [onContactPage]);
+
+ // Si on n'est pas sur la page /contact => juste un lien
+ if (!onContactPage) {
+ return (
+
+ );
+ }
+
+ // Page /contact => liens + formulaire
+ return (
+
+
+
+
+ {data?.email ? (
+
+
+ Votre nom
+
+
+
+
+ Votre email
+
+
+
+
+ Sujet
+
+
+
+
+ Message
+
+
+
+
+
+ Envoyer
+
+
+ Réinitialiser
+
+
+
+ ) : (
+
Une erreur est survenue. Vous pouvez tout de même me contacter directement par email : Me joindre par email
+ )}
+
+
+ );
+}
\ No newline at end of file
diff --git a/frontend/app/components/Header.tsx b/frontend/app/components/Header.tsx
new file mode 100644
index 0000000..10549a1
--- /dev/null
+++ b/frontend/app/components/Header.tsx
@@ -0,0 +1,148 @@
+"use client";
+// app/components/Header.tsx
+import Link from "next/link";
+import { useEffect, useState } from "react";
+
+export default function Header() {
+ const [theme, setTheme] = useState<"light" | "dark">("light");
+ const [isTop, setIsTop] = useState(true);
+
+ // Initialise le thème en fonction du localStorage ou de la préférence système
+ useEffect(() => {
+ const saved = typeof window !== "undefined" ? localStorage.getItem("theme") : null;
+ const prefersDark = typeof window !== "undefined" && window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches;
+ const initial = (saved === "light" || saved === "dark") ? (saved as "light" | "dark") : (prefersDark ? "dark" : "light");
+ setTheme(initial);
+ document.documentElement.setAttribute("data-theme", initial);
+ }, []);
+
+ // Détecte si l'on est tout en haut de la page
+ useEffect(() => {
+ const onScroll = () => setIsTop(window.scrollY <= 0);
+ onScroll(); // état initial au chargement
+ window.addEventListener("scroll", onScroll, { passive: true });
+ return () => window.removeEventListener("scroll", onScroll);
+ }, []);
+
+ const toggleTheme = () => {
+ const next = theme === "dark" ? "light" : "dark";
+ setTheme(next);
+ document.documentElement.setAttribute("data-theme", next);
+ try {
+ localStorage.setItem("theme", next);
+ } catch {}
+ };
+
+ return (
+
+
+
+ Anthony Violet
+ Développeur Freelance & Digital Nomad
+
+
+
+
+ Projets
+ Compétences
+ Blog Voyage
+ Contact
+ Malt
+ LinkedIn
+
+
+
+
+ {theme === "dark" ? "☀️" : "🌙"}
+
+ Travaillons ensemble
+
+
+
+ /*
+
+
+ Anthony Violet
+ // Développeur Full Stack & Créateur de Jeux
+
+
+
+
+ /projets
+ /tech-stack
+ /toine-traveller
+ /contact@terminal
+
+
+
+ En ligne █
+ Disponible pour missions
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Projets
+ Services
+ Blog
+ Me contacter
+
+
+
+
+
+
+ */
+ );
+}
\ No newline at end of file
diff --git a/frontend/app/components/Services.tsx b/frontend/app/components/Services.tsx
new file mode 100644
index 0000000..f44d70d
--- /dev/null
+++ b/frontend/app/components/Services.tsx
@@ -0,0 +1,35 @@
+async function GetServices() {
+ const res = await fetch(`${process.env.BACKEND_URL}/services`, {
+ cache: process.env.CACHE
+ });
+
+ if (!res.ok) {
+ throw new Error("Erreur lors de la lecture des services du CV.")
+ }
+ return res.json();
+}
+
+export default async function Services() {
+ const services = await GetServices();
+
+ return (
+
+
+
Services proposés
+
+ {services.map((service: any) => (
+
+
+
+
{service.name}
+ {"description" in service && service.description ? (
+
{service.description}
+ ) : null}
+
+
+ ))}
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/frontend/app/components/Skills.tsx b/frontend/app/components/Skills.tsx
new file mode 100644
index 0000000..ae37d3e
--- /dev/null
+++ b/frontend/app/components/Skills.tsx
@@ -0,0 +1,73 @@
+async function GetSkills() {
+ const res = await fetch(`${process.env.BACKEND_URL}/cv/skills`, {
+ cache: process.env.CACHE
+ });
+
+ if (!res.ok) {
+ throw new Error("Erreur lors du chargement des skills du CV");
+ }
+ return res.json();
+}
+export default async function Skills(){
+ const skills = await GetSkills();
+
+ const obj: Record = Array.isArray(skills) ? (skills[0] ?? {}) : {};
+ const entries = Object.entries(obj) as [string, string[]][];
+
+ if (entries.length === 0) {
+ return (
+
+
+
Mes compétences
+
Aucune compétence à afficher.
+
+
+ );
+ }
+
+ return (
+ /*
+ *
+ {mySkillsEntries.length > 0 && (
+
+ )}
+ * */
+
+ );
+}
\ No newline at end of file
diff --git a/frontend/app/contact/page.tsx b/frontend/app/contact/page.tsx
new file mode 100644
index 0000000..f3ab34a
--- /dev/null
+++ b/frontend/app/contact/page.tsx
@@ -0,0 +1,8 @@
+import ContactComponent from "@/app/components/Contact";
+export default function Contact() {
+ return (
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/frontend/app/css/colors.css b/frontend/app/css/colors.css
new file mode 100644
index 0000000..8625be1
--- /dev/null
+++ b/frontend/app/css/colors.css
@@ -0,0 +1,69 @@
+/* Design tokens — CV/Portfolio blanc & orange */
+:root {
+ /* Couleurs de base (clair) */
+ --color-bg: #ffffff;
+ --color-surface: #ffffff;
+ --color-text: #1f2937;
+ --color-muted: #6b7280;
+ --color-border: #efefef;
+ --color-accent: #ff6b35;
+
+ /* Variantes d’accent */
+ --color-accent-50: #fff4ec;
+ --color-accent-100: #ffe4d6;
+ --color-accent-200: #ffcfba;
+ --color-accent-600: #e55a2b;
+ --color-accent-700: #c74d25;
+
+ /* Rayons et ombres */
+ --radius-sm: 6px;
+ --radius-md: 10px;
+ --radius-lg: 14px;
+ --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.06);
+ --shadow-md: 0 2px 10px rgba(0, 0, 0, 0.08);
+
+ /* Layout */
+ --header-height: 80px;
+ --container-max: 1100px;
+
+ /* Espacements */
+ --space-1: 0.25rem;
+ --space-2: 0.5rem;
+ --space-3: 0.75rem;
+ --space-4: 1rem;
+ --space-5: 1.25rem;
+ --space-6: 1.5rem;
+ --space-8: 2rem;
+ --space-12: 3rem;
+ --space-16: 4rem;
+}
+
+/* Thème sombre (préférence système par défaut) */
+@media (prefers-color-scheme: dark) {
+ :root {
+ --color-bg: #1f2937;
+ --color-surface: #0e141b;
+ --color-text: #e5e7eb;
+ --color-muted: #9ca3af;
+ --color-border: #0b0f14;
+ /* accent inchangé pour la cohérence de marque */
+ }
+}
+
+/* Thème forcé via data-attribute */
+html[data-theme="light"] {
+ --color-bg: #ffffff;
+ --color-surface: #FFF7F7;
+ --color-text: #1f2937;
+ --color-muted: #6b7280;
+ --color-border: #efefef;
+}
+
+html[data-theme="dark"] {
+ --color-bg: #1f2937;
+ --color-surface: #0e141b;
+ --color-text: #e5e7eb;
+ --color-muted: #9ca3af;
+ --color-border: #0b0f14;
+ --color-accent-50: #041D36;
+}
\ No newline at end of file
diff --git a/frontend/app/css/components.css b/frontend/app/css/components.css
new file mode 100644
index 0000000..ced2ac7
--- /dev/null
+++ b/frontend/app/css/components.css
@@ -0,0 +1,576 @@
+/* Composants réutilisables — minimal & pro */
+
+/* Titres de section avec liseré orange */
+.section-title {
+ display: inline-block;
+ position: relative;
+ padding-bottom: 0.4rem;
+ margin-bottom: var(--space-6, 1.5rem);
+}
+
+.section-title::after {
+ content: "";
+ position: absolute;
+ left: 0;
+ bottom: 0;
+ width: 56px;
+ height: 3px;
+ background: var(--color-accent, #ff6b35);
+ border-radius: 999px;
+}
+
+/* Grille responsive */
+.grid {
+ display: grid;
+ gap: var(--space-6, 1.5rem);
+ grid-template-columns: repeat(auto-fit, minmax(min(100%, 260px), 1fr));
+}
+
+/* Carte */
+.card {
+ background: var(--color-surface, #ffffff);
+ border: 1px solid var(--color-border, #efefef);
+ border-radius: var(--radius-md, 10px);
+ box-shadow: var(--shadow-sm, 0 1px 2px rgba(0,0,0,0.06));
+ padding: var(--space-6, 1.5rem);
+ transition: transform 0.15s ease, box-shadow 0.2s ease, border-color 0.2s ease;
+}
+
+.card:hover {
+ transform: translateY(-2px);
+ border-color: var(--color-accent-100, #ffe4d6);
+ box-shadow: var(--shadow-md, 0 2px 10px rgba(0,0,0,0.08));
+}
+
+/* Blocs génériques (layout de sections) */
+.block {
+ background: var(--color-surface, #ffffff);
+ border: 1px solid var(--color-border, #efefef);
+ border-radius: var(--radius-md, 10px);
+ padding: var(--space-6, 1.5rem);
+ box-shadow: var(--shadow-sm, 0 1px 2px rgba(0,0,0,0.06));
+}
+
+.block--split {
+ display: flex;
+ align-items: center;
+ gap: var(--space-6, 1.5rem);
+ flex-wrap: wrap;
+}
+
+.block__media {
+ flex: 0 0 96px;
+ height: 96px;
+ border-radius: var(--radius-md, 10px);
+ background: var(--color-accent-50, #fff4ec);
+ border: 1px solid var(--color-accent-100, #ffe4d6);
+}
+
+.block__content {
+ flex: 1 1 360px;
+}
+
+/* Feature (icône + texte) */
+.feature {
+ display: flex;
+ align-items: flex-start;
+ gap: var(--space-4, 1rem);
+}
+
+.feature__icon {
+ width: 40px;
+ height: 40px;
+ border-radius: 999px;
+ background: var(--color-accent-50, #fff4ec);
+ border: 1px solid var(--color-accent-100, #ffe4d6);
+ box-shadow: inset 0 0 0 2px rgba(255, 107, 53, 0.08);
+}
+
+.feature__title {
+ margin: 0;
+}
+
+/* Barre d'appel à action */
+.cta-bar {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: var(--space-4, 1rem);
+ padding: var(--space-5, 1.25rem) var(--space-6, 1.5rem);
+ border-radius: var(--radius-md, 10px);
+ background: var(--color-accent-50, #fff4ec);
+ border: 1px solid var(--color-accent-100, #ffe4d6);
+}
+
+/* Blocs génériques (layout de sections) */
+.block {
+ background: var(--color-surface, #ffffff);
+ border: 1px solid var(--color-border, #efefef);
+ border-radius: var(--radius-md, 10px);
+ padding: var(--space-6, 1.5rem);
+ box-shadow: var(--shadow-sm, 0 1px 2px rgba(0,0,0,0.06));
+}
+
+.block--split {
+ display: flex;
+ align-items: center;
+ gap: var(--space-6, 1.5rem);
+ flex-wrap: wrap;
+}
+
+.block__media {
+ flex: 0 0 96px;
+ height: 96px;
+ border-radius: var(--radius-md, 10px);
+ background: var(--color-accent-50, #fff4ec);
+ border: 1px solid var(--color-accent-100, #ffe4d6);
+}
+
+.block__content {
+ flex: 1 1 360px;
+}
+
+/* Feature (icône + texte) */
+.feature {
+ display: flex;
+ align-items: flex-start;
+ gap: var(--space-4, 1rem);
+}
+
+.feature__icon {
+ width: 40px;
+ height: 40px;
+ border-radius: 999px;
+ background: var(--color-accent-50, #fff4ec);
+ border: 1px solid var(--color-accent-100, #ffe4d6);
+ box-shadow: inset 0 0 0 2px rgba(255, 107, 53, 0.08);
+}
+
+.feature__title {
+ margin: 0;
+}
+
+/* Barre d'appel à action */
+.cta-bar {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: var(--space-4, 1rem);
+ padding: var(--space-5, 1.25rem) var(--space-6, 1.5rem);
+ border-radius: var(--radius-md, 10px);
+ background: var(--color-accent-50, #fff4ec);
+ border: 1px solid var(--color-accent-100, #ffe4d6);
+}
+
+/* =========================
+ DEV HERO + badges techno
+ ========================= */
+.dev-hero {
+ position: relative;
+ background: linear-gradient(180deg, var(--color-surface), var(--color-accent-50, #fff4ec));
+ border-bottom: 1px solid var(--color-border, #efefef);
+ padding-block: var(--space-16, 4rem);
+ overflow: hidden;
+}
+
+.dev-hero::before {
+ content: "";
+ position: absolute;
+ inset: -10% -20% auto -20%;
+ height: 220px;
+ background: radial-gradient(closest-side, color-mix(in oklab, var(--color-accent) 12%, transparent), transparent 70%);
+ opacity: .35;
+ filter: blur(20px);
+}
+
+.dev-hero .headline {
+ font-weight: 800;
+ letter-spacing: -0.02em;
+}
+
+.dev-hero .subheadline {
+ color: var(--color-muted, #6b7280);
+ max-width: 70ch;
+}
+
+.tech-badges {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.5rem;
+ margin-top: var(--space-4, 1rem);
+}
+
+.badge {
+ display: inline-flex;
+ align-items: center;
+ padding: 0.35rem 0.6rem;
+ border-radius: 999px;
+ border: 1px solid var(--color-accent-100, #ffe4d6);
+ background: var(--color-accent-50, #fff4ec);
+ color: var(--color-accent-700, #c74d25);
+ font-weight: 600;
+ font-size: 0.9rem;
+}
+
+.badge--mono {
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
+ letter-spacing: .02em;
+}
+
+/* =========================
+ Projets — liste en cartes
+ ========================= */
+.projects-list {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+ display: grid;
+ gap: var(--space-6, 1.5rem);
+ grid-template-columns: repeat(auto-fit, minmax(min(100%, 280px), 1fr));
+}
+
+.projects-list > li {
+ background: var(--color-surface, #ffffff);
+ border: 1px solid var(--color-border, #efefef);
+ border-radius: var(--radius-md, 10px);
+ padding: var(--space-6, 1.5rem);
+ box-shadow: var(--shadow-sm, 0 1px 2px rgba(0,0,0,0.06));
+ transition: transform 0.15s ease, box-shadow 0.2s ease, border-color 0.2s ease;
+}
+
+.projects-list > li:hover {
+ transform: translateY(-2px);
+ border-color: var(--color-accent-100, #ffe4d6);
+ box-shadow: var(--shadow-md, 0 2px 10px rgba(0,0,0,0.08));
+}
+
+.projects-list h3 {
+ margin: 0 0 var(--space-2, 0.5rem);
+ font-size: 1.125rem;
+}
+
+.projects-list p {
+ margin: 0;
+ color: var(--color-muted, #6b7280);
+}
+
+/* =========================
+ Services — cartes (features)
+ ========================= */
+#services ul {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+ display: grid;
+ gap: var(--space-6, 1.5rem);
+ grid-template-columns: repeat(auto-fit, minmax(min(100%, 260px), 1fr));
+}
+
+.service-card {
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-3, 0.75rem);
+}
+
+/* =====================================
+ Compétences — skill cards & meters
+ ===================================== */
+.skill-grid {
+ display: grid;
+ gap: var(--space-6, 1.5rem);
+ grid-template-columns: repeat(auto-fit, minmax(min(100%, 280px), 1fr));
+}
+
+/* Carte de compétence — plus expressive et accessible */
+.skill-card {
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-4, 1rem);
+ background: var(--color-surface, #ffffff);
+ border: 1px solid var(--color-border, #efefef);
+ border-radius: var(--radius-md, 10px);
+ padding: var(--space-6, 1.5rem);
+ box-shadow: var(--shadow-sm, 0 1px 2px rgba(0,0,0,0.06));
+ transition:
+ transform 0.15s ease,
+ box-shadow 0.2s ease,
+ border-color 0.2s ease,
+ background-color 0.2s ease;
+}
+
+/* Accent latéral qui apparaît au survol (met en valeur la carte) */
+.skill-card::after {
+ content: "";
+ position: absolute;
+ inset: 0 auto 0 0;
+ width: 4px;
+ background: transparent;
+ border-top-left-radius: var(--radius-md, 10px);
+ border-bottom-left-radius: var(--radius-md, 10px);
+ transition: background-color 0.2s ease;
+}
+
+.skill-card:hover {
+ transform: translateY(-2px);
+ border-color: var(--color-accent-100, #ffe4d6);
+ box-shadow: var(--shadow-md, 0 2px 10px rgba(0,0,0,0.08));
+}
+
+.skill-card:hover::after {
+ background: var(--color-accent, #ff6b35);
+}
+
+/* Focus clavier visible (accessibilité) */
+.skill-card:focus-within {
+ border-color: var(--color-accent, #ff6b35);
+ box-shadow: 0 0 0 3px color-mix(in oklab, var(--color-accent, #ff6b35) 22%, transparent);
+}
+
+/* Sous-blocs optionnels */
+.skill-card__header {
+ display: flex;
+ align-items: center;
+ gap: var(--space-3, 0.75rem);
+}
+
+.skill-card__icon {
+ width: 36px;
+ height: 36px;
+ border-radius: 10px;
+ background: var(--color-accent-50, #fff4ec);
+ border: 1px solid var(--color-accent-100, #ffe4d6);
+ flex: 0 0 auto;
+}
+
+.skill-card__body {
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-3, 0.75rem);
+}
+
+.skill-card__footer {
+ margin-top: auto;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: var(--space-3, 0.75rem);
+ padding-top: var(--space-4, 1rem);
+ border-top: 1px solid var(--color-border, #efefef);
+}
+
+.skill-card__title {
+ margin: 0 0 var(--space-3, 0.75rem);
+}
+
+.skill-card__tags {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.5rem;
+}
+
+.skill-meter {
+ height: 8px;
+ background: #ffffff;
+ border: 1px solid var(--color-border, #efefef);
+ border-radius: 999px;
+ overflow: hidden;
+}
+
+.skill-meter__bar {
+ height: 100%;
+ width: var(--level, 70%);
+ background: linear-gradient(90deg, var(--color-accent, #ff6b35), var(--color-accent-700, #c74d25));
+}
+
+/* =====================================
+ Code block et stats
+ ===================================== */
+.code-block {
+ background: #0b0f14;
+ color: #e5e7eb;
+ border-radius: var(--radius-md, 10px);
+ padding: var(--space-6, 1.5rem);
+ border: 1px solid #1f2937;
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
+}
+
+.code-block .code-accent {
+ color: #ffb38f;
+}
+
+.stats {
+ display: grid;
+ gap: var(--space-6, 1.5rem);
+ grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
+ margin-top: var(--space-6, 1.5rem);
+}
+
+.stat {
+ background: #ffffff;
+ border: 1px solid var(--color-border, #efefef);
+ border-radius: var(--radius-md, 10px);
+ padding: var(--space-5, 1.25rem);
+ text-align: center;
+ box-shadow: var(--shadow-sm, 0 1px 2px rgba(0,0,0,0.06));
+}
+
+.stat .stat__value {
+ font-size: 1.5rem;
+ font-weight: 800;
+ color: var(--color-accent-700, #c74d25);
+}
+
+.stat .stat__label {
+ color: var(--color-muted, #6b7280);
+ font-size: 0.9rem;
+}
+
+/* =====================================
+ Catégories + puces (chips)
+ ===================================== */
+section > ul > li {
+ background: var(--color-surface, #ffffff);
+ border: 1px solid var(--color-border, #efefef);
+ border-radius: var(--radius-md, 10px);
+ padding: var(--space-5, 1.25rem) var(--space-6, 1.5rem);
+}
+
+section > ul > li + li {
+ margin-top: var(--space-4, 1rem);
+}
+
+section > ul > li > h3 {
+ margin: 0 0 var(--space-3, 0.75rem);
+}
+
+section > ul > li > ul {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+ display: flex;
+ gap: 0.5rem;
+ flex-wrap: wrap;
+}
+
+section > ul > li > ul > li {
+ padding: 0.25rem 0.5rem;
+ border-radius: 999px;
+ background: var(--color-accent-50, #fff4ec);
+ color: var(--color-accent-700, #c74d25);
+ border: 1px solid var(--color-accent-100, #ffe4d6);
+ font-size: 0.85rem;
+ font-weight: 600;
+}
+
+/* =========================
+ Projets — liste en cartes
+ ========================= */
+.projects-list {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+ display: grid;
+ gap: var(--space-6, 1.5rem);
+ grid-template-columns: repeat(auto-fit, minmax(min(100%, 280px), 1fr));
+}
+
+.projects-list > li {
+ background: var(--color-surface, #ffffff);
+ border: 1px solid var(--color-border, #efefef);
+ border-radius: var(--radius-md, 10px);
+ padding: var(--space-6, 1.5rem);
+ box-shadow: var(--shadow-sm, 0 1px 2px rgba(0,0,0,0.06));
+ transition: transform 0.15s ease, box-shadow 0.2s ease, border-color 0.2s ease;
+}
+
+.projects-list > li:hover {
+ transform: translateY(-2px);
+ border-color: var(--color-accent-100, #ffe4d6);
+ box-shadow: var(--shadow-md, 0 2px 10px rgba(0,0,0,0.08));
+}
+
+.projects-list h3 {
+ margin: 0 0 var(--space-2, 0.5rem);
+ font-size: 1.125rem;
+}
+
+.projects-list p {
+ margin: 0;
+ color: var(--color-muted, #6b7280);
+}
+
+/* =========================
+ Services — cartes simples
+ ========================= */
+#services ul {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+ display: grid;
+ gap: var(--space-6, 1.5rem);
+ grid-template-columns: repeat(auto-fit, minmax(min(100%, 260px), 1fr));
+}
+
+#services li {
+ position: relative;
+ background: var(--color-surface, #ffffff);
+ border: 1px solid var(--color-border, #efefef);
+ border-radius: var(--radius-md, 10px);
+ padding: var(--space-5, 1.25rem) var(--space-6, 1.5rem);
+ box-shadow: var(--shadow-sm, 0 1px 2px rgba(0,0,0,0.06));
+ transition: transform 0.15s ease, box-shadow 0.2s ease, border-color 0.2s ease;
+}
+
+#services li::before {
+ content: "";
+ position: absolute;
+ inset: 0 auto 0 0;
+ width: 4px;
+ background: var(--color-accent, #ff6b35);
+ border-top-left-radius: var(--radius-md, 10px);
+ border-bottom-left-radius: var(--radius-md, 10px);
+}
+
+#services li:hover {
+ transform: translateY(-2px);
+ border-color: var(--color-accent-100, #ffe4d6);
+ box-shadow: var(--shadow-md, 0 2px 10px rgba(0,0,0,0.08));
+}
+
+/* =====================================
+ Compétences — catégories + puces (chips)
+ ===================================== */
+section > ul > li {
+ background: var(--color-surface, #ffffff);
+ border: 1px solid var(--color-border, #efefef);
+ border-radius: var(--radius-md, 10px);
+ padding: var(--space-5, 1.25rem) var(--space-6, 1.5rem);
+}
+
+section > ul > li + li {
+ margin-top: var(--space-4, 1rem);
+}
+
+section > ul > li > h3 {
+ margin: 0 0 var(--space-3, 0.75rem);
+}
+
+section > ul > li > ul {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+ display: flex;
+ gap: 0.5rem;
+ flex-wrap: wrap;
+}
+
+section > ul > li > ul > li {
+ padding: 0.25rem 0.5rem;
+ border-radius: 999px;
+ background: var(--color-accent-50, #fff4ec);
+ color: var(--color-accent-700, #c74d25);
+ border: 1px solid var(--color-accent-100, #ffe4d6);
+ font-size: 0.85rem;
+ font-weight: 600;
+}
diff --git a/frontend/app/css/forms.css b/frontend/app/css/forms.css
new file mode 100644
index 0000000..dc603ef
--- /dev/null
+++ b/frontend/app/css/forms.css
@@ -0,0 +1,112 @@
+/* Formulaires minimalistes — blanc & orange */
+label {
+ display: block;
+ margin-bottom: var(--space-2, 0.5rem);
+ font-weight: 600;
+}
+
+input[type="text"],
+input[type="email"],
+input[type="url"],
+input[type="tel"],
+input[type="password"],
+input[type="number"],
+select,
+textarea {
+ width: 100%;
+ max-width: 640px;
+ background: #ffffff;
+ color: var(--color-text, #1f2937);
+ border: 1px solid var(--color-border, #efefef);
+ border-radius: var(--radius-sm, 6px);
+ padding: 0.625rem 0.75rem;
+ outline: none;
+ transition: border-color 0.2s ease, box-shadow 0.2s ease, background-color 0.2s ease;
+}
+
+input:hover,
+select:hover,
+textarea:hover {
+ border-color: var(--color-accent-100, #ffe4d6);
+}
+
+textarea {
+ min-height: 140px;
+ resize: vertical;
+}
+
+input::placeholder,
+textarea::placeholder {
+ color: var(--color-muted, #6b7280);
+}
+
+input:focus,
+select:focus,
+textarea:focus {
+ border-color: var(--color-accent, #ff6b35);
+ box-shadow: 0 0 0 3px color-mix(in oklab, var(--color-accent, #ff6b35) 22%, transparent);
+ background: #fff;
+}
+
+/* Boutons */
+button,
+.button {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: 0.5rem;
+ padding: 0.6rem 1rem;
+ border: 1px solid transparent;
+ border-radius: var(--radius-sm, 6px);
+ background: var(--color-accent, #ff6b35);
+ color: #fff;
+ font-weight: 600;
+ cursor: pointer;
+ transition: transform 0.05s ease, background-color 0.2s ease, box-shadow 0.2s ease;
+ text-decoration: none;
+}
+
+button:hover,
+.button:hover {
+ background: var(--color-accent-600, #e55a2b);
+ box-shadow: 0 6px 14px rgba(255, 107, 53, 0.2);
+}
+
+button:active,
+.button:active {
+ transform: translateY(0.5px);
+ background: var(--color-accent-700, #c74d25);
+}
+
+button:focus-visible,
+.button:focus-visible {
+ outline: 2px solid var(--color-accent, #ff6b35);
+ outline-offset: 2px;
+ box-shadow: 0 0 0 4px color-mix(in oklab, var(--color-accent, #ff6b35) 25%, transparent);
+}
+
+button:disabled,
+.button:disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+}
+
+/* Variantes */
+.button--elevated {
+ box-shadow: 0 8px 18px rgba(255, 107, 53, 0.25);
+}
+
+.button--soft {
+ background: var(--color-accent-50, #fff4ec);
+ color: var(--color-accent-700, #c74d25);
+ border: 1px solid var(--color-accent-100, #ffe4d6);
+}
+
+.button--soft:hover {
+ background: var(--color-accent-100, #ffe4d6);
+ color: var(--color-accent-700, #c74d25);
+}
+
+.button--pill {
+ border-radius: 999px;
+}
\ No newline at end of file
diff --git a/frontend/app/css/globals.css b/frontend/app/css/globals.css
new file mode 100644
index 0000000..2062877
--- /dev/null
+++ b/frontend/app/css/globals.css
@@ -0,0 +1,131 @@
+/* Reset léger et bases */
+*,
+*::before,
+*::after {
+ box-sizing: border-box;
+}
+
+html, body {
+ height: 100%;
+}
+
+body {
+ margin: 0;
+ background: var(--color-bg, #ffffff);
+ color: var(--color-text, #1f2937);
+ min-height: 100svh;
+ padding-top: var(--header-height, 80px); /* espace pour le header fixe */
+ transition: background-color 0.25s ease, color 0.25s ease;
+ transition: background-color 0.25s ease, color 0.25s ease;
+}
+
+/* Images fluides */
+img, svg, video {
+ max-width: 100%;
+ height: auto;
+ display: block;
+}
+
+/* Mise en page commune */
+main {
+ display: block;
+}
+
+.container {
+ max-width: var(--container-max, 1100px);
+ margin-inline: auto;
+ padding-inline: min(5vw, var(--space-6, 1.5rem));
+}
+
+.section {
+ padding-block: var(--space-16, 4rem) var(--space-12, 3rem);
+}
+
+.hero {
+ background: var(--color-surface, #ffffff);
+ border-bottom: 1px solid var(--color-border, #efefef);
+ box-shadow: inset 0 -6px 0 var(--color-accent-50, #fff4ec);
+ padding-block: var(--space-16, 4rem);
+}
+
+.hero h2 {
+ margin: 0 0 var(--space-2, 0.5rem);
+}
+
+.hero p {
+ margin: 0;
+ max-width: 60ch;
+ color: var(--color-muted, #6b7280);
+}
+
+/* Footer minimal */
+footer {
+ background: var(--color-bg, #ffffff);
+ border-top: 1px solid var(--color-border, #efefef);
+ margin-top: var(--space-16, 4rem);
+ padding: var(--space-6, 1.5rem) min(5vw, var(--space-6, 1.5rem));
+ color: var(--color-muted, #6b7280);
+ text-align: center;
+}
+
+/* Accessibilité et confort */
+:focus-visible {
+ outline: 2px solid var(--color-accent, #ff6b35);
+ outline-offset: 2px;
+ border-radius: var(--radius-sm, 6px);
+}
+
+/* Sélection de texte en orange doux */
+::selection {
+ background: var(--color-accent-100, #ffe4d6);
+ color: var(--color-text, #1f2937);
+}
+
+/* Scroll fluide + offset pour header fixe */
+html {
+ scroll-behavior: smooth;
+ color-scheme: light;
+}
+
+html[data-theme="dark"] {
+ color-scheme: dark;
+ color-scheme: light;
+}
+
+html[data-theme="dark"] {
+ color-scheme: dark;
+}
+
+/* Tout élément ciblé par un id laissera la place au header */
+[id] {
+ scroll-margin-top: calc(var(--header-height, 80px) + 12px);
+}
+
+/* Séparateur horizontal discret */
+hr {
+ border: 0;
+ height: 1px;
+ background: var(--color-border, #efefef);
+ margin: var(--space-8, 2rem) 0;
+}
+
+/* Variantes de section */
+.section--alt {
+ background: var(--color-accent-50, #fff4ec);
+ border-top: 1px solid var(--color-border, #efefef);
+ border-bottom: 1px solid var(--color-border, #efefef);
+}
+
+.lead {
+ font-size: 1.125rem;
+ color: var(--color-muted, #6b7280);
+}
+
+/* Fin: préférences de mouvement */
+@media (prefers-reduced-motion: reduce) {
+ * {
+ animation: none !important;
+ transition: none !important;
+ scroll-behavior: auto !important;
+ }
+}
\ No newline at end of file
diff --git a/frontend/app/css/header_CBPNK.css b/frontend/app/css/header_CBPNK.css
new file mode 100644
index 0000000..d5be805
--- /dev/null
+++ b/frontend/app/css/header_CBPNK.css
@@ -0,0 +1,66 @@
+.cyberpunk {
+ background: #0a0a1a;
+ padding: 1rem 5%;
+ font-family: 'Courier New', monospace;
+ color: #00ff88;
+ position: fixed;
+ width: 100%;
+ top: 0;
+ z-index: 1000;
+}
+
+.cyberpunk .glitch-logo {
+ font-size: 1.8rem;
+ position: relative;
+}
+
+.cyberpunk .glitch-logo span {
+ animation: glitch 1.5s infinite;
+}
+
+@keyframes glitch {
+ 0% { transform: skew(0deg); }
+ 20% { transform: skew(2deg); }
+ 40% { transform: skew(-1deg); }
+ 60% { transform: skew(1deg); }
+ 80% { transform: skew(-2deg); }
+ 100% { transform: skew(0deg); }
+}
+
+.cyberpunk .subtitle {
+ font-size: 0.9rem;
+ opacity: 0.8;
+}
+
+.cyberpunk .neon-nav ul {
+ display: flex;
+ list-style: none;
+ gap: 1.5rem;
+}
+
+.cyberpunk .neon-nav a {
+ color: #00ff88;
+ text-decoration: none;
+ position: relative;
+}
+
+.cyberpunk .neon-nav a::after {
+ content: '';
+ position: absolute;
+ bottom: -5px;
+ left: 0;
+ width: 0;
+ height: 2px;
+ background: #00ff88;
+ transition: width 0.3s;
+}
+
+.cyberpunk .neon-nav a:hover::after {
+ width: 100%;
+}
+
+.cyberpunk .status-bar {
+ font-size: 0.8rem;
+ margin-top: 0.5rem;
+ color: #ff6b35;
+}
diff --git a/frontend/app/css/header_DARK.css b/frontend/app/css/header_DARK.css
new file mode 100644
index 0000000..b3cbeff
--- /dev/null
+++ b/frontend/app/css/header_DARK.css
@@ -0,0 +1,43 @@
+.dark-header {
+ background: #1a1a2e;
+ padding: 1rem 5%;
+ position: fixed;
+ width: 100%;
+ top: 0;
+ z-index: 1000;
+}
+
+.dark-header .container {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.dark-header .logo img {
+ height: 50px;
+}
+
+.dark-header nav ul {
+ display: flex;
+ list-style: none;
+ gap: 2rem;
+}
+
+.dark-header nav a {
+ color: #e6e6e6;
+ text-decoration: none;
+ font-weight: 500;
+ transition: color 0.3s;
+}
+
+.dark-header nav a:hover {
+ color: #ff6b35;
+}
+
+.dark-header .theme-toggle button {
+ background: none;
+ border: none;
+ color: #e6e6e6;
+ font-size: 1.2rem;
+ cursor: pointer;
+}
diff --git a/frontend/app/css/header_IT.css b/frontend/app/css/header_IT.css
new file mode 100644
index 0000000..4ec0f90
--- /dev/null
+++ b/frontend/app/css/header_IT.css
@@ -0,0 +1,87 @@
+.interactive-header {
+ text-align: center;
+ padding: 3rem 5%;
+ background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
+ position: relative;
+ overflow: hidden;
+}
+
+.interactive-header .typewriter h1 {
+ font-size: 2.5rem;
+ margin: 0;
+ color: #333;
+}
+
+.interactive-header .typed-text {
+ color: #ff6b35;
+ border-right: 2px solid #ff6b35;
+ animation: blink 0.7s infinite;
+}
+
+@keyframes blink {
+ 0%, 100% { opacity: 1; }
+ 50% { opacity: 0; }
+}
+
+.interactive-header p {
+ font-size: 1.2rem;
+ margin: 0.5rem 0 2rem;
+ color: #666;
+}
+
+.interactive-header .tech-tag {
+ background: #ff6b35;
+ color: white;
+ padding: 0.2rem 0.5rem;
+ border-radius: 3px;
+ font-weight: 600;
+}
+
+.interactive-header nav ul {
+ display: flex;
+ justify-content: center;
+ list-style: none;
+ gap: 2rem;
+}
+
+.interactive-header nav a {
+ color: #333;
+ text-decoration: none;
+ font-weight: 500;
+ padding-bottom: 0.5rem;
+ position: relative;
+}
+
+.interactive-header nav a::after {
+ content: '';
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ width: 0;
+ height: 2px;
+ background: #ff6b35;
+ transition: width 0.3s;
+}
+
+.interactive-header nav a:hover::after {
+ width: 100%;
+}
+
+.interactive-header .language-switcher {
+ position: absolute;
+ top: 1rem;
+ right: 1rem;
+}
+
+.interactive-header .language-switcher button {
+ background: none;
+ border: 1px solid #ccc;
+ padding: 0.3rem 0.6rem;
+ cursor: pointer;
+}
+
+.interactive-header .language-switcher button:first-child {
+ background: #ff6b35;
+ color: white;
+ border-color: #ff6b35;
+}
diff --git a/frontend/app/css/header_LB.css b/frontend/app/css/header_LB.css
new file mode 100644
index 0000000..6206524
--- /dev/null
+++ b/frontend/app/css/header_LB.css
@@ -0,0 +1,75 @@
+.sidebar-header {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ background: #f8f8f8;
+ padding: 2rem 1rem;
+ position: fixed;
+ height: 100vh;
+ width: 250px;
+ left: 0;
+ top: 0;
+ z-index: 1000;
+ box-shadow: 2px 0 10px rgba(0, 0, 0, 0.1);
+}
+
+.sidebar-header .profile-pic img {
+ width: 80px;
+ height: 80px;
+ border-radius: 50%;
+ object-fit: cover;
+ border: 3px solid #ff6b35;
+}
+
+.sidebar-header .profile-info {
+ text-align: center;
+ margin: 1rem 0;
+}
+
+.sidebar-header h1 {
+ font-size: 1.3rem;
+ margin: 0.5rem 0;
+ color: #333;
+}
+
+.sidebar-header p {
+ font-size: 0.9rem;
+ color: #666;
+}
+
+.sidebar-header nav ul {
+ list-style: none;
+ width: 100%;
+ padding: 0;
+}
+
+.sidebar-header nav li {
+ margin: 1rem 0;
+}
+
+.sidebar-header nav a {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ color: #333;
+ text-decoration: none;
+ padding: 0.5rem;
+ border-radius: 5px;
+ transition: background 0.3s;
+}
+
+.sidebar-header nav a:hover {
+ background: #ff6b35;
+ color: white;
+}
+
+.sidebar-header .social-links {
+ display: flex;
+ gap: 1rem;
+ margin-top: 2rem;
+}
+
+.sidebar-header .social-links a {
+ color: #333;
+ font-size: 1.2rem;
+}
diff --git a/frontend/app/css/header_MP.css b/frontend/app/css/header_MP.css
new file mode 100644
index 0000000..f544048
--- /dev/null
+++ b/frontend/app/css/header_MP.css
@@ -0,0 +1,115 @@
+.minimalist-pro {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 1rem 5%;
+ background: var(--color-surface);
+ color: var(--color-text);
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.08);
+ border-bottom: 1px solid var(--color-border);
+ position: fixed;
+ width: 100%;
+ top: 0;
+ z-index: 1000;
+ box-sizing: border-box; /* important pour éviter l’élargissement avec le padding */
+ overflow-x: auto; /* autorise un scroll horizontal si vraiment nécessaire */
+ height: var(--header-height, 80px);
+ transition: background-color 0.25s ease, color 0.25s ease, box-shadow 0.25s ease, border-color 0.25s ease;
+}
+
+/* Quand la page est tout en haut: enlever la bordure (et l’ombre pour un rendu plat) */
+.minimalist-pro.is-top {
+ border-bottom-color: transparent;
+ box-shadow: none;
+}
+
+.minimalist-pro .logo {
+ font-weight: 700;
+ font-size: 1.25rem;
+ min-width: 0; /* permet de rétrécir correctement en flex */
+}
+
+.minimalist-pro .name {
+ color: inherit;
+}
+
+.minimalist-pro .tagline {
+ display: block;
+ font-size: 0.85rem;
+ color: var(--color-muted);
+ font-weight: 400;
+}
+
+.minimalist-pro nav ul {
+ display: flex;
+ list-style: none;
+ gap: 2rem;
+ flex-wrap: wrap; /* évite le débordement: passe à la ligne si pas assez de place */
+ margin: 0;
+ padding: 0;
+}
+
+.minimalist-pro nav a {
+ text-decoration: none;
+ color: inherit;
+ font-weight: 600;
+ transition: color 0.2s ease;
+ white-space: nowrap; /* garde chaque lien sur une ligne, mais la UL peut retourner à la ligne */
+}
+
+.minimalist-pro nav a:hover {
+ color: var(--color-accent);
+}
+
+.minimalist-pro .cta {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.75rem;
+}
+
+.minimalist-pro .button {
+ background: var(--color-accent);
+ color: #fff;
+ padding: 0.55rem 1rem;
+ border-radius: var(--radius-sm);
+ text-decoration: none;
+ font-weight: 700;
+ flex-shrink: 0; /* ne rétrécit pas, mais ne force pas l’élargissement grâce au wrap de la UL */
+ transition: background-color 0.2s ease, box-shadow 0.2s ease;
+}
+
+.minimalist-pro .button:hover {
+ background: var(--color-accent-600);
+ box-shadow: 0 6px 14px rgba(255, 107, 53, 0.2);
+}
+
+/* Bouton de bascule du thème */
+.theme-toggle__btn {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 2.25rem;
+ height: 2.25rem;
+ border-radius: 999px;
+ border: 1px solid var(--color-border);
+ background: var(--color-surface);
+ color: var(--color-text);
+ transition: background-color 0.2s ease, border-color 0.2s ease, transform 0.05s ease;
+ cursor: pointer;
+}
+
+.theme-toggle__btn:hover {
+ background: var(--color-accent-50);
+ border-color: var(--color-accent-100);
+}
+
+.theme-toggle__btn:active {
+ transform: translateY(0.5px);
+}
+
+/* Réduit l’espace entre liens sur écrans moyens pour limiter les risques de débordement */
+@media (max-width: 1024px) {
+ .minimalist-pro nav ul {
+ gap: 1rem;
+ }
+}
\ No newline at end of file
diff --git a/frontend/app/css/links.css b/frontend/app/css/links.css
new file mode 100644
index 0000000..798a411
--- /dev/null
+++ b/frontend/app/css/links.css
@@ -0,0 +1,31 @@
+/* Liens minimaux et professionnels (underline animé) */
+a {
+ color: inherit;
+ text-decoration: none;
+ background-image: linear-gradient(var(--color-accent, #ff6b35), var(--color-accent, #ff6b35));
+ background-position: 0 100%;
+ background-size: 0% 2px;
+ background-repeat: no-repeat;
+ transition: background-size 0.25s ease, color 0.2s ease, opacity 0.2s ease;
+}
+
+a:hover {
+ color: var(--color-accent-700, #c74d25);
+ background-size: 100% 2px;
+}
+
+a:active {
+ opacity: 0.9;
+}
+
+a:focus-visible {
+ outline: 2px solid var(--color-accent, #ff6b35);
+ outline-offset: 2px;
+ border-radius: var(--radius-sm, 6px);
+}
+
+/* Variante de lien "bouton texte" */
+.link-button {
+ color: var(--color-accent, #ff6b35);
+ font-weight: 600;
+}
\ No newline at end of file
diff --git a/frontend/app/css/typography.css b/frontend/app/css/typography.css
new file mode 100644
index 0000000..4d05cda
--- /dev/null
+++ b/frontend/app/css/typography.css
@@ -0,0 +1,66 @@
+/* Typographie */
+html {
+ font-size: 16px;
+}
+
+body {
+ font-family: Inter, ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "Apple Color Emoji", "Segoe UI Emoji";
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ line-height: 1.7;
+ letter-spacing: 0.1px;
+ color: var(--color-text, #1f2937);
+}
+
+h1, h2, h3, h4 {
+ line-height: 1.25;
+ margin: 0 0 var(--space-3, 0.75rem);
+ font-weight: 700;
+ color: var(--color-text, #1f2937);
+}
+
+h1 { font-size: clamp(2rem, 1.2rem + 1.8vw, 2.5rem); }
+h2 { font-size: clamp(1.5rem, 1.1rem + 1.1vw, 2rem); }
+h3 { font-size: clamp(1.25rem, 1rem + 0.7vw, 1.5rem); }
+h4 { font-size: clamp(1.1rem, 0.95rem + 0.5vw, 1.25rem); }
+
+p {
+ margin: 0 0 var(--space-4, 1rem);
+ color: var(--color-text, #1f2937);
+}
+
+small { color: var(--color-muted, #6b7280); }
+strong { font-weight: 700; }
+
+/* Utilitaires de texte */
+.text-accent { color: var(--color-accent, #ff6b35); }
+.eyebrow {
+ display: inline-block;
+ text-transform: uppercase;
+ letter-spacing: .08em;
+ font-size: 0.75rem;
+ color: var(--color-muted, #6b7280);
+}
+
+/* Accent “dev” */
+.mono {
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
+ letter-spacing: .02em;
+}
+
+.headline--code::before { content: "<"; color: var(--color-accent, #ff6b35); margin-right: 0.25rem; }
+.headline--code::after { content: " />"; color: var(--color-accent, #ff6b35); margin-left: 0.25rem; }
+
+/* Code inline et bloc */
+code {
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
+ background: var(--color-accent-50, #fff4ec);
+ border: 1px solid var(--color-accent-100, #ffe4d6);
+ color: var(--color-text, #1f2937);
+ border-radius: 6px;
+ padding: 0.1rem 0.35rem;
+}
+
+pre {
+ margin: 0;
+}
\ No newline at end of file
diff --git a/frontend/app/css/utilities.css b/frontend/app/css/utilities.css
new file mode 100644
index 0000000..5fcaec0
--- /dev/null
+++ b/frontend/app/css/utilities.css
@@ -0,0 +1,130 @@
+/* Utilitaires légers */
+
+/* Centrer un bloc dans son conteneur et limiter la largeur */
+.container--narrow {
+ max-width: 820px;
+ margin-inline: auto;
+}
+
+/* Centrage */
+.u-center {
+ display: grid;
+ place-items: center;
+}
+
+.u-text-center { text-align: center; }
+.u-muted { color: var(--color-muted, #6b7280); }
+
+/* Rythme vertical: ajoute un espace entre les éléments frères */
+.flow > * + * {
+ margin-top: var(--space-4, 1rem);
+}
+
+/* Ombre au survol */
+.shadow-hover {
+ transition: box-shadow 0.2s ease, transform 0.15s ease;
+}
+.shadow-hover:hover {
+ transform: translateY(-2px);
+ box-shadow: var(--shadow-md, 0 2px 10px rgba(0,0,0,0.08));
+}
+
+/* Visually hidden pour l’accessibilité */
+.visually-hidden,
+.sr-only {
+ position: absolute !important;
+ height: 1px; width: 1px;
+ overflow: hidden;
+ clip: rect(1px, 1px, 1px, 1px);
+ white-space: nowrap;
+ border: 0; padding: 0; margin: -1px;
+}
+
+/* Flexbox utilitaires */
+.flex { display: flex; }
+.flex-col { display: flex; flex-direction: column; }
+.flex-row { display: flex; flex-direction: row; }
+.items-center { align-items: center; }
+.items-start { align-items: flex-start; }
+.items-end { align-items: flex-end; }
+.justify-between { justify-content: space-between; }
+.justify-center { justify-content: center; }
+.gap-2 { gap: var(--space-2, 0.5rem); }
+.gap-4 { gap: var(--space-4, 1rem); }
+.gap-6 { gap: var(--space-6, 1.5rem); }
+
+/* Grilles rapides */
+.grid-2 { display: grid; gap: var(--space-6, 1.5rem); grid-template-columns: repeat(2, 1fr); }
+.grid-3 { display: grid; gap: var(--space-6, 1.5rem); grid-template-columns: repeat(3, 1fr); }
+.grid-4 { display: grid; gap: var(--space-6, 1.5rem); grid-template-columns: repeat(4, 1fr); }
+
+@media (max-width: 900px) {
+ .grid-3 { grid-template-columns: repeat(2, 1fr); }
+ .grid-4 { grid-template-columns: repeat(2, 1fr); }
+}
+@media (max-width: 640px) {
+ .grid-2, .grid-3, .grid-4 { grid-template-columns: 1fr; }
+}
+
+/* Piles verticales */
+.stack > * + * { margin-top: var(--space-4, 1rem); }
+.stack-sm > * + * { margin-top: var(--space-3, 0.75rem); }
+.stack-lg > * + * { margin-top: var(--space-6, 1.5rem); }
+
+/* Espacements rapides */
+.mt-0 { margin-top: 0 !important; }
+.mt-4 { margin-top: var(--space-4, 1rem) !important; }
+.mt-8 { margin-top: var(--space-8, 2rem) !important; }
+.mb-0 { margin-bottom: 0 !important; }
+.mb-4 { margin-bottom: var(--space-4, 1rem) !important; }
+.mb-8 { margin-bottom: var(--space-8, 2rem) !important; }
+
+/* Largeurs utiles */
+.max-w-prose { max-width: 65ch; }
+.max-w-wide { max-width: var(--container-max, 1100px); }
+.w-full { width: 100%; }
+
+/* Groupes de boutons */
+.btn-group {
+ display: inline-flex;
+ gap: 0.5rem;
+ flex-wrap: wrap;
+}
+
+/* Grilles rapides */
+.grid-2 { display: grid; gap: var(--space-6, 1.5rem); grid-template-columns: repeat(2, 1fr); }
+.grid-3 { display: grid; gap: var(--space-6, 1.5rem); grid-template-columns: repeat(3, 1fr); }
+.grid-4 { display: grid; gap: var(--space-6, 1.5rem); grid-template-columns: repeat(4, 1fr); }
+
+@media (max-width: 900px) {
+ .grid-3 { grid-template-columns: repeat(2, 1fr); }
+ .grid-4 { grid-template-columns: repeat(2, 1fr); }
+}
+@media (max-width: 640px) {
+ .grid-2, .grid-3, .grid-4 { grid-template-columns: 1fr; }
+}
+
+/* Piles verticales */
+.stack > * + * { margin-top: var(--space-4, 1rem); }
+.stack-sm > * + * { margin-top: var(--space-3, 0.75rem); }
+.stack-lg > * + * { margin-top: var(--space-6, 1.5rem); }
+
+/* Espacements rapides */
+.mt-0 { margin-top: 0 !important; }
+.mt-4 { margin-top: var(--space-4, 1rem) !important; }
+.mt-8 { margin-top: var(--space-8, 2rem) !important; }
+.mb-0 { margin-bottom: 0 !important; }
+.mb-4 { margin-bottom: var(--space-4, 1rem) !important; }
+.mb-8 { margin-bottom: var(--space-8, 2rem) !important; }
+
+/* Largeurs utiles */
+.max-w-prose { max-width: 65ch; }
+.max-w-wide { max-width: var(--container-max, 1100px); }
+.w-full { width: 100%; }
+
+/* Groupes de boutons */
+.btn-group {
+ display: inline-flex;
+ gap: 0.5rem;
+ flex-wrap: wrap;
+}
diff --git a/frontend/app/favicon.ico b/frontend/app/favicon.ico
new file mode 100644
index 0000000..718d6fe
Binary files /dev/null and b/frontend/app/favicon.ico differ
diff --git a/frontend/app/layout.tsx b/frontend/app/layout.tsx
new file mode 100644
index 0000000..ed9f3b5
--- /dev/null
+++ b/frontend/app/layout.tsx
@@ -0,0 +1,26 @@
+import "./css/colors.css";
+import "./css/typography.css";
+import "./css/globals.css";
+import "./css/components.css";
+import "./css/utilities.css";
+import "./css/forms.css";
+import "./css/header_MP.css";
+import "./css/links.css";
+
+import type { ReactNode } from "react";
+import Header from "./components/Header";
+
+export default function RootLayout({ children }: { children: ReactNode }) {
+ return (
+
+
+
+ {children}
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx
new file mode 100644
index 0000000..8a667ba
--- /dev/null
+++ b/frontend/app/page.tsx
@@ -0,0 +1,18 @@
+import Skills from "@/app/components/Skills";
+import Services from "@/app/components/Services";
+import Contact from "@/app/components/Contact";
+import About from "@/app/components/About";
+
+export default function Home() {
+ return (
+
+ )
+}
diff --git a/frontend/app/projects/new/page.tsx b/frontend/app/projects/new/page.tsx
new file mode 100644
index 0000000..85f997b
--- /dev/null
+++ b/frontend/app/projects/new/page.tsx
@@ -0,0 +1,87 @@
+"use client";
+
+import { useState } from "react";
+import { useRouter } from "next/navigation";
+
+export default function NewProjectPage() {
+ const [formData, setFormData] = useState({
+ name: "",
+ description: "",
+ technologies: "",
+ image: "",
+ url: "",
+ source: "",
+ status: "En cours",
+ });
+
+ const router = useRouter();
+
+ const handleChange = (e: React.ChangeEvent) => {
+ const { name, value } = e.target;
+ setFormData((prev) => ({ ...prev, [name]: value }));
+ };
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+
+ const res = await fetch("/api/projects", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(formData),
+ });
+
+ if (res.ok) {
+ router.push("/projects");
+ } else {
+ const error = await res.json();
+ alert(`Erreur lors de la tentative d'ajout d'un projet : ${error.error}`);
+ }
+ };
+
+ return (
+
+
Créer un projet
+
+ {Object.keys(formData).map((key) => (
+
+ {key.replace("_", " ")}
+ {key === "description" ? (
+
+ ) : key === "status" ? (
+
+ En cours
+ Terminé
+ Futur Projet
+
+ ) : (
+
+ )}
+
+ ))}
+
+ Sauvegarder
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/frontend/app/projects/page.tsx b/frontend/app/projects/page.tsx
new file mode 100644
index 0000000..38c335a
--- /dev/null
+++ b/frontend/app/projects/page.tsx
@@ -0,0 +1,31 @@
+async function getProjects() {
+ const res = await fetch("http://127.0.0.1:5000/api/projects/", {
+ cache: "no-store",
+ });
+
+ if(!res.ok){
+ throw new Error("Erreur lors de la récupération des projets depuis l'API");
+ }
+
+ return res.json();
+}
+
+export default async function ProjectsPage() {
+ const projects = await getProjects();
+
+ return(
+
+
+
Mes projets
+
+ {projects.map((p: any) => (
+
+ {p.name}
+ {p.description}
+
+ ))}
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/frontend/next.config.ts b/frontend/next.config.ts
new file mode 100644
index 0000000..e9ffa30
--- /dev/null
+++ b/frontend/next.config.ts
@@ -0,0 +1,7 @@
+import type { NextConfig } from "next";
+
+const nextConfig: NextConfig = {
+ /* config options here */
+};
+
+export default nextConfig;
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
new file mode 100644
index 0000000..889b2ce
--- /dev/null
+++ b/frontend/package-lock.json
@@ -0,0 +1,1736 @@
+{
+ "name": "frontend",
+ "version": "0.1.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "frontend",
+ "version": "0.1.0",
+ "dependencies": {
+ "next": "15.5.3",
+ "react": "19.1.0",
+ "react-dom": "19.1.0"
+ },
+ "devDependencies": {
+ "@tailwindcss/postcss": "^4",
+ "@types/node": "^20",
+ "@types/react": "^19",
+ "@types/react-dom": "^19",
+ "tailwindcss": "^4",
+ "typescript": "^5"
+ }
+ },
+ "node_modules/@alloc/quick-lru": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
+ "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@emnapi/runtime": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.5.0.tgz",
+ "integrity": "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@img/sharp-darwin-arm64": {
+ "version": "0.34.3",
+ "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.3.tgz",
+ "integrity": "sha512-ryFMfvxxpQRsgZJqBd4wsttYQbCxsJksrv9Lw/v798JcQ8+w84mBWuXwl+TT0WJ/WrYOLaYpwQXi3sA9nTIaIg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-darwin-arm64": "1.2.0"
+ }
+ },
+ "node_modules/@img/sharp-darwin-x64": {
+ "version": "0.34.3",
+ "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.3.tgz",
+ "integrity": "sha512-yHpJYynROAj12TA6qil58hmPmAwxKKC7reUqtGLzsOHfP7/rniNGTL8tjWX6L3CTV4+5P4ypcS7Pp+7OB+8ihA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-darwin-x64": "1.2.0"
+ }
+ },
+ "node_modules/@img/sharp-libvips-darwin-arm64": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.0.tgz",
+ "integrity": "sha512-sBZmpwmxqwlqG9ueWFXtockhsxefaV6O84BMOrhtg/YqbTaRdqDE7hxraVE3y6gVM4eExmfzW4a8el9ArLeEiQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-darwin-x64": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.0.tgz",
+ "integrity": "sha512-M64XVuL94OgiNHa5/m2YvEQI5q2cl9d/wk0qFTDVXcYzi43lxuiFTftMR1tOnFQovVXNZJ5TURSDK2pNe9Yzqg==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-arm": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.0.tgz",
+ "integrity": "sha512-mWd2uWvDtL/nvIzThLq3fr2nnGfyr/XMXlq8ZJ9WMR6PXijHlC3ksp0IpuhK6bougvQrchUAfzRLnbsen0Cqvw==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-arm64": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.0.tgz",
+ "integrity": "sha512-RXwd0CgG+uPRX5YYrkzKyalt2OJYRiJQ8ED/fi1tq9WQW2jsQIn0tqrlR5l5dr/rjqq6AHAxURhj2DVjyQWSOA==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-ppc64": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.0.tgz",
+ "integrity": "sha512-Xod/7KaDDHkYu2phxxfeEPXfVXFKx70EAFZ0qyUdOjCcxbjqyJOEUpDe6RIyaunGxT34Anf9ue/wuWOqBW2WcQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-s390x": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.0.tgz",
+ "integrity": "sha512-eMKfzDxLGT8mnmPJTNMcjfO33fLiTDsrMlUVcp6b96ETbnJmd4uvZxVJSKPQfS+odwfVaGifhsB07J1LynFehw==",
+ "cpu": [
+ "s390x"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-x64": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.0.tgz",
+ "integrity": "sha512-ZW3FPWIc7K1sH9E3nxIGB3y3dZkpJlMnkk7z5tu1nSkBoCgw2nSRTFHI5pB/3CQaJM0pdzMF3paf9ckKMSE9Tg==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linuxmusl-arm64": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.0.tgz",
+ "integrity": "sha512-UG+LqQJbf5VJ8NWJ5Z3tdIe/HXjuIdo4JeVNADXBFuG7z9zjoegpzzGIyV5zQKi4zaJjnAd2+g2nna8TZvuW9Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linuxmusl-x64": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.0.tgz",
+ "integrity": "sha512-SRYOLR7CXPgNze8akZwjoGBoN1ThNZoqpOgfnOxmWsklTGVfJiGJoC/Lod7aNMGA1jSsKWM1+HRX43OP6p9+6Q==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-linux-arm": {
+ "version": "0.34.3",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.3.tgz",
+ "integrity": "sha512-oBK9l+h6KBN0i3dC8rYntLiVfW8D8wH+NPNT3O/WBHeW0OQWCjfWksLUaPidsrDKpJgXp3G3/hkmhptAW0I3+A==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-arm": "1.2.0"
+ }
+ },
+ "node_modules/@img/sharp-linux-arm64": {
+ "version": "0.34.3",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.3.tgz",
+ "integrity": "sha512-QdrKe3EvQrqwkDrtuTIjI0bu6YEJHTgEeqdzI3uWJOH6G1O8Nl1iEeVYRGdj1h5I21CqxSvQp1Yv7xeU3ZewbA==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-arm64": "1.2.0"
+ }
+ },
+ "node_modules/@img/sharp-linux-ppc64": {
+ "version": "0.34.3",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.3.tgz",
+ "integrity": "sha512-GLtbLQMCNC5nxuImPR2+RgrviwKwVql28FWZIW1zWruy6zLgA5/x2ZXk3mxj58X/tszVF69KK0Is83V8YgWhLA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-ppc64": "1.2.0"
+ }
+ },
+ "node_modules/@img/sharp-linux-s390x": {
+ "version": "0.34.3",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.3.tgz",
+ "integrity": "sha512-3gahT+A6c4cdc2edhsLHmIOXMb17ltffJlxR0aC2VPZfwKoTGZec6u5GrFgdR7ciJSsHT27BD3TIuGcuRT0KmQ==",
+ "cpu": [
+ "s390x"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-s390x": "1.2.0"
+ }
+ },
+ "node_modules/@img/sharp-linux-x64": {
+ "version": "0.34.3",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.3.tgz",
+ "integrity": "sha512-8kYso8d806ypnSq3/Ly0QEw90V5ZoHh10yH0HnrzOCr6DKAPI6QVHvwleqMkVQ0m+fc7EH8ah0BB0QPuWY6zJQ==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-x64": "1.2.0"
+ }
+ },
+ "node_modules/@img/sharp-linuxmusl-arm64": {
+ "version": "0.34.3",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.3.tgz",
+ "integrity": "sha512-vAjbHDlr4izEiXM1OTggpCcPg9tn4YriK5vAjowJsHwdBIdx0fYRsURkxLG2RLm9gyBq66gwtWI8Gx0/ov+JKQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linuxmusl-arm64": "1.2.0"
+ }
+ },
+ "node_modules/@img/sharp-linuxmusl-x64": {
+ "version": "0.34.3",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.3.tgz",
+ "integrity": "sha512-gCWUn9547K5bwvOn9l5XGAEjVTTRji4aPTqLzGXHvIr6bIDZKNTA34seMPgM0WmSf+RYBH411VavCejp3PkOeQ==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linuxmusl-x64": "1.2.0"
+ }
+ },
+ "node_modules/@img/sharp-wasm32": {
+ "version": "0.34.3",
+ "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.3.tgz",
+ "integrity": "sha512-+CyRcpagHMGteySaWos8IbnXcHgfDn7pO2fiC2slJxvNq9gDipYBN42/RagzctVRKgxATmfqOSulgZv5e1RdMg==",
+ "cpu": [
+ "wasm32"
+ ],
+ "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/runtime": "^1.4.4"
+ },
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-win32-arm64": {
+ "version": "0.34.3",
+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.3.tgz",
+ "integrity": "sha512-MjnHPnbqMXNC2UgeLJtX4XqoVHHlZNd+nPt1kRPmj63wURegwBhZlApELdtxM2OIZDRv/DFtLcNhVbd1z8GYXQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "Apache-2.0 AND LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-win32-ia32": {
+ "version": "0.34.3",
+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.3.tgz",
+ "integrity": "sha512-xuCdhH44WxuXgOM714hn4amodJMZl3OEvf0GVTm0BEyMeA2to+8HEdRPShH0SLYptJY1uBw+SCFP9WVQi1Q/cw==",
+ "cpu": [
+ "ia32"
+ ],
+ "license": "Apache-2.0 AND LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-win32-x64": {
+ "version": "0.34.3",
+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.3.tgz",
+ "integrity": "sha512-OWwz05d++TxzLEv4VnsTz5CmZ6mI6S05sfQGEMrNrQcOEERbX46332IvE7pO/EUiw7jUrrS40z/M7kPyjfl04g==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "Apache-2.0 AND LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@isaacs/fs-minipass": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz",
+ "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "minipass": "^7.0.4"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.13",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/remapping": {
+ "version": "2.3.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.31",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@next/env": {
+ "version": "15.5.3",
+ "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.3.tgz",
+ "integrity": "sha512-RSEDTRqyihYXygx/OJXwvVupfr9m04+0vH8vyy0HfZ7keRto6VX9BbEk0J2PUk0VGy6YhklJUSrgForov5F9pw==",
+ "license": "MIT"
+ },
+ "node_modules/@next/swc-darwin-arm64": {
+ "version": "15.5.3",
+ "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.3.tgz",
+ "integrity": "sha512-nzbHQo69+au9wJkGKTU9lP7PXv0d1J5ljFpvb+LnEomLtSbJkbZyEs6sbF3plQmiOB2l9OBtN2tNSvCH1nQ9Jg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@next/swc-darwin-x64": {
+ "version": "15.5.3",
+ "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.3.tgz",
+ "integrity": "sha512-w83w4SkOOhekJOcA5HBvHyGzgV1W/XvOfpkrxIse4uPWhYTTRwtGEM4v/jiXwNSJvfRvah0H8/uTLBKRXlef8g==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@next/swc-linux-arm64-gnu": {
+ "version": "15.5.3",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.3.tgz",
+ "integrity": "sha512-+m7pfIs0/yvgVu26ieaKrifV8C8yiLe7jVp9SpcIzg7XmyyNE7toC1fy5IOQozmr6kWl/JONC51osih2RyoXRw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@next/swc-linux-arm64-musl": {
+ "version": "15.5.3",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.3.tgz",
+ "integrity": "sha512-u3PEIzuguSenoZviZJahNLgCexGFhso5mxWCrrIMdvpZn6lkME5vc/ADZG8UUk5K1uWRy4hqSFECrON6UKQBbQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@next/swc-linux-x64-gnu": {
+ "version": "15.5.3",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.3.tgz",
+ "integrity": "sha512-lDtOOScYDZxI2BENN9m0pfVPJDSuUkAD1YXSvlJF0DKwZt0WlA7T7o3wrcEr4Q+iHYGzEaVuZcsIbCps4K27sA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@next/swc-linux-x64-musl": {
+ "version": "15.5.3",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.3.tgz",
+ "integrity": "sha512-9vWVUnsx9PrY2NwdVRJ4dUURAQ8Su0sLRPqcCCxtX5zIQUBES12eRVHq6b70bbfaVaxIDGJN2afHui0eDm+cLg==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@next/swc-win32-arm64-msvc": {
+ "version": "15.5.3",
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.3.tgz",
+ "integrity": "sha512-1CU20FZzY9LFQigRi6jM45oJMU3KziA5/sSG+dXeVaTm661snQP6xu3ykGxxwU5sLG3sh14teO/IOEPVsQMRfA==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@next/swc-win32-x64-msvc": {
+ "version": "15.5.3",
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.3.tgz",
+ "integrity": "sha512-JMoLAq3n3y5tKXPQwCK5c+6tmwkuFDa2XAxz8Wm4+IVthdBZdZGh+lmiLUHg9f9IDwIQpUjp+ysd6OkYTyZRZw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@swc/helpers": {
+ "version": "0.5.15",
+ "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
+ "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.8.0"
+ }
+ },
+ "node_modules/@tailwindcss/node": {
+ "version": "4.1.13",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.13.tgz",
+ "integrity": "sha512-eq3ouolC1oEFOAvOMOBAmfCIqZBJuvWvvYWh5h5iOYfe1HFC6+GZ6EIL0JdM3/niGRJmnrOc+8gl9/HGUaaptw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/remapping": "^2.3.4",
+ "enhanced-resolve": "^5.18.3",
+ "jiti": "^2.5.1",
+ "lightningcss": "1.30.1",
+ "magic-string": "^0.30.18",
+ "source-map-js": "^1.2.1",
+ "tailwindcss": "4.1.13"
+ }
+ },
+ "node_modules/@tailwindcss/oxide": {
+ "version": "4.1.13",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.13.tgz",
+ "integrity": "sha512-CPgsM1IpGRa880sMbYmG1s4xhAy3xEt1QULgTJGQmZUeNgXFR7s1YxYygmJyBGtou4SyEosGAGEeYqY7R53bIA==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "dependencies": {
+ "detect-libc": "^2.0.4",
+ "tar": "^7.4.3"
+ },
+ "engines": {
+ "node": ">= 10"
+ },
+ "optionalDependencies": {
+ "@tailwindcss/oxide-android-arm64": "4.1.13",
+ "@tailwindcss/oxide-darwin-arm64": "4.1.13",
+ "@tailwindcss/oxide-darwin-x64": "4.1.13",
+ "@tailwindcss/oxide-freebsd-x64": "4.1.13",
+ "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.13",
+ "@tailwindcss/oxide-linux-arm64-gnu": "4.1.13",
+ "@tailwindcss/oxide-linux-arm64-musl": "4.1.13",
+ "@tailwindcss/oxide-linux-x64-gnu": "4.1.13",
+ "@tailwindcss/oxide-linux-x64-musl": "4.1.13",
+ "@tailwindcss/oxide-wasm32-wasi": "4.1.13",
+ "@tailwindcss/oxide-win32-arm64-msvc": "4.1.13",
+ "@tailwindcss/oxide-win32-x64-msvc": "4.1.13"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-android-arm64": {
+ "version": "4.1.13",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.13.tgz",
+ "integrity": "sha512-BrpTrVYyejbgGo57yc8ieE+D6VT9GOgnNdmh5Sac6+t0m+v+sKQevpFVpwX3pBrM2qKrQwJ0c5eDbtjouY/+ew==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-darwin-arm64": {
+ "version": "4.1.13",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.13.tgz",
+ "integrity": "sha512-YP+Jksc4U0KHcu76UhRDHq9bx4qtBftp9ShK/7UGfq0wpaP96YVnnjFnj3ZFrUAjc5iECzODl/Ts0AN7ZPOANQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-darwin-x64": {
+ "version": "4.1.13",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.13.tgz",
+ "integrity": "sha512-aAJ3bbwrn/PQHDxCto9sxwQfT30PzyYJFG0u/BWZGeVXi5Hx6uuUOQEI2Fa43qvmUjTRQNZnGqe9t0Zntexeuw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-freebsd-x64": {
+ "version": "4.1.13",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.13.tgz",
+ "integrity": "sha512-Wt8KvASHwSXhKE/dJLCCWcTSVmBj3xhVhp/aF3RpAhGeZ3sVo7+NTfgiN8Vey/Fi8prRClDs6/f0KXPDTZE6nQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
+ "version": "4.1.13",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.13.tgz",
+ "integrity": "sha512-mbVbcAsW3Gkm2MGwA93eLtWrwajz91aXZCNSkGTx/R5eb6KpKD5q8Ueckkh9YNboU8RH7jiv+ol/I7ZyQ9H7Bw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
+ "version": "4.1.13",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.13.tgz",
+ "integrity": "sha512-wdtfkmpXiwej/yoAkrCP2DNzRXCALq9NVLgLELgLim1QpSfhQM5+ZxQQF8fkOiEpuNoKLp4nKZ6RC4kmeFH0HQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-arm64-musl": {
+ "version": "4.1.13",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.13.tgz",
+ "integrity": "sha512-hZQrmtLdhyqzXHB7mkXfq0IYbxegaqTmfa1p9MBj72WPoDD3oNOh1Lnxf6xZLY9C3OV6qiCYkO1i/LrzEdW2mg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-x64-gnu": {
+ "version": "4.1.13",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.13.tgz",
+ "integrity": "sha512-uaZTYWxSXyMWDJZNY1Ul7XkJTCBRFZ5Fo6wtjrgBKzZLoJNrG+WderJwAjPzuNZOnmdrVg260DKwXCFtJ/hWRQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-x64-musl": {
+ "version": "4.1.13",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.13.tgz",
+ "integrity": "sha512-oXiPj5mi4Hdn50v5RdnuuIms0PVPI/EG4fxAfFiIKQh5TgQgX7oSuDWntHW7WNIi/yVLAiS+CRGW4RkoGSSgVQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-wasm32-wasi": {
+ "version": "4.1.13",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.13.tgz",
+ "integrity": "sha512-+LC2nNtPovtrDwBc/nqnIKYh/W2+R69FA0hgoeOn64BdCX522u19ryLh3Vf3F8W49XBcMIxSe665kwy21FkhvA==",
+ "bundleDependencies": [
+ "@napi-rs/wasm-runtime",
+ "@emnapi/core",
+ "@emnapi/runtime",
+ "@tybys/wasm-util",
+ "@emnapi/wasi-threads",
+ "tslib"
+ ],
+ "cpu": [
+ "wasm32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/core": "^1.4.5",
+ "@emnapi/runtime": "^1.4.5",
+ "@emnapi/wasi-threads": "^1.0.4",
+ "@napi-rs/wasm-runtime": "^0.2.12",
+ "@tybys/wasm-util": "^0.10.0",
+ "tslib": "^2.8.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
+ "version": "4.1.13",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.13.tgz",
+ "integrity": "sha512-dziTNeQXtoQ2KBXmrjCxsuPk3F3CQ/yb7ZNZNA+UkNTeiTGgfeh+gH5Pi7mRncVgcPD2xgHvkFCh/MhZWSgyQg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-win32-x64-msvc": {
+ "version": "4.1.13",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.13.tgz",
+ "integrity": "sha512-3+LKesjXydTkHk5zXX01b5KMzLV1xl2mcktBJkje7rhFUpUlYJy7IMOLqjIRQncLTa1WZZiFY/foAeB5nmaiTw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tailwindcss/postcss": {
+ "version": "4.1.13",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.13.tgz",
+ "integrity": "sha512-HLgx6YSFKJT7rJqh9oJs/TkBFhxuMOfUKSBEPYwV+t78POOBsdQ7crhZLzwcH3T0UyUuOzU/GK5pk5eKr3wCiQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@alloc/quick-lru": "^5.2.0",
+ "@tailwindcss/node": "4.1.13",
+ "@tailwindcss/oxide": "4.1.13",
+ "postcss": "^8.4.41",
+ "tailwindcss": "4.1.13"
+ }
+ },
+ "node_modules/@types/node": {
+ "version": "20.19.14",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.14.tgz",
+ "integrity": "sha512-gqiKWld3YIkmtrrg9zDvg9jfksZCcPywXVN7IauUGhilwGV/yOyeUsvpR796m/Jye0zUzMXPKe8Ct1B79A7N5Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~6.21.0"
+ }
+ },
+ "node_modules/@types/react": {
+ "version": "19.1.13",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.13.tgz",
+ "integrity": "sha512-hHkbU/eoO3EG5/MZkuFSKmYqPbSVk5byPFa3e7y/8TybHiLMACgI8seVYlicwk7H5K/rI2px9xrQp/C+AUDTiQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "csstype": "^3.0.2"
+ }
+ },
+ "node_modules/@types/react-dom": {
+ "version": "19.1.9",
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.9.tgz",
+ "integrity": "sha512-qXRuZaOsAdXKFyOhRBg6Lqqc0yay13vN7KrIg4L7N4aaHN68ma9OK3NE1BoDFgFOTfM7zg+3/8+2n8rLUH3OKQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "^19.0.0"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001741",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001741.tgz",
+ "integrity": "sha512-QGUGitqsc8ARjLdgAfxETDhRbJ0REsP6O3I96TAth/mVjh2cYzN2u+3AzPP3aVSm2FehEItaJw1xd+IGBXWeSw==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "CC-BY-4.0"
+ },
+ "node_modules/chownr": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz",
+ "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/client-only": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
+ "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
+ "license": "MIT"
+ },
+ "node_modules/color": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
+ "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "color-convert": "^2.0.1",
+ "color-string": "^1.9.0"
+ },
+ "engines": {
+ "node": ">=12.5.0"
+ }
+ },
+ "node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/color-string": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz",
+ "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "color-name": "^1.0.0",
+ "simple-swizzle": "^0.2.2"
+ }
+ },
+ "node_modules/csstype": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
+ "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/detect-libc": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.0.tgz",
+ "integrity": "sha512-vEtk+OcP7VBRtQZ1EJ3bdgzSfBjgnEalLTp5zjJrS+2Z1w2KZly4SBdac/WDU3hhsNAZ9E8SC96ME4Ey8MZ7cg==",
+ "devOptional": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/enhanced-resolve": {
+ "version": "5.18.3",
+ "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz",
+ "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.2.4",
+ "tapable": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/graceful-fs": {
+ "version": "4.2.11",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
+ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/is-arrayish": {
+ "version": "0.3.4",
+ "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz",
+ "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==",
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/jiti": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.5.1.tgz",
+ "integrity": "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jiti": "lib/jiti-cli.mjs"
+ }
+ },
+ "node_modules/lightningcss": {
+ "version": "1.30.1",
+ "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz",
+ "integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==",
+ "dev": true,
+ "license": "MPL-2.0",
+ "dependencies": {
+ "detect-libc": "^2.0.3"
+ },
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ },
+ "optionalDependencies": {
+ "lightningcss-darwin-arm64": "1.30.1",
+ "lightningcss-darwin-x64": "1.30.1",
+ "lightningcss-freebsd-x64": "1.30.1",
+ "lightningcss-linux-arm-gnueabihf": "1.30.1",
+ "lightningcss-linux-arm64-gnu": "1.30.1",
+ "lightningcss-linux-arm64-musl": "1.30.1",
+ "lightningcss-linux-x64-gnu": "1.30.1",
+ "lightningcss-linux-x64-musl": "1.30.1",
+ "lightningcss-win32-arm64-msvc": "1.30.1",
+ "lightningcss-win32-x64-msvc": "1.30.1"
+ }
+ },
+ "node_modules/lightningcss-darwin-arm64": {
+ "version": "1.30.1",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.1.tgz",
+ "integrity": "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-x64": {
+ "version": "1.30.1",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.1.tgz",
+ "integrity": "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-freebsd-x64": {
+ "version": "1.30.1",
+ "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.1.tgz",
+ "integrity": "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm-gnueabihf": {
+ "version": "1.30.1",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.1.tgz",
+ "integrity": "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-gnu": {
+ "version": "1.30.1",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.1.tgz",
+ "integrity": "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-musl": {
+ "version": "1.30.1",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.1.tgz",
+ "integrity": "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-gnu": {
+ "version": "1.30.1",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.1.tgz",
+ "integrity": "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-musl": {
+ "version": "1.30.1",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.1.tgz",
+ "integrity": "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-arm64-msvc": {
+ "version": "1.30.1",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.1.tgz",
+ "integrity": "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-x64-msvc": {
+ "version": "1.30.1",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.1.tgz",
+ "integrity": "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/magic-string": {
+ "version": "0.30.19",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz",
+ "integrity": "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.5"
+ }
+ },
+ "node_modules/minipass": {
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
+ "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ }
+ },
+ "node_modules/minizlib": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz",
+ "integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "minipass": "^7.1.2"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
+ "node_modules/mkdirp": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz",
+ "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "mkdirp": "dist/cjs/src/bin.js"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.11",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/next": {
+ "version": "15.5.3",
+ "resolved": "https://registry.npmjs.org/next/-/next-15.5.3.tgz",
+ "integrity": "sha512-r/liNAx16SQj4D+XH/oI1dlpv9tdKJ6cONYPwwcCC46f2NjpaRWY+EKCzULfgQYV6YKXjHBchff2IZBSlZmJNw==",
+ "license": "MIT",
+ "dependencies": {
+ "@next/env": "15.5.3",
+ "@swc/helpers": "0.5.15",
+ "caniuse-lite": "^1.0.30001579",
+ "postcss": "8.4.31",
+ "styled-jsx": "5.1.6"
+ },
+ "bin": {
+ "next": "dist/bin/next"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^19.8.0 || >= 20.0.0"
+ },
+ "optionalDependencies": {
+ "@next/swc-darwin-arm64": "15.5.3",
+ "@next/swc-darwin-x64": "15.5.3",
+ "@next/swc-linux-arm64-gnu": "15.5.3",
+ "@next/swc-linux-arm64-musl": "15.5.3",
+ "@next/swc-linux-x64-gnu": "15.5.3",
+ "@next/swc-linux-x64-musl": "15.5.3",
+ "@next/swc-win32-arm64-msvc": "15.5.3",
+ "@next/swc-win32-x64-msvc": "15.5.3",
+ "sharp": "^0.34.3"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.1.0",
+ "@playwright/test": "^1.51.1",
+ "babel-plugin-react-compiler": "*",
+ "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0",
+ "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0",
+ "sass": "^1.3.0"
+ },
+ "peerDependenciesMeta": {
+ "@opentelemetry/api": {
+ "optional": true
+ },
+ "@playwright/test": {
+ "optional": true
+ },
+ "babel-plugin-react-compiler": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/next/node_modules/postcss": {
+ "version": "8.4.31",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
+ "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.6",
+ "picocolors": "^1.0.0",
+ "source-map-js": "^1.0.2"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "license": "ISC"
+ },
+ "node_modules/postcss": {
+ "version": "8.5.6",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
+ "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.11",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/react": {
+ "version": "19.1.0",
+ "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
+ "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "19.1.0",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz",
+ "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==",
+ "license": "MIT",
+ "dependencies": {
+ "scheduler": "^0.26.0"
+ },
+ "peerDependencies": {
+ "react": "^19.1.0"
+ }
+ },
+ "node_modules/scheduler": {
+ "version": "0.26.0",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz",
+ "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==",
+ "license": "MIT"
+ },
+ "node_modules/semver": {
+ "version": "7.7.2",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
+ "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
+ "license": "ISC",
+ "optional": true,
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/sharp": {
+ "version": "0.34.3",
+ "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.3.tgz",
+ "integrity": "sha512-eX2IQ6nFohW4DbvHIOLRB3MHFpYqaqvXd3Tp5e/T/dSH83fxaNJQRvDMhASmkNTsNTVF2/OOopzRCt7xokgPfg==",
+ "hasInstallScript": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "dependencies": {
+ "color": "^4.2.3",
+ "detect-libc": "^2.0.4",
+ "semver": "^7.7.2"
+ },
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-darwin-arm64": "0.34.3",
+ "@img/sharp-darwin-x64": "0.34.3",
+ "@img/sharp-libvips-darwin-arm64": "1.2.0",
+ "@img/sharp-libvips-darwin-x64": "1.2.0",
+ "@img/sharp-libvips-linux-arm": "1.2.0",
+ "@img/sharp-libvips-linux-arm64": "1.2.0",
+ "@img/sharp-libvips-linux-ppc64": "1.2.0",
+ "@img/sharp-libvips-linux-s390x": "1.2.0",
+ "@img/sharp-libvips-linux-x64": "1.2.0",
+ "@img/sharp-libvips-linuxmusl-arm64": "1.2.0",
+ "@img/sharp-libvips-linuxmusl-x64": "1.2.0",
+ "@img/sharp-linux-arm": "0.34.3",
+ "@img/sharp-linux-arm64": "0.34.3",
+ "@img/sharp-linux-ppc64": "0.34.3",
+ "@img/sharp-linux-s390x": "0.34.3",
+ "@img/sharp-linux-x64": "0.34.3",
+ "@img/sharp-linuxmusl-arm64": "0.34.3",
+ "@img/sharp-linuxmusl-x64": "0.34.3",
+ "@img/sharp-wasm32": "0.34.3",
+ "@img/sharp-win32-arm64": "0.34.3",
+ "@img/sharp-win32-ia32": "0.34.3",
+ "@img/sharp-win32-x64": "0.34.3"
+ }
+ },
+ "node_modules/simple-swizzle": {
+ "version": "0.2.4",
+ "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz",
+ "integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "is-arrayish": "^0.3.1"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/styled-jsx": {
+ "version": "5.1.6",
+ "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz",
+ "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==",
+ "license": "MIT",
+ "dependencies": {
+ "client-only": "0.0.1"
+ },
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "peerDependencies": {
+ "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0"
+ },
+ "peerDependenciesMeta": {
+ "@babel/core": {
+ "optional": true
+ },
+ "babel-plugin-macros": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/tailwindcss": {
+ "version": "4.1.13",
+ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.13.tgz",
+ "integrity": "sha512-i+zidfmTqtwquj4hMEwdjshYYgMbOrPzb9a0M3ZgNa0JMoZeFC6bxZvO8yr8ozS6ix2SDz0+mvryPeBs2TFE+w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tapable": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.3.tgz",
+ "integrity": "sha512-ZL6DDuAlRlLGghwcfmSn9sK3Hr6ArtyudlSAiCqQ6IfE+b+HHbydbYDIG15IfS5do+7XQQBdBiubF/cV2dnDzg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ }
+ },
+ "node_modules/tar": {
+ "version": "7.4.3",
+ "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz",
+ "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "@isaacs/fs-minipass": "^4.0.0",
+ "chownr": "^3.0.0",
+ "minipass": "^7.1.2",
+ "minizlib": "^3.0.1",
+ "mkdirp": "^3.0.1",
+ "yallist": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "license": "0BSD"
+ },
+ "node_modules/typescript": {
+ "version": "5.9.2",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz",
+ "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
+ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/yallist": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz",
+ "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": ">=18"
+ }
+ }
+ }
+}
diff --git a/frontend/package.json b/frontend/package.json
new file mode 100644
index 0000000..18165ea
--- /dev/null
+++ b/frontend/package.json
@@ -0,0 +1,23 @@
+{
+ "name": "frontend",
+ "version": "0.1.0",
+ "private": true,
+ "scripts": {
+ "dev": "next dev --turbopack",
+ "build": "next build --turbopack",
+ "start": "next start"
+ },
+ "dependencies": {
+ "react": "19.1.0",
+ "react-dom": "19.1.0",
+ "next": "15.5.3"
+ },
+ "devDependencies": {
+ "typescript": "^5",
+ "@types/node": "^20",
+ "@types/react": "^19",
+ "@types/react-dom": "^19",
+ "@tailwindcss/postcss": "^4",
+ "tailwindcss": "^4"
+ }
+}
diff --git a/frontend/postcss.config.mjs b/frontend/postcss.config.mjs
new file mode 100644
index 0000000..c7bcb4b
--- /dev/null
+++ b/frontend/postcss.config.mjs
@@ -0,0 +1,5 @@
+const config = {
+ plugins: ["@tailwindcss/postcss"],
+};
+
+export default config;
diff --git a/frontend/public/file.svg b/frontend/public/file.svg
new file mode 100644
index 0000000..004145c
--- /dev/null
+++ b/frontend/public/file.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/public/globe.svg b/frontend/public/globe.svg
new file mode 100644
index 0000000..567f17b
--- /dev/null
+++ b/frontend/public/globe.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/public/next.svg b/frontend/public/next.svg
new file mode 100644
index 0000000..5174b28
--- /dev/null
+++ b/frontend/public/next.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/public/vercel.svg b/frontend/public/vercel.svg
new file mode 100644
index 0000000..7705396
--- /dev/null
+++ b/frontend/public/vercel.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/public/window.svg b/frontend/public/window.svg
new file mode 100644
index 0000000..b2b2a44
--- /dev/null
+++ b/frontend/public/window.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json
new file mode 100644
index 0000000..d8b9323
--- /dev/null
+++ b/frontend/tsconfig.json
@@ -0,0 +1,27 @@
+{
+ "compilerOptions": {
+ "target": "ES2017",
+ "lib": ["dom", "dom.iterable", "esnext"],
+ "allowJs": true,
+ "skipLibCheck": true,
+ "strict": true,
+ "noEmit": true,
+ "esModuleInterop": true,
+ "module": "esnext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "jsx": "preserve",
+ "incremental": true,
+ "plugins": [
+ {
+ "name": "next"
+ }
+ ],
+ "paths": {
+ "@/*": ["./*"]
+ }
+ },
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
+ "exclude": ["node_modules"]
+}
diff --git a/index.html b/index.html
deleted file mode 100644
index 35db15c..0000000
--- a/index.html
+++ /dev/null
@@ -1,54 +0,0 @@
-
-
-
-
-
-
-
-
-
- Anthony VIOLET | Consultant en développement web & applications
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file