fix mobile screen issues
This commit is contained in:
@@ -4,7 +4,7 @@ import { Sparkles, ChevronDown, ChevronUp, Sliders, Loader2, Star } from 'lucide
|
||||
import { useCreateJobPosting, useUpdateJobPosting, useGenerateJobDescription, useJobPosting, useRoleCategories, useCreateRoleCategory } from '@/lib/krowHooks';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import { ROLE_CATEGORIES } from '@/lib/roleCategories';
|
||||
import { CERT_OPTIONS, CRITERIA_LABELS, ENGLISH_LEVELS, defaultPosition } from '@/lib/positionModel';
|
||||
import { CERT_OPTIONS, CRITERIA_LABELS, ENGLISH_LEVELS, defaultPosition, toPositionPayload } from '@/lib/positionModel';
|
||||
import { SkillRequirementsField } from '@/components/krow/SkillRequirementsField';
|
||||
import { SkillSurface } from '@/components/skills/SkillSurface';
|
||||
import OwliverAvatar from '@/components/krow/OwliverAvatar';
|
||||
@@ -130,11 +130,11 @@ export default function CreatePosition({ prefill: prefillProp, embedded = false,
|
||||
// Create draft if not exists
|
||||
let id = draftId;
|
||||
if (!id) {
|
||||
const draft = await createJob.mutateAsync({ ...form, status: 'draft', pay_range_min: Number(form.pay_range_min) || 0, pay_range_max: Number(form.pay_range_max) || 0 });
|
||||
const draft = await createJob.mutateAsync(toPositionPayload(form, { status: 'draft' }));
|
||||
id = draft.id;
|
||||
setDraftId(id);
|
||||
}
|
||||
const result = await generateDesc.mutateAsync({ data: { ...form, pay_range_min: Number(form.pay_range_min) || 0, pay_range_max: Number(form.pay_range_max) || 0 }, draftId: id });
|
||||
const result = await generateDesc.mutateAsync({ data: toPositionPayload(form), draftId: id });
|
||||
setAiResult(result);
|
||||
toast.success('Job description generated!');
|
||||
} catch {
|
||||
@@ -153,11 +153,7 @@ export default function CreatePosition({ prefill: prefillProp, embedded = false,
|
||||
}
|
||||
try {
|
||||
const payload = {
|
||||
...form,
|
||||
status,
|
||||
company: String(form.company || '').trim(),
|
||||
pay_range_min: Number(form.pay_range_min) || 0,
|
||||
pay_range_max: Number(form.pay_range_max) || 0,
|
||||
...toPositionPayload(form, { status }),
|
||||
...(aiResult ? { description: aiResult.description, responsibilities: aiResult.responsibilities, qualifications: aiResult.qualifications, nice_to_haves: aiResult.nice_to_haves, ai_generated: true } : {}),
|
||||
};
|
||||
let createdId = draftId;
|
||||
@@ -190,12 +186,15 @@ export default function CreatePosition({ prefill: prefillProp, embedded = false,
|
||||
<div className={embedded ? '' : 'max-w-3xl mx-auto'}>
|
||||
{/* Header */}
|
||||
{!embedded && (
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<div className="flex items-start justify-between gap-4 mb-6">
|
||||
<div className="min-w-0">
|
||||
<h1 className="text-[22px] font-bold text-[#111827]">Create a Position</h1>
|
||||
<p className="text-[13px] text-[#6B7280] mt-0.5">Set hiring criteria once — KROW interviews and ranks every applicant automatically.</p>
|
||||
</div>
|
||||
<button onClick={() => navigate('/admin/positions')} className="text-[13px] text-[#6B7280] hover:text-[#111827]">Cancel</button>
|
||||
{/* `shrink-0` and a real gap: without them the subtitle's intrinsic
|
||||
width pushed Cancel against the edge of a phone screen with the
|
||||
title wrapping underneath it. */}
|
||||
<button onClick={() => navigate('/admin/positions')} className="shrink-0 pt-1 text-[13px] text-[#6B7280] hover:text-[#111827]">Cancel</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
import { Navigate, Outlet, useLocation } from 'react-router-dom';
|
||||
import { hasAdminSession } from '@/lib/admin/session';
|
||||
import { useAuth } from '@/lib/AuthContext';
|
||||
|
||||
/**
|
||||
* Gate on the Admin console.
|
||||
*
|
||||
* Without this, `/admin` renders the Control Center for anyone who types the URL,
|
||||
* because the demo's data client reports the seeded user as permanently signed in
|
||||
* (see session.js). The console is meant to be entered through its own sign-in,
|
||||
* so the guard sends an unauthenticated visitor to the login page.
|
||||
* It asks the auth context, which reflects `GET /me` — a real question to the
|
||||
* server about a real session — rather than the `sessionStorage` flag it used
|
||||
* to read. That flag could be set from the browser console; a session row keyed
|
||||
* by an HttpOnly cookie cannot.
|
||||
*
|
||||
* This guard is deliberately kept even though it now sits inside
|
||||
* `ProtectedRoute`, which checks the same thing. It is not redundant defence in
|
||||
* depth for its own sake: it is the component that owns "the Admin console is
|
||||
* entered through its own sign-in", and it is where a role check will go in
|
||||
* Phase 3D, when being signed in stops being sufficient to be here.
|
||||
*
|
||||
* The attempted path travels along in location state, so signing in returns you
|
||||
* to where you were going rather than dropping you on the Control Center. That
|
||||
@@ -16,8 +22,14 @@ import { hasAdminSession } from '@/lib/admin/session';
|
||||
*/
|
||||
export default function AdminRoute() {
|
||||
const location = useLocation();
|
||||
const { isAuthenticated, isLoadingAuth, authChecked } = useAuth();
|
||||
|
||||
if (!hasAdminSession()) {
|
||||
// ProtectedRoute has already waited for the check, so this is belt and
|
||||
// braces: without it a direct render of this route would redirect to the
|
||||
// login page during the first tick and bounce a signed-in operator out.
|
||||
if (isLoadingAuth || !authChecked) return null;
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return <Navigate to="/admin/login" replace state={{ from: location.pathname }} />;
|
||||
}
|
||||
|
||||
|
||||
@@ -250,7 +250,7 @@ export default function AdminAnalytics() {
|
||||
/>
|
||||
<Surface variant="solid" radius="lg" padding="none" elevation="xs" className="overflow-hidden border border-border">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full border-collapse text-body-sm">
|
||||
<table className="w-full min-w-[36rem] border-collapse text-body-sm">
|
||||
<caption className="sr-only">Hires, quality, speed and review outcome by role</caption>
|
||||
<thead>
|
||||
<tr className="border-b border-border bg-surface-subtle">
|
||||
|
||||
@@ -325,7 +325,7 @@ export default function AdminCandidatesAnalysis() {
|
||||
<SectionTitle id="fit" title="Position fit" meta={`${positionFit.length} open roles`} />
|
||||
<Surface variant="solid" radius="lg" padding="none" elevation="xs" className="overflow-hidden">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full border-collapse text-body-sm">
|
||||
<table className="w-full min-w-[40rem] border-collapse text-body-sm">
|
||||
<caption className="sr-only">
|
||||
Applicant supply against candidates clearing the bar, per open role
|
||||
</caption>
|
||||
|
||||
@@ -922,7 +922,7 @@ export default function ControlCenter() {
|
||||
>
|
||||
<div className="overflow-hidden rounded-xl border border-border bg-surface">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full border-collapse">
|
||||
<table className="w-full min-w-[36rem] border-collapse">
|
||||
<caption className="sr-only">Most recent platform activity</caption>
|
||||
<thead>
|
||||
<tr className="border-b border-border bg-surface-subtle">
|
||||
|
||||
@@ -4,14 +4,27 @@ import { AlertCircle, ArrowRight, Loader2, Eye, EyeOff, Sparkles, CheckCircle2 }
|
||||
import { cn } from '@/lib/utils';
|
||||
import { KROW_LOGO_URL } from '@/assets/brand';
|
||||
import { Checkbox } from '@/components/ds';
|
||||
import { base44 } from '@/api/base44Client';
|
||||
import { startAdminSession } from '@/lib/admin/session';
|
||||
import { useAuth } from '@/lib/AuthContext';
|
||||
|
||||
const DEMO_EMAIL = 'demo@krow.app';
|
||||
|
||||
/**
|
||||
* The one message every failed sign-in shows.
|
||||
*
|
||||
* The API answers a wrong password, an unknown address, an account with no
|
||||
* password and a suspended account with the same 401 and the same body, so that
|
||||
* a stranger cannot learn which addresses are registered. Rendering the server's
|
||||
* message verbatim would be equally correct; this is the same statement in the
|
||||
* product's own voice, and — importantly — it is used for every failure, so this
|
||||
* page does not reintroduce the distinction the API removes.
|
||||
*/
|
||||
const GENERIC_AUTH_ERROR =
|
||||
'That email and password do not match an account. Check both and try again.';
|
||||
|
||||
export default function AdminLogin() {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const { login } = useAuth();
|
||||
|
||||
const from = location.state?.from;
|
||||
const returnTo = typeof from === 'string' && from.startsWith('/admin') && from !== '/admin/login'
|
||||
@@ -28,8 +41,11 @@ export default function AdminLogin() {
|
||||
const next = {};
|
||||
if (!form.email.trim()) next.email = 'Enter your work email';
|
||||
else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(form.email.trim())) next.email = 'Enter a valid email address';
|
||||
// Only "did you type anything". A client-side length rule here would be a
|
||||
// guess at the server's policy that goes stale the moment the policy moves,
|
||||
// and it tells someone with a valid short legacy password that their own
|
||||
// password is invalid.
|
||||
if (!form.password) next.password = 'Enter your password';
|
||||
else if (form.password.length < 8) next.password = 'Passwords are at least 8 characters';
|
||||
setErrors(next);
|
||||
return Object.keys(next).length === 0;
|
||||
};
|
||||
@@ -40,17 +56,26 @@ export default function AdminLogin() {
|
||||
if (!validate()) return;
|
||||
|
||||
setStatus('loading');
|
||||
await new Promise((r) => setTimeout(r, 900));
|
||||
|
||||
if (form.password.toLowerCase() === 'wrong') {
|
||||
try {
|
||||
// The session is created server-side and arrives as an HttpOnly cookie.
|
||||
// Nothing on this page sees, stores or forwards a token — there is no
|
||||
// token here to mishandle.
|
||||
await login({
|
||||
email: form.email.trim(),
|
||||
password: form.password,
|
||||
rememberMe: form.remember,
|
||||
});
|
||||
} catch (error) {
|
||||
setStatus('idle');
|
||||
setAuthError('That email and password do not match an administrator account.');
|
||||
// One message for every rejection. A rate-limited attempt is the single
|
||||
// exception worth distinguishing, because "wait a moment" is advice the
|
||||
// person can act on and it reveals nothing about any account.
|
||||
setAuthError(error?.status === 429
|
||||
? (error.message || 'Too many sign-in attempts. Wait a few minutes and try again.')
|
||||
: GENERIC_AUTH_ERROR);
|
||||
return;
|
||||
}
|
||||
|
||||
await base44.auth.updateMe({ account_type: 'admin' });
|
||||
localStorage.setItem('activeRole', 'admin');
|
||||
startAdminSession();
|
||||
setStatus('success');
|
||||
setTimeout(() => navigate(returnTo, { replace: true }), 320);
|
||||
};
|
||||
@@ -192,7 +217,7 @@ export default function AdminLogin() {
|
||||
<button
|
||||
type="button"
|
||||
disabled={busy}
|
||||
onClick={() => setAuthError('Password resets are disabled in the demo.')}
|
||||
onClick={() => setAuthError('Password resets are not available yet — ask an administrator to set a new password.')}
|
||||
className="text-[#D97706] hover:text-[#B45309] font-semibold transition-colors"
|
||||
>
|
||||
Forgot password?
|
||||
@@ -213,7 +238,8 @@ export default function AdminLogin() {
|
||||
</form>
|
||||
|
||||
<p className="text-center text-xs text-[#94A3B8] leading-relaxed pt-2">
|
||||
Demo environment — enter any 8+ character password to sign in. Use <span className="text-[#64748B] font-semibold">wrong</span> to test error validation.
|
||||
Sign in with your Krow account. Passwords are set by an administrator with
|
||||
the <span className="text-[#64748B] font-semibold">setpassword</span> utility.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -91,8 +91,10 @@ export default function AdminProfile() {
|
||||
<AdminPage
|
||||
title="Admin Profile"
|
||||
subtitle="Manage your platform credentials, system permissions and security governance."
|
||||
/* Three buttons need 295px; a 320px phone has 273 after the page gutters,
|
||||
so the row wraps instead of pushing Sign Out off the edge. */
|
||||
actions={
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<button
|
||||
onClick={() => navigate('/admin/settings')}
|
||||
className="inline-flex items-center gap-1.5 px-3.5 py-2 rounded-xl bg-white border border-[#E0E4EA] text-[#101820] text-[13px] font-semibold hover:bg-[#F8F8F8] transition-all shadow-xs"
|
||||
@@ -173,13 +175,13 @@ export default function AdminProfile() {
|
||||
</div>
|
||||
|
||||
{/* Quick stats in Hero */}
|
||||
<div className="flex items-center gap-3 border-t md:border-t-0 md:border-l border-white/20 pt-4 md:pt-0 md:pl-6">
|
||||
<div className="bg-white/10 backdrop-blur-md rounded-2xl p-4 border border-white/15 min-w-[130px] text-center">
|
||||
<div className="flex flex-wrap items-center gap-3 border-t md:border-t-0 md:border-l md:flex-nowrap border-white/20 pt-4 md:pt-0 md:pl-6">
|
||||
<div className="flex-1 md:flex-none bg-white/10 backdrop-blur-md rounded-2xl p-4 border border-white/15 min-w-[130px] text-center">
|
||||
<p className="text-[11px] uppercase tracking-wider text-blue-200 font-semibold">Scopes</p>
|
||||
<p className="text-2xl font-bold text-white mt-0.5">{PERMISSIONS.length}</p>
|
||||
<span className="text-[10px] text-blue-200">Full Control</span>
|
||||
</div>
|
||||
<div className="bg-white/10 backdrop-blur-md rounded-2xl p-4 border border-white/15 min-w-[130px] text-center">
|
||||
<div className="flex-1 md:flex-none bg-white/10 backdrop-blur-md rounded-2xl p-4 border border-white/15 min-w-[130px] text-center">
|
||||
<p className="text-[11px] uppercase tracking-wider text-blue-200 font-semibold">Security Level</p>
|
||||
<p className="text-2xl font-bold text-emerald-300 mt-0.5">Tier 1</p>
|
||||
<span className="text-[10px] text-blue-200">Max Privileges</span>
|
||||
|
||||
@@ -55,18 +55,18 @@ export default function AdminSettings() {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3 shrink-0">
|
||||
<div className="bg-white/10 backdrop-blur-md rounded-2xl p-4 border border-white/15 min-w-[130px] text-center">
|
||||
<div className="flex flex-wrap items-center gap-3 sm:flex-nowrap sm:shrink-0">
|
||||
<div className="flex-1 sm:flex-none bg-white/10 backdrop-blur-md rounded-2xl p-4 border border-white/15 min-w-[130px] text-center">
|
||||
<p className="text-[11px] uppercase tracking-wider text-blue-200 font-semibold">Active Staff</p>
|
||||
<p className="text-2xl font-bold text-white mt-0.5">{staff.length}</p>
|
||||
<span className="text-[10px] text-blue-200">On Platform</span>
|
||||
</div>
|
||||
<div className="bg-white/10 backdrop-blur-md rounded-2xl p-4 border border-white/15 min-w-[130px] text-center">
|
||||
<div className="flex-1 sm:flex-none bg-white/10 backdrop-blur-md rounded-2xl p-4 border border-white/15 min-w-[130px] text-center">
|
||||
<p className="text-[11px] uppercase tracking-wider text-blue-200 font-semibold">Positions</p>
|
||||
<p className="text-2xl font-bold text-emerald-300 mt-0.5">{postings.length}</p>
|
||||
<span className="text-[10px] text-blue-200">Active Roles</span>
|
||||
</div>
|
||||
<div className="bg-white/10 backdrop-blur-md rounded-2xl p-4 border border-white/15 min-w-[130px] text-center">
|
||||
<div className="flex-1 sm:flex-none bg-white/10 backdrop-blur-md rounded-2xl p-4 border border-white/15 min-w-[130px] text-center">
|
||||
<p className="text-[11px] uppercase tracking-wider text-blue-200 font-semibold">Clients</p>
|
||||
<p className="text-2xl font-bold text-sky-300 mt-0.5">{clientCount}</p>
|
||||
<span className="text-[10px] text-blue-200">Organizations</span>
|
||||
|
||||
@@ -83,8 +83,8 @@ export default function AdminWorkspace() {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3 shrink-0">
|
||||
<div className="bg-white/10 backdrop-blur-md rounded-2xl p-4 border border-white/15 min-w-[130px] text-center">
|
||||
<div className="flex flex-wrap items-center gap-3 sm:flex-nowrap sm:shrink-0">
|
||||
<div className="flex-1 sm:flex-none bg-white/10 backdrop-blur-md rounded-2xl p-4 border border-white/15 min-w-[130px] text-center">
|
||||
<p className="text-[11px] uppercase tracking-wider text-blue-200 font-semibold">Registered Skills</p>
|
||||
<p className="text-2xl font-bold text-white mt-0.5">{skills.length}</p>
|
||||
<span className="text-[10px] text-emerald-300 font-medium">{activeSkills} Active</span>
|
||||
@@ -92,7 +92,7 @@ export default function AdminWorkspace() {
|
||||
{/* Was "Training Paths". Replaced rather than dropped: the slot is
|
||||
useful, and the split of agent skills across the two lists is
|
||||
what this page actually governs. */}
|
||||
<div className="bg-white/10 backdrop-blur-md rounded-2xl p-4 border border-white/15 min-w-[130px] text-center">
|
||||
<div className="flex-1 sm:flex-none bg-white/10 backdrop-blur-md rounded-2xl p-4 border border-white/15 min-w-[130px] text-center">
|
||||
<p className="text-[11px] uppercase tracking-wider text-blue-200 font-semibold">Skill Kinds</p>
|
||||
<p className="text-2xl font-bold text-sky-300 mt-0.5">{owliverCount + boardCount}</p>
|
||||
<span className="text-[10px] text-blue-200">{owliverCount} Owliver · {boardCount} Board</span>
|
||||
|
||||
@@ -197,7 +197,12 @@ function AgentTableRow({ agent, shipped, overridden, onOpen, onAction }) {
|
||||
<button
|
||||
type="button"
|
||||
onClick={onOpen}
|
||||
className="font-heading text-body-sm font-semibold text-ink-1 hover:text-krow-blue transition-colors text-left focus-visible:outline-none"
|
||||
/* On a phone the identity column claims the width of the name
|
||||
rather than stacking "Control Center Agent" three words tall,
|
||||
which it can afford because the table already scrolls inside
|
||||
its own container there. Released at `sm`, so every wider
|
||||
layout distributes its columns exactly as it did before. */
|
||||
className="whitespace-nowrap sm:whitespace-normal font-heading text-body-sm font-semibold text-ink-1 hover:text-krow-blue transition-colors text-left focus-visible:outline-none"
|
||||
>
|
||||
{agent.name}
|
||||
</button>
|
||||
@@ -493,7 +498,7 @@ export default function AdminWorkspaceAgents() {
|
||||
/* Clean Modern Directory Table View */
|
||||
<div className="overflow-hidden rounded-2xl border border-border bg-surface shadow-xs">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-left border-collapse">
|
||||
<table className="w-full min-w-[48rem] text-left border-collapse">
|
||||
<thead>
|
||||
<tr className="border-b border-border bg-surface-subtle/60 text-[11px] font-semibold uppercase tracking-wider text-ink-4">
|
||||
<th className="py-3 pl-4 pr-3 font-semibold">Agent</th>
|
||||
|
||||
Reference in New Issue
Block a user