the rest
This commit is contained in:
110
internal/tmpl/_partials.tmpl
Normal file
110
internal/tmpl/_partials.tmpl
Normal 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
106
internal/tmpl/booking.tmpl
Normal 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}}
|
||||
64
internal/tmpl/catalog.tmpl
Normal file
64
internal/tmpl/catalog.tmpl
Normal 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}}
|
||||
53
internal/tmpl/gallery.tmpl
Normal file
53
internal/tmpl/gallery.tmpl
Normal 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
176
internal/tmpl/home.tmpl
Normal 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
124
internal/tmpl/layout.tmpl
Normal 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}}
|
||||
163
internal/tmpl/ory_redirect.tmpl
Normal file
163
internal/tmpl/ory_redirect.tmpl
Normal 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
147
internal/tmpl/profile.tmpl
Normal 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}}
|
||||
Reference in New Issue
Block a user