Files
krow_talent_app/src/App.tsx

203 lines
11 KiB
TypeScript

/* Side-effect import: initialises i18next and subscribes it to the language
store, before any component calls t(). Imported here rather than in main so
that every entry point into the app gets it. */
import '@/lib/i18n';
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