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>