├── log
└── .keep
├── storage
└── .keep
├── tmp
└── .keep
├── vendor
└── .keep
├── lib
├── assets
│ └── .keep
└── tasks
│ └── .keep
├── public
├── favicon.ico
├── apple-touch-icon.png
├── apple-touch-icon-precomposed.png
├── robots.txt
├── 500.html
├── 422.html
└── 404.html
├── test
├── helpers
│ └── .keep
├── mailers
│ └── .keep
├── models
│ ├── .keep
│ ├── item_test.rb
│ ├── user_test.rb
│ ├── ask_item_test.rb
│ ├── job_item_test.rb
│ ├── new_item_test.rb
│ ├── top_item_test.rb
│ └── show_item_test.rb
├── system
│ └── .keep
├── controllers
│ └── .keep
├── fixtures
│ ├── .keep
│ ├── files
│ │ └── .keep
│ ├── top_items.yml
│ ├── ask_items.yml
│ ├── job_items.yml
│ ├── new_items.yml
│ ├── show_items.yml
│ ├── users.yml
│ └── items.yml
├── integration
│ └── .keep
├── jobs
│ ├── load_ask_item_job_test.rb
│ ├── load_job_item_job_test.rb
│ ├── load_new_item_job_test.rb
│ ├── load_top_item_job_test.rb
│ ├── load_ask_items_job_test.rb
│ ├── load_job_items_job_test.rb
│ ├── load_news_items_job_test.rb
│ ├── load_show_item_job_test.rb
│ ├── load_show_items_job_test.rb
│ ├── load_top_items_job_test.rb
│ └── load_user_details_job_test.rb
├── workers
│ └── load_item_details_job_test.rb
├── application_system_test_case.rb
└── test_helper.rb
├── .ruby-version
├── app
├── assets
│ ├── images
│ │ ├── .keep
│ │ ├── train_192.png
│ │ ├── train_48.png
│ │ ├── train_512.png
│ │ ├── cloud-download.svg
│ │ └── cloud-check.svg
│ ├── config
│ │ └── manifest.js
│ └── stylesheets
│ │ ├── sass
│ │ ├── grid
│ │ │ ├── _all.sass
│ │ │ └── tiles.sass
│ │ ├── layout
│ │ │ ├── _all.sass
│ │ │ ├── footer.sass
│ │ │ ├── section.sass
│ │ │ └── hero.sass
│ │ ├── base
│ │ │ ├── _all.sass
│ │ │ ├── minireset.sass
│ │ │ └── generic.sass
│ │ ├── utilities
│ │ │ ├── animations.sass
│ │ │ ├── _all.sass
│ │ │ ├── controls.sass
│ │ │ ├── initial-variables.sass
│ │ │ ├── functions.sass
│ │ │ └── derived-variables.sass
│ │ ├── components
│ │ │ ├── _all.sass
│ │ │ ├── media.sass
│ │ │ ├── list.sass
│ │ │ ├── menu.sass
│ │ │ ├── level.sass
│ │ │ ├── card.sass
│ │ │ ├── breadcrumb.sass
│ │ │ ├── dropdown.sass
│ │ │ ├── message.sass
│ │ │ ├── panel.sass
│ │ │ ├── modal.sass
│ │ │ ├── pagination.sass
│ │ │ └── tabs.sass
│ │ └── elements
│ │ │ ├── _all.sass
│ │ │ ├── icon.sass
│ │ │ ├── container.sass
│ │ │ ├── box.sass
│ │ │ ├── other.sass
│ │ │ ├── notification.sass
│ │ │ ├── image.sass
│ │ │ ├── title.sass
│ │ │ ├── progress.sass
│ │ │ ├── tag.sass
│ │ │ ├── table.sass
│ │ │ └── content.sass
│ │ └── bulma.sass
├── models
│ ├── concerns
│ │ └── .keep
│ ├── ask_item.rb
│ ├── job_item.rb
│ ├── new_item.rb
│ ├── show_item.rb
│ ├── top_item.rb
│ ├── application_record.rb
│ ├── user.rb
│ └── item.rb
├── controllers
│ ├── concerns
│ │ └── .keep
│ ├── application_controller.rb
│ ├── items_controller.rb
│ ├── users_controller.rb
│ ├── service_worker_controller.rb
│ ├── tops_controller.rb
│ ├── news_controller.rb
│ ├── shows_controller.rb
│ ├── jobs_controller.rb
│ └── asks_controller.rb
├── views
│ ├── layouts
│ │ ├── mailer.text.erb
│ │ ├── mailer.html.erb
│ │ ├── _page_navigation.html.erb
│ │ └── application.html.erb
│ ├── service_worker
│ │ ├── offline.html.erb
│ │ ├── manifest.json.erb
│ │ └── service_worker.js.erb
│ ├── ask_items
│ │ └── _ask_item.html.erb
│ ├── new_items
│ │ └── _new_item.html.erb
│ ├── job_items
│ │ └── _job_item.html.erb
│ ├── show_items
│ │ └── _show_item.erb
│ ├── top_items
│ │ └── _top_item.html.erb
│ ├── users
│ │ ├── _metadata.html.erb
│ │ └── show.html.erb
│ ├── news
│ │ └── show.html.erb
│ ├── asks
│ │ └── show.html.erb
│ ├── jobs
│ │ └── show.html.erb
│ ├── tops
│ │ └── show.html.erb
│ ├── shows
│ │ └── show.html.erb
│ └── items
│ │ ├── _comments_header.html.erb
│ │ ├── show.html.erb
│ │ ├── _comments.html.erb
│ │ └── _item.html.erb
├── helpers
│ ├── application_helper.rb
│ └── items_helper.rb
├── jobs
│ ├── application_job.rb
│ ├── load_ask_items_job.rb
│ ├── load_job_items_job.rb
│ ├── load_new_items_job.rb
│ ├── load_top_items_job.rb
│ ├── load_show_items_job.rb
│ ├── load_user_details_job.rb
│ ├── load_ask_item_job.rb
│ ├── load_job_item_job.rb
│ ├── load_new_item_job.rb
│ ├── load_top_item_job.rb
│ └── load_show_item_job.rb
├── channels
│ ├── application_cable
│ │ ├── channel.rb
│ │ └── connection.rb
│ ├── comments_channel.rb
│ ├── user_channel.rb
│ ├── item_channel.rb
│ ├── items_list_channel.rb
│ ├── ask_item_channel.rb
│ ├── new_item_channel.rb
│ ├── top_item_channel.rb
│ ├── job_item_channel.rb
│ └── show_item_channel.rb
├── mailers
│ └── application_mailer.rb
├── javascript
│ ├── channels
│ │ ├── index.js
│ │ └── consumer.js
│ ├── controllers
│ │ ├── bulma_navbar_controller.js
│ │ ├── index.js
│ │ ├── user_controller.js
│ │ ├── toggle_controller.js
│ │ ├── comments_controller.js
│ │ ├── ask_item_controller.js
│ │ ├── item_location_controller.js
│ │ ├── items_controller.js
│ │ ├── item_controller.js
│ │ ├── service_worker_controller.js
│ │ └── swipable_controller.js
│ ├── cables
│ │ └── cable.js
│ └── packs
│ │ └── application.js
└── workers
│ └── load_item_details_worker.rb
├── .browserslistrc
├── .env
├── .postcssrc.yml
├── config
├── webpack
│ ├── environment.js
│ ├── test.js
│ ├── production.js
│ └── development.js
├── spring.rb
├── environment.rb
├── initializers
│ ├── mime_types.rb
│ ├── application_controller_renderer.rb
│ ├── cookies_serializer.rb
│ ├── filter_parameter_logging.rb
│ ├── permissions_policy.rb
│ ├── wrap_parameters.rb
│ ├── backtrace_silencers.rb
│ ├── assets.rb
│ ├── inflections.rb
│ ├── content_security_policy.rb
│ └── new_framework_defaults_6_1.rb
├── boot.rb
├── cable.yml
├── schedule.rb
├── credentials.yml.enc
├── routes.rb
├── database.yml
├── application.rb
├── locales
│ └── en.yml
├── storage.yml
├── puma.rb
├── deploy.rb
├── webpacker.yml
├── deploy
│ ├── staging.rb
│ └── production.rb
└── environments
│ ├── test.rb
│ └── development.rb
├── bin
├── rake
├── rails
├── spring
├── webpack
├── webpack-dev-server
├── yarn
├── setup
└── bundle
├── README.md
├── Procfile
├── config.ru
├── db
├── migrate
│ ├── 20181206162906_add_kid_location_to_item.rb
│ ├── 20190116193355_add_loading_details_to_item.rb
│ ├── 20181116153242_create_top_items.rb
│ ├── 20181204140608_create_new_items.rb
│ ├── 20181205143748_create_ask_items.rb
│ ├── 20181205160529_create_job_items.rb
│ ├── 20181205141656_create_show_items.rb
│ ├── 20181207133624_create_users.rb
│ └── 20181114014928_create_items.rb
├── seeds.rb
└── schema.rb
├── Rakefile
├── postcss.config.js
├── package.json
├── .gitignore
├── LICENSE
├── Capfile
├── babel.config.js
└── Gemfile
/log/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/storage/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tmp/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vendor/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/lib/assets/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/lib/tasks/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/helpers/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/mailers/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/models/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/system/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.ruby-version:
--------------------------------------------------------------------------------
1 | 2.7.2
2 |
--------------------------------------------------------------------------------
/app/assets/images/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/controllers/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/fixtures/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/integration/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.browserslistrc:
--------------------------------------------------------------------------------
1 | defaults
2 |
--------------------------------------------------------------------------------
/app/models/concerns/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/apple-touch-icon.png:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/fixtures/files/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/controllers/concerns/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/apple-touch-icon-precomposed.png:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.env:
--------------------------------------------------------------------------------
1 | REDIS_SERVER=redis://localhost:6379
2 |
--------------------------------------------------------------------------------
/app/views/layouts/mailer.text.erb:
--------------------------------------------------------------------------------
1 | <%= yield %>
2 |
--------------------------------------------------------------------------------
/app/assets/config/manifest.js:
--------------------------------------------------------------------------------
1 | //= link_tree ../images
2 |
--------------------------------------------------------------------------------
/app/helpers/application_helper.rb:
--------------------------------------------------------------------------------
1 | module ApplicationHelper
2 | end
3 |
--------------------------------------------------------------------------------
/.postcssrc.yml:
--------------------------------------------------------------------------------
1 | plugins:
2 | postcss-import: {}
3 | postcss-cssnext: {}
4 |
--------------------------------------------------------------------------------
/app/jobs/application_job.rb:
--------------------------------------------------------------------------------
1 | class ApplicationJob < ActiveJob::Base
2 | end
3 |
--------------------------------------------------------------------------------
/app/models/ask_item.rb:
--------------------------------------------------------------------------------
1 | class AskItem < ApplicationRecord
2 | belongs_to :item
3 | end
4 |
--------------------------------------------------------------------------------
/app/models/job_item.rb:
--------------------------------------------------------------------------------
1 | class JobItem < ApplicationRecord
2 | belongs_to :item
3 | end
4 |
--------------------------------------------------------------------------------
/app/models/new_item.rb:
--------------------------------------------------------------------------------
1 | class NewItem < ApplicationRecord
2 | belongs_to :item
3 | end
4 |
--------------------------------------------------------------------------------
/app/models/show_item.rb:
--------------------------------------------------------------------------------
1 | class ShowItem < ApplicationRecord
2 | belongs_to :item
3 | end
4 |
--------------------------------------------------------------------------------
/app/models/top_item.rb:
--------------------------------------------------------------------------------
1 | class TopItem < ApplicationRecord
2 | belongs_to :item
3 | end
4 |
--------------------------------------------------------------------------------
/app/assets/images/train_192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/johnbeatty/hnpwa-app/HEAD/app/assets/images/train_192.png
--------------------------------------------------------------------------------
/app/assets/images/train_48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/johnbeatty/hnpwa-app/HEAD/app/assets/images/train_48.png
--------------------------------------------------------------------------------
/app/assets/images/train_512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/johnbeatty/hnpwa-app/HEAD/app/assets/images/train_512.png
--------------------------------------------------------------------------------
/app/assets/stylesheets/sass/grid/_all.sass:
--------------------------------------------------------------------------------
1 | @charset "utf-8"
2 |
3 | @import "columns.sass"
4 | @import "tiles.sass"
5 |
--------------------------------------------------------------------------------
/app/models/application_record.rb:
--------------------------------------------------------------------------------
1 | class ApplicationRecord < ActiveRecord::Base
2 | self.abstract_class = true
3 | end
4 |
--------------------------------------------------------------------------------
/config/webpack/environment.js:
--------------------------------------------------------------------------------
1 | const { environment } = require('@rails/webpacker')
2 |
3 | module.exports = environment
4 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
2 |
--------------------------------------------------------------------------------
/app/channels/application_cable/channel.rb:
--------------------------------------------------------------------------------
1 | module ApplicationCable
2 | class Channel < ActionCable::Channel::Base
3 | end
4 | end
5 |
--------------------------------------------------------------------------------
/config/spring.rb:
--------------------------------------------------------------------------------
1 | Spring.watch(
2 | ".ruby-version",
3 | ".rbenv-vars",
4 | "tmp/restart.txt",
5 | "tmp/caching-dev.txt"
6 | )
7 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/sass/layout/_all.sass:
--------------------------------------------------------------------------------
1 | @charset "utf-8"
2 |
3 | @import "hero.sass"
4 | @import "section.sass"
5 | @import "footer.sass"
6 |
--------------------------------------------------------------------------------
/app/channels/application_cable/connection.rb:
--------------------------------------------------------------------------------
1 | module ApplicationCable
2 | class Connection < ActionCable::Connection::Base
3 | end
4 | end
5 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/sass/base/_all.sass:
--------------------------------------------------------------------------------
1 | @charset "utf-8"
2 |
3 | @import "minireset.sass"
4 | @import "generic.sass"
5 | @import "helpers.sass"
6 |
--------------------------------------------------------------------------------
/app/mailers/application_mailer.rb:
--------------------------------------------------------------------------------
1 | class ApplicationMailer < ActionMailer::Base
2 | default from: 'from@example.com'
3 | layout 'mailer'
4 | end
5 |
--------------------------------------------------------------------------------
/app/controllers/application_controller.rb:
--------------------------------------------------------------------------------
1 | ITEMS_PER_PAGE ||= 32
2 | FIRST_PAGE ||= 0
3 |
4 | class ApplicationController < ActionController::Base
5 | end
6 |
--------------------------------------------------------------------------------
/bin/rake:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | load File.expand_path("spring", __dir__)
3 | require_relative "../config/boot"
4 | require "rake"
5 | Rake.application.run
6 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Hacker News Progressive Web App
2 |
3 | This is an implementation the Hacker News Progressive Web App, built entirely out of Ruby on Rails and Stimulus.js
--------------------------------------------------------------------------------
/app/assets/stylesheets/sass/utilities/animations.sass:
--------------------------------------------------------------------------------
1 | @keyframes spinAround
2 | from
3 | transform: rotate(0deg)
4 | to
5 | transform: rotate(359deg)
6 |
--------------------------------------------------------------------------------
/app/controllers/items_controller.rb:
--------------------------------------------------------------------------------
1 | class ItemsController < ApplicationController
2 |
3 | def show
4 | @item = Item.find_by_hn_id params[:id]
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/config/environment.rb:
--------------------------------------------------------------------------------
1 | # Load the Rails application.
2 | require_relative "application"
3 |
4 | # Initialize the Rails application.
5 | Rails.application.initialize!
6 |
--------------------------------------------------------------------------------
/test/models/item_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | class ItemTest < ActiveSupport::TestCase
4 | # test "the truth" do
5 | # assert true
6 | # end
7 | end
8 |
--------------------------------------------------------------------------------
/test/models/user_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | class UserTest < ActiveSupport::TestCase
4 | # test "the truth" do
5 | # assert true
6 | # end
7 | end
8 |
--------------------------------------------------------------------------------
/test/models/ask_item_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | class AskItemTest < ActiveSupport::TestCase
4 | # test "the truth" do
5 | # assert true
6 | # end
7 | end
8 |
--------------------------------------------------------------------------------
/test/models/job_item_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | class JobItemTest < ActiveSupport::TestCase
4 | # test "the truth" do
5 | # assert true
6 | # end
7 | end
8 |
--------------------------------------------------------------------------------
/test/models/new_item_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | class NewItemTest < ActiveSupport::TestCase
4 | # test "the truth" do
5 | # assert true
6 | # end
7 | end
8 |
--------------------------------------------------------------------------------
/test/models/top_item_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | class TopItemTest < ActiveSupport::TestCase
4 | # test "the truth" do
5 | # assert true
6 | # end
7 | end
8 |
--------------------------------------------------------------------------------
/test/fixtures/top_items.yml:
--------------------------------------------------------------------------------
1 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
2 |
3 | one:
4 | item: one
5 |
6 | two:
7 | item: two
8 |
--------------------------------------------------------------------------------
/test/models/show_item_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | class ShowItemTest < ActiveSupport::TestCase
4 | # test "the truth" do
5 | # assert true
6 | # end
7 | end
8 |
--------------------------------------------------------------------------------
/test/jobs/load_ask_item_job_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | class LoadAskItemJobTest < ActiveJob::TestCase
4 | # test "the truth" do
5 | # assert true
6 | # end
7 | end
8 |
--------------------------------------------------------------------------------
/test/jobs/load_job_item_job_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | class LoadJobItemJobTest < ActiveJob::TestCase
4 | # test "the truth" do
5 | # assert true
6 | # end
7 | end
8 |
--------------------------------------------------------------------------------
/test/jobs/load_new_item_job_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | class LoadNewItemJobTest < ActiveJob::TestCase
4 | # test "the truth" do
5 | # assert true
6 | # end
7 | end
8 |
--------------------------------------------------------------------------------
/test/jobs/load_top_item_job_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | class LoadTopItemJobTest < ActiveJob::TestCase
4 | # test "the truth" do
5 | # assert true
6 | # end
7 | end
8 |
--------------------------------------------------------------------------------
/Procfile:
--------------------------------------------------------------------------------
1 | webpack: bin/webpack --watch --progress
2 | sidekiq: bundle exec sidekiq -c 25 -q default,mailers
3 | sidekiq_comments: bundle exec sidekiq -c 25 -q comments
4 | caching: memcached
5 |
--------------------------------------------------------------------------------
/config.ru:
--------------------------------------------------------------------------------
1 | # This file is used by Rack-based servers to start the application.
2 |
3 | require_relative "config/environment"
4 |
5 | run Rails.application
6 | Rails.application.load_server
7 |
--------------------------------------------------------------------------------
/test/jobs/load_ask_items_job_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | class LoadAskItemsJobTest < ActiveJob::TestCase
4 | # test "the truth" do
5 | # assert true
6 | # end
7 | end
8 |
--------------------------------------------------------------------------------
/test/jobs/load_job_items_job_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | class LoadJobItemsJobTest < ActiveJob::TestCase
4 | # test "the truth" do
5 | # assert true
6 | # end
7 | end
8 |
--------------------------------------------------------------------------------
/test/jobs/load_news_items_job_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | class LoadNewsItemsJobTest < ActiveJob::TestCase
4 | # test "the truth" do
5 | # assert true
6 | # end
7 | end
8 |
--------------------------------------------------------------------------------
/test/jobs/load_show_item_job_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | class LoadShowItemJobTest < ActiveJob::TestCase
4 | # test "the truth" do
5 | # assert true
6 | # end
7 | end
8 |
--------------------------------------------------------------------------------
/test/jobs/load_show_items_job_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | class LoadShowItemsJobTest < ActiveJob::TestCase
4 | # test "the truth" do
5 | # assert true
6 | # end
7 | end
8 |
--------------------------------------------------------------------------------
/test/jobs/load_top_items_job_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | class LoadTopItemsJobTest < ActiveJob::TestCase
4 | # test "the truth" do
5 | # assert true
6 | # end
7 | end
8 |
--------------------------------------------------------------------------------
/config/webpack/test.js:
--------------------------------------------------------------------------------
1 | process.env.NODE_ENV = process.env.NODE_ENV || 'development'
2 |
3 | const environment = require('./environment')
4 |
5 | module.exports = environment.toWebpackConfig()
6 |
--------------------------------------------------------------------------------
/test/jobs/load_user_details_job_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | class LoadUserDetailsJobTest < ActiveJob::TestCase
4 | # test "the truth" do
5 | # assert true
6 | # end
7 | end
8 |
--------------------------------------------------------------------------------
/test/workers/load_item_details_job_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | class LoadItemDetailsJobTest < ActiveJob::TestCase
4 | # test "the truth" do
5 | # assert true
6 | # end
7 | end
8 |
--------------------------------------------------------------------------------
/config/webpack/production.js:
--------------------------------------------------------------------------------
1 | process.env.NODE_ENV = process.env.NODE_ENV || 'production'
2 |
3 | const environment = require('./environment')
4 |
5 | module.exports = environment.toWebpackConfig()
6 |
--------------------------------------------------------------------------------
/db/migrate/20181206162906_add_kid_location_to_item.rb:
--------------------------------------------------------------------------------
1 | class AddKidLocationToItem < ActiveRecord::Migration[5.2]
2 | def change
3 | add_column :items, :kid_location, :bigint
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/app/views/service_worker/offline.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
It looks like you've lost your Internet connection
4 |
You may need to reconnect to Wi-Fi.
5 |
--------------------------------------------------------------------------------
/config/initializers/mime_types.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Add new mime types for use in respond_to blocks:
4 | # Mime::Type.register "text/richtext", :rtf
5 |
--------------------------------------------------------------------------------
/config/webpack/development.js:
--------------------------------------------------------------------------------
1 | process.env.NODE_ENV = process.env.NODE_ENV || 'development'
2 |
3 | const environment = require('./environment')
4 |
5 | module.exports = environment.toWebpackConfig()
6 |
--------------------------------------------------------------------------------
/bin/rails:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | load File.expand_path("spring", __dir__)
3 | APP_PATH = File.expand_path('../config/application', __dir__)
4 | require_relative "../config/boot"
5 | require "rails/commands"
6 |
--------------------------------------------------------------------------------
/test/application_system_test_case.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 |
3 | class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
4 | driven_by :selenium, using: :chrome, screen_size: [1400, 1400]
5 | end
6 |
--------------------------------------------------------------------------------
/db/migrate/20190116193355_add_loading_details_to_item.rb:
--------------------------------------------------------------------------------
1 | class AddLoadingDetailsToItem < ActiveRecord::Migration[5.2]
2 | def change
3 | add_column :items, :loading_details, :boolean, default: false
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/test/fixtures/ask_items.yml:
--------------------------------------------------------------------------------
1 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
2 |
3 | one:
4 | item: one
5 | location:
6 |
7 | two:
8 | item: two
9 | location:
10 |
--------------------------------------------------------------------------------
/test/fixtures/job_items.yml:
--------------------------------------------------------------------------------
1 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
2 |
3 | one:
4 | item: one
5 | location:
6 |
7 | two:
8 | item: two
9 | location:
10 |
--------------------------------------------------------------------------------
/test/fixtures/new_items.yml:
--------------------------------------------------------------------------------
1 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
2 |
3 | one:
4 | location:
5 | item: one
6 |
7 | two:
8 | location:
9 | item: two
10 |
--------------------------------------------------------------------------------
/test/fixtures/show_items.yml:
--------------------------------------------------------------------------------
1 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
2 |
3 | one:
4 | item: one
5 | location:
6 |
7 | two:
8 | item: two
9 | location:
10 |
--------------------------------------------------------------------------------
/config/boot.rb:
--------------------------------------------------------------------------------
1 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
2 |
3 | require "bundler/setup" # Set up gems listed in the Gemfile.
4 | require "bootsnap/setup" # Speed up boot time by caching expensive operations.
5 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/sass/layout/footer.sass:
--------------------------------------------------------------------------------
1 | $footer-background-color: $white-bis !default
2 | $footer-padding: 3rem 1.5rem 6rem !default
3 |
4 | .footer
5 | background-color: $footer-background-color
6 | padding: $footer-padding
7 |
--------------------------------------------------------------------------------
/app/controllers/users_controller.rb:
--------------------------------------------------------------------------------
1 | class UsersController < ApplicationController
2 |
3 | def show
4 | @user_id = params[:id]
5 | @user = User.find_by_hn_id @user_id
6 | LoadUserDetailsJob.perform_later @user_id
7 | end
8 | end
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | # Add your own tasks in files placed in lib/tasks ending in .rake,
2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
3 |
4 | require_relative 'config/application'
5 |
6 | Rails.application.load_tasks
7 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/sass/utilities/_all.sass:
--------------------------------------------------------------------------------
1 | @charset "utf-8"
2 |
3 | @import "initial-variables.sass"
4 | @import "functions.sass"
5 | @import "derived-variables.sass"
6 | @import "animations.sass"
7 | @import "mixins.sass"
8 | @import "controls.sass"
9 |
--------------------------------------------------------------------------------
/app/javascript/channels/index.js:
--------------------------------------------------------------------------------
1 | // Load all the channels within this directory and all subdirectories.
2 | // Channel files must be named *_channel.js.
3 |
4 | const channels = require.context('.', true, /_channel\.js$/)
5 | channels.keys().forEach(channels)
6 |
--------------------------------------------------------------------------------
/app/helpers/items_helper.rb:
--------------------------------------------------------------------------------
1 | module ItemsHelper
2 |
3 | def item_url(item)
4 | if item.url.nil?
5 | item_path(item.hn_id)
6 | else
7 | item.url
8 | end
9 | end
10 |
11 | def item_title_url(item)
12 |
13 | end
14 | end
15 |
16 |
--------------------------------------------------------------------------------
/app/views/ask_items/_ask_item.html.erb:
--------------------------------------------------------------------------------
1 | <%= cache ask_item do %>
2 |
3 | <%= render ask_item.item %>
4 |
5 | <% end %>
--------------------------------------------------------------------------------
/app/views/new_items/_new_item.html.erb:
--------------------------------------------------------------------------------
1 | <%= cache new_item do %>
2 |
3 | <%= render new_item.item %>
4 |
5 | <% end %>
--------------------------------------------------------------------------------
/app/controllers/service_worker_controller.rb:
--------------------------------------------------------------------------------
1 | class ServiceWorkerController < ApplicationController
2 | protect_from_forgery except: :service_worker
3 |
4 | def service_worker
5 | end
6 |
7 | def manifest
8 | end
9 |
10 | def offline
11 | end
12 | end
--------------------------------------------------------------------------------
/app/views/job_items/_job_item.html.erb:
--------------------------------------------------------------------------------
1 | <%= cache job_item do %>
2 |
3 | <%= render job_item.item %>
4 |
5 | <% end %>
6 |
--------------------------------------------------------------------------------
/app/views/show_items/_show_item.erb:
--------------------------------------------------------------------------------
1 | <%= cache show_item do %>
2 |
3 | <%= render show_item.item %>
4 |
5 | <% end %>
6 |
--------------------------------------------------------------------------------
/app/views/top_items/_top_item.html.erb:
--------------------------------------------------------------------------------
1 | <%= cache top_item do %>
2 |
3 | <%= render top_item.item %>
4 |
5 | <% end %>
6 |
--------------------------------------------------------------------------------
/app/channels/comments_channel.rb:
--------------------------------------------------------------------------------
1 | class CommentsChannel < ApplicationCable::Channel
2 |
3 | def follow(data)
4 | stop_all_streams
5 | stream_from "CommentsChannel:#{data['parent_id']}"
6 | end
7 |
8 | def unfollow
9 | stop_all_streams
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/app/models/user.rb:
--------------------------------------------------------------------------------
1 | class User < ApplicationRecord
2 |
3 | def populate(json)
4 | self.about = json['about'] if json['about']
5 | self.karma = json['karma'] if json['karma']
6 | self.created = DateTime.strptime("#{json['created']}",'%s') if json['created']
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/config/initializers/application_controller_renderer.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # ActiveSupport::Reloader.to_prepare do
4 | # ApplicationController.renderer.defaults.merge!(
5 | # http_host: 'example.org',
6 | # https: false
7 | # )
8 | # end
9 |
--------------------------------------------------------------------------------
/db/migrate/20181116153242_create_top_items.rb:
--------------------------------------------------------------------------------
1 | class CreateTopItems < ActiveRecord::Migration[5.2]
2 | def change
3 | create_table :top_items do |t|
4 | t.references :item, foreign_key: true
5 | t.bigint :location
6 |
7 | t.timestamps
8 | end
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/db/migrate/20181204140608_create_new_items.rb:
--------------------------------------------------------------------------------
1 | class CreateNewItems < ActiveRecord::Migration[5.2]
2 | def change
3 | create_table :new_items do |t|
4 | t.references :item, foreign_key: true
5 | t.bigint :location
6 |
7 | t.timestamps
8 | end
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/db/migrate/20181205143748_create_ask_items.rb:
--------------------------------------------------------------------------------
1 | class CreateAskItems < ActiveRecord::Migration[5.2]
2 | def change
3 | create_table :ask_items do |t|
4 | t.references :item, foreign_key: true
5 | t.bigint :location
6 |
7 | t.timestamps
8 | end
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/db/migrate/20181205160529_create_job_items.rb:
--------------------------------------------------------------------------------
1 | class CreateJobItems < ActiveRecord::Migration[5.2]
2 | def change
3 | create_table :job_items do |t|
4 | t.references :item, foreign_key: true
5 | t.bigint :location
6 |
7 | t.timestamps
8 | end
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: [
3 | require('postcss-import'),
4 | require('postcss-flexbugs-fixes'),
5 | require('postcss-preset-env')({
6 | autoprefixer: {
7 | flexbox: 'no-2009'
8 | },
9 | stage: 3
10 | })
11 | ]
12 | }
13 |
--------------------------------------------------------------------------------
/config/initializers/cookies_serializer.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Specify a serializer for the signed and encrypted cookie jars.
4 | # Valid options are :json, :marshal, and :hybrid.
5 | Rails.application.config.action_dispatch.cookies_serializer = :json
6 |
--------------------------------------------------------------------------------
/db/migrate/20181205141656_create_show_items.rb:
--------------------------------------------------------------------------------
1 | class CreateShowItems < ActiveRecord::Migration[5.2]
2 | def change
3 | create_table :show_items do |t|
4 | t.references :item, foreign_key: true
5 | t.bigint :location
6 |
7 | t.timestamps
8 | end
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/app/views/layouts/mailer.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
8 |
9 |
10 | <%= yield %>
11 |
12 |
--------------------------------------------------------------------------------
/app/channels/user_channel.rb:
--------------------------------------------------------------------------------
1 | class UserChannel < ApplicationCable::Channel
2 | def subscribed
3 | stop_all_streams
4 | stream_from "UserChannel#{params[:user_id]}"
5 | LoadUserDetailsJob.perform_later @user_id
6 | end
7 |
8 | def unsubscribed
9 | stop_all_streams
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/app/channels/item_channel.rb:
--------------------------------------------------------------------------------
1 | class ItemChannel < ApplicationCable::Channel
2 |
3 | def follow(data)
4 | stop_all_streams
5 | stream_from "ItemChannel:#{data['id']}"
6 | LoadItemDetailsWorker.perform_async data['id']
7 | end
8 |
9 | def unfollow
10 | stop_all_streams
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/app/javascript/channels/consumer.js:
--------------------------------------------------------------------------------
1 | // Action Cable provides the framework to deal with WebSockets in Rails.
2 | // You can generate new channels where WebSocket features live using the `rails generate channel` command.
3 |
4 | import { createConsumer } from "@rails/actioncable"
5 |
6 | export default createConsumer()
7 |
--------------------------------------------------------------------------------
/config/cable.yml:
--------------------------------------------------------------------------------
1 | development:
2 | adapter: redis
3 | url: "redis://localhost:6379/1"
4 | channel_prefix: hnpwa_dev
5 |
6 | test:
7 | adapter: test
8 |
9 | production:
10 | adapter: redis
11 | url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %>
12 | channel_prefix: hnpwa_production
13 |
--------------------------------------------------------------------------------
/config/initializers/filter_parameter_logging.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Configure sensitive parameters which will be filtered from the log file.
4 | Rails.application.config.filter_parameters += [
5 | :passw, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn
6 | ]
7 |
--------------------------------------------------------------------------------
/test/fixtures/users.yml:
--------------------------------------------------------------------------------
1 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
2 |
3 | one:
4 | hn_id: MyString
5 | created:
6 | delay:
7 | karma:
8 | about: MyText
9 |
10 | two:
11 | hn_id: MyString
12 | created:
13 | delay:
14 | karma:
15 | about: MyText
16 |
--------------------------------------------------------------------------------
/config/schedule.rb:
--------------------------------------------------------------------------------
1 | env :PATH, ENV['PATH']
2 |
3 | every 5.minute do
4 | runner 'LoadTopItemsJob.perform_later'
5 | runner 'LoadNewItemsJob.perform_later'
6 | end
7 |
8 | every 20.minute do
9 | runner 'LoadShowItemsJob.perform_later'
10 | runner 'LoadJobItemsJob.perform_later'
11 | runner 'LoadAskItemsJob.perform_later'
12 | end
--------------------------------------------------------------------------------
/db/migrate/20181207133624_create_users.rb:
--------------------------------------------------------------------------------
1 | class CreateUsers < ActiveRecord::Migration[5.2]
2 | def change
3 | create_table :users do |t|
4 | t.string :hn_id
5 | t.datetime :created
6 | t.bigint :delay
7 | t.bigint :karma
8 | t.text :about
9 |
10 | t.timestamps
11 | end
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/test/test_helper.rb:
--------------------------------------------------------------------------------
1 | ENV['RAILS_ENV'] ||= 'test'
2 | require_relative '../config/environment'
3 | require 'rails/test_help'
4 |
5 | class ActiveSupport::TestCase
6 | # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order.
7 | fixtures :all
8 |
9 | # Add more helper methods to be used by all tests here...
10 | end
11 |
--------------------------------------------------------------------------------
/app/javascript/controllers/bulma_navbar_controller.js:
--------------------------------------------------------------------------------
1 | import { Controller } from 'stimulus'
2 |
3 | export default class extends Controller {
4 |
5 | static targets = ['burger', 'menu'];
6 |
7 | toggle(event) {
8 | this.burgerTarget.classList.toggle('is-active');
9 | this.menuTarget.classList.toggle('is-active');
10 | }
11 | }
--------------------------------------------------------------------------------
/app/channels/items_list_channel.rb:
--------------------------------------------------------------------------------
1 | class ItemsListChannel < ApplicationCable::Channel
2 | def follow(data)
3 | stop_all_streams
4 | items = data['items']
5 | unless items.nil?
6 | items.each do |item|
7 | stream_from "ItemsListChannel:#{item}"
8 | end
9 | end
10 | end
11 |
12 | def unfollow
13 | stop_all_streams
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/app/controllers/tops_controller.rb:
--------------------------------------------------------------------------------
1 | class TopsController < ApplicationController
2 | def show
3 | @page = params[:page] ? params[:page].to_i : FIRST_PAGE
4 | @top_item = TopItem.order(:updated_at).last
5 | @top_items = TopItem.order(:location).limit(ITEMS_PER_PAGE).offset(@page * ITEMS_PER_PAGE).includes(:item)
6 | @total_pages = TopItem.count / ITEMS_PER_PAGE
7 | end
8 | end
--------------------------------------------------------------------------------
/app/controllers/news_controller.rb:
--------------------------------------------------------------------------------
1 | class NewsController < ApplicationController
2 |
3 | def show
4 | @page = params[:page] ? params[:page].to_i : FIRST_PAGE
5 | @new_item = NewItem.order(:updated_at).last
6 | @new_items = NewItem.order(:location).limit(ITEMS_PER_PAGE).offset(@page * ITEMS_PER_PAGE).includes(:item)
7 | @total_pages = NewItem.count / ITEMS_PER_PAGE
8 | end
9 | end
--------------------------------------------------------------------------------
/app/controllers/shows_controller.rb:
--------------------------------------------------------------------------------
1 | class ShowsController < ApplicationController
2 |
3 | def show
4 | @page = params[:page] ? params[:page].to_i : FIRST_PAGE
5 | @show_item = ShowItem.order(:updated_at).last
6 | @show_items = ShowItem.order(:location).limit(ITEMS_PER_PAGE).offset(@page * ITEMS_PER_PAGE).includes(:item)
7 | @total_pages = ShowItem.count / ITEMS_PER_PAGE
8 | end
9 | end
--------------------------------------------------------------------------------
/app/javascript/cables/cable.js:
--------------------------------------------------------------------------------
1 | // from https://evilmartians.com/chronicles/evil-front-part-3
2 | import { createConsumer } from "@rails/actioncable"
3 |
4 | let consumer;
5 |
6 | function createChannel(...args) {
7 | if (!consumer) {
8 | consumer = createConsumer();
9 | }
10 |
11 | return consumer.subscriptions.create(...args);
12 | }
13 |
14 | export default createChannel;
15 |
--------------------------------------------------------------------------------
/app/channels/ask_item_channel.rb:
--------------------------------------------------------------------------------
1 | class AskItemChannel < ApplicationCable::Channel
2 | def follow(data)
3 | stop_all_streams
4 | locations = data['locations']
5 | unless locations.nil?
6 | locations.each do |location|
7 | stream_from "AskItemChannel:#{location}"
8 | end
9 | end
10 | end
11 |
12 | def unfollow
13 | stop_all_streams
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/app/channels/new_item_channel.rb:
--------------------------------------------------------------------------------
1 | class NewItemChannel < ApplicationCable::Channel
2 | def follow(data)
3 | stop_all_streams
4 | locations = data['locations']
5 | unless locations.nil?
6 | locations.each do |location|
7 | stream_from "NewItemChannel:#{location}"
8 | end
9 | end
10 | end
11 |
12 | def unfollow
13 | stop_all_streams
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/app/channels/top_item_channel.rb:
--------------------------------------------------------------------------------
1 | class TopItemChannel < ApplicationCable::Channel
2 | def follow(data)
3 | stop_all_streams
4 | locations = data['locations']
5 | unless locations.nil?
6 | locations.each do |location|
7 | stream_from "TopItemChannel:#{location}"
8 | end
9 | end
10 | end
11 |
12 | def unfollow
13 | stop_all_streams
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/db/seeds.rb:
--------------------------------------------------------------------------------
1 | # This file should contain all the record creation needed to seed the database with its default values.
2 | # The data can then be loaded with the rails db:seed command (or created alongside the database with db:setup).
3 | #
4 | # Examples:
5 | #
6 | # movies = Movie.create([{ name: 'Star Wars' }, { name: 'Lord of the Rings' }])
7 | # Character.create(name: 'Luke', movie: movies.first)
8 |
--------------------------------------------------------------------------------
/app/channels/job_item_channel.rb:
--------------------------------------------------------------------------------
1 | class JobItemChannel < ApplicationCable::Channel
2 |
3 | def follow(data)
4 | stop_all_streams
5 | locations = data['locations']
6 | unless locations.nil?
7 | locations.each do |location|
8 | stream_from "JobItemChannel:#{location}"
9 | end
10 | end
11 | end
12 |
13 | def unfollow
14 | stop_all_streams
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/app/channels/show_item_channel.rb:
--------------------------------------------------------------------------------
1 | class ShowItemChannel < ApplicationCable::Channel
2 | def follow(data)
3 | stop_all_streams
4 | locations = data['locations']
5 | unless locations.nil?
6 | locations.each do |location|
7 | stream_from "ShowItemChannel:#{location}"
8 | end
9 | end
10 | end
11 |
12 | def unfollow
13 | stop_all_streams
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/app/controllers/jobs_controller.rb:
--------------------------------------------------------------------------------
1 | class JobsController < ApplicationController
2 |
3 | def show
4 | @page = params[:page] ? params[:page].to_i : FIRST_PAGE
5 | @job_item = JobItem.order(:updated_at).last
6 | @job_items = JobItem.order(:location).limit(ITEMS_PER_PAGE).offset(@page * ITEMS_PER_PAGE).includes(:item)
7 | @total_pages = JobItem.count / ITEMS_PER_PAGE
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/app/controllers/asks_controller.rb:
--------------------------------------------------------------------------------
1 | class AsksController < ApplicationController
2 |
3 | def show
4 | @page = params[:page] ? params[:page].to_i : FIRST_PAGE
5 | @ask_item = AskItem.order(:updated_at).last
6 | @ask_items = AskItem.order(:location).limit(ITEMS_PER_PAGE).offset(@page * ITEMS_PER_PAGE).includes(:item)
7 | @total_pages = AskItem.count / ITEMS_PER_PAGE
8 | end
9 |
10 | end
11 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/sass/components/_all.sass:
--------------------------------------------------------------------------------
1 | @charset "utf-8"
2 |
3 | @import "breadcrumb.sass"
4 | @import "card.sass"
5 | @import "dropdown.sass"
6 | @import "level.sass"
7 | @import "list.sass"
8 | @import "media.sass"
9 | @import "menu.sass"
10 | @import "message.sass"
11 | @import "modal.sass"
12 | @import "navbar.sass"
13 | @import "pagination.sass"
14 | @import "panel.sass"
15 | @import "tabs.sass"
16 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/sass/elements/_all.sass:
--------------------------------------------------------------------------------
1 | @charset "utf-8"
2 |
3 | @import "box.sass"
4 | @import "button.sass"
5 | @import "container.sass"
6 | @import "content.sass"
7 | @import "form.sass"
8 | @import "icon.sass"
9 | @import "image.sass"
10 | @import "notification.sass"
11 | @import "progress.sass"
12 | @import "table.sass"
13 | @import "tag.sass"
14 | @import "title.sass"
15 |
16 | @import "other.sass"
17 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/sass/layout/section.sass:
--------------------------------------------------------------------------------
1 | $section-padding: 3rem 1.5rem !default
2 | $section-padding-medium: 9rem 1.5rem !default
3 | $section-padding-large: 18rem 1.5rem !default
4 |
5 | .section
6 | padding: $section-padding
7 | // Responsiveness
8 | +desktop
9 | // Sizes
10 | &.is-medium
11 | padding: $section-padding-medium
12 | &.is-large
13 | padding: $section-padding-large
14 |
--------------------------------------------------------------------------------
/bin/spring:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | if !defined?(Spring) && [nil, "development", "test"].include?(ENV["RAILS_ENV"])
3 | # Load Spring without loading other gems in the Gemfile, for speed.
4 | require "bundler"
5 | Bundler.locked_gems.specs.find { |spec| spec.name == "spring" }&.tap do |spring|
6 | Gem.use_paths Gem.dir, Bundler.bundle_path.to_s, *Gem.path
7 | gem "spring", spring.version
8 | require "spring/binstub"
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "hnpwa",
3 | "private": true,
4 | "dependencies": {
5 | "@rails/actioncable": "^6.1.0",
6 | "@rails/activestorage": "^6.1.0",
7 | "@rails/ujs": "^6.1.0",
8 | "@rails/webpacker": "^5.2.1",
9 | "local-time": "^2.1.0",
10 | "stimulus": "^2.0.0",
11 | "swipejs": "^2.3.0",
12 | "turbolinks": "^5.2.0"
13 | },
14 | "devDependencies": {
15 | "webpack-dev-server": "^3.11.0"
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/app/javascript/controllers/index.js:
--------------------------------------------------------------------------------
1 | // Load all the controllers within this directory and all subdirectories.
2 | // Controller files must be named *_controller.js.
3 |
4 | import { Application } from "stimulus"
5 | import { definitionsFromContext } from "stimulus/webpack-helpers"
6 |
7 | const application = Application.start()
8 | const context = require.context("controllers", true, /_controller\.js$/)
9 | application.load(definitionsFromContext(context))
10 |
--------------------------------------------------------------------------------
/config/initializers/permissions_policy.rb:
--------------------------------------------------------------------------------
1 | # Define an application-wide HTTP permissions policy. For further
2 | # information see https://developers.google.com/web/updates/2018/06/feature-policy
3 | #
4 | # Rails.application.config.permissions_policy do |f|
5 | # f.camera :none
6 | # f.gyroscope :none
7 | # f.microphone :none
8 | # f.usb :none
9 | # f.fullscreen :self
10 | # f.payment :self, "https://secure.example.com"
11 | # end
12 |
--------------------------------------------------------------------------------
/config/credentials.yml.enc:
--------------------------------------------------------------------------------
1 | jzntcDze8kCBllNk4o0kq7GB5EtrwO9ojA7qd9iZ7v+Pg0m0n9Meq7ZBLOcI0yl/Y1HrRGwMZQWTnJLuJWl71aroQjG+msNu1xZSBmA+6kzgF2ozdUc3HHuCkyhDj9CNmz3MWJZJluhE+m11WJBCSvovmLGRly5n1urFOEbZGkLpUbmSnGJM8oo8wa1wYFw8+aOYGC9Xui7zTkIIUPgzNpbqBqx3srq4djQtF90UuT1gPup5Bs4zyxvz5SDzPQErar6MSGfarCVxesgfjgqKR91a0brqaI+fRmPt6o10rUm7xnXeK985Adsx6Kr/foXHiUIYYDZkGggZIIbIDx+cnvaF0LEDdQyGfZTgUusfASogNrPhvsLUINQLyAc3YsMEgUGY/x7bXacxf4PEjmrd9T1/3gc9Rspu2vN0--d/2Pu1T3f0NWwRH4--E0wYV7TgQ+0Ztb0M9yOjPQ==
--------------------------------------------------------------------------------
/app/assets/stylesheets/bulma.sass:
--------------------------------------------------------------------------------
1 | @charset "utf-8"
2 | /*! bulma.io v0.7.4 | MIT License | github.com/jgthms/bulma */
3 |
4 | $link: #283848
5 | $navbar-background-color: #C50001
6 | $navbar-item-color: #fff
7 | $navbar-item-active-color: #C50001
8 | $navbar-item-active-background-color: #fff
9 |
10 | @import "sass/utilities/_all"
11 | @import "sass/base/_all"
12 | @import "sass/elements/_all"
13 | @import "sass/components/_all"
14 | @import "sass/grid/_all"
15 | @import "sass/layout/_all"
16 |
--------------------------------------------------------------------------------
/app/jobs/load_ask_items_job.rb:
--------------------------------------------------------------------------------
1 | class LoadAskItemsJob < ApplicationJob
2 | queue_as :default
3 |
4 | def perform(*args)
5 | ask_stories_json = JSON.parse HTTP.get("https://hacker-news.firebaseio.com/v0/askstories.json?print=pretty").to_s
6 |
7 | ask_stories_json.each_with_index do |hn_story_id, ask_news_location|
8 | LoadAskItemJob.perform_later ask_news_location, hn_story_id
9 | end
10 |
11 | AskItem.where("location >= ?", ask_stories_json.length).destroy_all
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/app/jobs/load_job_items_job.rb:
--------------------------------------------------------------------------------
1 | class LoadJobItemsJob < ApplicationJob
2 | queue_as :default
3 |
4 | def perform(*args)
5 | job_stories_json = JSON.parse HTTP.get("https://hacker-news.firebaseio.com/v0/jobstories.json?print=pretty").to_s
6 |
7 | job_stories_json.each_with_index do |hn_story_id, job_news_location|
8 | LoadJobItemJob.perform_later job_news_location, hn_story_id
9 | end
10 |
11 | JobItem.where("location >= ?", job_stories_json.length).destroy_all
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/app/jobs/load_new_items_job.rb:
--------------------------------------------------------------------------------
1 | class LoadNewItemsJob < ApplicationJob
2 | queue_as :default
3 |
4 | def perform(*args)
5 | new_stories_json = JSON.parse HTTP.get("https://hacker-news.firebaseio.com/v0/newstories.json?print=pretty").to_s
6 |
7 | new_stories_json.each_with_index do |hn_story_id, new_news_location|
8 | LoadNewItemJob.perform_later new_news_location, hn_story_id
9 | end
10 |
11 | NewItem.where("location >= ?", new_stories_json.length).destroy_all
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/app/jobs/load_top_items_job.rb:
--------------------------------------------------------------------------------
1 | class LoadTopItemsJob < ApplicationJob
2 | queue_as :default
3 |
4 | def perform(*args)
5 | top_stories_json = JSON.parse HTTP.get("https://hacker-news.firebaseio.com/v0/topstories.json?print=pretty").to_s
6 |
7 | top_stories_json.each_with_index do |hn_story_id, top_news_location|
8 | LoadTopItemJob.perform_later top_news_location, hn_story_id
9 | end
10 |
11 | TopItem.where("location >= ?", top_stories_json.length).destroy_all
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/app/jobs/load_show_items_job.rb:
--------------------------------------------------------------------------------
1 | class LoadShowItemsJob < ApplicationJob
2 | queue_as :default
3 |
4 | def perform(*args)
5 | show_stories_json = JSON.parse HTTP.get("https://hacker-news.firebaseio.com/v0/showstories.json?print=pretty").to_s
6 |
7 | show_stories_json.each_with_index do |hn_story_id, show_news_location|
8 | LoadShowItemJob.perform_later show_news_location, hn_story_id
9 | end
10 |
11 | ShowItem.where("location >= ?", show_stories_json.length).destroy_all
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/bin/webpack:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | ENV["RAILS_ENV"] ||= ENV["RACK_ENV"] || "development"
4 | ENV["NODE_ENV"] ||= "development"
5 |
6 | require "pathname"
7 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
8 | Pathname.new(__FILE__).realpath)
9 |
10 | require "rubygems"
11 | require "bundler/setup"
12 |
13 | require "webpacker"
14 | require "webpacker/webpack_runner"
15 |
16 | APP_ROOT = File.expand_path("..", __dir__)
17 | Dir.chdir(APP_ROOT) do
18 | Webpacker::WebpackRunner.run(ARGV)
19 | end
20 |
--------------------------------------------------------------------------------
/bin/webpack-dev-server:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | ENV["RAILS_ENV"] ||= ENV["RACK_ENV"] || "development"
4 | ENV["NODE_ENV"] ||= "development"
5 |
6 | require "pathname"
7 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
8 | Pathname.new(__FILE__).realpath)
9 |
10 | require "rubygems"
11 | require "bundler/setup"
12 |
13 | require "webpacker"
14 | require "webpacker/dev_server_runner"
15 |
16 | APP_ROOT = File.expand_path("..", __dir__)
17 | Dir.chdir(APP_ROOT) do
18 | Webpacker::DevServerRunner.run(ARGV)
19 | end
20 |
--------------------------------------------------------------------------------
/config/initializers/wrap_parameters.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # This file contains settings for ActionController::ParamsWrapper which
4 | # is enabled by default.
5 |
6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array.
7 | ActiveSupport.on_load(:action_controller) do
8 | wrap_parameters format: [:json]
9 | end
10 |
11 | # To enable root element in JSON for ActiveRecord objects.
12 | # ActiveSupport.on_load(:active_record) do
13 | # self.include_root_in_json = true
14 | # end
15 |
--------------------------------------------------------------------------------
/app/views/users/_metadata.html.erb:
--------------------------------------------------------------------------------
1 | <% if user %>
2 | <%= user.about.html_safe unless user.about.nil? %>
3 | Created <%= local_time_ago user.created unless user.created.nil? %>
4 | Karma: <%= user.karma %>
5 | <% else %>
6 |
7 |
8 |
9 |
10 |
11 | Loading details
12 |
13 | <% end %>
--------------------------------------------------------------------------------
/config/initializers/backtrace_silencers.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces.
4 | # Rails.backtrace_cleaner.add_silencer { |line| /my_noisy_library/.match?(line) }
5 |
6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code
7 | # by setting BACKTRACE=1 before calling your invocation, like "BACKTRACE=1 ./bin/rails runner 'MyClass.perform'".
8 | Rails.backtrace_cleaner.remove_silencers! if ENV["BACKTRACE"]
9 |
--------------------------------------------------------------------------------
/app/views/service_worker/manifest.json.erb:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "HNWPA",
3 | "name": "Hacker News Progressive Web App",
4 | "icons": [
5 | {
6 | "src": "<%= asset_path('train_192.png') %>",
7 | "type": "image/png",
8 | "sizes": "192x192"
9 | },
10 | {
11 | "src": "<%= asset_path('train_512.png') %>",
12 | "type": "image/png",
13 | "sizes": "512x512"
14 | }
15 | ],
16 | "start_url": "<%= top_path %>",
17 | "background_color": "#f2f3f5",
18 | "display": "standalone",
19 | "scope": "<%= root_path %>",
20 | "theme_color": "#f60"
21 | }
22 |
--------------------------------------------------------------------------------
/db/migrate/20181114014928_create_items.rb:
--------------------------------------------------------------------------------
1 | class CreateItems < ActiveRecord::Migration[5.2]
2 | def change
3 | create_table :items do |t|
4 | t.bigint :hn_id
5 | t.bigint :parent_id
6 | t.boolean :deleted
7 | t.integer :hn_type
8 | t.string :by
9 | t.datetime :time
10 | t.text :text
11 | t.boolean :dead
12 | t.bigint :parent
13 | t.bigint :poll
14 | t.string :url
15 | t.string :host
16 | t.integer :score
17 | t.string :title
18 | t.integer :descendants
19 |
20 | t.timestamps
21 | end
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/app/views/news/show.html.erb:
--------------------------------------------------------------------------------
1 | <%- content_for( :new_items_active ) { "is-active" } %>
2 | <%= render partial: 'layouts/page_navigation', locals: { page: @page, total_pages: @total_pages, path: new_path } %>
3 | <%= cache ['new-list', @new_item, @page] do %>
4 |
5 |
6 | <% @new_items.each_slice(4) do |slice| %>
7 | <% slice.each do |item| %>
8 | <%= render item %>
9 | <% end %>
10 | <% end %>
11 |
12 |
13 | <% end %>
14 |
--------------------------------------------------------------------------------
/app/views/asks/show.html.erb:
--------------------------------------------------------------------------------
1 | <%- content_for( :ask_items_active ) { "is-active" } %>
2 | <%= render partial: 'layouts/page_navigation', locals: { page: @page, total_pages: @total_pages, path: ask_path } %>
3 | <%= cache ['asks-list', @ask_item, @page] do %>
4 |
5 |
6 | <% @ask_items.each_slice(4) do |slice| %>
7 | <% slice.each do |item| %>
8 | <%= render item %>
9 | <% end %>
10 | <% end %>
11 |
12 |
13 | <% end %>
14 |
--------------------------------------------------------------------------------
/app/views/jobs/show.html.erb:
--------------------------------------------------------------------------------
1 | <%- content_for( :job_items_active ) { "is-active" } %>
2 | <%= render partial: 'layouts/page_navigation', locals: { page: @page, total_pages: @total_pages, path: job_path } %>
3 | <%= cache ['news-list', @job_item, @page] do %>
4 |
5 |
6 | <% @job_items.each_slice(4) do |slice| %>
7 | <% slice.each do |item| %>
8 | <%= render item %>
9 | <% end %>
10 | <% end %>
11 |
12 |
13 | <% end %>
14 |
--------------------------------------------------------------------------------
/app/views/tops/show.html.erb:
--------------------------------------------------------------------------------
1 | <%- content_for( :top_items_active ) { "is-active" } %>
2 | <%= render partial: 'layouts/page_navigation', locals: { page: @page, total_pages: @total_pages, path: top_path } %>
3 | <%= cache ['news-list', @top_item, @page] do %>
4 |
5 |
6 | <% @top_items.each_slice(4) do |slice| %>
7 | <% slice.each do |item| %>
8 | <%= render item %>
9 | <% end %>
10 | <% end %>
11 |
12 |
13 | <% end %>
14 |
--------------------------------------------------------------------------------
/app/views/shows/show.html.erb:
--------------------------------------------------------------------------------
1 | <%- content_for( :show_items_active ) { "is-active" } %>
2 | <%= render partial: 'layouts/page_navigation', locals: { page: @page, total_pages: @total_pages, path: show_path } %>
3 | <%= cache ['shows-list', @show_item, @page] do %>
4 |
5 |
6 | <% @show_items.each_slice(4) do |slice| %>
7 | <% slice.each do |item| %>
8 | <%= render item %>
9 | <% end %>
10 | <% end %>
11 |
12 |
13 | <% end %>
14 |
--------------------------------------------------------------------------------
/app/views/layouts/_page_navigation.html.erb:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/sass/elements/icon.sass:
--------------------------------------------------------------------------------
1 | $icon-dimensions: 1.5rem !default
2 | $icon-dimensions-small: 1rem !default
3 | $icon-dimensions-medium: 2rem !default
4 | $icon-dimensions-large: 3rem !default
5 |
6 | .icon
7 | align-items: center
8 | display: inline-flex
9 | justify-content: center
10 | height: $icon-dimensions
11 | width: $icon-dimensions
12 | // Sizes
13 | &.is-small
14 | height: $icon-dimensions-small
15 | width: $icon-dimensions-small
16 | &.is-medium
17 | height: $icon-dimensions-medium
18 | width: $icon-dimensions-medium
19 | &.is-large
20 | height: $icon-dimensions-large
21 | width: $icon-dimensions-large
22 |
--------------------------------------------------------------------------------
/config/routes.rb:
--------------------------------------------------------------------------------
1 | require 'sidekiq/web'
2 |
3 | Rails.application.routes.draw do
4 |
5 | namespace :manage do
6 | mount Sidekiq::Web => '/sidekiq'
7 | Sidekiq::Web.set :session_secret, Rails.application.credentials.dig(:secret_key_base)
8 | end
9 |
10 | resource :top
11 | resource :new
12 | resource :show
13 | resource :ask
14 | resource :job
15 | resources :items, only: [:show]
16 | get "/user/:id" => "users#show", as: :user
17 |
18 | get '/service-worker.js' => "service_worker#service_worker"
19 | get '/manifest.json' => "service_worker#manifest"
20 | get '/offline.html' => "service_worker#offline"
21 | root "tops#show"
22 | end
23 |
--------------------------------------------------------------------------------
/bin/yarn:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | require 'pathname'
3 |
4 | APP_ROOT = File.expand_path('..', __dir__)
5 | Dir.chdir(APP_ROOT) do
6 | executable_path = ENV["PATH"].split(File::PATH_SEPARATOR).find do |path|
7 | normalized_path = File.expand_path(path)
8 |
9 | normalized_path != __dir__ && File.executable?(Pathname.new(normalized_path).join('yarn'))
10 | end
11 |
12 | if executable_path
13 | exec File.expand_path(Pathname.new(executable_path).join('yarn')), *ARGV
14 | else
15 | $stderr.puts "Yarn executable was not detected in the system."
16 | $stderr.puts "Download Yarn at https://yarnpkg.com/en/docs/install"
17 | exit 1
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/config/initializers/assets.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Version of your assets, change this if you want to expire all your assets.
4 | Rails.application.config.assets.version = '1.0'
5 |
6 | # Add additional assets to the asset load path.
7 | # Rails.application.config.assets.paths << Emoji.images_path
8 | # Add Yarn node_modules folder to the asset load path.
9 | Rails.application.config.assets.paths << Rails.root.join('node_modules')
10 |
11 | # Precompile additional assets.
12 | # application.js, application.css, and all non-JS/CSS in the app/assets
13 | # folder are already added.
14 | # Rails.application.config.assets.precompile += %w( admin.js admin.css )
15 |
--------------------------------------------------------------------------------
/app/jobs/load_user_details_job.rb:
--------------------------------------------------------------------------------
1 | class LoadUserDetailsJob < ApplicationJob
2 | queue_as :comments
3 |
4 | def perform(hn_user_id)
5 | begin
6 | user_json = JSON.parse HTTP.get("https://hacker-news.firebaseio.com/v0/user/#{hn_user_id}.json").to_s
7 | user = User.where(hn_id: hn_user_id).first_or_create
8 | user.populate(user_json)
9 | user.save
10 |
11 | ActionCable.server.broadcast "UserChannel#{hn_user_id}", {
12 | user_metadata: UsersController.render( partial: 'metadata', locals: { user: user } ).squish,
13 | user_id: hn_user_id
14 | }
15 | rescue URI::InvalidURIError => error
16 | logger.error error
17 | end
18 |
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/app/javascript/controllers/user_controller.js:
--------------------------------------------------------------------------------
1 | import { Controller } from "stimulus"
2 | import createChannel from "cables/cable";
3 |
4 | export default class extends Controller {
5 | static targets = [ 'metadata' ]
6 |
7 | connect() {
8 | console.log(`user id: ${this.data.get("id")}`);
9 | let userController = this;
10 | console.log(userController.metadataTarget)
11 | createChannel({ channel: "UserChannel", user_id: this.data.get("id") }, {
12 | received({ user_metadata, user_id }) {
13 | console.log('received')
14 | console.log(user_metadata)
15 | console.log(user_id)
16 | userController.metadataTarget.innerHTML = user_metadata
17 | }
18 | });
19 | }
20 | }
--------------------------------------------------------------------------------
/test/fixtures/items.yml:
--------------------------------------------------------------------------------
1 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
2 |
3 | one:
4 | hn_id:
5 | parent_id:
6 | deleted: false
7 | type: 1
8 | by: MyString
9 | time: 2018-11-13 20:49:28
10 | text: MyText
11 | dead: false
12 | parent:
13 | poll:
14 | url: MyString
15 | host: MyString
16 | score: 1
17 | title: MyString
18 | descendants: 1
19 |
20 | two:
21 | hn_id:
22 | parent_id:
23 | deleted: false
24 | type: 1
25 | by: MyString
26 | time: 2018-11-13 20:49:28
27 | text: MyText
28 | dead: false
29 | parent:
30 | poll:
31 | url: MyString
32 | host: MyString
33 | score: 1
34 | title: MyString
35 | descendants: 1
36 |
--------------------------------------------------------------------------------
/app/views/items/_comments_header.html.erb:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/config/initializers/inflections.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Add new inflection rules using the following format. Inflections
4 | # are locale specific, and you may define rules for as many different
5 | # locales as you wish. All of these examples are active by default:
6 | # ActiveSupport::Inflector.inflections(:en) do |inflect|
7 | # inflect.plural /^(ox)$/i, '\1en'
8 | # inflect.singular /^(ox)en/i, '\1'
9 | # inflect.irregular 'person', 'people'
10 | # inflect.uncountable %w( fish sheep )
11 | # end
12 |
13 | # These inflection rules are supported but not enabled by default:
14 | # ActiveSupport::Inflector.inflections(:en) do |inflect|
15 | # inflect.acronym 'RESTful'
16 | # end
17 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/sass/elements/container.sass:
--------------------------------------------------------------------------------
1 | .container
2 | margin: 0 auto
3 | position: relative
4 | +desktop
5 | max-width: $desktop - (2 * $gap)
6 | width: $desktop - (2 * $gap)
7 | &.is-fluid
8 | margin-left: $gap
9 | margin-right: $gap
10 | max-width: none
11 | width: auto
12 | +until-widescreen
13 | &.is-widescreen
14 | max-width: $widescreen - (2 * $gap)
15 | width: auto
16 | +until-fullhd
17 | &.is-fullhd
18 | max-width: $fullhd - (2 * $gap)
19 | width: auto
20 | +widescreen
21 | max-width: $widescreen - (2 * $gap)
22 | width: $widescreen - (2 * $gap)
23 | +fullhd
24 | max-width: $fullhd - (2 * $gap)
25 | width: $fullhd - (2 * $gap)
26 |
--------------------------------------------------------------------------------
/app/views/items/show.html.erb:
--------------------------------------------------------------------------------
1 | <%= cache ['item', @item] do %>
2 |
3 |
4 | <%= render @item %>
5 |
6 |
7 |
10 |
11 |
12 | <% if @item.descendants > 0 %>
13 | <%= render partial: 'comments', locals: { item: @item } %>
14 | <% end %>
15 |
16 |
17 |
18 | <% end %>
--------------------------------------------------------------------------------
/app/views/users/show.html.erb:
--------------------------------------------------------------------------------
1 | <%= cache ['user', @user_id, @user ] do %>
2 |
3 |
8 |
9 | <%= render partial: 'metadata', locals: { user: @user } %>
10 |
11 |
19 |
20 | <% end %>
--------------------------------------------------------------------------------
/app/javascript/packs/application.js:
--------------------------------------------------------------------------------
1 | /* eslint no-console:0 */
2 | // This file is automatically compiled by Webpack, along with any other files
3 | // present in this directory. You're encouraged to place your actual application logic in
4 | // a relevant structure within app/javascript and only use these pack files to reference
5 | // that code so it'll be compiled.
6 | //
7 | // To reference this file, add <%= javascript_pack_tag 'application' %> to the appropriate
8 | // layout file, like app/views/layouts/application.html.erb
9 |
10 | require("@rails/ujs").start();
11 | require("turbolinks").start();
12 | require("@rails/activestorage").start();
13 | require("channels");
14 |
15 | import "controllers";
16 |
17 | import LocalTime from "local-time";
18 | LocalTime.start();
19 |
--------------------------------------------------------------------------------
/app/javascript/controllers/toggle_controller.js:
--------------------------------------------------------------------------------
1 | import { Controller } from "stimulus";
2 |
3 | export default class extends Controller {
4 | static targets = ["comments", "toggle", "link"];
5 | static classes = ["open"];
6 | static values = { count: Number };
7 | toggle() {
8 | if (this.toggleTarget.classList.toggle(this.openClass)) {
9 | this.linkTarget.innerHTML = `[-]`;
10 | this.commentsTarget.style = "";
11 | } else {
12 | this.linkTarget.innerHTML = `[+] ${this.commentsLabel()} collapsed`;
13 | this.commentsTarget.style = "display: none;";
14 | }
15 | }
16 |
17 | commentsLabel() {
18 | let count = this.countValue;
19 | if (count == 1) {
20 | return "1 reply";
21 | } else {
22 | return `${count} replies`;
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/sass/elements/box.sass:
--------------------------------------------------------------------------------
1 | $box-color: $text !default
2 | $box-background-color: $white !default
3 | $box-radius: $radius-large !default
4 | $box-shadow: 0 2px 3px rgba($black, 0.1), 0 0 0 1px rgba($black, 0.1) !default
5 | $box-padding: 1.25rem !default
6 |
7 | $box-link-hover-shadow: 0 2px 3px rgba($black, 0.1), 0 0 0 1px $link !default
8 | $box-link-active-shadow: inset 0 1px 2px rgba($black, 0.2), 0 0 0 1px $link !default
9 |
10 | .box
11 | @extend %block
12 | background-color: $box-background-color
13 | border-radius: $box-radius
14 | box-shadow: $box-shadow
15 | color: $box-color
16 | display: block
17 | padding: $box-padding
18 |
19 | a.box
20 | &:hover,
21 | &:focus
22 | box-shadow: $box-link-hover-shadow
23 | &:active
24 | box-shadow: $box-link-active-shadow
25 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/sass/elements/other.sass:
--------------------------------------------------------------------------------
1 | .block
2 | @extend %block
3 |
4 | .delete
5 | @extend %delete
6 |
7 | .heading
8 | display: block
9 | font-size: 11px
10 | letter-spacing: 1px
11 | margin-bottom: 5px
12 | text-transform: uppercase
13 |
14 | .highlight
15 | @extend %block
16 | font-weight: $weight-normal
17 | max-width: 100%
18 | overflow: hidden
19 | padding: 0
20 | pre
21 | overflow: auto
22 | max-width: 100%
23 |
24 | .loader
25 | @extend %loader
26 |
27 | .number
28 | align-items: center
29 | background-color: $background
30 | border-radius: $radius-rounded
31 | display: inline-flex
32 | font-size: $size-medium
33 | height: 2em
34 | justify-content: center
35 | margin-right: 1.5rem
36 | min-width: 2.5em
37 | padding: 0.25rem 0.5rem
38 | text-align: center
39 | vertical-align: top
40 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/sass/grid/tiles.sass:
--------------------------------------------------------------------------------
1 | .tile
2 | align-items: stretch
3 | display: block
4 | flex-basis: 0
5 | flex-grow: 1
6 | flex-shrink: 1
7 | min-height: min-content
8 | // Modifiers
9 | &.is-ancestor
10 | margin-left: -0.75rem
11 | margin-right: -0.75rem
12 | margin-top: -0.75rem
13 | &:last-child
14 | margin-bottom: -0.75rem
15 | &:not(:last-child)
16 | margin-bottom: 0.75rem
17 | &.is-child
18 | margin: 0 !important
19 | &.is-parent
20 | padding: 0.75rem
21 | &.is-vertical
22 | flex-direction: column
23 | & > .tile.is-child:not(:last-child)
24 | margin-bottom: 1.5rem !important
25 | // Responsiveness
26 | +tablet
27 | &:not(.is-child)
28 | display: flex
29 | @for $i from 1 through 12
30 | &.is-#{$i}
31 | flex: none
32 | width: ($i / 12) * 100%
33 |
--------------------------------------------------------------------------------
/config/database.yml:
--------------------------------------------------------------------------------
1 | # SQLite version 3.x
2 | # gem install sqlite3
3 | #
4 | # Ensure the SQLite 3 gem is defined in your Gemfile
5 | # gem 'sqlite3'
6 | #
7 |
8 | default: &default
9 | adapter: postgresql
10 | encoding: unicode
11 | host: localhost
12 | username: hnpwa
13 | password: hnpwa
14 | # For details on connection pooling, see Rails configuration guide
15 | # http://guides.rubyonrails.org/configuring.html#database-pooling
16 | pool: 30
17 |
18 | development:
19 | <<: *default
20 | database: hnpwa
21 |
22 | # Warning: The database defined as "test" will be erased and
23 | # re-generated from your development database when you run "rake".
24 | # Do not set this db to the same as development or production.
25 | test:
26 | <<: *default
27 | database: hnpwa_test
28 |
29 | production:
30 | <<: *default
31 | database: hnpwa_prod
32 |
--------------------------------------------------------------------------------
/config/application.rb:
--------------------------------------------------------------------------------
1 | require_relative "boot"
2 |
3 | require "rails/all"
4 |
5 | # Require the gems listed in Gemfile, including any gems
6 | # you've limited to :test, :development, or :production.
7 | Bundler.require(*Rails.groups)
8 |
9 | module Hnpwa
10 | class Application < Rails::Application
11 | # Initialize configuration defaults for originally generated Rails version.
12 | config.load_defaults 6.0
13 |
14 | # Configuration for the application, engines, and railties goes here.
15 | #
16 | # These settings can be overridden in specific environments using the files
17 | # in config/environments, which are processed later.
18 | #
19 | # config.time_zone = "Central Time (US & Canada)"
20 | # config.eager_load_paths << Rails.root.join("extras")
21 |
22 | config.active_job.queue_adapter = :sidekiq
23 |
24 | config.cache_store = :mem_cache_store
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/app/assets/images/cloud-download.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/config/locales/en.yml:
--------------------------------------------------------------------------------
1 | # Files in the config/locales directory are used for internationalization
2 | # and are automatically loaded by Rails. If you want to use locales other
3 | # than English, add the necessary files in this directory.
4 | #
5 | # To use the locales, use `I18n.t`:
6 | #
7 | # I18n.t 'hello'
8 | #
9 | # In views, this is aliased to just `t`:
10 | #
11 | # <%= t('hello') %>
12 | #
13 | # To use a different locale, set it with `I18n.locale`:
14 | #
15 | # I18n.locale = :es
16 | #
17 | # This would use the information in config/locales/es.yml.
18 | #
19 | # The following keys must be escaped otherwise they will not be retrieved by
20 | # the default I18n backend:
21 | #
22 | # true, false, on, off, yes, no
23 | #
24 | # Instead, surround them with single quotes.
25 | #
26 | # en:
27 | # 'true': 'foo'
28 | #
29 | # To learn more, please read the Rails Internationalization guide
30 | # available at https://guides.rubyonrails.org/i18n.html.
31 |
32 | en:
33 | hello: "Hello world"
34 |
--------------------------------------------------------------------------------
/app/javascript/controllers/comments_controller.js:
--------------------------------------------------------------------------------
1 | import { Controller } from "stimulus";
2 | import createChannel from "cables/cable";
3 |
4 | export default class extends Controller {
5 | static targets = ["comments"];
6 |
7 | initialize() {
8 | let thisController = this;
9 | this.thisChannel = createChannel("CommentsChannel", {
10 | connected() {
11 | thisController.listen();
12 | },
13 | received({ comments, parent_id, item_id }) {
14 | if (thisController.data.get("hn-id") == item_id) {
15 | thisController.commentsTarget.innerHTML = comments;
16 | }
17 | },
18 | });
19 | }
20 |
21 | connect() {
22 | this.listen();
23 | }
24 |
25 | disconnect() {
26 | if (this.thisChannel) {
27 | this.thisChannel.perform("unfollow");
28 | }
29 | }
30 |
31 | listen() {
32 | if (this.thisChannel.consumer.connection.isOpen()) {
33 | this.thisChannel.perform("follow", { parent_id: this.data.get("hn-id") });
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/sass/elements/notification.sass:
--------------------------------------------------------------------------------
1 | $notification-background-color: $background !default
2 | $notification-radius: $radius !default
3 | $notification-padding: 1.25rem 2.5rem 1.25rem 1.5rem !default
4 |
5 | .notification
6 | @extend %block
7 | background-color: $notification-background-color
8 | border-radius: $notification-radius
9 | padding: $notification-padding
10 | position: relative
11 | a:not(.button):not(.dropdown-item)
12 | color: currentColor
13 | text-decoration: underline
14 | strong
15 | color: currentColor
16 | code,
17 | pre
18 | background: $white
19 | pre code
20 | background: transparent
21 | & > .delete
22 | position: absolute
23 | right: 0.5rem
24 | top: 0.5rem
25 | .title,
26 | .subtitle,
27 | .content
28 | color: currentColor
29 | // Colors
30 | @each $name, $pair in $colors
31 | $color: nth($pair, 1)
32 | $color-invert: nth($pair, 2)
33 | &.is-#{$name}
34 | background-color: $color
35 | color: $color-invert
36 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files for more about ignoring files.
2 | #
3 | # If you find yourself ignoring temporary files generated by your text editor
4 | # or operating system, you probably want to add a global ignore instead:
5 | # git config --global core.excludesfile '~/.gitignore_global'
6 |
7 | # Ignore bundler config.
8 | /.bundle
9 |
10 | # Ignore the default SQLite database.
11 | /db/*.sqlite3
12 | /db/*.sqlite3-journal
13 |
14 | # Ignore all logfiles and tempfiles.
15 | /log/*
16 | /tmp/*
17 | !/log/.keep
18 | !/tmp/.keep
19 |
20 | # Ignore uploaded files in development
21 | /storage/*
22 | !/storage/.keep
23 |
24 | /node_modules
25 | /yarn-error.log
26 |
27 | /public/assets
28 | .byebug_history
29 |
30 | # Ignore master key for decrypting credentials and more.
31 | /config/master.key
32 | /public/packs
33 | /public/packs-test
34 | /node_modules
35 | yarn-debug.log*
36 | .yarn-integrity
37 | .DS_Store
38 |
39 | /public/packs
40 | /public/packs-test
41 | /node_modules
42 | /yarn-error.log
43 | yarn-debug.log*
44 | .yarn-integrity
45 |
--------------------------------------------------------------------------------
/app/assets/images/cloud-check.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/sass/components/media.sass:
--------------------------------------------------------------------------------
1 | .media
2 | align-items: flex-start
3 | display: flex
4 | text-align: left
5 | .content:not(:last-child)
6 | margin-bottom: 0.75rem
7 | .media
8 | border-top: 1px solid rgba($border, 0.5)
9 | display: flex
10 | padding-top: 0.75rem
11 | .content:not(:last-child),
12 | .control:not(:last-child)
13 | margin-bottom: 0.5rem
14 | .media
15 | padding-top: 0.5rem
16 | & + .media
17 | margin-top: 0.5rem
18 | & + .media
19 | border-top: 1px solid rgba($border, 0.5)
20 | margin-top: 1rem
21 | padding-top: 1rem
22 | // Sizes
23 | &.is-large
24 | & + .media
25 | margin-top: 1.5rem
26 | padding-top: 1.5rem
27 |
28 | .media-left,
29 | .media-right
30 | flex-basis: auto
31 | flex-grow: 0
32 | flex-shrink: 0
33 |
34 | .media-left
35 | margin-right: 1rem
36 |
37 | .media-right
38 | margin-left: 1rem
39 |
40 | .media-content
41 | flex-basis: auto
42 | flex-grow: 1
43 | flex-shrink: 1
44 | text-align: left
45 |
46 | +mobile
47 | .media-content
48 | overflow-x: auto
49 |
--------------------------------------------------------------------------------
/app/jobs/load_ask_item_job.rb:
--------------------------------------------------------------------------------
1 | class LoadAskItemJob < ApplicationJob
2 | queue_as :default
3 |
4 | def perform(ask_news_location, hn_story_id)
5 | begin
6 | story_json = JSON.parse HTTP.get("https://hacker-news.firebaseio.com/v0/item/#{hn_story_id}.json?print=pretty").to_s
7 | if story_json.nil?
8 | return
9 | end
10 | item = Item.where(hn_id: hn_story_id).first_or_create
11 | item.populate(story_json)
12 | item.save
13 |
14 | ask_item = AskItem.where(location: ask_news_location).first_or_create
15 | ask_item.item = item
16 | ask_item.save
17 |
18 | ActionCable.server.broadcast "AskItemChannel:#{ask_item.location}", {
19 | message: AsksController.render( ask_item.item ).squish,
20 | location: ask_item.location
21 | }
22 | ActionCable.server.broadcast "ItemsListChannel:#{ask_item.item.id}", {
23 | item: ItemsController.render( ask_item.item ).squish,
24 | item_id: ask_item.item.id
25 | }
26 | rescue URI::InvalidURIError => error
27 | logger.error error
28 | end
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/app/jobs/load_job_item_job.rb:
--------------------------------------------------------------------------------
1 | class LoadJobItemJob < ApplicationJob
2 | queue_as :default
3 |
4 | def perform(job_news_location, hn_story_id)
5 | begin
6 | story_json = JSON.parse HTTP.get("https://hacker-news.firebaseio.com/v0/item/#{hn_story_id}.json?print=pretty").to_s
7 | if story_json.nil?
8 | return
9 | end
10 | item = Item.where(hn_id: hn_story_id).first_or_create
11 | item.populate(story_json)
12 | item.save
13 |
14 | job_item = JobItem.where(location: job_news_location).first_or_create
15 | job_item.item = item
16 | job_item.save
17 |
18 | ActionCable.server.broadcast "JobItemChannel:#{job_item.location}", {
19 | message: JobsController.render( job_item.item ).squish,
20 | location: job_item.location
21 | }
22 | ActionCable.server.broadcast "ItemsListChannel:#{job_item.item.id}", {
23 | item: ItemsController.render( job_item.item ).squish,
24 | item_id: job_item.item.id
25 | }
26 | rescue URI::InvalidURIError => error
27 | logger.error error
28 | end
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/app/jobs/load_new_item_job.rb:
--------------------------------------------------------------------------------
1 | class LoadNewItemJob < ApplicationJob
2 | queue_as :default
3 |
4 | def perform(new_news_location, hn_story_id)
5 | begin
6 | story_json = JSON.parse HTTP.get("https://hacker-news.firebaseio.com/v0/item/#{hn_story_id}.json?print=pretty").to_s
7 | if story_json.nil?
8 | return
9 | end
10 | item = Item.where(hn_id: hn_story_id).first_or_create
11 | item.populate(story_json)
12 | item.save
13 |
14 | new_item = NewItem.where(location: new_news_location).first_or_create
15 | new_item.item = item
16 | new_item.save
17 |
18 | ActionCable.server.broadcast "NewItemChannel:#{new_item.location}", {
19 | message: NewsController.render( new_item.item ).squish,
20 | location: new_item.location
21 | }
22 | ActionCable.server.broadcast "ItemsListChannel:#{new_item.item.id}", {
23 | item: ItemsController.render( new_item.item ).squish,
24 | item_id: new_item.item.id
25 | }
26 | rescue URI::InvalidURIError => error
27 | logger.error error
28 | end
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/app/jobs/load_top_item_job.rb:
--------------------------------------------------------------------------------
1 | class LoadTopItemJob < ApplicationJob
2 | queue_as :default
3 |
4 | def perform(top_news_location, hn_story_id)
5 | begin
6 | story_json = JSON.parse HTTP.get("https://hacker-news.firebaseio.com/v0/item/#{hn_story_id}.json?print=pretty").to_s
7 | if story_json.nil?
8 | return
9 | end
10 | item = Item.where(hn_id: hn_story_id).first_or_create
11 | item.populate(story_json)
12 | item.save
13 |
14 | top_item = TopItem.where(location: top_news_location).first_or_create
15 | top_item.item = item
16 | top_item.save
17 |
18 | ActionCable.server.broadcast "TopItemChannel:#{top_item.location}", {
19 | message: TopsController.render( top_item.item ).squish,
20 | location: top_item.location
21 | }
22 | ActionCable.server.broadcast "ItemsListChannel:#{top_item.item.id}", {
23 | item: ItemsController.render( top_item.item ).squish,
24 | item_id: top_item.item.id
25 | }
26 | rescue URI::InvalidURIError => error
27 | logger.error error
28 | end
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/app/javascript/controllers/ask_item_controller.js:
--------------------------------------------------------------------------------
1 | import { Controller } from "stimulus"
2 | import createChannel from "cables/cable";
3 |
4 | export default class extends Controller {
5 | initialize() {
6 | let thisController = this;
7 | this.channel = createChannel( "AskItemChannel", {
8 | connected() {
9 | thisController.listen()
10 | },
11 | received({ message, location }) {
12 | let existingItem = document.querySelector(`[data-location='${ location }']`)
13 | if (existingItem) {
14 | existingItem.innerHTML = message
15 | }
16 | }
17 | });
18 | }
19 |
20 | connect() {
21 | this.listen()
22 | }
23 |
24 | disconnect() {
25 | if (this.channel) {
26 | this.channel.perform('unfollow')
27 | }
28 | }
29 |
30 | listen() {
31 | if (this.channel) {
32 | let locations = []
33 | for (const value of document.querySelectorAll(`[data-location]`)) {
34 | locations.push( value.getAttribute('data-location') )
35 | }
36 | this.channel.perform('follow', { locations: locations } )
37 | }
38 | }
39 | }
--------------------------------------------------------------------------------
/app/jobs/load_show_item_job.rb:
--------------------------------------------------------------------------------
1 | class LoadShowItemJob < ApplicationJob
2 | queue_as :default
3 |
4 | def perform(show_news_location, hn_story_id)
5 | begin
6 | story_json = JSON.parse HTTP.get("https://hacker-news.firebaseio.com/v0/item/#{hn_story_id}.json?print=pretty").to_s
7 | if story_json.nil?
8 | return
9 | end
10 | item = Item.where(hn_id: hn_story_id).first_or_create
11 | item.populate(story_json)
12 | item.save
13 |
14 | show_item = ShowItem.where(location: show_news_location).first_or_create
15 | show_item.item = item
16 | show_item.save
17 |
18 | ActionCable.server.broadcast "ShowItemChannel:#{show_item.location}", {
19 | message: ShowsController.render( show_item.item ).squish,
20 | location: show_item.location
21 | }
22 | ActionCable.server.broadcast "ItemsListChannel:#{show_item.item.id}", {
23 | item: ItemsController.render( show_item.item ).squish,
24 | item_id: show_item.item.id
25 | }
26 | rescue URI::InvalidURIError => error
27 | logger.error error
28 | end
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/app/javascript/controllers/item_location_controller.js:
--------------------------------------------------------------------------------
1 | import { Controller } from "stimulus"
2 | import createChannel from "cables/cable";
3 |
4 | export default class extends Controller {
5 | initialize() {
6 | let thisController = this;
7 | this.channel = createChannel( this.data.get("channel"), {
8 | connected() {
9 | thisController.listen()
10 | },
11 | received({ message, location }) {
12 | let existingItem = document.querySelector(`[data-location='${ location }']`)
13 | if (existingItem) {
14 | existingItem.innerHTML = message
15 | }
16 | }
17 | });
18 | }
19 |
20 | connect() {
21 | this.listen()
22 | }
23 |
24 | disconnect() {
25 | if (this.channel) {
26 | this.channel.perform('unfollow')
27 | }
28 | }
29 |
30 | listen() {
31 | if (this.channel) {
32 | let locations = []
33 | for (const value of document.querySelectorAll(`[data-location]`)) {
34 | locations.push( value.getAttribute('data-location') )
35 | }
36 | this.channel.perform('follow', { locations: locations } )
37 | }
38 | }
39 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 John Beatty
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/sass/base/minireset.sass:
--------------------------------------------------------------------------------
1 | /*! minireset.css v0.0.4 | MIT License | github.com/jgthms/minireset.css */
2 | // Blocks
3 | html,
4 | body,
5 | p,
6 | ol,
7 | ul,
8 | li,
9 | dl,
10 | dt,
11 | dd,
12 | blockquote,
13 | figure,
14 | fieldset,
15 | legend,
16 | textarea,
17 | pre,
18 | iframe,
19 | hr,
20 | h1,
21 | h2,
22 | h3,
23 | h4,
24 | h5,
25 | h6
26 | margin: 0
27 | padding: 0
28 |
29 | // Headings
30 | h1,
31 | h2,
32 | h3,
33 | h4,
34 | h5,
35 | h6
36 | font-size: 100%
37 | font-weight: normal
38 |
39 | // List
40 | ul
41 | list-style: none
42 |
43 | // Form
44 | button,
45 | input,
46 | select,
47 | textarea
48 | margin: 0
49 |
50 | // Box sizing
51 | html
52 | box-sizing: border-box
53 |
54 | *
55 | &,
56 | &::before,
57 | &::after
58 | box-sizing: inherit
59 |
60 | // Media
61 | img,
62 | embed,
63 | iframe,
64 | object,
65 | video
66 | height: auto
67 | max-width: 100%
68 |
69 | audio
70 | max-width: 100%
71 |
72 | // Iframe
73 | iframe
74 | border: 0
75 |
76 | // Table
77 | table
78 | border-collapse: collapse
79 | border-spacing: 0
80 |
81 | td,
82 | th
83 | padding: 0
84 | text-align: left
85 |
--------------------------------------------------------------------------------
/bin/setup:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | require "fileutils"
3 |
4 | # path to your application root.
5 | APP_ROOT = File.expand_path('..', __dir__)
6 |
7 | def system!(*args)
8 | system(*args) || abort("\n== Command #{args} failed ==")
9 | end
10 |
11 | FileUtils.chdir APP_ROOT do
12 | # This script is a way to set up or update your development environment automatically.
13 | # This script is idempotent, so that you can run it at any time and get an expectable outcome.
14 | # Add necessary setup steps to this file.
15 |
16 | puts '== Installing dependencies =='
17 | system! 'gem install bundler --conservative'
18 | system('bundle check') || system!('bundle install')
19 |
20 | # Install JavaScript dependencies
21 | system! 'bin/yarn'
22 |
23 | # puts "\n== Copying sample files =="
24 | # unless File.exist?('config/database.yml')
25 | # FileUtils.cp 'config/database.yml.sample', 'config/database.yml'
26 | # end
27 |
28 | puts "\n== Preparing database =="
29 | system! 'bin/rails db:prepare'
30 |
31 | puts "\n== Removing old logs and tempfiles =="
32 | system! 'bin/rails log:clear tmp:clear'
33 |
34 | puts "\n== Restarting application server =="
35 | system! 'bin/rails restart'
36 | end
37 |
--------------------------------------------------------------------------------
/config/storage.yml:
--------------------------------------------------------------------------------
1 | test:
2 | service: Disk
3 | root: <%= Rails.root.join("tmp/storage") %>
4 |
5 | local:
6 | service: Disk
7 | root: <%= Rails.root.join("storage") %>
8 |
9 | # Use rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key)
10 | # amazon:
11 | # service: S3
12 | # access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %>
13 | # secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %>
14 | # region: us-east-1
15 | # bucket: your_own_bucket
16 |
17 | # Remember not to checkin your GCS keyfile to a repository
18 | # google:
19 | # service: GCS
20 | # project: your_project
21 | # credentials: <%= Rails.root.join("path/to/gcs.keyfile") %>
22 | # bucket: your_own_bucket
23 |
24 | # Use rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key)
25 | # microsoft:
26 | # service: AzureStorage
27 | # storage_account_name: your_account_name
28 | # storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %>
29 | # container: your_container_name
30 |
31 | # mirror:
32 | # service: Mirror
33 | # primary: local
34 | # mirrors: [ amazon, google, microsoft ]
35 |
--------------------------------------------------------------------------------
/app/javascript/controllers/items_controller.js:
--------------------------------------------------------------------------------
1 | import { Controller } from "stimulus"
2 | import createChannel from "cables/cable";
3 |
4 | export default class extends Controller {
5 | static targets = [ ]
6 |
7 | initialize() {
8 |
9 | let thisController = this;
10 | this.channel = createChannel( "ItemsListChannel", {
11 | connected() {
12 | thisController.listen()
13 | },
14 | received({ item, item_id }) {
15 | let existingItem = document.querySelector(`[data-item-id='${ item_id }']`)
16 | if (existingItem) {
17 | let html = new DOMParser().parseFromString( item , 'text/html');
18 | const itemHTML = html.body.firstChild;
19 | existingItem.parentNode.replaceChild(itemHTML, existingItem);
20 | }
21 | }
22 | });
23 |
24 | }
25 |
26 | connect() {
27 | this.listen()
28 | }
29 |
30 | disconnect() {
31 | if (this.channel) {
32 | this.channel.perform('unfollow')
33 | }
34 | }
35 |
36 | listen() {
37 | if (this.channel) {
38 | let items = []
39 | for (const value of document.querySelectorAll(`[data-item-id]`)) {
40 | items.push( value.getAttribute('data-item-id') )
41 | }
42 | this.channel.perform('follow', { items: items } )
43 | }
44 | }
45 | }
--------------------------------------------------------------------------------
/app/assets/stylesheets/sass/components/list.sass:
--------------------------------------------------------------------------------
1 | $list-background-color: $white !default
2 | $list-shadow: 0 2px 3px rgba($black, 0.1), 0 0 0 1px rgba($black, 0.1) !default
3 | $list-radius: $radius !default
4 |
5 | $list-item-border: 1px solid $border !default
6 | $list-item-color: $text !default
7 | $list-item-active-background-color: $link !default
8 | $list-item-active-color: $link-invert !default
9 | $list-item-hover-background-color: $background !default
10 |
11 | .list
12 | @extend %block
13 | background-color: $list-background-color
14 | border-radius: $list-radius
15 | box-shadow: $list-shadow
16 | // &.is-hoverable > .list-item:hover:not(.is-active)
17 | // background-color: $list-item-hover-background-color
18 | // cursor: pointer
19 |
20 | .list-item
21 | display: block
22 | padding: 0.5em 1em
23 | &:not(a)
24 | color: $list-item-color
25 | &:first-child
26 | border-top-left-radius: $list-radius
27 | border-top-right-radius: $list-radius
28 | &:last-child
29 | border-top-left-radius: $list-radius
30 | border-top-right-radius: $list-radius
31 | &:not(:last-child)
32 | border-bottom: $list-item-border
33 | &.is-active
34 | background-color: $list-item-active-background-color
35 | color: $list-item-active-color
36 |
37 | a.list-item
38 | background-color: $list-item-hover-background-color
39 | cursor: pointer
--------------------------------------------------------------------------------
/app/views/items/_comments.html.erb:
--------------------------------------------------------------------------------
1 | <%= cache ['comments-list', item] do %>
2 | <% item.kids.order(:kid_location).each do | kid | %>
3 |
4 | <% unless item.story? %>
5 |
8 | <% end %>
9 |
10 |
11 |
16 |
21 |
22 |
23 | <%= kid.text.html_safe unless kid.text.nil? %>
24 |
25 | <% if kid.kids.count > 0 %>
26 | <%= render partial: 'comments', locals: { item: kid } %>
27 | <% end %>
28 |
29 |
30 | <% end %>
31 | <% end %>
32 |
--------------------------------------------------------------------------------
/app/javascript/controllers/item_controller.js:
--------------------------------------------------------------------------------
1 | import { Controller } from "stimulus";
2 | import createChannel from "cables/cable";
3 |
4 | export default class extends Controller {
5 | static targets = ["metadata", "commentsHeader", "progress"];
6 | static values = { id: String };
7 |
8 | initialize() {
9 | let thisController = this;
10 | this.thisChannel = createChannel(
11 | { channel: "ItemChannel" },
12 | {
13 | connected() {
14 | thisController.loadDetails();
15 | },
16 | received({ item, comments_header, progress, item_id }) {
17 | if (thisController.idValue == item_id) {
18 | if (item) {
19 | thisController.metadataTarget.innerHTML = item;
20 | }
21 | if (comments_header) {
22 | thisController.commentsHeaderTarget.innerHTML = comments_header;
23 | }
24 | if (progress) {
25 | thisController.progressTarget.value = progress;
26 | }
27 | }
28 | },
29 | }
30 | );
31 | }
32 |
33 | connect() {
34 | this.loadDetails();
35 | }
36 |
37 | disconnect() {
38 | if (this.thisChannel) {
39 | this.thisChannel.perform("unfollow");
40 | }
41 | }
42 |
43 | loadDetails() {
44 | if (this.thisChannel) {
45 | this.thisChannel.perform("follow", { id: this.idValue });
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/app/views/items/_item.html.erb:
--------------------------------------------------------------------------------
1 | <%= cache item do %>
2 |
3 |
15 |
16 |
17 |
18 | <% if item.url.nil? %>
19 | <%= link_to item.title, item_path(item.hn_id) %>
20 | <% else %>
21 | <%= link_to item.title, item.url, { target: '_blank', rel: 'noopener' } %>
22 | <% end %>
23 |
24 |
25 | <%= local_time_ago item.time %>
26 |
27 |
28 |
40 |
41 | <% end %>
42 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/sass/components/menu.sass:
--------------------------------------------------------------------------------
1 | $menu-item-color: $text !default
2 | $menu-item-radius: $radius-small !default
3 | $menu-item-hover-color: $text-strong !default
4 | $menu-item-hover-background-color: $background !default
5 | $menu-item-active-color: $link-invert !default
6 | $menu-item-active-background-color: $link !default
7 |
8 | $menu-list-border-left: 1px solid $border !default
9 |
10 | $menu-label-color: $text-light !default
11 |
12 | .menu
13 | font-size: $size-normal
14 | // Sizes
15 | &.is-small
16 | font-size: $size-small
17 | &.is-medium
18 | font-size: $size-medium
19 | &.is-large
20 | font-size: $size-large
21 |
22 | .menu-list
23 | line-height: 1.25
24 | a
25 | border-radius: $menu-item-radius
26 | color: $menu-item-color
27 | display: block
28 | padding: 0.5em 0.75em
29 | &:hover
30 | background-color: $menu-item-hover-background-color
31 | color: $menu-item-hover-color
32 | // Modifiers
33 | &.is-active
34 | background-color: $menu-item-active-background-color
35 | color: $menu-item-active-color
36 | li
37 | ul
38 | border-left: $menu-list-border-left
39 | margin: 0.75em
40 | padding-left: 0.75em
41 |
42 | .menu-label
43 | color: $menu-label-color
44 | font-size: 0.75em
45 | letter-spacing: 0.1em
46 | text-transform: uppercase
47 | &:not(:first-child)
48 | margin-top: 1em
49 | &:not(:last-child)
50 | margin-bottom: 1em
51 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/sass/elements/image.sass:
--------------------------------------------------------------------------------
1 | $dimensions: 16 24 32 48 64 96 128 !default
2 |
3 | .image
4 | display: block
5 | position: relative
6 | img
7 | display: block
8 | height: auto
9 | width: 100%
10 | &.is-rounded
11 | border-radius: $radius-rounded
12 | // Ratio
13 | &.is-square,
14 | &.is-1by1,
15 | &.is-5by4,
16 | &.is-4by3,
17 | &.is-3by2,
18 | &.is-5by3,
19 | &.is-16by9,
20 | &.is-2by1,
21 | &.is-3by1,
22 | &.is-4by5,
23 | &.is-3by4,
24 | &.is-2by3,
25 | &.is-3by5,
26 | &.is-9by16,
27 | &.is-1by2,
28 | &.is-1by3
29 | img,
30 | .has-ratio
31 | @extend %overlay
32 | height: 100%
33 | width: 100%
34 | &.is-square,
35 | &.is-1by1
36 | padding-top: 100%
37 | &.is-5by4
38 | padding-top: 80%
39 | &.is-4by3
40 | padding-top: 75%
41 | &.is-3by2
42 | padding-top: 66.6666%
43 | &.is-5by3
44 | padding-top: 60%
45 | &.is-16by9
46 | padding-top: 56.25%
47 | &.is-2by1
48 | padding-top: 50%
49 | &.is-3by1
50 | padding-top: 33.3333%
51 | &.is-4by5
52 | padding-top: 125%
53 | &.is-3by4
54 | padding-top: 133.3333%
55 | &.is-2by3
56 | padding-top: 150%
57 | &.is-3by5
58 | padding-top: 166.6666%
59 | &.is-9by16
60 | padding-top: 177.7777%
61 | &.is-1by2
62 | padding-top: 200%
63 | &.is-1by3
64 | padding-top: 300%
65 | // Sizes
66 | @each $dimension in $dimensions
67 | &.is-#{$dimension}x#{$dimension}
68 | height: $dimension * 1px
69 | width: $dimension * 1px
70 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/sass/utilities/controls.sass:
--------------------------------------------------------------------------------
1 | $control-radius: $radius !default
2 | $control-radius-small: $radius-small !default
3 |
4 | $control-border-width: 1px !default
5 |
6 | $control-height: 2.25em !default
7 | $control-line-height: 1.5 !default
8 |
9 | $control-padding-vertical: calc(0.375em - #{$control-border-width}) !default
10 | $control-padding-horizontal: calc(0.625em - #{$control-border-width}) !default
11 |
12 | =control
13 | -moz-appearance: none
14 | -webkit-appearance: none
15 | align-items: center
16 | border: $control-border-width solid transparent
17 | border-radius: $control-radius
18 | box-shadow: none
19 | display: inline-flex
20 | font-size: $size-normal
21 | height: $control-height
22 | justify-content: flex-start
23 | line-height: $control-line-height
24 | padding-bottom: $control-padding-vertical
25 | padding-left: $control-padding-horizontal
26 | padding-right: $control-padding-horizontal
27 | padding-top: $control-padding-vertical
28 | position: relative
29 | vertical-align: top
30 | // States
31 | &:focus,
32 | &.is-focused,
33 | &:active,
34 | &.is-active
35 | outline: none
36 | &[disabled],
37 | fieldset[disabled] &
38 | cursor: not-allowed
39 |
40 | %control
41 | +control
42 |
43 | // The controls sizes use mixins so they can be used at different breakpoints
44 | =control-small
45 | border-radius: $control-radius-small
46 | font-size: $size-small
47 | =control-medium
48 | font-size: $size-medium
49 | =control-large
50 | font-size: $size-large
51 |
--------------------------------------------------------------------------------
/config/initializers/content_security_policy.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Define an application-wide content security policy
4 | # For further information see the following documentation
5 | # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy
6 |
7 | # Rails.application.config.content_security_policy do |policy|
8 | # policy.default_src :self, :https
9 | # policy.font_src :self, :https, :data
10 | # policy.img_src :self, :https, :data
11 | # policy.object_src :none
12 | # policy.script_src :self, :https
13 | # policy.style_src :self, :https
14 | # # If you are using webpack-dev-server then specify webpack-dev-server host
15 | # policy.connect_src :self, :https, "http://localhost:3035", "ws://localhost:3035" if Rails.env.development?
16 |
17 | # # Specify URI for violation reports
18 | # # policy.report_uri "/csp-violation-report-endpoint"
19 | # end
20 |
21 | # If you are using UJS then enable automatic nonce generation
22 | # Rails.application.config.content_security_policy_nonce_generator = -> request { SecureRandom.base64(16) }
23 |
24 | # Set the nonce only to specific directives
25 | # Rails.application.config.content_security_policy_nonce_directives = %w(script-src)
26 |
27 | # Report CSP violations to a specified URI
28 | # For further information see the following documentation:
29 | # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only
30 | # Rails.application.config.content_security_policy_report_only = true
31 |
--------------------------------------------------------------------------------
/Capfile:
--------------------------------------------------------------------------------
1 | # Load DSL and set up stages
2 | require "capistrano/setup"
3 |
4 | # Include default deployment tasks
5 | require "capistrano/deploy"
6 |
7 | # Load the SCM plugin appropriate to your project:
8 | #
9 | # require "capistrano/scm/hg"
10 | # install_plugin Capistrano::SCM::Hg
11 | # or
12 | # require "capistrano/scm/svn"
13 | # install_plugin Capistrano::SCM::Svn
14 | # or
15 | require "capistrano/scm/git"
16 | install_plugin Capistrano::SCM::Git
17 |
18 | # Include tasks from other gems included in your Gemfile
19 | #
20 | # For documentation on these, see for example:
21 | #
22 | # https://github.com/capistrano/rvm
23 | # https://github.com/capistrano/rbenv
24 | # https://github.com/capistrano/chruby
25 | # https://github.com/capistrano/bundler
26 | # https://github.com/capistrano/rails
27 | # https://github.com/capistrano/passenger
28 | #
29 | # require "capistrano/rvm"
30 | # require "capistrano/rbenv"
31 | # require "capistrano/chruby"
32 | # require "capistrano/bundler"
33 | # require "capistrano/rails/assets"
34 | # require "capistrano/rails/migrations"
35 | # require "capistrano/passenger"
36 |
37 | require "capistrano/bundler"
38 | require "capistrano/rails/assets"
39 | require "capistrano/rails/migrations"
40 | require "whenever/capistrano"
41 | require 'capistrano/rails'
42 | require 'capistrano/passenger'
43 | require 'capistrano/rbenv'
44 |
45 | set :rbenv_type, :user
46 | set :rbenv_ruby, '2.7.2'
47 |
48 | # Load custom tasks from `lib/capistrano/tasks` if you have any defined
49 | Dir.glob("lib/capistrano/tasks/*.rake").each { |r| import r }
50 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/sass/components/level.sass:
--------------------------------------------------------------------------------
1 | .level
2 | @extend %block
3 | align-items: center
4 | justify-content: space-between
5 | code
6 | border-radius: $radius
7 | img
8 | display: inline-block
9 | vertical-align: top
10 | // Modifiers
11 | &.is-mobile
12 | display: flex
13 | .level-left,
14 | .level-right
15 | display: flex
16 | .level-left + .level-right
17 | margin-top: 0
18 | .level-item
19 | &:not(:last-child)
20 | margin-bottom: 0
21 | margin-right: 0.75rem
22 | &:not(.is-narrow)
23 | flex-grow: 1
24 | // Responsiveness
25 | +tablet
26 | display: flex
27 | & > .level-item
28 | &:not(.is-narrow)
29 | flex-grow: 1
30 |
31 | .level-item
32 | align-items: center
33 | display: flex
34 | flex-basis: auto
35 | flex-grow: 0
36 | flex-shrink: 0
37 | justify-content: center
38 | .title,
39 | .subtitle
40 | margin-bottom: 0
41 | // Responsiveness
42 | +mobile
43 | &:not(:last-child)
44 | margin-bottom: 0.75rem
45 |
46 | .level-left,
47 | .level-right
48 | flex-basis: auto
49 | flex-grow: 0
50 | flex-shrink: 0
51 | .level-item
52 | // Modifiers
53 | &.is-flexible
54 | flex-grow: 1
55 | // Responsiveness
56 | +tablet
57 | &:not(:last-child)
58 | margin-right: 0.75rem
59 |
60 | .level-left
61 | align-items: center
62 | justify-content: flex-start
63 | // Responsiveness
64 | +mobile
65 | & + .level-right
66 | margin-top: 1.5rem
67 | +tablet
68 | display: flex
69 |
70 | .level-right
71 | align-items: center
72 | justify-content: flex-end
73 | // Responsiveness
74 | +tablet
75 | display: flex
76 |
--------------------------------------------------------------------------------
/app/javascript/controllers/service_worker_controller.js:
--------------------------------------------------------------------------------
1 | import { Controller } from "stimulus";
2 |
3 | export default class extends Controller {
4 | static targets = ["pageSavedNotice", "savingPageNotice"];
5 |
6 | connect() {
7 | if (navigator.serviceWorker) {
8 | if (navigator.serviceWorker.controller) {
9 | // If the service worker is already running, skip to state change
10 | this.stateChange();
11 | } else {
12 | // Register the service worker, and wait for it to become active
13 | navigator.serviceWorker
14 | .register("/service-worker.js", { scope: "./" })
15 | .then(function (reg) {
16 | console.log("[Companion]", "Service worker registered!");
17 | console.log(reg);
18 | });
19 | navigator.serviceWorker.addEventListener(
20 | "controllerchange",
21 | this.controllerChange.bind(this)
22 | );
23 | }
24 | }
25 | }
26 |
27 | controllerChange(event) {
28 | console.log(
29 | '[controllerchange] A "controllerchange" event has happened ' +
30 | "within navigator.serviceWorker: ",
31 | event
32 | );
33 | navigator.serviceWorker.controller.addEventListener(
34 | "statechange",
35 | this.stateChange.bind(this)
36 | );
37 | }
38 |
39 | stateChange() {
40 | let state = navigator.serviceWorker.controller.state;
41 | console.log(
42 | "[controllerchange][statechange] " + 'A "statechange" has occured: ',
43 | state
44 | );
45 |
46 | if (state === "activated" || state === "redundant") {
47 | this.savingPageNoticeTarget.classList.add("is-hidden");
48 | this.pageSavedNoticeTarget.classList.remove("is-hidden");
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/app/models/item.rb:
--------------------------------------------------------------------------------
1 | class Item < ApplicationRecord
2 | has_one :top_item
3 | has_one :new_item
4 | has_one :job_item
5 | has_one :ask_item
6 | has_one :show_item
7 | has_one :hn_parent, class_name: 'Item', primary_key: 'parent', foreign_key: 'hn_id'
8 |
9 | # belongs_to :item_parent, class_name: 'Item', foreign_key: 'parent_id'
10 |
11 | has_many :kids, class_name: "Item", primary_key: 'hn_id', foreign_key: 'parent'
12 | after_save :update_list_item
13 | enum hn_type: [:job, :story, :comment, :poll, :pollopt]
14 |
15 | def populate(json)
16 | if json.nil?
17 | return
18 | end
19 | self.hn_id = json['id'] if json['id']
20 | self.deleted = json['deleted'] if json['deleted']
21 | self.hn_type = json['type'] if json['type']
22 | self.by = json['by'] if json['by']
23 | self.time = DateTime.strptime("#{json['time']}",'%s') if json['time']
24 | self.text = json['text'] if json['text']
25 | self.dead = json['dead'] if json['dead']
26 | self.parent = json['parent'] if json['parent']
27 | self.poll = json['poll'] if json['poll']
28 | if json['url']
29 | self.url = json['url']
30 | host = URI.parse( json['url'] ).host
31 | self.host = host.gsub("www.", "") unless host.nil?
32 | end
33 | self.score = json['score'] if json['score']
34 | self.descendants = json['descendants'] if json['descendants']
35 | self.title = json['title'] if json['title']
36 | end
37 |
38 | def update_list_item
39 | if self.story?
40 | top_item.touch if top_item.present?
41 | new_item.touch if new_item.present?
42 | show_item.touch if show_item.present?
43 | ask_item.touch if ask_item.present?
44 | elsif self.job?
45 | job_item.touch if job_item.present?
46 | elsif self.comment?
47 | hn_parent.touch if hn_parent.present?
48 | end
49 | end
50 | end
51 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/sass/elements/title.sass:
--------------------------------------------------------------------------------
1 | $title-color: $grey-darker !default
2 | $title-size: $size-3 !default
3 | $title-weight: $weight-semibold !default
4 | $title-line-height: 1.125 !default
5 | $title-strong-color: inherit !default
6 | $title-strong-weight: inherit !default
7 | $title-sub-size: 0.75em !default
8 | $title-sup-size: 0.75em !default
9 |
10 | $subtitle-color: $grey-dark !default
11 | $subtitle-size: $size-5 !default
12 | $subtitle-weight: $weight-normal !default
13 | $subtitle-line-height: 1.25 !default
14 | $subtitle-strong-color: $grey-darker !default
15 | $subtitle-strong-weight: $weight-semibold !default
16 | $subtitle-negative-margin: -1.25rem !default
17 |
18 | .title,
19 | .subtitle
20 | @extend %block
21 | word-break: break-word
22 | em,
23 | span
24 | font-weight: inherit
25 | sub
26 | font-size: $title-sub-size
27 | sup
28 | font-size: $title-sup-size
29 | .tag
30 | vertical-align: middle
31 |
32 | .title
33 | color: $title-color
34 | font-size: $title-size
35 | font-weight: $title-weight
36 | line-height: $title-line-height
37 | strong
38 | color: $title-strong-color
39 | font-weight: $title-strong-weight
40 | & + .highlight
41 | margin-top: -0.75rem
42 | &:not(.is-spaced) + .subtitle
43 | margin-top: $subtitle-negative-margin
44 | // Sizes
45 | @each $size in $sizes
46 | $i: index($sizes, $size)
47 | &.is-#{$i}
48 | font-size: $size
49 |
50 | .subtitle
51 | color: $subtitle-color
52 | font-size: $subtitle-size
53 | font-weight: $subtitle-weight
54 | line-height: $subtitle-line-height
55 | strong
56 | color: $subtitle-strong-color
57 | font-weight: $subtitle-strong-weight
58 | &:not(.is-spaced) + .title
59 | margin-top: $subtitle-negative-margin
60 | // Sizes
61 | @each $size in $sizes
62 | $i: index($sizes, $size)
63 | &.is-#{$i}
64 | font-size: $size
65 |
--------------------------------------------------------------------------------
/public/500.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | We're sorry, but something went wrong (500)
5 |
6 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
We're sorry, but something went wrong.
62 |
63 |
If you are the application owner check the logs for more information.
64 |
65 |
66 |
67 |
--------------------------------------------------------------------------------
/config/puma.rb:
--------------------------------------------------------------------------------
1 | # Puma can serve each request in a thread from an internal thread pool.
2 | # The `threads` method setting takes two numbers: a minimum and maximum.
3 | # Any libraries that use thread pools should be configured to match
4 | # the maximum value specified for Puma. Default is set to 5 threads for minimum
5 | # and maximum; this matches the default thread size of Active Record.
6 | #
7 | max_threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }
8 | min_threads_count = ENV.fetch("RAILS_MIN_THREADS") { max_threads_count }
9 | threads min_threads_count, max_threads_count
10 |
11 | # Specifies the `worker_timeout` threshold that Puma will use to wait before
12 | # terminating a worker in development environments.
13 | #
14 | worker_timeout 3600 if ENV.fetch("RAILS_ENV", "development") == "development"
15 |
16 | # Specifies the `port` that Puma will listen on to receive requests; default is 3000.
17 | #
18 | port ENV.fetch("PORT") { 3000 }
19 |
20 | # Specifies the `environment` that Puma will run in.
21 | #
22 | environment ENV.fetch("RAILS_ENV") { "development" }
23 |
24 | # Specifies the `pidfile` that Puma will use.
25 | pidfile ENV.fetch("PIDFILE") { "tmp/pids/server.pid" }
26 |
27 | # Specifies the number of `workers` to boot in clustered mode.
28 | # Workers are forked web server processes. If using threads and workers together
29 | # the concurrency of the application would be max `threads` * `workers`.
30 | # Workers do not work on JRuby or Windows (both of which do not support
31 | # processes).
32 | #
33 | # workers ENV.fetch("WEB_CONCURRENCY") { 2 }
34 |
35 | # Use the `preload_app!` method when specifying a `workers` number.
36 | # This directive tells Puma to first boot the application and load code
37 | # before forking the application. This takes advantage of Copy On Write
38 | # process behavior so workers use less memory.
39 | #
40 | # preload_app!
41 |
42 | # Allow puma to be restarted by `rails restart` command.
43 | plugin :tmp_restart
44 |
--------------------------------------------------------------------------------
/public/422.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | The change you wanted was rejected (422)
5 |
6 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
The change you wanted was rejected.
62 |
Maybe you tried to change something you didn't have access to.
63 |
64 |
If you are the application owner check the logs for more information.
65 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/public/404.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | The page you were looking for doesn't exist (404)
5 |
6 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
The page you were looking for doesn't exist.
62 |
You may have mistyped the address or the page may have moved.
63 |
64 |
If you are the application owner check the logs for more information.
65 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/sass/components/card.sass:
--------------------------------------------------------------------------------
1 | $card-color: $text !default
2 | $card-background-color: $white !default
3 | $card-shadow: 0 2px 3px rgba($black, 0.1), 0 0 0 1px rgba($black, 0.1) !default
4 |
5 | $card-header-background-color: transparent !default
6 | $card-header-color: $text-strong !default
7 | $card-header-shadow: 0 1px 2px rgba($black, 0.1) !default
8 | $card-header-weight: $weight-bold !default
9 |
10 | $card-content-background-color: transparent !default
11 |
12 | $card-footer-background-color: transparent !default
13 | $card-footer-border-top: 1px solid $border !default
14 |
15 | .card
16 | background-color: $card-background-color
17 | box-shadow: $card-shadow
18 | color: $card-color
19 | max-width: 100%
20 | position: relative
21 |
22 | .card-header
23 | background-color: $card-header-background-color
24 | align-items: stretch
25 | box-shadow: $card-header-shadow
26 | display: flex
27 |
28 | .card-header-title
29 | align-items: center
30 | color: $card-header-color
31 | display: flex
32 | flex-grow: 1
33 | font-weight: $card-header-weight
34 | padding: 0.75rem
35 | &.is-centered
36 | justify-content: center
37 |
38 | .card-header-icon
39 | align-items: center
40 | cursor: pointer
41 | display: flex
42 | justify-content: center
43 | padding: 0.75rem
44 |
45 | .card-image
46 | display: block
47 | position: relative
48 |
49 | .card-content
50 | background-color: $card-content-background-color
51 | padding: 1.5rem
52 |
53 | .card-footer
54 | background-color: $card-footer-background-color
55 | border-top: $card-footer-border-top
56 | align-items: stretch
57 | display: flex
58 |
59 | .card-footer-item
60 | align-items: center
61 | display: flex
62 | flex-basis: 0
63 | flex-grow: 1
64 | flex-shrink: 0
65 | justify-content: center
66 | padding: 0.75rem
67 | &:not(:last-child)
68 | border-right: $card-footer-border-top
69 |
70 | // Combinations
71 |
72 | .card
73 | .media:not(:last-child)
74 | margin-bottom: 0.75rem
75 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/sass/components/breadcrumb.sass:
--------------------------------------------------------------------------------
1 | $breadcrumb-item-color: $link !default
2 | $breadcrumb-item-hover-color: $link-hover !default
3 | $breadcrumb-item-active-color: $text-strong !default
4 |
5 | $breadcrumb-item-padding-vertical: 0 !default
6 | $breadcrumb-item-padding-horizontal: 0.75em !default
7 |
8 | $breadcrumb-item-separator-color: $grey-light !default
9 |
10 | .breadcrumb
11 | @extend %block
12 | @extend %unselectable
13 | font-size: $size-normal
14 | white-space: nowrap
15 | a
16 | align-items: center
17 | color: $breadcrumb-item-color
18 | display: flex
19 | justify-content: center
20 | padding: $breadcrumb-item-padding-vertical $breadcrumb-item-padding-horizontal
21 | &:hover
22 | color: $breadcrumb-item-hover-color
23 | li
24 | align-items: center
25 | display: flex
26 | &:first-child a
27 | padding-left: 0
28 | &.is-active
29 | a
30 | color: $breadcrumb-item-active-color
31 | cursor: default
32 | pointer-events: none
33 | & + li::before
34 | color: $breadcrumb-item-separator-color
35 | content: "\0002f"
36 | ul,
37 | ol
38 | align-items: flex-start
39 | display: flex
40 | flex-wrap: wrap
41 | justify-content: flex-start
42 | .icon
43 | &:first-child
44 | margin-right: 0.5em
45 | &:last-child
46 | margin-left: 0.5em
47 | // Alignment
48 | &.is-centered
49 | ol,
50 | ul
51 | justify-content: center
52 | &.is-right
53 | ol,
54 | ul
55 | justify-content: flex-end
56 | // Sizes
57 | &.is-small
58 | font-size: $size-small
59 | &.is-medium
60 | font-size: $size-medium
61 | &.is-large
62 | font-size: $size-large
63 | // Styles
64 | &.has-arrow-separator
65 | li + li::before
66 | content: "\02192"
67 | &.has-bullet-separator
68 | li + li::before
69 | content: "\02022"
70 | &.has-dot-separator
71 | li + li::before
72 | content: "\000b7"
73 | &.has-succeeds-separator
74 | li + li::before
75 | content: "\0227B"
76 |
--------------------------------------------------------------------------------
/config/deploy.rb:
--------------------------------------------------------------------------------
1 | # config valid for current version and patch releases of Capistrano
2 | lock "~> 3.14.1"
3 |
4 | set :application, "hnpwa"
5 | set :repo_url, "git@github.com:johnbeatty/hnpwa-app.git"
6 |
7 |
8 | # Default branch is :master
9 | # ask :branch, `git rev-parse --abbrev-ref HEAD`.chomp
10 |
11 | # Default deploy_to directory is /var/www/my_app_name
12 | set :deploy_to, "/home/deploy/hnpwa"
13 |
14 | # Default value for :format is :airbrussh.
15 | # set :format, :airbrussh
16 |
17 | # You can configure the Airbrussh format using :format_options.
18 | # These are the defaults.
19 | # set :format_options, command_output: true, log_file: "log/capistrano.log", color: :auto, truncate: :auto
20 |
21 | # Default value for :pty is false
22 | # set :pty, true
23 |
24 | # Default value for :linked_files is []
25 | # append :linked_files, "config/database.yml"
26 | append :linked_files, "config/master.key"
27 | append :linked_files, "config/database.yml"
28 |
29 | # Default value for linked_dirs is []
30 | append :linked_dirs, 'log', 'tmp/pids', 'tmp/cache', 'tmp/sockets', 'vendor/bundle', '.bundle', 'public/system', 'public/uploads'
31 |
32 |
33 | # Default value for default_env is {}
34 | # set :default_env, { path: "/opt/ruby/bin:$PATH" }
35 |
36 | # Default value for local_user is ENV['USER']
37 | # set :local_user, -> { `git config user.name`.chomp }
38 |
39 | # Default value for keep_releases is 5
40 | # set :keep_releases, 5
41 |
42 | # Uncomment the following to require manually verifying the host key before first deploy.
43 | # set :ssh_options, verify_host_key: :secure
44 |
45 | namespace :sidekiq do
46 | task :quiet do
47 | on roles(:app) do
48 | puts capture("pgrep -f 'sidekiq' | xargs kill -TSTP")
49 | end
50 | end
51 | task :restart do
52 | on roles(:app) do
53 | execute :sudo, :systemctl, :restart, :sidekiq
54 | execute :sudo, :systemctl, :restart, :sidekiq_comments
55 | end
56 | end
57 | end
58 |
59 | after 'deploy:starting', 'sidekiq:quiet'
60 | after 'deploy:reverted', 'sidekiq:restart'
61 | after 'deploy:published', 'sidekiq:restart'
62 |
63 |
64 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = function(api) {
2 | var validEnv = ['development', 'test', 'production']
3 | var currentEnv = api.env()
4 | var isDevelopmentEnv = api.env('development')
5 | var isProductionEnv = api.env('production')
6 | var isTestEnv = api.env('test')
7 |
8 | if (!validEnv.includes(currentEnv)) {
9 | throw new Error(
10 | 'Please specify a valid `NODE_ENV` or ' +
11 | '`BABEL_ENV` environment variables. Valid values are "development", ' +
12 | '"test", and "production". Instead, received: ' +
13 | JSON.stringify(currentEnv) +
14 | '.'
15 | )
16 | }
17 |
18 | return {
19 | presets: [
20 | isTestEnv && [
21 | require('@babel/preset-env').default,
22 | {
23 | targets: {
24 | node: 'current'
25 | }
26 | }
27 | ],
28 | (isProductionEnv || isDevelopmentEnv) && [
29 | require('@babel/preset-env').default,
30 | {
31 | forceAllTransforms: true,
32 | useBuiltIns: 'entry',
33 | modules: false,
34 | exclude: ['transform-typeof-symbol']
35 | }
36 | ]
37 | ].filter(Boolean),
38 | plugins: [
39 | require('babel-plugin-macros'),
40 | require('@babel/plugin-syntax-dynamic-import').default,
41 | isTestEnv && require('babel-plugin-dynamic-import-node'),
42 | require('@babel/plugin-transform-destructuring').default,
43 | [
44 | require('@babel/plugin-proposal-class-properties').default,
45 | {
46 | loose: true
47 | }
48 | ],
49 | [
50 | require('@babel/plugin-proposal-object-rest-spread').default,
51 | {
52 | useBuiltIns: true
53 | }
54 | ],
55 | [
56 | require('@babel/plugin-transform-runtime').default,
57 | {
58 | helpers: false,
59 | regenerator: true
60 | }
61 | ],
62 | [
63 | require('@babel/plugin-transform-regenerator').default,
64 | {
65 | async: false
66 | }
67 | ]
68 | ].filter(Boolean)
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/sass/elements/progress.sass:
--------------------------------------------------------------------------------
1 | $progress-bar-background-color: $border !default
2 | $progress-value-background-color: $text !default
3 |
4 | $progress-indeterminate-duration: 1.5s !default
5 |
6 | .progress
7 | @extend %block
8 | -moz-appearance: none
9 | -webkit-appearance: none
10 | border: none
11 | border-radius: $radius-rounded
12 | display: block
13 | height: $size-normal
14 | overflow: hidden
15 | padding: 0
16 | width: 100%
17 | &::-webkit-progress-bar
18 | background-color: $progress-bar-background-color
19 | &::-webkit-progress-value
20 | background-color: $progress-value-background-color
21 | &::-moz-progress-bar
22 | background-color: $progress-value-background-color
23 | &::-ms-fill
24 | background-color: $progress-value-background-color
25 | border: none
26 | &:indeterminate
27 | animation-duration: $progress-indeterminate-duration
28 | animation-iteration-count: infinite
29 | animation-name: moveIndeterminate
30 | animation-timing-function: linear
31 | background-color: $progress-bar-background-color
32 | background-image: linear-gradient(to right, $text 30%, $progress-bar-background-color 30%)
33 | background-position: top left
34 | background-repeat: no-repeat
35 | background-size: 150% 150%
36 | &::-webkit-progress-bar
37 | background-color: transparent
38 | &::-moz-progress-bar
39 | background-color: transparent
40 | // Colors
41 | @each $name, $pair in $colors
42 | $color: nth($pair, 1)
43 | &.is-#{$name}
44 | &::-webkit-progress-value
45 | background-color: $color
46 | &::-moz-progress-bar
47 | background-color: $color
48 | &::-ms-fill
49 | background-color: $color
50 | &:indeterminate
51 | background-image: linear-gradient(to right, $color 30%, $progress-bar-background-color 30%)
52 |
53 | // Sizes
54 | &.is-small
55 | height: $size-small
56 | &.is-medium
57 | height: $size-medium
58 | &.is-large
59 | height: $size-large
60 |
61 | @keyframes moveIndeterminate
62 | from
63 | background-position: 200% 0
64 | to
65 | background-position: -200% 0
66 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/sass/components/dropdown.sass:
--------------------------------------------------------------------------------
1 | $dropdown-content-background-color: $white !default
2 | $dropdown-content-arrow: $link !default
3 | $dropdown-content-offset: 4px !default
4 | $dropdown-content-radius: $radius !default
5 | $dropdown-content-shadow: 0 2px 3px rgba($black, 0.1), 0 0 0 1px rgba($black, 0.1) !default
6 | $dropdown-content-z: 20 !default
7 |
8 | $dropdown-item-color: $grey-dark !default
9 | $dropdown-item-hover-color: $black !default
10 | $dropdown-item-hover-background-color: $background !default
11 | $dropdown-item-active-color: $link-invert !default
12 | $dropdown-item-active-background-color: $link !default
13 |
14 | $dropdown-divider-background-color: $border !default
15 |
16 | .dropdown
17 | display: inline-flex
18 | position: relative
19 | vertical-align: top
20 | &.is-active,
21 | &.is-hoverable:hover
22 | .dropdown-menu
23 | display: block
24 | &.is-right
25 | .dropdown-menu
26 | left: auto
27 | right: 0
28 | &.is-up
29 | .dropdown-menu
30 | bottom: 100%
31 | padding-bottom: $dropdown-content-offset
32 | padding-top: initial
33 | top: auto
34 |
35 | .dropdown-menu
36 | display: none
37 | left: 0
38 | min-width: 12rem
39 | padding-top: $dropdown-content-offset
40 | position: absolute
41 | top: 100%
42 | z-index: $dropdown-content-z
43 |
44 | .dropdown-content
45 | background-color: $dropdown-content-background-color
46 | border-radius: $dropdown-content-radius
47 | box-shadow: $dropdown-content-shadow
48 | padding-bottom: 0.5rem
49 | padding-top: 0.5rem
50 |
51 | .dropdown-item
52 | color: $dropdown-item-color
53 | display: block
54 | font-size: 0.875rem
55 | line-height: 1.5
56 | padding: 0.375rem 1rem
57 | position: relative
58 |
59 | a.dropdown-item,
60 | button.dropdown-item
61 | padding-right: 3rem
62 | text-align: left
63 | white-space: nowrap
64 | width: 100%
65 | &:hover
66 | background-color: $dropdown-item-hover-background-color
67 | color: $dropdown-item-hover-color
68 | &.is-active
69 | background-color: $dropdown-item-active-background-color
70 | color: $dropdown-item-active-color
71 |
72 | .dropdown-divider
73 | background-color: $dropdown-divider-background-color
74 | border: none
75 | display: block
76 | height: 1px
77 | margin: 0.5rem 0
78 |
--------------------------------------------------------------------------------
/config/webpacker.yml:
--------------------------------------------------------------------------------
1 | # Note: You must restart bin/webpack-dev-server for changes to take effect
2 |
3 | default: &default
4 | source_path: app/javascript
5 | source_entry_path: packs
6 | public_root_path: public
7 | public_output_path: packs
8 | cache_path: tmp/cache/webpacker
9 | check_yarn_integrity: false
10 | webpack_compile_output: false
11 |
12 | # Additional paths webpack should lookup modules
13 | # ['app/assets', 'engine/foo/app/assets']
14 | resolved_paths: []
15 |
16 | # Reload manifest.json on all requests so we reload latest compiled packs
17 | cache_manifest: false
18 |
19 | # Extract and emit a css file
20 | extract_css: false
21 |
22 | static_assets_extensions:
23 | - .jpg
24 | - .jpeg
25 | - .png
26 | - .gif
27 | - .tiff
28 | - .ico
29 | - .svg
30 | - .eot
31 | - .otf
32 | - .ttf
33 | - .woff
34 | - .woff2
35 |
36 | extensions:
37 | - .mjs
38 | - .js
39 | - .sass
40 | - .scss
41 | - .css
42 | - .module.sass
43 | - .module.scss
44 | - .module.css
45 | - .png
46 | - .svg
47 | - .gif
48 | - .jpeg
49 | - .jpg
50 |
51 | development:
52 | <<: *default
53 | compile: true
54 |
55 | # Verifies that versions and hashed value of the package contents in the project's package.json
56 | check_yarn_integrity: true
57 |
58 | # Reference: https://webpack.js.org/configuration/dev-server/
59 | dev_server:
60 | https: false
61 | host: localhost
62 | port: 3035
63 | public: localhost:3035
64 | hmr: false
65 | # Inline should be set to true if using HMR
66 | inline: true
67 | overlay: true
68 | compress: true
69 | disable_host_check: true
70 | use_local_ip: false
71 | quiet: false
72 | headers:
73 | 'Access-Control-Allow-Origin': '*'
74 | watch_options:
75 | ignored: '**/node_modules/**'
76 |
77 |
78 | test:
79 | <<: *default
80 | compile: true
81 |
82 | # Compile test packs to a separate directory
83 | public_output_path: packs-test
84 |
85 | production:
86 | <<: *default
87 |
88 | # Production depends on precompilation of packs prior to booting for performance.
89 | compile: false
90 |
91 | # Extract and emit a css file
92 | extract_css: true
93 |
94 | # Cache manifest.json for performance
95 | cache_manifest: true
96 |
--------------------------------------------------------------------------------
/config/deploy/staging.rb:
--------------------------------------------------------------------------------
1 | # server-based syntax
2 | # ======================
3 | # Defines a single server with a list of roles and multiple properties.
4 | # You can define all roles on a single server, or split them:
5 |
6 | # server "example.com", user: "deploy", roles: %w{app db web}, my_property: :my_value
7 | # server "example.com", user: "deploy", roles: %w{app web}, other_property: :other_value
8 | # server "db.example.com", user: "deploy", roles: %w{db}
9 |
10 |
11 |
12 | # role-based syntax
13 | # ==================
14 |
15 | # Defines a role with one or multiple servers. The primary server in each
16 | # group is considered to be the first unless any hosts have the primary
17 | # property set. Specify the username and a domain or IP for the server.
18 | # Don't use `:all`, it's a meta role.
19 |
20 | # role :app, %w{deploy@example.com}, my_property: :my_value
21 | # role :web, %w{user1@primary.com user2@additional.com}, other_property: :other_value
22 | # role :db, %w{deploy@example.com}
23 |
24 |
25 |
26 | # Configuration
27 | # =============
28 | # You can set any configuration variable like in config/deploy.rb
29 | # These variables are then only loaded and set in this stage.
30 | # For available Capistrano configuration variables see the documentation page.
31 | # http://capistranorb.com/documentation/getting-started/configuration/
32 | # Feel free to add new variables to customise your setup.
33 |
34 |
35 |
36 | # Custom SSH Options
37 | # ==================
38 | # You may pass any option but keep in mind that net/ssh understands a
39 | # limited set of options, consult the Net::SSH documentation.
40 | # http://net-ssh.github.io/net-ssh/classes/Net/SSH.html#method-c-start
41 | #
42 | # Global options
43 | # --------------
44 | # set :ssh_options, {
45 | # keys: %w(/home/rlisowski/.ssh/id_rsa),
46 | # forward_agent: false,
47 | # auth_methods: %w(password)
48 | # }
49 | #
50 | # The server-based syntax can be used to override options:
51 | # ------------------------------------
52 | # server "example.com",
53 | # user: "user_name",
54 | # roles: %w{web app},
55 | # ssh_options: {
56 | # user: "user_name", # overrides user setting above
57 | # keys: %w(/home/user_name/.ssh/id_rsa),
58 | # forward_agent: false,
59 | # auth_methods: %w(publickey password)
60 | # # password: "please use keys"
61 | # }
62 |
--------------------------------------------------------------------------------
/app/javascript/controllers/swipable_controller.js:
--------------------------------------------------------------------------------
1 | // Huge thanks to Ana Tudor via https://codepen.io/thebabydino/pen/PRWqMg/ https://css-tricks.com/simple-swipe-with-vanilla-javascript/
2 |
3 | const MOVE_PAGE_THRESHOLD = 0.3;
4 |
5 | import { Controller } from "stimulus";
6 | import Swipe from "swipejs";
7 |
8 | export default class extends Controller {
9 | static targets = ["view"];
10 |
11 | initialize() {
12 | this.i = 0;
13 | this.x0 = null;
14 | this.locked = false;
15 | this.ini;
16 | }
17 |
18 | connect() {
19 | addEventListener("resize", this.size.bind(this), false);
20 |
21 | this.viewTarget.addEventListener("mousedown", this.lock.bind(this), false);
22 | this.viewTarget.addEventListener("touchstart", this.lock.bind(this), false);
23 |
24 | this.viewTarget.addEventListener("mouseup", this.move.bind(this), false);
25 | this.viewTarget.addEventListener("touchend", this.move.bind(this), false);
26 |
27 | this.mySwipe = new Swipe(this.viewTarget, {
28 | draggable: true,
29 | continuous: false,
30 | });
31 |
32 | this.size();
33 | }
34 |
35 | lock(event) {
36 | let initialX = unify(event).clientX;
37 | if (
38 | initialX < this.quarterWidth ||
39 | initialX + this.quarterWidth > this.width
40 | ) {
41 | this.x0 = initialX;
42 | this.locked = true;
43 | }
44 | }
45 |
46 | move(event) {
47 | if (this.locked) {
48 | let dx = unify(event).clientX - this.x0;
49 | let s = Math.sign(dx);
50 | let f = +((s * dx) / this.width).toFixed(2);
51 |
52 | this.ini = this.i - s * f;
53 |
54 | if (this.ini < -MOVE_PAGE_THRESHOLD) {
55 | window.history.back();
56 | } else if (this.ini > MOVE_PAGE_THRESHOLD) {
57 | window.history.forward();
58 | }
59 |
60 | if ((this.i > 0 || s < 0) && (this.i < 0 || s > 0) && f > 0.2) {
61 | this.i -= s;
62 | f = 1 - f;
63 | }
64 |
65 | this.x0 = null;
66 | this.locked = false;
67 | }
68 | }
69 |
70 | size() {
71 | this.width = window.innerWidth;
72 | this.quarterWidth = this.width * MOVE_PAGE_THRESHOLD;
73 | }
74 | }
75 |
76 | function easeInOut(k) {
77 | return 0.5 * (Math.sin((k - 0.5) * Math.PI) + 1);
78 | }
79 |
80 | function unify(event) {
81 | return event.changedTouches ? event.changedTouches[0] : event;
82 | }
83 |
--------------------------------------------------------------------------------
/config/deploy/production.rb:
--------------------------------------------------------------------------------
1 | # server-based syntax
2 | # ======================
3 | # Defines a single server with a list of roles and multiple properties.
4 | # You can define all roles on a single server, or split them:
5 |
6 | server "45.33.48.112", user: "deploy", roles: %w(web app db)
7 | # server "example.com", user: "deploy", roles: %w{app db web}, my_property: :my_value
8 | # server "example.com", user: "deploy", roles: %w{app web}, other_property: :other_value
9 | # server "db.example.com", user: "deploy", roles: %w{db}
10 |
11 |
12 |
13 | # role-based syntax
14 | # ==================
15 |
16 | # Defines a role with one or multiple servers. The primary server in each
17 | # group is considered to be the first unless any hosts have the primary
18 | # property set. Specify the username and a domain or IP for the server.
19 | # Don't use `:all`, it's a meta role.
20 |
21 | # role :app, %w{deploy@example.com}, my_property: :my_value
22 | # role :web, %w{user1@primary.com user2@additional.com}, other_property: :other_value
23 | # role :db, %w{deploy@example.com}
24 |
25 |
26 |
27 | # Configuration
28 | # =============
29 | # You can set any configuration variable like in config/deploy.rb
30 | # These variables are then only loaded and set in this stage.
31 | # For available Capistrano configuration variables see the documentation page.
32 | # http://capistranorb.com/documentation/getting-started/configuration/
33 | # Feel free to add new variables to customise your setup.
34 |
35 |
36 |
37 | # Custom SSH Options
38 | # ==================
39 | # You may pass any option but keep in mind that net/ssh understands a
40 | # limited set of options, consult the Net::SSH documentation.
41 | # http://net-ssh.github.io/net-ssh/classes/Net/SSH.html#method-c-start
42 | #
43 | # Global options
44 | # --------------
45 | # set :ssh_options, {
46 | # keys: %w(/home/rlisowski/.ssh/id_rsa),
47 | # forward_agent: false,
48 | # auth_methods: %w(password)
49 | # }
50 | #
51 | # The server-based syntax can be used to override options:
52 | # ------------------------------------
53 | # server "example.com",
54 | # user: "user_name",
55 | # roles: %w{web app},
56 | # ssh_options: {
57 | # user: "user_name", # overrides user setting above
58 | # keys: %w(/home/user_name/.ssh/id_rsa),
59 | # forward_agent: false,
60 | # auth_methods: %w(publickey password)
61 | # # password: "please use keys"
62 | # }
63 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/sass/utilities/initial-variables.sass:
--------------------------------------------------------------------------------
1 | // Colors
2 |
3 | $black: hsl(0, 0%, 4%) !default
4 | $black-bis: hsl(0, 0%, 7%) !default
5 | $black-ter: hsl(0, 0%, 14%) !default
6 |
7 | $grey-darker: hsl(0, 0%, 21%) !default
8 | $grey-dark: hsl(0, 0%, 29%) !default
9 | $grey: hsl(0, 0%, 48%) !default
10 | $grey-light: hsl(0, 0%, 71%) !default
11 | $grey-lighter: hsl(0, 0%, 86%) !default
12 |
13 | $white-ter: hsl(0, 0%, 96%) !default
14 | $white-bis: hsl(0, 0%, 98%) !default
15 | $white: hsl(0, 0%, 100%) !default
16 |
17 | $orange: hsl(14, 100%, 53%) !default
18 | $yellow: hsl(48, 100%, 67%) !default
19 | $green: hsl(141, 71%, 48%) !default
20 | $turquoise: hsl(171, 100%, 41%) !default
21 | $cyan: hsl(204, 86%, 53%) !default
22 | $blue: hsl(217, 71%, 53%) !default
23 | $purple: hsl(271, 100%, 71%) !default
24 | $red: hsl(348, 100%, 61%) !default
25 |
26 | // Typography
27 |
28 | $family-sans-serif: BlinkMacSystemFont, -apple-system, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", "Helvetica", "Arial", sans-serif !default
29 | $family-monospace: monospace !default
30 | $render-mode: optimizeLegibility !default
31 |
32 | $size-1: 3rem !default
33 | $size-2: 2.5rem !default
34 | $size-3: 2rem !default
35 | $size-4: 1.5rem !default
36 | $size-5: 1.25rem !default
37 | $size-6: 1rem !default
38 | $size-7: 0.75rem !default
39 |
40 | $weight-light: 300 !default
41 | $weight-normal: 400 !default
42 | $weight-medium: 500 !default
43 | $weight-semibold: 600 !default
44 | $weight-bold: 700 !default
45 |
46 | // Responsiveness
47 |
48 | // The container horizontal gap, which acts as the offset for breakpoints
49 | $gap: 64px !default
50 | // 960, 1152, and 1344 have been chosen because they are divisible by both 12 and 16
51 | $tablet: 769px !default
52 | // 960px container + 4rem
53 | $desktop: 960px + (2 * $gap) !default
54 | // 1152px container + 4rem
55 | $widescreen: 1152px + (2 * $gap) !default
56 | $widescreen-enabled: true !default
57 | // 1344px container + 4rem
58 | $fullhd: 1344px + (2 * $gap) !default
59 | $fullhd-enabled: true !default
60 |
61 | // Miscellaneous
62 |
63 | $easing: ease-out !default
64 | $radius-small: 2px !default
65 | $radius: 4px !default
66 | $radius-large: 6px !default
67 | $radius-rounded: 290486px !default
68 | $speed: 86ms !default
69 |
70 | // Flags
71 |
72 | $variable-columns: true !default
73 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 | git_source(:github) { |repo| "https://github.com/#{repo}.git" }
3 |
4 | ruby '2.7.2'
5 |
6 | # Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
7 | gem 'rails', '~> 6.1'
8 | # Use postgres as the database for Active Record
9 | gem 'pg', '>= 0.18', '< 2.0'
10 | # Use Puma as the app server
11 | gem 'puma', '~> 4.1'
12 | # Use SCSS for stylesheets
13 | gem 'sass-rails', '~> 5'
14 | # Transpile app-like JavaScript. Read more: https://github.com/rails/webpacker
15 | gem 'webpacker', '~> 4.0'
16 | # Turbolinks makes navigating your web application faster. Read more: https://github.com/turbolinks/turbolinks
17 | gem 'turbolinks', '~> 5'
18 | # Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder
19 | gem 'jbuilder', '~> 2.5'
20 | # Use Redis adapter to run Action Cable in production
21 | gem 'redis', '~> 4.0'
22 | # Use ActiveModel has_secure_password
23 | # gem 'bcrypt', '~> 3.1.7'
24 |
25 | # Use ActiveStorage variant
26 | # gem 'mini_magick', '~> 4.8'
27 |
28 | # Reduces boot times through caching; required in config/boot.rb
29 | gem 'bootsnap', '>= 1.4.2', require: false
30 |
31 | group :development, :test do
32 | # Call 'byebug' anywhere in the code to stop execution and get a debugger console
33 | gem 'byebug', platforms: [:mri, :mingw, :x64_mingw]
34 | end
35 |
36 | group :development do
37 | # Access an interactive console on exception pages or by calling 'console' anywhere in the code.
38 | gem 'web-console', '>= 3.3.0'
39 | gem 'listen', '>= 3.0.5', '< 3.2'
40 | # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring
41 | gem 'spring'
42 | gem 'spring-watcher-listen', '~> 2.0.0'
43 | gem 'foreman'
44 | gem 'capistrano-rails'
45 | gem 'capistrano-sidekiq'
46 | gem 'capistrano-passenger'
47 | gem 'capistrano-rbenv'
48 | end
49 |
50 | group :test do
51 | # Adds support for Capybara system testing and selenium driver
52 | gem 'capybara', '>= 2.15'
53 | gem 'selenium-webdriver'
54 | # Easy installation and use of web drivers to run system tests with browsers
55 | gem 'webdrivers'
56 | end
57 |
58 | # Windows does not include zoneinfo files, so bundle the tzinfo-data gem
59 | gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]
60 |
61 | gem 'http'
62 |
63 | gem 'local_time'
64 |
65 | gem 'sidekiq'
66 | gem 'sidekiq-unique-jobs'
67 |
68 | gem 'dalli'
69 |
70 | gem 'whenever', :require => false
71 |
--------------------------------------------------------------------------------
/config/environments/test.rb:
--------------------------------------------------------------------------------
1 | require "active_support/core_ext/integer/time"
2 |
3 | # The test environment is used exclusively to run your application's
4 | # test suite. You never need to work with it otherwise. Remember that
5 | # your test database is "scratch space" for the test suite and is wiped
6 | # and recreated between test runs. Don't rely on the data there!
7 |
8 | Rails.application.configure do
9 | # Settings specified here will take precedence over those in config/application.rb.
10 |
11 | config.cache_classes = false
12 | config.action_view.cache_template_loading = true
13 |
14 | # Do not eager load code on boot. This avoids loading your whole application
15 | # just for the purpose of running a single test. If you are using a tool that
16 | # preloads Rails for running tests, you may have to set it to true.
17 | config.eager_load = false
18 |
19 | # Configure public file server for tests with Cache-Control for performance.
20 | config.public_file_server.enabled = true
21 | config.public_file_server.headers = {
22 | 'Cache-Control' => "public, max-age=#{1.hour.to_i}"
23 | }
24 |
25 | # Show full error reports and disable caching.
26 | config.consider_all_requests_local = true
27 | config.action_controller.perform_caching = false
28 | config.cache_store = :null_store
29 |
30 | # Raise exceptions instead of rendering exception templates.
31 | config.action_dispatch.show_exceptions = false
32 |
33 | # Disable request forgery protection in test environment.
34 | config.action_controller.allow_forgery_protection = false
35 |
36 | # Store uploaded files on the local file system in a temporary directory.
37 | config.active_storage.service = :test
38 |
39 | config.action_mailer.perform_caching = false
40 |
41 | # Tell Action Mailer not to deliver emails to the real world.
42 | # The :test delivery method accumulates sent emails in the
43 | # ActionMailer::Base.deliveries array.
44 | config.action_mailer.delivery_method = :test
45 |
46 | # Print deprecation notices to the stderr.
47 | config.active_support.deprecation = :stderr
48 |
49 | # Raise exceptions for disallowed deprecations.
50 | config.active_support.disallowed_deprecation = :raise
51 |
52 | # Tell Active Support which deprecation messages to disallow.
53 | config.active_support.disallowed_deprecation_warnings = []
54 |
55 | # Raises error for missing translations.
56 | # config.i18n.raise_on_missing_translations = true
57 |
58 | # Annotate rendered view with file names.
59 | # config.action_view.annotate_rendered_view_with_filenames = true
60 | end
61 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/sass/utilities/functions.sass:
--------------------------------------------------------------------------------
1 | @function mergeColorMaps($bulma-colors, $custom-colors)
2 | // we return at least bulma hardcoded colors
3 | $merged-colors: $bulma-colors
4 |
5 | // we want a map as input
6 | @if type-of($custom-colors) == 'map'
7 | @each $name, $components in $custom-colors
8 | // color name should be a string and colors pair a list with at least one element
9 | @if type-of($name) == 'string' and (type-of($components) == 'list' or type-of($components) == 'color') and length($components) >= 1
10 | $color-base: null
11 |
12 | // the param can either be a single color
13 | // or a list of 2 colors
14 | @if type-of($components) == 'color'
15 | $color-base: $components
16 | @else if type-of($components) == 'list'
17 | $color-base: nth($components, 1)
18 |
19 | $color-invert: null
20 | // is an inverted color provided in the list
21 | @if length($components) > 1
22 | $color-invert: nth($components, 2)
23 |
24 | // we only want a color as base color
25 | @if type-of($color-base) == 'color'
26 | // if inverted color is not provided or is not a color we compute it
27 | @if type-of($color-invert) != 'color'
28 | $color-invert: findColorInvert($color-base)
29 |
30 | // we merge this colors elements as map with bulma colors (we can override them this way, no multiple definition for the same name)
31 | $merged-colors: map_merge($merged-colors, ($name: ($color-base, $color-invert)))
32 |
33 | @return $merged-colors
34 |
35 | @function powerNumber($number, $exp)
36 | $value: 1
37 | @if $exp > 0
38 | @for $i from 1 through $exp
39 | $value: $value * $number
40 | @else if $exp < 0
41 | @for $i from 1 through -$exp
42 | $value: $value / $number
43 | @return $value
44 |
45 | @function colorLuminance($color)
46 | $color-rgb: ('red': red($color),'green': green($color),'blue': blue($color))
47 | @each $name, $value in $color-rgb
48 | $adjusted: 0
49 | $value: $value / 255
50 | @if $value < 0.03928
51 | $value: $value / 12.92
52 | @else
53 | $value: ($value + .055) / 1.055
54 | $value: powerNumber($value, 2)
55 | $color-rgb: map-merge($color-rgb, ($name: $value))
56 | @return (map-get($color-rgb, 'red') * .2126) + (map-get($color-rgb, 'green') * .7152) + (map-get($color-rgb, 'blue') * .0722)
57 |
58 | @function findColorInvert($color)
59 | @if (colorLuminance($color) > 0.55)
60 | @return rgba(#000, 0.7)
61 | @else
62 | @return #fff
63 |
--------------------------------------------------------------------------------
/app/workers/load_item_details_worker.rb:
--------------------------------------------------------------------------------
1 | class LoadItemDetailsWorker
2 | include Sidekiq::Worker
3 | sidekiq_options queue: 'comments', lock: :until_and_while_executing
4 |
5 |
6 | def perform(hn_id)
7 | @item = Item.find_by_hn_id hn_id
8 |
9 | begin
10 | http = HTTP.persistent "https://hacker-news.firebaseio.com"
11 | item_json = JSON.parse http.get("/v0/item/#{@item.hn_id}.json").to_s
12 | if item_json.nil?
13 | return
14 | end
15 | @item.populate(item_json)
16 | @item.save
17 |
18 | puts "total descendants #{@item.descendants}"
19 | @count = 0
20 | load_kids(http, @item.hn_id, item_json)
21 | puts "total count #{@count}"
22 | ensure
23 | http.close if http
24 | end
25 | @item.touch
26 | if @item.story?
27 | ActionCable.server.broadcast "ItemChannel:#{@item.hn_id}", {
28 | item: ItemsController.render( partial: 'item', locals: {item: @item} ).squish,
29 | comments_header: ItemsController.render( partial: 'comments_header', locals: {item: @item, completed: true} ).squish,
30 | progress: nil,
31 | item_id: @item.hn_id
32 | }
33 | ActionCable.server.broadcast "ItemsListChannel:#{@item.id}", {
34 | item: ItemsController.render( @item ).squish,
35 | item_id: @item.id
36 | }
37 | ActionCable.server.broadcast "CommentsChannel:#{@item.hn_id}", {
38 | comments: ItemsController.render( partial: 'comments', locals: {item: @item} ).squish,
39 | parent_id: @item.hn_id,
40 | item_id: @item.hn_id
41 | }
42 | end
43 | end
44 |
45 | def load_kids(http, parent_id, item_json)
46 | @count += 1
47 | puts "load kids start #{@count}"
48 | ActionCable.server.broadcast "ItemChannel:#{@item.hn_id}", {
49 | item: nil,
50 | comments_header: nil,
51 | progress: [((@count.to_f/ [@item.descendants.to_f, 1].max ) * 100).to_i, 99].min,
52 | item_id: @item.hn_id
53 | }
54 |
55 | if item_json and item_json.has_key? 'kids'
56 | item_json['kids'].each_with_index do |kid_hn_id, kid_location|
57 |
58 | kid_json = JSON.parse http.get("/v0/item/#{kid_hn_id}.json").to_s
59 | if kid_json.nil?
60 | next
61 | end
62 |
63 | kid = Item.where(hn_id: kid_hn_id).first_or_create
64 | kid.kid_location = kid_location
65 | kid.parent_id = parent_id
66 | kid.populate(kid_json)
67 | kid.save
68 |
69 | load_kids(http, kid.hn_id, kid_json)
70 | end
71 | end
72 | puts "load kids stop #{@count}"
73 | end
74 | end
75 |
--------------------------------------------------------------------------------
/app/views/service_worker/service_worker.js.erb:
--------------------------------------------------------------------------------
1 | var CACHE_VERSION = 'v1';
2 | var CACHE_NAME = CACHE_VERSION + ':sw-cache-';
3 | var REQUIRED_FILES = [
4 | '<%= asset_pack_path 'application.js' %>',
5 | '<%= stylesheet_path 'application.css' %>',
6 | '/',
7 | '/top',
8 | '/new',
9 | '/show',
10 | '/ask',
11 | '/job',
12 | '/offline.html',
13 | '<%= asset_path 'cloud-check.svg' %>',
14 | '<%= asset_path 'cloud-download.svg' %>',
15 | '<%= asset_path 'train_48.png' %>',
16 | '<%= asset_path 'train_192.png' %>',
17 | '<%= asset_path 'train_512.png' %>',
18 | ];
19 |
20 | function onInstall(event) {
21 | console.log('[Serviceworker]', "Installing!", event);
22 | event.waitUntil(
23 | caches.open(CACHE_NAME)
24 | .then(function prefill(cache) {
25 | console.log('[install] Caches opened');
26 | return cache.addAll(REQUIRED_FILES);
27 | })
28 | .then(function() {
29 | console.log('[install] All required resources have been cached');
30 | return self.skipWaiting();
31 | })
32 | );
33 | }
34 |
35 | function onActivate(event) {
36 | console.log('[Serviceworker]', "Activating!", event);
37 | event.waitUntil(
38 | caches.keys().then(function(cacheNames) {
39 | return Promise.all(
40 | cacheNames.filter(function(cacheName) {
41 | // Return true if you want to remove this cache,
42 | // but remember that caches are shared across
43 | // the whole origin
44 | return cacheName.indexOf(CACHE_VERSION) !== 0;
45 | }).map(function(cacheName) {
46 | return caches.delete(cacheName);
47 | })
48 | );
49 | })
50 | );
51 | console.log('[activate] Claiming ServiceWorker');
52 | event.waitUntil(self.clients.claim());
53 | }
54 |
55 | // Borrowed from https://github.com/TalAter/UpUp
56 | function onFetch(event) {
57 | event.respondWith(
58 | // try to return untouched request from network first
59 | fetch(event.request).catch(function() {
60 | // if it fails, try to return request from the cache
61 | return caches.match(event.request).then(function(response) {
62 | if (response) {
63 | return response;
64 | }
65 | // if not found in cache, return default offline content for navigate requests
66 | if (event.request.mode === 'navigate' ||
67 | (event.request.method === 'GET' && event.request.headers.get('accept').includes('text/html'))) {
68 | console.log('[Serviceworker]', "Fetching offline content", event);
69 | return caches.match('/offline.html');
70 | }
71 | })
72 | })
73 | );
74 | }
75 |
76 | self.addEventListener('install', onInstall);
77 | self.addEventListener('activate', onActivate);
78 | self.addEventListener('fetch', onFetch);
79 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/sass/base/generic.sass:
--------------------------------------------------------------------------------
1 | $body-background-color: $white !default
2 | $body-size: 16px !default
3 | $body-rendering: optimizeLegibility !default
4 | $body-family: $family-primary !default
5 | $body-color: $text !default
6 | $body-weight: $weight-normal !default
7 | $body-line-height: 1.5 !default
8 |
9 | $code-family: $family-code !default
10 | $code-padding: 0.25em 0.5em 0.25em !default
11 | $code-weight: normal !default
12 | $code-size: 0.875em !default
13 |
14 | $hr-background-color: $background !default
15 | $hr-height: 2px !default
16 | $hr-margin: 1.5rem 0 !default
17 |
18 | $strong-color: $text-strong !default
19 | $strong-weight: $weight-bold !default
20 |
21 | html
22 | background-color: $body-background-color
23 | font-size: $body-size
24 | -moz-osx-font-smoothing: grayscale
25 | -webkit-font-smoothing: antialiased
26 | min-width: 300px
27 | overflow-x: hidden
28 | overflow-y: scroll
29 | text-rendering: $body-rendering
30 | text-size-adjust: 100%
31 |
32 | article,
33 | aside,
34 | figure,
35 | footer,
36 | header,
37 | hgroup,
38 | section
39 | display: block
40 |
41 | body,
42 | button,
43 | input,
44 | select,
45 | textarea
46 | font-family: $body-family
47 |
48 | code,
49 | pre
50 | -moz-osx-font-smoothing: auto
51 | -webkit-font-smoothing: auto
52 | font-family: $code-family
53 |
54 | body
55 | color: $body-color
56 | font-size: 1rem
57 | font-weight: $body-weight
58 | line-height: $body-line-height
59 |
60 | // Inline
61 |
62 | a
63 | color: $link
64 | cursor: pointer
65 | text-decoration: none
66 | strong
67 | color: currentColor
68 | &:hover
69 | color: $link-hover
70 |
71 | code
72 | background-color: $code-background
73 | color: $code
74 | font-size: $code-size
75 | font-weight: $code-weight
76 | padding: $code-padding
77 |
78 | hr
79 | background-color: $hr-background-color
80 | border: none
81 | display: block
82 | height: $hr-height
83 | margin: $hr-margin
84 |
85 | img
86 | height: auto
87 | max-width: 100%
88 |
89 | input[type="checkbox"],
90 | input[type="radio"]
91 | vertical-align: baseline
92 |
93 | small
94 | font-size: 0.875em
95 |
96 | span
97 | font-style: inherit
98 | font-weight: inherit
99 |
100 | strong
101 | color: $strong-color
102 | font-weight: $strong-weight
103 |
104 | // Block
105 |
106 | fieldset
107 | border: none
108 |
109 | pre
110 | +overflow-touch
111 | background-color: $pre-background
112 | color: $pre
113 | font-size: 0.875em
114 | overflow-x: auto
115 | padding: 1.25rem 1.5rem
116 | white-space: pre
117 | word-wrap: normal
118 | code
119 | background-color: transparent
120 | color: currentColor
121 | font-size: 1em
122 | padding: 0
123 |
124 | table
125 | td,
126 | th
127 | text-align: left
128 | vertical-align: top
129 | th
130 | color: $text-strong
131 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/sass/components/message.sass:
--------------------------------------------------------------------------------
1 | $message-background-color: $background !default
2 | $message-radius: $radius !default
3 |
4 | $message-header-background-color: $text !default
5 | $message-header-color: $text-invert !default
6 | $message-header-weight: $weight-bold !default
7 | $message-header-padding: 0.75em 1em !default
8 | $message-header-radius: $radius !default
9 |
10 | $message-body-border-color: $border !default
11 | $message-body-border-width: 0 0 0 4px !default
12 | $message-body-color: $text !default
13 | $message-body-padding: 1.25em 1.5em !default
14 | $message-body-radius: $radius !default
15 |
16 | $message-body-pre-background-color: $white !default
17 | $message-body-pre-code-background-color: transparent !default
18 |
19 | $message-header-body-border-width: 0 !default
20 |
21 | .message
22 | @extend %block
23 | background-color: $message-background-color
24 | border-radius: $message-radius
25 | font-size: $size-normal
26 | strong
27 | color: currentColor
28 | a:not(.button):not(.tag):not(.dropdown-item)
29 | color: currentColor
30 | text-decoration: underline
31 | // Sizes
32 | &.is-small
33 | font-size: $size-small
34 | &.is-medium
35 | font-size: $size-medium
36 | &.is-large
37 | font-size: $size-large
38 | // Colors
39 | @each $name, $pair in $colors
40 | $color: nth($pair, 1)
41 | $color-invert: nth($pair, 2)
42 | $color-lightning: max((100% - lightness($color)) - 2%, 0%)
43 | $color-luminance: colorLuminance($color)
44 | $darken-percentage: $color-luminance * 70%
45 | $desaturate-percentage: $color-luminance * 30%
46 | &.is-#{$name}
47 | background-color: lighten($color, $color-lightning)
48 | .message-header
49 | background-color: $color
50 | color: $color-invert
51 | .message-body
52 | border-color: $color
53 | color: desaturate(darken($color, $darken-percentage), $desaturate-percentage)
54 |
55 | .message-header
56 | align-items: center
57 | background-color: $message-header-background-color
58 | border-radius: $message-header-radius $message-header-radius 0 0
59 | color: $message-header-color
60 | display: flex
61 | font-weight: $message-header-weight
62 | justify-content: space-between
63 | line-height: 1.25
64 | padding: $message-header-padding
65 | position: relative
66 | .delete
67 | flex-grow: 0
68 | flex-shrink: 0
69 | margin-left: 0.75em
70 | & + .message-body
71 | border-width: $message-header-body-border-width
72 | border-top-left-radius: 0
73 | border-top-right-radius: 0
74 |
75 | .message-body
76 | border-color: $message-body-border-color
77 | border-radius: $message-body-radius
78 | border-style: solid
79 | border-width: $message-body-border-width
80 | color: $message-body-color
81 | padding: $message-body-padding
82 | code,
83 | pre
84 | background-color: $message-body-pre-background-color
85 | pre code
86 | background-color: $message-body-pre-code-background-color
87 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/sass/utilities/derived-variables.sass:
--------------------------------------------------------------------------------
1 | $primary: $turquoise !default
2 |
3 | $info: $cyan !default
4 | $success: $green !default
5 | $warning: $yellow !default
6 | $danger: $red !default
7 |
8 | $light: $white-ter !default
9 | $dark: $grey-darker !default
10 |
11 | // Invert colors
12 |
13 | $orange-invert: findColorInvert($orange) !default
14 | $yellow-invert: findColorInvert($yellow) !default
15 | $green-invert: findColorInvert($green) !default
16 | $turquoise-invert: findColorInvert($turquoise) !default
17 | $cyan-invert: findColorInvert($cyan) !default
18 | $blue-invert: findColorInvert($blue) !default
19 | $purple-invert: findColorInvert($purple) !default
20 | $red-invert: findColorInvert($red) !default
21 |
22 | $primary-invert: $turquoise-invert !default
23 | $info-invert: $cyan-invert !default
24 | $success-invert: $green-invert !default
25 | $warning-invert: $yellow-invert !default
26 | $danger-invert: $red-invert !default
27 | $light-invert: $dark !default
28 | $dark-invert: $light !default
29 |
30 | // General colors
31 |
32 | $background: $white-ter !default
33 |
34 | $border: $grey-lighter !default
35 | $border-hover: $grey-light !default
36 |
37 | // Text colors
38 |
39 | $text: $grey-dark !default
40 | $text-invert: findColorInvert($text) !default
41 | $text-light: $grey !default
42 | $text-strong: $grey-darker !default
43 |
44 | // Code colors
45 |
46 | $code: $red !default
47 | $code-background: $background !default
48 |
49 | $pre: $text !default
50 | $pre-background: $background !default
51 |
52 | // Link colors
53 |
54 | $link: $blue !default
55 | $link-invert: $blue-invert !default
56 | $link-visited: $purple !default
57 |
58 | $link-hover: $grey-darker !default
59 | $link-hover-border: $grey-light !default
60 |
61 | $link-focus: $grey-darker !default
62 | $link-focus-border: $blue !default
63 |
64 | $link-active: $grey-darker !default
65 | $link-active-border: $grey-dark !default
66 |
67 | // Typography
68 |
69 | $family-primary: $family-sans-serif !default
70 | $family-secondary: $family-sans-serif !default
71 | $family-code: $family-monospace !default
72 |
73 | $size-small: $size-7 !default
74 | $size-normal: $size-6 !default
75 | $size-medium: $size-5 !default
76 | $size-large: $size-4 !default
77 |
78 | // Lists and maps
79 | $custom-colors: null !default
80 | $custom-shades: null !default
81 |
82 | $colors: mergeColorMaps(("white": ($white, $black), "black": ($black, $white), "light": ($light, $light-invert), "dark": ($dark, $dark-invert), "primary": ($primary, $primary-invert), "link": ($link, $link-invert), "info": ($info, $info-invert), "success": ($success, $success-invert), "warning": ($warning, $warning-invert), "danger": ($danger, $danger-invert)), $custom-colors) !default
83 | $shades: mergeColorMaps(("black-bis": $black-bis, "black-ter": $black-ter, "grey-darker": $grey-darker, "grey-dark": $grey-dark, "grey": $grey, "grey-light": $grey-light, "grey-lighter": $grey-lighter, "white-ter": $white-ter, "white-bis": $white-bis), $custom-shades) !default
84 |
85 | $sizes: $size-1 $size-2 $size-3 $size-4 $size-5 $size-6 $size-7 !default
86 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/sass/components/panel.sass:
--------------------------------------------------------------------------------
1 | $panel-item-border: 1px solid $border !default
2 |
3 | $panel-heading-background-color: $background !default
4 | $panel-heading-color: $text-strong !default
5 | $panel-heading-line-height: 1.25 !default
6 | $panel-heading-padding: 0.5em 0.75em !default
7 | $panel-heading-radius: $radius !default
8 | $panel-heading-size: 1.25em !default
9 | $panel-heading-weight: $weight-light !default
10 |
11 | $panel-tab-border-bottom: 1px solid $border !default
12 | $panel-tab-active-border-bottom-color: $link-active-border !default
13 | $panel-tab-active-color: $link-active !default
14 |
15 | $panel-list-item-color: $text !default
16 | $panel-list-item-hover-color: $link !default
17 |
18 | $panel-block-color: $text-strong !default
19 | $panel-block-hover-background-color: $background !default
20 | $panel-block-active-border-left-color: $link !default
21 | $panel-block-active-color: $link-active !default
22 | $panel-block-active-icon-color: $link !default
23 |
24 | $panel-icon-color: $text-light !default
25 |
26 | .panel
27 | font-size: $size-normal
28 | &:not(:last-child)
29 | margin-bottom: 1.5rem
30 |
31 | .panel-heading,
32 | .panel-tabs,
33 | .panel-block
34 | border-bottom: $panel-item-border
35 | border-left: $panel-item-border
36 | border-right: $panel-item-border
37 | &:first-child
38 | border-top: $panel-item-border
39 |
40 | .panel-heading
41 | background-color: $panel-heading-background-color
42 | border-radius: $panel-heading-radius $panel-heading-radius 0 0
43 | color: $panel-heading-color
44 | font-size: $panel-heading-size
45 | font-weight: $panel-heading-weight
46 | line-height: $panel-heading-line-height
47 | padding: $panel-heading-padding
48 |
49 | .panel-tabs
50 | align-items: flex-end
51 | display: flex
52 | font-size: 0.875em
53 | justify-content: center
54 | a
55 | border-bottom: $panel-tab-border-bottom
56 | margin-bottom: -1px
57 | padding: 0.5em
58 | // Modifiers
59 | &.is-active
60 | border-bottom-color: $panel-tab-active-border-bottom-color
61 | color: $panel-tab-active-color
62 |
63 | .panel-list
64 | a
65 | color: $panel-list-item-color
66 | &:hover
67 | color: $panel-list-item-hover-color
68 |
69 | .panel-block
70 | align-items: center
71 | color: $panel-block-color
72 | display: flex
73 | justify-content: flex-start
74 | padding: 0.5em 0.75em
75 | input[type="checkbox"]
76 | margin-right: 0.75em
77 | & > .control
78 | flex-grow: 1
79 | flex-shrink: 1
80 | width: 100%
81 | &.is-wrapped
82 | flex-wrap: wrap
83 | &.is-active
84 | border-left-color: $panel-block-active-border-left-color
85 | color: $panel-block-active-color
86 | .panel-icon
87 | color: $panel-block-active-icon-color
88 |
89 | a.panel-block,
90 | label.panel-block
91 | cursor: pointer
92 | &:hover
93 | background-color: $panel-block-hover-background-color
94 |
95 | .panel-icon
96 | +fa(14px, 1em)
97 | color: $panel-icon-color
98 | margin-right: 0.75em
99 | .fa
100 | font-size: inherit
101 | line-height: inherit
102 |
--------------------------------------------------------------------------------
/app/views/layouts/application.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Hacker News Progressive Web App
5 | <%= csrf_meta_tags %>
6 | <%= csp_meta_tag %>
7 | <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
8 | <%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
9 | <%= action_cable_meta_tag %>
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | <%= image_tag "train_48.png", width: "23px", alt: "Home Page"%>
21 |
22 |
23 | <%= image_tag "cloud-check.svg", width: "23px", alt: "Webpage saved for offline use", data: { 'service-worker-target':"pageSavedNotice" }, class: "is-hidden" %>
24 | <%= image_tag "cloud-download.svg", width: "23px", alt: "Saving webpage for offline use", data: { 'service-worker-target':"savingPageNotice" }, class: "" %>
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
47 |
48 |
49 |
50 |
51 |
52 | <%= yield %>
53 |
54 |
55 |
56 |
57 |
58 |
59 |
--------------------------------------------------------------------------------
/config/environments/development.rb:
--------------------------------------------------------------------------------
1 | require "active_support/core_ext/integer/time"
2 |
3 | Rails.application.configure do
4 | # Settings specified here will take precedence over those in config/application.rb.
5 |
6 | # In the development environment your application's code is reloaded any time
7 | # it changes. This slows down response time but is perfect for development
8 | # since you don't have to restart the web server when you make code changes.
9 | config.cache_classes = false
10 |
11 | # Do not eager load code on boot.
12 | config.eager_load = false
13 |
14 | # Show full error reports.
15 | config.consider_all_requests_local = true
16 |
17 | # Enable/disable caching. By default caching is disabled.
18 | # Run rails dev:cache to toggle caching.
19 | if Rails.root.join('tmp', 'caching-dev.txt').exist?
20 | config.action_controller.perform_caching = true
21 | config.action_controller.enable_fragment_cache_logging = true
22 |
23 | config.cache_store = :mem_cache_store
24 | config.public_file_server.headers = {
25 | 'Cache-Control' => "public, max-age=#{2.days.to_i}"
26 | }
27 | else
28 | config.action_controller.perform_caching = false
29 |
30 | config.cache_store = :null_store
31 | end
32 |
33 | # Store uploaded files on the local file system (see config/storage.yml for options).
34 | config.active_storage.service = :local
35 |
36 | # Don't care if the mailer can't send.
37 | config.action_mailer.raise_delivery_errors = false
38 |
39 | config.action_mailer.perform_caching = false
40 |
41 | # Print deprecation notices to the Rails logger.
42 | config.active_support.deprecation = :log
43 |
44 | # Raise exceptions for disallowed deprecations.
45 | config.active_support.disallowed_deprecation = :raise
46 |
47 | # Tell Active Support which deprecation messages to disallow.
48 | config.active_support.disallowed_deprecation_warnings = []
49 |
50 | # Raise an error on page load if there are pending migrations.
51 | config.active_record.migration_error = :page_load
52 |
53 | # Highlight code that triggered database queries in logs.
54 | config.active_record.verbose_query_logs = true
55 |
56 | # Debug mode disables concatenation and preprocessing of assets.
57 | # This option may cause significant delays in view rendering with a large
58 | # number of complex assets.
59 | config.assets.debug = true
60 |
61 | # Suppress logger output for asset requests.
62 | config.assets.quiet = true
63 |
64 | # Raises error for missing translations.
65 | # config.i18n.raise_on_missing_translations = true
66 |
67 | # Annotate rendered view with file names.
68 | # config.action_view.annotate_rendered_view_with_filenames = true
69 |
70 | # Use an evented file watcher to asynchronously detect changes in source code,
71 | # routes, locales, etc. This feature depends on the listen gem.
72 | config.file_watcher = ActiveSupport::EventedFileUpdateChecker
73 |
74 | # Uncomment if you wish to allow Action Cable access from any origin.
75 | # config.action_cable.disable_request_forgery_protection = true
76 |
77 | config.hosts << "hnpwa.127.0.0.1.xip.io"
78 | config.hosts << "hnpwa.127.0.0.1.nip.io"
79 | config.hosts << "hnpwa.test"
80 | end
81 |
--------------------------------------------------------------------------------
/config/initializers/new_framework_defaults_6_1.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 | #
3 | # This file contains migration options to ease your Rails 6.1 upgrade.
4 | #
5 | # Once upgraded flip defaults one by one to migrate to the new default.
6 | #
7 | # Read the Guide for Upgrading Ruby on Rails for more info on each option.
8 |
9 | # Support for inversing belongs_to -> has_many Active Record associations.
10 | # Rails.application.config.active_record.has_many_inversing = true
11 |
12 | # Track Active Storage variants in the database.
13 | # Rails.application.config.active_storage.track_variants = true
14 |
15 | # Apply random variation to the delay when retrying failed jobs.
16 | # Rails.application.config.active_job.retry_jitter = 0.15
17 |
18 | # Stop executing `after_enqueue`/`after_perform` callbacks if
19 | # `before_enqueue`/`before_perform` respectively halts with `throw :abort`.
20 | # Rails.application.config.active_job.skip_after_callbacks_if_terminated = true
21 |
22 | # Specify cookies SameSite protection level: either :none, :lax, or :strict.
23 | #
24 | # This change is not backwards compatible with earlier Rails versions.
25 | # It's best enabled when your entire app is migrated and stable on 6.1.
26 | # Rails.application.config.action_dispatch.cookies_same_site_protection = :lax
27 |
28 | # Generate CSRF tokens that are encoded in URL-safe Base64.
29 | #
30 | # This change is not backwards compatible with earlier Rails versions.
31 | # It's best enabled when your entire app is migrated and stable on 6.1.
32 | # Rails.application.config.action_controller.urlsafe_csrf_tokens = true
33 |
34 | # Specify whether `ActiveSupport::TimeZone.utc_to_local` returns a time with an
35 | # UTC offset or a UTC time.
36 | # ActiveSupport.utc_to_local_returns_utc_offset_times = true
37 |
38 | # Change the default HTTP status code to `308` when redirecting non-GET/HEAD
39 | # requests to HTTPS in `ActionDispatch::SSL` middleware.
40 | # Rails.application.config.action_dispatch.ssl_default_redirect_status = 308
41 |
42 | # Use new connection handling API. For most applications this won't have any
43 | # effect. For applications using multiple databases, this new API provides
44 | # support for granular connection swapping.
45 | # Rails.application.config.active_record.legacy_connection_handling = false
46 |
47 | # Make `form_with` generate non-remote forms by default.
48 | # Rails.application.config.action_view.form_with_generates_remote_forms = false
49 |
50 | # Set the default queue name for the analysis job to the queue adapter default.
51 | # Rails.application.config.active_storage.queues.analysis = nil
52 |
53 | # Set the default queue name for the purge job to the queue adapter default.
54 | # Rails.application.config.active_storage.queues.purge = nil
55 |
56 | # Set the default queue name for the incineration job to the queue adapter default.
57 | # Rails.application.config.action_mailbox.queues.incineration = nil
58 |
59 | # Set the default queue name for the routing job to the queue adapter default.
60 | # Rails.application.config.action_mailbox.queues.routing = nil
61 |
62 | # Set the default queue name for the mail deliver job to the queue adapter default.
63 | # Rails.application.config.action_mailer.deliver_later_queue_name = nil
64 |
--------------------------------------------------------------------------------
/bin/bundle:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | # frozen_string_literal: true
3 |
4 | #
5 | # This file was generated by Bundler.
6 | #
7 | # The application 'bundle' is installed as part of a gem, and
8 | # this file is here to facilitate running it.
9 | #
10 |
11 | require "rubygems"
12 |
13 | m = Module.new do
14 | module_function
15 |
16 | def invoked_as_script?
17 | File.expand_path($0) == File.expand_path(__FILE__)
18 | end
19 |
20 | def env_var_version
21 | ENV["BUNDLER_VERSION"]
22 | end
23 |
24 | def cli_arg_version
25 | return unless invoked_as_script? # don't want to hijack other binstubs
26 | return unless "update".start_with?(ARGV.first || " ") # must be running `bundle update`
27 | bundler_version = nil
28 | update_index = nil
29 | ARGV.each_with_index do |a, i|
30 | if update_index && update_index.succ == i && a =~ Gem::Version::ANCHORED_VERSION_PATTERN
31 | bundler_version = a
32 | end
33 | next unless a =~ /\A--bundler(?:[= ](#{Gem::Version::VERSION_PATTERN}))?\z/
34 | bundler_version = $1 || ">= 0.a"
35 | update_index = i
36 | end
37 | bundler_version
38 | end
39 |
40 | def gemfile
41 | gemfile = ENV["BUNDLE_GEMFILE"]
42 | return gemfile if gemfile && !gemfile.empty?
43 |
44 | File.expand_path("../../Gemfile", __FILE__)
45 | end
46 |
47 | def lockfile
48 | lockfile =
49 | case File.basename(gemfile)
50 | when "gems.rb" then gemfile.sub(/\.rb$/, gemfile)
51 | else "#{gemfile}.lock"
52 | end
53 | File.expand_path(lockfile)
54 | end
55 |
56 | def lockfile_version
57 | return unless File.file?(lockfile)
58 | lockfile_contents = File.read(lockfile)
59 | return unless lockfile_contents =~ /\n\nBUNDLED WITH\n\s{2,}(#{Gem::Version::VERSION_PATTERN})\n/
60 | Regexp.last_match(1)
61 | end
62 |
63 | def bundler_version
64 | @bundler_version ||= begin
65 | env_var_version || cli_arg_version ||
66 | lockfile_version || "#{Gem::Requirement.default}.a"
67 | end
68 | end
69 |
70 | def load_bundler!
71 | ENV["BUNDLE_GEMFILE"] ||= gemfile
72 |
73 | # must dup string for RG < 1.8 compatibility
74 | activate_bundler(bundler_version.dup)
75 | end
76 |
77 | def activate_bundler(bundler_version)
78 | if Gem::Version.correct?(bundler_version) && Gem::Version.new(bundler_version).release < Gem::Version.new("2.0")
79 | bundler_version = "< 2"
80 | end
81 | gem_error = activation_error_handling do
82 | gem "bundler", bundler_version
83 | end
84 | return if gem_error.nil?
85 | require_error = activation_error_handling do
86 | require "bundler/version"
87 | end
88 | return if require_error.nil? && Gem::Requirement.new(bundler_version).satisfied_by?(Gem::Version.new(Bundler::VERSION))
89 | warn "Activating bundler (#{bundler_version}) failed:\n#{gem_error.message}\n\nTo install the version of bundler this project requires, run `gem install bundler -v '#{bundler_version}'`"
90 | exit 42
91 | end
92 |
93 | def activation_error_handling
94 | yield
95 | nil
96 | rescue StandardError, LoadError => e
97 | e
98 | end
99 | end
100 |
101 | m.load_bundler!
102 |
103 | if m.invoked_as_script?
104 | load Gem.bin_path("bundler", "bundle")
105 | end
106 |
--------------------------------------------------------------------------------
/db/schema.rb:
--------------------------------------------------------------------------------
1 | # This file is auto-generated from the current state of the database. Instead
2 | # of editing this file, please use the migrations feature of Active Record to
3 | # incrementally modify your database, and then regenerate this schema definition.
4 | #
5 | # Note that this schema.rb definition is the authoritative source for your
6 | # database schema. If you need to create the application database on another
7 | # system, you should be using db:schema:load, not running all the migrations
8 | # from scratch. The latter is a flawed and unsustainable approach (the more migrations
9 | # you'll amass, the slower it'll run and the greater likelihood for issues).
10 | #
11 | # It's strongly recommended that you check this file into your version control system.
12 |
13 | ActiveRecord::Schema.define(version: 2019_01_16_193355) do
14 |
15 | # These are extensions that must be enabled in order to support this database
16 | enable_extension "plpgsql"
17 |
18 | create_table "ask_items", force: :cascade do |t|
19 | t.bigint "item_id"
20 | t.bigint "location"
21 | t.datetime "created_at", null: false
22 | t.datetime "updated_at", null: false
23 | t.index ["item_id"], name: "index_ask_items_on_item_id"
24 | end
25 |
26 | create_table "items", force: :cascade do |t|
27 | t.bigint "hn_id"
28 | t.bigint "parent_id"
29 | t.boolean "deleted"
30 | t.integer "hn_type"
31 | t.string "by"
32 | t.datetime "time"
33 | t.text "text"
34 | t.boolean "dead"
35 | t.bigint "parent"
36 | t.bigint "poll"
37 | t.string "url"
38 | t.string "host"
39 | t.integer "score"
40 | t.string "title"
41 | t.integer "descendants"
42 | t.datetime "created_at", null: false
43 | t.datetime "updated_at", null: false
44 | t.bigint "kid_location"
45 | t.boolean "loading_details", default: false
46 | end
47 |
48 | create_table "job_items", force: :cascade do |t|
49 | t.bigint "item_id"
50 | t.bigint "location"
51 | t.datetime "created_at", null: false
52 | t.datetime "updated_at", null: false
53 | t.index ["item_id"], name: "index_job_items_on_item_id"
54 | end
55 |
56 | create_table "new_items", force: :cascade do |t|
57 | t.bigint "item_id"
58 | t.bigint "location"
59 | t.datetime "created_at", null: false
60 | t.datetime "updated_at", null: false
61 | t.index ["item_id"], name: "index_new_items_on_item_id"
62 | end
63 |
64 | create_table "show_items", force: :cascade do |t|
65 | t.bigint "item_id"
66 | t.bigint "location"
67 | t.datetime "created_at", null: false
68 | t.datetime "updated_at", null: false
69 | t.index ["item_id"], name: "index_show_items_on_item_id"
70 | end
71 |
72 | create_table "top_items", force: :cascade do |t|
73 | t.bigint "item_id"
74 | t.bigint "location"
75 | t.datetime "created_at", null: false
76 | t.datetime "updated_at", null: false
77 | t.index ["item_id"], name: "index_top_items_on_item_id"
78 | end
79 |
80 | create_table "users", force: :cascade do |t|
81 | t.string "hn_id"
82 | t.datetime "created"
83 | t.bigint "delay"
84 | t.bigint "karma"
85 | t.text "about"
86 | t.datetime "created_at", null: false
87 | t.datetime "updated_at", null: false
88 | end
89 |
90 | add_foreign_key "ask_items", "items"
91 | add_foreign_key "job_items", "items"
92 | add_foreign_key "new_items", "items"
93 | add_foreign_key "show_items", "items"
94 | add_foreign_key "top_items", "items"
95 | end
96 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/sass/components/modal.sass:
--------------------------------------------------------------------------------
1 | $modal-z: 40 !default
2 |
3 | $modal-background-background-color: rgba($black, 0.86) !default
4 |
5 | $modal-content-width: 640px !default
6 | $modal-content-margin-mobile: 20px !default
7 | $modal-content-spacing-mobile: 160px !default
8 | $modal-content-spacing-tablet: 40px !default
9 |
10 | $modal-close-dimensions: 40px !default
11 | $modal-close-right: 20px !default
12 | $modal-close-top: 20px !default
13 |
14 | $modal-card-spacing: 40px !default
15 |
16 | $modal-card-head-background-color: $background !default
17 | $modal-card-head-border-bottom: 1px solid $border !default
18 | $modal-card-head-padding: 20px !default
19 | $modal-card-head-radius: $radius-large !default
20 |
21 | $modal-card-title-color: $text-strong !default
22 | $modal-card-title-line-height: 1 !default
23 | $modal-card-title-size: $size-4 !default
24 |
25 | $modal-card-foot-radius: $radius-large !default
26 | $modal-card-foot-border-top: 1px solid $border !default
27 |
28 | $modal-card-body-background-color: $white !default
29 | $modal-card-body-padding: 20px !default
30 |
31 | .modal
32 | @extend %overlay
33 | align-items: center
34 | display: none
35 | flex-direction: column
36 | justify-content: center
37 | overflow: hidden
38 | position: fixed
39 | z-index: $modal-z
40 | // Modifiers
41 | &.is-active
42 | display: flex
43 |
44 | .modal-background
45 | @extend %overlay
46 | background-color: $modal-background-background-color
47 |
48 | .modal-content,
49 | .modal-card
50 | margin: 0 $modal-content-margin-mobile
51 | max-height: calc(100vh - #{$modal-content-spacing-mobile})
52 | overflow: auto
53 | position: relative
54 | width: 100%
55 | // Responsiveness
56 | +tablet
57 | margin: 0 auto
58 | max-height: calc(100vh - #{$modal-content-spacing-tablet})
59 | width: $modal-content-width
60 |
61 | .modal-close
62 | @extend %delete
63 | background: none
64 | height: $modal-close-dimensions
65 | position: fixed
66 | right: $modal-close-right
67 | top: $modal-close-top
68 | width: $modal-close-dimensions
69 |
70 | .modal-card
71 | display: flex
72 | flex-direction: column
73 | max-height: calc(100vh - #{$modal-card-spacing})
74 | overflow: hidden
75 | -ms-overflow-y: visible
76 |
77 | .modal-card-head,
78 | .modal-card-foot
79 | align-items: center
80 | background-color: $modal-card-head-background-color
81 | display: flex
82 | flex-shrink: 0
83 | justify-content: flex-start
84 | padding: $modal-card-head-padding
85 | position: relative
86 |
87 | .modal-card-head
88 | border-bottom: $modal-card-head-border-bottom
89 | border-top-left-radius: $modal-card-head-radius
90 | border-top-right-radius: $modal-card-head-radius
91 |
92 | .modal-card-title
93 | color: $modal-card-title-color
94 | flex-grow: 1
95 | flex-shrink: 0
96 | font-size: $modal-card-title-size
97 | line-height: $modal-card-title-line-height
98 |
99 | .modal-card-foot
100 | border-bottom-left-radius: $modal-card-foot-radius
101 | border-bottom-right-radius: $modal-card-foot-radius
102 | border-top: $modal-card-foot-border-top
103 | .button
104 | &:not(:last-child)
105 | margin-right: 10px
106 |
107 | .modal-card-body
108 | +overflow-touch
109 | background-color: $modal-card-body-background-color
110 | flex-grow: 1
111 | flex-shrink: 1
112 | overflow: auto
113 | padding: $modal-card-body-padding
114 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/sass/elements/tag.sass:
--------------------------------------------------------------------------------
1 | $tag-background-color: $background !default
2 | $tag-color: $text !default
3 | $tag-radius: $radius !default
4 | $tag-delete-margin: 1px !default
5 |
6 | .tags
7 | align-items: center
8 | display: flex
9 | flex-wrap: wrap
10 | justify-content: flex-start
11 | .tag
12 | margin-bottom: 0.5rem
13 | &:not(:last-child)
14 | margin-right: 0.5rem
15 | &:last-child
16 | margin-bottom: -0.5rem
17 | &:not(:last-child)
18 | margin-bottom: 1rem
19 | // Sizes
20 | &.are-medium
21 | .tag:not(.is-normal):not(.is-large)
22 | font-size: $size-normal
23 | &.are-large
24 | .tag:not(.is-normal):not(.is-medium)
25 | font-size: $size-medium
26 | &.has-addons
27 | .tag
28 | margin-right: 0
29 | &:not(:first-child)
30 | border-bottom-left-radius: 0
31 | border-top-left-radius: 0
32 | &:not(:last-child)
33 | border-bottom-right-radius: 0
34 | border-top-right-radius: 0
35 | &.is-centered
36 | justify-content: center
37 | .tag
38 | margin-right: 0.25rem
39 | margin-left: 0.25rem
40 | &.is-right
41 | justify-content: flex-end
42 | .tag
43 | &:not(:first-child)
44 | margin-left: 0.5rem
45 | &:not(:last-child)
46 | margin-right: 0
47 | &.has-addons
48 | .tag
49 | margin-right: 0
50 | &:not(:first-child)
51 | margin-left: 0
52 | border-bottom-left-radius: 0
53 | border-top-left-radius: 0
54 | &:not(:last-child)
55 | border-bottom-right-radius: 0
56 | border-top-right-radius: 0
57 |
58 | .tag:not(body)
59 | align-items: center
60 | background-color: $tag-background-color
61 | border-radius: $tag-radius
62 | color: $tag-color
63 | display: inline-flex
64 | font-size: $size-small
65 | height: 2em
66 | justify-content: center
67 | line-height: 1.5
68 | padding-left: 0.75em
69 | padding-right: 0.75em
70 | white-space: nowrap
71 | .delete
72 | margin-left: 0.25rem
73 | margin-right: -0.375rem
74 | // Colors
75 | @each $name, $pair in $colors
76 | $color: nth($pair, 1)
77 | $color-invert: nth($pair, 2)
78 | &.is-#{$name}
79 | background-color: $color
80 | color: $color-invert
81 | // Sizes
82 | &.is-normal
83 | font-size: $size-small
84 | &.is-medium
85 | font-size: $size-normal
86 | &.is-large
87 | font-size: $size-medium
88 | .icon
89 | &:first-child:not(:last-child)
90 | margin-left: -0.375em
91 | margin-right: 0.1875em
92 | &:last-child:not(:first-child)
93 | margin-left: 0.1875em
94 | margin-right: -0.375em
95 | &:first-child:last-child
96 | margin-left: -0.375em
97 | margin-right: -0.375em
98 | // Modifiers
99 | &.is-delete
100 | margin-left: $tag-delete-margin
101 | padding: 0
102 | position: relative
103 | width: 2em
104 | &::before,
105 | &::after
106 | background-color: currentColor
107 | content: ""
108 | display: block
109 | left: 50%
110 | position: absolute
111 | top: 50%
112 | transform: translateX(-50%) translateY(-50%) rotate(45deg)
113 | transform-origin: center center
114 | &::before
115 | height: 1px
116 | width: 50%
117 | &::after
118 | height: 50%
119 | width: 1px
120 | &:hover,
121 | &:focus
122 | background-color: darken($tag-background-color, 5%)
123 | &:active
124 | background-color: darken($tag-background-color, 10%)
125 | &.is-rounded
126 | border-radius: $radius-rounded
127 |
128 | a.tag
129 | &:hover
130 | text-decoration: underline
131 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/sass/elements/table.sass:
--------------------------------------------------------------------------------
1 | $table-color: $grey-darker !default
2 | $table-background-color: $white !default
3 |
4 | $table-cell-border: 1px solid $grey-lighter !default
5 | $table-cell-border-width: 0 0 1px !default
6 | $table-cell-padding: 0.5em 0.75em !default
7 | $table-cell-heading-color: $text-strong !default
8 |
9 | $table-head-cell-border-width: 0 0 2px !default
10 | $table-head-cell-color: $text-strong !default
11 | $table-foot-cell-border-width: 2px 0 0 !default
12 | $table-foot-cell-color: $text-strong !default
13 |
14 | $table-head-background-color: transparent !default
15 | $table-body-background-color: transparent !default
16 | $table-foot-background-color: transparent !default
17 |
18 | $table-row-hover-background-color: $white-bis !default
19 |
20 | $table-row-active-background-color: $primary !default
21 | $table-row-active-color: $primary-invert !default
22 |
23 | $table-striped-row-even-background-color: $white-bis !default
24 | $table-striped-row-even-hover-background-color: $white-ter !default
25 |
26 | .table
27 | @extend %block
28 | background-color: $table-background-color
29 | color: $table-color
30 | td,
31 | th
32 | border: $table-cell-border
33 | border-width: $table-cell-border-width
34 | padding: $table-cell-padding
35 | vertical-align: top
36 | // Colors
37 | @each $name, $pair in $colors
38 | $color: nth($pair, 1)
39 | $color-invert: nth($pair, 2)
40 | &.is-#{$name}
41 | background-color: $color
42 | border-color: $color
43 | color: $color-invert
44 | // Modifiers
45 | &.is-narrow
46 | white-space: nowrap
47 | width: 1%
48 | &.is-selected
49 | background-color: $table-row-active-background-color
50 | color: $table-row-active-color
51 | a,
52 | strong
53 | color: currentColor
54 | th
55 | color: $table-cell-heading-color
56 | text-align: left
57 | tr
58 | &.is-selected
59 | background-color: $table-row-active-background-color
60 | color: $table-row-active-color
61 | a,
62 | strong
63 | color: currentColor
64 | td,
65 | th
66 | border-color: $table-row-active-color
67 | color: currentColor
68 | thead
69 | background-color: $table-head-background-color
70 | td,
71 | th
72 | border-width: $table-head-cell-border-width
73 | color: $table-head-cell-color
74 | tfoot
75 | background-color: $table-foot-background-color
76 | td,
77 | th
78 | border-width: $table-foot-cell-border-width
79 | color: $table-foot-cell-color
80 | tbody
81 | background-color: $table-body-background-color
82 | tr
83 | &:last-child
84 | td,
85 | th
86 | border-bottom-width: 0
87 | // Modifiers
88 | &.is-bordered
89 | td,
90 | th
91 | border-width: 1px
92 | tr
93 | &:last-child
94 | td,
95 | th
96 | border-bottom-width: 1px
97 | &.is-fullwidth
98 | width: 100%
99 | &.is-hoverable
100 | tbody
101 | tr:not(.is-selected)
102 | &:hover
103 | background-color: $table-row-hover-background-color
104 | &.is-striped
105 | tbody
106 | tr:not(.is-selected)
107 | &:hover
108 | background-color: $table-row-hover-background-color
109 | &:nth-child(even)
110 | background-color: $table-striped-row-even-hover-background-color
111 | &.is-narrow
112 | td,
113 | th
114 | padding: 0.25em 0.5em
115 | &.is-striped
116 | tbody
117 | tr:not(.is-selected)
118 | &:nth-child(even)
119 | background-color: $table-striped-row-even-background-color
120 |
121 | .table-container
122 | @extend %block
123 | +overflow-touch
124 | overflow: auto
125 | overflow-y: hidden
126 | max-width: 100%
127 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/sass/components/pagination.sass:
--------------------------------------------------------------------------------
1 | $pagination-color: $grey-darker !default
2 | $pagination-border-color: $grey-lighter !default
3 | $pagination-margin: -0.25rem !default
4 | $pagination-min-width: $control-height !default
5 |
6 | $pagination-hover-color: $link-hover !default
7 | $pagination-hover-border-color: $link-hover-border !default
8 |
9 | $pagination-focus-color: $link-focus !default
10 | $pagination-focus-border-color: $link-focus-border !default
11 |
12 | $pagination-active-color: $link-active !default
13 | $pagination-active-border-color: $link-active-border !default
14 |
15 | $pagination-disabled-color: $grey !default
16 | $pagination-disabled-background-color: $grey-lighter !default
17 | $pagination-disabled-border-color: $grey-lighter !default
18 |
19 | $pagination-current-color: $link-invert !default
20 | $pagination-current-background-color: $link !default
21 | $pagination-current-border-color: $link !default
22 |
23 | $pagination-ellipsis-color: $grey-light !default
24 |
25 | $pagination-shadow-inset: inset 0 1px 2px rgba($black, 0.2)
26 |
27 | .pagination
28 | font-size: $size-normal
29 | margin: $pagination-margin
30 | // Sizes
31 | &.is-small
32 | font-size: $size-small
33 | &.is-medium
34 | font-size: $size-medium
35 | &.is-large
36 | font-size: $size-large
37 | &.is-rounded
38 | .pagination-previous,
39 | .pagination-next
40 | padding-left: 1em
41 | padding-right: 1em
42 | border-radius: $radius-rounded
43 | .pagination-link
44 | border-radius: $radius-rounded
45 |
46 | .pagination,
47 | .pagination-list
48 | align-items: center
49 | display: flex
50 | justify-content: center
51 | text-align: center
52 |
53 | .pagination-previous,
54 | .pagination-next,
55 | .pagination-link,
56 | .pagination-ellipsis
57 | @extend %control
58 | @extend %unselectable
59 | font-size: 1em
60 | padding-left: 0.5em
61 | padding-right: 0.5em
62 | justify-content: center
63 | margin: 0.25rem
64 | text-align: center
65 |
66 | .pagination-previous,
67 | .pagination-next,
68 | .pagination-link
69 | border-color: $pagination-border-color
70 | color: $pagination-color
71 | min-width: $pagination-min-width
72 | &:hover
73 | border-color: $pagination-hover-border-color
74 | color: $pagination-hover-color
75 | &:focus
76 | border-color: $pagination-focus-border-color
77 | &:active
78 | box-shadow: $pagination-shadow-inset
79 | &[disabled]
80 | background-color: $pagination-disabled-background-color
81 | border-color: $pagination-disabled-border-color
82 | box-shadow: none
83 | color: $pagination-disabled-color
84 | opacity: 0.5
85 |
86 | .pagination-previous,
87 | .pagination-next
88 | padding-left: 0.75em
89 | padding-right: 0.75em
90 | white-space: nowrap
91 |
92 | .pagination-link
93 | &.is-current
94 | background-color: $pagination-current-background-color
95 | border-color: $pagination-current-border-color
96 | color: $pagination-current-color
97 |
98 | .pagination-ellipsis
99 | color: $pagination-ellipsis-color
100 | pointer-events: none
101 |
102 | .pagination-list
103 | flex-wrap: wrap
104 |
105 | +mobile
106 | .pagination
107 | flex-wrap: wrap
108 | .pagination-previous,
109 | .pagination-next
110 | flex-grow: 1
111 | flex-shrink: 1
112 | .pagination-list
113 | li
114 | flex-grow: 1
115 | flex-shrink: 1
116 |
117 | +tablet
118 | .pagination-list
119 | flex-grow: 1
120 | flex-shrink: 1
121 | justify-content: flex-start
122 | order: 1
123 | .pagination-previous
124 | order: 2
125 | .pagination-next
126 | order: 3
127 | .pagination
128 | justify-content: space-between
129 | &.is-centered
130 | .pagination-previous
131 | order: 1
132 | .pagination-list
133 | justify-content: center
134 | order: 2
135 | .pagination-next
136 | order: 3
137 | &.is-right
138 | .pagination-previous
139 | order: 1
140 | .pagination-next
141 | order: 2
142 | .pagination-list
143 | justify-content: flex-end
144 | order: 3
145 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/sass/elements/content.sass:
--------------------------------------------------------------------------------
1 | $content-heading-color: $text-strong !default
2 | $content-heading-weight: $weight-semibold !default
3 | $content-heading-line-height: 1.125 !default
4 |
5 | $content-blockquote-background-color: $background !default
6 | $content-blockquote-border-left: 5px solid $border !default
7 | $content-blockquote-padding: 1.25em 1.5em !default
8 |
9 | $content-pre-padding: 1.25em 1.5em !default
10 |
11 | $content-table-cell-border: 1px solid $border !default
12 | $content-table-cell-border-width: 0 0 1px !default
13 | $content-table-cell-padding: 0.5em 0.75em !default
14 | $content-table-cell-heading-color: $text-strong !default
15 | $content-table-head-cell-border-width: 0 0 2px !default
16 | $content-table-head-cell-color: $text-strong !default
17 | $content-table-foot-cell-border-width: 2px 0 0 !default
18 | $content-table-foot-cell-color: $text-strong !default
19 |
20 | .content
21 | @extend %block
22 | // Inline
23 | li + li
24 | margin-top: 0.25em
25 | // Block
26 | p,
27 | dl,
28 | ol,
29 | ul,
30 | blockquote,
31 | pre,
32 | table
33 | &:not(:last-child)
34 | margin-bottom: 1em
35 | h1,
36 | h2,
37 | h3,
38 | h4,
39 | h5,
40 | h6
41 | color: $content-heading-color
42 | font-weight: $content-heading-weight
43 | line-height: $content-heading-line-height
44 | h1
45 | font-size: 2em
46 | margin-bottom: 0.5em
47 | &:not(:first-child)
48 | margin-top: 1em
49 | h2
50 | font-size: 1.75em
51 | margin-bottom: 0.5714em
52 | &:not(:first-child)
53 | margin-top: 1.1428em
54 | h3
55 | font-size: 1.5em
56 | margin-bottom: 0.6666em
57 | &:not(:first-child)
58 | margin-top: 1.3333em
59 | h4
60 | font-size: 1.25em
61 | margin-bottom: 0.8em
62 | h5
63 | font-size: 1.125em
64 | margin-bottom: 0.8888em
65 | h6
66 | font-size: 1em
67 | margin-bottom: 1em
68 | blockquote
69 | background-color: $content-blockquote-background-color
70 | border-left: $content-blockquote-border-left
71 | padding: $content-blockquote-padding
72 | ol
73 | list-style-position: outside
74 | margin-left: 2em
75 | margin-top: 1em
76 | &:not([type])
77 | list-style-type: decimal
78 | &.is-lower-alpha
79 | list-style-type: lower-alpha
80 | &.is-lower-roman
81 | list-style-type: lower-roman
82 | &.is-upper-alpha
83 | list-style-type: upper-alpha
84 | &.is-upper-roman
85 | list-style-type: upper-roman
86 | ul
87 | list-style: disc outside
88 | margin-left: 2em
89 | margin-top: 1em
90 | ul
91 | list-style-type: circle
92 | margin-top: 0.5em
93 | ul
94 | list-style-type: square
95 | dd
96 | margin-left: 2em
97 | figure
98 | margin-left: 2em
99 | margin-right: 2em
100 | text-align: center
101 | &:not(:first-child)
102 | margin-top: 2em
103 | &:not(:last-child)
104 | margin-bottom: 2em
105 | img
106 | display: inline-block
107 | figcaption
108 | font-style: italic
109 | pre
110 | +overflow-touch
111 | overflow-x: auto
112 | padding: $content-pre-padding
113 | white-space: pre
114 | word-wrap: normal
115 | sup,
116 | sub
117 | font-size: 75%
118 | table
119 | width: 100%
120 | td,
121 | th
122 | border: $content-table-cell-border
123 | border-width: $content-table-cell-border-width
124 | padding: $content-table-cell-padding
125 | vertical-align: top
126 | th
127 | color: $content-table-cell-heading-color
128 | text-align: left
129 | thead
130 | td,
131 | th
132 | border-width: $content-table-head-cell-border-width
133 | color: $content-table-head-cell-color
134 | tfoot
135 | td,
136 | th
137 | border-width: $content-table-foot-cell-border-width
138 | color: $content-table-foot-cell-color
139 | tbody
140 | tr
141 | &:last-child
142 | td,
143 | th
144 | border-bottom-width: 0
145 | // Sizes
146 | &.is-small
147 | font-size: $size-small
148 | &.is-medium
149 | font-size: $size-medium
150 | &.is-large
151 | font-size: $size-large
152 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/sass/layout/hero.sass:
--------------------------------------------------------------------------------
1 | // Main container
2 |
3 | .hero
4 | align-items: stretch
5 | display: flex
6 | flex-direction: column
7 | justify-content: space-between
8 | .navbar
9 | background: none
10 | .tabs
11 | ul
12 | border-bottom: none
13 | // Colors
14 | @each $name, $pair in $colors
15 | $color: nth($pair, 1)
16 | $color-invert: nth($pair, 2)
17 | &.is-#{$name}
18 | background-color: $color
19 | color: $color-invert
20 | a:not(.button):not(.dropdown-item):not(.tag),
21 | strong
22 | color: inherit
23 | .title
24 | color: $color-invert
25 | .subtitle
26 | color: rgba($color-invert, 0.9)
27 | a:not(.button),
28 | strong
29 | color: $color-invert
30 | .navbar-menu
31 | +touch
32 | background-color: $color
33 | .navbar-item,
34 | .navbar-link
35 | color: rgba($color-invert, 0.7)
36 | a.navbar-item,
37 | .navbar-link
38 | &:hover,
39 | &.is-active
40 | background-color: darken($color, 5%)
41 | color: $color-invert
42 | .tabs
43 | a
44 | color: $color-invert
45 | opacity: 0.9
46 | &:hover
47 | opacity: 1
48 | li
49 | &.is-active a
50 | opacity: 1
51 | &.is-boxed,
52 | &.is-toggle
53 | a
54 | color: $color-invert
55 | &:hover
56 | background-color: rgba($black, 0.1)
57 | li.is-active a
58 | &,
59 | &:hover
60 | background-color: $color-invert
61 | border-color: $color-invert
62 | color: $color
63 | // Modifiers
64 | &.is-bold
65 | $gradient-top-left: darken(saturate(adjust-hue($color, -10deg), 10%), 10%)
66 | $gradient-bottom-right: lighten(saturate(adjust-hue($color, 10deg), 5%), 5%)
67 | background-image: linear-gradient(141deg, $gradient-top-left 0%, $color 71%, $gradient-bottom-right 100%)
68 | +mobile
69 | .navbar-menu
70 | background-image: linear-gradient(141deg, $gradient-top-left 0%, $color 71%, $gradient-bottom-right 100%)
71 | // Responsiveness
72 | // +mobile
73 | // .nav-toggle
74 | // span
75 | // background-color: $color-invert
76 | // &:hover
77 | // background-color: rgba($black, 0.1)
78 | // &.is-active
79 | // span
80 | // background-color: $color-invert
81 | // .nav-menu
82 | // .nav-item
83 | // border-top-color: rgba($color-invert, 0.2)
84 | // Sizes
85 | &.is-small
86 | .hero-body
87 | padding-bottom: 1.5rem
88 | padding-top: 1.5rem
89 | &.is-medium
90 | +tablet
91 | .hero-body
92 | padding-bottom: 9rem
93 | padding-top: 9rem
94 | &.is-large
95 | +tablet
96 | .hero-body
97 | padding-bottom: 18rem
98 | padding-top: 18rem
99 | &.is-halfheight,
100 | &.is-fullheight,
101 | &.is-fullheight-with-navbar
102 | .hero-body
103 | align-items: center
104 | display: flex
105 | & > .container
106 | flex-grow: 1
107 | flex-shrink: 1
108 | &.is-halfheight
109 | min-height: 50vh
110 | &.is-fullheight
111 | min-height: 100vh
112 |
113 | // Components
114 |
115 | .hero-video
116 | @extend %overlay
117 | overflow: hidden
118 | video
119 | left: 50%
120 | min-height: 100%
121 | min-width: 100%
122 | position: absolute
123 | top: 50%
124 | transform: translate3d(-50%, -50%, 0)
125 | // Modifiers
126 | &.is-transparent
127 | opacity: 0.3
128 | // Responsiveness
129 | +mobile
130 | display: none
131 |
132 | .hero-buttons
133 | margin-top: 1.5rem
134 | // Responsiveness
135 | +mobile
136 | .button
137 | display: flex
138 | &:not(:last-child)
139 | margin-bottom: 0.75rem
140 | +tablet
141 | display: flex
142 | justify-content: center
143 | .button:not(:last-child)
144 | margin-right: 1.5rem
145 |
146 | // Containers
147 |
148 | .hero-head,
149 | .hero-foot
150 | flex-grow: 0
151 | flex-shrink: 0
152 |
153 | .hero-body
154 | flex-grow: 1
155 | flex-shrink: 0
156 | padding: 3rem 1.5rem
157 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/sass/components/tabs.sass:
--------------------------------------------------------------------------------
1 | $tabs-border-bottom-color: $border !default
2 | $tabs-border-bottom-style: solid !default
3 | $tabs-border-bottom-width: 1px !default
4 | $tabs-link-color: $text !default
5 | $tabs-link-hover-border-bottom-color: $text-strong !default
6 | $tabs-link-hover-color: $text-strong !default
7 | $tabs-link-active-border-bottom-color: $link !default
8 | $tabs-link-active-color: $link !default
9 | $tabs-link-padding: 0.5em 1em !default
10 |
11 | $tabs-boxed-link-radius: $radius !default
12 | $tabs-boxed-link-hover-background-color: $background !default
13 | $tabs-boxed-link-hover-border-bottom-color: $border !default
14 |
15 | $tabs-boxed-link-active-background-color: $white !default
16 | $tabs-boxed-link-active-border-color: $border !default
17 | $tabs-boxed-link-active-border-bottom-color: transparent !default
18 |
19 | $tabs-toggle-link-border-color: $border !default
20 | $tabs-toggle-link-border-style: solid !default
21 | $tabs-toggle-link-border-width: 1px !default
22 | $tabs-toggle-link-hover-background-color: $background !default
23 | $tabs-toggle-link-hover-border-color: $border-hover !default
24 | $tabs-toggle-link-radius: $radius !default
25 | $tabs-toggle-link-active-background-color: $link !default
26 | $tabs-toggle-link-active-border-color: $link !default
27 | $tabs-toggle-link-active-color: $link-invert !default
28 |
29 | .tabs
30 | @extend %block
31 | +overflow-touch
32 | @extend %unselectable
33 | align-items: stretch
34 | display: flex
35 | font-size: $size-normal
36 | justify-content: space-between
37 | overflow: hidden
38 | overflow-x: auto
39 | white-space: nowrap
40 | a
41 | align-items: center
42 | border-bottom-color: $tabs-border-bottom-color
43 | border-bottom-style: $tabs-border-bottom-style
44 | border-bottom-width: $tabs-border-bottom-width
45 | color: $tabs-link-color
46 | display: flex
47 | justify-content: center
48 | margin-bottom: -#{$tabs-border-bottom-width}
49 | padding: $tabs-link-padding
50 | vertical-align: top
51 | &:hover
52 | border-bottom-color: $tabs-link-hover-border-bottom-color
53 | color: $tabs-link-hover-color
54 | li
55 | display: block
56 | &.is-active
57 | a
58 | border-bottom-color: $tabs-link-active-border-bottom-color
59 | color: $tabs-link-active-color
60 | ul
61 | align-items: center
62 | border-bottom-color: $tabs-border-bottom-color
63 | border-bottom-style: $tabs-border-bottom-style
64 | border-bottom-width: $tabs-border-bottom-width
65 | display: flex
66 | flex-grow: 1
67 | flex-shrink: 0
68 | justify-content: flex-start
69 | &.is-left
70 | padding-right: 0.75em
71 | &.is-center
72 | flex: none
73 | justify-content: center
74 | padding-left: 0.75em
75 | padding-right: 0.75em
76 | &.is-right
77 | justify-content: flex-end
78 | padding-left: 0.75em
79 | .icon
80 | &:first-child
81 | margin-right: 0.5em
82 | &:last-child
83 | margin-left: 0.5em
84 | // Alignment
85 | &.is-centered
86 | ul
87 | justify-content: center
88 | &.is-right
89 | ul
90 | justify-content: flex-end
91 | // Styles
92 | &.is-boxed
93 | a
94 | border: 1px solid transparent
95 | border-radius: $tabs-boxed-link-radius $tabs-boxed-link-radius 0 0
96 | &:hover
97 | background-color: $tabs-boxed-link-hover-background-color
98 | border-bottom-color: $tabs-boxed-link-hover-border-bottom-color
99 | li
100 | &.is-active
101 | a
102 | background-color: $tabs-boxed-link-active-background-color
103 | border-color: $tabs-boxed-link-active-border-color
104 | border-bottom-color: $tabs-boxed-link-active-border-bottom-color !important
105 | &.is-fullwidth
106 | li
107 | flex-grow: 1
108 | flex-shrink: 0
109 | &.is-toggle
110 | a
111 | border-color: $tabs-toggle-link-border-color
112 | border-style: $tabs-toggle-link-border-style
113 | border-width: $tabs-toggle-link-border-width
114 | margin-bottom: 0
115 | position: relative
116 | &:hover
117 | background-color: $tabs-toggle-link-hover-background-color
118 | border-color: $tabs-toggle-link-hover-border-color
119 | z-index: 2
120 | li
121 | & + li
122 | margin-left: -#{$tabs-toggle-link-border-width}
123 | &:first-child a
124 | border-radius: $tabs-toggle-link-radius 0 0 $tabs-toggle-link-radius
125 | &:last-child a
126 | border-radius: 0 $tabs-toggle-link-radius $tabs-toggle-link-radius 0
127 | &.is-active
128 | a
129 | background-color: $tabs-toggle-link-active-background-color
130 | border-color: $tabs-toggle-link-active-border-color
131 | color: $tabs-toggle-link-active-color
132 | z-index: 1
133 | ul
134 | border-bottom: none
135 | &.is-toggle-rounded
136 | li
137 | &:first-child a
138 | border-bottom-left-radius: $radius-rounded
139 | border-top-left-radius: $radius-rounded
140 | padding-left: 1.25em
141 | &:last-child a
142 | border-bottom-right-radius: $radius-rounded
143 | border-top-right-radius: $radius-rounded
144 | padding-right: 1.25em
145 | // Sizes
146 | &.is-small
147 | font-size: $size-small
148 | &.is-medium
149 | font-size: $size-medium
150 | &.is-large
151 | font-size: $size-large
152 |
--------------------------------------------------------------------------------