diff --git a/frontend/src/app/AuthenticatedApp.tsx b/frontend/src/app/AuthenticatedApp.tsx
index aeaa4bc..c6df1d8 100644
--- a/frontend/src/app/AuthenticatedApp.tsx
+++ b/frontend/src/app/AuthenticatedApp.tsx
@@ -5,7 +5,7 @@ import { AppFooter } from '../components/layout/AppFooter'
import { GoGuessHeader } from '../components/layout/GoGuessHeader'
import { WorkspaceUtilities } from '../components/layout/WorkspaceUtilities'
import { NotFound } from '../components/ui/NotFound'
-import { CalendarPage, InboxPage, ScheduledInterviewPage } from '../features/interviews'
+import { InboxPage, ScheduledInterviewPage } from '../features/interviews'
import { CreateJobPage, JobDetailPage, JobsPage } from '../features/jobs'
import {
CreateParticipantPage,
@@ -13,7 +13,7 @@ import {
ParticipantsPage,
} from '../features/participants'
import { QuestionLibraryPage, QuestionPage } from '../features/questions'
-import { ProfilePage, UsersPage } from '../features/users'
+import {CalendarPage, ProfilePage, UsersPage} from '../features/users'
export function AuthenticatedApp({ onLogout }: { onLogout: () => void }) {
const [open, setOpen] = useState(false)
diff --git a/frontend/src/components/actions/errorMessage.ts b/frontend/src/components/actions/errorMessage.ts
index c2d7205..e2a6b9b 100644
--- a/frontend/src/components/actions/errorMessage.ts
+++ b/frontend/src/components/actions/errorMessage.ts
@@ -5,3 +5,9 @@ export function errorMessage(error: unknown) {
? error.message
: 'Something went wrong'
}
+
+export function message(error: unknown) {
+ return error instanceof ApiError || error instanceof Error
+ ? error.message
+ : 'Something went wrong'
+}
\ No newline at end of file
diff --git a/frontend/src/components/actions/index.ts b/frontend/src/components/actions/index.ts
index 0965917..e1d29bd 100644
--- a/frontend/src/components/actions/index.ts
+++ b/frontend/src/components/actions/index.ts
@@ -1,4 +1,4 @@
export { useAuthSession } from './Account'
export { ErrorAlert } from './ErrorAlert'
-export { errorMessage } from './errorMessage'
+export { errorMessage, message } from './errorMessage'
export { Loading } from './Loading'
diff --git a/frontend/src/components/layout/SideBar.tsx b/frontend/src/components/layout/SideBar.tsx
index 16aa999..964bf78 100644
--- a/frontend/src/components/layout/SideBar.tsx
+++ b/frontend/src/components/layout/SideBar.tsx
@@ -43,7 +43,6 @@ export function SideBar({
icon={}
close={close}
/>
- } close={close} />
} close={close} />
diff --git a/frontend/src/components/layout/WorkspaceUtilities.tsx b/frontend/src/components/layout/WorkspaceUtilities.tsx
index fcdd84a..e9b3815 100644
--- a/frontend/src/components/layout/WorkspaceUtilities.tsx
+++ b/frontend/src/components/layout/WorkspaceUtilities.tsx
@@ -1,21 +1,25 @@
-import { Inbox } from 'lucide-react'
-import { NavLink } from 'react-router-dom'
-import type { User } from '../../types'
+import {CalendarDays, Inbox, Mail} from 'lucide-react'
+import {NavLink} from 'react-router-dom'
+import type {User} from '../../types'
-export function WorkspaceUtilities({ user }: { user: User | null }) {
- const profileLabel = user?.displayName || user?.email || 'Profile'
- return (
-
- )
+export function WorkspaceUtilities({user}: { user: User | null }) {
+ const profileLabel = user?.displayName || user?.email || 'Profile'
+ return (
+
+ )
}
diff --git a/frontend/src/components/ui/PageHeader.tsx b/frontend/src/components/ui/PageHeader.tsx
index 0358c41..6333a21 100644
--- a/frontend/src/components/ui/PageHeader.tsx
+++ b/frontend/src/components/ui/PageHeader.tsx
@@ -15,7 +15,6 @@ export function PageHeader({
{eyebrow}
-
{title}
{description &&
{description}
}
{action}
diff --git a/frontend/src/config/navigation.ts b/frontend/src/config/navigation.ts
index 1b608ae..a3457ae 100644
--- a/frontend/src/config/navigation.ts
+++ b/frontend/src/config/navigation.ts
@@ -15,13 +15,13 @@ export const footerLinkGroups: FooterLinkGroup[] = [
{ label: 'Jobs', href: '/jobs' },
{ label: 'Participants', href: '/participants' },
{ label: 'Question library', href: '/questions' },
- { label: 'Calendar', href: '/calendar' },
],
},
{
title: 'Useful links',
links: [
{ label: 'Your inbox', href: '/inbox' },
+ { label: 'Calendar', href: '/calendar' },
{ label: 'Your profile', href: '/profile' },
{ label: 'Interview team', href: '/users' },
],
diff --git a/frontend/src/features/interviews/index.ts b/frontend/src/features/interviews/index.ts
index f1f3b98..db7eaa8 100644
--- a/frontend/src/features/interviews/index.ts
+++ b/frontend/src/features/interviews/index.ts
@@ -1,2 +1,2 @@
-export { CalendarPage, InboxPage, ParticipantMeetingPage, ScheduledInterviewPage } from './pages'
+export { InboxPage, ParticipantMeetingPage, ScheduledInterviewPage } from './pages'
export { ScheduleInterviewForm } from './components/ScheduleInterviewForm'
diff --git a/frontend/src/features/interviews/pages.tsx b/frontend/src/features/interviews/pages.tsx
index 5feab26..3fdadf0 100644
--- a/frontend/src/features/interviews/pages.tsx
+++ b/frontend/src/features/interviews/pages.tsx
@@ -1,453 +1,392 @@
-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'
-}
+import {Check, Copy, UsersRound, X} from 'lucide-react'
+import {type FormEvent, useEffect, useState} from 'react'
+import {Link, useParams} from 'react-router-dom'
+import {api} from '../../api/client'
+import type {ParticipantMeeting, ScheduledInterview} from '../../types'
+import {ErrorAlert, Loading} from '../../components/ui/AsyncState'
+import {DateAndPlace, InterviewCard} from './components/InterviewCard'
+import {message} from "../../components/actions";
export function InboxPage() {
- const [items, setItems] = useState([])
- const [error, setError] = useState('')
- const [loading, setLoading] = useState(true)
+ 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))
- }, [])
+ 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))
+ 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.
-
- )}
-
- )}
- >
- )
+ 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)
- const [linkCopied, setLinkCopied] = useState(false)
- const loadedInterviewID = interview?.id
+ 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)
+ const loadedInterviewID = interview?.id
- 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
+ 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 (!loadedInterviewID) return
+ const controller = new AbortController()
+ void api.scheduledInterviews
+ .subscribe(
+ id,
+ (event) => {
+ if (event.type === 'note.created') {
+ setNotes((current) =>
+ current.some((item) => item.id === event.note.id)
+ ? current
+ : [...current, event.note],
+ )
+ }
+ if (event.type === 'document.updated') {
+ setDocument(event.sharedDocument)
+ setInterview((current) =>
+ current ? {...current, sharedDocument: event.sharedDocument} : current,
+ )
+ }
+ },
+ controller.signal,
+ (value) => setError(message(value)),
+ )
+ .catch((value) => {
+ if (!controller.signal.aborted) setError(message(value))
+ })
+ return () => controller.abort()
+ }, [id, loadedInterviewID])
+
+ async function setStatus(status: string) {
+ setError('')
+ try {
+ setInterview(await api.scheduledInterviews.setStatus(id, status))
+ } catch (value) {
+ setError(message(value))
+ }
}
- }, [id])
- useEffect(() => {
- if (!loadedInterviewID) return
- const controller = new AbortController()
- void api.scheduledInterviews
- .subscribe(
- id,
- (event) => {
- if (event.type === 'note.created') {
+ 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 === event.note.id)
- ? current
- : [...current, event.note],
+ current.some((item) => item.id === note.id) ? current : [...current, note],
)
- }
- if (event.type === 'document.updated') {
- setDocument(event.sharedDocument)
- setInterview((current) =>
- current ? { ...current, sharedDocument: event.sharedDocument } : current,
- )
- }
- },
- controller.signal,
- (value) => setError(message(value)),
- )
- .catch((value) => {
- if (!controller.signal.aborted) setError(message(value))
- })
- return () => controller.abort()
- }, [id, loadedInterviewID])
-
- async function setStatus(status: string) {
- setError('')
- try {
- setInterview(await api.scheduledInterviews.setStatus(id, status))
- } catch (value) {
- setError(message(value))
+ form.reset()
+ } 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 copyParticipantLink(url: string) {
+ setError('')
+ try {
+ await navigator.clipboard.writeText(url)
+ setLinkCopied(true)
+ } catch {
+ setError('Could not copy the participant interview link')
+ }
}
- }
- 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' && (
-
- )}
-
-