Compare commits
No commits in common. "9b48ba73e2e1c0a141d2e102915e0a1c3e8492b5" and "d9de0fdd73a560d960a572c66b37cb780519ad96" have entirely different histories.
9b48ba73e2
...
d9de0fdd73
1 changed files with 153 additions and 221 deletions
|
|
@ -428,34 +428,13 @@ watch(
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const { resetForm } = form;
|
const { resetForm, meta } = form;
|
||||||
|
const isFormValid = computed(
|
||||||
/** Field name -> what the organiser calls it, for the invalid-submit toast. */
|
() =>
|
||||||
const FIELD_LABELS: Record<string, string> = {
|
meta.value.valid &&
|
||||||
name: "Title",
|
!hasPromoErrors(promoErrors.value) &&
|
||||||
info: "Description",
|
!hasWaveErrors.value,
|
||||||
event_start_date: "Start date",
|
);
|
||||||
event_start_time: "Start time",
|
|
||||||
event_end_date: "End date",
|
|
||||||
event_end_time: "End time",
|
|
||||||
location: "Location",
|
|
||||||
currency: "Price currency",
|
|
||||||
payment_methods: "Payment methods",
|
|
||||||
fiat_currency: "Fiat currency",
|
|
||||||
amount_tickets: "Tickets",
|
|
||||||
price_per_ticket: "Price",
|
|
||||||
notification_subject: "Subject",
|
|
||||||
notification_body: "Body",
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Open whichever collapsible section holds a problem, so the reason is on
|
|
||||||
* screen rather than hidden behind a closed panel.
|
|
||||||
*/
|
|
||||||
function revealSectionFor(field: string) {
|
|
||||||
if (field === "promo_codes") promoOpen.value = true;
|
|
||||||
if (field === "ticket_waves") wavesOpen.value = true;
|
|
||||||
}
|
|
||||||
const isLoading = ref(false);
|
const isLoading = ref(false);
|
||||||
|
|
||||||
const today = computed(() => format(new Date(), "yyyy-MM-dd"));
|
const today = computed(() => format(new Date(), "yyyy-MM-dd"));
|
||||||
|
|
@ -469,200 +448,160 @@ function toggleCategory(cat: string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const onSubmit = form.handleSubmit(
|
const onSubmit = form.handleSubmit(async (formValues) => {
|
||||||
async (formValues) => {
|
if (!isFormValid.value) return;
|
||||||
// Promo codes and ticket waves are not part of the Zod schema, so
|
|
||||||
// vee-validate cannot block on them. Check here and OPEN the section
|
if (!paymentService) {
|
||||||
// that holds the problem — both editors live inside collapsibles, and
|
toastService.error("Payment service not available");
|
||||||
// an error rendered inside a closed one is invisible.
|
return;
|
||||||
if (hasWaveErrors.value) {
|
}
|
||||||
revealSectionFor("ticket_waves");
|
|
||||||
toastService.error("Check the ticket waves below");
|
const paymentSvc = paymentService as any;
|
||||||
return;
|
const preferredWallet = paymentSvc?.getPreferredWallet?.();
|
||||||
}
|
if (!preferredWallet) {
|
||||||
if (hasPromoErrors(promoErrors.value)) {
|
toastService.error("No wallet available. Please connect a wallet first.");
|
||||||
revealSectionFor("promo_codes");
|
return;
|
||||||
toastService.error("Check the promo codes below");
|
}
|
||||||
return;
|
|
||||||
|
isLoading.value = true;
|
||||||
|
try {
|
||||||
|
const eventData: CreateEventRequest = {
|
||||||
|
name: formValues.name,
|
||||||
|
event_start_date: withLocalTzOffset(
|
||||||
|
foldDateTime(formValues.event_start_date, formValues.event_start_time),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
if (!isEditMode.value) {
|
||||||
|
// Wallet binds at creation. The backend ignores the field on
|
||||||
|
// update so we leave it off the edit payload for clean wire.
|
||||||
|
eventData.wallet = preferredWallet.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!paymentService) {
|
// Optional fields — only include if provided
|
||||||
toastService.error("Payment service not available");
|
if (formValues.info) eventData.info = formValues.info;
|
||||||
return;
|
if (formValues.event_end_date) {
|
||||||
|
eventData.event_end_date = withLocalTzOffset(
|
||||||
|
foldDateTime(formValues.event_end_date, formValues.event_end_time),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
if (formValues.location) eventData.location = formValues.location;
|
||||||
const paymentSvc = paymentService as any;
|
if (bannerImages.value.length > 0) {
|
||||||
const preferredWallet = paymentSvc?.getPreferredWallet?.();
|
eventData.banner = imageService.getImageUrl(bannerImages.value[0].alias);
|
||||||
if (!preferredWallet) {
|
} else if (isEditMode.value) {
|
||||||
toastService.error("No wallet available. Please connect a wallet first.");
|
// User cleared the banner during edit — propagate the null so the
|
||||||
return;
|
// backend wipes the field instead of keeping the old image.
|
||||||
|
eventData.banner = null;
|
||||||
}
|
}
|
||||||
|
if (formValues.currency) eventData.currency = formValues.currency;
|
||||||
|
// allow_fiat always sends (derived from the rail list) so a
|
||||||
|
// true→false flip propagates on edit; fiat_currency only sends when
|
||||||
|
// fiat is on (no point persisting a rail-currency the backend won't use).
|
||||||
|
const acceptsFiat = formValues.payment_methods.includes("fiat");
|
||||||
|
eventData.allow_fiat = acceptsFiat;
|
||||||
|
if (acceptsFiat && formValues.fiat_currency) {
|
||||||
|
eventData.fiat_currency = formValues.fiat_currency;
|
||||||
|
}
|
||||||
|
if (formValues.amount_tickets !== undefined)
|
||||||
|
eventData.amount_tickets = formValues.amount_tickets;
|
||||||
|
if (formValues.price_per_ticket)
|
||||||
|
eventData.price_per_ticket = formValues.price_per_ticket;
|
||||||
|
if (selectedCategories.value.length > 0)
|
||||||
|
eventData.categories = selectedCategories.value;
|
||||||
|
|
||||||
isLoading.value = true;
|
// Notification config + promo codes go inside the `extra` envelope.
|
||||||
try {
|
// On edit overlay onto the existing event.extra so fields only the
|
||||||
const eventData: CreateEventRequest = {
|
// LNbits admin UI sets (conditional, min_tickets) survive the
|
||||||
name: formValues.name,
|
// round-trip. promo_codes is always sent explicitly: what the
|
||||||
event_start_date: withLocalTzOffset(
|
// organizer sees in the editor is the truth, an empty list clears.
|
||||||
foldDateTime(
|
// Ticket waves. The price / capacity / currency fields above ARE the
|
||||||
formValues.event_start_date,
|
// primary wave, so they have to be written INTO it: the backend
|
||||||
formValues.event_start_time,
|
// derives the event-level values FROM the waves on every write
|
||||||
),
|
// (`sync_event_ticket_waves`), so sending a changed amount_tickets
|
||||||
),
|
// beside an unchanged wave list leaves the wave's old number winning
|
||||||
};
|
// and the organizer's edit is silently discarded.
|
||||||
if (!isEditMode.value) {
|
// Seeded so a NEW event's implied primary wave matches what the
|
||||||
// Wallet binds at creation. The backend ignores the field on
|
// backend would synthesize. `create_event` stamps `time` and derives
|
||||||
// update so we leave it off the edit payload for clean wire.
|
// the wave window from it plus the closing/end/start dates; without
|
||||||
eventData.wallet = preferredWallet.id;
|
// the same inputs here the wave lands with empty date strings, which
|
||||||
|
// the backend's date parse throws on for every later read.
|
||||||
|
const storedWaves = ensureTicketWaves({
|
||||||
|
...(props.event ?? {}),
|
||||||
|
time: props.event?.time ?? new Date().toISOString(),
|
||||||
|
event_start_date: eventData.event_start_date,
|
||||||
|
event_end_date: eventData.event_end_date ?? null,
|
||||||
|
closing_date: props.event?.closing_date ?? null,
|
||||||
|
extra: props.event?.extra ?? null,
|
||||||
|
});
|
||||||
|
const [primaryWave] = applyFormToPrimaryWave(storedWaves, {
|
||||||
|
closing_date: eventData.closing_date,
|
||||||
|
currency: formValues.currency,
|
||||||
|
allow_fiat: formValues.payment_methods.includes("fiat"),
|
||||||
|
fiat_currency: formValues.fiat_currency,
|
||||||
|
amount_tickets: formValues.amount_tickets,
|
||||||
|
price_per_ticket: formValues.price_per_ticket ?? 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Notification config + promo codes go inside the `extra` envelope.
|
||||||
|
// On edit overlay onto the existing event.extra so fields only the
|
||||||
|
// LNbits admin UI sets (conditional, min_tickets) survive the
|
||||||
|
// round-trip. promo_codes is always sent explicitly: what the
|
||||||
|
// organizer sees in the editor is the truth, an empty list clears.
|
||||||
|
eventData.extra = {
|
||||||
|
...(props.event?.extra ?? {}),
|
||||||
|
payment_methods: formValues.payment_methods,
|
||||||
|
email_notifications: formValues.email_notifications,
|
||||||
|
nostr_notifications: formValues.nostr_notifications,
|
||||||
|
notification_subject: formValues.notification_subject,
|
||||||
|
notification_body: formValues.notification_body,
|
||||||
|
promo_codes: toWirePromoCodes(promoRows.value),
|
||||||
|
ticket_waves: [primaryWave, ...extraWaves.value],
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isEditMode.value) {
|
||||||
|
if (!props.onUpdateEvent || !props.event?.id) {
|
||||||
|
toastService.error("Update handler missing");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
await props.onUpdateEvent(props.event.id, eventData);
|
||||||
// Optional fields — only include if provided
|
toastService.success(
|
||||||
if (formValues.info) eventData.info = formValues.info;
|
willLandInPending.value
|
||||||
if (formValues.event_end_date) {
|
? "Updated — awaiting re-approval. Hidden from the public feed until reviewed."
|
||||||
eventData.event_end_date = withLocalTzOffset(
|
: "Event updated!",
|
||||||
foldDateTime(formValues.event_end_date, formValues.event_end_time),
|
);
|
||||||
);
|
emit("event-updated");
|
||||||
|
} else {
|
||||||
|
if (!props.onCreateEvent) {
|
||||||
|
toastService.error("Create handler missing");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
if (formValues.location) eventData.location = formValues.location;
|
await props.onCreateEvent(eventData);
|
||||||
if (bannerImages.value.length > 0) {
|
toastService.success(
|
||||||
eventData.banner = imageService.getImageUrl(
|
willLandInPending.value
|
||||||
bannerImages.value[0].alias,
|
? "Submitted! Awaiting admin approval — your draft is visible on your feed with a Pending badge."
|
||||||
);
|
: "Event submitted!",
|
||||||
} else if (isEditMode.value) {
|
);
|
||||||
// User cleared the banner during edit — propagate the null so the
|
emit("event-created");
|
||||||
// backend wipes the field instead of keeping the old image.
|
|
||||||
eventData.banner = null;
|
|
||||||
}
|
|
||||||
if (formValues.currency) eventData.currency = formValues.currency;
|
|
||||||
// allow_fiat always sends (derived from the rail list) so a
|
|
||||||
// true→false flip propagates on edit; fiat_currency only sends when
|
|
||||||
// fiat is on (no point persisting a rail-currency the backend won't use).
|
|
||||||
const acceptsFiat = formValues.payment_methods.includes("fiat");
|
|
||||||
eventData.allow_fiat = acceptsFiat;
|
|
||||||
if (acceptsFiat && formValues.fiat_currency) {
|
|
||||||
eventData.fiat_currency = formValues.fiat_currency;
|
|
||||||
}
|
|
||||||
if (formValues.amount_tickets !== undefined)
|
|
||||||
eventData.amount_tickets = formValues.amount_tickets;
|
|
||||||
if (formValues.price_per_ticket)
|
|
||||||
eventData.price_per_ticket = formValues.price_per_ticket;
|
|
||||||
if (selectedCategories.value.length > 0)
|
|
||||||
eventData.categories = selectedCategories.value;
|
|
||||||
|
|
||||||
// Notification config + promo codes go inside the `extra` envelope.
|
|
||||||
// On edit overlay onto the existing event.extra so fields only the
|
|
||||||
// LNbits admin UI sets (conditional, min_tickets) survive the
|
|
||||||
// round-trip. promo_codes is always sent explicitly: what the
|
|
||||||
// organizer sees in the editor is the truth, an empty list clears.
|
|
||||||
// Ticket waves. The price / capacity / currency fields above ARE the
|
|
||||||
// primary wave, so they have to be written INTO it: the backend
|
|
||||||
// derives the event-level values FROM the waves on every write
|
|
||||||
// (`sync_event_ticket_waves`), so sending a changed amount_tickets
|
|
||||||
// beside an unchanged wave list leaves the wave's old number winning
|
|
||||||
// and the organizer's edit is silently discarded.
|
|
||||||
// Seeded so a NEW event's implied primary wave matches what the
|
|
||||||
// backend would synthesize. `create_event` stamps `time` and derives
|
|
||||||
// the wave window from it plus the closing/end/start dates; without
|
|
||||||
// the same inputs here the wave lands with empty date strings, which
|
|
||||||
// the backend's date parse throws on for every later read.
|
|
||||||
const storedWaves = ensureTicketWaves({
|
|
||||||
...(props.event ?? {}),
|
|
||||||
time: props.event?.time ?? new Date().toISOString(),
|
|
||||||
event_start_date: eventData.event_start_date,
|
|
||||||
event_end_date: eventData.event_end_date ?? null,
|
|
||||||
closing_date: props.event?.closing_date ?? null,
|
|
||||||
extra: props.event?.extra ?? null,
|
|
||||||
});
|
|
||||||
const [primaryWave] = applyFormToPrimaryWave(storedWaves, {
|
|
||||||
closing_date: eventData.closing_date,
|
|
||||||
currency: formValues.currency,
|
|
||||||
allow_fiat: formValues.payment_methods.includes("fiat"),
|
|
||||||
fiat_currency: formValues.fiat_currency,
|
|
||||||
amount_tickets: formValues.amount_tickets,
|
|
||||||
price_per_ticket: formValues.price_per_ticket ?? 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Notification config + promo codes go inside the `extra` envelope.
|
|
||||||
// On edit overlay onto the existing event.extra so fields only the
|
|
||||||
// LNbits admin UI sets (conditional, min_tickets) survive the
|
|
||||||
// round-trip. promo_codes is always sent explicitly: what the
|
|
||||||
// organizer sees in the editor is the truth, an empty list clears.
|
|
||||||
eventData.extra = {
|
|
||||||
...(props.event?.extra ?? {}),
|
|
||||||
payment_methods: formValues.payment_methods,
|
|
||||||
email_notifications: formValues.email_notifications,
|
|
||||||
nostr_notifications: formValues.nostr_notifications,
|
|
||||||
notification_subject: formValues.notification_subject,
|
|
||||||
notification_body: formValues.notification_body,
|
|
||||||
promo_codes: toWirePromoCodes(promoRows.value),
|
|
||||||
ticket_waves: [primaryWave, ...extraWaves.value],
|
|
||||||
};
|
|
||||||
|
|
||||||
if (isEditMode.value) {
|
|
||||||
if (!props.onUpdateEvent || !props.event?.id) {
|
|
||||||
toastService.error("Update handler missing");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await props.onUpdateEvent(props.event.id, eventData);
|
|
||||||
toastService.success(
|
|
||||||
willLandInPending.value
|
|
||||||
? "Updated — awaiting re-approval. Hidden from the public feed until reviewed."
|
|
||||||
: "Event updated!",
|
|
||||||
);
|
|
||||||
emit("event-updated");
|
|
||||||
} else {
|
|
||||||
if (!props.onCreateEvent) {
|
|
||||||
toastService.error("Create handler missing");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await props.onCreateEvent(eventData);
|
|
||||||
toastService.success(
|
|
||||||
willLandInPending.value
|
|
||||||
? "Submitted! Awaiting admin approval — your draft is visible on your feed with a Pending badge."
|
|
||||||
: "Event submitted!",
|
|
||||||
);
|
|
||||||
emit("event-created");
|
|
||||||
}
|
|
||||||
|
|
||||||
resetForm();
|
|
||||||
selectedCategories.value = [];
|
|
||||||
promoRows.value = [];
|
|
||||||
extraWaves.value = [];
|
|
||||||
knownWaveIds.value = [];
|
|
||||||
bannerImages.value = [];
|
|
||||||
emit("update:open", false);
|
|
||||||
} catch (error) {
|
|
||||||
const errorMessage =
|
|
||||||
error instanceof Error
|
|
||||||
? error.message
|
|
||||||
: isEditMode.value
|
|
||||||
? "Failed to update event"
|
|
||||||
: "Failed to create event";
|
|
||||||
toastService.error(errorMessage);
|
|
||||||
} finally {
|
|
||||||
isLoading.value = false;
|
|
||||||
}
|
}
|
||||||
},
|
|
||||||
// vee-validate blocks the submit when the schema fails. Say WHY: a field
|
resetForm();
|
||||||
// whose FormMessage sits inside a closed section — or is not rendered at
|
selectedCategories.value = [];
|
||||||
// all — otherwise leaves a dead button and no explanation, which is the
|
promoRows.value = [];
|
||||||
// dead end disabling the button used to create.
|
extraWaves.value = [];
|
||||||
({ errors }) => {
|
knownWaveIds.value = [];
|
||||||
const fields = Object.keys(errors);
|
bannerImages.value = [];
|
||||||
fields.forEach(revealSectionFor);
|
emit("update:open", false);
|
||||||
const named = fields.map((f) => FIELD_LABELS[f] ?? f);
|
} catch (error) {
|
||||||
toastService.error(
|
const errorMessage =
|
||||||
named.length
|
error instanceof Error
|
||||||
? `Check ${named.join(", ")}`
|
? error.message
|
||||||
: "Some details still need attention",
|
: isEditMode.value
|
||||||
);
|
? "Failed to update event"
|
||||||
const first = fields[0];
|
: "Failed to create event";
|
||||||
if (first) {
|
toastService.error(errorMessage);
|
||||||
document
|
} finally {
|
||||||
.querySelector(`[name="${first}"], #${CSS.escape(first)}`)
|
isLoading.value = false;
|
||||||
?.scrollIntoView({ behavior: "smooth", block: "center" });
|
}
|
||||||
}
|
});
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleOpenChange = (open: boolean) => {
|
const handleOpenChange = (open: boolean) => {
|
||||||
if (!open && !isLoading.value) {
|
if (!open && !isLoading.value) {
|
||||||
|
|
@ -1237,17 +1176,10 @@ const handleOpenChange = (open: boolean) => {
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<!-- Disabled only while a submit is genuinely in flight, NOT on
|
|
||||||
validity. vee-validate's `meta.valid` is false before the form
|
|
||||||
has ever validated, counts disabled and conditionally rendered
|
|
||||||
fields against you, and is documented to go stale
|
|
||||||
(logaretm/vee-validate#4630). Gating on it produced a dead
|
|
||||||
button with no visible reason; let the click through and the
|
|
||||||
invalid handler says what is wrong. -->
|
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
class="w-full sm:w-auto"
|
class="w-full sm:w-auto"
|
||||||
:disabled="isLoading"
|
:disabled="isLoading || !isFormValid"
|
||||||
>
|
>
|
||||||
<Loader2 v-if="isLoading" class="w-4 h-4 mr-2 animate-spin" />
|
<Loader2 v-if="isLoading" class="w-4 h-4 mr-2 animate-spin" />
|
||||||
{{
|
{{
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue