pages
All checks were successful
ci / docker_image (push) Successful in 2m57s
ci / deploy (push) Successful in 28s

This commit is contained in:
Drake Marino 2026-02-04 18:49:35 -06:00
parent 8ea632a14f
commit 5cd3af719d
9 changed files with 433 additions and 124 deletions

View File

@ -12,7 +12,7 @@
--card-foreground: oklch(0.141 0.005 285.823); --card-foreground: oklch(0.141 0.005 285.823);
--popover: oklch(1 0 0); --popover: oklch(1 0 0);
--popover-foreground: oklch(0.141 0.005 285.823); --popover-foreground: oklch(0.141 0.005 285.823);
--primary: oklch(0.606 0.25 292.717); --primary: oklch(0.5852 0.25 292.717);
--primary-foreground: oklch(0.969 0.016 293.756); --primary-foreground: oklch(0.969 0.016 293.756);
--secondary: oklch(0.967 0.001 286.375); --secondary: oklch(0.967 0.001 286.375);
--secondary-foreground: oklch(0.21 0.006 285.885); --secondary-foreground: oklch(0.21 0.006 285.885);
@ -40,6 +40,7 @@
--warning: oklch(0.84 0.16 84); --warning: oklch(0.84 0.16 84);
--error: oklch(0.577 0.245 27.325); --error: oklch(0.577 0.245 27.325);
--positive: oklch(0.5 0.2067 147.18); --positive: oklch(0.5 0.2067 147.18);
--edit: oklch(0.5852 0.2263 260.47);
} }
.dark { .dark {
@ -77,6 +78,7 @@
--warning: oklch(0.84 0.16 84); --warning: oklch(0.84 0.16 84);
--error: oklch(0.704 0.191 22.216); --error: oklch(0.704 0.191 22.216);
--positive: oklch(0.7522 0.2067 147.18); --positive: oklch(0.7522 0.2067 147.18);
--edit: oklch(0.6098 0.1872 260.47);
} }
@theme inline { @theme inline {
@ -118,6 +120,7 @@
--color-warning: var(--warning); --color-warning: var(--warning);
--color-error: var(--error); --color-error: var(--error);
--color-positive: var(--positive); --color-positive: var(--positive);
--color-edit: var(--edit);
} }
@layer base { @layer base {

View File

@ -5,11 +5,13 @@
import LocationIcon from '@lucide/svelte/icons/map-pinned'; import LocationIcon from '@lucide/svelte/icons/map-pinned';
import CheckIcon from '@lucide/svelte/icons/check'; import CheckIcon from '@lucide/svelte/icons/check';
import XIcon from '@lucide/svelte/icons/x'; import XIcon from '@lucide/svelte/icons/x';
import PencilIcon from '@lucide/svelte/icons/pencil';
import { Button } from '$lib/components/ui/button'; import { Button } from '$lib/components/ui/button';
import * as Tooltip from '$lib/components/ui/tooltip'; import * as Tooltip from '$lib/components/ui/tooltip';
import { dateFormatOptions } from '$lib/shared'; import { dateFormatOptions } from '$lib/shared';
import { approveDenyItem } from '$lib/db/items.remote'; import { approveDenyItem } from '$lib/db/items.remote';
import { invalidateAll } from '$app/navigation'; import { invalidateAll } from '$app/navigation';
import NoImagePlaceholder from './no-image-placeholder.svelte';
export let item: Item = <Item>{ export let item: Item = <Item>{
id: 2, id: 2,
@ -18,7 +20,8 @@
approvedDate: new Date(), approvedDate: new Date(),
description: 'A matte black water bottle with a black lid and a "BKLYN BENTO" logo on the side.', description: 'A matte black water bottle with a black lid and a "BKLYN BENTO" logo on the side.',
transferred: true, transferred: true,
foundLocation: 'By the tennis courts.' foundLocation: 'By the tennis courts.',
image: true
}; };
export let admin = false; export let admin = false;
@ -28,11 +31,24 @@
} else { } else {
timeSincePosted = Math.round(timeSincePosted); timeSincePosted = Math.round(timeSincePosted);
} }
</script> </script>
<div <div
class="h-full bg-card text-card-foreground flex flex-col gap-2 rounded-xl border shadow-sm max-w-sm overflow-hidden min-2-3xs"> class="h-full bg-card text-card-foreground flex flex-col gap-2 rounded-xl border shadow-sm max-w-sm overflow-hidden min-2-3xs">
<img src="https://fbla26.marinodev.com/uploads/{item.id}.jpg" alt="" class="object-cover max-h-48"> {#if item.image}
<img src="https://fbla26.marinodev.com/uploads/{item.id}.jpg" class="object-cover min-h-48 max-h-48"
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="flex-col flex h-full px-2 pb-2">
<!-- <div class="font-bold inline-block">{item.title}</div>--> <!-- <div class="font-bold inline-block">{item.title}</div>-->
@ -69,22 +85,32 @@
</div> </div>
{/if} {/if}
{#if admin && item.approvedDate === null} {#if admin}
<div class="mt-2 justify-between flex"> <div class="mt-2 justify-between flex">
<Button variant="ghost" class="text-positive" {#if item.approvedDate === null}
onclick={async () => {await approveDenyItem({id: item.id, approved: true});
<Button variant="ghost" class="text-positive"
onclick={async () => {await approveDenyItem({id: item.id, approved: true});
invalidateAll()}}> invalidateAll()}}>
<CheckIcon /> <CheckIcon />
Approve Approve
</Button> </Button>
<Button variant="ghost" class="text-destructive" <Button variant="ghost" class="text-destructive"
onclick={async () => {await approveDenyItem({id: item.id, approved: false}); onclick={async () => {await approveDenyItem({id: item.id, approved: false});
invalidateAll()}}> invalidateAll()}}>
<XIcon /> <XIcon />
Deny Deny
</Button>
{/if}
<Button variant="ghost" class="text-edit"
onclick={async () => {}}>
<PencilIcon />
Edit
</Button> </Button>
</div> </div>
{/if} {/if}
</div> </div>
</div> </div>

View File

@ -0,0 +1,122 @@
<script>
export let className = '';
</script>
<svg
version="1.1"
id="svg2"
viewBox="0 0 99.999999 99.999999"
height="100"
width="100"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/"
class={className}>
<defs
id="defs4">
<linearGradient
id="linearGradient967">
<stop
id="stop963"
offset="0"
style="stop-color:#c2c2c2;stop-opacity:1" />
<stop
id="stop965"
offset="1"
style="stop-color:#9f9f9f;stop-opacity:1" />
</linearGradient>
<linearGradient
gradientTransform="translate(-45.254833,0.35355338)"
gradientUnits="userSpaceOnUse"
y2="108.77648"
x2="658.45801"
y1="6.5995569"
x1="660.06653"
id="linearGradient969"
xlink:href="#linearGradient967" />
</defs>
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<g
transform="translate(-399.13437,-122.79051)"
id="layer1">
<g
transform="matrix(0.60784825,0,0,0.67101051,134.74354,126.08684)"
id="g1015">
<rect
style="fill:#9f9f9f;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2.2995;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect1002"
width="82.489967"
height="90.470001"
x="398.75827"
y="178.74706"
ry="8.3970251"
transform="rotate(-16.342822)" />
<g
id="g1000"
transform="rotate(16.320529,538.13563,-184.89727)">
<rect
ry="4.5961938"
y="1.6498091"
x="547.18585"
height="115.96551"
width="107.83378"
id="rect961"
style="fill:url(#linearGradient969);fill-opacity:1;fill-rule:nonzero;stroke:#ffffff;stroke-width:5.398;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none" />
<g
style="stroke:#ffffff;stroke-width:13.0708;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.17265471,0,0,0.17265471,512.49324,-6.3296456)"
id="g875">
<rect
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#cccccc;fill-opacity:1;fill-rule:evenodd;stroke:#ffffff;stroke-width:13.0708;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
id="rect3338"
width="491.10556"
height="449.99814"
x="270"
y="107.36227" />
<rect
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#ffffff;stroke-width:13.0708;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
id="rect3342"
width="491.10559"
height="209.99976"
x="270"
y="107.36227" />
<path
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#cccccc;fill-opacity:1;fill-rule:evenodd;stroke:#ffffff;stroke-width:13.0708;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;paint-order:stroke;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
d="m 300,317.36255 38.46147,-53.53818 60.53097,-45.16084 15.88277,18.57394 13.61285,-38.68356 8.20133,-2.98188 13.3106,-28.2093 180,179.99979"
id="path3344" />
<path
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#b3b3b3;fill-opacity:1;fill-rule:evenodd;stroke:#ffffff;stroke-width:13.0708;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;paint-order:stroke;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
d="m 180,60 c 4.09311,16.474688 7.71219,33.067277 10.85156,49.75 2.38256,12.66097 4.48857,25.37408 6.31641,38.12695 l -22.06445,-7.16015 -46.11133,-29.41602 5.32422,46.42578 -1.61524,24.78711 10.05274,30.37695 73.18554,-11.75585 L 300,180 252.19922,102.56641 242.5,117.5 215.375,95.375 Z"
transform="translate(270,107.36227)"
id="path3390-0" />
<path
id="path3358"
d="m 419.99999,347.36252 81.89918,-74.42959 18.50574,-9.68009 23.6512,-44.18894 25.94388,-21.70121 179.99999,179.99979"
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#cccccc;fill-opacity:1;fill-rule:evenodd;stroke:#ffffff;stroke-width:13.0708;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;paint-order:stroke;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
<path
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#b3b3b3;fill-opacity:1;fill-rule:evenodd;stroke:#ffffff;stroke-width:13.0708;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;paint-order:stroke;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
d="m 569.99999,197.36269 35.9388,80.91289 v 30.11038 30.11038 l 22.45864,19.46652 c 6.52453,-6.45031 14.14893,-11.78526 22.44431,-15.70477 14.8245,-7.00447 31.33823,-9.35959 47.17057,-13.6217 6.42776,-1.73037 12.90672,-3.85419 18.21343,-7.87277 1.35174,-1.02362 2.61592,-2.16281 3.77424,-3.40107 h -30 l -40.52149,-40.55006 -29.85645,-48.91972 -10.25307,8.83886 z"
id="path3386" />
<path
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#ffffff;stroke-width:13.0708;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;paint-order:stroke;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
d="m 419.99999,557.36227 c -0.41699,-9.60089 -8.81759,-17.60878 17.1252,-30.66806 31.8318,-16.02389 125.895,-35.88836 152.1537,-59.98434 19.42709,-17.82687 -70.4154,-37.66945 -55.0191,-59.07323 6.981,-9.70528 59.037,-19.96947 82.1463,-30.27386 21.90569,-9.76799 15.14129,-19.80328 31.4046,-29.97507 15.7092,-9.82558 68.3499,-19.77358 72.18929,-30.02516 -10.41359,10.52188 -68.83379,20.40327 -89.99999,30.00026 -22.3377,10.128 -21.4689,19.93018 -49.4313,29.48367 -30.1245,10.29239 -89.142,20.55268 -102.7077,30.51626 -28.4133,20.86858 46.863,42.59995 16.2024,59.99993 C 452.54309,490.92554 344.7219,510.65712 300,527.3626 c -30.9039,11.54369 -28.4079,17.74799 -30,29.99967"
id="path3370" />
</g>
</g>
</g>
</g>
</svg>

View File

@ -0,0 +1,117 @@
<script lang="ts">
import * as Dialog from '$lib/components/ui/dialog';
import { Button, buttonVariants } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import * as RadioGroup from '$lib/components/ui/radio-group';
import { Label } from '$lib/components/ui/label';
import * as Field from '$lib/components/ui/field';
import ImageUpload from '$lib/components/custom/image-upload/image-upload.svelte';
import { genDescription } from '$lib/db/items.remote';
import { EMAIL_REGEX_STRING } from '$lib/consts';
let itemLocation: string | undefined = $state('');
let foundLocation: string | undefined = $state();
let description: string | undefined = $state();
let isGenerating = $state(false);
async function onSelect() {
isGenerating = true;
description = await genDescription();
isGenerating = false;
}
let { open = $bindable() }: { open: boolean } = $props();
</script>
<Dialog.Root bind:open>
<Dialog.Content>
<Dialog.Header>
<Dialog.Title>Submit Found Item</Dialog.Title>
<Dialog.Description>
Your item will need to be approved before becoming public.
</Dialog.Description>
</Dialog.Header>
<form method="post" action="?/create" enctype="multipart/form-data">
<Field.Group>
<ImageUpload onSelect={onSelect} required />
<Field.Field>
<Field.Label for="description">
Description <span class="text-error">*</span>
</Field.Label>
<Input
id="description"
name="description"
bind:value={description}
placeholder="A red leather book bag..."
maxlength={200}
required
/>
</Field.Field>
<Field.Field>
<Field.Label for="foundLocation">
Where did you find it?
</Field.Label>
<Input
id="foundLocation"
name="foundLocation"
bind:value={foundLocation}
placeholder="By the tennis courts."
required
/>
</Field.Field>
<RadioGroup.Root name="location" bind:value={itemLocation}>
<div class="flex items-center space-x-2">
<RadioGroup.Item value="finderPossession" id="finderPossession" />
<Label for="finderPossession">
I still have the item.
</Label>
</div>
<div class="flex items-center space-x-2">
<RadioGroup.Item value="turnedIn" id="turnedIn" />
<Label for="turnedIn">
I turned the item in to the school lost and found.
</Label>
</div>
</RadioGroup.Root>
<Field.Field
class={itemLocation !== "finderPossession" ? "hidden pointer-events-none opacity-50" : ""}
>
<Field.Label for="email">
Your Email
</Field.Label>
<Input
id="email"
name="email"
placeholder="name@domain.com"
pattern={EMAIL_REGEX_STRING}
required={itemLocation === "finderPossession"}
/>
</Field.Field>
</Field.Group>
<Dialog.Footer class="mt-4">
<Dialog.Close
type="button"
class={buttonVariants({ variant: "outline" })}
>
Cancel
</Dialog.Close>
<Button type="submit">Submit</Button>
</Dialog.Footer>
</form>
</Dialog.Content>
</Dialog.Root>
{#if isGenerating}
<div class="fixed inset-0 bg-black/75 z-999999 w-screen h-screen justify-center items-center flex">
<p class="text-6xl text-primary">Loading...</p>
</div>
{/if}

View File

@ -10,4 +10,6 @@ export interface Item {
transferred: boolean; // to L&F location transferred: boolean; // to L&F location
keywords?: string[]; keywords?: string[];
foundLocation: string; foundLocation: string;
deleted: boolean;
image: boolean;
} }

View File

@ -22,7 +22,11 @@
<span class="hidden sm:block">MarinoDev Lost & Found</span> <span class="hidden sm:block">MarinoDev Lost & Found</span>
</a> </a>
<div class="items-center flex gap-4"> <div class="items-center flex gap-4">
<a href="/login" class="{buttonVariants({ variant: 'outline' })}">Admin Log in</a> <div class="inline-block">
<a href="/account" class={buttonVariants({variant: 'outline'})}>
Account
</a>
</div>
<div class="inline-block"> <div class="inline-block">
<Button onclick={toggleMode} variant="outline" size="icon"> <Button onclick={toggleMode} variant="outline" size="icon">
<SunIcon <SunIcon

View File

@ -0,0 +1,96 @@
<script lang="ts">
import { Button, buttonVariants } from '$lib/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '$lib/components/ui/card';
import SubmitItemDialog from '$lib/components/custom/submit-item-dialog.svelte';
import { cn } from '$lib/utils';
let createDialogOpen: boolean = $state(false);
function openCreateDialog() {
createDialogOpen = true;
}
</script>
<section class="relative overflow-hidden">
<!-- Hero -->
<div class="mx-auto max-w-6xl px-6 py-24 text-center">
<h1 class="text-4xl font-bold tracking-tight sm:text-5xl">
Waukesha West Lost &amp; Found
</h1>
<p class="mx-auto mt-6 max-w-2xl text-lg text-muted-foreground">
Lost something at school? Found something that isnt yours?
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">
<Button size="lg" class="px-8" onclick={openCreateDialog}>
Submit a Found Item
</Button>
<a href="/items" class={cn(buttonVariants({ variant: 'outline', size: 'lg' }), 'px-8')}>
Browse Lost Items
</a>
</div>
</div>
<!-- Subtle background accent -->
<div
aria-hidden="true"
class="absolute inset-x-0 -top-40 -z-10 transform-gpu overflow-hidden blur-3xl"
>
<div
class="relative left-[calc(50%-11rem)] aspect-[1155/678] w-[36rem] -translate-x-1/2 rotate-[30deg] bg-gradient-to-tr from-primary to-primary/40 opacity-20 sm:left-[calc(50%-30rem)] sm:w-[72rem]"
></div>
</div>
</section>
<section class="mx-auto max-w-6xl px-6 py-20">
<div class="grid gap-8 md:grid-cols-3">
<Card>
<CardHeader>
<CardTitle>Found an Item?</CardTitle>
</CardHeader>
<CardContent class="space-y-3 text-sm text-muted-foreground">
<p>
Turn it in digitally in less than a minute. Add a description and where you found it.
</p>
<Button class="mt-2 w-full" onclick={openCreateDialog}>Submit Item</Button>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Lost Something?</CardTitle>
</CardHeader>
<CardContent class="space-y-3 text-sm text-muted-foreground">
<p>
Browse items that have been turned in by students and staff.
</p>
<a href="/items" class={cn(buttonVariants({ variant: 'outline', size: 'lg' }), 'mt-2 w-full')}>Browse Items</a>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Safe &amp; School-Run</CardTitle>
</CardHeader>
<CardContent class="space-y-3 text-sm text-muted-foreground">
<p>
Managed by Waukesha West staff. Items are reviewed before being listed.
</p>
</CardContent>
</Card>
</div>
</section>
<section class="mx-auto max-w-6xl px-6 pb-24">
<div class="flex items-center justify-center">
<p class="text-xs text-muted-foreground">
Staff only:
<a href="/login" class="ml-1 underline underline-offset-4 hover:text-foreground">
Admin sign in
</a>
</p>
</div>
</section>
<SubmitItemDialog bind:open={createDialogOpen} />

View File

@ -30,11 +30,11 @@ export const actions: Actions = {
location = getFormString(data, 'location'); location = getFormString(data, 'location');
const file = data.get('image'); const file = data.get('image');
if (file === null) // if (file === null)
return fail(400, { // return fail(400, {
message: `Missing required field image.`, // message: `Missing required field image.`,
success: false // success: false
}); // });
if (!email && location !== 'turnedIn') { if (!email && location !== 'turnedIn') {
fail(400, { fail(400, {
@ -43,27 +43,28 @@ export const actions: Actions = {
}); });
} }
if (!(file instanceof File)) { let outputBuffer: Buffer | undefined;
return fail(400, { message: 'No file uploaded or file is invalid', success: false }); if (file) {
} if (!(file instanceof File)) {
return fail(400, { message: 'No file uploaded or file is invalid', success: false });
}
// Convert File → Buffer // Convert File → Buffer
const inputBuffer = Buffer.from(await file.arrayBuffer()); const inputBuffer = Buffer.from(await file.arrayBuffer());
// Detect format (Sharp does this internally) // Detect format (Sharp does this internally)
const image = sharp(inputBuffer); const image = sharp(inputBuffer);
const metadata = await image.metadata(); const metadata = await image.metadata();
let outputBuffer: Buffer; if (metadata.format === 'jpeg') {
// Already JPG → keep as-is
if (metadata.format === 'jpeg') { outputBuffer = inputBuffer;
// Already JPG → keep as-is } else {
outputBuffer = inputBuffer; // Convert to JPG
} else { outputBuffer = await image
// Convert to JPG .jpeg({ quality: 90 }) // adjust if needed
outputBuffer = await image .toBuffer();
.jpeg({ quality: 90 }) // adjust if needed }
.toBuffer();
} }
const response = await sql` const response = await sql`
@ -71,7 +72,8 @@ export const actions: Actions = {
emails, emails,
description, description,
transferred, transferred,
found_location found_location,
image
) VALUES ( ) VALUES (
${ ${
location === 'turnedIn' location === 'turnedIn'
@ -89,19 +91,22 @@ export const actions: Actions = {
}, },
${description}, ${description},
${location === 'turnedIn'}, ${location === 'turnedIn'},
${foundLocation} ${foundLocation},
${file instanceof File}
) )
RETURNING id; RETURNING id;
`; `;
try { if (outputBuffer) {
// It's a good idea to validate the filename to prevent path traversal attacks try {
const savePath = path.join('uploads', `${response[0]['id']}.jpg`); // It's a good idea to validate the filename to prevent path traversal attacks
const savePath = path.join('uploads', `${response[0]['id']}.jpg`);
writeFileSync(savePath, outputBuffer); writeFileSync(savePath, outputBuffer);
} catch (err) { } catch (err) {
console.error('File upload failed:', err); console.error('File upload failed:', err);
return fail(500, { message: 'Internal server error during file upload', success: false }); return fail(500, { message: 'Internal server error during file upload', success: false });
}
} }
} catch (e) { } catch (e) {
return fail(400, { return fail(400, {

View File

@ -11,88 +11,26 @@
import { Label } from '$lib/components/ui/label'; import { Label } from '$lib/components/ui/label';
import ItemListing from '$lib/components/custom/item-listing.svelte'; import ItemListing from '$lib/components/custom/item-listing.svelte';
import { FieldSeparator } from '$lib/components/ui/field'; import { FieldSeparator } from '$lib/components/ui/field';
import SubmitItemDialog from '$lib/components/custom/submit-item-dialog.svelte';
// import { type Item } from '$lib/types/item'; // import { type Item } from '$lib/types/item';
let itemLocation: string | undefined = $state(''); let createDialogOpen: boolean = $state(false);
let foundLocation: string | undefined = $state();
let description: string | undefined = $state();
let isGenerating = $state(false);
async function onSelect() { function openCreateDialog() {
isGenerating = true; createDialogOpen = true;
description = await genDescription();
isGenerating = false;
} }
let { data }: PageProps = $props(); let { data }: PageProps = $props();
</script> </script>
<div class="max-w-7xl mx-auto px-4"> <div class="max-w-7xl mx-auto px-4">
<div class="justify-between flex"> <div class="justify-between flex">
<h1 class="font-semibold text-4xl mb-4 mt-2">Found Items</h1> <h1 class="text-3xl mb-2 mt-2">Found Items</h1>
<div class="inline-block"> <div class="inline-block">
<Dialog.Root> <Button variant="default" class="" onclick={openCreateDialog}>
<Dialog.Trigger class={buttonVariants({ variant: 'default' })} type="button" Submit a Found Item
>Post an item </Button>
</Dialog.Trigger
>
<Dialog.Content>
<Dialog.Header>
<Dialog.Title>Submit Found Item</Dialog.Title>
<Dialog.Description>
Your item will need to be approved before becoming public.
</Dialog.Description>
</Dialog.Header>
<form method="post" action="?/create" enctype="multipart/form-data">
<Field.Group>
<ImageUpload onSelect={onSelect} required={true} />
<Field.Field>
<Field.Label for="description">
Description<span class="text-error">*</span>
</Field.Label>
<Input id="description" name="description" bind:value={description}
placeholder="A red leather book bag..." maxlength={200}
required />
</Field.Field>
<Field.Field>
<Field.Label for="foundLocation">
Where did you find it?
</Field.Label>
<Input id="foundLocation" name="foundLocation" bind:value={foundLocation}
placeholder="By the tennis courts." required />
</Field.Field>
<RadioGroup.Root name="location" bind:value={itemLocation}>
<div class="flex items-center space-x-2">
<RadioGroup.Item value="finderPossession" id="finderPossession" />
<Label for="finderPossession">I still have the item.</Label>
</div>
<div class="flex items-center space-x-2">
<RadioGroup.Item value="turnedIn" id="turnedIn" />
<Label for="turnedIn">I turned the item in to the school lost and found.</Label>
</div>
</RadioGroup.Root>
<Field.Field class={itemLocation !== 'finderPossession' ? 'disabled hidden' : ''}>
<Field.Label for="email">
Your Email
</Field.Label>
<Input id="email" name="email" placeholder="name@domain.com"
class={itemLocation !== 'finderPossession' ? 'disabled' : ''} pattern={EMAIL_REGEX_STRING}
required={itemLocation === 'finderPossesion'} />
<!-- <Field.Error>Enter a valid email address.</Field.Error>-->
</Field.Field>
</Field.Group>
<Dialog.Footer class="mt-4">
<Dialog.Close class={buttonVariants({ variant: "outline" })} type="button"
>Cancel
</Dialog.Close
>
<Button type="submit">Submit</Button>
</Dialog.Footer>
</form>
</Dialog.Content>
</Dialog.Root>
</div> </div>
</div> </div>
@ -101,7 +39,7 @@
{#if data.user && data.items.some( {#if data.user && data.items.some(
(item) => item.approvedDate === null (item) => item.approvedDate === null
)} )}
<FieldSeparator class="col-span-full text-lg mb-2 h-8"> <FieldSeparator class="col-span-full text-lg h-8">
Pending items Pending items
</FieldSeparator> </FieldSeparator>
{/if} {/if}
@ -127,13 +65,9 @@
{/each} {/each}
</div> </div>
</div> </div>
<SubmitItemDialog bind:open={createDialogOpen} />
{#if isGenerating}
<div class="fixed inset-0 bg-black/75 z-999999 w-screen h-screen justify-center items-center flex">
<p class="text-6xl text-primary">Loading...</p>
</div>
{/if}