-
- {!apiKeysQuery.isLoading && apiKeysQuery.data?.length ? (
- apiKeysQuery.data?.map((apiKey) => (
-
-
{apiKey.name}
-
{apiKey.permission}
-
{apiKey.partialToken}
-
- ))
- ) : (
-
No API keys added
- )}
+
+
+
+
+ Name
+ Token
+ Permission
+ Last used
+ Created at
+
+
+
+ {apiKeysQuery.data?.map((apiKey) => (
+
+ {apiKey.name}
+ {apiKey.partialToken}
+ {apiKey.permission}
+
+ {apiKey.lastUsed
+ ? formatDistanceToNow(apiKey.lastUsed)
+ : "Never"}
+
+
+ {formatDistanceToNow(apiKey.createdAt, { addSuffix: true })}
+
+
+ ))}
+
+
);
diff --git a/apps/web/src/app/(dashboard)/dashboard/dashboard-chart.tsx b/apps/web/src/app/(dashboard)/dashboard/dashboard-chart.tsx
new file mode 100644
index 0000000..411217d
--- /dev/null
+++ b/apps/web/src/app/(dashboard)/dashboard/dashboard-chart.tsx
@@ -0,0 +1,92 @@
+import {
+ AreaChart,
+ Area,
+ XAxis,
+ YAxis,
+ CartesianGrid,
+ Tooltip,
+} from "recharts";
+
+const data = [
+ {
+ name: "Page A",
+ uv: 4000,
+ pv: 2400,
+ amt: 2400,
+ },
+ {
+ name: "Page B",
+ uv: 3000,
+ pv: 1398,
+ amt: 2210,
+ },
+ {
+ name: "Page C",
+ uv: 2000,
+ pv: 9800,
+ amt: 2290,
+ },
+ {
+ name: "Page D",
+ uv: 2780,
+ pv: 3908,
+ amt: 2000,
+ },
+ {
+ name: "Page E",
+ uv: 1890,
+ pv: 4800,
+ amt: 2181,
+ },
+ {
+ name: "Page F",
+ uv: 2390,
+ pv: 3800,
+ amt: 2500,
+ },
+ {
+ name: "Page G",
+ uv: 3490,
+ pv: 4300,
+ amt: 2100,
+ },
+];
+
+export default function DashboardChart() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/apps/web/src/app/(dashboard)/dashboard/page.tsx b/apps/web/src/app/(dashboard)/dashboard/page.tsx
index 1cc08a0..df34dd7 100644
--- a/apps/web/src/app/(dashboard)/dashboard/page.tsx
+++ b/apps/web/src/app/(dashboard)/dashboard/page.tsx
@@ -1,3 +1,5 @@
+"use client";
+
import Link from "next/link";
import {
Bell,
@@ -23,7 +25,15 @@ import {
} from "@unsend/ui/src/dropdown-menu";
import { Input } from "@unsend/ui/src/input";
import { Sheet, SheetContent, SheetTrigger } from "@unsend/ui/src/sheet";
+import DashboardChart from "./dashboard-chart";
export default function Dashboard() {
- return
Hello
;
+ return (
+
+ );
}
diff --git a/apps/web/src/app/(dashboard)/domains/[domainId]/delete-domain.tsx b/apps/web/src/app/(dashboard)/domains/[domainId]/delete-domain.tsx
index b37373a..0886be7 100644
--- a/apps/web/src/app/(dashboard)/domains/[domainId]/delete-domain.tsx
+++ b/apps/web/src/app/(dashboard)/domains/[domainId]/delete-domain.tsx
@@ -49,7 +49,7 @@ export const DeleteDomain: React.FC<{ domain: Domain }> = ({ domain }) => {
onOpenChange={(_open) => (_open !== open ? setOpen(_open) : null)}
>
-
diff --git a/apps/web/src/app/(dashboard)/domains/[domainId]/page.tsx b/apps/web/src/app/(dashboard)/domains/[domainId]/page.tsx
index 2cc6506..5d43d31 100644
--- a/apps/web/src/app/(dashboard)/domains/[domainId]/page.tsx
+++ b/apps/web/src/app/(dashboard)/domains/[domainId]/page.tsx
@@ -287,7 +287,7 @@ const DomainSettings: React.FC<{ domain: Domain }> = ({ domain }) => {
-
Danger
+
Danger
Deleting a domain will stop sending emails with this domain.
diff --git a/apps/web/src/app/(dashboard)/domains/add-domain.tsx b/apps/web/src/app/(dashboard)/domains/add-domain.tsx
index 60abc83..3c94a2d 100644
--- a/apps/web/src/app/(dashboard)/domains/add-domain.tsx
+++ b/apps/web/src/app/(dashboard)/domains/add-domain.tsx
@@ -15,6 +15,7 @@ import {
import { api } from "~/trpc/react";
import { useState } from "react";
+import { Plus } from "lucide-react";
export default function AddDomain() {
const [open, setOpen] = useState(false);
@@ -43,7 +44,10 @@ export default function AddDomain() {
onOpenChange={(_open) => (_open !== open ? setOpen(_open) : null)}
>
- Add domain
+
+
+ Add domain
+
diff --git a/apps/web/src/app/(dashboard)/domains/domain-list.tsx b/apps/web/src/app/(dashboard)/domains/domain-list.tsx
index f2cc993..407654a 100644
--- a/apps/web/src/app/(dashboard)/domains/domain-list.tsx
+++ b/apps/web/src/app/(dashboard)/domains/domain-list.tsx
@@ -20,7 +20,7 @@ export default function DomainsList() {
))
) : (
- No domains
+ No domains Added
)}
diff --git a/apps/web/src/app/(dashboard)/emails/email-list.tsx b/apps/web/src/app/(dashboard)/emails/email-list.tsx
index 57d6091..7adb015 100644
--- a/apps/web/src/app/(dashboard)/emails/email-list.tsx
+++ b/apps/web/src/app/(dashboard)/emails/email-list.tsx
@@ -21,6 +21,7 @@ import {
MailX,
} from "lucide-react";
import { formatDistance, formatDistanceToNow } from "date-fns";
+import { EmailStatus } from "@prisma/client";
export default function EmailsList() {
const emailsQuery = api.email.emails.useQuery();
@@ -65,40 +66,40 @@ export default function EmailsList() {
);
}
-const EmailIcon: React.FC<{ status: string }> = ({ status }) => {
+const EmailIcon: React.FC<{ status: EmailStatus }> = ({ status }) => {
switch (status) {
- case "Send":
+ case "SENT":
return (
//
@@ -113,26 +114,27 @@ const EmailIcon: React.FC<{ status: string }> = ({ status }) => {
}
};
-const EmailStatusBadge: React.FC<{ status: string }> = ({ 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 "Send":
+ case "SENT":
badgeColor = "bg-gray-400/10 text-gray-500 border-gray-400/10";
break;
- case "Delivery":
- case "Delayed":
+ case "DELIVERED":
badgeColor = "bg-emerald-500/10 text-emerald-500 border-emerald-600/10";
break;
- case "Bounced":
+ case "BOUNCED":
badgeColor = "bg-red-500/10 text-red-800 border-red-600/10";
break;
- case "Clicked":
+ case "CLICKED":
badgeColor = "bg-cyan-500/10 text-cyan-600 border-cyan-600/10";
break;
- case "Opened":
+ case "OPENED":
badgeColor = "bg-indigo-500/10 text-indigo-600 border-indigo-600/10";
break;
- case "Complained":
+ 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:
@@ -141,9 +143,9 @@ const EmailStatusBadge: React.FC<{ status: string }> = ({ status }) => {
return (
- {status}
+ {status.toLowerCase().split("_").join(" ")}
);
};
diff --git a/apps/web/src/app/(dashboard)/layout.tsx b/apps/web/src/app/(dashboard)/layout.tsx
index 66dd980..0419194 100644
--- a/apps/web/src/app/(dashboard)/layout.tsx
+++ b/apps/web/src/app/(dashboard)/layout.tsx
@@ -2,6 +2,7 @@ import Link from "next/link";
import { redirect } from "next/navigation";
import {
Bell,
+ BellRing,
CircleUser,
Globe,
Home,
@@ -10,6 +11,7 @@ import {
LineChart,
Mail,
Menu,
+ MessageSquareMore,
Package,
Package2,
Search,
@@ -85,6 +87,16 @@ export default async function AuthenticatedDashboardLayout({
Domains
+
+
+ SMS
+
+
+
+
+ Push notification
+
+
API keys
diff --git a/apps/web/src/app/(dashboard)/nav-button.tsx b/apps/web/src/app/(dashboard)/nav-button.tsx
index 3855092..fadbc88 100644
--- a/apps/web/src/app/(dashboard)/nav-button.tsx
+++ b/apps/web/src/app/(dashboard)/nav-button.tsx
@@ -7,15 +7,31 @@ import React from "react";
export const NavButton: React.FC<{
href: string;
children: React.ReactNode;
-}> = ({ href, children }) => {
+ comingSoon?: boolean;
+}> = ({ href, children, comingSoon }) => {
const pathname = usePathname();
const isActive = pathname?.startsWith(href);
+ if (comingSoon) {
+ return (
+
+
+ {children}
+
+
+ soon
+
+
+ );
+ }
+
return (
{children}
diff --git a/apps/web/src/app/api/ses_callback/route.ts b/apps/web/src/app/api/ses_callback/route.ts
index f226bd1..1e08af0 100644
--- a/apps/web/src/app/api/ses_callback/route.ts
+++ b/apps/web/src/app/api/ses_callback/route.ts
@@ -1,6 +1,7 @@
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) {
console.log("GET", req);
@@ -10,67 +11,26 @@ export async function GET(req: Request) {
export async function POST(req: Request) {
const data = await req.json();
+ console.log(data, data.Message);
+
if (data.Type === "SubscriptionConfirmation") {
return handleSubscription(data);
}
- console.log(data, data.Message);
-
let message = null;
try {
message = JSON.parse(data.Message || "{}");
+ const status = await parseSesHook(message);
+ if (!status) {
+ return Response.json({ data: "Error is parsing hook" }, { status: 400 });
+ }
+
+ return Response.json({ data: "Success" });
} catch (e) {
- console.log(e);
+ console.error(e);
+ return Response.json({ data: "Error is parsing hook" }, { status: 400 });
}
-
- const emailId = message?.mail.messageId;
-
- console.log(emailId, message);
-
- if (!emailId) {
- return Response.json({ data: "Email not found" });
- }
-
- const email = await db.email.findUnique({
- where: {
- id: emailId,
- },
- });
-
- if (!email || !message.mail) {
- return Response.json({ data: "Email not found" });
- }
-
- console.log("FOund email", email);
-
- await db.email.update({
- where: {
- id: email.id,
- },
- data: {
- latestStatus: message.eventType,
- },
- });
-
- await db.emailEvent.upsert({
- where: {
- emailId_status: {
- emailId,
- status: message.eventType,
- },
- },
- update: {
- data: message[message.eventType.toLowerCase()],
- },
- create: {
- emailId,
- status: message.eventType,
- data: message[message.eventType.toLowerCase()],
- },
- });
-
- return Response.json({ data: "Hello" });
}
async function handleSubscription(message: any) {
@@ -78,5 +38,5 @@ async function handleSubscription(message: any) {
method: "GET",
});
- return Response.json({ data: "Hello" });
+ return Response.json({ data: "Success" });
}
diff --git a/apps/web/src/server/api/routers/api.ts b/apps/web/src/server/api/routers/api.ts
index 5145d16..27245e9 100644
--- a/apps/web/src/server/api/routers/api.ts
+++ b/apps/web/src/server/api/routers/api.ts
@@ -34,6 +34,7 @@ export const apiRouter = createTRPCRouter({
permission: true,
partialToken: true,
lastUsed: true,
+ createdAt: true,
},
});
diff --git a/apps/web/src/server/ses.ts b/apps/web/src/server/aws/ses.ts
similarity index 100%
rename from apps/web/src/server/ses.ts
rename to apps/web/src/server/aws/ses.ts
diff --git a/apps/web/src/server/aws/setup.ts b/apps/web/src/server/aws/setup.ts
index 24d664c..7044d33 100644
--- a/apps/web/src/server/aws/setup.ts
+++ b/apps/web/src/server/aws/setup.ts
@@ -4,7 +4,7 @@ import { APP_SETTINGS } from "~/utils/constants";
import { createTopic, subscribeEndpoint } from "./sns";
import { env } from "~/env";
import { AppSettingsService } from "~/server/service/app-settings-service";
-import { addWebhookConfiguration } from "../ses";
+import { addWebhookConfiguration } from "./ses";
import { EventType } from "@aws-sdk/client-sesv2";
const GENERAL_EVENTS: EventType[] = [
diff --git a/apps/web/src/server/service/domain-service.ts b/apps/web/src/server/service/domain-service.ts
index ead67c5..e968d48 100644
--- a/apps/web/src/server/service/domain-service.ts
+++ b/apps/web/src/server/service/domain-service.ts
@@ -1,7 +1,7 @@
import dns from "dns";
import util from "util";
import * as tldts from "tldts";
-import * as ses from "~/server/ses";
+import * as ses from "~/server/aws/ses";
import { db } from "~/server/db";
const dnsResolveTxt = util.promisify(dns.resolveTxt);
diff --git a/apps/web/src/server/service/email-service.ts b/apps/web/src/server/service/email-service.ts
index 267097c..d68a31f 100644
--- a/apps/web/src/server/service/email-service.ts
+++ b/apps/web/src/server/service/email-service.ts
@@ -1,6 +1,6 @@
import { EmailContent } from "~/types";
import { db } from "../db";
-import { sendEmailThroughSes } from "../ses";
+import { sendEmailThroughSes } from "../aws/ses";
import { APP_SETTINGS } from "~/utils/constants";
export async function sendEmail(
@@ -45,7 +45,7 @@ export async function sendEmail(
subject,
text,
html,
- id: messageId,
+ sesEmailId: messageId,
teamId,
domainId: domain.id,
},
diff --git a/apps/web/src/server/service/ses-hook-parser.ts b/apps/web/src/server/service/ses-hook-parser.ts
new file mode 100644
index 0000000..316605e
--- /dev/null
+++ b/apps/web/src/server/service/ses-hook-parser.ts
@@ -0,0 +1,91 @@
+import { EmailStatus } from "@prisma/client";
+import { SesEvent, SesEventDataKey, SesEventType } from "~/types/aws-types";
+import { db } from "../db";
+
+export async function parseSesHook(data: SesEvent) {
+ const mailStatus = getEmailStatus(data);
+
+ if (!mailStatus) {
+ console.error("Unknown email status", data);
+ return false;
+ }
+
+ const sesEmailId = data.mail.messageId;
+
+ const mailData = getEmailData(data);
+
+ const email = await db.email.findUnique({
+ where: {
+ sesEmailId,
+ },
+ });
+
+ if (!email) {
+ console.error("Email not found", data);
+ return false;
+ }
+
+ await db.email.update({
+ where: {
+ id: email.id,
+ },
+ data: {
+ latestStatus: mailStatus,
+ },
+ });
+
+ await db.emailEvent.upsert({
+ where: {
+ emailId_status: {
+ emailId: email.id,
+ status: mailStatus,
+ },
+ },
+ update: {
+ data: mailData as any,
+ },
+ create: {
+ emailId: email.id,
+ status: mailStatus,
+ data: mailData as any,
+ },
+ });
+
+ return true;
+}
+
+function getEmailStatus(data: SesEvent) {
+ const { eventType } = data;
+
+ if (eventType === "Send") {
+ return EmailStatus.SENT;
+ } else if (eventType === "Delivery") {
+ return EmailStatus.DELIVERED;
+ } else if (eventType === "Bounce") {
+ return EmailStatus.BOUNCED;
+ } else if (eventType === "Complaint") {
+ return EmailStatus.COMPLAINED;
+ } else if (eventType === "Reject") {
+ return EmailStatus.REJECTED;
+ } else if (eventType === "Open") {
+ return EmailStatus.OPENED;
+ } else if (eventType === "Click") {
+ return EmailStatus.CLICKED;
+ } else if (eventType === "Rendering Failure") {
+ return EmailStatus.RENDERING_FAILURE;
+ } else if (eventType === "DeliveryDelay") {
+ return EmailStatus.DELIVERY_DELAYED;
+ }
+}
+
+function getEmailData(data: SesEvent) {
+ const { eventType } = data;
+
+ if (eventType === "Rendering Failure") {
+ return data.renderingFailure;
+ } else if (eventType === "DeliveryDelay") {
+ return data.deliveryDelay;
+ } else {
+ return data[eventType.toLowerCase() as SesEventDataKey];
+ }
+}
diff --git a/apps/web/src/types/aws-types.ts b/apps/web/src/types/aws-types.ts
new file mode 100644
index 0000000..4549502
--- /dev/null
+++ b/apps/web/src/types/aws-types.ts
@@ -0,0 +1,138 @@
+export interface SnsNotificationMessage {
+ Type: string;
+ MessageId: string;
+ TopicArn: string;
+ Subject?: string;
+ Message: string; // This is a JSON string that needs to be parsed into one of the SES event types below
+ Timestamp: string;
+ SignatureVersion: string;
+ Signature: string;
+ SigningCertURL: string;
+ UnsubscribeURL: string;
+}
+
+export interface SesMail {
+ timestamp: string;
+ source: string;
+ messageId: string;
+ destination: string[];
+ headersTruncated: boolean;
+ headers: Array<{ name: string; value: string }>;
+ commonHeaders: {
+ from: string[];
+ to: string[];
+ messageId: string;
+ subject?: string;
+ };
+ tags: { [key: string]: string[] };
+}
+
+export interface SesBounce {
+ bounceType: string;
+ bounceSubType: string;
+ bouncedRecipients: Array<{
+ emailAddress: string;
+ action: string;
+ status: string;
+ diagnosticCode?: string;
+ }>;
+ timestamp: string;
+ feedbackId: string;
+ reportingMTA: string;
+}
+
+export interface SesComplaint {
+ complainedRecipients: Array<{
+ emailAddress: string;
+ }>;
+ timestamp: string;
+ feedbackId: string;
+ complaintFeedbackType: string;
+ userAgent: string;
+ complaintSubType?: string;
+ arrivedDate?: string;
+}
+
+export interface SesDelivery {
+ timestamp: string;
+ processingTimeMillis: number;
+ recipients: string[];
+ smtpResponse: string;
+ reportingMTA: string;
+ remoteMtaIp?: string;
+}
+
+export interface SesSend {
+ timestamp: string;
+ smtpResponse: string;
+ reportingMTA: string;
+ recipients: string[];
+}
+
+export interface SesReject {
+ reason: string;
+ timestamp: string;
+}
+
+export interface SesOpen {
+ ipAddress: string;
+ timestamp: string;
+ userAgent: string;
+}
+
+export interface SesClick {
+ ipAddress: string;
+ timestamp: string;
+ userAgent: string;
+ link: string;
+ linkTags: { [key: string]: string };
+}
+
+export interface SesRenderingFailure {
+ errorMessage: string;
+ templateName: string;
+}
+
+export interface SesDeliveryDelay {
+ delayType: string;
+ expirationTime: string;
+ delayedRecipients: string[];
+ timestamp: string;
+}
+
+export type SesEventType =
+ | "Bounce"
+ | "Complaint"
+ | "Delivery"
+ | "Send"
+ | "Reject"
+ | "Open"
+ | "Click"
+ | "Rendering Failure"
+ | "DeliveryDelay";
+
+export type SesEventDataKey =
+ | "bounce"
+ | "complaint"
+ | "delivery"
+ | "send"
+ | "reject"
+ | "open"
+ | "click"
+ | "renderingFailure"
+ | "deliveryDelay";
+
+export interface SesEvent {
+ eventType: SesEventType;
+ mail: SesMail;
+ bounce?: SesBounce;
+ complaint?: SesComplaint;
+ delivery?: SesDelivery;
+ send?: SesSend;
+ reject?: SesReject;
+ open?: SesOpen;
+ click?: SesClick;
+ renderingFailure?: SesRenderingFailure;
+ deliveryDelay?: SesDeliveryDelay;
+ // Additional fields for other event types can be added here
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 84dd738..c717b6b 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -154,6 +154,9 @@ importers:
react-dom:
specifier: 18.2.0
version: 18.2.0(react@18.2.0)
+ recharts:
+ specifier: ^2.12.5
+ version: 2.12.5(react-dom@18.2.0)(react@18.2.0)
server-only:
specifier: ^0.0.1
version: 0.0.1
@@ -2922,6 +2925,48 @@ packages:
resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==}
dev: false
+ /@types/d3-array@3.2.1:
+ resolution: {integrity: sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==}
+ dev: false
+
+ /@types/d3-color@3.1.3:
+ resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==}
+ dev: false
+
+ /@types/d3-ease@3.0.2:
+ resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==}
+ dev: false
+
+ /@types/d3-interpolate@3.0.4:
+ resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==}
+ dependencies:
+ '@types/d3-color': 3.1.3
+ dev: false
+
+ /@types/d3-path@3.1.0:
+ resolution: {integrity: sha512-P2dlU/q51fkOc/Gfl3Ul9kicV7l+ra934qBFXCFhrZMOL6du1TM0pm1ThYvENukyOn5h9v+yMJ9Fn5JK4QozrQ==}
+ dev: false
+
+ /@types/d3-scale@4.0.8:
+ resolution: {integrity: sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ==}
+ dependencies:
+ '@types/d3-time': 3.0.3
+ dev: false
+
+ /@types/d3-shape@3.1.6:
+ resolution: {integrity: sha512-5KKk5aKGu2I+O6SONMYSNflgiP0WfZIQvVUMan50wHsLG1G94JlxEVnCpQARfTtzytuY0p/9PXXZb3I7giofIA==}
+ dependencies:
+ '@types/d3-path': 3.1.0
+ dev: false
+
+ /@types/d3-time@3.0.3:
+ resolution: {integrity: sha512-2p6olUZ4w3s+07q3Tm2dbiMZy5pCDfYwtLXXHUnVzXgQlZ/OyPtUz6OL382BkOuGlLXqfT+wqv8Fw2v8/0geBw==}
+ dev: false
+
+ /@types/d3-timer@3.0.2:
+ resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==}
+ dev: false
+
/@types/eslint@8.56.5:
resolution: {integrity: sha512-u5/YPJHo1tvkSF2CE0USEkxon82Z5DBy2xR+qfyYNszpX9qcs4sT6uq2kBbj4BXY1+DBGDPnrhMZV3pKWGNukw==}
dependencies:
@@ -3836,6 +3881,77 @@ packages:
/csstype@3.1.3:
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
+ /d3-array@3.2.4:
+ resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==}
+ engines: {node: '>=12'}
+ dependencies:
+ internmap: 2.0.3
+ dev: false
+
+ /d3-color@3.1.0:
+ resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==}
+ engines: {node: '>=12'}
+ dev: false
+
+ /d3-ease@3.0.1:
+ resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==}
+ engines: {node: '>=12'}
+ dev: false
+
+ /d3-format@3.1.0:
+ resolution: {integrity: sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==}
+ engines: {node: '>=12'}
+ dev: false
+
+ /d3-interpolate@3.0.1:
+ resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==}
+ engines: {node: '>=12'}
+ dependencies:
+ d3-color: 3.1.0
+ dev: false
+
+ /d3-path@3.1.0:
+ resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==}
+ engines: {node: '>=12'}
+ dev: false
+
+ /d3-scale@4.0.2:
+ resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==}
+ engines: {node: '>=12'}
+ dependencies:
+ d3-array: 3.2.4
+ d3-format: 3.1.0
+ d3-interpolate: 3.0.1
+ d3-time: 3.1.0
+ d3-time-format: 4.1.0
+ dev: false
+
+ /d3-shape@3.2.0:
+ resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==}
+ engines: {node: '>=12'}
+ dependencies:
+ d3-path: 3.1.0
+ dev: false
+
+ /d3-time-format@4.1.0:
+ resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==}
+ engines: {node: '>=12'}
+ dependencies:
+ d3-time: 3.1.0
+ dev: false
+
+ /d3-time@3.1.0:
+ resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==}
+ engines: {node: '>=12'}
+ dependencies:
+ d3-array: 3.2.4
+ dev: false
+
+ /d3-timer@3.0.1:
+ resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==}
+ engines: {node: '>=12'}
+ dev: false
+
/damerau-levenshtein@1.0.8:
resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==}
dev: true
@@ -3867,6 +3983,10 @@ packages:
ms: 2.1.2
dev: true
+ /decimal.js-light@2.5.1:
+ resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==}
+ dev: false
+
/deep-is@0.1.4:
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
dev: true
@@ -3935,6 +4055,13 @@ packages:
esutils: 2.0.3
dev: true
+ /dom-helpers@5.2.1:
+ resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==}
+ dependencies:
+ '@babel/runtime': 7.24.0
+ csstype: 3.1.3
+ dev: false
+
/dotenv-cli@7.4.1:
resolution: {integrity: sha512-fE1aywjRrWGxV3miaiUr3d2zC/VAiuzEGghi+QzgIA9fEf/M5hLMaRSXb4IxbUAwGmaLi0IozdZddnVU96acag==}
hasBin: true
@@ -4208,7 +4335,7 @@ packages:
enhanced-resolve: 5.16.0
eslint: 8.57.0
eslint-module-utils: 2.8.1(@typescript-eslint/parser@6.21.0)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0)
- eslint-plugin-import: 2.29.1(eslint@8.57.0)
+ eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.2.0)(eslint@8.57.0)
fast-glob: 3.3.2
get-tsconfig: 4.7.3
is-core-module: 2.13.1
@@ -4703,10 +4830,19 @@ packages:
engines: {node: '>=0.10.0'}
dev: true
+ /eventemitter3@4.0.7:
+ resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==}
+ dev: false
+
/fast-deep-equal@3.1.3:
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
dev: true
+ /fast-equals@5.0.1:
+ resolution: {integrity: sha512-WF1Wi8PwwSY7/6Kx0vKXtw8RwuSGoM1bvDaJbu7MxDlR1vovZjIAKrnzyrThgAjm6JDTu0fVgWXDlMGspodfoQ==}
+ engines: {node: '>=6.0.0'}
+ dev: false
+
/fast-glob@3.3.2:
resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==}
engines: {node: '>=8.6.0'}
@@ -5106,6 +5242,11 @@ packages:
side-channel: 1.0.6
dev: true
+ /internmap@2.0.3:
+ resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==}
+ engines: {node: '>=12'}
+ dev: false
+
/invariant@2.2.4:
resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==}
dependencies:
@@ -5480,7 +5621,6 @@ packages:
/lodash@4.17.21:
resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
- dev: true
/loose-envify@1.4.0:
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
@@ -6172,7 +6312,6 @@ packages:
loose-envify: 1.4.0
object-assign: 4.1.1
react-is: 16.13.1
- dev: true
/property-information@5.6.0:
resolution: {integrity: sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA==}
@@ -6200,7 +6339,6 @@ packages:
/react-is@16.13.1:
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
- dev: true
/react-remove-scroll-bar@2.3.6(@types/react@18.2.66)(react@18.2.0):
resolution: {integrity: sha512-DtSYaao4mBmX+HDo5YWYdBWQwYIQQshUV/dVxFxK+KM26Wjwp1gZ6rv6OC3oujI6Bfu6Xyg3TwK533AQutsn/g==}
@@ -6237,6 +6375,19 @@ packages:
use-sidecar: 1.1.2(@types/react@18.2.66)(react@18.2.0)
dev: false
+ /react-smooth@4.0.1(react-dom@18.2.0)(react@18.2.0):
+ resolution: {integrity: sha512-OE4hm7XqR0jNOq3Qmk9mFLyd6p2+j6bvbPJ7qlB7+oo0eNcL2l7WQzG6MBnT3EXY6xzkLMUBec3AfewJdA0J8w==}
+ peerDependencies:
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0
+ react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
+ dependencies:
+ fast-equals: 5.0.1
+ prop-types: 15.8.1
+ react: 18.2.0
+ react-dom: 18.2.0(react@18.2.0)
+ react-transition-group: 4.4.5(react-dom@18.2.0)(react@18.2.0)
+ dev: false
+
/react-style-singleton@2.2.1(@types/react@18.2.66)(react@18.2.0):
resolution: {integrity: sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==}
engines: {node: '>=10'}
@@ -6267,6 +6418,20 @@ packages:
refractor: 3.6.0
dev: false
+ /react-transition-group@4.4.5(react-dom@18.2.0)(react@18.2.0):
+ resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==}
+ peerDependencies:
+ react: '>=16.6.0'
+ react-dom: '>=16.6.0'
+ dependencies:
+ '@babel/runtime': 7.24.0
+ dom-helpers: 5.2.1
+ loose-envify: 1.4.0
+ prop-types: 15.8.1
+ react: 18.2.0
+ react-dom: 18.2.0(react@18.2.0)
+ dev: false
+
/react@18.2.0:
resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==}
engines: {node: '>=0.10.0'}
@@ -6303,6 +6468,31 @@ packages:
dependencies:
picomatch: 2.3.1
+ /recharts-scale@0.4.5:
+ resolution: {integrity: sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==}
+ dependencies:
+ decimal.js-light: 2.5.1
+ dev: false
+
+ /recharts@2.12.5(react-dom@18.2.0)(react@18.2.0):
+ resolution: {integrity: sha512-Cy+BkqrFIYTHJCyKHJEPvbHE2kVQEP6PKbOHJ8ztRGTAhvHuUnCwDaKVb13OwRFZ0QNUk1QvGTDdgWSMbuMtKw==}
+ engines: {node: '>=14'}
+ peerDependencies:
+ react: ^16.0.0 || ^17.0.0 || ^18.0.0
+ react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0
+ dependencies:
+ clsx: 2.1.0
+ eventemitter3: 4.0.7
+ lodash: 4.17.21
+ react: 18.2.0
+ react-dom: 18.2.0(react@18.2.0)
+ react-is: 16.13.1
+ react-smooth: 4.0.1(react-dom@18.2.0)(react@18.2.0)
+ recharts-scale: 0.4.5
+ tiny-invariant: 1.3.3
+ victory-vendor: 36.9.2
+ dev: false
+
/reflect.getprototypeof@1.0.5:
resolution: {integrity: sha512-62wgfC8dJWrmxv44CA36pLDnP6KKl3Vhxb7PL+8+qrrFMMoJij4vgiMP8zV4O8+CBMXY1mHxI5fITGHXFHVmQQ==}
engines: {node: '>= 0.4'}
@@ -6783,6 +6973,10 @@ packages:
dependencies:
any-promise: 1.3.0
+ /tiny-invariant@1.3.3:
+ resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==}
+ dev: false
+
/tldts-core@6.1.16:
resolution: {integrity: sha512-rxnuCux+zn3hMF57nBzr1m1qGZH7Od2ErbDZjVm04fk76cEynTg3zqvHjx5BsBl8lvRTjpzIhsEGMHDH/Hr2Vw==}
dev: false
@@ -7049,6 +7243,25 @@ packages:
spdx-expression-parse: 3.0.1
dev: true
+ /victory-vendor@36.9.2:
+ resolution: {integrity: sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==}
+ dependencies:
+ '@types/d3-array': 3.2.1
+ '@types/d3-ease': 3.0.2
+ '@types/d3-interpolate': 3.0.4
+ '@types/d3-scale': 4.0.8
+ '@types/d3-shape': 3.1.6
+ '@types/d3-time': 3.0.3
+ '@types/d3-timer': 3.0.2
+ d3-array: 3.2.4
+ d3-ease: 3.0.1
+ d3-interpolate: 3.0.1
+ d3-scale: 4.0.2
+ d3-shape: 3.2.0
+ d3-time: 3.1.0
+ d3-timer: 3.0.1
+ dev: false
+
/which-boxed-primitive@1.0.2:
resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==}
dependencies: