Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/asubap/website/llms.txt

Use this file to discover all available pages before exploring further.

The Member Dashboard (/member) is the primary landing destination for any authenticated general-member. It is wrapped in a ProtectedRoute and immediately calls POST /member-info/ to hydrate the member’s profile from the backend. The page is anchored by MemberDescription, which renders the full left-column profile card and also mounts EventMember (the right-column event panel) and ProfileEditModal within its JSX tree.

Page Architecture

MemberView composes one top-level component that owns the full authenticated layout:

MemberDescription

Renders the profile card (photo, name, major, email, phone), the hours breakdown, the About section, and the Edit Profile button. Also owns the announcements bell, the Slack shortcut, and mounts EventMember and ProfileEditModal internally.

EventMember

Rendered inside MemberDescription. Independently fetches GET /events and categorises events into In-Session, Upcoming, and Attended. Uses EventCard for each row.

ProfileEditModal

A modal layered on top of MemberDescription that handles form state, profile-picture upload/delete, and the POST /member-info/edit-member-info/ save call.

Data Flow

1

Session check

MemberView reads the Supabase session from useAuth(). If no session exists, ProtectedRoute redirects to login before this page mounts.
2

Fetch member info

fetchUserDetails() is called on mount. It calls:
POST /member-info/
Authorization: Bearer <access_token>
Content-Type: application/json

{ "user_email": "<email>" }
The response array’s first element is spread into userDetails state.
3

Render MemberDescription

All user fields are forwarded as props. onRefreshUserDetails (a reference to fetchUserDetails(false)) is also passed so child components can trigger a silent refresh — for example, after a successful check-in.
4

Visibility-change refresh

MemberDescription attaches a visibilitychange listener and calls onRefreshUserDetails() whenever the browser tab regains focus, keeping hours and rank current after tab switches.
5

EventMember fetch

EventMember runs its own GET /events fetch inside useEffect. Events are sorted by event_date then partitioned by isEventInSession() and compared to now.

Profile Fields

MemberView maintains a typed userDetails state object whose keys match the backend response from POST /member-info/:
{
  id: string;
  user_email: string;
  name: string;
  major: string;
  phone: string;
  graduating_year: string;
  rank: string;              // "pledge" | "inducted" | "alumni"
  member_status: string;     // "Looking for Internship" | "Looking for Full-time" | "Not Looking"
  about: string;
  total_hours: string;
  development_hours: string;
  professional_hours: string;
  service_hours: string;
  social_hours: string;
  event_attendance: any[];
}
These values are passed as individual props to MemberDescription, which maps them to the MemberDetail shape (camelCase) for internal state and the ProfileEditModal.
rank drives the isAlumni() check throughout the dashboard. When rank === "alumni", the hours breakdown row, the announcements bell, and the Slack button are all removed from the DOM.

Hours Breakdown

Non-alumni members see five numeric values displayed below their basic info:
FieldBackend keyDescription
Total Hourstotal_hoursSum of all categories
Social Hourssocial_hoursSocial event attendance
Professional Hoursprofessional_hoursProfessional development events
Service Hoursservice_hoursCommunity service events
Development Hoursdevelopment_hoursChapter development events

Edit Profile Modal

Clicking Edit Profile opens ProfileEditModal. The modal uses a useRef snapshot of the initial data to detect unsaved changes and prompts a discard-confirmation dialog on close if any field was modified.

Editable fields

FieldInput typeRequiredNotes
NametextCannot be empty or "N/A"
EmailemailDisabled — cannot change
Phone Numbertel
Major(s)textFree-text; multiple majors as one string
Graduation YeartextMaps to graduationDate in MemberDetail
StatusselectLooking for Internship / Looking for Full-time / Not Looking
AbouttextareaMinimum 1 non-"N/A" character

Save endpoint

POST /member-info/edit-member-info/
Authorization: Bearer <access_token>
Content-Type: application/json

{
  "user_email": "jane@example.com",
  "about": "...",
  "name": "Jane Doe",
  "graduating_year": "2026",
  "major": "Accounting & Information Systems",
  "phone": "555-0100",
  "member_status": "Looking for Internship",
  "member_rank": "inducted"
}
On success the parent onSave callback updates local profileData state, and a toast confirmation is shown. On failure the error detail from the response body is surfaced via showToast.

Event Panels

EventMember renders three scrollable lists (max-height 400px on sm screens, 600px on mobile):
Filtered by isEventInSession(event_date, event_time, event_hours). The helper computes start and end = start + event_hours × 3600 seconds; the event is in-session when now ∈ [start, end]. Each card shows hideRSVP={true} (RSVP is hidden during the session). A successful check-in calls fetchEvents() and onRefreshUserDetails?.() to refresh both the event list and the hours totals.
All non-in-session events where event_date/time >= now, sorted ascending by date. Renders EventCard with full RSVP and check-in buttons (subject to alumni restrictions).
Sourced from the eventAttendance prop (the event_attendance array from POST /member-info/), passed from MemberView through MemberDescription into EventMember. Items are sorted descending by event_date (most recent first). Each item is adapted to the Event shape before passing to EventCard:
const eventForCard: Event = {
  id: attendedEvent.event_id?.toString() || index.toString(),
  event_name: attendedEvent.event_name || 'Event',
  event_description: attendedEvent.event_description || 'No description available',
  rsvp_count: 0,
  attending_count: 0,
  event_lat: null,
  event_long: null,
  user_rsvped: false,
  user_attended: true,  // always true
  can_check_in: false,
  // ...remaining fields from attendedEvent
};

Announcements Bell

The bell icon (from lucide-react) in the top-right of the profile column is only visible to non-alumni. It tracks unread count using localStorage under the key readAnnouncementIds.
// Storage helpers inside MemberDescription
const READ_ANNOUNCEMENTS_KEY = "readAnnouncementIds";

const getReadAnnouncementIdsFromStorage = (): string[] => { /* ... */ };
const addAnnouncementsToReadStorage = (idsToAdd: string[]) => { /* ... */ };
Opening the modal calls addAnnouncementsToReadStorage(allFetchedAnnouncementIds) to mark all currently fetched announcements as read, then recalculates the badge count. The badge caps display at 9+ if there are more than nine unread items. Announcements are fetched from:
GET /announcements
Authorization: Bearer <access_token>
The response is passed directly to MemberAnnouncementsListModal.

Slack Shortcut

A second icon button next to the bell opens https://beta-alpha-psi-space.slack.com/ in a new tab. Like the bell, it is hidden for alumni.

Alumni Restrictions Summary

import { isAlumni, canAccessFeature } from "../../utils/permissions";

// isAlumni("alumni")      → true
// isAlumni("inducted")    → false
// isAlumni("pledge")      → false

// canAccessFeature("alumni", "announcements") → false
// canAccessFeature("alumni", "slack-access")  → false
// canAccessFeature("inducted", "event-rsvp")  → true
When isAlumni(rank) returns true:
  • Hours section is removed
  • Announcements bell is removed
  • Slack button is removed
  • EventCard hides the RSVP and Check-In buttons (handled in EventCard itself via the same isAlumniUser flag)

Extending the Dashboard

To add a new profile field, update the userDetails state shape in MemberView, add it to the POST /member-info/edit-member-info/ payload in ProfileEditModal.handleSubmit, and render it in MemberDescription. All three files must change in sync.
To add a new hours category:
  1. Add the backend key to the userDetails state object in MemberView.
  2. Pass it as a new prop from MemberView → MemberDescription → EventMember as needed.
  3. Add a corresponding entry to the MemberDetail type in types/index.ts.
  4. Render it alongside the existing hour rows in MemberDescription.

Build docs developers (and LLMs) love