diff --git a/.astro/types.d.ts b/.astro/types.d.ts
index 03d7cc4..f964fe0 100644
--- a/.astro/types.d.ts
+++ b/.astro/types.d.ts
@@ -1,2 +1 @@
///
-///
\ No newline at end of file
diff --git a/admin.mjs b/admin.mjs
index 1003082..ad8c06a 100644
--- a/admin.mjs
+++ b/admin.mjs
@@ -2,11 +2,16 @@ import AdminJS from 'adminjs'
import AdminJSFastify from '@adminjs/fastify'
import { Database, Resource, getModelByName } from '@adminjs/prisma'
import bcrypt from 'bcrypt'
+import Stripe from 'stripe'
import { exec } from 'child_process'
import { prisma } from './src/lib/db.mjs'
AdminJS.registerAdapter({ Database, Resource })
+const stripe = process.env.STRIPE_SECRET_KEY
+ ? new Stripe(process.env.STRIPE_SECRET_KEY)
+ : null
+
// ── Auto-build (prod only) ──────────────────────────────────────────────────
let buildTimeout = null
let buildInProgress = false
@@ -31,6 +36,92 @@ function triggerBuild() {
const afterBuildHook = async (response) => { triggerBuild(); return response }
+// ── Stripe price sync after product edit ─────────────────────────────────────
+async function syncPriceToStripe(response) {
+ if (!stripe) return response
+
+ const product = await prisma.product.findUnique({
+ where: { id: response.record.params.id },
+ })
+ if (!product) return response
+
+ const priceCents = product.price
+ const currency = (product.currency || 'EUR').toLowerCase()
+ const slug = product.slug
+
+ try {
+ // 1. Find or create Stripe product by metadata.slug
+ let stripeProduct = null
+ const existing = await stripe.products.search({ query: `metadata["slug"]:"${slug}"` })
+ if (existing.data.length > 0) {
+ stripeProduct = existing.data[0]
+ // Update product info
+ await stripe.products.update(stripeProduct.id, {
+ name: product.productDisplayName,
+ description: product.description,
+ images: product.ogImage ? [product.ogImage] : undefined,
+ active: true,
+ })
+ } else if (priceCents) {
+ stripeProduct = await stripe.products.create({
+ name: product.productDisplayName,
+ description: product.description,
+ images: product.ogImage ? [product.ogImage] : [],
+ metadata: { slug, stripeKey: product.stripeKey || slug, dbId: product.id },
+ })
+ console.log(`[stripe] Product created: ${stripeProduct.id}`)
+ }
+
+ if (!stripeProduct) return response
+
+ // 2. If price is null, archive old price and clear DB
+ if (!priceCents) {
+ if (product.stripePriceId) {
+ await stripe.prices.update(product.stripePriceId, { active: false })
+ await prisma.product.update({ where: { id: product.id }, data: { stripePriceId: null } })
+ console.log(`[stripe] Price archived (product has no price now)`)
+ }
+ return response
+ }
+
+ // 3. Check if current Stripe price matches
+ if (product.stripePriceId) {
+ try {
+ const currentPrice = await stripe.prices.retrieve(product.stripePriceId)
+ if (currentPrice.unit_amount === priceCents && currentPrice.currency === currency && currentPrice.active) {
+ // Price unchanged
+ return response
+ }
+ // Archive old price
+ await stripe.prices.update(product.stripePriceId, { active: false })
+ console.log(`[stripe] Old price archived: ${product.stripePriceId}`)
+ } catch {
+ // Old price doesn't exist, continue
+ }
+ }
+
+ // 4. Create new price
+ const newPrice = await stripe.prices.create({
+ product: stripeProduct.id,
+ unit_amount: priceCents,
+ currency,
+ metadata: { slug, dbId: product.id },
+ })
+ console.log(`[stripe] New price created: ${newPrice.id} (${priceCents / 100} ${currency.toUpperCase()})`)
+
+ // 5. Update DB
+ await prisma.product.update({
+ where: { id: product.id },
+ data: { stripePriceId: newPrice.id },
+ })
+
+ } catch (err) {
+ console.error(`[stripe] Sync error for ${slug}:`, err.message)
+ }
+
+ return response
+}
+
// ── AdminJS setup ───────────────────────────────────────────────────────────
export async function setupAdmin(app) {
const admin = new AdminJS({
@@ -56,8 +147,8 @@ export async function setupAdmin(app) {
seoDescription: { type: 'textarea' },
},
actions: {
- new: { after: [afterBuildHook] },
- edit: { after: [afterBuildHook] },
+ new: { after: [syncPriceToStripe, afterBuildHook] },
+ edit: { after: [syncPriceToStripe, afterBuildHook] },
delete: { after: [afterBuildHook] },
},
},
diff --git a/nginx.conf b/nginx.conf
index 64316af..edd2acc 100644
--- a/nginx.conf
+++ b/nginx.conf
@@ -7,7 +7,7 @@ server {
# ── API proxy → Fastify ──────────────────────────────────────────────────
location /api/ {
- proxy_pass http://127.0.0.1:3001;
+ proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
@@ -16,20 +16,20 @@ server {
# ── SEO (dynamique depuis DB) ────────────────────────────────────────────
location = /robots.txt {
- proxy_pass http://127.0.0.1:3001;
+ proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
location = /sitemap.xml {
- proxy_pass http://127.0.0.1:3001;
+ proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
# ── Admin proxy → Fastify (AdminJS) ──────────────────────────────────────
location ^~ /admin {
- proxy_pass http://127.0.0.1:3001;
+ proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
diff --git a/prisma/seed.mjs b/prisma/seed.mjs
index 7c216ae..7210e0e 100644
--- a/prisma/seed.mjs
+++ b/prisma/seed.mjs
@@ -25,7 +25,7 @@ const PRODUCTS = [
price: 180000, // 1800 EUR in cents
currency: 'EUR',
availability: 'https://schema.org/LimitedAvailability',
- stripePriceId: 'price_1T5SBlE5wMMoCUP5ZcjEStwe',
+ stripePriceId: null,
stripeKey: 'lumiere_orbitale',
isPublished: true,
},
diff --git a/public/style.css b/public/style.css
index 020ff3e..fd4088b 100644
--- a/public/style.css
+++ b/public/style.css
@@ -470,10 +470,17 @@ hr { border: none; border-top: var(--border); margin: 0; }
transition: background 0.15s, color 0.15s;
pointer-events: auto;
}
-.checkout-btn:hover {
+.checkout-btn:hover:not(:disabled) {
background: var(--clr-black);
color: #e8a800;
}
+.checkout-btn--disabled {
+ background: #999;
+ border-color: #999;
+ color: #ddd;
+ cursor: default;
+ pointer-events: none;
+}
/* Form qui se déploie */
.checkout-form-wrap {
diff --git a/src/scripts/main.js b/src/scripts/main.js
index 1ed09df..4e766c4 100644
--- a/src/scripts/main.js
+++ b/src/scripts/main.js
@@ -526,17 +526,23 @@ document.addEventListener('DOMContentLoaded', () => {
const stripeKey = card.dataset.stripeKey;
const isOrderable = price && stripeKey;
+ checkoutSection.style.display = 'block';
+
if (isOrderable) {
currentStripeKey = stripeKey;
checkoutPriceEl.textContent = formatPrice(parseInt(price, 10));
- checkoutSection.style.display = 'block';
+ checkoutToggleBtn.textContent = '[ COMMANDER CETTE PIÈCE ]';
+ checkoutToggleBtn.disabled = false;
+ checkoutToggleBtn.classList.remove('checkout-btn--disabled');
} else {
currentStripeKey = null;
- checkoutSection.style.display = 'none';
+ checkoutPriceEl.textContent = '';
+ checkoutToggleBtn.textContent = 'PROCHAINEMENT DISPONIBLE';
+ checkoutToggleBtn.disabled = true;
+ checkoutToggleBtn.classList.add('checkout-btn--disabled');
}
checkoutFormWrap.style.display = 'none';
- checkoutToggleBtn.textContent = '[ COMMANDER CETTE PIÈCE ]';
checkoutSubmitBtn.disabled = false;
checkoutSubmitBtn.textContent = 'PROCÉDER AU PAIEMENT →';
checkoutForm.reset();