Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 14 additions & 9 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,18 +55,23 @@ jobs:
run: |
yarn install --pure-lockfile

- name: Build and run tests
- name: Build JS and CSS assets
run: |
bin/rails javascript:build
bin/rails css:build

- name: Setup database
env:
DATABASE_URL: postgres://postgres:@localhost:5432/test
#POSTGRES_PASSWORD: postgres
RAILS_ENV: test
RAILS_MASTER_KEY: ${{ secrets.RAILS_TEST_MASTER_KEY }}
run: |
sudo apt-get -yqq install libpq-dev
gem install bundler
bundle install --jobs 4 --retry 3
bin/rails javascript:build
bin/rails css:build
bundle exec rails db:prepare
bundle exec rails test
bundle exec rails db:create
bundle exec rails db:schema:load

- name: Run tests
env:
DATABASE_URL: postgres://postgres:@localhost:5432/test
RAILS_ENV: test
RAILS_MASTER_KEY: ${{ secrets.RAILS_TEST_MASTER_KEY }}
run: bundle exec rails test
4 changes: 4 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ gem 'pg', '~> 1.1' # Use postgresql as the database for Active Record
gem 'premailer-rails', '~> 1.12' # This gem is a drop in solution for styling HTML emails with CSS
gem 'puma', '~> 6.0' # Use the Puma web server [https://github.com/puma/puma]
gem 'rails', '~> 7.2.2.1' # Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main"
# Pagination
gem 'pagy', '~> 9.4.0'
# gem 'kaminari'
# gem 'kaminari-tailwind'
gem 'redis', '~> 4.0' # Use Redis adapter to run Action Cable in production
# An ActionMailer adapter to send email using SendGrid's HTTPS Web API (instead of SMTP).
gem 'rack-attack' # Rack middleware for blocking & throttling abusive requests
Expand Down
2 changes: 2 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,7 @@ GEM
racc (~> 1.4)
orm_adapter (0.5.0)
ostruct (0.6.3)
pagy (9.4.0)
parallel (1.27.0)
parser (3.3.9.0)
ast (~> 2.4.1)
Expand Down Expand Up @@ -473,6 +474,7 @@ DEPENDENCIES
mini_magick (~> 4.12)
mocha
motor-admin (~> 0.4.7)
pagy (~> 9.4.0)
pg (~> 1.1)
premailer-rails (~> 1.12)
puma (~> 6.0)
Expand Down
2 changes: 2 additions & 0 deletions app/controllers/application_controller.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# frozen_string_literal: true

class ApplicationController < ActionController::Base
include Pagy::Backend

before_action :authenticate_user! # All users should be authenticated in all controllers by default
before_action :configure_permitted_parameters, if: :devise_controller?

Expand Down
19 changes: 19 additions & 0 deletions app/controllers/learning_materials_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# frozen_string_literal: true

class LearningMaterialsController < ApplicationController
sleep 3
skip_before_action :authenticate_user!, only: %i[index]

def index
@materials = filter_and_paginate_materials
@featured_materials = LearningMaterial.featured.limit(2)
end

private

def filter_and_paginate_materials
@q = params[:q].to_s.strip
@level = params[:level].to_s.presence
LearningMaterial.search(@q).by_level(@level).recent_first
end
end
1 change: 1 addition & 0 deletions app/helpers/application_helper.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# frozen_string_literal: true

module ApplicationHelper
include Pagy::Frontend
end
46 changes: 46 additions & 0 deletions app/models/learning_material.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# frozen_string_literal: true

# == Schema Information
#
# Table name: learning_materials
#
# id :bigint not null, primary key
# featured :boolean
# level :integer
# link :string
# thumbnail :string
# title :string
# created_at :datetime not null
# updated_at :datetime not null
#
# Indexes
#
# index_learning_materials_on_featured (featured)
# index_learning_materials_on_level (level)
# index_learning_materials_on_title (title)
#
class LearningMaterial < ApplicationRecord
# Map app-level attribute names to the current DB column names
# DB columns (per schema.rb): thumbnail, link
# App code expects: thumbnail_url, link_url
alias_attribute :thumbnail_url, :thumbnail
alias_attribute :link_url, :link

enum :level, { beginner: 0, intermediate: 1, expert: 2 }

validates :title, presence: true
validates :level, presence: true
validates :link_url, presence: true, format: URI::DEFAULT_PARSER.make_regexp(%w[http https])
validates :thumbnail_url, allow_blank: true, format: URI::DEFAULT_PARSER.make_regexp(%w[http https])

scope :featured, -> { where(featured: true) }
scope :by_level, ->(lvl) { lvl.present? ? where(level: levels[lvl]) : all }
scope :search, lambda { |query|
if query.present?
where('title ILIKE ?', "%#{sanitize_sql_like(query)}%")
else
all
end
}
scope :recent_first, -> { order(created_at: :desc) }
end
2 changes: 1 addition & 1 deletion app/views/layouts/_footer.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

<li class="hover:text-red-600 hover:underline"><%= link_to 'Chapters', chapters_path %></li>

<li class="hover:text-red-600 hover:underline"><%= link_to 'Learning materials', landing_learn_path %></li>
<li class="hover:text-red-600 hover:underline"><%= link_to 'Learning materials', learning_materials_path %></li>

<% if FeatureFlag.find_by(name: 'projects').try(:enabled) %>
<li class="hover:text-red-600 hover:underline"><%= link_to 'Projects', '#' %></li>
Expand Down
4 changes: 2 additions & 2 deletions app/views/layouts/_navbar.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
<li><a href="#">Projects</a></li>
<% end %>

<li><%= link_to 'Learning Materials', landing_learn_path %></li>
<li><%= link_to 'Learning Materials', learning_materials_path %></li>

<% if user_signed_in? %>
<li><%= button_to "Sign out", destroy_user_session_path, method: :delete %></li>
Expand Down Expand Up @@ -64,7 +64,7 @@
</a>
<% end %>

<%= link_to 'Learning Materials', landing_learn_path,
<%= link_to 'Learning Materials', learning_materials_path,
class: 'hover:text-red-600' %>
</nav>
<% if user_signed_in? %>
Expand Down
72 changes: 72 additions & 0 deletions app/views/learning_materials/index.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<% content_for :title, 'Learning Materials' %>
<% content_for(:description, "A list of ruby and ruby on rails learning resources compiled by the ARC community") %>

<div class="grid pt-20 px-4 md:px-10 lg:px-32 mx-auto place-content-center">

<h1 class="text-4xl pt-6 pb-6 font-bold">Learning Materials</h1>

<div class="mb-6">
<%= form_with url: learning_materials_path, method: :get, local: true, class: 'grid grid-cols-1 md:grid-cols-6 gap-4' do %>
<div class="md:col-span-4">
<%= text_field_tag :q, @q, placeholder: 'Search materials (e.g., Rails, Ruby, Active Record, Hotwire)', class: 'input input-bordered w-full', id: 'q' %>
</div>
<div>
<%= select_tag :level, options_for_select([['All Levels', ''], ['Beginner', 'beginner'], ['Intermediate', 'intermediate'], ['Expert', 'expert']], @level), class: 'select select-bordered w-full', id: 'level' %>
</div>
<div>
<%= submit_tag 'Search', class: 'btn btn-primary w-full' %>
</div>
<% end %>
</div>

<hr>

<% if @featured_materials.any? %>
<section class="mt-10 mb-10 pb-10 border-b border-gray-300">
<h2 class="text-3xl font-bold text-red-600 mb-6">Featured</h2>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-2 gap-4">
<% @featured_materials.each do |m| %>
<div class="card bg-base-100 shadow-md overflow-hidden w-full transition-transform duration-300 hover:-translate-y-1 hover:shadow-xl">
<% if m.thumbnail_url.present? %>
<figure class="w-full aspect-[4/3] max-h-40 overflow-hidden">
<img src="<%= m.thumbnail_url %>" alt="<%= m.title %> thumbnail" class="w-full h-full object-cover transition-transform duration-300 ease-out hover:scale-105">
</figure>
<% end %>
<div class="card-body p-2">
<span class="badge badge-secondary capitalize w-fit text-[10px]"><%= m.level %></span>
<h3 class="card-title text-lg text-black mt-1"><%= m.title %></h3>
<div class="card-actions justify-start mt-2">
<%= link_to 'Open resource', m.link_url, class: 'link link-primary text-sm transition-colors duration-200 hover:underline', target: '_blank', rel: 'noopener' %>
</div>
</div>
</div>
<% end %>
</div>
</section>
<% end %>

<section class="mb-6">
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6"> <!-- slightly bigger gap -->
<% @materials.each_with_index do |m, i| %>
<div class="pb-6 border-b border-dotted border-gray-300 w-full max-w-md"> <!-- from sm → md -->
<div class="card bg-base-100 shadow-md overflow-hidden w-full max-w-md transition-transform duration-300 hover:-translate-y-1 hover:shadow-xl"> <!-- slightly larger card -->
<% if m.thumbnail_url.present? %>
<figure class="w-full aspect-video max-h-48 overflow-hidden"> <!-- a little taller thumbnail -->
<img src="<%= m.thumbnail_url %>" alt="<%= m.title %> thumbnail" class="w-full h-full object-cover transition-transform duration-300 ease-out hover:scale-105">
</figure>
<% end %>
<div class="card-body p-3 md:p-4"> <!-- slightly more breathing room -->
<span class="badge badge-ghost capitalize w-fit text-xs md:text-sm"><%= m.level %></span>
<h3 class="card-title text-base mt-1"><%= m.title %></h3>
<div class="card-actions justify-start mt-2">
<%= link_to 'Open resource', m.link_url, class: 'link link-primary text-sm transition-colors duration-200 hover:underline', target: '_blank', rel: 'noopener' %>
</div>
</div>
</div>
</div>
<% end %>
</div>

</section>

</div>
2 changes: 1 addition & 1 deletion config/credentials/development.yml.enc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
aEJpSXtkbpJ+02mEMP6qN+mYSs87DE+ZW+++VDp6DzOj6aMxLPyyZp0x8CjE18wNhtdD7j1oqia0cWtxHssxF7/rni5tIPo2J1klt5N3ZAUgKvP3x4Y8bATpd3qGOAktwXTcxg+LzT3+yQ2vT8S1RziVgX06fLgq0FXIX1u2u81Gz4KRl04ORc3OlwKdQsHYStydiT5zqG32G+VAjk9pZ9lBOBZZ1flzxG4=--mit11HznYmyyUEMs--3ZJkExWkUP51X82vdR6GFg==
3H4nnxxnw/WTujQUde9PtRABneENnc1Tvzm85snGxMRv32ZpBYke+JDwK5GKkIPaz/z1cdisizAAjwwKtXcGubTqVbKvv1L0BEV4BVOmb+YNyUnZ3q6nCQQy3ebrP1R22KcFYwCuzFwrB9Bi0oKsrS8nBTu8N/lnmhPWi1CIRM/GHfZsJ/7+Fowz9V1nW19O7afiRWnL3sr/QnNFduwBWeIg0Y9K8+P8qDo=--wMS2VGWWEYyQqbyB--3lfbuWZFsvcQ96iLCz+DLA==
1 change: 1 addition & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
resources :projects, only: %i[index show]
resources :chapters, only: %i[index show]
resources :countries, only: %i[index show]
resources :learning_materials, only: %i[index]
devise_for :users, controllers: {
registrations: 'users/registrations', # Override devise registration controller
sessions: 'users/sessions',
Expand Down
24 changes: 24 additions & 0 deletions db/migrate/20250907005500_create_learning_materials.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# frozen_string_literal: true

class CreateLearningMaterials < ActiveRecord::Migration[7.2]
def change
# Guard against existing table to avoid PG::DuplicateTable in environments where
# the table was created previously (e.g., manual setup or earlier branch).
return if table_exists?(:learning_materials)

create_table :learning_materials do |t|
t.string :title, null: false
t.integer :level, null: false, default: 0
t.string :thumbnail_url
t.string :link_url, null: false
t.boolean :featured, null: false, default: false
t.text :description

t.timestamps
end

add_index :learning_materials, :level
add_index :learning_materials, :featured
add_index :learning_materials, :title
end
end
25 changes: 14 additions & 11 deletions db/schema.rb

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

24 changes: 19 additions & 5 deletions db/seeds.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,22 @@

# This file should contain all the record creation needed to seed the database with its default values.
# The data can then be loaded with the bin/rails db:seed command (or created alongside the database with db:setup).
#
# Examples:
#
# movies = Movie.create([{ name: "Star Wars" }, { name: "Lord of the Rings" }])
# Character.create(name: "Luke", movie: movies.first)

# Sample learning materials for Rails & Ruby
if defined?(LearningMaterial)
LearningMaterial.find_or_create_by!(title: 'Rails Guides') do |lm|
lm.level = :beginner
lm.thumbnail_url = 'https://rubyonrails.org/images/rails-logo.svg'
lm.link_url = 'https://guides.rubyonrails.org/'
lm.featured = true
lm.description = 'Official Rails Guides for beginners to experts.'
end

LearningMaterial.find_or_create_by!(title: 'Ruby Official') do |lm|
lm.level = :intermediate
lm.thumbnail_url = 'https://www.ruby-lang.org/images/header-ruby-logo.png'
lm.link_url = 'https://www.ruby-lang.org/en/documentation/'
lm.featured = false
lm.description = 'Official Ruby documentation and resources.'
end
end
Loading