feat: Rework login page & appState initialization
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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" />
|
||||
|
||||
46
src/lib/components/TextField.svelte
Normal file
46
src/lib/components/TextField.svelte
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
17
src/lib/pages/PageNotFound.svelte
Normal file
17
src/lib/pages/PageNotFound.svelte
Normal 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>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -11,7 +11,8 @@ export default {
|
||||
darken: '#07050166',
|
||||
lighten: '#fde68a20',
|
||||
// 'highlight-foreground': '#E7E5E4'
|
||||
'highlight-foreground': '#ffe6abcc'
|
||||
'highlight-foreground': '#ffe6abcc',
|
||||
'highlight-background': '#2925247F'
|
||||
},
|
||||
keyframes: {
|
||||
timer: {
|
||||
|
||||
Reference in New Issue
Block a user