mirror of
https://github.com/bartvdbraak/omnidash.git
synced 2025-07-17 05:29:11 +00:00
feat: re-import of settings
This commit is contained in:
parent
6e12f454b3
commit
28721b4ba5
46 changed files with 913 additions and 793 deletions
9
src/routes/(user)/+layout.server.ts
Normal file
9
src/routes/(user)/+layout.server.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
import { redirect } from '@sveltejs/kit';
|
||||
|
||||
export const load = async ({ locals }: { locals: App.Locals }) => {
|
||||
if (!locals.pocketBase.authStore.isValid) {
|
||||
throw redirect(303, '/auth');
|
||||
}
|
||||
|
||||
return {};
|
||||
};
|
|
@ -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} />
|
|
@ -0,0 +1,58 @@
|
|||
<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}
|
|
@ -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>
|
|
@ -0,0 +1,86 @@
|
|||
<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 { Ticket } from '../(data)/schemas';
|
||||
import type { AnyPlugins } from 'svelte-headless-table/plugins';
|
||||
import type { TableViewModel } from 'svelte-headless-table';
|
||||
import { defaultPageSize } from '.';
|
||||
|
||||
export let tableModel: TableViewModel<Ticket, 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: defaultPageSize, label: defaultPageSize.toString() }}
|
||||
>
|
||||
<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.Item value="100">100</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="h-8 w-8 p-0"
|
||||
on:click={() => ($pageIndex = $pageIndex - 1)}
|
||||
disabled={!$hasPreviousPage}
|
||||
>
|
||||
<span class="sr-only">Go to previous page</span>
|
||||
<ChevronLeft size={15} />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
class="h-8 w-8 p-0"
|
||||
on:click={() => ($pageIndex = $pageIndex + 1)}
|
||||
disabled={!$hasNextPage}
|
||||
>
|
||||
<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"
|
||||
on:click={() => ($pageIndex = Math.ceil($rows.length / $pageRows.length) - 1)}
|
||||
disabled={!$hasNextPage}
|
||||
>
|
||||
<span class="sr-only">Go to last page</span>
|
||||
<DoubleArrowRight size={15} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -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}
|
|
@ -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 { ticketSchema, type Ticket } from '../(data)/schemas';
|
||||
|
||||
export let row: Ticket;
|
||||
const ticket = ticketSchema.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={ticket.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>
|
|
@ -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}
|
|
@ -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>
|
|
@ -0,0 +1,69 @@
|
|||
<script lang="ts">
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { DataTableFacetedFilter, DataTableViewOptions } from '.';
|
||||
import type { Ticket } from '../(data)/schemas';
|
||||
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';
|
||||
import type { TableViewModel } from 'svelte-headless-table';
|
||||
import type { AnyPlugins } from 'svelte-headless-table/plugins';
|
||||
|
||||
export let tableModel: TableViewModel<Ticket, 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 tickets..."
|
||||
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>
|
|
@ -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 { Ticket } from '../(data)/schemas';
|
||||
import type { TableViewModel } from 'svelte-headless-table';
|
||||
import type { AnyPlugins } from 'svelte-headless-table/plugins';
|
||||
|
||||
export let tableModel: TableViewModel<Ticket, 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>
|
216
src/routes/(user)/dashboard/(components)/data-table.svelte
Normal file
216
src/routes/(user)/dashboard/(components)/data-table.svelte
Normal file
|
@ -0,0 +1,216 @@
|
|||
<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,
|
||||
defaultPageSize
|
||||
} from '.';
|
||||
|
||||
import type { Ticket } from '../(data)/schemas';
|
||||
|
||||
export let data: Ticket[];
|
||||
|
||||
const table = createTable(readable(data), {
|
||||
select: addSelectedRows(),
|
||||
sort: addSortBy({
|
||||
toggleOrder: ['asc', 'desc']
|
||||
}),
|
||||
page: addPagination({ initialPageSize: defaultPageSize }),
|
||||
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 'Ticket ID';
|
||||
},
|
||||
id: 'ticket',
|
||||
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 === 'ticket'}
|
||||
<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>
|
12
src/routes/(user)/dashboard/(components)/index.ts
Normal file
12
src/routes/(user)/dashboard/(components)/index.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
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';
|
||||
|
||||
export const defaultPageSize = 15;
|
71
src/routes/(user)/dashboard/(data)/data.ts
Normal file
71
src/routes/(user)/dashboard/(data)/data.ts
Normal file
|
@ -0,0 +1,71 @@
|
|||
import {
|
||||
ArrowDown,
|
||||
ArrowRight,
|
||||
ArrowUp,
|
||||
CheckCircled,
|
||||
Circle,
|
||||
CrossCircled,
|
||||
QuestionMarkCircled,
|
||||
Stopwatch
|
||||
} from 'radix-icons-svelte';
|
||||
|
||||
export const labels = [
|
||||
{
|
||||
value: 'bug',
|
||||
label: 'Incident'
|
||||
},
|
||||
{
|
||||
value: 'feature',
|
||||
label: 'Change'
|
||||
},
|
||||
{
|
||||
value: 'documentation',
|
||||
label: 'Information'
|
||||
}
|
||||
];
|
||||
|
||||
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
|
||||
}
|
||||
];
|
18
src/routes/(user)/dashboard/(data)/schemas.ts
Normal file
18
src/routes/(user)/dashboard/(data)/schemas.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
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 ticketSchema = z.object({
|
||||
id: z.string(),
|
||||
// solution: z.string().optional(),
|
||||
title: z.string(),
|
||||
status: z.string(),
|
||||
label: z.string(),
|
||||
// createdAt: z.date(),
|
||||
// updatedAt: z.date(),
|
||||
priority: z.string() // z.number().min(0).max(4),
|
||||
// source: z.number(),
|
||||
// assignee: z.string(),
|
||||
});
|
||||
|
||||
export type Ticket = z.infer<typeof ticketSchema>;
|
802
src/routes/(user)/dashboard/(data)/tickets.json
Normal file
802
src/routes/(user)/dashboard/(data)/tickets.json
Normal file
|
@ -0,0 +1,802 @@
|
|||
[
|
||||
{
|
||||
"id": "TASK-8782",
|
||||
"title": "You can't compress the program without quantifying the open-source SSD pixel!",
|
||||
"status": "in progress",
|
||||
"label": "documentation",
|
||||
"priority": "medium",
|
||||
"assignee": ""
|
||||
},
|
||||
{
|
||||
"id": "TASK-7878",
|
||||
"title": "Try to calculate the EXE feed, maybe it will index the multi-byte pixel!",
|
||||
"status": "backlog",
|
||||
"label": "documentation",
|
||||
"priority": "medium",
|
||||
"assignee": ""
|
||||
},
|
||||
{
|
||||
"id": "TASK-7839",
|
||||
"title": "We need to bypass the neural TCP card!",
|
||||
"status": "todo",
|
||||
"label": "bug",
|
||||
"priority": "high",
|
||||
"assignee": ""
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"assignee": ""
|
||||
},
|
||||
{
|
||||
"id": "TASK-8686",
|
||||
"title": "I'll parse the wireless SSL protocol, that should driver the API panel!",
|
||||
"status": "canceled",
|
||||
"label": "feature",
|
||||
"priority": "medium",
|
||||
"assignee": ""
|
||||
},
|
||||
{
|
||||
"id": "TASK-1280",
|
||||
"title": "Use the digital TLS panel, then you can transmit the haptic system!",
|
||||
"status": "done",
|
||||
"label": "bug",
|
||||
"priority": "high",
|
||||
"assignee": ""
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"assignee": ""
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"assignee": ""
|
||||
},
|
||||
{
|
||||
"id": "TASK-7184",
|
||||
"title": "We need to program the back-end THX pixel!",
|
||||
"status": "todo",
|
||||
"label": "feature",
|
||||
"priority": "low",
|
||||
"assignee": ""
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"assignee": ""
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"assignee": ""
|
||||
},
|
||||
{
|
||||
"id": "TASK-6699",
|
||||
"title": "I'll transmit the wireless JBOD capacitor, that should hard drive the SSD feed!",
|
||||
"status": "backlog",
|
||||
"label": "documentation",
|
||||
"priority": "medium",
|
||||
"assignee": ""
|
||||
},
|
||||
{
|
||||
"id": "TASK-2858",
|
||||
"title": "We need to override the online UDP bus!",
|
||||
"status": "backlog",
|
||||
"label": "bug",
|
||||
"priority": "medium",
|
||||
"assignee": ""
|
||||
},
|
||||
{
|
||||
"id": "TASK-9864",
|
||||
"title": "I'll reboot the 1080p FTP panel, that should matrix the HEX hard drive!",
|
||||
"status": "done",
|
||||
"label": "bug",
|
||||
"priority": "high",
|
||||
"assignee": ""
|
||||
},
|
||||
{
|
||||
"id": "TASK-8404",
|
||||
"title": "We need to generate the virtual HEX alarm!",
|
||||
"status": "in progress",
|
||||
"label": "bug",
|
||||
"priority": "low",
|
||||
"assignee": ""
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"assignee": ""
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"assignee": ""
|
||||
},
|
||||
{
|
||||
"id": "TASK-6938",
|
||||
"title": "Use the redundant SCSI application, then you can hack the optical alarm!",
|
||||
"status": "todo",
|
||||
"label": "documentation",
|
||||
"priority": "high",
|
||||
"assignee": ""
|
||||
},
|
||||
{
|
||||
"id": "TASK-9885",
|
||||
"title": "We need to compress the auxiliary VGA driver!",
|
||||
"status": "backlog",
|
||||
"label": "bug",
|
||||
"priority": "high",
|
||||
"assignee": ""
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"assignee": ""
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"assignee": ""
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"assignee": ""
|
||||
},
|
||||
{
|
||||
"id": "TASK-7068",
|
||||
"title": "You can't generate the capacitor without indexing the wireless HEX pixel!",
|
||||
"status": "canceled",
|
||||
"label": "bug",
|
||||
"priority": "low",
|
||||
"assignee": ""
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"assignee": ""
|
||||
},
|
||||
{
|
||||
"id": "TASK-5326",
|
||||
"title": "We need to hack the redundant UTF8 transmitter!",
|
||||
"status": "todo",
|
||||
"label": "bug",
|
||||
"priority": "low",
|
||||
"assignee": ""
|
||||
},
|
||||
{
|
||||
"id": "TASK-6274",
|
||||
"title": "Use the virtual PCI circuit, then you can parse the bluetooth alarm!",
|
||||
"status": "canceled",
|
||||
"label": "documentation",
|
||||
"priority": "low",
|
||||
"assignee": ""
|
||||
},
|
||||
{
|
||||
"id": "TASK-1571",
|
||||
"title": "I'll input the neural DRAM circuit, that should protocol the SMTP interface!",
|
||||
"status": "in progress",
|
||||
"label": "feature",
|
||||
"priority": "medium",
|
||||
"assignee": ""
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"assignee": ""
|
||||
},
|
||||
{
|
||||
"id": "TASK-5581",
|
||||
"title": "I'll synthesize the digital COM pixel, that should transmitter the UTF8 protocol!",
|
||||
"status": "backlog",
|
||||
"label": "documentation",
|
||||
"priority": "high",
|
||||
"assignee": ""
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"assignee": ""
|
||||
},
|
||||
{
|
||||
"id": "TASK-8484",
|
||||
"title": "We need to parse the solid state UDP firewall!",
|
||||
"status": "in progress",
|
||||
"label": "bug",
|
||||
"priority": "low",
|
||||
"assignee": ""
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"assignee": ""
|
||||
},
|
||||
{
|
||||
"id": "TASK-9616",
|
||||
"title": "We need to synthesize the cross-platform ASCII pixel!",
|
||||
"status": "in progress",
|
||||
"label": "feature",
|
||||
"priority": "medium",
|
||||
"assignee": ""
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"assignee": ""
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"assignee": ""
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"assignee": ""
|
||||
},
|
||||
{
|
||||
"id": "TASK-2290",
|
||||
"title": "I'll compress the virtual JSON panel, that should application the UTF8 bus!",
|
||||
"status": "canceled",
|
||||
"label": "documentation",
|
||||
"priority": "high",
|
||||
"assignee": ""
|
||||
},
|
||||
{
|
||||
"id": "TASK-1533",
|
||||
"title": "You can't input the firewall without overriding the wireless TCP firewall!",
|
||||
"status": "done",
|
||||
"label": "bug",
|
||||
"priority": "high",
|
||||
"assignee": ""
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"assignee": ""
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"assignee": ""
|
||||
},
|
||||
{
|
||||
"id": "TASK-7103",
|
||||
"title": "We need to parse the multi-byte EXE bandwidth!",
|
||||
"status": "canceled",
|
||||
"label": "feature",
|
||||
"priority": "low",
|
||||
"assignee": ""
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"assignee": ""
|
||||
},
|
||||
{
|
||||
"id": "TASK-3415",
|
||||
"title": "Use the cross-platform XML application, then you can quantify the solid state feed!",
|
||||
"status": "todo",
|
||||
"label": "feature",
|
||||
"priority": "high",
|
||||
"assignee": ""
|
||||
},
|
||||
{
|
||||
"id": "TASK-8339",
|
||||
"title": "Try to calculate the DNS interface, maybe it will input the bluetooth capacitor!",
|
||||
"status": "in progress",
|
||||
"label": "feature",
|
||||
"priority": "low",
|
||||
"assignee": ""
|
||||
},
|
||||
{
|
||||
"id": "TASK-6995",
|
||||
"title": "Try to hack the XSS bandwidth, maybe it will override the bluetooth matrix!",
|
||||
"status": "todo",
|
||||
"label": "feature",
|
||||
"priority": "high",
|
||||
"assignee": ""
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"assignee": ""
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"assignee": ""
|
||||
},
|
||||
{
|
||||
"id": "TASK-8790",
|
||||
"title": "I'll back up the optical COM alarm, that should alarm the RSS capacitor!",
|
||||
"status": "done",
|
||||
"label": "bug",
|
||||
"priority": "medium",
|
||||
"assignee": ""
|
||||
},
|
||||
{
|
||||
"id": "TASK-8980",
|
||||
"title": "Try to navigate the SQL transmitter, maybe it will back up the virtual firewall!",
|
||||
"status": "canceled",
|
||||
"label": "bug",
|
||||
"priority": "low",
|
||||
"assignee": ""
|
||||
},
|
||||
{
|
||||
"id": "TASK-7342",
|
||||
"title": "Use the neural CLI card, then you can parse the online port!",
|
||||
"status": "backlog",
|
||||
"label": "documentation",
|
||||
"priority": "low",
|
||||
"assignee": ""
|
||||
},
|
||||
{
|
||||
"id": "TASK-5608",
|
||||
"title": "I'll hack the haptic SSL program, that should bus the UDP transmitter!",
|
||||
"status": "canceled",
|
||||
"label": "documentation",
|
||||
"priority": "low",
|
||||
"assignee": ""
|
||||
},
|
||||
{
|
||||
"id": "TASK-1606",
|
||||
"title": "I'll generate the bluetooth PNG firewall, that should pixel the SSL driver!",
|
||||
"status": "done",
|
||||
"label": "feature",
|
||||
"priority": "medium",
|
||||
"assignee": ""
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"assignee": ""
|
||||
},
|
||||
{
|
||||
"id": "TASK-4167",
|
||||
"title": "Use the cross-platform SMS circuit, then you can synthesize the optical feed!",
|
||||
"status": "canceled",
|
||||
"label": "bug",
|
||||
"priority": "medium",
|
||||
"assignee": ""
|
||||
},
|
||||
{
|
||||
"id": "TASK-9581",
|
||||
"title": "You can't index the port without hacking the cross-platform XSS monitor!",
|
||||
"status": "backlog",
|
||||
"label": "documentation",
|
||||
"priority": "low",
|
||||
"assignee": ""
|
||||
},
|
||||
{
|
||||
"id": "TASK-8806",
|
||||
"title": "We need to bypass the back-end SSL panel!",
|
||||
"status": "done",
|
||||
"label": "bug",
|
||||
"priority": "medium",
|
||||
"assignee": ""
|
||||
},
|
||||
{
|
||||
"id": "TASK-6542",
|
||||
"title": "Try to quantify the RSS firewall, maybe it will quantify the open-source system!",
|
||||
"status": "done",
|
||||
"label": "feature",
|
||||
"priority": "low",
|
||||
"assignee": ""
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"assignee": ""
|
||||
},
|
||||
{
|
||||
"id": "TASK-9549",
|
||||
"title": "You can't bypass the bus without connecting the neural JBOD bus!",
|
||||
"status": "todo",
|
||||
"label": "feature",
|
||||
"priority": "high",
|
||||
"assignee": ""
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"assignee": ""
|
||||
},
|
||||
{
|
||||
"id": "TASK-1427",
|
||||
"title": "Use the auxiliary PCI circuit, then you can calculate the cross-platform interface!",
|
||||
"status": "done",
|
||||
"label": "documentation",
|
||||
"priority": "high",
|
||||
"assignee": ""
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"assignee": ""
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"assignee": ""
|
||||
},
|
||||
{
|
||||
"id": "TASK-3973",
|
||||
"title": "I'll parse the back-end ADP array, that should bandwidth the RSS bandwidth!",
|
||||
"status": "todo",
|
||||
"label": "feature",
|
||||
"priority": "medium",
|
||||
"assignee": ""
|
||||
},
|
||||
{
|
||||
"id": "TASK-7962",
|
||||
"title": "Use the wireless RAM program, then you can hack the cross-platform feed!",
|
||||
"status": "canceled",
|
||||
"label": "bug",
|
||||
"priority": "low",
|
||||
"assignee": ""
|
||||
},
|
||||
{
|
||||
"id": "TASK-3360",
|
||||
"title": "You can't quantify the program without synthesizing the neural OCR interface!",
|
||||
"status": "done",
|
||||
"label": "feature",
|
||||
"priority": "medium",
|
||||
"assignee": ""
|
||||
},
|
||||
{
|
||||
"id": "TASK-9887",
|
||||
"title": "Use the auxiliary ASCII sensor, then you can connect the solid state port!",
|
||||
"status": "backlog",
|
||||
"label": "bug",
|
||||
"priority": "medium",
|
||||
"assignee": ""
|
||||
},
|
||||
{
|
||||
"id": "TASK-3649",
|
||||
"title": "I'll input the virtual USB system, that should circuit the DNS monitor!",
|
||||
"status": "in progress",
|
||||
"label": "feature",
|
||||
"priority": "medium",
|
||||
"assignee": ""
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"assignee": ""
|
||||
},
|
||||
{
|
||||
"id": "TASK-5150",
|
||||
"title": "I'll hack the wireless XSS port, that should transmitter the IP interface!",
|
||||
"status": "canceled",
|
||||
"label": "feature",
|
||||
"priority": "medium",
|
||||
"assignee": ""
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"assignee": ""
|
||||
},
|
||||
{
|
||||
"id": "TASK-6884",
|
||||
"title": "Use the digital PCI circuit, then you can synthesize the multi-byte microchip!",
|
||||
"status": "canceled",
|
||||
"label": "feature",
|
||||
"priority": "high",
|
||||
"assignee": ""
|
||||
},
|
||||
{
|
||||
"id": "TASK-1591",
|
||||
"title": "We need to connect the mobile XSS driver!",
|
||||
"status": "in progress",
|
||||
"label": "feature",
|
||||
"priority": "high",
|
||||
"assignee": ""
|
||||
},
|
||||
{
|
||||
"id": "TASK-3802",
|
||||
"title": "Try to override the ASCII protocol, maybe it will parse the virtual matrix!",
|
||||
"status": "in progress",
|
||||
"label": "feature",
|
||||
"priority": "low",
|
||||
"assignee": ""
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"assignee": ""
|
||||
},
|
||||
{
|
||||
"id": "TASK-9739",
|
||||
"title": "We need to hack the multi-byte HDD bus!",
|
||||
"status": "done",
|
||||
"label": "documentation",
|
||||
"priority": "medium",
|
||||
"assignee": ""
|
||||
},
|
||||
{
|
||||
"id": "TASK-4424",
|
||||
"title": "Try to hack the HEX alarm, maybe it will connect the optical pixel!",
|
||||
"status": "in progress",
|
||||
"label": "documentation",
|
||||
"priority": "medium",
|
||||
"assignee": ""
|
||||
},
|
||||
{
|
||||
"id": "TASK-3922",
|
||||
"title": "You can't back up the capacitor without generating the wireless PCI program!",
|
||||
"status": "backlog",
|
||||
"label": "bug",
|
||||
"priority": "low",
|
||||
"assignee": ""
|
||||
},
|
||||
{
|
||||
"id": "TASK-4921",
|
||||
"title": "I'll index the open-source IP feed, that should system the GB application!",
|
||||
"status": "canceled",
|
||||
"label": "bug",
|
||||
"priority": "low",
|
||||
"assignee": ""
|
||||
},
|
||||
{
|
||||
"id": "TASK-5814",
|
||||
"title": "We need to calculate the 1080p AGP feed!",
|
||||
"status": "backlog",
|
||||
"label": "bug",
|
||||
"priority": "high",
|
||||
"assignee": ""
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"assignee": ""
|
||||
},
|
||||
{
|
||||
"id": "TASK-4535",
|
||||
"title": "Try to copy the JSON circuit, maybe it will connect the wireless feed!",
|
||||
"status": "in progress",
|
||||
"label": "feature",
|
||||
"priority": "low",
|
||||
"assignee": ""
|
||||
},
|
||||
{
|
||||
"id": "TASK-4463",
|
||||
"title": "We need to copy the solid state AGP monitor!",
|
||||
"status": "done",
|
||||
"label": "documentation",
|
||||
"priority": "low",
|
||||
"assignee": ""
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"assignee": ""
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"assignee": ""
|
||||
},
|
||||
{
|
||||
"id": "TASK-3838",
|
||||
"title": "I'll bypass the online TCP application, that should panel the AGP system!",
|
||||
"status": "backlog",
|
||||
"label": "bug",
|
||||
"priority": "high",
|
||||
"assignee": ""
|
||||
},
|
||||
{
|
||||
"id": "TASK-1340",
|
||||
"title": "We need to navigate the virtual PNG circuit!",
|
||||
"status": "todo",
|
||||
"label": "bug",
|
||||
"priority": "medium",
|
||||
"assignee": ""
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"assignee": ""
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"assignee": ""
|
||||
},
|
||||
{
|
||||
"id": "TASK-6319",
|
||||
"title": "We need to copy the multi-byte SCSI program!",
|
||||
"status": "backlog",
|
||||
"label": "bug",
|
||||
"priority": "high",
|
||||
"assignee": ""
|
||||
},
|
||||
{
|
||||
"id": "TASK-4369",
|
||||
"title": "Try to input the SCSI bus, maybe it will generate the 1080p pixel!",
|
||||
"status": "backlog",
|
||||
"label": "bug",
|
||||
"priority": "high",
|
||||
"assignee": ""
|
||||
},
|
||||
{
|
||||
"id": "TASK-9035",
|
||||
"title": "We need to override the solid state PNG array!",
|
||||
"status": "canceled",
|
||||
"label": "documentation",
|
||||
"priority": "low",
|
||||
"assignee": ""
|
||||
},
|
||||
{
|
||||
"id": "TASK-3970",
|
||||
"title": "You can't index the transmitter without quantifying the haptic ASCII card!",
|
||||
"status": "todo",
|
||||
"label": "documentation",
|
||||
"priority": "medium",
|
||||
"assignee": ""
|
||||
},
|
||||
{
|
||||
"id": "TASK-4473",
|
||||
"title": "You can't bypass the protocol without overriding the neural RSS program!",
|
||||
"status": "todo",
|
||||
"label": "documentation",
|
||||
"priority": "low",
|
||||
"assignee": ""
|
||||
},
|
||||
{
|
||||
"id": "TASK-4136",
|
||||
"title": "You can't hack the hard drive without hacking the primary JSON program!",
|
||||
"status": "canceled",
|
||||
"label": "bug",
|
||||
"priority": "medium",
|
||||
"assignee": ""
|
||||
},
|
||||
{
|
||||
"id": "TASK-3939",
|
||||
"title": "Use the back-end SQL firewall, then you can connect the neural hard drive!",
|
||||
"status": "done",
|
||||
"label": "feature",
|
||||
"priority": "low",
|
||||
"assignee": ""
|
||||
},
|
||||
{
|
||||
"id": "TASK-2007",
|
||||
"title": "I'll input the back-end USB protocol, that should bandwidth the PCI system!",
|
||||
"status": "backlog",
|
||||
"label": "bug",
|
||||
"priority": "high",
|
||||
"assignee": ""
|
||||
},
|
||||
{
|
||||
"id": "TASK-7516",
|
||||
"title": "Use the primary SQL program, then you can generate the auxiliary transmitter!",
|
||||
"status": "done",
|
||||
"label": "documentation",
|
||||
"priority": "medium",
|
||||
"assignee": ""
|
||||
},
|
||||
{
|
||||
"id": "TASK-6906",
|
||||
"title": "Try to back up the DRAM system, maybe it will reboot the online transmitter!",
|
||||
"status": "done",
|
||||
"label": "feature",
|
||||
"priority": "high",
|
||||
"assignee": ""
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"assignee": ""
|
||||
}
|
||||
]
|
14
src/routes/(user)/dashboard/+layout.svelte
Normal file
14
src/routes/(user)/dashboard/+layout.svelte
Normal file
|
@ -0,0 +1,14 @@
|
|||
<script>
|
||||
import Separator from '$lib/components/ui/separator/separator.svelte';
|
||||
</script>
|
||||
|
||||
<div class="space-y-6 p-10 pb-16 md:block">
|
||||
<div class="space-y-0.5">
|
||||
<h2 class="text-2xl font-bold tracking-tight">Dashboard</h2>
|
||||
<p class="text-muted-foreground">
|
||||
Utilize the filtering options and actions to manage your tickets.
|
||||
</p>
|
||||
</div>
|
||||
<Separator class="my-6" />
|
||||
<slot />
|
||||
</div>
|
8
src/routes/(user)/dashboard/+page.svelte
Normal file
8
src/routes/(user)/dashboard/+page.svelte
Normal file
|
@ -0,0 +1,8 @@
|
|||
<script lang="ts">
|
||||
import DataTable from './(components)/data-table.svelte';
|
||||
import ticketData from './(data)/tickets.json';
|
||||
</script>
|
||||
|
||||
<div class="space-y-6">
|
||||
<DataTable data={ticketData} />
|
||||
</div>
|
43
src/routes/(user)/settings/(components)/sidebar-nav.svelte
Normal file
43
src/routes/(user)/settings/(components)/sidebar-nav.svelte
Normal 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>
|
45
src/routes/(user)/settings/+layout.svelte
Normal file
45
src/routes/(user)/settings/+layout.svelte
Normal file
|
@ -0,0 +1,45 @@
|
|||
<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",
|
||||
},
|
||||
{
|
||||
title: "Display",
|
||||
href: "/settings/display",
|
||||
},
|
||||
];
|
||||
</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>
|
25
src/routes/(user)/settings/+page.server.ts
Normal file
25
src/routes/(user)/settings/+page.server.ts
Normal file
|
@ -0,0 +1,25 @@
|
|||
import type { PageServerLoad } from "./$types";
|
||||
import { superValidate } from "sveltekit-superforms";
|
||||
import { zod } from "sveltekit-superforms/adapters";
|
||||
import { profileFormSchema } from "./profile-form.svelte";
|
||||
import { fail, type Actions } from "@sveltejs/kit";
|
||||
|
||||
export const load: PageServerLoad = async () => {
|
||||
return {
|
||||
form: await superValidate(zod(profileFormSchema)),
|
||||
};
|
||||
};
|
||||
|
||||
export const actions: Actions = {
|
||||
default: async (event) => {
|
||||
const form = await superValidate(event, zod(profileFormSchema));
|
||||
if (!form.valid) {
|
||||
return fail(400, {
|
||||
form,
|
||||
});
|
||||
}
|
||||
return {
|
||||
form,
|
||||
};
|
||||
},
|
||||
};
|
15
src/routes/(user)/settings/+page.svelte
Normal file
15
src/routes/(user)/settings/+page.svelte
Normal 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>
|
26
src/routes/(user)/settings/account/+page.server.ts
Normal file
26
src/routes/(user)/settings/account/+page.server.ts
Normal file
|
@ -0,0 +1,26 @@
|
|||
import { superValidate } from "sveltekit-superforms";
|
||||
import { zod } from "sveltekit-superforms/adapters";
|
||||
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(zod(accountFormSchema)),
|
||||
};
|
||||
};
|
||||
|
||||
export const actions: Actions = {
|
||||
default: async (event) => {
|
||||
const form = await superValidate(event, zod(accountFormSchema));
|
||||
if (!form.valid) {
|
||||
console.log(form);
|
||||
return fail(400, {
|
||||
form,
|
||||
});
|
||||
}
|
||||
return {
|
||||
form,
|
||||
};
|
||||
},
|
||||
};
|
18
src/routes/(user)/settings/account/+page.svelte
Normal file
18
src/routes/(user)/settings/account/+page.svelte
Normal 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>
|
179
src/routes/(user)/settings/account/account-form.svelte
Normal file
179
src/routes/(user)/settings/account/account-form.svelte
Normal file
|
@ -0,0 +1,179 @@
|
|||
<script lang="ts" context="module">
|
||||
import { z } from "zod";
|
||||
|
||||
const languages = [
|
||||
{ label: "English", value: "en" },
|
||||
{ label: "French", value: "fr" },
|
||||
{ label: "German", value: "de" },
|
||||
{ label: "Spanish", value: "es" },
|
||||
{ label: "Portuguese", value: "pt" },
|
||||
{ label: "Russian", value: "ru" },
|
||||
{ label: "Japanese", value: "ja" },
|
||||
{ label: "Korean", value: "ko" },
|
||||
{ label: "Chinese", value: "zh" },
|
||||
] as const;
|
||||
|
||||
type Language = (typeof languages)[number]["value"];
|
||||
|
||||
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(languages.map((lang) => lang.value) as [Language, ...Language[]]),
|
||||
dob: z
|
||||
.string()
|
||||
.datetime()
|
||||
// we're setting it optional so the user can clear the date and we don't run into
|
||||
// type issues, but we refine it to make sure it's not undefined
|
||||
.optional()
|
||||
.refine((date) => (date === undefined ? false : true), "Please select a valid date."),
|
||||
});
|
||||
|
||||
export type AccountFormSchema = typeof accountFormSchema;
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { Calendar as CalendarIcon, CaretSort, Check } from "radix-icons-svelte";
|
||||
import SuperDebug, { type SuperValidated, type Infer, superForm } from "sveltekit-superforms";
|
||||
import { zodClient } from "sveltekit-superforms/adapters";
|
||||
import * as Form from "$lib/components/ui/form";
|
||||
import * as Popover from "$lib/components/ui/popover";
|
||||
import * as Command from "$lib/components/ui/command";
|
||||
import { Calendar } from "$lib/components/ui/calendar";
|
||||
import { Input } from "$lib/components/ui/input";
|
||||
import { buttonVariants } from "$lib/components/ui/button";
|
||||
import { cn } from "$lib/utils";
|
||||
import { browser } from "$app/environment";
|
||||
import {
|
||||
DateFormatter,
|
||||
getLocalTimeZone,
|
||||
type DateValue,
|
||||
parseDate,
|
||||
} from "@internationalized/date";
|
||||
|
||||
export let data: SuperValidated<Infer<AccountFormSchema>>;
|
||||
|
||||
const form = superForm(data, {
|
||||
validators: zodClient(accountFormSchema),
|
||||
});
|
||||
const { form: formData, enhance, validate } = form;
|
||||
|
||||
const df = new DateFormatter("en-US", {
|
||||
dateStyle: "long",
|
||||
});
|
||||
|
||||
let dobValue: DateValue | undefined = $formData.dob ? parseDate($formData.dob) : undefined;
|
||||
</script>
|
||||
|
||||
<form method="POST" class="space-y-8" use:enhance>
|
||||
<Form.Field name="name" {form}>
|
||||
<Form.Control let:attrs>
|
||||
<Form.Label>Name</Form.Label>
|
||||
<Input {...attrs} bind:value={$formData.name} />
|
||||
</Form.Control>
|
||||
<Form.FieldErrors />
|
||||
</Form.Field>
|
||||
<Form.Field {form} name="dob" class="flex flex-col">
|
||||
<Form.Control let:attrs>
|
||||
<Form.Label>Date of Birth</Form.Label>
|
||||
<Popover.Root>
|
||||
<Popover.Trigger
|
||||
class={cn(
|
||||
buttonVariants({ variant: "outline" }),
|
||||
"w-[240px] justify-start text-left font-normal",
|
||||
!dobValue && "text-muted-foreground"
|
||||
)}
|
||||
{...attrs}
|
||||
>
|
||||
<CalendarIcon class="mr-2 h-4 w-4" />
|
||||
{dobValue ? df.format(dobValue.toDate(getLocalTimeZone())) : "Pick a date"}
|
||||
</Popover.Trigger>
|
||||
<Popover.Content class="w-auto p-0" align="start">
|
||||
<Calendar
|
||||
bind:value={dobValue}
|
||||
isDateDisabled={(currDate) => {
|
||||
const currDateObj = currDate.toDate(getLocalTimeZone());
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
|
||||
if (currDateObj > today || currDate.year < 1900) return true;
|
||||
|
||||
return false;
|
||||
}}
|
||||
onValueChange={(value) => {
|
||||
if (value === undefined) {
|
||||
$formData.dob = undefined;
|
||||
validate("dob");
|
||||
return;
|
||||
}
|
||||
$formData.dob = value.toDate(getLocalTimeZone()).toISOString();
|
||||
validate("dob");
|
||||
}}
|
||||
/>
|
||||
</Popover.Content>
|
||||
<input hidden bind:value={$formData.dob} name={attrs.name} />
|
||||
</Popover.Root>
|
||||
</Form.Control>
|
||||
<Form.FieldErrors />
|
||||
</Form.Field>
|
||||
|
||||
<Form.Field {form} name="language" class="flex flex-col">
|
||||
<Popover.Root>
|
||||
<Form.Control let:attrs>
|
||||
<Form.Label>Language</Form.Label>
|
||||
<Popover.Trigger
|
||||
role="combobox"
|
||||
class={cn(
|
||||
buttonVariants({ variant: "outline" }),
|
||||
"w-[200px] justify-between",
|
||||
!$formData.language && "text-muted-foreground"
|
||||
)}
|
||||
{...attrs}
|
||||
>
|
||||
{languages.find((lang) => lang.value === $formData.language)?.label ||
|
||||
"Select a language"}
|
||||
<CaretSort class="ml-2 size-4 shrink-0 opacity-50" />
|
||||
</Popover.Trigger>
|
||||
<input hidden bind:value={$formData.language} name={attrs.name} />
|
||||
</Form.Control>
|
||||
<Popover.Content class="w-[200px] p-0">
|
||||
<Command.Root>
|
||||
<Command.Input placeholder="Search language..." />
|
||||
<Command.Empty>No language found.</Command.Empty>
|
||||
<Command.List>
|
||||
{#each languages as language}
|
||||
<Command.Item
|
||||
{...form}
|
||||
value={language.label}
|
||||
onSelect={() => {
|
||||
$formData.language = language.value;
|
||||
validate("language");
|
||||
}}
|
||||
>
|
||||
<Check
|
||||
class={cn(
|
||||
"mr-2 size-4",
|
||||
language.value === $formData.language
|
||||
? "opacity-100"
|
||||
: "opacity-0"
|
||||
)}
|
||||
/>
|
||||
{language.label}
|
||||
</Command.Item>
|
||||
{/each}
|
||||
</Command.List>
|
||||
</Command.Root>
|
||||
</Popover.Content>
|
||||
</Popover.Root>
|
||||
</Form.Field>
|
||||
|
||||
<Form.Button>Update account</Form.Button>
|
||||
</form>
|
||||
|
||||
{#if browser}
|
||||
<SuperDebug data={$formData} />
|
||||
{/if}
|
25
src/routes/(user)/settings/appearance/+page.server.ts
Normal file
25
src/routes/(user)/settings/appearance/+page.server.ts
Normal file
|
@ -0,0 +1,25 @@
|
|||
import { superValidate } from "sveltekit-superforms";
|
||||
import { zod } from "sveltekit-superforms/adapters";
|
||||
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(zod(appearanceFormSchema)),
|
||||
};
|
||||
};
|
||||
|
||||
export const actions: Actions = {
|
||||
default: async (event) => {
|
||||
const form = await superValidate(event, zod(appearanceFormSchema));
|
||||
if (!form.valid) {
|
||||
return fail(400, {
|
||||
form,
|
||||
});
|
||||
}
|
||||
return {
|
||||
form,
|
||||
};
|
||||
},
|
||||
};
|
18
src/routes/(user)/settings/appearance/+page.svelte
Normal file
18
src/routes/(user)/settings/appearance/+page.svelte
Normal 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>
|
132
src/routes/(user)/settings/appearance/appearance-form.svelte
Normal file
132
src/routes/(user)/settings/appearance/appearance-form.svelte
Normal file
|
@ -0,0 +1,132 @@
|
|||
<script lang="ts" context="module">
|
||||
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 { ChevronDown } from "radix-icons-svelte";
|
||||
import { browser } from "$app/environment";
|
||||
import SuperDebug, { type SuperValidated, type Infer, superForm } from "sveltekit-superforms";
|
||||
import * as Form from "$lib/components/ui/form";
|
||||
import * as RadioGroup from "$lib/components/ui/radio-group";
|
||||
import Label from "$lib/components/ui/label/label.svelte";
|
||||
import { zodClient } from "sveltekit-superforms/adapters";
|
||||
import { cn } from "$lib/utils";
|
||||
import { buttonVariants } from "$lib/components/ui/button";
|
||||
export let data: SuperValidated<Infer<AppearanceFormSchema>>;
|
||||
|
||||
const form = superForm(data, {
|
||||
validators: zodClient(appearanceFormSchema),
|
||||
});
|
||||
|
||||
const { form: formData, enhance } = form;
|
||||
</script>
|
||||
|
||||
<form method="POST" use:enhance class="space-y-8">
|
||||
<Form.Field {form} name="font">
|
||||
<Form.Control let:attrs>
|
||||
<Form.Label>Font</Form.Label>
|
||||
<div class="relative w-max">
|
||||
<select
|
||||
{...attrs}
|
||||
class={cn(
|
||||
buttonVariants({ variant: "outline" }),
|
||||
"w-[200px] appearance-none font-normal"
|
||||
)}
|
||||
bind:value={$formData.font}
|
||||
>
|
||||
<option value="inter">Inter</option>
|
||||
<option value="manrope">Manrope</option>
|
||||
<option value="system">System</option>
|
||||
</select>
|
||||
<ChevronDown class="absolute right-3 top-2.5 size-4 opacity-50" />
|
||||
</div>
|
||||
</Form.Control>
|
||||
<Form.Description>Set the font you want to use in the dashboard.</Form.Description>
|
||||
<Form.FieldErrors />
|
||||
</Form.Field>
|
||||
<Form.Fieldset {form} name="theme">
|
||||
<Form.Legend>Theme</Form.Legend>
|
||||
<Form.Description>Select the theme for the dashboard.</Form.Description>
|
||||
<Form.FieldErrors />
|
||||
<RadioGroup.Root
|
||||
class="grid max-w-md grid-cols-2 gap-8 pt-2"
|
||||
orientation="horizontal"
|
||||
bind:value={$formData.theme}
|
||||
>
|
||||
<Form.Control let:attrs>
|
||||
<Label class="[&:has([data-state=checked])>div]:border-primary">
|
||||
<RadioGroup.Item {...attrs} 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>
|
||||
</Form.Control>
|
||||
<Form.Control let:attrs>
|
||||
<Label class="[&:has([data-state=checked])>div]:border-primary">
|
||||
<RadioGroup.Item {...attrs} 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.Control>
|
||||
<RadioGroup.Input name="theme" />
|
||||
</RadioGroup.Root>
|
||||
</Form.Fieldset>
|
||||
<Form.Button>Update preferences</Form.Button>
|
||||
</form>
|
||||
|
||||
{#if browser}
|
||||
<SuperDebug data={$formData} />
|
||||
{/if}
|
25
src/routes/(user)/settings/display/+page.server.ts
Normal file
25
src/routes/(user)/settings/display/+page.server.ts
Normal file
|
@ -0,0 +1,25 @@
|
|||
import type { PageServerLoad } from "./$types";
|
||||
import { superValidate } from "sveltekit-superforms";
|
||||
import { zod } from "sveltekit-superforms/adapters";
|
||||
import { displayFormSchema } from "./display-form.svelte";
|
||||
import { fail, type Actions } from "@sveltejs/kit";
|
||||
|
||||
export const load: PageServerLoad = async () => {
|
||||
return {
|
||||
form: await superValidate(zod(displayFormSchema)),
|
||||
};
|
||||
};
|
||||
|
||||
export const actions: Actions = {
|
||||
default: async (event) => {
|
||||
const form = await superValidate(event, zod(displayFormSchema));
|
||||
if (!form.valid) {
|
||||
return fail(400, {
|
||||
form,
|
||||
});
|
||||
}
|
||||
return {
|
||||
form,
|
||||
};
|
||||
},
|
||||
};
|
17
src/routes/(user)/settings/display/+page.svelte
Normal file
17
src/routes/(user)/settings/display/+page.svelte
Normal file
|
@ -0,0 +1,17 @@
|
|||
<script lang="ts">
|
||||
import type { PageData } from "./$types";
|
||||
import DisplayForm from "./display-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">Display</h3>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
Turn items on or off to control what's displayed in the app.
|
||||
</p>
|
||||
</div>
|
||||
<Separator />
|
||||
<DisplayForm data={data.form} />
|
||||
</div>
|
94
src/routes/(user)/settings/display/display-form.svelte
Normal file
94
src/routes/(user)/settings/display/display-form.svelte
Normal file
|
@ -0,0 +1,94 @@
|
|||
<script lang="ts" context="module">
|
||||
import { z } from "zod";
|
||||
const items = [
|
||||
{
|
||||
id: "recents",
|
||||
label: "Recents",
|
||||
},
|
||||
{
|
||||
id: "home",
|
||||
label: "Home",
|
||||
},
|
||||
{
|
||||
id: "applications",
|
||||
label: "Applications",
|
||||
},
|
||||
{
|
||||
id: "desktop",
|
||||
label: "Desktop",
|
||||
},
|
||||
{
|
||||
id: "downloads",
|
||||
label: "Downloads",
|
||||
},
|
||||
{
|
||||
id: "documents",
|
||||
label: "Documents",
|
||||
},
|
||||
] as const;
|
||||
export const displayFormSchema = z.object({
|
||||
items: z.array(z.string()).refine((value) => value.some((item) => item), {
|
||||
message: "You have to select at least one item.",
|
||||
}),
|
||||
});
|
||||
export type DisplayFormSchema = typeof displayFormSchema;
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import * as Form from "$lib/components/ui/form";
|
||||
import * as Checkbox from "$lib/components/ui/checkbox";
|
||||
import { type SuperValidated, type Infer, superForm } from "sveltekit-superforms";
|
||||
import SuperDebug from "sveltekit-superforms";
|
||||
import { zodClient } from "sveltekit-superforms/adapters";
|
||||
import { browser } from "$app/environment";
|
||||
|
||||
export let data: SuperValidated<Infer<DisplayFormSchema>>;
|
||||
|
||||
const form = superForm(data, {
|
||||
validators: zodClient(displayFormSchema),
|
||||
});
|
||||
|
||||
const { form: formData, enhance } = form;
|
||||
</script>
|
||||
|
||||
<form method="POST" class="space-y-8" use:enhance>
|
||||
<Form.Fieldset {form} name="items" class="space-y-0">
|
||||
<div class="mb-4">
|
||||
<Form.Legend class="text-base">Sidebar</Form.Legend>
|
||||
<Form.Description>
|
||||
Select the items you want to display in the sidebar.
|
||||
</Form.Description>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
{#each items as item}
|
||||
{@const checked = $formData.items.includes(item.id)}
|
||||
<div class="flex flex-row items-center space-x-3">
|
||||
<Form.Control let:attrs>
|
||||
{@const { name, ...rest } = attrs}
|
||||
<Checkbox.Root
|
||||
{...rest}
|
||||
{checked}
|
||||
onCheckedChange={(v) => {
|
||||
if (v) {
|
||||
$formData.items = [...$formData.items, item.id];
|
||||
} else {
|
||||
$formData.items = $formData.items.filter((i) => i !== item.id);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Form.Label class="font-normal">
|
||||
{item.label}
|
||||
</Form.Label>
|
||||
<input type="checkbox" {name} hidden value={item.id} {checked} />
|
||||
</Form.Control>
|
||||
</div>
|
||||
{/each}
|
||||
<Form.FieldErrors />
|
||||
</div>
|
||||
</Form.Fieldset>
|
||||
<Form.Button>Update display</Form.Button>
|
||||
</form>
|
||||
|
||||
{#if browser}
|
||||
<SuperDebug data={$formData} />
|
||||
{/if}
|
25
src/routes/(user)/settings/notifications/+page.server.ts
Normal file
25
src/routes/(user)/settings/notifications/+page.server.ts
Normal file
|
@ -0,0 +1,25 @@
|
|||
import { superValidate } from "sveltekit-superforms";
|
||||
import { zod } from "sveltekit-superforms/adapters";
|
||||
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(zod(notificationsFormSchema)),
|
||||
};
|
||||
};
|
||||
|
||||
export const actions: Actions = {
|
||||
default: async (event) => {
|
||||
const form = await superValidate(event, zod(notificationsFormSchema));
|
||||
if (!form.valid) {
|
||||
return fail(400, {
|
||||
form,
|
||||
});
|
||||
}
|
||||
return {
|
||||
form,
|
||||
};
|
||||
},
|
||||
};
|
16
src/routes/(user)/settings/notifications/+page.svelte
Normal file
16
src/routes/(user)/settings/notifications/+page.svelte
Normal 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>
|
|
@ -0,0 +1,145 @@
|
|||
<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 SuperDebug, { type SuperValidated, type Infer, superForm } from "sveltekit-superforms";
|
||||
import * as Form from "$lib/components/ui/form";
|
||||
import * as RadioGroup from "$lib/components/ui/radio-group";
|
||||
import { Switch } from "$lib/components/ui/switch";
|
||||
import { zodClient } from "sveltekit-superforms/adapters";
|
||||
import Checkbox from "$lib/components/ui/checkbox/checkbox.svelte";
|
||||
import { browser } from "$app/environment";
|
||||
|
||||
export let data: SuperValidated<Infer<NotificationFormSchema>>;
|
||||
|
||||
const form = superForm(data, {
|
||||
validators: zodClient(notificationsFormSchema),
|
||||
});
|
||||
|
||||
const { form: formData, enhance } = form;
|
||||
</script>
|
||||
|
||||
<form method="POST" use:enhance class="space-y-8">
|
||||
<Form.Fieldset {form} name="type">
|
||||
<Form.Legend>Notify me about...</Form.Legend>
|
||||
<Form.Control>
|
||||
<RadioGroup.Root bind:value={$formData.type}>
|
||||
<div class="flex items-center space-x-3">
|
||||
<Form.Control let:attrs>
|
||||
<RadioGroup.Item value="all" {...attrs} />
|
||||
<Form.Label>All new messages</Form.Label>
|
||||
</Form.Control>
|
||||
</div>
|
||||
<div class="flex items-center space-x-3">
|
||||
<Form.Control let:attrs>
|
||||
<RadioGroup.Item value="mentions" {...attrs} />
|
||||
<Form.Label>Direct messages and mentions</Form.Label>
|
||||
</Form.Control>
|
||||
</div>
|
||||
<div class="flex items-center space-x-3">
|
||||
<Form.Control let:attrs>
|
||||
<RadioGroup.Item value="none" {...attrs} />
|
||||
<Form.Label>Nothing</Form.Label>
|
||||
</Form.Control>
|
||||
</div>
|
||||
<RadioGroup.Input name="type" />
|
||||
</RadioGroup.Root>
|
||||
</Form.Control>
|
||||
</Form.Fieldset>
|
||||
<div>
|
||||
<h3 class="mb-4 text-lg font-medium">Email Notifications</h3>
|
||||
<div class="space-y-4">
|
||||
<Form.Field
|
||||
{form}
|
||||
name="communication_emails"
|
||||
class="flex flex-row items-center justify-between rounded-lg border p-4"
|
||||
>
|
||||
<Form.Control let:attrs>
|
||||
<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>
|
||||
<Switch includeInput {...attrs} bind:checked={$formData.communication_emails} />
|
||||
</Form.Control>
|
||||
</Form.Field>
|
||||
<Form.Field
|
||||
{form}
|
||||
name="marketing_emails"
|
||||
class="flex flex-row items-center justify-between rounded-lg border p-4"
|
||||
>
|
||||
<Form.Control let:attrs>
|
||||
<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>
|
||||
<Switch includeInput {...attrs} bind:checked={$formData.marketing_emails} />
|
||||
</Form.Control>
|
||||
</Form.Field>
|
||||
<Form.Field
|
||||
{form}
|
||||
name="social_emails"
|
||||
class="flex flex-row items-center justify-between rounded-lg border p-4"
|
||||
>
|
||||
<Form.Control let:attrs>
|
||||
<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>
|
||||
<Switch includeInput {...attrs} bind:checked={$formData.social_emails} />
|
||||
</Form.Control>
|
||||
</Form.Field>
|
||||
<Form.Field
|
||||
{form}
|
||||
name="security_emails"
|
||||
class="flex flex-row items-center justify-between rounded-lg border p-4"
|
||||
>
|
||||
<Form.Control let:attrs>
|
||||
<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>
|
||||
<Switch includeInput {...attrs} bind:checked={$formData.security_emails} />
|
||||
</Form.Control>
|
||||
</Form.Field>
|
||||
</div>
|
||||
</div>
|
||||
<Form.Field {form} name="mobile" class="flex flex-row items-start space-x-3 space-y-0">
|
||||
<Form.Control let:attrs>
|
||||
<Checkbox {...attrs} bind:checked={$formData.mobile} />
|
||||
<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="/examples/forms"
|
||||
>mobile settings</a
|
||||
> page.
|
||||
</Form.Description>
|
||||
</div>
|
||||
<input name={attrs.name} bind:value={$formData.mobile} hidden />
|
||||
</Form.Control>
|
||||
</Form.Field>
|
||||
<Form.Button>Update notifications</Form.Button>
|
||||
</form>
|
||||
|
||||
{#if browser}
|
||||
<SuperDebug data={$formData} />
|
||||
{/if}
|
122
src/routes/(user)/settings/profile-form.svelte
Normal file
122
src/routes/(user)/settings/profile-form.svelte
Normal file
|
@ -0,0 +1,122 @@
|
|||
<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."),
|
||||
urls: z
|
||||
.array(z.string().url())
|
||||
.default(["https://shadcn.com", "https://twitter.com/shadcn"]),
|
||||
});
|
||||
export type ProfileFormSchema = typeof profileFormSchema;
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import * as Form from "$lib/components/ui/form";
|
||||
import * as Select from "$lib/components/ui/select";
|
||||
import { Input } from "$lib/components/ui/input";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import { Textarea } from "$lib/components/ui/textarea";
|
||||
import { type SuperValidated, type Infer, superForm } from "sveltekit-superforms";
|
||||
import SuperDebug from "sveltekit-superforms";
|
||||
import { zodClient } from "sveltekit-superforms/adapters";
|
||||
import { cn } from "$lib/utils";
|
||||
import { browser } from "$app/environment";
|
||||
|
||||
export let data: SuperValidated<Infer<ProfileFormSchema>>;
|
||||
|
||||
const form = superForm(data, {
|
||||
validators: zodClient(profileFormSchema),
|
||||
});
|
||||
|
||||
const { form: formData, enhance } = form;
|
||||
|
||||
function addUrl() {
|
||||
$formData.urls = [...$formData.urls, ""];
|
||||
}
|
||||
|
||||
$: selectedEmail = {
|
||||
label: $formData.email,
|
||||
value: $formData.email,
|
||||
};
|
||||
</script>
|
||||
|
||||
<form method="POST" class="space-y-8" use:enhance>
|
||||
<Form.Field {form} name="username">
|
||||
<Form.Control let:attrs>
|
||||
<Form.Label>Username</Form.Label>
|
||||
<Input placeholder="@shadcn" {...attrs} bind:value={$formData.username} />
|
||||
</Form.Control>
|
||||
<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.FieldErrors />
|
||||
</Form.Field>
|
||||
|
||||
<Form.Field {form} name="email">
|
||||
<Form.Control let:attrs>
|
||||
<Form.Label>Email</Form.Label>
|
||||
<Select.Root
|
||||
selected={selectedEmail}
|
||||
onSelectedChange={(s) => {
|
||||
s && ($formData.email = s.value);
|
||||
}}
|
||||
>
|
||||
<Select.Trigger {...attrs}>
|
||||
<Select.Value placeholder="Select a verified email to display" />
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
<Select.Item value="m@example.com" label="m@example.com" />
|
||||
<Select.Item value="m@google.com" label="m@google.com" />
|
||||
<Select.Item value="m@support.com" label="m@supporte.com" />
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
<input hidden name={attrs.name} bind:value={$formData.email} />
|
||||
</Form.Control>
|
||||
<Form.Description>
|
||||
You can manage verified email addresses in your <a href="/examples/forms"
|
||||
>email settings</a
|
||||
>.
|
||||
</Form.Description>
|
||||
<Form.FieldErrors />
|
||||
</Form.Field>
|
||||
<Form.Field {form} name="bio">
|
||||
<Form.Control let:attrs>
|
||||
<Form.Label>Bio</Form.Label>
|
||||
<Textarea {...attrs} bind:value={$formData.bio} />
|
||||
</Form.Control>
|
||||
<Form.Description>
|
||||
You can <span>@mention</span> other users and organizations to link to them.
|
||||
</Form.Description>
|
||||
<Form.FieldErrors />
|
||||
</Form.Field>
|
||||
<div>
|
||||
<Form.Fieldset {form} name="urls">
|
||||
<Form.Legend>URLs</Form.Legend>
|
||||
{#each $formData.urls as _, i}
|
||||
<Form.ElementField {form} name="urls[{i}]">
|
||||
<Form.Description class={cn(i !== 0 && "sr-only")}>
|
||||
Add links to your website, blog, or social media profiles.
|
||||
</Form.Description>
|
||||
<Form.Control let:attrs>
|
||||
<Input {...attrs} bind:value={$formData.urls[i]} />
|
||||
</Form.Control>
|
||||
<Form.FieldErrors />
|
||||
</Form.ElementField>
|
||||
{/each}
|
||||
</Form.Fieldset>
|
||||
<Button type="button" variant="outline" size="sm" class="mt-2" on:click={addUrl}>
|
||||
Add URL
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Form.Button>Update profile</Form.Button>
|
||||
</form>
|
||||
|
||||
{#if browser}
|
||||
<SuperDebug data={$formData} />
|
||||
{/if}
|
Loading…
Add table
Add a link
Reference in a new issue