feat: Rework login page & appState initialization

This commit is contained in:
Aleksi Lassila
2024-04-09 21:20:23 +03:00
parent b5e7e4deff
commit 914e9faccc
11 changed files with 162 additions and 83 deletions

View File

@@ -11,16 +11,10 @@
import SeriesPage from './lib/components/SeriesPage/SeriesPage.svelte';
import Sidebar from './lib/components/Sidebar/Sidebar.svelte';
import LoginPage from './lib/pages/LoginPage.svelte';
import { getReiverrApiClient } from './lib/apis/reiverr/reiverr-api';
import { appState } from './lib/stores/app-state.store';
import MoviePage from './lib/pages/MoviePage.svelte';
import ModalStack from './lib/components/Modal/ModalStack.svelte';
getReiverrApiClient()
.GET('/user', {})
.then((res) => res.data)
.then((user) => appState.setUser(user || null))
.catch(() => appState.setUser(null));
import PageNotFound from './lib/pages/PageNotFound.svelte';
appState.subscribe((s) => console.log('appState', s));
</script>
@@ -58,7 +52,7 @@
</Route>
<Route path="movie/:id" component={MoviePage} />
<Route path="*">
<div>404</div>
<PageNotFound />
</Route>
</Container>
</Router>

View File

@@ -50,6 +50,14 @@ html[data-useragent*="Tizen"] .selectable {
border-width: 2px;
}
.selected {
@apply outline-none outline-0 border-2 border-highlight-foreground;
}
.unselected {
@apply outline-none outline-0 border-2 border-transparent;
}
.peer-selectable {
@apply peer-focus-visible:outline outline-2 outline-[#f0cd6dc2] outline-offset-2;
}

View File

@@ -1,12 +1,15 @@
import createClient from 'openapi-fetch';
import type { paths } from './reiverr.generated';
import type { components, paths } from './reiverr.generated';
import { get } from 'svelte/store';
import { appState } from '../../stores/app-state.store';
import type { Api } from '../api.interface';
export type ReiverrUser = components['schemas']['UserDto'];
export class ReiverrApi implements Api<paths> {
getClient(basePath?: string) {
const token = get(appState).token;
getClient(basePath?: string, _token?: string) {
const token = _token || get(appState).token;
console.log('token', token);
return createClient<paths>({
baseUrl: (basePath || get(appState).serverBaseUrl) + '/api',
@@ -17,6 +20,20 @@ export class ReiverrApi implements Api<paths> {
})
});
}
async getUser() {
const res = await this.getClient()?.GET('/user', {});
return res.data;
}
authenticate(name: string, password: string) {
return this.getClient().POST('/auth', {
body: {
name,
password
}
});
}
}
export const reiverrApi = new ReiverrApi();

View File

@@ -16,7 +16,7 @@
{
'bg-highlight-foreground text-stone-900': $hasFoucus,
'hover:bg-highlight-foreground hover:text-stone-900': true,
'bg-stone-800/90': !$hasFoucus,
'bg-highlight-background': !$hasFoucus,
'cursor-pointer': !inactive,
'cursor-not-allowed pointer-events-none opacity-40': inactive
},
@@ -34,7 +34,9 @@
<slot name="icon" />
</div>
{/if}
<slot {hasFocus} />
<div class="flex-1 text-center">
<slot {hasFocus} />
</div>
{#if $$slots['icon-after']}
<div class="ml-2">
<slot name="icon-after" />

View File

@@ -0,0 +1,46 @@
<script lang="ts">
import Container from '../../Container.svelte';
import type { FormEventHandler, HTMLInputTypeAttribute } from 'svelte/elements';
import { createEventDispatcher } from 'svelte';
import { PLATFORM_TV } from '../constants';
import classNames from 'classnames';
export let value = '';
export let type: HTMLInputTypeAttribute = 'text';
const dispatch = createEventDispatcher<{
change: string;
}>();
let input: HTMLInputElement;
const handleChange = (e: Event) => {
value = e.target?.value;
console.log('e', e);
dispatch('change', e.target?.value);
};
</script>
<Container
on:enter={(e) => {
if (!PLATFORM_TV) {
e.detail.options.setFocusedElement = input;
}
}}
on:clickOrSelect={() => input?.focus()}
class={classNames('flex flex-col', $$restProps.class)}
let:hasFocus
>
<label class="text-sm text-zinc-300 mb-1">
<slot>Label</slot>
</label>
<input
class={classNames('bg-highlight-background px-4 py-1.5 rounded-lg', {
selected: hasFocus,
unselected: !hasFocus
})}
{type}
{value}
on:input={handleChange}
bind:this={input}
/>
</Container>

View File

@@ -1,24 +1,17 @@
<script lang="ts">
import { getReiverrApiClient } from '../apis/reiverr/reiverr-api';
import { getReiverrApiClient, reiverrApi } from '../apis/reiverr/reiverr-api';
import Container from '../../Container.svelte';
import { appState } from '../stores/app-state.store';
import TextField from '../components/TextField.svelte';
import Button from '../components/Button.svelte';
let name: string = 'test';
let password: string = 'test';
let error: string | undefined = undefined;
let input0: HTMLInputElement;
let input1: HTMLInputElement;
let input2: HTMLInputElement;
function handleLogin() {
getReiverrApiClient()
.POST('/auth', {
body: {
name,
password
}
})
reiverrApi
.authenticate(name, password)
.then((res) => {
if (res.error?.statusCode === 401) {
error = 'Invalid credentials. Please try again.';
@@ -27,7 +20,7 @@
} else {
const token = res.data.accessToken;
appState.setToken(token);
window.location.reload();
// window.location.reload();
}
})
.catch((err: Error) => {
@@ -36,36 +29,26 @@
}
</script>
<Container class="flex flex-col" focusOnMount>
<Container
class="w-full h-full max-w-xs mx-auto flex flex-col items-center justify-center"
focusOnMount
>
<h1 class="font-semibold tracking-wide text-xl w-full">Login to Reiverr</h1>
<TextField
value={$appState.serverBaseUrl}
on:change={(e) => appState.setBaseUrl(e.detail)}
class="mt-4 w-full"
>
Server
</TextField>
<TextField bind:value={name} class="mt-4 w-full">Name</TextField>
<TextField bind:value={password} type="password" class="mt-4 w-full">Name</TextField>
<Button on:clickOrSelect={handleLogin} class="mt-8 w-full">Submit</Button>
{#if error}
<div class="text-red-300">{error}</div>
{/if}
<div>
Server:
<Container on:click={() => input0?.focus()}>
<input
class="bg-stone-900"
type="text"
value={$appState.serverBaseUrl}
on:change={(e) => appState.setBaseUrl(e?.target?.value)}
bind:this={input0}
/>
</Container>
</div>
<div>
Name:
<Container on:click={() => input1?.focus()}>
<input class="bg-stone-900" type="text" bind:value={name} bind:this={input1} />
</Container>
</div>
<div>
Password:
<Container on:click={() => input2?.focus()}>
<input class="bg-stone-900" type="password" bind:value={password} bind:this={input2} />
</Container>
</div>
<Container on:click={handleLogin}>Submit</Container>
</Container>

View File

@@ -1,18 +1,10 @@
<script lang="ts">
import Container from '../../Container.svelte';
import { appState } from '../stores/app-state.store';
import { useNavigate } from 'svelte-navigator';
const navigate = useNavigate();
function handleLogout() {
console.log('Hello world');
appState.setToken(undefined);
// window.location.replace('/');
window.location.reload();
}
import Button from '../components/Button.svelte';
</script>
<Container class="pl-24">
<Container on:click={handleLogout} class="hover:bg-red-500">Log Out</Container>
{window.navigator.userAgent}
<Container class="pl-24 flex flex-col items-start" focusOnMount>
User agent: {window.navigator.userAgent}
<Button on:clickOrSelect={appState.logOut} class="hover:bg-red-500">Log Out</Button>
</Container>

View File

@@ -0,0 +1,17 @@
<script lang="ts">
import { useLocation, useNavigate } from 'svelte-navigator';
import Container from '../../Container.svelte';
const location = useLocation();
const navigate = useNavigate();
location.subscribe((l) => {
if (l.pathname === '/dist/index.html') {
navigate('/');
}
});
</script>
<Container class="pl-24 flex flex-col items-start" focusOnMount>
<div>404 {$location.pathname}</div>
</Container>

View File

@@ -13,7 +13,7 @@ export type NavigationActions = {
};
type FocusEventOptions = {
setFocusedElement: boolean;
setFocusedElement: boolean | HTMLElement;
propagate: boolean;
onFocus?: (
superOnFocus: FocusHandler,
@@ -146,7 +146,11 @@ export class Selectable {
propagateFocusUpdates(_options, this);
if (_options.setFocusedElement) {
this.htmlElement.focus({ preventScroll: true });
if (_options.setFocusedElement === true) {
this.htmlElement.focus({ preventScroll: true });
} else {
_options.setFocusedElement.focus({ preventScroll: true });
}
Selectable.focusedObject.set(this);
}
}

View File

@@ -1,23 +1,22 @@
import { derived, writable } from 'svelte/store';
import type { components } from '../apis/reiverr/reiverr.generated';
import { createLocalStorageStore } from './localstorage.store';
export type User = components['schemas']['UserDto'];
import { getReiverrApiClient, type ReiverrUser } from '../apis/reiverr/reiverr-api';
interface AuthenticationStoreData {
token?: string;
serverBaseUrl?: string;
}
const authenticationStore = createLocalStorageStore<AuthenticationStoreData>(
'authentication-token',
{
token: undefined,
serverBaseUrl: window?.location?.origin
}
);
function createAppState() {
const userStore = writable<User | null>(undefined);
const authenticationStore = createLocalStorageStore<AuthenticationStoreData>(
'authentication-token',
{
token: undefined,
serverBaseUrl: window?.location?.origin
}
);
const userStore = writable<ReiverrUser | null>(undefined);
const combinedStore = derived([userStore, authenticationStore], ([$user, $auth]) => {
return {
user: $user,
@@ -34,17 +33,33 @@ function createAppState() {
authenticationStore.update((p) => ({ ...p, token }));
}
function setUser(user: User | null) {
function setUser(user: ReiverrUser | null) {
userStore.set(user);
}
function logOut() {
setUser(null);
setToken(undefined);
}
return {
subscribe: combinedStore.subscribe,
setBaseUrl,
setToken,
setUser
setUser,
logOut
};
}
export const appState = createAppState();
export const appStateUser = derived(appState, ($state) => $state.user);
authenticationStore.subscribe((auth) => {
if (auth.token) {
getReiverrApiClient(auth.serverBaseUrl, auth.token)
?.GET('/user', {})
.then((user) => appState.setUser(user || null));
} else {
appState.setUser(null);
}
});

View File

@@ -11,7 +11,8 @@ export default {
darken: '#07050166',
lighten: '#fde68a20',
// 'highlight-foreground': '#E7E5E4'
'highlight-foreground': '#ffe6abcc'
'highlight-foreground': '#ffe6abcc',
'highlight-background': '#2925247F'
},
keyframes: {
timer: {