├── .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 | 4 | -------------------------------------------------------------------------------- /app/assets/images/cloud-download.svg: -------------------------------------------------------------------------------- 1 | 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 |
2 | <% unless completed %>
3 | <% if item.descendants > 0 %>
4 | Refreshing Comments...
5 | <% else %>
6 | Waiting for comments...
7 | <% end %>
8 |
9 |
13 |
14 | <% else %>
15 | <% if item.descendants > 0 %>
16 | Comments
17 | <% else %>
18 | No Comments
19 | <% end %>
20 | <% end %>
21 |
5 | <%= item.score %> 6 | <% if item.job? %> 7 | job 8 | <% end %> 9 | <% unless item.host.nil? %> 10 | <%= item.host %> 11 | 12 | <% end %> 13 |
14 |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 |You may need to reconnect to Wi-Fi.
5 |<%= 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 | 11 | Loading details 12 |
13 | <% end %> -------------------------------------------------------------------------------- /app/views/users/show.html.erb: -------------------------------------------------------------------------------- 1 | <%= cache ['user', @user_id, @user ] do %> 2 |5 | <%= @user_id %> 6 |
7 |You may have mistyped the address or the page may have moved.
63 |If you are the application owner check the logs for more information.
65 |Maybe you tried to change something you didn't have access to.
63 |If you are the application owner check the logs for more information.
65 |If you are the application owner check the logs for more information.
64 |