refactor using astro
This commit is contained in:
parent
ad400fbd6e
commit
8be3338265
1
.astro/content-assets.mjs
Normal file
1
.astro/content-assets.mjs
Normal file
@ -0,0 +1 @@
|
||||
export default new Map();
|
||||
1
.astro/content-modules.mjs
Normal file
1
.astro/content-modules.mjs
Normal file
@ -0,0 +1 @@
|
||||
export default new Map();
|
||||
199
.astro/content.d.ts
vendored
Normal file
199
.astro/content.d.ts
vendored
Normal 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
1
.astro/data-store.json
Normal 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
5
.astro/settings.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"_variables": {
|
||||
"lastUpdateCheck": 1772211077674
|
||||
}
|
||||
}
|
||||
2
.astro/types.d.ts
vendored
Normal file
2
.astro/types.d.ts
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
/// <reference types="astro/client" />
|
||||
/// <reference path="content.d.ts" />
|
||||
15
astro.config.mjs
Normal file
15
astro.config.mjs
Normal 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',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
12
nginx.conf
12
nginx.conf
@ -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
5719
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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"
|
||||
|
||||
@ -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 Poids: 3.2kg Alimentation: 220V — E27 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>
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@ -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>
|
||||
|
||||
19
server.mjs
19
server.mjs
@ -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
58
src/layouts/Base.astro
Normal 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>
|
||||
273
src/pages/collection/[slug].astro
Normal file
273
src/pages/collection/[slug].astro
Normal 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 Poids: 3.2kg Alimentation: 220V — E27 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 Poids plateau: 28kg Pieds: acier Ø60mm É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: 35–65cm (7 tailles) Dôme: Ø15–28cm Anneau néon: 8W — 3000K É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>
|
||||
/
|
||||
<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
276
src/pages/index.astro
Normal 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 Poids: 3.2kg Alimentation: 220V — E27 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 Poids plateau: 28kg Pieds: acier Ø60mm É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: 35–65cm (7 tailles) Dôme: Ø15–28cm Anneau néon: 8W — 3000K É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>
|
||||
/
|
||||
<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
167
src/pages/success.astro
Normal 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>
|
||||
/
|
||||
<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>
|
||||
@ -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"]
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user