update qr settings page to use page action

pull/3/head
TZGyn 2 years ago
parent 9024bd4f04
commit 79ec261c26
Signed by: TZGyn
GPG Key ID: 122EAF77AE81FD4A

Binary file not shown.

@ -14,8 +14,8 @@
},
"devDependencies": {
"@sveltejs/adapter-node": "^2.0.0",
"@sveltejs/kit": "^2.0.0",
"@sveltejs/vite-plugin-svelte": "^3.0.0",
"@sveltejs/kit": "^2.5.5",
"@sveltejs/vite-plugin-svelte": "^3.0.2",
"@types/qrcode": "^1.5.5",
"autoprefixer": "^10.4.14",
"bun-types": "^1.0.11",
@ -26,13 +26,13 @@
"prettier": "^3.1.0",
"prettier-plugin-svelte": "^3.1.0",
"prettier-plugin-tailwindcss": "^0.5.7",
"svelte": "^4.0.5",
"svelte": "^4.2.12",
"svelte-adapter-bun": "^0.5.1",
"svelte-check": "^3.4.3",
"tailwindcss": "^3.3.2",
"tslib": "^2.4.1",
"typescript": "^5.0.0",
"vite": "^5.0.0"
"vite": "^5.2.8"
},
"type": "module",
"dependencies": {
@ -40,10 +40,11 @@
"@prgm/sveltekit-progress-bar": "^2.0.0",
"@types/he": "^1.2.3",
"apexcharts": "^3.44.0",
"bits-ui": "^0.19.7",
"bits-ui": "^0.21.2",
"clsx": "^2.0.0",
"cmdk-sv": "^0.0.13",
"drizzle-orm": "latest",
"formsnap": "^1.0.0",
"he": "^1.2.0",
"lucide-svelte": "^0.356.0",
"mode-watcher": "^0.1.2",
@ -54,6 +55,7 @@
"postgres": "^3.4.3",
"qrcode": "^1.5.3",
"svelte-sonner": "^0.3.10",
"sveltekit-superforms": "^2.12.2",
"tailwind-merge": "^2.0.0",
"tailwind-variants": "^0.1.18",
"zod": "^3.22.4"

@ -1,9 +1,10 @@
<script lang="ts">
import * as Button from '$lib/components/ui/button'
type $$Props = Button.Props
type $$Events = Button.Events
import * as Button from "$lib/components/ui/button/index.js";
type $$Props = Button.Props;
type $$Events = Button.Events;
</script>
<Button.Root type="submit" {...$$restProps} on:click on:keydown>
<Button.Root type="submit" on:click on:keydown {...$$restProps}>
<slot />
</Button.Root>

@ -1,25 +0,0 @@
<script lang="ts">
import { getFormField } from 'formsnap'
import type { Checkbox as CheckboxPrimitive } from 'bits-ui'
import { Checkbox } from '$lib/components/ui/checkbox'
type $$Props = CheckboxPrimitive.Props
type $$Events = CheckboxPrimitive.Events
export let onCheckedChange: $$Props['onCheckedChange'] = undefined
const { name, setValue, attrStore, value } = getFormField()
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { name: nameAttr, value: valueAttr, ...rest } = $attrStore
</script>
<Checkbox
{...rest}
checked={typeof $value === 'boolean' ? $value : false}
onCheckedChange={(v) => {
onCheckedChange?.(v)
setValue(v)
}}
{...$$restProps}
on:click
on:keydown />
<input hidden {name} value={$value} />

@ -1,15 +1,17 @@
<script lang="ts">
import { Form as FormPrimitive } from 'formsnap'
import { cn } from '$lib/utils'
import type { HTMLAttributes } from 'svelte/elements'
import * as FormPrimitive from "formsnap";
import type { HTMLAttributes } from "svelte/elements";
import { cn } from "$lib/utils.js";
type $$Props = HTMLAttributes<HTMLSpanElement>
let className: string | undefined | null = undefined
export { className as class }
type $$Props = HTMLAttributes<HTMLSpanElement>;
let className: string | undefined | null = undefined;
export { className as class };
</script>
<FormPrimitive.Description
class={cn('text-sm text-muted-foreground', className)}
{...$$restProps}>
<slot />
class={cn("text-sm text-muted-foreground", className)}
{...$$restProps}
let:descriptionAttrs
>
<slot {descriptionAttrs} />
</FormPrimitive.Description>

@ -0,0 +1,25 @@
<script lang="ts" context="module">
import type { FormPathLeaves, SuperForm } from "sveltekit-superforms";
type T = Record<string, unknown>;
type U = FormPathLeaves<T>;
</script>
<script lang="ts" generics="T extends Record<string, unknown>, U extends FormPathLeaves<T>">
import type { HTMLAttributes } from "svelte/elements";
import * as FormPrimitive from "formsnap";
import { cn } from "$lib/utils.js";
type $$Props = FormPrimitive.ElementFieldProps<T, U> & HTMLAttributes<HTMLElement>;
export let form: SuperForm<T>;
export let name: U;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<FormPrimitive.ElementField {form} {name} let:constraints let:errors let:tainted let:value>
<div class={cn("space-y-2", className)}>
<slot {constraints} {errors} {tainted} {value} />
</div>
</FormPrimitive.ElementField>

@ -0,0 +1,26 @@
<script lang="ts">
import * as FormPrimitive from "formsnap";
import { cn } from "$lib/utils.js";
type $$Props = FormPrimitive.FieldErrorsProps & {
errorClasses?: string | undefined | null;
};
let className: $$Props["class"] = undefined;
export { className as class };
export let errorClasses: $$Props["class"] = undefined;
</script>
<FormPrimitive.FieldErrors
class={cn("text-sm font-medium text-destructive", className)}
{...$$restProps}
let:errors
let:fieldErrorsAttrs
let:errorAttrs
>
<slot {errors} {fieldErrorsAttrs} {errorAttrs}>
{#each errors as error}
<div {...errorAttrs} class={cn(errorClasses)}>{error}</div>
{/each}
</slot>
</FormPrimitive.FieldErrors>

@ -0,0 +1,25 @@
<script lang="ts" context="module">
import type { FormPath, SuperForm } from "sveltekit-superforms";
type T = Record<string, unknown>;
type U = FormPath<T>;
</script>
<script lang="ts" generics="T extends Record<string, unknown>, U extends FormPath<T>">
import type { HTMLAttributes } from "svelte/elements";
import * as FormPrimitive from "formsnap";
import { cn } from "$lib/utils.js";
type $$Props = FormPrimitive.FieldProps<T, U> & HTMLAttributes<HTMLElement>;
export let form: SuperForm<T>;
export let name: U;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<FormPrimitive.Field {form} {name} let:constraints let:errors let:tainted let:value>
<div class={cn("space-y-2", className)}>
<slot {constraints} {errors} {tainted} {value} />
</div>
</FormPrimitive.Field>

@ -0,0 +1,30 @@
<script lang="ts" context="module">
import type { FormPath, SuperForm } from "sveltekit-superforms";
type T = Record<string, unknown>;
type U = FormPath<T>;
</script>
<script lang="ts" generics="T extends Record<string, unknown>, U extends FormPath<T>">
import * as FormPrimitive from "formsnap";
import { cn } from "$lib/utils.js";
type $$Props = FormPrimitive.FieldsetProps<T, U>;
export let form: SuperForm<T>;
export let name: U;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<FormPrimitive.Fieldset
{form}
{name}
let:constraints
let:errors
let:tainted
let:value
class={cn("space-y-2", className)}
>
<slot {constraints} {errors} {tainted} {value} />
</FormPrimitive.Fieldset>

@ -1,27 +0,0 @@
<script lang="ts">
import { getFormField } from 'formsnap'
import type { HTMLInputAttributes } from 'svelte/elements'
import { Input, type InputEvents } from '$lib/components/ui/input'
type $$Props = HTMLInputAttributes
type $$Events = InputEvents
const { attrStore, value } = getFormField()
</script>
<Input
{...$attrStore}
bind:value={$value}
{...$$restProps}
on:blur
on:change
on:click
on:focus
on:keydown
on:keypress
on:keyup
on:mouseover
on:mouseenter
on:mouseleave
on:paste
on:input />

@ -1,12 +0,0 @@
<script lang="ts">
import { cn } from '$lib/utils'
import type { HTMLAttributes } from 'svelte/elements'
type $$Props = HTMLAttributes<HTMLDivElement>
let className: string | undefined | null = undefined
export { className as class }
</script>
<div class={cn('space-y-2', className)} {...$$restProps}>
<slot />
</div>

@ -1,20 +1,17 @@
<script lang="ts">
import type { Label as LabelPrimitive } from 'bits-ui'
import { getFormField } from 'formsnap'
import { cn } from '$lib/utils'
import { Label } from '$lib/components/ui/label'
import type { Label as LabelPrimitive } from "bits-ui";
import { getFormControl } from "formsnap";
import { cn } from "$lib/utils.js";
import { Label } from "$lib/components/ui/label/index.js";
type $$Props = LabelPrimitive.Props
type $$Props = LabelPrimitive.Props;
let className: $$Props['class'] = undefined
export { className as class }
let className: $$Props["class"] = undefined;
export { className as class };
const { errors, ids } = getFormField()
const { labelAttrs } = getFormControl();
</script>
<Label
for={$ids.input}
class={cn($errors && 'text-destructive', className)}
{...$$restProps}>
<slot />
<Label {...$labelAttrs} class={cn("data-[fs-error]:text-destructive", className)} {...$$restProps}>
<slot {labelAttrs} />
</Label>

@ -0,0 +1,17 @@
<script lang="ts">
import * as FormPrimitive from "formsnap";
import { cn } from "$lib/utils.js";
type $$Props = FormPrimitive.LegendProps;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<FormPrimitive.Legend
{...$$restProps}
class={cn("text-sm font-medium leading-none data-[fs-error]:text-destructive", className)}
let:legendAttrs
>
<slot {legendAttrs} />
</FormPrimitive.Legend>

@ -1,23 +0,0 @@
<script lang="ts">
import { Form as FormPrimitive } from 'formsnap'
import { buttonVariants } from '$lib/components/ui/button'
import { cn } from '$lib/utils'
import { ChevronDown } from 'lucide-svelte'
import type { HTMLSelectAttributes } from 'svelte/elements'
type $$Props = HTMLSelectAttributes
let className: string | undefined | null = undefined
export { className as class }
</script>
<FormPrimitive.Select
class={cn(
buttonVariants({ variant: 'outline' }),
'appearance-none bg-transparent font-normal',
className,
)}
{...$$restProps}>
<slot />
</FormPrimitive.Select>
<ChevronDown class="absolute right-3 top-2.5 h-4 w-4 opacity-50" />

@ -1,21 +0,0 @@
<script lang="ts">
import { getFormField } from 'formsnap'
import type { RadioGroup as RadioGroupPrimitive } from 'bits-ui'
import * as RadioGroup from '$lib/components/ui/radio-group'
type $$Props = RadioGroupPrimitive.Props
const { attrStore, setValue, name, value } = getFormField()
export let onValueChange: $$Props['onValueChange'] = undefined
</script>
<RadioGroup.Root
{...$attrStore}
onValueChange={(v) => {
onValueChange?.(v)
setValue(v)
}}
{...$$restProps}>
<slot />
<input hidden {name} value={$value} />
</RadioGroup.Root>

@ -1,17 +0,0 @@
<script lang="ts">
import * as Select from '$lib/components/ui/select'
import type { Select as SelectPrimitive } from 'bits-ui'
import { getFormField } from 'formsnap'
type $$Props = SelectPrimitive.TriggerProps & {
placeholder?: string
}
type $$Events = SelectPrimitive.TriggerEvents
const { attrStore } = getFormField()
export let placeholder = ''
</script>
<Select.Trigger {...$$restProps} {...$attrStore} on:click on:keydown>
<Select.Value {placeholder} />
<slot />
</Select.Trigger>

@ -1,19 +0,0 @@
<script lang="ts">
import * as Select from '$lib/components/ui/select'
import { getFormField } from 'formsnap'
import type { Select as SelectPrimitive } from 'bits-ui'
type $$Props = SelectPrimitive.Props
const { setValue, name, value } = getFormField()
export let onSelectedChange: $$Props['onSelectedChange'] = undefined
</script>
<Select.Root
onSelectedChange={(v) => {
onSelectedChange?.(v)
setValue(v ? v.value : undefined)
}}
{...$$restProps}>
<slot />
<input hidden {name} value={$value} />
</Select.Root>

@ -1,23 +0,0 @@
<script lang="ts">
import { getFormField } from 'formsnap'
import type { Switch as SwitchPrimitive } from 'bits-ui'
import { Switch } from '$lib/components/ui/switch'
type $$Props = SwitchPrimitive.Props
type $$Events = SwitchPrimitive.Events
export let onCheckedChange: $$Props['onCheckedChange'] = undefined
const { name, setValue, attrStore, value } = getFormField()
</script>
<Switch
{...$attrStore}
checked={typeof $value === 'boolean' ? $value : false}
onCheckedChange={(v) => {
onCheckedChange?.(v)
setValue(v)
}}
{...$$restProps}
on:click
on:keydown />
<input hidden {name} value={$value} />

@ -1,31 +0,0 @@
<script lang="ts">
import { getFormField } from 'formsnap'
import type { HTMLTextareaAttributes } from 'svelte/elements'
import type { TextareaGetFormField } from '.'
import {
Textarea,
type TextareaEvents,
} from '$lib/components/ui/textarea'
type $$Props = HTMLTextareaAttributes
type $$Events = TextareaEvents
const { attrStore, value } = getFormField() as TextareaGetFormField
</script>
<Textarea
{...$attrStore}
bind:value={$value}
{...$$restProps}
on:blur
on:change
on:click
on:focus
on:keydown
on:keypress
on:keyup
on:mouseover
on:mouseenter
on:mouseleave
on:paste
on:input />

@ -1,13 +0,0 @@
<script lang="ts">
import { Form as FormPrimitive } from 'formsnap'
import { cn } from '$lib/utils'
import type { HTMLAttributes } from 'svelte/elements'
type $$Props = HTMLAttributes<HTMLParagraphElement>
let className: string | undefined | null = undefined
export { className as class }
</script>
<FormPrimitive.Validation
class={cn('text-sm font-medium text-destructive', className)}
{...$$restProps} />

@ -1,85 +1,33 @@
import { Form as FormPrimitive, getFormField } from 'formsnap'
import * as RadioGroupComp from '$lib/components/ui/radio-group'
import * as SelectComp from '$lib/components/ui/select'
import type { Writable } from 'svelte/store'
import Item from './form-item.svelte'
import Input from './form-input.svelte'
import Textarea from './form-textarea.svelte'
import Description from './form-description.svelte'
import Label from './form-label.svelte'
import Validation from './form-validation.svelte'
import Checkbox from './form-checkbox.svelte'
import Switch from './form-switch.svelte'
import NativeSelect from './form-native-select.svelte'
import RadioGroup from './form-radio-group.svelte'
import Select from './form-select.svelte'
import SelectTrigger from './form-select-trigger.svelte'
import Button from './form-button.svelte'
import * as FormPrimitive from "formsnap";
import Description from "./form-description.svelte";
import Label from "./form-label.svelte";
import FieldErrors from "./form-field-errors.svelte";
import Field from "./form-field.svelte";
import Fieldset from "./form-fieldset.svelte";
import Legend from "./form-legend.svelte";
import ElementField from "./form-element-field.svelte";
import Button from "./form-button.svelte";
const Root = FormPrimitive.Root
const Field = FormPrimitive.Field
const Control = FormPrimitive.Control
const RadioItem = RadioGroupComp.Item
const NativeRadio = FormPrimitive.Radio
const SelectContent = SelectComp.Content
const SelectLabel = SelectComp.Label
const SelectGroup = SelectComp.Group
const SelectItem = SelectComp.Item
const SelectSeparator = SelectComp.Separator
export type TextareaGetFormField = Omit<
ReturnType<typeof getFormField>,
'value'
> & {
value: Writable<string>
}
const Control = FormPrimitive.Control;
export {
Root,
Field,
Control,
Item,
Input,
Label,
Button,
Switch,
Select,
Checkbox,
Textarea,
Validation,
RadioGroup,
RadioItem,
FieldErrors,
Description,
SelectContent,
SelectLabel,
SelectGroup,
SelectItem,
SelectSeparator,
SelectTrigger,
NativeSelect,
NativeRadio,
Fieldset,
Legend,
ElementField,
//
Root as Form,
Field as FormField,
Control as FormControl,
Item as FormItem,
Input as FormInput,
Textarea as FormTextarea,
Description as FormDescription,
Label as FormLabel,
Validation as FormValidation,
NativeSelect as FormNativeSelect,
NativeRadio as FormNativeRadio,
Checkbox as FormCheckbox,
Switch as FormSwitch,
RadioGroup as FormRadioGroup,
RadioItem as FormRadioItem,
Select as FormSelect,
SelectContent as FormSelectContent,
SelectLabel as FormSelectLabel,
SelectGroup as FormSelectGroup,
SelectItem as FormSelectItem,
SelectSeparator as FormSelectSeparator,
SelectTrigger as FormSelectTrigger,
FieldErrors as FormFieldErrors,
Fieldset as FormFieldset,
Legend as FormLegend,
ElementField as FormElementField,
Button as FormButton,
}
};

@ -1,6 +1,7 @@
<script>
import { onMount } from 'svelte'
import QRCode from 'qrcode'
import { browser } from '$app/environment'
export let background = '#fff'
export let color = '#000'
@ -28,6 +29,8 @@
}
}
$: browser && background && color && generateQrCode()
onMount(() => {
generateQrCode()
})

@ -0,0 +1,56 @@
<script lang="ts">
import * as Form from '$lib/components/ui/form'
import { Input } from '$lib/components/ui/input'
import { formSchema, type FormSchema } from '../schema'
import {
type SuperValidated,
type Infer,
superForm,
} from 'sveltekit-superforms'
import { zodClient } from 'sveltekit-superforms/adapters'
import SuperDebug from 'sveltekit-superforms'
import { toast } from 'svelte-sonner'
import DemoQr from './DemoQR.svelte'
export let data: SuperValidated<Infer<FormSchema>>
const form = superForm(data, {
validators: zodClient(formSchema),
resetForm: true,
onResult: ({ result }) => {
if (result.status === 200) {
toast.success('QR settings updated')
}
if (result.status === 400) {
toast.error('Error updating QR settings')
}
},
})
const { form: formData, enhance } = form
</script>
<DemoQr
background={$formData.qr_background}
color={$formData.qr_foreground} />
<form method="POST" use:enhance class="flex flex-col gap-6">
<Form.Field {form} name="qr_background" class="flex flex-col gap-2">
<Form.Control let:attrs>
<Form.Label>Background Color</Form.Label>
<Input {...attrs} bind:value={$formData.qr_background} />
</Form.Control>
<Form.Description>QR Code background color</Form.Description>
<Form.FieldErrors />
</Form.Field>
<Form.Field {form} name="qr_foreground" class="flex flex-col gap-2">
<Form.Control let:attrs>
<Form.Label>Foreground Color</Form.Label>
<Input {...attrs} bind:value={$formData.qr_foreground} />
</Form.Control>
<Form.Description>QR Code foreground color</Form.Description>
<Form.FieldErrors />
</Form.Field>
<Form.Button class="w-fit">Save</Form.Button>
</form>
<SuperDebug data={formData} />

@ -1,10 +1,53 @@
import { db } from '$lib/db'
import type { PageServerLoad } from './$types'
import type { PageServerLoad, Actions } from './$types'
import { fail } from '@sveltejs/kit'
import { superValidate } from 'sveltekit-superforms'
import { formSchema } from './schema'
import { zod } from 'sveltekit-superforms/adapters'
import { setting } from '$lib/db/schema'
import { eq } from 'drizzle-orm'
export const load = (async (event) => {
const settings = await db.query.setting.findFirst({
where: (setting, { eq }) =>
eq(setting.userId, event.locals.user.id),
})
return { settings }
const qr_background = settings?.qr_background || '#000'
const qr_foreground = settings?.qr_foreground || '#fff'
return {
settings,
form: await superValidate(
{ qr_background, qr_foreground },
zod(formSchema),
),
}
}) satisfies PageServerLoad
export const actions: Actions = {
default: async (event) => {
const form = await superValidate(event, zod(formSchema))
if (!form.valid) {
return fail(400, {
form,
})
}
const userId = event.locals.user.id
const settings = await db.query.setting.findFirst({
where: (settingData, { eq }) => eq(settingData.userId, userId),
})
if (!settings) {
await db.insert(setting).values({ userId })
}
await db
.update(setting)
.set(form.data)
.where(eq(setting.userId, userId))
return {
form,
}
},
}

@ -1,85 +1,19 @@
<script lang="ts">
import { Separator } from '$lib/components/ui/separator'
import { Input } from '$lib/components/ui/input'
import { Label } from '$lib/components/ui/label'
import type { PageData } from './$types'
import Button from '$lib/components/ui/button/button.svelte'
import { Loader2 } from 'lucide-svelte'
import { invalidateAll } from '$app/navigation'
import { toast } from 'svelte-sonner'
import DemoQr from './(components)/DemoQR.svelte'
import Form from './(components)/form.svelte'
export let data: PageData
let isLoading = false
let qr_data = {
background_color: data.settings?.qr_background || '#fff',
foreground_color: data.settings?.qr_foreground || '#000',
}
const submit = async () => {
isLoading = true
try {
const response = await fetch('/api/account/qr', {
method: 'PUT',
body: JSON.stringify({
qr_background: qr_data.background_color,
qr_foreground: qr_data.foreground_color,
}),
})
const body = await response.json()
if (body.success) {
toast.success('QR Details Updated')
await invalidateAll()
}
} catch (error) {
toast.error('Error Occurred')
} finally {
isLoading = false
}
}
</script>
<div class="space-y-6">
<div>
<h3 class="text-lg font-medium">Account</h3>
<p class="text-muted-foreground text-sm">
Update your account settings.
<h3 class="text-lg font-medium">QR</h3>
<p class="text-sm text-muted-foreground">
Update your QR settings.
</p>
</div>
<Separator />
{#key qr_data}
<DemoQr
background={qr_data.background_color}
color={qr_data.foreground_color} />
{/key}
<div class="flex w-full max-w-sm flex-col gap-2">
<Label for="background_color">Background Color</Label>
<Input
type="text"
id="background_color"
placeholder="#ffffff"
bind:value={qr_data.background_color} />
</div>
<div class="flex w-full max-w-sm flex-col gap-2">
<Label for="foreground_color">Foreground Color</Label>
<Input
type="text"
id="foreground_color"
placeholder="#000000"
bind:value={qr_data.foreground_color} />
</div>
<Button disabled={isLoading} on:click={submit} class="flex gap-2">
{#if isLoading}
<Loader2 class="animate-spin" />
{/if}
Save</Button>
<Form data={data.form} />
</div>

@ -0,0 +1,14 @@
import { z } from 'zod'
export const formSchema = z.object({
qr_background: z
.string()
.min(1, { message: 'Background color is required' })
.max(7),
qr_foreground: z
.string()
.min(1, { message: 'Foreground color is required' })
.max(7),
})
export type FormSchema = typeof formSchema

@ -1,32 +0,0 @@
import { db } from '$lib/db'
import { setting } from '$lib/db/schema'
import { qrUpdateSchema } from '$lib/server/types'
import { eq } from 'drizzle-orm'
import type { RequestHandler } from './$types'
export const PUT: RequestHandler = async (event) => {
const body = await event.request.json()
const qrUpdate = qrUpdateSchema.safeParse(body)
const userId = event.locals.user.id
if (!qrUpdate.success) {
return new Response(JSON.stringify({ success: false }))
}
try {
const settings = await db.query.setting.findFirst({
where: (settingData, { eq }) => eq(settingData.userId, userId),
})
if (!settings) {
await db.insert(setting).values({ userId })
}
await db
.update(setting)
.set(qrUpdate.data)
.where(eq(setting.userId, userId))
return new Response(JSON.stringify({ success: true }))
} catch (error) {
return new Response(JSON.stringify({ success: false }))
}
}
Loading…
Cancel
Save