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
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

/app/assets/builds/*
!/app/assets/builds/.keep
5 changes: 4 additions & 1 deletion Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,14 @@ gem 'pg'
gem 'pry-rails'
gem 'puma'
gem 'rails', '~> 7.0.3'
gem 'rspec-rails'
gem 'rspec-rails', group: [:development, :test]
gem 'factory_bot_rails', group: [:development, :test]
gem 'faker', group: [:development, :test]
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 'bullet'
55 changes: 36 additions & 19 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -66,22 +66,25 @@ GEM
i18n (>= 1.6, < 2)
minitest (>= 5.1)
tzinfo (~> 2.0)
addressable (2.8.1)
public_suffix (>= 2.0.2, < 6.0)
addressable (2.8.7)
public_suffix (>= 2.0.2, < 7.0)
base64 (0.2.0)
bcrypt (3.1.18)
bindex (0.8.1)
builder (3.2.4)
bullet (7.2.0)
activesupport (>= 3.0.0)
uniform_notifier (~> 1.11)
byebug (11.1.3)
capybara (3.37.1)
capybara (3.40.0)
addressable
matrix
mini_mime (>= 0.1.3)
nokogiri (~> 1.8)
nokogiri (~> 1.11)
rack (>= 1.6.0)
rack-test (>= 0.6.3)
regexp_parser (>= 1.5, < 3.0)
xpath (~> 3.2)
childprocess (4.1.0)
coderay (1.1.3)
coffee-rails (5.0.0)
coffee-script (>= 2.2.0)
Expand All @@ -102,6 +105,13 @@ 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.2)
i18n (>= 1.8.11, < 2)
ffi (1.15.5)
globalid (1.0.0)
activesupport (>= 5.0)
Expand All @@ -113,6 +123,7 @@ GEM
listen (3.7.1)
rb-fsevent (~> 0.10, >= 0.10.3)
rb-inotify (~> 0.9, >= 0.9.10)
logger (1.6.0)
loofah (2.19.0)
crass (~> 1.0.2)
nokogiri (>= 1.5.9)
Expand All @@ -121,8 +132,8 @@ GEM
marcel (1.0.2)
matrix (0.4.2)
method_source (1.0.0)
mini_mime (1.1.2)
mini_portile2 (2.8.0)
mini_mime (1.1.5)
mini_portile2 (2.8.7)
minitest (5.16.3)
net-imap (0.2.3)
digest
Expand All @@ -139,8 +150,8 @@ GEM
net-protocol
timeout
nio4r (2.5.8)
nokogiri (1.13.8)
mini_portile2 (~> 2.8.0)
nokogiri (1.16.6)
mini_portile2 (~> 2.8.2)
racc (~> 1.4)
orm_adapter (0.5.0)
pg (1.4.3)
Expand All @@ -149,12 +160,12 @@ GEM
method_source (~> 1.0)
pry-rails (0.3.9)
pry (>= 0.10.4)
public_suffix (5.0.0)
public_suffix (6.0.0)
puma (5.6.5)
nio4r (~> 2.0)
racc (1.6.0)
rack (2.2.4)
rack-test (2.0.2)
racc (1.8.0)
rack (2.2.9)
rack-test (2.1.0)
rack (>= 1.3)
rails (7.0.4)
actioncable (= 7.0.4)
Expand Down Expand Up @@ -186,11 +197,12 @@ GEM
rb-fsevent (0.11.2)
rb-inotify (0.10.1)
ffi (~> 1.0)
regexp_parser (2.5.0)
regexp_parser (2.9.2)
responders (3.0.1)
actionpack (>= 5.0)
railties (>= 5.0)
rexml (3.2.5)
rexml (3.3.2)
strscan
rspec-core (3.11.0)
rspec-support (~> 3.11.0)
rspec-expectations (3.11.1)
Expand Down Expand Up @@ -219,8 +231,9 @@ GEM
sprockets (> 3.0)
sprockets-rails
tilt
selenium-webdriver (4.4.0)
childprocess (>= 0.5, < 5.0)
selenium-webdriver (4.23.0)
base64 (~> 0.2)
logger (~> 1.4)
rexml (~> 3.2, >= 3.2.5)
rubyzip (>= 1.2.2, < 3.0)
websocket (~> 1.0)
Expand All @@ -232,7 +245,7 @@ GEM
actionpack (>= 5.2)
activesupport (>= 5.2)
sprockets (>= 3.0.0)
strscan (3.0.4)
strscan (3.1.0)
thor (1.2.1)
tilt (2.0.11)
timeout (0.3.0)
Expand All @@ -243,14 +256,15 @@ GEM
concurrent-ruby (~> 1.0)
uglifier (4.2.0)
execjs (>= 0.3.0, < 3)
uniform_notifier (1.16.0)
warden (1.2.9)
rack (>= 2.0.9)
web-console (4.2.0)
actionview (>= 6.0.0)
activemodel (>= 6.0.0)
bindex (>= 0.4.0)
railties (>= 6.0.0)
websocket (1.2.9)
websocket (1.2.11)
websocket-driver (0.7.5)
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5)
Expand All @@ -262,10 +276,13 @@ PLATFORMS
ruby

DEPENDENCIES
bullet
byebug
capybara
coffee-rails
devise
factory_bot_rails
faker
jbuilder
listen
pg
Expand Down
12 changes: 12 additions & 0 deletions NOTES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
## Notes

To test: `bundle exec rspec`

TODO:
- [ ] Add model and system tests
- [ ] Add error handling
- [ ] Decide on ordering of "Interesting Stories" and how far back to display
- [ ] Explore other caching strategies



2 changes: 2 additions & 0 deletions Procfile.dev
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
web: bin/rails server
css: bin/rails tailwindcss:watch
Empty file added app/assets/builds/.keep
Empty file.
1 change: 1 addition & 0 deletions app/assets/config/manifest.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
//= link_tree ../images
//= link_directory ../javascripts .js
//= link_directory ../stylesheets .css
//= link_tree ../builds
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!

def index
@stories = FetchTopStoriesService.call
@interesting_stories = Story.joins(:user_stories).distinct.includes([:users])
end

def mark_as_interesting
story = Story.find_or_create_by(hn_id: params[:hn_id], title: params[:title], url: params[:url])
current_user.user_stories.find_or_create_by(story: story)
redirect_to root_path, notice: 'Story marked as interesting.'
end
end
2 changes: 2 additions & 0 deletions app/helpers/stories_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
module StoriesHelper
end
4 changes: 4 additions & 0 deletions app/models/story.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
class Story < ApplicationRecord
has_many :user_stories
has_many :users, through: :user_stories
end
1 change: 1 addition & 0 deletions app/models/user.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
class User < ApplicationRecord
has_many :user_stories
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable and :omniauthable
devise :database_authenticatable, :registerable,
Expand Down
4 changes: 4 additions & 0 deletions app/models/user_story.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
class UserStory < ApplicationRecord
belongs_to :user
belongs_to :story
end
26 changes: 26 additions & 0 deletions app/services/fetch_top_stories_service.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
class FetchTopStoriesService
require 'net/http'
require 'json'

TOP_STORIES_URL = 'https://hacker-news.firebaseio.com/v0/topstories.json'
STORY_URL = 'https://hacker-news.firebaseio.com/v0/item/'

def self.call
story_ids = fetch_top_story_ids.first(30)
story_ids.map { |id| fetch_story_details(id) }
end

def self.fetch_top_story_ids
response = Net::HTTP.get(URI(TOP_STORIES_URL))
JSON.parse(response)
end

def self.fetch_story_details(story_id)
Rails.cache.fetch("story_#{story_id}", expires_in: 1.day) do
Rails.logger.info "Cache miss for story_id: #{story_id}. Fetching from API."
response = Net::HTTP.get(URI("#{STORY_URL}#{story_id}.json"))
JSON.parse(response)
end
end
end

16 changes: 16 additions & 0 deletions app/views/devise/confirmations/new.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<h2>Resend confirmation instructions</h2>

<%= form_for(resource, as: resource_name, url: confirmation_path(resource_name), html: { method: :post }) do |f| %>
<%= render "devise/shared/error_messages", resource: resource %>

<div class="field">
<%= f.label :email %><br />
<%= f.email_field :email, autofocus: true, autocomplete: "email", value: (resource.pending_reconfirmation? ? resource.unconfirmed_email : resource.email) %>
</div>

<div class="actions">
<%= f.submit "Resend confirmation instructions" %>
</div>
<% end %>

<%= render "devise/shared/links" %>
5 changes: 5 additions & 0 deletions app/views/devise/mailer/confirmation_instructions.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<p>Welcome <%= @email %>!</p>

<p>You can confirm your account email through the link below:</p>

<p><%= link_to 'Confirm my account', confirmation_url(@resource, confirmation_token: @token) %></p>
7 changes: 7 additions & 0 deletions app/views/devise/mailer/email_changed.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<p>Hello <%= @email %>!</p>

<% if @resource.try(:unconfirmed_email?) %>
<p>We're contacting you to notify you that your email is being changed to <%= @resource.unconfirmed_email %>.</p>
<% else %>
<p>We're contacting you to notify you that your email has been changed to <%= @resource.email %>.</p>
<% end %>
3 changes: 3 additions & 0 deletions app/views/devise/mailer/password_change.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<p>Hello <%= @resource.email %>!</p>

<p>We're contacting you to notify you that your password has been changed.</p>
8 changes: 8 additions & 0 deletions app/views/devise/mailer/reset_password_instructions.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<p>Hello <%= @resource.email %>!</p>

<p>Someone has requested a link to change your password. You can do this through the link below.</p>

<p><%= link_to 'Change my password', edit_password_url(@resource, reset_password_token: @token) %></p>

<p>If you didn't request this, please ignore this email.</p>
<p>Your password won't change until you access the link above and create a new one.</p>
7 changes: 7 additions & 0 deletions app/views/devise/mailer/unlock_instructions.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<p>Hello <%= @resource.email %>!</p>

<p>Your account has been locked due to an excessive number of unsuccessful sign in attempts.</p>

<p>Click the link below to unlock your account:</p>

<p><%= link_to 'Unlock my account', unlock_url(@resource, unlock_token: @token) %></p>
25 changes: 25 additions & 0 deletions app/views/devise/passwords/edit.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<h2>Change your password</h2>

<%= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :put }) do |f| %>
<%= render "devise/shared/error_messages", resource: resource %>
<%= f.hidden_field :reset_password_token %>

<div class="field">
<%= f.label :password, "New password" %><br />
<% if @minimum_password_length %>
<em>(<%= @minimum_password_length %> characters minimum)</em><br />
<% end %>
<%= f.password_field :password, autofocus: true, autocomplete: "new-password" %>
</div>

<div class="field">
<%= f.label :password_confirmation, "Confirm new password" %><br />
<%= f.password_field :password_confirmation, autocomplete: "new-password" %>
</div>

<div class="actions">
<%= f.submit "Change my password" %>
</div>
<% end %>

<%= render "devise/shared/links" %>
16 changes: 16 additions & 0 deletions app/views/devise/passwords/new.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<h2>Forgot your password?</h2>

<%= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :post }) do |f| %>
<%= render "devise/shared/error_messages", resource: resource %>

<div class="field">
<%= f.label :email %><br />
<%= f.email_field :email, autofocus: true, autocomplete: "email" %>
</div>

<div class="actions">
<%= f.submit "Send me reset password instructions" %>
</div>
<% end %>

<%= render "devise/shared/links" %>
Loading