Files
Crussell/frontend/src/routes/prices/+page.svelte
T

229 lines
7.3 KiB
Svelte
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script lang="ts">
import { resolve } from '$app/paths';
import * as Card from '$lib/components/ui/card/index.js';
import { Button } from '$lib/components/ui/button/index.js';
import { Skeleton } from '$lib/components/ui/skeleton/index.js';
import { toast } from 'svelte-sonner';
import { authStore } from '$lib/stores/auth.svelte';
import { formatDuration } from '$lib/utils/format';
type Service = {
id: string;
name: string;
description: string;
price: number;
duration_minutes: number;
patch_test_duration_hours: number;
minimum_age_required: number;
};
let services = $state<Service[]>([]);
let servicesLoading = $state(true);
async function fetchServices() {
servicesLoading = true;
try {
const response = await fetch('/api/services', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${authStore.currentToken}`
}
});
if (response.ok) {
const data: Service[] = await response.json();
services = data;
} else {
console.error('Failed to fetch services:', response.status);
toast.error('Failed to load services');
services = [];
}
} catch (err) {
console.error('Error fetching services:', err);
toast.error('Network error loading services');
services = [];
} finally {
servicesLoading = false;
}
}
function formatPrice(price: number): string {
return ${price.toFixed(2)}`;
}
$effect(() => {
fetchServices();
});
</script>
<div class="mx-auto max-w-4xl space-y-8 p-4 md:p-6">
<div class="text-center">
<h1 class="font-['Playfair_Display'] text-4xl font-bold">Our Prices</h1>
<p class="mt-2 text-gray-600">Professional beauty treatments with transparent pricing</p>
</div>
{#if servicesLoading}
<div class="space-y-4">
{#each Array(5) as _, i (i)}
<Card.Root class="shadow-sm">
<Card.Content>
<div class="flex items-start justify-between">
<div class="flex-1 space-y-2">
<Skeleton class="h-5 w-40" />
<Skeleton class="h-4 w-64" />
<Skeleton class="h-4 w-24" />
</div>
<div class="space-y-2 text-right">
<Skeleton class="ml-auto h-6 w-20" />
<Skeleton class="ml-auto h-4 w-16" />
</div>
</div>
</Card.Content>
</Card.Root>
{/each}
</div>
{:else if services.length === 0}
<Card.Root class="shadow-sm">
<Card.Content>
<div class="py-12 text-center">
<div class="mb-4 text-lg text-gray-600">No services available at the moment.</div>
<Button href={resolve('/contact')} variant="outline">Contact Us</Button>
</div>
</Card.Content>
</Card.Root>
{:else}
<div class="space-y-4">
{#each services as service (service.id)}
<Card.Root class="shadow-sm transition-all duration-200 hover:shadow-md">
<Card.Content>
<div class="flex items-start justify-between gap-4">
<div class="min-w-0 flex-1">
<h4 class="text-lg font-semibold text-foreground">{service.name}</h4>
<p class="mt-1 text-sm text-muted-foreground">{service.description}</p>
<div class="mt-3 flex flex-wrap gap-2">
{#if service.patch_test_duration_hours > 0}
<span
class="inline-flex items-center gap-1.5 rounded-full bg-amber-100 px-3 py-1 text-xs font-medium text-muted-foreground"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-3.5 w-3.5"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fill-rule="evenodd"
d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z"
clip-rule="evenodd"
/>
</svg>
Patch test required {service.patch_test_duration_hours}h before
</span>
{/if}
{#if service.minimum_age_required > 0}
<span
class="inline-flex items-center gap-1.5 rounded-full bg-amber-100 px-3 py-1 text-xs font-medium text-muted-foreground"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-3.5 w-3.5"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fill-rule="evenodd"
d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z"
clip-rule="evenodd"
/>
</svg>
Minimum age: {service.minimum_age_required} years
</span>
{/if}
</div>
</div>
<div class="shrink-0 text-right">
<div class="text-lg font-semibold text-primary">{formatPrice(service.price)}</div>
<div class="text-sm text-muted-foreground">
{formatDuration(service.duration_minutes)}
</div>
</div>
</div>
</Card.Content>
</Card.Root>
{/each}
</div>
<Card.Root class="border-dashed border-gray-200 bg-gray-50/50 shadow-sm">
<Card.Content class="p-6 text-center">
<p class="text-sm text-gray-600">
Need something different? Select the closest service and add a note, or contact us for a bespoke treatment.
</p>
<a href={resolve('/contact')} class="mt-1 inline-block text-sm font-medium text-blue-600 hover:underline">
Arrange a custom booking →
</a>
</Card.Content>
</Card.Root>
<Card.Root class="border-gray-200 bg-gray-50/80 shadow-sm">
<Card.Header>
<Card.Title class="flex items-center gap-2 text-gray-700">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fill-rule="evenodd"
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z"
clip-rule="evenodd"
/>
</svg>
Important Information
</Card.Title>
</Card.Header>
<Card.Content>
<ul class="space-y-2 text-sm text-gray-600">
<li>
<span class="font-medium">Prices and durations</span> are estimates and may vary based on
individual requirements
</li>
<li>
<span class="font-medium">Patch tests</span> are required 2448 hours before certain treatments
— look for the amber badge above
</li>
<li>
<span class="font-medium">24 hours notice</span> required for cancellations but do get in
contact with us directly for exceptional circumstances
</li>
<li>
<span class="font-medium">Payment</span> is due at the time of service
</li>
<li>
<span class="font-medium">Deposit</span> may be required for frequent no-shows
</li>
</ul>
</Card.Content>
</Card.Root>
<Card.Root class="bg-gradient-to-br from-primary/5 to-secondary/30 shadow-sm">
<Card.Content class="p-6 text-center md:p-8">
<h2 class="mb-4 text-lg font-semibold text-primary md:text-xl">Ready to Book?</h2>
<div class="flex flex-col gap-3 md:flex-row md:justify-center md:gap-4">
<Button href={resolve('/book')} class="px-6 py-3 md:px-8">Book Appointment</Button>
<Button
href={resolve('/contact')}
variant="outline"
class="border-primary/30 px-6 py-3 hover:bg-primary/10 md:px-8"
>
Get in Touch
</Button>
</div>
</Card.Content>
</Card.Root>
{/if}
</div>