Skip to content

Team NetworkShield — Loop#5

Open
ToluGIT wants to merge 1 commit intorguhack:mainfrom
ToluGIT:submission
Open

Team NetworkShield — Loop#5
ToluGIT wants to merge 1 commit intorguhack:mainfrom
ToluGIT:submission

Conversation

@ToluGIT
Copy link

@ToluGIT ToluGIT commented Mar 1, 2026

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

  • Next.js 15 (App Router)
  • Prisma + Turso (SQLite edge database)
  • Tailwind CSS
  • Recharts for data visualization
  • Deployed on Vercel

Features

  • Dashboard — Grade projection with UK Honours classification
  • Simulator — What-if grade scenarios with real-time classification updates
  • Peers — Find classmates by module and skill for study groups
  • Stress Pulse — Anonymous mood check-ins with cohort-wide analytics
  • Study Spots — Campus locations with real-time occupancy
  • Campus — Cohort statistics and module analytics
  • Wrapped — Shareable degree progress summary

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
Copilot AI review requested due to automatic review settings March 1, 2026 12:30
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +60 to +62
useEffect(() => {
refresh().finally(() => setLoading(false));
}, []);
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +104 to +106
useEffect(() => {
refresh().finally(() => setLoading(false));
}, []);
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +51 to +53
{current?.avatar && (
<img src={current.avatar} alt="" className="w-5 h-5 rounded-full" />
)}
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +124 to +131
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";
}
}
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +230 to +233
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;
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +74 to +80
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;
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Comment on lines +121 to +127
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 }));

Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +6 to +13
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",
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants