Skip to content

Commit d0eb566

Browse files
committed
refactor: update ComingSoon component and enhance NewHackathonTab logic
- Changed the class name in the ComingSoon component for improved styling consistency. - Refactored the NewHackathonTab to streamline step completion logic and introduced a new method for setting steps from draft data. - Enhanced the ParticipantTab to normalize initial participant data and added error handling for form submissions. - Updated participantSchema to allow null values for maxParticipants, improving data validation. - Improved the transformFromApiFormat function to ensure proper handling of prize tiers and judging criteria.
1 parent 42fef27 commit d0eb566

7 files changed

Lines changed: 166 additions & 108 deletions

File tree

components/ComingSoon.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ const FeatureBlock = ({
6060
transition={{ duration: 0.6, delay: index * 0.12 }}
6161
viewport={{ once: true }}
6262
whileHover={{ scale: 1.02 }}
63-
className='relative overflow-hidden rounded-3xl bg-gradient-to-br from-[#0e0e0e] to-[#1a1a1a] p-10'
63+
className='relative overflow-hidden rounded-3xl bg-linear-to-br from-[#0e0e0e] to-[#1a1a1a] p-10'
6464
>
6565
<div className='pointer-events-none absolute -top-16 -right-16 opacity-5'>
6666
<Icon className='h-64 w-64' />

components/organization/hackathons/new/NewHackathonTab.tsx

Lines changed: 33 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,9 @@ export default function NewHackathonTab({
5050
steps,
5151
navigateToStep,
5252
canAccessStep,
53-
updateStepCompletion,
53+
setStepsFromDraft,
5454
setActiveTab,
55+
updateStepCompletion,
5556
} = useHackathonSteps('information');
5657

5758
// Use ref to store the callback to avoid circular dependency
@@ -83,74 +84,41 @@ export default function NewHackathonTab({
8384
const onDraftLoaded = useCallback(
8485
(formData: any, firstIncompleteStep: StepKey) => {
8586
setStepData(formData);
86-
setActiveTab(firstIncompleteStep);
8787

88-
// Use the validation helper to determine completion status
89-
// This checks if the step data has meaningful values, not just empty objects
90-
const newSteps: Record<StepKey, (typeof steps)[StepKey]> = {
91-
information: {
92-
status: isStepDataValid('information', formData)
93-
? 'completed'
94-
: 'active',
95-
isCompleted: isStepDataValid('information', formData),
96-
},
97-
timeline: {
98-
status: isStepDataValid('timeline', formData)
99-
? 'completed'
100-
: 'pending',
101-
isCompleted: isStepDataValid('timeline', formData),
102-
},
103-
participation: {
104-
status: isStepDataValid('participation', formData)
105-
? 'completed'
106-
: 'pending',
107-
isCompleted: isStepDataValid('participation', formData),
108-
},
109-
rewards: {
110-
status: isStepDataValid('rewards', formData)
111-
? 'completed'
112-
: 'pending',
113-
isCompleted: isStepDataValid('rewards', formData),
114-
},
115-
resources: {
116-
status: isStepDataValid('resources', formData)
117-
? 'completed'
118-
: 'pending',
119-
isCompleted: isStepDataValid('resources', formData),
120-
},
121-
judging: {
122-
status: isStepDataValid('judging', formData)
123-
? 'completed'
124-
: 'pending',
125-
isCompleted: isStepDataValid('judging', formData),
126-
},
127-
collaboration: {
128-
status: isStepDataValid('collaboration', formData)
129-
? 'completed'
130-
: 'pending',
131-
isCompleted: isStepDataValid('collaboration', formData),
132-
},
133-
review: {
134-
status: 'pending',
135-
isCompleted: false,
136-
},
137-
};
88+
// Build step state: completed before active, active for firstIncomplete, pending after
89+
const stepOrder = [
90+
'information',
91+
'timeline',
92+
'participation',
93+
'rewards',
94+
'resources',
95+
'judging',
96+
'collaboration',
97+
'review',
98+
] as StepKey[];
99+
const activeIndex = stepOrder.indexOf(firstIncompleteStep);
100+
const newSteps: Record<StepKey, (typeof steps)[StepKey]> = {} as Record<
101+
StepKey,
102+
(typeof steps)[StepKey]
103+
>;
138104

139-
// Set the first incomplete step as active
140-
newSteps[firstIncompleteStep] = {
141-
...newSteps[firstIncompleteStep],
142-
status: 'active',
143-
};
144-
145-
// Update all step completions
146-
Object.keys(newSteps).forEach(key => {
147-
updateStepCompletion(
148-
key as StepKey,
149-
newSteps[key as StepKey].isCompleted
150-
);
105+
stepOrder.forEach((key, index) => {
106+
const isCompleted = isStepDataValid(key, formData);
107+
if (index < activeIndex) {
108+
newSteps[key] = { status: 'completed', isCompleted: true };
109+
} else if (index === activeIndex) {
110+
newSteps[key] = { status: 'active', isCompleted };
111+
} else {
112+
newSteps[key] = {
113+
status: 'pending',
114+
isCompleted: key === 'review' ? false : isCompleted,
115+
};
116+
}
151117
});
118+
119+
setStepsFromDraft(newSteps, firstIncompleteStep);
152120
},
153-
[setStepData, setActiveTab, updateStepCompletion]
121+
[setStepData, setStepsFromDraft]
154122
);
155123

156124
// Update the ref when the callback changes

components/organization/hackathons/new/tabs/ParticipantTab.tsx

Lines changed: 73 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -75,33 +75,67 @@ const tabVisibility = [
7575
{ name: 'rulesTab' as const, label: 'Rules' },
7676
];
7777

78+
const defaultParticipantValues: ParticipantFormData = {
79+
participantType: 'individual',
80+
teamMin: 2,
81+
teamMax: 5,
82+
maxParticipants: undefined,
83+
require_github: true,
84+
require_demo_video: true,
85+
require_other_links: true,
86+
detailsTab: true,
87+
participantsTab: true,
88+
resourcesTab: true,
89+
submissionTab: true,
90+
announcementsTab: true,
91+
discussionTab: true,
92+
winnersTab: true,
93+
sponsorsTab: true,
94+
joinATeamTab: true,
95+
rulesTab: true,
96+
};
97+
98+
function normalizeParticipantInitialData(
99+
data: ParticipantFormData | undefined
100+
): ParticipantFormData {
101+
if (!data) return defaultParticipantValues;
102+
const type = data.participantType;
103+
const needsTeamSize = type === 'team' || type === 'team_or_individual';
104+
return {
105+
...defaultParticipantValues,
106+
...data,
107+
teamMin: needsTeamSize
108+
? data.teamMin && data.teamMin >= 1
109+
? data.teamMin
110+
: 2
111+
: data.teamMin,
112+
teamMax: needsTeamSize
113+
? data.teamMax && data.teamMax >= 1
114+
? data.teamMax
115+
: 5
116+
: data.teamMax,
117+
maxParticipants:
118+
data.maxParticipants != null && data.maxParticipants >= 1
119+
? data.maxParticipants
120+
: undefined,
121+
};
122+
}
123+
78124
export default function ParticipantTab({
125+
onContinue,
79126
onSave,
80127
initialData,
81128
isLoading = false,
82129
isRegistrationClosed = false,
83130
}: ParticipantTabProps) {
131+
const normalizedInitial = React.useMemo(
132+
() => normalizeParticipantInitialData(initialData),
133+
[initialData]
134+
);
135+
84136
const form = useForm<ParticipantFormData>({
85137
resolver: zodResolver(participantSchema),
86-
defaultValues: initialData || {
87-
participantType: 'individual',
88-
teamMin: 2,
89-
teamMax: 5,
90-
maxParticipants: undefined,
91-
require_github: true,
92-
require_demo_video: true,
93-
require_other_links: true,
94-
detailsTab: true,
95-
participantsTab: true,
96-
resourcesTab: true,
97-
submissionTab: true,
98-
announcementsTab: true,
99-
discussionTab: true,
100-
winnersTab: true,
101-
sponsorsTab: true,
102-
joinATeamTab: true,
103-
rulesTab: true,
104-
},
138+
defaultValues: normalizedInitial,
105139
});
106140

107141
const participantType = form.watch('participantType');
@@ -110,16 +144,17 @@ export default function ParticipantTab({
110144
maxParticipants !== undefined && maxParticipants !== null;
111145

112146
React.useEffect(() => {
113-
if (initialData) {
114-
form.reset(initialData);
115-
}
116-
}, [initialData, form]);
147+
form.reset(normalizedInitial);
148+
}, [normalizedInitial, form]);
117149

118150
const onSubmit = async (data: ParticipantFormData) => {
119151
try {
120152
if (onSave) {
121153
await onSave(data);
122154
}
155+
if (onContinue) {
156+
onContinue();
157+
}
123158
} catch (error: unknown) {
124159
const err = error as {
125160
response?: { data?: { message?: string | string[] } };
@@ -182,9 +217,23 @@ export default function ParticipantTab({
182217
</div>
183218
);
184219

220+
const handleInvalid = (errors: Record<string, { message?: string }>) => {
221+
const firstKey = Object.keys(errors)[0];
222+
const firstMessage =
223+
firstKey && errors[firstKey]?.message
224+
? errors[firstKey].message
225+
: undefined;
226+
toast.error('Please fix the errors below before continuing.', {
227+
description: firstMessage,
228+
});
229+
};
230+
185231
return (
186232
<Form {...form}>
187-
<form onSubmit={form.handleSubmit(onSubmit)} className='space-y-8'>
233+
<form
234+
onSubmit={form.handleSubmit(onSubmit, handleInvalid)}
235+
className='space-y-8'
236+
>
188237
{/* Participant Type */}
189238
<div className='space-y-4'>
190239
<div>

components/organization/hackathons/new/tabs/schemas/participantSchema.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@ export const participantSchema = z
77
.default('individual'),
88
teamMin: z.number().min(1).max(20).optional(),
99
teamMax: z.number().min(1).max(20).optional(),
10-
maxParticipants: z.number().int().min(1).optional(),
10+
maxParticipants: z
11+
.union([z.number().int().min(1), z.null()])
12+
.optional()
13+
.transform(val => (val === null ? undefined : val)),
1114
require_github: z.boolean().optional(),
1215
require_demo_video: z.boolean().optional(),
1316
require_other_links: z.boolean().optional(),

hooks/use-hackathon-draft.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,11 @@ import type { RewardsFormData } from '@/components/organization/hackathons/new/t
99
import type { ResourcesFormData } from '@/components/organization/hackathons/new/tabs/schemas/resourcesSchema';
1010
import type { JudgingFormData } from '@/components/organization/hackathons/new/tabs/schemas/judgingSchema';
1111
import type { CollaborationFormData } from '@/components/organization/hackathons/new/tabs/schemas/collaborationSchema';
12-
import type { StepKey } from '@/components/organization/hackathons/new/constants';
12+
import {
13+
type StepKey,
14+
STEP_ORDER,
15+
} from '@/components/organization/hackathons/new/constants';
16+
import { isStepDataValid } from '@/lib/utils/hackathon-step-validation';
1317

1418
interface StepData {
1519
information?: InfoFormData;
@@ -68,7 +72,6 @@ export const useHackathonDraft = ({
6872
loadDraft();
6973
}, [initialDraftId, organizationId, fetchDraft]);
7074

71-
// In hooks/use-hackathon-draft.ts useEffect:
7275
useEffect(() => {
7376
if (
7477
currentDraft &&
@@ -80,8 +83,11 @@ export const useHackathonDraft = ({
8083
setStepData(formData);
8184
draftInitializedRef.current = currentDraft.id;
8285

86+
const firstIncompleteStep =
87+
STEP_ORDER.find(key => !isStepDataValid(key, formData)) ?? 'review';
88+
8389
if (onDraftLoaded) {
84-
onDraftLoaded(formData, 'information' as StepKey);
90+
onDraftLoaded(formData, firstIncompleteStep);
8591
}
8692
} else if (currentDraft && currentDraft.id === initialDraftId) {
8793
console.log('⏭️ Draft already initialized, skipping'); // Debug

hooks/use-hackathon-steps.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ interface UseHackathonStepsReturn {
1010
activeTab: StepKey;
1111
steps: Record<StepKey, StepData>;
1212
setActiveTab: (tab: StepKey) => void;
13+
setStepsFromDraft: (
14+
steps: Record<StepKey, StepData>,
15+
activeStep: StepKey
16+
) => void;
1317
navigateToStep: (stepKey: StepKey, skipAccessCheck?: boolean) => void;
1418
canAccessStep: (stepKey: StepKey) => boolean;
1519
updateStepCompletion: (
@@ -91,6 +95,14 @@ export const useHackathonSteps = (
9195
[canAccessStep, getCurrentStepIndex]
9296
);
9397

98+
const setStepsFromDraft = useCallback(
99+
(stepsState: Record<StepKey, StepData>, activeStep: StepKey) => {
100+
setSteps(stepsState);
101+
setActiveTab(activeStep);
102+
},
103+
[]
104+
);
105+
94106
const updateStepCompletion = useCallback(
95107
(stepKey: StepKey, isCompleted: boolean, nextStep?: StepKey) => {
96108
setSteps(prev => {
@@ -124,6 +136,7 @@ export const useHackathonSteps = (
124136
activeTab,
125137
steps,
126138
setActiveTab,
139+
setStepsFromDraft,
127140
navigateToStep,
128141
canAccessStep,
129142
updateStepCompletion,

0 commit comments

Comments
 (0)