Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
267 changes: 267 additions & 0 deletions apps/web/app/(auth)/(app)/app/onboarding/client.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,267 @@
"use client";
import { cn } from "@/lib/utils";
import { Check } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Particles } from "@/components/particles";
import { Input } from "@/components/ui/input";
import { useToast } from "@/hooks/use-toast";
import { useState } from "react";
import { trpc } from "@/lib/trpc/client";
import { CardDescription, CardHeader, CardTitle, Card, CardContent } from "@/components/ui/card";

const steps = [
{
id: "1",
name: "Create your team",
description: "Before we can get started, we need to create your team.",
},
{
id: "2",
name: "Add a slug",
description: "Where should your team belong?",
},
{
id: "3",
name: "Review your team",
description: "Let's make sure everything is correct before we create your team",
},
];

type Props = {
tenantId: string;
};
export const Onboarding: React.FC<Props> = (props) => {
const [currentStep, setCurrentStep] = useState(0);
const [teamName, setTeamName] = useState("");
const [teamSlug, setTeamSlug] = useState("");
const { toast } = useToast();
const user = trpc.team.create.useMutation({
onSuccess() {
toast({
title: "Team Created",
description: "Your team has been created",
});
},
onError(err) {
console.error(err);
toast({ title: "Error", description: err.message, variant: "destructive" });
},
});

async function createTeam() {
await user.mutateAsync({
id: props.tenantId,
name: teamName,
slug: teamSlug,
});
}
return (
<div>
<ol
role="list"
className="overflow-hidden border-b rounded-md lg:flex lg:rounded-none divide-x divide-white/10 border-white/10 bg-primary-900"
>
{steps.map((step, stepIdx) => (
<li key={step.id} className="relative overflow-hidden lg:flex-1">
<Step
key={step.id}
id={step.id}
title={step.name}
description={step.description}
state={
stepIdx < currentStep
? "completed"
: stepIdx === currentStep
? "current"
: "upcoming"
}
/>
</li>
))}
</ol>

<main className="p-4 md:p-6 lg:p-8">
{currentStep === 0 && (
<div className="flex flex-col gap-4 md:gap-6 lg:gap-8">
<Card>
<CardHeader>
<CardTitle>Choose your team Name</CardTitle>
<CardDescription>Let's choose a name for your team'</CardDescription>
</CardHeader>
<CardContent>
<div>
<div className="flex items-center justify-center px-2 py-1 mt-8 rounded gap-4 ">
<Input
name="teamName"
type="text"
placeholder="Team Name"
value={teamName}
onChange={(e) =>
setTeamName(e.target.value.replace(/[^a-zA-Z0-9_-\s]+/g, ""))
}
/>
</div>
</div>
<div className="flex justify-end ">
<Button
disabled={teamName.length < 1}
className="my-4 w-1/3"
onClick={() => {
setCurrentStep(1);
setTeamSlug(teamName.replace(/\s+/g, "-").toLowerCase());
}}
>
Next
</Button>
</div>
</CardContent>
</Card>
</div>
)}
{teamName && currentStep === 1 && (
<div className="flex flex-col gap-4 md:gap-6 lg:gap-8">
<Card>
<CardHeader>
<CardTitle>Choose your team slug</CardTitle>
<CardDescription>
Let's choose a slug for your team. by default we used your team name as a slug.
You can change it if you want.'
</CardDescription>
</CardHeader>
<CardContent>
<div>
<div className="flex items-start justify-between px-2 py-1 mt-8 rounded gap-4 ">
<Input
name="teamSlug"
type="text"
defaultValue={teamSlug}
onChange={(e) =>
setTeamSlug(e.target.value.replace(/^[a-zA-Z0-9-_\.]+$/, ""))
}
placeholder="Team Slug"
/>
</div>
</div>
<div className="flex justify-end items- gap-4 px-2">
<Button
variant="secondary"
className="my-4 w-1/3"
onClick={() => setCurrentStep(0)}
>
Previous
</Button>
<Button
disabled={teamSlug.length < 1}
className="my-4 w-1/3"
onClick={() => setCurrentStep(2)}
>
Next
</Button>
</div>
</CardContent>
</Card>
</div>
)}
{teamSlug && currentStep === 2 && (
<div className="flex flex-col gap-4 md:gap-6 lg:gap-8">
<Card>
<CardHeader>
<CardTitle>Review Your Team </CardTitle>
<CardDescription>
Let's make sure everything is correct before we create your team.
</CardDescription>
</CardHeader>
<CardContent>
<div>
<div className="flex flex-col items-center justify-center px-2 py-1 mt-8 rounded gap-4">
<p>
{" "}
<span className="font-bold">Team Name: </span> {teamName}
</p>
<p>
{" "}
<span className="font-bold">Team Slug: </span> {teamSlug}
</p>
</div>
</div>
<div className="flex justify-end gap-4 px-2">
<Button
variant="secondary"
className="my-4 w-1/3"
onClick={() => setCurrentStep(1)}
>
Previous
</Button>
<Button
disabled={!(teamName && teamSlug)}
className=" my-4 w-1/3"
onClick={createTeam}
>
Submit
</Button>
</div>
</CardContent>
</Card>
</div>
)}
</main>
</div>
);
};

type StepProps = {
id: string;
title: string;
description: string;
state: "current" | "upcoming" | "completed";
};

const Step: React.FC<StepProps> = ({ id, title, description, state }) => {
return (
<div
className={cn("group h-full flex items-start px-6 py-5 text-sm font-medium duration-1000", {
"bg-gray-400/5 hover:bg-gray-600/10 ": state === "current",
})}
>
<Particles
className="absolute inset-0 -z-10 "
vy={-1}
quantity={50}
staticity={200}
color="#7c3aed"
/>

<span
className={cn(
"flex justify-center items-center rounded w-6 h-6 text-xs font-medium ring-1 ring-inset",
{
"bg-gray-100/10 text-gray-900 ring-gray-100/10 shadow-xl shadow-gray-100/10 group-hover:shadow-gray-200/70 duration-1000":
state === "current",
"text-gray-600 ring-gray-400/30": state === "upcoming",
"text-gray-700 ring-gray-200/70": state === "completed",
},
)}
>
{state === "completed" ? <Check className="w-3 h-3" /> : id}
</span>
<span className="ml-4 mt-0.5 flex min-w-0 flex-col">
<span
className={cn("text-sm font-medium", {
"text-zinc-600": state === "current",
"text-zinc-800": state !== "current",
})}
>
{title}
</span>
<span
className={cn("text-sm font-medium", {
"text-zinc-600": state === "current",
"text-zinc-800": state !== "current",
})}
>
{description}
</span>
</span>
</div>
);
};
16 changes: 16 additions & 0 deletions apps/web/app/(auth)/(app)/app/onboarding/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Onboarding } from "./client";
import { getTenantId } from "@/lib/auth";
import { db, schema, eq } from "@unkey/db";
import { redirect } from "next/navigation";

export default async function OnboardingPage() {
const tenantId = getTenantId();
let tenant = await db.query.tenants.findFirst({
where: eq(schema.tenants.id, tenantId),
});
if (tenant) {
redirect("/app");
}

return <Onboarding tenantId={tenantId} />;
}
11 changes: 6 additions & 5 deletions apps/web/app/(auth)/auth/sign-in/[[...sign-in]]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"use client";
import * as React from "react";
import { useAuth } from "@clerk/nextjs"
import { useRouter } from 'next/navigation'
import { useAuth } from "@clerk/nextjs";
import { useRouter } from "next/navigation";
import { EmailSignIn } from "../email-signin";
import { OAuthSignIn } from "../oauth-signin";
import { EmailCode } from "../email-code";
Expand All @@ -13,10 +13,11 @@ export default function AuthenticationPage() {

const { isSignedIn, isLoaded } = useAuth();

if (!isLoaded) return null;
if (!isLoaded) {
return null;
}
if (isSignedIn) {

router.push("/app")
router.push("/app");
return null;
}
return (
Expand Down
1 change: 0 additions & 1 deletion apps/web/app/(auth)/auth/sign-in/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
export default function AuthLayout(props: { children: React.ReactNode }) {

return (
<>
<div className="grid h-screen place-items-center">
Expand Down
11 changes: 6 additions & 5 deletions apps/web/app/(auth)/auth/sign-up/[[...sign-up]]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"use client";
import * as React from "react";
import { useAuth } from "@clerk/nextjs"
import { useRouter } from 'next/navigation'
import { useAuth } from "@clerk/nextjs";
import { useRouter } from "next/navigation";
import { EmailSignUp } from "../email-signup";
import { OAuthSignUp } from "../oauth-signup";
import { EmailCode } from "../email-code";
Expand All @@ -12,10 +12,11 @@ export default function AuthenticationPage() {

const { isSignedIn, isLoaded } = useAuth();

if (!isLoaded) return null;
if (!isLoaded) {
return null;
}
if (isSignedIn) {

router.push("/app")
router.push("/app");
return null;
}
const [verify, setVerify] = React.useState(false);
Expand Down
1 change: 0 additions & 1 deletion apps/web/app/(auth)/auth/sign-up/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

export default function AuthLayout(props: { children: React.ReactNode }) {
return (
<>
Expand Down
2 changes: 1 addition & 1 deletion apps/web/lib/auth.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { auth } from "@clerk/nextjs/app-beta";
import { auth } from "@clerk/nextjs";
import { notFound } from "next/navigation";

/**
Expand Down
3 changes: 2 additions & 1 deletion apps/web/lib/trpc/routers/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { t } from "../trpc";
import { keyRouter } from "./key";
import { apiRouter } from "./api";

import { teamRouter } from "./team";
export const router = t.router({
key: keyRouter,
api: apiRouter,
team: teamRouter,
});

// export type definition of API
Expand Down
29 changes: 29 additions & 0 deletions apps/web/lib/trpc/routers/team.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { db, schema } from "@unkey/db";
import { TRPCError } from "@trpc/server";
import { z } from "zod";

import { t, auth } from "../trpc";
import { newId } from "@unkey/id";

export const teamRouter = t.router({
create: t.procedure
.use(auth)
.input(
z.object({
id: z.string(),
name: z.string().min(1).max(50),
slug: z.string().min(1).max(50).regex(/^[a-zA-Z0-9-_\.]+$/),
}),
)
.mutation(async ({ input }) => {
const id = input.id;
await db.insert(schema.tenants).values({
id,
name: input.name,
slug: input.slug,
});
return {
id,
};
}),
});