feat: rework entire project structure

This commit is contained in:
2026-03-23 00:04:04 +01:00
parent 6282951b88
commit 8185757933
26 changed files with 1414 additions and 86 deletions

View 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>
);
};

View 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>
);
}

View 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>
);
};

View 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>
);
};

View 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>
);
};