Compare commits
2 Commits
09cdce350d
...
3925f4e67a
| Author | SHA1 | Date | |
|---|---|---|---|
| 3925f4e67a | |||
| a0efc6c628 |
39
package-lock.json
generated
39
package-lock.json
generated
@ -24,7 +24,7 @@
|
||||
"@eslint/compat": "^1.4.0",
|
||||
"@eslint/js": "^9.36.0",
|
||||
"@internationalized/date": "^3.10.1",
|
||||
"@lucide/svelte": "^0.561.0",
|
||||
"@lucide/svelte": "^1.7.0",
|
||||
"@sveltejs/adapter-node": "^5.3.2",
|
||||
"@sveltejs/kit": "2.49.5",
|
||||
"@sveltejs/vite-plugin-svelte": "^6.2.0",
|
||||
@ -47,6 +47,7 @@
|
||||
"prettier-plugin-tailwindcss": "^0.6.14",
|
||||
"svelte": "^5.39.5",
|
||||
"svelte-check": "^4.3.2",
|
||||
"svelte-sonner": "^1.1.0",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"tailwind-variants": "^3.2.2",
|
||||
"tailwindcss": "^4.1.13",
|
||||
@ -1337,9 +1338,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@lucide/svelte": {
|
||||
"version": "0.561.0",
|
||||
"resolved": "https://registry.npmjs.org/@lucide/svelte/-/svelte-0.561.0.tgz",
|
||||
"integrity": "sha512-vofKV2UFVrKE6I4ewKJ3dfCXSV6iP6nWVmiM83MLjsU91EeJcEg7LoWUABLp/aOTxj1HQNbJD1f3g3L0JQgH9A==",
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@lucide/svelte/-/svelte-1.7.0.tgz",
|
||||
"integrity": "sha512-YytBKOUBGox7yWcykZnYxOkn5WpR5G1qYXLYXV/j1B79SOTTEKzB+s5yF5Rq9l9OkweDStNH2b4yTqfvhEhV8g==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"peerDependencies": {
|
||||
@ -6311,6 +6312,36 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"version": "0.10.6",
|
||||
"resolved": "https://registry.npmjs.org/svelte-toolbelt/-/svelte-toolbelt-0.10.6.tgz",
|
||||
|
||||
@ -19,7 +19,7 @@
|
||||
"@eslint/compat": "^1.4.0",
|
||||
"@eslint/js": "^9.36.0",
|
||||
"@internationalized/date": "^3.10.1",
|
||||
"@lucide/svelte": "^0.561.0",
|
||||
"@lucide/svelte": "^1.7.0",
|
||||
"@sveltejs/adapter-node": "^5.3.2",
|
||||
"@sveltejs/kit": "2.49.5",
|
||||
"@sveltejs/vite-plugin-svelte": "^6.2.0",
|
||||
@ -42,6 +42,7 @@
|
||||
"prettier-plugin-tailwindcss": "^0.6.14",
|
||||
"svelte": "^5.39.5",
|
||||
"svelte-check": "^4.3.2",
|
||||
"svelte-sonner": "^1.1.0",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"tailwind-variants": "^3.2.2",
|
||||
"tailwindcss": "^4.1.13",
|
||||
|
||||
61
src/app.css
61
src/app.css
@ -133,3 +133,64 @@
|
||||
}
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { User, UserPayload } from '$lib/types/user';
|
||||
import { DefaultUserSettings, type User, type UserPayload } from '$lib/types/user';
|
||||
import bcrypt from 'bcrypt';
|
||||
import sql from '$lib/db/db.server';
|
||||
import { type Cookies, error } from '@sveltejs/kit';
|
||||
@ -9,7 +9,10 @@ export function setJWTCookie(cookies: Cookies, user: User) {
|
||||
const payload = {
|
||||
id: user.id,
|
||||
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) {
|
||||
@ -65,8 +68,7 @@ export async function login(email: string, password: string): Promise<User> {
|
||||
if (await bcrypt.compare(password, user.passwordHash!)) {
|
||||
delete user.passwordHash;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
||||
sql`
|
||||
await sql`
|
||||
UPDATE users
|
||||
SET last_sign_in = NOW()
|
||||
WHERE id = ${user.id};
|
||||
|
||||
@ -30,7 +30,6 @@
|
||||
}
|
||||
|
||||
function handleFiles(files: FileList | null) {
|
||||
console.log('handleFiles');
|
||||
if (!files || files.length === 0) return;
|
||||
|
||||
const selected = files[0];
|
||||
|
||||
@ -16,19 +16,21 @@
|
||||
import { approveDenyItem, restoreClaimedItem } from '$lib/db/items.remote';
|
||||
import { invalidateAll } from '$app/navigation';
|
||||
import NoImagePlaceholder from './no-image-placeholder.svelte';
|
||||
import type { User } from '$lib/types/user';
|
||||
|
||||
export let item: Item = <Item>{};
|
||||
export let admin = false;
|
||||
export let user: User | null = null;
|
||||
// export let admin = false;
|
||||
export let editCallback: (item: Item) => void;
|
||||
export let inquireCallback: (item: Item) => void;
|
||||
export let claimCallback: (item: Item) => void;
|
||||
|
||||
let timeSincePosted: number | string = (new Date().getTime() - item.foundDate.getTime()) / 1000 / 60 / 60 / 24; // days
|
||||
if (timeSincePosted < 1) {
|
||||
timeSincePosted = '<1';
|
||||
} else {
|
||||
timeSincePosted = Math.round(timeSincePosted);
|
||||
}
|
||||
// if (timeSincePosted < 1) {
|
||||
// timeSincePosted = '<1';
|
||||
// } else {
|
||||
// timeSincePosted = Math.round(timeSincePosted);
|
||||
// }
|
||||
|
||||
</script>
|
||||
|
||||
@ -39,20 +41,14 @@
|
||||
alt="Lost item">
|
||||
{:else}
|
||||
<div class="min-h-48 w-full bg-accent flex flex-col justify-center">
|
||||
|
||||
<div class="justify-center flex ">
|
||||
|
||||
<NoImagePlaceholder className="" />
|
||||
</div>
|
||||
<p class="text-center mt-4">No image available</p>
|
||||
</div>
|
||||
{/if}
|
||||
<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>
|
||||
|
||||
{#if item.transferred}
|
||||
<Badge variant="secondary" class="inline-block">In Lost & Found</Badge>
|
||||
{:else}
|
||||
@ -62,8 +58,9 @@
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger
|
||||
>
|
||||
<Badge variant="outline" class="inline-block">{timeSincePosted}
|
||||
day{(timeSincePosted === 1 || timeSincePosted === '<1') ? '' : 's'} ago
|
||||
<Badge variant="outline"
|
||||
class="inline-block {user?.settings !== null && user?.settings !== undefined && timeSincePosted >= user.settings.staleItemDays ? 'text-warning' : ''}">{timeSincePosted < 1 ? "<1" : Math.round(timeSincePosted)}
|
||||
day{(timeSincePosted <= 1) ? '' : 's'} ago
|
||||
</Badge>
|
||||
</Tooltip.Trigger
|
||||
>
|
||||
@ -72,18 +69,28 @@
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</Tooltip.Provider>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="flex-1">{item.description}</div>
|
||||
{#if item.foundLocation}
|
||||
<div class="mt-2">
|
||||
<div>
|
||||
|
||||
<Tooltip.Provider>
|
||||
<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>
|
||||
{/if}
|
||||
|
||||
{#if admin}
|
||||
{#if user !== null}
|
||||
<div class="mt-2 justify-between flex">
|
||||
{#if item.approvedDate === null}
|
||||
|
||||
@ -104,7 +111,7 @@
|
||||
<Button variant="ghost" class="text-action"
|
||||
onclick={() => {editCallback(item)}}>
|
||||
<PencilIcon />
|
||||
Manage
|
||||
{!item.approvedDate ? 'Edit' : 'Manage'}
|
||||
</Button>
|
||||
{:else}
|
||||
<Button variant="ghost" class="text-destructive"
|
||||
|
||||
@ -1,38 +1,39 @@
|
||||
<script lang="ts" module>
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
import type { HTMLAnchorAttributes, HTMLButtonAttributes } from "svelte/elements";
|
||||
import { type VariantProps, tv } from "tailwind-variants";
|
||||
import { cn, type WithElementRef } from '$lib/utils.js';
|
||||
import type { HTMLAnchorAttributes, HTMLButtonAttributes } from 'svelte/elements';
|
||||
import { type VariantProps, tv } from 'tailwind-variants';
|
||||
|
||||
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: {
|
||||
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:
|
||||
"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:
|
||||
"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",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
'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',
|
||||
ghost: 'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50',
|
||||
link: 'text-primary underline-offset-4 hover:underline'
|
||||
},
|
||||
size: {
|
||||
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",
|
||||
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
||||
icon: "size-9",
|
||||
"icon-sm": "size-8",
|
||||
"icon-lg": "size-10",
|
||||
},
|
||||
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',
|
||||
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-sm': 'size-8',
|
||||
'icon-lg': 'size-10'
|
||||
}
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
variant: 'default',
|
||||
size: 'default'
|
||||
}
|
||||
});
|
||||
|
||||
export type ButtonVariant = VariantProps<typeof buttonVariants>["variant"];
|
||||
export type ButtonSize = VariantProps<typeof buttonVariants>["size"];
|
||||
export type ButtonVariant = VariantProps<typeof buttonVariants>['variant'];
|
||||
export type ButtonSize = VariantProps<typeof buttonVariants>['size'];
|
||||
|
||||
export type ButtonProps = WithElementRef<HTMLButtonAttributes> &
|
||||
WithElementRef<HTMLAnchorAttributes> & {
|
||||
@ -44,11 +45,11 @@
|
||||
<script lang="ts">
|
||||
let {
|
||||
class: className,
|
||||
variant = "default",
|
||||
size = "default",
|
||||
variant = 'default',
|
||||
size = 'default',
|
||||
ref = $bindable(null),
|
||||
href = undefined,
|
||||
type = "button",
|
||||
type = 'button',
|
||||
disabled,
|
||||
children,
|
||||
...restProps
|
||||
|
||||
@ -23,7 +23,6 @@ export const genDescription = command(v.string(), async (data) => {
|
||||
const description = await LLMDescribe(
|
||||
`data:image/jpeg;base64,${outputBuffer.toString('base64')}`
|
||||
);
|
||||
console.log(description);
|
||||
|
||||
return description;
|
||||
});
|
||||
|
||||
@ -75,9 +75,6 @@ export async function sendNewInquiryEmail(inquiryId: number) {
|
||||
};
|
||||
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
|
||||
await transporter.sendMail({
|
||||
from: `Westuffind Notifier <${process.env.EMAIL_USER}>`,
|
||||
@ -90,7 +87,6 @@ export async function sendNewInquiryEmail(inquiryId: number) {
|
||||
}
|
||||
|
||||
export async function sendInquiryMessageEmail(inquiryId: number, sender: Sender) {
|
||||
console.log(inquiryId);
|
||||
const [item]: Item[] = await sql`
|
||||
SELECT
|
||||
i.*,
|
||||
|
||||
@ -21,9 +21,6 @@ export async function LLMDescribe(imageData: string) {
|
||||
temperature: 0.2
|
||||
};
|
||||
|
||||
console.log('AIing it');
|
||||
console.log(payload);
|
||||
|
||||
const res = await fetch(
|
||||
`http://${process.env.LLAMA_HOST!}:${process.env.LLAMA_PORT!}/v1/chat/completions`,
|
||||
{
|
||||
@ -37,7 +34,7 @@ export async function LLMDescribe(imageData: string) {
|
||||
|
||||
if (!res.ok) {
|
||||
console.log(await res.text());
|
||||
process.exit(1);
|
||||
// process.exit(1);
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
@ -21,22 +21,28 @@
|
||||
This is the official place to reconnect items with their owners.
|
||||
</p>
|
||||
|
||||
<div class="mt-10 flex flex-col items-center justify-center gap-4 sm:flex-row">
|
||||
<div class="mt-10 flex flex-col items-center justify-center gap-4 ">
|
||||
<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
|
||||
size="lg"
|
||||
class="px-8"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
class="text-muted-foreground"
|
||||
onclick={openCreateDialog}
|
||||
aria-label="Submit a found item"
|
||||
>
|
||||
Submit a Found Item
|
||||
Submit found item
|
||||
</Button>
|
||||
<a
|
||||
href="/items"
|
||||
class={cn(buttonVariants({ variant: 'outline', size: 'lg' }), 'px-8')}
|
||||
aria-label="Browse lost items"
|
||||
>
|
||||
Browse Lost Items
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -66,6 +72,7 @@
|
||||
</p>
|
||||
<Button
|
||||
class="mt-2 w-full"
|
||||
variant="outline"
|
||||
onclick={openCreateDialog}
|
||||
aria-label="Submit a found item"
|
||||
>
|
||||
@ -97,7 +104,7 @@
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle id="safe-heading">Safe & School-Run</CardTitle>
|
||||
<CardTitle id="safe-heading">Safe & Effective</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent
|
||||
class="space-y-3 text-sm text-muted-foreground"
|
||||
@ -106,6 +113,9 @@
|
||||
<p>
|
||||
Managed by Waukesha West staff. Items are reviewed before being listed.
|
||||
</p>
|
||||
<p>
|
||||
We've returned 120+ items back to their owners!
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
import type { PageServerLoad } from '$types';
|
||||
import sql from '$lib/db/db.server';
|
||||
import type { User } from '$lib/types/user';
|
||||
import type { User, UserSettings } 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 }) => {
|
||||
if (!locals || !locals.user) {
|
||||
@ -11,7 +14,63 @@ export const load: PageServerLoad = async ({ locals }) => {
|
||||
SELECT * FROM users WHERE id = ${locals.user.id}
|
||||
`;
|
||||
|
||||
console.log(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;
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
<!-- src/routes/account/+page.svelte -->
|
||||
<!-- src/routes/account/++page.svelte -->
|
||||
<script lang="ts">
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '$lib/components/ui/card';
|
||||
import { Badge } from '$lib/components/ui/badge';
|
||||
@ -9,36 +9,44 @@
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { DefaultUserSettings } from '$lib/types/user';
|
||||
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 } = $props();
|
||||
let { data, form } = $props();
|
||||
|
||||
function signOut() {
|
||||
document.cookie = 'jwt=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;';
|
||||
window.location.href = '/';
|
||||
}
|
||||
|
||||
let form = $derived({
|
||||
name: data.userData.name,
|
||||
email: data.userData.email,
|
||||
staleItemDays:
|
||||
data.userData.settings?.staleItemDays ?? DefaultUserSettings.staleItemDays,
|
||||
notifyAllApprovedInquiries:
|
||||
data.userData.settings?.notifyAllApprovedInquiries ?? false,
|
||||
notifyAllTurnedInInquiries:
|
||||
data.userData.settings?.notifyAllTurnedInInquiries ?? false
|
||||
});
|
||||
// Use top-level variables for two-way binding. Binding to object properties
|
||||
// like `form.name` doesn't create proper reactive two-way bindings in Svelte.
|
||||
let name: string = $state(data.userData.name);
|
||||
let email: string = $state(data.userData.email);
|
||||
let staleItemDays: number =
|
||||
$state(data.userData.settings?.staleItemDays ?? DefaultUserSettings.staleItemDays);
|
||||
let notifyAllApprovedInquiries: boolean =
|
||||
$state(data.userData.settings?.notifyAllApprovedInquiries ?? false);
|
||||
let notifyAllTurnedInInquiries: boolean =
|
||||
$state(data.userData.settings?.notifyAllTurnedInInquiries ?? false);
|
||||
|
||||
const formatDate = (date: Date | string) => {
|
||||
const d = new Date(date);
|
||||
return d.toLocaleString();
|
||||
};
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Account</title>
|
||||
</svelte:head>
|
||||
|
||||
<Toaster></Toaster>
|
||||
|
||||
<div class="container mx-auto max-w-3xl py-10">
|
||||
|
||||
<Card class="rounded-2xl shadow-sm">
|
||||
<CardHeader>
|
||||
<div class="flex items-center justify-between">
|
||||
@ -49,7 +57,20 @@
|
||||
|
||||
<CardContent class="space-y-6">
|
||||
<!-- Editable Profile Form -->
|
||||
<form method="POST" use:enhance class="space-y-6">
|
||||
<form method="POST" class="space-y-6" use:enhance={({ formElement, formData, action, cancel, submitter }) => {
|
||||
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="space-y-2">
|
||||
<Label for="username">Username</Label>
|
||||
@ -57,64 +78,95 @@
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="name">Full Name</Label>
|
||||
<Input id="name" name="name" bind:value={form.name} />
|
||||
<Label for="name">Full Name<span class="text-error">*</span></Label>
|
||||
<Input id="name" name="name" bind:value={name} />
|
||||
</div>
|
||||
|
||||
<div class="space-y-2 sm:col-span-2">
|
||||
<Label for="email">Email</Label>
|
||||
<Input id="email" name="email" type="email" bind:value={form.email} />
|
||||
<Label for="email">Email<span class="text-error">*</span></Label>
|
||||
<Input id="email" name="email" type="email" bind:value={email} />
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="staleItemDays">Stale Item Days</Label>
|
||||
<Input
|
||||
id="staleItemDays"
|
||||
name="staleItemDays"
|
||||
type="number"
|
||||
bind:value={form.staleItemDays}
|
||||
min="0"
|
||||
/>
|
||||
<Label for="newPassword">New Password</Label>
|
||||
<Input id="newPassword" name="newPassword" />
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="retypeNewPassword">Retype New Password</Label>
|
||||
<Input id="retypeNewPassword" name="retypeNewPassword" />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div class="space-y-4">
|
||||
<h2 class="text-lg font-semibold">Notifications</h2>
|
||||
|
||||
<h2 class="text-lg font-semibold">Settings</h2>
|
||||
<div class="flex items-center justify-between">
|
||||
<Label for="staleItemDays" class="text-base">
|
||||
<div>
|
||||
<p class="font-medium">Notify All Approved</p>
|
||||
<p class="font-medium">Stale Item Days</p>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
Receive notifications when inquiries are approved.
|
||||
Number of days without activity before items show up as stale.
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
name="notifyAllApprovedInquiries"
|
||||
bind:checked={form.notifyAllApprovedInquiries}
|
||||
</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">
|
||||
<Label for="notifyAllApprovedInquiries" class="text-base">
|
||||
<div>
|
||||
<p class="font-medium">Notify All Turned In</p>
|
||||
|
||||
<p class="font-medium">Notify for All Approved Items</p>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
Receive notifications when inquiries are turned in.
|
||||
Receive notifications for all approved items, not just yours.
|
||||
</p>
|
||||
</div>
|
||||
</Label>
|
||||
<Switch
|
||||
name="notifyAllApprovedInquiries"
|
||||
id="notifyAllApprovedInquiries"
|
||||
bind:checked={notifyAllApprovedInquiries}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<Label for="notifyAllTurnedInInquiries" class="text-base">
|
||||
<div>
|
||||
|
||||
<p class="font-medium">Notify for All Turned In Items</p>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
Receive notifications for all items turned in to the school lost-and-found.
|
||||
</p>
|
||||
</div>
|
||||
</Label>
|
||||
<Switch
|
||||
name="notifyAllTurnedInInquiries"
|
||||
bind:checked={form.notifyAllTurnedInInquiries}
|
||||
id="notifyAllTurnedInInquiries"
|
||||
bind:checked={notifyAllTurnedInInquiries}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between pt-4">
|
||||
<div class="text-sm text-muted-foreground">
|
||||
Member since {formatDate(data.userData.createdAt)} · Last sign in {formatDate(
|
||||
data.userData.lastSignIn
|
||||
)}
|
||||
<p>
|
||||
|
||||
Member since {formatDate(data.userData.createdAt)}
|
||||
</p>
|
||||
<p>
|
||||
|
||||
Last sign in {formatDate(data.userData.lastSignIn)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2">
|
||||
|
||||
@ -86,7 +86,7 @@
|
||||
|
||||
{#each data.items as item (item.id)}
|
||||
{#if item.approvedDate === null}
|
||||
<ItemListing item={item} admin={data.user !== null} editCallback={openEditDialog}
|
||||
<ItemListing item={item} user={data.user} editCallback={openEditDialog}
|
||||
inquireCallback={openInquireDialog} claimCallback={openClaimDialog} />
|
||||
{/if}
|
||||
{/each}
|
||||
@ -102,7 +102,7 @@
|
||||
|
||||
{#each data.items as item (item.id)}
|
||||
{#if item.approvedDate !== null && item.claimedDate === null}
|
||||
<ItemListing item={item} admin={data.user !== null} editCallback={openEditDialog}
|
||||
<ItemListing item={item} user={data.user} editCallback={openEditDialog}
|
||||
inquireCallback={openInquireDialog} claimCallback={openClaimDialog} />
|
||||
{/if}
|
||||
{/each}
|
||||
@ -115,7 +115,7 @@
|
||||
</FieldSeparator>
|
||||
{#each data.items as item (item.id)}
|
||||
{#if item.claimedDate !== null}
|
||||
<ItemListing item={item} admin={data.user !== null} editCallback={openEditDialog}
|
||||
<ItemListing item={item} user={data.user} editCallback={openEditDialog}
|
||||
inquireCallback={openInquireDialog} claimCallback={openClaimDialog} />
|
||||
{/if}
|
||||
{/each}
|
||||
|
||||
@ -47,8 +47,6 @@ export const load: PageServerLoad = async ({ url, locals, params }) => {
|
||||
WHERE i.id = (SELECT item_id FROM inquiry_threads WHERE id = ${inquiryId})
|
||||
GROUP BY i.id;`;
|
||||
|
||||
console.log(item);
|
||||
|
||||
return { item };
|
||||
};
|
||||
|
||||
|
||||
@ -3,7 +3,7 @@ import { describe, expect, it } from 'vitest';
|
||||
import { render } from 'vitest-browser-svelte';
|
||||
import Page from './items/+page.svelte';
|
||||
|
||||
describe('/+page.svelte', () => {
|
||||
describe('/++page.svelte', () => {
|
||||
it('should render h1', async () => {
|
||||
render(Page);
|
||||
|
||||
|
||||
21
src/routes/test/+page.svelte
Normal file
21
src/routes/test/+page.svelte
Normal file
@ -0,0 +1,21 @@
|
||||
<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>
|
||||
Loading…
Reference in New Issue
Block a user