Some checks are pending
E2E / Playwright e2e (push) Waiting to run
- Introduced new ContactAvatar and ContactAvatarPicker components for enhanced avatar management in contact views. - Updated ContactDetailView and ContactFormView to utilize the new avatar components, improving user experience when adding or editing contacts. - Enhanced ContactHoverCard and ContactRow components to display avatars, providing a more visually appealing interface. - Added loading and error states in ContactsListView for better user feedback during data fetching. - Implemented a new ContactsLoadState component to handle loading and error scenarios in the contacts list. - Updated package.json to include @formkit/auto-animate for improved UI animations.
79 lines
2.7 KiB
TypeScript
79 lines
2.7 KiB
TypeScript
"use client"
|
|
|
|
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
|
|
import { gravatarUrl, primaryContactEmail } from "@/lib/contact-avatar"
|
|
import { avatarColor, senderInitial } from "@/lib/sender-display"
|
|
import { cn } from "@/lib/utils"
|
|
import type { FullContact } from "@/lib/contacts/types"
|
|
|
|
export type ContactAvatarSize = "xs" | "sm" | "md" | "lg" | "xl" | "2xl"
|
|
|
|
const SIZE_CONFIG: Record<
|
|
ContactAvatarSize,
|
|
{ className: string; gravatar: number; text: string }
|
|
> = {
|
|
xs: { className: "size-8", gravatar: 64, text: "text-xs" },
|
|
sm: { className: "size-10", gravatar: 80, text: "text-sm" },
|
|
md: { className: "size-14", gravatar: 112, text: "text-lg" },
|
|
lg: { className: "size-20", gravatar: 160, text: "text-2xl" },
|
|
xl: { className: "size-24", gravatar: 192, text: "text-3xl" },
|
|
"2xl": { className: "size-28", gravatar: 224, text: "text-4xl" },
|
|
}
|
|
|
|
export interface ContactAvatarProps {
|
|
contact?: Pick<FullContact, "avatarUrl" | "emails" | "firstName" | "lastName">
|
|
/** Override display name for initials fallback. */
|
|
name?: string
|
|
/** Override email for Gravatar fallback. */
|
|
email?: string
|
|
/** Override stored avatar URL. */
|
|
avatarUrl?: string
|
|
size?: ContactAvatarSize
|
|
className?: string
|
|
alt?: string
|
|
}
|
|
|
|
export function contactAvatarLabel(
|
|
contact: Pick<FullContact, "firstName" | "lastName" | "emails"> | undefined,
|
|
nameOverride?: string,
|
|
emailOverride?: string,
|
|
): string {
|
|
if (nameOverride?.trim()) return nameOverride.trim()
|
|
if (contact) {
|
|
const fromName = `${contact.firstName ?? ""} ${contact.lastName ?? ""}`.trim()
|
|
if (fromName) return fromName
|
|
}
|
|
return emailOverride?.trim() || primaryContactEmail(contact ?? {}) || "?"
|
|
}
|
|
|
|
export function ContactAvatar({
|
|
contact,
|
|
name: nameOverride,
|
|
email: emailOverride,
|
|
avatarUrl: avatarUrlOverride,
|
|
size = "sm",
|
|
className,
|
|
alt,
|
|
}: ContactAvatarProps) {
|
|
const config = SIZE_CONFIG[size]
|
|
const name = contactAvatarLabel(contact, nameOverride, emailOverride)
|
|
const email = emailOverride?.trim() || primaryContactEmail(contact ?? {})
|
|
const avatarUrl = avatarUrlOverride ?? contact?.avatarUrl
|
|
const gravatar = email ? gravatarUrl(email, config.gravatar) : undefined
|
|
const initial = senderInitial(name)
|
|
const color = avatarColor(name)
|
|
|
|
return (
|
|
<Avatar className={cn("shrink-0", config.className, className)}>
|
|
{avatarUrl ? <AvatarImage src={avatarUrl} alt={alt ?? name} /> : null}
|
|
{gravatar ? <AvatarImage src={gravatar} alt={alt ?? name} /> : null}
|
|
<AvatarFallback
|
|
className={cn("font-medium text-white", config.text)}
|
|
style={{ backgroundColor: color }}
|
|
>
|
|
{initial}
|
|
</AvatarFallback>
|
|
</Avatar>
|
|
)
|
|
}
|