feat(account): add editable phone and password change with validation

- Add editable phone field in /account General tab with UK phone
  validation
- Create PUT /api/user/change-password endpoint in backend
- Add zxcvbn password strength meter to change password modal
- Add "passwords don't match" validation message to both /account and
  /register
- Fix navbar logout reactivity with invalidateAll and $derived values
- Fix a11y warnings: add labels, roles, and keyboard handlers
- Remove unused CSS from account page
This commit is contained in:
2026-02-20 20:17:38 +00:00
parent 41dc839830
commit 5a4cd29b44
7 changed files with 285 additions and 141 deletions
@@ -416,27 +416,14 @@
</AlertDialog.Root>
<style>
/* Hide number input arrows for Chrome, Safari, Edge */
input.no-spin::-webkit-outer-spin-button,
input.no-spin::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
/* Hide number input arrows for Firefox */
input.no-spin[type='number'] {
/* Hide number input arrows for all number inputs in the component */
:global(input[type='number']) {
-moz-appearance: textfield;
appearance: textfield;
}
/* Remove arrows from all number inputs in the component */
input[type='number'] {
-moz-appearance: textfield;
appearance: textfield;
}
input[type='number']::-webkit-outer-spin-button,
input[type='number']::-webkit-inner-spin-button {
:global(input[type='number']::-webkit-outer-spin-button),
:global(input[type='number']::-webkit-inner-spin-button) {
-webkit-appearance: none;
margin: 0;
}
@@ -440,7 +440,7 @@
</div>
<div class="space-y-1">
<label class="text-sm font-medium text-gray-700">Tags</label>
<label for="tag-input" class="text-sm font-medium text-gray-700">Tags</label>
<div class="relative">
<div
@@ -494,6 +494,9 @@
? 'bg-primary/10 text-primary font-medium'
: 'hover:bg-gray-100'}"
onclick={() => selectSuggestion(s)}
onkeydown={(e) => (e.key === 'Enter' || e.key === ' ') && selectSuggestion(s)}
role="button"
tabindex="0"
>
{s}
</div>
@@ -16,19 +16,21 @@
{ href: '/today', label: 'Today', showWhen: 'admin', width: 'w-28' }
];
let mobileMenuOpen: boolean = false;
let mobileMenuOpen = $state(false);
function toggleMenu() {
mobileMenuOpen = !mobileMenuOpen;
}
// Close mobile menu when navigation starts
$: if ($navigating) {
mobileMenuOpen = false;
}
$effect(() => {
if ($navigating) {
mobileMenuOpen = false;
}
});
// Helper to decide visibility
// Helper to decide visibility - use $derived for reactivity
const canShow = (link: { href: string; label: string; showWhen: string; width: string }) => {
if (authStore.isLoading) return true; // keep skeleton placeholders
if (authStore.isLoading) return true;
// hide booking link for admins
if (link.href === '/book' && authStore.currentUser?.role === 'admin') return false;
@@ -40,6 +42,11 @@
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
@@ -80,7 +87,7 @@
<div class="hidden space-x-8 md:flex">
{#each links as link}
{#if canShow(link)}
{#if authStore.isLoading}
{#if isLoading}
<Skeleton class={`h-4 ${link.width} rounded`} />
{:else}
<a href={link.href} class="font-medium text-gray-800 hover:text-primary"
@@ -93,17 +100,17 @@
<!-- Desktop Login Button -->
<div class="hidden items-center md:flex">
{#if !authStore.isLoading && !authStore.isAuthenticated && $page.url.pathname !== '/login'}
{#if !isLoading && !isAuthenticated && $page.url.pathname !== '/login'}
<Button href="/login">Login</Button>
{/if}
{#if authStore.isLoading}
{#if isLoading}
<Skeleton class="h-8 w-16 rounded" />
{/if}
</div>
<!-- Mobile: Burger -->
<div class="flex items-center md:hidden">
<button on:click={toggleMenu} class="focus:outline-none" aria-label="Toggle menu">
<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"
@@ -123,7 +130,7 @@
<div class="space-y-1 px-2 pt-2 pb-3">
{#each links as link}
{#if canShow(link)}
{#if authStore.isLoading}
{#if isLoading}
<Skeleton class="h-4 w-full rounded" />
{:else}
<a
@@ -136,9 +143,9 @@
{/if}
{/each}
{#if authStore.isLoading}
{#if isLoading}
<Skeleton class="mt-2 h-8 w-full rounded" />
{:else if !authStore.isAuthenticated}
{:else if !isAuthenticated}
<Button href="/login" class="mt-2 w-full text-center">Login</Button>
{/if}
</div>