diff --git a/app/(landing)/hackathons/preview/[orgId]/[draftId]/page.tsx b/app/(landing)/hackathons/preview/[orgId]/[draftId]/page.tsx index 2cc8283a..4a13e5e0 100644 --- a/app/(landing)/hackathons/preview/[orgId]/[draftId]/page.tsx +++ b/app/(landing)/hackathons/preview/[orgId]/[draftId]/page.tsx @@ -132,7 +132,15 @@ export default function DraftPreviewPage({ params }: PreviewPageProps) { timezone: draft.data.timeline?.timezone || 'UTC', startDate: draft.data.timeline?.startDate || '', - endDate: draft.data.timeline?.winnerAnnouncementDate || '', + endDate: + draft.data.timeline?.winnersAnnouncedAt || + draft.data.timeline?.winnerAnnouncementDate || + draft.data.timeline?.judgingEnd || + draft.data.timeline?.judgingDate || + draft.data.timeline?.judgingStart || + draft.data.timeline?.submissionDeadline || + draft.data.timeline?.startDate || + '', submissionDeadline: draft.data.timeline?.submissionDeadline || '', registrationDeadline: draft.data.participation?.registrationDeadline || '', diff --git a/app/(landing)/organizations/[id]/hackathons/page.tsx b/app/(landing)/organizations/[id]/hackathons/page.tsx index 1f0fb3e0..699c3905 100644 --- a/app/(landing)/organizations/[id]/hackathons/page.tsx +++ b/app/(landing)/organizations/[id]/hackathons/page.tsx @@ -41,8 +41,7 @@ const calculateDraftCompletion = (draft: HackathonDraft): number => { draft.data.information?.categories, draft.data.timeline?.startDate, draft.data.timeline?.submissionDeadline, - draft.data.timeline?.judgingDate, - draft.data.timeline?.winnerAnnouncementDate, + draft.data.timeline?.judgingStart || draft.data.timeline?.judgingDate, draft.data.timeline?.timezone, draft.data.participation?.participantType, draft.data.rewards?.prizeTiers?.length, @@ -402,7 +401,12 @@ export default function HackathonsPage() { ? (hackathon as HackathonDraft).data.timeline ?.submissionDeadline || (hackathon as HackathonDraft).data.timeline - ?.winnerAnnouncementDate + ?.winnersAnnouncedAt || + (hackathon as HackathonDraft).data.timeline + ?.winnerAnnouncementDate || + (hackathon as HackathonDraft).data.timeline?.judgingEnd || + (hackathon as HackathonDraft).data.timeline?.judgingDate || + (hackathon as HackathonDraft).data.timeline?.judgingStart : (hackathon as Hackathon).submissionDeadline || (hackathon as Hackathon).endDate; const totalPrize = isDraft diff --git a/app/sitemap.ts b/app/sitemap.ts index 7189496d..e5db2450 100644 --- a/app/sitemap.ts +++ b/app/sitemap.ts @@ -3,7 +3,7 @@ import { getBlogPosts } from '@/lib/api/blog'; import { getHackathons } from '@/lib/api/hackathons'; import { getCrowdfundingProjects } from '@/features/projects/api'; import type { BlogPost } from '@/types/blog'; -import type { Hackathon } from '@/types/hackathon/core'; +import type { Hackathon as HackathonAPI } from '@/lib/api/hackathons'; import type { Crowdfunding } from '@/features/projects/types'; // Constants @@ -173,14 +173,14 @@ async function fetchHackathonsSitemap(): Promise { } return response.data.hackathons - .filter((hackathon: Hackathon) => { + .filter((hackathon: HackathonAPI) => { // Validate required fields if (!hackathon.slug) { return false; } return true; }) - .map((hackathon: Hackathon) => ({ + .map((hackathon: HackathonAPI) => ({ url: `${SITE_URL}/hackathons/${hackathon.slug}`, lastModified: new Date( hackathon.updatedAt || hackathon.publishedAt || new Date() diff --git a/components/organization/hackathons/new/NewHackathonTab.tsx b/components/organization/hackathons/new/NewHackathonTab.tsx index ffe91b12..41059492 100644 --- a/components/organization/hackathons/new/NewHackathonTab.tsx +++ b/components/organization/hackathons/new/NewHackathonTab.tsx @@ -159,7 +159,7 @@ export default function NewHackathonTab({ onDraftLoadedRef.current = onDraftLoaded; }, [onDraftLoaded]); - const { isPublishing, publish } = useHackathonPublish({ + const { isPublishing, publish, publishResponse } = useHackathonPublish({ organizationId: derivedOrgId || '', stepData, draftId: draftId || '', @@ -323,6 +323,7 @@ export default function NewHackathonTab({ isSavingDraft={isSavingDraft} organizationId={derivedOrgId} draftId={draftId} + publishResponse={publishResponse} /> diff --git a/components/organization/hackathons/new/tabs/ReviewTab.tsx b/components/organization/hackathons/new/tabs/ReviewTab.tsx index 24dbf922..32681179 100644 --- a/components/organization/hackathons/new/tabs/ReviewTab.tsx +++ b/components/organization/hackathons/new/tabs/ReviewTab.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import { InfoFormData } from './schemas/infoSchema'; import { TimelineFormData } from './schemas/timelineSchema'; import { ParticipantFormData } from './schemas/participantSchema'; @@ -22,6 +22,7 @@ import { SectionRenderer } from './components/review/SectionRenderer'; import { usePrizePoolCalculations } from '@/hooks/use-prize-pool-calculations'; import { REVIEW_SECTION_CONFIG } from './constants/review-sections'; import { toast } from 'sonner'; +import type { PublishResponseData } from '@/hooks/use-hackathon-publish'; interface ReviewTabProps { allData: { @@ -37,9 +38,9 @@ interface ReviewTabProps { onSaveDraft?: () => Promise; isLoading?: boolean; isSavingDraft?: boolean; - hackathonUrl?: string; organizationId?: string; draftId?: string | null; + publishResponse?: PublishResponseData | null; } export default function ReviewTab({ @@ -49,9 +50,9 @@ export default function ReviewTab({ onSaveDraft, isLoading = false, isSavingDraft = false, - hackathonUrl, organizationId, draftId, + publishResponse, }: ReviewTabProps) { const [showDraftModal, setShowDraftModal] = useState(false); const [showPublishedModal, setShowPublishedModal] = useState(false); @@ -60,11 +61,18 @@ export default function ReviewTab({ const { totalPrizePool, platformFee, totalFunding } = usePrizePoolCalculations(allData.rewards); + // Show published modal only when we have a successful publish response + useEffect(() => { + if (publishResponse) { + setShowPublishedModal(true); + } + }, [publishResponse]); + const handlePublish = async () => { try { if (onPublish) { await onPublish(); - setShowPublishedModal(true); + // Modal will be shown automatically when publishResponse is set via useEffect } } catch { // Error is handled in the hook, so we don't need to show another toast @@ -154,7 +162,8 @@ export default function ReviewTab({ ); diff --git a/components/organization/hackathons/new/tabs/TimelineTab.tsx b/components/organization/hackathons/new/tabs/TimelineTab.tsx index 0c813066..0d9fc0e7 100644 --- a/components/organization/hackathons/new/tabs/TimelineTab.tsx +++ b/components/organization/hackathons/new/tabs/TimelineTab.tsx @@ -3,25 +3,32 @@ import { FormControl, FormField, FormItem, - FormLabel, FormMessage, } from '@/components/ui/form'; -import { useForm } from 'react-hook-form'; +import { useFieldArray, useForm } from 'react-hook-form'; import React from 'react'; import { zodResolver } from '@hookform/resolvers/zod'; import { timelineSchema, TimelineFormData } from './schemas/timelineSchema'; import { BoundlessButton } from '@/components/buttons'; -import { CalendarIcon } from 'lucide-react'; +import { Plus, Trash2 } from 'lucide-react'; import { toast } from 'sonner'; -import { format } from 'date-fns'; -import { cn } from '@/lib/utils'; import { Button } from '@/components/ui/button'; -import { Calendar } from '@/components/ui/calendar'; +import { Switch } from '@/components/ui/switch'; +import { Input } from '@/components/ui/input'; +import { Textarea } from '@/components/ui/textarea'; import { - Popover, - PopoverContent, - PopoverTrigger, -} from '@/components/ui/popover'; + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import FieldLabel from './components/timeline/FieldLabel'; +import DateTimeInput from './components/timeline/DateTimeInput'; +import { + TIMELINE_FIELD_TOOLTIPS, + TIMEZONES, +} from './components/timeline/timelineConstants'; interface TimelineTabProps { onContinue?: () => void; @@ -39,14 +46,24 @@ export default function TimelineTab({ resolver: zodResolver(timelineSchema), defaultValues: { startDate: initialData?.startDate || undefined, - endDate: initialData?.endDate || undefined, - registrationDeadline: initialData?.registrationDeadline || undefined, submissionDeadline: initialData?.submissionDeadline || undefined, + judgingStart: initialData?.judgingStart || undefined, + endDate: initialData?.endDate || undefined, + judgingEnd: initialData?.judgingEnd || undefined, + winnersAnnouncedAt: initialData?.winnersAnnouncedAt || undefined, timezone: initialData?.timezone || 'UTC', phases: initialData?.phases || [], }, }); + const phasesFieldArray = useFieldArray({ + control: form.control, + name: 'phases', + }); + + const hasJudgingEnd = !!form.watch('judgingEnd'); + const hasWinnersAnnouncedAt = !!form.watch('winnersAnnouncedAt'); + const onSubmit = async (data: TimelineFormData) => { try { if (onSave) { @@ -61,186 +78,344 @@ export default function TimelineTab({ return (
- - ( - - - Start Date * - - - - - - - - - date < new Date()} - initialFocus + +
+ ( + + + + + + )} + /> + + ( + + + + + + )} + /> + + ( + + + + + + )} + /> + + ( + + + + + + )} + /> + + ( + + + + + + + + )} + /> +
+ +
+
+
+ +

+ Add an end date for judging. +

+
+ { + if (checked) { + const fallbackDate = + form.getValues('judgingStart') || + form.getValues('submissionDeadline') || + new Date(); + form.setValue('judgingEnd', fallbackDate, { + shouldValidate: true, + }); + } else { + form.setValue('judgingEnd', undefined, { + shouldValidate: true, + }); + } + }} + /> +
+ + {hasJudgingEnd && ( + ( + + + - - - - + + + )} + /> )} - /> - - ( - - - Submission Deadline * - - - - - - - - - date < new Date()} - initialFocus +
+ +
+
+
+ +

+ Set a public results date. +

+
+ { + if (checked) { + const fallbackDate = + form.getValues('judgingEnd') || + form.getValues('judgingStart') || + new Date(); + form.setValue('winnersAnnouncedAt', fallbackDate, { + shouldValidate: true, + }); + } else { + form.setValue('winnersAnnouncedAt', undefined, { + shouldValidate: true, + }); + } + }} + /> +
+ + {hasWinnersAnnouncedAt && ( + ( + + + - - - - + + + )} + /> )} - /> - - ( - - - Judging * - - - - - +
+ + {phasesFieldArray.fields.length === 0 ? ( +
+ No phases added yet. +
+ ) : ( +
+ {phasesFieldArray.fields.map((phase, index) => ( +
+
+

+ Phase {index + 1} +

+ +
+ +
+ ( + + + + + + + )} - - - - - - date < new Date()} - initialFocus - /> - - - - - )} - /> - - ( - - - Winner Announcement * - - - - - - - - - date < new Date()} - initialFocus + /> +
+ + ( + + + +