fix(frontend): Svelte 5 reactivity, each block keys, and path resolution
Add proper keys to #each blocks across 15+ components to fix reordering bugs. Replace new Date() with SvelteDate in reactive contexts. Use $derived for computed values (totalPages). Use resolve() from $app/paths for all internal navigation hrefs. Add ARIA labels and keyboard accessibility to NavBar mobile menu. Remove unused handleRetry from UserPaymentModal. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import { slide } from 'svelte/transition';
|
||||
import { slide, fly } from 'svelte/transition';
|
||||
import { navigating, page } from '$app/stores';
|
||||
import { goto } from '$app/navigation';
|
||||
import { resolve } from '$app/paths';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Skeleton } from '$lib/components/ui/skeleton';
|
||||
@@ -126,12 +126,12 @@
|
||||
|
||||
<!-- Center: Desktop Links -->
|
||||
<div class="hidden space-x-8 md:flex">
|
||||
{#each links as link}
|
||||
{#each links as link (link.href)}
|
||||
{#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"
|
||||
<a href={resolve(link.href)} class="font-medium text-gray-800 hover:text-primary"
|
||||
>{link.label}</a
|
||||
>
|
||||
{/if}
|
||||
@@ -142,7 +142,7 @@
|
||||
<div class="hidden items-center gap-4 md:flex">
|
||||
{#if isAuthenticated}
|
||||
<a
|
||||
href="/notifications"
|
||||
href={resolve('/notifications')}
|
||||
class="relative text-gray-600 hover:text-primary"
|
||||
aria-label="Notifications"
|
||||
>
|
||||
@@ -170,7 +170,7 @@
|
||||
</a>
|
||||
{/if}
|
||||
{#if !isLoading && !isAuthenticated && $page.url.pathname !== '/login'}
|
||||
<Button href="/login">Login</Button>
|
||||
<Button href={resolve('/login')}>Login</Button>
|
||||
{/if}
|
||||
{#if isLoading}
|
||||
<Skeleton class="h-8 w-16 rounded" />
|
||||
@@ -179,18 +179,19 @@
|
||||
|
||||
<!-- Mobile: Burger -->
|
||||
<div class="flex items-center md:hidden">
|
||||
<button onclick={toggleMenu} class="relative 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>
|
||||
{#if unreadCount > 0 && !mobileMenuOpen}
|
||||
<button
|
||||
onclick={toggleMenu}
|
||||
class="relative h-6 w-6 focus:outline-none"
|
||||
aria-label="Toggle menu"
|
||||
class:open={mobileMenuOpen}
|
||||
>
|
||||
<span class="hamburger-line"></span>
|
||||
<span class="hamburger-line"></span>
|
||||
<span class="hamburger-line"></span>
|
||||
<span class="hamburger-line"></span>
|
||||
{#if unreadCount > 0}
|
||||
<span
|
||||
class="absolute -top-1 -right-1.5 flex h-4 w-4 items-center justify-center rounded-full bg-red-500 text-[10px] font-bold text-white"
|
||||
class="notification-badge absolute -top-1 -right-1.5 flex h-4 w-4 items-center justify-center rounded-full bg-red-500 text-[10px] font-bold text-white"
|
||||
>
|
||||
{unreadCount > 9 ? '9+' : unreadCount}
|
||||
</span>
|
||||
@@ -203,7 +204,11 @@
|
||||
{#if mobileMenuOpen}
|
||||
<div
|
||||
class="fixed inset-0 top-16 bg-black/30 backdrop-blur-[1px] md:hidden"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
aria-label="Close menu"
|
||||
onclick={toggleMenu}
|
||||
onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); toggleMenu(); } }}
|
||||
></div>
|
||||
{/if}
|
||||
|
||||
@@ -214,13 +219,13 @@
|
||||
class="relative z-50 border-b border-gray-200 bg-background md:hidden"
|
||||
>
|
||||
<div class="space-y-1 px-2 pt-2 pb-3">
|
||||
{#each links as link}
|
||||
{#each links as link (link.href)}
|
||||
{#if canShow(link)}
|
||||
{#if isLoading}
|
||||
<Skeleton class="h-4 w-full rounded" />
|
||||
{:else}
|
||||
<a
|
||||
href={link.href}
|
||||
href={resolve(link.href)}
|
||||
class="block rounded px-3 py-2 text-center text-primary hover:text-gray-800"
|
||||
>
|
||||
{link.label}
|
||||
@@ -231,12 +236,13 @@
|
||||
|
||||
{#if isAuthenticated}
|
||||
<a
|
||||
href="/notifications"
|
||||
href={resolve('/notifications')}
|
||||
class="flex items-center justify-center gap-2 rounded px-3 py-2 text-primary hover:text-gray-800"
|
||||
>
|
||||
<span>Notifications</span>
|
||||
{#if unreadCount > 0}
|
||||
<span
|
||||
in:fly={{ y: -8, duration: 200, delay: 50 }}
|
||||
class="flex h-5 w-5 items-center justify-center rounded-full bg-red-500 text-[10px] font-bold text-white"
|
||||
>
|
||||
{unreadCount > 9 ? '9+' : unreadCount}
|
||||
@@ -248,7 +254,7 @@
|
||||
{#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>
|
||||
<Button href={resolve('/login')} class="mt-2 w-full text-center">Login</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
@@ -259,4 +265,70 @@
|
||||
.frosty-nav {
|
||||
backdrop-filter: saturate(180%) blur(10px);
|
||||
}
|
||||
|
||||
/* Hamburger animation - 4 lines, middle two overlap when closed */
|
||||
.hamburger-line {
|
||||
display: block;
|
||||
position: absolute;
|
||||
height: 2px;
|
||||
width: 100%;
|
||||
background: #374151;
|
||||
border-radius: 2px;
|
||||
opacity: 1;
|
||||
left: 0;
|
||||
transform: rotate(0deg);
|
||||
transition: 0.125s ease-in-out;
|
||||
}
|
||||
|
||||
.hamburger-line:nth-child(1) {
|
||||
top: 3px;
|
||||
}
|
||||
|
||||
/* Spans 2 and 3 overlap at same position, span 3 hidden when closed */
|
||||
.hamburger-line:nth-child(2) {
|
||||
top: 10px;
|
||||
}
|
||||
|
||||
.hamburger-line:nth-child(3) {
|
||||
top: 10px;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.hamburger-line:nth-child(4) {
|
||||
top: 17px;
|
||||
}
|
||||
|
||||
/* Open state - transform to X */
|
||||
.open .hamburger-line:nth-child(1) {
|
||||
top: 10px;
|
||||
width: 0%;
|
||||
left: 50%;
|
||||
}
|
||||
|
||||
.open .hamburger-line:nth-child(2) {
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
.open .hamburger-line:nth-child(3) {
|
||||
opacity: 1;
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
|
||||
.open .hamburger-line:nth-child(4) {
|
||||
top: 10px;
|
||||
width: 0%;
|
||||
left: 50%;
|
||||
}
|
||||
|
||||
/* Badge animations */
|
||||
.notification-badge {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
transition: opacity 0.2s ease, transform 0.2s ease;
|
||||
}
|
||||
|
||||
.open .notification-badge {
|
||||
opacity: 0;
|
||||
transform: translateY(8px);
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user