first commit
This commit is contained in:
commit
b216a187bd
34 changed files with 4829 additions and 0 deletions
BIN
.DS_Store
vendored
Normal file
BIN
.DS_Store
vendored
Normal file
Binary file not shown.
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
.idea
|
||||
BIN
Static/.DS_Store
vendored
Normal file
BIN
Static/.DS_Store
vendored
Normal file
Binary file not shown.
1130
Static/css/admin.css
Normal file
1130
Static/css/admin.css
Normal file
File diff suppressed because it is too large
Load diff
710
Static/css/style-secondaire.css
Normal file
710
Static/css/style-secondaire.css
Normal file
|
|
@ -0,0 +1,710 @@
|
|||
/* 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);
|
||||
}
|
||||
837
Static/css/style.css
Normal file
837
Static/css/style.css
Normal file
|
|
@ -0,0 +1,837 @@
|
|||
/* 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;
|
||||
}
|
||||
}
|
||||
BIN
Static/img/bg-login.jpg
Normal file
BIN
Static/img/bg-login.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 MiB |
135
Static/js/admin.js
Normal file
135
Static/js/admin.js
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
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' });
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
301
Static/js/main.js
Normal file
301
Static/js/main.js
Normal file
|
|
@ -0,0 +1,301 @@
|
|||
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');
|
||||
});
|
||||
11
Static/js/readJson.js
Normal file
11
Static/js/readJson.js
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
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 };
|
||||
315
Static/js/technologies.js
Normal file
315
Static/js/technologies.js
Normal file
|
|
@ -0,0 +1,315 @@
|
|||
// 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
Normal file
312
Views/about.html
Normal file
|
|
@ -0,0 +1,312 @@
|
|||
<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>
|
||||
58
Views/contact.html
Normal file
58
Views/contact.html
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
<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>
|
||||
69
Views/home.html
Normal file
69
Views/home.html
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
<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>
|
||||
17
Views/index.html
Normal file
17
Views/index.html
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
<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>
|
||||
19
Views/projects.html
Normal file
19
Views/projects.html
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
<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
Normal file
BIN
admin/.DS_Store
vendored
Normal file
Binary file not shown.
83
admin/SECURITY_README.md
Normal file
83
admin/SECURITY_README.md
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
# 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
Normal file
119
admin/config.php
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
<?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');
|
||||
}
|
||||
?>
|
||||
111
admin/contact.php
Normal file
111
admin/contact.php
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
<?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>
|
||||
34
admin/delete-project.php
Normal file
34
admin/delete-project.php
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
<?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>";
|
||||
}
|
||||
?>
|
||||
60
admin/generate_password_hash.php
Normal file
60
admin/generate_password_hash.php
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
<?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>
|
||||
61
admin/home.php
Normal file
61
admin/home.php
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
<?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>
|
||||
37
admin/htaccess
Normal file
37
admin/htaccess
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
# 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
|
||||
15
admin/includes/nav.php
Normal file
15
admin/includes/nav.php
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<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>
|
||||
36
admin/index.php
Normal file
36
admin/index.php
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
<?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>
|
||||
22
admin/init.php
Normal file
22
admin/init.php
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
<?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();
|
||||
?>
|
||||
61
admin/login.php
Normal file
61
admin/login.php
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
<?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
admin/login_attempts.json
Normal file
1
admin/login_attempts.json
Normal file
|
|
@ -0,0 +1 @@
|
|||
[]
|
||||
27
admin/logout.php
Normal file
27
admin/logout.php
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
<?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;
|
||||
?>
|
||||
126
admin/projects.php
Normal file
126
admin/projects.php
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
<?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>
|
||||
14
data/contacts.json
Normal file
14
data/contacts.json
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"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"
|
||||
}
|
||||
53
data/projects.json
Normal file
53
data/projects.json
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
[
|
||||
{
|
||||
"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
|
||||
}
|
||||
]
|
||||
54
index.html
Normal file
54
index.html
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
<!DOCTYPE html>
|
||||
<!--[if lt IE 7]> <html class="no-js lt-ie9 lt-ie8 lt-ie7"> <![endif]-->
|
||||
<!--[if IE 7]> <html class="no-js lt-ie9 lt-ie8"> <![endif]-->
|
||||
<!--[if IE 8]> <html class="no-js lt-ie9"> <![endif]-->
|
||||
<!--[if gt IE 8]> <html class="no-js"> <!--<![endif]-->
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<title>Anthony VIOLET | Consultant en développement web & applications</title>
|
||||
<meta name="description" content="Consultant spécialisé en développement web et applications. Expertise en solutions digitales, interfaces modernes et stratégies techniques sur mesure.">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="theme-color" content="#0f172a">
|
||||
<meta name="keywords" content="développement web, applications, consultant, expertise technique, développeur">
|
||||
<meta property="og:title" content="Anthony VIOLET | Consultant en développement web & applications">
|
||||
<meta property="og:description" content="Expertise en solutions digitales, interfaces modernes et stratégies techniques sur mesure.">
|
||||
<meta property="og:type" content="website">
|
||||
<!-- Polices Google Fonts -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Montserrat:wght@400;600;700;800&family=JetBrains+Mono:wght@400;700&display=swap" rel="stylesheet">
|
||||
<!-- Style principal -->
|
||||
<link rel="stylesheet" href="./Static/css/style.css">
|
||||
<script type="module" src="./Static/js/main.js" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
<!--[if lt IE 7]>
|
||||
<p class="browsehappy">You are using an <strong>outdated</strong> browser. Please <a href="#">upgrade your browser</a> to improve your experience.</p>
|
||||
<![endif]-->
|
||||
<header>
|
||||
<div class="particles" id="particles"></div>
|
||||
<div class="overlay"></div>
|
||||
<div class="header-content">
|
||||
<h1>Anthony VIOLET</h1>
|
||||
<p><span class="typing-text" id="typing-text">Développeur Web & Applications</span><span class="cursor">|</span></p>
|
||||
</div>
|
||||
</header>
|
||||
<nav>
|
||||
<ul>
|
||||
<li><a href="#" data-page="home" id="home">Home</a></li>
|
||||
<li><a href="#" data-page="about" id="about">A propos de moi</a></li>
|
||||
<li><a href="#" data-page="projects" id="projects">Projets</a></li>
|
||||
<li><a href="#" data-page="contact" id="contact">Contact</a></li>
|
||||
<li><a href="./admin/" data-page="admin" id="admin">Admin</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
<main>
|
||||
<div class="main-content"></div>
|
||||
</main>
|
||||
<footer>
|
||||
<p>© 2024 - 2025 Anthony VIOLET. All rights reserved.</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Add table
Add a link
Reference in a new issue