Browse Source

fix(question-file): add upload progress percentage, fix border jumping on tap, sync initial stored files, and fix hardware back history handling

master
mortezaei 2 days ago
parent
commit
ceb2c88593
  1. 2
      src/app/questions-list/questions-list-client.tsx
  2. 49
      src/components/Componentes/question-file.tsx
  3. 15
      src/hooks/marriage/use-upload-tmp-media.ts

2
src/app/questions-list/questions-list-client.tsx

@ -147,7 +147,7 @@ export default function QuestionsListClient() {
if (typeof window !== "undefined") {
const url = new URL(window.location.href);
url.searchParams.set("section", slug);
window.history.pushState({ section: slug }, "", url.toString());
window.history.replaceState({ section: slug }, "", url.toString());
}
}, []);

49
src/components/Componentes/question-file.tsx

@ -234,9 +234,17 @@ export function QuestionFile({
parseInitialDocs(storedValue),
);
const [isUploading, setIsUploading] = useState(false);
const [uploadProgress, setUploadProgress] = useState<number | null>(null);
const isInitiatorRef = useRef(false);
const fileInputRef = useRef<HTMLInputElement>(null);
// Sync state when answers load asynchronously from server/cache
useEffect(() => {
if (storedValue !== undefined && storedValue !== null) {
setUploadedDocs(parseInitialDocs(storedValue));
}
}, [storedValue, parseInitialDocs]);
const acceptedFiles = (question.extras?.options ?? [])
.map((option) => option.replace(/^\./, ""))
.join(", ");
@ -262,10 +270,12 @@ export function QuestionFile({
});
}
setIsUploading(false);
setUploadProgress(null);
},
onError: (error) => {
console.error("File upload error:", error);
setIsUploading(false);
setUploadProgress(null);
},
});
@ -284,11 +294,20 @@ export function QuestionFile({
case "picking":
break;
case "picked":
case "progress":
setIsUploading(true);
setUploadProgress(0);
break;
case "progress": {
setIsUploading(true);
const p = (event.data as any)?.progress;
if (typeof p === "number") {
setUploadProgress(Math.round(p));
}
break;
}
case "completed": {
setIsUploading(false);
setUploadProgress(null);
isInitiatorRef.current = false;
const incomingFiles = event.data?.files || [];
const newDocs: UploadedDoc[] = [];
@ -341,6 +360,7 @@ export function QuestionFile({
case "cancelled":
case "failed":
setIsUploading(false);
setUploadProgress(null);
isInitiatorRef.current = false;
break;
}
@ -380,12 +400,13 @@ export function QuestionFile({
if (!files || files.length === 0) return;
setIsUploading(true);
setUploadProgress(0);
const newDocs: UploadedDoc[] = [];
for (let i = 0; i < files.length; i++) {
const file = files[i];
try {
const res = await uploadTmpMedia(file);
const res = await uploadTmpMedia(file, (p) => setUploadProgress(p));
if (res.path) {
const resolved = res.path.startsWith("http")
? res.path
@ -402,6 +423,7 @@ export function QuestionFile({
}
setIsUploading(false);
setUploadProgress(null);
if (newDocs.length > 0) {
setUploadedDocs((prev) => {
@ -484,10 +506,10 @@ export function QuestionFile({
: undefined
}
className={[
"relative flex w-full flex-col items-center justify-center rounded-[29px] border-2 border-dashed border-[#8D8D8D] bg-[#F7F7F7] text-center transition-all duration-200 focus-within:outline-2 focus-within:outline-offset-4 focus-within:outline-[#6F6F6F] hover:border-[#777777] active:border-[#111111] overflow-hidden p-4",
"relative flex w-full flex-col items-center justify-center rounded-[29px] border-2 border-dashed border-[#8D8D8D] bg-[#F7F7F7] text-center transition-colors duration-200 hover:border-[#6F6F6F] overflow-hidden select-none",
hasDocuments
? "min-h-[156px]"
: "aspect-[727/330] min-h-[156px] cursor-pointer",
? "min-h-[156px] p-3.5"
: "aspect-[727/330] min-h-[156px] cursor-pointer p-4",
].join(" ")}
>
{hasDocuments ? (
@ -608,12 +630,21 @@ export function QuestionFile({
</div>
)}
{/* Smooth Circular Loading Spinner Overlay (Matching Flutter ProfileAvatar) */}
{/* Smooth Circular Loading Spinner Overlay with Upload Progress */}
{isPending && (
<div className="absolute inset-0 z-30 flex flex-col items-center justify-center rounded-[29px] bg-black/40 backdrop-blur-[1px] transition-all duration-200 gap-2">
<div className="h-8 w-8 animate-spin rounded-full border-[2.5px] border-white border-t-transparent shadow-sm" />
<div className="absolute inset-0 z-30 flex flex-col items-center justify-center rounded-[29px] bg-black/50 backdrop-blur-[1px] transition-all duration-200 gap-2">
<div className="relative flex items-center justify-center">
<div className="h-10 w-10 animate-spin rounded-full border-[2.5px] border-white border-t-transparent shadow-sm" />
{uploadProgress !== null && uploadProgress > 0 && (
<span className="absolute text-[10px] font-bold text-white leading-none">
{uploadProgress}%
</span>
)}
</div>
<span className="text-xs font-semibold text-white">
{t.uploading}
{uploadProgress !== null && uploadProgress > 0
? `${t.uploading} (${uploadProgress}%)`
: t.uploading}
</span>
</div>
)}

15
src/hooks/marriage/use-upload-tmp-media.ts

@ -8,7 +8,10 @@ import type { UploadTmpMediaResponse } from "./types";
const CSRF_TOKEN =
"53kqNKySTv3q4K3OolQqLEgaeF9pdPdAEnxrMARaUfvFrIGK57Qje67ifYUDMUQP";
export async function uploadTmpMedia(file: File) {
export async function uploadTmpMedia(
file: File,
onProgress?: (progressPercent: number) => void,
) {
const formData = new FormData();
formData.append("file", file);
@ -20,6 +23,14 @@ export async function uploadTmpMedia(file: File) {
Accept: "application/json",
"X-CSRFToken": CSRF_TOKEN,
},
onUploadProgress: (progressEvent) => {
if (progressEvent.total && onProgress) {
const percent = Math.round(
(progressEvent.loaded * 100) / progressEvent.total,
);
onProgress(percent);
}
},
},
);
@ -31,6 +42,6 @@ export function useUploadTmpMediaMutation(
) {
return useMutation({
...options,
mutationFn: uploadTmpMedia,
mutationFn: (file: File) => uploadTmpMedia(file),
});
}
Loading…
Cancel
Save