feat: Logging in multiple users, user selection

This commit is contained in:
Aleksi Lassila
2024-06-12 23:10:48 +03:00
parent 5c1a4d4206
commit f5220e21e0
15 changed files with 226 additions and 88 deletions

View File

@@ -15,6 +15,8 @@
import { localSettings } from './lib/stores/localstorage.store'; import { localSettings } from './lib/stores/localstorage.store';
import { user } from './lib/stores/user.store'; import { user } from './lib/stores/user.store';
import { sessions } from './lib/stores/session.store'; import { sessions } from './lib/stores/session.store';
import SplashScreen from './lib/pages/SplashScreen.svelte';
import UsersPage from './lib/pages/UsersPage.svelte';
user.subscribe((s) => console.log('user', s)); user.subscribe((s) => console.log('user', s));
sessions.subscribe((s) => console.log('sessions', s)); sessions.subscribe((s) => console.log('sessions', s));
@@ -57,15 +59,9 @@
<I18n /> <I18n />
<!--<Container class="w-full h-full overflow-auto text-white scrollbar-hide">--> <!--<Container class="w-full h-full overflow-auto text-white scrollbar-hide">-->
{#if $user === undefined} {#if $user === undefined}
<div class="h-full w-full flex flex-col items-center justify-center"> <SplashScreen />
<div class="flex items-center justify-center hover:text-inherit selectable rounded-sm mb-2">
<div class="rounded-full bg-amber-300 h-4 w-4 mr-2" />
<h1 class="font-display uppercase font-semibold tracking-wider text-xl">Reiverr</h1>
</div>
<div>Loading...</div>
</div>
{:else if $user === null} {:else if $user === null}
<LoginPage /> <UsersPage />
{:else if $user.onboardingDone === false} {:else if $user.onboardingDone === false}
<OnboardingPage /> <OnboardingPage />
{:else} {:else}
@@ -92,10 +88,10 @@
<StackRouter stack={stackRouter} /> <StackRouter stack={stackRouter} />
<!-- </Container>--> <!-- </Container>-->
<!-- </Router>--> <!-- </Router>-->
<ModalStack />
{/if} {/if}
<ModalStack />
<NotificationStack /> <NotificationStack />
<NavigationDebugger /> <NavigationDebugger />

View File

@@ -22,10 +22,10 @@ export class ReiverrApi implements Api<paths> {
}); });
} }
isSetupDone = async (): Promise<boolean> => // isSetupDone = async (): Promise<boolean> =>
this.getClient() // this.getClient()
?.GET('/user/isSetupDone') // ?.GET('/user/isSetupDone')
.then((res) => res.data || false) || false; // .then((res) => res.data || false) || false;
async getUser() { async getUser() {
const res = await this.getClient()?.GET('/user', {}); const res = await this.getClient()?.GET('/user', {});

View File

@@ -0,0 +1,9 @@
<script lang="ts">
import { Plus } from 'radix-icons-svelte';
</script>
<div class="absolute inset-0 bg-secondary-800/75 flex items-center justify-center">
<div class="rounded-full p-2.5 bg-secondary-800/75">
<Plus size={32} />
</div>
</div>

View File

@@ -0,0 +1,14 @@
<script lang="ts">
import Dialog from './Dialog.svelte';
import Login from '../Login.svelte';
import { navigate } from '../StackRouter/StackRouter.js';
</script>
<Dialog let:close>
<Login
on:login={() => {
close();
navigate('/', { refresh: true });
}}
/>
</Dialog>

View File

@@ -27,7 +27,7 @@
$$restProps.class $$restProps.class
)} )}
> >
<slot /> <slot close={handleClose} />
</div> </div>
</div> </div>
</Modal> </Modal>

View File

@@ -0,0 +1,64 @@
<script lang="ts">
import Container from '../../Container.svelte';
import TextField from '../components/TextField.svelte';
import Button from '../components/Button.svelte';
import { createLocalStorageStore } from '../stores/localstorage.store';
import { sessions } from '../stores/session.store';
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher<{ login: null }>();
const baseUrl = createLocalStorageStore('baseUrl', window.location.origin || '');
const name = createLocalStorageStore('username', '');
let password: string = '';
let error: string | undefined = undefined;
let loading = false;
function handleLogin() {
loading = true;
sessions
.addSession($baseUrl, $name, password)
.then((res) => {
console.log('res', res);
if (res?.request?.status === 401) {
error = 'Invalid credentials. Please try again.';
} else if (res?.request.status !== 200) {
error = 'Error occurred: ' + res.request.statusText;
} else {
dispatch('login');
}
})
.catch((err: Error) => {
error = err.name + ': ' + err.message;
})
.finally(() => {
loading = false;
});
}
</script>
<Container class="flex flex-col" focusOnMount>
<h1 class="header2 w-full mb-2">Login to Reiverr</h1>
<div class="header1 mb-4">
If this is your first time logging in, a new account will be created based on your credentials.
</div>
<TextField value={$baseUrl} on:change={(e) => baseUrl.set(e.detail)} class="mb-4 w-full">
Server
</TextField>
<TextField value={$name} on:change={({ detail }) => name.set(detail)} class="mb-4 w-full">
Name
</TextField>
<TextField bind:value={password} type="password" class="mb-8 w-full">Password</TextField>
<Button type="primary-dark" disabled={loading} on:clickOrSelect={handleLogin} class="mb-4 w-full"
>Submit</Button
>
{#if error}
<div class="text-red-300 text-center">{error}</div>
{/if}
</Container>

View File

@@ -5,6 +5,7 @@
import classNames from 'classnames'; import classNames from 'classnames';
import { Plus, PlusCircled } from 'radix-icons-svelte'; import { Plus, PlusCircled } from 'radix-icons-svelte';
import { getCardDimensions } from '../../utils'; import { getCardDimensions } from '../../utils';
import AddElementOverlay from '../AddElementOverlay.svelte';
export let backdropUrl: string; export let backdropUrl: string;
@@ -31,10 +32,6 @@
class="bg-cover bg-center absolute inset-0" class="bg-cover bg-center absolute inset-0"
style={`background-image: url('${backdropUrl}')`} style={`background-image: url('${backdropUrl}')`}
/> />
<div class="absolute inset-0 bg-secondary-800/75 flex items-center justify-center"> <AddElementOverlay />
<div class="rounded-full p-2.5 bg-secondary-800/75">
<Plus size={32} />
</div>
</div>
</Container> </Container>
</AnimateScale> </AnimateScale>

View File

@@ -16,6 +16,7 @@
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { useTabs } from '../Tab/Tab'; import { useTabs } from '../Tab/Tab';
import { user } from '../../stores/user.store'; import { user } from '../../stores/user.store';
import { sessions } from '../../stores/session.store';
enum Tabs { enum Tabs {
Users, Users,
@@ -121,7 +122,7 @@
<Container <Container
class="w-full h-12 cursor-pointer" class="w-full h-12 cursor-pointer"
on:clickOrSelect={selectIndex(Tabs.Users)} on:clickOrSelect={() => sessions.setActiveSession()}
let:hasFocus let:hasFocus
> >
<div <div

View File

@@ -113,7 +113,14 @@ export function useStackRouter({
}; };
} }
const navigate = (routeString: string, options: { replaceStack?: boolean } = {}) => { const navigate = (
routeString: string,
options: { replaceStack?: boolean; refresh?: boolean } = {}
) => {
if (options.refresh) {
location.assign(routeString);
return;
}
const page: Page = routeStringToRoute(routeString); const page: Page = routeStringToRoute(routeString);
const replaceStack = page.route.root || options.replaceStack || false; const replaceStack = page.route.root || options.replaceStack || false;

View File

@@ -1,65 +1,9 @@
<script lang="ts"> <script lang="ts">
import Container from '../../Container.svelte'; import Login from '../components/Login.svelte';
import TextField from '../components/TextField.svelte';
import Button from '../components/Button.svelte';
import { createLocalStorageStore } from '../stores/localstorage.store';
import { sessions } from '../stores/session.store';
const baseUrl = createLocalStorageStore('baseUrl', window.location.origin || '');
const name = createLocalStorageStore('username', '');
let password: string = '';
let error: string | undefined = undefined;
let loading = false;
function handleLogin() {
loading = true;
sessions
.addSession($baseUrl, $name, password)
.then((res) => {
if (res.error?.statusCode === 401) {
error = 'Invalid credentials. Please try again.';
} else if (res.error) {
error = 'Error occurred: ' + res.error.message;
}
})
.catch((err: Error) => {
error = err.name + ': ' + err.message;
})
.finally(() => {
loading = false;
});
}
</script> </script>
<div class="w-full h-full flex items-center justify-center"> <div class="w-full h-full flex items-center justify-center">
<Container class="flex flex-col bg-primary-800 rounded-2xl p-10 shadow-xl max-w-lg" focusOnMount> <div class="bg-primary-800 rounded-2xl p-10 shadow-xl max-w-lg">
<h1 class="header2 w-full mb-2">Login to Reiverr</h1> <Login />
<div class="header1 mb-4"> </div>
If this is your first time logging in, a new account will be created based on your
credentials.
</div>
<TextField value={$baseUrl} on:change={(e) => baseUrl.set(e.detail)} class="mb-4 w-full">
Server
</TextField>
<TextField value={$name} on:change={({ detail }) => name.set(detail)} class="mb-4 w-full">
Name
</TextField>
<TextField bind:value={password} type="password" class="mb-8 w-full">Password</TextField>
<Button
type="primary-dark"
disabled={loading}
on:clickOrSelect={handleLogin}
class="mb-4 w-full">Submit</Button
>
{#if error}
<div class="text-red-300 text-center">{error}</div>
{/if}
</Container>
</div> </div>

View File

@@ -0,0 +1,7 @@
<div class="h-full w-full flex flex-col items-center justify-center">
<div class="flex items-center justify-center hover:text-inherit selectable rounded-sm mb-2">
<div class="rounded-full bg-amber-300 h-4 w-4 mr-2" />
<h1 class="font-display uppercase font-semibold tracking-wider text-xl">Reiverr</h1>
</div>
<div>Loading...</div>
</div>

View File

@@ -1,5 +1,93 @@
<script> <script lang="ts">
import DetachedPage from '../components/DetachedPage/DetachedPage.svelte'; import DetachedPage from '../components/DetachedPage/DetachedPage.svelte';
import { type Session, sessions } from '../stores/session.store.js';
import { reiverrApi } from '../apis/reiverr/reiverr-api';
import Container from '../../Container.svelte';
import Button from '../components/Button.svelte';
import { TMDB_PROFILE_LARGE } from '../constants';
import classNames from 'classnames';
import AnimateScale from '../components/AnimateScale.svelte';
import { navigate } from '../components/StackRouter/StackRouter';
import { createModal } from '../components/Modal/modal.store';
import AddUserDialog from '../components/Dialog/AddUserDialog.svelte';
import Login from '../components/Login.svelte';
import { Plus, Trash } from 'radix-icons-svelte';
import AddElementOverlay from '../components/AddElementOverlay.svelte';
$: users = getUsers($sessions.sessions);
async function getUsers(sessions: Session[]) {
return Promise.all(
sessions.map(async (session) =>
reiverrApi
.getClient(session.baseUrl, session.token)
.GET('/user')
.then((r) => ({ session, user: r.data }))
)
).then((us) => us.filter((u) => !!u.user));
}
function handleSwitchUser({ session, user }: Awaited<typeof users>[number]) {
sessions.setActiveSession(session);
navigate('/');
}
</script> </script>
<DetachedPage sidebar={false} class="px-32 py-16">Users Page</DetachedPage> <DetachedPage sidebar={false} class="px-32 py-16 h-full flex flex-col items-center justify-center">
{#await users then users}
{#if users?.length}
<h1 class="header4 mb-16">Who is watching?</h1>
<Container direction="grid" gridCols={4} class="flex space-x-8 mb-16">
{#each users as item}
{@const user = item.user}
<Container let:hasFocus on:clickOrSelect={() => user && handleSwitchUser(item)}>
<AnimateScale {hasFocus}>
<div
class={classNames('w-40 h-40 bg-center bg-cover mb-4 rounded-xl', {
selected: hasFocus
})}
style={`background-image: url('${TMDB_PROFILE_LARGE}/wo2hJpn04vbtmh0B9utCFdsQhxM.jpg')`}
/>
<div class={classNames('text-center header1', { '!text-secondary-100': hasFocus })}>
{user?.name}
</div>
</AnimateScale>
</Container>
{/each}
<Container let:hasFocus on:clickOrSelect={() => createModal(AddUserDialog, {})}>
<AnimateScale {hasFocus}>
<div
class={classNames('relative overflow-hidden rounded-xl mb-4 w-40 h-40', {
selected: hasFocus
})}
>
<div
class={`w-full h-full bg-center bg-cover`}
style={`background-image: url('${TMDB_PROFILE_LARGE}/wo2hJpn04vbtmh0B9utCFdsQhxM.jpg')`}
/>
<AddElementOverlay />
</div>
<!-- <div class={classNames('text-center header1', { '!text-secondary-100': hasFocus })}>-->
<!-- Add User-->
<!-- </div>-->
</AnimateScale>
</Container>
</Container>
<Container direction="horizontal" class="flex space-x-4">
<Button
on:clickOrSelect={() => {
sessions.removeSessions();
navigate('/');
}}
icon={Trash}
>
Remove all Accounts
</Button>
</Container>
{:else}
<div class="bg-primary-800 rounded-2xl p-10 shadow-xl max-w-lg">
<Login />
</div>
{/if}
{/await}
</DetachedPage>

View File

@@ -724,6 +724,8 @@ export class Selectable {
this._addChildCount++; this._addChildCount++;
// console.log('Incremented addChildCount to', this._addChildCount); // console.log('Incremented addChildCount to', this._addChildCount);
if (focusIndex > 0) this.focusIndex.update((prev) => prev + 1); // TODO: Maybe needs fixing pt1 if (focusIndex > 0) this.focusIndex.update((prev) => prev + 1); // TODO: Maybe needs fixing pt1
} else if (this.children.length === 0 && get(this.hasFocus)) {
childToFocus = child;
} }
if (this._removedChildrenCount > 1) { if (this._removedChildrenCount > 1) {

View File

@@ -1,8 +1,10 @@
import { createLocalStorageStore } from './localstorage.store'; import { createLocalStorageStore } from './localstorage.store';
import type { operations } from '../apis/reiverr/reiverr.generated'; import type { operations } from '../apis/reiverr/reiverr.generated';
import axios from 'axios'; import axios from 'axios';
import { get } from 'svelte/store';
export interface Session { export interface Session {
id: string;
baseUrl: string; baseUrl: string;
token: string; token: string;
} }
@@ -15,7 +17,7 @@ function useSessions() {
} }
); );
function setActiveSession(session: Session) { function setActiveSession(session?: Session) {
sessions.update((s) => ({ ...s, activeSession: session })); sessions.update((s) => ({ ...s, activeSession: session }));
} }
@@ -33,12 +35,13 @@ function useSessions() {
if (res.status !== 200) return res; if (res.status !== 200) return res;
const session = { const session = {
id: res.data.user.id,
baseUrl, baseUrl,
token: res.data.accessToken token: res.data.accessToken
}; };
sessions.update((s) => { sessions.update((s) => {
const sessions = s.sessions.concat(session); const sessions = s.sessions.filter((s) => s.id !== session.id).concat(session);
return { return {
sessions, sessions,
activeSession: activate ? session : s.activeSession activeSession: activate ? session : s.activeSession
@@ -59,11 +62,16 @@ function useSessions() {
}); });
} }
function removeSessions() {
sessions.set({ sessions: [] });
}
return { return {
subscribe: sessions.subscribe, subscribe: sessions.subscribe,
setActiveSession, setActiveSession,
addSession, addSession,
removeSession removeSession,
removeSessions
}; };
} }

View File

@@ -26,7 +26,8 @@ function useUser() {
Authorization: 'Bearer ' + activeSession.token Authorization: 'Bearer ' + activeSession.token
} }
}) })
.then((r) => r.data); .then((r) => r.data)
.catch(() => null);
if (lastActiveSession === activeSession) userStore.set(user); if (lastActiveSession === activeSession) userStore.set(user);
}); });