pages
This commit is contained in:
parent
8ea632a14f
commit
5cd3af719d
@ -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 {
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
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
|
transferred: boolean; // to L&F location
|
||||||
keywords?: string[];
|
keywords?: string[];
|
||||||
foundLocation: string;
|
foundLocation: string;
|
||||||
|
deleted: boolean;
|
||||||
|
image: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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');
|
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, {
|
||||||
|
|||||||
@ -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}
|
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user