Skip to content

Commit 4a2d403

Browse files
authored
Merge pull request #586 from PotLock/fix/campaign-form-validation
Added set current button and fixed validation issues on create campaign page
2 parents 8498af5 + b48b9e6 commit 4a2d403

3 files changed

Lines changed: 107 additions & 50 deletions

File tree

src/common/lib/datetime.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ export const timestamp = preprocess(
9999
);
100100

101101
export const futureTimestamp = timestamp.refine(
102-
(value) => value > Temporal.Now.instant().epochMilliseconds,
102+
(value) => value >= Temporal.Now.instant().epochMilliseconds - 60_000,
103103
{ message: "Cannot be in the past" },
104104
);
105105

src/entities/campaign/components/editor.tsx

Lines changed: 104 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -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

src/entities/campaign/hooks/forms.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,12 +78,13 @@ export const useCampaignForm = ({ campaignId, ftId, onUpdateSuccess }: CampaignF
7878

7979
const isDisabled = useMemo(
8080
() =>
81-
!self.formState.isDirty ||
81+
(!isNewCampaign && !self.formState.isDirty) ||
8282
!self.formState.isValid ||
8383
self.formState.isSubmitting ||
8484
(values.ft_id !== NATIVE_TOKEN_ID && !isTokenDataLoading && token === undefined),
8585

8686
[
87+
isNewCampaign,
8788
isTokenDataLoading,
8889
self.formState.isDirty,
8990
self.formState.isSubmitting,

0 commit comments

Comments
 (0)