feat(activities): UI tweaks across feed, detail, hosting, calendar, scan, shell #91

Merged
padreug merged 25 commits from feat/ui-tweaks into dev 2026-06-10 16:35:50 +00:00
2 changed files with 57 additions and 32 deletions
Showing only changes of commit 1dfb025df3 - Show all commits

feat(activities): refine activity card for pending/rejected + compact

- Wash out pending/rejected events with opacity-50 + grayscale on a
  wrapper div so the operator sees at a glance the event isn't live,
  not just the small badge.
- Pull the status badge OUT of the wash-out wrapper and absolute-
  position it on Card root (bottom-2 left-2, z-10) so it stays in
  full color above the dim card. Both pending and rejected use the
  destructive token — the label text differentiates the two states.
  Bottom-left so it doesn't collide with the category chip on full
  cards or the thumbnail on compact ones.
- Compact rows in the Hosting view now show a small left-aligned
  thumbnail (w-20 h-20, self-center, ml-3, rounded-md) when the
  event carries an image — host can still recognize each event at a
  glance without paying the visual weight of a full hero.
- Card root becomes `relative overflow-hidden`; the wrapper div
  owns the conditional flex-row (compact) / flex-col (default)
  layout and the opacity/grayscale toggling.
Padreug 2026-06-04 22:46:41 +02:00 committed by padreug

View file

@ -62,19 +62,46 @@ const isPast = computed(() => {
if (!end || isNaN(end.getTime())) return false if (!end || isNaN(end.getTime())) return false
return end.getTime() < Date.now() return end.getTime() < Date.now()
}) })
// Pending / rejected events get a washed-out look so the user
// sees at a glance the event isn't live, not just the small badge.
const isNonApproved = computed(
() => !!props.event.lnbitsStatus && props.event.lnbitsStatus !== 'approved',
)
</script> </script>
<template> <template>
<Card <Card
class="overflow-hidden cursor-pointer hover:shadow-lg transition-shadow duration-200 flex flex-col" class="relative cursor-pointer hover:shadow-lg transition-shadow duration-200"
@click="emit('click', event)" @click="emit('click', event)"
> >
<!-- Wash-out wrapper. The pending/rejected status badge below sits
OUTSIDE this wrapper so it stays in full color and reads
clearly even when the card is dimmed + desaturated. -->
<div
class="transition-opacity duration-200"
:class="[
compact ? 'flex flex-row' : 'flex flex-col',
isNonApproved ? 'opacity-50 grayscale hover:opacity-90' : '',
]"
>
<!-- Compact thumbnail small square preview on the left of the
row when the event carries an image. `self-center` keeps it
vertically centered against a taller content column so we
don't get a top-anchored thumb with dead space below. -->
<img
v-if="compact && event.image"
:src="event.image"
:alt="event.title"
class="w-20 h-20 object-cover shrink-0 self-center ml-3 rounded-md"
loading="lazy"
/>
<!-- Image with overlaid badges. Cards without an image (or in <!-- Image with overlaid badges. Cards without an image (or in
compact mode) skip the hero area entirely and surface their compact mode) skip the hero area entirely and surface their
badges inline at the top of the content block the solid- badges inline at the top of the content block the solid-
color placeholder + calendar glyph wasn't communicating color placeholder + calendar glyph wasn't communicating
anything the title + details don't already. --> anything the title + details don't already. -->
<div v-if="event.image && !compact" class="relative aspect-[16/9] overflow-hidden"> <div v-if="event.image && !compact" class="relative aspect-[16/9] overflow-hidden rounded-t-lg">
<img <img
:src="event.image" :src="event.image"
:alt="event.title" :alt="event.title"
@ -110,27 +137,13 @@ const isPast = computed(() => {
{{ priceDisplay }} {{ priceDisplay }}
</Badge> </Badge>
<!-- Pending/rejected overlay for the creator's own non-approved <!-- Past badge shown when the event has already ended. The
drafts. Only present when the event originated from a pending/rejected status badge that used to share this slot
local LNbits event (Nostr-sourced events have no is now an absolute overlay on Card root, above the wash-out,
lnbitsStatus). --> so we still suppress Past when isNonApproved (the status
badge is more actionable in that case). -->
<Badge <Badge
v-if="event.lnbitsStatus && event.lnbitsStatus !== 'approved'" v-if="isPast && !isNonApproved"
:variant="event.lnbitsStatus === 'rejected' ? 'destructive' : 'secondary'"
class="absolute bottom-2 left-2 text-xs capitalize"
>
{{ event.lnbitsStatus === 'rejected' ? 'Rejected' : 'Pending review' }}
</Badge>
<!-- Past badge shown when the event has already ended.
Only relevant on the feed when the "Past events" filter
chip is toggled on (otherwise these cards aren't rendered);
on the detail page the card view isn't used. Suppressed
when a pending/rejected status badge is taking the same
slot that case is the creator's own past draft, which is
vanishingly rare and the status hint is more actionable. -->
<Badge
v-if="isPast && !(event.lnbitsStatus && event.lnbitsStatus !== 'approved')"
variant="outline" variant="outline"
class="absolute bottom-2 left-2 text-xs gap-1 bg-background/80 backdrop-blur" class="absolute bottom-2 left-2 text-xs gap-1 bg-background/80 backdrop-blur"
> >
@ -158,14 +171,7 @@ const isPast = computed(() => {
Yours Yours
</Badge> </Badge>
<Badge <Badge
v-if="event.lnbitsStatus && event.lnbitsStatus !== 'approved'" v-if="isPast && !isNonApproved"
:variant="event.lnbitsStatus === 'rejected' ? 'destructive' : 'secondary'"
class="text-xs capitalize"
>
{{ event.lnbitsStatus === 'rejected' ? 'Rejected' : 'Pending review' }}
</Badge>
<Badge
v-if="isPast && !(event.lnbitsStatus && event.lnbitsStatus !== 'approved')"
variant="outline" variant="outline"
class="text-xs gap-1" class="text-xs gap-1"
> >
@ -251,5 +257,22 @@ const isPast = computed(() => {
</div> </div>
</div> </div>
</CardContent> </CardContent>
</div>
<!-- Status badge absolutely positioned on Card root so it sits
ABOVE the wash-out wrapper and keeps its full color.
Pending + rejected both lean on the destructive token so the
non-approved state reads as "needs attention" in every theme;
the label text differentiates the two specific states.
Bottom-right with a slight downward spill so it anchors
visually without competing with the category chip in the
badge row (full cards) or the thumbnail (compact cards). -->
<Badge
v-if="isNonApproved"
variant="destructive"
class="absolute -bottom-1 right-2 z-10 text-xs capitalize shadow"
>
{{ event.lnbitsStatus === 'rejected' ? 'Rejected' : 'Pending review' }}
</Badge>
</Card> </Card>
</template> </template>

View file

@ -52,10 +52,12 @@ const { t } = useI18n()
</div> </div>
<!-- Event grid compact mode collapses to a single column of <!-- Event grid compact mode collapses to a single column of
tight rows; default mode is the responsive card grid. --> tight rows; default mode is the responsive card grid. The
compact gap is bumped a notch so the status badge spilling
past the card's bottom edge has room to sit between cards. -->
<div <div
v-else v-else
:class="compact ? 'flex flex-col gap-2' : 'grid gap-4 sm:grid-cols-2 lg:grid-cols-3'" :class="compact ? 'flex flex-col gap-4' : 'grid gap-4 sm:grid-cols-2 lg:grid-cols-3'"
> >
<EventCard <EventCard
v-for="event in events" v-for="event in events"