rogasper commited on
Commit
ca9bdc2
·
1 Parent(s): 83e82db

feat: add CommunityModal and CommunityChannelList components for community engagement. Integrate community channels into the sidebar and landing page, enhancing user interaction with WhatsApp and Discord links. Implement CommunityCard in account settings for quick access to community resources.

Browse files
apps/web/src/components/CommunityChannelList.tsx ADDED
@@ -0,0 +1,90 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { MaterialIcon } from "@/components/ui/MaterialIcon";
2
+ import {
3
+ COMMUNITY_PLATFORM_META,
4
+ getCommunitySections,
5
+ type CommunityChannel,
6
+ type CommunityPlatform,
7
+ } from "@/lib/community-links";
8
+
9
+ function ChannelRow({
10
+ channel,
11
+ platform,
12
+ variant,
13
+ }: {
14
+ channel: CommunityChannel;
15
+ platform: CommunityPlatform;
16
+ variant: "modal" | "card";
17
+ }) {
18
+ const meta = COMMUNITY_PLATFORM_META[platform];
19
+ const baseClass =
20
+ variant === "modal"
21
+ ? `w-full flex items-start gap-3 py-3.5 px-4 rounded-[var(--radius-lg)] font-semibold transition-all text-[var(--clay-black)] border-2 clay-hover ${meta?.linkClass ?? ""}`
22
+ : `flex items-start gap-3 rounded-[var(--radius-lg)] border px-4 py-3 transition-colors ${meta?.linkClass ?? "border-[var(--oat-border)] bg-[var(--warm-cream)]/60"}`;
23
+
24
+ const iconClass = meta?.iconClass ?? "text-[var(--matcha-700)]";
25
+
26
+ if (!channel.href) {
27
+ return (
28
+ <div
29
+ className={`${baseClass} opacity-70 cursor-not-allowed border-dashed`}
30
+ aria-disabled="true"
31
+ >
32
+ <MaterialIcon name={channel.icon} className={`text-2xl shrink-0 mt-0.5 ${iconClass}`} />
33
+ <span className="text-left flex-1">
34
+ <span className="flex items-center gap-2 flex-wrap">
35
+ <span className="block font-bold">{channel.name}</span>
36
+ <span className="text-[10px] font-bold uppercase tracking-wide px-2 py-0.5 rounded-full bg-[var(--oat-light)] text-[var(--warm-charcoal)]">
37
+ Segera hadir
38
+ </span>
39
+ </span>
40
+ <span className="block text-sm font-normal text-[var(--warm-charcoal)] mt-0.5">
41
+ {channel.description}
42
+ </span>
43
+ </span>
44
+ </div>
45
+ );
46
+ }
47
+
48
+ return (
49
+ <a
50
+ href={channel.href}
51
+ target="_blank"
52
+ rel="noopener noreferrer"
53
+ className={baseClass}
54
+ >
55
+ <MaterialIcon name={channel.icon} className={`text-2xl shrink-0 mt-0.5 ${iconClass}`} />
56
+ <span className="text-left">
57
+ <span className={`block font-bold ${variant === "card" ? "text-sm" : ""}`}>
58
+ {channel.name}
59
+ </span>
60
+ <span
61
+ className={`block font-normal text-[var(--warm-charcoal)] mt-0.5 ${variant === "card" ? "text-xs" : "text-sm"}`}
62
+ >
63
+ {channel.description}
64
+ </span>
65
+ </span>
66
+ </a>
67
+ );
68
+ }
69
+
70
+ export function CommunityChannelList({ variant }: { variant: "modal" | "card" }) {
71
+ const sections = getCommunitySections();
72
+
73
+ return (
74
+ <div className={variant === "modal" ? "flex flex-col gap-5" : "space-y-5"}>
75
+ {sections.map(({ platform, meta, channels }) => (
76
+ <section key={platform} className="space-y-2">
77
+ <h3 className="flex items-center gap-2 text-xs font-bold uppercase tracking-wider text-[var(--warm-charcoal)]">
78
+ <MaterialIcon name={meta.icon} className={`text-base ${meta.iconClass}`} />
79
+ {meta.label}
80
+ </h3>
81
+ <div className={variant === "modal" ? "flex flex-col gap-2" : "space-y-2"}>
82
+ {channels.map((channel) => (
83
+ <ChannelRow key={channel.id} channel={channel} platform={platform} variant={variant} />
84
+ ))}
85
+ </div>
86
+ </section>
87
+ ))}
88
+ </div>
89
+ );
90
+ }
apps/web/src/components/CommunityModal.tsx ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@labas/ui/components/dialog";
2
+ import { MaterialIcon } from "@/components/ui/MaterialIcon";
3
+ import { CommunityChannelList } from "@/components/CommunityChannelList";
4
+
5
+ export function CommunityModal({
6
+ isOpen,
7
+ onOpenChange,
8
+ }: {
9
+ isOpen: boolean;
10
+ onOpenChange: (open: boolean) => void;
11
+ }) {
12
+ return (
13
+ <Dialog open={isOpen} onOpenChange={onOpenChange}>
14
+ <DialogContent className="sm:max-w-[440px] p-6 rounded-[var(--radius-xl)] bg-[var(--pure-white)] border-2 border-[var(--oat-border)]">
15
+ <DialogHeader>
16
+ <div className="mx-auto w-16 h-16 bg-[var(--matcha-300)]/30 rounded-full flex items-center justify-center mb-2">
17
+ <MaterialIcon name="forum" className="text-4xl text-[var(--matcha-800)]" />
18
+ </div>
19
+ <DialogTitle className="text-center text-2xl font-headline font-extrabold text-[var(--clay-black)]">
20
+ Gabung Komunitas Labas
21
+ </DialogTitle>
22
+ <DialogDescription className="text-center text-base text-[var(--warm-charcoal)] pt-2 leading-relaxed">
23
+ Belajar bahasa, lapor bug, atau ikut ngembangin Labas — pilih channel yang paling pas.
24
+ </DialogDescription>
25
+ </DialogHeader>
26
+ <div className="mt-6">
27
+ <CommunityChannelList variant="modal" />
28
+ <button
29
+ type="button"
30
+ onClick={() => onOpenChange(false)}
31
+ className="mt-5 w-full text-sm text-[var(--warm-silver)] hover:text-[var(--clay-black)] font-semibold transition-colors"
32
+ >
33
+ Nanti Saja
34
+ </button>
35
+ </div>
36
+ </DialogContent>
37
+ </Dialog>
38
+ );
39
+ }
apps/web/src/components/routes/LandingPage.tsx CHANGED
@@ -3,6 +3,7 @@ import { Button } from "@labas/ui/components/button";
3
  import { Card, CardContent } from "@labas/ui/components/card";
4
  import { MaterialIcon } from "@/components/ui/MaterialIcon";
5
  import { useState } from "react";
 
6
 
7
  function FaqItem({ question, answer, isDark }: { question: string; answer: string; isDark?: boolean }) {
8
  const [isOpen, setIsOpen] = useState(false);
@@ -37,8 +38,11 @@ function FaqItem({ question, answer, isDark }: { question: string; answer: strin
37
  }
38
 
39
  export function LandingPage() {
 
 
40
  return (
41
  <div className="min-h-screen bg-[var(--warm-cream)] flex flex-col font-sans selection:bg-[var(--matcha-300)] selection:text-[var(--clay-black)]">
 
42
  {/* Navbar */}
43
  <nav className="w-full px-6 py-4 md:px-12 lg:px-16 flex items-center justify-between max-w-7xl mx-auto z-50 sticky top-0 bg-[var(--warm-cream)] border-b-2 border-[var(--oat-border)]">
44
  <div className="flex items-center gap-3">
@@ -46,6 +50,15 @@ export function LandingPage() {
46
  <span className="font-headline font-semibold text-2xl tracking-[-0.64px] text-[var(--clay-black)] hidden sm:block">Labas</span>
47
  </div>
48
  <div className="flex items-center gap-2 sm:gap-4">
 
 
 
 
 
 
 
 
 
49
  <a href="https://saweria.co/rogasper" target="_blank" rel="noopener noreferrer" className="text-[var(--warm-charcoal)] hover:text-[var(--pomegranate-400)] transition-colors p-2 hidden md:flex items-center gap-2" aria-label="Support via Saweria">
50
  <MaterialIcon name="favorite" className="text-xl" />
51
  <span className="font-semibold text-base">Support</span>
@@ -495,6 +508,14 @@ export function LandingPage() {
495
  <MaterialIcon name="local_cafe" />
496
  <span className="font-semibold">Ko-fi</span>
497
  </a>
 
 
 
 
 
 
 
 
498
  <a href="https://rogasper.com" target="_blank" rel="noopener noreferrer" className="flex items-center gap-2 hover:text-[var(--matcha-700)] transition-colors">
499
  <MaterialIcon name="language" />
500
  <span className="font-semibold">rogasper.com</span>
 
3
  import { Card, CardContent } from "@labas/ui/components/card";
4
  import { MaterialIcon } from "@/components/ui/MaterialIcon";
5
  import { useState } from "react";
6
+ import { CommunityModal } from "@/components/CommunityModal";
7
 
8
  function FaqItem({ question, answer, isDark }: { question: string; answer: string; isDark?: boolean }) {
9
  const [isOpen, setIsOpen] = useState(false);
 
38
  }
39
 
40
  export function LandingPage() {
41
+ const [communityModalOpen, setCommunityModalOpen] = useState(false);
42
+
43
  return (
44
  <div className="min-h-screen bg-[var(--warm-cream)] flex flex-col font-sans selection:bg-[var(--matcha-300)] selection:text-[var(--clay-black)]">
45
+ <CommunityModal isOpen={communityModalOpen} onOpenChange={setCommunityModalOpen} />
46
  {/* Navbar */}
47
  <nav className="w-full px-6 py-4 md:px-12 lg:px-16 flex items-center justify-between max-w-7xl mx-auto z-50 sticky top-0 bg-[var(--warm-cream)] border-b-2 border-[var(--oat-border)]">
48
  <div className="flex items-center gap-3">
 
50
  <span className="font-headline font-semibold text-2xl tracking-[-0.64px] text-[var(--clay-black)] hidden sm:block">Labas</span>
51
  </div>
52
  <div className="flex items-center gap-2 sm:gap-4">
53
+ <button
54
+ type="button"
55
+ onClick={() => setCommunityModalOpen(true)}
56
+ className="text-[var(--warm-charcoal)] hover:text-[var(--matcha-700)] transition-colors p-2 hidden md:flex items-center gap-2"
57
+ aria-label="Komunitas WhatsApp"
58
+ >
59
+ <MaterialIcon name="forum" className="text-xl" />
60
+ <span className="font-semibold text-base">Komunitas</span>
61
+ </button>
62
  <a href="https://saweria.co/rogasper" target="_blank" rel="noopener noreferrer" className="text-[var(--warm-charcoal)] hover:text-[var(--pomegranate-400)] transition-colors p-2 hidden md:flex items-center gap-2" aria-label="Support via Saweria">
63
  <MaterialIcon name="favorite" className="text-xl" />
64
  <span className="font-semibold text-base">Support</span>
 
508
  <MaterialIcon name="local_cafe" />
509
  <span className="font-semibold">Ko-fi</span>
510
  </a>
511
+ <button
512
+ type="button"
513
+ onClick={() => setCommunityModalOpen(true)}
514
+ className="flex items-center gap-2 hover:text-[#128C7E] transition-colors"
515
+ >
516
+ <MaterialIcon name="forum" />
517
+ <span className="font-semibold">Komunitas WhatsApp</span>
518
+ </button>
519
  <a href="https://rogasper.com" target="_blank" rel="noopener noreferrer" className="flex items-center gap-2 hover:text-[var(--matcha-700)] transition-colors">
520
  <MaterialIcon name="language" />
521
  <span className="font-semibold">rogasper.com</span>
apps/web/src/components/settings/AccountSettings.tsx CHANGED
@@ -6,6 +6,7 @@ import { Button } from "@labas/ui/components/button";
6
  import { toast } from "sonner";
7
  import { getErrorMessage } from "@/lib/error-utils";
8
  import { authClient } from "@/lib/auth-client";
 
9
 
10
  export function AccountSettings() {
11
  const { data: session } = authClient.useSession();
@@ -158,6 +159,8 @@ export function AccountSettings() {
158
  </form>
159
  </CardContent>
160
  </Card>
 
 
161
  </div>
162
  );
163
  }
 
6
  import { toast } from "sonner";
7
  import { getErrorMessage } from "@/lib/error-utils";
8
  import { authClient } from "@/lib/auth-client";
9
+ import { CommunityCard } from "@/components/settings/CommunityCard";
10
 
11
  export function AccountSettings() {
12
  const { data: session } = authClient.useSession();
 
159
  </form>
160
  </CardContent>
161
  </Card>
162
+
163
+ <CommunityCard />
164
  </div>
165
  );
166
  }
apps/web/src/components/settings/CommunityCard.tsx ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@labas/ui/components/card";
2
+ import { CommunityChannelList } from "@/components/CommunityChannelList";
3
+
4
+ export function CommunityCard() {
5
+ return (
6
+ <Card className="clay-shadow bg-[var(--pure-white)] border-2 border-[var(--oat-border)] rounded-[var(--radius-xl)]">
7
+ <CardHeader>
8
+ <CardTitle className="font-headline text-[var(--clay-black)]">Komunitas</CardTitle>
9
+ <CardDescription className="text-[var(--warm-charcoal)]">
10
+ WhatsApp untuk chat cepat; Discord untuk diskusi terstruktur (segera hadir).
11
+ </CardDescription>
12
+ </CardHeader>
13
+ <CardContent>
14
+ <CommunityChannelList variant="card" />
15
+ </CardContent>
16
+ </Card>
17
+ );
18
+ }
apps/web/src/components/sidebar.tsx CHANGED
@@ -5,6 +5,7 @@ import { authClient } from "@/lib/auth-client";
5
  import { trpc } from "@/utils/trpc";
6
  import { useSidebar } from "@/hooks/use-sidebar";
7
  import { triggerGlobalTour } from "@/components/TourGuide";
 
8
 
9
  interface NavItem {
10
  to: string;
@@ -91,6 +92,7 @@ export function Sidebar() {
91
  const { collapsed, toggle } = useSidebar();
92
  const { data: session } = authClient.useSession();
93
  const isLoggedIn = !!session;
 
94
 
95
  const { data: adminData } = useQuery(
96
  trpc.admin.isAdmin.queryOptions(undefined, { enabled: isLoggedIn }),
@@ -104,6 +106,7 @@ export function Sidebar() {
104
 
105
  return (
106
  <>
 
107
  {/* Desktop Sidebar */}
108
  <aside
109
  className={`fixed left-0 top-0 h-full flex flex-col z-40 bg-[var(--warm-cream)] border-r border-[var(--oat-border)] hidden md:flex transition-all duration-300 overflow-y-auto ${
@@ -182,6 +185,19 @@ export function Sidebar() {
182
  );
183
  })}
184
 
 
 
 
 
 
 
 
 
 
 
 
 
 
185
  {/* Help Tour Button */}
186
  <button
187
  onClick={triggerGlobalTour}
 
5
  import { trpc } from "@/utils/trpc";
6
  import { useSidebar } from "@/hooks/use-sidebar";
7
  import { triggerGlobalTour } from "@/components/TourGuide";
8
+ import { CommunityModal } from "@/components/CommunityModal";
9
 
10
  interface NavItem {
11
  to: string;
 
92
  const { collapsed, toggle } = useSidebar();
93
  const { data: session } = authClient.useSession();
94
  const isLoggedIn = !!session;
95
+ const [communityModalOpen, setCommunityModalOpen] = useState(false);
96
 
97
  const { data: adminData } = useQuery(
98
  trpc.admin.isAdmin.queryOptions(undefined, { enabled: isLoggedIn }),
 
106
 
107
  return (
108
  <>
109
+ <CommunityModal isOpen={communityModalOpen} onOpenChange={setCommunityModalOpen} />
110
  {/* Desktop Sidebar */}
111
  <aside
112
  className={`fixed left-0 top-0 h-full flex flex-col z-40 bg-[var(--warm-cream)] border-r border-[var(--oat-border)] hidden md:flex transition-all duration-300 overflow-y-auto ${
 
185
  );
186
  })}
187
 
188
+ <button
189
+ type="button"
190
+ onClick={() => setCommunityModalOpen(true)}
191
+ aria-label="Komunitas WhatsApp"
192
+ className={`flex items-center gap-3 rounded-[var(--radius-lg)] transition-all clay-hover cursor-pointer text-[var(--warm-charcoal)] hover:bg-[var(--oat-light)] hover:text-[var(--clay-black)] w-full ${
193
+ collapsed ? "justify-center py-3 px-2" : "py-3 px-3 text-left"
194
+ }`}
195
+ title={collapsed ? "Komunitas" : undefined}
196
+ >
197
+ <NavIcon name="forum" />
198
+ {!collapsed && <span className="text-sm font-medium">Komunitas</span>}
199
+ </button>
200
+
201
  {/* Help Tour Button */}
202
  <button
203
  onClick={triggerGlobalTour}
apps/web/src/lib/community-links.ts ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export type CommunityPlatform = "whatsapp" | "discord";
2
+
3
+ export type CommunityChannel = {
4
+ id: string;
5
+ platform: CommunityPlatform;
6
+ name: string;
7
+ description: string;
8
+ /** null = belum live (tampil sebagai "Segera hadir") */
9
+ href: string | null;
10
+ icon: string;
11
+ };
12
+
13
+ export const COMMUNITY_PLATFORM_META: Record<
14
+ CommunityPlatform,
15
+ { label: string; icon: string; linkClass: string; iconClass: string }
16
+ > = {
17
+ whatsapp: {
18
+ label: "WhatsApp",
19
+ icon: "chat",
20
+ linkClass:
21
+ "bg-[#25D366]/10 border-[#25D366]/30 hover:bg-[#25D366]/20",
22
+ iconClass: "text-[#128C7E]",
23
+ },
24
+ discord: {
25
+ label: "Discord",
26
+ icon: "forum",
27
+ linkClass:
28
+ "bg-[#5865F2]/10 border-[#5865F2]/30 hover:bg-[#5865F2]/20",
29
+ iconClass: "text-[#5865F2]",
30
+ },
31
+ };
32
+
33
+ /** Satu sumber data — tambah Discord cukup isi `href` di entri bawah. */
34
+ export const COMMUNITY_CHANNELS: CommunityChannel[] = [
35
+ {
36
+ id: "labas-whatsapp",
37
+ platform: "whatsapp",
38
+ name: "Labas — Open Source",
39
+ description: "Laporkan bug, usulkan fitur, atau diskusi pengembangan Labas.",
40
+ href: "https://chat.whatsapp.com/JT6bDIXMQ5x6SUQCA1aDWB",
41
+ icon: "groups",
42
+ },
43
+ {
44
+ id: "learning-whatsapp",
45
+ platform: "whatsapp",
46
+ name: "Rogasper Learning",
47
+ description: "Berbagi tips, trik, dan berita terbaru dari Rogasper Learning.",
48
+ href: "https://chat.whatsapp.com/IrQOUGB3jhR7WnOoilCill",
49
+ icon: "school",
50
+ },
51
+ // Aktifkan saat server Discord siap — cukup isi href:
52
+ {
53
+ id: "labas-discord",
54
+ platform: "discord",
55
+ name: "Labas Discord",
56
+ description: "Diskusi async, bug report terstruktur, dan pengumuman fitur baru.",
57
+ href: null,
58
+ icon: "chat",
59
+ },
60
+ ];
61
+
62
+ export const COMMUNITY_PROMPT_STORAGE_KEY = "labas_community_prompt_seen";
63
+
64
+ const PLATFORM_ORDER: CommunityPlatform[] = ["whatsapp", "discord"];
65
+
66
+ export function getCommunitySections() {
67
+ return PLATFORM_ORDER.map((platform) => ({
68
+ platform,
69
+ meta: COMMUNITY_PLATFORM_META[platform],
70
+ channels: COMMUNITY_CHANNELS.filter((c) => c.platform === platform),
71
+ })).filter((section) => section.channels.length > 0);
72
+ }
apps/web/src/routes/dashboard.tsx CHANGED
@@ -9,6 +9,8 @@ import { Card, CardContent } from "@labas/ui/components/card";
9
  import { MaterialIcon } from "@/components/ui/MaterialIcon";
10
  import { TourGuide } from "@/components/TourGuide";
11
  import { DonationModal } from "@/components/DonationModal";
 
 
12
 
13
  export const Route = createFileRoute("/dashboard")({
14
  staticData: routeShell.app,
@@ -38,25 +40,36 @@ function HomeComponent() {
38
  const { session } = Route.useRouteContext();
39
  const [donationModalOpen, setDonationModalOpen] = useState(false);
40
  const [donationTrigger, setDonationTrigger] = useState<"exam" | "generate" | null>(null);
 
 
 
 
 
 
 
 
41
 
42
  useEffect(() => {
43
  const prompt = localStorage.getItem("pendingDonationPrompt") as "exam" | "generate" | null;
44
  const lastPromptStr = localStorage.getItem("lastDonationPromptTime");
45
  const lastPromptTime = lastPromptStr ? parseInt(lastPromptStr, 10) : 0;
46
-
47
- // 24 hours cooldown
48
  const COOLDOWN = 24 * 60 * 60 * 1000;
49
  const now = Date.now();
50
 
 
51
  if (prompt) {
52
  localStorage.removeItem("pendingDonationPrompt");
53
-
54
  if (now - lastPromptTime > COOLDOWN) {
55
  setDonationTrigger(prompt);
56
  setDonationModalOpen(true);
57
  localStorage.setItem("lastDonationPromptTime", now.toString());
 
58
  }
59
  }
 
 
 
 
60
  }, []);
61
 
62
  const overview = useQuery(trpc.stats.overview.queryOptions());
@@ -82,6 +95,7 @@ function HomeComponent() {
82
  onOpenChange={setDonationModalOpen}
83
  triggerAction={donationTrigger}
84
  />
 
85
  {/* Header */}
86
  <section className="mb-8">
87
  <h1 className="text-4xl font-headline font-extrabold text-[var(--clay-black)] tracking-tight">
 
9
  import { MaterialIcon } from "@/components/ui/MaterialIcon";
10
  import { TourGuide } from "@/components/TourGuide";
11
  import { DonationModal } from "@/components/DonationModal";
12
+ import { CommunityModal } from "@/components/CommunityModal";
13
+ import { COMMUNITY_PROMPT_STORAGE_KEY } from "@/lib/community-links";
14
 
15
  export const Route = createFileRoute("/dashboard")({
16
  staticData: routeShell.app,
 
40
  const { session } = Route.useRouteContext();
41
  const [donationModalOpen, setDonationModalOpen] = useState(false);
42
  const [donationTrigger, setDonationTrigger] = useState<"exam" | "generate" | null>(null);
43
+ const [communityModalOpen, setCommunityModalOpen] = useState(false);
44
+
45
+ const handleCommunityModalChange = (open: boolean) => {
46
+ setCommunityModalOpen(open);
47
+ if (!open) {
48
+ localStorage.setItem(COMMUNITY_PROMPT_STORAGE_KEY, "1");
49
+ }
50
+ };
51
 
52
  useEffect(() => {
53
  const prompt = localStorage.getItem("pendingDonationPrompt") as "exam" | "generate" | null;
54
  const lastPromptStr = localStorage.getItem("lastDonationPromptTime");
55
  const lastPromptTime = lastPromptStr ? parseInt(lastPromptStr, 10) : 0;
 
 
56
  const COOLDOWN = 24 * 60 * 60 * 1000;
57
  const now = Date.now();
58
 
59
+ let showedDonation = false;
60
  if (prompt) {
61
  localStorage.removeItem("pendingDonationPrompt");
 
62
  if (now - lastPromptTime > COOLDOWN) {
63
  setDonationTrigger(prompt);
64
  setDonationModalOpen(true);
65
  localStorage.setItem("lastDonationPromptTime", now.toString());
66
+ showedDonation = true;
67
  }
68
  }
69
+
70
+ if (!showedDonation && !localStorage.getItem(COMMUNITY_PROMPT_STORAGE_KEY)) {
71
+ setCommunityModalOpen(true);
72
+ }
73
  }, []);
74
 
75
  const overview = useQuery(trpc.stats.overview.queryOptions());
 
95
  onOpenChange={setDonationModalOpen}
96
  triggerAction={donationTrigger}
97
  />
98
+ <CommunityModal isOpen={communityModalOpen} onOpenChange={handleCommunityModalChange} />
99
  {/* Header */}
100
  <section className="mb-8">
101
  <h1 className="text-4xl font-headline font-extrabold text-[var(--clay-black)] tracking-tight">