diff --git a/web/.env.example b/web/.env.example index ea2a39f..433c7ce 100644 --- a/web/.env.example +++ b/web/.env.example @@ -39,3 +39,8 @@ VITE_TURNSTILE_SITE_KEY= # PostHog error tracking. Unset means errors stay in the console. VITE_POSTHOG_KEY= VITE_POSTHOG_HOST=https://us.i.posthog.com + +# Dashboard modules that are built but not currently offered. Set to "true" to +# show them; anything else (including absent) keeps them hidden. +VITE_FEATURE_KNOWLEDGE_LIBRARY= +VITE_FEATURE_ESSAYS= diff --git a/web/src/components/ApplicationTrackingModule.tsx b/web/src/components/ApplicationTrackingModule.tsx index ef42379..ea0fa7d 100644 --- a/web/src/components/ApplicationTrackingModule.tsx +++ b/web/src/components/ApplicationTrackingModule.tsx @@ -90,6 +90,17 @@ export default function ApplicationTrackingModule({ open, onClose, onEditIncome, // (clicking a logo in Discover's list strip). Any other tab change clears it. const [statusSchool, setStatusSchool] = useState(null) const switchTab = useCallback((t: TabId) => { setStatusSchool(null); setTab(t) }, []) + + // The module stays mounted while closed โ€” it holds the college list and its + // checklist โ€” so without this it reopens wherever it was left. Someone who + // last looked at Discover came back to Discover, days later, with no memory + // of having gone there. Adjusted on the open transition rather than in an + // effect, so the first paint is already the right tab. + const [wasOpen, setWasOpen] = useState(open) + if (open !== wasOpen) { + setWasOpen(open) + if (open) { setTab('status'); setStatusSchool(null) } + } const openInStatus = useCallback((collegeId: string | null) => { setStatusSchool(collegeId); setTab('status') }, []) // Dismissal persists via mark_intro_seen, but that round-trips through a // profile refresh; track it locally so the banner goes away on the click. diff --git a/web/src/components/Dashboard.tsx b/web/src/components/Dashboard.tsx index 3dfc9a6..bf4f583 100644 --- a/web/src/components/Dashboard.tsx +++ b/web/src/components/Dashboard.tsx @@ -29,6 +29,9 @@ import type { Demographics } from '../types/user' import FinancialAidModule from './FinancialAidModule' import ApplicationTrackingModule from './ApplicationTrackingModule' import FeeWaiverNotice from './FeeWaiverNotice' +import { FEATURES, moduleEnabled } from '../lib/features' +import { useApplications } from '../contexts/ApplicationsContext' +import { moduleStatusLines } from '../lib/moduleStatus' import { feeWaiverEligibility, shouldShowFeeWaiverNotice, FEE_WAIVER_NOTICE_KEY } from '../lib/feeWaivers' import EssaysModule from './EssaysModule' import KnowledgeLibraryModule from './KnowledgeLibraryModule' @@ -95,11 +98,31 @@ interface Props { onSignOut?: () => void } -const MODULES: { key: string; sub: string; color: string; emoji: string }[] = [ - { key: 'Knowledge Library', sub: 'Start Here', color: '#3F5BA9', emoji: '๐Ÿ“š' }, - { key: 'Financial Aid', sub: 'Scholarship Hunt', color: '#C47A12', emoji: '๐Ÿ’ฐ' }, - { key: 'College Essays', sub: 'Drafting Season', color: '#1D7FC4', emoji: '๐Ÿชถ' }, - { key: 'Application Tracking', sub: 'Building Your List', color: '#7048C8', emoji: '๐Ÿ“‹' }, +/** + * Every module the dashboard knows. What it offers is this, filtered by the + * feature flags โ€” see lib/features.ts. + * + * `blurb` says what the module is for, in the student's terms. The old cards + * carried only a two-word tagline ("Scholarship Hunt"), which reads as + * decoration rather than an answer to "what is this and why would I open it". + */ +const MODULES: { key: string; sub: string; blurb: string; color: string; emoji: string }[] = [ + { + key: 'Knowledge Library', sub: 'Start Here', color: '#3F5BA9', emoji: '๐Ÿ“š', + blurb: 'What actually matters in getting into college and paying for it, and where to do each part.', + }, + { + key: 'Financial Aid', sub: 'Scholarship Hunt', color: '#C47A12', emoji: '๐Ÿ’ฐ', + blurb: 'Find scholarships you qualify for, track what you have applied to, and work out what a college will really cost.', + }, + { + key: 'College Essays', sub: 'Drafting Season', color: '#1D7FC4', emoji: '๐Ÿชถ', + blurb: 'Draft every essay in one place, with word counts and status, so nothing lives in a document you cannot find later.', + }, + { + key: 'Application Tracking', sub: 'Building Your List', color: '#7048C8', emoji: '๐Ÿ“‹', + blurb: 'Build your college list, keep every deadline and requirement together, and see where each application stands.', + }, ] @@ -207,7 +230,7 @@ export default function Dashboard({ startIdx, answers, firstName, onSignOut }: P // Next-due deadline per module card, derived from the student's college list. const { events: deadlineEvents, failed: deadlinesFailed, toggleDone, addOwn, removeOwn, correctDate, - setNote, + setNote, trackedScholarships, } = useDeadlineEvents(startIdx, { // Entering or leaving a module refetches, so a scholarship added in one // shows up without the panel ever going dark. @@ -217,6 +240,13 @@ export default function Dashboard({ startIdx, answers, firstName, onSignOut }: P // never learns the two are the same thing. onNotice: toast.info, }) + const { apps } = useApplications() + const moduleStatus = useMemo(() => moduleStatusLines({ + colleges: apps.length, + submitted: apps.filter((a) => a.status === 'submitted').length, + scholarships: trackedScholarships, + }), [apps, trackedScholarships]) + // One clock for every dated view on this page, so the week strip, the panel // and the module chips can't disagree about which day is today. const now = useMemo(() => new Date(), []) @@ -255,7 +285,7 @@ export default function Dashboard({ startIdx, answers, firstName, onSignOut }: P // Sort modules by need (lower answer = higher priority) // TODO: use shared constants for module key mapping - const sorted = [...MODULES].sort((a, b) => { + const sorted = MODULES.filter((m) => moduleEnabled(m.key)).sort((a, b) => { const aScore = answers[a.key.toLowerCase().replace(/ /g, '-')] ?? 2 const bScore = answers[b.key.toLowerCase().replace(/ /g, '-')] ?? 2 return aScore - bScore @@ -377,7 +407,7 @@ export default function Dashboard({ startIdx, answers, firstName, onSignOut }: P initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: 0.1 + i * 0.06, duration: 0.5, ease: EASE_OUT }} - whileHover={{ y: -4, transition: { duration: 0.2 } }} + whileHover={{ y: -2, transition: { duration: 0.2 } }} onClick={() => { if (mod.key === 'Financial Aid' && !profile?.settings?.intros_seen?.includes('fafsa')) { setShowFafsaIntro(true) @@ -386,28 +416,43 @@ export default function Dashboard({ startIdx, answers, firstName, onSignOut }: P } }} > -
- {mod.emoji} -
+
-
-

{mod.key}

+

{mod.key}

+

{mod.blurb}

+ {/* What is in there, and what is next. The pill used to + sit beside the title in the module's own colour, + which made the loudest thing on the row a date the + panel to the right already lists. */} +
+ {moduleStatus[mod.key] && {moduleStatus[mod.key]}} + {moduleStatus[mod.key] && nextDueByModule[mod.key] && ( + + )} {nextDueByModule[mod.key] && ( )}
-

{mod.sub}

-
- Open โ†’ -
+ ))}
@@ -599,13 +644,15 @@ export default function Dashboard({ startIdx, answers, firstName, onSignOut }: P /> - setOpenModule(null)}> - setOpenModule(null)} - onOpenModule={(key) => setOpenModule(key)} - /> - + {FEATURES.knowledgeLibrary && ( + setOpenModule(null)}> + setOpenModule(null)} + onOpenModule={(key) => setOpenModule(key)} + /> + + )} setOpenModule(null)}> - setOpenModule(null)}> - setOpenModule(null)} - aside={deadlineRail} - /> - + {FEATURES.essays && ( + setOpenModule(null)}> + setOpenModule(null)} + aside={deadlineRail} + /> + + )} {/* FAFSA intro โ†’ definition โ†’ module chain */} diff --git a/web/src/components/FeeWaiverNotice.tsx b/web/src/components/FeeWaiverNotice.tsx index 7fbf643..bad8417 100644 --- a/web/src/components/FeeWaiverNotice.tsx +++ b/web/src/components/FeeWaiverNotice.tsx @@ -26,6 +26,14 @@ interface Props { onDismiss: () => void } +/** One line instead of three. The dashboard's job is to get a student to the + * week ahead; the full case for waivers is made inside the module, where + * there is room for it and a reason to be reading it. */ +const SHORT = { + likely: 'Fees run $50โ€“$90 a school, and waivers cover them in full.', + unknown: 'Add your household income and we can tell you whether you qualify.', +} as const + const COPY = { likely: { title: 'You likely qualify for application fee waivers', @@ -64,9 +72,9 @@ export default function FeeWaiverNotice({ eligibility, variant, onPrimary, onDis border: `1px solid ${C.border}`, borderLeft: `3px solid ${MC}`, borderRadius: 14, - padding: '14px 16px', + padding: variant === 'dashboard' ? '11px 14px' : '14px 16px', boxShadow: C.shadow1, - marginBottom: variant === 'dashboard' ? 20 : 16, + marginBottom: variant === 'dashboard' ? 14 : 16, }} >