172 lines
5.5 KiB
JavaScript
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}
|
|
>
|
|
×
|
|
</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>
|
|
);
|
|
}
|