add animal modal and page
All checks were successful
Test and deploy staging / staging (push) Successful in 1m54s
All checks were successful
Test and deploy staging / staging (push) Successful in 1m54s
This commit is contained in:
139
src/lib/animaldex/AnimalDetails.svelte
Normal file
139
src/lib/animaldex/AnimalDetails.svelte
Normal file
@@ -0,0 +1,139 @@
|
||||
<script lang="ts" module>
|
||||
export interface AnimalDetail {
|
||||
id: number;
|
||||
species: string;
|
||||
breed: string | null;
|
||||
animalName: string | null;
|
||||
description: string | null;
|
||||
sightings: {
|
||||
id: number;
|
||||
seenAt: string;
|
||||
reporterName: string | null;
|
||||
lat: number;
|
||||
lng: number;
|
||||
photos: { id: number; url: string }[];
|
||||
}[];
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { resolve } from '$app/paths';
|
||||
import ArrowRight from 'lucide-svelte/icons/arrow-right';
|
||||
import PawPrint from 'lucide-svelte/icons/paw-print';
|
||||
|
||||
let { animal }: { animal: AnimalDetail } = $props();
|
||||
|
||||
const title = $derived(animal.animalName || animal.species);
|
||||
const allPhotos = $derived(animal.sightings.flatMap((s) => s.photos));
|
||||
let activePhotoId = $state<number | null>(null);
|
||||
const activePhoto = $derived(allPhotos.find((p) => p.id === activePhotoId) ?? allPhotos[0]);
|
||||
const firstSeen = $derived(animal.sightings.at(-1)?.seenAt);
|
||||
const lastSeen = $derived(animal.sightings[0]?.seenAt);
|
||||
|
||||
function formatDate(value: string) {
|
||||
return new Date(value).toLocaleDateString('nl-NL', {
|
||||
day: 'numeric',
|
||||
month: 'short',
|
||||
year: 'numeric'
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="grid gap-8 lg:grid-cols-[minmax(0,3fr)_minmax(0,2fr)]">
|
||||
<div>
|
||||
<div class="aspect-[4/3] overflow-hidden rounded bg-[var(--site-surface)]">
|
||||
{#if activePhoto}
|
||||
<img src={activePhoto.url} alt={title} class="h-full w-full object-cover" />
|
||||
{:else}
|
||||
<div class="flex h-full items-center justify-center text-[var(--site-muted)]">
|
||||
<PawPrint size={64} aria-hidden="true" />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if allPhotos.length > 1}
|
||||
<ul class="mt-3 flex gap-2 overflow-x-auto pb-1" aria-label="Photos">
|
||||
{#each allPhotos as photo (photo.id)}
|
||||
<li class="shrink-0">
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => (activePhotoId = photo.id)}
|
||||
aria-label="Show photo"
|
||||
aria-pressed={photo.id === activePhoto?.id}
|
||||
class="block h-16 w-16 overflow-hidden rounded border-2 {photo.id === activePhoto?.id
|
||||
? 'border-[var(--site-text)]'
|
||||
: 'border-transparent opacity-70 hover:opacity-100'}"
|
||||
>
|
||||
<img src={photo.url} alt="" class="h-full w-full object-cover" />
|
||||
</button>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 class="text-3xl font-semibold tracking-tight sm:text-4xl">{title}</h2>
|
||||
<p class="mt-2 text-lg text-[var(--site-muted)]">
|
||||
{animal.species}{animal.breed ? ` · ${animal.breed}` : ''}
|
||||
</p>
|
||||
|
||||
{#if animal.description}
|
||||
<p class="mt-5 text-base whitespace-pre-line">{animal.description}</p>
|
||||
{/if}
|
||||
|
||||
<dl class="mt-6 grid grid-cols-3 gap-4 border-y border-[var(--site-border)] py-4">
|
||||
<div>
|
||||
<dt class="text-sm text-[var(--site-muted)]">sightings</dt>
|
||||
<dd class="text-xl font-semibold">{animal.sightings.length}</dd>
|
||||
</div>
|
||||
{#if firstSeen}
|
||||
<div>
|
||||
<dt class="text-sm text-[var(--site-muted)]">first seen</dt>
|
||||
<dd class="text-base font-medium">{formatDate(firstSeen)}</dd>
|
||||
</div>
|
||||
{/if}
|
||||
{#if lastSeen}
|
||||
<div>
|
||||
<dt class="text-sm text-[var(--site-muted)]">last seen</dt>
|
||||
<dd class="text-base font-medium">{formatDate(lastSeen)}</dd>
|
||||
</div>
|
||||
{/if}
|
||||
</dl>
|
||||
|
||||
<a
|
||||
href={resolve(`/projects/animaldex/register?animalId=${animal.id}`)}
|
||||
class="mt-6 inline-flex items-center gap-2 rounded bg-[var(--site-text)] px-4 py-2.5 text-base font-medium text-[var(--site-bg)]"
|
||||
>
|
||||
i saw this animal too <ArrowRight size={16} aria-hidden="true" />
|
||||
</a>
|
||||
|
||||
<h3 class="mt-8 mb-2 text-lg font-semibold">sighting history</h3>
|
||||
<ul class="divide-y divide-[var(--site-border)] border-t border-[var(--site-border)]">
|
||||
{#each animal.sightings as s (s.id)}
|
||||
<li class="flex items-center gap-3 py-3 text-base">
|
||||
{#if s.photos[0]}
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => (activePhotoId = s.photos[0].id)}
|
||||
aria-label="Show photo from {formatDate(s.seenAt)}"
|
||||
class="h-10 w-10 shrink-0 overflow-hidden rounded"
|
||||
>
|
||||
<img src={s.photos[0].url} alt="" class="h-full w-full object-cover" />
|
||||
</button>
|
||||
{:else}
|
||||
<span
|
||||
class="flex h-10 w-10 shrink-0 items-center justify-center rounded bg-[var(--site-surface)] text-[var(--site-muted)]"
|
||||
><PawPrint size={18} aria-hidden="true" /></span
|
||||
>
|
||||
{/if}
|
||||
<span class="min-w-0">
|
||||
<span class="block">{formatDate(s.seenAt)}</span>
|
||||
{#if s.reporterName}<span class="block text-[var(--site-muted)]"
|
||||
>by {s.reporterName}</span
|
||||
>{/if}
|
||||
</span>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
Reference in New Issue
Block a user