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} diff --git a/components/frontend/src/routes/(app)/my/hackathon/[id]/timeline/manage/[phaseId]/edit/+page.server.ts b/components/frontend/src/routes/(app)/my/hackathon/[id]/timeline/manage/[phaseId]/edit/+page.server.ts index 56f0374e..8cf02a3a 100644 --- a/components/frontend/src/routes/(app)/my/hackathon/[id]/timeline/manage/[phaseId]/edit/+page.server.ts +++ b/components/frontend/src/routes/(app)/my/hackathon/[id]/timeline/manage/[phaseId]/edit/+page.server.ts @@ -48,6 +48,16 @@ export const load: PageServerLoad = async (event) => { return { hackathonId: hackathon.id, + // Arrived here straight from Add Phase, which redirects with `?added` because + // `Create` cannot store dates and this is where they get set. Carried in the + // URL rather than in state, so a phase legitimately left undated is not told + // it was just created when it is reached from the list. + // + // The form posts to `?/save`, which replaces the whole query string — so the + // notice is gone by the second render, failed save included. That is the + // wanted behaviour rather than a limitation: it answers "why am I on Edit", + // and after submitting once nobody is asking. + justAdded: event.url.searchParams.has("added"), phase: { id: result.phase.id, name: result.phase.name, diff --git a/components/frontend/src/routes/(app)/my/hackathon/[id]/timeline/manage/[phaseId]/edit/+page.svelte b/components/frontend/src/routes/(app)/my/hackathon/[id]/timeline/manage/[phaseId]/edit/+page.svelte index 3fe26460..e3a6d748 100644 --- a/components/frontend/src/routes/(app)/my/hackathon/[id]/timeline/manage/[phaseId]/edit/+page.svelte +++ b/components/frontend/src/routes/(app)/my/hackathon/[id]/timeline/manage/[phaseId]/edit/+page.svelte @@ -25,8 +25,18 @@ ← Back to Manage Timeline

Edit Phase

+

- 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 @@

Add Phase

- 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.