diff --git a/app/api/companies/[slug]/members/check-invitation/route.ts b/app/api/companies/[slug]/members/check-invitation/route.ts new file mode 100644 index 00000000..c4a4d987 --- /dev/null +++ b/app/api/companies/[slug]/members/check-invitation/route.ts @@ -0,0 +1,81 @@ +import { NextRequest, NextResponse } from 'next/server' +import { createClient } from '@/lib/supabase/server' +import { companyService } from '@/lib/services/company-service' + +// Force Node.js runtime for API routes +export const runtime = 'nodejs' + +/** + * GET /api/companies/[slug]/members/check-invitation + * Check if the current user has an invitation to this company + * Requires authentication + */ +export async function GET( + request: NextRequest, + { params }: { params: Promise<{ slug: string }> } +) { + try { + const { slug } = await params + + // Check authentication + const supabase = await createClient() + const { + data: { user }, + error: authError, + } = await supabase.auth.getUser() + + if (authError || !user) { + return NextResponse.json( + { success: false, error: 'Unauthorized: Authentication required' }, + { status: 401 } + ) + } + + // Get company + const company = await companyService.getCompanyBySlug(slug) + + if (!company) { + return NextResponse.json( + { + success: false, + error: 'Company not found', + }, + { status: 404 } + ) + } + + // Check if user has a membership (any status) + const { data: membership, error: membershipError } = await supabase + .from('company_members') + .select('*') + .eq('company_id', company.id) + .eq('user_id', user.id) + .single() + + if (membershipError) { + if (membershipError.code === 'PGRST116') { + // No membership found + return NextResponse.json({ + success: true, + membership: null, + }) + } + throw membershipError + } + + return NextResponse.json({ + success: true, + membership, + }) + } catch (error) { + console.error('Error in GET /api/companies/[slug]/members/check-invitation:', error) + + return NextResponse.json( + { + success: false, + error: 'Internal server error', + }, + { status: 500 } + ) + } +} diff --git a/app/auth/forgot-password/page.tsx b/app/auth/forgot-password/page.tsx index 8e88e71f..0c287bac 100644 --- a/app/auth/forgot-password/page.tsx +++ b/app/auth/forgot-password/page.tsx @@ -36,14 +36,25 @@ export default function ForgotPasswordPage() { }) if (error) { - throw error + console.error("Supabase error:", error) + + // Show specific error messages + if (error.message.includes("Email") || error.message.includes("SMTP")) { + toast.error("Email service not configured. Please contact support.") + } else if (error.message.includes("rate limit")) { + toast.error("Too many requests. Please try again later.") + } else { + toast.error(error.message || "Failed to send reset link. Please try again.") + } + return } toast.success("Password reset link sent! Check your email.") setFormData({ email: "" }) } catch (error) { console.error("Error sending reset link:", error) - toast.error("Failed to send reset link. Please try again.") + const errorMessage = error instanceof Error ? error.message : "Failed to send reset link. Please try again." + toast.error(errorMessage) } finally { setIsLoading(false) } diff --git a/app/dashboard/company/[slug]/accept-invitation/page.tsx b/app/dashboard/company/[slug]/accept-invitation/page.tsx index 0b81eb6e..685cb706 100644 --- a/app/dashboard/company/[slug]/accept-invitation/page.tsx +++ b/app/dashboard/company/[slug]/accept-invitation/page.tsx @@ -41,14 +41,13 @@ export default function AcceptInvitationPage() { setCompanyName(companyData.company?.name || companySlug) // Check if user has a pending invitation - const membersResponse = await fetch(`/api/companies/${companySlug}/members`) - if (!membersResponse.ok) { - throw new Error('Failed to check invitation status') + const invitationResponse = await fetch(`/api/companies/${companySlug}/members/check-invitation`) + if (!invitationResponse.ok) { + const errorData = await invitationResponse.json() + throw new Error(errorData.error || 'Failed to check invitation status') } - const membersData = await membersResponse.json() - const members = membersData.members || [] - - const userMembership = members.find((m: { user_id: string; status: string }) => m.user_id === user.id) + const invitationData = await invitationResponse.json() + const userMembership = invitationData.membership if (!userMembership) { setError('No invitation found for your account') diff --git a/app/dashboard/company/[slug]/events/[eventSlug]/edit/page.tsx b/app/dashboard/company/[slug]/events/[eventSlug]/edit/page.tsx new file mode 100644 index 00000000..0639f8a1 --- /dev/null +++ b/app/dashboard/company/[slug]/events/[eventSlug]/edit/page.tsx @@ -0,0 +1,167 @@ +'use client' + +import { useState, useEffect, useCallback } from 'react' +import { useRouter, useParams } from 'next/navigation' +import { useCompanyContext } from '@/contexts/CompanyContext' +import { EventForm } from '@/components/dashboard/EventForm' +import { ArrowLeft, Trash2 } from 'lucide-react' +import { Button } from '@/components/ui/button' +import Link from 'next/link' +import { Event } from '@/types/events' +import { toast } from 'sonner' +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from '@/components/ui/alert-dialog' + +export default function EditEventPage() { + const router = useRouter() + const params = useParams() + const slug = params.slug as string + const { currentCompany, loading: companyLoading } = useCompanyContext() + const [event, setEvent] = useState(null) + const [loading, setLoading] = useState(true) + const [deleting, setDeleting] = useState(false) + + const fetchEvent = useCallback(async () => { + try { + setLoading(true) + const response = await fetch(`/api/events/${slug}`) + + if (!response.ok) { + throw new Error('Failed to fetch event') + } + + const data = await response.json() + setEvent(data.event) + } catch (error) { + console.error('Error fetching event:', error) + toast.error('Failed to load event') + router.push('/dashboard/company/events') + } finally { + setLoading(false) + } + }, [slug, router]) + + useEffect(() => { + if (slug) { + fetchEvent() + } + }, [slug, fetchEvent]) + + const handleSuccess = (updatedEvent: Event) => { + setEvent(updatedEvent) + toast.success('Event updated successfully!') + } + + const handleDelete = async () => { + try { + setDeleting(true) + const response = await fetch(`/api/events/${slug}`, { + method: 'DELETE', + }) + + if (!response.ok) { + throw new Error('Failed to delete event') + } + + toast.success('Event deleted successfully!') + router.push('/dashboard/company/events') + } catch (error) { + console.error('Error deleting event:', error) + toast.error('Failed to delete event') + } finally { + setDeleting(false) + } + } + + if (loading || companyLoading || !currentCompany) { + return ( +
+
+
+ ) + } + + if (!event) { + return ( +
+
+

Event not found

+

+ The event you're looking for doesn't exist or you don't have access to it. +

+ + + +
+
+ ) + } + + return ( +
+ {/* Header */} +
+
+ + + +
+

Edit Event

+

+ Update your event details +

+
+
+ + {/* Delete Button */} + + + + + + + Are you sure? + + This action cannot be undone. This will permanently delete the event + "{event.title}" and all associated data. + + + + Cancel + + {deleting ? 'Deleting...' : 'Delete Event'} + + + + +
+ + {/* Event Form */} + +
+ ) +} diff --git a/app/dashboard/company/[slug]/events/create/page.tsx b/app/dashboard/company/[slug]/events/create/page.tsx new file mode 100644 index 00000000..eb0d5e81 --- /dev/null +++ b/app/dashboard/company/[slug]/events/create/page.tsx @@ -0,0 +1,53 @@ +'use client' + +import { useRouter } from 'next/navigation' +import { useCompanyContext } from '@/contexts/CompanyContext' +import { EventForm } from '@/components/dashboard/EventForm' +import { ArrowLeft } from 'lucide-react' +import { Button } from '@/components/ui/button' +import Link from 'next/link' + +export default function CreateEventPage() { + const router = useRouter() + const { currentCompany, loading } = useCompanyContext() + + const handleSuccess = () => { + // Redirect to events list after successful creation + router.push('/dashboard/company/events') + } + + if (loading || !currentCompany) { + return ( +
+
+
+ ) + } + + return ( +
+ {/* Header */} +
+ + + +
+

Create Event

+

+ Create a new event for your company +

+
+
+ + {/* Event Form */} + +
+ ) +} diff --git a/app/dashboard/company/[slug]/events/page.tsx b/app/dashboard/company/[slug]/events/page.tsx new file mode 100644 index 00000000..cce7d92d --- /dev/null +++ b/app/dashboard/company/[slug]/events/page.tsx @@ -0,0 +1,290 @@ +'use client' + +import { useState, useEffect, useCallback } from 'react' +import { useCompanyContext } from '@/contexts/CompanyContext' +import { Button } from '@/components/ui/button' +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' +import { Badge } from '@/components/ui/badge' +import { Input } from '@/components/ui/input' +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table' +import { Calendar, Search, Plus, Edit, Eye, Clock, CheckCircle, XCircle, AlertCircle } from 'lucide-react' +import { toast } from 'sonner' +import Link from 'next/link' +import { Event } from '@/types/events' + +export default function CompanyEventsPage() { + const { currentCompany, loading: companyLoading } = useCompanyContext() + const [events, setEvents] = useState([]) + const [loading, setLoading] = useState(true) + const [searchTerm, setSearchTerm] = useState('') + + const fetchEvents = useCallback(async () => { + if (!currentCompany) return + + try { + setLoading(true) + // Fetch all events (not just approved) for company members + const response = await fetch(`/api/companies/${currentCompany.slug}/events?status=all&limit=100`) + + if (!response.ok) { + throw new Error('Failed to fetch events') + } + + const data = await response.json() + setEvents(data.events || []) + } catch (error) { + console.error('Error fetching events:', error) + toast.error('Failed to load events') + } finally { + setLoading(false) + } + }, [currentCompany]) + + useEffect(() => { + if (currentCompany) { + fetchEvents() + } + }, [currentCompany, fetchEvents]) + + const filteredEvents = events.filter(event => + event.title.toLowerCase().includes(searchTerm.toLowerCase()) || + event.category.toLowerCase().includes(searchTerm.toLowerCase()) + ) + + const stats = { + total: events.length, + approved: events.filter(e => e.approval_status === 'approved').length, + pending: events.filter(e => e.approval_status === 'pending').length, + draft: events.filter(e => e.status === 'draft').length, + } + + const getApprovalBadge = (status: string) => { + switch (status) { + case 'approved': + return ( + + + Approved + + ) + case 'pending': + return ( + + + Pending + + ) + case 'rejected': + return ( + + + Rejected + + ) + case 'changes_requested': + return ( + + + Changes Requested + + ) + default: + return {status} + } + } + + const getStatusBadge = (status: string) => { + switch (status) { + case 'live': + case 'published': + return Live + case 'draft': + return Draft + case 'cancelled': + return Cancelled + case 'completed': + return Completed + default: + return {status} + } + } + + if (companyLoading || !currentCompany) { + return ( +
+
+
+ ) + } + + return ( +
+ {/* Header */} +
+
+

Events

+

+ Manage your company's events and hackathons +

+
+ + + +
+ + {/* Stats Cards */} +
+ + + Total Events + + + +
{stats.total}
+
+
+ + + Approved + + + +
{stats.approved}
+
+
+ + + Pending Review + + + +
{stats.pending}
+
+
+ + + Drafts + + + +
{stats.draft}
+
+
+
+ + {/* Search */} +
+
+ + setSearchTerm(e.target.value)} + className="pl-10" + /> +
+
+ + {/* Events Table */} + + + All Events ({filteredEvents.length}) + + View and manage all your events + + + + {loading ? ( +
+
+
+ ) : filteredEvents.length === 0 ? ( +
+ +

+ No events found +

+

+ {searchTerm ? 'Try adjusting your search' : 'Get started by creating your first event'} +

+ {!searchTerm && ( + + + + )} +
+ ) : ( + + + + Event + Date + Category + Status + Approval + Views + Registered + Actions + + + + {filteredEvents.map((event) => ( + + +
+ {event.title} + + {event.excerpt} + +
+
+ +
+ + {new Date(event.date).toLocaleDateString()} +
+
+ + {event.category} + + {getStatusBadge(event.status)} + {getApprovalBadge(event.approval_status)} + +
+ + {event.views || 0} +
+
+ {event.registered || 0} + +
+ + + + {event.approval_status === 'approved' && ( + + + + )} +
+
+
+ ))} +
+
+ )} +
+
+
+ ) +} diff --git a/app/events/page.tsx b/app/events/page.tsx index fbfa4928..e57d6783 100644 --- a/app/events/page.tsx +++ b/app/events/page.tsx @@ -1,6 +1,6 @@ "use client" -import { useState, useEffect, useRef } from "react" +import React, { useState, useEffect, useRef } from "react" import { Button } from "@/components/ui/button" import { Card } from "@/components/ui/card" @@ -16,7 +16,7 @@ import Image from "next/image"; import "keen-slider/keen-slider.min.css" import { useKeenSlider } from "keen-slider/react" import { cn } from "@/lib/utils"; -import { useEvents, useFeaturedEvents } from "@/hooks/useEvents" +import { useEvents } from "@/hooks/useEvents" import { CompanyBadge } from "@/components/companies/CompanyBadge" import type { Company } from "@/types/company" @@ -46,17 +46,22 @@ export default function EventsPage() { const [industries, setIndustries] = useState([]) const [companySizes] = useState(['startup', 'small', 'medium', 'large', 'enterprise']) - // Use custom hooks for data fetching - const { data: eventsData, loading: eventsLoading, error: eventsError } = useEvents({ + // Memoize params to prevent infinite re-renders + const eventsParams = React.useMemo(() => ({ search: searchTerm, category: selectedCategory, dateFilter: dateFilter === "Upcoming" ? "upcoming" : dateFilter === "All" ? "all" : "all", company_id: selectedCompany !== "All" ? selectedCompany : undefined, company_industry: selectedIndustry !== "All" ? selectedIndustry : undefined, company_size: selectedCompanySize !== "All" ? selectedCompanySize : undefined - }) + }), [searchTerm, selectedCategory, dateFilter, selectedCompany, selectedIndustry, selectedCompanySize]) + + // Use custom hooks for data fetching + const { data: eventsData, loading: eventsLoading, error: eventsError } = useEvents(eventsParams) - const { loading: featuredLoading } = useFeaturedEvents(5) + // Temporarily disable featured events to fix loading issue + // const { loading: featuredLoading } = useFeaturedEvents(5) + const featuredLoading = false // Fetch companies for filter useEffect(() => { @@ -83,6 +88,15 @@ export default function EventsPage() { // Extract events from the response const events = eventsData?.events || [] const isLoading = eventsLoading || featuredLoading + + // Debug logging + console.log('Events Page Debug:', { + eventsLoading, + featuredLoading, + isLoading, + eventsCount: events.length, + eventsData + }) // Unique values for filters @@ -213,7 +227,10 @@ export default function EventsPage() { setTimeout(() => setCopiedEventId(null), 1500) } + console.log('About to check isLoading:', isLoading) + if (isLoading) { + console.log('Rendering loading spinner') return (
@@ -223,6 +240,8 @@ export default function EventsPage() {
) } + + console.log('Rendering main content with', events.length, 'events') return (
diff --git a/lib/services/events.ts b/lib/services/events.ts index 34386391..1929f070 100644 --- a/lib/services/events.ts +++ b/lib/services/events.ts @@ -353,16 +353,25 @@ class EventsService { } // Prepare event data with company context + // Payment constraint: if price is "Free", payment must be "Not Required" + // if price is not "Free", payment must be "Required" + const paymentValue = eventData.price === 'Free' ? 'Not Required' : (eventData.payment || 'Required') + + // Remove status field - events start as draft/pending based on approval_status + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { status, ...eventDataWithoutStatus } = eventData + const eventPayload = { - ...eventData, + ...eventDataWithoutStatus, company_id: companyId, created_by: userId, approval_status: 'pending' as const, - views: 0, - clicks: 0, is_codeunia_event: false, + payment: paymentValue, } + console.log('Event payload:', JSON.stringify(eventPayload, null, 2)) + const { data: event, error } = await supabase .from('events') .insert([eventPayload]) @@ -421,6 +430,7 @@ class EventsService { views, clicks, company, + status: _status, ...updateData } = eventData /* eslint-enable @typescript-eslint/no-unused-vars */