account page

This commit is contained in:
2026-02-07 00:45:35 -06:00
parent ee80f849bd
commit cd128d7914
13 changed files with 379 additions and 94 deletions
@@ -23,7 +23,7 @@
</Dialog.Description>
</Dialog.Header>
<form method="post" action="?/claim">
<form method="post" action="?/claim&id={item?.id}">
<Field.Group>
<div class="flex gap-4">
+23 -6
View File
@@ -7,11 +7,13 @@
import XIcon from '@lucide/svelte/icons/x';
import PencilIcon from '@lucide/svelte/icons/pencil';
import NotebookPenIcon from '@lucide/svelte/icons/notebook-pen';
import TrashIcon from '@lucide/svelte/icons/trash';
import StarIcon from '@lucide/svelte/icons/star';
import ArchiveRestoreIcon from '@lucide/svelte/icons/archive-restore';
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 { approveDenyItem, restoreClaimedItem } from '$lib/db/items.remote';
import { invalidateAll } from '$app/navigation';
import NoImagePlaceholder from './no-image-placeholder.svelte';
@@ -98,11 +100,26 @@
Deny
</Button>
{/if}
<Button variant="ghost" class="text-action"
onclick={() => {editCallback(item)}}>
<PencilIcon />
Manage
</Button>
{#if item.claimedDate === null}
<Button variant="ghost" class="text-action"
onclick={() => {editCallback(item)}}>
<PencilIcon />
Manage
</Button>
{:else}
<Button variant="ghost" class="text-destructive"
onclick={async () => {await approveDenyItem({id: item.id, approved: false});
invalidateAll()}}>
<TrashIcon />
Delete
</Button>
<Button variant="ghost" class="text-action"
onclick={async () => {await restoreClaimedItem(item.id);
invalidateAll()}}>
<ArchiveRestoreIcon />
Restore
</Button>
{/if}
</div>
{:else}
+7
View File
@@ -0,0 +1,7 @@
import Root from "./switch.svelte";
export {
Root,
//
Root as Switch,
};
@@ -0,0 +1,29 @@
<script lang="ts">
import { Switch as SwitchPrimitive } from "bits-ui";
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
checked = $bindable(false),
...restProps
}: WithoutChildrenOrChild<SwitchPrimitive.RootProps> = $props();
</script>
<SwitchPrimitive.Root
bind:ref
bind:checked
data-slot="switch"
class={cn(
"data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 peer inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...restProps}
>
<SwitchPrimitive.Thumb
data-slot="switch-thumb"
class={cn(
"bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0"
)}
/>
</SwitchPrimitive.Root>
+2 -1
View File
@@ -25,4 +25,5 @@ const EMAIL_REGEX =
/^(?!\.)(?!.*\.\.)([a-z0-9_'+\-\.]*)[a-z0-9_'+\-]@([a-z0-9][a-z0-9\-]*\.)+[a-z]{2,}$/i;
// Replace single quote with HTML entity or remove it from the character class
export const EMAIL_REGEX_STRING = EMAIL_REGEX.source.replace(/'/g, '&#39;');
export const EMAIL_REGEX_STRING =
"^(?!\\.)(?!.*\\.\\.)([a-zA-Z0-9_'+\\-\\.]*)[a-zA-Z0-9_'+\\-]@([a-zA-Z0-9][a-zA-Z0-9\\-]*\\.)+[a-zA-Z]{2,}$";
+11
View File
@@ -58,3 +58,14 @@ export const approveDenyItem = command(
}
}
);
export const restoreClaimedItem = command(v.number(), async (id) => {
const { cookies } = getRequestEvent();
verifyJWT(cookies);
const reponse = await sql`
UPDATE items
SET claimed_date = null
WHERE id = ${id};
`;
});
+51 -42
View File
@@ -1,9 +1,8 @@
import nodemailer from 'nodemailer';
import sql from '$lib/db/db.server';
import type { Item } from '$lib/types/item';
import jwt from 'jsonwebtoken';
import { type inquiryTokenPayload, Sender } from '$lib/types/inquiries';
import { threadId } from 'node:worker_threads';
import type { Item } from '$lib/types/item';
// Create a transporter object using SMTP transport
export const transporter = nodemailer.createTransport({
@@ -47,7 +46,7 @@ export async function sendNewInquiryEmail(inquiryId: number) {
//
// `;
const item: Item = await sql`
const [item]: Item[] = await sql`
SELECT
i.*,
json_agg(
@@ -57,7 +56,7 @@ export async function sendNewInquiryEmail(inquiryId: number) {
'created_at', t.created_at,
'messages', m.messages
)
) FILTER (WHERE t.id = ${threadId}) AS threads
) FILTER ( WHERE t.id = ${inquiryId} ) AS threads
FROM items i
LEFT JOIN inquiry_threads t
ON t.item_id = i.id
@@ -77,49 +76,42 @@ export async function sendNewInquiryEmail(inquiryId: number) {
const replyToken = jwt.sign(tokenPayload, process.env.JWT_SECRET!);
console.log(item);
console.log(item.threads);
console.log(replyToken);
// Send mail with defined transport object
// await transporter.sendMail({
// from: `Westuffind Notifier <${process.env.EMAIL_USER}>`,
// to: item.emails,
// // to: 'drake@marinodev.com', // TEMPORARY EMAIL FOR TESTING
// replyTo: `${process.env.EMAIL_USER!.split('@')[0]}+${replyToken}${process.env.EMAIL_USER!.split('@')[1]}`,
// subject: 'New Item Inquiry!',
// text: `Someone has made an inquiry on the item with description: ${item.description}\nThey ask: ${item.threads![0].messages[0].body}\n\n\nRespond to this email directly, or click the below link to reply on Westuffinder\n${process.env.BASE_URL}/items/${item.id}/inquiries/${inquiryId}?token=${replyToken}`
// });
await transporter.sendMail({
from: `Westuffind Notifier <${process.env.EMAIL_USER}>`,
to: item.emails,
// to: 'drake@marinodev.com', // TEMPORARY EMAIL FOR TESTING
replyTo: `${process.env.EMAIL_USER!.split('@')[0]}+${replyToken}${process.env.EMAIL_USER!.split('@')[1]}`,
subject: 'New Item Inquiry!',
text: `Someone has made an inquiry on the item with description: ${item.description}\nThey ask: ${item.threads![0].messages[0].body}\n\n\nRespond to this email directly, or click the below link to reply on Westuffinder\n${process.env.BASE_URL}/items/${item.id}/inquiries/${inquiryId}?token=${replyToken}`
});
}
export async function sendInquiryMessageEmail(inquiryId: number, sender: Sender) {
const item: Item = await sql`
SELECT json_agg(item_data) AS result
FROM (
SELECT
i.*,
(
SELECT json_agg(thread_data)
FROM (
SELECT
it.id,
it.item_id,
(
SELECT json_agg(im)
FROM inquiry_messages im
WHERE im.thread_id = it.id
ORDER BY im.created_at
) AS messages
FROM inquiry_threads it
WHERE it.id = ${inquiryId}
) AS thread_data
) AS threads
FROM items i
WHERE i.id = (
SELECT item_id
FROM inquiry_threads
WHERE id = ${inquiryId}
)
) AS item_data;
`;
const [item]: Item[] = await sql`
SELECT
i.*,
json_agg(
jsonb_build_object(
'id', t.id,
'item_id', t.item_id,
'created_at', t.created_at,
'messages', m.messages
)
) FILTER ( WHERE t.id = ${inquiryId} ) AS threads
FROM items i
LEFT JOIN inquiry_threads t
ON t.item_id = i.id
LEFT JOIN LATERAL (
SELECT
json_agg(im.* ORDER BY im.created_at) AS messages
FROM inquiry_messages im
WHERE im.thread_id = t.id
) m ON TRUE
WHERE i.id = (SELECT item_id FROM inquiry_threads WHERE id = ${inquiryId})
GROUP BY i.id;`;
const tokenPayload: inquiryTokenPayload = {
threadId: inquiryId,
@@ -137,3 +129,20 @@ export async function sendInquiryMessageEmail(inquiryId: number, sender: Sender)
text: `Someone has replied to the inquiry on the item with description: ${item.description}\nThey say: ${item.threads![0].messages[item.threads![0].messages.length - 1].body}\n\n\nRespond to this email directly, or click the below link to reply on Westuffinder\n${process.env.BASE_URL}/items/${item.id}/inquiries/${inquiryId}?token=${replyToken}`
});
}
export async function sendClaimEmail(id: number, email: string) {
const [item]: Item[] = await sql`
SELECT * FROM items WHERE id = ${id};`;
if (!item.transferred) {
// Send mail with defined transport object
await transporter.sendMail({
from: `Westuffind Notifier <${process.env.EMAIL_USER}>`,
to: item.emails,
replyTo: email,
// to: 'drake@marinodev.com', // TEMPORARY EMAIL FOR TESTING
subject: 'Your Item was Claimed!',
text: `Someone has claimed your item with description: ${item.description}\nReply to this email explaining how they can pick up the item from you. Replies to this email go directly to the claimer.`
});
}
}