├── .browserslistrc ├── .env ├── .gitignore ├── .postcssrc.yml ├── .ruby-version ├── Capfile ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── Procfile ├── README.md ├── Rakefile ├── app ├── assets │ ├── config │ │ └── manifest.js │ ├── images │ │ ├── .keep │ │ ├── cloud-check.svg │ │ ├── cloud-download.svg │ │ ├── train_192.png │ │ ├── train_48.png │ │ └── train_512.png │ └── stylesheets │ │ ├── application.sass │ │ ├── bulma.sass │ │ └── sass │ │ ├── base │ │ ├── _all.sass │ │ ├── generic.sass │ │ ├── helpers.sass │ │ └── minireset.sass │ │ ├── components │ │ ├── _all.sass │ │ ├── breadcrumb.sass │ │ ├── card.sass │ │ ├── dropdown.sass │ │ ├── level.sass │ │ ├── list.sass │ │ ├── media.sass │ │ ├── menu.sass │ │ ├── message.sass │ │ ├── modal.sass │ │ ├── navbar.sass │ │ ├── pagination.sass │ │ ├── panel.sass │ │ └── tabs.sass │ │ ├── elements │ │ ├── _all.sass │ │ ├── box.sass │ │ ├── button.sass │ │ ├── container.sass │ │ ├── content.sass │ │ ├── form.sass │ │ ├── icon.sass │ │ ├── image.sass │ │ ├── notification.sass │ │ ├── other.sass │ │ ├── progress.sass │ │ ├── table.sass │ │ ├── tag.sass │ │ └── title.sass │ │ ├── grid │ │ ├── _all.sass │ │ ├── columns.sass │ │ └── tiles.sass │ │ ├── layout │ │ ├── _all.sass │ │ ├── footer.sass │ │ ├── hero.sass │ │ └── section.sass │ │ └── utilities │ │ ├── _all.sass │ │ ├── animations.sass │ │ ├── controls.sass │ │ ├── derived-variables.sass │ │ ├── functions.sass │ │ ├── initial-variables.sass │ │ └── mixins.sass ├── channels │ ├── application_cable │ │ ├── channel.rb │ │ └── connection.rb │ ├── ask_item_channel.rb │ ├── comments_channel.rb │ ├── item_channel.rb │ ├── items_list_channel.rb │ ├── job_item_channel.rb │ ├── new_item_channel.rb │ ├── show_item_channel.rb │ ├── top_item_channel.rb │ └── user_channel.rb ├── controllers │ ├── application_controller.rb │ ├── asks_controller.rb │ ├── concerns │ │ └── .keep │ ├── items_controller.rb │ ├── jobs_controller.rb │ ├── news_controller.rb │ ├── service_worker_controller.rb │ ├── shows_controller.rb │ ├── tops_controller.rb │ └── users_controller.rb ├── helpers │ ├── application_helper.rb │ └── items_helper.rb ├── javascript │ ├── cables │ │ └── cable.js │ ├── channels │ │ ├── consumer.js │ │ └── index.js │ ├── controllers │ │ ├── ask_item_controller.js │ │ ├── bulma_navbar_controller.js │ │ ├── comments_controller.js │ │ ├── index.js │ │ ├── item_controller.js │ │ ├── item_location_controller.js │ │ ├── items_controller.js │ │ ├── service_worker_controller.js │ │ ├── swipable_controller.js │ │ ├── toggle_controller.js │ │ └── user_controller.js │ └── packs │ │ └── application.js ├── jobs │ ├── application_job.rb │ ├── load_ask_item_job.rb │ ├── load_ask_items_job.rb │ ├── load_job_item_job.rb │ ├── load_job_items_job.rb │ ├── load_new_item_job.rb │ ├── load_new_items_job.rb │ ├── load_show_item_job.rb │ ├── load_show_items_job.rb │ ├── load_top_item_job.rb │ ├── load_top_items_job.rb │ └── load_user_details_job.rb ├── mailers │ └── application_mailer.rb ├── models │ ├── application_record.rb │ ├── ask_item.rb │ ├── concerns │ │ └── .keep │ ├── item.rb │ ├── job_item.rb │ ├── new_item.rb │ ├── show_item.rb │ ├── top_item.rb │ └── user.rb ├── views │ ├── ask_items │ │ └── _ask_item.html.erb │ ├── asks │ │ └── show.html.erb │ ├── items │ │ ├── _comments.html.erb │ │ ├── _comments_header.html.erb │ │ ├── _item.html.erb │ │ └── show.html.erb │ ├── job_items │ │ └── _job_item.html.erb │ ├── jobs │ │ └── show.html.erb │ ├── layouts │ │ ├── _page_navigation.html.erb │ │ ├── application.html.erb │ │ ├── mailer.html.erb │ │ └── mailer.text.erb │ ├── new_items │ │ └── _new_item.html.erb │ ├── news │ │ └── show.html.erb │ ├── service_worker │ │ ├── manifest.json.erb │ │ ├── offline.html.erb │ │ └── service_worker.js.erb │ ├── show_items │ │ └── _show_item.erb │ ├── shows │ │ └── show.html.erb │ ├── top_items │ │ └── _top_item.html.erb │ ├── tops │ │ └── show.html.erb │ └── users │ │ ├── _metadata.html.erb │ │ └── show.html.erb └── workers │ └── load_item_details_worker.rb ├── babel.config.js ├── bin ├── bundle ├── rails ├── rake ├── setup ├── spring ├── webpack ├── webpack-dev-server └── yarn ├── config.ru ├── config ├── application.rb ├── boot.rb ├── cable.yml ├── credentials.yml.enc ├── database.yml ├── deploy.rb ├── deploy │ ├── production.rb │ └── staging.rb ├── environment.rb ├── environments │ ├── development.rb │ ├── production.rb │ └── test.rb ├── initializers │ ├── application_controller_renderer.rb │ ├── assets.rb │ ├── backtrace_silencers.rb │ ├── content_security_policy.rb │ ├── cookies_serializer.rb │ ├── filter_parameter_logging.rb │ ├── inflections.rb │ ├── mime_types.rb │ ├── new_framework_defaults_6_1.rb │ ├── permissions_policy.rb │ └── wrap_parameters.rb ├── locales │ └── en.yml ├── puma.rb ├── routes.rb ├── schedule.rb ├── spring.rb ├── storage.yml ├── webpack │ ├── development.js │ ├── environment.js │ ├── production.js │ └── test.js └── webpacker.yml ├── db ├── migrate │ ├── 20181114014928_create_items.rb │ ├── 20181116153242_create_top_items.rb │ ├── 20181204140608_create_new_items.rb │ ├── 20181205141656_create_show_items.rb │ ├── 20181205143748_create_ask_items.rb │ ├── 20181205160529_create_job_items.rb │ ├── 20181206162906_add_kid_location_to_item.rb │ ├── 20181207133624_create_users.rb │ └── 20190116193355_add_loading_details_to_item.rb ├── schema.rb └── seeds.rb ├── lib ├── assets │ └── .keep └── tasks │ └── .keep ├── log └── .keep ├── package.json ├── postcss.config.js ├── public ├── 404.html ├── 422.html ├── 500.html ├── apple-touch-icon-precomposed.png ├── apple-touch-icon.png ├── favicon.ico └── robots.txt ├── storage └── .keep ├── test ├── application_system_test_case.rb ├── controllers │ └── .keep ├── fixtures │ ├── .keep │ ├── ask_items.yml │ ├── files │ │ └── .keep │ ├── items.yml │ ├── job_items.yml │ ├── new_items.yml │ ├── show_items.yml │ ├── top_items.yml │ └── users.yml ├── helpers │ └── .keep ├── integration │ └── .keep ├── jobs │ ├── load_ask_item_job_test.rb │ ├── load_ask_items_job_test.rb │ ├── load_job_item_job_test.rb │ ├── load_job_items_job_test.rb │ ├── load_new_item_job_test.rb │ ├── load_news_items_job_test.rb │ ├── load_show_item_job_test.rb │ ├── load_show_items_job_test.rb │ ├── load_top_item_job_test.rb │ ├── load_top_items_job_test.rb │ └── load_user_details_job_test.rb ├── mailers │ └── .keep ├── models │ ├── .keep │ ├── ask_item_test.rb │ ├── item_test.rb │ ├── job_item_test.rb │ ├── new_item_test.rb │ ├── show_item_test.rb │ ├── top_item_test.rb │ └── user_test.rb ├── system │ └── .keep ├── test_helper.rb └── workers │ └── load_item_details_job_test.rb ├── tmp └── .keep ├── vendor └── .keep └── yarn.lock /.browserslistrc: -------------------------------------------------------------------------------- 1 | defaults 2 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | REDIS_SERVER=redis://localhost:6379 2 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.postcssrc.yml: -------------------------------------------------------------------------------- 1 | plugins: 2 | postcss-import: {} 3 | postcss-cssnext: {} 4 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 2.7.2 2 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /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/config/manifest.js: -------------------------------------------------------------------------------- 1 | //= link_tree ../images 2 | -------------------------------------------------------------------------------- /app/assets/images/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnbeatty/hnpwa-app/144b266e9e1ca6606abb3d0cfd7af26e2144e871/app/assets/images/.keep -------------------------------------------------------------------------------- /app/assets/images/cloud-check.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/assets/images/cloud-download.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/assets/images/train_192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnbeatty/hnpwa-app/144b266e9e1ca6606abb3d0cfd7af26e2144e871/app/assets/images/train_192.png -------------------------------------------------------------------------------- /app/assets/images/train_48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnbeatty/hnpwa-app/144b266e9e1ca6606abb3d0cfd7af26e2144e871/app/assets/images/train_48.png -------------------------------------------------------------------------------- /app/assets/images/train_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnbeatty/hnpwa-app/144b266e9e1ca6606abb3d0cfd7af26e2144e871/app/assets/images/train_512.png -------------------------------------------------------------------------------- /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/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/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/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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/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/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/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/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/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/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/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/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/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/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/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/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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/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/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/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/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 | -------------------------------------------------------------------------------- /app/assets/stylesheets/sass/grid/_all.sass: -------------------------------------------------------------------------------- 1 | @charset "utf-8" 2 | 3 | @import "columns.sass" 4 | @import "tiles.sass" 5 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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/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 | -------------------------------------------------------------------------------- /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/assets/stylesheets/sass/utilities/animations.sass: -------------------------------------------------------------------------------- 1 | @keyframes spinAround 2 | from 3 | transform: rotate(0deg) 4 | to 5 | transform: rotate(359deg) 6 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /app/channels/application_cable/channel.rb: -------------------------------------------------------------------------------- 1 | module ApplicationCable 2 | class Channel < ActionCable::Channel::Base 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /app/channels/application_cable/connection.rb: -------------------------------------------------------------------------------- 1 | module ApplicationCable 2 | class Connection < ActionCable::Connection::Base 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /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/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/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/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/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/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/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/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 | -------------------------------------------------------------------------------- /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/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | ITEMS_PER_PAGE ||= 32 2 | FIRST_PAGE ||= 0 3 | 4 | class ApplicationController < ActionController::Base 5 | end 6 | -------------------------------------------------------------------------------- /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/controllers/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnbeatty/hnpwa-app/144b266e9e1ca6606abb3d0cfd7af26e2144e871/app/controllers/concerns/.keep -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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/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/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/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 -------------------------------------------------------------------------------- /app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | end 3 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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/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/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/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/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 | -------------------------------------------------------------------------------- /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/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 | } -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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/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 | } -------------------------------------------------------------------------------- /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/jobs/application_job.rb: -------------------------------------------------------------------------------- 1 | class ApplicationJob < ActiveJob::Base 2 | end 3 | -------------------------------------------------------------------------------- /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_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_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_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_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_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_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/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 | -------------------------------------------------------------------------------- /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/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_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/mailers/application_mailer.rb: -------------------------------------------------------------------------------- 1 | class ApplicationMailer < ActionMailer::Base 2 | default from: 'from@example.com' 3 | layout 'mailer' 4 | end 5 | -------------------------------------------------------------------------------- /app/models/application_record.rb: -------------------------------------------------------------------------------- 1 | class ApplicationRecord < ActiveRecord::Base 2 | self.abstract_class = true 3 | end 4 | -------------------------------------------------------------------------------- /app/models/ask_item.rb: -------------------------------------------------------------------------------- 1 | class AskItem < ApplicationRecord 2 | belongs_to :item 3 | end 4 | -------------------------------------------------------------------------------- /app/models/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnbeatty/hnpwa-app/144b266e9e1ca6606abb3d0cfd7af26e2144e871/app/models/concerns/.keep -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /app/views/ask_items/_ask_item.html.erb: -------------------------------------------------------------------------------- 1 | <%= cache ask_item do %> 2 |
3 | <%= render ask_item.item %> 4 |
5 | <% end %> -------------------------------------------------------------------------------- /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/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 |
6 | 7 |
8 | <% end %> 9 |
10 | 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/views/items/_comments_header.html.erb: -------------------------------------------------------------------------------- 1 |

2 | <% unless completed %> 3 | <% if item.descendants > 0 %> 4 | Refreshing Comments... 5 | <% else %> 6 | Waiting for comments... 7 | <% end %> 8 |
9 | 10 | 11 | 12 | 13 | 0% 14 | <% else %> 15 | <% if item.descendants > 0 %> 16 | Comments 17 | <% else %> 18 | No Comments 19 | <% end %> 20 | <% end %> 21 |

-------------------------------------------------------------------------------- /app/views/items/_item.html.erb: -------------------------------------------------------------------------------- 1 | <%= cache item do %> 2 |
3 |
4 |

5 | <%= item.score %> 6 | <% if item.job? %> 7 |   job 8 | <% end %> 9 | <% unless item.host.nil? %> 10 |   <%= item.host %> 11 | 12 | <% end %> 13 |

14 |
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/views/items/show.html.erb: -------------------------------------------------------------------------------- 1 | <%= cache ['item', @item] do %> 2 |
3 |
4 | <%= render @item %> 5 |
6 |
7 |
8 | <%= render partial: 'comments_header', locals: { item: @item, completed: false } %> 9 |
10 | 11 |
12 | <% if @item.descendants > 0 %> 13 | <%= render partial: 'comments', locals: { item: @item } %> 14 | <% end %> 15 |
16 |
17 |
18 | <% 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/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/layouts/_page_navigation.html.erb: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /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 | 48 |
49 |
50 |
51 |
52 | <%= yield %> 53 |
54 |
55 |
56 |
57 | 58 | 59 | -------------------------------------------------------------------------------- /app/views/layouts/mailer.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | <%= yield %> 11 | 12 | -------------------------------------------------------------------------------- /app/views/layouts/mailer.text.erb: -------------------------------------------------------------------------------- 1 | <%= yield %> 2 | -------------------------------------------------------------------------------- /app/views/new_items/_new_item.html.erb: -------------------------------------------------------------------------------- 1 | <%= cache new_item do %> 2 |
3 | <%= render new_item.item %> 4 |
5 | <% end %> -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 |
-------------------------------------------------------------------------------- /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/views/show_items/_show_item.erb: -------------------------------------------------------------------------------- 1 | <%= cache show_item do %> 2 |
3 | <%= render show_item.item %> 4 |
5 | <% end %> 6 | -------------------------------------------------------------------------------- /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/top_items/_top_item.html.erb: -------------------------------------------------------------------------------- 1 | <%= cache top_item do %> 2 |
3 | <%= render top_item.item %> 4 |
5 | <% end %> 6 | -------------------------------------------------------------------------------- /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/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 %> -------------------------------------------------------------------------------- /app/views/users/show.html.erb: -------------------------------------------------------------------------------- 1 | <%= cache ['user', @user_id, @user ] do %> 2 |
3 |
4 |

5 | <%= @user_id %> 6 |

7 |
8 |
9 | <%= render partial: 'metadata', locals: { user: @user } %> 10 |
11 | 19 |
20 | <% end %> -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/credentials.yml.enc: -------------------------------------------------------------------------------- 1 | jzntcDze8kCBllNk4o0kq7GB5EtrwO9ojA7qd9iZ7v+Pg0m0n9Meq7ZBLOcI0yl/Y1HrRGwMZQWTnJLuJWl71aroQjG+msNu1xZSBmA+6kzgF2ozdUc3HHuCkyhDj9CNmz3MWJZJluhE+m11WJBCSvovmLGRly5n1urFOEbZGkLpUbmSnGJM8oo8wa1wYFw8+aOYGC9Xui7zTkIIUPgzNpbqBqx3srq4djQtF90UuT1gPup5Bs4zyxvz5SDzPQErar6MSGfarCVxesgfjgqKR91a0brqaI+fRmPt6o10rUm7xnXeK985Adsx6Kr/foXHiUIYYDZkGggZIIbIDx+cnvaF0LEDdQyGfZTgUusfASogNrPhvsLUINQLyAc3YsMEgUGY/x7bXacxf4PEjmrd9T1/3gc9Rspu2vN0--d/2Pu1T3f0NWwRH4--E0wYV7TgQ+0Ztb0M9yOjPQ== -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require_relative "application" 3 | 4 | # Initialize the Rails application. 5 | Rails.application.initialize! 6 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /config/spring.rb: -------------------------------------------------------------------------------- 1 | Spring.watch( 2 | ".ruby-version", 3 | ".rbenv-vars", 4 | "tmp/restart.txt", 5 | "tmp/caching-dev.txt" 6 | ) 7 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /config/webpack/environment.js: -------------------------------------------------------------------------------- 1 | const { environment } = require('@rails/webpacker') 2 | 3 | module.exports = environment 4 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/assets/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnbeatty/hnpwa-app/144b266e9e1ca6606abb3d0cfd7af26e2144e871/lib/assets/.keep -------------------------------------------------------------------------------- /lib/tasks/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnbeatty/hnpwa-app/144b266e9e1ca6606abb3d0cfd7af26e2144e871/lib/tasks/.keep -------------------------------------------------------------------------------- /log/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnbeatty/hnpwa-app/144b266e9e1ca6606abb3d0cfd7af26e2144e871/log/.keep -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /public/apple-touch-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnbeatty/hnpwa-app/144b266e9e1ca6606abb3d0cfd7af26e2144e871/public/apple-touch-icon-precomposed.png -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnbeatty/hnpwa-app/144b266e9e1ca6606abb3d0cfd7af26e2144e871/public/apple-touch-icon.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnbeatty/hnpwa-app/144b266e9e1ca6606abb3d0cfd7af26e2144e871/public/favicon.ico -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 2 | -------------------------------------------------------------------------------- /storage/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnbeatty/hnpwa-app/144b266e9e1ca6606abb3d0cfd7af26e2144e871/storage/.keep -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /test/controllers/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnbeatty/hnpwa-app/144b266e9e1ca6606abb3d0cfd7af26e2144e871/test/controllers/.keep -------------------------------------------------------------------------------- /test/fixtures/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnbeatty/hnpwa-app/144b266e9e1ca6606abb3d0cfd7af26e2144e871/test/fixtures/.keep -------------------------------------------------------------------------------- /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/files/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnbeatty/hnpwa-app/144b266e9e1ca6606abb3d0cfd7af26e2144e871/test/fixtures/files/.keep -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /test/helpers/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnbeatty/hnpwa-app/144b266e9e1ca6606abb3d0cfd7af26e2144e871/test/helpers/.keep -------------------------------------------------------------------------------- /test/integration/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnbeatty/hnpwa-app/144b266e9e1ca6606abb3d0cfd7af26e2144e871/test/integration/.keep -------------------------------------------------------------------------------- /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_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_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_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_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_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_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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/mailers/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnbeatty/hnpwa-app/144b266e9e1ca6606abb3d0cfd7af26e2144e871/test/mailers/.keep -------------------------------------------------------------------------------- /test/models/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnbeatty/hnpwa-app/144b266e9e1ca6606abb3d0cfd7af26e2144e871/test/models/.keep -------------------------------------------------------------------------------- /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/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/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/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/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/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/system/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnbeatty/hnpwa-app/144b266e9e1ca6606abb3d0cfd7af26e2144e871/test/system/.keep -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /tmp/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnbeatty/hnpwa-app/144b266e9e1ca6606abb3d0cfd7af26e2144e871/tmp/.keep -------------------------------------------------------------------------------- /vendor/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnbeatty/hnpwa-app/144b266e9e1ca6606abb3d0cfd7af26e2144e871/vendor/.keep --------------------------------------------------------------------------------