first commit
This commit is contained in:
parent
b216a187bd
commit
f73c77f548
119 changed files with 4504 additions and 4829 deletions
24
frontend/app/components/About.tsx
Normal file
24
frontend/app/components/About.tsx
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
async function GetAboutDatas(){
|
||||
const res = await fetch(`${process.env.BACKEND_URL}/cv/about`, {
|
||||
cache: process.env.CACHE
|
||||
});
|
||||
|
||||
if(!res.ok){
|
||||
throw new Error("Erreur lors de la lecture des données de contact.")
|
||||
}
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export default async function About(){
|
||||
const about_text = await GetAboutDatas();
|
||||
|
||||
return (
|
||||
<section className="dev-hero">
|
||||
<div className="container container--narrow">
|
||||
<span className="eyebrow">Développeur web</span>
|
||||
<h1 className="headline headline--code">Anthony Violet</h1>
|
||||
<p className="subheadline">{about_text}</p>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
104
frontend/app/components/Contact.tsx
Normal file
104
frontend/app/components/Contact.tsx
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useEffect, useState } from "react";
|
||||
import { usePathname } from "next/navigation";
|
||||
|
||||
type ContactData = {
|
||||
email?: string;
|
||||
linkedin?: string;
|
||||
github?: string;
|
||||
};
|
||||
|
||||
export default function ContactComponent() {
|
||||
const pathname = usePathname();
|
||||
const onContactPage = pathname === "/contact";
|
||||
|
||||
const [data, setData] = useState<ContactData | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!onContactPage) return;
|
||||
|
||||
// Essaye avec BACKEND_URL, sinon fallback vers une route locale hypothétique
|
||||
const backend = (process.env.BACKEND_URL as string) || "http://127.0.0.1:5000/api";
|
||||
const url = backend ? `${backend}/contact` : "http://127.0.0.1:5000/api/contact";
|
||||
|
||||
fetch(url, { cache: "no-store" })
|
||||
.then((res) => (res.ok ? res.json() : Promise.reject(new Error("fetch contact failed"))))
|
||||
.then((json) => setData(json))
|
||||
.catch(() => setData(null));
|
||||
}, [onContactPage]);
|
||||
|
||||
// Si on n'est pas sur la page /contact => juste un lien
|
||||
if (!onContactPage) {
|
||||
return (
|
||||
<div className="block">
|
||||
<div className="btn-group">
|
||||
<Link href="/contact" className="btn btn--primary btn--pill">
|
||||
Me contacter
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Page /contact => liens + formulaire
|
||||
return (
|
||||
<div className="block">
|
||||
<div className="stack">
|
||||
<div className="btn-group">
|
||||
{data?.linkedin ? (
|
||||
<a href={data.linkedin} target="_blank" rel="noopener noreferrer" className="link-button">
|
||||
LinkedIn
|
||||
</a>
|
||||
) : null}
|
||||
{data?.github ? (
|
||||
<a href={data.github} target="_blank" rel="noopener noreferrer" className="link-button">
|
||||
GitHub
|
||||
</a>
|
||||
) : null}
|
||||
{data?.email ? (
|
||||
<a href={`mailto:${data.email}`} className="link-button">
|
||||
Email direct
|
||||
</a>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{data?.email ? (
|
||||
<form action={`mailto:${data.email}`} method="post" encType="text/plain" className="stack">
|
||||
<div>
|
||||
<label htmlFor="name">Votre nom</label>
|
||||
<input id="name" name="Nom" type="text" required />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="email">Votre email</label>
|
||||
<input id="email" name="Email" type="email" required />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="subject">Sujet</label>
|
||||
<input id="subject" name="Sujet" type="text" placeholder="Prise de contact" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="message">Message</label>
|
||||
<textarea id="message" name="Message" required />
|
||||
</div>
|
||||
|
||||
<div className="btn-group">
|
||||
<button type="submit" className="btn btn--primary btn--pill">
|
||||
Envoyer
|
||||
</button>
|
||||
<button type="reset" className="btn btn--ghost">
|
||||
Réinitialiser
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
) : (
|
||||
<p className="u-muted">Une erreur est survenue. Vous pouvez tout de même me contacter directement par email : <a href="mailto:violet.anthony90@gmail.com">Me joindre par email</a></p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
148
frontend/app/components/Header.tsx
Normal file
148
frontend/app/components/Header.tsx
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
"use client";
|
||||
// app/components/Header.tsx
|
||||
import Link from "next/link";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export default function Header() {
|
||||
const [theme, setTheme] = useState<"light" | "dark">("light");
|
||||
const [isTop, setIsTop] = useState(true);
|
||||
|
||||
// Initialise le thème en fonction du localStorage ou de la préférence système
|
||||
useEffect(() => {
|
||||
const saved = typeof window !== "undefined" ? localStorage.getItem("theme") : null;
|
||||
const prefersDark = typeof window !== "undefined" && window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches;
|
||||
const initial = (saved === "light" || saved === "dark") ? (saved as "light" | "dark") : (prefersDark ? "dark" : "light");
|
||||
setTheme(initial);
|
||||
document.documentElement.setAttribute("data-theme", initial);
|
||||
}, []);
|
||||
|
||||
// Détecte si l'on est tout en haut de la page
|
||||
useEffect(() => {
|
||||
const onScroll = () => setIsTop(window.scrollY <= 0);
|
||||
onScroll(); // état initial au chargement
|
||||
window.addEventListener("scroll", onScroll, { passive: true });
|
||||
return () => window.removeEventListener("scroll", onScroll);
|
||||
}, []);
|
||||
|
||||
const toggleTheme = () => {
|
||||
const next = theme === "dark" ? "light" : "dark";
|
||||
setTheme(next);
|
||||
document.documentElement.setAttribute("data-theme", next);
|
||||
try {
|
||||
localStorage.setItem("theme", next);
|
||||
} catch {}
|
||||
};
|
||||
|
||||
return (
|
||||
<header className={isTop ? "minimalist-pro is-top" : "minimalist-pro"}>
|
||||
<div className="logo">
|
||||
<Link href="/">
|
||||
<span className="name">Anthony Violet</span>
|
||||
<span className="tagline">Développeur Freelance & Digital Nomad</span>
|
||||
</Link>
|
||||
</div>
|
||||
<nav>
|
||||
<ul>
|
||||
<li><Link href="/projects" >Projets</Link></li>
|
||||
<li><Link href="/competences">Compétences</Link></li>
|
||||
<li><Link href="#blog">Blog Voyage</Link></li>
|
||||
<li><Link href="/contact">Contact</Link></li>
|
||||
<li><Link href="https://fr.malt.be/profile/anthonyviolet1" target="_blank">Malt</Link></li>
|
||||
<li><Link href="https://www.linkedin.com/in/anthony-violet/" target="_blank">LinkedIn</Link></li>
|
||||
</ul>
|
||||
</nav>
|
||||
<div className="cta">
|
||||
<button
|
||||
type="button"
|
||||
className="theme-toggle__btn"
|
||||
aria-label={theme === "dark" ? "Activer le thème clair" : "Activer le thème sombre"}
|
||||
onClick={toggleTheme}
|
||||
title={theme === "dark" ? "Mode clair" : "Mode sombre"}
|
||||
>
|
||||
{theme === "dark" ? "☀️" : "🌙"}
|
||||
</button>
|
||||
<Link href="mailto:tonemail@domaine.com" className="button">Travaillons ensemble</Link>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
/*<header className="cyberpunk">
|
||||
<div className="glitch-logo">
|
||||
<Link href="/">
|
||||
<span data-text="Anthony Violet">Anthony Violet</span>
|
||||
<span className="subtitle">// Développeur Full Stack & Créateur de Jeux</span>
|
||||
</Link>
|
||||
</div>
|
||||
<nav className="neon-nav">
|
||||
<ul>
|
||||
<li><Link href="#projets" className="neon-link">/projets</Link></li>
|
||||
<li><Link href="#tech" className="neon-link">/tech-stack</Link></li>
|
||||
<li><Link href="#blog" className="neon-link">/toine-traveller</Link></li>
|
||||
<li><Link href="#contact" className="neon-link">/contact@terminal</Link></li>
|
||||
</ul>
|
||||
</nav>
|
||||
<div className="status-bar">
|
||||
<span>En ligne █</span>
|
||||
<span>Disponible pour missions</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<header className="sidebar-header">
|
||||
<div className="profile-pic">
|
||||
<img src="ton-avatar.jpg" alt="Anthony Violet" />
|
||||
</div>
|
||||
<div className="profile-info">
|
||||
<h1>Anthony Violet</h1>
|
||||
<p>Freelance Dev | Digital Nomad | Builder de jeux & apps</p>
|
||||
</div>
|
||||
<nav>
|
||||
<ul>
|
||||
<li><Link href="#projets"><i className="icon-code"></i> Projets</Link></li>
|
||||
<li><Link href="#about"><i className="icon-user"></i> À propos</Link></li>
|
||||
<li><Link href="#blog"><i className="icon-globe"></i> Blog</Link></li>
|
||||
<li><Link href="#contact"><i className="icon-mail"></i> contact</Link></li>
|
||||
</ul>
|
||||
</nav>
|
||||
<div className="social-links">
|
||||
<Link href="https://github.com/tonuser" target="_blank"><i className="icon-github"></i></Link>
|
||||
<Link href="https://linkedin.com/in/anthony-violet" target="_blank"><i className="icon-linkedin"></i></Link>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<header className="interactive-header">
|
||||
<div className="typewriter">
|
||||
<h1>Bonjour, je suis <span className="typed-text">Anthony Violet</span></h1>
|
||||
<p>Je code en <span className="tech-tag">JavaScript</span>, <span className="tech-tag">Python</span>, et j'explore le monde.</p>
|
||||
</div>
|
||||
<nav>
|
||||
<ul>
|
||||
<li><Link href="#projets">Mes réalisations</Link></li>
|
||||
<li><Link href="#cv">Mon CV</Link></li>
|
||||
<li><Link href="#blog">Mes aventures</Link></li>
|
||||
</ul>
|
||||
</nav>
|
||||
<div className="language-switcher">
|
||||
<button>FR</button>
|
||||
<button>EN</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<header className="dark-header">
|
||||
<div className="container">
|
||||
<Link href="/" className="logo">
|
||||
<img src="logo.svg" alt="Logo Anthony Violet" />
|
||||
</Link>
|
||||
<nav>
|
||||
<ul>
|
||||
<li><Link href="#projets">Projets</Link></li>
|
||||
<li><Link href="#services">Services</Link></li>
|
||||
<li><Link href="#blog">Blog</Link></li>
|
||||
<li><Link href="#contact">Me contacter</Link></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
<div className="theme-toggle">
|
||||
<button id="theme-button"><i className="icon-moon"></i></button>
|
||||
</div>
|
||||
</header>*/
|
||||
);
|
||||
}
|
||||
35
frontend/app/components/Services.tsx
Normal file
35
frontend/app/components/Services.tsx
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
async function GetServices() {
|
||||
const res = await fetch(`${process.env.BACKEND_URL}/services`, {
|
||||
cache: process.env.CACHE
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error("Erreur lors de la lecture des services du CV.")
|
||||
}
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export default async function Services() {
|
||||
const services = await GetServices();
|
||||
|
||||
return (
|
||||
<section id="services" className="section">
|
||||
<div className="container">
|
||||
<h2 className="section-title">Services proposés</h2>
|
||||
<ul className="grid-3">
|
||||
{services.map((service: any) => (
|
||||
<li key={service.id} className="service-card card feature">
|
||||
<span className="feature__icon" aria-hidden="true" />
|
||||
<div>
|
||||
<h3 className="feature__title">{service.name}</h3>
|
||||
{"description" in service && service.description ? (
|
||||
<p className="u-muted">{service.description}</p>
|
||||
) : null}
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
73
frontend/app/components/Skills.tsx
Normal file
73
frontend/app/components/Skills.tsx
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
async function GetSkills() {
|
||||
const res = await fetch(`${process.env.BACKEND_URL}/cv/skills`, {
|
||||
cache: process.env.CACHE
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error("Erreur lors du chargement des skills du CV");
|
||||
}
|
||||
return res.json();
|
||||
}
|
||||
export default async function Skills(){
|
||||
const skills = await GetSkills();
|
||||
|
||||
const obj: Record<string, string[]> = Array.isArray(skills) ? (skills[0] ?? {}) : {};
|
||||
const entries = Object.entries(obj) as [string, string[]][];
|
||||
|
||||
if (entries.length === 0) {
|
||||
return (
|
||||
<section id="skills" className="section">
|
||||
<div className="container">
|
||||
<h2 className="section-title">Mes compétences</h2>
|
||||
<p className="u-muted">Aucune compétence à afficher.</p>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
/*
|
||||
*
|
||||
{mySkillsEntries.length > 0 && (
|
||||
<section id="competences" className="section">
|
||||
<div className="container">
|
||||
<h2 className="section-title">Compétences clés</h2>
|
||||
<ul className="skill-grid">
|
||||
{mySkillsEntries.map(([category, items]) => (
|
||||
<li key={category} className="skill-card">
|
||||
<h3 className="skill-card__title">{category}</h3>
|
||||
<div className="skill-card__tags">
|
||||
{items.map((it) => (
|
||||
<span key={it} className="badge">{it}</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="skill-meter" aria-hidden="true">
|
||||
<div className="skill-meter__bar" style={{["--level" as any]: "75%"}}/>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
* */
|
||||
<section id="competences" className="section">
|
||||
<div className="container">
|
||||
<h2 className="section-title">Mes compétences</h2>
|
||||
<ul className="skill-grid">
|
||||
{entries.map(([category, skills]) => (
|
||||
<li key={category} className="skill-card">
|
||||
<h3 className="skill-card__title">{category}</h3>
|
||||
<div className="skill-card__tags">
|
||||
{skills.map((skill: string) => (
|
||||
<span key={skill} className="badge">{skill}</span>
|
||||
))}
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue