Skip to content

Commit 408533f

Browse files
Add chat attachments UI features - keep only chat attachment files, drop wallet changes
1 parent c7469be commit 408533f

7 files changed

Lines changed: 869 additions & 68 deletions

File tree

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
"use client";
2+
3+
import { useRef } from "react";
4+
import { cn } from "@/lib/cn";
5+
import { Icon, ICON_PATHS } from "@/components/ui/Icon";
6+
7+
interface AttachmentButtonProps {
8+
onFilesSelected: (files: File[]) => void;
9+
disabled?: boolean;
10+
className?: string;
11+
accept?: string;
12+
multiple?: boolean;
13+
}
14+
15+
export function AttachmentButton({
16+
onFilesSelected,
17+
disabled = false,
18+
className,
19+
accept = "*/*",
20+
multiple = true,
21+
}: AttachmentButtonProps) {
22+
const fileInputRef = useRef<HTMLInputElement>(null);
23+
24+
const handleClick = () => {
25+
if (!disabled) {
26+
fileInputRef.current?.click();
27+
}
28+
};
29+
30+
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
31+
const files = Array.from(e.target.files || []);
32+
if (files.length > 0) {
33+
onFilesSelected(files);
34+
}
35+
// Reset input so same files can be selected again
36+
if (fileInputRef.current) {
37+
fileInputRef.current.value = "";
38+
}
39+
};
40+
41+
return (
42+
<>
43+
<input
44+
ref={fileInputRef}
45+
type="file"
46+
accept={accept}
47+
multiple={multiple}
48+
onChange={handleFileChange}
49+
className="hidden"
50+
aria-label="Attach files"
51+
/>
52+
<button
53+
type="button"
54+
onClick={handleClick}
55+
disabled={disabled}
56+
className={cn(
57+
"p-2.5 rounded-xl transition-all duration-200",
58+
disabled
59+
? cn(
60+
"bg-background text-text-secondary",
61+
"shadow-[3px_3px_6px_#d1d5db,-3px_-3px_6px_#ffffff]",
62+
"cursor-not-allowed"
63+
)
64+
: cn(
65+
"bg-background text-text-primary",
66+
"shadow-[3px_3px_6px_#d1d5db,-3px_-3px_6px_#ffffff]",
67+
"hover:shadow-[2px_2px_4px_#d1d5db,-2px_-2px_4px_#ffffff]",
68+
"active:shadow-[inset_2px_2px_4px_#d1d5db,inset_-2px_-2px_4px_#ffffff]"
69+
),
70+
className
71+
)}
72+
title="Attach files"
73+
aria-label="Attach files"
74+
>
75+
<Icon path={ICON_PATHS.paperclip} size="md" />
76+
</button>
77+
</>
78+
);
79+
}
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
"use client";
2+
3+
import { useState, useEffect } from "react";
4+
import { cn } from "@/lib/cn";
5+
import { Icon, ICON_PATHS, LoadingSpinner } from "@/components/ui/Icon";
6+
import { getFileTypeCategory, formatFileSize } from "@/lib/api/chat-attachments";
7+
import type { FileUploadProgress } from "@/types/chat.types";
8+
9+
interface AttachmentPreviewProps {
10+
uploadProgress: FileUploadProgress;
11+
onCancel: () => void;
12+
onRetry?: () => void;
13+
className?: string;
14+
}
15+
16+
export function AttachmentPreview({
17+
uploadProgress,
18+
onCancel,
19+
onRetry,
20+
className,
21+
}: AttachmentPreviewProps) {
22+
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
23+
const fileCategory = getFileTypeCategory(uploadProgress.mimeType);
24+
25+
useEffect(() => {
26+
// Generate preview for images
27+
if (fileCategory === "image" && uploadProgress.status !== "error") {
28+
// In a real implementation, you'd create an object URL from the file
29+
// For now, we'll use a placeholder
30+
setPreviewUrl(null);
31+
}
32+
}, [fileCategory, uploadProgress.status]);
33+
34+
const getIconForFileType = () => {
35+
switch (fileCategory) {
36+
case "image":
37+
return ICON_PATHS.image;
38+
case "video":
39+
return ICON_PATHS.video;
40+
case "document":
41+
return ICON_PATHS.document;
42+
default:
43+
return ICON_PATHS.file;
44+
}
45+
};
46+
47+
return (
48+
<div
49+
className={cn(
50+
"flex items-center gap-3 p-3 rounded-xl",
51+
"bg-background",
52+
"shadow-[inset_2px_2px_4px_#d1d5db,inset_-2px_-2px_4px_#ffffff]",
53+
className
54+
)}
55+
>
56+
{/* File thumbnail or icon */}
57+
<div className="shrink-0 w-12 h-12 rounded-lg overflow-hidden bg-primary/5 flex items-center justify-center">
58+
{previewUrl ? (
59+
<img src={previewUrl} alt={uploadProgress.fileName} className="w-full h-full object-cover" />
60+
) : (
61+
<Icon path={getIconForFileType()} size="md" className="text-text-secondary" />
62+
)}
63+
</div>
64+
65+
{/* File info */}
66+
<div className="flex-1 min-w-0">
67+
<p className="text-sm font-medium text-text-primary truncate">{uploadProgress.fileName}</p>
68+
<div className="flex items-center gap-2 mt-1">
69+
<span className="text-xs text-text-secondary">{formatFileSize(uploadProgress.fileSize)}</span>
70+
{uploadProgress.status === "uploading" && (
71+
<span className="text-xs text-text-secondary">{uploadProgress.progress}%</span>
72+
)}
73+
{uploadProgress.status === "error" && (
74+
<span className="text-xs text-error">• Failed to upload</span>
75+
)}
76+
{uploadProgress.status === "completed" && (
77+
<span className="text-xs text-green-600">• Uploaded</span>
78+
)}
79+
</div>
80+
</div>
81+
82+
{/* Status indicator */}
83+
<div className="shrink-0">
84+
{uploadProgress.status === "uploading" && (
85+
<div className="flex items-center gap-2">
86+
<LoadingSpinner size="sm" className="text-primary" />
87+
<button
88+
type="button"
89+
onClick={onCancel}
90+
className="p-1 rounded hover:bg-border-light transition-colors"
91+
title="Cancel upload"
92+
>
93+
<Icon path={ICON_PATHS.close} size="sm" className="text-text-secondary" />
94+
</button>
95+
</div>
96+
)}
97+
{uploadProgress.status === "completed" && (
98+
<button
99+
type="button"
100+
onClick={onCancel}
101+
className="p-1 rounded hover:bg-border-light transition-colors"
102+
title="Remove"
103+
>
104+
<Icon path={ICON_PATHS.close} size="sm" className="text-text-secondary" />
105+
</button>
106+
)}
107+
{uploadProgress.status === "error" && (
108+
<div className="flex items-center gap-2">
109+
<button
110+
type="button"
111+
onClick={onRetry}
112+
className="p-1 rounded hover:bg-border-light transition-colors"
113+
title="Retry upload"
114+
>
115+
<Icon path={ICON_PATHS.refresh} size="sm" className="text-text-secondary" />
116+
</button>
117+
<button
118+
type="button"
119+
onClick={onCancel}
120+
className="p-1 rounded hover:bg-border-light transition-colors"
121+
title="Remove"
122+
>
123+
<Icon path={ICON_PATHS.close} size="sm" className="text-text-secondary" />
124+
</button>
125+
</div>
126+
)}
127+
</div>
128+
129+
{/* Progress bar */}
130+
{uploadProgress.status === "uploading" && (
131+
<div className="absolute bottom-0 left-0 right-0 h-1 bg-border-light rounded-b-xl overflow-hidden">
132+
<div
133+
className="h-full bg-primary transition-all duration-300"
134+
style={{ width: `${uploadProgress.progress}%` }}
135+
/>
136+
</div>
137+
)}
138+
</div>
139+
);
140+
}

0 commit comments

Comments
 (0)