grocery-app/frontend/src/components/manage/CreateJoinHousehold.jsx

172 lines
5.5 KiB
JavaScript

import { useContext, useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import { HouseholdContext } from "../../context/HouseholdContext";
import useActionToast from "../../hooks/useActionToast";
import getApiErrorMessage from "../../lib/getApiErrorMessage";
import "../../styles/components/manage/CreateJoinHousehold.css";
function extractInviteToken(value) {
const trimmed = value.trim();
if (!trimmed) return null;
const directMatch = trimmed.match(/^\/?invite\/([a-zA-Z0-9]+)$/);
if (directMatch) return directMatch[1];
try {
const parsed = new URL(trimmed, window.location.origin);
const urlMatch = parsed.pathname.match(/^\/invite\/([a-zA-Z0-9]+)$/);
if (urlMatch) return urlMatch[1];
} catch {
return null;
}
return null;
}
export default function CreateJoinHousehold({ initialMode = "create", onClose }) {
const navigate = useNavigate();
const toast = useActionToast();
const { createHousehold: createHouseholdWithContext } = useContext(HouseholdContext);
const [mode, setMode] = useState(initialMode === "join" ? "join" : "create");
const [householdName, setHouseholdName] = useState("");
const [inviteLink, setInviteLink] = useState("");
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
useEffect(() => {
setMode(initialMode === "join" ? "join" : "create");
setError("");
}, [initialMode]);
const handleCreate = async (e) => {
e.preventDefault();
if (!householdName.trim()) return;
setLoading(true);
setError("");
try {
await createHouseholdWithContext(householdName);
toast.success("Created household", `Created household ${householdName.trim()}`);
onClose();
} catch (err) {
console.error("Failed to create household:", err);
const message = getApiErrorMessage(err, "Failed to create household");
setError(message);
toast.error("Create household failed", `Create household failed: ${message}`);
} finally {
setLoading(false);
}
};
const handleJoin = async (e) => {
e.preventDefault();
if (!inviteLink.trim()) return;
setLoading(true);
setError("");
try {
const inviteToken = extractInviteToken(inviteLink);
if (!inviteToken) {
const message = "Use a household invite link like /invite/abcd1234.";
setError(message);
toast.error("Open invite link failed", message);
return;
}
toast.info("Opening invite link", "Checking invite details");
onClose();
navigate(`/invite/${inviteToken}`);
} finally {
setLoading(false);
}
};
return (
<div className="create-join-modal-overlay" onClick={onClose}>
<div className="create-join-modal" onClick={(e) => e.stopPropagation()}>
<div className="modal-header">
<h2>Household</h2>
<button
className="close-btn"
type="button"
aria-label="Close household dialog"
onClick={onClose}
>
&times;
</button>
</div>
<div className="mode-tabs">
<button
className={`mode-tab ${mode === "create" ? "active" : ""}`}
onClick={() => setMode("create")}
>
Create New
</button>
<button
className={`mode-tab ${mode === "join" ? "active" : ""}`}
onClick={() => setMode("join")}
>
Join Existing
</button>
</div>
{error && <div className="error-message">{error}</div>}
{mode === "create" ? (
<form onSubmit={handleCreate} className="household-form">
<div className="form-group">
<label htmlFor="householdName">Household Name</label>
<input
id="householdName"
type="text"
value={householdName}
onChange={(e) => setHouseholdName(e.target.value)}
placeholder="e.g., Smith Family"
required
autoFocus
/>
</div>
<div className="form-actions">
<button type="button" onClick={onClose} className="btn-secondary">
Cancel
</button>
<button type="submit" className="btn-primary" disabled={loading}>
{loading ? "Creating..." : "Create Household"}
</button>
</div>
</form>
) : (
<form onSubmit={handleJoin} className="household-form">
<div className="form-group">
<label htmlFor="inviteLink">Invite Link</label>
<input
id="inviteLink"
type="text"
value={inviteLink}
onChange={(e) => setInviteLink(e.target.value)}
placeholder="https://.../invite/your-token"
required
autoFocus
/>
<p className="form-hint">
Paste the full invite URL or a local path like /invite/your-token
</p>
</div>
<div className="form-actions">
<button type="button" onClick={onClose} className="btn-secondary">
Cancel
</button>
<button type="submit" className="btn-primary" disabled={loading}>
{loading ? "Opening..." : "Open Invite"}
</button>
</div>
</form>
)}
</div>
</div>
);
}