refactor using astro

This commit is contained in:
ordinarthur 2026-02-27 18:14:08 +01:00
parent ad400fbd6e
commit 8be3338265
19 changed files with 6784 additions and 64 deletions

View File

@ -0,0 +1 @@
export default new Map();

View File

@ -0,0 +1 @@
export default new Map();

199
.astro/content.d.ts vendored Normal file
View File

@ -0,0 +1,199 @@
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;
};
}
}
declare module 'astro:content' {
type Flatten<T> = T extends { [K: string]: infer U } ? U : never;
export type CollectionKey = keyof AnyEntryMap;
export type CollectionEntry<C extends CollectionKey> = Flatten<AnyEntryMap[C]>;
export type ContentCollectionKey = keyof ContentEntryMap;
export type DataCollectionKey = keyof DataEntryMap;
type AllValuesOf<T> = T extends any ? T[keyof T] : never;
type ValidContentEntrySlug<C extends keyof ContentEntryMap> = AllValuesOf<
ContentEntryMap[C]
>['slug'];
export type ReferenceDataEntry<
C extends CollectionKey,
E extends keyof DataEntryMap[C] = string,
> = {
collection: C;
id: E;
};
export type ReferenceContentEntry<
C extends keyof ContentEntryMap,
E extends ValidContentEntrySlug<C> | (string & {}) = string,
> = {
collection: C;
slug: E;
};
export type ReferenceLiveEntry<C extends keyof LiveContentConfig['collections']> = {
collection: C;
id: string;
};
/** @deprecated Use `getEntry` instead. */
export function getEntryBySlug<
C extends keyof ContentEntryMap,
E extends ValidContentEntrySlug<C> | (string & {}),
>(
collection: C,
// Note that this has to accept a regular string too, for SSR
entrySlug: E,
): E extends ValidContentEntrySlug<C>
? Promise<CollectionEntry<C>>
: Promise<CollectionEntry<C> | undefined>;
/** @deprecated Use `getEntry` instead. */
export function getDataEntryById<C extends keyof DataEntryMap, E extends keyof DataEntryMap[C]>(
collection: C,
entryId: E,
): Promise<CollectionEntry<C>>;
export function getCollection<C extends keyof AnyEntryMap, E extends CollectionEntry<C>>(
collection: C,
filter?: (entry: CollectionEntry<C>) => entry is E,
): Promise<E[]>;
export function getCollection<C extends keyof AnyEntryMap>(
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 ContentEntryMap,
E extends ValidContentEntrySlug<C> | (string & {}),
>(
entry: ReferenceContentEntry<C, E>,
): E extends ValidContentEntrySlug<C>
? Promise<CollectionEntry<C>>
: Promise<CollectionEntry<C> | undefined>;
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 ContentEntryMap,
E extends ValidContentEntrySlug<C> | (string & {}),
>(
collection: C,
slug: E,
): E extends ValidContentEntrySlug<C>
? Promise<CollectionEntry<C>>
: 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 ContentEntryMap>(
entries: ReferenceContentEntry<C, ValidContentEntrySlug<C>>[],
): Promise<CollectionEntry<C>[]>;
export function getEntries<C extends keyof DataEntryMap>(
entries: ReferenceDataEntry<C, keyof DataEntryMap[C]>[],
): Promise<CollectionEntry<C>[]>;
export function render<C extends keyof AnyEntryMap>(
entry: AnyEntryMap[C][string],
): Promise<RenderResult>;
export function reference<C extends keyof AnyEntryMap>(
collection: C,
): import('astro/zod').ZodEffects<
import('astro/zod').ZodString,
C extends keyof ContentEntryMap
? ReferenceContentEntry<C, ValidContentEntrySlug<C>>
: ReferenceDataEntry<C, keyof DataEntryMap[C]>
>;
// 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.
export function reference<C extends string>(
collection: C,
): import('astro/zod').ZodEffects<import('astro/zod').ZodString, never>;
type ReturnTypeOrOriginal<T> = T extends (...args: any[]) => infer R ? R : T;
type InferEntrySchema<C extends keyof AnyEntryMap> = import('astro/zod').infer<
ReturnTypeOrOriginal<Required<ContentConfig['collections'][C]>['schema']>
>;
type ContentEntryMap = {
};
type DataEntryMap = {
};
type AnyEntryMap = ContentEntryMap & 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 ExtractDataType<T> = ExtractLoaderTypes<T>['data'];
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 = typeof import("../src/content.config.mjs");
export type LiveContentConfig = never;
}

1
.astro/data-store.json Normal file
View File

@ -0,0 +1 @@
[["Map",1,2],"meta::meta",["Map",3,4,5,6],"astro-version","5.18.0","astro-config-digest","{\"root\":{},\"srcDir\":{},\"publicDir\":{},\"outDir\":{},\"cacheDir\":{},\"compressHTML\":true,\"base\":\"/\",\"trailingSlash\":\"ignore\",\"output\":\"static\",\"scopedStyleStrategy\":\"attribute\",\"build\":{\"format\":\"directory\",\"client\":{},\"server\":{},\"assets\":\"_astro\",\"serverEntry\":\"entry.mjs\",\"redirects\":true,\"inlineStylesheets\":\"auto\",\"concurrency\":1},\"server\":{\"open\":false,\"host\":false,\"port\":4321,\"streaming\":true,\"allowedHosts\":[]},\"redirects\":{},\"image\":{\"endpoint\":{\"route\":\"/_image\"},\"service\":{\"entrypoint\":\"astro/assets/services/sharp\",\"config\":{}},\"domains\":[],\"remotePatterns\":[],\"responsiveStyles\":false},\"devToolbar\":{\"enabled\":true},\"markdown\":{\"syntaxHighlight\":{\"type\":\"shiki\",\"excludeLangs\":[\"math\"]},\"shikiConfig\":{\"langs\":[],\"langAlias\":{},\"theme\":\"github-dark\",\"themes\":{},\"wrap\":false,\"transformers\":[]},\"remarkPlugins\":[],\"rehypePlugins\":[],\"remarkRehype\":{},\"gfm\":true,\"smartypants\":true},\"security\":{\"checkOrigin\":true,\"allowedDomains\":[],\"actionBodySizeLimit\":1048576},\"env\":{\"schema\":{},\"validateSecrets\":false},\"experimental\":{\"clientPrerender\":false,\"contentIntellisense\":false,\"headingIdCompat\":false,\"preserveScriptOrder\":false,\"liveContentCollections\":false,\"csp\":false,\"staticImportMetaEnv\":false,\"chromeDevtoolsWorkspace\":false,\"failOnPrerenderConflict\":false,\"svgo\":false},\"legacy\":{\"collections\":false}}"]

5
.astro/settings.json Normal file
View File

@ -0,0 +1,5 @@
{
"_variables": {
"lastUpdateCheck": 1772211077674
}
}

2
.astro/types.d.ts vendored Normal file
View File

@ -0,0 +1,2 @@
/// <reference types="astro/client" />
/// <reference path="content.d.ts" />

15
astro.config.mjs Normal file
View File

@ -0,0 +1,15 @@
// @ts-check
import { defineConfig } from 'astro/config';
export default defineConfig({
output: 'static',
outDir: './dist',
server: { port: 4321 },
vite: {
server: {
proxy: {
'/api': 'http://127.0.0.1:8888',
},
},
},
});

View File

@ -2,7 +2,7 @@ server {
listen 80;
server_name rebours.studio;
root /var/www/rebours/public;
root /var/www/rebours/dist;
index index.html;
# HTML : jamais caché
@ -10,9 +10,9 @@ server {
add_header Cache-Control "no-store";
}
# CSS / JS : revalidation obligatoire à chaque requête
# CSS / JS avec hash Astro : cache long immutable
location ~* \.(css|js)$ {
add_header Cache-Control "no-cache";
add_header Cache-Control "public, max-age=31536000, immutable";
}
# Assets (images, fonts) : cache long
@ -20,12 +20,8 @@ server {
add_header Cache-Control "public, max-age=31536000, immutable";
}
location = /success {
try_files /success.html =404;
}
location / {
try_files $uri $uri/ /index.html;
try_files $uri $uri/ $uri.html /index.html;
}
location /api/ {

5719
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -3,12 +3,17 @@
"private": true,
"type": "module",
"scripts": {
"dev": "NODE_ENV=development node --watch server.mjs",
"start": "NODE_ENV=production node server.mjs"
"dev": "concurrently \"astro dev\" \"NODE_ENV=development node --watch server.mjs\"",
"build": "astro build",
"preview": "astro preview",
"server": "NODE_ENV=production node server.mjs",
"astro": "astro"
},
"dependencies": {
"@fastify/cors": "^10.0.2",
"@fastify/static": "^9.0.0",
"astro": "^5.17.1",
"concurrently": "^9.0.0",
"dotenv": "^17.3.1",
"fastify": "^5.3.2",
"stripe": "^20.3.1"

View File

@ -45,7 +45,7 @@
"@type": "Offer",
"itemOffered": {
"@type": "Product",
"name": "LUMIÈRE ORBITALE",
"name": "Solar Altar",
"description": "Lampe de table unique. Béton texturé coulé à la main + dôme céramique laqué.",
"image": "https://rebours.studio/assets/lamp-violet.jpg"
},
@ -193,7 +193,7 @@
<article class="product-card"
data-index="PROJET_001"
data-name="LUMIÈRE_ORBITALE"
data-name="Solar_Altar"
data-type="LAMPE DE TABLE"
data-mat="BÉTON TEXTURÉ + DÔME CÉRAMIQUE LAQUÉ"
data-year="2026"
@ -202,16 +202,16 @@
data-specs="H: 45cm / Ø: 18cm&#10;Poids: 3.2kg&#10;Alimentation: 220V — E27&#10;Câble: tressé rouge 2m"
data-notes="Inspiré des lampadaires soviétiques des années 60. Le béton est coulé à la main dans des moules uniques. La peinture acrylique est appliquée au spalter."
data-img="/assets/lamp-violet.jpg"
aria-label="Ouvrir le détail de LUMIÈRE ORBITALE">
aria-label="Ouvrir le détail de Solar Altar">
<div class="card-img-wrap">
<img src="/assets/lamp-violet.jpg"
alt="LUMIÈRE ORBITALE — Lampe béton violet, dôme céramique bleu, REBOURS 2026"
alt="Solar Altar — Lampe béton violet, dôme céramique bleu, REBOURS 2026"
width="600" height="600"
loading="lazy">
</div>
<div class="card-meta">
<span class="card-index">001</span>
<span class="card-name">LUMIÈRE_ORBITALE</span>
<span class="card-name">Solar_Altar</span>
<span class="card-arrow"></span>
</div>
</article>

View File

@ -160,7 +160,16 @@ document.addEventListener('DOMContentLoaded', () => {
}
});
function openPanel(card) {
// Slug à partir du nom du produit : "Solar_Altar" → "lumiere-orbitale"
function toSlug(name) {
return name
.toLowerCase()
.normalize('NFD').replace(/[\u0300-\u036f]/g, '')
.replace(/_/g, '-')
.replace(/[^a-z0-9-]/g, '');
}
function openPanel(card, pushState = true) {
fields.img.src = card.dataset.img;
fields.img.alt = card.dataset.name;
fields.index.textContent = card.dataset.index;
@ -192,23 +201,55 @@ document.addEventListener('DOMContentLoaded', () => {
// Refresh cursor sur les nouveaux éléments
attachCursorHover(panel.querySelectorAll('summary, .panel-close, .checkout-btn, .checkout-submit'));
// Mise à jour de l'URL
if (pushState) {
const slug = toSlug(card.dataset.name);
history.pushState({ slug }, '', `/collection/${slug}`);
}
}
function closePanel() {
function closePanel(pushState = true) {
panel.classList.remove('is-open');
panel.setAttribute('aria-hidden', 'true');
document.body.style.overflow = '';
if (pushState) {
history.pushState({}, '', '/');
}
}
cards.forEach(card => {
card.addEventListener('click', () => openPanel(card));
});
panelClose.addEventListener('click', closePanel);
// Ouverture automatique si on arrive directement sur /collection/[slug]
if (window.__OPEN_PANEL__) {
const name = window.__OPEN_PANEL__;
const card = [...cards].find(c => c.dataset.name === name);
if (card) openPanel(card, false);
}
panelClose.addEventListener('click', () => closePanel());
// Echap pour fermer
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') closePanel();
});
// Bouton retour navigateur
window.addEventListener('popstate', () => {
if (panel.classList.contains('is-open')) {
closePanel(false);
} else {
// Tente de rouvrir si on navigue vers un slug connu
const match = location.pathname.match(/^\/collection\/(.+)$/);
if (match) {
const slug = match[1];
const card = [...cards].find(c => toSlug(c.dataset.name) === slug);
if (card) openPanel(card, false);
}
}
});
});

View File

@ -133,7 +133,7 @@
<p class="label">// RÉCAPITULATIF</p>
<hr>
<div id="order-details" style="display:none; flex-direction:column; gap:0;">
<div class="info-row"><span class="info-key">PRODUIT</span><span>LUMIÈRE_ORBITALE</span></div>
<div class="info-row"><span class="info-key">PRODUIT</span><span>Solar_Altar</span></div>
<div class="info-row"><span class="info-key">COLLECTION</span><span>001 — ÉDITION UNIQUE</span></div>
<div class="info-row"><span class="info-key">MONTANT</span><span id="amount-display"></span></div>
<div class="info-row"><span class="info-key">EMAIL</span><span id="email-display"></span></div>

View File

@ -1,17 +1,11 @@
import Fastify from 'fastify'
import cors from '@fastify/cors'
import staticPlugin from '@fastify/static'
import Stripe from 'stripe'
import { readFileSync } from 'node:fs'
import { join, dirname } from 'node:path'
import { fileURLToPath } from 'node:url'
import dotenv from 'dotenv'
dotenv.config()
const __dirname = dirname(fileURLToPath(import.meta.url))
const isDev = process.env.NODE_ENV !== 'production'
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY ?? '')
const DOMAIN = process.env.DOMAIN ?? 'http://localhost:3000'
const DOMAIN = process.env.DOMAIN ?? 'http://localhost:4321'
const PRODUCTS = {
lumiere_orbitale: {
@ -22,15 +16,6 @@ const PRODUCTS = {
const app = Fastify({ logger: true })
await app.register(cors, { origin: '*', methods: ['GET', 'POST'] })
// ── Statique en dev uniquement (en prod c'est nginx qui sert public/) ─────────
if (isDev) {
await app.register(staticPlugin, {
root: join(__dirname, 'public'),
prefix: '/',
decorateReply: false,
})
}
// ── SEO ───────────────────────────────────────────────────────────────────────
app.get('/robots.txt', (_, reply) => {
reply
@ -125,7 +110,7 @@ app.post('/api/webhook', {
// ── Start ─────────────────────────────────────────────────────────────────────
try {
await app.listen({ port: process.env.PORT ?? 8888, host: '127.0.0.1' })
await app.listen({ port: process.env.PORT ?? 3000, host: '127.0.0.1' })
} catch (err) {
app.log.error(err)
process.exit(1)

58
src/layouts/Base.astro Normal file
View File

@ -0,0 +1,58 @@
---
export interface Props {
title: string;
description?: string;
ogImage?: string;
canonical?: string;
}
const {
title,
description = 'REBOUR Studio crée du mobilier d\'art contemporain inspiré du Space Age et du mouvement Memphis. Pièces uniques fabriquées à Paris. Collection 001 en cours.',
ogImage = 'https://rebours.studio/assets/lamp-violet.jpg',
canonical = 'https://rebour.studio/',
} = Astro.props;
---
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- SEO Primary -->
<title>{title}</title>
<meta name="description" content={description}>
<meta name="keywords" content="mobilier art, design contemporain, space age, memphis design, lampe béton, Paris, pièce unique">
<meta name="author" content="REBOURS Studio">
<meta name="robots" content="index, follow">
<link rel="canonical" href={canonical}>
<!-- Open Graph -->
<meta property="og:type" content="website">
<meta property="og:url" content={canonical}>
<meta property="og:title" content={title}>
<meta property="og:description" content={description}>
<meta property="og:image" content={ogImage}>
<meta property="og:locale" content="fr_FR">
<meta property="og:site_name" content="REBOURS Studio">
<!-- Twitter Card -->
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content={title}>
<meta name="twitter:description" content={description}>
<meta name="twitter:image" content={ogImage}>
<!-- Fonts -->
<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=Space+Mono:ital,wght@0,400;0,700;1,400&display=swap" rel="stylesheet">
<slot name="head" />
<link rel="stylesheet" href="/style.css">
</head>
<body>
<slot />
</body>
</html>

View File

@ -0,0 +1,273 @@
---
import Base from '../../layouts/Base.astro';
export function getStaticPaths() {
const PRODUCTS = [
{
slug: 'lumiere-orbitale',
name: 'Solar_Altar',
title: 'REBOURS — Solar Altar | Collection 001',
description: 'Lampe de table unique. Béton texturé coulé à la main + dôme céramique laqué. Pièce unique fabriquée à Paris.',
ogImage: 'https://rebours.studio/assets/lamp-violet.jpg',
},
{
slug: 'table-terrazzo',
name: 'TABLE_TERRAZZO',
title: 'REBOURS — TABLE TERRAZZO | Collection 001',
description: 'Table basse et étagère modulaire. Terrazzo fait main + acier tubulaire. Pièce unique fabriquée à Paris.',
ogImage: 'https://rebours.studio/assets/table-terrazzo.jpg',
},
{
slug: 'module-serie',
name: 'MODULE_SÉRIE',
title: 'REBOURS — MODULE SÉRIE | Collection 001',
description: 'Série de 7 lampes béton colorées, dôme laqué et néon. Édition limitée fabriquée à Paris.',
ogImage: 'https://rebours.studio/assets/lampes-serie.jpg',
},
];
return PRODUCTS.map(p => ({ params: { slug: p.slug }, props: p }));
}
const { slug, title, description, ogImage, name } = Astro.props;
---
<Base
title={title}
description={description}
ogImage={ogImage}
canonical={`https://rebour.studio/collection/${slug}`}
>
<!-- On charge index.html entier et on ouvre le panel via JS au load -->
<meta name="x-open-panel" content={name} />
<!-- Même contenu que index.astro — redirige vers / avec panel ouvert -->
<script is:inline>
// Avant le DOM : on note quel panel ouvrir
window.__OPEN_PANEL__ = document.querySelector('meta[name="x-open-panel"]')?.content;
</script>
<!-- Grid background -->
<div id="interactive-grid" class="interactive-grid"></div>
<!-- PRODUCT PANEL (overlay) -->
<div id="product-panel" class="product-panel" aria-hidden="true">
<div class="panel-close" id="panel-close">
<span>← RETOUR</span>
</div>
<div class="panel-inner">
<div class="panel-img-col">
<img id="panel-img" src="" alt="">
</div>
<div class="panel-info-col">
<p class="panel-index" id="panel-index"></p>
<h2 id="panel-name"></h2>
<hr>
<div class="panel-meta">
<div class="panel-meta-row">
<span class="meta-key">TYPE</span>
<span id="panel-type"></span>
</div>
<div class="panel-meta-row">
<span class="meta-key">MATÉRIAUX</span>
<span id="panel-mat"></span>
</div>
<div class="panel-meta-row">
<span class="meta-key">ANNÉE</span>
<span id="panel-year"></span>
</div>
<div class="panel-meta-row">
<span class="meta-key">STATUS</span>
<span id="panel-status" class="red"></span>
</div>
</div>
<hr>
<p id="panel-desc" class="panel-desc"></p>
<hr>
<details class="accordion">
<summary>SPÉCIFICATIONS TECHNIQUES <span>↓</span></summary>
<div class="accordion-body" id="panel-specs"></div>
</details>
<details class="accordion">
<summary>NOTES DE CONCEPTION <span>↓</span></summary>
<div class="accordion-body" id="panel-notes"></div>
</details>
<hr>
<div id="checkout-section" style="display:none;">
<div class="checkout-price-line">
<span class="checkout-price">1 800 €</span>
<span class="checkout-edition">ÉDITION UNIQUE — 1/1</span>
</div>
<button id="checkout-toggle-btn" class="checkout-btn">
[ COMMANDER CETTE PIÈCE ]
</button>
<div id="checkout-form-wrap" class="checkout-form-wrap" style="display:none;">
<form id="checkout-form" class="checkout-form">
<div class="checkout-form-field">
<label for="checkout-email">EMAIL *</label>
<input type="email" id="checkout-email" name="email" placeholder="votre@email.com" required autocomplete="off">
</div>
<div class="checkout-form-note">
Pièce fabriquée à Paris. Délai : 6 à 8 semaines.<br>
Paiement sécurisé via Stripe.
</div>
<button type="submit" class="checkout-submit" id="checkout-submit-btn">
PROCÉDER AU PAIEMENT →
</button>
</form>
</div>
</div>
<div class="panel-footer">
<span class="blink">■</span> COLLECTION_001 — W.I.P
</div>
</div>
</div>
</div>
<div class="page-wrapper">
<header class="header">
<a href="/" class="logo-text" aria-label="REBOURS — Accueil">REBOURS</a>
<nav class="header-nav" aria-label="Navigation principale">
<a href="/#collection">COLLECTION_001</a>
<a href="/#contact">CONTACT</a>
<span class="wip-tag"><span class="blink">■</span> W.I.P</span>
</nav>
</header>
<main>
<section class="hero" aria-label="Introduction">
<div class="hero-left">
<p class="label">// ARCHIVE_001 — 2026</p>
<h1>REBOURS<br>STUDIO</h1>
<p class="hero-sub">Mobilier d'art contemporain.<br>Space Age × Memphis.</p>
<p class="hero-sub mono-sm">STATUS: [PROTOTYPE EN COURS]<br>COLLECTION_001 — BIENTÔT DISPONIBLE</p>
</div>
<div class="hero-right">
<img
src="/assets/table-terrazzo.jpg"
alt="REBOURS — Table Terrazzo, plateau terrazzo et acier tubulaire, Paris 2026"
class="hero-img"
width="1024" height="1024"
fetchpriority="high">
</div>
</section>
<hr>
<section class="collection" id="collection" aria-label="Collection 001">
<div class="collection-header">
<p class="label">// COLLECTION_001</p>
<span class="label">3 OBJETS — CLIQUER POUR OUVRIR</span>
</div>
<div class="product-grid">
<article class="product-card"
data-index="PROJET_001"
data-name="Solar_Altar"
data-type="LAMPE DE TABLE"
data-mat="BÉTON TEXTURÉ + DÔME CÉRAMIQUE LAQUÉ"
data-year="2026"
data-status="PROTOTYPE [80%]"
data-desc="Exploration de la lumière à travers des contraintes géométriques. Le dôme sphérique en céramique laquée coiffe un corps en béton texturé peint à la main. Chaque pièce est unique."
data-specs="H: 45cm / Ø: 18cm&#10;Poids: 3.2kg&#10;Alimentation: 220V — E27&#10;Câble: tressé rouge 2m"
data-notes="Inspiré des lampadaires soviétiques des années 60. Le béton est coulé à la main dans des moules uniques. La peinture acrylique est appliquée au spalter."
data-img="/assets/lamp-violet.jpg"
aria-label="Ouvrir le détail de Solar Altar">
<div class="card-img-wrap">
<img src="/assets/lamp-violet.jpg" alt="Solar Altar — Lampe béton violet, dôme céramique bleu, REBOURS 2026" width="600" height="600" loading="lazy">
</div>
<div class="card-meta">
<span class="card-index">001</span>
<span class="card-name">Solar_Altar</span>
<span class="card-arrow">↗</span>
</div>
</article>
<article class="product-card"
data-index="PROJET_002"
data-name="TABLE_TERRAZZO"
data-type="TABLE BASSE + ÉTAGÈRE MODULAIRE"
data-mat="TERRAZZO + ACIER TUBULAIRE + RÉSINE"
data-year="2026"
data-status="STRUCTURAL_TEST"
data-desc="Collision du brutalisme et de la couleur Memphis. Le plateau en terrazzo fait à la main intègre des inclusions de marbre rose et bleu. Les colonnes cylindriques bicolores sont en acier peint au four."
data-specs="Table: L120 × P60 × H38cm&#10;Poids plateau: 28kg&#10;Pieds: acier Ø60mm&#10;Étagère: H180 × L80 × P35cm"
data-notes="Le terrazzo est réalisé dans l'atelier de Pantin. Chaque dalle est unique. L'étagère est assemblée à partir de tubes industriels récupérés et de panneaux laqués."
data-img="/assets/table-terrazzo.jpg"
aria-label="Ouvrir le détail de TABLE TERRAZZO">
<div class="card-img-wrap">
<img src="/assets/table-terrazzo.jpg" alt="TABLE TERRAZZO — Table basse terrazzo et étagère acier, REBOUR 2026" width="600" height="600" loading="lazy">
</div>
<div class="card-meta">
<span class="card-index">002</span>
<span class="card-name">TABLE_TERRAZZO</span>
<span class="card-arrow">↗</span>
</div>
</article>
<article class="product-card"
data-index="PROJET_003"
data-name="MODULE_SÉRIE"
data-type="LAMPES — SÉRIE LIMITÉE"
data-mat="BÉTON COLORÉ + DÔME LAQUÉ + NÉON"
data-year="2026"
data-status="FINAL_ASSEMBLY"
data-desc="Série de 7 lampes aux corps béton colorés, chacune avec un dôme d'une couleur différente. Les néons horizontaux créent un anneau lumineux entre le dôme et le corps."
data-specs="H: 3565cm (7 tailles)&#10;Dôme: Ø1528cm&#10;Anneau néon: 8W — 3000K&#10;Édition: 7 ex. par coloris"
data-notes="Les corps sont coulés en série mais peints individuellement. Les dômes sont réalisés par un souffleur de verre artisanal. Le câble tressé rouge est la signature de la série."
data-img="/assets/lampes-serie.jpg"
aria-label="Ouvrir le détail de MODULE SÉRIE">
<div class="card-img-wrap">
<img src="/assets/lampes-serie.jpg" alt="MODULE SÉRIE — Collection de 7 lampes béton colorées, REBOUR 2026" width="600" height="600" loading="lazy">
</div>
<div class="card-meta">
<span class="card-index">003</span>
<span class="card-name">MODULE_SÉRIE</span>
<span class="card-arrow">↗</span>
</div>
</article>
</div>
</section>
<section class="newsletter" id="contact" aria-label="Accès anticipé">
<div class="nl-left">
<p class="label">// ACCÈS_ANTICIPÉ</p>
<h2>REJOINDRE<br>L'EXPÉRIENCE</h2>
</div>
<div class="nl-right">
<form class="nl-form" onsubmit="event.preventDefault();" aria-label="Inscription newsletter">
<label for="nl-email">EMAIL :</label>
<div class="nl-row">
<input type="email" id="nl-email" name="email" placeholder="votre@email.com" autocomplete="email" required>
<button type="submit">ENVOYER →</button>
</div>
<p class="mono-sm"><span class="blink">■</span> CONNECTION_STATUS: PENDING</p>
</form>
</div>
</section>
</main>
<footer class="footer">
<span>© 2026 REBOURS STUDIO — PARIS</span>
<nav aria-label="Liens secondaires">
<a href="https://instagram.com/rebour.studio" rel="noopener" target="_blank">INSTAGRAM</a>
&nbsp;/&nbsp;
<a href="mailto:contact@rebour.studio">CONTACT</a>
</nav>
</footer>
</div>
<div class="cursor-dot"></div>
<div class="cursor-outline"></div>
<script src="/main.js" is:inline></script>
</Base>

276
src/pages/index.astro Normal file
View File

@ -0,0 +1,276 @@
---
import Base from '../layouts/Base.astro';
const schemaOrg = {
"@context": "https://schema.org",
"@type": "Store",
"name": "REBOURS Studio",
"description": "Mobilier d'art contemporain. Space Age × Memphis. Pièces uniques fabriquées à Paris.",
"url": "https://rebours.studio",
"image": "https://rebours.studio/assets/lamp-violet.jpg",
"address": { "@type": "PostalAddress", "addressLocality": "Paris", "addressCountry": "FR" },
"hasOfferCatalog": {
"@type": "OfferCatalog",
"name": "Collection 001",
"itemListElement": [{
"@type": "Offer",
"itemOffered": {
"@type": "Product",
"name": "Solar Altar",
"description": "Lampe de table unique. Béton texturé coulé à la main + dôme céramique laqué.",
"image": "https://rebours.studio/assets/lamp-violet.jpg"
},
"price": "1800",
"priceCurrency": "EUR",
"availability": "https://schema.org/LimitedAvailability"
}]
}
};
---
<Base
title="REBOURS — Mobilier d'art contemporain | Collection 001"
description="REBOUR Studio crée du mobilier d'art contemporain inspiré du Space Age et du mouvement Memphis. Pièces uniques fabriquées à Paris. Collection 001 en cours."
canonical="https://rebour.studio/"
>
<Fragment slot="head">
<script type="application/ld+json" set:html={JSON.stringify(schemaOrg)} />
</Fragment>
<!-- Grid background -->
<div id="interactive-grid" class="interactive-grid"></div>
<!-- PRODUCT PANEL (overlay) -->
<div id="product-panel" class="product-panel" aria-hidden="true">
<div class="panel-close" id="panel-close">
<span>← RETOUR</span>
</div>
<div class="panel-inner">
<div class="panel-img-col">
<img id="panel-img" src="" alt="">
</div>
<div class="panel-info-col">
<p class="panel-index" id="panel-index"></p>
<h2 id="panel-name"></h2>
<hr>
<div class="panel-meta">
<div class="panel-meta-row">
<span class="meta-key">TYPE</span>
<span id="panel-type"></span>
</div>
<div class="panel-meta-row">
<span class="meta-key">MATÉRIAUX</span>
<span id="panel-mat"></span>
</div>
<div class="panel-meta-row">
<span class="meta-key">ANNÉE</span>
<span id="panel-year"></span>
</div>
<div class="panel-meta-row">
<span class="meta-key">STATUS</span>
<span id="panel-status" class="red"></span>
</div>
</div>
<hr>
<p id="panel-desc" class="panel-desc"></p>
<hr>
<details class="accordion">
<summary>SPÉCIFICATIONS TECHNIQUES <span>↓</span></summary>
<div class="accordion-body" id="panel-specs"></div>
</details>
<details class="accordion">
<summary>NOTES DE CONCEPTION <span>↓</span></summary>
<div class="accordion-body" id="panel-notes"></div>
</details>
<hr>
<div id="checkout-section" style="display:none;">
<div class="checkout-price-line">
<span class="checkout-price">1 800 €</span>
<span class="checkout-edition">ÉDITION UNIQUE — 1/1</span>
</div>
<button id="checkout-toggle-btn" class="checkout-btn">
[ COMMANDER CETTE PIÈCE ]
</button>
<div id="checkout-form-wrap" class="checkout-form-wrap" style="display:none;">
<form id="checkout-form" class="checkout-form">
<div class="checkout-form-field">
<label for="checkout-email">EMAIL *</label>
<input type="email" id="checkout-email" name="email" placeholder="votre@email.com" required autocomplete="off">
</div>
<div class="checkout-form-note">
Pièce fabriquée à Paris. Délai : 6 à 8 semaines.<br>
Paiement sécurisé via Stripe.
</div>
<button type="submit" class="checkout-submit" id="checkout-submit-btn">
PROCÉDER AU PAIEMENT →
</button>
</form>
</div>
</div>
<div class="panel-footer">
<span class="blink">■</span> COLLECTION_001 — W.I.P
</div>
</div>
</div>
</div>
<div class="page-wrapper">
<header class="header">
<a href="/" class="logo-text" aria-label="REBOURS — Accueil">REBOURS</a>
<nav class="header-nav" aria-label="Navigation principale">
<a href="#collection">COLLECTION_001</a>
<a href="#contact">CONTACT</a>
<span class="wip-tag"><span class="blink">■</span> W.I.P</span>
</nav>
</header>
<main>
<!-- HERO -->
<section class="hero" aria-label="Introduction">
<div class="hero-left">
<p class="label">// ARCHIVE_001 — 2026</p>
<h1>REBOURS<br>STUDIO</h1>
<p class="hero-sub">Mobilier d'art contemporain.<br>Space Age × Memphis.</p>
<p class="hero-sub mono-sm">STATUS: [PROTOTYPE EN COURS]<br>COLLECTION_001 — BIENTÔT DISPONIBLE</p>
</div>
<div class="hero-right">
<img
src="/assets/table-terrazzo.jpg"
alt="REBOURS — Table Terrazzo, plateau terrazzo et acier tubulaire, Paris 2026"
class="hero-img"
width="1024" height="1024"
fetchpriority="high">
</div>
</section>
<hr>
<!-- COLLECTION GRID -->
<section class="collection" id="collection" aria-label="Collection 001">
<div class="collection-header">
<p class="label">// COLLECTION_001</p>
<span class="label">3 OBJETS — CLIQUER POUR OUVRIR</span>
</div>
<div class="product-grid">
<article class="product-card"
data-index="PROJET_001"
data-name="Solar_Altar"
data-type="LAMPE DE TABLE"
data-mat="BÉTON TEXTURÉ + DÔME CÉRAMIQUE LAQUÉ"
data-year="2026"
data-status="PROTOTYPE [80%]"
data-desc="Exploration de la lumière à travers des contraintes géométriques. Le dôme sphérique en céramique laquée coiffe un corps en béton texturé peint à la main. Chaque pièce est unique."
data-specs="H: 45cm / Ø: 18cm&#10;Poids: 3.2kg&#10;Alimentation: 220V — E27&#10;Câble: tressé rouge 2m"
data-notes="Inspiré des lampadaires soviétiques des années 60. Le béton est coulé à la main dans des moules uniques. La peinture acrylique est appliquée au spalter."
data-img="/assets/lamp-violet.jpg"
aria-label="Ouvrir le détail de Solar Altar">
<div class="card-img-wrap">
<img src="/assets/lamp-violet.jpg"
alt="Solar Altar — Lampe béton violet, dôme céramique bleu, REBOURS 2026"
width="600" height="600"
loading="lazy">
</div>
<div class="card-meta">
<span class="card-index">001</span>
<span class="card-name">Solar_Altar</span>
<span class="card-arrow">↗</span>
</div>
</article>
<article class="product-card"
data-index="PROJET_002"
data-name="TABLE_TERRAZZO"
data-type="TABLE BASSE + ÉTAGÈRE MODULAIRE"
data-mat="TERRAZZO + ACIER TUBULAIRE + RÉSINE"
data-year="2026"
data-status="STRUCTURAL_TEST"
data-desc="Collision du brutalisme et de la couleur Memphis. Le plateau en terrazzo fait à la main intègre des inclusions de marbre rose et bleu. Les colonnes cylindriques bicolores sont en acier peint au four."
data-specs="Table: L120 × P60 × H38cm&#10;Poids plateau: 28kg&#10;Pieds: acier Ø60mm&#10;Étagère: H180 × L80 × P35cm"
data-notes="Le terrazzo est réalisé dans l'atelier de Pantin. Chaque dalle est unique. L'étagère est assemblée à partir de tubes industriels récupérés et de panneaux laqués."
data-img="/assets/table-terrazzo.jpg"
aria-label="Ouvrir le détail de TABLE TERRAZZO">
<div class="card-img-wrap">
<img src="/assets/table-terrazzo.jpg"
alt="TABLE TERRAZZO — Table basse terrazzo et étagère acier, REBOUR 2026"
width="600" height="600"
loading="lazy">
</div>
<div class="card-meta">
<span class="card-index">002</span>
<span class="card-name">TABLE_TERRAZZO</span>
<span class="card-arrow">↗</span>
</div>
</article>
<article class="product-card"
data-index="PROJET_003"
data-name="MODULE_SÉRIE"
data-type="LAMPES — SÉRIE LIMITÉE"
data-mat="BÉTON COLORÉ + DÔME LAQUÉ + NÉON"
data-year="2026"
data-status="FINAL_ASSEMBLY"
data-desc="Série de 7 lampes aux corps béton colorés, chacune avec un dôme d'une couleur différente. Les néons horizontaux créent un anneau lumineux entre le dôme et le corps."
data-specs="H: 3565cm (7 tailles)&#10;Dôme: Ø1528cm&#10;Anneau néon: 8W — 3000K&#10;Édition: 7 ex. par coloris"
data-notes="Les corps sont coulés en série mais peints individuellement. Les dômes sont réalisés par un souffleur de verre artisanal. Le câble tressé rouge est la signature de la série."
data-img="/assets/lampes-serie.jpg"
aria-label="Ouvrir le détail de MODULE SÉRIE">
<div class="card-img-wrap">
<img src="/assets/lampes-serie.jpg"
alt="MODULE SÉRIE — Collection de 7 lampes béton colorées, REBOUR 2026"
width="600" height="600"
loading="lazy">
</div>
<div class="card-meta">
<span class="card-index">003</span>
<span class="card-name">MODULE_SÉRIE</span>
<span class="card-arrow">↗</span>
</div>
</article>
</div>
</section>
<!-- NEWSLETTER -->
<section class="newsletter" id="contact" aria-label="Accès anticipé">
<div class="nl-left">
<p class="label">// ACCÈS_ANTICIPÉ</p>
<h2>REJOINDRE<br>L'EXPÉRIENCE</h2>
</div>
<div class="nl-right">
<form class="nl-form" onsubmit="event.preventDefault();" aria-label="Inscription newsletter">
<label for="nl-email">EMAIL :</label>
<div class="nl-row">
<input type="email" id="nl-email" name="email" placeholder="votre@email.com" autocomplete="email" required>
<button type="submit">ENVOYER →</button>
</div>
<p class="mono-sm"><span class="blink">■</span> CONNECTION_STATUS: PENDING</p>
</form>
</div>
</section>
</main>
<footer class="footer">
<span>© 2026 REBOURS STUDIO — PARIS</span>
<nav aria-label="Liens secondaires">
<a href="https://instagram.com/rebour.studio" rel="noopener" target="_blank">INSTAGRAM</a>
&nbsp;/&nbsp;
<a href="mailto:contact@rebour.studio">CONTACT</a>
</nav>
</footer>
</div>
<div class="cursor-dot"></div>
<div class="cursor-outline"></div>
<script src="/main.js" is:inline></script>
</Base>

167
src/pages/success.astro Normal file
View File

@ -0,0 +1,167 @@
---
import Base from '../layouts/Base.astro';
---
<Base
title="REBOUR — COMMANDE CONFIRMÉE"
description="Votre commande REBOURS Studio a été confirmée."
canonical="https://rebour.studio/success"
>
<style>
main {
flex-grow: 1;
display: grid;
grid-template-columns: 1fr 1fr;
}
.left {
border-right: var(--border);
padding: 5rem 2rem;
display: flex;
flex-direction: column;
justify-content: flex-end;
gap: 2rem;
position: relative;
overflow: hidden;
}
.product-img {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: cover;
opacity: 0.55;
}
.slabel { font-size: 0.75rem; color: #888; }
h1 {
font-size: clamp(2.5rem, 5vw, 4.5rem);
font-weight: 700;
line-height: 0.95;
letter-spacing: -0.02em;
}
.status-line {
font-size: 0.82rem;
line-height: 1.8;
color: #555;
}
.amount {
display: inline-block;
background: var(--clr-red);
padding: 0.3rem 0.7rem;
font-weight: 700;
font-size: 1rem;
}
.right {
padding: 5rem 2rem;
display: flex;
flex-direction: column;
justify-content: flex-end;
gap: 1.5rem;
}
.info-row {
display: flex;
gap: 1.5rem;
padding: 0.6rem 0;
border-bottom: 1px solid rgba(0,0,0,0.1);
font-size: 0.8rem;
align-items: baseline;
}
.info-key { color: #888; width: 8rem; flex-shrink: 0; font-size: 0.72rem; }
a.back {
display: inline-block;
border: var(--border);
padding: 0.9rem 1.5rem;
font-family: var(--font-mono);
font-size: 0.78rem;
font-weight: 700;
text-decoration: none;
color: var(--clr-black);
transition: background 0.15s;
align-self: flex-start;
margin-top: 1rem;
}
a.back:hover { background: var(--clr-black); color: var(--clr-white); }
#loading { color: #888; font-size: 0.78rem; }
</style>
<div style="display:flex; flex-direction:column; min-height:100vh;">
<header class="header">
<a href="/" class="logo-text">REBOURS</a>
<span style="font-size:0.78rem;color:#888">COLLECTION_001</span>
</header>
<main>
<div class="left">
<img id="product-img" class="product-img" src="/assets/lamp-violet.jpg" alt="">
<p class="slabel" style="position:relative">// COMMANDE_CONFIRMÉE</p>
<h1 style="position:relative">MERCI<br>POUR<br>VOTRE<br>COMMANDE</h1>
<p class="status-line" id="loading" style="position:relative">Vérification du paiement...</p>
</div>
<div class="right">
<p class="slabel">// RÉCAPITULATIF</p>
<hr>
<div id="order-details" style="display:none; flex-direction:column; gap:0;">
<div class="info-row"><span class="info-key">PRODUIT</span><span>Solar_Altar</span></div>
<div class="info-row"><span class="info-key">COLLECTION</span><span>001 — ÉDITION UNIQUE</span></div>
<div class="info-row"><span class="info-key">MONTANT</span><span id="amount-display"></span></div>
<div class="info-row"><span class="info-key">EMAIL</span><span id="email-display"></span></div>
<div class="info-row"><span class="info-key">DÉLAI</span><span>6 À 8 SEMAINES</span></div>
<div class="info-row"><span class="info-key">STATUS</span><span style="color:var(--clr-red); font-weight:700">CONFIRMÉ ■</span></div>
</div>
<p style="font-size:0.78rem; line-height:1.8; color:#555; margin-top:1rem;">
Un email de confirmation vous sera envoyé.<br>
Votre lampe est fabriquée à la main à Paris.
</p>
<a href="/" class="back">← RETOUR À LA COLLECTION</a>
</div>
</main>
<footer class="footer">
<span>© 2026 REBOURS STUDIO — PARIS</span>
<nav aria-label="Liens secondaires">
<a href="https://instagram.com/rebour.studio" rel="noopener" target="_blank">INSTAGRAM</a>
&nbsp;/&nbsp;
<a href="mailto:contact@rebour.studio">CONTACT</a>
</nav>
</footer>
</div>
<script>
const params = new URLSearchParams(window.location.search);
const sessionId = params.get('session_id');
const PRODUCT_IMAGES: Record<string, string> = {
lumiere_orbitale: '/assets/lamp-violet.jpg',
table_terrazzo: '/assets/table-terrazzo.jpg',
module_serie: '/assets/lampes-serie.jpg',
};
if (sessionId) {
fetch(`/api/session/${sessionId}`)
.then(r => r.json())
.then((data: { amount?: number; customer_email?: string; product?: string }) => {
const loading = document.getElementById('loading');
const orderDetails = document.getElementById('order-details');
if (loading) loading.style.display = 'none';
if (orderDetails) orderDetails.style.display = 'flex';
const amount = data.amount ? `${(data.amount / 100).toLocaleString('fr-FR')} €` : '—';
const amountEl = document.getElementById('amount-display');
const emailEl = document.getElementById('email-display');
if (amountEl) amountEl.textContent = amount;
if (emailEl) emailEl.textContent = data.customer_email ?? '—';
if (data.product && PRODUCT_IMAGES[data.product]) {
const img = document.getElementById('product-img') as HTMLImageElement;
if (img) img.src = PRODUCT_IMAGES[data.product]!;
}
})
.catch(() => {
const loading = document.getElementById('loading');
if (loading) loading.textContent = 'Commande enregistrée.';
});
} else {
const loading = document.getElementById('loading');
if (loading) loading.textContent = 'Commande enregistrée.';
}
</script>
</Base>

View File

@ -1,29 +1,5 @@
{
"compilerOptions": {
// Environment setup & latest features
"lib": ["ESNext"],
"target": "ESNext",
"module": "Preserve",
"moduleDetection": "force",
"jsx": "react-jsx",
"allowJs": true,
// Bundler mode
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"noEmit": true,
// Best practices
"strict": true,
"skipLibCheck": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
// Some stricter flags (disabled by default)
"noUnusedLocals": false,
"noUnusedParameters": false,
"noPropertyAccessFromIndexSignature": false
}
"extends": "astro/tsconfigs/strict",
"include": [".astro/types.d.ts", "**/*"],
"exclude": ["dist", "node_modules"]
}