feat: add dashboard and settings pages and components

This commit is contained in:
Bart van der Braak 2024-01-31 01:24:57 +01:00
parent cb51a4507d
commit 39a36462bb
109 changed files with 3770 additions and 116 deletions

View file

@ -1,9 +1,10 @@
import { error, redirect } from '@sveltejs/kit';
import type { Actions } from './$types';
export const actions = {
default: async ({ request, locals }: { request: Request; locals: App.Locals }) => {
const body = Object.fromEntries(await request.formData());
try {
const email = body.email.toString();
const password = body.password.toString();
@ -20,5 +21,15 @@ export const actions = {
}
throw redirect(303, '/');
}
};
},
// TODO: Implement MS Auth
// msauth: async ({ request, cookies }) => {
// const form = await request.formData();
// const token = form.get('token');
// if (!token || typeof token !== 'string') {
// throw redirect(303, '/login');
// }
// cookies.set('pb_auth', JSON.stringify({ token: token }), { path: '/' });
// throw redirect(303, '/');
// }
} satisfies Actions;

View file

@ -1,9 +1,10 @@
<script lang="ts">
import { enhance } from '$app/forms';
import { Icons } from '$lib/components/site/icons';
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import { Label } from '$lib/components/ui/label';
import * as Alert from "$lib/components/ui/alert";
import * as Alert from '$lib/components/ui/alert';
import { cn } from '$lib/utils';
export let form;
@ -19,7 +20,7 @@
</p>
</div>
<div class={cn('grid gap-6')} {...$$restProps}>
<form method="POST">
<form method="POST" use:enhance={() => { isLoading = true; }}>
<div class="grid gap-2">
<div class="grid gap-1">
<Label class="sr-only" for="email">Email</Label>
@ -36,7 +37,13 @@
</div>
<div class="grid gap-1">
<Label class="sr-only" for="password">Password</Label>
<Input id="password" name="password" type="password" disabled={isLoading} placeholder="Password" />
<Input
id="password"
name="password"
type="password"
disabled={isLoading}
placeholder="Password"
/>
</div>
<Button type="submit" disabled={isLoading}>
{#if isLoading}
@ -48,9 +55,7 @@
{#if form?.notVerified}
<Alert.Root>
<Alert.Title></Alert.Title>
<Alert.Description>
You must verify your email before you can login.
</Alert.Description>
<Alert.Description>You must verify your email before you can login.</Alert.Description>
</Alert.Root>
{/if}
</form>
@ -62,15 +67,17 @@
<span class="bg-background text-muted-foreground px-2"> Or continue with </span>
</div>
</div>
<Button variant="outline" type="button" disabled={isLoading}>
{#if isLoading}
<Icons.spinner class="mr-2 h-4 w-4 animate-spin" />
{:else}
<Icons.microsoft class="mr-2 h-4 w-4" />
{/if}
{' '}
Microsoft
</Button>
<form action="/?msauth" method="POST">
<Button type="submit" variant="outline" disabled={true} class="w-full">
{#if isLoading}
<Icons.spinner class="mr-2 h-4 w-4 animate-spin" />
{:else}
<Icons.microsoft class="mr-2 h-4 w-4" />
{/if}
{' '}
Microsoft
</Button>
</form>
</div>
<p class="text-muted-foreground px-8 text-center text-sm">
Don't have an account? <a class="text-primary underline" href="/register">Sign up.</a> <br />

View file

@ -1,4 +1,5 @@
<script lang="ts">
import { enhance } from '$app/forms';
import { Icons } from '$lib/components/site/icons';
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
@ -17,7 +18,7 @@
</p>
</div>
<div class={cn('grid gap-6')} {...$$restProps}>
<form method="POST">
<form method="POST" use:enhance={() => { isLoading = true; }}>
<div class="grid gap-2">
<div class="grid gap-1">
<Label class="sr-only" for="email">Name</Label>
@ -50,7 +51,7 @@
<Label class="sr-only" for="password">Confirm Password</Label>
<Input id="password" name="passwordConfirm" type="password" disabled={isLoading} placeholder="Confirm password" />
</div>
<Button type="submit" disabled={isLoading}>
<Button type="submit" disabled={isLoading} on:click={() => isLoading = true}>
{#if isLoading}
<Icons.spinner class="mr-2 h-4 w-4 animate-spin" />
{/if}
@ -66,7 +67,7 @@
<span class="bg-background text-muted-foreground px-2"> Or continue with </span>
</div>
</div>
<Button variant="outline" type="button" disabled={isLoading}>
<Button variant="outline" type="button" disabled={true}>
{#if isLoading}
<Icons.spinner class="mr-2 h-4 w-4 animate-spin" />
{:else}

View file

@ -1,4 +1,5 @@
<script lang="ts">
import { enhance } from '$app/forms';
import { Icons } from '$lib/components/site/icons';
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
@ -20,7 +21,11 @@
</p>
</div>
<div class={cn('grid gap-6')} {...$$restProps}>
<form method="POST">
<form method="POST" use:enhance={() => { isLoading = true;
return async ({ update }) => {
isLoading = false;
update();
}; }}>
<div class="grid gap-2">
<div class="grid gap-1">
<Label class="sr-only" for="email">Email</Label>

View file

@ -0,0 +1,12 @@
<script lang="ts">
import { Checkbox } from "$lib/components/ui/checkbox";
import type { HTMLButtonAttributes } from "svelte/elements";
import type { Writable } from "svelte/store";
type $$Props = HTMLButtonAttributes & {
checked: Writable<boolean>;
};
export let checked: Writable<boolean>;
</script>
<Checkbox bind:checked={$checked} {...$$restProps} />

View file

@ -0,0 +1,62 @@
<script lang="ts">
import { ArrowDown, ArrowUp, CaretSort } from "radix-icons-svelte";
import { cn } from "$lib/utils";
import { Button } from "$lib/components/ui/button";
import * as DropdownMenu from "$lib/components/ui/dropdown-menu";
let className: string | undefined | null = undefined;
export { className as class };
export let props: {
select: never;
sort: {
order: "desc" | "asc" | undefined;
toggle: (event: Event) => void;
clear: () => void;
disabled: boolean;
};
filter: never;
};
function handleAscSort(e: Event) {
if (props.sort.order === "asc") {
return;
}
props.sort.toggle(e);
}
function handleDescSort(e: Event) {
if (props.sort.order === "desc") {
return;
}
props.sort.toggle(e);
}
</script>
{#if !props.sort.disabled}
<div class={cn("flex items-center", className)}>
<DropdownMenu.Root>
<DropdownMenu.Trigger asChild let:builder>
<Button
variant="ghost"
builders={[builder]}
class="-ml-3 h-8 data-[state=open]:bg-accent"
>
<slot />
{#if props.sort.order === "desc"}
<ArrowDown class="ml-2 h-4 w-4" />
{:else if props.sort.order === "asc"}
<ArrowUp class="ml-2 h-4 w-4" />
{:else}
<CaretSort class="ml-2 h-4 w-4" />
{/if}
</Button>
</DropdownMenu.Trigger>
<DropdownMenu.Content align="start">
<DropdownMenu.Item on:click={handleAscSort}>Asc</DropdownMenu.Item>
<DropdownMenu.Item on:click={handleDescSort}>Desc</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Root>
</div>
{:else}
<slot />
{/if}

View file

@ -0,0 +1,96 @@
<script lang="ts">
import { PlusCircled, Check } from "radix-icons-svelte";
import * as Command from "$lib/components/ui/command";
import * as Popover from "$lib/components/ui/popover";
import { Button } from "$lib/components/ui/button";
import { cn } from "$lib/utils";
import Separator from "$lib/components/ui/separator/separator.svelte";
import Badge from "$lib/components/ui/badge/badge.svelte";
import type { statuses } from "../(data)/data";
export let filterValues: string[] = [];
export let title: string;
export let options = [] as typeof statuses;
let open = false;
const handleSelect = (currentValue: string) => {
if (Array.isArray(filterValues) && filterValues.includes(currentValue)) {
filterValues = filterValues.filter((v) => v !== currentValue);
} else {
filterValues = [...(Array.isArray(filterValues) ? filterValues : []), currentValue];
}
};
</script>
<Popover.Root bind:open>
<Popover.Trigger asChild let:builder>
<Button builders={[builder]} variant="outline" size="sm" class="h-8 border-dashed">
<PlusCircled class="mr-2 h-4 w-4" />
{title}
{#if filterValues.length > 0}
<Separator orientation="vertical" class="mx-2 h-4" />
<Badge variant="secondary" class="rounded-sm px-1 font-normal lg:hidden">
{filterValues.length}
</Badge>
<div class="hidden space-x-1 lg:flex">
{#if filterValues.length > 2}
<Badge variant="secondary" class="rounded-sm px-1 font-normal">
{filterValues.length} Selected
</Badge>
{:else}
{#each filterValues as option}
<Badge variant="secondary" class="rounded-sm px-1 font-normal">
{option}
</Badge>
{/each}
{/if}
</div>
{/if}
</Button>
</Popover.Trigger>
<Popover.Content class="w-[200px] p-0" align="start" side="bottom">
<Command.Root>
<Command.Input placeholder={title} />
<Command.List>
<Command.Empty>No results found.</Command.Empty>
<Command.Group>
{#each options as option}
<Command.Item
value={option.value}
onSelect={(currentValue) => {
handleSelect(currentValue);
}}
>
<div
class={cn(
"mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary",
filterValues.includes(option.value)
? "bg-primary text-primary-foreground"
: "opacity-50 [&_svg]:invisible"
)}
>
<Check className={cn("h-4 w-4")} />
</div>
<span>
{option.label}
</span>
</Command.Item>
{/each}
</Command.Group>
{#if filterValues.length > 0}
<Command.Separator />
<Command.Item
class="justify-center text-center"
onSelect={() => {
filterValues = [];
}}
>
Clear filters
</Command.Item>
{/if}
</Command.List>
</Command.Root>
</Popover.Content>
</Popover.Root>

View file

@ -0,0 +1,89 @@
<script lang="ts">
import { Button } from "$lib/components/ui/button";
import {
ChevronRight,
ChevronLeft,
DoubleArrowRight,
DoubleArrowLeft
} from "radix-icons-svelte";
import * as Select from "$lib/components/ui/select";
import type { Task } from "../(data)/schemas";
import type { AnyPlugins } from "svelte-headless-table/plugins";
import type { TableViewModel } from "svelte-headless-table";
export let tableModel: TableViewModel<Task, AnyPlugins>;
const { pageRows, pluginStates, rows } = tableModel;
const { hasNextPage, hasPreviousPage, pageIndex, pageCount, pageSize } = pluginStates.page;
const { selectedDataIds } = pluginStates.select;
</script>
<div class="flex items-center justify-between px-2">
<div class="flex-1 text-sm text-muted-foreground">
{Object.keys($selectedDataIds).length} of{" "}
{$rows.length} row(s) selected.
</div>
<div class="flex items-center space-x-6 lg:space-x-8">
<div class="flex items-center space-x-2">
<p class="text-sm font-medium">Rows per page</p>
<Select.Root
onSelectedChange={(selected) => pageSize.set(Number(selected?.value))}
selected={{ value: 10, label: "10" }}
>
<Select.Trigger class="w-[180px]">
<Select.Value placeholder="Select page size" />
</Select.Trigger>
<Select.Content>
<Select.Item value="10">10</Select.Item>
<Select.Item value="20">20</Select.Item>
<Select.Item value="30">30</Select.Item>
<Select.Item value="40">40</Select.Item>
<Select.Item value="50">50</Select.Item>
</Select.Content>
</Select.Root>
</div>
<div class="flex w-[100px] items-center justify-center text-sm font-medium">
Page {$pageIndex + 1} of {$pageCount}
</div>
<div class="flex items-center space-x-2">
<Button
variant="outline"
class="hidden h-8 w-8 p-0 lg:flex"
on:click={() => ($pageIndex = 0)}
disabled={!$hasPreviousPage}
>
<span class="sr-only">Go to first page</span>
<DoubleArrowLeft size={15} />
</Button>
<Button
variant="outline"
class="p-0 w-8 h-8"
on:click={() => ($pageIndex = $pageIndex - 1)}
disabled={!$hasPreviousPage}
>
<span class="sr-only">Go to previous page</span>
<ChevronLeft size={15} />
</Button>
<Button
variant="outline"
class="p-0 w-8 h-8"
disabled={!$hasNextPage}
on:click={() => ($pageIndex = $pageIndex + 1)}
>
<span class="sr-only">Go to next page</span>
<ChevronRight size={15} />
</Button>
<Button
variant="outline"
class="hidden h-8 w-8 p-0 lg:flex"
disabled={!$hasNextPage}
on:click={() => ($pageIndex = Math.ceil($rows.length / $pageRows.length) - 1)}
>
<span class="sr-only">Go to last page</span>
<DoubleArrowRight size={15} />
</Button>
</div>
</div>
</div>

View file

@ -0,0 +1,15 @@
<script lang="ts">
import { priorities } from "../(data)/data";
export let value: string;
const priority = priorities.find((priority) => priority.value === value);
const Icon = priority?.icon;
</script>
{#if priority}
<div class="flex items-center">
{#if Icon}
<Icon class="mr-2 h-4 w-4 text-muted-foreground" />
{/if}
<span>{priority.label}</span>
</div>
{/if}

View file

@ -0,0 +1,46 @@
<script lang="ts">
import { DotsHorizontal } from "radix-icons-svelte";
import { Button } from "$lib/components/ui/button";
import * as DropdownMenu from "$lib/components/ui/dropdown-menu";
import { labels } from "../(data)/data";
import { taskSchema, type Task } from "../(data)/schemas";
export let row: Task;
const task = taskSchema.parse(row);
</script>
<DropdownMenu.Root>
<DropdownMenu.Trigger asChild let:builder>
<Button
variant="ghost"
builders={[builder]}
class="flex h-8 w-8 p-0 data-[state=open]:bg-muted"
>
<DotsHorizontal class="h-4 w-4" />
<span class="sr-only">Open menu</span>
</Button>
</DropdownMenu.Trigger>
<DropdownMenu.Content class="w-[160px]" align="end">
<DropdownMenu.Item>Edit</DropdownMenu.Item>
<DropdownMenu.Item>Make a copy</DropdownMenu.Item>
<DropdownMenu.Item>Favorite</DropdownMenu.Item>
<DropdownMenu.Separator />
<DropdownMenu.Sub>
<DropdownMenu.SubTrigger>Labels</DropdownMenu.SubTrigger>
<DropdownMenu.SubContent>
<DropdownMenu.RadioGroup value={task.label}>
{#each labels as label}
<DropdownMenu.RadioItem value={label.value}>
{label.label}
</DropdownMenu.RadioItem>
{/each}
</DropdownMenu.RadioGroup>
</DropdownMenu.SubContent>
</DropdownMenu.Sub>
<DropdownMenu.Separator />
<DropdownMenu.Item>
Delete
<DropdownMenu.Shortcut>⌘⌫</DropdownMenu.Shortcut>
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Root>

View file

@ -0,0 +1,16 @@
<script lang="ts">
import { statuses } from "../(data)/data";
export let value: string;
const status = statuses.find((status) => status.value === value);
const Icon = status?.icon;
</script>
{#if status}
<div class="flex w-[100px] items-center">
{#if Icon}
<Icon class="mr-2 h-4 w-4 text-muted-foreground" />
{/if}
<span>{status.label}</span>
</div>
{/if}

View file

@ -0,0 +1,17 @@
<script lang="ts">
import { Badge } from "$lib/components/ui/badge";
import { labels } from "../(data)/data";
export let value: string;
export let labelValue: string;
const label = labels.find((label) => label.value === labelValue);
</script>
<div class="flex space-x-2">
{#if label}
<Badge variant="outline">{label.label}</Badge>
{/if}
<span class="max-w-[500px] truncate font-medium">
{value}
</span>
</div>

View file

@ -0,0 +1,69 @@
<script lang="ts">
import { Input } from "$lib/components/ui/input";
import { DataTableFacetedFilter, DataTableViewOptions } from ".";
import type { AnyPlugins } from "svelte-headless-table/lib/types/TablePlugin";
import type { Task } from "../(data)/schemas";
import type { TableViewModel } from "svelte-headless-table/lib/createViewModel";
import Button from "$lib/components/ui/button/button.svelte";
import { Cross2 } from "radix-icons-svelte";
import { statuses, priorities } from "../(data)/data";
import type { Writable } from "svelte/store";
export let tableModel: TableViewModel<Task, AnyPlugins>;
const { pluginStates } = tableModel;
const {
filterValue
}: {
filterValue: Writable<string>;
} = pluginStates.filter;
const {
filterValues
}: {
filterValues: Writable<{
status: string[];
priority: string[];
}>;
} = pluginStates.colFilter;
$: showReset = Object.values({ ...$filterValues, $filterValue }).some((v) => v.length > 0);
</script>
<div class="flex items-center justify-between">
<div class="flex flex-1 items-center space-x-2">
<Input
placeholder="Filter tasks..."
class="h-8 w-[150px] lg:w-[250px]"
type="search"
bind:value={$filterValue}
/>
<DataTableFacetedFilter
bind:filterValues={$filterValues.status}
title="Status"
options={statuses}
/>
<DataTableFacetedFilter
bind:filterValues={$filterValues.priority}
title="Priority"
options={priorities}
/>
{#if showReset}
<Button
on:click={() => {
$filterValue = "";
$filterValues.status = [];
$filterValues.priority = [];
}}
variant="ghost"
class="h-8 px-2 lg:px-3"
>
Reset
<Cross2 class="ml-2 h-4 w-4" />
</Button>
{/if}
</div>
<DataTableViewOptions {tableModel} />
</div>

View file

@ -0,0 +1,42 @@
<script lang="ts">
import { MixerHorizontal } from "radix-icons-svelte";
import { Button } from "$lib/components/ui/button";
import * as DropdownMenu from "$lib/components/ui/dropdown-menu";
import type { AnyPlugins } from "svelte-headless-table/lib/types/TablePlugin";
import type { Task } from "../(data)/schemas";
import type { TableViewModel } from "svelte-headless-table/lib/createViewModel";
export let tableModel: TableViewModel<Task, AnyPlugins>;
const { pluginStates, flatColumns } = tableModel;
const { hiddenColumnIds } = pluginStates.hide;
const ids = flatColumns.map((col: { id: string }) => col.id);
let hideForId = Object.fromEntries(ids.map((id: string) => [id, true]));
$: $hiddenColumnIds = Object.entries(hideForId)
.filter(([, hide]) => !hide)
.map(([id]) => id);
const hidableCols = ["title", "status", "priority"];
</script>
<DropdownMenu.Root>
<DropdownMenu.Trigger asChild let:builder>
<Button variant="outline" size="sm" class="ml-auto hidden h-8 lg:flex" builders={[builder]}>
<MixerHorizontal class="mr-2 h-4 w-4" />
View
</Button>
</DropdownMenu.Trigger>
<DropdownMenu.Content>
<DropdownMenu.Label>Toggle columns</DropdownMenu.Label>
<DropdownMenu.Separator />
{#each flatColumns as col}
{#if hidableCols.includes(col.id)}
<DropdownMenu.CheckboxItem bind:checked={hideForId[col.id]}>
{col.header}
</DropdownMenu.CheckboxItem>
{/if}
{/each}
</DropdownMenu.Content>
</DropdownMenu.Root>

View file

@ -0,0 +1,222 @@
<script lang="ts">
import { get, readable } from "svelte/store";
import { Render, Subscribe, createRender, createTable } from "svelte-headless-table";
import * as Table from "$lib/components/ui/table";
import {
addColumnFilters,
addHiddenColumns,
addPagination,
addSelectedRows,
addSortBy,
addTableFilter
} from "svelte-headless-table/plugins";
import {
DataTableCheckbox,
DataTableTitleCell,
DataTableStatusCell,
DataTableRowActions,
DataTablePriorityCell,
DataTableColumnHeader,
DataTableToolbar,
DataTablePagination
} from ".";
import type { Task } from "../(data)/schemas";
export let data: Task[];
const table = createTable(readable(data), {
select: addSelectedRows(),
sort: addSortBy({
toggleOrder: ["asc", "desc"]
}),
page: addPagination(),
filter: addTableFilter({
fn: ({ filterValue, value }) => {
return value.toLowerCase().includes(filterValue.toLowerCase());
}
}),
colFilter: addColumnFilters(),
hide: addHiddenColumns()
});
const columns = table.createColumns([
table.display({
id: "select",
header: (_, { pluginStates }) => {
const { allPageRowsSelected } = pluginStates.select;
return createRender(DataTableCheckbox, {
checked: allPageRowsSelected,
"aria-label": "Select all"
});
},
cell: ({ row }, { pluginStates }) => {
const { getRowState } = pluginStates.select;
const { isSelected } = getRowState(row);
return createRender(DataTableCheckbox, {
checked: isSelected,
"aria-label": "Select row",
class: "translate-y-[2px]"
});
},
plugins: {
sort: {
disable: true
}
}
}),
table.column({
accessor: "id",
header: () => {
return "Task";
},
id: "task",
plugins: {
sort: {
disable: true
}
}
}),
table.column({
accessor: "title",
header: "Title",
id: "title",
cell: ({ value, row }) => {
if (row.isData()) {
return createRender(DataTableTitleCell, {
value,
labelValue: row.original.label
});
}
return value;
}
}),
table.column({
accessor: "status",
header: "Status",
id: "status",
cell: ({ value }) => {
return createRender(DataTableStatusCell, {
value
});
},
plugins: {
colFilter: {
fn: ({ filterValue, value }) => {
if (filterValue.length === 0) return true;
if (!Array.isArray(filterValue) || typeof value !== "string") return true;
return filterValue.some((filter) => {
return value.includes(filter);
});
},
initialFilterValue: [],
render: ({ filterValue }) => {
return get(filterValue);
}
}
}
}),
table.column({
accessor: "priority",
id: "priority",
header: "Priority",
cell: ({ value }) => {
return createRender(DataTablePriorityCell, {
value
});
},
plugins: {
colFilter: {
fn: ({ filterValue, value }) => {
if (filterValue.length === 0) return true;
if (!Array.isArray(filterValue) || typeof value !== "string") return true;
return filterValue.some((filter) => {
return value.includes(filter);
});
},
initialFilterValue: [],
render: ({ filterValue }) => {
return get(filterValue);
}
}
}
}),
table.display({
id: "actions",
header: () => {
return "";
},
cell: ({ row }) => {
if (row.isData() && row.original) {
return createRender(DataTableRowActions, {
row: row.original
});
}
return "";
}
})
]);
const tableModel = table.createViewModel(columns);
const { headerRows, pageRows, tableAttrs, tableBodyAttrs } = tableModel;
</script>
<div class="space-y-4">
<DataTableToolbar {tableModel} />
<div class="rounded-md border">
<Table.Root {...$tableAttrs}>
<Table.Header>
{#each $headerRows as headerRow}
<Subscribe rowAttrs={headerRow.attrs()}>
<Table.Row>
{#each headerRow.cells as cell (cell.id)}
<Subscribe
attrs={cell.attrs()}
let:attrs
props={cell.props()}
let:props
>
<Table.Head {...attrs}>
{#if cell.id !== "select" && cell.id !== "actions"}
<DataTableColumnHeader {props}
><Render
of={cell.render()}
/></DataTableColumnHeader
>
{:else}
<Render of={cell.render()} />
{/if}
</Table.Head>
</Subscribe>
{/each}
</Table.Row>
</Subscribe>
{/each}
</Table.Header>
<Table.Body {...$tableBodyAttrs}>
{#each $pageRows as row (row.id)}
<Subscribe rowAttrs={row.attrs()} let:rowAttrs>
<Table.Row {...rowAttrs}>
{#each row.cells as cell (cell.id)}
<Subscribe attrs={cell.attrs()} let:attrs>
<Table.Cell {...attrs}>
{#if cell.id === "task"}
<div class="w-[80px]">
<Render of={cell.render()} />
</div>
{:else}
<Render of={cell.render()} />
{/if}
</Table.Cell>
</Subscribe>
{/each}
</Table.Row>
</Subscribe>
{/each}
</Table.Body>
</Table.Root>
</div>
<DataTablePagination {tableModel} />
</div>

View file

@ -0,0 +1,10 @@
export { default as DataTableCheckbox } from "./data-table-checkbox.svelte";
export { default as DataTableTitleCell } from "./data-table-title-cell.svelte";
export { default as DataTableStatusCell } from "./data-table-status-cell.svelte";
export { default as DataTableRowActions } from "./data-table-row-actions.svelte";
export { default as DataTablePriorityCell } from "./data-table-priority-cell.svelte";
export { default as DataTableColumnHeader } from "./data-table-column-header.svelte";
export { default as DataTableToolbar } from "./data-table-toolbar.svelte";
export { default as DataTablePagination } from "./data-table-pagination.svelte";
export { default as DataTableViewOptions } from "./data-table-view-options.svelte";
export { default as DataTableFacetedFilter } from "./data-table-faceted-filter.svelte";

View file

@ -0,0 +1,45 @@
<script lang="ts">
import * as Avatar from "$lib/components/ui/avatar";
import { Button } from "$lib/components/ui/button";
import * as DropdownMenu from "$lib/components/ui/dropdown-menu";
</script>
<DropdownMenu.Root>
<DropdownMenu.Trigger asChild let:builder>
<Button variant="ghost" builders={[builder]} class="relative h-8 w-8 rounded-full">
<Avatar.Root class="h-9 w-9">
<Avatar.Image src="/avatars/03.png" alt="@shadcn" />
<Avatar.Fallback>SC</Avatar.Fallback>
</Avatar.Root>
</Button>
</DropdownMenu.Trigger>
<DropdownMenu.Content class="w-56" align="end">
<DropdownMenu.Label class="font-normal">
<div class="flex flex-col space-y-1">
<p class="text-sm font-medium leading-none">shadcn</p>
<p class="text-xs leading-none text-muted-foreground">m@example.com</p>
</div>
</DropdownMenu.Label>
<DropdownMenu.Separator />
<DropdownMenu.Group>
<DropdownMenu.Item>
Profile
<DropdownMenu.Shortcut>⇧⌘P</DropdownMenu.Shortcut>
</DropdownMenu.Item>
<DropdownMenu.Item>
Billing
<DropdownMenu.Shortcut>⌘B</DropdownMenu.Shortcut>
</DropdownMenu.Item>
<DropdownMenu.Item>
Settings
<DropdownMenu.Shortcut>⌘S</DropdownMenu.Shortcut>
</DropdownMenu.Item>
<DropdownMenu.Item>New Team</DropdownMenu.Item>
</DropdownMenu.Group>
<DropdownMenu.Separator />
<DropdownMenu.Item>
Log out
<DropdownMenu.Shortcut>⇧⌘Q</DropdownMenu.Shortcut>
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Root>

View file

@ -0,0 +1,71 @@
import {
ArrowDown,
ArrowRight,
ArrowUp,
CheckCircled,
Circle,
CrossCircled,
QuestionMarkCircled,
Stopwatch
} from "radix-icons-svelte";
export const labels = [
{
value: "bug",
label: "Bug"
},
{
value: "feature",
label: "Feature"
},
{
value: "documentation",
label: "Documentation"
}
];
export const statuses = [
{
value: "backlog",
label: "Backlog",
icon: QuestionMarkCircled
},
{
value: "todo",
label: "Todo",
icon: Circle
},
{
value: "in progress",
label: "In Progress",
icon: Stopwatch
},
{
value: "done",
label: "Done",
icon: CheckCircled
},
{
value: "canceled",
label: "Canceled",
icon: CrossCircled
}
];
export const priorities = [
{
label: "Low",
value: "low",
icon: ArrowDown
},
{
label: "Medium",
value: "medium",
icon: ArrowRight
},
{
label: "High",
value: "high",
icon: ArrowUp
}
];

View file

@ -0,0 +1,13 @@
import { z } from "zod";
// We're keeping a simple non-relational schema here.
// IRL, you will have a schema for your data models.
export const taskSchema = z.object({
id: z.string(),
title: z.string(),
status: z.string(),
label: z.string(),
priority: z.string()
});
export type Task = z.infer<typeof taskSchema>;

View file

@ -0,0 +1,702 @@
[
{
"id": "TASK-8782",
"title": "You can't compress the program without quantifying the open-source SSD pixel!",
"status": "in progress",
"label": "documentation",
"priority": "medium"
},
{
"id": "TASK-7878",
"title": "Try to calculate the EXE feed, maybe it will index the multi-byte pixel!",
"status": "backlog",
"label": "documentation",
"priority": "medium"
},
{
"id": "TASK-7839",
"title": "We need to bypass the neural TCP card!",
"status": "todo",
"label": "bug",
"priority": "high"
},
{
"id": "TASK-5562",
"title": "The SAS interface is down, bypass the open-source pixel so we can back up the PNG bandwidth!",
"status": "backlog",
"label": "feature",
"priority": "medium"
},
{
"id": "TASK-8686",
"title": "I'll parse the wireless SSL protocol, that should driver the API panel!",
"status": "canceled",
"label": "feature",
"priority": "medium"
},
{
"id": "TASK-1280",
"title": "Use the digital TLS panel, then you can transmit the haptic system!",
"status": "done",
"label": "bug",
"priority": "high"
},
{
"id": "TASK-7262",
"title": "The UTF8 application is down, parse the neural bandwidth so we can back up the PNG firewall!",
"status": "done",
"label": "feature",
"priority": "high"
},
{
"id": "TASK-1138",
"title": "Generating the driver won't do anything, we need to quantify the 1080p SMTP bandwidth!",
"status": "in progress",
"label": "feature",
"priority": "medium"
},
{
"id": "TASK-7184",
"title": "We need to program the back-end THX pixel!",
"status": "todo",
"label": "feature",
"priority": "low"
},
{
"id": "TASK-5160",
"title": "Calculating the bus won't do anything, we need to navigate the back-end JSON protocol!",
"status": "in progress",
"label": "documentation",
"priority": "high"
},
{
"id": "TASK-5618",
"title": "Generating the driver won't do anything, we need to index the online SSL application!",
"status": "done",
"label": "documentation",
"priority": "medium"
},
{
"id": "TASK-6699",
"title": "I'll transmit the wireless JBOD capacitor, that should hard drive the SSD feed!",
"status": "backlog",
"label": "documentation",
"priority": "medium"
},
{
"id": "TASK-2858",
"title": "We need to override the online UDP bus!",
"status": "backlog",
"label": "bug",
"priority": "medium"
},
{
"id": "TASK-9864",
"title": "I'll reboot the 1080p FTP panel, that should matrix the HEX hard drive!",
"status": "done",
"label": "bug",
"priority": "high"
},
{
"id": "TASK-8404",
"title": "We need to generate the virtual HEX alarm!",
"status": "in progress",
"label": "bug",
"priority": "low"
},
{
"id": "TASK-5365",
"title": "Backing up the pixel won't do anything, we need to transmit the primary IB array!",
"status": "in progress",
"label": "documentation",
"priority": "low"
},
{
"id": "TASK-1780",
"title": "The CSS feed is down, index the bluetooth transmitter so we can compress the CLI protocol!",
"status": "todo",
"label": "documentation",
"priority": "high"
},
{
"id": "TASK-6938",
"title": "Use the redundant SCSI application, then you can hack the optical alarm!",
"status": "todo",
"label": "documentation",
"priority": "high"
},
{
"id": "TASK-9885",
"title": "We need to compress the auxiliary VGA driver!",
"status": "backlog",
"label": "bug",
"priority": "high"
},
{
"id": "TASK-3216",
"title": "Transmitting the transmitter won't do anything, we need to compress the virtual HDD sensor!",
"status": "backlog",
"label": "documentation",
"priority": "medium"
},
{
"id": "TASK-9285",
"title": "The IP monitor is down, copy the haptic alarm so we can generate the HTTP transmitter!",
"status": "todo",
"label": "bug",
"priority": "high"
},
{
"id": "TASK-1024",
"title": "Overriding the microchip won't do anything, we need to transmit the digital OCR transmitter!",
"status": "in progress",
"label": "documentation",
"priority": "low"
},
{
"id": "TASK-7068",
"title": "You can't generate the capacitor without indexing the wireless HEX pixel!",
"status": "canceled",
"label": "bug",
"priority": "low"
},
{
"id": "TASK-6502",
"title": "Navigating the microchip won't do anything, we need to bypass the back-end SQL bus!",
"status": "todo",
"label": "bug",
"priority": "high"
},
{
"id": "TASK-5326",
"title": "We need to hack the redundant UTF8 transmitter!",
"status": "todo",
"label": "bug",
"priority": "low"
},
{
"id": "TASK-6274",
"title": "Use the virtual PCI circuit, then you can parse the bluetooth alarm!",
"status": "canceled",
"label": "documentation",
"priority": "low"
},
{
"id": "TASK-1571",
"title": "I'll input the neural DRAM circuit, that should protocol the SMTP interface!",
"status": "in progress",
"label": "feature",
"priority": "medium"
},
{
"id": "TASK-9518",
"title": "Compressing the interface won't do anything, we need to compress the online SDD matrix!",
"status": "canceled",
"label": "documentation",
"priority": "medium"
},
{
"id": "TASK-5581",
"title": "I'll synthesize the digital COM pixel, that should transmitter the UTF8 protocol!",
"status": "backlog",
"label": "documentation",
"priority": "high"
},
{
"id": "TASK-2197",
"title": "Parsing the feed won't do anything, we need to copy the bluetooth DRAM bus!",
"status": "todo",
"label": "documentation",
"priority": "low"
},
{
"id": "TASK-8484",
"title": "We need to parse the solid state UDP firewall!",
"status": "in progress",
"label": "bug",
"priority": "low"
},
{
"id": "TASK-9892",
"title": "If we back up the application, we can get to the UDP application through the multi-byte THX capacitor!",
"status": "done",
"label": "documentation",
"priority": "high"
},
{
"id": "TASK-9616",
"title": "We need to synthesize the cross-platform ASCII pixel!",
"status": "in progress",
"label": "feature",
"priority": "medium"
},
{
"id": "TASK-9744",
"title": "Use the back-end IP card, then you can input the solid state hard drive!",
"status": "done",
"label": "documentation",
"priority": "low"
},
{
"id": "TASK-1376",
"title": "Generating the alarm won't do anything, we need to generate the mobile IP capacitor!",
"status": "backlog",
"label": "documentation",
"priority": "low"
},
{
"id": "TASK-7382",
"title": "If we back up the firewall, we can get to the RAM alarm through the primary UTF8 pixel!",
"status": "todo",
"label": "feature",
"priority": "low"
},
{
"id": "TASK-2290",
"title": "I'll compress the virtual JSON panel, that should application the UTF8 bus!",
"status": "canceled",
"label": "documentation",
"priority": "high"
},
{
"id": "TASK-1533",
"title": "You can't input the firewall without overriding the wireless TCP firewall!",
"status": "done",
"label": "bug",
"priority": "high"
},
{
"id": "TASK-4920",
"title": "Bypassing the hard drive won't do anything, we need to input the bluetooth JSON program!",
"status": "in progress",
"label": "bug",
"priority": "high"
},
{
"id": "TASK-5168",
"title": "If we synthesize the bus, we can get to the IP panel through the virtual TLS array!",
"status": "in progress",
"label": "feature",
"priority": "low"
},
{
"id": "TASK-7103",
"title": "We need to parse the multi-byte EXE bandwidth!",
"status": "canceled",
"label": "feature",
"priority": "low"
},
{
"id": "TASK-4314",
"title": "If we compress the program, we can get to the XML alarm through the multi-byte COM matrix!",
"status": "in progress",
"label": "bug",
"priority": "high"
},
{
"id": "TASK-3415",
"title": "Use the cross-platform XML application, then you can quantify the solid state feed!",
"status": "todo",
"label": "feature",
"priority": "high"
},
{
"id": "TASK-8339",
"title": "Try to calculate the DNS interface, maybe it will input the bluetooth capacitor!",
"status": "in progress",
"label": "feature",
"priority": "low"
},
{
"id": "TASK-6995",
"title": "Try to hack the XSS bandwidth, maybe it will override the bluetooth matrix!",
"status": "todo",
"label": "feature",
"priority": "high"
},
{
"id": "TASK-8053",
"title": "If we connect the program, we can get to the UTF8 matrix through the digital UDP protocol!",
"status": "todo",
"label": "feature",
"priority": "medium"
},
{
"id": "TASK-4336",
"title": "If we synthesize the microchip, we can get to the SAS sensor through the optical UDP program!",
"status": "todo",
"label": "documentation",
"priority": "low"
},
{
"id": "TASK-8790",
"title": "I'll back up the optical COM alarm, that should alarm the RSS capacitor!",
"status": "done",
"label": "bug",
"priority": "medium"
},
{
"id": "TASK-8980",
"title": "Try to navigate the SQL transmitter, maybe it will back up the virtual firewall!",
"status": "canceled",
"label": "bug",
"priority": "low"
},
{
"id": "TASK-7342",
"title": "Use the neural CLI card, then you can parse the online port!",
"status": "backlog",
"label": "documentation",
"priority": "low"
},
{
"id": "TASK-5608",
"title": "I'll hack the haptic SSL program, that should bus the UDP transmitter!",
"status": "canceled",
"label": "documentation",
"priority": "low"
},
{
"id": "TASK-1606",
"title": "I'll generate the bluetooth PNG firewall, that should pixel the SSL driver!",
"status": "done",
"label": "feature",
"priority": "medium"
},
{
"id": "TASK-7872",
"title": "Transmitting the circuit won't do anything, we need to reboot the 1080p RSS monitor!",
"status": "canceled",
"label": "feature",
"priority": "medium"
},
{
"id": "TASK-4167",
"title": "Use the cross-platform SMS circuit, then you can synthesize the optical feed!",
"status": "canceled",
"label": "bug",
"priority": "medium"
},
{
"id": "TASK-9581",
"title": "You can't index the port without hacking the cross-platform XSS monitor!",
"status": "backlog",
"label": "documentation",
"priority": "low"
},
{
"id": "TASK-8806",
"title": "We need to bypass the back-end SSL panel!",
"status": "done",
"label": "bug",
"priority": "medium"
},
{
"id": "TASK-6542",
"title": "Try to quantify the RSS firewall, maybe it will quantify the open-source system!",
"status": "done",
"label": "feature",
"priority": "low"
},
{
"id": "TASK-6806",
"title": "The VGA protocol is down, reboot the back-end matrix so we can parse the CSS panel!",
"status": "canceled",
"label": "documentation",
"priority": "low"
},
{
"id": "TASK-9549",
"title": "You can't bypass the bus without connecting the neural JBOD bus!",
"status": "todo",
"label": "feature",
"priority": "high"
},
{
"id": "TASK-1075",
"title": "Backing up the driver won't do anything, we need to parse the redundant RAM pixel!",
"status": "done",
"label": "feature",
"priority": "medium"
},
{
"id": "TASK-1427",
"title": "Use the auxiliary PCI circuit, then you can calculate the cross-platform interface!",
"status": "done",
"label": "documentation",
"priority": "high"
},
{
"id": "TASK-1907",
"title": "Hacking the circuit won't do anything, we need to back up the online DRAM system!",
"status": "todo",
"label": "documentation",
"priority": "high"
},
{
"id": "TASK-4309",
"title": "If we generate the system, we can get to the TCP sensor through the optical GB pixel!",
"status": "backlog",
"label": "bug",
"priority": "medium"
},
{
"id": "TASK-3973",
"title": "I'll parse the back-end ADP array, that should bandwidth the RSS bandwidth!",
"status": "todo",
"label": "feature",
"priority": "medium"
},
{
"id": "TASK-7962",
"title": "Use the wireless RAM program, then you can hack the cross-platform feed!",
"status": "canceled",
"label": "bug",
"priority": "low"
},
{
"id": "TASK-3360",
"title": "You can't quantify the program without synthesizing the neural OCR interface!",
"status": "done",
"label": "feature",
"priority": "medium"
},
{
"id": "TASK-9887",
"title": "Use the auxiliary ASCII sensor, then you can connect the solid state port!",
"status": "backlog",
"label": "bug",
"priority": "medium"
},
{
"id": "TASK-3649",
"title": "I'll input the virtual USB system, that should circuit the DNS monitor!",
"status": "in progress",
"label": "feature",
"priority": "medium"
},
{
"id": "TASK-3586",
"title": "If we quantify the circuit, we can get to the CLI feed through the mobile SMS hard drive!",
"status": "in progress",
"label": "bug",
"priority": "low"
},
{
"id": "TASK-5150",
"title": "I'll hack the wireless XSS port, that should transmitter the IP interface!",
"status": "canceled",
"label": "feature",
"priority": "medium"
},
{
"id": "TASK-3652",
"title": "The SQL interface is down, override the optical bus so we can program the ASCII interface!",
"status": "backlog",
"label": "feature",
"priority": "low"
},
{
"id": "TASK-6884",
"title": "Use the digital PCI circuit, then you can synthesize the multi-byte microchip!",
"status": "canceled",
"label": "feature",
"priority": "high"
},
{
"id": "TASK-1591",
"title": "We need to connect the mobile XSS driver!",
"status": "in progress",
"label": "feature",
"priority": "high"
},
{
"id": "TASK-3802",
"title": "Try to override the ASCII protocol, maybe it will parse the virtual matrix!",
"status": "in progress",
"label": "feature",
"priority": "low"
},
{
"id": "TASK-7253",
"title": "Programming the capacitor won't do anything, we need to bypass the neural IB hard drive!",
"status": "backlog",
"label": "bug",
"priority": "high"
},
{
"id": "TASK-9739",
"title": "We need to hack the multi-byte HDD bus!",
"status": "done",
"label": "documentation",
"priority": "medium"
},
{
"id": "TASK-4424",
"title": "Try to hack the HEX alarm, maybe it will connect the optical pixel!",
"status": "in progress",
"label": "documentation",
"priority": "medium"
},
{
"id": "TASK-3922",
"title": "You can't back up the capacitor without generating the wireless PCI program!",
"status": "backlog",
"label": "bug",
"priority": "low"
},
{
"id": "TASK-4921",
"title": "I'll index the open-source IP feed, that should system the GB application!",
"status": "canceled",
"label": "bug",
"priority": "low"
},
{
"id": "TASK-5814",
"title": "We need to calculate the 1080p AGP feed!",
"status": "backlog",
"label": "bug",
"priority": "high"
},
{
"id": "TASK-2645",
"title": "Synthesizing the system won't do anything, we need to navigate the multi-byte HDD firewall!",
"status": "todo",
"label": "documentation",
"priority": "medium"
},
{
"id": "TASK-4535",
"title": "Try to copy the JSON circuit, maybe it will connect the wireless feed!",
"status": "in progress",
"label": "feature",
"priority": "low"
},
{
"id": "TASK-4463",
"title": "We need to copy the solid state AGP monitor!",
"status": "done",
"label": "documentation",
"priority": "low"
},
{
"id": "TASK-9745",
"title": "If we connect the protocol, we can get to the GB system through the bluetooth PCI microchip!",
"status": "canceled",
"label": "feature",
"priority": "high"
},
{
"id": "TASK-2080",
"title": "If we input the bus, we can get to the RAM matrix through the auxiliary RAM card!",
"status": "todo",
"label": "bug",
"priority": "medium"
},
{
"id": "TASK-3838",
"title": "I'll bypass the online TCP application, that should panel the AGP system!",
"status": "backlog",
"label": "bug",
"priority": "high"
},
{
"id": "TASK-1340",
"title": "We need to navigate the virtual PNG circuit!",
"status": "todo",
"label": "bug",
"priority": "medium"
},
{
"id": "TASK-6665",
"title": "If we parse the monitor, we can get to the SSD hard drive through the cross-platform AGP alarm!",
"status": "canceled",
"label": "feature",
"priority": "low"
},
{
"id": "TASK-7585",
"title": "If we calculate the hard drive, we can get to the SSL program through the multi-byte CSS microchip!",
"status": "backlog",
"label": "feature",
"priority": "low"
},
{
"id": "TASK-6319",
"title": "We need to copy the multi-byte SCSI program!",
"status": "backlog",
"label": "bug",
"priority": "high"
},
{
"id": "TASK-4369",
"title": "Try to input the SCSI bus, maybe it will generate the 1080p pixel!",
"status": "backlog",
"label": "bug",
"priority": "high"
},
{
"id": "TASK-9035",
"title": "We need to override the solid state PNG array!",
"status": "canceled",
"label": "documentation",
"priority": "low"
},
{
"id": "TASK-3970",
"title": "You can't index the transmitter without quantifying the haptic ASCII card!",
"status": "todo",
"label": "documentation",
"priority": "medium"
},
{
"id": "TASK-4473",
"title": "You can't bypass the protocol without overriding the neural RSS program!",
"status": "todo",
"label": "documentation",
"priority": "low"
},
{
"id": "TASK-4136",
"title": "You can't hack the hard drive without hacking the primary JSON program!",
"status": "canceled",
"label": "bug",
"priority": "medium"
},
{
"id": "TASK-3939",
"title": "Use the back-end SQL firewall, then you can connect the neural hard drive!",
"status": "done",
"label": "feature",
"priority": "low"
},
{
"id": "TASK-2007",
"title": "I'll input the back-end USB protocol, that should bandwidth the PCI system!",
"status": "backlog",
"label": "bug",
"priority": "high"
},
{
"id": "TASK-7516",
"title": "Use the primary SQL program, then you can generate the auxiliary transmitter!",
"status": "done",
"label": "documentation",
"priority": "medium"
},
{
"id": "TASK-6906",
"title": "Try to back up the DRAM system, maybe it will reboot the online transmitter!",
"status": "done",
"label": "feature",
"priority": "high"
},
{
"id": "TASK-5207",
"title": "The SMS interface is down, copy the bluetooth bus so we can quantify the VGA card!",
"status": "in progress",
"label": "bug",
"priority": "low"
}
]

View file

@ -1,8 +1,18 @@
<script lang="ts">
import DataTable from "./(components)/data-table.svelte";
import UserNav from "./(components)/user-nav.svelte";
import data from "./(data)/tasks.json";
</script>
<h1>Create a new dashboard</h1>
<form method="POST">
<button type="submit">Create dashboard</button>
</form>
<div class="hidden h-full flex-1 flex-col space-y-8 p-8 md:flex">
<div class="flex items-center justify-between space-y-2">
<div>
<h2 class="text-2xl font-bold tracking-tight">Welcome back!</h2>
<p class="text-muted-foreground">Here's a list of your tasks for this month!</p>
</div>
<div class="flex items-center space-x-2">
<UserNav />
</div>
</div>
<DataTable {data} />
</div>

View file

@ -0,0 +1,43 @@
<script lang="ts">
import { cn } from "$lib/utils";
import { page } from "$app/stores";
import { Button } from "$lib/components/ui/button";
import { cubicInOut } from "svelte/easing";
import { crossfade } from "svelte/transition";
let className: string | undefined | null = undefined;
export let items: { href: string; title: string }[];
export { className as class };
const [send, receive] = crossfade({
duration: 250,
easing: cubicInOut
});
</script>
<nav class={cn("flex space-x-2 lg:flex-col lg:space-x-0 lg:space-y-1", className)}>
{#each items as item}
{@const isActive = $page.url.pathname === item.href}
<Button
href={item.href}
variant="ghost"
class={cn(
!isActive && "hover:underline",
"relative justify-start hover:bg-transparent"
)}
data-sveltekit-noscroll
>
{#if isActive}
<div
class="absolute inset-0 rounded-md bg-muted"
in:send={{ key: "active-sidebar-tab" }}
out:receive={{ key: "active-sidebar-tab" }}
/>
{/if}
<div class="relative">
{item.title}
</div>
</Button>
{/each}
</nav>

View file

@ -0,0 +1,41 @@
<script lang="ts">
import { Separator } from "$lib/components/ui/separator";
import SidebarNav from "./(components)/sidebar-nav.svelte";
const sidebarNavItems = [
{
title: "Profile",
href: "/settings"
},
{
title: "Account",
href: "/settings/account"
},
{
title: "Appearance",
href: "/settings/appearance"
},
{
title: "Notifications",
href: "/settings/notifications"
}
];
</script>
<div class="hidden space-y-6 p-10 pb-16 md:block">
<div class="space-y-0.5">
<h2 class="text-2xl font-bold tracking-tight">Settings</h2>
<p class="text-muted-foreground">
Manage your account settings and set e-mail preferences.
</p>
</div>
<Separator class="my-6" />
<div class="flex flex-col space-y-8 lg:flex-row lg:space-x-12 lg:space-y-0">
<aside class="-mx-4 lg:w-1/5">
<SidebarNav items={sidebarNavItems} />
</aside>
<div class="flex-1 lg:max-w-2xl">
<slot />
</div>
</div>
</div>

View file

@ -0,0 +1,24 @@
import type { PageServerLoad } from "./$types";
import { superValidate } from "sveltekit-superforms/server";
import { profileFormSchema } from "./profile-form.svelte";
import { fail, type Actions } from "@sveltejs/kit";
export const load: PageServerLoad = async () => {
return {
form: await superValidate(profileFormSchema)
};
};
export const actions: Actions = {
default: async (event) => {
const form = await superValidate(event, profileFormSchema);
if (!form.valid) {
return fail(400, {
form
});
}
return {
form
};
}
};

View file

@ -0,0 +1,15 @@
<script lang="ts">
import type { PageData } from "./$types";
import ProfileForm from "./profile-form.svelte";
import { Separator } from "$lib/components/ui/separator";
export let data: PageData;
</script>
<div class="space-y-6">
<div>
<h3 class="text-lg font-medium">Profile</h3>
<p class="text-sm text-muted-foreground">This is how others will see you on the site.</p>
</div>
<Separator />
<ProfileForm data={data.form} />
</div>

View file

@ -0,0 +1,24 @@
import { superValidate } from "sveltekit-superforms/server";
import type { PageServerLoad } from "./$types";
import { accountFormSchema } from "./account-form.svelte";
import { fail, type Actions } from "@sveltejs/kit";
export const load: PageServerLoad = async () => {
return {
form: await superValidate(accountFormSchema)
};
};
export const actions: Actions = {
default: async (event) => {
const form = await superValidate(event, accountFormSchema);
if (!form.valid) {
return fail(400, {
form
});
}
return {
form
};
}
};

View file

@ -0,0 +1,18 @@
<script lang="ts">
import { Separator } from "$lib/components/ui/separator";
import AccountForm from "./account-form.svelte";
import type { PageData } from "./$types";
export let data: PageData;
</script>
<div class="space-y-6">
<div>
<h3 class="text-lg font-medium">Account</h3>
<p class="text-sm text-muted-foreground">
Update your account settings. Set your preferred language and timezone.
</p>
</div>
<Separator />
<AccountForm data={data.form} />
</div>

View file

@ -0,0 +1,85 @@
<script lang="ts" context="module">
import { z } from "zod";
const languages = {
en: "English",
fr: "French",
de: "German",
es: "Spanish",
pt: "Portuguese",
ru: "Russian",
ja: "Japanese",
ko: "Korean",
zh: "Chinese"
} as const;
type Language = keyof typeof languages;
export const accountFormSchema = z.object({
name: z
.string({
required_error: "Required."
})
.min(2, "Name must be at least 2 characters.")
.max(30, "Name must not be longer than 30 characters"),
// Hack: https://github.com/colinhacks/zod/issues/2280
language: z.enum(Object.keys(languages) as [Language, ...Language[]])
});
export type AccountFormSchema = typeof accountFormSchema;
</script>
<script lang="ts">
import * as Form from "$lib/components/ui/form";
import type { SuperValidated } from "sveltekit-superforms";
import { cn } from "$lib/utils";
export let data: SuperValidated<AccountFormSchema>;
</script>
<Form.Root
method="POST"
class="space-y-8"
let:config
schema={accountFormSchema}
form={data}
debug={true}
>
<Form.Item>
<Form.Field name="name" {config}>
<Form.Label>Name</Form.Label>
<Form.Input placeholder="Your name" />
<Form.Description>
This is the name that will be displayed on your profile and in emails.
</Form.Description>
<Form.Validation />
</Form.Field>
</Form.Item>
<Form.Item>
<Form.Field {config} name="language" let:attrs>
{@const { value } = attrs.input}
<Form.Label>Language</Form.Label>
<Form.Select selected={{ value, label: languages[value] }}>
<Form.SelectTrigger
placeholder="Select language"
class={cn(
"w-[200px] justify-between",
!attrs.input.value && "text-muted-foreground"
)}
/>
<Form.SelectContent class="h-52 overflow-y-auto">
{#each Object.entries(languages) as [value, lang]}
<Form.SelectItem {value}>
{lang}
</Form.SelectItem>
{/each}
</Form.SelectContent>
</Form.Select>
<Form.Description>
This is the language that will be used in the dashboard.
</Form.Description>
<Form.Validation />
</Form.Field>
</Form.Item>
<Form.Button>Update account</Form.Button>
</Form.Root>

View file

@ -0,0 +1,24 @@
import { superValidate } from "sveltekit-superforms/server";
import type { PageServerLoad } from "../$types";
import { appearanceFormSchema } from "./appearance-form.svelte";
import { fail, type Actions } from "@sveltejs/kit";
export const load: PageServerLoad = async () => {
return {
form: await superValidate(appearanceFormSchema)
};
};
export const actions: Actions = {
default: async (event) => {
const form = await superValidate(event, appearanceFormSchema);
if (!form.valid) {
return fail(400, {
form
});
}
return {
form
};
}
};

View file

@ -0,0 +1,18 @@
<script lang="ts">
import { Separator } from "$lib/components/ui/separator";
import type { PageData } from "./$types";
import AppearanceForm from "./appearance-form.svelte";
export let data: PageData;
</script>
<div class="space-y-6">
<div>
<h3 class="text-lg font-medium">Appearance</h3>
<p class="text-sm text-muted-foreground">
Customize the appearance of the app. Automatically switch between day and night themes.
</p>
</div>
<Separator />
<AppearanceForm data={data.form} />
</div>

View file

@ -0,0 +1,108 @@
<script lang="ts" context="module">
import type { SuperValidated } from "sveltekit-superforms";
import { z } from "zod";
export const appearanceFormSchema = z.object({
theme: z.enum(["light", "dark"], {
required_error: "Please select a theme."
}),
font: z.enum(["inter", "manrope", "system"], {
invalid_type_error: "Select a font",
required_error: "Please select a font."
})
});
export type AppearanceFormSchema = typeof appearanceFormSchema;
</script>
<script lang="ts">
import * as Form from "$lib/components/ui/form";
import Label from "$lib/components/ui/label/label.svelte";
export let data: SuperValidated<AppearanceFormSchema>;
</script>
<Form.Root
schema={appearanceFormSchema}
form={data}
class="space-y-8"
method="POST"
let:config
debug={true}
>
<Form.Item>
<Form.Field {config} name="font">
<Form.Label>Font</Form.Label>
<div class="w-max">
<Form.NativeSelect class="w-[200px]">
<option value="inter">Inter</option>
<option value="manrope">Manrope</option>
<option value="system">System</option>
</Form.NativeSelect>
</div>
<Form.Description>Set the font you want to use in the dashboard.</Form.Description>
<Form.Validation />
</Form.Field>
</Form.Item>
<Form.Item>
<Form.Field {config} name="theme">
<Form.Label>Theme</Form.Label>
<Form.Description>Select the theme for the dashboard.</Form.Description>
<Form.Validation />
<Form.RadioGroup class="grid max-w-md grid-cols-2 gap-8 pt-2" orientation="horizontal">
<Label for="light" class="[&:has([data-state=checked])>div]:border-primary">
<Form.RadioItem id="light" value="light" class="sr-only" />
<div
class="items-center rounded-md border-2 border-muted p-1 hover:border-accent"
>
<div class="space-y-2 rounded-sm bg-[#ecedef] p-2">
<div class="space-y-2 rounded-md bg-white p-2 shadow-sm">
<div class="h-2 w-[80px] rounded-lg bg-[#ecedef]" />
<div class="h-2 w-[100px] rounded-lg bg-[#ecedef]" />
</div>
<div
class="flex items-center space-x-2 rounded-md bg-white p-2 shadow-sm"
>
<div class="h-4 w-4 rounded-full bg-[#ecedef]" />
<div class="h-2 w-[100px] rounded-lg bg-[#ecedef]" />
</div>
<div
class="flex items-center space-x-2 rounded-md bg-white p-2 shadow-sm"
>
<div class="h-4 w-4 rounded-full bg-[#ecedef]" />
<div class="h-2 w-[100px] rounded-lg bg-[#ecedef]" />
</div>
</div>
</div>
<span class="block w-full p-2 text-center font-normal"> Light </span>
</Label>
<Label for="dark" class="[&:has([data-state=checked])>div]:border-primary">
<Form.RadioItem id="dark" value="dark" class="sr-only" />
<div
class="items-center rounded-md border-2 border-muted bg-popover p-1 hover:bg-accent hover:text-accent-foreground"
>
<div class="space-y-2 rounded-sm bg-slate-950 p-2">
<div class="space-y-2 rounded-md bg-slate-800 p-2 shadow-sm">
<div class="h-2 w-[80px] rounded-lg bg-slate-400" />
<div class="h-2 w-[100px] rounded-lg bg-slate-400" />
</div>
<div
class="flex items-center space-x-2 rounded-md bg-slate-800 p-2 shadow-sm"
>
<div class="h-4 w-4 rounded-full bg-slate-400" />
<div class="h-2 w-[100px] rounded-lg bg-slate-400" />
</div>
<div
class="flex items-center space-x-2 rounded-md bg-slate-800 p-2 shadow-sm"
>
<div class="h-4 w-4 rounded-full bg-slate-400" />
<div class="h-2 w-[100px] rounded-lg bg-slate-400" />
</div>
</div>
</div>
<span class="block w-full p-2 text-center font-normal"> Dark </span>
</Label>
</Form.RadioGroup>
</Form.Field>
</Form.Item>
<Form.Button>Update preferences</Form.Button>
</Form.Root>

View file

@ -0,0 +1,24 @@
import { superValidate } from "sveltekit-superforms/server";
import type { PageServerLoad } from "../$types";
import { notificationsFormSchema } from "./notifications-form.svelte";
import { fail, type Actions } from "@sveltejs/kit";
export const load: PageServerLoad = async () => {
return {
form: await superValidate(notificationsFormSchema)
};
};
export const actions: Actions = {
default: async (event) => {
const form = await superValidate(event, notificationsFormSchema);
if (!form.valid) {
return fail(400, {
form
});
}
return {
form
};
}
};

View file

@ -0,0 +1,16 @@
<script lang="ts">
import { Separator } from "$lib/components/ui/separator";
import NotificationsForm from "./notifications-form.svelte";
import type { PageData } from "./$types";
export let data: PageData;
</script>
<div class="space-y-6">
<div>
<h3 class="text-lg font-medium">Notifications</h3>
<p class="text-sm text-muted-foreground">Configure how you receive notifications.</p>
</div>
<Separator />
<NotificationsForm data={data.form} />
</div>

View file

@ -0,0 +1,113 @@
<script lang="ts" context="module">
import { z } from "zod";
export const notificationsFormSchema = z.object({
type: z.enum(["all", "mentions", "none"], {
required_error: "You need to select a notification type."
}),
mobile: z.boolean().default(false).optional(),
communication_emails: z.boolean().default(false).optional(),
social_emails: z.boolean().default(false).optional(),
marketing_emails: z.boolean().default(false).optional(),
security_emails: z.boolean()
});
type NotificationFormSchema = typeof notificationsFormSchema;
</script>
<script lang="ts">
import type { SuperValidated } from "sveltekit-superforms";
import * as Form from "$lib/components/ui/form";
import { Label } from "$lib/components/ui/label";
export let data: SuperValidated<NotificationFormSchema>;
</script>
<Form.Root
form={data}
schema={notificationsFormSchema}
let:config
method="POST"
class="space-y-8"
debug={true}
>
<Form.Item>
<Form.Field {config} name="type">
<Form.Label>Notify me about...</Form.Label>
<Form.RadioGroup class="flex flex-col space-y-1">
<div class="flex items-center space-x-3">
<Form.RadioItem value="all" id="all" />
<Label for="all" class="font-normal">All new messages</Label>
</div>
<div class="flex items-center space-x-3">
<Form.RadioItem value="mentions" id="mentions" />
<Label for="mentions" class="font-normal">Direct messages and mentions</Label>
</div>
<div class="flex items-center space-x-3">
<Form.RadioItem value="none" id="none" />
<Label for="none" class="font-normal">Nothing</Label>
</div>
</Form.RadioGroup>
</Form.Field>
</Form.Item>
<div>
<h3 class="mb-4 text-lg font-medium">Email Notifications</h3>
<div class="space-y-4">
<Form.Field {config} name="communication_emails">
<Form.Item class="flex flex-row items-center justify-between rounded-lg border p-4">
<div class="space-y-0.5">
<Form.Label class="text-base">Communication emails</Form.Label>
<Form.Description>
Receive emails about your account activity.
</Form.Description>
</div>
<Form.Switch />
</Form.Item>
</Form.Field>
<Form.Field {config} name="marketing_emails">
<Form.Item class="flex flex-row items-center justify-between rounded-lg border p-4">
<div class="space-y-0.5">
<Form.Label class="text-base">Marketing emails</Form.Label>
<Form.Description>
Receive emails about new products, features, and more.
</Form.Description>
</div>
<Form.Switch />
</Form.Item>
</Form.Field>
<Form.Field {config} name="social_emails">
<Form.Item class="flex flex-row items-center justify-between rounded-lg border p-4">
<div class="space-y-0.5">
<Form.Label class="text-base">Social emails</Form.Label>
<Form.Description>
Receive emails for friend requests, follows, and more.
</Form.Description>
</div>
<Form.Switch />
</Form.Item>
</Form.Field>
<Form.Field {config} name="security_emails">
<Form.Item class="flex flex-row items-center justify-between rounded-lg border p-4">
<div class="space-y-0.5">
<Form.Label class="text-base">Security emails</Form.Label>
<Form.Description>
Receive emails about your account activity and security.
</Form.Description>
</div>
<Form.Switch />
</Form.Item>
</Form.Field>
</div>
</div>
<Form.Field {config} name="mobile">
<Form.Item class="flex flex-row items-start space-x-3 space-y-0">
<Form.Checkbox />
<div class="space-y-1 leading-none">
<Form.Label>Use different settings for my mobile devices</Form.Label>
<Form.Description>
You can manage your mobile notifications in the{" "}<a href="/settings"
>mobile settings</a
> page.
</Form.Description>
</div>
</Form.Item>
</Form.Field>
<Form.Button>Update notifications</Form.Button>
</Form.Root>

View file

@ -0,0 +1,88 @@
<script lang="ts" context="module">
import { z } from "zod";
export const profileFormSchema = z.object({
username: z
.string()
.min(2, "Username must be at least 2 characters.")
.max(30, "Username must not be longer than 30 characters"),
email: z.string({ required_error: "Please select an email to display" }).email(),
bio: z.string().min(4).max(160).default("I own a computer."),
website: z
.string()
.url({ message: "Please enter a valid URL." })
.default("https://shadcn-svelte.com")
});
export type ProfileFormSchema = typeof profileFormSchema;
</script>
<script lang="ts">
import * as Form from "$lib/components/ui/form";
import type { SuperValidated } from "sveltekit-superforms";
export let data: SuperValidated<ProfileFormSchema>;
</script>
<Form.Root
form={data}
schema={profileFormSchema}
let:config
method="POST"
class="space-y-8"
debug={true}
>
<Form.Item>
<Form.Field {config} name="username">
<Form.Label>Username</Form.Label>
<Form.Input placeholder="@shadcn" />
<Form.Description>
This is your public display name. It can be your real name or a pseudonym. You can
only change this once every 30 days.
</Form.Description>
<Form.Validation />
</Form.Field>
</Form.Item>
<Form.Item>
<Form.Field {config} name="email">
<Form.Label>Email</Form.Label>
<Form.Select>
<Form.SelectTrigger placeholder="Select a verified email to display" />
<Form.SelectContent>
<Form.SelectItem value="m@example.com" label="m@example.com"
>m@example.com
</Form.SelectItem>
<Form.SelectItem value="m@google.com" label="m@google.com"
>m@google.com
</Form.SelectItem>
<Form.SelectItem value="m@support.com" label="m@support.com"
>m@support.com
</Form.SelectItem>
</Form.SelectContent>
</Form.Select>
<Form.Description>
You can manage verified email addresses in your <a href="/examples/forms"
>email settings</a
>.
</Form.Description>
<Form.Validation />
</Form.Field>
</Form.Item>
<Form.Item>
<Form.Field {config} name="bio">
<Form.Label>Bio</Form.Label>
<Form.Textarea placeholder="Tell us a little bit about yourself" class="resize-none" />
<Form.Description>
You can <span>@mention</span> other users and organizations to link to them.
</Form.Description>
<Form.Validation />
</Form.Field>
</Form.Item>
<Form.Item>
<Form.Field {config} name="website">
<Form.Label>Website</Form.Label>
<Form.Input />
<Form.Description>Your personal website, blog, or portfolio.</Form.Description>
<Form.Validation />
</Form.Field>
</Form.Item>
<Form.Button>Update profile</Form.Button>
</Form.Root>

View file

@ -1,6 +1,6 @@
import type { PageServerLoad } from './$types';
import type { LayoutServerLoad } from './$types';
export const load: PageServerLoad = (async ({ locals }: { locals: App.Locals }) => {
export const load: LayoutServerLoad = (async ({ locals }: { locals: App.Locals }) => {
return {
authenticated: locals.pocketBase.authStore.isValid
};

View file

@ -15,7 +15,7 @@
<div class="relative flex min-h-screen flex-col" id="page">
<SiteNavBar authenticated={data.authenticated} />
<main class="container relative mb-4 mt-12 max-w-[980px] flex-1">
<main class="container relative mb-4 mt-12 flex-1">
<div in:fade={{ duration: 200, delay: 100 }} out:fade={{ duration: 100 }}>
<slot />
</div>

View file

@ -1,7 +0,0 @@
import type { PageServerLoad } from './$types';
export const load = (async ({ locals }) => {
return {
authenticated: locals.pocketBase.authStore.isValid
};
}) satisfies PageServerLoad;

View file

@ -1,17 +1,31 @@
<script lang="ts">
import type { PageData } from './$types';
export let data: PageData;
import { Icons } from '$lib/components/site';
import Button from '$lib/components/ui/button/button.svelte';
import Separator from '$lib/components/ui/separator/separator.svelte';
import { siteConfig } from '$lib/config/site';
</script>
<h1>Login with SvelteKit and Pocketbase</h1>
<div class="pb-16 pt-32 md:pb-32 md:pt-52">
<div class="container mx-auto text-center">
<h1
class="bg-gradient-to-r from-zinc-200/60 via-zinc-200 to-zinc-200/60 bg-clip-text pb-4 text-6xl font-extrabold tracking-tight text-transparent lg:text-7xl"
>
<span class="inline-block align-top decoration-inherit text-balance">One Dashboard, Countless Solutions</span
>
</h1>
<p class="mb-8 text-lg text-zinc-300">
Tame ticket overload and keep your operations teams sane
</p>
<div
class="mx-auto flex max-w-xs flex-col items-center gap-4 sm:inline-flex sm:max-w-none sm:flex-row sm:justify-center"
>
{#if data.authenticated}
<a href="/board/">Create a new game</a>
{:else}
<div class="links">
<a href="/login">Log in</a>
<p>or</p>
<a href="/register">Register</a>
<Button href="/dashboard" class="group flex w-full items-center transition duration-150 ease-in-out">
Get Started <Icons.arrowRight class="text-primary-500 ml-1 h-3 w-3 tracking-normal transition-transform duration-150 ease-in-out group-hover:translate-x-0.5"/>
</Button>
<Button href={siteConfig.links.gitHubProject} variant="outline">
Star on GitHub
</Button>
</div>
</div>
{/if}
</div>