init
This commit is contained in:
commit
9d172a4422
154
.astro/content.d.ts
vendored
Normal file
154
.astro/content.d.ts
vendored
Normal file
@ -0,0 +1,154 @@
|
||||
declare module 'astro:content' {
|
||||
export interface RenderResult {
|
||||
Content: import('astro/runtime/server/index.js').AstroComponentFactory;
|
||||
headings: import('astro').MarkdownHeading[];
|
||||
remarkPluginFrontmatter: Record<string, any>;
|
||||
}
|
||||
interface Render {
|
||||
'.md': Promise<RenderResult>;
|
||||
}
|
||||
|
||||
export interface RenderedContent {
|
||||
html: string;
|
||||
metadata?: {
|
||||
imagePaths: Array<string>;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
}
|
||||
|
||||
type Flatten<T> = T extends { [K: string]: infer U } ? U : never;
|
||||
|
||||
export type CollectionKey = keyof DataEntryMap;
|
||||
export type CollectionEntry<C extends CollectionKey> = Flatten<DataEntryMap[C]>;
|
||||
|
||||
type AllValuesOf<T> = T extends any ? T[keyof T] : never;
|
||||
|
||||
export type ReferenceDataEntry<
|
||||
C extends CollectionKey,
|
||||
E extends keyof DataEntryMap[C] = string,
|
||||
> = {
|
||||
collection: C;
|
||||
id: E;
|
||||
};
|
||||
|
||||
export type ReferenceLiveEntry<C extends keyof LiveContentConfig['collections']> = {
|
||||
collection: C;
|
||||
id: string;
|
||||
};
|
||||
|
||||
export function getCollection<C extends keyof DataEntryMap, E extends CollectionEntry<C>>(
|
||||
collection: C,
|
||||
filter?: (entry: CollectionEntry<C>) => entry is E,
|
||||
): Promise<E[]>;
|
||||
export function getCollection<C extends keyof DataEntryMap>(
|
||||
collection: C,
|
||||
filter?: (entry: CollectionEntry<C>) => unknown,
|
||||
): Promise<CollectionEntry<C>[]>;
|
||||
|
||||
export function getLiveCollection<C extends keyof LiveContentConfig['collections']>(
|
||||
collection: C,
|
||||
filter?: LiveLoaderCollectionFilterType<C>,
|
||||
): Promise<
|
||||
import('astro').LiveDataCollectionResult<LiveLoaderDataType<C>, LiveLoaderErrorType<C>>
|
||||
>;
|
||||
|
||||
export function getEntry<
|
||||
C extends keyof DataEntryMap,
|
||||
E extends keyof DataEntryMap[C] | (string & {}),
|
||||
>(
|
||||
entry: ReferenceDataEntry<C, E>,
|
||||
): E extends keyof DataEntryMap[C]
|
||||
? Promise<DataEntryMap[C][E]>
|
||||
: Promise<CollectionEntry<C> | undefined>;
|
||||
export function getEntry<
|
||||
C extends keyof DataEntryMap,
|
||||
E extends keyof DataEntryMap[C] | (string & {}),
|
||||
>(
|
||||
collection: C,
|
||||
id: E,
|
||||
): E extends keyof DataEntryMap[C]
|
||||
? string extends keyof DataEntryMap[C]
|
||||
? Promise<DataEntryMap[C][E]> | undefined
|
||||
: Promise<DataEntryMap[C][E]>
|
||||
: Promise<CollectionEntry<C> | undefined>;
|
||||
export function getLiveEntry<C extends keyof LiveContentConfig['collections']>(
|
||||
collection: C,
|
||||
filter: string | LiveLoaderEntryFilterType<C>,
|
||||
): Promise<import('astro').LiveDataEntryResult<LiveLoaderDataType<C>, LiveLoaderErrorType<C>>>;
|
||||
|
||||
/** Resolve an array of entry references from the same collection */
|
||||
export function getEntries<C extends keyof DataEntryMap>(
|
||||
entries: ReferenceDataEntry<C, keyof DataEntryMap[C]>[],
|
||||
): Promise<CollectionEntry<C>[]>;
|
||||
|
||||
export function render<C extends keyof DataEntryMap>(
|
||||
entry: DataEntryMap[C][string],
|
||||
): Promise<RenderResult>;
|
||||
|
||||
export function reference<
|
||||
C extends
|
||||
| keyof DataEntryMap
|
||||
// Allow generic `string` to avoid excessive type errors in the config
|
||||
// if `dev` is not running to update as you edit.
|
||||
// Invalid collection names will be caught at build time.
|
||||
| (string & {}),
|
||||
>(
|
||||
collection: C,
|
||||
): import('astro/zod').ZodPipe<
|
||||
import('astro/zod').ZodString,
|
||||
import('astro/zod').ZodTransform<
|
||||
C extends keyof DataEntryMap
|
||||
? {
|
||||
collection: C;
|
||||
id: string;
|
||||
}
|
||||
: never,
|
||||
string
|
||||
>
|
||||
>;
|
||||
|
||||
type ReturnTypeOrOriginal<T> = T extends (...args: any[]) => infer R ? R : T;
|
||||
type InferEntrySchema<C extends keyof DataEntryMap> = import('astro/zod').infer<
|
||||
ReturnTypeOrOriginal<Required<ContentConfig['collections'][C]>['schema']>
|
||||
>;
|
||||
type ExtractLoaderConfig<T> = T extends { loader: infer L } ? L : never;
|
||||
type InferLoaderSchema<
|
||||
C extends keyof DataEntryMap,
|
||||
L = ExtractLoaderConfig<ContentConfig['collections'][C]>,
|
||||
> = L extends { schema: import('astro/zod').ZodSchema }
|
||||
? import('astro/zod').infer<L['schema']>
|
||||
: any;
|
||||
|
||||
type DataEntryMap = {
|
||||
|
||||
};
|
||||
|
||||
type ExtractLoaderTypes<T> = T extends import('astro/loaders').LiveLoader<
|
||||
infer TData,
|
||||
infer TEntryFilter,
|
||||
infer TCollectionFilter,
|
||||
infer TError
|
||||
>
|
||||
? { data: TData; entryFilter: TEntryFilter; collectionFilter: TCollectionFilter; error: TError }
|
||||
: { data: never; entryFilter: never; collectionFilter: never; error: never };
|
||||
type ExtractEntryFilterType<T> = ExtractLoaderTypes<T>['entryFilter'];
|
||||
type ExtractCollectionFilterType<T> = ExtractLoaderTypes<T>['collectionFilter'];
|
||||
type ExtractErrorType<T> = ExtractLoaderTypes<T>['error'];
|
||||
|
||||
type LiveLoaderDataType<C extends keyof LiveContentConfig['collections']> =
|
||||
LiveContentConfig['collections'][C]['schema'] extends undefined
|
||||
? ExtractDataType<LiveContentConfig['collections'][C]['loader']>
|
||||
: import('astro/zod').infer<
|
||||
Exclude<LiveContentConfig['collections'][C]['schema'], undefined>
|
||||
>;
|
||||
type LiveLoaderEntryFilterType<C extends keyof LiveContentConfig['collections']> =
|
||||
ExtractEntryFilterType<LiveContentConfig['collections'][C]['loader']>;
|
||||
type LiveLoaderCollectionFilterType<C extends keyof LiveContentConfig['collections']> =
|
||||
ExtractCollectionFilterType<LiveContentConfig['collections'][C]['loader']>;
|
||||
type LiveLoaderErrorType<C extends keyof LiveContentConfig['collections']> = ExtractErrorType<
|
||||
LiveContentConfig['collections'][C]['loader']
|
||||
>;
|
||||
|
||||
export type ContentConfig = never;
|
||||
export type LiveContentConfig = never;
|
||||
}
|
||||
5
.astro/settings.json
Normal file
5
.astro/settings.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"_variables": {
|
||||
"lastUpdateCheck": 1775383544839
|
||||
}
|
||||
}
|
||||
1
.astro/types.d.ts
vendored
Normal file
1
.astro/types.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
/// <reference types="astro/client" />
|
||||
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
node_modules/
|
||||
dist/
|
||||
public/
|
||||
13
astro.config.mjs
Normal file
13
astro.config.mjs
Normal file
@ -0,0 +1,13 @@
|
||||
import { defineConfig } from 'astro/config';
|
||||
import react from '@astrojs/react';
|
||||
|
||||
export default defineConfig({
|
||||
integrations: [react()],
|
||||
output: 'static',
|
||||
site: 'https://aureliebarre.fr',
|
||||
vite: {
|
||||
css: {
|
||||
preprocessorOptions: {}
|
||||
}
|
||||
}
|
||||
});
|
||||
6192
package-lock.json
generated
Normal file
6192
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
30
package.json
Normal file
30
package.json
Normal file
@ -0,0 +1,30 @@
|
||||
{
|
||||
"name": "aurelie-barre-portfolio",
|
||||
"type": "module",
|
||||
"version": "1.0.0",
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"build": "astro build",
|
||||
"preview": "astro preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"astro": "^6.1.3",
|
||||
"@astrojs/react": "^4.2.1",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"@sanity/client": "^7.3.0",
|
||||
"@sanity/image-url": "^1.1.0",
|
||||
"motion": "^12.7.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^19.1.0",
|
||||
"@types/react-dom": "^19.1.2",
|
||||
"typescript": "^5.8.3"
|
||||
},
|
||||
"pnpm": {
|
||||
"onlyBuiltDependencies": [
|
||||
"esbuild",
|
||||
"sharp"
|
||||
]
|
||||
}
|
||||
}
|
||||
4009
pnpm-lock.yaml
generated
Normal file
4009
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
77
sanity/schemas/processStep.ts
Normal file
77
sanity/schemas/processStep.ts
Normal file
@ -0,0 +1,77 @@
|
||||
import { defineType, defineField } from 'sanity';
|
||||
|
||||
export default defineType({
|
||||
name: 'processStep',
|
||||
title: 'Étape du processus',
|
||||
type: 'document',
|
||||
fields: [
|
||||
defineField({
|
||||
name: 'title',
|
||||
title: "Titre de l'étape",
|
||||
type: 'string',
|
||||
validation: (rule) => rule.required(),
|
||||
}),
|
||||
defineField({
|
||||
name: 'stepNumber',
|
||||
title: "Numéro d'étape",
|
||||
type: 'number',
|
||||
validation: (rule) => rule.required().min(1).max(10),
|
||||
}),
|
||||
defineField({
|
||||
name: 'subtitle',
|
||||
title: 'Sous-titre',
|
||||
type: 'string',
|
||||
}),
|
||||
defineField({
|
||||
name: 'description',
|
||||
title: 'Description',
|
||||
type: 'text',
|
||||
rows: 4,
|
||||
}),
|
||||
defineField({
|
||||
name: 'icon',
|
||||
title: "Icône / illustration de l'étape",
|
||||
type: 'image',
|
||||
}),
|
||||
defineField({
|
||||
name: 'gallery',
|
||||
title: 'Photos illustratives',
|
||||
type: 'array',
|
||||
of: [
|
||||
{
|
||||
type: 'image',
|
||||
options: { hotspot: true },
|
||||
fields: [
|
||||
defineField({ name: 'alt', title: 'Texte alternatif', type: 'string' }),
|
||||
defineField({ name: 'caption', title: 'Légende', type: 'string' }),
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
defineField({
|
||||
name: 'duration',
|
||||
title: 'Durée estimée',
|
||||
type: 'string',
|
||||
}),
|
||||
],
|
||||
preview: {
|
||||
select: {
|
||||
title: 'title',
|
||||
subtitle: 'subtitle',
|
||||
stepNumber: 'stepNumber',
|
||||
},
|
||||
prepare({ title, subtitle, stepNumber }) {
|
||||
return {
|
||||
title: `${stepNumber}. ${title}`,
|
||||
subtitle,
|
||||
};
|
||||
},
|
||||
},
|
||||
orderings: [
|
||||
{
|
||||
title: 'Par numéro',
|
||||
name: 'stepNumberAsc',
|
||||
by: [{ field: 'stepNumber', direction: 'asc' }],
|
||||
},
|
||||
],
|
||||
});
|
||||
138
sanity/schemas/project.ts
Normal file
138
sanity/schemas/project.ts
Normal file
@ -0,0 +1,138 @@
|
||||
import { defineType, defineField } from 'sanity';
|
||||
|
||||
export default defineType({
|
||||
name: 'project',
|
||||
title: 'Projet',
|
||||
type: 'document',
|
||||
fields: [
|
||||
defineField({
|
||||
name: 'title',
|
||||
title: 'Titre du projet',
|
||||
type: 'string',
|
||||
validation: (rule) => rule.required(),
|
||||
}),
|
||||
defineField({
|
||||
name: 'slug',
|
||||
title: 'Slug (URL)',
|
||||
type: 'slug',
|
||||
options: { source: 'title', maxLength: 96 },
|
||||
validation: (rule) => rule.required(),
|
||||
}),
|
||||
defineField({
|
||||
name: 'category',
|
||||
title: 'Catégorie',
|
||||
type: 'string',
|
||||
options: {
|
||||
list: [
|
||||
{ title: 'Réalisation Pérenne', value: 'perenne' },
|
||||
{ title: 'Création Événement', value: 'event' },
|
||||
],
|
||||
layout: 'radio',
|
||||
},
|
||||
validation: (rule) => rule.required(),
|
||||
}),
|
||||
defineField({
|
||||
name: 'client',
|
||||
title: 'Client',
|
||||
type: 'string',
|
||||
validation: (rule) => rule.required(),
|
||||
}),
|
||||
defineField({
|
||||
name: 'date',
|
||||
title: 'Date du projet',
|
||||
type: 'date',
|
||||
}),
|
||||
defineField({
|
||||
name: 'location',
|
||||
title: 'Lieu',
|
||||
type: 'string',
|
||||
}),
|
||||
defineField({
|
||||
name: 'description',
|
||||
title: 'Description courte',
|
||||
type: 'text',
|
||||
rows: 3,
|
||||
}),
|
||||
defineField({
|
||||
name: 'heroImage',
|
||||
title: 'Image principale',
|
||||
type: 'image',
|
||||
options: { hotspot: true },
|
||||
fields: [
|
||||
defineField({
|
||||
name: 'alt',
|
||||
title: 'Texte alternatif',
|
||||
type: 'string',
|
||||
}),
|
||||
],
|
||||
}),
|
||||
defineField({
|
||||
name: 'gallery',
|
||||
title: 'Galerie photos',
|
||||
type: 'array',
|
||||
of: [
|
||||
{
|
||||
type: 'image',
|
||||
options: { hotspot: true },
|
||||
fields: [
|
||||
defineField({
|
||||
name: 'alt',
|
||||
title: 'Texte alternatif',
|
||||
type: 'string',
|
||||
}),
|
||||
defineField({
|
||||
name: 'size',
|
||||
title: "Taille d'affichage",
|
||||
type: 'string',
|
||||
options: {
|
||||
list: [
|
||||
{ title: 'Petit', value: 'small' },
|
||||
{ title: 'Moyen', value: 'medium' },
|
||||
{ title: 'Grand', value: 'large' },
|
||||
],
|
||||
},
|
||||
initialValue: 'medium',
|
||||
}),
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
defineField({
|
||||
name: 'tags',
|
||||
title: 'Tags',
|
||||
type: 'array',
|
||||
of: [{ type: 'string' }],
|
||||
options: { layout: 'tags' },
|
||||
}),
|
||||
defineField({
|
||||
name: 'featured',
|
||||
title: "Mis en avant sur la page d'accueil",
|
||||
type: 'boolean',
|
||||
initialValue: false,
|
||||
}),
|
||||
defineField({
|
||||
name: 'order',
|
||||
title: "Ordre d'affichage",
|
||||
type: 'number',
|
||||
}),
|
||||
],
|
||||
preview: {
|
||||
select: {
|
||||
title: 'title',
|
||||
subtitle: 'client',
|
||||
media: 'heroImage',
|
||||
},
|
||||
},
|
||||
orderings: [
|
||||
{
|
||||
title: 'Par ordre',
|
||||
name: 'orderAsc',
|
||||
by: [{ field: 'order', direction: 'asc' }],
|
||||
},
|
||||
{
|
||||
title: 'Par date',
|
||||
name: 'dateDesc',
|
||||
by: [{ field: 'date', direction: 'desc' }],
|
||||
},
|
||||
],
|
||||
});
|
||||
22
sanity/schemas/siteSettings.ts
Normal file
22
sanity/schemas/siteSettings.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { defineType, defineField } from 'sanity';
|
||||
|
||||
export default defineType({
|
||||
name: 'siteSettings',
|
||||
title: 'Paramètres du site',
|
||||
type: 'document',
|
||||
fields: [
|
||||
defineField({ name: 'siteName', title: 'Nom du site', type: 'string' }),
|
||||
defineField({ name: 'tagline', title: 'Accroche', type: 'string' }),
|
||||
defineField({ name: 'description', title: 'Description SEO', type: 'text', rows: 3 }),
|
||||
defineField({ name: 'email', title: 'Email de contact', type: 'string' }),
|
||||
defineField({ name: 'phone', title: 'Téléphone', type: 'string' }),
|
||||
defineField({ name: 'instagram', title: 'Instagram', type: 'url' }),
|
||||
defineField({ name: 'linkedin', title: 'LinkedIn', type: 'url' }),
|
||||
defineField({
|
||||
name: 'heroImages',
|
||||
title: "Images hero page d'accueil",
|
||||
type: 'array',
|
||||
of: [{ type: 'image', options: { hotspot: true } }],
|
||||
}),
|
||||
],
|
||||
});
|
||||
108
src/components/Footer.astro
Normal file
108
src/components/Footer.astro
Normal file
@ -0,0 +1,108 @@
|
||||
---
|
||||
const year = new Date().getFullYear();
|
||||
---
|
||||
|
||||
<footer class="footer">
|
||||
<div class="container">
|
||||
<div class="divider"></div>
|
||||
<div class="footer__inner">
|
||||
<div class="footer__left">
|
||||
<p class="footer__name">Aurélie Barré</p>
|
||||
<p class="footer__tagline text-secondary">Design d'intérieur & Création événementielle</p>
|
||||
</div>
|
||||
|
||||
<div class="footer__center">
|
||||
<a href="mailto:contact@aureliebarre.fr" class="footer__link">contact@aureliebarre.fr</a>
|
||||
</div>
|
||||
|
||||
<div class="footer__right">
|
||||
<div class="footer__social">
|
||||
<a href="https://instagram.com/aureliebarre" target="_blank" rel="noopener noreferrer" aria-label="Instagram" class="footer__social-link">
|
||||
Instagram
|
||||
</a>
|
||||
<a href="https://linkedin.com/in/aureliebarre" target="_blank" rel="noopener noreferrer" aria-label="LinkedIn" class="footer__social-link">
|
||||
LinkedIn
|
||||
</a>
|
||||
</div>
|
||||
<p class="footer__copyright text-light">© {year}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<style>
|
||||
.footer {
|
||||
padding-top: var(--space-xl);
|
||||
padding-bottom: var(--space-lg);
|
||||
}
|
||||
|
||||
.footer__inner {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
padding-top: var(--space-xl);
|
||||
gap: var(--space-lg);
|
||||
}
|
||||
|
||||
.footer__name {
|
||||
font-family: var(--font-serif);
|
||||
font-size: 1.1rem;
|
||||
margin-bottom: var(--space-xs);
|
||||
}
|
||||
|
||||
.footer__tagline {
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.footer__link {
|
||||
font-size: 0.85rem;
|
||||
letter-spacing: 0.02em;
|
||||
color: var(--color-text-secondary);
|
||||
transition: color var(--duration-fast) var(--ease-out);
|
||||
}
|
||||
|
||||
.footer__link:hover {
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
.footer__right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.footer__social {
|
||||
display: flex;
|
||||
gap: var(--space-lg);
|
||||
margin-bottom: var(--space-sm);
|
||||
}
|
||||
|
||||
.footer__social-link {
|
||||
font-size: 0.85rem;
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.footer__social-link:hover {
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
.footer__copyright {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.footer__inner {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.footer__right {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.footer__social {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
257
src/components/Header.astro
Normal file
257
src/components/Header.astro
Normal file
@ -0,0 +1,257 @@
|
||||
---
|
||||
const currentPath = Astro.url.pathname;
|
||||
|
||||
const navItems = [
|
||||
{ label: 'Réalisations Pérennes', href: '/realisations-perennes' },
|
||||
{ label: 'Créations Événement', href: '/creations-evenement' },
|
||||
{ label: 'Processus Créatif', href: '/processus' },
|
||||
];
|
||||
---
|
||||
|
||||
<header class="header">
|
||||
<div class="header__inner container">
|
||||
<a href="/" class="header__logo">
|
||||
<span class="header__logo-name">Aurélie Barré</span>
|
||||
</a>
|
||||
|
||||
<nav class="header__nav" aria-label="Navigation principale">
|
||||
<ul class="header__nav-list">
|
||||
{navItems.map(item => (
|
||||
<li>
|
||||
<a
|
||||
href={item.href}
|
||||
class:list={['header__nav-link', { 'active': currentPath.startsWith(item.href) }]}
|
||||
>
|
||||
{item.label}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<button class="header__menu-btn" aria-label="Ouvrir le menu" aria-expanded="false">
|
||||
<span class="header__menu-line"></span>
|
||||
<span class="header__menu-line"></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Mobile overlay -->
|
||||
<div class="header__overlay" aria-hidden="true">
|
||||
<nav class="header__overlay-nav">
|
||||
{navItems.map(item => (
|
||||
<a href={item.href} class="header__overlay-link">
|
||||
{item.label}
|
||||
</a>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<script>
|
||||
const btn = document.querySelector('.header__menu-btn');
|
||||
const overlay = document.querySelector('.header__overlay');
|
||||
const header = document.querySelector('.header');
|
||||
|
||||
btn?.addEventListener('click', () => {
|
||||
const isOpen = btn.getAttribute('aria-expanded') === 'true';
|
||||
btn.setAttribute('aria-expanded', String(!isOpen));
|
||||
overlay?.setAttribute('aria-hidden', String(isOpen));
|
||||
header?.classList.toggle('menu-open');
|
||||
document.body.style.overflow = isOpen ? '' : 'hidden';
|
||||
});
|
||||
|
||||
// Close on link click
|
||||
document.querySelectorAll('.header__overlay-link').forEach(link => {
|
||||
link.addEventListener('click', () => {
|
||||
btn?.setAttribute('aria-expanded', 'false');
|
||||
overlay?.setAttribute('aria-hidden', 'true');
|
||||
header?.classList.remove('menu-open');
|
||||
document.body.style.overflow = '';
|
||||
});
|
||||
});
|
||||
|
||||
// Header scroll effect
|
||||
let lastScroll = 0;
|
||||
window.addEventListener('scroll', () => {
|
||||
const scrollY = window.scrollY;
|
||||
if (scrollY > 100) {
|
||||
header?.classList.add('scrolled');
|
||||
} else {
|
||||
header?.classList.remove('scrolled');
|
||||
}
|
||||
lastScroll = scrollY;
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.header {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 100;
|
||||
height: var(--header-height);
|
||||
background-color: transparent;
|
||||
transition: background-color var(--duration-normal) var(--ease-out),
|
||||
box-shadow var(--duration-normal) var(--ease-out);
|
||||
}
|
||||
|
||||
.header.scrolled {
|
||||
background-color: rgba(250, 250, 248, 0.95);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
box-shadow: 0 1px 0 var(--color-border-light);
|
||||
}
|
||||
|
||||
.header__inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.header__logo {
|
||||
text-decoration: none;
|
||||
z-index: 101;
|
||||
}
|
||||
|
||||
.header__logo-name {
|
||||
font-family: var(--font-serif);
|
||||
font-size: 1.4rem;
|
||||
color: var(--color-text);
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
|
||||
.header__nav-list {
|
||||
display: flex;
|
||||
gap: var(--space-xl);
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.header__nav-link {
|
||||
font-size: 0.85rem;
|
||||
font-weight: 400;
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-text-secondary);
|
||||
text-decoration: none;
|
||||
position: relative;
|
||||
padding-bottom: 2px;
|
||||
transition: color var(--duration-fast) var(--ease-out);
|
||||
}
|
||||
|
||||
.header__nav-link::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 0;
|
||||
height: 1px;
|
||||
background-color: var(--color-accent);
|
||||
transition: width var(--duration-normal) var(--ease-out);
|
||||
}
|
||||
|
||||
.header__nav-link:hover,
|
||||
.header__nav-link.active {
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.header__nav-link:hover::after,
|
||||
.header__nav-link.active::after {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Mobile menu button */
|
||||
.header__menu-btn {
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 8px;
|
||||
z-index: 101;
|
||||
}
|
||||
|
||||
.header__menu-line {
|
||||
display: block;
|
||||
width: 24px;
|
||||
height: 1.5px;
|
||||
background-color: var(--color-text);
|
||||
transition: transform var(--duration-normal) var(--ease-out),
|
||||
opacity var(--duration-fast);
|
||||
}
|
||||
|
||||
.menu-open .header__menu-line:first-child {
|
||||
transform: translateY(3.75px) rotate(45deg);
|
||||
}
|
||||
|
||||
.menu-open .header__menu-line:last-child {
|
||||
transform: translateY(-3.75px) rotate(-45deg);
|
||||
}
|
||||
|
||||
/* Mobile overlay */
|
||||
.header__overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: var(--color-bg);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: opacity var(--duration-normal) var(--ease-out),
|
||||
visibility var(--duration-normal);
|
||||
z-index: 99;
|
||||
}
|
||||
|
||||
.menu-open .header__overlay {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.header__overlay-nav {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: var(--space-lg);
|
||||
}
|
||||
|
||||
.header__overlay-link {
|
||||
font-family: var(--font-serif);
|
||||
font-size: clamp(1.8rem, 5vw, 2.5rem);
|
||||
color: var(--color-text);
|
||||
text-decoration: none;
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
transition: opacity var(--duration-normal) var(--ease-out),
|
||||
transform var(--duration-normal) var(--ease-out),
|
||||
color var(--duration-fast);
|
||||
}
|
||||
|
||||
.menu-open .header__overlay-link {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.menu-open .header__overlay-link:nth-child(1) { transition-delay: 100ms; }
|
||||
.menu-open .header__overlay-link:nth-child(2) { transition-delay: 200ms; }
|
||||
.menu-open .header__overlay-link:nth-child(3) { transition-delay: 300ms; }
|
||||
|
||||
.header__overlay-link:hover {
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.header__nav {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.header__menu-btn {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
255
src/components/MasonryGallery.tsx
Normal file
255
src/components/MasonryGallery.tsx
Normal file
@ -0,0 +1,255 @@
|
||||
import { useState, useCallback, useEffect, useRef } from 'react';
|
||||
import { motion, AnimatePresence } from 'motion/react';
|
||||
|
||||
interface GalleryImage {
|
||||
src: string;
|
||||
alt: string;
|
||||
size?: 'small' | 'medium' | 'large';
|
||||
}
|
||||
|
||||
interface Props {
|
||||
images: GalleryImage[];
|
||||
}
|
||||
|
||||
export default function MasonryGallery({ images }: Props) {
|
||||
const [selectedIndex, setSelectedIndex] = useState<number | null>(null);
|
||||
const [columns, setColumns] = useState(3);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Responsive columns
|
||||
useEffect(() => {
|
||||
const updateColumns = () => {
|
||||
const width = window.innerWidth;
|
||||
if (width < 640) setColumns(1);
|
||||
else if (width < 1024) setColumns(2);
|
||||
else setColumns(3);
|
||||
};
|
||||
updateColumns();
|
||||
window.addEventListener('resize', updateColumns);
|
||||
return () => window.removeEventListener('resize', updateColumns);
|
||||
}, []);
|
||||
|
||||
// Distribute images across columns
|
||||
const getColumns = useCallback(() => {
|
||||
const cols: GalleryImage[][] = Array.from({ length: columns }, () => []);
|
||||
images.forEach((img, i) => {
|
||||
cols[i % columns].push(img);
|
||||
});
|
||||
return cols;
|
||||
}, [images, columns]);
|
||||
|
||||
// Lightbox keyboard nav
|
||||
useEffect(() => {
|
||||
const handleKey = (e: KeyboardEvent) => {
|
||||
if (selectedIndex === null) return;
|
||||
if (e.key === 'Escape') setSelectedIndex(null);
|
||||
if (e.key === 'ArrowRight') setSelectedIndex(prev =>
|
||||
prev !== null && prev < images.length - 1 ? prev + 1 : prev
|
||||
);
|
||||
if (e.key === 'ArrowLeft') setSelectedIndex(prev =>
|
||||
prev !== null && prev > 0 ? prev - 1 : prev
|
||||
);
|
||||
};
|
||||
window.addEventListener('keydown', handleKey);
|
||||
return () => window.removeEventListener('keydown', handleKey);
|
||||
}, [selectedIndex, images.length]);
|
||||
|
||||
// Lock body scroll when lightbox open
|
||||
useEffect(() => {
|
||||
document.body.style.overflow = selectedIndex !== null ? 'hidden' : '';
|
||||
return () => { document.body.style.overflow = ''; };
|
||||
}, [selectedIndex]);
|
||||
|
||||
const flatIndex = (colIdx: number, rowIdx: number) => {
|
||||
return rowIdx * columns + colIdx;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div ref={containerRef} style={gridContainerStyle(columns)}>
|
||||
{getColumns().map((col, colIdx) => (
|
||||
<div key={colIdx} style={columnStyle}>
|
||||
{col.map((img, rowIdx) => {
|
||||
const idx = flatIndex(colIdx, rowIdx);
|
||||
return (
|
||||
<motion.div
|
||||
key={idx}
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: idx * 0.06, duration: 0.6, ease: [0.16, 1, 0.3, 1] }}
|
||||
style={imageWrapperStyle}
|
||||
onClick={() => setSelectedIndex(idx)}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label={`Voir ${img.alt}`}
|
||||
>
|
||||
<img
|
||||
src={img.src}
|
||||
alt={img.alt}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
style={imageStyle}
|
||||
onMouseEnter={(e) => {
|
||||
(e.target as HTMLImageElement).style.transform = 'scale(1.03)';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
(e.target as HTMLImageElement).style.transform = 'scale(1)';
|
||||
}}
|
||||
/>
|
||||
</motion.div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Lightbox */}
|
||||
<AnimatePresence>
|
||||
{selectedIndex !== null && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
style={lightboxStyle}
|
||||
onClick={() => setSelectedIndex(null)}
|
||||
>
|
||||
{/* Close button */}
|
||||
<button
|
||||
style={closeButtonStyle}
|
||||
onClick={() => setSelectedIndex(null)}
|
||||
aria-label="Fermer"
|
||||
>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="white" strokeWidth="1.5">
|
||||
<line x1="4" y1="4" x2="20" y2="20" />
|
||||
<line x1="20" y1="4" x2="4" y2="20" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{/* Counter */}
|
||||
<div style={counterStyle}>
|
||||
{selectedIndex + 1} / {images.length}
|
||||
</div>
|
||||
|
||||
{/* Navigation arrows */}
|
||||
{selectedIndex > 0 && (
|
||||
<button
|
||||
style={{ ...arrowStyle, left: '2rem' }}
|
||||
onClick={(e) => { e.stopPropagation(); setSelectedIndex(selectedIndex - 1); }}
|
||||
aria-label="Photo précédente"
|
||||
>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="white" strokeWidth="1.5">
|
||||
<polyline points="15,4 7,12 15,20" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
{selectedIndex < images.length - 1 && (
|
||||
<button
|
||||
style={{ ...arrowStyle, right: '2rem' }}
|
||||
onClick={(e) => { e.stopPropagation(); setSelectedIndex(selectedIndex + 1); }}
|
||||
aria-label="Photo suivante"
|
||||
>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="white" strokeWidth="1.5">
|
||||
<polyline points="9,4 17,12 9,20" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Image */}
|
||||
<motion.img
|
||||
key={selectedIndex}
|
||||
src={images[selectedIndex].src}
|
||||
alt={images[selectedIndex].alt}
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.95 }}
|
||||
transition={{ duration: 0.3, ease: [0.16, 1, 0.3, 1] }}
|
||||
style={lightboxImageStyle}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// --- Inline Styles ---
|
||||
const gridContainerStyle = (cols: number): React.CSSProperties => ({
|
||||
display: 'grid',
|
||||
gridTemplateColumns: `repeat(${cols}, 1fr)`,
|
||||
gap: '1rem',
|
||||
});
|
||||
|
||||
const columnStyle: React.CSSProperties = {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '1rem',
|
||||
};
|
||||
|
||||
const imageWrapperStyle: React.CSSProperties = {
|
||||
overflow: 'hidden',
|
||||
cursor: 'pointer',
|
||||
borderRadius: '2px',
|
||||
backgroundColor: '#F3F1ED',
|
||||
};
|
||||
|
||||
const imageStyle: React.CSSProperties = {
|
||||
width: '100%',
|
||||
height: 'auto',
|
||||
display: 'block',
|
||||
transition: 'transform 0.6s cubic-bezier(0.16, 1, 0.3, 1)',
|
||||
};
|
||||
|
||||
const lightboxStyle: React.CSSProperties = {
|
||||
position: 'fixed',
|
||||
inset: 0,
|
||||
zIndex: 200,
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.92)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
cursor: 'pointer',
|
||||
};
|
||||
|
||||
const lightboxImageStyle: React.CSSProperties = {
|
||||
maxWidth: '90vw',
|
||||
maxHeight: '90vh',
|
||||
objectFit: 'contain',
|
||||
cursor: 'default',
|
||||
};
|
||||
|
||||
const closeButtonStyle: React.CSSProperties = {
|
||||
position: 'absolute',
|
||||
top: '2rem',
|
||||
right: '2rem',
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
cursor: 'pointer',
|
||||
padding: '8px',
|
||||
zIndex: 201,
|
||||
};
|
||||
|
||||
const counterStyle: React.CSSProperties = {
|
||||
position: 'absolute',
|
||||
top: '2rem',
|
||||
left: '50%',
|
||||
transform: 'translateX(-50%)',
|
||||
color: 'rgba(255,255,255,0.6)',
|
||||
fontSize: '0.85rem',
|
||||
letterSpacing: '0.08em',
|
||||
zIndex: 201,
|
||||
};
|
||||
|
||||
const arrowStyle: React.CSSProperties = {
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
transform: 'translateY(-50%)',
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
cursor: 'pointer',
|
||||
padding: '12px',
|
||||
zIndex: 201,
|
||||
opacity: 0.7,
|
||||
transition: 'opacity 0.2s',
|
||||
};
|
||||
238
src/components/ProcessTimeline.tsx
Normal file
238
src/components/ProcessTimeline.tsx
Normal file
@ -0,0 +1,238 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { motion, useInView } from 'motion/react';
|
||||
|
||||
interface ProcessStep {
|
||||
title: string;
|
||||
stepNumber: number;
|
||||
subtitle: string;
|
||||
description: string;
|
||||
duration?: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
steps: ProcessStep[];
|
||||
}
|
||||
|
||||
function StepCard({ step, index }: { step: ProcessStep; index: number }) {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const isInView = useInView(ref, { once: true, margin: '-100px 0px' });
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
ref={ref}
|
||||
initial={{ opacity: 0, y: 60 }}
|
||||
animate={isInView ? { opacity: 1, y: 0 } : { opacity: 0, y: 60 }}
|
||||
transition={{ duration: 0.8, delay: 0.1, ease: [0.16, 1, 0.3, 1] }}
|
||||
style={stepStyle}
|
||||
>
|
||||
<div style={stepLeftStyle}>
|
||||
<div style={stepNumberContainerStyle}>
|
||||
<motion.span
|
||||
initial={{ scale: 0 }}
|
||||
animate={isInView ? { scale: 1 } : { scale: 0 }}
|
||||
transition={{ delay: 0.2, duration: 0.5, ease: [0.16, 1, 0.3, 1] }}
|
||||
style={stepNumberStyle}
|
||||
>
|
||||
{String(step.stepNumber).padStart(2, '0')}
|
||||
</motion.span>
|
||||
{index < 4 && <div style={stepLineStyle} />}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={stepContentStyle}>
|
||||
<span style={stepSubtitleStyle}>{step.subtitle}</span>
|
||||
<h3 style={stepTitleStyle}>{step.title}</h3>
|
||||
<p style={stepDescStyle}>{step.description}</p>
|
||||
{step.duration && (
|
||||
<span style={stepDurationStyle}>{step.duration}</span>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ProcessTimeline({ steps }: Props) {
|
||||
const [activeStep, setActiveStep] = useState(0);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
if (!containerRef.current) return;
|
||||
const children = containerRef.current.children;
|
||||
const viewportCenter = window.innerHeight / 2;
|
||||
|
||||
for (let i = children.length - 1; i >= 0; i--) {
|
||||
const rect = children[i].getBoundingClientRect();
|
||||
if (rect.top < viewportCenter) {
|
||||
setActiveStep(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('scroll', handleScroll, { passive: true });
|
||||
return () => window.removeEventListener('scroll', handleScroll);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div style={containerStyle}>
|
||||
{/* Progress indicator */}
|
||||
<div style={progressBarContainerStyle}>
|
||||
<div style={progressTrackStyle}>
|
||||
<motion.div
|
||||
style={progressFillStyle}
|
||||
animate={{ height: `${((activeStep + 1) / steps.length) * 100}%` }}
|
||||
transition={{ duration: 0.6, ease: [0.16, 1, 0.3, 1] }}
|
||||
/>
|
||||
</div>
|
||||
{steps.map((_, i) => (
|
||||
<motion.div
|
||||
key={i}
|
||||
style={{
|
||||
...progressDotStyle,
|
||||
top: `${(i / (steps.length - 1)) * 100}%`,
|
||||
}}
|
||||
animate={{
|
||||
backgroundColor: i <= activeStep ? '#C4A77D' : '#E8E6E1',
|
||||
scale: i === activeStep ? 1.3 : 1,
|
||||
}}
|
||||
transition={{ duration: 0.3 }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Steps */}
|
||||
<div ref={containerRef} style={stepsContainerStyle}>
|
||||
{steps.map((step, index) => (
|
||||
<StepCard key={step.stepNumber} step={step} index={index} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// --- Styles ---
|
||||
const containerStyle: React.CSSProperties = {
|
||||
display: 'flex',
|
||||
gap: '3rem',
|
||||
position: 'relative',
|
||||
maxWidth: '900px',
|
||||
margin: '0 auto',
|
||||
};
|
||||
|
||||
const progressBarContainerStyle: React.CSSProperties = {
|
||||
position: 'sticky',
|
||||
top: '40vh',
|
||||
height: '300px',
|
||||
width: '2px',
|
||||
alignSelf: 'flex-start',
|
||||
display: 'none', // Hidden on mobile, shown via media query in parent
|
||||
};
|
||||
|
||||
const progressTrackStyle: React.CSSProperties = {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
width: '2px',
|
||||
backgroundColor: '#E8E6E1',
|
||||
borderRadius: '1px',
|
||||
};
|
||||
|
||||
const progressFillStyle: React.CSSProperties = {
|
||||
width: '100%',
|
||||
backgroundColor: '#C4A77D',
|
||||
borderRadius: '1px',
|
||||
};
|
||||
|
||||
const progressDotStyle: React.CSSProperties = {
|
||||
position: 'absolute',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
width: '8px',
|
||||
height: '8px',
|
||||
borderRadius: '50%',
|
||||
zIndex: 1,
|
||||
};
|
||||
|
||||
const stepsContainerStyle: React.CSSProperties = {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '6rem',
|
||||
flex: 1,
|
||||
};
|
||||
|
||||
const stepStyle: React.CSSProperties = {
|
||||
display: 'flex',
|
||||
gap: '2rem',
|
||||
alignItems: 'flex-start',
|
||||
};
|
||||
|
||||
const stepLeftStyle: React.CSSProperties = {
|
||||
flexShrink: 0,
|
||||
width: '60px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
};
|
||||
|
||||
const stepNumberContainerStyle: React.CSSProperties = {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
gap: '0.5rem',
|
||||
};
|
||||
|
||||
const stepNumberStyle: React.CSSProperties = {
|
||||
fontFamily: "'DM Serif Display', Georgia, serif",
|
||||
fontSize: '2rem',
|
||||
color: '#C4A77D',
|
||||
lineHeight: 1,
|
||||
};
|
||||
|
||||
const stepLineStyle: React.CSSProperties = {
|
||||
width: '1px',
|
||||
height: '100px',
|
||||
backgroundColor: '#E8E6E1',
|
||||
marginTop: '0.5rem',
|
||||
};
|
||||
|
||||
const stepContentStyle: React.CSSProperties = {
|
||||
flex: 1,
|
||||
paddingTop: '0.25rem',
|
||||
};
|
||||
|
||||
const stepSubtitleStyle: React.CSSProperties = {
|
||||
fontSize: '0.75rem',
|
||||
letterSpacing: '0.1em',
|
||||
textTransform: 'uppercase' as const,
|
||||
color: '#9A9A9A',
|
||||
display: 'block',
|
||||
marginBottom: '0.5rem',
|
||||
};
|
||||
|
||||
const stepTitleStyle: React.CSSProperties = {
|
||||
fontFamily: "'DM Serif Display', Georgia, serif",
|
||||
fontSize: 'clamp(1.5rem, 3vw, 2rem)',
|
||||
fontWeight: 400,
|
||||
marginBottom: '1rem',
|
||||
lineHeight: 1.3,
|
||||
color: '#1A1A1A',
|
||||
};
|
||||
|
||||
const stepDescStyle: React.CSSProperties = {
|
||||
fontSize: '1rem',
|
||||
lineHeight: 1.7,
|
||||
color: '#6B6B6B',
|
||||
maxWidth: '55ch',
|
||||
};
|
||||
|
||||
const stepDurationStyle: React.CSSProperties = {
|
||||
display: 'inline-block',
|
||||
marginTop: '1rem',
|
||||
fontSize: '0.8rem',
|
||||
letterSpacing: '0.04em',
|
||||
color: '#C4A77D',
|
||||
padding: '4px 12px',
|
||||
border: '1px solid #E8E6E1',
|
||||
borderRadius: '2px',
|
||||
};
|
||||
123
src/components/ProjectCard.astro
Normal file
123
src/components/ProjectCard.astro
Normal file
@ -0,0 +1,123 @@
|
||||
---
|
||||
interface Props {
|
||||
title: string;
|
||||
client: string;
|
||||
slug: string;
|
||||
category: 'perenne' | 'event';
|
||||
heroImage: string;
|
||||
description: string;
|
||||
tags?: string[];
|
||||
index?: number;
|
||||
}
|
||||
|
||||
const { title, client, slug, category, heroImage, description, tags = [], index = 0 } = Astro.props;
|
||||
const baseUrl = category === 'perenne' ? '/realisations-perennes' : '/creations-evenement';
|
||||
const href = `${baseUrl}/${slug}`;
|
||||
const delay = `${index * 100}ms`;
|
||||
---
|
||||
|
||||
<a href={href} class="project-card reveal" style={`transition-delay: ${delay}`}>
|
||||
<div class="project-card__image img-hover-zoom">
|
||||
<img
|
||||
src={heroImage}
|
||||
alt={title}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
<div class="project-card__overlay">
|
||||
<span class="project-card__cta">Découvrir</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="project-card__info">
|
||||
<span class="project-card__client text-light">{client}</span>
|
||||
<h3 class="project-card__title">{title}</h3>
|
||||
{tags.length > 0 && (
|
||||
<div class="project-card__tags">
|
||||
{tags.slice(0, 3).map(tag => (
|
||||
<span class="project-card__tag">{tag}</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<style>
|
||||
.project-card {
|
||||
display: block;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.project-card__image {
|
||||
position: relative;
|
||||
aspect-ratio: 4 / 3;
|
||||
background-color: var(--color-bg-alt);
|
||||
margin-bottom: var(--space-md);
|
||||
}
|
||||
|
||||
.project-card__image img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.project-card__overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(26, 26, 26, 0);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: background var(--duration-normal) var(--ease-out);
|
||||
}
|
||||
|
||||
.project-card__cta {
|
||||
font-size: 0.85rem;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
color: white;
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
transition: opacity var(--duration-normal) var(--ease-out),
|
||||
transform var(--duration-normal) var(--ease-out);
|
||||
}
|
||||
|
||||
.project-card:hover .project-card__overlay {
|
||||
background: rgba(26, 26, 26, 0.25);
|
||||
}
|
||||
|
||||
.project-card:hover .project-card__cta {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.project-card__client {
|
||||
font-size: 0.75rem;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
display: block;
|
||||
margin-bottom: var(--space-xs);
|
||||
}
|
||||
|
||||
.project-card__title {
|
||||
font-family: var(--font-serif);
|
||||
font-size: 1.2rem;
|
||||
font-weight: 400;
|
||||
margin-bottom: var(--space-sm);
|
||||
}
|
||||
|
||||
.project-card__tags {
|
||||
display: flex;
|
||||
gap: var(--space-sm);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.project-card__tag {
|
||||
font-size: 0.7rem;
|
||||
letter-spacing: 0.04em;
|
||||
color: var(--color-text-light);
|
||||
padding: 2px 8px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 2px;
|
||||
}
|
||||
</style>
|
||||
10
src/env.d.ts
vendored
Normal file
10
src/env.d.ts
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
/// <reference types="astro/client" />
|
||||
|
||||
interface ImportMetaEnv {
|
||||
readonly PUBLIC_SANITY_PROJECT_ID: string;
|
||||
readonly PUBLIC_SANITY_DATASET: string;
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv;
|
||||
}
|
||||
79
src/layouts/BaseLayout.astro
Normal file
79
src/layouts/BaseLayout.astro
Normal file
@ -0,0 +1,79 @@
|
||||
---
|
||||
import Header from '../components/Header.astro';
|
||||
import Footer from '../components/Footer.astro';
|
||||
import '../styles/global.css';
|
||||
|
||||
interface Props {
|
||||
title?: string;
|
||||
description?: string;
|
||||
image?: string;
|
||||
}
|
||||
|
||||
const {
|
||||
title = 'Aurélie Barré — Design d\'intérieur & Création événementielle',
|
||||
description = 'Portfolio d\'Aurélie Barré, designeuse d\'intérieur et directrice artistique spécialisée en design d\'expérience et création événementielle.',
|
||||
image = '/og-image.jpg'
|
||||
} = Astro.props;
|
||||
|
||||
const canonicalURL = new URL(Astro.url.pathname, Astro.site);
|
||||
---
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="description" content={description} />
|
||||
<link rel="canonical" href={canonicalURL} />
|
||||
|
||||
<!-- Open Graph -->
|
||||
<meta property="og:title" content={title} />
|
||||
<meta property="og:description" content={description} />
|
||||
<meta property="og:image" content={image} />
|
||||
<meta property="og:url" content={canonicalURL} />
|
||||
<meta property="og:type" content="website" />
|
||||
|
||||
<!-- Fonts: DM Serif Display + Inter -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=DM+Serif+Display:ital@0;1&family=Inter:wght@300;400;500&display=swap" rel="stylesheet" />
|
||||
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<title>{title}</title>
|
||||
</head>
|
||||
<body>
|
||||
<Header />
|
||||
|
||||
<main class="page-enter">
|
||||
<slot />
|
||||
</main>
|
||||
|
||||
<Footer />
|
||||
|
||||
<script>
|
||||
// Intersection Observer for scroll reveal animations
|
||||
const observerCallback: IntersectionObserverCallback = (entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
entry.target.classList.add('visible');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const observer = new IntersectionObserver(observerCallback, {
|
||||
threshold: 0.1,
|
||||
rootMargin: '0px 0px -50px 0px'
|
||||
});
|
||||
|
||||
document.querySelectorAll('.reveal').forEach((el) => {
|
||||
observer.observe(el);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
<style>
|
||||
main {
|
||||
min-height: calc(100vh - var(--header-height));
|
||||
}
|
||||
</style>
|
||||
346
src/lib/mock-data.ts
Normal file
346
src/lib/mock-data.ts
Normal file
@ -0,0 +1,346 @@
|
||||
// Mock data for development without Sanity connection
|
||||
// Will be replaced by Sanity queries in production
|
||||
|
||||
export interface MockProject {
|
||||
_id: string;
|
||||
title: string;
|
||||
slug: { current: string };
|
||||
category: 'perenne' | 'event';
|
||||
client: string;
|
||||
date: string;
|
||||
location?: string;
|
||||
description: string;
|
||||
heroImage: string;
|
||||
gallery: Array<{ src: string; alt: string; size: 'small' | 'medium' | 'large' }>;
|
||||
tags: string[];
|
||||
featured: boolean;
|
||||
order: number;
|
||||
}
|
||||
|
||||
// Helper: generate gallery from numbered files
|
||||
function makeGallery(basePath: string, count: number, ext: string, title: string): MockProject['gallery'] {
|
||||
const sizes: Array<'small' | 'medium' | 'large'> = ['large', 'medium', 'small', 'medium', 'large', 'small', 'medium', 'large', 'small', 'medium', 'large', 'medium'];
|
||||
return Array.from({ length: count }, (_, i) => ({
|
||||
src: `${basePath}/${String(i + 1).padStart(2, '0')}.${ext}`,
|
||||
alt: `${title} — Photo ${i + 1}`,
|
||||
size: sizes[i % sizes.length],
|
||||
}));
|
||||
}
|
||||
|
||||
export const mockProjects: MockProject[] = [
|
||||
// --- EVENTS ---
|
||||
{
|
||||
_id: '1',
|
||||
title: 'Inside Moët & Chandon',
|
||||
slug: { current: 'moet-chandon' },
|
||||
category: 'event',
|
||||
client: 'Moët & Chandon',
|
||||
date: '2019-06',
|
||||
location: 'Paris, Champs-Élysées',
|
||||
description: 'Scénographie d\'un rooftop éphémère avec vue sur l\'Arc de Triomphe pour le lancement de la collection Inside.',
|
||||
heroImage: '/images/events/moet/01.jpg',
|
||||
gallery: makeGallery('/images/events/moet', 12, 'jpg', 'Moët & Chandon'),
|
||||
tags: ['Luxe', 'Rooftop', 'Scénographie'],
|
||||
featured: true,
|
||||
order: 1,
|
||||
},
|
||||
{
|
||||
_id: '2',
|
||||
title: 'Engie — Convention',
|
||||
slug: { current: 'engie' },
|
||||
category: 'event',
|
||||
client: 'Engie',
|
||||
date: '2019-06',
|
||||
location: 'Paris',
|
||||
description: 'Design global et direction artistique pour la convention annuelle Engie. Création d\'univers immersifs déclinés en plusieurs espaces.',
|
||||
heroImage: '/images/events/engie/01.jpg',
|
||||
gallery: makeGallery('/images/events/engie', 10, 'jpg', 'Engie'),
|
||||
tags: ['Corporate', 'Convention', 'Direction artistique'],
|
||||
featured: false,
|
||||
order: 2,
|
||||
},
|
||||
{
|
||||
_id: '3',
|
||||
title: 'Martell — Lancement',
|
||||
slug: { current: 'martell' },
|
||||
category: 'event',
|
||||
client: 'Martell',
|
||||
date: '2019-09',
|
||||
location: 'Paris',
|
||||
description: 'Conception et réalisation de l\'espace de lancement pour Martell. Un mélange subtil de tradition et de modernité.',
|
||||
heroImage: '/images/events/martell/01.jpg',
|
||||
gallery: makeGallery('/images/events/martell', 5, 'jpg', 'Martell'),
|
||||
tags: ['Luxe', 'Lancement produit', 'Spiritueux'],
|
||||
featured: false,
|
||||
order: 3,
|
||||
},
|
||||
{
|
||||
_id: '4',
|
||||
title: 'AlUla — Paris & Arabie',
|
||||
slug: { current: 'alula' },
|
||||
category: 'event',
|
||||
client: 'AlUla',
|
||||
date: '2020-01',
|
||||
location: 'Paris & Arabie Saoudite',
|
||||
description: 'Création d\'un parcours sensoriel entre Paris et l\'Arabie Saoudite, mêlant patrimoine culturel et design contemporain.',
|
||||
heroImage: '/images/events/alula/01.jpg',
|
||||
gallery: makeGallery('/images/events/alula', 12, 'jpg', 'AlUla'),
|
||||
tags: ['International', 'Culture', 'Immersif'],
|
||||
featured: true,
|
||||
order: 4,
|
||||
},
|
||||
{
|
||||
_id: '5',
|
||||
title: 'LVMH — Métaverse',
|
||||
slug: { current: 'lvmh-metaverse' },
|
||||
category: 'event',
|
||||
client: 'LVMH',
|
||||
date: '2024-03',
|
||||
location: 'Paris',
|
||||
description: 'Direction artistique de l\'événement LVMH Métaverse. Une immersion entre monde physique et digital.',
|
||||
heroImage: '/images/events/lvmh-metaverse/01.jpg',
|
||||
gallery: makeGallery('/images/events/lvmh-metaverse', 12, 'jpg', 'LVMH Métaverse'),
|
||||
tags: ['Luxe', 'Digital', 'Innovation'],
|
||||
featured: true,
|
||||
order: 5,
|
||||
},
|
||||
{
|
||||
_id: '6',
|
||||
title: 'Dior — Christmas Popup',
|
||||
slug: { current: 'dior-christmas' },
|
||||
category: 'event',
|
||||
client: 'Dior',
|
||||
date: '2022-12',
|
||||
location: 'Paris',
|
||||
description: 'Scénographie du popup de Noël Dior. Un univers féerique mêlant bleu nuit, or et cristal.',
|
||||
heroImage: '/images/events/dior/01.jpeg',
|
||||
gallery: makeGallery('/images/events/dior', 12, 'jpeg', 'Dior Christmas'),
|
||||
tags: ['Luxe', 'Popup', 'Noël'],
|
||||
featured: true,
|
||||
order: 6,
|
||||
},
|
||||
{
|
||||
_id: '7',
|
||||
title: 'AMI — Showroom',
|
||||
slug: { current: 'ami' },
|
||||
category: 'event',
|
||||
client: 'AMI Paris',
|
||||
date: '2023-06',
|
||||
location: 'Paris',
|
||||
description: 'Design et aménagement du showroom AMI Paris pour la Fashion Week. Minimalisme parisien et chaleur.',
|
||||
heroImage: '/images/events/ami/01.jpg',
|
||||
gallery: makeGallery('/images/events/ami', 12, 'jpg', 'AMI Paris'),
|
||||
tags: ['Mode', 'Showroom', 'Fashion Week'],
|
||||
featured: false,
|
||||
order: 7,
|
||||
},
|
||||
{
|
||||
_id: '8',
|
||||
title: 'Aurora',
|
||||
slug: { current: 'aurora' },
|
||||
category: 'event',
|
||||
client: 'Aurora',
|
||||
date: '2023-07',
|
||||
location: 'Paris',
|
||||
description: 'Création d\'un espace immersif pour l\'événement Aurora. Lumières, textures et mise en scène poétique.',
|
||||
heroImage: '/images/events/aurora/01.jpg',
|
||||
gallery: makeGallery('/images/events/aurora', 5, 'jpg', 'Aurora'),
|
||||
tags: ['Immersif', 'Lumière', 'Poétique'],
|
||||
featured: false,
|
||||
order: 8,
|
||||
},
|
||||
{
|
||||
_id: '9',
|
||||
title: 'Symposium Ryad',
|
||||
slug: { current: 'symposium-ryad' },
|
||||
category: 'event',
|
||||
client: 'Symposium',
|
||||
date: '2023-11',
|
||||
location: 'Riyad, Arabie Saoudite',
|
||||
description: 'Direction artistique et scénographie du symposium international à Riyad.',
|
||||
heroImage: '/images/events/symposium-ryad/01.jpeg',
|
||||
gallery: makeGallery('/images/events/symposium-ryad', 7, 'jpeg', 'Symposium Ryad'),
|
||||
tags: ['International', 'Corporate', 'Moyen-Orient'],
|
||||
featured: false,
|
||||
order: 9,
|
||||
},
|
||||
{
|
||||
_id: '10',
|
||||
title: 'L\'Oréal',
|
||||
slug: { current: 'loreal' },
|
||||
category: 'event',
|
||||
client: 'L\'Oréal',
|
||||
date: '2024-04',
|
||||
location: 'Paris',
|
||||
description: 'Conception de l\'espace événementiel L\'Oréal. Innovation beauté et design d\'expérience.',
|
||||
heroImage: '/images/events/loreal/01.jpeg',
|
||||
gallery: makeGallery('/images/events/loreal', 8, 'jpeg', 'L\'Oréal'),
|
||||
tags: ['Beauté', 'Innovation', 'Corporate'],
|
||||
featured: false,
|
||||
order: 10,
|
||||
},
|
||||
{
|
||||
_id: '11',
|
||||
title: 'LVMH Dare',
|
||||
slug: { current: 'lvmh-dare' },
|
||||
category: 'event',
|
||||
client: 'LVMH',
|
||||
date: '2024-03',
|
||||
location: 'Paris',
|
||||
description: 'Direction artistique de l\'événement LVMH Dare, célébrant l\'innovation au sein du groupe.',
|
||||
heroImage: '/images/events/lvmh-dare/01.jpeg',
|
||||
gallery: makeGallery('/images/events/lvmh-dare', 12, 'jpeg', 'LVMH Dare'),
|
||||
tags: ['Luxe', 'Innovation', 'Corporate'],
|
||||
featured: false,
|
||||
order: 11,
|
||||
},
|
||||
{
|
||||
_id: '12',
|
||||
title: 'La Première',
|
||||
slug: { current: 'la-premiere' },
|
||||
category: 'event',
|
||||
client: 'La Première',
|
||||
date: '2023-05',
|
||||
location: 'Paris',
|
||||
description: 'Création de l\'univers visuel et spatial pour l\'événement La Première.',
|
||||
heroImage: '/images/events/la-premiere/01.jpeg',
|
||||
gallery: makeGallery('/images/events/la-premiere', 10, 'jpeg', 'La Première'),
|
||||
tags: ['Exclusif', 'Design', 'Événementiel'],
|
||||
featured: false,
|
||||
order: 12,
|
||||
},
|
||||
{
|
||||
_id: '13',
|
||||
title: 'Ministry',
|
||||
slug: { current: 'ministry' },
|
||||
category: 'event',
|
||||
client: 'Ministry',
|
||||
date: '2022-03',
|
||||
location: 'Paris',
|
||||
description: 'Aménagement et design d\'espace pour Ministry.',
|
||||
heroImage: '/images/events/ministry/01.jpg',
|
||||
gallery: makeGallery('/images/events/ministry', 4, 'jpg', 'Ministry'),
|
||||
tags: ['Design', 'Espace', 'Événementiel'],
|
||||
featured: false,
|
||||
order: 13,
|
||||
},
|
||||
{
|
||||
_id: '14',
|
||||
title: 'LVMH Digital',
|
||||
slug: { current: 'lvmh-digital' },
|
||||
category: 'event',
|
||||
client: 'LVMH',
|
||||
date: '2020-06',
|
||||
location: 'Paris',
|
||||
description: 'Conception de l\'événement LVMH Digital. Transition numérique et design d\'expérience.',
|
||||
heroImage: '/images/events/lvmh-digital/01.jpeg',
|
||||
gallery: makeGallery('/images/events/lvmh-digital', 12, 'jpeg', 'LVMH Digital'),
|
||||
tags: ['Luxe', 'Digital', 'Corporate'],
|
||||
featured: false,
|
||||
order: 14,
|
||||
},
|
||||
{
|
||||
_id: '15',
|
||||
title: 'Veolia',
|
||||
slug: { current: 'veolia' },
|
||||
category: 'event',
|
||||
client: 'Veolia',
|
||||
date: '2025-04',
|
||||
location: 'Paris',
|
||||
description: 'Design événementiel pour Veolia. Un espace éco-responsable et innovant.',
|
||||
heroImage: '/images/events/veolia/01.jpeg',
|
||||
gallery: makeGallery('/images/events/veolia', 6, 'jpeg', 'Veolia'),
|
||||
tags: ['RSE', 'Corporate', 'Innovation'],
|
||||
featured: false,
|
||||
order: 15,
|
||||
},
|
||||
// --- PÉRENNES ---
|
||||
{
|
||||
_id: '18',
|
||||
title: 'Cisco — Siège social',
|
||||
slug: { current: 'cisco-siege' },
|
||||
category: 'perenne',
|
||||
client: 'Cisco',
|
||||
date: '2023-10',
|
||||
location: 'Paris',
|
||||
description: 'Aménagement du showroom et des espaces de réception du siège Cisco France. Design contemporain et fonctionnel.',
|
||||
heroImage: '/images/perennes/cisco/01.jpeg',
|
||||
gallery: makeGallery('/images/perennes/cisco', 7, 'jpeg', 'Cisco Siège'),
|
||||
tags: ['Corporate', 'Showroom', 'Bureaux'],
|
||||
featured: true,
|
||||
order: 1,
|
||||
},
|
||||
{
|
||||
_id: '19',
|
||||
title: 'Devoteam — Bureaux',
|
||||
slug: { current: 'devoteam' },
|
||||
category: 'perenne',
|
||||
client: 'Devoteam',
|
||||
date: '2019-05',
|
||||
location: 'Paris',
|
||||
description: 'Réaménagement des espaces de travail Devoteam. Alliance entre collaboration et bien-être au travail.',
|
||||
heroImage: '/images/perennes/devoteam/01.jpg',
|
||||
gallery: makeGallery('/images/perennes/devoteam', 8, 'jpg', 'Devoteam'),
|
||||
tags: ['Corporate', 'Bureaux', 'Workspace'],
|
||||
featured: true,
|
||||
order: 2,
|
||||
},
|
||||
];
|
||||
|
||||
export const mockProcessSteps = [
|
||||
{
|
||||
_id: 'p1',
|
||||
title: 'Prise de contact',
|
||||
stepNumber: 1,
|
||||
subtitle: 'Écoute & Compréhension',
|
||||
description: 'Première rencontre pour comprendre votre univers, vos envies et vos contraintes. Un échange libre autour d\'un café pour poser les bases d\'une collaboration sur mesure.',
|
||||
duration: '1 à 2 semaines',
|
||||
},
|
||||
{
|
||||
_id: 'p2',
|
||||
title: 'Conception & Moodboard',
|
||||
stepNumber: 2,
|
||||
subtitle: 'Recherche & Inspiration',
|
||||
description: 'Création de planches d\'inspiration, palettes de couleurs et premiers croquis. Une phase de recherche créative pour traduire vos envies en un univers visuel cohérent.',
|
||||
duration: '2 à 3 semaines',
|
||||
},
|
||||
{
|
||||
_id: 'p3',
|
||||
title: 'Sélection & Sourcing',
|
||||
stepNumber: 3,
|
||||
subtitle: 'Mobilier, Matériaux & Peintures',
|
||||
description: 'Curation minutieuse de chaque élément : mobilier, tissus, matériaux, luminaires, peintures. Chaque pièce est choisie pour son caractère, sa qualité et sa cohérence avec l\'ensemble.',
|
||||
duration: '2 à 4 semaines',
|
||||
},
|
||||
{
|
||||
_id: 'p4',
|
||||
title: 'Réalisation',
|
||||
stepNumber: 4,
|
||||
subtitle: 'De la théorie à la pratique',
|
||||
description: 'Coordination des artisans et prestataires, suivi de chantier, gestion des imprévus. Une présence terrain pour garantir que chaque détail respecte la vision initiale.',
|
||||
duration: '4 à 12 semaines',
|
||||
},
|
||||
{
|
||||
_id: 'p5',
|
||||
title: 'Livraison & Retour',
|
||||
stepNumber: 5,
|
||||
subtitle: 'Le résultat & l\'expérience',
|
||||
description: 'Livraison clé en main, shooting photo du résultat final, et retour d\'expérience avec vous. Parce qu\'un projet réussi, c\'est avant tout un client qui se sent chez lui.',
|
||||
duration: 'Le jour J',
|
||||
},
|
||||
];
|
||||
|
||||
export function getMockProjects(category?: 'perenne' | 'event'): MockProject[] {
|
||||
if (category) {
|
||||
return mockProjects.filter(p => p.category === category);
|
||||
}
|
||||
return mockProjects;
|
||||
}
|
||||
|
||||
export function getMockFeaturedProjects(): MockProject[] {
|
||||
return mockProjects.filter(p => p.featured);
|
||||
}
|
||||
|
||||
export function getMockProjectBySlug(slug: string): MockProject | undefined {
|
||||
return mockProjects.find(p => p.slug.current === slug);
|
||||
}
|
||||
88
src/lib/sanity.ts
Normal file
88
src/lib/sanity.ts
Normal file
@ -0,0 +1,88 @@
|
||||
import { createClient } from '@sanity/client';
|
||||
import imageUrlBuilder from '@sanity/image-url';
|
||||
import type { SanityImageSource } from '@sanity/image-url/lib/types/types';
|
||||
|
||||
export const sanityClient = createClient({
|
||||
projectId: import.meta.env.PUBLIC_SANITY_PROJECT_ID || 'your-project-id',
|
||||
dataset: import.meta.env.PUBLIC_SANITY_DATASET || 'production',
|
||||
apiVersion: '2024-01-01',
|
||||
useCdn: true,
|
||||
});
|
||||
|
||||
const builder = imageUrlBuilder(sanityClient);
|
||||
|
||||
export function urlFor(source: SanityImageSource) {
|
||||
return builder.image(source);
|
||||
}
|
||||
|
||||
// --- Types ---
|
||||
export interface Project {
|
||||
_id: string;
|
||||
title: string;
|
||||
slug: { current: string };
|
||||
category: 'perenne' | 'event';
|
||||
client: string;
|
||||
date: string;
|
||||
location?: string;
|
||||
description: string;
|
||||
heroImage: SanityImageSource & { alt?: string };
|
||||
gallery: Array<SanityImageSource & {
|
||||
alt?: string;
|
||||
size?: 'small' | 'medium' | 'large';
|
||||
}>;
|
||||
tags?: string[];
|
||||
featured?: boolean;
|
||||
order?: number;
|
||||
}
|
||||
|
||||
export interface ProcessStep {
|
||||
_id: string;
|
||||
title: string;
|
||||
stepNumber: number;
|
||||
subtitle: string;
|
||||
description: any; // Block content
|
||||
icon?: SanityImageSource;
|
||||
gallery?: Array<SanityImageSource & { alt?: string; caption?: string }>;
|
||||
duration?: string;
|
||||
}
|
||||
|
||||
// --- Queries ---
|
||||
export async function getProjects(category?: 'perenne' | 'event'): Promise<Project[]> {
|
||||
const filter = category
|
||||
? `*[_type == "project" && category == "${category}"]`
|
||||
: `*[_type == "project"]`;
|
||||
|
||||
return sanityClient.fetch(`${filter} | order(order asc, date desc) {
|
||||
_id, title, slug, category, client, date, location, description,
|
||||
heroImage { ..., alt },
|
||||
gallery[] { ..., alt, size },
|
||||
tags, featured, order
|
||||
}`);
|
||||
}
|
||||
|
||||
export async function getProjectBySlug(slug: string): Promise<Project | null> {
|
||||
return sanityClient.fetch(
|
||||
`*[_type == "project" && slug.current == $slug][0] {
|
||||
_id, title, slug, category, client, date, location, description,
|
||||
heroImage { ..., alt },
|
||||
gallery[] { ..., alt, size },
|
||||
tags, featured, order
|
||||
}`,
|
||||
{ slug }
|
||||
);
|
||||
}
|
||||
|
||||
export async function getProcessSteps(): Promise<ProcessStep[]> {
|
||||
return sanityClient.fetch(`*[_type == "processStep"] | order(stepNumber asc) {
|
||||
_id, title, stepNumber, subtitle, description, icon, duration,
|
||||
gallery[] { ..., alt, caption }
|
||||
}`);
|
||||
}
|
||||
|
||||
export async function getFeaturedProjects(): Promise<Project[]> {
|
||||
return sanityClient.fetch(`*[_type == "project" && featured == true] | order(order asc) {
|
||||
_id, title, slug, category, client, date,
|
||||
heroImage { ..., alt },
|
||||
description, tags
|
||||
}`);
|
||||
}
|
||||
234
src/pages/creations-evenement/[slug].astro
Normal file
234
src/pages/creations-evenement/[slug].astro
Normal file
@ -0,0 +1,234 @@
|
||||
---
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import MasonryGallery from '../../components/MasonryGallery.tsx';
|
||||
import { getMockProjects, getMockProjectBySlug } from '../../lib/mock-data';
|
||||
|
||||
export function getStaticPaths() {
|
||||
const projects = getMockProjects('event');
|
||||
return projects.map(p => ({ params: { slug: p.slug.current } }));
|
||||
}
|
||||
|
||||
const { slug } = Astro.params;
|
||||
const project = getMockProjectBySlug(slug!);
|
||||
|
||||
if (!project) {
|
||||
return Astro.redirect('/creations-evenement');
|
||||
}
|
||||
|
||||
const dateFormatted = new Date(project.date).toLocaleDateString('fr-FR', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
});
|
||||
---
|
||||
|
||||
<BaseLayout title={`${project.title} — Aurélie Barré`} description={project.description}>
|
||||
<!-- Hero Image -->
|
||||
<section class="project-hero">
|
||||
<div class="project-hero__image">
|
||||
<img src={project.heroImage} alt={project.title} />
|
||||
<div class="project-hero__overlay"></div>
|
||||
</div>
|
||||
<div class="project-hero__content container">
|
||||
<span class="project-hero__client">{project.client}</span>
|
||||
<h1>{project.title}</h1>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Project Info -->
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<div class="project-meta reveal">
|
||||
<div class="project-meta__item">
|
||||
<span class="project-meta__label">Client</span>
|
||||
<span class="project-meta__value">{project.client}</span>
|
||||
</div>
|
||||
<div class="project-meta__item">
|
||||
<span class="project-meta__label">Date</span>
|
||||
<span class="project-meta__value">{dateFormatted}</span>
|
||||
</div>
|
||||
{project.location && (
|
||||
<div class="project-meta__item">
|
||||
<span class="project-meta__label">Lieu</span>
|
||||
<span class="project-meta__value">{project.location}</span>
|
||||
</div>
|
||||
)}
|
||||
<div class="project-meta__item">
|
||||
<span class="project-meta__label">Type</span>
|
||||
<span class="project-meta__value">Création événementielle</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="project-description reveal">
|
||||
<p>{project.description}</p>
|
||||
</div>
|
||||
|
||||
{project.tags && project.tags.length > 0 && (
|
||||
<div class="project-tags reveal">
|
||||
{project.tags.map(tag => (
|
||||
<span class="project-tag">{tag}</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Gallery (Masonry) -->
|
||||
{project.gallery && project.gallery.length > 0 && (
|
||||
<section class="section" style="background-color: var(--color-bg-alt);">
|
||||
<div class="container">
|
||||
<MasonryGallery
|
||||
client:visible
|
||||
images={project.gallery.map(img => ({
|
||||
src: img.src,
|
||||
alt: img.alt,
|
||||
size: img.size,
|
||||
}))}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
<!-- Back link -->
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<a href="/creations-evenement" class="back-link">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" style="margin-right: 8px;">
|
||||
<path d="M13 8H3M3 8L7 4M3 8L7 12" stroke="currentColor" stroke-width="1.2"/>
|
||||
</svg>
|
||||
Retour aux créations événement
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
</BaseLayout>
|
||||
|
||||
<style>
|
||||
/* Hero */
|
||||
.project-hero {
|
||||
position: relative;
|
||||
height: 70vh;
|
||||
min-height: 500px;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.project-hero__image {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
}
|
||||
|
||||
.project-hero__image img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.project-hero__overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(
|
||||
to top,
|
||||
rgba(0, 0, 0, 0.5) 0%,
|
||||
rgba(0, 0, 0, 0.1) 50%,
|
||||
rgba(0, 0, 0, 0) 100%
|
||||
);
|
||||
}
|
||||
|
||||
.project-hero__content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
padding-bottom: var(--space-2xl);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.project-hero__client {
|
||||
font-size: 0.75rem;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
opacity: 0.7;
|
||||
display: block;
|
||||
margin-bottom: var(--space-sm);
|
||||
}
|
||||
|
||||
.project-hero__content h1 {
|
||||
color: white;
|
||||
font-size: clamp(2rem, 5vw, 3.5rem);
|
||||
}
|
||||
|
||||
/* Meta */
|
||||
.project-meta {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: var(--space-lg);
|
||||
padding-bottom: var(--space-xl);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
margin-bottom: var(--space-xl);
|
||||
}
|
||||
|
||||
.project-meta__label {
|
||||
font-size: 0.7rem;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-text-light);
|
||||
display: block;
|
||||
margin-bottom: var(--space-xs);
|
||||
}
|
||||
|
||||
.project-meta__value {
|
||||
font-size: 0.95rem;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
/* Description */
|
||||
.project-description {
|
||||
margin-bottom: var(--space-xl);
|
||||
}
|
||||
|
||||
.project-description p {
|
||||
font-size: 1.15rem;
|
||||
line-height: 1.8;
|
||||
color: var(--color-text-secondary);
|
||||
max-width: 700px;
|
||||
}
|
||||
|
||||
/* Tags */
|
||||
.project-tags {
|
||||
display: flex;
|
||||
gap: var(--space-sm);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.project-tag {
|
||||
font-size: 0.75rem;
|
||||
letter-spacing: 0.04em;
|
||||
color: var(--color-text-light);
|
||||
padding: 4px 12px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
/* Back link */
|
||||
.back-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
font-size: 0.85rem;
|
||||
letter-spacing: 0.04em;
|
||||
color: var(--color-text-secondary);
|
||||
text-decoration: none;
|
||||
transition: color var(--duration-fast);
|
||||
}
|
||||
|
||||
.back-link:hover {
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.project-meta {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.project-hero {
|
||||
height: 50vh;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
81
src/pages/creations-evenement/index.astro
Normal file
81
src/pages/creations-evenement/index.astro
Normal file
@ -0,0 +1,81 @@
|
||||
---
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import ProjectCard from '../../components/ProjectCard.astro';
|
||||
import { getMockProjects } from '../../lib/mock-data';
|
||||
|
||||
const projects = getMockProjects('event');
|
||||
---
|
||||
|
||||
<BaseLayout title="Créations Événement — Aurélie Barré" description="Découvrez les créations événementielles d'Aurélie Barré : scénographies, pop-ups et événements pour les plus grandes maisons.">
|
||||
<section class="page-hero">
|
||||
<div class="container">
|
||||
<span class="page-hero__label">Portfolio</span>
|
||||
<h1>Créations<br /><em>Événement</em></h1>
|
||||
<p class="page-hero__desc text-secondary">
|
||||
Scénographies éphémères, pop-ups, lancements, conventions —
|
||||
chaque événement est une expérience unique conçue sur mesure.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<div class="projects-grid">
|
||||
{projects.map((project, index) => (
|
||||
<ProjectCard
|
||||
title={project.title}
|
||||
client={project.client}
|
||||
slug={project.slug.current}
|
||||
category={project.category}
|
||||
heroImage={project.heroImage}
|
||||
description={project.description}
|
||||
tags={project.tags}
|
||||
index={index}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</BaseLayout>
|
||||
|
||||
<style>
|
||||
.page-hero {
|
||||
padding-top: calc(var(--header-height) + var(--space-3xl));
|
||||
padding-bottom: var(--space-xl);
|
||||
}
|
||||
|
||||
.page-hero__label {
|
||||
font-size: 0.75rem;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-text-light);
|
||||
display: block;
|
||||
margin-bottom: var(--space-md);
|
||||
}
|
||||
|
||||
.page-hero h1 {
|
||||
margin-bottom: var(--space-lg);
|
||||
}
|
||||
|
||||
.page-hero h1 em {
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
.page-hero__desc {
|
||||
font-size: 1.1rem;
|
||||
line-height: 1.8;
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.projects-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: var(--space-xl) var(--space-lg);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.projects-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
249
src/pages/index.astro
Normal file
249
src/pages/index.astro
Normal file
@ -0,0 +1,249 @@
|
||||
---
|
||||
import BaseLayout from '../layouts/BaseLayout.astro';
|
||||
import ProjectCard from '../components/ProjectCard.astro';
|
||||
import { getMockFeaturedProjects } from '../lib/mock-data';
|
||||
|
||||
const featured = getMockFeaturedProjects();
|
||||
const events = featured.filter(p => p.category === 'event');
|
||||
const perennes = featured.filter(p => p.category === 'perenne');
|
||||
---
|
||||
|
||||
<BaseLayout>
|
||||
<!-- Hero Section -->
|
||||
<section class="hero">
|
||||
<div class="container">
|
||||
<div class="hero__content">
|
||||
<h1 class="hero__title">
|
||||
<span class="hero__title-line">Design</span>
|
||||
<span class="hero__title-line hero__title-accent">d'expérience</span>
|
||||
</h1>
|
||||
<p class="hero__subtitle">
|
||||
Aurélie Barré — Directrice artistique & designeuse d'intérieur.<br />
|
||||
Du concept à la réalisation, chaque espace raconte une histoire.
|
||||
</p>
|
||||
<div class="hero__cta">
|
||||
<a href="/processus" class="hero__link">
|
||||
Découvrir mon processus
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" style="margin-left: 8px;">
|
||||
<path d="M3 8H13M13 8L9 4M13 8L9 12" stroke="currentColor" stroke-width="1.2"/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Featured Events -->
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<div class="section-header reveal">
|
||||
<span class="section-label">Sélection</span>
|
||||
<h2>Créations Événement</h2>
|
||||
<a href="/creations-evenement" class="section-link">
|
||||
Voir tous les projets
|
||||
<svg width="14" height="14" viewBox="0 0 16 16" fill="none">
|
||||
<path d="M3 8H13M13 8L9 4M13 8L9 12" stroke="currentColor" stroke-width="1.2"/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="projects-grid">
|
||||
{events.slice(0, 4).map((project, index) => (
|
||||
<ProjectCard
|
||||
title={project.title}
|
||||
client={project.client}
|
||||
slug={project.slug.current}
|
||||
category={project.category}
|
||||
heroImage={project.heroImage}
|
||||
description={project.description}
|
||||
tags={project.tags}
|
||||
index={index}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Featured Pérennes -->
|
||||
{perennes.length > 0 && (
|
||||
<section class="section" style="background-color: var(--color-bg-alt);">
|
||||
<div class="container">
|
||||
<div class="section-header reveal">
|
||||
<span class="section-label">Sélection</span>
|
||||
<h2>Réalisations Pérennes</h2>
|
||||
<a href="/realisations-perennes" class="section-link">
|
||||
Voir tous les projets
|
||||
<svg width="14" height="14" viewBox="0 0 16 16" fill="none">
|
||||
<path d="M3 8H13M13 8L9 4M13 8L9 12" stroke="currentColor" stroke-width="1.2"/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="projects-grid">
|
||||
{perennes.map((project, index) => (
|
||||
<ProjectCard
|
||||
title={project.title}
|
||||
client={project.client}
|
||||
slug={project.slug.current}
|
||||
category={project.category}
|
||||
heroImage={project.heroImage}
|
||||
description={project.description}
|
||||
tags={project.tags}
|
||||
index={index}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
<!-- Process Teaser -->
|
||||
<section class="section process-teaser">
|
||||
<div class="container">
|
||||
<div class="process-teaser__inner reveal">
|
||||
<span class="section-label">Processus</span>
|
||||
<h2>Comment je travaille</h2>
|
||||
<p class="process-teaser__text text-secondary">
|
||||
De la prise de contact à la livraison, chaque projet suit un processus créatif rigoureux
|
||||
qui garantit un résultat à la hauteur de vos ambitions.
|
||||
</p>
|
||||
<div class="process-teaser__steps">
|
||||
<span class="process-teaser__step">01 — Prise de contact</span>
|
||||
<span class="process-teaser__step">02 — Conception & Moodboard</span>
|
||||
<span class="process-teaser__step">03 — Sélection & Sourcing</span>
|
||||
<span class="process-teaser__step">04 — Réalisation</span>
|
||||
<span class="process-teaser__step">05 — Livraison & Retour</span>
|
||||
</div>
|
||||
<a href="/processus" class="hero__link" style="margin-top: 2rem; display: inline-flex;">
|
||||
Découvrir en détail
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" style="margin-left: 8px;">
|
||||
<path d="M3 8H13M13 8L9 4M13 8L9 12" stroke="currentColor" stroke-width="1.2"/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</BaseLayout>
|
||||
|
||||
<style>
|
||||
/* Hero */
|
||||
.hero {
|
||||
min-height: 90vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-top: var(--header-height);
|
||||
}
|
||||
|
||||
.hero__content {
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
.hero__title {
|
||||
margin-bottom: var(--space-lg);
|
||||
}
|
||||
|
||||
.hero__title-line {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.hero__title-accent {
|
||||
font-style: italic;
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
.hero__subtitle {
|
||||
font-size: clamp(1rem, 2vw, 1.2rem);
|
||||
color: var(--color-text-secondary);
|
||||
line-height: 1.8;
|
||||
margin-bottom: var(--space-xl);
|
||||
}
|
||||
|
||||
.hero__link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
font-size: 0.85rem;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-accent);
|
||||
text-decoration: none;
|
||||
transition: color var(--duration-fast) var(--ease-out);
|
||||
}
|
||||
|
||||
.hero__link:hover {
|
||||
color: var(--color-accent-hover);
|
||||
}
|
||||
|
||||
/* Section Headers */
|
||||
.section-header {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: baseline;
|
||||
gap: var(--space-md);
|
||||
margin-bottom: var(--space-xl);
|
||||
}
|
||||
|
||||
.section-label {
|
||||
font-size: 0.75rem;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-text-light);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.section-header h2 {
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.section-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 0.85rem;
|
||||
letter-spacing: 0.04em;
|
||||
color: var(--color-accent);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.section-link:hover {
|
||||
color: var(--color-accent-hover);
|
||||
}
|
||||
|
||||
/* Projects Grid */
|
||||
.projects-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: var(--space-xl) var(--space-lg);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.projects-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* Process Teaser */
|
||||
.process-teaser__inner {
|
||||
max-width: 700px;
|
||||
}
|
||||
|
||||
.process-teaser__text {
|
||||
font-size: 1.1rem;
|
||||
line-height: 1.8;
|
||||
margin-top: var(--space-md);
|
||||
margin-bottom: var(--space-xl);
|
||||
}
|
||||
|
||||
.process-teaser__steps {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-md);
|
||||
}
|
||||
|
||||
.process-teaser__step {
|
||||
font-family: var(--font-serif);
|
||||
font-size: 1.1rem;
|
||||
color: var(--color-text-secondary);
|
||||
padding-bottom: var(--space-md);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
</style>
|
||||
117
src/pages/processus.astro
Normal file
117
src/pages/processus.astro
Normal file
@ -0,0 +1,117 @@
|
||||
---
|
||||
import BaseLayout from '../layouts/BaseLayout.astro';
|
||||
import ProcessTimeline from '../components/ProcessTimeline.tsx';
|
||||
import { mockProcessSteps } from '../lib/mock-data';
|
||||
---
|
||||
|
||||
<BaseLayout title="Processus Créatif — Aurélie Barré" description="Découvrez le processus créatif d'Aurélie Barré, de la prise de contact à la livraison de votre projet de design d'intérieur.">
|
||||
<section class="page-hero">
|
||||
<div class="container">
|
||||
<span class="page-hero__label">Méthode</span>
|
||||
<h1>Processus<br /><em>Créatif</em></h1>
|
||||
<p class="page-hero__desc text-secondary">
|
||||
Chaque projet suit un parcours rigoureux en 5 étapes,
|
||||
de la première rencontre à la livraison finale.
|
||||
Une méthode éprouvée pour garantir un résultat à la hauteur de vos attentes.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section process-section">
|
||||
<div class="container">
|
||||
<ProcessTimeline client:visible steps={mockProcessSteps} />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- CTA -->
|
||||
<section class="section cta-section">
|
||||
<div class="container">
|
||||
<div class="cta-inner reveal">
|
||||
<h2>Un projet en tête ?</h2>
|
||||
<p class="text-secondary">
|
||||
Discutons de vos envies autour d'un café. Chaque projet commence par une conversation.
|
||||
</p>
|
||||
<a href="mailto:contact@aureliebarre.fr" class="cta-link">
|
||||
Prendre contact
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" style="margin-left: 8px;">
|
||||
<path d="M3 8H13M13 8L9 4M13 8L9 12" stroke="currentColor" stroke-width="1.2"/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</BaseLayout>
|
||||
|
||||
<style>
|
||||
.page-hero {
|
||||
padding-top: calc(var(--header-height) + var(--space-3xl));
|
||||
padding-bottom: var(--space-xl);
|
||||
}
|
||||
|
||||
.page-hero__label {
|
||||
font-size: 0.75rem;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-text-light);
|
||||
display: block;
|
||||
margin-bottom: var(--space-md);
|
||||
}
|
||||
|
||||
.page-hero h1 {
|
||||
margin-bottom: var(--space-lg);
|
||||
}
|
||||
|
||||
.page-hero h1 em {
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
.page-hero__desc {
|
||||
font-size: 1.1rem;
|
||||
line-height: 1.8;
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.process-section {
|
||||
padding-top: var(--space-xl);
|
||||
padding-bottom: var(--space-3xl);
|
||||
}
|
||||
|
||||
/* CTA */
|
||||
.cta-section {
|
||||
background-color: var(--color-bg-alt);
|
||||
}
|
||||
|
||||
.cta-inner {
|
||||
text-align: center;
|
||||
max-width: 500px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.cta-inner h2 {
|
||||
margin-bottom: var(--space-md);
|
||||
}
|
||||
|
||||
.cta-inner p {
|
||||
margin: 0 auto var(--space-xl);
|
||||
font-size: 1.05rem;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.cta-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
font-size: 0.85rem;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-accent);
|
||||
text-decoration: none;
|
||||
padding: 12px 32px;
|
||||
border: 1px solid var(--color-accent);
|
||||
transition: all var(--duration-fast) var(--ease-out);
|
||||
}
|
||||
|
||||
.cta-link:hover {
|
||||
background-color: var(--color-accent);
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
206
src/pages/realisations-perennes/[slug].astro
Normal file
206
src/pages/realisations-perennes/[slug].astro
Normal file
@ -0,0 +1,206 @@
|
||||
---
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import MasonryGallery from '../../components/MasonryGallery.tsx';
|
||||
import { getMockProjects, getMockProjectBySlug } from '../../lib/mock-data';
|
||||
|
||||
export function getStaticPaths() {
|
||||
const projects = getMockProjects('perenne');
|
||||
return projects.map(p => ({ params: { slug: p.slug.current } }));
|
||||
}
|
||||
|
||||
const { slug } = Astro.params;
|
||||
const project = getMockProjectBySlug(slug!);
|
||||
|
||||
if (!project) {
|
||||
return Astro.redirect('/realisations-perennes');
|
||||
}
|
||||
|
||||
const dateFormatted = new Date(project.date).toLocaleDateString('fr-FR', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
});
|
||||
---
|
||||
|
||||
<BaseLayout title={`${project.title} — Aurélie Barré`} description={project.description}>
|
||||
<!-- Hero Image -->
|
||||
<section class="project-hero">
|
||||
<div class="project-hero__image">
|
||||
<img src={project.heroImage} alt={project.title} />
|
||||
<div class="project-hero__overlay"></div>
|
||||
</div>
|
||||
<div class="project-hero__content container">
|
||||
<span class="project-hero__client">{project.client}</span>
|
||||
<h1>{project.title}</h1>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Project Info -->
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<div class="project-meta reveal">
|
||||
<div class="project-meta__item">
|
||||
<span class="project-meta__label">Client</span>
|
||||
<span class="project-meta__value">{project.client}</span>
|
||||
</div>
|
||||
<div class="project-meta__item">
|
||||
<span class="project-meta__label">Date</span>
|
||||
<span class="project-meta__value">{dateFormatted}</span>
|
||||
</div>
|
||||
{project.location && (
|
||||
<div class="project-meta__item">
|
||||
<span class="project-meta__label">Lieu</span>
|
||||
<span class="project-meta__value">{project.location}</span>
|
||||
</div>
|
||||
)}
|
||||
<div class="project-meta__item">
|
||||
<span class="project-meta__label">Type</span>
|
||||
<span class="project-meta__value">Réalisation pérenne</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="project-description reveal">
|
||||
<p>{project.description}</p>
|
||||
</div>
|
||||
|
||||
{project.tags && project.tags.length > 0 && (
|
||||
<div class="project-tags reveal">
|
||||
{project.tags.map(tag => (
|
||||
<span class="project-tag">{tag}</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Gallery (Masonry) -->
|
||||
{project.gallery && project.gallery.length > 0 && (
|
||||
<section class="section" style="background-color: var(--color-bg-alt);">
|
||||
<div class="container">
|
||||
<MasonryGallery
|
||||
client:visible
|
||||
images={project.gallery.map(img => ({
|
||||
src: img.src,
|
||||
alt: img.alt,
|
||||
size: img.size,
|
||||
}))}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
<!-- Back link -->
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<a href="/realisations-perennes" class="back-link">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" style="margin-right: 8px;">
|
||||
<path d="M13 8H3M3 8L7 4M3 8L7 12" stroke="currentColor" stroke-width="1.2"/>
|
||||
</svg>
|
||||
Retour aux réalisations pérennes
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
</BaseLayout>
|
||||
|
||||
<style>
|
||||
.project-hero {
|
||||
position: relative;
|
||||
height: 70vh;
|
||||
min-height: 500px;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.project-hero__image {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
}
|
||||
|
||||
.project-hero__image img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.project-hero__overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(to top, rgba(0,0,0,0.5) 0%, rgba(0,0,0,0.1) 50%, transparent 100%);
|
||||
}
|
||||
|
||||
.project-hero__content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
padding-bottom: var(--space-2xl);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.project-hero__client {
|
||||
font-size: 0.75rem;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
opacity: 0.7;
|
||||
display: block;
|
||||
margin-bottom: var(--space-sm);
|
||||
}
|
||||
|
||||
.project-hero__content h1 {
|
||||
color: white;
|
||||
font-size: clamp(2rem, 5vw, 3.5rem);
|
||||
}
|
||||
|
||||
.project-meta {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: var(--space-lg);
|
||||
padding-bottom: var(--space-xl);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
margin-bottom: var(--space-xl);
|
||||
}
|
||||
|
||||
.project-meta__label {
|
||||
font-size: 0.7rem;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-text-light);
|
||||
display: block;
|
||||
margin-bottom: var(--space-xs);
|
||||
}
|
||||
|
||||
.project-meta__value {
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.project-description { margin-bottom: var(--space-xl); }
|
||||
.project-description p {
|
||||
font-size: 1.15rem;
|
||||
line-height: 1.8;
|
||||
color: var(--color-text-secondary);
|
||||
max-width: 700px;
|
||||
}
|
||||
|
||||
.project-tags { display: flex; gap: var(--space-sm); flex-wrap: wrap; }
|
||||
.project-tag {
|
||||
font-size: 0.75rem;
|
||||
letter-spacing: 0.04em;
|
||||
color: var(--color-text-light);
|
||||
padding: 4px 12px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.back-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
font-size: 0.85rem;
|
||||
color: var(--color-text-secondary);
|
||||
text-decoration: none;
|
||||
transition: color var(--duration-fast);
|
||||
}
|
||||
.back-link:hover { color: var(--color-accent); }
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.project-meta { grid-template-columns: repeat(2, 1fr); }
|
||||
.project-hero { height: 50vh; }
|
||||
}
|
||||
</style>
|
||||
81
src/pages/realisations-perennes/index.astro
Normal file
81
src/pages/realisations-perennes/index.astro
Normal file
@ -0,0 +1,81 @@
|
||||
---
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import ProjectCard from '../../components/ProjectCard.astro';
|
||||
import { getMockProjects } from '../../lib/mock-data';
|
||||
|
||||
const projects = getMockProjects('perenne');
|
||||
---
|
||||
|
||||
<BaseLayout title="Réalisations Pérennes — Aurélie Barré" description="Découvrez les réalisations pérennes d'Aurélie Barré : aménagements de bureaux, showrooms et espaces de vie.">
|
||||
<section class="page-hero">
|
||||
<div class="container">
|
||||
<span class="page-hero__label">Portfolio</span>
|
||||
<h1>Réalisations<br /><em>Pérennes</em></h1>
|
||||
<p class="page-hero__desc text-secondary">
|
||||
Bureaux, showrooms, espaces de vie — des aménagements durables
|
||||
pensés pour sublimer chaque lieu au quotidien.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<div class="projects-grid">
|
||||
{projects.map((project, index) => (
|
||||
<ProjectCard
|
||||
title={project.title}
|
||||
client={project.client}
|
||||
slug={project.slug.current}
|
||||
category={project.category}
|
||||
heroImage={project.heroImage}
|
||||
description={project.description}
|
||||
tags={project.tags}
|
||||
index={index}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</BaseLayout>
|
||||
|
||||
<style>
|
||||
.page-hero {
|
||||
padding-top: calc(var(--header-height) + var(--space-3xl));
|
||||
padding-bottom: var(--space-xl);
|
||||
}
|
||||
|
||||
.page-hero__label {
|
||||
font-size: 0.75rem;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-text-light);
|
||||
display: block;
|
||||
margin-bottom: var(--space-md);
|
||||
}
|
||||
|
||||
.page-hero h1 {
|
||||
margin-bottom: var(--space-lg);
|
||||
}
|
||||
|
||||
.page-hero h1 em {
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
.page-hero__desc {
|
||||
font-size: 1.1rem;
|
||||
line-height: 1.8;
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.projects-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: var(--space-xl) var(--space-lg);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.projects-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
229
src/styles/global.css
Normal file
229
src/styles/global.css
Normal file
@ -0,0 +1,229 @@
|
||||
/* ==========================================================================
|
||||
Aurélie Barré Portfolio — Global Styles
|
||||
Direction: "Galerie Blanche" — Éditorial Light
|
||||
========================================================================== */
|
||||
|
||||
/* --- CSS Custom Properties --- */
|
||||
:root {
|
||||
/* Palette */
|
||||
--color-bg: #FAFAF8;
|
||||
--color-bg-alt: #F3F1ED;
|
||||
--color-surface: #FFFFFF;
|
||||
--color-text: #1A1A1A;
|
||||
--color-text-secondary: #6B6B6B;
|
||||
--color-text-light: #9A9A9A;
|
||||
--color-accent: #C4A77D;
|
||||
--color-accent-hover: #B08E5E;
|
||||
--color-border: #E8E6E1;
|
||||
--color-border-light: #F0EEEA;
|
||||
|
||||
/* Typography */
|
||||
--font-serif: 'DM Serif Display', Georgia, 'Times New Roman', serif;
|
||||
--font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
|
||||
/* Spacing */
|
||||
--space-xs: 0.25rem;
|
||||
--space-sm: 0.5rem;
|
||||
--space-md: 1rem;
|
||||
--space-lg: 2rem;
|
||||
--space-xl: 4rem;
|
||||
--space-2xl: 6rem;
|
||||
--space-3xl: 8rem;
|
||||
|
||||
/* Layout */
|
||||
--container-max: 1440px;
|
||||
--container-padding: clamp(1.5rem, 4vw, 4rem);
|
||||
--header-height: 80px;
|
||||
|
||||
/* Transitions */
|
||||
--ease-out: cubic-bezier(0.16, 1, 0.3, 1);
|
||||
--ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);
|
||||
--duration-fast: 200ms;
|
||||
--duration-normal: 400ms;
|
||||
--duration-slow: 800ms;
|
||||
}
|
||||
|
||||
/* --- Reset & Base --- */
|
||||
*, *::before, *::after {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
html {
|
||||
font-size: 16px;
|
||||
scroll-behavior: smooth;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-sans);
|
||||
font-size: 1rem;
|
||||
line-height: 1.7;
|
||||
color: var(--color-text);
|
||||
background-color: var(--color-bg);
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* --- Typography --- */
|
||||
h1, h2, h3, h4 {
|
||||
font-family: var(--font-serif);
|
||||
font-weight: 400;
|
||||
line-height: 1.2;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: clamp(2.5rem, 6vw, 5rem);
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: clamp(1.8rem, 4vw, 3rem);
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: clamp(1.3rem, 2.5vw, 1.8rem);
|
||||
}
|
||||
|
||||
p {
|
||||
max-width: 65ch;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
transition: color var(--duration-fast) var(--ease-out);
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
img {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/* --- Utilities --- */
|
||||
.container {
|
||||
width: 100%;
|
||||
max-width: var(--container-max);
|
||||
margin: 0 auto;
|
||||
padding-left: var(--container-padding);
|
||||
padding-right: var(--container-padding);
|
||||
}
|
||||
|
||||
.section {
|
||||
padding-top: var(--space-2xl);
|
||||
padding-bottom: var(--space-2xl);
|
||||
}
|
||||
|
||||
.text-accent {
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
.text-secondary {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.text-light {
|
||||
color: var(--color-text-light);
|
||||
}
|
||||
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
/* --- Divider --- */
|
||||
.divider {
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background-color: var(--color-border);
|
||||
}
|
||||
|
||||
/* --- Reveal Animation (applied via JS) --- */
|
||||
.reveal {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
transition: opacity var(--duration-slow) var(--ease-out),
|
||||
transform var(--duration-slow) var(--ease-out);
|
||||
}
|
||||
|
||||
.reveal.visible {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* --- Masonry Grid --- */
|
||||
.masonry-grid {
|
||||
columns: 3;
|
||||
column-gap: var(--space-md);
|
||||
}
|
||||
|
||||
.masonry-grid > * {
|
||||
break-inside: avoid;
|
||||
margin-bottom: var(--space-md);
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.masonry-grid {
|
||||
columns: 2;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.masonry-grid {
|
||||
columns: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* --- Image Hover Effect --- */
|
||||
.img-hover-zoom {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.img-hover-zoom img {
|
||||
transition: transform var(--duration-slow) var(--ease-out);
|
||||
}
|
||||
|
||||
.img-hover-zoom:hover img {
|
||||
transform: scale(1.04);
|
||||
}
|
||||
|
||||
/* --- Page Transitions --- */
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(20px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.page-enter {
|
||||
animation: fadeIn var(--duration-slow) var(--ease-out) both;
|
||||
}
|
||||
|
||||
/* --- Scrollbar --- */
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: var(--color-bg);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--color-border);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--color-text-light);
|
||||
}
|
||||
14
tsconfig.json
Normal file
14
tsconfig.json
Normal file
@ -0,0 +1,14 @@
|
||||
{
|
||||
"extends": "astro/tsconfigs/strict",
|
||||
"compilerOptions": {
|
||||
"jsx": "react-jsx",
|
||||
"jsxImportSource": "react",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["src/*"],
|
||||
"@components/*": ["src/components/*"],
|
||||
"@layouts/*": ["src/layouts/*"],
|
||||
"@lib/*": ["src/lib/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user