返回 presentation-ai
contributor-static.tsx
根目录 / src / components / notebook / presentation / editor / custom-elements / static / contributor-static.tsx
1 import { formatDistanceToNow } from "date-fns";
2 import { UserRound } from "lucide-react";
3 import { SlateElement, type SlateElementProps } from "platejs/static";
4 import type * as React from "react";
5
6 import { usePresentationOwner } from "@/hooks/presentation/usePresentationOwner";
7 import { cn } from "@/lib/utils";
8 import { usePresentationState } from "@/states/presentation-state";
9 import { type TContributorElement } from "../../lib";
10
11 function getJustifyClass(alignment: TContributorElement["alignment"]) {
12 if (alignment === "left") return "justify-start";
13 if (alignment === "right") return "justify-end";
14 return "justify-center";
15 }
16
17 function getOwnerDisplayName(name: string | null | undefined) {
18 return name?.trim() ? name : "Unknown owner";
19 }
20
21 function getInitials(name: string) {
22 return name
23 .split(/[\s@._-]+/)
24 .filter(Boolean)
25 .slice(0, 2)
26 .map((part) => part[0]?.toUpperCase())
27 .join("");
28 }
29
30 function getEditedLabel(updatedAt: string | null | undefined) {
31 if (!updatedAt) return "Just now";
32
33 const updatedDate = new Date(updatedAt);
34 if (Number.isNaN(updatedDate.getTime())) {
35 return "Just now";
36 }
37
38 return `Last edited ${formatDistanceToNow(updatedDate, { addSuffix: true })}`;
39 }
40
41 export function ContributorStatic(
42 props: SlateElementProps<TContributorElement>,
43 ) {
44 const presentationId = usePresentationState((s) => s.currentPresentationId);
45 const updatedAt = usePresentationState((s) => s.currentPresentationUpdatedAt);
46 const { data: owner } = usePresentationOwner(presentationId);
47 const ownerName = getOwnerDisplayName(owner?.name);
48 const editedLabel =
49 props.element.editedLabel ??
50 getEditedLabel(updatedAt ?? props.element.updatedAt ?? null);
51 const avatarUrl = owner?.image;
52 const avatarStyle = avatarUrl
53 ? ({
54 backgroundImage: `url(${JSON.stringify(avatarUrl)})`,
55 } satisfies React.CSSProperties)
56 : undefined;
57 const initials = getInitials(ownerName);
58 const alignment = props.element.alignment ?? "left";
59 const accentColor = "var(--presentation-primary)";
60 const backgroundColor = props.element.backgroundColor ?? "transparent";
61 const textColor =
62 props.element.textColor ??
63 props.element.color ??
64 "var(--presentation-heading)";
65 const style = {
66 "--presentation-contributor-accent": accentColor,
67 "--presentation-contributor-background": backgroundColor,
68 "--presentation-contributor-text": textColor,
69 } as React.CSSProperties;
70
71 return (
72 <SlateElement
73 {...props}
74 className={cn("relative my-2 flex w-full", getJustifyClass(alignment))}
75 style={style}
76 >
77 <div className="flex items-center gap-3 rounded-xl bg-(--presentation-contributor-background) px-4 py-3 text-(--presentation-contributor-text)">
78 <div
79 aria-label={avatarUrl ? ownerName : undefined}
80 className="flex size-12 shrink-0 items-center justify-center overflow-hidden rounded-full border border-(--presentation-contributor-accent)/50 bg-(--presentation-contributor-accent)/20 bg-cover bg-center text-(--presentation-contributor-accent)"
81 role={avatarUrl ? "img" : undefined}
82 style={avatarStyle}
83 >
84 {avatarUrl ? null : initials ? (
85 <span className="text-sm font-bold">{initials}</span>
86 ) : (
87 <UserRound className="size-6" />
88 )}
89 </div>
90 <div className="min-w-0">
91 <div className="text-2xl leading-tight font-bold">
92 <span>by </span>
93 <span>{ownerName}</span>
94 </div>
95 <div className="text-base leading-snug opacity-75">{editedLabel}</div>
96 </div>
97 </div>
98 </SlateElement>
99 );
100 }
101
101 lines Plain Text