diff --git a/components/frontend/src/lib/components/hackathon/CurrentStateCard.svelte b/components/frontend/src/lib/components/hackathon/CurrentStateCard.svelte
index 2d1c42cb..38257f45 100644
--- a/components/frontend/src/lib/components/hackathon/CurrentStateCard.svelte
+++ b/components/frontend/src/lib/components/hackathon/CurrentStateCard.svelte
@@ -76,15 +76,41 @@
// there is one at all. `new Date()` is only ever compared against, never
// rendered — the visible clock lives in `Countdown`, which is what keeps this
// from becoming a hydration mismatch.
- const boundary = $derived(nextBoundary(currentPhase, nextPhase, new Date()));
+ const boundary = $derived(nextBoundary(currentPhase, nextPhase, new Date(), declared));
// The two conditions deliberately kept out of the banner (see `stateAlerts`):
// untidy rather than blocking, so an organiser reads them here without being
// interrupted by them elsewhere.
+ //
+ // Both stay organiser-only, because both are prompts to act. A participant
+ // reading "no phase is declared current" learns nothing they can use — with no
+ // declaration the dates decide, which is what a timeline looks like anyway.
const noCurrentPhase = $derived(organiserVoice && hasState && currentPhase === null);
const phaseEnded = $derived(
organiserVoice && currentPhase?.endsAt !== undefined && currentPhase.endsAt < new Date(),
);
+
+ // The participant's version of the same fact, and the one thing manual mode
+ // owes them: with a phase declared current the dates on this card and on the
+ // timeline have stopped deciding anything, so a window that disagrees with the
+ // clock is not a mistake and not something to plan around.
+ //
+ // Both directions, unlike `phaseEnded` above: an organiser who declares a
+ // phase early leaves a "current" phase whose start is still days off, which
+ // reads just as oddly as one held past its end. Suppressing the countdown
+ // (see `nextBoundary`) removes the false statement; this is what replaces it.
+ //
+ // A declared phase whose window still covers now says nothing — the dates and
+ // the declaration agree, and there is nothing to explain.
+ const outsideWindow = $derived.by(() => {
+ if (!declared || !currentPhase) return false;
+ const now = new Date();
+ return (
+ (currentPhase.endsAt !== undefined && currentPhase.endsAt < now) ||
+ (currentPhase.startsAt !== undefined && currentPhase.startsAt > now)
+ );
+ });
+ const datesAreAGuide = $derived(!organiserVoice && outsideWindow);
+ {#if phase.id === data.nextPhaseId && phase.startsAt && !data.declared}
- Changes are visible to participants immediately. + {#if data.justAdded} + {data.phase.name} was added. + Give it a start and an end to place it on the timeline — or save it as + it is and schedule it later. + {:else} + Changes are visible to participants immediately. + {/if}
diff --git a/components/frontend/src/routes/(app)/my/hackathon/[id]/timeline/manage/new/+page.server.ts b/components/frontend/src/routes/(app)/my/hackathon/[id]/timeline/manage/new/+page.server.ts index 8f2ee279..edafd517 100644 --- a/components/frontend/src/routes/(app)/my/hackathon/[id]/timeline/manage/new/+page.server.ts +++ b/components/frontend/src/routes/(app)/my/hackathon/[id]/timeline/manage/new/+page.server.ts @@ -35,8 +35,9 @@ export const actions: Actions = { } const values = parsed.values + let created try { - await phase.create({ + created = await phase.create({ hackathonId: event.params.id, name: values.name, description: values.description, @@ -72,6 +73,21 @@ export const actions: Actions = { throw e } - redirect(303, resolve(`/my/hackathon/${event.params.id}/timeline/manage`)) + // Straight to the new phase's Edit page rather than back to the list, because + // `Create` discards dates (see the TODO above) and Edit is therefore the only + // place a phase can be scheduled at all. Landing an organizer on the form that + // finishes the job beats a list row reading "No dates set" that they have to + // notice and click. `?added` is what lets that page say why they are there. + // + // Falls back to the list if the RPC somehow reports no id: an organizer looking + // at their new phase in the list is a worse outcome than a broken Edit URL. + redirect( + 303, + created.phaseId + ? `${resolve( + `/my/hackathon/${event.params.id}/timeline/manage/${created.phaseId}/edit`, + )}?added` + : resolve(`/my/hackathon/${event.params.id}/timeline/manage`), + ) }, } diff --git a/components/frontend/src/routes/(app)/my/hackathon/[id]/timeline/manage/new/+page.svelte b/components/frontend/src/routes/(app)/my/hackathon/[id]/timeline/manage/new/+page.svelte index bc260c89..752ca2b2 100644 --- a/components/frontend/src/routes/(app)/my/hackathon/[id]/timeline/manage/new/+page.svelte +++ b/components/frontend/src/routes/(app)/my/hackathon/[id]/timeline/manage/new/+page.svelte @@ -30,8 +30,9 @@- Participants see the phase as soon as it is saved. Undated phases sort to the - top of the timeline until they are scheduled. + Participants see the phase as soon as it is saved, and the next step is + scheduling it — this form hands straight over to it. Undated phases sort to + the top of the timeline until they are scheduled.