import { CalendarDays, Check, MapPin, Plus, 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'
import type { Invitation, ParticipantMeeting, ScheduledInterview, User } from './types'
function message(error: unknown) {
return error instanceof ApiError || error instanceof Error
? error.message
: 'Something went wrong'
}
function ErrorAlert({ error }: { error: string }) {
return (
Request failed
{error}
)
}
function Loading() {
return (
)
}
function DateAndPlace({ interview }: { interview: ScheduledInterview | ParticipantMeeting }) {
return (
{new Date(interview.startsAt).toLocaleString()}
{interview.location}
)
}
export function UsersPage() {
const [users, setUsers] = useState([])
const [error, setError] = useState('')
const [loading, setLoading] = useState(true)
const [saving, setSaving] = useState(false)
useEffect(() => {
void api.users
.list()
.then(setUsers)
.catch((value) => setError(message(value)))
.finally(() => setLoading(false))
}, [])
async function create(event: FormEvent) {
event.preventDefault()
setSaving(true)
setError('')
const form = event.currentTarget
const values = new FormData(form)
try {
const created = await api.users.create({
email: String(values.get('email')),
password: String(values.get('password')),
displayName: String(values.get('displayName')),
})
setUsers((current) => [...current, created])
form.reset()
} catch (value) {
setError(message(value))
} finally {
setSaving(false)
}
}
return (
<>
{error && }
Interview team
{users.length} workspace users
{loading ? (
) : (
{users.map((user) => (
{user.displayName.slice(0, 2).toUpperCase()}
{user.displayName}
{user.email}
{user.role}
))}
)}
>
)
}
export function ScheduleInterviewForm({
jobId,
invitation,
onCreated,
onCancel,
}: {
jobId: number
invitation: Invitation
onCreated: (interview: ScheduledInterview) => void
onCancel: () => void
}) {
const [users, setUsers] = useState([])
const [selected, setSelected] = useState([])
const [error, setError] = useState('')
const [saving, setSaving] = useState(false)
useEffect(() => {
void api.users
.list()
.then(setUsers)
.catch((value) => setError(message(value)))
}, [])
async function submit(event: FormEvent) {
event.preventDefault()
const values = new FormData(event.currentTarget)
setSaving(true)
setError('')
try {
const startsAt = new Date(String(values.get('startsAt'))).toISOString()
onCreated(
await api.jobs.scheduleInterview(jobId, {
invitationId: invitation.id,
startsAt,
location: String(values.get('location')),
interviewerIds: selected,
sharedDocument: String(values.get('sharedDocument')),
}),
)
} catch (value) {
setError(message(value))
setSaving(false)
}
}
return (
)
}
function InterviewCard({
interview,
children,
}: {
interview: ScheduledInterview
children?: React.ReactNode
}) {
return (
{interview.status}
{interview.participantName}
{interview.jobTitle}
{children}
)
}
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 (
<>
{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 (
<>
{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)
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))
}
}
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' && (
)}
Interviewers only
Live notes
{notes.map((note) => (
{note.authorName}
{note.body}
))}
{interview.status === 'started' ? (
) : (
Notes can be added while the interview is started.
)}
Attendees
Interview team and response status.
{interview.attendees.map((attendee) => (
{attendee.displayName}
{attendee.email}
{attendee.status}
))}
>
)
}
export function ParticipantMeetingPage() {
const token = useParams().token ?? ''
const [meeting, setMeeting] = useState()
const [error, setError] = useState('')
useEffect(() => {
void api.participantMeetings
.get(token)
.then(setMeeting)
.catch((value) => setError(message(value)))
}, [token])
return (
G
GO GUESS
Talent workspace
{!meeting && !error && (
)}
{error && (
)}
{meeting && (
Your scheduled interview
{meeting.jobTitle}
Welcome, {meeting.participantName}.
{meeting.status}
Shared documentation
{meeting.sharedDocument || 'No shared documentation has been added yet.'}
)}
)
}