import React, { useEffect, useMemo, useRef, useState, useCallback } from "react"; import { createRoot } from "react-dom/client"; import { QueryClient, QueryClientProvider, useQueryClient, } from "@tanstack/react-query"; import { html, VIEWS } from "./constants.js"; import { currentDirectionsFor } from "./api.js"; import { directionsAfterColumnDrop } from "./direction-logic.js"; import { StatusText, RoleChip } from "./components.js"; import { useUserQuery, useGroupsQuery, useRolesQuery, useRoleSearchQuery, } from "./queries.js"; import { useLoginMutation, useCreateMutation, useSavePlayMutation, useDeletePlayMutation, useSaveRoleMutation, usePrefMutation, useScenarioPreferenceMutation, useLinkMutation, useCreateGroupMutation, useRemoveGroupMemberMutation, useShareToggleMutation, usePartnerRequestMutations, } from "./mutations.js"; import { optimisticBoard, removeKinkFromItemsPayload, playCommitKey } from "./board-utils.js"; import { DiscoverView } from "./discover.js"; import { normalizeDiscoverSheetDirections } from "./discover-flow.js"; import { ScenariosView } from "./scenarios.js"; import { MyPlaysView } from "./my-plays.js"; import { TogetherView } from "./together.js"; import { SettingsView } from "./settings.js"; import { initTelemetry } from "./telemetry.js"; import { preferredPartnerGroupId, resolvedPartnerGroups } from "./partner-groups.js"; import { nextStarterCompleted, starterViewState } from "./starter-flow.js"; const queryClient = new QueryClient(); const EMPTY_BOARD = { to_me: [], by_me: [], together: [], hidden: [], no_go: [] }; const STARTER_TARGET = 12; const PENDING_PARTNER_KEY = "kink_pending_partner"; function loadStoredAuth() { try { const raw = localStorage.getItem("kink_auth"); if (raw) { const payload = JSON.parse(raw); if (payload.userId && payload.token) return payload; } } catch {} return { userId: "", token: "" }; } function loadPendingPartnerIntent() { try { const url = new URL(window.location.href); const fromLink = (url.searchParams.get("link") || "").trim(); if (fromLink) return fromLink; return (localStorage.getItem(PENDING_PARTNER_KEY) || "").trim(); } catch { return ""; } } function persistPendingPartnerIntent(partnerId) { try { if (partnerId) localStorage.setItem(PENDING_PARTNER_KEY, partnerId); else localStorage.removeItem(PENDING_PARTNER_KEY); } catch {} try { const url = new URL(window.location.href); if (partnerId) url.searchParams.set("link", partnerId); else url.searchParams.delete("link"); const next = `${url.pathname}${url.search}${url.hash}`; window.history.replaceState({}, "", next || "/"); } catch {} } const GRAPH_DEBUG_UI_KEY = "kink_graph_debug_ui"; function readGraphDebugUi() { try { return localStorage.getItem(GRAPH_DEBUG_UI_KEY) === "1"; } catch { return false; } } function writeGraphDebugUi(value) { try { localStorage.setItem(GRAPH_DEBUG_UI_KEY, value ? "1" : "0"); } catch {} } function GraphDebugBar({ enabled, onToggle }) { return html`
`; } function AppInner() { const qc = useQueryClient(); const [auth, setAuthRaw] = useState(loadStoredAuth); const [authStep, setAuthStep] = useState(auth.userId ? null : "welcome"); const [pendingPartnerId, setPendingPartnerIdRaw] = useState(loadPendingPartnerIntent); const [status, setStatus] = useState(""); const [view, setView] = useState(VIEWS.DISCOVER); const [loginForm, setLoginForm] = useState({ user_id: "", private_token: "" }); const [createResult, setCreateResult] = useState(null); const [roleSearchInput, setRoleSearchInput] = useState(""); const [roleSearchDebounced, setRoleSearchDebounced] = useState(""); const [roleDebounceTimer, setRoleDebounceTimer] = useState(null); const [activeGroupId, setActiveGroupId] = useState(""); // True when the user has explicitly clicked "Just me" — discover should run solo even if the // user has partner groups. Without this flag the auto-pick effect below would silently snap // activeGroupId back to a preferred group on every render. const [partnerContextOptOut, setPartnerContextOptOut] = useState(false); const [playsFocusKinkId, setPlaysFocusKinkId] = useState(""); const [scenarioFocus, setScenarioFocus] = useState({ parentKinkId: "", scenarioKinkId: "" }); const [discoverPositiveDirections, setDiscoverPositiveDirections] = useState(["together"]); const [pendingPlaySaveCount, setPendingPlaySaveCount] = useState(0); const [starterCompleted, setStarterCompleted] = useState(false); const [graphDebugUi, setGraphDebugUi] = useState(readGraphDebugUi); const pendingPlayCommitRef = useRef(""); const autoLinkAttemptRef = useRef(""); const toggleGraphDebugUi = useCallback((next) => { setGraphDebugUi(next); writeGraphDebugUi(next); }, []); const setAuth = useCallback((value) => { setAuthRaw(value); if (value.userId && value.token) localStorage.setItem("kink_auth", JSON.stringify(value)); else localStorage.removeItem("kink_auth"); }, []); const setPendingPartnerIntent = useCallback((partnerId) => { const next = String(partnerId || "").trim(); setPendingPartnerIdRaw(next); persistPendingPartnerIntent(next); }, []); const logout = useCallback(() => { setAuth({ userId: "", token: "" }); qc.clear(); setView(VIEWS.DISCOVER); setAuthStep("welcome"); setActiveGroupId(""); setPlaysFocusKinkId(""); setScenarioFocus({ parentKinkId: "", scenarioKinkId: "" }); setDiscoverPositiveDirections(["together"]); setCreateResult(null); setPendingPlaySaveCount(0); setStarterCompleted(false); setStatus(""); }, [qc, setAuth]); const userQuery = useUserQuery(auth, { pauseUserRefetch: pendingPlaySaveCount > 0 }); const groupsQuery = useGroupsQuery(auth); const rolesQuery = useRolesQuery(auth); const roleSearchQuery = useRoleSearchQuery(roleSearchDebounced); const user = userQuery.data || null; const currentPlays = user?.plays || {}; const scenarioPreferences = user?.scenario_preferences || {}; const showImages = user?.preferences?.show_images ?? true; const savedCount = Object.keys(currentPlays).length; const selectedRoles = rolesQuery.data?.items || []; const selectedRoleIds = new Set(selectedRoles.map((role) => role.id)); const visibleRoleSearchItems = (roleSearchQuery.data?.items || []).filter((item) => !selectedRoleIds.has(item.kink.id)); const partnerGroups = resolvedPartnerGroups(user, groupsQuery.data); const partnerGroupsLoaded = !auth.userId || !auth.token || groupsQuery.isSuccess || groupsQuery.isError; const groupSelectionReady = Boolean(auth.userId && auth.token) && partnerGroupsLoaded && (!partnerGroups.length || Boolean(activeGroupId) || partnerContextOptOut); const starterState = starterViewState({ hasAuth: Boolean(auth.userId && auth.token), hasCreateResult: Boolean(createResult), userLoaded: userQuery.isSuccess, savedCount, starterTarget: STARTER_TARGET, pendingSaveCount: pendingPlaySaveCount, starterCompleted, }); const needsStarterFlow = starterState === "starter" || starterState === "finalizing"; /** Starter Discover always queries recs with group id ""; keep save/recs cache in sync if partner groups load mid-flow. */ const recsCacheGroupId = needsStarterFlow ? "" : activeGroupId; useEffect(() => { if (createResult) return; if (!auth.userId || !auth.token) return; if (!userQuery.isError || userQuery.isFetching) return; const statusCode = userQuery.error?.status; if (statusCode === 401 || statusCode === 404) { setStatus("That profile is gone. Create a new one or log in again."); logout(); } }, [createResult, auth.userId, auth.token, userQuery.isError, userQuery.isFetching, userQuery.error, logout]); useEffect(() => { if (createResult) return; if (!auth.userId || !auth.token) return; const bad = (q) => q.isError && !q.isFetching && (q.error?.status === 401 || q.error?.status === 404); if (bad(groupsQuery) || bad(rolesQuery)) { setStatus("That profile is gone. Create a new one or log in again."); logout(); } }, [ createResult, auth.userId, auth.token, groupsQuery.isError, groupsQuery.isFetching, groupsQuery.error, rolesQuery.isError, rolesQuery.isFetching, rolesQuery.error, logout, ]); useEffect(() => { if (!auth.userId || !auth.token) { setPendingPlaySaveCount(0); setStarterCompleted(false); return; } if (createResult || !userQuery.isSuccess) return; setStarterCompleted((current) => nextStarterCompleted({ starterCompleted: current, savedCount, starterTarget: STARTER_TARGET, pendingSaveCount: pendingPlaySaveCount, })); }, [auth.userId, auth.token, createResult, userQuery.isSuccess, savedCount, pendingPlaySaveCount]); useEffect(() => { if (!partnerGroups.length) { if (activeGroupId) setActiveGroupId(""); if (partnerContextOptOut) setPartnerContextOptOut(false); return; } // Respect an explicit "Just me" choice — only auto-pick a group when the user has not // opted out of partner context. Otherwise re-typing "Just me" each render would defeat // the toggle. if (partnerContextOptOut) return; const preferred = preferredPartnerGroupId(partnerGroups, activeGroupId); if (preferred !== activeGroupId) setActiveGroupId(preferred); }, [activeGroupId, partnerGroups, partnerContextOptOut]); function replacePartnerGroup(nextGroup) { qc.setQueryData(["partner-groups", auth.userId], (old) => ( old?.items ? { ...old, items: old.items.map((group) => (group.id === nextGroup.id ? { ...group, ...nextGroup } : group)) } : old )); qc.setQueryData(["user", auth.userId], (old) => ( old?.partner_groups ? { ...old, partner_groups: old.partner_groups.map((group) => (group.id === nextGroup.id ? { ...group, ...nextGroup } : group)) } : old )); } function removeMemberFromGroupCache(groupId, memberId) { const patch = (group) => group.id !== groupId ? group : { ...group, member_ids: (group.member_ids || []).filter((id) => id !== memberId), participant_ids: (group.participant_ids || []).filter((id) => id !== memberId), sharing_member_ids: (group.sharing_member_ids || []).filter((id) => id !== memberId), }; qc.setQueryData(["partner-groups", auth.userId], (old) => (old?.items ? { ...old, items: old.items.map(patch) } : old)); qc.setQueryData(["user", auth.userId], (old) => (old?.partner_groups ? { ...old, partner_groups: old.partner_groups.map(patch) } : old)); } function syncPostSaveCaches(userPayload, kinkId, interestState, directions) { qc.setQueryData(["user", auth.userId], userPayload); qc.setQueryData(["board", auth.userId], (old) => optimisticBoard(old || EMPTY_BOARD, kinkId, interestState, directions, qc, auth)); qc.setQueryData(["recs", auth.userId, recsCacheGroupId], (old) => removeKinkFromItemsPayload(old, kinkId)); qc.invalidateQueries({ queryKey: ["scenario-parents", auth.userId] }); if (activeGroupId) qc.invalidateQueries({ queryKey: ["group-overlap", auth.userId, activeGroupId] }); } const loginMutation = useLoginMutation({ setAuth, setCreateResult, setAuthOpen: () => {}, setStatus }); const createMutation = useCreateMutation({ setAuth, setCreateResult, setLoginForm, setStatus }); const savePlayMutation = useSavePlayMutation({ auth, activeGroupId: recsCacheGroupId, selectedKinkId: "", setSelectedKinkId: () => {}, setStatus, pendingPlayCommitRef, playCommitKey, syncPostSaveCaches, onSaveStart: () => setPendingPlaySaveCount((count) => count + 1), onSaveSettled: () => setPendingPlaySaveCount((count) => Math.max(0, count - 1)), }); const deletePlayMutation = useDeletePlayMutation({ auth, setStatus }); const handleDeletePlay = useCallback((kinkId) => { if (!kinkId) return; deletePlayMutation.mutate(kinkId); }, [deletePlayMutation]); const saveRoleMutation = useSaveRoleMutation({ auth, setStatus }); const prefMutation = usePrefMutation({ auth, setStatus }); const scenarioPreferenceMutation = useScenarioPreferenceMutation({ auth, activeGroupId, setStatus }); const linkMutation = useLinkMutation({ auth, setGroupDraftMembers: () => {}, setGroupNameInput: () => {}, setStatus }); const partnerRequestMutations = usePartnerRequestMutations({ auth, setStatus }); const createGroupMutation = useCreateGroupMutation({ auth, setActiveGroupId, setGroupNameInput: () => {}, setGroupDraftMembers: () => {}, setStatus }); const removeGroupMemberMutation = useRemoveGroupMemberMutation({ auth, removeMemberFromGroupCache, setStatus }); const shareToggleMutation = useShareToggleMutation({ auth, replacePartnerGroup, setStatus }); useEffect(() => { if (!pendingPartnerId || !auth.userId || !auth.token || !user) return; if (pendingPartnerId === auth.userId) { setPendingPartnerIntent(""); setStatus("You cannot connect a profile to itself."); return; } if ((user.partners || []).includes(pendingPartnerId)) { setPendingPartnerIntent(""); setStatus("That partner is already connected."); return; } if ((user.outgoing_partner_requests || []).includes(pendingPartnerId)) { setPendingPartnerIntent(""); setStatus("Connection request already sent."); return; } if ((user.incoming_partner_requests || []).includes(pendingPartnerId)) { setPendingPartnerIntent(""); setStatus("They already requested a connection. Open Together to accept."); return; } if (autoLinkAttemptRef.current === pendingPartnerId || linkMutation.isPending) return; autoLinkAttemptRef.current = pendingPartnerId; linkMutation.mutate(pendingPartnerId, { onSettled: () => { autoLinkAttemptRef.current = ""; setPendingPartnerIntent(""); }, }); }, [pendingPartnerId, auth.userId, auth.token, user, setPendingPartnerIntent, linkMutation]); function normalizeDirectionsForSave(interestState, directions = []) { const cleaned = [...new Set((directions || []).filter(Boolean))].sort(); if (cleaned.length) return cleaned; return ["love", "like", "curious"].includes(interestState) ? [...new Set(normalizeDiscoverSheetDirections(discoverPositiveDirections).filter(Boolean))].sort() : []; } const handleDiscoverDirectionsChange = useCallback((update) => { setDiscoverPositiveDirections((prev) => { const prevNorm = normalizeDiscoverSheetDirections(prev); const next = typeof update === "function" ? update(prevNorm) : update; return normalizeDiscoverSheetDirections(next); }); }, []); const handleRate = useCallback((kinkId, interestState, directions = null) => { const kid = String(kinkId); const prevState = currentPlays[kid] || null; const nextDirections = normalizeDirectionsForSave(interestState, directions ?? currentDirectionsFor(currentPlays, kid)); const normalized = [...nextDirections].sort(); const existing = [...(prevState?.directions || [])].sort(); if ( !needsStarterFlow && prevState?.interest_state === interestState && JSON.stringify(existing) === JSON.stringify(normalized) ) { return Promise.resolve(null); } const commitKey = playCommitKey(kid, interestState, normalized); if (!needsStarterFlow && pendingPlayCommitRef.current === commitKey) return Promise.resolve(null); pendingPlayCommitRef.current = commitKey; return savePlayMutation.mutateAsync({ kinkId: kid, interestState, directions: normalized }); }, [currentPlays, discoverPositiveDirections, needsStarterFlow, savePlayMutation]); const handleDirectionToggle = useCallback((kinkId, direction) => { const play = currentPlays[kinkId]; if (!play?.interest_state) { setStatus("Rate this play first."); return; } const current = play.directions || []; let next; if (current.includes(direction)) next = current.filter((value) => value !== direction); else if (current.length === 1 && current[0] === "together" && (direction === "to_me" || direction === "by_me")) next = [direction]; else next = [...current, direction]; if (!next.length) { setStatus("Choose at least one direction."); return; } const interestState = play.interest_state === "not_interested" ? "curious" : play.interest_state; handleRate(kinkId, interestState, next); }, [currentPlays, handleRate]); const handleDirectionColumnChange = useCallback((kinkId, column) => { const play = currentPlays[kinkId]; if (!play?.interest_state) { setStatus("Rate this play first."); return; } if (!["to_me", "by_me", "together"].includes(column)) return; const next = directionsAfterColumnDrop(play.directions, column); const prevSorted = [...new Set((play.directions || []).filter(Boolean))].sort(); const nextSorted = [...next].sort(); const dirsUnchanged = JSON.stringify(prevSorted) === JSON.stringify(nextSorted); if (dirsUnchanged && play.interest_state !== "not_interested") return; const interestState = play.interest_state === "not_interested" ? "curious" : play.interest_state; handleRate(kinkId, interestState, next); }, [currentPlays, handleRate]); const handleScenarioRate = useCallback((parentKinkId, scenarioKinkId, interestState, directions = null) => { const existing = scenarioPreferences[scenarioKinkId] || null; const parentDirections = currentPlays[parentKinkId]?.directions || []; const nextDirections = normalizeDirectionsForSave( interestState, directions ?? existing?.directions ?? parentDirections, ); return scenarioPreferenceMutation.mutateAsync({ parentKinkId, scenarioKinkId, interestState, directions: nextDirections, }); }, [scenarioPreferences, currentPlays, scenarioPreferenceMutation]); const handleOpenScenarios = useCallback((parentKinkId, scenarioKinkId = "") => { setScenarioFocus({ parentKinkId: parentKinkId || "", scenarioKinkId: scenarioKinkId || "" }); setView(VIEWS.SCENARIOS); }, []); const handleNavigate = useCallback((targetView, kinkId) => { setView(targetView); if (kinkId) setPlaysFocusKinkId(kinkId); }, []); const handleRoleSearch = useCallback((value) => { setRoleSearchInput(value); if (roleDebounceTimer) clearTimeout(roleDebounceTimer); setRoleDebounceTimer(setTimeout(() => setRoleSearchDebounced(value.trim()), 300)); }, [roleDebounceTimer]); if (!auth.userId || !auth.token || createResult) { return html` <${Onboarding} step=${authStep || "welcome"} setStep=${setAuthStep} loginForm=${loginForm} setLoginForm=${setLoginForm} createResult=${createResult} status=${status} loginMutation=${loginMutation} createMutation=${createMutation} saveRoleMutation=${saveRoleMutation} roleSearchInput=${roleSearchInput} onRoleSearch=${handleRoleSearch} roleSearchItems=${visibleRoleSearchItems} selectedRoles=${selectedRoles} pendingPartnerId=${pendingPartnerId} onFinish=${() => { const uid = auth.userId; setCreateResult(null); setAuthStep(null); if (uid) { qc.invalidateQueries({ queryKey: ["user", uid] }); qc.invalidateQueries({ queryKey: ["partner-groups", uid] }); qc.invalidateQueries({ queryKey: ["roles", uid] }); } }} /> `; } if (Boolean(auth.userId && auth.token) && userQuery.isLoading) { return html`
<${GraphDebugBar} enabled=${graphDebugUi} onToggle=${toggleGraphDebugUi} />
Loading your profile…
<${StatusText} text=${status} />
`; } if (needsStarterFlow) { return html`
<${GraphDebugBar} enabled=${graphDebugUi} onToggle=${toggleGraphDebugUi} />
Start broad

${starterState === "finalizing" ? "Finishing your starter picks. Full discovery unlocks as soon as those saves land." : `Rate ${STARTER_TARGET} common starter cards first. After that, discovery opens up and partner influence can join in.`}

${pendingPartnerId ? html`

Connection invite saved for ${pendingPartnerId}. It will be sent automatically once your profile is ready.

` : null}
<${DiscoverView} auth=${auth} activeGroupId="" groupSelectionReady=${true} partnerGroups=${[]} onActiveGroupChange=${() => {}} photosOnCards=${showImages} onPhotoPreference=${(value) => prefMutation.mutate(value)} onRate=${handleRate} onNavigate=${handleNavigate} onOpenScenarios=${handleOpenScenarios} savedCount=${savedCount} currentPlays=${currentPlays} discoverSheetDirections=${discoverPositiveDirections} onDiscoverSheetDirectionsChange=${handleDiscoverDirectionsChange} starterMode=${true} starterFinalizing=${starterState === "finalizing"} starterTarget=${STARTER_TARGET} showSearch=${false} headerTitle="Starter deck" headerSubtitle="Rate a dozen broad starters before full discovery unlocks." /> <${StatusText} text=${status} />
`; } const incomingRequests = (user?.incoming_partner_requests || []).length; const tabs = [ { key: VIEWS.DISCOVER, label: "Discover", icon: "\uD83D\uDD25" }, { key: VIEWS.SCENARIOS, label: "Scenarios", icon: "\uD83C\uDFAC" }, { key: VIEWS.PLAYS, label: "My Plays", icon: "\uD83C\uDFB5", badge: savedCount || null }, { key: VIEWS.TOGETHER, label: "Together", icon: "\uD83E\uDD1D", attention: incomingRequests > 0, badge: incomingRequests || null }, { key: VIEWS.SETTINGS, label: "Settings", icon: "\u2699\uFE0F" }, ]; return html`
<${GraphDebugBar} enabled=${graphDebugUi} onToggle=${toggleGraphDebugUi} /> ${view === VIEWS.DISCOVER ? html` <${DiscoverView} auth=${auth} activeGroupId=${activeGroupId} groupSelectionReady=${groupSelectionReady} partnerGroups=${partnerGroups} partnerContextOptOut=${partnerContextOptOut} onActiveGroupChange=${(nextId) => { if (nextId === "") { // Explicit "Just me" — opt out so the auto-pick effect won't override. setPartnerContextOptOut(true); setActiveGroupId(""); } else { setPartnerContextOptOut(false); setActiveGroupId(nextId); } }} photosOnCards=${showImages} onPhotoPreference=${(value) => prefMutation.mutate(value)} onRate=${handleRate} onNavigate=${handleNavigate} onOpenScenarios=${handleOpenScenarios} savedCount=${savedCount} currentPlays=${currentPlays} discoverSheetDirections=${discoverPositiveDirections} onDiscoverSheetDirectionsChange=${handleDiscoverDirectionsChange} /> ` : null} ${view === VIEWS.SCENARIOS ? html` <${ScenariosView} auth=${auth} currentPlays=${currentPlays} scenarioPreferences=${scenarioPreferences} onScenarioRate=${handleScenarioRate} onNavigateToPlays=${(kinkId) => handleNavigate(VIEWS.PLAYS, kinkId)} focusParentKinkId=${scenarioFocus.parentKinkId} focusScenarioKinkId=${scenarioFocus.scenarioKinkId} onScenarioFocusConsumed=${() => setScenarioFocus({ parentKinkId: "", scenarioKinkId: "" })} /> ` : null} ${view === VIEWS.PLAYS ? html` <${MyPlaysView} auth=${auth} currentPlays=${currentPlays} scenarioPreferences=${scenarioPreferences} showImages=${showImages} onRate=${handleRate} onDirectionToggle=${handleDirectionToggle} onDirectionColumnChange=${handleDirectionColumnChange} onRateScenario=${handleScenarioRate} onNavigate=${handleNavigate} onOpenScenarios=${handleOpenScenarios} onDeletePlay=${handleDeletePlay} focusKinkId=${playsFocusKinkId} onFocusKinkConsumed=${() => setPlaysFocusKinkId("")} /> ` : null} ${view === VIEWS.TOGETHER ? html` <${TogetherView} auth=${auth} user=${user} currentPlays=${currentPlays} scenarioPreferences=${scenarioPreferences} onRate=${handleRate} onDirectionToggle=${handleDirectionToggle} onDeletePlay=${handleDeletePlay} onRateScenario=${handleScenarioRate} onNavigate=${handleNavigate} onOpenScenarios=${handleOpenScenarios} setStatus=${setStatus} graphDebugUi=${graphDebugUi} linkMutation=${linkMutation} partnerRequestMutations=${partnerRequestMutations} createGroupMutation=${createGroupMutation} shareToggleMutation=${shareToggleMutation} /> ` : null} ${view === VIEWS.SETTINGS ? html` <${SettingsView} auth=${auth} user=${user} status=${status} selectedRoles=${selectedRoles} roleSearchInput=${roleSearchInput} onRoleSearch=${handleRoleSearch} roleSearchItems=${visibleRoleSearchItems} saveRoleMutation=${saveRoleMutation} onPhotoPreference=${(value) => prefMutation.mutate(value)} logout=${logout} onDeleted=${logout} setStatus=${setStatus} /> ` : null}
`; } function NewProfileCredentials({ createResult }) { const [revealed, setRevealed] = useState(false); const [justCopied, setJustCopied] = useState(false); const token = createResult.private_token || ""; const masked = `••••••••${token.slice(-4)}`; useEffect(() => { if (!justCopied) return; const t = setTimeout(() => setJustCopied(false), 1500); return () => clearTimeout(t); }, [justCopied]); const copy = () => { try { navigator.clipboard?.writeText(token); } catch {} setJustCopied(true); }; return html`
Profile ID
${createResult.id}
Private pass
${revealed ? html`${token}` : html`${masked}`}
Save the private pass somewhere safe. You will need it to reopen this profile.
`; } function Onboarding({ step, setStep, loginForm, setLoginForm, createResult, status, loginMutation, createMutation, saveRoleMutation, roleSearchInput, onRoleSearch, roleSearchItems, selectedRoles, pendingPartnerId, onFinish, }) { const [showLoginToken, setShowLoginToken] = useState(false); if (createResult) { return html`

Set up your profile

Keep these credentials. You need them to reopen the profile later.

<${NewProfileCredentials} createResult=${createResult} /> ${pendingPartnerId ? html`
Invite saved for ${pendingPartnerId}. The connection request will go out after this setup.
` : null}
Pick your roles
Optional. You can edit these later in Settings.
onRoleSearch(e.target.value)} /> ${selectedRoles.length ? html`
${selectedRoles.map((item) => html` <${RoleChip} key=${item.id} kink=${item} selected=${true} onToggle=${(id, selected) => saveRoleMutation.mutate({ kinkId: id, selected })} /> `)}
` : html`
No roles selected yet.
`} ${roleSearchItems.length ? html`
${roleSearchItems.map((item) => html` <${RoleChip} key=${item.kink.id} kink=${item.kink} selected=${false} onToggle=${(id, selected) => saveRoleMutation.mutate({ kinkId: id, selected })} /> `)}
` : null}
<${StatusText} text=${status} />
`; } if (step === "login") { return html`

Welcome back

Open your profile to continue discovery.

${pendingPartnerId ? html`
You were invited by ${pendingPartnerId}.
` : null} setLoginForm((value) => ({ ...value, user_id: e.target.value }))} />
setLoginForm((value) => ({ ...value, private_token: e.target.value }))} />
<${StatusText} text=${status} />
`; } return html`

Play List

Discover what you like. See what you share. Keep private things private by default.

${pendingPartnerId ? html`
You were invited by ${pendingPartnerId}. Create or open a profile and we’ll stage the connection for you.
` : null}
<${StatusText} text=${status} />
`; } function App() { return html`<${QueryClientProvider} client=${queryClient}><${AppInner} />`; } createRoot(document.getElementById("root")).render(html`<${App} />`); initTelemetry();