correct js
This commit is contained in:
parent
45fd3c18c0
commit
82226b2313
293
public/main.js
293
public/main.js
@ -1,115 +1,206 @@
|
|||||||
import Fastify from 'fastify'
|
/**
|
||||||
import cors from '@fastify/cors'
|
* REBOUR — Main Script
|
||||||
import Stripe from 'stripe'
|
*/
|
||||||
import { readFileSync } from 'node:fs'
|
|
||||||
import dotenv from 'dotenv';
|
|
||||||
dotenv.config()
|
|
||||||
|
|
||||||
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY ?? '')
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
const DOMAIN = process.env.DOMAIN ?? 'http://localhost:3000'
|
|
||||||
|
|
||||||
const PRODUCTS = {
|
// ---- CUSTOM CURSOR ----
|
||||||
lumiere_orbitale: {
|
const cursorDot = document.querySelector('.cursor-dot');
|
||||||
name: 'LUMIÈRE_ORBITALE — REBOUR',
|
const cursorOutline = document.querySelector('.cursor-outline');
|
||||||
description: 'Lampe de table unique. Béton texturé coulé à la main + dôme céramique laqué. Collection 001.',
|
|
||||||
amount: 180000,
|
|
||||||
currency: 'eur',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
const app = Fastify({ logger: true })
|
let mouseX = 0, mouseY = 0;
|
||||||
|
let outlineX = 0, outlineY = 0;
|
||||||
|
let rafId = null;
|
||||||
|
|
||||||
await app.register(cors, { origin: '*', methods: ['GET', 'POST'] })
|
window.addEventListener('mousemove', (e) => {
|
||||||
|
mouseX = e.clientX;
|
||||||
// ── SEO ───────────────────────────────────────────────────────────────────────
|
mouseY = e.clientY;
|
||||||
app.get('/robots.txt', (_, reply) => {
|
// Première fois : initialise l'outline à la position courante et rend visible
|
||||||
reply
|
if (outlineX === 0 && outlineY === 0) {
|
||||||
.type('text/plain')
|
outlineX = mouseX;
|
||||||
.header('Cache-Control', 'public, max-age=86400')
|
outlineY = mouseY;
|
||||||
.send(`User-agent: *\nAllow: /\nSitemap: ${DOMAIN}/sitemap.xml\n`)
|
cursorDot.style.opacity = '1';
|
||||||
})
|
cursorOutline.style.opacity = '1';
|
||||||
|
|
||||||
app.get('/sitemap.xml', (_, reply) => {
|
|
||||||
const today = new Date().toISOString().split('T')[0]
|
|
||||||
reply
|
|
||||||
.type('application/xml')
|
|
||||||
.header('Cache-Control', 'public, max-age=86400')
|
|
||||||
.send(
|
|
||||||
`<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n <url><loc>${DOMAIN}/</loc><lastmod>${today}</lastmod><changefreq>weekly</changefreq><priority>1.0</priority></url>\n</urlset>`
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
// ── Checkout Stripe ───────────────────────────────────────────────────────────
|
|
||||||
app.post('/api/checkout', async (request, reply) => {
|
|
||||||
const { product, email } = request.body ?? {}
|
|
||||||
const p = PRODUCTS[product]
|
|
||||||
if (!p) return reply.code(404).send({ error: 'Produit inconnu' })
|
|
||||||
|
|
||||||
const session = await stripe.checkout.sessions.create({
|
|
||||||
mode: 'payment',
|
|
||||||
payment_method_types: ['card'],
|
|
||||||
line_items: [{
|
|
||||||
price_data: {
|
|
||||||
currency: p.currency,
|
|
||||||
unit_amount: p.amount,
|
|
||||||
product_data: { name: p.name, description: p.description },
|
|
||||||
},
|
|
||||||
quantity: 1,
|
|
||||||
}],
|
|
||||||
success_url: `${DOMAIN}/success?session_id={CHECKOUT_SESSION_ID}`,
|
|
||||||
cancel_url: `${DOMAIN}/#collection`,
|
|
||||||
locale: 'fr',
|
|
||||||
customer_email: email ?? undefined,
|
|
||||||
custom_text: {
|
|
||||||
submit: { message: 'Pièce unique — fabriquée à Paris. Délai : 6 à 8 semaines.' },
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
return { url: session.url }
|
|
||||||
})
|
|
||||||
|
|
||||||
// ── Vérification session ──────────────────────────────────────────────────────
|
|
||||||
app.get('/api/session/:id', async (request) => {
|
|
||||||
const session = await stripe.checkout.sessions.retrieve(request.params.id)
|
|
||||||
return {
|
|
||||||
status: session.payment_status,
|
|
||||||
amount: session.amount_total,
|
|
||||||
currency: session.currency,
|
|
||||||
customer_email: session.customer_details?.email ?? null,
|
|
||||||
}
|
}
|
||||||
})
|
cursorDot.style.transform = `translate(calc(-50% + ${mouseX}px), calc(-50% + ${mouseY}px))`;
|
||||||
|
if (!rafId) rafId = requestAnimationFrame(animateOutline);
|
||||||
|
}, { once: false });
|
||||||
|
|
||||||
// ── Webhook Stripe ────────────────────────────────────────────────────────────
|
function animateOutline() {
|
||||||
app.addContentTypeParser('application/json', { parseAs: 'buffer' }, (req, body, done) => {
|
rafId = null;
|
||||||
done(null, body)
|
outlineX += (mouseX - outlineX) * 0.18;
|
||||||
})
|
outlineY += (mouseY - outlineY) * 0.18;
|
||||||
|
cursorOutline.style.transform = `translate(calc(-50% + ${outlineX}px), calc(-50% + ${outlineY}px))`;
|
||||||
|
if (Math.abs(mouseX - outlineX) > 0.1 || Math.abs(mouseY - outlineY) > 0.1) {
|
||||||
|
rafId = requestAnimationFrame(animateOutline);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
app.post('/api/webhook', async (request, reply) => {
|
function attachCursorHover(elements) {
|
||||||
const sig = request.headers['stripe-signature']
|
elements.forEach(el => {
|
||||||
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET
|
el.addEventListener('mouseenter', () => {
|
||||||
if (!sig || !webhookSecret) return reply.code(400).send('Missing signature')
|
cursorOutline.style.width = '38px';
|
||||||
|
cursorOutline.style.height = '38px';
|
||||||
|
cursorDot.style.opacity = '0';
|
||||||
|
});
|
||||||
|
el.addEventListener('mouseleave', () => {
|
||||||
|
cursorOutline.style.width = '26px';
|
||||||
|
cursorOutline.style.height = '26px';
|
||||||
|
cursorDot.style.opacity = '1';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
attachCursorHover(document.querySelectorAll('a, button, input, .product-card, summary, .panel-close'));
|
||||||
|
|
||||||
|
// ---- INTERACTIVE GRID ----
|
||||||
|
const gridContainer = document.getElementById('interactive-grid');
|
||||||
|
const CELL = 60;
|
||||||
|
const COLORS = [
|
||||||
|
'rgba(232,168,0,0.45)',
|
||||||
|
'rgba(232,168,0,0.32)',
|
||||||
|
'rgba(232,168,0,0.18)',
|
||||||
|
];
|
||||||
|
|
||||||
|
function buildGrid() {
|
||||||
|
if (!gridContainer) return;
|
||||||
|
gridContainer.innerHTML = '';
|
||||||
|
const cols = Math.ceil(window.innerWidth / CELL);
|
||||||
|
const rows = Math.ceil(window.innerHeight / CELL);
|
||||||
|
gridContainer.style.display = 'grid';
|
||||||
|
gridContainer.style.gridTemplateColumns = `repeat(${cols}, ${CELL}px)`;
|
||||||
|
gridContainer.style.gridTemplateRows = `repeat(${rows}, ${CELL}px)`;
|
||||||
|
|
||||||
|
for (let i = 0; i < cols * rows; i++) {
|
||||||
|
const cell = document.createElement('div');
|
||||||
|
cell.className = 'grid-cell';
|
||||||
|
cell.addEventListener('mouseenter', () => {
|
||||||
|
cell.style.transition = 'none';
|
||||||
|
cell.style.backgroundColor = COLORS[Math.floor(Math.random() * COLORS.length)];
|
||||||
|
});
|
||||||
|
cell.addEventListener('mouseleave', () => {
|
||||||
|
cell.style.transition = 'background-color 1.4s ease-out';
|
||||||
|
cell.style.backgroundColor = 'transparent';
|
||||||
|
});
|
||||||
|
gridContainer.appendChild(cell);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buildGrid();
|
||||||
|
let rt;
|
||||||
|
window.addEventListener('resize', () => { clearTimeout(rt); rt = setTimeout(buildGrid, 150); });
|
||||||
|
|
||||||
|
// ---- PRODUCT PANEL ----
|
||||||
|
const panel = document.getElementById('product-panel');
|
||||||
|
const panelClose = document.getElementById('panel-close');
|
||||||
|
const cards = document.querySelectorAll('.product-card');
|
||||||
|
|
||||||
|
// Champs du panel
|
||||||
|
const fields = {
|
||||||
|
img: document.getElementById('panel-img'),
|
||||||
|
index: document.getElementById('panel-index'),
|
||||||
|
name: document.getElementById('panel-name'),
|
||||||
|
type: document.getElementById('panel-type'),
|
||||||
|
mat: document.getElementById('panel-mat'),
|
||||||
|
year: document.getElementById('panel-year'),
|
||||||
|
status: document.getElementById('panel-status'),
|
||||||
|
desc: document.getElementById('panel-desc'),
|
||||||
|
specs: document.getElementById('panel-specs'),
|
||||||
|
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...';
|
||||||
|
|
||||||
let event
|
|
||||||
try {
|
try {
|
||||||
event = stripe.webhooks.constructEvent(request.body, sig, webhookSecret)
|
const res = await fetch('/api/checkout', {
|
||||||
} catch {
|
method: 'POST',
|
||||||
return reply.code(400).send('Webhook Error')
|
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;
|
||||||
|
fields.index.textContent = card.dataset.index;
|
||||||
|
fields.name.textContent = card.dataset.name;
|
||||||
|
fields.type.textContent = card.dataset.type;
|
||||||
|
fields.mat.textContent = card.dataset.mat;
|
||||||
|
fields.year.textContent = card.dataset.year;
|
||||||
|
fields.status.textContent = card.dataset.status;
|
||||||
|
fields.desc.textContent = card.dataset.desc;
|
||||||
|
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'));
|
||||||
|
|
||||||
|
panel.classList.add('is-open');
|
||||||
|
panel.setAttribute('aria-hidden', 'false');
|
||||||
|
document.body.style.overflow = 'hidden';
|
||||||
|
|
||||||
|
// Refresh cursor sur les nouveaux éléments
|
||||||
|
attachCursorHover(panel.querySelectorAll('summary, .panel-close, .checkout-btn, .checkout-submit'));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.type === 'checkout.session.completed') {
|
function closePanel() {
|
||||||
const session = event.data.object
|
panel.classList.remove('is-open');
|
||||||
if (session.payment_status === 'paid') {
|
panel.setAttribute('aria-hidden', 'true');
|
||||||
app.log.info(`✓ Paiement — ${session.id} — ${session.customer_details?.email}`)
|
document.body.style.overflow = '';
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return { received: true }
|
cards.forEach(card => {
|
||||||
})
|
card.addEventListener('click', () => openPanel(card));
|
||||||
|
});
|
||||||
|
|
||||||
// ── Start ─────────────────────────────────────────────────────────────────────
|
panelClose.addEventListener('click', closePanel);
|
||||||
try {
|
|
||||||
await app.listen({ port: 3000, host: '127.0.0.1' })
|
// Echap pour fermer
|
||||||
} catch (err) {
|
document.addEventListener('keydown', (e) => {
|
||||||
app.log.error(err)
|
if (e.key === 'Escape') closePanel();
|
||||||
process.exit(1)
|
});
|
||||||
}
|
|
||||||
|
});
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user