inquiries and claims
This commit is contained in:
@@ -3,6 +3,7 @@ import bcrypt from 'bcrypt';
|
||||
import sql from '$lib/db/db.server';
|
||||
import { type Cookies, error } from '@sveltejs/kit';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import type { inquiryTokenPayload } from '$lib/types/inquiries.server';
|
||||
|
||||
export function setJWTCookie(cookies: Cookies, user: User) {
|
||||
const payload = {
|
||||
@@ -25,6 +26,17 @@ export function setJWTCookie(cookies: Cookies, user: User) {
|
||||
cookies.set('jwt', JWT, { maxAge, path: '/', httpOnly: false, secure });
|
||||
}
|
||||
|
||||
export function createInquiryToken(payload: inquiryTokenPayload) {
|
||||
if (process.env.JWT_SECRET === undefined) {
|
||||
throw Error('JWT_SECRET not defined');
|
||||
}
|
||||
if (process.env.BASE_URL === undefined) {
|
||||
throw Error('BASE_URL not defined');
|
||||
}
|
||||
|
||||
return jwt.sign(payload, process.env.JWT_SECRET, { expiresIn: '30d' });
|
||||
}
|
||||
|
||||
export function verifyJWT(cookies: Cookies): UserPayload {
|
||||
const JWT = cookies.get('jwt');
|
||||
if (!JWT) throw error(403, 'Unauthorized');
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
<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 type { Item } from '$lib/types/item.server';
|
||||
import NoImagePlaceholder from './no-image-placeholder.svelte';
|
||||
import LocationIcon from '@lucide/svelte/icons/map-pinned';
|
||||
import { EMAIL_REGEX_STRING } from '$lib/consts';
|
||||
|
||||
|
||||
let { open = $bindable(), item }: { open: boolean, item: Item | undefined } = $props();
|
||||
</script>
|
||||
|
||||
<Dialog.Root bind:open>
|
||||
<Dialog.Content>
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>Claim Item</Dialog.Title>
|
||||
<Dialog.Description>
|
||||
Only submit this if you are sure the item is yours.<br>Claiming the item will remove it from public display.
|
||||
</Dialog.Description>
|
||||
</Dialog.Header>
|
||||
|
||||
<form method="post" action="?/inquire">
|
||||
<Field.Group>
|
||||
<div class="flex gap-4">
|
||||
|
||||
{#if item?.image}
|
||||
<img src="https://fbla26.marinodev.com/uploads/{item.id}.jpg"
|
||||
class="object-cover max-w-48 max-h-48 rounded-2xl"
|
||||
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="inline-block">
|
||||
<div class="flex-1">{item?.description}</div>
|
||||
{#if item?.foundLocation}
|
||||
<div class="mt-2">
|
||||
<LocationIcon class="float-left mr-1" size={24} />
|
||||
<div>{item?.foundLocation}</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<Field.Field>
|
||||
|
||||
<Field.Label for="email">
|
||||
Your Email <span class="text-error">*</span>
|
||||
</Field.Label>
|
||||
<Input
|
||||
id="email"
|
||||
name="email"
|
||||
placeholder="name@domain.com"
|
||||
pattern={EMAIL_REGEX_STRING}
|
||||
required
|
||||
/>
|
||||
</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>
|
||||
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
<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 { Separator } from '$lib/components/ui/separator';
|
||||
|
||||
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';
|
||||
import type { Item } from '$lib/types/item.server';
|
||||
import { Textarea } from '$lib/components/ui/textarea';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
import { dateFormatOptions } from '$lib/shared';
|
||||
|
||||
let { open = $bindable(), item = $bindable() }: { open: boolean, item: Item | undefined } = $props();
|
||||
|
||||
|
||||
let itemLocation: string = $derived(item ? item?.transferred ? 'turnedIn' : 'finderPossession' : '');
|
||||
let foundLocation: string | undefined = $derived(item?.foundLocation);
|
||||
let description: string = $derived(item ? item.description : '');
|
||||
let isGenerating = $state(false);
|
||||
|
||||
async function onSelect() {
|
||||
isGenerating = true;
|
||||
description = await genDescription();
|
||||
isGenerating = false;
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<Dialog.Root bind:open>
|
||||
<Dialog.Content class="max-w-[calc(100%-2rem)] md:max-w-3xl">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>Manage Item</Dialog.Title>
|
||||
<Dialog.Description>
|
||||
It will be updated immediately.
|
||||
</Dialog.Description>
|
||||
</Dialog.Header>
|
||||
|
||||
<div class="flex">
|
||||
<form method="post" action={'?/edit&id=' + item?.id} enctype="multipart/form-data" class="flex-2">
|
||||
|
||||
<Field.Group>
|
||||
<ImageUpload onSelect={onSelect} canRemove={false}
|
||||
previewUrl={'https://fbla26.marinodev.com/uploads/' + item?.id + '.jpg'} />
|
||||
|
||||
<Field.Field>
|
||||
<Field.Label for="description">
|
||||
Description <span class="text-error">*</span>
|
||||
</Field.Label>
|
||||
<Textarea
|
||||
id="description"
|
||||
name="description"
|
||||
bind:value={description}
|
||||
placeholder="A red leather book bag..."
|
||||
maxlength={200}
|
||||
rows={3}
|
||||
required
|
||||
/>
|
||||
</Field.Field>
|
||||
|
||||
<Field.Field>
|
||||
<Field.Label for="foundLocation">
|
||||
Where was it found?
|
||||
</Field.Label>
|
||||
<Input
|
||||
id="foundLocation"
|
||||
name="foundLocation"
|
||||
bind:value={foundLocation}
|
||||
placeholder="By the tennis courts."
|
||||
/>
|
||||
</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">
|
||||
The finder has the item.
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-2">
|
||||
<RadioGroup.Item value="turnedIn" id="turnedIn" />
|
||||
<Label for="turnedIn">
|
||||
The item is in the lost and found.
|
||||
</Label>
|
||||
</div>
|
||||
</RadioGroup.Root>
|
||||
|
||||
<Field.Field
|
||||
class={itemLocation !== "finderPossession" ? "hidden pointer-events-none opacity-50" : ""}
|
||||
>
|
||||
<Field.Label for="email">
|
||||
The finder's email <span class="text-error">*</span>
|
||||
</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>
|
||||
{#if item?.threads}
|
||||
<Separator orientation="vertical" class="mx-4" />
|
||||
<div class="inline-block flex-2">
|
||||
|
||||
<h2 class="text-lg leading-none font-semibold">Item inquiries:</h2>
|
||||
{#each item.threads as thread (thread)}
|
||||
<a href="/items/{item.id}/inquiries/{thread.id}" class="mt-4">
|
||||
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<Card.Title>Date</Card.Title>
|
||||
<Card.Content>Inquirer: {thread.messages[0].body}
|
||||
</Card.Content>
|
||||
</Card.Header>
|
||||
</Card.Root>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</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}
|
||||
@@ -7,12 +7,15 @@
|
||||
export let required = false;
|
||||
export let disabled = false;
|
||||
export let onSelect: ((file: File) => void) | null = null;
|
||||
export let canRemove = !required;
|
||||
|
||||
|
||||
let inputEl: HTMLInputElement | null = null;
|
||||
let previewUrl: string | null = null;
|
||||
export let previewUrl: string | null = null;
|
||||
let dragging = false;
|
||||
|
||||
console.log(previewUrl);
|
||||
|
||||
function openFileDialog() {
|
||||
if (!disabled) {
|
||||
inputEl?.click();
|
||||
@@ -93,7 +96,7 @@
|
||||
</div>
|
||||
{/if}
|
||||
</button>
|
||||
{#if previewUrl}
|
||||
{#if previewUrl && canRemove}
|
||||
<button class="hover:text-destructive p-2" on:click={cleanupPreview} type="button">
|
||||
<X size={24} class="inline" />
|
||||
<span class="inline align-middle">Remove image</span>
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
<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 type { Item } from '$lib/types/item.server';
|
||||
import NoImagePlaceholder from './no-image-placeholder.svelte';
|
||||
import LocationIcon from '@lucide/svelte/icons/map-pinned';
|
||||
import { EMAIL_REGEX_STRING } from '$lib/consts';
|
||||
|
||||
|
||||
let { open = $bindable(), item }: { open: boolean, item: Item | undefined } = $props();
|
||||
</script>
|
||||
|
||||
<Dialog.Root bind:open>
|
||||
<Dialog.Content>
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>Item Inquiry</Dialog.Title>
|
||||
<Dialog.Description>
|
||||
Have a question about an item? Ask here!
|
||||
</Dialog.Description>
|
||||
</Dialog.Header>
|
||||
|
||||
<form method="post" action="?/inquire">
|
||||
<Field.Group>
|
||||
<div class="flex gap-4">
|
||||
|
||||
{#if item?.image}
|
||||
<img src="https://fbla26.marinodev.com/uploads/{item.id}.jpg"
|
||||
class="object-cover max-w-48 max-h-48 rounded-2xl"
|
||||
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="inline-block">
|
||||
<div class="flex-1">{item?.description}</div>
|
||||
{#if item?.foundLocation}
|
||||
<div class="mt-2">
|
||||
<LocationIcon class="float-left mr-1" size={24} />
|
||||
<div>{item?.foundLocation}</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Field.Field>
|
||||
<Field.Label for="description">
|
||||
Please describe your inquiry <span class="text-error">*</span>
|
||||
</Field.Label>
|
||||
<Input
|
||||
id="inquiry"
|
||||
name="inquiry"
|
||||
placeholder="Is there a ..."
|
||||
maxlength={200}
|
||||
required
|
||||
/>
|
||||
</Field.Field>
|
||||
<Field.Field>
|
||||
|
||||
<Field.Label for="email">
|
||||
Your Email <span class="text-error">*</span>
|
||||
</Field.Label>
|
||||
<Input
|
||||
id="email"
|
||||
name="email"
|
||||
placeholder="name@domain.com"
|
||||
pattern={EMAIL_REGEX_STRING}
|
||||
required
|
||||
/>
|
||||
</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>
|
||||
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
<script lang="ts">
|
||||
|
||||
import type { Item } from '$lib/types/item';
|
||||
import type { Item } from '$lib/types/item.server';
|
||||
import { Badge } from '$lib/components/ui/badge';
|
||||
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 NotebookPenIcon from '@lucide/svelte/icons/notebook-pen';
|
||||
import StarIcon from '@lucide/svelte/icons/star';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import * as Tooltip from '$lib/components/ui/tooltip';
|
||||
import { dateFormatOptions } from '$lib/shared';
|
||||
@@ -13,17 +15,11 @@
|
||||
import { invalidateAll } from '$app/navigation';
|
||||
import NoImagePlaceholder from './no-image-placeholder.svelte';
|
||||
|
||||
export let item: Item = <Item>{
|
||||
id: 2,
|
||||
// title: 'Water Bottle',
|
||||
foundDate: new Date(),
|
||||
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.',
|
||||
image: true
|
||||
};
|
||||
export let item: Item = <Item>{};
|
||||
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) {
|
||||
@@ -103,12 +99,25 @@
|
||||
</Button>
|
||||
{/if}
|
||||
<Button variant="ghost" class="text-edit"
|
||||
onclick={async () => {}}>
|
||||
onclick={() => {editCallback(item)}}>
|
||||
<PencilIcon />
|
||||
Edit
|
||||
Manage
|
||||
</Button>
|
||||
|
||||
</div>
|
||||
{:else}
|
||||
<div class="mt-2 justify-between flex">
|
||||
<Button variant="ghost" class="text-edit"
|
||||
onclick={() => {inquireCallback(item)}}>
|
||||
<NotebookPenIcon />
|
||||
Inquire
|
||||
</Button>
|
||||
<Button variant="ghost" class="text-primary"
|
||||
onclick={() => {claimCallback(item)}}>
|
||||
<StarIcon />
|
||||
Claim
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
</div>
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
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';
|
||||
import { Textarea } from '$lib/components/ui/textarea';
|
||||
|
||||
let itemLocation: string | undefined = $state('');
|
||||
let foundLocation: string | undefined = $state();
|
||||
@@ -41,12 +42,13 @@
|
||||
<Field.Label for="description">
|
||||
Description <span class="text-error">*</span>
|
||||
</Field.Label>
|
||||
<Input
|
||||
<Textarea
|
||||
id="description"
|
||||
name="description"
|
||||
bind:value={description}
|
||||
placeholder="A red leather book bag..."
|
||||
maxlength={200}
|
||||
rows={3}
|
||||
required
|
||||
/>
|
||||
</Field.Field>
|
||||
@@ -60,7 +62,6 @@
|
||||
name="foundLocation"
|
||||
bind:value={foundLocation}
|
||||
placeholder="By the tennis courts."
|
||||
required
|
||||
/>
|
||||
</Field.Field>
|
||||
|
||||
@@ -84,7 +85,7 @@
|
||||
class={itemLocation !== "finderPossession" ? "hidden pointer-events-none opacity-50" : ""}
|
||||
>
|
||||
<Field.Label for="email">
|
||||
Your Email
|
||||
Your Email <span class="text-error">*</span>
|
||||
</Field.Label>
|
||||
<Input
|
||||
id="email"
|
||||
|
||||
@@ -12,11 +12,8 @@ export const genDescription = query(async () => {
|
||||
export const approveDenyItem = query(
|
||||
v.object({ id: v.number(), approved: v.boolean() }),
|
||||
async ({ id, approved }) => {
|
||||
console.log('called');
|
||||
|
||||
const { cookies } = getRequestEvent();
|
||||
const userPayload = verifyJWT(cookies);
|
||||
console.log('1');
|
||||
|
||||
if (approved) {
|
||||
const reponse = await sql`
|
||||
@@ -35,7 +32,6 @@ export const approveDenyItem = query(
|
||||
)
|
||||
WHERE id = ${id};
|
||||
`;
|
||||
console.log(reponse);
|
||||
} else {
|
||||
await sql`
|
||||
DELETE FROM items WHERE id = ${id};
|
||||
|
||||
@@ -4,10 +4,22 @@ export enum Sender {
|
||||
INQUIRER = 'inquirer'
|
||||
}
|
||||
|
||||
export interface message {
|
||||
export interface Message {
|
||||
id: number;
|
||||
threadId: number;
|
||||
// threadId: number;
|
||||
sender: Sender;
|
||||
body: string;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
export interface Thread {
|
||||
id: number;
|
||||
messages: Message[];
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
export interface inquiryTokenPayload {
|
||||
sender: Sender;
|
||||
threadId: number;
|
||||
messageId?: number;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { Thread } from '$lib/types/inquiries.server';
|
||||
|
||||
export interface Item {
|
||||
id: number;
|
||||
emails?: string[];
|
||||
@@ -12,4 +14,5 @@ export interface Item {
|
||||
foundLocation: string;
|
||||
deleted: boolean;
|
||||
image: boolean;
|
||||
threads?: Thread[];
|
||||
}
|
||||
Reference in New Issue
Block a user