feat: [frontend] Create the parallel scroll for the latest entry
Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
parent
17dc49482c
commit
b91f6f81f8
5 changed files with 233 additions and 4 deletions
|
|
@ -4,8 +4,7 @@ export const load: PageServerLoad = async ({ locals, url }) => {
|
|||
let successMessage: null | string = null;
|
||||
if (url.searchParams.get('created')) {
|
||||
successMessage = `Adventure created, check back in a few minutes`;
|
||||
url.searchParams.delete('created');
|
||||
url.searchParams.
|
||||
url.searchParams.delete('created');
|
||||
}
|
||||
return {
|
||||
successMessage
|
||||
|
|
|
|||
|
|
@ -23,8 +23,10 @@
|
|||
|
||||
{#each adventures as adventure (adventure.id)}
|
||||
<div class="adventure-card">
|
||||
<h2>{adventure.title}</h2>
|
||||
<p>{adventure.description}</p>
|
||||
<a href={resolve('/app/adventures/[id]', { id: adventure.id })}>
|
||||
<h2>{adventure.title}</h2>
|
||||
<p>{adventure.description}</p>
|
||||
</a>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
|
|
|||
29
frontend/src/routes/app/adventures/[id]/+page.server.ts
Normal file
29
frontend/src/routes/app/adventures/[id]/+page.server.ts
Normal 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
|
||||
};
|
||||
};
|
||||
18
frontend/src/routes/app/adventures/[id]/+page.svelte
Normal file
18
frontend/src/routes/app/adventures/[id]/+page.svelte
Normal 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>
|
||||
181
frontend/src/routes/app/adventures/[id]/LatestEntry.svelte
Normal file
181
frontend/src/routes/app/adventures/[id]/LatestEntry.svelte
Normal 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>
|
||||
Loading…
Reference in a new issue