import { Check, Copy, UsersRound, X } from 'lucide-react' import { type FormEvent, useEffect, useMemo, useState } from 'react' import { Link, useParams } from 'react-router-dom' import { api, ApiError } from '../../api/client' import type { ParticipantMeeting, ScheduledInterview } from '../../types' import { ErrorAlert, Loading } from '../../components/ui/AsyncState' import { DateAndPlace, InterviewCard } from './components/InterviewCard' function message(error: unknown) { return error instanceof ApiError || error instanceof Error ? error.message : 'Something went wrong' } export function InboxPage() { const [items, setItems] = useState([]) const [error, setError] = useState('') const [loading, setLoading] = useState(true) useEffect(() => { void api.inbox .list() .then(setItems) .catch((value) => setError(message(value))) .finally(() => setLoading(false)) }, []) async function respond(id: number, status: 'accepted' | 'declined') { setError('') try { const updated = await api.inbox.respond(id, status) setItems((current) => current.map((item) => (item.id === id ? updated : item))) } catch (value) { setError(message(value)) } } return ( <>
Invitations

Inbox

Respond to interview invitations assigned to you.

{error && } {loading ? ( ) : (
{items.map((item) => (
))} {!items.length && (

Inbox zero

You have no interview invitations.

)}
)} ) } export function CalendarPage() { const [items, setItems] = useState([]) const [error, setError] = useState('') const [loading, setLoading] = useState(true) useEffect(() => { void api .calendar() .then(setItems) .catch((value) => setError(message(value))) .finally(() => setLoading(false)) }, []) const groups = useMemo(() => { return [...items] .sort((a, b) => a.startsAt.localeCompare(b.startsAt)) .reduce>((result, item) => { const key = new Date(item.startsAt).toLocaleDateString(undefined, { weekday: 'long', month: 'long', day: 'numeric', year: 'numeric', }) ;(result[key] ??= []).push(item) return result }, {}) }, [items]) return ( <>
Schedule

Calendar

Your accepted and organized interviews, grouped by date.

{error && } {loading ? ( ) : ( Object.entries(groups).map(([date, interviews]) => (

{date}

{interviews.map((item) => ( Open session ))}
)) )} ) } export function ScheduledInterviewPage() { const id = Number(useParams().id) const [interview, setInterview] = useState() const [document, setDocument] = useState('') const [notes, setNotes] = useState([]) const [error, setError] = useState('') const [saving, setSaving] = useState(false) const [linkCopied, setLinkCopied] = useState(false) useEffect(() => { let active = true void Promise.all([api.scheduledInterviews.get(id), api.scheduledInterviews.notes(id)]) .then(([item, loadedNotes]) => { if (!active) return setInterview(item) setDocument(item.sharedDocument) setNotes(loadedNotes) }) .catch((value) => active && setError(message(value))) return () => { active = false } }, [id]) useEffect(() => { if (interview?.status !== 'started') return const controller = new AbortController() void api.scheduledInterviews .subscribe( id, (note) => { setNotes((current) => current.some((item) => item.id === note.id) ? current : [...current, note], ) }, controller.signal, ) .catch((value) => { if (!controller.signal.aborted) setError(message(value)) }) return () => controller.abort() }, [id, interview?.status]) async function setStatus(status: string) { setError('') try { setInterview(await api.scheduledInterviews.setStatus(id, status)) } catch (value) { setError(message(value)) } } async function saveDocument(event: FormEvent) { event.preventDefault() setSaving(true) setError('') try { setInterview(await api.scheduledInterviews.updateDocument(id, document)) } catch (value) { setError(message(value)) } finally { setSaving(false) } } async function addNote(event: FormEvent) { event.preventDefault() const form = event.currentTarget const body = String(new FormData(form).get('body')).trim() if (!body) return try { const note = await api.scheduledInterviews.addNote(id, body) setNotes((current) => current.some((item) => item.id === note.id) ? current : [...current, note], ) form.reset() } catch (value) { setError(message(value)) } } async function copyParticipantLink(url: string) { setError('') try { await navigator.clipboard.writeText(url) setLinkCopied(true) } catch { setError('Could not copy the participant interview link') } } if (!interview && !error) return if (!interview) return return ( <> ← Calendar
Interviewer session

{interview.participantName}

{interview.jobTitle}

{interview.status}
{error && }
{interview.status !== 'started' && interview.status !== 'completed' && ( )} {interview.status === 'started' && ( )}
Visible to candidate

Shared documentation