Compare commits
No commits in common. "73d4d2fa064b876b050d059095ce3c9ca3ae8092" and "cd1ca94b116504b16c1fabc815c805c0a2983740" have entirely different histories.
73d4d2fa06
...
cd1ca94b11
|
|
@ -1,4 +1,4 @@
|
|||
WEBDAV_URL=https://nas.arfaoui.net:6006
|
||||
WEBDAV_PATH=/jalil.arfaoui.net/photos
|
||||
WEBDAV_USER=CleverCloud
|
||||
WEBDAV_PASS=pwd
|
||||
WEBDAV_PATH=/photo/Portfolio
|
||||
WEBDAV_USER=your_username
|
||||
WEBDAV_PASS=your_password
|
||||
9
.gitignore
vendored
|
|
@ -28,12 +28,3 @@ src/assets/images/photos/
|
|||
|
||||
# jetbrains setting folder
|
||||
.idea/
|
||||
|
||||
# claude code
|
||||
.claude/
|
||||
|
||||
# direnv cache
|
||||
.direnv/
|
||||
|
||||
# clever cloud
|
||||
.clever.json
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 289 KiB After Width: | Height: | Size: 289 KiB |
|
Before Width: | Height: | Size: 862 KiB After Width: | Height: | Size: 862 KiB |
BIN
public/assets/images/posts/code-canvas.jpg
Normal file
|
After Width: | Height: | Size: 86 KiB |
BIN
public/assets/images/posts/coffee.jpg
Normal file
|
After Width: | Height: | Size: 123 KiB |
BIN
public/assets/images/posts/flowchart.jpg
Normal file
|
After Width: | Height: | Size: 224 KiB |
BIN
public/assets/images/posts/perfect-coffee.jpg
Normal file
|
After Width: | Height: | Size: 172 KiB |
BIN
public/assets/images/posts/pour-over.jpg
Normal file
|
After Width: | Height: | Size: 54 KiB |
BIN
public/assets/images/posts/vintage-tech-01.jpg
Normal file
|
After Width: | Height: | Size: 150 KiB |
BIN
public/assets/images/posts/vintage-tech-02.jpg
Normal file
|
After Width: | Height: | Size: 335 KiB |
BIN
public/assets/images/posts/workspace.jpg
Normal file
|
After Width: | Height: | Size: 120 KiB |
|
|
@ -1,7 +1,7 @@
|
|||
import "dotenv/config";
|
||||
import { createClient } from "webdav";
|
||||
import { mkdir, writeFile, stat, readdir, rm } from "fs/promises";
|
||||
import { join, dirname, relative } from "path";
|
||||
import { mkdir, writeFile, stat } from "fs/promises";
|
||||
import { join, dirname } from "path";
|
||||
|
||||
interface FileStat {
|
||||
filename: string;
|
||||
|
|
@ -30,25 +30,13 @@ async function main() {
|
|||
|
||||
console.log(`Fetching images from ${WEBDAV_URL}${WEBDAV_PATH}...`);
|
||||
|
||||
// Collecter tous les fichiers/dossiers distants
|
||||
const remoteItems = new Set<string>();
|
||||
|
||||
await syncDirectory(client, WEBDAV_PATH, DEST_DIR, remoteItems);
|
||||
|
||||
// Supprimer les fichiers locaux qui n'existent plus sur le NAS
|
||||
await cleanupLocalFiles(DEST_DIR, remoteItems);
|
||||
await syncDirectory(client, WEBDAV_PATH, DEST_DIR);
|
||||
|
||||
console.log("Done!");
|
||||
}
|
||||
|
||||
async function syncDirectory(
|
||||
client: ReturnType<typeof createClient>,
|
||||
remotePath: string,
|
||||
localPath: string,
|
||||
remoteItems: Set<string>
|
||||
) {
|
||||
async function syncDirectory(client: ReturnType<typeof createClient>, remotePath: string, localPath: string) {
|
||||
await mkdir(localPath, { recursive: true });
|
||||
remoteItems.add(localPath);
|
||||
|
||||
const items = (await client.getDirectoryContents(remotePath)) as FileStat[];
|
||||
|
||||
|
|
@ -58,9 +46,8 @@ async function syncDirectory(
|
|||
|
||||
if (item.type === "directory") {
|
||||
console.log(` [dir] ${item.basename}/`);
|
||||
await syncDirectory(client, remoteItemPath, localItemPath, remoteItems);
|
||||
await syncDirectory(client, remoteItemPath, localItemPath);
|
||||
} else if (item.type === "file" && /\.(jpg|jpeg|png|webp)$/i.test(item.basename)) {
|
||||
remoteItems.add(localItemPath);
|
||||
const needsDownload = await shouldDownload(localItemPath, item);
|
||||
|
||||
if (needsDownload) {
|
||||
|
|
@ -94,72 +81,6 @@ async function shouldDownload(localPath: string, remoteItem: FileStat): Promise<
|
|||
}
|
||||
}
|
||||
|
||||
async function cleanupLocalFiles(localDir: string, remoteItems: Set<string>) {
|
||||
const localFiles = await collectLocalFiles(localDir);
|
||||
let deletedCount = 0;
|
||||
|
||||
for (const localFile of localFiles) {
|
||||
if (!remoteItems.has(localFile)) {
|
||||
const relativePath = relative(localDir, localFile);
|
||||
console.log(` [delete] ${relativePath}`);
|
||||
await rm(localFile, { recursive: true, force: true });
|
||||
deletedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (deletedCount > 0) {
|
||||
console.log(`Deleted ${deletedCount} orphaned file(s)/folder(s)`);
|
||||
// Nettoyer les dossiers vides
|
||||
await cleanupEmptyDirs(localDir);
|
||||
}
|
||||
}
|
||||
|
||||
async function collectLocalFiles(dir: string): Promise<string[]> {
|
||||
const files: string[] = [];
|
||||
|
||||
try {
|
||||
const entries = await readdir(dir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = join(dir, entry.name);
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
files.push(fullPath);
|
||||
const subFiles = await collectLocalFiles(fullPath);
|
||||
files.push(...subFiles);
|
||||
} else if (/\.(jpg|jpeg|png|webp)$/i.test(entry.name)) {
|
||||
files.push(fullPath);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Dossier n'existe pas encore
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
async function cleanupEmptyDirs(dir: string) {
|
||||
try {
|
||||
const entries = await readdir(dir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
if (entry.isDirectory()) {
|
||||
const subDir = join(dir, entry.name);
|
||||
await cleanupEmptyDirs(subDir);
|
||||
|
||||
// Vérifier si le dossier est vide après nettoyage récursif
|
||||
const subEntries = await readdir(subDir);
|
||||
if (subEntries.length === 0) {
|
||||
console.log(` [delete] ${relative(DEST_DIR, subDir)}/ (empty)`);
|
||||
await rm(subDir, { recursive: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Ignore errors
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error("Error:", err.message);
|
||||
process.exit(1);
|
||||
|
|
|
|||
|
|
@ -16,22 +16,6 @@ const translations: Record<string, Record<string, string>> = {
|
|||
en: '/en/about',
|
||||
ar: '/ar/نبذة-عني'
|
||||
},
|
||||
// Photo
|
||||
'/photo': {
|
||||
fr: '/photo',
|
||||
en: '/en/photo',
|
||||
ar: '/ar/تصوير'
|
||||
},
|
||||
'/en/photo': {
|
||||
fr: '/photo',
|
||||
en: '/en/photo',
|
||||
ar: '/ar/تصوير'
|
||||
},
|
||||
'/ar/تصوير': {
|
||||
fr: '/photo',
|
||||
en: '/en/photo',
|
||||
ar: '/ar/تصوير'
|
||||
},
|
||||
// Page d'accueil
|
||||
'/': {
|
||||
fr: '/',
|
||||
|
|
|
|||
|
|
@ -1,28 +1,22 @@
|
|||
---
|
||||
import Logo from "../components/logo.astro";
|
||||
|
||||
// Détection de la langue courante par le path uniquement
|
||||
// Détection de la langue courante
|
||||
const pathname = Astro.url.pathname;
|
||||
const currentLang = pathname.startsWith('/en') ? 'en' : pathname.startsWith('/ar') ? 'ar' : 'fr';
|
||||
|
||||
// Menu localisé
|
||||
const menus = {
|
||||
fr: [
|
||||
{ name: 'Code', url: '/code' },
|
||||
{ name: 'Théâtre', url: '/theatre' },
|
||||
{ name: 'Photo', url: '/photo' },
|
||||
{ name: 'À propos', url: '/a-propos' }
|
||||
],
|
||||
en: [
|
||||
{ name: 'Code', url: '/en/code' },
|
||||
{ name: 'Acting', url: '/en/acting' },
|
||||
{ name: 'Photo', url: '/en/photo' },
|
||||
{ name: 'Photo', url: '/photo' },
|
||||
{ name: 'About', url: '/en/about' }
|
||||
],
|
||||
ar: [
|
||||
{ name: 'برمجة', url: '/ar/برمجة' },
|
||||
{ name: 'مسرح', url: '/ar/مسرح' },
|
||||
{ name: 'صور', url: '/ar/تصوير' },
|
||||
{ name: 'صور', url: '/photo' },
|
||||
{ name: 'نبذة عني', url: '/ar/نبذة-عني' }
|
||||
]
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,14 +2,12 @@
|
|||
import HomeIcon from "./icons/HomeIcon.astro";
|
||||
|
||||
const pathname = Astro.url.pathname;
|
||||
const currentLang = pathname.startsWith('/en') ? 'en' : pathname.startsWith('/ar') ? 'ar' : 'fr';
|
||||
const isHome = pathname === '/' || pathname === '/en' || pathname === '/en/' || pathname === '/ar' || pathname === '/ar/';
|
||||
const homeUrl = currentLang === 'en' ? '/en' : currentLang === 'ar' ? '/ar' : '/';
|
||||
---
|
||||
|
||||
{!isHome && (
|
||||
<a
|
||||
href={homeUrl}
|
||||
href="/"
|
||||
class="group relative z-30 flex items-center text-neutral-600 dark:text-neutral-300 hover:text-black dark:hover:text-white transition-colors"
|
||||
title="Accueil"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
---
|
||||
import { Picture } from 'astro:assets';
|
||||
import HeroViewport from './HeroViewport.astro';
|
||||
import { getDateLocale, type Locale } from '../../utils/i18n';
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
|
|
@ -10,10 +9,9 @@ interface Props {
|
|||
tags?: string[];
|
||||
coverImage?: ImageMetadata;
|
||||
scrollTarget?: string;
|
||||
lang?: Locale;
|
||||
}
|
||||
|
||||
const { title, description, date, tags, coverImage, scrollTarget = '.info-section', lang = 'fr' } = Astro.props;
|
||||
const { title, description, date, tags, coverImage, scrollTarget = '.info-section' } = Astro.props;
|
||||
---
|
||||
|
||||
{coverImage && (
|
||||
|
|
@ -26,7 +24,7 @@ const { title, description, date, tags, coverImage, scrollTarget = '.info-sectio
|
|||
{description && <p class="album-description">{description}</p>}
|
||||
{date && (
|
||||
<time class="album-date">
|
||||
{date.toLocaleDateString(getDateLocale(lang), {
|
||||
{date.toLocaleDateString('fr-FR', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
|
|
|
|||
|
|
@ -4,19 +4,11 @@ import HeroViewport from './HeroViewport.astro';
|
|||
import Lightbox from './Lightbox.astro';
|
||||
import { Picture } from 'astro:assets';
|
||||
import { getEntry } from 'astro:content';
|
||||
import { getCategoryEntryId, type Locale } from '../../utils/i18n';
|
||||
|
||||
interface Props {
|
||||
category: string;
|
||||
lang?: Locale;
|
||||
}
|
||||
const { category } = Astro.props;
|
||||
|
||||
const { category, lang = 'fr' } = Astro.props;
|
||||
|
||||
// Récupérer les métadonnées de la catégorie (localisées avec fallback FR)
|
||||
const entryId = getCategoryEntryId(category, lang);
|
||||
const categoryData = await getEntry('photoCategories', entryId)
|
||||
?? await getEntry('photoCategories', category);
|
||||
// Récupérer les métadonnées de la catégorie
|
||||
const categoryData = await getEntry('photoCategories', category);
|
||||
|
||||
// Auto-détection des images du dossier de la catégorie
|
||||
const allImages = import.meta.glob<{ default: ImageMetadata }>('/src/assets/images/photos/categories/**/*.{jpg,jpeg,png,webp}');
|
||||
|
|
@ -54,7 +46,7 @@ const lightboxImages = images.map(img => ({
|
|||
---
|
||||
|
||||
<div id="category-gallery" class="category-container">
|
||||
<CategoryNav currentCategory={category} opaque={true} lang={lang} />
|
||||
<CategoryNav currentCategory={category} opaque={true} />
|
||||
|
||||
<!-- Image hero avec titre en overlay -->
|
||||
<HeroViewport targetSelector="#thumbnails">
|
||||
|
|
@ -97,7 +89,7 @@ const lightboxImages = images.map(img => ({
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<Lightbox images={lightboxImages} showCategory={true} category={category} lang={lang} />
|
||||
<Lightbox images={lightboxImages} showCategory={true} category={category} />
|
||||
|
||||
<script>
|
||||
let lastScrollY = window.scrollY;
|
||||
|
|
|
|||
|
|
@ -1,42 +1,27 @@
|
|||
---
|
||||
import { getCollection } from 'astro:content';
|
||||
import HomeIcon from '../icons/HomeIcon.astro';
|
||||
import { t, getPhotoBasePath, getPhotoBlogPath, getPhotoAlbumsPath, getHomePath, type Locale } from '../../utils/i18n';
|
||||
|
||||
interface Props {
|
||||
currentCategory?: string;
|
||||
opaque?: boolean;
|
||||
lang?: Locale;
|
||||
}
|
||||
const { currentCategory = '', opaque = false } = Astro.props;
|
||||
|
||||
const { currentCategory = '', opaque = false, lang = 'fr' } = Astro.props;
|
||||
// Récupérer les catégories depuis la collection, triées par order
|
||||
const photoCategories = await getCollection('photoCategories');
|
||||
const sortedCategories = photoCategories.sort((a, b) => (a.data.order || 99) - (b.data.order || 99));
|
||||
|
||||
const photoBasePath = getPhotoBasePath(lang);
|
||||
const homePath = getHomePath(lang);
|
||||
|
||||
// Récupérer les catégories depuis la collection, filtrées par langue
|
||||
const allCategories = await getCollection('photoCategories');
|
||||
const langCategories = allCategories.filter(c => (c.data.lang ?? 'fr') === lang);
|
||||
const effectiveCategories = langCategories.length > 0 ? langCategories : allCategories.filter(c => (c.data.lang ?? 'fr') === 'fr');
|
||||
const sortedCategories = effectiveCategories.sort((a, b) => (a.data.order || 99) - (b.data.order || 99));
|
||||
|
||||
// Extraire l'id de base (sans suffixe de langue)
|
||||
const categories = sortedCategories.map(cat => ({
|
||||
id: cat.id.replace(/\.(en|ar)$/, ''),
|
||||
title: cat.data.title,
|
||||
}));
|
||||
// Catégories photos uniquement
|
||||
const categories = sortedCategories.map(cat => ({ id: cat.id, title: cat.data.title }));
|
||||
---
|
||||
|
||||
<nav class={`category-navigation fixed top-0 left-0 right-0 z-50 ${opaque ? 'bg-black' : 'bg-black/30 backdrop-blur-sm'} transition-transform duration-300`}>
|
||||
<div class="nav-container">
|
||||
<!-- Titre du site à gauche -->
|
||||
<div class="site-title">
|
||||
<a href={homePath} class="site-link">
|
||||
<a href="/" class="site-link">
|
||||
<HomeIcon size={16} />
|
||||
<span class="site-name">{t('common', 'siteName', lang)}</span>
|
||||
<span class="site-name">Jalil Arfaoui</span>
|
||||
</a>
|
||||
<span class="nav-separator"></span>
|
||||
<a href={photoBasePath} class="nav-link">{t('nav', 'photo', lang)}</a>
|
||||
<a href="/photo" class="nav-link">Photo</a>
|
||||
</div>
|
||||
|
||||
<!-- Bouton hamburger (mobile) -->
|
||||
|
|
@ -49,10 +34,10 @@ const categories = sortedCategories.map(cat => ({
|
|||
<!-- Navigation -->
|
||||
<div class="nav-menu" id="navMenu">
|
||||
<a
|
||||
href={getPhotoBlogPath(lang)}
|
||||
href="/photo/blog"
|
||||
class={`nav-link blog-link ${currentCategory === 'blog' ? 'active' : ''}`}
|
||||
>
|
||||
{t('photo', 'photoFeed', lang)}
|
||||
Fil Photo
|
||||
</a>
|
||||
|
||||
<!-- Séparateur -->
|
||||
|
|
@ -63,7 +48,7 @@ const categories = sortedCategories.map(cat => ({
|
|||
{categories.map(cat => (
|
||||
<li>
|
||||
<a
|
||||
href={`${getPhotoAlbumsPath(lang)}/${cat.id}`}
|
||||
href={`/photo/albums/${cat.id}`}
|
||||
class={`nav-link ${currentCategory === cat.id ? 'active' : ''}`}
|
||||
data-category={cat.id}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -1,29 +1,19 @@
|
|||
---
|
||||
import { getCollection } from 'astro:content';
|
||||
import { t, getPhotoBlogPath, getPhotoAlbumsPath, type Locale } from '../../utils/i18n';
|
||||
|
||||
interface Props {
|
||||
lang?: Locale;
|
||||
}
|
||||
|
||||
const { lang = 'fr' } = Astro.props;
|
||||
|
||||
// Récupérer les catégories filtrées par langue
|
||||
const allCategories = await getCollection('photoCategories');
|
||||
const langCategories = allCategories.filter(c => (c.data.lang ?? 'fr') === lang);
|
||||
const effectiveCategories = langCategories.length > 0 ? langCategories : allCategories.filter(c => (c.data.lang ?? 'fr') === 'fr');
|
||||
const sortedCategories = effectiveCategories.sort((a, b) => (a.data.order || 99) - (b.data.order || 99));
|
||||
const photoCategories = await getCollection('photoCategories');
|
||||
const sortedCategories = photoCategories.sort((a, b) => (a.data.order || 99) - (b.data.order || 99));
|
||||
---
|
||||
|
||||
<section id="explore-section" class="explore-section">
|
||||
<div class="explore-content">
|
||||
<div class="explore-card">
|
||||
<h2 class="explore-card-title">{t('photo', 'categories', lang)}</h2>
|
||||
<p class="explore-card-desc">{t('photo', 'browseByTheme', lang)}</p>
|
||||
<h2 class="explore-card-title">Catégories</h2>
|
||||
<p class="explore-card-desc">Parcourir les photos par thème</p>
|
||||
<ul class="category-list">
|
||||
{sortedCategories.map(cat => (
|
||||
<li>
|
||||
<a href={`${getPhotoAlbumsPath(lang)}/${cat.id.replace(/\.(en|ar)$/, '')}`} class="category-link">
|
||||
<a href={`/photo/albums/${cat.id}`} class="category-link">
|
||||
{cat.data.title}
|
||||
</a>
|
||||
</li>
|
||||
|
|
@ -32,10 +22,10 @@ const sortedCategories = effectiveCategories.sort((a, b) => (a.data.order || 99)
|
|||
</div>
|
||||
|
||||
<div class="explore-card">
|
||||
<h2 class="explore-card-title">{t('photo', 'photoFeed', lang)}</h2>
|
||||
<p class="explore-card-desc">{t('photo', 'feedDescription', lang)}</p>
|
||||
<a href={getPhotoBlogPath(lang)} class="explore-cta">
|
||||
{t('photo', 'viewFeed', lang)}
|
||||
<h2 class="explore-card-title">Fil Photo</h2>
|
||||
<p class="explore-card-desc">Parcourir les séries chronologiques, reportages et histoires en images</p>
|
||||
<a href="/photo/blog" class="explore-cta">
|
||||
Voir le fil
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<line x1="5" y1="12" x2="19" y2="12"/>
|
||||
<polyline points="12 5 19 12 12 19"/>
|
||||
|
|
|
|||
|
|
@ -1,49 +1,45 @@
|
|||
---
|
||||
import { getCollection } from 'astro:content';
|
||||
import { t, type Locale } from '../../utils/i18n';
|
||||
|
||||
interface Props {
|
||||
images: { src: string; alt: string; title?: string }[];
|
||||
albumTitle?: string;
|
||||
showCategory?: boolean;
|
||||
category?: string;
|
||||
lang?: Locale;
|
||||
}
|
||||
|
||||
const { images, albumTitle = '', showCategory = false, category = '', lang = 'fr' } = Astro.props;
|
||||
const { images, albumTitle = '', showCategory = false, category = '' } = Astro.props;
|
||||
|
||||
const imagesForJS = JSON.stringify(images);
|
||||
|
||||
// Construire les labels depuis la collection filtrée par langue
|
||||
const allCategories = await getCollection('photoCategories');
|
||||
const langCategories = allCategories.filter(c => (c.data.lang ?? 'fr') === lang);
|
||||
const effectiveCategories = langCategories.length > 0 ? langCategories : allCategories.filter(c => (c.data.lang ?? 'fr') === 'fr');
|
||||
// Construire les labels depuis la collection
|
||||
const photoCategories = await getCollection('photoCategories');
|
||||
const categoryLabels: Record<string, string> = {
|
||||
'blog': t('photo', 'photoFeed', lang),
|
||||
...Object.fromEntries(effectiveCategories.map(cat => [cat.id.replace(/\.(en|ar)$/, ''), cat.data.title]))
|
||||
'blog': 'Fil Photo',
|
||||
...Object.fromEntries(photoCategories.map(cat => [cat.id, cat.data.title]))
|
||||
};
|
||||
---
|
||||
|
||||
<div id="lightbox" class="lightbox hidden">
|
||||
<div class="lightbox-controls">
|
||||
<button class="lightbox-fullscreen" aria-label={t('photo', 'fullscreen', lang)}>
|
||||
<button class="lightbox-fullscreen" aria-label="Plein écran">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="lightbox-close" aria-label={t('photo', 'close', lang)}>
|
||||
<button class="lightbox-close" aria-label="Fermer">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M18 6L6 18M6 6l12 12" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button class="lightbox-prev" aria-label={t('photo', 'previousImage', lang)}>
|
||||
<button class="lightbox-prev" aria-label="Image précédente">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M15 18L9 12L15 6" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="lightbox-next" aria-label={t('photo', 'nextImage', lang)}>
|
||||
<button class="lightbox-next" aria-label="Image suivante">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M9 18L15 12L9 6" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
|
|
|||
|
|
@ -1,24 +1,12 @@
|
|||
---
|
||||
import PhotoLanguageSwitcher from './PhotoLanguageSwitcher.astro';
|
||||
import { t, getAboutPath, type Locale } from '../../utils/i18n';
|
||||
|
||||
interface Props {
|
||||
lang?: Locale;
|
||||
}
|
||||
|
||||
const { lang = 'fr' } = Astro.props;
|
||||
---
|
||||
|
||||
<footer class="photo-footer">
|
||||
<div class="photo-footer-inner">
|
||||
<div class="photo-footer-links">
|
||||
<a href={getAboutPath(lang)}>{t('photo', 'about', lang)}</a>
|
||||
<a href="mailto:jalil@arfaoui.net">{t('photo', 'contact', lang)}</a>
|
||||
<a href="/a-propos">À propos</a>
|
||||
<a href="mailto:jalil@arfaoui.net">Contact</a>
|
||||
<a href="https://instagram.com/l.i.l.a.j" target="_blank" rel="noopener noreferrer">Instagram</a>
|
||||
</div>
|
||||
<PhotoLanguageSwitcher lang={lang} />
|
||||
<div class="photo-footer-copy">
|
||||
© Jalil Arfaoui <a href="https://creativecommons.org/licenses/by-nc/4.0/" target="_blank" rel="noopener noreferrer">Creative Commons CC-BY-NC 4.0</a>
|
||||
© Jalil Arfaoui <a href="https://creativecommons.org/licenses/by-nc/4.0/" target="_blank" rel="noopener noreferrer">Creative Commons CC-BY-NC 4.0</a>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
|
@ -48,12 +36,6 @@ const { lang = 'fr' } = Astro.props;
|
|||
.photo-footer-links {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.photo-footer-copy {
|
||||
flex: 1;
|
||||
text-align: end;
|
||||
}
|
||||
|
||||
.photo-footer-links a,
|
||||
|
|
|
|||
|
|
@ -3,14 +3,6 @@ import CategoryNav from './CategoryNav.astro';
|
|||
import Slideshow from './Slideshow.astro';
|
||||
import SlideControls from './SlideControls.astro';
|
||||
import favorites from '../../data/favorites.json';
|
||||
import type { Locale } from '../../utils/i18n';
|
||||
|
||||
interface Props {
|
||||
category?: string;
|
||||
lang?: Locale;
|
||||
}
|
||||
|
||||
const { lang = 'fr' } = Astro.props;
|
||||
|
||||
// Auto-détection des images
|
||||
const allImages = import.meta.glob<{ default: ImageMetadata }>('/src/assets/images/photos/categories/**/*.{jpg,jpeg,png,webp}');
|
||||
|
|
@ -52,9 +44,9 @@ const imagesForJS = JSON.stringify(images.map(img => ({
|
|||
---
|
||||
|
||||
<div id="photo-gallery" class="gallery-container">
|
||||
<CategoryNav currentCategory="" lang={lang} />
|
||||
<Slideshow images={images} lang={lang} />
|
||||
<SlideControls lang={lang} />
|
||||
<CategoryNav currentCategory="" />
|
||||
<Slideshow images={images} />
|
||||
<SlideControls />
|
||||
</div>
|
||||
|
||||
<script is:inline define:vars={{ imagesForJS }}>
|
||||
|
|
|
|||
|
|
@ -1,55 +0,0 @@
|
|||
---
|
||||
import { getPhotoBasePath, type Locale } from '../../utils/i18n';
|
||||
|
||||
interface Props {
|
||||
lang?: Locale;
|
||||
}
|
||||
|
||||
const { lang = 'fr' } = Astro.props;
|
||||
|
||||
const languages = [
|
||||
{ code: 'fr' as const, label: 'FR', name: 'Français' },
|
||||
{ code: 'en' as const, label: 'EN', name: 'English' },
|
||||
{ code: 'ar' as const, label: 'ع', name: 'العربية' },
|
||||
];
|
||||
---
|
||||
|
||||
<div class="photo-lang-switcher">
|
||||
{languages.map((l, i) => (
|
||||
<>
|
||||
{i > 0 && <span class="lang-sep">·</span>}
|
||||
{l.code === lang ? (
|
||||
<span class="lang-active" title={l.name}>{l.label}</span>
|
||||
) : (
|
||||
<a href={getPhotoBasePath(l.code)} class="lang-link" title={l.name} hreflang={l.code}>{l.label}</a>
|
||||
)}
|
||||
</>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.photo-lang-switcher {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.lang-sep {
|
||||
color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.lang-active {
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.lang-link {
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
text-decoration: none;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.lang-link:hover {
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,18 +1,11 @@
|
|||
---
|
||||
import { t, type Locale } from '../../utils/i18n';
|
||||
|
||||
interface Props {
|
||||
lang?: Locale;
|
||||
}
|
||||
|
||||
const { lang = 'fr' } = Astro.props;
|
||||
---
|
||||
|
||||
<div class="slide-controls">
|
||||
<button
|
||||
id="prev-btn"
|
||||
class="control-btn prev-btn"
|
||||
aria-label={t('photo', 'previousImage', lang)}
|
||||
aria-label="Image précédente"
|
||||
>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M15 18l-6-6 6-6"/>
|
||||
|
|
@ -22,7 +15,7 @@ const { lang = 'fr' } = Astro.props;
|
|||
<button
|
||||
id="next-btn"
|
||||
class="control-btn next-btn"
|
||||
aria-label={t('photo', 'nextImage', lang)}
|
||||
aria-label="Image suivante"
|
||||
>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M9 18l6-6-6-6"/>
|
||||
|
|
|
|||
|
|
@ -1,13 +1,11 @@
|
|||
---
|
||||
import { Picture } from 'astro:assets';
|
||||
import { t, type Locale } from '../../utils/i18n';
|
||||
|
||||
interface Props {
|
||||
images: { src: ImageMetadata; alt: string }[];
|
||||
lang?: Locale;
|
||||
}
|
||||
|
||||
const { images = [], lang = 'fr' } = Astro.props;
|
||||
const { images = [] } = Astro.props;
|
||||
---
|
||||
|
||||
<div id="slideshow-container" class="slideshow-wrapper">
|
||||
|
|
@ -35,7 +33,7 @@ const { images = [], lang = 'fr' } = Astro.props;
|
|||
<button
|
||||
class={`indicator ${index === 0 ? 'active' : ''}`}
|
||||
data-slide={index}
|
||||
aria-label={`${t('photo', 'goToImage', lang)} ${index + 1}`}
|
||||
aria-label={`Aller à l'image ${index + 1}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,25 +0,0 @@
|
|||
---
|
||||
import PhotoLayout from '../../../layouts/PhotoLayout.astro';
|
||||
import CategoryGrid from '../CategoryGrid.astro';
|
||||
import { getCategoryEntryId, type Locale } from '../../../utils/i18n';
|
||||
import { getEntry } from 'astro:content';
|
||||
|
||||
interface Props {
|
||||
category: string;
|
||||
lang?: Locale;
|
||||
}
|
||||
|
||||
const { category, lang = 'fr' } = Astro.props;
|
||||
|
||||
// Récupérer le titre localisé pour le title de la page
|
||||
const entryId = getCategoryEntryId(category, lang);
|
||||
const categoryData = await getEntry('photoCategories', entryId)
|
||||
?? await getEntry('photoCategories', category);
|
||||
const categoryLabel = categoryData?.data.title || category;
|
||||
|
||||
const title = `${categoryLabel} - Jalil Arfaoui`;
|
||||
---
|
||||
|
||||
<PhotoLayout title={title} enableScroll={true} lang={lang}>
|
||||
<CategoryGrid category={category} lang={lang} />
|
||||
</PhotoLayout>
|
||||
|
|
@ -1,338 +0,0 @@
|
|||
---
|
||||
import PhotoLayout from '../../../layouts/PhotoLayout.astro';
|
||||
import CategoryNav from '../CategoryNav.astro';
|
||||
import { getCollection } from 'astro:content';
|
||||
import { Picture } from 'astro:assets';
|
||||
import { t, getPhotoBlogPath, getDateLocale, getPostBaseSlug, type Locale } from '../../../utils/i18n';
|
||||
|
||||
interface Props {
|
||||
lang?: Locale;
|
||||
}
|
||||
|
||||
const { lang = 'fr' } = Astro.props;
|
||||
|
||||
const photoBlogPath = getPhotoBlogPath(lang);
|
||||
const dateLocale = getDateLocale(lang);
|
||||
|
||||
// Importer toutes les images pour résoudre les cover images
|
||||
const allImages = import.meta.glob<{ default: ImageMetadata }>(
|
||||
'/src/assets/images/photos/blog/**/*.{jpg,jpeg,png,webp}'
|
||||
);
|
||||
|
||||
// Récupération des posts photo filtrés par langue
|
||||
const allPhotoBlogPosts = (await getCollection('photoBlogPosts'))
|
||||
.filter(post => (post.data.lang ?? 'fr') === lang);
|
||||
|
||||
// Tri par date (plus récent en premier)
|
||||
const sortedPosts = allPhotoBlogPosts.sort((a, b) =>
|
||||
new Date(b.data.date).getTime() - new Date(a.data.date).getTime()
|
||||
);
|
||||
|
||||
// Résoudre les cover images via le glob
|
||||
const postsWithImages = await Promise.all(sortedPosts.map(async (post) => {
|
||||
const year = post.data.date.getFullYear();
|
||||
const baseSlug = getPostBaseSlug(post.id);
|
||||
const coverPath = `/src/assets/images/photos/blog/${year}/${baseSlug}/${post.data.coverImage}`;
|
||||
const loader = allImages[coverPath];
|
||||
const resolvedCoverImage = loader ? (await loader()).default : undefined;
|
||||
// Slug nettoyé pour les URLs
|
||||
const cleanSlug = `${year}/${baseSlug}`;
|
||||
return { ...post, resolvedCoverImage, cleanSlug };
|
||||
}));
|
||||
|
||||
// Séparer les posts à la une des autres
|
||||
const featuredPosts = postsWithImages.filter(post => post.data.featured);
|
||||
const regularPosts = postsWithImages.filter(post => !post.data.featured);
|
||||
---
|
||||
|
||||
<PhotoLayout title={`${t('photo', 'blogTitle', lang)} - Jalil Arfaoui`} enableScroll={true} lang={lang}>
|
||||
<div class="blog-container">
|
||||
<CategoryNav currentCategory="blog" opaque={false} lang={lang} />
|
||||
|
||||
<!-- Section héro avec posts à la une -->
|
||||
{featuredPosts.length > 0 && (
|
||||
<section class="featured-section">
|
||||
<div class="featured-grid">
|
||||
{featuredPosts.map(post => (
|
||||
<article class="featured-post">
|
||||
<a href={`${photoBlogPath}/${post.cleanSlug}`} class="post-link">
|
||||
<div class="post-image">
|
||||
{post.resolvedCoverImage && <Picture src={post.resolvedCoverImage} alt={post.data.title} widths={[600, 900, 1200]} formats={['webp']} />}
|
||||
<div class="post-overlay">
|
||||
<div class="post-content">
|
||||
<span class="post-badge">{t('photo', 'featured', lang)}</span>
|
||||
<h2 class="post-title">{post.data.title}</h2>
|
||||
<time class="post-date">
|
||||
{new Date(post.data.date).toLocaleDateString(dateLocale, {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
})}
|
||||
</time>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
<!-- Grille des autres posts -->
|
||||
<section class="posts-grid">
|
||||
<div class="grid-container">
|
||||
{regularPosts.map(post => (
|
||||
<article class="post-item">
|
||||
<a href={`${photoBlogPath}/${post.cleanSlug}`} class="post-link">
|
||||
{post.resolvedCoverImage && <Picture src={post.resolvedCoverImage} alt={post.data.title} widths={[400, 600, 800]} formats={['webp']} />}
|
||||
<div class="post-overlay">
|
||||
<div class="overlay-content">
|
||||
<h3 class="post-title">{post.data.title}</h3>
|
||||
<time class="post-date">
|
||||
{new Date(post.data.date).toLocaleDateString(dateLocale, {
|
||||
year: 'numeric',
|
||||
month: 'short'
|
||||
})}
|
||||
</time>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</PhotoLayout>
|
||||
|
||||
<style>
|
||||
.blog-container {
|
||||
background: #000000;
|
||||
color: #ffffff;
|
||||
min-height: 100vh;
|
||||
padding-top: 53px;
|
||||
}
|
||||
|
||||
/* Section à la une */
|
||||
.featured-section {
|
||||
padding: 16px 16px 0;
|
||||
}
|
||||
|
||||
.featured-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(500px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.featured-post {
|
||||
position: relative;
|
||||
height: 450px;
|
||||
overflow: hidden;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.featured-post .post-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.featured-post img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.featured-post:hover img {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.featured-post .post-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(to bottom, rgba(0,0,0,0.1) 0%, rgba(0,0,0,0.7) 100%);
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
.featured-post .post-content {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.post-badge {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
padding: 4px 12px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
margin-bottom: 12px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.featured-post .post-title {
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
margin: 0 0 4px 0;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.featured-post .post-description {
|
||||
font-size: 15px;
|
||||
margin: 0 0 10px 0;
|
||||
opacity: 0.9;
|
||||
line-height: 1.3;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.featured-post .post-date {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* Grille des posts */
|
||||
.posts-grid {
|
||||
padding: 16px 16px 100px;
|
||||
}
|
||||
|
||||
.grid-container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.post-item {
|
||||
overflow: hidden;
|
||||
border-radius: 4px;
|
||||
background: #111;
|
||||
}
|
||||
|
||||
.post-link {
|
||||
display: block;
|
||||
position: relative;
|
||||
aspect-ratio: 3/2;
|
||||
}
|
||||
|
||||
.featured-post .post-link {
|
||||
aspect-ratio: auto;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.post-link img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.post-item .post-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: linear-gradient(to top, rgba(0,0,0,0.85) 0%, rgba(0,0,0,0.5) 25%, rgba(0,0,0,0) 50%);
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
padding: 12px 16px;
|
||||
transition: background 0.3s ease;
|
||||
}
|
||||
|
||||
.post-item:hover .post-overlay {
|
||||
background: linear-gradient(to top, rgba(0,0,0,0.95) 0%, rgba(0,0,0,0.7) 25%, rgba(0,0,0,0) 50%);
|
||||
}
|
||||
|
||||
.post-item:hover img {
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
.post-item .overlay-content {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.post-item .post-title {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
margin: 0 0 2px 0;
|
||||
line-height: 1.3;
|
||||
text-shadow: 0 1px 3px rgba(0,0,0,0.8);
|
||||
}
|
||||
|
||||
.post-item .post-subtitle {
|
||||
font-size: 14px;
|
||||
margin: 0 0 6px 0;
|
||||
opacity: 0.9;
|
||||
line-height: 1.2;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.post-item .post-date {
|
||||
font-size: 12px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 1200px) {
|
||||
.grid-container {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.featured-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.featured-post {
|
||||
height: 300px;
|
||||
}
|
||||
|
||||
.featured-post .post-overlay {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.featured-post .post-title {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.grid-container {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.blog-container {
|
||||
padding-top: 53px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.grid-container {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.featured-section {
|
||||
padding: 12px 12px 0;
|
||||
}
|
||||
|
||||
.posts-grid {
|
||||
padding: 12px 12px 80px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,119 +0,0 @@
|
|||
---
|
||||
import PhotoLayout from '../../../layouts/PhotoLayout.astro';
|
||||
import CategoryNav from '../CategoryNav.astro';
|
||||
import AlbumHeader from '../AlbumHeader.astro';
|
||||
import MasonryGallery from '../MasonryGallery.astro';
|
||||
import Lightbox from '../Lightbox.astro';
|
||||
import { getPostBaseSlug, type Locale } from '../../../utils/i18n';
|
||||
|
||||
interface Props {
|
||||
post: any;
|
||||
lang?: Locale;
|
||||
}
|
||||
|
||||
const { post, lang = 'fr' } = Astro.props;
|
||||
|
||||
// Importer toutes les images du dossier photos
|
||||
const allImages = import.meta.glob<{ default: ImageMetadata }>('/src/assets/images/photos/blog/**/*.{jpg,jpeg,png,webp}');
|
||||
|
||||
const { Content } = await post.render();
|
||||
|
||||
const baseSlug = getPostBaseSlug(post.id);
|
||||
|
||||
// Construire le chemin de l'album avec l'année
|
||||
const year = post.data.date.getFullYear();
|
||||
const albumPath = `/src/assets/images/photos/blog/${year}/${baseSlug}/`;
|
||||
const albumImages = Object.keys(allImages)
|
||||
.filter(path => path.startsWith(albumPath))
|
||||
.sort();
|
||||
|
||||
// Résoudre la cover image depuis le glob
|
||||
const coverPath = `/src/assets/images/photos/blog/${year}/${baseSlug}/${post.data.coverImage}`;
|
||||
const coverImageLoader = allImages[coverPath];
|
||||
const coverImage = coverImageLoader ? (await coverImageLoader()).default : undefined;
|
||||
|
||||
// Résoudre les images de la galerie
|
||||
const galleryImages = await Promise.all(
|
||||
albumImages.map(async (imagePath) => {
|
||||
const loader = allImages[imagePath];
|
||||
const img = await loader();
|
||||
const filename = imagePath.split('/').pop() || '';
|
||||
return {
|
||||
src: img.default,
|
||||
alt: filename.replace(/\.[^/.]+$/, '').replace(/-/g, ' ').replace(/^\d+-/, ''),
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
// Données pour la lightbox
|
||||
const lightboxImages = galleryImages.map(img => ({
|
||||
src: img.src.src,
|
||||
alt: img.alt
|
||||
}));
|
||||
---
|
||||
|
||||
<PhotoLayout title={`${post.data.title} - Blog Photo - Jalil Arfaoui`} enableScroll={true} lang={lang}>
|
||||
<div class="album-container">
|
||||
<CategoryNav currentCategory="blog" opaque={false} lang={lang} />
|
||||
|
||||
<AlbumHeader
|
||||
title={post.data.title}
|
||||
description={post.data.description}
|
||||
date={new Date(post.data.date)}
|
||||
tags={post.data.tags}
|
||||
coverImage={coverImage}
|
||||
scrollTarget="#album-content"
|
||||
lang={lang}
|
||||
/>
|
||||
|
||||
<div id="album-content">
|
||||
{post.body && (
|
||||
<div class="post-content">
|
||||
<Content />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<MasonryGallery images={galleryImages} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Lightbox images={lightboxImages} albumTitle={post.data.title} lang={lang} />
|
||||
</PhotoLayout>
|
||||
|
||||
<style>
|
||||
.album-container {
|
||||
background: #000;
|
||||
color: #fff;
|
||||
min-height: 100vh;
|
||||
padding-top: var(--header-height, 53px);
|
||||
}
|
||||
|
||||
.post-content {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 0 20px 40px;
|
||||
line-height: 1.6;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.post-content :global(h1),
|
||||
.post-content :global(h2),
|
||||
.post-content :global(h3) {
|
||||
margin: 2em 0 1em 0;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.post-content :global(p) {
|
||||
margin: 1.5em 0;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.post-content :global(img) {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
border-radius: 8px;
|
||||
margin: 2em 0;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
---
|
||||
import PhotoLayout from '../../../layouts/PhotoLayout.astro';
|
||||
import PhotoGallery from '../PhotoGallery.astro';
|
||||
import HeroViewport from '../HeroViewport.astro';
|
||||
import ExploreSection from '../ExploreSection.astro';
|
||||
import { t, type Locale } from '../../../utils/i18n';
|
||||
|
||||
interface Props {
|
||||
lang?: Locale;
|
||||
}
|
||||
|
||||
const { lang = 'fr' } = Astro.props;
|
||||
|
||||
const title = `${t('photo', 'galleryTitle', lang)} - Jalil Arfaoui`;
|
||||
---
|
||||
|
||||
<PhotoLayout title={title} enableScroll={true} lang={lang}>
|
||||
<PhotoGallery category="all" lang={lang} />
|
||||
<HeroViewport targetSelector="#explore-section" transparent />
|
||||
<ExploreSection lang={lang} />
|
||||
</PhotoLayout>
|
||||
|
|
@ -85,7 +85,6 @@ const photoCategoriesCollection = defineCollection({
|
|||
title: z.string(),
|
||||
subtitle: z.string(),
|
||||
order: z.number().optional(),
|
||||
lang: z.enum(['fr', 'en', 'ar']).default('fr'),
|
||||
}),
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +0,0 @@
|
|||
{
|
||||
"title": "ثقافات وتقاليد",
|
||||
"subtitle": "ثراء التقاليد الإنسانية",
|
||||
"order": 4,
|
||||
"lang": "ar"
|
||||
}
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
{
|
||||
"title": "Cultures & Traditions",
|
||||
"subtitle": "Richness of human traditions",
|
||||
"order": 4,
|
||||
"lang": "en"
|
||||
}
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
{
|
||||
"title": "محرّكات",
|
||||
"subtitle": "ميكانيكا وقوة في حركة",
|
||||
"order": 7,
|
||||
"lang": "ar"
|
||||
}
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
{
|
||||
"title": "Engines",
|
||||
"subtitle": "Mechanics and power in motion",
|
||||
"order": 7,
|
||||
"lang": "en"
|
||||
}
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
{
|
||||
"title": "يوميات",
|
||||
"subtitle": "لحظات من الحياة اليومية",
|
||||
"order": 8,
|
||||
"lang": "ar"
|
||||
}
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
{
|
||||
"title": "Everyday Life",
|
||||
"subtitle": "Moments of everyday life",
|
||||
"order": 8,
|
||||
"lang": "en"
|
||||
}
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
{
|
||||
"title": "موسيقى واحتفالات",
|
||||
"subtitle": "نغمات وأغانٍ واهتزازات تحتفي بلحظات حياتنا الكبيرة والصغيرة",
|
||||
"order": 5,
|
||||
"lang": "ar"
|
||||
}
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
{
|
||||
"title": "Music & Celebrations",
|
||||
"subtitle": "Notes, songs and vibrations celebrating life's big and small moments",
|
||||
"order": 5,
|
||||
"lang": "en"
|
||||
}
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
{
|
||||
"title": "طبيعة",
|
||||
"subtitle": "سحر العالم الطبيعي",
|
||||
"order": 3,
|
||||
"lang": "ar"
|
||||
}
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
{
|
||||
"title": "Nature",
|
||||
"subtitle": "The magic of the natural world",
|
||||
"order": 3,
|
||||
"lang": "en"
|
||||
}
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
{
|
||||
"title": "مناظر طبيعية",
|
||||
"subtitle": "جمال المناظر والأماكن",
|
||||
"order": 2,
|
||||
"lang": "ar"
|
||||
}
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
{
|
||||
"title": "Landscapes",
|
||||
"subtitle": "Beauty of landscapes and places",
|
||||
"order": 2,
|
||||
"lang": "en"
|
||||
}
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
{
|
||||
"title": "بورتريهات",
|
||||
"subtitle": "تعابير ومشاعر ملتقطة",
|
||||
"order": 1,
|
||||
"lang": "ar"
|
||||
}
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
{
|
||||
"title": "Portraits",
|
||||
"subtitle": "Captured expressions and emotions",
|
||||
"order": 1,
|
||||
"lang": "en"
|
||||
}
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
{
|
||||
"title": "رياضة",
|
||||
"subtitle": "حركة وجهد وتجاوز للذات",
|
||||
"order": 6,
|
||||
"lang": "ar"
|
||||
}
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
{
|
||||
"title": "Sports",
|
||||
"subtitle": "Movement, effort and pushing limits",
|
||||
"order": 6,
|
||||
"lang": "en"
|
||||
}
|
||||
|
|
@ -1,18 +1,11 @@
|
|||
---
|
||||
import PhotoFooter from '../components/photo/PhotoFooter.astro';
|
||||
import type { Locale } from '../utils/i18n';
|
||||
|
||||
interface Props {
|
||||
title?: string;
|
||||
enableScroll?: boolean;
|
||||
lang?: Locale;
|
||||
}
|
||||
|
||||
const { title = "Galerie Photo - Jalil Arfaoui", enableScroll = false, lang = 'fr' } = Astro.props;
|
||||
const { title = "Galerie Photo - Jalil Arfaoui", enableScroll = false } = Astro.props;
|
||||
---
|
||||
|
||||
<!doctype html>
|
||||
<html lang={lang} dir={lang === 'ar' ? 'rtl' : 'ltr'}>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
|
|
@ -34,7 +27,7 @@ const { title = "Galerie Photo - Jalil Arfaoui", enableScroll = false, lang = 'f
|
|||
<body class={`antialiased bg-black text-white ${enableScroll ? '' : 'overflow-hidden'}`} style="font-family: 'Karla', 'Helvetica Neue', Helvetica, Arial, sans-serif;">
|
||||
<slot />
|
||||
|
||||
<PhotoFooter lang={lang} />
|
||||
<PhotoFooter />
|
||||
|
||||
<Fragment set:html={import.meta.env.FOOTER_INJECT} />
|
||||
</body>
|
||||
|
|
|
|||
|
|
@ -1,9 +1,7 @@
|
|||
---
|
||||
import { Image } from "astro:assets";
|
||||
import PageHeading from "../components/page-heading.astro";
|
||||
import Layout from "../layouts/main.astro";
|
||||
import Link from "../components/Link.astro";
|
||||
import jalilPhoto from "../assets/images/jalil.jpg";
|
||||
---
|
||||
|
||||
<Layout title="À propos - Jalil Arfaoui">
|
||||
|
|
@ -13,7 +11,7 @@ import jalilPhoto from "../assets/images/jalil.jpg";
|
|||
description="Développeur artisan, comédien improvisateur, photographe curieux."
|
||||
/>
|
||||
|
||||
<Image src={jalilPhoto} class="relative z-30 w-full my-10 rounded-xl" alt="Jalil Arfaoui" />
|
||||
<img src="/assets/images/jalil.jpg" class="relative z-30 w-full my-10 rounded-xl" alt="Jalil Arfaoui" />
|
||||
|
||||
<h2 class="mb-4 text-2xl font-bold dark:text-neutral-200">Qui suis-je ?</h2>
|
||||
<div class="space-y-4 text-gray-600 dark:text-neutral-400 leading-relaxed">
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
---
|
||||
import { Image } from "astro:assets";
|
||||
import Layout from "../../layouts/main.astro";
|
||||
import jalilPhoto from "../../assets/images/jalil-2.jpg";
|
||||
---
|
||||
|
||||
<Layout title="جليل عرفاوي - حِرَفي برمجة • ممثل ارتجالي • مصوّر">
|
||||
|
|
@ -20,9 +18,10 @@ import jalilPhoto from "../../assets/images/jalil-2.jpg";
|
|||
</p>
|
||||
|
||||
<div class="flex flex-wrap gap-4">
|
||||
<a href="/ar/برمجة" class="inline-flex items-center px-6 py-3 text-sm font-semibold text-white bg-blue-600 rounded-full hover:bg-blue-700 transition-colors duration-200">
|
||||
<span class="wip-link inline-flex items-center px-6 py-3 text-sm font-semibold text-white/60 bg-blue-600/50 rounded-full cursor-not-allowed" title="قيد الإنشاء">
|
||||
اكتشف أعمالي
|
||||
</a>
|
||||
<span class="mr-2">🚧</span>
|
||||
</span>
|
||||
<a href="/ar/نبذة-عني" class="inline-flex items-center px-6 py-3 text-sm font-semibold text-neutral-700 bg-neutral-100 dark:bg-neutral-800 dark:text-neutral-200 rounded-full hover:bg-neutral-200 dark:hover:bg-neutral-700 transition-colors duration-200">
|
||||
تعرّف عليّ أكثر
|
||||
</a>
|
||||
|
|
@ -33,8 +32,8 @@ import jalilPhoto from "../../assets/images/jalil-2.jpg";
|
|||
<div class="relative z-50 w-full max-w-sm mx-auto">
|
||||
<div class="relative z-30 p-1 bg-gradient-to-br from-blue-500 via-purple-500 to-pink-500 rounded-3xl">
|
||||
<div class="bg-white dark:bg-neutral-950 rounded-3xl p-4">
|
||||
<Image
|
||||
src={jalilPhoto}
|
||||
<img
|
||||
src="/assets/images/jalil-2.jpg"
|
||||
alt="جليل عرفاوي"
|
||||
loading="eager"
|
||||
decoding="auto"
|
||||
|
|
@ -60,9 +59,15 @@ import jalilPhoto from "../../assets/images/jalil-2.jpg";
|
|||
أكثر من 20 سنة في بناء البرمجيات. اليوم، أختار مشاريعي: برمجيات حرّة، أدوات مفيدة، لا شيء يُقيّد. Craftsmanship، DDD، TypeScript — وهاجس التحيّزات التي نضعها في الكود دون أن ندري. أُدرّس البرمجة وأنشّط مجتمع Software Crafters في ألبي.
|
||||
</p>
|
||||
<div class="space-y-3">
|
||||
<a href="/ar/برمجة" class="block text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-200 font-medium">
|
||||
← اكتشف المزيد
|
||||
</a>
|
||||
<span class="block text-blue-400/50 dark:text-blue-600/50 cursor-not-allowed" title="قيد الإنشاء">
|
||||
← المسار المهني 🚧
|
||||
</span>
|
||||
<span class="block text-blue-400/50 dark:text-blue-600/50 cursor-not-allowed" title="قيد الإنشاء">
|
||||
← مشاريعي 🚧
|
||||
</span>
|
||||
<span class="block text-blue-400/50 dark:text-blue-600/50 cursor-not-allowed" title="قيد الإنشاء">
|
||||
← المدوّنة التقنية 🚧
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -77,9 +82,9 @@ import jalilPhoto from "../../assets/images/jalil-2.jpg";
|
|||
ممثل ارتجالي منذ 2008، من ضواحي باريس إلى المغرب قبل تأسيس فرقة Les Particules في ألبي. اليوم أخوض أيضًا غمار المسرح المكتوب.
|
||||
</p>
|
||||
<div class="space-y-3">
|
||||
<a href="/ar/مسرح" class="block text-orange-600 dark:text-orange-400 hover:text-orange-800 dark:hover:text-orange-200 font-medium">
|
||||
← المسار الفني
|
||||
</a>
|
||||
<span class="block text-orange-400/50 dark:text-orange-600/50 cursor-not-allowed" title="قيد الإنشاء">
|
||||
← المسار الفني 🚧
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -94,10 +99,10 @@ import jalilPhoto from "../../assets/images/jalil-2.jpg";
|
|||
أصوّر كل شيء تقريبًا: وجوه، حفلات، محرّكات، الحياة اليومية. لا تخصّص، مجرّد فضول.
|
||||
</p>
|
||||
<div class="space-y-3">
|
||||
<a href="/ar/تصوير" class="block text-purple-600 dark:text-purple-400 hover:text-purple-800 dark:hover:text-purple-200 font-medium">
|
||||
<a href="/photo" class="block text-purple-600 dark:text-purple-400 hover:text-purple-800 dark:hover:text-purple-200 font-medium">
|
||||
← معرض الصور
|
||||
</a>
|
||||
<a href="/ar/تصوير/مدونة" class="block text-purple-600 dark:text-purple-400 hover:text-purple-800 dark:hover:text-purple-200 font-medium">
|
||||
<a href="/photo/blog" class="block text-purple-600 dark:text-purple-400 hover:text-purple-800 dark:hover:text-purple-200 font-medium">
|
||||
← مُدوّنة الصور
|
||||
</a>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,68 +0,0 @@
|
|||
---
|
||||
import PageHeading from "../../components/page-heading.astro";
|
||||
import Layout from "../../layouts/main.astro";
|
||||
import Link from "../../components/Link.astro";
|
||||
---
|
||||
|
||||
<Layout title="برمجة - جليل عرفاوي">
|
||||
<section dir="rtl" lang="ar" class="relative z-20 max-w-2xl mx-auto my-12 px-7 lg:px-0">
|
||||
<PageHeading
|
||||
title="برمجة"
|
||||
description="أكثر من 20 سنة في بناء البرمجيات. Craftsmanship، TDD، DDD — وهاجس التحيّزات التي نضعها في الكود دون أن ندري."
|
||||
/>
|
||||
|
||||
<h2 class="mt-10 mb-4 text-2xl font-bold dark:text-neutral-200">ما أفعله</h2>
|
||||
<div class="space-y-4 text-gray-600 dark:text-neutral-400 leading-relaxed">
|
||||
<p>
|
||||
مطوّر مستقل مقيم في <strong class="text-gray-800 dark:text-neutral-200">ألبي، فرنسا</strong>، أرافق الفرق كمطوّر أول، أو قائد تقني، أو مدرب تقني. أفضّل البرمجيات الحرّة والأدوات التي تلبي احتياجات حقيقية.
|
||||
</p>
|
||||
<p>
|
||||
منهجيّتي: <strong class="text-gray-800 dark:text-neutral-200">TDD، الكود النظيف، تصميم المجالات</strong>. الكود مادّة تُصاغ بعناية، لا سلعة تُنتج بالجملة.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<h2 class="mt-10 mb-4 text-2xl font-bold dark:text-neutral-200">المهارات</h2>
|
||||
<div class="space-y-4 text-gray-600 dark:text-neutral-400 leading-relaxed">
|
||||
<p>
|
||||
<strong class="text-gray-800 dark:text-neutral-200">اللغات</strong> — TypeScript/JavaScript، PHP، Elixir
|
||||
</p>
|
||||
<p>
|
||||
<strong class="text-gray-800 dark:text-neutral-200">الممارسات</strong> — TDD، الكود النظيف، تصميم المجالات، العمارة السداسية، إعادة الهيكلة المستمرّة
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<h2 class="mt-10 mb-4 text-2xl font-bold dark:text-neutral-200">مشاريع مفتوحة المصدر</h2>
|
||||
<div class="space-y-4 text-gray-600 dark:text-neutral-400 leading-relaxed">
|
||||
<p>
|
||||
<Link href="https://debats.co" external>Débats.co</Link> — منصّة تعاونية لتلخيص النقاشات المجتمعية.
|
||||
</p>
|
||||
<p>
|
||||
<Link href="https://github.com/dis-moi" external>DisMoi</Link> — إضافة للمتصفّح في مجال التكنولوجيا المدنية، تضيف معلومات سياقية على الويب.
|
||||
</p>
|
||||
<p>
|
||||
<Link href="https://github.com/betagouv/mon-entreprise" external>mon-entreprise</Link> — المساعد الرسمي لروّاد الأعمال في فرنسا، مشروع <Link href="https://beta.gouv.fr/" external>beta.gouv</Link>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<h2 class="mt-10 mb-4 text-2xl font-bold dark:text-neutral-200">المجتمع</h2>
|
||||
<div class="space-y-4 text-gray-600 dark:text-neutral-400 leading-relaxed">
|
||||
<p>
|
||||
أنشّط مجتمع <Link href="https://www.meetup.com/software-crafters-albi/" external>Software Crafters Albi</Link> منذ 2018. نلتقي بانتظام للحديث عن الكود والممارسات وحرفة البرمجة.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<h2 class="mt-10 mb-4 text-2xl font-bold dark:text-neutral-200">التدريس</h2>
|
||||
<div class="space-y-4 text-gray-600 dark:text-neutral-400 leading-relaxed">
|
||||
<p>
|
||||
أُدرّس البرمجة في <Link href="https://www.univ-jfc.fr/" external>جامعة شامبوليون</Link> في ألبي.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<h2 class="mt-10 mb-4 text-2xl font-bold dark:text-neutral-200">المسار</h2>
|
||||
<div class="space-y-4 text-gray-600 dark:text-neutral-400 leading-relaxed">
|
||||
<p>
|
||||
حاليًا مطوّر في <Link href="https://www.urssaf.fr/" external>Urssaf</Link>. قبل ذلك، مسار مرّ عبر <Link href="https://while42.org/" external>while42</Link> وشركات ناشئة واستشارات. خرّيج <Link href="https://www.uvsq.fr/" external>UVSQ</Link> (أفضل مشروع تخرّج 2003).
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
</Layout>
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
---
|
||||
import PhotoHomeContent from '../../components/photo/pages/PhotoHomeContent.astro';
|
||||
---
|
||||
|
||||
<PhotoHomeContent lang="ar" />
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
---
|
||||
import PhotoAlbumContent from '../../../../components/photo/pages/PhotoAlbumContent.astro';
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const categories = [
|
||||
'portraits', 'places', 'nature',
|
||||
'cultures', 'music', 'sports', 'engines', 'everyday'
|
||||
];
|
||||
|
||||
return categories.map(category => ({
|
||||
params: { category },
|
||||
props: { category }
|
||||
}));
|
||||
}
|
||||
|
||||
const { category } = Astro.params;
|
||||
---
|
||||
|
||||
<PhotoAlbumContent category={category} lang="ar" />
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
---
|
||||
import PhotoBlogPostContent from '../../../../../components/photo/pages/PhotoBlogPostContent.astro';
|
||||
import { getCollection } from 'astro:content';
|
||||
import { getPostBaseSlug } from '../../../../../utils/i18n';
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const allPhotoBlogPosts = await getCollection('photoBlogPosts');
|
||||
const arPosts = allPhotoBlogPosts.filter(post => post.data.lang === 'ar');
|
||||
return arPosts.map(post => {
|
||||
const slug = getPostBaseSlug(post.id);
|
||||
return {
|
||||
params: {
|
||||
year: String(post.data.date.getFullYear()),
|
||||
slug,
|
||||
},
|
||||
props: { post },
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
const { post } = Astro.props;
|
||||
---
|
||||
|
||||
<PhotoBlogPostContent post={post} lang="ar" />
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
---
|
||||
import PhotoBlogIndexContent from '../../../../components/photo/pages/PhotoBlogIndexContent.astro';
|
||||
---
|
||||
|
||||
<PhotoBlogIndexContent lang="ar" />
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
---
|
||||
import PageHeading from "../../components/page-heading.astro";
|
||||
import Layout from "../../layouts/main.astro";
|
||||
import Link from "../../components/Link.astro";
|
||||
---
|
||||
|
||||
<Layout title="مسرح - جليل عرفاوي">
|
||||
<section dir="rtl" lang="ar" class="relative z-20 max-w-2xl mx-auto my-12 px-7 lg:px-0">
|
||||
<PageHeading
|
||||
title="مسرح"
|
||||
description="ممثل ارتجالي منذ 2008، وأخوض اليوم أيضًا غمار المسرح المكتوب."
|
||||
/>
|
||||
|
||||
<h2 class="mt-10 mb-4 text-2xl font-bold dark:text-neutral-200">Les Touchatou</h2>
|
||||
<div class="space-y-4 text-gray-600 dark:text-neutral-400 leading-relaxed">
|
||||
<p>
|
||||
بدأ كل شيء عام 2008 مع <Link href="https://improchatou78.wordpress.com/" external>فرقة Touchatou</Link>، فرقة ارتجال قرب باريس. اكتشاف مباريات الارتجال، التمارين، تلك الطاقة الفريدة حيث يُبنى كل شيء من لا شيء.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<h2 class="mt-10 mb-4 text-2xl font-bold dark:text-neutral-200">LaTTIFA</h2>
|
||||
<div class="space-y-4 text-gray-600 dark:text-neutral-400 leading-relaxed">
|
||||
<p>
|
||||
بين 2013 و2015، في المغرب، انضممت إلى <strong class="text-gray-800 dark:text-neutral-200">LaTTIFA</strong> — فرقة ارتجال مسرحي فرنسية-عربية. التمثيل بلغتين، والتنقّل بين ثقافتين على الخشبة.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<h2 class="mt-10 mb-4 text-2xl font-bold dark:text-neutral-200">Les Particules</h2>
|
||||
<div class="space-y-4 text-gray-600 dark:text-neutral-400 leading-relaxed">
|
||||
<p>
|
||||
في ألبي، شاركت في تأسيس <Link href="https://les-particules.org/" external>Les Particules</Link>، فرقة ارتجال أمثّل معها بانتظام. عروض، مباريات، ورشات.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<h2 class="mt-10 mb-4 text-2xl font-bold dark:text-neutral-200">Lost in Traduction</h2>
|
||||
<div class="space-y-4 text-gray-600 dark:text-neutral-400 leading-relaxed">
|
||||
<p>
|
||||
ممثل في <Link href="https://www.e-translation-agency.com/fr/lost-in-traduction-fr/lost-in-traduction-episode-1-the-pilot/" external>Lost in Traduction</Link>، سلسلة ويب من إنتاج <Link href="https://www.e-translation-agency.com/" external>E translation agency</Link> لإزالة الغموض عن قطاع الترجمة المهنية.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<h2 class="mt-10 mb-4 text-2xl font-bold dark:text-neutral-200">المسرح المكتوب</h2>
|
||||
<div class="space-y-4 text-gray-600 dark:text-neutral-400 leading-relaxed">
|
||||
<p>
|
||||
بعد سنوات من الارتجال، أخوض اليوم أيضًا غمار المسرح المكتوب. اتّجاه جديد، نفس الدافع.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
</Layout>
|
||||
|
|
@ -1,9 +1,7 @@
|
|||
---
|
||||
import { Image } from "astro:assets";
|
||||
import PageHeading from "../../components/page-heading.astro";
|
||||
import Layout from "../../layouts/main.astro";
|
||||
import Link from "../../components/Link.astro";
|
||||
import jalilPhoto from "../../assets/images/jalil.jpg";
|
||||
---
|
||||
|
||||
<Layout title="نبذة عني - جليل عرفاوي">
|
||||
|
|
@ -13,7 +11,7 @@ import jalilPhoto from "../../assets/images/jalil.jpg";
|
|||
description="حِرَفي برمجة، ممثل ارتجالي، مصوّر فضولي."
|
||||
/>
|
||||
|
||||
<Image src={jalilPhoto} class="relative z-30 w-full my-10 rounded-xl" alt="جليل عرفاوي" />
|
||||
<img src="/assets/images/jalil.jpg" class="relative z-30 w-full my-10 rounded-xl" alt="جليل عرفاوي" />
|
||||
|
||||
<h2 class="mb-4 text-2xl font-bold dark:text-neutral-200">من أنا؟</h2>
|
||||
<div class="space-y-4 text-gray-600 dark:text-neutral-400 leading-relaxed">
|
||||
|
|
|
|||
|
|
@ -1,68 +0,0 @@
|
|||
---
|
||||
import PageHeading from "../components/page-heading.astro";
|
||||
import Layout from "../layouts/main.astro";
|
||||
import Link from "../components/Link.astro";
|
||||
---
|
||||
|
||||
<Layout title="Code - Jalil Arfaoui">
|
||||
<section class="relative z-20 max-w-2xl mx-auto my-12 px-7 lg:px-0">
|
||||
<PageHeading
|
||||
title="Code"
|
||||
description="Plus de 20 ans à construire du logiciel. Craftsmanship, TDD, DDD — et une obsession pour les biais qu'on met dans le code sans le savoir."
|
||||
/>
|
||||
|
||||
<h2 class="mt-10 mb-4 text-2xl font-bold dark:text-neutral-200">Ce que je fais</h2>
|
||||
<div class="space-y-4 text-gray-600 dark:text-neutral-400 leading-relaxed">
|
||||
<p>
|
||||
Développeur freelance basé à <strong class="text-gray-800 dark:text-neutral-200">Albi</strong>, j'accompagne les équipes comme développeur senior, tech lead ou coach technique. Je privilégie le logiciel libre et les outils qui répondent à de vrais besoins.
|
||||
</p>
|
||||
<p>
|
||||
Mon approche : <strong class="text-gray-800 dark:text-neutral-200">TDD, Clean Code, Domain-Driven Design</strong>. Le code est un matériau qu'on façonne avec soin, pas une marchandise qu'on produit à la chaîne.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<h2 class="mt-10 mb-4 text-2xl font-bold dark:text-neutral-200">Compétences</h2>
|
||||
<div class="space-y-4 text-gray-600 dark:text-neutral-400 leading-relaxed">
|
||||
<p>
|
||||
<strong class="text-gray-800 dark:text-neutral-200">Langages</strong> — TypeScript/JavaScript, PHP, Elixir
|
||||
</p>
|
||||
<p>
|
||||
<strong class="text-gray-800 dark:text-neutral-200">Pratiques</strong> — TDD, Clean Code, Domain-Driven Design, architecture hexagonale, refactoring continu
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<h2 class="mt-10 mb-4 text-2xl font-bold dark:text-neutral-200">Projets open source</h2>
|
||||
<div class="space-y-4 text-gray-600 dark:text-neutral-400 leading-relaxed">
|
||||
<p>
|
||||
<Link href="https://debats.co" external>Débats.co</Link> — Plateforme collaborative de synthèse des débats de société.
|
||||
</p>
|
||||
<p>
|
||||
<Link href="https://github.com/dis-moi" external>DisMoi</Link> — Extension navigateur civic tech, pour ajouter de l'information contextuelle sur le web.
|
||||
</p>
|
||||
<p>
|
||||
<Link href="https://github.com/betagouv/mon-entreprise" external>mon-entreprise</Link> — L'assistant officiel des entrepreneurs, un projet <Link href="https://beta.gouv.fr/" external>beta.gouv</Link>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<h2 class="mt-10 mb-4 text-2xl font-bold dark:text-neutral-200">Communauté</h2>
|
||||
<div class="space-y-4 text-gray-600 dark:text-neutral-400 leading-relaxed">
|
||||
<p>
|
||||
J'anime les <Link href="https://www.meetup.com/software-crafters-albi/" external>Software Crafters d'Albi</Link> depuis 2018. On se retrouve régulièrement pour parler code, pratiques et artisanat logiciel.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<h2 class="mt-10 mb-4 text-2xl font-bold dark:text-neutral-200">Enseignement</h2>
|
||||
<div class="space-y-4 text-gray-600 dark:text-neutral-400 leading-relaxed">
|
||||
<p>
|
||||
J'enseigne la programmation à <Link href="https://www.univ-jfc.fr/" external>l'université Champollion</Link> à Albi.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<h2 class="mt-10 mb-4 text-2xl font-bold dark:text-neutral-200">Parcours</h2>
|
||||
<div class="space-y-4 text-gray-600 dark:text-neutral-400 leading-relaxed">
|
||||
<p>
|
||||
Actuellement développeur pour l'<Link href="https://www.urssaf.fr/" external>Urssaf</Link>. Avant ça, un chemin passé par <Link href="https://while42.org/" external>while42</Link>, des startups et du conseil. Diplômé de l'<Link href="https://www.uvsq.fr/" external>UVSQ</Link> (meilleur projet de promo 2003).
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
</Layout>
|
||||
|
|
@ -1,9 +1,7 @@
|
|||
---
|
||||
import { Image } from "astro:assets";
|
||||
import PageHeading from "../../components/page-heading.astro";
|
||||
import Layout from "../../layouts/main.astro";
|
||||
import Link from "../../components/Link.astro";
|
||||
import jalilPhoto from "../../assets/images/jalil.jpg";
|
||||
---
|
||||
|
||||
<Layout title="About - Jalil Arfaoui">
|
||||
|
|
@ -13,7 +11,7 @@ import jalilPhoto from "../../assets/images/jalil.jpg";
|
|||
description="Software craftsman, improv actor, curious photographer."
|
||||
/>
|
||||
|
||||
<Image src={jalilPhoto} class="relative z-30 w-full my-10 rounded-xl" alt="Jalil Arfaoui" />
|
||||
<img src="/assets/images/jalil.jpg" class="relative z-30 w-full my-10 rounded-xl" alt="Jalil Arfaoui" />
|
||||
|
||||
<h2 class="mb-4 text-2xl font-bold dark:text-neutral-200">Who am I?</h2>
|
||||
<div class="space-y-4 text-gray-600 dark:text-neutral-400 leading-relaxed">
|
||||
|
|
|
|||
|
|
@ -1,49 +0,0 @@
|
|||
---
|
||||
import PageHeading from "../../components/page-heading.astro";
|
||||
import Layout from "../../layouts/main.astro";
|
||||
import Link from "../../components/Link.astro";
|
||||
---
|
||||
|
||||
<Layout title="Acting - Jalil Arfaoui">
|
||||
<section class="relative z-20 max-w-2xl mx-auto my-12 px-7 lg:px-0">
|
||||
<PageHeading
|
||||
title="Acting"
|
||||
description="Improv actor since 2008, now also taking on scripted theater."
|
||||
/>
|
||||
|
||||
<h2 class="mt-10 mb-4 text-2xl font-bold dark:text-neutral-200">Les Touchatou</h2>
|
||||
<div class="space-y-4 text-gray-600 dark:text-neutral-400 leading-relaxed">
|
||||
<p>
|
||||
It all started in 2008 with <Link href="https://improchatou78.wordpress.com/" external>les Touchatou</Link>, an improv troupe near Paris. Discovering improv matches, exercises, that unique energy where everything is built from nothing.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<h2 class="mt-10 mb-4 text-2xl font-bold dark:text-neutral-200">LaTTIFA</h2>
|
||||
<div class="space-y-4 text-gray-600 dark:text-neutral-400 leading-relaxed">
|
||||
<p>
|
||||
Between 2013 and 2015, in Morocco, I joined <strong class="text-gray-800 dark:text-neutral-200">LaTTIFA</strong> — a French-Arab improv theater troupe. Performing in two languages, navigating two cultures on stage.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<h2 class="mt-10 mb-4 text-2xl font-bold dark:text-neutral-200">Les Particules</h2>
|
||||
<div class="space-y-4 text-gray-600 dark:text-neutral-400 leading-relaxed">
|
||||
<p>
|
||||
In Albi, I co-founded <Link href="https://les-particules.org/" external>Les Particules</Link>, an improv troupe I perform with regularly. Shows, matches, workshops.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<h2 class="mt-10 mb-4 text-2xl font-bold dark:text-neutral-200">Lost in Traduction</h2>
|
||||
<div class="space-y-4 text-gray-600 dark:text-neutral-400 leading-relaxed">
|
||||
<p>
|
||||
Actor in <Link href="https://www.e-translation-agency.com/fr/lost-in-traduction-fr/lost-in-traduction-episode-1-the-pilot/" external>Lost in Traduction</Link>, a web series produced by <Link href="https://www.e-translation-agency.com/" external>E translation agency</Link> to demystify the professional translation industry.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<h2 class="mt-10 mb-4 text-2xl font-bold dark:text-neutral-200">Scripted theater</h2>
|
||||
<div class="space-y-4 text-gray-600 dark:text-neutral-400 leading-relaxed">
|
||||
<p>
|
||||
After years of improvisation, I'm now also taking on scripted theater. New direction, same drive.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
</Layout>
|
||||
|
|
@ -1,68 +0,0 @@
|
|||
---
|
||||
import PageHeading from "../../components/page-heading.astro";
|
||||
import Layout from "../../layouts/main.astro";
|
||||
import Link from "../../components/Link.astro";
|
||||
---
|
||||
|
||||
<Layout title="Code - Jalil Arfaoui">
|
||||
<section class="relative z-20 max-w-2xl mx-auto my-12 px-7 lg:px-0">
|
||||
<PageHeading
|
||||
title="Code"
|
||||
description="Over 20 years building software. Craftsmanship, TDD, DDD — and an obsession with the biases we unknowingly put into code."
|
||||
/>
|
||||
|
||||
<h2 class="mt-10 mb-4 text-2xl font-bold dark:text-neutral-200">What I do</h2>
|
||||
<div class="space-y-4 text-gray-600 dark:text-neutral-400 leading-relaxed">
|
||||
<p>
|
||||
Freelance developer based in <strong class="text-gray-800 dark:text-neutral-200">Albi, France</strong>, I work with teams as a senior developer, tech lead or technical coach. I favor free software and tools that address real needs.
|
||||
</p>
|
||||
<p>
|
||||
My approach: <strong class="text-gray-800 dark:text-neutral-200">TDD, Clean Code, Domain-Driven Design</strong>. Code is a material to be crafted with care, not a commodity to be mass-produced.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<h2 class="mt-10 mb-4 text-2xl font-bold dark:text-neutral-200">Skills</h2>
|
||||
<div class="space-y-4 text-gray-600 dark:text-neutral-400 leading-relaxed">
|
||||
<p>
|
||||
<strong class="text-gray-800 dark:text-neutral-200">Languages</strong> — TypeScript/JavaScript, PHP, Elixir
|
||||
</p>
|
||||
<p>
|
||||
<strong class="text-gray-800 dark:text-neutral-200">Practices</strong> — TDD, Clean Code, Domain-Driven Design, hexagonal architecture, continuous refactoring
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<h2 class="mt-10 mb-4 text-2xl font-bold dark:text-neutral-200">Open source projects</h2>
|
||||
<div class="space-y-4 text-gray-600 dark:text-neutral-400 leading-relaxed">
|
||||
<p>
|
||||
<Link href="https://debats.co" external>Débats.co</Link> — A collaborative platform for synthesizing public debates.
|
||||
</p>
|
||||
<p>
|
||||
<Link href="https://github.com/dis-moi" external>DisMoi</Link> — A civic tech browser extension that adds contextual information to the web.
|
||||
</p>
|
||||
<p>
|
||||
<Link href="https://github.com/betagouv/mon-entreprise" external>mon-entreprise</Link> — The official assistant for entrepreneurs in France, a <Link href="https://beta.gouv.fr/" external>beta.gouv</Link> project.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<h2 class="mt-10 mb-4 text-2xl font-bold dark:text-neutral-200">Community</h2>
|
||||
<div class="space-y-4 text-gray-600 dark:text-neutral-400 leading-relaxed">
|
||||
<p>
|
||||
I've been running the <Link href="https://www.meetup.com/software-crafters-albi/" external>Software Crafters Albi</Link> meetup since 2018. We gather regularly to discuss code, practices and software craftsmanship.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<h2 class="mt-10 mb-4 text-2xl font-bold dark:text-neutral-200">Teaching</h2>
|
||||
<div class="space-y-4 text-gray-600 dark:text-neutral-400 leading-relaxed">
|
||||
<p>
|
||||
I teach programming at <Link href="https://www.univ-jfc.fr/" external>Université Champollion</Link> in Albi.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<h2 class="mt-10 mb-4 text-2xl font-bold dark:text-neutral-200">Background</h2>
|
||||
<div class="space-y-4 text-gray-600 dark:text-neutral-400 leading-relaxed">
|
||||
<p>
|
||||
Currently a developer at <Link href="https://www.urssaf.fr/" external>Urssaf</Link>. Before that, a path through <Link href="https://while42.org/" external>while42</Link>, startups and consulting. Graduated from <Link href="https://www.uvsq.fr/" external>UVSQ</Link> (best graduation project 2003).
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
</Layout>
|
||||
|
|
@ -1,7 +1,5 @@
|
|||
---
|
||||
import { Image } from "astro:assets";
|
||||
import Layout from "../../layouts/main.astro";
|
||||
import jalilPhoto from "../../assets/images/jalil-2.jpg";
|
||||
---
|
||||
|
||||
<Layout title="Jalil Arfaoui - Software Craftsman • Improv Actor • Photographer">
|
||||
|
|
@ -20,9 +18,10 @@ import jalilPhoto from "../../assets/images/jalil-2.jpg";
|
|||
</p>
|
||||
|
||||
<div class="flex flex-wrap gap-4">
|
||||
<a href="/en/code" class="inline-flex items-center px-6 py-3 text-sm font-semibold text-white bg-blue-600 rounded-full hover:bg-blue-700 transition-colors duration-200">
|
||||
<span class="wip-link inline-flex items-center px-6 py-3 text-sm font-semibold text-white/60 bg-blue-600/50 rounded-full cursor-not-allowed" title="Under construction">
|
||||
View my work
|
||||
</a>
|
||||
<span class="ml-2">🚧</span>
|
||||
</span>
|
||||
<a href="/en/about" class="inline-flex items-center px-6 py-3 text-sm font-semibold text-neutral-700 bg-neutral-100 dark:bg-neutral-800 dark:text-neutral-200 rounded-full hover:bg-neutral-200 dark:hover:bg-neutral-700 transition-colors duration-200">
|
||||
Learn more
|
||||
</a>
|
||||
|
|
@ -33,8 +32,8 @@ import jalilPhoto from "../../assets/images/jalil-2.jpg";
|
|||
<div class="relative z-50 w-full max-w-sm mx-auto">
|
||||
<div class="relative z-30 p-1 bg-gradient-to-br from-blue-500 via-purple-500 to-pink-500 rounded-3xl">
|
||||
<div class="bg-white dark:bg-neutral-950 rounded-3xl p-4">
|
||||
<Image
|
||||
src={jalilPhoto}
|
||||
<img
|
||||
src="/assets/images/jalil-2.jpg"
|
||||
alt="Jalil Arfaoui"
|
||||
loading="eager"
|
||||
decoding="auto"
|
||||
|
|
@ -60,9 +59,15 @@ import jalilPhoto from "../../assets/images/jalil-2.jpg";
|
|||
Over 20 years building software. Today, I choose my projects: free software, useful tools, nothing that alienates. Craftsmanship, DDD, TypeScript — and an obsession with the biases we unknowingly put into code. I teach programming and run the Software Crafters meetup in Albi.
|
||||
</p>
|
||||
<div class="space-y-3">
|
||||
<a href="/en/code" class="block text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-200 font-medium">
|
||||
→ Learn more
|
||||
</a>
|
||||
<span class="block text-blue-400/50 dark:text-blue-600/50 cursor-not-allowed" title="Under construction">
|
||||
→ Professional journey 🚧
|
||||
</span>
|
||||
<span class="block text-blue-400/50 dark:text-blue-600/50 cursor-not-allowed" title="Under construction">
|
||||
→ My projects 🚧
|
||||
</span>
|
||||
<span class="block text-blue-400/50 dark:text-blue-600/50 cursor-not-allowed" title="Under construction">
|
||||
→ Technical blog 🚧
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -77,9 +82,9 @@ import jalilPhoto from "../../assets/images/jalil-2.jpg";
|
|||
Improv actor since 2008, from the Paris suburbs to Morocco before co-founding Les Particules in Albi. Now also taking on scripted theater.
|
||||
</p>
|
||||
<div class="space-y-3">
|
||||
<a href="/en/acting" class="block text-orange-600 dark:text-orange-400 hover:text-orange-800 dark:hover:text-orange-200 font-medium">
|
||||
→ Artistic journey
|
||||
</a>
|
||||
<span class="block text-orange-400/50 dark:text-orange-600/50 cursor-not-allowed" title="Under construction">
|
||||
→ Artistic journey 🚧
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -94,10 +99,10 @@ import jalilPhoto from "../../assets/images/jalil-2.jpg";
|
|||
I photograph a bit of everything: faces, concerts, engines, everyday life. No specialty, just curiosity.
|
||||
</p>
|
||||
<div class="space-y-3">
|
||||
<a href="/en/photo" class="block text-purple-600 dark:text-purple-400 hover:text-purple-800 dark:hover:text-purple-200 font-medium">
|
||||
<a href="/photo" class="block text-purple-600 dark:text-purple-400 hover:text-purple-800 dark:hover:text-purple-200 font-medium">
|
||||
→ Photo portfolio
|
||||
</a>
|
||||
<a href="/en/photo/blog" class="block text-purple-600 dark:text-purple-400 hover:text-purple-800 dark:hover:text-purple-200 font-medium">
|
||||
<a href="/photo/blog" class="block text-purple-600 dark:text-purple-400 hover:text-purple-800 dark:hover:text-purple-200 font-medium">
|
||||
→ Photo Feed
|
||||
</a>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,5 +0,0 @@
|
|||
---
|
||||
import PhotoHomeContent from '../../components/photo/pages/PhotoHomeContent.astro';
|
||||
---
|
||||
|
||||
<PhotoHomeContent lang="en" />
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
---
|
||||
import PhotoAlbumContent from '../../../../components/photo/pages/PhotoAlbumContent.astro';
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const categories = [
|
||||
'portraits', 'places', 'nature',
|
||||
'cultures', 'music', 'sports', 'engines', 'everyday'
|
||||
];
|
||||
|
||||
return categories.map(category => ({
|
||||
params: { category },
|
||||
props: { category }
|
||||
}));
|
||||
}
|
||||
|
||||
const { category } = Astro.params;
|
||||
---
|
||||
|
||||
<PhotoAlbumContent category={category} lang="en" />
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
---
|
||||
import PhotoBlogPostContent from '../../../../../components/photo/pages/PhotoBlogPostContent.astro';
|
||||
import { getCollection } from 'astro:content';
|
||||
import { getPostBaseSlug } from '../../../../../utils/i18n';
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const allPhotoBlogPosts = await getCollection('photoBlogPosts');
|
||||
const enPosts = allPhotoBlogPosts.filter(post => post.data.lang === 'en');
|
||||
return enPosts.map(post => {
|
||||
const slug = getPostBaseSlug(post.id);
|
||||
return {
|
||||
params: {
|
||||
year: String(post.data.date.getFullYear()),
|
||||
slug,
|
||||
},
|
||||
props: { post },
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
const { post } = Astro.props;
|
||||
---
|
||||
|
||||
<PhotoBlogPostContent post={post} lang="en" />
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
---
|
||||
import PhotoBlogIndexContent from '../../../../components/photo/pages/PhotoBlogIndexContent.astro';
|
||||
---
|
||||
|
||||
<PhotoBlogIndexContent lang="en" />
|
||||
|
|
@ -1,7 +1,5 @@
|
|||
---
|
||||
import { Image } from "astro:assets";
|
||||
import Layout from "../layouts/main.astro";
|
||||
import jalilPhoto from "../assets/images/jalil-2.jpg";
|
||||
---
|
||||
|
||||
<Layout title="Jalil Arfaoui - Développeur artisan • Comédien improvisateur • Photographe">
|
||||
|
|
@ -20,9 +18,10 @@ import jalilPhoto from "../assets/images/jalil-2.jpg";
|
|||
</p>
|
||||
|
||||
<div class="flex flex-wrap gap-4">
|
||||
<a href="/code" class="inline-flex items-center px-6 py-3 text-sm font-semibold text-white bg-blue-600 rounded-full hover:bg-blue-700 transition-colors duration-200">
|
||||
<span class="wip-link inline-flex items-center px-6 py-3 text-sm font-semibold text-white/60 bg-blue-600/50 rounded-full cursor-not-allowed" title="En construction">
|
||||
Voir mon travail
|
||||
</a>
|
||||
<span class="ml-2">🚧</span>
|
||||
</span>
|
||||
<a href="/a-propos" class="inline-flex items-center px-6 py-3 text-sm font-semibold text-neutral-700 bg-neutral-100 dark:bg-neutral-800 dark:text-neutral-200 rounded-full hover:bg-neutral-200 dark:hover:bg-neutral-700 transition-colors duration-200">
|
||||
En savoir plus
|
||||
</a>
|
||||
|
|
@ -33,8 +32,8 @@ import jalilPhoto from "../assets/images/jalil-2.jpg";
|
|||
<div class="relative z-50 w-full max-w-sm mx-auto">
|
||||
<div class="relative z-30 p-1 bg-gradient-to-br from-blue-500 via-purple-500 to-pink-500 rounded-3xl">
|
||||
<div class="bg-white dark:bg-neutral-950 rounded-3xl p-4">
|
||||
<Image
|
||||
src={jalilPhoto}
|
||||
<img
|
||||
src="/assets/images/jalil-2.jpg"
|
||||
alt="Jalil Arfaoui"
|
||||
loading="eager"
|
||||
decoding="auto"
|
||||
|
|
@ -60,9 +59,15 @@ import jalilPhoto from "../assets/images/jalil-2.jpg";
|
|||
Plus de 20 ans à construire du logiciel. Aujourd'hui, je choisis mes projets : du libre, de l'utile, rien qui aliène. Craftsmanship, DDD, TypeScript — et une obsession pour les biais qu'on met dans le code sans le savoir. J'enseigne la programmation et j'anime les Software Crafters d'Albi.
|
||||
</p>
|
||||
<div class="space-y-3">
|
||||
<a href="/code" class="block text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-200 font-medium">
|
||||
→ En savoir plus
|
||||
</a>
|
||||
<span class="block text-blue-400/50 dark:text-blue-600/50 cursor-not-allowed" title="En construction">
|
||||
→ Parcours professionnel 🚧
|
||||
</span>
|
||||
<span class="block text-blue-400/50 dark:text-blue-600/50 cursor-not-allowed" title="En construction">
|
||||
→ Mes projets 🚧
|
||||
</span>
|
||||
<span class="block text-blue-400/50 dark:text-blue-600/50 cursor-not-allowed" title="En construction">
|
||||
→ Blog technique 🚧
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -77,9 +82,9 @@ import jalilPhoto from "../assets/images/jalil-2.jpg";
|
|||
Improvisateur depuis 2008, passé par les Yvelines et le Maroc avant de cofonder Les Particules à Albi. Aujourd'hui je m'attaque aussi au théâtre écrit.
|
||||
</p>
|
||||
<div class="space-y-3">
|
||||
<a href="/theatre" class="block text-orange-600 dark:text-orange-400 hover:text-orange-800 dark:hover:text-orange-200 font-medium">
|
||||
→ Parcours artistique
|
||||
</a>
|
||||
<span class="block text-orange-400/50 dark:text-orange-600/50 cursor-not-allowed" title="En construction">
|
||||
→ Parcours artistique 🚧
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,14 @@
|
|||
---
|
||||
import PhotoHomeContent from '../components/photo/pages/PhotoHomeContent.astro';
|
||||
import PhotoLayout from '../layouts/PhotoLayout.astro';
|
||||
import PhotoGallery from '../components/photo/PhotoGallery.astro';
|
||||
import HeroViewport from '../components/photo/HeroViewport.astro';
|
||||
import ExploreSection from '../components/photo/ExploreSection.astro';
|
||||
|
||||
const title = "Galerie Photo - Jalil Arfaoui";
|
||||
---
|
||||
|
||||
<PhotoHomeContent lang="fr" />
|
||||
<PhotoLayout title={title} enableScroll={true}>
|
||||
<PhotoGallery category="all" />
|
||||
<HeroViewport targetSelector="#explore-section" transparent />
|
||||
<ExploreSection />
|
||||
</PhotoLayout>
|
||||
|
|
|
|||
36
src/pages/photo/albums/[...category].astro
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
---
|
||||
import PhotoLayout from '../../../layouts/PhotoLayout.astro';
|
||||
import CategoryGrid from '../../../components/photo/CategoryGrid.astro';
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const categories = [
|
||||
'blog', 'portraits', 'places', 'nature',
|
||||
'cultures', 'music', 'sports', 'engines', 'everyday'
|
||||
];
|
||||
|
||||
return categories.map(category => ({
|
||||
params: { category },
|
||||
props: { category }
|
||||
}));
|
||||
}
|
||||
|
||||
const { category } = Astro.params;
|
||||
|
||||
const categoryLabels: Record<string, string> = {
|
||||
'blog': 'Blog',
|
||||
'portraits': 'Portraits',
|
||||
'places': 'Paysages',
|
||||
'nature': 'Nature',
|
||||
'cultures': 'Cultures',
|
||||
'music': 'Musique',
|
||||
'sports': 'Sports',
|
||||
'engines': 'Moteurs',
|
||||
'everyday': 'Quotidien'
|
||||
};
|
||||
|
||||
const title = `Galerie ${category ? categoryLabels[category] || category : ''} - Jalil Arfaoui`;
|
||||
---
|
||||
|
||||
<PhotoLayout title={title} enableScroll={true}>
|
||||
<CategoryGrid category={category} />
|
||||
</PhotoLayout>
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
---
|
||||
import PhotoAlbumContent from '../../../components/photo/pages/PhotoAlbumContent.astro';
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const categories = [
|
||||
'portraits', 'places', 'nature',
|
||||
'cultures', 'music', 'sports', 'engines', 'everyday'
|
||||
];
|
||||
|
||||
return categories.map(category => ({
|
||||
params: { category },
|
||||
props: { category }
|
||||
}));
|
||||
}
|
||||
|
||||
const { category } = Astro.params;
|
||||
---
|
||||
|
||||
<PhotoAlbumContent category={category} lang="fr" />
|
||||
|
|
@ -1,14 +1,19 @@
|
|||
---
|
||||
import PhotoBlogPostContent from '../../../../components/photo/pages/PhotoBlogPostContent.astro';
|
||||
import PhotoLayout from '../../../../layouts/PhotoLayout.astro';
|
||||
import CategoryNav from '../../../../components/photo/CategoryNav.astro';
|
||||
import AlbumHeader from '../../../../components/photo/AlbumHeader.astro';
|
||||
import MasonryGallery from '../../../../components/photo/MasonryGallery.astro';
|
||||
import Lightbox from '../../../../components/photo/Lightbox.astro';
|
||||
import { getCollection } from 'astro:content';
|
||||
import { getPostBaseSlug } from '../../../../utils/i18n';
|
||||
|
||||
// Importer toutes les images du dossier photos
|
||||
const allImages = import.meta.glob<{ default: ImageMetadata }>('/src/assets/images/photos/blog/**/*.{jpg,jpeg,png,webp}');
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const allPhotoBlogPosts = await getCollection('photoBlogPosts');
|
||||
// Uniquement les posts FR (lang absent ou 'fr')
|
||||
const frPosts = allPhotoBlogPosts.filter(post => (post.data.lang ?? 'fr') === 'fr');
|
||||
return frPosts.map(post => {
|
||||
const slug = getPostBaseSlug(post.id);
|
||||
return allPhotoBlogPosts.map(post => {
|
||||
// Le slug Astro inclut le préfixe d'année (ex: "2015/enigma.en")
|
||||
const slug = post.slug.replace(/^\d{4}\//, '');
|
||||
return {
|
||||
params: {
|
||||
year: String(post.data.date.getFullYear()),
|
||||
|
|
@ -20,6 +25,104 @@ export async function getStaticPaths() {
|
|||
}
|
||||
|
||||
const { post } = Astro.props;
|
||||
const { Content } = await post.render();
|
||||
|
||||
// Slug de base sans préfixe d'année ni suffixe de langue (2015/enigma.en → enigma)
|
||||
const baseSlug = post.slug.replace(/^\d{4}\//, '').replace(/\.(en|ar)$/, '');
|
||||
|
||||
// Construire le chemin de l'album avec l'année
|
||||
const year = post.data.date.getFullYear();
|
||||
const albumPath = `/src/assets/images/photos/blog/${year}/${baseSlug}/`;
|
||||
const albumImages = Object.keys(allImages)
|
||||
.filter(path => path.startsWith(albumPath))
|
||||
.sort();
|
||||
|
||||
// Résoudre la cover image depuis le glob
|
||||
const coverPath = `/src/assets/images/photos/blog/${year}/${baseSlug}/${post.data.coverImage}`;
|
||||
const coverImageLoader = allImages[coverPath];
|
||||
const coverImage = coverImageLoader ? (await coverImageLoader()).default : undefined;
|
||||
|
||||
// Résoudre les images de la galerie
|
||||
const galleryImages = await Promise.all(
|
||||
albumImages.map(async (imagePath) => {
|
||||
const loader = allImages[imagePath];
|
||||
const img = await loader();
|
||||
const filename = imagePath.split('/').pop() || '';
|
||||
return {
|
||||
src: img.default,
|
||||
alt: filename.replace(/\.[^/.]+$/, '').replace(/-/g, ' ').replace(/^\d+-/, ''),
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
// Données pour la lightbox
|
||||
const lightboxImages = galleryImages.map(img => ({
|
||||
src: img.src.src,
|
||||
alt: img.alt
|
||||
}));
|
||||
---
|
||||
|
||||
<PhotoBlogPostContent post={post} lang="fr" />
|
||||
<PhotoLayout title={`${post.data.title} - Blog Photo - Jalil Arfaoui`} enableScroll={true} hideFooter={false}>
|
||||
<div class="album-container">
|
||||
<CategoryNav currentCategory="blog" opaque={false} />
|
||||
|
||||
<AlbumHeader
|
||||
title={post.data.title}
|
||||
description={post.data.description}
|
||||
date={new Date(post.data.date)}
|
||||
tags={post.data.tags}
|
||||
coverImage={coverImage}
|
||||
scrollTarget="#album-content"
|
||||
/>
|
||||
|
||||
<div id="album-content">
|
||||
{post.body && (
|
||||
<div class="post-content">
|
||||
<Content />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<MasonryGallery images={galleryImages} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Lightbox images={lightboxImages} albumTitle={post.data.title} />
|
||||
</PhotoLayout>
|
||||
|
||||
<style>
|
||||
.album-container {
|
||||
background: #000;
|
||||
color: #fff;
|
||||
min-height: 100vh;
|
||||
padding-top: var(--header-height, 53px);
|
||||
}
|
||||
|
||||
.post-content {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 0 20px 40px;
|
||||
line-height: 1.6;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.post-content :global(h1),
|
||||
.post-content :global(h2),
|
||||
.post-content :global(h3) {
|
||||
margin: 2em 0 1em 0;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.post-content :global(p) {
|
||||
margin: 1.5em 0;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.post-content :global(img) {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
border-radius: 8px;
|
||||
margin: 2em 0;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,326 @@
|
|||
---
|
||||
import PhotoBlogIndexContent from '../../../components/photo/pages/PhotoBlogIndexContent.astro';
|
||||
import PhotoLayout from '../../../layouts/PhotoLayout.astro';
|
||||
import CategoryNav from '../../../components/photo/CategoryNav.astro';
|
||||
import { getCollection } from 'astro:content';
|
||||
import { Picture } from 'astro:assets';
|
||||
|
||||
// Importer toutes les images pour résoudre les cover images
|
||||
const allImages = import.meta.glob<{ default: ImageMetadata }>(
|
||||
'/src/assets/images/photos/blog/**/*.{jpg,jpeg,png,webp}'
|
||||
);
|
||||
|
||||
// Récupération des posts photo (langue par défaut : FR)
|
||||
const allPhotoBlogPosts = (await getCollection('photoBlogPosts'))
|
||||
.filter(post => (post.data.lang ?? 'fr') === 'fr');
|
||||
|
||||
// Tri par date (plus récent en premier)
|
||||
const sortedPosts = allPhotoBlogPosts.sort((a, b) =>
|
||||
new Date(b.data.date).getTime() - new Date(a.data.date).getTime()
|
||||
);
|
||||
|
||||
// Résoudre les cover images via le glob
|
||||
const postsWithImages = await Promise.all(sortedPosts.map(async (post) => {
|
||||
const year = post.data.date.getFullYear();
|
||||
const baseSlug = post.slug.replace(/^\d{4}\//, '').replace(/\.(en|ar)$/, '');
|
||||
const coverPath = `/src/assets/images/photos/blog/${year}/${baseSlug}/${post.data.coverImage}`;
|
||||
const loader = allImages[coverPath];
|
||||
const resolvedCoverImage = loader ? (await loader()).default : undefined;
|
||||
return { ...post, resolvedCoverImage };
|
||||
}));
|
||||
|
||||
// Séparer les posts à la une des autres
|
||||
const featuredPosts = postsWithImages.filter(post => post.data.featured);
|
||||
const regularPosts = postsWithImages.filter(post => !post.data.featured);
|
||||
---
|
||||
|
||||
<PhotoBlogIndexContent lang="fr" />
|
||||
<PhotoLayout title="Blog Photo - Jalil Arfaoui" enableScroll={true} hideFooter={false}>
|
||||
<div class="blog-container">
|
||||
<CategoryNav currentCategory="blog" opaque={false} />
|
||||
|
||||
<!-- Section héro avec posts à la une -->
|
||||
{featuredPosts.length > 0 && (
|
||||
<section class="featured-section">
|
||||
<div class="featured-grid">
|
||||
{featuredPosts.map(post => (
|
||||
<article class="featured-post">
|
||||
<a href={`/photo/blog/${post.slug}`} class="post-link">
|
||||
<div class="post-image">
|
||||
{post.resolvedCoverImage && <Picture src={post.resolvedCoverImage} alt={post.data.title} widths={[600, 900, 1200]} formats={['webp']} />}
|
||||
<div class="post-overlay">
|
||||
<div class="post-content">
|
||||
<span class="post-badge">À la une</span>
|
||||
<h2 class="post-title">{post.data.title}</h2>
|
||||
<time class="post-date">
|
||||
{new Date(post.data.date).toLocaleDateString('fr-FR', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
})}
|
||||
</time>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
<!-- Grille des autres posts -->
|
||||
<section class="posts-grid">
|
||||
<div class="grid-container">
|
||||
{regularPosts.map(post => (
|
||||
<article class="post-item">
|
||||
<a href={`/photo/blog/${post.slug}`} class="post-link">
|
||||
{post.resolvedCoverImage && <Picture src={post.resolvedCoverImage} alt={post.data.title} widths={[400, 600, 800]} formats={['webp']} />}
|
||||
<div class="post-overlay">
|
||||
<div class="overlay-content">
|
||||
<h3 class="post-title">{post.data.title}</h3>
|
||||
<time class="post-date">
|
||||
{new Date(post.data.date).toLocaleDateString('fr-FR', {
|
||||
year: 'numeric',
|
||||
month: 'short'
|
||||
})}
|
||||
</time>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</PhotoLayout>
|
||||
|
||||
<style>
|
||||
.blog-container {
|
||||
background: #000000;
|
||||
color: #ffffff;
|
||||
min-height: 100vh;
|
||||
padding-top: 53px; /* Hauteur du header fixe */
|
||||
}
|
||||
|
||||
/* Section à la une */
|
||||
.featured-section {
|
||||
padding: 16px 16px 0;
|
||||
}
|
||||
|
||||
.featured-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(500px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.featured-post {
|
||||
position: relative;
|
||||
height: 450px;
|
||||
overflow: hidden;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.featured-post .post-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.featured-post img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.featured-post:hover img {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.featured-post .post-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(to bottom, rgba(0,0,0,0.1) 0%, rgba(0,0,0,0.7) 100%);
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
.featured-post .post-content {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.post-badge {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
padding: 4px 12px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
margin-bottom: 12px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.featured-post .post-title {
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
margin: 0 0 4px 0;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.featured-post .post-description {
|
||||
font-size: 15px;
|
||||
margin: 0 0 10px 0;
|
||||
opacity: 0.9;
|
||||
line-height: 1.3;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.featured-post .post-date {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* Grille des posts */
|
||||
.posts-grid {
|
||||
padding: 16px 16px 100px;
|
||||
}
|
||||
|
||||
.grid-container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.post-item {
|
||||
overflow: hidden;
|
||||
border-radius: 4px;
|
||||
background: #111;
|
||||
}
|
||||
|
||||
.post-link {
|
||||
display: block;
|
||||
position: relative;
|
||||
aspect-ratio: 3/2;
|
||||
}
|
||||
|
||||
.featured-post .post-link {
|
||||
aspect-ratio: auto;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.post-link img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.post-item .post-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: linear-gradient(to top, rgba(0,0,0,0.85) 0%, rgba(0,0,0,0.5) 25%, rgba(0,0,0,0) 50%);
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
padding: 12px 16px;
|
||||
transition: background 0.3s ease;
|
||||
}
|
||||
|
||||
.post-item:hover .post-overlay {
|
||||
background: linear-gradient(to top, rgba(0,0,0,0.95) 0%, rgba(0,0,0,0.7) 25%, rgba(0,0,0,0) 50%);
|
||||
}
|
||||
|
||||
.post-item:hover img {
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
.post-item .overlay-content {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.post-item .post-title {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
margin: 0 0 2px 0;
|
||||
line-height: 1.3;
|
||||
text-shadow: 0 1px 3px rgba(0,0,0,0.8);
|
||||
}
|
||||
|
||||
.post-item .post-subtitle {
|
||||
font-size: 14px;
|
||||
margin: 0 0 6px 0;
|
||||
opacity: 0.9;
|
||||
line-height: 1.2;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.post-item .post-date {
|
||||
font-size: 12px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 1200px) {
|
||||
.grid-container {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.featured-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.featured-post {
|
||||
height: 300px;
|
||||
}
|
||||
|
||||
.featured-post .post-overlay {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.featured-post .post-title {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.grid-container {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.blog-container {
|
||||
padding-top: 53px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.grid-container {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.featured-section {
|
||||
padding: 12px 12px 0;
|
||||
}
|
||||
|
||||
.posts-grid {
|
||||
padding: 12px 12px 80px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
---
|
||||
import PageHeading from "../components/page-heading.astro";
|
||||
import Layout from "../layouts/main.astro";
|
||||
import Link from "../components/Link.astro";
|
||||
---
|
||||
|
||||
<Layout title="Théâtre - Jalil Arfaoui">
|
||||
<section class="relative z-20 max-w-2xl mx-auto my-12 px-7 lg:px-0">
|
||||
<PageHeading
|
||||
title="Théâtre"
|
||||
description="Improvisateur depuis 2008, comédien de théâtre écrit aujourd'hui aussi."
|
||||
/>
|
||||
|
||||
<h2 class="mt-10 mb-4 text-2xl font-bold dark:text-neutral-200">Les Touchatou</h2>
|
||||
<div class="space-y-4 text-gray-600 dark:text-neutral-400 leading-relaxed">
|
||||
<p>
|
||||
Tout a commencé en 2008 avec <Link href="https://improchatou78.wordpress.com/" external>les Touchatou</Link>, une troupe d'improvisation dans les Yvelines. Découverte du match d'impro, des exercices, de cette énergie particulière où tout se construit à partir de rien.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<h2 class="mt-10 mb-4 text-2xl font-bold dark:text-neutral-200">LaTTIFA</h2>
|
||||
<div class="space-y-4 text-gray-600 dark:text-neutral-400 leading-relaxed">
|
||||
<p>
|
||||
Entre 2013 et 2015, au Maroc, j'ai rejoint la <strong class="text-gray-800 dark:text-neutral-200">LaTTIFA</strong> — une troupe franco-arabe d'improvisation théâtrale. Jouer dans deux langues, naviguer entre deux cultures sur scène.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<h2 class="mt-10 mb-4 text-2xl font-bold dark:text-neutral-200">Les Particules</h2>
|
||||
<div class="space-y-4 text-gray-600 dark:text-neutral-400 leading-relaxed">
|
||||
<p>
|
||||
À Albi, j'ai cofondé <Link href="https://les-particules.org/" external>Les Particules</Link>, une troupe d'improvisation avec qui je joue régulièrement. Des spectacles, des matchs, des ateliers.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<h2 class="mt-10 mb-4 text-2xl font-bold dark:text-neutral-200">Lost in Traduction</h2>
|
||||
<div class="space-y-4 text-gray-600 dark:text-neutral-400 leading-relaxed">
|
||||
<p>
|
||||
Acteur dans <Link href="https://www.e-translation-agency.com/fr/lost-in-traduction-fr/lost-in-traduction-episode-1-the-pilot/" external>Lost in Traduction</Link>, une web série produite par <Link href="https://www.e-translation-agency.com/" external>E translation agency</Link> pour démystifier le secteur de la traduction professionnelle.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<h2 class="mt-10 mb-4 text-2xl font-bold dark:text-neutral-200">Théâtre écrit</h2>
|
||||
<div class="space-y-4 text-gray-600 dark:text-neutral-400 leading-relaxed">
|
||||
<p>
|
||||
Après des années d'improvisation, je m'attaque aujourd'hui au théâtre écrit. Nouvelle direction, même élan.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
</Layout>
|
||||
|
|
@ -47,7 +47,6 @@ export const translations = {
|
|||
uses: { fr: 'Utilise', en: 'Uses', ar: 'يستخدم' },
|
||||
},
|
||||
common: {
|
||||
siteName: { fr: 'Jalil Arfaoui', en: 'Jalil Arfaoui', ar: 'جليل عرفاوي' },
|
||||
readMore: { fr: 'Lire la suite', en: 'Read more', ar: 'اقرأ المزيد' },
|
||||
backToHome: { fr: 'Retour à l\'accueil', en: 'Back to home', ar: 'العودة إلى الرئيسية' },
|
||||
publishedOn: { fr: 'Publié le', en: 'Published on', ar: 'نشر في' },
|
||||
|
|
@ -66,23 +65,6 @@ export const translations = {
|
|||
photo: { fr: 'Photographie', en: 'Photography', ar: 'تصوير' },
|
||||
dev: { fr: 'Développement', en: 'Development', ar: 'تطوير' },
|
||||
},
|
||||
photo: {
|
||||
galleryTitle: { fr: 'Galerie Photo', en: 'Photo Gallery', ar: 'معرض الصور' },
|
||||
blogTitle: { fr: 'Blog Photo', en: 'Photo Blog', ar: 'مدونة الصور' },
|
||||
photoFeed: { fr: 'Fil Photo', en: 'Photo Feed', ar: 'سلسلة الصور' },
|
||||
categories: { fr: 'Catégories', en: 'Categories', ar: 'الفئات' },
|
||||
browseByTheme: { fr: 'Parcourir les photos par thème', en: 'Browse photos by theme', ar: 'تصفّح الصور حسب الموضوع' },
|
||||
feedDescription: { fr: 'Parcourir les séries chronologiques, reportages et histoires en images', en: 'Browse chronological series, reports and stories in images', ar: 'تصفّح السلاسل الزمنية والتقارير والقصص المصوّرة' },
|
||||
viewFeed: { fr: 'Voir le fil', en: 'View feed', ar: 'عرض السلسلة' },
|
||||
featured: { fr: 'À la une', en: 'Featured', ar: 'مميّز' },
|
||||
about: { fr: 'À propos', en: 'About', ar: 'نبذة' },
|
||||
contact: { fr: 'Contact', en: 'Contact', ar: 'تواصل' },
|
||||
fullscreen: { fr: 'Plein écran', en: 'Fullscreen', ar: 'شاشة كاملة' },
|
||||
close: { fr: 'Fermer', en: 'Close', ar: 'إغلاق' },
|
||||
previousImage: { fr: 'Image précédente', en: 'Previous image', ar: 'الصورة السابقة' },
|
||||
nextImage: { fr: 'Image suivante', en: 'Next image', ar: 'الصورة التالية' },
|
||||
goToImage: { fr: "Aller à l'image", en: 'Go to image', ar: 'انتقل إلى الصورة' },
|
||||
},
|
||||
pages: {
|
||||
home: {
|
||||
title: { fr: 'Jalil Arfaoui', en: 'Jalil Arfaoui', ar: 'جليل عرفاوي' },
|
||||
|
|
@ -123,54 +105,3 @@ export const translations = {
|
|||
}
|
||||
}
|
||||
};
|
||||
|
||||
/** Helper de traduction typé */
|
||||
export function t(section: keyof typeof translations, key: string, locale: Locale): string {
|
||||
const sectionData = translations[section] as Record<string, LocalizedContent>;
|
||||
return sectionData?.[key]?.[locale] ?? sectionData?.[key]?.fr ?? key;
|
||||
}
|
||||
|
||||
/** Chemin de base photo selon la langue */
|
||||
export function getPhotoBasePath(locale: Locale): string {
|
||||
if (locale === 'ar') return '/ar/تصوير';
|
||||
if (locale === 'en') return '/en/photo';
|
||||
return '/photo';
|
||||
}
|
||||
|
||||
/** Chemin du blog photo selon la langue */
|
||||
export function getPhotoBlogPath(locale: Locale): string {
|
||||
return `${getPhotoBasePath(locale)}/${locale === 'ar' ? 'مدونة' : 'blog'}`;
|
||||
}
|
||||
|
||||
/** Chemin des albums photo selon la langue */
|
||||
export function getPhotoAlbumsPath(locale: Locale): string {
|
||||
return `${getPhotoBasePath(locale)}/${locale === 'ar' ? 'ألبومات' : 'albums'}`;
|
||||
}
|
||||
|
||||
/** Chemin "À propos" selon la langue */
|
||||
export function getAboutPath(locale: Locale): string {
|
||||
if (locale === 'en') return '/en/about';
|
||||
if (locale === 'ar') return '/ar/نبذة-عني';
|
||||
return '/a-propos';
|
||||
}
|
||||
|
||||
/** Locale pour les dates */
|
||||
export function getDateLocale(locale: Locale): string {
|
||||
const dateLocales: Record<Locale, string> = { fr: 'fr-FR', en: 'en-US', ar: 'ar-SA' };
|
||||
return dateLocales[locale];
|
||||
}
|
||||
|
||||
/** ID de catégorie localisé (portraits → portraits.en pour EN) */
|
||||
export function getCategoryEntryId(categoryId: string, locale: Locale): string {
|
||||
return locale === 'fr' ? categoryId : `${categoryId}.${locale}`;
|
||||
}
|
||||
|
||||
/** Chemin d'accueil selon la langue */
|
||||
export function getHomePath(locale: Locale): string {
|
||||
return locale === 'fr' ? '/' : `/${locale}`;
|
||||
}
|
||||
|
||||
/** Slug de base d'un photo blog post depuis son id (ex: "2015/enigma.en.md" → "enigma") */
|
||||
export function getPostBaseSlug(postId: string): string {
|
||||
return postId.replace(/^\d{4}\//, '').replace(/\.(en|ar)?\.mdx?$/, '');
|
||||
}
|
||||