update ui design for all pages
This commit is contained in:
184
src/components/forge/SkillDetail.jsx
Normal file
184
src/components/forge/SkillDetail.jsx
Normal file
@@ -0,0 +1,184 @@
|
||||
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. */
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user