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.
116 lines
3.3 KiB
TypeScript
116 lines
3.3 KiB
TypeScript
"use client"
|
|
|
|
import { useRef } from "react"
|
|
import { Plus, User, X } from "lucide-react"
|
|
import { toast } from "sonner"
|
|
import { ContactAvatar } from "@/components/gmail/contacts/contact-avatar"
|
|
import { readAvatarFromFile } from "@/lib/contact-avatar"
|
|
import { cn } from "@/lib/utils"
|
|
import {
|
|
CONTACTS_PAGE_AVATAR_ADD_BADGE_CLASS,
|
|
CONTACTS_PAGE_AVATAR_PLACEHOLDER_LARGE_CLASS,
|
|
CONTACTS_PANEL_AVATAR_PLACEHOLDER_CLASS,
|
|
} from "@/lib/contacts-chrome-classes"
|
|
|
|
interface ContactAvatarPickerProps {
|
|
avatarUrl?: string
|
|
displayName: string
|
|
email?: string
|
|
onChange: (avatarUrl: string | undefined) => void
|
|
variant?: "panel" | "page"
|
|
className?: string
|
|
}
|
|
|
|
export function ContactAvatarPicker({
|
|
avatarUrl,
|
|
displayName,
|
|
email,
|
|
onChange,
|
|
variant = "panel",
|
|
className,
|
|
}: ContactAvatarPickerProps) {
|
|
const fileRef = useRef<HTMLInputElement>(null)
|
|
const isPage = variant === "page"
|
|
|
|
async function handleFileChange(e: React.ChangeEvent<HTMLInputElement>) {
|
|
const file = e.target.files?.[0]
|
|
e.target.value = ""
|
|
if (!file) return
|
|
try {
|
|
const dataUrl = await readAvatarFromFile(file)
|
|
onChange(dataUrl)
|
|
} catch (err) {
|
|
toast.error(err instanceof Error ? err.message : "Impossible d'ajouter la photo.")
|
|
}
|
|
}
|
|
|
|
function openPicker() {
|
|
fileRef.current?.click()
|
|
}
|
|
|
|
function removePhoto(e: React.MouseEvent) {
|
|
e.stopPropagation()
|
|
onChange(undefined)
|
|
}
|
|
|
|
const hasPhoto = !!avatarUrl || !!displayName
|
|
|
|
return (
|
|
<div className={cn("relative flex flex-col items-center", className)}>
|
|
<button
|
|
type="button"
|
|
onClick={openPicker}
|
|
className="group relative rounded-full focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40"
|
|
aria-label={avatarUrl ? "Changer la photo" : "Ajouter une photo"}
|
|
>
|
|
{hasPhoto ? (
|
|
<ContactAvatar
|
|
avatarUrl={avatarUrl}
|
|
name={displayName}
|
|
email={email}
|
|
size={isPage ? "2xl" : "lg"}
|
|
/>
|
|
) : isPage ? (
|
|
<div className={CONTACTS_PAGE_AVATAR_PLACEHOLDER_LARGE_CLASS}>
|
|
<User className="h-12 w-12" />
|
|
</div>
|
|
) : (
|
|
<div className={CONTACTS_PANEL_AVATAR_PLACEHOLDER_CLASS}>
|
|
<User className="h-8 w-8" />
|
|
</div>
|
|
)}
|
|
|
|
<div className={CONTACTS_PAGE_AVATAR_ADD_BADGE_CLASS}>
|
|
<Plus className="h-4 w-4" />
|
|
</div>
|
|
|
|
{avatarUrl ? (
|
|
<span
|
|
role="button"
|
|
tabIndex={0}
|
|
onClick={removePhoto}
|
|
onKeyDown={(e) => {
|
|
if (e.key === "Enter" || e.key === " ") {
|
|
e.preventDefault()
|
|
onChange(undefined)
|
|
}
|
|
}}
|
|
className="absolute -right-1 -top-1 flex size-6 items-center justify-center rounded-full border border-border bg-background text-muted-foreground opacity-0 shadow-sm transition-opacity group-hover:opacity-100 hover:text-foreground"
|
|
aria-label="Supprimer la photo"
|
|
>
|
|
<X className="h-3.5 w-3.5" />
|
|
</span>
|
|
) : null}
|
|
</button>
|
|
|
|
<input
|
|
ref={fileRef}
|
|
type="file"
|
|
accept="image/jpeg,image/png,image/gif,image/webp"
|
|
className="hidden"
|
|
onChange={handleFileChange}
|
|
/>
|
|
</div>
|
|
)
|
|
}
|