feat: [frontend] Create the parallel scroll for the latest entry

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
wilson 2026-05-04 11:46:27 +01:00
parent 17dc49482c
commit b91f6f81f8
5 changed files with 233 additions and 4 deletions

View file

@ -4,8 +4,7 @@ export const load: PageServerLoad = async ({ locals, url }) => {
let successMessage: null | string = null; let successMessage: null | string = null;
if (url.searchParams.get('created')) { if (url.searchParams.get('created')) {
successMessage = `Adventure created, check back in a few minutes`; successMessage = `Adventure created, check back in a few minutes`;
url.searchParams.delete('created'); url.searchParams.delete('created');
url.searchParams.
} }
return { return {
successMessage successMessage

View file

@ -23,8 +23,10 @@
{#each adventures as adventure (adventure.id)} {#each adventures as adventure (adventure.id)}
<div class="adventure-card"> <div class="adventure-card">
<h2>{adventure.title}</h2> <a href={resolve('/app/adventures/[id]', { id: adventure.id })}>
<p>{adventure.description}</p> <h2>{adventure.title}</h2>
<p>{adventure.description}</p>
</a>
</div> </div>
{/each} {/each}
</div> </div>

View file

@ -0,0 +1,29 @@
import { getAdventureBffAdventureAdventureIdGet } from '@client';
import type { PageServerLoad } from './$types';
import { error } from '@sveltejs/kit';
export const load: PageServerLoad = async ({ locals, params }) => {
const response = await getAdventureBffAdventureAdventureIdGet({
path: {
adventure_id: params.id
},
headers: {
Authorization: `Bearer ${locals.authToken}`
}
});
if (response.error) {
console.error(`Error fetching an adventure:`);
console.error({ error: response.error });
return error(400, `Error loading adventure`);
}
const { title, entries, current_entry_choices } = response.data;
response.data.entries.forEach((e) => console.log(e.story_text));
return {
title: title,
entries,
choices: current_entry_choices
};
};

View file

@ -0,0 +1,18 @@
<script lang="ts">
import type { PageProps } from './$types';
import LatestEntry from './LatestEntry.svelte';
const { data }: PageProps = $props();
const latestEntry = $derived(data.entries[data.entries.length - 1]);
</script>
<div class="app-page">
<h1 class="page-title">{data.title}</h1>
{#if latestEntry}
<LatestEntry sourceText={latestEntry.story_text} translationText={latestEntry.translation} />
{/if}
</div>
<style>
</style>

View file

@ -0,0 +1,181 @@
<script lang="ts">
type Props = {
sourceText: string | null | undefined;
translationText: string | null | undefined;
};
const { sourceText, translationText }: 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;
function toParagraphs(text: string | null | undefined): string[] {
return (text ?? '')
.split(/\n\s*\n/g)
.map((paragraph) => paragraph.trim())
.filter(Boolean);
}
function syncScrollPosition(
origin: HTMLDivElement | undefined,
target: HTMLDivElement | undefined
) {
if (!origin || !target) {
return;
}
const originRange = origin.scrollHeight - origin.clientHeight;
const targetRange = target.scrollHeight - target.clientHeight;
const progress = originRange <= 0 ? 0 : origin.scrollTop / originRange;
target.scrollTop = targetRange <= 0 ? 0 : progress * targetRange;
}
function handleSourceScroll() {
if (suppressSourceScroll) {
suppressSourceScroll = false;
return;
}
suppressTranslationScroll = true;
syncScrollPosition(sourcePane, translationPane);
}
function handleTranslationScroll() {
if (suppressTranslationScroll) {
suppressTranslationScroll = false;
return;
}
suppressSourceScroll = true;
syncScrollPosition(translationPane, sourcePane);
}
</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}
</div>
</div>
<div class="pane translation">
<header class="translation-header">
<p class="eyebrow">Translation</p>
</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>
</div>
</section>
<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-entry-pane-padding: var(--space-3);
--latest-entry-pane-shadow: var(--shadow-tonal-sm);
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
gap: var(--space-2);
align-items: stretch;
}
.pane {
display: flex;
flex-direction: column;
min-height: 0;
height: var(--latest-entry-pane-height);
overflow: hidden;
}
.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);
}
.translation-header {
display: flex;
flex-direction: column;
gap: var(--space-1);
padding: var(--latest-entry-pane-padding);
padding-bottom: var(--space-4);
}
.translation-header .eyebrow {
font-family: var(--font-label);
font-size: var(--text-label-md);
font-weight: var(--weight-medium);
letter-spacing: var(--tracking-wide);
text-transform: uppercase;
}
.latest-entry__pane-body {
flex: 1;
min-height: 0;
overflow-y: auto;
padding: var(--latest-entry-pane-padding);
scrollbar-gutter: stable;
scroll-behavior: auto;
}
.latest-entry__pane-body::-webkit-scrollbar {
width: 0.75rem;
}
.latest-entry__pane-body::-webkit-scrollbar-track {
background-color: transparent;
}
.latest-entry__pane-body::-webkit-scrollbar-thumb {
background-color: color-mix(in srgb, var(--color-primary) 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);
color: var(--color-on-surface);
text-wrap: pretty;
}
.paragraph + .paragraph {
margin-top: var(--space-3);
padding-top: var(--space-3);
}
.pane .paragraph {
font-size: clamp(1.2rem, 1rem + 0.35vw, 1.45rem);
}
</style>