feat(calendar): show contact avatars on event cards

This commit is contained in:
Ruslan Bakiev
2026-02-26 12:18:14 +07:00
parent 5063dfdecf
commit 0a470d3922
2 changed files with 103 additions and 12 deletions

View File

@@ -960,6 +960,32 @@ function threadChannelLabel(thread: { id: string; channels: CommItem["channel"][
return "No channel"; return "No channel";
} }
const contactByName = computed(() => {
const map = new Map<string, Contact>();
for (const contact of contacts.value) {
const key = (contact.name ?? "").trim();
if (!key || map.has(key)) continue;
map.set(key, contact);
}
return map;
});
function avatarSrcForCalendarEvent(event: CalendarEvent) {
const contactName = String(event.contact ?? "").trim();
if (!contactName) return "";
const contact = contactByName.value.get(contactName);
if (!contact) return "";
return avatarSrcForThread({ id: contact.id, avatar: contact.avatar });
}
function markCalendarAvatarBroken(event: CalendarEvent) {
const contactName = String(event.contact ?? "").trim();
if (!contactName) return;
const contact = contactByName.value.get(contactName);
if (!contact) return;
markAvatarBroken(contact.id);
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Auth display // Auth display
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -1437,6 +1463,9 @@ onBeforeUnmount(() => {
:focused-calendar-event="focusedCalendarEvent" :focused-calendar-event="focusedCalendarEvent"
:format-day="formatDay" :format-day="formatDay"
:format-time="formatTime" :format-time="formatTime"
:avatar-src-for-calendar-event="avatarSrcForCalendarEvent"
:mark-calendar-avatar-broken="markCalendarAvatarBroken"
:contact-initials="contactInitials"
:set-calendar-content-wrap-ref="setCalendarContentWrapRef" :set-calendar-content-wrap-ref="setCalendarContentWrapRef"
:shift-calendar="shiftCalendar" :shift-calendar="shiftCalendar"
:set-calendar-content-scroll-ref="setCalendarContentScrollRef" :set-calendar-content-scroll-ref="setCalendarContentScrollRef"

View File

@@ -1,12 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
type CalendarEvent = { import type { CalendarEvent } from "~~/app/composables/useCalendar";
id: string;
title: string;
start: string;
end: string;
contact: string;
note: string;
};
type YearMonthItem = { type YearMonthItem = {
monthIndex: number; monthIndex: number;
@@ -48,6 +41,9 @@ defineProps<{
focusedCalendarEvent: CalendarEvent | null; focusedCalendarEvent: CalendarEvent | null;
formatDay: (iso: string) => string; formatDay: (iso: string) => string;
formatTime: (iso: string) => string; formatTime: (iso: string) => string;
avatarSrcForCalendarEvent: (event: CalendarEvent) => string;
markCalendarAvatarBroken: (event: CalendarEvent) => void;
contactInitials: (contactName: string) => string;
setCalendarContentWrapRef: (element: HTMLDivElement | null) => void; setCalendarContentWrapRef: (element: HTMLDivElement | null) => void;
shiftCalendar: (step: number) => void; shiftCalendar: (step: number) => void;
setCalendarContentScrollRef: (element: HTMLDivElement | null) => void; setCalendarContentScrollRef: (element: HTMLDivElement | null) => void;
@@ -138,6 +134,23 @@ defineProps<{
> >
<p class="text-xs font-semibold uppercase tracking-wide text-success/80">Review focus event</p> <p class="text-xs font-semibold uppercase tracking-wide text-success/80">Review focus event</p>
<p class="text-sm font-medium text-base-content">{{ focusedCalendarEvent.title }}</p> <p class="text-sm font-medium text-base-content">{{ focusedCalendarEvent.title }}</p>
<div class="mt-1 flex items-center gap-1.5">
<div class="avatar shrink-0">
<div class="h-5 w-5 rounded-full ring-1 ring-base-300/70">
<img
v-if="avatarSrcForCalendarEvent(focusedCalendarEvent)"
:src="avatarSrcForCalendarEvent(focusedCalendarEvent)"
:alt="focusedCalendarEvent.contact"
@error="markCalendarAvatarBroken(focusedCalendarEvent)"
/>
<span
v-else
class="flex h-full w-full items-center justify-center text-[9px] font-semibold text-base-content/65"
>{{ contactInitials(focusedCalendarEvent.contact) }}</span>
</div>
</div>
<p class="truncate text-xs text-base-content/70">{{ focusedCalendarEvent.contact || "Unknown contact" }}</p>
</div>
<p class="text-xs text-base-content/70"> <p class="text-xs text-base-content/70">
{{ formatDay(focusedCalendarEvent.start) }} · {{ formatTime(focusedCalendarEvent.start) }} - {{ formatTime(focusedCalendarEvent.end) }} {{ formatDay(focusedCalendarEvent.start) }} · {{ formatTime(focusedCalendarEvent.start) }} - {{ formatTime(focusedCalendarEvent.end) }}
</p> </p>
@@ -285,11 +298,27 @@ defineProps<{
<button <button
v-for="event in monthCellEvents(cell.events)" v-for="event in monthCellEvents(cell.events)"
:key="event.id" :key="event.id"
class="block w-full truncate rounded px-1 text-left text-[10px] text-base-content/70 transition hover:underline" class="block w-full rounded px-1 text-left text-[10px] text-base-content/70 transition hover:underline"
:class="isReviewHighlightedEvent(event.id) ? 'bg-success/20 text-success-content ring-1 ring-success/40' : ''" :class="isReviewHighlightedEvent(event.id) ? 'bg-success/20 text-success-content ring-1 ring-success/40' : ''"
@click.stop="openThreadFromCalendarItem(event)" @click.stop="openThreadFromCalendarItem(event)"
> >
{{ formatTime(event.start) }} {{ event.title }} <span class="flex items-center gap-1">
<span class="avatar shrink-0">
<span class="inline-flex h-3.5 w-3.5 rounded-full ring-1 ring-base-300/70">
<img
v-if="avatarSrcForCalendarEvent(event)"
:src="avatarSrcForCalendarEvent(event)"
:alt="event.contact"
@error="markCalendarAvatarBroken(event)"
/>
<span
v-else
class="flex h-full w-full items-center justify-center text-[7px] font-semibold text-base-content/65"
>{{ contactInitials(event.contact) }}</span>
</span>
</span>
<span class="truncate">{{ formatTime(event.start) }} {{ event.title }}</span>
</span>
</button> </button>
</button> </button>
</div> </div>
@@ -329,7 +358,24 @@ defineProps<{
:class="isReviewHighlightedEvent(event.id) ? 'bg-success/20 ring-1 ring-success/45' : 'bg-base-200 hover:bg-base-300/80'" :class="isReviewHighlightedEvent(event.id) ? 'bg-success/20 ring-1 ring-success/45' : 'bg-base-200 hover:bg-base-300/80'"
@click.stop="openThreadFromCalendarItem(event)" @click.stop="openThreadFromCalendarItem(event)"
> >
{{ formatTime(event.start) }} - {{ event.title }} ({{ event.contact }}) <div class="flex items-center gap-1.5">
<div class="avatar shrink-0">
<div class="h-5 w-5 rounded-full ring-1 ring-base-300/70">
<img
v-if="avatarSrcForCalendarEvent(event)"
:src="avatarSrcForCalendarEvent(event)"
:alt="event.contact"
@error="markCalendarAvatarBroken(event)"
/>
<span
v-else
class="flex h-full w-full items-center justify-center text-[8px] font-semibold text-base-content/65"
>{{ contactInitials(event.contact) }}</span>
</div>
</div>
<span class="truncate">{{ formatTime(event.start) }} - {{ event.title }}</span>
</div>
<p class="ml-7 mt-0.5 truncate text-[11px] text-base-content/65">{{ event.contact }}</p>
</button> </button>
<p v-if="day.events.length === 0" class="pt-1 text-xs text-base-content/50">No events</p> <p v-if="day.events.length === 0" class="pt-1 text-xs text-base-content/50">No events</p>
</div> </div>
@@ -351,7 +397,23 @@ defineProps<{
@click="openThreadFromCalendarItem(event)" @click="openThreadFromCalendarItem(event)"
> >
<p class="font-medium">{{ event.title }}</p> <p class="font-medium">{{ event.title }}</p>
<p class="text-xs text-base-content/60">{{ event.contact }}</p> <div class="mt-1 flex items-center gap-1.5">
<div class="avatar shrink-0">
<div class="h-6 w-6 rounded-full ring-1 ring-base-300/70">
<img
v-if="avatarSrcForCalendarEvent(event)"
:src="avatarSrcForCalendarEvent(event)"
:alt="event.contact"
@error="markCalendarAvatarBroken(event)"
/>
<span
v-else
class="flex h-full w-full items-center justify-center text-[9px] font-semibold text-base-content/65"
>{{ contactInitials(event.contact) }}</span>
</div>
</div>
<p class="truncate text-xs text-base-content/60">{{ event.contact }}</p>
</div>
<p class="text-xs text-base-content/60">{{ formatTime(event.start) }} - {{ formatTime(event.end) }}</p> <p class="text-xs text-base-content/60">{{ formatTime(event.start) }} - {{ formatTime(event.end) }}</p>
<p class="mt-1 text-sm text-base-content/80">{{ event.note }}</p> <p class="mt-1 text-sm text-base-content/80">{{ event.note }}</p>
</button> </button>