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

View File

@@ -0,0 +1,97 @@
import * as React from 'react';
import { ChevronRight } from 'lucide-react';
import { cn } from '@/lib/utils';
/**
* Shared page furniture for the Admin product.
*
* Admin pages are dense and numerous, so the header, the section labels and the
* vertical rhythm are defined once here. That is what makes seven independently
* built pages read as one product rather than seven takes on a dashboard.
*/
/**
* AdminPage — the standard page frame: title, subtitle, right-aligned controls,
* then stacked sections.
*
* Controls live in the page rather than in the global chrome, which is why the
* Admin header only needs one 56px bar.
*/
/** @param {any} props */
export function AdminPage({ title, subtitle, meta, actions, tabs, children, className }) {
return (
<div className={cn('space-y-6', className)}>
<header className="flex flex-wrap items-center justify-between gap-4 border-b border-border/50 pb-4">
<div className="min-w-0 space-y-1">
<div className="flex items-center gap-2.5">
<h1 className="font-heading text-title-lg font-bold tracking-tight text-ink-1 sm:text-2xl">{title}</h1>
{/* A count of whatever the page is a list of, stated beside its
name rather than only inside the list. */}
{meta && <span className="text-body-sm tabular-nums text-ink-4">{meta}</span>}
<span className="inline-flex items-center gap-1.5 rounded-full bg-blue-50 px-2.5 py-0.5 text-[11px] font-semibold text-blue-600 dark:bg-blue-950/60 dark:text-blue-400 border border-blue-200/60 dark:border-blue-800/40">
<span className="h-1.5 w-1.5 rounded-full bg-blue-600 dark:bg-blue-400" />
Live System
</span>
</div>
{subtitle && <p className="text-body-sm text-ink-3 font-normal">{subtitle}</p>}
</div>
{actions && <div className="flex flex-wrap items-center gap-2.5">{actions}</div>}
</header>
{tabs}
{children}
</div>
);
}
/**
* SectionTitle — the label above a region.
*
* Sentence-case and small rather than a heavy all-caps overline: on a page with
* four or five sections, loud labels compete with the data they introduce.
*/
/** @param {any} props */
export function SectionTitle({ id, title, meta, action, className }) {
return (
<div className={cn('flex flex-wrap items-baseline justify-between gap-2', className)}>
<div className="flex items-baseline gap-2">
<h2 id={id} className="font-heading text-body font-semibold text-ink-1">{title}</h2>
{meta && <span className="text-caption text-ink-4">{meta}</span>}
</div>
{action && (
<button
type="button"
onClick={action.onClick}
className="inline-flex items-center gap-0.5 rounded text-caption font-medium text-krow-blue transition-colors hover:underline
focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-krow-blue/50"
>
{action.label}
<ChevronRight className="h-3 w-3" aria-hidden="true" />
</button>
)}
</div>
);
}
/**
* Toolbar — the controls row above a table or list.
*
* Search takes the remaining space, filters keep their natural width, and the
* primary action is pushed to the end, so every management page has the same
* control geometry.
*/
/** @param {any} props */
export function Toolbar({ search = null, filters = null, actions = null, meta = null, className = '' }) {
return (
<div className={cn('space-y-2', className)}>
<div className="flex flex-col gap-2 lg:flex-row lg:items-center">
{search && <div className="min-w-0 lg:max-w-xs lg:flex-1">{search}</div>}
{filters && <div className="flex flex-wrap items-center gap-2">{filters}</div>}
{actions && <div className="flex items-center gap-2 lg:ml-auto">{actions}</div>}
</div>
{meta && <p className="text-caption text-ink-4">{meta}</p>}
</div>
);
}

View File

@@ -0,0 +1,73 @@
import {
Briefcase, ChefHat, ConciergeBell, HardHat, PackageSearch, ShieldCheck, Sparkles, Truck, Wine,
} from 'lucide-react';
import { cn } from '@/lib/utils';
/**
* RoleGlyph — the visual identity on a position card.
*
* Positions in this dataset have no photograph, and there is nothing to
* photograph anyway: a role is not a person. The two obvious fallbacks are both
* worse than this one — two-letter initials ("BA", "SE") read as a placeholder
* someone forgot to replace, and stock photography attaches an invented face to a
* job requisition, which is misleading on a page about hiring real people.
*
* So the glyph is the role's own trade: a chef's hat, a wine glass, a hard hat.
* It is drawn from the existing icon language and the existing brand tint, it is
* stable for a given category, and it tells you what kind of work the position is
* from across the room — which is the actual job of an image in a card grid.
*/
/** Category to icon. Matched loosely, so a new "Head Chef" still gets the hat. */
/** @type {[RegExp, React.ComponentType<any>][]} */
const GLYPHS = [
[/chef|cook|kitchen/i, ChefHat],
[/bartend|bar|mixolog/i, Wine],
[/server|wait|host|banquet|concierge/i, ConciergeBell],
[/security|guard|officer/i, ShieldCheck],
[/pick|warehouse|stock|inventory/i, PackageSearch],
[/driver|delivery|logistic/i, Truck],
[/construct|labour|labor|site/i, HardHat],
];
export function iconForRole(category, title = '') {
const subject = `${category || ''} ${title || ''}`;
const match = GLYPHS.find(([pattern]) => pattern.test(subject));
return match ? match[1] : Briefcase;
}
const SIZES = {
sm: { box: 'h-9 w-9 rounded-lg', icon: 'h-4 w-4' },
md: { box: 'h-11 w-11 rounded-xl', icon: 'h-5 w-5' },
lg: { box: 'h-14 w-14 rounded-xl', icon: 'h-6 w-6' },
};
/** @param {any} props */
export function RoleGlyph({ category, title, size = 'md', aiGenerated = false, className = '' }) {
const Icon = iconForRole(category, title);
const s = SIZES[size] || SIZES.md;
return (
<span
className={cn(
'relative grid shrink-0 place-items-center bg-krow-blue-tint text-krow-blue ring-1 ring-krow-blue/10',
s.box,
className
)}
aria-hidden="true"
>
<Icon className={s.icon} />
{/* AI-built positions carry the same spark used everywhere else for
generated content, so provenance is legible without a second badge
competing for room in the card header. */}
{aiGenerated && (
<span className="absolute -right-1 -top-1 grid h-4 w-4 place-items-center rounded-full bg-krow-yellow text-ink-1 ring-2 ring-white">
<Sparkles className="h-2 w-2" />
</span>
)}
</span>
);
}
export default RoleGlyph;

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 }