1
1
mirror of https://github.com/theoludwig/theoludwig.git synced 2025-09-15 07:43:29 +02:00

Compare commits

...

5 Commits

24 changed files with 1175 additions and 1077 deletions

View File

@@ -28,7 +28,7 @@ The commit message guidelines adheres to [Conventional Commits](https://www.conv
### Prerequisites ### Prerequisites
- [Node.js](https://nodejs.org/) >= v24.0.0 [(`nvm install 24`)](https://nvm.sh) - [Node.js](https://nodejs.org/) >= v24.0.0 [(`nvm install 24`)](https://nvm.sh)
- [pnpm](https://pnpm.io/) v10.15.0 [(`npm install --global corepack@0.34.0 && corepack enable`)](https://github.com/nodejs/corepack) - [pnpm](https://pnpm.io/) v10.15.1 [(`npm install --global corepack@0.34.0 && corepack enable`)](https://github.com/nodejs/corepack)
- [Docker](https://www.docker.com/) - [Docker](https://www.docker.com/)
### Installation ### Installation

View File

@@ -8,7 +8,7 @@
<a href="https://github.com/theoludwig"><img alt="GitHub" src="https://img.shields.io/badge/-GitHub-5A5A5A?style=flat&labelColor=5A5A5A&logo=github&logoColor=white"/></a> <a href="https://github.com/theoludwig"><img alt="GitHub" src="https://img.shields.io/badge/-GitHub-5A5A5A?style=flat&labelColor=5A5A5A&logo=github&logoColor=white"/></a>
<a href="https://gitlab.com/theoludwig"><img alt="GitLab" src="https://img.shields.io/badge/-GitLab-303030?style=flat&labelColor=303030&logo=gitlab&logoColor=white"/></a> <a href="https://gitlab.com/theoludwig"><img alt="GitLab" src="https://img.shields.io/badge/-GitLab-303030?style=flat&labelColor=303030&logo=gitlab&logoColor=white"/></a>
<a href="https://www.npmjs.com/~theoludwig"><img alt="npm" src="https://img.shields.io/badge/-npm-c4302b?style=flat&labelColor=c4302b&logo=npm&logoColor=white"/></a> <a href="https://www.npmjs.com/~theoludwig"><img alt="npm" src="https://img.shields.io/badge/-npm-c4302b?style=flat&labelColor=c4302b&logo=npm&logoColor=white"/></a>
<a href="https://twitter.com/theoludwig_"><img alt="Twitter" src="https://img.shields.io/badge/-Twitter-1ca0f1?style=flat&labelColor=1ca0f1&logo=x&logoColor=white"/></a> <a href="https://twitter.com/theoludwig_"><img alt="X/Twitter" src="https://img.shields.io/badge/-Twitter-1ca0f1?style=flat&labelColor=1ca0f1&logo=x&logoColor=white"/></a>
<a href="https://www.youtube.com/@theo_ludwig"><img alt="YouTube" src="https://img.shields.io/badge/-YouTube-c4302b?style=flat&labelColor=c4302b&logo=youtube&logoColor=white"/></a> <a href="https://www.youtube.com/@theo_ludwig"><img alt="YouTube" src="https://img.shields.io/badge/-YouTube-c4302b?style=flat&labelColor=c4302b&logo=youtube&logoColor=white"/></a>
<a href="https://www.twitch.tv/theoludwig"><img alt="Twitch" src="https://img.shields.io/badge/-Twitch-9147FF?style=flat&labelColor=9147FF&logo=twitch&logoColor=white"/></a> <a href="https://www.twitch.tv/theoludwig"><img alt="Twitch" src="https://img.shields.io/badge/-Twitch-9147FF?style=flat&labelColor=9147FF&logo=twitch&logoColor=white"/></a>
<a href="https://theoludwig.fr/"><img alt="Website" src="https://img.shields.io/badge/-Website-181818?style=flat&labelColor=181818&logo=Google-Chrome&logoColor=white"/></a> <a href="https://theoludwig.fr/"><img alt="Website" src="https://img.shields.io/badge/-Website-181818?style=flat&labelColor=181818&logo=Google-Chrome&logoColor=white"/></a>
@@ -27,21 +27,28 @@
"nationality": "Alsace, France", "nationality": "Alsace, France",
"interests": ["Developer Full Stack", "Open-Source Enthusiast"], "interests": ["Developer Full Stack", "Open-Source Enthusiast"],
"skills": { "skills": {
"programmingLanguages": [ "software-development": [
"JavaScript/TypeScript", "TypeScript",
"Python", "React.js (+ Next.js)",
"C/C++", "Tailwind CSS",
"PHP" "Node.js",
"tRPC/oRPC",
"PostgreSQL"
], ],
"frontend": ["HTML/CSS", "Tailwind CSS", "React.js/Next.js"], "sys-admin": [
"backend": ["Laravel", "Node.js", "Fastify", "PostgreSQL"], "Docker",
"tools": [ "Proxmox",
"Caddy",
"GitHub Actions",
"GitLab CI/CD"
],
"software-tools": [
"GNU/Linux", "GNU/Linux",
"Arch Linux", "Arch Linux",
"Visual Studio Code", "Visual Studio Code",
"Git", "Git"
"Docker" ],
] "systems-programming": ["C/C++", "Rust", "Go"]
} }
} }
``` ```

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -3,7 +3,7 @@
"version": "0.0.0-develop", "version": "0.0.0-develop",
"private": true, "private": true,
"type": "module", "type": "module",
"packageManager": "pnpm@10.15.0+sha512.486ebc259d3e999a4e8691ce03b5cac4a71cbeca39372a9b762cb500cfdf0873e2cb16abe3d951b1ee2cf012503f027b98b6584e4df22524e0c7450d9ec7aa7b", "packageManager": "pnpm@10.15.1+sha512.34e538c329b5553014ca8e8f4535997f96180a1d0f614339357449935350d924e22f8614682191264ec33d1462ac21561aff97f6bb18065351c162c7e8f6de67",
"engines": { "engines": {
"node": ">=24.0.0" "node": ">=24.0.0"
}, },

View File

@@ -17,7 +17,7 @@ Keep in mind that I will not translate the posts in French, all the posts will b
I plan to publish new posts when I have something new to share. There's no schedule, so stay tuned! I plan to publish new posts when I have something new to share. There's no schedule, so stay tuned!
To stay informed of new blog post and to ask questions, feel free to follow me on Twitter: [@theoludwig\_](https://twitter.com/theoludwig_). To stay informed of new blog post and to ask questions, feel free to follow me on X/Twitter: [@theoludwig\_](https://twitter.com/theoludwig_).
## Project based learning ## Project based learning

View File

@@ -37,7 +37,7 @@ The idea is that a user can create an account to authenticate with an email addr
## History ## History
The idea for the project has existed since May 13, 2020, symbolized by a [publication on Twitter](https://twitter.com/theoludwig_/status/1260638175246135296) by the creator: Théo LUDWIG. The idea for the project has existed since May 13, 2020, symbolized by a [publication on X/Twitter](https://twitter.com/theoludwig_/status/1260638175246135296) by the creator: Théo LUDWIG.
The main goal is to put into **practice knowledge in web development** and computer science in general on a concrete project that can **easily evolve over time** where you can add many features. The main goal is to put into **practice knowledge in web development** and computer science in general on a concrete project that can **easily evolve over time** where you can add many features.

View File

@@ -41,6 +41,7 @@
"institution": "IUT Robert Schuman in Illkirch-Graffenstaden", "institution": "IUT Robert Schuman in Illkirch-Graffenstaden",
"study-type": "University Bachelor of Technology (BUT) Computer Science", "study-type": "University Bachelor of Technology (BUT) Computer Science",
"years": { "years": {
"title": "2021 - 2024",
"2021-2022": { "2021-2022": {
"courses": { "courses": {
"java": "Object Oriented Development in Java", "java": "Object Oriented Development in Java",
@@ -87,19 +88,28 @@
"title": "Studies" "title": "Studies"
}, },
"interests": { "interests": {
"fusey": "<link>Fusey (fusey.gg)</link>: website I'm developing for the game ARK that tracks the number of players connected to the servers in real time and has over ~5,000 visitors each month, ~100,000 members on Discord, and ~120,000 followers on X/Twitter.", "title": "Interests & hobbies",
"fusey": "<link>Fusey (fusey.gg)</link>: website I'm developing for the game ARK that tracks the number of players connected to the servers in real time and has <strong>over ~5,000 visitors each month, ~100,000 members on Discord, and ~120,000 followers on X/Twitter</strong>.",
"open-source": "Open-Source Enthusiast" "open-source": "Open-Source Enthusiast"
}, },
"work": { "work": {
"ircad": { "ircad": {
"duration": "4 years", "duration": "4 years",
"position": "Full Stack Web Developer Apprentice", "position": "Full Stack Web Developer Apprentice",
"summary": "Development of WebSurg, a virtual university dedicated to medical-surgical training, in React.js/Next.js and API Platform with Symfony." "tasks": {
"WebSurg": "Development of WebSurg, a virtual university dedicated to medical-surgical training, built with React.js/Next.js and API Platform with Symfony.",
"Figma": "Integration of Figma mockups for WebSurg, website dedicated to medical-surgical training.",
"IRCAD-Core": "IRCAD Core, an internal tool for managing medical training sessions and their requirements (anatomical models, medications, staff training, etc.).",
"feature-logs": "History and traceability of data modifications (what? who? when?) in IRCAD Core.",
"feature-permissions": "Advanced permissions system and OAuth2 authentication, with read, write, and delete access restricted for specific users of the IRCAD Core application.",
"feature-search": "Search engine with filters, sorting, and customizable display for each user of IRCAD Core.",
"feature-architecture": "IRCAD Core project architecture in TypeScript Monorepo with Turborepo, automatic deployment (CI/CD) and self-hosted internally with Docker Compose."
}
}, },
"numerize": { "numerize": {
"duration": "3 months", "duration": "3 months",
"position": "Full Stack Web Developer Intern", "position": "Full Stack Web Developer Intern",
"summary": "Development of an DMS (Document Management System) tool in React.js, Laravel and GraphQL." "summary": "Development of a DMS (Document Management System) tool in React.js, Laravel and GraphQL."
}, },
"title": "Work experiences" "title": "Work experiences"
} }
@@ -155,6 +165,10 @@
"title": "Projects" "title": "Projects"
}, },
"skills": { "skills": {
"software-development": "Software Development",
"sys-admin": "SysAdmin",
"systems-programming": "Systems Programming",
"backend": "Backend", "backend": "Backend",
"driving-license": "Driving license", "driving-license": "Driving license",
"frontend": "Frontend", "frontend": "Frontend",

View File

@@ -41,6 +41,7 @@
"institution": "IUT Robert Schuman à Illkirch-Graffenstaden", "institution": "IUT Robert Schuman à Illkirch-Graffenstaden",
"study-type": "Bachelor Universitaire de Technologie (BUT) Informatique", "study-type": "Bachelor Universitaire de Technologie (BUT) Informatique",
"years": { "years": {
"title": "2021 - 2024",
"2021-2022": { "2021-2022": {
"courses": { "courses": {
"java": "Développement Orientée Objet en Java", "java": "Développement Orientée Objet en Java",
@@ -87,14 +88,23 @@
"title": "Études" "title": "Études"
}, },
"interests": { "interests": {
"fusey": "<link>Fusey (fusey.gg)</link> : site web que je développe pour le jeu ARK qui permet de suivre en temps réel le nombre de joueurs connectés sur les serveurs et a plus de ~5 000 visiteurs chaque mois, ~100 000 membres sur Discord et ~120 000 followers sur X/Twitter.", "title": "Intérêts & loisirs",
"fusey": "<link>Fusey (fusey.gg)</link> : site web que je développe pour le jeu ARK qui permet de suivre en temps réel le nombre de joueurs connectés sur les serveurs et a plus de <strong>~5 000 visiteurs chaque mois, ~100 000 membres sur Discord et ~120 000 followers sur X/Twitter</strong>.",
"open-source": "Enthousiaste de l'Open-Source" "open-source": "Enthousiaste de l'Open-Source"
}, },
"work": { "work": {
"ircad": { "ircad": {
"duration": "4 ans", "duration": "4 ans",
"position": "Apprenti Développeur Web Full Stack", "position": "Apprenti Développeur Web Full Stack",
"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." "tasks": {
"WebSurg": "Développement de WebSurg, une université virtuelle consacrée à la formation médico-chirurgicale, en React.js/Next.js et API Platform avec Symfony.",
"Figma": "Intégration des maquettes Figma pour WebSurg, site web consacrée à la formation médico-chirurgicale.",
"IRCAD-Core": "IRCAD Core, outil interne de gestion des formations médicales et leurs besoins (modèles anatomiques, médicaments, formations du personnel, etc.).",
"feature-logs": "Historique et traçabilité des modifications des données (quoi? qui? quand?) dans IRCAD Core.",
"feature-permissions": "Système de permissions avancé et authentification OAuth2, avec accès en lecture, écriture et suppression restreint pour des utilisateurs spécifiques de l'application IRCAD Core.",
"feature-search": "Moteur de recherche avec filtres, tris et ordre d'affichage personnalisable pour IRCAD Core.",
"feature-architecture": "Architecture du projet IRCAD Core en Monorepo TypeScript avec Turborepo, déploiement automatique (CI/CD) et hébergé en interne avec Docker Compose."
}
}, },
"numerize": { "numerize": {
"duration": "3 mois", "duration": "3 mois",
@@ -155,6 +165,10 @@
"title": "Projets" "title": "Projets"
}, },
"skills": { "skills": {
"software-development": "Développement informatique",
"sys-admin": "SysAdmin",
"systems-programming": "Programmation Système",
"backend": "Backend", "backend": "Backend",
"driving-license": "Permis B", "driving-license": "Permis B",
"frontend": "Frontend", "frontend": "Frontend",

View File

@@ -14,59 +14,64 @@ export const CurriculumVitaeEducation: React.FC<
years: t("curriculum-vitae.education.cnam.years.2024-2027.title"), years: t("curriculum-vitae.education.cnam.years.2024-2027.title"),
studyType: t("curriculum-vitae.education.cnam.study-type"), studyType: t("curriculum-vitae.education.cnam.study-type"),
institution: t("curriculum-vitae.education.cnam.institution"), institution: t("curriculum-vitae.education.cnam.institution"),
score: t("curriculum-vitae.education.cnam.years.2024-2027.description"), // score: t("curriculum-vitae.education.cnam.years.2024-2027.description"),
courses: [], courses: [],
}, },
{ {
years: t("curriculum-vitae.education.iut.years.2023-2024.title"), years: t("curriculum-vitae.education.iut.years.title"),
studyType: t("curriculum-vitae.education.iut.study-type"), studyType: t("curriculum-vitae.education.iut.study-type"),
institution: t("curriculum-vitae.education.iut.institution"), institution: t("curriculum-vitae.education.iut.institution"),
score: t("curriculum-vitae.education.iut.years.2023-2024.description"),
courses: [ courses: [
t("curriculum-vitae.education.iut.years.2023-2024.courses.web"), {
t("curriculum-vitae.education.iut.years.2023-2024.courses.ci-cd"), year: t("curriculum-vitae.education.iut.years.2021-2022.description"),
t( title: t("curriculum-vitae.education.iut.years.2021-2022.title"),
"curriculum-vitae.education.iut.years.2023-2024.courses.complexity-algorithms", courses: [
), t("curriculum-vitae.education.iut.years.2021-2022.courses.java"),
t("curriculum-vitae.education.iut.years.2023-2024.courses.no-sql"), 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"),
],
},
{
year: t("curriculum-vitae.education.iut.years.2022-2023.description"),
title: t("curriculum-vitae.education.iut.years.2022-2023.title"),
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.sql-security"),
t(
"curriculum-vitae.education.iut.years.2022-2023.courses.systems-c",
),
],
},
{
year: t("curriculum-vitae.education.iut.years.2023-2024.description"),
title: t("curriculum-vitae.education.iut.years.2023-2024.title"),
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"), // years: t("curriculum-vitae.education.lycee.years.2019-2021.title"),
studyType: t("curriculum-vitae.education.iut.study-type"), // studyType: t("curriculum-vitae.education.lycee.study-type"),
institution: t("curriculum-vitae.education.iut.institution"), // institution: t("curriculum-vitae.education.lycee.institution"),
score: t("curriculum-vitae.education.iut.years.2022-2023.description"), // score: t("curriculum-vitae.education.lycee.score"),
courses: [ // 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 ( return (
@@ -79,35 +84,47 @@ export const CurriculumVitaeEducation: React.FC<
{educations.map((education) => { {educations.map((education) => {
return ( return (
<li key={education.years} className="card card-nested"> <li key={education.years} className="card card-nested">
<div className="content"> <p className="relative m-0">
<p className="relative m-0"> <strong>{education.studyType}</strong>
<strong>{education.studyType}</strong> </p>
</p>
{/* {education.score != null ? (
<p className="relative m-0"> <p className="relative m-0">
<strong>{education.score}</strong> <strong>{education.score}</strong>
</p> </p>
) : (
<></>
)} */}
<p className="text-muted m-0">{education.institution}</p> <p className="text-muted m-0">{education.institution}</p>
<p className="text-muted m-0"> <p className="text-muted m-0">
<small>{education.years}</small> <small>{education.years}</small>
</p> </p>
{education.courses.length > 0 ? ( {education.courses.length > 0 ? (
<ul <ul className="list-none">
style={{ {education.courses.map(({ year, title, courses }) => {
paddingInlineStart: 20, return (
}} <li key={year}>
> <span className="font-medium">{year}</span>
{education.courses.map((course) => { <span> ({title})</span>
return <li key={course}>{course}</li> <ul
})} style={{
</ul> paddingInlineStart: 20,
) : ( }}
<></> >
)} {courses.map((course) => {
</div> return <li key={course}>{course}</li>
})}
</ul>
</li>
)
})}
</ul>
) : (
<></>
)}
</li> </li>
) )
})} })}

View File

@@ -10,31 +10,34 @@ export const CurriculumVitaeInterests: React.FC<
const t = useTranslations() const t = useTranslations()
const interests = [ const interests = [
t("curriculum-vitae.interests.open-source"), <strong key="open-source">
{t("curriculum-vitae.interests.open-source")}
</strong>,
t.rich("curriculum-vitae.interests.fusey", { t.rich("curriculum-vitae.interests.fusey", {
link: (children) => { link: (children) => {
return ( return (
<a href="https://fusey.gg" target="_blank"> <a href="https://fusey.gg" target="_blank" className="font-semibold">
{children} {children}
</a> </a>
) )
}, },
strong: (children) => {
return <strong>{children}</strong>
},
}), }),
] ]
return ( return (
<CurriculumVitaeSection <CurriculumVitaeSection
id="interests" id="interests"
title={t("home.interests.title")} title={t("curriculum-vitae.interests.title")}
icon={<FaHeart size={24} />} icon={<FaHeart size={24} />}
> >
<ul className="list-unstyled m-0"> <ul className="list-unstyled m-0">
{interests.map((interest, index) => { {interests.map((interest, index) => {
return ( return (
<li key={index} className="card card-nested"> <li key={index} className="card card-nested max-w-2xl">
<p> <p>{interest}</p>
<strong>{interest}</strong>
</p>
</li> </li>
) )
})} })}

View File

@@ -9,13 +9,21 @@ export const CurriculumVitaeWork: React.FC<CurriculumVitaeWorkProps> = () => {
const workExperiences = [ const workExperiences = [
{ {
summary: t("curriculum-vitae.work.ircad.summary"),
website: "https://ircad.fr/", website: "https://ircad.fr/",
name: "IRCAD", name: "IRCAD",
location: "1 Place de l'Hôpital, FR-67000 Strasbourg", location: "1 Place de l'Hôpital, FR-67000 Strasbourg",
position: t("curriculum-vitae.work.ircad.position"), position: t("curriculum-vitae.work.ircad.position"),
dates: "28/08/2023 - 31/08/2027", dates: "28/08/2023 - 31/08/2027",
duration: t("curriculum-vitae.work.ircad.duration"), duration: t("curriculum-vitae.work.ircad.duration"),
tasks: [
// t("curriculum-vitae.work.ircad.tasks.WebSurg"),
t("curriculum-vitae.work.ircad.tasks.Figma"),
t("curriculum-vitae.work.ircad.tasks.IRCAD-Core"),
t("curriculum-vitae.work.ircad.tasks.feature-logs"),
t("curriculum-vitae.work.ircad.tasks.feature-permissions"),
t("curriculum-vitae.work.ircad.tasks.feature-search"),
t("curriculum-vitae.work.ircad.tasks.feature-architecture"),
],
}, },
{ {
summary: t("curriculum-vitae.work.numerize.summary"), summary: t("curriculum-vitae.work.numerize.summary"),
@@ -25,6 +33,7 @@ export const CurriculumVitaeWork: React.FC<CurriculumVitaeWorkProps> = () => {
position: t("curriculum-vitae.work.numerize.position"), position: t("curriculum-vitae.work.numerize.position"),
dates: "11/04/2023 - 26/07/2023", dates: "11/04/2023 - 26/07/2023",
duration: t("curriculum-vitae.work.numerize.duration"), duration: t("curriculum-vitae.work.numerize.duration"),
tasks: [],
}, },
] ]
@@ -50,16 +59,36 @@ export const CurriculumVitaeWork: React.FC<CurriculumVitaeWorkProps> = () => {
<strong>{workExperience.position}</strong> <strong>{workExperience.position}</strong>
</p> </p>
<p className="text-muted"> <p className="text-muted m-0">
<small> <small>
<span className="space-right"> <span className="space-right">
{workExperience.dates} ({workExperience.duration}) {workExperience.dates} ({workExperience.duration})
</span> </span>
</small> </small>
</p> </p>
<div className="mt-2">
<p>{workExperience.summary}</p> {workExperience.tasks.length > 0 ? (
</div> <ul
style={{
paddingInlineStart: 20,
}}
className="space-y-1"
>
{workExperience.tasks.map((task) => {
return <li key={task}>{task}</li>
})}
</ul>
) : (
<></>
)}
{workExperience.summary != null ? (
<div className="mt-2">
<p>{workExperience.summary}</p>
</div>
) : (
<></>
)}
</li> </li>
) )
})} })}

View File

@@ -2,9 +2,9 @@ import { Icon } from "./Icon.tsx"
export const TwitterIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => { export const TwitterIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => {
return ( return (
<Icon {...props}> <Icon {...props} viewBox="0 0 1200 1227">
<title>Twitter</title> <title>X/Twitter</title>
<path d="M23.953 4.57a10 10 0 01-2.825.775 4.958 4.958 0 002.163-2.723c-.951.555-2.005.959-3.127 1.184a4.92 4.92 0 00-8.384 4.482C7.69 8.095 4.067 6.13 1.64 3.162a4.822 4.822 0 00-.666 2.475c0 1.71.87 3.213 2.188 4.096a4.904 4.904 0 01-2.228-.616v.06a4.923 4.923 0 003.946 4.827 4.996 4.996 0 01-2.212.085 4.936 4.936 0 004.604 3.417 9.867 9.867 0 01-6.102 2.105c-.39 0-.779-.023-1.17-.067a13.995 13.995 0 007.557 2.209c9.053 0 13.998-7.496 13.998-13.985 0-.21 0-.42-.015-.63A9.935 9.935 0 0024 4.59z" /> <path d="M714.163 519.284L1160.89 0H1055.03L667.137 450.887L357.328 0H0L468.492 681.821L0 1226.37H105.866L515.491 750.218L842.672 1226.37H1200L714.137 519.284H714.163ZM569.165 687.828L521.697 619.934L144.011 79.6944H306.615L611.412 515.685L658.88 583.579L1055.08 1150.3H892.476L569.165 687.854V687.828Z" />
</Icon> </Icon>
) )
} }

View File

@@ -26,7 +26,7 @@ export const SocialMediaList: React.FC<SocialMediaListProps> = () => {
<SocialMediaItem <SocialMediaItem
link="https://twitter.com/theoludwig_" link="https://twitter.com/theoludwig_"
ariaLabel="Twitter" ariaLabel="X/Twitter"
> >
<TwitterIcon /> <TwitterIcon />
</SocialMediaItem> </SocialMediaItem>

View File

@@ -110,29 +110,64 @@ export const SKILLS = {
link: "https://www.docker.com/", link: "https://www.docker.com/",
image: "/images/skills/Docker.webp", image: "/images/skills/Docker.webp",
}, },
"tRPC/oRPC": {
link: "https://trpc.io/",
image: "/images/skills/tRPC.webp",
},
Rust: {
link: "https://www.rust-lang.org/",
image: "/images/skills/Rust.webp",
},
Caddy: {
link: "https://caddyserver.com/",
image: "/images/skills/Caddy.webp",
},
Proxmox: {
link: "https://www.proxmox.com/",
image: {
light: "/images/skills/Proxmox-light.webp",
dark: "/images/skills/Proxmox-dark.webp",
},
},
"GitHub Actions": {
link: "https://github.com/features/actions",
image: {
light: "/images/skills/GitHub-light.webp",
dark: "/images/skills/GitHub-dark.webp",
},
},
"GitLab CI/CD": {
link: "https://docs.gitlab.com/ci",
image: "/images/skills/GitLab.webp",
},
Go: {
link: "https://go.dev/",
image: "/images/skills/Go.webp",
},
} as const } as const
export type SkillName = keyof typeof SKILLS export type SkillName = keyof typeof SKILLS
export const SKILL_CATEGORIES = [ export const SKILL_CATEGORIES = [
"programming-languages", "software-development",
"frontend", "sys-admin",
"backend", "systems-programming",
"software-tools", "software-tools",
] as const ] as const
export type SkillCategory = (typeof SKILL_CATEGORIES)[number] export type SkillCategory = (typeof SKILL_CATEGORIES)[number]
export const SKILL_NAMES_BY_CATEGORY = { export const SKILL_NAMES_BY_CATEGORY = {
"programming-languages": ["TypeScript", "Python", "C/C++", "PHP"], "software-development": [
frontend: ["HTML", "CSS", "Tailwind CSS", "React.js (+ Next.js)"], "TypeScript",
backend: ["Laravel", "Node.js", "Fastify", "PostgreSQL"], "React.js (+ Next.js)",
"software-tools": [ "Tailwind CSS",
"GNU/Linux", "Node.js",
"Arch Linux", "tRPC/oRPC",
"Visual Studio Code", "PostgreSQL",
"Git",
"Docker",
], ],
"sys-admin": ["Docker", "Proxmox", "Caddy", "GitHub Actions", "GitLab CI/CD"],
"software-tools": ["GNU/Linux", "Arch Linux", "Visual Studio Code", "Git"],
"systems-programming": ["C/C++", "Rust", "Go"],
} as const satisfies { } as const satisfies {
[key in SkillCategory]: SkillName[] [key in SkillCategory]: SkillName[]
} }

1879
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -12,14 +12,14 @@ catalog:
# TypeScript # TypeScript
"typescript": "5.9.2" "typescript": "5.9.2"
"@total-typescript/ts-reset": "0.6.1" "@total-typescript/ts-reset": "0.6.1"
"@types/node": "24.3.0" "@types/node": "24.3.1"
# Utils # Utils
"mime": "4.0.7" "mime": "4.0.7"
# React.js/Next.js # React.js/Next.js
"next": &next "15.5.2" "next": &next "15.5.2"
"next-intl": "4.3.5" "next-intl": "4.3.7"
"next-themes": "0.4.6" "next-themes": "0.4.6"
"react": "19.1.1" "react": "19.1.1"
"react-dom": "19.1.1" "react-dom": "19.1.1"
@@ -48,11 +48,11 @@ catalog:
# ESLint # ESLint
"globals": "16.3.0" "globals": "16.3.0"
"typescript-eslint": "8.41.0" "typescript-eslint": "8.43.0"
"eslint": "9.34.0" "eslint": "9.35.0"
"eslint-config-conventions": "20.1.3" "eslint-config-conventions": "20.2.0"
"eslint-plugin-promise": "7.2.1" "eslint-plugin-promise": "7.2.1"
"eslint-plugin-unicorn": "60.0.0" "eslint-plugin-unicorn": "61.0.2"
"eslint-plugin-import-x": "4.16.1" "eslint-plugin-import-x": "4.16.1"
"@next/eslint-plugin-next": *next "@next/eslint-plugin-next": *next
"eslint-plugin-react": "7.37.5" "eslint-plugin-react": "7.37.5"
@@ -64,7 +64,7 @@ catalog:
"editorconfig-checker": "6.1.0" "editorconfig-checker": "6.1.0"
# Storybook # Storybook
"storybook": &storybook "9.1.3" "storybook": &storybook "9.1.5"
"@storybook/addon-docs": *storybook "@storybook/addon-docs": *storybook
"@storybook/addon-a11y": *storybook "@storybook/addon-a11y": *storybook
"@storybook/nextjs": *storybook "@storybook/nextjs": *storybook
@@ -76,17 +76,17 @@ catalog:
# Testing # Testing
"playwright": &playwright "1.55.0" "playwright": &playwright "1.55.0"
"@playwright/test": *playwright "@playwright/test": *playwright
"start-server-and-test": "2.0.13" "start-server-and-test": "2.1.0"
# CSS # CSS
"postcss": "8.5.6" "postcss": "8.5.6"
"@tailwindcss/postcss": "4.1.12" "@tailwindcss/postcss": "4.1.13"
"@tailwindcss/typography": "0.5.16" "@tailwindcss/typography": "0.5.16"
"tailwindcss": "4.1.12" "tailwindcss": "4.1.13"
"tailwind-merge": "3.3.1" "tailwind-merge": "3.3.1"
"clsx": "2.1.1" "clsx": "2.1.1"
"cva": "1.0.0-beta.4" "cva": "1.0.0-beta.4"
"@fontsource/montserrat": "5.2.6" "@fontsource/montserrat": "5.2.7"
onlyBuiltDependencies: onlyBuiltDependencies:
- "@swc/core" - "@swc/core"