@@ -96,10 +96,9 @@ export const CampaignEditor = ({ existingData, campaignId, close }: CampaignEdit
9696 if ( selectedTime < minTime ) {
9797 // Auto-correct to minimum valid time (silently)
9898 const correctedTime = Temporal . Now . instant ( ) . add ( { minutes : 1 } ) . epochMilliseconds ;
99-
100- form . setValue ( "start_ms" , correctedTime , { shouldValidate } ) ;
99+ form . setValue ( "start_ms" , correctedTime , { shouldValidate, shouldDirty : true } ) ;
101100 } else {
102- form . setValue ( "start_ms" , selectedTime , { shouldValidate } ) ;
101+ form . setValue ( "start_ms" , selectedTime , { shouldValidate, shouldDirty : true } ) ;
103102 }
104103 } ;
105104
@@ -127,19 +126,17 @@ export const CampaignEditor = ({ existingData, campaignId, close }: CampaignEdit
127126
128127 if ( selectedTime < minTime ) {
129128 // Auto-correct to minimum valid time (silently)
130- form . setValue ( "end_ms" , minTime , { shouldValidate } ) ;
129+ form . setValue ( "end_ms" , minTime , { shouldValidate, shouldDirty : true } ) ;
131130 } else {
132- form . setValue ( "end_ms" , selectedTime , { shouldValidate } ) ;
131+ form . setValue ( "end_ms" , selectedTime , { shouldValidate, shouldDirty : true } ) ;
133132 }
134133 } ;
135134
136- // Update minimum datetime every minute to prevent past date selection
137- // This runs only on client-side to avoid SSG/SSR issues
135+ // Keep the min attribute on datetime inputs up to date (client-side only).
138136 useEffect ( ( ) => {
139137 const updateMinDateTime = ( ) => {
140- const now = Temporal . Now . instant ( ) . add ( { minutes : 1 } ) ;
141-
142- const newMin = now
138+ const newMin = Temporal . Now . instant ( )
139+ . add ( { minutes : 1 } )
143140 . toZonedDateTimeISO ( Temporal . Now . timeZoneId ( ) )
144141 . toPlainDateTime ( )
145142 . toString ( { smallestUnit : "minute" } ) ;
@@ -156,6 +153,12 @@ export const CampaignEditor = ({ existingData, campaignId, close }: CampaignEdit
156153 return ( ) => clearInterval ( interval ) ;
157154 } , [ ] ) ;
158155
156+ // "Set to current" — sets start date to right now
157+ const handleStartNow = ( ) => {
158+ const startEpoch = Temporal . Now . instant ( ) . add ( { minutes : 1 } ) . epochMilliseconds ;
159+ form . setValue ( "start_ms" , startEpoch , { shouldDirty : true , shouldValidate : true } ) ;
160+ } ;
161+
159162 // Track project fields visibility to prevent them from disappearing
160163 useEffect ( ( ) => {
161164 const shouldShow =
@@ -203,10 +206,49 @@ export const CampaignEditor = ({ existingData, campaignId, close }: CampaignEdit
203206 } else return null ;
204207 } , [ existingData , token ] ) ;
205208
206- // Check if datetime fields have validation errors
207- const hasDateTimeErrors = useMemo ( ( ) => {
208- return form . formState . errors . start_ms || form . formState . errors . end_ms ;
209- } , [ form . formState . errors ] ) ;
209+ const fieldErrorMessages = useMemo ( ( ) => {
210+ const errors = form . formState . errors ;
211+
212+ const fields : [ string , string ] [ ] = [
213+ [ "name" , "Campaign Name" ] ,
214+ [ "description" , "Description" ] ,
215+ [ "target_amount" , "Target Amount" ] ,
216+ [ "min_amount" , "Minimum Target Amount" ] ,
217+ [ "max_amount" , "Maximum Target Amount" ] ,
218+ [ "start_ms" , "Start Date" ] ,
219+ [ "end_ms" , "End Date" ] ,
220+ [ "recipient" , "Recipient" ] ,
221+ [ "cover_image_url" , "Cover Image URL" ] ,
222+ [ "referral_fee_basis_points" , "Referral Fee" ] ,
223+ [ "creator_fee_basis_points" , "Creator Fee" ] ,
224+ [ "ft_id" , "Token" ] ,
225+ ] ;
226+
227+ const messages : string [ ] = [ ] ;
228+
229+ for ( const [ key , label ] of fields ) {
230+ const error = errors [ key as keyof typeof errors ] ;
231+
232+ if ( error ?. message ) {
233+ messages . push ( `${ label } : ${ String ( error . message ) } ` ) ;
234+ }
235+ }
236+
237+ return messages ;
238+ } , [
239+ form . formState . errors . name ,
240+ form . formState . errors . description ,
241+ form . formState . errors . target_amount ,
242+ form . formState . errors . min_amount ,
243+ form . formState . errors . max_amount ,
244+ form . formState . errors . start_ms ,
245+ form . formState . errors . end_ms ,
246+ form . formState . errors . recipient ,
247+ form . formState . errors . cover_image_url ,
248+ form . formState . errors . referral_fee_basis_points ,
249+ form . formState . errors . creator_fee_basis_points ,
250+ form . formState . errors . ft_id ,
251+ ] ) ;
210252
211253 // TODO: Use `useEnhancedForm` for form setup instead, this effect is called upon EVERY RENDER,
212254 // TODO: which impacts UX and performance SUBSTANTIALLY!
@@ -340,14 +382,12 @@ export const CampaignEditor = ({ existingData, campaignId, close }: CampaignEdit
340382
341383 < Form { ...form } >
342384 < form
343- onSubmit = { ( e ) => {
344- e . preventDefault ( ) ;
345-
385+ onSubmit = { form . handleSubmit ( ( values ) => {
346386 onSubmit ( {
347- ...form . getValues ( ) ,
387+ ...values ,
348388 allow_fee_avoidance : avoidFee ,
349389 } ) ;
350- } }
390+ } ) }
351391 >
352392 < div className = "mb-8 mt-8" >
353393 { showProjectFields && (
@@ -673,29 +713,39 @@ export const CampaignEditor = ({ existingData, campaignId, close }: CampaignEdit
673713 ) }
674714 >
675715 { ! campaignId ? (
676- < FormField
677- control = { form . control }
678- name = "start_ms"
679- render = { ( { field : { value, onChange : _onChange , ...field } } ) => (
680- < TextField
681- { ...field }
682- required = { true }
683- label = "Start Date"
684- min = { minStartDateTime }
685- value = {
686- typeof value === "number"
687- ? Temporal . Instant . fromEpochMilliseconds ( value )
688- . toZonedDateTimeISO ( Temporal . Now . timeZoneId ( ) )
689- . toPlainDateTime ( )
690- . toString ( { smallestUnit : "minute" } )
691- : undefined
692- }
693- onChange = { ( e ) => handleStartDateChange ( e . target . value ) }
694- classNames = { { root : "lg:w-90 md:w-90 mb-8 md:mb-0" } }
695- type = "datetime-local"
696- />
697- ) }
698- />
716+ < div className = "lg:w-90 md:w-90 mb-8 flex flex-col md:mb-0" >
717+ < FormField
718+ control = { form . control }
719+ name = "start_ms"
720+ render = { ( { field : { value, onChange : _onChange , ...field } } ) => (
721+ < TextField
722+ { ...field }
723+ required = { true }
724+ label = "Start Date"
725+ min = { minStartDateTime }
726+ value = {
727+ typeof value === "number"
728+ ? Temporal . Instant . fromEpochMilliseconds ( value )
729+ . toZonedDateTimeISO ( Temporal . Now . timeZoneId ( ) )
730+ . toPlainDateTime ( )
731+ . toString ( { smallestUnit : "minute" } )
732+ : undefined
733+ }
734+ onChange = { ( e ) => handleStartDateChange ( e . target . value ) }
735+ type = "datetime-local"
736+ />
737+ ) }
738+ />
739+
740+ < Button
741+ type = "button"
742+ variant = "standard-outline"
743+ onClick = { handleStartNow }
744+ className = "mt-2 w-fit"
745+ >
746+ Set to current
747+ </ Button >
748+ </ div >
699749 ) : (
700750 existingData ?. start_at &&
701751 toTimestamp ( existingData ?. start_at ) > Temporal . Now . instant ( ) . epochMilliseconds && (
@@ -823,12 +873,18 @@ export const CampaignEditor = ({ existingData, campaignId, close }: CampaignEdit
823873
824874 < div className = "my-10 flex flex-row-reverse justify-between" >
825875 < div className = "flex flex-col items-end gap-2" >
826- { isDisabled && ! form . formState . isSubmitting && (
827- < p className = "text-sm text-orange-600" >
828- { hasDateTimeErrors
829- ? "Please check the start and end dates above"
830- : "Please fill in all required fields correctly" }
831- </ p >
876+ { ! form . formState . isSubmitting && isDisabled && (
877+ < div className = "flex flex-col items-end gap-1" >
878+ { fieldErrorMessages . length > 0 ? (
879+ fieldErrorMessages . map ( ( msg , i ) => (
880+ < p key = { i } className = "text-sm text-orange-600" >
881+ { msg }
882+ </ p >
883+ ) )
884+ ) : (
885+ < p className = "text-sm text-orange-600" > Please fill in all required fields</ p >
886+ ) }
887+ </ div >
832888 ) }
833889 < Button variant = "brand-filled" disabled = { isDisabled } type = "submit" >
834890 { isUpdate ? "Update" : "Create" } Campaign
0 commit comments