diff --git a/src/common/lib/datetime.ts b/src/common/lib/datetime.ts index 0dbd56df..b5862d42 100644 --- a/src/common/lib/datetime.ts +++ b/src/common/lib/datetime.ts @@ -99,7 +99,7 @@ export const timestamp = preprocess( ); export const futureTimestamp = timestamp.refine( - (value) => value > Temporal.Now.instant().epochMilliseconds, + (value) => value >= Temporal.Now.instant().epochMilliseconds - 60_000, { message: "Cannot be in the past" }, ); diff --git a/src/entities/campaign/components/editor.tsx b/src/entities/campaign/components/editor.tsx index 029a7063..dd9a979c 100644 --- a/src/entities/campaign/components/editor.tsx +++ b/src/entities/campaign/components/editor.tsx @@ -96,10 +96,9 @@ export const CampaignEditor = ({ existingData, campaignId, close }: CampaignEdit if (selectedTime < minTime) { // Auto-correct to minimum valid time (silently) const correctedTime = Temporal.Now.instant().add({ minutes: 1 }).epochMilliseconds; - - form.setValue("start_ms", correctedTime, { shouldValidate }); + form.setValue("start_ms", correctedTime, { shouldValidate, shouldDirty: true }); } else { - form.setValue("start_ms", selectedTime, { shouldValidate }); + form.setValue("start_ms", selectedTime, { shouldValidate, shouldDirty: true }); } }; @@ -127,19 +126,17 @@ export const CampaignEditor = ({ existingData, campaignId, close }: CampaignEdit if (selectedTime < minTime) { // Auto-correct to minimum valid time (silently) - form.setValue("end_ms", minTime, { shouldValidate }); + form.setValue("end_ms", minTime, { shouldValidate, shouldDirty: true }); } else { - form.setValue("end_ms", selectedTime, { shouldValidate }); + form.setValue("end_ms", selectedTime, { shouldValidate, shouldDirty: true }); } }; - // Update minimum datetime every minute to prevent past date selection - // This runs only on client-side to avoid SSG/SSR issues + // Keep the min attribute on datetime inputs up to date (client-side only). useEffect(() => { const updateMinDateTime = () => { - const now = Temporal.Now.instant().add({ minutes: 1 }); - - const newMin = now + const newMin = Temporal.Now.instant() + .add({ minutes: 1 }) .toZonedDateTimeISO(Temporal.Now.timeZoneId()) .toPlainDateTime() .toString({ smallestUnit: "minute" }); @@ -156,6 +153,12 @@ export const CampaignEditor = ({ existingData, campaignId, close }: CampaignEdit return () => clearInterval(interval); }, []); + // "Set to current" — sets start date to right now + const handleStartNow = () => { + const startEpoch = Temporal.Now.instant().add({ minutes: 1 }).epochMilliseconds; + form.setValue("start_ms", startEpoch, { shouldDirty: true, shouldValidate: true }); + }; + // Track project fields visibility to prevent them from disappearing useEffect(() => { const shouldShow = @@ -203,10 +206,49 @@ export const CampaignEditor = ({ existingData, campaignId, close }: CampaignEdit } else return null; }, [existingData, token]); - // Check if datetime fields have validation errors - const hasDateTimeErrors = useMemo(() => { - return form.formState.errors.start_ms || form.formState.errors.end_ms; - }, [form.formState.errors]); + const fieldErrorMessages = useMemo(() => { + const errors = form.formState.errors; + + const fields: [string, string][] = [ + ["name", "Campaign Name"], + ["description", "Description"], + ["target_amount", "Target Amount"], + ["min_amount", "Minimum Target Amount"], + ["max_amount", "Maximum Target Amount"], + ["start_ms", "Start Date"], + ["end_ms", "End Date"], + ["recipient", "Recipient"], + ["cover_image_url", "Cover Image URL"], + ["referral_fee_basis_points", "Referral Fee"], + ["creator_fee_basis_points", "Creator Fee"], + ["ft_id", "Token"], + ]; + + const messages: string[] = []; + + for (const [key, label] of fields) { + const error = errors[key as keyof typeof errors]; + + if (error?.message) { + messages.push(`${label}: ${String(error.message)}`); + } + } + + return messages; + }, [ + form.formState.errors.name, + form.formState.errors.description, + form.formState.errors.target_amount, + form.formState.errors.min_amount, + form.formState.errors.max_amount, + form.formState.errors.start_ms, + form.formState.errors.end_ms, + form.formState.errors.recipient, + form.formState.errors.cover_image_url, + form.formState.errors.referral_fee_basis_points, + form.formState.errors.creator_fee_basis_points, + form.formState.errors.ft_id, + ]); // TODO: Use `useEnhancedForm` for form setup instead, this effect is called upon EVERY RENDER, // TODO: which impacts UX and performance SUBSTANTIALLY! @@ -340,14 +382,12 @@ export const CampaignEditor = ({ existingData, campaignId, close }: CampaignEdit