diff --git a/app/(landing)/hackathons/[slug]/components/tabs/contents/FindTeam.tsx b/app/(landing)/hackathons/[slug]/components/tabs/contents/FindTeam.tsx index a31a6989d..b152c38bc 100644 --- a/app/(landing)/hackathons/[slug]/components/tabs/contents/FindTeam.tsx +++ b/app/(landing)/hackathons/[slug]/components/tabs/contents/FindTeam.tsx @@ -88,7 +88,9 @@ const FindTeam = () => { !!hackathon?.id && hackathon.participantType !== 'INDIVIDUAL' ); - const teams = teamsResponse?.data?.teams || []; + const teams = (teamsResponse?.data?.teams || []).filter( + t => !myTeam || t.id !== myTeam.id + ); const handleJoin = (team: Team) => { setSelectedTeam(team); @@ -99,14 +101,6 @@ const FindTeam = () => { const isIndividualOnly = hackathon.participantType === 'INDIVIDUAL'; - if (myTeam) { - return ( - - - - ); - } - return ( { ) : ( <> + {myTeam && ( +
+ +
+
+ )} +
-

Open Teams

+

+ {myTeam ? 'Other Teams' : 'Open Teams'} +

Find builders to collaborate with on your project.

diff --git a/app/(landing)/hackathons/[slug]/components/tabs/contents/submissions/SubmissionCard.tsx b/app/(landing)/hackathons/[slug]/components/tabs/contents/submissions/SubmissionCard.tsx index c1a65ecf0..26d00747b 100644 --- a/app/(landing)/hackathons/[slug]/components/tabs/contents/submissions/SubmissionCard.tsx +++ b/app/(landing)/hackathons/[slug]/components/tabs/contents/submissions/SubmissionCard.tsx @@ -94,7 +94,7 @@ const SubmissionCard = ({ submission }: SubmissionCardProps) => { {`${projectName} ) : logo ? ( // eslint-disable-next-line @next/next/no-img-element diff --git a/app/(landing)/hackathons/[slug]/components/tabs/contents/teams/MyTeamView.tsx b/app/(landing)/hackathons/[slug]/components/tabs/contents/teams/MyTeamView.tsx index b698bd0df..7aaea60b4 100644 --- a/app/(landing)/hackathons/[slug]/components/tabs/contents/teams/MyTeamView.tsx +++ b/app/(landing)/hackathons/[slug]/components/tabs/contents/teams/MyTeamView.tsx @@ -186,7 +186,14 @@ const MyTeamView = ({ team, hackathonSlug }: MyTeamViewProps) => {

- {team.teamName} + + {team.teamName} +

diff --git a/app/(landing)/hackathons/[slug]/components/tabs/contents/teams/TeamCard.tsx b/app/(landing)/hackathons/[slug]/components/tabs/contents/teams/TeamCard.tsx index 9737000b8..603212d4e 100644 --- a/app/(landing)/hackathons/[slug]/components/tabs/contents/teams/TeamCard.tsx +++ b/app/(landing)/hackathons/[slug]/components/tabs/contents/teams/TeamCard.tsx @@ -1,6 +1,7 @@ 'use client'; -import React from 'react'; +import React, { useCallback } from 'react'; +import { useParams } from 'next/navigation'; import { cn } from '@/lib/utils'; import { Team, TeamMember } from '@/lib/api/hackathons/teams'; import GroupAvatar from '@/components/avatars/GroupAvatar'; @@ -14,6 +15,16 @@ interface TeamCardProps { } const TeamCard = ({ team, onJoin, onMessageLeader }: TeamCardProps) => { + const { slug } = useParams<{ slug: string }>(); + + const openTeamDetails = useCallback(() => { + if (!slug || !team.id) return; + window.open( + `/hackathons/${slug}/teams/${team.id}`, + '_blank', + 'noopener,noreferrer' + ); + }, [slug, team.id]); const { teamName, description, @@ -32,7 +43,18 @@ const TeamCard = ({ team, onJoin, onMessageLeader }: TeamCardProps) => { }; return ( -
+
{ + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + openTeamDetails(); + } + }} + className='group hover:border-primary/20 flex h-full cursor-pointer flex-col overflow-hidden rounded-2xl border border-white/5 bg-[#0A0B0D] p-8 transition-all hover:bg-[#0D0F12]' + >
@@ -70,7 +92,10 @@ const TeamCard = ({ team, onJoin, onMessageLeader }: TeamCardProps) => { variant='outline' size='sm' className='h-11 shrink-0 rounded-xl border-white/10 bg-white/5 px-4 text-sm font-bold text-white transition-all hover:bg-white/10' - onClick={e => onMessageLeader(team, e.currentTarget)} + onClick={e => { + e.stopPropagation(); + onMessageLeader(team, e.currentTarget); + }} aria-label='Message team leader' > @@ -81,7 +106,10 @@ const TeamCard = ({ team, onJoin, onMessageLeader }: TeamCardProps) => { variant='outline' size='sm' className='border-primary/20 text-primary hover:bg-primary h-11 w-full rounded-xl bg-[#232B20]/30 px-6 text-sm font-bold transition-all hover:text-black sm:w-auto' - onClick={() => onJoin?.(team)} + onClick={e => { + e.stopPropagation(); + onJoin?.(team); + }} disabled={memberCount >= maxSize} > Join Team diff --git a/app/(landing)/hackathons/[slug]/components/tabs/index.tsx b/app/(landing)/hackathons/[slug]/components/tabs/index.tsx index 051605d14..1678d4e49 100644 --- a/app/(landing)/hackathons/[slug]/components/tabs/index.tsx +++ b/app/(landing)/hackathons/[slug]/components/tabs/index.tsx @@ -14,7 +14,10 @@ import FindTeam from './contents/FindTeam'; import SponsorsTab from './contents/SponsorsTab'; import { useEffect, useState, useMemo } from 'react'; import { useHackathonData } from '@/lib/providers/hackathonProvider'; -import { useHackathonAnnouncements } from '@/hooks/hackathon/use-hackathon-queries'; +import { + useHackathonAnnouncements, + useHackathonTeams, +} from '@/hooks/hackathon/use-hackathon-queries'; import { useCommentSystem } from '@/hooks/use-comment-system'; import { CommentEntityType } from '@/types/comment'; import { Megaphone } from 'lucide-react'; @@ -46,6 +49,17 @@ const HackathonTabs = ({ sidebar }: HackathonTabsProps) => { enabled: !!currentHackathon?.id, }); + const participantType = currentHackathon?.participantType; + const isTeamHackathon = + participantType === 'TEAM' || participantType === 'TEAM_OR_INDIVIDUAL'; + + const { data: teamsCountResponse } = useHackathonTeams( + slug, + { page: 1, limit: 1 }, + !!slug && isTeamHackathon + ); + const teamsTotal = teamsCountResponse?.data?.pagination?.total ?? 0; + const [activeTab, setActiveTab] = useState('overview'); const hackathonTabs = useMemo(() => { @@ -55,10 +69,6 @@ const HackathonTabs = ({ sidebar }: HackathonTabsProps) => { const hasWinners = !!(winners && winners.length > 0); const hasAnnouncements = announcements.length > 0; - const participantType = currentHackathon.participantType; - const isTeamHackathon = - participantType === 'TEAM' || participantType === 'TEAM_OR_INDIVIDUAL'; - const tabs = [ { id: 'overview', label: 'Overview' }, ...(hasParticipants @@ -105,6 +115,7 @@ const HackathonTabs = ({ sidebar }: HackathonTabsProps) => { tabs.push({ id: 'team-formation', label: 'Find Team', + badge: teamsTotal, }); } @@ -159,6 +170,8 @@ const HackathonTabs = ({ sidebar }: HackathonTabsProps) => { announcements, announcementsLoading, generalLoading, + isTeamHackathon, + teamsTotal, ]); useEffect(() => { diff --git a/app/(landing)/hackathons/[slug]/teams/[teamId]/page.tsx b/app/(landing)/hackathons/[slug]/teams/[teamId]/page.tsx new file mode 100644 index 000000000..d5ee5b4f2 --- /dev/null +++ b/app/(landing)/hackathons/[slug]/teams/[teamId]/page.tsx @@ -0,0 +1,253 @@ +'use client'; + +import { use } from 'react'; +import Link from 'next/link'; +import { useHackathon, useTeam } from '@/hooks/hackathon/use-hackathon-queries'; +import { Button } from '@/components/ui/button'; +import BasicAvatar from '@/components/avatars/BasicAvatar'; +import { + ArrowLeft, + Calendar, + Crown, + Loader2, + Mail, + Users, + XCircle, +} from 'lucide-react'; + +export default function TeamDetailsPage({ + params, +}: { + params: Promise<{ slug: string; teamId: string }>; +}) { + const { slug, teamId } = use(params); + + const { data: hackathon } = useHackathon(slug); + const { data: team, isLoading, isError } = useTeam(slug, teamId); + + const formattedCreatedAt = team?.createdAt + ? new Date(team.createdAt).toLocaleDateString('en-US', { + day: '2-digit', + month: 'short', + year: 'numeric', + }) + : ''; + + const status = team?.isOpen ? 'OPEN' : 'CLOSED'; + + return ( +
+
+ + + + + {isLoading ? ( +
+ +

+ Loading team... +

+
+ ) : isError || !team ? ( +
+ +
+

Team not found

+

+ This team may have been disbanded or never existed. +

+
+
+ ) : ( +
+
+
+
+
+ {team.teamName.charAt(0).toUpperCase()} +
+
+

+ {team.teamName} +

+
+ + {status} + + + + {team.memberCount}/{team.maxSize} builders + + {formattedCreatedAt && ( + + + Created {formattedCreatedAt} + + )} +
+
+
+
+ + {team.description && ( +

+ {team.description} +

+ )} +
+ +
+
+
+

+ Members +

+
+ {team.members && team.members.length > 0 ? ( + team.members.map(member => { + const isLeader = member.userId === team.leader?.id; + const profileHref = member.username + ? `/profile/${member.username}` + : null; + const content = ( + <> +
+ + {isLeader && ( + + + Leader + + )} +
+ {!isLeader && ( + + {member.role} + + )} + + ); + return profileHref ? ( + + {content} + + ) : ( +
+ {content} +
+ ); + }) + ) : ( +

+ No members listed. +

+ )} +
+
+ + {team.lookingFor && team.lookingFor.length > 0 && ( +
+

+ Roles Needed +

+
+ {team.lookingFor.map((role, idx) => ( +
+ {role.role} + {role.skills && role.skills.length > 0 && ( + + {role.skills.join(', ')} + + )} +
+ ))} +
+
+ )} +
+ +
+ {team.leader && ( +
+

+ Team Leader +

+ {team.leader.username ? ( + + + + ) : ( + + )} +
+ )} + + {team.contactInfo && ( +
+

+ Contact +

+
+ + {team.contactInfo} +
+ {team.contactMethod && ( +

+ Via {team.contactMethod} +

+ )} +
+ )} +
+
+
+ )} +
+
+ ); +} diff --git a/app/(landing)/organizations/[id]/hackathons/[hackathonId]/participants/page.tsx b/app/(landing)/organizations/[id]/hackathons/[hackathonId]/participants/page.tsx index 3423f4af0..5a2d9f71c 100644 --- a/app/(landing)/organizations/[id]/hackathons/[hackathonId]/participants/page.tsx +++ b/app/(landing)/organizations/[id]/hackathons/[hackathonId]/participants/page.tsx @@ -128,6 +128,10 @@ const ParticipantsPage: React.FC = () => { const [statistics, setStatistics] = useState<{ participantsCount: number; submissionsCount: number; + soloParticipants: number; + totalTeams: number; + soloSubmissions: number; + teamSubmissions: number; } | null>(null); const [statisticsLoading, setStatisticsLoading] = useState(false); @@ -143,10 +147,18 @@ const ParticipantsPage: React.FC = () => { const stats = response.data as { totalSubmissions?: number; activeParticipants?: number; + soloParticipants?: number; + totalTeams?: number; + soloSubmissions?: number; + teamSubmissions?: number; }; setStatistics({ participantsCount: stats.activeParticipants ?? 0, submissionsCount: stats.totalSubmissions ?? 0, + soloParticipants: stats.soloParticipants ?? 0, + totalTeams: stats.totalTeams ?? 0, + soloSubmissions: stats.soloSubmissions ?? 0, + teamSubmissions: stats.teamSubmissions ?? 0, }); } catch (err) { reportError(err, { @@ -277,7 +289,7 @@ const ParticipantsPage: React.FC = () => { }>
-
+
{ } showTrend={true} /> + + (null); const [hackathon, setHackathon] = useState(null); + const [submissionStats, setSubmissionStats] = useState<{ + soloSubmissions: number; + teamSubmissions: number; + totalSubmissions: number; + } | null>(null); + const [submissionStatsLoading, setSubmissionStatsLoading] = useState(false); useEffect(() => { if (hackathonId) { @@ -58,6 +66,35 @@ export default function SubmissionsPage() { } }, [hackathonId]); + useEffect(() => { + if (!organizationId || !hackathonId) return; + const loadStats = async () => { + setSubmissionStatsLoading(true); + try { + const res = await getHackathonStatistics(organizationId, hackathonId); + const stats = res.data as { + totalSubmissions?: number; + soloSubmissions?: number; + teamSubmissions?: number; + }; + setSubmissionStats({ + totalSubmissions: stats.totalSubmissions ?? 0, + soloSubmissions: stats.soloSubmissions ?? 0, + teamSubmissions: stats.teamSubmissions ?? 0, + }); + } catch (err) { + reportError(err, { + context: 'org-submissions-fetchStats', + organizationId, + hackathonId, + }); + } finally { + setSubmissionStatsLoading(false); + } + }; + loadStats(); + }, [organizationId, hackathonId]); + useEffect(() => { const fetchSession = async () => { try { @@ -142,6 +179,36 @@ export default function SubmissionsPage() {
) : (
+
+ + + +
+ ([]); + const [hackathonSlug, setHackathonSlug] = useState(''); + const [loading, setLoading] = useState(false); + const [search, setSearch] = useState(''); + const [statusFilter, setStatusFilter] = useState('all'); + const [submissionFilter, setSubmissionFilter] = + useState('all'); + const [page, setPage] = useState(1); + const [pageSize, setPageSize] = useState(DEFAULT_PAGE_SIZE); + const [totalPages, setTotalPages] = useState(0); + const [totalTeams, setTotalTeams] = useState(0); + + useEffect(() => { + if (!hackathonId) return; + let cancelled = false; + const load = async () => { + setLoading(true); + try { + const limit = Math.min(pageSize, MAX_PAGE_SIZE); + const teamsRes = await getTeams(hackathonId, { + page, + limit, + search: search.trim() || undefined, + openOnly: false, + }); + if (cancelled) return; + setTeams(teamsRes?.data?.teams ?? []); + setTotalPages(teamsRes?.data?.pagination?.totalPages ?? 0); + setTotalTeams(teamsRes?.data?.pagination?.total ?? 0); + } catch (err) { + reportError(err, { + context: 'organizer-teams-load', + organizationId, + hackathonId, + }); + } finally { + if (!cancelled) setLoading(false); + } + }; + load(); + return () => { + cancelled = true; + }; + }, [hackathonId, organizationId, page, pageSize, search]); + + useEffect(() => { + if (!hackathonId) return; + let cancelled = false; + const loadHackathon = async () => { + try { + const hackRes = await getHackathon(hackathonId); + if (!cancelled) { + setHackathonSlug(hackRes?.data?.slug ?? hackathonId); + } + } catch (err) { + reportError(err, { + context: 'organizer-teams-load-hackathon', + organizationId, + hackathonId, + }); + if (!cancelled) setHackathonSlug(hackathonId); + } + }; + loadHackathon(); + return () => { + cancelled = true; + }; + }, [hackathonId, organizationId]); + + const filteredTeams = useMemo(() => { + return teams.filter(team => { + if (statusFilter === 'open' && !team.isOpen) return false; + if (statusFilter === 'closed' && team.isOpen) return false; + if (submissionFilter === 'submitted' && !team.hasSubmission) return false; + if (submissionFilter === 'not_submitted' && team.hasSubmission) + return false; + return true; + }); + }, [teams, statusFilter, submissionFilter]); + + const table = useReactTable({ + data: teams, + columns: [], + getCoreRowModel: getCoreRowModel(), + manualPagination: true, + pageCount: totalPages, + state: { + pagination: { + pageIndex: page - 1, + pageSize, + }, + }, + onPaginationChange: updater => { + if (typeof updater === 'function') { + const next = updater({ pageIndex: page - 1, pageSize }); + if (next.pageSize !== pageSize) { + setPageSize(Math.min(next.pageSize, MAX_PAGE_SIZE)); + setPage(1); + } else { + setPage(next.pageIndex + 1); + } + } + }, + }); + + const stats = useMemo(() => { + const submittedOnPage = teams.filter(t => t.hasSubmission).length; + const openOnPage = teams.filter(t => t.isOpen).length; + return { + total: totalTeams, + open: openOnPage, + submitted: submittedOnPage, + notSubmitted: teams.length - submittedOnPage, + }; + }, [teams, totalTeams]); + + return ( + }> +
+
+
+

+ Teams +

+

+ All teams formed for this hackathon and whether they have + submitted. +

+
+
+ +
+
+ + + + +
+ +
+
+ + { + setSearch(e.target.value); + setPage(1); + }} + className='focus:border-primary focus:ring-primary border-gray-700/50 bg-gray-900/50 pl-10 text-white placeholder:text-gray-500' + /> +
+
+ + +
+
+ + {loading ? ( +
+
+ +

Loading teams...

+
+
+ ) : filteredTeams.length === 0 ? ( +
+ +

+ {teams.length === 0 + ? 'No teams yet for this hackathon.' + : 'No teams match the current filters.'} +

+
+ ) : ( +
+
+
Team
+
Leader
+
Members
+
Submission
+
Status
+
+
    + {filteredTeams.map(team => { + const href = `/hackathons/${hackathonSlug}/teams/${team.id}`; + return ( +
  • + +
    +
    +
    + {team.teamName?.charAt(0).toUpperCase()} +
    +
    +

    + {team.teamName} +

    +

    + Created{' '} + {new Date(team.createdAt).toLocaleDateString()} +

    +
    +
    +
    +
    + {team.leader ? ( +
    + + +
    + ) : ( + + No leader + + )} +
    +
    + + + {team.memberCount}/{team.maxSize} + +
    +
    + {team.hasSubmission ? ( + + + Submitted + + ) : ( + + + Pending + + )} +
    +
    + + {team.isOpen ? 'Open' : 'Closed'} + +
    + +
  • + ); + })} +
+
+ )} + +
+ +
+
+
+
+ ); +} diff --git a/app/me/hackathons/submissions/submission-components.tsx b/app/me/hackathons/submissions/submission-components.tsx index 378c5b661..c55dab34e 100644 --- a/app/me/hackathons/submissions/submission-components.tsx +++ b/app/me/hackathons/submissions/submission-components.tsx @@ -227,7 +227,7 @@ export function SubmissionsSheetContent({ src={submission.hackathon?.banner || submission.logo!} alt={submission.projectName} fill - className='object-cover opacity-80' + className='object-contain opacity-80' /> {submission.logo && submission.hackathon?.banner && (
@@ -236,7 +236,7 @@ export function SubmissionsSheetContent({ src={submission.logo} alt='Project logo' fill - className='object-cover' + className='object-contain' />
@@ -450,7 +450,7 @@ export function TableRow({ src={submission.logo} alt={submission.projectName} fill - className='object-cover' + className='object-contain' />
) : ( diff --git a/components/organization/hackathons/details/HackathonSidebar.tsx b/components/organization/hackathons/details/HackathonSidebar.tsx index 42da982fb..ec66f48a7 100644 --- a/components/organization/hackathons/details/HackathonSidebar.tsx +++ b/components/organization/hackathons/details/HackathonSidebar.tsx @@ -3,6 +3,7 @@ import { Settings, LayoutDashboard, Users, + UsersRound, BarChartBig, Megaphone, FileText, @@ -321,6 +322,16 @@ export default function HackathonSidebar({ description: 'View all submissions', disabled: hackathonId?.startsWith('draft-'), }, + { + icon: UsersRound, + label: 'Teams', + href: + basePath !== '#' && !hackathonId?.startsWith('draft-') + ? `${basePath}/teams` + : '#', + description: 'View all teams', + disabled: hackathonId?.startsWith('draft-'), + }, { icon: BarChartBig, label: 'Judging', diff --git a/components/organization/hackathons/submissions/SubmissionsList.tsx b/components/organization/hackathons/submissions/SubmissionsList.tsx index c1f414858..30029d9a1 100644 --- a/components/organization/hackathons/submissions/SubmissionsList.tsx +++ b/components/organization/hackathons/submissions/SubmissionsList.tsx @@ -16,6 +16,7 @@ import { reportError } from '@/lib/error-reporting'; import { Card, CardContent } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; +import { BoundlessButton } from '@/components/buttons/BoundlessButton'; import { Checkbox } from '@/components/ui/checkbox'; import { Input } from '@/components/ui/input'; import { @@ -157,10 +158,6 @@ export function SubmissionsList({ router.push(`/projects/${submissionId}?type=submission`); }; - const isBeforeDeadline = hackathon?.submissionDeadline - ? new Date() < new Date(hackathon.submissionDeadline) - : false; - if (submissions.length === 0 && !loading) { return (
@@ -287,49 +284,45 @@ export function SubmissionsList({ onClick={e => e.stopPropagation()} > {subData.status === 'SUBMITTED' ? ( - + : 'Approve'} + ) : ( - + )} {onDisqualify && ( - + )}
)} @@ -506,18 +499,13 @@ export function SubmissionsList({ onClick={e => handleReview(e, subData.id, 'SHORTLISTED') } - disabled={ - reviewingId === subData.id || - isBeforeDeadline - } - className={`cursor-pointer text-green-500 focus:bg-green-900/20 focus:text-green-400 ${isBeforeDeadline ? 'cursor-not-allowed opacity-50' : ''}`} + disabled={reviewingId === subData.id} + className='text-primary focus:bg-primary/10 focus:text-primary cursor-pointer' > {reviewingId === subData.id ? 'Approving...' - : isBeforeDeadline - ? 'Before Deadline' - : 'Approve'} + : 'Approve'} ) : (
- - - +
) : ( diff --git a/components/project-details/project-sidebar/ProjectSidebarHeader.tsx b/components/project-details/project-sidebar/ProjectSidebarHeader.tsx index 8ff1b2f7f..070386341 100644 --- a/components/project-details/project-sidebar/ProjectSidebarHeader.tsx +++ b/components/project-details/project-sidebar/ProjectSidebarHeader.tsx @@ -42,7 +42,7 @@ export function ProjectSidebarHeader({ alt={project.title} width={80} height={80} - className='relative h-20 w-20 rounded-xl object-cover ring-2 ring-gray-800/50' + className='relative h-20 w-20 rounded-xl object-contain ring-2 ring-gray-800/50' />
diff --git a/hooks/hackathon/use-hackathon-queries.ts b/hooks/hackathon/use-hackathon-queries.ts index a588d4daa..af8bad539 100644 --- a/hooks/hackathon/use-hackathon-queries.ts +++ b/hooks/hackathon/use-hackathon-queries.ts @@ -91,6 +91,8 @@ export const hackathonKeys = { // a 4-element key that does NOT prefix-match cached ['…',slug,{...params}]. teamsBase: (idOrSlug: string) => ['hackathon', 'teams', idOrSlug] as const, myTeam: (idOrSlug: string) => ['hackathon', 'myTeam', idOrSlug] as const, + team: (idOrSlug: string, teamId: string) => + ['hackathon', 'team', idOrSlug, teamId] as const, myInvitations: (idOrSlug: string, status?: string) => ['hackathon', 'myInvitations', idOrSlug, status] as const, teamInvitations: (idOrSlug: string, teamId: string, status?: string) => @@ -301,6 +303,20 @@ export function useHackathonTeams( // Legacy alias export const useTeamPosts = useHackathonTeams; +/** + * Fetch a single team by id. + */ +export function useTeam(idOrSlug: string, teamId: string, enabled = true) { + return useQuery({ + queryKey: hackathonKeys.team(idOrSlug, teamId), + queryFn: async () => { + const response = await getTeamDetails(idOrSlug, teamId); + return response.data; + }, + enabled: !!idOrSlug && !!teamId && enabled, + }); +} + /** * Fetch current user's team. */ diff --git a/lib/api/hackathons/teams.ts b/lib/api/hackathons/teams.ts index 083dfa5b4..a031c2b88 100644 --- a/lib/api/hackathons/teams.ts +++ b/lib/api/hackathons/teams.ts @@ -50,6 +50,7 @@ export interface Team { organizationId?: string; views?: number; contactCount?: number; + hasSubmission?: boolean; createdAt: string; updatedAt: string; posts?: Team[];