Skip to content

Commit 22d9579

Browse files
authored
Merge pull request #124 from JudahSan/FT/learning_materials
Add learning materials functionality with controller, model, views, and tests
2 parents c268965 + 373ba5e commit 22d9579

File tree

19 files changed

+403
-29
lines changed

19 files changed

+403
-29
lines changed

.github/workflows/ci.yml

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -55,18 +55,23 @@ jobs:
5555
run: |
5656
yarn install --pure-lockfile
5757
58-
- name: Build and run tests
58+
- name: Build JS and CSS assets
59+
run: |
60+
bin/rails javascript:build
61+
bin/rails css:build
62+
63+
- name: Setup database
5964
env:
6065
DATABASE_URL: postgres://postgres:@localhost:5432/test
61-
#POSTGRES_PASSWORD: postgres
6266
RAILS_ENV: test
6367
RAILS_MASTER_KEY: ${{ secrets.RAILS_TEST_MASTER_KEY }}
6468
run: |
65-
sudo apt-get -yqq install libpq-dev
66-
gem install bundler
67-
bundle install --jobs 4 --retry 3
68-
bin/rails javascript:build
69-
bin/rails css:build
70-
bundle exec rails db:prepare
71-
bundle exec rails test
69+
bundle exec rails db:create
70+
bundle exec rails db:schema:load
7271
72+
- name: Run tests
73+
env:
74+
DATABASE_URL: postgres://postgres:@localhost:5432/test
75+
RAILS_ENV: test
76+
RAILS_MASTER_KEY: ${{ secrets.RAILS_TEST_MASTER_KEY }}
77+
run: bundle exec rails test

Gemfile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ gem 'pg', '~> 1.1' # Use postgresql as the database for Active Record
2323
gem 'premailer-rails', '~> 1.12' # This gem is a drop in solution for styling HTML emails with CSS
2424
gem 'puma', '~> 6.0' # Use the Puma web server [https://github.com/puma/puma]
2525
gem 'rails', '~> 7.2.2.1' # Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main"
26+
# Pagination
27+
gem 'pagy', '~> 9.4.0'
28+
# gem 'kaminari'
29+
# gem 'kaminari-tailwind'
2630
gem 'redis', '~> 4.0' # Use Redis adapter to run Action Cable in production
2731
# An ActionMailer adapter to send email using SendGrid's HTTPS Web API (instead of SMTP).
2832
gem 'rack-attack' # Rack middleware for blocking & throttling abusive requests

Gemfile.lock

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,7 @@ GEM
260260
racc (~> 1.4)
261261
orm_adapter (0.5.0)
262262
ostruct (0.6.3)
263+
pagy (9.4.0)
263264
parallel (1.27.0)
264265
parser (3.3.9.0)
265266
ast (~> 2.4.1)
@@ -473,6 +474,7 @@ DEPENDENCIES
473474
mini_magick (~> 4.12)
474475
mocha
475476
motor-admin (~> 0.4.7)
477+
pagy (~> 9.4.0)
476478
pg (~> 1.1)
477479
premailer-rails (~> 1.12)
478480
puma (~> 6.0)

app/controllers/application_controller.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
# frozen_string_literal: true
22

33
class ApplicationController < ActionController::Base
4+
include Pagy::Backend
5+
46
before_action :authenticate_user! # All users should be authenticated in all controllers by default
57
before_action :configure_permitted_parameters, if: :devise_controller?
68

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# frozen_string_literal: true
2+
3+
class LearningMaterialsController < ApplicationController
4+
sleep 3
5+
skip_before_action :authenticate_user!, only: %i[index]
6+
7+
def index
8+
@materials = filter_and_paginate_materials
9+
@featured_materials = LearningMaterial.featured.limit(2)
10+
end
11+
12+
private
13+
14+
def filter_and_paginate_materials
15+
@q = params[:q].to_s.strip
16+
@level = params[:level].to_s.presence
17+
LearningMaterial.search(@q).by_level(@level).recent_first
18+
end
19+
end

app/helpers/application_helper.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
# frozen_string_literal: true
22

33
module ApplicationHelper
4+
include Pagy::Frontend
45
end

app/models/learning_material.rb

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# frozen_string_literal: true
2+
3+
# == Schema Information
4+
#
5+
# Table name: learning_materials
6+
#
7+
# id :bigint not null, primary key
8+
# featured :boolean
9+
# level :integer
10+
# link :string
11+
# thumbnail :string
12+
# title :string
13+
# created_at :datetime not null
14+
# updated_at :datetime not null
15+
#
16+
# Indexes
17+
#
18+
# index_learning_materials_on_featured (featured)
19+
# index_learning_materials_on_level (level)
20+
# index_learning_materials_on_title (title)
21+
#
22+
class LearningMaterial < ApplicationRecord
23+
# Map app-level attribute names to the current DB column names
24+
# DB columns (per schema.rb): thumbnail, link
25+
# App code expects: thumbnail_url, link_url
26+
alias_attribute :thumbnail_url, :thumbnail
27+
alias_attribute :link_url, :link
28+
29+
enum :level, { beginner: 0, intermediate: 1, expert: 2 }
30+
31+
validates :title, presence: true
32+
validates :level, presence: true
33+
validates :link_url, presence: true, format: URI::DEFAULT_PARSER.make_regexp(%w[http https])
34+
validates :thumbnail_url, allow_blank: true, format: URI::DEFAULT_PARSER.make_regexp(%w[http https])
35+
36+
scope :featured, -> { where(featured: true) }
37+
scope :by_level, ->(lvl) { lvl.present? ? where(level: levels[lvl]) : all }
38+
scope :search, lambda { |query|
39+
if query.present?
40+
where('title ILIKE ?', "%#{sanitize_sql_like(query)}%")
41+
else
42+
all
43+
end
44+
}
45+
scope :recent_first, -> { order(created_at: :desc) }
46+
end

app/views/layouts/_footer.html.erb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

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

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

1717
<% if FeatureFlag.find_by(name: 'projects').try(:enabled) %>
1818
<li class="hover:text-red-600 hover:underline"><%= link_to 'Projects', '#' %></li>

app/views/layouts/_navbar.html.erb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
<li><a href="#">Projects</a></li>
2424
<% end %>
2525

26-
<li><%= link_to 'Learning Materials', landing_learn_path %></li>
26+
<li><%= link_to 'Learning Materials', learning_materials_path %></li>
2727

2828
<% if user_signed_in? %>
2929
<li><%= button_to "Sign out", destroy_user_session_path, method: :delete %></li>
@@ -64,7 +64,7 @@
6464
</a>
6565
<% end %>
6666

67-
<%= link_to 'Learning Materials', landing_learn_path,
67+
<%= link_to 'Learning Materials', learning_materials_path,
6868
class: 'hover:text-red-600' %>
6969
</nav>
7070
<% if user_signed_in? %>
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
<% content_for :title, 'Learning Materials' %>
2+
<% content_for(:description, "A list of ruby and ruby on rails learning resources compiled by the ARC community") %>
3+
4+
<div class="grid pt-20 px-4 md:px-10 lg:px-32 mx-auto place-content-center">
5+
6+
<h1 class="text-4xl pt-6 pb-6 font-bold">Learning Materials</h1>
7+
8+
<div class="mb-6">
9+
<%= form_with url: learning_materials_path, method: :get, local: true, class: 'grid grid-cols-1 md:grid-cols-6 gap-4' do %>
10+
<div class="md:col-span-4">
11+
<%= text_field_tag :q, @q, placeholder: 'Search materials (e.g., Rails, Ruby, Active Record, Hotwire)', class: 'input input-bordered w-full', id: 'q' %>
12+
</div>
13+
<div>
14+
<%= select_tag :level, options_for_select([['All Levels', ''], ['Beginner', 'beginner'], ['Intermediate', 'intermediate'], ['Expert', 'expert']], @level), class: 'select select-bordered w-full', id: 'level' %>
15+
</div>
16+
<div>
17+
<%= submit_tag 'Search', class: 'btn btn-primary w-full' %>
18+
</div>
19+
<% end %>
20+
</div>
21+
22+
<hr>
23+
24+
<% if @featured_materials.any? %>
25+
<section class="mt-10 mb-10 pb-10 border-b border-gray-300">
26+
<h2 class="text-3xl font-bold text-red-600 mb-6">Featured</h2>
27+
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-2 gap-4">
28+
<% @featured_materials.each do |m| %>
29+
<div class="card bg-base-100 shadow-md overflow-hidden w-full transition-transform duration-300 hover:-translate-y-1 hover:shadow-xl">
30+
<% if m.thumbnail_url.present? %>
31+
<figure class="w-full aspect-[4/3] max-h-40 overflow-hidden">
32+
<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">
33+
</figure>
34+
<% end %>
35+
<div class="card-body p-2">
36+
<span class="badge badge-secondary capitalize w-fit text-[10px]"><%= m.level %></span>
37+
<h3 class="card-title text-lg text-black mt-1"><%= m.title %></h3>
38+
<div class="card-actions justify-start mt-2">
39+
<%= link_to 'Open resource', m.link_url, class: 'link link-primary text-sm transition-colors duration-200 hover:underline', target: '_blank', rel: 'noopener' %>
40+
</div>
41+
</div>
42+
</div>
43+
<% end %>
44+
</div>
45+
</section>
46+
<% end %>
47+
48+
<section class="mb-6">
49+
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6"> <!-- slightly bigger gap -->
50+
<% @materials.each_with_index do |m, i| %>
51+
<div class="pb-6 border-b border-dotted border-gray-300 w-full max-w-md"> <!-- from sm → md -->
52+
<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 -->
53+
<% if m.thumbnail_url.present? %>
54+
<figure class="w-full aspect-video max-h-48 overflow-hidden"> <!-- a little taller thumbnail -->
55+
<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">
56+
</figure>
57+
<% end %>
58+
<div class="card-body p-3 md:p-4"> <!-- slightly more breathing room -->
59+
<span class="badge badge-ghost capitalize w-fit text-xs md:text-sm"><%= m.level %></span>
60+
<h3 class="card-title text-base mt-1"><%= m.title %></h3>
61+
<div class="card-actions justify-start mt-2">
62+
<%= link_to 'Open resource', m.link_url, class: 'link link-primary text-sm transition-colors duration-200 hover:underline', target: '_blank', rel: 'noopener' %>
63+
</div>
64+
</div>
65+
</div>
66+
</div>
67+
<% end %>
68+
</div>
69+
70+
</section>
71+
72+
</div>

0 commit comments

Comments
 (0)