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
16 changes: 15 additions & 1 deletion Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,25 @@ gem 'pg'
gem 'pry-rails'
gem 'puma'
gem 'rails', '~> 7.0.3'
gem 'rspec-rails'
gem 'sass-rails'
gem 'selenium-webdriver', group: [:development, :test]
gem 'spring', group: :development
gem 'turbolinks'
gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]
gem 'uglifier'
gem 'web-console', group: :development
gem 'httparty'
gem 'pry-byebug'

group :test do
gem 'rspec-rails'
gem 'factory_bot_rails'
gem 'faker'
gem 'database_cleaner'
gem 'rails-controller-testing'
gem 'shoulda-matchers', '~> 4.0'
end

group :test, :development do
gem 'database_cleaner-active_record'
end
38 changes: 38 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ GEM
addressable (2.8.1)
public_suffix (>= 2.0.2, < 6.0)
bcrypt (3.1.18)
bigdecimal (3.1.8)
bindex (0.8.1)
builder (3.2.4)
byebug (11.1.3)
Expand All @@ -92,6 +93,13 @@ GEM
coffee-script-source (1.12.2)
concurrent-ruby (1.1.10)
crass (1.0.6)
csv (3.3.0)
database_cleaner (2.0.2)
database_cleaner-active_record (>= 2, < 3)
database_cleaner-active_record (2.1.0)
activerecord (>= 5.a)
database_cleaner-core (~> 2.0.0)
database_cleaner-core (2.0.1)
devise (4.8.1)
bcrypt (~> 3.0)
orm_adapter (~> 0.1)
Expand All @@ -102,9 +110,20 @@ GEM
digest (3.1.0)
erubi (1.11.0)
execjs (2.8.1)
factory_bot (6.4.6)
activesupport (>= 5.0.0)
factory_bot_rails (6.4.3)
factory_bot (~> 6.4)
railties (>= 5.0.0)
faker (3.4.1)
i18n (>= 1.8.11, < 2)
ffi (1.15.5)
globalid (1.0.0)
activesupport (>= 5.0)
httparty (0.22.0)
csv
mini_mime (>= 1.0.0)
multi_xml (>= 0.5.2)
i18n (1.12.0)
concurrent-ruby (~> 1.0)
jbuilder (2.11.5)
Expand All @@ -124,6 +143,8 @@ GEM
mini_mime (1.1.2)
mini_portile2 (2.8.0)
minitest (5.16.3)
multi_xml (0.7.1)
bigdecimal (~> 3.1)
net-imap (0.2.3)
digest
net-protocol
Expand All @@ -147,6 +168,9 @@ GEM
pry (0.14.1)
coderay (~> 1.1)
method_source (~> 1.0)
pry-byebug (3.10.1)
byebug (~> 11.0)
pry (>= 0.13, < 0.15)
pry-rails (0.3.9)
pry (>= 0.10.4)
public_suffix (5.0.0)
Expand All @@ -170,6 +194,10 @@ GEM
activesupport (= 7.0.4)
bundler (>= 1.15.0)
railties (= 7.0.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.0.3)
activesupport (>= 4.2.0)
nokogiri (>= 1.6)
Expand Down Expand Up @@ -224,6 +252,8 @@ GEM
rexml (~> 3.2, >= 3.2.5)
rubyzip (>= 1.2.2, < 3.0)
websocket (~> 1.0)
shoulda-matchers (4.5.1)
activesupport (>= 4.2.0)
spring (4.1.0)
sprockets (4.1.1)
concurrent-ruby (~> 1.0)
Expand Down Expand Up @@ -265,16 +295,24 @@ DEPENDENCIES
byebug
capybara
coffee-rails
database_cleaner
database_cleaner-active_record
devise
factory_bot_rails
faker
httparty
jbuilder
listen
pg
pry-byebug
pry-rails
puma
rails (~> 7.0.3)
rails-controller-testing
rspec-rails
sass-rails
selenium-webdriver
shoulda-matchers (~> 4.0)
spring
turbolinks
tzinfo-data
Expand Down
Binary file added app/assets/images/star-empty-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added app/assets/images/star-gold-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
16 changes: 16 additions & 0 deletions app/assets/stylesheets/application.css
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,19 @@
*= require_tree .
*= require_self
*/

.story h2 {
display: flex;
align-items: center;
}

.inline-form {
margin-right: 10px;
display: inline;
}

.flag-icon {
cursor: pointer;
width: 20px;
height: 20px;
}
4 changes: 4 additions & 0 deletions app/controllers/application_controller.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
class ApplicationController < ActionController::Base
protect_from_forgery with: :exception

def after_sign_in_path_for(resource)
stories_path
end
end
23 changes: 23 additions & 0 deletions app/controllers/flags_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
class FlagsController < ApplicationController
before_action :authenticate_user!

def create
story_data = HackerNewsService.new.fetch_story(params[:story_id])
if story_data
story = Story.find_or_create_by(id: story_data['id']) do |s|
s.title = story_data['title']
s.url = story_data['url']
end
current_user.flagged_stories << story unless current_user.flagged_stories.exists?(story.id)
redirect_to stories_path, notice: 'Story flagged successfully.'
else
redirect_to stories_path, alert: 'Story not found.'
end
end

def destroy
story = Story.find(params[:story_id])
current_user.flagged_stories.delete(story)
redirect_to stories_path, notice: 'Story unflagged successfully.'
end
end
14 changes: 14 additions & 0 deletions app/controllers/stories_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
class StoriesController < ApplicationController
before_action :authenticate_user!, except: [:index, :show]
before_action :load_stories, :flagged_stories

private

def load_stories
@stories = HackerNewsService.new.fetch_top_stories
end

def flagged_stories
@flagged_stories = Flag.includes(:story, :user).all.group_by(&:story_id)
end
end
4 changes: 4 additions & 0 deletions app/models/flag.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
class Flag < ApplicationRecord
belongs_to :user
belongs_to :story
end
7 changes: 7 additions & 0 deletions app/models/story.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
class Story < ApplicationRecord
has_many :flags, dependent: :destroy
has_many :flagged_by_users, through: :flags, source: :user

validates :title, presence: true, uniqueness: { scope: :url }
validates :url, presence: true
end
6 changes: 6 additions & 0 deletions app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,10 @@ class User < ApplicationRecord
# :confirmable, :lockable, :timeoutable and :omniauthable
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable
has_many :flags
has_many :flagged_stories, through: :flags, source: :story

def name
first_name + " " + last_name
end
end
25 changes: 25 additions & 0 deletions app/services/hacker_news_service.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
require 'net/http'
require 'json'

class HackerNewsService
TOP_STORIES_URL = 'https://hacker-news.firebaseio.com/v0/topstories.json'
STORY_URL = 'https://hacker-news.firebaseio.com/v0/item/%{id}.json'

def fetch_top_stories(limit = 20)
story_ids = fetch_story_ids
story_ids.first(limit).map { |id| fetch_story(id) }
end

def fetch_story(id)
url = STORY_URL % { id: id }
response = HTTParty.get(url)
response.parsed_response
end

private

def fetch_story_ids
response = HTTParty.get(TOP_STORIES_URL)
response.parsed_response
end
end
13 changes: 13 additions & 0 deletions app/views/layouts/application.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,19 @@
<%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>
</head>
<body>
<div>
<% if current_user %>
<%= link_to 'Sign out', destroy_user_session_path, method: :delete %>
<% else %>
<%= link_to 'Sign in', new_user_session_path %>
<% end %>
</div>
<div>
<%= link_to 'Latest News Stories', stories_path %>
</div>
<div>
<%= link_to 'Team Favorites', flagged_stories_path %>
</div>
<p class="notice">
<%= notice %>
</p>
Expand Down
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="story">
<h2>
<% if current_user.flagged_stories.exists?(story['id']) %>
<%= form_with url: story_flag_path(story['id']), method: :delete, local: true, class: 'inline-form' do %>
<%= image_tag 'star-gold-icon.png', alt: 'Unflag', data: { confirm: 'Are you sure you want to unflag this story?' }, class: 'flag-icon', onclick: 'this.closest("form").submit()' %>
<% end %>
<% else %>
<%= form_with url: story_flag_path(story['id']), method: :post, local: true, class: 'inline-form' do %>
<%= image_tag 'star-empty-icon.png', alt: 'Flag', class: 'flag-icon', onclick: 'this.closest("form").submit()' %>
<% end %>
<% end %>
<a href="<%= story['url'] %>"><%= story['title'] %></a>
</h2>
</div>
19 changes: 19 additions & 0 deletions app/views/stories/flagged_stories.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<h1>Flagged Stories</h1>
<ul>
<% @flagged_stories.each do |story_id, flags| %>
<div class="story">
<h2>
<% if current_user.flagged_stories.exists?(story_id) %>
<%= form_with url: story_flag_path(Story.find(story_id)), method: :delete, local: true, class: 'inline-form' do |form| %>
<%= image_tag 'star-gold-icon.png', alt: 'Unflag', data: { confirm: 'Are you sure you want to unflag this story?' }, class: 'flag-icon', onclick: 'this.closest("form").submit()' %>
<% end %>
<% else %>
<%= form_with url: story_flag_path(Story.find(story_id)), method: :post, local: true, class: 'inline-form' do %>
<%= image_tag 'star-empty-icon.png', alt: 'Flag', class: 'flag-icon', onclick: 'this.closest("form").submit()' %>
<% end %>
<% end %>
<a href="<%= flags.first.story.url %>"><%= flags.first.story.title %></a> &nbsp; - flagged by <%= flags.map(&:user).map(&:name).to_sentence %>
</h2>
</div>
<% end %>
</ul>
6 changes: 6 additions & 0 deletions app/views/stories/index.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<h1>Top Hacker News Stories</h1>
<ul>
<% @stories.each do |story| %>
<%= render partial: 'story', locals: { story: story } %>
<% end %>
</ul>
3 changes: 3 additions & 0 deletions config/application.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ class Application < Rails::Application
# Initialize configuration defaults for originally generated Rails version.
config.load_defaults 5.1

config.active_record.legacy_connection_handling = false
config.action_controller.urlsafe_csrf_tokens = true

# Settings in config/environments/* take precedence over those specified here.
# Application configuration should go into files in config/initializers
# -- all .rb files in that directory are automatically loaded.
Expand Down
2 changes: 1 addition & 1 deletion config/environments/test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
# test suite. You never need to work with it otherwise. Remember that
# your test database is "scratch space" for the test suite and is wiped
# and recreated between test runs. Don't rely on the data there!
config.cache_classes = true
config.cache_classes = false

# Do not eager load code on boot. This avoids loading your whole application
# just for the purpose of running a single test. If you are using a tool that
Expand Down
6 changes: 6 additions & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
Rails.application.routes.draw do
devise_for :users
root to: 'pages#home'

get 'flagged_stories', to: 'stories#flagged_stories', as: 'flagged_stories'

resources :stories do
resource :flag, only: [:create, :destroy]
end
end
12 changes: 12 additions & 0 deletions db/migrate/20240704180117_create_stories.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
class CreateStories < ActiveRecord::Migration[7.0]
def change
create_table :stories do |t|
t.string :title
t.string :url

t.timestamps
end

add_index :stories, [:title, :url], unique: true
end
end
10 changes: 10 additions & 0 deletions db/migrate/20240704182024_create_flags.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
class CreateFlags < ActiveRecord::Migration[6.1]
def change
create_table :flags do |t|
t.references :user, null: false, foreign_key: true
t.references :story, null: false, foreign_key: true

t.timestamps
end
end
end
Loading