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
36 changes: 33 additions & 3 deletions app/controllers/projects_controller.rb
Original file line number Diff line number Diff line change
@@ -1,20 +1,50 @@
# frozen_string_literal: true

class ProjectsController < ApplicationController
skip_before_action :authenticate_user!, only: %i[index show]
before_action :set_project, only: %i[show]

# GET /projects or /projects.json
def index
@projects = Project.all
load_featured_project
load_projects
render_turbo_frame if turbo_frame_request?
end

# GET /projects/1 or /projects/1.json
def show; end
def show
@related_projects = Project.where(chapter_id: @project.chapter_id)
.where.not(id: @project.id)
.limit(3)
end

private

def load_featured_project
featured_scope = Project.featured
return unless featured_scope.exists?

@total_featured = featured_scope.count
@current_offset = params[:featured_offset].to_i
@featured_project = featured_scope.offset(@current_offset % @total_featured).first
end

def load_projects
projects_scope = Project.includes(:chapter).not_featured
projects_scope = projects_scope.search(params[:query]) if params[:query].present?
@pagy, @projects = pagy(projects_scope.order(created_at: :desc), items: 9)
end

def render_turbo_frame
if request.headers['Turbo-Frame'] == 'featured_project'
render partial: 'featured_project', locals: { featured_project: @featured_project }
else
render partial: 'projects', locals: { projects: @projects, pagy: @pagy }
end
end

# Use callbacks to share common setup or constraints between actions.
def set_project
@project = Project.find(params[:id])
@project = Project.find_by!(slug: params[:id])
end
end
9 changes: 9 additions & 0 deletions app/helpers/projects_helper.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
# frozen_string_literal: true

module ProjectsHelper
def project_image(project, css_class: '')
if project.image.attached?
image_tag project.image, alt: project.name, class: css_class
else
image_tag "https://placehold.co/600x400?text=#{CGI.escape(project.name)}",
alt: project.name,
class: css_class
end
end
end
85 changes: 77 additions & 8 deletions app/models/project.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,92 @@
#
# Table name: projects
#
# id :bigint not null, primary key
# description :text
# end_date :datetime
# name :string
# start_date :datetime
# created_at :datetime not null
# updated_at :datetime not null
# chapter_id :bigint not null
# id :bigint not null, primary key
# description :text
# end_date :datetime
# featured :boolean default(FALSE), not null
# featured_order :integer
# git_link :string
# intro :text
# name :string
# owner_name :string
# preview_link :string
# slug :string
# start_date :datetime
# created_at :datetime not null
# updated_at :datetime not null
# chapter_id :bigint not null
#
# Indexes
#
# index_projects_on_chapter_id (chapter_id)
# index_projects_on_featured (featured)
# index_projects_on_slug (slug) UNIQUE
#
# Foreign Keys
#
# fk_rails_... (chapter_id => chapters.id)
#
class Project < ApplicationRecord
# Attachments
has_one_attached :image

# Associations
belongs_to :chapter
has_many :project_contributors, dependent: :destroy
has_many :contributors, through: :project_contributors, source: :user

# Callbacks
before_validation :generate_slug, if: -> { name.present? && (slug.blank? || name_changed?) }

# Validations
validates :name, presence: true
validates :slug, presence: true, uniqueness: true
validates :intro, length: { maximum: 200 }, allow_blank: true
validates :preview_link, format: { with: URI::DEFAULT_PARSER.make_regexp(%w[http https]), allow_blank: true }
validates :git_link, format: { with: URI::DEFAULT_PARSER.make_regexp(%w[http https]), allow_blank: true }

# Scopes
scope :featured, -> { where(featured: true).order(:featured_order, :created_at) }
scope :not_featured, -> { where(featured: false) }
scope :search, lambda { |query|
where('name ILIKE ? OR description ILIKE ? OR intro ILIKE ?',
"%#{sanitize_sql_like(query)}%",
"%#{sanitize_sql_like(query)}%",
"%#{sanitize_sql_like(query)}%")
}

# Methods
def image?
image.attached?
end

def to_param
slug
end

def add_contributor(user, role: 'contributor')
project_contributors.find_or_create_by(user: user) do |pc|
pc.role = role
end
end

def remove_contributor(user)
project_contributors.where(user: user).destroy_all
end

private

def generate_slug
base_slug = name.parameterize
candidate_slug = base_slug
counter = 1

while Project.where(slug: candidate_slug).where.not(id: id).exists?
candidate_slug = "#{base_slug}-#{counter}"
counter += 1
end

self.slug = candidate_slug
end
end
9 changes: 9 additions & 0 deletions app/models/project_contributor.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# frozen_string_literal: true

class ProjectContributor < ApplicationRecord
belongs_to :project
belongs_to :user

validates :user_id, uniqueness: { scope: :project_id }
validates :role, inclusion: { in: %w[creator maintainer contributor], allow_blank: true }
end
2 changes: 2 additions & 0 deletions app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ class User < ApplicationRecord
# Associations
has_many :users_chapters, dependent: :nullify
has_many :chapters, through: :users_chapters
has_many :project_contributors, dependent: :destroy
has_many :contributed_projects, through: :project_contributors, source: :project

# Callbacks
before_create :set_defaults # Set model defaults before create
Expand Down
69 changes: 0 additions & 69 deletions app/views/landing/home/_projects.html.erb

This file was deleted.

4 changes: 0 additions & 4 deletions app/views/landing/index.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,6 @@

<%= render 'landing/home/activities' %>

<% if FeatureFlag.find_by(name: 'projects').try(:enabled) %>
<%= render 'landing/home/projects' %>
<% end %>

<%= render 'landing/home/chapters' %>

<%= render 'landing/home/featured_sponsors' %>
Expand Down
6 changes: 1 addition & 5 deletions app/views/layouts/_footer.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,8 @@
<% end %>

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

<li class="hover:text-red-600 hover:underline"><%= link_to 'Projects', projects_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>
<% end %>
</ul>

<ul class="flex flex-col gap-3">
Expand Down
13 changes: 3 additions & 10 deletions app/views/layouts/_navbar.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,7 @@
<% end %>

<li><%= link_to 'Chapters', chapters_path %></li>

<% if FeatureFlag.find_by(name: 'projects').try(:enabled) %>
<li><a href="#">Projects</a></li>
<% end %>

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

<% if user_signed_in? %>
Expand Down Expand Up @@ -58,11 +54,8 @@
<%= link_to 'Chapters', chapters_path,
class: "hover:text-red-600" %>

<% if FeatureFlag.find_by(name: 'projects').try(:enabled) %>
<a href="#" class="hover:text-red-600">
Projects
</a>
<% end %>
<%= link_to 'Projects', projects_path,
class: "hover:text-red-600" %>

<%= link_to 'Learning Materials', learning_materials_path,
class: 'hover:text-red-600' %>
Expand Down
1 change: 1 addition & 0 deletions app/views/layouts/application.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
<%= csp_meta_tag %>
<%= favicon_link_tag 'favicon.ico' %>

<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" integrity="sha512-DTOQO9RWCH3ppGqcWaEA1BIZOC6xxalwEsw9c2QQeAIftl+Vegovlnee1c9QX4TctnWMn13TZye+giMm8e2LwA==" crossorigin="anonymous" referrerpolicy="no-referrer" />
<%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
<%= javascript_include_tag "https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit", "data-turbo-track": "reload", defer: true %>
<%= javascript_include_tag "application", "data-turbo-track": "reload", defer: true %>
Expand Down
29 changes: 29 additions & 0 deletions app/views/layouts/projects.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Projects - African Ruby Community</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
<%= favicon_link_tag 'favicon.ico' %>

<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" integrity="sha512-DTOQO9RWCH3ppGqcWaEA1BIZOC6xxalwEsw9c2QQeAIftl+Vegovlnee1c9QX4TctnWMn13TZye+giMm8e2LwA==" crossorigin="anonymous" referrerpolicy="no-referrer" />
<%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
<%= javascript_include_tag "https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit", "data-turbo-track": "reload", defer: true %>
<%= javascript_include_tag "application", "data-turbo-track": "reload", defer: true %>
<%= Sentry.get_trace_propagation_meta.html_safe %>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="bg-white text-gray-900">
<div class="bg-gray-50">
<%= render 'layouts/navbar' %>

<main class="mx-8" style="min-height: 45vh">
<%= yield %>
</main>

<%= render 'layouts/flash_messages' %>
<%= render 'layouts/footer' %>
</div>
</body>
</html>
Loading