alumni_lookup

Phase 9.5: Community Welcome Content

Canonical sources: Portal philosophy, posture, and language live in /docs/planning/champion-portal/source/README.md.
Use these sources (including CHRIST_CENTERED__IDENTITY_STATEMENT.md) when writing specs or user-facing copy. Prefer quoting/paraphrasing over inventing new language.

Status: ✅ Complete
Estimated Effort: 3-4 days
Dependencies: Phase 9.1 (Community Seed Data)
Related Docs: 9.1-community-seed-data.md, DESIGN-GUIDELINES.md


Overview

When a Champion joins a community, they should receive a warm, informative welcome that explains what the community is about and suggests initial actions. This welcome content appears as an “About” panel on the community landing page and can be dismissed after a period or manually hidden.

Why Not News?

We considered using the existing News system for welcome content but determined it’s the wrong fit:

Aspect News Community About Panel
Purpose Time-sensitive announcements Static welcome/orientation
Lifecycle Publishes → ages → archives Persistent until dismissed
Visibility Appears in feeds, multiple places Shows once on landing page
Repetition Would appear in every community’s news One welcome per community
Content type “What’s happening” “What is this place”

Decision: Welcome content lives as a dedicated “About” panel on the community landing page, extending the existing display_description pattern.


Architecture

Storage

Add ActionText rich text to communities:

# app/models/cp/community.rb
has_rich_text :welcome_content

No migration needed for ActionText—it uses the existing action_text_rich_texts table.

Welcome Pack Templates

Store templates in YAML, organized by community type and (for affinities) by category:

# config/welcome_packs.yml
district:
  - title: "Welcome to {{district_name}}!"
    body: |
      You've found your local Bruin community! Here in {{district_name}}, 
      Champions gather for coffee meetups, networking events, and watch parties.
      
      **A few ideas to get started:**
      - Browse the directory to see who else is nearby
      - Check upcoming events in the area
      - Introduce yourself in the discussion board
      
      We're glad you're here. 🎉
      
  - title: "{{district_name}} Champions"
    body: |
      Welcome to the {{district_name}} chapter of Alumni Champions!
      
      This is your home base for connecting with fellow Bruins in the area.
      Whether you're new to town or have been here for years, there's always
      room at the table.
      
      **Quick actions:**
      - Say hello in Discussions
      - Find Champions near you
      - See what events are coming up

college:
  - title: "Welcome, {{college_name}} Alum!"
    body: |
      You're joining a community of fellow {{college_name}} graduates.
      
      This space is for staying connected to your college, sharing updates,
      and helping current students navigate their Belmont journey.
      
      **Get started:**
      - Connect with classmates in the directory
      - Share career updates or advice
      - Keep up with college news

major:
  - title: "{{major_name}} Alumni"
    body: |
      Welcome to the {{major_name}} alumni community!
      
      Whether you're using your degree every day or took a different path,
      you share something with everyone here—the experience of studying
      {{major_name}} at Belmont.
      
      **Jump in:**
      - See who else studied {{major_name}}
      - Share job opportunities or career advice
      - Reconnect with old classmates

industry:
  - title: "{{industry_name}} Professionals"
    body: |
      You've joined a community of Belmont alumni working in {{industry_name}}.
      
      This is a great place to network, share opportunities, and support
      fellow Bruins in your field.
      
      **Get started:**
      - Browse professionals in your industry
      - Share job leads or advice
      - Ask questions and offer expertise

affinity:
  # Organized by affinity category (from affinities.category column)
  Athletics:
    - title: "Welcome, Fellow Bruin Athlete!"
      body: |
        You're part of the {{affinity_name}} alumni community.
        
        Whether you played, managed, trained, or cheered, your time as a
        Bruin athlete shaped who you are. This is your space to stay connected.
        
        **Get involved:**
        - Reconnect with teammates
        - Follow current team news
        - Attend alumni games and events
        
  Greek Life:
    - title: "Welcome, {{affinity_name}}!"
      body: |
        You've joined your chapter's alumni community on the Champion Portal.
        
        Greek life creates bonds that last a lifetime. Use this space to
        stay connected with your brothers or sisters.
        
        **Get started:**
        - Find chapter alumni in the directory
        - Share updates and milestones
        - Support current members
        
  Campus Life:
    - title: "{{affinity_name}} Alumni"
      body: |
        Welcome to the {{affinity_name}} alumni community!
        
        Your involvement in {{affinity_name}} was part of what made your
        Belmont experience unique. Stay connected with others who shared it.
        
        **Jump in:**
        - Reconnect with fellow members
        - Share memories and updates
        - Support current students
        
  Instrumental Ensembles:
    - title: "{{affinity_name}} Alumni"
      body: |
        Welcome to the {{affinity_name}} alumni network!
        
        Music brought you together at Belmont. This community keeps that
        connection alive after graduation.
        
        **Get involved:**
        - Find fellow musicians
        - Share performance updates
        - Stay connected to the ensemble
        
  Vocal Ensembles:
    - title: "{{affinity_name}} Alumni"
      body: |
        Welcome to the {{affinity_name}} community!
        
        Your voice was part of something special at Belmont. Stay connected
        with the singers who shared that stage with you.
        
        **Get started:**
        - Reconnect with fellow singers
        - Share musical updates
        - Support current members
        
  Spiritual Life:
    - title: "{{affinity_name}} Community"
      body: |
        Welcome to the {{affinity_name}} alumni community.
        
        Faith and service were central to your Belmont experience. This
        space helps you stay connected to others who shared that journey.
        
        **Get involved:**
        - Connect with fellow alumni
        - Share how your faith journey continues
        - Support current students
        
  Geographic:
    - title: "{{affinity_name}} Alumni"
      body: |
        Welcome to the {{affinity_name}} community!
        
        You share a connection through {{affinity_name}}. This is your
        space to stay in touch.
        
        **Get started:**
        - Find fellow alumni
        - Share updates
        - Stay connected
        
  Post Graduation:
    - title: "{{affinity_name}} Alumni"
      body: |
        Welcome to the {{affinity_name}} community!
        
        Your post-graduation involvement with Belmont connects you with
        others who share that experience.
        
        **Get started:**
        - Connect with fellow members
        - Share experiences
        - Stay involved
        
  default:
    - title: "Welcome to {{affinity_name}}!"
      body: |
        You've joined the {{affinity_name}} alumni community.
        
        This is your space to connect with others who share your
        affiliation with {{affinity_name}}.
        
        **Get started:**
        - Browse the member directory
        - Join the conversation
        - Stay connected

custom:
  - title: "Welcome to {{community_name}}!"
    body: |
      You've joined a special community on the Champion Portal.
      
      This space was created for a specific purpose. Look around,
      get to know the other members, and dive in!
      
      **Get started:**
      - See who else is here
      - Check the discussion board
      - Introduce yourself

Welcome Content Generator

# app/services/cp/welcome_content_generator.rb
module Cp
  class WelcomeContentGenerator
    PACKS = YAML.load_file(Rails.root.join("config/welcome_packs.yml"))
    
    def initialize(community)
      @community = community
    end
    
    def generate
      pack = select_pack
      return nil unless pack
      
      interpolate(pack)
    end
    
    private
    
    def select_pack
      packs = packs_for_type
      return nil if packs.blank?
      
      # Rotate through packs based on community ID for variety
      packs[@community.id % packs.length]
    end
    
    def packs_for_type
      case @community.community_type
      when "district", "college", "major", "industry", "custom"
        PACKS[@community.community_type]
      when "affinity"
        affinity_packs
      else
        []
      end
    end
    
    def affinity_packs
      # Use the affinity's category to select appropriate welcome pack
      category = @community.affinity&.category
      
      if category.present? && PACKS.dig("affinity", category).present?
        PACKS.dig("affinity", category)
      else
        PACKS.dig("affinity", "default") || []
      end
    end
    
    def interpolate(pack)
      title = interpolate_string(pack["title"])
      body = interpolate_string(pack["body"])
      
      { title: title, body: body }
    end
    
    def interpolate_string(str)
      str.gsub("{{district_name}}", @community.name)
         .gsub("{{college_name}}", @community.name)
         .gsub("{{major_name}}", @community.name)
         .gsub("{{industry_name}}", @community.name)
         .gsub("{{affinity_name}}", @community.name)
         .gsub("{{community_name}}", @community.name)
    end
  end
end

Community Model Updates

# app/models/cp/community.rb

# Add ActionText
has_rich_text :welcome_content

# Generate welcome content on creation
after_create :generate_welcome_content

private

def generate_welcome_content
  return if welcome_content.present?
  
  generator = Cp::WelcomeContentGenerator.new(self)
  result = generator.generate
  
  return unless result
  
  # Build HTML from title + body
  html = <<~HTML
    <h3>#{result[:title]}</h3>
    #{result[:body].gsub(/\n\n/, '</p><p>').gsub(/\n/, '<br>')}
  HTML
  
  update(welcome_content: html)
end

Champion Dismissal Tracking

Champions can dismiss welcome content either:

  1. Manually — Click “Got it” or similar dismiss button
  2. Automatically — After viewing the community page 3+ times

Schema

# Migration: add_dismissed_welcome_to_cp_champion_communities
add_column :cp_champion_communities, :welcome_dismissed_at, :datetime
add_column :cp_champion_communities, :community_view_count, :integer, default: 0

Logic

# app/models/cp/champion_community.rb
def welcome_dismissed?
  welcome_dismissed_at.present? || community_view_count >= 10
end

def dismiss_welcome!
  update(welcome_dismissed_at: Time.current)
end

def increment_view_count!
  increment!(:community_view_count)
end

UI Display

Community Show Page

Add welcome panel to the community landing page, displayed below the header and above the tabs/content:

<%# app/views/cp/communities/show.html.erb %>

<%# ... header section ... %>

<%# Welcome Content Panel %>
<% if show_welcome_panel?(@community, @membership) %>
  <div class="mx-4 sm:mx-0 mb-6" id="welcome-panel">
    <div class="bg-gradient-to-br from-blue-50 to-indigo-50 rounded-xl border border-blue-100 p-6">
      <div class="flex items-start justify-between gap-4">
        <div class="flex-1 prose prose-sm max-w-none">
          <%= @community.welcome_content %>
        </div>
        
        <button type="button" 
                class="flex-shrink-0 text-gray-400 hover:text-gray-600"
                data-controller="dismiss-welcome"
                data-dismiss-welcome-url-value="<%= dismiss_welcome_cp_community_path(@community) %>"
                data-dismiss-welcome-panel-value="welcome-panel"
                aria-label="Dismiss welcome message">
          <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
          </svg>
        </button>
      </div>
      
      <div class="mt-4 pt-4 border-t border-blue-100">
        <button type="button"
                class="text-sm text-blue-600 hover:text-blue-800 font-medium"
                data-controller="dismiss-welcome"
                data-dismiss-welcome-url-value="<%= dismiss_welcome_cp_community_path(@community) %>"
                data-dismiss-welcome-panel-value="welcome-panel">
          Got it, thanks!
        </button>
      </div>
    </div>
  </div>
<% end %>

<%# ... rest of page ... %>

Helper Method

# app/helpers/cp/communities_helper.rb
def show_welcome_panel?(community, membership)
  return false unless community.welcome_content.present?
  return false unless membership.present?
  return false if membership.welcome_dismissed?
  
  true
end

Stimulus Controller

// app/javascript/controllers/dismiss_welcome_controller.js
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static values = { url: String, panel: String }
  
  connect() {
    // Nothing needed on connect
  }
  
  dismiss() {
    // Hide panel immediately for responsiveness
    const panel = document.getElementById(this.panelValue)
    if (panel) {
      panel.style.opacity = "0"
      panel.style.transition = "opacity 0.3s ease-out"
      setTimeout(() => panel.remove(), 300)
    }
    
    // Persist to server
    fetch(this.urlValue, {
      method: "POST",
      headers: {
        "X-CSRF-Token": document.querySelector("[name='csrf-token']").content,
        "Accept": "application/json"
      }
    })
  }
}

Controller Endpoint

# app/controllers/cp/communities_controller.rb
def dismiss_welcome
  membership = @community.champion_communities.find_by(champion: current_cp_champion)
  
  if membership
    membership.dismiss_welcome!
    head :ok
  else
    head :not_found
  end
end

Route

# config/routes.rb (within cp namespace)
resources :communities do
  post :dismiss_welcome, on: :member
end

View Count Tracking

Increment view count each time a champion visits their community page:

# app/controllers/cp/communities_controller.rb
def show
  @community = Cp::Community.find(params[:id])
  @membership = @community.champion_communities.find_by(champion: current_cp_champion)
  
  # Track view for welcome dismissal
  @membership&.increment_view_count! unless @membership&.welcome_dismissed?
  
  # ... rest of show action ...
end

Admin Interface

Community Edit

Add field to community edit form for staff to customize welcome content:

<%# app/views/champions/communities/edit.html.erb %>

<div class="mb-6">
  <label class="block text-sm font-medium text-gray-700 mb-2">
    Welcome Content
    <span class="text-gray-400 font-normal">(shown to new members)</span>
  </label>
  
  <%= form.rich_text_area :welcome_content,
      class: "block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500" %>
  
  <p class="mt-2 text-sm text-gray-500">
    This content appears on the community page for new members. 
    Leave blank to use the auto-generated welcome message.
  </p>
  
  <% if @community.welcome_content.blank? %>
    <button type="button" 
            class="mt-2 text-sm text-blue-600 hover:text-blue-800"
            data-action="click->community-form#regenerateWelcome">
      Generate default welcome content
    </button>
  <% end %>
</div>

Regenerate Welcome Content

Add rake task for bulk regeneration:

# lib/tasks/welcome_content.rake
namespace :welcome_content do
  desc "Regenerate welcome content for all communities missing it"
  task generate_missing: :environment do
    count = 0
    
    Cp::Community.active.find_each do |community|
      next if community.welcome_content.present?
      
      generator = Cp::WelcomeContentGenerator.new(community)
      result = generator.generate
      
      next unless result
      
      html = <<~HTML
        <h3>#{result[:title]}</h3>
        #{result[:body].gsub(/\n\n/, '</p><p>').gsub(/\n/, '<br>')}
      HTML
      
      community.update(welcome_content: html)
      count += 1
      print "."
    end
    
    puts "\nGenerated welcome content for #{count} communities"
  end
  
  desc "Preview welcome content for a community type"
  task :preview, [:type] => :environment do |t, args|
    type = args[:type] || "district"
    
    packs = YAML.load_file(Rails.root.join("config/welcome_packs.yml"))
    
    if type == "affinity"
      packs["affinity"].each do |category, templates|
        puts "\n=== Affinity: #{category} ==="
        templates.each_with_index do |pack, i|
          puts "\n--- Template #{i + 1} ---"
          puts "Title: #{pack['title']}"
          puts pack['body']
        end
      end
    else
      templates = packs[type]
      templates.each_with_index do |pack, i|
        puts "\n--- Template #{i + 1} ---"
        puts "Title: #{pack['title']}"
        puts pack['body']
      end
    end
  end
end

Testing

Model Tests

# test/models/cp/community_welcome_test.rb
require "test_helper"

class Cp::CommunityWelcomeTest < ActiveSupport::TestCase
  test "generates welcome content on creation for district" do
    community = Cp::Community.create!(
      name: "Nashville",
      community_type: :district,
      active: true
    )
    
    assert community.welcome_content.present?
    assert_includes community.welcome_content.to_plain_text, "Nashville"
  end
  
  test "generates welcome content for affinity using category" do
    affinity = affinities(:phi_mu) # Has category: "Greek Life"
    community = Cp::Community.create!(
      name: affinity.name,
      community_type: :affinity,
      affinity: affinity,
      active: true
    )
    
    assert community.welcome_content.present?
    # Should use Greek Life template, not default
    assert_includes community.welcome_content.to_plain_text, "chapter"
  end
  
  test "uses default affinity template when category not found" do
    affinity = Affinity.create!(name: "Unknown Group", category: "Nonexistent")
    community = Cp::Community.create!(
      name: affinity.name,
      community_type: :affinity,
      affinity: affinity,
      active: true
    )
    
    assert community.welcome_content.present?
  end
  
  test "rotates through packs based on community ID" do
    # Create multiple communities to test rotation
    communities = 3.times.map do |i|
      Cp::Community.create!(
        name: "City #{i}",
        community_type: :district,
        active: true
      )
    end
    
    # At least 2 should have different content (if multiple packs exist)
    contents = communities.map { |c| c.welcome_content.to_plain_text }
    # This test assumes district has multiple packs
  end
end

Champion Community Tests

# test/models/cp/champion_community_dismissal_test.rb
require "test_helper"

class Cp::ChampionCommunityDismissalTest < ActiveSupport::TestCase
  setup do
    @membership = cp_champion_communities(:sarah_nashville)
  end
  
  test "welcome not dismissed by default" do
    refute @membership.welcome_dismissed?
  end
  
  test "manual dismissal works" do
    @membership.dismiss_welcome!
    
    assert @membership.welcome_dismissed?
    assert @membership.welcome_dismissed_at.present?
  end
  
  test "auto-dismisses after 3 views" do
    refute @membership.welcome_dismissed?
    
    3.times { @membership.increment_view_count! }
    
    assert @membership.welcome_dismissed?
  end
  
  test "view count increments" do
    assert_equal 0, @membership.community_view_count
    
    @membership.increment_view_count!
    
    assert_equal 1, @membership.community_view_count
  end
end

Controller Tests

# test/controllers/cp/communities_dismiss_welcome_test.rb
require "test_helper"

class Cp::CommunitiesDismissWelcomeTest < ActionDispatch::IntegrationTest
  setup do
    @champion = cp_champions(:sarah_champion)
    @community = cp_communities(:nashville)
    sign_in @champion
  end
  
  test "dismisses welcome content" do
    membership = @community.champion_communities.find_by(champion: @champion)
    assert_not membership.welcome_dismissed?
    
    post dismiss_welcome_cp_community_url(@community)
    
    assert_response :ok
    membership.reload
    assert membership.welcome_dismissed?
  end
  
  test "returns 404 for non-member" do
    @community = cp_communities(:atlanta) # Champion not a member
    
    post dismiss_welcome_cp_community_url(@community)
    
    assert_response :not_found
  end
end

Service Tests

# test/services/cp/welcome_content_generator_test.rb
require "test_helper"

class Cp::WelcomeContentGeneratorTest < ActiveSupport::TestCase
  test "generates content for district community" do
    community = Cp::Community.new(
      id: 1,
      name: "Nashville",
      community_type: :district
    )
    
    generator = Cp::WelcomeContentGenerator.new(community)
    result = generator.generate
    
    assert result.present?
    assert result[:title].present?
    assert result[:body].present?
    assert_includes result[:title], "Nashville"
  end
  
  test "generates content for affinity with category" do
    affinity = affinities(:phi_mu)
    community = Cp::Community.new(
      id: 1,
      name: affinity.name,
      community_type: :affinity,
      affinity: affinity
    )
    
    generator = Cp::WelcomeContentGenerator.new(community)
    result = generator.generate
    
    assert result.present?
    assert_includes result[:body], "chapter" # Greek Life template
  end
  
  test "interpolates all variable types" do
    %w[district college major industry].each do |type|
      community = Cp::Community.new(
        id: 1,
        name: "Test Name",
        community_type: type
      )
      
      generator = Cp::WelcomeContentGenerator.new(community)
      result = generator.generate
      
      refute_includes result[:title], "{{"
      refute_includes result[:body], "{{"
    end
  end
end

Deliverables

Files to Create

File Purpose
config/welcome_packs.yml Welcome content templates by community type
app/services/cp/welcome_content_generator.rb Service to generate welcome content
app/javascript/controllers/dismiss_welcome_controller.js Stimulus controller for dismissal
lib/tasks/welcome_content.rake Rake tasks for content management
db/migrate/xxx_add_welcome_tracking_to_champion_communities.rb Migration for dismissal tracking
test/models/cp/community_welcome_test.rb Model tests
test/models/cp/champion_community_dismissal_test.rb Dismissal tracking tests
test/services/cp/welcome_content_generator_test.rb Service tests
test/controllers/cp/communities_dismiss_welcome_test.rb Controller tests

Files to Modify

File Changes
app/models/cp/community.rb Add has_rich_text :welcome_content, after_create callback
app/models/cp/champion_community.rb Add dismissal methods
app/controllers/cp/communities_controller.rb Add dismiss_welcome action, view tracking
app/views/cp/communities/show.html.erb Add welcome panel UI
app/helpers/cp/communities_helper.rb Add show_welcome_panel? helper
config/routes.rb Add dismiss_welcome route
app/views/champions/communities/edit.html.erb Add welcome content editor

Acceptance Criteria


Design Notes

Visual Style

The welcome panel should feel:

Content Tone

Per LANGUAGE_STYLE_GUIDE.md:


Future Enhancements

Not in scope for 9.5, but noted for future:

  1. A/B testing — Test different welcome packs for effectiveness
  2. Personalization — Include champion’s name or graduation year
  3. Analytics — Track dismissal rates to improve content
  4. Seasonal variants — Different welcome content for different times of year
  5. Video welcome — Support embedded video in welcome content