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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@
/yarn-error.log

.byebug_history
public/
2 changes: 1 addition & 1 deletion .ruby-version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
3.1.2
3.2.2
7 changes: 7 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -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"
49 changes: 48 additions & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -101,18 +102,38 @@ 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)
concurrent-ruby (~> 1.0)
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)
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -266,23 +307,29 @@ DEPENDENCIES
capybara
coffee-rails
devise
font-awesome-sass
jbuilder
kaminari
listen
pagy (~> 9.0.5)
pg
pry-rails
puma
rails (~> 7.0.3)
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
35 changes: 20 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,27 +1,32 @@
# 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:

* [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`
36 changes: 36 additions & 0 deletions app/assets/javascripts/application.js
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
})
})
92 changes: 92 additions & 0 deletions app/assets/javascripts/news_stories.js
Original file line number Diff line number Diff line change
@@ -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 = `
<div class="alert ${alertClass} alert-dismissible fade show" role="alert">
<i class="${iconClass} alert-icon"></i>
<strong>${type.charAt(0).toUpperCase() + type.slice(1)}:</strong> ${message}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
`;

flashContainer.insertAdjacentHTML('beforeend', alertHtml);

setTimeout(() => {
const alertElement = flashContainer.lastElementChild;
if (alertElement) {
alertElement.remove();
}
if (flashContainer.children.length === 0) {
flashContainer.remove();
}
}, 5000);
}

function createFlashContainer() {
const container = document.createElement('div');
container.className = 'alert-float';
document.body.insertBefore(container, document.body.firstChild);
return container;
}

function getAlertClass(type) {
switch (type) {
case 'success': return 'alert-success';
case 'error': return 'alert-danger';
case 'alert': return 'alert-warning';
case 'notice': return 'alert-info';
default: return 'alert-secondary';
}
}

function getIconClass(type) {
switch (type) {
case 'success': return 'fas fa-check-circle';
case 'error': return 'fas fa-exclamation-circle';
case 'alert': return 'fas fa-exclamation-triangle';
case 'notice': return 'fas fa-info-circle';
default: return 'fas fa-bell';
}
}
}
15 changes: 0 additions & 15 deletions app/assets/stylesheets/application.css

This file was deleted.

Loading