feat: fill pages
All checks were successful
Publish Docker Images / build-and-publish (push) Successful in 10m25s
All checks were successful
Publish Docker Images / build-and-publish (push) Successful in 10m25s
This commit is contained in:
@@ -2,9 +2,7 @@
|
||||
<UApp :locale="locales[locale]">
|
||||
<AppNavbar />
|
||||
<UMain>
|
||||
<NuxtLayout>
|
||||
<NuxtPage />
|
||||
</NuxtLayout>
|
||||
<NuxtPage />
|
||||
</UMain>
|
||||
<AppFooter />
|
||||
</UApp>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
@import 'tailwindcss';
|
||||
@import '@nuxt/ui';
|
||||
@import './colors.css';
|
||||
@import './ui/index.css';
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
@layer base {
|
||||
@import 'tailwindcss';
|
||||
@theme {
|
||||
--color-text-50: var(--text-50);
|
||||
--color-text: var(--text);
|
||||
--color-text-100: var(--text-100);
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
:root {
|
||||
--ui-primary: var(--color-primary);
|
||||
--ui-secondary: var(--color-secondary);
|
||||
--ui-success: var(--color-accent);
|
||||
--ui-primary: var(--primary);
|
||||
--ui-secondary: var(--secondary);
|
||||
--ui-success: var(--accent);
|
||||
--ui-info: var(--ui-color-info-500);
|
||||
--ui-warning: var(--ui-color-warning-500);
|
||||
--ui-error: var(--ui-color-error-500);
|
||||
}
|
||||
.dark {
|
||||
--ui-primary: var(--color-primary-dark);
|
||||
--ui-secondary: var(--color-secondary-dark);
|
||||
--ui-success: var(--color-accent);
|
||||
--ui-primary: var(--primary);
|
||||
--ui-secondary: var(--secondary);
|
||||
--ui-success: var(--accent);
|
||||
--ui-info: var(--ui-color-info-400);
|
||||
--ui-warning: var(--ui-color-warning-400);
|
||||
--ui-error: var(--ui-color-error-400);
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
:root {
|
||||
--ui-text-dimmed: var(--text-400);
|
||||
--ui-text-muted: var(--text-500);
|
||||
--ui-text-dimmed: var(--text-800);
|
||||
--ui-text-muted: var(--text-700);
|
||||
--ui-text-toned: var(--text-600);
|
||||
--ui-text: var(--text);
|
||||
--ui-text-highlighted: var(--text-900);
|
||||
--ui-text-inverted: var(--text-50);
|
||||
}
|
||||
.dark {
|
||||
--ui-text-dimmed: var(--text-500);
|
||||
--ui-text-muted: var(--text-400);
|
||||
--ui-text-toned: var(--text-300);
|
||||
--ui-text-dimmed: var(--text-800);
|
||||
--ui-text-muted: var(--text-700);
|
||||
--ui-text-toned: var(--text-600);
|
||||
--ui-text: var(--text);
|
||||
--ui-text-highlighted: var(--text);
|
||||
--ui-text-inverted: var(--text-50);
|
||||
|
||||
@@ -1,12 +1,20 @@
|
||||
<template>
|
||||
<UFooter>
|
||||
<UFooter class="bg-background-200">
|
||||
<template #left>
|
||||
<p class="text-300 txt-sm">
|
||||
Copyright © {{ new Date().getFullYear() }}
|
||||
</p>
|
||||
<div class="flex flex-col gap-2">
|
||||
<p class="text-text-800 text-sm">
|
||||
Copyright © {{ new Date().getFullYear() }}
|
||||
</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>
|
||||
|
||||
<UNavigationMenu :items="items" variant="link" />
|
||||
<UNavigationMenu :items="items" variant="link" :orientation="orientation" />
|
||||
|
||||
<template #right>
|
||||
<UButton
|
||||
@@ -23,11 +31,24 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { NavigationMenuItem } from '@nuxt/ui';
|
||||
import { version } from '../../package.json';
|
||||
|
||||
const { isMobile } = useDevice();
|
||||
const orientation = computed(() => (isMobile ? 'vertical' : 'horizontal'));
|
||||
const { getMeta } = useBackend();
|
||||
const { data: meta } = getMeta();
|
||||
const items = computed<NavigationMenuItem[]>(() => [
|
||||
{
|
||||
label: $t('footer.links.source'),
|
||||
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>
|
||||
|
||||
44
frontend/app/components/VocalSynth/Projects.vue
Normal file
44
frontend/app/components/VocalSynth/Projects.vue
Normal file
@@ -0,0 +1,44 @@
|
||||
<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 projects?.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">
|
||||
const { getJsonData, getCachedData } = useDataJson('vocalSynth');
|
||||
await getJsonData('content_data_');
|
||||
const projects = getCachedData();
|
||||
const external = (url: string) => url.startsWith('http');
|
||||
</script>
|
||||
21
frontend/app/components/VocalSynth/Tools.vue
Normal file
21
frontend/app/components/VocalSynth/Tools.vue
Normal file
@@ -0,0 +1,21 @@
|
||||
<template>
|
||||
<UPageCard class="bg-background-100 my-10">
|
||||
<p class="text-xl">
|
||||
{{ $t('pages.vocal-synthesis.tools') }}
|
||||
</p>
|
||||
<div class="flex flex-row gap-5 flex-wrap">
|
||||
<UBadge
|
||||
v-for="tool in data?.tools"
|
||||
:key="tool"
|
||||
size="md"
|
||||
variant="solid"
|
||||
>{{ tool }}</UBadge
|
||||
>
|
||||
</div>
|
||||
</UPageCard>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const { getCachedData } = useDataJson('vocalSynth');
|
||||
const data = getCachedData();
|
||||
</script>
|
||||
37
frontend/app/composables/useApi.ts
Normal file
37
frontend/app/composables/useApi.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
export function useApi() {
|
||||
const config = useRuntimeConfig();
|
||||
const backend = config.public.apiBase;
|
||||
const commonBaseOptions = { baseURL: backend };
|
||||
|
||||
const get = (endpoint, options = {}) =>
|
||||
useFetch(endpoint, {
|
||||
method: 'GET',
|
||||
...commonBaseOptions,
|
||||
...options,
|
||||
});
|
||||
|
||||
const post = (endpoint, body, options = {}) =>
|
||||
$fetch(endpoint, {
|
||||
method: 'POST',
|
||||
body,
|
||||
...commonBaseOptions,
|
||||
...options,
|
||||
});
|
||||
|
||||
const put = (endpoint, body, options = {}) =>
|
||||
$fetch(endpoint, {
|
||||
body,
|
||||
method: 'PUT',
|
||||
...commonBaseOptions,
|
||||
...options,
|
||||
});
|
||||
|
||||
const del = (endpoint, options = {}) =>
|
||||
$fetch(endpoint, {
|
||||
method: 'DELETE',
|
||||
...commonBaseOptions,
|
||||
...options,
|
||||
});
|
||||
|
||||
return { get, post, put, del };
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
export const useBackend = () => {
|
||||
const api = useApi();
|
||||
|
||||
const getMeta = (options = {}) => api.get('/meta', options);
|
||||
const postContact = (contact: ContactRequest, options = {}) =>
|
||||
api.post('/contact', contact, options);
|
||||
return { getMeta, postContact };
|
||||
};
|
||||
|
||||
33
frontend/app/composables/useDataJson.ts
Normal file
33
frontend/app/composables/useDataJson.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
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 getJsonData = async (collectionPrefix: string = 'content_data_') => {
|
||||
const { data } = await useAsyncData(key.value, async () => {
|
||||
const collection = (collectionPrefix + locale.value) as keyof Collections;
|
||||
const allData = await queryCollection(collection).all();
|
||||
const filtered = allData.filter((source) => source.meta.path == slug.value)[0];
|
||||
return filtered?.meta;
|
||||
}, {
|
||||
watch: [locale], // Automatically refresh when locale changes
|
||||
});
|
||||
return data;
|
||||
};
|
||||
|
||||
const getCachedData = () => {
|
||||
const { data } = useNuxtData(key.value);
|
||||
return data;
|
||||
};
|
||||
|
||||
return { getJsonData, 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>
|
||||
<div>
|
||||
<div class="min-h-screen mx-auto px-4 py-8 max-w-6xl">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
35
frontend/app/pages/[...slug].vue
Normal file
35
frontend/app/pages/[...slug].vue
Normal file
@@ -0,0 +1,35 @@
|
||||
<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">
|
||||
import { withLeadingSlash } from 'ufo';
|
||||
import type { Collections } from '@nuxt/content';
|
||||
|
||||
const route = useRoute();
|
||||
const { locale } = useI18n();
|
||||
const slug = computed(() => withLeadingSlash(String(route.params.slug)));
|
||||
|
||||
const { data: page } = await useAsyncData(
|
||||
'page-' + slug.value,
|
||||
async () => {
|
||||
const collection = ('content_' + locale.value) as keyof Collections;
|
||||
const content = await queryCollection(collection).path(slug.value).first();
|
||||
if (!content && locale.value !== 'en') {
|
||||
return await queryCollection('content_en').path(slug.value).first();
|
||||
}
|
||||
return content;
|
||||
},
|
||||
{
|
||||
watch: [locale], // Refresh when locale changes
|
||||
},
|
||||
);
|
||||
|
||||
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,59 @@
|
||||
<template>
|
||||
<div>
|
||||
{{ $t('pages.resume.name') }}
|
||||
</div>
|
||||
<NuxtLayout name="default">
|
||||
<h1 class="text-4xl text-highlighted font-bold mb-8">
|
||||
{{ $t('pages.resume.name') }}
|
||||
</h1>
|
||||
<UPageCard v-if="resumeContent" 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>
|
||||
<div class="flex flex-row gap-2 flex-wrap">
|
||||
<UBadge
|
||||
v-for="tool in item.tools"
|
||||
:key="tool"
|
||||
size="md"
|
||||
variant="solid"
|
||||
>
|
||||
{{ tool }}
|
||||
</UBadge>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</UTimeline>
|
||||
</UPageCard>
|
||||
<UPageCard v-if="resumeContent" class="bg-background-100 my-10">
|
||||
<p>
|
||||
{{ $t('pages.resume.education') }}
|
||||
</p>
|
||||
<UTimeline
|
||||
v-model="valueEd"
|
||||
reverse
|
||||
:items="resumeContent?.education"
|
||||
class="w-full"
|
||||
/>
|
||||
</UPageCard>
|
||||
</NuxtLayout>
|
||||
</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/contact.ts
Normal file
11
frontend/app/types/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/meta.ts
Normal file
4
frontend/app/types/meta.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export interface MetaResponse {
|
||||
version: string,
|
||||
name: string
|
||||
}
|
||||
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[]
|
||||
}
|
||||
Reference in New Issue
Block a user