diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 00000000..13566b81
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,8 @@
+# Default ignored files
+/shelf/
+/workspace.xml
+# Editor-based HTTP Client requests
+/httpRequests/
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml
diff --git a/.idea/dataSources.xml b/.idea/dataSources.xml
new file mode 100644
index 00000000..8eadcd12
--- /dev/null
+++ b/.idea/dataSources.xml
@@ -0,0 +1,39 @@
+
+
+
+
+ postgresql
+ true
+ true
+ $PROJECT_DIR$/config/database.yml
+ org.postgresql.Driver
+ jdbc:postgresql://127.0.0.1:5432/topnews_production
+ $ProjectFileDir$
+
+
+ postgresql
+ true
+ true
+ $PROJECT_DIR$/config/database.yml
+ org.postgresql.Driver
+ jdbc:postgresql://127.0.0.1:5432/topnews_development
+ $ProjectFileDir$
+
+
+ postgresql
+ true
+ true
+ $PROJECT_DIR$/config/database.yml
+ org.postgresql.Driver
+ jdbc:postgresql://127.0.0.1:5432/topnews_test
+ $ProjectFileDir$
+
+
+ redis
+ true
+ jdbc.RedisDriver
+ jdbc:redis://localhost:6379/0
+ $ProjectFileDir$
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 00000000..3205808e
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/.idea/topnews.iml b/.idea/topnews.iml
new file mode 100644
index 00000000..641a91f6
--- /dev/null
+++ b/.idea/topnews.iml
@@ -0,0 +1,533 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ file://$MODULE_DIR$/app
+
+
+ file://$MODULE_DIR$/app/assets
+
+
+ file://$MODULE_DIR$/app/channels
+
+
+ file://$MODULE_DIR$/app/controllers
+
+
+ file://$MODULE_DIR$/app/helpers
+
+
+ file://$MODULE_DIR$/app/mailers
+
+
+ file://$MODULE_DIR$/app/models
+
+
+ file://$MODULE_DIR$/app/views
+
+
+ file://$MODULE_DIR$/config
+
+
+ file://$MODULE_DIR$/config/cable.yml
+
+
+ file://$MODULE_DIR$/config/database.yml
+
+
+ file://$MODULE_DIR$/config/environment.rb
+
+
+ file://$MODULE_DIR$/config/environments
+
+
+ file://$MODULE_DIR$/config/initializers
+
+
+ file://$MODULE_DIR$/config/locales
+
+
+ file://$MODULE_DIR$/config/routes
+
+
+ file://$MODULE_DIR$/config/routes.rb
+
+
+ file://$MODULE_DIR$/config
+
+
+ file://$MODULE_DIR$/db
+
+
+ file://$MODULE_DIR$/db/migrate
+
+
+ file://$MODULE_DIR$/db/seeds.rb
+
+
+ file://$MODULE_DIR$/lib
+
+
+ file://$MODULE_DIR$/lib/assets
+
+
+ file://$MODULE_DIR$/lib/tasks
+
+
+ file://$MODULE_DIR$/lib/templates
+
+
+ file://$MODULE_DIR$/log/development.log
+
+
+ file://$MODULE_DIR$/public
+
+
+ file://$MODULE_DIR$/public/javascripts
+
+
+ file://$MODULE_DIR$/public/stylesheets
+
+
+ file://$MODULE_DIR$/tmp
+
+
+ file://$MODULE_DIR$/vendor
+
+
+ file://$MODULE_DIR$/vendor/assets
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 00000000..35eb1ddf
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Gemfile b/Gemfile
index fd2e2b45..1cee0897 100644
--- a/Gemfile
+++ b/Gemfile
@@ -2,22 +2,29 @@ source 'https://rubygems.org'
ruby File.read('.ruby-version').chomp
-gem 'byebug', platforms: [:mri, :mingw, :x64_mingw], group: [:development, :test]
-gem 'capybara', group: [:development, :test]
+gem 'byebug', platforms: %i[mri mingw x64_mingw], group: %i[development test]
+gem 'capybara', group: %i[development test]
gem 'coffee-rails'
gem 'devise'
gem 'factory_bot_rails'
+gem 'httparty'
gem 'jbuilder'
gem 'listen', group: :development
gem 'pg'
gem 'pry-rails'
gem 'puma'
gem 'rails', '~> 7.0.8'
+gem 'redis'
gem 'rspec-rails'
+gem 'rubocop', group: 'development', require: false
gem 'sass-rails'
-gem 'selenium-webdriver', group: [:development, :test]
+gem 'selenium-webdriver', group: %i[development test]
+gem 'sidekiq'
+gem 'sidekiq-cron', '~> 1.2'
+gem 'sidekiq-scheduler'
+gem 'sinatra', require: nil
gem 'spring', group: :development
gem 'turbolinks'
-gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]
+gem 'tzinfo-data', platforms: %i[mingw mswin x64_mingw jruby]
gem 'uglifier'
gem 'web-console', group: :development
diff --git a/Gemfile.lock b/Gemfile.lock
index 7d7a3577..448d61dc 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -68,8 +68,10 @@ GEM
tzinfo (~> 2.0)
addressable (2.8.7)
public_suffix (>= 2.0.2, < 7.0)
+ ast (2.4.2)
base64 (0.2.0)
bcrypt (3.1.20)
+ bigdecimal (3.1.8)
bindex (0.8.1)
builder (3.3.0)
byebug (11.1.3)
@@ -91,7 +93,9 @@ GEM
execjs
coffee-script-source (1.12.2)
concurrent-ruby (1.3.4)
+ connection_pool (2.4.1)
crass (1.0.6)
+ csv (3.3.0)
date (3.3.4)
devise (4.9.4)
bcrypt (~> 3.0)
@@ -101,24 +105,35 @@ GEM
warden (~> 1.2.3)
diff-lcs (1.5.1)
erubi (1.13.0)
+ et-orbi (1.2.11)
+ tzinfo
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)
- ffi (1.17.0)
+ ffi (1.17.0-arm64-darwin)
+ fugit (1.11.1)
+ et-orbi (~> 1, >= 1.2.11)
+ raabro (~> 1.4)
globalid (1.2.1)
activesupport (>= 6.1)
+ httparty (0.22.0)
+ csv
+ mini_mime (>= 1.0.0)
+ multi_xml (>= 0.5.2)
i18n (1.14.5)
concurrent-ruby (~> 1.0)
jbuilder (2.12.0)
actionview (>= 5.0.0)
activesupport (>= 5.0.0)
+ json (2.7.2)
+ language_server-protocol (3.17.0.3)
listen (3.9.0)
rb-fsevent (~> 0.10, >= 0.10.3)
rb-inotify (~> 0.9, >= 0.9.10)
- logger (1.6.0)
+ logger (1.6.1)
loofah (2.22.0)
crass (~> 1.0.2)
nokogiri (>= 1.12.0)
@@ -131,8 +146,11 @@ GEM
matrix (0.4.2)
method_source (1.1.0)
mini_mime (1.1.5)
- mini_portile2 (2.8.7)
minitest (5.25.1)
+ multi_xml (0.7.1)
+ bigdecimal (~> 3.1)
+ mustermann (3.0.3)
+ ruby2_keywords (~> 0.0.1)
net-imap (0.4.14)
date
net-protocol
@@ -143,10 +161,13 @@ GEM
net-smtp (0.5.0)
net-protocol
nio4r (2.7.3)
- nokogiri (1.16.7)
- mini_portile2 (~> 2.8.2)
+ nokogiri (1.16.7-arm64-darwin)
racc (~> 1.4)
orm_adapter (0.5.0)
+ parallel (1.26.3)
+ parser (3.3.5.0)
+ ast (~> 2.4.1)
+ racc
pg (1.5.7)
pry (0.14.2)
coderay (~> 1.1)
@@ -156,8 +177,12 @@ GEM
public_suffix (6.0.1)
puma (6.4.2)
nio4r (~> 2.0)
+ raabro (1.4.0)
racc (1.8.1)
rack (2.2.9)
+ rack-protection (3.2.0)
+ base64 (>= 0.1.0)
+ rack (~> 2.2, >= 2.2.4)
rack-test (2.1.0)
rack (>= 1.3)
rails (7.0.8.4)
@@ -188,10 +213,15 @@ GEM
rake (>= 12.2)
thor (~> 1.0)
zeitwerk (~> 2.5)
+ rainbow (3.1.1)
rake (13.2.1)
rb-fsevent (0.11.2)
rb-inotify (0.11.1)
ffi (~> 1.0)
+ redis (5.3.0)
+ redis-client (>= 0.22.0)
+ redis-client (0.22.2)
+ connection_pool
regexp_parser (2.9.2)
responders (3.1.1)
actionpack (>= 5.2)
@@ -215,7 +245,23 @@ GEM
rspec-mocks (~> 3.13)
rspec-support (~> 3.13)
rspec-support (3.13.1)
+ rubocop (1.66.1)
+ json (~> 2.3)
+ language_server-protocol (>= 3.17.0)
+ parallel (~> 1.10)
+ parser (>= 3.3.0.2)
+ rainbow (>= 2.2.2, < 4.0)
+ regexp_parser (>= 2.4, < 3.0)
+ rubocop-ast (>= 1.32.2, < 2.0)
+ ruby-progressbar (~> 1.7)
+ unicode-display_width (>= 2.4.0, < 3.0)
+ rubocop-ast (1.32.3)
+ parser (>= 3.3.1.0)
+ ruby-progressbar (1.13.0)
+ ruby2_keywords (0.0.5)
rubyzip (2.3.2)
+ rufus-scheduler (3.9.2)
+ fugit (~> 1.1, >= 1.11.1)
sass-rails (6.0.0)
sassc-rails (~> 2.1, >= 2.1.1)
sassc (2.4.0)
@@ -232,6 +278,25 @@ GEM
rexml (~> 3.2, >= 3.2.5)
rubyzip (>= 1.2.2, < 3.0)
websocket (~> 1.0)
+ sidekiq (7.3.2)
+ concurrent-ruby (< 2)
+ connection_pool (>= 2.3.0)
+ logger
+ rack (>= 2.2.4)
+ redis-client (>= 0.22.2)
+ sidekiq-cron (1.12.0)
+ fugit (~> 1.8)
+ globalid (>= 1.0.1)
+ sidekiq (>= 6)
+ sidekiq-scheduler (5.0.6)
+ rufus-scheduler (~> 3.2)
+ sidekiq (>= 6, < 8)
+ tilt (>= 1.4.0, < 3)
+ sinatra (3.2.0)
+ mustermann (~> 3.0)
+ rack (~> 2.2, >= 2.2.4)
+ rack-protection (= 3.2.0)
+ tilt (~> 2.0)
spring (4.2.1)
sprockets (4.2.1)
concurrent-ruby (~> 1.0)
@@ -251,6 +316,7 @@ GEM
concurrent-ruby (~> 1.0)
uglifier (4.2.0)
execjs (>= 0.3.0, < 3)
+ unicode-display_width (2.6.0)
warden (1.2.9)
rack (>= 2.0.9)
web-console (4.2.1)
@@ -267,7 +333,7 @@ GEM
zeitwerk (2.6.17)
PLATFORMS
- ruby
+ arm64-darwin-24
DEPENDENCIES
byebug
@@ -275,15 +341,22 @@ DEPENDENCIES
coffee-rails
devise
factory_bot_rails
+ httparty
jbuilder
listen
pg
pry-rails
puma
rails (~> 7.0.8)
+ redis
rspec-rails
+ rubocop
sass-rails
selenium-webdriver
+ sidekiq
+ sidekiq-cron (~> 1.2)
+ sidekiq-scheduler
+ sinatra
spring
turbolinks
tzinfo-data
@@ -294,4 +367,4 @@ RUBY VERSION
ruby 3.2.3p157
BUNDLED WITH
- 2.3.22
+ 2.4.19
diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js
index 46b20359..427149e9 100644
--- a/app/assets/javascripts/application.js
+++ b/app/assets/javascripts/application.js
@@ -13,3 +13,9 @@
//= require rails-ujs
//= require turbolinks
//= require_tree .
+// app/assets/javascripts/application.js
+
+//= require rails-ujs
+//= require turbolinks
+//= require_tree .
+//= require stories
\ No newline at end of file
diff --git a/app/assets/javascripts/stories.js b/app/assets/javascripts/stories.js
new file mode 100644
index 00000000..49b28ed2
--- /dev/null
+++ b/app/assets/javascripts/stories.js
@@ -0,0 +1,45 @@
+document.addEventListener('DOMContentLoaded', () => {
+ const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
+
+ document.querySelectorAll('.star').forEach(starElement => {
+ starElement.addEventListener('click', async (event) => {
+ const storyId = starElement.getAttribute('data-id');
+ const isStarred = starElement.classList.contains('starred');
+ const response = await fetch(`/stories/${storyId}/${isStarred ? 'unstar' : 'star'}`, {
+ method: 'PATCH',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-CSRF-Token': csrfToken
+ }
+ });
+
+ if (response.ok) {
+ const json = await response.json();
+ if (json.success) {
+ starElement.classList.toggle('starred');
+ const isStarred = starElement.classList.contains('starred');
+ starElement.textContent = isStarred ? '★' : '☆';
+ updateStarredBy(starElement, json.starred_by);
+ window.location.reload();
+ } else {
+ console.error(json.message || 'Failed to star/unstar the story');
+ }
+ } else {
+ console.error('Failed to star/unstar the story');
+ }
+ });
+ });
+
+ function updateStarredBy(starElement, starredBy) {
+ const storyNode = starElement.closest('li');
+ const starredByElement = storyNode.querySelector('.starred-by');
+ if (starredByElement) {
+ if (starredBy.length > 0) {
+ starredByElement.textContent = `Starred by: ${starredBy.join(', ')}`;
+ } else {
+ starredByElement.textContent = 'Starred by: No one yet';
+ }
+ }
+ }
+
+});
\ No newline at end of file
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 1c07694e..542c2e6a 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -1,3 +1,7 @@
class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
+
+ def after_sign_in_path_for(resource)
+ user_root_path
+ end
end
diff --git a/app/controllers/stories_controller.rb b/app/controllers/stories_controller.rb
new file mode 100644
index 00000000..7a231024
--- /dev/null
+++ b/app/controllers/stories_controller.rb
@@ -0,0 +1,64 @@
+class StoriesController < ApplicationController
+ before_action :authenticate_user!
+ skip_before_action :verify_authenticity_token, only: [:star, :unstar]
+
+ def index
+ @starred_stories = StarredStory.includes(:user, :story).map(&:story).uniq
+ @stories = fetch_stories_from_redis
+ @stories = fetch_stories_from_service if @stories.empty?
+ add_starred_data_to_stories(@stories)
+ end
+
+ def star
+ story_id = params[:id]
+ story = Story.find_by(id: story_id)
+
+ if story
+ if current_user.starred_stories.exists?(story_id: story_id)
+ render json: { success: false, message: 'Story already starred' }, status: :unprocessable_entity
+ else
+ current_user.starred_stories.create!(story_id: story_id)
+ render json: { success: true, starred_by: story.starred_by_names, story: story }
+ end
+ else
+ render json: { success: false }, status: :not_found
+ end
+ end
+
+ def unstar
+ story_id = params[:id]
+ starred_story = current_user.starred_stories.find_by(story_id: story_id)
+
+ if starred_story
+ starred_story.destroy
+ story = Story.find_by(id: story_id)
+ render json: { success: true, starred_by: story.starred_by_names, story: story }
+ else
+ render json: { success: false, message: 'Story not starred' }, status: :not_found
+ end
+ end
+
+ private
+
+ def fetch_stories_from_redis
+ stories_json = $redis.lrange(StoryFetcherService::STORY_LIST_KEY, 0, -1)
+ stories_json.map { |story| JSON.parse(story, symbolize_names: true) }
+ end
+
+ def fetch_stories_from_service
+ service = StoryFetcherService.new
+ service.perform
+ end
+
+ def add_starred_data_to_stories(stories)
+ # Get all starred story IDs for the current user
+ starred_story_ids = current_user.starred_stories.pluck(:story_id)
+
+ # Mark stories as starred if they are in the user's starred stories
+ stories.each do |story|
+ story['is_starred'] = starred_story_ids.include?(story['id'].to_s)
+ end
+
+ stories
+ end
+end
\ No newline at end of file
diff --git a/app/helpers/stories_helper.rb b/app/helpers/stories_helper.rb
new file mode 100644
index 00000000..20391ba1
--- /dev/null
+++ b/app/helpers/stories_helper.rb
@@ -0,0 +1,42 @@
+module StoriesHelper
+ def story_url(story)
+ fetch_story_attribute(story, :url)
+ end
+
+ def story_title(story)
+ fetch_story_attribute(story, :title)
+ end
+
+ def story_author(story)
+ fetch_story_attribute(story, :author)
+ end
+
+ def story_type(story)
+ fetch_story_attribute(story, :type)
+ end
+
+ def story_published_at(story)
+ published_at = fetch_story_attribute(story, :published_at)
+ story_published_time = if published_at.is_a?(Integer)
+ Time.at(published_at)
+ else
+ Time.parse(published_at.to_s)
+ end
+ story_published_time.strftime('%B %d, %Y %I:%M %p %Z')
+ end
+
+ def story_starred?(story, user)
+ user.starred_stories.exists?(story_id: story[:id])
+ end
+
+ def story_starred_by(story)
+ starred_stories = StarredStory.includes(:user).where(story_id: story[:id])
+ starred_stories.map { |star| "#{star.user.first_name} #{star.user.last_name}" }.join(', ')
+ end
+
+ private
+
+ def fetch_story_attribute(story, attribute)
+ story.respond_to?(attribute) ? story.public_send(attribute) : story[attribute]
+ end
+end
\ No newline at end of file
diff --git a/app/models/starred_story.rb b/app/models/starred_story.rb
new file mode 100644
index 00000000..da78d97c
--- /dev/null
+++ b/app/models/starred_story.rb
@@ -0,0 +1,7 @@
+class StarredStory < ApplicationRecord
+ belongs_to :user
+ belongs_to :story
+
+ validates :story_id, presence: true
+ validates :user_id, uniqueness: { scope: :story_id }
+end
diff --git a/app/models/story.rb b/app/models/story.rb
new file mode 100644
index 00000000..3dadda01
--- /dev/null
+++ b/app/models/story.rb
@@ -0,0 +1,7 @@
+class Story < ApplicationRecord
+ has_many :starred_stories, dependent: :destroy
+
+ def starred_by_names
+ starred_stories.joins(:user).pluck('users.first_name', 'users.last_name').map { |name| name.join(' ') }
+ end
+end
diff --git a/app/models/user.rb b/app/models/user.rb
index b2091f9a..f7adc1ab 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -1,6 +1,14 @@
+# file: user.rb
class User < ApplicationRecord
- # Include default devise modules. Others available are:
- # :confirmable, :lockable, :timeoutable and :omniauthable
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable
-end
+
+ has_many :starred_stories, dependent: :destroy
+ has_many :stories, through: :starred_stories
+
+ validates :email, presence: true
+
+ def admin?
+ admin
+ end
+end
\ No newline at end of file
diff --git a/app/services/story_fetcher_service.rb b/app/services/story_fetcher_service.rb
new file mode 100644
index 00000000..e782e46c
--- /dev/null
+++ b/app/services/story_fetcher_service.rb
@@ -0,0 +1,67 @@
+class StoryFetcherService
+ require 'httparty'
+
+ STORY_LIST_KEY = 'stories:top_news'.freeze
+ TOP_STORIES_URL = 'https://hacker-news.firebaseio.com/v0/topstories.json'.freeze
+ STORY_DETAILS_URL = 'https://hacker-news.firebaseio.com/v0/item/%{id}.json'.freeze
+
+ def self.perform
+ new.perform
+ end
+
+ def perform
+ stories = fetch_top_stories
+ store_stories_in_redis(stories)
+ store_stories_in_db(stories)
+ end
+
+ private
+
+ def fetch_top_stories(limit = 10)
+ story_ids = fetch_top_story_ids.take(limit)
+ story_ids.map { |id| fetch_story_details(id) }
+ end
+
+ def fetch_top_story_ids
+ response = HTTParty.get(TOP_STORIES_URL)
+ response.parsed_response
+ end
+
+ def fetch_story_details(id)
+ response = HTTParty.get(STORY_DETAILS_URL % { id: id })
+ transform_story_data(response.parsed_response)
+ end
+
+ def transform_story_data(story_data)
+ {
+ id: story_data['id'],
+ title: story_data['title'],
+ author: story_data['by'],
+ type: story_data['type'],
+ published_at: story_data['time'],
+ url: story_data['url']
+ }
+ end
+
+ def store_stories_in_redis(stories)
+ $redis.multi do
+ $redis.del(STORY_LIST_KEY)
+ stories.each do |story|
+ $redis.rpush(STORY_LIST_KEY, story.to_json)
+ end
+ end
+ end
+
+ def store_stories_in_db(stories)
+ stories.each do |story|
+ db_story = Story.find_or_initialize_by(id: story[:id])
+ db_story.update(
+ title: story[:title],
+ url: story[:url],
+ author: story[:author],
+ story_type: story[:type],
+ published_at: Time.at(story[:published_at])
+ )
+ end
+ end
+end
diff --git a/app/views/devise/confirmations/new.html.erb b/app/views/devise/confirmations/new.html.erb
new file mode 100644
index 00000000..b12dd0cb
--- /dev/null
+++ b/app/views/devise/confirmations/new.html.erb
@@ -0,0 +1,16 @@
+
Resend confirmation instructions
+
+<%= form_for(resource, as: resource_name, url: confirmation_path(resource_name), html: { method: :post }) do |f| %>
+ <%= render "devise/shared/error_messages", resource: resource %>
+
+
+ <%= f.label :email %>
+ <%= f.email_field :email, autofocus: true, autocomplete: "email", value: (resource.pending_reconfirmation? ? resource.unconfirmed_email : resource.email) %>
+
+
+
+ <%= f.submit "Resend confirmation instructions" %>
+
+<% end %>
+
+<%= render "devise/shared/links" %>
diff --git a/app/views/devise/mailer/confirmation_instructions.html.erb b/app/views/devise/mailer/confirmation_instructions.html.erb
new file mode 100644
index 00000000..dc55f64f
--- /dev/null
+++ b/app/views/devise/mailer/confirmation_instructions.html.erb
@@ -0,0 +1,5 @@
+Welcome <%= @email %>!
+
+You can confirm your account email through the link below:
+
+<%= link_to 'Confirm my account', confirmation_url(@resource, confirmation_token: @token) %>
diff --git a/app/views/devise/mailer/email_changed.html.erb b/app/views/devise/mailer/email_changed.html.erb
new file mode 100644
index 00000000..32f4ba80
--- /dev/null
+++ b/app/views/devise/mailer/email_changed.html.erb
@@ -0,0 +1,7 @@
+Hello <%= @email %>!
+
+<% if @resource.try(:unconfirmed_email?) %>
+ We're contacting you to notify you that your email is being changed to <%= @resource.unconfirmed_email %>.
+<% else %>
+ We're contacting you to notify you that your email has been changed to <%= @resource.email %>.
+<% end %>
diff --git a/app/views/devise/mailer/password_change.html.erb b/app/views/devise/mailer/password_change.html.erb
new file mode 100644
index 00000000..b41daf47
--- /dev/null
+++ b/app/views/devise/mailer/password_change.html.erb
@@ -0,0 +1,3 @@
+Hello <%= @resource.email %>!
+
+We're contacting you to notify you that your password has been changed.
diff --git a/app/views/devise/mailer/reset_password_instructions.html.erb b/app/views/devise/mailer/reset_password_instructions.html.erb
new file mode 100644
index 00000000..f667dc12
--- /dev/null
+++ b/app/views/devise/mailer/reset_password_instructions.html.erb
@@ -0,0 +1,8 @@
+Hello <%= @resource.email %>!
+
+Someone has requested a link to change your password. You can do this through the link below.
+
+<%= link_to 'Change my password', edit_password_url(@resource, reset_password_token: @token) %>
+
+If you didn't request this, please ignore this email.
+Your password won't change until you access the link above and create a new one.
diff --git a/app/views/devise/mailer/unlock_instructions.html.erb b/app/views/devise/mailer/unlock_instructions.html.erb
new file mode 100644
index 00000000..41e148bf
--- /dev/null
+++ b/app/views/devise/mailer/unlock_instructions.html.erb
@@ -0,0 +1,7 @@
+Hello <%= @resource.email %>!
+
+Your account has been locked due to an excessive number of unsuccessful sign in attempts.
+
+Click the link below to unlock your account:
+
+<%= link_to 'Unlock my account', unlock_url(@resource, unlock_token: @token) %>
diff --git a/app/views/devise/passwords/edit.html.erb b/app/views/devise/passwords/edit.html.erb
new file mode 100644
index 00000000..5fbb9ff0
--- /dev/null
+++ b/app/views/devise/passwords/edit.html.erb
@@ -0,0 +1,25 @@
+Change your password
+
+<%= 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 %>
+
+
+ <%= f.label :password, "New password" %>
+ <% if @minimum_password_length %>
+ (<%= @minimum_password_length %> characters minimum)
+ <% end %>
+ <%= f.password_field :password, autofocus: true, autocomplete: "new-password" %>
+
+
+
+ <%= f.label :password_confirmation, "Confirm new password" %>
+ <%= f.password_field :password_confirmation, autocomplete: "new-password" %>
+
+
+
+ <%= f.submit "Change my password" %>
+
+<% end %>
+
+<%= render "devise/shared/links" %>
diff --git a/app/views/devise/passwords/new.html.erb b/app/views/devise/passwords/new.html.erb
new file mode 100644
index 00000000..9b486b81
--- /dev/null
+++ b/app/views/devise/passwords/new.html.erb
@@ -0,0 +1,16 @@
+Forgot your password?
+
+<%= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :post }) do |f| %>
+ <%= render "devise/shared/error_messages", resource: resource %>
+
+
+ <%= f.label :email %>
+ <%= f.email_field :email, autofocus: true, autocomplete: "email" %>
+
+
+
+ <%= f.submit "Send me reset password instructions" %>
+
+<% end %>
+
+<%= render "devise/shared/links" %>
diff --git a/app/views/devise/registrations/edit.html.erb b/app/views/devise/registrations/edit.html.erb
new file mode 100644
index 00000000..b82e3365
--- /dev/null
+++ b/app/views/devise/registrations/edit.html.erb
@@ -0,0 +1,43 @@
+Edit <%= resource_name.to_s.humanize %>
+
+<%= form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put }) do |f| %>
+ <%= render "devise/shared/error_messages", resource: resource %>
+
+
+ <%= f.label :email %>
+ <%= f.email_field :email, autofocus: true, autocomplete: "email" %>
+
+
+ <% if devise_mapping.confirmable? && resource.pending_reconfirmation? %>
+ Currently waiting confirmation for: <%= resource.unconfirmed_email %>
+ <% end %>
+
+
+ <%= f.label :password %> (leave blank if you don't want to change it)
+ <%= f.password_field :password, autocomplete: "new-password" %>
+ <% if @minimum_password_length %>
+
+ <%= @minimum_password_length %> characters minimum
+ <% end %>
+
+
+
+ <%= f.label :password_confirmation %>
+ <%= f.password_field :password_confirmation, autocomplete: "new-password" %>
+
+
+
+ <%= f.label :current_password %> (we need your current password to confirm your changes)
+ <%= f.password_field :current_password, autocomplete: "current-password" %>
+
+
+
+ <%= f.submit "Update" %>
+
+<% end %>
+
+Cancel my account
+
+Unhappy? <%= button_to "Cancel my account", registration_path(resource_name), data: { confirm: "Are you sure?", turbo_confirm: "Are you sure?" }, method: :delete %>
+
+<%= link_to "Back", :back %>
diff --git a/app/views/devise/registrations/new.html.erb b/app/views/devise/registrations/new.html.erb
new file mode 100644
index 00000000..d655b66f
--- /dev/null
+++ b/app/views/devise/registrations/new.html.erb
@@ -0,0 +1,29 @@
+Sign up
+
+<%= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| %>
+ <%= render "devise/shared/error_messages", resource: resource %>
+
+
+ <%= f.label :email %>
+ <%= f.email_field :email, autofocus: true, autocomplete: "email" %>
+
+
+
+ <%= f.label :password %>
+ <% if @minimum_password_length %>
+ (<%= @minimum_password_length %> characters minimum)
+ <% end %>
+ <%= f.password_field :password, autocomplete: "new-password" %>
+
+
+
+ <%= f.label :password_confirmation %>
+ <%= f.password_field :password_confirmation, autocomplete: "new-password" %>
+
+
+
+ <%= f.submit "Sign up" %>
+
+<% end %>
+
+<%= render "devise/shared/links" %>
diff --git a/app/views/devise/sessions/new.html.erb b/app/views/devise/sessions/new.html.erb
new file mode 100644
index 00000000..5ede9648
--- /dev/null
+++ b/app/views/devise/sessions/new.html.erb
@@ -0,0 +1,26 @@
+Log in
+
+<%= form_for(resource, as: resource_name, url: session_path(resource_name)) do |f| %>
+
+ <%= f.label :email %>
+ <%= f.email_field :email, autofocus: true, autocomplete: "email" %>
+
+
+
+ <%= f.label :password %>
+ <%= f.password_field :password, autocomplete: "current-password" %>
+
+
+ <% if devise_mapping.rememberable? %>
+
+ <%= f.check_box :remember_me %>
+ <%= f.label :remember_me %>
+
+ <% end %>
+
+
+ <%= f.submit "Log in" %>
+
+<% end %>
+
+<%= render "devise/shared/links" %>
diff --git a/app/views/devise/shared/_error_messages.html.erb b/app/views/devise/shared/_error_messages.html.erb
new file mode 100644
index 00000000..cabfe307
--- /dev/null
+++ b/app/views/devise/shared/_error_messages.html.erb
@@ -0,0 +1,15 @@
+<% if resource.errors.any? %>
+
+
+ <%= I18n.t("errors.messages.not_saved",
+ count: resource.errors.count,
+ resource: resource.class.model_name.human.downcase)
+ %>
+
+
+ <% resource.errors.full_messages.each do |message| %>
+ <%= message %>
+ <% end %>
+
+
+<% end %>
diff --git a/app/views/devise/shared/_links.html.erb b/app/views/devise/shared/_links.html.erb
new file mode 100644
index 00000000..7a75304b
--- /dev/null
+++ b/app/views/devise/shared/_links.html.erb
@@ -0,0 +1,25 @@
+<%- if controller_name != 'sessions' %>
+ <%= link_to "Log in", new_session_path(resource_name) %>
+<% end %>
+
+<%- if devise_mapping.registerable? && controller_name != 'registrations' %>
+ <%= link_to "Sign up", new_registration_path(resource_name) %>
+<% end %>
+
+<%- if devise_mapping.recoverable? && controller_name != 'passwords' && controller_name != 'registrations' %>
+ <%= link_to "Forgot your password?", new_password_path(resource_name) %>
+<% end %>
+
+<%- if devise_mapping.confirmable? && controller_name != 'confirmations' %>
+ <%= link_to "Didn't receive confirmation instructions?", new_confirmation_path(resource_name) %>
+<% end %>
+
+<%- if devise_mapping.lockable? && resource_class.unlock_strategy_enabled?(:email) && controller_name != 'unlocks' %>
+ <%= link_to "Didn't receive unlock instructions?", new_unlock_path(resource_name) %>
+<% end %>
+
+<%- if devise_mapping.omniauthable? %>
+ <%- resource_class.omniauth_providers.each do |provider| %>
+ <%= button_to "Sign in with #{OmniAuth::Utils.camelize(provider)}", omniauth_authorize_path(resource_name, provider), data: { turbo: false } %>
+ <% end %>
+<% end %>
diff --git a/app/views/devise/unlocks/new.html.erb b/app/views/devise/unlocks/new.html.erb
new file mode 100644
index 00000000..ffc34de8
--- /dev/null
+++ b/app/views/devise/unlocks/new.html.erb
@@ -0,0 +1,16 @@
+Resend unlock instructions
+
+<%= form_for(resource, as: resource_name, url: unlock_path(resource_name), html: { method: :post }) do |f| %>
+ <%= render "devise/shared/error_messages", resource: resource %>
+
+
+ <%= f.label :email %>
+ <%= f.email_field :email, autofocus: true, autocomplete: "email" %>
+
+
+
+ <%= f.submit "Resend unlock instructions" %>
+
+<% end %>
+
+<%= render "devise/shared/links" %>
diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb
index 331a7ed0..41b6ba1d 100644
--- a/app/views/layouts/application.html.erb
+++ b/app/views/layouts/application.html.erb
@@ -1,19 +1,97 @@
-
+
-
-
- Top News
- <%= csrf_meta_tags %>
- <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
- <%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>
-
-
-
- <%= notice %>
-
-
- <%= alert %>
-
- <%= yield %>
-
-
+
+
+ Top News
+ <%= csrf_meta_tags %>
+ <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
+ <%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>
+
+
+
+
+
Top News
+
+
+
+
+
<%= notice %>
+
<%= alert %>
+ <%= yield %>
+
+
+