Files
doormilxpress_astryx/src/components/forge/SkillDetail.jsx
2026-08-17 17:33:56 +05:30

186 lines
7.6 KiB
JavaScript

import React from 'react';
import { Link } from 'react-router-dom';
import { ArrowRight, Check, Sparkles } from 'lucide-react';
import { cn } from '@/lib/utils';
import { Badge, Drawer } from '@/components/ds';
import {
DIFF_TONE, owliverCriteria, statusOf, STATUS_TONE, trainingKinds, trainingOutline,
TYPE_HINT, TYPE_LABEL, typeOf,
} from './challengeMeta';
/**
* One skill, read end to end: what it teaches, how it is proven, what Owliver
* checks, what passing means, and how the workforce has done with it.
*
* A drawer rather than a route, because this is the managing view of a record
* whose route already belongs to something else — `/admin/university/:id` runs
* the challenge itself, which is the worker's experience and stays untouched.
* The link out to it is offered at the bottom for an admin who wants to see what
* the workforce sees.
*
* Sections appear only when the record has something to put in them. Reading a
* heading with nothing under it teaches the reader to distrust the rest.
*/
export function SkillDetail({ course, open, onOpenChange, usage }) {
if (!course) return null;
const status = statusOf(course);
const type = typeOf(course);
const criteria = owliverCriteria(course);
const outline = trainingOutline(course);
const kinds = trainingKinds(course);
const rubric = course.challenge?.rubric || [];
return (
<Drawer
open={open}
onOpenChange={onOpenChange}
size="lg"
title={course.title}
description={`${course.category}${course.difficulty ? ` · ${course.difficulty}` : ''}`}
>
<div className="space-y-6 px-5 py-5">
<div className="flex flex-wrap items-center gap-2">
<Badge variant={STATUS_TONE[status] || 'neutral'} size="sm">{status}</Badge>
{course.difficulty && (
<span className={cn('text-caption font-semibold capitalize', DIFF_TONE[course.difficulty] || 'text-ink-3')}>
{course.difficulty}
</span>
)}
{course.estimated_minutes > 0 && (
<span className="text-caption text-ink-4">{course.estimated_minutes} min</span>
)}
{course.proof_skill && (
<span className="text-caption text-ink-4">· Verifies {course.proof_skill}</span>
)}
</div>
{course.description && (
<p className="text-body-sm leading-relaxed text-ink-2">{course.description}</p>
)}
<Block title="Training" meta="What the workforce learns">
{outline.length > 0 ? (
<ol className="space-y-2">
{outline.map((step, i) => (
<li key={step} className="flex gap-3 text-body-sm text-ink-2">
<span className="mt-0.5 grid h-5 w-5 shrink-0 place-items-center rounded-full bg-krow-blue-tint text-[10px] font-bold tabular-nums text-krow-blue">
{i + 1}
</span>
<span className="min-w-0">{step}</span>
</li>
))}
</ol>
) : (
<p className="text-body-sm leading-relaxed text-ink-3">
This skill carries no written outline. It teaches through
{' '}
{kinds.length ? kinds.join(' and ').toLowerCase() : 'the challenge itself'}.
</p>
)}
{course.quiz?.length > 0 && (
<p className="mt-3 text-caption text-ink-4">
Includes a {course.quiz.length}-question knowledge check.
</p>
)}
</Block>
{course.challenge && (
<Block title="Proof" meta="How the workforce demonstrates the skill">
<p className="text-body-sm font-semibold text-ink-1">{TYPE_LABEL[type]}</p>
<p className="mt-1 text-body-sm leading-relaxed text-ink-3">{TYPE_HINT[type]}</p>
{course.challenge.prompt && (
<p className="mt-3 rounded-lg border border-border bg-surface-subtle p-3 text-body-sm italic leading-relaxed text-ink-2">
“{course.challenge.prompt}”
</p>
)}
</Block>
)}
{criteria.length > 0 && (
<Block
title="Owliver evaluation"
meta="What Owliver checks"
icon={Sparkles}
>
<ul className="space-y-2">
{rubric.map((r) => (
<li key={r.criterion} className="flex items-baseline justify-between gap-3 text-body-sm">
<span className="flex min-w-0 items-baseline gap-2 text-ink-2">
<Check className="h-3.5 w-3.5 shrink-0 translate-y-0.5 text-krow-blue" aria-hidden="true" />
<span className="min-w-0">{r.criterion}</span>
</span>
{r.weight != null && (
<span className="shrink-0 text-caption tabular-nums text-ink-4">{r.weight}%</span>
)}
</li>
))}
</ul>
</Block>
)}
{course.pass_score > 0 && (
<Block title="Verification" meta="What qualifies as passing">
<p className="text-body-sm leading-relaxed text-ink-2">
A submission verifies the skill at{' '}
<span className="font-semibold text-ink-1">{course.pass_score} or above</span>
{course.badge_reward
? <>, and earns the <span className="font-semibold text-ink-1">{course.badge_reward}</span> badge.</>
: '.'}
</p>
</Block>
)}
{/* Usage is counted from worker profiles. Assignment and "in progress"
have no field behind them, so the two states the records do carry are
the two states shown. */}
{usage && (usage.completed > 0 || usage.verified > 0) && (
<Block title="Workforce usage" meta="From worker profiles">
<dl className="grid grid-cols-2 gap-3">
{[
{ label: 'Completed', value: usage.completed },
{ label: 'Verified', value: usage.verified },
].map((row) => (
<div key={row.label} className="rounded-lg border border-border bg-surface-subtle px-3 py-2.5">
<dt className="text-[10px] font-bold uppercase tracking-wider text-ink-4">{row.label}</dt>
<dd className="mt-1 font-heading text-title font-bold tabular-nums text-ink-1">{row.value}</dd>
</div>
))}
</dl>
</Block>
)}
<Link
to={`/admin/university/${course.id}`}
className="group inline-flex items-center gap-1.5 rounded text-body-sm font-semibold text-krow-blue hover:underline
focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-krow-blue/50"
>
Open the challenge as the workforce sees it
<ArrowRight
className="h-3.5 w-3.5 transition-transform duration-200 group-hover:translate-x-0.5"
aria-hidden="true"
/>
</Link>
</div>
</Drawer>
);
}
/** A titled region of the record. */
/** @param {any} props */
function Block({ title, meta, icon: Icon, children }) {
return (
<section className="border-t border-border pt-4 first:border-0 first:pt-0">
<div className="mb-2.5 flex items-baseline gap-2">
<h3 className="inline-flex items-center gap-1.5 font-heading text-overline uppercase tracking-wider text-ink-4">
{Icon && <Icon className="h-3 w-3 text-krow-blue" aria-hidden="true" />}
{title}
</h3>
{meta && <span className="min-w-0 truncate text-caption text-ink-4">{meta}</span>}
</div>
{children}
</section>
);
}