Skip to content

Commit 716df2b

Browse files
committed
Add school profile frontend with fit summary
1 parent a607b80 commit 716df2b

11 files changed

Lines changed: 951 additions & 31 deletions

File tree

README.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
College Exploration Platform is a full-stack decision-support product for helping prospective and admitted students discover, compare, rank, and justify college choices with transparent data and deterministic scoring.
44

5-
Status: V1.9 deterministic ranking engine complete. Redis, pgvector, saved schools, comparisons, and deployment are intentionally not implemented yet.
5+
Status: V1.10 school profile frontend complete. Redis, pgvector, persisted saved schools, comparisons, and deployment are intentionally not implemented yet.
66

77
## Project Thesis
88

@@ -156,6 +156,7 @@ Useful local URL:
156156
- Web app: `http://localhost:3000`
157157
- Onboarding: `http://localhost:3000/onboarding`
158158
- Search UI: `http://localhost:3000/search`
159+
- School profile: `http://localhost:3000/schools/1`
159160

160161
Frontend environment:
161162

@@ -221,12 +222,15 @@ Expected future commands:
221222
## Limitations
222223

223224
- `/health`, `/ready`, `/schools/search`, `/schools/{id}`, and `/rankings` exist. Preference persistence, saved-school, and comparison endpoints are not implemented yet.
224-
- The frontend has a landing page, onboarding, search UI, route shell, UI primitives, and typed API client, but no backend preference persistence, persisted saved-school flows, comparison workflow, or profile pages yet.
225+
- The frontend has a landing page, onboarding, search UI, school profile pages, route shell, UI primitives, and typed API client, but no backend preference persistence, persisted saved-school flows, or full comparison workflow yet.
225226
- Onboarding stores a typed `PreferenceProfile` in browser `localStorage` and forwards supported filters such as state, setting, school type, and max net price to `/search`.
226227
- Search supports structured filters, sort controls, URL state, pagination, local save/compare state, loading/empty/error states, and API-backed result cards.
228+
- School profiles call `GET /schools/{id}`, render fit summary placeholders, academics, cost, outcomes, campus life, data-quality metadata, and a V2 similar-schools placeholder.
227229
- The "Best fit" sort is a UI placeholder until the frontend calls `POST /rankings`.
230+
- Profile fit score, category scores, top reasons, top tradeoffs, and ranking version remain unavailable on `GET /schools/{id}` unless the backend later adds or composes ranking output. The profile page labels these states explicitly as unavailable and uses `data_confidence_score` only as data-completeness confidence.
228231
- No Redis cache, pgvector integration, or deployment exists yet.
229232
- No performance metrics are available.
230233
- Seed data is synthetic and intended for deterministic local development, not factual school reporting.
231-
- End-to-end validation will be added after the product workflows exist.
234+
- Playwright smoke coverage exists for search, onboarding, and school profiles.
235+
- README screenshot checklist for V1.13: landing page, onboarding completion, search with filters, school profile fit summary, profile missing-data state, and compare tray.
232236
- Documentation is intentionally concise and should be updated as each implementation step lands.
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
"use client";
2+
3+
import Link from "next/link";
4+
5+
import { Button } from "@/components/ui/button";
6+
7+
export default function Error({ reset }: { error: Error & { digest?: string }; reset: () => void }) {
8+
return (
9+
<main className="mx-auto flex min-h-screen w-full max-w-3xl items-center justify-center px-5 py-12">
10+
<div className="rounded-lg border border-border bg-white p-8 shadow-soft">
11+
<h1 className="text-2xl font-semibold tracking-normal text-foreground">
12+
School profile failed to load
13+
</h1>
14+
<p className="mt-3 text-sm leading-6 text-muted-foreground">
15+
The profile page could not render. Keep the API server running and try again.
16+
</p>
17+
<div className="mt-6 flex gap-3">
18+
<Button type="button" onClick={reset}>Try again</Button>
19+
<Button asChild variant="secondary">
20+
<Link href="/search">Back to search</Link>
21+
</Button>
22+
</div>
23+
</div>
24+
</main>
25+
);
26+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { Card, CardContent, CardHeader } from "@/components/ui/card";
2+
import { Skeleton } from "@/components/ui/skeleton";
3+
4+
export default function Loading() {
5+
return (
6+
<main className="mx-auto min-h-screen w-full max-w-7xl px-5 pb-16 pt-8 sm:px-8">
7+
<Skeleton className="h-5 w-32" />
8+
<section className="mt-6 rounded-lg border border-border bg-white p-6 shadow-soft">
9+
<Skeleton className="h-6 w-36" />
10+
<Skeleton className="mt-5 h-12 w-3/4" />
11+
<Skeleton className="mt-3 h-5 w-72" />
12+
</section>
13+
<div className="mt-6 grid gap-6 lg:grid-cols-[minmax(0,1fr)_360px]">
14+
<div className="space-y-6">
15+
{Array.from({ length: 4 }, (_, index) => (
16+
<Card key={index}>
17+
<CardHeader>
18+
<Skeleton className="h-7 w-48" />
19+
</CardHeader>
20+
<CardContent>
21+
<Skeleton className="h-40 w-full" />
22+
</CardContent>
23+
</Card>
24+
))}
25+
</div>
26+
<Skeleton className="h-64 w-full" />
27+
</div>
28+
</main>
29+
);
30+
}

apps/web/app/schools/[id]/page.tsx

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import type { Metadata } from "next";
2+
3+
import { SchoolProfilePage } from "@/components/schools/school-profile";
4+
import { getSchoolProfile } from "@/lib/schools";
5+
6+
type SchoolPageProps = {
7+
params: Promise<{
8+
id: string;
9+
}>;
10+
};
11+
12+
export async function generateMetadata({ params }: SchoolPageProps): Promise<Metadata> {
13+
const { id } = await params;
14+
const schoolId = Number(id);
15+
16+
if (!Number.isInteger(schoolId) || schoolId <= 0) {
17+
return {
18+
title: "School Profile",
19+
description: "Explore a structured school profile.",
20+
};
21+
}
22+
23+
try {
24+
const profile = await getSchoolProfile(schoolId);
25+
return {
26+
title: profile.name,
27+
description: `${profile.name} profile, fit summary, academics, cost, outcomes, and campus life.`,
28+
};
29+
} catch {
30+
return {
31+
title: "School Profile",
32+
description: "Explore a structured school profile.",
33+
};
34+
}
35+
}
36+
37+
export default async function SchoolPage({ params }: SchoolPageProps) {
38+
const { id } = await params;
39+
const schoolId = Number(id);
40+
41+
return <SchoolProfilePage schoolId={Number.isInteger(schoolId) && schoolId > 0 ? schoolId : 0} />;
42+
}

0 commit comments

Comments
 (0)