All checks were successful
Build & Deploy Costco Grocery List / build (push) Successful in 11s
Build & Deploy Costco Grocery List / verify-images (push) Successful in 1s
Build & Deploy Costco Grocery List / deploy (push) Successful in 5s
Build & Deploy Costco Grocery List / notify (push) Successful in 0s
670 lines
25 KiB
JavaScript
670 lines
25 KiB
JavaScript
import React, { useContext, useEffect, useState } from "react";
|
||
import {
|
||
createGroupInviteLink,
|
||
decideGroupJoinRequest,
|
||
deleteGroupInviteLink,
|
||
deleteHousehold,
|
||
getGroupInviteLinks,
|
||
getGroupJoinPolicy,
|
||
getHouseholdMembers,
|
||
getPendingGroupJoinRequests,
|
||
removeMember,
|
||
revokeGroupInviteLink,
|
||
reviveGroupInviteLink,
|
||
setGroupJoinPolicy,
|
||
updateHousehold,
|
||
updateMemberRole,
|
||
} from "../../api/households";
|
||
import { ToggleButtonGroup } from "../common";
|
||
import ConfirmSlideModal from "../modals/ConfirmSlideModal";
|
||
import { AuthContext } from "../../context/AuthContext";
|
||
import { HouseholdContext } from "../../context/HouseholdContext";
|
||
import useActionToast from "../../hooks/useActionToast";
|
||
import getApiErrorMessage from "../../lib/getApiErrorMessage";
|
||
import "../../styles/components/manage/ManageHousehold.css";
|
||
|
||
const JOIN_POLICY_OPTIONS = [
|
||
{ label: "Disabled", value: "NOT_ACCEPTING" },
|
||
{ label: "Auto", value: "AUTO_ACCEPT" },
|
||
{ label: "Manual", value: "APPROVAL_REQUIRED" },
|
||
];
|
||
|
||
const ROLE_METADATA = {
|
||
owner: { icon: "👑", label: "Owner" },
|
||
admin: { icon: "🛠️", label: "Admin" },
|
||
member: { icon: "🙂", label: "Member" },
|
||
viewer: { icon: "👀", label: "Viewer" },
|
||
};
|
||
|
||
const STATUS_METADATA = {
|
||
Active: { tone: "active", icon: "🟢" },
|
||
Used: { tone: "used", icon: "⚪" },
|
||
Revoked: { tone: "revoked", icon: "🔴" },
|
||
Expired: { tone: "expired", icon: "🟠" },
|
||
};
|
||
|
||
function getRequesterLabel(request) {
|
||
return (
|
||
request.display_name?.trim() ||
|
||
request.name?.trim() ||
|
||
request.username?.trim() ||
|
||
`User #${request.user_id}`
|
||
);
|
||
}
|
||
|
||
export default function ManageHousehold() {
|
||
const { userId } = useContext(AuthContext);
|
||
const { activeHousehold, refreshHouseholds } = useContext(HouseholdContext);
|
||
const toast = useActionToast();
|
||
const [members, setMembers] = useState([]);
|
||
const [loading, setLoading] = useState(true);
|
||
const [editingName, setEditingName] = useState(false);
|
||
const [newName, setNewName] = useState("");
|
||
const [joinPolicy, setJoinPolicyValue] = useState("NOT_ACCEPTING");
|
||
const [inviteLinks, setInviteLinks] = useState([]);
|
||
const [pendingRequests, setPendingRequests] = useState([]);
|
||
const [inviteLoading, setInviteLoading] = useState(false);
|
||
const [inviteError, setInviteError] = useState("");
|
||
const [ttlDays, setTtlDays] = useState(7);
|
||
const [singleUseMode, setSingleUseMode] = useState("UNLIMITED");
|
||
const [pendingDecisionId, setPendingDecisionId] = useState(null);
|
||
const [isLeaveModalOpen, setIsLeaveModalOpen] = useState(false);
|
||
const [pendingRoleChange, setPendingRoleChange] = useState(null);
|
||
|
||
const isManager = ["owner", "admin"].includes(activeHousehold?.role);
|
||
const isOwner = activeHousehold?.role === "owner";
|
||
const isMemberOnly = activeHousehold?.role === "member";
|
||
|
||
useEffect(() => {
|
||
loadMembers();
|
||
if (isManager) {
|
||
loadJoinAndInvites();
|
||
} else {
|
||
setPendingRequests([]);
|
||
}
|
||
}, [activeHousehold?.id, isManager]);
|
||
|
||
const loadMembers = async () => {
|
||
if (!activeHousehold?.id) return;
|
||
setLoading(true);
|
||
try {
|
||
const response = await getHouseholdMembers(activeHousehold.id);
|
||
setMembers(response.data);
|
||
} catch (error) {
|
||
console.error("Failed to load members:", error);
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
const loadJoinAndInvites = async () => {
|
||
if (!activeHousehold?.id || !isManager) return;
|
||
setInviteLoading(true);
|
||
setInviteError("");
|
||
try {
|
||
const [policyResponse, linksResponse, requestsResponse] = await Promise.all([
|
||
getGroupJoinPolicy(activeHousehold.id),
|
||
getGroupInviteLinks(activeHousehold.id),
|
||
getPendingGroupJoinRequests(activeHousehold.id),
|
||
]);
|
||
setJoinPolicyValue(policyResponse.data.joinPolicy || "NOT_ACCEPTING");
|
||
setInviteLinks(linksResponse.data.links || []);
|
||
setPendingRequests(requestsResponse.data.requests || []);
|
||
} catch (error) {
|
||
setInviteError(error.response?.data?.error?.message || "Failed to load invite links");
|
||
} finally {
|
||
setInviteLoading(false);
|
||
}
|
||
};
|
||
|
||
const getLinkStatus = (link) => {
|
||
const now = Date.now();
|
||
if (link.single_use && link.used_at) return "Used";
|
||
if (link.revoked_at) return "Revoked";
|
||
if (new Date(link.expires_at).getTime() <= now) return "Expired";
|
||
return "Active";
|
||
};
|
||
|
||
const copyTextToClipboard = async (text) => {
|
||
if (!text) return false;
|
||
|
||
if (navigator?.clipboard?.writeText) {
|
||
try {
|
||
await navigator.clipboard.writeText(text);
|
||
return true;
|
||
} catch {
|
||
// Fall through to legacy copy fallback.
|
||
}
|
||
}
|
||
|
||
try {
|
||
const textArea = document.createElement("textarea");
|
||
textArea.value = text;
|
||
textArea.setAttribute("readonly", "");
|
||
textArea.style.position = "fixed";
|
||
textArea.style.top = "-9999px";
|
||
textArea.style.left = "-9999px";
|
||
document.body.appendChild(textArea);
|
||
textArea.focus();
|
||
textArea.select();
|
||
const copied = document.execCommand("copy");
|
||
document.body.removeChild(textArea);
|
||
return copied;
|
||
} catch {
|
||
return false;
|
||
}
|
||
};
|
||
|
||
const copyInviteLink = async (token) => {
|
||
const inviteUrl = `${window.location.origin}/invite/${encodeURIComponent(token)}`;
|
||
const copied = await copyTextToClipboard(inviteUrl);
|
||
if (copied) {
|
||
const tokenLast4 = String(token || "").slice(-4);
|
||
toast.info("Copied invite link", `Copied invite link ending in ${tokenLast4}`);
|
||
return;
|
||
}
|
||
|
||
toast.error(
|
||
"Copy invite link failed",
|
||
"Copy invite link failed: unable to access clipboard. Copy manually."
|
||
);
|
||
};
|
||
|
||
const handleCreateInviteLink = async () => {
|
||
try {
|
||
setInviteError("");
|
||
await createGroupInviteLink(activeHousehold.id, {
|
||
policy: joinPolicy,
|
||
singleUse: singleUseMode === "ONE_TIME",
|
||
ttlDays,
|
||
});
|
||
await loadJoinAndInvites();
|
||
toast.success("Created invite link", `Created invite link (${ttlDays} day TTL)`);
|
||
} catch (error) {
|
||
const message = getApiErrorMessage(error, "Failed to create invite link");
|
||
setInviteError(message);
|
||
toast.error("Create invite link failed", `Create invite link failed: ${message}`);
|
||
}
|
||
};
|
||
|
||
const handleUpdateJoinPolicy = async (value) => {
|
||
try {
|
||
setInviteError("");
|
||
await setGroupJoinPolicy(activeHousehold.id, value);
|
||
setJoinPolicyValue(value);
|
||
toast.success("Updated join policy", `Join policy set to ${value}`);
|
||
} catch (error) {
|
||
const message = getApiErrorMessage(error, "Failed to update join policy");
|
||
setInviteError(message);
|
||
toast.error("Update join policy failed", `Update join policy failed: ${message}`);
|
||
}
|
||
};
|
||
|
||
const handleJoinRequestDecision = async (request, decision) => {
|
||
const requesterName = getRequesterLabel(request);
|
||
setPendingDecisionId(request.id);
|
||
try {
|
||
setInviteError("");
|
||
await decideGroupJoinRequest(activeHousehold.id, request.id, decision);
|
||
await Promise.all([loadJoinAndInvites(), loadMembers()]);
|
||
if (decision === "APPROVE") {
|
||
toast.success("Approved join request", `Approved ${requesterName}`);
|
||
} else {
|
||
toast.info("Denied join request", `Denied ${requesterName}`);
|
||
}
|
||
} catch (error) {
|
||
const message = getApiErrorMessage(error, "Failed to update join request");
|
||
setInviteError(message);
|
||
toast.error("Join request update failed", `Join request update failed: ${message}`);
|
||
} finally {
|
||
setPendingDecisionId(null);
|
||
}
|
||
};
|
||
|
||
const handleRevokeInvite = async (linkId) => {
|
||
try {
|
||
setInviteError("");
|
||
await revokeGroupInviteLink(activeHousehold.id, linkId);
|
||
await loadJoinAndInvites();
|
||
toast.success("Revoked invite link", "Revoked invite link");
|
||
} catch (error) {
|
||
const message = getApiErrorMessage(error, "Failed to revoke invite link");
|
||
setInviteError(message);
|
||
toast.error("Revoke invite link failed", `Revoke invite link failed: ${message}`);
|
||
}
|
||
};
|
||
|
||
const handleReviveInvite = async (linkId) => {
|
||
try {
|
||
setInviteError("");
|
||
await reviveGroupInviteLink(activeHousehold.id, linkId, ttlDays);
|
||
await loadJoinAndInvites();
|
||
toast.success("Revived invite link", `Revived invite link for ${ttlDays} days`);
|
||
} catch (error) {
|
||
const message = getApiErrorMessage(error, "Failed to revive invite link");
|
||
setInviteError(message);
|
||
toast.error("Revive invite link failed", `Revive invite link failed: ${message}`);
|
||
}
|
||
};
|
||
|
||
const handleDeleteInvite = async (linkId) => {
|
||
if (!confirm("Delete this invite link permanently?")) return;
|
||
try {
|
||
setInviteError("");
|
||
await deleteGroupInviteLink(activeHousehold.id, linkId);
|
||
await loadJoinAndInvites();
|
||
toast.success("Deleted invite link", "Deleted invite link");
|
||
} catch (error) {
|
||
const message = getApiErrorMessage(error, "Failed to delete invite link");
|
||
setInviteError(message);
|
||
toast.error("Delete invite link failed", `Delete invite link failed: ${message}`);
|
||
}
|
||
};
|
||
|
||
const handleUpdateName = async () => {
|
||
if (!newName.trim() || newName === activeHousehold.name) {
|
||
setEditingName(false);
|
||
return;
|
||
}
|
||
|
||
try {
|
||
await updateHousehold(activeHousehold.id, newName);
|
||
await refreshHouseholds();
|
||
toast.success("Updated household name", `Updated household name to ${newName.trim()}`);
|
||
setEditingName(false);
|
||
} catch (error) {
|
||
const message = getApiErrorMessage(error, "Failed to update household name");
|
||
toast.error("Update household name failed", `Update household name failed: ${message}`);
|
||
}
|
||
};
|
||
|
||
const handleConfirmRoleChange = async () => {
|
||
if (!pendingRoleChange) return;
|
||
|
||
const { memberId, nextRole, memberName } = pendingRoleChange;
|
||
|
||
try {
|
||
await updateMemberRole(activeHousehold.id, memberId, nextRole);
|
||
await Promise.all([
|
||
loadMembers(),
|
||
nextRole === "owner" ? refreshHouseholds() : Promise.resolve(),
|
||
]);
|
||
if (nextRole === "owner") {
|
||
toast.success("Transferred household ownership", `Transferred ownership to ${memberName}`);
|
||
} else {
|
||
toast.success("Updated member role", `Updated role for ${memberName} to ${nextRole}`);
|
||
}
|
||
setPendingRoleChange(null);
|
||
} catch (error) {
|
||
const message = getApiErrorMessage(error, "Failed to update member role");
|
||
toast.error("Update member role failed", `Update member role failed: ${message}`);
|
||
}
|
||
};
|
||
|
||
const handleUpdateRole = (memberId, nextRole, memberName) => {
|
||
if (!nextRole) return;
|
||
|
||
setPendingRoleChange({ memberId, nextRole, memberName });
|
||
};
|
||
|
||
const handleRemoveMember = async (memberId, username) => {
|
||
if (!confirm(`Remove ${username} from this household?`)) return;
|
||
|
||
try {
|
||
await removeMember(activeHousehold.id, memberId);
|
||
await loadMembers();
|
||
toast.success("Removed member", `Removed member ${username}`);
|
||
} catch (error) {
|
||
const message = getApiErrorMessage(error, "Failed to remove member");
|
||
toast.error("Remove member failed", `Remove member failed: ${message}`);
|
||
}
|
||
};
|
||
|
||
const handleDeleteHousehold = async () => {
|
||
if (!confirm(`Delete "${activeHousehold.name}"? This will delete all lists and data. This cannot be undone.`)) return;
|
||
if (!confirm("Are you absolutely sure? Type DELETE to confirm.")) return;
|
||
|
||
try {
|
||
const householdName = activeHousehold.name;
|
||
await deleteHousehold(activeHousehold.id);
|
||
await refreshHouseholds();
|
||
toast.success("Deleted household", `Deleted household ${householdName}`);
|
||
} catch (error) {
|
||
const message = getApiErrorMessage(error, "Failed to delete household");
|
||
toast.error("Delete household failed", `Delete household failed: ${message}`);
|
||
}
|
||
};
|
||
|
||
const handleLeaveHousehold = async () => {
|
||
if (!activeHousehold?.id) return;
|
||
|
||
try {
|
||
const householdName = activeHousehold.name;
|
||
await removeMember(activeHousehold.id, parseInt(userId, 10));
|
||
setIsLeaveModalOpen(false);
|
||
await refreshHouseholds();
|
||
toast.success("Left household", `Left household ${householdName}`);
|
||
} catch (error) {
|
||
const message = getApiErrorMessage(error, "Failed to leave household");
|
||
toast.error("Leave household failed", `Leave household failed: ${message}`);
|
||
}
|
||
};
|
||
|
||
const managerCount = members.filter((member) => ["owner", "admin"].includes(member.role)).length;
|
||
const memberCount = members.filter((member) => member.role === "member").length;
|
||
|
||
return (
|
||
<div className="manage-household">
|
||
<section key="household-name" className="manage-section">
|
||
<div className="manage-section-header">
|
||
<div>
|
||
<p className="manage-section-eyebrow">Household</p>
|
||
<h2>Identity</h2>
|
||
<p className="section-description">
|
||
Keep the household name crisp and easy to recognize across invites and shared lists.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
{editingName ? (
|
||
<div className="edit-name-form">
|
||
<input
|
||
type="text"
|
||
value={newName}
|
||
onChange={(e) => setNewName(e.target.value)}
|
||
placeholder="Household name"
|
||
autoFocus
|
||
/>
|
||
<button onClick={handleUpdateName} className="btn-primary">Save</button>
|
||
<button onClick={() => setEditingName(false)} className="btn-secondary">Cancel</button>
|
||
</div>
|
||
) : (
|
||
<div className="name-display">
|
||
<div className="name-display-copy">
|
||
<h3>{activeHousehold.name}</h3>
|
||
<div className="household-summary-chips">
|
||
<span className="household-summary-chip">🏠 {members.length} people</span>
|
||
<span className="household-summary-chip">🛡️ {managerCount} managers</span>
|
||
<span className="household-summary-chip">🛒 {memberCount} shoppers</span>
|
||
</div>
|
||
</div>
|
||
{isManager && (
|
||
<button
|
||
onClick={() => {
|
||
setNewName(activeHousehold.name);
|
||
setEditingName(true);
|
||
}}
|
||
className="btn-secondary"
|
||
>
|
||
Edit Name
|
||
</button>
|
||
)}
|
||
</div>
|
||
)}
|
||
</section>
|
||
|
||
{isManager && (
|
||
<section key="join-and-invites" className="manage-section">
|
||
<div className="manage-section-header">
|
||
<div>
|
||
<p className="manage-section-eyebrow">Entry Rules</p>
|
||
<h2>Invite Links</h2>
|
||
<p className="section-description">
|
||
Decide how new people can enter, review manual approvals, then create invite links for the flow you want.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
{inviteError && <p className="section-error">{inviteError}</p>}
|
||
|
||
<ToggleButtonGroup
|
||
value={joinPolicy}
|
||
ariaLabel="Join policy options"
|
||
className="tbg-group manage-household-join-policy-toggle"
|
||
options={JOIN_POLICY_OPTIONS.map((option) => ({
|
||
...option,
|
||
disabled: inviteLoading,
|
||
}))}
|
||
onChange={handleUpdateJoinPolicy}
|
||
/>
|
||
|
||
<div className="pending-requests-summary">
|
||
<span className="pending-requests-summary-label">Pending approvals</span>
|
||
<span className="pending-requests-summary-count">{pendingRequests.length}</span>
|
||
</div>
|
||
|
||
{inviteLoading ? (
|
||
<p>Loading invite settings...</p>
|
||
) : pendingRequests.length === 0 ? (
|
||
<p className="section-description">No pending join requests right now.</p>
|
||
) : (
|
||
<div className="pending-requests-list">
|
||
{pendingRequests.map((request) => {
|
||
const requesterLabel = getRequesterLabel(request);
|
||
const isBusy = pendingDecisionId === request.id;
|
||
return (
|
||
<div key={request.id} className="pending-request-card">
|
||
<div className="pending-request-main">
|
||
<div className="pending-request-topline">
|
||
<p className="pending-request-name">{requesterLabel}</p>
|
||
<span className="pending-request-badge">🕒 Pending</span>
|
||
</div>
|
||
<p className="pending-request-meta">
|
||
@{request.username} • Requested {new Date(request.created_at).toLocaleString()}
|
||
</p>
|
||
</div>
|
||
<div className="pending-request-actions">
|
||
<button
|
||
className="btn-secondary btn-small member-role-action"
|
||
onClick={() => handleJoinRequestDecision(request, "APPROVE")}
|
||
disabled={isBusy}
|
||
>
|
||
{isBusy ? "Working..." : "Approve"}
|
||
</button>
|
||
<button
|
||
className="btn-danger btn-small"
|
||
onClick={() => handleJoinRequestDecision(request, "DENY")}
|
||
disabled={isBusy}
|
||
>
|
||
Deny
|
||
</button>
|
||
</div>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
)}
|
||
|
||
<div className="invite-controls">
|
||
<label>
|
||
<span className="invite-control-label">TTL</span>
|
||
<select value={ttlDays} onChange={(e) => setTtlDays(Number(e.target.value))}>
|
||
{[1, 2, 3, 4, 5, 6, 7].map((day) => (
|
||
<option key={day} value={day}>{day} day{day > 1 ? "s" : ""}</option>
|
||
))}
|
||
</select>
|
||
</label>
|
||
<label>
|
||
<span className="invite-control-label">Usage</span>
|
||
<select value={singleUseMode} onChange={(e) => setSingleUseMode(e.target.value)}>
|
||
<option value="UNLIMITED">Unlimited</option>
|
||
<option value="ONE_TIME">1 use</option>
|
||
</select>
|
||
</label>
|
||
<button className="btn-primary" onClick={handleCreateInviteLink} disabled={inviteLoading}>
|
||
Create link
|
||
</button>
|
||
</div>
|
||
|
||
{inviteLoading ? (
|
||
<p>Loading invite links...</p>
|
||
) : inviteLinks.length === 0 ? (
|
||
<p className="section-description">No invite links yet.</p>
|
||
) : (
|
||
<div className="invite-links-list">
|
||
{inviteLinks.map((link) => {
|
||
const status = getLinkStatus(link);
|
||
const isActive = status === "Active";
|
||
const statusMeta = STATUS_METADATA[status] || STATUS_METADATA.Active;
|
||
return (
|
||
<div key={link.id} className="invite-link-card">
|
||
<div className="invite-link-main">
|
||
<div className="invite-link-topline">
|
||
<p className="invite-link-token">Invite ending in {String(link.token).slice(-4)}</p>
|
||
<span className={`invite-status-badge is-${statusMeta.tone}`}>
|
||
{statusMeta.icon} {status}
|
||
</span>
|
||
</div>
|
||
<p className="invite-link-meta">
|
||
Policy: {link.policy} • Expires {new Date(link.expires_at).toLocaleString()}
|
||
</p>
|
||
</div>
|
||
<div className="invite-link-actions">
|
||
<button className="btn-secondary btn-small" onClick={() => copyInviteLink(link.token)}>
|
||
Copy link
|
||
</button>
|
||
{isActive ? (
|
||
<button className="btn-secondary btn-small" onClick={() => handleRevokeInvite(link.id)}>
|
||
Revoke
|
||
</button>
|
||
) : (
|
||
<button className="btn-secondary btn-small" onClick={() => handleReviveInvite(link.id)}>
|
||
Revive
|
||
</button>
|
||
)}
|
||
<button className="btn-danger btn-small" onClick={() => handleDeleteInvite(link.id)}>
|
||
Delete
|
||
</button>
|
||
</div>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
)}
|
||
</section>
|
||
)}
|
||
|
||
<section key="members" className="manage-section">
|
||
<div className="manage-section-header">
|
||
<div>
|
||
<p className="manage-section-eyebrow">People</p>
|
||
<h2>Members ({members.length})</h2>
|
||
<p className="section-description">
|
||
Role badges and compact actions make it easier to see who runs the household and who just shops.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
{loading ? (
|
||
<p>Loading members...</p>
|
||
) : (
|
||
<div className="members-list">
|
||
{members.map((member) => {
|
||
const roleMeta = ROLE_METADATA[member.role] || { icon: "👤", label: member.role };
|
||
const isSelf = member.id === parseInt(userId, 10);
|
||
|
||
return (
|
||
<div key={member.id} className="member-card">
|
||
<div className="member-main">
|
||
<div className="member-avatar" aria-hidden="true">{roleMeta.icon}</div>
|
||
<div className="member-info">
|
||
<div className="member-topline">
|
||
<span className={`member-role member-role-${member.role}`}>
|
||
{roleMeta.icon} {roleMeta.label}
|
||
</span>
|
||
{isSelf && <span className="member-self-pill">✨ You</span>}
|
||
</div>
|
||
<span className="member-name">{member.username}</span>
|
||
<span className="member-meta">ID #{member.id}</span>
|
||
</div>
|
||
</div>
|
||
{isManager && !isSelf && member.role !== "owner" && (
|
||
<div className="member-actions">
|
||
{isOwner && (
|
||
<button
|
||
onClick={() => handleUpdateRole(member.id, "owner", member.username)}
|
||
className="btn-primary btn-small member-owner-action"
|
||
>
|
||
Make Owner
|
||
</button>
|
||
)}
|
||
<button
|
||
onClick={() => handleUpdateRole(
|
||
member.id,
|
||
member.role === "admin" ? "member" : "admin",
|
||
member.username
|
||
)}
|
||
className="btn-secondary btn-small member-role-action"
|
||
>
|
||
{member.role === "admin" ? "Make Member" : "Make Admin"}
|
||
</button>
|
||
<button
|
||
onClick={() => handleRemoveMember(member.id, member.username)}
|
||
className="btn-danger btn-small"
|
||
>
|
||
Remove
|
||
</button>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
)}
|
||
</section>
|
||
|
||
{(isManager || isMemberOnly) && (
|
||
<section key="danger-zone" className="manage-section danger-zone">
|
||
<div className="manage-section-header">
|
||
<div>
|
||
<p className="manage-section-eyebrow">Final Actions</p>
|
||
<h2>Danger Zone</h2>
|
||
<p className="section-description">
|
||
{isMemberOnly
|
||
? "Leaving removes your access to this household."
|
||
: "Deleting a household is permanent and will delete all lists, items, and history."}
|
||
</p>
|
||
</div>
|
||
{isMemberOnly ? (
|
||
<button onClick={() => setIsLeaveModalOpen(true)} className="btn-danger">
|
||
Leave Household
|
||
</button>
|
||
) : (
|
||
<button onClick={handleDeleteHousehold} className="btn-danger">
|
||
Delete Household
|
||
</button>
|
||
)}
|
||
</div>
|
||
</section>
|
||
)}
|
||
|
||
<ConfirmSlideModal
|
||
isOpen={isLeaveModalOpen}
|
||
title={`Leave "${activeHousehold?.name || "this household"}"?`}
|
||
description="Slide all the way to confirm. You will lose access to this household."
|
||
confirmLabel="Leave Household"
|
||
onClose={() => setIsLeaveModalOpen(false)}
|
||
onConfirm={handleLeaveHousehold}
|
||
/>
|
||
|
||
<ConfirmSlideModal
|
||
isOpen={Boolean(pendingRoleChange)}
|
||
title={
|
||
pendingRoleChange?.nextRole === "owner"
|
||
? `Transfer ownership to ${pendingRoleChange?.memberName || "this member"}?`
|
||
: `Change ${pendingRoleChange?.memberName || "this member"} to ${pendingRoleChange?.nextRole || "member"}?`
|
||
}
|
||
description={
|
||
pendingRoleChange?.nextRole === "owner"
|
||
? "Slide to confirm. They will become the household owner and you will become an admin."
|
||
: "Slide to confirm this household role change."
|
||
}
|
||
confirmLabel={
|
||
pendingRoleChange?.nextRole === "owner"
|
||
? "Transfer Ownership"
|
||
: `Make ${pendingRoleChange?.nextRole === "admin" ? "Admin" : "Member"}`
|
||
}
|
||
onClose={() => setPendingRoleChange(null)}
|
||
onConfirm={handleConfirmRoleChange}
|
||
/>
|
||
</div>
|
||
);
|
||
}
|