feat: fill pages
This commit is contained in:
@@ -1,5 +0,0 @@
|
|||||||
export NIX_SHELL_NAME=frontend
|
|
||||||
|
|
||||||
# Local Variables:
|
|
||||||
# mode: dotenv
|
|
||||||
# End:
|
|
||||||
17
frontend/.prettierrc
Normal file
17
frontend/.prettierrc
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"arrowParens": "always",
|
||||||
|
"bracketSpacing": true,
|
||||||
|
"endOfLine": "lf",
|
||||||
|
"experimentalOperatorPosition": "start",
|
||||||
|
"experimentalTernaries": true,
|
||||||
|
"jsxSingleQuote": true,
|
||||||
|
"printWidth": 120,
|
||||||
|
"proseWrap": "preserve",
|
||||||
|
"quoteProps": "as-needed",
|
||||||
|
"semi": true,
|
||||||
|
"singleQuote": true,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"trailingComma": "all",
|
||||||
|
"useTabs": false,
|
||||||
|
"vueIndentScriptAndStyle": false
|
||||||
|
}
|
||||||
@@ -2,9 +2,7 @@
|
|||||||
<UApp :locale="locales[locale]">
|
<UApp :locale="locales[locale]">
|
||||||
<AppNavbar />
|
<AppNavbar />
|
||||||
<UMain>
|
<UMain>
|
||||||
<NuxtLayout>
|
<NuxtPage />
|
||||||
<NuxtPage />
|
|
||||||
</NuxtLayout>
|
|
||||||
</UMain>
|
</UMain>
|
||||||
<AppFooter />
|
<AppFooter />
|
||||||
</UApp>
|
</UApp>
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
@import 'tailwindcss';
|
|
||||||
@import '@nuxt/ui';
|
@import '@nuxt/ui';
|
||||||
@import './colors.css';
|
@import './colors.css';
|
||||||
@import './ui/index.css';
|
@import './ui/index.css';
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
@layer base {
|
@import 'tailwindcss';
|
||||||
|
@theme {
|
||||||
--color-text-50: var(--text-50);
|
--color-text-50: var(--text-50);
|
||||||
--color-text: var(--text);
|
--color-text: var(--text);
|
||||||
--color-text-100: var(--text-100);
|
--color-text-100: var(--text-100);
|
||||||
@@ -76,9 +77,9 @@
|
|||||||
--text-weight-bold: 700;
|
--text-weight-bold: 700;
|
||||||
|
|
||||||
--font-sans:
|
--font-sans:
|
||||||
Noto Sans, ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji",
|
Noto Sans, ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
|
||||||
"Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
'Noto Color Emoji';
|
||||||
--font-title:
|
--font-title:
|
||||||
Wittgenstein, ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji",
|
Wittgenstein, ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
|
||||||
"Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
'Noto Color Emoji';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
:root {
|
:root {
|
||||||
--ui-primary: var(--color-primary);
|
--ui-primary: var(--primary);
|
||||||
--ui-secondary: var(--color-secondary);
|
--ui-secondary: var(--secondary);
|
||||||
--ui-success: var(--color-accent);
|
--ui-success: var(--accent);
|
||||||
--ui-info: var(--ui-color-info-500);
|
--ui-info: var(--ui-color-info-500);
|
||||||
--ui-warning: var(--ui-color-warning-500);
|
--ui-warning: var(--ui-color-warning-500);
|
||||||
--ui-error: var(--ui-color-error-500);
|
--ui-error: var(--ui-color-error-500);
|
||||||
}
|
}
|
||||||
.dark {
|
.dark {
|
||||||
--ui-primary: var(--color-primary-dark);
|
--ui-primary: var(--primary);
|
||||||
--ui-secondary: var(--color-secondary-dark);
|
--ui-secondary: var(--secondary);
|
||||||
--ui-success: var(--color-accent);
|
--ui-success: var(--accent);
|
||||||
--ui-info: var(--ui-color-info-400);
|
--ui-info: var(--ui-color-info-400);
|
||||||
--ui-warning: var(--ui-color-warning-400);
|
--ui-warning: var(--ui-color-warning-400);
|
||||||
--ui-error: var(--ui-color-error-400);
|
--ui-error: var(--ui-color-error-400);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@import "./colors.css";
|
@import './colors.css';
|
||||||
@import "./text.css";
|
@import './text.css';
|
||||||
@import "./background.css";
|
@import './background.css';
|
||||||
@import "./border.css";
|
@import './border.css';
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
:root {
|
:root {
|
||||||
--ui-text-dimmed: var(--text-400);
|
--ui-text-dimmed: var(--text-800);
|
||||||
--ui-text-muted: var(--text-500);
|
--ui-text-muted: var(--text-700);
|
||||||
--ui-text-toned: var(--text-600);
|
--ui-text-toned: var(--text-600);
|
||||||
--ui-text: var(--text);
|
--ui-text: var(--text);
|
||||||
--ui-text-highlighted: var(--text-900);
|
--ui-text-highlighted: var(--text-900);
|
||||||
--ui-text-inverted: var(--text-50);
|
--ui-text-inverted: var(--text-50);
|
||||||
}
|
}
|
||||||
.dark {
|
.dark {
|
||||||
--ui-text-dimmed: var(--text-500);
|
--ui-text-dimmed: var(--text-800);
|
||||||
--ui-text-muted: var(--text-400);
|
--ui-text-muted: var(--text-700);
|
||||||
--ui-text-toned: var(--text-300);
|
--ui-text-toned: var(--text-600);
|
||||||
--ui-text: var(--text);
|
--ui-text: var(--text);
|
||||||
--ui-text-highlighted: var(--text);
|
--ui-text-highlighted: var(--text);
|
||||||
--ui-text-inverted: var(--text-50);
|
--ui-text-inverted: var(--text-50);
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<UFooter>
|
<UFooter class="bg-background-200">
|
||||||
<template #left>
|
<template #left>
|
||||||
<p class="text-300 txt-sm">
|
<div class="flex flex-col gap-2">
|
||||||
Copyright © {{ new Date().getFullYear() }}
|
<p class="text-text-800 text-sm">Copyright © {{ new Date().getFullYear() }}</p>
|
||||||
</p>
|
<p class="text-text-800 text-sm">{{ $t('footer.versions.frontend') }}: {{ version }}</p>
|
||||||
|
<p class="text-text-800 text-sm">{{ $t('footer.versions.backend') }}: {{ meta?.version }}</p>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<UNavigationMenu :items="items" variant="link" />
|
<UNavigationMenu :items="items" variant="link" :orientation="orientation" />
|
||||||
|
|
||||||
<template #right>
|
<template #right>
|
||||||
<UButton
|
<UButton
|
||||||
@@ -23,11 +25,24 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { NavigationMenuItem } from '@nuxt/ui';
|
import type { NavigationMenuItem } from '@nuxt/ui';
|
||||||
|
import { version } from '../../package.json';
|
||||||
|
|
||||||
|
const { isMobile } = useDevice();
|
||||||
|
const orientation = computed(() => (isMobile ? 'vertical' : 'horizontal'));
|
||||||
|
const { getMeta } = useBackend();
|
||||||
|
const meta = await getMeta();
|
||||||
const items = computed<NavigationMenuItem[]>(() => [
|
const items = computed<NavigationMenuItem[]>(() => [
|
||||||
{
|
{
|
||||||
label: $t('footer.links.source'),
|
label: $t('footer.links.source'),
|
||||||
to: 'https://labs.phundrak.com/phundrak/phundrak.com',
|
to: 'https://labs.phundrak.com/phundrak/phundrak.com',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: $t('footer.links.nuxt'),
|
||||||
|
to: 'https://nuxt.com/',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: $t('footer.links.rust'),
|
||||||
|
to: 'https://rust-lang.org/',
|
||||||
|
},
|
||||||
]);
|
]);
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
13
frontend/app/components/Ui/BadgeList.vue
Normal file
13
frontend/app/components/Ui/BadgeList.vue
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="tools" class="flex flex-row gap-1 flex-wrap">
|
||||||
|
<UBadge v-for="tool in tools" :key="tool" size="md" variant="solid">
|
||||||
|
{{ tool }}
|
||||||
|
</UBadge>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const { tools } = defineProps<{
|
||||||
|
tools: string[];
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
14
frontend/app/components/Ui/BadgeListCard.vue
Normal file
14
frontend/app/components/Ui/BadgeListCard.vue
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<template>
|
||||||
|
<UPageCard class="bg-background-100 my-10">
|
||||||
|
<p class="text-xl">
|
||||||
|
<slot />
|
||||||
|
</p>
|
||||||
|
<UiBadgeList :tools="tools" />
|
||||||
|
</UPageCard>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const { tools } = defineProps<{
|
||||||
|
tools: string[];
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
35
frontend/app/components/VocalSynth/Projects.vue
Normal file
35
frontend/app/components/VocalSynth/Projects.vue
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<template>
|
||||||
|
<UPageCard class="bg-background-100 my-10">
|
||||||
|
<p class="text-xl">
|
||||||
|
{{ $t('pages.vocal-synthesis.projects') }}
|
||||||
|
</p>
|
||||||
|
<div class="flex flex-col max-w gap-10">
|
||||||
|
<div v-for="project in data?.projects" :key="project.title" class="flex flex-row max-w gap-5">
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="bg-primary text-text-50 dark:bg-primary p-1 rounded-md min-w-13 w-13 h-13 min-h-13 flex justify-center my-2"
|
||||||
|
>
|
||||||
|
<UIcon :name="project.icon" class="size-11" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<div class="flex flex-row gap-2 items-baseline">
|
||||||
|
<ULink :to="project.link" class="text-2xl">
|
||||||
|
{{ project.title }}
|
||||||
|
</ULink>
|
||||||
|
<UIcon v-if="external(project.link)" name="mdi:link" class="size-5" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{{ project.description }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</UPageCard>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
// Inject data provided by the page to avoid hydration issues with MDC components
|
||||||
|
const data = inject('pageData');
|
||||||
|
const external = (url: string) => url.startsWith('http');
|
||||||
|
</script>
|
||||||
8
frontend/app/components/VocalSynth/Tools.vue
Normal file
8
frontend/app/components/VocalSynth/Tools.vue
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<template>
|
||||||
|
<UiBadgeListCard v-if="data" :tools="data.tools">{{ $t('pages.vocal-synthesis.tools') }}</UiBadgeListCard>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
// Inject data provided by the page to avoid hydration issues with MDC components
|
||||||
|
const data = inject('pageData');
|
||||||
|
</script>
|
||||||
@@ -1,10 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<UDropdownMenu
|
<UDropdownMenu :key="locale" :items="availableLocales" :content="{ align: 'start' }">
|
||||||
:key="locale"
|
<UButton color="neutral" variant="outline" icon="material-symbols:globe" :aria-label="$t('menu.language')" />
|
||||||
:items="availableLocales"
|
|
||||||
:content="{ align: 'start' }"
|
|
||||||
>
|
|
||||||
<UButton color="neutral" variant="outline" icon="material-symbols:globe" />
|
|
||||||
</UDropdownMenu>
|
</UDropdownMenu>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<UDropdownMenu
|
<UDropdownMenu :key="colorMode.preference" :items="themes" :content="{ align: 'start' }">
|
||||||
:key="colorMode.preference"
|
<UButton color="neutral" variant="outline" :icon="icons[currentColor]" :aria-label="$t('menu.theme')" />
|
||||||
:items="themes"
|
|
||||||
:content="{ align: 'start' }"
|
|
||||||
>
|
|
||||||
<UButton color="neutral" variant="outline" :icon="icons[currentColor]" />
|
|
||||||
</UDropdownMenu>
|
</UDropdownMenu>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
32
frontend/app/composables/useApi.ts
Normal file
32
frontend/app/composables/useApi.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import type { FetchOptions } from 'ofetch';
|
||||||
|
|
||||||
|
export const useApi = () => {
|
||||||
|
const config = useRuntimeConfig();
|
||||||
|
const apiFetch = $fetch.create({
|
||||||
|
baseURL: config.public.apiBase,
|
||||||
|
});
|
||||||
|
|
||||||
|
const get = <T>(url: string, options?: FetchOptions) => apiFetch<T>(url, { method: 'GET', ...options });
|
||||||
|
|
||||||
|
const post = <ResultT, PayloadT = Record<string, string | number | boolean>>(
|
||||||
|
url: string,
|
||||||
|
body?: PayloadT,
|
||||||
|
options?: FetchOptions,
|
||||||
|
) => apiFetch<ResultT>(url, { method: 'POST', body, ...options });
|
||||||
|
|
||||||
|
const put = <ResultT, PayloadT = Record<string, string | number | boolean>>(
|
||||||
|
url: string,
|
||||||
|
body?: PayloadT,
|
||||||
|
options?: FetchOptions,
|
||||||
|
) => apiFetch<ResultT>(url, { method: 'PUT', body, ...options });
|
||||||
|
|
||||||
|
const patch = <ResultT, PayloadT = Record<string, string | number | boolean>>(
|
||||||
|
url: string,
|
||||||
|
body?: PayloadT,
|
||||||
|
options?: FetchOptions,
|
||||||
|
) => apiFetch<ResultT>(url, { method: 'PATCH', body, ...options });
|
||||||
|
|
||||||
|
const del = <T>(url: string, options?: FetchOptions) => apiFetch<T>(url, { method: 'DELETE', ...options });
|
||||||
|
|
||||||
|
return { get, post, put, patch, del };
|
||||||
|
};
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
export const useBackend = () => {
|
||||||
|
const api = useApi();
|
||||||
|
|
||||||
|
const getMeta = () => api.get<MetaResponse>('/meta');
|
||||||
|
const postContact = (contact: ContactRequest) => api.post<ContactRequest, ContactResponse>('/contact', contact);
|
||||||
|
|
||||||
|
return { getMeta, postContact };
|
||||||
|
};
|
||||||
|
|||||||
65
frontend/app/composables/useDataJson.ts
Normal file
65
frontend/app/composables/useDataJson.ts
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import { withLeadingSlash } from 'ufo';
|
||||||
|
import type { Collections } from '@nuxt/content';
|
||||||
|
|
||||||
|
export const useDataJson = (prefix: string) => {
|
||||||
|
const route = useRoute();
|
||||||
|
const { locale } = useI18n();
|
||||||
|
const slug = computed(() => {
|
||||||
|
// Use route.params.slug for dynamic routes, or route.path for static routes
|
||||||
|
const slugValue = route.params.slug || route.path;
|
||||||
|
return withLeadingSlash(String(slugValue));
|
||||||
|
});
|
||||||
|
const key = computed(() => prefix + '-' + slug.value);
|
||||||
|
|
||||||
|
const getData = async <T>(
|
||||||
|
collectionPrefix: string,
|
||||||
|
options: {
|
||||||
|
useFilter?: boolean;
|
||||||
|
fallbackToEnglish?: boolean;
|
||||||
|
extractMeta?: boolean;
|
||||||
|
} = {},
|
||||||
|
) => {
|
||||||
|
const { useFilter = false, fallbackToEnglish = false, extractMeta = false } = options;
|
||||||
|
|
||||||
|
const { data } = await useAsyncData(
|
||||||
|
key.value,
|
||||||
|
async () => {
|
||||||
|
const collection = (collectionPrefix + locale.value) as keyof Collections;
|
||||||
|
|
||||||
|
let content;
|
||||||
|
if (useFilter) {
|
||||||
|
// For data collections, use .all() and filter
|
||||||
|
const allData = await queryCollection(collection).all();
|
||||||
|
content = allData.filter((source) => source.meta.path == slug.value)[0];
|
||||||
|
} else {
|
||||||
|
// For page collections, use .path().first()
|
||||||
|
content = await queryCollection(collection).path(slug.value).first();
|
||||||
|
if (!content && fallbackToEnglish && locale.value !== 'en') {
|
||||||
|
content = await queryCollection('content_en').path(slug.value).first();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return extractMeta ? content?.meta : content;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
watch: [locale], // Automatically refresh when locale changes
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return data as Ref<T | null>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getJsonData = async (collectionPrefix: string = 'content_data_') => {
|
||||||
|
return getData(collectionPrefix, { useFilter: true, extractMeta: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPageContent = async (collectionPrefix: string = 'content_', fallbackToEnglish: boolean = true) => {
|
||||||
|
return getData(collectionPrefix, { fallbackToEnglish });
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCachedData = () => {
|
||||||
|
const { data } = useNuxtData(key.value);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
return { getJsonData, getPageContent, getCachedData };
|
||||||
|
};
|
||||||
27
frontend/app/composables/useMeta.ts
Normal file
27
frontend/app/composables/useMeta.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
export interface MetaImageOptions {
|
||||||
|
url: string;
|
||||||
|
alt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MetaOptions {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
image?: MetaImageOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useMeta = (options: MetaOptions) => {
|
||||||
|
const titleSuffix = ' – Lucien Cartier-Tilet';
|
||||||
|
useSeoMeta({
|
||||||
|
title: () => options.title + titleSuffix,
|
||||||
|
ogTitle: () => options.title + titleSuffix,
|
||||||
|
twitterTitle: () => options.title + titleSuffix,
|
||||||
|
description: () => options.description,
|
||||||
|
ogDescription: () => options.description,
|
||||||
|
twitterDescription: () => options.description,
|
||||||
|
twitterCard: options.image ? 'summary_large_image' : 'summary',
|
||||||
|
ogImage: () => options.image?.url,
|
||||||
|
ogImageAlt: () => options.image?.alt,
|
||||||
|
twitterImage: () => options.image?.url,
|
||||||
|
twitterImageAlt: () => options.image?.alt,
|
||||||
|
});
|
||||||
|
};
|
||||||
5
frontend/app/layouts/centered.vue
Normal file
5
frontend/app/layouts/centered.vue
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<template>
|
||||||
|
<div class="text-center prose prose-lg mx-auto max-w-prose">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="min-h-screen mx-auto px-4 py-8 max-w-6xl">
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
22
frontend/app/pages/[...slug].vue
Normal file
22
frontend/app/pages/[...slug].vue
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<template>
|
||||||
|
<NuxtLayout v-if="page" :name="page.meta?.layout ?? 'default'">
|
||||||
|
<ContentRenderer :value="page" />
|
||||||
|
</NuxtLayout>
|
||||||
|
<div v-else>
|
||||||
|
<h1>Page not found</h1>
|
||||||
|
<p>This page doesn't exist in {{ locale }} language.</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const { getPageContent } = useDataJson('page');
|
||||||
|
const page = await getPageContent();
|
||||||
|
|
||||||
|
// Pre-fetch JSON data for MDC components to avoid hydration issues
|
||||||
|
const { getJsonData } = useDataJson('page-data');
|
||||||
|
const pageData = await getJsonData();
|
||||||
|
// Provide data to child MDC components
|
||||||
|
provide('pageData', pageData);
|
||||||
|
|
||||||
|
useMeta({ title: page.value?.title, description: page.value?.description });
|
||||||
|
</script>
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div>
|
|
||||||
{{ $t('pages.contact.name') }}
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
<template>
|
|
||||||
<h1>{{ $t('website.name') }}</h1>
|
|
||||||
</template>
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div>
|
|
||||||
{{ $t('pages.languages.name') }}
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@@ -1,5 +1,47 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<NuxtLayout name="default">
|
||||||
{{ $t('pages.resume.name') }}
|
<h1 class="text-4xl text-highlighted font-bold mb-8">
|
||||||
</div>
|
{{ $t('pages.resume.name') }}
|
||||||
|
</h1>
|
||||||
|
<UPageCard class="bg-background-100 my-10">
|
||||||
|
<p>
|
||||||
|
{{ $t('pages.resume.experience') }}
|
||||||
|
</p>
|
||||||
|
<UTimeline v-model="valueExp" reverse :items="resumeContent?.experience" class="w-full">
|
||||||
|
<template #description="{ item }">
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<p>
|
||||||
|
{{ item.description }}
|
||||||
|
</p>
|
||||||
|
<UiBadgeList :tools="item.tools" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</UTimeline>
|
||||||
|
</UPageCard>
|
||||||
|
<UPageCard class="bg-background-100 my-10">
|
||||||
|
<p>
|
||||||
|
{{ $t('pages.resume.education') }}
|
||||||
|
</p>
|
||||||
|
<UTimeline v-model="valueEd" reverse :items="resumeContent?.education" class="w-full" />
|
||||||
|
</UPageCard>
|
||||||
|
<UiBadgeListCard :tools="resumeContent?.otherTools">{{ $t('pages.resume.tools') }}</UiBadgeListCard>
|
||||||
|
<UiBadgeListCard :tools="resumeContent?.devops">{{ $t('pages.resume.devops') }}</UiBadgeListCard>
|
||||||
|
<UiBadgeListCard :tools="resumeContent?.os">{{ $t('pages.resume.os') }}</UiBadgeListCard>
|
||||||
|
<UiBadgeListCard :tools="resumeContent?.programmingLanguages">{{
|
||||||
|
$t('pages.resume.programmingLanguages')
|
||||||
|
}}</UiBadgeListCard>
|
||||||
|
<UiBadgeListCard :tools="resumeContent?.frameworks">{{ $t('pages.resume.frameworks') }}</UiBadgeListCard>
|
||||||
|
</NuxtLayout>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
useMeta({
|
||||||
|
title: $t('pages.resume.name'),
|
||||||
|
description: $t('pages.resume.description'),
|
||||||
|
});
|
||||||
|
const { getJsonData } = useDataJson('resume');
|
||||||
|
const resumeContent = await getJsonData();
|
||||||
|
const arrLength = (array?: T[]) => (array ? array.length - 1 : 0);
|
||||||
|
const valueExp = computed(() => arrLength(resumeContent.value?.experience));
|
||||||
|
const valueEd = computed(() => arrLength(resumeContent.value?.education));
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div>
|
|
||||||
{{ $t('pages.vocal-synthesis.name') }}
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
11
frontend/app/types/api/contact.ts
Normal file
11
frontend/app/types/api/contact.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
export interface ContactRequest {
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
message: string;
|
||||||
|
website?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ContactResponse {
|
||||||
|
success: boolean;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
4
frontend/app/types/api/error.ts
Normal file
4
frontend/app/types/api/error.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export interface ApiError {
|
||||||
|
message: string;
|
||||||
|
success: boolean;
|
||||||
|
}
|
||||||
4
frontend/app/types/api/meta.ts
Normal file
4
frontend/app/types/api/meta.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export interface MetaResponse {
|
||||||
|
version: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
export interface Dictionary<K, T> {
|
export interface Dictionary<K, T> {
|
||||||
[key: K]: T
|
[key: K]: T;
|
||||||
}
|
}
|
||||||
|
|||||||
13
frontend/app/types/resume.ts
Normal file
13
frontend/app/types/resume.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
export interface ResumeExperience extends TimelineItem {
|
||||||
|
tools: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ResumeContent {
|
||||||
|
experience: ResumeExperience[];
|
||||||
|
education: TimelineItem[];
|
||||||
|
otherTools: string[];
|
||||||
|
devops: string[];
|
||||||
|
os: string[];
|
||||||
|
programmingLanguages: string[];
|
||||||
|
frameworks: string[];
|
||||||
|
}
|
||||||
0
frontend/app/types/tool.ts
Normal file
0
frontend/app/types/tool.ts
Normal file
@@ -1,14 +1,17 @@
|
|||||||
import { defineCollection, defineContentConfig } from '@nuxt/content';
|
import { defineCollection, defineContentConfig } from '@nuxt/content';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
const commonSchema = z.object({});
|
const commonSchema = z.object({
|
||||||
|
title: z.string(),
|
||||||
|
description: z.string()
|
||||||
|
});
|
||||||
|
|
||||||
export default defineContentConfig({
|
export default defineContentConfig({
|
||||||
collections: {
|
collections: {
|
||||||
content_en: defineCollection({
|
content_en: defineCollection({
|
||||||
type: 'page',
|
type: 'page',
|
||||||
source: {
|
source: {
|
||||||
include: 'en/**',
|
include: 'en/**/*.md',
|
||||||
prefix: '',
|
prefix: '',
|
||||||
},
|
},
|
||||||
schema: commonSchema,
|
schema: commonSchema,
|
||||||
@@ -16,18 +19,24 @@ export default defineContentConfig({
|
|||||||
content_fr: defineCollection({
|
content_fr: defineCollection({
|
||||||
type: 'page',
|
type: 'page',
|
||||||
source: {
|
source: {
|
||||||
include: 'fr/**',
|
include: 'fr/**/*.md',
|
||||||
prefix: '',
|
prefix: '',
|
||||||
},
|
},
|
||||||
schema: commonSchema,
|
schema: commonSchema,
|
||||||
}),
|
}),
|
||||||
content_lfn: defineCollection({
|
content_data_en: defineCollection({
|
||||||
type: 'page',
|
type: 'data',
|
||||||
source: {
|
source: {
|
||||||
include: 'lfn/**',
|
include: 'en/**/*.json',
|
||||||
prefix: '',
|
prefix: ''
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
content_data_fr: defineCollection({
|
||||||
|
type: 'data',
|
||||||
|
source: {
|
||||||
|
include: 'fr/**/*.json',
|
||||||
|
prefix: ''
|
||||||
},
|
},
|
||||||
schema: commonSchema,
|
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
12
frontend/content/en/index.md
Normal file
12
frontend/content/en/index.md
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
---
|
||||||
|
layout: centered
|
||||||
|
title: Home
|
||||||
|
description: Personal Website
|
||||||
|
---
|
||||||
|
|
||||||
|
# Welcome
|
||||||
|
|
||||||
|
Web Developer • Worldbuilder • Conlanger
|
||||||
|
|
||||||
|
Hi, I'm Lucien Cartier-Tilet. I work as a web developer and consultant. Outside of work, I spend time on worldbuilding
|
||||||
|
and constructed languages.
|
||||||
66
frontend/content/en/resume.json
Normal file
66
frontend/content/en/resume.json
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
{
|
||||||
|
"experience": [
|
||||||
|
{
|
||||||
|
"date": "Since Septembre 2023",
|
||||||
|
"title": "Consultant – Aubay",
|
||||||
|
"description": "Web development consultant working on enterprise applications. Continued focus on Angular front-end development and Java Spring Boot back-end services with PostgreSQL databases.",
|
||||||
|
"tools": [
|
||||||
|
"Angular",
|
||||||
|
"TypeScript",
|
||||||
|
"Java Spring Boot",
|
||||||
|
"Java Spring Batch",
|
||||||
|
"PostgreSQL",
|
||||||
|
"VS Code",
|
||||||
|
"Eclipse",
|
||||||
|
"IntelliJ Idea",
|
||||||
|
"Git"
|
||||||
|
],
|
||||||
|
"icon": "mdi:laptop"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "February 2023 – August 2023",
|
||||||
|
"title": "Intern – Aubay",
|
||||||
|
"description": "Web application development internship focused on full-stack development. Worked on projects using Angular for front-end and Java Spring Boot for back-end, with PostgreSQL databases.",
|
||||||
|
"tools": ["Angular", "TypeScript", "Java Spring Boot", "PostgreSQL", "VS Code", "Eclipse", "Git"],
|
||||||
|
"icon": "mdi:book"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "October 2014 – July 2018",
|
||||||
|
"title": "CTO – Voxwave",
|
||||||
|
"description": "Co-founded a startup specialized in creating French virtual singers using vocal synthesis. Developed singing synthesis vocal libraries, conducted linguistic research, provided user support, and trained recruits in vocal library development. Led technical development of ALYS, the first professional French singing voice library.",
|
||||||
|
"tools": ["Alter/Ego", "UTAU", "FL Studio", "iZotope RX", "T-RackS CS"],
|
||||||
|
"icon": "mdi:waveform"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"education": [
|
||||||
|
{
|
||||||
|
"date": "September 2022 – September 2023",
|
||||||
|
"title": "Master's Degree in Hypermedia Technologies – University of Paris 8",
|
||||||
|
"description": "Obtained Master's degree in THYP (Hypermedia Technologies) on 11 September 2023. Repeated the year for health reasons without any lasting effects.",
|
||||||
|
"icon": "mdi:network"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "September 2020 – September 2021",
|
||||||
|
"title": "Master's Degree in Computer Science – University of Paris 8",
|
||||||
|
"description": "First year of my Master’s degree.",
|
||||||
|
"icon": "mdi:code-tags"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "September 2016 – July 2019",
|
||||||
|
"title": "Bachelor's Degree in Computer Science – University of Paris 8",
|
||||||
|
"description": "Bachelor's degree in Computer Science obtained in July 2019",
|
||||||
|
"icon": "mdi:school-outline"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "Septembre 2013 – Décembre 2014",
|
||||||
|
"title": "English Literature – Université Lyon 2",
|
||||||
|
"description": "One and a half years of literary English studies in an LLCE English degree. Studies interrupted following the creation of VoxWave.",
|
||||||
|
"icon": "mdi:book-open-page-variant"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"otherTools": ["Emacs", "Vim", "jj", "PostgreSQL", "SQLite"],
|
||||||
|
"devops": ["GitHub", "Gitlab", "Gitea", "GitHub Actions", "Drone.io", "Docker", "Podman"],
|
||||||
|
"os": ["NixOS", "Debian", "Arch Linux", "Void Linux", "Alpine Linux", "Windows"],
|
||||||
|
"programmingLanguages": ["TypeScript", "Rust", "C", "EmacsLisp", "Bash/Zsh", "C++", "Python", "CommonLisp"],
|
||||||
|
"frameworks": ["Angular", "Vue", "Nuxt", "Spring Boot", "Poem (Rust)"]
|
||||||
|
}
|
||||||
35
frontend/content/en/vocal-synthesis.json
Normal file
35
frontend/content/en/vocal-synthesis.json
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"projects": [
|
||||||
|
{
|
||||||
|
"title": "BSUP01 KEINE Tashi series",
|
||||||
|
"icon": "mdi:microphone",
|
||||||
|
"description": "Released starting October 2012. My second vocal library, recorded with better equipment than my first attempt. The series included several Japanese vocal libraries, with the Extend Power version being my best work in this series.",
|
||||||
|
"link": "/keine-tashi"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "First Tibetan vocal libraries",
|
||||||
|
"icon": "mdi:earth",
|
||||||
|
"description": "BSUP01 KEINE Tashi and BSUP02 Drölma were the first Tibetan vocal libraries for singing synthesis worldwide.",
|
||||||
|
"link": "/keine-tashi"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "ALYS prototypes for UTAU",
|
||||||
|
"icon": "mdi:flask",
|
||||||
|
"description": "Created ALYS 001 JPN, ALYS 001 FRA, and ALYS 002 FRA as test versions while working at VoxWave. Known as ALYS4UTAU.",
|
||||||
|
"link": "https://alys.phundrak.com/en/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "ALYS for Alter/Ego",
|
||||||
|
"icon": "mdi:package",
|
||||||
|
"description": "The first commercial vocal library for Alter/Ego, and the first professional French singing vocal library. Development took well over a year, with eight to nine additional months for the first major update. Now available free of charge.",
|
||||||
|
"link": "https://alys.phundrak.com/en/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "LEORA",
|
||||||
|
"description": "A French singing vocal library developed at VoxWave alongside ALYS.",
|
||||||
|
"icon": "mdi:music",
|
||||||
|
"link": "https://alys.phundrak.com/en/faq#are-there-any-plans-for-leora"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tools": ["Alter/Ego", "UTAU", "VOCALOID", "ChipSpeech", "FL Studio", "Audacity", "iZotope RX", "T-RackS CS", "C++"]
|
||||||
|
}
|
||||||
17
frontend/content/en/vocal-synthesis.md
Normal file
17
frontend/content/en/vocal-synthesis.md
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
---
|
||||||
|
title: Vocal Synthesis
|
||||||
|
description:
|
||||||
|
Vocal synthesis projects from 2011-2018, including ALYS, the first professional French singing voice library for
|
||||||
|
Alter/Ego.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Vocal Synthesis
|
||||||
|
|
||||||
|
I worked in singing vocal synthesis from 2011 to 2018. I created vocal libraries for UTAU and Alter/Ego, including the
|
||||||
|
first professional French singing voice library.
|
||||||
|
|
||||||
|
:::VocalSynthProjects
|
||||||
|
:::
|
||||||
|
|
||||||
|
:::VocalSynthTools
|
||||||
|
:::
|
||||||
12
frontend/content/fr/index.md
Normal file
12
frontend/content/fr/index.md
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
---
|
||||||
|
layout: centered
|
||||||
|
title: Accueil
|
||||||
|
description: Site web personnel
|
||||||
|
---
|
||||||
|
|
||||||
|
# Bienvenue
|
||||||
|
|
||||||
|
Développeur web • Créateur d’univers • Idéolinguiste
|
||||||
|
|
||||||
|
Bonjour, je m'appelle Lucien Cartier-Tilet. Je travaille comme développeur web et consultant. En dehors du travail, je
|
||||||
|
consacre mon temps à la création d'univers et de langues construites.
|
||||||
66
frontend/content/fr/resume.json
Normal file
66
frontend/content/fr/resume.json
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
{
|
||||||
|
"experience": [
|
||||||
|
{
|
||||||
|
"date": "Depuis septembre 2023",
|
||||||
|
"title": "Consultant – Aubay",
|
||||||
|
"description": "Consultant en développement web travaillant sur des applications d'entreprise. Je continue à me concentrer sur le développement front-end Angular et les services back-end Java Spring Boot avec des bases de données PostgreSQL.",
|
||||||
|
"tools": [
|
||||||
|
"Angular",
|
||||||
|
"TypeScript",
|
||||||
|
"Java Spring Boot",
|
||||||
|
"Java Spring Batch",
|
||||||
|
"PostgreSQL",
|
||||||
|
"VS Code",
|
||||||
|
"Eclipse",
|
||||||
|
"IntelliJ Idea",
|
||||||
|
"Git"
|
||||||
|
],
|
||||||
|
"icon": "mdi:laptop"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "Février 2023 – Août 2023",
|
||||||
|
"title": "Stagiaire – Aubay",
|
||||||
|
"description": "Stage en développement d'applications web axé sur le développement full-stack. J'ai travaillé sur des projets utilisant Angular pour le front-end et Java Spring Boot pour le back-end, avec des bases de données PostgreSQL.",
|
||||||
|
"tools": ["Angular", "TypeScript", "Java Spring Boot", "PostgreSQL", "VS Code", "Eclipse", "Git"],
|
||||||
|
"icon": "mdi:book"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "Octobre 2014 – Juillet 2018",
|
||||||
|
"title": "Directeur technique – Voxwave",
|
||||||
|
"description": "Co-fondateur d'une start-up spécialisée dans la création de chanteurs virtuels français à l'aide de la synthèse vocale. Développement de banques vocales de synthèse chantée, recherche linguistique, assistance aux utilisateurs et formation des recrues au développement de banques vocales. Direction du développement technique d'ALYS, la première banques vocale professionnelle de chant en français.",
|
||||||
|
"tools": ["Alter/Ego", "UTAU", "FL Studio", "iZotope RX", "T-RackS CS"],
|
||||||
|
"icon": "mdi:waveform"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"education": [
|
||||||
|
{
|
||||||
|
"date": "Septembre 2022 – Septembre 2023",
|
||||||
|
"title": "Master 2 Technologies de l’Hypermédia – Université Paris 8",
|
||||||
|
"description": "Obtention du diplôme Master 2 THYP le 11 septembre 2023. Redoublement pour causes de santé sans séquelles.",
|
||||||
|
"icon": "mdi:network"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "Septembre 2020 – Septembre 2021",
|
||||||
|
"title": "Master 1 Informatique – Université Paris 8",
|
||||||
|
"description": "",
|
||||||
|
"icon": "mdi:code-tags"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "Septembre 2016 – Juillet 2019",
|
||||||
|
"title": "Licence Informatique – Université Paris 8",
|
||||||
|
"description": "Licence d’Informatique obtenue en Juillet 2019",
|
||||||
|
"icon": "mdi:school-outline"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "Septembre 2013 – Décembre 2014",
|
||||||
|
"title": "Anglais LLCE – Université Lyon 2",
|
||||||
|
"description": "Un an et demi d’études d’anglais littéraire en licence d’anglais LLCE. Études interrompues suite à la création de VoxWave.",
|
||||||
|
"icon": "mdi:book-open-page-variant"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"otherTools": ["Emacs", "Vim", "jj", "PostgreSQL", "SQLite"],
|
||||||
|
"devops": ["GitHub", "Gitlab", "Gitea", "GitHub Actions", "Drone.io", "Docker", "Podman"],
|
||||||
|
"os": ["NixOS", "Debian", "Arch Linux", "Void Linux", "Alpine Linux", "Windows"],
|
||||||
|
"programmingLanguages": ["TypeScript", "Rust", "C", "EmacsLisp", "Bash/Zsh", "C++", "Python", "CommonLisp"],
|
||||||
|
"frameworks": ["Angular", "Vue", "Nuxt", "Spring Boot", "Poem (Rust)"]
|
||||||
|
}
|
||||||
35
frontend/content/fr/vocal-synthesis.json
Normal file
35
frontend/content/fr/vocal-synthesis.json
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"projects": [
|
||||||
|
{
|
||||||
|
"title": "BSUP01 KEINE Tashi",
|
||||||
|
"icon": "mdi:microphone",
|
||||||
|
"description": "Sortie en octobre 2012. Ma deuxième bibliothèque vocale, enregistrée avec un équipement de meilleure qualité que ma première tentative. La série comprenait plusieurs bibliothèques vocales japonaises, la version Extend Power étant ma meilleure réalisation dans cette série.",
|
||||||
|
"link": "/keine-tashi"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Premières bibliothèques vocales tibétaines",
|
||||||
|
"icon": "mdi:earth",
|
||||||
|
"description": "BSUP01 KEINE Tashi et BSUP02 Drölma ont été les premières bibliothèques vocales tibétaines au monde dédiées à la synthèse vocale chantée.",
|
||||||
|
"link": "/keine-tashi"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Prototypes ALYS pour UTAU",
|
||||||
|
"icon": "mdi:flask",
|
||||||
|
"description": "Création des versions test ALYS 001 JPN, ALYS 001 FRA et ALYS 002 FRA chez VoxWave. Connue sous le nom d'ALYS4UTAU.",
|
||||||
|
"link": "https://alys.phundrak.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "ALYS pour Alter/Ego",
|
||||||
|
"icon": "mdi:package",
|
||||||
|
"description": "La première bibliothèque vocale commerciale pour Alter/Ego, et la première bibliothèque vocale professionnelle en français. Son développement a pris plus d'un an, avec huit à neuf mois supplémentaires pour la première mise à jour majeure. Elle est désormais disponible gratuitement.",
|
||||||
|
"link": "https://alys.phundrak.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "LEORA",
|
||||||
|
"description": "Une bibliothèque vocale française développée chez VoxWave en collaboration avec ALYS.",
|
||||||
|
"icon": "mdi:music",
|
||||||
|
"link": "https://alys.phundrak.com/faq#y-a-t-il-quelque-chose-de-prevu-pour-leora"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tools": ["Alter/Ego", "UTAU", "VOCALOID", "ChipSpeech", "FL Studio", "Audacity", "iZotope RX", "T-RackS CS", "C++"]
|
||||||
|
}
|
||||||
15
frontend/content/fr/vocal-synthesis.md
Normal file
15
frontend/content/fr/vocal-synthesis.md
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
---
|
||||||
|
title: Synthèse vocale
|
||||||
|
description:Projets de synthèse vocale de 2011 à 2018, dont ALYS, la première bibliothèque professionnelle de voix chantées en français pour Alter/Ego.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Synthèse vocale
|
||||||
|
|
||||||
|
J'ai travaillé dans le domaine de la synthèse vocale chantée de 2011 à 2018. J'ai créé des bibliothèques vocales pour
|
||||||
|
UTAU et Alter/Ego, notamment la première bibliothèque professionnelle de voix chantées en français.
|
||||||
|
|
||||||
|
:::VocalSynthProjects
|
||||||
|
:::
|
||||||
|
|
||||||
|
:::VocalSynthTools
|
||||||
|
:::
|
||||||
@@ -4,23 +4,35 @@
|
|||||||
"langSwitch": "Language"
|
"langSwitch": "Language"
|
||||||
},
|
},
|
||||||
"menu": {
|
"menu": {
|
||||||
"name": "Menu"
|
"name": "Menu",
|
||||||
|
"language": "Language Selector",
|
||||||
|
"theme": "Theme Selector"
|
||||||
},
|
},
|
||||||
"theme": {
|
"theme": {
|
||||||
"name": "theme",
|
"name": "theme",
|
||||||
"dark": "Dark",
|
"dark": "Dark",
|
||||||
"light": "Light",
|
"light": "Light",
|
||||||
"system": "System"
|
"system": "Auto"
|
||||||
},
|
},
|
||||||
"pages": {
|
"pages": {
|
||||||
"home": {
|
"home": {
|
||||||
"name": "Home"
|
"name": "Home"
|
||||||
},
|
},
|
||||||
"resume": {
|
"resume": {
|
||||||
"name": "Resume"
|
"name": "Resume",
|
||||||
|
"description": "",
|
||||||
|
"experience": "Experience",
|
||||||
|
"education": "Education",
|
||||||
|
"tools": "Tools",
|
||||||
|
"devops": "Devops Tools",
|
||||||
|
"os": "Operating Systems",
|
||||||
|
"programmingLanguages": "Programming Languages",
|
||||||
|
"frameworks": "Frameworks"
|
||||||
},
|
},
|
||||||
"vocal-synthesis": {
|
"vocal-synthesis": {
|
||||||
"name": "Vocal Synthesis"
|
"name": "Vocal Synthesis",
|
||||||
|
"projects": "Key Projects",
|
||||||
|
"tools": "Tools"
|
||||||
},
|
},
|
||||||
"languages": {
|
"languages": {
|
||||||
"name": "Languages & Worldbuilding"
|
"name": "Languages & Worldbuilding"
|
||||||
@@ -31,7 +43,13 @@
|
|||||||
},
|
},
|
||||||
"footer": {
|
"footer": {
|
||||||
"links": {
|
"links": {
|
||||||
"source": "Website’s source code"
|
"source": "Website’s source code",
|
||||||
|
"nuxt": "Frontend made with Nuxt",
|
||||||
|
"rust": "Backend made with Rust"
|
||||||
|
},
|
||||||
|
"versions": {
|
||||||
|
"frontend": "Frontend Version",
|
||||||
|
"backend": "Backend Version"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,23 +4,35 @@
|
|||||||
"langSwitch": "Language"
|
"langSwitch": "Language"
|
||||||
},
|
},
|
||||||
"menu": {
|
"menu": {
|
||||||
"name": "Menu"
|
"name": "Menu",
|
||||||
|
"language": "Choix de la langue",
|
||||||
|
"theme": "Thème du site web"
|
||||||
},
|
},
|
||||||
"theme": {
|
"theme": {
|
||||||
"name": "Thème",
|
"name": "Thème",
|
||||||
"dark": "Sombre",
|
"dark": "Sombre",
|
||||||
"light": "Clair",
|
"light": "Clair",
|
||||||
"system": "Système"
|
"system": "Auto"
|
||||||
},
|
},
|
||||||
"pages": {
|
"pages": {
|
||||||
"home": {
|
"home": {
|
||||||
"name": "Accueil"
|
"name": "Accueil"
|
||||||
},
|
},
|
||||||
"resume": {
|
"resume": {
|
||||||
"name": "CV"
|
"name": "CV",
|
||||||
|
"description": "",
|
||||||
|
"experience": "Expérience",
|
||||||
|
"education": "Éducation",
|
||||||
|
"tools": "Outils",
|
||||||
|
"devops": "Outils Devops",
|
||||||
|
"os": "Systèmes d’exploitation",
|
||||||
|
"programmingLanguages": "Langages de programmation",
|
||||||
|
"frameworks": "Frameworks"
|
||||||
},
|
},
|
||||||
"vocal-synthesis": {
|
"vocal-synthesis": {
|
||||||
"name": "Synthèse Vocale"
|
"name": "Synthèse Vocale",
|
||||||
|
"projects": "Projets principaux",
|
||||||
|
"tools": "Outils"
|
||||||
},
|
},
|
||||||
"languages": {
|
"languages": {
|
||||||
"name": "Langues et Univers Fictifs"
|
"name": "Langues et Univers Fictifs"
|
||||||
@@ -31,7 +43,13 @@
|
|||||||
},
|
},
|
||||||
"footer": {
|
"footer": {
|
||||||
"links": {
|
"links": {
|
||||||
"source": "Code source du site web "
|
"source": "Code source du site web",
|
||||||
|
"nuxt": "Frontend fait avec Nuxt",
|
||||||
|
"rust": "Backend fait avec Rust"
|
||||||
|
},
|
||||||
|
"versions": {
|
||||||
|
"frontend": "Frontend Version",
|
||||||
|
"backend": "Backend Version"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ export default defineNuxtConfig({
|
|||||||
'@nuxt/test-utils',
|
'@nuxt/test-utils',
|
||||||
'@nuxt/ui',
|
'@nuxt/ui',
|
||||||
'@nuxt/content',
|
'@nuxt/content',
|
||||||
'@vueuse/nuxt',
|
|
||||||
'@nuxtjs/i18n',
|
'@nuxtjs/i18n',
|
||||||
'@nuxtjs/turnstile',
|
'@nuxtjs/turnstile',
|
||||||
'@nuxtjs/device',
|
'@nuxtjs/device',
|
||||||
@@ -34,7 +33,7 @@ export default defineNuxtConfig({
|
|||||||
locales: [
|
locales: [
|
||||||
{ code: 'en', name: 'English', language: 'en-UK', file: 'en.json' },
|
{ code: 'en', name: 'English', language: 'en-UK', file: 'en.json' },
|
||||||
{ code: 'fr', name: 'Français', language: 'fr-FR', file: 'fr.json' },
|
{ code: 'fr', name: 'Français', language: 'fr-FR', file: 'fr.json' },
|
||||||
// { code: 'lfn', name: 'Lingua Franca Nova', language: 'lfn', file: 'lfn.json' },
|
// { code: 'lfn', name: 'Elefen', language: 'lfn', file: 'lfn.json' },
|
||||||
// { code: 'ei', name: 'Eittlandic', language: 'ei-ST', file: 'ei.json' },
|
// { code: 'ei', name: 'Eittlandic', language: 'ei-ST', file: 'ei.json' },
|
||||||
],
|
],
|
||||||
strategy: 'no_prefix',
|
strategy: 'no_prefix',
|
||||||
@@ -54,7 +53,10 @@ export default defineNuxtConfig({
|
|||||||
},
|
},
|
||||||
icon: {
|
icon: {
|
||||||
serverBundle: {
|
serverBundle: {
|
||||||
collections: ['material-symbols']
|
collections: ['material-symbols', 'mdi']
|
||||||
|
},
|
||||||
|
clientBundle: {
|
||||||
|
scan: true,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
postcss: {
|
postcss: {
|
||||||
@@ -71,5 +73,8 @@ export default defineNuxtConfig({
|
|||||||
turnstile: {
|
turnstile: {
|
||||||
secretKey: '', // Overriden by NUXT_TURNSTILE_SECRET_KEY
|
secretKey: '', // Overriden by NUXT_TURNSTILE_SECRET_KEY
|
||||||
},
|
},
|
||||||
|
public: {
|
||||||
|
apiBase: process.env.NUXT_PUBLIC_API_BASE || 'http://localhost:3100/api',
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,13 +2,17 @@
|
|||||||
"name": "frontend",
|
"name": "frontend",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
"version": "0.1.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "nuxt build",
|
"build": "nuxt build",
|
||||||
"dev": "nuxt dev",
|
"dev": "nuxt dev",
|
||||||
"generate": "nuxt generate",
|
"generate": "nuxt generate",
|
||||||
"preview": "nuxt preview",
|
"preview": "nuxt preview",
|
||||||
"postinstall": "nuxt prepare",
|
"postinstall": "nuxt prepare",
|
||||||
"cleanup": "nuxt cleanup"
|
"cleanup": "nuxt cleanup",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"format": "prettier --write app/ i18n/ content/",
|
||||||
|
"format-check": "prettier --check app/ i18n/ content/"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nuxt/content": "3.8.0",
|
"@nuxt/content": "3.8.0",
|
||||||
@@ -23,8 +27,6 @@
|
|||||||
"@nuxtjs/device": "3.2.4",
|
"@nuxtjs/device": "3.2.4",
|
||||||
"@nuxtjs/tailwindcss": "7.0.0-beta.0",
|
"@nuxtjs/tailwindcss": "7.0.0-beta.0",
|
||||||
"@nuxtjs/turnstile": "1.1.1",
|
"@nuxtjs/turnstile": "1.1.1",
|
||||||
"@vueuse/core": "^14.0.0",
|
|
||||||
"@vueuse/nuxt": "14.0.0",
|
|
||||||
"better-sqlite3": "^12.4.1",
|
"better-sqlite3": "^12.4.1",
|
||||||
"eslint": "^9.39.1",
|
"eslint": "^9.39.1",
|
||||||
"nitropack": "^2.12.9",
|
"nitropack": "^2.12.9",
|
||||||
@@ -38,6 +40,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@iconify-json/material-symbols": "^1.2.44",
|
"@iconify-json/material-symbols": "^1.2.44",
|
||||||
"@iconify-json/material-symbols-light": "^1.2.44",
|
"@iconify-json/material-symbols-light": "^1.2.44",
|
||||||
|
"@iconify-json/mdi": "^1.2.3",
|
||||||
"@nuxtjs/i18n": "^10.2.0",
|
"@nuxtjs/i18n": "^10.2.0",
|
||||||
"@tailwindcss/postcss": "^4.1.17",
|
"@tailwindcss/postcss": "^4.1.17",
|
||||||
"autoprefixer": "^10.4.22",
|
"autoprefixer": "^10.4.22",
|
||||||
@@ -45,5 +48,10 @@
|
|||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.6",
|
||||||
"tailwindcss": "^4.1.17",
|
"tailwindcss": "^4.1.17",
|
||||||
"zod": "^4.1.12"
|
"zod": "^4.1.12"
|
||||||
|
},
|
||||||
|
"pnpm": {
|
||||||
|
"overrides": {
|
||||||
|
"sharp": "0.33.4"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
2518
frontend/pnpm-lock.yaml
generated
2518
frontend/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user