From 04c7c2a97a80277da84b32ac34e7afffa7d8027f Mon Sep 17 00:00:00 2001 From: raae Date: Thu, 30 Oct 2025 15:44:51 +0100 Subject: [PATCH 1/7] Implement tanstack form for additional forms --- src/routes/email-list.index.tsx | 102 +++++++++++++++++++++++--------- 1 file changed, 73 insertions(+), 29 deletions(-) diff --git a/src/routes/email-list.index.tsx b/src/routes/email-list.index.tsx index 001a445..294714a 100644 --- a/src/routes/email-list.index.tsx +++ b/src/routes/email-list.index.tsx @@ -1,7 +1,9 @@ -import { useEffect, useState } from "react"; +import { useEffect, useRef } from "react"; import { createFileRoute } from "@tanstack/react-router"; import { useMutation, useQuery } from "@tanstack/react-query"; import { Button, ListControls } from "@triozer/framer-toolbox"; +import { revalidateLogic, useForm } from "@tanstack/react-form"; +import { z } from "zod"; import { useConfiguration } from "../custom-code"; import { @@ -34,17 +36,61 @@ export function EmailListIndex() { enabled: !!domain && !isInvalid, }); - const [uid, setUid] = useState(undefined); - const emailLists = query.data?.items; const defaultUid = emailLists?.[0]?.Uid; - const config: EmailListEmbedConfig = { uid: uid || defaultUid || "" }; + const currentUid = singleOutsetaEmbed?.controls.emailListUid as + | string + | undefined; + + const form = useForm({ + defaultValues: { + uid: (currentUid || defaultUid || "") as string, + }, + validationLogic: revalidateLogic({ + mode: "submit", + modeAfterSubmission: "change", + }), + validators: { + // When lists exist, require a non-empty uid + onSubmit: z.object({ uid: z.string().trim() }).optional(), + }, + onSubmit: async ({ value }) => { + const cfg: EmailListEmbedConfig = { uid: value.uid }; + switch (embedMode) { + case "embed": + await embedMutation.mutateAsync(); + break; + } + }, + }); + // Keep form value in sync when controls or data load change useEffect(() => { - const uid = singleOutsetaEmbed?.controls.emailListUid as string; - setUid(uid); - }, [singleOutsetaEmbed?.controls.emailListUid]); + const next = (currentUid || defaultUid || "") as string; + // Only update if different to avoid extra renders + if (form.state.values.uid !== next) { + form.setFieldValue("uid", next); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [currentUid, defaultUid]); + + const config: EmailListEmbedConfig = { + uid: (form.useStore((s) => s.values.uid) as string) || "", + }; + + // Auto-apply on uid change when an embed exists + const didInit = useRef(false); + const selectedUid = form.useStore((s) => s.values.uid) as string | undefined; + useEffect(() => { + if (!didInit.current) { + didInit.current = true; + return; + } + if (!singleOutsetaEmbed) return; + embedMutation.mutate(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [selectedUid, singleOutsetaEmbed]); const embedMutation = useMutation({ mutationFn: () => upsertEmailListEmbed(config, singleOutsetaEmbed), @@ -56,45 +102,43 @@ export function EmailListIndex() { label: item.Name, })) || []; - if (uid !== undefined && query.isSuccess) { + if (selectedUid !== undefined && query.isSuccess) { // After the query has succeeded - if (!items.find((item) => item.value === uid)) { + if (!items.find((item) => item.value === selectedUid)) { // Check if the uid is valid and if not add an invalid item to the top of the list items.unshift({ - value: uid, - label: `Invalid - ${uid ? uid : "empty"}`, + value: selectedUid, + label: `Invalid - ${selectedUid ? selectedUid : "empty"}`, }); } } return (
{ - if (!singleOutsetaEmbed) return; - embedMutation.mutate(); - }} onSubmit={(event) => { event.preventDefault(); - - switch (embedMode) { - case "embed": - embedMutation.mutate(); - break; - } + form.handleSubmit(); }} > - setUid(value)} - disabled={items.length === 0} - /> + + {(field) => ( + field.handleChange(value)} + onBlur={field.handleBlur} + disabled={items.length === 0} + /> + )} + {embedMode === "embed" && !singleOutsetaEmbed && ( - + )} From a1890ad874fced414b33ce7db4d2d3f25d7322bd Mon Sep 17 00:00:00 2001 From: raae Date: Thu, 30 Oct 2025 15:49:15 +0100 Subject: [PATCH 2/7] Tanstack form for lead capture --- src/routes/lead-capture.index.tsx | 100 +++++++++++++++++++++--------- 1 file changed, 71 insertions(+), 29 deletions(-) diff --git a/src/routes/lead-capture.index.tsx b/src/routes/lead-capture.index.tsx index a2303e8..e98f703 100644 --- a/src/routes/lead-capture.index.tsx +++ b/src/routes/lead-capture.index.tsx @@ -1,7 +1,9 @@ -import { useEffect, useState } from "react"; +import { useEffect, useRef } from "react"; import { createFileRoute } from "@tanstack/react-router"; import { useMutation, useQuery } from "@tanstack/react-query"; import { Button, ListControls } from "@triozer/framer-toolbox"; +import { revalidateLogic, useForm } from "@tanstack/react-form"; +import { z } from "zod"; import { useConfiguration } from "../custom-code"; import { @@ -34,17 +36,59 @@ export function LeadCaptureIndex() { enabled: !!domain && !isInvalid, }); - const [uid, setUid] = useState(undefined); - const leadCaptures = query.data?.items; const defaultUid = leadCaptures?.[0]?.Uid; - const config: LeadCaptureEmbedConfig = { uid: uid || defaultUid || "" }; + const currentUid = singleOutsetaEmbed?.controls.leadCaptureUid as + | string + | undefined; + + const form = useForm({ + defaultValues: { + uid: (currentUid || defaultUid || "") as string, + }, + validationLogic: revalidateLogic({ + mode: "submit", + modeAfterSubmission: "change", + }), + validators: { + onSubmit: z.object({ uid: z.string().trim() }).optional(), + }, + onSubmit: async ({ value }) => { + const cfg: LeadCaptureEmbedConfig = { uid: value.uid }; + switch (embedMode) { + case "embed": + await embedMutation.mutateAsync(); + break; + } + }, + }); + // Keep form value in sync when controls or data load change useEffect(() => { - const uid = singleOutsetaEmbed?.controls.leadCaptureUid as string; - setUid(uid); - }, [singleOutsetaEmbed?.controls.leadCaptureUid]); + const next = (currentUid || defaultUid || "") as string; + if (form.state.values.uid !== next) { + form.setFieldValue("uid", next); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [currentUid, defaultUid]); + + const config: LeadCaptureEmbedConfig = { + uid: (form.useStore((s) => s.values.uid) as string) || "", + }; + + // Auto-apply on uid change when an embed exists + const didInit = useRef(false); + const selectedUid = form.useStore((s) => s.values.uid) as string | undefined; + useEffect(() => { + if (!didInit.current) { + didInit.current = true; + return; + } + if (!singleOutsetaEmbed) return; + embedMutation.mutate(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [selectedUid, singleOutsetaEmbed]); const embedMutation = useMutation({ mutationFn: () => upsertLeadCaptureEmbed(config, singleOutsetaEmbed), @@ -56,45 +100,43 @@ export function LeadCaptureIndex() { label: item.Name, })) || []; - if (uid !== undefined && query.isSuccess) { + if (selectedUid !== undefined && query.isSuccess) { // After the query has succeeded - if (!items.find((item) => item.value === uid)) { + if (!items.find((item) => item.value === selectedUid)) { // Check if the uid is valid and if not add an invalid item to the top of the list items.unshift({ - value: uid, - label: `Invalid - ${uid ? uid : "empty"}`, + value: selectedUid, + label: `Invalid - ${selectedUid ? selectedUid : "empty"}`, }); } } return ( { - if (!singleOutsetaEmbed) return; - embedMutation.mutate(); - }} onSubmit={(event) => { event.preventDefault(); - - switch (embedMode) { - case "embed": - embedMutation.mutate(); - break; - } + form.handleSubmit(); }} > - setUid(value)} - disabled={items.length === 0} - /> + + {(field) => ( + field.handleChange(value)} + onBlur={field.handleBlur} + disabled={items.length === 0} + /> + )} + {embedMode === "embed" && !singleOutsetaEmbed && ( - + )} From 6d5b4855e98c45d368fe7d8d5a4a41a66ad959dd Mon Sep 17 00:00:00 2001 From: raae Date: Thu, 30 Oct 2025 15:54:33 +0100 Subject: [PATCH 3/7] Tanstack form for auth embed --- src/routes/auth.index.tsx | 327 ++++++++++++++++++++++++-------------- 1 file changed, 207 insertions(+), 120 deletions(-) diff --git a/src/routes/auth.index.tsx b/src/routes/auth.index.tsx index 5737397..dd89745 100644 --- a/src/routes/auth.index.tsx +++ b/src/routes/auth.index.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from "react"; +import { useEffect, useRef } from "react"; import { Button, ListControls, @@ -7,6 +7,8 @@ import { } from "@triozer/framer-toolbox"; import { createFileRoute } from "@tanstack/react-router"; import { useMutation, useQuery } from "@tanstack/react-query"; +import { revalidateLogic, useForm } from "@tanstack/react-form"; +import { z } from "zod"; import { getPlanData, @@ -42,22 +44,6 @@ function AuthRegisterPage() { const singleOutsetaEmbed = useSingleOutsetaEmbedSelection(); const embedControls = singleOutsetaEmbed?.controls; - const [preselect, setPreselect] = useState( - embedControls?.registerPreselect as RegisterPreselectOption, - ); - const [familyUid, setFamilyUid] = useState( - embedControls?.registerFamilyUid as string, - ); - const [planUid, setPlanUid] = useState( - embedControls?.registerPlanUid as string, - ); - const [applyDiscountCode, setApplyDiscount] = useState( - !!(embedControls?.registerDiscountCode as string), - ); - const [discountCode, setDiscountCode] = useState( - embedControls?.registerDiscountCode as string, - ); - const planFamilies = query.data?.PlanFamilies; const defaultFamilyUid = query.data?.PlanFamilies?.[0]?.Uid; const defaultPlanUid = query.data?.PlanFamilies?.[0]?.Plans[0]?.Uid; @@ -66,52 +52,130 @@ function AuthRegisterPage() { const { embedMode } = useEmbedMode(); - const config: AuthEmbedConfig = { - widgetMode: "register", - preselect, - familyUid: familyUid || defaultFamilyUid, - planUid: planUid || defaultPlanUid, - discountCode, - }; + const form = useForm({ + defaultValues: { + preselect: (embedControls?.registerPreselect as RegisterPreselectOption) || + ("none" as RegisterPreselectOption), + familyUid: (embedControls?.registerFamilyUid as string) || (defaultFamilyUid as string | undefined) || "", + planUid: (embedControls?.registerPlanUid as string) || (defaultPlanUid as string | undefined) || "", + applyDiscountCode: !!(embedControls?.registerDiscountCode as string), + discountCode: (embedControls?.registerDiscountCode as string) || "", + }, + validationLogic: revalidateLogic({ mode: "submit", modeAfterSubmission: "change" }), + validators: { + onSubmit: z + .object({ + preselect: z.custom(), + familyUid: z.string().optional(), + planUid: z.string().optional(), + applyDiscountCode: z.boolean(), + discountCode: z.string(), + }) + .refine((v) => v.preselect !== "family" || !!(v.familyUid && v.familyUid.trim()), { + message: "Family is required", + path: ["familyUid"], + }) + .refine((v) => v.preselect !== "plan" || !!(v.familyUid && v.planUid && v.planUid.trim()), { + message: "Plan is required", + path: ["planUid"], + }) + .refine((v) => !v.applyDiscountCode || !!v.discountCode.trim(), { + message: "Discount code required", + path: ["discountCode"], + }) + .optional(), + }, + onSubmit: async () => { + switch (embedMode) { + case "embed": + await embedMutation.mutateAsync(); + break; + } + }, + }); + // Keep form values in sync when embed controls or defaults change useEffect(() => { - const preselect = - embedControls?.registerPreselect as RegisterPreselectOption; - const familyUid = embedControls?.registerFamilyUid as string; - const planUid = embedControls?.registerPlanUid as string; - const code = embedControls?.registerDiscountCode as string; - - setPreselect(preselect || "none"); - setFamilyUid(familyUid); - setPlanUid(planUid); - setDiscountCode(code); - if (code && !applyDiscountCode) { - setApplyDiscount(true); + const nextPreselect = + (embedControls?.registerPreselect as RegisterPreselectOption) || + ("none" as RegisterPreselectOption); + const nextFamily = + (embedControls?.registerFamilyUid as string) || defaultFamilyUid || ""; + const nextPlan = + (embedControls?.registerPlanUid as string) || defaultPlanUid || ""; + const nextCode = (embedControls?.registerDiscountCode as string) || ""; + const nextApply = !!nextCode; + + if (form.state.values.preselect !== nextPreselect) { + form.setFieldValue("preselect", nextPreselect); + } + if (form.state.values.familyUid !== nextFamily) { + form.setFieldValue("familyUid", nextFamily); + } + if (form.state.values.planUid !== nextPlan) { + form.setFieldValue("planUid", nextPlan); + } + if (form.state.values.discountCode !== nextCode) { + form.setFieldValue("discountCode", nextCode); } + if (form.state.values.applyDiscountCode !== nextApply) { + form.setFieldValue("applyDiscountCode", nextApply); + } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [ embedControls?.registerPreselect, embedControls?.registerFamilyUid, embedControls?.registerPlanUid, embedControls?.registerDiscountCode, + defaultFamilyUid, + defaultPlanUid, ]); + const formValues = form.useStore((s) => s.values); + const computedConfig: AuthEmbedConfig = { + widgetMode: "register", + preselect: formValues.preselect as RegisterPreselectOption, + familyUid: (formValues.familyUid as string) || (defaultFamilyUid as string | undefined), + planUid: (formValues.planUid as string) || (defaultPlanUid as string | undefined), + discountCode: formValues.applyDiscountCode ? (formValues.discountCode as string) : "", + }; + const embedMutation = useMutation({ - mutationFn: () => upsertAuthEmbed(config, singleOutsetaEmbed), + mutationFn: () => upsertAuthEmbed(computedConfig, singleOutsetaEmbed), }); + // Auto-apply on any relevant value change when an embed exists + const didInit = useRef(false); + const autoApplyValues = form.useStore((s) => [ + s.values.preselect, + s.values.familyUid, + s.values.planUid, + s.values.applyDiscountCode, + s.values.discountCode, + ]); + useEffect(() => { + if (!didInit.current) { + didInit.current = true; + return; + } + if (!singleOutsetaEmbed) return; + embedMutation.mutate(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [singleOutsetaEmbed, ...autoApplyValues]); + const planFamilyItems = planFamilies?.map((family) => ({ value: family.Uid, label: family.Name, })) || []; - if (familyUid !== undefined && query.isSuccess) { + if (formValues.familyUid !== undefined && query.isSuccess) { // After the query has succeeded - if (!planFamilyItems.find((item) => item.value === familyUid)) { + if (!planFamilyItems.find((item) => item.value === formValues.familyUid)) { // Check if the uid is valid and if not add an invalid item to the top of the list planFamilyItems.unshift({ - value: familyUid, - label: `Invalid - ${familyUid ? familyUid : "empty"}`, + value: formValues.familyUid as string, + label: `Invalid - ${formValues.familyUid ? (formValues.familyUid as string) : "empty"}`, }); } } @@ -119,8 +183,8 @@ function AuthRegisterPage() { const planItems = planFamilies ?.find((family) => { - if (familyUid) { - return family.Uid === familyUid; + if (formValues.familyUid) { + return family.Uid === formValues.familyUid; } else { return family.Uid === defaultFamilyUid; } @@ -130,109 +194,132 @@ function AuthRegisterPage() { label: plan.Name, })) || []; - if (planUid !== undefined && query.isSuccess) { + if (formValues.planUid !== undefined && query.isSuccess) { // After the query has succeeded - if (!planItems.find((item) => item.value === planUid)) { + if (!planItems.find((item) => item.value === formValues.planUid)) { // Check if the uid is valid and if not add an invalid item to the top of the list planItems.unshift({ - value: planUid, - label: `Invalid - ${planUid ? planUid : "empty"}`, + value: formValues.planUid as string, + label: `Invalid - ${formValues.planUid ? (formValues.planUid as string) : "empty"}`, }); } } return ( { - if (!singleOutsetaEmbed) return; - embedMutation.mutate(); - }} onSubmit={(event) => { event.preventDefault(); - - switch (embedMode) { - case "embed": - embedMutation.mutate(); - break; - } + form.handleSubmit(); }} > - ({ - value: option, - label: RegisterPreselectOptionLabels[option], - }))} - value={preselect} - onChange={(value) => setPreselect(value)} - /> + + {(field) => ( + ({ + value: option, + label: RegisterPreselectOptionLabels[option], + }))} + value={field.state.value as RegisterPreselectOption} + onChange={(value) => field.handleChange(value)} + onBlur={field.handleBlur} + /> + )} + {/* Display plan family selection when preselect is set to "family" or "plan" */} - {preselect !== "none" && ( - setFamilyUid(value)} - disabled={query.isPending} - /> - )} + s.values.preselect}> + {(preselect) => ( + <> + {preselect !== "none" && ( + + {(field) => ( + field.handleChange(value)} + onBlur={field.handleBlur} + disabled={query.isPending} + /> + )} + + )} + + )} + {/* Display plan selection when preselect is set to "plan" */} - {preselect === "plan" && ( - setPlanUid(value)} - disabled={query.isPending} - /> - )} + s.values.preselect}> + {(preselect) => ( + <> + {preselect === "plan" && ( + + {(field) => ( + field.handleChange(value)} + onBlur={field.handleBlur} + disabled={query.isPending} + /> + )} + + )} + + )} + - { - setApplyDiscount(value); - if (!value && singleOutsetaEmbed) { - setDiscountCode(""); - // HACK: This is a workaround to update the discount - // code in the embed when disables the discount code - upsertAuthEmbed( - { widgetMode: "register", discountCode: "" }, - singleOutsetaEmbed, - ); - } - }} - /> - - {applyDiscountCode && ( - <> - setDiscountCode(value)} + + {(field) => ( + field.handleChange(value)} + onBlur={field.handleBlur} /> - {!discountCodeExists && ( -

- Remember to add{" "} - - the discount code in Outseta - - . -

- )} - - )} + )} +
+ + s.values.applyDiscountCode}> + {(applyDiscountCode) => ( + <> + {applyDiscountCode && ( + + {(field) => ( + field.handleChange(value)} + onBlur={field.handleBlur} + /> + )} + + )} + {applyDiscountCode && !discountCodeExists && ( +

+ Remember to add{" "} + + the discount code in Outseta + + . +

+ )} + + )} +
{embedMode === "embed" && !singleOutsetaEmbed && ( - + )} {embedMode === "popup" && ( - + )}
From d5c341915cb02ac1d5e072d8052c04bd93376695 Mon Sep 17 00:00:00 2001 From: raae Date: Thu, 30 Oct 2025 16:43:47 +0100 Subject: [PATCH 4/7] feat(form): migrate auth.login to TanStack Form --- src/routes/auth.login.tsx | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/src/routes/auth.login.tsx b/src/routes/auth.login.tsx index 5abcead..1be4c23 100644 --- a/src/routes/auth.login.tsx +++ b/src/routes/auth.login.tsx @@ -1,6 +1,7 @@ import { createFileRoute } from "@tanstack/react-router"; import { useMutation } from "@tanstack/react-query"; import { Button } from "@triozer/framer-toolbox"; +import { revalidateLogic, useForm } from "@tanstack/react-form"; import { upsertAuthEmbed, @@ -41,25 +42,34 @@ export function AuthLoginPage() { }, }); + const form = useForm({ + defaultValues: {}, + validationLogic: revalidateLogic({ mode: "submit", modeAfterSubmission: "change" }), + onSubmit: async () => { + switch (embedMode) { + case "embed": + await embedMutation.mutateAsync(); + break; + case "popup": + await popupMutation.mutateAsync(); + break; + } + }, + }); + return ( { event.preventDefault(); - - switch (embedMode) { - case "embed": - embedMutation.mutate(); - break; - case "popup": - popupMutation.mutate(); - break; - } + form.handleSubmit(); }} > {embedMode === "embed" && !singleOutsetaEmbed && ( - + )} {embedMode === "popup" && ( From 3d39fa2f2d99b39cf9c0377a625a659c084aae1e Mon Sep 17 00:00:00 2001 From: raae Date: Thu, 30 Oct 2025 17:15:17 +0100 Subject: [PATCH 5/7] feat(form): migrate support to TanStack Form --- src/routes/support.index.tsx | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/src/routes/support.index.tsx b/src/routes/support.index.tsx index 6d4b253..40ccc85 100644 --- a/src/routes/support.index.tsx +++ b/src/routes/support.index.tsx @@ -2,6 +2,7 @@ import { createFileRoute } from "@tanstack/react-router"; import { addSupportEmbed, supportPopupUrl } from "../outseta"; import { useMutation } from "@tanstack/react-query"; import { Button } from "@triozer/framer-toolbox"; +import { revalidateLogic, useForm } from "@tanstack/react-form"; import { useEmbedMode, useSingleOutsetaEmbedSelection } from "../app-state"; import { EmbedModeListControls, PopupLinkFormSection } from "../common"; import { useConfiguration } from "../custom-code"; @@ -19,22 +20,31 @@ function Support() { mutationFn: () => addSupportEmbed(), }); + const form = useForm({ + defaultValues: {}, + validationLogic: revalidateLogic({ mode: "submit", modeAfterSubmission: "change" }), + onSubmit: async () => { + switch (embedMode) { + case "embed": + await embedMutation.mutateAsync(); + break; + } + }, + }); + return ( { event.preventDefault(); - - switch (embedMode) { - case "embed": - embedMutation.mutate(); - break; - } + form.handleSubmit(); }} > {embedMode === "embed" && !singleOutsetaEmbedSelection && ( - + )} From 99c3ea870497953861036a64debf1cdeefd9326d Mon Sep 17 00:00:00 2001 From: raae Date: Thu, 30 Oct 2025 17:16:44 +0100 Subject: [PATCH 6/7] feat(form): migrate user profile to TanStack Form --- src/routes/user.index.tsx | 94 +++++++++++++++++++++++++++------------ 1 file changed, 65 insertions(+), 29 deletions(-) diff --git a/src/routes/user.index.tsx b/src/routes/user.index.tsx index 6999865..e7b2429 100644 --- a/src/routes/user.index.tsx +++ b/src/routes/user.index.tsx @@ -1,7 +1,8 @@ -import { useEffect, useState } from "react"; +import { useEffect, useRef } from "react"; import { createFileRoute } from "@tanstack/react-router"; import { Button, ListControls } from "@triozer/framer-toolbox"; import { useMutation } from "@tanstack/react-query"; +import { revalidateLogic, useForm } from "@tanstack/react-form"; import { upsertProfileEmbed, @@ -29,13 +30,38 @@ function User() { const singleSupportsLink = useSingleSupportsLinkSelection(); const singleOutsetaEmbed = useSingleOutsetaEmbedSelection(); - const [tab, setTab] = useState("profile"); + const form = useForm({ + defaultValues: { + tab: + ((singleOutsetaEmbed?.controls.profileDefaultTab as ProfileTabOption) || + "profile") as ProfileTabOption, + }, + validationLogic: revalidateLogic({ mode: "submit", modeAfterSubmission: "change" }), + onSubmit: async () => { + switch (embedMode) { + case "embed": + await embedMutation.mutateAsync(); + break; + case "popup": + await popupMutation.mutateAsync(); + break; + } + }, + }); - const config: ProfileEmbedConfig = { tab }; + const config: ProfileEmbedConfig = { + tab: (form.useStore((s) => s.values.tab) as ProfileTabOption) || "profile", + }; + // Sync form value when embed control changes useEffect(() => { - const tab = singleOutsetaEmbed?.controls.profileDefaultTab; - setTab((tab as ProfileTabOption) || "profile"); + const next = + ((singleOutsetaEmbed?.controls.profileDefaultTab as ProfileTabOption) || + "profile") as ProfileTabOption; + if (form.state.values.tab !== next) { + form.setFieldValue("tab", next); + } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [singleOutsetaEmbed?.controls.profileDefaultTab]); const embedMutation = useMutation({ @@ -52,43 +78,53 @@ function User() { }, }); + // Auto-apply on tab change when an embed exists + const didInit = useRef(false); + const selectedTab = form.useStore((s) => s.values.tab) as ProfileTabOption; + useEffect(() => { + if (!didInit.current) { + didInit.current = true; + return; + } + if (!singleOutsetaEmbed) return; + embedMutation.mutate(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [selectedTab, singleOutsetaEmbed]); + return ( { - if (!singleOutsetaEmbed) return; - embedMutation.mutate(); - }} onSubmit={(event) => { event.preventDefault(); - - switch (embedMode) { - case "embed": - embedMutation.mutate(); - break; - case "popup": - popupMutation.mutate(); - break; - } + form.handleSubmit(); }} > - ({ - value: option, - label: ProfileTabOptionLabels[option], - }))} - value={tab} - onChange={(value) => setTab(value)} - /> + + {(field) => ( + ({ + value: option, + label: ProfileTabOptionLabels[option], + }))} + value={field.state.value as ProfileTabOption} + onChange={(value) => field.handleChange(value)} + onBlur={field.handleBlur} + /> + )} + {embedMode === "embed" && !singleOutsetaEmbed && ( - + )} {embedMode === "popup" && singleSupportsLink && ( - + )} {embedMode === "popup" && !singleSupportsLink && ( From 50ac17b0bea56deeb2f275e4ed95032b046d0a22 Mon Sep 17 00:00:00 2001 From: raae Date: Thu, 6 Nov 2025 10:51:12 +0100 Subject: [PATCH 7/7] feat(form): migrate auth.logout to TanStack Form --- src/routes/auth.logout.tsx | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/src/routes/auth.logout.tsx b/src/routes/auth.logout.tsx index 2d16a2d..fa774ff 100644 --- a/src/routes/auth.logout.tsx +++ b/src/routes/auth.logout.tsx @@ -1,22 +1,34 @@ import { createFileRoute } from "@tanstack/react-router"; +import { revalidateLogic, useForm } from "@tanstack/react-form"; import { CopyButton, PageListControls } from "../common"; -import { useState } from "react"; export const Route = createFileRoute("/auth/logout")({ component: AuthLogoutPage, }); export function AuthLogoutPage() { - const [postLogoutPath, setPostLogoutPath] = useState("/"); + const form = useForm({ + defaultValues: { + postLogoutPath: "/", + }, + validationLogic: revalidateLogic({ mode: "submit", modeAfterSubmission: "change" }), + }); + + const postLogoutPath = form.useStore((s) => s.values.postLogoutPath) as string; return ( - setPostLogoutPath(value)} - /> + + {(field) => ( + field.handleChange(value)} + onBlur={field.handleBlur} + /> + )} +