feat: [frontend] Update the adventure page, show past entries and future ones
Some checks are pending
/ test (push) Waiting to run

This commit is contained in:
wilson 2026-05-06 22:51:55 +01:00
parent 1b54536647
commit fac5d26220
8 changed files with 792 additions and 61 deletions

View file

@ -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);
}

View file

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

View file

@ -1,18 +1,92 @@
<script lang="ts">
import type { PageProps } from './$types';
import LatestEntry from './LatestEntry.svelte';
import PreviousEntries from './PreviousEntries.svelte';
const { data }: PageProps = $props();
const { data, params }: PageProps = $props();
const latestEntry = $derived(data.entries[data.entries.length - 1]);
const previousEntries = $derived.by(() => {
const allEntries = data.entries ?? [];
return allEntries.slice(0, -1).map((entry, index) => {
const nextEntry = allEntries[index + 1];
return {
id: entry.id,
text: entry.story_text!,
possibleChoices: (entry.possible_choices ?? []).map((choice) => ({
id: choice.id,
text: choice.text,
isSelected: nextEntry?.generated_from_choice_id === choice.id
}))
};
});
});
</script>
<div class="app-page">
<h1 class="page-title">{data.title}</h1>
<div class="adventure-page">
<header class="adventure-page__header">
<p class="adventure-page__kicker">Choose your own adventure</p>
<h1 class="adventure-page__title">{data.title}</h1>
</header>
<PreviousEntries entries={previousEntries} />
{#if latestEntry}
<LatestEntry sourceText={latestEntry.story_text} translationText={latestEntry.translation} />
<LatestEntry
sourceText={latestEntry.story_text}
translationText={latestEntry.translation}
audioUrl={latestEntry.audio_url!}
nextStepsOptions={data.choices.map((choice) => ({
label: choice.text,
id: choice.id
}))}
adventureId={params.id}
/>
{/if}
</div>
<style>
.adventure-page {
max-width: 96rem;
margin: 0 auto;
padding: var(--space-16) clamp(var(--space-3), 8vw, 7rem) var(--space-10)
clamp(var(--space-3), 5vw, 4.5rem);
display: grid;
gap: var(--space-6);
background-color: var(--color-surface);
}
.adventure-page__header {
max-width: 64ch;
}
.adventure-page__kicker {
margin: 0;
font-family: var(--font-label);
font-size: var(--text-label-md);
font-weight: var(--weight-medium);
letter-spacing: var(--tracking-wide);
text-transform: uppercase;
color: color-mix(in srgb, var(--color-on-surface) 72%, transparent);
}
.adventure-page__title {
margin: var(--space-2) 0 0;
font-family: var(--font-display);
font-size: clamp(2rem, 1.75rem + 1.9vw, var(--text-display-lg));
font-weight: var(--weight-semibold);
line-height: 1.04;
letter-spacing: var(--tracking-tight);
color: var(--color-on-surface);
text-wrap: balance;
}
@media (max-width: 56rem) {
.adventure-page {
padding: var(--space-8) var(--space-3) var(--space-8);
gap: var(--space-5);
}
}
</style>

View file

@ -1,18 +1,27 @@
<script lang="ts">
import NextSteps from './NextSteps.svelte';
import { selectNextStep } from './selectNextStep.remote';
type Props = {
adventureId: string;
sourceText: string | null | undefined;
translationText: string | null | undefined;
audioUrl: string;
nextStepsOptions: { label: string; id: string }[];
};
const { sourceText, translationText }: Props = $props();
const { adventureId, sourceText, translationText, audioUrl, nextStepsOptions }: Props = $props();
const sourceParagraphs = $derived.by(() => toParagraphs(sourceText));
const translationParagraphs = $derived.by(() => toParagraphs(translationText));
let sourcePane: HTMLDivElement | undefined;
let translationPane: HTMLDivElement | undefined;
let suppressSourceScroll = false;
let suppressTranslationScroll = false;
let lastClickedParagraphIndex: number | null = $state(null);
let sourcePane = $state<HTMLDivElement | undefined>();
let translationPane = $state<HTMLDivElement | undefined>();
let suppressSourceScroll = $state(false);
let suppressTranslationScroll = $state(false);
let translationVisible = $state(false);
let translationTimer: ReturnType<typeof setTimeout> | null = null;
function toParagraphs(text: string | null | undefined): string[] {
return (text ?? '')
@ -55,50 +64,158 @@
suppressSourceScroll = true;
syncScrollPosition(translationPane, sourcePane);
}
function showTranslation() {
translationVisible = true;
if (translationTimer !== null) {
clearTimeout(translationTimer);
}
translationTimer = setTimeout(() => {
translationVisible = false;
translationTimer = null;
}, 20000);
}
function handleParagraphClicked(paragraphIndex: number) {
lastClickedParagraphIndex = paragraphIndex;
}
async function handleNextStepSelect(optionId: string) {
const result = await selectNextStep({ adventureId, possibleChoiceId: optionId });
console.log({ result });
}
</script>
<section class="latest-entry">
<div class="pane">
<div class="latest-entry__pane-body" bind:this={sourcePane} onscroll={handleSourceScroll}>
{#each sourceParagraphs as paragraph, index (index)}
<p class="paragraph" data-paragraph-index={index} data-language="target">
{paragraph}
</p>
{/each}
<section class="latest-story" aria-label="Current story entry">
<header class="latest-story__header">
<div class="latest-story__title-group">
<p class="latest-story__kicker">Current entry</p>
<h2 class="latest-story__title">Now reading</h2>
</div>
</div>
<div class="pane translation">
<header class="translation-header">
<p class="eyebrow">Translation</p>
</header>
<div class="audio-dock" aria-label="Listening controls">
<p class="audio-dock__label">Listen</p>
<audio class="audio-dock__player" controls preload="metadata">
<source src={audioUrl} type="audio/wav" />
</audio>
</div>
</header>
<div
class="latest-entry__pane-body"
bind:this={translationPane}
onscroll={handleTranslationScroll}
>
{#each translationParagraphs as paragraph, index (index)}
<p class="paragraph" data-paragraph-index={index} data-language="source">
{paragraph}
</p>
{/each}
<div class="latest-entry">
<div class="pane source-pane">
<div class="latest-entry__pane-body" bind:this={sourcePane} onscroll={handleSourceScroll}>
{#each sourceParagraphs as paragraph, index (index)}
<p
class="paragraph"
class:active={lastClickedParagraphIndex === index}
data-paragraph-index={index}
data-language="source"
onclick={() => handleParagraphClicked(index)}
>
{paragraph}
</p>
{/each}
</div>
</div>
<div class="pane translation-pane" data-visible={translationVisible}>
<header class="translation-header">
<p class="translation-header__label">Translation</p>
<button class="dict-toggle" onclick={showTranslation}>
<span class="dict-toggle-label">Reveal for 20 seconds</span>
</button>
</header>
{#if translationVisible}
<div
class="latest-entry__pane-body"
bind:this={translationPane}
onscroll={handleTranslationScroll}
>
{#each translationParagraphs as paragraph, index (index)}
<p
class="paragraph"
class:active={lastClickedParagraphIndex === index}
data-paragraph-index={index}
data-language="translation"
onclick={() => handleParagraphClicked(index)}
>
{paragraph}
</p>
{/each}
</div>
{/if}
</div>
</div>
</section>
<NextSteps options={nextStepsOptions} onSelect={handleNextStepSelect} />
<style>
.latest-entry {
--latest-entry-border: color-mix(in srgb, var(--colour-outline-variant) 32%, transparent);
--latest-entry-divider: color-mix(in srgb, var(--colour-outline-variant) 18%, transparent);
--latest-entry-pane-height: clamp(20rem, 90vh, 50rem);
.latest-story {
--latest-entry-pane-height: clamp(20rem, 84vh, 48rem);
--latest-entry-pane-padding: var(--space-3);
--latest-entry-pane-shadow: var(--shadow-tonal-sm);
padding: var(--space-3);
border-radius: var(--radius-xl);
background-color: var(--color-surface-container-low);
}
.latest-story__header {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(16rem, 24rem);
gap: var(--space-4);
align-items: end;
margin-bottom: var(--space-4);
padding: var(--space-1) var(--space-1) var(--space-2);
}
.latest-story__kicker {
margin: 0;
font-family: var(--font-label);
font-size: var(--text-label-md);
font-weight: var(--weight-medium);
letter-spacing: var(--tracking-wide);
text-transform: uppercase;
color: color-mix(in srgb, var(--color-on-surface) 72%, transparent);
}
.latest-story__title {
margin: var(--space-1) 0 0;
font-family: var(--font-display);
font-size: clamp(1.5rem, 1.32rem + 1vw, 2.2rem);
line-height: var(--leading-tight);
color: var(--color-on-surface);
}
.audio-dock {
padding: var(--space-3);
border-radius: var(--radius-lg);
background-color: var(--color-surface-container-lowest);
display: grid;
gap: var(--space-2);
}
.audio-dock__label {
margin: 0;
font-family: var(--font-label);
font-size: var(--text-label-md);
font-weight: var(--weight-medium);
letter-spacing: var(--tracking-wide);
text-transform: uppercase;
color: color-mix(in srgb, var(--color-on-surface) 72%, transparent);
}
.audio-dock__player {
width: 100%;
height: 2.5rem;
accent-color: var(--color-primary);
}
.latest-entry {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
gap: var(--space-2);
gap: var(--space-3);
align-items: stretch;
}
@ -108,35 +225,78 @@
min-height: 0;
height: var(--latest-entry-pane-height);
overflow: hidden;
border-radius: var(--radius-lg);
}
.pane.translation {
background:
linear-gradient(
180deg,
color-mix(in srgb, var(--color-primary-container) 22%, transparent),
transparent 24%
),
var(--latest-entry-pane-surface);
border: 1px solid var(--latest-entry-border);
border-radius: var(--radius-xl);
box-shadow: var(--latest-entry-pane-shadow);
.source-pane {
background-color: var(--color-surface-container-lowest);
}
.translation-pane {
transition: height var(--duration-normal) var(--ease-standard);
background-color: var(--color-surface-container);
height: auto;
}
.translation-pane[data-visible='true'] {
height: var(--latest-entry-pane-height);
}
.translation-pane[data-visible='false'] {
height: auto;
}
.translation-header {
display: flex;
flex-direction: column;
gap: var(--space-1);
justify-content: space-between;
align-items: center;
gap: var(--space-2);
padding: var(--latest-entry-pane-padding);
padding-bottom: var(--space-4);
padding-bottom: var(--space-2);
}
.translation-header .eyebrow {
.translation-header__label {
margin: 0;
font-family: var(--font-label);
font-size: var(--text-label-md);
font-weight: var(--weight-medium);
letter-spacing: var(--tracking-wide);
text-transform: uppercase;
color: color-mix(in srgb, var(--color-on-surface) 70%, transparent);
}
.dict-toggle {
display: inline-flex;
align-items: center;
justify-content: center;
padding: var(--space-2) var(--space-2);
background-color: var(--color-primary);
color: var(--color-on-primary);
border: none;
border-radius: var(--radius-md);
font-family: var(--font-label);
font-size: var(--text-label-md);
font-weight: var(--weight-medium);
letter-spacing: var(--tracking-wider);
text-transform: uppercase;
cursor: pointer;
transition:
opacity var(--duration-fast) var(--ease-standard),
transform var(--duration-fast) var(--ease-standard);
}
.dict-toggle:hover {
opacity: 0.88;
transform: translateY(-1px);
}
.dict-toggle:active {
opacity: 0.75;
transform: translateY(0);
}
.dict-toggle-label {
white-space: nowrap;
}
.latest-entry__pane-body {
@ -146,6 +306,18 @@
padding: var(--latest-entry-pane-padding);
scrollbar-gutter: stable;
scroll-behavior: auto;
animation: slideIn var(--duration-normal) var(--ease-standard) forwards;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(-8px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.latest-entry__pane-body::-webkit-scrollbar {
@ -157,17 +329,25 @@
}
.latest-entry__pane-body::-webkit-scrollbar-thumb {
background-color: color-mix(in srgb, var(--color-primary) 24%, transparent);
background-color: color-mix(in srgb, var(--colour-yellow-300) 24%, transparent);
border: 0.1875rem solid transparent;
border-radius: var(--radius-full);
background-clip: content-box;
}
.paragraph {
font-size: var(--text-body-xl);
line-height: var(--leading-loose);
font-size: clamp(1.1rem, 1rem + 0.35vw, 1.35rem);
line-height: var(--leading-relaxed);
color: var(--color-on-surface);
text-wrap: pretty;
cursor: pointer;
transition: background-color var(--duration-fast) var(--ease-standard);
}
.paragraph.active {
background-color: color-mix(in srgb, var(--color-primary-container) 56%, transparent);
border-radius: var(--radius-md);
}
.paragraph + .paragraph {
@ -175,7 +355,33 @@
padding-top: var(--space-3);
}
.pane .paragraph {
font-size: clamp(1.2rem, 1rem + 0.35vw, 1.45rem);
@media (max-width: 64rem) {
.latest-story__header {
grid-template-columns: 1fr;
align-items: stretch;
}
.audio-dock {
max-width: 26rem;
}
}
@media (max-width: 56rem) {
.latest-story {
padding: var(--space-2);
}
.latest-entry {
grid-template-columns: 1fr;
}
.pane {
height: clamp(16rem, 62vh, 30rem);
}
.translation-header {
flex-direction: column;
align-items: flex-start;
}
}
</style>

View file

@ -0,0 +1,173 @@
<script lang="ts">
type Props = {
options: { label: string; id: string }[];
onSelect: (optionId: string) => void;
};
const { options, onSelect }: Props = $props();
let isSubmitting = $state(false);
const handleOptionSelect = async (optionId: string) => {
if (isSubmitting) {
return;
}
isSubmitting = true;
try {
await onSelect(optionId);
} finally {
isSubmitting = false;
}
};
</script>
<section class="next-steps" aria-label="Choose what happens next">
<header class="next-steps__header">
<p class="next-steps__kicker">Choose your path</p>
<h2 class="next-steps__title">What happens next?</h2>
</header>
<ol class="next-steps__list">
{#each options as option, index (option.id)}
<li class="next-steps__item">
<button
class="next-steps__button"
onclick={() => handleOptionSelect(option.id)}
disabled={isSubmitting}
>
<span class="next-steps__index" aria-hidden="true"
>{String(index + 1).padStart(2, '0')}</span
>
<span class="next-steps__label">{option.label}</span>
<span class="next-steps__meta" aria-hidden="true">Choose</span>
</button>
</li>
{/each}
</ol>
</section>
<style>
.next-steps {
--next-steps-surface: var(--color-surface-container-low);
--next-steps-surface-hover: var(--color-surface-container-lowest);
max-width: 72ch;
margin: var(--space-6) auto 0;
padding: clamp(1rem, 0.9rem + 0.9vw, 1.8rem);
border-radius: var(--radius-xl);
background-color: var(--next-steps-surface);
}
.next-steps__header {
margin-bottom: var(--space-4);
}
.next-steps__kicker {
margin: 0;
font-family: var(--font-label);
font-size: var(--text-label-md);
letter-spacing: 0.08em;
text-transform: uppercase;
color: color-mix(in srgb, var(--color-on-surface) 70%, transparent);
}
.next-steps__title {
margin: var(--space-1) 0 0;
font-family: var(--font-display);
font-size: clamp(1.35rem, 1.2rem + 0.9vw, 2rem);
line-height: 1.15;
color: var(--color-on-surface);
}
.next-steps__list {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
gap: var(--space-3);
}
.next-steps__item {
margin: 0;
}
.next-steps__button {
width: 100%;
display: grid;
grid-template-columns: auto 1fr auto;
align-items: center;
gap: var(--space-3);
padding: clamp(0.9rem, 0.8rem + 0.5vw, 1.2rem);
border: none;
border-radius: var(--radius-lg);
background: var(--color-surface-container);
text-align: left;
cursor: pointer;
transition:
transform var(--duration-fast) var(--ease-standard),
background-color var(--duration-fast) var(--ease-standard);
}
.next-steps__index {
font-family: var(--font-label);
font-size: var(--text-label-md);
font-weight: var(--weight-semibold);
letter-spacing: 0.08em;
color: color-mix(in srgb, var(--color-on-surface) 72%, transparent);
}
.next-steps__label {
font-family: var(--font-body);
font-size: clamp(1rem, 0.95rem + 0.3vw, 1.18rem);
line-height: 1.35;
color: var(--color-on-surface);
text-wrap: pretty;
}
.next-steps__meta {
font-family: var(--font-label);
font-size: var(--text-label-md);
font-weight: var(--weight-medium);
letter-spacing: var(--tracking-wide);
text-transform: uppercase;
line-height: 1;
color: color-mix(in srgb, var(--color-primary) 72%, var(--color-on-surface));
}
.next-steps__button:hover:enabled {
background: var(--next-steps-surface-hover);
transform: translateY(-1px);
}
.next-steps__button:hover:enabled .next-steps__meta,
.next-steps__button:focus-visible .next-steps__meta {
color: var(--color-primary);
}
.next-steps__button:focus-visible {
outline: 2px solid color-mix(in srgb, var(--colour-outline-variant) 20%, transparent);
outline-offset: 2px;
}
.next-steps__button:disabled {
opacity: 0.7;
cursor: progress;
}
@media (max-width: 42rem) {
.next-steps {
margin-top: var(--space-4);
padding: var(--space-3);
}
.next-steps__button {
grid-template-columns: auto 1fr;
}
.next-steps__meta {
display: none;
}
}
</style>

View file

@ -0,0 +1,220 @@
<script lang="ts">
type Props = {
entries: {
id: string;
text: string;
possibleChoices: {
id: string;
text: string;
isSelected: boolean;
}[];
}[];
};
const { entries }: Props = $props();
function toParagraphs(text: string): string[] {
return text
.split(/\n\s*\n/g)
.map((paragraph) => paragraph.trim())
.filter(Boolean);
}
</script>
{#if entries.length > 0}
<section class="previous-entries" aria-label="Previous story entries">
<header class="previous-entries__header">
<h2 class="previous-entries__title">Previous entries</h2>
</header>
<ol class="previous-entries__list">
{#each entries as entry, index (entry.id)}
<li class="previous-entries__item">
<article class="entry-card" aria-label={`Entry ${index + 1}`}>
<header class="entry-card__header">
<p class="entry-card__index">Entry {String(index + 1).padStart(2, '0')}</p>
</header>
<div class="entry-card__body">
{#each toParagraphs(entry.text) as paragraph, paragraphIndex (paragraphIndex)}
<p class="entry-card__paragraph">{paragraph}</p>
{/each}
</div>
{#if entry.possibleChoices.length > 0}
<footer class="entry-card__choices">
<p class="entry-card__choices-label">Possible choices</p>
<ul class="entry-card__choices-list" aria-label="Choices for this entry">
{#each entry.possibleChoices as choice, choiceIndex (choice.id)}
<li class="entry-card__choice" class:entry-card__choice--selected={choice.isSelected}>
<span class="entry-card__choice-index" aria-hidden="true"
>{String(choiceIndex + 1).padStart(2, '0')}</span
>
<span class="entry-card__choice-text">{choice.text}</span>
{#if choice.isSelected}
<span class="entry-card__choice-state">Selected</span>
{/if}
</li>
{/each}
</ul>
</footer>
{/if}
</article>
</li>
{/each}
</ol>
</section>
{/if}
<style>
.previous-entries {
--previous-surface: var(--color-surface-container-low);
--previous-surface-elevated: var(--color-surface-container-lowest);
max-width: 88rem;
border-radius: var(--radius-xl);
}
.previous-entries__header {
margin-bottom: var(--space-4);
}
.previous-entries__title {
margin: var(--space-1) 0 0;
font-family: var(--font-display);
font-size: clamp(1.3rem, 1.12rem + 0.85vw, 1.9rem);
line-height: 1.15;
color: var(--color-on-surface);
}
.previous-entries__list {
list-style: none;
padding: 0;
margin: 0;
display: grid;
gap: var(--space-3);
grid-template-columns: 1fr;
}
.entry-card {
padding: clamp(0.9rem, 0.8rem + 0.7vw, 1.4rem);
border-radius: var(--radius-lg);
background-color: var(--previous-surface-elevated);
}
.entry-card__header {
margin-bottom: var(--space-2);
}
.entry-card__index {
margin: 0;
font-family: var(--font-label);
font-size: var(--text-label-md);
font-weight: var(--weight-semibold);
letter-spacing: 0.08em;
text-transform: uppercase;
color: color-mix(in srgb, var(--color-on-surface) 72%, transparent);
}
.entry-card__body::-webkit-scrollbar {
width: 0.65rem;
}
.entry-card__body::-webkit-scrollbar-track {
background: transparent;
}
.entry-card__body::-webkit-scrollbar-thumb {
background-color: color-mix(in srgb, var(--colour-yellow-300) 26%, transparent);
border: 0.16rem solid transparent;
border-radius: var(--radius-full);
background-clip: content-box;
}
.entry-card__paragraph {
font-family: var(--font-body);
font-size: clamp(1rem, 0.97rem + 0.2vw, 1.12rem);
line-height: var(--leading-loose);
color: var(--color-on-surface);
text-wrap: pretty;
}
.entry-card__paragraph + .entry-card__paragraph {
margin-top: var(--space-3);
padding-top: var(--space-3);
}
.entry-card__choices {
margin-top: var(--space-3);
padding-top: var(--space-2);
}
.entry-card__choices-label {
margin: 0 0 var(--space-2);
font-family: var(--font-label);
font-size: var(--text-label-md);
font-weight: var(--weight-medium);
letter-spacing: var(--tracking-wide);
text-transform: uppercase;
color: color-mix(in srgb, var(--color-on-surface) 72%, transparent);
}
.entry-card__choices-list {
list-style: none;
margin: 0;
padding: 0;
display: grid;
gap: var(--space-2);
}
.entry-card__choice {
display: grid;
grid-template-columns: auto 1fr auto;
align-items: start;
gap: var(--space-2);
padding: var(--space-2);
border-radius: var(--radius-md);
}
.entry-card__choice-index {
font-family: var(--font-label);
font-size: var(--text-label-md);
font-weight: var(--weight-medium);
letter-spacing: var(--tracking-wide);
color: color-mix(in srgb, var(--color-on-surface) 62%, transparent);
}
.entry-card__choice-text {
font-family: var(--font-body);
font-size: var(--text-body-lg);
line-height: var(--leading-relaxed);
color: color-mix(in srgb, var(--color-on-surface) 84%, transparent);
text-wrap: pretty;
}
.entry-card__choice-state {
font-family: var(--font-label);
font-size: var(--text-label-md);
font-weight: var(--weight-medium);
letter-spacing: var(--tracking-wide);
text-transform: uppercase;
color: var(--color-primary);
}
.entry-card__choice--selected {
background-color: color-mix(in srgb, var(--color-primary-container) 56%, transparent);
}
.entry-card__choice--selected .entry-card__choice-text {
color: var(--color-on-surface);
}
@media (max-width: 42rem) {
.previous-entries {
padding: var(--space-2);
}
.entry-card {
padding: var(--space-3);
}
}
</style>

View file

@ -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;
}
);

View file

@ -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: '/',