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 (
+
+ 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
-
+
+
+
+
+
@@ -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.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 (
}> */}
+ {/* */}
);
}
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
@@ -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'}