diff --git a/apps/web/package.json b/apps/web/package.json index 22edc17..039acd2 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -34,10 +34,12 @@ "hono": "^4.2.2", "install": "^0.13.0", "lucide-react": "^0.359.0", - "next": "^14.1.3", + "mime-types": "^2.1.35", + "next": "^14.2.1", "next-auth": "^4.24.6", "pnpm": "^8.15.5", "prisma": "^5.11.0", + "query-string": "^9.0.0", "react": "18.2.0", "react-dom": "18.2.0", "recharts": "^2.12.5", @@ -48,6 +50,7 @@ }, "devDependencies": { "@types/eslint": "^8.56.2", + "@types/mime-types": "^2.1.4", "@types/node": "^20.11.20", "@types/react": "^18.2.57", "@types/react-dom": "^18.2.19", diff --git a/apps/web/public/Logo-1.png b/apps/web/public/Logo-1.png new file mode 100644 index 0000000..a238f96 Binary files /dev/null and b/apps/web/public/Logo-1.png differ diff --git a/apps/web/public/Logo-2.png b/apps/web/public/Logo-2.png new file mode 100644 index 0000000..a0b3266 Binary files /dev/null and b/apps/web/public/Logo-2.png differ diff --git a/apps/web/src/app/(dashboard)/emails/email-details.tsx b/apps/web/src/app/(dashboard)/emails/email-details.tsx new file mode 100644 index 0000000..6726306 --- /dev/null +++ b/apps/web/src/app/(dashboard)/emails/email-details.tsx @@ -0,0 +1,103 @@ +"use client"; + +import { useEffect } from "react"; +import { useRouter } from "next/navigation"; +import { api } from "~/trpc/react"; +import { Separator } from "@unsend/ui/src/separator"; +import { EmailStatusBadge, EmailStatusIcon } from "./email-status-badge"; +import { formatDate } from "date-fns"; +import { EmailStatus } from "@prisma/client"; +import { JsonValue } from "@prisma/client/runtime/library"; +import { SesDeliveryDelay } from "~/types/aws-types"; +import { DELIVERY_DELAY_ERRORS } from "~/lib/constants/ses-errors"; + +export default function EmailDetails({ emailId }: { emailId: string }) { + const emailQuery = api.email.getEmail.useQuery({ id: emailId }); + + return ( +
+
+
+

{emailQuery.data?.to}

+ +
+
+
+
+
+ From + {emailQuery.data?.from} +
+ +
+ To + {emailQuery.data?.to} +
+ +
+ Subject + {emailQuery.data?.subject} +
+
+
+
+
+
+
+
Events History
+
+
+
+ {emailQuery.data?.emailEvents.map((evt) => ( +
+
+ +
+
+
+ +
+
+ {formatDate(evt.createdAt, "MMM dd, hh:mm a")} +
+
+ +
+
+
+ ))} +
+
+
+
+
+
+ ); +} + +const EmailStatusText = ({ + status, + data, +}: { + status: EmailStatus; + data: JsonValue; +}) => { + if (status === "SENT") { + return ( +
+ We received your request and sent the email to recipient's server. +
+ ); + } else if (status === "DELIVERED") { + return
Mail is successfully delivered to the recipient.
; + } else if (status === "DELIVERY_DELAYED") { + const _errorData = data as unknown as SesDeliveryDelay; + const errorMessage = DELIVERY_DELAY_ERRORS[_errorData.delayType]; + + return
{errorMessage}
; + } + return
{status}
; +}; diff --git a/apps/web/src/app/(dashboard)/emails/email-list.tsx b/apps/web/src/app/(dashboard)/emails/email-list.tsx index 7adb015..a973ebe 100644 --- a/apps/web/src/app/(dashboard)/emails/email-list.tsx +++ b/apps/web/src/app/(dashboard)/emails/email-list.tsx @@ -3,7 +3,6 @@ import Link from "next/link"; import { Table, - TableCaption, TableHeader, TableRow, TableHead, @@ -20,15 +19,85 @@ import { MailWarning, MailX, } from "lucide-react"; -import { formatDistance, formatDistanceToNow } from "date-fns"; +import { formatDistanceToNow } from "date-fns"; import { EmailStatus } from "@prisma/client"; +import { EmailStatusBadge } from "./email-status-badge"; +import { useState } from "react"; +import EmailDetails from "./email-details"; +import { useRouter } from "next/navigation"; +import { useSearchParams } from "next/navigation"; // Adjust the import based on your project setup +import dynamic from "next/dynamic"; +import { useUrlState } from "~/hooks/useUrlState"; +import { Button } from "@unsend/ui/src/button"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@unsend/ui/src/select"; + +/* Stupid hydrating error. And I so stupid to understand the stupid NextJS docs. Because they stupid change it everyday */ +const DynamicSheetWithNoSSR = dynamic( + () => import("@unsend/ui/src/sheet").then((mod) => mod.Sheet), + { ssr: false } +); + +const DynamicSheetContentWithNoSSR = dynamic( + () => import("@unsend/ui/src/sheet").then((mod) => mod.SheetContent), + { ssr: false } +); export default function EmailsList() { - const emailsQuery = api.email.emails.useQuery(); + const [selectedEmail, setSelectedEmail] = useUrlState("emailId"); + const [page, setPage] = useUrlState("page", "1"); + const [status, setStatus] = useUrlState("status"); + + const pageNumber = Number(page); + + const emailsQuery = api.email.emails.useQuery({ + page: pageNumber, + status: status?.toUpperCase() as EmailStatus, + }); + + const handleSelectEmail = (emailId: string) => { + setSelectedEmail(emailId); + }; + + const handleSheetChange = (isOpen: boolean) => { + if (!isOpen) { + setSelectedEmail(null); + } + }; return ( -
-
+
+
+ +
+
@@ -41,26 +110,62 @@ export default function EmailsList() { - {emailsQuery.data?.map((email) => ( - - - -

{email.to}

-
- - - {/* - {email.latestStatus ?? "Sent"} - */} - - {email.subject} - - {formatDistanceToNow(email.createdAt, { addSuffix: true })} + {emailsQuery.data?.emails.length ? ( + emailsQuery.data?.emails.map((email) => ( + handleSelectEmail(email.id)} + className=" cursor-pointer" + > + +
+ +

{email.to}

+
+
+ + + + {email.subject} + + {formatDistanceToNow(email.createdAt, { addSuffix: true })} + +
+ )) + ) : ( + + + No emails found - ))} + )}
+ + + + {selectedEmail ? : null} + + +
+
+ +
); @@ -113,39 +218,3 @@ const EmailIcon: React.FC<{ status: EmailStatus }> = ({ status }) => { ); } }; - -const EmailStatusBadge: React.FC<{ status: EmailStatus }> = ({ status }) => { - let badgeColor = "bg-gray-400/10 text-gray-500 border-gray-400/10"; // Default color - switch (status) { - case "SENT": - badgeColor = "bg-gray-400/10 text-gray-500 border-gray-400/10"; - break; - case "DELIVERED": - badgeColor = "bg-emerald-500/10 text-emerald-500 border-emerald-600/10"; - break; - case "BOUNCED": - badgeColor = "bg-red-500/10 text-red-800 border-red-600/10"; - break; - case "CLICKED": - badgeColor = "bg-cyan-500/10 text-cyan-600 border-cyan-600/10"; - break; - case "OPENED": - badgeColor = "bg-indigo-500/10 text-indigo-600 border-indigo-600/10"; - break; - case "DELIVERY_DELAYED": - badgeColor = "bg-yellow-500/10 text-yellow-600 border-yellow-600/10"; - case "COMPLAINED": - badgeColor = "bg-yellow-500/10 text-yellow-600 border-yellow-600/10"; - break; - default: - badgeColor = "bg-gray-400/10 text-gray-500 border-gray-400/10"; - } - - return ( -
- {status.toLowerCase().split("_").join(" ")} -
- ); -}; diff --git a/apps/web/src/app/(dashboard)/emails/email-status-badge.tsx b/apps/web/src/app/(dashboard)/emails/email-status-badge.tsx new file mode 100644 index 0000000..4116ea3 --- /dev/null +++ b/apps/web/src/app/(dashboard)/emails/email-status-badge.tsx @@ -0,0 +1,83 @@ +import { EmailStatus } from "@prisma/client"; + +export const EmailStatusBadge: React.FC<{ status: EmailStatus }> = ({ + status, +}) => { + let badgeColor = "bg-gray-400/10 text-gray-400 border-gray-400/10"; // Default color + switch (status) { + case "SENT": + badgeColor = "bg-gray-400/10 text-gray-400 border-gray-400/10"; + break; + case "DELIVERED": + badgeColor = "bg-emerald-500/10 text-emerald-500 border-emerald-600/10"; + break; + case "BOUNCED": + badgeColor = "bg-red-500/10 text-red-800 border-red-600/10"; + break; + case "CLICKED": + badgeColor = "bg-cyan-500/10 text-cyan-600 border-cyan-600/10"; + break; + case "OPENED": + badgeColor = "bg-indigo-500/10 text-indigo-600 border-indigo-600/10"; + break; + case "DELIVERY_DELAYED": + badgeColor = "bg-yellow-500/10 text-yellow-600 border-yellow-600/10"; + case "COMPLAINED": + badgeColor = "bg-yellow-500/10 text-yellow-600 border-yellow-600/10"; + break; + default: + badgeColor = "bg-gray-400/10 text-gray-400 border-gray-400/10"; + } + + return ( +
+ {status.toLowerCase().split("_").join(" ")} +
+ ); +}; + +export const EmailStatusIcon: React.FC<{ status: EmailStatus }> = ({ + status, +}) => { + let outsideColor = "bg-gray-600"; + let insideColor = "bg-gray-600/50"; + + switch (status) { + case "DELIVERED": + outsideColor = "bg-emerald-500/40"; + insideColor = "bg-emerald-500"; + break; + case "BOUNCED": + outsideColor = "bg-red-500/40"; + insideColor = "bg-red-500"; + break; + case "CLICKED": + outsideColor = "bg-cyan-500/40"; + insideColor = "bg-cyan-500"; + break; + case "OPENED": + outsideColor = "bg-indigo-500/40"; + insideColor = "bg-indigo-500"; + break; + case "DELIVERY_DELAYED": + outsideColor = "bg-yellow-500/40"; + insideColor = "bg-yellow-500"; + case "COMPLAINED": + outsideColor = "bg-yellow-500/40"; + insideColor = "bg-yellow-500"; + break; + default: + outsideColor = "bg-gray-600/40"; + insideColor = "bg-gray-600"; + } + + return ( +
+
+
+ ); +}; diff --git a/apps/web/src/app/(dashboard)/emails/page.tsx b/apps/web/src/app/(dashboard)/emails/page.tsx index 401f972..6fdcd78 100644 --- a/apps/web/src/app/(dashboard)/emails/page.tsx +++ b/apps/web/src/app/(dashboard)/emails/page.tsx @@ -1,13 +1,20 @@ import type { Metadata } from "next"; -import EmailList from "./email-list"; +import { Suspense } from "react"; +import dynamic from "next/dynamic"; +const EmailList = dynamic( + () => import("./email-list").then((mod) => mod.default), + { ssr: false } +); export default async function EmailsPage() { return (

Emails

+ {/* Loading...
}> */} + {/* */}
); } diff --git a/apps/web/src/app/(dashboard)/layout.tsx b/apps/web/src/app/(dashboard)/layout.tsx index 0419194..9124407 100644 --- a/apps/web/src/app/(dashboard)/layout.tsx +++ b/apps/web/src/app/(dashboard)/layout.tsx @@ -3,7 +3,9 @@ import { redirect } from "next/navigation"; import { Bell, BellRing, + BookUser, CircleUser, + Code, Globe, Home, KeyRound, @@ -17,6 +19,7 @@ import { Search, ShoppingCart, Users, + Volume2, } from "lucide-react"; import { Button } from "@unsend/ui/src/button"; @@ -61,12 +64,8 @@ export default async function AuthenticatedDashboardLayout({
- Unsend + Unsend + Unsend
@@ -87,6 +86,16 @@ export default async function AuthenticatedDashboardLayout({ Domains + + + Contacts + + + + + Marketing + + SMS @@ -98,8 +107,8 @@ export default async function AuthenticatedDashboardLayout({ - - API keys + + Developer settings
diff --git a/apps/web/src/app/api/ses_callback/route.ts b/apps/web/src/app/api/ses_callback/route.ts index 1e08af0..fda5ed9 100644 --- a/apps/web/src/app/api/ses_callback/route.ts +++ b/apps/web/src/app/api/ses_callback/route.ts @@ -1,6 +1,3 @@ -import { headers } from "next/headers"; -import { hashToken } from "~/server/auth"; -import { db } from "~/server/db"; import { parseSesHook } from "~/server/service/ses-hook-parser"; export async function GET(req: Request) { @@ -22,14 +19,15 @@ export async function POST(req: Request) { try { message = JSON.parse(data.Message || "{}"); const status = await parseSesHook(message); + console.log("Error is parsing hook", status); if (!status) { - return Response.json({ data: "Error is parsing hook" }, { status: 400 }); + return Response.json({ data: "Error is parsing hook" }); } return Response.json({ data: "Success" }); } catch (e) { console.error(e); - return Response.json({ data: "Error is parsing hook" }, { status: 400 }); + return Response.json({ data: "Error is parsing hook" }); } } diff --git a/apps/web/src/hooks/useUrlState.ts b/apps/web/src/hooks/useUrlState.ts new file mode 100644 index 0000000..a5e0ab9 --- /dev/null +++ b/apps/web/src/hooks/useUrlState.ts @@ -0,0 +1,36 @@ +import { useCallback, useState } from "react"; +import qs from "query-string"; + +/** + * A custom hook to use URL as state + * @param key The query parameter key. + */ +export function useUrlState(key: string, defaultValue: string | null = null) { + const [state, setState] = useState(() => { + if (typeof window === "undefined") return null; + const queryValue = qs.parse(window.location.search)[key]; + if (queryValue !== undefined) { + return (Array.isArray(queryValue) ? queryValue[0] : queryValue) ?? null; + } + return defaultValue; + }); + + // Update URL when state changes + const setUrlState = useCallback( + (newValue: string | null) => { + setState(newValue); + const newQuery = { + ...qs.parse(window.location.search), + [key]: newValue, + }; + const newUrl = qs.stringifyUrl({ + url: window.location.href, + query: newQuery, + }); + window.history.replaceState({}, "", newUrl); + }, + [key] + ); + + return [state, setUrlState] as const; +} diff --git a/apps/web/src/lib/constants/ses-errors.ts b/apps/web/src/lib/constants/ses-errors.ts new file mode 100644 index 0000000..1aced89 --- /dev/null +++ b/apps/web/src/lib/constants/ses-errors.ts @@ -0,0 +1,46 @@ +export const DELIVERY_DELAY_ERRORS = { + InternalFailure: "An internal Unsend issue caused the message to be delayed.", + General: "A generic failure occurred during the SMTP conversation.", + MailboxFull: + "The recipient's mailbox is full and is unable to receive additional messages.", + SpamDetected: + "The recipient's mail server has detected a large amount of unsolicited email from your account.", + RecipientServerError: + "A temporary issue with the recipient's email server is preventing the delivery of the message.", + IPFailure: + "The IP address that's sending the message is being blocked or throttled by the recipient's email provider.", + TransientCommunicationFailure: + "There was a temporary communication failure during the SMTP conversation with the recipient's email provider.", + BYOIPHostNameLookupUnavailable: + "Unsend was unable to look up the DNS hostname for your IP addresses. This type of delay only occurs when you use Bring Your Own IP.", + Undetermined: + "Unsend wasn't able to determine the reason for the delivery delay.", + SendingDeferral: + "Unsend has deemed it appropriate to internally defer the message.", +}; + +export const BOUNCE_ERROR_MESSAGES = { + Undetermined: "Unsend was unable to determine a specific bounce reason.", + Permanent: { + General: + "Unsend received a general hard bounce. If you receive this type of bounce, you should remove the recipient's email address from your mailing list.", + NoEmail: + "Unsend received a permanent hard bounce because the target email address does not exist. If you receive this type of bounce, you should remove the recipient's email address from your mailing list.", + Suppressed: + "Unsend has suppressed sending to this address because it has a recent history of bouncing as an invalid address. To override the global suppression list, see Using the Unsend account-level suppression list.", + OnAccountSuppressionList: + "Unsend has suppressed sending to this address because it is on the account-level suppression list. This does not count toward your bounce rate metric.", + }, + Transient: { + General: + "Unsend received a general bounce. You may be able to successfully send to this recipient in the future.", + MailboxFull: + "Unsend received a mailbox full bounce. You may be able to successfully send to this recipient in the future.", + MessageTooLarge: + "Unsend received a message too large bounce. You may be able to successfully send to this recipient if you reduce the size of the message.", + ContentRejected: + "Unsend received a content rejected bounce. You may be able to successfully send to this recipient if you change the content of the message.", + AttachmentRejected: + "Unsend received an attachment rejected bounce. You may be able to successfully send to this recipient if you remove or change the attachment.", + }, +}; diff --git a/apps/web/src/server/api/routers/email.ts b/apps/web/src/server/api/routers/email.ts index a7d4dca..ad462d8 100644 --- a/apps/web/src/server/api/routers/email.ts +++ b/apps/web/src/server/api/routers/email.ts @@ -1,24 +1,55 @@ +import { EmailStatus } from "@prisma/client"; import { z } from "zod"; -import { - createTRPCRouter, - protectedProcedure, - publicProcedure, - teamProcedure, -} from "~/server/api/trpc"; +import { createTRPCRouter, teamProcedure } from "~/server/api/trpc"; import { db } from "~/server/db"; -import { createDomain, getDomain } from "~/server/service/domain-service"; + +const statuses = Object.values(EmailStatus) as [EmailStatus]; + +const DEFAULT_LIMIT = 30; export const emailRouter = createTRPCRouter({ - emails: teamProcedure.query(async ({ ctx }) => { - const emails = await db.email.findMany({ - where: { - teamId: ctx.team.id, - }, - }); + emails: teamProcedure + .input( + z.object({ + page: z.number().optional(), + status: z.enum(statuses).optional().nullable(), + domain: z.number().optional(), + }) + ) + .query(async ({ ctx, input }) => { + const page = input.page || 1; + const limit = DEFAULT_LIMIT; + const offset = (page - 1) * limit; - return emails; - }), + const whereConditions = { + teamId: ctx.team.id, + ...(input.status ? { latestStatus: input.status } : {}), + ...(input.domain ? { domainId: input.domain } : {}), + }; + + const countP = db.email.count({ where: whereConditions }); + + const emailsP = db.email.findMany({ + where: whereConditions, + select: { + id: true, + createdAt: true, + latestStatus: true, + subject: true, + to: true, + }, + orderBy: { + createdAt: "desc", + }, + skip: offset, + take: limit, + }); + + const [emails, count] = await Promise.all([emailsP, countP]); + + return { emails, totalPage: Math.ceil(count / limit) }; + }), getEmail: teamProcedure .input(z.object({ id: z.string() })) @@ -30,7 +61,7 @@ export const emailRouter = createTRPCRouter({ include: { emailEvents: { orderBy: { - createdAt: "desc", + createdAt: "asc", }, }, }, diff --git a/apps/web/src/server/aws/ses.ts b/apps/web/src/server/aws/ses.ts index 8b9e6de..d6efcc1 100644 --- a/apps/web/src/server/aws/ses.ts +++ b/apps/web/src/server/aws/ses.ts @@ -10,6 +10,7 @@ import { EventType, } from "@aws-sdk/client-sesv2"; import { generateKeyPairSync } from "crypto"; +import mime from "mime-types"; import { env } from "~/env"; import { EmailContent } from "~/types"; import { APP_SETTINGS } from "~/utils/constants"; @@ -154,6 +155,63 @@ export async function sendEmailThroughSes({ } } +export async function sendEmailWithAttachments({ + to, + from, + subject, + text, + html, + attachments, + region = "us-east-1", + configurationSetName, +}: EmailContent & { + region?: string; + configurationSetName: string; + attachments: { filename: string; content: string }[]; +}) { + const sesClient = getSesClient(region); + const boundary = "NextPart"; + let rawEmail = `From: ${from}\n`; + rawEmail += `To: ${to}\n`; + rawEmail += `Subject: ${subject}\n`; + rawEmail += `MIME-Version: 1.0\n`; + rawEmail += `Content-Type: multipart/mixed; boundary="${boundary}"\n\n`; + rawEmail += `--${boundary}\n`; + rawEmail += `Content-Type: text/html; charset="UTF-8"\n\n`; + rawEmail += `${html}\n\n`; + + for (const attachment of attachments) { + const content = attachment.content; // Convert buffer to base64 + const mimeType = + mime.lookup(attachment.filename) || "application/octet-stream"; + rawEmail += `--${boundary}\n`; + rawEmail += `Content-Type: ${mimeType}; name="${attachment.filename}"\n`; + rawEmail += `Content-Disposition: attachment; filename="${attachment.filename}"\n`; + rawEmail += `Content-Transfer-Encoding: base64\n\n`; + rawEmail += `${content}\n\n`; + } + + rawEmail += `--${boundary}--`; + + const command = new SendEmailCommand({ + Content: { + Raw: { + Data: Buffer.from(rawEmail), + }, + }, + ConfigurationSetName: configurationSetName, + }); + + try { + const response = await sesClient.send(command); + console.log("Email with attachments sent! Message ID:", response.MessageId); + return response.MessageId; + } catch (error) { + console.error("Failed to send email with attachments", error); + throw new Error("Failed to send email with attachments"); + } +} + export async function addWebhookConfiguration( configName: string, topicArn: string, diff --git a/apps/web/src/server/public-api/api/send_email.ts b/apps/web/src/server/public-api/api/send_email.ts index 0aac629..0ded2da 100644 --- a/apps/web/src/server/public-api/api/send_email.ts +++ b/apps/web/src/server/public-api/api/send_email.ts @@ -19,6 +19,14 @@ const route = createRoute({ subject: z.string(), text: z.string().optional(), html: z.string().optional(), + attachments: z + .array( + z.object({ + filename: z.string(), + content: z.string(), + }) + ) + .optional(), }), }, }, diff --git a/apps/web/src/server/service/email-service.ts b/apps/web/src/server/service/email-service.ts index d68a31f..2fd8070 100644 --- a/apps/web/src/server/service/email-service.ts +++ b/apps/web/src/server/service/email-service.ts @@ -1,12 +1,12 @@ import { EmailContent } from "~/types"; import { db } from "../db"; -import { sendEmailThroughSes } from "../aws/ses"; +import { sendEmailThroughSes, sendEmailWithAttachments } from "../aws/ses"; import { APP_SETTINGS } from "~/utils/constants"; export async function sendEmail( emailContent: EmailContent & { teamId: number } ) { - const { to, from, subject, text, html, teamId } = emailContent; + const { to, from, subject, text, html, teamId, attachments } = emailContent; const fromDomain = from.split("@")[1]; @@ -24,18 +24,33 @@ export async function sendEmail( throw new Error("Domain is not verified"); } - const messageId = await sendEmailThroughSes({ - to, - from, - subject, - text, - html, - region: domain.region, - configurationSetName: getConfigurationSetName( - domain.clickTracking, - domain.openTracking - ), - }); + const messageId = attachments + ? await sendEmailWithAttachments({ + to, + from, + subject, + text, + html, + region: domain.region, + configurationSetName: getConfigurationSetName( + domain.clickTracking, + domain.openTracking + ), + attachments, + }) + : await sendEmailThroughSes({ + to, + from, + subject, + text, + html, + region: domain.region, + configurationSetName: getConfigurationSetName( + domain.clickTracking, + domain.openTracking + ), + attachments, + }); if (messageId) { return await db.email.create({ diff --git a/apps/web/src/types/aws-types.ts b/apps/web/src/types/aws-types.ts index 4549502..ddcbb11 100644 --- a/apps/web/src/types/aws-types.ts +++ b/apps/web/src/types/aws-types.ts @@ -28,8 +28,16 @@ export interface SesMail { } export interface SesBounce { - bounceType: string; - bounceSubType: string; + bounceType: "Transient" | "Permanent" | "Undetermined"; + bounceSubType: + | "General" + | "NoEmail" + | "Suppressed" + | "OnAccountSuppressionList " + | "MailboxFull" + | "MessageTooLarge" + | "ContentRejected" + | "AttachmentRejected"; bouncedRecipients: Array<{ emailAddress: string; action: string; @@ -94,7 +102,17 @@ export interface SesRenderingFailure { } export interface SesDeliveryDelay { - delayType: string; + delayType: + | "InternalFailure" + | "General" + | "MailboxFull" + | "SpamDetected" + | "RecipientServerError" + | "IPFailure" + | "TransientCommunicationFailure" + | "BYOIPHostNameLookupUnavailable" + | "Undetermined" + | "SendingDeferral"; expirationTime: string; delayedRecipients: string[]; timestamp: string; diff --git a/apps/web/src/types/index.ts b/apps/web/src/types/index.ts index 85ae5b7..141a88e 100644 --- a/apps/web/src/types/index.ts +++ b/apps/web/src/types/index.ts @@ -4,4 +4,8 @@ export type EmailContent = { subject: string; text?: string; html?: string; + attachments?: { + filename: string; + content: string; + }[]; }; diff --git a/packages/ui/package.json b/packages/ui/package.json index 1a1169d..5ce22ba 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -31,6 +31,8 @@ "@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-dropdown-menu": "^2.0.6", "@radix-ui/react-label": "^2.0.2", + "@radix-ui/react-select": "^2.0.0", + "@radix-ui/react-separator": "^1.0.3", "@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-switch": "^1.0.3", "@radix-ui/react-tabs": "^1.0.4", diff --git a/packages/ui/src/select.tsx b/packages/ui/src/select.tsx new file mode 100644 index 0000000..9c7fd46 --- /dev/null +++ b/packages/ui/src/select.tsx @@ -0,0 +1,160 @@ +"use client"; + +import * as React from "react"; +import * as SelectPrimitive from "@radix-ui/react-select"; +import { Check, ChevronDown, ChevronUp } from "lucide-react"; + +import { cn } from "../lib/utils"; + +const Select = SelectPrimitive.Root; + +const SelectGroup = SelectPrimitive.Group; + +const SelectValue = SelectPrimitive.Value; + +const SelectTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + span]:line-clamp-1", + className + )} + {...props} + > + {children} + + + + +)); +SelectTrigger.displayName = SelectPrimitive.Trigger.displayName; + +const SelectScrollUpButton = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)); +SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName; + +const SelectScrollDownButton = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)); +SelectScrollDownButton.displayName = + SelectPrimitive.ScrollDownButton.displayName; + +const SelectContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, position = "popper", ...props }, ref) => ( + + + + + {children} + + + + +)); +SelectContent.displayName = SelectPrimitive.Content.displayName; + +const SelectLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +SelectLabel.displayName = SelectPrimitive.Label.displayName; + +const SelectItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + + {children} + +)); +SelectItem.displayName = SelectPrimitive.Item.displayName; + +const SelectSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +SelectSeparator.displayName = SelectPrimitive.Separator.displayName; + +export { + Select, + SelectGroup, + SelectValue, + SelectTrigger, + SelectContent, + SelectLabel, + SelectItem, + SelectSeparator, + SelectScrollUpButton, + SelectScrollDownButton, +}; diff --git a/packages/ui/src/separator.tsx b/packages/ui/src/separator.tsx new file mode 100644 index 0000000..cb18c4f --- /dev/null +++ b/packages/ui/src/separator.tsx @@ -0,0 +1,31 @@ +"use client"; + +import * as React from "react"; +import * as SeparatorPrimitive from "@radix-ui/react-separator"; + +import { cn } from "../lib/utils"; + +const Separator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>( + ( + { className, orientation = "horizontal", decorative = true, ...props }, + ref + ) => ( + + ) +); +Separator.displayName = SeparatorPrimitive.Root.displayName; + +export { Separator }; diff --git a/packages/ui/styles/globals.css b/packages/ui/styles/globals.css index abc2f19..f21dba8 100644 --- a/packages/ui/styles/globals.css +++ b/packages/ui/styles/globals.css @@ -62,7 +62,7 @@ --border: 217.2 32.6% 17.5%; --input: 217.2 32.6% 17.5%; - --ring: 212.7 26.8% 83.9%; + --ring: 217.2 32.6% 17.5%; } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c717b6b..c201ecf 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -114,7 +114,7 @@ importers: version: 11.0.0-next-beta.318(@trpc/server@11.0.0-next-beta.318) '@trpc/next': specifier: next - version: 11.0.0-next-beta.318(@tanstack/react-query@5.28.4)(@trpc/client@11.0.0-next-beta.318)(@trpc/react-query@11.0.0-next-beta.318)(@trpc/server@11.0.0-next-beta.318)(next@14.1.3)(react-dom@18.2.0)(react@18.2.0) + version: 11.0.0-next-beta.318(@tanstack/react-query@5.28.4)(@trpc/client@11.0.0-next-beta.318)(@trpc/react-query@11.0.0-next-beta.318)(@trpc/server@11.0.0-next-beta.318)(next@14.2.1)(react-dom@18.2.0)(react@18.2.0) '@trpc/react-query': specifier: next version: 11.0.0-next-beta.318(@tanstack/react-query@5.28.4)(@trpc/client@11.0.0-next-beta.318)(@trpc/server@11.0.0-next-beta.318)(react-dom@18.2.0)(react@18.2.0) @@ -136,18 +136,24 @@ importers: lucide-react: specifier: ^0.359.0 version: 0.359.0(react@18.2.0) + mime-types: + specifier: ^2.1.35 + version: 2.1.35 next: - specifier: ^14.1.3 - version: 14.1.3(react-dom@18.2.0)(react@18.2.0) + specifier: ^14.2.1 + version: 14.2.1(react-dom@18.2.0)(react@18.2.0) next-auth: specifier: ^4.24.6 - version: 4.24.7(next@14.1.3)(react-dom@18.2.0)(react@18.2.0) + version: 4.24.7(next@14.2.1)(react-dom@18.2.0)(react@18.2.0) pnpm: specifier: ^8.15.5 version: 8.15.5 prisma: specifier: ^5.11.0 version: 5.11.0 + query-string: + specifier: ^9.0.0 + version: 9.0.0 react: specifier: 18.2.0 version: 18.2.0 @@ -173,6 +179,9 @@ importers: '@types/eslint': specifier: ^8.56.2 version: 8.56.5 + '@types/mime-types': + specifier: ^2.1.4 + version: 2.1.4 '@types/node': specifier: ^20.11.20 version: 20.11.27 @@ -275,6 +284,12 @@ importers: '@radix-ui/react-label': specifier: ^2.0.2 version: 2.0.2(@types/react-dom@18.2.22)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-select': + specifier: ^2.0.0 + version: 2.0.0(@types/react-dom@18.2.22)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-separator': + specifier: ^1.0.3 + version: 1.0.3(@types/react-dom@18.2.22)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-slot': specifier: ^1.0.2 version: 1.0.2(@types/react@18.2.66)(react@18.2.0) @@ -1588,14 +1603,14 @@ packages: resolution: {integrity: sha512-9b8mPpKrfeGRuhFH5iO1iwCLeIIsV6+H1sRfxbkoGXIyQE2BTsPd9zqSqQJ+pv5sJ/hT5M1zvOFL02MnEezFug==} dev: true - /@next/env@14.1.3: - resolution: {integrity: sha512-VhgXTvrgeBRxNPjyfBsDIMvgsKDxjlpw4IAUsHCX8Gjl1vtHUYRT3+xfQ/wwvLPDd/6kqfLqk9Pt4+7gysuCKQ==} - dev: false - /@next/env@14.1.4: resolution: {integrity: sha512-e7X7bbn3Z6DWnDi75UWn+REgAbLEqxI8Tq2pkFOFAMpWAWApz/YCUhtWMWn410h8Q2fYiYL7Yg5OlxMOCfFjJQ==} dev: false + /@next/env@14.2.1: + resolution: {integrity: sha512-qsHJle3GU3CmVx7pUoXcghX4sRN+vINkbLdH611T8ZlsP//grzqVW87BSUgOZeSAD4q7ZdZicdwNe/20U2janA==} + dev: false + /@next/eslint-plugin-next@14.1.3: resolution: {integrity: sha512-VCnZI2cy77Yaj3L7Uhs3+44ikMM1VD/fBMwvTBb3hIaTIuqa+DmG4dhUDq+MASu3yx97KhgsVJbsas0XuiKyww==} dependencies: @@ -1608,15 +1623,6 @@ packages: glob: 10.3.10 dev: true - /@next/swc-darwin-arm64@14.1.3: - resolution: {integrity: sha512-LALu0yIBPRiG9ANrD5ncB3pjpO0Gli9ZLhxdOu6ZUNf3x1r3ea1rd9Q+4xxUkGrUXLqKVK9/lDkpYIJaCJ6AHQ==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [darwin] - requiresBuild: true - dev: false - optional: true - /@next/swc-darwin-arm64@14.1.4: resolution: {integrity: sha512-ubmUkbmW65nIAOmoxT1IROZdmmJMmdYvXIe8211send9ZYJu+SqxSnJM4TrPj9wmL6g9Atvj0S/2cFmMSS99jg==} engines: {node: '>= 10'} @@ -1626,10 +1632,10 @@ packages: dev: false optional: true - /@next/swc-darwin-x64@14.1.3: - resolution: {integrity: sha512-E/9WQeXxkqw2dfcn5UcjApFgUq73jqNKaE5bysDm58hEUdUGedVrnRhblhJM7HbCZNhtVl0j+6TXsK0PuzXTCg==} + /@next/swc-darwin-arm64@14.2.1: + resolution: {integrity: sha512-kGjnjcIJehEcd3rT/3NAATJQndAEELk0J9GmGMXHSC75TMnvpOhONcjNHbjtcWE5HUQnIHy5JVkatrnYm1QhVw==} engines: {node: '>= 10'} - cpu: [x64] + cpu: [arm64] os: [darwin] requiresBuild: true dev: false @@ -1644,11 +1650,11 @@ packages: dev: false optional: true - /@next/swc-linux-arm64-gnu@14.1.3: - resolution: {integrity: sha512-USArX9B+3rZSXYLFvgy0NVWQgqh6LHWDmMt38O4lmiJNQcwazeI6xRvSsliDLKt+78KChVacNiwvOMbl6g6BBw==} + /@next/swc-darwin-x64@14.2.1: + resolution: {integrity: sha512-dAdWndgdQi7BK2WSXrx4lae7mYcOYjbHJUhvOUnJjMNYrmYhxbbvJ2xElZpxNxdfA6zkqagIB9He2tQk+l16ew==} engines: {node: '>= 10'} - cpu: [arm64] - os: [linux] + cpu: [x64] + os: [darwin] requiresBuild: true dev: false optional: true @@ -1662,8 +1668,8 @@ packages: dev: false optional: true - /@next/swc-linux-arm64-musl@14.1.3: - resolution: {integrity: sha512-esk1RkRBLSIEp1qaQXv1+s6ZdYzuVCnDAZySpa62iFTMGTisCyNQmqyCTL9P+cLJ4N9FKCI3ojtSfsyPHJDQNw==} + /@next/swc-linux-arm64-gnu@14.2.1: + resolution: {integrity: sha512-2ZctfnyFOGvTkoD6L+DtQtO3BfFz4CapoHnyLTXkOxbZkVRgg3TQBUjTD/xKrO1QWeydeo8AWfZRg8539qNKrg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] @@ -1680,10 +1686,10 @@ packages: dev: false optional: true - /@next/swc-linux-x64-gnu@14.1.3: - resolution: {integrity: sha512-8uOgRlYEYiKo0L8YGeS+3TudHVDWDjPVDUcST+z+dUzgBbTEwSSIaSgF/vkcC1T/iwl4QX9iuUyUdQEl0Kxalg==} + /@next/swc-linux-arm64-musl@14.2.1: + resolution: {integrity: sha512-jazZXctiaanemy4r+TPIpFP36t1mMwWCKMsmrTRVChRqE6putyAxZA4PDujx0SnfvZHosjdkx9xIq9BzBB5tWg==} engines: {node: '>= 10'} - cpu: [x64] + cpu: [arm64] os: [linux] requiresBuild: true dev: false @@ -1698,8 +1704,8 @@ packages: dev: false optional: true - /@next/swc-linux-x64-musl@14.1.3: - resolution: {integrity: sha512-DX2zqz05ziElLoxskgHasaJBREC5Y9TJcbR2LYqu4r7naff25B4iXkfXWfcp69uD75/0URmmoSgT8JclJtrBoQ==} + /@next/swc-linux-x64-gnu@14.2.1: + resolution: {integrity: sha512-VjCHWCjsAzQAAo8lkBOLEIkBZFdfW+Z18qcQ056kL4KpUYc8o59JhLDCBlhg+hINQRgzQ2UPGma2AURGOH0+Qg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] @@ -1716,11 +1722,11 @@ packages: dev: false optional: true - /@next/swc-win32-arm64-msvc@14.1.3: - resolution: {integrity: sha512-HjssFsCdsD4GHstXSQxsi2l70F/5FsRTRQp8xNgmQs15SxUfUJRvSI9qKny/jLkY3gLgiCR3+6A7wzzK0DBlfA==} + /@next/swc-linux-x64-musl@14.2.1: + resolution: {integrity: sha512-7HZKYKvAp4nAHiHIbY04finRqjeYvkITOGOurP1aLMexIFG/1+oCnqhGogBdc4lao/lkMW1c+AkwWSzSlLasqw==} engines: {node: '>= 10'} - cpu: [arm64] - os: [win32] + cpu: [x64] + os: [linux] requiresBuild: true dev: false optional: true @@ -1734,10 +1740,10 @@ packages: dev: false optional: true - /@next/swc-win32-ia32-msvc@14.1.3: - resolution: {integrity: sha512-DRuxD5axfDM1/Ue4VahwSxl1O5rn61hX8/sF0HY8y0iCbpqdxw3rB3QasdHn/LJ6Wb2y5DoWzXcz3L1Cr+Thrw==} + /@next/swc-win32-arm64-msvc@14.2.1: + resolution: {integrity: sha512-YGHklaJ/Cj/F0Xd8jxgj2p8po4JTCi6H7Z3Yics3xJhm9CPIqtl8erlpK1CLv+HInDqEWfXilqatF8YsLxxA2Q==} engines: {node: '>= 10'} - cpu: [ia32] + cpu: [arm64] os: [win32] requiresBuild: true dev: false @@ -1752,10 +1758,10 @@ packages: dev: false optional: true - /@next/swc-win32-x64-msvc@14.1.3: - resolution: {integrity: sha512-uC2DaDoWH7h1P/aJ4Fok3Xiw6P0Lo4ez7NbowW2VGNXw/Xv6tOuLUcxhBYZxsSUJtpeknCi8/fvnSpyCFp4Rcg==} + /@next/swc-win32-ia32-msvc@14.2.1: + resolution: {integrity: sha512-o+ISKOlvU/L43ZhtAAfCjwIfcwuZstiHVXq/BDsZwGqQE0h/81td95MPHliWCnFoikzWcYqh+hz54ZB2FIT8RA==} engines: {node: '>= 10'} - cpu: [x64] + cpu: [ia32] os: [win32] requiresBuild: true dev: false @@ -1770,6 +1776,15 @@ packages: dev: false optional: true + /@next/swc-win32-x64-msvc@14.2.1: + resolution: {integrity: sha512-GmRoTiLcvCLifujlisknv4zu9/C4i9r0ktsA8E51EMqJL4bD4CpO7lDYr7SrUxCR0tS4RVcrqKmCak24T0ohaw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: false + optional: true + /@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1: resolution: {integrity: sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg==} dependencies: @@ -1854,6 +1869,12 @@ packages: '@prisma/debug': 5.11.0 dev: false + /@radix-ui/number@1.0.1: + resolution: {integrity: sha512-T5gIdVO2mmPW3NNhjNgEP3cqMXjXL9UbO0BzWcXfvdBs+BohbQxvd/K5hSVKmn9/lbTdsQVKbUcP5WLCwvUbBg==} + dependencies: + '@babel/runtime': 7.24.0 + dev: false + /@radix-ui/primitive@1.0.1: resolution: {integrity: sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw==} dependencies: @@ -2267,6 +2288,68 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@radix-ui/react-select@2.0.0(@types/react-dom@18.2.22)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-RH5b7af4oHtkcHS7pG6Sgv5rk5Wxa7XI8W5gvB1N/yiuDGZxko1ynvOiVhFM7Cis2A8zxF9bTOUVbRDzPepe6w==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.24.0 + '@radix-ui/number': 1.0.1 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-collection': 1.0.3(@types/react-dom@18.2.22)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.66)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.66)(react@18.2.0) + '@radix-ui/react-direction': 1.0.1(@types/react@18.2.66)(react@18.2.0) + '@radix-ui/react-dismissable-layer': 1.0.5(@types/react-dom@18.2.22)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-focus-guards': 1.0.1(@types/react@18.2.66)(react@18.2.0) + '@radix-ui/react-focus-scope': 1.0.4(@types/react-dom@18.2.22)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-id': 1.0.1(@types/react@18.2.66)(react@18.2.0) + '@radix-ui/react-popper': 1.1.3(@types/react-dom@18.2.22)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-portal': 1.0.4(@types/react-dom@18.2.22)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.22)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-slot': 1.0.2(@types/react@18.2.66)(react@18.2.0) + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.66)(react@18.2.0) + '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.66)(react@18.2.0) + '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.2.66)(react@18.2.0) + '@radix-ui/react-use-previous': 1.0.1(@types/react@18.2.66)(react@18.2.0) + '@radix-ui/react-visually-hidden': 1.0.3(@types/react-dom@18.2.22)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0) + '@types/react': 18.2.66 + '@types/react-dom': 18.2.22 + aria-hidden: 1.2.4 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-remove-scroll: 2.5.5(@types/react@18.2.66)(react@18.2.0) + dev: false + + /@radix-ui/react-separator@1.0.3(@types/react-dom@18.2.22)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-itYmTy/kokS21aiV5+Z56MZB54KrhPgn6eHDKkFeOLR34HMN2s8PaN47qZZAGnvupcjxHaFZnW4pQEh0BvvVuw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.24.0 + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.22)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0) + '@types/react': 18.2.66 + '@types/react-dom': 18.2.22 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@radix-ui/react-slot@1.0.2(@types/react@18.2.66)(react@18.2.0): resolution: {integrity: sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==} peerDependencies: @@ -2439,6 +2522,27 @@ packages: react: 18.2.0 dev: false + /@radix-ui/react-visually-hidden@1.0.3(@types/react-dom@18.2.22)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-D4w41yN5YRKtu464TLnByKzMDG/JlMPHtfZgQAu9v6mNakUqGUI9vUrfQKz8NK41VMm/xbZbh76NUTVtIYqOMA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.24.0 + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.22)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0) + '@types/react': 18.2.66 + '@types/react-dom': 18.2.22 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@radix-ui/rect@1.0.1: resolution: {integrity: sha512-fyrgCaedtvMg9NK3en0pnOYJdtfwxUcNolezkNPUsoX57X8oQk+NkqcvzHXD2uKNij6GXmWU9NDru2IWjrO4BQ==} dependencies: @@ -2822,12 +2926,23 @@ packages: tslib: 2.6.2 dev: false + /@swc/counter@0.1.3: + resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} + dev: false + /@swc/helpers@0.5.2: resolution: {integrity: sha512-E4KcWTpoLHqwPHLxidpOqQbcrZVgi0rsmmZXUle1jXmJfuIf/UWpczUJ7MZZ5tlxytgJXyp0w4PGkkeLiuIdZw==} dependencies: tslib: 2.6.2 dev: false + /@swc/helpers@0.5.5: + resolution: {integrity: sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==} + dependencies: + '@swc/counter': 0.1.3 + tslib: 2.6.2 + dev: false + /@t3-oss/env-core@0.9.2(typescript@5.4.2)(zod@3.22.4): resolution: {integrity: sha512-KgWXljUTHgO3o7GMZQPAD5+P+HqpauMNNHowlm7V2b9IeMitSUpNKwG6xQrup/xARWHTdxRVIl0mSI4wCevQhQ==} peerDependencies: @@ -2876,7 +2991,7 @@ packages: '@trpc/server': 11.0.0-next-beta.318 dev: false - /@trpc/next@11.0.0-next-beta.318(@tanstack/react-query@5.28.4)(@trpc/client@11.0.0-next-beta.318)(@trpc/react-query@11.0.0-next-beta.318)(@trpc/server@11.0.0-next-beta.318)(next@14.1.3)(react-dom@18.2.0)(react@18.2.0): + /@trpc/next@11.0.0-next-beta.318(@tanstack/react-query@5.28.4)(@trpc/client@11.0.0-next-beta.318)(@trpc/react-query@11.0.0-next-beta.318)(@trpc/server@11.0.0-next-beta.318)(next@14.2.1)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-qeWfJ1vPm7GchLmMZz5Gj+mBka0CRci0bCKEhGoG8RSvI/+9GbbhZHKRRDnlsN81CgexJ2e2nULET9ESO6rt+Q==} peerDependencies: '@tanstack/react-query': ^5.25.0 @@ -2896,7 +3011,7 @@ packages: '@trpc/client': 11.0.0-next-beta.318(@trpc/server@11.0.0-next-beta.318) '@trpc/react-query': 11.0.0-next-beta.318(@tanstack/react-query@5.28.4)(@trpc/client@11.0.0-next-beta.318)(@trpc/server@11.0.0-next-beta.318)(react-dom@18.2.0)(react@18.2.0) '@trpc/server': 11.0.0-next-beta.318 - next: 14.1.3(react-dom@18.2.0)(react@18.2.0) + next: 14.2.1(react-dom@18.2.0)(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: false @@ -2992,6 +3107,10 @@ packages: resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} dev: true + /@types/mime-types@2.1.4: + resolution: {integrity: sha512-lfU4b34HOri+kAY5UheuFMWPDOI+OPceBSHZKp69gEyTL/mmJ4cnU6Y/rlme3UL3GyOn6Y42hyIEw0/q8sWx5w==} + dev: true + /@types/node@20.11.27: resolution: {integrity: sha512-qyUZfMnCg1KEz57r7pzFtSGt49f6RPkPBis3Vo4PbS7roQEDn22hiHzl/Lo1q4i4hDEgBJmBF/NTNg2XR0HbFg==} dependencies: @@ -3987,6 +4106,11 @@ packages: resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==} dev: false + /decode-uri-component@0.4.1: + resolution: {integrity: sha512-+8VxcR21HhTy8nOt6jf20w0c9CADrw1O8d+VZ/YzzCt4bJ3uBjw+D1q2osAB8RnpwwaeYBxy0HyKQxD5JBMuuQ==} + engines: {node: '>=14.16'} + dev: false + /deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} dev: true @@ -4892,6 +5016,11 @@ packages: dependencies: to-regex-range: 5.0.1 + /filter-obj@5.1.0: + resolution: {integrity: sha512-qWeTREPoT7I0bifpPUXtxkZJ1XJzxWtfoWWkdVGqa+eCr3SHW/Ocp89o8vLvbUuQnadybJpjOKu4V+RwO6sGng==} + engines: {node: '>=14.16'} + dev: false + /find-up@4.1.0: resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} engines: {node: '>=8'} @@ -5670,6 +5799,18 @@ packages: braces: 3.0.2 picomatch: 2.3.1 + /mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + dev: false + + /mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + dependencies: + mime-db: 1.52.0 + dev: false + /min-indent@1.0.1: resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} engines: {node: '>=4'} @@ -5719,7 +5860,7 @@ packages: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} dev: true - /next-auth@4.24.7(next@14.1.3)(react-dom@18.2.0)(react@18.2.0): + /next-auth@4.24.7(next@14.2.1)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-iChjE8ov/1K/z98gdKbn2Jw+2vLgJtVV39X+rCP5SGnVQuco7QOr19FRNGMIrD8d3LYhHWV9j9sKLzq1aDWWQQ==} peerDependencies: next: ^12.2.5 || ^13 || ^14 @@ -5734,7 +5875,7 @@ packages: '@panva/hkdf': 1.1.1 cookie: 0.5.0 jose: 4.15.5 - next: 14.1.3(react-dom@18.2.0)(react@18.2.0) + next: 14.2.1(react-dom@18.2.0)(react@18.2.0) oauth: 0.9.15 openid-client: 5.6.5 preact: 10.19.6 @@ -5754,45 +5895,6 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false - /next@14.1.3(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-oexgMV2MapI0UIWiXKkixF8J8ORxpy64OuJ/J9oVUmIthXOUCcuVEZX+dtpgq7wIfIqtBwQsKEDXejcjTsan9g==} - engines: {node: '>=18.17.0'} - hasBin: true - peerDependencies: - '@opentelemetry/api': ^1.1.0 - react: ^18.2.0 - react-dom: ^18.2.0 - sass: ^1.3.0 - peerDependenciesMeta: - '@opentelemetry/api': - optional: true - sass: - optional: true - dependencies: - '@next/env': 14.1.3 - '@swc/helpers': 0.5.2 - busboy: 1.6.0 - caniuse-lite: 1.0.30001597 - graceful-fs: 4.2.11 - postcss: 8.4.31 - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - styled-jsx: 5.1.1(react@18.2.0) - optionalDependencies: - '@next/swc-darwin-arm64': 14.1.3 - '@next/swc-darwin-x64': 14.1.3 - '@next/swc-linux-arm64-gnu': 14.1.3 - '@next/swc-linux-arm64-musl': 14.1.3 - '@next/swc-linux-x64-gnu': 14.1.3 - '@next/swc-linux-x64-musl': 14.1.3 - '@next/swc-win32-arm64-msvc': 14.1.3 - '@next/swc-win32-ia32-msvc': 14.1.3 - '@next/swc-win32-x64-msvc': 14.1.3 - transitivePeerDependencies: - - '@babel/core' - - babel-plugin-macros - dev: false - /next@14.1.4(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-1WTaXeSrUwlz/XcnhGTY7+8eiaFvdet5z9u3V2jb+Ek1vFo0VhHKSAIJvDWfQpttWjnyw14kBeq28TPq7bTeEQ==} engines: {node: '>=18.17.0'} @@ -5832,6 +5934,48 @@ packages: - babel-plugin-macros dev: false + /next@14.2.1(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-SF3TJnKdH43PMkCcErLPv+x/DY1YCklslk3ZmwaVoyUfDgHKexuKlf9sEfBQ69w+ue8jQ3msLb+hSj1T19hGag==} + engines: {node: '>=18.17.0'} + hasBin: true + peerDependencies: + '@opentelemetry/api': ^1.1.0 + '@playwright/test': ^1.41.2 + react: ^18.2.0 + react-dom: ^18.2.0 + sass: ^1.3.0 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + '@playwright/test': + optional: true + sass: + optional: true + dependencies: + '@next/env': 14.2.1 + '@swc/helpers': 0.5.5 + busboy: 1.6.0 + caniuse-lite: 1.0.30001597 + graceful-fs: 4.2.11 + postcss: 8.4.31 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + styled-jsx: 5.1.1(react@18.2.0) + optionalDependencies: + '@next/swc-darwin-arm64': 14.2.1 + '@next/swc-darwin-x64': 14.2.1 + '@next/swc-linux-arm64-gnu': 14.2.1 + '@next/swc-linux-arm64-musl': 14.2.1 + '@next/swc-linux-x64-gnu': 14.2.1 + '@next/swc-linux-x64-musl': 14.2.1 + '@next/swc-win32-arm64-msvc': 14.2.1 + '@next/swc-win32-ia32-msvc': 14.2.1 + '@next/swc-win32-x64-msvc': 14.2.1 + transitivePeerDependencies: + - '@babel/core' + - babel-plugin-macros + dev: false + /node-releases@2.0.14: resolution: {integrity: sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==} dev: true @@ -6324,6 +6468,15 @@ packages: engines: {node: '>=6'} dev: true + /query-string@9.0.0: + resolution: {integrity: sha512-4EWwcRGsO2H+yzq6ddHcVqkCQ2EFUSfDMEjF8ryp8ReymyZhIuaFRGLomeOQLkrzacMHoyky2HW0Qe30UbzkKw==} + engines: {node: '>=18'} + dependencies: + decode-uri-component: 0.4.1 + filter-obj: 5.1.0 + split-on-first: 3.0.0 + dev: false + /queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} @@ -6753,6 +6906,11 @@ packages: resolution: {integrity: sha512-sh8PWc/ftMqAAdFiBu6Fy6JUOYjqDJBJvIhpfDMyHrr0Rbp5liZqd4TjtQ/RgfLjKFZb+LMx5hpml5qOWy0qvg==} dev: true + /split-on-first@3.0.0: + resolution: {integrity: sha512-qxQJTx2ryR0Dw0ITYyekNQWpz6f8dGd7vffGNflQQ3Iqj9NJ6qiZ7ELpZsJ/QBhIVAiDfXdag3+Gp8RvWa62AA==} + engines: {node: '>=12'} + dev: false + /streamsearch@1.1.0: resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} engines: {node: '>=10.0.0'}