chore(ts-migration): migrate the app shell, routing and the last non-agent pages
Phase 10: `App`, `main`, `index.html`, and the four remaining `src/pages` files. ROUTING IS PROVEN UNCHANGED, not assumed. The 49 `<Route>` elements were fingerprinted before the rename and compared after: byte-identical, all 44 paths, all 13 `<Navigate>` redirects, the same nesting under `ProtectedRoute` and `AdminRoute`, and the same provider order - AuthProvider, then QueryClientProvider, then Router, with ScrollToTop and AuthenticatedApp inside and both toasters as siblings. There is no lazy loading to preserve; there never was any. Two coordinated edits the rename forced. `main` imported `@/App.jsx` by explicit extension, which stops resolving the moment `App` is `.tsx`; it is now extensionless `@/App`. `index.html` pointed its module script at `/src/main.jsx`; it points at `/src/main.tsx`. Both are required, and missing either would have been a blank page rather than a type error. ONE GENUINE SEMANTIC DIFFERENCE, INVESTIGATED AND ACCEPTED. esbuild elides unused imports under the TypeScript loader but keeps them under the JavaScript one, so `App` drops thirteen imports: `Layout`, `Overview`, `Positions`, `Candidates`, `HiredHistory`, `TalentPool`, `UserTracking`, `Analytics`, `Profile`, `WorkerProfile`, `KrowIdentity`, `Owliver` and `DesignSystem`. All thirteen are dead - each has zero JSX uses in `App`, because every route that once rendered them now `<Navigate>`s to an `/admin/*` equivalent. Before accepting it I checked that none of the thirteen modules can do anything when evaluated: no bare side-effect imports, no top-level calls, and every top-level binding a literal or a function declaration. The consequence is 942 fewer bytes in the index chunk and thirteen modules no longer evaluated at startup. Nothing observable changes, and the routes those pages are reached through are unaffected - they are reached through the `/admin` tree, which is untouched. That is the first time in this migration the production bundle has changed for a reason other than a comment, so it is recorded here rather than left to be noticed later. `CreatePosition` needed the only real typing. `vetting_criteria` is a `jsonb` column holding the five weighting percentages the page edits, typed `unknown` by the registry, and both the total and the three render sites read through it. `onDone` is called with the created id, so it takes arguments - the generator had classed it as zero-arg, and that rule is now narrowed to `onClose` alone. The generator's entity-import rule was narrowed first, as instructed: it now counts a type as used only when it appears in a type position inside a generated interface, rather than anywhere in the file text. That is what produced four unused-import lint errors in the previous batch. Verified: tsc 21 -> 20, set-difference showing one removed and none added; zero errors in any of the six files; all four pages emit byte-identical JavaScript; route fingerprint identical; npm test 1684/1691 with the same seven failures; Owliver baseline 59/59; lint 0 errors; no deferred agent-region file touched; baseline artifacts untouched. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HBG1wnuRfJKCstGB8Fekr8
This commit is contained in:
197
src/App.tsx
Normal file
197
src/App.tsx
Normal file
@@ -0,0 +1,197 @@
|
||||
import { Toaster } from "@/components/ui/toaster"
|
||||
import { Toaster as HotToaster } from 'react-hot-toast';
|
||||
import { QueryClientProvider } from '@tanstack/react-query'
|
||||
import { queryClientInstance } from '@/lib/query-client'
|
||||
import { Navigate, BrowserRouter as Router, Route, Routes, useParams } from 'react-router-dom';
|
||||
import PageNotFound from './lib/PageNotFound';
|
||||
import { AuthProvider, useAuth } from '@/lib/AuthContext';
|
||||
import ScrollToTop from './components/ScrollToTop';
|
||||
import ProtectedRoute from '@/components/ProtectedRoute';
|
||||
import Layout from '@/layouts/Layout';
|
||||
import Overview from '@/pages/Overview';
|
||||
import Positions from '@/pages/Positions';
|
||||
import PositionDetail from '@/pages/PositionDetail';
|
||||
import Candidates from '@/pages/Candidates';
|
||||
import HiredHistory from '@/pages/HiredHistory';
|
||||
import TalentPool from '@/pages/TalentPool';
|
||||
import Apply from '@/pages/Apply';
|
||||
import UserTracking from '@/pages/UserTracking';
|
||||
import Analytics from '@/pages/Analytics';
|
||||
import CreatePosition from '@/pages/CreatePosition';
|
||||
import Profile from '@/pages/Profile';
|
||||
import University from '@/pages/University';
|
||||
import CourseDetail from '@/pages/CourseDetail';
|
||||
import WorkerProfile from '@/pages/WorkerProfile';
|
||||
import KrowIdentity from '@/pages/KrowIdentity';
|
||||
import Owliver from '@/pages/Owliver';
|
||||
import EmployeeDashboard from '@/pages/EmployeeDashboard';
|
||||
import OpportunityDetail from '@/pages/OpportunityDetail';
|
||||
import DesignSystem from '@/pages/DesignSystem';
|
||||
|
||||
/* Admin product — its own layout and route tree, so the Admin redesign can
|
||||
diverge without touching the Employer or Talent experiences. */
|
||||
import AdminLayout from '@/layouts/AdminLayout';
|
||||
import ControlCenter from '@/pages/admin/ControlCenter';
|
||||
import AdminPositions from '@/pages/admin/Positions';
|
||||
import AdminCandidates from '@/pages/admin/Candidates';
|
||||
import AdminCandidateProfile from '@/pages/admin/CandidateProfile';
|
||||
import AdminCandidatesAnalysis from '@/pages/admin/CandidatesAnalysis';
|
||||
import AdminHiredHistory from '@/pages/admin/HiredHistory';
|
||||
import AdminTalentPool from '@/pages/admin/TalentPool';
|
||||
import AdminTalentProfile from '@/pages/admin/TalentProfile';
|
||||
import AdminAnalytics from '@/pages/admin/Analytics';
|
||||
import AdminActivity from '@/pages/admin/Activity';
|
||||
import AdminProfile from '@/pages/admin/Profile';
|
||||
import AdminSettings from '@/pages/admin/Settings';
|
||||
import AdminWorkspace from '@/pages/admin/Workspace';
|
||||
import AdminWorkspaceSkills from '@/pages/admin/WorkspaceSkills';
|
||||
import AdminWorkspaceAgents from '@/pages/admin/WorkspaceAgents';
|
||||
import AdminAgentDetail from '@/pages/admin/AgentDetail';
|
||||
import AdminSkillEditor from '@/pages/admin/SkillEditor';
|
||||
import AdminOwliverSkillEditor from '@/pages/admin/OwliverSkillEditor';
|
||||
import AdminSkillDevelopment from '@/pages/admin/SkillDevelopment';
|
||||
import AdminLogin from '@/pages/admin/Login';
|
||||
import AdminRoute from '@/pages/admin/AdminRoute';
|
||||
|
||||
/** `/university/:id` → the same challenge inside the Admin shell, id preserved. */
|
||||
const LegacyCourseRedirect = () => {
|
||||
const { id } = useParams();
|
||||
return <Navigate to={`/admin/university/${id}`} replace />;
|
||||
};
|
||||
|
||||
/** `/positions/:id` → the same position inside the Admin shell, id preserved. */
|
||||
const LegacyPositionRedirect = () => {
|
||||
const { id } = useParams();
|
||||
return <Navigate to={`/admin/positions/${id}`} replace />;
|
||||
};
|
||||
|
||||
const AuthenticatedApp = () => {
|
||||
const { isLoadingAuth, isLoadingPublicSettings } = useAuth();
|
||||
|
||||
if (isLoadingPublicSettings || isLoadingAuth) {
|
||||
return (
|
||||
<div className="fixed inset-0 flex items-center justify-center">
|
||||
<div className="w-8 h-8 border-4 border-slate-200 border-t-slate-800 rounded-full animate-spin"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Routes>
|
||||
{/* Admin sign-in — outside the protected boundary. */}
|
||||
<Route path="/admin/login" element={<AdminLogin />} />
|
||||
|
||||
<Route element={<ProtectedRoute />}>
|
||||
{/* The application opens on the Admin console, which is entered through
|
||||
its own sign-in. Employer keeps every one of its routes, including
|
||||
Overview at an explicit path — nothing of that product is removed,
|
||||
only the address the app happens to start at. */}
|
||||
<Route path="/" element={<Navigate to="/admin/login" replace />} />
|
||||
|
||||
<Route element={<AdminRoute />}>
|
||||
<Route path="/admin" element={<AdminLayout />}>
|
||||
<Route index element={<ControlCenter />} />
|
||||
<Route path="positions" element={<AdminPositions />} />
|
||||
<Route path="positions/new" element={<CreatePosition />} />
|
||||
<Route path="positions/:id" element={<PositionDetail />} />
|
||||
<Route path="candidates" element={<AdminCandidates />} />
|
||||
{/* The full Candidate Intelligence profile. A page rather than a
|
||||
drawer: the record carries around forty fields and fifteen
|
||||
screening dimensions, which a 480px column cannot show without
|
||||
hiding most of it. */}
|
||||
<Route path="candidates/:id" element={<AdminCandidateProfile />} />
|
||||
{/* Candidate Analysis. A sibling segment, not `candidates/:id`:
|
||||
the analysis reads the whole pool, so it is not addressed by a
|
||||
candidate id and must not be matched as one. The route is the
|
||||
one the `candidates-analysis` surface declares, so the skill
|
||||
table, the assistant placement table and this agree. */}
|
||||
<Route path="candidates-analysis" element={<AdminCandidatesAnalysis />} />
|
||||
<Route path="hired" element={<AdminHiredHistory />} />
|
||||
<Route path="talent-pool" element={<AdminTalentPool />} />
|
||||
{/* The person, keyed by worker profile — distinct from
|
||||
`candidates/:id`, which is one person FOR ONE POSITION. Two
|
||||
routes because they answer different questions; one screen
|
||||
would push the product back towards a candidate record per
|
||||
vacancy, which is the thing `worker_profiles` exists to stop. */}
|
||||
<Route path="talent/:id" element={<AdminTalentProfile />} />
|
||||
<Route path="university" element={<University />} />
|
||||
<Route path="university/:id" element={<CourseDetail />} />
|
||||
<Route path="analytics" element={<AdminAnalytics />} />
|
||||
<Route path="activity" element={<AdminActivity />} />
|
||||
<Route path="profile" element={<AdminProfile />} />
|
||||
{/* Two centres, deliberately separate. Settings configures the
|
||||
account and the system; Workspace & Skills configures the
|
||||
workforce — Owliver's capabilities, training and progression —
|
||||
each with its own pages underneath rather than one page holding
|
||||
both subjects. */}
|
||||
<Route path="settings" element={<AdminSettings />} />
|
||||
<Route path="workspace" element={<AdminWorkspace />} />
|
||||
<Route path="workspace/agents" element={<AdminWorkspaceAgents />} />
|
||||
{/* Static before dynamic, so `agents/new` cannot be read as an
|
||||
agent whose id is "new". */}
|
||||
<Route path="workspace/agents/new" element={<AdminAgentDetail />} />
|
||||
<Route path="workspace/agents/:id" element={<AdminAgentDetail />} />
|
||||
<Route path="workspace/skills" element={<AdminWorkspaceSkills />} />
|
||||
{/* Two editors, because a UI skill and an Owliver skill configure
|
||||
different things. Static segments rank above the dynamic `:id`,
|
||||
so `skills/owliver/...` cannot be read as a skill called
|
||||
"owliver" — and the two original addresses are untouched. */}
|
||||
<Route path="workspace/skills/new" element={<AdminSkillEditor />} />
|
||||
<Route path="workspace/skills/owliver/new" element={<AdminOwliverSkillEditor />} />
|
||||
<Route path="workspace/skills/owliver/:id" element={<AdminOwliverSkillEditor />} />
|
||||
<Route path="workspace/skills/:id" element={<AdminSkillEditor />} />
|
||||
<Route path="workspace/skill-development" element={<AdminSkillDevelopment />} />
|
||||
</Route>
|
||||
</Route>
|
||||
|
||||
{/* Redirect top-level legacy aliases to the unified global shell */}
|
||||
{/* Employee (talent) opportunity flow — full-page routes, no drawer.
|
||||
Standalone (outside AdminLayout/AdminRoute, which are the employer
|
||||
shell and gate); authenticated via ProtectedRoute. */}
|
||||
<Route path="/employee" element={<div className="min-h-screen bg-[#F8FAFC]"><div className="max-w-5xl mx-auto px-4 py-8"><EmployeeDashboard /></div></div>} />
|
||||
<Route path="/opportunities/:id" element={<div className="min-h-screen bg-[#F8FAFC]"><div className="max-w-3xl mx-auto px-4 py-8"><OpportunityDetail /></div></div>} />
|
||||
<Route path="/apply" element={<div className="min-h-screen bg-[#F8FAFC]"><div className="max-w-3xl mx-auto px-4 py-8"><Apply /></div></div>} />
|
||||
<Route path="/overview" element={<Navigate to="/admin" replace />} />
|
||||
<Route path="/positions" element={<Navigate to="/admin/positions" replace />} />
|
||||
{/* `/positions` only matches the exact path, so the sub-routes need
|
||||
their own aliases or an inbound link lands on the 404 page. */}
|
||||
<Route path="/positions/new" element={<Navigate to="/admin/positions/new" replace />} />
|
||||
<Route path="/positions/:id" element={<LegacyPositionRedirect />} />
|
||||
<Route path="/candidates" element={<Navigate to="/admin/candidates" replace />} />
|
||||
<Route path="/hired" element={<Navigate to="/admin/hired" replace />} />
|
||||
<Route path="/talent-pool" element={<Navigate to="/admin/talent-pool" replace />} />
|
||||
<Route path="/university" element={<Navigate to="/admin/university" replace />} />
|
||||
{/* Same treatment as every other legacy alias: the challenge opens inside
|
||||
the application shell rather than on a bare page, so Forge → challenge
|
||||
keeps its navigation, header and page frame. Existing links to
|
||||
/university/:id are unchanged and still resolve here. */}
|
||||
<Route path="/university/:id" element={<LegacyCourseRedirect />} />
|
||||
<Route path="/analytics" element={<Navigate to="/admin/analytics" replace />} />
|
||||
<Route path="/activity" element={<Navigate to="/admin/activity" replace />} />
|
||||
<Route path="/tracking" element={<Navigate to="/admin/activity" replace />} />
|
||||
</Route>
|
||||
<Route path="*" element={<PageNotFound />} />
|
||||
</Routes>
|
||||
);
|
||||
};
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<AuthProvider>
|
||||
<QueryClientProvider client={queryClientInstance}>
|
||||
<Router>
|
||||
<ScrollToTop />
|
||||
<AuthenticatedApp />
|
||||
</Router>
|
||||
<Toaster />
|
||||
{/* The design system's `toast` helper wraps react-hot-toast, which needs
|
||||
its own renderer mounted. Without this every toast in the app fired
|
||||
into nothing — confirmations, errors and the "not in the demo"
|
||||
notices all silently did nothing. */}
|
||||
<HotToaster position="bottom-right" />
|
||||
</QueryClientProvider>
|
||||
</AuthProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
Reference in New Issue
Block a user