feat: caching
This commit is contained in:
@@ -3,16 +3,26 @@
|
||||
|
||||
interface Props {
|
||||
src: string;
|
||||
alt?: string;
|
||||
children?: Snippet;
|
||||
}
|
||||
|
||||
let { src, children }: Props = $props();
|
||||
let { src, alt = "", children }: Props = $props();
|
||||
|
||||
const placeholder = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiPjxyZWN0IGZpbGw9IiMzMzMiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiLz48L3N2Zz4=';
|
||||
|
||||
let imageSrc = $derived(src || placeholder);
|
||||
</script>
|
||||
|
||||
<div class="grid grid-cols-1 grid-rows-1">
|
||||
<img
|
||||
{src}
|
||||
src={imageSrc}
|
||||
{alt}
|
||||
class="row-start-1 col-start-1 pointer-events-none w-full h-96 object-cover"
|
||||
onerror={(e) => {
|
||||
const img = e.currentTarget as HTMLImageElement;
|
||||
img.src = placeholder;
|
||||
}}
|
||||
/>
|
||||
<div class="row-start-1 col-start-1">
|
||||
{@render children?.()}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
const cache = new Map<string, { timestamp: number; data: any }>();
|
||||
const DEFAULT_TTL = 1000 * 60 * 15; // 15 minutes default TTL
|
||||
|
||||
/**
|
||||
* Caches the result of a promise-returning function.
|
||||
* @param key Unique key for the cache entry.
|
||||
* @param fetcher Function that returns a promise with the data to cache.
|
||||
* @param ttl Time to live in milliseconds. Defaults to 15 minutes.
|
||||
*/
|
||||
export async function withCache<T>(key: string, fetcher: () => Promise<T>, ttl = DEFAULT_TTL): Promise<T> {
|
||||
const now = Date.now();
|
||||
const entry = cache.get(key);
|
||||
|
||||
if (entry && (now - entry.timestamp < ttl)) {
|
||||
return entry.data as T;
|
||||
}
|
||||
|
||||
try {
|
||||
const data = await fetcher();
|
||||
cache.set(key, { timestamp: now, data });
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error(`Error fetching data for key ${key}:`, error);
|
||||
// If fetch fails but we have stale data, return it?
|
||||
// For now, let's bubble the error up so the page handles it (e.g. 404),
|
||||
// or maybe just re-throw.
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Manually clears a specific cache key.
|
||||
*/
|
||||
export function clearCache(key: string) {
|
||||
cache.delete(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the entire cache.
|
||||
*/
|
||||
export function clearAllCache() {
|
||||
cache.clear();
|
||||
}
|
||||
+87
-10
@@ -1,21 +1,98 @@
|
||||
import { createClient } from "webdav";
|
||||
import { env } from "$env/dynamic/private";
|
||||
import { withCache } from "./cache";
|
||||
|
||||
if (!env.NEXTCLOUD_URL || !env.NEXTCLOUD_USER || !env.NEXTCLOUD_PASSWORD) {
|
||||
console.warn("Nextcloud environment variables are not fully set. WebDAV client might not work correctly.");
|
||||
console.warn(
|
||||
"Nextcloud environment variables are not fully set. WebDAV client might not work correctly.",
|
||||
);
|
||||
}
|
||||
|
||||
// Construct the standard Nextcloud WebDAV URL: https://example.com/remote.php/dav/files/USERNAME/
|
||||
// Construct the standard Nextcloud WebDAV URL: https://example.com/remote.php/dav/files/USERNAME/[BASE_DIR/]
|
||||
const getWebdavUrl = () => {
|
||||
if (!env.NEXTCLOUD_URL) return "";
|
||||
const base = env.NEXTCLOUD_URL.endsWith("/") ? env.NEXTCLOUD_URL : `${env.NEXTCLOUD_URL}/`;
|
||||
return `${base}remote.php/dav/files/${env.NEXTCLOUD_USER}/`;
|
||||
|
||||
let url = env.NEXTCLOUD_URL.endsWith("/")
|
||||
? env.NEXTCLOUD_URL
|
||||
: `${env.NEXTCLOUD_URL}/`;
|
||||
url += `remote.php/dav/files/${env.NEXTCLOUD_USER}/`;
|
||||
|
||||
if (env.NEXTCLOUD_BASE_DIR) {
|
||||
// Remove leading/trailing slashes from base dir to ensure clean concatenation
|
||||
const baseDir = env.NEXTCLOUD_BASE_DIR.replace(/^\/+|\/+$/g, "");
|
||||
if (baseDir) {
|
||||
url += `${baseDir}/`;
|
||||
}
|
||||
}
|
||||
|
||||
return url;
|
||||
};
|
||||
|
||||
export const client = createClient(
|
||||
getWebdavUrl(),
|
||||
{
|
||||
username: env.NEXTCLOUD_USER || "",
|
||||
password: env.NEXTCLOUD_PASSWORD || ""
|
||||
export const client = createClient(getWebdavUrl(), {
|
||||
username: env.NEXTCLOUD_USER || "",
|
||||
password: env.NEXTCLOUD_PASSWORD || "",
|
||||
});
|
||||
|
||||
export async function getCoverImage(slug: string): Promise<string | null> {
|
||||
return withCache(`cover:${slug}`, async () => {
|
||||
try {
|
||||
const contents = await client.getDirectoryContents("/" + slug);
|
||||
console.log(contents);
|
||||
if (!Array.isArray(contents)) return null;
|
||||
|
||||
const cover = contents.find(
|
||||
(item: any) =>
|
||||
item.type === "file" &&
|
||||
item.basename.match(/^cover\.(webp|jpg|jpeg|png)$/i),
|
||||
);
|
||||
|
||||
return cover ? `/assets/${slug}/${cover.basename}` : null;
|
||||
} catch (e) {
|
||||
console.warn(`Failed to check cover image for ${slug}:`, e);
|
||||
return null;
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export async function getPages() {
|
||||
return withCache("pages", async () => {
|
||||
try {
|
||||
const items = await client.getDirectoryContents("/");
|
||||
|
||||
if (!Array.isArray(items)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const directories = items.filter(
|
||||
(item: any) =>
|
||||
item.type === "directory" && !item.basename.startsWith("."),
|
||||
);
|
||||
|
||||
const pages = await Promise.all(
|
||||
directories.map(async (item: any) => {
|
||||
const slug = item.basename;
|
||||
const title = slug.charAt(0).toUpperCase() + slug.slice(1);
|
||||
const coverImage = await getCoverImage(slug);
|
||||
|
||||
return {
|
||||
slug,
|
||||
title,
|
||||
coverImage,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
return pages;
|
||||
} catch (e) {
|
||||
console.error("Failed to fetch pages from Nextcloud:", e);
|
||||
return [];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export async function getPageContent(path: string): Promise<string> {
|
||||
return withCache(`content:${path}`, async () => {
|
||||
const content = await client.getFileContents(path, { format: "text" });
|
||||
return content as string;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import type { LayoutServerLoad } from './$types';
|
||||
import { getPages } from '$lib/server/nextcloud';
|
||||
|
||||
export const load: LayoutServerLoad = async () => {
|
||||
const pages = await getPages();
|
||||
return {
|
||||
pages
|
||||
};
|
||||
};
|
||||
+42
-35
@@ -1,41 +1,48 @@
|
||||
<script lang="ts">
|
||||
import ImageLinkTile from "$lib/components/ImageLinkTile.svelte";
|
||||
import ImageLinkTile from "$lib/components/ImageLinkTile.svelte";
|
||||
import type { PageData } from "./$types";
|
||||
|
||||
import mouse from "$lib/images/home/mouse.webp";
|
||||
import drummachine from "$lib/images/home/drummachine.webp";
|
||||
import scouts from "$lib/images/home/scouts.webp";
|
||||
import console_image from "$lib/images/home/console.webp";
|
||||
import lauch from "$lib/images/home/lauch.webp";
|
||||
// Import local images to preserve existing design
|
||||
import mouse from "$lib/images/home/mouse.webp";
|
||||
import drummachine from "$lib/images/home/drummachine.webp";
|
||||
import scouts from "$lib/images/home/scouts.webp";
|
||||
import console_image from "$lib/images/home/console.webp";
|
||||
import lauch from "$lib/images/home/lauch.webp";
|
||||
import monitor from "$lib/images/informatik/monitor.webp"; // Using monitor for Informatik if console is not preferred
|
||||
|
||||
let { data }: { data: PageData } = $props();
|
||||
|
||||
// Map slugs to local images for backward compatibility/styling
|
||||
const staticImages: Record<string, string> = {
|
||||
informatik: console_image,
|
||||
fotos: mouse,
|
||||
video: lauch,
|
||||
musig: drummachine,
|
||||
};
|
||||
|
||||
// Helper to resolve image source: Local override -> Nextcloud cover -> Placeholder (handled by ImageTile)
|
||||
const getTileImage = (page: { slug: string; coverImage: string | null }) => {
|
||||
return staticImages[page.slug.toLowerCase()] || page.coverImage || "";
|
||||
};
|
||||
</script>
|
||||
|
||||
<main>
|
||||
<ImageLinkTile src={console_image} href="/informatik">
|
||||
<h2 class="text-3xl font-bold text-white hover:underline text-shadow-lg">
|
||||
Informatik
|
||||
</h2>
|
||||
</ImageLinkTile>
|
||||
<main class="">
|
||||
{#if data.pages}
|
||||
{#each data.pages as page}
|
||||
<ImageLinkTile src={getTileImage(page)} href={`/${page.slug}`}>
|
||||
<h2
|
||||
class="text-3xl font-bold text-white hover:underline text-shadow-lg"
|
||||
>
|
||||
{page.title}
|
||||
</h2>
|
||||
</ImageLinkTile>
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
<ImageLinkTile src={scouts} href="https://pfadifrisco.ch/" target="_blank">
|
||||
<h2 class="text-3xl font-bold text-white hover:underline text-shadow-lg">
|
||||
Pfadi
|
||||
</h2>
|
||||
</ImageLinkTile>
|
||||
|
||||
<ImageLinkTile src={mouse} href="/fotos">
|
||||
<h2 class="text-3xl font-bold text-white hover:underline text-shadow-lg">
|
||||
Fotos
|
||||
</h2>
|
||||
</ImageLinkTile>
|
||||
|
||||
<ImageLinkTile src={lauch} href="/video">
|
||||
<h2 class="text-3xl font-bold text-white hover:underline text-shadow-lg">
|
||||
Videos
|
||||
</h2>
|
||||
</ImageLinkTile>
|
||||
|
||||
<ImageLinkTile src={drummachine} href="/musig">
|
||||
<h2 class="text-3xl font-bold text-white hover:underline text-shadow-lg">
|
||||
Musig
|
||||
</h2>
|
||||
</ImageLinkTile>
|
||||
<!-- Static/External Links -->
|
||||
<ImageLinkTile src={scouts} href="https://pfadifrisco.ch/" target="_blank">
|
||||
<h2 class="text-3xl font-bold text-white hover:underline text-shadow-lg">
|
||||
Pfadi
|
||||
</h2>
|
||||
</ImageLinkTile>
|
||||
</main>
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { getCoverImage, getPageContent } from "$lib/server/nextcloud";
|
||||
import type { PageServerLoad } from "./$types";
|
||||
import { error } from "@sveltejs/kit";
|
||||
|
||||
export const load: PageServerLoad = async ({ params }) => {
|
||||
const { slug } = params;
|
||||
// Capitalize the first letter to match the convention: fotos -> Fotos.md
|
||||
const filename = slug.charAt(0).toUpperCase() + slug.slice(1) + ".md";
|
||||
const path = `/${slug}/${filename}`;
|
||||
|
||||
try {
|
||||
const [content, coverImage] = await Promise.all([
|
||||
getPageContent(path),
|
||||
getCoverImage(slug)
|
||||
]);
|
||||
|
||||
return {
|
||||
content,
|
||||
title: slug.charAt(0).toUpperCase() + slug.slice(1),
|
||||
slug,
|
||||
coverImage
|
||||
};
|
||||
} catch (e) {
|
||||
console.error(`Error fetching ${filename} from Nextcloud`, e);
|
||||
throw error(404, "Page not found");
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
<script lang="ts">
|
||||
import ImageTile from "$lib/components/ImageTile.svelte";
|
||||
import Markdown from "$lib/components/Markdown.svelte";
|
||||
import type { PageData } from "./$types";
|
||||
|
||||
// Legacy images
|
||||
import sunne_untergang from "$lib/images/fotos/sunne_untergang.webp";
|
||||
import monitor from "$lib/images/informatik/monitor.webp";
|
||||
import plattespiler from "$lib/images/musig/plattespiler.webp";
|
||||
import jochen from "$lib/images/video/jochen.webp";
|
||||
|
||||
let { data }: { data: PageData } = $props();
|
||||
|
||||
const legacyHeaders: Record<string, string> = {
|
||||
fotos: sunne_untergang,
|
||||
informatik: monitor,
|
||||
musig: plattespiler,
|
||||
video: jochen
|
||||
};
|
||||
|
||||
// Fallback image data URI
|
||||
const placeholder = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiPjxyZWN0IGZpbGw9IiMzMzMiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiLz48L3N2Zz4=';
|
||||
|
||||
// Use the cover image found by the server, or legacy local image, or placeholder
|
||||
let headerImage = $derived(
|
||||
data.coverImage ||
|
||||
legacyHeaders[data.slug.toLowerCase()] ||
|
||||
"" // ImageTile handles empty/null src with placeholder or its own logic
|
||||
);
|
||||
</script>
|
||||
|
||||
<ImageTile src={headerImage} alt={data.title}>
|
||||
<h2 class="flex items-center justify-center h-full text-3xl font-bold text-white text-shadow-lg">
|
||||
{data.title}
|
||||
</h2>
|
||||
</ImageTile>
|
||||
|
||||
<main class="flex items-center justify-center pt-8 bg-black min-h-screen px-12">
|
||||
<section class="max-w-screen-md w-full">
|
||||
<Markdown content={data.content} basePath={`/${data.slug}`} />
|
||||
</section>
|
||||
</main>
|
||||
@@ -1,16 +0,0 @@
|
||||
import { client } from "$lib/server/nextcloud";
|
||||
import type { PageServerLoad } from "./$types";
|
||||
|
||||
export const load: PageServerLoad = async () => {
|
||||
try {
|
||||
const content = await client.getFileContents("/fotos/Fotos.md", { format: "text" });
|
||||
return {
|
||||
content: content as string
|
||||
};
|
||||
} catch (e) {
|
||||
console.error("Error fetching Fotos.md from Nextcloud", e);
|
||||
return {
|
||||
content: "Failed to load content from Nextcloud."
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -1,21 +0,0 @@
|
||||
<script lang="ts">
|
||||
import ImageTile from "$lib/components/ImageTile.svelte";
|
||||
import Markdown from "$lib/components/Markdown.svelte";
|
||||
import HeadImage from "$lib/images/fotos/sunne_untergang.webp";
|
||||
import type { PageData } from "./$types";
|
||||
|
||||
let { data }: { data: PageData } = $props();
|
||||
</script>
|
||||
|
||||
<ImageTile src={HeadImage}>
|
||||
<h2
|
||||
class="flex items-center justify-center h-full text-3xl font-bold text-white text-shadow-lg"
|
||||
>
|
||||
Fotos
|
||||
</h2>
|
||||
</ImageTile>
|
||||
<main class="flex items-center justify-center pt-8 bg-black min-h-screen px-12">
|
||||
<section class="max-w-screen-md">
|
||||
<Markdown content={data.content} basePath="/fotos" />
|
||||
</section>
|
||||
</main>
|
||||
@@ -1,16 +0,0 @@
|
||||
import { client } from "$lib/server/nextcloud";
|
||||
import type { PageServerLoad } from "./$types";
|
||||
|
||||
export const load: PageServerLoad = async () => {
|
||||
try {
|
||||
const content = await client.getFileContents("/informatik/Informatik.md", { format: "text" });
|
||||
return {
|
||||
content: content as string
|
||||
};
|
||||
} catch (e) {
|
||||
console.error("Error fetching Informatik.md from Nextcloud", e);
|
||||
return {
|
||||
content: "Failed to load content from Nextcloud."
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -1,21 +0,0 @@
|
||||
<script lang="ts">
|
||||
import ImageTile from "$lib/components/ImageTile.svelte";
|
||||
import Markdown from "$lib/components/Markdown.svelte";
|
||||
import HeadImage from "$lib/images/informatik/monitor.webp";
|
||||
import type { PageData } from "./$types";
|
||||
|
||||
let { data }: { data: PageData } = $props();
|
||||
</script>
|
||||
|
||||
<ImageTile src={HeadImage}>
|
||||
<h2
|
||||
class="flex items-center justify-center h-full text-3xl font-bold text-white text-shadow-lg"
|
||||
>
|
||||
Informatik
|
||||
</h2>
|
||||
</ImageTile>
|
||||
<main class="flex items-center justify-center pt-8 bg-black min-h-screen px-12">
|
||||
<section class="max-w-screen-md">
|
||||
<Markdown content={data.content} basePath="/informatik" />
|
||||
</section>
|
||||
</main>
|
||||
@@ -1,16 +0,0 @@
|
||||
import { client } from "$lib/server/nextcloud";
|
||||
import type { PageServerLoad } from "./$types";
|
||||
|
||||
export const load: PageServerLoad = async () => {
|
||||
try {
|
||||
const content = await client.getFileContents("/musig/Musig.md", { format: "text" });
|
||||
return {
|
||||
content: content as string
|
||||
};
|
||||
} catch (e) {
|
||||
console.error("Error fetching Musig.md from Nextcloud", e);
|
||||
return {
|
||||
content: "Failed to load content from Nextcloud."
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -1,21 +0,0 @@
|
||||
<script lang="ts">
|
||||
import ImageTile from "$lib/components/ImageTile.svelte";
|
||||
import Markdown from "$lib/components/Markdown.svelte";
|
||||
import HeadImage from "$lib/images/musig/plattespiler.webp";
|
||||
import type { PageData } from "./$types";
|
||||
|
||||
let { data }: { data: PageData } = $props();
|
||||
</script>
|
||||
|
||||
<ImageTile src={HeadImage}>
|
||||
<h2
|
||||
class="flex items-center justify-center h-full text-3xl font-bold text-white text-shadow-lg"
|
||||
>
|
||||
Musig
|
||||
</h2>
|
||||
</ImageTile>
|
||||
<main class="flex items-center justify-center pt-8 bg-black min-h-screen px-12">
|
||||
<section class="max-w-screen-md">
|
||||
<Markdown content={data.content} basePath="/musig" />
|
||||
</section>
|
||||
</main>
|
||||
@@ -1,16 +0,0 @@
|
||||
import { client } from "$lib/server/nextcloud";
|
||||
import type { PageServerLoad } from "./$types";
|
||||
|
||||
export const load: PageServerLoad = async () => {
|
||||
try {
|
||||
const content = await client.getFileContents("/video/Video.md", { format: "text" });
|
||||
return {
|
||||
content: content as string
|
||||
};
|
||||
} catch (e) {
|
||||
console.error("Error fetching Video.md from Nextcloud", e);
|
||||
return {
|
||||
content: "Failed to load content from Nextcloud."
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -1,21 +0,0 @@
|
||||
<script lang="ts">
|
||||
import ImageTile from "$lib/components/ImageTile.svelte";
|
||||
import Markdown from "$lib/components/Markdown.svelte";
|
||||
import HeadImage from "$lib/images/video/jochen.webp";
|
||||
import type { PageData } from "./$types";
|
||||
|
||||
let { data }: { data: PageData } = $props();
|
||||
</script>
|
||||
|
||||
<ImageTile src={HeadImage}>
|
||||
<h2
|
||||
class="flex items-center justify-center h-full text-3xl font-bold text-white text-shadow-lg"
|
||||
>
|
||||
Video Sammlig:
|
||||
</h2>
|
||||
</ImageTile>
|
||||
<main class="flex items-center justify-center pt-8 bg-black min-h-screen px-12">
|
||||
<section class="max-w-screen-md">
|
||||
<Markdown content={data.content} basePath="/video" />
|
||||
</section>
|
||||
</main>
|
||||
Reference in New Issue
Block a user