ultisuite-client/components/gmail/contacts/contact-avatar-picker.tsx
R3D347HR4Y 07d57f13a8
Some checks are pending
E2E / Playwright e2e (push) Waiting to run
Add Contact Avatar Features and Improve UI Components
- 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.
2026-06-06 20:26:51 +02:00

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>
)
}