From fac5d2622010005ef0b32bc7e1e0d468abe946c0 Mon Sep 17 00:00:00 2001 From: wilson Date: Wed, 6 May 2026 22:51:55 +0100 Subject: [PATCH] feat: [frontend] Update the adventure page, show past entries and future ones --- frontend/src/app.css | 22 +- frontend/src/hooks.server.ts | 1 + .../routes/app/adventures/[id]/+page.svelte | 82 ++++- .../app/adventures/[id]/LatestEntry.svelte | 314 +++++++++++++++--- .../app/adventures/[id]/NextSteps.svelte | 173 ++++++++++ .../adventures/[id]/PreviousEntries.svelte | 220 ++++++++++++ .../adventures/[id]/selectNextStep.remote.ts | 33 ++ frontend/src/routes/login/+page.server.ts | 8 +- 8 files changed, 792 insertions(+), 61 deletions(-) create mode 100644 frontend/src/routes/app/adventures/[id]/NextSteps.svelte create mode 100644 frontend/src/routes/app/adventures/[id]/PreviousEntries.svelte create mode 100644 frontend/src/routes/app/adventures/[id]/selectNextStep.remote.ts diff --git a/frontend/src/app.css b/frontend/src/app.css index c359d48..68eda51 100644 --- a/frontend/src/app.css +++ b/frontend/src/app.css @@ -82,6 +82,19 @@ --colour-green-900: #1b5e20; --colour-green-950: #0b2f10; + /** Colour: Yellow palette, from Tailwind's yellow palette */ + --colour-yellow-50: #fffbeb; + --colour-yellow-100: #fef3c7; + --colour-yellow-200: #fde68a; + --colour-yellow-300: #fcd34d; + --colour-yellow-400: #fbbf24; + --colour-yellow-500: #f59e0b; + --colour-yellow-600: #d97706; + --colour-yellow-700: #b45309; + --colour-yellow-800: #92400e; + --colour-yellow-900: #78350f; + --colour-yellow-950: #451a03; + /* --- Color: On-Surface --- */ --color-on-surface: #2f342e; /* replaces pure black */ --color-on-surface-variant: #5c605b; @@ -126,7 +139,7 @@ --leading-relaxed: 1.6; /* "Digital Paper" body text minimum */ --leading-loose: 1.8; - /* --- Typography: Letter Spacing --- */ + /* --- Typography: Letter Spacing --- */; --tracking-tight: -0.025em; --tracking-normal: 0em; --tracking-wide: 0.05rem; /* label-md metadata */ @@ -305,7 +318,7 @@ body { background-color var(--duration-normal) var(--ease-standard); } -.btn-primary { +.btn-primary, .btn.primary { background-color: var(--color-primary); color: var(--color-on-primary); } @@ -626,3 +639,8 @@ LAYOUT: APP PAGE color: var(--color-on-surface); } } + +.app-page.full-bleed { + max-width: none; + padding: var(--space-0); +} diff --git a/frontend/src/hooks.server.ts b/frontend/src/hooks.server.ts index 61059da..635196f 100644 --- a/frontend/src/hooks.server.ts +++ b/frontend/src/hooks.server.ts @@ -32,6 +32,7 @@ export const handle: Handle = async ({ event, resolve }) => { } else { event.locals.isAdmin = false; console.log(`Not valid and no token`); + event.cookies.delete(COOKIE_NAME_AUTH_TOKEN, { path: '/' }); } const { pathname } = event.url; diff --git a/frontend/src/routes/app/adventures/[id]/+page.svelte b/frontend/src/routes/app/adventures/[id]/+page.svelte index dd64946..4bd7381 100644 --- a/frontend/src/routes/app/adventures/[id]/+page.svelte +++ b/frontend/src/routes/app/adventures/[id]/+page.svelte @@ -1,18 +1,92 @@ -
-

{data.title}

+
+
+

Choose your own adventure

+

{data.title}

+
+ + {#if latestEntry} - + ({ + label: choice.text, + id: choice.id + }))} + adventureId={params.id} + /> {/if}
diff --git a/frontend/src/routes/app/adventures/[id]/LatestEntry.svelte b/frontend/src/routes/app/adventures/[id]/LatestEntry.svelte index 880efdaf8..ff6c7b7 100644 --- a/frontend/src/routes/app/adventures/[id]/LatestEntry.svelte +++ b/frontend/src/routes/app/adventures/[id]/LatestEntry.svelte @@ -1,18 +1,27 @@ -
-
-
- {#each sourceParagraphs as paragraph, index (index)} -

- {paragraph} -

- {/each} +
+
+
+

Current entry

+

Now reading

-
-
-
-

Translation

-
+
+

Listen

+ +
+ -
- {#each translationParagraphs as paragraph, index (index)} -

- {paragraph} -

- {/each} +
+
+
+ {#each sourceParagraphs as paragraph, index (index)} +

handleParagraphClicked(index)} + > + {paragraph} +

+ {/each} +
+
+ +
+
+

Translation

+ +
+ + {#if translationVisible} +
+ {#each translationParagraphs as paragraph, index (index)} +

handleParagraphClicked(index)} + > + {paragraph} +

+ {/each} +
+ {/if}
+ + diff --git a/frontend/src/routes/app/adventures/[id]/NextSteps.svelte b/frontend/src/routes/app/adventures/[id]/NextSteps.svelte new file mode 100644 index 0000000..42561c4 --- /dev/null +++ b/frontend/src/routes/app/adventures/[id]/NextSteps.svelte @@ -0,0 +1,173 @@ + + +
+
+

Choose your path

+

What happens next?

+
+ +
    + {#each options as option, index (option.id)} +
  1. + +
  2. + {/each} +
+
+ + diff --git a/frontend/src/routes/app/adventures/[id]/PreviousEntries.svelte b/frontend/src/routes/app/adventures/[id]/PreviousEntries.svelte new file mode 100644 index 0000000..11b81fd --- /dev/null +++ b/frontend/src/routes/app/adventures/[id]/PreviousEntries.svelte @@ -0,0 +1,220 @@ + + +{#if entries.length > 0} +
+
+

Previous entries

+
+ +
    + {#each entries as entry, index (entry.id)} +
  1. +
    +
    +

    Entry {String(index + 1).padStart(2, '0')}

    +
    + +
    + {#each toParagraphs(entry.text) as paragraph, paragraphIndex (paragraphIndex)} +

    {paragraph}

    + {/each} +
    + + {#if entry.possibleChoices.length > 0} +
    +

    Possible choices

    +
      + {#each entry.possibleChoices as choice, choiceIndex (choice.id)} +
    • + + {choice.text} + {#if choice.isSelected} + Selected + {/if} +
    • + {/each} +
    +
    + {/if} +
    +
  2. + {/each} +
+
+{/if} + + diff --git a/frontend/src/routes/app/adventures/[id]/selectNextStep.remote.ts b/frontend/src/routes/app/adventures/[id]/selectNextStep.remote.ts new file mode 100644 index 0000000..b37fd6d --- /dev/null +++ b/frontend/src/routes/app/adventures/[id]/selectNextStep.remote.ts @@ -0,0 +1,33 @@ +import { command, getRequestEvent } from '$app/server'; +import { recordDecisionApiAdventuresAdventureIdDecisionsPost } from '@client'; +import * as v from 'valibot'; + +const selectNextStepSchema = v.object({ + adventureId: v.string(), + possibleChoiceId: v.string() +}); + +export const selectNextStep = command( + selectNextStepSchema, + async ({ adventureId, possibleChoiceId }) => { + const { locals } = getRequestEvent(); + const { error, response, data } = await recordDecisionApiAdventuresAdventureIdDecisionsPost({ + headers: { + Authorization: `Bearer ${locals.authToken}` + }, + path: { + adventure_id: adventureId + }, + body: { + choice_id: possibleChoiceId + } + }); + + if (error) { + console.error('Error recording decision:', error); + throw new Error('Failed to record decision'); + } + + return data; + } +); diff --git a/frontend/src/routes/login/+page.server.ts b/frontend/src/routes/login/+page.server.ts index 22f8324..dbb5a7a 100644 --- a/frontend/src/routes/login/+page.server.ts +++ b/frontend/src/routes/login/+page.server.ts @@ -18,7 +18,7 @@ export const actions = { const email = formData.get('email') as string; const password = formData.get('password') as string; - const { response, data } = await loginApiAuthLoginPost({ + const { response, data, error } = await loginApiAuthLoginPost({ headers: { 'Content-Type': 'application/json', Authorization: locals.authToken ? `Bearer ${locals.authToken}` : '' @@ -26,6 +26,12 @@ export const actions = { body: { email, password } }); + if (error) { + console.error(`Error logging in:`, { error }); + cookies.delete(COOKIE_NAME_AUTH_TOKEN, { path: '/' }); + return { success: false, error }; + } + if (response.status === 200 && data) { cookies.set(COOKIE_NAME_AUTH_TOKEN, data.access_token, { path: '/',