Skip to content

Commit 0192e34

Browse files
github-actions[bot]ItsnotakaMarfuen
authored
[dev] [Itsnotaka] daniel/confirm-before-upload-screenshot (#1850)
* feat(tasks): add screenshot reminder dialog for file uploads * feat(comments): implement screenshot reminder dialog for file uploads * refactor(comments): remove unused interfaces and clean up code --------- Co-authored-by: Daniel Fu <[email protected]> Co-authored-by: Mariano Fuentes <[email protected]>
1 parent e1e2d2a commit 0192e34

File tree

2 files changed

+123
-48
lines changed

2 files changed

+123
-48
lines changed

apps/app/src/app/(app)/[orgId]/tasks/[taskId]/components/TaskBody.tsx

Lines changed: 65 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,19 @@
22

33
import { useTaskAttachmentActions, useTaskAttachments } from '@/hooks/use-tasks-api';
44
import { Button } from '@comp/ui/button';
5-
import { FileIcon, FileText, ImageIcon, Loader2, Upload, X } from 'lucide-react';
5+
import {
6+
Dialog,
7+
DialogContent,
8+
DialogDescription,
9+
DialogFooter,
10+
DialogHeader,
11+
DialogTitle,
12+
} from '@comp/ui/dialog';
13+
import { Camera, FileIcon, FileText, ImageIcon, Loader2, Upload, X } from 'lucide-react';
614
import type React from 'react';
715
import { useCallback, useEffect, useRef, useState } from 'react';
816
import { toast } from 'sonner';
917

10-
// Removed ApiAttachment interface - using database Attachment type directly
11-
1218
interface TaskBodyProps {
1319
taskId: string;
1420
title?: string;
@@ -37,6 +43,8 @@ export function TaskBody({
3743
const [isUploading, setIsUploading] = useState(false);
3844
const [busyAttachmentId, setBusyAttachmentId] = useState<string | null>(null);
3945
const [isDragging, setIsDragging] = useState(false);
46+
const [showReminderDialog, setShowReminderDialog] = useState(false);
47+
const [pendingFiles, setPendingFiles] = useState<FileList | File[] | null>(null);
4048

4149
// Auto-resize function for textarea
4250
const autoResizeTextarea = useCallback(() => {
@@ -161,14 +169,35 @@ export function TaskBody({
161169
[uploadAttachment, refreshAttachments],
162170
);
163171

164-
// Handle multiple file uploads using API
172+
const initiateUpload = useCallback((files: FileList | File[]) => {
173+
if (!files || files.length === 0) return;
174+
setPendingFiles(files);
175+
setShowReminderDialog(true);
176+
}, []);
177+
178+
const handleReminderConfirm = useCallback(() => {
179+
setShowReminderDialog(false);
180+
if (pendingFiles) {
181+
processFiles(pendingFiles);
182+
setPendingFiles(null);
183+
}
184+
}, [pendingFiles, processFiles]);
185+
186+
const handleReminderClose = useCallback(() => {
187+
setShowReminderDialog(false);
188+
setPendingFiles(null);
189+
if (fileInputRef.current) {
190+
fileInputRef.current.value = '';
191+
}
192+
}, []);
193+
165194
const handleFileSelectMultiple = useCallback(
166-
async (event: React.ChangeEvent<HTMLInputElement>) => {
195+
(event: React.ChangeEvent<HTMLInputElement>) => {
167196
const files = event.target.files;
168197
if (!files || files.length === 0) return;
169-
await processFiles(files);
198+
initiateUpload(files);
170199
},
171-
[processFiles],
200+
[initiateUpload],
172201
);
173202

174203
const triggerFileInput = () => {
@@ -199,7 +228,7 @@ export function TaskBody({
199228
}, []);
200229

201230
const handleDrop = useCallback(
202-
async (e: React.DragEvent) => {
231+
(e: React.DragEvent) => {
203232
e.preventDefault();
204233
e.stopPropagation();
205234
setIsDragging(false);
@@ -208,10 +237,10 @@ export function TaskBody({
208237

209238
const files = e.dataTransfer.files;
210239
if (files && files.length > 0) {
211-
await processFiles(files);
240+
initiateUpload(Array.from(files));
212241
}
213242
},
214-
[isUploading, busyAttachmentId, processFiles],
243+
[isUploading, busyAttachmentId, initiateUpload],
215244
);
216245

217246
const handleDownloadClick = async (attachmentId: string) => {
@@ -395,6 +424,32 @@ export function TaskBody({
395424
</div>
396425
)}
397426
</div>
427+
428+
<Dialog open={showReminderDialog} onOpenChange={(open) => !open && handleReminderClose()}>
429+
<DialogContent className="sm:max-w-[425px]">
430+
<DialogHeader>
431+
<div className="flex items-center gap-3">
432+
<div className="rounded-full bg-primary/10 p-2">
433+
<Camera className="h-5 w-5 text-primary" />
434+
</div>
435+
<DialogTitle>Screenshot Requirements</DialogTitle>
436+
</div>
437+
<DialogDescription className="pt-2">
438+
Ensure your organisation name is clearly visible within the screenshot.
439+
</DialogDescription>
440+
</DialogHeader>
441+
<p className="text-sm text-muted-foreground">
442+
Auditors require this to verify the source of the data; without it, evidence may be
443+
rejected.
444+
</p>
445+
<DialogFooter>
446+
<Button variant="outline" onClick={handleReminderClose}>
447+
Cancel
448+
</Button>
449+
<Button onClick={handleReminderConfirm}>Continue Upload</Button>
450+
</DialogFooter>
451+
</DialogContent>
452+
</Dialog>
398453
</div>
399454
);
400455
}

apps/app/src/components/comments/CommentForm.tsx

Lines changed: 58 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,39 @@
11
'use client';
22

33
import { useComments, useCommentWithAttachments } from '@/hooks/use-comments-api';
4-
import { authClient } from '@/utils/auth-client';
54
import { Button } from '@comp/ui/button';
5+
import {
6+
Dialog,
7+
DialogContent,
8+
DialogDescription,
9+
DialogFooter,
10+
DialogHeader,
11+
DialogTitle,
12+
} from '@comp/ui/dialog';
613
import { Textarea } from '@comp/ui/textarea';
714
import type { CommentEntityType } from '@db';
8-
import { FileIcon, Loader2, Paperclip, X } from 'lucide-react';
9-
import { useParams } from 'next/navigation';
15+
import { Camera, FileIcon, Loader2, Paperclip, X } from 'lucide-react';
1016
import type React from 'react';
11-
import { useCallback, useEffect, useRef, useState } from 'react';
17+
import { useCallback, useRef, useState } from 'react';
1218
import { toast } from 'sonner';
1319

1420
interface CommentFormProps {
1521
entityId: string;
1622
entityType: CommentEntityType;
1723
}
1824

19-
// Removed PendingAttachment interface - using File objects directly with API hooks
20-
2125
export function CommentForm({ entityId, entityType }: CommentFormProps) {
22-
const session = authClient.useSession();
23-
const params = useParams();
2426
const [newComment, setNewComment] = useState('');
2527
const [pendingFiles, setPendingFiles] = useState<File[]>([]);
2628
const [isSubmitting, setIsSubmitting] = useState(false);
2729
const fileInputRef = useRef<HTMLInputElement>(null);
28-
const [hasMounted, setHasMounted] = useState(false);
30+
const [showReminderDialog, setShowReminderDialog] = useState(false);
31+
const [filesToAdd, setFilesToAdd] = useState<File[]>([]);
2932

3033
// Use SWR hooks for generic comments
3134
const { mutate: refreshComments } = useComments(entityId, entityType);
3235
const { createCommentWithFiles } = useCommentWithAttachments();
3336

34-
useEffect(() => {
35-
setHasMounted(true);
36-
}, []);
37-
3837
const triggerFileInput = () => {
3938
fileInputRef.current?.click();
4039
};
@@ -52,42 +51,38 @@ export function CommentForm({ entityId, entityType }: CommentFormProps) {
5251
for (const file of newFiles) {
5352
if (file.size > MAX_FILE_SIZE_BYTES) {
5453
toast.error(`File "${file.name}" exceeds the ${MAX_FILE_SIZE_MB}MB limit.`);
54+
if (fileInputRef.current) fileInputRef.current.value = '';
5555
return;
5656
}
5757
}
5858

59-
// Add files to pending list
60-
setPendingFiles((prev) => [...prev, ...newFiles]);
59+
setFilesToAdd(newFiles);
60+
setShowReminderDialog(true);
61+
}, []);
62+
63+
const handleReminderConfirm = useCallback(() => {
64+
setShowReminderDialog(false);
65+
if (filesToAdd.length > 0) {
66+
setPendingFiles((prev) => [...prev, ...filesToAdd]);
67+
filesToAdd.forEach((file) => {
68+
toast.success(`File "${file.name}" ready for attachment.`);
69+
});
70+
setFilesToAdd([]);
71+
}
6172
if (fileInputRef.current) fileInputRef.current.value = '';
73+
}, [filesToAdd]);
6274

63-
newFiles.forEach((file) => {
64-
toast.success(`File "${file.name}" ready for attachment.`);
65-
});
75+
const handleReminderClose = useCallback(() => {
76+
setShowReminderDialog(false);
77+
setFilesToAdd([]);
78+
if (fileInputRef.current) fileInputRef.current.value = '';
6679
}, []);
6780

6881
const handleRemovePendingFile = (fileIndexToRemove: number) => {
6982
setPendingFiles((prev) => prev.filter((_, index) => index !== fileIndexToRemove));
7083
toast.info('File removed from comment draft.');
7184
};
7285

73-
const handlePendingFileClick = (fileIndex: number) => {
74-
const file = pendingFiles[fileIndex];
75-
if (!file) {
76-
console.error('Could not find pending file for index:', fileIndex);
77-
toast.error('Could not find file data.');
78-
return;
79-
}
80-
81-
// Create object URL for preview
82-
const url = URL.createObjectURL(file);
83-
84-
// Open in new tab
85-
window.open(url, '_blank', 'noopener,noreferrer');
86-
87-
// Clean up the object URL after a short delay
88-
setTimeout(() => URL.revokeObjectURL(url), 100);
89-
};
90-
9186
const handleCommentSubmit = async () => {
9287
if (!newComment.trim() && pendingFiles.length === 0) return;
9388

@@ -115,7 +110,6 @@ export function CommentForm({ entityId, entityType }: CommentFormProps) {
115110

116111
// Always show the actual form - no loading gate
117112
// Users can start typing immediately, authentication is checked on submit
118-
119113
const handleKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
120114
if (
121115
(event.metaKey || event.ctrlKey) &&
@@ -167,7 +161,7 @@ export function CommentForm({ entityId, entityType }: CommentFormProps) {
167161
<button
168162
onClick={() => handleRemovePendingFile(index)}
169163
disabled={isSubmitting}
170-
className="text-muted-foreground hover:text-destructive transition-colors flex-shrink-0"
164+
className="text-muted-foreground hover:text-destructive transition-colors shrink-0"
171165
aria-label={`Remove ${file.name}`}
172166
>
173167
<X className="h-3 w-3" />
@@ -202,6 +196,32 @@ export function CommentForm({ entityId, entityType }: CommentFormProps) {
202196
</div>
203197
</div>
204198
</div>
199+
200+
<Dialog open={showReminderDialog} onOpenChange={(open) => !open && handleReminderClose()}>
201+
<DialogContent className="sm:max-w-[425px]">
202+
<DialogHeader>
203+
<div className="flex items-center gap-3">
204+
<div className="rounded-full bg-primary/10 p-2">
205+
<Camera className="h-5 w-5 text-primary" />
206+
</div>
207+
<DialogTitle>Screenshot Requirements</DialogTitle>
208+
</div>
209+
<DialogDescription className="pt-2">
210+
Ensure your organisation name is clearly visible within the screenshot.
211+
</DialogDescription>
212+
</DialogHeader>
213+
<p className="text-sm text-muted-foreground">
214+
Auditors require this to verify the source of the data; without it, evidence may be
215+
rejected.
216+
</p>
217+
<DialogFooter>
218+
<Button variant="outline" onClick={handleReminderClose}>
219+
Cancel
220+
</Button>
221+
<Button onClick={handleReminderConfirm}>Continue</Button>
222+
</DialogFooter>
223+
</DialogContent>
224+
</Dialog>
205225
</div>
206226
);
207227
}

0 commit comments

Comments
 (0)