diff --git a/app/(landing)/organizations/[id]/hackathons/[hackathonId]/submissions/page.tsx b/app/(landing)/organizations/[id]/hackathons/[hackathonId]/submissions/page.tsx new file mode 100644 index 00000000..9846a9de --- /dev/null +++ b/app/(landing)/organizations/[id]/hackathons/[hackathonId]/submissions/page.tsx @@ -0,0 +1,90 @@ +'use client'; + +import { useParams } from 'next/navigation'; +import { useEffect } from 'react'; +import { Loader2, AlertCircle } from 'lucide-react'; +import { useHackathonSubmissions } from '@/hooks/hackathon/use-hackathon-submissions'; +import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; +import { AuthGuard } from '@/components/auth'; +import Loading from '@/components/Loading'; +import { SubmissionsManagement } from '@/components/organization/hackathons/submissions/SubmissionsManagement'; + +export default function SubmissionsPage() { + const params = useParams(); + const hackathonId = params.hackathonId as string; + + const { + submissions, + pagination, + filters, + loading, + error, + fetchSubmissions, + updateFilters, + goToPage, + refresh, + } = useHackathonSubmissions(hackathonId); + + useEffect(() => { + if (hackathonId) { + fetchSubmissions(); + } + }, [hackathonId, fetchSubmissions]); + + if (error) { + return ( +
+ + + Unable to load submissions + + {error} + + +
+ ); + } + + return ( + }> +
+ {/* Header */} +
+
+

+ Submissions +

+

+ View and manage all hackathon submissions +

+
+
+ + {/* Main Content */} +
+ {loading && submissions.length === 0 ? ( +
+
+ +

Loading submissions...

+
+
+ ) : ( + + )} +
+
+
+ ); +} diff --git a/app/(landing)/projects/[id]/page.tsx b/app/(landing)/projects/[id]/page.tsx index 3413f50e..1052d5d1 100644 --- a/app/(landing)/projects/[id]/page.tsx +++ b/app/(landing)/projects/[id]/page.tsx @@ -5,6 +5,14 @@ import { notFound } from 'next/navigation'; import { getCrowdfundingProject } from '@/lib/api/project'; import type { Crowdfunding } from '@/types/project'; import { useEffect, useState } from 'react'; +import { useSearchParams } from 'next/navigation'; +import { + getSubmissionDetails, + getHackathon, + type ParticipantSubmission, +} from '@/lib/api/hackathons'; +import type { Hackathon } from '@/lib/api/hackathons'; +import type { Milestone, TeamMember, SocialLink } from '@/types/project'; interface ProjectPageProps { params: Promise<{ @@ -12,18 +20,81 @@ interface ProjectPageProps { }>; } -function ProjectContent({ id }: { id: string }) { +function ProjectContent({ + id, + isSubmission = false, +}: { + id: string; + isSubmission?: boolean; +}) { const [project, setProject] = useState(null); const [error, setError] = useState(null); const [loading, setLoading] = useState(true); useEffect(() => { + const fetchSubmission = async (submissionId: string) => { + try { + const submissionRes = await getSubmissionDetails(submissionId); + + if (submissionRes && submissionRes.data) { + const submission = submissionRes.data; + const subData = submission as any; + + // Try to fetch hackathon details using getHackathon (by ID) + let hackathon: Hackathon | null = null; + try { + if (subData.hackathonId) { + const hackathonRes = await getHackathon(subData.hackathonId); + hackathon = hackathonRes.data; + } + } catch (err) { + console.error('Failed to fetch hackathon details', err); + } + + if (hackathon) { + const mappedProject = mapSubmissionToCrowdfunding( + submission, + hackathon + ); + setProject(mappedProject); + return; + } else { + // If hackathon details are missing, we might want to handle it gracefully + // or throw. For now, let's strictly require hackathon details for the map + throw new Error('Hackathon details not found'); + } + } + throw new Error('Submission not found'); + } catch (e) { + console.error('Failed to fetch submission:', e); + throw e; + } + }; + const fetchProjectData = async () => { try { setLoading(true); setError(null); - const projectData = await getCrowdfundingProject(id); - setProject(projectData); + + // If query param specifies submission, try that first/only + if (isSubmission) { + await fetchSubmission(id); + return; + } + + // Try fetching as crowdfunding project first + try { + const projectData = await getCrowdfundingProject(id); + if (projectData) { + setProject(projectData); + return; + } + } catch (e) { + // Ignore error and try fetching as submission + console.log('Not a crowdfunding project, checking submission...', e); + // Fallback to submission check + await fetchSubmission(id); + } } catch { setError('Failed to fetch project data'); } finally { @@ -32,7 +103,7 @@ function ProjectContent({ id }: { id: string }) { }; fetchProjectData(); - }, [id]); + }, [id, isSubmission]); if (loading) { return ; @@ -42,17 +113,245 @@ function ProjectContent({ id }: { id: string }) { notFound(); } + if (error || !project) { + notFound(); + } + return (
- +
); } +// Helper function to map Submission to Crowdfunding type +function mapSubmissionToCrowdfunding( + submission: ParticipantSubmission & { members?: any[] }, + hackathon: Hackathon +): Crowdfunding { + const subData = submission as any; + const hackData = hackathon as any; + + const now = new Date(); + + // Helper to determine status + const getStatus = ( + start?: string, + end?: string + ): 'completed' | 'pending' | 'active' => { + if (!start || !end) return 'pending'; + const startDate = new Date(start); + const endDate = new Date(end); + if (now > endDate) return 'completed'; + if (now >= startDate && now <= endDate) return 'active'; + return 'pending'; + }; + + // Map Hackathon Timeline to Milestones + const milestones: Milestone[] = [ + { + id: 'registration', + name: 'Registration', + description: 'Registration period for the hackathon', + amount: 0, + status: getStatus( + hackData.timeline?.registrationStart, + hackData.timeline?.registrationEnd + ), + startDate: + hackData.timeline?.registrationStart || new Date().toISOString(), + endDate: hackData.timeline?.registrationEnd || new Date().toISOString(), + }, + { + id: 'submission', + name: 'Submission', + description: 'Project submission period', + amount: 0, + status: getStatus( + hackData.timeline?.submissionStart, + hackData.timeline?.submissionEnd + ), + startDate: hackData.timeline?.submissionStart || new Date().toISOString(), + endDate: hackData.timeline?.submissionEnd || new Date().toISOString(), + }, + { + id: 'judging', + name: 'Judging', + description: 'Judging period', + amount: 0, + status: getStatus( + hackData.timeline?.judgingStart, + hackData.timeline?.judgingEnd + ), + startDate: hackData.timeline?.judgingStart || new Date().toISOString(), + endDate: hackData.timeline?.judgingEnd || new Date().toISOString(), + }, + { + id: 'winners', + name: 'Winners Announced', + description: 'Announcement of hackathon winners', + amount: 0, + status: getStatus( + hackData.timeline?.winnersAnnounced, + hackData.timeline?.winnersAnnounced + ), + startDate: + hackData.timeline?.winnersAnnounced || new Date().toISOString(), + endDate: hackData.timeline?.winnersAnnounced || new Date().toISOString(), + }, + ]; + + // Map Social Links + const socialLinks: SocialLink[] = (subData.links || []).map((link: any) => ({ + platform: link.type || 'website', + url: link.url, + })); + + // Map Team Members + const teamMembers: TeamMember[] = ( + subData.teamMembers || + subData.members || + [] + ).map((m: any) => ({ + name: m.user?.name || m.name || 'Team Member', + role: m.role || 'Member', + email: '', + image: m.user?.image || m.image, + username: m.user?.username || m.username, + })); + + // Also add the submitter if not in team + if (teamMembers.length === 0 && (subData.participantId || subData.userId)) { + // We might lack detailed user info here, so we use placeholders or available data + teamMembers.push({ + name: subData.participant?.name || subData.user?.name || 'Submitter', + role: 'Leader', + email: subData.participant?.email || subData.user?.email || '', + image: + subData.participant?.image || + subData.user?.image || + subData.logo || + undefined, + username: + subData.participant?.username || subData.user?.username || 'submitter', + }); + } + + const projectId = subData.id || subData._id || ''; + + // Find demo video in links if not provided directly + let demoVideoUrl = subData.videoUrl || ''; + if (!demoVideoUrl && socialLinks.length > 0) { + const vidLink = socialLinks.find( + l => + l.url.includes('youtube.com') || + l.url.includes('youtu.be') || + l.url.includes('vimeo') + ); + if (vidLink) { + demoVideoUrl = vidLink.url; + } + } + + return { + id: projectId, + projectId: projectId, + fundingGoal: 0, + fundingRaised: 0, + fundingCurrency: 'USD', + fundingEndDate: + hackData.timeline?.submissionEnd || new Date().toISOString(), + contributors: [], + team: teamMembers, + contact: { primary: '', backup: '' }, + socialLinks: socialLinks, + milestones: milestones, + stakeholders: null, + trustlessWorkStatus: 'active', + escrowAddress: '', + escrowType: 'none', + escrowDetails: null, + creationTxHash: null, + transactionHash: '', + createdAt: subData.createdAt || new Date().toISOString(), + updatedAt: subData.updatedAt || new Date().toISOString(), + project: { + id: projectId, + title: subData.projectName || 'Untitled Project', + tagline: subData.category || 'Hackathon Project', + description: subData.description || '', + summary: subData.introduction || subData.description || '', + vision: null, + details: null, + category: subData.category || 'General', + status: subData.status || 'pending', + creatorId: subData.participantId || subData.userId || '', + organizationId: subData.organizationId || null, + teamMembers: teamMembers, + banner: null, + logo: subData.logo || '', + thumbnail: null, + githubUrl: + socialLinks.find((l: SocialLink) => + l.platform.toLowerCase().includes('github') + )?.url || '', + gitlabUrl: null, + bitbucketUrl: null, + projectWebsite: + socialLinks.find( + (l: SocialLink) => l.platform === 'website' || l.platform === 'demo' + )?.url || '', + demoVideo: demoVideoUrl, + whitepaperUrl: null, + pitchVideoUrl: null, + socialLinks: socialLinks, + contact: { primary: '', backup: '' }, + whitepaper: null, + pitchDeck: null, + votes: typeof subData.votes === 'number' ? subData.votes : 0, + voting: null, + tags: [], + approvedById: null, + approvedAt: null, + createdAt: subData.createdAt || new Date().toISOString(), + updatedAt: subData.updatedAt || new Date().toISOString(), + creator: { + id: subData.userId || '', + name: subData.participant?.name || subData.user?.name || 'Creator', + email: subData.participant?.email || subData.user?.email || '', + emailVerified: false, + image: subData.participant?.image || subData.user?.image || '', + createdAt: '', + updatedAt: '', + lastLoginMethod: '', + role: '', + banned: false, + banReason: null, + banExpires: null, + username: + subData.participant?.username || subData.user?.username || 'creator', + displayUsername: + subData.participant?.username || subData.user?.username || 'creator', + metadata: null, + twoFactorEnabled: false, + }, + organization: null, + milestones: milestones, + }, + }; +} + export default function ProjectPage({ params }: ProjectPageProps) { const [id, setId] = useState(null); + const searchParams = useSearchParams(); + const isSubmission = searchParams.get('type') === 'submission'; useEffect(() => { const getParams = async () => { @@ -66,5 +365,5 @@ export default function ProjectPage({ params }: ProjectPageProps) { return ; } - return ; + return ; } diff --git a/components/hackathons/submissions/CreateSubmissionModal.tsx b/components/hackathons/submissions/CreateSubmissionModal.tsx index fe248e77..0b6ab5d5 100644 --- a/components/hackathons/submissions/CreateSubmissionModal.tsx +++ b/components/hackathons/submissions/CreateSubmissionModal.tsx @@ -1,6 +1,5 @@ 'use client'; - -import { useState, useCallback, useEffect } from 'react'; +import React, { useState, useCallback, useEffect } from 'react'; import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { z } from 'zod'; @@ -310,7 +309,8 @@ export function CreateSubmissionModal({ ); }, []); - const handleNext = async () => { + const handleNext = async (e: React.MouseEvent) => { + e.preventDefault(); let isValid = false; if (currentStep === 0) { diff --git a/components/hackathons/submissions/submissionCard.tsx b/components/hackathons/submissions/submissionCard.tsx index 71ea947a..2afe17db 100644 --- a/components/hackathons/submissions/submissionCard.tsx +++ b/components/hackathons/submissions/submissionCard.tsx @@ -1,11 +1,27 @@ 'use client'; -import React, { useState } from 'react'; -import { ArrowUp, ThumbsUp, MessageCircle } from 'lucide-react'; +import React from 'react'; +import { + ThumbsUp, + MessageCircle, + Pin, + MoreHorizontal, + Edit, + Trash, +} from 'lucide-react'; +import { useRouter } from 'next/navigation'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { BoundlessButton } from '@/components/buttons'; import Image from 'next/image'; +import { formatDistanceToNow } from 'date-fns'; +import { useSubmissionVote } from '@/hooks/hackathon/use-submission-vote'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; interface SubmissionCardProps { title: string; @@ -26,7 +42,11 @@ interface SubmissionCardProps { onViewClick?: () => void; onUpvoteClick?: () => void; onCommentClick?: () => void; + onEditClick?: () => void; + onDeleteClick?: () => void; hasUserUpvoted?: boolean; + isPinned?: boolean; + isMySubmission?: boolean; } const SubmissionCard = ({ @@ -42,114 +62,140 @@ const SubmissionCard = ({ comments = 0, submittedDate, image = '/placeholder.svg', + submissionId, onViewClick, onUpvoteClick, onCommentClick, + onEditClick, + onDeleteClick, hasUserUpvoted = false, + isPinned = false, + isMySubmission = false, }: SubmissionCardProps) => { - const [isVoting, setIsVoting] = useState(false); - const [userVote, setUserVote] = useState(hasUserUpvoted ? 1 : 0); - const [currentUpvotes, setCurrentUpvotes] = useState( - votes?.current || upvotes - ); + const router = useRouter(); + + // Use custom hook for vote management + const { + voteCount, + hasVoted, + isLoading: isVoting, + toggleVote, + } = useSubmissionVote(submissionId || ''); + + // Use hook data if submissionId is provided, otherwise fall back to props + const currentUpvotes = submissionId ? voteCount : votes?.current || upvotes; + const userHasVoted = submissionId ? hasVoted : hasUserUpvoted; // Combine category and categories const allCategories = category ? [category, ...categories] : categories; const handleUpvote = async (e: React.MouseEvent) => { e.stopPropagation(); - if (isVoting) return; + if (isVoting || !submissionId) return; - setIsVoting(true); - - if (userVote === 1) { - setUserVote(0); - setCurrentUpvotes(prev => prev - 1); - } else { - setUserVote(1); - setCurrentUpvotes(prev => prev + 1); + try { + await toggleVote(); + onUpvoteClick?.(); + } catch (error) { + console.error('Error voting:', error); } - - await new Promise(resolve => setTimeout(resolve, 300)); - setIsVoting(false); - - onUpvoteClick?.(); }; const handleCommentClick = (e: React.MouseEvent) => { e.stopPropagation(); + if (submissionId) { + router.push(`/projects/${submissionId}?type=submission&tab=comments`); + } onCommentClick?.(); }; + const handleEditClick = (e: React.MouseEvent) => { + e.stopPropagation(); + onEditClick?.(); + }; + + const handleDeleteClick = (e: React.MouseEvent) => { + e.stopPropagation(); + onDeleteClick?.(); + }; + const formatDate = (dateString?: string) => { if (!dateString) return 'Recently'; - - const date = new Date(dateString); - const now = new Date(); - const diffTime = Math.abs(now.getTime() - date.getTime()); - const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); - - if (diffDays === 0) return 'Today'; - if (diffDays === 1) return 'Yesterday'; - if (diffDays < 7) return `${diffDays} days ago`; - if (diffDays < 30) return `${Math.floor(diffDays / 7)} weeks ago`; - if (diffDays < 365) return `${Math.floor(diffDays / 30)} months ago`; - return date.toLocaleDateString('en-US', { - month: 'short', - day: 'numeric', - year: 'numeric', - }); + return formatDistanceToNow(new Date(dateString), { addSuffix: true }); }; return (
+ {isPinned && ( +
+ + Your Submission +
+ )} + {/* Header with Avatar and Status */}
-

{submitterName}

+
+

{submitterName}

+ {/* We can add username here if we had it available in props */} +
+
+ +
+ + {status} + + + {isMySubmission && ( + + + + + + + + Edit Submission + + + + Delete Submission + + + + )}
- {/* - - -
-
- - - - {submitterName.slice(0, 2).toUpperCase()} - - -
- - {submitterName} - -
-
-
-
*/} - - - {status} -
{/* Categories */} @@ -203,18 +249,17 @@ const SubmissionCard = ({ onClick={handleUpvote} disabled={isVoting} className={`flex h-12 flex-1 items-center justify-center gap-2 rounded-lg text-base font-semibold shadow-lg transition-all duration-200 hover:shadow-xl ${ - userVote === 1 + userHasVoted ? 'border-primary/20 bg-primary/10 text-primary border' : 'bg-[#A7F950] text-black hover:bg-[#A7F950]' }`} > - {userVote === 1 ? ( - - ) : ( - - )} + - {isVoting ? 'Voting...' : userVote === 1 ? 'Upvoted' : 'Upvote'} + {isVoting ? 'Voting...' : userHasVoted ? 'Upvoted' : 'Upvote'} {currentUpvotes} diff --git a/components/hackathons/submissions/submissionTab.tsx b/components/hackathons/submissions/submissionTab.tsx index d0b916a0..2784c649 100644 --- a/components/hackathons/submissions/submissionTab.tsx +++ b/components/hackathons/submissions/submissionTab.tsx @@ -1,7 +1,7 @@ 'use client'; import React, { useState } from 'react'; -import { Search, ChevronDown, Plus, Edit } from 'lucide-react'; +import { Search, ChevronDown, Plus } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { @@ -17,7 +17,18 @@ import { useSubmissions } from '@/hooks/hackathon/use-submissions'; import { useSubmission } from '@/hooks/hackathon/use-submission'; import { useHackathonData } from '@/lib/providers/hackathonProvider'; import { useAuthStatus } from '@/hooks/use-auth'; -// import { useParams } from 'next/navigation'; +import { useRouter } from 'next/navigation'; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from '@/components/ui/alert-dialog'; +import { Loader2 } from 'lucide-react'; interface SubmissionTabProps { // hackathonSlugOrId?: string; @@ -35,6 +46,7 @@ const SubmissionTab: React.FC = ({ const { currentHackathon } = useHackathonData(); const hackathonId = currentHackathon?.id || ''; const orgId = organizationId || undefined; + const router = useRouter(); const { submissions, @@ -53,10 +65,15 @@ const SubmissionTab: React.FC = ({ string | null >(null); const [showDetailModal, setShowDetailModal] = useState(false); + const [submissionToDelete, setSubmissionToDelete] = useState( + null + ); + const [isDeleting, setIsDeleting] = useState(false); const { submission: mySubmission, isFetching: isLoadingMySubmission, fetchMySubmission, + remove: removeSubmission, } = useSubmission({ hackathonSlugOrId: hackathonId, organizationId: orgId, @@ -65,8 +82,7 @@ const SubmissionTab: React.FC = ({ const handleViewSubmission = (submissionId?: string) => { if (submissionId) { - setSelectedSubmissionId(submissionId); - setShowDetailModal(true); + router.push(`/projects/${submissionId}?type=submission`); } }; @@ -84,6 +100,23 @@ const SubmissionTab: React.FC = ({ } }; + const handleDeleteClick = (submissionId: string) => { + setSubmissionToDelete(submissionId); + }; + + const handleConfirmDelete = async () => { + if (submissionToDelete) { + setIsDeleting(true); + try { + await removeSubmission(submissionToDelete); + setSubmissionToDelete(null); + } catch (error) { + console.error('Failed to delete submission:', error); + } finally { + setIsDeleting(false); + } + } + }; return (
{/* Stats Section */} @@ -114,70 +147,7 @@ const SubmissionTab: React.FC = ({ )} */}
- {/* My Submission Section */} - {isAuthenticated && hackathonId && isRegistered && ( -
- {isLoadingMySubmission ? ( -
-

Loading your submission...

-
- ) : mySubmission ? ( -
-
-

- Your Submission -

- -
- setShowCreateModal(true)} - onUpvoteClick={() => {}} - onCommentClick={() => {}} - /> -
- ) : ( -
-

- You haven't submitted a project yet. -

- -
- )} -
- )} + {/* My Submission Section Removed - Integrated into Grid */} {/* Filters */}
@@ -250,11 +220,66 @@ const SubmissionTab: React.FC = ({
+ {/* Submissions Grid with Create Button if no submission */} + {!isLoadingMySubmission && + !mySubmission && + isAuthenticated && + isRegistered && ( +
+

+ You haven't submitted a project yet. +

+ +
+ )} + {/* Submissions Grid */} - {submissions.length > 0 ? ( + {submissions.length > 0 || mySubmission ? (
+ {/* Pinned User Submission */} + {mySubmission && ( + handleViewSubmission(mySubmission.id)} + onEditClick={() => setShowCreateModal(true)} + onDeleteClick={() => handleDeleteClick(mySubmission.id)} + onUpvoteClick={() => {}} + onCommentClick={() => {}} + /> + )} + {submissions - .filter(submission => submission.status === 'Approved') + .filter( + submission => + // Filter out approved submissions, and optionally filter out my own submission if it's already shown as pinned + submission.status === 'Approved' && + (mySubmission ? submission._id !== mySubmission.id : true) + ) .map((submission, index) => ( = ({ } onUpvoteClick={() => { if (!isAuthenticated) { - // Authentication check is handled in the hook, but we can show a message return; } handleUpvoteSubmission((submission as { _id?: string })?._id); }} onCommentClick={() => { if (!isAuthenticated) { - // Authentication check is handled in the hook, but we can show a message return; } handleCommentSubmission( @@ -334,6 +357,44 @@ const SubmissionTab: React.FC = ({ )} )} + + { + if (!open) { + setSubmissionToDelete(null); + } + }} + > + + + Delete Submission + + Are you sure you want to delete this submission? This action + cannot be undone. + + + + + Cancel + + + {isDeleting ? ( + <> + + Deleting... + + ) : ( + 'Delete' + )} + + + +
); }; diff --git a/components/hackathons/team-formation/CreateTeamPostModal.tsx b/components/hackathons/team-formation/CreateTeamPostModal.tsx index a8a75b59..2c029d94 100644 --- a/components/hackathons/team-formation/CreateTeamPostModal.tsx +++ b/components/hackathons/team-formation/CreateTeamPostModal.tsx @@ -35,33 +35,26 @@ const roleSchema = z.object({ skills: z.array(z.string()).optional(), }); -const teamPostSchema = z - .object({ - projectName: z - .string() - .min(3, 'Project name must be at least 3 characters'), - projectDescription: z - .string() - .min(50, 'Description must be at least 50 characters'), - lookingFor: z - .array(roleSchema) - .min(1, 'At least one role is required') - .max(10, 'Maximum 10 roles allowed'), - currentTeamSize: z - .number() - .min(1, 'Current team size must be at least 1') - .max(50, 'Current team size cannot exceed 50'), - maxTeamSize: z - .number() - .min(2, 'Max team size must be at least 2') - .max(50, 'Max team size cannot exceed 50'), - contactMethod: z.enum(['email', 'telegram', 'discord', 'github', 'other']), - contactInfo: z.string().min(1, 'Contact info is required'), - }) - .refine(data => data.maxTeamSize > data.currentTeamSize, { - message: 'Max team size must be greater than current team size', - path: ['maxTeamSize'], - }); +const teamPostSchema = z.object({ + teamName: z + .string() + .min(3, 'Team name must be at least 3 characters') + .max(100, 'Team name cannot exceed 100 characters'), + description: z + .string() + .min(10, 'Description must be at least 10 characters') + .max(500, 'Description cannot exceed 500 characters'), + lookingFor: z + .array(roleSchema) + .min(1, 'At least one role is required') + .max(10, 'Maximum 10 roles allowed'), + maxSize: z + .number() + .min(2, 'Max team size must be at least 2') + .max(50, 'Max team size cannot exceed 50'), + contactMethod: z.enum(['email', 'telegram', 'discord', 'github', 'other']), + contactInfo: z.string().min(1, 'Contact info is required'), +}); type TeamPostFormData = z.infer; @@ -95,11 +88,13 @@ export function CreateTeamPostModal({ const form = useForm({ resolver: zodResolver(teamPostSchema), defaultValues: { - projectName: initialData?.projectName || '', - projectDescription: initialData?.projectDescription || '', - lookingFor: initialData?.lookingFor || [{ role: '', skills: [] }], - currentTeamSize: initialData?.currentTeamSize || 1, - maxTeamSize: initialData?.maxTeamSize || 2, + teamName: initialData?.teamName || '', + description: initialData?.description || '', + lookingFor: initialData?.lookingFor.map(role => ({ + role, + skills: [], + })) || [{ role: '', skills: [] }], + maxSize: initialData?.maxSize || 5, contactMethod: initialData?.contactMethod || 'email', contactInfo: initialData?.contactInfo || '', }, @@ -111,12 +106,11 @@ export function CreateTeamPostModal({ useEffect(() => { if (open && initialData) { form.reset({ - projectName: initialData.projectName, - projectDescription: initialData.projectDescription, - lookingFor: initialData.lookingFor, - currentTeamSize: initialData.currentTeamSize, - maxTeamSize: initialData.maxTeamSize, - contactMethod: initialData.contactMethod, + teamName: initialData.teamName, + description: initialData.description, + lookingFor: initialData.lookingFor.map(role => ({ role, skills: [] })), + maxSize: initialData.maxSize, + contactMethod: initialData.contactMethod || 'email', contactInfo: initialData.contactInfo, }); } else if (!open) { @@ -205,10 +199,25 @@ export function CreateTeamPostModal({ } if (isEditMode && initialData) { - await updatePost(initialData.id, data); + const updatePayload = { + teamName: data.teamName, + description: data.description, + lookingFor: data.lookingFor.map(r => r.role), + isOpen: data.lookingFor.length > 0, + contactInfo: { + method: data.contactMethod, + value: data.contactInfo, + }, + }; + await updatePost(initialData.id, updatePayload); toast.success('Team post updated successfully'); } else { - await createPost(data); + const createPayload = { + ...data, + lookingFor: data.lookingFor.map(r => r.role), + maxSize: data.maxSize, + }; + await createPost(createPayload); toast.success('Team post created successfully'); } @@ -238,43 +247,46 @@ export function CreateTeamPostModal({
- {/* Project Name */} + {/* Team Name */} ( - Project Name + Team Name + + 3-100 characters. + )} /> - {/* Project Description */} + {/* Description */} ( - Project Description + Description