Skip to content
Merged
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
9 changes: 2 additions & 7 deletions src/components/EventApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import type { ABTest, ETHDenverEvent } from '@/lib/types';
import { resolveItemVariants, getVisitorId } from '@/lib/ab-testing';
import { Header } from './Header';
import { FilterBar } from './FilterBar';
import { ViewToolbar } from './ViewToolbar';
import { ListView } from './ListView';
import { GalleryView } from './GalleryView';
import { TableView } from './TableView';
Expand Down Expand Up @@ -488,8 +489,6 @@ export function EventApp({ initialConference, initialEvents }: { initialConferen
return (
<div className="min-h-screen bg-[var(--theme-bg-primary)]">
<Header
viewMode={viewMode}
onViewChange={setViewMode}
events={events}
itinerary={itinerary}
onOpenFriends={handleOpenFriends}
Expand All @@ -504,8 +503,6 @@ export function EventApp({ initialConference, initialEvents }: { initialConferen
return (
<div className="min-h-screen bg-[var(--theme-bg-primary)]">
<Header
viewMode={viewMode}
onViewChange={setViewMode}
events={events}
itinerary={itinerary}
onOpenFriends={handleOpenFriends}
Expand All @@ -528,8 +525,6 @@ export function EventApp({ initialConference, initialEvents }: { initialConferen
return (
<div className="h-dvh flex flex-col bg-[var(--theme-bg-primary)] overflow-hidden">
<Header
viewMode={viewMode}
onViewChange={setViewMode}
events={events}
itinerary={itinerary}
onOpenFriends={handleOpenFriends}
Expand Down Expand Up @@ -571,7 +566,6 @@ export function EventApp({ initialConference, initialEvents }: { initialConferen
onToggleFriend={toggleFriend}
searchQuery={filters.searchQuery}
onSearchChange={handleSearchChange}
eventCount={filteredEvents.length}
onSubmitEvent={handleOpenSubmitEvent}
onSignIn={handleOpenSignIn}
orgNames={orgNames}
Expand All @@ -586,6 +580,7 @@ export function EventApp({ initialConference, initialEvents }: { initialConferen
friendEventCount={friendEventCount}
orgEventCount={orgEventCount}
/>
<ViewToolbar viewMode={viewMode} onViewChange={setViewMode} eventCount={filteredEvents.length} />
</div>

{/* Main content area */}
Expand Down
6 changes: 2 additions & 4 deletions src/components/FilterBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ interface FilterBarProps {
onToggleFriend: (friendId: string) => void;
searchQuery: string;
onSearchChange: (query: string) => void;
eventCount: number;
onSubmitEvent?: () => void;
onSignIn?: () => void;
conferenceTabs?: TabConfig[];
Expand Down Expand Up @@ -68,7 +67,6 @@ export const FilterBar = memo(function FilterBar({
onToggleFriend,
searchQuery,
onSearchChange,
eventCount,
onSubmitEvent,
onSignIn,
orgNames,
Expand Down Expand Up @@ -202,7 +200,7 @@ export const FilterBar = memo(function FilterBar({

{/* Desktop: inline search bar between conference dropdown and Now */}
<div className="hidden md:flex items-center gap-2 flex-1">
<SearchBar value={searchQuery} onChange={onSearchChange} eventCount={eventCount} />
<SearchBar value={searchQuery} onChange={onSearchChange} />
{onSubmitEvent && (
<button
onClick={onSubmitEvent}
Expand Down Expand Up @@ -296,7 +294,7 @@ export const FilterBar = memo(function FilterBar({

{/* Search bar — mobile only (desktop is inline in the row above) */}
<div className="md:hidden flex items-center gap-2">
<SearchBar value={searchQuery} onChange={onSearchChange} eventCount={eventCount} />
<SearchBar value={searchQuery} onChange={onSearchChange} />
{onSubmitEvent && (
<button
onClick={onSubmitEvent}
Expand Down
9 changes: 1 addition & 8 deletions src/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,11 @@
import Image from 'next/image';
import { User, MapPin, Loader2 } from 'lucide-react';
import { trackAuthPrompt } from '@/lib/analytics';
import { ViewMode, ETHDenverEvent } from '@/lib/types';
import { ViewToggle } from './ViewToggle';
import { ETHDenverEvent } from '@/lib/types';
import { useAuth } from '@/contexts/AuthContext';
import { AuthModal, UserMenu } from './AuthModal';

interface HeaderProps {
viewMode: ViewMode;
onViewChange: (mode: ViewMode) => void;
events: ETHDenverEvent[];
itinerary: Set<string>;
onOpenFriends: () => void;
Expand All @@ -24,8 +21,6 @@
}

export const Header = memo(function Header({
viewMode,
onViewChange,
events,
itinerary,
onOpenFriends,
Expand All @@ -45,13 +40,11 @@
<div className="px-2 sm:px-4 py-3 flex items-center justify-between gap-4">
{/* Left: Branding */}
<div className="flex items-center min-w-0">
<a href="/" style={{ marginTop: '-4px' }}><Image src="/logo.png" alt="plan.wtf" width={130} height={36} style={{ filter: 'var(--theme-header-logo-filter)' }} priority /></a>

Check failure on line 43 in src/components/Header.tsx

View workflow job for this annotation

GitHub Actions / check

Do not use an `<a>` element to navigate to `/`. Use `<Link />` from `next/link` instead. See: https://nextjs.org/docs/messages/no-html-link-for-pages
</div>

{/* Right: Controls */}
<div className="flex items-center gap-3 shrink-0">
<ViewToggle viewMode={viewMode} onViewChange={onViewChange} />

{/* Proximity check-in indicator */}
{hasNearbyLiveEvents && user && (
<button
Expand Down
5 changes: 2 additions & 3 deletions src/components/SearchBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,9 @@ import { trackSearch } from '@/lib/analytics';
interface SearchBarProps {
value: string;
onChange: (query: string) => void;
eventCount?: number;
}

export function SearchBar({ value, onChange, eventCount }: SearchBarProps) {
export function SearchBar({ value, onChange }: SearchBarProps) {
const [localValue, setLocalValue] = useState(value);
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);

Expand Down Expand Up @@ -57,7 +56,7 @@ export function SearchBar({ value, onChange, eventCount }: SearchBarProps) {
type="text"
value={localValue}
onChange={(e) => handleChange(e.target.value)}
placeholder={eventCount != null ? `${eventCount} events` : 'Events'}
placeholder="Search events…"
className="w-full pl-10 pr-9 py-2 bg-[var(--theme-filter-control-bg)] border border-[var(--theme-filter-control-border)] text-[var(--theme-filter-active)] placeholder-[var(--theme-filter-text)] rounded-lg text-sm focus:outline-none focus:border-[var(--theme-accent)] transition-colors"
/>
{localValue && (
Expand Down
58 changes: 38 additions & 20 deletions src/components/ViewToggle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,28 +17,46 @@ const views: { mode: ViewMode; icon: typeof Map; label: string }[] = [
{ mode: 'gallery', icon: LayoutGrid, label: 'Gallery' },
];

/**
* Segmented control for switching how events are displayed.
* Rendered in the content toolbar (directly above the events), not the site header.
* On small screens only the active option shows its text label; the rest are
* icon-only with an accessible name and a tooltip.
*/
export function ViewToggle({ viewMode, onViewChange }: ViewToggleProps) {
return (
<div className="flex rounded-lg border border-[var(--theme-header-control-border)] overflow-hidden">
{views.map(({ mode, icon: Icon, label }) => (
<button
key={mode}
onClick={() => { trackViewChange(mode); onViewChange(mode); }}
className={clsx(
'flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium transition-colors cursor-pointer',
viewMode === mode
? 'text-[var(--theme-header-accent)]'
: 'text-[var(--theme-header-text)] hover:text-[var(--theme-header-text-hover)] active:text-[var(--theme-header-text-hover)]'
)}
style={viewMode === mode
? { backgroundColor: 'var(--theme-header-accent-muted)' }
: { backgroundColor: 'var(--theme-header-control-bg)' }}
aria-label={`${label} view`}
>
<Icon className="w-4 h-4" />
<span className="hidden sm:inline">{label}</span>
</button>
))}
<div
role="group"
aria-label="View events as"
className="flex rounded-lg border border-[var(--theme-border-primary)] overflow-hidden"
>
{views.map(({ mode, icon: Icon, label }) => {
const active = viewMode === mode;
return (
<button
key={mode}
type="button"
onClick={() => {
trackViewChange(mode);
onViewChange(mode);
}}
aria-pressed={active}
aria-label={`${label} view`}
title={`${label} view`}
className={clsx(
'flex items-center gap-1.5 h-8 px-2.5 sm:px-3 text-sm font-medium transition-colors cursor-pointer border-l first:border-l-0 border-[var(--theme-border-primary)]',
'focus-visible:outline-2 focus-visible:outline-offset-[-2px] focus-visible:outline-[var(--theme-accent)]',
active
? 'text-[var(--theme-accent)]'
: 'text-[var(--theme-text-secondary)] hover:text-[var(--theme-text-primary)] active:text-[var(--theme-text-primary)]'
)}
style={{ backgroundColor: active ? 'var(--theme-accent-muted)' : 'var(--theme-bg-primary)' }}
>
<Icon className="w-4 h-4 shrink-0" aria-hidden="true" />
<span className={active ? 'inline' : 'hidden sm:inline'}>{label}</span>
</button>
);
})}
</div>
);
}
27 changes: 27 additions & 0 deletions src/components/ViewToolbar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
'use client';

import { memo } from 'react';
import { ViewMode } from '@/lib/types';
import { ViewToggle } from './ViewToggle';

interface ViewToolbarProps {
viewMode: ViewMode;
onViewChange: (mode: ViewMode) => void;
eventCount: number;
}

/**
* Toolbar row directly above the events content: result count on the left,
* view mode switcher on the right.
*/
export const ViewToolbar = memo(function ViewToolbar({ viewMode, onViewChange, eventCount }: ViewToolbarProps) {
return (
<div className="px-2 sm:px-4 py-2 flex items-center justify-between gap-3 bg-[var(--theme-bg-list)] border-b border-[var(--theme-border-secondary)]">
<p className="text-sm text-[var(--theme-text-secondary)] tabular-nums whitespace-nowrap" aria-live="polite">
<span className="font-semibold text-[var(--theme-text-primary)]">{eventCount.toLocaleString()}</span>{' '}
{eventCount === 1 ? 'event' : 'events'}
</p>
<ViewToggle viewMode={viewMode} onViewChange={onViewChange} />
</div>
);
});
Loading