feat(ui): image display improvements across admin and public views

- MarketCard: object-fit contain with padding instead of cropped 16:9;
  city-initial placeholder so all cards are uniform height in the grid;
  imgFailed state falls back to placeholder on broken URLs
- Admin market detail: show image thumbnail + Bild-URL link in Details
- Admin edit form: live image preview below Bild-URL input
- Public detail page: contain + max-height 250px instead of cover crop
- onerror handlers hide broken images on public card and detail pages
- Time inputs changed to text + pattern for reliable 24h display
This commit is contained in:
2026-04-25 12:46:13 +02:00
parent 9d457462d5
commit 9d9520bcad
4 changed files with 84 additions and 13 deletions

View File

@@ -477,7 +477,27 @@
placeholder={mode === 'public' ? 'Name des Veranstalters' : ''}
/>
<Input label="Bild-URL" name="image_url" type="url" value={imageUrl} />
<Input
label="Bild-URL"
name="image_url"
type="url"
value={imageUrl}
oninput={(e) => {
imageUrl = e.currentTarget.value;
}}
/>
{#if imageUrl}
<div class="mt-2">
<img
src={imageUrl}
alt="Bildvorschau"
class="max-h-48 rounded-lg object-contain"
onerror={(e) => {
(e.currentTarget as HTMLImageElement).style.display = 'none';
}}
/>
</div>
{/if}
</fieldset>
<fieldset class="space-y-4">
@@ -504,7 +524,9 @@
</div>
<Input
label="Von"
type="time"
type="text"
pattern="([01][0-9]|2[0-3]):[0-5][0-9]"
placeholder="HH:MM"
value={row.open}
oninput={(e) => {
row.open = e.currentTarget.value;
@@ -512,7 +534,9 @@
/>
<Input
label="Bis"
type="time"
type="text"
pattern="([01][0-9]|2[0-3]):[0-5][0-9]"
placeholder="HH:MM"
value={row.close}
oninput={(e) => {
row.close = e.currentTarget.value;

View File

@@ -6,6 +6,7 @@
}
let { market }: Props = $props();
let imgFailed = $state(false);
function formatDate(dateStr: string): string {
return new Date(dateStr).toLocaleDateString('de-DE', {
@@ -25,14 +26,24 @@
href="/markt/{market.slug}"
class="group bg-vellum block rounded-lg border border-stone-200 shadow-sm transition-shadow hover:shadow-md dark:border-stone-700"
>
{#if market.image_url}
<div class="aspect-[16/9] overflow-hidden rounded-t-lg">
<img
src={market.image_url}
alt={market.name}
class="h-full w-full object-cover transition-transform group-hover:scale-105"
loading="lazy"
/>
{#if market.image_url && !imgFailed}
<img
src={market.image_url}
alt={market.name}
class="w-full rounded-t-lg"
style="padding: 16px 16px 0; max-height: 150px; object-fit: contain;"
loading="lazy"
onerror={() => {
imgFailed = true;
}}
/>
{:else}
<div
class="flex h-[150px] items-center justify-center rounded-t-lg bg-gradient-to-br from-stone-800 to-stone-900 dark:from-stone-900 dark:to-stone-950"
>
<span class="text-5xl font-bold text-stone-600 uppercase select-none dark:text-stone-700">
{market.city.charAt(0)}
</span>
</div>
{/if}
<div class="p-4">

View File

@@ -302,6 +302,18 @@
<!-- Market details -->
<div class="rounded-lg border border-stone-200 p-6 dark:border-stone-700">
<h2 class="mb-4 text-lg font-semibold">Details</h2>
{#if data.market.image_url}
<div class="mb-4">
<img
src={data.market.image_url}
alt={data.market.name}
class="max-h-48 rounded-lg object-contain"
onerror={(e) => {
(e.currentTarget as HTMLImageElement).style.display = 'none';
}}
/>
</div>
{/if}
<dl class="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div>
<dt class="text-sm font-medium text-stone-500 dark:text-stone-400">Beschreibung</dt>
@@ -359,6 +371,21 @@
{data.market.slug}
</dd>
</div>
{#if data.market.image_url}
<div class="sm:col-span-2">
<dt class="text-sm font-medium text-stone-500 dark:text-stone-400">Bild-URL</dt>
<dd class="mt-1 text-sm break-all text-stone-900 dark:text-stone-100">
<a
href={data.market.image_url}
target="_blank"
rel="noopener noreferrer"
class="text-primary-600 dark:text-primary-400 hover:underline"
>
{data.market.image_url}
</a>
</dd>
</div>
{/if}
</dl>
</div>

View File

@@ -253,8 +253,17 @@
</nav>
{#if market.image_url}
<div class="mb-8 overflow-hidden rounded-lg">
<img src={market.image_url} alt={market.name} class="h-64 w-full object-cover sm:h-80" />
<div class="mb-8 rounded-lg">
<img
src={market.image_url}
alt={market.name}
class="w-full rounded-lg"
style="object-fit: contain; max-height: 250px;"
onerror={(e) => {
const wrap = e.currentTarget.parentElement;
if (wrap) wrap.style.display = 'none';
}}
/>
</div>
{/if}