Spaces:
Sleeping
Sleeping
| import React, { useMemo, useState } from "react"; | |
| import { html, partnerGroupChipLabel } from "./constants.js"; | |
| import { api, authHeaders } from "./api.js"; | |
| import { RoleChip, StatusText, ConfirmDialog } from "./components.js"; | |
| export function SettingsView({ | |
| auth, | |
| user, | |
| status, | |
| selectedRoles, | |
| roleSearchInput, | |
| onRoleSearch, | |
| roleSearchItems, | |
| saveRoleMutation, | |
| onPhotoPreference, | |
| logout, | |
| onDeleted, | |
| setStatus, | |
| }) { | |
| const [busy, setBusy] = useState(""); | |
| const [confirmDelete, setConfirmDelete] = useState(false); | |
| const sharingSummary = useMemo(() => { | |
| const groups = user?.partner_groups || []; | |
| return groups.map((group) => { | |
| const others = (group.participant_ids || []).filter((id) => id && id !== auth.userId); | |
| const visibleTo = group.my_share_full_list ? others : []; | |
| const visibleFrom = (group.sharing_member_ids || []).filter((id) => id && id !== auth.userId); | |
| return { group, visibleTo, visibleFrom }; | |
| }); | |
| }, [user?.partner_groups, auth.userId]); | |
| async function handleExport() { | |
| try { | |
| setBusy("export"); | |
| const payload = await api(`/users/${auth.userId}/export`, { headers: authHeaders(auth.token) }); | |
| const blob = new Blob([JSON.stringify(payload, null, 2)], { type: "application/json" }); | |
| const url = URL.createObjectURL(blob); | |
| const a = document.createElement("a"); | |
| a.href = url; | |
| a.download = `${auth.userId}-export.json`; | |
| document.body.appendChild(a); | |
| a.click(); | |
| a.remove(); | |
| URL.revokeObjectURL(url); | |
| setStatus("Export downloaded."); | |
| } catch (error) { | |
| setStatus(error.message); | |
| } finally { | |
| setBusy(""); | |
| } | |
| } | |
| async function handleDelete() { | |
| try { | |
| setBusy("delete"); | |
| await api(`/users/${auth.userId}`, { | |
| method: "DELETE", | |
| headers: authHeaders(auth.token), | |
| }); | |
| setStatus("Account deleted."); | |
| onDeleted?.(); | |
| } catch (error) { | |
| setStatus(error.message); | |
| } finally { | |
| setBusy(""); | |
| setConfirmDelete(false); | |
| } | |
| } | |
| return html` | |
| <${ConfirmDialog} | |
| open=${confirmDelete} | |
| title="Delete your account?" | |
| body="This deletes your profile, all plays, and all partner connections. This cannot be undone." | |
| confirmLabel=${busy === "delete" ? "Deleting…" : "Delete forever"} | |
| cancelLabel="Keep account" | |
| destructive=${true} | |
| onConfirm=${handleDelete} | |
| onCancel=${() => setConfirmDelete(false)} | |
| /> | |
| <div data-testid="settings-view"> | |
| <div className="top-bar"> | |
| <div> | |
| <div className="top-bar-title">Settings</div> | |
| <div className="tiny">Privacy, roles, and account controls</div> | |
| </div> | |
| </div> | |
| <div className="stack"> | |
| <div className="card stack"> | |
| <div> | |
| <div className="tiny">Profile ID</div> | |
| <code className="settings-code">${auth.userId}</code> | |
| </div> | |
| <label className="discover-photo-toggle settings-toggle"> | |
| <input | |
| data-testid="settings-show-images-toggle" | |
| type="checkbox" | |
| checked=${user?.preferences?.show_images ?? true} | |
| onChange=${(e) => onPhotoPreference?.(e.target.checked)} | |
| /> | |
| <span>Show photos on cards</span> | |
| </label> | |
| <button className="ghost sm" onClick=${logout}>Log out</button> | |
| </div> | |
| <div className="card stack"> | |
| <div> | |
| <strong>Roles</strong> | |
| <div className="tiny">Optional. These help with framing, not matching.</div> | |
| </div> | |
| <input | |
| data-testid="settings-role-search" | |
| placeholder="Search roles..." | |
| value=${roleSearchInput} | |
| onInput=${(e) => onRoleSearch(e.target.value)} | |
| /> | |
| ${selectedRoles.length | |
| ? html` | |
| <div className="chip-flow"> | |
| ${selectedRoles.map((item) => html` | |
| <${RoleChip} | |
| key=${item.id} | |
| kink=${item} | |
| selected=${true} | |
| onToggle=${(id, selected) => saveRoleMutation.mutate({ kinkId: id, selected })} | |
| /> | |
| `)} | |
| </div> | |
| ` | |
| : html`<div className="tiny">No roles selected.</div>`} | |
| ${roleSearchItems.length | |
| ? html` | |
| <div className="chip-flow"> | |
| ${roleSearchItems.map((item) => html` | |
| <${RoleChip} | |
| key=${item.kink.id} | |
| kink=${item.kink} | |
| selected=${false} | |
| onToggle=${(id, selected) => saveRoleMutation.mutate({ kinkId: id, selected })} | |
| /> | |
| `)} | |
| </div> | |
| ` | |
| : null} | |
| </div> | |
| <div className="card stack"> | |
| <div> | |
| <strong>Sharing</strong> | |
| <div className="tiny">Private by default. Full-list sharing is group-specific.</div> | |
| </div> | |
| ${sharingSummary.length | |
| ? html` | |
| <div className="stack-sm"> | |
| ${sharingSummary.map(({ group, visibleTo, visibleFrom }) => html` | |
| <div key=${group.id} className="settings-share-row"> | |
| <div> | |
| <div className="settings-share-title">${partnerGroupChipLabel(group, auth.userId)}</div> | |
| <div className="tiny"> | |
| ${visibleTo.length ? `They can view my list: ${visibleTo.join(", ")}` : "They cannot view my full list"} | |
| </div> | |
| <div className="tiny"> | |
| ${visibleFrom.length ? `I can view: ${visibleFrom.join(", ")}` : "I cannot view anyone else's full list"} | |
| </div> | |
| </div> | |
| </div> | |
| `)} | |
| </div> | |
| ` | |
| : html`<div className="tiny">No partner groups yet.</div>`} | |
| </div> | |
| <div className="card stack"> | |
| <div> | |
| <strong>Data</strong> | |
| <div className="tiny">Export your data or remove the account entirely.</div> | |
| </div> | |
| <div className="row" style=${{ gap: 8, flexWrap: "wrap" }}> | |
| <button data-testid="settings-export-data" onClick=${handleExport} disabled=${busy === "export"}>${busy === "export" ? "Exporting…" : "Export data"}</button> | |
| <button className="ghost" data-testid="settings-delete-account" onClick=${() => setConfirmDelete(true)}>Delete account</button> | |
| </div> | |
| </div> | |
| <${StatusText} text=${status} /> | |
| </div> | |
| </div> | |
| `; | |
| } | |