Add create team page
This commit is contained in:
@@ -31,6 +31,7 @@ import { Sheet, SheetContent, SheetTrigger } from "@unsend/ui/src/sheet";
|
|||||||
import { NextAuthProvider } from "~/providers/next-auth";
|
import { NextAuthProvider } from "~/providers/next-auth";
|
||||||
import { getServerAuthSession } from "~/server/auth";
|
import { getServerAuthSession } from "~/server/auth";
|
||||||
import { NavButton } from "./nav-button";
|
import { NavButton } from "./nav-button";
|
||||||
|
import { db } from "~/server/db";
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
title: "Unsend",
|
title: "Unsend",
|
||||||
@@ -53,6 +54,16 @@ export default async function AuthenticatedDashboardLayout({
|
|||||||
redirect("/wait-list");
|
redirect("/wait-list");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const teamUser = await db.teamUser.findFirst({
|
||||||
|
where: {
|
||||||
|
userId: session.user.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!teamUser) {
|
||||||
|
redirect("/create-team");
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NextAuthProvider session={session}>
|
<NextAuthProvider session={session}>
|
||||||
<div className="flex min-h-screen w-full h-full">
|
<div className="flex min-h-screen w-full h-full">
|
||||||
|
92
apps/web/src/app/create-team/page.tsx
Normal file
92
apps/web/src/app/create-team/page.tsx
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { Button } from "@unsend/ui/src/button";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormMessage,
|
||||||
|
} from "@unsend/ui/src/form";
|
||||||
|
import { Input } from "@unsend/ui/src/input";
|
||||||
|
import { Spinner } from "@unsend/ui/src/spinner";
|
||||||
|
import { api } from "~/trpc/react";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
|
||||||
|
const FormSchema = z.object({
|
||||||
|
name: z.string().min(2, {
|
||||||
|
message: "Team name must be at least 2 characters.",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default function CreateTeam() {
|
||||||
|
const createTeam = api.team.createTeam.useMutation();
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const form = useForm<z.infer<typeof FormSchema>>({
|
||||||
|
resolver: zodResolver(FormSchema),
|
||||||
|
defaultValues: {
|
||||||
|
name: "",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
function onSubmit(data: z.infer<typeof FormSchema>) {
|
||||||
|
createTeam.mutate(data, {
|
||||||
|
onSuccess: () => {
|
||||||
|
router.replace("/dashboard");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center min-h-screen ">
|
||||||
|
<div className=" w-[300px] flex flex-col gap-8">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-2xl font-semibold text-center">Create Team</h1>
|
||||||
|
</div>
|
||||||
|
<Form {...form}>
|
||||||
|
<form
|
||||||
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
|
className=" flex flex-col gap-8 w-full"
|
||||||
|
>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="name"
|
||||||
|
render={({ field, formState }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder="Team name"
|
||||||
|
className="w-full"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
{formState.errors.name ? (
|
||||||
|
<FormMessage />
|
||||||
|
) : (
|
||||||
|
<FormDescription>
|
||||||
|
Request admin to join existing team
|
||||||
|
</FormDescription>
|
||||||
|
)}
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Button type="submit" disabled={createTeam.isPending}>
|
||||||
|
{createTeam.isPending ? (
|
||||||
|
<Spinner className="w-5 h-5" />
|
||||||
|
) : (
|
||||||
|
"Create"
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@@ -1,33 +1,16 @@
|
|||||||
import Link from "next/link";
|
|
||||||
|
|
||||||
import { getServerAuthSession } from "~/server/auth";
|
import { getServerAuthSession } from "~/server/auth";
|
||||||
import { Button } from "@unsend/ui/src/button";
|
import { redirect } from "next/navigation";
|
||||||
import { SendHorizonal } from "lucide-react";
|
|
||||||
|
|
||||||
export default async function Home() {
|
export default async function Home() {
|
||||||
const session = await getServerAuthSession();
|
const session = await getServerAuthSession();
|
||||||
|
|
||||||
return (
|
if (!session?.user) {
|
||||||
<main className="h-screen">
|
redirect("/login");
|
||||||
<h1 className="text-center text-4xl mt-20 flex gap-4 justify-center items-center">
|
}
|
||||||
<SendHorizonal />
|
|
||||||
Send emails in minutes. Completely open source
|
if (!session.user.isBetaUser) {
|
||||||
</h1>
|
redirect("/wait-list");
|
||||||
<div className="flex justify-center mt-10">
|
} else {
|
||||||
{session?.user ? (
|
redirect("/dashboard");
|
||||||
<Button className="mx-auto">
|
}
|
||||||
<Link href="/dashboard" className="mx-auto">
|
|
||||||
Send email
|
|
||||||
</Link>
|
|
||||||
</Button>
|
|
||||||
) : (
|
|
||||||
<Button className="mx-auto">
|
|
||||||
<Link href="api/auth/signin" className="mx-auto">
|
|
||||||
Signin
|
|
||||||
</Link>
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
@@ -2,6 +2,7 @@ import { domainRouter } from "~/server/api/routers/domain";
|
|||||||
import { createCallerFactory, createTRPCRouter } from "~/server/api/trpc";
|
import { createCallerFactory, createTRPCRouter } from "~/server/api/trpc";
|
||||||
import { apiRouter } from "./routers/api";
|
import { apiRouter } from "./routers/api";
|
||||||
import { emailRouter } from "./routers/email";
|
import { emailRouter } from "./routers/email";
|
||||||
|
import { teamRouter } from "./routers/team";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is the primary router for your server.
|
* This is the primary router for your server.
|
||||||
@@ -12,6 +13,7 @@ export const appRouter = createTRPCRouter({
|
|||||||
domain: domainRouter,
|
domain: domainRouter,
|
||||||
apiKey: apiRouter,
|
apiKey: apiRouter,
|
||||||
email: emailRouter,
|
email: emailRouter,
|
||||||
|
team: teamRouter,
|
||||||
});
|
});
|
||||||
|
|
||||||
// export type definition of API
|
// export type definition of API
|
||||||
|
50
apps/web/src/server/api/routers/team.ts
Normal file
50
apps/web/src/server/api/routers/team.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { createTRPCRouter, protectedProcedure } from "~/server/api/trpc";
|
||||||
|
|
||||||
|
export const teamRouter = createTRPCRouter({
|
||||||
|
createTeam: protectedProcedure
|
||||||
|
.input(z.object({ name: z.string() }))
|
||||||
|
.mutation(async ({ ctx, input }) => {
|
||||||
|
const teams = await ctx.db.team.findMany({
|
||||||
|
where: {
|
||||||
|
teamUsers: {
|
||||||
|
some: {
|
||||||
|
userId: ctx.session.user.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (teams) {
|
||||||
|
console.log("User already has a team");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx.db.team.create({
|
||||||
|
data: {
|
||||||
|
name: input.name,
|
||||||
|
teamUsers: {
|
||||||
|
create: {
|
||||||
|
userId: ctx.session.user.id,
|
||||||
|
role: "ADMIN",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
|
||||||
|
getTeams: protectedProcedure.query(async ({ ctx }) => {
|
||||||
|
const teams = await ctx.db.team.findMany({
|
||||||
|
where: {
|
||||||
|
teamUsers: {
|
||||||
|
some: {
|
||||||
|
userId: ctx.session.user.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return teams;
|
||||||
|
}),
|
||||||
|
});
|
50
packages/ui/src/spinner.tsx
Normal file
50
packages/ui/src/spinner.tsx
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
export const Spinner: React.FC<React.SVGProps<SVGSVGElement>> = (props) => {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
version="1.1"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlnsXlink="http://www.w3.org/1999/xlink"
|
||||||
|
x="0px"
|
||||||
|
y="0px"
|
||||||
|
viewBox="0 0 2400 2400"
|
||||||
|
xmlSpace="preserve"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<g
|
||||||
|
strokeWidth="200"
|
||||||
|
strokeLinecap="round"
|
||||||
|
className="stroke-primary-foreground"
|
||||||
|
fill="none"
|
||||||
|
id="spinner"
|
||||||
|
>
|
||||||
|
<line x1="1200" y1="600" x2="1200" y2="100" />
|
||||||
|
<line opacity="0.5" x1="1200" y1="2300" x2="1200" y2="1800" />
|
||||||
|
<line opacity="0.917" x1="900" y1="680.4" x2="650" y2="247.4" />
|
||||||
|
<line opacity="0.417" x1="1750" y1="2152.6" x2="1500" y2="1719.6" />
|
||||||
|
<line opacity="0.833" x1="680.4" y1="900" x2="247.4" y2="650" />
|
||||||
|
<line opacity="0.333" x1="2152.6" y1="1750" x2="1719.6" y2="1500" />
|
||||||
|
<line opacity="0.75" x1="600" y1="1200" x2="100" y2="1200" />
|
||||||
|
<line opacity="0.25" x1="2300" y1="1200" x2="1800" y2="1200" />
|
||||||
|
<line opacity="0.667" x1="680.4" y1="1500" x2="247.4" y2="1750" />
|
||||||
|
<line opacity="0.167" x1="2152.6" y1="650" x2="1719.6" y2="900" />
|
||||||
|
<line opacity="0.583" x1="900" y1="1719.6" x2="650" y2="2152.6" />
|
||||||
|
<line opacity="0.083" x1="1750" y1="247.4" x2="1500" y2="680.4" />
|
||||||
|
<animateTransform
|
||||||
|
attributeName="transform"
|
||||||
|
attributeType="XML"
|
||||||
|
type="rotate"
|
||||||
|
keyTimes="0;0.08333;0.16667;0.25;0.33333;0.41667;0.5;0.58333;0.66667;0.75;0.83333;0.91667"
|
||||||
|
values="0 1199 1199;30 1199 1199;60 1199 1199;90 1199 1199;120 1199 1199;150 1199 1199;180 1199 1199;210 1199 1199;240 1199 1199;270 1199 1199;300 1199 1199;330 1199 1199"
|
||||||
|
dur="0.83333s"
|
||||||
|
begin="0s"
|
||||||
|
repeatCount="indefinite"
|
||||||
|
calcMode="discrete"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Spinner;
|
Reference in New Issue
Block a user