mirror of
https://github.com/guezoloic/website.git
synced 2026-03-28 18:03:50 +00:00
feat: rework entire project structure
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"
|
|
||||||
24
app/components/layout/Section.tsx
Normal file
24
app/components/layout/Section.tsx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
||||||
53
app/components/sections/About.tsx
Normal file
53
app/components/sections/About.tsx
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
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 "@app/components/layout/Section";
|
||||||
|
|
||||||
|
import content from "@app/data/content.json"
|
||||||
|
import Window from "@app/components/ui/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>
|
||||||
|
);
|
||||||
|
};
|
||||||
112
app/components/sections/Navbar.tsx
Normal file
112
app/components/sections/Navbar.tsx
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
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 "@app/components/ui/Button";
|
||||||
|
import content from "@app/data/content.json";
|
||||||
|
import { MenuState } from "@/app/page";
|
||||||
|
import Lang from "@app/components/ui/LangSwitcher"
|
||||||
|
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
82
app/components/sections/Projects.tsx
Normal file
82
app/components/sections/Projects.tsx
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import { motion } from "framer-motion";
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import Section from "@app/components/layout/Section";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import Window from "@app/components/ui/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>
|
||||||
|
);
|
||||||
|
};
|
||||||
54
app/components/sections/Skills.tsx
Normal file
54
app/components/sections/Skills.tsx
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { motion } from "framer-motion";
|
||||||
|
import * as SiIcons from "react-icons/si";
|
||||||
|
import Section from "@app/components/layout/Section";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
import content from "@app/data/content.json"
|
||||||
|
import Window from "@app/components/ui/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>
|
||||||
|
);
|
||||||
|
};
|
||||||
50
app/components/sections/Title.tsx
Normal file
50
app/components/sections/Title.tsx
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import { motion } from "framer-motion";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
import content from "@app/data/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>
|
||||||
|
);
|
||||||
|
};
|
||||||
32
app/components/three/Three.tsx
Normal file
32
app/components/three/Three.tsx
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import * as THREE from "three";
|
||||||
|
import Main from "@app/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>
|
||||||
|
);
|
||||||
|
};
|
||||||
38
app/components/ui/Button.tsx
Normal file
38
app/components/ui/Button.tsx
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
22
app/components/ui/LangSwitcher.tsx
Normal file
22
app/components/ui/LangSwitcher.tsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import Button from '@app/components/ui/Button';
|
||||||
|
|
||||||
|
import i18n from "@app/lib/i18n";
|
||||||
|
|
||||||
|
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>
|
||||||
|
)
|
||||||
|
}
|
||||||
43
app/components/ui/Window.tsx
Normal file
43
app/components/ui/Window.tsx
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
||||||
13
app/data/assets.json
Normal file
13
app/data/assets.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"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"
|
||||||
|
]
|
||||||
|
}
|
||||||
139
app/data/content.json
Normal file
139
app/data/content.json
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
{
|
||||||
|
"name": "GUEZO Loïc",
|
||||||
|
"career": "me.career",
|
||||||
|
"profile_image": "https://web-bucket.s3.fr-par.scw.cloud/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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -22,5 +22,10 @@
|
|||||||
body {
|
body {
|
||||||
background: var(--background);
|
background: var(--background);
|
||||||
color: var(--foreground);
|
color: var(--foreground);
|
||||||
font-family: Arial, Helvetica, sans-serif;
|
height: 100dvh;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
background-color: #1e3d2e;
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
|
/* font-family: Arial, Helvetica, sans-serif; */
|
||||||
}
|
}
|
||||||
|
|||||||
23
app/lib/i18n.ts
Normal file
23
app/lib/i18n.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import i18n from 'i18next';
|
||||||
|
import { initReactI18next } from 'react-i18next';
|
||||||
|
|
||||||
|
import en from '@app/locales/en.json';
|
||||||
|
import fr from '@app/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;
|
||||||
44
app/locales/en.json
Normal file
44
app/locales/en.json
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
{
|
||||||
|
"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": [
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
""
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
44
app/locales/fr.json
Normal file
44
app/locales/fr.json
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
{
|
||||||
|
"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": [
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
""
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
104
app/page.tsx
104
app/page.tsx
@@ -1,65 +1,49 @@
|
|||||||
import Image from "next/image";
|
"use client";
|
||||||
|
|
||||||
|
import Title from "@app/components/sections/Title";
|
||||||
|
import Navbar from "@app/components/sections/Navbar";
|
||||||
|
import About from "@app/components/sections/About";
|
||||||
|
import Skills from "@app/components/sections/Skills";
|
||||||
|
import Projects from "@app/components/sections/Projects";
|
||||||
|
|
||||||
|
import dynamic from 'next/dynamic';
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
import '@app/lib/i18n';
|
||||||
|
|
||||||
|
export type MenuState = {
|
||||||
|
about: boolean;
|
||||||
|
skills: boolean;
|
||||||
|
projects: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Three = dynamic(() => import('@app/components/three/Three'), {
|
||||||
|
ssr: false,
|
||||||
|
loading: () => <div className="fixed inset-0 bg-black" />
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
export default function Home() {
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col flex-1 items-center justify-center bg-zinc-50 font-sans dark:bg-black">
|
<div className="relative w-full h-screen">
|
||||||
<main className="flex flex-1 w-full max-w-3xl flex-col items-center justify-between py-32 px-16 bg-white dark:bg-black sm:items-start">
|
<Three />
|
||||||
<Image
|
<Title isOpen={isOpen}/>
|
||||||
className="dark:invert"
|
<Navbar state={state} setState={setState} isOpen={isOpen} />
|
||||||
src="/next.svg"
|
<About id="about" open={state.about} onClose={() => closeSection("about")} />
|
||||||
alt="Next.js logo"
|
<Skills id="skills" open={state.skills} onClose={() => closeSection("skills")} />
|
||||||
width={100}
|
<Projects id="projects" open={state.projects} onClose={() => closeSection("projects")} />
|
||||||
height={20}
|
|
||||||
priority
|
|
||||||
/>
|
|
||||||
<div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left">
|
|
||||||
<h1 className="max-w-xs text-3xl font-semibold leading-10 tracking-tight text-black dark:text-zinc-50">
|
|
||||||
To get started, edit the page.tsx file.
|
|
||||||
</h1>
|
|
||||||
<p className="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400">
|
|
||||||
Looking for a starting point or more instructions? Head over to{" "}
|
|
||||||
<a
|
|
||||||
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
||||||
className="font-medium text-zinc-950 dark:text-zinc-50"
|
|
||||||
>
|
|
||||||
Templates
|
|
||||||
</a>{" "}
|
|
||||||
or the{" "}
|
|
||||||
<a
|
|
||||||
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
||||||
className="font-medium text-zinc-950 dark:text-zinc-50"
|
|
||||||
>
|
|
||||||
Learning
|
|
||||||
</a>{" "}
|
|
||||||
center.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-4 text-base font-medium sm:flex-row">
|
|
||||||
<a
|
|
||||||
className="flex h-12 w-full items-center justify-center gap-2 rounded-full bg-foreground px-5 text-background transition-colors hover:bg-[#383838] dark:hover:bg-[#ccc] md:w-[158px]"
|
|
||||||
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
className="dark:invert"
|
|
||||||
src="/vercel.svg"
|
|
||||||
alt="Vercel logomark"
|
|
||||||
width={16}
|
|
||||||
height={16}
|
|
||||||
/>
|
|
||||||
Deploy Now
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
className="flex h-12 w-full items-center justify-center rounded-full border border-solid border-black/[.08] px-5 transition-colors hover:border-transparent hover:bg-black/[.04] dark:border-white/[.145] dark:hover:bg-[#1a1a1a] md:w-[158px]"
|
|
||||||
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
Documentation
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
92
app/three/animQueue.ts
Normal file
92
app/three/animQueue.ts
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
import * as THREE from "three";
|
||||||
|
import Animation from "./animation";
|
||||||
|
import assets from "@app/data/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();
|
||||||
|
}
|
||||||
|
}
|
||||||
81
app/three/animation.ts
Normal file
81
app/three/animation.ts
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
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://web-bucket.s3.fr-par.scw.cloud/${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
|
||||||
|
}
|
||||||
|
}
|
||||||
34
app/three/camera.ts
Normal file
34
app/three/camera.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
109
app/three/main.ts
Normal file
109
app/three/main.ts
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
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 "@app/data/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);
|
||||||
|
}
|
||||||
|
}
|
||||||
29
app/three/model.ts
Normal file
29
app/three/model.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
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://web-bucket.s3.fr-par.scw.cloud/${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.setCrossOrigin('anonymous');
|
||||||
|
this.loader.load(
|
||||||
|
model_url,
|
||||||
|
(gltf: GLTF) => resolve(gltf.scene),
|
||||||
|
undefined,
|
||||||
|
reject
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
239
package-lock.json
generated
239
package-lock.json
generated
@@ -8,9 +8,14 @@
|
|||||||
"name": "website",
|
"name": "website",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@heroicons/react": "^2.2.0",
|
||||||
|
"@types/three": "^0.183.1",
|
||||||
|
"framer-motion": "^12.38.0",
|
||||||
"next": "16.2.1",
|
"next": "16.2.1",
|
||||||
"react": "19.2.4",
|
"react": "^19.2.4",
|
||||||
"react-dom": "19.2.4"
|
"react-dom": "19.2.4",
|
||||||
|
"react-i18next": "^16.6.1",
|
||||||
|
"react-icons": "^5.6.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "^4",
|
"@tailwindcss/postcss": "^4",
|
||||||
@@ -19,7 +24,8 @@
|
|||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
"eslint": "^9",
|
"eslint": "^9",
|
||||||
"eslint-config-next": "16.2.1",
|
"eslint-config-next": "16.2.1",
|
||||||
"tailwindcss": "^4",
|
"tailwindcss": "^4.2.2",
|
||||||
|
"three": "^0.183.2",
|
||||||
"typescript": "^5"
|
"typescript": "^5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -228,6 +234,15 @@
|
|||||||
"node": ">=6.0.0"
|
"node": ">=6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@babel/runtime": {
|
||||||
|
"version": "7.29.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz",
|
||||||
|
"integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@babel/template": {
|
"node_modules/@babel/template": {
|
||||||
"version": "7.28.6",
|
"version": "7.28.6",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
|
||||||
@@ -276,6 +291,12 @@
|
|||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@dimforge/rapier3d-compat": {
|
||||||
|
"version": "0.12.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz",
|
||||||
|
"integrity": "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==",
|
||||||
|
"license": "Apache-2.0"
|
||||||
|
},
|
||||||
"node_modules/@emnapi/core": {
|
"node_modules/@emnapi/core": {
|
||||||
"version": "1.9.1",
|
"version": "1.9.1",
|
||||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.1.tgz",
|
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.1.tgz",
|
||||||
@@ -453,6 +474,15 @@
|
|||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@heroicons/react": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@heroicons/react/-/react-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-LMcepvRaS9LYHJGsF0zzmgKCUim/X3N/DQKc4jepAXJ7l8QxJ1PmxJzqplF2Z3FE4PqBAIGyJAQ/w4B5dsqbtQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">= 16 || ^19.0.0-rc"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@humanfs/core": {
|
"node_modules/@humanfs/core": {
|
||||||
"version": "0.19.1",
|
"version": "0.19.1",
|
||||||
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
|
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
|
||||||
@@ -1585,6 +1615,12 @@
|
|||||||
"tailwindcss": "4.2.2"
|
"tailwindcss": "4.2.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@tweenjs/tween.js": {
|
||||||
|
"version": "23.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz",
|
||||||
|
"integrity": "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@tybys/wasm-util": {
|
"node_modules/@tybys/wasm-util": {
|
||||||
"version": "0.10.1",
|
"version": "0.10.1",
|
||||||
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
|
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
|
||||||
@@ -1647,6 +1683,33 @@
|
|||||||
"@types/react": "^19.2.0"
|
"@types/react": "^19.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/stats.js": {
|
||||||
|
"version": "0.17.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.4.tgz",
|
||||||
|
"integrity": "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/three": {
|
||||||
|
"version": "0.183.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/three/-/three-0.183.1.tgz",
|
||||||
|
"integrity": "sha512-f2Pu5Hrepfgavttdye3PsH5RWyY/AvdZQwIVhrc4uNtvF7nOWJacQKcoVJn0S4f0yYbmAE6AR+ve7xDcuYtMGw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@dimforge/rapier3d-compat": "~0.12.0",
|
||||||
|
"@tweenjs/tween.js": "~23.1.3",
|
||||||
|
"@types/stats.js": "*",
|
||||||
|
"@types/webxr": ">=0.5.17",
|
||||||
|
"@webgpu/types": "*",
|
||||||
|
"fflate": "~0.8.2",
|
||||||
|
"meshoptimizer": "~1.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/webxr": {
|
||||||
|
"version": "0.5.24",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.24.tgz",
|
||||||
|
"integrity": "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||||
"version": "8.57.1",
|
"version": "8.57.1",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.1.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.1.tgz",
|
||||||
@@ -2235,6 +2298,12 @@
|
|||||||
"win32"
|
"win32"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"node_modules/@webgpu/types": {
|
||||||
|
"version": "0.1.69",
|
||||||
|
"resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.69.tgz",
|
||||||
|
"integrity": "sha512-RPmm6kgRbI8e98zSD3RVACvnuktIja5+yLgDAkTmxLr90BEwdTXRQWNLF3ETTTyH/8mKhznZuN5AveXYFEsMGQ==",
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
"node_modules/acorn": {
|
"node_modules/acorn": {
|
||||||
"version": "8.16.0",
|
"version": "8.16.0",
|
||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
|
||||||
@@ -3637,6 +3706,12 @@
|
|||||||
"reusify": "^1.0.4"
|
"reusify": "^1.0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/fflate": {
|
||||||
|
"version": "0.8.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
|
||||||
|
"integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/file-entry-cache": {
|
"node_modules/file-entry-cache": {
|
||||||
"version": "8.0.0",
|
"version": "8.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
|
||||||
@@ -3717,6 +3792,33 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/framer-motion": {
|
||||||
|
"version": "12.38.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.38.0.tgz",
|
||||||
|
"integrity": "sha512-rFYkY/pigbcswl1XQSb7q424kSTQ8q6eAC+YUsSKooHQYuLdzdHjrt6uxUC+PRAO++q5IS7+TamgIw1AphxR+g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"motion-dom": "^12.38.0",
|
||||||
|
"motion-utils": "^12.36.0",
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@emotion/is-prop-valid": "*",
|
||||||
|
"react": "^18.0.0 || ^19.0.0",
|
||||||
|
"react-dom": "^18.0.0 || ^19.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@emotion/is-prop-valid": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/function-bind": {
|
"node_modules/function-bind": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||||
@@ -3836,9 +3938,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/get-tsconfig": {
|
"node_modules/get-tsconfig": {
|
||||||
"version": "4.13.6",
|
"version": "4.13.7",
|
||||||
"resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz",
|
"resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.7.tgz",
|
||||||
"integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==",
|
"integrity": "sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -4022,6 +4124,47 @@
|
|||||||
"hermes-estree": "0.25.1"
|
"hermes-estree": "0.25.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/html-parse-stringify": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"void-elements": "3.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/i18next": {
|
||||||
|
"version": "25.10.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/i18next/-/i18next-25.10.4.tgz",
|
||||||
|
"integrity": "sha512-XsE/6eawy090meuFU0BTY9BtmWr1m9NSwLr0NK7/A04LA58wdAvDsi9WNOJ40Qb1E9NIPbvnVLZEN2fWDd3/3Q==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "individual",
|
||||||
|
"url": "https://www.locize.com/i18next"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "individual",
|
||||||
|
"url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "individual",
|
||||||
|
"url": "https://www.locize.com"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.29.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"typescript": "^5"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"typescript": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ignore": {
|
"node_modules/ignore": {
|
||||||
"version": "5.3.2",
|
"version": "5.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
||||||
@@ -5007,6 +5150,12 @@
|
|||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/meshoptimizer": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-Vix+QlA1YYT3FwmBBZ+49cE5y/b+pRrcXKqGpS5ouh33d3lSp2PoTpCw19E0cKDFWalembrHnIaZetf27a+W2g==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/micromatch": {
|
"node_modules/micromatch": {
|
||||||
"version": "4.0.8",
|
"version": "4.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
|
||||||
@@ -5044,6 +5193,21 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/motion-dom": {
|
||||||
|
"version": "12.38.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.38.0.tgz",
|
||||||
|
"integrity": "sha512-pdkHLD8QYRp8VfiNLb8xIBJis1byQ9gPT3Jnh2jqfFtAsWUA3dEepDlsWe/xMpO8McV+VdpKVcp+E+TGJEtOoA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"motion-utils": "^12.36.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/motion-utils": {
|
||||||
|
"version": "12.36.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.36.0.tgz",
|
||||||
|
"integrity": "sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/ms": {
|
"node_modules/ms": {
|
||||||
"version": "2.1.3",
|
"version": "2.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
@@ -5562,6 +5726,42 @@
|
|||||||
"react": "^19.2.4"
|
"react": "^19.2.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-i18next": {
|
||||||
|
"version": "16.6.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-16.6.1.tgz",
|
||||||
|
"integrity": "sha512-izjXh+AkBLy3h3xe3sh6Gg1flhFHc3UyzsMftMKYJr2Z7WvAZQIdjjpHypctN41zFoeLdJUNGDgP1+Qich2fYg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.29.2",
|
||||||
|
"html-parse-stringify": "^3.0.1",
|
||||||
|
"use-sync-external-store": "^1.6.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"i18next": ">= 25.6.2",
|
||||||
|
"react": ">= 16.8.0",
|
||||||
|
"typescript": "^5"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"react-dom": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react-native": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"typescript": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/react-icons": {
|
||||||
|
"version": "5.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.6.0.tgz",
|
||||||
|
"integrity": "sha512-RH93p5ki6LfOiIt0UtDyNg/cee+HLVR6cHHtW3wALfo+eOHTp8RnU2kRkI6E+H19zMIs03DyxUG/GfZMOGvmiA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-is": {
|
"node_modules/react-is": {
|
||||||
"version": "16.13.1",
|
"version": "16.13.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||||
@@ -6202,6 +6402,13 @@
|
|||||||
"url": "https://opencollective.com/webpack"
|
"url": "https://opencollective.com/webpack"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/three": {
|
||||||
|
"version": "0.183.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/three/-/three-0.183.2.tgz",
|
||||||
|
"integrity": "sha512-di3BsL2FEQ1PA7Hcvn4fyJOlxRRgFYBpMTcyOgkwJIaDOdJMebEFPA+t98EvjuljDx4hNulAGwF6KIjtwI5jgQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/tinyglobby": {
|
"node_modules/tinyglobby": {
|
||||||
"version": "0.2.15",
|
"version": "0.2.15",
|
||||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
|
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
|
||||||
@@ -6403,7 +6610,7 @@
|
|||||||
"version": "5.9.3",
|
"version": "5.9.3",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
@@ -6539,6 +6746,24 @@
|
|||||||
"punycode": "^2.1.0"
|
"punycode": "^2.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/use-sync-external-store": {
|
||||||
|
"version": "1.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
|
||||||
|
"integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/void-elements": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/which": {
|
"node_modules/which": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||||
|
|||||||
12
package.json
12
package.json
@@ -9,9 +9,14 @@
|
|||||||
"lint": "eslint"
|
"lint": "eslint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@heroicons/react": "^2.2.0",
|
||||||
|
"@types/three": "^0.183.1",
|
||||||
|
"framer-motion": "^12.38.0",
|
||||||
"next": "16.2.1",
|
"next": "16.2.1",
|
||||||
"react": "19.2.4",
|
"react": "^19.2.4",
|
||||||
"react-dom": "19.2.4"
|
"react-dom": "19.2.4",
|
||||||
|
"react-i18next": "^16.6.1",
|
||||||
|
"react-icons": "^5.6.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "^4",
|
"@tailwindcss/postcss": "^4",
|
||||||
@@ -20,7 +25,8 @@
|
|||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
"eslint": "^9",
|
"eslint": "^9",
|
||||||
"eslint-config-next": "16.2.1",
|
"eslint-config-next": "16.2.1",
|
||||||
"tailwindcss": "^4",
|
"tailwindcss": "^4.2.2",
|
||||||
|
"three": "^0.183.2",
|
||||||
"typescript": "^5"
|
"typescript": "^5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,8 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["./*"]
|
"@/*": ["./*"],
|
||||||
|
"@app/*": ["./app/*"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
|
|||||||
Reference in New Issue
Block a user