diff --git a/src/components/Componentes/error-toast.tsx b/src/components/Componentes/error-toast.tsx index f23b366..b94a390 100644 --- a/src/components/Componentes/error-toast.tsx +++ b/src/components/Componentes/error-toast.tsx @@ -1,16 +1,18 @@ "use client"; import { useEffect, useState } from "react"; -import { IoAlertCircle, IoClose, IoCheckmarkCircle } from "react-icons/io5"; +import { IoClose } from "react-icons/io5"; type ErrorToastProps = { + title?: string; message: string; onClose: () => void; duration?: number; - variant?: "error" | "success"; + variant?: "error" | "success" | "warning" | "info"; }; export default function ErrorToast({ + title, message, onClose, duration = 4000, @@ -32,42 +34,56 @@ export default function ErrorToast({ setTimeout(onClose, 300); }; - const isSuccess = variant === "success"; + const getBorderColor = () => { + switch (variant) { + case "success": + return "border-t-[#10B981]"; + case "warning": + return "border-t-[#F59E0B]"; + case "info": + return "border-t-[#3B82F6]"; + case "error": + default: + return "border-t-[#F0445B]"; + } + }; return (
- {isSuccess ? ( - - ) : ( - - )} - - {message} - +
+ {title && ( + + {title} + + )} + + {message} + +
+
); } + diff --git a/src/components/Componentes/question-section-flow.tsx b/src/components/Componentes/question-section-flow.tsx index 69b5aa2..a9056d7 100644 --- a/src/components/Componentes/question-section-flow.tsx +++ b/src/components/Componentes/question-section-flow.tsx @@ -12,6 +12,7 @@ import QuestionProgressTracker, { } from "./question-progress-tracker"; import QuestionSnapList from "./question-snap-list"; import type { QuestionField } from "@/lib/schema-adapter"; +import ErrorToast from "./error-toast"; import NoticeBox from "./notice-box"; import AnswerPaceSheet from "@/app/questions-list/[slug]/answer-pace-sheet"; import { FixToTheEnd } from "./fix-to-the-end"; @@ -50,6 +51,7 @@ function SectionFlowContent({ const { markQuestionPassed, isCompleted } = useQuestionProgress(); const [activeQuestionIndex, setActiveQuestionIndex] = useState(0); const [isSubmitting, setIsSubmitting] = useState(false); + const [errorMessage, setErrorMessage] = useState(null); const handleQuestionExit = useCallback(() => { void flushAnswers({ force: true }); @@ -60,19 +62,46 @@ function SectionFlowContent({ return; } setIsSubmitting(true); + setErrorMessage(null); - try { + const MAX_RETRIES = 3; + let success = false; + + for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) { + try { + await flushAnswers({ force: true }); + success = true; + break; + } catch (err) { + console.warn( + `[CONTINUE] flushAnswers attempt ${attempt}/${MAX_RETRIES} failed:`, + err, + ); + if (attempt < MAX_RETRIES) { + // Silent delay between retries while maintaining loading spinner + await new Promise((resolve) => setTimeout(resolve, 800)); + } + } + } + + if (success) { markFirstEntryCompleted(); - await flushAnswers({ force: true }); - } catch { - // ignore - } finally { if (onExit) { onExit(); } else { const target = localizePath(exitHref || "/questions-list", locale); router.replace(target); } + } else { + setIsSubmitting(false); + const isPersian = locale === "fa" || locale === "fa-ir"; + const isArabic = locale === "ar"; + const msg = isPersian + ? "خطا در اتصال به اینترنت. پاسخ‌ها با سرور همگام نشدند؛ لطفاً اتصال خود را بررسی و دوباره روی ادامه بزنید." + : isArabic + ? "خطأ في الاتصال بالإنترنت. تعذر مزامنة الإجابات مع الخادم؛ يرجى التحقق من الاتصال والمحاولة مرة أخرى." + : "Internet connection error. Answers could not be synced with server. Please check your connection and tap Continue again."; + setErrorMessage(msg); } }, [exitHref, onExit, flushAnswers, locale, router, isSubmitting]); @@ -137,6 +166,14 @@ function SectionFlowContent({ {process.env.NODE_ENV === "development" ? ( ) : null} + {errorMessage && ( + setErrorMessage(null)} + duration={5000} + variant="error" + /> + )} diff --git a/src/components/Componentes/slider-page.test.tsx b/src/components/Componentes/slider-page.test.tsx index 3077a8c..adceb6d 100644 --- a/src/components/Componentes/slider-page.test.tsx +++ b/src/components/Componentes/slider-page.test.tsx @@ -41,7 +41,7 @@ vi.mock('@/translations/provider', () => ({ useI18n: () => ({ locale: 'en', dictionary: { - "Failed to update profile basic details. Please try again.": "Failed to update profile basic details. Please try again.", + "Something went wrong. Please check your internet connection and try again.": "Something went wrong. Please check your internet connection and try again.", "Accept & Continue": "Accept & Continue", }, }), @@ -134,7 +134,7 @@ describe('SliderPage', () => { fireEvent.click(finishBtn); await waitFor(() => { - expect(screen.getByRole('alert')).toHaveTextContent('Failed to update profile basic details. Please try again.'); + expect(screen.getByRole('alert')).toHaveTextContent('Something went wrong. Please check your internet connection and try again.'); }); expect(mockReplace).not.toHaveBeenCalled(); diff --git a/src/components/Componentes/slider-page.tsx b/src/components/Componentes/slider-page.tsx index 60b30c0..30d5129 100644 --- a/src/components/Componentes/slider-page.tsx +++ b/src/components/Componentes/slider-page.tsx @@ -11,6 +11,7 @@ import { import { localizePath } from "@/translations/config"; import { useI18n } from "@/translations/provider"; import Button from "./button"; +import ErrorToast from "./error-toast"; import NavigationButton from "./navigation-button"; import type { GenderAnswer, RegistrationAnswer } from "./slider-slide"; import { SliderSlideFive } from "./slider-slide-five"; @@ -116,13 +117,22 @@ export default function SliderPage({ onClose }: SliderPageProps = {}) { router.replace(localizedTarget); } catch (error) { console.error("Failed to complete onboarding:", error); - setSubmitError(t["Failed to update profile basic details. Please try again."] || "Failed to update profile basic details. Please try again."); + setSubmitError( + t["Something went wrong. Please check your internet connection and try again."] || + "Something went wrong. Please check your internet connection and try again." + ); setIsSubmitting(false); } }; return (
+ {submitError && ( + setSubmitError(null)} + /> + )}
) : activeSlide === maxSlideIndex ? ( -
- {submitError && ( -
- {submitError} -
- )} - -
+ ) : (