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";
|
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"
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user