-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathapply-section.tsx
More file actions
141 lines (131 loc) · 4.95 KB
/
Copy pathapply-section.tsx
File metadata and controls
141 lines (131 loc) · 4.95 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
import { useMemo } from 'react';
import { useQuery } from '@tanstack/react-query';
import { createRoute, Link, useParams, redirect } from '@tanstack/react-router';
import { rootRoute } from './root';
import { fetchMe, fetchApplication, listFiles } from '../api/client';
import { SectionHeading } from '../components/Layout';
import { SavedIndicator } from '../features/applicant/SavedIndicator';
import { SectionNav, nextSectionSlug } from '../features/applicant/SectionNav';
import { Field, RequiredMark } from '../features/applicant/Field';
import { useApplication, useSaveResponses } from '../features/applicant/useApplication';
import { sectionBySlug, questionsInSection } from '@rp2/shared';
async function ensureAuthAndSection({
context,
params,
}: {
context: { queryClient: import('@tanstack/react-query').QueryClient };
params: { section: string };
}) {
const me = await fetchMe();
if (!me) throw redirect({ to: '/auth/request' });
if (me.roles.includes('guardian') && !me.roles.includes('applicant')) {
throw redirect({ to: '/parent' });
}
const section = sectionBySlug(params.section);
if (!section) throw redirect({ to: '/apply' });
const app = await context.queryClient.ensureQueryData({
queryKey: ['application'],
queryFn: fetchApplication,
});
if (app.status !== 'draft' && app.status !== 'awaiting_guardian') {
throw redirect({ to: '/status' });
}
}
function SectionPage() {
const { section: slug } = useParams({ from: applySectionRoute.id });
const q = useApplication();
const filesQuery = useQuery({
queryKey: ['files'],
queryFn: async () => (await listFiles()).files,
});
const save = useSaveResponses();
const section = useMemo(() => sectionBySlug(slug)!, [slug]);
const questions = useMemo(() => questionsInSection(section.key), [section.key]);
const responses = q.data?.responses ?? {};
const files = filesQuery.data ?? [];
const locked =
q.data?.status !== undefined &&
q.data.status !== 'draft' &&
q.data.status !== 'awaiting_guardian';
// Once the parent has accepted, the applicant loses the ability to change
// their guardian_email. Same lock applies whether the invite came from the
// student or from the parent (parent-initiated links land with acceptedAt
// already set).
const guardianEmailLocked = q.data?.guardian?.acceptedAt != null;
const next = nextSectionSlug(section.key);
return (
<div className="max-w-6xl mx-auto px-6 py-10 grid grid-cols-1 md:grid-cols-[220px_1fr] gap-10">
<aside className="md:sticky md:top-6 md:self-start">
<div className="mb-6">
<p className="smallcaps text-accent mb-2">Application</p>
<SavedIndicator
updatedAt={q.data?.updatedAt ?? null}
saving={save.isPending}
/>
</div>
<SectionNav
responses={responses}
files={files}
currentSection={section.key}
/>
</aside>
<main>
<SectionHeading number={section.index}>{section.title}</SectionHeading>
<p className="text-muted italic text-sm mt-3">
Fields marked <RequiredMark /> are required.
</p>
<div className="mt-8 prose-mm">
{questions.map((question) => {
const perFieldLocked =
question.key === 'guardian_email' && guardianEmailLocked;
return (
<div key={question.key}>
<Field
question={question}
value={responses[question.key]}
disabled={!!locked || save.isPending || perFieldLocked}
onSave={(value) => save.mutate({ [question.key]: value })}
/>
{perFieldLocked && (
<p className="text-muted italic text-sm -mt-6 mb-8">
Locked — your parent or guardian has confirmed this
address. Write to us if this looks wrong.
</p>
)}
</div>
);
})}
</div>
<hr />
<div className="flex items-center justify-between">
<Link to="/apply" className="btn btn-ghost no-underline">
Save and exit
</Link>
{next ? (
<Link
to="/apply/$section"
params={{ section: next }}
className="btn btn-primary no-underline"
onClick={() => {
// ensure current field focus commits a blur-based save
(document.activeElement as HTMLElement | null)?.blur?.();
}}
>
Save and continue →
</Link>
) : (
<Link to="/apply" className="btn btn-primary no-underline">
Review before submitting
</Link>
)}
</div>
</main>
</div>
);
}
export const applySectionRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/apply/$section',
beforeLoad: ensureAuthAndSection,
component: SectionPage,
});