refactor(client): delete the UO client half (phase 3, slice 3)
35 files and 5,332 lines out — twelve public pages, seven admin views, two player views, eight components, the two `data/` leaves and the three `lib/` ones, plus the two tests that came with them. §2.7.1's estimate of 51 files / ~3,700 lines was measured differently and is corrected in the docs PR. The seams core keeps, each smaller than what it replaced: Nine rows leave the public header and six leave the admin sidebar, and both lists are now free of `feature` gates and of `IconShard`. `moduleTitle` already handled a module page's heading, so the six TITLES entries and the `/admin/characters` branch of `sectionTitle` simply go. `/player` had `PlayerCharacters` as its index — a UO page — and rather than name a replacement or invent a landing screen it now resolves to the first row of the portal nav this viewer can reach (`firstDestinationFor`, beside `allowedPathsFor` and reading the BASE nav for the same reason: an override is presentation and where everybody lands is behaviour). With the module installed that is still Characters, so a player's first screen after signing in does not change. Deliberately generic and deliberately not in the portal layout — the admin index is the same question with a hardcoded answer, and if the two logged-in areas ever become one this is what serves both. `game_account_signup` goes with the rest of core's UO prose: the mode list, the derived public flag, the validation and a Site Settings field whose help text named Bridge.cfg. The row itself is untouched and module-uo reads it through ctx.settings — the data stays, the semantics move. KNOWN BREAK, accepted by the org lead: the shipped Android app reads `gameAccountSignup` off `/public/settings` (PublicDto.kt:80). The field has a `= false` default so nothing crashes; the app silently stops offering game-account creation until it reads the module's `/public/shard/features` instead. Out of scope here, recorded in the Android plan, and it lands well before this workstream's cutover reaches `main`. 620 server + 161 client tests. Manifest 158 public + 2 internal, unchanged; routes.guards unchanged. The OpenAPI spec loses exactly one property, and only because it was hand-written in swagger.js — regeneration alone would have left the spec documenting a field core no longer returns. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,10 +1,11 @@
|
||||
import { useMemo } from 'react'
|
||||
import { NavLink, Outlet, useNavigate, useLocation } from 'react-router-dom'
|
||||
import { NavLink, Navigate, Outlet, useNavigate, useLocation } from 'react-router-dom'
|
||||
import MoonDot from '../../components/MoonDot.jsx'
|
||||
import BrandLogo from '../../components/BrandLogo.jsx'
|
||||
import { useAuth } from '../../contexts/AuthContext.jsx'
|
||||
import { useSite } from '../../contexts/SiteContext.jsx'
|
||||
import { applyNavOverrides } from '../../lib/navOverrides.js'
|
||||
import { firstDestinationFor } from '../../lib/adminNav.js'
|
||||
import { useNavOverrides } from '../../lib/useNavOverrides.js'
|
||||
import { withModuleNav } from '../../modules/nav.js'
|
||||
import { useFeatureGate } from '../../modules/features.jsx'
|
||||
@@ -32,29 +33,37 @@ function Icon({ children, size = 16 }) {
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
const IconUser = () => <Icon><circle cx="12" cy="8" r="4" /><path d="M4 21a8 8 0 0 1 16 0" /></Icon>
|
||||
const IconGear = () => <Icon><circle cx="12" cy="12" r="3" /><path d="M12 2v3M12 19v3M2 12h3M19 12h3M4.9 4.9l2.1 2.1M17 17l2.1 2.1M19.1 4.9L17 7M7 17l-2.1 2.1" /></Icon>
|
||||
const IconShield = () => <Icon><path d="M12 3l7 3v5c0 5-3.5 8-7 10-3.5-2-7-5-7-10V6z" /><path d="M9 12l2 2 4-4" /></Icon>
|
||||
|
||||
// Exported because Admin -> Navigation edits this list. It stays declared here;
|
||||
// the editor may only relabel, reorder and hide what it finds (§7). No CORE row
|
||||
// carries a gate — every player sees all three — but an installed module's rows
|
||||
// join this list before the merge and may carry a `feature`, so the filter after
|
||||
// it is not dead code.
|
||||
// carries a gate — every player sees both — but an installed module's rows join
|
||||
// this list before the merge and may carry a `feature`, so the filter after it
|
||||
// is not dead code.
|
||||
//
|
||||
// "Characters" was the first row and left with the client half in slice 3; the
|
||||
// UO module registers it again at `/player/uo/characters`, in this position,
|
||||
// with `order: 0`.
|
||||
export const NAV = [
|
||||
{ to: '/player', label: 'Characters', end: true, icon: IconUser },
|
||||
{ to: '/account/appeals', label: 'Appeals', icon: IconShield },
|
||||
{ to: '/account', label: 'Account', end: true, icon: IconGear },
|
||||
]
|
||||
|
||||
// The sticky content header mirrors the active page. Character sheets live under
|
||||
// /player/char/:serial and keep their own in-page back link.
|
||||
// The sticky content header mirrors the active page. A module's pages are not
|
||||
// here and cannot be — core does not know what they are called — so they title
|
||||
// from their own nav row, the same rule AdminLayout's `moduleTitle` follows.
|
||||
const TITLES = {
|
||||
'/player': 'Characters',
|
||||
'/account': 'Account',
|
||||
'/account/appeals': 'Appeals',
|
||||
}
|
||||
|
||||
function moduleTitle(baseNav, pathname) {
|
||||
return baseNav
|
||||
.filter((i) => i.moduleId && (pathname === i.to || pathname.startsWith(`${i.to}/`)))
|
||||
.sort((a, b) => b.to.length - a.to.length)[0]?.label
|
||||
}
|
||||
|
||||
const navBtnBase = {
|
||||
textAlign: 'left',
|
||||
borderRadius: 8,
|
||||
@@ -80,9 +89,7 @@ export default function PlayerPortalLayout() {
|
||||
)
|
||||
const navigate = useNavigate()
|
||||
const location = useLocation()
|
||||
const title =
|
||||
TITLES[location.pathname] ||
|
||||
(location.pathname.startsWith('/player/char/') ? 'Character' : 'Player Portal')
|
||||
const title = TITLES[location.pathname] || moduleTitle(baseNav, location.pathname) || 'Player Portal'
|
||||
|
||||
async function signOut() {
|
||||
await logout()
|
||||
@@ -182,3 +189,28 @@ export default function PlayerPortalLayout() {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* What `/player` renders.
|
||||
*
|
||||
* It used to be `PlayerCharacters`, a UO page, which left the portal with no
|
||||
* index at all when the client half was extracted (slice 3). Rather than pick a
|
||||
* fixed destination or invent a core landing page, the index resolves to the
|
||||
* first row of the portal nav this viewer can actually reach — so with the UO
|
||||
* module installed a player still arrives at their characters, exactly as
|
||||
* before, and with nothing installed they arrive at Account.
|
||||
*
|
||||
* Resolved from the BASE nav, before overrides: where everybody lands is
|
||||
* behaviour, and an override is presentation (`firstDestinationFor`). `replace`
|
||||
* so the back button leaves the portal rather than bouncing off this redirect.
|
||||
*
|
||||
* The same question exists one area over — the admin index is a hardcoded
|
||||
* Dashboard — and if the two logged-in areas ever become one, this is the shape
|
||||
* that answers for both. Nothing here assumes a portal separate from admin.
|
||||
*/
|
||||
export function PlayerIndex() {
|
||||
const { user } = useAuth()
|
||||
const baseNav = useMemo(() => withModuleNav(NAV, 'player'), [])
|
||||
const to = firstDestinationFor(baseNav, user?.role, '/account')
|
||||
return <Navigate to={to} replace />
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user