Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
9002d79
fix(mobile): improve auth page mobile styling with proper margins and…
JacksonR64 Jun 19, 2025
f309030
fix(calendar): improve add to calendar card responsive design and layout
JacksonR64 Jun 19, 2025
9376854
feat(calendar): move disconnect to user dropdown and improve UX
JacksonR64 Jun 19, 2025
daf5645
feat(nav): add admin/staff role badges to navigation bar
JacksonR64 Jun 19, 2025
cc02d83
fix(refund): correct price display showing amounts 100x larger
JacksonR64 Jun 19, 2025
da3da8a
feat(nav): restrict Create Event to staff/admin and reorder navigation
JacksonR64 Jun 19, 2025
7017712
feat(nav): implement optimistic UI for auth loading state
JacksonR64 Jun 19, 2025
1e5c95b
feat(cards): improve event card description consistency and layout
JacksonR64 Jun 19, 2025
5f60538
fix(auth): improve authentication session handling and resolve redire…
JacksonR64 Jun 19, 2025
075265d
fix(typescript): resolve boolean type assignment in middleware
JacksonR64 Jun 19, 2025
2122c6b
fix(e2e): resolve CI pipeline failures for E2E and smoke tests
JacksonR64 Jun 19, 2025
e7dc982
fix(mobile): disable zoom and horizontal scrolling for better UX
JacksonR64 Jun 20, 2025
4603c05
feat(events): add Free and Soon badges with improved color scheme
JacksonR64 Jun 20, 2025
61cf4b6
fix(ui): improve dark mode compatibility and navigation layout
JacksonR64 Jun 20, 2025
84fb84b
refactor(events): improve event detail layout and reduce redundancy
JacksonR64 Jun 20, 2025
1564510
feat(homepage): implement past events toggle functionality
JacksonR64 Jun 20, 2025
d6877c4
feat(forms): enhance event creation form layout and usability
JacksonR64 Jun 20, 2025
7c091f9
chore: remove unused favicon and add cursor ignore file
JacksonR64 Jun 20, 2025
80e5b5a
fix(ui): standardize logo size across navigation and footer
JacksonR64 Jun 20, 2025
fa4e59c
Merge branch 'main' into feat/past-events-toggle
JacksonR64 Jun 20, 2025
0a885f9
fix(lint): remove unused imports and variables in EventForm
JacksonR64 Jun 20, 2025
601360e
fix(syntax): resolve JSX parsing error in EventForm component
JacksonR64 Jun 20, 2025
7262300
fix(accessibility): resolve lighthouse accessibility issues to improv…
JacksonR64 Jun 20, 2025
b687c04
feat(auth): add comprehensive input validation and security utilities
JacksonR64 Jun 20, 2025
7de8a8e
fix(security): enforce production encryption key requirement
JacksonR64 Jun 20, 2025
37589ac
feat(auth): add rate limiting and input validation to OAuth endpoints
JacksonR64 Jun 20, 2025
64418e6
feat(security): enhance security headers and authentication protection
JacksonR64 Jun 20, 2025
d71ea6a
docs(security): add comprehensive security improvements documentation
JacksonR64 Jun 20, 2025
090605a
feat(a11y): implement comprehensive accessibility improvements and da…
JacksonR64 Jun 20, 2025
bac380b
Merge branch 'main' into feature/security-authentication-best-practices
JacksonR64 Jun 20, 2025
ed038a9
fix(e2e): standardize data-testid attributes for test compatibility
JacksonR64 Jun 20, 2025
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
484 changes: 484 additions & 0 deletions ACCESSIBILITY_IMPROVEMENTS.md

Large diffs are not rendered by default.

74 changes: 58 additions & 16 deletions components/events/EventCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,8 @@ function SafeImage({

if (hasError || !src) {
return (
<div className={`bg-muted flex items-center justify-center ${className}`}>
<ImageIcon className="w-8 h-8 text-muted-foreground" />
<div className={`bg-muted flex items-center justify-center ${className}`} role="img" aria-label="Event image unavailable">
<ImageIcon className="w-8 h-8 text-muted-foreground" aria-hidden="true" />
</div>
);
}
Expand Down Expand Up @@ -166,12 +166,19 @@ export function EventCard({

// Default Card Style (same as current homepage implementation)
function DefaultCard({ event, size, featured, showImage, className, onClick, spotsRemaining, isUpcoming, hasPrice, lowestPrice, isSoon }: Readonly<CardComponentProps>) {
const cardId = `event-card-${event.id}`


return (
<Card
size={size}
variant={featured ? 'elevated' : 'default'}
className={`hover:shadow-lg transition-shadow cursor-pointer group ${className}`}
onClick={onClick}
role="article"
aria-labelledby={`${cardId}-title`}
aria-describedby={`${cardId}-description ${cardId}-details`}
data-test-id={`event-card-${event.id}`}
>
{showImage && event.image_url && (
<div className="relative w-full h-48 overflow-hidden rounded-t-lg">
Expand All @@ -186,7 +193,11 @@ function DefaultCard({ event, size, featured, showImage, className, onClick, spo
/>
{featured && (
<div className="absolute top-3 left-3">
<span className="bg-primary text-primary-foreground px-2 py-1 rounded-full text-xs font-medium">
<span
className="bg-primary text-primary-foreground px-2 py-1 rounded-full text-xs font-medium"
aria-label="Featured event"
data-test-id="featured-badge"
>
Featured
</span>
</div>
Expand All @@ -196,59 +207,86 @@ function DefaultCard({ event, size, featured, showImage, className, onClick, spo

<CardHeader>
<div className="flex items-start justify-between">
<CardTitle as={featured ? 'h2' : 'h3'} className={`${featured ? 'text-lg' : 'text-base'} line-clamp-2 min-h-[3rem] leading-relaxed pr-2`}>
<CardTitle
as={featured ? 'h2' : 'h3'}
className={`${featured ? 'text-lg' : 'text-base'} line-clamp-2 min-h-[3rem] leading-relaxed pr-2`}
id={`${cardId}-title`}
>
{event.title}
</CardTitle>
<div className="flex gap-2">
{!event.is_paid && isUpcoming && (
<span className="text-xs bg-green-100 text-green-800 px-2 py-1 rounded-full">
<span
className="text-xs bg-green-100 text-green-800 px-2 py-1 rounded-full"
aria-label="Free event"
data-test-id="free-badge"
>

Free
</span>
)}
{event.is_paid && (
<span className="text-xs bg-blue-100 text-blue-800 px-2 py-1 rounded-full">
<span
className="text-xs bg-blue-100 text-blue-800 px-2 py-1 rounded-full"
aria-label={`Paid event, ${hasPrice ? `starting at ${formatPrice(lowestPrice)}` : 'pricing available'}`}
data-test-id="paid-badge"
>

{hasPrice ? formatPrice(lowestPrice) : 'Paid'}
</span>
)}
{isSoon && (
<span className="text-xs bg-orange-100 text-orange-800 px-2 py-1 rounded-full">
<span
className="text-xs bg-orange-100 text-orange-800 px-2 py-1 rounded-full"
aria-label="Event starting soon"
data-test-id="soon-badge"
>

Soon
</span>
)}
{!isUpcoming && (
<span className="text-xs bg-yellow-100 text-yellow-800 px-2 py-1 rounded-full">
<span
className="text-xs bg-yellow-100 text-yellow-800 px-2 py-1 rounded-full"
aria-label="Past event"
data-test-id="past-badge"
>

Past
</span>
)}
</div>
</div>
<CardDescription className="min-h-[3rem] line-clamp-2 text-sm leading-relaxed">
<CardDescription
className="min-h-[3rem] line-clamp-2 text-sm leading-relaxed"
id={`${cardId}-description`}
>
{getEventCardDescription(event.description, event.short_description)}
</CardDescription>
</CardHeader>

<CardContent>
<div className="space-y-2 text-sm">
<div className="space-y-2 text-sm" id={`${cardId}-details`}>
<div className="flex items-center gap-2 text-muted-foreground">
<Calendar className="w-4 h-4 flex-shrink-0" />
<Calendar className="w-4 h-4 flex-shrink-0" aria-hidden="true" />
<span>{formatDateTime(event.start_time)}</span>
</div>

<div className="flex items-center gap-2 text-muted-foreground">
<MapPin className="w-4 h-4 flex-shrink-0" />
<MapPin className="w-4 h-4 flex-shrink-0" aria-hidden="true" />
<span className="line-clamp-2">{formatLocationForCard(event.location)}</span>
</div>

<div className="flex items-center gap-2 text-muted-foreground">
<Users className="w-4 h-4 flex-shrink-0" />
<Users className="w-4 h-4 flex-shrink-0" aria-hidden="true" />
<span>
{event.rsvp_count} attending
{spotsRemaining && spotsRemaining > 0 && ` β€’ ${spotsRemaining} spots left`}
</span>
</div>

<div className="flex items-center gap-2 text-muted-foreground">
<Tag className="w-4 h-4 flex-shrink-0" />
<Tag className="w-4 h-4 flex-shrink-0" aria-hidden="true" />
<span className="capitalize">{event.category || 'General'}</span>
</div>
</div>
Expand All @@ -258,9 +296,13 @@ function DefaultCard({ event, size, featured, showImage, className, onClick, spo
<span className="text-sm text-muted-foreground">
by {event.organizer.display_name}
</span>
<button className="text-blue-600 hover:text-blue-800 font-medium text-sm group-hover:underline flex items-center gap-1">
<button
className="text-blue-600 hover:text-blue-800 font-medium text-sm group-hover:underline flex items-center gap-1"
aria-label={`View details for ${event.title}`}
data-test-id="view-details-button"
>
View Details
<ExternalLink className="w-3 h-3" />
<ExternalLink className="w-3 h-3" aria-hidden="true" />
</button>
</CardFooter>
</Card>
Expand Down
4 changes: 2 additions & 2 deletions components/events/EventDetailClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ export function EventDetailClient({ event }: EventDetailClientProps) {
{/* Registration/Ticket Section */}
{event.is_paid && ticketTypes.length > 0 ? (
checkoutStep === 'tickets' ? (
<div className="space-y-4" data-test-id="ticket-selection">
<div className="space-y-4" data-test-id="ticket-section">
<div data-test-id="ticket-selection-component">
<TicketSelection
eventId={event.id}
Expand Down Expand Up @@ -269,7 +269,7 @@ export function EventDetailClient({ event }: EventDetailClientProps) {
</div>
)
) : (
<div data-test-id="rsvp-component">
<div data-test-id="rsvp-section">
<RSVPTicketSection
eventId={event.database_id || event.id}
eventTitle={event.title}
Expand Down
55 changes: 39 additions & 16 deletions components/ui/Navigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,25 @@ export function Navigation({


return (
<header className={`bg-card shadow-sm border-b border-border sticky top-0 z-50 ${className}`} data-test-id="homepage-header">
<>
{/* Skip link for keyboard navigation */}
<a
href="#main-content"
className="sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4 focus:bg-primary focus:text-primary-foreground focus:px-4 focus:py-2 focus:rounded-lg focus:z-[60] focus:outline-none focus:ring-2 focus:ring-ring"
data-test-id="skip-to-main-content"
>
Skip to main content
</a>
<header className={`bg-card shadow-sm border-b border-border sticky top-0 z-50 ${className}`} data-test-id="homepage-header">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex items-center justify-between h-16">
{/* Left side - Logo and Admin/Staff Badge */}
<div className="flex items-center gap-3">
<Link href="/" className="flex items-center gap-2">
<Link href="/" className="flex items-center gap-2" data-test-id="homepage-logo">
<Image
src="/logo.svg"
alt=""
alt="LocalLoop logo"

width={48}
height={48}
className="w-12 h-12"
Expand All @@ -54,15 +64,19 @@ export function Navigation({

{/* Admin/Staff Badge */}
{user && (isAdmin || isStaff) && (
<div className={`flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-medium ${
isAdmin
? 'bg-red-100 text-red-700 border border-red-200'
: 'bg-blue-100 text-blue-700 border border-blue-200'
}`}>
<div
className={`flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-medium ${
isAdmin
? 'bg-red-100 text-red-700 border border-red-200'
: 'bg-blue-100 text-blue-700 border border-blue-200'
}`}
aria-label={`Current user role: ${isAdmin ? 'Administrator' : 'Staff member'}`}
data-test-id="user-role-badge"
>
{isAdmin ? (
<Settings className="w-3 h-3" />
<Settings className="w-3 h-3" aria-hidden="true" />
) : (
<Shield className="w-3 h-3" />
<Shield className="w-3 h-3" aria-hidden="true" />
)}
<span className="hidden sm:inline">
{isAdmin ? 'Admin' : 'Staff'}
Expand All @@ -74,10 +88,10 @@ export function Navigation({
{/* Right side - Full Navigation (always shown) */}
<>
{/* Desktop Navigation */}
<nav className="hidden md:flex items-center gap-6" data-test-id="desktop-navigation">
<nav className="hidden md:flex items-center gap-6" aria-label="Primary navigation" data-test-id="desktop-navigation">

{(isStaff || isAdmin) && (
<Link href="/staff" className="text-muted-foreground hover:text-foreground transition-colors">
<Link href="/staff" className="text-muted-foreground hover:text-foreground transition-colors" data-test-id="staff-link">
Staff
</Link>
)}
Expand All @@ -95,6 +109,7 @@ export function Navigation({
<Link
href="/my-events"
className="text-muted-foreground hover:text-foreground transition-colors"
data-test-id="my-events-link"
>
My Events
</Link>
Expand All @@ -119,6 +134,7 @@ export function Navigation({
className={`bg-primary text-primary-foreground px-4 py-2 rounded-lg hover:bg-primary/90 transition-all duration-200 ${
authLoading ? 'opacity-75 pointer-events-none' : 'opacity-100'
}`}
data-test-id="sign-in-link"
>
Sign In
</Link>
Expand All @@ -133,17 +149,17 @@ export function Navigation({
data-test-id="mobile-menu-button"
>
{isMobileMenuOpen ? (
<X className="w-6 h-6 text-muted-foreground" />
<X className="w-6 h-6 text-muted-foreground" aria-hidden="true" />
) : (
<Menu className="w-6 h-6 text-muted-foreground" />
<Menu className="w-6 h-6 text-muted-foreground" aria-hidden="true" />
)}
</button>
</>
</div>

{/* Mobile Navigation */}
{isMobileMenuOpen && (
<div className="md:hidden border-t border-border py-4">
<div className="md:hidden border-t border-border py-4" data-test-id="mobile-navigation">
{/* Mobile Admin/Staff Badge */}
{user && (isAdmin || isStaff) && (
<div className="mb-4 flex justify-center">
Expand All @@ -162,13 +178,15 @@ export function Navigation({
</div>
)}

<nav className="flex flex-col space-y-4">
<nav className="flex flex-col space-y-4" aria-label="Mobile navigation">


{(isStaff || isAdmin) && (
<Link
href="/staff"
className="text-muted-foreground hover:text-foreground transition-colors py-2"
onClick={() => setIsMobileMenuOpen(false)}
data-test-id="mobile-staff-link"
>
Staff
</Link>
Expand All @@ -179,6 +197,7 @@ export function Navigation({
href="/create-event"
className="text-muted-foreground hover:text-foreground transition-colors py-2"
onClick={() => setIsMobileMenuOpen(false)}
data-test-id="mobile-create-event-link"
>
Create Event
</Link>
Expand All @@ -188,6 +207,7 @@ export function Navigation({
href="/my-events"
className="text-muted-foreground hover:text-foreground transition-colors py-2"
onClick={() => setIsMobileMenuOpen(false)}
data-test-id="mobile-my-events-link"
>
My Events
</Link>
Expand All @@ -198,6 +218,7 @@ export function Navigation({
setIsMobileMenuOpen(false)
}}
className="text-muted-foreground hover:text-foreground transition-colors py-2 text-left"
data-test-id="mobile-browse-events-button"
>
Browse Events
</button>
Expand All @@ -215,6 +236,7 @@ export function Navigation({
authLoading ? 'opacity-75 pointer-events-none' : 'opacity-100'
}`}
onClick={() => setIsMobileMenuOpen(false)}
data-test-id="mobile-sign-in-link"
>
Sign In
</Link>
Expand All @@ -224,5 +246,6 @@ export function Navigation({
)}
</div>
</header>
</>
)
}
1 change: 1 addition & 0 deletions components/ui/button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export interface ButtonProps
variant?: "default" | "destructive" | "outline" | "secondary" | "ghost" | "link"
size?: "default" | "sm" | "lg" | "icon"
asChild?: boolean
"data-testid"?: string
}

const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
Expand Down
Loading
Loading