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