{{ .ProductName }}
{{ .ProductDescription }}
-diff --git a/internal/web/routes.go b/internal/web/routes.go new file mode 100644 index 0000000..2ea2ee6 --- /dev/null +++ b/internal/web/routes.go @@ -0,0 +1,11 @@ +package web + +import ( + "net/http" +) + +func (h *Handler) registerRoutes(mux *http.ServeMux) { + mux.HandleFunc("POST /api/checkout", h.createCheckoutSession) + mux.Handle("GET /", http.HandlerFunc(h.renderCheckoutPage)) + mux.Handle("GET /static/", http.StripPrefix("/static/", http.FileServer(http.FS(h.fs)))) +} diff --git a/internal/web/server.go b/internal/web/server.go index be7b8b2..f122f98 100644 --- a/internal/web/server.go +++ b/internal/web/server.go @@ -23,6 +23,7 @@ type Handler struct { cfg config.Config checkout checkoutService page *template.Template + fs fs.FS } // NewServer constructs the root HTTP handler, wiring Stripe-backed endpoints as they are implemented. @@ -35,12 +36,10 @@ func NewServer(cfg config.Config) http.Handler { panic(fmt.Errorf("failed to load static assets: %w", err)) } - h := &Handler{cfg: cfg, checkout: checkoutSvc, page: tmpl} + h := &Handler{cfg: cfg, checkout: checkoutSvc, page: tmpl, fs: staticFS} mux := http.NewServeMux() - mux.HandleFunc("POST /api/checkout", h.createCheckoutSession) - mux.Handle("GET /", http.HandlerFunc(h.renderCheckoutPage)) - mux.Handle("GET /static/", http.StripPrefix("/static/", http.FileServer(http.FS(staticFS)))) + h.registerRoutes(mux) return mux } diff --git a/web/static/app.js b/web/static/app.js index 37c23dd..f19dffd 100644 --- a/web/static/app.js +++ b/web/static/app.js @@ -1,8 +1,8 @@ (() => { - const form = document.getElementById('checkout-form'); - const button = document.getElementById('checkout-button'); - const qtyInput = document.getElementById('quantity'); - const message = document.getElementById('message'); + const form = document.getElementById("checkout-form"); + const button = document.getElementById("checkout-button"); + const qtyInput = document.getElementById("quantity"); + const message = document.getElementById("message"); if (!form || !button || !qtyInput) { return; @@ -13,46 +13,46 @@ return; } message.textContent = text; - message.style.color = isError ? '#dc2626' : '#16a34a'; + message.style.color = isError ? "#dc2626" : "#16a34a"; }; - form.addEventListener('submit', async (event) => { + form.addEventListener("submit", async (event) => { event.preventDefault(); const quantity = Number.parseInt(qtyInput.value, 10); if (!Number.isFinite(quantity) || quantity <= 0) { - setMessage('Enter a quantity of at least 1.'); + setMessage("Enter a quantity of at least 1."); qtyInput.focus(); return; } try { button.disabled = true; - setMessage('Contacting Stripe…', false); + setMessage("Contacting Stripe…", false); - const response = await fetch('/api/checkout', { - method: 'POST', + const response = await fetch("/api/checkout", { + method: "POST", headers: { - 'Content-Type': 'application/json', + "Content-Type": "application/json", }, body: JSON.stringify({ quantity }), }); if (!response.ok) { const errorText = await response.text(); - throw new Error(errorText || 'Checkout request failed.'); + throw new Error(errorText || "Checkout request failed."); } const data = await response.json(); if (!data || !data.url) { - throw new Error('Checkout response missing redirect URL.'); + throw new Error("Checkout response missing redirect URL."); } - setMessage('Redirecting to Stripe…', false); + setMessage("Redirecting to Stripe…", false); window.location.href = data.url; } catch (err) { - console.error('Checkout failed', err); - setMessage('Unable to start checkout. Please try again.'); + console.error("Checkout failed", err); + setMessage("Unable to start checkout. Please try again."); button.disabled = false; } }); diff --git a/web/static/main.css b/web/static/main.css new file mode 100644 index 0000000..5834d89 --- /dev/null +++ b/web/static/main.css @@ -0,0 +1,83 @@ +:root { + color-scheme: light dark; + font-family: + "Inter", + system-ui, + -apple-system, + sans-serif; + line-height: 1.5; +} +body { + margin: 0; + display: flex; + min-height: 100vh; + background: radial-gradient(circle at top, #fdfbfb, #ebedee); + justify-content: center; + align-items: center; + padding: 2rem; +} +.card { + background: rgba(255, 255, 255, 0.95); + border-radius: 18px; + box-shadow: 0 20px 45px rgba(15, 23, 42, 0.12); + padding: 2.5rem; + max-width: 420px; + width: 100%; +} +h1 { + margin-top: 0; + font-size: 1.75rem; + color: #0f172a; +} +p { + margin: 0.5rem 0 1.5rem; + color: #475569; +} +.price { + font-size: 1.5rem; + font-weight: 600; + color: #2563eb; + margin-bottom: 1.5rem; +} +form { + display: flex; + flex-direction: column; + gap: 1.25rem; +} +label { + font-weight: 600; + color: #1e293b; +} +input[type="number"] { + width: 100%; + padding: 0.75rem 1rem; + border-radius: 12px; + border: 1px solid #cbd5f5; + font-size: 1rem; +} +button { + background: linear-gradient(135deg, #2563eb, #7c3aed); + border: none; + border-radius: 12px; + color: #fff; + font-size: 1rem; + font-weight: 600; + padding: 0.9rem 1.2rem; + cursor: pointer; + transition: + transform 0.15s ease, + box-shadow 0.15s ease; +} +button:hover:not([disabled]) { + transform: translateY(-1px); + box-shadow: 0 12px 30px rgba(37, 99, 235, 0.25); +} +button[disabled] { + opacity: 0.65; + cursor: wait; +} +#message { + min-height: 1.5rem; + color: #dc2626; + font-size: 0.95rem; +} diff --git a/web/templates/index.html b/web/templates/index.html index 178a422..9af67a9 100644 --- a/web/templates/index.html +++ b/web/templates/index.html @@ -4,93 +4,20 @@
{{ .ProductDescription }}
-