pages
This commit is contained in:
parent
8ea632a14f
commit
5cd3af719d
@ -12,7 +12,7 @@
|
||||
--card-foreground: oklch(0.141 0.005 285.823);
|
||||
--popover: oklch(1 0 0);
|
||||
--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);
|
||||
--secondary: oklch(0.967 0.001 286.375);
|
||||
--secondary-foreground: oklch(0.21 0.006 285.885);
|
||||
@ -40,6 +40,7 @@
|
||||
--warning: oklch(0.84 0.16 84);
|
||||
--error: oklch(0.577 0.245 27.325);
|
||||
--positive: oklch(0.5 0.2067 147.18);
|
||||
--edit: oklch(0.5852 0.2263 260.47);
|
||||
}
|
||||
|
||||
.dark {
|
||||
@ -77,6 +78,7 @@
|
||||
--warning: oklch(0.84 0.16 84);
|
||||
--error: oklch(0.704 0.191 22.216);
|
||||
--positive: oklch(0.7522 0.2067 147.18);
|
||||
--edit: oklch(0.6098 0.1872 260.47);
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
@ -118,6 +120,7 @@
|
||||
--color-warning: var(--warning);
|
||||
--color-error: var(--error);
|
||||
--color-positive: var(--positive);
|
||||
--color-edit: var(--edit);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
|
||||
@ -5,11 +5,13 @@
|
||||
import LocationIcon from '@lucide/svelte/icons/map-pinned';
|
||||
import CheckIcon from '@lucide/svelte/icons/check';
|
||||
import XIcon from '@lucide/svelte/icons/x';
|
||||
import PencilIcon from '@lucide/svelte/icons/pencil';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import * as Tooltip from '$lib/components/ui/tooltip';
|
||||
import { dateFormatOptions } from '$lib/shared';
|
||||
import { approveDenyItem } from '$lib/db/items.remote';
|
||||
import { invalidateAll } from '$app/navigation';
|
||||
import NoImagePlaceholder from './no-image-placeholder.svelte';
|
||||
|
||||
export let item: Item = <Item>{
|
||||
id: 2,
|
||||
@ -18,7 +20,8 @@
|
||||
approvedDate: new Date(),
|
||||
description: 'A matte black water bottle with a black lid and a "BKLYN BENTO" logo on the side.',
|
||||
transferred: true,
|
||||
foundLocation: 'By the tennis courts.'
|
||||
foundLocation: 'By the tennis courts.',
|
||||
image: true
|
||||
};
|
||||
export let admin = false;
|
||||
|
||||
@ -28,11 +31,24 @@
|
||||
} else {
|
||||
timeSincePosted = Math.round(timeSincePosted);
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<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">
|
||||
<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="font-bold inline-block">{item.title}</div>-->
|
||||
@ -69,8 +85,10 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if admin && item.approvedDate === null}
|
||||
{#if admin}
|
||||
<div class="mt-2 justify-between flex">
|
||||
{#if item.approvedDate === null}
|
||||
|
||||
<Button variant="ghost" class="text-positive"
|
||||
onclick={async () => {await approveDenyItem({id: item.id, approved: true});
|
||||
invalidateAll()}}>
|
||||
@ -83,8 +101,16 @@
|
||||
<XIcon />
|
||||
Deny
|
||||
</Button>
|
||||
{/if}
|
||||
<Button variant="ghost" class="text-edit"
|
||||
onclick={async () => {}}>
|
||||
<PencilIcon />
|
||||
Edit
|
||||
</Button>
|
||||
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
122
src/lib/components/custom/no-image-placeholder.svelte
Normal file
122
src/lib/components/custom/no-image-placeholder.svelte
Normal 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>
|
||||
117
src/lib/components/custom/submit-item-dialog.svelte
Normal file
117
src/lib/components/custom/submit-item-dialog.svelte
Normal 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}
|
||||
2
src/lib/types/item.d.ts
vendored
2
src/lib/types/item.d.ts
vendored
@ -10,4 +10,6 @@ export interface Item {
|
||||
transferred: boolean; // to L&F location
|
||||
keywords?: string[];
|
||||
foundLocation: string;
|
||||
deleted: boolean;
|
||||
image: boolean;
|
||||
}
|
||||
|
||||
@ -22,7 +22,11 @@
|
||||
<span class="hidden sm:block">MarinoDev Lost & Found</span>
|
||||
</a>
|
||||
<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">
|
||||
<Button onclick={toggleMode} variant="outline" size="icon">
|
||||
<SunIcon
|
||||
|
||||
@ -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 & Found
|
||||
</h1>
|
||||
<p class="mx-auto mt-6 max-w-2xl text-lg text-muted-foreground">
|
||||
Lost something at school? Found something that isn’t 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 & 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} />
|
||||
@ -30,11 +30,11 @@ export const actions: Actions = {
|
||||
location = getFormString(data, 'location');
|
||||
|
||||
const file = data.get('image');
|
||||
if (file === null)
|
||||
return fail(400, {
|
||||
message: `Missing required field image.`,
|
||||
success: false
|
||||
});
|
||||
// if (file === null)
|
||||
// return fail(400, {
|
||||
// message: `Missing required field image.`,
|
||||
// success: false
|
||||
// });
|
||||
|
||||
if (!email && location !== 'turnedIn') {
|
||||
fail(400, {
|
||||
@ -43,6 +43,8 @@ export const actions: Actions = {
|
||||
});
|
||||
}
|
||||
|
||||
let outputBuffer: Buffer | undefined;
|
||||
if (file) {
|
||||
if (!(file instanceof File)) {
|
||||
return fail(400, { message: 'No file uploaded or file is invalid', success: false });
|
||||
}
|
||||
@ -54,8 +56,6 @@ export const actions: Actions = {
|
||||
const image = sharp(inputBuffer);
|
||||
const metadata = await image.metadata();
|
||||
|
||||
let outputBuffer: Buffer;
|
||||
|
||||
if (metadata.format === 'jpeg') {
|
||||
// Already JPG → keep as-is
|
||||
outputBuffer = inputBuffer;
|
||||
@ -65,13 +65,15 @@ export const actions: Actions = {
|
||||
.jpeg({ quality: 90 }) // adjust if needed
|
||||
.toBuffer();
|
||||
}
|
||||
}
|
||||
|
||||
const response = await sql`
|
||||
INSERT INTO items (
|
||||
emails,
|
||||
description,
|
||||
transferred,
|
||||
found_location
|
||||
found_location,
|
||||
image
|
||||
) VALUES (
|
||||
${
|
||||
location === 'turnedIn'
|
||||
@ -89,11 +91,13 @@ export const actions: Actions = {
|
||||
},
|
||||
${description},
|
||||
${location === 'turnedIn'},
|
||||
${foundLocation}
|
||||
${foundLocation},
|
||||
${file instanceof File}
|
||||
)
|
||||
RETURNING id;
|
||||
`;
|
||||
|
||||
if (outputBuffer) {
|
||||
try {
|
||||
// It's a good idea to validate the filename to prevent path traversal attacks
|
||||
const savePath = path.join('uploads', `${response[0]['id']}.jpg`);
|
||||
@ -103,6 +107,7 @@ export const actions: Actions = {
|
||||
console.error('File upload failed:', err);
|
||||
return fail(500, { message: 'Internal server error during file upload', success: false });
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
return fail(400, {
|
||||
message: e instanceof Error ? e.message : 'Unknown error occurred',
|
||||
|
||||
@ -11,88 +11,26 @@
|
||||
import { Label } from '$lib/components/ui/label';
|
||||
import ItemListing from '$lib/components/custom/item-listing.svelte';
|
||||
import { FieldSeparator } from '$lib/components/ui/field';
|
||||
import SubmitItemDialog from '$lib/components/custom/submit-item-dialog.svelte';
|
||||
|
||||
// import { type Item } from '$lib/types/item';
|
||||
|
||||
let itemLocation: string | undefined = $state('');
|
||||
let foundLocation: string | undefined = $state();
|
||||
let description: string | undefined = $state();
|
||||
let isGenerating = $state(false);
|
||||
let createDialogOpen: boolean = $state(false);
|
||||
|
||||
async function onSelect() {
|
||||
isGenerating = true;
|
||||
description = await genDescription();
|
||||
isGenerating = false;
|
||||
function openCreateDialog() {
|
||||
createDialogOpen = true;
|
||||
}
|
||||
|
||||
|
||||
let { data }: PageProps = $props();
|
||||
</script>
|
||||
|
||||
<div class="max-w-7xl mx-auto px-4">
|
||||
<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">
|
||||
<Dialog.Root>
|
||||
<Dialog.Trigger class={buttonVariants({ variant: 'default' })} type="button"
|
||||
>Post an item
|
||||
</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>
|
||||
<Button variant="default" class="" onclick={openCreateDialog}>
|
||||
Submit a Found Item
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -101,7 +39,7 @@
|
||||
{#if data.user && data.items.some(
|
||||
(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
|
||||
</FieldSeparator>
|
||||
{/if}
|
||||
@ -127,13 +65,9 @@
|
||||
{/each}
|
||||
|
||||
</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}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user