Canonical sources: Portal philosophy, posture, and language live in
/docs/planning/champion-portal/source/README.md.
Use these sources (includingCHRIST_CENTERED__IDENTITY_STATEMENT.md) when writing specs or user-facing copy. Prefer quoting/paraphrasing over inventing new language.
Champion Portal Development Sub-Phase 9.1
Status: ✅ Complete (February 2026)
Estimated Effort: 1 week
Prerequisites: Phase 5 CompleteRelated Documents:
- README.md — Phase 9 Overview
- ../../development/DESIGN-GUIDELINES.md — Visual standards
- ../../development/LANGUAGE_STYLE_GUIDE.md — Voice & tone
- ../phase-1/1.12-community-foundation.md — Community infrastructure
Implemented: February 2026
%w[help_find_you confirm_education location champion_role profession photo bio affinities join_communities]for_wizard scope orders by type priority (district → college → major → affinity → industry), limits to 5app/controllers/cp/profile_wizard_controller.rb — STEPS array, load_step_data, process_step_updateapp/models/cp/community_suggestion.rb — for_wizard scope with integer enum valuesapp/models/cp/champion.rb — should_see_community_step?, pending_community_suggestions methodsapp/services/cp/community_detection_service.rb — Fixed college_desc → college_nameapp/views/cp/profile_wizard/_step_join_communities.html.erb — Main step partialapp/views/cp/profile_wizard/_suggestion_card.html.erb — Individual card partial with Turbo Framecollege_nameNone — all acceptance criteria met
Sub-Phase 9.1 adds a “Join Communities” step to the profile wizard, ensuring Champions enter the portal as members of relevant communities rather than passive observers.
Champions who join communities during onboarding are 3x more likely to return within 7 days.
The current wizard ends at affinities — Champions land on a dashboard showing community suggestions they could pursue. But “could” becomes “won’t” for most users. Moving community joining into the wizard flow captures attention when engagement intent is highest.
Location: app/controllers/cp/profile_wizard_controller.rb
STEPS = %w[help_find_you confirm_education location champion_role profession photo bio affinities]
REQUIRED_STEPS = %w[location]
CONDITIONAL_STEPS = %w[help_find_you confirm_education]
Flow Diagram:
[Start] → help_find_you (conditional) → confirm_education (conditional) → location (required)
→ champion_role → profession → photo → bio → affinities → [Dashboard]
Model: Cp::CommunitySuggestion
How suggestions are generated:
Current touchpoints:
/communities/discoverGap: Suggestions generate after wizard but aren’t surfaced during wizard.
# app/models/cp/community_suggestion.rb
class Cp::CommunitySuggestion < ApplicationRecord
belongs_to :champion, class_name: 'Cp::Champion'
belongs_to :community, class_name: 'Cp::Community'
enum :status, { pending: 0, declined: 1 }
scope :active, -> { pending }
scope :by_type, ->(type) { joins(:community).where(communities: { community_type: type }) }
def accept!
Cp::ChampionCommunity.create!(champion: champion, community: community)
destroy!
end
def decline!
update!(status: :declined, responded_at: Time.current)
end
end
Key insight: The accept! and decline! methods provide the exact pattern needed for wizard integration.
[Start] → help_find_you → confirm_education → location → champion_role
→ profession → photo → bio → affinities → join_communities → [Dashboard]
join_communitiesBehavior:
STEPS = %w[help_find_you confirm_education location champion_role profession photo bio affinities join_communities]
REQUIRED_STEPS = %w[location]
CONDITIONAL_STEPS = %w[help_find_you confirm_education]
# join_communities is optional — Champions can skip
| Step | User Action | System Response |
|---|---|---|
| 1 | Completes affinities step | System generates community suggestions |
| 2 | Arrives at join_communities | Sees 3-5 suggested communities |
| 3 | Clicks “Join” on Nashville Champions | Creates ChampionCommunity, updates UI |
| 4 | Clicks “Join” on Music Business | Creates ChampionCommunity, updates UI |
| 5 | Clicks “Not Now” on Phi Mu | Marks suggestion as declined |
| 6 | Clicks “Continue to Dashboard” | Redirects to dashboard with 2 communities |
| Step | User Action | System Response |
|---|---|---|
| 1 | Completes affinities step | System generates suggestions (location + intended major only) |
| 2 | Arrives at join_communities | Sees district + 1-2 college/major communities |
| 3 | Joins district community | Creates ChampionCommunity |
| 4 | Clicks “Continue to Dashboard” | Redirects with 1 community |
| Step | User Action | System Response |
|---|---|---|
| 1 | Completes affinities step | System generates suggestions |
| 2 | Arrives at join_communities | Sees suggestions |
| 3 | Clicks “Skip for Now” | Suggestions remain pending |
| 4 | Redirects to dashboard | Dashboard shows suggestions widget |
| Step | User Action | System Response |
|---|---|---|
| 1 | Completes affinities (none selected) | System has limited data |
| 2 | Arrives at join_communities | Sees only district community (from location) |
| 3 | Joins or skips | Proceeds to dashboard |
| ID | Requirement | Priority |
|---|---|---|
| 9.1.1 | join_communities step appears after affinities |
Must |
| 9.1.2 | Community suggestions display with name, type icon, and member count | Must |
| 9.1.3 | “Join” action creates ChampionCommunity without page reload |
Must |
| 9.1.4 | “Not Now” action marks suggestion as declined | Must |
| 9.1.5 | “Continue to Dashboard” works after any number of joins/declines | Must |
| 9.1.6 | Step can be skipped entirely (“Skip for Now”) | Must |
| 9.1.7 | Already-joined communities don’t appear as suggestions | Must |
| 9.1.8 | Progress indicator shows correct step (e.g., “Step 9 of 9”) | Should |
| 9.1.9 | Step only appears for verified Champions | Should |
| 9.1.10 | Suggestions ordered by relevance (district first, then education, then affinities) | Should |
| Scenario | Expected Behavior |
|---|---|
| No suggestions available | Show message: “We’ll suggest communities as you explore!” + Continue button |
| All suggestions already joined | Skip step automatically |
| Champion has 10+ suggestions | Show top 5 with “See more communities” link |
| Network error on join | Show error toast, keep button enabled to retry |
Follow DESIGN-GUIDELINES.md:
| Element | Guideline Reference |
|---|---|
| Page background | §3.1 — Warm neutrals, not pure white |
| Community cards | §5.1 — Cards with hover shadow, rounded corners |
| Join button | §5.2 — Primary (Belmont Blue) |
| Not Now button | §5.2 — Ghost/secondary style |
| Icons | §3.3 — Heroicons outline style |
| Empty state | §5.5 — Encouraging, action-oriented |
┌─────────────────────────────────────────────────────────────────────────┐
│ 🧭 Step 9 of 9 │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ [progress bar: ████████████████████████████████████░] │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ ╭─────────────────────────────────────────────────────────────────────╮ │
│ │ │ │
│ │ 🎯 Join Your Communities │ │
│ │ │ │
│ │ Based on your profile, here are some communities where │ │
│ │ you'll find fellow Bruins: │ │
│ │ │ │
│ ╰─────────────────────────────────────────────────────────────────────╯ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 📍 Nashville Champions │ │
│ │ 23 Champions in your city │ │
│ │ [Join] [Not Now] │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 🎵 Music Business │ │
│ │ Connect with 45 Music Business alumni │ │
│ │ [Join] [Not Now] │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 🏛️ Mike Curb College of Entertainment │ │
│ │ Your college community with 78 members │ │
│ │ [Join] [Not Now] │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ │
│ │
│ You can explore more communities anytime from your dashboard. │
│ │
│ [Skip for Now] [Continue to Dashboard →] │
│ │
└─────────────────────────────────────────────────────────────────────────┘
| State | Visual Treatment |
|---|---|
| Default | White card, visible Join/Not Now buttons |
| Joined | Green left border, “Joined ✓” badge, no buttons |
| Declined | Removed from view (or grayed with “Skipped”) |
| Loading | Join button shows spinner |
| Element | Copy | Guideline |
|---|---|---|
| Step title | “Join Your Communities” | Active, welcoming |
| Subtitle | “Based on your profile, here are some communities where you’ll find fellow Bruins:” | Personal, warm |
| Join button | “Join” | Clear action |
| Decline button | “Not Now” | Not “Decline” or “Skip” — implies they can join later |
| Skip link | “Skip for Now” | Same non-permanence tone |
| Continue button | “Continue to Dashboard →” | Active, forward movement |
| Empty state | “We’ll suggest communities as you explore!” | Encouraging, not blaming |
The existing models support this feature:
Cp::CommunitySuggestion — tracks suggestions with accept/declineCp::ChampionCommunity — created on joinCp::CommunitySuggestion:
# Scope to get suggestions suitable for wizard
scope :for_wizard, -> {
pending
.includes(:community)
.where(communities: { active: true, hidden: false })
.order(Arel.sql("CASE cp_communities.community_type
WHEN 'district' THEN 1
WHEN 'college' THEN 2
WHEN 'major' THEN 3
WHEN 'affinity' THEN 4
WHEN 'industry' THEN 5
ELSE 6 END"))
.limit(5)
}
Cp::Champion:
# Check if Champion should see join_communities step
def should_see_community_step?
verified? && pending_community_suggestions.any?
end
def pending_community_suggestions
community_suggestions.for_wizard
end
Currently, suggestions generate via:
Change: Ensure suggestions generate before join_communities step loads:
# In profile_wizard_controller.rb
def load_step_data
case current_step
when 'join_communities'
generate_community_suggestions_if_needed
@suggestions = current_cp_champion.pending_community_suggestions
# ... other steps
end
end
Files to modify:
app/controllers/cp/profile_wizard_controller.rb
join_communities to STEPSload_step_data case for new stepprocess_step_update case (minimal — joins are AJAX)Files to create:
app/views/cp/profile_wizard/_join_communities.html.erbFiles to create:
app/views/cp/profile_wizard/_community_suggestion_card.html.erbFeatures:
Files to modify:
app/controllers/cp/community_suggestions_controller.rb
accept and decline actions support Turbo StreamFiles to create:
app/views/cp/community_suggestions/accept.turbo_stream.erbapp/views/cp/community_suggestions/decline.turbo_stream.erbBehavior:
Files to modify:
app/controllers/cp/profile_wizard_controller.rb
Behavior:
File: test/controllers/cp/profile_wizard_controller_test.rb
# New tests to add:
test "join_communities step loads pending suggestions" do
sign_in @verified_champion
get cp_profile_wizard_url(step: 'join_communities')
assert_response :success
assert assigns(:suggestions).present?
end
test "join_communities step skipped when no suggestions" do
sign_in @champion_with_no_suggestions
get cp_profile_wizard_url(step: 'join_communities')
assert_redirected_to cp_dashboard_url
end
test "join_communities step requires verified champion" do
sign_in @unverified_champion
get cp_profile_wizard_url(step: 'join_communities')
# Should skip to next valid step or dashboard
end
test "skip action preserves pending suggestions" do
sign_in @verified_champion
post cp_profile_wizard_url(step: 'join_communities'), params: { skip: true }
assert @verified_champion.community_suggestions.pending.any?
assert_redirected_to cp_dashboard_url
end
File: test/models/cp/community_suggestion_test.rb
# New tests to add:
test "for_wizard scope orders by community type priority" do
suggestions = @champion.community_suggestions.for_wizard
types = suggestions.map { |s| s.community.community_type }
assert_equal types, types.sort_by { |t|
%w[district college major affinity industry custom].index(t)
}
end
test "for_wizard scope limits to 5 suggestions" do
# Create 10 suggestions
10.times { create(:community_suggestion, champion: @champion) }
assert_equal 5, @champion.community_suggestions.for_wizard.count
end
File: test/system/cp/profile_wizard_test.rb
test "completing wizard with community joins" do
sign_in @new_champion
# Navigate through wizard to join_communities
visit cp_profile_wizard_url
# ... complete steps ...
assert_selector "h2", text: "Join Your Communities"
# Join a community
within first(".community-suggestion-card") do
click_button "Join"
end
assert_selector ".community-joined-badge", text: "Joined"
click_button "Continue to Dashboard"
assert_current_path cp_dashboard_path
assert @new_champion.reload.communities.any?
end
See Completion Summary at top of document.
None — all acceptance criteria met.
Document created: January 2026
Completed: February 2026