chore: replace old vitejs project to nextjs

This commit is contained in:
2026-03-22 11:03:18 +01:00
parent 1ebba2c12c
commit 2e44b41cfe
34 changed files with 4434 additions and 3182 deletions

View File

@@ -1,11 +0,0 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
version: 2
updates:
- package-ecosystem: "npm" # See documentation for possible values
directory: "/" # Location of package manifests
schedule:
interval: "weekly"

View File

@@ -1,98 +0,0 @@
name: Docker
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
on:
schedule:
- cron: '24 22 * * *'
push:
branches: [ "main" ]
# Publish semver tags as releases.
tags: [ 'v*.*.*' ]
pull_request:
branches: [ "main" ]
env:
# Use docker.io for Docker Hub if empty
REGISTRY: ghcr.io
# github.repository as <account>/<repo>
IMAGE_NAME: ${{ github.repository }}
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
# This is used to complete the identity challenge
# with sigstore/fulcio when running outside of PRs.
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
# Install the cosign tool except on PR
# https://github.com/sigstore/cosign-installer
- name: Install cosign
if: github.event_name != 'pull_request'
uses: sigstore/cosign-installer@59acb6260d9c0ba8f4a2f9d9b48431a222b68e20 #v3.5.0
with:
cosign-release: 'v2.2.4'
# Set up BuildKit Docker container builder to be able to build
# multi-platform images and export cache
# https://github.com/docker/setup-buildx-action
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # v3.0.0
# Login against a Docker registry except on PR
# https://github.com/docker/login-action
- name: Log into registry ${{ env.REGISTRY }}
if: github.event_name != 'pull_request'
uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # v3.0.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# Extract metadata (tags, labels) for Docker
# https://github.com/docker/metadata-action
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@96383f45573cb7f253c731d3b3ab81c87ef81934 # v5.0.0
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
# Build and push Docker image with Buildx (don't push on PR)
# https://github.com/docker/build-push-action
- name: Build and push Docker image
id: build-and-push
uses: docker/build-push-action@0565240e2d4ab88bba5387d719585280857ece09 # v5.0.0
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
# Sign the resulting Docker image digest except on PRs.
# This will only write to the public Rekor transparency log when the Docker
# repository is public to avoid leaking data. If you would like to publish
# transparency data even for private images, pass --force to cosign below.
# https://github.com/sigstore/cosign
- name: Sign the published Docker image
if: ${{ github.event_name != 'pull_request' }}
env:
# https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions#using-an-intermediate-environment-variable
TAGS: ${{ steps.meta.outputs.tags }}
DIGEST: ${{ steps.build-and-push.outputs.digest }}
# This step uses the identity token to provision an ephemeral certificate
# against the sigstore community Fulcio instance.
run: echo "${TAGS}" | xargs -I {} cosign sign --yes {}@${DIGEST}

56
.gitignore vendored
View File

@@ -1,25 +1,41 @@
# Logs
logs
*.log
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
.pnpm-debug.log*
public
node_modules
dist
dist-ssr
*.local
# env files (can opt-in for committing if needed)
.env*
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts

View File

@@ -1,17 +0,0 @@
# Build
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
# Nginx
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
VOLUME /usr/share/nginx/html/data
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

View File

@@ -1 +1,36 @@
# website
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.

View File

@@ -1,29 +0,0 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import { defineConfig, globalIgnores } from 'eslint/config'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{js,jsx}'],
extends: [
js.configs.recommended,
reactHooks.configs['recommended-latest'],
reactRefresh.configs.vite,
],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
parserOptions: {
ecmaVersion: 'latest',
ecmaFeatures: { jsx: true },
sourceType: 'module',
},
},
rules: {
'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
},
},
])

View File

@@ -1,87 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<title>Loïc GUEZO</title>
<meta name="description" content="Portfolio of Loïc GUEZO, a French student aiming to become an Embedded Systems developer. Experience in AI, Game Development, and Embedded Systems.">
<meta name="keywords" content="Loïc GUEZO, Embedded Systems developer, electronics, IoT, microcontrollers, game development, programming, France">
<meta name="author" content="Loïc GUEZO">
<meta name="robots" content="index, follow, max-image-preview:large">
<meta name="language" content="en-US">
<meta name="geo.region" content="FR">
<meta name="geo.placename" content="France">
<meta property="og:type" content="website">
<meta property="og:site_name" content="Loïc GUEZO Portfolio">
<meta property="og:title" content="Loïc GUEZO | Portfolio">
<meta property="og:description" content="Portfolio of Loïc GUEZO | Embedded Systems, AI, and Game Development projects.">
<meta property="og:url" content="https://guezoloic.com">
<meta property="og:image" content="https://guezoloic.com/data/preview.png">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
<meta property="og:image:alt" content="Portfolio of Loïc GUEZO | Embedded Systems, AI, and Game Development projects.">
<meta property="og:locale" content="fr_FR">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:site" content="@GuezoLoic">
<meta name="twitter:creator" content="@GuezoLoic">
<meta name="twitter:title" content="Loïc GUEZO | Portfolio">
<meta name="twitter:description" content="Portfolio of Loïc GUEZO | Embedded Systems, AI, and Game Development projects.">
<meta name="twitter:image" content="https://guezoloic.com/data/preview.png">
<meta name="twitter:image:alt" content="Portfolio of Loïc GUEZO">
<link rel="canonical" href="https://guezoloic.com">
<link rel="icon" type="image/x-icon" href="https://guezoloic.com/data/favicon.ico">
<link rel="icon" type="image/png" sizes="16x16" href="https://guezoloic.com/data/favicon-16x16.png">
<link rel="icon" type="image/png" sizes="32x32" href="https://guezoloic.com/data/favicon-32x32.png">
<link rel="apple-touch-icon" sizes="180x180" href="https://guezoloic.com/data/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="192x192" href="https://guezoloic.com/data/android-chrome-192x192.png">
<link rel="icon" type="image/png" sizes="512x512" href="https://guezoloic.com/data/android-chrome-512x512.png">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700&display=swap" rel="stylesheet">
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Person",
"name": "Loïc GUEZO",
"jobTitle": "Student in Embedded Systems",
"description": "Portfolio of Loïc GUEZO, a French student aiming to become an Embedded Systems developer. Experience in AI, Game Development, and Embedded Systems.",
"url": "https://guezoloic.com",
"image": "https://guezoloic.com/profile.png",
"sameAs": [
"https://github.com/guezoloic",
"https://linkedin.com/in/guezoloic"
],
"knowsAbout": [
"Embedded Systems",
"Artificial Intelligence",
"Game Development",
"IoT",
"Programming",
"Microcontrollers"
],
"alumniOf": {
"@type": "Organization",
"name": "UPEC"
},
"nationality": {
"@type": "Country",
"name": "France"
}
}
</script>
<link href="/src/styles/style.css" rel="stylesheet">
<link rel="preload" href="/src/Main.tsx" as="script">
<link rel="modulepreload" href="/src/Main.tsx">
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/Main.tsx"></script>
<noscript><h1>Hello There!</h1></noscript>
</body>
</html>

View File

@@ -1,22 +0,0 @@
server {
listen 80;
server_name www.guezoloic.com;
return 301 $scheme://guezoloic.com;
}
server {
listen 80;
server_name guezoloic.com;
root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri /index.html;
}
location /data/ {
alias /usr/share/nginx/html/data/;
# autoindex on;
}
}

6004
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,43 +1,26 @@
{
"name": "website",
"version": "0.1.0",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"lint": "eslint .",
"preview": "vite preview"
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "eslint"
},
"dependencies": {
"@fortawesome/free-solid-svg-icons": "^7.0.0",
"@fortawesome/react-fontawesome": "^0.2.3",
"@heroicons/react": "^2.2.0",
"@tailwindplus/elements": "^1.0.4",
"framer-motion": "^12.23.12",
"lucide-react": "^0.536.0",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-i18next": "^15.7.2",
"react-icons": "^5.5.0",
"react-router-dom": "^7.7.1",
"simple-icons": "^15.11.0",
"three": "^0.178.0"
"next": "16.2.1",
"react": "19.2.4",
"react-dom": "19.2.4"
},
"devDependencies": {
"@eslint/js": "^9.30.1",
"@tailwindcss/vite": "^4.2.1",
"@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6",
"@types/three": "^0.179.0",
"@vitejs/plugin-react": "^4.6.0",
"autoprefixer": "^10.4.21",
"eslint": "^9.30.1",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20",
"globals": "^16.3.0",
"postcss": "^8.5.6",
"tailwindcss": "^4.1.11",
"vite": "^7.0.4"
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "16.2.1",
"tailwindcss": "^4",
"typescript": "^5"
}
}

View File

@@ -1,41 +0,0 @@
import Title from "./pages/Title";
import Three from "./pages/Three";
import Navbar from "./pages/Navbar";
import About from "./pages/About";
import Skills from "./pages/Skills";
import { useState } from "react";
import './utils/translation';
import Projects from "./pages/Projects";
export type MenuState = {
about: boolean;
skills: boolean;
projects: boolean;
};
export default function App() {
const [state, setState] = useState<MenuState>({
about: false,
skills: false,
projects: false,
});
const closeSection = (key: keyof MenuState) => {
setState(prev => ({ ...prev, [key]: false }));
};
const isOpen = Object.values(state).some(value => value === true);
return (
<div className="relative w-full h-screen">
<Three />
<Title isOpen={isOpen}/>
<Navbar state={state} setState={setState} isOpen={isOpen} />
<About id="about" open={state.about} onClose={() => closeSection("about")} />
<Skills id="skills" open={state.skills} onClose={() => closeSection("skills")} />
<Projects id="projects" open={state.projects} onClose={() => closeSection("projects")} />
</div>
);
}

View File

@@ -1,4 +0,0 @@
import ReactDOM from 'react-dom/client';
import App from './App';
ReactDOM.createRoot(document.getElementById('root')!).render( <App /> );

View File

@@ -1,38 +0,0 @@
import { ReactNode } from "react";
type ButtonProps = {
children: ReactNode;
onClick?: () => void;
label: string;
variant?: "icon" | "text";
className?: string;
};
export default function Button({ children, onClick, label, variant = "icon", className = "" }: ButtonProps) {
const BASECLASS = "cursor-pointer flex items-center justify-center backdrop-blur-sm \
bg-black/17 shadow-md text-white transition-all duration-200 ease-out \
hover:bg-white/15 active:scale-95 shadow-lg shadow-black/50 \
pointer-events-auto hover:shadow-black/0";
// dictionary to choose if it's a icon or text button
const variants: Record<typeof variant, string> = {
icon: "rounded-full w-12 h-12 md:w-14 md:h-14 hover:scale-110",
text: "rounded-3xl px-4 h-12 md:h-14 md:px-6 max-w-max hover:scale-105",
};
return (
<div className="relative group">
<button onClick={onClick} aria-label={label} className={`${BASECLASS} ${variants[variant]}`}>
{children}
</button>
{label && (
<span className="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 px-2 py-1
rounded-md text-xs text-white bg-black/80 opacity-0
group-hover:opacity-100 transition-opacity whitespace-nowrap">
{label}
</span>
)}
</div>
);
}

View File

@@ -1,22 +0,0 @@
import Button from './Button';
import i18n from "../utils/translation";
export default function Lang() {
const toggleLanguage = () => {
const newLang = i18n.language === "fr" ? "en" : "fr";
i18n.changeLanguage(newLang);
};
const nextLangLabel = i18n.language === "fr" ? "EN" : "FR";
return (
<Button
onClick={toggleLanguage}
label={`Lang: ${nextLangLabel}`}
variant="icon"
>
{nextLangLabel}
</Button>
)
}

View File

@@ -1,24 +0,0 @@
import React, { JSX } from "react";
type SectionProps = {
children: JSX.Element;
title: string;
id: string
};
export default function Section({ children, title, id }: SectionProps) {
return (
<section
id={id}
className="my-3 relative max-w-5xl mx-auto mt-5 rounded-2xl flex flex-col gap-8 text-gray-100"
>
<h2
className="text-3xl sm:text-5xl font-extrabold bg-clip-text text-transparent
bg-gradient-to-r from-green-200 via-emerald-600 to-green-800"
>
{title}
</h2>
{children}
</section>
);
};

View File

@@ -1,43 +0,0 @@
import React, { ReactNode, useEffect } from "react";
import { motion, AnimatePresence } from "framer-motion";
interface SectionProps {
open: boolean;
onClose: () => void;
children: ReactNode;
}
export default function Window ({ open, onClose, children }: SectionProps) {
useEffect(() => {
if (open) document.body.style.overflow = "hidden";
else document.body.style.overflow = "";
}, [open]);
return (
<AnimatePresence>
{open && (
<motion.div
className="fixed inset-0 z-10 bg-black/30 backdrop-blur-sm"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{
duration: 0.3
}}
>
<div className="h-full overflow-y-auto text-white">
<motion.div
initial={{ opacity: 0, y: 40 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 40 }}
transition={{ duration: 0.3 }}
className="flex flex-col md:flex-row items-center justify-center gap-10 px-6 md:px-24 py-20 md:py-32"
>
{children}
</motion.div>
</div>
</motion.div>
)}
</AnimatePresence>
);
};

View File

@@ -1,13 +0,0 @@
{
"model": "BASEmodel.glb",
"idle_animation": "idle.glb",
"welcome_animation": "waving.glb",
"animations": [
"StandingW_BriefcaseIdle.glb",
"Acknowledging.glb",
"ArmStretching.glb",
"OffensiveIdle.glb",
"ThoughtfulHeadShake.glb",
"DwarfIdle.glb"
]
}

View File

@@ -1,139 +0,0 @@
{
"name": "GUEZO Loïc",
"career": "me.career",
"profile_image": "https://guezoloic.com/data/guezoloic.png",
"navbar": {
"buttons": [
{
"label": "about.label",
"action": "about",
"icon": ""
},
{
"label": "skills.label",
"action": "skills",
"icon": "CodeBracketIcon"
},
{
"label": "projects.label",
"action": "projects",
"icon": "ServerIcon"
}
]
},
"about": [
{
"icon": "LightBulbIcon",
"text": "about.content.0"
},
{
"icon": "CodeBracketIcon",
"text": "about.content.1"
},
{
"icon": "CpuChipIcon",
"text": "about.content.2"
},
{
"icon": "PuzzlePieceIcon",
"text": "about.content.3"
},
{
"icon": "Cog6ToothIcon",
"text": "about.content.4"
},
{
"icon": "RocketLaunchIcon",
"text": "about.content.5"
},
{
"icon": "ServerStackIcon",
"text": "about.content.6"
}
],
"skills": [
{
"title": "skills.content.0",
"tags": [
{
"name": "C",
"icon": "SiC"
},
{
"name": "C++",
"icon": "SiCplusplus"
},
{
"name": "Rust",
"icon": "SiRust"
},
{
"name": "Python",
"icon": "SiPython"
},
{
"name": "Java",
"icon": "SiJava"
},
{
"name": "Bash",
"icon": "SiGnubash"
},
{
"name": "HTML/CSS/JS/TS",
"icon": "SiHtml5"
},
{
"name": "React",
"icon": "SiReact"
}
]
},
{
"title": "skills.content.1",
"tags": [
{
"name": "Git",
"icon": "SiGit"
},
{
"name": "Docker",
"icon": "SiDocker"
},
{
"name": "Android Studio",
"icon": "SiAndroid"
}
]
},
{
"title": "skills.content.2",
"tags": [
{
"name": "Linux/Linux Server",
"icon": "SiLinux"
},
{
"name": "Proxmox",
"icon": "SiProxmox"
},
{
"name": "Jetson Nano",
"icon": "SiNvidia"
},
{
"name": "Raspberry Pi",
"icon": "SiRaspberrypi"
},
{
"name": "Arduino",
"icon": "SiArduino"
},
{
"name": "ESP32",
"icon": "SiEspressif"
}
]
}
]
}

View File

@@ -1,44 +0,0 @@
{
"me": {
"title": "Hello There! 👋",
"subTitle": "I'm glad you're here",
"career": "IT Student"
},
"about": {
"label": "About",
"title": "Hi, I'm Loïc! 👋",
"content": [
"Curious by nature, I like to explore what happens under all the layers of a computer system.",
"I started with simple Python programs, then I gradually discovered a fascination for low-level programming.",
"By learning the basics of Intel x86_64 assembly in my degree and touching electronics, I realized that I love learning and designing at a fundamental level.",
"Over time, I got closer and closer to low-level programming and completed a few small projects.",
"These projects allowed me to manipulate and experiment with the functioning of different hardware.",
"I am also interested in various projects, such as creating a small AI or a graphical engine on a terminal.",
"Computer tools and hardware have given me a complete vision of embedded systems, and I want to continue learning in this field.",
"I aim to combine software and hardware to design complex and efficient systems."
]
},
"skills": {
"label": "Skills",
"title": "My Skills",
"content": [
"Programming Languages",
"Tools",
"Hardware & Embedded Systems"
]
},
"projects": {
"label": "Projects",
"title": "My Projects"
},
"links": {
"label": "Links",
"title": "Links",
"descriptions": [
"",
"",
"",
""
]
}
}

View File

@@ -1,44 +0,0 @@
{
"me": {
"title": "Bienvenue ! 👋",
"subTitle": "Ravi·e de vous voir ici",
"career": "Étudiant info"
},
"about": {
"label": "À propos",
"title": "Bonjour, je suis Loïc ! 👋",
"content": [
"Curieux de nature, j'aime explorer ce qui se passe sous toutes les couches d'un système informatique.",
"J'ai commencé par de simples programmes en Python, puis j'ai progressivement découvert une fascination pour le bas niveau.",
"En apprenant les bases de l'assembleur Intel x86_64 en licence et en touchant à l'électronique, j'ai compris que j'adore apprendre et concevoir à un niveau fondamental.",
"Au fil du temps, je me suis rapproché de la programmation bas niveau et réalisé quelques petits projets.",
"Ces projets m'ont permis de manipuler et d'expérimenter le fonctionnement de différents matériels.",
"Je m'intéresse aussi à des projets variés, comme créer une petite IA ou un moteur graphique sur terminal.",
"Les outils informatique et matériels m'ont donné une vision complète des systèmes embarqués et je souhaite continuer à apprendre dans cette voie.",
"Je cherche à combiner logiciel et matériel pour concevoir des systèmes complexes et efficaces."
]
},
"skills": {
"label": "Compétences",
"title": "Mes Compétences",
"content": [
"Langages Informatique",
"Outils",
"Matériel & Systèmes embarqués"
]
},
"projects": {
"label": "Projets",
"title": "Mes Projets"
},
"links": {
"label": "Liens",
"title": "Liens",
"descriptions": [
"",
"",
"",
""
]
}
}

View File

@@ -1,53 +0,0 @@
import React, { JSX } from "react";
import { motion } from "framer-motion";
import * as SOLID from "@heroicons/react/24/solid";
import { useTranslation } from 'react-i18next';
import Section from "../components/Section";
import content from "../json/content.json"
import Window from "../components/Window";
type AboutProps = {
id: string;
open: boolean;
onClose: () => void;
};
export default function About({ id, open, onClose }: AboutProps) {
const { t } = useTranslation();
const paragraphs = content.about;
return (
<Window open={open} onClose={onClose}>
<div className="flex justify-center items-center h-full w-full md:px-6">
<div className="max-w-3xl w-full bg-black/21 rounded-2xl p-8 space-y-4 shadow-lg">
<Section id={id} title={t('about.title')}>
<div className="flex flex-col gap-1">
{paragraphs.map((paragraph, i) => {
const Icon = (SOLID as Record<string, React.ElementType>)[paragraph.icon];
return (
<motion.div
key={i}
className="flex items-start gap-3 p-4 rounded-xl hover:bg-black/30 transition-colors"
initial={{ opacity: 0, y: 15 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: false, amount: 0 }}
transition={{ duration: 0.6, delay: i * 0.08 }}
>
<span className="mt-1 text-emerald-400">
<Icon className="w-6 h-6" />
</span>
<p className="text-sm md:text-base leading-relaxed text-white">
{t(paragraph.text)}
</p>
</motion.div>
)
})}
</div>
</Section>
</div>
</div>
</Window>
);
};

View File

@@ -1,112 +0,0 @@
import React, { Dispatch, SetStateAction } from "react";
import { useTranslation } from "react-i18next";
import { motion, AnimatePresence, Variants } from "framer-motion";
import * as SOLID from "@heroicons/react/24/solid";
import Button from "../components/Button";
import content from "../json/content.json";
import { MenuState } from "../App";
import Lang from "../components/Lang"
type NavbarProps = {
state: MenuState;
setState: Dispatch<SetStateAction<MenuState>>;
isOpen: boolean;
};
const navVariants: Variants = {
initial: { scaleY: 0.8, scaleX: 0.1, opacity: 0.7 },
animate: { scaleY: 1, scaleX: 1, opacity: 1, transition: { duration: 0.3, ease: [0.42, 0, 0.58, 1] } },
};
const exitVariants: Variants = {
initial: { scaleY: 1, scaleX: 5, opacity: 0.7 },
animate: { scaleY: 1, scaleX: 1, opacity: 1, transition: { duration: 0.3, ease: [0.42, 0.66, 0.58, 1] } },
};
export default function Navbar({ state, setState, isOpen }: NavbarProps) {
const { t } = useTranslation();
const handleClick = (key: keyof MenuState) => {
setState(prev => ({ ...prev, [key]: true }));
};
const closeWindows = () => {
setState(prev => {
const newState: MenuState = {} as MenuState;
for (const key in prev) newState[key as keyof MenuState] = false;
return newState;
});
};
const mainButton = content.navbar.buttons[0];
return (
<AnimatePresence>
{isOpen ? (
<motion.div
key="close"
className="fixed bottom-4 left-1/2 transform -translate-x-1/2 flex justify-center z-50"
variants={exitVariants}
initial="initial"
animate="animate"
exit="exit"
>
<Button
onClick={closeWindows}
label="exit"
variant="icon"
>
<SOLID.XMarkIcon className="w-8 h-8 text-white" />
</Button>
</motion.div>
) : (
<motion.div
key="menu"
className="fixed bottom-4 left-1/2 transform -translate-x-1/2 flex items-center gap-2 z-50"
variants={navVariants}
initial="initial"
animate="animate"
exit="exit"
>
<Button
onClick={() => handleClick(mainButton.action as keyof MenuState)}
label={t(mainButton.label)}
variant="text"
>
<div className="flex flex-col items-center justify-center whitespace-nowrap">
<span className="text-base md:text-lg font-bold text-white drop-shadow-lg">
{content.name}
</span>
<span className="text-xs md:text-sm text-gray-300 font-light">
{t(content.career)}
</span>
</div>
</Button>
{content.navbar.buttons.slice(1).map((btn, i) => {
const Icon = (SOLID as Record<string, React.ElementType>)[btn.icon];
return (
<div className="relative group" key={i}>
<Button
onClick={() => handleClick(btn.action as keyof MenuState)}
label={t(btn.label)}
variant="icon"
>
{Icon && <Icon className="w-6 h-6 text-white" />}
</Button>
<span className="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 px-2 py-1 rounded-md text-xs text-white bg-black/80 opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap">
{t(btn.label)}
</span>
</div>
);
})}
<Lang />
</motion.div>
)}
</AnimatePresence>
);
}

View File

@@ -1,82 +0,0 @@
import { motion } from "framer-motion";
import React, { useEffect, useState } from "react";
import Section from "../components/Section";
import { useTranslation } from "react-i18next";
import Window from "../components/Window";
interface ProjectProps {
name: string;
description: string;
html_url: string;
language: string | null;
}
type ProjectsProps = {
id: string;
open: boolean;
onClose: () => void;
};
export default function Projects({ id, open, onClose }: ProjectsProps) {
const [repos, setRepos] = useState<ProjectProps[]>([]);
useEffect(() => {
const fetchRepos = async () => {
try {
const res = await fetch(`https://api.github.com/users/guezoloic/repos?per_page=100`);
const data = await res.json();
const sorted = data
.filter((repo: any) => !repo.fork)
.sort((a: any, b: any) => b.stargazers_count - a.stargazers_count)
.slice(0, 6)
.map((repo: any) => ({
name: repo.name,
description: repo.description,
html_url: repo.html_url,
language: repo.language,
}));
setRepos(sorted);
} catch (err) {
console.error("Error while loading repos", err);
}
};
fetchRepos();
}, []);
const { t } = useTranslation();
return (
<Window open={open} onClose={onClose}>
<Section id={id} title={t("projects.title")}>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
{repos.map((repo, i) => (
<motion.a
key={repo.name}
href={repo.html_url}
target="_blank"
rel="noopener noreferrer"
className="bg-black/30 p-6 rounded-2xl shadow-lg hover:scale-105 transition-transform duration-300 flex flex-col gap-3"
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: false, amount: 0.6 }}
transition={{ duration: 0.4, delay: i * 0.1 }}
>
<h3 className="text-xl font-semibold text-white">{repo.name}</h3>
<p className="text-gray-200 text-sm">
{repo.description || "No description"}
</p>
{repo.language && (
<span className="text-sm font-medium text-emerald-400">
{repo.language}
</span>
)}
</motion.a>
))}
</div>
</Section>
</Window>
);
};

View File

@@ -1,54 +0,0 @@
import React from "react";
import { motion } from "framer-motion";
import * as SiIcons from "react-icons/si";
import Section from "../components/Section";
import { useTranslation } from "react-i18next";
import content from "../json/content.json"
import Window from "../components/Window";
type SkillsProps = {
id: string;
open: boolean;
onClose: () => void;
};
export default function Skills({ id, open, onClose }: SkillsProps) {
const { t } = useTranslation();
const skillsData = content.skills;
return (
<Window open={open} onClose={onClose}>
<Section id={id} title={t("skills.title")}>
{skillsData.map((section, i) => (
<div key={i} className="flex flex-col gap-2">
<div className="flex flex-col gap-2 max-w-3xl w-full bg-black/20 rounded-2xl p-8 space-y-4 shadow-lg">
<h2 className="text-xl font-semibold font-extrabold bg-clip-text text-transparent
bg-gradient-to-r from-green-200 via-emerald-600 to-green-800">{t(section.title)}:</h2>
<div className="flex flex-wrap gap-2">
{section.tags.map((tag, j) => {
const Icon = (SiIcons as Record<string, React.ElementType>)[tag.icon];
return (
<motion.div
key={j}
className="flex items-center gap-2 p-2"
initial={{ opacity: 0, y: 15 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: false, amount: 0 }}
transition={{ duration: 0.5, delay: j * 0.08 }}
>
<div className="flex items-center gap-2 p-1.5">
{Icon && <Icon className="w-5 h-5 text-emerald-400 mt-1" />}
<span className="text-sm md:text-base">{tag.name}</span>
</div>
</motion.div>
);
})}
</div>
</div>
</div>
))}
</Section>
</Window>
);
};

View File

@@ -1,32 +0,0 @@
import { useEffect, useRef, useState } from "react";
import * as THREE from "three";
import Main from "../three/main";
export default function Three() {
const mountRef = useRef<HTMLDivElement>(null); // Parent canva element
const [loading, setLoading] = useState(true); // Loading boolean element
useEffect(() => {
if (!window.WebGL2RenderingContext) return;
const loadingManager = new THREE.LoadingManager(() => setLoading(false));
new Main(mountRef.current!, loadingManager);
}, []);
// canva must exist before loading so that Main doesn't crash
return (
<div className="fixed w-full h-full inset-0 z-0 overflow-hidden">
{window.WebGL2RenderingContext ?
<>
<div ref={mountRef} className="top-0 left-0 w-full h-full" />
{loading && (
<div className="absolute top-1/2 left-1/2 w-16 h-16 -translate-x-1/2 -translate-y-1/2 border-4 border-white border-t-transparent rounded-full animate-spin" />
)}
</>
:
<div className="text-white text-center mt-10">
WebGL2 is not supported.
</div>
}
</div>
);
};

View File

@@ -1,50 +0,0 @@
import { motion } from "framer-motion";
import { useTranslation } from "react-i18next";
import content from "../json/content.json"
export default function Title({isOpen}: {isOpen: boolean}) {
const { t } = useTranslation();
return (
<motion.section
className="flex items-center justify-center"
style={isOpen ? { overflow: "hidden" } : undefined}
initial={{ opacity: 0, y: 50 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ amount: 0.2, once: false }}
transition={{ duration: 0.3, ease: "easeOut" }}
>
<div className="absolute top-6 bg-transparent px-4 md:px-12 pt-6 md:pt-5 max-w-5xl flex flex-row justify-between items-center gap-15">
<motion.div
className="text-x2l md:text-3xl font-bold text-white leading-tight"
initial={{ opacity: 0, x: -50 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: false, amount: 0.5 }}
transition={{ duration: 0.3 }}
>
<h3>{t("me.title")}</h3>
<p className="text-sm font-medium text-gray-400">{t("me.subTitle")}</p>
</motion.div>
<motion.div
initial={{ opacity: 0, x: 25 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: false, amount: 0.5 }}
transition={{ duration: 0.3 }}
>
<img
src={content.profile_image}
alt={content.name}
loading="lazy"
className="w-auto h-17 md:h-20 rounded-full object-cover shadow-2xl border-4 border-white/20 transform transition-transform duration-500 hover:scale-105 self-start block"
width={96}
height={96}
/>
</motion.div>
</div>
</motion.section>
);
};

View File

@@ -1,33 +0,0 @@
@import "tailwindcss";
* {
scrollbar-width: thin;
scrollbar-color: #2f5643 #1e3d2e;
}
html,
body {
height: 100dvh;
margin: 0;
padding: 0;
background-color: #1e3d2e;
font-family: 'Inter', sans-serif;
}
::-webkit-scrollbar {
width: 12px;
}
::-webkit-scrollbar-track {
background: #1e3d2e;
}
::-webkit-scrollbar-thumb {
background-color: #2f5643;
border-radius: 6px;
border: 3px solid #1e3d2e;
}
::-webkit-scrollbar-thumb:hover {
background-color: #3f6b55;
}

View File

@@ -1,92 +0,0 @@
import * as THREE from "three";
import Animation from "./animation";
import assets from "../json/assets.json"
const ANIMATION_ASSETS = assets.animations;
export default class AnimationQueue {
private animation: Animation;
private queue: THREE.AnimationAction[] = [];
private currentAction: THREE.AnimationAction | null = null;
private mixer: THREE.AnimationMixer;
private randomIntervalId: number | null = null;
constructor(animation: Animation) {
this.mixer = animation.getMixer();
this.animation = animation;
}
public onqueue(action: THREE.AnimationAction) {
this.queue.push(action);
this.tryPlayNext();
}
public replaceFirst(action: THREE.AnimationAction) {
this.queue.unshift(action);
this.tryPlayNext(true);
}
public async tryPlayNext(force: boolean = false) {
if (this.currentAction) {
if (force) {
this.currentAction.fadeOut(this.animation.getFadeout());
this.currentAction = null;
} else {
if ((this.currentAction as any).isBasicClone) {
this.currentAction.fadeOut(this.animation.getFadeout());
this.currentAction = null;
} else {
return;
}
}
}
if (!this.queue.length) this.queue.push(this.animation.getBasicAction());
const NEXTACTION = this.queue.shift()!;
NEXTACTION.reset();
NEXTACTION.setLoop(THREE.LoopOnce, 1);
NEXTACTION.clampWhenFinished = true;
NEXTACTION.fadeIn(this.animation.getFadein()).play();
const onFinish = (e: any) => {
if (e.action === this.currentAction) {
if (this.currentAction) this.currentAction.fadeOut(this.animation.getFadeout());
this.mixer.removeEventListener("finished", onFinish);
this.currentAction = null;
return this.tryPlayNext();
}
};
this.currentAction = NEXTACTION;
this.mixer.addEventListener("finished", onFinish);
}
public startRandom() {
if (this.randomIntervalId !== null) return;
this.randomIntervalId = window.setInterval(async () => {
if (!this.mixer) return;
const RANDOMINDEX = Math.floor(Math.random() * ANIMATION_ASSETS.length);
this.onqueue(await this.animation.loadAnimation(ANIMATION_ASSETS[RANDOMINDEX]));
}, 30_000);
}
public stopRandom() {
if (this.randomIntervalId !== null) {
clearInterval(this.randomIntervalId);
this.randomIntervalId = null;
}
}
public clearQueue() {
this.queue = [];
}
public stop() {
this.clearQueue();
this.currentAction?.fadeOut(this.animation.getFadeout());
this.currentAction = null;
this.stopRandom();
}
}

View File

@@ -1,81 +0,0 @@
import * as THREE from "three";
import { GLTF, GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
export default class Animation {
private mixer: THREE.AnimationMixer;
private loader: GLTFLoader;
private actions: Map<string, THREE.AnimationAction> = new Map();
private basicAction!: THREE.AnimationAction;
private fadein: number;
private fadeout: number;
constructor(
model: THREE.Object3D,
loader: GLTFLoader,
basicAction_url: string,
fadein: number = 0.5, fadeout: number = 0.8) {
this.mixer = new THREE.AnimationMixer(model);
this.loader = loader;
this.fadein = fadein;
this.fadeout = fadeout;
this.setBasicAction(basicAction_url);
}
public loadAnimation(url: string): Promise<THREE.AnimationAction> {
url = `https://guezoloic.com/data/${url}`;
return new Promise((resolve, reject) => {
if (this.actions.has(url)) return resolve(this.actions.get(url)!);
this.loader.load(
url,
(gltf: GLTF) => {
if (!gltf.animations.length) return reject(new Error(`${url} has no animations`));
let clip = gltf.animations[0];
if (clip.tracks.some(track => track.name.endsWith('.position'))) {
clip = clip.clone();
clip.tracks = clip.tracks.filter(track => !track.name.endsWith('.position'));
}
const action = this.mixer.clipAction(clip);
action.stop();
this.actions.set(url, action);
resolve(action);
},
undefined,
reject
);
});
}
public setBasicAction(url: string) {
this.loadAnimation(url).then(action => {
this.basicAction = action;
});
}
public getBasicAction(): THREE.AnimationAction {
const clipClone = this.basicAction.getClip().clone();
const action = this.mixer.clipAction(clipClone);
(action as any).isBasicClone = true;
return action;
}
public getFadein(): number { return this.fadein; }
public getFadeout(): number { return this.fadeout; }
public update(delta: number) {
this.mixer?.update(delta);
}
public getMixer(): THREE.AnimationMixer {
if (!this.mixer) throw new Error("Mixer not initialized yet!");
return this.mixer
}
}

View File

@@ -1,34 +0,0 @@
import * as THREE from "three";
export default class Camera {
private camera: THREE.PerspectiveCamera;
private box = new THREE.Box3();
private size = new THREE.Vector3();
private center = new THREE.Vector3();
constructor(camera: THREE.PerspectiveCamera, model: THREE.Object3D) {
this.camera = camera;
this.box = this.box.setFromObject(model);
}
public centerCamera(
mouvement: (box: THREE.Box3, size: THREE.Vector3, center: THREE.Vector3) => void
) {
this.box.getSize(this.size);
this.box.getCenter(this.center);
mouvement(this.box, this.size, this.center);
}
public positionCamera() {
const fov = this.camera.fov * (Math.PI / 180);
const distance = this.size.y / (2 * Math.tan(fov / 2));
this.camera.position.set(0, 0, distance * 1.2);
this.camera.lookAt(0, 0, 0);
}
public getCamera(): THREE.PerspectiveCamera {
return this.camera;
}
}

View File

@@ -1,109 +0,0 @@
import * as THREE from "three";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import Model from "./model";
import Camera from "./camera";
import Animation from "./animation";
import assets from "../json/assets.json"
import AnimationQueue from "./animQueue";
export default class Main {
private element!: HTMLElement;
private loadingManager?: THREE.LoadingManager;
private scene!: THREE.Scene;
private renderer!: THREE.WebGLRenderer;
private camera!: THREE.PerspectiveCamera;
private controls!: OrbitControls;
private loader!: GLTFLoader;
private clock: THREE.Clock;
private animation!: Animation;
constructor(htmlelement: HTMLElement, loadingManager?: THREE.LoadingManager) {
this.element = htmlelement;
this.loadingManager = loadingManager;
this.clock = new THREE.Clock();
this.init();
}
private async init() {
this.scene = new THREE.Scene();
this.camera = new THREE.PerspectiveCamera(
75, // fov
this.element.clientWidth / this.element.clientHeight, // aspect
0.1, 1000 // near, far
);
this.renderer = new THREE.WebGLRenderer({
antialias: true,
alpha: true
});
this.renderer.setSize(
this.element.clientWidth,
this.element.clientHeight
);
this.element.appendChild(this.renderer.domElement);
const ambientLight = new THREE.AmbientLight(0xffffff, 1.5);
this.scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 1.5);
directionalLight.position.set(5, 5, 5);
this.scene.add(directionalLight);
this.controls = new OrbitControls(this.camera, this.element);
this.controls.minPolarAngle = Math.PI / 2;
this.controls.maxPolarAngle = Math.PI / 2;
this.controls.enableZoom = false;
this.controls.enablePan = false;
this.renderer.domElement.style.touchAction = 'pan-y';
this.loader = new GLTFLoader(this.loadingManager);
window.addEventListener("resize", () => {
const WIDTH = window.innerWidth;
const HEIGHT = window.innerHeight;
this.camera.aspect = WIDTH / HEIGHT;
this.camera.updateProjectionMatrix();
this.renderer.setSize(WIDTH, HEIGHT);
});
const MODEL = await (new Model(this.loader)).init(assets.model, this.scene);
const CAMERA = new Camera(this.camera, MODEL);
CAMERA.centerCamera((_box: any, size: any, center: any) => {
MODEL.position.sub(center);
MODEL.position.y -= size.y * 0.3;
});
CAMERA.positionCamera();
// Animation
this.animation = new Animation(MODEL, this.loader, assets.idle_animation);
const ANIMATION_QUEUE = new AnimationQueue(this.animation);
ANIMATION_QUEUE.onqueue(await this.animation.loadAnimation(assets.welcome_animation));
ANIMATION_QUEUE.startRandom();
this.animate();
}
// animate must be an arrow key so that "this"
// can be pointed to the Main instance
private animate = () => {
requestAnimationFrame(this.animate);
const delta = this.clock.getDelta();
if (this.animation) this.animation.update(delta);
this.controls.update();
this.renderer.render(this.scene, this.camera);
}
}

View File

@@ -1,28 +0,0 @@
import * as THREE from "three";
import { GLTF, GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
export default class Model {
private loader: GLTFLoader;
constructor(loader: GLTFLoader) {
this.loader = loader;
}
public async init(model_url: string, scene: THREE.Scene, isVisible: boolean = true): Promise<THREE.Object3D> {
const MODEL = await this.loadModel(`https://guezoloic.com/data/${model_url}`);
MODEL.visible = isVisible;
scene.add(MODEL);
return MODEL;
}
public loadModel(model_url: string): Promise<THREE.Object3D> {
return new Promise((resolve, reject) => {
this.loader.load(
model_url,
(gltf: GLTF) => resolve(gltf.scene),
undefined,
reject
);
});
}
}

View File

@@ -1,23 +0,0 @@
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import en from '../locales/en.json';
import fr from '../locales/fr.json';
const resources = { en: { translation: en }, fr: { translation: fr } };
const userLang = navigator.language.startsWith('fr') ? 'fr' : 'en';
i18n
.use(initReactI18next)
.init({
resources,
lng: userLang,
fallbackLng: 'en',
interpolation: { escapeValue: false },
})
;
document.documentElement.lang = i18n.language;
export default i18n;

View File

@@ -1,11 +0,0 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
// https://vite.dev/config/
export default defineConfig({
plugins: [
tailwindcss(),
react()
],
})