This commit is contained in:
tumillanino
2025-10-28 12:36:37 +11:00
parent 186d6768fb
commit dad3ec655f
31 changed files with 2256 additions and 0 deletions

View File

@@ -0,0 +1,110 @@
{{define "header"}}
<!-- Navigation Drawer -->
<div id="nav-drawer" class="drawer">
<div class="drawer-overlay"></div>
<div class="drawer-content">
<div class="drawer-header">
<div class="drawer-title">Menu</div>
<button class="drawer-close" aria-label="Close menu">
<svg class="drawer-nav-icon"><use href="/static/icons.svg#icon-x"/></svg>
</button>
</div>
<div class="drawer-body">
<nav class="drawer-nav">
<a href="/" {{if eq .ActivePage "home"}}class="active"{{end}}>
<svg class="drawer-nav-icon"><use href="/static/icons.svg#icon-home"/></svg>
Home
</a>
<a href="/catalog" {{if eq .ActivePage "catalog"}}class="active"{{end}}>
<svg class="drawer-nav-icon"><use href="/static/icons.svg#icon-catalog"/></svg>
Catalog
</a>
<a href="/gallery" {{if eq .ActivePage "gallery"}}class="active"{{end}}>
<svg class="drawer-nav-icon"><use href="/static/icons.svg#icon-image"/></svg>
Gallery
</a>
<a href="/booking" {{if eq .ActivePage "booking"}}class="active"{{end}}>
<svg class="drawer-nav-icon"><use href="/static/icons.svg#icon-calendar"/></svg>
Book
</a>
<a href="/profile" {{if eq .ActivePage "profile"}}class="active"{{end}}>
<svg class="drawer-nav-icon"><use href="/static/icons.svg#icon-user"/></svg>
Profile
</a>
</nav>
</div>
</div>
</div>
<!-- Header -->
<header class="header-bar">
<div style="display: flex; align-items: center; gap: 1rem;">
<button class="menu-btn" onclick="window.navDrawer?.toggle()" aria-label="Toggle menu">
<div class="menu-icon">
<span></span>
<span></span>
<span></span>
</div>
</button>
<a href="/" class="header-brand" style="position: relative;">
<img src="/assets/balloon.png" alt="Decor By Hannah's">
Decor By Hannah's
<img src="/assets/party-icons/party-hat.png" alt="" style="position: absolute; top: -8px; right: -15px; width: 25px; height: 25px; transform: rotate(20deg);">
</a>
</div>
<div style="display: flex; align-items: center; gap: 1rem;">
<nav class="header-nav">
<a href="/" {{if eq .ActivePage "home"}}class="active"{{end}}>Home</a>
<a href="/catalog" {{if eq .ActivePage "catalog"}}class="active"{{end}}>Catalog</a>
<a href="/gallery" {{if eq .ActivePage "gallery"}}class="active"{{end}}>Gallery</a>
<a href="/booking" {{if eq .ActivePage "booking"}}class="active"{{end}}>Book</a>
<a href="/profile" {{if eq .ActivePage "profile"}}class="active"{{end}}>Profile</a>
</nav>
<button id="themeToggle" class="btn btn-outline" aria-label="Toggle theme">
<svg id="sunIcon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"></circle><line x1="12" y1="1" x2="12" y2="3"></line><line x1="12" y1="21" x2="12" y2="23"></line><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line><line x1="1" y1="12" x2="3" y2="12"></line><line x1="21" y1="12" x2="23" y2="12"></line><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line></svg>
<svg id="moonIcon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display: none;"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path></svg>
</button>
</div>
</header>
{{end}}
{{define "footer"}}
<footer style="position: relative;">
<img src="/assets/party-icons/penguin-wearing-party-hat.png" alt="" style="position: absolute; bottom: 10px; left: 20px; width: 50px; height: 50px; opacity: 0.6;">
<img src="/assets/party-icons/party-hat-with-horn.png" alt="" style="position: absolute; bottom: 10px; right: 20px; width: 50px; height: 50px; opacity: 0.6; transform: rotate(-20deg);">
<small>© 2025 Decor By Hannah's. All rights reserved.</small>
</footer>
{{end}}
{{define "theme-script"}}
<script>
(function() {
const themeToggle = document.getElementById('themeToggle');
const sunIcon = document.getElementById('sunIcon');
const moonIcon = document.getElementById('moonIcon');
const html = document.documentElement;
const savedTheme = localStorage.getItem('theme') || 'light';
html.setAttribute('data-theme', savedTheme);
updateIcons(savedTheme);
function updateIcons(theme) {
if (theme === 'dark') {
sunIcon.style.display = 'none';
moonIcon.style.display = 'block';
} else {
sunIcon.style.display = 'block';
moonIcon.style.display = 'none';
}
}
themeToggle.addEventListener('click', () => {
const currentTheme = html.getAttribute('data-theme');
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
html.setAttribute('data-theme', newTheme);
localStorage.setItem('theme', newTheme);
updateIcons(newTheme);
});
})();
</script>
{{end}}

106
internal/tmpl/booking.tmpl Normal file
View File

@@ -0,0 +1,106 @@
{{define "booking.tmpl"}}
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Book a Service - Decor By Hannahs</title>
<link rel="stylesheet" href="/static/css/styles.css">
<script>
(function() {
const savedTheme = localStorage.getItem('theme') || 'light';
document.documentElement.setAttribute('data-theme', savedTheme);
})();
</script>
<script src="/static/js/alpine.js" defer></script>
<script src="/static/htmx/htmx.min.js" defer></script>
<script src="/static/js/drawer.js" defer></script>
</head>
<body>
{{template "header" .}}
<main>
<div style="max-width: 600px; margin: 0 auto;">
<div style="margin-bottom: 2rem; position: relative;">
<img src="/assets/party-icons/birthday-cake.png" alt="Birthday cake" style="position: absolute; top: -20px; right: -40px; width: 90px; height: 90px; opacity: 0.85;">
<h1 style="font-size: 2.5rem; font-weight: 700; margin-bottom: 0.5rem;">Book a Service</h1>
<p style="color: hsl(var(--muted-foreground)); font-size: 1.125rem;">Fill out the form below to request a booking</p>
</div>
<div class="card">
<form id="bookingForm">
<div class="form-group">
<label class="form-label" for="service_id">Service ID</label>
<input class="form-input" type="text" id="service_id" name="service_id" placeholder="Enter service ID" required>
</div>
<div class="form-group">
<label class="form-label" for="event_date">Event Date & Time</label>
<input class="form-input" type="datetime-local" id="event_date" name="event_date" required>
</div>
<div class="form-group">
<label class="form-label" for="address">Address</label>
<input class="form-input" type="text" id="address" name="address" placeholder="Event location">
</div>
<div class="form-group">
<label class="form-label" for="notes">Additional Notes</label>
<textarea class="form-textarea" id="notes" name="notes" placeholder="Any special requests or details..."></textarea>
</div>
<div style="display: flex; gap: 0.5rem; margin-top: 1.5rem;">
<button type="submit" class="btn btn-primary" style="flex: 1;">Request Booking</button>
<a href="/catalog" class="btn btn-outline">Back to Catalog</a>
</div>
</form>
</div>
</div>
</main>
{{template "footer"}}
{{template "theme-script"}}
<script>
document.getElementById('bookingForm').addEventListener('submit', async function(e){
e.preventDefault();
const f = e.target;
const submitBtn = f.querySelector('button[type="submit"]');
const originalText = submitBtn.textContent;
submitBtn.disabled = true;
submitBtn.textContent = 'Submitting...';
const body = {
user_id: "",
service_id: f.service_id.value,
event_date: new Date(f.event_date.value).toISOString(),
address: f.address.value,
notes: f.notes.value
};
try {
const res = await fetch('/api/bookings', {
method: 'POST',
headers: {'Content-Type':'application/json'},
body: JSON.stringify(body)
});
if (res.ok) {
alert('Booking requested successfully! We will contact you soon.');
f.reset();
} else {
const error = await res.text();
alert('Error creating booking: ' + error);
}
} catch (err) {
alert('Network error. Please try again.');
} finally {
submitBtn.disabled = false;
submitBtn.textContent = originalText;
}
});
</script>
</body>
</html>
{{end}}

View File

@@ -0,0 +1,64 @@
{{define "catalog.tmpl"}}
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Catalog - Decor By Hannahs</title>
<link rel="stylesheet" href="/static/css/styles.css">
<script>
(function() {
const savedTheme = localStorage.getItem('theme') || 'light';
document.documentElement.setAttribute('data-theme', savedTheme);
})();
</script>
<script src="/static/js/alpine.js" defer></script>
<script src="/static/htmx/htmx.min.js" defer></script>
<script src="/static/js/drawer.js" defer></script>
</head>
<body>
{{template "header" .}}
<main>
<div style="margin-bottom: 2rem; position: relative;">
<img src="/assets/party-icons/party-hat-with-horn.png" alt="Party horn" style="position: absolute; top: -10px; right: 20px; width: 70px; height: 70px; transform: rotate(-15deg); opacity: 0.9;">
<h1 style="font-size: 2.5rem; font-weight: 700; margin-bottom: 0.5rem;">Our Services</h1>
<p style="color: hsl(var(--muted-foreground)); font-size: 1.125rem;">Browse our decoration packages and services</p>
</div>
<div class="grid grid-cols-2">
{{ range .Services }}
<div class="card">
<div class="card-header">
<h3 class="card-title">{{ .Name }}</h3>
{{ if .ImageUrl.Valid }}
<div style="display: flex; align-items: center; gap: 0.5rem; margin-top: 0.5rem;">
<span class="badge" style="background: hsl(var(--accent));">Featured</span>
</div>
{{ end }}
</div>
<div class="card-content">
<p>{{ .Description }}</p>
{{ if .ImageUrl.Valid }}
<img src="{{ .ImageUrl.String }}" alt="{{ .Name }}" style="width: 100%; height: 200px; object-fit: cover; border-radius: var(--radius); margin-top: 1rem;">
{{ end }}
</div>
<div class="card-footer">
<a href="/booking" class="btn btn-primary">Book This Service</a>
<a href="#" class="btn btn-outline">Learn More</a>
</div>
</div>
{{ else }}
<div class="card" style="grid-column: 1 / -1; text-align: center; padding: 3rem;">
<p style="color: hsl(var(--muted-foreground)); font-size: 1.125rem;">No services available yet. Check back soon!</p>
</div>
{{ end }}
</div>
</main>
{{template "footer"}}
{{template "theme-script"}}
</body>
</html>
{{end}}

View File

@@ -0,0 +1,53 @@
{{define "gallery.tmpl"}}
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>See Our Work - Decor By Hannahs</title>
<link rel="stylesheet" href="/static/css/styles.css">
<script>
(function() {
const savedTheme = localStorage.getItem('theme') || 'light';
document.documentElement.setAttribute('data-theme', savedTheme);
})();
</script>
<script src="/static/js/alpine.js" defer></script>
<script src="/static/htmx/htmx.min.js" defer></script>
<script src="/static/js/drawer.js" defer></script>
</head>
<body>
{{template "header" .}}
<main>
<div style="margin-bottom: 2rem; position: relative;">
<img src="/assets/party-icons/happy-sun-wearing-party-hat.png" alt="Happy sun" style="position: absolute; top: -15px; left: 260px; width: 80px; height: 80px; opacity: 0.9;">
<img src="/assets/party-icons/jester-hat.png" alt="Jester hat" style="position: absolute; top: -10px; right: 30px; width: 65px; height: 65px; transform: rotate(25deg); opacity: 0.85;">
<h1 style="font-size: 2.5rem; font-weight: 700; margin-bottom: 0.5rem;">See Our Work</h1>
<p style="color: hsl(var(--muted-foreground)); font-size: 1.125rem;">Browse our recent decoration projects</p>
</div>
<div class="grid grid-cols-3">
{{ range .Photos }}
<div class="card">
<img src="{{ .URL }}" alt="{{ if .Caption }}{{ .Caption }}{{ else }}Gallery photo{{ end }}" style="width: 100%; height: 250px; object-fit: cover; border-radius: var(--radius);">
{{ if .Caption }}
<div class="card-content">
<p>{{ .Caption }}</p>
</div>
{{ end }}
</div>
{{ else }}
<div class="card" style="grid-column: 1 / -1; text-align: center; padding: 3rem;">
<p style="color: hsl(var(--muted-foreground)); font-size: 1.125rem;">No photos available yet. Check back soon!</p>
</div>
{{ end }}
</div>
</main>
{{template "footer"}}
{{template "theme-script"}}
</body>
</html>
{{end}}

176
internal/tmpl/home.tmpl Normal file
View File

@@ -0,0 +1,176 @@
{{define "home.tmpl"}}
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Home - Decor By Hannahs</title>
<link rel="stylesheet" href="/static/css/styles.css">
<script>
(function() {
const savedTheme = localStorage.getItem('theme') || 'light';
document.documentElement.setAttribute('data-theme', savedTheme);
})();
</script>
<script src="/static/js/alpine.js" defer></script>
<script src="/static/htmx/htmx.min.js" defer></script>
<script src="/static/js/drawer.js" defer></script>
</head>
<body>
{{template "header" .}}
<main>
<div class="hero" style="position: relative;">
<img src="/assets/party-icons/penguin-wearing-party-hat.png" alt="Party penguin" style="position: absolute; top: 20px; right: 10%; width: 100px; height: 100px; opacity: 0.8; animation: float 3s ease-in-out infinite;">
<img src="/assets/party-icons/jester-hat.png" alt="Jester hat" style="position: absolute; bottom: 20px; left: 10%; width: 80px; height: 80px; opacity: 0.8; animation: float 4s ease-in-out infinite; animation-delay: 1s;">
<h1>Welcome to Decor By Hannah's</h1>
<p>We take care of all the headache for your party decorations so you don't have to</p>
<div style="display: flex; gap: 1rem; justify-content: center; margin-top: 2rem; flex-wrap: wrap;">
<a href="/catalog" class="btn btn-primary">Browse Services</a>
<a href="/booking" class="btn btn-outline">Book Now</a>
</div>
</div>
<style>
@keyframes float {
0%, 100% { transform: translateY(0px); }
50% { transform: translateY(-20px); }
}
</style>
<div class="grid grid-cols-3" style="margin-top: 4rem;">
<div class="card" style="position: relative; overflow: visible;">
<img src="/assets/party-icons/party-hat.png" alt="Party hat" style="position: absolute; top: -50px; left: -45px; width: 80px; height: 80px; transform: rotate(-25deg); z-index: 10;"> <div class="card-header">
<h3 class="card-title">Professional Setup</h3>
<p class="card-description">Expert decoration services</p>
</div>
<div class="card-content">
<p>Our experienced team handles everything from setup to teardown, ensuring your event looks perfect.</p>
</div>
</div>
<div class="card" style="position: relative; overflow: visible;">
<img src="/assets/party-icons/birthday-cake.png" alt="Birthday cake" style="position: absolute; bottom: -15px; left: -15px; width: 70px; height: 70px; z-index: 10;">
<div class="card-header">
<h3 class="card-title">Custom Themes</h3>
<p class="card-description">Tailored to your vision</p>
</div>
<div class="card-content">
<p>Choose from our curated themes or work with us to create something uniquely yours.</p>
</div>
</div>
<div class="card" style="position: relative; overflow: visible;">
<img src="/assets/party-icons/happy-sun-wearing-party-hat.png" alt="Happy sun" style="position: absolute; top: -25px; right: -10px; width: 90px; height: 90px; z-index: 10;">
<div class="card-header">
<h3 class="card-title">Stress-Free Planning</h3>
<p class="card-description">We handle the details</p>
</div>
<div class="card-content">
<p>Focus on enjoying your event while we take care of all the decoration logistics.</p>
</div>
</div>
</div>
{{ if .Photos }}
<div style="margin-top: 4rem;">
<h2 style="font-size: 2rem; font-weight: 700; text-align: center; margin-bottom: 2rem;">See Our Work</h2>
<div class="carousel" id="carousel">
<div class="carousel-container">
<div class="carousel-track" id="carouselTrack">
{{ range .Photos }}
<div class="carousel-slide">
<img src="{{ .URL }}" alt="{{ if .Caption }}{{ .Caption }}{{ else }}Gallery photo{{ end }}" style="width: 100%; height: 500px; object-fit: cover; border-radius: var(--radius);">
{{ if .Caption }}
<p style="text-align: center; margin-top: 1rem; color: hsl(var(--muted-foreground));">{{ .Caption }}</p>
{{ end }}
</div>
{{ end }}
</div>
</div>
<button class="carousel-btn carousel-btn-prev" id="prevBtn" aria-label="Previous slide">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"></polyline></svg>
</button>
<button class="carousel-btn carousel-btn-next" id="nextBtn" aria-label="Next slide">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"></polyline></svg>
</button>
<div class="carousel-indicators" id="indicators"></div>
</div>
</div>
{{ end }}
</main>
{{template "footer"}}
{{template "theme-script"}}
<script>
(function() {
const track = document.getElementById('carouselTrack');
if (!track) return;
const prevBtn = document.getElementById('prevBtn');
const nextBtn = document.getElementById('nextBtn');
const indicators = document.getElementById('indicators');
const slides = track.querySelectorAll('.carousel-slide');
const totalSlides = slides.length;
let currentSlide = 0;
let autoplayInterval;
if (totalSlides === 0) return;
for (let i = 0; i < totalSlides; i++) {
const btn = document.createElement('button');
btn.className = 'carousel-indicator';
btn.setAttribute('aria-label', `Go to slide ${i + 1}`);
btn.addEventListener('click', () => goToSlide(i));
indicators.appendChild(btn);
}
function updateCarousel() {
track.style.transform = `translateX(-${currentSlide * 100}%)`;
const indicatorBtns = indicators.querySelectorAll('.carousel-indicator');
indicatorBtns.forEach((btn, i) => {
btn.classList.toggle('active', i === currentSlide);
});
}
function goToSlide(index) {
currentSlide = index;
updateCarousel();
resetAutoplay();
}
function nextSlide() {
currentSlide = (currentSlide + 1) % totalSlides;
updateCarousel();
}
function prevSlide() {
currentSlide = (currentSlide - 1 + totalSlides) % totalSlides;
updateCarousel();
resetAutoplay();
}
function startAutoplay() {
autoplayInterval = setInterval(nextSlide, 5000);
}
function resetAutoplay() {
clearInterval(autoplayInterval);
startAutoplay();
}
prevBtn.addEventListener('click', prevSlide);
nextBtn.addEventListener('click', () => {
nextSlide();
resetAutoplay();
});
updateCarousel();
startAutoplay();
})();
</script>
</body>
</html>
{{end}}

124
internal/tmpl/layout.tmpl Normal file
View File

@@ -0,0 +1,124 @@
{{define "layout.tmpl"}}
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{block "title" .}}Decor By Hannahs{{end}}</title>
<link rel="stylesheet" href="/static/css/styles.css">
<script src="/static/js/alpine.js" defer></script>
<script src="/static/htmx/htmx.min.js" defer></script>
<script src="/static/js/drawer.js" defer></script>
<link rel="icon" href="/assets/balloon.png" type="image/png">
</head>
<body>
<!-- Navigation Drawer -->
<div id="nav-drawer" class="drawer">
<div class="drawer-overlay"></div>
<div class="drawer-content">
<div class="drawer-header">
<div class="drawer-title">Menu</div>
<button class="drawer-close" aria-label="Close menu">
<svg class="drawer-nav-icon"><use href="/static/icons.svg#icon-x"/></svg>
</button>
</div>
<div class="drawer-body">
<nav class="drawer-nav">
<a href="/" {{block "nav-home-active" .}}{{end}}>
<svg class="drawer-nav-icon"><use href="/static/icons.svg#icon-home"/></svg>
Home
</a>
<a href="/catalog" {{block "nav-catalog-active" .}}{{end}}>
<svg class="drawer-nav-icon"><use href="/static/icons.svg#icon-catalog"/></svg>
Catalog
</a>
<a href="/gallery" {{block "nav-gallery-active" .}}{{end}}>
<svg class="drawer-nav-icon"><use href="/static/icons.svg#icon-image"/></svg>
Gallery
</a>
<a href="/booking" {{block "nav-booking-active" .}}{{end}}>
<svg class="drawer-nav-icon"><use href="/static/icons.svg#icon-calendar"/></svg>
Book
</a>
<a href="/profile" {{block "nav-profile-active" .}}{{end}}>
<svg class="drawer-nav-icon"><use href="/static/icons.svg#icon-user"/></svg>
Profile
</a>
</nav>
</div>
</div>
</div>
<!-- Header -->
<header class="header-bar">
<div style="display: flex; align-items: center; gap: 1rem;">
<button class="menu-btn" onclick="window.navDrawer?.toggle()" aria-label="Toggle menu">
<div class="menu-icon">
<span></span>
<span></span>
<span></span>
</div>
</button>
<a href="/" class="header-brand">
<img src="/assets/balloon.png" alt="Decor By Hannah's">
Decor By Hannah's
</a>
</div>
<div style="display: flex; align-items: center; gap: 1rem;">
<nav class="header-nav">
<a href="/" {{block "header-home-active" .}}{{end}}>Home</a>
<a href="/catalog" {{block "header-catalog-active" .}}{{end}}>Catalog</a>
<a href="/gallery" {{block "header-gallery-active" .}}{{end}}>Gallery</a>
<a href="/booking" {{block "header-booking-active" .}}{{end}}>Book</a>
<a href="/profile" {{block "header-profile-active" .}}{{end}}>Profile</a>
</nav>
<button id="themeToggle" class="btn btn-outline" aria-label="Toggle theme">
<svg id="sunIcon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"></circle><line x1="12" y1="1" x2="12" y2="3"></line><line x1="12" y1="21" x2="12" y2="23"></line><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line><line x1="1" y1="12" x2="3" y2="12"></line><line x1="21" y1="12" x2="23" y2="12"></line><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line></svg>
<svg id="moonIcon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display: none;"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path></svg>
</button>
</div>
</header>
<main>
{{block "content" .}}{{end}}
</main>
<footer>
<small>© 2025 Decor By Hannah's. All rights reserved.</small>
</footer>
<script>
(function() {
const themeToggle = document.getElementById('themeToggle');
const sunIcon = document.getElementById('sunIcon');
const moonIcon = document.getElementById('moonIcon');
const html = document.documentElement;
const savedTheme = localStorage.getItem('theme') || 'light';
html.setAttribute('data-theme', savedTheme);
updateIcons(savedTheme);
function updateIcons(theme) {
if (theme === 'dark') {
sunIcon.style.display = 'none';
moonIcon.style.display = 'block';
} else {
sunIcon.style.display = 'block';
moonIcon.style.display = 'none';
}
}
themeToggle.addEventListener('click', () => {
const currentTheme = html.getAttribute('data-theme');
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
html.setAttribute('data-theme', newTheme);
localStorage.setItem('theme', newTheme);
updateIcons(newTheme);
});
})();
</script>
{{block "scripts" .}}{{end}}
</body>
</html>
{{end}}

View File

@@ -0,0 +1,163 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Redirecting - Decor By Hannah's</title>
<link rel="stylesheet" href="/static/css/styles.css">
<script src="/static/htmx/htmx.min.js"></script>
<link rel="icon" href="/assets/balloon.png" type="image/png">
<style>
.toast {
position: fixed;
top: 20px;
right: 20px;
background: var(--primary-color);
color: white;
padding: 1rem 1.5rem;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
display: flex;
align-items: center;
gap: 0.75rem;
min-width: 300px;
animation: slideIn 0.3s ease-out;
z-index: 9999;
}
.toast.success {
background: #10b981;
}
.toast.info {
background: #3b82f6;
}
.toast.warning {
background: #f59e0b;
}
.toast.error {
background: #ef4444;
}
.toast-icon {
width: 24px;
height: 24px;
flex-shrink: 0;
}
.toast-message {
flex: 1;
font-weight: 500;
}
@keyframes slideIn {
from {
transform: translateX(400px);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
@keyframes slideOut {
from {
transform: translateX(0);
opacity: 1;
}
to {
transform: translateX(400px);
opacity: 0;
}
}
.toast.hiding {
animation: slideOut 0.3s ease-in forwards;
}
.redirect-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
text-align: center;
padding: 2rem;
}
.redirect-content {
max-width: 500px;
}
.spinner {
width: 50px;
height: 50px;
border: 4px solid rgba(0, 0, 0, 0.1);
border-left-color: var(--primary-color);
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 2rem;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
</style>
</head>
<body>
<div id="toast-container"></div>
<div class="redirect-container">
<div class="redirect-content">
<div class="spinner"></div>
<h1>Redirecting...</h1>
<p>You will be redirected to the home page shortly.</p>
</div>
</div>
<script>
(function() {
function showToast(message, type) {
const container = document.getElementById('toast-container');
const icons = {
success: '<svg class="toast-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path></svg>',
info: '<svg class="toast-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>',
warning: '<svg class="toast-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path></svg>',
error: '<svg class="toast-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path></svg>'
};
const toast = document.createElement('div');
toast.className = `toast ${type}`;
toast.innerHTML = `
${icons[type] || icons.info}
<span class="toast-message">${message}</span>
`;
container.appendChild(toast);
setTimeout(() => {
toast.classList.add('hiding');
setTimeout(() => {
container.removeChild(toast);
}, 300);
}, 3000);
}
const message = {{.Message}};
const type = {{.Type}};
showToast(message, type);
setTimeout(() => {
window.location.href = '/';
}, 2000);
})();
</script>
</body>
</html>

147
internal/tmpl/profile.tmpl Normal file
View File

@@ -0,0 +1,147 @@
{{define "profile.tmpl"}}
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Profile - Decor By Hannahs</title>
<link rel="stylesheet" href="/static/css/styles.css">
<script>
(function() {
const savedTheme = localStorage.getItem('theme') || 'light';
document.documentElement.setAttribute('data-theme', savedTheme);
})();
</script>
<script src="/static/js/alpine.js" defer></script>
<script src="/static/htmx/htmx.min.js" defer></script>
<script src="/static/js/drawer.js" defer></script>
<style>
.status-badge {
display: inline-block;
padding: 0.25rem 0.75rem;
border-radius: 9999px;
font-size: 0.875rem;
font-weight: 500;
}
.status-pending {
background: hsl(var(--warning) / 0.1);
color: hsl(var(--warning));
}
.status-confirmed {
background: hsl(var(--success) / 0.1);
color: hsl(var(--success));
}
.status-completed {
background: hsl(var(--muted) / 0.5);
color: hsl(var(--muted-foreground));
}
.status-cancelled {
background: hsl(var(--destructive) / 0.1);
color: hsl(var(--destructive));
}
.booking-card {
border: 1px solid hsl(var(--border));
border-radius: 8px;
padding: 1rem;
margin-bottom: 1rem;
transition: box-shadow 0.2s;
}
.booking-card:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
</style>
</head>
<body>
{{template "header" .}}
<main>
<div style="max-width: 1000px; margin: 0 auto;">
<div style="margin-bottom: 2rem;">
<h1 style="font-size: 2.5rem; font-weight: 700; margin-bottom: 0.5rem;">Welcome, {{.DisplayName}}</h1>
<p style="color: hsl(var(--muted-foreground)); font-size: 1.125rem;">Manage your account and bookings</p>
</div>
<div class="grid grid-cols-1" style="gap: 1.5rem;">
<div class="card">
<div class="card-header">
<h3 class="card-title">Account Information</h3>
<p class="card-description">Your personal details</p>
</div>
<div class="card-content">
<div style="display: flex; flex-direction: column; gap: 1rem;">
<div>
<strong>Email:</strong> {{.Email}}
</div>
<div style="display: flex; gap: 1rem; flex-wrap: wrap;">
<a href="{{.OrySettingsURL}}" class="btn btn-outline">Account Settings</a>
<a href="/logout" class="btn btn-outline" style="color: hsl(var(--destructive)); border-color: hsl(var(--destructive));">Logout</a>
</div>
</div>
</div>
</div>
<div class="card">
<div class="card-header">
<h3 class="card-title">Your Bookings</h3>
<p class="card-description">View and manage your service bookings</p>
</div>
<div class="card-content">
{{if .Bookings}}
{{range .Bookings}}
<div class="booking-card">
<div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 0.75rem;">
<div>
<h4 style="font-size: 1.25rem; font-weight: 600; margin-bottom: 0.25rem;">{{.ServiceName}}</h4>
<p style="color: hsl(var(--muted-foreground)); font-size: 0.875rem;">Booked on {{.CreatedAt}}</p>
</div>
<span class="status-badge status-{{.Status}}">{{.Status}}</span>
</div>
<div style="display: grid; gap: 0.5rem; margin-bottom: 0.75rem;">
<div style="display: flex; gap: 0.5rem;">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect>
<line x1="16" y1="2" x2="16" y2="6"></line>
<line x1="8" y1="2" x2="8" y2="6"></line>
<line x1="3" y1="10" x2="21" y2="10"></line>
</svg>
<span style="font-size: 0.875rem;">Event Date: <strong>{{.EventDate}}</strong></span>
</div>
{{if .Address}}
<div style="display: flex; gap: 0.5rem;">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"></path>
<circle cx="12" cy="10" r="3"></circle>
</svg>
<span style="font-size: 0.875rem;">{{.Address}}</span>
</div>
{{end}}
<div style="display: flex; gap: 0.5rem;">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="12" y1="1" x2="12" y2="23"></line>
<path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"></path>
</svg>
<span style="font-size: 0.875rem;">Price: <strong>${{printf "%.2f" (divf .ServicePriceCents 100)}}</strong></span>
</div>
</div>
{{if .Notes}}
<div style="padding: 0.75rem; background: hsl(var(--muted) / 0.3); border-radius: 6px; font-size: 0.875rem;">
<strong>Notes:</strong> {{.Notes}}
</div>
{{end}}
</div>
{{end}}
{{else}}
<p style="color: hsl(var(--muted-foreground));">No bookings yet. <a href="/booking" style="color: hsl(var(--primary)); text-decoration: underline;">Book your first service</a></p>
{{end}}
</div>
</div>
</div>
</div>
</main>
{{template "footer"}}
{{template "theme-script"}}
</body>
</html>
{{end}}