TRUG (Trójmiasto Ruby User Group) website - a modern Rails 8 application for managing and displaying meetup information. Migrated from Middleman static site generator.
- Framework: Rails 8.1.2
- Ruby: 4.0.0
- Database: SQLite (with separate databases for cache and queue)
- CSS: Pure CSS with CSS custom properties (no preprocessors)
- JavaScript: Vanilla ES6 with Importmaps, Turbo Rails
- Testing: Minitest + Capybara + Selenium
- Deployment: Kamal
- Authentication: GitHub OAuth (Rails 8 built-in)
# Install dependencies
bin/setup
# Start development server
bin/rails server
# Run tests
bin/rails testtrug-rails/
├── app/
│ ├── assets/
│ │ ├── images/ # Logo, icons, backgrounds
│ │ └── stylesheets/ # Modular CSS files
│ ├── controllers/
│ │ ├── admin/ # Admin controllers (layout: admin)
│ │ ├── application_controller.rb
│ │ ├── attendances_controller.rb
│ │ ├── github_sessions_controller.rb
│ │ ├── pages_controller.rb
│ │ └── video_thumbnails_controller.rb
│ ├── javascript/ # Importmap entry point
│ ├── models/
│ │ ├── meetup.rb
│ │ ├── talk.rb
│ │ ├── user.rb
│ │ ├── attendance.rb
│ │ └── session.rb
│ └── views/
│ ├── layouts/ # application.html.erb, admin.html.erb
│ ├── pages/ # home, archive
│ └── admin/ # dashboard, meetups, talks
├── config/
│ ├── routes.rb
│ ├── importmap.rb # JavaScript imports
│ ├── deploy.yml # Kamal deployment config
│ └── database.yml
├── db/
│ ├── migrate/
│ └── schema.rb
├── test/
│ ├── system/ # E2E tests with Capybara
│ ├── models/
│ └── controllers/
└── lib/tasks/
└── migrate_meetups.rake
| Variable | Description |
|---|---|
GITHUB_CLIENT_ID |
GitHub OAuth app client ID |
GITHUB_CLIENT_SECRET |
GitHub OAuth app secret |
GITHUB_TOKEN |
GitHub personal access token (for admin checks) |
GITHUB_REPO |
Repository for admin authorization (default: 3cityRUG/TRUG) |
RAILS_MASTER_KEY |
Rails encryption key for credentials |
Central entity for meetup events with associated talks and attendances.
class Meetup < ApplicationRecord
has_many :talks, dependent: :destroy
has_many :attendances, dependent: :destroy
validates :number, presence: true, uniqueness: true
validates :date, presence: true
scope :ordered, -> { order(date: :desc) }
scope :upcoming, -> { where("date >= ?", Date.today).order(date: :asc) }
scope :past, -> { where("date < ?", Date.today).order(date: :desc) }
endPresentation talks belonging to meetups.
class Talk < ApplicationRecord
belongs_to :meetup
validates :title, presence: true
validates :speaker_name, presence: true
scope :for_meetup, ->(meetup) { where(meetup_id: meetup.id) }
endTracks attendee participation with GitHub username and status.
class Attendance < ApplicationRecord
belongs_to :meetup
belongs_to :user, optional: true
STATUSES = { maybe: 0, yes: 1, no: 2 }.freeze
scope :for_meetup, ->(meetup) { where(meetup_id: meetup.id) }
scope :confirmed, -> { where(status: STATUSES[:yes]) }
def status_name
STATUSES.key(status).to_s if status.present?
end
endRails 8 authentication with GitHub OAuth integration.
class User < ApplicationRecord
has_secure_password
has_many :sessions, dependent: :destroy
normalizes :email_address, with: ->(e) { e.strip.downcase }
def self.from_github(github_data)
user = find_or_initialize_by(github_id: github_data["id"].to_s)
unless user.persisted?
user.github_username = github_data["login"]
user.email_address = github_data["email"] || "#{github_data["login"]}@github.local"
user.password = SecureRandom.hex(32)
user.save!
end
user
end
def github?
github_id.present?
end
endRails.application.routes.draw do
root "pages#home"
get "/archive", to: "pages#archive"
resource :session, only: :destroy
resource :github_session, controller: :github_sessions
get "/auth/:provider", to: redirect { ... }
resources :attendances, only: [ :new, :create ]
get "/video-thumbnails/:provider/:id", to: "video_thumbnails#show"
get "up" => "rails/health#show", as: :rails_health_check
namespace :admin do
root "dashboard#index"
resources :meetups do
resources :talks, except: [ :index ]
end
resources :talks, only: [ :edit, :update, :destroy ]
end
endDesign tokens for consistent theming:
:root {
--color-brand: #e25454;
--color-brand-dark: #c23e3e;
--color-dark: #505050;
--color-light: #ffffff;
--color-gray-light: #e0e0e0;
--color-gray-dark: #333;
--font-family: Lato, -apple-system, BlinkMacSystemFont, sans-serif;
--border-radius: 50px;
--spacing-sm: 0.5rem;
--spacing-md: 1rem;
--spacing-lg: 2rem;
}Flexbox-based grid with breakpoint at 768px:
.container { max-width: 1200px; margin: 0 auto; padding: 0 24px; }
.grid { display: flex; flex-wrap: wrap; margin: 0 -15px; }
.grid-item.half { width: 50%; }
.grid-item.one-third { width: 33.333%; }
@media (max-width: 768px) {
.grid-item.half, .grid-item.one-third { width: 100%; }
}BEM-like component-modifier pattern:
- Blocks:
.navbar,.btn,.video-container - Elements:
.navbar-brand,.btn-icon - Modifiers:
.btn-brand,.btn-ghost,.video-playing
pin "application"
pin "@hotwired/turbo-rails", to: "turbo.min.js"
pin "@hotwired/stimulus", to: "stimulus.min.js"
pin "archive", to: "archive.js"
pin "site", to: "site.js"Lazy-loading video player for YouTube and Vimeo with click-to-play functionality.
# config/deploy.yml
service: trug
image: trug-rails
servers:
web:
- 192.0.2.1
registry:
username: trug-bot
password:
- KAMAL_REGISTRY_PASSWORD
volumes:
- "trug-storage:/rails/storage"
- "trug-sqlite:/rails/db"
traefik:
options:
- "--entrypoints.websecure.http.tls=true"bin/rails test # All tests
bin/rails test test/system/ # E2E tests
bin/rails test test/models/ # Model tests
bin/rails test test/controllers/ # Controller testsSystem tests use Capybara with Selenium (headless Chrome).
bin/rails meetups:migrate_from_yaml # Import from legacy Middleman data
bin/rails meetups:fetch_vimeo_thumbs # Fetch Vimeo video thumbnails
bin/rails db:drop db:create # Reset database
bin/rails assets:clobber # Clear asset cacheGitHub OAuth Tests: Fixed test failures caused by fixture conflicts. Tests now use unique GitHub IDs that don't conflict with fixture data.
OmniAuth CSRF Protection: Updated omniauth-rails_csrf_protection from 1.0.2 to 2.0.1 to fix Rails 8.2 deprecation warning.
Favicon: Updated from placeholder to use the TRUG logo SVG (public/icon.svg).
Landing Page Typography: Enhanced .landing-lead ("Trójmiejska Grupa Użytkowników Ruby" heading) with:
- Increased font-size from 2.5rem to 3rem
- Increased font-weight from 600 to 700
- Added text-shadow for better visibility
Vimeo CDN doesn't allow cross-origin requests. Solution: Use oEmbed API proxy endpoint at /video-thumbnails/vimeo/:id.
Use button_to with method: :delete and form: { data: { turbo_confirm: "..." } } instead of link_to with data: { turbo_method: :delete }.
Admin checks require GitHub repo collaborator status. In development mode, admin check is bypassed.