Add UI improvements
This commit is contained in:
@@ -27,6 +27,7 @@
|
||||
"@trpc/react-query": "next",
|
||||
"@trpc/server": "next",
|
||||
"@unsend/ui": "workspace:*",
|
||||
"date-fns": "^3.6.0",
|
||||
"install": "^0.13.0",
|
||||
"lucide-react": "^0.359.0",
|
||||
"next": "^14.1.3",
|
||||
|
@@ -101,17 +101,19 @@ enum DomainStatus {
|
||||
}
|
||||
|
||||
model Domain {
|
||||
id Int @id @default(autoincrement())
|
||||
name String @unique
|
||||
teamId Int
|
||||
status DomainStatus @default(PENDING)
|
||||
region String @default("us-east-1")
|
||||
publicKey String
|
||||
dkimStatus String?
|
||||
spfDetails String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
team Team @relation(fields: [teamId], references: [id], onDelete: Cascade)
|
||||
id Int @id @default(autoincrement())
|
||||
name String @unique
|
||||
teamId Int
|
||||
status DomainStatus @default(PENDING)
|
||||
region String @default("us-east-1")
|
||||
clickTracking Boolean @default(false)
|
||||
openTracking Boolean @default(false)
|
||||
publicKey String
|
||||
dkimStatus String?
|
||||
spfDetails String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
team Team @relation(fields: [teamId], references: [id], onDelete: Cascade)
|
||||
}
|
||||
|
||||
enum ApiPermission {
|
||||
|
@@ -1,6 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import { DomainStatus } from "@prisma/client";
|
||||
import { formatDistanceToNow } from "date-fns";
|
||||
import Link from "next/link";
|
||||
import { Switch } from "@unsend/ui/src/switch";
|
||||
import { api } from "~/trpc/react";
|
||||
|
||||
export default function DomainsList() {
|
||||
@@ -8,15 +11,54 @@ export default function DomainsList() {
|
||||
|
||||
return (
|
||||
<div className="mt-10">
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex flex-col gap-6">
|
||||
{!domainsQuery.isLoading && domainsQuery.data?.length ? (
|
||||
domainsQuery.data?.map((domain) => (
|
||||
<Link key={domain.id} href={`/domains/${domain.id}`}>
|
||||
<div className="p-2 px-4 border rounded-lg flex justify-between">
|
||||
<p>{domain.name}</p>
|
||||
<p className=" capitalize">{domain.status.toLowerCase()}</p>
|
||||
<div key={domain.id}>
|
||||
<div className=" pr-8 border rounded-lg flex items-stretch">
|
||||
<StatusIndicator status={domain.status} />
|
||||
<div className="flex justify-between w-full pl-8 py-4">
|
||||
<div className="flex flex-col gap-4 w-1/5">
|
||||
<Link
|
||||
href={`/domains/${domain.id}`}
|
||||
className="text-lg font-medium underline underline-offset-4 decoration-dashed"
|
||||
>
|
||||
{domain.name}
|
||||
</Link>
|
||||
<DomainStatusBadge status={domain.status} />
|
||||
</div>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Created at
|
||||
</p>
|
||||
<p className="text-sm">
|
||||
{formatDistanceToNow(new Date(domain.createdAt), {
|
||||
addSuffix: true,
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">Region</p>
|
||||
|
||||
<p className="text-sm flex items-center gap-2">
|
||||
<span className="text-2xl">🇺🇸</span> {domain.region}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="flex gap-2 items-center">
|
||||
<p className="text-sm">Click tracking</p>
|
||||
<Switch className="data-[state=checked]:bg-emerald-500" />
|
||||
</div>
|
||||
<div className="flex gap-2 items-center">
|
||||
<p className="text-sm">Open tracking</p>
|
||||
<Switch className="data-[state=checked]:bg-emerald-500" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div>No domains</div>
|
||||
@@ -25,3 +67,55 @@ export default function DomainsList() {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const DomainStatusBadge: React.FC<{ status: DomainStatus }> = ({ status }) => {
|
||||
let badgeColor = "bg-gray-400/10 text-gray-500 border-gray-400/10"; // Default color
|
||||
switch (status) {
|
||||
case DomainStatus.NOT_STARTED:
|
||||
badgeColor = "bg-gray-400/10 text-gray-500 border-gray-400/10";
|
||||
break;
|
||||
case DomainStatus.SUCCESS:
|
||||
badgeColor = "bg-emerald-500/10 text-emerald-500 border-emerald-600/10";
|
||||
break;
|
||||
case DomainStatus.FAILED:
|
||||
badgeColor = "bg-red-500/10 text-red-800 border-red-600/10";
|
||||
break;
|
||||
case DomainStatus.TEMPORARY_FAILURE:
|
||||
case DomainStatus.PENDING:
|
||||
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 (
|
||||
<div
|
||||
className={`border text-center w-[120px] text-sm capitalize rounded-full py-0.5 ${badgeColor}`}
|
||||
>
|
||||
{status === "SUCCESS" ? "Verified" : status.toLowerCase()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const StatusIndicator: React.FC<{ status: DomainStatus }> = ({ status }) => {
|
||||
let badgeColor = "bg-gray-400"; // Default color
|
||||
switch (status) {
|
||||
case DomainStatus.NOT_STARTED:
|
||||
badgeColor = "bg-gray-400";
|
||||
break;
|
||||
case DomainStatus.SUCCESS:
|
||||
badgeColor = "bg-emerald-500";
|
||||
break;
|
||||
case DomainStatus.FAILED:
|
||||
badgeColor = "bg-red-500";
|
||||
break;
|
||||
case DomainStatus.TEMPORARY_FAILURE:
|
||||
case DomainStatus.PENDING:
|
||||
badgeColor = "bg-yellow-500";
|
||||
break;
|
||||
default:
|
||||
badgeColor = "bg-gray-400";
|
||||
}
|
||||
|
||||
return <div className={` w-[1px] ${badgeColor} my-1.5 rounded-full`}></div>;
|
||||
};
|
||||
|
@@ -1,31 +1,149 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import {
|
||||
Table,
|
||||
TableCaption,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
TableHead,
|
||||
TableBody,
|
||||
TableCell,
|
||||
} from "@unsend/ui/src/table";
|
||||
import { Badge } from "@unsend/ui/src/badge";
|
||||
import { api } from "~/trpc/react";
|
||||
import {
|
||||
Mail,
|
||||
MailCheck,
|
||||
MailOpen,
|
||||
MailSearch,
|
||||
MailWarning,
|
||||
MailX,
|
||||
} from "lucide-react";
|
||||
import { formatDistance, formatDistanceToNow } from "date-fns";
|
||||
|
||||
export default function DomainsList() {
|
||||
export default function EmailsList() {
|
||||
const emailsQuery = api.email.emails.useQuery();
|
||||
|
||||
return (
|
||||
<div className="mt-10">
|
||||
<div className="flex flex-col gap-2 w-full">
|
||||
{!emailsQuery.isLoading && emailsQuery.data?.length ? (
|
||||
emailsQuery.data?.map((email) => (
|
||||
<Link key={email.id} href={`/email/${email.id}`} className="w-full">
|
||||
<div className="p-2 px-4 border rounded-lg flex justify-between w-full">
|
||||
<p>{email.to}</p>
|
||||
<p className=" capitalize">
|
||||
{email.latestStatus?.toLowerCase()}
|
||||
</p>
|
||||
<p>{email.subject}</p>
|
||||
<p>{email.createdAt.toLocaleDateString()}</p>
|
||||
</div>
|
||||
</Link>
|
||||
))
|
||||
) : (
|
||||
<div>No domains</div>
|
||||
)}
|
||||
<div className="flex rounded-xl border shadow">
|
||||
<Table className="">
|
||||
<TableHeader className="">
|
||||
<TableRow className=" bg-muted/30">
|
||||
<TableHead className="rounded-tl-xl">To</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead>Subject</TableHead>
|
||||
<TableHead className="text-right rounded-tr-xl">
|
||||
Sent at
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{emailsQuery.data?.map((email) => (
|
||||
<TableRow key={email.id}>
|
||||
<TableCell className="font-medium flex gap-4 items-center">
|
||||
<EmailIcon status={email.latestStatus ?? "Send"} />
|
||||
<p>{email.to}</p>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<EmailStatusBadge status={email.latestStatus ?? "Sent"} />
|
||||
{/* <Badge className="w-[100px] flex py-1 justify-center text-emerald-400 hover:bg-emerald-500/10 bg-emerald-500/10 rounded">
|
||||
{email.latestStatus ?? "Sent"}
|
||||
</Badge> */}
|
||||
</TableCell>
|
||||
<TableCell>{email.subject}</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{formatDistanceToNow(email.createdAt, { addSuffix: true })}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const EmailIcon: React.FC<{ status: string }> = ({ status }) => {
|
||||
switch (status) {
|
||||
case "Send":
|
||||
return (
|
||||
// <div className="border border-gray-400/60 p-2 rounded-lg bg-gray-400/10">
|
||||
<Mail className="w-6 h-6 text-gray-500 " />
|
||||
// </div>
|
||||
);
|
||||
case "Delivery":
|
||||
case "Delayed":
|
||||
return (
|
||||
// <div className="border border-emerald-600/60 p-2 rounded-lg bg-emerald-500/10">
|
||||
<MailCheck className="w-6 h-6 text-emerald-800" />
|
||||
// </div>
|
||||
);
|
||||
case "Bounced":
|
||||
return (
|
||||
// <div className="border border-red-600/60 p-2 rounded-lg bg-red-500/10">
|
||||
<MailX className="w-6 h-6 text-red-900" />
|
||||
// </div>
|
||||
);
|
||||
case "Clicked":
|
||||
return (
|
||||
// <div className="border border-cyan-600/60 p-2 rounded-lg bg-cyan-500/10">
|
||||
<MailSearch className="w-6 h-6 text-cyan-700" />
|
||||
// </div>
|
||||
);
|
||||
case "Opened":
|
||||
return (
|
||||
// <div className="border border-indigo-600/60 p-2 rounded-lg bg-indigo-500/10">
|
||||
<MailOpen className="w-6 h-6 text-indigo-700" />
|
||||
// </div>
|
||||
);
|
||||
case "Complained":
|
||||
return (
|
||||
// <div className="border border-yellow-600/60 p-2 rounded-lg bg-yellow-500/10">
|
||||
<MailWarning className="w-6 h-6 text-yellow-700" />
|
||||
// </div>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
// <div className="border border-gray-400/60 p-2 rounded-lg">
|
||||
<Mail className="w-6 h-6" />
|
||||
// </div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const EmailStatusBadge: React.FC<{ status: string }> = ({ status }) => {
|
||||
let badgeColor = "bg-gray-400/10 text-gray-500 border-gray-400/10"; // Default color
|
||||
switch (status) {
|
||||
case "Send":
|
||||
badgeColor = "bg-gray-400/10 text-gray-500 border-gray-400/10";
|
||||
break;
|
||||
case "Delivery":
|
||||
case "Delayed":
|
||||
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 "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 (
|
||||
<div
|
||||
className={`border text-center w-[120px] rounded-full py-0.5 ${badgeColor}`}
|
||||
>
|
||||
{status}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@@ -176,7 +176,7 @@ export default async function AuthenticatedDashboardLayout({
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</header>
|
||||
<main className="flex flex-1 flex-col gap-4 p-4 lg:gap-6 lg:p-6">
|
||||
<main className="flex flex-1 flex-col gap-4 p-4 w-full lg:max-w-6xl mx-auto lg:gap-6 lg:p-6">
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
|
Reference in New Issue
Block a user