alumni_lookup

Phase 5: Pre-MVP Updates

Status: In Progress
Estimated Effort: 4-6 weeks
Prerequisites: Phase 4 Complete (Mobile-First Interface Cleanup)

Related Documents:


Table of Contents

  1. Overview
  2. Why This Phase Exists
  3. Sub-Phases
  4. Definition of Success
  5. Tests to Create
  6. Documentation Updates

1. Overview

Phase 5 adds final features required for MVP launch — capabilities that enhance the core Champion experience before opening to all users.

What This Phase Delivers

Feature Description
User State Experience Alignment Intentional UX for anonymous, email-verified, and champion-verified states
Faculty/Staff Support Belmont faculty/staff can join as Champions with manual approval
Almost Alumni Support Current students with anticipated graduation dates
Friending/Favoriting Save other Champions for quick access
Alumni Matching Help new signups connect with their alumni record
Photo Albums Champions and events can have photo galleries

MVP Launch Point

Phase 5 completion = MVP ready. After this phase, the Champion Portal can soft launch to initial users.


2. Why This Phase Exists

Gap Analysis from Beta Feedback

Anticipated needs before broader launch:

  1. User state experiences are inconsistent — Anonymous, email-verified, and champion-verified users see different things but the differentiation is not intentional
  2. Faculty/staff want to participate — Belmont employees who want to support Champions
  3. Almost Alumni need a path — Current students who will be alumni but don’t have degrees yet
  4. Champions want to save contacts — “Follow” or “Favorite” frequently-contacted Champions
  5. Signup friction — Some users struggle to connect their Champion signup with alumni record
  6. Events need photos — Post-event engagement through shared photos

3. Sub-Phases

Sub-Phase 5.1: User State Experience Alignment

Goal: Create intentional, differentiated UX for three distinct user states with consistent messaging and appropriate feature gating.

→ See 5.1-user-experiences.md for full specification.

Summary: | User State | Can Access | Experience Theme | |————|————|——————| | Anonymous | Landing page, public event/post URLs | “Welcome—here’s what awaits” | | Email Verified | Dashboard (limited), profile edit | “You’re almost there—verification in progress” | | Champion Verified | Full portal access | “Welcome home—your community” |

Key Deliverables:


Sub-Phase 5.2: Almost Alumni + Faculty/Staff Support

Goal: Allow current students (Almost Alumni) and Belmont faculty/staff to become Champions with clear verification, profile display, directory behavior, and upgrade logic.

In Scope:

Out of Scope:


5.2a Membership Types and Data Model

Membership Types:

Data Model Changes:

# cp_champions table additions
add_column :cp_champions, :membership_type, :integer, default: 0, null: false
# 0: alumni (default)
# 1: almost_alumni (current student)
# 2: faculty_staff

# Almost Alumni fields
add_column :cp_champions, :anticipated_graduation_date, :date
add_column :cp_champions, :anticipated_college_code, :string
add_column :cp_champions, :anticipated_program, :string

# Faculty/Staff fields
add_column :cp_champions, :work_email, :string
add_column :cp_champions, :profession, :string
add_column :cp_champions, :affiliated_college_code, :string
add_column :cp_champions, :affiliated_program, :string

# Optional: future-facing ID standardization
add_column :cp_champions, :bqid, :string

# alumni_id becomes optional
change_column_null :cp_champions, :alumni_id, true

# BUID uniqueness (prevent duplicate Champion identities)
add_index :cp_champions, :buid, unique: true

5.2b Verification and Approval Workflow (Lookup Portal)

Where Verification Lives:

Core Rule:

Domain Eligibility Hints (not auto-approval):

Verification Requirements:


5.2c Signup and Wizard UX

Default assumption: alumni match flow.

Membership resolution step: /profile/wizard/confirm_education

Flow:

  1. Attempt match (current behavior)
  2. If match shown, user confirms or selects “none of these are me”
  3. If no match or user rejects matches, show connection selector:
    • I’m a Belmont graduate (alumni)
    • I’m a current student (Almost Alumni)
    • I’m Belmont faculty/staff

Required fields at signup:

Membership change requests:


5.2d Profile Display Rules


5.2e Directory Behavior and Filters


5.2f Almost Alumni Upgrade to Alumni (Degree Import Hook)

Trigger: hook into degree import pipeline in Alumni Lookup.

Matching: for each alumni record updated during import, find Champions where:

Upgrade: if alumni now has one or more degrees:

Optional: staff-only “Recheck education” action for a single Champion.


Deliverables:

Completion Summary (Phase 5.2): Implemented Almost Alumni and Faculty/Staff support in January 2026. Champions can now sign up as “Almost Alumni” (current students awaiting graduation) or “Faculty/Staff” with appropriate fields for each membership type. The directory and profile views display anticipated education for Almost Alumni and profession/affiliation for Faculty/Staff with appropriate badges. Also includes bio step in wizard, major autocomplete, graduation month/year picker, and admin membership type change UI.


Sub-Phase 5.3: Adding Someone to Your Contacts

Goal: Champions can save other Champions as personal contacts for quick access, enhanced privacy controls, and personalized content surfacing.

Use Cases:


5.3a Data Model

# cp_champion_contacts table
create_table :cp_champion_contacts do |t|
  t.references :champion, null: false, foreign_key: { to_table: :cp_champions }
  t.references :contact, null: false, foreign_key: { to_table: :cp_champions }
  t.boolean :mutual, default: false, null: false  # true if both added each other
  t.string :notes, limit: 255  # Optional private note (nice-to-have)
  t.timestamps
end

add_index :cp_champion_contacts, [:champion_id, :contact_id], unique: true, name: "idx_champion_contacts_unique"
add_index :cp_champion_contacts, [:contact_id, :champion_id], name: "idx_contacts_reverse_lookup"
add_index :cp_champion_contacts, [:champion_id, :mutual], name: "idx_champion_contacts_mutual"

Mutual Tracking Logic:


5.3b Privacy Settings Enhancement

Current privacy model: Dropdown with public | district_only | hidden

New privacy model: Checkbox-based with granular control

Privacy Options Structure:

Who can see [field]?
○ Anyone
○ Limit who can see:
   ☐ Alumni in my district
   ☐ Alumni in my communities
   ☐ Community Leaders of my communities
   ☐ My contacts

Fields affected:

Data Model Changes:

# Add to cp_champions
add_column :cp_champions, :location_privacy, :jsonb, default: { "level": "anyone" }, null: false
# level: "anyone" | "limited"
# when limited, includes array: ["district", "communities", "community_leaders", "contacts"]

add_column :cp_champions, :messaging_privacy, :jsonb, default: { "level": "anyone" }, null: false
# level: "anyone" | "limited"
# when limited, includes array: ["district", "communities", "community_leaders", "contacts"]

Privacy Logic (one-directional):


5.3c Notifications

New notification types:

Notification preferences:

# In notification_preferences JSONB
{
  "contact_added": {
    "enabled": true,
    "channels": ["in_app", "email"]  # Options: in_app, email, or both
  }
}

Email notification: Immediate, includes:

Dashboard prompt: “Sarah added you as a contact—add her back?”


5.3d Messaging: New Message Composer

Location: Modal triggered from /messages index page

UX Pattern (Instagram-style):

┌──────────────────────────────────────┐
│ New Message                      ✕   │
├──────────────────────────────────────┤
│ To: [Search contacts...]             │
│     ┌─────────────────────────────┐  │
│     │ Sarah Champion  ✕           │  │
│     └─────────────────────────────┘  │
├──────────────────────────────────────┤
│ Suggested                            │
│ ┌────┐                               │
│ │ 📷 │ Jane Doe              ○      │
│ └────┘ Nashville, TN                 │
│ ┌────┐                               │
│ │ 📷 │ Mike Smith            ○      │
│ └────┘ Atlanta, GA                   │
│ ┌────┐                               │
│ │ 📷 │ Sarah Champion        ●      │
│ └────┘ Dallas, TX                    │
│                                      │
│ ┌──────────────────────────────────┐ │
│ │            Chat                  │ │
│ └──────────────────────────────────┘ │
└──────────────────────────────────────┘

Behavior:

  1. Modal opens with “Suggested” showing your contacts
  2. Search field filters contacts as you type
  3. Select one contact (radio button style)
  4. Click “Chat” → navigates to /messages/new?recipient=:champion_id or existing thread

Scope: Contacts only. To message someone not in contacts, use Directory → Profile → “Say Hello”


5.3e Directory Integration

My Contacts View: /directory/contacts

Filter Toggle: In directory filter dropdown


5.3f Profile Integration

Add/Remove Button:

Contact Info on Profile:


5.3g Popularity Algorithm Enhancement (Nice-to-Have)

Scope: Discussion boards only (events/news backlogged for future)

Formula update:

def popularity_score(viewer: nil)
  base_score = (reactions_count * 2) + (comments_count * 3) + (time_factor * 10)
  
  # Apply contact boost if viewer provided and author is in viewer's contacts
  if viewer && viewer.contact_ids.include?(author_id)
    base_score * 1.3
  else
    base_score
  end
end

Implementation:


5.3h Community Suggestions (Nice-to-Have)

Trigger: When adding a new contact, check if any of their communities now have 5+ of your contacts

Logic:

# On contact add
def check_community_suggestions(champion, new_contact)
  new_contact.communities.each do |community|
    next if champion.communities.include?(community)
    next if champion.community_suggestions.exists?(community: community)
    
    contacts_in_community = champion.contacts
      .joins(:communities)
      .where(cp_champion_communities: { community_id: community.id })
      .count
    
    if contacts_in_community >= 5
      champion.community_suggestions.create!(
        community: community,
        reason: "contacts_threshold",
        metadata: { contact_count: contacts_in_community }
      )
    end
  end
end

Display: Uses existing community suggestion UI on dashboard


5.3i Activity Tracking

New event types:

# Add to Cp::ActivityEvent::EVENT_TYPES
contact_added
contact_removed
contact_notes_updated  # if notes implemented

5.3 Deliverables

Must-Haves (Priority 1):

Deferred to Backlog (Priority 2):

Backlogged:


5.3 UI Summary

Location Element
Profile Contact button (icon-only: filled user = contact, outline = non-contact)
Directory Cards Contact button in top-right corner
Directory “My Contacts Only” toggle in filters
Directory /directory/contacts route for contacts-only view
Messages “New Message” button → Modal with contact search
Notifications “X added you as a contact” in-app notification + email
Settings Location privacy with “Contacts Only” option
Settings Messaging privacy with “Contacts Only” option

5.3 Completion Summary (January 2026)

What Was Implemented:

Additional Enhancements (included with 5.3):

Deferred to Backlog:


Sub-Phase 5.4: Alumni Like Me (Recommendations)

Goal: Surface Champions who share commonalities with the current user, making it easier to find people they’re likely to know or want to connect with.

Use Cases:


5.4a Match Scoring Algorithm

Scoring Factors:

Factor Points Notes
Same College +20 Exact college match
Same Major +25 Exact major match
Same Grad Year +15 Exact year match
Grad Year ±1 +10 Within 1 year
Grad Year ±2-3 +5 Within 2-3 years
Same Industry +15 Exact industry match
Shared Affinity +10 each Per matching affinity (max 3 counted = 30 pts)
Same District +10 Same metro area

Maximum Score: 100+ (uncapped, but diminishing returns)

Minimum Threshold: 15 points to appear in recommendations

Example Scoring:

User A: College of Music, Music Business '18, Entertainment Industry, Nashville, [Phi Mu, SAAC]
User B: College of Music, Music Business '17, Entertainment Industry, Nashville, [Phi Mu]

Score for B relative to A:
  +20 (same college)
  +25 (same major)
  +10 (grad year within 1)
  +15 (same industry)
  +10 (same district)
  +10 (shared affinity: Phi Mu)
  = 90 points → HIGH recommendation

5.4b Service Design

Service Class: Cp::AlumniLikeMeService

class Cp::AlumniLikeMeService
  WEIGHTS = {
    college: 20,
    major: 25,
    grad_year_exact: 15,
    grad_year_close: 10,  # ±1 year
    grad_year_near: 5,    # ±2-3 years
    industry: 15,
    affinity: 10,         # per match, max 3
    district: 10
  }.freeze

  MINIMUM_SCORE = 15
  MAX_AFFINITIES_COUNTED = 3

  def initialize(champion)
    @champion = champion
    @profile = extract_profile(champion)
  end

  # Returns array of { champion:, score:, reasons: [] }
  def recommendations(limit: 20, exclude_contacts: true)
    candidates = base_query(exclude_contacts)
    scored = candidates.map { |c| score_candidate(c) }
    scored
      .select { |r| r[:score] >= MINIMUM_SCORE }
      .sort_by { |r| -r[:score] }
      .first(limit)
  end

  # For specific use cases
  def top_matches(limit: 5)
    recommendations(limit: limit)
  end

  # For directory default view
  def directory_suggestions(limit: 12)
    recommendations(limit: limit, exclude_contacts: true)
  end

  private

  def extract_profile(champion)
    {
      college_codes: champion.alumni&.degrees&.map { |d| d.major&.college_code }&.compact&.uniq || [],
      major_codes: champion.alumni&.degrees&.map(&:major_code)&.compact&.uniq || [],
      grad_years: champion.alumni&.degrees&.map { |d| d.degree_date&.year }&.compact&.uniq || [],
      industry: champion.industry,
      district_id: champion.district_id,
      affinity_ids: champion.affinity_ids
    }
  end

  def score_candidate(candidate)
    score = 0
    reasons = []

    # College match
    if (@profile[:college_codes] & candidate_colleges(candidate)).any?
      score += WEIGHTS[:college]
      reasons << :college
    end

    # Major match
    if (@profile[:major_codes] & candidate_majors(candidate)).any?
      score += WEIGHTS[:major]
      reasons << :major
    end

    # Grad year proximity
    year_diff = min_year_difference(candidate)
    if year_diff == 0
      score += WEIGHTS[:grad_year_exact]
      reasons << :grad_year_exact
    elsif year_diff == 1
      score += WEIGHTS[:grad_year_close]
      reasons << :grad_year_close
    elsif year_diff.between?(2, 3)
      score += WEIGHTS[:grad_year_near]
      reasons << :grad_year_near
    end

    # Industry match
    if @profile[:industry].present? && @profile[:industry] == candidate.industry
      score += WEIGHTS[:industry]
      reasons << :industry
    end

    # District match
    if @profile[:district_id].present? && @profile[:district_id] == candidate.district_id
      score += WEIGHTS[:district]
      reasons << :district
    end

    # Affinity matches (capped at 3)
    shared_affinities = @profile[:affinity_ids] & candidate.affinity_ids
    affinity_count = [shared_affinities.count, MAX_AFFINITIES_COUNTED].min
    if affinity_count > 0
      score += WEIGHTS[:affinity] * affinity_count
      reasons << :affinities
    end

    { champion: candidate, score: score, reasons: reasons }
  end
end

Reusable Design:


5.4c Caching Strategy

Problem: Scoring all Champions against current user is expensive.

Solution: Pre-compute recommendations nightly + real-time fallback.

Approach 1: Cached Recommendations Table (Preferred)

# cp_champion_recommendations table
create_table :cp_champion_recommendations do |t|
  t.references :champion, null: false, foreign_key: { to_table: :cp_champions }
  t.references :recommended, null: false, foreign_key: { to_table: :cp_champions }
  t.integer :score, null: false
  t.string :reasons, array: true, default: []
  t.timestamps
end

add_index :cp_champion_recommendations, [:champion_id, :score], order: { score: :desc }
add_index :cp_champion_recommendations, [:champion_id, :recommended_id], unique: true

Nightly Job: Cp::RefreshRecommendationsJob

Real-time Fallback:


5.4d UI Integration Points

1. Directory Default View

2. Dashboard Widget (Complete)

3. Profile Page (Future)


5.4e Match Reason Display

UI Pattern: Subtle badge or label explaining why recommended

Reason Display Text
:college “Same college”
:major “Same major”
:grad_year_exact “Class of [YYYY]”
:grad_year_close “Similar class year”
:industry “Same industry”
:district “In your area”
:affinities “Shared interests”

Display Priority: Show top 2 reasons only (highest weighted)


5.4 Deliverables

Must-Haves:

Nice-to-Haves (include if time permits):

Backlogged:


5.4 Completion Summary

Implemented January 2026:

Service: app/services/cp/alumni_like_me_service.rb

UI Integration:

Activity Tracking:

Files Created/Modified:

Tests: 19 tests, 37 assertions, 0 failures

Design Decisions:


5.4 Activity Tracking

New event types:

# Add to Cp::ActivityEvent::EVENT_TYPES
recommendation_viewed    # When recommendations displayed
recommendation_clicked   # When user clicks a recommended Champion

Sub-Phase 5.5: Photo Albums ✅ COMPLETE

Goal: Create a vibrant photo experience that brings a sense of community and togetherness, showing life and action across the Champion network.

Emotional Intent: Photos should make Champions feel connected to something bigger—seeing fellow Bruins gathering, celebrating, and representing Belmont across the country.

Completion Summary (January 2026):

Use Cases:


5.5a Data Model

# cp_photo_albums table
create_table :cp_photo_albums do |t|
  t.string :title, null: false
  t.string :slug, null: false
  t.text :description
  t.references :created_by, null: false, foreign_key: { to_table: :users }  # Staff user
  t.references :event, foreign_key: { to_table: :cp_events }  # Optional: 0-1 event
  t.integer :status, default: 0, null: false  # 0: draft, 1: published
  t.datetime :published_at
  t.integer :photos_count, default: 0  # Counter cache
  t.timestamps
end

add_index :cp_photo_albums, :slug, unique: true
add_index :cp_photo_albums, :status
add_index :cp_photo_albums, [:status, :published_at]

# cp_photo_album_communities (join table for multi-community association)
create_table :cp_photo_album_communities do |t|
  t.references :photo_album, null: false, foreign_key: { to_table: :cp_photo_albums }
  t.references :community, null: false, foreign_key: { to_table: :cp_communities }
  t.timestamps
end

add_index :cp_photo_album_communities, [:photo_album_id, :community_id], unique: true, name: "idx_album_communities_unique"

# cp_photos table (separate model for photo metadata)
create_table :cp_photos do |t|
  t.references :photo_album, null: false, foreign_key: { to_table: :cp_photo_albums }
  t.string :caption
  t.string :photographer_credit
  t.date :taken_on  # Defaults to event date or upload date
  t.integer :position, null: false, default: 0  # For ordering
  t.boolean :featured, default: false
  t.datetime :featured_at
  t.timestamps
end

add_index :cp_photos, [:photo_album_id, :position]
add_index :cp_photos, [:featured, :featured_at], where: "featured = true", order: { featured_at: :desc }

Key Design Decisions:

Album Visibility Rules:


5.5b Album Types & Associations

Example Scenarios:

Album Event Communities Who Can See
“Belmont on Broadway 2026” Belmont on Broadway 2026 event NYC District, Musical Theater, CMPA Members of ANY of those communities
“Nashville Meetups Spring 2026” None Nashville District Nashville community members
“Homecoming 2025 Highlights” None None (global) ALL Champions
“Music Business Alumni Gathering” MB Alumni Mixer event Music Business Major Music Business community members

Association Logic:

class Cp::PhotoAlbum < ApplicationRecord
  belongs_to :created_by, class_name: "User"
  belongs_to :event, class_name: "Cp::Event", optional: true
  
  has_many :photo_album_communities, dependent: :destroy
  has_many :communities, through: :photo_album_communities
  
  has_many :photos, -> { order(position: :asc) }, dependent: :destroy
  
  enum status: { draft: 0, published: 1 }
  
  scope :global, -> { left_joins(:photo_album_communities).where(cp_photo_album_communities: { id: nil }) }
  scope :for_community, ->(community) { joins(:photo_album_communities).where(cp_photo_album_communities: { community_id: community.id }) }
  
  def global?
    communities.empty?
  end
  
  def visible_to?(champion)
    return true if global?
    (communities.pluck(:id) & champion.community_ids).any?
  end
end

5.5c Photo Model & Active Storage

class Cp::Photo < ApplicationRecord
  belongs_to :photo_album, counter_cache: :photos_count
  
  has_one_attached :image
  
  validates :image, presence: true
  validates :image, content_type: ['image/jpeg', 'image/png', 'image/gif', 'image/webp'],
                    size: { less_than: 10.megabytes }
  
  before_create :set_default_taken_on
  before_create :set_position
  
  scope :featured, -> { where(featured: true).order(featured_at: :desc) }
  scope :ordered, -> { order(position: :asc) }
  
  def feature!
    update!(featured: true, featured_at: Time.current)
  end
  
  def unfeature!
    update!(featured: false, featured_at: nil)
  end
  
  private
  
  def set_default_taken_on
    self.taken_on ||= photo_album.event&.start_date || Date.current
  end
  
  def set_position
    self.position = (photo_album.photos.maximum(:position) || -1) + 1
  end
end

Image Variants:

# In Cp::Photo model
def thumbnail
  image.variant(resize_to_fill: [200, 200])
end

def display
  image.variant(resize_to_limit: [1200, 800])
end

def carousel
  image.variant(resize_to_fill: [400, 300])
end

5.5d Champion Portal Routes & Views

URL Structure:

# config/routes.rb (Champion Portal)
namespace :cp do
  resources :photos, only: [:index] do
    collection do
      get :featured
    end
  end
  
  resources :albums, only: [:index, :show], param: :slug do
    resources :photos, only: [:show], module: :albums
  end
end

Routes: | Route | Purpose | |——-|———| | /photos | Index with tabs: “By Album” / “Featured” | | /photos/featured | Featured photos only (AJAX tab) | | /albums | All accessible albums | | /albums/:slug | Single album grid view | | /albums/:slug/:id | Direct link to photo (opens lightbox) |


5.5e Photos Index Page (/photos)

Layout: Tabbed interface

┌─────────────────────────────────────────────────────────────────┐
│ Photos                                                          │
├─────────────────────────────────────────────────────────────────┤
│ [By Album]  [Featured]                                          │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  "By Album" Tab:                                                │
│  ┌─────────────┐ ┌─────────────┐ ┌─────────────┐               │
│  │ [Cover]     │ │ [Cover]     │ │ [Cover]     │               │
│  │             │ │             │ │             │               │
│  │ Album Title │ │ Album Title │ │ Album Title │               │
│  │ 24 photos   │ │ 12 photos   │ │ 8 photos    │               │
│  └─────────────┘ └─────────────┘ └─────────────┘               │
│                                                                 │
│  ┌─────────────┐ ┌─────────────┐ ┌─────────────┐               │
│  │ ...         │ │ ...         │ │ ...         │               │
│  └─────────────┘ └─────────────┘ └─────────────┘               │
│                                                                 │
│  [Load more albums...]                                          │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

“By Album” Tab:

“Featured” Tab:

┌─────────────────────────────────────────────────────────────────┐
│  ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐        │
│  │ Photo  │ │ Photo  │ │ Photo  │ │ Photo  │ │ Photo  │        │
│  │        │ │        │ │        │ │        │ │        │        │
│  │ Caption│ │ Caption│ │ Caption│ │ Caption│ │ Caption│        │
│  │ 📍Event│ │ 📍Comm │ │ 📍Event│ │ 📍Comm │ │ 📍Event│        │
│  └────────┘ └────────┘ └────────┘ └────────┘ └────────┘        │
│                                                                 │
│  ┌────────┐ ┌────────┐ ┌────────┐ ...                          │
│  └────────┘ └────────┘ └────────┘                              │
│                                                                 │
│  [Load more...]                                                 │
└─────────────────────────────────────────────────────────────────┘

5.5f Album Detail Page (/albums/:slug)

┌─────────────────────────────────────────────────────────────────┐
│ ← Back to Photos                                                │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│ Belmont on Broadway 2026                                        │
│ 24 photos · April 15, 2026                                      │
│                                                                 │
│ Description text here if provided...                            │
│                                                                 │
│ 📍 Event: Belmont on Broadway 2026                              │
│ 🏷️ Communities: NYC District · Musical Theater · CMPA           │
│                                                                 │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  ┌────┐ ┌────┐ ┌────┐ ┌────┐ ┌────┐ ┌────┐                     │
│  │    │ │ ⭐ │ │    │ │    │ │ ⭐ │ │    │                     │
│  └────┘ └────┘ └────┘ └────┘ └────┘ └────┘                     │
│                                                                 │
│  ┌────┐ ┌────┐ ┌────┐ ┌────┐ ┌────┐ ┌────┐                     │
│  │    │ │    │ │ ⭐ │ │    │ │    │ │    │                     │
│  └────┘ └────┘ └────┘ └────┘ └────┘ └────┘                     │
│                                                                 │
│  [Load more photos...]                                          │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

5.5g Lightbox Component

Trigger: Click any photo thumbnail

┌─────────────────────────────────────────────────────────────────┐
│                                                     ✕           │
│                                                                 │
│  ◀                    [PHOTO]                             ▶     │
│                                                                 │
│                                                                 │
├─────────────────────────────────────────────────────────────────┤
│ "Champions celebrating after the show"                          │
│ 📷 Photo by: Jane Smith · April 15, 2026                        │
│ 📍 From: Belmont on Broadway 2026                               │
│                                                                 │
│ [⚑ Report]                                                      │
└─────────────────────────────────────────────────────────────────┘

Features:

Stimulus Controller: lightbox_controller.js


Location: Between Discussions and News sections on dashboard

┌─────────────────────────────────────────────────────────────────┐
│ 📷 From the Community                              [View All →] │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  ◀  ┌────────────┐ ┌────────────┐ ┌────────────┐  ▶            │
│     │            │ │            │ │            │               │
│     │   Photo    │ │   Photo    │ │   Photo    │               │
│     │            │ │            │ │            │               │
│     │ "Caption"  │ │ "Caption"  │ │ "Caption"  │               │
│     │ 📍 Event   │ │ 📍 Commun. │ │ 📍 Event   │               │
│     └────────────┘ └────────────┘ └────────────┘               │
│                                                                 │
│                        ● ○ ○ ○                                  │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Behavior:

Stimulus Controller: carousel_controller.js


5.5i Community Page Integration

Location: On Community show page, similar placement to dashboard

┌─────────────────────────────────────────────────────────────────┐
│ Nashville Champions                                             │
├─────────────────────────────────────────────────────────────────┤
│ [About] [Members] [Discussions] [Events] [Photos]               │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│ (If "Photos" tab or section:)                                   │
│                                                                 │
│ Featured Photos                                    [View All →] │
│ ┌────────────┐ ┌────────────┐ ┌────────────┐                   │
│ │   Photo    │ │   Photo    │ │   Photo    │                   │
│ └────────────┘ └────────────┘ └────────────┘                   │
│                                                                 │
│ Albums                                                          │
│ ┌─────────────┐ ┌─────────────┐                                │
│ │ Album Cover │ │ Album Cover │                                │
│ │ Title       │ │ Title       │                                │
│ │ 12 photos   │ │ 8 photos    │                                │
│ └─────────────┘ └─────────────┘                                │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

5.5j Event Page Integration

Location: On Event show page (for past events with photos)

┌─────────────────────────────────────────────────────────────────┐
│ Belmont on Broadway 2026                                        │
│ April 15, 2026 · NYC                                            │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│ (Event details...)                                              │
│                                                                 │
│ ─────────────────────────────────────────────────────────────   │
│                                                                 │
│ 📷 Event Photos                                   [View Album]  │
│ ┌────┐ ┌────┐ ┌────┐ ┌────┐ ┌────┐ ┌────┐                      │
│ │    │ │    │ │    │ │    │ │    │ │+18 │                      │
│ └────┘ └────┘ └────┘ └────┘ └────┘ └────┘                      │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

5.5k Admin Interface (Lookup Portal)

Location: Champion Admin > Photos (new section)

Sidebar: Between Events and Feedback

Routes:

# config/routes.rb (Lookup Portal, champions namespace)
namespace :champions do
  resources :photo_albums do
    member do
      patch :publish
      patch :unpublish
    end
    resources :photos, only: [:create, :destroy] do
      member do
        patch :feature
        patch :unfeature
        patch :move  # For reordering
      end
      collection do
        post :bulk_upload
      end
    end
  end
end

Album Index:

┌─────────────────────────────────────────────────────────────────┐
│ Photo Albums                                    [+ New Album]   │
├─────────────────────────────────────────────────────────────────┤
│ Filter: [All ▼] [Published ▼] [Community... ▼]    🔍 Search    │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│ ┌─────┬────────────────────────┬────────┬────────┬──────────┐  │
│ │Cover│ Title                  │ Photos │ Status │ Actions  │  │
│ ├─────┼────────────────────────┼────────┼────────┼──────────┤  │
│ │ 📷  │ Belmont on Broadway    │ 24     │ ● Pub  │ Edit Del │  │
│ │ 📷  │ Nashville Spring 2026  │ 12     │ ○ Draft│ Edit Del │  │
│ │ 📷  │ Homecoming 2025        │ 45     │ ● Pub  │ Edit Del │  │
│ └─────┴────────────────────────┴────────┴────────┴──────────┘  │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Album Form:

┌─────────────────────────────────────────────────────────────────┐
│ New Album / Edit Album                                          │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│ Title *         [________________________________]              │
│ Slug            [________________________________] (auto-gen)   │
│ Description     [________________________________]              │
│                 [________________________________]              │
│                                                                 │
│ Event           [Select event... ▼] (optional)                  │
│                                                                 │
│ Communities     ☐ Nashville District                            │
│                 ☐ Music Business                                │
│                 ☐ College of Music                              │
│                 ☐ CMPA                                          │
│                 (Searchable checkbox list)                      │
│                                                                 │
│ Status          ○ Draft  ● Published                            │
│                                                                 │
│ ────────────────────────────────────────────────────────────    │
│                                                                 │
│ Photos (drag to reorder)                        [+ Add Photos]  │
│                                                                 │
│ ┌────────────────────────────────────────────────────────────┐  │
│ │ ≡ [Thumb] Caption: Nashville meetup...  ⭐ Featured  🗑️   │  │
│ │ ≡ [Thumb] Caption: Group photo at...    ☆ Feature   🗑️   │  │
│ │ ≡ [Thumb] Caption:                      ☆ Feature   🗑️   │  │
│ └────────────────────────────────────────────────────────────┘  │
│                                                                 │
│ ┌──────────────────────────────────────────────────────────┐    │
│ │  Drag and drop photos here, or click to upload           │    │
│ │  (Max 10MB per photo, JPEG/PNG/WebP)                     │    │
│ └──────────────────────────────────────────────────────────┘    │
│                                                                 │
│                              [Cancel]  [Save Album]             │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Photo Edit (inline or modal):

Drag-and-Drop Upload:

Reordering:


5.5l Permission Model

Action admin portal_admin staff
View album list
Create album
Edit album
Delete album
Upload photos
Feature photos
Delete photos

Future (CL Rollout):


5.5m File Handling

Active Storage Configuration:

Variants: | Name | Dimensions | Usage | |——|————|——-| | thumbnail | 200×200 (fill) | Album grid, admin list | | carousel | 400×300 (fill) | Dashboard carousel | | display | 1200×800 (limit) | Lightbox view |

Constraints:

Validation:

# In Cp::Photo
validates :image, attached: true,
                  content_type: ['image/jpeg', 'image/png', 'image/gif', 'image/webp'],
                  size: { less_than: 10.megabytes }

validate :album_photo_limit

def album_photo_limit
  if photo_album.photos.count >= 100
    errors.add(:base, "Album has reached the maximum of 100 photos")
  end
end

5.5n Activity Tracking

New event types:

# Add to Cp::ActivityEvent::EVENT_TYPES
album_created
album_published
photos_uploaded       # metadata: { count: N }
photo_featured
photo_viewed          # When lightbox opened
photo_reported

5.5 Deliverables

Must-Haves (Priority 1):

Nice-to-Haves (Priority 2 — include if time):

Backlogged:

Additional Implemented:


5.5 UI Summary

Location Element Status
Dashboard “From the Community” carousel (featured photos) ✅ Implemented
Nav Photos accessible via dashboard or /photo-albums ✅ Implemented
/photo-albums Index listing all published albums ✅ Implemented
/photo-albums/:slug Album detail with photo grid + lightbox ✅ Implemented
Community Page Featured photos + albums for that community ⏸️ Deferred
Event Page Event photos section (if album exists) ⏸️ Deferred
Champion Admin Photo Albums section with full CRUD ✅ Implemented
Event Admin “Create Photo Album” button for past events ✅ Implemented

5.5 Files to Create

Models:

Controllers (Champion Portal):

Controllers (Admin):

Views (Champion Portal):

Views (Admin):

JavaScript:

Migrations:

Tests:


Sub-Phase 5.6: Beta Feedback System ✅ COMPLETE

Goal: Capture and manage user feedback during beta period with database persistence and admin interface.

Context: During beta testing, Champions can submit feedback directly from the portal. Feedback is stored in the database for tracking, with an admin interface in the Lookup Portal’s Champion Admin section.

Data Model:

# cp_feedbacks table
create_table :cp_feedbacks do |t|
  t.references :cp_champion, null: false, foreign_key: true
  t.string :category, null: false  # bug, feature_request, general, other
  t.text :message, null: false
  t.string :page_url
  t.string :user_agent
  t.string :status, default: 'new'  # new, read, in_progress, resolved, closed
  t.text :internal_notes
  t.datetime :read_at
  t.references :read_by, foreign_key: { to_table: :users }
  t.timestamps
end

Deliverables:

UI Patterns:

Permission Model: | Action | portal_admin | can_support_respond | staff | |——–|————–|———————|——-| | View feedback list | ✅ | ✅ | ❌ | | View feedback detail | ✅ | ✅ | ❌ | | Update status/notes | ✅ | ✅ | ❌ |

Files Created:

Files Modified:


4. Definition of Success

Functional Acceptance Criteria

Quality Criteria

MVP Readiness


5. Tests to Create

Beta Feedback Tests ✅ COMPLETE

Model Tests

Controller Tests

Integration Tests


6. Documentation Updates

After completing Phase 5:


Questions to Resolve

Question Status Notes
Faculty/Staff verification process? TBD Require BUID + @belmont.edu eligibility hint; staff approval always required
Almost Alumni verification? TBD Use @bruins.belmont.edu eligibility hint; staff approval always required
Almost Alumni → Alumni conversion trigger? TBD Hook into degree import pipeline; optional manual recheck
Mutual friending vs one-way favorites? ✅ One-way Favorites, not mutual friend requests
Album per event or multiple? TBD Single “Event Photos” album probably sufficient
Photo moderation needed? TBD Flag inappropriate photos?
Lookup Portal Almost Alumni workflow? TBD Need to formalize process for adding students to Alumni table

Document Purpose
../../JOBS-TO-BE-DONE.md User needs context
../../features/01-AUTHENTICATION.md Auth for non-alumni
../phase-1/1.5-staff-verification.md Verification patterns (Lookup Portal: /champions/verification)

🚀 Phase 5 completion = MVP SOFT LAUNCH READY

After this phase, the Champion Portal is ready for initial public users.