cult-ui
Display
Animation
Navigation

intro-disclosure

A multi-step onboarding and feature introduction component with responsive design..

animated
button
card
dialog
effect
flex
gradient
grid
hover
list
motion
navigation
positioning
select
text
transition

Source Code

Files
intro-disclosure
1"use client"
2 
3import { useEffect, useState } from "react"
4import { useRouter } from "next/navigation"
5import { ChevronDownIcon, ResetIcon } from "@radix-ui/react-icons"
6import { DatabaseIcon } from "lucide-react"
7import { toast } from "sonner"
8 
9import { cn } from "@/lib/utils"
10import { Button } from "@/components/ui/button"
11import {
12  Collapsible,
13  CollapsibleContent,
14  CollapsibleTrigger,
15} from "@/components/ui/collapsible"
16 
17import { IntroDisclosure } from "../ui/intro-disclosure"
18 
19const steps = [
20  {
21    title: "Welcome to Cult UI",
22    short_description: "Discover our modern component library",
23    full_description:
24      "Welcome to Cult UI! Let's explore how our beautifully crafted components can help you build stunning user interfaces with ease.",
25    media: {
26      type: "image" as const,
27      src: "/feature-3.png",
28      alt: "Cult UI components overview",
29    },
30  },
31  {
32    title: "Customizable Components",
33    short_description: "Style and adapt to your needs",
34    full_description:
35      "Every component is built with customization in mind. Use our powerful theming system with Tailwind CSS to match your brand perfectly.",
36    media: {
37      type: "image" as const,
38      src: "/feature-2.png",
39      alt: "Component customization interface",
40    },
41    action: {
42      label: "View Theme Builder",
43      href: "/docs/theming",
44    },
45  },
46  {
47    title: "Responsive & Accessible",
48    short_description: "Built for everyone",
49    full_description:
50      "All components are fully responsive and follow WAI-ARIA guidelines, ensuring your application works seamlessly across all devices and is accessible to everyone.",
51    media: {
52      type: "image" as const,
53 
54      src: "/feature-1.png",
55      alt: "Responsive design demonstration",
56    },
57  },
58  {
59    title: "Start Building",
60    short_description: "Create your next project",
61    full_description:
62      "You're ready to start building! Check out our comprehensive documentation and component examples to create your next amazing project.",
63    action: {
64      label: "View Components",
65      href: "/docs/components",
66    },
67  },
68]
69 
70type StorageState = {
71  desktop: string | null
72  mobile: string | null
73}
74 
75export function IntroDisclosureDemo() {
76  const router = useRouter()
77  const [open, setOpen] = useState(true)
78  const [openMobile, setOpenMobile] = useState(true)
79  const [debugOpen, setDebugOpen] = useState(false)
80  const [storageState, setStorageState] = useState<StorageState>({
81    desktop: null,
82    mobile: null,
83  })
84 
85  const updateStorageState = () => {
86    setStorageState({
87      desktop: localStorage.getItem("feature_intro-demo"),
88      mobile: localStorage.getItem("feature_intro-demo-mobile"),
89    })
90  }
91 
92  // Update storage state whenever localStorage changes
93  useEffect(() => {
94    updateStorageState()
95    window.addEventListener("storage", updateStorageState)
96    return () => window.removeEventListener("storage", updateStorageState)
97  }, [])
98 
99  // Update storage state after reset
100  const handleReset = () => {
101    // localStorage.removeItem("feature_intro-demo")
102    setOpen(true)
103    if (storageState.desktop === "false") {
104      toast.info("Clear the local storage to trigger the feature again")
105      setDebugOpen(true)
106    }
107    if (storageState.desktop === null) {
108      updateStorageState()
109    }
110  }
111 
112  const handleResetMobile = () => {
113    // localStorage.removeItem("feature_intro-demo-mobile")
114    setOpenMobile(true)
115    updateStorageState()
116  }
117 
118  const handleClearDesktop = () => {
119    localStorage.removeItem("feature_intro-demo")
120    updateStorageState()
121    router.refresh()
122    toast.success("Desktop storage cleared")
123  }
124 
125  const handleClearMobile = () => {
126    localStorage.removeItem("feature_intro-demo-mobile")
127    updateStorageState()
128    router.refresh()
129    toast.success("Mobile storage cleared")
130  }
131 
132  const handleDebugOpenChange = (open: boolean) => {
133    if (open) {
134      updateStorageState()
135    }
136    setDebugOpen(open)
137  }
138 
139  return (
140    <div className="w-full space-y-8">
141      <div className="rounded-lg border bg-card text-card-foreground shadow-sm">
142        <div className="p-6">
143          <h2 className="text-2xl font-semibold leading-none tracking-tight mb-4">
144            IntroDisclosure Demo
145          </h2>
146          <p className="text-muted-foreground mb-6">
147            Experience our feature introduction component in both desktop and
148            mobile variants. Click the reset buttons to restart the demos.
149          </p>
150        </div>
151 
152        <div className="grid grid-cols-1 md:grid-cols-2 gap-8 p-6 pt-0">
153          <div className="flex flex-col">
154            <div
155              className={cn(
156                "flex flex-col gap-6 rounded-lg border-2 p-6 transition-colors",
157                !open && "border-muted bg-muted/50",
158                open && "border-primary"
159              )}
160            >
161              <div className="flex flex-col md:flex-row gap-4 items-center justify-between">
162                <div className="flex   flex-col">
163                  <p className="text-sm text-muted-foreground text-left">
164                    (Disclosure)
165                  </p>
166                  <h3 className="text-xl font-semibold">Desktop View</h3>
167                </div>
168                <button
169                  onClick={handleReset}
170                  className="inline-flex items-center justify-center rounded-full bg-primary px-4 py-2 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50"
171                >
172                  <ResetIcon className="mr-2 h-4 w-4" />
173                  Start Demo
174                </button>
175              </div>
176              <IntroDisclosure
177                open={open}
178                setOpen={setOpen}
179                steps={steps}
180                featureId="intro-demo"
181                showProgressBar={false}
182                onComplete={() => toast.success("Tour completed")}
183                onSkip={() => toast.info("Tour skipped")}
184              />
185              <div className="text-sm text-muted-foreground">
186                Status: {open ? "Active" : "Completed/Skipped"}
187              </div>
188            </div>
189          </div>
190 
191          <div className="flex flex-col">
192            <div
193              className={cn(
194                "flex flex-col gap-6 rounded-lg border-2 p-6 transition-colors",
195                !openMobile && "border-muted bg-muted/50",
196                openMobile && "border-primary"
197              )}
198            >
199              <div className="flex flex-col md:flex-row gap-4 items-center justify-between">
200                <div className="flex  flex-col">
201                  <p className="text-sm text-muted-foreground">
202                    (Drawer + Swipe)
203                  </p>
204                  <h3 className="text-xl font-semibold">Mobile View</h3>
205                </div>
206                <button
207                  onClick={handleResetMobile}
208                  className="inline-flex items-center justify-center rounded-full bg-primary px-4 py-2 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50"
209                >
210                  <ResetIcon className="mr-2 h-4 w-4" />
211                  Start Demo
212                </button>
213              </div>
214              <IntroDisclosure
215                open={openMobile}
216                setOpen={setOpenMobile}
217                steps={steps}
218                featureId="intro-demo-mobile"
219                onComplete={() => toast.success("Mobile tour completed")}
220                onSkip={() => toast.info("Mobile tour skipped")}
221                forceVariant="mobile"
222              />
223              <div className="text-sm text-muted-foreground">
224                Status: {openMobile ? "Active" : "Completed/Skipped"}
225              </div>
226            </div>
227          </div>
228        </div>
229 
230        <div className="border-t p-4">
231          <Collapsible
232            open={debugOpen}
233            onOpenChange={handleDebugOpenChange}
234            className="w-full"
235          >
236            <CollapsibleTrigger className="flex w-full items-center justify-between rounded-lg p-2  text-sm hover:bg-muted/50">
237              <div className="flex flex-col items-start text-left">
238                <h4 className="flex items-center gap-2 text-sm font-semibold">
239                  <DatabaseIcon className="size-4" />{" "}
240                  <span className="text-muted-foreground">
241                    Browser Local Storage State
242                  </span>
243                </h4>
244                <p className="text-sm text-muted-foreground mb-4 max-w-xl">
245                  These values represent the "Don't show again" checkbox state.
246                  <br />- When set to{" "}
247                  <code className="bg-background px-1">true</code>, the intro
248                  will be hidden. <br /> - When{" "}
249                  <code className="bg-background px-1">null</code>, the intro
250                  will be shown.
251                </p>
252              </div>
253              <ChevronDownIcon
254                className={cn(
255                  "size-8 transition-transform duration-200",
256                  debugOpen && "rotate-180"
257                )}
258              />
259            </CollapsibleTrigger>
260            <CollapsibleContent className="space-y-2">
261              <div className="rounded-md bg-muted p-4 text-sm">
262                <div className="space-y-4">
263                  <div className="flex items-center justify-between gap-4">
264                    <div className="flex-1">
265                      <span className="text-muted-foreground">
266                        Desktop State:{" "}
267                      </span>
268                      <code className="rounded bg-background px-2 py-1">
269                        {storageState.desktop === null
270                          ? "null"
271                          : storageState.desktop}
272                      </code>
273                    </div>
274                    <Button size="sm" onClick={handleClearDesktop}>
275                      Reset Local Storage
276                    </Button>
277                  </div>
278                  <div className="flex items-center justify-between gap-4">
279                    <div className="flex-1">
280                      <span className="text-muted-foreground">
281                        Mobile State:{" "}
282                      </span>
283                      <code className="rounded bg-background px-2 py-1">
284                        {storageState.mobile === null
285                          ? "null"
286                          : storageState.mobile}
287                      </code>
288                    </div>
289                    <Button size="sm" onClick={handleClearMobile}>
290                      Reset Local Storage
291                    </Button>
292                  </div>
293                </div>
294              </div>
295            </CollapsibleContent>
296          </Collapsible>
297        </div>
298      </div>
299    </div>
300  )
301}