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;
|
listen 80;
|
||||||
server_name rebours.studio;
|
server_name rebours.studio;
|
||||||
|
|
||||||
root /var/www/rebours/public;
|
root /var/www/rebours/dist;
|
||||||
index index.html;
|
index index.html;
|
||||||
|
|
||||||
# HTML : jamais caché
|
# HTML : jamais caché
|
||||||
@ -10,9 +10,9 @@ server {
|
|||||||
add_header Cache-Control "no-store";
|
add_header Cache-Control "no-store";
|
||||||
}
|
}
|
||||||
|
|
||||||
# CSS / JS : revalidation obligatoire à chaque requête
|
# CSS / JS avec hash Astro : cache long immutable
|
||||||
location ~* \.(css|js)$ {
|
location ~* \.(css|js)$ {
|
||||||
add_header Cache-Control "no-cache";
|
add_header Cache-Control "public, max-age=31536000, immutable";
|
||||||
}
|
}
|
||||||
|
|
||||||
# Assets (images, fonts) : cache long
|
# Assets (images, fonts) : cache long
|
||||||
@ -20,12 +20,8 @@ server {
|
|||||||
add_header Cache-Control "public, max-age=31536000, immutable";
|
add_header Cache-Control "public, max-age=31536000, immutable";
|
||||||
}
|
}
|
||||||
|
|
||||||
location = /success {
|
|
||||||
try_files /success.html =404;
|
|
||||||
}
|
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
try_files $uri $uri/ /index.html;
|
try_files $uri $uri/ $uri.html /index.html;
|
||||||
}
|
}
|
||||||
|
|
||||||
location /api/ {
|
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,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "NODE_ENV=development node --watch server.mjs",
|
"dev": "concurrently \"astro dev\" \"NODE_ENV=development node --watch server.mjs\"",
|
||||||
"start": "NODE_ENV=production node server.mjs"
|
"build": "astro build",
|
||||||
|
"preview": "astro preview",
|
||||||
|
"server": "NODE_ENV=production node server.mjs",
|
||||||
|
"astro": "astro"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fastify/cors": "^10.0.2",
|
"@fastify/cors": "^10.0.2",
|
||||||
"@fastify/static": "^9.0.0",
|
"@fastify/static": "^9.0.0",
|
||||||
|
"astro": "^5.17.1",
|
||||||
|
"concurrently": "^9.0.0",
|
||||||
"dotenv": "^17.3.1",
|
"dotenv": "^17.3.1",
|
||||||
"fastify": "^5.3.2",
|
"fastify": "^5.3.2",
|
||||||
"stripe": "^20.3.1"
|
"stripe": "^20.3.1"
|
||||||
|
|||||||
@ -45,7 +45,7 @@
|
|||||||
"@type": "Offer",
|
"@type": "Offer",
|
||||||
"itemOffered": {
|
"itemOffered": {
|
||||||
"@type": "Product",
|
"@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é.",
|
"description": "Lampe de table unique. Béton texturé coulé à la main + dôme céramique laqué.",
|
||||||
"image": "https://rebours.studio/assets/lamp-violet.jpg"
|
"image": "https://rebours.studio/assets/lamp-violet.jpg"
|
||||||
},
|
},
|
||||||
@ -193,7 +193,7 @@
|
|||||||
|
|
||||||
<article class="product-card"
|
<article class="product-card"
|
||||||
data-index="PROJET_001"
|
data-index="PROJET_001"
|
||||||
data-name="LUMIÈRE_ORBITALE"
|
data-name="Solar_Altar"
|
||||||
data-type="LAMPE DE TABLE"
|
data-type="LAMPE DE TABLE"
|
||||||
data-mat="BÉTON TEXTURÉ + DÔME CÉRAMIQUE LAQUÉ"
|
data-mat="BÉTON TEXTURÉ + DÔME CÉRAMIQUE LAQUÉ"
|
||||||
data-year="2026"
|
data-year="2026"
|
||||||
@ -202,16 +202,16 @@
|
|||||||
data-specs="H: 45cm / Ø: 18cm Poids: 3.2kg Alimentation: 220V — E27 Câble: tressé rouge 2m"
|
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-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"
|
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">
|
<div class="card-img-wrap">
|
||||||
<img src="/assets/lamp-violet.jpg"
|
<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"
|
width="600" height="600"
|
||||||
loading="lazy">
|
loading="lazy">
|
||||||
</div>
|
</div>
|
||||||
<div class="card-meta">
|
<div class="card-meta">
|
||||||
<span class="card-index">001</span>
|
<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>
|
<span class="card-arrow">↗</span>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</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.src = card.dataset.img;
|
||||||
fields.img.alt = card.dataset.name;
|
fields.img.alt = card.dataset.name;
|
||||||
fields.index.textContent = card.dataset.index;
|
fields.index.textContent = card.dataset.index;
|
||||||
@ -192,23 +201,55 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
// Refresh cursor sur les nouveaux éléments
|
// Refresh cursor sur les nouveaux éléments
|
||||||
attachCursorHover(panel.querySelectorAll('summary, .panel-close, .checkout-btn, .checkout-submit'));
|
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.classList.remove('is-open');
|
||||||
panel.setAttribute('aria-hidden', 'true');
|
panel.setAttribute('aria-hidden', 'true');
|
||||||
document.body.style.overflow = '';
|
document.body.style.overflow = '';
|
||||||
|
|
||||||
|
if (pushState) {
|
||||||
|
history.pushState({}, '', '/');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cards.forEach(card => {
|
cards.forEach(card => {
|
||||||
card.addEventListener('click', () => openPanel(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
|
// Echap pour fermer
|
||||||
document.addEventListener('keydown', (e) => {
|
document.addEventListener('keydown', (e) => {
|
||||||
if (e.key === 'Escape') closePanel();
|
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>
|
<p class="label">// RÉCAPITULATIF</p>
|
||||||
<hr>
|
<hr>
|
||||||
<div id="order-details" style="display:none; flex-direction:column; gap:0;">
|
<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">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">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">EMAIL</span><span id="email-display"></span></div>
|
||||||
|
|||||||
19
server.mjs
19
server.mjs
@ -1,17 +1,11 @@
|
|||||||
import Fastify from 'fastify'
|
import Fastify from 'fastify'
|
||||||
import cors from '@fastify/cors'
|
import cors from '@fastify/cors'
|
||||||
import staticPlugin from '@fastify/static'
|
|
||||||
import Stripe from 'stripe'
|
import Stripe from 'stripe'
|
||||||
import { readFileSync } from 'node:fs'
|
|
||||||
import { join, dirname } from 'node:path'
|
|
||||||
import { fileURLToPath } from 'node:url'
|
|
||||||
import dotenv from 'dotenv'
|
import dotenv from 'dotenv'
|
||||||
dotenv.config()
|
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 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 = {
|
const PRODUCTS = {
|
||||||
lumiere_orbitale: {
|
lumiere_orbitale: {
|
||||||
@ -22,15 +16,6 @@ const PRODUCTS = {
|
|||||||
const app = Fastify({ logger: true })
|
const app = Fastify({ logger: true })
|
||||||
await app.register(cors, { origin: '*', methods: ['GET', 'POST'] })
|
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 ───────────────────────────────────────────────────────────────────────
|
// ── SEO ───────────────────────────────────────────────────────────────────────
|
||||||
app.get('/robots.txt', (_, reply) => {
|
app.get('/robots.txt', (_, reply) => {
|
||||||
reply
|
reply
|
||||||
@ -125,7 +110,7 @@ app.post('/api/webhook', {
|
|||||||
|
|
||||||
// ── Start ─────────────────────────────────────────────────────────────────────
|
// ── Start ─────────────────────────────────────────────────────────────────────
|
||||||
try {
|
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) {
|
} catch (err) {
|
||||||
app.log.error(err)
|
app.log.error(err)
|
||||||
process.exit(1)
|
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": {
|
"extends": "astro/tsconfigs/strict",
|
||||||
// Environment setup & latest features
|
"include": [".astro/types.d.ts", "**/*"],
|
||||||
"lib": ["ESNext"],
|
"exclude": ["dist", "node_modules"]
|
||||||
"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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user