chore: clean up and restructure repository

This commit is contained in:
2026-08-21 11:11:21 +05:30
parent b2e6868824
commit dcf9770ada
106 changed files with 239 additions and 5654 deletions

193
README.md
View File

@@ -2,38 +2,99 @@
AI-powered hiring for event and hospitality staffing — a standalone demo build of KROW Forge.
Runs with no backend, no API keys, and no login. `npm install && npm run dev` and the app is fully
Runs with no backend, no API keys and no real login. `npm install && npm run dev` and the app is fully
populated.
```bash
npm install
npm run dev
npm run dev # http://localhost:5173 — opens on /admin/login
```
## Architecture
## Stack
Vite + React 18, React Router 6, React Query 5, Tailwind + shadcn/ui, Framer Motion, Recharts.
Vite 6 · React 19 · React Router 6 · React Query 5 · Tailwind + shadcn/ui · Framer Motion ·
Recharts + MUI X Charts · react-hot-toast.
## Structure
```
src/
api/ data client — entity store, demo dataset, local AI engine
assets/ brand asset references
main.jsx entry — mounts <App/>, imports index.css
App.jsx providers + the entire route table
index.css design tokens (HSL CSS variables) + utility layers
api/ the backend seam — see below
base44Client.js the contract the production app talks to
store.js in-memory entity store, mirrored to localStorage
aiEngine.js local, deterministic InvokeLLM
seed.js demo dataset
attendanceSeed.js shift/attendance dataset
lib/ domain logic and data hooks
krowHooks.js React Query hooks over the entity store
krowAi.js AI workflows
krowScore.js score engine
AuthContext.jsx auth provider
admin/ admin session, permissions, position insights
skills/ the Owliver skill system (registry, resolver, flows)
agents/ the agent system (registry, runtime, lifecycle)
skills/ *.md skill definitions — loaded by import.meta.glob
agents/ *.md agent definitions — loaded by import.meta.glob
components/
ui/ 50 shadcn primitives (design system)
krow/ domain components
talent/ talent-portal components
proving/ Proving Ground challenge components
hooks/ shared hooks
layouts/ Layout — header, nav pill, control bar, mobile sheet
lib/ data hooks, score engine, AI workflows, auth context, utils
pages/ 18 route components
utils/ helpers
ui/ 23 shadcn primitives
ds/ 30 design-system components (the app's real vocabulary)
admin/ admin page shell + role glyphs
krow/ domain components (+ talent/, proving/)
charts/ MUI X Charts wrappers and theme
agents/ agent configure / canvas / test panel
forge/ KROW Forge skill + challenge components
skills/ skill authoring surfaces
ai-assistant/ Owliver — panel, routing, capabilities, history
layouts/ AdminLayout — header nav, ⌘K search, assistant panel
pages/ route components (admin/ holds the live product)
hooks/ shared hooks
assets/ brand asset references
```
### The backend seam
## Routes
The production app talks to a Base44 backend through `src/api/base44Client.js`. This demo keeps
that module path, export name, and full method contract, and swaps only the transport:
The app opens on **`/admin/login`**. Everything else lives under `/admin/*` behind two guards:
`ProtectedRoute` (auth) and `AdminRoute` (admin session), rendered inside `AdminLayout`.
| Path | Page |
| --- | --- |
| `/admin/login` | Admin sign-in — outside the protected boundary |
| `/admin` | Control Center |
| `/admin/positions`, `positions/new`, `positions/:id` | Positions, Create, Detail |
| `/admin/candidates`, `candidates/:id` | Candidates, Candidate Intelligence profile |
| `/admin/hired` · `/admin/talent-pool` | Hired History, Talent Pool |
| `/admin/university`, `university/:id` | KROW Forge, challenge detail |
| `/admin/analytics` · `/admin/activity` | Analytics, Activity |
| `/admin/profile` · `/admin/settings` | Profile, Settings |
| `/admin/workspace` | Workspace hub |
| `/admin/workspace/agents`, `agents/new`, `agents/:id` | Agent list, agent editor |
| `/admin/workspace/skills`, `skills/new`, `skills/:id` | UI skill list and editor |
| `/admin/workspace/skills/owliver/new`, `owliver/:id` | Owliver skill editor |
| `/admin/workspace/skill-development` | Skill Development |
| `*` | `lib/PageNotFound.jsx` |
Legacy top-level paths (`/overview`, `/positions`, `/candidates`, `/hired`, `/talent-pool`,
`/university`, `/analytics`, `/activity`, `/tracking`) all `Navigate` into their `/admin/*`
equivalent, id preserved. Static route segments are declared before dynamic ones so
`workspace/agents/new` cannot be read as an agent whose id is `"new"`.
> **Note.** `App.jsx` still imports a set of pre-redesign Employer/Talent pages
> (`Overview`, `Apply`, `WorkerProfile`, `KrowIdentity`, `EmployeeDashboard`, `DesignSystem`, …)
> and `layouts/Layout.jsx`. None of them are mounted on a route — they are unreachable at runtime
> and retained pending a decision on whether to re-route or remove them.
## The backend seam
The production app talks to a Base44 backend through `src/api/base44Client.js`. This demo keeps that
module path, export name and full method contract, and swaps only the transport:
| Contract | Demo implementation |
| --- | --- |
@@ -43,56 +104,48 @@ that module path, export name, and full method contract, and swaps only the tran
| `base44.auth.*` | seeded employer/admin user, always signed in |
Because the seam did not move, everything above it — `lib/krowHooks.js`, `lib/krowAi.js`,
`lib/provingGround.js`, `lib/krowScore.js`, every page and component — is unchanged from the
reference implementation.
`lib/krowScore.js`, every page and component — is unchanged from the reference implementation.
### The AI engine
## The AI engine
Every AI workflow in the app funnels through one call, `InvokeLLM({ prompt, response_json_schema })`.
`api/aiEngine.js` reimplements that function locally. It recognizes each workflow by the phrase its
prompt opens with, reads the structured fields the prompt already carries (`Years Experience: 6`,
`Required Certifications: …`, the talent-pool JSON block), and scores them deterministically.
Every AI workflow funnels through one call, `InvokeLLM({ prompt, response_json_schema })`.
`api/aiEngine.js` reimplements it locally: it recognises each workflow by the phrase its prompt opens
with, reads the structured fields the prompt already carries (`Years Experience: 6`,
`Required Certifications: …`, the talent-pool JSON block) and scores them deterministically.
Nine workflows are implemented: candidate screening, job-description generation, résumé building,
interview questions, interview evaluation, talent matching, Owliver's conversation, Career DNA
building, and Proving Ground evaluation.
building and Proving Ground evaluation. Results respond to the data rather than being canned — the
same input always returns the same output.
Results respond to the data rather than being canned — screening a 7-year native-English candidate
and a 1-year candidate against the same posting returns genuinely different scores, breakdowns,
strengths and gaps. The same input always returns the same output.
## Skills and agents
### Owliver
Two registries discover their definitions from markdown at build time:
Two assistants share one visual language:
| Registry | Glob | Files |
| --- | --- | --- |
| `lib/skills/registry.js` | `import.meta.glob('/src/skills/**/*.md')` | 18 Owliver skills, 5 workforce training skills |
| `lib/agents/registry.js` | `import.meta.glob('/src/agents/**/*.md')` | 9 agent definitions |
- **`OwliverAssistant`** — the hiring assistant, mounted on **Overview**, **Candidates** and
**Analytics** only. Answers from the same React Query caches the pages render from, so what it
says and what the dashboard shows can never disagree.
- **`OwliverChatBubble`** — the talent-facing profile builder, mounted in the Talent role.
**These globs are absolute paths.** Moving or renaming `src/skills/` or `src/agents/` makes the
registry silently return nothing — no build error, no import failure, just an empty registry. Add
definitions by dropping a new `.md` file into the right folder; nothing else needs to change.
## Demo data
Seeded in `src/api/seed.js`: 7 positions (5 open), 22 applicants, 9 AI-scored candidates averaging
76, 4 AI interviews, 3 hires, 6 talent-pool profiles, 4 Proving Ground courses, and an activity log.
Seeded in `src/api/seed.js` — positions, applicants, AI-scored candidates, interviews, hires,
talent-pool profiles, Proving Ground courses and an activity log; shift data in
`src/api/attendanceSeed.js`.
Edits persist to `localStorage` — screen candidates, hire someone, create a position, and the
numbers move everywhere. To restore the shipped dataset:
Edits persist to `localStorage`. To restore the shipped dataset:
```js
// in the browser console
localStorage.removeItem('krow_demo_db'); location.reload();
```
Bumping `STORAGE_VERSION` in `src/api/store.js` invalidates saved snapshots automatically, so
changing the seed does not require a manual clear.
## Roles
The header role switcher moves between three surfaces, persisted in `localStorage`:
- **Employer** — Overview, Positions, Candidates, Hired History, Talent Pool, Analytics
- **Admin** — adds Control Center, KROW Forge, Activity
- **Talent** — My Portal, KROW Forge, Opportunities
Bumping `STORAGE_VERSION` in `src/api/store.js` (currently `8`) invalidates saved snapshots
automatically, so changing the seed does not require a manual clear.
## Design system
@@ -109,15 +162,49 @@ Tokens live in `src/index.css` as HSL CSS variables consumed by `tailwind.config
| Headings | Sora |
| Body | Inter |
The page background is `bg-[#F8FAFC]` under the `gradient-mesh` utility — four radial gradients in
blue, yellow and mint. Surfaces use `glass` and `glass-card` (backdrop blur with saturation).
`src/components/ds/` is the vocabulary the app actually writes in — `Surface`, `KpiCard`,
`DataTable`, `PageHeader`, `Modal`, `toast` and the rest, exported through `ds/index.js`.
`src/components/ui/` holds the shadcn primitives those are built on. Surfaces use `glass` and
`glass-card`; the page background is the `gradient-mesh` utility.
Toasts go through `ds/toast` (a wrapper over `react-hot-toast`), rendered by `<HotToaster>` in
`App.jsx`.
## Scripts
| Command | Purpose |
| --- | --- |
| `npm run dev` | Dev server (honors `PORT`) |
| `npm run dev` | Dev server (honors `PORT`, default 5173) |
| `npm run build` | Production build |
| `npm run preview` | Serve the build |
| `npm run lint` | ESLint |
| `npm run lint` | ESLint — currently clean |
| `npm run lint:fix` | ESLint with `--fix` |
| `npm test` | `scripts/skill-check.mjs` — 835 assertions over the skill and agent systems |
| `npm run typecheck` | `tsc -p ./jsconfig.json` with `checkJs` |
### The test harness
`npm test` boots a real Vite dev server in middleware mode and `ssrLoadModule`s the actual
application modules, so it exercises the real `@/` alias and `import.meta.glob` pipeline rather than
mocks. It also diffs Owliver's behaviour across eleven page contexts against a committed snapshot,
`scripts/__baseline__/owliver-baseline.json`; regenerate that deliberately with
`node scripts/owliver-baseline.mjs --write`, never to turn a red check green.
Because it loads modules by absolute path, the harness pins the locations of `src/api/`, `src/lib/`,
`src/lib/skills/`, `src/lib/agents/`, `src/components/ai-assistant/`, `src/App.jsx`,
`src/layouts/AdminLayout.jsx` and a handful of agent files. Move any of them and update
`scripts/owliver-capture.mjs` and `scripts/skill-check.mjs` in the same change.
### Known-failing checks
Two checks fail on a clean checkout and are tracked as separate work, not regressions:
- `npm test` — 834/835 pass; `the seeded overtime climb is found` fails.
- `npm run typecheck` — 59 errors, all pre-existing JSDoc/inference gaps in `lib/krowAi.js`,
`lib/skills/*`, `lib/positionModel.js` and a few components.
## Deployment
`Dockerfile` builds with `node:22-alpine` (capped at 1 GB heap for small hosts) and serves the
static `dist/` from `nginx:alpine` using `nginx.conf` — gzip, immutable `/assets/` caching and an
SPA fallback to `index.html` so client-side routes resolve on refresh.

View File

@@ -9,7 +9,8 @@ export default [
files: [
"src/components/**/*.{js,mjs,cjs,jsx}",
"src/pages/**/*.{js,mjs,cjs,jsx}",
"src/Layout.jsx",
"src/layouts/**/*.{js,mjs,cjs,jsx}",
"src/hooks/**/*.{js,mjs,cjs,jsx}",
],
ignores: ["src/lib/**/*", "src/components/ui/**/*"],
...pluginJs.configs.recommended,

1458
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -13,68 +13,38 @@
"preview": "vite preview"
},
"dependencies": {
"@astryxdesign/cli": "^0.3.0",
"@astryxdesign/core": "^0.3.0",
"@astryxdesign/theme-neutral": "^0.3.0",
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1",
"@hookform/resolvers": "^4.1.2",
"@mui/material": "^9.3.1",
"@mui/x-charts": "^9.11.1",
"@radix-ui/react-accordion": "^1.2.3",
"@radix-ui/react-alert-dialog": "^1.1.6",
"@radix-ui/react-aspect-ratio": "^1.1.2",
"@radix-ui/react-avatar": "^1.1.3",
"@radix-ui/react-checkbox": "^1.1.4",
"@radix-ui/react-collapsible": "^1.1.3",
"@radix-ui/react-context-menu": "^2.2.6",
"@radix-ui/react-dialog": "^1.1.6",
"@radix-ui/react-dropdown-menu": "^2.1.6",
"@radix-ui/react-hover-card": "^1.1.6",
"@radix-ui/react-label": "^2.1.2",
"@radix-ui/react-menubar": "^1.1.6",
"@radix-ui/react-navigation-menu": "^1.2.5",
"@radix-ui/react-popover": "^1.1.6",
"@radix-ui/react-progress": "^1.1.2",
"@radix-ui/react-radio-group": "^1.2.3",
"@radix-ui/react-scroll-area": "^1.2.3",
"@radix-ui/react-select": "^2.1.6",
"@radix-ui/react-separator": "^1.1.2",
"@radix-ui/react-slider": "^1.2.3",
"@radix-ui/react-slot": "^1.1.2",
"@radix-ui/react-switch": "^1.1.3",
"@radix-ui/react-tabs": "^1.1.3",
"@radix-ui/react-toast": "^1.2.2",
"@radix-ui/react-toggle": "^1.1.2",
"@radix-ui/react-toggle-group": "^1.1.2",
"@radix-ui/react-tooltip": "^1.1.8",
"@stylexjs/stylex": "^0.19.0",
"@tanstack/react-query": "^5.84.1",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.0.0",
"date-fns": "^3.6.0",
"embla-carousel-react": "^8.5.2",
"framer-motion": "^11.16.4",
"html2canvas": "^1.4.1",
"input-otp": "^1.4.2",
"lucide-react": "^0.475.0",
"moment": "^2.30.1",
"next-themes": "^0.4.4",
"react": "^19.2.8",
"react-day-picker": "^9.14.0",
"react-dom": "^19.2.8",
"react-hook-form": "^7.54.2",
"react-hot-toast": "^2.6.0",
"react-markdown": "^9.0.1",
"react-resizable-panels": "^2.1.7",
"react-router-dom": "^6.26.0",
"recharts": "^3.10.1",
"sonner": "^2.0.1",
"tailwind-merge": "^3.0.2",
"tailwindcss-animate": "^1.0.7",
"vaul": "^1.1.2",
"zod": "^3.24.2"
"tailwindcss-animate": "^1.0.7"
},
"devDependencies": {
"@eslint/js": "^9.19.0",
@@ -86,7 +56,6 @@
"eslint": "^9.19.0",
"eslint-plugin-react": "^7.37.4",
"eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-react-refresh": "^0.4.18",
"eslint-plugin-unused-imports": "^4.3.0",
"globals": "^15.14.0",
"postcss": "^8.5.3",

View File

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 52 KiB

View File

@@ -15,7 +15,7 @@ import { toFICO } from '@/lib/talentHome';
/* The vetting criteria's own labels. Read from the position model rather than
restated here, so the panel calls a weight what the form calls it. */
import { CRITERIA_LABELS } from '@/lib/positionModel';
import { PERMISSIONS, PROFILE_ACTIONS } from '@/pages/admin/permissions';
import { PERMISSIONS, PROFILE_ACTIONS } from '@/lib/admin/permissions';
/**
* Admin capabilities — Control Center, Candidates Analysis, Activity.

View File

@@ -1,6 +1,6 @@
import { plural, verb } from './insights';
import { prioritise, workforceStatusFor } from '@/lib/workforce';
import { PERMISSIONS } from '@/pages/admin/permissions';
import { PERMISSIONS } from '@/lib/admin/permissions';
/**
* Dynamic greetings and suggested prompts.

View File

@@ -162,7 +162,7 @@ function normalizeDraftChip(chip) {
/* "Continue the Bartender draft", however it was stored: the label is the
question, and no capability or route may intercept it. */
if (CONTINUE_DRAFT.test(label) || CONTINUE_DRAFT.test(prompt)) {
const { capability, route, ...rest } = chip;
const { capability: _capability, route: _route, ...rest } = chip;
return { ...rest, prompt: CONTINUE_DRAFT.test(label) ? label : prompt };
}
@@ -170,7 +170,7 @@ function normalizeDraftChip(chip) {
and told the reader to open the form. The same words now resolve to the
draft flow, which continues one or offers the several by name. */
if (OLD_DRAFTS_PROMPT.test(prompt) || chip.capability === 'position-drafts') {
const { capability, route, ...rest } = chip;
const { capability: _capability, route: _route, ...rest } = chip;
return { ...rest, prompt: prompt || 'Which positions are still in draft?' };
}

View File

@@ -95,7 +95,7 @@ export default function AIInterviewModal({ open, onClose, application, job, cond
setMessages(newMessages);
lastUserMessageTime.current = Date.now();
await speak(question);
} catch (_e) {
} catch {
setError('Interview connection issue. Please retry.');
}
}, [jobTitle, speak, language]);
@@ -182,7 +182,7 @@ export default function AIInterviewModal({ open, onClose, application, job, cond
} });
setEvaluation(result);
setPhase('done');
} catch (_e) {
} catch {
setError('Evaluation failed. Please try again.');
setPhase('active');
}

View File

@@ -1,28 +0,0 @@
import React from 'react';
const SNAPSHOTS = [
{ key: 'krow', label: 'KROW Score', get: (p) => Math.round(p.krow_score || 0) },
{ key: 'badges', label: 'Other Badges', get: (p) => (p.earned_badges || []).length },
{ key: 'skills', label: 'Skills', get: (p) => (p.skills || []).length },
{ key: 'shares', label: 'Shares', get: () => 0 },
];
export default function ActivitySnapshot({ profile }) {
return (
<div className="rounded-2xl bg-[#E8EEFE] p-6 border border-[#C7D4F7]">
<h3 className="font-serif text-center text-[18px] font-semibold mb-5" style={{ color: '#333F48' }}>
Activity snapshot
</h3>
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3 sm:gap-4">
{SNAPSHOTS.map((s) => (
<div key={s.key} className="bg-white rounded-2xl shadow-sm p-4 flex flex-col items-center justify-center aspect-square">
<span className="text-[34px] font-bold leading-none" style={{ color: '#0838E0' }}>
{s.get(profile)}
</span>
<span className="text-[12px] text-[#333F48] mt-2 text-center">{s.label}</span>
</div>
))}
</div>
</div>
);
}

View File

@@ -1,9 +1,8 @@
import React from 'react';
import { Users, TrendingUp, Brain, Mic, UserCheck, Target } from 'lucide-react';
export default function AnalyticsMetrics({ applications, interviews, staff }) {
export default function AnalyticsMetrics({ applications, interviews: _interviews, staff }) {
const totalApplicants = applications.length;
const screened = applications.filter(a => a.status !== 'applied');
const scored = applications.filter(a => a.ai_score > 0);
const avgScore = scored.length ? Math.round(scored.reduce((s, a) => s + a.ai_score, 0) / scored.length) : 0;
const interviewed = applications.filter(a => a.status === 'interview' || a.interview_id);

View File

@@ -1,296 +0,0 @@
import React, { useState } from 'react';
import { Button } from '@/components/ui/button';
import { Label } from '@/components/ui/label';
import { Input } from '@/components/ui/input';
import { Loader2, UploadCloud, FileCheck2, X, Camera, RefreshCw } from 'lucide-react';
import { useCertifications } from '@/lib/krowHooks';
import { base44 } from '@/api/base44Client';
const DEFAULT_CERTS = ['Food Handler Card', 'ServSafe', 'ABC License', 'TIPS Certified', 'CPR/First Aid', 'Background Check Cleared'];
const AVAIL_OPTIONS = ['Weekdays', 'Weekends', 'Evenings', 'Mornings', 'Overnight', 'On-Call'];
const ENGLISH_LEVELS = ['basic', 'conversational', 'fluent', 'native'];
export default function ApplicantForm({ onSubmit, submitting }) {
const [form, setForm] = useState({
applicant_name: '',
email: '',
phone: '',
years_experience: 0,
english_level: 'basic',
certifications: [],
availability: [],
skills: [],
professional_summary: '',
cover_letter: '',
certification_files: [],
selfie_url: '',
companies_worked: [],
});
const [customSkill, setCustomSkill] = useState('');
const [customCompany, setCustomCompany] = useState('');
const [uploadingSelfie, setUploadingSelfie] = useState(false);
const [uploadingCert, setUploadingCert] = useState(false);
const [uploadError, setUploadError] = useState('');
const { data: certifications = [] } = useCertifications();
const certOptions = certifications.length > 0 ? certifications.map(c => c.name) : DEFAULT_CERTS;
const update = (field, value) => setForm({ ...form, [field]: value });
const toggleCert = (cert) => {
update('certifications', form.certifications.includes(cert)
? form.certifications.filter(c => c !== cert)
: [...form.certifications, cert]);
};
const toggleAvail = (slot) => {
update('availability', form.availability.includes(slot)
? form.availability.filter(a => a !== slot)
: [...form.availability, slot]);
};
const handleCertUpload = async (e) => {
const files = Array.from(e.target.files || []);
if (files.length === 0) return;
setUploadingCert(true);
try {
const uploaded = [];
for (const file of files) {
const { file_url } = await base44.integrations.Core.UploadFile({ file });
uploaded.push(file_url);
}
update('certification_files', [...form.certification_files, ...uploaded]);
} catch (err) {
// ignore
}
setUploadingCert(false);
e.target.value = '';
};
const removeCertFile = (idx) => {
update('certification_files', form.certification_files.filter((_, i) => i !== idx));
};
const handleSelfieUpload = async (e) => {
const file = e.target.files?.[0];
if (!file) return;
setUploadingSelfie(true);
try {
const { file_url } = await base44.integrations.Core.UploadFile({ file });
update('selfie_url', file_url);
} catch (err) {
// ignore
}
setUploadingSelfie(false);
e.target.value = '';
};
const handleSubmit = (e) => {
e.preventDefault();
if (form.certification_files.length === 0) {
setUploadError('Please upload at least one certification document to continue.');
return;
}
if (!form.selfie_url) {
setUploadError("Please take a selfie photo before submitting — it is required for your interview.");
return;
}
setUploadError('');
onSubmit(form);
};
return (
<form onSubmit={handleSubmit} className="space-y-5">
{/* The Resume Dies — identity-first. Create yours at /identity */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<Label className="text-[13px] text-[#374151]">Full Name *</Label>
<Input value={form.applicant_name} onChange={(e) => update('applicant_name', e.target.value)} required className="mt-1.5" />
</div>
<div>
<Label className="text-[13px] text-[#374151]">Email *</Label>
<Input type="email" value={form.email} onChange={(e) => update('email', e.target.value)} required className="mt-1.5" />
</div>
<div>
<Label className="text-[13px] text-[#374151]">Phone</Label>
<Input value={form.phone} onChange={(e) => update('phone', e.target.value)} className="mt-1.5" />
</div>
<div>
<Label className="text-[13px] text-[#374151]">Years of Experience</Label>
<Input type="number" min="0" value={form.years_experience} onChange={(e) => update('years_experience', parseInt(e.target.value) || 0)} className="mt-1.5" />
</div>
<div>
<Label className="text-[13px] text-[#374151]">English Level</Label>
<select value={form.english_level} onChange={(e) => update('english_level', e.target.value)} className="w-full mt-1.5 h-9 rounded-md border border-[#E5E7EB] bg-white px-3 text-[13px] focus:border-[#0838E0] focus:outline-none">
{ENGLISH_LEVELS.map(l => <option key={l} value={l}>{l.charAt(0).toUpperCase() + l.slice(1)}</option>)}
</select>
</div>
</div>
<div>
<Label className="text-[13px] text-[#374151]">Skills</Label>
<div className="flex gap-2 mt-1.5">
<Input value={customSkill} onChange={(e) => setCustomSkill(e.target.value)} placeholder="Add a skill..." className="flex-1" />
<Button type="button" variant="outline" onClick={() => { if (customSkill.trim()) { update('skills', [...form.skills, customSkill.trim()]); setCustomSkill(''); } }}>Add</Button>
</div>
{form.skills.length > 0 && (
<div className="flex flex-wrap gap-1.5 mt-2">
{form.skills.map((s, i) => (
<span key={i} className="inline-flex items-center gap-1 px-2 py-1 rounded-full bg-[#F3F4F6] text-[12px] text-[#374151]">
{s}
<button type="button" onClick={() => update('skills', form.skills.filter((_, idx) => idx !== i))} className="text-[#9CA3AF] hover:text-[#EF4444]">×</button>
</span>
))}
</div>
)}
</div>
<div>
<Label className="text-[13px] text-[#374151]">Companies You've Worked At</Label>
<div className="flex gap-2 mt-1.5">
<Input value={customCompany} onChange={(e) => setCustomCompany(e.target.value)} placeholder="e.g. Google, Meta, Marriott..." className="flex-1" />
<Button type="button" variant="outline" onClick={() => { if (customCompany.trim()) { update('companies_worked', [...form.companies_worked, customCompany.trim()]); setCustomCompany(''); } }}>Add</Button>
</div>
{form.companies_worked.length > 0 && (
<div className="flex flex-wrap gap-1.5 mt-2">
{form.companies_worked.map((c, i) => (
<span key={i} className="inline-flex items-center gap-1 px-2 py-1 rounded-full bg-[#EEF3FE] text-[12px] text-[#1E40AF] font-medium border border-[#DBEAFE]">
{c}
<button type="button" onClick={() => update('companies_worked', form.companies_worked.filter((_, idx) => idx !== i))} className="text-[#9CA3AF] hover:text-[#EF4444]">×</button>
</span>
))}
</div>
)}
</div>
<div>
<Label className="text-[13px] text-[#374151]">Certifications</Label>
<div className="flex flex-wrap gap-2 mt-1.5">
{certOptions.map(cert => (
<button
key={cert}
type="button"
onClick={() => toggleCert(cert)}
className={`px-3 py-1.5 rounded-full text-[12px] font-medium border transition-all ${
form.certifications.includes(cert) ? 'bg-[#0838E0] border-[#0838E0] text-white' : 'border-[#E5E7EB] text-[#6B7280] hover:border-[#D1D5DB]'
}`}
>
{cert}
</button>
))}
</div>
{/* Certification file upload */}
<div className="mt-3">
<Label className="text-[12px] text-[#6B7280]">Upload certification documents</Label>
<label className="mt-1.5 flex flex-col items-center justify-center gap-1.5 border-2 border-dashed border-[#E5E7EB] rounded-lg py-4 cursor-pointer hover:border-[#0838E0] hover:bg-[#EEF3FE]/40 transition-colors">
{uploadingCert ? (
<Loader2 className="w-5 h-5 text-[#0838E0] animate-spin" />
) : (
<UploadCloud className="w-5 h-5 text-[#9CA3AF]" />
)}
<span className="text-[12px] text-[#6B7280]">
{uploadingCert ? 'Uploading...' : 'Click to upload (PDF, JPG, PNG)'}
</span>
<input
type="file"
accept=".pdf,.jpg,.jpeg,.png"
multiple
onChange={handleCertUpload}
disabled={uploadingCert}
className="hidden"
/>
</label>
{uploadError && (
<p className="text-[12px] text-[#EF4444] mt-1">{uploadError}</p>
)}
{form.certification_files.length > 0 && (
<div className="mt-2 space-y-1.5">
{form.certification_files.map((url, i) => (
<div key={i} className="flex items-center justify-between gap-2 px-3 py-1.5 rounded-lg bg-[#F0FDF4] border border-[#BBF7D0]">
<span className="flex items-center gap-1.5 text-[12px] text-[#166534] truncate">
<FileCheck2 className="w-3.5 h-3.5 shrink-0" />
{decodeURIComponent(url.split('/').pop()?.split('?')[0] || `Certification ${i + 1}`)}
</span>
<button type="button" onClick={() => removeCertFile(i)} className="text-[#9CA3AF] hover:text-[#EF4444] shrink-0">
<X className="w-3.5 h-3.5" />
</button>
</div>
))}
</div>
)}
</div>
</div>
{/* Selfie Photo */}
<div>
<Label className="text-[13px] text-[#374151]">Selfie Photo *</Label>
<p className="text-[11px] text-[#6B7280] mt-0.5 mb-2">Take a quick selfie so your interviewer can see you before the interview.</p>
{form.selfie_url ? (
<div className="flex items-center gap-3">
<img src={form.selfie_url} alt="Selfie" className="w-20 h-20 rounded-xl object-cover border border-[#E5E7EB]" />
<button
type="button"
onClick={() => update('selfie_url', '')}
className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-[#F3F4F6] hover:bg-[#E5E7EB] text-[#374151] font-medium text-[13px] transition-colors"
>
<RefreshCw className="w-3.5 h-3.5" /> Retake
</button>
</div>
) : (
<label className="flex items-center gap-3 border-2 border-dashed border-[#E5E7EB] rounded-xl p-4 cursor-pointer hover:border-[#0838E0] hover:bg-[#EEF3FE]/40 transition-colors">
{uploadingSelfie ? (
<Loader2 className="w-8 h-8 text-[#0838E0] animate-spin shrink-0" />
) : (
<div className="w-12 h-12 rounded-full bg-[#EEF3FE] flex items-center justify-center shrink-0">
<Camera className="w-6 h-6 text-[#0838E0]" />
</div>
)}
<div className="flex-1">
<p className="text-[13px] font-medium text-[#374151]">
{uploadingSelfie ? 'Uploading...' : 'Take or upload a selfie'}
</p>
<p className="text-[11px] text-[#6B7280]">Click to use your camera or select a photo</p>
</div>
<input
type="file"
accept="image/*"
capture="user"
onChange={handleSelfieUpload}
disabled={uploadingSelfie}
className="hidden"
/>
</label>
)}
</div>
<div>
<Label className="text-[13px] text-[#374151]">Availability</Label>
<div className="flex flex-wrap gap-2 mt-1.5">
{AVAIL_OPTIONS.map(slot => (
<button
key={slot}
type="button"
onClick={() => toggleAvail(slot)}
className={`px-3 py-1.5 rounded-full text-[12px] font-medium border transition-all ${
form.availability.includes(slot) ? 'bg-[#059669] border-[#059669] text-white' : 'border-[#E5E7EB] text-[#6B7280] hover:border-[#D1D5DB]'
}`}
>
{slot}
</button>
))}
</div>
</div>
<div>
<Label className="text-[13px] text-[#374151]">Professional Summary</Label>
<textarea value={form.professional_summary} onChange={(e) => update('professional_summary', e.target.value)} className="w-full mt-1.5 min-h-[70px] p-3 text-[13px] rounded-lg border border-[#E5E7EB] focus:border-[#0838E0] focus:outline-none resize-none" />
</div>
<Button type="submit" disabled={submitting} className="w-full bg-[#0838E0] hover:bg-[#062BAF] h-11 text-[14px]">
{submitting ? <Loader2 className="w-4 h-4 mr-2 animate-spin" /> : null}
Submit Application
</Button>
</form>
);
}

View File

@@ -1,20 +0,0 @@
import React from 'react';
import { Award } from 'lucide-react';
import { cn } from '@/lib/utils';
const LEVEL_STYLES = {
bronze: 'bg-[#FEF3C7] text-[#92400E] border-[#FCD34D]',
silver: 'bg-[#F3F4F6] text-[#4B5563] border-[#D1D5DB]',
gold: 'bg-[#FEF9E7] text-[#92400E] border-[#F9E547]',
platinum: 'bg-[#EEF3FE] text-[#1E40AF] border-[#BFDBFE]',
};
export default function BadgeChip({ name, level = 'bronze', earned_date, size = 'md' }) {
return (
<div className={cn('inline-flex items-center gap-1.5 rounded-full border px-3 py-1.5', LEVEL_STYLES[level] || LEVEL_STYLES.bronze)}>
<Award className={size === 'sm' ? 'w-3 h-3' : 'w-3.5 h-3.5'} />
<span className={cn('font-medium', size === 'sm' ? 'text-[11px]' : 'text-[12px]')}>{name}</span>
{earned_date && <span className="text-[10px] opacity-70">· {new Date(earned_date).toLocaleDateString()}</span>}
</div>
);
}

View File

@@ -117,7 +117,7 @@ export default function CandidateCard({ application, jobTitle, rank, onAction, o
try {
await base44.entities.JobApplication.update(application.id, { status: 'rejected' });
toast.success(`${application.applicant_name} declined`);
} catch (_err) {
} catch {
toast.error('Failed to decline candidate');
}
}
@@ -131,7 +131,7 @@ export default function CandidateCard({ application, jobTitle, rank, onAction, o
try {
await base44.entities.JobApplication.delete(application.id);
toast.success(`${application.applicant_name} deleted`);
} catch (_err) {
} catch {
toast.error('Failed to delete candidate');
}
}

View File

@@ -1,50 +0,0 @@
import React from 'react';
// A learning path rendered as a quiet vertical journey to the target role.
export default function CareerJourney({ path, completedIds = [] }) {
const steps = path.steps || [];
const target = path.target_role || path.name;
const allDone = steps.length > 0 && steps.every((s) => completedIds.has(s.course_id));
const rungs = [
...steps.map((s, i) => ({
label: s.course_title || `Step ${i + 1}`,
done: completedIds.has(s.course_id),
})),
{ label: target, done: allDone, isTarget: true },
];
const doneCount = steps.filter((s) => completedIds.has(s.course_id)).length;
const pct = steps.length ? Math.round((doneCount / steps.length) * 100) : 0;
return (
<div className="py-6">
<div className="flex items-baseline justify-between mb-5">
<h3 className="text-[17px] font-semibold text-[#0F172A]">{path.name}</h3>
<span className="text-[13px] text-[#64748B]">{pct}%</span>
</div>
{path.description && <p className="text-[13px] text-[#64748B] mb-4">{path.description}</p>}
<div className="relative pl-6">
<div className="absolute left-[7px] top-2 bottom-2 w-px bg-[#E2E8F0]" />
{rungs.map((r, i) => (
<div key={i} className="relative flex items-center gap-3 py-2">
<div
className={`absolute -left-6 w-3.5 h-3.5 rounded-full ${
r.done ? 'bg-[#0838E0]' : 'bg-white border-2 border-[#E2E8F0]'
}`}
/>
<span
className={`text-[15px] ${
r.isTarget
? 'font-semibold text-[#0838E0]'
: r.done
? 'text-[#0F172A] font-medium'
: 'text-[#64748B]'
}`}
>
{r.label}
</span>
</div>
))}
</div>
</div>
);
}

View File

@@ -1,43 +0,0 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { ArrowRight } from 'lucide-react';
/** @param {any} props */
function Stage({ label, value, sub = '', accent = false, to = null }) {
const body = (
<div className="text-center sm:text-left">
<div className="text-[11px] font-medium uppercase tracking-wider text-[#94A3B8]">{label}</div>
<div className={`text-[20px] sm:text-[26px] font-semibold mt-1 tracking-tight ${accent ? 'text-[#0838E0]' : 'text-[#0F172A]'}`}>
{value}
</div>
{sub && <div className="text-[13px] text-[#64748B] mt-0.5">{sub}</div>}
</div>
);
return to ? (
<Link to={to} className="hover:opacity-80 transition-opacity">{body}</Link>
) : body;
}
export default function CareerTrajectory({ profile, nextPath, nextReady, futureRole, futureIncome }) {
const current = profile?.current_position || profile?.desired_position || 'Starting Out';
const next = nextPath?.target_role || nextPath?.name || 'Next Role';
return (
<section className="text-center pt-4 pb-12 sm:pt-8 sm:pb-20">
<span className="text-[12px] font-medium tracking-wide text-[#0838E0] uppercase">Your Career</span>
<h1 className="text-[32px] sm:text-[44px] font-bold tracking-tight text-[#0F172A] mt-4 leading-[1.05]">
Everything revolves around your future.
</h1>
<p className="text-[15px] text-[#64748B] mt-4 max-w-sm mx-auto">
Not a university. A Career Acceleration Engine.
</p>
<div className="mt-12 sm:mt-16 flex flex-col sm:flex-row items-center justify-center gap-5 sm:gap-10">
<Stage label="Current" value={current} />
<ArrowRight className="w-4 h-4 text-[#CBD5E1] rotate-90 sm:rotate-0" />
<Stage label="Next Role" value={next} sub={`${nextReady}% ready`} accent to={nextPath ? '/university' : null} />
<ArrowRight className="w-4 h-4 text-[#CBD5E1] rotate-90 sm:rotate-0" />
<Stage label="Future" value={futureRole} sub={futureIncome || ' '} />
</div>
</section>
);
}

View File

@@ -20,7 +20,7 @@ export default function CertificationManager() {
await createCert.mutateAsync({ name });
setNewCert('');
toast.success(`"${name}" added to certifications`);
} catch (e) {
} catch {
toast.error('Failed to add certification');
}
};
@@ -29,7 +29,7 @@ export default function CertificationManager() {
try {
await deleteCert.mutateAsync(cert.id);
toast.success(`"${cert.name}" removed`);
} catch (e) {
} catch {
toast.error('Failed to remove certification');
}
};

View File

@@ -2,7 +2,7 @@ import React from 'react';
import { motion } from 'framer-motion';
import { Clock, DollarSign, Award, Zap, Scale, Heart } from 'lucide-react';
export default function ImpactMetrics({ applications, interviews, staff }) {
export default function ImpactMetrics({ applications, interviews, staff: _staff }) {
const hired = applications.filter(a => a.status === 'hired');
const screened = applications.filter(a => a.ai_score > 0);

View File

@@ -15,7 +15,7 @@ function getTier(score) {
return 3;
}
export default function KROWCandidateLeaderboard({ applications, maxBars = 4 }) {
export default function KROWCandidateLeaderboard({ applications }) {
const screened = applications.filter(a => a.ai_score > 0 || a.status !== 'applied');
const counts = TIERS.map((_, i) => screened.filter(a => getTier(a.ai_score || 0) === i).length);
const total = counts.reduce((a, b) => a + b, 0) || 1;

View File

@@ -1,24 +0,0 @@
import React from 'react';
// Apple-style focal statement: a quiet vertical list beside a single thin divider.
export default function KrowSays() {
const lines = ['Learn this.', 'Prove it.', 'Get hired.'];
return (
<div>
<p className="text-[13px] font-medium text-[#333333] mb-6">KROW says</p>
<div className="relative pl-6">
<div className="absolute left-0 top-2 bottom-2 w-px bg-[#CCCCCC]" />
<ul className="space-y-5 sm:space-y-6">
{lines.map((l) => (
<li
key={l}
className="text-[26px] sm:text-[34px] font-semibold tracking-tight text-[#333333] leading-tight"
>
{l}
</li>
))}
</ul>
</div>
</div>
);
}

View File

@@ -1,20 +0,0 @@
import React from 'react';
export default function KrowScoreRing({ score = 0, size = 64, label = 'KROW Score' }) {
const radius = (size - 8) / 2;
const circ = 2 * Math.PI * radius;
const offset = circ - (Math.min(100, score) / 100) * circ;
const color = score >= 80 ? '#0838E0' : score >= 60 ? '#333F48' : score >= 40 ? '#6B7280' : '#EF4444';
return (
<div className="flex flex-col items-center">
<div className="relative flex items-center justify-center" style={{ width: size, height: size }}>
<svg width={size} height={size} className="-rotate-90">
<circle cx={size / 2} cy={size / 2} r={radius} fill="none" stroke="#E5E7EB" strokeWidth="5" />
<circle cx={size / 2} cy={size / 2} r={radius} fill="none" stroke={color} strokeWidth="5" strokeDasharray={circ} strokeDashoffset={offset} strokeLinecap="round" />
</svg>
<span className="absolute font-bold text-[#111827]" style={{ fontSize: size * 0.28 }}>{score}</span>
</div>
{label && <span className="text-[11px] text-[#6B7280] mt-1">{label}</span>}
</div>
);
}

View File

@@ -1,28 +0,0 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { ArrowRight, Route } from 'lucide-react';
export default function LearningPathCard({ path, completedIds = [] }) {
const steps = path.steps || [];
const done = steps.filter(s => completedIds.has(s.course_id)).length;
const pct = steps.length ? Math.round((done / steps.length) * 100) : 0;
return (
<Link to="/university" className="block bg-white border border-[#E5E7EB] rounded-xl p-5 hover:border-[#0838E0] hover:shadow-sm transition-all">
<div className="flex items-center gap-2 mb-1">
<div className="w-8 h-8 rounded-lg bg-[#0838E0] flex items-center justify-center shrink-0"><Route className="w-4 h-4 text-white" /></div>
<h3 className="font-semibold text-[#111827] text-[15px]">{path.name}</h3>
</div>
<p className="text-[12px] text-[#6B7280] mb-3">{path.description || `Career track to ${path.target_role || path.name}`}</p>
<div className="flex flex-wrap items-center gap-1.5 mb-3">
{steps.flatMap((s, i) => [
<span key={`s${i}`} className={`text-[11px] px-2 py-0.5 rounded-full ${completedIds.has(s.course_id) ? 'bg-[#F0FDF4] text-[#166534]' : 'bg-[#F3F4F6] text-[#6B7280]'}`}>{s.course_title || `Step ${i + 1}`}</span>,
i < steps.length - 1 ? <ArrowRight key={`a${i}`} className="w-3 h-3 text-[#D1D5DB]" /> : null,
])}
</div>
<div className="flex items-center gap-2">
<div className="flex-1 h-2 rounded-full bg-[#F3F4F6] overflow-hidden"><div className="h-full bg-[#0838E0]" style={{ width: `${pct}%` }} /></div>
<span className="text-[11px] font-medium text-[#6B7280]">{done}/{steps.length}</span>
</div>
</Link>
);
}

View File

@@ -1,37 +0,0 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { ArrowRight } from 'lucide-react';
import OwliverAvatar from '@/components/krow/OwliverAvatar';
export default function OwliverCoachCard({ nextCourse, matchCount, avgPay }) {
return (
<section className="py-12 sm:py-16 border-t border-[#F1F5F9]">
<div className="flex flex-col items-center text-center max-w-xl mx-auto">
<OwliverAvatar className="w-14 h-14" rounded="rounded-full" />
<h2 className="text-[12px] font-medium text-[#94A3B8] uppercase tracking-wider mt-4">Owliver · AI Coach</h2>
<p className="text-[18px] sm:text-[22px] font-medium text-[#0F172A] mt-3 leading-snug">
{nextCourse ? (
<>
Complete <span className="text-[#0838E0]">{nextCourse.title}</span> to unlock stronger matches.
</>
) : (
<>Keep building your profile and I'll map your fastest path forward.</>
)}
</p>
<p className="text-[13px] text-[#64748B] mt-3">
{matchCount > 0
? `${matchCount} open roles match you now${avgPay ? ` · avg ${avgPay}` : ''}`
: 'No matches yet — keep learning.'}
</p>
{nextCourse && (
<Link
to={`/university/${nextCourse.id}`}
className="mt-7 inline-flex items-center gap-1.5 bg-[#0838E0] text-white text-[14px] font-medium px-6 py-3 rounded-full hover:bg-[#062BAF] transition-colors"
>
Start Learning <ArrowRight className="w-4 h-4" />
</Link>
)}
</div>
</section>
);
}

View File

@@ -49,7 +49,7 @@ export default function RateEndorseModal({ open, onClose, staff, reviewerName })
});
toast.success(`Rated ${staff.name}`);
onClose();
} catch (e) {
} catch {
toast.error('Failed to save rating');
}
};

View File

@@ -21,7 +21,7 @@ function buildVoices(application) {
const years = application?.years_experience || 0;
const source = companies.length > 0
? companies.slice(0, 3)
: strengths.slice(0, 3).map((s) => 'Verified Reference');
: strengths.slice(0, 3).map(() => 'Verified Reference');
if (source.length === 0) return [];
return source.map((c, i) => {
const name = SUPERVISOR_NAMES[i % SUPERVISOR_NAMES.length];

View File

@@ -1,34 +0,0 @@
import React from 'react';
import { Briefcase, User } from 'lucide-react';
import { cn } from '@/lib/utils';
const ROLES = [
{ value: 'recruiter', label: 'Recruiter / Admin', desc: 'Hire and manage candidates', icon: Briefcase },
{ value: 'applicant', label: 'Applicant', desc: 'Apply to open positions', icon: User },
];
export default function RoleSelector({ value, onChange }) {
return (
<div className="grid grid-cols-2 gap-3">
{ROLES.map((r) => {
const Icon = r.icon;
const active = value === r.value;
return (
<button
key={r.value}
type="button"
onClick={() => onChange(r.value)}
className={cn(
'p-4 rounded-xl border-2 text-left transition-all',
active ? 'border-[#0838E0] bg-[#EEF3FE]' : 'border-[#E5E7EB] hover:border-[#D1D5DB]'
)}
>
<Icon className={cn('w-5 h-5 mb-2', active ? 'text-[#0838E0]' : 'text-[#9CA3AF]')} />
<p className={cn('text-[13px] font-semibold', active ? 'text-[#1E40AF]' : 'text-[#374151]')}>{r.label}</p>
<p className="text-[11px] text-[#6B7280] mt-0.5">{r.desc}</p>
</button>
);
})}
</div>
);
}

View File

@@ -1,63 +0,0 @@
import React from 'react';
// Skill levels: Claimed → Practiced → Verified → Expert → Elite
const LEVELS = [
{ name: 'Claimed', fill: 35, bar: 'bg-[#CBD5E1]', text: 'text-[#94A3B8]' },
{ name: 'Practiced', fill: 62, bar: 'bg-[#93C5FD]', text: 'text-[#3B82F6]' },
{ name: 'Verified', fill: 84, bar: 'bg-[#0838E0]', text: 'text-[#0838E0]' },
{ name: 'Expert', fill: 94, bar: 'bg-[#6366F1]', text: 'text-[#6366F1]' },
{ name: 'Elite', fill: 100, bar: 'bg-[#F9E547]', text: 'text-[#92400E]' },
];
function levelFor(skill, badges, courses) {
const s = skill.toLowerCase();
const certHits = (badges || []).filter((b) => (b.name || '').toLowerCase().includes(s)).length;
const courseHits = (courses || []).filter((c) => (c.title || '').toLowerCase().includes(s)).length;
const score = certHits * 2 + courseHits;
if (score >= 6) return 4;
if (score >= 3) return 3;
if (certHits >= 1) return 2;
if (courseHits >= 1) return 1;
return 0;
}
export default function SkillDNA({ profile }) {
const skills = profile?.skills || [];
const badges = profile?.earned_badges || [];
const courses = profile?.completed_courses || [];
return (
<section className="py-12 sm:py-16 border-t border-[#F1F5F9]">
<h2 className="text-[22px] sm:text-[26px] font-semibold text-[#0F172A] tracking-tight">Skill DNA</h2>
<p className="text-[14px] text-[#64748B] mt-2 max-w-md">
Every shift updates these bars. Employers trust them.
</p>
{!skills.length ? (
<p className="text-[14px] text-[#94A3B8] mt-8">
Add skills to your profile to start tracking your Skill DNA.
</p>
) : (
<div className="mt-8 space-y-5 max-w-2xl">
{skills.map((sk, i) => {
const lvl = LEVELS[levelFor(sk, badges, courses)];
return (
<div key={i}>
<div className="flex items-center justify-between mb-1.5">
<span className="text-[14px] font-medium text-[#0F172A]">{sk}</span>
<span className={`text-[12px] font-medium ${lvl.text}`}>{lvl.name}</span>
</div>
<div className="h-2 rounded-full bg-[#F1F5F9] overflow-hidden">
<div
className={`h-full ${lvl.bar} rounded-full transition-all duration-500`}
style={{ width: `${lvl.fill}%` }}
/>
</div>
</div>
);
})}
</div>
)}
</section>
);
}

View File

@@ -24,13 +24,6 @@ function getPortraitSeed(name) {
return PORTRAIT_PHOTOS[h % PORTRAIT_PHOTOS.length];
}
const MATCH_LABEL_STYLES = {
'Strong Match': 'bg-[#EEF3FE] text-[#0838E0]',
'Good Match': 'bg-[#F0FDF4] text-[#16A34A]',
'Possible Fit': 'bg-[#FFFBEB] text-[#D97706]',
'Weak Fit': 'bg-[#F3F4F6] text-[#6B7280]',
};
const RECO_STYLES = {
Hire: 'bg-[#16A34A] text-white',
Interview: 'bg-[#0838E0] text-white',
@@ -53,7 +46,7 @@ export default function SuggestedTalent({ job, onContact, onSchedule, onHire })
try {
const res = await matchTalent.mutateAsync({ job, profiles });
if (!cancelled && res?.matches) setMatches(res.matches);
} catch (e) {
} catch {
if (!cancelled) setMatches([]);
}
})();

View File

@@ -6,25 +6,6 @@ import CareerScoreGauge from '@/components/krow/talent/CareerScoreGauge';
import ExperienceEndorsements, { verifiedExperience } from '@/components/krow/talent/ExperienceEndorsements';
import EliteTalentScore from '@/components/krow/talent/EliteTalentScore';
const PORTRAIT_PHOTOS = [
'photo-1500648767731-7d3514ad8b31',
'photo-1531123897727-8f129e1688ce',
'photo-1573496359142-b8d87734a5a2',
'photo-1487412720507-e7ab37603c6f',
'photo-1502685104226-ee3a97a2f2c8',
'photo-1544005313-94ddf0286df2',
'photo-1494790108377-be9c29b29330',
'photo-1438761681033-6461ffad8d80',
'photo-1517841905240-472988babdf9',
'photo-1463453091185-61582044d556',
];
function getPortraitSeed(name) {
let h = 0;
for (let i = 0; i < (name || '').length; i++) h = (h * 31 + name.charCodeAt(i)) >>> 0;
return PORTRAIT_PHOTOS[h % PORTRAIT_PHOTOS.length];
}
function getInitials(name) {
return (name || '').split(' ').map(w => w[0]).slice(0, 2).join('').toUpperCase() || '??';
}
@@ -74,35 +55,17 @@ function PillList({ items, className = 'bg-[#F0F4F8] text-[#0047AB]' }) {
);
}
function ScoreFactor({ label, value }) {
return (
<div className="inline-flex items-center justify-between gap-2 px-3 py-1.5 rounded-full border border-[#E5E7EB] bg-white">
<span className="text-[11px] text-[#6B7280] font-medium">{label}</span>
<span className="text-[12px] font-bold text-[#2563EB]">{value}</span>
</div>
);
}
/** @param {any} props */
export default function TalentDetailModal({ open, onClose, profile, onContact, onSchedule, onHire, matchScore, matchReasons }) {
if (!profile) return null;
const badges = profile.earned_badges || [];
const capabilities = (profile.capabilities || []).filter((c) => c.status === 'verified' || c.status === 'pending');
const experience = profile.experience || [];
const strengths = profile.strengths || [];
const verifiedExperienceList = verifiedExperience(profile).length > 0;
const krowCode = getKrowIdCode(profile);
const reportDate = formatDate(profile.updated_date || new Date());
const openToWork = profile.status === 'active' && (profile.availability?.length > 0 || !profile.current_position);
const reliability = Math.round(profile.reliability_score || 0);
const attendance = Math.round(profile.attendance_score || 0);
const performance = Math.round(profile.performance_score || 0);
const leadership = Math.round(profile.leadership_potential || 0);
const aiInterview = Math.round(profile.ai_interview_score || 0);
const xp = Math.round(profile.xp || 0);
const identityVerified = capabilities.length > 0 || badges.length > 0;
const identityTag = identityVerified ? 'Verified' : 'Pending';
const identityClass = identityVerified ? 'bg-[#EEF3FE] text-[#1E40AF]' : 'bg-[#FFF7ED] text-[#B45309]';

View File

@@ -1,114 +0,0 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { motion } from 'framer-motion';
import {
Fingerprint, GraduationCap, Mic, BadgeCheck, Handshake, Briefcase,
LineChart, Star, TrendingUp, Crown, Presentation, Repeat, ArrowRight,
} from 'lucide-react';
// The Credit Bureau for Human Talent — the career flywheel.
const STEPS = [
{ key: 'identity', label: 'Create Identity', to: '/identity', icon: Fingerprint },
{ key: 'learn', label: 'Learn', to: '/university', icon: GraduationCap },
{ key: 'interview', label: 'Interview', to: '/owliver', icon: Mic },
{ key: 'verify', label: 'Verify Skills', to: '/identity', icon: BadgeCheck },
{ key: 'hired', label: 'Get Hired', to: '/apply', icon: Handshake },
{ key: 'work', label: 'Work', to: '/me', icon: Briefcase },
{ key: 'track', label: 'Track Performance', to: '/me', icon: LineChart },
{ key: 'reputation', label: 'Earn Reputation', to: '/me', icon: Star },
{ key: 'promoted', label: 'Get Promoted', to: '/university', icon: TrendingUp },
{ key: 'leader', label: 'Become Leader', to: '/university', icon: Crown },
{ key: 'train', label: 'Train Others', to: '/university', icon: Presentation },
{ key: 'repeat', label: 'Repeat', to: '/university', icon: Repeat, loop: true },
];
export function stepStatus(profile, apps = []) {
const hired = apps.some(a => a.status === 'hired');
const worked = (profile.experience || []).length > 0 || hired;
const isDone = {
identity: !!profile.selfie_url && (profile.skills || []).length > 0,
learn: (profile.completed_courses || []).length > 0,
interview: (profile.ai_interview_score || 0) > 0 || !!(profile.personality || profile.strengths?.length),
verify: (profile.certifications || []).length > 0 || (profile.earned_badges || []).length > 0,
hired,
work: worked,
track: (profile.performance_score || 0) > 0 || (profile.attendance_score || 0) > 0 && worked,
reputation: (profile.client_rating || 0) > 0 || (profile.supervisor_rating || 0) > 0 || (profile.krow_score || 0) >= 50,
promoted: (profile.leadership_potential || 0) >= 40,
leader: (profile.leadership_potential || 0) >= 70,
train: (profile.leadership_potential || 0) >= 85 && (profile.completed_courses || []).length >= 3,
repeat: false,
};
return isDone;
}
export default function TalentLoop({ profile, applications = [] }) {
const done = stepStatus(profile, applications);
const reached = STEPS.filter(s => done[s.key]).length;
const current = STEPS.find(s => !done[s.key]) || STEPS[STEPS.length - 1];
const progress = Math.round((reached / (STEPS.length - 1)) * 100);
return (
<section className="glass-card border border-white/60 rounded-2xl p-6 sm:p-8 shadow-sm">
<div className="flex items-end justify-between flex-wrap gap-3 mb-1">
<div>
<span className="text-[11px] font-medium uppercase tracking-[0.18em] text-[#0838E0]">The Talent Loop</span>
<h2 className="text-[22px] font-heading font-bold text-[#0F172A] mt-1 tracking-tight">Build your reputation. Not just your resume.</h2>
</div>
<span className="text-[12px] text-[#64748B]">{reached}/{STEPS.length - 1} stages</span>
</div>
<div className="h-1.5 bg-[#EEF2F7] rounded-full overflow-hidden mt-4 mb-7">
<motion.div
initial={{ width: 0 }}
animate={{ width: `${progress}%` }}
transition={{ duration: 0.8, ease: 'easeOut' }}
className="h-full bg-[#0838E0] rounded-full"
/>
</div>
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-6 gap-3">
{STEPS.map((s, i) => {
const isDone = done[s.key];
const isCurrent = s.key === current.key && !s.loop;
const Icon = s.icon;
return (
<Link
key={s.key}
to={s.to}
className={`group relative flex flex-col items-start gap-2 rounded-xl border p-3 transition-all ${
isDone
? 'bg-[#0838E0] border-[#0838E0] text-white'
: isCurrent
? 'bg-white border-[#0838E0] ring-2 ring-[#0838E0]/20'
: 'bg-white border-[#E5E7EB] hover:border-[#0838E0]/50'
}`}
>
<div className="flex items-center justify-between w-full">
<span className={`text-[10px] font-semibold ${isDone ? 'text-white/70' : 'text-[#94A3B8]'}`}>{String(i + 1).padStart(2, '0')}</span>
{s.loop && <Repeat className={`w-3.5 h-3.5 ${isDone ? 'text-white/70' : 'text-[#94A3B8]'}`} />}
</div>
<Icon className={`w-5 h-5 ${isDone ? 'text-white' : isCurrent ? 'text-[#0838E0]' : 'text-[#64748B]'}`} />
<span className={`text-[12px] font-medium leading-tight ${isDone ? 'text-white' : isCurrent ? 'text-[#0838E0]' : 'text-[#334155]'}`}>
{s.label}
</span>
</Link>
);
})}
</div>
<div className="flex items-center justify-between gap-3 mt-6 pt-5 border-t border-[#EEF2F7] flex-wrap">
<div>
<p className="text-[11px] text-[#94A3B8] font-medium">UP NEXT</p>
<p className="text-[15px] font-semibold text-[#0F172A]">{current.label}</p>
</div>
<Link
to={current.to}
className="inline-flex items-center gap-1.5 text-[13px] font-semibold text-[#0838E0] hover:gap-2.5 transition-all"
>
Continue <ArrowRight className="w-4 h-4" />
</Link>
</div>
</section>
);
}

View File

@@ -1,81 +0,0 @@
import React from 'react';
import { ShieldCheck, Check, Dot } from 'lucide-react';
// KROW verifies everything. This is the trust layer — what's proven vs. claimed.
const ITEMS = [
{ key: 'identity', label: 'Identity', hint: 'Selfie + name' },
{ key: 'certifications', label: 'Certifications', hint: 'Uploaded docs' },
{ key: 'skills', label: 'Skills', hint: 'Declared' },
{ key: 'work', label: 'Work History', hint: 'Experience' },
{ key: 'interview', label: 'AI Interview', hint: 'Passed' },
{ key: 'attendance', label: 'Attendance', hint: 'Shifts' },
{ key: 'reputation', label: 'Reputation', hint: 'Ratings' },
{ key: 'performance', label: 'Performance', hint: 'Score' },
{ key: 'learning', label: 'Learning', hint: 'Courses' },
{ key: 'promotions', label: 'Promotions', hint: 'Growth' },
{ key: 'references', label: 'References', hint: 'Verified' },
];
function statusOf(profile, key) {
const has = (v) => v !== undefined && v !== null && v !== '' && !(Array.isArray(v) && v.length === 0) && v !== 0;
const map = {
identity: has(profile.selfie_url) && has(profile.full_name),
certifications: (profile.certifications || []).length > 0,
skills: (profile.skills || []).length > 0,
work: (profile.experience || []).length > 0,
interview: (profile.ai_interview_score || 0) > 0,
attendance: (profile.attendance_score || 0) > 0 && (profile.experience || []).length > 0,
reputation: (profile.client_rating || 0) > 0 || (profile.supervisor_rating || 0) > 0,
performance: (profile.performance_score || 0) > 0,
learning: (profile.completed_courses || []).length > 0,
promotions: (profile.leadership_potential || 0) >= 40,
references: (profile.supervisor_rating || 0) > 0,
};
return map[key] ? 'verified' : 'pending';
}
export default function TrustLayer({ profile }) {
const verified = ITEMS.filter((i) => statusOf(profile, i.key) === 'verified').length;
return (
<section className="glass-card border border-white/60 rounded-2xl p-6 sm:p-8 shadow-sm">
<div className="flex items-start justify-between gap-4 flex-wrap mb-6">
<div className="flex items-start gap-3">
<div className="w-10 h-10 rounded-xl bg-[#0838E0] flex items-center justify-center shrink-0">
<ShieldCheck className="w-5 h-5 text-white" />
</div>
<div>
<span className="text-[11px] font-medium uppercase tracking-[0.18em] text-[#94A3B8]">Trust Layer</span>
<h2 className="text-[20px] font-heading font-bold text-[#0F172A] tracking-tight">Verified, not claimed.</h2>
<p className="text-[13px] text-[#64748B] mt-0.5">Everything KROW has proven about you. Impossible to fake.</p>
</div>
</div>
<div className="text-right">
<div className="text-[28px] font-heading font-bold text-[#0838E0] leading-none">{verified}<span className="text-[#94A3B8] text-[18px]">/{ITEMS.length}</span></div>
<div className="text-[11px] text-[#94A3B8] font-medium mt-1">verified</div>
</div>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-x-6 gap-y-2.5">
{ITEMS.map((item) => {
const st = statusOf(profile, item.key);
return (
<div key={item.key} className="flex items-center gap-2.5 py-1.5 border-b border-[#F1F5F9]">
{st === 'verified' ? (
<span className="w-5 h-5 rounded-full bg-[#0838E0] flex items-center justify-center shrink-0">
<Check className="w-3 h-3 text-white" />
</span>
) : (
<span className="w-5 h-5 rounded-full bg-[#FEF3C7] flex items-center justify-center shrink-0">
<Dot className="w-4 h-4 text-[#D97706]" />
</span>
)}
<span className={`text-[13px] font-medium ${st === 'verified' ? 'text-[#0F172A]' : 'text-[#64748B]'}`}>{item.label}</span>
<span className="text-[11px] text-[#94A3B8] ml-auto">{st === 'verified' ? 'Verified' : 'Pending'}</span>
</div>
);
})}
</div>
</section>
);
}

View File

@@ -1,38 +0,0 @@
import React from 'react';
import { ShieldCheck, Plus } from 'lucide-react';
// The transcript replacement — verified capabilities as visual chips,
// each backed by evidence. Empty until the worker proves something.
export default function CapabilityTimeline({ profile, compact }) {
const caps = (profile?.capabilities || []).filter((c) => c.status === 'verified' || c.status === 'pending');
return (
<section className={compact ? 'py-2' : 'py-8'}>
<div className="flex items-center justify-between mb-3">
<h2 className="text-[15px] font-semibold text-[#0F172A]">Proven skills</h2>
<span className="text-[11px] text-[#94A3B8]">{caps.length} verified</span>
</div>
{!caps.length ? (
<div className="rounded-2xl border border-dashed border-[#E2E8F0] bg-[#F8FAFC] p-6 text-center">
<div className="w-10 h-10 rounded-full bg-white border border-[#E2E8F0] flex items-center justify-center mx-auto mb-2">
<Plus className="w-4 h-4 text-[#94A3B8]" />
</div>
<p className="text-[12px] text-[#64748B]">No verified skills yet — prove a challenge to start your chain.</p>
</div>
) : (
<div className="flex flex-wrap gap-2">
{caps.map((c, i) => (
<span key={i} className="inline-flex items-center gap-1.5 rounded-full bg-[#F0FDF4] border border-[#BBF7D0] px-3 py-1.5">
<ShieldCheck className="w-3.5 h-3.5 text-[#16A34A]" />
<span className="text-[12px] font-medium text-[#166534]">{c.skill}</span>
{c.verified_date && (
<span className="text-[10px] text-[#16A34A]/70">
{new Date(c.verified_date).toLocaleDateString('en-US', { month: 'short' })}
</span>
)}
</span>
))}
</div>
)}
</section>
);
}

View File

@@ -1,62 +0,0 @@
import React from 'react';
import { ShieldCheck, Award, Languages as LangIcon, MessageSquare, Briefcase, Star, Medal, FolderOpen, FileText } from 'lucide-react';
function WalletTile({ icon: Icon, label, value, tone = 'neutral' }) {
const valueColor = {
verified: 'text-[#10B981]',
neutral: 'text-[#0F172A]',
}[tone];
return (
<div className="rounded-xl bg-white border border-[#E6E8EB] p-4 transition-colors hover:border-[#0838E0]">
<div className="flex items-center gap-1.5 text-[#7C818A] mb-2">
<Icon className="w-3.5 h-3.5" />
<span className="text-[10px] font-semibold uppercase tracking-[0.06em]">{label}</span>
</div>
<div className={`text-[19px] font-bold leading-tight ${valueColor}`}>{value}</div>
</div>
);
}
export default function CareerWallet({ profile }) {
const certs = profile.certifications || [];
const langs = profile.languages || [];
const completed = profile.completed_courses || [];
const badges = profile.earned_badges || [];
const hasInterview = (profile.ai_interview_score || 0) > 0;
const identityOk = !!profile.selfie_url;
return (
<section className="rounded-2xl bg-[#F8F9FB] border border-[#E6E8EB] p-6 sm:p-8">
<div className="flex items-baseline justify-between mb-5">
<div>
<h2 className="text-[19px] font-bold text-[#1A1D21] tracking-tight">Career Wallet</h2>
<p className="text-[12px] text-[#7C818A] mt-0.5">Verified once. Portable everywhere.</p>
</div>
<span className="text-[11px] font-semibold text-[#10B981] hidden sm:inline">Everything verified</span>
</div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
<WalletTile icon={ShieldCheck} label="Identity" value={identityOk ? 'Verified' : '—'} tone={identityOk ? 'verified' : 'neutral'} />
<WalletTile icon={Award} label="Certifications" value={certs.length} />
<WalletTile icon={LangIcon} label="Languages" value={langs.length} />
<WalletTile icon={MessageSquare} label="AI Interview" value={hasInterview ? 'Passed' : '0'} tone={hasInterview ? 'verified' : 'neutral'} />
<WalletTile icon={Briefcase} label="Completed Jobs" value={completed.length} />
<WalletTile icon={Star} label="Employer Rating" value={profile.client_rating ? `${profile.client_rating}` : '—'} />
<WalletTile icon={Medal} label="Badges" value={badges.length} />
<WalletTile icon={FolderOpen} label="Portfolio" value="—" />
<WalletTile icon={FileText} label="Resume" value={profile.resume_url ? 'Yes' : 'Optional'} />
</div>
{certs.length > 0 && (
<div className="flex flex-wrap gap-2 mt-5">
{certs.map((c, i) => (
<span key={i} className="inline-flex items-center gap-1.5 rounded-full bg-white border border-[#E6E8EB] text-[#1A1D21] text-[12px] font-medium px-3 py-1.5">
<Award className="w-3.5 h-3.5 text-[#0838E0]" />{c}
</span>
))}
</div>
)}
</section>
);
}

View File

@@ -1,123 +0,0 @@
import React, { useState } from 'react';
import { Building2, MapPin, Check, X, ArrowLeftRight, Sparkles, Hand } from 'lucide-react';
const STATUS = {
accepted: { txt: 'Accepted', cls: 'bg-[#ECFDF5] text-[#16A34A]', icon: Check },
declined: { txt: 'Declined', cls: 'bg-[#F3F4F6] text-[#6B7280]', icon: X },
countered: { txt: 'Counter sent', cls: 'bg-[#EEF3FE] text-[#0838E0]', icon: ArrowLeftRight },
};
function DemandCard({ rec, decision, counterAsk, onAccept, onDecline, onCounter, onSend }) {
const { job, match } = rec;
const [countering, setCountering] = useState(false);
const [ask, setAsk] = useState(counterAsk ?? job.pay_range_max ?? job.pay_range_min ?? '');
const pay = job.pay_range_min && job.pay_range_max ? `$${job.pay_range_min}–$${job.pay_range_max}/hr` : '';
return (
<div className="rounded-2xl bg-white border border-[#E5E7EB] p-5">
<div className="flex items-start gap-3">
<div className="w-11 h-11 rounded-xl bg-[#EEF3FE] text-[#0838E0] flex items-center justify-center shrink-0">
<Building2 className="w-5 h-5" />
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 flex-wrap">
<h3 className="text-[15px] font-bold text-[#0F172A] truncate">{job.title}</h3>
<span className="inline-flex items-center gap-0.5 text-[11px] font-bold text-[#0838E0] bg-[#EEF3FE] px-2 py-0.5 rounded-full">{match}% match</span>
</div>
<p className="text-[13px] text-[#6B7280] mt-0.5 flex items-center gap-1.5 flex-wrap">
<span className="inline-flex items-center gap-0.5"><MapPin className="w-3 h-3" />{job.location || 'Remote'}</span>
{pay && <span>· {pay}</span>}
<span className="text-[#0838E0] font-medium">wants to interview you</span>
</p>
</div>
</div>
{decision ? (
<div className="mt-4 flex items-center gap-1.5">
{(() => { const S = STATUS[decision]; const I = S.icon; return (
<span className={`inline-flex items-center gap-1.5 text-[12px] font-semibold px-3 py-1.5 rounded-full ${S.cls}`}>
<I className="w-3.5 h-3.5" /> {S.txt}{decision === 'countered' && counterAsk ? ` · $${counterAsk}/hr` : ''}
</span>
); })()}
</div>
) : (
<>
<div className="flex items-center gap-2 mt-4">
<button onClick={() => onAccept(rec)} className="flex-1 inline-flex items-center justify-center gap-1.5 h-9 rounded-xl bg-[#0838E0] text-white text-[13px] font-semibold hover:bg-[#062BAF] transition-colors">
<Check className="w-4 h-4" /> Accept
</button>
<button onClick={() => onDecline(rec)} className="flex-1 inline-flex items-center justify-center gap-1.5 h-9 rounded-xl border border-[#E5E7EB] text-[#6B7280] text-[13px] font-semibold hover:bg-[#F9FAFB] transition-colors">
<X className="w-4 h-4" /> Decline
</button>
<button onClick={() => setCountering(v => !v)} className="flex-1 inline-flex items-center justify-center gap-1.5 h-9 rounded-xl border border-[#0838E0] text-[#0838E0] text-[13px] font-semibold hover:bg-[#EEF3FE] transition-colors">
<ArrowLeftRight className="w-4 h-4" /> Counter
</button>
</div>
{countering && (
<div className="flex items-center gap-2 mt-3 pt-3 border-t border-[#F3F4F6]">
<span className="text-[12px] text-[#6B7280]">Counter at</span>
<input type="number" value={ask} onChange={(e) => setAsk(e.target.value)} className="w-24 h-8 px-2 text-[13px] rounded-lg border border-[#E5E7EB] focus:border-[#0838E0] focus:outline-none" />
<span className="text-[12px] text-[#6B7280]">/hr</span>
<button onClick={() => onSend(rec, ask)} className="ml-auto h-8 px-3 rounded-lg bg-[#0838E0] text-white text-[12px] font-semibold hover:bg-[#062BAF]">Send</button>
</div>
)}
</>
)}
</div>
);
}
export default function CompaniesCompeting({ jobRecs = [] }) {
const [decisions, setDecisions] = useState({});
const [asks, setAsks] = useState({});
const decide = (id, kind, ask) => {
setDecisions((d) => ({ ...d, [id]: kind }));
if (ask != null) setAsks((a) => ({ ...a, [id]: ask }));
};
const live = jobRecs.filter((r) => !decisions[r.job.id]);
return (
<section className="rounded-2xl bg-white border border-[#E5E7EB] p-6 sm:p-8">
<div className="flex items-center justify-between mb-5">
<div>
<h2 className="text-[18px] font-bold text-[#0F172A] flex items-center gap-2">
<Sparkles className="w-4 h-4 text-[#0838E0]" /> Companies are competing for you
</h2>
<p className="text-[13px] text-[#6B7280] mt-0.5">You're in demand. Accept, decline, or counter.</p>
</div>
{live.length > 0 && (
<span className="inline-flex items-center gap-1.5 text-[12px] font-bold text-[#16A34A] bg-[#ECFDF5] px-3 py-1.5 rounded-full">
<span className="relative flex h-2 w-2"><span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-[#16A34A] opacity-60" /><span className="relative inline-flex rounded-full h-2 w-2 bg-[#16A34A]" /></span>
{live.length} live
</span>
)}
</div>
{jobRecs.length === 0 ? (
<div className="text-center py-8">
<div className="w-12 h-12 rounded-2xl bg-[#F9FAFB] text-[#9CA3AF] flex items-center justify-center mx-auto mb-3"><Hand className="w-6 h-6" /></div>
<p className="text-[14px] font-medium text-[#0F172A]">No live offers yet</p>
<p className="text-[13px] text-[#6B7280] mt-1">Keep leveling up — companies will come to you.</p>
</div>
) : (
<div className="space-y-3">
{jobRecs.map((rec) => (
<DemandCard
key={rec.job.id}
rec={rec}
decision={decisions[rec.job.id]}
counterAsk={asks[rec.job.id]}
onAccept={(r) => decide(r.job.id, 'accepted')}
onDecline={(r) => decide(r.job.id, 'declined')}
onCounter={() => {}}
onSend={(r, ask) => decide(r.job.id, 'countered', ask)}
/>
))}
</div>
)}
</section>
);
}

View File

@@ -1,50 +0,0 @@
import React from 'react';
const SHIFTS = ['Morning Shift', 'Evening Shift', 'Weekend', 'Full Time'];
const STATES = ['Interested', 'Interview', 'Offer', 'Offer'];
export default function CompetingOffers({ jobRecs = [] }) {
const offers = (jobRecs || []).slice(0, 4);
return (
<section className="rounded-2xl bg-white border border-[#E5E7EB] p-6">
<h2 className="text-[15px] font-semibold text-[#0F172A]">Companies Competing For You</h2>
<div className="mt-4 space-y-3">
{offers.map((j, i) => {
const pay = j.pay_range_min || 31;
const title = (j.title || 'Opportunity').split(' (')[0];
const state = STATES[i] || 'Interested';
const shift = SHIFTS[i % SHIFTS.length];
return (
<div key={j.id || i} className="rounded-xl border border-[#E5E7EB] p-4">
<div className="flex items-start justify-between">
<div>
<p className="text-[15px] font-semibold text-[#0F172A]">{title}</p>
<div className="mt-1 flex flex-wrap gap-x-3 text-[12px]">
<span className="inline-flex items-center gap-1 text-[#0838E0] font-semibold">
<span className="w-1.5 h-1.5 rounded-full bg-[#0838E0]" /> {state}
</span>
<span className="text-[#0F172A] font-medium">${pay}/hr</span>
<span className="text-[#6B7280]">{shift}</span>
</div>
</div>
{i === 1 && <span className="text-[11px] text-[#94A3B8]">Tomorrow</span>}
</div>
<div className="mt-3 flex gap-2">
<button className="flex-1 text-[12px] font-semibold text-white bg-[#0838E0] rounded-full py-1.5 hover:bg-[#062BAF] transition-colors">
Accept
</button>
<button className="flex-1 text-[12px] font-semibold text-[#0838E0] border border-[#0838E0] rounded-full py-1.5 hover:bg-[#EEF3FE] transition-colors">
Counter
</button>
<button className="text-[12px] font-semibold text-[#94A3B8] border border-[#E5E7EB] rounded-full px-3 py-1.5 hover:text-[#EF4444] hover:border-[#FECACA] transition-colors">
Decline
</button>
</div>
</div>
);
})}
</div>
</section>
);
}

View File

@@ -4,8 +4,6 @@ import { verifiedExperience } from './VerifiedExperienceList';
export { verifiedExperience };
import { clientEndorsements } from '@/lib/talentInsights';
const ENDORSEMENT_ROLES = ['Event Director', 'Operations Lead', 'GM', 'Owner', 'HR Partner'];
function Stars({ count }) {
return (
<div className="flex items-center gap-0.5">

View File

@@ -1,31 +0,0 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { BookOpen } from 'lucide-react';
export default function LearnToday({ courses = [] }) {
const recs = courses.filter((c) => c.status !== 'archived').slice(0, 4);
return (
<section className="rounded-2xl bg-white border border-[#E5E7EB] p-6 sm:p-8">
<h2 className="text-[18px] font-bold text-[#0F172A]">Learn</h2>
<p className="text-[13px] text-[#6B7280] mb-5">Micro-learning. 5-minute lessons. Learning increases value immediately.</p>
{recs.length === 0 ? (
<p className="text-[13px] text-[#6B7280]">No recommendations yet.</p>
) : (
<div className="space-y-2">
{recs.map((c) => (
<Link key={c.id} to={`/university/${c.id}`} className="flex items-center justify-between gap-3 p-3 rounded-xl hover:bg-[#F9FAFB] border border-transparent hover:border-[#E5E7EB] transition-colors">
<div className="flex items-center gap-3">
<div className="w-9 h-9 rounded-lg bg-[#EEF3FE] text-[#0838E0] flex items-center justify-center"><BookOpen className="w-4 h-4" /></div>
<div>
<div className="text-[14px] font-medium text-[#0F172A]">{c.title}</div>
<div className="text-[11px] text-[#6B7280]">{c.estimated_minutes} min · {c.category}</div>
</div>
</div>
<span className="text-[12px] font-bold text-[#0838E0] whitespace-nowrap">+{Math.max(1, Math.round((c.xp || 100) / 20))} Score</span>
</Link>
))}
</div>
)}
</section>
);
}

View File

@@ -1,38 +0,0 @@
import React from 'react';
import { TrendingUp } from 'lucide-react';
import { getTalentScore, toFICO } from '@/lib/talentHome';
export default function ReputationHistory({ profile }) {
const cur = toFICO(getTalentScore(profile));
const pts = [290, 322, 340, cur];
const w = 280;
const h = 72;
const pad = 6;
const min = 280;
const max = 850;
const x = (i) => pad + (i * (w - 2 * pad)) / (pts.length - 1);
const y = (v) => h - pad - ((v - min) / (max - min)) * (h - 2 * pad);
const path = pts.map((v, i) => `${i ? 'L' : 'M'}${x(i).toFixed(1)},${y(v).toFixed(1)}`).join(' ');
return (
<section className="rounded-2xl bg-white border border-[#E5E7EB] p-6">
<div className="flex items-center justify-between">
<h2 className="text-[15px] font-semibold text-[#0F172A]">Reputation History</h2>
<span className="inline-flex items-center gap-1 text-[12px] font-semibold text-[#16A34A]">
<TrendingUp className="w-3.5 h-3.5" /> {cur}
</span>
</div>
<svg viewBox={`0 0 ${w} ${h}`} className="mt-3 w-full">
<path d={path} fill="none" stroke="#0838E0" strokeWidth={2.5} strokeLinecap="round" strokeLinejoin="round" />
{pts.map((v, i) => (
<circle key={i} cx={x(i)} cy={y(v)} r={3} fill="#0838E0" />
))}
</svg>
<div className="flex justify-between text-[11px] text-[#94A3B8] mt-1">
{pts.map((v, i) => (
<span key={i}>{v}</span>
))}
</div>
</section>
);
}

View File

@@ -1,44 +0,0 @@
import React from 'react';
import { getFactors } from '@/lib/talentHome';
function statusFor(v) {
if (v >= 80) return { txt: 'Excellent', color: 'text-[#16A34A]', dot: 'bg-[#16A34A]' };
if (v >= 60) return { txt: 'Good', color: 'text-[#0838E0]', dot: 'bg-[#0838E0]' };
if (v >= 40) return { txt: 'Fair', color: 'text-[#D97706]', dot: 'bg-[#D97706]' };
if (v > 0) return { txt: 'Poor', color: 'text-[#DC2626]', dot: 'bg-[#DC2626]' };
return { txt: 'No data', color: 'text-[#9CA3AF]', dot: 'bg-[#D1D5DB]' };
}
function Factor({ label, value, weight }) {
const s = statusFor(value);
return (
<div className="flex items-center gap-3 py-2.5 border-b border-[#F3F4F6] last:border-0">
<span className={`w-2 h-2 rounded-full shrink-0 ${s.dot}`} />
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between">
<span className="text-[13px] font-medium text-[#0F172A] truncate">{label}</span>
<span className="text-[11px] text-[#9CA3AF]">{weight}% weight</span>
</div>
<div className="h-1.5 bg-[#F3F4F6] rounded-full mt-1.5 overflow-hidden">
<div className="h-full bg-[#93C5FD] rounded-full" style={{ width: `${value}%` }} />
</div>
</div>
<span className={`text-[12px] font-semibold w-20 text-right ${s.color}`}>{s.txt}</span>
</div>
);
}
export default function TalentCreditScore({ profile }) {
const factors = getFactors(profile);
return (
<section className="rounded-2xl bg-white border border-[#E5E7EB] p-6 sm:p-8">
<div className="flex items-center justify-between mb-3">
<h2 className="text-[18px] font-bold text-[#0F172A]">What's Driving Your Score</h2>
<span className="text-[11px] text-[#9CA3AF]">Updated this month</span>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-x-8">
{factors.map((f) => <Factor key={f.key} label={f.label} value={f.value} weight={f.weight} />)}
</div>
</section>
);
}

View File

@@ -1,37 +0,0 @@
import React from 'react';
import { Star } from 'lucide-react';
export default function VerifiedExperience({ profile }) {
const fromExp = (profile.experience || []).map((e) => ({
company: e.company || 'Company',
shifts: Math.max(1, Math.round((e.years || 1) * 20)),
}));
const list = fromExp.length
? fromExp
: [
{ company: 'Google', shifts: 58 },
{ company: 'Adobe', shifts: 26 },
{ company: 'EA', shifts: 15 },
];
return (
<section className="rounded-2xl bg-white border border-[#E5E7EB] p-6">
<h2 className="text-[15px] font-semibold text-[#0F172A]">Verified Experience</h2>
<div className="mt-4 space-y-2">
{list.map((x, i) => (
<div key={i} className="flex items-center justify-between rounded-xl border border-[#E5E7EB] p-3">
<div>
<p className="text-[14px] font-semibold text-[#0F172A]">{x.company}</p>
<p className="text-[12px] text-[#16A34A] font-medium">{x.shifts} shifts</p>
</div>
<div className="flex">
{Array.from({ length: 5 }).map((_, s) => (
<Star key={s} className="w-3.5 h-3.5 text-[#F59E0B] fill-[#F59E0B]" />
))}
</div>
</div>
))}
</div>
</section>
);
}

View File

@@ -53,7 +53,7 @@ function Stars({ count }) {
// Verified Experience & Network — employer-facing list of the worker's last
// 3 jobs with time worked, a review rating, and reference / endorse actions.
export default function VerifiedExperienceList({ profile, onReference, onEndorse }) {
export default function VerifiedExperienceList({ profile, onReference }) {
const list = verifiedExperience(profile);
if (list.length === 0) return null;

View File

@@ -1,58 +0,0 @@
import React from 'react';
// Scallop radius for the flower-like outer border
const SCALLOP_R = (R, n) => R * Math.sin(Math.PI / n);
function scallopPath(cx, cy, R, n) {
const r = SCALLOP_R(R, n);
const pts = [];
for (let i = 0; i < n; i++) {
const a = (i / n) * 2 * Math.PI - Math.PI / 2;
pts.push([cx + R * Math.cos(a), cy + R * Math.sin(a)]);
}
let d = `M ${pts[0][0].toFixed(2)} ${pts[0][1].toFixed(2)} `;
for (let i = 0; i < n; i++) {
const p2 = pts[(i + 1) % n];
d += `A ${r.toFixed(2)} ${r.toFixed(2)} 0 0 0 ${p2[0].toFixed(2)} ${p2[1].toFixed(2)} `;
}
return d + 'Z';
}
function starPath(cx, cy, outer, inner, points = 5) {
let d = '';
for (let i = 0; i < points * 2; i++) {
const r = i % 2 === 0 ? outer : inner;
const a = (i / (points * 2)) * 2 * Math.PI - Math.PI / 2;
const x = cx + r * Math.cos(a);
const y = cy + r * Math.sin(a);
d += `${i === 0 ? 'M' : 'L'} ${x.toFixed(2)} ${y.toFixed(2)} `;
}
return d + 'Z';
}
export default function VerifiedStamp({ size = 26, className = '' }) {
const cx = 50, cy = 50, R = 44, n = 12;
const green = '#9FD34E';
const textPath = `M ${cx} ${cy - 36} A 36 36 0 1 1 ${cx - 0.01} ${cy - 36}`;
const label = 'VERIFIED \u2022 VERIFIED \u2022 VERIFIED \u2022';
return (
<svg
width={size}
height={size}
viewBox="0 0 100 100"
className={className}
role="img"
aria-label="Verified identity"
>
<path d={scallopPath(cx, cy, R, n)} fill={green} />
<circle cx={cx} cy={cy} r={36} fill="#FFFFFF" />
<circle cx={cx} cy={cy} r={36} fill="none" stroke={green} strokeWidth="1.5" />
<path id="verified-stamp-arc" d={textPath} fill="none" />
<text fontSize="6.5" fontWeight="700" fill={green} letterSpacing="0.5" fontFamily="Sora, sans-serif">
<textPath href="#verified-stamp-arc" startOffset="0%">{label}</textPath>
</text>
<path d={starPath(cx, cy + 1, 13, 5.5)} fill={green} />
</svg>
);
}

View File

@@ -1,43 +0,0 @@
import React from 'react';
import { Eye, FileText, Calendar, Clock } from 'lucide-react';
export default function YourMarket({ profile, jobRecs = [] }) {
const companies = (jobRecs || []).slice(0, 3).map(r => (r.title || 'Company').split(' ')[0]);
const availability = (profile.availability || [])[0] || 'Available This Week';
const cards = [
{ icon: Eye, label: 'Companies viewing you', dots: companies.length ? companies : ['Google', 'Bon Appetit', 'Compass'] },
{ icon: FileText, label: 'Applications', value: '2 Pending' },
{ icon: Calendar, label: 'Interviews', value: 'Tomorrow 9AM' },
{ icon: Clock, label: 'Availability', value: availability },
];
return (
<section className="rounded-2xl bg-white border border-[#E5E7EB] p-6">
<h2 className="text-[15px] font-semibold text-[#0F172A]">Your Market</h2>
<div className="mt-4 grid grid-cols-1 sm:grid-cols-2 gap-3">
{cards.map((c) => {
const Icon = c.icon;
return (
<div key={c.label} className="rounded-xl border border-[#E5E7EB] p-4">
<div className="flex items-center gap-2 text-[11px] uppercase tracking-wide text-[#6B7280]">
<Icon className="w-3.5 h-3.5" /> {c.label}
</div>
{c.dots ? (
<div className="mt-2 flex flex-wrap items-center gap-x-3 gap-y-1">
{c.dots.map((co, i) => (
<span key={i} className="inline-flex items-center gap-1.5 text-[13px] font-medium text-[#0F172A]">
<span className="w-1.5 h-1.5 rounded-full bg-[#16A34A]" /> {co}
</span>
))}
</div>
) : (
<p className="mt-1 text-[15px] font-semibold text-[#0F172A]">{c.value}</p>
)}
</div>
);
})}
</div>
</section>
);
}

View File

@@ -1,43 +0,0 @@
import React from 'react';
import { Star } from 'lucide-react';
const SEED = [
{ name: 'Luis', role: 'Supervisor', events: 12, action: 'Request Reference' },
{ name: 'Chef Antonio', role: 'Chef', events: 45, action: 'Endorse Skill' },
];
export default function YourNetwork({ profile }) {
const fromExp = (profile.experience || []).slice(0, 2).map((e, i) => ({
name: SEED[i]?.name || e.role || 'Colleague',
role: e.role || 'Colleague',
events: Math.max(1, Math.round((e.years || 1) * 10)),
action: i === 0 ? 'Request Reference' : 'Endorse Skill',
}));
const data = fromExp.length ? fromExp : SEED;
return (
<section className="rounded-2xl bg-white border border-[#E5E7EB] p-6">
<h2 className="text-[15px] font-semibold text-[#0F172A]">Your Network</h2>
<div className="mt-4 space-y-3">
{data.map((n, i) => (
<div key={i} className="flex items-center justify-between rounded-xl border border-[#E5E7EB] p-4">
<div>
<p className="text-[14px] font-semibold text-[#0F172A]">{n.name}</p>
<p className="text-[12px] text-[#6B7280]">
{n.role} · {n.events} events
</p>
<div className="mt-1 flex">
{Array.from({ length: 5 }).map((_, s) => (
<Star key={s} className="w-3.5 h-3.5 text-[#F59E0B] fill-[#F59E0B]" />
))}
</div>
</div>
<button className="text-[12px] font-semibold text-[#0838E0] border border-[#0838E0] rounded-full px-3 py-1.5 hover:bg-[#EEF3FE] transition-colors">
{n.action}
</button>
</div>
))}
</div>
</section>
);
}

View File

@@ -1,229 +0,0 @@
import React from 'react';
import { Sparkles, Upload } from 'lucide-react';
import { Button, Field, Input, Modal, Textarea } from '@/components/ds';
import { usePreferences, useUpdatePreferences } from '@/lib/krowHooks';
import { PAGE_KEYS, validateSkillSource } from '@/lib/skills/registry';
import { skillTemplate, upsertCustomSkill } from '@/lib/skills/customSkills';
import {
isReadableDefinition, normalizeUpload, owliverFieldsFromSource, patchFrontmatter,
} from '@/lib/skills/skillFields';
import { reportSave } from '@/lib/skills/saveFeedback';
/**
* Add an Owliver skill.
*
* One dialog, opened from Settings and from Owliver's own header, writing to
* one registry — a second registration path would be a second source of truth
* by another name.
*
* What is stored is the Markdown itself, not a parsed record. A custom skill is
* then exactly the artefact a file on disk is, read by the same parser, and can
* be copied into `src/skills/` later without conversion.
*
* The fields above the editor are a starting point, not a second schema: typing
* in them composes frontmatter in the existing format, which the author can
* then edit directly. The Markdown is always what gets saved.
*/
export function AddSkillDialog({ open, onOpenChange }) {
const preferences = usePreferences();
const updatePreferences = useUpdatePreferences();
const [meta, setMeta] = React.useState({ id: '', name: '', description: '', pages: '' });
const [source, setSource] = React.useState('');
const [touched, setTouched] = React.useState(false);
const [error, setError] = React.useState('');
const fileRef = React.useRef(null);
/* A fresh sheet each time it opens, so an abandoned draft is not waiting the
next time someone adds a skill. */
React.useEffect(() => {
if (!open) return;
setMeta({ id: '', name: '', description: '', pages: '' });
setSource('');
setTouched(false);
setError('');
}, [open]);
/**
* A field edit, written into the definition.
*
* A fresh draft is composed from the template; once there is a definition to
* preserve — an uploaded file, or Markdown typed by hand — the field patches
* the key it owns and leaves everything else alone. It used to stop here
* entirely, so after an upload these four boxes edited nothing that was saved.
*/
const syncFromMeta = (next) => {
setMeta(next);
const pages = next.pages.split(',').map((p) => p.trim()).filter(Boolean);
if (!touched) {
setSource(skillTemplate({
id: next.id,
name: next.name,
description: next.description,
pages,
}));
return;
}
setSource(patchFrontmatter(source, {
id: next.id || undefined,
name: next.name || undefined,
description: next.description || undefined,
pages: pages.length ? pages : undefined,
}));
};
/**
* A definition uploaded from a file.
*
* Filled the Markdown box and left every field beside it empty, and said
* nothing about whether the file was usable until Add was pressed. Both are
* fixed here: the frontmatter fills the fields, and an unusable file says so
* at the moment it arrives.
*/
const readFile = (event) => {
const file = event.target.files?.[0];
if (!file) return;
const reader = new FileReader();
reader.onerror = () => setError('That file could not be read.');
reader.onload = () => {
const raw = normalizeUpload(reader.result);
setSource(raw);
setTouched(true);
/* Nothing readable: the fields stay as they were rather than emptying to
a row of placeholders that read as values. */
if (!isReadableDefinition(raw)) {
setError('That file has no frontmatter block, so there is nothing to read into the fields. '
+ 'A definition starts with `---` on its own line.');
return;
}
setError(validateSkillSource(raw) || '');
const fields = owliverFieldsFromSource(raw);
setMeta({
id: fields.id,
name: fields.name,
description: fields.description,
pages: fields.pages.join(', '),
});
};
reader.readAsText(file);
event.target.value = '';
};
const save = () => {
const problem = validateSkillSource(source);
if (problem) {
setError(problem);
return;
}
const { skill, next } = upsertCustomSkill(preferences.customSkills || [], source);
updatePreferences.mutate({ customSkills: next }, reportSave(`${skill.name} added`));
onOpenChange(false);
};
return (
<Modal
open={open}
onOpenChange={onOpenChange}
title="Add Owliver skill"
description="Define a skill in Markdown. It attaches itself to the pages it names."
icon={Sparkles}
size="lg"
footer={
<>
<Button variant="outline" onClick={() => onOpenChange(false)}>Cancel</Button>
<Button onClick={save} loading={updatePreferences.isPending}>Add skill</Button>
</>
}
>
<div className="space-y-4">
<div className="grid gap-4 sm:grid-cols-2">
<Field label="Skill name">
<Input
value={meta.name}
placeholder="e.g. Chef Training"
onChange={(e) => syncFromMeta({ ...meta, name: e.target.value })}
/>
</Field>
<Field label="Skill ID" hint="Lower-case, dashes.">
<Input
value={meta.id}
placeholder="e.g. chef-training"
onChange={(e) => syncFromMeta({ ...meta, id: e.target.value })}
/>
</Field>
</div>
<Field label="Description">
<Input
value={meta.description}
placeholder="e.g. Helps Owliver explain chef training paths."
onChange={(e) => syncFromMeta({ ...meta, description: e.target.value })}
/>
</Field>
<Field label="Pages" hint={`Comma separated. Known pages: ${PAGE_KEYS.join(', ')}`}>
<Input
value={meta.pages}
placeholder="university"
onChange={(e) => syncFromMeta({ ...meta, pages: e.target.value })}
/>
</Field>
<Field
label="Markdown definition"
hint="This is what gets saved. Edit it directly if you prefer."
error={error}
>
<Textarea
value={source}
rows={12}
spellCheck={false}
className="font-mono text-[12px] leading-relaxed"
placeholder="---&#10;id: my-skill&#10;name: My Skill&#10;pages:&#10; - positions&#10;---"
onChange={(e) => {
const next = e.target.value;
setSource(next);
setTouched(true);
/* Markdown → fields, the direction that was missing. Same
extractor the upload path uses; guarded so a half-typed
definition cannot wipe fields that are already filled in.
See the fuller note in the two skill editors. */
if (isReadableDefinition(next)) {
const fields = owliverFieldsFromSource(next);
if (fields.name) {
setMeta({
id: fields.id,
name: fields.name,
description: fields.description,
pages: fields.pages.join(', '),
});
}
}
if (error) setError('');
}}
/>
</Field>
<div className="flex items-center gap-2">
<input ref={fileRef} type="file" accept=".md,text/markdown" onChange={readFile} className="hidden" />
<Button variant="outline" size="sm" onClick={() => fileRef.current?.click()}>
<Upload aria-hidden="true" /> Upload .md
</Button>
<p className="text-caption text-ink-4">
Stored as data in this browser, for this workspace. It is never executed.
</p>
</div>
</div>
</Modal>
);
}

View File

@@ -1,41 +0,0 @@
import * as React from "react"
import * as AccordionPrimitive from "@radix-ui/react-accordion"
import { ChevronDown } from "lucide-react"
import { cn } from "@/lib/utils"
const Accordion = AccordionPrimitive.Root
const AccordionItem = React.forwardRef(({ className, ...props }, ref) => (
<AccordionPrimitive.Item ref={ref} className={cn("border-b", className)} {...props} />
))
AccordionItem.displayName = "AccordionItem"
const AccordionTrigger = React.forwardRef(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger
ref={ref}
className={cn(
"flex flex-1 items-center justify-between py-4 text-sm font-medium transition-all hover:underline text-left [&[data-state=open]>svg]:rotate-180",
className
)}
{...props}>
{children}
<ChevronDown
className="h-4 w-4 shrink-0 text-muted-foreground transition-transform duration-200" />
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
))
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
const AccordionContent = React.forwardRef(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Content
ref={ref}
className="overflow-hidden text-sm data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
{...props}>
<div className={cn("pb-4 pt-0", className)}>{children}</div>
</AccordionPrimitive.Content>
))
AccordionContent.displayName = AccordionPrimitive.Content.displayName
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }

View File

@@ -1,97 +0,0 @@
import * as React from "react"
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button"
const AlertDialog = AlertDialogPrimitive.Root
const AlertDialogTrigger = AlertDialogPrimitive.Trigger
const AlertDialogPortal = AlertDialogPrimitive.Portal
const AlertDialogOverlay = React.forwardRef(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Overlay
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
ref={ref} />
))
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
const AlertDialogContent = React.forwardRef(({ className, ...props }, ref) => (
<AlertDialogPortal>
<AlertDialogOverlay />
<AlertDialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className
)}
{...props} />
</AlertDialogPortal>
))
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
const AlertDialogHeader = ({
className,
...props
}) => (
<div
className={cn("flex flex-col space-y-2 text-center sm:text-left", className)}
{...props} />
)
AlertDialogHeader.displayName = "AlertDialogHeader"
const AlertDialogFooter = ({
className,
...props
}) => (
<div
className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
{...props} />
)
AlertDialogFooter.displayName = "AlertDialogFooter"
const AlertDialogTitle = React.forwardRef(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Title ref={ref} className={cn("text-lg font-semibold", className)} {...props} />
))
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
const AlertDialogDescription = React.forwardRef(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props} />
))
AlertDialogDescription.displayName =
AlertDialogPrimitive.Description.displayName
const AlertDialogAction = React.forwardRef(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Action ref={ref} className={cn(buttonVariants(), className)} {...props} />
))
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
const AlertDialogCancel = React.forwardRef(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Cancel
ref={ref}
className={cn(buttonVariants({ variant: "outline" }), "mt-2 sm:mt-0", className)}
{...props} />
))
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
export {
AlertDialog,
AlertDialogPortal,
AlertDialogOverlay,
AlertDialogTrigger,
AlertDialogContent,
AlertDialogHeader,
AlertDialogFooter,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogAction,
AlertDialogCancel,
}

View File

@@ -1,47 +0,0 @@
import * as React from "react"
import { cva } from "class-variance-authority";
import { cn } from "@/lib/utils"
const alertVariants = cva(
"relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7",
{
variants: {
variant: {
default: "bg-background text-foreground",
destructive:
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
},
},
defaultVariants: {
variant: "default",
},
}
)
const Alert = React.forwardRef(({ className, variant, ...props }, ref) => (
<div
ref={ref}
role="alert"
className={cn(alertVariants({ variant }), className)}
{...props} />
))
Alert.displayName = "Alert"
const AlertTitle = React.forwardRef(({ className, ...props }, ref) => (
<h5
ref={ref}
className={cn("mb-1 font-medium leading-none tracking-tight", className)}
{...props} />
))
AlertTitle.displayName = "AlertTitle"
const AlertDescription = React.forwardRef(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("text-sm [&_p]:leading-relaxed", className)}
{...props} />
))
AlertDescription.displayName = "AlertDescription"
export { Alert, AlertTitle, AlertDescription }

View File

@@ -1,5 +0,0 @@
import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"
const AspectRatio = AspectRatioPrimitive.Root
export { AspectRatio }

View File

@@ -1,35 +0,0 @@
"use client"
import * as React from "react"
import * as AvatarPrimitive from "@radix-ui/react-avatar"
import { cn } from "@/lib/utils"
const Avatar = React.forwardRef(({ className, ...props }, ref) => (
<AvatarPrimitive.Root
ref={ref}
className={cn("relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full", className)}
{...props} />
))
Avatar.displayName = AvatarPrimitive.Root.displayName
const AvatarImage = React.forwardRef(({ className, ...props }, ref) => (
<AvatarPrimitive.Image
ref={ref}
className={cn("aspect-square h-full w-full", className)}
{...props} />
))
AvatarImage.displayName = AvatarPrimitive.Image.displayName
const AvatarFallback = React.forwardRef(({ className, ...props }, ref) => (
<AvatarPrimitive.Fallback
ref={ref}
className={cn(
"flex h-full w-full items-center justify-center rounded-full bg-muted",
className
)}
{...props} />
))
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
export { Avatar, AvatarImage, AvatarFallback }

View File

@@ -1,74 +0,0 @@
import * as React from "react"
import { ChevronLeft, ChevronRight } from "lucide-react"
import { DayPicker } from "react-day-picker"
import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button"
// react-day-picker v9 renamed every element key and replaced the IconLeft /
// IconRight slots with a single `Chevron` that receives an orientation. The
// styling below is the same as before the upgrade, remapped onto the v9 names.
function Calendar({
className,
classNames,
showOutsideDays = true,
...props
}) {
return (
(<DayPicker
showOutsideDays={showOutsideDays}
className={cn("p-3", className)}
classNames={{
months: "flex flex-col sm:flex-row gap-4 relative",
month: "space-y-4",
month_caption: "flex justify-center pt-1 relative items-center h-7",
caption_label: "text-sm font-medium",
nav: "flex items-center gap-1 absolute inset-x-1 top-1 justify-between z-10",
button_previous: cn(
buttonVariants({ variant: "outline" }),
"h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100"
),
button_next: cn(
buttonVariants({ variant: "outline" }),
"h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100"
),
month_grid: "w-full border-collapse space-y-1",
weekdays: "flex",
weekday: "text-muted-foreground rounded-md w-8 font-normal text-[0.8rem]",
week: "flex w-full mt-2",
day: cn(
"relative p-0 text-center text-sm focus-within:relative focus-within:z-20",
"[&:has([aria-selected])]:bg-accent [&:has([aria-selected].day-outside)]:bg-accent/50",
props.mode === "range"
? "[&:has(>.day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md"
: "[&:has([aria-selected])]:rounded-md"
),
day_button: cn(
buttonVariants({ variant: "ghost" }),
"h-8 w-8 p-0 font-normal aria-selected:opacity-100"
),
range_start: "day-range-start",
range_end: "day-range-end",
selected:
"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
today: "bg-accent text-accent-foreground",
outside:
"day-outside text-muted-foreground aria-selected:bg-accent/50 aria-selected:text-muted-foreground",
disabled: "text-muted-foreground opacity-50",
range_middle:
"aria-selected:bg-accent aria-selected:text-accent-foreground",
hidden: "invisible",
...classNames,
}}
components={{
Chevron: ({ className, orientation, ...props }) => {
const Icon = orientation === "left" ? ChevronLeft : ChevronRight
return <Icon className={cn("h-4 w-4", className)} {...props} />
},
}}
{...props} />)
);
}
Calendar.displayName = "Calendar"
export { Calendar }

View File

@@ -1,193 +0,0 @@
import * as React from "react"
import useEmblaCarousel from "embla-carousel-react";
import { ArrowLeft, ArrowRight } from "lucide-react"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
const CarouselContext = React.createContext(null)
function useCarousel() {
const context = React.useContext(CarouselContext)
if (!context) {
throw new Error("useCarousel must be used within a <Carousel />")
}
return context
}
const Carousel = React.forwardRef((
{
orientation = "horizontal",
opts,
setApi,
plugins,
className,
children,
...props
},
ref
) => {
const [carouselRef, api] = useEmblaCarousel({
...opts,
axis: orientation === "horizontal" ? "x" : "y",
}, plugins)
const [canScrollPrev, setCanScrollPrev] = React.useState(false)
const [canScrollNext, setCanScrollNext] = React.useState(false)
const onSelect = React.useCallback((api) => {
if (!api) {
return
}
setCanScrollPrev(api.canScrollPrev())
setCanScrollNext(api.canScrollNext())
}, [])
const scrollPrev = React.useCallback(() => {
api?.scrollPrev()
}, [api])
const scrollNext = React.useCallback(() => {
api?.scrollNext()
}, [api])
const handleKeyDown = React.useCallback((event) => {
if (event.key === "ArrowLeft") {
event.preventDefault()
scrollPrev()
} else if (event.key === "ArrowRight") {
event.preventDefault()
scrollNext()
}
}, [scrollPrev, scrollNext])
React.useEffect(() => {
if (!api || !setApi) {
return
}
setApi(api)
}, [api, setApi])
React.useEffect(() => {
if (!api) {
return
}
onSelect(api)
api.on("reInit", onSelect)
api.on("select", onSelect)
return () => {
api?.off("select", onSelect)
};
}, [api, onSelect])
return (
(<CarouselContext.Provider
value={{
carouselRef,
api: api,
opts,
orientation:
orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
scrollPrev,
scrollNext,
canScrollPrev,
canScrollNext,
}}>
<div
ref={ref}
onKeyDownCapture={handleKeyDown}
className={cn("relative", className)}
role="region"
aria-roledescription="carousel"
{...props}>
{children}
</div>
</CarouselContext.Provider>)
);
})
Carousel.displayName = "Carousel"
const CarouselContent = React.forwardRef(({ className, ...props }, ref) => {
const { carouselRef, orientation } = useCarousel()
return (
(<div ref={carouselRef} className="overflow-hidden">
<div
ref={ref}
className={cn(
"flex",
orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
className
)}
{...props} />
</div>)
);
})
CarouselContent.displayName = "CarouselContent"
const CarouselItem = React.forwardRef(({ className, ...props }, ref) => {
const { orientation } = useCarousel()
return (
(<div
ref={ref}
role="group"
aria-roledescription="slide"
className={cn(
"min-w-0 shrink-0 grow-0 basis-full",
orientation === "horizontal" ? "pl-4" : "pt-4",
className
)}
{...props} />)
);
})
CarouselItem.displayName = "CarouselItem"
const CarouselPrevious = React.forwardRef(({ className, variant = "outline", size = "icon", ...props }, ref) => {
const { orientation, scrollPrev, canScrollPrev } = useCarousel()
return (
(<Button
ref={ref}
variant={variant}
size={size}
className={cn("absolute h-8 w-8 rounded-full", orientation === "horizontal"
? "-left-12 top-1/2 -translate-y-1/2"
: "-top-12 left-1/2 -translate-x-1/2 rotate-90", className)}
disabled={!canScrollPrev}
onClick={scrollPrev}
{...props}>
<ArrowLeft className="h-4 w-4" />
<span className="sr-only">Previous slide</span>
</Button>)
);
})
CarouselPrevious.displayName = "CarouselPrevious"
const CarouselNext = React.forwardRef(({ className, variant = "outline", size = "icon", ...props }, ref) => {
const { orientation, scrollNext, canScrollNext } = useCarousel()
return (
(<Button
ref={ref}
variant={variant}
size={size}
className={cn("absolute h-8 w-8 rounded-full", orientation === "horizontal"
? "-right-12 top-1/2 -translate-y-1/2"
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90", className)}
disabled={!canScrollNext}
onClick={scrollNext}
{...props}>
<ArrowRight className="h-4 w-4" />
<span className="sr-only">Next slide</span>
</Button>)
);
})
CarouselNext.displayName = "CarouselNext"
export { Carousel, CarouselContent, CarouselItem, CarouselPrevious, CarouselNext };

View File

@@ -1,309 +0,0 @@
"use client";
import * as React from "react"
import * as RechartsPrimitive from "recharts"
import { cn } from "@/lib/utils"
// Format: { THEME_NAME: CSS_SELECTOR }
const THEMES = {
light: "",
dark: ".dark"
}
const ChartContext = React.createContext(null)
function useChart() {
const context = React.useContext(ChartContext)
if (!context) {
throw new Error("useChart must be used within a <ChartContainer />")
}
return context
}
const ChartContainer = React.forwardRef(({ id, className, children, config, ...props }, ref) => {
const uniqueId = React.useId()
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`
return (
(<ChartContext.Provider value={{ config }}>
<div
data-chart={chartId}
ref={ref}
className={cn(
"flex aspect-video justify-center text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-none [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-none [&_.recharts-surface]:outline-none",
className
)}
{...props}>
<ChartStyle id={chartId} config={config} />
<RechartsPrimitive.ResponsiveContainer>
{children}
</RechartsPrimitive.ResponsiveContainer>
</div>
</ChartContext.Provider>)
);
})
ChartContainer.displayName = "Chart"
const ChartStyle = ({
id,
config
}) => {
const colorConfig = Object.entries(config).filter(([, config]) => config.theme || config.color)
if (!colorConfig.length) {
return null
}
return (
(<style
dangerouslySetInnerHTML={{
__html: Object.entries(THEMES)
.map(([theme, prefix]) => `
${prefix} [data-chart=${id}] {
${colorConfig
.map(([key, itemConfig]) => {
const color =
itemConfig.theme?.[theme] ||
itemConfig.color
return color ? ` --color-${key}: ${color};` : null
})
.join("\n")}
}
`)
.join("\n"),
}} />)
);
}
const ChartTooltip = RechartsPrimitive.Tooltip
const ChartTooltipContent = React.forwardRef((
{
active,
payload,
className,
indicator = "dot",
hideLabel = false,
hideIndicator = false,
label,
labelFormatter,
labelClassName,
formatter,
color,
nameKey,
labelKey,
},
ref
) => {
const { config } = useChart()
const tooltipLabel = React.useMemo(() => {
if (hideLabel || !payload?.length) {
return null
}
const [item] = payload
const key = `${labelKey || item.dataKey || item.name || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)
const value =
!labelKey && typeof label === "string"
? config[label]?.label || label
: itemConfig?.label
if (labelFormatter) {
return (
(<div className={cn("font-medium", labelClassName)}>
{labelFormatter(value, payload)}
</div>)
);
}
if (!value) {
return null
}
return <div className={cn("font-medium", labelClassName)}>{value}</div>;
}, [
label,
labelFormatter,
payload,
hideLabel,
labelClassName,
config,
labelKey,
])
if (!active || !payload?.length) {
return null
}
const nestLabel = payload.length === 1 && indicator !== "dot"
return (
(<div
ref={ref}
className={cn(
"grid min-w-[8rem] items-start gap-1.5 rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl",
className
)}>
{!nestLabel ? tooltipLabel : null}
<div className="grid gap-1.5">
{payload.map((item, index) => {
const key = `${nameKey || item.name || item.dataKey || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)
const indicatorColor = color || item.payload.fill || item.color
return (
(<div
key={item.dataKey}
className={cn(
"flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground",
indicator === "dot" && "items-center"
)}>
{formatter && item?.value !== undefined && item.name ? (
formatter(item.value, item.name, item, index, item.payload)
) : (
<>
{itemConfig?.icon ? (
<itemConfig.icon />
) : (
!hideIndicator && (
<div
className={cn("shrink-0 rounded-[2px] border-[--color-border] bg-[--color-bg]", {
"h-2.5 w-2.5": indicator === "dot",
"w-1": indicator === "line",
"w-0 border-[1.5px] border-dashed bg-transparent":
indicator === "dashed",
"my-0.5": nestLabel && indicator === "dashed",
})}
style={
{
"--color-bg": indicatorColor,
"--color-border": indicatorColor
}
} />
)
)}
<div
className={cn(
"flex flex-1 justify-between leading-none",
nestLabel ? "items-end" : "items-center"
)}>
<div className="grid gap-1.5">
{nestLabel ? tooltipLabel : null}
<span className="text-muted-foreground">
{itemConfig?.label || item.name}
</span>
</div>
{item.value && (
<span className="font-mono font-medium tabular-nums text-foreground">
{item.value.toLocaleString()}
</span>
)}
</div>
</>
)}
</div>)
);
})}
</div>
</div>)
);
})
ChartTooltipContent.displayName = "ChartTooltip"
const ChartLegend = RechartsPrimitive.Legend
const ChartLegendContent = React.forwardRef((
{ className, hideIcon = false, payload, verticalAlign = "bottom", nameKey },
ref
) => {
const { config } = useChart()
if (!payload?.length) {
return null
}
return (
(<div
ref={ref}
className={cn(
"flex items-center justify-center gap-4",
verticalAlign === "top" ? "pb-3" : "pt-3",
className
)}>
{payload.map((item) => {
const key = `${nameKey || item.dataKey || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)
return (
(<div
key={item.value}
className={cn(
"flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground"
)}>
{itemConfig?.icon && !hideIcon ? (
<itemConfig.icon />
) : (
<div
className="h-2 w-2 shrink-0 rounded-[2px]"
style={{
backgroundColor: item.color,
}} />
)}
{itemConfig?.label}
</div>)
);
})}
</div>)
);
})
ChartLegendContent.displayName = "ChartLegend"
// Helper to extract item config from a payload.
function getPayloadConfigFromPayload(
config,
payload,
key
) {
if (typeof payload !== "object" || payload === null) {
return undefined
}
const payloadPayload =
"payload" in payload &&
typeof payload.payload === "object" &&
payload.payload !== null
? payload.payload
: undefined
let configLabelKey = key
if (
key in payload &&
typeof payload[key] === "string"
) {
configLabelKey = payload[key]
} else if (
payloadPayload &&
key in payloadPayload &&
typeof payloadPayload[key] === "string"
) {
configLabelKey = payloadPayload[key]
}
return configLabelKey in config
? config[configLabelKey]
: config[key];
}
export {
ChartContainer,
ChartTooltip,
ChartTooltipContent,
ChartLegend,
ChartLegendContent,
ChartStyle,
}

View File

@@ -1,116 +0,0 @@
import * as React from "react"
import { Command as CommandPrimitive } from "cmdk"
import { Search } from "lucide-react"
import { cn } from "@/lib/utils"
import { Dialog, DialogContent } from "@/components/ui/dialog"
const Command = React.forwardRef(({ className, ...props }, ref) => (
<CommandPrimitive
ref={ref}
className={cn(
"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
className
)}
{...props} />
))
Command.displayName = CommandPrimitive.displayName
const CommandDialog = ({
children,
...props
}) => {
return (
(<Dialog {...props}>
<DialogContent className="overflow-hidden p-0">
<Command
className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
{children}
</Command>
</DialogContent>
</Dialog>)
);
}
const CommandInput = React.forwardRef(({ className, ...props }, ref) => (
<div className="flex items-center border-b px-3" cmdk-input-wrapper="">
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
<CommandPrimitive.Input
ref={ref}
className={cn(
"flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props} />
</div>
))
CommandInput.displayName = CommandPrimitive.Input.displayName
const CommandList = React.forwardRef(({ className, ...props }, ref) => (
<CommandPrimitive.List
ref={ref}
className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
{...props} />
))
CommandList.displayName = CommandPrimitive.List.displayName
const CommandEmpty = React.forwardRef((props, ref) => (
<CommandPrimitive.Empty ref={ref} className="py-6 text-center text-sm" {...props} />
))
CommandEmpty.displayName = CommandPrimitive.Empty.displayName
const CommandGroup = React.forwardRef(({ className, ...props }, ref) => (
<CommandPrimitive.Group
ref={ref}
className={cn(
"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
className
)}
{...props} />
))
CommandGroup.displayName = CommandPrimitive.Group.displayName
const CommandSeparator = React.forwardRef(({ className, ...props }, ref) => (
<CommandPrimitive.Separator ref={ref} className={cn("-mx-1 h-px bg-border", className)} {...props} />
))
CommandSeparator.displayName = CommandPrimitive.Separator.displayName
const CommandItem = React.forwardRef(({ className, ...props }, ref) => (
<CommandPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
className
)}
{...props} />
))
CommandItem.displayName = CommandPrimitive.Item.displayName
const CommandShortcut = ({
className,
...props
}) => {
return (
(<span
className={cn("ml-auto text-xs tracking-widest text-muted-foreground", className)}
{...props} />)
);
}
CommandShortcut.displayName = "CommandShortcut"
export {
Command,
CommandDialog,
CommandInput,
CommandList,
CommandEmpty,
CommandGroup,
CommandItem,
CommandShortcut,
CommandSeparator,
}

View File

@@ -1,156 +0,0 @@
import * as React from "react"
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu"
import { Check, ChevronRight, Circle } from "lucide-react"
import { cn } from "@/lib/utils"
const ContextMenu = ContextMenuPrimitive.Root
const ContextMenuTrigger = ContextMenuPrimitive.Trigger
const ContextMenuGroup = ContextMenuPrimitive.Group
const ContextMenuPortal = ContextMenuPrimitive.Portal
const ContextMenuSub = ContextMenuPrimitive.Sub
const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup
const ContextMenuSubTrigger = React.forwardRef(({ className, inset, children, ...props }, ref) => (
<ContextMenuPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
inset && "pl-8",
className
)}
{...props}>
{children}
<ChevronRight className="ml-auto h-4 w-4" />
</ContextMenuPrimitive.SubTrigger>
))
ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName
const ContextMenuSubContent = React.forwardRef(({ className, ...props }, ref) => (
<ContextMenuPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props} />
))
ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName
const ContextMenuContent = React.forwardRef(({ className, ...props }, ref) => (
<ContextMenuPrimitive.Portal>
<ContextMenuPrimitive.Content
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props} />
</ContextMenuPrimitive.Portal>
))
ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName
const ContextMenuItem = React.forwardRef(({ className, inset, ...props }, ref) => (
<ContextMenuPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
inset && "pl-8",
className
)}
{...props} />
))
ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName
const ContextMenuCheckboxItem = React.forwardRef(({ className, children, checked, ...props }, ref) => (
<ContextMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
checked={checked}
{...props}>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<ContextMenuPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</ContextMenuPrimitive.ItemIndicator>
</span>
{children}
</ContextMenuPrimitive.CheckboxItem>
))
ContextMenuCheckboxItem.displayName =
ContextMenuPrimitive.CheckboxItem.displayName
const ContextMenuRadioItem = React.forwardRef(({ className, children, ...props }, ref) => (
<ContextMenuPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<ContextMenuPrimitive.ItemIndicator>
<Circle className="h-4 w-4 fill-current" />
</ContextMenuPrimitive.ItemIndicator>
</span>
{children}
</ContextMenuPrimitive.RadioItem>
))
ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName
const ContextMenuLabel = React.forwardRef(({ className, inset, ...props }, ref) => (
<ContextMenuPrimitive.Label
ref={ref}
className={cn(
"px-2 py-1.5 text-sm font-semibold text-foreground",
inset && "pl-8",
className
)}
{...props} />
))
ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName
const ContextMenuSeparator = React.forwardRef(({ className, ...props }, ref) => (
<ContextMenuPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-border", className)}
{...props} />
))
ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName
const ContextMenuShortcut = ({
className,
...props
}) => {
return (
(<span
className={cn("ml-auto text-xs tracking-widest text-muted-foreground", className)}
{...props} />)
);
}
ContextMenuShortcut.displayName = "ContextMenuShortcut"
export {
ContextMenu,
ContextMenuTrigger,
ContextMenuContent,
ContextMenuItem,
ContextMenuCheckboxItem,
ContextMenuRadioItem,
ContextMenuLabel,
ContextMenuSeparator,
ContextMenuShortcut,
ContextMenuGroup,
ContextMenuPortal,
ContextMenuSub,
ContextMenuSubContent,
ContextMenuSubTrigger,
ContextMenuRadioGroup,
}

View File

@@ -1,92 +0,0 @@
"use client"
import * as React from "react"
import { Drawer as DrawerPrimitive } from "vaul"
import { cn } from "@/lib/utils"
const Drawer = ({
shouldScaleBackground = true,
...props
}) => (
<DrawerPrimitive.Root shouldScaleBackground={shouldScaleBackground} {...props} />
)
Drawer.displayName = "Drawer"
const DrawerTrigger = DrawerPrimitive.Trigger
const DrawerPortal = DrawerPrimitive.Portal
const DrawerClose = DrawerPrimitive.Close
const DrawerOverlay = React.forwardRef(({ className, ...props }, ref) => (
<DrawerPrimitive.Overlay
ref={ref}
className={cn("fixed inset-0 z-50 bg-black/80", className)}
{...props} />
))
DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName
const DrawerContent = React.forwardRef(({ className, children, ...props }, ref) => (
<DrawerPortal>
<DrawerOverlay />
<DrawerPrimitive.Content
ref={ref}
className={cn(
"fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border bg-background",
className
)}
{...props}>
<div className="mx-auto mt-4 h-2 w-[100px] rounded-full bg-muted" />
{children}
</DrawerPrimitive.Content>
</DrawerPortal>
))
DrawerContent.displayName = "DrawerContent"
const DrawerHeader = ({
className,
...props
}) => (
<div
className={cn("grid gap-1.5 p-4 text-center sm:text-left", className)}
{...props} />
)
DrawerHeader.displayName = "DrawerHeader"
const DrawerFooter = ({
className,
...props
}) => (
<div className={cn("mt-auto flex flex-col gap-2 p-4", className)} {...props} />
)
DrawerFooter.displayName = "DrawerFooter"
const DrawerTitle = React.forwardRef(({ className, ...props }, ref) => (
<DrawerPrimitive.Title
ref={ref}
className={cn("text-lg font-semibold leading-none tracking-tight", className)}
{...props} />
))
DrawerTitle.displayName = DrawerPrimitive.Title.displayName
const DrawerDescription = React.forwardRef(({ className, ...props }, ref) => (
<DrawerPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props} />
))
DrawerDescription.displayName = DrawerPrimitive.Description.displayName
export {
Drawer,
DrawerPortal,
DrawerOverlay,
DrawerTrigger,
DrawerClose,
DrawerContent,
DrawerHeader,
DrawerFooter,
DrawerTitle,
DrawerDescription,
}

View File

@@ -1,134 +0,0 @@
"use client";
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { Controller, FormProvider, useFormContext } from "react-hook-form";
import { cn } from "@/lib/utils"
import { Label } from "@/components/ui/label"
const Form = FormProvider
const FormFieldContext = React.createContext({})
const FormField = (
{
...props
}
) => {
return (
(<FormFieldContext.Provider value={{ name: props.name }}>
<Controller {...props} />
</FormFieldContext.Provider>)
);
}
const useFormField = () => {
const fieldContext = React.useContext(FormFieldContext)
const itemContext = React.useContext(FormItemContext)
const { getFieldState, formState } = useFormContext()
const fieldState = getFieldState(fieldContext.name, formState)
if (!fieldContext) {
throw new Error("useFormField should be used within <FormField>")
}
const { id } = itemContext
return {
id,
name: fieldContext.name,
formItemId: `${id}-form-item`,
formDescriptionId: `${id}-form-item-description`,
formMessageId: `${id}-form-item-message`,
...fieldState,
}
}
const FormItemContext = React.createContext({})
const FormItem = React.forwardRef(({ className, ...props }, ref) => {
const id = React.useId()
return (
(<FormItemContext.Provider value={{ id }}>
<div ref={ref} className={cn("space-y-2", className)} {...props} />
</FormItemContext.Provider>)
);
})
FormItem.displayName = "FormItem"
const FormLabel = React.forwardRef(({ className, ...props }, ref) => {
const { error, formItemId } = useFormField()
return (
(<Label
ref={ref}
className={cn(error && "text-destructive", className)}
htmlFor={formItemId}
{...props} />)
);
})
FormLabel.displayName = "FormLabel"
const FormControl = React.forwardRef(({ ...props }, ref) => {
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
return (
(<Slot
ref={ref}
id={formItemId}
aria-describedby={
!error
? `${formDescriptionId}`
: `${formDescriptionId} ${formMessageId}`
}
aria-invalid={!!error}
{...props} />)
);
})
FormControl.displayName = "FormControl"
const FormDescription = React.forwardRef(({ className, ...props }, ref) => {
const { formDescriptionId } = useFormField()
return (
(<p
ref={ref}
id={formDescriptionId}
className={cn("text-[0.8rem] text-muted-foreground", className)}
{...props} />)
);
})
FormDescription.displayName = "FormDescription"
const FormMessage = React.forwardRef(({ className, children, ...props }, ref) => {
const { error, formMessageId } = useFormField()
const body = error ? String(error?.message) : children
if (!body) {
return null
}
return (
(<p
ref={ref}
id={formMessageId}
className={cn("text-[0.8rem] font-medium text-destructive", className)}
{...props}>
{body}
</p>)
);
})
FormMessage.displayName = "FormMessage"
export {
useFormField,
Form,
FormItem,
FormLabel,
FormControl,
FormDescription,
FormMessage,
FormField,
}

View File

@@ -1,53 +0,0 @@
import * as React from "react"
import { OTPInput, OTPInputContext } from "input-otp"
import { Minus } from "lucide-react"
import { cn } from "@/lib/utils"
const InputOTP = React.forwardRef(({ className, containerClassName, ...props }, ref) => (
<OTPInput
ref={ref}
containerClassName={cn("flex items-center gap-2 has-[:disabled]:opacity-50", containerClassName)}
className={cn("disabled:cursor-not-allowed", className)}
{...props} />
))
InputOTP.displayName = "InputOTP"
const InputOTPGroup = React.forwardRef(({ className, ...props }, ref) => (
<div ref={ref} className={cn("flex items-center", className)} {...props} />
))
InputOTPGroup.displayName = "InputOTPGroup"
const InputOTPSlot = React.forwardRef(({ index, className, ...props }, ref) => {
const inputOTPContext = React.useContext(OTPInputContext)
const { char, hasFakeCaret, isActive } = inputOTPContext.slots[index]
return (
(<div
ref={ref}
className={cn(
"relative flex h-9 w-9 items-center justify-center border-y border-r border-input text-sm shadow-sm transition-all first:rounded-l-md first:border-l last:rounded-r-md",
isActive && "z-10 ring-1 ring-ring",
className
)}
{...props}>
{char}
{hasFakeCaret && (
<div
className="pointer-events-none absolute inset-0 flex items-center justify-center">
<div className="h-4 w-px animate-caret-blink bg-foreground duration-1000" />
</div>
)}
</div>)
);
})
InputOTPSlot.displayName = "InputOTPSlot"
const InputOTPSeparator = React.forwardRef(({ ...props }, ref) => (
<div ref={ref} role="separator" {...props}>
<Minus />
</div>
))
InputOTPSeparator.displayName = "InputOTPSeparator"
export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator }

View File

@@ -1,200 +0,0 @@
"use client"
import * as React from "react"
import * as MenubarPrimitive from "@radix-ui/react-menubar"
import { Check, ChevronRight, Circle } from "lucide-react"
import { cn } from "@/lib/utils"
function MenubarMenu({
...props
}) {
return <MenubarPrimitive.Menu {...props} />;
}
function MenubarGroup({
...props
}) {
return <MenubarPrimitive.Group {...props} />;
}
function MenubarPortal({
...props
}) {
return <MenubarPrimitive.Portal {...props} />;
}
function MenubarRadioGroup({
...props
}) {
return <MenubarPrimitive.RadioGroup {...props} />;
}
function MenubarSub({
...props
}) {
return <MenubarPrimitive.Sub data-slot="menubar-sub" {...props} />;
}
const Menubar = React.forwardRef(({ className, ...props }, ref) => (
<MenubarPrimitive.Root
ref={ref}
className={cn(
"flex h-9 items-center space-x-1 rounded-md border bg-background p-1 shadow-sm",
className
)}
{...props} />
))
Menubar.displayName = MenubarPrimitive.Root.displayName
const MenubarTrigger = React.forwardRef(({ className, ...props }, ref) => (
<MenubarPrimitive.Trigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-3 py-1 text-sm font-medium outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
className
)}
{...props} />
))
MenubarTrigger.displayName = MenubarPrimitive.Trigger.displayName
const MenubarSubTrigger = React.forwardRef(({ className, inset, children, ...props }, ref) => (
<MenubarPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
inset && "pl-8",
className
)}
{...props}>
{children}
<ChevronRight className="ml-auto h-4 w-4" />
</MenubarPrimitive.SubTrigger>
))
MenubarSubTrigger.displayName = MenubarPrimitive.SubTrigger.displayName
const MenubarSubContent = React.forwardRef(({ className, ...props }, ref) => (
<MenubarPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props} />
))
MenubarSubContent.displayName = MenubarPrimitive.SubContent.displayName
const MenubarContent = React.forwardRef((
{ className, align = "start", alignOffset = -4, sideOffset = 8, ...props },
ref
) => (
<MenubarPrimitive.Portal>
<MenubarPrimitive.Content
ref={ref}
align={align}
alignOffset={alignOffset}
sideOffset={sideOffset}
className={cn(
"z-50 min-w-[12rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props} />
</MenubarPrimitive.Portal>
))
MenubarContent.displayName = MenubarPrimitive.Content.displayName
const MenubarItem = React.forwardRef(({ className, inset, ...props }, ref) => (
<MenubarPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
inset && "pl-8",
className
)}
{...props} />
))
MenubarItem.displayName = MenubarPrimitive.Item.displayName
const MenubarCheckboxItem = React.forwardRef(({ className, children, checked, ...props }, ref) => (
<MenubarPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
checked={checked}
{...props}>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<MenubarPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</MenubarPrimitive.ItemIndicator>
</span>
{children}
</MenubarPrimitive.CheckboxItem>
))
MenubarCheckboxItem.displayName = MenubarPrimitive.CheckboxItem.displayName
const MenubarRadioItem = React.forwardRef(({ className, children, ...props }, ref) => (
<MenubarPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<MenubarPrimitive.ItemIndicator>
<Circle className="h-4 w-4 fill-current" />
</MenubarPrimitive.ItemIndicator>
</span>
{children}
</MenubarPrimitive.RadioItem>
))
MenubarRadioItem.displayName = MenubarPrimitive.RadioItem.displayName
const MenubarLabel = React.forwardRef(({ className, inset, ...props }, ref) => (
<MenubarPrimitive.Label
ref={ref}
className={cn("px-2 py-1.5 text-sm font-semibold", inset && "pl-8", className)}
{...props} />
))
MenubarLabel.displayName = MenubarPrimitive.Label.displayName
const MenubarSeparator = React.forwardRef(({ className, ...props }, ref) => (
<MenubarPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props} />
))
MenubarSeparator.displayName = MenubarPrimitive.Separator.displayName
const MenubarShortcut = ({
className,
...props
}) => {
return (
(<span
className={cn("ml-auto text-xs tracking-widest text-muted-foreground", className)}
{...props} />)
);
}
MenubarShortcut.displayname = "MenubarShortcut"
export {
Menubar,
MenubarMenu,
MenubarTrigger,
MenubarContent,
MenubarItem,
MenubarSeparator,
MenubarLabel,
MenubarCheckboxItem,
MenubarRadioGroup,
MenubarRadioItem,
MenubarPortal,
MenubarSubContent,
MenubarSubTrigger,
MenubarGroup,
MenubarSub,
MenubarShortcut,
}

View File

@@ -1,104 +0,0 @@
import * as React from "react"
import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu"
import { cva } from "class-variance-authority"
import { ChevronDown } from "lucide-react"
import { cn } from "@/lib/utils"
const NavigationMenu = React.forwardRef(({ className, children, ...props }, ref) => (
<NavigationMenuPrimitive.Root
ref={ref}
className={cn(
"relative z-10 flex max-w-max flex-1 items-center justify-center",
className
)}
{...props}>
{children}
<NavigationMenuViewport />
</NavigationMenuPrimitive.Root>
))
NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName
const NavigationMenuList = React.forwardRef(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.List
ref={ref}
className={cn(
"group flex flex-1 list-none items-center justify-center space-x-1",
className
)}
{...props} />
))
NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName
const NavigationMenuItem = NavigationMenuPrimitive.Item
const navigationMenuTriggerStyle = cva(
"group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[active]:bg-accent/50 data-[state=open]:bg-accent/50"
)
const NavigationMenuTrigger = React.forwardRef(({ className, children, ...props }, ref) => (
<NavigationMenuPrimitive.Trigger
ref={ref}
className={cn(navigationMenuTriggerStyle(), "group", className)}
{...props}>
{children}{" "}
<ChevronDown
className="relative top-[1px] ml-1 h-3 w-3 transition duration-300 group-data-[state=open]:rotate-180"
aria-hidden="true" />
</NavigationMenuPrimitive.Trigger>
))
NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName
const NavigationMenuContent = React.forwardRef(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.Content
ref={ref}
className={cn(
"left-0 top-0 w-full data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 md:absolute md:w-auto ",
className
)}
{...props} />
))
NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName
const NavigationMenuLink = NavigationMenuPrimitive.Link
const NavigationMenuViewport = React.forwardRef(({ className, ...props }, ref) => (
<div className={cn("absolute left-0 top-full flex justify-center")}>
<NavigationMenuPrimitive.Viewport
className={cn(
"origin-top-center relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border bg-popover text-popover-foreground shadow data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 md:w-[var(--radix-navigation-menu-viewport-width)]",
className
)}
ref={ref}
{...props} />
</div>
))
NavigationMenuViewport.displayName =
NavigationMenuPrimitive.Viewport.displayName
const NavigationMenuIndicator = React.forwardRef(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.Indicator
ref={ref}
className={cn(
"top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in",
className
)}
{...props}>
<div
className="relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm bg-border shadow-md" />
</NavigationMenuPrimitive.Indicator>
))
NavigationMenuIndicator.displayName =
NavigationMenuPrimitive.Indicator.displayName
export {
navigationMenuTriggerStyle,
NavigationMenu,
NavigationMenuList,
NavigationMenuItem,
NavigationMenuContent,
NavigationMenuTrigger,
NavigationMenuLink,
NavigationMenuIndicator,
NavigationMenuViewport,
}

View File

@@ -1,100 +0,0 @@
import * as React from "react"
import { ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react"
import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button";
const Pagination = ({
className,
...props
}) => (
<nav
role="navigation"
aria-label="pagination"
className={cn("mx-auto flex w-full justify-center", className)}
{...props} />
)
Pagination.displayName = "Pagination"
const PaginationContent = React.forwardRef(({ className, ...props }, ref) => (
<ul
ref={ref}
className={cn("flex flex-row items-center gap-1", className)}
{...props} />
))
PaginationContent.displayName = "PaginationContent"
const PaginationItem = React.forwardRef(({ className, ...props }, ref) => (
<li ref={ref} className={cn("", className)} {...props} />
))
PaginationItem.displayName = "PaginationItem"
const PaginationLink = ({
className,
isActive,
size = "icon",
...props
}) => (
<a
aria-current={isActive ? "page" : undefined}
className={cn(buttonVariants({
variant: isActive ? "outline" : "ghost",
size,
}), className)}
{...props} />
)
PaginationLink.displayName = "PaginationLink"
const PaginationPrevious = ({
className,
...props
}) => (
<PaginationLink
aria-label="Go to previous page"
size="default"
className={cn("gap-1 pl-2.5", className)}
{...props}>
<ChevronLeft className="h-4 w-4" />
<span>Previous</span>
</PaginationLink>
)
PaginationPrevious.displayName = "PaginationPrevious"
const PaginationNext = ({
className,
...props
}) => (
<PaginationLink
aria-label="Go to next page"
size="default"
className={cn("gap-1 pr-2.5", className)}
{...props}>
<span>Next</span>
<ChevronRight className="h-4 w-4" />
</PaginationLink>
)
PaginationNext.displayName = "PaginationNext"
const PaginationEllipsis = ({
className,
...props
}) => (
<span
aria-hidden
className={cn("flex h-9 w-9 items-center justify-center", className)}
{...props}>
<MoreHorizontal className="h-4 w-4" />
<span className="sr-only">More pages</span>
</span>
)
PaginationEllipsis.displayName = "PaginationEllipsis"
export {
Pagination,
PaginationContent,
PaginationLink,
PaginationItem,
PaginationPrevious,
PaginationNext,
PaginationEllipsis,
}

View File

@@ -1,23 +0,0 @@
"use client"
import * as React from "react"
import * as ProgressPrimitive from "@radix-ui/react-progress"
import { cn } from "@/lib/utils"
const Progress = React.forwardRef(({ className, value, ...props }, ref) => (
<ProgressPrimitive.Root
ref={ref}
className={cn(
"relative h-2 w-full overflow-hidden rounded-full bg-primary/20",
className
)}
{...props}>
<ProgressPrimitive.Indicator
className="h-full w-full flex-1 bg-primary transition-all"
style={{ transform: `translateX(-${100 - (value || 0)}%)` }} />
</ProgressPrimitive.Root>
))
Progress.displayName = ProgressPrimitive.Root.displayName
export { Progress }

View File

@@ -1,42 +0,0 @@
"use client"
import { GripVertical } from "lucide-react"
import * as ResizablePrimitive from "react-resizable-panels"
import { cn } from "@/lib/utils"
const ResizablePanelGroup = ({
className,
...props
}) => (
<ResizablePrimitive.PanelGroup
className={cn(
"flex h-full w-full data-[panel-group-direction=vertical]:flex-col",
className
)}
{...props} />
)
const ResizablePanel = ResizablePrimitive.Panel
const ResizableHandle = ({
withHandle,
className,
...props
}) => (
<ResizablePrimitive.PanelResizeHandle
className={cn(
"relative flex w-px items-center justify-center bg-border after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-1 data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:-translate-y-1/2 data-[panel-group-direction=vertical]:after:translate-x-0 [&[data-panel-group-direction=vertical]>div]:rotate-90",
className
)}
{...props}>
{withHandle && (
<div
className="z-10 flex h-4 w-3 items-center justify-center rounded-sm border bg-border">
<GripVertical className="h-2.5 w-2.5" />
</div>
)}
</ResizablePrimitive.PanelResizeHandle>
)
export { ResizablePanelGroup, ResizablePanel, ResizableHandle }

View File

@@ -1,38 +0,0 @@
import * as React from "react"
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
import { cn } from "@/lib/utils"
const ScrollArea = React.forwardRef(({ className, children, ...props }, ref) => (
<ScrollAreaPrimitive.Root
ref={ref}
className={cn("relative overflow-hidden", className)}
{...props}>
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
))
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
const ScrollBar = React.forwardRef(({ className, orientation = "vertical", ...props }, ref) => (
<ScrollAreaPrimitive.ScrollAreaScrollbar
ref={ref}
orientation={orientation}
className={cn(
"flex touch-none select-none transition-colors",
orientation === "vertical" &&
"h-full w-2.5 border-l border-l-transparent p-[1px]",
orientation === "horizontal" &&
"h-2.5 flex-col border-t border-t-transparent p-[1px]",
className
)}
{...props}>
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
</ScrollAreaPrimitive.ScrollAreaScrollbar>
))
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
export { ScrollArea, ScrollBar }

View File

@@ -1,23 +0,0 @@
import * as React from "react"
import * as SeparatorPrimitive from "@radix-ui/react-separator"
import { cn } from "@/lib/utils"
const Separator = React.forwardRef((
{ className, orientation = "horizontal", decorative = true, ...props },
ref
) => (
<SeparatorPrimitive.Root
ref={ref}
decorative={decorative}
orientation={orientation}
className={cn(
"shrink-0 bg-border",
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
className
)}
{...props} />
))
Separator.displayName = SeparatorPrimitive.Root.displayName
export { Separator }

View File

@@ -1,29 +0,0 @@
"use client";
import { useTheme } from "next-themes"
import { Toaster as Sonner } from "sonner"
const Toaster = ({
...props
}) => {
const { theme = "system" } = useTheme()
return (
(<Sonner
theme={theme}
className="toaster group"
toastOptions={{
classNames: {
toast:
"group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg",
description: "group-[.toast]:text-muted-foreground",
actionButton:
"group-[.toast]:bg-primary group-[.toast]:text-primary-foreground",
cancelButton:
"group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
},
}}
{...props} />)
);
}
export { Toaster }

View File

@@ -1,86 +0,0 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Table = React.forwardRef(({ className, ...props }, ref) => (
<div className="relative w-full overflow-auto">
<table
ref={ref}
className={cn("w-full caption-bottom text-sm", className)}
{...props} />
</div>
))
Table.displayName = "Table"
const TableHeader = React.forwardRef(({ className, ...props }, ref) => (
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
))
TableHeader.displayName = "TableHeader"
const TableBody = React.forwardRef(({ className, ...props }, ref) => (
<tbody
ref={ref}
className={cn("[&_tr:last-child]:border-0", className)}
{...props} />
))
TableBody.displayName = "TableBody"
const TableFooter = React.forwardRef(({ className, ...props }, ref) => (
<tfoot
ref={ref}
className={cn("border-t bg-muted/50 font-medium [&>tr]:last:border-b-0", className)}
{...props} />
))
TableFooter.displayName = "TableFooter"
const TableRow = React.forwardRef(({ className, ...props }, ref) => (
<tr
ref={ref}
className={cn(
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
className
)}
{...props} />
))
TableRow.displayName = "TableRow"
const TableHead = React.forwardRef(({ className, ...props }, ref) => (
<th
ref={ref}
className={cn(
"h-10 px-2 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
className
)}
{...props} />
))
TableHead.displayName = "TableHead"
const TableCell = React.forwardRef(({ className, ...props }, ref) => (
<td
ref={ref}
className={cn(
"p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
className
)}
{...props} />
))
TableCell.displayName = "TableCell"
const TableCaption = React.forwardRef(({ className, ...props }, ref) => (
<caption
ref={ref}
className={cn("mt-4 text-sm text-muted-foreground", className)}
{...props} />
))
TableCaption.displayName = "TableCaption"
export {
Table,
TableHeader,
TableBody,
TableFooter,
TableHead,
TableRow,
TableCell,
TableCaption,
}

View File

@@ -1,44 +0,0 @@
import * as React from "react"
import * as TabsPrimitive from "@radix-ui/react-tabs"
import { cn } from "@/lib/utils"
const Tabs = TabsPrimitive.Root
/** @type {React.ForwardRefExoticComponent<any>} */
const TabsList = React.forwardRef(({ className, ...props }, ref) => (
<TabsPrimitive.List
ref={ref}
className={cn(
"inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground",
className
)}
{...props} />
))
TabsList.displayName = TabsPrimitive.List.displayName
/** @type {React.ForwardRefExoticComponent<any>} */
const TabsTrigger = React.forwardRef(({ className, ...props }, ref) => (
<TabsPrimitive.Trigger
ref={ref}
className={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow",
className
)}
{...props} />
))
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
/** @type {React.ForwardRefExoticComponent<any>} */
const TabsContent = React.forwardRef(({ className, ...props }, ref) => (
<TabsPrimitive.Content
ref={ref}
className={cn(
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
className
)}
{...props} />
))
TabsContent.displayName = TabsPrimitive.Content.displayName
export { Tabs, TabsList, TabsTrigger, TabsContent }

View File

@@ -1,44 +0,0 @@
"use client";
import * as React from "react"
import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group"
import { cn } from "@/lib/utils"
import { toggleVariants } from "@/components/ui/toggle"
const ToggleGroupContext = React.createContext({
size: "default",
variant: "default",
})
const ToggleGroup = React.forwardRef(({ className, variant, size, children, ...props }, ref) => (
<ToggleGroupPrimitive.Root
ref={ref}
className={cn("flex items-center justify-center gap-1", className)}
{...props}>
<ToggleGroupContext.Provider value={{ variant, size }}>
{children}
</ToggleGroupContext.Provider>
</ToggleGroupPrimitive.Root>
))
ToggleGroup.displayName = ToggleGroupPrimitive.Root.displayName
const ToggleGroupItem = React.forwardRef(({ className, children, variant, size, ...props }, ref) => {
const context = React.useContext(ToggleGroupContext)
return (
(<ToggleGroupPrimitive.Item
ref={ref}
className={cn(toggleVariants({
variant: context.variant || variant,
size: context.size || size,
}), className)}
{...props}>
{children}
</ToggleGroupPrimitive.Item>)
);
})
ToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName
export { ToggleGroup, ToggleGroupItem }

View File

@@ -1,38 +0,0 @@
import * as React from "react"
import * as TogglePrimitive from "@radix-ui/react-toggle"
import { cva } from "class-variance-authority";
import { cn } from "@/lib/utils"
const toggleVariants = cva(
"inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
{
variants: {
variant: {
default: "bg-transparent",
outline:
"border border-input bg-transparent shadow-sm hover:bg-accent hover:text-accent-foreground",
},
size: {
default: "h-9 px-2 min-w-9",
sm: "h-8 px-1.5 min-w-8",
lg: "h-10 px-2.5 min-w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
const Toggle = React.forwardRef(({ className, variant, size, ...props }, ref) => (
<TogglePrimitive.Root
ref={ref}
className={cn(toggleVariants({ variant, size, className }))}
{...props} />
))
Toggle.displayName = TogglePrimitive.Root.displayName
export { Toggle, toggleVariants }

View File

@@ -1,19 +0,0 @@
import * as React from "react"
const MOBILE_BREAKPOINT = 768
export function useIsMobile() {
const [isMobile, setIsMobile] = React.useState(undefined)
React.useEffect(() => {
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
const onChange = () => {
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
}
mql.addEventListener("change", onChange)
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
return () => mql.removeEventListener("change", onChange);
}, [])
return !!isMobile
}

View File

@@ -15,7 +15,7 @@ import {
} from '@/components/ui/dropdown-menu';
import { Sheet, SheetContent, SheetHeader, SheetTitle } from '@/components/ui/sheet';
import { AssistantPanel, AssistantPanelProvider } from '@/components/ai-assistant';
import { endAdminSession } from '@/pages/admin/session';
import { endAdminSession } from '@/lib/admin/session';
import { useCurrentUser } from '@/lib/krowHooks';
/**

View File

@@ -9,7 +9,7 @@ import {
import { activitySignals, signalLabel } from '@/lib/activitySignals';
import { demandFor } from '@/lib/workforce';
import { getScoreBand } from '@/lib/talentHome';
import { buildPosition } from '@/pages/admin/positionInsights';
import { buildPosition } from '@/lib/admin/positionInsights';
/**
* The one place a skill's declared data source becomes real data.

View File

@@ -1,160 +0,0 @@
import React from 'react';
import { useNavigate } from 'react-router-dom';
import { motion } from 'framer-motion';
import { useJobPostings, useApplications, useStaff } from '@/lib/krowHooks';
import PipelineFunnel from '@/components/krow/PipelineFunnel';
import KROWCandidateLeaderboard from '@/components/krow/KROWCandidateLeaderboard';
import { Briefcase, Users, Brain, ClipboardCheck, UserCheck, BadgeCheck, Gauge, ChevronRight, Sparkles } from 'lucide-react';
import { AssistantTrigger } from '@/components/ai-assistant';
const METRICS = [
{ icon: Briefcase, label: 'Open Positions', bg: 'bg-[#0838E0]', glow: 'shadow-blue-600/20' },
{ icon: Users, label: 'Applicants', bg: 'bg-[#333F48]', glow: 'shadow-slate-600/20' },
{ icon: Brain, label: 'AI Screened', bg: 'bg-[#0838E0]', glow: 'shadow-blue-600/20' },
{ icon: ClipboardCheck, label: 'Shortlisted', bg: 'bg-[#F9E547]', glow: 'shadow-yellow-400/30' },
{ icon: UserCheck, label: 'Hired', bg: 'bg-[#0838E0]', glow: 'shadow-blue-600/20' },
{ icon: BadgeCheck, label: 'Active Staff', bg: 'bg-[#333F48]', glow: 'shadow-slate-600/20' },
{ icon: Gauge, label: 'Avg KROW Score', bg: 'bg-[#0838E0]', glow: 'shadow-blue-600/20' },
{ icon: Sparkles, label: 'AI Interviews', bg: 'bg-[#F9E547]', glow: 'shadow-yellow-400/30' },
];
function MetricCard({ icon: Icon, value, label, bg, glow, index }) {
const isYellow = bg === 'bg-[#F9E547]';
return (
<motion.div
initial={{ opacity: 0, y: 16 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.05, duration: 0.4, ease: 'easeOut' }}
whileHover={{ y: -4, transition: { duration: 0.2 } }}
className="glass-card border border-white/60 rounded-2xl p-4 shadow-sm hover:shadow-lg transition-shadow"
>
<div className={`w-10 h-10 rounded-xl ${bg} flex items-center justify-center shadow-lg ${glow} mb-3`}>
<Icon className={`w-5 h-5 ${isYellow ? 'text-[#0838E0]' : 'text-white'}`} />
</div>
<div className="text-3xl font-heading font-bold gradient-text">{value}</div>
<div className="text-[11px] text-[#9CA3AF] font-medium tracking-wide mt-0.5">{label}</div>
</motion.div>
);
}
export default function AdminDashboard() {
const navigate = useNavigate();
const { data: postings = [] } = useJobPostings();
const { data: applications = [] } = useApplications();
const { data: staff = [] } = useStaff();
const openPositions = postings.filter(p => p.status === 'active');
const screened = applications.filter(a => ['ai_screened', 'shortlisted', 'interview', 'hired'].includes(a.status));
const shortlisted = applications.filter(a => ['shortlisted', 'interview', 'hired'].includes(a.status));
const hired = applications.filter(a => a.status === 'hired');
const activeStaff = staff.filter(s => s.status === 'active' || s.status === 'onboarding');
const scored = applications.filter(a => a.ai_score > 0);
const avgScore = scored.length ? Math.round(scored.reduce((s, a) => s + a.ai_score, 0) / scored.length) : 0;
const interviews = applications.filter(a => a.status === 'interview' || a.interview_id).length;
const values = [
openPositions.length,
applications.length,
screened.length,
shortlisted.length,
hired.length,
activeStaff.length,
avgScore,
interviews,
];
const statsForPosting = (id) => {
const apps = applications.filter(a => a.job_posting_id === id);
return { applied: apps.length, screened: apps.filter(a => a.status !== 'applied').length, hired: apps.filter(a => a.status === 'hired').length };
};
const recentStaff = [...activeStaff].sort((a, b) => new Date(b.hire_date || 0).getTime() - new Date(a.hire_date || 0).getTime()).slice(0, 6);
return (
<div className="space-y-8">
{/* Hero */}
<motion.div initial={{ opacity: 0, y: -12 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.5 }} className="flex items-center justify-between flex-wrap gap-4">
<div>
<span className="text-[11px] font-medium tracking-wide text-[#0838E0] bg-[#EEF3FE] px-2.5 py-1 rounded-full">Admin · Employer</span>
<h1 className="text-3xl font-heading font-bold text-[#111827] mt-2">Workforce <span className="gradient-text">Control Center</span></h1>
<p className="text-[14px] text-[#6B7280] mt-1">Full hiring pipeline, staffing & talent intelligence in one view</p>
</div>
<div className="flex items-center gap-2">
<AssistantTrigger />
</div>
</motion.div>
{/* Metrics */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{METRICS.map((m, i) => (
<MetricCard key={m.label} icon={m.icon} value={values[i]} label={m.label} bg={m.bg} glow={m.glow} index={i} />
))}
</div>
{/* Pipeline funnel + Active positions */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<PipelineFunnel applications={applications} />
<motion.div initial={{ opacity: 0, x: 16 }} animate={{ opacity: 1, x: 0 }} transition={{ delay: 0.3 }} className="glass-card border border-white/60 rounded-2xl p-6 shadow-sm">
<div className="flex items-center justify-between mb-5">
<h3 className="font-heading font-semibold text-[#111827] text-[16px]">Active Positions</h3>
<button onClick={() => navigate('/positions')} className="text-[12px] text-[#0838E0] hover:underline inline-flex items-center gap-1 font-medium">View all <ChevronRight className="w-3 h-3" /></button>
</div>
<div className="space-y-1">
{openPositions.length === 0 && (
<div className="text-center py-10">
<div className="w-12 h-12 rounded-full bg-white/50 flex items-center justify-center mx-auto mb-3"><Briefcase className="w-5 h-5 text-[#9CA3AF]" /></div>
<p className="text-[13px] text-[#9CA3AF]">No active positions</p>
</div>
)}
{openPositions.slice(0, 6).map((p, idx) => {
const s = statsForPosting(p.id);
return (
<motion.div key={p.id} initial={{ opacity: 0, x: 8 }} animate={{ opacity: 1, x: 0 }} transition={{ delay: 0.4 + idx * 0.05 }} onClick={() => navigate(`/positions/${p.id}`)} className="flex items-center gap-3 p-2.5 rounded-xl hover:bg-white/50 transition-colors cursor-pointer group">
<div className="w-1 h-9 rounded-full bg-[#0838E0] shrink-0" />
<div className="flex-1 min-w-0">
<div className="text-[13px] font-medium text-[#111827] truncate">{p.title}</div>
<div className="text-[11px] text-[#6B7280]">{s.applied} applied · {s.screened} screened · {s.hired} hired</div>
</div>
<ChevronRight className="w-4 h-4 text-[#D1D5DB] group-hover:text-[#0838E0] transition-colors" />
</motion.div>
);
})}
</div>
</motion.div>
</div>
{/* Leaderboard + Active staff */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<KROWCandidateLeaderboard applications={applications} />
<motion.div initial={{ opacity: 0, x: 16 }} animate={{ opacity: 1, x: 0 }} transition={{ delay: 0.4 }} className="glass-card border border-white/60 rounded-2xl p-6 shadow-sm">
<div className="flex items-center justify-between mb-5">
<h3 className="font-heading font-semibold text-[#111827] text-[16px]">Active Staff</h3>
<span className="text-[12px] text-[#6B7280]">{activeStaff.length} on roster</span>
</div>
<div className="space-y-1">
{recentStaff.length === 0 && (
<div className="text-center py-10">
<div className="w-12 h-12 rounded-full bg-white/50 flex items-center justify-center mx-auto mb-3"><UserCheck className="w-5 h-5 text-[#9CA3AF]" /></div>
<p className="text-[13px] text-[#9CA3AF]">No hired staff yet</p>
</div>
)}
{recentStaff.map((s, _idx) => (
<div key={s.id} className="flex items-center gap-3 p-2.5 rounded-xl hover:bg-white/50 transition-colors">
<div className="w-9 h-9 rounded-full bg-[#333F48] flex items-center justify-center text-white text-[12px] font-semibold shrink-0">
{s.name?.split(' ').map(w => w[0]).slice(0, 2).join('').toUpperCase()}
</div>
<div className="flex-1 min-w-0">
<div className="text-[13px] font-medium text-[#111827] truncate">{s.name}</div>
<div className="text-[11px] text-[#6B7280] truncate">{s.role} · <span className="text-[#0838E0] font-medium">{s.profile_tier}</span></div>
</div>
<span className={`text-[10px] px-2 py-0.5 rounded-full font-medium ${s.status === 'active' ? 'bg-[#ECFDF5] text-[#065F46]' : 'bg-[#FFFBEB] text-[#92400E]'}`}>{s.status}</span>
</div>
))}
</div>
</motion.div>
</div>
</div>
);
}

View File

@@ -13,7 +13,7 @@ export default function Analytics() {
const { data: applications = [], isLoading: appsLoading } = useApplications();
const { data: interviews = [], isLoading: intLoading } = useInterviews();
const { data: staff = [], isLoading: staffLoading } = useStaff();
const { data: postings = [] } = useJobPostings();
useJobPostings();
if (appsLoading || intLoading || staffLoading) {
return (

View File

@@ -15,7 +15,7 @@ export default function Apply() {
const jobId = searchParams.get('job');
const { data: postings = [], isLoading } = useJobPostings();
const createApplication = useCreateApplication();
const { data: profile, isLoading: profileLoading } = useWorkerProfile();
const { data: profile } = useWorkerProfile();
const { data: courses = [] } = useCourses();
const [selectedJob, setSelectedJob] = useState(null);
const [showForm, setShowForm] = useState(false);
@@ -65,7 +65,7 @@ export default function Apply() {
toast.success('Application created from your KROW Identity!');
setShowForm(false);
setInterviewApp({ ...app, job_posting_id: selectedJob.id });
} catch (e) {
} catch {
toast.error('Failed to start application');
} finally {
setStarting(false);

View File

@@ -24,7 +24,7 @@ export default function Candidates() {
try {
await updateApp.mutateAsync({ id: app.id, data: { status: 'rejected' } });
toast.success(`${app.applicant_name} declined`);
} catch (e) {
} catch {
toast.error('Failed to decline candidate');
}
};
@@ -33,7 +33,7 @@ export default function Candidates() {
try {
await base44.entities.JobApplication.delete(app.id);
toast.success(`${app.applicant_name} deleted`);
} catch (e) {
} catch {
toast.error('Failed to delete candidate');
}
};
@@ -79,7 +79,7 @@ export default function Candidates() {
try {
await screenAll.mutateAsync({ applications: unscreened, job: firstJob });
toast.success(`Screened ${unscreened.length} candidates`);
} catch (e) {
} catch {
toast.error('Screening failed');
}
};

View File

@@ -137,7 +137,7 @@ export default function CreatePosition({ prefill: prefillProp, embedded = false,
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 });
setAiResult(result);
toast.success('Job description generated!');
} catch (e) {
} catch {
toast.error('AI generation failed. Please try again.');
}
};
@@ -181,7 +181,7 @@ export default function CreatePosition({ prefill: prefillProp, embedded = false,
} else {
navigate('/admin/positions');
}
} catch (e) {
} catch {
toast.error('Failed to save');
}
};

View File

@@ -13,19 +13,6 @@ const QUESTION_COUNT = 6;
const AVAIL_TOKENS = ['Weekdays', 'Weekends', 'Evenings', 'Mornings', 'Overnight', 'On-Call'];
// Owliver's wisdom — Career DNA: a living professional identity, built from signals not claims
const LEARNS = [
'Identity — who they are',
'Skills — what they can do',
'Experience — what they proved',
'Learning speed — how fast they grow',
'Reliability — can employers trust them',
'Communication — can they explain',
'Leadership — do people follow them',
'Adaptability — can they pivot',
'Work style — builder, operator, leader',
'Culture match — where they thrive',
'Reputation — verified over time',
];
const UI = {
en: {
@@ -106,7 +93,7 @@ export default function Owliver({ compact = false }) {
const [transcript, setTranscript] = useState('');
const [isListening, setIsListening] = useState(false);
const [isSpeaking, setIsSpeaking] = useState(false);
const [timeLeft, setTimeLeft] = useState(TOTAL_SECONDS);
const [, setTimeLeft] = useState(TOTAL_SECONDS);
const [extracted, setExtracted] = useState(null);
const [error, setError] = useState('');
const [ttsEnabled, setTtsEnabled] = useState(true);
@@ -169,7 +156,7 @@ export default function Owliver({ compact = false }) {
setMessages(newMessages);
lastUserTime.current = Date.now();
await speak(question);
} catch (e) {
} catch {
setError('Owliver lost his train of thought. Try again.');
}
}, [workerName, speak, language]);
@@ -266,7 +253,7 @@ export default function Owliver({ compact = false }) {
}
setExtracted({ ...result, availability: normalizeAvail(result.availability) });
setPhase('done');
} catch (e) {
} catch {
setError('Owliver could not finish building your profile. Try again.');
setPhase('active');
}

View File

@@ -174,7 +174,7 @@ export default function PositionDetail() {
try {
const app = await admitTalent(profile);
setMessageApp(app);
} catch (e) {
} catch {
toast.error('Could not open talent');
}
};
@@ -183,7 +183,7 @@ export default function PositionDetail() {
try {
const app = await admitTalent(profile, 'interview');
setScheduleApp(app);
} catch (e) {
} catch {
toast.error('Could not schedule talent');
}
};
@@ -193,7 +193,7 @@ export default function PositionDetail() {
const app = await admitTalent(profile, 'interview');
await hireCandidate.mutateAsync({ application: app, job });
toast.success(`${profile.full_name} hired from the talent pool!`);
} catch (e) {
} catch {
toast.error('Failed to hire');
}
};
@@ -209,7 +209,7 @@ export default function PositionDetail() {
try {
await updateJob.mutateAsync({ id, data: { status: newStatus } });
toast.success(`Position ${newStatus === 'active' ? 'is now hiring' : newStatus}`);
} catch (e) {
} catch {
toast.error('Failed to update status');
}
};
@@ -223,7 +223,7 @@ export default function PositionDetail() {
try {
await screenAll.mutateAsync({ applications: unscreened, job });
toast.success(`Screened ${unscreened.length} applicants`);
} catch (e) {
} catch {
toast.error('Screening failed');
}
};
@@ -232,7 +232,7 @@ export default function PositionDetail() {
try {
await hireCandidate.mutateAsync({ application: app, job });
toast.success(`${app.applicant_name} hired! Staff record created.`);
} catch (e) {
} catch {
toast.error('Failed to hire');
}
};
@@ -249,7 +249,7 @@ export default function PositionDetail() {
try {
await updateApp.mutateAsync({ id: app.id, data: { status: 'rejected' } });
toast.success(`${app.applicant_name} declined`);
} catch (e) {
} catch {
toast.error('Failed to decline candidate');
}
};
@@ -258,7 +258,7 @@ export default function PositionDetail() {
try {
await base44.entities.JobApplication.delete(app.id);
toast.success(`${app.applicant_name} deleted`);
} catch (e) {
} catch {
toast.error('Failed to delete candidate');
}
};

View File

@@ -7,7 +7,7 @@ import {
AddSkillTraining, ForgeHeader, ForgeProgress, ForgeRail, SkillDetail, SkillLibrary,
SkillProgressCard, SkillProgressDetail, statusOf, workforceUsage,
} from '@/components/forge';
import { SectionTitle } from '@/pages/admin/_shell';
import { SectionTitle } from '@/components/admin/PageShell';
import { FilterSelect } from '@/pages/admin/Positions';
import { completedCourseIds, skillStates } from '@/lib/skillGraph';
import { SkillSurface } from '@/components/skills/SkillSurface';

View File

@@ -6,7 +6,7 @@ import {
Avatar, Badge, DataTable, MetricStrip, SearchInput, Surface, Timeline,
} from '@/components/ds';
import { useUserActivity } from '@/lib/krowHooks';
import { AdminPage, SectionTitle, Toolbar } from '@/pages/admin/_shell';
import { AdminPage, SectionTitle, Toolbar } from '@/components/admin/PageShell';
import { FilterSelect } from '@/pages/admin/Positions';
import { SkillSurface } from '@/components/skills/SkillSurface';

View File

@@ -1,5 +1,5 @@
import { Navigate, Outlet, useLocation } from 'react-router-dom';
import { hasAdminSession } from './session';
import { hasAdminSession } from '@/lib/admin/session';
/**
* Gate on the Admin console.

View File

@@ -9,7 +9,7 @@ import { DepartmentPerformance } from '@/components/charts/DepartmentPerformance
import { HiringFlow } from '@/components/charts/HiringFlow';
import { HiringTrendChart } from '@/components/charts/HiringTrendChart';
import { useSize } from '@/hooks/use-size';
import { AdminPage, SectionTitle } from '@/pages/admin/_shell';
import { AdminPage, SectionTitle } from '@/components/admin/PageShell';
import { SkillSurface } from '@/components/skills/SkillSurface';
import {
buildEfficiency, buildFunnel, buildInsights, buildTrend, byDepartment, byPosition,

View File

@@ -3,7 +3,7 @@ import { Button, SearchInput } from '@/components/ds';
import { useApplications, useInterviews, useJobPostings, useUpdateApplication, useHireCandidate } from '@/lib/krowHooks';
import { base44 } from '@/api/base44Client';
import { toast } from 'react-hot-toast';
import { AdminPage, Toolbar } from '@/pages/admin/_shell';
import { AdminPage, Toolbar } from '@/components/admin/PageShell';
import { FilterSelect } from '@/pages/admin/Positions';
import CandidateCard from '@/components/krow/CandidateCard';
import { SkillSurface } from '@/components/skills/SkillSurface';
@@ -29,7 +29,7 @@ const SORTS = {
export default function AdminCandidates() {
const { data: applications = [], isLoading } = useApplications();
const { data: postings = [] } = useJobPostings();
const { data: interviews = [] } = useInterviews();
useInterviews();
const updateApp = useUpdateApplication();
const hireCandidate = useHireCandidate();
@@ -47,7 +47,7 @@ export default function AdminCandidates() {
try {
await updateApp.mutateAsync({ id: app.id, data: { status: 'rejected' } });
toast.success(`${app.applicant_name} declined`);
} catch (e) {
} catch {
toast.error('Failed to decline candidate');
}
};
@@ -56,7 +56,7 @@ export default function AdminCandidates() {
try {
await base44.entities.JobApplication.delete(app.id);
toast.success(`${app.applicant_name} deleted`);
} catch (e) {
} catch {
toast.error('Failed to delete candidate');
}
};
@@ -66,7 +66,7 @@ export default function AdminCandidates() {
const job = postings.find(p => p.id === app.job_posting_id);
await hireCandidate.mutateAsync({ application: app, job });
toast.success(`${app.applicant_name} hired!`);
} catch (e) {
} catch {
toast.error('Failed to hire candidate');
}
};

View File

@@ -10,7 +10,7 @@ import {
} from '@/components/ds';
import { useApplications, useInterviews, useJobPostings, useStaff, useWorkerProfiles } from '@/lib/krowHooks';
import { buildFacts } from '@/components/ai-assistant/insights';
import { AdminPage, SectionTitle } from '@/pages/admin/_shell';
import { AdminPage, SectionTitle } from '@/components/admin/PageShell';
import { SkillSurface } from '@/components/skills/SkillSurface';
/**

View File

@@ -16,7 +16,7 @@ import {
useApplications, useInterviews, useJobPostings, useStaff, useUserActivity, useWorkerProfiles,
} from '@/lib/krowHooks';
import { buildFacts } from '@/components/ai-assistant/insights';
import { AdminPage } from '@/pages/admin/_shell';
import { AdminPage } from '@/components/admin/PageShell';
import { SkillSurface } from '@/components/skills/SkillSurface';
/**

View File

@@ -6,7 +6,7 @@ import {
Surface, Timeline,
} from '@/components/ds';
import { useApplications, useJobPostings, useStaff } from '@/lib/krowHooks';
import { AdminPage, SectionTitle } from '@/pages/admin/_shell';
import { AdminPage, SectionTitle } from '@/components/admin/PageShell';
import { SkillSurface } from '@/components/skills/SkillSurface';
import { hiresWithFill, summarise } from '@/lib/hiringRecords';

View File

@@ -5,7 +5,7 @@ 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 '@/pages/admin/session';
import { startAdminSession } from '@/lib/admin/session';
const DEMO_EMAIL = 'demo@krow.app';
@@ -80,7 +80,7 @@ export default function AdminLogin() {
<div className="relative z-10 my-auto py-6 flex flex-col items-center">
<div className="relative rounded-2xl overflow-hidden shadow-md border border-[#E2E8F0] bg-white p-2 max-w-xs">
<img
src="/krow%20logo.jpeg"
src="/krow-logo-visual.jpeg"
alt="KROW Logo Visual"
className="w-full h-auto object-cover rounded-xl transition-transform duration-500 hover:scale-105"
/>

View File

@@ -1,72 +0,0 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { ChevronRight } from 'lucide-react';
import { cn } from '@/lib/utils';
import { Surface } from '@/components/ds';
import { SectionTitle } from '@/pages/admin/_shell';
/**
* The entry-point row, and the group it lives in.
*
* Settings and Workspace & Skills are two different subjects, but they are the
* same *kind* of page: a short list of destinations, each saying what it manages
* and how much of it there is. Sharing the furniture is what stops the two from
* drifting into different-looking navigation for the same job — and it is the
* reason a row added to either one needs no styling decisions.
*
* `meta` is always a real figure counted from real records. A row with nothing
* to count simply omits it rather than showing a zero that means "not measured".
*/
/** One destination. The whole row is the target, so there is no small link. */
export function NavRow({ icon: Icon, title, description, meta = null, to, action = null }) {
return (
<Link
to={to}
className="group flex items-center gap-3.5 px-4 py-3.5 transition-colors hover:bg-surface-subtle
focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-krow-blue/50"
>
<span className="grid h-9 w-9 shrink-0 place-items-center rounded-xl bg-krow-blue-tint text-krow-blue">
<Icon className="h-4 w-4" aria-hidden="true" />
</span>
<span className="min-w-0 flex-1">
<span className="block text-body-sm font-semibold text-ink-1">{title}</span>
<span className="block text-caption leading-relaxed text-ink-3">{description}</span>
{/* Below `sm` the count sits under the description rather than being
dropped: on a phone it is the same fact, just in the only place
there is room for it. */}
{meta && <span className="mt-0.5 block text-caption tabular-nums text-ink-4 sm:hidden">{meta}</span>}
</span>
{meta && <span className="hidden shrink-0 text-caption tabular-nums text-ink-4 sm:block">{meta}</span>}
{action && (
<span className="hidden shrink-0 text-caption font-semibold text-krow-blue md:block">{action}</span>
)}
<ChevronRight
className="h-4 w-4 shrink-0 text-ink-4 transition-transform duration-base group-hover:translate-x-0.5"
aria-hidden="true"
/>
</Link>
);
}
/** One group of rows. Same container and spacing in every group, on both pages. */
export function NavGroup({ id, title, children, className }) {
return (
<section aria-labelledby={id} className={cn('space-y-3', className)}>
<SectionTitle id={id} title={title} />
<Surface
variant="solid"
radius="lg"
padding="none"
elevation="xs"
className="divide-y divide-border overflow-hidden"
>
{children}
</Surface>
</section>
);
}

View File

@@ -19,7 +19,7 @@ import {
periodLabel, sectionTypeLabel, shapeForCapability, sourceSupportsOption, sourcesForShape,
surfaceFor,
} from '@/lib/skills/surfaces';
import { AdminPage, SectionTitle } from '@/pages/admin/_shell';
import { AdminPage, SectionTitle } from '@/components/admin/PageShell';
/**
* Add or edit an Owliver skill.

View File

@@ -21,9 +21,9 @@ import {
import { SkillSurface } from '@/components/skills/SkillSurface';
import { useAssistantPanel, usePublishPageContext } from '@/components/ai-assistant';
import { continueDraftRequest } from '@/lib/skills/draftFlow';
import { AdminPage, SectionTitle } from '@/pages/admin/_shell';
import { RoleGlyph } from '@/pages/admin/RoleGlyph';
import { buildPosition, payLabel } from '@/pages/admin/positionInsights';
import { AdminPage, SectionTitle } from '@/components/admin/PageShell';
import { RoleGlyph } from '@/components/admin/RoleGlyph';
import { buildPosition, payLabel } from '@/lib/admin/positionInsights';
/**
* Admin Positions — a hiring workspace, one card per role.

View File

@@ -9,9 +9,9 @@ import { Button, Field, Input, Modal, Toggle, toast,
} from '@/components/ds';
import { base44 } from '@/api/base44Client';
import { useCurrentUser, useUpdateProfile, useUserActivity } from '@/lib/krowHooks';
import { AdminPage } from '@/pages/admin/_shell';
import { endAdminSession } from '@/pages/admin/session';
import { PERMISSIONS } from '@/pages/admin/permissions';
import { AdminPage } from '@/components/admin/PageShell';
import { endAdminSession } from '@/lib/admin/session';
import { PERMISSIONS } from '@/lib/admin/permissions';
import { SkillSurface } from '@/components/skills/SkillSurface';
/**

View File

@@ -1,5 +1,5 @@
import React, { useMemo } from 'react';
import { useNavigate, Link } from 'react-router-dom';
import { Link } from 'react-router-dom';
import {
Activity, Bell, Building2, KeyRound, Shield, User as UserIcon, Users,
ChevronRight, Sliders,
@@ -10,14 +10,13 @@ import { useAssistantPanel } from '@/components/ai-assistant';
import {
useJobPostings, usePreferences, useStaff, useUpdatePreferences,
} from '@/lib/krowHooks';
import { AdminPage } from '@/pages/admin/_shell';
import { AdminPage } from '@/components/admin/PageShell';
/**
* Admin Settings — Full-width platform configuration control center.
*/
export default function AdminSettings() {
const navigate = useNavigate();
const preferences = usePreferences();
const updatePreferences = useUpdatePreferences();
const panel = useAssistantPanel();

View File

@@ -7,7 +7,7 @@ import { useCourses, useCurrentUser, useWorkerProfiles } from '@/lib/krowHooks';
import { useWorkforcePaths } from '@/lib/skills/usePageSkills';
import { profileForEmail } from '@/lib/skillGraph';
import { SkillProgressDetail } from '@/components/forge';
import { AdminPage } from '@/pages/admin/_shell';
import { AdminPage } from '@/components/admin/PageShell';
/**
* Skill development — every training path, as a management list.

Some files were not shown because too many files have changed in this diff Show More