forked from cms/tibi-svelte-starter
✨ feat: enhance accessibility with skip to main content button and improve navigation handling
🔧 fix: update navigation href resolution to include localized paths 🆕 feat: add new FeatureIcon component for feature boxes 🎨 style: improve styling for prose elements in richtext blocks 🛠️ refactor: streamline medialib image loading and caching logic 📦 chore: update mock data handling to support new medialib entries 🔄 chore: synchronize i18n initialization and locale management 📝 docs: update video tour descriptions to reflect recent changes
This commit is contained in:
@@ -1,47 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { getDBEntries, getDBEntry } from "../lib/api"
|
||||
import { apiBaseURL } from "../config"
|
||||
import { apiBaseOverride } from "../lib/store"
|
||||
import { get } from "svelte/store"
|
||||
|
||||
// Medialib cache (module-level)
|
||||
const medialibCache: { [id: string]: MedialibEntry } = {}
|
||||
let loadQueue: string[] = []
|
||||
let debounceTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
async function processQueue() {
|
||||
if (loadQueue.length) {
|
||||
const _ids = [...loadQueue]
|
||||
loadQueue = []
|
||||
const entries = await getDBEntries(
|
||||
"medialib",
|
||||
{ _id: { $in: _ids } },
|
||||
"_id",
|
||||
undefined,
|
||||
undefined,
|
||||
"public"
|
||||
)
|
||||
entries.forEach((entry: MedialibEntry) => {
|
||||
if (entry.id) medialibCache[entry.id] = entry
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async function loadMedialibEntry(id: string): Promise<MedialibEntry> {
|
||||
if (medialibCache[id]) return medialibCache[id]
|
||||
loadQueue.push(id)
|
||||
await new Promise<void>((resolve) => {
|
||||
if (debounceTimer) clearTimeout(debounceTimer)
|
||||
debounceTimer = setTimeout(async () => {
|
||||
await processQueue()
|
||||
resolve()
|
||||
}, 50)
|
||||
})
|
||||
return medialibCache[id]
|
||||
}
|
||||
import { currentLanguage, DEFAULT_LANGUAGE } from "../lib/i18n"
|
||||
|
||||
interface Props {
|
||||
id: string
|
||||
entry?: MedialibEntry | null
|
||||
filter?: string | null
|
||||
noPlaceholder?: boolean
|
||||
caption?: string
|
||||
@@ -54,6 +17,7 @@
|
||||
|
||||
let {
|
||||
id,
|
||||
entry = null,
|
||||
filter = null,
|
||||
noPlaceholder = false,
|
||||
caption = "",
|
||||
@@ -64,11 +28,10 @@
|
||||
style = "",
|
||||
}: Props = $props()
|
||||
|
||||
let loading = $state(true)
|
||||
let entry = $state<MedialibEntry | null>(null)
|
||||
let fileSrc = $state<string | null>(null)
|
||||
let imgEl = $state<HTMLImageElement | null>(null)
|
||||
let currentFilter = $state<string>("l-webp")
|
||||
const effectiveId = $derived(entry?.id || entry?._id || id || "")
|
||||
const fileSrc = $derived(resolveFileSrc(entry?.file?.src, entry?.id || entry?._id || effectiveId))
|
||||
|
||||
// Sync explicit filter prop reactively
|
||||
$effect(() => {
|
||||
@@ -94,34 +57,14 @@
|
||||
return false
|
||||
}
|
||||
|
||||
async function loadFile() {
|
||||
if (!id) return
|
||||
loading = true
|
||||
entry = null
|
||||
fileSrc = null
|
||||
try {
|
||||
const _apiBase = get(apiBaseOverride) || apiBaseURL
|
||||
entry =
|
||||
typeof window !== "undefined"
|
||||
? await loadMedialibEntry(id)
|
||||
: await getDBEntry("medialib", { _id: id }, "public")
|
||||
if (entry?.file?.src) {
|
||||
fileSrc = _apiBase + "medialib/" + id + "/" + entry.file.src
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
loading = false
|
||||
function resolveFileSrc(src: string | undefined, entryId: string | undefined): string | null {
|
||||
if (!src) return null
|
||||
if (/^(https?:)?\/\//.test(src) || src.startsWith("/")) return src
|
||||
if (!entryId) return null
|
||||
const normalizedApiBase = apiBaseURL.replace(/\/+$/, "")
|
||||
return `${normalizedApiBase}/medialib/${entryId}/${src.replace(/^\/+/, "")}`
|
||||
}
|
||||
|
||||
// SSR: fire-and-forget — $effect does NOT run during SSR.
|
||||
// loadFile() internally checks if id is set.
|
||||
if (typeof window === "undefined") loadFile()
|
||||
|
||||
$effect(() => {
|
||||
if (id) loadFile()
|
||||
})
|
||||
|
||||
// ResizeObserver: only when no explicit filter and raster image
|
||||
$effect(() => {
|
||||
const el = imgEl
|
||||
@@ -147,21 +90,24 @@
|
||||
if (filter) return src + `?filter=${filter}`
|
||||
return src + `?filter=${currentFilter}`
|
||||
}
|
||||
|
||||
function resolveLocalizedText(value: string | LocalizedText | undefined, lang: string): string {
|
||||
if (!value) return ""
|
||||
if (typeof value === "string") return value
|
||||
|
||||
return value[lang] || value[DEFAULT_LANGUAGE] || Object.values(value).find((entry) => !!entry) || ""
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if id}
|
||||
{#if loading}
|
||||
{#if !noPlaceholder}
|
||||
<img src="/assets/img/placeholder-image.svg" alt="loading" />
|
||||
{/if}
|
||||
{:else if entry && fileSrc}
|
||||
{#if effectiveId}
|
||||
{#if entry && fileSrc}
|
||||
{#if showCaption && caption}
|
||||
<figure>
|
||||
<picture>
|
||||
<img
|
||||
bind:this={imgEl}
|
||||
src={getSrc(fileSrc, entry)}
|
||||
alt={entry.alt || ""}
|
||||
alt={resolveLocalizedText(entry.alt, $currentLanguage)}
|
||||
data-entry-id={id}
|
||||
loading={lazy ? "lazy" : undefined}
|
||||
{style}
|
||||
@@ -176,7 +122,7 @@
|
||||
<img
|
||||
bind:this={imgEl}
|
||||
src={getSrc(fileSrc, entry)}
|
||||
alt={entry.alt || ""}
|
||||
alt={resolveLocalizedText(entry.alt, $currentLanguage)}
|
||||
data-entry-id={id}
|
||||
loading={lazy ? "lazy" : undefined}
|
||||
{style}
|
||||
@@ -185,7 +131,7 @@
|
||||
{/if}
|
||||
{:else if !noPlaceholder}
|
||||
<picture>
|
||||
<img src="/assets/img/placeholder-image.svg" alt="not found" data-entry-id={id} />
|
||||
<img src="/assets/img/placeholder-image.svg" alt="not found" data-entry-id={effectiveId} />
|
||||
</picture>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
Reference in New Issue
Block a user