v0.12.47 Add Child alias update
This commit is contained in:
@@ -23,7 +23,7 @@ export default function ProfileModal({ onClose }) {
|
||||
const [newPw, setNewPw] = useState('');
|
||||
const [confirmPw, setConfirmPw] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [tab, setTab] = useState('profile'); // 'profile' | 'password' | 'notifications' | 'appearance' | 'add-child'
|
||||
const [tab, setTab] = useState('profile'); // 'profile' | 'password' | 'notifications' | 'appearance'
|
||||
const [pushTesting, setPushTesting] = useState(false);
|
||||
const [pushResult, setPushResult] = useState(null);
|
||||
const [notifPermission, setNotifPermission] = useState(
|
||||
@@ -36,20 +36,8 @@ export default function ProfileModal({ onClose }) {
|
||||
const [hideAdminTag, setHideAdminTag] = useState(!!user?.hide_admin_tag);
|
||||
const [allowDm, setAllowDm] = useState(user?.allow_dm !== 0);
|
||||
|
||||
// Minor age protection state
|
||||
const [loginType, setLoginType] = useState('all_ages');
|
||||
const [guardiansGroupId,setGuardiansGroupId] = useState(null);
|
||||
const [showAddChild, setShowAddChild] = useState(false);
|
||||
const [aliases, setAliases] = useState([]);
|
||||
// Add Child form state
|
||||
const [childList, setChildList] = useState([]); // pending aliases to add
|
||||
const [childForm, setChildForm] = useState({ firstName:'', lastName:'', email:'', dob:'', phone:'' });
|
||||
const [childFormAvatar, setChildFormAvatar] = useState(null);
|
||||
const [childSaving, setChildSaving] = useState(false);
|
||||
// Mixed Age: minor user search
|
||||
const [minorSearch, setMinorSearch] = useState('');
|
||||
const [minorResults, setMinorResults] = useState([]);
|
||||
const [selectedMinor, setSelectedMinor] = useState(null);
|
||||
// Minor age protection — DOB/phone display only
|
||||
const [loginType, setLoginType] = useState('all_ages');
|
||||
|
||||
const savedScale = parseFloat(localStorage.getItem(LS_FONT_KEY));
|
||||
const [fontScale, setFontScale] = useState(
|
||||
@@ -62,32 +50,13 @@ export default function ProfileModal({ onClose }) {
|
||||
return () => window.removeEventListener('resize', onResize);
|
||||
}, []);
|
||||
|
||||
// Load login type + check if user is in guardians group
|
||||
// Load login type for DOB/phone field visibility
|
||||
useEffect(() => {
|
||||
Promise.all([api.getSettings(), api.getMyUserGroups()]).then(([{ settings: s }, { userGroups }]) => {
|
||||
const lt = s.feature_login_type || 'all_ages';
|
||||
const gid = parseInt(s.feature_guardians_group_id);
|
||||
setLoginType(lt);
|
||||
setGuardiansGroupId(gid || null);
|
||||
if (lt === 'guardian_only') {
|
||||
// In guardian_only mode all authenticated users are guardians — always show Add Child
|
||||
setShowAddChild(true);
|
||||
} else if (lt === 'mixed_age' && gid) {
|
||||
const inGroup = (userGroups || []).some(g => g.id === gid);
|
||||
setShowAddChild(inGroup);
|
||||
}
|
||||
api.getSettings().then(({ settings: s }) => {
|
||||
setLoginType(s.feature_login_type || 'all_ages');
|
||||
}).catch(() => {});
|
||||
api.getAliases().then(({ aliases }) => setAliases(aliases || [])).catch(() => {});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (loginType === 'mixed_age' && minorSearch.length >= 1) {
|
||||
api.searchMinorUsers(minorSearch).then(({ users }) => setMinorResults(users || [])).catch(() => {});
|
||||
} else {
|
||||
setMinorResults([]);
|
||||
}
|
||||
}, [minorSearch, loginType]);
|
||||
|
||||
const applyFontScale = (val) => {
|
||||
setFontScale(val);
|
||||
document.documentElement.style.setProperty('--font-scale', val);
|
||||
@@ -109,46 +78,6 @@ export default function ProfileModal({ onClose }) {
|
||||
}
|
||||
};
|
||||
|
||||
const handleSaveChildren = async () => {
|
||||
if (childList.length === 0) return;
|
||||
setChildSaving(true);
|
||||
try {
|
||||
if (loginType === 'mixed_age') {
|
||||
// Link each selected minor
|
||||
for (const minor of childList) {
|
||||
await api.linkMinor(minor.id);
|
||||
}
|
||||
toast('Guardian link request sent — awaiting manager approval', 'success');
|
||||
} else {
|
||||
// Create aliases
|
||||
for (const child of childList) {
|
||||
const { alias } = await api.createAlias({ firstName: child.firstName, lastName: child.lastName, email: child.email, dateOfBirth: child.dob, phone: child.phone });
|
||||
if (child.avatarFile) {
|
||||
await api.uploadAliasAvatar(alias.id, child.avatarFile);
|
||||
}
|
||||
}
|
||||
toast('Children saved', 'success');
|
||||
const { aliases: fresh } = await api.getAliases();
|
||||
setAliases(fresh || []);
|
||||
}
|
||||
setChildList([]);
|
||||
setChildForm({ firstName:'', lastName:'', email:'', dob:'', phone:'' });
|
||||
setSelectedMinor(null);
|
||||
} catch (e) {
|
||||
toast(e.message, 'error');
|
||||
} finally {
|
||||
setChildSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveAlias = async (aliasId) => {
|
||||
try {
|
||||
await api.deleteAlias(aliasId);
|
||||
setAliases(prev => prev.filter(a => a.id !== aliasId));
|
||||
toast('Child removed', 'success');
|
||||
} catch (e) { toast(e.message, 'error'); }
|
||||
};
|
||||
|
||||
const handleAvatarUpload = async (e) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
@@ -219,7 +148,6 @@ export default function ProfileModal({ onClose }) {
|
||||
<option value="password">Change Password</option>
|
||||
<option value="notifications">Notifications</option>
|
||||
<option value="appearance">Appearance</option>
|
||||
{showAddChild && <option value="add-child">Add Child</option>}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@@ -447,122 +375,6 @@ export default function ProfileModal({ onClose }) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{tab === 'add-child' && (
|
||||
<div className="flex-col gap-3">
|
||||
{/* Existing saved aliases */}
|
||||
{aliases.length > 0 && (
|
||||
<div>
|
||||
<div className="text-sm font-medium" style={{ color: 'var(--text-secondary)', marginBottom: 8 }}>Saved Children</div>
|
||||
<div style={{ border: '1px solid var(--border)', borderRadius: 'var(--radius)', overflow: 'hidden', marginBottom: 12 }}>
|
||||
{aliases.map((a, i) => (
|
||||
<div key={a.id} style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '9px 12px', borderBottom: i < aliases.length - 1 ? '1px solid var(--border)' : 'none' }}>
|
||||
<span style={{ flex: 1, fontSize: 14 }}>{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={() => handleRemoveAlias(a.id)} style={{ background: 'none', border: 'none', cursor: 'pointer', color: 'var(--error)', fontSize: 16, lineHeight: 1 }}>×</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{loginType === 'guardian_only' ? (
|
||||
<>
|
||||
<div className="text-sm font-medium" style={{ color: 'var(--text-secondary)' }}>Add a Child</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: isMobile ? '1fr' : '1fr 1fr', gap: 10 }}>
|
||||
<div className="flex-col gap-1">
|
||||
<label className="text-sm" style={{ color: 'var(--text-secondary)' }}>First Name *</label>
|
||||
<input className="input" value={childForm.firstName} onChange={e => setChildForm(p=>({...p,firstName:e.target.value}))} autoComplete="off" autoCapitalize="words" />
|
||||
</div>
|
||||
<div className="flex-col gap-1">
|
||||
<label className="text-sm" style={{ color: 'var(--text-secondary)' }}>Last Name *</label>
|
||||
<input className="input" value={childForm.lastName} onChange={e => setChildForm(p=>({...p,lastName:e.target.value}))} autoComplete="off" autoCapitalize="words" />
|
||||
</div>
|
||||
<div className="flex-col gap-1">
|
||||
<label className="text-sm" style={{ color: 'var(--text-secondary)' }}>Date of Birth</label>
|
||||
<input className="input" placeholder="YYYY-MM-DD" value={childForm.dob} onChange={e => setChildForm(p=>({...p,dob:e.target.value}))} autoComplete="off" />
|
||||
</div>
|
||||
<div className="flex-col gap-1">
|
||||
<label className="text-sm" style={{ color: 'var(--text-secondary)' }}>Phone</label>
|
||||
<input className="input" type="tel" value={childForm.phone} onChange={e => setChildForm(p=>({...p,phone:e.target.value}))} autoComplete="off" />
|
||||
</div>
|
||||
<div className="flex-col gap-1" style={{ gridColumn: isMobile ? '1' : '1 / -1' }}>
|
||||
<label className="text-sm" style={{ color: 'var(--text-secondary)' }}>Email (optional)</label>
|
||||
<input className="input" type="email" value={childForm.email} onChange={e => setChildForm(p=>({...p,email:e.target.value}))} autoComplete="off" />
|
||||
</div>
|
||||
<div className="flex-col gap-1" style={{ gridColumn: isMobile ? '1' : '1 / -1' }}>
|
||||
<label className="text-sm" style={{ color: 'var(--text-secondary)' }}>Avatar (optional)</label>
|
||||
<input type="file" accept="image/*" onChange={e => setChildForm(p=>({...p,avatarFile:e.target.files?.[0]||null}))} />
|
||||
</div>
|
||||
</div>
|
||||
<button className="btn btn-secondary btn-sm" style={{ alignSelf: 'flex-start' }}
|
||||
onClick={() => {
|
||||
if (!childForm.firstName.trim() || !childForm.lastName.trim()) return toast('First and last name required','error');
|
||||
setChildList(prev => [...prev, { ...childForm }]);
|
||||
setChildForm({ firstName:'', lastName:'', email:'', dob:'', phone:'', avatarFile:null });
|
||||
}}>
|
||||
+ Add
|
||||
</button>
|
||||
{childList.length > 0 && (
|
||||
<div style={{ border: '1px solid var(--border)', borderRadius: 'var(--radius)', overflow: 'hidden' }}>
|
||||
{childList.map((c, i) => (
|
||||
<div key={i} style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '9px 12px', borderBottom: i < childList.length - 1 ? '1px solid var(--border)' : 'none' }}>
|
||||
<span style={{ flex: 1, fontSize: 14 }}>{c.firstName} {c.lastName}</span>
|
||||
<span style={{ fontSize: 12, color: 'var(--text-tertiary)', fontStyle: 'italic' }}>Pending save</span>
|
||||
<button onClick={() => setChildList(prev => prev.filter((_,j) => j !== i))} style={{ background: 'none', border: 'none', cursor: 'pointer', color: 'var(--error)', fontSize: 16 }}>×</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : loginType === 'mixed_age' ? (
|
||||
<>
|
||||
<div className="text-sm font-medium" style={{ color: 'var(--text-secondary)' }}>Link a Child Account</div>
|
||||
<p style={{ fontSize: 12, color: 'var(--text-tertiary)', margin: 0 }}>Search for a minor user account to link to your guardian profile. The link requires manager approval.</p>
|
||||
<input className="input" placeholder="Search minor users..." value={minorSearch} onChange={e => setMinorSearch(e.target.value)} autoComplete="off" />
|
||||
{minorResults.length > 0 && (
|
||||
<div style={{ border: '1px solid var(--border)', borderRadius: 'var(--radius)', maxHeight: 160, overflowY: 'auto' }}>
|
||||
{minorResults.map(u => (
|
||||
<div key={u.id} style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '9px 12px', borderBottom: '1px solid var(--border)', cursor: 'pointer' }}
|
||||
onClick={() => { setSelectedMinor(u); setMinorSearch(''); setMinorResults([]); }}>
|
||||
<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>}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{selectedMinor && (
|
||||
<div style={{ border: '1px solid var(--border)', borderRadius: 'var(--radius)', padding: '10px 14px' }}>
|
||||
<div style={{ fontSize: 14, fontWeight: 600 }}>{selectedMinor.first_name} {selectedMinor.last_name}</div>
|
||||
{selectedMinor.date_of_birth && <div style={{ fontSize: 12, color: 'var(--text-tertiary)' }}>{selectedMinor.date_of_birth.slice(0,10)}</div>}
|
||||
<div style={{ display: 'flex', gap: 8, marginTop: 8 }}>
|
||||
<button className="btn btn-secondary btn-sm"
|
||||
onClick={() => { setChildList(prev => [...prev, selectedMinor]); setSelectedMinor(null); }}>
|
||||
+ Add to list
|
||||
</button>
|
||||
<button className="btn btn-sm" style={{ background: 'none', border: '1px solid var(--border)' }} onClick={() => setSelectedMinor(null)}>Clear</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{childList.length > 0 && (
|
||||
<div style={{ border: '1px solid var(--border)', borderRadius: 'var(--radius)', overflow: 'hidden' }}>
|
||||
{childList.map((c, i) => (
|
||||
<div key={c.id || i} style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '9px 12px', borderBottom: i < childList.length - 1 ? '1px solid var(--border)' : 'none' }}>
|
||||
<span style={{ flex: 1, fontSize: 14 }}>{c.first_name || c.firstName} {c.last_name || c.lastName}</span>
|
||||
<span style={{ fontSize: 12, color: 'var(--warning)', fontStyle: 'italic' }}>Pending approval</span>
|
||||
<button onClick={() => setChildList(prev => prev.filter((_,j) => j !== i))} style={{ background: 'none', border: 'none', cursor: 'pointer', color: 'var(--error)', fontSize: 16 }}>×</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : null}
|
||||
|
||||
<button className="btn btn-primary" onClick={handleSaveChildren} disabled={childSaving || childList.length === 0}>
|
||||
{childSaving ? 'Saving…' : 'Save'}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{tab === 'appearance' && (
|
||||
<div className="flex-col gap-3">
|
||||
<div className="flex-col gap-2">
|
||||
|
||||
Reference in New Issue
Block a user