first commit
This commit is contained in:
parent
b216a187bd
commit
f73c77f548
119 changed files with 4504 additions and 4829 deletions
BIN
.DS_Store
vendored
BIN
.DS_Store
vendored
Binary file not shown.
45
.gitignore
vendored
45
.gitignore
vendored
|
|
@ -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
|
||||
|
|
|
|||
BIN
Static/.DS_Store
vendored
BIN
Static/.DS_Store
vendored
Binary file not shown.
1130
Static/css/admin.css
1130
Static/css/admin.css
File diff suppressed because it is too large
Load diff
|
|
@ -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,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1440 320"><path fill="white" fill-opacity="1" d="M0,96L40,112C80,128,160,160,240,160C320,160,400,128,480,117.3C560,107,640,117,720,144C800,171,880,213,960,218.7C1040,224,1120,192,1200,186.7C1280,181,1360,203,1400,213.3L1440,224L1440,320L1400,320C1360,320,1280,320,1200,320C1120,320,1040,320,960,320C880,320,800,320,720,320C640,320,560,320,480,320C400,320,320,320,240,320C160,320,80,320,40,320L0,320Z"></path></svg>');
|
||||
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);
|
||||
}
|
||||
|
|
@ -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,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1440 320"><path fill="white" fill-opacity="1" d="M0,96L40,112C80,128,160,160,240,160C320,160,400,128,480,117.3C560,107,640,117,720,144C800,171,880,213,960,218.7C1040,224,1120,192,1200,186.7C1280,181,1360,203,1400,213.3L1440,224L1440,320L1400,320C1360,320,1280,320,1200,320C1120,320,1040,320,960,320C880,320,800,320,720,320C640,320,560,320,480,320C400,320,320,320,240,320C160,320,80,320,40,320L0,320Z"></path></svg>');
|
||||
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;
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.2 MiB |
|
|
@ -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 = `
|
||||
<div class="card-title">
|
||||
<h3>${project.type}</h3>
|
||||
</div>
|
||||
<div class="action-btn">
|
||||
<button class="btn-small btn-danger" data-id="delete-project" data-project-id="${project.id}">Supprimer</button>
|
||||
<button class="btn-small btn-warning" data-id="edit-project" data-project-id="${project.id}">Modifier</button>
|
||||
</div>
|
||||
<p>Etat : ${state}</p>
|
||||
<h3>${project.name}</h3>
|
||||
<p>${project.description}</p>
|
||||
${project.technologies && project.technologies.length > 0 ? `
|
||||
<div class="card-tags">
|
||||
${project.technologies.map(tech => `<span class="tag">${tech}</span>`).join('')}
|
||||
</div>` : ''}
|
||||
${project.link ? `<center><a href="${project.link}" target="_blank" class="btn-primary">Voir le projet</a></center>` : ''}
|
||||
`;
|
||||
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' });
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
|
|
@ -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 = `
|
||||
<div class="project-card-header">
|
||||
<h3>${project.type}</h3>
|
||||
</div>
|
||||
<div class="project-card-content">
|
||||
<h4>${project.name}</h4>
|
||||
<p>${project.description}</p>
|
||||
${project.link ? `<center><a href="${project.link}" target="_blank" class="btn">Voir le projet</a></center>` : ''}
|
||||
<p></p>
|
||||
<div class="project-tags">
|
||||
${project.technologies && project.technologies.length > 0 ?
|
||||
project.technologies.map(tech => `<span class="tag">${tech}</span>`).join('') : ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
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 = `<div style="text-align: center; padding: 2rem;">
|
||||
<h3 style="color: var(--success-color);">Message envoyé avec succès!</h3>
|
||||
<p>Merci pour votre message. Je vous répondrai dans les plus brefs délais.</p>
|
||||
</div>`;
|
||||
}, 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');
|
||||
});
|
||||
|
|
@ -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 };
|
||||
|
|
@ -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 = `
|
||||
<div class="tech-controls">
|
||||
<div class="tech-search">
|
||||
<input type="text" id="tech-search" placeholder="Rechercher une technologie...">
|
||||
<span class="search-icon">🔍</span>
|
||||
</div>
|
||||
<div class="tech-quick-actions">
|
||||
<button type="button" class="btn-quick" id="select-all">Tout sélectionner</button>
|
||||
<button type="button" class="btn-quick" id="deselect-all">Tout désélectionner</button>
|
||||
<button type="button" class="btn-quick" id="toggle-categories">Replier/Déplier</button>
|
||||
</div>
|
||||
<div class="selected-count">
|
||||
<span id="count-display">0 technologie(s) sélectionnée(s)</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tech-categories">
|
||||
`;
|
||||
|
||||
for (const [categoryKey, category] of Object.entries(this.technologies)) {
|
||||
html += `
|
||||
<div class="tech-category" data-category="${categoryKey}">
|
||||
<div class="category-header" data-toggle="${categoryKey}">
|
||||
<h4>${category.label}</h4>
|
||||
<span class="category-toggle">▼</span>
|
||||
<span class="category-count">(${category.items.length})</span>
|
||||
</div>
|
||||
<div class="category-items" id="category-${categoryKey}">
|
||||
`;
|
||||
|
||||
category.items.forEach(tech => {
|
||||
const id = this.generateId(tech);
|
||||
html += `
|
||||
<div class="tech-item">
|
||||
<input type="checkbox" id="${id}" name="tags[]" value="${tech}" class="tech-checkbox">
|
||||
<label for="${id}" class="tech-label">${tech}</label>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
html += `
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
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;
|
||||
}
|
||||
312
Views/about.html
312
Views/about.html
|
|
@ -1,312 +0,0 @@
|
|||
<section class="section">
|
||||
<h1>À propos de moi</h1>
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 2rem; align-items: flex-start;">
|
||||
<div style="flex: 1 1 300px;">
|
||||
<p>Je suis Anthony VIOLET, développeur web et designer freelance avec une passion pour la création d'expériences numériques innovantes.</p>
|
||||
<p>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.</p>
|
||||
<p>Mon approche du développement est centrée sur l'utilisateur, avec un focus sur la performance, l'accessibilité et l'esthétique.</p>
|
||||
</div>
|
||||
<div style="flex: 1 1 300px;">
|
||||
<h3>Mon parcours</h3>
|
||||
<p>Diplômé en développement web et multimédia, j'ai travaillé sur divers projets allant de sites vitrines à des applications web complexes.</p>
|
||||
<p>Ma curiosité et ma volonté d'apprentissage me poussent à me tenir constamment informé des dernières tendances et technologies du web.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<h2>Mes valeurs</h2>
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 1.5rem; margin-top: 1rem;">
|
||||
<div style="flex: 1 1 200px; background-color: var(--light-color); padding: 1.5rem; border-radius: var(--border-radius); box-shadow: 0 2px 4px rgba(0,0,0,0.05);">
|
||||
<h3>Qualité</h3>
|
||||
<p>Je m'engage à livrer un travail soigné et de haute qualité, respectant les standards du web et les bonnes pratiques.</p>
|
||||
</div>
|
||||
<div style="flex: 1 1 200px; background-color: var(--light-color); padding: 1.5rem; border-radius: var(--border-radius); box-shadow: 0 2px 4px rgba(0,0,0,0.05);">
|
||||
<h3>Innovation</h3>
|
||||
<p>Je cherche constamment à explorer de nouvelles solutions et approches pour résoudre les défis de conception et de développement.</p>
|
||||
</div>
|
||||
<div style="flex: 1 1 200px; background-color: var(--light-color); padding: 1.5rem; border-radius: var(--border-radius); box-shadow: 0 2px 4px rgba(0,0,0,0.05);">
|
||||
<h3>Communication</h3>
|
||||
<p>Je privilégie une communication claire et transparente tout au long du processus de création.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<h2>Mes compétences</h2>
|
||||
<div style="margin-top: 2rem;">
|
||||
<!-- Python et ses frameworks -->
|
||||
<h3 style="color: var(--primary-color)">Python</h3>
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 1.5rem; margin: 1rem 0 2rem 0;">
|
||||
<div style="flex: 1;">
|
||||
<div style="display: flex; align-items: center; margin-bottom: 0.8rem;">
|
||||
<div style="width: 150px; font-weight: 500;">NumPy</div>
|
||||
<div style="flex-grow: 1; height: 8px; background-color: #e0e0e0; border-radius: 4px; overflow: hidden;">
|
||||
<div style="width: 85%; height: 100%; background-color: var(--primary-color);"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; margin-bottom: 0.8rem;">
|
||||
<div style="width: 150px; font-weight: 500;">Pandas</div>
|
||||
<div style="flex-grow: 1; height: 8px; background-color: #e0e0e0; border-radius: 4px; overflow: hidden;">
|
||||
<div style="width: 90%; height: 100%; background-color: var(--primary-color);"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; margin-bottom: 0.8rem;">
|
||||
<div style="width: 150px; font-weight: 500;">SciKit-Learn</div>
|
||||
<div style="flex-grow: 1; height: 8px; background-color: #e0e0e0; border-radius: 4px; overflow: hidden;">
|
||||
<div style="width: 75%; height: 100%; background-color: var(--primary-color);"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- JavaScript et ses frameworks -->
|
||||
<h3 style="color: var(--secondary-color)">JavaScript</h3>
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 1.5rem; margin: 1rem 0 2rem 0;">
|
||||
<div style="flex: 1;">
|
||||
<div style="display: flex; align-items: center; margin-bottom: 0.8rem;">
|
||||
<div style="width: 150px; font-weight: 500;">Node.JS</div>
|
||||
<div style="flex-grow: 1; height: 8px; background-color: #e0e0e0; border-radius: 4px; overflow: hidden;">
|
||||
<div style="width: 85%; height: 100%; background-color: var(--secondary-color);"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; margin-bottom: 0.8rem;">
|
||||
<div style="width: 150px; font-weight: 500;">React/JS</div>
|
||||
<div style="flex-grow: 1; height: 8px; background-color: #e0e0e0; border-radius: 4px; overflow: hidden;">
|
||||
<div style="width: 70%; height: 100%; background-color: var(--secondary-color);"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; margin-bottom: 0.8rem;">
|
||||
<div style="width: 150px; font-weight: 500;">Angular</div>
|
||||
<div style="flex-grow: 1; height: 8px; background-color: #e0e0e0; border-radius: 4px; overflow: hidden;">
|
||||
<div style="width: 60%; height: 100%; background-color: var(--secondary-color);"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; margin-bottom: 0.8rem;">
|
||||
<div style="width: 150px; font-weight: 500;">Laravel</div>
|
||||
<div style="flex-grow: 1; height: 8px; background-color: #e0e0e0; border-radius: 4px; overflow: hidden;">
|
||||
<div style="width: 50%; height: 100%; background-color: var(--secondary-color);"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- PHP et ses frameworks -->
|
||||
<h3 style="color: var(--accent-color)">PHP</h3>
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 1.5rem; margin: 1rem 0 2rem 0;">
|
||||
<div style="flex: 1;">
|
||||
<div style="display: flex; align-items: center; margin-bottom: 0.8rem;">
|
||||
<div style="width: 150px; font-weight: 500;">Symfony</div>
|
||||
<div style="flex-grow: 1; height: 8px; background-color: #e0e0e0; border-radius: 4px; overflow: hidden;">
|
||||
<div style="width: 85%; height: 100%; background-color: var(--accent-color);"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; margin-bottom: 0.8rem;">
|
||||
<div style="width: 150px; font-weight: 500;">Procédural & OO</div>
|
||||
<div style="flex-grow: 1; height: 8px; background-color: #e0e0e0; border-radius: 4px; overflow: hidden;">
|
||||
<div style="width: 90%; height: 100%; background-color: var(--accent-color);"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- C# -->
|
||||
<h3 style="color: var(--success-color)">C#</h3>
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 1.5rem; margin: 1rem 0 2rem 0;">
|
||||
<div style="flex: 1;">
|
||||
<div style="display: flex; align-items: center; margin-bottom: 0.8rem;">
|
||||
<div style="width: 150px; font-weight: 500;">Unity</div>
|
||||
<div style="flex-grow: 1; height: 8px; background-color: #e0e0e0; border-radius: 4px; overflow: hidden;">
|
||||
<div style="width: 65%; height: 100%; background-color: var(--success-color);"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; margin-bottom: 0.8rem;">
|
||||
<div style="width: 150px; font-weight: 500;">Godot</div>
|
||||
<div style="flex-grow: 1; height: 8px; background-color: #e0e0e0; border-radius: 4px; overflow: hidden;">
|
||||
<div style="width: 80%; height: 100%; background-color: var(--success-color);"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; margin-bottom: 0.8rem;">
|
||||
<div style="width: 150px; font-weight: 500;">Logiciels</div>
|
||||
<div style="flex-grow: 1; height: 8px; background-color: #e0e0e0; border-radius: 4px; overflow: hidden;">
|
||||
<div style="width: 30%; height: 100%; background-color: var(--success-color);"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Langues -->
|
||||
<div style="margin-top: 2rem;">
|
||||
<h3>Langues</h3>
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 2rem; margin: 1rem 0;">
|
||||
<div style="flex: 1 1 200px; text-align: center;">
|
||||
<h4>Français</h4>
|
||||
<div style="position: relative; width: 100px; height: 100px; border-radius: 50%; background: conic-gradient(var(--accent-color) 100%, #e0e0e0 0); margin: 10px auto;">
|
||||
<div style="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;">
|
||||
<span style="font-weight: bold; font-size: 1.2rem;">100%</span>
|
||||
</div>
|
||||
</div>
|
||||
<p>Natif</p>
|
||||
</div>
|
||||
<div style="flex: 1 1 200px; text-align: center;">
|
||||
<h4>Anglais</h4>
|
||||
<div style="position: relative; width: 100px; height: 100px; border-radius: 50%; background: conic-gradient(var(--accent-color) 30%, #e0e0e0 0); margin: 10px auto;">
|
||||
<div style="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;">
|
||||
<span style="font-weight: bold; font-size: 1.2rem;">30%</span>
|
||||
</div>
|
||||
</div>
|
||||
<p>Basique</p>
|
||||
</div>
|
||||
<div style="flex: 1 1 200px; text-align: center;">
|
||||
<h4>Espagnol</h4>
|
||||
<div style="position: relative; width: 100px; height: 100px; border-radius: 50%; background: conic-gradient(var(--accent-color) 70%, #e0e0e0 0); margin: 10px auto;">
|
||||
<div style="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;">
|
||||
<span style="font-weight: bold; font-size: 1.2rem;">70%</span>
|
||||
</div>
|
||||
</div>
|
||||
<p>Intermédiaire</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Technologies préférées (badges) -->
|
||||
<h3 style="margin-top: 2rem;">Technologies utilisées</h3>
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 1rem; margin-top: 1rem;">
|
||||
<div style="flex: 0 0 auto; padding: 0.5rem 1rem; background-color: var(--primary-color); color: white; border-radius: 20px;">HTML5/CSS3</div>
|
||||
<div style="flex: 0 0 auto; padding: 0.5rem 1rem; background-color: var(--primary-color); color: white; border-radius: 20px;">JavaScript</div>
|
||||
<div style="flex: 0 0 auto; padding: 0.5rem 1rem; background-color: var(--primary-color); color: white; border-radius: 20px;">Python</div>
|
||||
<div style="flex: 0 0 auto; padding: 0.5rem 1rem; background-color: var(--primary-color); color: white; border-radius: 20px;">PHP</div>
|
||||
<div style="flex: 0 0 auto; padding: 0.5rem 1rem; background-color: var(--primary-color); color: white; border-radius: 20px;">SQL</div>
|
||||
<div style="flex: 0 0 auto; padding: 0.5rem 1rem; background-color: var(--primary-color); color: white; border-radius: 20px;">Node.js</div>
|
||||
<div style="flex: 0 0 auto; padding: 0.5rem 1rem; background-color: var(--primary-color); color: white; border-radius: 20px;">React</div>
|
||||
<div style="flex: 0 0 auto; padding: 0.5rem 1rem; background-color: var(--primary-color); color: white; border-radius: 20px;">Angular</div>
|
||||
<div style="flex: 0 0 auto; padding: 0.5rem 1rem; background-color: var(--primary-color); color: white; border-radius: 20px;">Pandas</div>
|
||||
<div style="flex: 0 0 auto; padding: 0.5rem 1rem; background-color: var(--primary-color); color: white; border-radius: 20px;">NumPy</div>
|
||||
<div style="flex: 0 0 auto; padding: 0.5rem 1rem; background-color: var(--primary-color); color: white; border-radius: 20px;">Symfony</div>
|
||||
<div style="flex: 0 0 auto; padding: 0.5rem 1rem; background-color: var(--primary-color); color: white; border-radius: 20px;">Laravel</div>
|
||||
<div style="flex: 0 0 auto; padding: 0.5rem 1rem; background-color: var(--primary-color); color: white; border-radius: 20px;">SciKit-Learn</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<h2>Contactez-moi</h2>
|
||||
<p>Vous souhaitez discuter d'un projet ou simplement en savoir plus sur mon travail?</p>
|
||||
<a id="contact" class="btn">Envoyez-moi un message</a>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<h2>Dernières expériences professionnelles</h2>
|
||||
<div style="position: relative; padding-left: 30px; margin-left: 20px; border-left: 2px solid var(--primary-color);">
|
||||
<!-- Expérience TipsIt -->
|
||||
<div style="position: relative; margin-bottom: 2.5rem;">
|
||||
<div style="position: absolute; width: 20px; height: 20px; background-color: var(--primary-color); border-radius: 50%; left: -41px; top: 0;"></div>
|
||||
<h3>Stagiaire chez Tryptik</h3>
|
||||
<p style="color: var(--primary-color); margin: 0.3rem 0;">11/2024 - 12/2024</p>
|
||||
<p style="margin-top: 0.8rem;">Développement application, maintien ambiance SCRUM, développement web.</p>
|
||||
<div style="margin-top: 0.8rem; display: flex; flex-wrap: wrap; gap: 0.5rem;">
|
||||
<span style="background-color: var(--light-color); padding: 0.2rem 0.5rem; border-radius: 4px; font-size: 0.8rem;">SCRUM</span>
|
||||
<span style="background-color: var(--light-color); padding: 0.2rem 0.5rem; border-radius: 4px; font-size: 0.8rem;">Développement web</span>
|
||||
<span style="background-color: var(--light-color); padding: 0.2rem 0.5rem; border-radius: 4px; font-size: 0.8rem;">Application</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Collecte verdanges -->
|
||||
<div style="position: relative; margin-bottom: 2.5rem;">
|
||||
<div style="position: absolute; width: 20px; height: 20px; background-color: var(--primary-color); border-radius: 50%; left: -41px; top: 0;"></div>
|
||||
<h3>Collecte vendanges</h3>
|
||||
<p style="color: var(--primary-color); margin: 0.3rem 0;">2023</p>
|
||||
<p style="margin-top: 0.8rem;">Récolte, ramassage et transport de nature.</p>
|
||||
<div style="margin-top: 0.8rem; display: flex; flex-wrap: wrap; gap: 0.5rem;">
|
||||
<span style="background-color: var(--light-color); padding: 0.2rem 0.5rem; border-radius: 4px; font-size: 0.8rem;">Travail d'équipe</span>
|
||||
<span style="background-color: var(--light-color); padding: 0.2rem 0.5rem; border-radius: 4px; font-size: 0.8rem;">Rigueur</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Assistant de vie -->
|
||||
<div style="position: relative; margin-bottom: 2.5rem;">
|
||||
<div style="position: absolute; width: 20px; height: 20px; background-color: var(--primary-color); border-radius: 50%; left: -41px; top: 0;"></div>
|
||||
<h3>Assistant de vie</h3>
|
||||
<p style="color: var(--primary-color); margin: 0.3rem 0;">2023</p>
|
||||
<p style="margin-top: 0.8rem;">Fourni des assistances aux personnes âgées dans leurs activités quotidiennes, en assurant le service des repas et en proposant des activités.</p>
|
||||
<div style="margin-top: 0.8rem; display: flex; flex-wrap: wrap; gap: 0.5rem;">
|
||||
<span style="background-color: var(--light-color); padding: 0.2rem 0.5rem; border-radius: 4px; font-size: 0.8rem;">Assistance</span>
|
||||
<span style="background-color: var(--light-color); padding: 0.2rem 0.5rem; border-radius: 4px; font-size: 0.8rem;">Service</span>
|
||||
<span style="background-color: var(--light-color); padding: 0.2rem 0.5rem; border-radius: 4px; font-size: 0.8rem;">Conseil</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Divers métiers -->
|
||||
<div style="position: relative;">
|
||||
<div style="position: absolute; width: 20px; height: 20px; background-color: var(--primary-color); border-radius: 50%; left: -41px; top: 0;"></div>
|
||||
<h3>Divers métiers</h3>
|
||||
<p style="color: var(--primary-color); margin: 0.3rem 0;">2010 - 2017</p>
|
||||
<p style="margin-top: 0.8rem;">Barman, boucher, préparateur de commandes, vendeur en magasin de matériaux.</p>
|
||||
<div style="margin-top: 0.8rem; display: flex; flex-wrap: wrap; gap: 0.5rem;">
|
||||
<span style="background-color: var(--light-color); padding: 0.2rem 0.5rem; border-radius: 4px; font-size: 0.8rem;">Polyvalence</span>
|
||||
<span style="background-color: var(--light-color); padding: 0.2rem 0.5rem; border-radius: 4px; font-size: 0.8rem;">Adaptabilité</span>
|
||||
<span style="background-color: var(--light-color); padding: 0.2rem 0.5rem; border-radius: 4px; font-size: 0.8rem;">Service client</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<h2>Formation</h2>
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 1.5rem; margin-top: 1.5rem;">
|
||||
<div style="flex: 1 1 300px; background-color: var(--light-color); padding: 1.5rem; border-radius: var(--border-radius); box-shadow: 0 2px 4px rgba(0,0,0,0.05);">
|
||||
<h3>Développeur Gaming</h3>
|
||||
<p style="margin-top: 0.5rem; color: var(--primary-color);">Technocité - 2025</p>
|
||||
<p style="margin-top: 0.5rem;">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..</p>
|
||||
<p style="margin-top: 0.5rem;">Formé en tant que développeur gaming, j'ai acquis à maîtriser le C# ainsi que le moteur Unity.</p>
|
||||
</div>
|
||||
<div style="flex: 1 1 300px; background-color: var(--light-color); padding: 1.5rem; border-radius: var(--border-radius); box-shadow: 0 2px 4px rgba(0,0,0,0.05);">
|
||||
<h3>Développeur Web orientée data analyste</h3>
|
||||
<p style="margin-top: 0.5rem; color: var(--primary-color);">Technofutur TIC - 2024</p>
|
||||
<p style="margin-top: 0.5rem;">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...).</p>
|
||||
<p style="margin-top: 0.5rem;">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...)).</p>
|
||||
</div>
|
||||
<div style="flex: 1 1 300px; background-color: var(--light-color); padding: 1.5rem; border-radius: var(--border-radius); box-shadow: 0 2px 4px rgba(0,0,0,0.05);">
|
||||
<h3>Préparateur de commande en entrepôt</h3>
|
||||
<p style="margin-top: 0.5rem; color: var(--primary-color);">AFPA Dijon - France - 2010</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<h2>À propos</h2>
|
||||
<div style="margin-top: 1.5rem;">
|
||||
<p>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...).</p>
|
||||
<p style="margin-top: 1rem;">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...)).</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<h2>Centres d'intérêt</h2>
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 1.5rem; margin-top: 1.5rem;">
|
||||
<div style="flex: 1 1 200px; text-align: center; padding: 1rem;">
|
||||
<div style="width: 60px; height: 60px; background-color: var(--success-color); border-radius: 50%; display: flex; align-items: center; justify-content: center; margin: 0 auto 1rem auto;">
|
||||
<span style="color: white; font-size: 1.5rem;">💻</span>
|
||||
</div>
|
||||
<h3>Développement et Programmation</h3>
|
||||
</div>
|
||||
<div style="flex: 1 1 200px; text-align: center; padding: 1rem;">
|
||||
<div style="width: 60px; height: 60px; background-color: var(--accent-color); border-radius: 50%; display: flex; align-items: center; justify-content: center; margin: 0 auto 1rem auto;">
|
||||
<span style="color: white; font-size: 1.5rem;">✈️</span>
|
||||
</div>
|
||||
<h3>Voyages</h3>
|
||||
</div>
|
||||
<div style="flex: 1 1 200px; text-align: center; padding: 1rem;">
|
||||
<div style="width: 60px; height: 60px; background-color: var(--primary-color); border-radius: 50%; display: flex; align-items: center; justify-content: center; margin: 0 auto 1rem auto;">
|
||||
<span style="color: white; font-size: 1.5rem;">🎮</span>
|
||||
</div>
|
||||
<h3>Gaming</h3>
|
||||
</div>
|
||||
<div style="flex: 1 1 200px; text-align: center; padding: 1rem;">
|
||||
<div style="width: 60px; height: 60px; background-color: var(--secondary-color); border-radius: 50%; display: flex; align-items: center; justify-content: center; margin: 0 auto 1rem auto;">
|
||||
<span style="color: white; font-size: 1.5rem;">📚</span>
|
||||
</div>
|
||||
<h3>Lecture de romans</h3>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
<section class="section">
|
||||
<h1>Contactez-moi</h1>
|
||||
<p>Vous avez un projet en tête ou une question? N'hésitez pas à me contacter en utilisant le formulaire ci-dessous.</p>
|
||||
</section>
|
||||
|
||||
<section class="section contact-page">
|
||||
<form style="margin-top: 1rem;">
|
||||
<div style="margin-bottom: 1.5rem;">
|
||||
<label for="name" style="display: block; margin-bottom: 0.5rem; font-weight: 500;">Nom</label>
|
||||
<input type="text" id="name" name="name" placeholder="Votre nom" style="width: 100%; padding: 0.75rem; border: 1px solid #e0e0e0; border-radius: var(--border-radius); font-family: inherit; font-size: 1rem;">
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 1.5rem;">
|
||||
<label for="email" style="display: block; margin-bottom: 0.5rem; font-weight: 500;">Email</label>
|
||||
<input type="email" id="email" name="email" placeholder="Votre adresse email" style="width: 100%; padding: 0.75rem; border: 1px solid #e0e0e0; border-radius: var(--border-radius); font-family: inherit; font-size: 1rem;">
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 1.5rem;">
|
||||
<label for="subject" style="display: block; margin-bottom: 0.5rem; font-weight: 500;">Sujet</label>
|
||||
<input type="text" id="subject" name="subject" placeholder="Sujet de votre message" style="width: 100%; padding: 0.75rem; border: 1px solid #e0e0e0; border-radius: var(--border-radius); font-family: inherit; font-size: 1rem;">
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 1.5rem;">
|
||||
<label for="message" style="display: block; margin-bottom: 0.5rem; font-weight: 500;">Message</label>
|
||||
<textarea id="message" name="message" rows="5" placeholder="Votre message" style="width: 100%; padding: 0.75rem; border: 1px solid #e0e0e0; border-radius: var(--border-radius); font-family: inherit; font-size: 1rem; resize: vertical;"></textarea>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn" style="border: none; cursor: pointer;">Envoyer le message</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<h2>Autres moyens de contact</h2>
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 2rem; margin-top: 1.5rem;">
|
||||
<div style="flex: 1 1 250px;">
|
||||
<h3>Email</h3>
|
||||
<p><a href="" data-page="externe" data-id="email"></a></p>
|
||||
</div>
|
||||
<div style="flex: 1 1 250px;">
|
||||
<h3>Téléphone</h3>
|
||||
<p data-id="phone">+33 6 XX XX XX XX</p>
|
||||
</div>
|
||||
<div style="flex: 1 1 250px;">
|
||||
<h3>Réseaux sociaux</h3>
|
||||
<div style="display: flex; gap: 1rem; margin-top: 0.5rem;">
|
||||
<a href="#" style="text-decoration: none;" data-page="externe" data-id="linkedin">LinkedIn</a>
|
||||
<a href="#" style="text-decoration: none;" data-page="externe" data-id="twitter">Twitter</a>
|
||||
<a href="#" style="text-decoration: none;" data-page="externe" data-id="github">GitHub</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<h2>Disponibilité</h2>
|
||||
<p>Je suis actuellement disponible pour des projets freelance à temps partiel.</p>
|
||||
<p>N'hésitez pas à me contacter pour discuter de votre projet et obtenir un devis personnalisé.</p>
|
||||
</section>
|
||||
|
|
@ -1,69 +0,0 @@
|
|||
<section class="section">
|
||||
<h1>Bienvenue sur mon portfolio</h1>
|
||||
<!--<p>Je suis Anthony VIOLET, développeur web passionné par la création d'expériences utilisateur modernes et intuitives.</p>
|
||||
<p>Explorez mes projets et découvrez mes compétences en développement web et design.</p>-->
|
||||
<p><a data-page="projects" class="btn">Voir mes projets</a></p>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<h2>Mes compétences</h2>
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 1.2rem; margin-top: 1.5rem;">
|
||||
<div style="background-color: var(--light-color); padding: 1.2rem; border-radius: var(--border-radius); box-shadow: 0 2px 4px rgba(0,0,0,0.05); flex: 1 1 200px; transition: transform 0.3s ease;">
|
||||
<h3 style="color: var(--primary-color);">Python</h3>
|
||||
<p style="margin-top: 0.8rem;">NumPy, Pandas, SciKit-Learn</p>
|
||||
<div style="height: 6px; background-color: #e0e0e0; margin-top: 1rem; border-radius: 3px; overflow: hidden;">
|
||||
<div style="width: 85%; height: 100%; background-color: var(--primary-color);"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="background-color: var(--light-color); padding: 1.2rem; border-radius: var(--border-radius); box-shadow: 0 2px 4px rgba(0,0,0,0.05); flex: 1 1 200px; transition: transform 0.3s ease;">
|
||||
<h3 style="color: var(--secondary-color);">JavaScript</h3>
|
||||
<p style="margin-top: 0.8rem;">Node.js, React/JS, Angular</p>
|
||||
<div style="height: 6px; background-color: #e0e0e0; margin-top: 1rem; border-radius: 3px; overflow: hidden;">
|
||||
<div style="width: 75%; height: 100%; background-color: var(--secondary-color);"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="background-color: var(--light-color); padding: 1.2rem; border-radius: var(--border-radius); box-shadow: 0 2px 4px rgba(0,0,0,0.05); flex: 1 1 200px; transition: transform 0.3s ease;">
|
||||
<h3 style="color: var(--accent-color);">PHP</h3>
|
||||
<p style="margin-top: 0.8rem;">Symfony, Laravel, OOP</p>
|
||||
<div style="height: 6px; background-color: #e0e0e0; margin-top: 1rem; border-radius: 3px; overflow: hidden;">
|
||||
<div style="width: 80%; height: 100%; background-color: var(--accent-color);"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="text-align: center; margin-top: 1.5rem;">
|
||||
<a id="about" class="btn">Voir toutes mes compétences</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<h2>Services proposés</h2>
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 1.5rem; margin-top: 1.5rem;">
|
||||
<div style="flex: 1 1 300px;">
|
||||
<h3 style="display: flex; align-items: center;">
|
||||
<span style="display: inline-flex; align-items: center; justify-content: center; width: 40px; height: 40px; background-color: var(--primary-color); color: white; border-radius: 8px; margin-right: 10px;">🌐</span>
|
||||
Création de site web
|
||||
</h3>
|
||||
<p>Des sites web modernes, responsives et optimisés pour tous les appareils.</p>
|
||||
</div>
|
||||
<div style="flex: 1 1 300px;">
|
||||
<h3 style="display: flex; align-items: center;">
|
||||
<span style="display: inline-flex; align-items: center; justify-content: center; width: 40px; height: 40px; background-color: var(--secondary-color); color: white; border-radius: 8px; margin-right: 10px;">📊</span>
|
||||
Analyse de données
|
||||
</h3>
|
||||
<p>Traitement et visualisation de données avec Python (NumPy, Pandas, SciKit-Learn).</p>
|
||||
</div>
|
||||
<div style="flex: 1 1 300px;">
|
||||
<h3 style="display: flex; align-items: center;">
|
||||
<span style="display: inline-flex; align-items: center; justify-content: center; width: 40px; height: 40px; background-color: var(--accent-color); color: white; border-radius: 8px; margin-right: 10px;">💻</span>
|
||||
Développement web
|
||||
</h3>
|
||||
<p>Applications web performantes avec JavaScript (Node.js, React, Angular) et PHP (Symfony).</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<h2>Contactez-moi</h2>
|
||||
<p>Vous avez un projet en tête ou une question? N'hésitez pas à me contacter!</p>
|
||||
<a id="contact" class="btn">Me contacter</a>
|
||||
</section>
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
<section class="section" id="admin-sanctuary">
|
||||
<h2>Admin</h2>
|
||||
<p>Ravi de te revoir, <span style="color:var(--success-color)"><?= $admin["username"]; ?></span> !</p>
|
||||
<div style="display:inline-block;float:right;">
|
||||
<a href="#" data-page="admin-logout" id="admin" class="btn">Partir</a>
|
||||
</div>
|
||||
<div class="info">
|
||||
Espace d'administration protégé.
|
||||
</div>
|
||||
<div class="navbar-admin">
|
||||
<ul>
|
||||
<li>Réglages généraux</li>
|
||||
<li>Les projets</li>
|
||||
<li>Infos contact</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
<section class="section">
|
||||
<h1>Mes projets</h1>
|
||||
<p>Découvrez une sélection de mes réalisations récentes.</p>
|
||||
</section>
|
||||
|
||||
<div class="projects-grid" style="display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 2rem; margin: 2rem 0;"></div>
|
||||
|
||||
<section class="section">
|
||||
<!--<h2>Autres projets</h2>
|
||||
<p>En plus des projets présentés ci-dessus, j'ai également travaillé sur:</p>
|
||||
<ul style="margin-left: 1.5rem;">
|
||||
<li>Refonte UI/UX d'une application web de gestion de projet</li>
|
||||
<li>Développement d'un tableau de bord administratif</li>
|
||||
<li>Site web pour un photographe avec galerie dynamique</li>
|
||||
<li>Blog personnel avec système de gestion de contenu</li>
|
||||
</ul>-->
|
||||
<p style="margin-top: 1.5rem;">Vous avez un projet en tête? N'hésitez pas à me contacter pour en discuter!</p>
|
||||
<a id="contact" class="btn" style="margin-top: 1rem;">Me contacter</a>
|
||||
</section>
|
||||
BIN
admin/.DS_Store
vendored
BIN
admin/.DS_Store
vendored
Binary file not shown.
|
|
@ -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
|
||||
119
admin/config.php
119
admin/config.php
|
|
@ -1,119 +0,0 @@
|
|||
<?php
|
||||
// Configuration de sécurité
|
||||
// IMPORTANT: Remplacez le hash ci-dessous par le hash de votre mot de passe
|
||||
// Utilisez generate_password_hash.php pour générer le hash
|
||||
define('ADMIN_PASSWORD_HASH', '$2y$10$5L/rCnFdy8GEXBH85Rce.ujXeb9JC1LH0Uvyltx3p2EtbhxXKJpna'); // Hash de "Toine1990@"
|
||||
define('ADMIN_USERNAME', 'toine');
|
||||
define('SESSION_TIMEOUT', 3600); // 1 heure
|
||||
define('MAX_LOGIN_ATTEMPTS', 5);
|
||||
define('LOGIN_LOCKOUT_TIME', 900); // 15 minutes
|
||||
|
||||
// Configuration de session sécurisée - DOIT être appelé AVANT session_start()
|
||||
function configureSecureSession() {
|
||||
// Paramètres de sécurité pour les sessions
|
||||
ini_set('session.cookie_httponly', 1);
|
||||
ini_set('session.cookie_secure', isset($_SERVER['HTTPS']));
|
||||
ini_set('session.use_strict_mode', 1);
|
||||
ini_set('session.cookie_samesite', 'Strict');
|
||||
}
|
||||
|
||||
// Fonction à appeler APRÈS session_start()
|
||||
function initSecureSession() {
|
||||
// Régénérer l'ID de session périodiquement
|
||||
if (!isset($_SESSION['last_regeneration'])) {
|
||||
$_SESSION['last_regeneration'] = time();
|
||||
} elseif (time() - $_SESSION['last_regeneration'] > 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');
|
||||
}
|
||||
?>
|
||||
|
|
@ -1,111 +0,0 @@
|
|||
<?php
|
||||
require_once './includes/nav.php';
|
||||
require_once 'config.php';
|
||||
|
||||
// Vérifier l'authentification
|
||||
if (!isAuthenticated()) {
|
||||
header("Location: ?page=login");
|
||||
exit;
|
||||
}
|
||||
$message = "";
|
||||
$firstname = "";
|
||||
$lastname = "";
|
||||
$email = "";
|
||||
$gsm = "";
|
||||
$linkedin = "";
|
||||
$twitter = "";
|
||||
$github = "";
|
||||
|
||||
if($_POST){
|
||||
if (!isset($_POST['csrf_token']) || !verifyCSRFToken($_POST['csrf_token'])) {
|
||||
echo "<div class='alert alert-error'>Token de sécurité invalide.</div>";
|
||||
} 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 = '<div class="alert alert-success">Formulaire soumis</div>';
|
||||
|
||||
$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 = '<div class="alert alert-success">Données de contact mises à jour avec succès !</div>';
|
||||
} else {
|
||||
$message = '<div class="alert alert-error">Erreur lors de la mise à jour des données de contact.</div>';
|
||||
}
|
||||
}
|
||||
}
|
||||
} 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'] ?? '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
||||
<section>
|
||||
<div class="dashboard">
|
||||
<h1>Données de contacts</h1>
|
||||
<p>Les données de contacts affichées ici sont reprise sur le site dans la rubrique contact</p>
|
||||
<div class="form-project">
|
||||
<form action="" method="post">
|
||||
<?= $message; ?>
|
||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(generateCSRFToken()); ?>">
|
||||
<div class="form-group">
|
||||
<input type="text" name="firstname" placeholder="Prénom" value="<?= $firstname; ?>" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="text" name="lastname" placeholder="Nom de famille" value="<?= $lastname; ?>" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="email" name="email" placeholder="Adresse email" value="<?= $email; ?>" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="tel" name="gsm" placeholder="Numéro de téléphone" value="<?= $gsm; ?>" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="text" name="linkedin" placeholder="LinkedIn" value="<?= $linkedin; ?>" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="text" name="twitter" placeholder="X (Twitter)" value="<?= $twitter; ?>" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="text" name="github" placeholder="Github" value="<?= $github; ?>" />
|
||||
</div>
|
||||
<button type="submit" class="btn-success">Mettre à jour</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
<?php
|
||||
require_once './includes/nav.php';
|
||||
$id = isset($_GET['id']) ? sanitizeInput($_GET['id']) : null;
|
||||
if (!$id) {
|
||||
echo "<div class='alert alert-error'>ID de projet manquant.</div>";
|
||||
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 "<div class='alert alert-error'>Projet non trouvé.</div>";
|
||||
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 "<div class='alert alert-success'>Projet supprimé avec succès !</div>";
|
||||
} else {
|
||||
echo "<div class='alert alert-error'>Erreur lors de la suppression du projet.</div>";
|
||||
}
|
||||
?>
|
||||
|
|
@ -1,60 +0,0 @@
|
|||
<?php
|
||||
// Script à exécuter UNE FOIS pour générer le hash de votre mot de passe
|
||||
// Ensuite, supprimez ce fichier ou déplacez-le hors du répertoire web
|
||||
|
||||
echo "<h2>Générateur de hash pour mot de passe</h2>";
|
||||
|
||||
if ($_POST && isset($_POST['password'])) {
|
||||
$password = $_POST['password'];
|
||||
$hash = password_hash($password, PASSWORD_DEFAULT);
|
||||
|
||||
echo "<div style='background: #f0f8ff; padding: 15px; border: 1px solid #0066cc; margin: 10px 0;'>";
|
||||
echo "<strong>Hash généré :</strong><br>";
|
||||
echo "<code style='background: #e8e8e8; padding: 5px; word-break: break-all;'>" . htmlspecialchars($hash) . "</code><br><br>";
|
||||
echo "<strong>Copiez ce hash dans config.php à la place de ADMIN_PASSWORD_HASH</strong>";
|
||||
echo "</div>";
|
||||
|
||||
// Vérification
|
||||
if (password_verify($password, $hash)) {
|
||||
echo "<p style='color: green;'>✓ Vérification réussie - Le hash fonctionne correctement</p>";
|
||||
} else {
|
||||
echo "<p style='color: red;'>✗ Erreur de vérification</p>";
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Générateur de Hash</title>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; max-width: 600px; margin: 50px auto; padding: 20px; }
|
||||
input, button { padding: 10px; margin: 5px 0; }
|
||||
input[type="password"] { width: 300px; }
|
||||
button { background: #0066cc; color: white; border: none; cursor: pointer; }
|
||||
button:hover { background: #0052a3; }
|
||||
.warning { background: #fff3cd; padding: 15px; border: 1px solid #ffeaa7; color: #856404; margin: 20px 0; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="warning">
|
||||
<strong>⚠️ ATTENTION :</strong> Supprimez ce fichier après utilisation pour des raisons de sécurité !
|
||||
</div>
|
||||
|
||||
<form method="post">
|
||||
<label for="password">Entrez votre mot de passe :</label><br>
|
||||
<input type="password" name="password" id="password" required><br>
|
||||
<button type="submit">Générer le hash</button>
|
||||
</form>
|
||||
|
||||
<div style="margin-top: 30px; font-size: 14px; color: #666;">
|
||||
<h3>Instructions :</h3>
|
||||
<ol>
|
||||
<li>Entrez votre mot de passe ci-dessus</li>
|
||||
<li>Copiez le hash généré</li>
|
||||
<li>Remplacez la valeur de ADMIN_PASSWORD_HASH dans config.php</li>
|
||||
<li><strong>Supprimez ce fichier (generate_password_hash.php)</strong></li>
|
||||
</ol>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,61 +0,0 @@
|
|||
<?php require_once './includes/nav.php'; ?>
|
||||
<section>
|
||||
<div class="dashboard">
|
||||
<h1>Tableau de bord</h1>
|
||||
<p class="welcome-message">Bienvenue dans l'espace d'administration de votre site personnel.</p>
|
||||
|
||||
<div class="dashboard-grid">
|
||||
<div class="dashboard-card">
|
||||
<h3>🎨 Projets</h3>
|
||||
<p>Gérez vos projets et réalisations</p>
|
||||
<a href="./?page=projects" class="btn-primary">Accéder</a>
|
||||
</div>
|
||||
|
||||
<div class="dashboard-card">
|
||||
<h3>📧 Contacts</h3>
|
||||
<p>Consultez les données de contact</p>
|
||||
<a href="./?page=contact" class="btn-primary">Accéder</a>
|
||||
</div>
|
||||
|
||||
<div class="dashboard-card">
|
||||
<h3>📊 Statistiques</h3>
|
||||
<div class="stats">
|
||||
<div class="stat-item">
|
||||
<span class="stat-number">5</span>
|
||||
<span class="stat-label">Projets actifs</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-number">12</span>
|
||||
<span class="stat-label">Messages reçus</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dashboard-card">
|
||||
<h3>🚀 Actions rapides</h3>
|
||||
<div class="quick-actions">
|
||||
<a href="../index.html" class="btn-secondary">Voir le site</a>
|
||||
<a href="./logout.php" class="btn-danger">Se déconnecter</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--<div class="recent-activity">
|
||||
<h3>Activité récente</h3>
|
||||
<div class="activity-list">
|
||||
<div class="activity-item">
|
||||
<span class="activity-date">Aujourd'hui</span>
|
||||
<span class="activity-desc">Connexion à l'administration</span>
|
||||
</div>
|
||||
<div class="activity-item">
|
||||
<span class="activity-date">Hier</span>
|
||||
<span class="activity-desc">Nouveau message de contact reçu</span>
|
||||
</div>
|
||||
<div class="activity-item">
|
||||
<span class="activity-date">3 jours</span>
|
||||
<span class="activity-desc">Projet "Portfolio React" mis à jour</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>-->
|
||||
</div>
|
||||
</section>
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
# Sécurisation du dossier d'administration
|
||||
|
||||
# Cacher les fichiers sensibles
|
||||
<Files "config.php">
|
||||
Require all denied
|
||||
</Files>
|
||||
|
||||
<Files "login_attempts.json">
|
||||
Require all denied
|
||||
</Files>
|
||||
|
||||
<Files "generate_password_hash.php">
|
||||
Require all denied
|
||||
</Files>
|
||||
|
||||
# Protection contre les attaques par force brute
|
||||
<RequireAll>
|
||||
Require all granted
|
||||
# Limiter les requêtes POST (optionnel, à configurer selon vos besoins)
|
||||
</RequireAll>
|
||||
|
||||
# Headers de sécurité
|
||||
<IfModule mod_headers.c>
|
||||
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'"
|
||||
</IfModule>
|
||||
|
||||
# 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
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
<nav>
|
||||
<ul>
|
||||
<li class="title">Administration</li>
|
||||
<li class="nav-item"><a href="./?page=home">Accueil</a></li>
|
||||
<li class="nav-item"><a href="./?page=projects">Les projets</a></li>
|
||||
<li class="nav-item"><a href="./?page=contact">Données de contacts</a></li>
|
||||
<li class="nav-item"><a href="../index.html">Retour au site</a></li>
|
||||
<li class="nav-item">
|
||||
<form method="post" action="./logout.php" style="display: inline;">
|
||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(generateCSRFToken()); ?>">
|
||||
<button type="submit" style="background: none; border: none; color: inherit; text-decoration: underline; cursor: pointer;">Déconnexion</button>
|
||||
</form>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
<?php
|
||||
require_once 'init.php';
|
||||
|
||||
$connected = "false";
|
||||
|
||||
if (!isset($_GET["page"])) {
|
||||
header("Location: ?page=home");
|
||||
exit;
|
||||
}
|
||||
|
||||
$page = sanitizeInput($_GET["page"]);
|
||||
|
||||
// Vérifier l'authentification
|
||||
if (isAuthenticated()) {
|
||||
$admin = array(
|
||||
"username" => $_SESSION['username']
|
||||
);
|
||||
$connected = "true";
|
||||
} else {
|
||||
$page = "login";
|
||||
}
|
||||
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="stylesheet" href="../Static/css/admin.css">
|
||||
<script type="module" src="../Static/js/admin.js" defer></script>
|
||||
<title>Administration</title>
|
||||
</head>
|
||||
<body<?= ($page == "login") ? " class='login-page'" : "" ?>>
|
||||
<?php require_once $page.".php" ?>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
<?php
|
||||
// Fichier d'initialisation global pour l'administration
|
||||
// Ce fichier doit être inclus au début de chaque page admin
|
||||
|
||||
require_once __DIR__ . '/config.php';
|
||||
|
||||
// Fonction pour démarrer une session sécurisée
|
||||
function startSecureSession() {
|
||||
// Ne démarrer la session que si elle n'est pas déjà active
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
// Configurer la session AVANT de la démarrer
|
||||
configureSecureSession();
|
||||
session_start();
|
||||
|
||||
// Initialiser après le démarrage
|
||||
initSecureSession();
|
||||
}
|
||||
}
|
||||
|
||||
// Démarrer la session sécurisée
|
||||
startSecureSession();
|
||||
?>
|
||||
|
|
@ -1,61 +0,0 @@
|
|||
<?php
|
||||
require_once 'config.php';
|
||||
|
||||
$msg = "";
|
||||
$client_ip = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
|
||||
|
||||
// Vérifier si l'IP est bloquée
|
||||
if (!checkLoginAttempts($client_ip)) {
|
||||
$remaining_time = LOGIN_LOCKOUT_TIME - (time() - json_decode(file_get_contents(__DIR__ . '/login_attempts.json'), true)[$client_ip]['last_attempt']);
|
||||
$msg = '<span style="color:red">Trop de tentatives de connexion. Réessayez dans ' . ceil($remaining_time / 60) . ' minutes.</span>';
|
||||
} 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 = '<span style="color:red">Token de sécurité invalide. Veuillez réessayer.</span>';
|
||||
} 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 = '<span style="color:green">Connexion réussie !</span><br><a href="?page=home">Accéder à l\'administration</a>';
|
||||
} else {
|
||||
// Connexion échouée
|
||||
recordLoginAttempt($client_ip, false);
|
||||
$msg = '<span style="color:red">Identifiants incorrects.</span>';
|
||||
|
||||
// Ajouter un délai pour ralentir les attaques par force brute
|
||||
sleep(2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
?>
|
||||
<section class="login" id="admin-sanctuary">
|
||||
<h2>Connexion</h2>
|
||||
<?= $msg; ?>
|
||||
<form method="post" action="./?page=login" style="margin-top: 1rem;" autocomplete="off">
|
||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(generateCSRFToken()); ?>">
|
||||
<div style="margin-bottom: 1.5rem;">
|
||||
<label for="username" style="display: block; margin-bottom: 0.5rem; font-weight: 500;">Utilisateur</label>
|
||||
<input type="text" name="username" id="username" placeholder="Utilisateur" required autocomplete="username" />
|
||||
</div>
|
||||
<div style="margin-bottom: 1.5rem;">
|
||||
<label for="userPassword" style="display: block; margin-bottom: 0.5rem; font-weight: 500;">Mot de passe</label>
|
||||
<input type="password" name="userPassword" id="userPassword" placeholder="Mot de passe" required autocomplete="current-password" />
|
||||
</div>
|
||||
<button type="submit" class="btn" id="admin-login">Se connecter</button>
|
||||
</form>
|
||||
</section>
|
||||
|
|
@ -1 +0,0 @@
|
|||
[]
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
<?php
|
||||
require_once 'init.php';
|
||||
|
||||
// Vérifier le token CSRF pour la déconnexion
|
||||
if ($_POST && isset($_POST['csrf_token'])) {
|
||||
if (verifyCSRFToken($_POST['csrf_token'])) {
|
||||
// Nettoyer complètement la session
|
||||
$_SESSION = array();
|
||||
|
||||
// Détruire le cookie de session
|
||||
if (isset($_COOKIE[session_name()])) {
|
||||
setcookie(session_name(), '', time()-3600, '/');
|
||||
}
|
||||
|
||||
// Détruire la session
|
||||
session_destroy();
|
||||
|
||||
// Redirection sécurisée
|
||||
header("Location: ../index.html");
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
// Si pas de POST ou token invalide, rediriger vers l'admin
|
||||
header("Location: ?page=home");
|
||||
exit;
|
||||
?>
|
||||
|
|
@ -1,126 +0,0 @@
|
|||
<?php
|
||||
require_once './includes/nav.php';
|
||||
|
||||
if($_POST) {
|
||||
// Vérifier le token CSRF
|
||||
if (!isset($_POST['csrf_token']) || !verifyCSRFToken($_POST['csrf_token'])) {
|
||||
echo "<div class='alert alert-error'>Token de sécurité invalide.</div>";
|
||||
} 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 "<div class='alert alert-error'>Tous les champs obligatoires doivent être remplis.</div>";
|
||||
} elseif (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $start_date)) {
|
||||
echo "<div class='alert alert-error'>Format de date de début invalide.</div>";
|
||||
} elseif ($end_date && !preg_match('/^\d{4}-\d{2}-\d{2}$/', $end_date)) {
|
||||
echo "<div class='alert alert-error'>Format de date de fin invalide.</div>";
|
||||
} 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 "<div class='alert alert-success'>Projet '$name' créé avec succès !</div>";
|
||||
// Reset form fields
|
||||
unset($_POST);
|
||||
} else {
|
||||
echo "<div class='alert alert-error'>Erreur lors de la sauvegarde du projet.</div>";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
?>
|
||||
<section>
|
||||
<div class="dashboard" data-type="projects">
|
||||
<div class="projects-actions">
|
||||
<h1>Projets</h1>
|
||||
<a href="#" class="btn-success" data-id="creation-project-btn">Ajouter projet</a>
|
||||
</div>
|
||||
<div class="form-project hidden">
|
||||
<form action="" method="post">
|
||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(generateCSRFToken()); ?>">
|
||||
<div class="form-group">
|
||||
<label for="project-type">Type du projet *</label>
|
||||
<input type="text" id="project-type" name="type" placeholder="Ex: Site vitrine e-commerce" required maxlength="100">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="project-name">Nom du projet *</label>
|
||||
<input type="text" id="project-name" name="name" placeholder="Ex: Sandwicherie" required maxlength="150">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="project-description">Description *</label>
|
||||
<textarea id="project-description" name="description" placeholder="Décrivez votre projet, les technologies utilisées, les défis relevés..." required maxlength="1000"></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="project-image">Lien du projet</label>
|
||||
<input type="url" id="project-link" name="link" placeholder="https://exemple.com/projet" maxlength="255">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="project-technologies">Technologies utilisées *</label>
|
||||
<div class="technologies-grid">
|
||||
<!-- Le contenu sera généré par JavaScript -->
|
||||
<div class="loading-technologies">
|
||||
<p>Chargement des technologies...</p>
|
||||
</div>
|
||||
</div>
|
||||
<input type="hidden" id="selected-technologies" name="technologies">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="project-tags">Tags (séparés par des virgules)</label>
|
||||
<input type="text" id="project-tags" name="tags" placeholder="Ex: responsive, moderne, e-commerce" maxlength="200">
|
||||
<small class="form-help">Ajoutez des mots-clés pour décrire votre projet (séparés par des virgules)</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="project-start-date">Date de début *</label>
|
||||
<input type="date" id="project-start-date" name="start_date" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="project-end-date">Date de fin</label>
|
||||
<input type="date" id="project-end-date" name="end_date">
|
||||
</div>
|
||||
<button type="submit" class="btn-success">Enregistrer</button>
|
||||
</form>
|
||||
</div>
|
||||
<div data-id="projects">
|
||||
<div class="projects-grid"></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script src="../Static/js/technologies.js"></script>
|
||||
8
backend/.idea/.gitignore
generated
vendored
Normal file
8
backend/.idea/.gitignore
generated
vendored
Normal file
|
|
@ -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
|
||||
16
backend/.idea/backend.iml
generated
Normal file
16
backend/.idea/backend.iml
generated
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="PYTHON_MODULE" version="4">
|
||||
<component name="Flask">
|
||||
<option name="enabled" value="true" />
|
||||
</component>
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<excludeFolder url="file://$MODULE_DIR$/.venv" />
|
||||
</content>
|
||||
<orderEntry type="jdk" jdkName="Python 3.13 (backend)" jdkType="Python SDK" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
<component name="TemplatesService">
|
||||
<option name="TEMPLATE_CONFIGURATION" value="Jinja2" />
|
||||
</component>
|
||||
</module>
|
||||
4
backend/.idea/encodings.xml
generated
Normal file
4
backend/.idea/encodings.xml
generated
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Encoding" addBOMForNewFiles="with BOM under Windows, with no BOM otherwise" />
|
||||
</project>
|
||||
6
backend/.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
6
backend/.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<component name="InspectionProjectProfileManager">
|
||||
<settings>
|
||||
<option name="USE_PROJECT_PROFILE" value="false" />
|
||||
<version value="1.0" />
|
||||
</settings>
|
||||
</component>
|
||||
7
backend/.idea/misc.xml
generated
Normal file
7
backend/.idea/misc.xml
generated
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Black">
|
||||
<option name="sdkName" value="Python 3.13 (backend)" />
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.13 (backend)" project-jdk-type="Python SDK" />
|
||||
</project>
|
||||
8
backend/.idea/modules.xml
generated
Normal file
8
backend/.idea/modules.xml
generated
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/backend.iml" filepath="$PROJECT_DIR$/.idea/backend.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
6
backend/.idea/vcs.xml
generated
Normal file
6
backend/.idea/vcs.xml
generated
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
0
backend/__init__.py
Normal file
0
backend/__init__.py
Normal file
BIN
backend/__pycache__/app.cpython-313.pyc
Normal file
BIN
backend/__pycache__/app.cpython-313.pyc
Normal file
Binary file not shown.
32
backend/app.py
Normal file
32
backend/app.py
Normal file
|
|
@ -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)
|
||||
9
backend/data/contact.json
Normal file
9
backend/data/contact.json
Normal file
|
|
@ -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"
|
||||
}
|
||||
36
backend/data/cv.json
Normal file
36
backend/data/cv.json
Normal file
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
]
|
||||
34
backend/data/projects.json
Normal file
34
backend/data/projects.json
Normal file
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
]
|
||||
14
backend/data/services.json
Normal file
14
backend/data/services.json
Normal file
|
|
@ -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."
|
||||
}
|
||||
]
|
||||
0
backend/models/__init__.py
Normal file
0
backend/models/__init__.py
Normal file
BIN
backend/models/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
backend/models/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
backend/models/__pycache__/contact_model.cpython-313.pyc
Normal file
BIN
backend/models/__pycache__/contact_model.cpython-313.pyc
Normal file
Binary file not shown.
BIN
backend/models/__pycache__/cv_model.cpython-313.pyc
Normal file
BIN
backend/models/__pycache__/cv_model.cpython-313.pyc
Normal file
Binary file not shown.
BIN
backend/models/__pycache__/project_model.cpython-313.pyc
Normal file
BIN
backend/models/__pycache__/project_model.cpython-313.pyc
Normal file
Binary file not shown.
BIN
backend/models/__pycache__/service_model.cpython-313.pyc
Normal file
BIN
backend/models/__pycache__/service_model.cpython-313.pyc
Normal file
Binary file not shown.
24
backend/models/contact_model.py
Normal file
24
backend/models/contact_model.py
Normal file
|
|
@ -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,
|
||||
}
|
||||
20
backend/models/cv_model.py
Normal file
20
backend/models/cv_model.py
Normal file
|
|
@ -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
|
||||
}
|
||||
31
backend/models/project_model.py
Normal file
31
backend/models/project_model.py
Normal file
|
|
@ -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"],
|
||||
}
|
||||
14
backend/models/service_model.py
Normal file
14
backend/models/service_model.py
Normal file
|
|
@ -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
|
||||
}
|
||||
3
backend/requirements.txt
Normal file
3
backend/requirements.txt
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
flask
|
||||
flask-cors
|
||||
dotenv
|
||||
0
backend/routes/__init__.py
Normal file
0
backend/routes/__init__.py
Normal file
BIN
backend/routes/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
backend/routes/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
backend/routes/__pycache__/contact.cpython-313.pyc
Normal file
BIN
backend/routes/__pycache__/contact.cpython-313.pyc
Normal file
Binary file not shown.
BIN
backend/routes/__pycache__/cv.cpython-313.pyc
Normal file
BIN
backend/routes/__pycache__/cv.cpython-313.pyc
Normal file
Binary file not shown.
BIN
backend/routes/__pycache__/projects.cpython-313.pyc
Normal file
BIN
backend/routes/__pycache__/projects.cpython-313.pyc
Normal file
Binary file not shown.
BIN
backend/routes/__pycache__/services.cpython-313.pyc
Normal file
BIN
backend/routes/__pycache__/services.cpython-313.pyc
Normal file
Binary file not shown.
29
backend/routes/contact.py
Normal file
29
backend/routes/contact.py
Normal file
|
|
@ -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
|
||||
41
backend/routes/cv.py
Normal file
41
backend/routes/cv.py
Normal file
|
|
@ -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)
|
||||
48
backend/routes/projects.py
Normal file
48
backend/routes/projects.py
Normal file
|
|
@ -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('/<project_id>', 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
|
||||
15
backend/routes/services.py
Normal file
15
backend/routes/services.py
Normal file
|
|
@ -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)
|
||||
0
backend/utils/__init__.py
Normal file
0
backend/utils/__init__.py
Normal file
BIN
backend/utils/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
backend/utils/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
backend/utils/__pycache__/data_loader.cpython-313.pyc
Normal file
BIN
backend/utils/__pycache__/data_loader.cpython-313.pyc
Normal file
Binary file not shown.
BIN
backend/utils/__pycache__/enums.cpython-313.pyc
Normal file
BIN
backend/utils/__pycache__/enums.cpython-313.pyc
Normal file
Binary file not shown.
BIN
backend/utils/__pycache__/json_crud.cpython-313.pyc
Normal file
BIN
backend/utils/__pycache__/json_crud.cpython-313.pyc
Normal file
Binary file not shown.
8
backend/utils/data_loader.py
Normal file
8
backend/utils/data_loader.py
Normal file
|
|
@ -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)
|
||||
21
backend/utils/enums.py
Normal file
21
backend/utils/enums.py
Normal file
|
|
@ -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"
|
||||
],
|
||||
}
|
||||
33
backend/utils/json_crud.py
Normal file
33
backend/utils/json_crud.py
Normal file
|
|
@ -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)
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
]
|
||||
41
frontend/.gitignore
vendored
Normal file
41
frontend/.gitignore
vendored
Normal file
|
|
@ -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
|
||||
8
frontend/.idea/.gitignore
generated
vendored
Normal file
8
frontend/.idea/.gitignore
generated
vendored
Normal file
|
|
@ -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
|
||||
3
frontend/.idea/dictionaries/project.xml
generated
Normal file
3
frontend/.idea/dictionaries/project.xml
generated
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<component name="ProjectDictionaryState">
|
||||
<dictionary name="project" />
|
||||
</component>
|
||||
4
frontend/.idea/encodings.xml
generated
Normal file
4
frontend/.idea/encodings.xml
generated
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Encoding" addBOMForNewFiles="with BOM under Windows, with no BOM otherwise" />
|
||||
</project>
|
||||
12
frontend/.idea/frontend.iml
generated
Normal file
12
frontend/.idea/frontend.iml
generated
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="WEB_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/temp" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/tmp" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
8
frontend/.idea/modules.xml
generated
Normal file
8
frontend/.idea/modules.xml
generated
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/frontend.iml" filepath="$PROJECT_DIR$/.idea/frontend.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
6
frontend/.idea/vcs.xml
generated
Normal file
6
frontend/.idea/vcs.xml
generated
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
36
frontend/README.md
Normal file
36
frontend/README.md
Normal file
|
|
@ -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.
|
||||
17
frontend/app/api/projects/route.ts
Normal file
17
frontend/app/api/projects/route.ts
Normal file
|
|
@ -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 });
|
||||
}
|
||||
24
frontend/app/components/About.tsx
Normal file
24
frontend/app/components/About.tsx
Normal file
|
|
@ -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 (
|
||||
<section className="dev-hero">
|
||||
<div className="container container--narrow">
|
||||
<span className="eyebrow">Développeur web</span>
|
||||
<h1 className="headline headline--code">Anthony Violet</h1>
|
||||
<p className="subheadline">{about_text}</p>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
104
frontend/app/components/Contact.tsx
Normal file
104
frontend/app/components/Contact.tsx
Normal file
|
|
@ -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<ContactData | null>(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 (
|
||||
<div className="block">
|
||||
<div className="btn-group">
|
||||
<Link href="/contact" className="btn btn--primary btn--pill">
|
||||
Me contacter
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Page /contact => liens + formulaire
|
||||
return (
|
||||
<div className="block">
|
||||
<div className="stack">
|
||||
<div className="btn-group">
|
||||
{data?.linkedin ? (
|
||||
<a href={data.linkedin} target="_blank" rel="noopener noreferrer" className="link-button">
|
||||
LinkedIn
|
||||
</a>
|
||||
) : null}
|
||||
{data?.github ? (
|
||||
<a href={data.github} target="_blank" rel="noopener noreferrer" className="link-button">
|
||||
GitHub
|
||||
</a>
|
||||
) : null}
|
||||
{data?.email ? (
|
||||
<a href={`mailto:${data.email}`} className="link-button">
|
||||
Email direct
|
||||
</a>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{data?.email ? (
|
||||
<form action={`mailto:${data.email}`} method="post" encType="text/plain" className="stack">
|
||||
<div>
|
||||
<label htmlFor="name">Votre nom</label>
|
||||
<input id="name" name="Nom" type="text" required />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="email">Votre email</label>
|
||||
<input id="email" name="Email" type="email" required />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="subject">Sujet</label>
|
||||
<input id="subject" name="Sujet" type="text" placeholder="Prise de contact" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="message">Message</label>
|
||||
<textarea id="message" name="Message" required />
|
||||
</div>
|
||||
|
||||
<div className="btn-group">
|
||||
<button type="submit" className="btn btn--primary btn--pill">
|
||||
Envoyer
|
||||
</button>
|
||||
<button type="reset" className="btn btn--ghost">
|
||||
Réinitialiser
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
) : (
|
||||
<p className="u-muted">Une erreur est survenue. Vous pouvez tout de même me contacter directement par email : <a href="mailto:violet.anthony90@gmail.com">Me joindre par email</a></p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
148
frontend/app/components/Header.tsx
Normal file
148
frontend/app/components/Header.tsx
Normal file
|
|
@ -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 (
|
||||
<header className={isTop ? "minimalist-pro is-top" : "minimalist-pro"}>
|
||||
<div className="logo">
|
||||
<Link href="/">
|
||||
<span className="name">Anthony Violet</span>
|
||||
<span className="tagline">Développeur Freelance & Digital Nomad</span>
|
||||
</Link>
|
||||
</div>
|
||||
<nav>
|
||||
<ul>
|
||||
<li><Link href="/projects" >Projets</Link></li>
|
||||
<li><Link href="/competences">Compétences</Link></li>
|
||||
<li><Link href="#blog">Blog Voyage</Link></li>
|
||||
<li><Link href="/contact">Contact</Link></li>
|
||||
<li><Link href="https://fr.malt.be/profile/anthonyviolet1" target="_blank">Malt</Link></li>
|
||||
<li><Link href="https://www.linkedin.com/in/anthony-violet/" target="_blank">LinkedIn</Link></li>
|
||||
</ul>
|
||||
</nav>
|
||||
<div className="cta">
|
||||
<button
|
||||
type="button"
|
||||
className="theme-toggle__btn"
|
||||
aria-label={theme === "dark" ? "Activer le thème clair" : "Activer le thème sombre"}
|
||||
onClick={toggleTheme}
|
||||
title={theme === "dark" ? "Mode clair" : "Mode sombre"}
|
||||
>
|
||||
{theme === "dark" ? "☀️" : "🌙"}
|
||||
</button>
|
||||
<Link href="mailto:tonemail@domaine.com" className="button">Travaillons ensemble</Link>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
/*<header className="cyberpunk">
|
||||
<div className="glitch-logo">
|
||||
<Link href="/">
|
||||
<span data-text="Anthony Violet">Anthony Violet</span>
|
||||
<span className="subtitle">// Développeur Full Stack & Créateur de Jeux</span>
|
||||
</Link>
|
||||
</div>
|
||||
<nav className="neon-nav">
|
||||
<ul>
|
||||
<li><Link href="#projets" className="neon-link">/projets</Link></li>
|
||||
<li><Link href="#tech" className="neon-link">/tech-stack</Link></li>
|
||||
<li><Link href="#blog" className="neon-link">/toine-traveller</Link></li>
|
||||
<li><Link href="#contact" className="neon-link">/contact@terminal</Link></li>
|
||||
</ul>
|
||||
</nav>
|
||||
<div className="status-bar">
|
||||
<span>En ligne █</span>
|
||||
<span>Disponible pour missions</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<header className="sidebar-header">
|
||||
<div className="profile-pic">
|
||||
<img src="ton-avatar.jpg" alt="Anthony Violet" />
|
||||
</div>
|
||||
<div className="profile-info">
|
||||
<h1>Anthony Violet</h1>
|
||||
<p>Freelance Dev | Digital Nomad | Builder de jeux & apps</p>
|
||||
</div>
|
||||
<nav>
|
||||
<ul>
|
||||
<li><Link href="#projets"><i className="icon-code"></i> Projets</Link></li>
|
||||
<li><Link href="#about"><i className="icon-user"></i> À propos</Link></li>
|
||||
<li><Link href="#blog"><i className="icon-globe"></i> Blog</Link></li>
|
||||
<li><Link href="#contact"><i className="icon-mail"></i> contact</Link></li>
|
||||
</ul>
|
||||
</nav>
|
||||
<div className="social-links">
|
||||
<Link href="https://github.com/tonuser" target="_blank"><i className="icon-github"></i></Link>
|
||||
<Link href="https://linkedin.com/in/anthony-violet" target="_blank"><i className="icon-linkedin"></i></Link>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<header className="interactive-header">
|
||||
<div className="typewriter">
|
||||
<h1>Bonjour, je suis <span className="typed-text">Anthony Violet</span></h1>
|
||||
<p>Je code en <span className="tech-tag">JavaScript</span>, <span className="tech-tag">Python</span>, et j'explore le monde.</p>
|
||||
</div>
|
||||
<nav>
|
||||
<ul>
|
||||
<li><Link href="#projets">Mes réalisations</Link></li>
|
||||
<li><Link href="#cv">Mon CV</Link></li>
|
||||
<li><Link href="#blog">Mes aventures</Link></li>
|
||||
</ul>
|
||||
</nav>
|
||||
<div className="language-switcher">
|
||||
<button>FR</button>
|
||||
<button>EN</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<header className="dark-header">
|
||||
<div className="container">
|
||||
<Link href="/" className="logo">
|
||||
<img src="logo.svg" alt="Logo Anthony Violet" />
|
||||
</Link>
|
||||
<nav>
|
||||
<ul>
|
||||
<li><Link href="#projets">Projets</Link></li>
|
||||
<li><Link href="#services">Services</Link></li>
|
||||
<li><Link href="#blog">Blog</Link></li>
|
||||
<li><Link href="#contact">Me contacter</Link></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
<div className="theme-toggle">
|
||||
<button id="theme-button"><i className="icon-moon"></i></button>
|
||||
</div>
|
||||
</header>*/
|
||||
);
|
||||
}
|
||||
35
frontend/app/components/Services.tsx
Normal file
35
frontend/app/components/Services.tsx
Normal file
|
|
@ -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 (
|
||||
<section id="services" className="section">
|
||||
<div className="container">
|
||||
<h2 className="section-title">Services proposés</h2>
|
||||
<ul className="grid-3">
|
||||
{services.map((service: any) => (
|
||||
<li key={service.id} className="service-card card feature">
|
||||
<span className="feature__icon" aria-hidden="true" />
|
||||
<div>
|
||||
<h3 className="feature__title">{service.name}</h3>
|
||||
{"description" in service && service.description ? (
|
||||
<p className="u-muted">{service.description}</p>
|
||||
) : null}
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
73
frontend/app/components/Skills.tsx
Normal file
73
frontend/app/components/Skills.tsx
Normal file
|
|
@ -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<string, string[]> = Array.isArray(skills) ? (skills[0] ?? {}) : {};
|
||||
const entries = Object.entries(obj) as [string, string[]][];
|
||||
|
||||
if (entries.length === 0) {
|
||||
return (
|
||||
<section id="skills" className="section">
|
||||
<div className="container">
|
||||
<h2 className="section-title">Mes compétences</h2>
|
||||
<p className="u-muted">Aucune compétence à afficher.</p>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
/*
|
||||
*
|
||||
{mySkillsEntries.length > 0 && (
|
||||
<section id="competences" className="section">
|
||||
<div className="container">
|
||||
<h2 className="section-title">Compétences clés</h2>
|
||||
<ul className="skill-grid">
|
||||
{mySkillsEntries.map(([category, items]) => (
|
||||
<li key={category} className="skill-card">
|
||||
<h3 className="skill-card__title">{category}</h3>
|
||||
<div className="skill-card__tags">
|
||||
{items.map((it) => (
|
||||
<span key={it} className="badge">{it}</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="skill-meter" aria-hidden="true">
|
||||
<div className="skill-meter__bar" style={{["--level" as any]: "75%"}}/>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
* */
|
||||
<section id="competences" className="section">
|
||||
<div className="container">
|
||||
<h2 className="section-title">Mes compétences</h2>
|
||||
<ul className="skill-grid">
|
||||
{entries.map(([category, skills]) => (
|
||||
<li key={category} className="skill-card">
|
||||
<h3 className="skill-card__title">{category}</h3>
|
||||
<div className="skill-card__tags">
|
||||
{skills.map((skill: string) => (
|
||||
<span key={skill} className="badge">{skill}</span>
|
||||
))}
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
8
frontend/app/contact/page.tsx
Normal file
8
frontend/app/contact/page.tsx
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import ContactComponent from "@/app/components/Contact";
|
||||
export default function Contact() {
|
||||
return (
|
||||
<div>
|
||||
<ContactComponent />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
69
frontend/app/css/colors.css
Normal file
69
frontend/app/css/colors.css
Normal file
|
|
@ -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;
|
||||
}
|
||||
576
frontend/app/css/components.css
Normal file
576
frontend/app/css/components.css
Normal file
|
|
@ -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;
|
||||
}
|
||||
112
frontend/app/css/forms.css
Normal file
112
frontend/app/css/forms.css
Normal file
|
|
@ -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;
|
||||
}
|
||||
131
frontend/app/css/globals.css
Normal file
131
frontend/app/css/globals.css
Normal file
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
66
frontend/app/css/header_CBPNK.css
Normal file
66
frontend/app/css/header_CBPNK.css
Normal file
|
|
@ -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;
|
||||
}
|
||||
43
frontend/app/css/header_DARK.css
Normal file
43
frontend/app/css/header_DARK.css
Normal file
|
|
@ -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;
|
||||
}
|
||||
87
frontend/app/css/header_IT.css
Normal file
87
frontend/app/css/header_IT.css
Normal file
|
|
@ -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;
|
||||
}
|
||||
75
frontend/app/css/header_LB.css
Normal file
75
frontend/app/css/header_LB.css
Normal file
|
|
@ -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;
|
||||
}
|
||||
115
frontend/app/css/header_MP.css
Normal file
115
frontend/app/css/header_MP.css
Normal file
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue