Some checks are pending
E2E / Playwright e2e (push) Waiting to run
- Refactored metadata for contacts, administration, and Ulticards pages to utilize dynamic app names and descriptions. - Introduced new product pages for Ultiai, Ultical, Ulticards, Ultidrive, Ultimail, and Ultimeet with appropriate metadata. - Enhanced layout components to ensure consistent styling and functionality across new product sections. - Updated various components to replace hardcoded labels with dynamic references to improve maintainability and consistency.
162 lines
5.5 KiB
TypeScript
162 lines
5.5 KiB
TypeScript
"use client"
|
||
|
||
import { useMemo, useState } from "react"
|
||
import { format } from "date-fns"
|
||
import { InvitationTimeChipText } from "@/components/gmail/invitation-time-chip-text"
|
||
import { AgendaMark } from "@/components/suite/agenda-mark"
|
||
import { Icon } from "@iconify/react"
|
||
import { ThumbsDown, ThumbsUp, Users, MoreVertical } from "lucide-react"
|
||
import {
|
||
VIDEO_CONFERENCE_LOGOS,
|
||
formatInvitationAttendeeLine,
|
||
type ParsedCalendarInvitation,
|
||
} from "@/lib/calendar-invitation"
|
||
import { ensureVcLogosCollection } from "@/lib/register-vc-logos"
|
||
import { cn } from "@/lib/utils"
|
||
import { MAIL_INVITATION_CARD_CLASS } from "@/lib/mail-chrome-classes"
|
||
|
||
function attendeeDisplayList(inv: ParsedCalendarInvitation): {
|
||
organizerLine?: string
|
||
othersLine?: string
|
||
} {
|
||
const orgEmail = inv.organizer?.email
|
||
const organizerLine =
|
||
orgEmail || inv.organizer?.name
|
||
? `${orgEmail ?? inv.organizer?.name} – Organisateur`
|
||
: undefined
|
||
|
||
const others = inv.attendees.filter((a) => {
|
||
const e = a.email?.toLowerCase()
|
||
return e && e !== orgEmail?.toLowerCase()
|
||
})
|
||
const line = formatInvitationAttendeeLine(
|
||
others.length > 0 ? others : inv.attendees,
|
||
4
|
||
)
|
||
return {
|
||
organizerLine,
|
||
othersLine: line || undefined,
|
||
}
|
||
}
|
||
|
||
const RSVP_BTN =
|
||
"rounded-full bg-[#1a73e8] px-4 py-2 text-sm font-medium text-white shadow-sm transition-colors hover:bg-[#1557b0]"
|
||
|
||
const RSVP_SECONDARY =
|
||
"rounded-full border border-border bg-mail-surface px-4 py-2 text-sm font-medium text-primary transition-colors hover:bg-accent"
|
||
|
||
export function CalendarInvitationPreview({
|
||
invitation,
|
||
className,
|
||
calendarAppName = "Agenda",
|
||
}: {
|
||
invitation: ParsedCalendarInvitation
|
||
className?: string
|
||
/** Nom de l'app calendrier affiché dans le bouton « Ouvrir dans … ». */
|
||
calendarAppName?: string
|
||
}) {
|
||
ensureVcLogosCollection()
|
||
|
||
const { organizerLine, othersLine } = useMemo(
|
||
() => attendeeDisplayList(invitation),
|
||
[invitation]
|
||
)
|
||
|
||
const confIcon = VIDEO_CONFERENCE_LOGOS[invitation.conferenceProvider]
|
||
|
||
const [rsvp, setRsvp] = useState<string | null>(null)
|
||
|
||
return (
|
||
<div
|
||
className={cn(
|
||
MAIL_INVITATION_CARD_CLASS,
|
||
className
|
||
)}
|
||
>
|
||
<div className="flex flex-col gap-3 md:flex-row md:items-start md:justify-between md:gap-4">
|
||
<div className="min-w-0 flex-1 space-y-2">
|
||
<div className="flex flex-wrap items-center gap-2 text-sm text-muted-foreground">
|
||
<Icon icon={confIcon} className="size-5 shrink-0" aria-hidden />
|
||
<InvitationTimeChipText
|
||
start={invitation.start}
|
||
end={invitation.end}
|
||
/>
|
||
</div>
|
||
<h2 className="text-xl font-normal leading-snug text-foreground">
|
||
{invitation.summary}
|
||
</h2>
|
||
{organizerLine && (
|
||
<p className="text-sm text-foreground/90">{organizerLine}</p>
|
||
)}
|
||
{othersLine && (
|
||
<p className="flex flex-wrap items-start gap-1.5 text-sm text-foreground/90">
|
||
<Users className="mt-0.5 size-4 shrink-0 text-muted-foreground" aria-hidden />
|
||
<span>{othersLine}</span>
|
||
</p>
|
||
)}
|
||
</div>
|
||
|
||
<div className="flex shrink-0 flex-row items-start gap-3 md:flex-col md:items-end">
|
||
<div className="flex size-12 shrink-0 items-center justify-center rounded-lg border border-border bg-mail-surface shadow-sm">
|
||
<AgendaMark className="size-9 object-contain" />
|
||
</div>
|
||
<div className="min-w-0 text-right text-sm leading-snug text-muted-foreground">
|
||
<p className="font-medium text-foreground">Dans votre agenda</p>
|
||
<p className="mt-0.5">Aucun autre événement à cette date</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="mt-4 flex flex-wrap items-center gap-2">
|
||
{(["Oui", "Non", "Peut-être"] as const).map((label) => (
|
||
<button
|
||
key={label}
|
||
type="button"
|
||
className={cn(RSVP_BTN, rsvp === label && "ring-2 ring-[#202124]/30")}
|
||
onClick={() => setRsvp(label)}
|
||
>
|
||
{label}
|
||
</button>
|
||
))}
|
||
<button type="button" className={RSVP_SECONDARY}>
|
||
Proposer un autre horaire
|
||
</button>
|
||
<a
|
||
href={`/agenda/day/${format(invitation.start, "yyyy-MM-dd")}`}
|
||
className={RSVP_SECONDARY}
|
||
>
|
||
Ouvrir dans {calendarAppName}
|
||
</a>
|
||
<button
|
||
type="button"
|
||
className="ml-auto flex size-10 items-center justify-center rounded-full border border-border bg-mail-surface text-muted-foreground hover:bg-accent md:ml-0"
|
||
aria-label="Plus d’options"
|
||
>
|
||
<MoreVertical className="size-[18px]" strokeWidth={1.5} />
|
||
</button>
|
||
</div>
|
||
|
||
<div className="mt-4 flex flex-wrap items-center justify-between gap-2 border-t border-border/60 pt-3 text-xs text-muted-foreground">
|
||
<span>D’après cet e-mail</span>
|
||
<div className="flex items-center gap-2">
|
||
<span>Correct ?</span>
|
||
<button
|
||
type="button"
|
||
className="rounded p-1 text-[#5f6368] hover:bg-black/5"
|
||
aria-label="Oui, correct"
|
||
>
|
||
<ThumbsUp className="size-4" strokeWidth={1.5} />
|
||
</button>
|
||
<button
|
||
type="button"
|
||
className="rounded p-1 text-[#5f6368] hover:bg-black/5"
|
||
aria-label="Non, incorrect"
|
||
>
|
||
<ThumbsDown className="size-4" strokeWidth={1.5} />
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|