Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 51 additions & 37 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,16 +39,30 @@ function parseOidcHash(): {
return { token: params.get("oidc_token"), error: params.get("oidc_error") };
}

/** Hidden admin entry: visiting /admin or /admin/* routes straight to org-auth. */
function isAdminPath(): boolean {
return window.location.pathname.replace(/\/+$/, "").startsWith("/admin");
}

function initialPage(): Page {
if (parseOidcHash()?.error) return "login";
if (isAdminPath()) {
const storedOrgToken = localStorage.getItem("org_token");
return storedOrgToken ? "org-dashboard" : "org-auth";
}
return "landing";
}

function App() {
// Initial page/loader are derived from the URL so an OIDC return never flashes
// the landing page: a token shows the loader, an error drops straight to login.
const [page, setPage] = useState<Page>(() =>
parseOidcHash()?.error ? "login" : "landing",
);
const [page, setPage] = useState<Page>(initialPage);
const [auth, setAuth] = useState<AuthState | null>(null);
const [activeInterviewId, setActiveInterviewId] = useState<number | null>(
null,
);
const [orgToken, setOrgToken] = useState<string | null>(() =>
localStorage.getItem("org_token"),
);
const [orgName, setOrgName] = useState<string | undefined>(undefined);
const [hydrating, setHydrating] = useState<boolean>(() =>
Boolean(parseOidcHash()?.token),
);
Expand All @@ -59,15 +73,14 @@ function App() {
const oidc = parseOidcHash();
if (!oidc) return;

// Strip the token/error from the URL so it isn't kept in history or re-read.
window.history.replaceState(
null,
"",
window.location.pathname + window.location.search,
);

const token = oidc.token;
if (!token) return; // error case already reflected in the initial page state
if (!token) return;

localStorage.setItem("token", token);
fetchCurrentUser(token)
Expand Down Expand Up @@ -109,15 +122,14 @@ function App() {
};

const handleOrgLoginSuccess = (token: string) => {
localStorage.setItem("org_token", token);
setOrgToken(token);
setPage("org-dashboard");
};

const handleOrgSignupSuccess = (data: OrgSignupResponse) => {
const token = data.organization
? (localStorage.getItem("org_token") ?? "")
: "";
setOrgToken(token);
localStorage.setItem("org_token", data.access_token);
setOrgToken(data.access_token);
setOrgName(data.organization?.id?.toString());
setPage("org-dashboard");
};
Expand All @@ -126,7 +138,8 @@ function App() {
localStorage.removeItem("org_token");
setOrgToken(null);
setOrgName(undefined);
setPage("landing");
// Keep the user on /admin so they can sign back in without retyping the URL.
setPage("org-auth");
};

const handleAttemptInterview = (interviewId: number) => {
Expand All @@ -139,6 +152,15 @@ function App() {
setPage("dashboard");
};

const handleBackFromOrgAuth = () => {
// /admin URL: there's no public landing for admins, so a back press just
// clears the admin path and sends them home.
if (isAdminPath()) {
window.history.replaceState(null, "", "/");
}
setPage("landing");
};

if (hydrating) {
return <OidcLoader />;
}
Expand Down Expand Up @@ -199,26 +221,34 @@ function App() {
<OrgAuthPage
onLoginSuccess={handleOrgLoginSuccess}
onSignupSuccess={handleOrgSignupSuccess}
onBack={() => setPage("landing")}
onBack={handleBackFromOrgAuth}
/>
);

case "org-dashboard":
case "org-dashboard": {
const token = orgToken ?? localStorage.getItem("org_token") ?? "";
if (!token) {
// No token: render the auth page directly instead of mutating
// page state during render (would violate React purity rules).
return (
<OrgAuthPage
onLoginSuccess={handleOrgLoginSuccess}
onSignupSuccess={handleOrgSignupSuccess}
onBack={handleBackFromOrgAuth}
/>
);
}
return (
<OrgDashboardPage
token={orgToken ?? localStorage.getItem("org_token") ?? ""}
token={token}
orgName={orgName}
onLogout={handleOrgLogout}
/>
);
}

default:
return (
<LandingPage
onLoginClick={() => setPage("login")}
onOrgLoginClick={() => setPage("org-auth")}
/>
);
return <LandingPage onLoginClick={() => setPage("login")} />;
}
}

Expand All @@ -231,20 +261,4 @@ function OidcLoader() {
);
}

function Placeholder({ label, onBack }: { label: string; onBack: () => void }) {
return (
<div className="min-h-screen bg-slate-50 flex flex-col items-center justify-center gap-4 text-slate-900">
<div className="text-5xl">🚀</div>
<p className="text-xl font-semibold">{label} — coming soon</p>
<button
onClick={onBack}
className="text-blue-600 hover:underline text-sm mt-2"
>
← Back to home
</button>
</div>
);
}

export default App;

15 changes: 0 additions & 15 deletions frontend/src/components/LandingPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import Orb from "./Orb";

export interface LandingPageProps {
onLoginClick?: () => void;
onOrgLoginClick?: () => void;
}

interface Stat {
Expand All @@ -28,7 +27,6 @@ const STATS: Stat[] = [

export default function LandingPage({
onLoginClick,
onOrgLoginClick,
}: LandingPageProps): JSX.Element {
return (
<div
Expand Down Expand Up @@ -151,19 +149,6 @@ export default function LandingPage({
</div>

<div style={{ display: "flex", gap: 14, alignItems: "center" }}>
<button
onClick={onOrgLoginClick}
style={{
background: "transparent",
border: "none",
color: "#4b5563",
fontSize: 14,
fontWeight: 500,
cursor: "pointer",
}}
>
For Organisations
</button>
<button
onClick={onLoginClick}
style={{
Expand Down
14 changes: 7 additions & 7 deletions frontend/src/features/org/OrgAuthPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ const OrgAuthPage: React.FC<OrgAuthPageProps> = ({
>
{/* LEFT: hiring-focused marketing */}
<div style={{ position: "relative" }}>
<Pill text="Organisation Portal" />
<Pill text="Admin Portal" />
<h1
style={{
fontSize: 56,
Expand All @@ -127,8 +127,8 @@ const OrgAuthPage: React.FC<OrgAuthPageProps> = ({
fontWeight: 500,
}}
>
Set up your organisation in minutes. Let AI run first-round
interviews so your team only meets the candidates worth meeting.
Sign in to manage interviews, review candidates, and view the
leaderboard once sessions complete.
</p>

<div
Expand Down Expand Up @@ -278,7 +278,7 @@ const OrgAuthPage: React.FC<OrgAuthPageProps> = ({
transition: "all 0.2s",
}}
>
{t === "signup" ? "Create Organisation" : "Sign In"}
{t === "signup" ? "Create Admin" : "Sign In"}
</button>
))}
</div>
Expand Down Expand Up @@ -367,7 +367,7 @@ const OrgSignupInline: React.FC<OrgSignupInlineProps> = ({
<LightInput
id="org-signup-username"
name="username"
label="Organisation Username"
label="Admin Username"
type="text"
placeholder="acme_corp"
autoComplete="username"
Expand Down Expand Up @@ -424,7 +424,7 @@ const OrgSignupInline: React.FC<OrgSignupInlineProps> = ({
</>
) : (
<>
Create Organisation Account
Create Admin Account
<ArrowIcon />
</>
)}
Expand Down Expand Up @@ -555,7 +555,7 @@ const OrgLoginInline: React.FC<OrgLoginInlineProps> = ({
padding: 0,
}}
>
Register your organisation
Register an admin account
</button>
</p>
</form>
Expand Down
Loading
Loading