This commit is contained in:
2026-05-10 09:46:07 +12:00
parent cfc193b713
commit 2f2466ecac
81 changed files with 2571 additions and 413 deletions
+60 -2
View File
@@ -1,10 +1,13 @@
<script lang="ts">
import { api } from '$lib/api';
import { goto } from '$app/navigation';
import { clientSession, sessionHydrated } from '$lib/session';
import Skeleton from '$lib/components/Skeleton.svelte';
import type { DashboardSummary } from '$lib/types';
import { getWorkspaceHomeHref } from '$lib/workspace-access';
import packageInfo from '../../package.json';
import { Sunrise, Sun, Sunset, Moon } from 'lucide-svelte';
import { tick } from 'svelte';
type Segment = {
label: string;
@@ -32,8 +35,11 @@
let email = $state('');
let password = $state('');
let isLoggingIn = $state(false);
let postLoginRedirecting = $state(false);
let loginError = $state('');
let passwordInput: HTMLInputElement | null = null;
let emailInput = $state<HTMLInputElement | null>(null);
let passwordInput = $state<HTMLInputElement | null>(null);
let loginFocusArmed = $state(true);
const currentYear = new Date().getFullYear();
const appVersion = `v${packageInfo.version}`;
const releaseStage = 'Beta';
@@ -50,11 +56,17 @@
// system. The response is shape-compatible with the legacy client
// session, so the rest of the app continues to work unchanged.
const session = await api.internalLogin(email, password);
const targetHref = getWorkspaceHomeHref(session);
postLoginRedirecting = targetHref !== '/';
clientSession.set(session);
if (targetHref !== '/') {
await goto(targetHref, { replaceState: true });
}
} catch (error) {
loginError = error instanceof Error ? error.message : 'Unable to sign in';
triggerPasswordShake();
} finally {
postLoginRedirecting = false;
isLoggingIn = false;
}
}
@@ -84,6 +96,18 @@
);
}
$effect(() => {
if ($sessionHydrated && !$clientSession) {
if (loginFocusArmed && emailInput) {
loginFocusArmed = false;
tick().then(() => emailInput?.focus());
}
return;
}
loginFocusArmed = true;
});
function currency(value: number | null | undefined, digits = 2) {
if (value === null || value === undefined) {
return 'N/A';
@@ -363,7 +387,13 @@
<form class="signin-form auth-form" onsubmit={handleLogin}>
<label class="field">
<span>Email</span>
<input bind:value={email} type="email" autocomplete="username" placeholder="Email" autofocus />
<input
bind:this={emailInput}
bind:value={email}
type="email"
autocomplete="username"
placeholder="Email"
/>
</label>
<label class="field field-password" class:is-invalid={Boolean(loginError)}>
@@ -401,6 +431,34 @@
</div>
</div>
</section>
{:else if postLoginRedirecting}
<section class="auth-stage auth-stage-loading">
<div class="auth-card auth-card-loading">
<div class="auth-header">
<div class="client-logo-block">
<img class="hero-login-logo" src="/logo-hsf.png" alt="Lean 101" />
<div class="client-logo-copy">
<p class="eyebrow">Opening Workspace</p>
<strong>Hunter Premium Produce</strong>
<span>Applying your role permissions now</span>
</div>
</div>
</div>
<div class="auth-copy">
<h2>Preparing your workspace.</h2>
<p>Routing you directly to the first area your role is allowed to open.</p>
</div>
<div class="auth-loading-panel">
<span class="loading-pulse" aria-hidden="true"></span>
<div>
<strong>Applying Access Rules</strong>
<p>Dashboard access is skipped for roles that do not have permission.</p>
</div>
</div>
</div>
</section>
{:else}
<section class="dashboard-intro">
<div class="greeting-row">