Initial commit
This commit is contained in:
0
projects/__init__.py
Normal file
0
projects/__init__.py
Normal file
25
projects/urls.py
Normal file
25
projects/urls.py
Normal file
@@ -0,0 +1,25 @@
|
||||
"""
|
||||
URLs de l'application projects.
|
||||
"""
|
||||
|
||||
from django.urls import path
|
||||
from . import views
|
||||
|
||||
app_name = 'projects'
|
||||
|
||||
urlpatterns = [
|
||||
# Page surprise ❤️
|
||||
path('loutre', views.loutre, name='loutre'),
|
||||
|
||||
# Page d'accueil
|
||||
path('', views.home, name='home'),
|
||||
|
||||
# CV viewer
|
||||
path('cv/', views.cv, name='cv'),
|
||||
|
||||
# Liste de tous les projets avec filtres
|
||||
path('projets/', views.project_list, name='list'),
|
||||
|
||||
# Détail d'un projet par son slug
|
||||
path('projets/<slug:slug>/', views.project_detail, name='detail'),
|
||||
]
|
||||
125
projects/utils.py
Normal file
125
projects/utils.py
Normal file
@@ -0,0 +1,125 @@
|
||||
"""
|
||||
Utilitaires pour charger et manipuler les données projets depuis le JSON.
|
||||
Modifier uniquement data/projects.json pour mettre à jour le portfolio.
|
||||
"""
|
||||
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Optional, List, Dict
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
def load_config() -> Dict:
|
||||
"""
|
||||
Charge la configuration globale du site depuis data/config.json.
|
||||
Contient : profil, navbar, compétences, expériences, contact, footer, SEO.
|
||||
Pour modifier les infos du site, éditez uniquement data/config.json.
|
||||
"""
|
||||
config_path: Path = settings.CONFIG_JSON_PATH
|
||||
|
||||
if not config_path.exists():
|
||||
return {}
|
||||
|
||||
with open(config_path, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def load_projects() -> List[Dict]:
|
||||
"""
|
||||
Charge tous les projets depuis le fichier JSON.
|
||||
Retourne une liste de dictionnaires projets.
|
||||
"""
|
||||
json_path: Path = settings.PROJECTS_JSON_PATH
|
||||
|
||||
if not json_path.exists():
|
||||
return []
|
||||
|
||||
with open(json_path, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
|
||||
projects = data.get('projects', [])
|
||||
|
||||
# Générer un slug unique pour chaque projet (utilisé dans les URLs)
|
||||
for i, project in enumerate(projects):
|
||||
project['slug'] = _slugify(project.get('title', f'projet-{i}'))
|
||||
project['id'] = i # Index pour retrouver le projet facilement
|
||||
|
||||
return projects
|
||||
|
||||
|
||||
def get_project_by_slug(slug: str) -> Optional[Dict]:
|
||||
"""
|
||||
Retourne un projet spécifique par son slug.
|
||||
"""
|
||||
projects = load_projects()
|
||||
for project in projects:
|
||||
if project.get('slug') == slug:
|
||||
return project
|
||||
return None
|
||||
|
||||
|
||||
def get_all_categories(projects: List[Dict]) -> List[str]:
|
||||
"""
|
||||
Retourne la liste unique de toutes les catégories disponibles.
|
||||
"""
|
||||
categories = set()
|
||||
for project in projects:
|
||||
cat = project.get('category', '').strip()
|
||||
if cat:
|
||||
categories.add(cat)
|
||||
return sorted(list(categories))
|
||||
|
||||
|
||||
def get_all_technologies(projects: List[Dict]) -> List[str]:
|
||||
"""
|
||||
Retourne la liste unique de toutes les technologies utilisées,
|
||||
triées par fréquence décroissante (les plus utilisées en premier).
|
||||
"""
|
||||
freq: Dict[str, int] = {}
|
||||
for project in projects:
|
||||
for tech in project.get('technologies', []):
|
||||
t = tech.strip()
|
||||
freq[t] = freq.get(t, 0) + 1
|
||||
return sorted(freq.keys(), key=lambda t: (-freq[t], t))
|
||||
|
||||
|
||||
def filter_projects(projects: List[Dict], category: str = '', tech: str = '') -> List[Dict]:
|
||||
"""
|
||||
Filtre les projets par catégorie et/ou technologie.
|
||||
"""
|
||||
filtered = projects
|
||||
|
||||
if category:
|
||||
filtered = [p for p in filtered if p.get('category', '').lower() == category.lower()]
|
||||
|
||||
if tech:
|
||||
filtered = [
|
||||
p for p in filtered
|
||||
if any(t.lower() == tech.lower() for t in p.get('technologies', []))
|
||||
]
|
||||
|
||||
return filtered
|
||||
|
||||
|
||||
def _slugify(text: str) -> str:
|
||||
"""
|
||||
Convertit un titre en slug URL-safe.
|
||||
Ex: "Mon Super Projet" → "mon-super-projet"
|
||||
"""
|
||||
text = text.lower().strip()
|
||||
# Remplacer les caractères accentués
|
||||
replacements = {
|
||||
'à': 'a', 'â': 'a', 'ä': 'a',
|
||||
'é': 'e', 'è': 'e', 'ê': 'e', 'ë': 'e',
|
||||
'î': 'i', 'ï': 'i',
|
||||
'ô': 'o', 'ö': 'o',
|
||||
'ù': 'u', 'û': 'u', 'ü': 'u',
|
||||
'ç': 'c', 'ñ': 'n',
|
||||
}
|
||||
for char, replacement in replacements.items():
|
||||
text = text.replace(char, replacement)
|
||||
# Remplacer tout caractère non alphanumérique par un tiret
|
||||
text = re.sub(r'[^a-z0-9]+', '-', text)
|
||||
text = text.strip('-')
|
||||
return text
|
||||
97
projects/views.py
Normal file
97
projects/views.py
Normal file
@@ -0,0 +1,97 @@
|
||||
"""
|
||||
Vues principales du portfolio.
|
||||
Toutes les données viennent de :
|
||||
- data/projects.json → projets
|
||||
- data/config.json → profil, compétences, contact, footer, SEO...
|
||||
"""
|
||||
|
||||
from django.shortcuts import render
|
||||
from django.http import Http404
|
||||
from .utils import (
|
||||
load_config,
|
||||
load_projects,
|
||||
get_project_by_slug,
|
||||
get_all_categories,
|
||||
get_all_technologies,
|
||||
filter_projects,
|
||||
)
|
||||
|
||||
|
||||
def loutre(request):
|
||||
"""Page surprise ❤️"""
|
||||
return render(request, 'loutre.html')
|
||||
|
||||
|
||||
def cv(request):
|
||||
"""Page visionneuse CV"""
|
||||
return render(request, 'cv.html')
|
||||
|
||||
|
||||
def _base_context():
|
||||
"""
|
||||
Retourne le contexte de base commun à toutes les pages
|
||||
(config complète du site + total projets).
|
||||
"""
|
||||
config = load_config()
|
||||
all_projects = load_projects()
|
||||
return config, {
|
||||
'config': config,
|
||||
'total_projects': len(all_projects),
|
||||
}
|
||||
|
||||
|
||||
def home(request):
|
||||
"""
|
||||
Page d'accueil : hero section + compétences + aperçu des projets.
|
||||
"""
|
||||
config, context = _base_context()
|
||||
all_projects = load_projects()
|
||||
|
||||
context.update({
|
||||
'featured_projects': all_projects[:3],
|
||||
'all_techs': get_all_technologies(all_projects),
|
||||
})
|
||||
return render(request, 'home.html', context)
|
||||
|
||||
|
||||
def project_list(request):
|
||||
"""
|
||||
Page liste des projets avec filtres par catégorie et technologie.
|
||||
"""
|
||||
config, context = _base_context()
|
||||
all_projects = load_projects()
|
||||
|
||||
selected_category = request.GET.get('category', '').strip()
|
||||
selected_tech = request.GET.get('tech', '').strip()
|
||||
projects = filter_projects(all_projects, category=selected_category, tech=selected_tech)
|
||||
|
||||
context.update({
|
||||
'projects': projects,
|
||||
'categories': get_all_categories(all_projects),
|
||||
'technologies': get_all_technologies(all_projects),
|
||||
'selected_category': selected_category,
|
||||
'selected_tech': selected_tech,
|
||||
'total_count': len(projects),
|
||||
})
|
||||
return render(request, 'projects/list.html', context)
|
||||
|
||||
|
||||
def project_detail(request, slug):
|
||||
"""
|
||||
Page détail d'un projet : description complète + galerie + highlights.
|
||||
"""
|
||||
project = get_project_by_slug(slug)
|
||||
|
||||
if project is None:
|
||||
raise Http404("Projet introuvable.")
|
||||
|
||||
config, context = _base_context()
|
||||
all_projects = load_projects()
|
||||
current_index = project.get('id', 0)
|
||||
|
||||
context.update({
|
||||
'project': project,
|
||||
'prev_project': all_projects[current_index - 1] if current_index > 0 else None,
|
||||
'next_project': all_projects[current_index + 1] if current_index < len(all_projects) - 1 else None,
|
||||
})
|
||||
return render(request, 'projects/detail.html', context)
|
||||
Reference in New Issue
Block a user