Initial commit
This commit is contained in:
60
templates/base.html
Normal file
60
templates/base.html
Normal file
@@ -0,0 +1,60 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="description" content="{{ config.seo.meta_description }}">
|
||||
<meta name="keywords" content="{{ config.seo.meta_keywords }}">
|
||||
<title>{% block title %}{{ config.seo.site_title }}{% endblock %}</title>
|
||||
{% load static %}
|
||||
<link rel="stylesheet" href="{% static 'css/style.css' %}">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- ─── Navigation ──────────────────────────────────────────────────── -->
|
||||
<nav class="navbar">
|
||||
<div class="nav-container">
|
||||
<a href="{% url 'projects:home' %}" class="nav-logo">
|
||||
<span class="logo-bracket"><</span>
|
||||
{{ config.navbar.logo_text }}
|
||||
<span class="logo-bracket">/></span>
|
||||
</a>
|
||||
<ul class="nav-links">
|
||||
{% for link in config.navbar.links %}
|
||||
<li>
|
||||
<a href="{% url link.url_name %}{{ link.anchor }}"
|
||||
class="nav-link {% if request.resolver_match.url_name == 'home' and forloop.first %}active{% endif %}{% if 'projet' in request.path and forloop.counter == 2 %}active{% endif %}">
|
||||
{{ link.label }}
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<button class="nav-toggle" id="navToggle" aria-label="Menu">
|
||||
<span></span><span></span><span></span>
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- ─── Contenu principal ────────────────────────────────────────────── -->
|
||||
<main class="main-content">
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
|
||||
<!-- ─── Footer ──────────────────────────────────────────────────────── -->
|
||||
<footer class="footer">
|
||||
<div class="footer-container">
|
||||
<p class="footer-text">
|
||||
<span class="logo-bracket"><</span>
|
||||
{{ config.footer.name }} — {{ config.footer.role }}
|
||||
<span class="logo-bracket">/></span>
|
||||
</p>
|
||||
<p class="footer-sub">{{ config.footer.built_with }} · {{ total_projects }} projet{{ total_projects|pluralize }}</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script src="{% static 'js/main.js' %}"></script>
|
||||
{% block extra_js %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
83
templates/cv.html
Normal file
83
templates/cv.html
Normal file
@@ -0,0 +1,83 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}CV — Alexandre Gut{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<section class="page-header">
|
||||
<div class="page-header-container">
|
||||
<span class="section-tag">Curriculum Vitae</span>
|
||||
<h1 class="page-title">Mon CV</h1>
|
||||
<p class="page-sub">Ingénieur Informatique & Systèmes Embarqués</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section cv-section">
|
||||
<div class="cv-container">
|
||||
|
||||
<!-- Boutons actions -->
|
||||
<div class="cv-actions">
|
||||
<a href="{% static 'cv_alexandre_gut.pdf' %}" download="CV_Alexandre_Gut.pdf" class="btn btn-primary">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
|
||||
<polyline points="7 10 12 15 17 10"/>
|
||||
<line x1="12" y1="15" x2="12" y2="3"/>
|
||||
</svg>
|
||||
Télécharger le CV
|
||||
</a>
|
||||
<a href="{% static 'cv_alexandre_gut.pdf' %}" target="_blank" class="btn btn-outline">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>
|
||||
<polyline points="15 3 21 3 21 9"/>
|
||||
<line x1="10" y1="14" x2="21" y2="3"/>
|
||||
</svg>
|
||||
Ouvrir dans un nouvel onglet
|
||||
</a>
|
||||
<a href="/" class="btn btn-ghost">
|
||||
← Retour
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Visionneuse PDF -->
|
||||
<div class="cv-viewer-wrapper">
|
||||
<iframe
|
||||
src="{% static 'cv_alexandre_gut.pdf' %}#toolbar=0&navpanes=0&scrollbar=1"
|
||||
class="cv-iframe"
|
||||
title="CV Alexandre Gut"
|
||||
></iframe>
|
||||
|
||||
<!-- Fallback si l'iframe ne charge pas -->
|
||||
<div class="cv-fallback" id="cvFallback" style="display:none;">
|
||||
<div class="cv-fallback-icon">📄</div>
|
||||
<p>La prévisualisation n'est pas disponible dans ce navigateur.</p>
|
||||
<a href="{% static 'cv_alexandre_gut.pdf' %}" download="CV_Alexandre_Gut.pdf" class="btn btn-primary">
|
||||
Télécharger le CV
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
// Détection mobile → rediriger vers le PDF directement
|
||||
const iframe = document.querySelector('.cv-iframe');
|
||||
if (iframe) {
|
||||
iframe.addEventListener('error', () => {
|
||||
iframe.style.display = 'none';
|
||||
document.getElementById('cvFallback').style.display = 'flex';
|
||||
});
|
||||
}
|
||||
|
||||
// Sur mobile, montrer le fallback directement
|
||||
if (window.innerWidth < 768) {
|
||||
if (iframe) iframe.style.display = 'none';
|
||||
const fallback = document.getElementById('cvFallback');
|
||||
if (fallback) fallback.style.display = 'flex';
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
191
templates/home.html
Normal file
191
templates/home.html
Normal file
@@ -0,0 +1,191 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}{{ config.seo.site_title }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<!-- ─── Hero Section ─────────────────────────────────────────────────────── -->
|
||||
<section class="hero">
|
||||
<div class="hero-container">
|
||||
<div class="hero-content">
|
||||
{% if config.profile.available %}
|
||||
<div class="hero-badge">{{ config.profile.available_label }}</div>
|
||||
{% endif %}
|
||||
<h1 class="hero-title">
|
||||
{{ config.hero.greeting }}
|
||||
<span class="gradient-text">{{ config.profile.first_name }}</span>
|
||||
</h1>
|
||||
<p class="hero-subtitle">{{ config.profile.subtitle }}</p>
|
||||
<div class="hero-actions">
|
||||
<a href="{% url 'projects:list' %}" class="btn btn-primary">
|
||||
{{ config.hero.cta_primary_label }}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
|
||||
</a>
|
||||
<a href="#contact" class="btn btn-outline">{{ config.hero.cta_secondary_label }}</a>
|
||||
<a href="{% url 'projects:cv' %}" class="btn btn-cv">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/></svg>
|
||||
Mon CV
|
||||
</a>
|
||||
</div>
|
||||
<div class="hero-stats">
|
||||
{% for stat in config.about.stats %}
|
||||
<div class="stat">
|
||||
<span class="stat-number">{{ stat.value }}</span>
|
||||
<span class="stat-label">{{ stat.label }}</span>
|
||||
</div>
|
||||
{% if not forloop.last %}<div class="stat-divider"></div>{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="hero-visual">
|
||||
<div class="code-window">
|
||||
<div class="code-header">
|
||||
<span class="dot red"></span>
|
||||
<span class="dot yellow"></span>
|
||||
<span class="dot green"></span>
|
||||
<span class="code-filename">{{ config.hero.code_window.filename }}</span>
|
||||
</div>
|
||||
<pre class="code-content"><code><span class="code-keyword">class</span> <span class="code-class">{{ config.profile.first_name }}</span>:
|
||||
<span class="code-keyword">def</span> <span class="code-func">__init__</span>(self):
|
||||
self.role = <span class="code-str">"{{ config.profile.title }}"</span>{% for group in config.skills.groups %}
|
||||
self.{{ group.key }} = [{% for item in group.items|slice:":4" %}<span class="code-str">"{{ item }}"</span>{% if not forloop.last %}, {% endif %}{% endfor %}{% if group.items|length > 4 %}, <span class="code-comment">...</span>{% endif %}]{% endfor %}
|
||||
self.email = <span class="code-str">"{{ config.profile.email }}"</span></code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ─── Compétences ──────────────────────────────────────────────────────── -->
|
||||
<section class="section" id="competences">
|
||||
<div class="section-container">
|
||||
<div class="section-header">
|
||||
<span class="section-tag">{{ config.skills.section_tag }}</span>
|
||||
<h2 class="section-title">{{ config.skills.section_title }}</h2>
|
||||
<p class="section-sub">{{ config.skills.section_subtitle }}</p>
|
||||
</div>
|
||||
<div class="skills-groups">
|
||||
{% for group in config.skills.groups %}
|
||||
<div class="skill-group">
|
||||
<h3 class="skill-group-label">{{ group.label }}</h3>
|
||||
<div class="tech-grid">
|
||||
{% for item in group.items %}
|
||||
<div class="tech-badge">
|
||||
<span class="tech-dot"></span>
|
||||
{{ item }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ─── Expériences ──────────────────────────────────────────────────────── -->
|
||||
{% if config.experience.items %}
|
||||
<section class="section section-dark" id="experience">
|
||||
<div class="section-container">
|
||||
<div class="section-header">
|
||||
<span class="section-tag">{{ config.experience.section_tag }}</span>
|
||||
<h2 class="section-title">{{ config.experience.section_title }}</h2>
|
||||
<p class="section-sub">{{ config.experience.section_subtitle }}</p>
|
||||
</div>
|
||||
<div class="experience-list">
|
||||
{% for exp in config.experience.items %}
|
||||
<div class="experience-card">
|
||||
<div class="exp-header">
|
||||
<div>
|
||||
<h3 class="exp-company">{{ exp.company }}</h3>
|
||||
<p class="exp-role">{{ exp.role }}</p>
|
||||
</div>
|
||||
<span class="exp-period">{{ exp.period }}</span>
|
||||
</div>
|
||||
<p class="exp-description">{{ exp.description }}</p>
|
||||
<div class="exp-tags">
|
||||
{% for tag in exp.tags %}
|
||||
<span class="tech-tag">{{ tag }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
<!-- ─── Projets en vedette ───────────────────────────────────────────────── -->
|
||||
<section class="section" id="projets">
|
||||
<div class="section-container">
|
||||
<div class="section-header">
|
||||
<span class="section-tag">Réalisations</span>
|
||||
<h2 class="section-title">Projets sélectionnés</h2>
|
||||
<p class="section-sub">Mes projets les plus représentatifs</p>
|
||||
</div>
|
||||
<div class="projects-grid">
|
||||
{% for project in featured_projects %}
|
||||
<div class="project-card" data-category="{{ project.category }}">
|
||||
<div class="card-image-wrapper">
|
||||
{% if project.images %}
|
||||
<img src="{{ project.images.0 }}" alt="{{ project.title }}" class="card-image" onerror="this.parentElement.classList.add('no-image')">
|
||||
{% else %}
|
||||
<div class="card-image-placeholder">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="2" y="3" width="20" height="14" rx="2"/><path d="M8 21h8M12 17v4"/></svg>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="card-category-badge">{{ project.category }}</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h3 class="card-title">{{ project.title }}</h3>
|
||||
<p class="card-desc">{{ project.short_description }}</p>
|
||||
<div class="card-techs">
|
||||
{% for tech in project.technologies|slice:":4" %}
|
||||
<span class="tech-tag">{{ tech }}</span>
|
||||
{% endfor %}
|
||||
{% if project.technologies|length > 4 %}
|
||||
<span class="tech-tag tech-tag-more">+{{ project.technologies|length|add:"-4" }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<a href="{% url 'projects:detail' project.slug %}" class="card-link">
|
||||
Voir le projet
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% empty %}
|
||||
<p class="empty-state">Aucun projet disponible. Ajoutez vos projets dans <code>data/projects.json</code>.</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% if total_projects > 3 %}
|
||||
<div class="section-footer">
|
||||
<a href="{% url 'projects:list' %}" class="btn btn-outline">
|
||||
Voir tous les projets ({{ total_projects }})
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ─── Contact ─────────────────────────────────────────────────────────── -->
|
||||
<section class="section section-dark" id="contact">
|
||||
<div class="section-container">
|
||||
<div class="section-header">
|
||||
<span class="section-tag">{{ config.contact.section_tag }}</span>
|
||||
<h2 class="section-title">{{ config.contact.section_title }}</h2>
|
||||
<p class="section-sub">{{ config.contact.section_subtitle }}</p>
|
||||
</div>
|
||||
<div class="contact-grid">
|
||||
{% for item in config.contact.items %}
|
||||
<a href="{{ item.url }}" {% if 'http' in item.url %}target="_blank"{% endif %} class="contact-card">
|
||||
<div class="contact-icon">{{ item.icon }}</div>
|
||||
<div>
|
||||
<div class="contact-label">{{ item.label }}</div>
|
||||
<div class="contact-value">{{ item.value }}</div>
|
||||
</div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{% endblock %}
|
||||
176
templates/loutre.html
Normal file
176
templates/loutre.html
Normal file
@@ -0,0 +1,176 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>❤️ Bon appétit</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: linear-gradient(135deg, #fff5f7 0%, #ffe4ec 50%, #fff0f5 100%);
|
||||
font-family: 'Georgia', serif;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Particules flottantes */
|
||||
.particles {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
}
|
||||
.particle {
|
||||
position: absolute;
|
||||
font-size: 1.2rem;
|
||||
animation: floatUp linear infinite;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
@keyframes floatUp {
|
||||
0% { transform: translateY(110vh) rotate(0deg); opacity: 0; }
|
||||
10% { opacity: 0.7; }
|
||||
90% { opacity: 0.5; }
|
||||
100% { transform: translateY(-10vh) rotate(360deg); opacity: 0; }
|
||||
}
|
||||
|
||||
/* Carte centrale */
|
||||
.card {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
background: rgba(255,255,255,0.85);
|
||||
backdrop-filter: blur(12px);
|
||||
border-radius: 32px;
|
||||
padding: 56px 64px;
|
||||
box-shadow: 0 20px 60px rgba(255, 100, 130, 0.2), 0 4px 20px rgba(0,0,0,0.06);
|
||||
text-align: center;
|
||||
max-width: 420px;
|
||||
animation: cardIn 0.8s cubic-bezier(0.34, 1.56, 0.64, 1) both;
|
||||
}
|
||||
|
||||
@keyframes cardIn {
|
||||
from { opacity: 0; transform: scale(0.7) translateY(30px); }
|
||||
to { opacity: 1; transform: scale(1) translateY(0); }
|
||||
}
|
||||
|
||||
/* Cœur SVG animé */
|
||||
.heart-wrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.heart-svg {
|
||||
width: 110px;
|
||||
height: 110px;
|
||||
animation: heartbeat 1.2s ease-in-out infinite;
|
||||
filter: drop-shadow(0 6px 18px rgba(255, 80, 100, 0.45));
|
||||
}
|
||||
|
||||
@keyframes heartbeat {
|
||||
0% { transform: scale(1); }
|
||||
14% { transform: scale(1.18); }
|
||||
28% { transform: scale(1); }
|
||||
42% { transform: scale(1.12); }
|
||||
70% { transform: scale(1); }
|
||||
100% { transform: scale(1); }
|
||||
}
|
||||
|
||||
/* Texte principal */
|
||||
.main-text {
|
||||
font-size: 1.9rem;
|
||||
font-weight: bold;
|
||||
color: #d63060;
|
||||
letter-spacing: -0.3px;
|
||||
margin-bottom: 10px;
|
||||
animation: fadeIn 0.6s 0.4s both;
|
||||
}
|
||||
|
||||
.sub-text {
|
||||
font-size: 1rem;
|
||||
color: #e07090;
|
||||
font-style: italic;
|
||||
animation: fadeIn 0.6s 0.7s both;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
/* Ligne décorative */
|
||||
.divider {
|
||||
width: 48px;
|
||||
height: 3px;
|
||||
background: linear-gradient(90deg, #ff6b8a, #ff9eb5);
|
||||
border-radius: 2px;
|
||||
margin: 20px auto;
|
||||
animation: fadeIn 0.6s 0.5s both;
|
||||
}
|
||||
|
||||
/* Petite signature */
|
||||
.signature {
|
||||
margin-top: 28px;
|
||||
font-size: 0.8rem;
|
||||
color: #cca0b0;
|
||||
letter-spacing: 0.5px;
|
||||
animation: fadeIn 0.6s 1s both;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- Particules -->
|
||||
<div class="particles" id="particles"></div>
|
||||
|
||||
<!-- Carte -->
|
||||
<div class="card">
|
||||
<div class="heart-wrapper">
|
||||
<svg class="heart-svg" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="hg" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#ff4f6d"/>
|
||||
<stop offset="100%" style="stop-color:#ff8fa3"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path d="M50 88 C50 88 10 62 10 36 C10 20 22 10 36 10 C43 10 50 16 50 16 C50 16 57 10 64 10 C78 10 90 20 90 36 C90 62 50 88 50 88Z"
|
||||
fill="url(#hg)"/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div class="main-text">Bon appétit</div>
|
||||
<div class="divider"></div>
|
||||
<div class="sub-text">petite loutre 🦦</div>
|
||||
|
||||
<div class="signature">avec tout mon amour ❤️</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Génère des mini cœurs flottants
|
||||
const emojis = ['❤️', '🦦', '✨', '💕', '🌸', '💖'];
|
||||
const container = document.getElementById('particles');
|
||||
|
||||
function spawnParticle() {
|
||||
const el = document.createElement('div');
|
||||
el.className = 'particle';
|
||||
el.textContent = emojis[Math.floor(Math.random() * emojis.length)];
|
||||
el.style.left = Math.random() * 100 + 'vw';
|
||||
el.style.fontSize = (0.8 + Math.random() * 1.2) + 'rem';
|
||||
const dur = 6 + Math.random() * 8;
|
||||
el.style.animationDuration = dur + 's';
|
||||
el.style.animationDelay = (Math.random() * dur) + 's';
|
||||
container.appendChild(el);
|
||||
setTimeout(() => el.remove(), (dur + 2) * 1000);
|
||||
}
|
||||
|
||||
// Lance les particules
|
||||
for (let i = 0; i < 18; i++) setTimeout(spawnParticle, i * 350);
|
||||
setInterval(spawnParticle, 800);
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
212
templates/projects/detail.html
Normal file
212
templates/projects/detail.html
Normal file
@@ -0,0 +1,212 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}{{ project.title }} — {{ config.seo.site_title }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<!-- ─── En-tête projet ───────────────────────────────────────────────────── -->
|
||||
<section class="project-hero">
|
||||
<div class="project-hero-container">
|
||||
<nav class="breadcrumb">
|
||||
<a href="{% url 'projects:home' %}">Accueil</a>
|
||||
<span class="breadcrumb-sep">›</span>
|
||||
<a href="{% url 'projects:list' %}">Projets</a>
|
||||
<span class="breadcrumb-sep">›</span>
|
||||
<span>{{ project.title }}</span>
|
||||
</nav>
|
||||
<div class="project-hero-content">
|
||||
<div class="project-meta-top">
|
||||
<span class="project-category-badge">{{ project.category }}</span>
|
||||
{% if project.context %}<span class="project-context-badge">{{ project.context }}</span>{% endif %}
|
||||
{% if project.complexity %}<span class="complexity-badge complexity-{{ project.complexity|lower }}">{{ project.complexity }}</span>{% endif %}
|
||||
</div>
|
||||
<h1 class="project-hero-title">{{ project.title }}</h1>
|
||||
<p class="project-hero-desc">{{ project.short_description }}</p>
|
||||
<div class="project-techs-header">
|
||||
{% for tech in project.technologies %}
|
||||
<span class="tech-tag tech-tag-lg">{{ tech }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% if project.github_url or project.demo_url %}
|
||||
<div class="project-links">
|
||||
{% if project.github_url %}
|
||||
<a href="{{ project.github_url }}" target="_blank" class="btn btn-outline btn-sm">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0 0 24 12c0-6.63-5.37-12-12-12z"/></svg>
|
||||
Voir le code
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if project.demo_url %}
|
||||
<a href="{{ project.demo_url }}" target="_blank" class="btn btn-primary btn-sm">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg>
|
||||
Voir la démo
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ─── Contenu détaillé ─────────────────────────────────────────────────── -->
|
||||
<section class="section">
|
||||
<div class="detail-container">
|
||||
<div class="detail-main">
|
||||
|
||||
<!-- Galerie images -->
|
||||
{% if project.images %}
|
||||
<div class="gallery-section">
|
||||
<h2 class="detail-section-title">Galerie</h2>
|
||||
<div class="gallery-grid">
|
||||
{% for img in project.images %}
|
||||
<div class="gallery-item" onclick="openLightbox('{{ img }}')">
|
||||
<img src="{{ img }}" alt="{{ project.title }} — image {{ forloop.counter }}" loading="lazy">
|
||||
<div class="gallery-overlay">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2"><path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7"/></svg>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Problématique & Solution -->
|
||||
{% if project.problem or project.solution %}
|
||||
<div class="problem-solution-grid">
|
||||
{% if project.problem %}
|
||||
<div class="ps-card ps-problem">
|
||||
<div class="ps-icon">⚡</div>
|
||||
<h3 class="ps-title">Problématique</h3>
|
||||
<p class="ps-text">{{ project.problem }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if project.solution %}
|
||||
<div class="ps-card ps-solution">
|
||||
<div class="ps-icon">✓</div>
|
||||
<h3 class="ps-title">Solution apportée</h3>
|
||||
<p class="ps-text">{{ project.solution }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Description complète -->
|
||||
<div class="description-section">
|
||||
<h2 class="detail-section-title">Description du projet</h2>
|
||||
<div class="description-content">
|
||||
{{ project.full_description|linebreaks }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Résultat -->
|
||||
{% if project.outcome %}
|
||||
<div class="outcome-section">
|
||||
<h2 class="detail-section-title">Résultat</h2>
|
||||
<div class="outcome-card">
|
||||
<span class="outcome-icon">🎯</span>
|
||||
<p>{{ project.outcome }}</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
|
||||
<!-- ─── Sidebar ──────────────────────────────────────────────────── -->
|
||||
<aside class="detail-sidebar">
|
||||
|
||||
<!-- Infos clés -->
|
||||
<div class="sidebar-card">
|
||||
<h3 class="sidebar-card-title">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
|
||||
Informations
|
||||
</h3>
|
||||
<ul class="info-list">
|
||||
{% if project.context %}
|
||||
<li class="info-item"><span class="info-label">Contexte</span><span class="info-value">{{ project.context }}</span></li>
|
||||
{% endif %}
|
||||
{% if project.period %}
|
||||
<li class="info-item"><span class="info-label">Période</span><span class="info-value">{{ project.period }}</span></li>
|
||||
{% endif %}
|
||||
{% if project.role %}
|
||||
<li class="info-item"><span class="info-label">Rôle</span><span class="info-value">{{ project.role }}</span></li>
|
||||
{% endif %}
|
||||
{% if project.complexity %}
|
||||
<li class="info-item"><span class="info-label">Complexité</span><span class="info-value">{{ project.complexity }}</span></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Points clés -->
|
||||
{% if project.highlights %}
|
||||
<div class="sidebar-card">
|
||||
<h3 class="sidebar-card-title">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg>
|
||||
Points clés
|
||||
</h3>
|
||||
<ul class="highlights-list">
|
||||
{% for highlight in project.highlights %}
|
||||
<li class="highlight-item">
|
||||
<span class="highlight-check">✓</span>
|
||||
{{ highlight }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Technologies -->
|
||||
<div class="sidebar-card">
|
||||
<h3 class="sidebar-card-title">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>
|
||||
Technologies
|
||||
</h3>
|
||||
<div class="sidebar-techs">
|
||||
{% for tech in project.technologies %}
|
||||
<a href="{% url 'projects:list' %}?tech={{ tech }}" class="tech-tag tech-tag-clickable">{{ tech }}</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Catégorie -->
|
||||
<div class="sidebar-card">
|
||||
<h3 class="sidebar-card-title">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 20h16M4 4h16M9 9h6M9 15h6"/></svg>
|
||||
Catégorie
|
||||
</h3>
|
||||
<a href="{% url 'projects:list' %}?category={{ project.category }}" class="category-link">
|
||||
{{ project.category }}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</aside>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ─── Navigation projet précédent / suivant ────────────────────────────── -->
|
||||
<section class="project-nav-section">
|
||||
<div class="project-nav-container">
|
||||
{% if prev_project %}
|
||||
<a href="{% url 'projects:detail' prev_project.slug %}" class="project-nav-card project-nav-prev">
|
||||
<span class="nav-direction">← Précédent</span>
|
||||
<span class="nav-project-title">{{ prev_project.title }}</span>
|
||||
</a>
|
||||
{% else %}<div></div>{% endif %}
|
||||
|
||||
<a href="{% url 'projects:list' %}" class="btn btn-outline">Tous les projets</a>
|
||||
|
||||
{% if next_project %}
|
||||
<a href="{% url 'projects:detail' next_project.slug %}" class="project-nav-card project-nav-next">
|
||||
<span class="nav-direction">Suivant →</span>
|
||||
<span class="nav-project-title">{{ next_project.title }}</span>
|
||||
</a>
|
||||
{% else %}<div></div>{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ─── Lightbox ─────────────────────────────────────────────────────────── -->
|
||||
<div class="lightbox" id="lightbox" onclick="closeLightbox()">
|
||||
<button class="lightbox-close" onclick="closeLightbox()">✕</button>
|
||||
<img class="lightbox-img" id="lightboxImg" src="" alt="">
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
167
templates/projects/list.html
Normal file
167
templates/projects/list.html
Normal file
@@ -0,0 +1,167 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Projets — Portfolio Alexandre{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<!-- ─── En-tête page projets ─────────────────────────────────────────────── -->
|
||||
<section class="page-header">
|
||||
<div class="page-header-container">
|
||||
<span class="section-tag">Réalisations</span>
|
||||
<h1 class="page-title">Mes Projets</h1>
|
||||
<p class="page-sub">{{ total_count }} projet{{ total_count|pluralize }} — filtrez par technologie ou catégorie</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ─── Filtres ──────────────────────────────────────────────────────────── -->
|
||||
<section class="filters-section">
|
||||
<div class="filters-container">
|
||||
<form method="GET" action="{% url 'projects:list' %}" class="filters-form" id="filtersForm">
|
||||
|
||||
<!-- Filtre catégorie -->
|
||||
<div class="filter-group">
|
||||
<label class="filter-label">Catégorie</label>
|
||||
<div class="filter-pills" id="categoryPills">
|
||||
<button type="button"
|
||||
class="filter-pill {% if not selected_category %}active{% endif %}"
|
||||
data-filter="category"
|
||||
data-value="">
|
||||
Toutes
|
||||
</button>
|
||||
{% for cat in categories %}
|
||||
<button type="button"
|
||||
class="filter-pill {% if selected_category == cat %}active{% endif %}{% if forloop.counter > 5 %} pill-hidden{% endif %}"
|
||||
data-filter="category"
|
||||
data-value="{{ cat }}"
|
||||
{% if forloop.counter > 5 %}data-extra="true"{% endif %}>
|
||||
{{ cat }}
|
||||
</button>
|
||||
{% endfor %}
|
||||
{% if categories|length > 5 %}
|
||||
<button type="button" class="filter-pill pill-more" data-target="categoryPills" data-total="{{ categories|length|add:'-5' }}">
|
||||
+{{ categories|length|add:"-5" }} voir plus
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
<input type="hidden" name="category" id="categoryInput" value="{{ selected_category }}">
|
||||
</div>
|
||||
|
||||
<!-- Filtre technologie -->
|
||||
<div class="filter-group">
|
||||
<label class="filter-label">Technologie</label>
|
||||
<div class="filter-pills" id="techPills">
|
||||
<button type="button"
|
||||
class="filter-pill {% if not selected_tech %}active{% endif %}"
|
||||
data-filter="tech"
|
||||
data-value="">
|
||||
Toutes
|
||||
</button>
|
||||
{% for tech in technologies %}
|
||||
<button type="button"
|
||||
class="filter-pill {% if selected_tech == tech %}active{% endif %}{% if forloop.counter > 5 %} pill-hidden{% endif %}"
|
||||
data-filter="tech"
|
||||
data-value="{{ tech }}"
|
||||
{% if forloop.counter > 5 %}data-extra="true"{% endif %}>
|
||||
{{ tech }}
|
||||
</button>
|
||||
{% endfor %}
|
||||
{% if technologies|length > 5 %}
|
||||
<button type="button" class="filter-pill pill-more" data-target="techPills" data-total="{{ technologies|length|add:'-5' }}">
|
||||
+{{ technologies|length|add:"-5" }} voir plus
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
<input type="hidden" name="tech" id="techInput" value="{{ selected_tech }}">
|
||||
</div>
|
||||
|
||||
{% if selected_category or selected_tech %}
|
||||
<a href="{% url 'projects:list' %}" class="btn btn-ghost btn-sm">
|
||||
✕ Réinitialiser les filtres
|
||||
</a>
|
||||
{% endif %}
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ─── Grille projets ───────────────────────────────────────────────────── -->
|
||||
<section class="section">
|
||||
<div class="section-container">
|
||||
|
||||
{% if selected_category or selected_tech %}
|
||||
<div class="filter-result-info">
|
||||
<span>{{ total_count }} résultat{{ total_count|pluralize }}</span>
|
||||
{% if selected_category %}<span class="filter-tag-active">{{ selected_category }}</span>{% endif %}
|
||||
{% if selected_tech %}<span class="filter-tag-active">{{ selected_tech }}</span>{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="projects-grid projects-grid-full" id="projectsGrid">
|
||||
{% for project in projects %}
|
||||
<div class="project-card">
|
||||
<div class="card-image-wrapper">
|
||||
{% if project.images %}
|
||||
<img src="{{ project.images.0 }}" alt="{{ project.title }}" class="card-image" onerror="this.parentElement.classList.add('no-image')">
|
||||
{% else %}
|
||||
<div class="card-image-placeholder">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="2" y="3" width="20" height="14" rx="2"/><path d="M8 21h8M12 17v4"/></svg>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="card-category-badge">{{ project.category }}</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h3 class="card-title">{{ project.title }}</h3>
|
||||
<p class="card-desc">{{ project.short_description }}</p>
|
||||
<div class="card-techs">
|
||||
{% for tech in project.technologies|slice:":5" %}
|
||||
<span class="tech-tag">{{ tech }}</span>
|
||||
{% endfor %}
|
||||
{% if project.technologies|length > 5 %}
|
||||
<span class="tech-tag tech-tag-more">+{{ project.technologies|length|add:"-5" }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<a href="{% url 'projects:detail' project.slug %}" class="card-link">
|
||||
Voir le projet
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% empty %}
|
||||
<div class="empty-state-container">
|
||||
<div class="empty-icon">🔍</div>
|
||||
<h3>Aucun projet trouvé</h3>
|
||||
<p>Essayez de modifier vos filtres ou <a href="{% url 'projects:list' %}">réinitialisez</a>.</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
document.querySelectorAll('.pill-more').forEach(btn => {
|
||||
const container = document.getElementById(btn.dataset.target);
|
||||
// Sélectionne uniquement les pills "extra" (au-delà des 5 premières)
|
||||
const extraPills = container.querySelectorAll('.pill-hidden[data-filter]');
|
||||
const total = parseInt(btn.dataset.total || extraPills.length);
|
||||
|
||||
btn.addEventListener('click', () => {
|
||||
const isExpanded = btn.dataset.expanded === 'true';
|
||||
|
||||
if (!isExpanded) {
|
||||
container.querySelectorAll('.pill-hidden[data-filter]').forEach(p => p.classList.remove('pill-hidden'));
|
||||
btn.textContent = 'voir moins ↑';
|
||||
btn.classList.add('pill-less');
|
||||
btn.dataset.expanded = 'true';
|
||||
} else {
|
||||
container.querySelectorAll('.filter-pill[data-extra="true"]').forEach(p => p.classList.add('pill-hidden'));
|
||||
btn.textContent = '+' + total + ' voir plus';
|
||||
btn.classList.remove('pill-less');
|
||||
btn.dataset.expanded = 'false';
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user