mirror of
https://github.com/guezoloic/website.git
synced 2026-03-28 18:03:50 +00:00
chore: replace old vitejs project to nextjs
This commit is contained in:
11
.github/dependabot.yml
vendored
11
.github/dependabot.yml
vendored
@@ -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"
|
|
||||||
98
.github/workflows/docker-publish.yml
vendored
98
.github/workflows/docker-publish.yml
vendored
@@ -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
56
.gitignore
vendored
@@ -1,25 +1,41 @@
|
|||||||
# Logs
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
logs
|
|
||||||
*.log
|
# 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*
|
npm-debug.log*
|
||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
pnpm-debug.log*
|
.pnpm-debug.log*
|
||||||
lerna-debug.log*
|
|
||||||
|
|
||||||
public
|
# env files (can opt-in for committing if needed)
|
||||||
node_modules
|
.env*
|
||||||
dist
|
|
||||||
dist-ssr
|
|
||||||
*.local
|
|
||||||
|
|
||||||
# Editor directories and files
|
# vercel
|
||||||
.vscode/*
|
.vercel
|
||||||
!.vscode/extensions.json
|
|
||||||
.idea
|
# typescript
|
||||||
.DS_Store
|
*.tsbuildinfo
|
||||||
*.suo
|
next-env.d.ts
|
||||||
*.ntvs*
|
|
||||||
*.njsproj
|
|
||||||
*.sln
|
|
||||||
*.sw?
|
|
||||||
|
|||||||
17
Dockerfile
17
Dockerfile
@@ -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;"]
|
|
||||||
37
README.md
37
README.md
@@ -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.
|
||||||
|
|||||||
@@ -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_]' }],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
])
|
|
||||||
87
index.html
87
index.html
@@ -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>
|
|
||||||
22
nginx.conf
22
nginx.conf
@@ -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
6004
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
49
package.json
49
package.json
@@ -1,43 +1,26 @@
|
|||||||
{
|
{
|
||||||
"name": "website",
|
"name": "website",
|
||||||
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.0",
|
|
||||||
"type": "module",
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "next dev",
|
||||||
"build": "vite build",
|
"build": "next build",
|
||||||
"lint": "eslint .",
|
"start": "next start",
|
||||||
"preview": "vite preview"
|
"lint": "eslint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/free-solid-svg-icons": "^7.0.0",
|
"next": "16.2.1",
|
||||||
"@fortawesome/react-fontawesome": "^0.2.3",
|
"react": "19.2.4",
|
||||||
"@heroicons/react": "^2.2.0",
|
"react-dom": "19.2.4"
|
||||||
"@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"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.30.1",
|
"@tailwindcss/postcss": "^4",
|
||||||
"@tailwindcss/vite": "^4.2.1",
|
"@types/node": "^20",
|
||||||
"@types/react": "^19.1.8",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19.1.6",
|
"@types/react-dom": "^19",
|
||||||
"@types/three": "^0.179.0",
|
"eslint": "^9",
|
||||||
"@vitejs/plugin-react": "^4.6.0",
|
"eslint-config-next": "16.2.1",
|
||||||
"autoprefixer": "^10.4.21",
|
"tailwindcss": "^4",
|
||||||
"eslint": "^9.30.1",
|
"typescript": "^5"
|
||||||
"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"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
41
src/App.tsx
41
src/App.tsx
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
import ReactDOM from 'react-dom/client';
|
|
||||||
import App from './App';
|
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById('root')!).render( <App /> );
|
|
||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -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"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -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"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -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": [
|
|
||||||
"",
|
|
||||||
"",
|
|
||||||
"",
|
|
||||||
""
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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": [
|
|
||||||
"",
|
|
||||||
"",
|
|
||||||
"",
|
|
||||||
""
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
@@ -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()
|
|
||||||
],
|
|
||||||
})
|
|
||||||
Reference in New Issue
Block a user