/* global React, Icon, Origin, SyncChip, Meter, CB, RoleChip, JobChip, Portrait, Tag, Empty, Seg, Input, useRoute */ // ============================================================ // Screens 8-9: Free Company overview + Settings / sync status // ============================================================ const { useState: useS89, useMemo: useM89 } = React; // Fake-but-plausible FC roster builder function buildFCRoster() { const FIRST = ['Aerith','Korr','Liliane','Yumi','Veska','Olric','Thalia','Imre','Mira','Brennan','Soren','Kael','Nyx','Riven','Zara','Tobin','Marin','Cinder','Vex','Halen','Pia','Jorah','Sable','Ravi','Bram','Lys','Orin','Sage','Tess','Quinn','Hale','Mara','Ennis','Roe','Sienna','Calla','Drake','Petra','Lior','Nova','Faye','Aldric','Bree','Caelan','Demi','Eira','Finn','Gretta','Hilde','Iva']; const LAST = ['Nightsong','Brassbottom','d\'Aubreville','Shiroyama','Voidsong','Stonebreak','Greycloak','Kalvain','Sundwalker','Holt','Caelduin','Vesper','Brightmoor','Kestrel','Wynter','Halewind','Frostbloom','Rookward','Stardrift','Embergale','Aldermoor','Thornwell','Pellgrove','Maris','Crowmere','Silverkin','Faewright','Driftwood','Halcyon','Mosswood','Lockewood','Holloway','Brenholme','Caradine','Mirewood','Penbrook','Lyonette','Vossfeld','Quill','Riverford','Sablewood','Thrushe','Vellichor','Whitlock','Yseult','Zantor','Abernathy','Beauchamp','Cervantes','Dunmoor']; const JOBS = ['PLD','WAR','DRK','GNB','WHM','SCH','AST','SGE','MNK','DRG','NIN','SAM','RPR','VPR','BRD','MCH','DNC','BLM','SMN','RDM','PCT']; const ranks = ['Member','Member','Member','Member','Member','Member','Sergeant','Sergeant','Lieutenant','Master']; const out = []; let seed = 7; const rand = () => { seed = (seed * 9301 + 49297) % 233280; return seed / 233280; }; for (let i = 0; i < 72; i++) { const f = FIRST[Math.floor(rand() * FIRST.length)]; const l = LAST[Math.floor(rand() * LAST.length)]; const mainJob = JOBS[Math.floor(rand() * JOBS.length)]; const capCount = Math.floor(rand() * 14) + 1; const ilvl = Math.floor(680 + rand() * 60); const mounts = Math.floor(40 + rand() * 270); const linked = rand() > 0.42; const priv = !linked && rand() > 0.55; const lastOn = Math.floor(rand() * 14); const rank = ranks[Math.floor(rand() * ranks.length)]; out.push({ name: `${f} ${l}`, mainJob, capCount, ilvl, mounts, linked, priv, lastOn, rank, }); } return out; } const FC_ROSTER = buildFCRoster(); function ScreenFC() { const [sortKey, setSortKey] = useS89('lastOn'); const [filter, setFilter] = useS89('all'); const [q, setQ] = useS89(''); const filtered = useM89(() => { let r = FC_ROSTER.filter(m => { if (filter === 'linked' && !m.linked) return false; if (filter === 'unlinked' && m.linked) return false; if (q && !m.name.toLowerCase().includes(q.toLowerCase())) return false; return true; }); if (sortKey === 'name') r = [...r].sort((a, b) => a.name.localeCompare(b.name)); else if (sortKey === 'ilvl') r = [...r].sort((a, b) => b.ilvl - a.ilvl); else if (sortKey === 'mounts') r = [...r].sort((a, b) => b.mounts - a.mounts); else if (sortKey === 'capCount') r = [...r].sort((a, b) => b.capCount - a.capCount); else r = [...r].sort((a, b) => a.lastOn - b.lastOn); return r; }, [sortKey, filter, q]); const linkedCount = FC_ROSTER.filter(m => m.linked).length; const avgIlvl = Math.round(FC_ROSTER.reduce((a, m) => a + m.ilvl, 0) / FC_ROSTER.length); const totalMounts = FC_ROSTER.reduce((a, m) => a + (m.linked ? m.mounts : 0), 0); return (
«WoH» Aether · Sargatanas

Whispers of Hydaelyn

Founded Eorzea time 8/12 · Rank 30 · Master Aerith Nightsong
Members
{FC_ROSTER.length}/100
{linkedCount} bound ·
Avg item level
{avgIlvl}
Mounts collected
{totalMounts.toLocaleString()}
across linked members ·
Avg weekly logins
52%
last 4 weeks ·
} placeholder="Search by name…" value={q} onChange={e => setQ(e.target.value)} />
NameCapiLvlMountsLast on {filtered.map((m, i) => ( ))}
Rank Main Source
{m.name}
{!m.linked && (
{m.priv ? 'Private profile · limited data' : 'Not on Aether Hub yet'}
)}
{m.rank} {m.linked ? `${m.capCount}/22` : } {m.linked ? m.ilvl : } {m.linked ? {m.mounts} /312 : m.priv ? private : } {m.lastOn === 0 ? 'today' : `${m.lastOn}d ago`} {m.linked ? : m.priv ? : roster only}
Roster is auto-imported from the Lodestone FC page every 24h. Collection detail only fills in for members who've bound their character — invite them so this table fills out.
); } function SortableTh({ k, k2, onSort, children, cls }) { const active = k === k2; return ( ); } // -------------------- 9. Settings / Sync -------------------- function ScreenSettings() { const sections = [ { id: 'account', label: 'Account' }, { id: 'chars', label: 'Characters' }, { id: 'privacy', label: 'Privacy & data' }, { id: 'discord', label: 'Discord' }, { id: 'danger', label: 'Danger zone' }, ]; const [active, setActive] = useS89('chars'); return (

Settings

Manage your account, bound characters, and what data we sync.
{/* Account */} {active === 'account' && (

Account

JK
jakob.kohl
Signed in with Discord · jakob#4382
Email notifications
Raid night reminders, prog summaries, FC weekly digest.
{}} />
)} {/* Characters & sync */} {active === 'chars' && (

Bound characters

)} {/* Privacy & data */} {active === 'privacy' && (

Privacy & data

What we read and don't read

Aether Hub only reads public data — your Lodestone character page, FFLogs reports you've made public, and a community-maintained item catalog. We never read your game client, never use addons, and never ask for your Square Enix password. We can't see anything that isn't already public.

Sync sources
Toggle which public sources we read from.
{[ { name: 'Lodestone', sub: 'Character page, FC roster', on: true }, { name: 'FFLogs', sub: 'Public reports for raid nights', on: true }, { name: 'Community item API', sub: 'Catalog of items & icons', on: true }, { name: 'Discord activity', sub: 'Only your linked server', on: false }, ].map(s => (
{s.name}
{s.sub}
))}
Export your data
Download everything we've stored about you, as JSON.
)} {/* Discord */} {active === 'discord' && (

Discord

WoH
Whispers of Hydaelyn
148 members · Aether Hub bot active online
Bot commands
What members can run in your server.
{[ { cmd: '/loot ', what: 'Log a loot drop into the static loot table.' }, { cmd: '/raid log ', what: 'Import an FFLogs report into the current raid night.' }, { cmd: '/static', what: 'Post the next raid night and pull RSVPs.' }, { cmd: '/relic ', what: 'DM your current relic progress.' }, { cmd: '/fc weekly', what: 'Post the FC weekly digest in this channel.' }, ].map(c => (
{c.cmd}
{c.what}
))}
)} {/* Danger zone */} {active === 'danger' && (

Danger zone

Unbind a character
Removes the character from your account and stops all syncing.
Delete account
Permanently delete your account and all associated data. This can't be undone.
)}
); } function CharSyncCard({ name, world, state, last, next, main, warn, err }) { return (
{name} {main && Main}
{world}
{next && Next sync {next}}
{warn && (
{warn}
)} {err && (
{err}
)}
); } function Toggle({ on: defaultOn }) { const [on, setOn] = useS89(defaultOn); return ( ); } Object.assign(window, { ScreenFC, ScreenSettings });