Files
Crussell/frontend/src/lib/components/layout/NavBar.svelte
T
popertots 970cc5554d feat: add email verification, profile pictures, deposits, and calendar
export
Backend:
- Add email verification code generation and verification endpoints
- Add profile picture upload with S3 storage and image processing
- Add deposit_required field to users with 48h advance booking
  requirement
- Add loyalty stamps that accumulate on completed bookings
- Auto-transition bookings: confirmed → in_progress → completed
- Add booking cancellation handler with no-show detection
- Add ICS calendar file download endpoint for bookings
- Sync bookings to CalDAV on confirmation
  Frontend:
- Add schedule page route
- Add avatar and image-cropper UI components
- Update shadcn-svelte components (button, dialog)
- Add "Add to Calendar" button in booking modal
  Database:
- Add verification_codes table
- Add profile_pic_url, loyalty_stamps, deposits_required to users
- Various schema updates
2026-02-21 18:48:29 +00:00

162 lines
4.9 KiB
Svelte

<script lang="ts">
import { Button } from '$lib/components/ui/button';
import { Skeleton } from '$lib/components/ui/skeleton';
import { navigating, page } from '$app/stores';
import { authStore } from '$lib/stores/auth.svelte';
// Centralized link definition
const links = [
{ href: '/', label: 'Home', showWhen: 'always', width: 'w-12' },
{ href: '/prices', label: 'Price List', showWhen: 'guest', width: 'w-20' },
{ href: '/schedule', label: 'My Schedule', showWhen: 'auth', width: 'w-24' },
{ href: '/book', label: 'Book an appointment', showWhen: 'auth', width: 'w-36' },
{ href: '/portfolio', label: 'Portfolio', showWhen: 'always', width: 'w-20' },
{ href: '/admin', label: 'Admin Dashboard', showWhen: 'admin', width: 'w-28' },
{ href: '/today', label: 'Today', showWhen: 'admin', width: 'w-28' },
{ href: '/contact', label: 'Contact', showWhen: 'always', width: 'w-16' },
{ href: '/account', label: 'My Account', showWhen: 'auth', width: 'w-24' }
];
let mobileMenuOpen = $state(false);
function toggleMenu() {
mobileMenuOpen = !mobileMenuOpen;
}
// Close mobile menu when navigation starts
$effect(() => {
if ($navigating) {
mobileMenuOpen = false;
}
});
// Helper to decide visibility - use $derived for reactivity
const canShow = (link: { href: string; label: string; showWhen: string; width: string }) => {
if (authStore.isLoading) return true;
// hide booking link for admins
if (link.href === '/book' && authStore.currentUser?.role === 'admin') return false;
if (link.href === '/contact' && authStore.currentUser?.role === 'admin') return false;
if (link.showWhen === 'always') return true;
if (link.showWhen === 'guest' && !authStore.isAuthenticated) return true;
if (link.showWhen === 'auth' && authStore.isAuthenticated) return true;
if (link.showWhen === 'admin' && authStore.currentUser?.role === 'admin') return true;
return false;
};
// Derived values for auth state - ensures reactivity
let isAuthenticated = $derived(authStore.isAuthenticated);
let currentUserRole = $derived(authStore.currentUser?.role);
let isLoading = $derived(authStore.isLoading);
</script>
<nav
class="fixed top-0 left-0 z-50 w-full border-b border-gray-200"
class:frosty-nav={!mobileMenuOpen}
class:bg-background={mobileMenuOpen}
>
<div class="mx-auto max-w-7xl px-4 md:px-6 lg:px-8">
<div class="flex h-16 items-center justify-between">
<!-- Left: Social Icons -->
<div class="flex items-center space-x-4">
<a
href="https://instagram.com"
target="_blank"
rel="noopener"
class="text-gray-600 hover:text-primary"
aria-label="Instagram"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<rect x="2" y="2" width="20" height="20" rx="5" ry="5"></rect>
<path d="M16 11.37A4 4 0 1 1 12.63 8 4 4 0 0 1 16 11.37z"></path>
<line x1="17.5" y1="6.5" x2="17.51" y2="6.5"></line>
</svg>
</a>
</div>
<!-- Center: Desktop Links -->
<div class="hidden space-x-8 md:flex">
{#each links as link}
{#if canShow(link)}
{#if isLoading}
<Skeleton class={`h-4 ${link.width} rounded`} />
{:else}
<a href={link.href} class="font-medium text-gray-800 hover:text-primary"
>{link.label}</a
>
{/if}
{/if}
{/each}
</div>
<!-- Desktop Login Button -->
<div class="hidden items-center md:flex">
{#if !isLoading && !isAuthenticated && $page.url.pathname !== '/login'}
<Button href="/login">Login</Button>
{/if}
{#if isLoading}
<Skeleton class="h-8 w-16 rounded" />
{/if}
</div>
<!-- Mobile: Burger -->
<div class="flex items-center md:hidden">
<button onclick={toggleMenu} class="focus:outline-none" aria-label="Toggle menu">
<svg class="h-6 w-6 text-gray-700" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 6h16M4 12h16M4 18h16"
/>
</svg>
</button>
</div>
</div>
</div>
<!-- Mobile Menu -->
{#if mobileMenuOpen}
<div class="border-b border-gray-200 bg-background md:hidden">
<div class="space-y-1 px-2 pt-2 pb-3">
{#each links as link}
{#if canShow(link)}
{#if isLoading}
<Skeleton class="h-4 w-full rounded" />
{:else}
<a
href={link.href}
class="block rounded px-3 py-2 text-center text-primary hover:text-gray-800"
>
{link.label}
</a>
{/if}
{/if}
{/each}
{#if isLoading}
<Skeleton class="mt-2 h-8 w-full rounded" />
{:else if !isAuthenticated}
<Button href="/login" class="mt-2 w-full text-center">Login</Button>
{/if}
</div>
</div>
{/if}
</nav>
<style>
.frosty-nav {
backdrop-filter: saturate(180%) blur(10px);
}
</style>