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_URL=https://nas.arfaoui.net:6006
|
||||||
WEBDAV_PATH=/jalil.arfaoui.net/photos
|
WEBDAV_PATH=/photo/Portfolio
|
||||||
WEBDAV_USER=CleverCloud
|
WEBDAV_USER=your_username
|
||||||
WEBDAV_PASS=pwd
|
WEBDAV_PASS=your_password
|
||||||
9
.gitignore
vendored
|
|
@ -28,12 +28,3 @@ src/assets/images/photos/
|
||||||
|
|
||||||
# jetbrains setting folder
|
# jetbrains setting folder
|
||||||
.idea/
|
.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 "dotenv/config";
|
||||||
import { createClient } from "webdav";
|
import { createClient } from "webdav";
|
||||||
import { mkdir, writeFile, stat, readdir, rm } from "fs/promises";
|
import { mkdir, writeFile, stat } from "fs/promises";
|
||||||
import { join, dirname, relative } from "path";
|
import { join, dirname } from "path";
|
||||||
|
|
||||||
interface FileStat {
|
interface FileStat {
|
||||||
filename: string;
|
filename: string;
|
||||||
|
|
@ -30,25 +30,13 @@ async function main() {
|
||||||
|
|
||||||
console.log(`Fetching images from ${WEBDAV_URL}${WEBDAV_PATH}...`);
|
console.log(`Fetching images from ${WEBDAV_URL}${WEBDAV_PATH}...`);
|
||||||
|
|
||||||
// Collecter tous les fichiers/dossiers distants
|
await syncDirectory(client, WEBDAV_PATH, DEST_DIR);
|
||||||
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);
|
|
||||||
|
|
||||||
console.log("Done!");
|
console.log("Done!");
|
||||||
}
|
}
|
||||||
|
|
||||||
async function syncDirectory(
|
async function syncDirectory(client: ReturnType<typeof createClient>, remotePath: string, localPath: string) {
|
||||||
client: ReturnType<typeof createClient>,
|
|
||||||
remotePath: string,
|
|
||||||
localPath: string,
|
|
||||||
remoteItems: Set<string>
|
|
||||||
) {
|
|
||||||
await mkdir(localPath, { recursive: true });
|
await mkdir(localPath, { recursive: true });
|
||||||
remoteItems.add(localPath);
|
|
||||||
|
|
||||||
const items = (await client.getDirectoryContents(remotePath)) as FileStat[];
|
const items = (await client.getDirectoryContents(remotePath)) as FileStat[];
|
||||||
|
|
||||||
|
|
@ -58,9 +46,8 @@ async function syncDirectory(
|
||||||
|
|
||||||
if (item.type === "directory") {
|
if (item.type === "directory") {
|
||||||
console.log(` [dir] ${item.basename}/`);
|
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)) {
|
} else if (item.type === "file" && /\.(jpg|jpeg|png|webp)$/i.test(item.basename)) {
|
||||||
remoteItems.add(localItemPath);
|
|
||||||
const needsDownload = await shouldDownload(localItemPath, item);
|
const needsDownload = await shouldDownload(localItemPath, item);
|
||||||
|
|
||||||
if (needsDownload) {
|
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) => {
|
main().catch((err) => {
|
||||||
console.error("Error:", err.message);
|
console.error("Error:", err.message);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
|
|
|
||||||
|
|
@ -16,22 +16,6 @@ const translations: Record<string, Record<string, string>> = {
|
||||||
en: '/en/about',
|
en: '/en/about',
|
||||||
ar: '/ar/نبذة-عني'
|
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
|
// Page d'accueil
|
||||||
'/': {
|
'/': {
|
||||||
fr: '/',
|
fr: '/',
|
||||||
|
|
|
||||||
|
|
@ -1,28 +1,22 @@
|
||||||
---
|
---
|
||||||
import Logo from "../components/logo.astro";
|
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 pathname = Astro.url.pathname;
|
||||||
const currentLang = pathname.startsWith('/en') ? 'en' : pathname.startsWith('/ar') ? 'ar' : 'fr';
|
const currentLang = pathname.startsWith('/en') ? 'en' : pathname.startsWith('/ar') ? 'ar' : 'fr';
|
||||||
|
|
||||||
// Menu localisé
|
// Menu localisé
|
||||||
const menus = {
|
const menus = {
|
||||||
fr: [
|
fr: [
|
||||||
{ name: 'Code', url: '/code' },
|
|
||||||
{ name: 'Théâtre', url: '/theatre' },
|
|
||||||
{ name: 'Photo', url: '/photo' },
|
{ name: 'Photo', url: '/photo' },
|
||||||
{ name: 'À propos', url: '/a-propos' }
|
{ name: 'À propos', url: '/a-propos' }
|
||||||
],
|
],
|
||||||
en: [
|
en: [
|
||||||
{ name: 'Code', url: '/en/code' },
|
{ name: 'Photo', url: '/photo' },
|
||||||
{ name: 'Acting', url: '/en/acting' },
|
|
||||||
{ name: 'Photo', url: '/en/photo' },
|
|
||||||
{ name: 'About', url: '/en/about' }
|
{ name: 'About', url: '/en/about' }
|
||||||
],
|
],
|
||||||
ar: [
|
ar: [
|
||||||
{ name: 'برمجة', url: '/ar/برمجة' },
|
{ name: 'صور', url: '/photo' },
|
||||||
{ name: 'مسرح', url: '/ar/مسرح' },
|
|
||||||
{ name: 'صور', url: '/ar/تصوير' },
|
|
||||||
{ name: 'نبذة عني', url: '/ar/نبذة-عني' }
|
{ name: 'نبذة عني', url: '/ar/نبذة-عني' }
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -2,14 +2,12 @@
|
||||||
import HomeIcon from "./icons/HomeIcon.astro";
|
import HomeIcon from "./icons/HomeIcon.astro";
|
||||||
|
|
||||||
const pathname = Astro.url.pathname;
|
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 isHome = pathname === '/' || pathname === '/en' || pathname === '/en/' || pathname === '/ar' || pathname === '/ar/';
|
||||||
const homeUrl = currentLang === 'en' ? '/en' : currentLang === 'ar' ? '/ar' : '/';
|
|
||||||
---
|
---
|
||||||
|
|
||||||
{!isHome && (
|
{!isHome && (
|
||||||
<a
|
<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"
|
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"
|
title="Accueil"
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
---
|
---
|
||||||
import { Picture } from 'astro:assets';
|
import { Picture } from 'astro:assets';
|
||||||
import HeroViewport from './HeroViewport.astro';
|
import HeroViewport from './HeroViewport.astro';
|
||||||
import { getDateLocale, type Locale } from '../../utils/i18n';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
title: string;
|
title: string;
|
||||||
|
|
@ -10,10 +9,9 @@ interface Props {
|
||||||
tags?: string[];
|
tags?: string[];
|
||||||
coverImage?: ImageMetadata;
|
coverImage?: ImageMetadata;
|
||||||
scrollTarget?: string;
|
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 && (
|
{coverImage && (
|
||||||
|
|
@ -26,7 +24,7 @@ const { title, description, date, tags, coverImage, scrollTarget = '.info-sectio
|
||||||
{description && <p class="album-description">{description}</p>}
|
{description && <p class="album-description">{description}</p>}
|
||||||
{date && (
|
{date && (
|
||||||
<time class="album-date">
|
<time class="album-date">
|
||||||
{date.toLocaleDateString(getDateLocale(lang), {
|
{date.toLocaleDateString('fr-FR', {
|
||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
month: 'long',
|
month: 'long',
|
||||||
day: 'numeric'
|
day: 'numeric'
|
||||||
|
|
|
||||||
|
|
@ -4,19 +4,11 @@ import HeroViewport from './HeroViewport.astro';
|
||||||
import Lightbox from './Lightbox.astro';
|
import Lightbox from './Lightbox.astro';
|
||||||
import { Picture } from 'astro:assets';
|
import { Picture } from 'astro:assets';
|
||||||
import { getEntry } from 'astro:content';
|
import { getEntry } from 'astro:content';
|
||||||
import { getCategoryEntryId, type Locale } from '../../utils/i18n';
|
|
||||||
|
|
||||||
interface Props {
|
const { category } = Astro.props;
|
||||||
category: string;
|
|
||||||
lang?: Locale;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { category, lang = 'fr' } = Astro.props;
|
// Récupérer les métadonnées de la catégorie
|
||||||
|
const categoryData = await getEntry('photoCategories', category);
|
||||||
// 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);
|
|
||||||
|
|
||||||
// Auto-détection des images du dossier de la catégorie
|
// 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}');
|
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">
|
<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 -->
|
<!-- Image hero avec titre en overlay -->
|
||||||
<HeroViewport targetSelector="#thumbnails">
|
<HeroViewport targetSelector="#thumbnails">
|
||||||
|
|
@ -97,7 +89,7 @@ const lightboxImages = images.map(img => ({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Lightbox images={lightboxImages} showCategory={true} category={category} lang={lang} />
|
<Lightbox images={lightboxImages} showCategory={true} category={category} />
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
let lastScrollY = window.scrollY;
|
let lastScrollY = window.scrollY;
|
||||||
|
|
|
||||||
|
|
@ -1,42 +1,27 @@
|
||||||
---
|
---
|
||||||
import { getCollection } from 'astro:content';
|
import { getCollection } from 'astro:content';
|
||||||
import HomeIcon from '../icons/HomeIcon.astro';
|
import HomeIcon from '../icons/HomeIcon.astro';
|
||||||
import { t, getPhotoBasePath, getPhotoBlogPath, getPhotoAlbumsPath, getHomePath, type Locale } from '../../utils/i18n';
|
|
||||||
|
|
||||||
interface Props {
|
const { currentCategory = '', opaque = false } = Astro.props;
|
||||||
currentCategory?: string;
|
|
||||||
opaque?: boolean;
|
|
||||||
lang?: Locale;
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
// Catégories photos uniquement
|
||||||
const homePath = getHomePath(lang);
|
const categories = sortedCategories.map(cat => ({ id: cat.id, title: cat.data.title }));
|
||||||
|
|
||||||
// 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,
|
|
||||||
}));
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<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`}>
|
<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">
|
<div class="nav-container">
|
||||||
<!-- Titre du site à gauche -->
|
<!-- Titre du site à gauche -->
|
||||||
<div class="site-title">
|
<div class="site-title">
|
||||||
<a href={homePath} class="site-link">
|
<a href="/" class="site-link">
|
||||||
<HomeIcon size={16} />
|
<HomeIcon size={16} />
|
||||||
<span class="site-name">{t('common', 'siteName', lang)}</span>
|
<span class="site-name">Jalil Arfaoui</span>
|
||||||
</a>
|
</a>
|
||||||
<span class="nav-separator"></span>
|
<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>
|
</div>
|
||||||
|
|
||||||
<!-- Bouton hamburger (mobile) -->
|
<!-- Bouton hamburger (mobile) -->
|
||||||
|
|
@ -46,13 +31,13 @@ const categories = sortedCategories.map(cat => ({
|
||||||
<span class="hamburger-line"></span>
|
<span class="hamburger-line"></span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- Navigation -->
|
<!-- Navigation -->
|
||||||
<div class="nav-menu" id="navMenu">
|
<div class="nav-menu" id="navMenu">
|
||||||
<a
|
<a
|
||||||
href={getPhotoBlogPath(lang)}
|
href="/photo/blog"
|
||||||
class={`nav-link blog-link ${currentCategory === 'blog' ? 'active' : ''}`}
|
class={`nav-link blog-link ${currentCategory === 'blog' ? 'active' : ''}`}
|
||||||
>
|
>
|
||||||
{t('photo', 'photoFeed', lang)}
|
Fil Photo
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<!-- Séparateur -->
|
<!-- Séparateur -->
|
||||||
|
|
@ -63,7 +48,7 @@ const categories = sortedCategories.map(cat => ({
|
||||||
{categories.map(cat => (
|
{categories.map(cat => (
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a
|
||||||
href={`${getPhotoAlbumsPath(lang)}/${cat.id}`}
|
href={`/photo/albums/${cat.id}`}
|
||||||
class={`nav-link ${currentCategory === cat.id ? 'active' : ''}`}
|
class={`nav-link ${currentCategory === cat.id ? 'active' : ''}`}
|
||||||
data-category={cat.id}
|
data-category={cat.id}
|
||||||
>
|
>
|
||||||
|
|
@ -278,4 +263,4 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -1,29 +1,19 @@
|
||||||
---
|
---
|
||||||
import { getCollection } from 'astro:content';
|
import { getCollection } from 'astro:content';
|
||||||
import { t, getPhotoBlogPath, getPhotoAlbumsPath, type Locale } from '../../utils/i18n';
|
|
||||||
|
|
||||||
interface Props {
|
const photoCategories = await getCollection('photoCategories');
|
||||||
lang?: Locale;
|
const sortedCategories = photoCategories.sort((a, b) => (a.data.order || 99) - (b.data.order || 99));
|
||||||
}
|
|
||||||
|
|
||||||
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));
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<section id="explore-section" class="explore-section">
|
<section id="explore-section" class="explore-section">
|
||||||
<div class="explore-content">
|
<div class="explore-content">
|
||||||
<div class="explore-card">
|
<div class="explore-card">
|
||||||
<h2 class="explore-card-title">{t('photo', 'categories', lang)}</h2>
|
<h2 class="explore-card-title">Catégories</h2>
|
||||||
<p class="explore-card-desc">{t('photo', 'browseByTheme', lang)}</p>
|
<p class="explore-card-desc">Parcourir les photos par thème</p>
|
||||||
<ul class="category-list">
|
<ul class="category-list">
|
||||||
{sortedCategories.map(cat => (
|
{sortedCategories.map(cat => (
|
||||||
<li>
|
<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}
|
{cat.data.title}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
@ -32,10 +22,10 @@ const sortedCategories = effectiveCategories.sort((a, b) => (a.data.order || 99)
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="explore-card">
|
<div class="explore-card">
|
||||||
<h2 class="explore-card-title">{t('photo', 'photoFeed', lang)}</h2>
|
<h2 class="explore-card-title">Fil Photo</h2>
|
||||||
<p class="explore-card-desc">{t('photo', 'feedDescription', lang)}</p>
|
<p class="explore-card-desc">Parcourir les séries chronologiques, reportages et histoires en images</p>
|
||||||
<a href={getPhotoBlogPath(lang)} class="explore-cta">
|
<a href="/photo/blog" class="explore-cta">
|
||||||
{t('photo', 'viewFeed', lang)}
|
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">
|
<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"/>
|
<line x1="5" y1="12" x2="19" y2="12"/>
|
||||||
<polyline points="12 5 19 12 12 19"/>
|
<polyline points="12 5 19 12 12 19"/>
|
||||||
|
|
|
||||||
|
|
@ -1,49 +1,45 @@
|
||||||
---
|
---
|
||||||
import { getCollection } from 'astro:content';
|
import { getCollection } from 'astro:content';
|
||||||
import { t, type Locale } from '../../utils/i18n';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
images: { src: string; alt: string; title?: string }[];
|
images: { src: string; alt: string; title?: string }[];
|
||||||
albumTitle?: string;
|
albumTitle?: string;
|
||||||
showCategory?: boolean;
|
showCategory?: boolean;
|
||||||
category?: string;
|
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);
|
const imagesForJS = JSON.stringify(images);
|
||||||
|
|
||||||
// Construire les labels depuis la collection filtrée par langue
|
// Construire les labels depuis la collection
|
||||||
const allCategories = await getCollection('photoCategories');
|
const photoCategories = 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 categoryLabels: Record<string, string> = {
|
const categoryLabels: Record<string, string> = {
|
||||||
'blog': t('photo', 'photoFeed', lang),
|
'blog': 'Fil Photo',
|
||||||
...Object.fromEntries(effectiveCategories.map(cat => [cat.id.replace(/\.(en|ar)$/, ''), cat.data.title]))
|
...Object.fromEntries(photoCategories.map(cat => [cat.id, cat.data.title]))
|
||||||
};
|
};
|
||||||
---
|
---
|
||||||
|
|
||||||
<div id="lightbox" class="lightbox hidden">
|
<div id="lightbox" class="lightbox hidden">
|
||||||
<div class="lightbox-controls">
|
<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">
|
<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"/>
|
<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>
|
</svg>
|
||||||
</button>
|
</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">
|
<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"/>
|
<path d="M18 6L6 18M6 6l12 12" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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">
|
<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"/>
|
<path d="M15 18L9 12L15 6" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</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">
|
<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"/>
|
<path d="M9 18L15 12L9 6" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
</svg>
|
</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">
|
<footer class="photo-footer">
|
||||||
<div class="photo-footer-inner">
|
<div class="photo-footer-inner">
|
||||||
<div class="photo-footer-links">
|
<div class="photo-footer-links">
|
||||||
<a href={getAboutPath(lang)}>{t('photo', 'about', lang)}</a>
|
<a href="/a-propos">À propos</a>
|
||||||
<a href="mailto:jalil@arfaoui.net">{t('photo', 'contact', lang)}</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>
|
<a href="https://instagram.com/l.i.l.a.j" target="_blank" rel="noopener noreferrer">Instagram</a>
|
||||||
</div>
|
</div>
|
||||||
<PhotoLanguageSwitcher lang={lang} />
|
|
||||||
<div class="photo-footer-copy">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
@ -48,12 +36,6 @@ const { lang = 'fr' } = Astro.props;
|
||||||
.photo-footer-links {
|
.photo-footer-links {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 15px;
|
gap: 15px;
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.photo-footer-copy {
|
|
||||||
flex: 1;
|
|
||||||
text-align: end;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.photo-footer-links a,
|
.photo-footer-links a,
|
||||||
|
|
|
||||||
|
|
@ -3,14 +3,6 @@ import CategoryNav from './CategoryNav.astro';
|
||||||
import Slideshow from './Slideshow.astro';
|
import Slideshow from './Slideshow.astro';
|
||||||
import SlideControls from './SlideControls.astro';
|
import SlideControls from './SlideControls.astro';
|
||||||
import favorites from '../../data/favorites.json';
|
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
|
// Auto-détection des images
|
||||||
const allImages = import.meta.glob<{ default: ImageMetadata }>('/src/assets/images/photos/categories/**/*.{jpg,jpeg,png,webp}');
|
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">
|
<div id="photo-gallery" class="gallery-container">
|
||||||
<CategoryNav currentCategory="" lang={lang} />
|
<CategoryNav currentCategory="" />
|
||||||
<Slideshow images={images} lang={lang} />
|
<Slideshow images={images} />
|
||||||
<SlideControls lang={lang} />
|
<SlideControls />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script is:inline define:vars={{ imagesForJS }}>
|
<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">
|
<div class="slide-controls">
|
||||||
<button
|
<button
|
||||||
id="prev-btn"
|
id="prev-btn"
|
||||||
class="control-btn 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">
|
<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"/>
|
<path d="M15 18l-6-6 6-6"/>
|
||||||
|
|
@ -22,7 +15,7 @@ const { lang = 'fr' } = Astro.props;
|
||||||
<button
|
<button
|
||||||
id="next-btn"
|
id="next-btn"
|
||||||
class="control-btn 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">
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
<path d="M9 18l6-6-6-6"/>
|
<path d="M9 18l6-6-6-6"/>
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,11 @@
|
||||||
---
|
---
|
||||||
import { Picture } from 'astro:assets';
|
import { Picture } from 'astro:assets';
|
||||||
import { t, type Locale } from '../../utils/i18n';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
images: { src: ImageMetadata; alt: string }[];
|
images: { src: ImageMetadata; alt: string }[];
|
||||||
lang?: Locale;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const { images = [], lang = 'fr' } = Astro.props;
|
const { images = [] } = Astro.props;
|
||||||
---
|
---
|
||||||
|
|
||||||
<div id="slideshow-container" class="slideshow-wrapper">
|
<div id="slideshow-container" class="slideshow-wrapper">
|
||||||
|
|
@ -35,7 +33,7 @@ const { images = [], lang = 'fr' } = Astro.props;
|
||||||
<button
|
<button
|
||||||
class={`indicator ${index === 0 ? 'active' : ''}`}
|
class={`indicator ${index === 0 ? 'active' : ''}`}
|
||||||
data-slide={index}
|
data-slide={index}
|
||||||
aria-label={`${t('photo', 'goToImage', lang)} ${index + 1}`}
|
aria-label={`Aller à l'image ${index + 1}`}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</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(),
|
title: z.string(),
|
||||||
subtitle: z.string(),
|
subtitle: z.string(),
|
||||||
order: z.number().optional(),
|
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 PhotoFooter from '../components/photo/PhotoFooter.astro';
|
||||||
import type { Locale } from '../utils/i18n';
|
|
||||||
|
|
||||||
interface Props {
|
const { title = "Galerie Photo - Jalil Arfaoui", enableScroll = false } = Astro.props;
|
||||||
title?: string;
|
|
||||||
enableScroll?: boolean;
|
|
||||||
lang?: Locale;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { title = "Galerie Photo - Jalil Arfaoui", enableScroll = false, lang = 'fr' } = Astro.props;
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang={lang} dir={lang === 'ar' ? 'rtl' : 'ltr'}>
|
<html lang="fr">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<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;">
|
<body class={`antialiased bg-black text-white ${enableScroll ? '' : 'overflow-hidden'}`} style="font-family: 'Karla', 'Helvetica Neue', Helvetica, Arial, sans-serif;">
|
||||||
<slot />
|
<slot />
|
||||||
|
|
||||||
<PhotoFooter lang={lang} />
|
<PhotoFooter />
|
||||||
|
|
||||||
<Fragment set:html={import.meta.env.FOOTER_INJECT} />
|
<Fragment set:html={import.meta.env.FOOTER_INJECT} />
|
||||||
</body>
|
</body>
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,7 @@
|
||||||
---
|
---
|
||||||
import { Image } from "astro:assets";
|
|
||||||
import PageHeading from "../components/page-heading.astro";
|
import PageHeading from "../components/page-heading.astro";
|
||||||
import Layout from "../layouts/main.astro";
|
import Layout from "../layouts/main.astro";
|
||||||
import Link from "../components/Link.astro";
|
import Link from "../components/Link.astro";
|
||||||
import jalilPhoto from "../assets/images/jalil.jpg";
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout title="À propos - Jalil Arfaoui">
|
<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."
|
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>
|
<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">
|
<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 Layout from "../../layouts/main.astro";
|
||||||
import jalilPhoto from "../../assets/images/jalil-2.jpg";
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout title="جليل عرفاوي - حِرَفي برمجة • ممثل ارتجالي • مصوّر">
|
<Layout title="جليل عرفاوي - حِرَفي برمجة • ممثل ارتجالي • مصوّر">
|
||||||
|
|
@ -20,9 +18,10 @@ import jalilPhoto from "../../assets/images/jalil-2.jpg";
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="flex flex-wrap gap-4">
|
<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 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>
|
</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-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="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">
|
<div class="bg-white dark:bg-neutral-950 rounded-3xl p-4">
|
||||||
<Image
|
<img
|
||||||
src={jalilPhoto}
|
src="/assets/images/jalil-2.jpg"
|
||||||
alt="جليل عرفاوي"
|
alt="جليل عرفاوي"
|
||||||
loading="eager"
|
loading="eager"
|
||||||
decoding="auto"
|
decoding="auto"
|
||||||
|
|
@ -60,9 +59,15 @@ import jalilPhoto from "../../assets/images/jalil-2.jpg";
|
||||||
أكثر من 20 سنة في بناء البرمجيات. اليوم، أختار مشاريعي: برمجيات حرّة، أدوات مفيدة، لا شيء يُقيّد. Craftsmanship، DDD، TypeScript — وهاجس التحيّزات التي نضعها في الكود دون أن ندري. أُدرّس البرمجة وأنشّط مجتمع Software Crafters في ألبي.
|
أكثر من 20 سنة في بناء البرمجيات. اليوم، أختار مشاريعي: برمجيات حرّة، أدوات مفيدة، لا شيء يُقيّد. Craftsmanship، DDD، TypeScript — وهاجس التحيّزات التي نضعها في الكود دون أن ندري. أُدرّس البرمجة وأنشّط مجتمع Software Crafters في ألبي.
|
||||||
</p>
|
</p>
|
||||||
<div class="space-y-3">
|
<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">
|
<span class="block text-blue-400/50 dark:text-blue-600/50 cursor-not-allowed" title="قيد الإنشاء">
|
||||||
← اكتشف المزيد
|
← المسار المهني 🚧
|
||||||
</a>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -77,9 +82,9 @@ import jalilPhoto from "../../assets/images/jalil-2.jpg";
|
||||||
ممثل ارتجالي منذ 2008، من ضواحي باريس إلى المغرب قبل تأسيس فرقة Les Particules في ألبي. اليوم أخوض أيضًا غمار المسرح المكتوب.
|
ممثل ارتجالي منذ 2008، من ضواحي باريس إلى المغرب قبل تأسيس فرقة Les Particules في ألبي. اليوم أخوض أيضًا غمار المسرح المكتوب.
|
||||||
</p>
|
</p>
|
||||||
<div class="space-y-3">
|
<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">
|
<span class="block text-orange-400/50 dark:text-orange-600/50 cursor-not-allowed" title="قيد الإنشاء">
|
||||||
← المسار الفني
|
← المسار الفني 🚧
|
||||||
</a>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -94,10 +99,10 @@ import jalilPhoto from "../../assets/images/jalil-2.jpg";
|
||||||
أصوّر كل شيء تقريبًا: وجوه، حفلات، محرّكات، الحياة اليومية. لا تخصّص، مجرّد فضول.
|
أصوّر كل شيء تقريبًا: وجوه، حفلات، محرّكات، الحياة اليومية. لا تخصّص، مجرّد فضول.
|
||||||
</p>
|
</p>
|
||||||
<div class="space-y-3">
|
<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>
|
||||||
<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>
|
</a>
|
||||||
</div>
|
</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 PageHeading from "../../components/page-heading.astro";
|
||||||
import Layout from "../../layouts/main.astro";
|
import Layout from "../../layouts/main.astro";
|
||||||
import Link from "../../components/Link.astro";
|
import Link from "../../components/Link.astro";
|
||||||
import jalilPhoto from "../../assets/images/jalil.jpg";
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout title="نبذة عني - جليل عرفاوي">
|
<Layout title="نبذة عني - جليل عرفاوي">
|
||||||
|
|
@ -13,7 +11,7 @@ import jalilPhoto from "../../assets/images/jalil.jpg";
|
||||||
description="حِرَفي برمجة، ممثل ارتجالي، مصوّر فضولي."
|
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>
|
<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">
|
<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 PageHeading from "../../components/page-heading.astro";
|
||||||
import Layout from "../../layouts/main.astro";
|
import Layout from "../../layouts/main.astro";
|
||||||
import Link from "../../components/Link.astro";
|
import Link from "../../components/Link.astro";
|
||||||
import jalilPhoto from "../../assets/images/jalil.jpg";
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout title="About - Jalil Arfaoui">
|
<Layout title="About - Jalil Arfaoui">
|
||||||
|
|
@ -13,7 +11,7 @@ import jalilPhoto from "../../assets/images/jalil.jpg";
|
||||||
description="Software craftsman, improv actor, curious photographer."
|
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>
|
<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">
|
<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 Layout from "../../layouts/main.astro";
|
||||||
import jalilPhoto from "../../assets/images/jalil-2.jpg";
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout title="Jalil Arfaoui - Software Craftsman • Improv Actor • Photographer">
|
<Layout title="Jalil Arfaoui - Software Craftsman • Improv Actor • Photographer">
|
||||||
|
|
@ -20,9 +18,10 @@ import jalilPhoto from "../../assets/images/jalil-2.jpg";
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="flex flex-wrap gap-4">
|
<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
|
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">
|
<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
|
Learn more
|
||||||
</a>
|
</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-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="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">
|
<div class="bg-white dark:bg-neutral-950 rounded-3xl p-4">
|
||||||
<Image
|
<img
|
||||||
src={jalilPhoto}
|
src="/assets/images/jalil-2.jpg"
|
||||||
alt="Jalil Arfaoui"
|
alt="Jalil Arfaoui"
|
||||||
loading="eager"
|
loading="eager"
|
||||||
decoding="auto"
|
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.
|
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>
|
</p>
|
||||||
<div class="space-y-3">
|
<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">
|
<span class="block text-blue-400/50 dark:text-blue-600/50 cursor-not-allowed" title="Under construction">
|
||||||
→ Learn more
|
→ Professional journey 🚧
|
||||||
</a>
|
</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>
|
</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.
|
Improv actor since 2008, from the Paris suburbs to Morocco before co-founding Les Particules in Albi. Now also taking on scripted theater.
|
||||||
</p>
|
</p>
|
||||||
<div class="space-y-3">
|
<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">
|
<span class="block text-orange-400/50 dark:text-orange-600/50 cursor-not-allowed" title="Under construction">
|
||||||
→ Artistic journey
|
→ Artistic journey 🚧
|
||||||
</a>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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.
|
I photograph a bit of everything: faces, concerts, engines, everyday life. No specialty, just curiosity.
|
||||||
</p>
|
</p>
|
||||||
<div class="space-y-3">
|
<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
|
→ Photo portfolio
|
||||||
</a>
|
</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
|
→ Photo Feed
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</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 Layout from "../layouts/main.astro";
|
||||||
import jalilPhoto from "../assets/images/jalil-2.jpg";
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout title="Jalil Arfaoui - Développeur artisan • Comédien improvisateur • Photographe">
|
<Layout title="Jalil Arfaoui - Développeur artisan • Comédien improvisateur • Photographe">
|
||||||
|
|
@ -20,9 +18,10 @@ import jalilPhoto from "../assets/images/jalil-2.jpg";
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="flex flex-wrap gap-4">
|
<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
|
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">
|
<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
|
En savoir plus
|
||||||
</a>
|
</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-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="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">
|
<div class="bg-white dark:bg-neutral-950 rounded-3xl p-4">
|
||||||
<Image
|
<img
|
||||||
src={jalilPhoto}
|
src="/assets/images/jalil-2.jpg"
|
||||||
alt="Jalil Arfaoui"
|
alt="Jalil Arfaoui"
|
||||||
loading="eager"
|
loading="eager"
|
||||||
decoding="auto"
|
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.
|
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>
|
</p>
|
||||||
<div class="space-y-3">
|
<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">
|
<span class="block text-blue-400/50 dark:text-blue-600/50 cursor-not-allowed" title="En construction">
|
||||||
→ En savoir plus
|
→ Parcours professionnel 🚧
|
||||||
</a>
|
</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>
|
</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.
|
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>
|
</p>
|
||||||
<div class="space-y-3">
|
<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">
|
<span class="block text-orange-400/50 dark:text-orange-600/50 cursor-not-allowed" title="En construction">
|
||||||
→ Parcours artistique
|
→ Parcours artistique 🚧
|
||||||
</a>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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 { 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() {
|
export async function getStaticPaths() {
|
||||||
const allPhotoBlogPosts = await getCollection('photoBlogPosts');
|
const allPhotoBlogPosts = await getCollection('photoBlogPosts');
|
||||||
// Uniquement les posts FR (lang absent ou 'fr')
|
return allPhotoBlogPosts.map(post => {
|
||||||
const frPosts = allPhotoBlogPosts.filter(post => (post.data.lang ?? 'fr') === 'fr');
|
// Le slug Astro inclut le préfixe d'année (ex: "2015/enigma.en")
|
||||||
return frPosts.map(post => {
|
const slug = post.slug.replace(/^\d{4}\//, '');
|
||||||
const slug = getPostBaseSlug(post.id);
|
|
||||||
return {
|
return {
|
||||||
params: {
|
params: {
|
||||||
year: String(post.data.date.getFullYear()),
|
year: String(post.data.date.getFullYear()),
|
||||||
|
|
@ -20,6 +25,104 @@ export async function getStaticPaths() {
|
||||||
}
|
}
|
||||||
|
|
||||||
const { post } = Astro.props;
|
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: 'يستخدم' },
|
uses: { fr: 'Utilise', en: 'Uses', ar: 'يستخدم' },
|
||||||
},
|
},
|
||||||
common: {
|
common: {
|
||||||
siteName: { fr: 'Jalil Arfaoui', en: 'Jalil Arfaoui', ar: 'جليل عرفاوي' },
|
|
||||||
readMore: { fr: 'Lire la suite', en: 'Read more', ar: 'اقرأ المزيد' },
|
readMore: { fr: 'Lire la suite', en: 'Read more', ar: 'اقرأ المزيد' },
|
||||||
backToHome: { fr: 'Retour à l\'accueil', en: 'Back to home', ar: 'العودة إلى الرئيسية' },
|
backToHome: { fr: 'Retour à l\'accueil', en: 'Back to home', ar: 'العودة إلى الرئيسية' },
|
||||||
publishedOn: { fr: 'Publié le', en: 'Published on', ar: 'نشر في' },
|
publishedOn: { fr: 'Publié le', en: 'Published on', ar: 'نشر في' },
|
||||||
|
|
@ -66,23 +65,6 @@ export const translations = {
|
||||||
photo: { fr: 'Photographie', en: 'Photography', ar: 'تصوير' },
|
photo: { fr: 'Photographie', en: 'Photography', ar: 'تصوير' },
|
||||||
dev: { fr: 'Développement', en: 'Development', 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: {
|
pages: {
|
||||||
home: {
|
home: {
|
||||||
title: { fr: 'Jalil Arfaoui', en: 'Jalil Arfaoui', ar: 'جليل عرفاوي' },
|
title: { fr: 'Jalil Arfaoui', en: 'Jalil Arfaoui', ar: 'جليل عرفاوي' },
|
||||||
|
|
@ -122,55 +104,4 @@ 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?$/, '');
|
|
||||||
}
|
|
||||||