Load the zoom view in strips so it opens instantly
The full-page captures (up to 1.3 MB each) are now cut into strips by scripts/ganzseiten-kacheln.mjs. The first strip is ~60 KB and is fetched as soon as a device scrolls into view; further strips load just before they are scrolled to. Strip sizes are in the file names, so the dialog has its full height immediately without layout jumps. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
|
@ -173,8 +173,8 @@ module.exports = [
|
|||
{ key: 'image', label: 'Bildschirmfoto', type: 'image' },
|
||||
{ key: 'mobileImage', label: 'Handy-Ansicht (Hochformat, leer = kein Handy)', type: 'image' },
|
||||
{ key: 'video', label: 'Video im Laptop (Pfad zu einer .mp4, leer = Standbild)', type: 'text' },
|
||||
{ key: 'fullImage', label: 'Ganze Seite, Computer (Pfad, öffnet sich beim Anklicken; leer = nicht vergrößerbar)', type: 'text' },
|
||||
{ key: 'fullMobileImage', label: 'Ganze Seite, Handy (Pfad, öffnet sich beim Anklicken)', type: 'text' },
|
||||
{ key: 'fullImage', label: 'Ganze Seite, Computer (Ordner mit Streifen, z. B. /mappe/ganz/name/; leer = nicht vergrößerbar)', type: 'text' },
|
||||
{ key: 'fullMobileImage', label: 'Ganze Seite, Handy (Ordner mit Streifen, z. B. /mappe/ganz/name-mobil/)', type: 'text' },
|
||||
{ key: 'kategorie', label: 'Kategorie', type: 'text' },
|
||||
{ key: 'titel', label: 'Titel / Name', type: 'text' },
|
||||
{ key: 'aufgabe', label: 'Die Aufgabe (leer = nur Text zeigen)', type: 'textarea' },
|
||||
|
|
|
|||
|
|
@ -187,8 +187,8 @@
|
|||
"image": "/mappe/elektro-rabe.webp",
|
||||
"mobileImage": "/mappe/elektro-rabe-mobil.webp",
|
||||
"video": "/mappe/elektro-rabe.mp4",
|
||||
"fullImage": "/mappe/elektro-rabe-ganz.webp",
|
||||
"fullMobileImage": "/mappe/elektro-rabe-mobil-ganz.webp"
|
||||
"fullImage": "/mappe/ganz/elektro-rabe/",
|
||||
"fullMobileImage": "/mappe/ganz/elektro-rabe-mobil/"
|
||||
},
|
||||
{
|
||||
"kategorie": "Konzeptarbeit · Tanzstudio",
|
||||
|
|
@ -199,8 +199,8 @@
|
|||
"image": "/mappe/maison-verte.webp",
|
||||
"mobileImage": "/mappe/maison-verte-mobil.webp",
|
||||
"video": "/mappe/maison-verte.mp4",
|
||||
"fullImage": "/mappe/maison-verte-ganz.webp",
|
||||
"fullMobileImage": "/mappe/maison-verte-mobil-ganz.jpg"
|
||||
"fullImage": "/mappe/ganz/maison-verte/",
|
||||
"fullMobileImage": "/mappe/ganz/maison-verte-mobil/"
|
||||
},
|
||||
{
|
||||
"kategorie": "Konzeptarbeit · Kosmetikstudio",
|
||||
|
|
@ -211,8 +211,8 @@
|
|||
"image": "/mappe/atelier-rosenhain.webp",
|
||||
"mobileImage": "/mappe/atelier-rosenhain-mobil.webp",
|
||||
"video": "",
|
||||
"fullImage": "/mappe/atelier-rosenhain-ganz.webp",
|
||||
"fullMobileImage": "/mappe/atelier-rosenhain-mobil-ganz.webp"
|
||||
"fullImage": "/mappe/ganz/atelier-rosenhain/",
|
||||
"fullMobileImage": "/mappe/ganz/atelier-rosenhain-mobil/"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 138 KiB |
|
Before Width: | Height: | Size: 252 KiB |
|
Before Width: | Height: | Size: 468 KiB |
BIN
public/mappe/ganz/atelier-rosenhain-mobil/000-780x1700.webp
Normal file
|
After Width: | Height: | Size: 63 KiB |
BIN
public/mappe/ganz/atelier-rosenhain-mobil/001-780x2496.webp
Normal file
|
After Width: | Height: | Size: 77 KiB |
BIN
public/mappe/ganz/atelier-rosenhain/000-1440x900.webp
Normal file
|
After Width: | Height: | Size: 51 KiB |
BIN
public/mappe/ganz/atelier-rosenhain/001-1440x263.webp
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
public/mappe/ganz/elektro-rabe-mobil/000-780x1700.webp
Normal file
|
After Width: | Height: | Size: 58 KiB |
BIN
public/mappe/ganz/elektro-rabe-mobil/001-780x3200.webp
Normal file
|
After Width: | Height: | Size: 96 KiB |
BIN
public/mappe/ganz/elektro-rabe-mobil/002-780x3200.webp
Normal file
|
After Width: | Height: | Size: 81 KiB |
BIN
public/mappe/ganz/elektro-rabe-mobil/003-780x3200.webp
Normal file
|
After Width: | Height: | Size: 94 KiB |
BIN
public/mappe/ganz/elektro-rabe-mobil/004-780x3200.webp
Normal file
|
After Width: | Height: | Size: 110 KiB |
BIN
public/mappe/ganz/elektro-rabe-mobil/005-780x1318.webp
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
public/mappe/ganz/elektro-rabe/000-1440x900.webp
Normal file
|
After Width: | Height: | Size: 54 KiB |
BIN
public/mappe/ganz/elektro-rabe/001-1440x1800.webp
Normal file
|
After Width: | Height: | Size: 101 KiB |
BIN
public/mappe/ganz/elektro-rabe/002-1440x1800.webp
Normal file
|
After Width: | Height: | Size: 93 KiB |
BIN
public/mappe/ganz/elektro-rabe/003-1440x65.webp
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
public/mappe/ganz/maison-verte-mobil/000-780x1700.webp
Normal file
|
After Width: | Height: | Size: 57 KiB |
BIN
public/mappe/ganz/maison-verte-mobil/001-780x3200.webp
Normal file
|
After Width: | Height: | Size: 92 KiB |
BIN
public/mappe/ganz/maison-verte-mobil/002-780x3200.webp
Normal file
|
After Width: | Height: | Size: 83 KiB |
BIN
public/mappe/ganz/maison-verte-mobil/003-780x3200.webp
Normal file
|
After Width: | Height: | Size: 69 KiB |
BIN
public/mappe/ganz/maison-verte-mobil/004-780x3200.webp
Normal file
|
After Width: | Height: | Size: 77 KiB |
BIN
public/mappe/ganz/maison-verte-mobil/005-780x3200.webp
Normal file
|
After Width: | Height: | Size: 86 KiB |
BIN
public/mappe/ganz/maison-verte-mobil/006-780x3200.webp
Normal file
|
After Width: | Height: | Size: 41 KiB |
BIN
public/mappe/ganz/maison-verte-mobil/007-780x3200.webp
Normal file
|
After Width: | Height: | Size: 62 KiB |
BIN
public/mappe/ganz/maison-verte-mobil/008-780x3200.webp
Normal file
|
After Width: | Height: | Size: 58 KiB |
BIN
public/mappe/ganz/maison-verte-mobil/009-780x3200.webp
Normal file
|
After Width: | Height: | Size: 119 KiB |
BIN
public/mappe/ganz/maison-verte-mobil/010-780x3200.webp
Normal file
|
After Width: | Height: | Size: 66 KiB |
BIN
public/mappe/ganz/maison-verte-mobil/011-780x344.webp
Normal file
|
After Width: | Height: | Size: 5.4 KiB |
BIN
public/mappe/ganz/maison-verte/000-1440x900.webp
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
public/mappe/ganz/maison-verte/001-1440x1800.webp
Normal file
|
After Width: | Height: | Size: 64 KiB |
BIN
public/mappe/ganz/maison-verte/002-1440x1800.webp
Normal file
|
After Width: | Height: | Size: 103 KiB |
BIN
public/mappe/ganz/maison-verte/003-1440x1800.webp
Normal file
|
After Width: | Height: | Size: 71 KiB |
BIN
public/mappe/ganz/maison-verte/004-1440x1800.webp
Normal file
|
After Width: | Height: | Size: 96 KiB |
BIN
public/mappe/ganz/maison-verte/005-1440x1800.webp
Normal file
|
After Width: | Height: | Size: 81 KiB |
BIN
public/mappe/ganz/maison-verte/006-1440x791.webp
Normal file
|
After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 485 KiB |
|
Before Width: | Height: | Size: 1.3 MiB |
43
scripts/ganzseiten-kacheln.mjs
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
// Schneidet die Ganzseitenaufnahmen der Mappe in Streifen, damit die
|
||||
// Großansicht sofort etwas zeigt und der Rest beim Scrollen nachlädt.
|
||||
// Aufruf: node scripts/ganzseiten-kacheln.mjs <Ordner mit den PNGs>
|
||||
// Erwartet <name>.png (Computer, 1440 px breit) und <name>-mobil.png (Handy, 780 px).
|
||||
// Ergebnis: public/mappe/ganz/<name>[-mobil]/NNN-BREITExHOEHE.webp
|
||||
// Die Maße stehen im Dateinamen, so braucht der Build kein Bildwerkzeug.
|
||||
import sharp from 'sharp';
|
||||
import { mkdirSync, readdirSync, rmSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const quelle = process.argv[2];
|
||||
if (!quelle) throw new Error('Ordner mit den PNGs angeben');
|
||||
const ziel = new URL('../public/mappe/ganz/', import.meta.url);
|
||||
|
||||
// Erster Streifen klein (schnell sichtbar), danach größere
|
||||
const STREIFEN = { desktop: [900, 1800], mobil: [1700, 3200] };
|
||||
|
||||
for (const datei of readdirSync(quelle).filter((f) => f.endsWith('.png') && !f.startsWith('v-'))) {
|
||||
const name = datei.replace(/\.png$/, '');
|
||||
const art = name.endsWith('-mobil') ? 'mobil' : 'desktop';
|
||||
const pfad = join(quelle, datei);
|
||||
const { width, height } = await sharp(pfad, { limitInputPixels: false }).metadata();
|
||||
const ordner = new URL(`${name}/`, ziel);
|
||||
rmSync(ordner, { recursive: true, force: true });
|
||||
mkdirSync(ordner, { recursive: true });
|
||||
|
||||
let top = 0;
|
||||
let i = 0;
|
||||
let summe = 0;
|
||||
while (top < height) {
|
||||
const h = Math.min(i === 0 ? STREIFEN[art][0] : STREIFEN[art][1], height - top);
|
||||
const out = new URL(`${String(i).padStart(3, '0')}-${width}x${h}.webp`, ordner);
|
||||
const info = await sharp(pfad, { limitInputPixels: false })
|
||||
.extract({ left: 0, top, width, height: h })
|
||||
.webp({ quality: 80 })
|
||||
.toFile(fileURLToPath(out));
|
||||
summe += info.size;
|
||||
top += h;
|
||||
i++;
|
||||
}
|
||||
console.log(`${name}: ${i} Streifen, ${Math.round(summe / 1024)} KB`);
|
||||
}
|
||||
|
|
@ -6,10 +6,12 @@
|
|||
* Mit `screenZoom`/`phoneZoom` wird das Gerät anklickbar und öffnet die ganze
|
||||
* Seite in der Großansicht (<Zoom>, einmal pro Seite eingebunden).
|
||||
*/
|
||||
import type { Streifen } from '../lib/ganzseiten';
|
||||
|
||||
export interface ZoomSource {
|
||||
title: string;
|
||||
desktop?: string;
|
||||
mobile?: string;
|
||||
desktop?: Streifen[];
|
||||
mobile?: Streifen[];
|
||||
}
|
||||
|
||||
interface Props {
|
||||
|
|
@ -27,8 +29,8 @@ const loading = eager ? 'eager' : 'lazy';
|
|||
const zoomAttrs = (z: ZoomSource, mode: 'desktop' | 'mobile') => ({
|
||||
'data-zoom': mode,
|
||||
'data-zoom-title': z.title,
|
||||
'data-zoom-desktop': z.desktop || undefined,
|
||||
'data-zoom-mobile': z.mobile || undefined,
|
||||
'data-zoom-desktop': z.desktop ? JSON.stringify(z.desktop) : undefined,
|
||||
'data-zoom-mobile': z.mobile ? JSON.stringify(z.mobile) : undefined,
|
||||
'aria-label': `${z.title} ${mode === 'desktop' ? 'am Computer' : 'auf dem Handy'} vergrößert ansehen`,
|
||||
});
|
||||
const laptopZoom = screenZoom && (screenZoom.desktop || screenZoom.mobile) ? screenZoom : undefined;
|
||||
|
|
|
|||
|
|
@ -25,12 +25,12 @@
|
|||
<div class="zoom-browser">
|
||||
<div class="zoom-browser-top" aria-hidden="true"><span></span><span></span><span></span></div>
|
||||
<div class="zoom-scroll" tabindex="0" aria-label="Seite am Computer, zum Durchscrollen">
|
||||
<img alt="" decoding="async" data-slot="desktop" />
|
||||
<div class="zoom-pages" data-slot="desktop"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="zoom-phone">
|
||||
<div class="zoom-scroll" tabindex="0" aria-label="Seite auf dem Handy, zum Durchscrollen">
|
||||
<img alt="" decoding="async" data-slot="mobile" />
|
||||
<div class="zoom-pages" data-slot="mobile"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -39,48 +39,145 @@
|
|||
</dialog>
|
||||
|
||||
<script>
|
||||
/*
|
||||
* Die Aufnahmen liegen als Streifen vor (scripts/ganzseiten-kacheln.mjs).
|
||||
* Schnell wird es durch drei Dinge:
|
||||
* 1. Streifen haben feste Maße, die Seite steht sofort in voller Höhe.
|
||||
* 2. Nur sichtbare Streifen laden, der Rest kurz bevor man hinscrollt.
|
||||
* 3. Erste Streifen werden vorab geholt, sobald ein Gerät in Sichtweite
|
||||
* kommt, und beim Zeigen oder Antippen zusätzlich der zweite.
|
||||
*/
|
||||
type Streifen = { src: string; w: number; h: number };
|
||||
type Mode = 'desktop' | 'mobile';
|
||||
|
||||
const parse = (raw?: string): Streifen[] => {
|
||||
try {
|
||||
return raw ? (JSON.parse(raw) as Streifen[]) : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const geholt = new Set<string>();
|
||||
const vorab = (liste: Streifen[], anzahl: number) => {
|
||||
liste.slice(0, anzahl).forEach(({ src }) => {
|
||||
if (geholt.has(src)) return;
|
||||
geholt.add(src);
|
||||
const img = new Image();
|
||||
img.decoding = 'async';
|
||||
img.src = src;
|
||||
});
|
||||
};
|
||||
const vorabFuer = (trigger: HTMLElement, anzahl: number) => {
|
||||
const mode = trigger.dataset.zoom as Mode;
|
||||
vorab(parse(mode === 'mobile' ? trigger.dataset.zoomMobile : trigger.dataset.zoomDesktop), anzahl);
|
||||
};
|
||||
|
||||
const dialog = document.querySelector<HTMLDialogElement>('dialog.zoom');
|
||||
if (dialog && typeof dialog.showModal === 'function') {
|
||||
const title = dialog.querySelector<HTMLElement>('.zoom-title')!;
|
||||
const stage = dialog.querySelector<HTMLElement>('.zoom-stage')!;
|
||||
const switcher = dialog.querySelector<HTMLElement>('.zoom-switch')!;
|
||||
const imgs = {
|
||||
desktop: dialog.querySelector<HTMLImageElement>('img[data-slot="desktop"]')!,
|
||||
mobile: dialog.querySelector<HTMLImageElement>('img[data-slot="mobile"]')!,
|
||||
const pages = {
|
||||
desktop: dialog.querySelector<HTMLElement>('.zoom-pages[data-slot="desktop"]')!,
|
||||
mobile: dialog.querySelector<HTMLElement>('.zoom-pages[data-slot="mobile"]')!,
|
||||
};
|
||||
let sources: Record<Mode, Streifen[]> = { desktop: [], mobile: [] };
|
||||
const observers: Partial<Record<Mode, IntersectionObserver>> = {};
|
||||
|
||||
const laden = (img: HTMLImageElement) => {
|
||||
if (img.dataset.src) {
|
||||
img.src = img.dataset.src;
|
||||
delete img.dataset.src;
|
||||
}
|
||||
};
|
||||
|
||||
const aufbauen = (mode: Mode) => {
|
||||
const box = pages[mode];
|
||||
const key = sources[mode].map((s) => s.src).join('|');
|
||||
if (box.dataset.key === key) return;
|
||||
box.dataset.key = key;
|
||||
observers[mode]?.disconnect();
|
||||
box.replaceChildren();
|
||||
const scroller = box.closest<HTMLElement>('.zoom-scroll')!;
|
||||
// Streifen erst laden, wenn sie in die Nähe des sichtbaren Bereichs kommen
|
||||
const io =
|
||||
'IntersectionObserver' in window
|
||||
? new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((e) => {
|
||||
if (!e.isIntersecting) return;
|
||||
laden(e.target as HTMLImageElement);
|
||||
io?.unobserve(e.target);
|
||||
});
|
||||
},
|
||||
{ root: scroller, rootMargin: '1500px 0px' },
|
||||
)
|
||||
: undefined;
|
||||
observers[mode] = io;
|
||||
sources[mode].forEach((s, i) => {
|
||||
const img = document.createElement('img');
|
||||
img.alt = '';
|
||||
img.decoding = 'async';
|
||||
if (s.w && s.h) {
|
||||
img.width = s.w;
|
||||
img.height = s.h;
|
||||
}
|
||||
if (i < 2 || !io) {
|
||||
if (i === 0) img.setAttribute('fetchpriority', 'high');
|
||||
img.src = s.src;
|
||||
} else {
|
||||
img.dataset.src = s.src;
|
||||
io.observe(img);
|
||||
}
|
||||
box.append(img);
|
||||
});
|
||||
};
|
||||
type Mode = keyof typeof imgs;
|
||||
let sources: Record<Mode, string> = { desktop: '', mobile: '' };
|
||||
|
||||
const show = (mode: Mode) => {
|
||||
stage.dataset.mode = mode;
|
||||
switcher.querySelectorAll<HTMLButtonElement>('button').forEach((b) => {
|
||||
b.setAttribute('aria-pressed', String(b.dataset.mode === mode));
|
||||
});
|
||||
const img = imgs[mode];
|
||||
if (img.getAttribute('src') !== sources[mode]) {
|
||||
img.classList.remove('is-loaded');
|
||||
img.src = sources[mode];
|
||||
}
|
||||
img.closest('.zoom-scroll')!.scrollTop = 0;
|
||||
aufbauen(mode);
|
||||
pages[mode].closest('.zoom-scroll')!.scrollTop = 0;
|
||||
// die andere Ansicht schon mal anwärmen
|
||||
vorab(sources[mode === 'desktop' ? 'mobile' : 'desktop'], 1);
|
||||
};
|
||||
|
||||
Object.values(imgs).forEach((img) => {
|
||||
img.addEventListener('load', () => img.classList.add('is-loaded'));
|
||||
// Vorab holen: erster Streifen, sobald ein Gerät in Sichtweite ist
|
||||
const trigger = document.querySelectorAll<HTMLElement>('[data-zoom]');
|
||||
if ('IntersectionObserver' in window) {
|
||||
const nah = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((e) => {
|
||||
if (!e.isIntersecting) return;
|
||||
vorabFuer(e.target as HTMLElement, 1);
|
||||
nah.unobserve(e.target);
|
||||
});
|
||||
},
|
||||
{ rootMargin: '300px 0px' },
|
||||
);
|
||||
trigger.forEach((t) => nah.observe(t));
|
||||
}
|
||||
// Absicht erkennen: Maus drauf, Finger drauf, Tastaturfokus, dann den zweiten Streifen dazu
|
||||
trigger.forEach((t) => {
|
||||
const absicht = () => vorabFuer(t, 2);
|
||||
t.addEventListener('pointerenter', absicht, { passive: true });
|
||||
t.addEventListener('touchstart', absicht, { passive: true });
|
||||
t.addEventListener('focus', absicht);
|
||||
});
|
||||
|
||||
document.addEventListener('click', (event) => {
|
||||
const trigger = (event.target as Element).closest<HTMLElement>('[data-zoom]');
|
||||
if (!trigger) return;
|
||||
sources = {
|
||||
desktop: trigger.dataset.zoomDesktop || '',
|
||||
mobile: trigger.dataset.zoomMobile || '',
|
||||
};
|
||||
title.textContent = trigger.dataset.zoomTitle || '';
|
||||
switcher.hidden = !(sources.desktop && sources.mobile);
|
||||
const wanted = trigger.dataset.zoom as Mode;
|
||||
show(sources[wanted] ? wanted : sources.desktop ? 'desktop' : 'mobile');
|
||||
const t = (event.target as Element).closest<HTMLElement>('[data-zoom]');
|
||||
if (!t) return;
|
||||
sources = { desktop: parse(t.dataset.zoomDesktop), mobile: parse(t.dataset.zoomMobile) };
|
||||
title.textContent = t.dataset.zoomTitle || '';
|
||||
switcher.hidden = !(sources.desktop.length && sources.mobile.length);
|
||||
const wanted = t.dataset.zoom as Mode;
|
||||
dialog.showModal();
|
||||
document.documentElement.classList.add('zoom-open');
|
||||
show(sources[wanted].length ? wanted : sources.desktop.length ? 'desktop' : 'mobile');
|
||||
});
|
||||
|
||||
switcher.addEventListener('click', (event) => {
|
||||
|
|
@ -257,16 +354,13 @@
|
|||
background: #fff;
|
||||
}
|
||||
|
||||
.zoom-scroll img {
|
||||
/* Streifen liegen lückenlos untereinander; bis sie da sind, zeigt die
|
||||
feste Höhe eine helle Fläche statt eines Sprungs */
|
||||
.zoom-pages :global(img) {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
opacity: 0;
|
||||
transition: opacity 0.25s;
|
||||
}
|
||||
|
||||
.zoom-scroll img.is-loaded {
|
||||
opacity: 1;
|
||||
background: #f1efe9;
|
||||
}
|
||||
|
||||
.zoom-hint {
|
||||
|
|
@ -302,9 +396,4 @@
|
|||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.zoom-scroll img {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
28
src/lib/ganzseiten.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import { readdirSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
|
||||
export interface Streifen {
|
||||
src: string;
|
||||
w: number;
|
||||
h: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Liest die Streifen einer Ganzseitenaufnahme aus public/<ordner> (beim Build).
|
||||
* Dateinamen: NNN-BREITExHOEHE.webp, erzeugt von scripts/ganzseiten-kacheln.mjs.
|
||||
* Ein einzelner Bildpfad statt eines Ordners geht auch (ein Streifen ohne Maße).
|
||||
*/
|
||||
export function streifen(ordner?: string): Streifen[] | undefined {
|
||||
if (!ordner) return undefined;
|
||||
if (!ordner.endsWith('/')) return [{ src: ordner, w: 0, h: 0 }];
|
||||
try {
|
||||
const liste = readdirSync(join(process.cwd(), 'public', ordner))
|
||||
.map((f) => f.match(/^\d+-(\d+)x(\d+)\.(webp|jpg|png)$/) && { f, m: f.match(/^\d+-(\d+)x(\d+)/)! })
|
||||
.filter((x): x is { f: string; m: RegExpMatchArray } => Boolean(x))
|
||||
.sort((a, b) => a.f.localeCompare(b.f))
|
||||
.map(({ f, m }) => ({ src: ordner + f, w: Number(m[1]), h: Number(m[2]) }));
|
||||
return liste.length ? liste : undefined;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ import Devices from '../components/Devices.astro';
|
|||
import Zoom from '../components/Zoom.astro';
|
||||
import Kicker from '../components/Kicker.astro';
|
||||
import { site, rich } from '../lib/content';
|
||||
import { streifen } from '../lib/ganzseiten';
|
||||
|
||||
const { hero, warum, preis, highlights, leistungen, mappe, ablauf, about, faq, kontakt } = site;
|
||||
|
||||
|
|
@ -11,7 +12,7 @@ const { hero, warum, preis, highlights, leistungen, mappe, ablauf, about, faq, k
|
|||
type MappeItem = (typeof mappe.items)[number];
|
||||
const zoomFor = (item?: MappeItem) =>
|
||||
item && !item.nameHidden && (item.fullImage || item.fullMobileImage)
|
||||
? { title: item.titel, desktop: item.fullImage, mobile: item.fullMobileImage }
|
||||
? { title: item.titel, desktop: streifen(item.fullImage), mobile: streifen(item.fullMobileImage) }
|
||||
: undefined;
|
||||
const findItem = (src: string) =>
|
||||
mappe.items.find((i) => src && (i.image === src || i.mobileImage === src));
|
||||
|
|
|
|||