feat(calendar): show contact avatars on event cards
This commit is contained in:
@@ -960,6 +960,32 @@ function threadChannelLabel(thread: { id: string; channels: CommItem["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
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -1437,6 +1463,9 @@ onBeforeUnmount(() => {
|
||||
:focused-calendar-event="focusedCalendarEvent"
|
||||
:format-day="formatDay"
|
||||
:format-time="formatTime"
|
||||
:avatar-src-for-calendar-event="avatarSrcForCalendarEvent"
|
||||
:mark-calendar-avatar-broken="markCalendarAvatarBroken"
|
||||
:contact-initials="contactInitials"
|
||||
:set-calendar-content-wrap-ref="setCalendarContentWrapRef"
|
||||
:shift-calendar="shiftCalendar"
|
||||
:set-calendar-content-scroll-ref="setCalendarContentScrollRef"
|
||||
|
||||
@@ -1,12 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
type CalendarEvent = {
|
||||
id: string;
|
||||
title: string;
|
||||
start: string;
|
||||
end: string;
|
||||
contact: string;
|
||||
note: string;
|
||||
};
|
||||
import type { CalendarEvent } from "~~/app/composables/useCalendar";
|
||||
|
||||
type YearMonthItem = {
|
||||
monthIndex: number;
|
||||
@@ -48,6 +41,9 @@ defineProps<{
|
||||
focusedCalendarEvent: CalendarEvent | null;
|
||||
formatDay: (iso: string) => string;
|
||||
formatTime: (iso: string) => string;
|
||||
avatarSrcForCalendarEvent: (event: CalendarEvent) => string;
|
||||
markCalendarAvatarBroken: (event: CalendarEvent) => void;
|
||||
contactInitials: (contactName: string) => string;
|
||||
setCalendarContentWrapRef: (element: HTMLDivElement | null) => void;
|
||||
shiftCalendar: (step: number) => 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-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">
|
||||
{{ formatDay(focusedCalendarEvent.start) }} · {{ formatTime(focusedCalendarEvent.start) }} - {{ formatTime(focusedCalendarEvent.end) }}
|
||||
</p>
|
||||
@@ -285,11 +298,27 @@ defineProps<{
|
||||
<button
|
||||
v-for="event in monthCellEvents(cell.events)"
|
||||
: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' : ''"
|
||||
@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>
|
||||
</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'"
|
||||
@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>
|
||||
<p v-if="day.events.length === 0" class="pt-1 text-xs text-base-content/50">No events</p>
|
||||
</div>
|
||||
@@ -351,7 +397,23 @@ defineProps<{
|
||||
@click="openThreadFromCalendarItem(event)"
|
||||
>
|
||||
<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="mt-1 text-sm text-base-content/80">{{ event.note }}</p>
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user