chore(ts-migration): migrate components/krow to TypeScript with real prop types
Phase 8, first batch: 56 files, plus three supporting edits outside the folder.
All 56 emit byte-identical JavaScript and the production bundle is byte-identical
to 543da9d.
This batch establishes the pattern for the remaining component work: real prop
interfaces built from actual call sites, reusing the generated entity types.
Fifty-four components now declare what they take -
`CandidateCard({ application: JobApplication, jobTitle?: string, rank: number })`,
`MatchedCandidates({ job: JobPosting, profiles: ProfileView[], ... })` - rather
than carrying a compatibility bag. Callbacks are optional because call sites
omit them; entity props are required because call sites always pass them. Where
a call site proved otherwise, the call site won: `ScheduleInterviewModal`'s
`position`, `CandidateCard`'s `jobTitle`, `TalentDetailModal`'s `matchScore` and
`matchReasons` are optional because real callers omit them.
Two view types absorb what the registry cannot describe. `ProfileView` and
`CourseView` narrow the `jsonb` columns these components read through -
`experience`, `completed_courses`, `earned_badges`, `capabilities`,
`score_breakdown`, `challenge`, `quiz` - to arrays and objects, while every
field backed by a real column keeps the registry's type. They are declared once
for the folder, and their narrowed fields stay REQUIRED: the registry has those
columns NOT NULL, and making them optional broke assignment back to the entity
where `ChallengeRunner` hands a course to a mutation.
TWO REAL DEFECTS FOUND, BOTH PRESERVED RATHER THAN FIXED:
1. `SuggestedTalent` calls `matchTalent.mutate({...}).then(...)`. React
Query's `mutate` returns `void`, so that `.then` throws at run time, and
`runMatch` is reachable from a button. `mutateAsync` is what the code
means. Swapping it turns a crash into a working feature, which is a
product change, not a migration one - so it is cast to compile and left
behaving exactly as it did. This one deserves a fix on its own terms.
2. `TalentPoolCard` renders a location row behind `profile.location &&`, but
`worker_profiles` has no `location` column - the registry has none and the
API cannot send one, so the row has never rendered. Recorded as an
optional field on `ProfileView` with a note, the same treatment as
`user.avatar_url` in Phase 7.
Also fixed, all type-only: the Web Speech API declared as the optional `Window`
members `AIInterviewModal` already feature-detects; four `new Promise<void>`
where `resolve()` takes no argument; `toast`'s options bag made optional on
every method, which callers had always omitted; five `krowHooks` query
arguments made optional, which callers had always omitted.
One automated pass was reverted rather than shipped. A local-component
annotator captured words out of the preceding JSDoc as prop names - producing
`interface FrameProps { one?: any; every?: any }` from a sentence about
"one surface, one padding, every step" - the same class of regex error as
Phase 6's. The whole folder was restored from the index and the sound steps
re-run, then local components were annotated with an index-spliced rewrite that
reads only the destructuring.
Verified: tsc 35 -> 22, set-difference showing thirteen removed and none added;
zero errors in any of the 56 files; all 56 emit byte-identical JavaScript;
production bundle byte-identical; npm test 1684/1691 with the same seven
failures; Owliver baseline 59/59; lint 0 errors; baseline artifacts untouched.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HBG1wnuRfJKCstGB8Fekr8
This commit is contained in:
76
src/components/krow/proving/MediaChallenge.tsx
Normal file
76
src/components/krow/proving/MediaChallenge.tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
import type { CourseView } from '../types';
|
||||
import React, { useState } from 'react';
|
||||
import { Loader2, Camera } from 'lucide-react';
|
||||
import { base44 } from '@/api/base44Client';
|
||||
import { Image } from '@/components/ui/image';
|
||||
|
||||
// Photo or video proof — capture/upload, then submit for AI evaluation.
|
||||
interface MediaChallengeProps {
|
||||
course: CourseView;
|
||||
onSubmit?: (...args: any[]) => void;
|
||||
submitting: boolean;
|
||||
}
|
||||
|
||||
export default function MediaChallenge({ course, onSubmit, submitting }: MediaChallengeProps) {
|
||||
const type = course?.challenge?.type || 'photo';
|
||||
const [mediaUrl, setMediaUrl] = useState('');
|
||||
const [uploading, setUploading] = useState(false);
|
||||
|
||||
const accept = type === 'video' ? 'video/*' : 'image/*';
|
||||
const capture = type === 'photo' ? 'environment' : undefined;
|
||||
|
||||
const handleFile = async (e) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
setUploading(true);
|
||||
try {
|
||||
const { file_url } = await base44.integrations.Core.UploadFile({ file });
|
||||
setMediaUrl(file_url);
|
||||
} catch {
|
||||
setMediaUrl('');
|
||||
} finally {
|
||||
setUploading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p className="text-[13px] text-ink-3 mb-3">
|
||||
{type === 'video' ? 'Record or upload a short video demonstrating the challenge.' : 'Take or upload a photo showing your work.'}
|
||||
</p>
|
||||
{mediaUrl ? (
|
||||
<div className="mb-3 rounded-xl overflow-hidden border border-border">
|
||||
{type === 'video' ? (
|
||||
<video src={mediaUrl} controls className="w-full max-h-64 object-cover" />
|
||||
) : (
|
||||
<Image src={mediaUrl} alt="Your proof" className="w-full max-h-64 object-cover" fittingType="fill" />
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<label className="flex flex-col items-center justify-center h-40 rounded-xl border-2 border-dashed border-border cursor-pointer hover:border-krow-blue transition-colors">
|
||||
{uploading ? (
|
||||
<Loader2 className="w-6 h-6 animate-spin text-krow-blue" />
|
||||
) : (
|
||||
<>
|
||||
<Camera className="w-6 h-6 text-ink-4 mb-2" />
|
||||
<span className="text-[13px] text-ink-3">Tap to {type === 'video' ? 'record' : 'capture'}</span>
|
||||
</>
|
||||
)}
|
||||
<input type="file" accept={accept} capture={capture} onChange={handleFile} className="hidden" disabled={uploading || submitting} />
|
||||
</label>
|
||||
)}
|
||||
<div className="flex gap-2">
|
||||
{mediaUrl && (
|
||||
<button onClick={() => setMediaUrl('')} className="h-10 px-4 rounded-xl border border-border text-[13px] font-medium text-ink-3">Retake</button>
|
||||
)}
|
||||
<button
|
||||
onClick={() => onSubmit(mediaUrl)}
|
||||
disabled={!mediaUrl || uploading || submitting}
|
||||
className="flex-1 h-10 rounded-xl bg-krow-navy text-white text-[13px] font-semibold disabled:opacity-40"
|
||||
>
|
||||
Submit proof for AI evaluation
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user