From f790cdedcc3ad8cf6aa9840bcad491557df24b02 Mon Sep 17 00:00:00 2001 From: ordinarthur Date: Tue, 24 Feb 2026 11:35:30 +0100 Subject: [PATCH] add stripe and elysia backend --- .DS_Store | Bin 6148 -> 6148 bytes .../use-bun-instead-of-node-vite-npm-pnpm.mdc | 111 ++++++++++++ .env.example | 3 + .gitignore | 34 ++++ README.md | 15 ++ bun.lock | 72 ++++++++ index.html | 27 +++ main.js | 55 +++++- package.json | 20 +++ server.ts | 140 +++++++++++++++ style.css | 106 +++++++++++ success.html | 168 ++++++++++++++++++ tsconfig.json | 29 +++ 13 files changed, 779 insertions(+), 1 deletion(-) create mode 100644 .cursor/rules/use-bun-instead-of-node-vite-npm-pnpm.mdc create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 README.md create mode 100644 bun.lock create mode 100644 package.json create mode 100644 server.ts create mode 100644 success.html create mode 100644 tsconfig.json diff --git a/.DS_Store b/.DS_Store index 3d6c9b339b4c4b4abeed9c06ed34833d6137ff79..788295894f8943262124c4130d2f65b0fc1e6a7d 100644 GIT binary patch literal 6148 zcmeHKPfrs;6n|4H-Gayh<&Q*@hF(k{2uh5`7;3?2^#Gv+LBP7*4rO6G)9fx3BqY7+ z!GlLXfFHn%9=&+<>cNX&z=J2R`sPmqOZA9Rv#)vcdo%Oiyq(`>=5+yp01*!)7oywDbj>5&8?k!u9t=Ur}>TwZGNjs_wIA_o382E^HI2Mc`sQ3#)3aVg|b z$A3tXj~?u*an*YSgiiBBZf7C+OTFcLqRp)!&zWCjCwM_DN}S=d+4w-ujlve zgBw>S(7t-@@GxogXD;7dC~mt|#-GWD27=d8ontv31iUId#cH^bd5?;f zWbQ)EvK>~*On*~MX0r25hJwUi5Ck zV=iYE92YSi-~fX%o`F3W$J_x_!97;X#94TQdi*RT(8rQ-{T~jBdJzK=1OF8RBtBc1&Eb}GZEe|{sI_U-C#XV%Uqhh` m!A6&3X^2uhgenE+(!xN@YHTRP5tQhUfTSU6A_o2_13v*}mj62d delta 78 zcmZoMXfc=|#>B`mu~2NHo+2a5#DLw5ER%Uy^d}3mf7z_S!OpU=;UeQ^b`E|HpsLM+ b9N(EI^NUytFaQA~0|OJ3X4xDevW6J|`P2}T diff --git a/.cursor/rules/use-bun-instead-of-node-vite-npm-pnpm.mdc b/.cursor/rules/use-bun-instead-of-node-vite-npm-pnpm.mdc new file mode 100644 index 0000000..ebda995 --- /dev/null +++ b/.cursor/rules/use-bun-instead-of-node-vite-npm-pnpm.mdc @@ -0,0 +1,111 @@ +--- +description: Use Bun instead of Node.js, npm, pnpm, or vite. +globs: "*.ts, *.tsx, *.html, *.css, *.js, *.jsx, package.json" +alwaysApply: false +--- + +Default to using Bun instead of Node.js. + +- Use `bun ` instead of `node ` or `ts-node ` +- Use `bun test` instead of `jest` or `vitest` +- Use `bun build ` instead of `webpack` or `esbuild` +- Use `bun install` instead of `npm install` or `yarn install` or `pnpm install` +- Use `bun run + + +``` + +With the following `frontend.tsx`: + +```tsx#frontend.tsx +import React from "react"; +import { createRoot } from "react-dom/client"; + +// import .css files directly and it works +import './index.css'; + +const root = createRoot(document.body); + +export default function Frontend() { + return

Hello, world!

; +} + +root.render(); +``` + +Then, run index.ts + +```sh +bun --hot ./index.ts +``` + +For more information, read the Bun API docs in `node_modules/bun-types/docs/**.mdx`. diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..eed70d3 --- /dev/null +++ b/.env.example @@ -0,0 +1,3 @@ +STRIPE_SECRET_KEY=sk_test_... +STRIPE_WEBHOOK_SECRET=whsec_... +DOMAIN=http://localhost:3000 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a14702c --- /dev/null +++ b/.gitignore @@ -0,0 +1,34 @@ +# dependencies (bun install) +node_modules + +# output +out +dist +*.tgz + +# code coverage +coverage +*.lcov + +# logs +logs +_.log +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# caches +.eslintcache +.cache +*.tsbuildinfo + +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store diff --git a/README.md b/README.md new file mode 100644 index 0000000..9296b37 --- /dev/null +++ b/README.md @@ -0,0 +1,15 @@ +# rebours + +To install dependencies: + +```bash +bun install +``` + +To run: + +```bash +bun run +``` + +This project was created using `bun init` in bun v1.3.9. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime. diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..5dfb10a --- /dev/null +++ b/bun.lock @@ -0,0 +1,72 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "rebours", + "dependencies": { + "@elysiajs/cors": "^1.4.1", + "@elysiajs/static": "^1.4.7", + "elysia": "^1.4.25", + "stripe": "^20.3.1", + }, + "devDependencies": { + "@types/bun": "latest", + }, + "peerDependencies": { + "typescript": "^5", + }, + }, + }, + "packages": { + "@borewit/text-codec": ["@borewit/text-codec@0.2.1", "", {}, "sha512-k7vvKPbf7J2fZ5klGRD9AeKfUvojuZIQ3BT5u7Jfv+puwXkUBUT5PVyMDfJZpy30CBDXGMgw7fguK/lpOMBvgw=="], + + "@elysiajs/cors": ["@elysiajs/cors@1.4.1", "", { "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-lQfad+F3r4mNwsxRKbXyJB8Jg43oAOXjRwn7sKUL6bcOW3KjUqUimTS+woNpO97efpzjtDE0tEjGk9DTw8lqTQ=="], + + "@elysiajs/static": ["@elysiajs/static@1.4.7", "", { "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-Go4kIXZ0G3iWfkAld07HmLglqIDMVXdyRKBQK/sVEjtpDdjHNb+rUIje73aDTWpZYg4PEVHUpi9v4AlNEwrQug=="], + + "@sinclair/typebox": ["@sinclair/typebox@0.34.48", "", {}, "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA=="], + + "@tokenizer/inflate": ["@tokenizer/inflate@0.4.1", "", { "dependencies": { "debug": "^4.4.3", "token-types": "^6.1.1" } }, "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA=="], + + "@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="], + + "@types/bun": ["@types/bun@1.3.9", "", { "dependencies": { "bun-types": "1.3.9" } }, "sha512-KQ571yULOdWJiMH+RIWIOZ7B2RXQGpL1YQrBtLIV3FqDcCu6FsbFUBwhdKUlCKUpS3PJDsHlJ1QKlpxoVR+xtw=="], + + "@types/node": ["@types/node@25.3.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A=="], + + "bun-types": ["bun-types@1.3.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-+UBWWOakIP4Tswh0Bt0QD0alpTY8cb5hvgiYeWCMet9YukHbzuruIEeXC2D7nMJPB12kbh8C7XJykSexEqGKJg=="], + + "cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="], + + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "elysia": ["elysia@1.4.25", "", { "dependencies": { "cookie": "^1.1.1", "exact-mirror": "^0.2.7", "fast-decode-uri-component": "^1.0.1", "memoirist": "^0.4.0" }, "peerDependencies": { "@sinclair/typebox": ">= 0.34.0 < 1", "@types/bun": ">= 1.2.0", "file-type": ">= 20.0.0", "openapi-types": ">= 12.0.0", "typescript": ">= 5.0.0" }, "optionalPeers": ["@types/bun", "typescript"] }, "sha512-liKjavH99Gpzrv9cDil6uYWmPuqESfPFV1FIaFSd3iNqo3y7e29sN43VxFIK8tWWnyi6eDAmi2SZk8hNAMQMyg=="], + + "exact-mirror": ["exact-mirror@0.2.7", "", { "peerDependencies": { "@sinclair/typebox": "^0.34.15" }, "optionalPeers": ["@sinclair/typebox"] }, "sha512-+MeEmDcLA4o/vjK2zujgk+1VTxPR4hdp23qLqkWfStbECtAq9gmsvQa3LW6z/0GXZyHJobrCnmy1cdeE7BjsYg=="], + + "fast-decode-uri-component": ["fast-decode-uri-component@1.0.1", "", {}, "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg=="], + + "file-type": ["file-type@21.3.0", "", { "dependencies": { "@tokenizer/inflate": "^0.4.1", "strtok3": "^10.3.4", "token-types": "^6.1.1", "uint8array-extras": "^1.4.0" } }, "sha512-8kPJMIGz1Yt/aPEwOsrR97ZyZaD1Iqm8PClb1nYFclUCkBi0Ma5IsYNQzvSFS9ib51lWyIw5mIT9rWzI/xjpzA=="], + + "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], + + "memoirist": ["memoirist@0.4.0", "", {}, "sha512-zxTgA0mSYELa66DimuNQDvyLq36AwDlTuVRbnQtB+VuTcKWm5Qc4z3WkSpgsFWHNhexqkIooqpv4hdcqrX5Nmg=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="], + + "stripe": ["stripe@20.3.1", "", { "peerDependencies": { "@types/node": ">=16" }, "optionalPeers": ["@types/node"] }, "sha512-k990yOT5G5rhX3XluRPw5Y8RLdJDW4dzQ29wWT66piHrbnM2KyamJ1dKgPsw4HzGHRWjDiSSdcI2WdxQUPV3aQ=="], + + "strtok3": ["strtok3@10.3.4", "", { "dependencies": { "@tokenizer/token": "^0.3.0" } }, "sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg=="], + + "token-types": ["token-types@6.1.2", "", { "dependencies": { "@borewit/text-codec": "^0.2.1", "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "uint8array-extras": ["uint8array-extras@1.5.0", "", {}, "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A=="], + + "undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], + } +} diff --git a/index.html b/index.html index 86cc464..7059413 100644 --- a/index.html +++ b/index.html @@ -63,6 +63,33 @@
+ + + + diff --git a/main.js b/main.js index a88a04d..918f506 100644 --- a/main.js +++ b/main.js @@ -87,6 +87,49 @@ document.addEventListener('DOMContentLoaded', () => { notes: document.getElementById('panel-notes'), }; + // ---- CHECKOUT LOGIC ---- + const checkoutSection = document.getElementById('checkout-section'); + const checkoutToggleBtn = document.getElementById('checkout-toggle-btn'); + const checkoutFormWrap = document.getElementById('checkout-form-wrap'); + const checkoutForm = document.getElementById('checkout-form'); + const checkoutSubmitBtn = document.getElementById('checkout-submit-btn'); + + // Toggle affichage du form + checkoutToggleBtn.addEventListener('click', () => { + const isOpen = checkoutFormWrap.style.display !== 'none'; + checkoutFormWrap.style.display = isOpen ? 'none' : 'block'; + checkoutToggleBtn.textContent = isOpen + ? '[ COMMANDER CETTE PIÈCE ]' + : '[ ANNULER ]'; + }); + + // Submit → appel API Elysia → redirect Stripe + checkoutForm.addEventListener('submit', async (e) => { + e.preventDefault(); + const email = document.getElementById('checkout-email').value; + + checkoutSubmitBtn.disabled = true; + checkoutSubmitBtn.textContent = 'CONNEXION STRIPE...'; + + try { + const res = await fetch('/api/checkout', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ product: 'lumiere_orbitale', email }), + }); + const data = await res.json(); + if (data.url) { + window.location.href = data.url; + } else { + throw new Error('No URL returned'); + } + } catch (err) { + checkoutSubmitBtn.disabled = false; + checkoutSubmitBtn.textContent = 'ERREUR — RÉESSAYER'; + console.error(err); + } + }); + function openPanel(card) { fields.img.src = card.dataset.img; fields.img.alt = card.dataset.name; @@ -100,6 +143,16 @@ document.addEventListener('DOMContentLoaded', () => { fields.specs.textContent = card.dataset.specs; fields.notes.textContent = card.dataset.notes; + // Affiche le bouton de commande uniquement pour PROJET_001 + const isOrderable = card.dataset.index === 'PROJET_001'; + checkoutSection.style.display = isOrderable ? 'block' : 'none'; + // Reset form state + checkoutFormWrap.style.display = 'none'; + checkoutToggleBtn.textContent = '[ COMMANDER CETTE PIÈCE ]'; + checkoutSubmitBtn.disabled = false; + checkoutSubmitBtn.textContent = 'PROCÉDER AU PAIEMENT →'; + checkoutForm.reset(); + // Ferme les accordéons panel.querySelectorAll('details').forEach(d => d.removeAttribute('open')); @@ -108,7 +161,7 @@ document.addEventListener('DOMContentLoaded', () => { document.body.style.overflow = 'hidden'; // Refresh cursor sur les nouveaux éléments - panel.querySelectorAll('summary, .panel-close').forEach(el => { + panel.querySelectorAll('summary, .panel-close, .checkout-btn, .checkout-submit').forEach(el => { el.addEventListener('mouseenter', () => { cursorOutline.style.width = '38px'; cursorOutline.style.height = '38px'; diff --git a/package.json b/package.json new file mode 100644 index 0000000..1035e29 --- /dev/null +++ b/package.json @@ -0,0 +1,20 @@ +{ + "name": "rebours", + "private": true, + "scripts": { + "dev": "bun run --hot server.ts", + "start": "bun run server.ts" + }, + "devDependencies": { + "@types/bun": "latest" + }, + "peerDependencies": { + "typescript": "^5" + }, + "dependencies": { + "@elysiajs/cors": "^1.4.1", + "@elysiajs/static": "^1.4.7", + "elysia": "^1.4.25", + "stripe": "^20.3.1" + } +} diff --git a/server.ts b/server.ts new file mode 100644 index 0000000..dad2d0a --- /dev/null +++ b/server.ts @@ -0,0 +1,140 @@ +import { Elysia, t } from 'elysia' +import { cors } from '@elysiajs/cors' +import { staticPlugin } from '@elysiajs/static' +import Stripe from 'stripe' + +const stripe = new Stripe(process.env.STRIPE_SECRET_KEY ?? '', { + apiVersion: '2025-01-27.acacia', +}) + +const DOMAIN = process.env.DOMAIN ?? 'http://localhost:3000' + +// Produit LUMIÈRE_ORBITALE — prix en centimes (EUR) +const PRODUCTS = { + lumiere_orbitale: { + name: 'LUMIÈRE_ORBITALE — REBOUR', + description: 'Lampe de table unique. Béton texturé coulé à la main + dôme céramique laqué. Collection 001.', + amount: 180000, // 1800 EUR + currency: 'eur', + }, +} + +const app = new Elysia() + .use(cors({ + origin: DOMAIN, + methods: ['GET', 'POST'], + })) + // Sert les fichiers statiques (html, css, js, assets) + .use(staticPlugin({ + assets: '.', + prefix: '/', + indexHTML: true, + })) + + // ── Créer une session de checkout Stripe ────────────────────────────────── + .post( + '/api/checkout', + async ({ body }) => { + const product = PRODUCTS[body.product as keyof typeof PRODUCTS] + if (!product) { + throw new Error('Produit inconnu') + } + + const session = await stripe.checkout.sessions.create({ + mode: 'payment', + payment_method_types: ['card'], + line_items: [ + { + price_data: { + currency: product.currency, + unit_amount: product.amount, + product_data: { + name: product.name, + description: product.description, + }, + }, + quantity: 1, + }, + ], + success_url: `${DOMAIN}/success.html?session_id={CHECKOUT_SESSION_ID}`, + cancel_url: `${DOMAIN}/#collection`, + locale: 'fr', + custom_text: { + submit: { message: 'Pièce unique — fabriquée à Paris. Délai de fabrication : 6 à 8 semaines.' }, + }, + }) + + return { url: session.url } + }, + { + body: t.Object({ + product: t.String(), + email: t.Optional(t.String({ format: 'email' })), + }), + } + ) + + // ── Vérification de session (page success) ──────────────────────────────── + .get( + '/api/session/:id', + async ({ params }) => { + const session = await stripe.checkout.sessions.retrieve(params.id) + return { + status: session.payment_status, + amount: session.amount_total, + currency: session.currency, + customer_email: session.customer_details?.email ?? null, + } + }, + { + params: t.Object({ id: t.String() }), + } + ) + + // ── Webhook Stripe ──────────────────────────────────────────────────────── + .post('/api/webhook', async ({ request, headers }) => { + const sig = headers['stripe-signature'] + const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET + + if (!sig || !webhookSecret) { + return new Response('Missing signature', { status: 400 }) + } + + let event: Stripe.Event + const rawBody = await request.arrayBuffer() + try { + event = stripe.webhooks.constructEvent( + Buffer.from(rawBody), + sig, + webhookSecret + ) + } catch (err) { + console.error('⚠ Webhook signature invalide:', err) + return new Response('Webhook Error', { status: 400 }) + } + + if (event.type === 'checkout.session.completed') { + const session = event.data.object as Stripe.Checkout.Session + if (session.payment_status === 'paid') { + console.log(`✓ Paiement reçu — session: ${session.id}`) + console.log(` Client: ${session.customer_details?.email}`) + console.log(` Montant: ${(session.amount_total ?? 0) / 100} EUR`) + // → ici : envoyer email confirmation, mettre à jour BDD, etc. + } + } + + return { received: true } + }) + + .listen(3000) + +console.log(` + ┌──────────────────────────────────────┐ + │ REBOUR — SERVEUR DÉMARRÉ │ + │ http://localhost:3000 │ + │ │ + │ POST /api/checkout │ + │ GET /api/session/:id │ + │ POST /api/webhook │ + └──────────────────────────────────────┘ +`) diff --git a/style.css b/style.css index c7540cf..9d77938 100644 --- a/style.css +++ b/style.css @@ -387,6 +387,112 @@ hr { border: none; border-top: var(--border); margin: 0; } padding-top: 0.5rem; } +/* ---- CHECKOUT SECTION ---- */ +.checkout-price-line { + display: flex; + align-items: baseline; + gap: 1.2rem; + margin-bottom: 1rem; +} +.checkout-price { + font-size: 1.6rem; + font-weight: 700; + letter-spacing: -0.01em; +} +.checkout-edition { + font-size: 0.72rem; + color: #888; +} + +/* Bouton jaune parking rectangulaire — aucun border-radius */ +.checkout-btn { + display: block; + width: 100%; + background: #e8a800; + color: var(--clr-black); + border: var(--border); + font-family: var(--font-mono); + font-size: 0.82rem; + font-weight: 700; + letter-spacing: 0.04em; + padding: 1.1rem 1.5rem; + text-align: center; + cursor: none; + transition: background 0.15s, color 0.15s; + pointer-events: auto; +} +.checkout-btn:hover { + background: var(--clr-black); + color: #e8a800; +} + +/* Form qui se déploie */ +.checkout-form-wrap { + border: var(--border); + border-top: none; + background: var(--clr-white); +} +.checkout-form { + display: flex; + flex-direction: column; + gap: 0; +} +.checkout-form-field { + display: flex; + flex-direction: column; + gap: 0.4rem; + padding: 1.1rem; + border-bottom: var(--border); +} +.checkout-form-field label { + font-size: 0.68rem; + font-weight: 700; + color: #888; + letter-spacing: 0.05em; +} +.checkout-form-field input { + border: none; + background: transparent; + font-family: var(--font-mono); + font-size: 0.85rem; + outline: none; + cursor: none; + padding: 0; + color: var(--clr-black); + pointer-events: auto; +} +.checkout-form-field input::placeholder { color: #bbb; } +.checkout-form-field input:focus { outline: none; } +.checkout-form-note { + padding: 0.9rem 1.1rem; + font-size: 0.72rem; + line-height: 1.8; + color: #777; + border-bottom: var(--border); +} +.checkout-submit { + background: var(--clr-black); + color: #e8a800; + border: none; + font-family: var(--font-mono); + font-size: 0.78rem; + font-weight: 700; + letter-spacing: 0.04em; + padding: 1.1rem 1.5rem; + cursor: none; + text-align: center; + transition: background 0.15s, color 0.15s; + pointer-events: auto; +} +.checkout-submit:hover { + background: #e8a800; + color: var(--clr-black); +} +.checkout-submit:disabled { + opacity: 0.5; + pointer-events: none; +} + /* ---- NEWSLETTER ---- */ .newsletter { display: grid; diff --git a/success.html b/success.html new file mode 100644 index 0000000..f73e50a --- /dev/null +++ b/success.html @@ -0,0 +1,168 @@ + + + + + + REBOUR — COMMANDE CONFIRMÉE + + + + + + +
+ + COLLECTION_001 +
+ +
+
+

// COMMANDE_CONFIRMÉE

+

MERCI
POUR
VOTRE
COMMANDE

+

Vérification du paiement...

+
+
+

// RÉCAPITULATIF

+
+ +

+ Un email de confirmation vous sera envoyé.
+ Votre lampe est fabriquée à la main à Paris. +

+ ← RETOUR À LA COLLECTION +
+
+ +
+ © 2026 REBOUR STUDIO — PARIS + INSTAGRAM / CONTACT +
+ + + + diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..bfa0fea --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,29 @@ +{ + "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 + } +}