Skip to content

Commit dbf8083

Browse files
committed
Fix persona dialog review feedback
1 parent 17b5b77 commit dbf8083

4 files changed

Lines changed: 232 additions & 168 deletions

File tree

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
import * as React from "react";
2+
import { Upload } from "lucide-react";
3+
4+
import { ProfileAvatar } from "@/features/profile/ui/ProfileAvatar";
5+
import { useAvatarUpload } from "@/features/profile/useAvatarUpload";
6+
import { cn } from "@/shared/lib/cn";
7+
import { Spinner } from "@/shared/ui/spinner";
8+
9+
function isAvatarFileDrag(event: React.DragEvent<HTMLElement>) {
10+
return Array.from(event.dataTransfer.types).includes("Files");
11+
}
12+
13+
export function AgentCreationPreview({
14+
avatarUrl,
15+
disabled = false,
16+
label,
17+
onUploadPendingChange,
18+
onSelectAvatar,
19+
}: {
20+
avatarUrl: string | null;
21+
disabled?: boolean;
22+
label: string;
23+
onUploadPendingChange?: (isPending: boolean) => void;
24+
onSelectAvatar: (avatarUrl: string) => void;
25+
}) {
26+
const [isDragOverAvatar, setIsDragOverAvatar] = React.useState(false);
27+
const avatarDragDepthRef = React.useRef(0);
28+
const {
29+
inputRef: avatarUploadInputRef,
30+
isUploading,
31+
errorMessage: uploadErrorMessage,
32+
clearError: clearUploadError,
33+
openPicker: openUploadPicker,
34+
uploadFile: uploadAvatarFile,
35+
handleFileChange: handleAvatarUploadFileChange,
36+
} = useAvatarUpload({
37+
onUploadSuccess: onSelectAvatar,
38+
});
39+
40+
React.useEffect(() => {
41+
onUploadPendingChange?.(isUploading);
42+
return () => {
43+
onUploadPendingChange?.(false);
44+
};
45+
}, [isUploading, onUploadPendingChange]);
46+
47+
const handleAvatarDragEnter = React.useCallback(
48+
(event: React.DragEvent<HTMLFieldSetElement>) => {
49+
if (disabled || !isAvatarFileDrag(event)) {
50+
return;
51+
}
52+
event.preventDefault();
53+
event.stopPropagation();
54+
avatarDragDepthRef.current += 1;
55+
event.dataTransfer.dropEffect = "copy";
56+
setIsDragOverAvatar(true);
57+
},
58+
[disabled],
59+
);
60+
61+
const handleAvatarDragOver = React.useCallback(
62+
(event: React.DragEvent<HTMLFieldSetElement>) => {
63+
if (disabled || !isAvatarFileDrag(event)) {
64+
return;
65+
}
66+
event.preventDefault();
67+
event.stopPropagation();
68+
event.dataTransfer.dropEffect = "copy";
69+
setIsDragOverAvatar(true);
70+
},
71+
[disabled],
72+
);
73+
74+
const handleAvatarDragLeave = React.useCallback(
75+
(event: React.DragEvent<HTMLFieldSetElement>) => {
76+
if (!isAvatarFileDrag(event)) {
77+
return;
78+
}
79+
event.preventDefault();
80+
event.stopPropagation();
81+
avatarDragDepthRef.current = Math.max(0, avatarDragDepthRef.current - 1);
82+
if (avatarDragDepthRef.current === 0) {
83+
setIsDragOverAvatar(false);
84+
}
85+
},
86+
[],
87+
);
88+
89+
const handleAvatarDrop = React.useCallback(
90+
(event: React.DragEvent<HTMLFieldSetElement>) => {
91+
if (!isAvatarFileDrag(event)) {
92+
return;
93+
}
94+
event.preventDefault();
95+
event.stopPropagation();
96+
avatarDragDepthRef.current = 0;
97+
setIsDragOverAvatar(false);
98+
99+
const file = event.dataTransfer.files[0];
100+
if (!file || disabled || isUploading) {
101+
return;
102+
}
103+
104+
clearUploadError();
105+
void uploadAvatarFile(file);
106+
},
107+
[clearUploadError, disabled, isUploading, uploadAvatarFile],
108+
);
109+
110+
return (
111+
<div className="mx-auto w-full max-w-[220px] lg:sticky lg:top-0">
112+
<fieldset
113+
aria-label="Agent avatar preview"
114+
className={cn(
115+
"group/avatar-preview relative m-0 aspect-[4/5] min-h-[240px] min-w-0 overflow-hidden rounded-xl border border-border/70 bg-muted/50 p-0 shadow-xs transition-[background-color,border-color,box-shadow] duration-150",
116+
isDragOverAvatar &&
117+
"border-dashed border-primary/70 bg-primary/5 ring-2 ring-primary/15",
118+
)}
119+
onDragEnter={handleAvatarDragEnter}
120+
onDragLeave={handleAvatarDragLeave}
121+
onDragOver={handleAvatarDragOver}
122+
onDrop={handleAvatarDrop}
123+
>
124+
<input
125+
accept="image/gif,image/jpeg,image/png,image/webp"
126+
className="hidden"
127+
onChange={handleAvatarUploadFileChange}
128+
ref={avatarUploadInputRef}
129+
type="file"
130+
/>
131+
132+
<div className="absolute inset-0 flex items-center justify-center">
133+
<ProfileAvatar
134+
avatarUrl={avatarUrl}
135+
className="h-36 w-36 text-4xl"
136+
label={label}
137+
/>
138+
</div>
139+
140+
{uploadErrorMessage ? (
141+
<p className="absolute inset-x-3 bottom-12 rounded-md bg-background/95 px-2 py-1 text-center text-xs text-destructive shadow-xs">
142+
{uploadErrorMessage}
143+
</p>
144+
) : null}
145+
146+
<div className="absolute inset-x-3 bottom-3 flex justify-center">
147+
<button
148+
className="inline-flex h-8 translate-y-1 items-center justify-center gap-1.5 rounded-full border border-border/70 bg-background/90 px-3 text-xs font-medium text-foreground opacity-0 shadow-xs transition-[background-color,opacity,transform] duration-150 hover:bg-muted focus-visible:translate-y-0 focus-visible:opacity-100 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring group-hover/avatar-preview:translate-y-0 group-hover/avatar-preview:opacity-100 group-focus-within/avatar-preview:translate-y-0 group-focus-within/avatar-preview:opacity-100"
149+
disabled={disabled || isUploading}
150+
onClick={() => {
151+
clearUploadError();
152+
openUploadPicker();
153+
}}
154+
type="button"
155+
>
156+
{isUploading ? (
157+
<Spinner className="h-3.5 w-3.5 border-2" />
158+
) : (
159+
<Upload className="h-3.5 w-3.5" />
160+
)}
161+
{isUploading ? "Uploading..." : "Edit avatar"}
162+
</button>
163+
</div>
164+
</fieldset>
165+
</div>
166+
);
167+
}

0 commit comments

Comments
 (0)