Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
0f8b1d1
event host stats
Mnpezz Feb 23, 2026
eeb3658
Update location search
Mnpezz Feb 23, 2026
103645f
Update map pins
Mnpezz Feb 23, 2026
e3323f8
links and map
Mnpezz Feb 23, 2026
1826cff
links and map
Mnpezz Feb 23, 2026
c64aca5
stats card
Mnpezz Feb 23, 2026
579f327
stats card
Mnpezz Feb 23, 2026
33fe1b7
Update stats card
Mnpezz Feb 23, 2026
5710136
stats card -
Mnpezz Feb 23, 2026
f64b75c
group cal
Mnpezz Feb 23, 2026
7a07f03
group cal
Mnpezz Feb 23, 2026
c818a49
group cal
Mnpezz Feb 23, 2026
6683efd
group cal
Mnpezz Feb 23, 2026
304e7f5
group cal
Mnpezz Feb 23, 2026
fe94f2e
group cal
Mnpezz Feb 23, 2026
34c1b35
group cal
Mnpezz Feb 23, 2026
cd31608
group cal update
Mnpezz Feb 23, 2026
6b7f14d
group cal update
Mnpezz Feb 23, 2026
8dc014a
group cal update
Mnpezz Feb 23, 2026
ca326dc
shared cal
Mnpezz Feb 23, 2026
2352ad7
shared cal
Mnpezz Feb 23, 2026
1164c26
group cal
Mnpezz Feb 23, 2026
fff1d42
cal inbox deny
Mnpezz Feb 23, 2026
3fb1836
cal inbox deny
Mnpezz Feb 23, 2026
df3f20b
cal inbox deny
Mnpezz Feb 23, 2026
8d6b0d7
cal inbox deny
Mnpezz Feb 23, 2026
712d7ba
auto calendars
Mnpezz Feb 23, 2026
602fdb5
auto calendars
Mnpezz Feb 23, 2026
23191e7
auto calendars
Mnpezz Feb 23, 2026
ac53d4d
edit cal
Mnpezz Feb 23, 2026
fbf566f
edit cal
Mnpezz Feb 23, 2026
3d80f0b
edit cal
Mnpezz Feb 23, 2026
6360b1c
edit cal
Mnpezz Feb 23, 2026
3feedd3
edit cal
Mnpezz Feb 23, 2026
e8bc462
edit cal fix
Mnpezz Feb 23, 2026
65d3e9b
edit cal fix
Mnpezz Feb 23, 2026
0327aad
edit cal fix
Mnpezz Feb 23, 2026
b9a5ce9
multiple locations for auto cal
Mnpezz Feb 23, 2026
98ed2b4
multiple locations for auto cal
Mnpezz Feb 23, 2026
c2b1567
Update MonthlyCalendarView.tsx
Mnpezz Feb 24, 2026
55be9e8
Add community calendars feed, creation flow, and responsiveness fixes
Mnpezz May 22, 2026
35fd57e
Merge upstream branch
Mnpezz May 22, 2026
9a41fd6
Smart Filter Update
Mnpezz May 22, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/AppRouter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ import { Skeleton } from "@/components/ui/skeleton";
// Lazy-loaded pages: only downloaded when the user navigates to them
const EventDetail = lazy(() => import("@/pages/EventDetail").then(m => ({ default: m.EventDetail })));
const CreateEvent = lazy(() => import("@/pages/CreateEvent").then(m => ({ default: m.CreateEvent })));
const CreateCalendar = lazy(() => import("@/pages/CreateCalendar").then(m => ({ default: m.CreateCalendar })));
const EditCalendar = lazy(() => import("@/pages/EditCalendar").then(m => ({ default: m.EditCalendar })));
const CalendarView = lazy(() => import("@/pages/CalendarView").then(m => ({ default: m.CalendarView })));
const CalendarsFeed = lazy(() => import("@/pages/CalendarsFeed").then(m => ({ default: m.CalendarsFeed })));
const Profile = lazy(() => import("@/pages/Profile").then(m => ({ default: m.Profile })));
const MyTickets = lazy(() => import("@/pages/MyTickets").then(m => ({ default: m.MyTickets })));
const SocialFeed = lazy(() => import("@/pages/SocialFeed").then(m => ({ default: m.SocialFeed })));
Expand All @@ -32,6 +36,10 @@ export default function AppRouter() {
<Route path="/" element={<Home />} />
<Route path="/event/:eventId" element={<EventDetail />} />
<Route path="/create" element={<CreateEvent />} />
<Route path="/create-calendar" element={<CreateCalendar />} />
<Route path="/edit-calendar/:naddr" element={<EditCalendar />} />
<Route path="/calendar/:naddr" element={<CalendarView />} />
<Route path="/calendars" element={<CalendarsFeed />} />
<Route path="/profile/:npub" element={<Profile />} />
<Route path="/tickets" element={<MyTickets />} />
<Route path="/feed" element={<SocialFeed />} />
Expand Down
156 changes: 156 additions & 0 deletions src/components/AddToGroupCalendarDialog.tsx
Original file line number Diff line number Diff line change
@@ -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<string | null>(null);
const [localStatus, setLocalStatus] = useState<Record<string, boolean>>({});

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 (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-md rounded-2xl p-6 border-2 shadow-2xl overflow-hidden bg-background">
<DialogHeader className="mb-6 space-y-3 pb-4 border-b">
<div className="mx-auto bg-primary/10 p-4 rounded-full w-16 h-16 flex items-center justify-center mb-2">
<CalendarDays className="h-8 w-8 text-primary" />
</div>
<DialogTitle className="text-2xl font-bold text-center">Add to Group Calendar</DialogTitle>
<DialogDescription className="text-center text-md">
Feature this event on one of your community calendars.
</DialogDescription>
</DialogHeader>

{isLoadingCalendars ? (
<div className="flex flex-col items-center justify-center py-8 space-y-4">
<Loader2 className="h-8 w-8 animate-spin text-primary" />
<p className="text-muted-foreground">Loading your calendars...</p>
</div>
) : userCalendars.length === 0 ? (
<div className="text-center py-8 px-4 rounded-xl border-2 border-dashed bg-muted/20">
<CalendarDays className="h-12 w-12 text-muted-foreground mx-auto mb-3 opacity-50" />
<h3 className="text-lg font-medium text-foreground mb-1">No Calendars Found</h3>
<p className="text-muted-foreground mb-4">You haven't created any group calendars yet.</p>
<Button asChild className="w-full">
<Link to="/create-calendar" onClick={() => onOpenChange(false)}>
<Plus className="h-4 w-4 mr-2" />
Create a Calendar
</Link>
</Button>
</div>
) : (
<div className="space-y-3 max-h-[60vh] overflow-y-auto pr-2 custom-scrollbar">
{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 (
<div
key={cal.id}
className="flex items-center justify-between p-3 rounded-xl border-2 border-border/50 bg-card hover:border-primary/50 transition-colors"
>
<div className="flex items-center gap-3 overflow-hidden">
<img
src={cal.image || "/default-calendar.png"}
alt={cal.title}
className="h-10 w-10 rounded-lg object-cover bg-muted shrink-0"
/>
<div className="flex-1 min-w-0">
<p className="font-semibold text-sm truncate">{cal.title}</p>
</div>
</div>

<Button
size="sm"
variant={isAdded ? "outline" : "outline"}
className={`ml-3 shrink-0 rounded-full transition-all w-[90px] ${isAdded
? 'bg-green-100/50 text-green-700 hover:bg-destructive hover:text-white border-transparent group'
: ''
}`}
disabled={isProcessing}
onClick={() => handleToggleCalendar(calendarCoordinate, isAdded)}
>
{isProcessing ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : isAdded ? (
<span className="flex items-center">
<Check className="h-3.5 w-3.5 mr-1 group-hover:hidden" />
<Minus className="h-3.5 w-3.5 mr-1 hidden group-hover:inline" />
<span className="group-hover:hidden">Added</span>
<span className="hidden group-hover:inline">Remove</span>
</span>
) : (
<><Plus className="h-3.5 w-3.5 mr-1" /> Add</>
)}
</Button>
</div>
);
})}
</div>
)}
</DialogContent>
</Dialog>
);
}
8 changes: 7 additions & 1 deletion src/components/AppNavigation.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Link, useLocation } from "react-router-dom";
import { Search, Plus, Ticket, User, Heart, QrCode } from "lucide-react";
import { Search, Plus, Ticket, User, Heart, QrCode, CalendarDays } from "lucide-react";
import { useIsMobile } from "@/hooks/useIsMobile";
import { useCurrentUser } from "@/hooks/useCurrentUser";
import { LoginArea } from "@/components/auth/LoginArea";
Expand Down Expand Up @@ -57,6 +57,12 @@ export function AppNavigation({ children }: AppNavigationProps) {
isActive: location.pathname === "/",
onClick: handleDiscoverClick,
},
{
href: "/calendars",
label: "Calendars",
icon: CalendarDays,
isActive: location.pathname === "/calendars",
},
{
href: "/feed",
label: "Feed",
Expand Down
154 changes: 93 additions & 61 deletions src/components/CalendarOptions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 { openUrl } from "@/lib/utils";
import type { DateBasedEvent, TimeBasedEvent, LiveEvent, RoomMeeting } from "@/lib/eventTypes";

Expand All @@ -18,6 +21,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 {
Expand Down Expand Up @@ -45,70 +50,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 (
<DropdownMenu open={isOpen} onOpenChange={setIsOpen}>
<DropdownMenuTrigger asChild>
<Button
variant="outline"
size="sm"
className={`flex items-center gap-1 sm:gap-2 px-2 sm:px-3 ${className}`}
>
<Calendar className="h-4 w-4" />
<span className="hidden sm:inline">Add to Calendar</span>
<span className="sm:hidden">Calendar</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-56">
<DropdownMenuItem onClick={handleQuickAdd} className="cursor-pointer">
<ExternalLink className="h-4 w-4 mr-2" />
Quick Add (Auto-detect)
</DropdownMenuItem>

<div className="border-t my-1" />

<DropdownMenuItem onClick={() => handleCalendarProvider('google')} className="cursor-pointer">
<div className="flex items-center">
<div className="w-4 h-4 mr-2 bg-blue-500 rounded-sm flex items-center justify-center">
<span className="text-white text-xs font-bold">G</span>
<>
<DropdownMenu open={isOpen} onOpenChange={setIsOpen}>
<DropdownMenuTrigger asChild>
<Button
variant="outline"
size="sm"
className={`flex items-center gap-1 sm:gap-2 px-2 sm:px-3 ${className}`}
>
<Calendar className="h-4 w-4" />
<span className="hidden sm:inline">Add to Calendar</span>
<span className="sm:hidden">Calendar</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-56">
<DropdownMenuItem onClick={handleQuickAdd} className="cursor-pointer">
<ExternalLink className="h-4 w-4 mr-2" />
Quick Add (Auto-detect)
</DropdownMenuItem>

{user && (
<DropdownMenuItem
onClick={() => {
setIsOpen(false);
setGroupCalendarDialogOpen(true);
}}
className="cursor-pointer font-semibold text-primary"
>
<CalendarDays className="h-4 w-4 mr-2" />
Group Calendar
</DropdownMenuItem>
)}

<div className="border-t my-1" />

<DropdownMenuItem onClick={() => handleCalendarProvider('google')} className="cursor-pointer">
<div className="flex items-center">
<div className="w-4 h-4 mr-2 bg-blue-500 rounded-sm flex items-center justify-center">
<span className="text-white text-xs font-bold">G</span>
</div>
Google Calendar
</div>
Google Calendar
</div>
</DropdownMenuItem>

<DropdownMenuItem onClick={() => handleCalendarProvider('outlook')} className="cursor-pointer">
<div className="flex items-center">
<div className="w-4 h-4 mr-2 bg-blue-600 rounded-sm flex items-center justify-center">
<span className="text-white text-xs font-bold">O</span>
</DropdownMenuItem>

<DropdownMenuItem onClick={() => handleCalendarProvider('outlook')} className="cursor-pointer">
<div className="flex items-center">
<div className="w-4 h-4 mr-2 bg-blue-600 rounded-sm flex items-center justify-center">
<span className="text-white text-xs font-bold">O</span>
</div>
Outlook Calendar
</div>
Outlook Calendar
</div>
</DropdownMenuItem>

<DropdownMenuItem onClick={() => handleCalendarProvider('yahoo')} className="cursor-pointer">
<div className="flex items-center">
<div className="w-4 h-4 mr-2 bg-purple-600 rounded-sm flex items-center justify-center">
<span className="text-white text-xs font-bold">Y</span>
</DropdownMenuItem>

<DropdownMenuItem onClick={() => handleCalendarProvider('yahoo')} className="cursor-pointer">
<div className="flex items-center">
<div className="w-4 h-4 mr-2 bg-purple-600 rounded-sm flex items-center justify-center">
<span className="text-white text-xs font-bold">Y</span>
</div>
Yahoo Calendar
</div>
Yahoo Calendar
</div>
</DropdownMenuItem>

<DropdownMenuItem onClick={() => handleCalendarProvider('apple')} className="cursor-pointer">
<div className="flex items-center">
<div className="w-4 h-4 mr-2 bg-gray-800 rounded-sm flex items-center justify-center">
<span className="text-white text-xs font-bold">🍎</span>
</DropdownMenuItem>

<DropdownMenuItem onClick={() => handleCalendarProvider('apple')} className="cursor-pointer">
<div className="flex items-center">
<div className="w-4 h-4 mr-2 bg-gray-800 rounded-sm flex items-center justify-center">
<span className="text-white text-xs font-bold">🍎</span>
</div>
Apple Calendar
</div>
Apple Calendar
</div>
</DropdownMenuItem>

<div className="border-t my-1" />

<DropdownMenuItem onClick={handleDownload} className="cursor-pointer">
<Download className="h-4 w-4 mr-2" />
Download .ics file
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</DropdownMenuItem>

<div className="border-t my-1" />

<DropdownMenuItem onClick={handleDownload} className="cursor-pointer">
<Download className="h-4 w-4 mr-2" />
Download .ics file
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>

{user && (
<AddToGroupCalendarDialog
open={groupCalendarDialogOpen}
onOpenChange={setGroupCalendarDialogOpen}
eventCoordinate={eventCoordinate}
/>
)}
</>
);
}
Loading