From 2923f4a4f264a916ad90d6bff0887b090acaa43c Mon Sep 17 00:00:00 2001 From: sadmann7 Date: Wed, 22 May 2024 15:28:13 +0600 Subject: [PATCH] feat: add onboarding --- package.json | 1 + pnpm-lock.yaml | 18 +++ src/app/(dashboard)/dashboard/layout.tsx | 2 +- .../onboarding/_components/connect-stripe.tsx | 70 +++++++++++ .../onboarding/_components/create-store.tsx | 110 ++++++++++++++++ .../onboarding/_components/intro.tsx | 79 ++++++++++++ .../onboarding/_components/onboarding.tsx | 30 +++++ src/app/(dashboard)/onboarding/page.tsx | 26 ++++ .../_components/create-store-dialog.tsx | 117 +++++------------- .../_components/create-store-form.tsx | 74 +++++++++++ .../_components/dashboard-header.tsx | 9 +- .../_components/dashboard-sidebar.tsx | 8 +- .../(dashboard)/store/[storeId]/layout.tsx | 30 +++-- src/app/(dashboard)/store/[storeId]/page.tsx | 8 +- src/components/icons.tsx | 10 +- src/components/layouts/auth-dropdown.tsx | 4 +- src/components/layouts/main-nav.tsx | 2 +- src/db/schema/utils.ts | 4 +- src/lib/actions/store.ts | 36 ++++-- src/lib/actions/stripe.ts | 4 +- 20 files changed, 505 insertions(+), 137 deletions(-) create mode 100644 src/app/(dashboard)/onboarding/_components/connect-stripe.tsx create mode 100644 src/app/(dashboard)/onboarding/_components/create-store.tsx create mode 100644 src/app/(dashboard)/onboarding/_components/intro.tsx create mode 100644 src/app/(dashboard)/onboarding/_components/onboarding.tsx create mode 100644 src/app/(dashboard)/onboarding/page.tsx create mode 100644 src/app/(dashboard)/store/[storeId]/_components/create-store-form.tsx diff --git a/package.json b/package.json index f9382825..350e964d 100644 --- a/package.json +++ b/package.json @@ -93,6 +93,7 @@ "react-medium-image-zoom": "^5.2.4", "react-syntax-highlighter": "^15.5.0", "react-textarea-autosize": "^8.5.3", + "react-use-measure": "^2.1.1", "remark-gfm": "^4.0.0", "resend": "^3.2.0", "server-only": "^0.0.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5639803d..b5e7d8a6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -203,6 +203,9 @@ dependencies: react-textarea-autosize: specifier: ^8.5.3 version: 8.5.3(@types/react@18.3.2)(react@18.3.1) + react-use-measure: + specifier: ^2.1.1 + version: 2.1.1(react-dom@18.3.1)(react@18.3.1) remark-gfm: specifier: ^4.0.0 version: 4.0.0 @@ -5837,6 +5840,10 @@ packages: resolution: {integrity: sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==} dev: false + /debounce@1.2.1: + resolution: {integrity: sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==} + dev: false + /debounce@2.0.0: resolution: {integrity: sha512-xRetU6gL1VJbs85Mc4FoEGSjQxzpdxRyFhe3lmWFyy2EzydIcD4xzUvRJMD+NPDfMwKNhxa3PvsIOU32luIWeA==} engines: {node: '>=18'} @@ -10479,6 +10486,17 @@ packages: react-dom: 18.3.1(react@18.3.1) dev: false + /react-use-measure@2.1.1(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-nocZhN26cproIiIduswYpV5y5lQpSQS1y/4KuvUCjSKmw7ZWIS/+g3aFnX3WdBkyuGUtTLif3UTqnLLhbDoQig==} + peerDependencies: + react: '>=16.13' + react-dom: '>=16.13' + dependencies: + debounce: 1.2.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + dev: false + /react@18.2.0: resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==} engines: {node: '>=0.10.0'} diff --git a/src/app/(dashboard)/dashboard/layout.tsx b/src/app/(dashboard)/dashboard/layout.tsx index 492b8e67..d80f0ba0 100644 --- a/src/app/(dashboard)/dashboard/layout.tsx +++ b/src/app/(dashboard)/dashboard/layout.tsx @@ -35,7 +35,7 @@ export default async function DashboardLayout({ />
- + { + if (!storeId) { + router.push("/onboarding") + } + }, [router, storeId]) + + return ( + + + + Now connect your store to Stripe + + {storeId && ( + + + + )} + + + ) +} diff --git a/src/app/(dashboard)/onboarding/_components/create-store.tsx b/src/app/(dashboard)/onboarding/_components/create-store.tsx new file mode 100644 index 00000000..7b3c6af5 --- /dev/null +++ b/src/app/(dashboard)/onboarding/_components/create-store.tsx @@ -0,0 +1,110 @@ +"use client" + +import * as React from "react" +import { useRouter, useSearchParams } from "next/navigation" +import { zodResolver } from "@hookform/resolvers/zod" +import { motion } from "framer-motion" +import { useForm } from "react-hook-form" +import { toast } from "sonner" + +import { createStore } from "@/lib/actions/store" +import { + createStoreSchema, + type CreateStoreSchema, +} from "@/lib/validations/store" +import { Button } from "@/components/ui/button" +import { Icons } from "@/components/icons" + +import { CreateStoreForm } from "../../store/[storeId]/_components/create-store-form" + +interface CreateStoreProps { + userId: string +} + +export function CreateStore({ userId }: CreateStoreProps) { + const router = useRouter() + const searchParams = useSearchParams() + const [isCreatePending, startCreateTransaction] = React.useTransition() + + const form = useForm({ + resolver: zodResolver(createStoreSchema), + defaultValues: { + name: "", + description: "", + }, + }) + + function onSubmit(input: CreateStoreSchema) { + startCreateTransaction(async () => { + const { data, error } = await createStore({ ...input, userId }) + + if (error) { + toast.error(error) + return + } + + if (data) { + const newSearchParams = new URLSearchParams(searchParams) + newSearchParams.set("step", "connect") + newSearchParams.set("store", data.id) + router.push(`/onboarding?${newSearchParams.toString()}`) + } + + form.reset() + }) + } + + return ( + + + + Let's start by creating your store + + + + + + + + + ) +} diff --git a/src/app/(dashboard)/onboarding/_components/intro.tsx b/src/app/(dashboard)/onboarding/_components/intro.tsx new file mode 100644 index 00000000..015cad3b --- /dev/null +++ b/src/app/(dashboard)/onboarding/_components/intro.tsx @@ -0,0 +1,79 @@ +"use client" + +import { useRouter } from "next/navigation" +import { motion } from "framer-motion" + +import { useDebounce } from "@/hooks/use-debounce" +import { Button } from "@/components/ui/button" + +export function Intro() { + const router = useRouter() + + const showText = useDebounce(true, 800) + + return ( + + {showText && ( + + + Welcome to Skateshop + + + Get started with your new store in just a few steps and start + selling your products online. + + + + + + )} + + ) +} diff --git a/src/app/(dashboard)/onboarding/_components/onboarding.tsx b/src/app/(dashboard)/onboarding/_components/onboarding.tsx new file mode 100644 index 00000000..ddc60b80 --- /dev/null +++ b/src/app/(dashboard)/onboarding/_components/onboarding.tsx @@ -0,0 +1,30 @@ +// @see https://github.com/juliusmarminge/acme-corp/blob/main/apps/nextjs/src/app/(dashboard)/onboarding/multi-step-form.tsx + +"use client" + +import { useSearchParams } from "next/navigation" +import { AnimatePresence } from "framer-motion" + +import { ConnectStripe } from "./connect-stripe" +import { CreateStore } from "./create-store" +import { Intro } from "./intro" + +interface OnboardingProps { + userId: string +} + +export function Onboarding({ userId }: OnboardingProps) { + const search = useSearchParams() + const step = search.get("step") + const storeId = search.get("store") + + return ( +
+ + {!step && } + {step === "create" && } + {step === "connect" && } + +
+ ) +} diff --git a/src/app/(dashboard)/onboarding/page.tsx b/src/app/(dashboard)/onboarding/page.tsx new file mode 100644 index 00000000..fedb36b6 --- /dev/null +++ b/src/app/(dashboard)/onboarding/page.tsx @@ -0,0 +1,26 @@ +import { type Metadata } from "next" +import { redirect } from "next/navigation" +import { auth } from "@clerk/nextjs/server" + +import { Shell } from "@/components/shell" + +import { Onboarding } from "./_components/onboarding" + +export const metadata: Metadata = { + title: "Onboarding", + description: "Get started with your new store", +} + +export default function OnboardingPage() { + const { userId } = auth() + + if (!userId) { + redirect("/signin") + } + + return ( + + + + ) +} diff --git a/src/app/(dashboard)/store/[storeId]/_components/create-store-dialog.tsx b/src/app/(dashboard)/store/[storeId]/_components/create-store-dialog.tsx index 7a7a78ee..48058102 100644 --- a/src/app/(dashboard)/store/[storeId]/_components/create-store-dialog.tsx +++ b/src/app/(dashboard)/store/[storeId]/_components/create-store-dialog.tsx @@ -4,12 +4,11 @@ import * as React from "react" import { useRouter } from "next/navigation" import { zodResolver } from "@hookform/resolvers/zod" import { HoverCardPortal } from "@radix-ui/react-hover-card" -import { useForm, type UseFormReturn } from "react-hook-form" +import { useForm } from "react-hook-form" import { toast } from "sonner" import { createStore } from "@/lib/actions/store" import { type getUserPlanMetrics } from "@/lib/queries/user" -import { cn } from "@/lib/utils" import { createStoreSchema, type CreateStoreSchema, @@ -36,24 +35,16 @@ import { DrawerTitle, DrawerTrigger, } from "@/components/ui/drawer" -import { - Form, - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage, -} from "@/components/ui/form" import { HoverCard, HoverCardContent, HoverCardTrigger, } from "@/components/ui/hover-card" -import { Input } from "@/components/ui/input" -import { Textarea } from "@/components/ui/textarea" import { Icons } from "@/components/icons" import { RateLimitAlert } from "@/components/rate-limit-alert" +import { CreateStoreForm } from "./create-store-form" + interface CreateStoreDialogProps extends React.ComponentPropsWithoutRef { userId: string @@ -67,7 +58,7 @@ export function CreateStoreDialog({ ...props }: CreateStoreDialogProps) { const router = useRouter() - const [loading, setLoading] = React.useState(false) + const [isCreatePending, startCreateTransaction] = React.useTransition() const isDesktop = useMediaQuery("(min-width: 640px)") const planMetrics = React.use(planMetricsPromise) @@ -82,24 +73,23 @@ export function CreateStoreDialog({ }, }) - async function onSubmit(input: CreateStoreSchema) { - setLoading(true) + function onSubmit(input: CreateStoreSchema) { + startCreateTransaction(async () => { + const { data, error } = await createStore({ ...input, userId }) - const { data, error } = await createStore({ ...input, userId }) + if (error) { + toast.error(error) + return + } - if (error) { - toast.error(error) - return - } + if (data) { + router.push(`/dashboard/stores/${data.id}`) + toast.success("Store created") + } - if (data) { - router.push(`/dashboard/stores/${data.id}`) - toast.success("Store created") - } - - setLoading(false) - onOpenChange?.(false) - form.reset() + onOpenChange?.(false) + form.reset() + }) } if (isDesktop) { @@ -140,8 +130,11 @@ export function CreateStoreDialog({ Cancel -