diff --git a/src/components/forge/NextSkill.jsx b/src/components/forge/NextSkill.tsx
similarity index 96%
rename from src/components/forge/NextSkill.jsx
rename to src/components/forge/NextSkill.tsx
index 316be1c..433414e 100644
--- a/src/components/forge/NextSkill.jsx
+++ b/src/components/forge/NextSkill.tsx
@@ -1,3 +1,4 @@
+import type { CourseView } from '@/types/views';
import React from 'react';
import { Link } from 'react-router-dom';
import { ArrowRight, Check, Lock } from 'lucide-react';
@@ -18,7 +19,12 @@ import { TYPE_HINT, TYPE_ICON, TYPE_LABEL, typeOf, proofSteps } from './challeng
*/
/** Progress toward a threshold: pips for a small count, a bar for a score. */
-function Progress({ have, need }) {
+interface ProgressProps {
+ have?: any;
+ need?: any;
+}
+
+function Progress({ have, need }: ProgressProps) {
const done = Math.min(have, need);
if (need <= 8) {
@@ -47,7 +53,15 @@ function Progress({ have, need }) {
);
}
-export function NextSkill({ course, locked, requirements = [], actionLabel, index = 1 }) {
+interface NextSkillProps {
+ course: CourseView;
+ locked?: boolean;
+ requirements?: any;
+ actionLabel?: any;
+ index?: number;
+}
+
+export function NextSkill({ course, locked, requirements = [], actionLabel, index = 1 }: NextSkillProps) {
if (!course) return null;
const type = typeOf(course);
diff --git a/src/components/forge/SkillCard.jsx b/src/components/forge/SkillCard.tsx
similarity index 97%
rename from src/components/forge/SkillCard.jsx
rename to src/components/forge/SkillCard.tsx
index f429e39..2a586a2 100644
--- a/src/components/forge/SkillCard.jsx
+++ b/src/components/forge/SkillCard.tsx
@@ -1,3 +1,4 @@
+import type { CourseView } from '@/types/views';
import React from 'react';
import { ArrowRight, Clock, Sparkles, BookOpen, ShieldCheck, Flame, Wine, Users, CheckCircle2 } from 'lucide-react';
import { cn } from '@/lib/utils';
@@ -52,7 +53,13 @@ const MODULE_STATE = {
locked: { label: 'Locked', className: 'bg-surface-sunken text-ink-4 border-border' },
};
-export function SkillCard({ course, onOpen, moduleState }) {
+interface SkillCardProps {
+ course: CourseView;
+ onOpen?: (...args: any[]) => void;
+ moduleState?: any;
+}
+
+export function SkillCard({ course, onOpen, moduleState }: SkillCardProps) {
const status = statusOf(course);
const type = typeOf(course);
const ProofIcon = TYPE_ICON[type] || TYPE_ICON.roleplay;
diff --git a/src/components/forge/SkillDetail.jsx b/src/components/forge/SkillDetail.tsx
similarity index 95%
rename from src/components/forge/SkillDetail.jsx
rename to src/components/forge/SkillDetail.tsx
index 2b0e1b8..47da639 100644
--- a/src/components/forge/SkillDetail.jsx
+++ b/src/components/forge/SkillDetail.tsx
@@ -1,3 +1,4 @@
+import type { CourseView } from '@/types/views';
import React from 'react';
import { Link } from 'react-router-dom';
import { ArrowRight, Check, Sparkles } from 'lucide-react';
@@ -21,7 +22,14 @@ import {
* 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 }) {
+interface SkillDetailProps {
+ course: CourseView;
+ open?: boolean;
+ onOpenChange?: (...args: any[]) => void;
+ usage?: any;
+}
+
+export function SkillDetail({ course, open, onOpenChange, usage }: SkillDetailProps) {
if (!course) return null;
const status = statusOf(course);
@@ -168,8 +176,14 @@ export function SkillDetail({ course, open, onOpenChange, usage }) {
}
/** A titled region of the record. */
-/** @param {any} props */
-function Block({ title, meta, icon: Icon, children }) {
+interface BlockProps {
+ title?: string;
+ meta?: any;
+ icon?: any;
+ children: React.ReactNode;
+}
+
+function Block({ title, meta, icon: Icon, children }: BlockProps) {
return (
diff --git a/src/components/forge/SkillLibrary.jsx b/src/components/forge/SkillLibrary.tsx
similarity index 90%
rename from src/components/forge/SkillLibrary.jsx
rename to src/components/forge/SkillLibrary.tsx
index b7aebf0..d42c354 100644
--- a/src/components/forge/SkillLibrary.jsx
+++ b/src/components/forge/SkillLibrary.tsx
@@ -21,6 +21,26 @@ import { SkillCard } from './SkillCard';
*/
const CARD_GRID = 'grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4';
+interface SkillLibraryProps {
+ results?: any;
+ total?: any;
+ search?: any;
+ onSearch?: (...args: any[]) => void;
+ category?: any;
+ onCategory?: (...args: any[]) => void;
+ categories?: any;
+ status?: any;
+ onStatus?: (...args: any[]) => void;
+ statuses?: any;
+ loading?: boolean;
+ filtered?: any;
+ onClear?: (...args: any[]) => void;
+ onOpen?: (...args: any[]) => void;
+ onCreate?: (...args: any[]) => void;
+ sectionRef?: any;
+ moduleStateFor?: any;
+}
+
export function SkillLibrary({
results,
total,
@@ -39,7 +59,7 @@ export function SkillLibrary({
onCreate,
sectionRef,
moduleStateFor,
-}) {
+}: SkillLibraryProps) {
return (
@@ -147,7 +167,14 @@ export function SkillLibrary({
}
/** One radio row of filter options, styled as the Forge underline control. */
-function FilterGroup({ label, options, value, onChange }) {
+interface FilterGroupProps {
+ label?: string;
+ options?: any;
+ value?: any;
+ onChange?: (...args: any[]) => void;
+}
+
+function FilterGroup({ label, options, value, onChange }: FilterGroupProps) {
if (options.length <= 1) return null;
return (
diff --git a/src/components/forge/SkillProgression.jsx b/src/components/forge/SkillProgression.tsx
similarity index 96%
rename from src/components/forge/SkillProgression.jsx
rename to src/components/forge/SkillProgression.tsx
index b9546e2..7605c81 100644
--- a/src/components/forge/SkillProgression.jsx
+++ b/src/components/forge/SkillProgression.tsx
@@ -32,8 +32,12 @@ const STATUS_META = {
* of their ladder is the one thing this component exists to make obvious, and a
* stack of rows makes that a reading exercise instead of a glance.
*/
-/** @param {any} props */
-export function SkillLadder({ state, className = '' }) {
+interface SkillLadderProps {
+ state?: any;
+ className?: string;
+}
+
+export function SkillLadder({ state, className = '' }: SkillLadderProps) {
return (
{state.levels.map((rung, i) => {
@@ -88,8 +92,12 @@ export function SkillLadder({ state, className = '' }) {
}
/** One skill: where this person stands on it, and the next thing to do. */
-/** @param {any} props */
-export function SkillProgressCard({ state, onOpen }) {
+interface SkillProgressCardProps {
+ state?: any;
+ onOpen?: (...args: any[]) => void;
+}
+
+export function SkillProgressCard({ state, onOpen }: SkillProgressCardProps) {
const status = STATUS_META[state.status] || STATUS_META.not_started;
const atTop = !state.nextLevel;
@@ -165,8 +173,14 @@ export function SkillProgressCard({ state, onOpen }) {
* hidden: the evidence behind a level is the reason to trust the level, and
* folding it away would leave the label standing on nothing.
*/
-/** @param {any} props */
-export function SkillProgressDetail({ state, open, onOpenChange, onStartTraining }) {
+interface SkillProgressDetailProps {
+ state?: any;
+ open?: boolean;
+ onOpenChange?: (...args: any[]) => void;
+ onStartTraining?: (...args: any[]) => void;
+}
+
+export function SkillProgressDetail({ state, open, onOpenChange, onStartTraining }: SkillProgressDetailProps) {
if (!state) return null;
const done = new Set(state.levels.flatMap((l) => l.completed.map((m) => m.id)));
diff --git a/src/components/forge/YourForge.jsx b/src/components/forge/YourForge.tsx
similarity index 97%
rename from src/components/forge/YourForge.jsx
rename to src/components/forge/YourForge.tsx
index 86c1622..e51fccc 100644
--- a/src/components/forge/YourForge.jsx
+++ b/src/components/forge/YourForge.tsx
@@ -13,7 +13,14 @@ const STEP_ITEMS = [
{ step: '04', title: 'Skill verified', sub: 'Earn verified badge', icon: ShieldCheck },
];
-export function YourForge({ skills = [], evidenceCount = 0, onBrowse, formatDate }) {
+interface YourForgeProps {
+ skills?: any;
+ evidenceCount?: any;
+ onBrowse?: (...args: any[]) => void;
+ formatDate?: any;
+}
+
+export function YourForge({ skills = [], evidenceCount = 0, onBrowse, formatDate }: YourForgeProps) {
const empty = skills.length === 0;
return (
diff --git a/src/components/forge/challengeMeta.js b/src/components/forge/challengeMeta.ts
similarity index 97%
rename from src/components/forge/challengeMeta.js
rename to src/components/forge/challengeMeta.ts
index 9bb369e..f9a7b3b 100644
--- a/src/components/forge/challengeMeta.js
+++ b/src/components/forge/challengeMeta.ts
@@ -1,3 +1,4 @@
+import type { ProfileView } from '@/types/views';
import { Camera, MessageSquare, ScanLine, Video } from 'lucide-react';
/**
@@ -44,7 +45,7 @@ export const DIFF_TONE = {
* progress so the distance to unlocking is legible rather than merely stated.
* Returns [] when a challenge has no gate.
*/
-export function buildRequirements(course, profile = {}) {
+export function buildRequirements(course: any, profile: Partial = {}) {
const req = course?.unlock_requirements;
if (!req) return [];
diff --git a/src/components/forge/index.js b/src/components/forge/index.ts
similarity index 100%
rename from src/components/forge/index.js
rename to src/components/forge/index.ts
diff --git a/src/components/krow/types.ts b/src/components/krow/types.ts
index 1b8a993..72a6159 100644
--- a/src/components/krow/types.ts
+++ b/src/components/krow/types.ts
@@ -1,55 +1,9 @@
-import type { Course, WorkerProfile } from '@/types/entities';
-
/**
- * A worker profile as the talent components read one.
+ * Re-export, so the folder's own imports keep working.
*
- * Five of `WorkerProfile`'s columns are `jsonb`, which the generated registry
- * types `unknown` — correctly, because nothing on the backend declares what is
- * inside them. These components iterate and read through all five, so they are
- * narrowed here to arrays. What the elements hold is still unstated, because it
- * still is; every field backed by a real column keeps the registry's type.
- *
- * Declared once for the folder rather than per file: it is the same profile
- * travelling through a card, a modal and a hero, and three copies of the same
- * narrowing would be three things to keep in step.
+ * `ProfileView` and `CourseView` started here and moved to `@/types/views` when
+ * a second folder needed them — `components/forge` reads the same `jsonb`
+ * columns off the same courses. Two copies of one narrowing would be two things
+ * to keep in step.
*/
-export type ProfileView = Omit<
- WorkerProfile,
- 'experience' | 'completed_courses' | 'earned_badges' | 'capabilities' | 'score_breakdown'
-> & {
- /* Required, not optional: the registry declares these columns NOT NULL, so
- making them optional here would make a `ProfileView` unassignable back to a
- `WorkerProfile` — which `ChallengeRunner` does when it hands one to a
- mutation. Narrowing the type is the point; loosening the presence is not. */
- experience: any[];
- completed_courses: any[];
- earned_badges: any[];
- capabilities: any[];
- score_breakdown: any;
- /**
- * NOT a column, and deliberately recorded as such.
- *
- * `TalentPoolCard` renders a MapPin row behind `profile.location &&`, but
- * `worker_profiles` has no `location` — the registry has no such field and
- * the API cannot send one, so that row has never rendered. Typed optional so
- * the dead branch keeps compiling unchanged; deleting it is a product
- * decision, not a migration one. Same class as `user.avatar_url`.
- */
- location?: string;
-};
-
-/**
- * A course as the proving-ground components read one.
- *
- * `challenge`, `unlock_requirements` and `quiz` are `jsonb`; the runner reads
- * `challenge.type`, `.prompt` and `.image_url` to decide which challenge UI to
- * show. Narrowed to `any` rather than given a shape, because the shape is not
- * declared anywhere the registry can see.
- */
-export type CourseView = Omit & {
- /* Required for the same reason as `ProfileView`'s: these columns are NOT NULL
- in the registry, so optional here would break assignment back to `Course`. */
- challenge: any;
- unlock_requirements: any;
- quiz: any;
-};
+export type { CourseView, ProfileView } from '@/types/views';
diff --git a/src/types/views.ts b/src/types/views.ts
new file mode 100644
index 0000000..1b8a993
--- /dev/null
+++ b/src/types/views.ts
@@ -0,0 +1,55 @@
+import type { Course, WorkerProfile } from '@/types/entities';
+
+/**
+ * A worker profile as the talent components read one.
+ *
+ * Five of `WorkerProfile`'s columns are `jsonb`, which the generated registry
+ * types `unknown` — correctly, because nothing on the backend declares what is
+ * inside them. These components iterate and read through all five, so they are
+ * narrowed here to arrays. What the elements hold is still unstated, because it
+ * still is; every field backed by a real column keeps the registry's type.
+ *
+ * Declared once for the folder rather than per file: it is the same profile
+ * travelling through a card, a modal and a hero, and three copies of the same
+ * narrowing would be three things to keep in step.
+ */
+export type ProfileView = Omit<
+ WorkerProfile,
+ 'experience' | 'completed_courses' | 'earned_badges' | 'capabilities' | 'score_breakdown'
+> & {
+ /* Required, not optional: the registry declares these columns NOT NULL, so
+ making them optional here would make a `ProfileView` unassignable back to a
+ `WorkerProfile` — which `ChallengeRunner` does when it hands one to a
+ mutation. Narrowing the type is the point; loosening the presence is not. */
+ experience: any[];
+ completed_courses: any[];
+ earned_badges: any[];
+ capabilities: any[];
+ score_breakdown: any;
+ /**
+ * NOT a column, and deliberately recorded as such.
+ *
+ * `TalentPoolCard` renders a MapPin row behind `profile.location &&`, but
+ * `worker_profiles` has no `location` — the registry has no such field and
+ * the API cannot send one, so that row has never rendered. Typed optional so
+ * the dead branch keeps compiling unchanged; deleting it is a product
+ * decision, not a migration one. Same class as `user.avatar_url`.
+ */
+ location?: string;
+};
+
+/**
+ * A course as the proving-ground components read one.
+ *
+ * `challenge`, `unlock_requirements` and `quiz` are `jsonb`; the runner reads
+ * `challenge.type`, `.prompt` and `.image_url` to decide which challenge UI to
+ * show. Narrowed to `any` rather than given a shape, because the shape is not
+ * declared anywhere the registry can see.
+ */
+export type CourseView = Omit & {
+ /* Required for the same reason as `ProfileView`'s: these columns are NOT NULL
+ in the registry, so optional here would break assignment back to `Course`. */
+ challenge: any;
+ unlock_requirements: any;
+ quiz: any;
+};