diff --git a/app/features/user-page/routes/u.$identifier.edit-widgets.tsx b/app/features/user-page/routes/u.$identifier.edit-widgets.tsx index 7ae301769..6cebe446c 100644 --- a/app/features/user-page/routes/u.$identifier.edit-widgets.tsx +++ b/app/features/user-page/routes/u.$identifier.edit-widgets.tsx @@ -15,10 +15,11 @@ import { } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; import { Search as SearchIcon } from "lucide-react"; -import { useState } from "react"; +import { useRef, useState } from "react"; import { flushSync } from "react-dom"; import { useTranslation } from "react-i18next"; import { Link, useFetcher, useLoaderData, useMatches } from "react-router"; +import * as R from "remeda"; import * as v from "valibot"; import { SendouButton } from "~/components/elements/Button"; import { Input } from "~/components/Input"; @@ -34,6 +35,7 @@ import { } from "~/features/user-page/core/widgets/portfolio"; import { getWidgetFormSchema } from "~/features/user-page/core/widgets/widget-form-schemas"; import { USER } from "~/features/user-page/user-page-constants"; +import { useUnsavedChangesChecker } from "~/form/UnsavedChangesGuard"; import { useHydrated } from "~/hooks/useHydrated"; import { useHasRole } from "~/modules/permissions/hooks"; import invariant from "~/utils/invariant"; @@ -67,6 +69,14 @@ export default function EditWidgetsPage() { >(data.currentWidgets); const [expandedWidgetId, setExpandedWidgetId] = useState(null); + const hasUnsavedChangesRef = useRef< + Parameters[0]["current"] + >(() => false); + hasUnsavedChangesRef.current = () => + fetcher.state === "idle" && + !R.isDeepEqual(selectedWidgets, data.currentWidgets); + useUnsavedChangesChecker(hasUnsavedChangesRef); + const mainWidgets = selectedWidgets.filter((w) => { const def = findWidgetById(w.id); return def?.slot === "main"; diff --git a/changelog/2026-09-05-widget-profile-for-everyone.md b/changelog/2026-09-05-widget-profile-for-everyone.md index 3a227f36e..a4fd807af 100644 --- a/changelog/2026-09-05-widget-profile-for-everyone.md +++ b/changelog/2026-09-05-widget-profile-for-everyone.md @@ -6,6 +6,7 @@ New user profile page now out for everyone and the default - Default layout roughly equivalent to old page: weapon pool, X Rank peaks, badges, bio, teams, verified social links, sensitivity and member number - Customize the widgets to build your own personal page +- Leaving the widget editor with unsaved changes will now warn you - Still Supporter exclusive: custom colors and some widgets (markdown bio, links, favorite stage, game badges, unverified peak XP and supporter since). Supporters also get more widget slots: 6 main and 7 side instead of 4 and 5. - Bios written in markdown now render formatted on the Plus Server voting page too - Battlefy account name is no longer part of the profile