feat(web): extract static assets and routes for clarity

Refactored web server to move route registration into a dedicated method
and extracted static CSS into its own file for better maintainability.
Updated HTML template to use external stylesheet and improved JS code
style consistency. This enhances separation of concerns and prepares
for easier future asset management.
This commit is contained in:
Ruidy 2025-09-27 23:01:04 +02:00
parent f0a6b128a0
commit 9814dc5e30
No known key found for this signature in database
GPG key ID: 705C24D202990805
5 changed files with 122 additions and 102 deletions

11
internal/web/routes.go Normal file
View file

@ -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))))
}

View file

@ -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
}

View file

@ -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;
}
});

83
web/static/main.css Normal file
View file

@ -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;
}

View file

@ -4,93 +4,20 @@
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>PayIt Checkout</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600&display=swap" />
<style>
: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;
}
</style>
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600&display=swap"
/>
<link rel="stylesheet" href="/static/main.css" />
</head>
<body>
<main class="card">
<h1>{{ .ProductName }}</h1>
<p>{{ .ProductDescription }}</p>
<div class="price">{{ .PriceDisplay }} <span class="currency">{{ .Currency }}</span></div>
<form id="checkout-form">
<div class="price">
{{ .PriceDisplay }} <span class="currency">{{ .Currency }}</span>
</div>
<form id="checkout-form" action="/api/checkout">
<label for="quantity">Quantity</label>
<input id="quantity" name="quantity" type="number" value="1" min="1" />
<button id="checkout-button" type="submit">Buy now</button>