Compare commits

..

No commits in common. "3925f4e67ae6927349482a950ddbdfc2cc7caa13" and "09cdce350dd18598863c4e3cbf0b61c00f0a7407" have entirely different histories.

17 changed files with 134 additions and 368 deletions

39
package-lock.json generated
View File

@ -24,7 +24,7 @@
"@eslint/compat": "^1.4.0", "@eslint/compat": "^1.4.0",
"@eslint/js": "^9.36.0", "@eslint/js": "^9.36.0",
"@internationalized/date": "^3.10.1", "@internationalized/date": "^3.10.1",
"@lucide/svelte": "^1.7.0", "@lucide/svelte": "^0.561.0",
"@sveltejs/adapter-node": "^5.3.2", "@sveltejs/adapter-node": "^5.3.2",
"@sveltejs/kit": "2.49.5", "@sveltejs/kit": "2.49.5",
"@sveltejs/vite-plugin-svelte": "^6.2.0", "@sveltejs/vite-plugin-svelte": "^6.2.0",
@ -47,7 +47,6 @@
"prettier-plugin-tailwindcss": "^0.6.14", "prettier-plugin-tailwindcss": "^0.6.14",
"svelte": "^5.39.5", "svelte": "^5.39.5",
"svelte-check": "^4.3.2", "svelte-check": "^4.3.2",
"svelte-sonner": "^1.1.0",
"tailwind-merge": "^3.4.0", "tailwind-merge": "^3.4.0",
"tailwind-variants": "^3.2.2", "tailwind-variants": "^3.2.2",
"tailwindcss": "^4.1.13", "tailwindcss": "^4.1.13",
@ -1338,9 +1337,9 @@
} }
}, },
"node_modules/@lucide/svelte": { "node_modules/@lucide/svelte": {
"version": "1.7.0", "version": "0.561.0",
"resolved": "https://registry.npmjs.org/@lucide/svelte/-/svelte-1.7.0.tgz", "resolved": "https://registry.npmjs.org/@lucide/svelte/-/svelte-0.561.0.tgz",
"integrity": "sha512-YytBKOUBGox7yWcykZnYxOkn5WpR5G1qYXLYXV/j1B79SOTTEKzB+s5yF5Rq9l9OkweDStNH2b4yTqfvhEhV8g==", "integrity": "sha512-vofKV2UFVrKE6I4ewKJ3dfCXSV6iP6nWVmiM83MLjsU91EeJcEg7LoWUABLp/aOTxj1HQNbJD1f3g3L0JQgH9A==",
"dev": true, "dev": true,
"license": "ISC", "license": "ISC",
"peerDependencies": { "peerDependencies": {
@ -6312,36 +6311,6 @@
} }
} }
}, },
"node_modules/svelte-sonner": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/svelte-sonner/-/svelte-sonner-1.1.0.tgz",
"integrity": "sha512-3lYM6ZIqWe+p9vwwWHGWP/ZdvHiUtzURsud2quIxivrX4rvpXh6i+geBGn0m3JS6KwW6W8VgbOl3xQMcDuh6gg==",
"dev": true,
"license": "MIT",
"dependencies": {
"runed": "^0.28.0"
},
"peerDependencies": {
"svelte": "^5.0.0"
}
},
"node_modules/svelte-sonner/node_modules/runed": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/runed/-/runed-0.28.0.tgz",
"integrity": "sha512-k2xx7RuO9hWcdd9f+8JoBeqWtYrm5CALfgpkg2YDB80ds/QE4w0qqu34A7fqiAwiBBSBQOid7TLxwxVC27ymWQ==",
"dev": true,
"funding": [
"https://github.com/sponsors/huntabyte",
"https://github.com/sponsors/tglide"
],
"license": "MIT",
"dependencies": {
"esm-env": "^1.0.0"
},
"peerDependencies": {
"svelte": "^5.7.0"
}
},
"node_modules/svelte-toolbelt": { "node_modules/svelte-toolbelt": {
"version": "0.10.6", "version": "0.10.6",
"resolved": "https://registry.npmjs.org/svelte-toolbelt/-/svelte-toolbelt-0.10.6.tgz", "resolved": "https://registry.npmjs.org/svelte-toolbelt/-/svelte-toolbelt-0.10.6.tgz",

View File

@ -19,7 +19,7 @@
"@eslint/compat": "^1.4.0", "@eslint/compat": "^1.4.0",
"@eslint/js": "^9.36.0", "@eslint/js": "^9.36.0",
"@internationalized/date": "^3.10.1", "@internationalized/date": "^3.10.1",
"@lucide/svelte": "^1.7.0", "@lucide/svelte": "^0.561.0",
"@sveltejs/adapter-node": "^5.3.2", "@sveltejs/adapter-node": "^5.3.2",
"@sveltejs/kit": "2.49.5", "@sveltejs/kit": "2.49.5",
"@sveltejs/vite-plugin-svelte": "^6.2.0", "@sveltejs/vite-plugin-svelte": "^6.2.0",
@ -42,7 +42,6 @@
"prettier-plugin-tailwindcss": "^0.6.14", "prettier-plugin-tailwindcss": "^0.6.14",
"svelte": "^5.39.5", "svelte": "^5.39.5",
"svelte-check": "^4.3.2", "svelte-check": "^4.3.2",
"svelte-sonner": "^1.1.0",
"tailwind-merge": "^3.4.0", "tailwind-merge": "^3.4.0",
"tailwind-variants": "^3.2.2", "tailwind-variants": "^3.2.2",
"tailwindcss": "^4.1.13", "tailwindcss": "^4.1.13",

View File

@ -133,64 +133,3 @@
} }
} }
@custom-variant data-open {
&:where([data-state="open"]), &:where([data-open]:not([data-open="false"])) {
@slot;
}
}
@custom-variant data-closed {
&:where([data-state="closed"]), &:where([data-closed]:not([data-closed="false"])) {
@slot;
}
}
@custom-variant data-checked {
&:where([data-state="checked"]), &:where([data-checked]:not([data-checked="false"])) {
@slot;
}
}
@custom-variant data-unchecked {
&:where([data-state="unchecked"]), &:where([data-unchecked]:not([data-unchecked="false"])) {
@slot;
}
}
@custom-variant data-selected {
&:where([data-selected]) {
@slot;
}
}
@custom-variant data-disabled {
&:where([data-disabled="true"]), &:where([data-disabled]:not([data-disabled="false"])) {
@slot;
}
}
@custom-variant data-active {
&:where([data-state="active"]), &:where([data-active]:not([data-active="false"])) {
@slot;
}
}
@custom-variant data-horizontal {
&:where([data-orientation="horizontal"]) {
@slot;
}
}
@custom-variant data-vertical {
&:where([data-orientation="vertical"]) {
@slot;
}
}
@utility no-scrollbar {
-ms-overflow-style: none;
scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
}
}

View File

@ -1,4 +1,4 @@
import { DefaultUserSettings, type User, type UserPayload } from '$lib/types/user'; import type { User, UserPayload } from '$lib/types/user';
import bcrypt from 'bcrypt'; import bcrypt from 'bcrypt';
import sql from '$lib/db/db.server'; import sql from '$lib/db/db.server';
import { type Cookies, error } from '@sveltejs/kit'; import { type Cookies, error } from '@sveltejs/kit';
@ -9,10 +9,7 @@ export function setJWTCookie(cookies: Cookies, user: User) {
const payload = { const payload = {
id: user.id, id: user.id,
email: user.email, email: user.email,
name: user.name, name: user.name
settings: user.settings || DefaultUserSettings,
createdAt: user.createdAt,
lastSignIn: user.lastSignIn
}; };
if (process.env.JWT_SECRET === undefined) { if (process.env.JWT_SECRET === undefined) {
@ -68,7 +65,8 @@ export async function login(email: string, password: string): Promise<User> {
if (await bcrypt.compare(password, user.passwordHash!)) { if (await bcrypt.compare(password, user.passwordHash!)) {
delete user.passwordHash; delete user.passwordHash;
await sql` // eslint-disable-next-line @typescript-eslint/no-unused-expressions
sql`
UPDATE users UPDATE users
SET last_sign_in = NOW() SET last_sign_in = NOW()
WHERE id = ${user.id}; WHERE id = ${user.id};

View File

@ -30,6 +30,7 @@
} }
function handleFiles(files: FileList | null) { function handleFiles(files: FileList | null) {
console.log('handleFiles');
if (!files || files.length === 0) return; if (!files || files.length === 0) return;
const selected = files[0]; const selected = files[0];

View File

@ -16,21 +16,19 @@
import { approveDenyItem, restoreClaimedItem } from '$lib/db/items.remote'; import { approveDenyItem, restoreClaimedItem } from '$lib/db/items.remote';
import { invalidateAll } from '$app/navigation'; import { invalidateAll } from '$app/navigation';
import NoImagePlaceholder from './no-image-placeholder.svelte'; import NoImagePlaceholder from './no-image-placeholder.svelte';
import type { User } from '$lib/types/user';
export let item: Item = <Item>{}; export let item: Item = <Item>{};
export let user: User | null = null; export let admin = false;
// export let admin = false;
export let editCallback: (item: Item) => void; export let editCallback: (item: Item) => void;
export let inquireCallback: (item: Item) => void; export let inquireCallback: (item: Item) => void;
export let claimCallback: (item: Item) => void; export let claimCallback: (item: Item) => void;
let timeSincePosted: number | string = (new Date().getTime() - item.foundDate.getTime()) / 1000 / 60 / 60 / 24; // days let timeSincePosted: number | string = (new Date().getTime() - item.foundDate.getTime()) / 1000 / 60 / 60 / 24; // days
// if (timeSincePosted < 1) { if (timeSincePosted < 1) {
// timeSincePosted = '<1'; timeSincePosted = '<1';
// } else { } else {
// timeSincePosted = Math.round(timeSincePosted); timeSincePosted = Math.round(timeSincePosted);
// } }
</script> </script>
@ -41,14 +39,20 @@
alt="Lost item"> alt="Lost item">
{:else} {:else}
<div class="min-h-48 w-full bg-accent flex flex-col justify-center"> <div class="min-h-48 w-full bg-accent flex flex-col justify-center">
<div class="justify-center flex "> <div class="justify-center flex ">
<NoImagePlaceholder className="" /> <NoImagePlaceholder className="" />
</div> </div>
<p class="text-center mt-4">No image available</p> <p class="text-center mt-4">No image available</p>
</div> </div>
{/if} {/if}
<div class="flex-col flex h-full px-2 pb-2"> <div class="flex-col flex h-full px-2 pb-2">
<!-- <div class="font-bold inline-block">{item.title}</div>-->
<!-- <div class="inline-block">-->
<div> <div>
{#if item.transferred} {#if item.transferred}
<Badge variant="secondary" class="inline-block">In Lost & Found</Badge> <Badge variant="secondary" class="inline-block">In Lost & Found</Badge>
{:else} {:else}
@ -58,9 +62,8 @@
<Tooltip.Root> <Tooltip.Root>
<Tooltip.Trigger <Tooltip.Trigger
> >
<Badge variant="outline" <Badge variant="outline" class="inline-block">{timeSincePosted}
class="inline-block {user?.settings !== null && user?.settings !== undefined && timeSincePosted >= user.settings.staleItemDays ? 'text-warning' : ''}">{timeSincePosted < 1 ? "<1" : Math.round(timeSincePosted)} day{(timeSincePosted === 1 || timeSincePosted === '<1') ? '' : 's'} ago
day{(timeSincePosted <= 1) ? '' : 's'} ago
</Badge> </Badge>
</Tooltip.Trigger </Tooltip.Trigger
> >
@ -69,28 +72,18 @@
</Tooltip.Content> </Tooltip.Content>
</Tooltip.Root> </Tooltip.Root>
</Tooltip.Provider> </Tooltip.Provider>
</div> </div>
<div class="flex-1">{item.description}</div> <div class="flex-1">{item.description}</div>
{#if item.foundLocation} {#if item.foundLocation}
<div> <div class="mt-2">
<LocationIcon class="float-left mr-1" size={24} />
<Tooltip.Provider> <div>{item.foundLocation}</div>
<Tooltip.Root>
<Tooltip.Trigger>
<div class="mt-2 text-muted-foreground flex">
<LocationIcon class="float-left mr-1" size={24} />
<div>{item.foundLocation}</div>
</div>
</Tooltip.Trigger
>
<Tooltip.Content>
<p>Item was found here</p>
</Tooltip.Content>
</Tooltip.Root>
</Tooltip.Provider>
</div> </div>
{/if} {/if}
{#if user !== null}
{#if admin}
<div class="mt-2 justify-between flex"> <div class="mt-2 justify-between flex">
{#if item.approvedDate === null} {#if item.approvedDate === null}
@ -111,7 +104,7 @@
<Button variant="ghost" class="text-action" <Button variant="ghost" class="text-action"
onclick={() => {editCallback(item)}}> onclick={() => {editCallback(item)}}>
<PencilIcon /> <PencilIcon />
{!item.approvedDate ? 'Edit' : 'Manage'} Manage
</Button> </Button>
{:else} {:else}
<Button variant="ghost" class="text-destructive" <Button variant="ghost" class="text-destructive"

View File

@ -1,55 +1,54 @@
<script lang="ts" module> <script lang="ts" module>
import { cn, type WithElementRef } from '$lib/utils.js'; import { cn, type WithElementRef } from "$lib/utils.js";
import type { HTMLAnchorAttributes, HTMLButtonAttributes } from 'svelte/elements'; import type { HTMLAnchorAttributes, HTMLButtonAttributes } from "svelte/elements";
import { type VariantProps, tv } from 'tailwind-variants'; import { type VariantProps, tv } from "tailwind-variants";
export const buttonVariants = tv({ export const buttonVariants = tv({
base: 'focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\'size-\'])]:size-4', base: "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
variants: { variants: {
variant: { variant: {
default: 'bg-primary text-primary-foreground hover:bg-primary/90 shadow-xs', default: "bg-primary text-primary-foreground hover:bg-primary/90 shadow-xs",
destructive: destructive:
'bg-destructive hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60 text-white shadow-xs', "bg-destructive hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60 text-white shadow-xs",
outline: outline:
'bg-background hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 border shadow-xs', "bg-background hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 border shadow-xs",
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80 shadow-xs', secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80 shadow-xs",
ghost: 'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50', ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: 'text-primary underline-offset-4 hover:underline' link: "text-primary underline-offset-4 hover:underline",
}, },
size: { size: {
default: 'h-9 px-4 py-2 has-[>svg]:px-3', default: "h-9 px-4 py-2 has-[>svg]:px-3",
sm: 'h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5', sm: "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5",
lg: 'h-10 rounded-md px-6 has-[>svg]:px-4', lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
xl: 'h-14 rounded-md px-8 has-[>svg]:px-6', icon: "size-9",
icon: 'size-9', "icon-sm": "size-8",
'icon-sm': 'size-8', "icon-lg": "size-10",
'icon-lg': 'size-10' },
}
}, },
defaultVariants: { defaultVariants: {
variant: 'default', variant: "default",
size: 'default' size: "default",
} },
}); });
export type ButtonVariant = VariantProps<typeof buttonVariants>['variant']; export type ButtonVariant = VariantProps<typeof buttonVariants>["variant"];
export type ButtonSize = VariantProps<typeof buttonVariants>['size']; export type ButtonSize = VariantProps<typeof buttonVariants>["size"];
export type ButtonProps = WithElementRef<HTMLButtonAttributes> & export type ButtonProps = WithElementRef<HTMLButtonAttributes> &
WithElementRef<HTMLAnchorAttributes> & { WithElementRef<HTMLAnchorAttributes> & {
variant?: ButtonVariant; variant?: ButtonVariant;
size?: ButtonSize; size?: ButtonSize;
}; };
</script> </script>
<script lang="ts"> <script lang="ts">
let { let {
class: className, class: className,
variant = 'default', variant = "default",
size = 'default', size = "default",
ref = $bindable(null), ref = $bindable(null),
href = undefined, href = undefined,
type = 'button', type = "button",
disabled, disabled,
children, children,
...restProps ...restProps

View File

@ -23,6 +23,7 @@ export const genDescription = command(v.string(), async (data) => {
const description = await LLMDescribe( const description = await LLMDescribe(
`data:image/jpeg;base64,${outputBuffer.toString('base64')}` `data:image/jpeg;base64,${outputBuffer.toString('base64')}`
); );
console.log(description);
return description; return description;
}); });

View File

@ -75,6 +75,9 @@ export async function sendNewInquiryEmail(inquiryId: number) {
}; };
const replyToken = jwt.sign(tokenPayload, process.env.JWT_SECRET!); const replyToken = jwt.sign(tokenPayload, process.env.JWT_SECRET!);
console.log(item);
console.log(item.threads);
console.log(replyToken);
// Send mail with defined transport object // Send mail with defined transport object
await transporter.sendMail({ await transporter.sendMail({
from: `Westuffind Notifier <${process.env.EMAIL_USER}>`, from: `Westuffind Notifier <${process.env.EMAIL_USER}>`,
@ -87,6 +90,7 @@ export async function sendNewInquiryEmail(inquiryId: number) {
} }
export async function sendInquiryMessageEmail(inquiryId: number, sender: Sender) { export async function sendInquiryMessageEmail(inquiryId: number, sender: Sender) {
console.log(inquiryId);
const [item]: Item[] = await sql` const [item]: Item[] = await sql`
SELECT SELECT
i.*, i.*,

View File

@ -21,6 +21,9 @@ export async function LLMDescribe(imageData: string) {
temperature: 0.2 temperature: 0.2
}; };
console.log('AIing it');
console.log(payload);
const res = await fetch( const res = await fetch(
`http://${process.env.LLAMA_HOST!}:${process.env.LLAMA_PORT!}/v1/chat/completions`, `http://${process.env.LLAMA_HOST!}:${process.env.LLAMA_PORT!}/v1/chat/completions`,
{ {
@ -34,7 +37,7 @@ export async function LLMDescribe(imageData: string) {
if (!res.ok) { if (!res.ok) {
console.log(await res.text()); console.log(await res.text());
// process.exit(1); process.exit(1);
} }
const data = await res.json(); const data = await res.json();

View File

@ -21,28 +21,22 @@
This is the official place to reconnect items with their owners. This is the official place to reconnect items with their owners.
</p> </p>
<div class="mt-10 flex flex-col items-center justify-center gap-4 "> <div class="mt-10 flex flex-col items-center justify-center gap-4 sm:flex-row">
<a
href="/items"
class={cn(buttonVariants({ variant: 'default', size: 'xl' }), 'text-base')}
aria-label="Browse lost items"
>
Browse lost items
</a>
<div class="w-40 flex items-center gap-2 h-2">
<div class="flex-1 h-px bg-border"></div>
<span class="text-muted-foreground whitespace-nowrap">Or</span>
<div class="flex-1 h-px bg-border"></div>
</div>
<Button <Button
size="sm" size="lg"
variant="outline" class="px-8"
class="text-muted-foreground"
onclick={openCreateDialog} onclick={openCreateDialog}
aria-label="Submit a found item" aria-label="Submit a found item"
> >
Submit found item Submit a Found Item
</Button> </Button>
<a
href="/items"
class={cn(buttonVariants({ variant: 'outline', size: 'lg' }), 'px-8')}
aria-label="Browse lost items"
>
Browse Lost Items
</a>
</div> </div>
</div> </div>
@ -72,7 +66,6 @@
</p> </p>
<Button <Button
class="mt-2 w-full" class="mt-2 w-full"
variant="outline"
onclick={openCreateDialog} onclick={openCreateDialog}
aria-label="Submit a found item" aria-label="Submit a found item"
> >
@ -104,7 +97,7 @@
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle id="safe-heading">Safe &amp; Effective</CardTitle> <CardTitle id="safe-heading">Safe &amp; School-Run</CardTitle>
</CardHeader> </CardHeader>
<CardContent <CardContent
class="space-y-3 text-sm text-muted-foreground" class="space-y-3 text-sm text-muted-foreground"
@ -113,9 +106,6 @@
<p> <p>
Managed by Waukesha West staff. Items are reviewed before being listed. Managed by Waukesha West staff. Items are reviewed before being listed.
</p> </p>
<p>
We've returned 120+ items back to their owners!
</p>
</CardContent> </CardContent>
</Card> </Card>
</div> </div>

View File

@ -1,9 +1,6 @@
import type { PageServerLoad } from '$types'; import type { PageServerLoad } from '$types';
import sql from '$lib/db/db.server'; import sql from '$lib/db/db.server';
import type { User, UserSettings } from '$lib/types/user'; import type { User } from '$lib/types/user';
import { type Actions, error, fail } from '@sveltejs/kit';
import { getFormString, getRequiredFormString } from '$lib/shared';
import bcrypt from 'bcrypt';
export const load: PageServerLoad = async ({ locals }) => { export const load: PageServerLoad = async ({ locals }) => {
if (!locals || !locals.user) { if (!locals || !locals.user) {
@ -14,63 +11,7 @@ export const load: PageServerLoad = async ({ locals }) => {
SELECT * FROM users WHERE id = ${locals.user.id} SELECT * FROM users WHERE id = ${locals.user.id}
`; `;
console.log(userData);
return { userData }; return { userData };
}; };
export const actions: Actions = {
default: async ({ request, url, locals, params }) => {
if (!locals || !locals.user) {
throw error(403, 'Need to be logged in!');
}
const data = await request.formData();
let name: string;
let email: string;
let newPassword: string | null;
let retypeNewPassword: string | null;
let staleItemDays: number;
let notifyAllApprovedInquiries: boolean;
let notifyAllTurnedInInquiries: boolean;
try {
name = getRequiredFormString(data, 'name');
email = getRequiredFormString(data, 'email');
newPassword = getFormString(data, 'newPassword');
retypeNewPassword = getFormString(data, 'retypeNewPassword');
staleItemDays = parseInt(getRequiredFormString(data, 'staleItemDays'));
notifyAllApprovedInquiries = getFormString(data, 'notifyAllApprovedInquiries') == 'on';
notifyAllTurnedInInquiries = getFormString(data, 'notifyAllTurnedInInquiries') == 'on';
if (newPassword !== retypeNewPassword) {
fail(400, { password: 'New passwords dont match!' });
}
const passwordHash = newPassword ? await bcrypt.hash(newPassword, 12) : null;
const settings: UserSettings = {
staleItemDays,
notifyAllApprovedInquiries,
notifyAllTurnedInInquiries
};
return await sql`
UPDATE users SET name = ${name},
email = ${email},
${passwordHash ? sql`password_hash = ${passwordHash},` : sql``}
settings = ${settings.toString()}
WHERE id = ${locals.user.id}
RETURNING *;
`;
} catch (e) {
console.log('fails');
console.log(e);
return fail(400, {
message: e instanceof Error ? e.message : 'Unknown error occurred',
success: false
});
}
return { success: true };
}
} satisfies Actions;

View File

@ -1,4 +1,4 @@
<!-- src/routes/account/++page.svelte --> <!-- src/routes/account/+page.svelte -->
<script lang="ts"> <script lang="ts">
import { Card, CardContent, CardHeader, CardTitle } from '$lib/components/ui/card'; import { Card, CardContent, CardHeader, CardTitle } from '$lib/components/ui/card';
import { Badge } from '$lib/components/ui/badge'; import { Badge } from '$lib/components/ui/badge';
@ -9,44 +9,36 @@
import { Button } from '$lib/components/ui/button'; import { Button } from '$lib/components/ui/button';
import { DefaultUserSettings } from '$lib/types/user'; import { DefaultUserSettings } from '$lib/types/user';
import { enhance } from '$app/forms'; import { enhance } from '$app/forms';
import { toast } from 'svelte-sonner';
import { Toaster } from '$lib/components/ui/sonner';
import CheckIcon from '@lucide/svelte/icons/check';
let { data, form } = $props(); let { data } = $props();
function signOut() { function signOut() {
document.cookie = 'jwt=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;'; document.cookie = 'jwt=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;';
window.location.href = '/'; window.location.href = '/';
} }
// Use top-level variables for two-way binding. Binding to object properties let form = $derived({
// like `form.name` doesn't create proper reactive two-way bindings in Svelte. name: data.userData.name,
let name: string = $state(data.userData.name); email: data.userData.email,
let email: string = $state(data.userData.email); staleItemDays:
let staleItemDays: number = data.userData.settings?.staleItemDays ?? DefaultUserSettings.staleItemDays,
$state(data.userData.settings?.staleItemDays ?? DefaultUserSettings.staleItemDays); notifyAllApprovedInquiries:
let notifyAllApprovedInquiries: boolean = data.userData.settings?.notifyAllApprovedInquiries ?? false,
$state(data.userData.settings?.notifyAllApprovedInquiries ?? false); notifyAllTurnedInInquiries:
let notifyAllTurnedInInquiries: boolean = data.userData.settings?.notifyAllTurnedInInquiries ?? false
$state(data.userData.settings?.notifyAllTurnedInInquiries ?? false); });
const formatDate = (date: Date | string) => { const formatDate = (date: Date | string) => {
const d = new Date(date); const d = new Date(date);
return d.toLocaleString(); return d.toLocaleString();
}; };
</script> </script>
<svelte:head> <svelte:head>
<title>Account</title> <title>Account</title>
</svelte:head> </svelte:head>
<Toaster></Toaster>
<div class="container mx-auto max-w-3xl py-10"> <div class="container mx-auto max-w-3xl py-10">
<Card class="rounded-2xl shadow-sm"> <Card class="rounded-2xl shadow-sm">
<CardHeader> <CardHeader>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
@ -57,20 +49,7 @@
<CardContent class="space-y-6"> <CardContent class="space-y-6">
<!-- Editable Profile Form --> <!-- Editable Profile Form -->
<form method="POST" class="space-y-6" use:enhance={({ formElement, formData, action, cancel, submitter }) => { <form method="POST" use:enhance class="space-y-6">
return async ({ result, update }) => {
console.log("bleh")
console.log(result.status)
if (result.status === 200) {
toast('Saved', {
icon: CheckIcon,
description: 'Your account has been saved!',
});
}
};
}
}>
<div class="grid gap-4 sm:grid-cols-2"> <div class="grid gap-4 sm:grid-cols-2">
<div class="space-y-2"> <div class="space-y-2">
<Label for="username">Username</Label> <Label for="username">Username</Label>
@ -78,95 +57,64 @@
</div> </div>
<div class="space-y-2"> <div class="space-y-2">
<Label for="name">Full Name<span class="text-error">*</span></Label> <Label for="name">Full Name</Label>
<Input id="name" name="name" bind:value={name} /> <Input id="name" name="name" bind:value={form.name} />
</div> </div>
<div class="space-y-2 sm:col-span-2"> <div class="space-y-2 sm:col-span-2">
<Label for="email">Email<span class="text-error">*</span></Label> <Label for="email">Email</Label>
<Input id="email" name="email" type="email" bind:value={email} /> <Input id="email" name="email" type="email" bind:value={form.email} />
</div> </div>
<div class="space-y-2"> <div class="space-y-2">
<Label for="newPassword">New Password</Label> <Label for="staleItemDays">Stale Item Days</Label>
<Input id="newPassword" name="newPassword" /> <Input
id="staleItemDays"
name="staleItemDays"
type="number"
bind:value={form.staleItemDays}
min="0"
/>
</div> </div>
<div class="space-y-2">
<Label for="retypeNewPassword">Retype New Password</Label>
<Input id="retypeNewPassword" name="retypeNewPassword" />
</div>
</div> </div>
<Separator /> <Separator />
<div class="space-y-4"> <div class="space-y-4">
<h2 class="text-lg font-semibold">Settings</h2> <h2 class="text-lg font-semibold">Notifications</h2>
<div class="flex items-center justify-between">
<Label for="staleItemDays" class="text-base">
<div>
<p class="font-medium">Stale Item Days</p>
<p class="text-sm text-muted-foreground">
Number of days without activity before items show up as stale.
</p>
</div>
</Label>
<Input
class="w-min inline-block"
id="staleItemDays"
name="staleItemDays"
type="number"
bind:value={staleItemDays}
min="0"
/>
</div>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<Label for="notifyAllApprovedInquiries" class="text-base"> <div>
<div> <p class="font-medium">Notify All Approved</p>
<p class="text-sm text-muted-foreground">
<p class="font-medium">Notify for All Approved Items</p> Receive notifications when inquiries are approved.
<p class="text-sm text-muted-foreground"> </p>
Receive notifications for all approved items, not just yours. </div>
</p>
</div>
</Label>
<Switch <Switch
name="notifyAllApprovedInquiries" name="notifyAllApprovedInquiries"
id="notifyAllApprovedInquiries" bind:checked={form.notifyAllApprovedInquiries}
bind:checked={notifyAllApprovedInquiries}
/> />
</div> </div>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<Label for="notifyAllTurnedInInquiries" class="text-base"> <div>
<div> <p class="font-medium">Notify All Turned In</p>
<p class="text-sm text-muted-foreground">
<p class="font-medium">Notify for All Turned In Items</p> Receive notifications when inquiries are turned in.
<p class="text-sm text-muted-foreground"> </p>
Receive notifications for all items turned in to the school lost-and-found. </div>
</p>
</div>
</Label>
<Switch <Switch
name="notifyAllTurnedInInquiries" name="notifyAllTurnedInInquiries"
id="notifyAllTurnedInInquiries" bind:checked={form.notifyAllTurnedInInquiries}
bind:checked={notifyAllTurnedInInquiries}
/> />
</div> </div>
</div> </div>
<div class="flex items-center justify-between pt-4"> <div class="flex items-center justify-between pt-4">
<div class="text-sm text-muted-foreground"> <div class="text-sm text-muted-foreground">
<p> Member since {formatDate(data.userData.createdAt)} · Last sign in {formatDate(
data.userData.lastSignIn
Member since {formatDate(data.userData.createdAt)} )}
</p>
<p>
Last sign in {formatDate(data.userData.lastSignIn)}
</p>
</div> </div>
<div class="flex gap-2"> <div class="flex gap-2">
@ -179,4 +127,4 @@
</form> </form>
</CardContent> </CardContent>
</Card> </Card>
</div> </div>

View File

@ -86,7 +86,7 @@
{#each data.items as item (item.id)} {#each data.items as item (item.id)}
{#if item.approvedDate === null} {#if item.approvedDate === null}
<ItemListing item={item} user={data.user} editCallback={openEditDialog} <ItemListing item={item} admin={data.user !== null} editCallback={openEditDialog}
inquireCallback={openInquireDialog} claimCallback={openClaimDialog} /> inquireCallback={openInquireDialog} claimCallback={openClaimDialog} />
{/if} {/if}
{/each} {/each}
@ -102,7 +102,7 @@
{#each data.items as item (item.id)} {#each data.items as item (item.id)}
{#if item.approvedDate !== null && item.claimedDate === null} {#if item.approvedDate !== null && item.claimedDate === null}
<ItemListing item={item} user={data.user} editCallback={openEditDialog} <ItemListing item={item} admin={data.user !== null} editCallback={openEditDialog}
inquireCallback={openInquireDialog} claimCallback={openClaimDialog} /> inquireCallback={openInquireDialog} claimCallback={openClaimDialog} />
{/if} {/if}
{/each} {/each}
@ -115,7 +115,7 @@
</FieldSeparator> </FieldSeparator>
{#each data.items as item (item.id)} {#each data.items as item (item.id)}
{#if item.claimedDate !== null} {#if item.claimedDate !== null}
<ItemListing item={item} user={data.user} editCallback={openEditDialog} <ItemListing item={item} admin={data.user !== null} editCallback={openEditDialog}
inquireCallback={openInquireDialog} claimCallback={openClaimDialog} /> inquireCallback={openInquireDialog} claimCallback={openClaimDialog} />
{/if} {/if}
{/each} {/each}

View File

@ -47,6 +47,8 @@ export const load: PageServerLoad = async ({ url, locals, params }) => {
WHERE i.id = (SELECT item_id FROM inquiry_threads WHERE id = ${inquiryId}) WHERE i.id = (SELECT item_id FROM inquiry_threads WHERE id = ${inquiryId})
GROUP BY i.id;`; GROUP BY i.id;`;
console.log(item);
return { item }; return { item };
}; };

View File

@ -3,7 +3,7 @@ import { describe, expect, it } from 'vitest';
import { render } from 'vitest-browser-svelte'; import { render } from 'vitest-browser-svelte';
import Page from './items/+page.svelte'; import Page from './items/+page.svelte';
describe('/++page.svelte', () => { describe('/+page.svelte', () => {
it('should render h1', async () => { it('should render h1', async () => {
render(Page); render(Page);

View File

@ -1,21 +0,0 @@
<script lang="ts">
import { toast } from 'svelte-sonner';
import { Button } from '$lib/components/ui/button/index.js';
import { Toaster } from '$lib/components/ui/sonner';
</script>
<Button
variant="outline"
onclick={() =>
toast("Event has been created", {
description: "Sunday, December 03, 2023 at 9:00 AM",
action: {
label: "Undo",
onClick: () => console.info("Undo")
}
})}
>
Show Toast
</Button>
<Toaster></Toaster>