From 0f8b1d1148b62e2989d06a572c635913f72b34f3 Mon Sep 17 00:00:00 2001 From: Mnpezz <93685835+Mnpezz@users.noreply.github.com> Date: Sun, 22 Feb 2026 23:50:16 -0600 Subject: [PATCH 01/42] event host stats --- src/pages/Profile.tsx | 58 ++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 54 insertions(+), 4 deletions(-) diff --git a/src/pages/Profile.tsx b/src/pages/Profile.tsx index 693249d..745fb71 100644 --- a/src/pages/Profile.tsx +++ b/src/pages/Profile.tsx @@ -258,6 +258,56 @@ export function Profile() { )} + + {/* Stats section */} + {!author.isLoading && !isLoadingCreated && ( +
+
+ {createdEvents.length} + Total Events +
+
+ + {createdEvents.filter((event) => { + const startTag = event.tags.find((t) => t[0] === "start")?.[1]; + if (!startTag) return false; + + try { + let startTimeMs = 0; + if (event.kind === 31922) { + // YYYY-MM-DD + startTimeMs = new Date(startTag).getTime(); + } else if (event.kind === 31923) { + // Unix timestamp in seconds + startTimeMs = parseInt(startTag) * 1000; + } + + // For current/upcoming events, we check if start time is in the future + // Note: We could also check end time if available to include ongoing events + const endTag = event.tags.find((t) => t[0] === "end")?.[1]; + if (endTag) { + let endTimeMs = 0; + if (event.kind === 31922) { + endTimeMs = new Date(endTag).getTime(); + } else if (event.kind === 31923) { + endTimeMs = parseInt(endTag) * 1000; + } + // If end time is in future or today + return endTimeMs >= Date.now(); + } + + // If no end time, we consider it current if it's within the last 24h or in the future + // Just subtracting 24h as a rough buffer for ongoing "day of" events + return startTimeMs >= Date.now() - 24 * 60 * 60 * 1000; + } catch (e) { + return false; + } + }).length} + + Current Events +
+
+ )} @@ -385,15 +435,15 @@ export function Profile() { status === "accepted" ? "bg-green-500/10 text-green-500" : status === "tentative" - ? "bg-yellow-500/10 text-yellow-500" - : "bg-red-500/10 text-red-500" + ? "bg-yellow-500/10 text-yellow-500" + : "bg-red-500/10 text-red-500" } > {status === "accepted" ? "Going" : status === "tentative" - ? "Maybe" - : "Can't Go"} + ? "Maybe" + : "Can't Go"} From eeb365816efe9770534f3b63e8b977644359cb8b Mon Sep 17 00:00:00 2001 From: Mnpezz <93685835+Mnpezz@users.noreply.github.com> Date: Sun, 22 Feb 2026 23:51:23 -0600 Subject: [PATCH 02/42] Update location search --- src/components/LocationSearch.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/LocationSearch.tsx b/src/components/LocationSearch.tsx index 77f9a8e..f677512 100644 --- a/src/components/LocationSearch.tsx +++ b/src/components/LocationSearch.tsx @@ -97,8 +97,8 @@ export function LocationSearch({ >
{value ? ( - {value} @@ -116,7 +116,7 @@ export function LocationSearch({ className="w-[--radix-popover-trigger-width] p-0 max-w-[90vw]" align="start" > - + Date: Mon, 23 Feb 2026 06:54:45 -0600 Subject: [PATCH 03/42] Update map pins Modified the useEvents hook in src/lib/eventUtils.ts (lines 121-134). --- src/lib/eventUtils.ts | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/src/lib/eventUtils.ts b/src/lib/eventUtils.ts index c6f3048..d2ea579 100644 --- a/src/lib/eventUtils.ts +++ b/src/lib/eventUtils.ts @@ -115,16 +115,35 @@ export function useEvents(options?: { const events = await nostr.query(filters, { signal }); if (!events || events.length === 0) { + // If no new events, just use the cache + const [cachedEvents, cachedRsvps] = await Promise.all([ + getCachedEvents(), + includeRSVPs ? getCachedRSVPs() : Promise.resolve([]), + ]); + + if (cachedEvents.length > 0 || cachedRsvps.length > 0) { + const allCached = [...cachedEvents, ...cachedRsvps] as AllEventTypes[]; + return deduplicateEvents(allCached); + } return []; } const typedEvents = events as unknown as AllEventTypes[]; - const result = deduplicateEvents(typedEvents); - // Cache events in background (non-blocking) - Promise.all(result.map(event => cacheEvent(event))).catch(() => {}); + // Cache new events in background (non-blocking) + Promise.all(typedEvents.map(event => cacheEvent(event))).catch(() => { }); + + // Fetch current cache to merge with new events + const [cachedEvents, cachedRsvps] = await Promise.all([ + getCachedEvents(), + includeRSVPs ? getCachedRSVPs() : Promise.resolve([]), + ]); + + // Merge and deduplicate + const allCached = [...cachedEvents, ...cachedRsvps] as AllEventTypes[]; + const combined = [...allCached, ...typedEvents]; - return result; + return deduplicateEvents(combined); } catch { return []; } From e3323f82a8da48e14f0ba124ddc1fbb03f5e9af8 Mon Sep 17 00:00:00 2001 From: Mnpezz <93685835+Mnpezz@users.noreply.github.com> Date: Mon, 23 Feb 2026 07:18:10 -0600 Subject: [PATCH 04/42] links and map --- src/components/LocationDisplay.tsx | 37 +++++++++++++++++++----------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/src/components/LocationDisplay.tsx b/src/components/LocationDisplay.tsx index 48fab63..938d129 100644 --- a/src/components/LocationDisplay.tsx +++ b/src/components/LocationDisplay.tsx @@ -27,24 +27,24 @@ const isURL = (text: string): boolean => { const getLocationIcon = (location: string) => { const lowerLocation = location.toLowerCase(); - + // Check if it's a URL if (isURL(location)) { - if (lowerLocation.includes("maps.google") || - lowerLocation.includes("goo.gl/maps") || - lowerLocation.includes("openstreetmap") || - lowerLocation.includes("mapquest") || - lowerLocation.includes("bing.com/maps")) { + if (lowerLocation.includes("maps.google") || + lowerLocation.includes("goo.gl/maps") || + lowerLocation.includes("openstreetmap") || + lowerLocation.includes("mapquest") || + lowerLocation.includes("bing.com/maps")) { return ; } return ; } - + // Check if it looks like a file path if (location.includes("/") && !location.includes(" ")) { return ; } - + // Default to map pin for addresses return ; }; @@ -53,12 +53,12 @@ const formatURL = (location: string): string => { if (location.startsWith("http://") || location.startsWith("https://")) { return location; } - + // Add https:// prefix if it looks like a domain if (/^[a-zA-Z0-9-]+\.[a-zA-Z]{2,}/.test(location.trim())) { return `https://${location}`; } - + return location; }; @@ -70,7 +70,7 @@ export function LocationDisplay({ location, className }: LocationDisplayProps) { const locationIsClickable = isURL(location); const icon = getLocationIcon(location); const displayLocation = location.trim(); - + if (locationIsClickable) { return ( + {icon} {displayLocation} -
+ + ); -} \ No newline at end of file +} From 1826cff6313911baa8dacb4fd90d3b100a4cdfd2 Mon Sep 17 00:00:00 2001 From: Mnpezz <93685835+Mnpezz@users.noreply.github.com> Date: Mon, 23 Feb 2026 07:19:09 -0600 Subject: [PATCH 05/42] links and map --- src/pages/EventDetail.tsx | 48 +++++++++++++++++++++++++++++++-------- 1 file changed, 39 insertions(+), 9 deletions(-) diff --git a/src/pages/EventDetail.tsx b/src/pages/EventDetail.tsx index ac50e5c..b262c52 100644 --- a/src/pages/EventDetail.tsx +++ b/src/pages/EventDetail.tsx @@ -75,6 +75,34 @@ function getStatusLabel(status: string) { } } +function LinkifiedText({ text }: { text: string }) { + if (!text) return null; + const urlRegex = /(https?:\/\/[^\s]+)/g; + const parts = text.split(urlRegex); + + return ( + <> + {parts.map((part, i) => { + if (part.match(urlRegex)) { + return ( + e.stopPropagation()} + > + {part} + + ); + } + return {part}; + })} + + ); +} + function EventAuthor({ pubkey }: { pubkey: string }) { const author = useAuthor(pubkey); const metadata = author.data?.metadata; @@ -139,8 +167,8 @@ export function EventDetail() { decodedEvent.type === "raw" ? decodedEvent.data : decodedEvent.type === "note" - ? decodedEvent.data - : decodedEvent.data.id; + ? decodedEvent.data + : decodedEvent.data.id; eventIdFromIdentifier = eventIdDecoded; } } catch (error) { @@ -354,8 +382,8 @@ export function EventDetail() {
{isLiveEventType(event) && getPlatformIcon(event) && ( - {getPlatformIcon(event)?.icon} @@ -394,7 +422,9 @@ export function EventDetail() {

📝 Description

-

{event.content}

+
+ +
{event.kind === 30311 && getLiveEventStatus(event as LiveEvent) === 'live' ? ( @@ -446,7 +476,7 @@ export function EventDetail() { )}
- + {getViewingUrl(event) && (

@@ -475,7 +505,7 @@ export function EventDetail() {

)} - + {event.kind === 30311 && (

This is a NIP-53 live event. Join the stream using the URL above or check the event description for more details.

From c64aca5beee596dd1b965bca3bb1c2df886a9aa0 Mon Sep 17 00:00:00 2001 From: Mnpezz <93685835+Mnpezz@users.noreply.github.com> Date: Mon, 23 Feb 2026 08:10:40 -0600 Subject: [PATCH 06/42] stats card --- src/pages/Home.tsx | 386 ++++++++++++++++++++++++++++----------------- 1 file changed, 239 insertions(+), 147 deletions(-) diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx index 990b9f1..659152b 100644 --- a/src/pages/Home.tsx +++ b/src/pages/Home.tsx @@ -29,7 +29,7 @@ import { CollapsibleContent, CollapsibleTrigger, } from "@/components/ui/collapsible"; -import { CalendarIcon, Search, X, Filter, ChevronDown, Grid3X3, Calendar as CalendarViewIcon, Map as MapIcon } from "lucide-react"; +import { CalendarIcon, Search, X, Filter, ChevronDown, Grid3X3, Calendar as CalendarViewIcon, Map as MapIcon, CalendarDays, Users, PartyPopper } from "lucide-react"; import { format } from "date-fns"; import { cn } from "@/lib/utils"; import type { DateRange } from "react-day-picker"; @@ -47,7 +47,7 @@ import { formatAmount } from "@/lib/lightning"; export function Home() { const [viewMode, setViewMode] = useState<"grid" | "calendar" | "map">("grid"); - + // Use regular loading for both views - simpler and more reliable const { data: allEventsData, isLoading, error } = useEvents({ limit: 500, // Higher limit to get more events @@ -58,8 +58,11 @@ export function Home() { // State for client-side pagination in grid view const [displayedEventCount, setDisplayedEventCount] = useState(50); - + const allEvents = useMemo(() => allEventsData || [], [allEventsData]); + const totalRSVPs = useMemo(() => { + return allEvents.filter(e => e.kind === 31925 && e.tags.find(t => t[0] === "status")?.[1] === "accepted").length; + }, [allEvents]); // Get unique pubkeys from all events for metadata lookup const uniquePubkeys = useMemo(() => { @@ -100,7 +103,7 @@ export function Home() { ? event.tags.find((tag) => tag[0] === "starts")?.[1] : event.tags.find((tag) => tag[0] === "start")?.[1]; const endTime = event.tags.find((tag) => tag[0] === "end")?.[1] || - event.tags.find((tag) => tag[0] === "ends")?.[1]; + event.tags.find((tag) => tag[0] === "ends")?.[1]; if (!startTime) return false; let eventStart: number; @@ -239,25 +242,25 @@ export function Home() { // Filter by keyword (location, username, title, description) if (keywordFilter) { const keyword = keywordFilter.toLowerCase(); - + // Get event fields to search const location = event.tags.find((tag) => tag[0] === "location")?.[1]?.toLowerCase() || ""; const title = event.tags.find((tag) => tag[0] === "title")?.[1]?.toLowerCase() || ""; const description = event.content.toLowerCase(); - + // Get author metadata for username search const authorMetadata = authorsMetadata[event.pubkey]; const username = authorMetadata?.name?.toLowerCase() || ""; const displayName = authorMetadata?.display_name?.toLowerCase() || ""; - + // Check if keyword matches any of these fields - const matchesKeyword = + const matchesKeyword = location.includes(keyword) || title.includes(keyword) || description.includes(keyword) || username.includes(keyword) || displayName.includes(keyword); - + if (!matchesKeyword) return false; } @@ -288,12 +291,12 @@ export function Home() { // Apply sorting - either by distance or by time const sortedEvents = useMemo(() => { if (!allFilteredEvents) return []; - + if (sortByDistance && locationCoords) { // Sort by distance from selected location return sortEventsByDistance(allFilteredEvents, locationCoords); } - + // Default: sort by start time return [...allFilteredEvents].sort((a, b) => { const getEventStartTime = (event: DateBasedEvent | TimeBasedEvent | LiveEvent | RoomMeeting | InteractiveRoom) => { @@ -343,7 +346,7 @@ export function Home() { // For grid view, limit the displayed events for pagination const filteredEvents = viewMode === "calendar" || viewMode === "map" - ? sortedEvents + ? sortedEvents : sortedEvents?.slice(0, displayedEventCount); const clearFilters = () => { @@ -368,7 +371,7 @@ export function Home() { // Load more functionality for grid view const canLoadMore = viewMode === "grid" && sortedEvents && displayedEventCount < sortedEvents.length; - + const loadMoreEvents = () => { setDisplayedEventCount(prev => prev + 50); }; @@ -378,7 +381,7 @@ export function Home() { const lastEventElementRef = useCallback((node: HTMLElement | null) => { if (isLoading) return; if (observer.current) observer.current.disconnect(); - + observer.current = new IntersectionObserver(entries => { if (entries[0].isIntersecting && canLoadMore) { loadMoreEvents(); @@ -387,7 +390,7 @@ export function Home() { threshold: 0.1, rootMargin: '100px' }); - + if (node) observer.current.observe(node); }, [isLoading, canLoadMore]); @@ -414,7 +417,7 @@ export function Home() { Find your next adventure, connect with your community

- + {/* View Mode Toggle */} - Grid - Calendar - @@ -452,6 +455,96 @@ export function Home() { + {/* Global Stats Overview */} + {calendarEvents && calendarEvents.length > 0 && ( +
+ + +
+ +
+

+ {calendarEvents.length} +

+

+ Total Events +

+
+
+ + + +
+ +
+

+ { + calendarEvents.filter((event) => { + const startDate = new Date(); + startDate.setHours(0, 0, 0, 0); + const endDate = new Date(); + endDate.setHours(23, 59, 59, 999); + + const startTime = (event.kind === 30311 || event.kind === 30312 || event.kind === 30313) + ? event.tags.find((tag) => tag[0] === "starts")?.[1] + : event.tags.find((tag) => tag[0] === "start")?.[1]; + + if (!startTime) return false; + + let timeMs = 0; + if (event.kind === 31922) { + if (startTime.match(/^\d{10}$/)) timeMs = parseInt(startTime) * 1000; + else if (startTime.match(/^\d{13}$/)) timeMs = parseInt(startTime); + else { + const [y, m, d] = startTime.split('-').map(Number); + timeMs = new Date(y, m - 1, d, 0, 0, 0).getTime(); + } + } else { + timeMs = parseInt(startTime) * 1000; + } + + return timeMs >= startDate.getTime() && timeMs <= endDate.getTime(); + }).length + } +

+

+ Happening Today +

+
+
+ + + +
+ +
+

+ {uniquePubkeys.length} +

+

+ Organizers +

+
+
+ + + +
+ + + +
+

+ {totalRSVPs} +

+

+ Total RSVPs +

+
+
+
+ )} + {/* Filters */} tag[0] === "image")?.[1]; const eventIdentifier = createEventIdentifier(event); - + // Extract pricing information const price = event.tags.find((tag) => tag[0] === "price")?.[1]; const lightningAddress = event.tags.find((tag) => tag[0] === "lud16")?.[1]; const isPaidEvent = price && lightningAddress; - + // Calculate attendee count from RSVPs - const eventAddress = event.tags.find((tag) => tag[0] === "d")?.[1] - ? `${event.kind}:${event.pubkey}:${event.tags.find((tag) => tag[0] === "d")?.[1]}` + const eventAddress = event.tags.find((tag) => tag[0] === "d")?.[1] + ? `${event.kind}:${event.pubkey}:${event.tags.find((tag) => tag[0] === "d")?.[1]}` : null; const eventId = event.id; - + // Get RSVPs for this event from allEventsData const rsvpEvents = (allEventsData || []) .filter((e): e is EventRSVP => e.kind === 31925) @@ -742,7 +835,7 @@ export function Home() { const hasAddress = eventAddress && e.tags.some((tag) => tag[0] === "a" && tag[1] === eventAddress); return hasEventId || hasAddress; }); - + // Get most recent RSVP for each user const latestRSVPs = rsvpEvents.reduce((acc, curr) => { const existingRSVP = acc.find((e) => e.pubkey === curr.pubkey); @@ -752,19 +845,19 @@ export function Home() { } return acc; }, [] as EventRSVP[]); - + // Count accepted RSVPs const acceptedRSVPs = latestRSVPs.filter( (e) => e.tags.find((tag) => tag[0] === "status")?.[1] === "accepted" ); const attendeeCount = acceptedRSVPs.length; - + // Check if this is a live event const live = isLiveEvent(event); const inPerson = isInPersonEvent(event); const streamingUrl = getStreamingUrl(event); const liveStatus = event.kind === 30311 ? getLiveEventStatus(event as LiveEvent) : null; - + // Get platform icon for live events const platformIcon = isLiveEventType(event) ? getPlatformIcon(event) : null; @@ -772,129 +865,128 @@ export function Home() { const isLastElement = index === filteredEvents.length - 1; return ( -
-
- {title} -
- - {/* Live Event Badge */} - {live && ( -
- - {liveStatus === 'live' ? ( - <> - - LIVE NOW - - ) : ( - <> - - LIVE EVENT - - )} - -
- )} - - {/* In-Person Badge */} - {inPerson && !live && ( -
- - 📍 In-Person - -
- )} -
- - - {platformIcon && ( - - {platformIcon.icon} - - )} - {title} - - {startTime && ( - - - - )} - - -

- {description} -

- - {/* Additional event details */} -
- {/* Pricing information */} - {isPaidEvent ? ( -
- 🎟️ - - {formatAmount(parseInt(price))} - -
- ) : ( -
- 🆓 - - Free Event - -
- )} - - {/* Attendee count (show for any count, but with different styling) */} - {attendeeCount > 0 && ( -
5 - ? 'bg-green-50 dark:bg-green-950/20 border-green-200 dark:border-green-800' - : 'bg-blue-50 dark:bg-blue-950/20 border-blue-200 dark:border-blue-800' - }`}> - 5 ? 'text-green-600 dark:text-green-400' : 'text-blue-600 dark:text-blue-400'}`}>👥 - 5 ? 'text-green-800 dark:text-green-200' : 'text-blue-800 dark:text-blue-200'}`}> - {attendeeCount} {attendeeCount === 1 ? 'person' : 'people'} going - +
+ {title} +
+ + {/* Live Event Badge */} + {live && ( +
+ + {liveStatus === 'live' ? ( + <> + + LIVE NOW + + ) : ( + <> + + LIVE EVENT + + )} +
)} - - {/* Live stream or location */} - {streamingUrl ? ( -
- 🎥 - Live Stream -
- ) : location && ( -
- 📍 - {location} - {sortByDistance && event.distance !== undefined && ( - - {formatDistance(event.distance)} away - - )} + + {/* In-Person Badge */} + {inPerson && !live && ( +
+ + 📍 In-Person +
)}
- + + + {platformIcon && ( + + {platformIcon.icon} + + )} + {title} + + {startTime && ( + + + + )} + + +

+ {description} +

+ + {/* Additional event details */} +
+ {/* Pricing information */} + {isPaidEvent ? ( +
+ 🎟️ + + {formatAmount(parseInt(price))} + +
+ ) : ( +
+ 🆓 + + Free Event + +
+ )} + + {/* Attendee count (show for any count, but with different styling) */} + {attendeeCount > 0 && ( +
5 + ? 'bg-green-50 dark:bg-green-950/20 border-green-200 dark:border-green-800' + : 'bg-blue-50 dark:bg-blue-950/20 border-blue-200 dark:border-blue-800' + }`}> + 5 ? 'text-green-600 dark:text-green-400' : 'text-blue-600 dark:text-blue-400'}`}>👥 + 5 ? 'text-green-800 dark:text-green-200' : 'text-blue-800 dark:text-blue-200'}`}> + {attendeeCount} {attendeeCount === 1 ? 'person' : 'people'} going + +
+ )} + + {/* Live stream or location */} + {streamingUrl ? ( +
+ 🎥 + Live Stream +
+ ) : location && ( +
+ 📍 + {location} + {sortByDistance && event.distance !== undefined && ( + + {formatDistance(event.distance)} away + + )} +
+ )} +
+
From 579f3274671ba673437b2aaca329e763eb8eb9f4 Mon Sep 17 00:00:00 2001 From: Mnpezz <93685835+Mnpezz@users.noreply.github.com> Date: Mon, 23 Feb 2026 08:11:17 -0600 Subject: [PATCH 07/42] stats card --- src/pages/Profile.tsx | 59 ++++++++++++++++++++++++++++++++++++++----- 1 file changed, 52 insertions(+), 7 deletions(-) diff --git a/src/pages/Profile.tsx b/src/pages/Profile.tsx index 745fb71..ae2c2fc 100644 --- a/src/pages/Profile.tsx +++ b/src/pages/Profile.tsx @@ -93,6 +93,24 @@ export function Profile() { staleTime: 30000, }); + const { + data: receivedRsvps = [], + isLoading: isLoadingReceivedRsvps, + } = useQuery({ + queryKey: ["receivedRsvps", pubkey], + queryFn: async ({ signal }) => { + if (!pubkey) return []; + const events = await nostr.query( + [{ kinds: [31925], "#p": [pubkey] }], + { signal: AbortSignal.any([signal, AbortSignal.timeout(3000)]) } + ); + return events as unknown as EventRSVP[]; + }, + enabled: !!pubkey, + retry: 1, + staleTime: 30000, + }); + // Fetch the actual events that were RSVP'd to const { data: rsvpEvents = [], @@ -261,13 +279,13 @@ export function Profile() { {/* Stats section */} {!author.isLoading && !isLoadingCreated && ( -
-
- {createdEvents.length} - Total Events +
+
+ {createdEvents.length} + Total Events
-
- +
+ {createdEvents.filter((event) => { const startTag = event.tags.find((t) => t[0] === "start")?.[1]; if (!startTag) return false; @@ -304,7 +322,34 @@ export function Profile() { } }).length} - Current Events + Current Events +
+
+ {isLoadingRSVPs ? ( + + ) : ( + + {rsvps.filter(r => r.tags.find(t => t[0] === "status")?.[1] === "accepted").length} + + )} + Attending +
+
+ {isLoadingReceivedRsvps ? ( + + ) : ( + + { + // Filter down to accepted RSVPs and get unique pubkeys + Array.from(new Set( + receivedRsvps + .filter(r => r.tags.find(t => t[0] === "status")?.[1] === "accepted") + .map(r => r.pubkey) + )).length + } + + )} + Total Attendees
)} From 33fe1b79e42048e6a829031b042f95d6af55b365 Mon Sep 17 00:00:00 2001 From: Mnpezz <93685835+Mnpezz@users.noreply.github.com> Date: Mon, 23 Feb 2026 08:22:02 -0600 Subject: [PATCH 08/42] Update stats card --- src/pages/Home.tsx | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx index 659152b..d5a85bb 100644 --- a/src/pages/Home.tsx +++ b/src/pages/Home.tsx @@ -1,5 +1,7 @@ import { useEvents } from "@/lib/eventUtils"; import { useMuteList } from "@/hooks/useMuteList"; +import { useNostr } from "@nostrify/react"; +import { useQuery } from "@tanstack/react-query"; import { useAuthorsMetadata } from "@/hooks/useAuthorsMetadata"; import { Card, @@ -59,10 +61,23 @@ export function Home() { // State for client-side pagination in grid view const [displayedEventCount, setDisplayedEventCount] = useState(50); + const { nostr } = useNostr(); + const { data: globalRsvps = [] } = useQuery({ + queryKey: ["globalTotalRsvps"], + queryFn: async ({ signal }) => { + const events = await nostr.query( + [{ kinds: [31925], limit: 5000 }], + { signal: AbortSignal.any([signal, AbortSignal.timeout(5000)]) } + ); + return events; + }, + staleTime: 60000, + }); + const allEvents = useMemo(() => allEventsData || [], [allEventsData]); const totalRSVPs = useMemo(() => { - return allEvents.filter(e => e.kind === 31925 && e.tags.find(t => t[0] === "status")?.[1] === "accepted").length; - }, [allEvents]); + return globalRsvps.filter((e: any) => e.tags.find((t: any) => t[0] === "status")?.[1] === "accepted").length; + }, [globalRsvps]); // Get unique pubkeys from all events for metadata lookup const uniquePubkeys = useMemo(() => { @@ -958,8 +973,8 @@ export function Home() { {/* Attendee count (show for any count, but with different styling) */} {attendeeCount > 0 && (
5 - ? 'bg-green-50 dark:bg-green-950/20 border-green-200 dark:border-green-800' - : 'bg-blue-50 dark:bg-blue-950/20 border-blue-200 dark:border-blue-800' + ? 'bg-green-50 dark:bg-green-950/20 border-green-200 dark:border-green-800' + : 'bg-blue-50 dark:bg-blue-950/20 border-blue-200 dark:border-blue-800' }`}> 5 ? 'text-green-600 dark:text-green-400' : 'text-blue-600 dark:text-blue-400'}`}>👥 5 ? 'text-green-800 dark:text-green-200' : 'text-blue-800 dark:text-blue-200'}`}> From 57101360ec2227307cf781d659d1f15a2f7f842e Mon Sep 17 00:00:00 2001 From: Mnpezz <93685835+Mnpezz@users.noreply.github.com> Date: Mon, 23 Feb 2026 08:37:22 -0600 Subject: [PATCH 09/42] stats card - --- src/pages/Home.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx index d5a85bb..05ab605 100644 --- a/src/pages/Home.tsx +++ b/src/pages/Home.tsx @@ -62,7 +62,7 @@ export function Home() { const [displayedEventCount, setDisplayedEventCount] = useState(50); const { nostr } = useNostr(); - const { data: globalRsvps = [] } = useQuery({ + const { data: globalRsvps = [], isLoading: isLoadingGlobalRsvps } = useQuery({ queryKey: ["globalTotalRsvps"], queryFn: async ({ signal }) => { const events = await nostr.query( @@ -550,7 +550,11 @@ export function Home() {

- {totalRSVPs} + {isLoadingGlobalRsvps ? ( + - + ) : ( + totalRSVPs + )}

Total RSVPs From f64b75c119e7d65d0ab44362670440a5bf47fef4 Mon Sep 17 00:00:00 2001 From: Mnpezz <93685835+Mnpezz@users.noreply.github.com> Date: Mon, 23 Feb 2026 09:48:42 -0600 Subject: [PATCH 10/42] group cal --- src/components/CalendarOptions.tsx | 154 +++++++++++++++++------------ 1 file changed, 93 insertions(+), 61 deletions(-) diff --git a/src/components/CalendarOptions.tsx b/src/components/CalendarOptions.tsx index 24fe412..726a166 100644 --- a/src/components/CalendarOptions.tsx +++ b/src/components/CalendarOptions.tsx @@ -6,8 +6,11 @@ import { DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; -import { Calendar, Download, ExternalLink } from "lucide-react"; +import { Calendar, Download, ExternalLink, CalendarDays } from "lucide-react"; import { downloadICS, openInCalendar, getCalendarOptions } from "@/lib/icsExport"; +import { createEventIdentifier } from "@/lib/nip19Utils"; +import { AddToGroupCalendarDialog } from "./AddToGroupCalendarDialog"; +import { useCurrentUser } from "@/hooks/useCurrentUser"; import type { DateBasedEvent, TimeBasedEvent, LiveEvent, RoomMeeting } from "@/lib/eventTypes"; interface CalendarOptionsProps { @@ -17,6 +20,8 @@ interface CalendarOptionsProps { export function CalendarOptions({ event, className }: CalendarOptionsProps) { const [isOpen, setIsOpen] = useState(false); + const [groupCalendarDialogOpen, setGroupCalendarDialogOpen] = useState(false); + const { user } = useCurrentUser(); const handleQuickAdd = () => { try { @@ -44,70 +49,97 @@ export function CalendarOptions({ event, className }: CalendarOptionsProps) { } }; + const isReplaceable = event.kind >= 30000 && event.kind < 40000; + const dTag = event.tags.find(t => t[0] === 'd')?.[1]; + const eventCoordinate = isReplaceable && dTag ? `${event.kind}:${event.pubkey}:${dTag}` : event.id; + return ( - - - - - - - - Quick Add (Auto-detect) - - -

- - handleCalendarProvider('google')} className="cursor-pointer"> -
-
- G + <> + + + + + + + + Quick Add (Auto-detect) + + + {user && ( + { + setIsOpen(false); + setGroupCalendarDialogOpen(true); + }} + className="cursor-pointer font-semibold text-primary" + > + + Group Calendar + + )} + +
+ + handleCalendarProvider('google')} className="cursor-pointer"> +
+
+ G +
+ Google Calendar
- Google Calendar -
- - - handleCalendarProvider('outlook')} className="cursor-pointer"> -
-
- O + + + handleCalendarProvider('outlook')} className="cursor-pointer"> +
+
+ O +
+ Outlook Calendar
- Outlook Calendar -
- - - handleCalendarProvider('yahoo')} className="cursor-pointer"> -
-
- Y + + + handleCalendarProvider('yahoo')} className="cursor-pointer"> +
+
+ Y +
+ Yahoo Calendar
- Yahoo Calendar -
- - - handleCalendarProvider('apple')} className="cursor-pointer"> -
-
- 🍎 + + + handleCalendarProvider('apple')} className="cursor-pointer"> +
+
+ 🍎 +
+ Apple Calendar
- Apple Calendar -
- - -
- - - - Download .ics file - - - + + +
+ + + + Download .ics file + + + + + {user && ( + + )} + ); } From 7a07f039c27b890f897a22d66220ceab3cd2a315 Mon Sep 17 00:00:00 2001 From: Mnpezz <93685835+Mnpezz@users.noreply.github.com> Date: Mon, 23 Feb 2026 09:49:32 -0600 Subject: [PATCH 11/42] group cal --- src/components/AddToGroupCalendarDialog.tsx | 156 ++++++++++++++++++++ 1 file changed, 156 insertions(+) create mode 100644 src/components/AddToGroupCalendarDialog.tsx diff --git a/src/components/AddToGroupCalendarDialog.tsx b/src/components/AddToGroupCalendarDialog.tsx new file mode 100644 index 0000000..7dd72c7 --- /dev/null +++ b/src/components/AddToGroupCalendarDialog.tsx @@ -0,0 +1,156 @@ +import { useState } from "react"; +import { useQueryClient } from "@tanstack/react-query"; +import { useNostr } from "@/hooks/useNostr"; +import { useCurrentUser } from "@/hooks/useCurrentUser"; +import { useNostrPublish } from "@/hooks/useNostrPublish"; +import { useUserCalendars, addEventToCalendar, removeEventFromCalendar, createCoordinate } from "@/lib/calendarUtils"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; +import { toast } from "sonner"; +import { CalendarDays, Loader2, Plus, Check, Minus } from "lucide-react"; +import { Link } from "react-router-dom"; + +export interface AddToGroupCalendarDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + eventCoordinate: string; // The NIP-19 naddr, nevent, or raw id/coord to be added +} + +export function AddToGroupCalendarDialog({ open, onOpenChange, eventCoordinate }: AddToGroupCalendarDialogProps) { + const { nostr } = useNostr(); + const { user } = useCurrentUser(); + const { data: userCalendars = [], isLoading: isLoadingCalendars } = useUserCalendars(user?.pubkey); + const { mutate: createEvent } = useNostrPublish(); + const queryClient = useQueryClient(); + + const [processingId, setProcessingId] = useState(null); + const [localStatus, setLocalStatus] = useState>({}); + + const handleToggleCalendar = async (calendarCoordinate: string, currentlyAdded: boolean) => { + if (!nostr || !user) return; + + setProcessingId(calendarCoordinate); + + try { + if (currentlyAdded) { + await removeEventFromCalendar(nostr, createEvent, calendarCoordinate, eventCoordinate); + setLocalStatus(prev => ({ ...prev, [calendarCoordinate]: false })); + toast.info("Event removed from calendar."); + } else { + await addEventToCalendar(nostr, createEvent, calendarCoordinate, eventCoordinate); + setLocalStatus(prev => ({ ...prev, [calendarCoordinate]: true })); + toast.success("Event added to calendar!"); + } + + // Invalidate the specific calendar query so it refreshes its event list + queryClient.invalidateQueries({ queryKey: ['calendarEvents', calendarCoordinate] }); + queryClient.invalidateQueries({ queryKey: ['calendars'] }); // Update the calendar map + + } catch (error: any) { + if (error.message === "Event is already in this calendar") { + toast.info(error.message); + setLocalStatus(prev => ({ ...prev, [calendarCoordinate]: true })); + } else if (error.message === "Event is not in this calendar") { + toast.info(error.message); + setLocalStatus(prev => ({ ...prev, [calendarCoordinate]: false })); + } else { + console.error("Failed to modify calendar:", error); + toast.error(error.message || "Failed to modify calendar"); + } + } finally { + setProcessingId(null); + } + }; + + return ( + + + +
+ +
+ Add to Group Calendar + + Feature this event on one of your community calendars. + +
+ + {isLoadingCalendars ? ( +
+ +

Loading your calendars...

+
+ ) : userCalendars.length === 0 ? ( +
+ +

No Calendars Found

+

You haven't created any group calendars yet.

+ +
+ ) : ( +
+ {userCalendars.map((cal) => { + const calendarCoordinate = createCoordinate(31924, cal.pubkey, cal.d); + const isProcessing = processingId === calendarCoordinate; + // Check local overrides first, otherwise fall back to fetched calendar states. + const isAdded = localStatus[calendarCoordinate] ?? cal.events.includes(eventCoordinate); + + return ( +
+
+ {cal.title} +
+

{cal.title}

+
+
+ + +
+ ); + })} +
+ )} +
+
+ ); +} From c818a4912df74c36ffec21b452f6dcd8f13e4b8c Mon Sep 17 00:00:00 2001 From: Mnpezz <93685835+Mnpezz@users.noreply.github.com> Date: Mon, 23 Feb 2026 09:50:22 -0600 Subject: [PATCH 12/42] group cal --- src/pages/CreateEvent.tsx | 68 ++++++++++++++++++++++++++++----------- 1 file changed, 50 insertions(+), 18 deletions(-) diff --git a/src/pages/CreateEvent.tsx b/src/pages/CreateEvent.tsx index 6b630de..3fcda5a 100644 --- a/src/pages/CreateEvent.tsx +++ b/src/pages/CreateEvent.tsx @@ -30,8 +30,8 @@ import { } from "@/lib/eventTimezone"; import { encodeGeohash } from "@/lib/geolocation"; import { generateRecurringEventDates } from "@/lib/recurringEventUtils"; -import { PartyPopper, Target, FileText, Calendar as CalendarIcon, Flag, Clock, Globe, Rocket } from "lucide-react"; - +import { useUserCalendars, createCoordinate } from "@/lib/calendarUtils"; +import { PartyPopper, Target, FileText, Calendar as CalendarIcon, Flag, Clock, Globe, Rocket, CalendarDays } from "lucide-react"; export function CreateEvent() { const navigate = useNavigate(); const { user } = useCurrentUser(); @@ -53,6 +53,7 @@ export function CreateEvent() { endDate: "", endTime: "", imageUrl: "", + selectedCalendarCoordinate: "", categories: [] as EventCategory[], ticketInfo: { enabled: false, @@ -77,7 +78,7 @@ export function CreateEvent() { } as EventbriteRecurringConfig, }); - + const { data: userCalendars = [], isLoading: isLoadingCalendars } = useUserCalendars(user?.pubkey); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); @@ -151,8 +152,8 @@ export function CreateEvent() { // Use Eventbrite-style config const recurringConfig = { enabled: true, - pattern: (formData.eventbriteRecurringConfig.repeatUnit === 'day' ? 'daily' : - formData.eventbriteRecurringConfig.repeatUnit === 'week' ? 'weekly' : 'monthly') as 'daily' | 'weekly' | 'monthly', + pattern: (formData.eventbriteRecurringConfig.repeatUnit === 'day' ? 'daily' : + formData.eventbriteRecurringConfig.repeatUnit === 'week' ? 'weekly' : 'monthly') as 'daily' | 'weekly' | 'monthly', interval: formData.eventbriteRecurringConfig.repeatEvery, maxOccurrences: formData.eventbriteRecurringConfig.maxOccurrences || 6, weeklyDays: formData.eventbriteRecurringConfig.repeatOnDays, @@ -162,7 +163,7 @@ export function CreateEvent() { } : undefined, timeMode: formData.eventbriteRecurringConfig.timeMode, }; - + eventDates = generateRecurringEventDates( formData.startDate, formData.endDate, @@ -179,7 +180,7 @@ export function CreateEvent() { maxOccurrences: 1, timeMode: 'single' as const, }; - + eventDates = generateRecurringEventDates( formData.startDate, formData.endDate, @@ -236,7 +237,7 @@ export function CreateEvent() { // Create a unique identifier for the event const uniqueId = formData.title.toLowerCase().replace(/\s+/g, "-") + "-" + Date.now() + "-" + index; - + const tags = [ ["d", uniqueId], // Unique identifier ["title", formData.title], @@ -253,11 +254,11 @@ export function CreateEvent() { 9 // 9 characters gives ~4.8m precision ); tags.push(["g", geohash]); - + // Also store raw coordinates for backwards compatibility tags.push(["lat", formData.locationDetails.lat.toString()]); tags.push(["lon", formData.locationDetails.lng.toString()]); - + if (formData.locationDetails.placeId) { tags.push(["place_id", formData.locationDetails.placeId]); } @@ -286,6 +287,11 @@ export function CreateEvent() { tags.push(["image", formData.imageUrl]); } + // Associate with Calendar if selected + if (formData.selectedCalendarCoordinate) { + tags.push(["a", formData.selectedCalendarCoordinate]); + } + // Add categories as 't' tags if provided if (formData.categories.length > 0) { for (const category of formData.categories) { @@ -318,7 +324,7 @@ export function CreateEvent() { // Wait for all events to be created const results = await Promise.allSettled(createEventPromises); - + const successful = results.filter(result => result.status === 'fulfilled').length; const failed = results.filter(result => result.status === 'rejected').length; @@ -328,17 +334,17 @@ export function CreateEvent() { } else { toast.success("Event created successfully! It should appear on the home page shortly."); } - + if (failed > 0) { toast.warning(`${failed} events failed to create. Please try again.`); } - + // Navigate back to home page where the user can see their new events navigate("/"); } else { toast.error("Failed to create events. Please try again."); } - + setIsSubmitting(false); } catch (error) { toast.error("Failed to create event"); @@ -440,6 +446,32 @@ export function CreateEvent() { } /> + {userCalendars.length > 0 && ( +
+ + +
+ )} +