mirror of
https://github.com/rjNemo/payit
synced 2026-06-06 02:16:40 +00:00
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:
parent
f0a6b128a0
commit
9814dc5e30
5 changed files with 122 additions and 102 deletions
11
internal/web/routes.go
Normal file
11
internal/web/routes.go
Normal 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))))
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
83
web/static/main.css
Normal 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;
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in a new issue