Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion src/components/EmojiReactions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export function EmojiReactions({
}}
className="text-[var(--theme-text-muted)] hover:text-[var(--theme-text-primary)] transition-colors cursor-pointer inline-flex items-center"
>
<ThumbsUp className="w-5 h-5 translate-y-[3px]" />
<ThumbsUp className="w-4 h-4" />
</button>

{showPicker && (
Expand Down
151 changes: 103 additions & 48 deletions src/components/EventCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ interface EventCardProps {
onRsvp?: () => void;
/** Compact mode for map popups — smaller text, no impression tracking */
compact?: boolean;
/** When true, RSVP + link buttons render above the flyer inside the card (star button is external) */
buttonsAboveFlyer?: boolean;
}

function FriendsGoingModal({
Expand Down Expand Up @@ -144,6 +146,7 @@ export const EventCard = memo(function EventCard({
rsvpStatus,
onRsvp,
compact,
buttonsAboveFlyer,
}: EventCardProps) {
const [showFriendsModal, setShowFriendsModal] = useState(false);
const [showCheckedInModal, setShowCheckedInModal] = useState(false);
Expand Down Expand Up @@ -222,45 +225,52 @@ export const EventCard = memo(function EventCard({
: `${event.startTime}${event.endTime ? ` - ${event.endTime}` : ''}`;

return (
<div ref={cardRef} className={`rounded-lg ${compact ? 'p-3' : 'p-4'} transition-colors group flex gap-${compact ? '3' : '4'} overflow-hidden ${
<div ref={cardRef} className={`rounded-lg p-3 transition-colors group flex gap-2.5 overflow-hidden ${
event.isFeatured
? 'bg-[var(--theme-bg-card)] border-2 hover:bg-[var(--theme-bg-card-hover)]'
: `bg-[var(--theme-bg-card)] border border-[var(--theme-border-primary)] hover:bg-[var(--theme-bg-card-hover)] hover:border-[var(--theme-border-primary)] active:bg-[var(--theme-bg-card-hover)]${hasFriends ? ' border-l-[3px]' : ''}`
}`}
style={event.isFeatured ? { borderColor: 'var(--theme-popup-featured-border)' } : hasFriends ? { borderLeftColor: 'var(--friend-blue)' } : undefined}
>
{/* Left column: action buttons + cover image */}
<div className="flex items-center shrink-0 gap-1">
<div className="flex flex-col items-center gap-1">
{onItineraryToggle && (
<StarButton
eventId={event.id}
isStarred={isInItinerary}
onToggle={onItineraryToggle}
/>
)}
{onRsvp && event.link && (
<div className="mt-px">
<RsvpButton eventLink={event.link} status={rsvpStatus ?? 'idle'} onClick={onRsvp} />
</div>
)}
{event.link && (
<button
onClick={handleCopyLink}
className="p-1 text-[var(--theme-text-muted)] hover:text-[var(--theme-text-secondary)] transition-colors cursor-pointer"
aria-label="Copy event link"
title="Copy link"
>
{copied ? (
<Check className="w-4 h-4 text-green-400" />
) : (
<Link className="w-4 h-4" />
)}
</button>
)}
{buttonsAboveFlyer ? (
<div className="shrink-0">
{/* Flyer image only */}
{event.link && <OGImage url={event.link} eventId={event.id} rsvpUrl={event.link} onOpenLightbox={onOpenLightbox} className="w-[106px] sm:w-[140px]" />}
</div>
{event.link && <OGImage url={event.link} eventId={event.id} rsvpUrl={event.link} onOpenLightbox={onOpenLightbox} />}
</div>
) : (
<div className="flex items-center shrink-0 gap-1">
<div className="flex flex-col items-center gap-1">
{onItineraryToggle && (
<StarButton
eventId={event.id}
isStarred={isInItinerary}
onToggle={onItineraryToggle}
/>
)}
{onRsvp && event.link && (
<div className="mt-px">
<RsvpButton eventLink={event.link} status={rsvpStatus ?? 'idle'} onClick={onRsvp} />
</div>
)}
{event.link && (
<button
onClick={handleCopyLink}
className="p-1 text-[var(--theme-text-muted)] hover:text-[var(--theme-text-secondary)] transition-colors cursor-pointer"
aria-label="Copy event link"
title="Copy link"
>
{copied ? (
<Check className="w-4 h-4 text-green-400" />
) : (
<Link className="w-4 h-4" />
)}
</button>
)}
</div>
{event.link && <OGImage url={event.link} eventId={event.id} rsvpUrl={event.link} onOpenLightbox={onOpenLightbox} />}
</div>
)}

{/* Right: event details */}
<div className="flex-1 min-w-0">
Expand Down Expand Up @@ -323,8 +333,12 @@ export const EventCard = memo(function EventCard({
'bg-green-400'
}`} title={liveUrgency === 'red' ? 'Ending soon' : liveUrgency === 'yellow' ? 'Less than 1hr left' : 'Live now'} />
)}
<Calendar className="w-3.5 h-3.5 shrink-0" />
<span>{event.date} · {timeDisplay}</span>
<Calendar className="w-3.5 h-3.5 shrink-0 hidden sm:inline" />
<span className="hidden sm:inline">{event.date} · {timeDisplay}</span>
<span className="sm:hidden inline-flex items-baseline gap-1 min-w-0">
<span className="shrink-0">{event.isAllDay ? 'All Day' : `${event.startTime}${event.endTime ? `-${event.endTime}` : ''}`}</span>
{event.address && <span className="text-[var(--theme-text-muted)] truncate min-w-0">· {shortenAddress(event.address)}</span>}
</span>
</p>
{(checkInCount ?? 0) > 0 && (
<span className="absolute -top-1 -right-3 min-w-[16px] h-[16px] flex items-center justify-center rounded-full bg-green-500 text-white text-[9px] font-bold px-0.5 pointer-events-none">
Expand All @@ -351,11 +365,11 @@ export const EventCard = memo(function EventCard({
)}
</div>

{/* Address */}
{/* Address — hidden on mobile (shown inline with time) */}
{event.address && (
<AddressLink address={event.address} navAddress={event.matchedAddress} lat={event.lat} lng={event.lng}
eventId={event.id} eventName={event.name}
className="w-full text-[var(--theme-text-muted)] hover:text-[var(--theme-text-secondary)] text-sm mt-1 flex items-start gap-1 overflow-hidden transition-colors min-w-0">
className="hidden sm:flex w-full text-[var(--theme-text-muted)] hover:text-[var(--theme-text-secondary)] text-sm mt-1 items-start gap-1 overflow-hidden transition-colors min-w-0">
<MapPin className="w-3.5 h-3.5 mt-0.5 shrink-0" />
<span className="truncate">{shortenAddress(event.address)}</span>
{userLocation && event.lat && event.lng && (
Expand All @@ -370,7 +384,7 @@ export const EventCard = memo(function EventCard({
)}

{/* Tags */}
<div className="flex flex-wrap items-center gap-1.5 mt-3">
<div className="flex flex-wrap items-center gap-1.5 mt-2">
{event.tags.map((tag) => (
<TagBadge key={tag} tag={tag} iconOnly={compact} />
))}
Expand All @@ -381,8 +395,41 @@ export const EventCard = memo(function EventCard({
<p className="text-[var(--theme-text-faint)] text-xs mt-1 italic truncate">{event.note}</p>
)}

{/* Bottom row: friends + reactions + checked-in indicator */}
<div className="flex items-center gap-2 mt-2">
{/* Bottom strip: action buttons + reactions */}
<div className="flex items-center gap-1.5 mt-1.5">
{buttonsAboveFlyer && onItineraryToggle && (
<StarButton
eventId={event.id}
isStarred={isInItinerary}
onToggle={onItineraryToggle}
size="sm"
/>
)}
{buttonsAboveFlyer && onRsvp && event.link && (
<RsvpButton eventLink={event.link} status={rsvpStatus ?? 'idle'} onClick={onRsvp} />
)}
{buttonsAboveFlyer && event.link && (
<button
onClick={handleCopyLink}
className="p-1 text-[var(--theme-text-muted)] hover:text-[var(--theme-text-secondary)] transition-colors cursor-pointer"
aria-label="Copy event link"
title="Copy link"
>
{copied ? (
<Check className="w-4 h-4 text-green-400" />
) : (
<Link className="w-4 h-4" />
)}
</button>
)}
{onToggleReaction && (
<EmojiReactions
eventId={event.id}
reactions={reactions}
onToggle={onToggleReaction}
compact={compact}
/>
)}
{friendsGoing && friendsGoing.length > 0 && (
<button
onClick={(e) => {
Expand All @@ -396,16 +443,6 @@ export const EventCard = memo(function EventCard({
<FriendAvatarStack friends={friendsGoing} maxShow={2} size="sm" />
</button>
)}
{onToggleReaction && (
<EmojiReactions
eventId={event.id}
reactions={reactions}
onToggle={onToggleReaction}
compact={compact}
/>
)}
{/* Comments disabled until social verification is in place */}
{/* <CommentSection eventId={event.id} commentCount={commentCount} eventName={event.name} /> */}
{checkedInFriends && checkedInFriends.length > 0 && (
<button
onClick={(e) => {
Expand Down Expand Up @@ -446,3 +483,21 @@ export const EventCard = memo(function EventCard({
</div>
);
});

/* ------------------------------------------------------------------ */
/* EventCardActions — external star button for buttonsAboveFlyer mode */
/* ------------------------------------------------------------------ */

export function EventCardActions({ event, isInItinerary, onItineraryToggle }: {
event: ETHDenverEvent;
isInItinerary: boolean;
onItineraryToggle: (eventId: string) => void;
}) {
return (
<StarButton
eventId={event.id}
isStarred={isInItinerary}
onToggle={onItineraryToggle}
/>
);
}
2 changes: 2 additions & 0 deletions src/components/EventPopup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ export const EventPopup = memo(function EventPopup({
rsvpStatus={rsvpStatus}
onRsvp={onRsvp}
compact
buttonsAboveFlyer
/>
</div>
</Popup>
Expand Down Expand Up @@ -155,6 +156,7 @@ export function MultiEventPopup({
onToggleReaction={onToggleReaction}
commentCount={commentCounts?.get(event.id)}
compact
buttonsAboveFlyer
/>
</div>
))}
Expand Down
5 changes: 3 additions & 2 deletions src/components/ListView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -436,7 +436,7 @@ export const ListView = memo(function ListView({
</div>
</div>
) : item.kind === 'ad' ? (
<div className="pt-3">
<div className="pt-2">
<NativeAdCard
ad={item.ad}
conference={conference}
Expand All @@ -445,7 +445,7 @@ export const ListView = memo(function ListView({
/>
</div>
) : (
<div className="pt-3">
<div className="pt-2">
<EventCard
event={item.event}
isInItinerary={itinerary?.has(item.event.id)}
Expand All @@ -465,6 +465,7 @@ export const ListView = memo(function ListView({
onOpenLightbox={() => setLightboxEventIndex(virtualRow.index)}
rsvpStatus={getRsvpStatus?.(item.event.id)}
onRsvp={item.event.link ? () => onRsvp?.(item.event.id, item.event.link!, item.event.name) : undefined}
buttonsAboveFlyer
/>
</div>
)}
Expand Down
22 changes: 13 additions & 9 deletions src/components/OGImage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,13 @@ interface OGImageProps {
isInItinerary?: boolean;
onItineraryToggle?: (eventId: string) => void;
friendsGoing?: FriendInfo[];
/** Optional className to override the default width classes on the thumbnail container */
className?: string;
}

export const imageCache = new Map<string, string | null>();

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<string | null>(
imageCache.get(url) ?? null
);
Expand Down Expand Up @@ -86,7 +88,7 @@ export function OGImage({ url, eventId, rsvpUrl, onOpenLightbox, isInItinerary,
<>
<div
ref={ref}
className="shrink-0 w-[88px] sm:w-[106px] rounded-lg overflow-hidden bg-stone-800/30 self-center cursor-pointer"
className={`shrink-0 ${className ?? 'w-[88px] sm:w-[106px]'} rounded-lg overflow-hidden bg-stone-800/30 self-center cursor-pointer`}
onClick={(e) => {
e.stopPropagation();
if (!imageUrl) return;
Expand All @@ -104,7 +106,7 @@ export function OGImage({ url, eventId, rsvpUrl, onOpenLightbox, isInItinerary,
<img
src={imageUrl}
alt=""
className="w-full h-auto rounded-lg"
className="w-full h-auto max-h-[106px] sm:max-h-[140px] object-cover rounded-lg"
loading="lazy"
onError={() => setError(true)}
/>
Expand All @@ -131,7 +133,7 @@ export function OGImage({ url, eventId, rsvpUrl, onOpenLightbox, isInItinerary,
<img
src={imageUrl}
alt=""
className="max-w-[60vw] max-h-[60vh] object-contain rounded-lg"
className="h-[60vh] max-w-[90vw] sm:max-w-[60vw] object-contain rounded-lg"
/>
<div className="flex items-center gap-3">
{eventId && onItineraryToggle && (
Expand Down Expand Up @@ -253,11 +255,13 @@ export function FlyerLightbox({ imageUrl, rsvpUrl, onClose, onPrev, onNext, even
/>
<div className="flex items-center gap-3">
{eventId && onItineraryToggle && (
<StarButton
eventId={eventId}
isStarred={isInItinerary ?? false}
onToggle={onItineraryToggle}
/>
<div style={{ '--theme-text-secondary': '#ffffff', '--theme-border-primary': 'rgba(255,255,255,0.6)', '--theme-accent': '#ffffff', '--theme-accent-muted': 'rgba(255,255,255,0.2)' } as React.CSSProperties}>
<StarButton
eventId={eventId}
isStarred={isInItinerary ?? false}
onToggle={onItineraryToggle}
/>
</div>
)}
{rsvpUrl && (
<a
Expand Down
1 change: 1 addition & 0 deletions src/components/TableView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -611,6 +611,7 @@ function EventDetailModal({
rsvpStatus={rsvpStatus}
onRsvp={onRsvp}
onOpenLightbox={onOpenLightbox ? () => onOpenLightbox() : undefined}
buttonsAboveFlyer
/>
</div>
</div>
Expand Down