flotilla/src/app/components/ClassifiedItem.svelte

105 lines
4.1 KiB
Svelte
Raw Normal View History

2026-02-03 19:00:13 +00:00
<script lang="ts">
import cx from "classnames"
import {formatTimestampRelative, uniq} from "@welshman/lib"
2026-02-03 19:00:13 +00:00
import type {TrustedEvent} from "@welshman/util"
2026-07-28 16:16:26 +00:00
import {getAddress} from "@welshman/util"
import {Classified} from "@welshman/domain"
import Gallery from "@assets/icons/gallery.svg?dataurl"
import CaseMinimalistic from "@assets/icons/case-minimalistic.svg?dataurl"
import {normalizeTopic} from "@lib/util"
import Icon from "@lib/components/Icon.svelte"
2026-02-03 19:00:13 +00:00
import Link from "@lib/components/Link.svelte"
import Cv from "@lib/components/Cv.svelte"
2026-02-04 01:09:30 +00:00
import CurrencySymbol from "@lib/components/CurrencySymbol.svelte"
2026-07-28 16:16:26 +00:00
import {reader} from "@app/core"
2026-02-03 19:00:13 +00:00
import ProfileLink from "@app/components/ProfileLink.svelte"
import ClassifiedActions from "@app/components/ClassifiedActions.svelte"
import type {FeedContext} from "@app/feeds"
import UnreadDot from "@app/components/UnreadDot.svelte"
2026-06-08 21:14:40 +00:00
import {makeClassifiedPath} from "@app/routes"
2026-02-03 19:00:13 +00:00
type Props = {
url: string
event: TrustedEvent
context: FeedContext
2026-02-03 19:00:13 +00:00
}
const {url, event, context}: Props = $props()
2026-02-03 19:00:13 +00:00
const path = $derived(makeClassifiedPath(url, getAddress(event)))
2026-08-22 20:12:11 +00:00
const classified = $derived(reader(Classified)(event))
const title = $derived(classified.title())
const summary = $derived(classified.summary() || event.content)
const images = $derived(uniq(classified.images()))
const cover = $derived(images[0])
const extraImages = $derived(images.slice(1))
2026-08-22 20:12:11 +00:00
const price = $derived(classified.price())
const topics = $derived(uniq(classified.topics() ?? []).map(normalizeTopic))
const isSold = $derived(classified.status() === "sold")
// Zero is the form's default, not a claim that the listing is free.
const hasPrice = $derived(Boolean(price && price.amount > 0))
2026-02-03 19:00:13 +00:00
</script>
<Cv
tag={Link}
class="group card card-interactive card-media flex h-full w-full flex-col"
href={makeClassifiedPath(url, getAddress(event))}>
<div class="relative aspect-[4/3] w-full overflow-hidden bg-surface-more">
<UnreadDot {path} class="absolute right-3 top-3" />
{#if cover}
<img
src={cover}
alt=""
class={cx(
"h-full w-full object-cover transition-transform duration-300 group-hover:scale-105",
isSold && "opacity-40 grayscale",
)} />
{:else}
<div class="flex h-full w-full items-center justify-center">
<Icon icon={CaseMinimalistic} size={10} class="opacity-20" />
</div>
{/if}
{#if isSold}
<span class="badge badge-neutral badge-sm absolute top-3 left-3">Sold</span>
{/if}
{#if extraImages.length > 0}
<span
class="absolute right-3 bottom-3 flex items-center gap-1 rounded-full bg-black/60 px-2 py-1 text-xs font-bold text-white">
<Icon icon={Gallery} size={3} />
{images.length}
</span>
{/if}
</div>
<div class="flex min-w-0 grow flex-col gap-2 p-4">
<p class="line-clamp-2 wrap-break-word font-bold leading-snug">{title}</p>
{#if hasPrice}
<p class="text-xl leading-none font-bold text-primary">
<CurrencySymbol code={price?.currency ?? "SAT"} />{price?.amount.toLocaleString()}
2026-02-03 19:00:13 +00:00
</p>
{/if}
{#if summary}
<p class="line-clamp-2 wrap-break-word text-sm text-content-muted">{summary}</p>
{/if}
{#if topics.length > 0}
<div class="flex min-w-0 flex-wrap gap-1">
{#each topics.slice(0, 2) as topic (topic)}
<span class="badge badge-sm rounded-full font-normal">#{topic}</span>
{/each}
{#if topics.length > 2}
<span class="badge badge-sm rounded-full font-normal">+{topics.length - 2}</span>
{/if}
</div>
{/if}
2026-02-03 22:18:58 +00:00
</div>
<div
class="flex min-w-0 items-center justify-between gap-2 border-t border-solid border-line px-4 py-3">
<span class="flex min-w-0 flex-col text-xs text-content-muted">
<span class="truncate">
<ProfileLink pubkey={event.pubkey} {url} />
</span>
<span class="truncate">{formatTimestampRelative(event.created_at)}</span>
2026-02-03 19:00:13 +00:00
</span>
<ClassifiedActions {url} {event} {context} showTopics={false} showStatus={false} />
2026-02-03 19:00:13 +00:00
</div>
</Cv>