account page
This commit is contained in:
@@ -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">
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
@@ -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, ''');
|
||||
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,}$";
|
||||
|
||||
@@ -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};
|
||||
`;
|
||||
});
|
||||
|
||||
@@ -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.`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user