Skip to content

Commit 5495e28

Browse files
authored
Merge pull request #313 from codeunia-dev/feat/multicompanylevel
feat(events): Add event management pages and invitation check API
2 parents 2544805 + 943ce60 commit 5495e28

File tree

8 files changed

+648
-18
lines changed

8 files changed

+648
-18
lines changed
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { NextRequest, NextResponse } from 'next/server'
2+
import { createClient } from '@/lib/supabase/server'
3+
import { companyService } from '@/lib/services/company-service'
4+
5+
// Force Node.js runtime for API routes
6+
export const runtime = 'nodejs'
7+
8+
/**
9+
* GET /api/companies/[slug]/members/check-invitation
10+
* Check if the current user has an invitation to this company
11+
* Requires authentication
12+
*/
13+
export async function GET(
14+
request: NextRequest,
15+
{ params }: { params: Promise<{ slug: string }> }
16+
) {
17+
try {
18+
const { slug } = await params
19+
20+
// Check authentication
21+
const supabase = await createClient()
22+
const {
23+
data: { user },
24+
error: authError,
25+
} = await supabase.auth.getUser()
26+
27+
if (authError || !user) {
28+
return NextResponse.json(
29+
{ success: false, error: 'Unauthorized: Authentication required' },
30+
{ status: 401 }
31+
)
32+
}
33+
34+
// Get company
35+
const company = await companyService.getCompanyBySlug(slug)
36+
37+
if (!company) {
38+
return NextResponse.json(
39+
{
40+
success: false,
41+
error: 'Company not found',
42+
},
43+
{ status: 404 }
44+
)
45+
}
46+
47+
// Check if user has a membership (any status)
48+
const { data: membership, error: membershipError } = await supabase
49+
.from('company_members')
50+
.select('*')
51+
.eq('company_id', company.id)
52+
.eq('user_id', user.id)
53+
.single()
54+
55+
if (membershipError) {
56+
if (membershipError.code === 'PGRST116') {
57+
// No membership found
58+
return NextResponse.json({
59+
success: true,
60+
membership: null,
61+
})
62+
}
63+
throw membershipError
64+
}
65+
66+
return NextResponse.json({
67+
success: true,
68+
membership,
69+
})
70+
} catch (error) {
71+
console.error('Error in GET /api/companies/[slug]/members/check-invitation:', error)
72+
73+
return NextResponse.json(
74+
{
75+
success: false,
76+
error: 'Internal server error',
77+
},
78+
{ status: 500 }
79+
)
80+
}
81+
}

app/auth/forgot-password/page.tsx

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,14 +36,25 @@ export default function ForgotPasswordPage() {
3636
})
3737

3838
if (error) {
39-
throw error
39+
console.error("Supabase error:", error)
40+
41+
// Show specific error messages
42+
if (error.message.includes("Email") || error.message.includes("SMTP")) {
43+
toast.error("Email service not configured. Please contact support.")
44+
} else if (error.message.includes("rate limit")) {
45+
toast.error("Too many requests. Please try again later.")
46+
} else {
47+
toast.error(error.message || "Failed to send reset link. Please try again.")
48+
}
49+
return
4050
}
4151

4252
toast.success("Password reset link sent! Check your email.")
4353
setFormData({ email: "" })
4454
} catch (error) {
4555
console.error("Error sending reset link:", error)
46-
toast.error("Failed to send reset link. Please try again.")
56+
const errorMessage = error instanceof Error ? error.message : "Failed to send reset link. Please try again."
57+
toast.error(errorMessage)
4758
} finally {
4859
setIsLoading(false)
4960
}

app/dashboard/company/[slug]/accept-invitation/page.tsx

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -41,14 +41,13 @@ export default function AcceptInvitationPage() {
4141
setCompanyName(companyData.company?.name || companySlug)
4242

4343
// Check if user has a pending invitation
44-
const membersResponse = await fetch(`/api/companies/${companySlug}/members`)
45-
if (!membersResponse.ok) {
46-
throw new Error('Failed to check invitation status')
44+
const invitationResponse = await fetch(`/api/companies/${companySlug}/members/check-invitation`)
45+
if (!invitationResponse.ok) {
46+
const errorData = await invitationResponse.json()
47+
throw new Error(errorData.error || 'Failed to check invitation status')
4748
}
48-
const membersData = await membersResponse.json()
49-
const members = membersData.members || []
50-
51-
const userMembership = members.find((m: { user_id: string; status: string }) => m.user_id === user.id)
49+
const invitationData = await invitationResponse.json()
50+
const userMembership = invitationData.membership
5251

5352
if (!userMembership) {
5453
setError('No invitation found for your account')
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
'use client'
2+
3+
import { useState, useEffect, useCallback } from 'react'
4+
import { useRouter, useParams } from 'next/navigation'
5+
import { useCompanyContext } from '@/contexts/CompanyContext'
6+
import { EventForm } from '@/components/dashboard/EventForm'
7+
import { ArrowLeft, Trash2 } from 'lucide-react'
8+
import { Button } from '@/components/ui/button'
9+
import Link from 'next/link'
10+
import { Event } from '@/types/events'
11+
import { toast } from 'sonner'
12+
import {
13+
AlertDialog,
14+
AlertDialogAction,
15+
AlertDialogCancel,
16+
AlertDialogContent,
17+
AlertDialogDescription,
18+
AlertDialogFooter,
19+
AlertDialogHeader,
20+
AlertDialogTitle,
21+
AlertDialogTrigger,
22+
} from '@/components/ui/alert-dialog'
23+
24+
export default function EditEventPage() {
25+
const router = useRouter()
26+
const params = useParams()
27+
const slug = params.slug as string
28+
const { currentCompany, loading: companyLoading } = useCompanyContext()
29+
const [event, setEvent] = useState<Event | null>(null)
30+
const [loading, setLoading] = useState(true)
31+
const [deleting, setDeleting] = useState(false)
32+
33+
const fetchEvent = useCallback(async () => {
34+
try {
35+
setLoading(true)
36+
const response = await fetch(`/api/events/${slug}`)
37+
38+
if (!response.ok) {
39+
throw new Error('Failed to fetch event')
40+
}
41+
42+
const data = await response.json()
43+
setEvent(data.event)
44+
} catch (error) {
45+
console.error('Error fetching event:', error)
46+
toast.error('Failed to load event')
47+
router.push('/dashboard/company/events')
48+
} finally {
49+
setLoading(false)
50+
}
51+
}, [slug, router])
52+
53+
useEffect(() => {
54+
if (slug) {
55+
fetchEvent()
56+
}
57+
}, [slug, fetchEvent])
58+
59+
const handleSuccess = (updatedEvent: Event) => {
60+
setEvent(updatedEvent)
61+
toast.success('Event updated successfully!')
62+
}
63+
64+
const handleDelete = async () => {
65+
try {
66+
setDeleting(true)
67+
const response = await fetch(`/api/events/${slug}`, {
68+
method: 'DELETE',
69+
})
70+
71+
if (!response.ok) {
72+
throw new Error('Failed to delete event')
73+
}
74+
75+
toast.success('Event deleted successfully!')
76+
router.push('/dashboard/company/events')
77+
} catch (error) {
78+
console.error('Error deleting event:', error)
79+
toast.error('Failed to delete event')
80+
} finally {
81+
setDeleting(false)
82+
}
83+
}
84+
85+
if (loading || companyLoading || !currentCompany) {
86+
return (
87+
<div className="flex items-center justify-center min-h-[400px]">
88+
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
89+
</div>
90+
)
91+
}
92+
93+
if (!event) {
94+
return (
95+
<div className="flex items-center justify-center min-h-[400px]">
96+
<div className="text-center">
97+
<h2 className="text-2xl font-bold mb-2">Event not found</h2>
98+
<p className="text-muted-foreground mb-4">
99+
The event you&apos;re looking for doesn&apos;t exist or you don&apos;t have access to it.
100+
</p>
101+
<Link href="/dashboard/company/events">
102+
<Button>Back to Events</Button>
103+
</Link>
104+
</div>
105+
</div>
106+
)
107+
}
108+
109+
return (
110+
<div className="space-y-6">
111+
{/* Header */}
112+
<div className="flex items-center justify-between">
113+
<div className="flex items-center gap-4">
114+
<Link href="/dashboard/company/events">
115+
<Button variant="outline" size="sm">
116+
<ArrowLeft className="h-4 w-4 mr-2" />
117+
Back to Events
118+
</Button>
119+
</Link>
120+
<div>
121+
<h1 className="text-3xl font-bold tracking-tight">Edit Event</h1>
122+
<p className="text-muted-foreground mt-1">
123+
Update your event details
124+
</p>
125+
</div>
126+
</div>
127+
128+
{/* Delete Button */}
129+
<AlertDialog>
130+
<AlertDialogTrigger asChild>
131+
<Button variant="outline" className="text-red-600 border-red-600 hover:bg-red-50">
132+
<Trash2 className="h-4 w-4 mr-2" />
133+
Delete Event
134+
</Button>
135+
</AlertDialogTrigger>
136+
<AlertDialogContent>
137+
<AlertDialogHeader>
138+
<AlertDialogTitle>Are you sure?</AlertDialogTitle>
139+
<AlertDialogDescription>
140+
This action cannot be undone. This will permanently delete the event
141+
&quot;{event.title}&quot; and all associated data.
142+
</AlertDialogDescription>
143+
</AlertDialogHeader>
144+
<AlertDialogFooter>
145+
<AlertDialogCancel>Cancel</AlertDialogCancel>
146+
<AlertDialogAction
147+
onClick={handleDelete}
148+
disabled={deleting}
149+
className="bg-red-600 hover:bg-red-700"
150+
>
151+
{deleting ? 'Deleting...' : 'Delete Event'}
152+
</AlertDialogAction>
153+
</AlertDialogFooter>
154+
</AlertDialogContent>
155+
</AlertDialog>
156+
</div>
157+
158+
{/* Event Form */}
159+
<EventForm
160+
company={currentCompany}
161+
event={event}
162+
mode="edit"
163+
onSuccess={handleSuccess}
164+
/>
165+
</div>
166+
)
167+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
'use client'
2+
3+
import { useRouter } from 'next/navigation'
4+
import { useCompanyContext } from '@/contexts/CompanyContext'
5+
import { EventForm } from '@/components/dashboard/EventForm'
6+
import { ArrowLeft } from 'lucide-react'
7+
import { Button } from '@/components/ui/button'
8+
import Link from 'next/link'
9+
10+
export default function CreateEventPage() {
11+
const router = useRouter()
12+
const { currentCompany, loading } = useCompanyContext()
13+
14+
const handleSuccess = () => {
15+
// Redirect to events list after successful creation
16+
router.push('/dashboard/company/events')
17+
}
18+
19+
if (loading || !currentCompany) {
20+
return (
21+
<div className="flex items-center justify-center min-h-[400px]">
22+
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
23+
</div>
24+
)
25+
}
26+
27+
return (
28+
<div className="space-y-6">
29+
{/* Header */}
30+
<div className="flex items-center gap-4">
31+
<Link href="/dashboard/company/events">
32+
<Button variant="outline" size="sm">
33+
<ArrowLeft className="h-4 w-4 mr-2" />
34+
Back to Events
35+
</Button>
36+
</Link>
37+
<div>
38+
<h1 className="text-3xl font-bold tracking-tight">Create Event</h1>
39+
<p className="text-muted-foreground mt-1">
40+
Create a new event for your company
41+
</p>
42+
</div>
43+
</div>
44+
45+
{/* Event Form */}
46+
<EventForm
47+
company={currentCompany}
48+
mode="create"
49+
onSuccess={handleSuccess}
50+
/>
51+
</div>
52+
)
53+
}

0 commit comments

Comments
 (0)