kink-discovery / frontend /settings.js
Perplexed7675's picture
Sync from kink_cli (Docker Space)
ad98c97 verified
Raw
History Blame Contribute Delete
6.82 kB
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>
`;
}