diff --git a/src/components/EventCard.tsx b/src/components/EventCard.tsx
index 1e4c2e9..d592842 100644
--- a/src/components/EventCard.tsx
+++ b/src/components/EventCard.tsx
@@ -42,6 +42,8 @@ interface EventCardProps {
onRsvp?: () => void;
/** Compact mode for map popups — smaller text, no impression tracking */
compact?: boolean;
+ /** When true, action buttons are rendered externally (ListView desktop) — hides internal button column and enlarges flyer */
+ externalActions?: boolean;
}
function FriendsGoingModal({
@@ -144,6 +146,7 @@ export const EventCard = memo(function EventCard({
rsvpStatus,
onRsvp,
compact,
+ externalActions,
}: EventCardProps) {
const [showFriendsModal, setShowFriendsModal] = useState(false);
const [showCheckedInModal, setShowCheckedInModal] = useState(false);
@@ -231,7 +234,7 @@ export const EventCard = memo(function EventCard({
>
{/* Left column: action buttons + cover image */}
-
+
{onItineraryToggle && (
)}
- {event.link &&
}
+ {event.link &&
}
{/* Right: event details */}
@@ -446,3 +449,66 @@ export const EventCard = memo(function EventCard({
);
});
+
+/* ------------------------------------------------------------------ */
+/* Standalone action buttons — rendered outside the card in ListView */
+/* ------------------------------------------------------------------ */
+
+interface EventCardActionsProps {
+ event: ETHDenverEvent;
+ isInItinerary: boolean;
+ onItineraryToggle?: (eventId: string) => void;
+ rsvpStatus?: 'idle' | 'confirmed';
+ onRsvp?: () => void;
+}
+
+export function EventCardActions({
+ event,
+ isInItinerary,
+ onItineraryToggle,
+ rsvpStatus,
+ onRsvp,
+}: EventCardActionsProps) {
+ const [copied, setCopied] = useState(false);
+
+ const handleCopyLink = (e: React.MouseEvent) => {
+ e.stopPropagation();
+ if (!event.link) return;
+ navigator.clipboard.writeText(event.link).then(() => {
+ trackCopyEventLink(event.name);
+ setCopied(true);
+ setTimeout(() => setCopied(false), 2000);
+ });
+ };
+
+ return (
+
+ {onItineraryToggle && (
+
+ )}
+ {onRsvp && event.link && (
+
+
+
+ )}
+ {event.link && (
+
+ )}
+
+ );
+}
diff --git a/src/components/ListView.tsx b/src/components/ListView.tsx
index 8bcbcac..8026add 100644
--- a/src/components/ListView.tsx
+++ b/src/components/ListView.tsx
@@ -5,7 +5,7 @@ import { useVirtualizer } from '@tanstack/react-virtual';
import type { ETHDenverEvent, ReactionEmoji, NativeAd, FriendInfo } from '@/lib/types';
import { formatDateLabel } from '@/lib/utils';
import { sortByStartTime } from '@/lib/time-parse';
-import { EventCard } from './EventCard';
+import { EventCard, EventCardActions } from './EventCard';
import { FeaturedSection } from './FeaturedSection';
import NativeAdCard from './NativeAdCard';
import { imageCache, FlyerLightbox } from './OGImage';
@@ -446,26 +446,40 @@ export const ListView = memo(function ListView({
) : (
-
setLightboxEventIndex(virtualRow.index)}
- rsvpStatus={getRsvpStatus?.(item.event.id)}
- onRsvp={item.event.link ? () => onRsvp?.(item.event.id, item.event.link!, item.event.name) : undefined}
- />
+
+
+ onRsvp?.(item.event.id, item.event.link!, item.event.name) : undefined}
+ />
+
+
+ setLightboxEventIndex(virtualRow.index)}
+ rsvpStatus={getRsvpStatus?.(item.event.id)}
+ onRsvp={item.event.link ? () => onRsvp?.(item.event.id, item.event.link!, item.event.name) : undefined}
+ externalActions
+ />
+
+
)}
diff --git a/src/components/OGImage.tsx b/src/components/OGImage.tsx
index 1512f54..ebbc6f5 100644
--- a/src/components/OGImage.tsx
+++ b/src/components/OGImage.tsx
@@ -16,11 +16,13 @@ interface OGImageProps {
isInItinerary?: boolean;
onItineraryToggle?: (eventId: string) => void;
friendsGoing?: FriendInfo[];
+ /** Override the default width classes (e.g. for enlarged flyer when buttons are external) */
+ className?: string;
}
export const imageCache = new Map();
-export function OGImage({ url, eventId, rsvpUrl, onOpenLightbox, isInItinerary, onItineraryToggle, friendsGoing }: OGImageProps) {
+export function OGImage({ url, eventId, rsvpUrl, onOpenLightbox, isInItinerary, onItineraryToggle, friendsGoing, className }: OGImageProps) {
const [imageUrl, setImageUrl] = useState(
imageCache.get(url) ?? null
);
@@ -86,7 +88,7 @@ export function OGImage({ url, eventId, rsvpUrl, onOpenLightbox, isInItinerary,
<>
{
e.stopPropagation();
if (!imageUrl) return;