Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ ITEMS_TIMEOUT=600
ITEMS_STALE=600
PUBLIC_CATALOG_HREF=/
PUBLIC_DEVPORTAL_HREF=/
PUBLIC_PUZZLE_HREF=/
PUBLIC_PUZZLE_HREF=/
PUBLIC_CACHE_DEBOUNCE_MS=500
1 change: 1 addition & 0 deletions helm/templates/configmap.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ data:
PUBLIC_CATALOG_HREF: 'http://catalog'
PUBLIC_DEVPORTAL_HREF: 'http://developer-portal'
PUBLIC_PUZZLE_HREF: ''
PUBLIC_CACHE_DEBOUNCE_MS: {{ .Values.cache.debounceMs | quote }}
npm_config_cache: /tmp/
# default.conf: {{ tpl (.Files.Get "config/default.conf") . | quote }}
{{- end }}
3 changes: 3 additions & 0 deletions helm/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ env:
port: 8080
targetPort: 8080

cache:
debounceMs: 500

resources:
enabled: true
value:
Expand Down
41 changes: 41 additions & 0 deletions src/lib/cache/demoCache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import type { File } from '$lib/types';

const NAMESPACE = 'demo-cache:v1';

function available(): boolean {
return typeof localStorage !== 'undefined';
}

export function cacheKey(client: string, name: string): string {
return `${NAMESPACE}:${client}/${name}`;
}

export function loadCache(key: string): File[] | null {
if (!available()) return null;
const raw = localStorage.getItem(key);
if (raw === null) return null;
try {
const files = JSON.parse(raw) as File[];
return Array.isArray(files) ? files : null;
} catch {
return null;
}
}

export function saveCache(key: string, files: File[]): void {
if (!available()) return;
try {
localStorage.setItem(key, JSON.stringify(files));
} catch {
// Ignore quota / private-mode write failures — caching is best-effort.
}
}

export function clearCache(key: string): void {
if (!available()) return;
try {
localStorage.removeItem(key);
} catch {
// Ignore — clearing is best-effort.
}
}
11 changes: 11 additions & 0 deletions src/lib/components/flems.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import type { File, Link } from '$lib/types';
export let files: File[];
export let links: Link[];
export let onChange: ((files: File[]) => void) | undefined = undefined;

const flemsBaseConfig = {
shareButton: false,
Expand All @@ -14,6 +15,15 @@

let flemsInstance: any;

function wireOnChange() {
if (!flemsInstance || !onChange) return;
// Flems' `onchange` is a registrar you CALL to install a handler,
// not a settable property (`onchange: fn => actions.onchange = fn`).
flemsInstance.onchange((state: { files: File[] }) =>
onChange?.(state.files.map((f) => ({ name: f.name, content: f.content })))
);
}

$: if (flemsInstance) {
flemsInstance.set({
...flemsBaseConfig,
Expand All @@ -29,6 +39,7 @@
files,
links: links.map((link) => ({ ...link, url: window.location.origin + link.url }))
});
wireOnChange();
});
</script>

Expand Down
117 changes: 94 additions & 23 deletions src/routes/demo/[client]/[name]/+page.svelte
Original file line number Diff line number Diff line change
@@ -1,31 +1,102 @@
<script lang="ts">
import { onMount, onDestroy } from 'svelte';
import { page } from '$app/stores';
import { env } from '$env/dynamic/public';
import Flems from '$lib/components/flems.svelte';
import type { File } from '$lib/types';
import { cacheKey, loadCache, saveCache, clearCache } from '$lib/cache/demoCache';

export let data;

const parsedDebounce = Number.parseInt(env.PUBLIC_CACHE_DEBOUNCE_MS ?? '', 10);
const debounceMs = Number.isNaN(parsedDebounce) || parsedDebounce < 0 ? 500 : parsedDebounce;

let files: File[] = data.files;
let fromCache = false;
let saveTimer: ReturnType<typeof setTimeout> | undefined;
let mounted = false;
let loadedKey = '';

// Recompute the cache key reactively so navigation reloads the right demo instead of keeping stale files.
$: key = cacheKey($page.params.client, $page.params.name);

function loadForKey(k: string) {
if (saveTimer) clearTimeout(saveTimer);
const cached = loadCache(k);
files = cached ?? data.files;
fromCache = cached !== null;
loadedKey = k;
}

// Read from localStorage only after mount (client-only, post-hydration) so the
// first client render matches SSR output and hydration stays clean.
onMount(() => {
mounted = true;
loadForKey(key);
});

$: if (mounted && key !== loadedKey) loadForKey(key);

onDestroy(() => {
if (saveTimer) clearTimeout(saveTimer);
});

function handleChange(edited: File[]) {
const savingKey = key;
if (saveTimer) clearTimeout(saveTimer);
saveTimer = setTimeout(() => saveCache(savingKey, edited), debounceMs);
}

function handleClear() {
if (!window.confirm('Discard your edits and reload the original example?')) return;
if (saveTimer) clearTimeout(saveTimer);
clearCache(key);
files = data.files;
fromCache = false;
}
</script>

<div class="h-full flex flex-row gap-3 p-3 bg-gray-50 dark:bg-gray-900">
<div
class="flex-1 min-w-0 rounded-lg overflow-hidden border border-gray-200 dark:border-gray-700 shadow-sm bg-white dark:bg-gray-800"
>
<Flems files={data.files} links={data.links} />
</div>
<aside
class="w-80 shrink-0 flex flex-col rounded-lg border border-gray-200 dark:border-gray-700 shadow-sm bg-white dark:bg-gray-800"
>
<header class="px-4 py-3 border-b border-gray-200 dark:border-gray-700">
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">
{data.displayName || data.demoName}
</h2>
</header>
<div class="flex-1 overflow-y-auto px-4 py-3">
{#if data.description}
<p class="text-sm leading-relaxed text-gray-700 dark:text-gray-300 whitespace-pre-line">
{data.description}
</p>
{:else}
<p class="text-sm italic text-gray-400 dark:text-gray-500">No description provided.</p>
{/if}
<div class="h-full flex flex-col">
{#if fromCache}
<div
class="flex items-center justify-between gap-3 px-4 py-2 text-sm bg-amber-50 dark:bg-amber-900/30 border-b border-amber-200 dark:border-amber-800 text-amber-800 dark:text-amber-200"
>
<span class="flex items-center gap-2">
<span aria-hidden="true">⟳</span> Loaded from cache
</span>
<button
type="button"
on:click={handleClear}
class="rounded-md px-2.5 py-1 text-xs font-medium bg-amber-100 hover:bg-amber-200 dark:bg-amber-800 dark:hover:bg-amber-700 text-amber-900 dark:text-amber-100"
>
Clear cache
</button>
</div>
</aside>
{/if}

<div class="flex-1 min-h-0 flex flex-row gap-3 p-3 bg-gray-50 dark:bg-gray-900">
<div
class="flex-1 min-w-0 rounded-lg overflow-hidden border border-gray-200 dark:border-gray-700 shadow-sm bg-white dark:bg-gray-800"
>
<Flems {files} links={data.links} onChange={handleChange} />
</div>
<aside
class="w-80 shrink-0 flex flex-col rounded-lg border border-gray-200 dark:border-gray-700 shadow-sm bg-white dark:bg-gray-800"
>
<header class="px-4 py-3 border-b border-gray-200 dark:border-gray-700">
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">
{data.displayName || data.demoName}
</h2>
</header>
<div class="flex-1 overflow-y-auto px-4 py-3">
{#if data.description}
<p class="text-sm leading-relaxed text-gray-700 dark:text-gray-300 whitespace-pre-line">
{data.description}
</p>
{:else}
<p class="text-sm italic text-gray-400 dark:text-gray-500">No description provided.</p>
{/if}
</div>
</aside>
</div>
</div>
Loading