|
1 | 1 | "use client"; |
2 | 2 |
|
3 | 3 | import { useState, useEffect } from "react"; |
4 | | -import { |
5 | | - Dialog, |
6 | | - DialogContent, |
7 | | - DialogHeader, |
8 | | - DialogTitle, |
9 | | - DialogDescription, |
10 | | -} from "@/components/ui/dialog"; |
| 4 | +import { Dialog, DialogPortal, DialogOverlay } from "@/components/ui/dialog"; |
| 5 | +import * as DialogPrimitive from "@radix-ui/react-dialog"; |
11 | 6 | import { Button } from "@/components/ui/button"; |
12 | | -import { Star, MessageCircle, X } from "lucide-react"; |
13 | | -import { DiscordIcon } from "@/components/icons/DiscordIcon"; |
14 | | -import { getDiscordLink, GITHUB_REPO_URL } from "@/lib/utils/links"; |
| 7 | +import { Star, X } from "lucide-react"; |
| 8 | +import { cn } from "@/lib/utils"; |
| 9 | +import { GITHUB_REPO_URL } from "@/lib/utils/links"; |
15 | 10 |
|
16 | | -const STORAGE_KEY = "portfolioly_support_dialog_shown"; |
| 11 | +const COOKIE_NAME = "portfolioly_support_shown_c"; |
| 12 | +const MAX_SHOW_COUNT = 2; |
| 13 | +const SHOW_DELAY_MS = 4500; |
| 14 | + |
| 15 | +function getCookie(name: string): string | null { |
| 16 | + if (typeof document === "undefined") return null; |
| 17 | + const match = document.cookie.match(new RegExp(`(^| )${name}=([^;]+)`)); |
| 18 | + return match ? match[2] : null; |
| 19 | +} |
| 20 | + |
| 21 | +function setCookie(name: string, value: string, days: number = 365) { |
| 22 | + const expires = new Date(Date.now() + days * 864e5).toUTCString(); |
| 23 | + document.cookie = `${name}=${value}; expires=${expires}; path=/; SameSite=Lax`; |
| 24 | +} |
| 25 | + |
| 26 | +function GitHubIcon({ className }: { className?: string }) { |
| 27 | + return ( |
| 28 | + <svg viewBox="0 0 24 24" fill="currentColor" className={className}> |
| 29 | + <path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" /> |
| 30 | + </svg> |
| 31 | + ); |
| 32 | +} |
17 | 33 |
|
18 | 34 | export function SupportDialog() { |
19 | 35 | const [open, setOpen] = useState(false); |
20 | 36 |
|
21 | 37 | useEffect(() => { |
22 | | - const hasShown = localStorage.getItem(STORAGE_KEY); |
23 | | - if (!hasShown) { |
24 | | - const timer = setTimeout(() => setOpen(true), 2000); |
| 38 | + const shownCount = parseInt(getCookie(COOKIE_NAME) || "0", 10); |
| 39 | + |
| 40 | + if (shownCount < MAX_SHOW_COUNT) { |
| 41 | + const timer = setTimeout(() => setOpen(true), SHOW_DELAY_MS); |
25 | 42 | return () => clearTimeout(timer); |
26 | 43 | } |
27 | 44 | }, []); |
28 | 45 |
|
29 | 46 | const handleClose = () => { |
30 | | - localStorage.setItem(STORAGE_KEY, "true"); |
| 47 | + const currentCount = parseInt(getCookie(COOKIE_NAME) || "0", 10); |
| 48 | + setCookie(COOKIE_NAME, String(currentCount + 1)); |
31 | 49 | setOpen(false); |
32 | 50 | }; |
33 | 51 |
|
34 | | - const discordLink = getDiscordLink(); |
35 | | - |
36 | 52 | return ( |
37 | 53 | <Dialog open={open} onOpenChange={(isOpen) => !isOpen && handleClose()}> |
38 | | - <DialogContent className="sm:max-w-md"> |
39 | | - <DialogHeader> |
40 | | - <DialogTitle className="flex items-center gap-2"> |
41 | | - <MessageCircle className="h-5 w-5 text-primary" /> |
42 | | - Welcome to Portfolioly! |
43 | | - </DialogTitle> |
44 | | - <DialogDescription> |
45 | | - We're glad you're here. Here's how you can connect |
46 | | - with us. |
47 | | - </DialogDescription> |
48 | | - </DialogHeader> |
| 54 | + <DialogPortal> |
| 55 | + <DialogOverlay className="data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 duration-300" /> |
| 56 | + <DialogPrimitive.Content |
| 57 | + className={cn( |
| 58 | + "fixed left-[50%] top-[50%] z-50 w-full max-w-md translate-x-[-50%] translate-y-[-50%] border bg-background shadow-lg sm:rounded-lg", |
| 59 | + "data-[state=open]:animate-in data-[state=closed]:animate-out", |
| 60 | + "data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0", |
| 61 | + "duration-300 ease-out" |
| 62 | + )} |
| 63 | + > |
| 64 | + <DialogPrimitive.Close |
| 65 | + className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2" |
| 66 | + onClick={handleClose} |
| 67 | + > |
| 68 | + <X className="h-4 w-4" /> |
| 69 | + <span className="sr-only">Close</span> |
| 70 | + </DialogPrimitive.Close> |
49 | 71 |
|
50 | | - <div className="space-y-4 py-4"> |
51 | | - <div className="flex items-start gap-3 p-3 rounded-lg bg-muted/50"> |
52 | | - <DiscordIcon className="h-5 w-5 mt-0.5 text-[#5865F2]" /> |
53 | | - <div className="flex-1"> |
54 | | - <p className="font-medium text-sm"> |
55 | | - Need help or have questions? |
56 | | - </p> |
57 | | - <p className="text-sm text-muted-foreground"> |
58 | | - Join our Discord community for support and updates. |
59 | | - </p> |
60 | | - <Button |
61 | | - variant="outline" |
62 | | - size="sm" |
63 | | - className="mt-2" |
64 | | - onClick={() => window.open(discordLink, "_blank")} |
65 | | - > |
66 | | - <DiscordIcon className="h-4 w-4 mr-2" /> |
67 | | - Join Discord |
68 | | - </Button> |
| 72 | + <div className="p-6 pb-5 space-y-4"> |
| 73 | + <div className="flex items-center gap-3"> |
| 74 | + <div className="flex items-center justify-center w-10 h-10 rounded-full bg-yellow-500/10"> |
| 75 | + <Star className="h-5 w-5 text-yellow-500" /> |
| 76 | + </div> |
| 77 | + <h3 className="text-lg font-semibold">Enjoying Portfolioly?</h3> |
69 | 78 | </div> |
70 | | - </div> |
71 | 79 |
|
72 | | - <div className="flex items-start gap-3 p-3 rounded-lg bg-muted/50"> |
73 | | - <Star className="h-5 w-5 mt-0.5 text-yellow-500" /> |
74 | | - <div className="flex-1"> |
75 | | - <p className="font-medium text-sm">Enjoying Portfolioly?</p> |
76 | | - <p className="text-sm text-muted-foreground"> |
77 | | - A star on GitHub helps us grow and improve! |
78 | | - </p> |
79 | | - <Button |
80 | | - variant="outline" |
81 | | - size="sm" |
82 | | - className="mt-2" |
83 | | - onClick={() => window.open(GITHUB_REPO_URL, "_blank")} |
| 80 | + <p className="text-muted-foreground leading-relaxed"> |
| 81 | + If you like what we're building, it would really help if you |
| 82 | + could star us on GitHub. It means a lot and helps others discover |
| 83 | + the project :) |
| 84 | + </p> |
| 85 | + |
| 86 | + <p className="text-sm text-muted-foreground"> |
| 87 | + Questions or issues?{" "} |
| 88 | + <a |
| 89 | + href={`${GITHUB_REPO_URL}/issues`} |
| 90 | + target="_blank" |
| 91 | + rel="noopener noreferrer" |
| 92 | + className="inline-flex items-center gap-1 text-foreground hover:underline font-medium" |
84 | 93 | > |
85 | | - <Star className="h-4 w-4 mr-2" /> |
86 | | - Star on GitHub |
87 | | - </Button> |
88 | | - </div> |
| 94 | + <GitHubIcon className="h-3.5 w-3.5" /> |
| 95 | + Report here |
| 96 | + </a> |
| 97 | + </p> |
89 | 98 | </div> |
90 | | - </div> |
91 | 99 |
|
92 | | - <div className="flex justify-end"> |
93 | | - <Button variant="ghost" size="sm" onClick={handleClose}> |
94 | | - <X className="h-4 w-4 mr-1" /> |
95 | | - Close |
96 | | - </Button> |
97 | | - </div> |
98 | | - </DialogContent> |
| 100 | + <div className="flex border-t"> |
| 101 | + <Button |
| 102 | + variant="ghost" |
| 103 | + className="flex-1 rounded-none h-12 text-muted-foreground hover:text-foreground" |
| 104 | + onClick={handleClose} |
| 105 | + > |
| 106 | + Continue |
| 107 | + </Button> |
| 108 | + <div className="w-px bg-border" /> |
| 109 | + <Button |
| 110 | + variant="ghost" |
| 111 | + className="flex-1 rounded-none h-12 gap-2 font-medium" |
| 112 | + onClick={() => { |
| 113 | + window.open(GITHUB_REPO_URL, "_blank"); |
| 114 | + handleClose(); |
| 115 | + }} |
| 116 | + > |
| 117 | + <GitHubIcon className="h-4 w-4" /> |
| 118 | + Star on GitHub |
| 119 | + </Button> |
| 120 | + </div> |
| 121 | + </DialogPrimitive.Content> |
| 122 | + </DialogPortal> |
99 | 123 | </Dialog> |
100 | 124 | ); |
101 | 125 | } |
0 commit comments