v0.12.51 updated "Mixed Age" login type.
This commit is contained in:
@@ -3,16 +3,25 @@ import { useToast } from '../contexts/ToastContext.jsx';
|
||||
import { useAuth } from '../contexts/AuthContext.jsx';
|
||||
import { api } from '../utils/api.js';
|
||||
|
||||
export default function AddChildAliasModal({ onClose }) {
|
||||
export default function AddChildAliasModal({ features = {}, onClose }) {
|
||||
const toast = useToast();
|
||||
const { user: currentUser } = useAuth();
|
||||
const loginType = features.loginType || 'guardian_only';
|
||||
const isMixedAge = loginType === 'mixed_age';
|
||||
|
||||
// ── Guardian-only state (alias form) ──────────────────────────────────────
|
||||
const [aliases, setAliases] = useState([]);
|
||||
const [editingAlias, setEditingAlias] = useState(null); // null = new entry
|
||||
const [editingAlias, setEditingAlias] = useState(null);
|
||||
const [form, setForm] = useState({ firstName: '', lastName: '', dob: '', phone: '', email: '' });
|
||||
const [avatarFile, setAvatarFile] = useState(null);
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
// Partner state
|
||||
// ── Mixed-age state (real minor users) ────────────────────────────────────
|
||||
const [minorPlayers, setMinorPlayers] = useState([]); // available + already-mine
|
||||
const [selectedMinorId, setSelectedMinorId] = useState('');
|
||||
const [addingMinor, setAddingMinor] = useState(false);
|
||||
|
||||
// ── Partner state (shared) ────────────────────────────────────────────────
|
||||
const [partner, setPartner] = useState(null);
|
||||
const [selectedPartnerId, setSelectedPartnerId] = useState('');
|
||||
const [respondSeparately, setRespondSeparately] = useState(false);
|
||||
@@ -20,20 +29,27 @@ export default function AddChildAliasModal({ onClose }) {
|
||||
const [savingPartner, setSavingPartner] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
Promise.all([
|
||||
api.getAliases(),
|
||||
api.getPartner(),
|
||||
api.searchUsers(''),
|
||||
]).then(([aliasRes, partnerRes, usersRes]) => {
|
||||
setAliases(aliasRes.aliases || []);
|
||||
const loads = [api.getPartner(), api.searchUsers('')];
|
||||
if (isMixedAge) {
|
||||
loads.push(api.getMinorPlayers());
|
||||
} else {
|
||||
loads.push(api.getAliases());
|
||||
}
|
||||
Promise.all(loads).then(([partnerRes, usersRes, thirdRes]) => {
|
||||
const p = partnerRes.partner || null;
|
||||
setPartner(p);
|
||||
setSelectedPartnerId(p?.id?.toString() || '');
|
||||
setRespondSeparately(p?.respond_separately || false);
|
||||
setAllUsers((usersRes.users || []).filter(u => u.id !== currentUser?.id));
|
||||
if (isMixedAge) {
|
||||
setMinorPlayers(thirdRes.users || []);
|
||||
} else {
|
||||
setAliases(thirdRes.aliases || []);
|
||||
}
|
||||
}).catch(() => {});
|
||||
}, []);
|
||||
}, [isMixedAge]);
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────
|
||||
const set = k => e => setForm(p => ({ ...p, [k]: e.target.value }));
|
||||
|
||||
const resetForm = () => {
|
||||
@@ -42,6 +58,47 @@ export default function AddChildAliasModal({ onClose }) {
|
||||
setAvatarFile(null);
|
||||
};
|
||||
|
||||
const lbl = (text, required) => (
|
||||
<label className="text-sm" style={{ color: 'var(--text-secondary)', display: 'block', marginBottom: 4 }}>
|
||||
{text}{required && <span style={{ color: 'var(--error)', marginLeft: 2 }}>*</span>}
|
||||
</label>
|
||||
);
|
||||
|
||||
// ── Partner handlers ──────────────────────────────────────────────────────
|
||||
const handleSavePartner = async () => {
|
||||
setSavingPartner(true);
|
||||
try {
|
||||
if (!selectedPartnerId) {
|
||||
await api.removePartner();
|
||||
setPartner(null);
|
||||
setRespondSeparately(false);
|
||||
if (!isMixedAge) {
|
||||
const { aliases: fresh } = await api.getAliases();
|
||||
setAliases(fresh || []);
|
||||
resetForm();
|
||||
} else {
|
||||
const { users: fresh } = await api.getMinorPlayers();
|
||||
setMinorPlayers(fresh || []);
|
||||
}
|
||||
toast('Spouse/Partner/Co-Parent removed', 'success');
|
||||
} else {
|
||||
const { partner: p } = await api.setPartner(parseInt(selectedPartnerId), respondSeparately);
|
||||
setPartner(p);
|
||||
setRespondSeparately(p?.respond_separately || false);
|
||||
if (!isMixedAge) {
|
||||
const { aliases: fresh } = await api.getAliases();
|
||||
setAliases(fresh || []);
|
||||
}
|
||||
toast('Spouse/Partner/Co-Parent saved', 'success');
|
||||
}
|
||||
} catch (e) {
|
||||
toast(e.message, 'error');
|
||||
} finally {
|
||||
setSavingPartner(false);
|
||||
}
|
||||
};
|
||||
|
||||
// ── Guardian-only alias handlers ──────────────────────────────────────────
|
||||
const handleSelectAlias = (a) => {
|
||||
if (editingAlias?.id === a.id) { resetForm(); return; }
|
||||
setEditingAlias(a);
|
||||
@@ -55,33 +112,7 @@ export default function AddChildAliasModal({ onClose }) {
|
||||
setAvatarFile(null);
|
||||
};
|
||||
|
||||
const handleSavePartner = async () => {
|
||||
setSavingPartner(true);
|
||||
try {
|
||||
if (!selectedPartnerId) {
|
||||
await api.removePartner();
|
||||
setPartner(null);
|
||||
setRespondSeparately(false);
|
||||
const { aliases: fresh } = await api.getAliases();
|
||||
setAliases(fresh || []);
|
||||
resetForm();
|
||||
toast('Spouse/Partner/Co-Parent removed', 'success');
|
||||
} else {
|
||||
const { partner: p } = await api.setPartner(parseInt(selectedPartnerId), respondSeparately);
|
||||
setPartner(p);
|
||||
setRespondSeparately(p?.respond_separately || false);
|
||||
const { aliases: fresh } = await api.getAliases();
|
||||
setAliases(fresh || []);
|
||||
toast('Spouse/Partner/Co-Parent saved', 'success');
|
||||
}
|
||||
} catch (e) {
|
||||
toast(e.message, 'error');
|
||||
} finally {
|
||||
setSavingPartner(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
const handleSaveAlias = async () => {
|
||||
if (!form.firstName.trim() || !form.lastName.trim())
|
||||
return toast('First and last name required', 'error');
|
||||
setSaving(true);
|
||||
@@ -117,7 +148,7 @@ export default function AddChildAliasModal({ onClose }) {
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (e, aliasId) => {
|
||||
const handleDeleteAlias = async (e, aliasId) => {
|
||||
e.stopPropagation();
|
||||
try {
|
||||
await api.deleteAlias(aliasId);
|
||||
@@ -127,11 +158,35 @@ export default function AddChildAliasModal({ onClose }) {
|
||||
} catch (err) { toast(err.message, 'error'); }
|
||||
};
|
||||
|
||||
const lbl = (text, required) => (
|
||||
<label className="text-sm" style={{ color: 'var(--text-secondary)', display: 'block', marginBottom: 4 }}>
|
||||
{text}{required && <span style={{ color: 'var(--error)', marginLeft: 2 }}>*</span>}
|
||||
</label>
|
||||
);
|
||||
// ── Mixed-age minor handlers ──────────────────────────────────────────────
|
||||
const myMinors = minorPlayers.filter(u => u.guardian_user_id === currentUser?.id);
|
||||
const availableMinors = minorPlayers.filter(u => !u.guardian_user_id);
|
||||
|
||||
const handleAddMinor = async () => {
|
||||
if (!selectedMinorId) return;
|
||||
setAddingMinor(true);
|
||||
try {
|
||||
await api.addGuardianChild(parseInt(selectedMinorId));
|
||||
const { users: fresh } = await api.getMinorPlayers();
|
||||
setMinorPlayers(fresh || []);
|
||||
setSelectedMinorId('');
|
||||
toast('Child added and account activated', 'success');
|
||||
} catch (e) {
|
||||
toast(e.message, 'error');
|
||||
} finally {
|
||||
setAddingMinor(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveMinor = async (e, minorId) => {
|
||||
e.stopPropagation();
|
||||
try {
|
||||
await api.removeGuardianChild(minorId);
|
||||
const { users: fresh } = await api.getMinorPlayers();
|
||||
setMinorPlayers(fresh || []);
|
||||
toast('Child removed', 'success');
|
||||
} catch (err) { toast(err.message, 'error'); }
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="modal-overlay" onClick={e => e.target === e.currentTarget && onClose()}>
|
||||
@@ -187,93 +242,169 @@ export default function AddChildAliasModal({ onClose }) {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Existing aliases list */}
|
||||
{aliases.length > 0 && (
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<div className="text-sm font-medium" style={{ color: 'var(--text-secondary)', marginBottom: 8 }}>
|
||||
Your Children — click to edit
|
||||
</div>
|
||||
<div style={{ border: '1px solid var(--border)', borderRadius: 'var(--radius)', overflow: 'hidden' }}>
|
||||
{aliases.map((a, i) => (
|
||||
<div
|
||||
key={a.id}
|
||||
onClick={() => handleSelectAlias(a)}
|
||||
style={{
|
||||
display: 'flex', alignItems: 'center', gap: 10,
|
||||
padding: '9px 12px', cursor: 'pointer',
|
||||
borderBottom: i < aliases.length - 1 ? '1px solid var(--border)' : 'none',
|
||||
background: editingAlias?.id === a.id ? 'var(--primary-light)' : 'transparent',
|
||||
}}
|
||||
>
|
||||
<span style={{ flex: 1, fontSize: 14, fontWeight: editingAlias?.id === a.id ? 600 : 400 }}>
|
||||
{a.first_name} {a.last_name}
|
||||
</span>
|
||||
{a.date_of_birth && (
|
||||
<span style={{ fontSize: 12, color: 'var(--text-tertiary)' }}>
|
||||
{a.date_of_birth.slice(0, 10)}
|
||||
</span>
|
||||
)}
|
||||
<button
|
||||
onClick={e => handleDelete(e, a.id)}
|
||||
style={{ background: 'none', border: 'none', cursor: 'pointer', color: 'var(--error)', fontSize: 18, lineHeight: 1, padding: '0 2px' }}
|
||||
aria-label="Remove"
|
||||
>×</button>
|
||||
{/* ── Mixed Age: link real minor users ── */}
|
||||
{isMixedAge && (
|
||||
<>
|
||||
{/* Current children list */}
|
||||
{myMinors.length > 0 && (
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<div className="text-sm font-medium" style={{ color: 'var(--text-secondary)', marginBottom: 8 }}>
|
||||
Your Children
|
||||
</div>
|
||||
))}
|
||||
<div style={{ border: '1px solid var(--border)', borderRadius: 'var(--radius)', overflow: 'hidden' }}>
|
||||
{myMinors.map((u, i) => (
|
||||
<div
|
||||
key={u.id}
|
||||
style={{
|
||||
display: 'flex', alignItems: 'center', gap: 10,
|
||||
padding: '9px 12px',
|
||||
borderBottom: i < myMinors.length - 1 ? '1px solid var(--border)' : 'none',
|
||||
}}
|
||||
>
|
||||
<span style={{ flex: 1, fontSize: 14 }}>{u.first_name} {u.last_name}</span>
|
||||
{u.date_of_birth && (
|
||||
<span style={{ fontSize: 12, color: 'var(--text-tertiary)' }}>
|
||||
{u.date_of_birth.slice(0, 10)}
|
||||
</span>
|
||||
)}
|
||||
<button
|
||||
onClick={e => handleRemoveMinor(e, u.id)}
|
||||
style={{ background: 'none', border: 'none', cursor: 'pointer', color: 'var(--error)', fontSize: 18, lineHeight: 1, padding: '0 2px' }}
|
||||
aria-label="Remove"
|
||||
>×</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Add minor from players group */}
|
||||
<div className="text-sm font-medium" style={{ color: 'var(--text-secondary)', marginBottom: 8 }}>
|
||||
Add Child
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 8 }}>
|
||||
<select
|
||||
className="input"
|
||||
style={{ flex: 1 }}
|
||||
value={selectedMinorId}
|
||||
onChange={e => setSelectedMinorId(e.target.value)}
|
||||
>
|
||||
<option value="">— Select a player —</option>
|
||||
{availableMinors.map(u => (
|
||||
<option key={u.id} value={u.id}>
|
||||
{u.first_name} {u.last_name}{u.date_of_birth ? ` (${u.date_of_birth.slice(0, 10)})` : ''}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={handleAddMinor}
|
||||
disabled={addingMinor || !selectedMinorId}
|
||||
style={{ whiteSpace: 'nowrap' }}
|
||||
>
|
||||
{addingMinor ? 'Adding…' : 'Add'}
|
||||
</button>
|
||||
</div>
|
||||
{availableMinors.length === 0 && myMinors.length === 0 && (
|
||||
<p className="text-sm" style={{ color: 'var(--text-tertiary)', marginTop: 8 }}>
|
||||
No minor players available to link.
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Form section label */}
|
||||
<div className="text-sm font-medium" style={{ color: 'var(--text-secondary)', marginBottom: 10 }}>
|
||||
{editingAlias
|
||||
? `Editing: ${editingAlias.first_name} ${editingAlias.last_name}`
|
||||
: 'Add Child'}
|
||||
</div>
|
||||
|
||||
{/* Form */}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 10 }}>
|
||||
<div>
|
||||
{lbl('First Name', true)}
|
||||
<input className="input" value={form.firstName} onChange={set('firstName')}
|
||||
autoComplete="off" autoCapitalize="words" />
|
||||
</div>
|
||||
<div>
|
||||
{lbl('Last Name', true)}
|
||||
<input className="input" value={form.lastName} onChange={set('lastName')}
|
||||
autoComplete="off" autoCapitalize="words" />
|
||||
</div>
|
||||
<div>
|
||||
{lbl('Date of Birth')}
|
||||
<input className="input" placeholder="YYYY-MM-DD" value={form.dob} onChange={set('dob')}
|
||||
autoComplete="off" />
|
||||
</div>
|
||||
<div>
|
||||
{lbl('Phone')}
|
||||
<input className="input" type="tel" value={form.phone} onChange={set('phone')}
|
||||
autoComplete="off" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
{lbl('Email (optional)')}
|
||||
<input className="input" type="email" value={form.email} onChange={set('email')}
|
||||
autoComplete="off" />
|
||||
</div>
|
||||
<div>
|
||||
{lbl('Avatar (optional)')}
|
||||
<input type="file" accept="image/*"
|
||||
onChange={e => setAvatarFile(e.target.files?.[0] || null)} />
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end', marginTop: 4 }}>
|
||||
{editingAlias && (
|
||||
<button className="btn btn-secondary" onClick={resetForm}>Cancel Edit</button>
|
||||
{/* ── Guardian Only: alias form ── */}
|
||||
{!isMixedAge && (
|
||||
<>
|
||||
{/* Existing aliases list */}
|
||||
{aliases.length > 0 && (
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<div className="text-sm font-medium" style={{ color: 'var(--text-secondary)', marginBottom: 8 }}>
|
||||
Your Children — click to edit
|
||||
</div>
|
||||
<div style={{ border: '1px solid var(--border)', borderRadius: 'var(--radius)', overflow: 'hidden' }}>
|
||||
{aliases.map((a, i) => (
|
||||
<div
|
||||
key={a.id}
|
||||
onClick={() => handleSelectAlias(a)}
|
||||
style={{
|
||||
display: 'flex', alignItems: 'center', gap: 10,
|
||||
padding: '9px 12px', cursor: 'pointer',
|
||||
borderBottom: i < aliases.length - 1 ? '1px solid var(--border)' : 'none',
|
||||
background: editingAlias?.id === a.id ? 'var(--primary-light)' : 'transparent',
|
||||
}}
|
||||
>
|
||||
<span style={{ flex: 1, fontSize: 14, fontWeight: editingAlias?.id === a.id ? 600 : 400 }}>
|
||||
{a.first_name} {a.last_name}
|
||||
</span>
|
||||
{a.date_of_birth && (
|
||||
<span style={{ fontSize: 12, color: 'var(--text-tertiary)' }}>
|
||||
{a.date_of_birth.slice(0, 10)}
|
||||
</span>
|
||||
)}
|
||||
<button
|
||||
onClick={e => handleDeleteAlias(e, a.id)}
|
||||
style={{ background: 'none', border: 'none', cursor: 'pointer', color: 'var(--error)', fontSize: 18, lineHeight: 1, padding: '0 2px' }}
|
||||
aria-label="Remove"
|
||||
>×</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<button className="btn btn-primary" onClick={handleSave} disabled={saving}>
|
||||
{saving ? 'Saving…' : editingAlias ? 'Update Alias' : 'Add Alias'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Form section label */}
|
||||
<div className="text-sm font-medium" style={{ color: 'var(--text-secondary)', marginBottom: 10 }}>
|
||||
{editingAlias
|
||||
? `Editing: ${editingAlias.first_name} ${editingAlias.last_name}`
|
||||
: 'Add Child'}
|
||||
</div>
|
||||
|
||||
{/* Form */}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 10 }}>
|
||||
<div>
|
||||
{lbl('First Name', true)}
|
||||
<input className="input" value={form.firstName} onChange={set('firstName')}
|
||||
autoComplete="off" autoCapitalize="words" />
|
||||
</div>
|
||||
<div>
|
||||
{lbl('Last Name', true)}
|
||||
<input className="input" value={form.lastName} onChange={set('lastName')}
|
||||
autoComplete="off" autoCapitalize="words" />
|
||||
</div>
|
||||
<div>
|
||||
{lbl('Date of Birth')}
|
||||
<input className="input" placeholder="YYYY-MM-DD" value={form.dob} onChange={set('dob')}
|
||||
autoComplete="off" />
|
||||
</div>
|
||||
<div>
|
||||
{lbl('Phone')}
|
||||
<input className="input" type="tel" value={form.phone} onChange={set('phone')}
|
||||
autoComplete="off" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
{lbl('Email (optional)')}
|
||||
<input className="input" type="email" value={form.email} onChange={set('email')}
|
||||
autoComplete="off" />
|
||||
</div>
|
||||
<div>
|
||||
{lbl('Avatar (optional)')}
|
||||
<input type="file" accept="image/*"
|
||||
onChange={e => setAvatarFile(e.target.files?.[0] || null)} />
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end', marginTop: 4 }}>
|
||||
{editingAlias && (
|
||||
<button className="btn btn-secondary" onClick={resetForm}>Cancel Edit</button>
|
||||
)}
|
||||
<button className="btn btn-primary" onClick={handleSaveAlias} disabled={saving}>
|
||||
{saving ? 'Saving…' : editingAlias ? 'Update Alias' : 'Add Alias'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user