0%
balqalam Logo
balqalam
FeaturesCommunityPricingDocumentation
Login
  • Introduction
  • Help center
  • Common questions
  • Get Started
  • Localhost
  • Architecture
  • Structure
  • Pattern
  • Page
  • Layout
  • Content
  • Types
  • Config
  • Actions
  • Queries
  • Authorization
  • Validation
  • Form
  • Table
  • Detail
  • Card
  • Util
  • Hooks
  • List Params
  • Views
  • README.md
  • ISSUE.md
  • Technology Stack
  • Database
  • File
  • CDN Assets
  • Entry Points
  • Dashboard
  • Authentication
  • Credentials
  • OAuth
  • Flow Diagrams
  • Multi-Tenancy
  • Offline
  • Onboarding
  • Onboarding Videos
  • Add Values
  • Admission
  • Application
  • Attendance
  • Compliance
  • Profile
  • Exams
  • Exam Wizard
  • Timetable
  • Classrooms
  • Notifications
  • Conference
  • LMS (Lumos)
  • Finance
  • Fee Management
  • Invoice
  • Wallet
  • Salary
  • Payroll
  • Timesheet
  • Expenses
  • Budget
  • Receipt
  • Accounts
  • Banking
  • Reports
  • Dashboard
  • Permissions
  • Messages
  • Integration Flow
  • Provision
  • AI Document Processing
  • Document Intelligence
  • Internationalization
  • Translation
  • Translation Guide
  • Icons
  • Docs Factory
  • Inspiration
  • Listings
  • Teachers
  • Students
  • Catalog
  • Library
  • Contributing
  • Code of conduct
  • GitHub Workflow
  • Database Seeds
  • Database Safety
  • Test Accounts
  • Bulk import
  • Playwright
  • Prettier
  • Block Rebound
  • Transportation

Profile

PreviousNext

GitHub-inspired multi-role profile — real contribution graph, earned badges, organizations, pinned items, activity feed, and cross-tenant-safe permission masking.

Every school user (student, teacher, parent, staff) gets a GitHub-style profile: a left sidebar (avatar, name, real stat counts, earned badges, organizations) and right content (role tabs, a real contribution graph, pinned items, and an activity feed). One optional catch-all route handles both own-profile and view-by-id.

Everything on the page is real and tenant-scoped. The block was rebuilt (2026-06-15) to remove the previous prototype's fabricated data — hardcoded stats, Math.random() activity, mock "Student of the Year" achievements, and "Chess Club" organizations. When a real source is missing, an honest empty state is shown.

Two layouts, one page

The page composes itself differently on a phone — it does not just narrow. Desktop keeps the profile column beside the content column, with a year rail of buttons next to the contribution graph. On a phone the order follows GitHub's own mobile profile: the tab rail comes first, then a 64px avatar beside the name, then the panel. The edit button goes full width, section headings step up a size, and the year rail becomes a dropdown on the graph heading. The feed shows six entries with the rest behind Show more activity — the server caps it at ten, so nothing is promised that is not already loaded. Editing keeps the one portrait and hangs the change-photo control off it, rather than drawing a second avatar inside the form.

The switch is useSidebar().isMobile (768px). Type-scale tweaks reset at md, the same 768px, so the layout and the type never disagree. The reference captures live in public/profile-github/.

What the contribution graph counts

The grid runs from January 1 to today for the year in progress — no one has contributions in the future, and empty months push the live weeks off a phone — and it opens scrolled to the most recent weeks, with the weekday column pinned.

Each role counts the domain rows that recorded its work:

RoleCounted from
Studentattendance, assignment submissions, graded results, library loans
Teacherattendance they marked, results they graded
Parentmessages they sent, plus their logged activity
Staffexpenses they approved, plus their logged activity

Staff and parents count UserActivity; students and teachers deliberately do not. A student's activity rows narrate the attendance and submissions already counted above them, so counting both would count each day twice. Staff have no domain table for most of what they do, which is why their graph and the activity feed beneath it would otherwise disagree.

If a demo profile's graph looks empty, that is data, not code: run pnpm db:seed:single profile-activity, which gives the demo accounts activity across most school days of the year. The read is cached for five minutes, so load twice before drawing a conclusion.

Routes

RoutePageBehaviour
/profileprofile/[[...id]]/page.tsxOwn profile (isOwner=true)
/profile/<id>profile/[[...id]]/page.tsxView by id (User, or wizard entity)

Path: src/app/[lang]/s/[subdomain]/(school-dashboard)/profile/[[...id]]/. The route has loading.tsx (layout-matched skeleton) and error.tsx (locale-aware boundary). id may be a User id, or a wizard-created Student/Teacher id that has no User row (handled by a fallback path).

Architecture

The page calls a single typed read and passes the result down to pure presenter components — no client component fetches or fabricates data.

page.tsx ──> getProfileView(targetId, lang)            // queries.ts (typed, permission-masked)
   │            └─ real stats · badges · organizations · pinned · activity · roleDetail
   └─> ProfileDetailContent ──> ProfileContent (client.tsx)
            ├─ ProfileSidebar   (avatar, stats, badges, organizations, edit entry)
            ├─ PinnedItems      (real pins; owner reorder/remove, persisted)
            ├─ ContributionGraph(SWR → getContributionData; empty grid when none)
            ├─ ContributionActivity (real UserActivity grouped by month)
            ├─ AchievementsGrid    (achievements tab: earned-badge grid)
            └─ Student/Teacher/Parent/Staff dashboards (real subjects/sections/children/orgs)

queries.ts is intentionally not "use server" so callers can wrap it in cache(). Client-callable mutations live in actions.ts ("use server").

Data model

ModelTablePurpose
UserActivityuser_activitiesActivity feed + contribution graph
PinnedItempinned_itemsUp to 6 pinned items per user
ProfileBadgeprofile_badgesEarned badges (awarded by the engine)
OrganizationorganizationsSchool clubs / committees / teams
OrganizationMembershiporganization_membershipsUser ↔ organization, with role
Achievement (existing)achievementsStudent achievement records → badges

GitHub-style fields (bio, website, timezone, pronouns, statusEmoji, statusMessage, socialLinks) live on User.

Deploy note: the migration-of-record prisma/migrations/20260615000000_add_profile_badges_organizations/ was applied to prod on 2026-06-15. To populate the demo school's profile surfaces run pnpm db:seed:single profile-extras then pnpm db:seed:single profile-activity.

Demo data (profile-activity seed)

Every profile section renders from real rows, so the demo needs real rows. The idempotent profile-activity seed (part of seedMain) creates, for the demo school: current-year attendance for the demo student's section (marked by the demo teacher — lights both graphs and earns the attendance badges), an Arabic activity feed for demo accounts + roster/teacher/guardian users, role-appropriate pinned items derived from real subjects/sections/children/ organizations, a parent↔teacher conversation whose parent-sent messages light the parent graph, expense approvals spread across admin/staff/accountant (staff graph), demo-student achievements in distinct categories, and the GitHub-style User fields (status, website, social links, timezone). Badge artwork is served from the CDN at hogwarts/<icon>.png (sources in /public/github, re-upload with npx tsx prisma/scripts/upload-badge-art.ts).

Badge earning engine

badges.ts → recomputeProfileBadges(userId, schoolId, role) derives badges from real signals and reconciles the user's auto-managed badge rows idempotently:

  • perfect_attendance — student with zero absences this year (or a teacher who marked attendance consistently)
  • top_of_class / honor_roll — student merit rank 1 / top 3
  • diligent_educator — teacher who graded/marked ≥ 20 items this year
  • active_contributor — ≥ 20 logged activities this year
  • community_member — belongs to ≥ 1 organization
  • a student's own Achievement records → one badge each (icon by category, level by scope)

Manually-awarded badges (keys outside the catalog / achievement_ prefix) are never touched. Trigger a refresh with the recomputeMyBadges server action.

Permissions (cross-tenant-safe)

getPermissionLevel (detail/permissions.ts) returns OWNER | ADMIN | STAFF | RELATED | PUBLIC. Elevation rules:

  • DEVELOPER is the only role with cross-school access.
  • ADMIN / TEACHER / STAFF / ACCOUNTANT are elevated only when viewerSchoolId === profileSchoolId — otherwise PUBLIC.
  • Same-school students/guardians get RELATED.

getProfileView masks sensitive fields (email, ids) for non-privileged viewers on top of the always-tenant-scoped DB query.

Server actions (actions.ts)

ActionPurpose
updateGitHubProfile(input)Persist name/bio/website/timezone/pronouns/status/social
uploadProfileAvatar(formData)Avatar → User.image and role profilePhotoUrl
getPinnedItems(userId?)Read pins (public-only for other users)
updatePinnedItems(items)Replace own pins (max 6) in a transaction
getContributionData(params)Heatmap data (role lookup schoolId-scoped)
getRecentActivity(userId?, limit)Activity feed (viewer-permission gated)
logUserActivity(input)Append a UserActivity row
recomputeMyBadges()Re-run the earning engine for the caller

Reads are in queries.ts (getProfileView); self-edit entity data in edit-role-actions.ts (getOwnEntity).

Real data, no mock

Every surface is DB-backed:

  • Stats — counts of the user's subjects / sections / students / children
  • Activity — real UserActivity rows (no Math.random())
  • Badges — earned via the engine from real attendance/results/merit
  • Organizations — real memberships (+ a teacher's primary department)
  • Pinned items — real PinnedItem rows; empty state when none

Known issues

  • DB tables deploy-pending (see Deploy note).
  • Adding a new pinned item from the UI is not yet wired (display + reorder + remove are).
  • profile-flows.spec.ts predates the rebuild — selectors + a cross-school isolation case are a follow-up.

See also

  • Tests: src/tests/school-dashboard/profile/
  • Block records: src/components/school-dashboard/profile/{README,ISSUE,CLAUDE}.md
ComplianceExams

On This Page

Two layouts, one pageWhat the contribution graph countsRoutesArchitectureData modelDemo data (profile-activity seed)Badge earning enginePermissions (cross-tenant-safe)Server actions (actions.ts)Real data, no mockKnown issuesSee also

Built by Databayt ·

Welcome to balqalam.

A great journey is about to begin.