fix formatting

This commit is contained in:
maxDorninger
2025-05-25 20:31:48 +02:00
parent 729a7ed647
commit b2dc7a18a6
39 changed files with 855 additions and 832 deletions

View File

@@ -1,85 +1,89 @@
<script lang="ts">
import {Button} from '$lib/components/ui/button/index.js';
import {env} from '$env/dynamic/public';
import * as Card from '$lib/components/ui/card/index.js';
import {ImageOff} from 'lucide-svelte';
import {goto} from '$app/navigation';
import {base} from '$app/paths';
import type {MetaDataProviderShowSearchResult} from '$lib/types.js';
import {toOptimizedURL} from "sveltekit-image-optimize/components";
import {Button} from '$lib/components/ui/button/index.js';
import {env} from '$env/dynamic/public';
import * as Card from '$lib/components/ui/card/index.js';
import {ImageOff} from 'lucide-svelte';
import {goto} from '$app/navigation';
import {base} from '$app/paths';
import type {MetaDataProviderShowSearchResult} from '$lib/types.js';
import {toOptimizedURL} from 'sveltekit-image-optimize/components';
let loading = $state(false);
let errorMessage = $state(null);
let {result}: { result: MetaDataProviderShowSearchResult } = $props();
console.log('Add Show Card Result: ', result);
let loading = $state(false);
let errorMessage = $state(null);
let {result}: { result: MetaDataProviderShowSearchResult } = $props();
console.log('Add Show Card Result: ', result);
async function addShow() {
loading = true;
let url = new URL(env.PUBLIC_API_URL + '/tv/shows');
url.searchParams.append('show_id', String(result.external_id));
url.searchParams.append('metadata_provider', result.metadata_provider);
const response = await fetch(url, {
method: 'POST',
credentials: 'include'
});
let responseData = await response.json();
console.log('Added Show: Response Data: ', responseData);
if (response.ok) {
await goto(base + '/dashboard/tv/' + responseData.id);
} else {
errorMessage = 'Error occurred: ' + responseData;
}
loading = false;
}
async function addShow() {
loading = true;
let url = new URL(env.PUBLIC_API_URL + '/tv/shows');
url.searchParams.append('show_id', String(result.external_id));
url.searchParams.append('metadata_provider', result.metadata_provider);
const response = await fetch(url, {
method: 'POST',
credentials: 'include'
});
let responseData = await response.json();
console.log('Added Show: Response Data: ', responseData);
if (response.ok) {
await goto(base + '/dashboard/tv/' + responseData.id);
} else {
errorMessage = 'Error occurred: ' + responseData;
}
loading = false;
}
</script>
<Card.Root class="h-full max-w-sm">
<Card.Header>
<Card.Title class="h-12 overflow-hidden leading-tight flex items-center">
{result.name}
{#if result.year != null}
({result.year})
{/if}
</Card.Title>
<Card.Description
class="truncate">{result.overview !== "" ? result.overview : "No overview available"}</Card.Description>
</Card.Header>
<Card.Content class="w-full h-96 flex items-center justify-center">
{#if result.poster_path != null}
<img
class="max-h-full max-w-full object-contain rounded-lg"
src={toOptimizedURL(result.poster_path)}
alt="{result.name}'s Poster Image"
/>
{:else}
<div class="w-full h-full flex items-center justify-center">
<ImageOff class="w-12 h-12 text-gray-400"/>
</div>
{/if}
</Card.Content>
<Card.Footer class="flex flex-col gap-2 items-start p-4 bg-card rounded-b-lg border-t">
<Button
class="w-full font-semibold"
disabled={result.added || loading}
onclick={() => addShow(result)}
>
{#if loading}
<span class="animate-pulse">Loading...</span>
{:else}
{result.added ? 'Show already exists' : 'Add Show'}
{/if}
</Button>
<div class="flex items-center gap-2 w-full">
{#if result.vote_average != null}
<span class="text-sm text-yellow-600 font-medium flex items-center">
<svg class="w-4 h-4 mr-1 text-yellow-400" fill="currentColor" viewBox="0 0 20 20"><path
d="M10 15l-5.878 3.09 1.122-6.545L.488 6.91l6.561-.955L10 0l2.951 5.955 6.561.955-4.756 4.635 1.122 6.545z"/></svg>
Rating: {Math.round(result.vote_average)}/10
</span>
{/if}
</div>
{#if errorMessage}
<p class="text-xs text-red-500 bg-red-50 rounded px-2 py-1 w-full">{errorMessage}</p>
{/if}
</Card.Footer>
<Card.Header>
<Card.Title class="flex h-12 items-center overflow-hidden leading-tight">
{result.name}
{#if result.year != null}
({result.year})
{/if}
</Card.Title>
<Card.Description class="truncate"
>{result.overview !== '' ? result.overview : 'No overview available'}</Card.Description
>
</Card.Header>
<Card.Content class="flex h-96 w-full items-center justify-center">
{#if result.poster_path != null}
<img
class="max-h-full max-w-full rounded-lg object-contain"
src={toOptimizedURL(result.poster_path)}
alt="{result.name}'s Poster Image"
/>
{:else}
<div class="flex h-full w-full items-center justify-center">
<ImageOff class="h-12 w-12 text-gray-400"/>
</div>
{/if}
</Card.Content>
<Card.Footer class="flex flex-col items-start gap-2 rounded-b-lg border-t bg-card p-4">
<Button
class="w-full font-semibold"
disabled={result.added || loading}
onclick={() => addShow(result)}
>
{#if loading}
<span class="animate-pulse">Loading...</span>
{:else}
{result.added ? 'Show already exists' : 'Add Show'}
{/if}
</Button>
<div class="flex w-full items-center gap-2">
{#if result.vote_average != null}
<span class="flex items-center text-sm font-medium text-yellow-600">
<svg class="mr-1 h-4 w-4 text-yellow-400" fill="currentColor" viewBox="0 0 20 20"
><path
d="M10 15l-5.878 3.09 1.122-6.545L.488 6.91l6.561-.955L10 0l2.951 5.955 6.561.955-4.756 4.635 1.122 6.545z"
/></svg
>
Rating: {Math.round(result.vote_average)}/10
</span>
{/if}
</div>
{#if errorMessage}
<p class="w-full rounded bg-red-50 px-2 py-1 text-xs text-red-500">{errorMessage}</p>
{/if}
</Card.Footer>
</Card.Root>

View File

@@ -1,98 +1,98 @@
<script lang="ts" module>
import {Home, Info, LifeBuoy, Send, Settings, TvIcon} from "lucide-svelte";
import {PUBLIC_VERSION} from '$env/static/public';
import {Home, Info, LifeBuoy, Send, Settings, TvIcon} from 'lucide-svelte';
import {PUBLIC_VERSION} from '$env/static/public';
const data = {
navMain: [
{
title: 'Dashboard',
url: '/dashboard',
icon: Home,
isActive: true
},
{
title: 'TV',
url: '/dashboard/tv',
icon: TvIcon,
isActive: true,
items: [
{
title: 'Add a show',
url: '/dashboard/tv/add-show'
},
{
title: 'Torrents',
url: '/dashboard/tv/torrents'
},
{
title: 'Requests',
url: '/dashboard/tv/requests'
}
]
},
{
title: 'Settings',
url: '/dashboard/settings',
icon: Settings,
isActive: true,
}
],
navSecondary: [
{
title: 'Support',
url: '#',
icon: LifeBuoy
},
{
title: 'Feedback',
url: '#',
icon: Send
},
{
title: 'About',
url: '/dashboard/about',
icon: Info
}
]
};
const data = {
navMain: [
{
title: 'Dashboard',
url: '/dashboard',
icon: Home,
isActive: true
},
{
title: 'TV',
url: '/dashboard/tv',
icon: TvIcon,
isActive: true,
items: [
{
title: 'Add a show',
url: '/dashboard/tv/add-show'
},
{
title: 'Torrents',
url: '/dashboard/tv/torrents'
},
{
title: 'Requests',
url: '/dashboard/tv/requests'
}
]
},
{
title: 'Settings',
url: '/dashboard/settings',
icon: Settings,
isActive: true
}
],
navSecondary: [
{
title: 'Support',
url: '#',
icon: LifeBuoy
},
{
title: 'Feedback',
url: '#',
icon: Send
},
{
title: 'About',
url: '/dashboard/about',
icon: Info
}
]
};
</script>
<script lang="ts">
import NavMain from '$lib/components/nav-main.svelte';
import NavSecondary from '$lib/components/nav-secondary.svelte';
import NavUser from '$lib/components/nav-user.svelte';
import * as Sidebar from '$lib/components/ui/sidebar/index.js';
import type {ComponentProps} from 'svelte';
import logo from '$lib/images/logo.svg';
import {base} from '$app/paths';
import NavMain from '$lib/components/nav-main.svelte';
import NavSecondary from '$lib/components/nav-secondary.svelte';
import NavUser from '$lib/components/nav-user.svelte';
import * as Sidebar from '$lib/components/ui/sidebar/index.js';
import type {ComponentProps} from 'svelte';
import logo from '$lib/images/logo.svg';
import {base} from '$app/paths';
let {ref = $bindable(null), ...restProps}: ComponentProps<typeof Sidebar.Root> = $props();
let {ref = $bindable(null), ...restProps}: ComponentProps<typeof Sidebar.Root> = $props();
</script>
<Sidebar.Root {...restProps} bind:ref variant="inset">
<Sidebar.Header>
<Sidebar.Menu>
<Sidebar.MenuItem>
<Sidebar.MenuButton size="lg">
{#snippet child({props})}
<a href="{base}/dashboard" {...props}>
<img class="size-12" src={logo} alt="Media Manager Logo"/>
<div class="grid flex-1 text-left text-sm leading-tight">
<span class="truncate font-semibold">Media Manager</span>
<span class="truncate text-xs">v{PUBLIC_VERSION}</span>
</div>
</a>
{/snippet}
</Sidebar.MenuButton>
</Sidebar.MenuItem>
</Sidebar.Menu>
</Sidebar.Header>
<Sidebar.Content>
<NavMain items={data.navMain}/>
<!-- <NavProjects projects={data.projects}/> -->
<NavSecondary class="mt-auto" items={data.navSecondary}/>
</Sidebar.Content>
<Sidebar.Footer>
<NavUser/>
</Sidebar.Footer>
<Sidebar.Header>
<Sidebar.Menu>
<Sidebar.MenuItem>
<Sidebar.MenuButton size="lg">
{#snippet child({props})}
<a href="{base}/dashboard" {...props}>
<img class="size-12" src={logo} alt="Media Manager Logo"/>
<div class="grid flex-1 text-left text-sm leading-tight">
<span class="truncate font-semibold">Media Manager</span>
<span class="truncate text-xs">v{PUBLIC_VERSION}</span>
</div>
</a>
{/snippet}
</Sidebar.MenuButton>
</Sidebar.MenuItem>
</Sidebar.Menu>
</Sidebar.Header>
<Sidebar.Content>
<NavMain items={data.navMain}/>
<!-- <NavProjects projects={data.projects}/> -->
<NavSecondary class="mt-auto" items={data.navSecondary}/>
</Sidebar.Content>
<Sidebar.Footer>
<NavUser/>
</Sidebar.Footer>
</Sidebar.Root>

View File

@@ -1,19 +1,19 @@
<script lang="ts">
import {env} from '$env/dynamic/public';
import {Button, buttonVariants} from '$lib/components/ui/button/index.js';
import {Input} from '$lib/components/ui/input';
import {Label} from '$lib/components/ui/label';
import {toast} from 'svelte-sonner';
import {env} from '$env/dynamic/public';
import {Button, buttonVariants} from '$lib/components/ui/button/index.js';
import {Input} from '$lib/components/ui/input';
import {Label} from '$lib/components/ui/label';
import {toast} from 'svelte-sonner';
import type {PublicIndexerQueryResult} from '$lib/types.js';
import {convertTorrentSeasonRangeToIntegerRange, getFullyQualifiedShowName} from '$lib/utils';
import {LoaderCircle} from 'lucide-svelte';
import type {PublicIndexerQueryResult} from '$lib/types.js';
import {convertTorrentSeasonRangeToIntegerRange, getFullyQualifiedShowName} from '$lib/utils';
import {LoaderCircle} from 'lucide-svelte';
import * as Dialog from '$lib/components/ui/dialog/index.js';
import * as Tabs from '$lib/components/ui/tabs/index.js';
import * as Select from '$lib/components/ui/select/index.js';
import * as Table from '$lib/components/ui/table/index.js';
let {show} = $props();
let {show} = $props();
let dialogueState = $state(false);
let selectedSeasonNumber: number = $state(1);
let torrents: PublicIndexerQueryResult[] = $state([]);
@@ -60,8 +60,8 @@
}
async function getTorrents(
season_number: number,
override: boolean = false
season_number: number,
override: boolean = false
): Promise<PublicIndexerQueryResult[]> {
isLoadingTorrents = true;
torrentsError = null;
@@ -150,14 +150,14 @@
<div class="grid w-full items-center gap-1.5">
{#if show?.seasons?.length > 0}
<Label for="season-number"
>Enter a season number from 1 to {show.seasons.at(-1).number}</Label
>Enter a season number from 1 to {show.seasons.at(-1).number}</Label
>
<Input
type="number"
class="max-w-sm"
id="season-number"
bind:value={selectedSeasonNumber}
max={show.seasons.at(-1).number}
type="number"
class="max-w-sm"
id="season-number"
bind:value={selectedSeasonNumber}
max={show.seasons.at(-1).number}
/>
<p class="text-sm text-muted-foreground">
Enter the season's number you want to search for. The first, usually 1, or the last
@@ -181,7 +181,7 @@
example a 1080p and a 4K version of a season.
</p>
<Label for="file-suffix-display"
>The files will be saved in the following directory:</Label
>The files will be saved in the following directory:</Label
>
<p class="text-sm text-muted-foreground" id="file-suffix-display">
{@render saveDirectoryPreview(show, filePathSuffix)}
@@ -198,10 +198,10 @@
{#if show?.seasons?.length > 0}
<Label for="query-override">Enter a custom query</Label>
<div class="flex w-full max-w-sm items-center space-x-2">
<Input type="text" id="query-override" bind:value={queryOverride}/>
<Input type="text" id="query-override" bind:value={queryOverride}/>
<Button
variant="secondary"
onclick={async () => {
variant="secondary"
onclick={async () => {
isLoadingTorrents = true;
torrentsError = null;
torrents = [];
@@ -223,11 +223,11 @@
</p>
<Label for="file-suffix">Filepath suffix</Label>
<Input
type="text"
class="max-w-sm"
id="file-suffix"
bind:value={filePathSuffix}
placeholder="1080P"
type="text"
class="max-w-sm"
id="file-suffix"
bind:value={filePathSuffix}
placeholder="1080P"
/>
<p class="text-sm text-muted-foreground">
This is necessary to differentiate between versions of the same season/show, for
@@ -235,7 +235,7 @@
</p>
<Label for="file-suffix-display"
>The files will be saved in the following directory:</Label
>The files will be saved in the following directory:</Label
>
<p class="text-sm text-muted-foreground" id="file-suffix-display">
{@render saveDirectoryPreview(show, filePathSuffix)}
@@ -251,7 +251,7 @@
<div class="mt-4 items-center">
{#if isLoadingTorrents}
<div class="flex w-full max-w-sm items-center space-x-2">
<LoaderCircle class="animate-spin"/>
<LoaderCircle class="animate-spin"/>
<p>Loading torrents...</p>
</div>
{:else if torrentsError}
@@ -287,9 +287,9 @@
</Table.Cell>
<Table.Cell class="text-right">
<Button
size="sm"
variant="outline"
onclick={() => {
size="sm"
variant="outline"
onclick={() => {
downloadTorrent(torrent.id);
}}
>

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import {Progress} from "$lib/components/ui/progress/index.js";
import {onMount} from "svelte";
import {Progress} from '$lib/components/ui/progress/index.js';
import {onMount} from 'svelte';
let value = $state(0);
@@ -13,4 +13,4 @@
});
</script>
<Progress value={value}/>
<Progress {value}/>

View File

@@ -12,7 +12,7 @@
let apiUrl = env.PUBLIC_API_URL;
let {oauthProvider} = $props();
let oauthProviderName = $derived(oauthProvider.oauth_name)
let oauthProviderName = $derived(oauthProvider.oauth_name);
let email = $state('');
let password = $state('');
@@ -112,19 +112,25 @@
async function handleOauth() {
try {
const response = await fetch(apiUrl + "/auth/cookie/" + oauthProviderName + "/authorize?scopes=email", {
method: 'GET',
headers: {
'Content-Type': 'application/json'
},
});
const response = await fetch(
apiUrl + '/auth/cookie/' + oauthProviderName + '/authorize?scopes=email',
{
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
}
);
if (response.ok) {
let result = await response.json();
console.log('Redirecting to OAuth provider:', oauthProviderName, "url: ", result.authorization_url);
toast.success("Redirecting to " + oauthProviderName + " for authentication...");
console.log(
'Redirecting to OAuth provider:',
oauthProviderName,
'url: ',
result.authorization_url
);
toast.success('Redirecting to ' + oauthProviderName + ' for authentication...');
window.location = result.authorization_url;
} else {
let errorText = await response.text();
toast.error(errorMessage);
@@ -143,8 +149,9 @@
<LoadingBar/>
{:then result}
{#if result.oauth_name != null}
<Button class="mt-2 w-full" onclick={() => handleOauth()} variant="outline">Login
with {result.oauth_name}</Button>
<Button class="mt-2 w-full" onclick={() => handleOauth()} variant="outline"
>Login with {result.oauth_name}</Button
>
{/if}
{/await}
{/snippet}
@@ -196,8 +203,7 @@
<div class="mt-4 text-center text-sm">
<Button onclick={() => (tabValue = 'register')} variant="link">
Don't have an account? Sign up
</Button
>
</Button>
</div>
</Card.Content>
</Card.Root>
@@ -242,12 +248,10 @@
<!-- TODO: dynamically display oauth providers based on config -->
{@render oauthLogin()}
<div class="mt-4 text-center text-sm">
<Button onclick={() => (tabValue = 'login')} variant="link"
>Already have an account? Login
</Button
>
</Button>
</div>
</Card.Content>
</Card.Root>

View File

@@ -23,7 +23,7 @@
</script>
<Sidebar.Group class="group-data-[collapsible=icon]:hidden">
<Sidebar.GroupLabel><!-- TODO: what to set this label to?? --> </Sidebar.GroupLabel>
<Sidebar.GroupLabel><!-- TODO: what to set this label to?? --></Sidebar.GroupLabel>
<Sidebar.Menu>
{#each projects as item (item.name)}
<Sidebar.MenuItem>

View File

@@ -9,8 +9,8 @@
import UserDetails from './user-details.svelte';
import UserRound from '@lucide/svelte/icons/user-round';
import {handleLogout} from '$lib/utils.ts';
import {goto} from "$app/navigation";
import {base} from "$app/paths";
import {goto} from '$app/navigation';
import {base} from '$app/paths';
const sidebar = useSidebar();
</script>
@@ -57,7 +57,7 @@
</div>
</DropdownMenu.Label>
<DropdownMenu.Separator/>
<DropdownMenu.Item onclick={() => goto(base+'/dashboard/settings#me')}>
<DropdownMenu.Item onclick={() => goto(base + '/dashboard/settings#me')}>
My Account
</DropdownMenu.Item>
<DropdownMenu.Separator/>

View File

@@ -1,25 +1,25 @@
<script lang="ts">
import Autoplay from "embla-carousel-autoplay";
import * as Carousel from "$lib/components/ui/carousel/index.js";
import type {MetaDataProviderShowSearchResult} from "$lib/types";
import AddShowCard from "$lib/components/add-show-card.svelte";
import Autoplay from 'embla-carousel-autoplay';
import * as Carousel from '$lib/components/ui/carousel/index.js';
import type {MetaDataProviderShowSearchResult} from '$lib/types';
import AddShowCard from '$lib/components/add-show-card.svelte';
let {shows}: { shows: MetaDataProviderShowSearchResult } = $props();
</script>
<Carousel.Root
opts={{
align: "start",
loop: true,
}}
align: 'start',
loop: true
}}
plugins={[
Autoplay({
delay: 2000,
stopOnInteraction: false,
stopOnMouseEnter: true,
playOnInit: true,
}),
]}
Autoplay({
delay: 2000,
stopOnInteraction: false,
stopOnMouseEnter: true,
playOnInit: true
})
]}
>
<Carousel.Content class="-ml-1">
{#each shows as show}

View File

@@ -152,8 +152,7 @@
<Dialog.Footer>
<Button disabled={isSubmittingRequest} onclick={() => (dialogOpen = false)} variant="outline"
>Cancel
</Button
>
</Button>
<Button disabled={isFormInvalid || isSubmittingRequest} onclick={handleRequestSeason}>
{#if isSubmittingRequest}
<LoaderCircle class="mr-2 h-4 w-4 animate-spin"/>

View File

@@ -7,8 +7,8 @@
import {Button} from '$lib/components/ui/button/index.js';
import {env} from '$env/dynamic/public';
import {toast} from 'svelte-sonner';
import {goto} from "$app/navigation";
import {base} from "$app/paths";
import {goto} from '$app/navigation';
import {base} from '$app/paths';
let {
requests,
@@ -135,7 +135,7 @@
class=""
size="sm"
variant="outline"
onclick={() => goto(base+"/dashboard/tv/"+request.show.id)}
onclick={() => goto(base + '/dashboard/tv/' + request.show.id)}
>
Download manually
</Button>

View File

@@ -1,75 +1,69 @@
<script lang="ts" module>
import type {WithElementRef} from "bits-ui";
import type {HTMLAnchorAttributes, HTMLButtonAttributes} from "svelte/elements";
import {type VariantProps, tv} from "tailwind-variants";
import type {WithElementRef} from 'bits-ui';
import type {HTMLAnchorAttributes, HTMLButtonAttributes} from 'svelte/elements';
import {type VariantProps, tv} from 'tailwind-variants';
export const buttonVariants = tv({
base: "focus-visible:ring-ring inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90 shadow",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90 shadow-sm",
outline:
"border-input bg-background hover:bg-accent hover:text-accent-foreground border shadow-sm",
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80 shadow-sm",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2",
sm: "h-8 rounded-md px-3 text-xs",
lg: "h-10 rounded-md px-8",
icon: "h-9 w-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
});
export const buttonVariants = tv({
base: 'focus-visible:ring-ring inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
variants: {
variant: {
default: 'bg-primary text-primary-foreground hover:bg-primary/90 shadow',
destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90 shadow-sm',
outline:
'border-input bg-background hover:bg-accent hover:text-accent-foreground border shadow-sm',
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80 shadow-sm',
ghost: 'hover:bg-accent hover:text-accent-foreground',
link: 'text-primary underline-offset-4 hover:underline'
},
size: {
default: 'h-9 px-4 py-2',
sm: 'h-8 rounded-md px-3 text-xs',
lg: 'h-10 rounded-md px-8',
icon: 'h-9 w-9'
}
},
defaultVariants: {
variant: 'default',
size: 'default'
}
});
export type ButtonVariant = VariantProps<typeof buttonVariants>["variant"];
export type ButtonSize = VariantProps<typeof buttonVariants>["size"];
export type ButtonVariant = VariantProps<typeof buttonVariants>['variant'];
export type ButtonSize = VariantProps<typeof buttonVariants>['size'];
export type ButtonProps = WithElementRef<HTMLButtonAttributes> &
WithElementRef<HTMLAnchorAttributes> & {
variant?: ButtonVariant;
size?: ButtonSize;
};
export type ButtonProps = WithElementRef<HTMLButtonAttributes> &
WithElementRef<HTMLAnchorAttributes> & {
variant?: ButtonVariant;
size?: ButtonSize;
};
</script>
<script lang="ts">
import {cn} from "$lib/utils.js";
import {cn} from '$lib/utils.js';
let {
class: className,
variant = "default",
size = "default",
ref = $bindable(null),
href = undefined,
type = "button",
children,
...restProps
}: ButtonProps = $props();
let {
class: className,
variant = 'default',
size = 'default',
ref = $bindable(null),
href = undefined,
type = 'button',
children,
...restProps
}: ButtonProps = $props();
</script>
{#if href}
<a
bind:this={ref}
class={cn(buttonVariants({ variant, size }), className)}
{href}
{...restProps}
>
{@render children?.()}
</a>
<a bind:this={ref} class={cn(buttonVariants({ variant, size }), className)} {href} {...restProps}>
{@render children?.()}
</a>
{:else}
<button
bind:this={ref}
class={cn(buttonVariants({ variant, size }), className)}
{type}
{...restProps}
>
{@render children?.()}
</button>
<button
bind:this={ref}
class={cn(buttonVariants({ variant, size }), className)}
{type}
{...restProps}
>
{@render children?.()}
</button>
{/if}

View File

@@ -2,8 +2,8 @@ import Root, {
type ButtonProps,
type ButtonSize,
type ButtonVariant,
buttonVariants,
} from "./button.svelte";
buttonVariants
} from './button.svelte';
export {
Root,
@@ -13,5 +13,5 @@ export {
buttonVariants,
type ButtonProps,
type ButtonSize,
type ButtonVariant,
type ButtonVariant
};

View File

@@ -1,9 +1,9 @@
<script lang="ts">
import emblaCarouselSvelte from "embla-carousel-svelte";
import type {WithElementRef} from "bits-ui";
import type {HTMLAttributes} from "svelte/elements";
import {getEmblaContext} from "./context.js";
import {cn} from "$lib/utils.js";
import emblaCarouselSvelte from 'embla-carousel-svelte';
import type {WithElementRef} from 'bits-ui';
import type {HTMLAttributes} from 'svelte/elements';
import {getEmblaContext} from './context.js';
import {cn} from '$lib/utils.js';
let {
ref = $bindable(null),
@@ -12,7 +12,7 @@
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
const emblaCtx = getEmblaContext("<Carousel.Content/>");
const emblaCtx = getEmblaContext('<Carousel.Content/>');
</script>
<!-- svelte-ignore event_directive_deprecated -->
@@ -21,20 +21,20 @@
on:emblaInit={emblaCtx.onInit}
use:emblaCarouselSvelte={{
options: {
container: "[data-embla-container]",
slides: "[data-embla-slide]",
container: '[data-embla-container]',
slides: '[data-embla-slide]',
...emblaCtx.options,
axis: emblaCtx.orientation === "horizontal" ? "x" : "y",
axis: emblaCtx.orientation === 'horizontal' ? 'x' : 'y'
},
plugins: emblaCtx.plugins,
plugins: emblaCtx.plugins
}}
>
<div
{...restProps}
bind:this={ref}
class={cn(
"flex",
emblaCtx.orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
'flex',
emblaCtx.orientation === 'horizontal' ? '-ml-4' : '-mt-4 flex-col',
className
)}
data-embla-container=""

View File

@@ -1,8 +1,8 @@
<script lang="ts">
import type {WithElementRef} from "bits-ui";
import type {HTMLAttributes} from "svelte/elements";
import {getEmblaContext} from "./context.js";
import {cn} from "$lib/utils.js";
import type {WithElementRef} from 'bits-ui';
import type {HTMLAttributes} from 'svelte/elements';
import {getEmblaContext} from './context.js';
import {cn} from '$lib/utils.js';
let {
ref = $bindable(null),
@@ -11,7 +11,7 @@
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
const emblaCtx = getEmblaContext("<Carousel.Item/>");
const emblaCtx = getEmblaContext('<Carousel.Item/>');
</script>
<div
@@ -19,8 +19,8 @@
aria-roledescription="slide"
bind:this={ref}
class={cn(
"min-w-0 shrink-0 grow-0 basis-full",
emblaCtx.orientation === "horizontal" ? "pl-4" : "pt-4",
'min-w-0 shrink-0 grow-0 basis-full',
emblaCtx.orientation === 'horizontal' ? 'pl-4' : 'pt-4',
className
)}
data-embla-slide=""

View File

@@ -1,29 +1,29 @@
<script lang="ts">
import ArrowRight from "@lucide/svelte/icons/arrow-right";
import type {WithoutChildren} from "bits-ui";
import {getEmblaContext} from "./context.js";
import {cn} from "$lib/utils.js";
import {Button, type Props} from "$lib/components/ui/button/index.js";
import ArrowRight from '@lucide/svelte/icons/arrow-right';
import type {WithoutChildren} from 'bits-ui';
import {getEmblaContext} from './context.js';
import {cn} from '$lib/utils.js';
import {Button, type Props} from '$lib/components/ui/button/index.js';
let {
ref = $bindable(null),
class: className,
variant = "outline",
size = "icon",
variant = 'outline',
size = 'icon',
...restProps
}: WithoutChildren<Props> = $props();
const emblaCtx = getEmblaContext("<Carousel.Next/>");
const emblaCtx = getEmblaContext('<Carousel.Next/>');
</script>
<Button
{...restProps}
bind:ref
class={cn(
"absolute size-8 touch-manipulation rounded-full",
emblaCtx.orientation === "horizontal"
? "-right-12 top-1/2 -translate-y-1/2"
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
'absolute size-8 touch-manipulation rounded-full',
emblaCtx.orientation === 'horizontal'
? '-right-12 top-1/2 -translate-y-1/2'
: '-bottom-12 left-1/2 -translate-x-1/2 rotate-90',
className
)}
disabled={!emblaCtx.canScrollNext}

View File

@@ -1,29 +1,29 @@
<script lang="ts">
import ArrowLeft from "@lucide/svelte/icons/arrow-left";
import type {WithoutChildren} from "bits-ui";
import {getEmblaContext} from "./context.js";
import {cn} from "$lib/utils.js";
import {Button, type Props} from "$lib/components/ui/button/index.js";
import ArrowLeft from '@lucide/svelte/icons/arrow-left';
import type {WithoutChildren} from 'bits-ui';
import {getEmblaContext} from './context.js';
import {cn} from '$lib/utils.js';
import {Button, type Props} from '$lib/components/ui/button/index.js';
let {
ref = $bindable(null),
class: className,
variant = "outline",
size = "icon",
variant = 'outline',
size = 'icon',
...restProps
}: WithoutChildren<Props> = $props();
const emblaCtx = getEmblaContext("<Carousel.Previous/>");
const emblaCtx = getEmblaContext('<Carousel.Previous/>');
</script>
<Button
{...restProps}
bind:ref
class={cn(
"absolute size-8 touch-manipulation rounded-full",
emblaCtx.orientation === "horizontal"
? "-left-12 top-1/2 -translate-y-1/2"
: "-top-12 left-1/2 -translate-x-1/2 rotate-90",
'absolute size-8 touch-manipulation rounded-full',
emblaCtx.orientation === 'horizontal'
? '-left-12 top-1/2 -translate-y-1/2'
: '-top-12 left-1/2 -translate-x-1/2 rotate-90',
className
)}
disabled={!emblaCtx.canScrollPrev}

View File

@@ -3,16 +3,16 @@
type CarouselAPI,
type CarouselProps,
type EmblaContext,
setEmblaContext,
} from "./context.js";
import {cn} from "$lib/utils.js";
setEmblaContext
} from './context.js';
import {cn} from '$lib/utils.js';
let {
opts = {},
plugins = [],
setApi = () => {
},
orientation = "horizontal",
orientation = 'horizontal',
class: className,
children,
...restProps
@@ -31,7 +31,7 @@
onInit,
scrollSnaps: [],
selectedIndex: 0,
scrollTo,
scrollTo
});
setEmblaContext(carouselState);
@@ -58,16 +58,16 @@
$effect(() => {
if (carouselState.api) {
onSelect(carouselState.api);
carouselState.api.on("select", onSelect);
carouselState.api.on("reInit", onSelect);
carouselState.api.on('select', onSelect);
carouselState.api.on('reInit', onSelect);
}
});
function handleKeyDown(e: KeyboardEvent) {
if (e.key === "ArrowLeft") {
if (e.key === 'ArrowLeft') {
e.preventDefault();
scrollPrev();
} else if (e.key === "ArrowRight") {
} else if (e.key === 'ArrowRight') {
e.preventDefault();
scrollNext();
}
@@ -85,11 +85,11 @@
$effect(() => {
return () => {
carouselState.api?.off("select", onSelect);
carouselState.api?.off('select', onSelect);
};
});
</script>
<div {...restProps} aria-roledescription="carousel" class={cn("relative", className)} role="region">
<div {...restProps} aria-roledescription="carousel" class={cn('relative', className)} role="region">
{@render children?.()}
</div>

View File

@@ -1,11 +1,11 @@
import type {EmblaCarouselSvelteType} from "embla-carousel-svelte";
import type emblaCarouselSvelte from "embla-carousel-svelte";
import {getContext, hasContext, setContext} from "svelte";
import type {WithElementRef} from "bits-ui";
import type {HTMLAttributes} from "svelte/elements";
import type {EmblaCarouselSvelteType} from 'embla-carousel-svelte';
import type emblaCarouselSvelte from 'embla-carousel-svelte';
import {getContext, hasContext, setContext} from 'svelte';
import type {WithElementRef} from 'bits-ui';
import type {HTMLAttributes} from 'svelte/elements';
export type CarouselAPI =
NonNullable<NonNullable<EmblaCarouselSvelteType["$$_attributes"]>["on:emblaInit"]> extends (
NonNullable<NonNullable<EmblaCarouselSvelteType['$$_attributes']>['on:emblaInit']> extends (
evt: CustomEvent<infer CarouselAPI>
) => void
? CarouselAPI
@@ -13,8 +13,8 @@ export type CarouselAPI =
type EmblaCarouselConfig = NonNullable<Parameters<typeof emblaCarouselSvelte>[1]>;
export type CarouselOptions = EmblaCarouselConfig["options"];
export type CarouselPlugins = EmblaCarouselConfig["plugins"];
export type CarouselOptions = EmblaCarouselConfig['options'];
export type CarouselPlugins = EmblaCarouselConfig['plugins'];
////
@@ -22,14 +22,14 @@ export type CarouselProps = {
opts?: CarouselOptions;
plugins?: CarouselPlugins;
setApi?: (api: CarouselAPI | undefined) => void;
orientation?: "horizontal" | "vertical";
orientation?: 'horizontal' | 'vertical';
} & WithElementRef<HTMLAttributes<HTMLDivElement>>;
const EMBLA_CAROUSEL_CONTEXT = Symbol("EMBLA_CAROUSEL_CONTEXT");
const EMBLA_CAROUSEL_CONTEXT = Symbol('EMBLA_CAROUSEL_CONTEXT');
export type EmblaContext = {
api: CarouselAPI | undefined;
orientation: "horizontal" | "vertical";
orientation: 'horizontal' | 'vertical';
scrollNext: () => void;
scrollPrev: () => void;
canScrollNext: boolean;
@@ -48,7 +48,7 @@ export function setEmblaContext(config: EmblaContext): EmblaContext {
return config;
}
export function getEmblaContext(name = "This component") {
export function getEmblaContext(name = 'This component') {
if (!hasContext(EMBLA_CAROUSEL_CONTEXT)) {
throw new Error(`${name} must be used within a <Carousel.Root> component`);
}

View File

@@ -1,8 +1,8 @@
import Root from "./carousel.svelte";
import Content from "./carousel-content.svelte";
import Item from "./carousel-item.svelte";
import Previous from "./carousel-previous.svelte";
import Next from "./carousel-next.svelte";
import Root from './carousel.svelte';
import Content from './carousel-content.svelte';
import Item from './carousel-item.svelte';
import Previous from './carousel-previous.svelte';
import Next from './carousel-next.svelte';
export {
Root,
@@ -15,5 +15,5 @@ export {
Content as CarouselContent,
Item as CarouselItem,
Previous as CarouselPrevious,
Next as CarouselNext,
Next as CarouselNext
};

View File

@@ -1,7 +1,7 @@
import Root from "./progress.svelte";
import Root from './progress.svelte';
export {
Root,
//
Root as Progress,
Root as Progress
};

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import {Progress as ProgressPrimitive, type WithoutChildrenOrChild} from "bits-ui";
import {cn} from "$lib/utils.js";
import {Progress as ProgressPrimitive, type WithoutChildrenOrChild} from 'bits-ui';
import {cn} from '$lib/utils.js';
let {
ref = $bindable(null),
@@ -14,11 +14,11 @@
<ProgressPrimitive.Root
{...restProps}
bind:ref
class={cn("bg-primary/20 relative h-2 w-full overflow-hidden rounded-full", className)}
class={cn('relative h-2 w-full overflow-hidden rounded-full bg-primary/20', className)}
{value}
>
<div
class="bg-primary h-full w-full flex-1 transition-all"
class="h-full w-full flex-1 bg-primary transition-all"
style={`transform: translateX(-${100 - (100 * (value ?? 0)) / (max ?? 1)}%)`}
></div>
</ProgressPrimitive.Root>

View File

@@ -1,5 +1,5 @@
import {Tooltip as TooltipPrimitive} from "bits-ui";
import Content from "./tooltip-content.svelte";
import {Tooltip as TooltipPrimitive} from 'bits-ui';
import Content from './tooltip-content.svelte';
const Root = TooltipPrimitive.Root;
const Trigger = TooltipPrimitive.Trigger;
@@ -14,5 +14,5 @@ export {
Root as Tooltip,
Content as TooltipContent,
Trigger as TooltipTrigger,
Provider as TooltipProvider,
Provider as TooltipProvider
};

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import {Tooltip as TooltipPrimitive} from "bits-ui";
import {cn} from "$lib/utils.js";
import {Tooltip as TooltipPrimitive} from 'bits-ui';
import {cn} from '$lib/utils.js';
let {
ref = $bindable(null),
@@ -14,7 +14,7 @@
{...restProps}
bind:ref
class={cn(
"bg-primary text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 overflow-hidden rounded-md px-3 py-1.5 text-xs",
'z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
className
)}
{sideOffset}

View File

@@ -2,13 +2,13 @@
import type {User} from '$lib/types.js';
import CheckmarkX from '$lib/components/checkmark-x.svelte';
import * as Table from '$lib/components/ui/table/index.js';
import {Button} from "$lib/components/ui/button/index.js";
import {env} from "$env/dynamic/public";
import {toast} from "svelte-sonner";
import * as Dialog from "$lib/components/ui/dialog/index.js";
import {Label} from "$lib/components/ui/label/index.js";
import * as RadioGroup from "$lib/components/ui/radio-group/index.js";
import {Input} from "$lib/components/ui/input/index.js";
import {Button} from '$lib/components/ui/button/index.js';
import {env} from '$env/dynamic/public';
import {toast} from 'svelte-sonner';
import * as Dialog from '$lib/components/ui/dialog/index.js';
import {Label} from '$lib/components/ui/label/index.js';
import * as RadioGroup from '$lib/components/ui/radio-group/index.js';
import {Input} from '$lib/components/ui/input/index.js';
let {users}: { users: User[] } = $props();
let sortedUsers = $derived(users.sort((a, b) => a.email.localeCompare(b.email)));
@@ -30,7 +30,7 @@
is_verified: selectedUser.is_verified,
is_active: selectedUser.is_active,
is_superuser: selectedUser.is_superuser,
...newPassword !== "" && {password: newPassword}
...(newPassword !== '' && {password: newPassword})
})
});
@@ -38,7 +38,7 @@
toast.success(`User ${selectedUser.email} updated successfully.`);
dialogOpen = false;
const idx = sortedUsers.findIndex(u => u.id === selectedUser.id);
const idx = sortedUsers.findIndex((u) => u.id === selectedUser.id);
if (idx !== -1) {
sortedUsers[idx] = selectedUser;
}
@@ -51,7 +51,9 @@
}
} catch (error) {
console.error('Error updating user:', error);
toast.error('Error updating user: ' + (error instanceof Error ? error.message : String(error)));
toast.error(
'Error updating user: ' + (error instanceof Error ? error.message : String(error))
);
} finally {
newPassword = '';
}
@@ -84,7 +86,13 @@
<CheckmarkX state={user.is_superuser}/>
</Table.Cell>
<Table.Cell>
<Button variant="secondary" onclick={() => {selectedUser=user; dialogOpen=true}}>
<Button
variant="secondary"
onclick={() => {
selectedUser = user;
dialogOpen = true;
}}
>
Edit
</Button>
</Table.Cell>
@@ -93,23 +101,24 @@
</Table.Body>
</Table.Root>
<Dialog.Root bind:open={dialogOpen}>
<Dialog.Content class="max-w-[600px] w-full rounded-lg shadow-lg bg-white p-6">
<Dialog.Content class="w-full max-w-[600px] rounded-lg bg-white p-6 shadow-lg">
<Dialog.Header>
<Dialog.Title class="text-xl font-semibold mb-1">
Edit user
</Dialog.Title>
<Dialog.Description class="text-sm text-gray-500 mb-4">
<Dialog.Title class="mb-1 text-xl font-semibold">Edit user</Dialog.Title>
<Dialog.Description class="mb-4 text-sm text-gray-500">
Edit {selectedUser?.email}
</Dialog.Description>
</Dialog.Header>
<div class="space-y-6">
<!-- Verified -->
<div>
<Label class="block text-sm font-medium mb-1" for="verified">Verified</Label>
<Label class="mb-1 block text-sm font-medium" for="verified">Verified</Label>
<RadioGroup.Root
class="flex gap-4"
onValueChange={(v) => { if (selectedUser) selectedUser.is_verified = v === 'true'; }}
value={selectedUser?.is_verified ? 'true' : 'false'}>
onValueChange={(v) => {
if (selectedUser) selectedUser.is_verified = v === 'true';
}}
value={selectedUser?.is_verified ? 'true' : 'false'}
>
<div class="flex items-center gap-1">
<RadioGroup.Item class="accent-green-600" id="verified-true" value="true"/>
<Label class="text-sm" for="verified-true">True</Label>
@@ -123,11 +132,14 @@
<hr/>
<!-- Active -->
<div>
<Label class="block text-sm font-medium mb-1" for="active">Active</Label>
<Label class="mb-1 block text-sm font-medium" for="active">Active</Label>
<RadioGroup.Root
class="flex gap-4"
onValueChange={(v) => { if (selectedUser) selectedUser.is_active = v === 'true'; }}
value={selectedUser?.is_active ? 'true' : 'false'}>
onValueChange={(v) => {
if (selectedUser) selectedUser.is_active = v === 'true';
}}
value={selectedUser?.is_active ? 'true' : 'false'}
>
<div class="flex items-center gap-1">
<RadioGroup.Item class="accent-green-600" id="active-true" value="true"/>
<Label class="text-sm" for="active-true">True</Label>
@@ -141,11 +153,14 @@
<hr/>
<!-- Super User -->
<div>
<Label class="block text-sm font-medium mb-1" for="superuser">Admin</Label>
<Label class="mb-1 block text-sm font-medium" for="superuser">Admin</Label>
<RadioGroup.Root
class="flex gap-4"
onValueChange={(v) => { if (selectedUser) selectedUser.is_superuser = v === 'true'; }}
value={selectedUser?.is_superuser ? 'true' : 'false'}>
onValueChange={(v) => {
if (selectedUser) selectedUser.is_superuser = v === 'true';
}}
value={selectedUser?.is_superuser ? 'true' : 'false'}
>
<div class="flex items-center gap-1">
<RadioGroup.Item class="accent-green-600" id="superuser-true" value="true"/>
<Label class="text-sm" for="superuser-true">True</Label>
@@ -158,7 +173,7 @@
</div>
<!-- Password -->
<div>
<Label class="block text-sm font-medium mb-1" for="superuser">Password</Label>
<Label class="mb-1 block text-sm font-medium" for="superuser">Password</Label>
<Input
bind:value={newPassword}
class="w-full"
@@ -169,8 +184,8 @@
</div>
</div>
<div class="mt-8 flex justify-end gap-2">
<Button onclick={() => dialogOpen = false} variant="outline">Cancel</Button>
<Button onclick={() => (dialogOpen = false)} variant="outline">Cancel</Button>
<Button onclick={() => saveUser()} variant="destructive">Save</Button>
</div>
</Dialog.Content>
</Dialog.Root>
</Dialog.Root>

View File

@@ -1,89 +1,88 @@
<script lang="ts">
import {Button} from "$lib/components/ui/button/index.js";
import {env} from "$env/dynamic/public";
import {toast} from "svelte-sonner";
import * as Dialog from "$lib/components/ui/dialog/index.js";
import {Label} from "$lib/components/ui/label/index.js";
import {Input} from "$lib/components/ui/input/index.js";
import {Button} from '$lib/components/ui/button/index.js';
import {env} from '$env/dynamic/public';
import {toast} from 'svelte-sonner';
import * as Dialog from '$lib/components/ui/dialog/index.js';
import {Label} from '$lib/components/ui/label/index.js';
import {Input} from '$lib/components/ui/input/index.js';
let newPassword: string = $state('');
let newEmail: string = $state('');
let dialogOpen = $state(false);
let newPassword: string = $state('');
let newEmail: string = $state('');
let dialogOpen = $state(false);
async function saveUser() {
try {
const response = await fetch(`${env.PUBLIC_API_URL}/users/me`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json'
},
credentials: 'include',
body: JSON.stringify({
...newPassword !== "" && {password: newPassword},
...newEmail !== "" && {password: newEmail}
async function saveUser() {
try {
const response = await fetch(`${env.PUBLIC_API_URL}/users/me`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json'
},
credentials: 'include',
body: JSON.stringify({
...(newPassword !== '' && {password: newPassword}),
...(newEmail !== '' && {password: newEmail})
})
});
})
});
if (response.ok) {
toast.success(`Updated details successfully.`);
dialogOpen = false;
} else {
const errorText = await response.text();
console.error(`Failed to update user: ${response.statusText}`, errorText);
toast.error(`Failed to update user: ${response.statusText}`);
}
} catch (error) {
console.error('Error updating user:', error);
toast.error('Error updating user: ' + (error instanceof Error ? error.message : String(error)));
} finally {
newPassword = '';
newEmail = '';
}
}
if (response.ok) {
toast.success(`Updated details successfully.`);
dialogOpen = false;
} else {
const errorText = await response.text();
console.error(`Failed to update user: ${response.statusText}`, errorText);
toast.error(`Failed to update user: ${response.statusText}`);
}
} catch (error) {
console.error('Error updating user:', error);
toast.error(
'Error updating user: ' + (error instanceof Error ? error.message : String(error))
);
} finally {
newPassword = '';
newEmail = '';
}
}
</script>
<Dialog.Root bind:open={dialogOpen}>
<Dialog.Trigger>
<Button class="w-full" onclick={() => dialogOpen = true} variant="outline">
Edit my details
</Button>
</Dialog.Trigger>
<Dialog.Content class="max-w-[600px] w-full rounded-lg shadow-lg bg-white p-6">
<Dialog.Header>
<Dialog.Title class="text-xl font-semibold mb-1">
Edit User Details
</Dialog.Title>
<Dialog.Description class="text-sm text-gray-500 mb-4">
Change your email or password. Leave fields empty to not change them.
</Dialog.Description>
</Dialog.Header>
<div class="space-y-6">
<!-- Email -->
<div>
<Label class="block text-sm font-medium mb-1" for="email">Email</Label>
<Input
bind:value={newEmail}
class="w-full"
id="email"
placeholder="Keep empty to not change the email"
type="email"
/>
</div>
<!-- Password -->
<div>
<Label class="block text-sm font-medium mb-1" for="password">Password</Label>
<Input
bind:value={newPassword}
class="w-full"
id="password"
placeholder="Keep empty to not change the password"
type="password"
/>
</div>
</div>
<div class="mt-8 flex justify-end gap-2">
<Button onclick={() => saveUser()} variant="destructive">Save</Button>
</div>
</Dialog.Content>
</Dialog.Root>
<Dialog.Trigger>
<Button class="w-full" onclick={() => (dialogOpen = true)} variant="outline">
Edit my details
</Button>
</Dialog.Trigger>
<Dialog.Content class="w-full max-w-[600px] rounded-lg bg-white p-6 shadow-lg">
<Dialog.Header>
<Dialog.Title class="mb-1 text-xl font-semibold">Edit User Details</Dialog.Title>
<Dialog.Description class="mb-4 text-sm text-gray-500">
Change your email or password. Leave fields empty to not change them.
</Dialog.Description>
</Dialog.Header>
<div class="space-y-6">
<!-- Email -->
<div>
<Label class="mb-1 block text-sm font-medium" for="email">Email</Label>
<Input
bind:value={newEmail}
class="w-full"
id="email"
placeholder="Keep empty to not change the email"
type="email"
/>
</div>
<!-- Password -->
<div>
<Label class="mb-1 block text-sm font-medium" for="password">Password</Label>
<Input
bind:value={newPassword}
class="w-full"
id="password"
placeholder="Keep empty to not change the password"
type="password"
/>
</div>
</div>
<div class="mt-8 flex justify-end gap-2">
<Button onclick={() => saveUser()} variant="destructive">Save</Button>
</div>
</Dialog.Content>
</Dialog.Root>

View File

@@ -1,13 +1,13 @@
<script lang="ts">
import AppSidebar from '$lib/components/app-sidebar.svelte';
import * as Sidebar from '$lib/components/ui/sidebar/index.js';
import type {LayoutProps} from './$types';
import {setContext} from 'svelte';
import {goto} from '$app/navigation';
import {base} from '$app/paths';
import {toast} from 'svelte-sonner';
import type {LayoutProps} from './$types';
import {setContext} from 'svelte';
import {goto} from '$app/navigation';
import {base} from '$app/paths';
import {toast} from 'svelte-sonner';
let {data, children}: LayoutProps = $props();
let {data, children}: LayoutProps = $props();
console.log('Received User Data: ', data.user);
if (!data.user.is_verified) {
toast.info('Your account requires verification. Redirecting...');
@@ -17,7 +17,7 @@
</script>
<Sidebar.Provider>
<AppSidebar/>
<AppSidebar/>
<Sidebar.Inset>
{@render children()}
</Sidebar.Inset>

View File

@@ -1,52 +1,51 @@
<script lang="ts">
import {Separator} from '$lib/components/ui/separator/index.js';
import * as Sidebar from '$lib/components/ui/sidebar/index.js';
import * as Breadcrumb from '$lib/components/ui/breadcrumb/index.js';
import RecommendedShowsCarousel from '$lib/components/recommended-shows-carousel.svelte';
import LoadingBar from '$lib/components/loading-bar.svelte';
import {base} from '$app/paths';
import {page} from '$app/state';
import type {MetaDataProviderShowSearchResult} from "$lib/types";
let recommendedShows: Promise<MetaDataProviderShowSearchResult[]> = page.data.tvRecommendations;
import {Separator} from '$lib/components/ui/separator/index.js';
import * as Sidebar from '$lib/components/ui/sidebar/index.js';
import * as Breadcrumb from '$lib/components/ui/breadcrumb/index.js';
import RecommendedShowsCarousel from '$lib/components/recommended-shows-carousel.svelte';
import LoadingBar from '$lib/components/loading-bar.svelte';
import {base} from '$app/paths';
import {page} from '$app/state';
import type {MetaDataProviderShowSearchResult} from '$lib/types';
let recommendedShows: Promise<MetaDataProviderShowSearchResult[]> = page.data.tvRecommendations;
</script>
<header class="flex h-16 shrink-0 items-center gap-2">
<div class="flex items-center gap-2 px-4">
<Sidebar.Trigger class="-ml-1"/>
<Separator class="mr-2 h-4" orientation="vertical"/>
<Breadcrumb.Root>
<Breadcrumb.List>
<Breadcrumb.Item class="hidden md:block">
<Breadcrumb.Link href="{base}/dashboard">MediaManager</Breadcrumb.Link>
</Breadcrumb.Item>
<Breadcrumb.Separator class="hidden md:block"/>
<Breadcrumb.Item>
<Breadcrumb.Page>Home</Breadcrumb.Page>
</Breadcrumb.Item>
</Breadcrumb.List>
</Breadcrumb.Root>
</div>
<div class="flex items-center gap-2 px-4">
<Sidebar.Trigger class="-ml-1"/>
<Separator class="mr-2 h-4" orientation="vertical"/>
<Breadcrumb.Root>
<Breadcrumb.List>
<Breadcrumb.Item class="hidden md:block">
<Breadcrumb.Link href="{base}/dashboard">MediaManager</Breadcrumb.Link>
</Breadcrumb.Item>
<Breadcrumb.Separator class="hidden md:block"/>
<Breadcrumb.Item>
<Breadcrumb.Page>Home</Breadcrumb.Page>
</Breadcrumb.Item>
</Breadcrumb.List>
</Breadcrumb.Root>
</div>
</header>
<div class="flex flex-1 flex-col gap-4 p-4 pt-0">
<h1 class="scroll-m-20 text-center text-4xl font-extrabold tracking-tight lg:text-5xl">
Dashboard
</h1>
<div class="min-h-[100vh] flex-1 rounded-xl md:min-h-min items-center justify-center p-4">
<div class="xl:max-w-[1200px] lg:max-w-[750px] md:max-w-[500px] sm:max-w-[200px] mx-auto">
<h3 class="scroll-m-20 text-2xl font-semibold tracking-tight text-center my-4">Trending Shows</h3>
{#await recommendedShows}
<LoadingBar/>
{:then recommendations}
<RecommendedShowsCarousel shows={recommendations}/>
<h1 class="scroll-m-20 text-center text-4xl font-extrabold tracking-tight lg:text-5xl">
Dashboard
</h1>
<div class="min-h-[100vh] flex-1 items-center justify-center rounded-xl p-4 md:min-h-min">
<div class="mx-auto sm:max-w-[200px] md:max-w-[500px] lg:max-w-[750px] xl:max-w-[1200px]">
<h3 class="my-4 scroll-m-20 text-center text-2xl font-semibold tracking-tight">
Trending Shows
</h3>
{#await recommendedShows}
<LoadingBar/>
{:then recommendations}
<RecommendedShowsCarousel shows={recommendations}/>
{/await}
</div>
</div>
{/await}
</div>
</div>
<!---
<!---
<div class="grid auto-rows-min gap-4 md:grid-cols-3">
<div class="aspect-video rounded-xl bg-muted/50"></div>
<div class="aspect-video rounded-xl bg-muted/50"></div>

View File

@@ -3,15 +3,14 @@ import type {PageLoad} from './$types';
const apiUrl = env.PUBLIC_API_URL;
export const load: PageLoad = async ({fetch}) => {
const response = await fetch(apiUrl + '/tv/recommended', {
method: 'GET',
headers: {
'Content-Type': 'application/json'
},
credentials: 'include'
});
const response = await fetch(apiUrl + '/tv/recommended', {
method: 'GET',
headers: {
'Content-Type': 'application/json'
},
credentials: 'include'
});
return {tvRecommendations: await response.json()};
};
return {tvRecommendations: await response.json()};
};

View File

@@ -1,53 +1,63 @@
<script lang="ts">
import {Separator} from '$lib/components/ui/separator/index.js';
import * as Sidebar from '$lib/components/ui/sidebar/index.js';
import * as Breadcrumb from '$lib/components/ui/breadcrumb/index.js';
import {base} from '$app/paths';
import logo from '$lib/images/logo.svg';
import {PUBLIC_VERSION} from '$env/static/public';
import {Separator} from '$lib/components/ui/separator/index.js';
import * as Sidebar from '$lib/components/ui/sidebar/index.js';
import * as Breadcrumb from '$lib/components/ui/breadcrumb/index.js';
import {base} from '$app/paths';
import logo from '$lib/images/logo.svg';
import {PUBLIC_VERSION} from '$env/static/public';
</script>
<header class="flex h-16 shrink-0 items-center gap-2">
<div class="flex items-center gap-2 px-4">
<Sidebar.Trigger class="-ml-1"/>
<Separator class="mr-2 h-4" orientation="vertical"/>
<Breadcrumb.Root>
<Breadcrumb.List>
<Breadcrumb.Item class="hidden md:block">
<Breadcrumb.Link href="{base}/dashboard">MediaManager</Breadcrumb.Link>
</Breadcrumb.Item>
<Breadcrumb.Separator class="hidden md:block"/>
<Breadcrumb.Item>
<Breadcrumb.Page>About</Breadcrumb.Page>
</Breadcrumb.Item>
</Breadcrumb.List>
</Breadcrumb.Root>
</div>
<div class="flex items-center gap-2 px-4">
<Sidebar.Trigger class="-ml-1"/>
<Separator class="mr-2 h-4" orientation="vertical"/>
<Breadcrumb.Root>
<Breadcrumb.List>
<Breadcrumb.Item class="hidden md:block">
<Breadcrumb.Link href="{base}/dashboard">MediaManager</Breadcrumb.Link>
</Breadcrumb.Item>
<Breadcrumb.Separator class="hidden md:block"/>
<Breadcrumb.Item>
<Breadcrumb.Page>About</Breadcrumb.Page>
</Breadcrumb.Item>
</Breadcrumb.List>
</Breadcrumb.Root>
</div>
</header>
<div class="flex flex-col items-center justify-center w-full py-12 px-4">
<img alt="Media Manager Logo" class="w-24 h-24 mb-4" src={logo}/>
<h1 class="text-4xl font-bold mb-2">About Media Manager</h1>
<p class="text-lg text-center max-w-2xl mb-6">
<strong>Media Manager</strong> is an all-in-one solution for organizing and building your media
library. Built for simplicity and modernity, it helps you keep track of your favorite shows and movies and
explore trending content—all in one place.
</p>
<p class="text-sm text-muted-foreground mb-2">
Version: v{PUBLIC_VERSION}
</p>
<div class="text-sm text-muted-foreground mb-6 my-6 flex items-center gap-2 lg:w-1/3 sm:w-1/2">
<a class="flex items-center gap-2" href="https://www.themoviedb.org/" target="_blank">
<img alt="TMDB Logo"
class="w-20 h-auto"
src="https://www.themoviedb.org/assets/2/v4/logos/v2/blue_square_2-d537fb228cf3ded904ef09b136fe3fec72548ebc1fea3fbbd1ad9e36364db38b.svg"/>
<span>Metadata provided by TMDB. Please consider adding missing information or subscribing.</span>
</a>
</div>
<div class="text-sm text-muted-foreground mb-6 my-6 flex items-center gap-2 lg:w-1/3 sm:w-1/2">
<a class="flex items-center gap-2" href="https://thetvdb.com/subscribe" target="_blank">
<img alt="TheTVDB Logo" class="w-20 h-auto" src="https://www.thetvdb.com/images/attribution/logo2.png"/>
<span>Metadata provided by TheTVDB. Please consider adding missing information or subscribing.</span>
</a>
</div>
</div>
<div class="flex w-full flex-col items-center justify-center px-4 py-12">
<img alt="Media Manager Logo" class="mb-4 h-24 w-24" src={logo}/>
<h1 class="mb-2 text-4xl font-bold">About Media Manager</h1>
<p class="mb-6 max-w-2xl text-center text-lg">
<strong>Media Manager</strong> is an all-in-one solution for organizing and building your media library.
Built for simplicity and modernity, it helps you keep track of your favorite shows and movies and
explore trending content—all in one place.
</p>
<p class="mb-2 text-sm text-muted-foreground">
Version: v{PUBLIC_VERSION}
</p>
<div class="my-6 mb-6 flex items-center gap-2 text-sm text-muted-foreground sm:w-1/2 lg:w-1/3">
<a class="flex items-center gap-2" href="https://www.themoviedb.org/" target="_blank">
<img
alt="TMDB Logo"
class="h-auto w-20"
src="https://www.themoviedb.org/assets/2/v4/logos/v2/blue_square_2-d537fb228cf3ded904ef09b136fe3fec72548ebc1fea3fbbd1ad9e36364db38b.svg"
/>
<span
>Metadata provided by TMDB. Please consider adding missing information or subscribing.</span
>
</a>
</div>
<div class="my-6 mb-6 flex items-center gap-2 text-sm text-muted-foreground sm:w-1/2 lg:w-1/3">
<a class="flex items-center gap-2" href="https://thetvdb.com/subscribe" target="_blank">
<img
alt="TheTVDB Logo"
class="h-auto w-20"
src="https://www.thetvdb.com/images/attribution/logo2.png"
/>
<span
>Metadata provided by TheTVDB. Please consider adding missing information or subscribing.</span
>
</a>
</div>
</div>

View File

@@ -1,58 +1,58 @@
<script lang="ts">
import UserTable from '$lib/components/user-data-table.svelte';
import {page} from '$app/state';
import * as Card from "$lib/components/ui/card/index.js";
import {getContext} from "svelte";
import UserSettings from '$lib/components/user-settings.svelte';
import {Separator} from "$lib/components/ui/separator";
import * as Sidebar from '$lib/components/ui/sidebar/index.js';
import * as Breadcrumb from '$lib/components/ui/breadcrumb/index.js';
import {base} from "$app/paths";
let currentUser = getContext("user")
let users = page.data.users;
import UserTable from '$lib/components/user-data-table.svelte';
import {page} from '$app/state';
import * as Card from '$lib/components/ui/card/index.js';
import {getContext} from 'svelte';
import UserSettings from '$lib/components/user-settings.svelte';
import {Separator} from '$lib/components/ui/separator';
import * as Sidebar from '$lib/components/ui/sidebar/index.js';
import * as Breadcrumb from '$lib/components/ui/breadcrumb/index.js';
import {base} from '$app/paths';
let currentUser = getContext('user');
let users = page.data.users;
</script>
<header class="flex h-16 shrink-0 items-center gap-2">
<div class="flex items-center gap-2 px-4">
<Sidebar.Trigger class="-ml-1"/>
<Separator class="mr-2 h-4" orientation="vertical"/>
<Breadcrumb.Root>
<Breadcrumb.List>
<Breadcrumb.Item class="hidden md:block">
<Breadcrumb.Link href="{base}/dashboard">MediaManager</Breadcrumb.Link>
</Breadcrumb.Item>
<Breadcrumb.Separator class="hidden md:block"/>
<Breadcrumb.Item>
<Breadcrumb.Page>Settings</Breadcrumb.Page>
</Breadcrumb.Item>
</Breadcrumb.List>
</Breadcrumb.Root>
</div>
<div class="flex items-center gap-2 px-4">
<Sidebar.Trigger class="-ml-1"/>
<Separator class="mr-2 h-4" orientation="vertical"/>
<Breadcrumb.Root>
<Breadcrumb.List>
<Breadcrumb.Item class="hidden md:block">
<Breadcrumb.Link href="{base}/dashboard">MediaManager</Breadcrumb.Link>
</Breadcrumb.Item>
<Breadcrumb.Separator class="hidden md:block"/>
<Breadcrumb.Item>
<Breadcrumb.Page>Settings</Breadcrumb.Page>
</Breadcrumb.Item>
</Breadcrumb.List>
</Breadcrumb.Root>
</div>
</header>
<div class="flex w-full flex-1 flex-col gap-4 p-4 pt-0 max-w-[1000px] mx-auto">
<h1 class="scroll-m-20 my-6 text-center text-4xl font-extrabold tracking-tight lg:text-5xl">
Settings
</h1>
<Card.Root id="me">
<Card.Header>
<Card.Title>You</Card.Title>
<Card.Description>Change your email or password</Card.Description>
</Card.Header>
<Card.Content>
<UserSettings/>
</Card.Content>
</Card.Root>
{#if currentUser().is_superuser}
<Card.Root id="users">
<Card.Header>
<Card.Title>Users</Card.Title>
<Card.Description>Edit or delete users</Card.Description>
</Card.Header>
<Card.Content>
<UserTable bind:users={users}/>
</Card.Content>
</Card.Root>
{/if}
<div class="mx-auto flex w-full max-w-[1000px] flex-1 flex-col gap-4 p-4 pt-0">
<h1 class="my-6 scroll-m-20 text-center text-4xl font-extrabold tracking-tight lg:text-5xl">
Settings
</h1>
<Card.Root id="me">
<Card.Header>
<Card.Title>You</Card.Title>
<Card.Description>Change your email or password</Card.Description>
</Card.Header>
<Card.Content>
<UserSettings/>
</Card.Content>
</Card.Root>
{#if currentUser().is_superuser}
<Card.Root id="users">
<Card.Header>
<Card.Title>Users</Card.Title>
<Card.Description>Edit or delete users</Card.Description>
</Card.Header>
<Card.Content>
<UserTable bind:users/>
</Card.Content>
</Card.Root>
{/if}
</div>

View File

@@ -2,32 +2,32 @@ import {env} from '$env/dynamic/public';
import type {PageLoad} from './$types';
export const load: PageLoad = async ({fetch}) => {
try {
const users = await fetch(env.PUBLIC_API_URL + "/users/all", {
method: 'GET',
headers: {
'Content-Type': 'application/json'
},
credentials: 'include'
});
try {
const users = await fetch(env.PUBLIC_API_URL + '/users/all', {
method: 'GET',
headers: {
'Content-Type': 'application/json'
},
credentials: 'include'
});
if (!users.ok) {
console.error(`Failed to fetch users: ${users.statusText}`);
return {
users: null,
};
}
if (!users.ok) {
console.error(`Failed to fetch users: ${users.statusText}`);
return {
users: null
};
}
const usersData = await users.json();
console.log('Fetched users:', usersData);
const usersData = await users.json();
console.log('Fetched users:', usersData);
return {
users: usersData,
};
} catch (error) {
console.error('Error fetching users:', error);
return {
users: null,
};
}
};
return {
users: usersData
};
} catch (error) {
console.error('Error fetching users:', error);
return {
users: null
};
}
};

View File

@@ -1,79 +1,80 @@
<script lang="ts">
import {page} from '$app/state';
import * as Card from '$lib/components/ui/card/index.js';
import {env} from '$env/dynamic/public';
import {Separator} from '$lib/components/ui/separator/index.js';
import * as Sidebar from '$lib/components/ui/sidebar/index.js';
import * as Breadcrumb from '$lib/components/ui/breadcrumb/index.js';
import {toOptimizedURL} from 'sveltekit-image-optimize/components';
import {getFullyQualifiedShowName} from '$lib/utils';
import logo from '$lib/images/svelte-logo.svg';
import LoadingBar from '$lib/components/loading-bar.svelte';
import {page} from '$app/state';
import * as Card from '$lib/components/ui/card/index.js';
import {env} from '$env/dynamic/public';
import {Separator} from '$lib/components/ui/separator/index.js';
import * as Sidebar from '$lib/components/ui/sidebar/index.js';
import * as Breadcrumb from '$lib/components/ui/breadcrumb/index.js';
import {toOptimizedURL} from 'sveltekit-image-optimize/components';
import {getFullyQualifiedShowName} from '$lib/utils';
import logo from '$lib/images/svelte-logo.svg';
import LoadingBar from '$lib/components/loading-bar.svelte';
let tvShowsPromise = page.data.tvShows;
let tvShowsPromise = page.data.tvShows;
</script>
<header class="flex h-16 shrink-0 items-center gap-2">
<div class="flex items-center gap-2 px-4">
<Sidebar.Trigger class="-ml-1"/>
<Separator class="mr-2 h-4" orientation="vertical"/>
<Breadcrumb.Root>
<Breadcrumb.List>
<Breadcrumb.Item class="hidden md:block">
<Breadcrumb.Link href="/dashboard">MediaManager</Breadcrumb.Link>
</Breadcrumb.Item>
<Breadcrumb.Separator class="hidden md:block"/>
<Breadcrumb.Item>
<Breadcrumb.Link href="/dashboard">Home</Breadcrumb.Link>
</Breadcrumb.Item>
<Breadcrumb.Separator class="hidden md:block"/>
<Breadcrumb.Item>
<Breadcrumb.Page>Shows</Breadcrumb.Page>
</Breadcrumb.Item>
</Breadcrumb.List>
</Breadcrumb.Root>
</div>
<div class="flex items-center gap-2 px-4">
<Sidebar.Trigger class="-ml-1"/>
<Separator class="mr-2 h-4" orientation="vertical"/>
<Breadcrumb.Root>
<Breadcrumb.List>
<Breadcrumb.Item class="hidden md:block">
<Breadcrumb.Link href="/dashboard">MediaManager</Breadcrumb.Link>
</Breadcrumb.Item>
<Breadcrumb.Separator class="hidden md:block"/>
<Breadcrumb.Item>
<Breadcrumb.Link href="/dashboard">Home</Breadcrumb.Link>
</Breadcrumb.Item>
<Breadcrumb.Separator class="hidden md:block"/>
<Breadcrumb.Item>
<Breadcrumb.Page>Shows</Breadcrumb.Page>
</Breadcrumb.Item>
</Breadcrumb.List>
</Breadcrumb.Root>
</div>
</header>
{#snippet loadingbar()}
<div class="flex flex-col items-center justify-center w-full col-span-full py-16 animate-fade-in">
<div class="w-1/2 max-w-xs">
<LoadingBar/>
</div>
</div>
<div class="animate-fade-in col-span-full flex w-full flex-col items-center justify-center py-16">
<div class="w-1/2 max-w-xs">
<LoadingBar/>
</div>
</div>
{/snippet}
<div class="flex w-full flex-1 flex-col gap-4 p-4 pt-0">
<h1 class="scroll-m-20 text-center text-4xl font-extrabold tracking-tight lg:text-5xl">
TV Shows
</h1>
<div class="grid w-full auto-rows-min gap-4 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5">
{#await tvShowsPromise}
{@render loadingbar()}
{:then tvShowsJson}
{#await tvShowsJson.json()}
{@render loadingbar()}
{:then tvShows}
{#each tvShows as show}
<a href={'/dashboard/tv/' + show.id}>
<Card.Root class="h-full ">
<Card.Header>
<Card.Title class="h-6 truncate">{getFullyQualifiedShowName(show)}</Card.Title>
<Card.Description class="truncate">{show.overview}</Card.Description>
</Card.Header>
<Card.Content>
<img
class="aspect-9/16 center h-auto max-w-full rounded-lg object-cover"
src={toOptimizedURL(`${env.PUBLIC_API_URL}/static/image/${show.id}.jpg`)}
alt="{getFullyQualifiedShowName(show)}'s Poster Image"
on:error={(e) => {
<h1 class="scroll-m-20 text-center text-4xl font-extrabold tracking-tight lg:text-5xl">
TV Shows
</h1>
<div
class="grid w-full auto-rows-min gap-4 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5"
>
{#await tvShowsPromise}
{@render loadingbar()}
{:then tvShowsJson}
{#await tvShowsJson.json()}
{@render loadingbar()}
{:then tvShows}
{#each tvShows as show}
<a href={'/dashboard/tv/' + show.id}>
<Card.Root class="h-full ">
<Card.Header>
<Card.Title class="h-6 truncate">{getFullyQualifiedShowName(show)}</Card.Title>
<Card.Description class="truncate">{show.overview}</Card.Description>
</Card.Header>
<Card.Content>
<img
class="aspect-9/16 center h-auto max-w-full rounded-lg object-cover"
src={toOptimizedURL(`${env.PUBLIC_API_URL}/static/image/${show.id}.jpg`)}
alt="{getFullyQualifiedShowName(show)}'s Poster Image"
on:error={(e) => {
e.target.src = logo;
}}
/>
</Card.Content>
</Card.Root>
</a>
{/each}
{/await}
{/await}
</div>
/>
</Card.Content>
</Card.Root>
</a>
{/each}
{/await}
{/await}
</div>
</div>

View File

@@ -1,8 +1,8 @@
<script lang="ts">
import {setContext} from 'svelte';
import type {LayoutProps} from './$types';
import {setContext} from 'svelte';
import type {LayoutProps} from './$types';
let {data, children}: LayoutProps = $props();
let {data, children}: LayoutProps = $props();
const showData = $derived(data.showData);
setContext('show', () => showData);

View File

@@ -1,18 +1,18 @@
<script lang="ts">
import {env} from '$env/dynamic/public';
import {Separator} from '$lib/components/ui/separator/index.js';
import {env} from '$env/dynamic/public';
import {Separator} from '$lib/components/ui/separator/index.js';
import * as Sidebar from '$lib/components/ui/sidebar/index.js';
import * as Breadcrumb from '$lib/components/ui/breadcrumb/index.js';
import {goto} from '$app/navigation';
import {ImageOff} from 'lucide-svelte';
import {goto} from '$app/navigation';
import {ImageOff} from 'lucide-svelte';
import * as Table from '$lib/components/ui/table/index.js';
import {getContext} from 'svelte';
import type {RichShowTorrent, Show, User} from '$lib/types.js';
import {getFullyQualifiedShowName} from '$lib/utils';
import {toOptimizedURL} from 'sveltekit-image-optimize/components';
import {getContext} from 'svelte';
import type {RichShowTorrent, Show, User} from '$lib/types.js';
import {getFullyQualifiedShowName} from '$lib/utils';
import {toOptimizedURL} from 'sveltekit-image-optimize/components';
import DownloadSeasonDialog from '$lib/components/download-season-dialog.svelte';
import CheckmarkX from '$lib/components/checkmark-x.svelte';
import {page} from '$app/state';
import {page} from '$app/state';
import TorrentTable from '$lib/components/torrent-table.svelte';
import RequestSeasonDialog from '$lib/components/request-season-dialog.svelte';
@@ -23,22 +23,22 @@
<header class="flex h-16 shrink-0 items-center gap-2">
<div class="flex items-center gap-2 px-4">
<Sidebar.Trigger class="-ml-1"/>
<Separator class="mr-2 h-4" orientation="vertical"/>
<Sidebar.Trigger class="-ml-1"/>
<Separator class="mr-2 h-4" orientation="vertical"/>
<Breadcrumb.Root>
<Breadcrumb.List>
<Breadcrumb.Item class="hidden md:block">
<Breadcrumb.Link href="/dashboard">MediaManager</Breadcrumb.Link>
</Breadcrumb.Item>
<Breadcrumb.Separator class="hidden md:block"/>
<Breadcrumb.Separator class="hidden md:block"/>
<Breadcrumb.Item>
<Breadcrumb.Link href="/dashboard">Home</Breadcrumb.Link>
</Breadcrumb.Item>
<Breadcrumb.Separator class="hidden md:block"/>
<Breadcrumb.Separator class="hidden md:block"/>
<Breadcrumb.Item>
<Breadcrumb.Link href="/dashboard/tv">Shows</Breadcrumb.Link>
</Breadcrumb.Item>
<Breadcrumb.Separator class="hidden md:block"/>
<Breadcrumb.Separator class="hidden md:block"/>
<Breadcrumb.Item>
<Breadcrumb.Page>{getFullyQualifiedShowName(show())}</Breadcrumb.Page>
</Breadcrumb.Item>
@@ -54,15 +54,15 @@
<div class="max-h-50% w-1/3 max-w-sm rounded-xl bg-muted/50">
{#if show().id}
<img
class="aspect-9/16 h-auto w-full rounded-lg object-cover"
src={toOptimizedURL(`${env.PUBLIC_API_URL}/static/image/${show().id}.jpg`)}
alt="{show().name}'s Poster Image"
class="aspect-9/16 h-auto w-full rounded-lg object-cover"
src={toOptimizedURL(`${env.PUBLIC_API_URL}/static/image/${show().id}.jpg`)}
alt="{show().name}'s Poster Image"
/>
{:else}
<div
class="aspect-9/16 flex h-auto w-full items-center justify-center rounded-lg bg-gray-200 text-gray-500"
class="aspect-9/16 flex h-auto w-full items-center justify-center rounded-lg bg-gray-200 text-gray-500"
>
<ImageOff size={48}/>
<ImageOff size={48}/>
</div>
{/if}
</div>
@@ -72,12 +72,12 @@
</p>
</div>
<div
class="flex h-full flex-auto flex-col items-center justify-center gap-2 rounded-xl bg-muted/50 p-4"
class="flex h-full flex-auto flex-col items-center justify-center gap-2 rounded-xl bg-muted/50 p-4"
>
{#if user().is_superuser}
<DownloadSeasonDialog show={show()}/>
<DownloadSeasonDialog show={show()}/>
{/if}
<RequestSeasonDialog show={show()}/>
<RequestSeasonDialog show={show()}/>
</div>
</div>
<div class="min-h-[100vh] flex-1 rounded-xl bg-muted/50 p-4 md:min-h-min">
@@ -96,12 +96,12 @@
{#if show().seasons.length > 0}
{#each show().seasons as season (season.id)}
<Table.Row
link={true}
onclick={() => goto('/dashboard/tv/' + show().id + '/' + season.number)}
link={true}
onclick={() => goto('/dashboard/tv/' + show().id + '/' + season.number)}
>
<Table.Cell class="min-w-[10px] font-medium">{season.number}</Table.Cell>
<Table.Cell class="min-w-[10px] font-medium">
<CheckmarkX state={season.downloaded}/>
<CheckmarkX state={season.downloaded}/>
</Table.Cell>
<Table.Cell class="min-w-[50px]">{season.name}</Table.Cell>
<Table.Cell class="max-w-[300px] truncate">{season.overview}</Table.Cell>
@@ -118,7 +118,7 @@
</div>
<div class="min-h-[100vh] flex-1 rounded-xl bg-muted/50 p-4 md:min-h-min">
<div class="w-full overflow-x-auto">
<TorrentTable torrents={torrents.torrents}/>
<TorrentTable torrents={torrents.torrents}/>
</div>
</div>
</div>

View File

@@ -13,7 +13,9 @@
const SeasonNumber = page.params.SeasonNumber;
let seasonFiles: PublicSeasonFile[] = $state(page.data.files);
let show: Show = getContext('show');
let season: Season = $derived(show().seasons.find((item) => item.number === parseInt(SeasonNumber)));
let season: Season = $derived(
show().seasons.find((item) => item.number === parseInt(SeasonNumber))
);
console.log('loaded files', seasonFiles);
</script>

View File

@@ -1,9 +1,9 @@
<script lang="ts">
import {page} from '$app/state';
import {Separator} from '$lib/components/ui/separator/index.js';
import {page} from '$app/state';
import {Separator} from '$lib/components/ui/separator/index.js';
import * as Sidebar from '$lib/components/ui/sidebar/index.js';
import * as Breadcrumb from '$lib/components/ui/breadcrumb/index.js';
import type {SeasonRequest} from '$lib/types';
import type {SeasonRequest} from '$lib/types';
import RequestsTable from '$lib/components/season-requests-table.svelte';
let requests: SeasonRequest[] = $state(page.data.requestsData);
@@ -12,22 +12,22 @@
<!-- TODO: ADD DIALOGUE TO MODIFY REQUEST -->
<header class="flex h-16 shrink-0 items-center gap-2">
<div class="flex items-center gap-2 px-4">
<Sidebar.Trigger class="-ml-1"/>
<Separator class="mr-2 h-4" orientation="vertical"/>
<Sidebar.Trigger class="-ml-1"/>
<Separator class="mr-2 h-4" orientation="vertical"/>
<Breadcrumb.Root>
<Breadcrumb.List>
<Breadcrumb.Item class="hidden md:block">
<Breadcrumb.Link href="/dashboard">MediaManager</Breadcrumb.Link>
</Breadcrumb.Item>
<Breadcrumb.Separator class="hidden md:block"/>
<Breadcrumb.Separator class="hidden md:block"/>
<Breadcrumb.Item>
<Breadcrumb.Link href="/dashboard">Home</Breadcrumb.Link>
</Breadcrumb.Item>
<Breadcrumb.Separator class="hidden md:block"/>
<Breadcrumb.Separator class="hidden md:block"/>
<Breadcrumb.Item>
<Breadcrumb.Link href="/dashboard/tv">Shows</Breadcrumb.Link>
</Breadcrumb.Item>
<Breadcrumb.Separator class="hidden md:block"/>
<Breadcrumb.Separator class="hidden md:block"/>
<Breadcrumb.Item>
<Breadcrumb.Page>TV Torrents</Breadcrumb.Page>
</Breadcrumb.Item>
@@ -40,5 +40,5 @@
<h1 class="scroll-m-20 text-center text-4xl font-extrabold tracking-tight lg:text-5xl">
Season Requests
</h1>
<RequestsTable bind:requests/>
<RequestsTable bind:requests/>
</div>

View File

@@ -1,36 +1,34 @@
<script lang="ts">
import LoginForm from '$lib/components/login-form.svelte';
import logo from '$lib/images/logo.svg';
import background from '$lib/images/pawel-czerwinski-NTYYL9Eb9y8-unsplash.jpg';
import {toOptimizedURL} from "sveltekit-image-optimize/components";
import {page} from '$app/state';
import LoginForm from '$lib/components/login-form.svelte';
import logo from '$lib/images/logo.svg';
import background from '$lib/images/pawel-czerwinski-NTYYL9Eb9y8-unsplash.jpg';
import {toOptimizedURL} from 'sveltekit-image-optimize/components';
import {page} from '$app/state';
let oauthProvider = page.data.oauthProvider;
let oauthProvider = page.data.oauthProvider;
</script>
<div class="grid min-h-svh lg:grid-cols-2">
<div class="flex flex-col gap-4 p-6 md:p-10">
<div class="flex justify-center gap-2 md:justify-start">
<a class="flex items-center gap-2 font-medium" href="##">
<div
class="text-primary-foreground flex size-16 items-center justify-center rounded-md"
>
<img alt="MediaManager Logo" class="size-12" src={logo}/>
</div>
<h1 class="scale-110">Media Manager</h1>
</a>
</div>
<div class="flex flex-1 items-center justify-center">
<div class="w-full max-w-xs">
<LoginForm oauthProvider={oauthProvider}/>
</div>
</div>
</div>
<div class="relative hidden lg:block">
<img
alt="background"
class="absolute inset-0 h-full w-full object-cover dark:brightness-[0.8] rounded-l-3xl "
src="{toOptimizedURL(background)}"
/>
</div>
</div>
<div class="flex flex-col gap-4 p-6 md:p-10">
<div class="flex justify-center gap-2 md:justify-start">
<a class="flex items-center gap-2 font-medium" href="##">
<div class="flex size-16 items-center justify-center rounded-md text-primary-foreground">
<img alt="MediaManager Logo" class="size-12" src={logo}/>
</div>
<h1 class="scale-110">Media Manager</h1>
</a>
</div>
<div class="flex flex-1 items-center justify-center">
<div class="w-full max-w-xs">
<LoginForm {oauthProvider}/>
</div>
</div>
</div>
<div class="relative hidden lg:block">
<img
alt="background"
class="absolute inset-0 h-full w-full rounded-l-3xl object-cover dark:brightness-[0.8]"
src={toOptimizedURL(background)}
/>
</div>
</div>

View File

@@ -3,15 +3,14 @@ import type {PageLoad} from './$types';
const apiUrl = env.PUBLIC_API_URL;
export const load: PageLoad = async ({fetch}) => {
const response = await fetch(apiUrl + '/auth/metadata', {
method: 'GET',
headers: {
'Content-Type': 'application/json'
},
credentials: 'include'
});
const response = await fetch(apiUrl + '/auth/metadata', {
method: 'GET',
headers: {
'Content-Type': 'application/json'
},
credentials: 'include'
});
return {oauthProvider: await response.json()};
};
return {oauthProvider: await response.json()};
};