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
4 changes: 2 additions & 2 deletions app/api/companies/[slug]/members/[userId]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ export const runtime = 'nodejs'

// Validation schema for role update
const updateRoleSchema = z.object({
role: z.enum(['owner', 'admin', 'editor', 'member'], {
errorMap: () => ({ message: 'Role must be owner, admin, editor, or member' }),
role: z.enum(['owner', 'admin', 'editor', 'viewer'], {
errorMap: () => ({ message: 'Role must be owner, admin, editor, or viewer' }),
}),
})

Expand Down
4 changes: 2 additions & 2 deletions app/api/companies/[slug]/members/invite/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ export const runtime = 'nodejs'
// Validation schema for invite request
const inviteSchema = z.object({
email: z.string().email('Invalid email address'),
role: z.enum(['admin', 'editor', 'member'], {
errorMap: () => ({ message: 'Role must be admin, editor, or member' }),
role: z.enum(['admin', 'editor', 'viewer'], {
errorMap: () => ({ message: 'Role must be admin, editor, or viewer' }),
}),
})

Expand Down
2 changes: 1 addition & 1 deletion app/dashboard/company/[slug]/accept-invitation/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ export default function AcceptInvitationPage() {
<>
<div className="bg-zinc-800/50 border border-zinc-700 rounded-lg p-4">
<p className="text-sm text-zinc-300 mb-2">
You&apos;ve been invited to join <strong className="text-white">{companyName}</strong> on CodeUnia.
You&apos;ve been invited to join <strong className="text-white">{companyName}</strong> on Codeunia.
</p>
<p className="text-sm text-zinc-400">
By accepting this invitation, you&apos;ll be able to collaborate with the team and manage events.
Expand Down
10 changes: 10 additions & 0 deletions app/dashboard/company/[slug]/analytics/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover
import { Skeleton } from '@/components/ui/skeleton'
import { AnalyticsCharts } from '@/components/dashboard/AnalyticsCharts'
import { useCompanyContext } from '@/contexts/CompanyContext'
import { usePendingInvitationRedirect } from '@/lib/hooks/usePendingInvitationRedirect'
import { CompanyAnalytics } from '@/types/company'
import { format, subDays, startOfMonth, endOfMonth, subMonths } from 'date-fns'
import { CalendarIcon, AlertCircle, TrendingUp } from 'lucide-react'
Expand All @@ -25,6 +26,7 @@ export default function AnalyticsPage() {
const params = useParams()
const companySlug = params?.slug as string
const { currentCompany, loading: companyLoading } = useCompanyContext()
const isPendingInvitation = usePendingInvitationRedirect()

const [analytics, setAnalytics] = useState<CompanyAnalytics[]>([])
const [loading, setLoading] = useState(true)
Expand Down Expand Up @@ -67,6 +69,14 @@ export default function AnalyticsPage() {
}
}, [currentCompany, fetchAnalytics])

if (companyLoading || isPendingInvitation) {
return (
<div className="flex items-center justify-center min-h-[60vh]">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
</div>
)
}

const handlePresetChange = (preset: PresetRange) => {
setSelectedPreset(preset)
const today = new Date()
Expand Down
10 changes: 10 additions & 0 deletions app/dashboard/company/[slug]/events/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import { useState, useEffect, useCallback } from 'react'
import { useCompanyContext } from '@/contexts/CompanyContext'
import { usePendingInvitationRedirect } from '@/lib/hooks/usePendingInvitationRedirect'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
Expand All @@ -25,6 +26,7 @@ import {

export default function CompanyEventsPage() {
const { currentCompany, loading: companyLoading } = useCompanyContext()
const isPendingInvitation = usePendingInvitationRedirect()
const [events, setEvents] = useState<Event[]>([])
const [loading, setLoading] = useState(true)
const [searchTerm, setSearchTerm] = useState('')
Expand Down Expand Up @@ -58,6 +60,14 @@ export default function CompanyEventsPage() {
}
}, [currentCompany, fetchEvents])

if (companyLoading || isPendingInvitation) {
return (
<div className="flex items-center justify-center min-h-[60vh]">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
</div>
)
}

const filteredEvents = events.filter(event =>
event.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
event.category.toLowerCase().includes(searchTerm.toLowerCase())
Expand Down
10 changes: 10 additions & 0 deletions app/dashboard/company/[slug]/hackathons/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import { useState, useEffect, useCallback } from 'react'
import { useCompanyContext } from '@/contexts/CompanyContext'
import { usePendingInvitationRedirect } from '@/lib/hooks/usePendingInvitationRedirect'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
Expand Down Expand Up @@ -29,6 +30,7 @@ interface Hackathon {

export default function CompanyHackathonsPage() {
const { currentCompany, loading: companyLoading } = useCompanyContext()
const isPendingInvitation = usePendingInvitationRedirect()
const [hackathons, setHackathons] = useState<Hackathon[]>([])
const [loading, setLoading] = useState(true)
const [searchTerm, setSearchTerm] = useState('')
Expand Down Expand Up @@ -61,6 +63,14 @@ export default function CompanyHackathonsPage() {
}
}, [currentCompany, fetchHackathons])

if (companyLoading || isPendingInvitation) {
return (
<div className="flex items-center justify-center min-h-[60vh]">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
</div>
)
}

const filteredHackathons = hackathons.filter(hackathon =>
hackathon.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
hackathon.category?.toLowerCase().includes(searchTerm.toLowerCase())
Expand Down
4 changes: 3 additions & 1 deletion app/dashboard/company/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import React from 'react'
import { useCompanyContext } from '@/contexts/CompanyContext'
import { usePendingInvitationRedirect } from '@/lib/hooks/usePendingInvitationRedirect'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import Link from 'next/link'
Expand All @@ -20,8 +21,9 @@ import { useSubscription } from '@/hooks/useSubscription'
export default function CompanySlugDashboardPage() {
const { currentCompany, userRole, loading, error } = useCompanyContext()
const { usage } = useSubscription(currentCompany?.slug)
const isPendingInvitation = usePendingInvitationRedirect()

if (loading) {
if (loading || isPendingInvitation) {
return (
<div className="flex items-center justify-center min-h-[60vh]">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
Expand Down
10 changes: 10 additions & 0 deletions app/dashboard/company/[slug]/settings/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import React, { useState } from 'react'
import { useCompanyContext } from '@/contexts/CompanyContext'
import { usePendingInvitationRedirect } from '@/lib/hooks/usePendingInvitationRedirect'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
Expand Down Expand Up @@ -34,6 +35,7 @@ import { useSubscription } from '@/hooks/useSubscription'

export default function CompanySettingsPage() {
const { currentCompany, userRole, loading: contextLoading, refreshCompany } = useCompanyContext()
const isPendingInvitation = usePendingInvitationRedirect()
const { usage } = useSubscription(currentCompany?.slug)
const { toast } = useToast()
const [loading, setLoading] = useState(false)
Expand Down Expand Up @@ -103,6 +105,14 @@ export default function CompanySettingsPage() {
}
}, [currentCompany])

if (contextLoading || isPendingInvitation) {
return (
<div className="flex items-center justify-center min-h-[60vh]">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
</div>
)
}

const handleInputChange = (field: string, value: string) => {
setFormData((prev) => ({ ...prev, [field]: value }))
}
Expand Down
12 changes: 11 additions & 1 deletion app/dashboard/company/[slug]/subscription/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,23 @@ export default async function SubscriptionPage({ params }: PageProps) {
.select('role, status')
.eq('company_id', company.id)
.eq('user_id', user.id)
.eq('status', 'active')
.single()

// Redirect if no membership found
if (!membership) {
redirect('/dashboard/company')
}

// Redirect pending invitations to accept page
if (membership.status === 'pending') {
redirect(`/dashboard/company/${slug}/accept-invitation`)
}

// Only active members can access subscription
if (membership.status !== 'active') {
redirect('/dashboard/company')
}

// Only owners and admins can manage subscription
if (!['owner', 'admin'].includes(membership.role)) {
redirect(`/dashboard/company/${slug}`)
Expand Down
4 changes: 3 additions & 1 deletion app/dashboard/company/[slug]/team/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import React from 'react'
import { useParams } from 'next/navigation'
import { TeamManagement } from '@/components/dashboard/TeamManagement'
import { useCompanyContext } from '@/contexts/CompanyContext'
import { usePendingInvitationRedirect } from '@/lib/hooks/usePendingInvitationRedirect'
import { Skeleton } from '@/components/ui/skeleton'
import { Alert, AlertDescription } from '@/components/ui/alert'
import { AlertCircle } from 'lucide-react'
Expand All @@ -12,8 +13,9 @@ export default function TeamPage() {
const params = useParams()
const companySlug = params?.slug as string
const { currentCompany, userRole, loading, error } = useCompanyContext()
const isPendingInvitation = usePendingInvitationRedirect()

if (loading) {
if (loading || isPendingInvitation) {
return (
<div className="container mx-auto py-8 px-4 max-w-7xl">
<div className="space-y-6">
Expand Down
13 changes: 12 additions & 1 deletion app/dashboard/company/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ function CompanyDashboardContent({
}) {
const params = useParams()
const companySlug = params?.slug as string
const { currentCompany, loading, error } = useCompanyContext()
const { currentCompany, userCompanies, loading, error } = useCompanyContext()

// Show loading while company context is loading
if (loading) {
Expand All @@ -140,6 +140,17 @@ function CompanyDashboardContent({
return <div className="bg-black min-h-screen w-full">{children}</div>
}

// Check if user has pending invitation (not yet accepted)
const membership = userCompanies.find(
(uc) => uc.company.slug === companySlug
)
const isPendingInvitation = membership?.status === 'pending'

// If user has pending invitation, don't show sidebar (only show invitation page)
if (isPendingInvitation) {
return <div className="bg-black min-h-screen w-full">{children}</div>
}

// Generate sidebar items with dynamic company slug
const sidebarItems: SidebarGroupType[] = [
{
Expand Down
18 changes: 9 additions & 9 deletions components/dashboard/TeamManagement.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,8 @@ export function TeamManagement({
const [removeDialogOpen, setRemoveDialogOpen] = useState(false)
const [selectedMember, setSelectedMember] = useState<CompanyMember | null>(null)
const [inviteEmail, setInviteEmail] = useState('')
const [inviteRole, setInviteRole] = useState<'admin' | 'editor' | 'member'>('member')
const [newRole, setNewRole] = useState<'owner' | 'admin' | 'editor' | 'member'>('member')
const [inviteRole, setInviteRole] = useState<'admin' | 'editor' | 'viewer'>('viewer')
const [newRole, setNewRole] = useState<'owner' | 'admin' | 'editor' | 'viewer'>('viewer')
const [submitting, setSubmitting] = useState(false)
const { toast } = useToast()

Expand Down Expand Up @@ -160,7 +160,7 @@ export function TeamManagement({

setInviteDialogOpen(false)
setInviteEmail('')
setInviteRole('member')
setInviteRole('viewer')
fetchMembers()
} catch (error) {
console.error('Error inviting member:', error)
Expand Down Expand Up @@ -257,7 +257,7 @@ export function TeamManagement({
// Open role dialog
const openRoleDialog = (member: CompanyMember) => {
setSelectedMember(member)
setNewRole(member.role as 'owner' | 'admin' | 'editor' | 'member')
setNewRole(member.role as 'owner' | 'admin' | 'editor' | 'viewer')
setRoleDialogOpen(true)
}

Expand Down Expand Up @@ -291,11 +291,11 @@ export function TeamManagement({
Active
</Badge>
)
case 'invited':
case 'pending':
return (
<Badge variant="outline" className="bg-yellow-500/10 text-yellow-500 border-yellow-500/20">
<Clock className="w-3 h-3 mr-1" />
Invited
Pending
</Badge>
)
case 'suspended':
Expand Down Expand Up @@ -494,13 +494,13 @@ export function TeamManagement({
<SelectValue />
</SelectTrigger>
<SelectContent className="bg-zinc-900 border-zinc-800">
<SelectItem value="member">Member - View only</SelectItem>
<SelectItem value="viewer">Viewer - View only</SelectItem>
<SelectItem value="editor">Editor - Create drafts</SelectItem>
<SelectItem value="admin">Admin - Full event management</SelectItem>
</SelectContent>
</Select>
<p className="text-xs text-muted-foreground">
{inviteRole === 'member' && 'Can view company events and analytics'}
{inviteRole === 'viewer' && 'Can view company events and analytics'}
{inviteRole === 'editor' && 'Can create draft events'}
{inviteRole === 'admin' && 'Can create, edit, and manage events'}
</p>
Expand Down Expand Up @@ -543,7 +543,7 @@ export function TeamManagement({
<SelectItem value="owner">Owner - Full control</SelectItem>
<SelectItem value="admin">Admin - Full event management</SelectItem>
<SelectItem value="editor">Editor - Create drafts</SelectItem>
<SelectItem value="member">Member - View only</SelectItem>
<SelectItem value="viewer">Viewer - View only</SelectItem>
</SelectContent>
</Select>
</div>
Expand Down
2 changes: 1 addition & 1 deletion components/help/CompanyFAQ.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ const faqData: FAQItem[] = [
{
category: 'Team Management',
question: 'Can team members see each other\'s activity?',
answer: 'Owners and Admins can see all team activity. Editors and Members see limited activity relevant to their role.',
answer: 'Owners and Admins can see all team activity. Editors and Viewers see limited activity relevant to their role.',
},
{
category: 'Team Management',
Expand Down
2 changes: 1 addition & 1 deletion lib/email/templates/team-invitation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ export const getTeamInvitationEmail = (params: TeamInvitationParams) => {
`

return {
subject: `You've been invited to join ${params.companyName} on CodeUnia`,
subject: `You've been invited to join ${params.companyName} on Codeunia`,
html: getEmailTemplate(content),
}
}
30 changes: 30 additions & 0 deletions lib/hooks/usePendingInvitationRedirect.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { useEffect } from 'react'
import { useRouter } from 'next/navigation'
import { useCompanyContext } from '@/contexts/CompanyContext'

/**
* Hook to redirect users with pending invitations to the accept-invitation page
* This prevents unauthorized access to company pages before accepting the invitation
*/
export function usePendingInvitationRedirect() {
const router = useRouter()
const { currentCompany, userCompanies, loading } = useCompanyContext()

useEffect(() => {
if (loading || !currentCompany) return

const membership = userCompanies.find(
(uc) => uc.company.slug === currentCompany.slug
)

if (membership?.status === 'pending') {
router.push(`/dashboard/company/${currentCompany.slug}/accept-invitation`)
}
}, [currentCompany, userCompanies, loading, router])

// Return whether the user has a pending invitation
const membership = userCompanies.find(
(uc) => uc.company?.slug === currentCompany?.slug
)
return membership?.status === 'pending'
}
Loading
Loading