✨ Add layouts, routing, transitions & pages (Story 1.4)
Default layout with sticky AppHeader (nav, LanguageSwitcher, mobile hamburger), AppFooter with social links. Minimal layout for express mode. 7 placeholder pages with localized EN routes. Page transitions (fade+slide), prefers-reduced-motion support, custom scroll behavior, error.vue, useSeo composable, SVG favicon. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
60
frontend/app/assets/css/transitions.css
Normal file
60
frontend/app/assets/css/transitions.css
Normal file
@@ -0,0 +1,60 @@
|
||||
/* Transition de page - effet "changement de zone" */
|
||||
.page-enter-active,
|
||||
.page-leave-active {
|
||||
transition: opacity 0.3s ease, transform 0.3s ease;
|
||||
}
|
||||
|
||||
.page-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
|
||||
.page-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
|
||||
/* Transition de layout */
|
||||
.layout-enter-active,
|
||||
.layout-leave-active {
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.layout-enter-from,
|
||||
.layout-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* Mobile menu slide */
|
||||
.slide-down-enter-active,
|
||||
.slide-down-leave-active {
|
||||
transition: opacity 0.2s ease, transform 0.2s ease;
|
||||
}
|
||||
|
||||
.slide-down-enter-from,
|
||||
.slide-down-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(-8px);
|
||||
}
|
||||
|
||||
/* Respect de prefers-reduced-motion */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.page-enter-active,
|
||||
.page-leave-active,
|
||||
.layout-enter-active,
|
||||
.layout-leave-active,
|
||||
.slide-down-enter-active,
|
||||
.slide-down-leave-active {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.page-enter-from,
|
||||
.page-leave-to,
|
||||
.layout-enter-from,
|
||||
.layout-leave-to,
|
||||
.slide-down-enter-from,
|
||||
.slide-down-leave-to {
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
44
frontend/app/components/layout/AppFooter.vue
Normal file
44
frontend/app/components/layout/AppFooter.vue
Normal file
@@ -0,0 +1,44 @@
|
||||
<template>
|
||||
<footer class="border-t border-sky-text/10 py-8 px-4">
|
||||
<div class="max-w-7xl mx-auto flex flex-col md:flex-row items-center justify-between gap-4">
|
||||
<p class="text-sm text-sky-text/50 font-ui">
|
||||
{{ $t('footer.copyright', { year: new Date().getFullYear() }) }}
|
||||
</p>
|
||||
|
||||
<div class="flex items-center gap-4">
|
||||
<a
|
||||
v-if="config.public.githubUrl"
|
||||
:href="config.public.githubUrl as string"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-sky-text/40 hover:text-sky-accent transition-colors"
|
||||
aria-label="GitHub"
|
||||
>
|
||||
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
|
||||
</svg>
|
||||
</a>
|
||||
<a
|
||||
v-if="config.public.linkedinUrl"
|
||||
:href="config.public.linkedinUrl as string"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-sky-text/40 hover:text-sky-accent transition-colors"
|
||||
aria-label="LinkedIn"
|
||||
>
|
||||
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<p class="text-xs text-sky-text/30 font-ui">
|
||||
{{ $t('footer.built_with') }}
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const config = useRuntimeConfig()
|
||||
</script>
|
||||
105
frontend/app/components/layout/AppHeader.vue
Normal file
105
frontend/app/components/layout/AppHeader.vue
Normal file
@@ -0,0 +1,105 @@
|
||||
<template>
|
||||
<header
|
||||
class="sticky top-0 z-50 transition-colors duration-300"
|
||||
:class="scrolled ? 'bg-sky-dark/90 backdrop-blur-sm shadow-lg' : 'bg-transparent'"
|
||||
>
|
||||
<div class="max-w-7xl mx-auto px-4 py-3 flex items-center justify-between">
|
||||
<NuxtLink :to="localePath('/')" class="text-xl font-narrative text-sky-accent font-bold">
|
||||
Skycel
|
||||
</NuxtLink>
|
||||
|
||||
<!-- Desktop nav -->
|
||||
<nav class="hidden md:flex items-center gap-6" aria-label="Main navigation">
|
||||
<NuxtLink
|
||||
v-for="item in navItems"
|
||||
:key="item.path"
|
||||
:to="localePath(item.path)"
|
||||
class="text-sm font-ui text-sky-text/70 hover:text-sky-accent transition-colors"
|
||||
active-class="!text-sky-accent"
|
||||
>
|
||||
{{ $t(item.label) }}
|
||||
</NuxtLink>
|
||||
</nav>
|
||||
|
||||
<div class="flex items-center gap-3">
|
||||
<!-- Placeholder barre progression (Epic 3) -->
|
||||
<div class="hidden md:block w-24 h-1.5 bg-sky-text/10 rounded-full overflow-hidden">
|
||||
<div class="h-full bg-sky-accent/40 rounded-full" style="width: 0%"></div>
|
||||
</div>
|
||||
|
||||
<UiLanguageSwitcher />
|
||||
|
||||
<!-- Mobile hamburger -->
|
||||
<button
|
||||
class="md:hidden text-sky-text p-1"
|
||||
aria-label="Menu"
|
||||
@click="mobileOpen = !mobileOpen"
|
||||
>
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
v-if="!mobileOpen"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M4 6h16M4 12h16M4 18h16"
|
||||
/>
|
||||
<path
|
||||
v-else
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mobile menu -->
|
||||
<Transition name="slide-down">
|
||||
<nav
|
||||
v-if="mobileOpen"
|
||||
class="md:hidden bg-sky-dark/95 backdrop-blur-sm border-t border-sky-text/10 px-4 py-4"
|
||||
aria-label="Mobile navigation"
|
||||
>
|
||||
<NuxtLink
|
||||
v-for="item in navItems"
|
||||
:key="item.path"
|
||||
:to="localePath(item.path)"
|
||||
class="block py-2 text-sm font-ui text-sky-text/70 hover:text-sky-accent transition-colors"
|
||||
active-class="!text-sky-accent"
|
||||
@click="mobileOpen = false"
|
||||
>
|
||||
{{ $t(item.label) }}
|
||||
</NuxtLink>
|
||||
</nav>
|
||||
</Transition>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const localePath = useLocalePath()
|
||||
|
||||
const mobileOpen = ref(false)
|
||||
const scrolled = ref(false)
|
||||
|
||||
const navItems = [
|
||||
{ path: '/projets', label: 'nav.projects' },
|
||||
{ path: '/competences', label: 'nav.skills' },
|
||||
{ path: '/parcours', label: 'nav.journey' },
|
||||
{ path: '/temoignages', label: 'nav.testimonials' },
|
||||
{ path: '/contact', label: 'nav.contact' },
|
||||
]
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('scroll', handleScroll, { passive: true })
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('scroll', handleScroll)
|
||||
})
|
||||
|
||||
function handleScroll() {
|
||||
scrolled.value = window.scrollY > 20
|
||||
}
|
||||
</script>
|
||||
38
frontend/app/composables/useSeo.ts
Normal file
38
frontend/app/composables/useSeo.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
interface SeoOptions {
|
||||
title: string
|
||||
description?: string
|
||||
image?: string
|
||||
url?: string
|
||||
}
|
||||
|
||||
export const useSeo = () => {
|
||||
const config = useRuntimeConfig()
|
||||
const route = useRoute()
|
||||
const { locale } = useI18n()
|
||||
|
||||
const setPageMeta = (options: SeoOptions) => {
|
||||
const siteUrl = config.public.siteUrl as string || 'https://skycel.fr'
|
||||
const fullUrl = options.url || `${siteUrl}${route.fullPath}`
|
||||
const imageUrl = options.image || `${siteUrl}/og-image.jpg`
|
||||
|
||||
useHead({
|
||||
title: options.title,
|
||||
})
|
||||
|
||||
useSeoMeta({
|
||||
title: options.title,
|
||||
description: options.description,
|
||||
ogTitle: options.title,
|
||||
ogDescription: options.description,
|
||||
ogImage: imageUrl,
|
||||
ogUrl: fullUrl,
|
||||
ogLocale: locale.value === 'fr' ? 'fr_FR' : 'en_US',
|
||||
twitterCard: 'summary_large_image',
|
||||
twitterTitle: options.title,
|
||||
twitterDescription: options.description,
|
||||
twitterImage: imageUrl,
|
||||
})
|
||||
}
|
||||
|
||||
return { setPageMeta }
|
||||
}
|
||||
40
frontend/app/error.vue
Normal file
40
frontend/app/error.vue
Normal file
@@ -0,0 +1,40 @@
|
||||
<template>
|
||||
<div class="min-h-screen bg-sky-dark text-sky-text flex flex-col items-center justify-center p-8">
|
||||
<h1 class="text-6xl font-bold text-sky-accent mb-4">
|
||||
{{ error?.statusCode || 500 }}
|
||||
</h1>
|
||||
|
||||
<p class="text-xl font-narrative mb-8 text-center max-w-md">
|
||||
{{ errorMessage }}
|
||||
</p>
|
||||
|
||||
<button
|
||||
class="px-6 py-3 bg-sky-accent text-sky-dark font-ui font-semibold rounded-lg hover:opacity-90 transition-opacity"
|
||||
@click="handleError"
|
||||
>
|
||||
{{ $t('common.back_home') }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{
|
||||
error: {
|
||||
statusCode: number
|
||||
message: string
|
||||
}
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const errorMessage = computed(() => {
|
||||
if (props.error?.statusCode === 404) {
|
||||
return t('error.404')
|
||||
}
|
||||
return t('error.generic')
|
||||
})
|
||||
|
||||
function handleError() {
|
||||
clearError({ redirect: '/' })
|
||||
}
|
||||
</script>
|
||||
11
frontend/app/layouts/default.vue
Normal file
11
frontend/app/layouts/default.vue
Normal file
@@ -0,0 +1,11 @@
|
||||
<template>
|
||||
<div class="min-h-screen bg-sky-dark text-sky-text flex flex-col">
|
||||
<LayoutAppHeader />
|
||||
|
||||
<main class="flex-1">
|
||||
<slot />
|
||||
</main>
|
||||
|
||||
<LayoutAppFooter />
|
||||
</div>
|
||||
</template>
|
||||
27
frontend/app/layouts/minimal.vue
Normal file
27
frontend/app/layouts/minimal.vue
Normal file
@@ -0,0 +1,27 @@
|
||||
<template>
|
||||
<div class="min-h-screen bg-sky-dark text-sky-text flex flex-col">
|
||||
<header class="p-4 flex justify-between items-center">
|
||||
<NuxtLink :to="localePath('/')" class="text-sky-accent font-narrative font-bold">
|
||||
Skycel
|
||||
</NuxtLink>
|
||||
<div class="flex items-center gap-4">
|
||||
<NuxtLink :to="localePath('/')" class="text-sky-text/70 hover:text-sky-accent text-sm font-ui transition-colors">
|
||||
{{ $t('common.back_to_adventure') }}
|
||||
</NuxtLink>
|
||||
<UiLanguageSwitcher />
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="flex-1">
|
||||
<slot />
|
||||
</main>
|
||||
|
||||
<footer class="p-4 text-center text-sky-text/50 text-sm font-ui">
|
||||
{{ $t('footer.copyright', { year: new Date().getFullYear() }) }}
|
||||
</footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const localePath = useLocalePath()
|
||||
</script>
|
||||
16
frontend/app/pages/competences.vue
Normal file
16
frontend/app/pages/competences.vue
Normal file
@@ -0,0 +1,16 @@
|
||||
<template>
|
||||
<div class="min-h-screen p-8">
|
||||
<h1 class="text-3xl font-narrative text-sky-text">{{ $t('pages.skills.title') }}</h1>
|
||||
<p class="mt-4 text-sky-text/70">{{ $t('pages.skills.description') }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const { setPageMeta } = useSeo()
|
||||
const { t } = useI18n()
|
||||
|
||||
setPageMeta({
|
||||
title: t('pages.skills.title'),
|
||||
description: t('pages.skills.description'),
|
||||
})
|
||||
</script>
|
||||
16
frontend/app/pages/contact.vue
Normal file
16
frontend/app/pages/contact.vue
Normal file
@@ -0,0 +1,16 @@
|
||||
<template>
|
||||
<div class="min-h-screen p-8">
|
||||
<h1 class="text-3xl font-narrative text-sky-text">{{ $t('pages.contact.title') }}</h1>
|
||||
<p class="mt-4 text-sky-text/70">{{ $t('pages.contact.description') }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const { setPageMeta } = useSeo()
|
||||
const { t } = useI18n()
|
||||
|
||||
setPageMeta({
|
||||
title: t('pages.contact.title'),
|
||||
description: t('pages.contact.description'),
|
||||
})
|
||||
</script>
|
||||
@@ -1,14 +1,31 @@
|
||||
<template>
|
||||
<div class="min-h-screen bg-sky-dark flex flex-col items-center justify-center gap-6">
|
||||
<h1 class="text-4xl font-narrative text-sky-text">{{ $t('landing.title') }}</h1>
|
||||
<div class="min-h-screen flex flex-col items-center justify-center gap-6 px-4">
|
||||
<h1 class="text-4xl md:text-5xl font-narrative text-sky-text text-center">{{ $t('landing.title') }}</h1>
|
||||
<p class="text-xl font-ui text-sky-text/70">{{ $t('landing.subtitle') }}</p>
|
||||
<div class="flex gap-4 mt-4">
|
||||
<button class="px-6 py-3 bg-sky-accent text-sky-dark font-ui font-semibold rounded-lg">
|
||||
<NuxtLink
|
||||
:to="localePath('/projets')"
|
||||
class="px-6 py-3 bg-sky-accent text-sky-dark font-ui font-semibold rounded-lg hover:opacity-90 transition-opacity"
|
||||
>
|
||||
{{ $t('landing.cta_adventure') }}
|
||||
</button>
|
||||
<button class="px-6 py-3 border border-sky-text/30 text-sky-text font-ui rounded-lg">
|
||||
</NuxtLink>
|
||||
<NuxtLink
|
||||
:to="localePath('/resume')"
|
||||
class="px-6 py-3 border border-sky-text/30 text-sky-text font-ui rounded-lg hover:border-sky-accent hover:text-sky-accent transition-colors"
|
||||
>
|
||||
{{ $t('landing.cta_express') }}
|
||||
</button>
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const { setPageMeta } = useSeo()
|
||||
const { t } = useI18n()
|
||||
const localePath = useLocalePath()
|
||||
|
||||
setPageMeta({
|
||||
title: t('meta.title'),
|
||||
description: t('meta.description'),
|
||||
})
|
||||
</script>
|
||||
|
||||
16
frontend/app/pages/parcours.vue
Normal file
16
frontend/app/pages/parcours.vue
Normal file
@@ -0,0 +1,16 @@
|
||||
<template>
|
||||
<div class="min-h-screen p-8">
|
||||
<h1 class="text-3xl font-narrative text-sky-text">{{ $t('pages.journey.title') }}</h1>
|
||||
<p class="mt-4 text-sky-text/70">{{ $t('pages.journey.description') }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const { setPageMeta } = useSeo()
|
||||
const { t } = useI18n()
|
||||
|
||||
setPageMeta({
|
||||
title: t('pages.journey.title'),
|
||||
description: t('pages.journey.description'),
|
||||
})
|
||||
</script>
|
||||
19
frontend/app/pages/projets/[slug].vue
Normal file
19
frontend/app/pages/projets/[slug].vue
Normal file
@@ -0,0 +1,19 @@
|
||||
<template>
|
||||
<div class="min-h-screen p-8">
|
||||
<h1 class="text-3xl font-narrative text-sky-text">{{ slug }}</h1>
|
||||
<p class="mt-4 text-sky-text/70">{{ $t('pages.projects.description') }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const route = useRoute()
|
||||
const slug = computed(() => route.params.slug as string)
|
||||
|
||||
const { setPageMeta } = useSeo()
|
||||
const { t } = useI18n()
|
||||
|
||||
setPageMeta({
|
||||
title: slug.value,
|
||||
description: t('pages.projects.description'),
|
||||
})
|
||||
</script>
|
||||
16
frontend/app/pages/projets/index.vue
Normal file
16
frontend/app/pages/projets/index.vue
Normal file
@@ -0,0 +1,16 @@
|
||||
<template>
|
||||
<div class="min-h-screen p-8">
|
||||
<h1 class="text-3xl font-narrative text-sky-text">{{ $t('pages.projects.title') }}</h1>
|
||||
<p class="mt-4 text-sky-text/70">{{ $t('pages.projects.description') }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const { setPageMeta } = useSeo()
|
||||
const { t } = useI18n()
|
||||
|
||||
setPageMeta({
|
||||
title: t('pages.projects.title'),
|
||||
description: t('pages.projects.description'),
|
||||
})
|
||||
</script>
|
||||
20
frontend/app/pages/resume.vue
Normal file
20
frontend/app/pages/resume.vue
Normal file
@@ -0,0 +1,20 @@
|
||||
<template>
|
||||
<div class="min-h-screen p-8">
|
||||
<h1 class="text-3xl font-narrative text-sky-text">{{ $t('pages.resume.title') }}</h1>
|
||||
<p class="mt-4 text-sky-text/70">{{ $t('pages.resume.description') }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
definePageMeta({
|
||||
layout: 'minimal',
|
||||
})
|
||||
|
||||
const { setPageMeta } = useSeo()
|
||||
const { t } = useI18n()
|
||||
|
||||
setPageMeta({
|
||||
title: t('pages.resume.title'),
|
||||
description: t('pages.resume.description'),
|
||||
})
|
||||
</script>
|
||||
16
frontend/app/pages/temoignages.vue
Normal file
16
frontend/app/pages/temoignages.vue
Normal file
@@ -0,0 +1,16 @@
|
||||
<template>
|
||||
<div class="min-h-screen p-8">
|
||||
<h1 class="text-3xl font-narrative text-sky-text">{{ $t('pages.testimonials.title') }}</h1>
|
||||
<p class="mt-4 text-sky-text/70">{{ $t('pages.testimonials.description') }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const { setPageMeta } = useSeo()
|
||||
const { t } = useI18n()
|
||||
|
||||
setPageMeta({
|
||||
title: t('pages.testimonials.title'),
|
||||
description: t('pages.testimonials.description'),
|
||||
})
|
||||
</script>
|
||||
27
frontend/app/router.options.ts
Normal file
27
frontend/app/router.options.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import type { RouterConfig } from '@nuxt/schema'
|
||||
|
||||
export default <RouterConfig>{
|
||||
scrollBehavior(to, _from, savedPosition) {
|
||||
if (savedPosition) {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve(savedPosition)
|
||||
}, 350)
|
||||
})
|
||||
}
|
||||
|
||||
if (to.hash) {
|
||||
return {
|
||||
el: to.hash,
|
||||
behavior: 'smooth',
|
||||
top: 80,
|
||||
}
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve({ top: 0, behavior: 'smooth' })
|
||||
}, 350)
|
||||
})
|
||||
},
|
||||
}
|
||||
@@ -14,7 +14,9 @@
|
||||
"discover": "Discover",
|
||||
"close": "Close",
|
||||
"loading": "Loading...",
|
||||
"language": "Language"
|
||||
"language": "Language",
|
||||
"back_home": "Back to home",
|
||||
"back_to_adventure": "Back to the adventure"
|
||||
},
|
||||
"landing": {
|
||||
"title": "Welcome to my universe",
|
||||
@@ -23,8 +25,8 @@
|
||||
"cta_express": "Express mode"
|
||||
},
|
||||
"error": {
|
||||
"404": "Page not found",
|
||||
"generic": "An error occurred"
|
||||
"404": "Oops! This page seems to have gotten lost in the code...",
|
||||
"generic": "An unexpected error occurred. The Bug is investigating..."
|
||||
},
|
||||
"meta": {
|
||||
"title": "Skycel - Célian's Portfolio",
|
||||
@@ -33,5 +35,31 @@
|
||||
"footer": {
|
||||
"copyright": "© {year} Célian — Skycel",
|
||||
"built_with": "Built with Nuxt & Laravel"
|
||||
},
|
||||
"pages": {
|
||||
"projects": {
|
||||
"title": "Projects",
|
||||
"description": "Discover my projects and achievements"
|
||||
},
|
||||
"skills": {
|
||||
"title": "Skills",
|
||||
"description": "My technical and soft skills"
|
||||
},
|
||||
"testimonials": {
|
||||
"title": "Testimonials",
|
||||
"description": "What people say about my work"
|
||||
},
|
||||
"journey": {
|
||||
"title": "Journey",
|
||||
"description": "My professional and personal journey"
|
||||
},
|
||||
"contact": {
|
||||
"title": "Contact",
|
||||
"description": "Get in touch with me"
|
||||
},
|
||||
"resume": {
|
||||
"title": "Quick Resume",
|
||||
"description": "The essentials at a glance"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,9 @@
|
||||
"discover": "Découvrir",
|
||||
"close": "Fermer",
|
||||
"loading": "Chargement...",
|
||||
"language": "Langue"
|
||||
"language": "Langue",
|
||||
"back_home": "Retour à l'accueil",
|
||||
"back_to_adventure": "Retour à l'aventure"
|
||||
},
|
||||
"landing": {
|
||||
"title": "Bienvenue dans mon univers",
|
||||
@@ -23,8 +25,8 @@
|
||||
"cta_express": "Mode express"
|
||||
},
|
||||
"error": {
|
||||
"404": "Page non trouvée",
|
||||
"generic": "Une erreur est survenue"
|
||||
"404": "Oups ! Cette page semble s'être perdue dans les méandres du code...",
|
||||
"generic": "Une erreur inattendue s'est produite. Le Bug enquête..."
|
||||
},
|
||||
"meta": {
|
||||
"title": "Skycel - Portfolio de Célian",
|
||||
@@ -33,5 +35,31 @@
|
||||
"footer": {
|
||||
"copyright": "© {year} Célian — Skycel",
|
||||
"built_with": "Construit avec Nuxt & Laravel"
|
||||
},
|
||||
"pages": {
|
||||
"projects": {
|
||||
"title": "Projets",
|
||||
"description": "Découvrez mes projets et réalisations"
|
||||
},
|
||||
"skills": {
|
||||
"title": "Compétences",
|
||||
"description": "Mes compétences techniques et humaines"
|
||||
},
|
||||
"testimonials": {
|
||||
"title": "Témoignages",
|
||||
"description": "Ce que l'on dit de mon travail"
|
||||
},
|
||||
"journey": {
|
||||
"title": "Parcours",
|
||||
"description": "Mon parcours professionnel et personnel"
|
||||
},
|
||||
"contact": {
|
||||
"title": "Contact",
|
||||
"description": "Prenez contact avec moi"
|
||||
},
|
||||
"resume": {
|
||||
"title": "Résumé Express",
|
||||
"description": "L'essentiel en un coup d'œil"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ export default defineNuxtConfig({
|
||||
'@nuxtjs/sitemap',
|
||||
],
|
||||
|
||||
css: ['~/assets/css/main.css'],
|
||||
css: ['~/assets/css/main.css', '~/assets/css/transitions.css'],
|
||||
|
||||
postcss: {
|
||||
plugins: {
|
||||
@@ -33,6 +33,7 @@ export default defineNuxtConfig({
|
||||
lazy: true,
|
||||
langDir: '../i18n/',
|
||||
detectBrowserLanguage: false,
|
||||
customRoutes: 'config',
|
||||
pages: {
|
||||
'projets/index': { en: '/projects' },
|
||||
'projets/[slug]': { en: '/projects/[slug]' },
|
||||
@@ -46,13 +47,22 @@ export default defineNuxtConfig({
|
||||
},
|
||||
|
||||
app: {
|
||||
head: {
|
||||
link: [
|
||||
{ rel: 'icon', type: 'image/svg+xml', href: '/favicon.svg' },
|
||||
],
|
||||
},
|
||||
pageTransition: { name: 'page', mode: 'out-in' },
|
||||
layoutTransition: { name: 'layout', mode: 'out-in' },
|
||||
},
|
||||
|
||||
runtimeConfig: {
|
||||
public: {
|
||||
apiUrl: process.env.NUXT_PUBLIC_API_URL || 'http://localhost:8000/api',
|
||||
apiKey: process.env.NUXT_PUBLIC_API_KEY || '',
|
||||
siteUrl: process.env.NUXT_PUBLIC_SITE_URL || 'https://skycel.fr',
|
||||
githubUrl: process.env.NUXT_PUBLIC_GITHUB_URL || 'https://git.araneite.dev/skycel',
|
||||
linkedinUrl: process.env.NUXT_PUBLIC_LINKEDIN_URL || '',
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
4
frontend/public/favicon.svg
Normal file
4
frontend/public/favicon.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
||||
<rect width="32" height="32" rx="6" fill="#0a0e1a"/>
|
||||
<text x="16" y="23" font-family="Arial, sans-serif" font-size="20" font-weight="bold" fill="#fa784f" text-anchor="middle">S</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 256 B |
Reference in New Issue
Block a user