Skip to content
Open
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
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
TOPNEWS_DATABASE_USER=
TOPNEWS_DATABASE_PASSWORD=
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,6 @@
/yarn-error.log

.byebug_history

# ignore env
.env
4 changes: 4 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,7 @@ gem 'turbolinks'
gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]
gem 'uglifier'
gem 'web-console', group: :development
gem 'dotenv', groups: [:development, :test]
gem 'faraday'
gem 'rails-controller-testing', group: :test
gem 'webmock', group: :test
26 changes: 26 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ GEM
public_suffix (>= 2.0.2, < 7.0)
base64 (0.2.0)
bcrypt (3.1.20)
bigdecimal (3.1.8)
bindex (0.8.1)
builder (3.3.0)
byebug (11.1.3)
Expand All @@ -91,6 +92,9 @@ GEM
execjs
coffee-script-source (1.12.2)
concurrent-ruby (1.3.4)
crack (1.0.0)
bigdecimal
rexml
crass (1.0.6)
date (3.3.4)
devise (4.9.4)
Expand All @@ -100,16 +104,23 @@ GEM
responders
warden (~> 1.2.3)
diff-lcs (1.5.1)
dotenv (3.1.2)
erubi (1.13.0)
execjs (2.9.1)
factory_bot (6.4.2)
activesupport (>= 5.0.0)
factory_bot_rails (6.4.3)
factory_bot (~> 6.4)
railties (>= 5.0.0)
faraday (2.11.0)
faraday-net_http (>= 2.0, < 3.4)
logger
faraday-net_http (3.3.0)
net-http
ffi (1.17.0)
globalid (1.2.1)
activesupport (>= 6.1)
hashdiff (1.1.1)
i18n (1.14.5)
concurrent-ruby (~> 1.0)
jbuilder (2.12.0)
Expand All @@ -133,6 +144,8 @@ GEM
mini_mime (1.1.5)
mini_portile2 (2.8.7)
minitest (5.25.1)
net-http (0.4.1)
uri
net-imap (0.4.14)
date
net-protocol
Expand Down Expand Up @@ -174,6 +187,10 @@ GEM
activesupport (= 7.0.8.4)
bundler (>= 1.15.0)
railties (= 7.0.8.4)
rails-controller-testing (1.0.5)
actionpack (>= 5.0.1.rc1)
actionview (>= 5.0.1.rc1)
activesupport (>= 5.0.1.rc1)
rails-dom-testing (2.2.0)
activesupport (>= 5.0.0)
minitest
Expand Down Expand Up @@ -251,13 +268,18 @@ GEM
concurrent-ruby (~> 1.0)
uglifier (4.2.0)
execjs (>= 0.3.0, < 3)
uri (0.13.1)
warden (1.2.9)
rack (>= 2.0.9)
web-console (4.2.1)
actionview (>= 6.0.0)
activemodel (>= 6.0.0)
bindex (>= 0.4.0)
railties (>= 6.0.0)
webmock (3.23.1)
addressable (>= 2.8.0)
crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0)
websocket (1.2.11)
websocket-driver (0.7.6)
websocket-extensions (>= 0.1.0)
Expand All @@ -274,13 +296,16 @@ DEPENDENCIES
capybara
coffee-rails
devise
dotenv
factory_bot_rails
faraday
jbuilder
listen
pg
pry-rails
puma
rails (~> 7.0.8)
rails-controller-testing
rspec-rails
sass-rails
selenium-webdriver
Expand All @@ -289,6 +314,7 @@ DEPENDENCIES
tzinfo-data
uglifier
web-console
webmock

RUBY VERSION
ruby 3.2.3p157
Expand Down
40 changes: 40 additions & 0 deletions app/controllers/stories_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
class StoriesController < ApplicationController
before_action :authenticate_user!, only: [:star, :unstar]
before_action :set_starred_stories, only: [:index, :starred]
def index

@stories = HackerNewsService.new.fetch_top_stories

# This was outside of the scope but thought this could be fun to add to the homepage :)
# Set @current_user_starred_stories if there is a current_user
if current_user
# Eager loading to prevent n1
@current_user_starred_stories = current_user.starred_stories.includes(:stars).distinct
end

end

def starred
# set_starred_stories handles getting starred stories
end

def star
story = Story.find_or_create_by(hacker_news_id: params[:hacker_news_id], title: params[:title], url: params[:url].presence || '/')
current_user.stars.create(story: story)
redirect_back_or_to root_path
end

def unstar
story = Story.find_by(hacker_news_id: params[:hacker_news_id])
star = current_user.stars.find_by(story: story)
star&.destroy
redirect_back_or_to root_path
end

private

def set_starred_stories
@starred_stories = Story.includes(:starred_by_users).joins(:stars).distinct
end

end
9 changes: 9 additions & 0 deletions app/helpers/stories_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
module StoriesHelper
def story_in_database?(story_id)
Story.exists?(hacker_news_id: story_id)
end

def story_starred_by_users(story_id)
Story.find_by(hacker_news_id: story_id)&.starred_by_users
end
end
14 changes: 14 additions & 0 deletions app/models/star.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
class Star < ApplicationRecord
belongs_to :user
belongs_to :story

# Callback to clean up story if it's no longer starred by any users
after_destroy :remove_story_if_unstarred

private

def remove_story_if_unstarred
# Destroy the story if no stars remain
story.destroy if story.stars.empty?
end
end
11 changes: 11 additions & 0 deletions app/models/story.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
class Story < ApplicationRecord
# We need to get all related stars and the users that starred them
# We could do a 1 to 1 relationship but since multiple people can star a we need to create a pivot table
has_many :stars, dependent: :destroy
has_many :starred_by_users, through: :stars, source: :user

# Make sure none of the fields are null and that
validates :hacker_news_id, presence: true, uniqueness: true
validates :title, :url, presence: true

end
5 changes: 5 additions & 0 deletions app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,9 @@ class User < ApplicationRecord
# :confirmable, :lockable, :timeoutable and :omniauthable
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable

# Has many stars
has_many :stars, dependent: :destroy
has_many :starred_stories, through: :stars, source: :story

end
26 changes: 26 additions & 0 deletions app/services/hacker_news_service.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
class HackerNewsService
HN_TOP_STORIES_URL = 'https://hacker-news.firebaseio.com/v0/topstories.json'
HN_ITEM_URL = 'https://hacker-news.firebaseio.com/v0/item/%<id>d.json'

def fetch_top_stories(limit = 15)
# TODO: Add pagination or lazy load
top_ids = fetch_top_story_ids.first(limit)
top_ids.map { |id| fetch_story_details(id) }.compact
end

private

def fetch_top_story_ids
response = Faraday.get(HN_TOP_STORIES_URL)
JSON.parse(response.body) if response.success?
rescue
[]
end

def fetch_story_details(story_id)
response = Faraday.get(format(HN_ITEM_URL, id: story_id))
JSON.parse(response.body).slice('id', 'title', 'url').transform_keys(&:to_sym) if response.success?
rescue
nil
end
end
16 changes: 16 additions & 0 deletions app/views/layouts/_header.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<header>
<nav class="px-4 lg:px-6 py-2.5 bg-slate-700">
<div class="flex flex-wrap justify-between items-center mx-auto">
<%= link_to 'Shasheesh', root_path, class:"self-center text-xl font-semibold whitespace-nowrap text-white" %>
<%= link_to 'Starred Stories', starred_stories_path, class:"self-center font-semibold whitespace-nowrap text-white" %>
<div class="flex items-center lg:order-2">
<% if user_signed_in? %>
<span class="text-gray-400 mr-5">Welcome, <%= current_user.first_name %>!</span>
<%= link_to 'Sign Out', destroy_user_session_path, method: :delete, class:"text-white bg-primary-700 hover:bg-primary-800 font-medium rounded-lg text-sm px-4 mr-2 bg-primary-600 hover:bg-primary-700" %>
<% else %>
<%= link_to 'Sign In', new_user_session_path, class:"animate text-white hover:bg-gray-50 animate focus:ring-4 focus:ring-gray-300 font-medium rounded-lg text-sm px-4 lg:px-5 py-2 lg:py-2.5 mr-2 hover:bg-gray-700 focus:outline-none dark:focus:ring-gray-800" %>
<% end %>
</div>
</div>
</nav>
</header>
15 changes: 7 additions & 8 deletions app/views/layouts/application.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,13 @@
<%= csrf_meta_tags %>
<%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
<%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>
</head>
<body>
<p class="notice">
<%= notice %>
</p>
<p class="alert">
<%= alert %>
</p>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.6.0/css/all.min.css" integrity="sha512-Kc323vGBEqzTmouAECnVceyQqyqdsSiqLQISBL29aUW4U/M7pSPA/gEUZQqv1cwx4OnYxTxve5UMg5GT6L4JJg==" crossorigin="anonymous" referrerpolicy="no-referrer" />
</head>
<body class="bg-slate-100 text-gray-800">
<%= render 'layouts/header' %>
<main class="m-10">
<%= yield %>
</main>
</body>
</html>
18 changes: 18 additions & 0 deletions app/views/stories/_star_button.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<% if user_signed_in? %>
<% if current_user.starred_stories.exists?(hacker_news_id: id) %>
<%= button_to unstar_stories_path(hacker_news_id: id), method: :delete, class: 'bg-green-500 text-white p-2 rounded-full hover:bg-red-600 transition flex items-center' do %>
<i class="fa-solid fa-star text-white unstar-btn"></i>
<span class="sr-only">Unstar</span>
<% end %>
<% else %>
<%= button_to star_stories_path(hacker_news_id: id, title: story[:title], url: story[:url]), method: :post, class: 'bg-blue-500 text-white p-2 rounded-full hover:bg-blue-600 transition flex items-center' do %>
<i class="fa-regular fa-star text-white star-btn"></i>
<span class="sr-only">Star</span>
<% end %>
<% end %>
<% else %>
<%= button_to new_user_session_path, class: 'bg-gray-300 text-white p-2 rounded-full hover:bg-gray-500 transition flex items-center' do %>
<i class="fa-regular fa-star text-white logged-out-star-button logged-out-star-btn"></i>
<span class="sr-only">Star</span>
<% end %>
<% end %>
14 changes: 14 additions & 0 deletions app/views/stories/_story.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<div class="bg-white rounded-lg p-4 flex flex-col justify-between hover:shadow-md transition duration-500">
<div class="flex justify-between items-center">
<%= link_to story[:title], story[:url], target: '_blank', class: 'text-lg font-semibold text-gray-800 mb-2' %>
<%= render 'star_button', story: story, id: id %>
</div>

<% if story_starred_by_users(id) %>
<p class="text-sm font-semibold text-blue-500 mt-2">Starred by:
<% story_starred_by_users(id).each do |user| %>
<span class="text-gray-500 font-normal"><%= user.first_name %> <%= user.last_name %></span>
<% end %>
</p>
<% end %>
</div>
15 changes: 15 additions & 0 deletions app/views/stories/index.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<h1 class="mb-4 text-4xl font-extrabold gap-5">Top Hacker News Stories</h1>
<div class="grid grid-cols-1 gap-3">
<% @stories.each do |story| %>
<%= render 'story', story: story, id: story[:id] %>
<% end %>
</div>

<% if @current_user_starred_stories.present? %>
<h1 class="mb-4 text-4xl font-extrabold gap-5 mt-10">Your Starred Stories</h1>
<div class="grid grid-cols-1 gap-3">
<% @current_user_starred_stories.each do |story| %>
<%= render 'story', story: story, id: story[:hacker_news_id] %>
<% end %>
</div>
<% end %>
13 changes: 13 additions & 0 deletions app/views/stories/starred.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@



<% if @starred_stories.any? %>
<h1 class="mb-4 text-4xl font-extrabold gap-5">Starred Stories</h1>
<div class="grid grid-cols-1 gap-3">
<% @starred_stories.each do |story| %>
<%= render 'story', story: story, id: story[:hacker_news_id] %>
<% end %>
</div>
<% else %>
<h1 class="mb-4 text-4xl font-extrabold gap-5 mt-10">Sorry there's no starred stories!</h1>
<% end %>
3 changes: 3 additions & 0 deletions config/application.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,8 @@ class Application < Rails::Application
# -- all .rb files in that directory are automatically loaded.

config.active_record.legacy_connection_handling = false

# Autoload Service
config.autoload_paths << Rails.root.join('app/services')
end
end
2 changes: 2 additions & 0 deletions config/database.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ default: &default
# For details on connection pooling, see Rails configuration guide
# http://guides.rubyonrails.org/configuring.html#database-pooling
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
username: <%= ENV['TOPNEWS_DATABASE_USER'] %>
password: <%= ENV['TOPNEWS_DATABASE_PASSWORD'] %>

development:
<<: *default
Expand Down
10 changes: 9 additions & 1 deletion config/routes.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
Rails.application.routes.draw do
devise_for :users
root to: 'pages#home'

resources :stories, only: [:index] do
post :star, on: :collection
delete :unstar, on: :collection
get :starred, on: :collection
end

root 'stories#index'

end
Loading