Add MVP version
This commit is contained in:
104
apps/web/src/app/(dashboard)/api-keys/add-api-key.tsx
Normal file
104
apps/web/src/app/(dashboard)/api-keys/add-api-key.tsx
Normal 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>
|
||||
);
|
||||
}
|
26
apps/web/src/app/(dashboard)/api-keys/api-list.tsx
Normal file
26
apps/web/src/app/(dashboard)/api-keys/api-list.tsx
Normal 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>
|
||||
);
|
||||
}
|
15
apps/web/src/app/(dashboard)/api-keys/page.tsx
Normal file
15
apps/web/src/app/(dashboard)/api-keys/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
5
apps/web/src/app/(dashboard)/dashboard/page.tsx
Normal file
5
apps/web/src/app/(dashboard)/dashboard/page.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import type { Metadata } from "next";
|
||||
|
||||
export default async function DashboardPage() {
|
||||
return <div>Hello world</div>;
|
||||
}
|
62
apps/web/src/app/(dashboard)/domains/[domainId]/page.tsx
Normal file
62
apps/web/src/app/(dashboard)/domains/[domainId]/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
78
apps/web/src/app/(dashboard)/domains/add-domain.tsx
Normal file
78
apps/web/src/app/(dashboard)/domains/add-domain.tsx
Normal 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>
|
||||
);
|
||||
}
|
27
apps/web/src/app/(dashboard)/domains/domain-list.tsx
Normal file
27
apps/web/src/app/(dashboard)/domains/domain-list.tsx
Normal 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>
|
||||
);
|
||||
}
|
15
apps/web/src/app/(dashboard)/domains/page.tsx
Normal file
15
apps/web/src/app/(dashboard)/domains/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
31
apps/web/src/app/(dashboard)/emails/email-list.tsx
Normal file
31
apps/web/src/app/(dashboard)/emails/email-list.tsx
Normal 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>
|
||||
);
|
||||
}
|
13
apps/web/src/app/(dashboard)/emails/page.tsx
Normal file
13
apps/web/src/app/(dashboard)/emails/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
42
apps/web/src/app/(dashboard)/layout.tsx
Normal file
42
apps/web/src/app/(dashboard)/layout.tsx
Normal 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>
|
||||
);
|
||||
}
|
@@ -1,43 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
|
||||
import { api } from "~/trpc/react";
|
||||
|
||||
export function CreatePost() {
|
||||
const router = useRouter();
|
||||
const [name, setName] = useState("");
|
||||
|
||||
const createPost = api.post.create.useMutation({
|
||||
onSuccess: () => {
|
||||
router.refresh();
|
||||
setName("");
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
createPost.mutate({ name });
|
||||
}}
|
||||
className="flex flex-col gap-2"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Title"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
className="w-full rounded-full px-4 py-2 text-black"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
className="rounded-full bg-white/10 px-10 py-3 font-semibold transition hover:bg-white/20"
|
||||
disabled={createPost.isPending}
|
||||
>
|
||||
{createPost.isPending ? "Submitting..." : "Submit"}
|
||||
</button>
|
||||
</form>
|
||||
);
|
||||
}
|
36
apps/web/src/app/api/email/route.ts
Normal file
36
apps/web/src/app/api/email/route.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { headers } from "next/headers";
|
||||
import { hashToken } from "~/server/auth";
|
||||
import { db } from "~/server/db";
|
||||
import { sendEmail } from "~/server/service/email-service";
|
||||
|
||||
export async function GET() {
|
||||
return Response.json({ data: "Hello" });
|
||||
}
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const token = headers().get("authorization")?.split(" ")[1];
|
||||
console.log(token);
|
||||
if (!token) {
|
||||
return new Response("authorization token is required", {
|
||||
status: 401,
|
||||
});
|
||||
}
|
||||
const hashedToken = hashToken(token);
|
||||
const team = await db.team.findFirst({
|
||||
where: {
|
||||
apiKeys: {
|
||||
some: {
|
||||
tokenHash: hashedToken,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const data = await req.json();
|
||||
try {
|
||||
const email = await sendEmail({ ...data, teamId: team?.id });
|
||||
return Response.json({ data: email });
|
||||
} catch (e) {
|
||||
return Response.json({ error: (e as Error).message }, { status: 500 });
|
||||
}
|
||||
}
|
6
apps/web/src/app/api/health/route.ts
Normal file
6
apps/web/src/app/api/health/route.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { setupAws } from "~/server/aws/setup";
|
||||
|
||||
export async function GET() {
|
||||
await setupAws();
|
||||
return Response.json({ data: "Healthy" });
|
||||
}
|
82
apps/web/src/app/api/ses_callback/route.ts
Normal file
82
apps/web/src/app/api/ses_callback/route.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { headers } from "next/headers";
|
||||
import { hashToken } from "~/server/auth";
|
||||
import { db } from "~/server/db";
|
||||
|
||||
export async function GET(req: Request) {
|
||||
console.log("GET", req);
|
||||
return Response.json({ data: "Hello" });
|
||||
}
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const data = await req.json();
|
||||
|
||||
if (data.Type === "SubscriptionConfirmation") {
|
||||
return handleSubscription(data);
|
||||
}
|
||||
|
||||
console.log(data, data.Message);
|
||||
|
||||
let message = null;
|
||||
|
||||
try {
|
||||
message = JSON.parse(data.Message || "{}");
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
|
||||
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) {
|
||||
const subResponse = await fetch(message.SubscribeURL, {
|
||||
method: "GET",
|
||||
});
|
||||
|
||||
return Response.json({ data: "Hello" });
|
||||
}
|
@@ -1,84 +1,34 @@
|
||||
import Link from "next/link";
|
||||
|
||||
import { CreatePost } from "~/app/_components/create-post";
|
||||
import { getServerAuthSession } from "~/server/auth";
|
||||
import { api } from "~/trpc/server";
|
||||
import { Button } from "@unsend/ui/src/button";
|
||||
import { SendHorizonal } from "lucide-react";
|
||||
|
||||
export default async function Home() {
|
||||
const hello = await api.post.hello({ text: "from tRPC" });
|
||||
const session = await getServerAuthSession();
|
||||
|
||||
return (
|
||||
<main className="flex min-h-screen flex-col items-center justify-center bg-gradient-to-b from-[#2e026d] to-[#15162c] text-white">
|
||||
<div className="container flex flex-col items-center justify-center gap-12 px-4 py-16 ">
|
||||
<h1 className="text-5xl font-extrabold tracking-tight sm:text-[5rem]">
|
||||
Create <span className="text-[hsl(280,100%,70%)]">T3</span> App
|
||||
</h1>
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:gap-8">
|
||||
<Link
|
||||
className="flex max-w-xs flex-col gap-4 rounded-xl bg-white/10 p-4 hover:bg-white/20"
|
||||
href="https://create.t3.gg/en/usage/first-steps"
|
||||
target="_blank"
|
||||
>
|
||||
<h3 className="text-2xl font-bold">First Steps →</h3>
|
||||
<div className="text-lg">
|
||||
Just the basics - Everything you need to know to set up your
|
||||
database and authentication.
|
||||
</div>
|
||||
</Link>
|
||||
<Link
|
||||
className="flex max-w-xs flex-col gap-4 rounded-xl bg-white/10 p-4 hover:bg-white/20"
|
||||
href="https://create.t3.gg/en/introduction"
|
||||
target="_blank"
|
||||
>
|
||||
<h3 className="text-2xl font-bold">Documentation →</h3>
|
||||
<div className="text-lg">
|
||||
Learn more about Create T3 App, the libraries it uses, and how to
|
||||
deploy it.
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
<Button>Hello</Button>
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<p className="text-2xl text-white">
|
||||
{hello ? hello.greeting : "Loading tRPC query..."}
|
||||
</p>
|
||||
|
||||
<div className="flex flex-col items-center justify-center gap-4">
|
||||
<p className="text-center text-2xl text-white">
|
||||
{session && <span>Logged in as {session.user?.name}</span>}
|
||||
</p>
|
||||
<Link
|
||||
href={session ? "/api/auth/signout" : "/api/auth/signin"}
|
||||
className="rounded-full bg-white/10 px-10 py-3 font-semibold no-underline transition hover:bg-white/20"
|
||||
>
|
||||
{session ? "Sign out" : "Sign in"}
|
||||
<main className="h-screen">
|
||||
<h1 className="text-center text-4xl mt-20 flex gap-4 justify-center items-center">
|
||||
<SendHorizonal />
|
||||
Send emails in minutes. Completely open source
|
||||
</h1>
|
||||
<div className="flex justify-center mt-10">
|
||||
{session?.user ? (
|
||||
<Button className="mx-auto">
|
||||
<Link href="/dashboard" className="mx-auto">
|
||||
Send email
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CrudShowcase />
|
||||
</Button>
|
||||
) : (
|
||||
<Button className="mx-auto">
|
||||
<Link href="api/auth/signin" className="mx-auto">
|
||||
Signin
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
async function CrudShowcase() {
|
||||
const session = await getServerAuthSession();
|
||||
if (!session?.user) return null;
|
||||
|
||||
const latestPost = await api.post.getLatest();
|
||||
|
||||
return (
|
||||
<div className="w-full max-w-xs">
|
||||
{latestPost ? (
|
||||
<p className="truncate">Your most recent post: {latestPost.name}</p>
|
||||
) : (
|
||||
<p>You have no posts yet.</p>
|
||||
)}
|
||||
|
||||
<CreatePost />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
Reference in New Issue
Block a user