format files

This commit is contained in:
maxDorninger
2025-09-12 20:07:06 +02:00
parent 6f8d3eea4e
commit 6203ec5ce0
21 changed files with 4249 additions and 4231 deletions

View File

@@ -14,7 +14,7 @@
media,
mediaType
}: {
media: components["schemas"]["PublicShow"] | components["schemas"]["PublicMovie"];
media: components['schemas']['PublicShow'] | components['schemas']['PublicMovie'];
mediaType: 'tv' | 'movie';
} = $props();

View File

@@ -1,128 +1,127 @@
<script lang="ts">
import {Button} from '$lib/components/ui/button/index.js';
import * as Card from '$lib/components/ui/card/index.js';
import {Input} from '$lib/components/ui/input/index.js';
import {Label} from '$lib/components/ui/label/index.js';
import {goto} from '$app/navigation';
import {toast} from 'svelte-sonner';
import {base} from '$app/paths';
import * as Alert from '$lib/components/ui/alert/index.js';
import AlertCircleIcon from '@lucide/svelte/icons/alert-circle';
import LoadingBar from '$lib/components/loading-bar.svelte';
import client from '$lib/api';
import {handleOauth} from "$lib/utils.ts";
import { Button } from '$lib/components/ui/button/index.js';
import * as Card from '$lib/components/ui/card/index.js';
import { Input } from '$lib/components/ui/input/index.js';
import { Label } from '$lib/components/ui/label/index.js';
import { goto } from '$app/navigation';
import { toast } from 'svelte-sonner';
import { base } from '$app/paths';
import * as Alert from '$lib/components/ui/alert/index.js';
import AlertCircleIcon from '@lucide/svelte/icons/alert-circle';
import LoadingBar from '$lib/components/loading-bar.svelte';
import client from '$lib/api';
import { handleOauth } from '$lib/utils.ts';
let {
oauthProviderNames
}: {
oauthProviderNames: string[];
} = $props();
let {
oauthProviderNames
}: {
oauthProviderNames: string[];
} = $props();
let email = $state('');
let password = $state('');
let errorMessage = $state('');
let isLoading = $state(false);
let email = $state('');
let password = $state('');
let errorMessage = $state('');
let isLoading = $state(false);
async function handleLogin(event: Event) {
event.preventDefault();
async function handleLogin(event: Event) {
event.preventDefault();
isLoading = true;
errorMessage = '';
isLoading = true;
errorMessage = '';
const {error,response} = await client.POST('/api/v1/auth/cookie/login', {
body: {
username: email,
password: password,
scope: "",
},
headers: {
"Content-Type": "application/x-www-form-urlencoded",
}
});
isLoading = false;
if (!error) {
console.log('Login successful!');
console.log('Received User Data: ', response);
errorMessage = 'Login successful! Redirecting...';
toast.success(errorMessage);
goto(base + '/dashboard');
} else {
let errorText = await response.text();
try {
const errorData = JSON.parse(errorText);
errorMessage = errorData.message || 'Login failed. Please check your credentials.';
} catch {
errorMessage = errorText || 'Login failed. Please check your credentials.';
}
toast.error(errorMessage);
console.error('Login failed:', response.status, errorText);
}
}
const { error, response } = await client.POST('/api/v1/auth/cookie/login', {
body: {
username: email,
password: password,
scope: ''
},
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
});
isLoading = false;
if (!error) {
console.log('Login successful!');
console.log('Received User Data: ', response);
errorMessage = 'Login successful! Redirecting...';
toast.success(errorMessage);
goto(base + '/dashboard');
} else {
let errorText = await response.text();
try {
const errorData = JSON.parse(errorText);
errorMessage = errorData.message || 'Login failed. Please check your credentials.';
} catch {
errorMessage = errorText || 'Login failed. Please check your credentials.';
}
toast.error(errorMessage);
console.error('Login failed:', response.status, errorText);
}
}
</script>
<Card.Root class="mx-auto max-w-sm">
<Card.Header>
<Card.Title class="text-2xl">Login</Card.Title>
<Card.Description>Enter your email below to log in to your account</Card.Description>
</Card.Header>
<Card.Content>
<form class="grid gap-4" onsubmit={handleLogin}>
<div class="grid gap-2">
<Label for="email">Email</Label>
<Input
autocomplete="email"
bind:value={email}
id="email"
placeholder="m@example.com"
required
type="email"
/>
</div>
<div class="grid gap-2">
<div class="flex items-center">
<Label for="password">Password</Label>
<a class="ml-auto inline-block text-sm underline" href="{base}/login/forgot-password">
Forgot your password?
</a>
</div>
<Input
autocomplete="current-password"
bind:value={password}
id="password"
required
type="password"
/>
</div>
<Card.Header>
<Card.Title class="text-2xl">Login</Card.Title>
<Card.Description>Enter your email below to log in to your account</Card.Description>
</Card.Header>
<Card.Content>
<form class="grid gap-4" onsubmit={handleLogin}>
<div class="grid gap-2">
<Label for="email">Email</Label>
<Input
autocomplete="email"
bind:value={email}
id="email"
placeholder="m@example.com"
required
type="email"
/>
</div>
<div class="grid gap-2">
<div class="flex items-center">
<Label for="password">Password</Label>
<a class="ml-auto inline-block text-sm underline" href="{base}/login/forgot-password">
Forgot your password?
</a>
</div>
<Input
autocomplete="current-password"
bind:value={password}
id="password"
required
type="password"
/>
</div>
{#if errorMessage}
<Alert.Root variant="destructive">
<AlertCircleIcon class="size-4"/>
<Alert.Title>Error</Alert.Title>
<Alert.Description>{errorMessage}</Alert.Description>
</Alert.Root>
{/if}
{#if errorMessage}
<Alert.Root variant="destructive">
<AlertCircleIcon class="size-4" />
<Alert.Title>Error</Alert.Title>
<Alert.Description>{errorMessage}</Alert.Description>
</Alert.Root>
{/if}
{#if isLoading}
<LoadingBar/>
{/if}
<Button class="w-full" disabled={isLoading} type="submit">Login</Button>
</form>
{#each oauthProviderNames as name (name)}
<div
class="after:border-border relative mt-2 text-center text-sm after:absolute after:inset-0 after:top-1/2 after:z-0 after:flex after:items-center after:border-t"
>
<span class="bg-background text-muted-foreground relative z-10 px-2">
Or continue with
</span>
</div>
<Button class="mt-2 w-full" onclick={() => handleOauth(name)} variant="outline"
>Login with {name}</Button
>
{/each}
<div class="mt-4 text-center text-sm">
<Button href="{base}/login/signup/" variant="link">Don't have an account? Sign up</Button>
</div>
</Card.Content>
{#if isLoading}
<LoadingBar />
{/if}
<Button class="w-full" disabled={isLoading} type="submit">Login</Button>
</form>
{#each oauthProviderNames as name (name)}
<div
class="after:border-border relative mt-2 text-center text-sm after:absolute after:inset-0 after:top-1/2 after:z-0 after:flex after:items-center after:border-t"
>
<span class="bg-background text-muted-foreground relative z-10 px-2">
Or continue with
</span>
</div>
<Button class="mt-2 w-full" onclick={() => handleOauth(name)} variant="outline"
>Login with {name}</Button
>
{/each}
<div class="mt-4 text-center text-sm">
<Button href="{base}/login/signup/" variant="link">Don't have an account? Sign up</Button>
</div>
</Card.Content>
</Card.Root>

View File

@@ -7,16 +7,16 @@
import { getFullyQualifiedMediaName, getTorrentQualityString } from '$lib/utils.js';
import { toast } from 'svelte-sonner';
import client from '$lib/api';
import type {components} from "$lib/api/api";
import type { components } from '$lib/api/api';
let { movie }: { movie: components["schemas"]["PublicMovie"] } = $props();
let { movie }: { movie: components['schemas']['PublicMovie'] } = $props();
let dialogOpen = $state(false);
let minQuality = $state<string | undefined>(undefined);
let wantedQuality = $state<string | undefined>(undefined);
let isSubmittingRequest = $state(false);
let submitRequestError = $state<string | null>(null);
const qualityValues: components["schemas"]["Quality"][] = [1, 2, 3, 4];
const qualityValues: components['schemas']['Quality'][] = [1, 2, 3, 4];
let qualityOptions = $derived(
qualityValues.map((q) => ({ value: q.toString(), label: getTorrentQualityString(q) }))
);
@@ -30,8 +30,8 @@
const { response } = await client.POST('/api/v1/movies/requests', {
body: {
movie_id: movie.id!,
min_quality: parseInt(minQuality!) as components["schemas"]["Quality"],
wanted_quality: parseInt(wantedQuality!) as components["schemas"]["Quality"]
min_quality: parseInt(minQuality!) as components['schemas']['Quality'],
wanted_quality: parseInt(wantedQuality!) as components['schemas']['Quality']
}
});
isSubmittingRequest = false;

View File

@@ -7,9 +7,9 @@
import { getFullyQualifiedMediaName, getTorrentQualityString } from '$lib/utils.js';
import { toast } from 'svelte-sonner';
import client from '$lib/api';
import type {components} from "$lib/api/api";
import type { components } from '$lib/api/api';
let { show }: { show: components["schemas"]["PublicShow"] } = $props();
let { show }: { show: components['schemas']['PublicShow'] } = $props();
let dialogOpen = $state(false);
let selectedSeasonsIds = $state<string[]>([]);
@@ -18,7 +18,7 @@
let isSubmittingRequest = $state(false);
let submitRequestError = $state<string | null>(null);
const qualityValues: components["schemas"]["Quality"][] = [1, 2, 3, 4];
const qualityValues: components['schemas']['Quality'][] = [1, 2, 3, 4];
let qualityOptions = $derived(
qualityValues.map((q) => ({ value: q.toString(), label: getTorrentQualityString(q) }))
);
@@ -38,8 +38,8 @@
const { response, error } = await client.POST('/api/v1/tv/seasons/requests', {
body: {
season_id: id,
min_quality: parseInt(minQuality!) as components["schemas"]["Quality"],
wanted_quality: parseInt(wantedQuality!) as components["schemas"]["Quality"]
min_quality: parseInt(minQuality!) as components['schemas']['Quality'],
wanted_quality: parseInt(wantedQuality!) as components['schemas']['Quality']
}
});

View File

@@ -1,203 +1,223 @@
<script lang="ts">
import {getFullyQualifiedMediaName, getTorrentQualityString} from '$lib/utils.js';
import CheckmarkX from '$lib/components/checkmark-x.svelte';
import type {components} from '$lib/api/api';
import { getFullyQualifiedMediaName, getTorrentQualityString } from '$lib/utils.js';
import CheckmarkX from '$lib/components/checkmark-x.svelte';
import type { components } from '$lib/api/api';
import * as Table from '$lib/components/ui/table/index.js';
import {getContext} from 'svelte';
import {Button} from '$lib/components/ui/button/index.js';
import {toast} from 'svelte-sonner';
import {goto} from '$app/navigation';
import {base} from '$app/paths';
import client from '$lib/api';
import * as Table from '$lib/components/ui/table/index.js';
import { getContext } from 'svelte';
import { Button } from '$lib/components/ui/button/index.js';
import { toast } from 'svelte-sonner';
import { goto } from '$app/navigation';
import { base } from '$app/paths';
import client from '$lib/api';
let {
requests,
filter = () => true,
isShow = true
}: {
requests: (components["schemas"]["RichSeasonRequest"] | components["schemas"]["RichMovieRequest"])[];
filter?: (request: components["schemas"]["RichSeasonRequest"] | components["schemas"]["RichMovieRequest"]) => boolean;
isShow: boolean;
} = $props();
const user: () => components["schemas"]["UserRead"] = getContext('user');
let {
requests,
filter = () => true,
isShow = true
}: {
requests: (
| components['schemas']['RichSeasonRequest']
| components['schemas']['RichMovieRequest']
)[];
filter?: (
request:
| components['schemas']['RichSeasonRequest']
| components['schemas']['RichMovieRequest']
) => boolean;
isShow: boolean;
} = $props();
const user: () => components['schemas']['UserRead'] = getContext('user');
async function approveRequest(requestId: string, currentAuthorizedStatus: boolean) {
let response;
if (isShow) {
const data = await client.PATCH('/api/v1/tv/seasons/requests/{season_request_id}', {
params: {
path: {
season_request_id: requestId
},
query: {
authorized_status: !currentAuthorizedStatus
}
}
});
response = data.response;
} else {
const data = await client.PATCH('/api/v1/movies/requests/{movie_request_id}', {
params: {
path: {
movie_request_id: requestId
},
query: {
authorized_status: !currentAuthorizedStatus
}
}
});
response = data.response;
}
if (response.ok) {
const requestIndex = requests.findIndex((r) => r.id === requestId);
if (requestIndex !== -1) {
let newAuthorizedStatus = !currentAuthorizedStatus;
requests[requestIndex]!.authorized = newAuthorizedStatus;
requests[requestIndex]!.authorized_by = newAuthorizedStatus ? user() : undefined;
}
toast.success(
`Request ${!currentAuthorizedStatus ? 'approved' : 'unapproved'} successfully.`
);
} else {
const errorText = await response.text();
console.error(`Failed to update request status ${response.statusText}`, errorText);
toast.error(`Failed to update request status: ${response.statusText}`);
}
}
async function approveRequest(requestId: string, currentAuthorizedStatus: boolean) {
let response;
if (isShow) {
const data = await client.PATCH('/api/v1/tv/seasons/requests/{season_request_id}', {
params: {
path: {
season_request_id: requestId
},
query: {
authorized_status: !currentAuthorizedStatus
}
}
});
response = data.response;
} else {
const data = await client.PATCH('/api/v1/movies/requests/{movie_request_id}', {
params: {
path: {
movie_request_id: requestId
},
query: {
authorized_status: !currentAuthorizedStatus
}
}
});
response = data.response;
}
if (response.ok) {
const requestIndex = requests.findIndex((r) => r.id === requestId);
if (requestIndex !== -1) {
let newAuthorizedStatus = !currentAuthorizedStatus;
requests[requestIndex]!.authorized = newAuthorizedStatus;
requests[requestIndex]!.authorized_by = newAuthorizedStatus ? user() : undefined;
}
toast.success(
`Request ${!currentAuthorizedStatus ? 'approved' : 'unapproved'} successfully.`
);
} else {
const errorText = await response.text();
console.error(`Failed to update request status ${response.statusText}`, errorText);
toast.error(`Failed to update request status: ${response.statusText}`);
}
}
async function deleteRequest(requestId: string) {
if (
!window.confirm(
'Are you sure you want to delete this season request? This action cannot be undone.'
)
) {
return;
}
let response;
if (isShow) {
const data = await client.DELETE('/api/v1/tv/seasons/requests/{request_id}', {
params: {
path: {
request_id: requestId
}
}
});
response = data.response;
} else {
const data = await client.DELETE('/api/v1/movies/requests/{movie_request_id}', {
params: {
path: {
movie_request_id: requestId
}
}
});
response = data.response;
}
if (response.ok) {
// remove the request from the list
const index = requests.findIndex((r) => r.id === requestId);
if (index > -1) {
requests.splice(index, 1);
}
toast.success('Request deleted successfully');
} else {
console.error(`Failed to delete request ${response.statusText}`, await response.text());
toast.error('Failed to delete request');
}
}
async function deleteRequest(requestId: string) {
if (
!window.confirm(
'Are you sure you want to delete this season request? This action cannot be undone.'
)
) {
return;
}
let response;
if (isShow) {
const data = await client.DELETE('/api/v1/tv/seasons/requests/{request_id}', {
params: {
path: {
request_id: requestId
}
}
});
response = data.response;
} else {
const data = await client.DELETE('/api/v1/movies/requests/{movie_request_id}', {
params: {
path: {
movie_request_id: requestId
}
}
});
response = data.response;
}
if (response.ok) {
// remove the request from the list
const index = requests.findIndex((r) => r.id === requestId);
if (index > -1) {
requests.splice(index, 1);
}
toast.success('Request deleted successfully');
} else {
console.error(`Failed to delete request ${response.statusText}`, await response.text());
toast.error('Failed to delete request');
}
}
</script>
<Table.Root>
<Table.Caption>A list of all requests.</Table.Caption>
<Table.Header>
<Table.Row>
<Table.Head>{isShow ? 'Show' : 'Movie'}</Table.Head>
{#if isShow}
<Table.Head>Season</Table.Head>
{/if}
<Table.Head>Minimum Quality</Table.Head>
<Table.Head>Wanted Quality</Table.Head>
<Table.Head>Requested by</Table.Head>
<Table.Head>Approved</Table.Head>
<Table.Head>Approved by</Table.Head>
<Table.Head>Actions</Table.Head>
</Table.Row>
</Table.Header>
<Table.Body>
{#each requests as request (request.id)}
{#if filter(request)}
<Table.Row>
<Table.Cell>
{#if isShow}
{getFullyQualifiedMediaName((request as components["schemas"]["RichSeasonRequest"]).show)}
{:else}
{getFullyQualifiedMediaName((request as components["schemas"]["RichMovieRequest"]).movie)}
{/if}
</Table.Cell>
{#if isShow}
<Table.Cell>
{(request as components["schemas"]["RichSeasonRequest"]).season.number}
</Table.Cell>
{/if}
<Table.Cell>
{getTorrentQualityString(request.min_quality)}
</Table.Cell>
<Table.Cell>
{getTorrentQualityString(request.wanted_quality)}
</Table.Cell>
<Table.Cell>
{request.requested_by?.email ?? 'N/A'}
</Table.Cell>
<Table.Cell>
<CheckmarkX state={request.authorized}/>
</Table.Cell>
<Table.Cell>
{request.authorized_by?.email ?? 'N/A'}
</Table.Cell>
<!-- TODO: ADD DIALOGUE TO MODIFY REQUEST -->
<Table.Cell class="flex max-w-[150px] flex-col gap-1">
{#if user().is_superuser}
<Button
class=""
size="sm"
onclick={() => approveRequest(request.id!, request.authorized)}
>
{request.authorized ? 'Unapprove' : 'Approve'}
</Button>
{#if isShow}
<Button
class=""
size="sm"
variant="outline"
onclick={() => goto(base + '/dashboard/tv/' + (request as components["schemas"]["RichSeasonRequest"]).show.id)}
>
Download manually
</Button>
{:else}
<Button
class=""
size="sm"
variant="outline"
onclick={() =>
goto(base + '/dashboard/movies/' + (request as components["schemas"]["RichMovieRequest"]).movie.id)}
>
Download manually
</Button>
{/if}
{/if}
{#if user().is_superuser || user().id === request.requested_by?.id}
<Button variant="destructive" size="sm" onclick={() => deleteRequest(request.id!)}
>Delete
</Button>
{/if}
</Table.Cell>
</Table.Row>
{/if}
{:else}
<Table.Row>
<Table.Cell colspan={8} class="text-center">There are currently no requests.</Table.Cell>
</Table.Row>
{/each}
</Table.Body>
<Table.Caption>A list of all requests.</Table.Caption>
<Table.Header>
<Table.Row>
<Table.Head>{isShow ? 'Show' : 'Movie'}</Table.Head>
{#if isShow}
<Table.Head>Season</Table.Head>
{/if}
<Table.Head>Minimum Quality</Table.Head>
<Table.Head>Wanted Quality</Table.Head>
<Table.Head>Requested by</Table.Head>
<Table.Head>Approved</Table.Head>
<Table.Head>Approved by</Table.Head>
<Table.Head>Actions</Table.Head>
</Table.Row>
</Table.Header>
<Table.Body>
{#each requests as request (request.id)}
{#if filter(request)}
<Table.Row>
<Table.Cell>
{#if isShow}
{getFullyQualifiedMediaName(
(request as components['schemas']['RichSeasonRequest']).show
)}
{:else}
{getFullyQualifiedMediaName(
(request as components['schemas']['RichMovieRequest']).movie
)}
{/if}
</Table.Cell>
{#if isShow}
<Table.Cell>
{(request as components['schemas']['RichSeasonRequest']).season.number}
</Table.Cell>
{/if}
<Table.Cell>
{getTorrentQualityString(request.min_quality)}
</Table.Cell>
<Table.Cell>
{getTorrentQualityString(request.wanted_quality)}
</Table.Cell>
<Table.Cell>
{request.requested_by?.email ?? 'N/A'}
</Table.Cell>
<Table.Cell>
<CheckmarkX state={request.authorized} />
</Table.Cell>
<Table.Cell>
{request.authorized_by?.email ?? 'N/A'}
</Table.Cell>
<!-- TODO: ADD DIALOGUE TO MODIFY REQUEST -->
<Table.Cell class="flex max-w-[150px] flex-col gap-1">
{#if user().is_superuser}
<Button
class=""
size="sm"
onclick={() => approveRequest(request.id!, request.authorized)}
>
{request.authorized ? 'Unapprove' : 'Approve'}
</Button>
{#if isShow}
<Button
class=""
size="sm"
variant="outline"
onclick={() =>
goto(
base +
'/dashboard/tv/' +
(request as components['schemas']['RichSeasonRequest']).show.id
)}
>
Download manually
</Button>
{:else}
<Button
class=""
size="sm"
variant="outline"
onclick={() =>
goto(
base +
'/dashboard/movies/' +
(request as components['schemas']['RichMovieRequest']).movie.id
)}
>
Download manually
</Button>
{/if}
{/if}
{#if user().is_superuser || user().id === request.requested_by?.id}
<Button variant="destructive" size="sm" onclick={() => deleteRequest(request.id!)}
>Delete
</Button>
{/if}
</Table.Cell>
</Table.Row>
{/if}
{:else}
<Table.Row>
<Table.Cell colspan={8} class="text-center">There are currently no requests.</Table.Cell>
</Table.Row>
{/each}
</Table.Body>
</Table.Root>

View File

@@ -1,131 +1,131 @@
<script lang="ts">
import {Button} from '$lib/components/ui/button/index.js';
import * as Card from '$lib/components/ui/card/index.js';
import {Input} from '$lib/components/ui/input/index.js';
import {Label} from '$lib/components/ui/label/index.js';
import {toast} from 'svelte-sonner';
import * as Alert from '$lib/components/ui/alert/index.js';
import AlertCircleIcon from '@lucide/svelte/icons/alert-circle';
import LoadingBar from '$lib/components/loading-bar.svelte';
import CheckCircle2Icon from '@lucide/svelte/icons/check-circle-2';
import {base} from '$app/paths';
import {handleOauth} from "$lib/utils.ts";
import client from '$lib/api';
import { Button } from '$lib/components/ui/button/index.js';
import * as Card from '$lib/components/ui/card/index.js';
import { Input } from '$lib/components/ui/input/index.js';
import { Label } from '$lib/components/ui/label/index.js';
import { toast } from 'svelte-sonner';
import * as Alert from '$lib/components/ui/alert/index.js';
import AlertCircleIcon from '@lucide/svelte/icons/alert-circle';
import LoadingBar from '$lib/components/loading-bar.svelte';
import CheckCircle2Icon from '@lucide/svelte/icons/check-circle-2';
import { base } from '$app/paths';
import { handleOauth } from '$lib/utils.ts';
import client from '$lib/api';
let email = $state('');
let password = $state('');
let errorMessage = $state('');
let successMessage = $state('');
let isLoading = $state(false);
let confirmPassword = $state('');
let {
oauthProviderNames
}: {
oauthProviderNames: string[];
} = $props();
let email = $state('');
let password = $state('');
let errorMessage = $state('');
let successMessage = $state('');
let isLoading = $state(false);
let confirmPassword = $state('');
let {
oauthProviderNames
}: {
oauthProviderNames: string[];
} = $props();
async function handleSignup(event: Event) {
event.preventDefault();
async function handleSignup(event: Event) {
event.preventDefault();
isLoading = true;
errorMessage = '';
successMessage = '';
const {response} = await client.POST('/api/v1/auth/register', {
body: {
email: email,
password: password,
is_active: null,
is_superuser: null,
is_verified: null
}
});
isLoading = false;
isLoading = true;
errorMessage = '';
successMessage = '';
const { response } = await client.POST('/api/v1/auth/register', {
body: {
email: email,
password: password,
is_active: null,
is_superuser: null,
is_verified: null
}
});
isLoading = false;
if (response.ok) {
successMessage = 'Registration successful! Please login.';
toast.success(successMessage);
} else {
toast.error('Registration failed');
}
}
if (response.ok) {
successMessage = 'Registration successful! Please login.';
toast.success(successMessage);
} else {
toast.error('Registration failed');
}
}
</script>
<Card.Root class="mx-auto max-w-sm">
<Card.Header>
<Card.Title class="text-xl">Sign Up</Card.Title>
<Card.Description>Enter your information to create an account</Card.Description>
</Card.Header>
<Card.Content>
<form class="grid gap-4" onsubmit={handleSignup}>
<div class="grid gap-2">
<Label for="email">Email</Label>
<Input
autocomplete="email"
bind:value={email}
id="email"
placeholder="m@example.com"
required
type="email"
/>
</div>
<div class="grid gap-2">
<Label for="password">Password</Label>
<Input
autocomplete="new-password"
bind:value={password}
id="password"
required
type="password"
/>
</div>
<div class="grid gap-2">
<Label for="password">Confirm Password</Label>
<Input
autocomplete="new-password"
bind:value={confirmPassword}
id="confirm-password"
required
type="password"
/>
</div>
{#if errorMessage}
<Alert.Root variant="destructive">
<AlertCircleIcon class="size-4"/>
<Alert.Title>Error</Alert.Title>
<Alert.Description>{errorMessage}</Alert.Description>
</Alert.Root>
{/if}
{#if successMessage}
<Alert.Root variant="default">
<CheckCircle2Icon class="size-4"/>
<Alert.Title>Success</Alert.Title>
<Alert.Description>{successMessage}</Alert.Description>
</Alert.Root>
{/if}
{#if isLoading}
<LoadingBar/>
{/if}
<Button
class="w-full"
disabled={isLoading || password !== confirmPassword || password === ''}
type="submit"
>Create an account
</Button>
</form>
{#each oauthProviderNames as name (name)}
<div
class="after:border-border relative mt-2 text-center text-sm after:absolute after:inset-0 after:top-1/2 after:z-0 after:flex after:items-center after:border-t"
>
<span class="bg-background text-muted-foreground relative z-10 px-2">
Or continue with
</span>
</div>
<Button class="mt-2 w-full" onclick={() => handleOauth(name)} variant="outline"
>Login with {name}</Button
>
{/each}
<div class="mt-4 text-center text-sm">
<Button href="{base}/login/" variant="link">Already have an account? Login</Button>
</div>
</Card.Content>
<Card.Header>
<Card.Title class="text-xl">Sign Up</Card.Title>
<Card.Description>Enter your information to create an account</Card.Description>
</Card.Header>
<Card.Content>
<form class="grid gap-4" onsubmit={handleSignup}>
<div class="grid gap-2">
<Label for="email">Email</Label>
<Input
autocomplete="email"
bind:value={email}
id="email"
placeholder="m@example.com"
required
type="email"
/>
</div>
<div class="grid gap-2">
<Label for="password">Password</Label>
<Input
autocomplete="new-password"
bind:value={password}
id="password"
required
type="password"
/>
</div>
<div class="grid gap-2">
<Label for="password">Confirm Password</Label>
<Input
autocomplete="new-password"
bind:value={confirmPassword}
id="confirm-password"
required
type="password"
/>
</div>
{#if errorMessage}
<Alert.Root variant="destructive">
<AlertCircleIcon class="size-4" />
<Alert.Title>Error</Alert.Title>
<Alert.Description>{errorMessage}</Alert.Description>
</Alert.Root>
{/if}
{#if successMessage}
<Alert.Root variant="default">
<CheckCircle2Icon class="size-4" />
<Alert.Title>Success</Alert.Title>
<Alert.Description>{successMessage}</Alert.Description>
</Alert.Root>
{/if}
{#if isLoading}
<LoadingBar />
{/if}
<Button
class="w-full"
disabled={isLoading || password !== confirmPassword || password === ''}
type="submit"
>Create an account
</Button>
</form>
{#each oauthProviderNames as name (name)}
<div
class="after:border-border relative mt-2 text-center text-sm after:absolute after:inset-0 after:top-1/2 after:z-0 after:flex after:items-center after:border-t"
>
<span class="bg-background text-muted-foreground relative z-10 px-2">
Or continue with
</span>
</div>
<Button class="mt-2 w-full" onclick={() => handleOauth(name)} variant="outline"
>Login with {name}</Button
>
{/each}
<div class="mt-4 text-center text-sm">
<Button href="{base}/login/" variant="link">Already have an account? Login</Button>
</div>
</Card.Content>
</Card.Root>

View File

@@ -9,11 +9,11 @@
import { Input } from '$lib/components/ui/input/index.js';
import { invalidateAll } from '$app/navigation';
import client from '$lib/api';
import type {components} from "$lib/api/api";
import type { components } from '$lib/api/api';
let { users }: { users: components["schemas"]["UserRead"] [] } = $props();
let { users }: { users: components['schemas']['UserRead'][] } = $props();
let sortedUsers = $derived(users.sort((a, b) => a.email.localeCompare(b.email)));
let selectedUser: components["schemas"]["UserRead"] | null = $state(null);
let selectedUser: components['schemas']['UserRead'] | null = $state(null);
let newPassword: string = $state('');
let newEmail: string = $state('');
let dialogOpen = $state(false);

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import { getContext } from 'svelte';
import type {components} from "$lib/api/api";
const user: () => components["schemas"]["UserRead"] = getContext('user');
import type { components } from '$lib/api/api';
const user: () => components['schemas']['UserRead'] = getContext('user');
</script>
<span class="truncate font-semibold">{user().email}</span>