Add MVP version

This commit is contained in:
KMKoushik
2024-03-24 17:43:56 +11:00
parent 9032efa9b2
commit bbc64b5392
49 changed files with 3249 additions and 298 deletions

View File

@@ -0,0 +1,104 @@
"use client";
import { Button } from "@unsend/ui/src/button";
import { Input } from "@unsend/ui/src/input";
import { Label } from "@unsend/ui/src/label";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@unsend/ui/src/dialog";
import { api } from "~/trpc/react";
import { useState } from "react";
export default function AddApiKey() {
const [open, setOpen] = useState(false);
const [name, setName] = useState("");
const [apiKey, setApiKey] = useState("");
const addDomainMutation = api.apiKey.createToken.useMutation();
const utils = api.useUtils();
function handleSave() {
addDomainMutation.mutate(
{
name,
permission: "FULL",
},
{
onSuccess: (data) => {
utils.apiKey.invalidate();
setApiKey(data);
},
}
);
}
function handleCopy() {
navigator.clipboard.writeText(apiKey);
setApiKey("");
setName("");
setOpen(false);
}
return (
<Dialog
open={open}
onOpenChange={(_open) => (_open !== open ? setOpen(_open) : null)}
>
<DialogTrigger asChild>
<Button>Add API Key</Button>
</DialogTrigger>
{apiKey ? (
<DialogContent>
<DialogHeader>
<DialogTitle>Copy API key</DialogTitle>
</DialogHeader>
<div className="py-2 bg-gray-200 rounded-lg">{apiKey}</div>
<DialogFooter>
<Button
type="submit"
onClick={handleCopy}
disabled={addDomainMutation.isPending}
>
Close
</Button>
</DialogFooter>
</DialogContent>
) : (
<DialogContent>
<DialogHeader>
<DialogTitle>Create a new API key</DialogTitle>
</DialogHeader>
<div className="py-2">
<Label htmlFor="name" className="text-right">
API key name
</Label>
<Input
id="name"
placeholder="prod key"
defaultValue=""
className="col-span-3"
onChange={(e) => setName(e.target.value)}
value={name}
/>
</div>
<DialogFooter>
<Button
type="submit"
onClick={handleSave}
disabled={addDomainMutation.isPending}
>
Save changes
</Button>
</DialogFooter>
</DialogContent>
)}
</Dialog>
);
}

View File

@@ -0,0 +1,26 @@
"use client";
import Link from "next/link";
import { api } from "~/trpc/react";
export default function ApiList() {
const apiKeysQuery = api.apiKey.getApiKeys.useQuery();
return (
<div className="mt-10">
<div className="flex flex-col gap-2">
{!apiKeysQuery.isLoading && apiKeysQuery.data?.length ? (
apiKeysQuery.data?.map((apiKey) => (
<div className="p-2 px-4 border rounded-lg flex justify-between">
<p>{apiKey.name}</p>
<p>{apiKey.permission}</p>
<p>{apiKey.partialToken}</p>
</div>
))
) : (
<div>No API keys added</div>
)}
</div>
</div>
);
}

View File

@@ -0,0 +1,15 @@
import type { Metadata } from "next";
import ApiList from "./api-list";
import AddApiKey from "./add-api-key";
export default async function ApiKeysPage() {
return (
<div>
<div className="flex justify-between items-center">
<h1 className="font-bold text-lg">API Keys</h1>
<AddApiKey />
</div>
<ApiList />
</div>
);
}

View File

@@ -0,0 +1,5 @@
import type { Metadata } from "next";
export default async function DashboardPage() {
return <div>Hello world</div>;
}

View File

@@ -0,0 +1,62 @@
"use client";
import type { Metadata } from "next";
import { api } from "~/trpc/react";
export default function DomainItemPage({
params,
}: {
params: { domainId: string };
}) {
const domainQuery = api.domain.getDomain.useQuery({
id: Number(params.domainId),
});
return (
<div>
{domainQuery.isLoading ? (
<p>Loading...</p>
) : (
<>
<div className="flex flex-col gap-2">
<div className="flex justify-between items-center">
<h1 className="font-bold text-lg">{domainQuery.data?.name}</h1>
</div>
<span className="text-xs capitalize bg-gray-200 rounded px-2 py-1 w-[80px] text-center">
{domainQuery.data?.status.toLowerCase()}
</span>
</div>
<div className="mt-8 border rounded p-4">
<p className="font-semibold">DNS records</p>
<div className="flex flex-col gap-4 mt-8">
<div className="flex justify-between">
<p>TXT</p>
<p>{`unsend._domainkey.${domainQuery.data?.name}`}</p>
<p className=" w-[200px] overflow-hidden text-ellipsis">{`p=${domainQuery.data?.publicKey}`}</p>
<p className=" capitalize">
{domainQuery.data?.dkimStatus?.toLowerCase()}
</p>
</div>
<div className="flex justify-between">
<p>TXT</p>
<p>{`send.${domainQuery.data?.name}`}</p>
<p className=" w-[200px] overflow-hidden text-ellipsis text-nowrap">{`"v=spf1 include:amazonses.com ~all"`}</p>
<p className=" capitalize">
{domainQuery.data?.spfDetails?.toLowerCase()}
</p>
</div>
<div className="flex justify-between">
<p>MX</p>
<p>{`send.${domainQuery.data?.name}`}</p>
<p className=" w-[200px] overflow-hidden text-ellipsis text-nowrap">{`feedback-smtp.${domainQuery.data?.region}.amazonses.com`}</p>
<p className=" capitalize">
{domainQuery.data?.spfDetails?.toLowerCase()}
</p>
</div>
</div>
</div>
</>
)}
</div>
);
}

View File

@@ -0,0 +1,78 @@
"use client";
import { Button } from "@unsend/ui/src/button";
import { Input } from "@unsend/ui/src/input";
import { Label } from "@unsend/ui/src/label";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@unsend/ui/src/dialog";
import { api } from "~/trpc/react";
import { useState } from "react";
export default function AddDomain() {
const [open, setOpen] = useState(false);
const [domainName, setDomainName] = useState("");
const addDomainMutation = api.domain.createDomain.useMutation();
const utils = api.useUtils();
function handleSave() {
addDomainMutation.mutate(
{
name: domainName,
},
{
onSuccess: () => {
utils.domain.domains.invalidate();
setOpen(false);
},
}
);
}
return (
<Dialog
open={open}
onOpenChange={(_open) => (_open !== open ? setOpen(_open) : null)}
>
<DialogTrigger asChild>
<Button>Add domain</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Add a new domain</DialogTitle>
<DialogDescription>This creates a new domain</DialogDescription>
</DialogHeader>
<div className="py-2">
<Label htmlFor="name" className="text-right">
Domain Name
</Label>
<Input
id="name"
placeholder="subdomain.example.com"
defaultValue=""
className="col-span-3"
onChange={(e) => setDomainName(e.target.value)}
value={domainName}
/>
</div>
<DialogFooter>
<Button
type="submit"
onClick={handleSave}
disabled={addDomainMutation.isPending}
>
Save changes
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@@ -0,0 +1,27 @@
"use client";
import Link from "next/link";
import { api } from "~/trpc/react";
export default function DomainsList() {
const domainsQuery = api.domain.domains.useQuery();
return (
<div className="mt-10">
<div className="flex flex-col gap-2">
{!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>
</Link>
))
) : (
<div>No domains</div>
)}
</div>
</div>
);
}

View File

@@ -0,0 +1,15 @@
import type { Metadata } from "next";
import DomainsList from "./domain-list";
import AddDomain from "./add-domain";
export default async function DomainsPage() {
return (
<div>
<div className="flex justify-between items-center">
<h1 className="font-bold text-lg">Domains</h1>
<AddDomain />
</div>
<DomainsList />
</div>
);
}

View File

@@ -0,0 +1,31 @@
"use client";
import Link from "next/link";
import { api } from "~/trpc/react";
export default function DomainsList() {
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>
</div>
);
}

View File

@@ -0,0 +1,13 @@
import type { Metadata } from "next";
import EmailList from "./email-list";
export default async function EmailsPage() {
return (
<div>
<div className="flex justify-between items-center">
<h1 className="font-bold text-lg">Emails</h1>
</div>
<EmailList />
</div>
);
}

View File

@@ -0,0 +1,42 @@
import Link from "next/link";
import { redirect } from "next/navigation";
import { NextAuthProvider } from "~/providers/next-auth";
import { getServerAuthSession } from "~/server/auth";
export const metadata = {
title: "Unsend",
description: "Generated by create-t3-app",
icons: [{ rel: "icon", url: "/favicon.ico" }],
};
export default async function AuthenticatedDashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
const session = await getServerAuthSession();
if (!session?.user) {
redirect("/");
}
return (
<NextAuthProvider session={session}>
<div className="h-screen flex">
<nav className="w-[200px] border border-r p-4">
<div className=" font-semibold text-xl">Unsend</div>
<div className="flex flex-col gap-3 mt-10">
<Link href="/dashboard">Dashboard</Link>
<Link href="/domains">Domains</Link>
<Link href="/emails">Emails</Link>
<Link href="/api-keys">API Keys</Link>
</div>
</nav>
<div className="flex-1">
<div className=" max-w-4xl mx-auto py-4">{children}</div>
</div>
</div>
</NextAuthProvider>
);
}