feat: polish
This commit is contained in:
parent
2e6177fe74
commit
4bd927bb4e
137 changed files with 6357 additions and 137560 deletions
80
src/lib/features/auth/LoginForm.svelte
Normal file
80
src/lib/features/auth/LoginForm.svelte
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { authStore } from '$auth';
|
||||
import { t } from '$i18n';
|
||||
|
||||
let username = $state('');
|
||||
let password = $state('');
|
||||
let error = $state('');
|
||||
let isLoading = $state(false);
|
||||
|
||||
async function handleLogin(e: Event) {
|
||||
e.preventDefault();
|
||||
if (!username || !password) {
|
||||
error = $t('login.fieldsRequired');
|
||||
return;
|
||||
}
|
||||
isLoading = true;
|
||||
error = '';
|
||||
try {
|
||||
await authStore.login(username, password);
|
||||
goto('/');
|
||||
} catch (err: unknown) {
|
||||
error = (err as Error).message || $t('login.invalidCredentials');
|
||||
} finally {
|
||||
isLoading = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<main class="container pt-3">
|
||||
<div class="text-center mt-5 mb-4">
|
||||
<img src="/logo-lg.svg" alt="Logo" width="300" class="rounded-3" />
|
||||
<h2 class="text-center mt-4 mb-5">{$t('app.title')}</h2>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-6 col-lg-4 offset-md-3 offset-lg-4">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">{$t('login.heading')}</h5>
|
||||
|
||||
{#if error}
|
||||
<div class="alert alert-danger mb-4" role="alert">{error}</div>
|
||||
{/if}
|
||||
|
||||
<form onsubmit={handleLogin} class="mt-4">
|
||||
<div class="form-floating mb-3">
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="username"
|
||||
placeholder={$t('login.username')}
|
||||
bind:value={username}
|
||||
required />
|
||||
<label for="username">{$t('login.username')}</label>
|
||||
</div>
|
||||
<div class="form-floating mb-3">
|
||||
<input
|
||||
type="password"
|
||||
class="form-control"
|
||||
id="password"
|
||||
placeholder={$t('login.password')}
|
||||
bind:value={password}
|
||||
required />
|
||||
<label for="password">{$t('login.password')}</label>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary w-100" disabled={isLoading}>
|
||||
{#if isLoading}
|
||||
<span class="spinner-border spinner-border-sm" role="status"></span>
|
||||
{$t('login.submitting')}
|
||||
{:else}
|
||||
{$t('login.submit')}
|
||||
{/if}
|
||||
</button>
|
||||
<a href="/" class="btn btn-secondary mt-3 w-100">{$t('login.back')}</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
80
src/lib/features/auth/Navbar.svelte
Normal file
80
src/lib/features/auth/Navbar.svelte
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import {
|
||||
Collapse,
|
||||
Dropdown,
|
||||
DropdownItem,
|
||||
DropdownMenu,
|
||||
DropdownToggle,
|
||||
Nav,
|
||||
NavItem,
|
||||
NavLink,
|
||||
Navbar,
|
||||
NavbarBrand,
|
||||
NavbarToggler,
|
||||
} from '@sveltestrap/sveltestrap';
|
||||
import { authStore } from '$auth';
|
||||
import { goto } from '$app/navigation';
|
||||
import { t } from '$i18n';
|
||||
|
||||
// Auth is already refreshed by the root layout before this mounts.
|
||||
let isOpen = $state(false);
|
||||
|
||||
async function handleLogout() {
|
||||
await authStore.logout();
|
||||
goto('/');
|
||||
}
|
||||
</script>
|
||||
|
||||
<Navbar color="light" light expand="lg" fixed="top" class="custom-navbar border-bottom">
|
||||
<NavbarBrand href="/" class="nav-full-height">
|
||||
<img src="/logo.svg" alt="Logo" height="34" class="d-inline-block align-text-top" />
|
||||
</NavbarBrand>
|
||||
<NavbarToggler on:click={() => (isOpen = !isOpen)} />
|
||||
<Collapse {isOpen} navbar expand="lg">
|
||||
<Nav class="me-auto mb-lg-0" navbar>
|
||||
<NavItem>
|
||||
<NavLink
|
||||
href="/predict"
|
||||
class="nav-full-height border border-top-0"
|
||||
active={$page.url.pathname === '/predict'}>
|
||||
{$t('nav.predict')}
|
||||
</NavLink>
|
||||
</NavItem>
|
||||
<NavItem>
|
||||
<NavLink
|
||||
href="/track"
|
||||
class="nav-full-height border border-top-0"
|
||||
active={$page.url.pathname === '/track'}>
|
||||
{$t('nav.track')}
|
||||
</NavLink>
|
||||
</NavItem>
|
||||
</Nav>
|
||||
<Nav navbar>
|
||||
{#if $authStore.status === 'authenticated' && $authStore.username}
|
||||
<Dropdown nav inNavbar>
|
||||
<DropdownToggle nav caret class="nav-full-height border border-top-0">
|
||||
{$authStore.username}
|
||||
</DropdownToggle>
|
||||
<DropdownMenu end>
|
||||
<DropdownItem href="/user/account">{$t('nav.account')}</DropdownItem>
|
||||
<DropdownItem href="/user/templates">{$t('nav.scenarios')}</DropdownItem>
|
||||
<DropdownItem href="/user/predictions">{$t('nav.predictionHistory')}</DropdownItem>
|
||||
<DropdownItem href="/user/flights">{$t('nav.trackingHistory')}</DropdownItem>
|
||||
<DropdownItem divider />
|
||||
<DropdownItem on:click={handleLogout}>{$t('nav.logout')}</DropdownItem>
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
{:else if $authStore.status === 'anonymous'}
|
||||
<NavItem>
|
||||
<NavLink
|
||||
href="/login"
|
||||
class="nav-full-height border border-top-0"
|
||||
active={$page.url.pathname === '/login'}>
|
||||
{$t('nav.login')}
|
||||
</NavLink>
|
||||
</NavItem>
|
||||
{/if}
|
||||
</Nav>
|
||||
</Collapse>
|
||||
</Navbar>
|
||||
2
src/lib/features/auth/index.ts
Normal file
2
src/lib/features/auth/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export { default as Navbar } from './Navbar.svelte';
|
||||
export { default as LoginForm } from './LoginForm.svelte';
|
||||
Loading…
Add table
Add a link
Reference in a new issue