diff --git a/app/api/auth/clear/route.ts b/app/api/auth/clear/route.ts new file mode 100644 index 0000000..983b761 --- /dev/null +++ b/app/api/auth/clear/route.ts @@ -0,0 +1,74 @@ +import { createServerClient } from '@supabase/ssr' +import { NextRequest, NextResponse } from 'next/server' +import { cookies } from 'next/headers' + +export async function POST(request: NextRequest) { + try { + const cookieStore = await cookies() + + const supabase = createServerClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, + { + cookies: { + getAll() { + return cookieStore.getAll() + }, + setAll(cookiesToSet) { + cookiesToSet.forEach(({ name, value, options }) => { + cookieStore.set(name, value, options) + }) + }, + }, + } + ) + + // Sign out from Supabase + await supabase.auth.signOut() + + // Manually clear all Supabase-related cookies + const allCookies = cookieStore.getAll() + const supabaseCookies = allCookies.filter(cookie => + cookie.name.includes('sb-') || cookie.name.includes('supabase') + ) + + // Create response that clears cookies + const response = NextResponse.redirect(new URL('/auth/login', request.url)) + + // Clear each Supabase cookie explicitly + supabaseCookies.forEach(cookie => { + response.cookies.set(cookie.name, '', { + expires: new Date(0), + path: '/', + domain: undefined, + httpOnly: false, + secure: process.env.NODE_ENV === 'production', + sameSite: 'lax' + }) + }) + + // Also clear common Supabase cookie patterns + const commonCookieNames = [ + 'sb-access-token', + 'sb-refresh-token', + 'supabase-auth-token', + 'supabase.auth.token' + ] + + commonCookieNames.forEach(cookieName => { + response.cookies.set(cookieName, '', { + expires: new Date(0), + path: '/', + domain: undefined, + httpOnly: false, + secure: process.env.NODE_ENV === 'production', + sameSite: 'lax' + }) + }) + + return response + } catch (error) { + console.error('Error clearing auth:', error) + return NextResponse.json({ error: 'Failed to clear auth' }, { status: 500 }) + } +} \ No newline at end of file diff --git a/app/auth/debug/page.tsx b/app/auth/debug/page.tsx new file mode 100644 index 0000000..2bd93f5 --- /dev/null +++ b/app/auth/debug/page.tsx @@ -0,0 +1,92 @@ +import { createServerClient } from '@supabase/ssr' +import { cookies } from 'next/headers' + +export default async function AuthDebugPage() { + const cookieStore = await cookies() + + const supabase = createServerClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, + { + cookies: { + getAll() { + return cookieStore.getAll() + }, + setAll(cookiesToSet) { + cookiesToSet.forEach(({ name, value, options }) => { + cookieStore.set(name, value, options) + }) + }, + }, + } + ) + + const { data: { session }, error } = await supabase.auth.getSession() + const { data: { user } } = await supabase.auth.getUser() + + const allCookies = cookieStore.getAll() + const supabaseCookies = allCookies.filter(cookie => + cookie.name.includes('sb-') || cookie.name.includes('supabase') + ) + + return ( +
+

Authentication Debug Information

+ +
+
+

Server-side Session

+
+            {JSON.stringify({ session: !!session, user: !!user, error }, null, 2)}
+          
+
+ +
+

Session Details

+
+            {JSON.stringify({
+              sessionExists: !!session,
+              userExists: !!user,
+              userId: user?.id,
+              userEmail: user?.email,
+              sessionExpiry: session?.expires_at,
+              accessTokenExists: !!session?.access_token,
+              refreshTokenExists: !!session?.refresh_token,
+              errorMessage: error?.message
+            }, null, 2)}
+          
+
+ +
+

All Cookies

+
+            {JSON.stringify(allCookies.map(c => ({ name: c.name, value: c.value.substring(0, 50) + '...' })), null, 2)}
+          
+
+ +
+

Supabase Cookies

+
+            {JSON.stringify(supabaseCookies.map(c => ({ 
+              name: c.name, 
+              hasValue: !!c.value,
+              valueLength: c.value.length
+            })), null, 2)}
+          
+
+ +
+

Clear Auth Action

+
+ +
+
+
+
+ ) +} \ No newline at end of file diff --git a/app/auth/login/page.tsx b/app/auth/login/page.tsx index d7ecdae..b0ffa88 100644 --- a/app/auth/login/page.tsx +++ b/app/auth/login/page.tsx @@ -18,6 +18,7 @@ export default function LoginPage() { signIn, signInWithGoogle, signInWithApple, + clearStaleAuthData, isGoogleAuthEnabled, isAppleAuthEnabled } = useAuth() @@ -98,9 +99,20 @@ export default function LoginPage() { } } + const handleClearAuthData = async () => { + try { + await clearStaleAuthData() + setError('') + // Optionally refresh the page to ensure clean state + window.location.reload() + } catch (error: unknown) { + setError(error instanceof Error ? error.message : 'Failed to clear auth data') + } + } + return ( -
-
+
+

Sign in to LocalLoop @@ -113,21 +125,21 @@ export default function LoginPage() {

-
+ {error && ( -
+
{error}
)} -
+
setEmail(e.target.value)} - className="relative block w-full px-3 py-2 border border-border placeholder-muted-foreground text-foreground bg-background rounded-t-md focus:outline-none focus:ring-primary focus:border-primary" + className="block w-full px-4 py-3 border border-border placeholder-muted-foreground text-foreground bg-background rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-primary text-base" placeholder="Email address" />
@@ -137,7 +149,7 @@ export default function LoginPage() { required value={password} onChange={(e) => setPassword(e.target.value)} - className="relative block w-full px-3 py-2 border border-border placeholder-muted-foreground text-foreground bg-background rounded-b-md focus:outline-none focus:ring-primary focus:border-primary" + className="block w-full px-4 py-3 border border-border placeholder-muted-foreground text-foreground bg-background rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-primary text-base" placeholder="Password" />
@@ -153,7 +165,7 @@ export default function LoginPage() { @@ -169,13 +181,13 @@ export default function LoginPage() {
-
+
{/* Google Auth Button */} +
) diff --git a/app/auth/signup/page.tsx b/app/auth/signup/page.tsx index bb2b7d8..dbc6b5a 100644 --- a/app/auth/signup/page.tsx +++ b/app/auth/signup/page.tsx @@ -63,8 +63,8 @@ export default function SignupPage() { } return ( -
-
+
+

Create your LocalLoop account @@ -77,21 +77,21 @@ export default function SignupPage() {

-
+ {error && ( -
+
{error}
)} -
+
setEmail(e.target.value)} - className="relative block w-full px-3 py-2 border border-border placeholder-muted-foreground text-foreground bg-background rounded-t-md focus:outline-none focus:ring-primary focus:border-primary" + className="block w-full px-4 py-3 border border-border placeholder-muted-foreground text-foreground bg-background rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-primary text-base" placeholder="Email address" />
@@ -101,7 +101,7 @@ export default function SignupPage() { required value={password} onChange={(e) => setPassword(e.target.value)} - className="relative block w-full px-3 py-2 border border-border placeholder-muted-foreground text-foreground bg-background rounded-b-md focus:outline-none focus:ring-primary focus:border-primary" + className="block w-full px-4 py-3 border border-border placeholder-muted-foreground text-foreground bg-background rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-primary text-base" placeholder="Password" />
@@ -111,7 +111,7 @@ export default function SignupPage() { @@ -127,13 +127,13 @@ export default function SignupPage() {
-
+
{/* Google Auth Button */} )} - -
+ ) : ( + + ) : ( + <> + + + + )} +
+ )} +
+
+ +
+
- ${(ticket.ticket_type.price * ticket.quantity).toFixed(2)} + {formatPrice(ticket.ticket_type.price * ticket.quantity)}
@@ -209,17 +209,17 @@ export default function RefundDialog({
Original Amount: - ${order.total_amount.toFixed(2)} + {formatPrice(order.total_amount)}
{!isEventCancelled && (
Processing Fee: - -${refundCalculation.stripeFee.toFixed(2)} + -{formatPrice(refundCalculation.stripeFee)}
)}
Refund Amount: - ${refundCalculation.netRefund.toFixed(2)} + {formatPrice(refundCalculation.netRefund)}
@@ -270,7 +270,7 @@ export default function RefundDialog({
Confirm Refund
- This action cannot be undone. Your refund of ${refundCalculation.netRefund.toFixed(2)} will be processed immediately. + This action cannot be undone. Your refund of {formatPrice(refundCalculation.netRefund)} will be processed immediately.
diff --git a/components/events/EventCard.tsx b/components/events/EventCard.tsx index 61f4517..c5332ae 100644 --- a/components/events/EventCard.tsx +++ b/components/events/EventCard.tsx @@ -4,7 +4,7 @@ import React from 'react'; import Image from 'next/image'; import { Calendar, MapPin, Users, Clock, Tag, ExternalLink, ImageIcon } from 'lucide-react'; import { Card, CardHeader, CardContent, CardFooter, CardTitle, CardDescription } from '@/components/ui'; -import { formatDateTime, formatPrice, truncateText } from '@/lib/utils'; +import { formatDateTime, formatPrice, truncateText, getEventCardDescription, formatLocationForCard } from '@/lib/utils'; // Event interface (simplified from database types) export interface EventData { @@ -188,7 +188,7 @@ function DefaultCard({ event, size, featured, showImage, className, onClick, spo
- + {event.title}
@@ -204,8 +204,8 @@ function DefaultCard({ event, size, featured, showImage, className, onClick, spo )}
- - {event.short_description || truncateText(event.description || '', 100)} + + {getEventCardDescription(event.description, event.short_description)}
@@ -218,7 +218,7 @@ function DefaultCard({ event, size, featured, showImage, className, onClick, spo
- {event.location || 'Location TBD'} + {formatLocationForCard(event.location)}
@@ -274,7 +274,7 @@ function PreviewListCard({ event, className, onClick, isUpcoming, hasPrice, lowe
-

+

{event.title}

@@ -291,8 +291,8 @@ function PreviewListCard({ event, className, onClick, isUpcoming, hasPrice, lowe
-

- {event.short_description || truncateText(event.description || '', 120)} +

+ {getEventCardDescription(event.description, event.short_description)}

@@ -302,7 +302,7 @@ function PreviewListCard({ event, className, onClick, isUpcoming, hasPrice, lowe - {truncateText(event.location || 'Location TBD', 25)} + {truncateText(formatLocationForCard(event.location), 25)} @@ -362,8 +362,8 @@ function FullListCard({ event, className, onClick, spotsRemaining, isUpcoming, h )}
- - {event.short_description || truncateText(event.description || '', 120)} + + {getEventCardDescription(event.description, event.short_description)} @@ -383,7 +383,7 @@ function FullListCard({ event, className, onClick, spotsRemaining, isUpcoming, h
-
{event.location || 'Location TBD'}
+
{formatLocationForCard(event.location)}
View on map
@@ -449,7 +449,7 @@ function CompactCard({ event, className, onClick, hasPrice, lowestPrice }: CardC
{new Date(event.start_time).toLocaleDateString()} - {truncateText(event.location || 'Location TBD', 20)} + {truncateText(formatLocationForCard(event.location), 20)} {event.rsvp_count} attending
@@ -489,7 +489,7 @@ function TimelineCard({ event, className, onClick, hasPrice, lowestPrice }: Card {/* Event Details */}
-

+

{event.title}

{event.is_paid && ( @@ -499,8 +499,8 @@ function TimelineCard({ event, className, onClick, hasPrice, lowestPrice }: Card )}
-

- {event.short_description || truncateText(event.description || '', 80)} +

+ {getEventCardDescription(event.description, event.short_description, 80)}

@@ -510,7 +510,7 @@ function TimelineCard({ event, className, onClick, hasPrice, lowestPrice }: Card - {truncateText(event.location || 'Location TBD', 25)} + {truncateText(formatLocationForCard(event.location), 25)} diff --git a/components/events/EventForm.tsx b/components/events/EventForm.tsx index 40f7520..de843d7 100644 --- a/components/events/EventForm.tsx +++ b/components/events/EventForm.tsx @@ -439,6 +439,22 @@ export default function EventForm({ eventId, isEdit = false, onSuccess, onCancel {validationErrors.title && (

{validationErrors.title}

)} +
+

+ Keep it concise and engaging +

+

60 ? "text-orange-600 font-medium" : "text-gray-400" + )}> + {formData.title.length}/60 +

+
+ {formData.title.length > 60 && ( +

+ Consider shortening your title for better display in event cards and search results +

+ )}
@@ -482,9 +498,22 @@ export default function EventForm({ eventId, isEdit = false, onSuccess, onCancel onChange={(e) => handleInputChange('short_description', e.target.value)} placeholder="Brief one-line description (optional)" /> -

- This appears in event listings and search results -

+
+

+ This appears in event listings and search results +

+

120 ? "text-orange-600 font-medium" : "text-gray-400" + )}> + {formData.short_description.length}/120 +

+
+ {formData.short_description.length > 120 && ( +

+ Consider shortening your description - it may be truncated in event listings +

+ )}
diff --git a/components/ui/Footer.tsx b/components/ui/Footer.tsx index c985aba..89c2920 100644 --- a/components/ui/Footer.tsx +++ b/components/ui/Footer.tsx @@ -1,5 +1,6 @@ import React from 'react'; import Link from 'next/link'; +import Image from 'next/image'; export function Footer() { return ( @@ -7,11 +8,14 @@ export function Footer() {
- LocalLoop + LocalLoop

Connecting communities through local events diff --git a/components/ui/Navigation.tsx b/components/ui/Navigation.tsx index b15e3a2..911c03a 100644 --- a/components/ui/Navigation.tsx +++ b/components/ui/Navigation.tsx @@ -2,8 +2,9 @@ import React, { useState } from 'react' import Link from 'next/link' +import Image from 'next/image' import { useRouter } from 'next/navigation' -import { Menu, X } from 'lucide-react' +import { Menu, X, Shield, Settings } from 'lucide-react' import { useAuth } from '@/lib/auth-context' import { useAuth as useAuthHook } from '@/lib/hooks/useAuth' import { ProfileDropdown } from '@/components/auth/ProfileDropdown' @@ -38,27 +39,51 @@ export function Navigation({

- {/* Left side - Logo (always shown, always clickable home button) */} - - LocalLoop - LocalLoop - + {/* Left side - Logo and Admin/Staff Badge */} +
+ + LocalLoop + LocalLoop + + + {/* Admin/Staff Badge */} + {user && (isAdmin || isStaff) && ( +
+ {isAdmin ? ( + + ) : ( + + )} + + {isAdmin ? 'Admin' : 'Staff'} + +
+ )} +
{/* Right side - Full Navigation (always shown) */} <> {/* Desktop Navigation */}