feat(shell): site header, footer, router with view stubs

SiteHeader renders a sticky bilingual nav with click-to-open dropdowns
for Concept / What's On / Collaborate / Reservations groups plus a flat
Marketplace link and an FR/EN toggle. Outside-click and Esc close the
open group; route changes reset both desktop and mobile state.

SiteFooter pins contact, address and social links to every page.

Router declares all eleven content routes plus a 404. Each view is a
"coming soon" stub so per-section content can land independently
without breaking the build.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Padreug 2026-06-08 17:20:20 +02:00
commit 2b675aed85
18 changed files with 516 additions and 41 deletions

View file

@ -1,7 +1,15 @@
<script setup lang="ts"> <script setup lang="ts">
import { RouterView } from 'vue-router' import { RouterView } from 'vue-router'
import SiteHeader from '@/components/SiteHeader.vue'
import SiteFooter from '@/components/SiteFooter.vue'
</script> </script>
<template> <template>
<div class="flex min-h-screen flex-col">
<SiteHeader />
<main class="flex-1">
<RouterView /> <RouterView />
</main>
<SiteFooter />
</div>
</template> </template>

View file

@ -0,0 +1,66 @@
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
const year = new Date().getFullYear()
</script>
<template>
<footer class="mt-16 border-t border-border bg-secondary/40">
<div class="mx-auto grid max-w-7xl gap-8 px-4 py-10 lg:px-6 md:grid-cols-3">
<div>
<div class="mb-3 flex items-center gap-3">
<img src="/cosmic-stag.png" alt="" class="h-10 w-10" />
<span class="font-serif text-lg">Château du Faune</span>
</div>
<p class="text-sm text-muted-foreground">{{ t('footer.tagline') }}</p>
</div>
<div>
<h3 class="mb-3 text-sm font-semibold uppercase tracking-wider">
{{ t('footer.contact') }}
</h3>
<address class="not-italic text-sm space-y-1 text-muted-foreground">
<div>456 Grand Rue de Bellissen</div>
<div>Château de Bénac, 09000 France</div>
<div>
<a href="mailto:chateaudufaune@ariege.io" class="hover:text-primary">
chateaudufaune@ariege.io
</a>
</div>
</address>
</div>
<div>
<h3 class="mb-3 text-sm font-semibold uppercase tracking-wider">
{{ t('footer.social') }}
</h3>
<ul class="space-y-1 text-sm">
<li>
<a
href="https://www.instagram.com/chateaudufaune"
target="_blank"
rel="noopener"
class="text-muted-foreground hover:text-primary"
>
Instagram @chateaudufaune
</a>
</li>
<li>
<a
href="https://www.linkedin.com/company/chateau-du-faune"
target="_blank"
rel="noopener"
class="text-muted-foreground hover:text-primary"
>
LinkedIn
</a>
</li>
</ul>
</div>
</div>
<div class="border-t border-border py-4 text-center text-xs text-muted-foreground">
© {{ year }} Château du Faune · Ariège · {{ t('footer.rights') }}
</div>
</footer>
</template>

View file

@ -0,0 +1,195 @@
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { RouterLink, useRoute } from 'vue-router'
const { t, locale } = useI18n()
const route = useRoute()
const openGroup = ref<string | null>(null)
const mobileOpen = ref(false)
const headerEl = ref<HTMLElement | null>(null)
interface NavItem {
to: string
label: string
}
interface NavGroup {
id: string
label: string
items: NavItem[]
}
const groups = computed<NavGroup[]>(() => [
{
id: 'concept',
label: t('nav.concept'),
items: [
{ to: '/concept', label: t('nav.artEcology') },
{ to: '/vision-values', label: t('nav.visionValues') },
{ to: '/gallery', label: t('nav.gallery') },
],
},
{
id: 'whatsOn',
label: t('nav.whatsOn'),
items: [
{ to: '/events', label: t('nav.events') },
{ to: '/symposium', label: t('nav.symposium') },
],
},
{
id: 'collaborate',
label: t('nav.collaborate'),
items: [
{ to: '/long-stays', label: t('nav.longStays') },
{ to: '/opportunities', label: t('nav.opportunities') },
],
},
{
id: 'reservations',
label: t('nav.reservations'),
items: [
{ to: '/reservations', label: t('nav.planYourVisit') },
{ to: '/accommodation', label: t('nav.accommodation') },
],
},
])
function toggle(id: string) {
openGroup.value = openGroup.value === id ? null : id
}
function closeAll() {
openGroup.value = null
mobileOpen.value = false
}
function toggleLocale() {
locale.value = locale.value === 'fr' ? 'en' : 'fr'
}
function onClickOutside(e: MouseEvent) {
if (!headerEl.value) return
if (!headerEl.value.contains(e.target as Node)) openGroup.value = null
}
function onKeydown(e: KeyboardEvent) {
if (e.key === 'Escape') closeAll()
}
watch(() => route.path, closeAll)
onMounted(() => {
document.addEventListener('click', onClickOutside)
document.addEventListener('keydown', onKeydown)
})
onUnmounted(() => {
document.removeEventListener('click', onClickOutside)
document.removeEventListener('keydown', onKeydown)
})
</script>
<template>
<header
ref="headerEl"
class="sticky top-0 z-40 border-b border-border bg-background/95 backdrop-blur"
>
<div class="mx-auto max-w-7xl px-4 lg:px-6">
<div class="flex h-16 items-center justify-between gap-4">
<RouterLink to="/" class="flex items-center gap-3" @click="closeAll">
<img src="/cosmic-stag.png" alt="" class="h-9 w-9 shrink-0" />
<span class="leading-tight">
<span class="block font-serif text-base tracking-tight text-foreground">
Château du Faune
</span>
<span class="block text-[11px] uppercase tracking-wider text-muted-foreground">
{{ t('common.tagline') }}
</span>
</span>
</RouterLink>
<nav class="hidden items-center gap-1 lg:flex">
<div v-for="g in groups" :key="g.id" class="relative">
<button
type="button"
class="rounded-md px-3 py-2 text-sm hover:bg-muted hover:text-primary"
:aria-expanded="openGroup === g.id"
@click.stop="toggle(g.id)"
>
{{ g.label }}
<span class="ml-1 inline-block text-xs"></span>
</button>
<ul
v-show="openGroup === g.id"
class="absolute right-0 top-full z-50 mt-1 min-w-52 rounded-md border border-border bg-popover py-1 shadow-md"
>
<li v-for="i in g.items" :key="i.to">
<RouterLink
:to="i.to"
class="block px-4 py-2 text-sm hover:bg-muted"
@click="closeAll"
>
{{ i.label }}
</RouterLink>
</li>
</ul>
</div>
<RouterLink
to="/marketplace"
class="rounded-md px-3 py-2 text-sm hover:bg-muted hover:text-primary"
>
{{ t('nav.marketplace') }}
</RouterLink>
<button
type="button"
class="ml-2 rounded-md border border-border px-2 py-1 text-xs uppercase tracking-wider text-muted-foreground hover:bg-muted"
@click="toggleLocale"
>
{{ locale === 'fr' ? 'EN' : 'FR' }}
</button>
</nav>
<button
type="button"
class="rounded-md p-2 lg:hidden"
:aria-label="t('nav.menu')"
:aria-expanded="mobileOpen"
@click="mobileOpen = !mobileOpen"
>
<span class="mb-1.5 block h-0.5 w-6 bg-foreground"></span>
<span class="mb-1.5 block h-0.5 w-6 bg-foreground"></span>
<span class="block h-0.5 w-6 bg-foreground"></span>
</button>
</div>
<div v-show="mobileOpen" class="border-t border-border py-3 lg:hidden">
<div v-for="g in groups" :key="g.id" class="py-2">
<div
class="px-2 pb-1 text-xs font-semibold uppercase tracking-wider text-muted-foreground"
>
{{ g.label }}
</div>
<RouterLink
v-for="i in g.items"
:key="i.to"
:to="i.to"
class="block rounded-md px-4 py-2 hover:bg-muted"
>
{{ i.label }}
</RouterLink>
</div>
<RouterLink to="/marketplace" class="block rounded-md px-4 py-2 hover:bg-muted">
{{ t('nav.marketplace') }}
</RouterLink>
<button
type="button"
class="mt-2 block w-full rounded-md border border-border px-4 py-2 text-left text-sm text-muted-foreground hover:bg-muted"
@click="toggleLocale"
>
{{ locale === 'fr' ? 'Switch to English' : 'Passer en français' }}
</button>
</div>
</div>
</header>
</template>

View file

@ -1,11 +1,37 @@
{ {
"app": { "app": {
"title": "Boilerplate Website" "title": "Château du Faune"
}, },
"home": { "common": {
"heading": "Welcome", "tagline": "Center for Art & Ecology",
"intro": "Edit src/views/HomeView.vue to begin.", "siteName": "Château du Faune",
"counter": "Count: {n}", "comingSoon": "Coming soon.",
"increment": "Increment" "learnMore": "Learn more",
"seeAll": "See all",
"contactUs": "Contact us"
},
"nav": {
"concept": "Concept",
"artEcology": "Art & Ecology",
"visionValues": "Vision & Values",
"gallery": "Gallery",
"whatsOn": "What's On",
"events": "Events & Programs",
"symposium": "Symposium II.0",
"collaborate": "Collaborate",
"longStays": "Long Stays",
"opportunities": "Opportunities",
"reservations": "Reservations",
"planYourVisit": "Plan Your Visit",
"accommodation": "Accommodation",
"marketplace": "Marketplace",
"menu": "Menu"
},
"footer": {
"tagline": "A farm, a residency and a refuge at the meeting point of art and ecology, in the Pyrenean foothills of Ariège.",
"contact": "Contact",
"social": "Follow us",
"address": "456 Grand Rue de Bellissen, Château de Bénac, 09000 France",
"rights": "All rights reserved."
} }
} }

View file

@ -1,11 +1,37 @@
{ {
"app": { "app": {
"title": "Modèle de site" "title": "Château du Faune"
}, },
"home": { "common": {
"heading": "Bienvenue", "tagline": "Centre pour l'art et l'écologie",
"intro": "Modifiez src/views/HomeView.vue pour commencer.", "siteName": "Château du Faune",
"counter": "Compteur : {n}", "comingSoon": "Bientôt disponible.",
"increment": "Incrémenter" "learnMore": "En savoir plus",
"seeAll": "Voir tout",
"contactUs": "Nous contacter"
},
"nav": {
"concept": "Concept",
"artEcology": "Art & Écologie",
"visionValues": "Vision & Valeurs",
"gallery": "Galerie",
"whatsOn": "À l'affiche",
"events": "Événements & Programmes",
"symposium": "Symposium II.0",
"collaborate": "Collaborer",
"longStays": "Séjours longue durée",
"opportunities": "Opportunités",
"reservations": "Réservations",
"planYourVisit": "Préparer votre visite",
"accommodation": "Hébergement",
"marketplace": "Boutique",
"menu": "Menu"
},
"footer": {
"tagline": "Une fermette, une résidence d'artistes et un refuge à la croisée de l'art et de l'écologie, au pied des Pyrénées ariégeoises.",
"contact": "Contact",
"social": "Suivez-nous",
"address": "456 Grand Rue de Bellissen, Château de Bénac, 09000 France",
"rights": "Tous droits réservés."
} }
} }

View file

@ -1,16 +1,54 @@
import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router' import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router'
const routes: RouteRecordRaw[] = [ const routes: RouteRecordRaw[] = [
{ path: '/', name: 'home', component: () => import('@/views/HomeView.vue') },
{ path: '/concept', name: 'concept', component: () => import('@/views/ConceptView.vue') },
{ {
path: '/', path: '/vision-values',
name: 'home', name: 'vision-values',
component: () => import('@/views/HomeView.vue'), component: () => import('@/views/VisionValuesView.vue'),
},
{ path: '/gallery', name: 'gallery', component: () => import('@/views/GalleryView.vue') },
{ path: '/events', name: 'events', component: () => import('@/views/EventsView.vue') },
{ path: '/symposium', name: 'symposium', component: () => import('@/views/SymposiumView.vue') },
{
path: '/long-stays',
name: 'long-stays',
component: () => import('@/views/LongStaysView.vue'),
},
{
path: '/opportunities',
name: 'opportunities',
component: () => import('@/views/OpportunitiesView.vue'),
},
{
path: '/reservations',
name: 'reservations',
component: () => import('@/views/ReservationsView.vue'),
},
{
path: '/accommodation',
name: 'accommodation',
component: () => import('@/views/AccommodationView.vue'),
},
{
path: '/marketplace',
name: 'marketplace',
component: () => import('@/views/MarketplaceView.vue'),
},
{
path: '/:pathMatch(.*)*',
name: 'not-found',
component: () => import('@/views/NotFoundView.vue'),
}, },
] ]
const router = createRouter({ const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL), history: createWebHistory(import.meta.env.BASE_URL),
routes, routes,
scrollBehavior() {
return { top: 0 }
},
}) })
export default router export default router

View file

@ -0,0 +1,12 @@
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
</script>
<template>
<article class="mx-auto max-w-4xl px-4 py-16">
<h1 class="font-serif text-4xl font-semibold tracking-tight">{{ t('nav.accommodation') }}</h1>
<p class="mt-4 text-muted-foreground">{{ t('common.comingSoon') }}</p>
</article>
</template>

12
src/views/ConceptView.vue Normal file
View file

@ -0,0 +1,12 @@
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
</script>
<template>
<article class="mx-auto max-w-4xl px-4 py-16">
<h1 class="font-serif text-4xl font-semibold tracking-tight">{{ t('nav.artEcology') }}</h1>
<p class="mt-4 text-muted-foreground">{{ t('common.comingSoon') }}</p>
</article>
</template>

12
src/views/EventsView.vue Normal file
View file

@ -0,0 +1,12 @@
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
</script>
<template>
<article class="mx-auto max-w-4xl px-4 py-16">
<h1 class="font-serif text-4xl font-semibold tracking-tight">{{ t('nav.events') }}</h1>
<p class="mt-4 text-muted-foreground">{{ t('common.comingSoon') }}</p>
</article>
</template>

12
src/views/GalleryView.vue Normal file
View file

@ -0,0 +1,12 @@
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
</script>
<template>
<article class="mx-auto max-w-4xl px-4 py-16">
<h1 class="font-serif text-4xl font-semibold tracking-tight">{{ t('nav.gallery') }}</h1>
<p class="mt-4 text-muted-foreground">{{ t('common.comingSoon') }}</p>
</article>
</template>

View file

@ -1,32 +1,15 @@
<script setup lang="ts"> <script setup lang="ts">
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useCounterStore } from '@/stores/counter'
const { t, locale } = useI18n() const { t } = useI18n()
const counter = useCounterStore()
function toggleLocale() {
locale.value = locale.value === 'en' ? 'fr' : 'en'
}
</script> </script>
<template> <template>
<main class="mx-auto max-w-2xl space-y-6 p-8"> <article class="mx-auto max-w-4xl px-4 py-16">
<h1 class="text-3xl font-bold">{{ t('home.heading') }}</h1> <h1 class="font-serif text-4xl font-semibold tracking-tight md:text-5xl">
<p class="text-muted-foreground">{{ t('home.intro') }}</p> {{ t('common.siteName') }}
</h1>
<div class="space-y-2"> <p class="mt-3 text-lg text-muted-foreground">{{ t('common.tagline') }}</p>
<p>{{ t('home.counter', { n: counter.count }) }}</p> <p class="mt-8 text-base">{{ t('common.comingSoon') }}</p>
<button </article>
class="rounded-md bg-primary px-4 py-2 text-primary-foreground hover:opacity-90"
@click="counter.increment"
>
{{ t('home.increment') }}
</button>
</div>
<button class="text-sm underline" @click="toggleLocale">
Switch to {{ locale === 'en' ? 'Français' : 'English' }}
</button>
</main>
</template> </template>

View file

@ -0,0 +1,12 @@
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
</script>
<template>
<article class="mx-auto max-w-4xl px-4 py-16">
<h1 class="font-serif text-4xl font-semibold tracking-tight">{{ t('nav.longStays') }}</h1>
<p class="mt-4 text-muted-foreground">{{ t('common.comingSoon') }}</p>
</article>
</template>

View file

@ -0,0 +1,12 @@
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
</script>
<template>
<article class="mx-auto max-w-4xl px-4 py-16">
<h1 class="font-serif text-4xl font-semibold tracking-tight">{{ t('nav.marketplace') }}</h1>
<p class="mt-4 text-muted-foreground">{{ t('common.comingSoon') }}</p>
</article>
</template>

View file

@ -0,0 +1,13 @@
<script setup lang="ts">
import { RouterLink } from 'vue-router'
</script>
<template>
<article class="mx-auto max-w-3xl px-4 py-24 text-center">
<h1 class="font-serif text-5xl font-semibold tracking-tight">404</h1>
<p class="mt-4 text-muted-foreground">Page introuvable / Page not found.</p>
<RouterLink to="/" class="mt-8 inline-block text-sm underline hover:text-primary">
Château du Faune
</RouterLink>
</article>
</template>

View file

@ -0,0 +1,12 @@
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
</script>
<template>
<article class="mx-auto max-w-4xl px-4 py-16">
<h1 class="font-serif text-4xl font-semibold tracking-tight">{{ t('nav.opportunities') }}</h1>
<p class="mt-4 text-muted-foreground">{{ t('common.comingSoon') }}</p>
</article>
</template>

View file

@ -0,0 +1,12 @@
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
</script>
<template>
<article class="mx-auto max-w-4xl px-4 py-16">
<h1 class="font-serif text-4xl font-semibold tracking-tight">{{ t('nav.planYourVisit') }}</h1>
<p class="mt-4 text-muted-foreground">{{ t('common.comingSoon') }}</p>
</article>
</template>

View file

@ -0,0 +1,12 @@
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
</script>
<template>
<article class="mx-auto max-w-4xl px-4 py-16">
<h1 class="font-serif text-4xl font-semibold tracking-tight">{{ t('nav.symposium') }}</h1>
<p class="mt-4 text-muted-foreground">{{ t('common.comingSoon') }}</p>
</article>
</template>

View file

@ -0,0 +1,12 @@
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
</script>
<template>
<article class="mx-auto max-w-4xl px-4 py-16">
<h1 class="font-serif text-4xl font-semibold tracking-tight">{{ t('nav.visionValues') }}</h1>
<p class="mt-4 text-muted-foreground">{{ t('common.comingSoon') }}</p>
</article>
</template>