diff --git a/src/components/common/HtmlIsland.tsx b/src/components/common/HtmlIsland.tsx
index ce1d21a..1bb69a4 100644
--- a/src/components/common/HtmlIsland.tsx
+++ b/src/components/common/HtmlIsland.tsx
@@ -2,6 +2,7 @@ import React, { useEffect, useState, useRef, useCallback } from "react";
import { toast } from "react-toastify";
import { useTranslation } from "react-i18next";
import { Namespaces } from "../../i18n/i18n";
+import ErrorBlock from "./ErrorBlock";
export interface HtmlIslandProps {
url: string;
@@ -20,29 +21,62 @@ const HtmlIsland: React.FC = ({
const { t: tIsland } = useTranslation(Namespaces.HtmlIsland);
//
- // Load the island HTML
+ // Retry-capable fetch helper
//
- const loadIsland = useCallback(() => {
- setError(null);
-
- fetch(url, { credentials: "include" })
- .then((r) => {
+ const fetchWithRetry = useCallback(
+ async (
+ attempt: number,
+ maxAttempts: number,
+ delayMs: number,
+ ): Promise => {
+ try {
+ const r = await fetch(url, { credentials: "include" });
if (!r.ok) throw new Error(`Failed to load island: ${r.status}`);
- return r.text();
- })
- .then((text) => setHtml(text))
- .catch(() => {
- setError(tIsland("island.loadError"));
- toast.error(tIsland("island.loadError"));
- });
- }, [url, tIsland]);
+ return await r.text();
+ } catch (err) {
+ if (attempt >= maxAttempts) {
+ throw err;
+ }
+
+ const jitter = Math.random() * 150;
+ const nextDelay = delayMs * Math.pow(2, attempt) + jitter;
+
+ await new Promise((resolve) => setTimeout(resolve, nextDelay));
+
+ return fetchWithRetry(attempt + 1, maxAttempts, delayMs);
+ }
+ },
+ [url],
+ );
//
- // Initial load + reload when URL changes
+ // Initial load + reload when URL changes (with retry + cancellation)
//
useEffect(() => {
- loadIsland();
- }, [loadIsland]);
+ let cancelled = false;
+
+ const load = async () => {
+ setError(null);
+
+ try {
+ const text = await fetchWithRetry(0, 4, 300);
+ if (!cancelled) {
+ setHtml(text);
+ }
+ } catch {
+ if (!cancelled) {
+ setError(tIsland("island.loadError"));
+ toast.error(tIsland("island.loadError"));
+ }
+ }
+ };
+
+ load();
+
+ return () => {
+ cancelled = true;
+ };
+ }, [url, tIsland, fetchWithRetry]);
//
// After HTML is injected, run scripts + bind form handling
@@ -98,8 +132,7 @@ const HtmlIsland: React.FC = ({
form.getAttribute("data-full-page-redirect") === "true";
if (shouldFullPageRedirect) {
- // Do NOT intercept this form – let the browser handle POST + redirect
- return;
+ return; // allow normal POST + redirect
}
const submitHandler = async (e: Event) => {
@@ -131,7 +164,14 @@ const HtmlIsland: React.FC = ({
toast.success(
successMessage ? successMessage : tIsland("island.saveSuccess"),
);
- loadIsland();
+ // reload island
+ try {
+ const text = await fetchWithRetry(0, 4, 300);
+ setHtml(text);
+ } catch {
+ setError(tIsland("island.loadError"));
+ toast.error(tIsland("island.loadError"));
+ }
return;
}
@@ -150,28 +190,14 @@ const HtmlIsland: React.FC = ({
form.addEventListener("submit", submitHandler);
- // Cleanup on re-render
return () => {
form.removeEventListener("submit", submitHandler);
};
- }, [html, islandId, successMessage, tIsland, loadIsland]);
+ }, [html, islandId, successMessage, tIsland, fetchWithRetry]);
return (
- {error && (
-
- {error}
-
- )}
+