mirror of
https://github.com/linsa-io/linsa.git
synced 2026-04-25 01:38:35 +02:00
feat: onboarding for existing users (#160)
* feat: onboarding for existing users * fix: escape character * chore: update msg
This commit is contained in:
@@ -1,6 +1,8 @@
|
|||||||
"use server"
|
"use server"
|
||||||
|
|
||||||
import { authedProcedure } from "@/lib/utils/auth-procedure"
|
import { authedProcedure } from "@/lib/utils/auth-procedure"
|
||||||
|
import { currentUser } from "@clerk/nextjs/server"
|
||||||
|
import { get } from "ronin"
|
||||||
import { create } from "ronin"
|
import { create } from "ronin"
|
||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
import { ZSAError } from "zsa"
|
import { ZSAError } from "zsa"
|
||||||
@@ -68,3 +70,9 @@ export const storeImage = authedProcedure
|
|||||||
throw new ZSAError("ERROR", "Failed to store image")
|
throw new ZSAError("ERROR", "Failed to store image")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const isExistingUser = async () => {
|
||||||
|
const clerkUser = await currentUser()
|
||||||
|
const roninUser = await get.existingStripeSubscriber.with({ email: clerkUser?.emailAddresses[0].emailAddress })
|
||||||
|
return clerkUser?.emailAddresses[0].emailAddress === roninUser?.email
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { DeepLinkProvider } from "@/lib/providers/deep-link-provider"
|
|||||||
import { GeistMono, GeistSans } from "./fonts"
|
import { GeistMono, GeistSans } from "./fonts"
|
||||||
import { JazzAndAuth } from "@/lib/providers/jazz-provider"
|
import { JazzAndAuth } from "@/lib/providers/jazz-provider"
|
||||||
import { TooltipProvider } from "@/components/ui/tooltip"
|
import { TooltipProvider } from "@/components/ui/tooltip"
|
||||||
|
import { LearnAnythingOnboarding } from "@/components/custom/learn-anything-onboarding"
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Learn Anything",
|
title: "Learn Anything",
|
||||||
@@ -42,6 +43,7 @@ export default function RootLayout({
|
|||||||
<body className={cn("h-full w-full font-sans antialiased", GeistSans.variable, GeistMono.variable)}>
|
<body className={cn("h-full w-full font-sans antialiased", GeistSans.variable, GeistMono.variable)}>
|
||||||
<Providers>
|
<Providers>
|
||||||
{children}
|
{children}
|
||||||
|
<LearnAnythingOnboarding />
|
||||||
<Toaster expand={false} />
|
<Toaster expand={false} />
|
||||||
</Providers>
|
</Providers>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
102
web/components/custom/learn-anything-onboarding.tsx
Normal file
102
web/components/custom/learn-anything-onboarding.tsx
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import React, { useEffect, useState } from "react"
|
||||||
|
import { atom, useAtom } from "jotai"
|
||||||
|
import { atomWithStorage } from "jotai/utils"
|
||||||
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogTitle
|
||||||
|
} from "@/components/ui/alert-dialog"
|
||||||
|
import { isExistingUser } from "@/app/actions"
|
||||||
|
import { usePathname } from "next/navigation"
|
||||||
|
|
||||||
|
const hasVisitedAtom = atomWithStorage("hasVisitedLearnAnything", false)
|
||||||
|
const isDialogOpenAtom = atom(true)
|
||||||
|
|
||||||
|
export function LearnAnythingOnboarding() {
|
||||||
|
const pathname = usePathname()
|
||||||
|
const [hasVisited, setHasVisited] = useAtom(hasVisitedAtom)
|
||||||
|
const [isOpen, setIsOpen] = useAtom(isDialogOpenAtom)
|
||||||
|
const [isFetching, setIsFetching] = useState(true)
|
||||||
|
const [isExisting, setIsExisting] = useState(false)
|
||||||
|
|
||||||
|
if (pathname === "/") return null
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const loadUser = async () => {
|
||||||
|
try {
|
||||||
|
const existingUser = await isExistingUser()
|
||||||
|
setIsExisting(existingUser)
|
||||||
|
setIsOpen(true)
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error loading user:", error)
|
||||||
|
} finally {
|
||||||
|
setIsFetching(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasVisited) {
|
||||||
|
loadUser()
|
||||||
|
}
|
||||||
|
}, [hasVisited, setIsOpen])
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
setIsOpen(false)
|
||||||
|
setHasVisited(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasVisited || isFetching) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AlertDialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
|
<AlertDialogContent className="max-w-xl">
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>
|
||||||
|
<h1 className="text-2xl font-bold">Welcome to Learn Anything!</h1>
|
||||||
|
</AlertDialogTitle>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
|
||||||
|
<AlertDialogDescription className="text-foreground/70 space-y-4 text-base leading-5">
|
||||||
|
{isExisting && (
|
||||||
|
<>
|
||||||
|
<p className="font-medium">Existing Customer Notice</p>
|
||||||
|
<p>
|
||||||
|
We noticed you are an existing Learn Anything customer. We sincerely apologize for any broken experience
|
||||||
|
you may have encountered on the old website. We've been working hard on this new version, which
|
||||||
|
addresses previous issues and offers more features. As an early customer, you're locked in at the{" "}
|
||||||
|
<strong>$3</strong> price for our upcoming pro version. Thank you for your support!
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<p>
|
||||||
|
Learn Anything is a learning platform that organizes knowledge in a social way. You can create pages, add
|
||||||
|
links, track learning status of any topic, and more things in the future.
|
||||||
|
</p>
|
||||||
|
<p>Try do these quick onboarding steps to get a feel for the product:</p>
|
||||||
|
<ul className="list-inside list-disc">
|
||||||
|
<li>Create your first page</li>
|
||||||
|
<li>Add a link to a resource</li>
|
||||||
|
<li>Update your learning status on a topic</li>
|
||||||
|
</ul>
|
||||||
|
<p>
|
||||||
|
If you have any questions, don't hesitate to reach out. Click on question mark button in the bottom right
|
||||||
|
corner and enter your message.
|
||||||
|
</p>
|
||||||
|
</AlertDialogDescription>
|
||||||
|
|
||||||
|
<AlertDialogFooter className="mt-4">
|
||||||
|
<AlertDialogCancel onClick={handleClose}>Close</AlertDialogCancel>
|
||||||
|
<AlertDialogAction onClick={handleClose}>Get Started</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LearnAnythingOnboarding
|
||||||
106
web/components/ui/alert-dialog.tsx
Normal file
106
web/components/ui/alert-dialog.tsx
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { buttonVariants } from "@/components/ui/button"
|
||||||
|
|
||||||
|
const AlertDialog = AlertDialogPrimitive.Root
|
||||||
|
|
||||||
|
const AlertDialogTrigger = AlertDialogPrimitive.Trigger
|
||||||
|
|
||||||
|
const AlertDialogPortal = AlertDialogPrimitive.Portal
|
||||||
|
|
||||||
|
const AlertDialogOverlay = React.forwardRef<
|
||||||
|
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<AlertDialogPrimitive.Overlay
|
||||||
|
className={cn(
|
||||||
|
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
ref={ref}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
|
||||||
|
|
||||||
|
const AlertDialogContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof AlertDialogPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<AlertDialogPortal>
|
||||||
|
<AlertDialogOverlay />
|
||||||
|
<AlertDialogPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border p-6 shadow-lg duration-200 sm:rounded-lg",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</AlertDialogPortal>
|
||||||
|
))
|
||||||
|
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
|
||||||
|
|
||||||
|
const AlertDialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
||||||
|
<div className={cn("flex flex-col space-y-2 text-center sm:text-left", className)} {...props} />
|
||||||
|
)
|
||||||
|
AlertDialogHeader.displayName = "AlertDialogHeader"
|
||||||
|
|
||||||
|
const AlertDialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
||||||
|
<div className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)} {...props} />
|
||||||
|
)
|
||||||
|
AlertDialogFooter.displayName = "AlertDialogFooter"
|
||||||
|
|
||||||
|
const AlertDialogTitle = React.forwardRef<
|
||||||
|
React.ElementRef<typeof AlertDialogPrimitive.Title>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<AlertDialogPrimitive.Title ref={ref} className={cn("text-lg font-semibold", className)} {...props} />
|
||||||
|
))
|
||||||
|
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
|
||||||
|
|
||||||
|
const AlertDialogDescription = React.forwardRef<
|
||||||
|
React.ElementRef<typeof AlertDialogPrimitive.Description>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<AlertDialogPrimitive.Description ref={ref} className={cn("text-muted-foreground text-sm", className)} {...props} />
|
||||||
|
))
|
||||||
|
AlertDialogDescription.displayName = AlertDialogPrimitive.Description.displayName
|
||||||
|
|
||||||
|
const AlertDialogAction = React.forwardRef<
|
||||||
|
React.ElementRef<typeof AlertDialogPrimitive.Action>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<AlertDialogPrimitive.Action ref={ref} className={cn(buttonVariants(), className)} {...props} />
|
||||||
|
))
|
||||||
|
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
|
||||||
|
|
||||||
|
const AlertDialogCancel = React.forwardRef<
|
||||||
|
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<AlertDialogPrimitive.Cancel
|
||||||
|
ref={ref}
|
||||||
|
className={cn(buttonVariants({ variant: "outline" }), "mt-2 sm:mt-0", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
|
||||||
|
|
||||||
|
export {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogPortal,
|
||||||
|
AlertDialogOverlay,
|
||||||
|
AlertDialogTrigger,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogTitle,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel
|
||||||
|
}
|
||||||
@@ -17,6 +17,7 @@
|
|||||||
"@nothing-but/utils": "^0.16.0",
|
"@nothing-but/utils": "^0.16.0",
|
||||||
"@omit/react-confirm-dialog": "^1.1.5",
|
"@omit/react-confirm-dialog": "^1.1.5",
|
||||||
"@omit/react-fancy-switch": "^0.1.3",
|
"@omit/react-fancy-switch": "^0.1.3",
|
||||||
|
"@radix-ui/react-alert-dialog": "^1.1.1",
|
||||||
"@radix-ui/react-avatar": "^1.1.0",
|
"@radix-ui/react-avatar": "^1.1.0",
|
||||||
"@radix-ui/react-checkbox": "^1.1.1",
|
"@radix-ui/react-checkbox": "^1.1.1",
|
||||||
"@radix-ui/react-context-menu": "^2.2.1",
|
"@radix-ui/react-context-menu": "^2.2.1",
|
||||||
|
|||||||
Reference in New Issue
Block a user