1
1
mirror of https://github.com/theoludwig/theoludwig.git synced 2024-12-08 00:44:30 +01:00

Compare commits

..

6 Commits

52 changed files with 1083 additions and 102 deletions

1
.gitignore vendored
View File

@ -15,7 +15,6 @@ coverage
out/ out/
dist/ dist/
build/ build/
apps/website/public/curriculum-vitae/
# misc # misc
.DS_Store .DS_Store

View File

@ -1,6 +1,6 @@
{ {
"name": "@repo/storybook", "name": "@repo/storybook",
"version": "4.0.0-staging.1", "version": "4.0.0",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {

View File

@ -0,0 +1,26 @@
import "@repo/config-tailwind/styles.css"
import type { LocaleProps } from "@repo/i18n/config"
import { Footer } from "@repo/ui/Layout/Footer"
import { Header } from "@repo/ui/Layout/Header"
import { ThemeProvider } from "@repo/ui/Layout/Header/SwitchTheme"
import { VERSION } from "@repo/utils/constants"
import { unstable_setRequestLocale } from "next-intl/server"
interface MainLayoutProps extends React.PropsWithChildren, LocaleProps {}
const MainLayout: React.FC<MainLayoutProps> = async (props) => {
const { children, params } = props
// Enable static rendering
unstable_setRequestLocale(params.locale)
return (
<ThemeProvider>
<Header />
{children}
<Footer version={VERSION} />
</ThemeProvider>
)
}
export default MainLayout

View File

@ -0,0 +1,21 @@
import "@repo/config-tailwind/styles.css"
import type { LocaleProps } from "@repo/i18n/config"
import { ThemeProvider } from "@repo/ui/Layout/Header/SwitchTheme"
import { unstable_setRequestLocale } from "next-intl/server"
interface CurriculumVitaeLayoutProps
extends React.PropsWithChildren,
LocaleProps {}
const CurriculumVitaeLayout: React.FC<CurriculumVitaeLayoutProps> = async (
props,
) => {
const { children, params } = props
// Enable static rendering
unstable_setRequestLocale(params.locale)
return <ThemeProvider forcedTheme="light">{children}</ThemeProvider>
}
export default CurriculumVitaeLayout

View File

@ -0,0 +1,16 @@
import type { LocaleProps } from "@repo/i18n/config"
import { CurriculumVitae } from "@repo/ui/CurriculumVitae"
import { unstable_setRequestLocale } from "next-intl/server"
interface CurriculumVitaeProps extends LocaleProps {}
const CurriculumVitaePage: React.FC<CurriculumVitaeProps> = (props) => {
const { params } = props
// Enable static rendering
unstable_setRequestLocale(params.locale)
return <CurriculumVitae />
}
export default CurriculumVitaePage

View File

@ -1,10 +1,6 @@
import "@repo/config-tailwind/styles.css" import "@repo/config-tailwind/styles.css"
import type { Locale, LocaleProps } from "@repo/i18n/config" import type { Locale, LocaleProps } from "@repo/i18n/config"
import { LOCALES } from "@repo/i18n/config" import { LOCALES } from "@repo/i18n/config"
import { Footer } from "@repo/ui/Layout/Footer"
import { Header } from "@repo/ui/Layout/Header"
import { ThemeProvider } from "@repo/ui/Layout/Header/SwitchTheme"
import { VERSION } from "@repo/utils/constants"
import type { Metadata } from "next" import type { Metadata } from "next"
import { NextIntlClientProvider } from "next-intl" import { NextIntlClientProvider } from "next-intl"
import { import {
@ -59,11 +55,7 @@ export const generateStaticParams = (): Array<{ locale: Locale }> => {
}) })
} }
interface LocaleLayoutProps extends React.PropsWithChildren { interface LocaleLayoutProps extends React.PropsWithChildren, LocaleProps {}
params: {
locale: Locale
}
}
const LocaleLayout: React.FC<LocaleLayoutProps> = async (props) => { const LocaleLayout: React.FC<LocaleLayoutProps> = async (props) => {
const { children, params } = props const { children, params } = props
@ -76,13 +68,9 @@ const LocaleLayout: React.FC<LocaleLayoutProps> = async (props) => {
return ( return (
<html lang={params.locale} suppressHydrationWarning> <html lang={params.locale} suppressHydrationWarning>
<body> <body>
<ThemeProvider> <NextIntlClientProvider messages={messages}>
<NextIntlClientProvider messages={messages}> {children}
<Header /> </NextIntlClientProvider>
{children}
<Footer version={VERSION} />
</NextIntlClientProvider>
</ThemeProvider>
</body> </body>
</html> </html>
) )

View File

@ -1,6 +1,6 @@
{ {
"name": "@repo/website", "name": "@repo/website",
"version": "4.0.0-staging.1", "version": "4.0.0",
"private": true, "private": true,
"type": "module", "type": "module",
"imports": { "imports": {

View File

@ -1 +0,0 @@
/*! modern-normalize v2.0.0 | MIT License | https://github.com/sindresorhus/modern-normalize */*,:before,:after{box-sizing:border-box}html{font-family:system-ui,Segoe UI,Roboto,Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji";line-height:1.15;-webkit-text-size-adjust:100%;-moz-tab-size:4;tab-size:4}body{margin:0}hr{height:0;color:inherit}abbr[title]{text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,select{text-transform:none}button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button}::-moz-focus-inner{border-style:none;padding:0}:-moz-focusring{outline:1px dotted ButtonText}:-moz-ui-invalid{box-shadow:none}legend{padding:0}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}body{font-family:Montserrat,Arial,"sans-serif";background:#f0f0f0;color:#333;line-height:1.42857143;font-size:14px}hr{margin-top:15px;margin-bottom:15px;border:0;border-top:1px solid #eee}p{margin:0}strong{font-weight:600}a{color:#337ab7;text-decoration:none}a:focus,a:hover{color:#23527c;text-decoration:underline}.link-disguise,.link-disguise:hover{color:inherit}.h1,.h2,.h3,h1,h2,h3{margin-top:20px;margin-bottom:10px}.h4,.h5,.h6,h4,h5,h6{margin-top:10px;margin-bottom:10px}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{font-family:inherit;font-weight:500;line-height:1.1;color:inherit}.h3,h3{font-size:24px}.h4,h4{font-size:18px}.h5,h5{font-size:14px}.container-fluid{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}.row{margin-right:-15px;margin-left:-15px}.clear-margin{margin:0}.relative{position:relative}.center-block{display:block;margin-right:auto;margin-left:auto}.text-center{text-align:center}.text-muted{color:#777}.text-uppercase{text-transform:uppercase}.list-unstyled{padding-left:0;list-style:none}.main{padding:5px}.title{font-weight:600}.profile-card-wrapper{position:relative}.card-wrapper{float:none!important;padding:5px}.profile-card-wrapper .profile-card{padding:10px}.card{background:#fff;border-radius:3px;padding:10px 0}.profile-pic{padding:10px 0}.profile-pic img{width:100px;height:100px;border-radius:50%;vertical-align:middle;border:0}.contact-details{display:flex;justify-content:center}.contact-details .detail{position:relative;min-height:1px;padding:10px}.social-links{line-height:2.5}.experience-description{margin-top:10px}.background-details .detail{display:table}.background-details .detail .icon,.background-details .detail .info{display:table-cell}.background-details .detail .icon{color:#707070}.background-details .detail .icon{min-width:45px;max-width:45px;text-align:center}.icon img{width:20px;height:20px}.background-details .detail .mobile-title{display:none}.card-nested{min-height:0;border-width:1px 0 0 0}.card-skills{position:relative}.labels{line-height:2}.space-top{margin-top:10px}.label{display:inline;padding:.2em .6em .3em;font-size:75%;font-weight:600;line-height:1;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25em}.label-keyword{display:inline-block;font-size:.9em;padding:5px;border:1px solid #357ebd;margin-right:5px}.label-keyword p{margin:0}.section-separated{display:flex}

View File

@ -1 +0,0 @@
(function(){const o=document.createElement("link").relList;if(o&&o.supports&&o.supports("modulepreload"))return;for(const e of document.querySelectorAll('link[rel="modulepreload"]'))r(e);new MutationObserver(e=>{for(const t of e)if(t.type==="childList")for(const c of t.addedNodes)c.tagName==="LINK"&&c.rel==="modulepreload"&&r(c)}).observe(document,{childList:!0,subtree:!0});function s(e){const t={};return e.integrity&&(t.integrity=e.integrity),e.referrerPolicy&&(t.referrerPolicy=e.referrerPolicy),e.crossOrigin==="use-credentials"?t.credentials="include":e.crossOrigin==="anonymous"?t.credentials="omit":t.credentials="same-origin",t}function r(e){if(e.ep)return;e.ep=!0;const t=s(e);fetch(e.href,t)}})();const i="31",l="03",u="2003",d=`${u}-${l}-${i}`,f=new Date(d),a=n=>{const o=new Date;let s=o.getFullYear()-n.getFullYear();const r=o.getMonth()-n.getMonth();return(r<0||r===0&&o.getDate()<n.getDate())&&s--,s},g=document.getElementById("year-old");g.textContent=a(f).toString();

File diff suppressed because one or more lines are too long

View File

@ -1,6 +1,6 @@
{ {
"name": "repo", "name": "repo",
"version": "4.0.0-staging.1", "version": "4.0.0",
"private": true, "private": true,
"type": "module", "type": "module",
"packageManager": "pnpm@9.5.0+sha512.140036830124618d624a2187b50d04289d5a087f326c9edfc0ccd733d76c4f52c3a313d4fc148794a2a9d81553016004e6742e8cf850670268a7387fc220c903", "packageManager": "pnpm@9.5.0+sha512.140036830124618d624a2187b50d04289d5a087f326c9edfc0ccd733d76c4f52c3a313d4fc148794a2a9d81553016004e6742e8cf850670268a7387fc220c903",

View File

@ -1,6 +1,6 @@
{ {
"name": "@repo/blog", "name": "@repo/blog",
"version": "4.0.0-staging.1", "version": "4.0.0",
"private": true, "private": true,
"type": "module", "type": "module",
"exports": { "exports": {

View File

@ -1,6 +1,6 @@
{ {
"name": "@repo/eslint-config", "name": "@repo/eslint-config",
"version": "4.0.0-staging.1", "version": "4.0.0",
"private": true, "private": true,
"main": ".eslintrc.json", "main": ".eslintrc.json",
"files": [ "files": [

View File

@ -1,6 +1,6 @@
{ {
"name": "@repo/config-tailwind", "name": "@repo/config-tailwind",
"version": "4.0.0-staging.1", "version": "4.0.0",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "./tailwind.config.js", "main": "./tailwind.config.js",

View File

@ -160,3 +160,144 @@ code .line:last-child {
white-space: normal !important; white-space: normal !important;
width: 100% !important; width: 100% !important;
} }
.curriculum-vitae {
background: #f0f0f0;
color: #333;
font-family: Arial, sans-serif;
hr {
margin-top: 15px;
margin-bottom: 15px;
border: 0;
border-top: 1px solid #eee;
}
a {
color: #337ab7;
text-decoration: none;
}
a:focus,
a:hover {
color: #23527c;
text-decoration: underline;
}
.link-disguise {
color: inherit;
}
.link-disguise:hover {
color: inherit;
}
.h1,
.h2,
.h3 {
margin-top: 20px;
margin-bottom: 10px;
}
.h4,
.h5,
.h6 {
margin-top: 10px;
margin-bottom: 10px;
}
.h1,
.h2,
.h3,
.h4,
.h5,
.h6 {
font-family: inherit;
line-height: 1.1;
color: inherit;
}
.h3 {
font-size: 24px;
}
.h4 {
font-size: 18px;
}
.h5 {
font-size: 14px;
}
.text-muted {
color: #414141;
}
.list-unstyled {
padding-left: 0;
list-style: none;
}
.card-wrapper {
float: none !important;
padding: 5px;
}
.card {
background: white;
border-radius: 3px;
padding: 10px 0;
}
.profile-pic {
padding: 10px 0;
}
.profile-pic img {
width: 100px;
height: 100px;
border-radius: 50%;
vertical-align: middle;
border: 0;
}
.social-links {
line-height: 2.5;
}
.background-details .detail {
display: table;
}
.background-details .detail .icon,
.background-details .detail .info {
display: table-cell;
}
.background-details .detail .icon {
color: #707070;
}
.background-details .detail .icon {
min-width: 45px;
max-width: 45px;
text-align: center;
}
.icon img {
width: 20px;
height: 20px;
}
.background-details .detail .mobile-title {
display: none;
}
.card-nested {
min-height: 0;
}
.labels {
line-height: 2;
}
.label {
display: inline;
padding: 0.2em 0.6em 0.3em;
font-size: 75%;
font-weight: 600;
line-height: 1;
text-align: center;
white-space: nowrap;
vertical-align: baseline;
border-radius: 0.25em;
}
.label-keyword {
display: inline-block;
font-size: 0.9em;
padding: 5px;
border: 1px solid #357ebd;
margin-right: 5px;
}
.label-keyword p {
margin: 0;
}
}

View File

@ -1,6 +1,6 @@
{ {
"name": "@repo/config-typescript", "name": "@repo/config-typescript",
"version": "4.0.0-staging.1", "version": "4.0.0",
"private": true, "private": true,
"files": [ "files": [
"tsconfig.json" "tsconfig.json"

View File

@ -1,6 +1,6 @@
{ {
"name": "@repo/i18n", "name": "@repo/i18n",
"version": "4.0.0-staging.1", "version": "4.0.0",
"private": true, "private": true,
"type": "module", "type": "module",
"exports": { "exports": {
@ -12,7 +12,8 @@
}, },
"scripts": { "scripts": {
"lint:eslint": "eslint src --max-warnings 0 --report-unused-disable-directives", "lint:eslint": "eslint src --max-warnings 0 --report-unused-disable-directives",
"lint:typescript": "tsc --noEmit" "lint:typescript": "tsc --noEmit",
"test": "vitest run"
}, },
"dependencies": { "dependencies": {
"deepmerge": "catalog:", "deepmerge": "catalog:",
@ -28,6 +29,7 @@
"@types/react-dom": "catalog:", "@types/react-dom": "catalog:",
"@total-typescript/ts-reset": "catalog:", "@total-typescript/ts-reset": "catalog:",
"eslint": "catalog:", "eslint": "catalog:",
"typescript": "catalog:" "typescript": "catalog:",
"vitest": "catalog:"
} }
} }

View File

@ -0,0 +1,7 @@
import { expectTypeOf, test } from "vitest"
import en from "../translations/en-US.json"
import fr from "../translations/fr-FR.json"
test("translations types should match", () => {
expectTypeOf(en).toEqualTypeOf(fr)
})

View File

@ -0,0 +1,181 @@
{
"$schema": "https://raw.githubusercontent.com/jsonresume/resume-schema/v1.0.0/schema.json",
"basics": {
"name": "Théo LUDWIG",
"label": "Développeur Full Stack • Étudiant",
"image": "https://theoludwig.fr/images/logo_background.png",
"email": "contact@theoludwig.fr",
"age": "31/03/2003",
"location": {
"address": "Alsace, France",
},
"url": "https://theoludwig.fr",
"summary": "Je me demande constamment comment améliorer notre présent, afin de rendre notre futur meilleur, particulièrement grâce aux progrès de l'informatique. <br /> Ma priorité réside dans la création d'expériences utilisateurs (UX) intuitives, répondant aux besoins des utilisateurs de la manière la plus efficace que possible.",
},
"education": [
{
"startDate": "2023",
"endDate": "2024",
"studyType": "Bachelor Universitaire de Technologie (BUT) Informatique",
"institution": "IUT Robert Schuman à Illkirch-Graffenstaden",
"score": "3ème année",
"courses": [
"Développement Web en Node.js et React.js",
"Intégration/Déploiement Continue et Docker",
"Complexité Algorithmique Théorique et Pratique en C++",
// "Projet développement LLM (Large Language Model) et NLP (Natural Language Processing)",
"Base de données NoSQL (Redis, MongoDB, Cassandra)",
],
},
{
"startDate": "2022",
"endDate": "2023",
"studyType": "Bachelor Universitaire de Technologie (BUT) Informatique",
"institution": "IUT Robert Schuman à Illkirch-Graffenstaden",
"score": "2ème année",
"courses": [
"Développement Web avec le framework Laravel en PHP",
"Qualité de développement et Tests automatisés",
"Patrons et Principes de conceptions (Code maintenable et réutilisable) en UML",
"Programmation systèmes en C (Multi-Thread, Serveur/Client UDP/TCP)",
"Sécurisation des accès à la base de données et PL/SQL",
],
},
{
"startDate": "2021",
"endDate": "2022",
"studyType": "Bachelor Universitaire de Technologie (BUT) Informatique",
"institution": "IUT Robert Schuman à Illkirch-Graffenstaden",
"score": "1ère année",
"courses": [
"Développement Orientée Objet en Java",
"Programmation systèmes en C (Allocation mémoire, Pointeurs, Structures)",
"Développement d'application Windows Forms (.NET Framework) en C#",
"Base de données relationnelles et langage SQL",
],
},
{
"startDate": "2019",
"endDate": "2021",
"studyType": "Baccalauréat Général (Mathématiques et Numériques Sciences Informatiques)",
"institution": "Lycée Heinrich Nessel à Haguenau",
"score": "Mention Assez Bien",
},
// {
// "startDate": "2014",
// "endDate": "2018",
// "studyType": "Diplôme national du brevet",
// "institution": "Collège Gustave Doré à Hochfelden",
// "score": "Mention Bien"
// }
],
"work": [
{
"summary": "Développement de WebSurg, une université virtuelle consacrée à la formation médico-chirurgicale, en React.js/Next.js et API Platform avec Symfony.",
"website": "https://ircad.fr/",
"name": "IRCAD",
"location": "1 Place de l'Hôpital, 67000 Strasbourg",
"position": "Alternant Développeur Web Full Stack",
"startDate": "2023-08-28",
"endDate": "2024-09-02",
"duration": "1 an",
},
{
"summary": "Développement d'un outil GED (Gestion Électronique de Documents) en React.js, Laravel et GraphQL.",
"website": "https://numerize.com/",
"name": "Numerize",
"location": "4 Rue Sophie Germain, 67720 Hœrdt",
"position": "Stagiaire Développeur Web Full Stack",
"startDate": "2023-04-11",
"endDate": "2023-07-26",
"duration": "4 mois",
},
// {
// "summary": "Agent administratif - Numérisation et archivage des plans électriques initialement sous format papier calque.",
// "website": "https://www.es.fr/",
// "name": "ÉS (Électricité de Strasbourg)",
// "location": "5 Rue André Marie Ampère, 67450 Mundolsheim",
// "position": "Emploi d'été en qualité d'agent administratif",
// "startDate": "2021-07-07",
// "endDate": "2021-07-30",
// "duration": "1 mois"
// },
{
"summary": "Développement d'un site web pour trouver un restaurant à la pause repas.",
"website": "https://itpartners.fr/",
"name": "Tribe | IT Partners",
"location": "16 Rue du Parc, 67205 Oberhausbergen",
"position": "Stage initiation métier développeur web",
"startDate": "2019-06-17",
"endDate": "2019-06-21",
"duration": "1 semaine",
},
{
"description": "interests",
"summary": "Développement site web en React.js et Strapi.<br /> Classé n°1 en France sur le Défi de l'entreprise <a href=\"https://toolpad.fr/\">ToolPad</a>.",
"website": "https://nuitdelinfo.com/",
"name": "La Nuit de l'info 2021",
"position": "Participation en équipe de 5 personnes",
"startDate": "2021-12-02",
"endDate": "2021-12-03",
"duration": "1 semaine",
},
{
"description": "interests",
"summary": "Hackathon développement d'une landing page et web scraping.",
"website": "https://wildcodeschool.fr/",
"name": "Wild Code School",
"location": "32 Rue du Bass. d'Austerlitz, 67100 Strasbourg",
"position": "Initiation métier Développeur web",
"startDate": "2019-06-24",
"endDate": "2019-06-28",
"duration": "1 semaine",
},
// {
// "summary": "Apprentissage du métier \"Chargé de communication\" et des logiciels de graphisme tels que \"Adobe Photoshop\".",
// "website": "https://es.fr/",
// "name": "ÉS (Électricité de Strasbourg)",
// "location": "26 Bd du Président-Wilson, 67000 Strasbourg",
// "position": "Stage de découverte (3ème)",
// "startDate": "2018-02-19",
// "endDate": "2018-02-23",
// "duration": "1 semaine"
// }
],
"interests": [
{
"name": "Enthousiaste de l'Open-Source",
},
{
"name": "Passionné de High-Tech",
},
],
"skills": [
{
"keywords": ["JavaScript/TypeScript", "Python", "C/C++", "PHP"],
"name": "Langages de programmation",
},
{
"keywords": ["HTML", "CSS", "Tailwind CSS", "React.js/Next.js"],
"name": "Frontend",
},
{
"keywords": ["Laravel", "Node.js", "Fastify", "PostgreSQL"],
"name": "Backend",
},
{
"keywords": [
"GNU/Linux",
"Arch Linux",
"Visual Studio Code",
"Git",
"Docker",
],
"name": "Logiciels et outils",
},
{
"keywords": ["Permis B", "Anglais"],
"name": "Autres",
},
],
}

View File

@ -54,7 +54,9 @@
"programming-languages": "Programming languages", "programming-languages": "Programming languages",
"frontend": "Frontend", "frontend": "Frontend",
"backend": "Backend", "backend": "Backend",
"software-tools": "Software and tools" "software-tools": "Software and tools",
"others": "Others",
"driving-license": "Driving license"
}, },
"portfolio": { "portfolio": {
"title": "Portfolio", "title": "Portfolio",
@ -71,5 +73,79 @@
"title": "Open-Source", "title": "Open-Source",
"description": "Most famous open source projects I contributed to." "description": "Most famous open source projects I contributed to."
} }
},
"curriculum-vitae": {
"description": "Developer Full Stack • Student",
"about": {
"title": "About",
"description": "I constantly wonder how to improve our present, to make our future better, particularly thanks to the advancements in computer science. <br></br> My priority is to craft intuitive user experiences (UX), that meet the needs of the users in the most efficient way possible."
},
"education": {
"title": "Studies",
"iut": {
"study-type": "University Bachelor of Technology (BUT) Computer Science",
"institution": "IUT Robert Schuman in Illkirch-Graffenstaden",
"years": {
"2023-2024": {
"title": "2023 - 2024",
"description": "3rd year",
"courses": {
"web": "Web development in Node.js and React.js",
"ci-cd": "Continuous Integration/Deployment (CI/CD) and Docker",
"complexity-algorithms": "Theoretical and Practical Algorithmic Complexity in C++",
"no-sql": "NoSQL database (Redis, MongoDB, Cassandra)"
}
},
"2022-2023": {
"title": "2022 - 2023",
"description": "2nd year",
"courses": {
"web": "Web development with the Laravel framework in PHP",
"tests": "Development Quality and Automated Testing",
"clean-code": "Design Patterns and Principles (Maintainable and Reusable Code) in UML",
"systems-c": "Systems programming in C (Multi-Thread, Server/Client UDP/TCP)",
"sql-security": "Securing database access and PL/SQL"
}
},
"2021-2022": {
"title": "2021 - 2022",
"description": "1st year",
"courses": {
"java": "Object Oriented Development in Java",
"systems-c": "Systems programming in C (Memory allocation, Pointers, Structures)",
"windows-forms": "Windows Forms (.NET Framework) Application Development in C#",
"sql": "Relational database and SQL language"
}
}
}
},
"lycee": {
"study-type": "General Baccalaureate (Mathematics and Computer Science)",
"institution": "Heinrich Nessel High School in Haguenau",
"score": "Mention Quite Good",
"years": {
"2019-2021": {
"title": "2019 - 2021"
}
}
}
},
"work": {
"title": "Work experiences",
"ircad": {
"summary": "Development of WebSurg, a virtual university dedicated to medical-surgical training, in React.js/Next.js and API Platform with Symfony.",
"position": "Full Stack Web Developer Apprentice",
"duration": "1 year"
},
"numerize": {
"summary": "Development of an DMS (Document Management System) tool in React.js, Laravel and GraphQL.",
"position": "Full Stack Web Developer Intern",
"duration": "4 months"
}
},
"interests": {
"open-source": "Open-Source Enthusiast",
"high-tech": "Passionate about High-Tech"
}
} }
} }

View File

@ -54,7 +54,9 @@
"programming-languages": "Langages de programmation", "programming-languages": "Langages de programmation",
"frontend": "Frontend", "frontend": "Frontend",
"backend": "Backend", "backend": "Backend",
"software-tools": "Logiciels et outils" "software-tools": "Logiciels et outils",
"others": "Autres",
"driving-license": "Permis B"
}, },
"portfolio": { "portfolio": {
"title": "Portfolio", "title": "Portfolio",
@ -71,5 +73,79 @@
"title": "Open-Source", "title": "Open-Source",
"description": "Projets open source les plus célèbres auxquels j'ai contribué." "description": "Projets open source les plus célèbres auxquels j'ai contribué."
} }
},
"curriculum-vitae": {
"description": "Développeur Full Stack • Étudiant",
"about": {
"title": "À propos",
"description": "Je me demande constamment comment améliorer notre présent, afin de rendre notre futur meilleur, particulièrement grâce aux progrès de l'informatique. <br></br> Ma priorité réside dans la création d'expériences utilisateurs (UX) intuitives, répondant aux besoins des utilisateurs de la manière la plus efficace que possible."
},
"education": {
"title": "Études",
"iut": {
"study-type": "Bachelor Universitaire de Technologie (BUT) Informatique",
"institution": "IUT Robert Schuman à Illkirch-Graffenstaden",
"years": {
"2023-2024": {
"title": "2023 - 2024",
"description": "3ème année",
"courses": {
"web": "Développement Web en Node.js et React.js",
"ci-cd": "Intégration/Déploiement Continue et Docker",
"complexity-algorithms": "Complexité Algorithmique Théorique et Pratique en C++",
"no-sql": "Base de données NoSQL (Redis, MongoDB, Cassandra)"
}
},
"2022-2023": {
"title": "2022 - 2023",
"description": "2ème année",
"courses": {
"web": "Développement Web avec le framework Laravel en PHP",
"tests": "Qualité de développement et Tests automatisés",
"clean-code": "Patrons et Principes de conceptions (Code maintenable et réutilisable) en UML",
"systems-c": "Programmation systèmes en C (Multi-Thread, Serveur/Client UDP/TCP)",
"sql-security": "Sécurisation des accès à la base de données et PL/SQL"
}
},
"2021-2022": {
"title": "2021 - 2022",
"description": "1ère année",
"courses": {
"java": "Développement Orientée Objet en Java",
"systems-c": "Programmation systèmes en C (Allocation mémoire, Pointeurs, Structures)",
"windows-forms": "Développement d'application Windows Forms (.NET Framework) en C#",
"sql": "Base de données relationnelles et langage SQL"
}
}
}
},
"lycee": {
"study-type": "Baccalauréat Général (Mathématiques et Numériques Sciences Informatiques)",
"institution": "Lycée Heinrich Nessel à Haguenau",
"score": "Mention Assez Bien",
"years": {
"2019-2021": {
"title": "2019 - 2021"
}
}
}
},
"work": {
"title": "Expériences professionnelles",
"ircad": {
"summary": "Développement de WebSurg, une université virtuelle consacrée à la formation médico-chirurgicale, en React.js/Next.js et API Platform avec Symfony.",
"position": "Apprenti Développeur Web Full Stack",
"duration": "1 an"
},
"numerize": {
"summary": "Développement d'un outil GED (Gestion Électronique de Documents) en React.js, Laravel et GraphQL.",
"position": "Stagiaire Développeur Web Full Stack",
"duration": "4 mois"
}
},
"interests": {
"open-source": "Enthousiaste de l'Open-Source",
"high-tech": "Passionné de High-Tech"
}
} }
} }

View File

@ -0,0 +1,9 @@
import { defineConfig } from "vitest/config"
export default defineConfig({
test: {
typecheck: {
enabled: true,
},
},
})

View File

@ -1,6 +1,6 @@
{ {
"name": "@repo/react-hooks", "name": "@repo/react-hooks",
"version": "4.0.0-staging.1", "version": "4.0.0",
"private": true, "private": true,
"type": "module", "type": "module",
"exports": { "exports": {

View File

@ -1,9 +1,10 @@
{ {
"name": "@repo/ui", "name": "@repo/ui",
"version": "4.0.0-staging.1", "version": "4.0.0",
"private": true, "private": true,
"type": "module", "type": "module",
"exports": { "exports": {
"./CurriculumVitae": "./src/CurriculumVitae/CurriculumVitae.tsx",
"./Design/Button": "./src/Design/Button/Button.tsx", "./Design/Button": "./src/Design/Button/Button.tsx",
"./Design/Link": "./src/Design/Link/Link.tsx", "./Design/Link": "./src/Design/Link/Link.tsx",
"./Design/Spinner": "./src/Design/Spinner/Spinner.tsx", "./Design/Spinner": "./src/Design/Spinner/Spinner.tsx",

View File

@ -0,0 +1,16 @@
import type { Meta, StoryObj } from "@storybook/react"
import { CurriculumVitae as CurriculumVitaeComponent } from "./CurriculumVitae"
const meta = {
title: "Curriculum Vitae/CurriculumVitae",
component: CurriculumVitaeComponent,
} satisfies Meta<typeof CurriculumVitaeComponent>
export default meta
type Story = StoryObj<typeof meta>
export const CurriculumVitae: Story = {
args: {},
}

View File

@ -0,0 +1,40 @@
import { CurriculumVitaeAbout } from "./CurriculumVitaeAbout"
import { CurriculumVitaeEducation } from "./CurriculumVitaeEducation"
import { CurriculumVitaeInterests } from "./CurriculumVitaeInterests"
import { CurriculumVitaeProfile } from "./CurriculumVitaeProfile"
import { CurriculumVitaeSkills } from "./CurriculumVitaeSkills"
import { CurriculumVitaeWork } from "./CurriculumVitaeWork"
export interface CurriculumVitaeProps {}
export const CurriculumVitae: React.FC<CurriculumVitaeProps> = () => {
return (
<main className="curriculum-vitae mx-auto px-4 text-sm">
<div className="-mx-4 p-2">
<section className="col-md-3 card-wrapper relative">
<CurriculumVitaeProfile />
<div className="card background-card">
<div className="background-details">
<CurriculumVitaeAbout />
<hr />
<section className="flex">
<CurriculumVitaeEducation />
<CurriculumVitaeSkills />
</section>
<hr />
<section className="flex">
<CurriculumVitaeWork />
<CurriculumVitaeInterests />
</section>
</div>
</div>
</section>
</div>
</main>
)
}

View File

@ -0,0 +1,19 @@
import { useTranslations } from "next-intl"
import { FaUser } from "react-icons/fa"
import { CurriculumVitaeSection } from "./CurriculumVitaeSection"
export interface CurriculumVitaeAboutProps {}
export const CurriculumVitaeAbout: React.FC<CurriculumVitaeAboutProps> = () => {
const t = useTranslations()
return (
<CurriculumVitaeSection
id="about"
title={t("curriculum-vitae.about.title")}
icon={<FaUser size={24} />}
>
<p>{t.rich("curriculum-vitae.about.description")}</p>
</CurriculumVitaeSection>
)
}

View File

@ -0,0 +1,110 @@
import { useTranslations } from "next-intl"
import { FaGraduationCap } from "react-icons/fa"
import { CurriculumVitaeSection } from "./CurriculumVitaeSection"
export interface CurriculumVitaeEducationProps {}
export const CurriculumVitaeEducation: React.FC<
CurriculumVitaeEducationProps
> = () => {
const t = useTranslations()
const educations = [
{
years: t("curriculum-vitae.education.iut.years.2023-2024.title"),
studyType: t("curriculum-vitae.education.iut.study-type"),
institution: t("curriculum-vitae.education.iut.institution"),
score: t("curriculum-vitae.education.iut.years.2023-2024.description"),
courses: [
t("curriculum-vitae.education.iut.years.2023-2024.courses.web"),
t("curriculum-vitae.education.iut.years.2023-2024.courses.ci-cd"),
t(
"curriculum-vitae.education.iut.years.2023-2024.courses.complexity-algorithms",
),
t("curriculum-vitae.education.iut.years.2023-2024.courses.no-sql"),
],
},
{
years: t("curriculum-vitae.education.iut.years.2022-2023.title"),
studyType: t("curriculum-vitae.education.iut.study-type"),
institution: t("curriculum-vitae.education.iut.institution"),
score: t("curriculum-vitae.education.iut.years.2022-2023.description"),
courses: [
t("curriculum-vitae.education.iut.years.2022-2023.courses.web"),
t("curriculum-vitae.education.iut.years.2022-2023.courses.tests"),
t("curriculum-vitae.education.iut.years.2022-2023.courses.clean-code"),
t("curriculum-vitae.education.iut.years.2022-2023.courses.systems-c"),
t(
"curriculum-vitae.education.iut.years.2022-2023.courses.sql-security",
),
],
},
{
years: t("curriculum-vitae.education.iut.years.2021-2022.title"),
studyType: t("curriculum-vitae.education.iut.study-type"),
institution: t("curriculum-vitae.education.iut.institution"),
score: t("curriculum-vitae.education.iut.years.2021-2022.description"),
courses: [
t("curriculum-vitae.education.iut.years.2021-2022.courses.java"),
t("curriculum-vitae.education.iut.years.2021-2022.courses.systems-c"),
t(
"curriculum-vitae.education.iut.years.2021-2022.courses.windows-forms",
),
t("curriculum-vitae.education.iut.years.2021-2022.courses.sql"),
],
},
{
years: t("curriculum-vitae.education.lycee.years.2019-2021.title"),
studyType: t("curriculum-vitae.education.lycee.study-type"),
institution: t("curriculum-vitae.education.lycee.institution"),
score: t("curriculum-vitae.education.lycee.score"),
courses: [],
},
]
return (
<CurriculumVitaeSection
id="education"
title={t("curriculum-vitae.education.title")}
icon={<FaGraduationCap size={24} />}
>
<ul className="list-unstyled m-0">
{educations.map((education) => {
return (
<li key={education.years} className="card card-nested">
<div className="content">
<p className="relative m-0">
<strong>{education.studyType}</strong>
</p>
<p className="relative m-0">
<strong>{education.score}</strong>
</p>
<p className="text-muted m-0">{education.institution}</p>
<p className="text-muted m-0">
<small>{education.years}</small>
</p>
{education.courses.length > 0 ? (
<ul
style={{
paddingInlineStart: 20,
}}
>
{education.courses.map((course) => {
return <li key={course}>{course}</li>
})}
</ul>
) : (
<></>
)}
</div>
</li>
)
})}
</ul>
</CurriculumVitaeSection>
)
}

View File

@ -0,0 +1,36 @@
import { useTranslations } from "next-intl"
import { FaHeart } from "react-icons/fa"
import { CurriculumVitaeSection } from "./CurriculumVitaeSection"
export interface CurriculumVitaeInterestsProps {}
export const CurriculumVitaeInterests: React.FC<
CurriculumVitaeInterestsProps
> = () => {
const t = useTranslations()
const interests = [
t("curriculum-vitae.interests.open-source"),
t("curriculum-vitae.interests.high-tech"),
]
return (
<CurriculumVitaeSection
id="interests"
title={t("home.interests.title")}
icon={<FaHeart size={24} />}
>
<ul className="list-unstyled m-0">
{interests.map((interest) => {
return (
<li key={interest} className="card card-nested">
<p>
<strong>{interest}</strong>
</p>
</li>
)
})}
</ul>
</CurriculumVitaeSection>
)
}

View File

@ -0,0 +1,59 @@
import { Link } from "@repo/i18n/navigation"
import { useTranslations } from "next-intl"
import Image from "next/image"
import { BirthDate } from "../Home/About/AboutList/BirthDate"
import { Locales } from "../Layout/Header/Locales/Locales"
export interface CurriculumVitaeProfileProps {}
export const CurriculumVitaeProfile: React.FC<
CurriculumVitaeProfileProps
> = () => {
const t = useTranslations()
return (
<div className="card p-2">
<div className="mx-2 flex print:hidden">
<Locales />
</div>
<div className="profile-pic-container">
<div className="profile-pic">
<Image
className="mx-auto block"
alt={t("meta.title")}
src="/images/logo_background.webp"
width={800}
height={800}
/>
</div>
<div className="name-and-profession text-center">
<h1 className="h3">
<strong>{t("meta.title")}</strong>
</h1>
<h2 className="text-muted h5">{t("curriculum-vitae.description")}</h2>
<h2 className="text-muted h5">
<BirthDate />
</h2>
<h2 className="text-muted h5">{t("home.about.nationality.value")}</h2>
</div>
</div>
<div className="flex justify-center">
<div className="relative px-3">
<span className="info">
<a className="link-disguise" href="mailto:contact@theoludwig.fr">
contact@theoludwig.fr
</a>
</span>
</div>
<div className="detail">
<span className="info">
<Link className="link-disguise" href="/">
https://theoludwig.fr/
</Link>
</span>
</div>
</div>
<hr />
</div>
)
}

View File

@ -0,0 +1,23 @@
export interface CurriculumVitaeSectionProps extends React.PropsWithChildren {
id: string
icon: React.ReactNode
title: string
}
export const CurriculumVitaeSection: React.FC<CurriculumVitaeSectionProps> = (
props,
) => {
const { id, icon, title, children } = props
return (
<section className="detail" id={id}>
<div className="icon">{icon}</div>
<div className="info">
<h2 className="h4 font-semibold uppercase">{title}</h2>
<div className="content">{children}</div>
</div>
</section>
)
}

View File

@ -0,0 +1,59 @@
import { useTranslations } from "next-intl"
import { FaToolbox } from "react-icons/fa"
import {
SKILL_CATEGORIES,
SKILL_NAMES_BY_CATEGORY,
} from "../Home/Skills/skills"
import { CurriculumVitaeSection } from "./CurriculumVitaeSection"
export interface CurriculumVitaeSkillsProps {}
export const CurriculumVitaeSkills: React.FC<
CurriculumVitaeSkillsProps
> = () => {
const t = useTranslations()
const skills = [
...SKILL_CATEGORIES.map((category) => {
const skillNames = SKILL_NAMES_BY_CATEGORY[category]
return {
category,
skillNames,
}
}),
{
category: "others",
skillNames: [t("locales.en-US"), t("home.skills.driving-license")],
},
] as const
return (
<CurriculumVitaeSection
id="skills"
title={t("home.skills.title")}
icon={<FaToolbox size={24} />}
>
<ul className="list-unstyled m-0">
{skills.map(({ category, skillNames }) => {
return (
<li key={category} className="card card-nested relative">
<div className="skill-info">
<strong>{t(`home.skills.${category}`)}</strong>
<div className="labels mt-2">
{skillNames.map((skillName) => {
return (
<p key={skillName} className="label label-keyword">
{skillName}
</p>
)
})}
</div>
</div>
</li>
)
})}
</ul>
</CurriculumVitaeSection>
)
}

View File

@ -0,0 +1,69 @@
import { useTranslations } from "next-intl"
import { MdWork } from "react-icons/md"
import { CurriculumVitaeSection } from "./CurriculumVitaeSection"
export interface CurriculumVitaeWorkProps {}
export const CurriculumVitaeWork: React.FC<CurriculumVitaeWorkProps> = () => {
const t = useTranslations()
const workExperiences = [
{
summary: t("curriculum-vitae.work.ircad.summary"),
website: "https://ircad.fr/",
name: "IRCAD",
location: "1 Place de l'Hôpital, FR-67000 Strasbourg",
position: t("curriculum-vitae.work.ircad.position"),
dates: "28/08/2023 - 02/09/2024",
duration: t("curriculum-vitae.work.ircad.duration"),
},
{
summary: t("curriculum-vitae.work.numerize.summary"),
website: "https://numerize.com/",
name: "Numerize",
location: "4 Rue Sophie Germain, FR-67720 Hœrdt",
position: t("curriculum-vitae.work.numerize.position"),
dates: "11/04/2023 - 26/07/2023",
duration: t("curriculum-vitae.work.numerize.duration"),
},
]
return (
<CurriculumVitaeSection
id="work-experience"
title={t("curriculum-vitae.work.title")}
icon={<MdWork size={24} />}
>
<ul className="list-unstyled m-0">
{workExperiences.map((workExperience) => {
return (
<li key={workExperience.name} className="card card-nested">
<p className="relative m-0">
<strong>
<a href={workExperience.website} target="_blank">
{workExperience.name}
</a>
</strong>
</p>
<p className="relative m-0">
<strong>{workExperience.position}</strong>
</p>
<p className="text-muted">
<small>
<span className="space-right">
{workExperience.dates} ({workExperience.duration})
</span>
</small>
</p>
<div className="mt-2">
<p>{workExperience.summary}</p>
</div>
</li>
)
})}
</ul>
</CurriculumVitaeSection>
)
}

View File

@ -1,6 +1,6 @@
import type { Meta, StoryObj } from "@storybook/react" import type { Meta, StoryObj } from "@storybook/react"
import { expect, fn, userEvent, within } from "@storybook/test" import { expect, fn, userEvent, within } from "@storybook/test"
import { FaCheck } from "react-icons/fa6" import { FaCheck } from "react-icons/fa"
import type { ButtonLinkProps } from "./Button" import type { ButtonLinkProps } from "./Button"
import { Button } from "./Button" import { Button } from "./Button"

View File

@ -13,8 +13,8 @@ export const AboutDescription: React.FC<AboutDescriptionProps> = () => {
{t.rich("home.about.description")} {t.rich("home.about.description")}
</Typography> </Typography>
<Button href="/curriculum-vitae/index.html" variant="outline"> <Button href="/curriculum-vitae" variant="outline">
Curriculum vitæ ({t("locales.fr-FR")}) Curriculum vitæ
</Button> </Button>
</div> </div>
) )

View File

@ -1,6 +1,6 @@
export interface AboutItemProps { export interface AboutItemProps {
label: string label: string
value: string value: React.ReactNode
link?: string link?: string
} }

View File

@ -1,27 +0,0 @@
"use client"
import { BIRTH_DATE } from "@repo/utils/constants"
import { getAge, getISODate } from "@repo/utils/dates"
import { useTranslations } from "next-intl"
import { useMemo } from "react"
import { AboutItem } from "./AboutItem"
export interface AboutItemBirthDateProps {}
export const AboutItemBirthDate: React.FC<AboutItemBirthDateProps> = () => {
const t = useTranslations()
const age = useMemo(() => {
return getAge(BIRTH_DATE)
}, [])
return (
<AboutItem
label={t("home.about.birth-date.label")}
value={t("home.about.birth-date.value", {
age,
birthDate: getISODate(BIRTH_DATE),
})}
/>
)
}

View File

@ -1,6 +1,6 @@
import { useTranslations } from "next-intl" import { useTranslations } from "next-intl"
import { AboutItem } from "./AboutItem" import { AboutItem } from "./AboutItem"
import { AboutItemBirthDate } from "./AboutItemBirthDate" import { BirthDate } from "./BirthDate"
export interface AboutListProps {} export interface AboutListProps {}
@ -13,7 +13,10 @@ export const AboutList: React.FC<AboutListProps> = () => {
label={t("home.about.pronouns.label")} label={t("home.about.pronouns.label")}
value={t("home.about.pronouns.value")} value={t("home.about.pronouns.value")}
/> />
<AboutItemBirthDate /> <AboutItem
label={t("home.about.birth-date.label")}
value={<BirthDate />}
/>
<AboutItem <AboutItem
label={t("home.about.nationality.label")} label={t("home.about.nationality.label")}
value={t("home.about.nationality.value")} value={t("home.about.nationality.value")}

View File

@ -0,0 +1,21 @@
"use client"
import { BIRTH_DATE, BIRTH_DATE_STRING } from "@repo/utils/constants"
import { getAge } from "@repo/utils/dates"
import { useTranslations } from "next-intl"
import { useMemo } from "react"
export interface BirthDateProps {}
export const BirthDate: React.FC<BirthDateProps> = () => {
const t = useTranslations()
const age = useMemo(() => {
return getAge(BIRTH_DATE)
}, [])
return t("home.about.birth-date.value", {
age,
birthDate: BIRTH_DATE_STRING,
})
}

View File

@ -5,7 +5,7 @@ import { useMemo } from "react"
import { Link } from "../../Design/Link/Link" import { Link } from "../../Design/Link/Link"
import { useTheme } from "../../Layout/Header/SwitchTheme" import { useTheme } from "../../Layout/Header/SwitchTheme"
import type { SkillName } from "./skills" import type { SkillName } from "./skills"
import { skills } from "./skills" import { SKILLS } from "./skills"
export interface SkillItemProps { export interface SkillItemProps {
skillName: SkillName skillName: SkillName
@ -14,7 +14,7 @@ export interface SkillItemProps {
export const SkillItem: React.FC<SkillItemProps> = (props) => { export const SkillItem: React.FC<SkillItemProps> = (props) => {
const { skillName } = props const { skillName } = props
const skill = skills[skillName] const skill = SKILLS[skillName]
const { theme } = useTheme() const { theme } = useTheme()

View File

@ -2,6 +2,7 @@ import { useTranslations } from "next-intl"
import { Section, SectionTitle } from "../../Layout/Section/Section" import { Section, SectionTitle } from "../../Layout/Section/Section"
import { SkillItem } from "./SkillItem" import { SkillItem } from "./SkillItem"
import { SkillsSection } from "./SkillsSection" import { SkillsSection } from "./SkillsSection"
import { SKILL_CATEGORIES, SKILL_NAMES_BY_CATEGORY } from "./skills"
export interface SkillsProps {} export interface SkillsProps {}
@ -12,34 +13,17 @@ export const Skills: React.FC<SkillsProps> = () => {
<Section verticalSpacing horizontalSpacing id="skills"> <Section verticalSpacing horizontalSpacing id="skills">
<SectionTitle>{t("home.skills.title")}</SectionTitle> <SectionTitle>{t("home.skills.title")}</SectionTitle>
<SkillsSection title={t("home.skills.programming-languages")}> {SKILL_CATEGORIES.map((category) => {
<SkillItem skillName="TypeScript" /> const skillNames = SKILL_NAMES_BY_CATEGORY[category]
<SkillItem skillName="Python" />
<SkillItem skillName="C/C++" />
<SkillItem skillName="PHP" />
</SkillsSection>
<SkillsSection title={t("home.skills.frontend")}> return (
<SkillItem skillName="HTML" /> <SkillsSection key={category} title={t(`home.skills.${category}`)}>
<SkillItem skillName="CSS" /> {skillNames.map((skillName) => {
<SkillItem skillName="Tailwind CSS" /> return <SkillItem key={skillName} skillName={skillName} />
<SkillItem skillName="React.js (+ Next.js)" /> })}
</SkillsSection> </SkillsSection>
)
<SkillsSection title={t("home.skills.backend")}> })}
<SkillItem skillName="Laravel" />
<SkillItem skillName="Node.js" />
<SkillItem skillName="Fastify" />
<SkillItem skillName="PostgreSQL" />
</SkillsSection>
<SkillsSection title={t("home.skills.software-tools")}>
<SkillItem skillName="GNU/Linux" />
<SkillItem skillName="Arch Linux" />
<SkillItem skillName="Visual Studio Code" />
<SkillItem skillName="Git" />
<SkillItem skillName="Docker" />
</SkillsSection>
</Section> </Section>
) )
} }

View File

@ -3,7 +3,7 @@ export interface Skill {
image: string | { [key: string]: string } image: string | { [key: string]: string }
} }
export const skills = { export const SKILLS = {
JavaScript: { JavaScript: {
link: "https://developer.mozilla.org/docs/Web/JavaScript", link: "https://developer.mozilla.org/docs/Web/JavaScript",
image: "/images/skills/JavaScript.webp", image: "/images/skills/JavaScript.webp",
@ -112,4 +112,27 @@ export const skills = {
}, },
} as const } as const
export type SkillName = keyof typeof skills export type SkillName = keyof typeof SKILLS
export const SKILL_CATEGORIES = [
"programming-languages",
"frontend",
"backend",
"software-tools",
] as const
export type SkillCategory = (typeof SKILL_CATEGORIES)[number]
export const SKILL_NAMES_BY_CATEGORY = {
"programming-languages": ["TypeScript", "Python", "C/C++", "PHP"],
frontend: ["HTML", "CSS", "Tailwind CSS", "React.js (+ Next.js)"],
backend: ["Laravel", "Node.js", "Fastify", "PostgreSQL"],
"software-tools": [
"GNU/Linux",
"Arch Linux",
"Visual Studio Code",
"Git",
"Docker",
],
} as const satisfies {
[key in SkillCategory]: SkillName[]
}

View File

@ -59,7 +59,7 @@ export const Locales: React.FC<LocalesProps> = () => {
<ul <ul
className={classNames( className={classNames(
"shadow-lightFlag dark:shadow-darkFlag bg-background dark:bg-background-dark absolute top-14 z-10 mt-4 flex w-32 list-none flex-col items-center justify-center rounded-lg p-0", "shadow-lightFlag dark:shadow-darkFlag bg-background dark:bg-background-dark absolute top-14 z-10 mt-2 flex w-32 list-none flex-col items-center justify-center rounded-lg p-0",
{ hidden: !isVisibleMenu }, { hidden: !isVisibleMenu },
)} )}
> >

View File

@ -11,16 +11,19 @@ export const THEMES = ["light", "dark"] as const
export type Theme = (typeof THEMES)[number] export type Theme = (typeof THEMES)[number]
export const THEME_DEFAULT = "dark" as Theme export const THEME_DEFAULT = "dark" as Theme
export interface ThemeProviderProps extends React.PropsWithChildren {} export interface ThemeProviderProps extends React.PropsWithChildren {
forcedTheme?: Theme
}
export const ThemeProvider: React.FC<ThemeProviderProps> = (props) => { export const ThemeProvider: React.FC<ThemeProviderProps> = (props) => {
const { children } = props const { children, forcedTheme } = props
return ( return (
<NextThemeProvider <NextThemeProvider
attribute="class" attribute="class"
defaultTheme={THEME_DEFAULT} defaultTheme={THEME_DEFAULT}
enableSystem={false} enableSystem={false}
forcedTheme={forcedTheme}
> >
{children} {children}
</NextThemeProvider> </NextThemeProvider>

View File

@ -1,6 +1,6 @@
{ {
"name": "@repo/utils", "name": "@repo/utils",
"version": "4.0.0-staging.1", "version": "4.0.0",
"private": true, "private": true,
"type": "module", "type": "module",
"exports": { "exports": {

3
pnpm-lock.yaml generated
View File

@ -642,6 +642,9 @@ importers:
typescript: typescript:
specifier: 'catalog:' specifier: 'catalog:'
version: 5.5.4 version: 5.5.4
vitest:
specifier: 'catalog:'
version: 2.0.4(@types/node@22.0.0)(@vitest/browser@2.0.4)(@vitest/ui@2.0.4)(terser@5.31.3)
packages/react-hooks: packages/react-hooks:
dependencies: dependencies: