Source identity
| Field | Recorded value |
|---|---|
| Source type | GitHub source |
| Original record | Janady13/missionmatch / frontend/src/screens/PublicCareers.tsx |
| Date | Not specified in source |
| Repository | Janady13/missionmatch |
| Revision | 720d47fae476b0d38477ae4401697d372b1ea2bc |
| Branch | feature/opportunity-to-outcome |
| Path or archive member | frontend/src/screens/PublicCareers.tsx |
| Original SHA-256 | 509822d922d00c1b04f0ab3f148b77ae25e0c9d9985dbc6b0f1f259d13bf474f |
| Git blob SHA | 2dd4848041aa8a2108cbce19618a382fec338482 |
| Reading-copy SHA-256 | 509822d922d00c1b04f0ab3f148b77ae25e0c9d9985dbc6b0f1f259d13bf474f |
Recovered source transcript
Download complete source text ↓599 lines · 20.8 KB
import { useEffect, useState } from 'react'
import { Link, useParams } from 'react-router-dom'
import { Building2, Check, Clock, Loader2, MapPin, Paperclip, ShieldCheck } from 'lucide-react'
// THE PUBLIC CAREERS PAGES.
//
// These are the only screens in this product a stranger sees, and the only unauthenticated ones that touch a
// tenant's data. Everything about them is deliberately different from the rest of the application.
//
// NO SESSION, NO SHELL, NO SHARED CLIENT. They are mounted OUTSIDE the authentication gate, beside the invite
// acceptance page which has the same property. They do not use lib/api, because that client mints a CSRF
// token and sends credentials, and a visitor with no session has neither: it would fire a pointless request
// at an authentication endpoint on every page load. They call fetch directly, with credentials omitted, so
// the request carries nothing at all.
//
// THEY ARE LIGHT ON THEIR OWN AND HEAVY ON HONESTY. A candidate is handing over their name, their history and
// often their clearance status. The consent they give is explicit, is recorded with what they agreed to, and
// is written in the sentence they actually read rather than referenced from somewhere else.
// publicGet and publicPost are the whole client. Credentials are omitted rather than defaulted, so no cookie
// is sent even if the visitor happens to be signed in to the recruiter application in another tab.
async function publicGet<T>(path: string): Promise<T> {
const res = await fetch(`/api${path}`, { credentials: 'omit' })
if (!res.ok) {
throw new Error(await publicError(res))
}
return (await res.json()) as T
}
async function publicError(res: Response): Promise<string> {
try {
const body = (await res.json()) as { error?: string }
if (body.error) return body.error
} catch {
// A non JSON body from a proxy or a gateway is not worth parsing twice.
}
// ONE MESSAGE FOR EVERY REASON A LINK MIGHT NOT WORK, matching what the server does. A page that said
// "this posting was closed" would confirm the posting existed, and a page that said "no such employer"
// would let somebody walk the list of organizations. Not found is not found.
if (res.status === 404) return 'This link is not available. It may have been closed or it may have expired.'
if (res.status === 429) return 'Too many requests from here. Wait a minute and try again.'
return 'Something went wrong at our end. Please try again shortly.'
}
interface PublicPosting {
token: string
title: string
summary?: string
responsibilities?: string
qualifications?: string
location?: string
remote_allowed: boolean
employment_type?: string
clearance?: string
pay_range?: string
published_at?: string
closes_at?: string
employer: string
}
// Frame is the whole visual chrome for these pages: a name, the content, and a footer. There is no
// navigation, because there is nowhere else for a visitor to go, and offering a menu into an application they
// cannot sign in to would be an invitation to a login screen they have no account for.
function Frame({ employer, children }: { employer?: string; children: React.ReactNode }) {
return (
<div className="min-h-screen bg-navy-950/40">
<header className="border-b border-line/70 px-6 py-4">
<div className="mx-auto flex max-w-3xl items-center gap-2.5">
<span className="grid size-8 place-items-center rounded-lg bg-gradient-to-br from-blue to-navy-700 text-gold">
<Building2 size={16} />
</span>
<span className="text-sm font-bold text-ink">{employer || 'Careers'}</span>
</div>
</header>
<main className="mx-auto max-w-3xl px-6 py-8">{children}</main>
<footer className="mx-auto max-w-3xl px-6 pb-10 text-[11px] leading-relaxed text-faint">
Applications are handled by the employer named above. Any clearance you state is recorded as self
reported and is never treated as verified. The real check is the hiring client facility security
officer through DISS at onboarding.
</footer>
</div>
)
}
function Loading() {
return (
<div className="grid place-items-center py-24 text-muted">
<Loader2 className="animate-spin" size={22} />
</div>
)
}
function Failed({ message }: { message: string }) {
return (
<div className="rounded-lg border border-line/70 bg-surface/80 px-4 py-8 text-center">
<p className="text-sm text-ink">{message}</p>
<p className="mt-2 text-[11px] text-faint">
If somebody sent you this link, ask them for a current one.
</p>
</div>
)
}
export function PublicCareersList() {
const { slug = '' } = useParams()
const [data, setData] = useState<{ employer: string; postings: PublicPosting[] } | null>(null)
const [err, setErr] = useState('')
useEffect(() => {
let live = true
publicGet<{ employer: string; postings: PublicPosting[] }>(`/careers/${encodeURIComponent(slug)}`)
.then((d) => live && setData(d))
.catch((e: Error) => live && setErr(e.message))
return () => {
live = false
}
}, [slug])
if (err) {
return (
<Frame>
<Failed message={err} />
</Frame>
)
}
if (!data) {
return (
<Frame>
<Loading />
</Frame>
)
}
return (
<Frame employer={data.employer}>
<h1 className="text-xl font-bold text-ink">Open roles</h1>
{data.postings.length === 0 ? (
<p className="mt-3 text-sm text-muted">
There is nothing open right now. It is worth checking again in a week.
</p>
) : (
<ul className="mt-4 space-y-2">
{data.postings.map((p) => (
<li key={p.token}>
<Link
to={`/careers/${encodeURIComponent(slug)}/${encodeURIComponent(p.token)}`}
className="block rounded-lg border border-line/70 bg-surface/80 px-4 py-3 hover:border-blue/60"
>
<div className="text-sm font-semibold text-ink">{p.title}</div>
<div className="mt-0.5 flex flex-wrap gap-3 text-[11px] text-faint">
{p.location && (
<span className="flex items-center gap-1">
<MapPin size={11} /> {p.location}
</span>
)}
{p.remote_allowed && <span>remote allowed</span>}
{p.clearance && (
<span className="flex items-center gap-1">
<ShieldCheck size={11} /> {p.clearance}
</span>
)}
{p.employment_type && <span>{p.employment_type.replace(/_/g, ' ')}</span>}
</div>
</Link>
</li>
))}
</ul>
)}
</Frame>
)
}
export function PublicPostingPage() {
const { slug = '', token = '' } = useParams()
const [p, setP] = useState<PublicPosting | null>(null)
const [err, setErr] = useState('')
useEffect(() => {
let live = true
publicGet<PublicPosting>(`/careers/${encodeURIComponent(slug)}/postings/${encodeURIComponent(token)}`)
.then((d) => live && setP(d))
.catch((e: Error) => live && setErr(e.message))
return () => {
live = false
}
}, [slug, token])
if (err) {
return (
<Frame>
<Failed message={err} />
</Frame>
)
}
if (!p) {
return (
<Frame>
<Loading />
</Frame>
)
}
return (
<Frame employer={p.employer}>
<h1 className="text-xl font-bold text-ink">{p.title}</h1>
<div className="mt-1 flex flex-wrap gap-3 text-[11px] text-faint">
{p.location && (
<span className="flex items-center gap-1">
<MapPin size={11} /> {p.location}
</span>
)}
{p.remote_allowed && <span>remote allowed</span>}
{p.clearance && (
<span className="flex items-center gap-1">
<ShieldCheck size={11} /> {p.clearance}
</span>
)}
{p.pay_range && <span>{p.pay_range}</span>}
{p.closes_at && (
<span className="flex items-center gap-1">
<Clock size={11} /> closes {new Date(p.closes_at).toLocaleDateString()}
</span>
)}
</div>
{p.summary && <Prose title="" body={p.summary} />}
{p.responsibilities && <Prose title="What you will do" body={p.responsibilities} />}
{p.qualifications && <Prose title="What you need" body={p.qualifications} />}
<ApplyForm slug={slug} token={token} />
</Frame>
)
}
function Prose({ title, body }: { title: string; body: string }) {
return (
<section className="mt-5">
{title && <h2 className="text-sm font-bold text-ink">{title}</h2>}
<p className="mt-1 whitespace-pre-wrap text-sm leading-relaxed text-muted">{body}</p>
</section>
)
}
const CLEARANCE_CHOICES = [
'',
'None',
'Public Trust',
'Secret',
'Top Secret',
'TS/SCI',
'TS/SCI with polygraph',
]
// The exact sentence somebody agrees to. It is stored with the application, so it has to be the words that
// were on the screen rather than a version number pointing at a policy that can change afterwards.
const CONSENT_TEXT =
'I agree that the employer may store the details I have provided, including any resume I attach, and may ' +
'contact me about this role and other roles they think I would suit. I understand that any clearance I ' +
'state here is self reported and is not treated as verified.'
function ApplyForm({ slug, token }: { slug: string; token: string }) {
const [full_name, setName] = useState('')
const [email, setEmail] = useState('')
const [phone, setPhone] = useState('')
const [location, setLocation] = useState('')
const [clearance, setClearance] = useState('')
const [cover_note, setNote] = useState('')
const [file, setFile] = useState<File | null>(null)
const [consented, setConsented] = useState(false)
const [sending, setSending] = useState(false)
const [sent, setSent] = useState(false)
const [err, setErr] = useState('')
if (sent) {
return (
<section className="mt-8 rounded-lg border border-ok/40 bg-ok/10 px-4 py-6 text-center">
<Check size={20} className="mx-auto text-ok" />
<p className="mt-2 text-sm font-semibold text-ink">Your application is in.</p>
<p className="mt-1 text-[11px] leading-relaxed text-muted">
A recruiter reads every application. If it is a fit you will hear from them directly, and there is
nothing further you need to do.
</p>
</section>
)
}
const submit = async (e: React.FormEvent) => {
e.preventDefault()
setErr('')
if (!consented) {
setErr('Please agree to the statement above before applying.')
return
}
setSending(true)
try {
// Multipart, because a resume rides along with the fields. The consent text is sent with the
// submission rather than assumed by the server, so what was agreed to is what gets stored.
const form = new FormData()
form.append('full_name', full_name)
form.append('email', email)
form.append('phone', phone)
form.append('location', location)
form.append('clearance', clearance)
form.append('cover_note', cover_note)
form.append('consent_text', CONSENT_TEXT)
form.append('consented', 'true')
form.append('source', 'careers_page')
if (file) form.append('resume', file)
const res = await fetch(
`/api/careers/${encodeURIComponent(slug)}/postings/${encodeURIComponent(token)}/apply`,
{ method: 'POST', credentials: 'omit', body: form },
)
if (!res.ok) {
throw new Error(await publicError(res))
}
setSent(true)
} catch (e) {
setErr((e as Error).message)
} finally {
setSending(false)
}
}
const field =
'w-full rounded-md border border-line/70 bg-navy-950/60 px-3 py-2 text-sm text-ink outline-none placeholder:text-faint focus:border-blue/60'
const label = 'block text-[11px] font-bold uppercase tracking-wider text-muted'
return (
<section className="mt-8 rounded-lg border border-line/70 bg-surface/80 p-4">
<h2 className="text-sm font-bold text-ink">Apply</h2>
<form className="mt-3 space-y-3" onSubmit={(e) => void submit(e)}>
<div className="grid gap-3 sm:grid-cols-2">
<label className={label}>
Your name
<input
required
value={full_name}
maxLength={200}
onChange={(e) => setName(e.target.value)}
className={`mt-1 ${field}`}
/>
</label>
<label className={label}>
Email
<input
required
type="email"
value={email}
maxLength={255}
onChange={(e) => setEmail(e.target.value)}
className={`mt-1 ${field}`}
/>
</label>
<label className={label}>
Phone
<input
value={phone}
maxLength={40}
onChange={(e) => setPhone(e.target.value)}
className={`mt-1 ${field}`}
/>
</label>
<label className={label}>
Where you are
<input
value={location}
maxLength={200}
placeholder="Columbia, MD"
onChange={(e) => setLocation(e.target.value)}
className={`mt-1 ${field}`}
/>
</label>
</div>
<label className={label}>
Clearance, if you hold one
<select value={clearance} onChange={(e) => setClearance(e.target.value)} className={`mt-1 ${field}`}>
{CLEARANCE_CHOICES.map((c) => (
<option key={c} value={c} className="bg-navy-900">
{c || 'Prefer not to say'}
</option>
))}
</select>
<span className="mt-1 block text-[11px] font-normal normal-case tracking-normal text-faint">
Recorded as stated by you. Nothing here is treated as verified, and leaving it blank does not
count against you.
</span>
</label>
<label className={label}>
Anything you want them to know
<textarea
value={cover_note}
rows={4}
maxLength={4000}
onChange={(e) => setNote(e.target.value)}
className={`mt-1 ${field}`}
/>
</label>
<label className={label}>
Resume
<input
type="file"
accept=".pdf,.doc,.docx,.rtf,.txt"
onChange={(e) => setFile(e.target.files?.[0] ?? null)}
className="mt-1 block w-full text-[11px] font-normal normal-case tracking-normal text-muted file:mr-2 file:rounded-md file:border file:border-line/70 file:bg-transparent file:px-2 file:py-1 file:text-[11px] file:font-semibold file:text-muted"
/>
{file && (
<span className="mt-1 flex items-center gap-1 text-[11px] font-normal normal-case tracking-normal text-faint">
<Paperclip size={11} /> {file.name}
</span>
)}
</label>
{/* THE CONSENT IS NOT PRE TICKED AND IS NOT A LINK TO A POLICY. It is the sentence, on the page,
beside the box, and the same words are stored with the application so what was agreed to can be
produced later exactly as it was read. */}
<label className="flex items-start gap-2 rounded-md border border-line/60 bg-navy-950/40 p-3 text-[11px] leading-relaxed text-muted">
<input
type="checkbox"
checked={consented}
onChange={(e) => setConsented(e.target.checked)}
className="mt-0.5"
/>
<span>{CONSENT_TEXT}</span>
</label>
{err && (
<p className="rounded-md border border-risk/40 bg-risk/10 px-3 py-2 text-[11px] text-risk">{err}</p>
)}
<button
type="submit"
disabled={sending || !consented}
className="rounded-md bg-blue/25 px-4 py-2 text-sm font-semibold text-blue-soft hover:bg-blue/35 disabled:opacity-50"
>
{sending ? 'Sending' : 'Apply'}
</button>
</form>
</section>
)
}
interface PublicSlot {
id: string
starts_at: string
ends_at: string
}
interface PublicOffer {
token: string
employer: string
role: string
candidate_name: string
kind: string
duration_mins: number
location_text?: string
interviewer?: string
message?: string
expires_at: string
slots: PublicSlot[]
}
export function PublicSchedulePage() {
const { slug = '', token = '' } = useParams()
const [offer, setOffer] = useState<PublicOffer | null>(null)
const [err, setErr] = useState('')
const [booking, setBooking] = useState('')
const [booked, setBooked] = useState<string | null>(null)
useEffect(() => {
let live = true
publicGet<PublicOffer>(`/careers/${encodeURIComponent(slug)}/schedule/${encodeURIComponent(token)}`)
.then((d) => live && setOffer(d))
.catch((e: Error) => live && setErr(e.message))
return () => {
live = false
}
}, [slug, token])
const book = async (slotID: string) => {
setErr('')
setBooking(slotID)
try {
const res = await fetch(
`/api/careers/${encodeURIComponent(slug)}/schedule/${encodeURIComponent(token)}/book`,
{
method: 'POST',
credentials: 'omit',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ slot_id: slotID }),
},
)
if (!res.ok) {
throw new Error(await publicError(res))
}
const body = (await res.json()) as { scheduled_at: string }
setBooked(body.scheduled_at)
} catch (e) {
setErr((e as Error).message)
} finally {
setBooking('')
}
}
if (err && !offer) {
return (
<Frame>
<Failed message={err} />
</Frame>
)
}
if (!offer) {
return (
<Frame>
<Loading />
</Frame>
)
}
if (booked) {
return (
<Frame employer={offer.employer}>
<div className="rounded-lg border border-ok/40 bg-ok/10 px-4 py-6 text-center">
<Check size={20} className="mx-auto text-ok" />
<p className="mt-2 text-sm font-semibold text-ink">You are booked in.</p>
<p className="mt-1 text-sm text-muted">{new Date(booked).toLocaleString()}</p>
<p className="mt-2 text-[11px] leading-relaxed text-faint">
A calendar invitation has been sent if an address is on file. If you need to move it, reply to
whoever sent you this link.
</p>
</div>
</Frame>
)
}
return (
<Frame employer={offer.employer}>
<h1 className="text-xl font-bold text-ink">Pick a time</h1>
<p className="mt-1 text-sm text-muted">
{offer.role}, a {offer.duration_mins} minute {offer.kind} interview
{offer.interviewer ? ` with ${offer.interviewer}` : ''}.
</p>
{offer.message && (
<p className="mt-3 whitespace-pre-wrap rounded-md border border-line/60 bg-navy-950/40 p-3 text-sm leading-relaxed text-muted">
{offer.message}
</p>
)}
{offer.location_text && (
<p className="mt-2 flex items-center gap-1.5 text-[11px] text-faint">
<MapPin size={11} /> {offer.location_text}
</p>
)}
<ul className="mt-5 space-y-2">
{offer.slots.length === 0 ? (
<li className="rounded-lg border border-line/70 px-4 py-6 text-center text-sm text-muted">
Every time offered has been taken. Reply to whoever sent you this link and they will offer more.
</li>
) : (
offer.slots.map((s) => (
<li key={s.id}>
<button
type="button"
disabled={!!booking}
onClick={() => void book(s.id)}
className="flex w-full items-center justify-between rounded-lg border border-line/70 bg-surface/80 px-4 py-3 text-left hover:border-blue/60 disabled:opacity-50"
>
<span className="text-sm font-semibold text-ink">
{new Date(s.starts_at).toLocaleString()}
</span>
<span className="text-[11px] text-faint">
{booking === s.id ? 'Booking' : 'Choose this'}
</span>
</button>
</li>
))
)}
</ul>
{/* Times render in the VISITOR'S zone because toLocaleString uses their browser. Saying so removes the
one doubt that makes somebody miss an interview: whether the time on screen is theirs or the
employer's. */}
<p className="mt-3 text-[11px] text-faint">
Times are shown in your own time zone. This link expires{' '}
{new Date(offer.expires_at).toLocaleDateString()}.
</p>
{err && (
<p className="mt-3 rounded-md border border-risk/40 bg-risk/10 px-3 py-2 text-[11px] text-risk">
{err}
</p>
)}
</Frame>
)
}