refonte: portfolio statique Astro (design moderne développeur)
Some checks failed
Deploy production / build-deploy (push) Failing after 56s
Some checks failed
Deploy production / build-deploy (push) Failing after 56s
- Remplace l'ancien Astro hybrid + admin par un site one-page statique (sortie static) - Thème clair/sombre sans flash, contenu typé (src/data/content.ts), zéro admin - Icônes simple-icons (SVG monochrome), polices auto-hébergées (RGPD) - SEO complet : canonical, Open Graph, Twitter Card, JSON-LD Person, robots.txt, sitemap.xml, image OG 1200x630 - CI Gitea de déploiement FTPS vers Plesk (.gitea/workflows/prod.yml)
This commit is contained in:
@@ -1,59 +0,0 @@
|
||||
import { SignJWT, jwtVerify } from 'jose';
|
||||
import bcrypt from 'bcryptjs';
|
||||
|
||||
// Astro dev (Vite) expose les .env via import.meta.env ; Node runtime prod via process.env.
|
||||
// On lit les deux pour couvrir tous les cas (dev, preview, prod Plesk Node).
|
||||
const env = {
|
||||
...(typeof process !== 'undefined' ? process.env : {}),
|
||||
...(import.meta.env as Record<string, string | undefined>),
|
||||
};
|
||||
|
||||
const ADMIN_PASSWORD_HASH = (env.ADMIN_PASSWORD_HASH as string | undefined) ?? '';
|
||||
const JWT_SECRET = (env.ADMIN_JWT_SECRET as string | undefined) ?? '';
|
||||
|
||||
// Lazy : on ne throw pas au chargement du module (Astro build importe ce fichier
|
||||
// en SSR sans forcément avoir le .env). On throw au premier usage runtime.
|
||||
let cachedKey: Uint8Array | null = null;
|
||||
function getSecretKey(): Uint8Array {
|
||||
if (cachedKey) return cachedKey;
|
||||
if (!JWT_SECRET || JWT_SECRET.length < 32) {
|
||||
throw new Error(
|
||||
'[auth] ADMIN_JWT_SECRET manquant ou trop court (>= 32 chars). ' +
|
||||
'Générer via `npm run hash:password -- <motdepasse>` et reporter dans .env.',
|
||||
);
|
||||
}
|
||||
cachedKey = new TextEncoder().encode(JWT_SECRET);
|
||||
return cachedKey;
|
||||
}
|
||||
|
||||
export const ADMIN_COOKIE = 'portfolio_admin';
|
||||
|
||||
export async function verifyPassword(password: string): Promise<boolean> {
|
||||
if (!ADMIN_PASSWORD_HASH) {
|
||||
console.error('[auth] ADMIN_PASSWORD_HASH manquant dans .env');
|
||||
return false;
|
||||
}
|
||||
return bcrypt.compare(password, ADMIN_PASSWORD_HASH);
|
||||
}
|
||||
|
||||
export async function createSession(): Promise<string> {
|
||||
return new SignJWT({ role: 'admin' })
|
||||
.setProtectedHeader({ alg: 'HS256' })
|
||||
.setIssuedAt()
|
||||
.setExpirationTime('7d')
|
||||
.sign(getSecretKey());
|
||||
}
|
||||
|
||||
export async function verifySession(token: string | undefined): Promise<boolean> {
|
||||
if (!token) return false;
|
||||
try {
|
||||
await jwtVerify(token, getSecretKey());
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function hashPasswordCLI(password: string): string {
|
||||
return bcrypt.hashSync(password, 12);
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
import { readFile, writeFile, mkdir, copyFile, access } from 'node:fs/promises';
|
||||
import { join } from 'node:path';
|
||||
import { CONTENT_SCHEMAS, type ContentKey } from '../content/schemas';
|
||||
import type { z } from 'zod';
|
||||
|
||||
const DATA_CONTENT_DIR = join(process.cwd(), 'data', 'content');
|
||||
const SEED_CONTENT_DIR = join(process.cwd(), 'public', 'content');
|
||||
|
||||
type ContentData<K extends ContentKey> = z.infer<(typeof CONTENT_SCHEMAS)[K]>;
|
||||
|
||||
async function ensureContentFile(key: ContentKey): Promise<string> {
|
||||
const target = join(DATA_CONTENT_DIR, `${key}.json`);
|
||||
try {
|
||||
await access(target);
|
||||
return target;
|
||||
} catch {
|
||||
await mkdir(DATA_CONTENT_DIR, { recursive: true });
|
||||
const seed = join(SEED_CONTENT_DIR, `${key}.json`);
|
||||
await copyFile(seed, target);
|
||||
return target;
|
||||
}
|
||||
}
|
||||
|
||||
export async function loadContent<K extends ContentKey>(key: K): Promise<ContentData<K>> {
|
||||
const filePath = await ensureContentFile(key);
|
||||
const raw = await readFile(filePath, 'utf-8');
|
||||
const parsed = JSON.parse(raw);
|
||||
const schema = CONTENT_SCHEMAS[key];
|
||||
return schema.parse(parsed) as ContentData<K>;
|
||||
}
|
||||
|
||||
export async function saveContent<K extends ContentKey>(
|
||||
key: K,
|
||||
data: unknown,
|
||||
): Promise<ContentData<K>> {
|
||||
const schema = CONTENT_SCHEMAS[key];
|
||||
const validated = schema.parse(data) as ContentData<K>;
|
||||
const filePath = await ensureContentFile(key);
|
||||
await writeFile(filePath, JSON.stringify(validated, null, 2) + '\n', 'utf-8');
|
||||
return validated;
|
||||
}
|
||||
|
||||
export async function loadAll() {
|
||||
const [profile, skills, experiences, formations, projects, interests, site] = await Promise.all([
|
||||
loadContent('profile'),
|
||||
loadContent('skills'),
|
||||
loadContent('experiences'),
|
||||
loadContent('formations'),
|
||||
loadContent('projects'),
|
||||
loadContent('interests'),
|
||||
loadContent('site'),
|
||||
]);
|
||||
return { profile, skills, experiences, formations, projects, interests, site };
|
||||
}
|
||||
82
src/lib/techIcons.ts
Normal file
82
src/lib/techIcons.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
/* ============================================================
|
||||
Icônes des technos via le paquet `simple-icons` (chemins SVG
|
||||
officiels). Rendu monochrome (currentColor) par <TechIcon/>,
|
||||
donc adaptatif au thème clair/sombre. Les technos absentes de
|
||||
simple-icons (Java, C#) utilisent un monogramme.
|
||||
============================================================ */
|
||||
import {
|
||||
siTypescript, siJavascript, siPython, siPhp, siNestjs, siNuxtdotjs, siNextdotjs,
|
||||
siVuedotjs, siAngular, siTailwindcss, siAstro, siMariadb, siMysql, siPostgresql,
|
||||
siRedis, siDocker, siGit, siAmazonwebservices, siTerraform, siNginx, siPlesk,
|
||||
siWebstorm, siPhpstorm, siPycharm, siClaude, siDebian, siUbuntu, siApple,
|
||||
siCplusplus, siDart, siKotlin, siSpring, siSymfony, siExpress, siFastapi,
|
||||
siFlutter, siGraphql, siVuetify, siBootstrap, siMongodb, siSqlite, siFirebase,
|
||||
siSwagger, siPostman, siStripe,
|
||||
} from "simple-icons";
|
||||
|
||||
export type TechDef = { path: string } | { mono: string };
|
||||
|
||||
export const techIcons: Record<string, TechDef> = {
|
||||
// Langages
|
||||
"TypeScript": { path: siTypescript.path },
|
||||
"JavaScript": { path: siJavascript.path },
|
||||
"Java": { mono: "Ja" }, // absent de simple-icons
|
||||
"Python": { path: siPython.path },
|
||||
"PHP": { path: siPhp.path },
|
||||
// Frameworks
|
||||
"NestJS": { path: siNestjs.path },
|
||||
"Nuxt": { path: siNuxtdotjs.path },
|
||||
"Next.js": { path: siNextdotjs.path },
|
||||
"Vue.js": { path: siVuedotjs.path },
|
||||
"Angular": { path: siAngular.path },
|
||||
"Tailwind CSS": { path: siTailwindcss.path },
|
||||
"Astro": { path: siAstro.path },
|
||||
// Bases de données
|
||||
"MariaDB": { path: siMariadb.path },
|
||||
"MySQL": { path: siMysql.path },
|
||||
"PostgreSQL": { path: siPostgresql.path },
|
||||
"Redis": { path: siRedis.path },
|
||||
"MongoDB": { path: siMongodb.path },
|
||||
"SQLite": { path: siSqlite.path },
|
||||
// Outils & DevOps
|
||||
"Docker": { path: siDocker.path },
|
||||
"Git": { path: siGit.path },
|
||||
"AWS": { path: siAmazonwebservices.path },
|
||||
"Terraform": { path: siTerraform.path },
|
||||
"NGINX": { path: siNginx.path },
|
||||
"Plesk": { path: siPlesk.path },
|
||||
// IDE & Éditeurs
|
||||
"WebStorm": { path: siWebstorm.path },
|
||||
"PhpStorm": { path: siPhpstorm.path },
|
||||
"PyCharm": { path: siPycharm.path },
|
||||
"Claude Code": { path: siClaude.path },
|
||||
// Systèmes
|
||||
"Debian": { path: siDebian.path },
|
||||
"Ubuntu": { path: siUbuntu.path },
|
||||
"macOS": { path: siApple.path },
|
||||
// Secondaire — langages
|
||||
"C#": { mono: "C#" }, // absent de simple-icons
|
||||
"C++": { path: siCplusplus.path },
|
||||
"Dart": { path: siDart.path },
|
||||
"Kotlin": { path: siKotlin.path },
|
||||
// Secondaire — frameworks & libs
|
||||
"Spring": { path: siSpring.path },
|
||||
"Symfony": { path: siSymfony.path },
|
||||
"Express.js": { path: siExpress.path },
|
||||
"FastAPI": { path: siFastapi.path },
|
||||
"Flutter": { path: siFlutter.path },
|
||||
"GraphQL": { path: siGraphql.path },
|
||||
"Vuetify": { path: siVuetify.path },
|
||||
"Bootstrap": { path: siBootstrap.path },
|
||||
// Secondaire — services & outils
|
||||
"Firebase": { path: siFirebase.path },
|
||||
"Swagger": { path: siSwagger.path },
|
||||
"Postman": { path: siPostman.path },
|
||||
"Stripe": { path: siStripe.path },
|
||||
};
|
||||
|
||||
/** Monogramme de repli pour une techno sans icône (2 premières lettres). */
|
||||
export function monogram(name: string): string {
|
||||
const clean = name.replace(/[^A-Za-z0-9]/g, "");
|
||||
return (clean.charAt(0).toUpperCase() + (clean.charAt(1) || "").toLowerCase()) || "?";
|
||||
}
|
||||
Reference in New Issue
Block a user