From 834d7ed00fa3ebabe8d2938857db0cc4155bf689 Mon Sep 17 00:00:00 2001
From: Lucien Cartier-Tilet
Date: Thu, 9 Jul 2026 16:24:51 +0200
Subject: [PATCH] feat: add projects page and rework home page
---
app/assets/css/colors.css | 46 +++++-----
app/assets/css/main.css | 1 +
app/assets/css/tailwind.css | 2 +-
app/assets/css/typography.css | 19 +++++
app/assets/css/ui/images.css | 9 ++
app/assets/css/ui/index.css | 1 +
app/components/AppNavbar.vue | 4 +-
app/components/Ui/ProjectLink.vue | 32 +++++++
app/components/Ui/SingleProject.vue | 23 +++++
app/components/VocalSynth/Projects.vue | 2 +-
app/components/navbar/ThemeSwitcher.vue | 2 +-
app/composables/useApi.ts | 37 ++++----
app/composables/useCrates.ts | 8 ++
app/composables/useGithub.ts | 10 +++
app/pages/projects.vue | 18 ++++
app/pages/resume.vue | 8 +-
app/types/crate.ts | 25 ++++++
app/types/github-repository.ts | 108 ++++++++++++++++++++++++
app/types/project.ts | 23 +++++
app/types/resume.ts | 50 ++++++++++-
app/types/time.ts | 9 ++
app/utils/i18n.ts | 21 +++++
content/en/projects.json | 85 +++++++++++++++++++
content/en/resume.json | 59 ++++++++++---
content/fr/index.md | 2 +
content/fr/languages.md | 2 +-
content/fr/projects.json | 85 +++++++++++++++++++
content/fr/resume.json | 60 ++++++++++---
i18n/locales/en.json | 3 +
i18n/locales/fr.json | 7 ++
package.json | 2 +
pnpm-lock.yaml | 34 ++++++++
32 files changed, 720 insertions(+), 77 deletions(-)
create mode 100644 app/assets/css/typography.css
create mode 100644 app/assets/css/ui/images.css
create mode 100644 app/components/Ui/ProjectLink.vue
create mode 100644 app/components/Ui/SingleProject.vue
create mode 100644 app/composables/useCrates.ts
create mode 100644 app/composables/useGithub.ts
create mode 100644 app/pages/projects.vue
create mode 100644 app/types/crate.ts
create mode 100644 app/types/github-repository.ts
create mode 100644 app/types/project.ts
create mode 100644 app/types/time.ts
create mode 100644 app/utils/i18n.ts
create mode 100644 content/en/projects.json
create mode 100644 content/fr/projects.json
diff --git a/app/assets/css/colors.css b/app/assets/css/colors.css
index bfd3670..9c2f284 100644
--- a/app/assets/css/colors.css
+++ b/app/assets/css/colors.css
@@ -1,5 +1,5 @@
:root {
- --text: oklch(38.30% 0.029 266.48);
+ --text: oklch(38.3% 0.029 266.48);
--text-50: oklch(95.82% 0.004 271.37);
--text-100: oklch(91.83% 0.009 264.52);
--text-200: oklch(83.53% 0.016 266.26);
@@ -8,30 +8,30 @@
--text-500: oklch(57.02% 0.047 267.31);
--text-600: oklch(48.66% 0.039 268.21);
--text-700: oklch(40.13% 0.031 265.23);
- --text-800: oklch(30.90% 0.021 265.90);
+ --text-800: oklch(30.9% 0.021 265.9);
--text-900: oklch(20.86% 0.013 264.25);
--text-950: oklch(15.46% 0.007 270.96);
--background: oklch(95.13% 0.007 260.73);
- --background-50: oklch(95.80% 0.007 268.55);
+ --background-50: oklch(95.8% 0.007 268.55);
--background-100: oklch(91.74% 0.012 259.82);
--background-200: oklch(83.07% 0.027 262.33);
--background-300: oklch(74.46% 0.041 261.48);
--background-400: oklch(65.63% 0.058 260.56);
--background-500: oklch(56.42% 0.075 261.41);
- --background-600: oklch(48.32% 0.062 260.40);
+ --background-600: oklch(48.32% 0.062 260.4);
--background-700: oklch(39.64% 0.048 261.18);
--background-800: oklch(30.43% 0.036 261.92);
--background-900: oklch(20.77% 0.018 259.72);
- --background-950: oklch(15.29% 0.010 255.44);
+ --background-950: oklch(15.29% 0.01 255.44);
--primary: oklch(77.15% 0.062 217.48);
- --primary-50: oklch(96.50% 0.009 222.06);
+ --primary-50: oklch(96.5% 0.009 222.06);
--primary-100: oklch(93.16% 0.019 213.42);
--primary-200: oklch(86.07% 0.039 217.46);
--primary-300: oklch(79.25% 0.057 216.55);
--primary-400: oklch(72.48% 0.075 217.32);
- --primary-500: oklch(65.88% 0.089 218.00);
+ --primary-500: oklch(65.88% 0.089 218);
--primary-600: oklch(55.99% 0.075 218.52);
--primary-700: oklch(45.64% 0.059 218.22);
--primary-800: oklch(34.67% 0.043 219.39);
@@ -41,7 +41,7 @@
--secondary: oklch(69.65% 0.059 248.69);
--secondary-50: oklch(95.95% 0.008 253.85);
--secondary-100: oklch(92.05% 0.015 244.73);
- --secondary-200: oklch(83.76% 0.030 248.19);
+ --secondary-200: oklch(83.76% 0.03 248.19);
--secondary-300: oklch(75.31% 0.048 249.46);
--secondary-400: oklch(66.99% 0.065 248.83);
--secondary-500: oklch(58.35% 0.083 249.96);
@@ -51,16 +51,16 @@
--secondary-900: oklch(20.99% 0.022 251.79);
--secondary-950: oklch(15.56% 0.012 241.97);
- --accent: oklch(59.38% 0.078 253.40);
- --accent-50: oklch(95.93% 0.007 247.90);
+ --accent: oklch(59.38% 0.078 253.4);
+ --accent-50: oklch(95.93% 0.007 247.9);
--accent-100: oklch(91.85% 0.015 251.16);
- --accent-200: oklch(83.39% 0.030 254.70);
+ --accent-200: oklch(83.39% 0.03 254.7);
--accent-300: oklch(74.95% 0.046 253.67);
--accent-400: oklch(66.37% 0.064 253.29);
--accent-500: oklch(57.47% 0.081 254.47);
--accent-600: oklch(49.19% 0.068 253.56);
--accent-700: oklch(40.31% 0.053 254.02);
- --accent-800: oklch(30.91% 0.038 255.00);
+ --accent-800: oklch(30.91% 0.038 255);
--accent-900: oklch(20.99% 0.022 251.79);
--accent-950: oklch(15.35% 0.012 260.39);
}
@@ -68,7 +68,7 @@
--text: oklch(76.63% 0.024 266.86);
--text-50: oklch(15.46% 0.007 270.96);
--text-100: oklch(20.86% 0.013 264.25);
- --text-200: oklch(30.90% 0.021 265.90);
+ --text-200: oklch(30.9% 0.021 265.9);
--text-300: oklch(40.13% 0.031 265.23);
--text-400: oklch(48.66% 0.039 268.21);
--text-500: oklch(57.02% 0.047 267.31);
@@ -79,17 +79,17 @@
--text-950: oklch(95.82% 0.004 271.37);
--background: oklch(16.29% 0.012 260.61);
- --background-50: oklch(15.29% 0.010 255.44);
+ --background-50: oklch(15.29% 0.01 255.44);
--background-100: oklch(20.77% 0.018 259.72);
--background-200: oklch(30.43% 0.036 261.92);
--background-300: oklch(39.64% 0.048 261.18);
- --background-400: oklch(48.32% 0.062 260.40);
+ --background-400: oklch(48.32% 0.062 260.4);
--background-500: oklch(56.42% 0.075 261.41);
--background-600: oklch(65.63% 0.058 260.56);
--background-700: oklch(74.46% 0.041 261.48);
--background-800: oklch(83.07% 0.027 262.33);
--background-900: oklch(91.74% 0.012 259.82);
- --background-950: oklch(95.80% 0.007 268.55);
+ --background-950: oklch(95.8% 0.007 268.55);
--primary: oklch(48.89% 0.064 217.48);
--primary-50: oklch(16.48% 0.015 212.62);
@@ -97,12 +97,12 @@
--primary-200: oklch(34.67% 0.043 219.39);
--primary-300: oklch(45.64% 0.059 218.22);
--primary-400: oklch(55.99% 0.075 218.52);
- --primary-500: oklch(65.88% 0.089 218.00);
+ --primary-500: oklch(65.88% 0.089 218);
--primary-600: oklch(72.48% 0.075 217.32);
--primary-700: oklch(79.25% 0.057 216.55);
--primary-800: oklch(86.07% 0.039 217.46);
--primary-900: oklch(93.16% 0.019 213.42);
- --primary-950: oklch(96.50% 0.009 222.06);
+ --primary-950: oklch(96.5% 0.009 222.06);
--secondary: oklch(47.12% 0.064 249.33);
--secondary-50: oklch(15.56% 0.012 241.97);
@@ -113,20 +113,20 @@
--secondary-500: oklch(58.35% 0.083 249.96);
--secondary-600: oklch(66.99% 0.065 248.83);
--secondary-700: oklch(75.31% 0.048 249.46);
- --secondary-800: oklch(83.76% 0.030 248.19);
+ --secondary-800: oklch(83.76% 0.03 248.19);
--secondary-900: oklch(92.05% 0.015 244.73);
--secondary-950: oklch(95.95% 0.008 253.85);
- --accent: oklch(55.80% 0.080 254.61);
+ --accent: oklch(55.8% 0.08 254.61);
--accent-50: oklch(15.35% 0.012 260.39);
--accent-100: oklch(20.99% 0.022 251.79);
- --accent-200: oklch(30.91% 0.038 255.00);
+ --accent-200: oklch(30.91% 0.038 255);
--accent-300: oklch(40.31% 0.053 254.02);
--accent-400: oklch(49.19% 0.068 253.56);
--accent-500: oklch(57.47% 0.081 254.47);
--accent-600: oklch(66.37% 0.064 253.29);
--accent-700: oklch(74.95% 0.046 253.67);
- --accent-800: oklch(83.39% 0.030 254.70);
+ --accent-800: oklch(83.39% 0.03 254.7);
--accent-900: oklch(91.85% 0.015 251.16);
- --accent-950: oklch(95.93% 0.007 247.90);
+ --accent-950: oklch(95.93% 0.007 247.9);
}
diff --git a/app/assets/css/main.css b/app/assets/css/main.css
index 2001e29..531aafa 100644
--- a/app/assets/css/main.css
+++ b/app/assets/css/main.css
@@ -2,6 +2,7 @@
@import './colors.css';
@import './ui/index.css';
@import './tailwind.css';
+@import './typography.css';
@source "../../../content/**/*";
diff --git a/app/assets/css/tailwind.css b/app/assets/css/tailwind.css
index c3d3117..7dc0b99 100644
--- a/app/assets/css/tailwind.css
+++ b/app/assets/css/tailwind.css
@@ -80,6 +80,6 @@
Noto Sans, ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
'Noto Color Emoji';
--font-title:
- Wittgenstein, ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
+ Montserrat, ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
'Noto Color Emoji';
}
diff --git a/app/assets/css/typography.css b/app/assets/css/typography.css
new file mode 100644
index 0000000..793f4eb
--- /dev/null
+++ b/app/assets/css/typography.css
@@ -0,0 +1,19 @@
+@layer base {
+ body {
+ font-family:
+ 'Noto Sans', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
+ 'Noto Color Emoji';
+ font-weight: 400;
+ }
+
+ h1,
+ h2,
+ h3,
+ h4,
+ h5 {
+ font-family:
+ 'Montserrat', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
+ 'Noto Color Emoji';
+ font-weight: 700;
+ }
+}
diff --git a/app/assets/css/ui/images.css b/app/assets/css/ui/images.css
new file mode 100644
index 0000000..deb4e4e
--- /dev/null
+++ b/app/assets/css/ui/images.css
@@ -0,0 +1,9 @@
+.hero-image {
+ max-width: 7rem;
+ height: auto;
+ display: block;
+}
+
+.centered-image {
+ margin: 0 auto;
+}
diff --git a/app/assets/css/ui/index.css b/app/assets/css/ui/index.css
index 0f421a3..ce51591 100644
--- a/app/assets/css/ui/index.css
+++ b/app/assets/css/ui/index.css
@@ -2,3 +2,4 @@
@import './text.css';
@import './background.css';
@import './border.css';
+@import './images.css';
diff --git a/app/components/AppNavbar.vue b/app/components/AppNavbar.vue
index 95c2c80..7f0373e 100644
--- a/app/components/AppNavbar.vue
+++ b/app/components/AppNavbar.vue
@@ -13,6 +13,8 @@
diff --git a/app/components/Ui/SingleProject.vue b/app/components/Ui/SingleProject.vue
new file mode 100644
index 0000000..beb0453
--- /dev/null
+++ b/app/components/Ui/SingleProject.vue
@@ -0,0 +1,23 @@
+
+
+ {{ project.name }}
+
+ {{ project.description }}
+
+
+
+
+
+
+
+
+
diff --git a/app/components/VocalSynth/Projects.vue b/app/components/VocalSynth/Projects.vue
index 6d87e64..a24efa0 100644
--- a/app/components/VocalSynth/Projects.vue
+++ b/app/components/VocalSynth/Projects.vue
@@ -4,7 +4,7 @@
{{ $t('pages.vocal-synthesis.projects') }}
-
+
type Theme = 'light' | 'dark' | 'system';
-const icons: Dictionary
= {
+const icons: Record = {
light: 'material-symbols:light-mode',
dark: 'material-symbols:dark-mode',
system: 'material-symbols:computer-outline',
diff --git a/app/composables/useApi.ts b/app/composables/useApi.ts
index 09228c1..c4a1185 100644
--- a/app/composables/useApi.ts
+++ b/app/composables/useApi.ts
@@ -13,12 +13,17 @@ export interface UseApi {
patch: (path: string, opts?: FetchOptions, immediate?: boolean, body?: B) => UseApiResponse;
}
+export interface CreateRequestOptions {
+ host?: string;
+ path: string;
+ immediate?: boolean;
+ method: HttpMethod;
+ body?: PayloadT;
+ opts?: FetchOptions;
+}
+
const createRequest = (
- method: HttpMethod,
- url: string,
- opts?: FetchOptions,
- immediate: boolean = true,
- body?: PayloadT,
+ options: CreateRequestOptions,
): QueryResult => {
const response = new QueryResult();
const { apiBase } = useRuntimeConfig().public;
@@ -28,10 +33,10 @@ const createRequest = (
response.error.value = null;
try {
- const res = await $fetch(url, {
- baseURL: apiBase,
- ...opts,
- method,
+ const res = await $fetch(options.path, {
+ ...options.opts,
+ baseURL: options.host ?? apiBase,
+ method: options.method,
body: requestBody ?? undefined,
});
response.data.value = res;
@@ -48,22 +53,22 @@ const createRequest = (
};
response.run = run;
- if (immediate) run(body);
+ if (options.immediate) run(options.body);
return response;
};
-export const useApi = (): UseApi => {
+export const useApi = (host?: string): UseApi => {
const get = (path: string, opts?: FetchOptions, immediate: boolean = true) =>
- createRequest('GET', path, opts, immediate);
+ createRequest({ method: 'GET', path, opts, immediate, host });
const del = (path: string, opts?: FetchOptions, immediate: boolean = true) =>
- createRequest('DELETE', path, opts, immediate);
+ createRequest({ method: 'DELETE', path, opts, immediate, host });
const post = (path: string, opts?: FetchOptions, immediate: boolean = true, body?: B) =>
- createRequest('POST', path, opts, immediate, body);
+ createRequest({ method: 'POST', path, opts, immediate, body, host });
const put = (path: string, opts?: FetchOptions, immediate: boolean = true, body?: B) =>
- createRequest('PUT', path, opts, immediate, body);
+ createRequest({ method: 'PUT', path, opts, immediate, body, host });
const patch = (path: string, opts?: FetchOptions, immediate: boolean = true, body?: B) =>
- createRequest('PATCH', path, opts, immediate, body);
+ createRequest({ method: 'PATCH', path, opts, immediate, body, host });
return { get, post, put, patch, del };
};
diff --git a/app/composables/useCrates.ts b/app/composables/useCrates.ts
new file mode 100644
index 0000000..4012cc9
--- /dev/null
+++ b/app/composables/useCrates.ts
@@ -0,0 +1,8 @@
+import type { CrateMeta } from '~/types/crate';
+
+export const useCrates = () => {
+ const api = useApi('https://crates.io/api/v1/');
+ const getCrateInfo = (name: string): UseApiResponse => api.get(`crates/${name}`);
+
+ return { getCrateInfo };
+};
diff --git a/app/composables/useGithub.ts b/app/composables/useGithub.ts
new file mode 100644
index 0000000..1ada521
--- /dev/null
+++ b/app/composables/useGithub.ts
@@ -0,0 +1,10 @@
+import type { GithubRepository } from '~/types/github-repository';
+
+export const useGithub = () => {
+ const api = useApi('https://api.github.com');
+
+ const getRepoMeta = (owner: string, name: string): UseApiResponse =>
+ api.get(`repos/${owner}/${name}`);
+
+ return { getRepoMeta };
+};
diff --git a/app/pages/projects.vue b/app/pages/projects.vue
new file mode 100644
index 0000000..5a285d4
--- /dev/null
+++ b/app/pages/projects.vue
@@ -0,0 +1,18 @@
+
+
+
+ {{ $t('pages.projects.name') }}
+
+
+
+
+
+
+
+
diff --git a/app/pages/resume.vue b/app/pages/resume.vue
index 3f4eba5..012048f 100644
--- a/app/pages/resume.vue
+++ b/app/pages/resume.vue
@@ -7,7 +7,7 @@
{{ $t('pages.resume.experience') }}
-
+
@@ -22,7 +22,7 @@
{{ $t('pages.resume.education') }}
-
+
{{ $t('pages.resume.tools') }}
{{ $t('pages.resume.devops') }}
@@ -35,7 +35,7 @@