diff --git a/.gitignore b/.gitignore
index 82701fed..afde131f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -17,3 +17,4 @@
/yarn-error.log
.byebug_history
+public/
diff --git a/.ruby-version b/.ruby-version
index ef538c28..be94e6f5 100644
--- a/.ruby-version
+++ b/.ruby-version
@@ -1 +1 @@
-3.1.2
+3.2.2
diff --git a/Gemfile b/Gemfile
index 5a8ffc43..045ad029 100644
--- a/Gemfile
+++ b/Gemfile
@@ -20,3 +20,10 @@ gem 'turbolinks'
gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]
gem 'uglifier'
gem 'web-console', group: :development
+gem "font-awesome-sass"
+gem 'turbo-rails'
+gem 'kaminari'
+gem "sidekiq-scheduler", "~> 5.0"
+gem "sidekiq", "~> 7.3"
+
+gem "pagy", "~> 9.0.5"
diff --git a/Gemfile.lock b/Gemfile.lock
index 14ec6457..b8346021 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -91,6 +91,7 @@ GEM
execjs
coffee-script-source (1.12.2)
concurrent-ruby (1.1.10)
+ connection_pool (2.4.1)
crass (1.0.6)
devise (4.8.1)
bcrypt (~> 3.0)
@@ -101,8 +102,15 @@ GEM
diff-lcs (1.5.0)
digest (3.1.0)
erubi (1.11.0)
+ et-orbi (1.2.11)
+ tzinfo
execjs (2.8.1)
ffi (1.15.5)
+ font-awesome-sass (6.5.2)
+ sassc (~> 2.0)
+ fugit (1.11.1)
+ et-orbi (~> 1, >= 1.2.11)
+ raabro (~> 1.4)
globalid (1.0.0)
activesupport (>= 5.0)
i18n (1.12.0)
@@ -110,9 +118,22 @@ GEM
jbuilder (2.11.5)
actionview (>= 5.0.0)
activesupport (>= 5.0.0)
+ kaminari (1.2.2)
+ activesupport (>= 4.1.0)
+ kaminari-actionview (= 1.2.2)
+ kaminari-activerecord (= 1.2.2)
+ kaminari-core (= 1.2.2)
+ kaminari-actionview (1.2.2)
+ actionview
+ kaminari-core (= 1.2.2)
+ kaminari-activerecord (1.2.2)
+ activerecord
+ kaminari-core (= 1.2.2)
+ kaminari-core (1.2.2)
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)
@@ -143,6 +164,7 @@ GEM
mini_portile2 (~> 2.8.0)
racc (~> 1.4)
orm_adapter (0.5.0)
+ pagy (9.0.5)
pg (1.4.3)
pry (0.14.1)
coderay (~> 1.1)
@@ -152,6 +174,7 @@ GEM
public_suffix (5.0.0)
puma (5.6.5)
nio4r (~> 2.0)
+ raabro (1.4.0)
racc (1.6.0)
rack (2.2.4)
rack-test (2.0.2)
@@ -186,6 +209,8 @@ GEM
rb-fsevent (0.11.2)
rb-inotify (0.10.1)
ffi (~> 1.0)
+ redis-client (0.22.2)
+ connection_pool
regexp_parser (2.5.0)
responders (3.0.1)
actionpack (>= 5.0)
@@ -209,6 +234,8 @@ GEM
rspec-support (~> 3.10)
rspec-support (3.11.1)
rubyzip (2.3.2)
+ rufus-scheduler (3.9.1)
+ fugit (~> 1.1, >= 1.1.6)
sass-rails (6.0.0)
sassc-rails (~> 2.1, >= 2.1.1)
sassc (2.4.0)
@@ -224,6 +251,16 @@ GEM
rexml (~> 3.2, >= 3.2.5)
rubyzip (>= 1.2.2, < 3.0)
websocket (~> 1.0)
+ sidekiq (7.3.1)
+ concurrent-ruby (< 2)
+ connection_pool (>= 2.3.0)
+ logger
+ rack (>= 2.2.4)
+ redis-client (>= 0.22.2)
+ sidekiq-scheduler (5.0.6)
+ rufus-scheduler (~> 3.2)
+ sidekiq (>= 6, < 8)
+ tilt (>= 1.4.0, < 3)
spring (4.1.0)
sprockets (4.1.1)
concurrent-ruby (~> 1.0)
@@ -236,6 +273,10 @@ GEM
thor (1.2.1)
tilt (2.0.11)
timeout (0.3.0)
+ turbo-rails (2.0.6)
+ actionpack (>= 6.0.0)
+ activejob (>= 6.0.0)
+ railties (>= 6.0.0)
turbolinks (5.2.1)
turbolinks-source (~> 5.2)
turbolinks-source (5.2.0)
@@ -266,8 +307,11 @@ DEPENDENCIES
capybara
coffee-rails
devise
+ font-awesome-sass
jbuilder
+ kaminari
listen
+ pagy (~> 9.0.5)
pg
pry-rails
puma
@@ -275,14 +319,17 @@ DEPENDENCIES
rspec-rails
sass-rails
selenium-webdriver
+ sidekiq (~> 7.3)
+ sidekiq-scheduler (~> 5.0)
spring
+ turbo-rails
turbolinks
tzinfo-data
uglifier
web-console
RUBY VERSION
- ruby 3.1.2p20
+ ruby 3.2.2p53
BUNDLED WITH
2.3.22
diff --git a/README.md b/README.md
index 500f71a1..a2b32492 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,5 @@
# Top News: Internal Team News Feed
-In order to evaluate your stengths as a developer, we are requesting you complete a brief take-home code challenge that involves some work on the full web stack. We expect this to take 2 to 4 hours of your time. After developing your solution, please submit a Pull Request on Github and we will discuss your code on a screenshare at the next interview.
-
## Introduction
[Hacker News](https://news.ycombinator.com/) is a well-known technology news aggregator service and forum maintained by seed stage investment firm, Y-Combinator. Via Firebase, Y-Combinator provides a simple JSON API to retrieve story information. The API requires no authentication and is documented in a [GitHub repo](https://github.com/HackerNews/API). The two most useful API calls are:
@@ -9,19 +7,26 @@ In order to evaluate your stengths as a developer, we are requesting you complet
* [List of top stories](https://hacker-news.firebaseio.com/v0/topstories.json)
* [Show story details](https://hacker-news.firebaseio.com/v0/item/8863.json)
-Suppose you have a small team of developers who all regularly browse Hacker News for industry insights. This team would like a simple way to flag articles that could be of interest to other team members and publish that list out to the rest of the team. This UI would be deployed for internal use so it would not require a public sign up but would be pre-populated with users who will be team members.
-When a team member signs in, they will see recent news stories and be able to star, flag, highlight, or otherwise mark a story as interesting. A separate list should display all the stories that were deemed interesting and the name of the person who marked it so.
+## Features
+
+* Users can sign in and out. Must be authenticated to view stories.
+* Users visits top news page and sees a list of current top Hacker News stories.
+* The stories available a paginated and a backgrounf job adds new stories every 3 minutes.
+* The user is able to pin a story.
+* The stories pinned by the team members display in both all news and pinned news tabs.
+* Each story should shows the name of the team member who flagged it.
+* Each story allows the member who added it to unpin it.
+
+
+## Installation
-## Requirements
+Run the following commands on your terminal to the app.
+ If ruby version doesn't load properly please run `rbenv local 3.2.2` or equivalent command based your ruby version manager.
-* Users should sign in and out. We have created a User model for you and pre-populated it with several users.
-* Users should come to a page and see a list of current top Hacker News stories.
-* This list does not necessarily need to be the current live list, but it should be a recent and continuously updated list.
-* The number of stories displayed is up to you.
-* The user should be able to star a story. The mechanism and display is up to you: flag, star, upvote, pick, etc. The UX is your choice.
-* The stories chosen by the team members should display. It can be a separate page or the same page, the choice is yours.
-* Each story should show the name of the team member or members who flagged it.
-* As an internal tool for a small team, performance optimization is not a requirement.
-* Be prepared to discuss known performance shortcomings of your solution and potential improvements.
-* UX design here is of little importance. The design can be minimal or it can have zero design at all.
+- `bundle install`
+- `rails db:create`
+- `rails db:migrate`
+- `rails db:seed`
+- `rails server`
+- `POLLING_INTERVAL=3m bundle exec sidekiq`
diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js
index 46b20359..2b03a2e9 100644
--- a/app/assets/javascripts/application.js
+++ b/app/assets/javascripts/application.js
@@ -10,6 +10,42 @@
// Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details
// about supported directives.
//
+// app/assets/javascripts/application.js
//= require rails-ujs
//= require turbolinks
+//= require news_stories
//= require_tree .
+
+document.addEventListener('DOMContentLoaded', function() {
+ console.log("loading application!!!")
+
+ var tabElms = document.querySelectorAll('button[data-bs-toggle="tab"]')
+ tabElms.forEach(function(tabElm) {
+ new bootstrap.Tab(tabElm)
+ })
+
+ // pagination logic
+ document.addEventListener('click', function(event) {
+ if (event.target.closest('.pagination a')) {
+ event.preventDefault()
+ var link = event.target.closest('.pagination a')
+ var url = link.href
+ var tabPane = link.closest('.tab-pane')
+ var tabId = tabPane.id
+
+ fetch(url)
+ .then(response => response.text())
+ .then(html => {
+ var parser = new DOMParser()
+ var doc = parser.parseFromString(html, 'text/html')
+ var newContent = doc.querySelector('#' + tabId)
+
+ if (newContent) {
+ tabPane.innerHTML = newContent.innerHTML
+ history.pushState({}, '', url)
+ }
+ })
+ .catch(error => console.error('Error:', error))
+ }
+ })
+})
\ No newline at end of file
diff --git a/app/assets/javascripts/news_stories.js b/app/assets/javascripts/news_stories.js
new file mode 100644
index 00000000..eecdf8b2
--- /dev/null
+++ b/app/assets/javascripts/news_stories.js
@@ -0,0 +1,92 @@
+function pinStory(storyId, button, event) {
+ console.log("Pinning or Unpinning a story")
+
+ event.preventDefault();
+ fetch(`/news_stories/${storyId}/pin`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content,
+ 'Accept': 'application/json'
+ },
+ body: JSON.stringify({ id: storyId }),
+
+ })
+ .then(response => {
+ if (!response.ok) {
+ throw new Error('Network response was not ok');
+ }
+ return response.text();
+ })
+ .then(() => {
+ let icon = button.querySelector('.unpinned-thumbtack-icon');
+
+ if (icon){
+ icon.classList.replace('unpinned-thumbtack-icon', 'pinned-thumbtack-icon');
+ showFlashMessage("Story successfully pinned!", 'success');
+ } else {
+ icon = button.querySelector('.pinned-thumbtack-icon');
+
+ icon.classList.replace('pinned-thumbtack-icon', 'unpinned-thumbtack-icon');
+ showFlashMessage("Story successfully unpinned!", 'alert');
+ }
+ })
+ .catch(error => {
+ console.error('Error:', error);
+ showFlashMessage('An error occurred while pinning the story.', 'error');
+ });
+
+ function showFlashMessage(message, type) {
+ const flashContainer = document.querySelector('.alert-float') || createFlashContainer();
+
+ const alertClass = getAlertClass(type);
+ const iconClass = getIconClass(type);
+
+ const alertHtml = `
+
+ <%= 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?" }, method: :delete %>