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:
@@ -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;
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user