Conversation
Team NetworkShield submission for RGU Hack 26. Loop helps university students project their degree classification, find study peers, track stress levels, and locate campus study spots. Built with Next.js 15, Prisma, Turso (SQLite), and Tailwind CSS. Live at https://www.myloop.tech
There was a problem hiding this comment.
Pull request overview
Introduces the initial “Loop” Next.js app (student grade projection + peer matching + wellbeing pulse + study spots) with supporting Prisma/Turso persistence, UI components, algorithms, seed data, and API routes.
Changes:
- Add core UK Honours classification + simulator, insights, leverage, and boundary-risk calculation utilities.
- Add product pages/components for Dashboard, Simulator, Peers, Pulse, Campus, and Study Spots (incl. Leaflet map).
- Add Prisma schema/migrations/seed + API routes for users, simulator data, peers, pulse check-ins, campus stats, and study-spot check-ins.
Reviewed changes
Copilot reviewed 56 out of 65 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| loop/tsconfig.json | TypeScript compiler configuration + path aliasing. |
| loop/src/types/index.ts | Shared domain types (users/modules/grades/classification/peer/campus). |
| loop/src/lib/utils.ts | Tailwind className merge helper (cn). |
| loop/src/lib/skill-resources.ts | Maps skills/modules to learning resources for peer cards. |
| loop/src/lib/risk-analysis.ts | Boundary risk analysis logic for classification safety margin. |
| loop/src/lib/mock-data.ts | Demo seed data (modules/students/pulse/spots). |
| loop/src/lib/leverage.ts | “Leverage” ranking of ungraded assessments by impact. |
| loop/src/lib/insights.ts | Generates actionable text insights from grades/classification. |
| loop/src/lib/db.ts | Prisma client setup (Turso adapter vs local SQLite). |
| loop/src/lib/constants.ts | Shared constants/types for moods, trends, spots. |
| loop/src/lib/classification.ts | Core UK Honours classification + grade-needed calculation. |
| loop/src/lib/campus-stats.ts | Builds aggregated campus stats from user/module records. |
| loop/src/lib/anonymous-client.ts | Anonymous local client id for pulse/spots check-ins. |
| loop/src/components/user-switcher.tsx | Client user selector for demo personas. |
| loop/src/components/theme-toggle.tsx | Theme toggle button component. |
| loop/src/components/theme-provider.tsx | Theme context + persistence via localStorage. |
| loop/src/components/peer-card.tsx | Peer matching card + skill resource links. |
| loop/src/components/nav.tsx | Top navigation with responsive/mobile behavior. |
| loop/src/components/nav-wrapper.tsx | Conditionally hides nav on landing page. |
| loop/src/components/campus-map.tsx | Leaflet map rendering for study spots. |
| loop/src/app/spots/page.tsx | Study Spots page (filters + map + check-in UX). |
| loop/src/app/spots/loading.tsx | Loading UI for Study Spots route. |
| loop/src/app/simulator/page.tsx | What-if simulator page with sliders + boundary bar. |
| loop/src/app/simulator/loading.tsx | Loading UI for Simulator route. |
| loop/src/app/pulse/page.tsx | Stress Pulse page (check-in + charts + module stress). |
| loop/src/app/pulse/loading.tsx | Loading UI for Pulse route. |
| loop/src/app/peers/page.tsx | Peer matching page with module/skill filters. |
| loop/src/app/peers/loading.tsx | Loading UI for Peers route. |
| loop/src/app/page.tsx | Marketing/landing page UI + entry CTAs. |
| loop/src/app/layout.tsx | Root layout wiring fonts, theme provider, nav wrapper. |
| loop/src/app/icon.tsx | Generated favicon via next/og. |
| loop/src/app/globals.css | Design system, animations, components styling, a11y focus. |
| loop/src/app/dashboard/page.tsx | Dashboard page combining classification, insights, risk, leverage. |
| loop/src/app/dashboard/loading.tsx | Loading UI for Dashboard route. |
| loop/src/app/campus/page.tsx | Campus analytics page (charts + module performance). |
| loop/src/app/campus/loading.tsx | Loading UI for Campus route. |
| loop/src/app/apple-icon.tsx | Generated Apple touch icon via next/og. |
| loop/src/app/api/users/route.ts | Users listing endpoint for demo switching. |
| loop/src/app/api/users/[id]/route.ts | Fetch single user (incl. modules/assessments/grades). |
| loop/src/app/api/spots/route.ts | Study spots read + check-in/out endpoints. |
| loop/src/app/api/simulator/[userId]/route.ts | Simulator data endpoint (user + module/assessment/grades). |
| loop/src/app/api/pulse/route.ts | Pulse check-in upsert + aggregated reporting endpoint. |
| loop/src/app/api/peers/route.ts | Peer profiles endpoint with optional filtering. |
| loop/src/app/api/campus/route.ts | Campus stats aggregation endpoint. |
| loop/public/window.svg | Static asset. |
| loop/public/vercel.svg | Static asset. |
| loop/public/next.svg | Static asset. |
| loop/public/globe.svg | Static asset. |
| loop/public/file.svg | Static asset. |
| loop/prisma/seed.ts | Seed script for demo users/modules/grades/pulse/spots. |
| loop/prisma/schema.prisma | Prisma data model definitions. |
| loop/prisma/migrations/migration_lock.toml | Prisma migration lock metadata. |
| loop/prisma/migrations/20260301004500_pulse_spots/migration.sql | Migration adding pulse/spots tables + missing columns. |
| loop/prisma/migrations/20260228173807_init/migration.sql | Initial migration for core tables. |
| loop/postcss.config.mjs | PostCSS config for Tailwind v4 plugin. |
| loop/package.json | App dependencies/scripts (Next/Prisma/Tailwind/etc.). |
| loop/next.config.ts | Next config placeholder. |
| loop/next-env.d.ts | Next.js TypeScript environment typings. |
| loop/eslint.config.mjs | ESLint config using Next core-web-vitals + TS rules. |
| loop/README.md | Project README (create-next-app template). |
| loop/.gitignore | App-level ignore rules (Next build, env, Prisma db, etc.). |
| .gitignore | Repo-level ignore rules (includes loop artifacts). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| useEffect(() => { | ||
| refresh().finally(() => setLoading(false)); | ||
| }, []); |
There was a problem hiding this comment.
refresh() can throw (non-2xx response), but the initial useEffect only uses .finally(...) and doesn’t .catch(...). This can lead to an unhandled promise rejection and no user-visible error state. Consider catching and setting an error state (or swallowing explicitly) before setting loading=false.
| useEffect(() => { | ||
| refresh().finally(() => setLoading(false)); | ||
| }, []); |
There was a problem hiding this comment.
Same as spots: refresh() can throw, but the initial useEffect doesn’t catch the rejection (only .finally). This can produce unhandled promise rejections and leaves the UI without an error path. Consider adding .catch(...) + an error state / fallback UI.
| {current?.avatar && ( | ||
| <img src={current.avatar} alt="" className="w-5 h-5 rounded-full" /> | ||
| )} |
There was a problem hiding this comment.
Avatar images use alt="", which makes them invisible to screen readers even though they convey which student is selected. Consider using a meaningful alt text like the user’s name (or alt="${u.name} avatar") so the switcher remains understandable for assistive tech users.
| export function getResourceTypeIcon(type: SkillResource["type"]): string { | ||
| switch (type) { | ||
| case "course": return "graduation-cap"; | ||
| case "docs": return "book-open"; | ||
| case "tool": return "wrench"; | ||
| case "community": return "users"; | ||
| } | ||
| } |
There was a problem hiding this comment.
getResourceTypeIcon is declared to return a string, but the switch has no default/return fallback. With strict enabled this will either fail type-checking or return undefined at runtime if an unexpected value slips through. Add a default case (or an exhaustive never check) that returns a valid icon string or throws.
| const needed = Math.ceil(Math.max(0, Math.min(100, hi)) * 10) / 10; | ||
| // If even 100% on remaining won't reach the target, report null | ||
| if (needed > 100) return null; | ||
| return needed; |
There was a problem hiding this comment.
calculateGradeNeeded() can never return null for an unreachable target: hi is clamped to <= 100, so needed > 100 is impossible. As a result, callers may be told "100%" is sufficient even when the target classification cannot be reached. Consider explicitly testing the result at 100% before the search (or after) and returning null if it still doesn’t meet target.
| function FitBounds({ spots }: { spots: Spot[] }) { | ||
| const map = useMap(); | ||
| if (spots.length > 0) { | ||
| const bounds = L.latLngBounds(spots.map((s) => [s.lat, s.lng])); | ||
| map.fitBounds(bounds, { padding: [40, 40], maxZoom: 17 }); | ||
| } | ||
| return null; |
There was a problem hiding this comment.
FitBounds() calls map.fitBounds(...) during render. That’s a side effect and will run on every re-render (e.g. any state change), which can cause the map to keep snapping/zooming unexpectedly and hurts performance. Move the fitBounds call into a useEffect keyed on spots (and optionally guard against identical bounds).
| const weeklyMood = buildWeeklyMood(checkIns); | ||
| const overallTrend = trendFromHistory(weeklyMood); | ||
|
|
||
| const modules = Array.from(moduleMap.values()) | ||
| .sort((a, b) => a.code.localeCompare(b.code)) | ||
| .map((module) => ({ ...module, trend: overallTrend })); | ||
|
|
There was a problem hiding this comment.
The trend field is computed once from weeklyMood built over all check-ins and then applied to every module (.map((module) => ({ ...module, trend: overallTrend }))). This makes every module show the same trend even if their histories differ. If the UI intends per-module trends, compute a weekly series per moduleCode and derive the trend from that per module.
| export async function GET() { | ||
| const users = await prisma.user.findMany({ | ||
| orderBy: { name: "asc" }, | ||
| }); | ||
|
|
||
| return NextResponse.json(users, { | ||
| headers: { | ||
| "Cache-Control": "s-maxage=120, stale-while-revalidate=300", |
There was a problem hiding this comment.
This endpoint returns full User records (including email) and caches the response at the edge. UserSwitcher only needs id/name/course/year/avatar, so returning email here increases unnecessary PII exposure. Prefer selecting only the required fields (and consider Cache-Control: private/no-store if sensitive fields must be returned).
Loop — Student Grade Projection & Wellbeing Platform
Live: https://loop-seven-woad.vercel.app/
Loop helps university students project their degree classification using the UK Honours weighted average system (L5×⅓ + L6×⅔), find study peers, track stress levels via anonymous check-ins, and locate campus study spots.
Tech Stack
Features