A headless popover form animation component with customizable options..
1"use client"
2
3import { useEffect, useState } from "react"
4import { Mail, Monitor, Moon, Sun } from "lucide-react"
5
6import {
7 PopoverForm,
8 PopoverFormButton,
9 PopoverFormCutOutLeftIcon,
10 PopoverFormCutOutRightIcon,
11 PopoverFormSeparator,
12 PopoverFormSuccess,
13} from "@/components/ui/popover-form"
14
15type FormState = "idle" | "loading" | "success"
16
17export function NewsletterSignupExample() {
18 const [formState, setFormState] = useState<FormState>("idle")
19 const [open, setOpen] = useState(false)
20 const [email, setEmail] = useState("")
21
22 function submit() {
23 setFormState("loading")
24 setTimeout(() => {
25 setFormState("success")
26 }, 1500)
27
28 setTimeout(() => {
29 setOpen(false)
30 setFormState("idle")
31 setEmail("")
32 }, 3300)
33 }
34
35 useEffect(() => {
36 const handleKeyDown = (event: KeyboardEvent) => {
37 if (event.key === "Escape") {
38 setOpen(false)
39 }
40 }
41
42 window.addEventListener("keydown", handleKeyDown)
43 return () => window.removeEventListener("keydown", handleKeyDown)
44 }, [])
45
46 return (
47 <div className="flex w-full items-center justify-center">
48 <PopoverForm
49 title="Newsletter Signup"
50 open={open}
51 setOpen={setOpen}
52 width="320px"
53 showCloseButton={formState !== "success"}
54 showSuccess={formState === "success"}
55 openChild={
56 <form
57 onSubmit={(e) => {
58 e.preventDefault()
59 if (!email) return
60 submit()
61 }}
62 className="p-4"
63 >
64 <div className="mb-4 space-y-2">
65 <label
66 htmlFor="email"
67 className="block text-sm font-medium text-muted-foreground mb-1"
68 >
69 Email address
70 </label>
71 <div className="relative">
72 <input
73 type="email"
74 id="email"
75 name="email"
76 placeholder="you@example.com"
77 value={email}
78 onChange={(e) => setEmail(e.target.value)}
79 className="w-full px-3 py-2 border rounded-md shadow-sm placeholder-muted-foreground focus:outline-none focus:ring-primary focus:border-primary"
80 required
81 />
82 <div className="absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none">
83 <Mail className=" text-muted-foreground size-4" />
84 </div>
85 </div>
86 <p className="text-muted-foreground text-xs tracking-tight">
87 Sick content to your mailbox every week!
88 </p>
89 </div>
90 <PopoverFormButton
91 loading={formState === "loading"}
92 text="Subscribe"
93 />
94 </form>
95 }
96 successChild={
97 <PopoverFormSuccess
98 title="Successfully subscribed!"
99 description="Thank you for joining our newsletter."
100 />
101 }
102 />
103 </div>
104 )
105}
106
107type Theme = "light" | "dark" | "system"
108
109export function ColorThemeSwitcherExample() {
110 const [theme, setTheme] = useState("")
111 const [open, setOpen] = useState(true)
112 const themes: Theme[] = ["light", "dark", "system"]
113
114 useEffect(() => {
115 const root = window.document.documentElement
116 root.classList.remove("light", "dark")
117 if (theme === "system") {
118 const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
119 .matches
120 ? "dark"
121 : "light"
122 root.classList.add(systemTheme)
123 } else {
124 if (theme) root.classList.add(theme)
125 }
126 }, [theme])
127
128 return (
129 <div className="flex w-full items-center justify-center">
130 <PopoverForm
131 showSuccess={false}
132 title="Choose theme"
133 open={open}
134 setOpen={setOpen}
135 width="200px"
136 height="175px"
137 showCloseButton={true}
138 openChild={
139 <div className="p-2">
140 <h3 className="text-sm tracking-tight text-muted-foreground">
141 Theme
142 </h3>
143
144 <div className="pt-2 space-y-2">
145 {themes.map((t) => (
146 <button
147 key={t}
148 onClick={() => setTheme(t)}
149 className={`w-full flex items-center px-3 py-2 text-sm rounded-md ${
150 theme === t
151 ? "bg-primary text-white"
152 : "hover:bg-gray-100 dark:hover:bg-gray-800"
153 }`}
154 >
155 {t === "light" && <Sun className="mr-2 h-4 w-4" />}
156 {t === "dark" && <Moon className="mr-2 h-4 w-4" />}
157 {t === "system" && <Monitor className="mr-2 h-4 w-4" />}
158 <span className="capitalize">{t}</span>
159 </button>
160 ))}
161 </div>
162 </div>
163 }
164 />
165 </div>
166 )
167}
168
169export function FeedbackFormExample() {
170 const [formState, setFormState] = useState<FormState>("idle")
171 const [open, setOpen] = useState(false)
172 const [feedback, setFeedback] = useState("")
173
174 function submit() {
175 setFormState("loading")
176 setTimeout(() => {
177 setFormState("success")
178 }, 1500)
179
180 setTimeout(() => {
181 setOpen(false)
182 setFormState("idle")
183 setFeedback("")
184 }, 3300)
185 }
186
187 useEffect(() => {
188 const handleKeyDown = (event: KeyboardEvent) => {
189 if (event.key === "Escape") {
190 setOpen(false)
191 }
192
193 if (
194 (event.ctrlKey || event.metaKey) &&
195 event.key === "Enter" &&
196 open &&
197 formState === "idle"
198 ) {
199 submit()
200 }
201 }
202
203 window.addEventListener("keydown", handleKeyDown)
204 return () => window.removeEventListener("keydown", handleKeyDown)
205 }, [open, formState])
206
207 return (
208 <div className="flex w-full items-center justify-center">
209 <PopoverForm
210 title="Feedback"
211 open={open}
212 setOpen={setOpen}
213 width="364px"
214 height="192px"
215 showCloseButton={formState !== "success"}
216 showSuccess={formState === "success"}
217 openChild={
218 <form
219 onSubmit={(e) => {
220 e.preventDefault()
221 if (!feedback) return
222 submit()
223 }}
224 className=""
225 >
226 <div className="relative">
227 <textarea
228 autoFocus
229 placeholder="Feedback"
230 value={feedback}
231 onChange={(e) => setFeedback(e.target.value)}
232 className="h-32 w-full resize-none rounded-t-lg p-3 text-sm outline-none"
233 required
234 />
235 </div>
236 <div className="relative flex h-12 items-center px-[10px]">
237 <PopoverFormSeparator />
238 <div className="absolute left-0 top-0 -translate-x-[1.5px] -translate-y-1/2">
239 <PopoverFormCutOutLeftIcon />
240 </div>
241 <div className="absolute right-0 top-0 translate-x-[1.5px] -translate-y-1/2 rotate-180">
242 <PopoverFormCutOutRightIcon />
243 </div>
244 <PopoverFormButton
245 loading={formState === "loading"}
246 text="Submit"
247 />
248 </div>
249 </form>
250 }
251 successChild={
252 <PopoverFormSuccess
253 title="Feedback Received"
254 description="Thank you for supporting our project!"
255 />
256 }
257 />
258 </div>
259 )
260}
261
262export function ContactFormExample() {
263 const [formState, setFormState] = useState<FormState>("idle")
264 const [open, setOpen] = useState(false)
265 const [name, setName] = useState("")
266 const [email, setEmail] = useState("")
267 const [message, setMessage] = useState("")
268
269 function submit() {
270 setFormState("loading")
271 setTimeout(() => {
272 setFormState("success")
273 }, 1500)
274
275 setTimeout(() => {
276 setOpen(false)
277 setFormState("idle")
278 setName("")
279 setEmail("")
280 setMessage("")
281 }, 3300)
282 }
283
284 return (
285 <div className="flex w-full items-center justify-center">
286 <PopoverForm
287 title="Click Here"
288 open={open}
289 setOpen={setOpen}
290 width="364px"
291 height="372px"
292 showCloseButton={formState !== "success"}
293 showSuccess={formState === "success"}
294 openChild={
295 <form
296 onSubmit={(e) => {
297 e.preventDefault()
298 if (!name || !email || !message) return
299 submit()
300 }}
301 className=" space-y-4"
302 >
303 <div className="px-4 pt-4">
304 <label
305 htmlFor="name"
306 className="block text-sm font-medium text-muted-foreground mb-1"
307 >
308 Name
309 </label>
310 <input
311 type="text"
312 id="name"
313 value={name}
314 onChange={(e) => setName(e.target.value)}
315 className="w-full px-3 py-2 border rounded-md shadow-sm focus:outline-none focus:ring-primary focus:border-primary bg-white dark:bg-black"
316 required
317 />
318 </div>
319 <div className="px-4">
320 <label
321 htmlFor="email"
322 className="block text-sm font-medium text-muted-foreground mb-1"
323 >
324 Email
325 </label>
326 <input
327 type="email"
328 id="email"
329 value={email}
330 onChange={(e) => setEmail(e.target.value)}
331 className="w-full px-3 py-2 border rounded-md shadow-sm focus:outline-none focus:ring-primary focus:border-primary bg-white dark:bg-black"
332 required
333 />
334 </div>
335 <div className="px-4">
336 <label
337 htmlFor="message"
338 className="block text-sm font-medium text-muted-foreground mb-1"
339 >
340 Message
341 </label>
342 <textarea
343 id="message"
344 value={message}
345 onChange={(e) => setMessage(e.target.value)}
346 className="w-full px-3 py-2 border rounded-md shadow-sm focus:outline-none focus:ring-primary focus:border-primary bg-white dark:bg-black"
347 rows={3}
348 required
349 />
350 </div>
351 <div className="relative flex h-12 items-center px-[10px]">
352 <PopoverFormSeparator />
353 <div className="absolute left-0 top-0 -translate-x-[1.5px] -translate-y-1/2">
354 <PopoverFormCutOutLeftIcon />
355 </div>
356 <div className="absolute right-0 top-0 translate-x-[1.5px] -translate-y-1/2 rotate-180">
357 <PopoverFormCutOutRightIcon />
358 </div>
359 <PopoverFormButton
360 loading={formState === "loading"}
361 text="Submit"
362 />
363 </div>
364 </form>
365 }
366 successChild={
367 <PopoverFormSuccess
368 title="Message Sent"
369 description="Thank you for contacting us. We'll get back to you soon!"
370 />
371 }
372 />
373 </div>
374 )
375}
376
377export function PopoverFormExamples() {
378 return (
379 <div className="space-y-8 grid grid-cols-1 ">
380 <FeedbackFormExample />
381 <ContactFormExample />
382 <NewsletterSignupExample />
383 <ColorThemeSwitcherExample />
384 </div>
385 )
386}