├── .babelrc ├── .circleci └── config.yml ├── .dockerignore ├── .gitignore ├── CODE_OF_CONDUCT.md ├── Dockerfile ├── Gemfile ├── Gemfile.lock ├── LICENSE.txt ├── Procfile ├── README.md ├── Rakefile ├── app ├── assets │ ├── images │ │ ├── .keep │ │ ├── circle-turtle-192x192.png │ │ ├── circle-turtle.png │ │ ├── circle-turtle.svg │ │ ├── pics │ │ │ ├── cash-cat.jpg │ │ │ └── rubiks-cube.jpeg │ │ ├── screenshots │ │ │ ├── developer_tools_network_offline.jpg │ │ │ └── developer_tools_network_offline_select.jpg │ │ ├── turtle-logo-120x120.png │ │ ├── turtle-logo-192x192.png │ │ └── turtle-logo.png │ ├── javascripts │ │ ├── application.js │ │ ├── cache-then-network │ │ │ ├── companion.js │ │ │ ├── index.js │ │ │ ├── init.js │ │ │ ├── render.js │ │ │ └── serviceworker.js │ │ ├── components.js │ │ ├── home │ │ │ ├── companion.js │ │ │ ├── index.js │ │ │ ├── manifest.json.erb │ │ │ └── serviceworker.js │ │ ├── manifest.json.erb │ │ ├── nav.js │ │ ├── offline-fallback │ │ │ ├── companion.js │ │ │ ├── index.js │ │ │ └── serviceworker.js.erb │ │ ├── push-react │ │ │ ├── app.js │ │ │ ├── companion.js │ │ │ ├── components.jsx │ │ │ ├── index.js │ │ │ ├── manifest.json.erb │ │ │ └── serviceworker.js │ │ ├── push-simple │ │ │ ├── app.js │ │ │ ├── companion.js │ │ │ ├── index.js │ │ │ ├── manifest.json.erb │ │ │ └── serviceworker.js │ │ └── utils │ │ │ ├── alertonce.js │ │ │ ├── caching.js │ │ │ └── logger.js │ └── stylesheets │ │ ├── _settings.scss │ │ ├── application.scss │ │ ├── layout.scss │ │ ├── materialze.css │ │ └── objects │ │ ├── buttons.scss │ │ ├── callout.scss │ │ ├── footer.scss │ │ ├── index.scss │ │ ├── media.scss │ │ ├── navbar.scss │ │ ├── status.scss │ │ ├── toggle.scss │ │ └── tweet.scss ├── clients │ ├── twitter_client.rb │ └── webpush_client.rb ├── controllers │ ├── application_controller.rb │ ├── concerns │ │ └── .keep │ ├── pages_controller.rb │ ├── push_notifications_controller.rb │ ├── streams_controller.rb │ ├── subscriptions_controller.rb │ └── welcome_controller.rb ├── helpers │ ├── application_helper.rb │ ├── meta_helper.rb │ ├── nav_helper.rb │ ├── settings_helper.rb │ ├── streams_helper.rb │ └── twitter_helper.rb ├── jobs │ └── webpush_job.rb ├── mailers │ └── .keep ├── models │ ├── .keep │ ├── concerns │ │ └── .keep │ └── tweet.rb └── views │ ├── layouts │ ├── _analytics.html.erb │ ├── _footer.html.erb │ ├── _navbar.html.erb │ ├── _sidenav.html.erb │ ├── application.html.erb │ └── offline.html.erb │ ├── pages │ ├── cache-then-network.html.erb │ ├── offline-fallback.html.erb │ ├── push-react.html.erb │ └── push-simple.html.erb │ ├── streams │ ├── _stream.html.erb │ ├── index.html.erb │ └── show.html.erb │ ├── tweets │ └── _tweet.html.erb │ └── welcome │ ├── index.html.erb │ └── offline.html.erb ├── bin ├── bundle ├── foreman ├── rails ├── rake ├── rspec ├── setup ├── spring ├── start └── update ├── config.ru ├── config ├── application.rb ├── boot.rb ├── cable.yml ├── database.yml.ci ├── database.yml.example ├── environment.rb ├── environments │ ├── development.rb │ ├── production.rb │ └── test.rb ├── initializers │ ├── application_controller_renderer.rb │ ├── assets.rb │ ├── backtrace_silencers.rb │ ├── cookies_serializer.rb │ ├── filter_parameter_logging.rb │ ├── high_voltage.rb │ ├── inflections.rb │ ├── mime_types.rb │ ├── new_framework_defaults.rb │ ├── serviceworker.rb │ ├── session_store.rb │ ├── sucker_punch.rb │ └── wrap_parameters.rb ├── locales │ ├── en.yml │ ├── meta.yml │ └── nav.yml ├── puma.rb ├── routes.rb ├── secrets.yml └── spring.rb ├── db ├── schema.rb └── seeds.rb ├── docker-compose.yml ├── lib ├── assets │ └── .keep ├── tasks │ └── .keep └── templates │ └── erb │ └── scaffold │ └── _form.html.erb ├── log └── .keep ├── package.json ├── public ├── 404.html ├── 422.html ├── 500.html ├── favicon.ico ├── robots.txt └── touch-icon.png ├── spec ├── clients │ ├── twitter_client_spec.rb │ └── webpush_client_spec.rb ├── rails_helper.rb ├── spec_helper.rb └── support │ ├── action_mailer.rb │ ├── database_cleaner.rb │ ├── factory_girl.rb │ └── webmock.rb ├── vendor └── assets │ ├── javascripts │ └── .keep │ └── stylesheets │ ├── .keep │ ├── Skeleton-2.0.4.zip │ ├── Skeleton-2.0.4 │ ├── css │ │ ├── normalize.css │ │ └── skeleton.css │ ├── images │ │ └── favicon.png │ └── index.html │ └── skeleton │ ├── normalize.css │ └── skeleton.css └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015"] 3 | } 4 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # This configuration was automatically generated from a CircleCI 1.0 config. 2 | # It should include any build commands you had along with commands that CircleCI 3 | # inferred from your project structure. We strongly recommend you read all the 4 | # comments in this file to understand the structure of CircleCI 2.0, as the idiom 5 | # for configuration has changed substantially in 2.0 to allow arbitrary jobs rather 6 | # than the prescribed lifecycle of 1.0. In general, we recommend using this generated 7 | # configuration as a reference rather than using it in production, though in most 8 | # cases it should duplicate the execution of your original 1.0 config. 9 | version: 2 10 | jobs: 11 | build: 12 | working_directory: ~/rossta/serviceworker-rails-sandbox 13 | parallelism: 1 14 | shell: /bin/bash --login 15 | # CircleCI 2.0 does not support environment variables that refer to each other the same way as 1.0 did. 16 | # If any of these refer to each other, rewrite them so that they don't or see https://circleci.com/docs/2.0/env-vars/#interpolating-environment-variables-to-set-other-environment-variables . 17 | environment: 18 | CIRCLE_ARTIFACTS: /tmp/circleci-artifacts 19 | CIRCLE_TEST_REPORTS: /tmp/circleci-test-results 20 | DATABASE_URL: postgres://ubuntu:@127.0.0.1:5432/circle_test 21 | TWITTER_CONSUMER_KEY: TWITTER_CONSUMER_KEY 22 | TWITTER_CONSUMER_SECRET: TWITTER_CONSUMER_SECRET 23 | TWITTER_ACCESS_TOKEN: TWITTER_ACCESS_TOKEN 24 | TWITTER_ACCESS_SECRET: TWITTER_ACCESS_SECRET 25 | GOOGLE_CLOUD_MESSAGE_API_KEY: GOOGLE_CLOUD_MESSAGE_API_KEY 26 | 27 | # In CircleCI 1.0 we used a pre-configured image with a large number of languages and other packages. 28 | # In CircleCI 2.0 you can now specify your own image, or use one of our pre-configured images. 29 | # The following configuration line tells CircleCI to use the specified docker image as the runtime environment for you job. 30 | # We have selected a pre-built image that mirrors the build environment we use on 31 | # the 1.0 platform, but we recommend you choose an image more tailored to the needs 32 | # of each job. For more information on choosing an image (or alternatively using a 33 | # VM instead of a container) see https://circleci.com/docs/2.0/executor-types/ 34 | # To see the list of pre-built images that CircleCI provides for most common languages see 35 | # https://circleci.com/docs/2.0/circleci-images/ 36 | docker: 37 | - image: circleci/build-image:ubuntu-14.04-XXL-upstart-1189-5614f37 38 | command: /sbin/init 39 | steps: 40 | # Machine Setup 41 | # If you break your build into multiple jobs with workflows, you will probably want to do the parts of this that are relevant in each 42 | # The following `checkout` command checks out your code to your working directory. In 1.0 we did this implicitly. In 2.0 you can choose where in the course of a job your code should be checked out. 43 | - checkout 44 | # Prepare for artifact and test results collection equivalent to how it was done on 1.0. 45 | # In many cases you can simplify this from what is generated here. 46 | # 'See docs on artifact collection here https://circleci.com/docs/2.0/artifacts/' 47 | - run: mkdir -p $CIRCLE_ARTIFACTS $CIRCLE_TEST_REPORTS 48 | # Dependencies 49 | # This would typically go in either a build or a build-and-test job when using workflows 50 | # Restore the dependency cache 51 | - restore_cache: 52 | keys: 53 | # This branch if available 54 | - v1-dep-{{ .Branch }}- 55 | # Default branch if not 56 | - v1-dep-master- 57 | # Any branch if there are none on the default branch - this should be unnecessary if you have your default branch configured correctly 58 | - v1-dep- 59 | # The following line was run implicitly in your 1.0 builds based on what CircleCI inferred about the structure of your project. In 2.0 you need to be explicit about which commands should be run. In some cases you can discard inferred commands if they are not relevant to your project. 60 | - run: if [ -z "${NODE_ENV:-}" ]; then export NODE_ENV=test; fi 61 | - run: export PATH="~/rossta/serviceworker-rails-sandbox/node_modules/.bin:$PATH" 62 | - run: npm install 63 | - run: echo -e "export RAILS_ENV=test\nexport RACK_ENV=test" >> $BASH_ENV 64 | - run: sed -i.bak "/gem ['\"]growl_notify\|autotest-fsevent\|rb-appscript\|rb-fsevent['\"].*, *$/ N; s/\n *//g; /gem ['\"]growl_notify\|autotest-fsevent\|rb-appscript\|rb-fsevent['\"]/ d" Gemfile 65 | - run: 'bundle check --path=vendor/bundle || bundle install --path=vendor/bundle 66 | --jobs=4 --retry=3 ' 67 | # Save dependency cache 68 | - save_cache: 69 | key: v1-dep-{{ .Branch }}-{{ epoch }} 70 | paths: 71 | # This is a broad list of cache paths to include many possible development environments 72 | # You can probably delete some of these entries 73 | - vendor/bundle 74 | - ~/.bundle 75 | - ./node_modules 76 | 77 | - run: 78 | command: cp config/database.yml.ci config/database.yml 79 | 80 | - run: 81 | command: bundle exec rake db:create db:schema:load --trace 82 | environment: 83 | RAILS_ENV: test 84 | RACK_ENV: test 85 | 86 | - run: 87 | command: bundle exec rspec 88 | 89 | - store_test_results: 90 | path: /tmp/circleci-test-results 91 | - store_artifacts: 92 | path: /tmp/circleci-artifacts 93 | - store_artifacts: 94 | path: /tmp/circleci-test-results 95 | 96 | deploy: 97 | machine: 98 | enabled: true 99 | working_directory: ~/rossta/serviceworker-rails-sandbox 100 | environment: 101 | HEROKU_APP_NAME: serviceworker-rails 102 | steps: 103 | - checkout 104 | - run: 105 | name: Deploy to Heroku 106 | command: | 107 | git push https://heroku:$HEROKU_API_KEY@git.heroku.com/$HEROKU_APP_NAME.git master 108 | 109 | workflows: 110 | version: 2 111 | build-and-deploy: 112 | jobs: 113 | - build 114 | - deploy: 115 | requires: 116 | - build 117 | filters: 118 | branches: 119 | only: master 120 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .dockerignore 2 | .git/* 3 | Gemfile.lock 4 | tmp/* 5 | log/* 6 | README.md 7 | -------------------------------------------------------------------------------- /.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 | .env* 10 | 11 | # Ignore all logfiles and tempfiles. 12 | /log/* 13 | !/log/.keep 14 | /tmp 15 | /node_modules 16 | config/database.yml 17 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | As contributors and maintainers of this project, and in the interest of 4 | fostering an open and welcoming community, we pledge to respect all people who 5 | contribute through reporting issues, posting feature requests, updating 6 | documentation, submitting pull requests or patches, and other activities. 7 | 8 | We are committed to making participation in this project a harassment-free 9 | experience for everyone, regardless of level of experience, gender, gender 10 | identity and expression, sexual orientation, disability, personal appearance, 11 | body size, race, ethnicity, age, religion, or nationality. 12 | 13 | Examples of unacceptable behavior by participants include: 14 | 15 | * The use of sexualized language or imagery 16 | * Personal attacks 17 | * Trolling or insulting/derogatory comments 18 | * Public or private harassment 19 | * Publishing other's private information, such as physical or electronic 20 | addresses, without explicit permission 21 | * Other unethical or unprofessional conduct 22 | 23 | Project maintainers have the right and responsibility to remove, edit, or 24 | reject comments, commits, code, wiki edits, issues, and other contributions 25 | that are not aligned to this Code of Conduct, or to ban temporarily or 26 | permanently any contributor for other behaviors that they deem inappropriate, 27 | threatening, offensive, or harmful. 28 | 29 | By adopting this Code of Conduct, project maintainers commit themselves to 30 | fairly and consistently applying these principles to every aspect of managing 31 | this project. Project maintainers who do not follow or enforce the Code of 32 | Conduct may be permanently removed from the project team. 33 | 34 | This code of conduct applies both within project spaces and in public spaces 35 | when an individual is representing the project or its community. 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 38 | reported by contacting a project maintainer at rosskaff@gmail.com. All 39 | complaints will be reviewed and investigated and will result in a response that 40 | is deemed necessary and appropriate to the circumstances. Maintainers are 41 | obligated to maintain confidentiality with regard to the reporter of an 42 | incident. 43 | 44 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 45 | version 1.3.0, available at 46 | [http://contributor-covenant.org/version/1/3/0/][version] 47 | 48 | [homepage]: http://contributor-covenant.org 49 | [version]: http://contributor-covenant.org/version/1/3/0/ -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ruby:2.3.1 2 | 3 | RUN apt-get update -qq && apt-get install -y build-essential 4 | 5 | # for postgres 6 | RUN apt-get install -y libpq-dev 7 | 8 | # for nokogiri 9 | RUN apt-get install -y libxml2-dev libxslt1-dev 10 | 11 | # for capybara-webkit 12 | RUN apt-get install -y libqt4-webkit libqt4-dev xvfb 13 | 14 | # for a JS runtime 15 | RUN apt-get install -y nodejs npm nodejs-legacy 16 | 17 | ENV APP_HOME /app 18 | RUN mkdir -p $APP_HOME 19 | 20 | # use changes to package.json to force Docker not to use the cache 21 | # when we change our application's nodejs dependencies: 22 | # ADD package.json /tmp/package.json 23 | # RUN cd /tmp && npm install 24 | # RUN cp -a /tmp/node_modules $APP_HOME 25 | 26 | WORKDIR $APP_HOME 27 | 28 | ENV BUNDLE_PATH /gems 29 | ENV NODE_PATH /node 30 | 31 | ADD . $APP_HOME 32 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | ruby "2.3.1" 4 | 5 | gem "rails", "~> 5.0.0" 6 | gem "pg", "~> 0.15" 7 | gem "puma" 8 | 9 | gem "sass-rails", "~> 5.0" 10 | gem "uglifier", ">= 1.3.0" 11 | gem "jquery-rails" 12 | gem "browserify-rails" 13 | gem "react-rails" 14 | gem "materialize-sass" 15 | gem "sdoc", "~> 0.4.0", group: :doc 16 | 17 | gem "rack-protection" 18 | gem "title" 19 | gem "flutie" 20 | gem "high_voltage" 21 | gem "i18n-tasks" 22 | 23 | gem "twitter" 24 | gem "ece" 25 | gem "webpush" 26 | gem "sucker_punch" 27 | 28 | gem "non-stupid-digest-assets" 29 | gem "serviceworker-rails", github: "rossta/serviceworker-rails", branch: "master" 30 | 31 | gem "nokogiri", "~> 1.8.2" 32 | 33 | group :development, :test do 34 | gem "factory_girl_rails" 35 | gem "faker" 36 | gem "pry-rails" 37 | gem "pry-rescue" 38 | gem "pry-byebug" 39 | gem "awesome_print" 40 | gem "dotenv-rails" 41 | end 42 | 43 | group :development do 44 | gem "web-console", "~> 2.0" 45 | gem "better_errors" 46 | gem "guard-bundler" 47 | gem "guard-rails" 48 | gem "rails_layout" 49 | gem "rb-fchange", require: false 50 | gem "rb-fsevent", require: false 51 | gem "rb-inotify", require: false 52 | gem "annotate" 53 | gem "spring" 54 | gem "foreman" 55 | end 56 | 57 | group :test do 58 | gem "rspec-rails" 59 | # gem "capybara" 60 | gem "shoulda-matchers" 61 | gem "database_cleaner" 62 | gem "launchy" 63 | # gem "poltergeist" 64 | # gem "formulaic" 65 | gem "timecop" 66 | gem "webmock" 67 | gem "vcr" 68 | gem "seed-fu" 69 | end 70 | 71 | gem "rails_12factor", group: :production 72 | gem "newrelic_rpm", ">= 3.7.3" 73 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GIT 2 | remote: git://github.com/rossta/serviceworker-rails.git 3 | revision: 2a31cfcb684ec0a7a202d4c42877eebf6a7afa80 4 | branch: master 5 | specs: 6 | serviceworker-rails (0.5.0) 7 | railties (>= 3.1) 8 | 9 | GEM 10 | remote: https://rubygems.org/ 11 | specs: 12 | actioncable (5.0.7) 13 | actionpack (= 5.0.7) 14 | nio4r (>= 1.2, < 3.0) 15 | websocket-driver (~> 0.6.1) 16 | actionmailer (5.0.7) 17 | actionpack (= 5.0.7) 18 | actionview (= 5.0.7) 19 | activejob (= 5.0.7) 20 | mail (~> 2.5, >= 2.5.4) 21 | rails-dom-testing (~> 2.0) 22 | actionpack (5.0.7) 23 | actionview (= 5.0.7) 24 | activesupport (= 5.0.7) 25 | rack (~> 2.0) 26 | rack-test (~> 0.6.3) 27 | rails-dom-testing (~> 2.0) 28 | rails-html-sanitizer (~> 1.0, >= 1.0.2) 29 | actionview (5.0.7) 30 | activesupport (= 5.0.7) 31 | builder (~> 3.1) 32 | erubis (~> 2.7.0) 33 | rails-dom-testing (~> 2.0) 34 | rails-html-sanitizer (~> 1.0, >= 1.0.3) 35 | activejob (5.0.7) 36 | activesupport (= 5.0.7) 37 | globalid (>= 0.3.6) 38 | activemodel (5.0.7) 39 | activesupport (= 5.0.7) 40 | activerecord (5.0.7) 41 | activemodel (= 5.0.7) 42 | activesupport (= 5.0.7) 43 | arel (~> 7.0) 44 | activesupport (5.0.7) 45 | concurrent-ruby (~> 1.0, >= 1.0.2) 46 | i18n (>= 0.7, < 2) 47 | minitest (~> 5.1) 48 | tzinfo (~> 1.1) 49 | addressable (2.4.0) 50 | annotate (2.7.1) 51 | activerecord (>= 3.2, < 6.0) 52 | rake (>= 10.4, < 12.0) 53 | arel (7.1.4) 54 | ast (2.3.0) 55 | awesome_print (1.7.0) 56 | babel-source (5.8.35) 57 | babel-transpiler (0.7.0) 58 | babel-source (>= 4.0, < 6) 59 | execjs (~> 2.0) 60 | better_errors (2.1.1) 61 | coderay (>= 1.0.0) 62 | erubis (>= 2.6.6) 63 | rack (>= 0.9.0) 64 | binding_of_caller (0.7.2) 65 | debug_inspector (>= 0.0.1) 66 | browserify-rails (3.1.0) 67 | railties (>= 4.0.0, < 5.1) 68 | sprockets (>= 3.5.2) 69 | buftok (0.2.0) 70 | builder (3.2.3) 71 | byebug (9.0.5) 72 | coderay (1.1.1) 73 | coffee-script-source (1.10.0) 74 | concurrent-ruby (1.0.5) 75 | connection_pool (2.2.0) 76 | crack (0.4.3) 77 | safe_yaml (~> 1.0.0) 78 | crass (1.0.4) 79 | database_cleaner (1.5.3) 80 | debug_inspector (0.0.2) 81 | diff-lcs (1.2.5) 82 | domain_name (0.5.20160615) 83 | unf (>= 0.0.5, < 1.0.0) 84 | dotenv (2.1.1) 85 | dotenv-rails (2.1.1) 86 | dotenv (= 2.1.1) 87 | railties (>= 4.0, < 5.1) 88 | easy_translate (0.5.0) 89 | json 90 | thread 91 | thread_safe 92 | ece (0.2.1) 93 | hkdf 94 | equalizer (0.0.10) 95 | erubis (2.7.0) 96 | execjs (2.7.0) 97 | factory_girl (4.7.0) 98 | activesupport (>= 3.0.0) 99 | factory_girl_rails (4.7.0) 100 | factory_girl (~> 4.7.0) 101 | railties (>= 3.0.0) 102 | faker (1.6.6) 103 | i18n (~> 0.5) 104 | faraday (0.9.2) 105 | multipart-post (>= 1.2, < 3) 106 | ffi (1.9.14) 107 | flutie (2.0.0) 108 | foreman (0.83.0) 109 | thor (~> 0.19.1) 110 | formatador (0.2.5) 111 | globalid (0.4.1) 112 | activesupport (>= 4.2.0) 113 | guard (2.14.0) 114 | formatador (>= 0.2.4) 115 | listen (>= 2.7, < 4.0) 116 | lumberjack (~> 1.0) 117 | nenv (~> 0.1) 118 | notiffany (~> 0.0) 119 | pry (>= 0.9.12) 120 | shellany (~> 0.0) 121 | thor (>= 0.18.1) 122 | guard-bundler (2.1.0) 123 | bundler (~> 1.0) 124 | guard (~> 2.2) 125 | guard-compat (~> 1.1) 126 | guard-compat (1.2.1) 127 | guard-rails (0.8.0) 128 | guard (~> 2.11) 129 | guard-compat (~> 1.0) 130 | hashdiff (0.3.0) 131 | high_voltage (3.0.0) 132 | highline (1.7.8) 133 | hkdf (0.3.0) 134 | http (1.0.4) 135 | addressable (~> 2.3) 136 | http-cookie (~> 1.0) 137 | http-form_data (~> 1.0.1) 138 | http_parser.rb (~> 0.6.0) 139 | http-cookie (1.0.2) 140 | domain_name (~> 0.5) 141 | http-form_data (1.0.1) 142 | http_parser.rb (0.6.0) 143 | i18n (0.9.5) 144 | concurrent-ruby (~> 1.0) 145 | i18n-tasks (0.9.5) 146 | activesupport (>= 4.0.2) 147 | ast (>= 2.1.0) 148 | easy_translate (>= 0.5.0) 149 | erubis 150 | highline (>= 1.7.3) 151 | i18n 152 | parser (>= 2.2.3.0) 153 | term-ansicolor (>= 1.3.2) 154 | terminal-table (>= 1.5.1) 155 | interception (0.5) 156 | jquery-rails (4.2.1) 157 | rails-dom-testing (>= 1, < 3) 158 | railties (>= 4.2.0) 159 | thor (>= 0.14, < 2.0) 160 | json (1.8.3) 161 | jwt (2.1.0) 162 | launchy (2.4.3) 163 | addressable (~> 2.3) 164 | listen (3.1.5) 165 | rb-fsevent (~> 0.9, >= 0.9.4) 166 | rb-inotify (~> 0.9, >= 0.9.7) 167 | ruby_dep (~> 1.2) 168 | loofah (2.2.2) 169 | crass (~> 1.0.2) 170 | nokogiri (>= 1.5.9) 171 | lumberjack (1.0.10) 172 | mail (2.7.0) 173 | mini_mime (>= 0.1.1) 174 | materialize-sass (0.97.7) 175 | sass (~> 3.3) 176 | memoizable (0.4.2) 177 | thread_safe (~> 0.3, >= 0.3.1) 178 | method_source (0.8.2) 179 | mini_mime (1.0.0) 180 | mini_portile2 (2.3.0) 181 | minitest (5.11.3) 182 | multipart-post (2.0.0) 183 | naught (1.1.0) 184 | nenv (0.3.0) 185 | newrelic_rpm (3.16.2.321) 186 | nio4r (2.3.1) 187 | nokogiri (1.8.2) 188 | mini_portile2 (~> 2.3.0) 189 | non-stupid-digest-assets (1.0.8) 190 | sprockets (>= 2.0) 191 | notiffany (0.1.1) 192 | nenv (~> 0.1) 193 | shellany (~> 0.0) 194 | parser (2.3.1.2) 195 | ast (~> 2.2) 196 | pg (0.18.4) 197 | pry (0.10.4) 198 | coderay (~> 1.1.0) 199 | method_source (~> 0.8.1) 200 | slop (~> 3.4) 201 | pry-byebug (3.4.0) 202 | byebug (~> 9.0) 203 | pry (~> 0.10) 204 | pry-rails (0.3.4) 205 | pry (>= 0.9.10) 206 | pry-rescue (1.4.4) 207 | interception (>= 0.5) 208 | pry 209 | puma (3.6.0) 210 | rack (2.0.5) 211 | rack-protection (2.0.2) 212 | rack 213 | rack-test (0.6.3) 214 | rack (>= 1.0) 215 | rails (5.0.7) 216 | actioncable (= 5.0.7) 217 | actionmailer (= 5.0.7) 218 | actionpack (= 5.0.7) 219 | actionview (= 5.0.7) 220 | activejob (= 5.0.7) 221 | activemodel (= 5.0.7) 222 | activerecord (= 5.0.7) 223 | activesupport (= 5.0.7) 224 | bundler (>= 1.3.0) 225 | railties (= 5.0.7) 226 | sprockets-rails (>= 2.0.0) 227 | rails-dom-testing (2.0.3) 228 | activesupport (>= 4.2.0) 229 | nokogiri (>= 1.6) 230 | rails-html-sanitizer (1.0.4) 231 | loofah (~> 2.2, >= 2.2.2) 232 | rails_12factor (0.0.3) 233 | rails_serve_static_assets 234 | rails_stdout_logging 235 | rails_layout (1.0.29) 236 | rails_serve_static_assets (0.0.5) 237 | rails_stdout_logging (0.0.5) 238 | railties (5.0.7) 239 | actionpack (= 5.0.7) 240 | activesupport (= 5.0.7) 241 | method_source 242 | rake (>= 0.8.7) 243 | thor (>= 0.18.1, < 2.0) 244 | rake (11.3.0) 245 | rb-fchange (0.0.6) 246 | ffi 247 | rb-fsevent (0.9.7) 248 | rb-inotify (0.9.7) 249 | ffi (>= 0.5.0) 250 | rdoc (4.2.2) 251 | json (~> 1.4) 252 | react-rails (1.8.2) 253 | babel-transpiler (>= 0.7.0) 254 | coffee-script-source (~> 1.8) 255 | connection_pool 256 | execjs 257 | railties (>= 3.2) 258 | tilt 259 | rspec-core (3.5.2) 260 | rspec-support (~> 3.5.0) 261 | rspec-expectations (3.5.0) 262 | diff-lcs (>= 1.2.0, < 2.0) 263 | rspec-support (~> 3.5.0) 264 | rspec-mocks (3.5.0) 265 | diff-lcs (>= 1.2.0, < 2.0) 266 | rspec-support (~> 3.5.0) 267 | rspec-rails (3.5.1) 268 | actionpack (>= 3.0) 269 | activesupport (>= 3.0) 270 | railties (>= 3.0) 271 | rspec-core (~> 3.5.0) 272 | rspec-expectations (~> 3.5.0) 273 | rspec-mocks (~> 3.5.0) 274 | rspec-support (~> 3.5.0) 275 | rspec-support (3.5.0) 276 | ruby_dep (1.4.0) 277 | safe_yaml (1.0.4) 278 | sass (3.4.22) 279 | sass-rails (5.0.6) 280 | railties (>= 4.0.0, < 6) 281 | sass (~> 3.1) 282 | sprockets (>= 2.8, < 4.0) 283 | sprockets-rails (>= 2.0, < 4.0) 284 | tilt (>= 1.1, < 3) 285 | sdoc (0.4.1) 286 | json (~> 1.7, >= 1.7.7) 287 | rdoc (~> 4.0) 288 | seed-fu (2.3.6) 289 | activerecord (>= 3.1) 290 | activesupport (>= 3.1) 291 | shellany (0.0.1) 292 | shoulda-matchers (3.1.1) 293 | activesupport (>= 4.0.0) 294 | simple_oauth (0.3.1) 295 | slop (3.6.0) 296 | spring (1.7.2) 297 | sprockets (3.7.1) 298 | concurrent-ruby (~> 1.0) 299 | rack (> 1, < 3) 300 | sprockets-rails (3.2.1) 301 | actionpack (>= 4.0) 302 | activesupport (>= 4.0) 303 | sprockets (>= 3.0.0) 304 | sucker_punch (2.0.2) 305 | concurrent-ruby (~> 1.0.0) 306 | term-ansicolor (1.3.2) 307 | tins (~> 1.0) 308 | terminal-table (1.6.0) 309 | thor (0.19.4) 310 | thread (0.2.2) 311 | thread_safe (0.3.6) 312 | tilt (2.0.5) 313 | timecop (0.8.1) 314 | tins (1.12.0) 315 | title (0.0.7) 316 | i18n 317 | rails (>= 3.1) 318 | twitter (5.16.0) 319 | addressable (~> 2.3) 320 | buftok (~> 0.2.0) 321 | equalizer (= 0.0.10) 322 | faraday (~> 0.9.0) 323 | http (~> 1.0) 324 | http_parser.rb (~> 0.6.0) 325 | json (~> 1.8) 326 | memoizable (~> 0.4.0) 327 | naught (~> 1.0) 328 | simple_oauth (~> 0.3.0) 329 | tzinfo (1.2.5) 330 | thread_safe (~> 0.1) 331 | uglifier (3.0.2) 332 | execjs (>= 0.3.0, < 3) 333 | unf (0.1.4) 334 | unf_ext 335 | unf_ext (0.0.7.2) 336 | vcr (3.0.3) 337 | web-console (2.3.0) 338 | activemodel (>= 4.0) 339 | binding_of_caller (>= 0.7.2) 340 | railties (>= 4.0) 341 | sprockets-rails (>= 2.0, < 4.0) 342 | webmock (2.1.0) 343 | addressable (>= 2.3.6) 344 | crack (>= 0.3.2) 345 | hashdiff 346 | webpush (0.3.4) 347 | hkdf (~> 0.2) 348 | jwt (~> 2.0) 349 | websocket-driver (0.6.5) 350 | websocket-extensions (>= 0.1.0) 351 | websocket-extensions (0.1.3) 352 | 353 | PLATFORMS 354 | ruby 355 | 356 | DEPENDENCIES 357 | annotate 358 | awesome_print 359 | better_errors 360 | browserify-rails 361 | database_cleaner 362 | dotenv-rails 363 | ece 364 | factory_girl_rails 365 | faker 366 | flutie 367 | foreman 368 | guard-bundler 369 | guard-rails 370 | high_voltage 371 | i18n-tasks 372 | jquery-rails 373 | launchy 374 | materialize-sass 375 | newrelic_rpm (>= 3.7.3) 376 | nokogiri (~> 1.8.2) 377 | non-stupid-digest-assets 378 | pg (~> 0.15) 379 | pry-byebug 380 | pry-rails 381 | pry-rescue 382 | puma 383 | rack-protection 384 | rails (~> 5.0.0) 385 | rails_12factor 386 | rails_layout 387 | rb-fchange 388 | rb-fsevent 389 | rb-inotify 390 | react-rails 391 | rspec-rails 392 | sass-rails (~> 5.0) 393 | sdoc (~> 0.4.0) 394 | seed-fu 395 | serviceworker-rails! 396 | shoulda-matchers 397 | spring 398 | sucker_punch 399 | timecop 400 | title 401 | twitter 402 | uglifier (>= 1.3.0) 403 | vcr 404 | web-console (~> 2.0) 405 | webmock 406 | webpush 407 | 408 | RUBY VERSION 409 | ruby 2.3.1p112 410 | 411 | BUNDLED WITH 412 | 1.14.6 413 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Ross Kaffenberger 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: bundle exec puma -C config/puma.rb 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Service Workers on Rails 2 | 3 | This sandbox demonstrates various use cases for "Service Workers on Rails". Integration of Service Workers with the Rails asset pipeline is provided by the [serviceworker-rails](https://github.com/rossta/serviceworker-rails) gem. 4 | 5 | ## Background 6 | 7 | The [Service Worker API](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API) is coming. Service Workers are scripts that act as a proxy between the browser and the server. Among other things, Service Workers can be used to: 8 | 9 | - enable the creation of effective offline experiences 10 | - increase perceived performance while online 11 | - access push notifications and background sync APIs. 12 | 13 | Service Workers are scripts that live outside the context of a rendered page. 14 | This means that there are some considerations for hosting Service Workers: 15 | 16 | - service workers must be served over HTTPS 17 | - service workers must be served within the "scope" of the page(s) they control ([pending override capability](https://slightlyoff.github.io/ServiceWorker/spec/service_worker/#service-worker-allowed)) 18 | - service workers should not be cached: browsers install service workers in the background so requesting and evaluating 19 | these scripts will not tie up page rendering 20 | 21 | ### Rails integration 22 | 23 | So you want to use Service Workers in your Rails app? Consider that the Rails asset pipeline bundles JavaScript assets so that they're typically finger-printed, heavily cached, and served out of the `/assets` directory. While we'd like to take advantage of the asset pipeline for transpiling our Service Worker scripts just like any other JavaScript assets, we need flexibility for how these assets are served to the client. 24 | 25 | This is where the [serviceworker-rails](https://github.com/rossta/serviceworker-rails) gem comes in. Using `serviceworker-rails` in your Rails app allows you to map Service Worker endpoints to bundled Rails assets and adds the appropriate (configurable) response headers. You can still take advantage of Rails pipeline with Service Workers in development and production! 26 | 27 | The sandbox serviceworker initializer in [`config/initializers/serviceworker.rb`](https://github.com/rossta/serviceworker-rails-sandbox/blob/master/config/initializers/serviceworker.rb) provides several examples for customizing how service worker endpoints should map to assets in the Rails asset pipeline. Corresponding serviceworker implementations for the examples can be found in [`app/assets/javascripts`](https://github.com/rossta/serviceworker-rails-sandbox/tree/master/app/assets/javascripts). 28 | 29 | ## Resources 30 | 31 | Examples in this sandbox are inspired by a variety of resources: 32 | 33 | * [Mozilla's Service Worker Cookbook](https://github.com/mozilla/serviceworker-cookbook/) 34 | * [Jake Archibald's Offline Cookbook](https://jakearchibald.com/2014/offline-cookbook/) 35 | * [Pony Foo Service Worker Articles](https://ponyfoo.com/articles/tagged/serviceworker) 36 | 37 | ## Development 38 | 39 | Check out the project with git, run `bundle install` and copy the 40 | `database.yml.example` to `database.yml` and edit to fit your needs. 41 | 42 | You could also try running with Docker. The `docker-compose.yml` assumes the 43 | existence of an `.env.docker` file. You may want copy the `.env.docker.example` 44 | file and set up your own environment variables. Assuming you have Docker 45 | installed and running, you could simply try running `docker-compose up`. YMMV. 46 | 47 | If desired, for certain features you'll need Twitter app credentials. Example keys can be found in 48 | `.env.docker.example`. 49 | 50 | Good luck! 51 | 52 | ## Contributing 53 | 54 | Bug reports and pull requests are welcome on GitHub at https://github.com/rossta/serviceworker-rails-sandbox. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct. 55 | 56 | ## License 57 | 58 | The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT). 59 | -------------------------------------------------------------------------------- /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 File.expand_path('../config/application', __FILE__) 5 | 6 | Rails.application.load_tasks 7 | -------------------------------------------------------------------------------- /app/assets/images/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rossta/serviceworker-rails-sandbox/3d16b9e7475e56045f97908a25029aa309ed1dea/app/assets/images/.keep -------------------------------------------------------------------------------- /app/assets/images/circle-turtle-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rossta/serviceworker-rails-sandbox/3d16b9e7475e56045f97908a25029aa309ed1dea/app/assets/images/circle-turtle-192x192.png -------------------------------------------------------------------------------- /app/assets/images/circle-turtle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rossta/serviceworker-rails-sandbox/3d16b9e7475e56045f97908a25029aa309ed1dea/app/assets/images/circle-turtle.png -------------------------------------------------------------------------------- /app/assets/images/circle-turtle.svg: -------------------------------------------------------------------------------- 1 | Created by Carla Diasfrom the Noun Project -------------------------------------------------------------------------------- /app/assets/images/pics/cash-cat.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rossta/serviceworker-rails-sandbox/3d16b9e7475e56045f97908a25029aa309ed1dea/app/assets/images/pics/cash-cat.jpg -------------------------------------------------------------------------------- /app/assets/images/pics/rubiks-cube.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rossta/serviceworker-rails-sandbox/3d16b9e7475e56045f97908a25029aa309ed1dea/app/assets/images/pics/rubiks-cube.jpeg -------------------------------------------------------------------------------- /app/assets/images/screenshots/developer_tools_network_offline.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rossta/serviceworker-rails-sandbox/3d16b9e7475e56045f97908a25029aa309ed1dea/app/assets/images/screenshots/developer_tools_network_offline.jpg -------------------------------------------------------------------------------- /app/assets/images/screenshots/developer_tools_network_offline_select.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rossta/serviceworker-rails-sandbox/3d16b9e7475e56045f97908a25029aa309ed1dea/app/assets/images/screenshots/developer_tools_network_offline_select.jpg -------------------------------------------------------------------------------- /app/assets/images/turtle-logo-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rossta/serviceworker-rails-sandbox/3d16b9e7475e56045f97908a25029aa309ed1dea/app/assets/images/turtle-logo-120x120.png -------------------------------------------------------------------------------- /app/assets/images/turtle-logo-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rossta/serviceworker-rails-sandbox/3d16b9e7475e56045f97908a25029aa309ed1dea/app/assets/images/turtle-logo-192x192.png -------------------------------------------------------------------------------- /app/assets/images/turtle-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rossta/serviceworker-rails-sandbox/3d16b9e7475e56045f97908a25029aa309ed1dea/app/assets/images/turtle-logo.png -------------------------------------------------------------------------------- /app/assets/javascripts/application.js: -------------------------------------------------------------------------------- 1 | // This is a manifest file that'll be compiled into application.js, which will include all the files 2 | // listed below. 3 | // 4 | // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts, 5 | // or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path. 6 | // 7 | // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the 8 | // compiled file. 9 | // 10 | // Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details 11 | // about supported directives. 12 | // 13 | //= require jquery 14 | //= require jquery_ujs 15 | //= require materialize-sprockets 16 | //= require ./nav 17 | //= require ./components 18 | //= require ./home 19 | -------------------------------------------------------------------------------- /app/assets/javascripts/cache-then-network/companion.js: -------------------------------------------------------------------------------- 1 | import Logger from 'utils/logger'; 2 | import {alertSWSupport} from 'utils/alertonce'; 3 | const logger = new Logger('[cache-then-network/client]'); 4 | 5 | function ready() { 6 | if (navigator.serviceWorker) { 7 | logger.log('Registering serviceworker'); 8 | navigator.serviceWorker.register('serviceworker.js', { scope: './' }) 9 | .then(function(reg) { 10 | logger.log(reg.scope, 'register'); 11 | logger.log('Service worker change, registered the service worker'); 12 | }); 13 | } else { 14 | alertSWSupport(); 15 | } 16 | } 17 | 18 | export { ready }; 19 | -------------------------------------------------------------------------------- /app/assets/javascripts/cache-then-network/index.js: -------------------------------------------------------------------------------- 1 | import { ready as companion } from 'cache-then-network/companion'; 2 | import { ready as init } from 'cache-then-network/init'; 3 | 4 | function ready() { 5 | companion(); 6 | init(); 7 | } 8 | 9 | $(document).ready(ready); 10 | $(document).on('page:load.cache-then-network', ready); 11 | $(document).on('page:before-change.cache-then-network', function() { 12 | $(document).unbind('page:load.cache-then-network'); 13 | }); 14 | -------------------------------------------------------------------------------- /app/assets/javascripts/cache-then-network/init.js: -------------------------------------------------------------------------------- 1 | import Logger from 'utils/logger'; 2 | import {cacheKey} from 'utils/caching'; 3 | import {renderPage, renderError} from 'cache-then-network/render'; 4 | import _ from 'lodash'; 5 | 6 | const target = "#demo"; 7 | const logger = new Logger('[cache-then-network/client]'); 8 | 9 | const state = { 10 | target: target, 11 | tweets: [], 12 | source: "", 13 | message: "", 14 | force: false 15 | }; 16 | 17 | function ready() { 18 | fetchData(); 19 | $(target).on('click', '.delete-cache', deleteCache); 20 | $(target).on('click', '.refresh', refresh); 21 | } 22 | 23 | function fetchData() { 24 | let networkDataReceived = false; 25 | 26 | renderPage({}, state); 27 | 28 | // fetch fresh data 29 | var networkUpdate = fetch('/streams/cats.json').then(function(response) { 30 | return response.json(); 31 | }).then(function(data) { 32 | networkDataReceived = true; 33 | logger.log('rendering from network after brief delay'); 34 | setTimeout(function() { 35 | renderPage({ 36 | tweets: data, 37 | source: "network", 38 | message: "Data updated from network and now cached." 39 | }, state); 40 | }, 1000); 41 | }); 42 | 43 | // fetch cached data 44 | caches.match('/streams/cats.json').then(function(response) { 45 | if (!response) throw Error("No data"); 46 | return response.json(); 47 | }).then(function(data) { 48 | // don't overwrite newer network data 49 | if (!networkDataReceived) { 50 | logger.log('rendering from cache'); 51 | renderPage({ 52 | tweets: data, 53 | source: "cache", 54 | message: "Data retrieved from cache, requesting update from network" 55 | }, state); 56 | } 57 | }).catch(function() { 58 | // we did not get cached data, the network is our last hope 59 | return networkUpdate; 60 | }).catch(renderError); 61 | } 62 | 63 | function deleteCache(e) { 64 | e.preventDefault(); 65 | caches.delete(cacheKey('cache-then-network')).then((success) => { 66 | logger.log('Cached deleted', success); 67 | renderPage({ force: true, message: "Cache is now deleted." }, state); 68 | }); 69 | } 70 | 71 | function refresh(e) { 72 | e.preventDefault(); 73 | state.tweets = []; 74 | location.reload(); 75 | } 76 | 77 | export { ready }; 78 | -------------------------------------------------------------------------------- /app/assets/javascripts/cache-then-network/render.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | 3 | let target; 4 | 5 | function render(html) { 6 | $(target).html(html); 7 | } 8 | 9 | function renderPage(data, state) { 10 | data = data || {}; 11 | 12 | target = state.target || "#demo"; 13 | state.tweets = state.tweets || []; 14 | data.tweets = data.tweets || []; 15 | 16 | if (data.tweets.length) { 17 | state.tweets = data.tweets; 18 | } else { 19 | if (!state.tweets.length) { 20 | renderLoading(data, state); 21 | } 22 | if (!data.force) { 23 | return; 24 | } 25 | } 26 | 27 | render(`
${statusToHtml(data)}${tweetsToHtml(state.tweets)}
`); 28 | } 29 | 30 | function renderError(e) { 31 | const msg = 'No data received from cache or network or error'; 32 | render(`
${msg}: ${e}
`); 33 | } 34 | 35 | function renderLoading(data, state) { 36 | if (state.tweets.length) return; 37 | render(`
Loading ...
`); 38 | setTimeout(renderDots, 250); 39 | } 40 | 41 | function renderDots(n) { 42 | n = n || 0; 43 | let dots = [".", ".", "."].filter((d, i) => i <= (n % 3)).join(""); 44 | let $dots = $(target).find('.dots'); 45 | if ($dots.length) { 46 | $dots.text(dots); 47 | setTimeout(() => renderDots(n+1), 250); 48 | } 49 | } 50 | 51 | function tweetsToHtml(tweets) { 52 | return tweets.sort((tw, i) => tw.id).map(tweetToHtml).join(""); 53 | } 54 | 55 | function tweetToHtml(tweet) { 56 | let pics = _(tweet.entities.media).map(img => ``).join("\n"); 57 | return ` 58 |
59 |
60 | 61 |
62 |
${tweet.text}
63 |
64 |
65 |
66 | ${pics} 67 |
68 |
69 | `; 70 | } 71 | 72 | function statusToHtml(data) { 73 | let deleteLink = ""; 74 | if(data.source === "network") { 75 | deleteLink = `Delete cache` 76 | } 77 | 78 | return ` 79 |
80 |
81 | 82 | Refresh 83 | ${deleteLink} 84 | 85 | ${data.message} 86 |
87 |
88 | `; 89 | } 90 | 91 | export { 92 | renderPage, 93 | renderError 94 | } 95 | -------------------------------------------------------------------------------- /app/assets/javascripts/cache-then-network/serviceworker.js: -------------------------------------------------------------------------------- 1 | import {cacheKey} from 'utils/caching'; 2 | import Logger from 'utils/logger'; 3 | const logger = new Logger('[cache-then-network/serviceworker]'); 4 | 5 | self.addEventListener('install', function onInstall() { 6 | logger.log('onInstall') 7 | }); 8 | 9 | self.addEventListener('fetch', function onFetch(event) { 10 | let request = event.request; 11 | let url = new URL(request.url); 12 | 13 | if (!url.pathname.endsWith('.json')) { return; } 14 | logger.log('onFetch', request.url); 15 | 16 | event.respondWith( 17 | caches.open(cacheKey('cache-then-network')).then((cache) => { 18 | return fetch(request).then((response) => { 19 | cache.put(request, response.clone()); 20 | return response; 21 | }); 22 | }) 23 | ); 24 | }); 25 | -------------------------------------------------------------------------------- /app/assets/javascripts/components.js: -------------------------------------------------------------------------------- 1 | //= require_self 2 | //= require react_ujs 3 | 4 | React = window.React = require('react'); 5 | -------------------------------------------------------------------------------- /app/assets/javascripts/home/companion.js: -------------------------------------------------------------------------------- 1 | import Logger from 'utils/logger'; 2 | import {alertSWSupport} from 'utils/alertonce'; 3 | const logger = new Logger('[home/client]'); 4 | 5 | if (navigator.serviceWorker) { 6 | logger.log('Registering serviceworker'); 7 | navigator.serviceWorker.register('serviceworker.js', { scope: './' }) 8 | .then(function(reg) { 9 | logger.log(reg.scope, 'register'); 10 | logger.log('Service worker change, registered the service worker'); 11 | }); 12 | } else { 13 | alertSWSupport(); 14 | } 15 | -------------------------------------------------------------------------------- /app/assets/javascripts/home/index.js: -------------------------------------------------------------------------------- 1 | //= require home/companion 2 | 3 | -------------------------------------------------------------------------------- /app/assets/javascripts/home/manifest.json.erb: -------------------------------------------------------------------------------- 1 | <% icon_sizes = Rails.configuration.serviceworker.icon_sizes %> 2 | { 3 | "name": "My Progressive Rails App", 4 | "short_name": "Progressive", 5 | "start_url": "/", 6 | "icons": [ 7 | <% icon_sizes.map { |s| "#{s}x#{s}" }.each.with_index do |dim, i| %> 8 | { 9 | "src": "<%= image_path "serviceworker-rails/heart-#{dim}.png" %>", 10 | "sizes": "<%= dim %>", 11 | "type": "image/png" 12 | }<%= i == (icon_sizes.length - 1) ? '' : ',' %> 13 | <% end %> 14 | ], 15 | "theme_color": "#000000", 16 | "background_color": "#FFFFFF", 17 | "display": "fullscreen", 18 | "orientation": "portrait" 19 | } 20 | -------------------------------------------------------------------------------- /app/assets/javascripts/home/serviceworker.js: -------------------------------------------------------------------------------- 1 | import {version} from 'utils/caching'; 2 | 3 | import Logger from 'utils/logger'; 4 | const logger = new Logger('[home/serviceworker]'); 5 | 6 | self.addEventListener('install', function onInstall(event) { 7 | logger.log('install event started.'); 8 | self.skipWaiting(); 9 | }); 10 | 11 | self.addEventListener('activate', function onActivate() { 12 | logger.log('activate event started.'); 13 | 14 | return caches 15 | .keys() 16 | .then((keys) => { 17 | return Promise.all( // We return a promise that settles when all outdated caches are deleted. 18 | keys 19 | .filter((key) => !key.startsWith(version)) 20 | .map((key) => caches.delete(key)) 21 | ); 22 | }) 23 | .then(() => { 24 | logger.log('activate event completed', 'removeOldCache completed.'); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /app/assets/javascripts/manifest.json.erb: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Service Worker Rails Sandbox", 3 | "short_name": "SW Rails", 4 | "start_url": "/", 5 | "display": "browser", 6 | "orientation": "portrait", 7 | "background_color": "#dd8923", 8 | "icons": [{ 9 | "src": "<%= image_path "circle-turtle-192x192.png" %>", 10 | "sizes": "192x192", 11 | "type": "image/png" 12 | }] 13 | } 14 | -------------------------------------------------------------------------------- /app/assets/javascripts/nav.js: -------------------------------------------------------------------------------- 1 | (function($) { 2 | 3 | // Detect touch screen and enable scrollbar if necessary 4 | function is_touch_device() { 5 | try { 6 | document.createEvent("TouchEvent"); 7 | return true; 8 | } catch (e) { 9 | return false; 10 | } 11 | } 12 | 13 | function ready() { 14 | // Initialize collapse button 15 | $(".button-collapse").sideNav(); 16 | // Initialize collapsible (uncomment the line below if you use the dropdown variation) 17 | // $('.collapsible').collapsible(); 18 | 19 | $("#nav-mobile ul.collapsible li li.active").parents("li").children("a.collapsible-header").click() 20 | 21 | if (is_touch_device()) { 22 | $('#nav-mobile').css({ overflow: 'auto'}); 23 | } 24 | } 25 | 26 | $(ready); // end of document ready 27 | 28 | })(jQuery); 29 | -------------------------------------------------------------------------------- /app/assets/javascripts/offline-fallback/companion.js: -------------------------------------------------------------------------------- 1 | import Logger from 'utils/logger'; 2 | import {alertSWSupport} from 'utils/alertonce'; 3 | const logger = new Logger('[offline-fallback/client]'); 4 | 5 | if (navigator.serviceWorker) { 6 | logger.log('Registering serviceworker'); 7 | navigator.serviceWorker.register('serviceworker.js', { scope: './' }) 8 | .then(function(reg) { 9 | logger.log(reg.scope, 'register'); 10 | logger.log('Service worker change, registered the service worker'); 11 | }); 12 | } else { 13 | alertSWSupport(); 14 | } 15 | -------------------------------------------------------------------------------- /app/assets/javascripts/offline-fallback/index.js: -------------------------------------------------------------------------------- 1 | //= require offline-fallback/companion 2 | -------------------------------------------------------------------------------- /app/assets/javascripts/offline-fallback/serviceworker.js.erb: -------------------------------------------------------------------------------- 1 | import {cacheKey} from 'utils/caching'; 2 | import Logger from 'utils/logger'; 3 | const logger = new Logger('[offline-fallback/serviceworker]'); 4 | 5 | function cacheOfflineResources() { 6 | logger.log('install event started.'); 7 | 8 | return caches 9 | .open(cacheKey('offline')) 10 | .then((cache) => cache.addAll([ 11 | "/pages/offline-fallback/offline", 12 | "<%= asset_path 'pics/cash-cat.jpg' %>", 13 | "<%= asset_path 'application.css' %>" 14 | ])) 15 | .then(() => { 16 | logger.log('installation event complete!'); 17 | }); 18 | } 19 | 20 | function fetchOrOffline(request) { 21 | logger.log('fetch event started', request.url); 22 | 23 | if (request.method !== 'GET') { return; } 24 | 25 | return fetch(request) 26 | .catch((error) => { 27 | logger.error('fetch failed, falling back to offline response in cache', error); 28 | return offlineResponse(request); 29 | }); 30 | } 31 | 32 | function offlineResponse(request) { 33 | let match; 34 | if (request.headers.get('accept').includes('text/html')) { 35 | match = '/pages/offline-fallback/offline'; 36 | } else { 37 | match = request; 38 | } 39 | 40 | return caches.open(cacheKey('offline')).then((cache) => cache.match(match)) 41 | } 42 | 43 | self.addEventListener('install', function onInstall(event) { 44 | event.waitUntil(cacheOfflineResources()); 45 | }); 46 | 47 | self.addEventListener('fetch', function onFetch(event) { 48 | let request = event.request; 49 | 50 | if (request.method !== 'GET') { return; } 51 | 52 | event.respondWith(fetchOrOffline(request)); 53 | }); 54 | -------------------------------------------------------------------------------- /app/assets/javascripts/push-react/app.js: -------------------------------------------------------------------------------- 1 | import { render, dismount } from 'push-react/components'; 2 | import Logger from 'utils/logger'; 3 | const logger = new Logger('[push-react/app]'); 4 | 5 | function setup(onSubscribed, onUnsubscribed) { 6 | if (!ServiceWorkerRegistration.prototype.showNotification) { 7 | logger.warn('Notifications are not supported in your browser'); 8 | return; 9 | } 10 | 11 | if (Notification.permission === 'denied') { 12 | logger.warn('You have blocked notifications'); 13 | return; 14 | } 15 | 16 | if (!window.PushManager) { 17 | logger.warn('Push messaging is not supported in your browser'); 18 | } 19 | 20 | navigator.serviceWorker.ready 21 | .then((serviceWorkerRegistration) => { 22 | logger.log('Initializing push button state'); 23 | serviceWorkerRegistration.pushManager.getSubscription() 24 | .then((subscription) => { 25 | if (!subscription) { 26 | logger.log('You are not currently subscribed to push notifications'); 27 | onUnsubscribed(); 28 | return; 29 | } 30 | 31 | onSubscribed(subscription); 32 | }) 33 | .catch((error) => { 34 | logger.warn('Error during getSubscription()', error); 35 | }); 36 | }); 37 | } 38 | 39 | function subscribe(onSubscribed, onUnsubscribed) { 40 | navigator.serviceWorker.ready.then((serviceWorkerRegistration) => { 41 | serviceWorkerRegistration.pushManager 42 | .subscribe({ 43 | userVisibleOnly: true, 44 | applicationServerKey: window.publicKey 45 | }) 46 | .then(onSubscribed) 47 | .catch((e) => { 48 | if (Notification.permission === 'denied') { 49 | logger.warn('Permission to send notifications denied'); 50 | } else { 51 | logger.error('Unable to subscribe to push', e); 52 | } 53 | onUnsubscribed(); 54 | }) 55 | }); 56 | } 57 | 58 | function unsubscribe(onUnsubscribed) { 59 | navigator.serviceWorker.ready 60 | .then((serviceWorkerRegistration) => { 61 | serviceWorkerRegistration.pushManager.getSubscription() 62 | .then((subscription) => { 63 | if (!subscription) { 64 | return onUnsubscribed(); 65 | } 66 | 67 | logger.log('Unsubscribing from push notifications', subscription.toJSON()); 68 | 69 | subscription.unsubscribe() 70 | .then(onUnsubscribed) 71 | .catch((e) => { 72 | logger.error('Error thrown while unsubscribing from push messaging', e); 73 | }); 74 | }); 75 | }); 76 | } 77 | 78 | function serverSubscribe(subscription) { 79 | window.usersubscription = JSON.stringify(subscription); 80 | let body = JSON.stringify({ subscription }); 81 | 82 | return fetch("/subscribe", { 83 | headers: formHeaders(), 84 | method: 'POST', 85 | credentials: 'include', 86 | body: body 87 | }) 88 | .catch((e) => { 89 | logger.error("Could not save subscription", e); 90 | }); 91 | } 92 | 93 | function serverUnsubscribe() { 94 | window.usersubscription = null; 95 | 96 | return fetch("/unsubscribe", { 97 | headers: formHeaders(), 98 | method: 'DELETE', 99 | credentials: 'include' 100 | }) 101 | .catch((e) => { 102 | logger.error("Could not save subscription", e); 103 | }); 104 | } 105 | 106 | function sendNotification() { 107 | return fetch("/push", { 108 | headers: formHeaders(), 109 | method: 'POST', 110 | credentials: 'include' 111 | }).then((response) => { 112 | logger.log("Push response", response); 113 | if (response.status >= 500) { 114 | logger.error(response.statusText); 115 | alert("Sorry, there was a problem sending the notification. Try resubscribing to push messages and resending."); 116 | } 117 | }) 118 | .catch((e) => { 119 | logger.error("Error sending notification", e); 120 | }); 121 | 122 | } 123 | 124 | function formHeaders() { 125 | return new Headers({ 126 | 'Content-Type': 'application/json', 127 | 'X-Requested-With': 'XMLHttpRequest', 128 | 'X-CSRF-Token': authenticityToken(), 129 | }); 130 | } 131 | 132 | function authenticityToken() { 133 | return document.querySelector('meta[name=csrf-token]').content; 134 | } 135 | 136 | function ready() { 137 | return render({ 138 | setup, 139 | subscribe, 140 | unsubscribe, 141 | serverSubscribe, 142 | serverUnsubscribe, 143 | sendNotification, 144 | }) 145 | } 146 | 147 | export { ready, dismount }; 148 | -------------------------------------------------------------------------------- /app/assets/javascripts/push-react/companion.js: -------------------------------------------------------------------------------- 1 | import Logger from 'utils/logger'; 2 | import {alertSWSupport} from 'utils/alertonce'; 3 | const logger = new Logger('[push-react/client]'); 4 | 5 | function ready() { 6 | if (navigator.serviceWorker) { 7 | logger.log('Registering serviceworker'); 8 | navigator.serviceWorker.register('/push-react/serviceworker.js') 9 | .then(function(reg) { 10 | logger.log(reg.scope, 'register'); 11 | logger.log('Service worker change, registered the service worker'); 12 | }); 13 | } else { 14 | alertSWSupport(); 15 | } 16 | } 17 | 18 | export { ready }; 19 | -------------------------------------------------------------------------------- /app/assets/javascripts/push-react/components.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import Logger from 'utils/logger'; 4 | const logger = new Logger('[push-react/app]'); 5 | 6 | const SendMessageButton = React.createClass({ 7 | onClick(e) { 8 | return this.props.onClick(e); 9 | }, 10 | 11 | render() { 12 | let { isEnabled, isSubscribed } = this.props; 13 | let disabled = !isEnabled || !isSubscribed; 14 | let classNames = ["btn waves-effect waves-light"] 15 | if (!isSubscribed) { 16 | classNames.push("disabled"); 17 | } 18 | 19 | return ( 20 | 24 | Send message 25 | 26 | ); 27 | } 28 | }); 29 | 30 | const PushSubscriptionToggle = React.createClass({ 31 | onChange(e) { 32 | return this.props.onChange(e); 33 | }, 34 | 35 | getLabel() { 36 | return this.props.isSubscribed ? 37 | "Unsubscribe from push messages" : 38 | "Subscribe to push messages"; 39 | }, 40 | 41 | render() { 42 | let { isEnabled, isSubscribed } = this.props; 43 | 44 | return ( 45 |
46 | 50 |
51 | 62 |
63 |
64 | ); 65 | } 66 | }); 67 | 68 | const PushControls = React.createClass({ 69 | getInitialState() { 70 | return { 71 | isSubscribed: false, 72 | isEnabled: true, 73 | }; 74 | }, 75 | 76 | componentDidMount() { 77 | const self = this; 78 | 79 | self.disable(); 80 | }, 81 | 82 | onChange() { 83 | if (this.state.isSubscribed) { 84 | this.unsubscribe(); 85 | } else { 86 | this.subscribe(); 87 | } 88 | }, 89 | 90 | onClick() { 91 | if (this.state.isSubscribed) { 92 | this.props.sendNotification(); 93 | } else { 94 | alert("Cannot send notification while push messages are disabled"); 95 | } 96 | }, 97 | 98 | subscribe() { 99 | this.disable(); 100 | 101 | this.props.subscribe(this.onSubscribed, this.onUnsubscribed); 102 | }, 103 | 104 | onSubscribed(subscription) { 105 | logger.log('Subscribing to push notifications', subscription.toJSON()); 106 | this.setState({ isEnabled: false, isSubscribed: true }); 107 | return this.props.serverSubscribe(subscription).then(this.enable); 108 | }, 109 | 110 | unsubscribe() { 111 | this.disable(); 112 | this.props.unsubscribe(this.onUnsubscribed); 113 | }, 114 | 115 | onUnsubscribed() { 116 | this.setState({ isEnabled: false, isSubscribed: false }); 117 | this.props.serverUnsubscribe().then(this.enable); 118 | }, 119 | 120 | enable() { 121 | this.setState({ isEnabled: true }); 122 | }, 123 | 124 | disable() { 125 | this.setState({ isEnabled: false }); 126 | }, 127 | 128 | render() { 129 | let state = this.state; 130 | return ( 131 |
132 |

Demo

133 | 134 | 135 |
136 | ); 137 | } 138 | }); 139 | 140 | function render(props) { 141 | const root = document.getElementById('push-react-app'); 142 | 143 | if (root) { 144 | ReactDOM.render(, root); 145 | } 146 | } 147 | 148 | function dismount() { 149 | const root = document.getElementById('push-react-app'); 150 | if (root) { 151 | ReactDOM.unmountComponentAtNode(root); 152 | } 153 | } 154 | 155 | export { render, dismount }; 156 | -------------------------------------------------------------------------------- /app/assets/javascripts/push-react/index.js: -------------------------------------------------------------------------------- 1 | import {ready as companion} from "push-react/companion"; 2 | import {ready, dismount} from "push-react/app"; 3 | 4 | $(document).ready(companion); 5 | $(document).ready(ready); 6 | $(document).on('page:load.push-react', ready); 7 | $(document).on('page:before-change.push-react', function() { 8 | dismount(); 9 | $(document).unbind('page:load.push-react'); 10 | }); 11 | -------------------------------------------------------------------------------- /app/assets/javascripts/push-react/manifest.json.erb: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Push React - Service Worker Rails Sandbox", 3 | "short_name": "push-react", 4 | "start_url": "./push-react/", 5 | "display": "standalone", 6 | "icons": [{ 7 | "src": "/assets/images/circle-turtle-192x192.png", 8 | "sizes": "192x192", 9 | "type": "image/png" 10 | }] 11 | } 12 | -------------------------------------------------------------------------------- /app/assets/javascripts/push-react/serviceworker.js: -------------------------------------------------------------------------------- 1 | import Logger from 'utils/logger'; 2 | const logger = new Logger('[push-react/serviceworker]'); 3 | 4 | logger.log("Hello!"); 5 | 6 | function onPush(event) { 7 | logger.log("Received push message", event); 8 | 9 | let title = (event.data && event.data.text()) || "Yay a message"; 10 | let body = "We have received a push message"; 11 | let tag = "push-react-demo-notification-tag"; 12 | var icon = '/assets/turtle-logo-120x120.png'; 13 | 14 | event.waitUntil( 15 | self.registration.showNotification(title, { body, icon, tag }) 16 | ) 17 | } 18 | 19 | self.addEventListener("push", onPush); 20 | -------------------------------------------------------------------------------- /app/assets/javascripts/push-simple/app.js: -------------------------------------------------------------------------------- 1 | import Logger from 'utils/logger'; 2 | const logger = new Logger('[push-simple/app]'); 3 | 4 | function ready() { 5 | setup(logSubscription); 6 | } 7 | 8 | function setup(onSubscribed) { 9 | logger.log('Setting up push subscription'); 10 | 11 | if (!window.PushManager) { 12 | logger.warn('Push messaging is not supported in your browser'); 13 | } 14 | 15 | if (!ServiceWorkerRegistration.prototype.showNotification) { 16 | logger.warn('Notifications are not supported in your browser'); 17 | return; 18 | } 19 | 20 | if (Notification.permission !== 'granted') { 21 | Notification.requestPermission(function (permission) { 22 | // If the user accepts, let's create a notification 23 | if (permission === "granted") { 24 | logger.log('Permission to receive notifications granted!'); 25 | subscribe(onSubscribed); 26 | } 27 | }); 28 | return; 29 | } else { 30 | logger.log('Permission to receive notifications granted!'); 31 | subscribe(onSubscribed); 32 | } 33 | } 34 | 35 | function subscribe(onSubscribed) { 36 | navigator.serviceWorker.ready.then((serviceWorkerRegistration) => { 37 | const pushManager = serviceWorkerRegistration.pushManager 38 | pushManager.getSubscription() 39 | .then((subscription) => { 40 | if (subscription) { 41 | refreshSubscription(pushManager, subscription, onSubscribed); 42 | } else { 43 | pushManagerSubscribe(pushManager, onSubscribed); 44 | } 45 | }) 46 | }); 47 | } 48 | 49 | function refreshSubscription(pushManager, subscription, onSubscribed) { 50 | logger.log('Refreshing subscription'); 51 | return subscription.unsubscribe().then((bool) => { 52 | pushManagerSubscribe(pushManager); 53 | }); 54 | } 55 | 56 | function pushManagerSubscribe(pushManager, onSubscribed) { 57 | logger.log('Subscribing started...'); 58 | 59 | pushManager.subscribe({ 60 | userVisibleOnly: true, 61 | applicationServerKey: window.publicKey 62 | }) 63 | .then(onSubscribed) 64 | .then(() => { logger.log('Subcribing finished: success!')}) 65 | .catch((e) => { 66 | if (Notification.permission === 'denied') { 67 | logger.warn('Permission to send notifications denied'); 68 | } else { 69 | logger.error('Unable to subscribe to push', e); 70 | } 71 | }); 72 | } 73 | 74 | function logSubscription(subscription) { 75 | logger.log("Current subscription", subscription.toJSON()); 76 | } 77 | 78 | function getSubscription() { 79 | return navigator.serviceWorker.ready 80 | .then((serviceWorkerRegistration) => { 81 | return serviceWorkerRegistration.pushManager.getSubscription() 82 | .catch((error) => { 83 | logger.warn('Error during getSubscription()', error); 84 | }); 85 | }); 86 | } 87 | 88 | function sendNotification() { 89 | getSubscription().then((subscription) => { 90 | return fetch("/push", { 91 | headers: formHeaders(), 92 | method: 'POST', 93 | credentials: 'include', 94 | body: JSON.stringify({ subscription: subscription.toJSON() }) 95 | }).then((response) => { 96 | logger.log("Push response", response); 97 | if (response.status >= 500) { 98 | logger.error(response.statusText); 99 | alert("Sorry, there was a problem sending the notification. Try resubscribing to push messages and resending."); 100 | } 101 | }) 102 | .catch((e) => { 103 | logger.error("Error sending notification", e); 104 | }); 105 | }) 106 | } 107 | 108 | function formHeaders() { 109 | return new Headers({ 110 | 'Content-Type': 'application/json', 111 | 'X-Requested-With': 'XMLHttpRequest', 112 | 'X-CSRF-Token': authenticityToken(), 113 | }); 114 | } 115 | 116 | function authenticityToken() { 117 | return document.querySelector('meta[name=csrf-token]').content; 118 | } 119 | 120 | export { 121 | ready, 122 | sendNotification, 123 | }; 124 | -------------------------------------------------------------------------------- /app/assets/javascripts/push-simple/companion.js: -------------------------------------------------------------------------------- 1 | import Logger from 'utils/logger'; 2 | import {alertSWSupport} from 'utils/alertonce'; 3 | const logger = new Logger('[push-simple/client]'); 4 | 5 | function ready() { 6 | if (navigator.serviceWorker) { 7 | logger.log('Registering serviceworker'); 8 | navigator.serviceWorker.register('/push-simple/serviceworker.js') 9 | .then(function(reg) { 10 | logger.log(reg.scope, 'register'); 11 | logger.log('Service worker change, registered the service worker'); 12 | }); 13 | } else { 14 | alertSWSupport(); 15 | } 16 | } 17 | 18 | export { ready }; 19 | -------------------------------------------------------------------------------- /app/assets/javascripts/push-simple/index.js: -------------------------------------------------------------------------------- 1 | import {ready as companion} from "push-simple/companion"; 2 | import {ready, sendNotification} from "push-simple/app"; 3 | 4 | $(document).ready(companion); 5 | $(document).ready(ready); 6 | $(document).on('page:load.push-simple', ready); 7 | $(document).on('page:before-change.push-simple', function() { 8 | $(document).unbind('page:load.push-simple'); 9 | }); 10 | 11 | $(document).on('page:send-notification.push-simple', sendNotification); 12 | $('.send-notification-button').on('click', sendNotification); 13 | 14 | console.log("Push simple"); 15 | -------------------------------------------------------------------------------- /app/assets/javascripts/push-simple/manifest.json.erb: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Push Simple - Service Worker Rails Sandbox", 3 | "short_name": "push-simple", 4 | "start_url": "./push-simple/", 5 | "display": "standalone", 6 | "icons": [{ 7 | "src": "/assets/images/circle-turtle-192x192.png", 8 | "sizes": "192x192", 9 | "type": "image/png" 10 | }] 11 | } 12 | -------------------------------------------------------------------------------- /app/assets/javascripts/push-simple/serviceworker.js: -------------------------------------------------------------------------------- 1 | import Logger from 'utils/logger'; 2 | const logger = new Logger('[push-simple/serviceworker]'); 3 | 4 | function onPush(event) { 5 | logger.log("Received push message", event); 6 | 7 | let title = (event.data && event.data.text()) || "Yay a message"; 8 | let body = "We have received a push message"; 9 | let tag = "push-simple-demo-notification-tag"; 10 | var icon = '/assets/turtle-logo-120x120.png'; 11 | 12 | event.waitUntil( 13 | self.registration.showNotification(title, { body, icon, tag }) 14 | ) 15 | } 16 | 17 | function onPushSubscriptionChange(event) { 18 | logger.log("Push subscription change event detected", event); 19 | } 20 | 21 | self.addEventListener("push", onPush); 22 | self.addEventListener("pushsubscriptionchange", onPushSubscriptionChange); 23 | -------------------------------------------------------------------------------- /app/assets/javascripts/utils/alertonce.js: -------------------------------------------------------------------------------- 1 | function alertOnce(labelSuffix, message) { 2 | if (!localStorage) { 3 | alert(message); 4 | return false; 5 | } 6 | 7 | const label = `alerted-${labelSuffix}`; 8 | const alerted = localStorage.getItem(label) || ''; 9 | 10 | if (alerted != "yes") { 11 | localStorage.setItem(label, 'yes'); 12 | alert(message); 13 | } 14 | 15 | return true; 16 | } 17 | 18 | function alertSWSupport() { 19 | return alertOnce("serviceWorker", "Sorry but the browser you're using does not support Service Workers yet! Check out caniuse.com to learn moreabout browser support"); 20 | } 21 | 22 | export { 23 | alertOnce, 24 | alertSWSupport 25 | } 26 | -------------------------------------------------------------------------------- /app/assets/javascripts/utils/caching.js: -------------------------------------------------------------------------------- 1 | const version = 'v05022016-1'; 2 | 3 | function cacheKey() { 4 | return [version, ...arguments].join(':'); 5 | } 6 | 7 | export { 8 | cacheKey, 9 | version 10 | } 11 | -------------------------------------------------------------------------------- /app/assets/javascripts/utils/logger.js: -------------------------------------------------------------------------------- 1 | export default class Logger { 2 | constructor(label) { 3 | this.label = label; 4 | } 5 | 6 | log() { 7 | console.log(this.label, ...arguments); 8 | } 9 | 10 | error() { 11 | console.error(this.label, ...arguments); 12 | } 13 | 14 | warn() { 15 | console.warn(this.label, ...arguments); 16 | } 17 | } 18 | 19 | -------------------------------------------------------------------------------- /app/assets/stylesheets/_settings.scss: -------------------------------------------------------------------------------- 1 | $subtleColor: #666; 2 | $primaryColor: #1cb495; 3 | $linkColor: $primaryColor; 4 | $primaryHoverColor: #1fcaa7; 5 | 6 | $mobileWidth: 100%; 7 | $tabletWidth: 60%; 8 | $desktopWidth: 40%; 9 | 10 | $color0: #1f1f1f; 11 | $color1: #594f4f; 12 | $color2: #547980; 13 | $color3: #45ada8; 14 | $color4: #9de0ad; 15 | $color5: #e5fcc2; 16 | 17 | @mixin clearfix() { 18 | overflow: hidden; 19 | &:before, 20 | &:after { 21 | content: ""; 22 | display: table; 23 | } 24 | &:after { 25 | clear: both; 26 | } 27 | &{ 28 | *zoom: 1; 29 | } 30 | } 31 | 32 | // @mixin font-smoothing($value: antialiased) { 33 | // @if $value == antialiased { 34 | // -webkit-font-smoothing: antialiased; 35 | // -moz-osx-font-smoothing: grayscale; 36 | // } 37 | // @else { 38 | // -webkit-font-smoothing: subpixel-antialiased; 39 | // -moz-osx-font-smoothing: auto; 40 | // } 41 | // } 42 | 43 | $breakpoints: (mobile: 350px, 44 | phablet: 550px, 45 | tablet: 750px, 46 | desktop: 1000px, 47 | hd: 1200px) !default; 48 | 49 | // /* Larger than mobile */ 50 | // @media (min-width: 400px) {} 51 | @mixin mobile { 52 | @media (min-width: #{map-get($breakpoints, mobile)}) { 53 | @content; 54 | } 55 | } 56 | 57 | // /* Larger than phablet (also point when grid becomes active) */ 58 | // @media (min-width: 550px) {} 59 | @mixin phablet { 60 | @media (min-width: #{map-get($breakpoints, phablet)}) { 61 | @content; 62 | } 63 | } 64 | 65 | // /* Larger than tablet */ 66 | // @media (min-width: 750px) {} 67 | @mixin tablet { 68 | @media (min-width: #{map-get($breakpoints, tablet)}) { 69 | @content; 70 | } 71 | } 72 | 73 | // /* Larger than desktop */ 74 | // @media (min-width: 1000px) {} 75 | @mixin desktop { 76 | @media (min-width: #{map-get($breakpoints, desktop)}) { 77 | @content; 78 | } 79 | } 80 | 81 | // /* Larger than Desktop HD */ 82 | // @media (min-width: 1200px) {} 83 | @mixin hd { 84 | @media (min-width: #{map-get($breakpoints, hd)}) { 85 | @content; 86 | } 87 | } 88 | 89 | @mixin bg-cloudy { 90 | background-color: #f5f3f6; 91 | color: #000; 92 | h1, h2, h3, h4, h5, h6, strong { 93 | color: #000; 94 | } 95 | } 96 | 97 | @mixin bg-gray { 98 | background-color: $color0; 99 | color: #FFF; 100 | h1, h2, h3, h4, h5, h6, strong { 101 | color: #FFF; 102 | } 103 | } 104 | 105 | @mixin bg-white { 106 | background-color: #fff; 107 | color: #000; 108 | h1, h2, h3, h4, h5, h6, strong { 109 | color: #000; 110 | } 111 | } 112 | 113 | @mixin content-section { 114 | padding: 1.5em 0; 115 | h1, h2, h3, h4, h5, h6 { 116 | margin-bottom: 0.75em; 117 | } 118 | 119 | p { 120 | font-size: 1.25em; 121 | line-height: 1.5em; 122 | } 123 | 124 | @include tablet { 125 | padding: 3em 0; 126 | } 127 | 128 | @include desktop { 129 | padding: 4em 0; 130 | h1, h2, h3, h4, h5, h6 { 131 | margin-bottom: 1em; 132 | } 133 | } 134 | } 135 | 136 | @mixin media-container { 137 | @include clearfix; 138 | padding: 0; 139 | list-style: none; 140 | } 141 | 142 | @mixin media-item($size: 70px) { 143 | @include clearfix; 144 | float: left; 145 | overflow:hidden; 146 | 147 | zoom:1; 148 | 149 | .bd { 150 | overflow:hidden; 151 | zoom:1; 152 | } 153 | 154 | img { 155 | width: $size; 156 | height: $size; 157 | float:left; 158 | margin-right: $size/5.5; 159 | display:block; 160 | } 161 | } 162 | 163 | @mixin callout { 164 | background-color: ghostwhite; 165 | padding: 2em; 166 | margin: 0 -2em; 167 | } 168 | -------------------------------------------------------------------------------- /app/assets/stylesheets/application.scss: -------------------------------------------------------------------------------- 1 | @import "materialize/components/color"; 2 | $primary-color: color("deep-purple", "lighten-2") !default; 3 | $secondary-color: color("light-green", "base") !default; 4 | @import 'materialize'; 5 | @import "layout"; 6 | @import "objects/index"; 7 | -------------------------------------------------------------------------------- /app/assets/stylesheets/layout.scss: -------------------------------------------------------------------------------- 1 | @import "settings"; 2 | 3 | footer { 4 | padding: 0 2em; 5 | } 6 | 7 | .not-visible { 8 | visibility: "hidden"; 9 | } 10 | 11 | .side-nav { 12 | .logo { 13 | text-align: center; 14 | img { 15 | width: 100px; 16 | } 17 | } 18 | } 19 | 20 | // Material Design overrides 21 | 22 | main, header, footer { 23 | padding-left: 300px; 24 | } 25 | 26 | @media only screen and (min-width: 993px) { 27 | .container { 28 | width: 85%; 29 | } 30 | } 31 | 32 | @media only screen and (max-width : 992px) { 33 | header, main, footer { 34 | padding-left: 0; 35 | } 36 | } 37 | 38 | a.button-collapse.top-nav { 39 | position: absolute; 40 | text-align: center; 41 | height: 48px; 42 | width: 48px; 43 | left: -10px; 44 | top: 0; 45 | float: none; 46 | margin-left: 1.5rem; 47 | color: #fff; 48 | font-size: 36px; 49 | z-index: 2; 50 | } 51 | -------------------------------------------------------------------------------- /app/assets/stylesheets/materialze.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rossta/serviceworker-rails-sandbox/3d16b9e7475e56045f97908a25029aa309ed1dea/app/assets/stylesheets/materialze.css -------------------------------------------------------------------------------- /app/assets/stylesheets/objects/buttons.scss: -------------------------------------------------------------------------------- 1 | .button { 2 | &.disabled { 3 | color: #CCC; 4 | cursor: none; 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /app/assets/stylesheets/objects/callout.scss: -------------------------------------------------------------------------------- 1 | @import "settings"; 2 | 3 | .callout { 4 | @include callout; 5 | } 6 | -------------------------------------------------------------------------------- /app/assets/stylesheets/objects/footer.scss: -------------------------------------------------------------------------------- 1 | footer { 2 | .content { 3 | margin-top: 2em; 4 | padding: 2em 0; 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /app/assets/stylesheets/objects/index.scss: -------------------------------------------------------------------------------- 1 | @import "objects/callout"; 2 | @import "objects/footer"; 3 | @import "objects/media"; 4 | @import "objects/navbar"; 5 | @import "objects/status"; 6 | @import "objects/toggle"; 7 | @import "objects/tweet"; 8 | -------------------------------------------------------------------------------- /app/assets/stylesheets/objects/media.scss: -------------------------------------------------------------------------------- 1 | @import "settings"; 2 | 3 | .media-container { 4 | @include media-container; 5 | } 6 | 7 | .media-item { 8 | @include media-item; 9 | } 10 | -------------------------------------------------------------------------------- /app/assets/stylesheets/objects/navbar.scss: -------------------------------------------------------------------------------- 1 | @import "settings"; 2 | 3 | .nav-container { 4 | min-height: 70px; } 5 | 6 | .navbar-link { 7 | text-transform: uppercase; 8 | font-size: 1em; } 9 | 10 | /* Larger than tablet */ 11 | @media (min-width: 750px) { 12 | /* Navbar */ 13 | .navbar + .docs-section { 14 | border-top-width: 0; } 15 | .navbar, 16 | .navbar-spacer { 17 | display: block; 18 | width: 100%; 19 | height: 6.5rem; 20 | background: #fff; 21 | z-index: 99; 22 | border-top: 1px solid #eee; 23 | border-bottom: 1px solid #eee; } 24 | .navbar-spacer { 25 | display: none; } 26 | .navbar > .container { 27 | width: 100%; } 28 | .navbar-list { 29 | list-style: none; 30 | margin-bottom: 0; } 31 | .navbar-item { 32 | position: relative; 33 | float: left; 34 | margin-bottom: 0; } 35 | .navbar-link { 36 | text-transform: uppercase; 37 | font-size: .9em; 38 | font-weight: 600; 39 | letter-spacing: .2rem; 40 | margin-right: 35px; 41 | text-decoration: none; 42 | line-height: 6.5rem; 43 | color: #222; } 44 | .navbar-link.active { 45 | color: #33C3F0; } 46 | .has-docked-nav .navbar { 47 | position: fixed; 48 | top: 0; 49 | left: 0; } 50 | .has-docked-nav .navbar-spacer { 51 | display: block; } 52 | /* Re-overiding the width 100% declaration to match size of % based container */ 53 | .has-docked-nav .navbar > .container { 54 | width: $mobileWidth; 55 | 56 | /* For devices larger than 400px */ 57 | @media (min-width: 400px) { 58 | width: $tabletWidth; } 59 | 60 | /* For devices larger than 550px */ 61 | @media (min-width: 550px) { 62 | width: $desktopWidth; } 63 | } 64 | 65 | /* Popover */ 66 | .popover.open { 67 | display: block; 68 | z-index: 1; 69 | } 70 | .popover { 71 | display: none; 72 | position: absolute; 73 | top: 0; 74 | left: 0; 75 | background: #fff; 76 | border: 1px solid #eee; 77 | border-radius: 4px; 78 | top: 92%; 79 | left: -50%; 80 | -webkit-filter: drop-shadow(0 0 6px rgba(0,0,0,.1)); 81 | -moz-filter: drop-shadow(0 0 6px rgba(0,0,0,.1)); 82 | filter: drop-shadow(0 0 6px rgba(0,0,0,.1)); } 83 | .popover-item:first-child .popover-link:after, 84 | .popover-item:first-child .popover-link:before { 85 | bottom: 100%; 86 | left: 50%; 87 | border: solid transparent; 88 | content: " "; 89 | height: 0; 90 | width: 0; 91 | position: absolute; 92 | pointer-events: none; } 93 | .popover-item:first-child .popover-link:after { 94 | border-color: rgba(255, 255, 255, 0); 95 | border-bottom-color: #fff; 96 | border-width: 10px; 97 | margin-left: -10px; } 98 | .popover-item:first-child .popover-link:before { 99 | border-color: rgba(238, 238, 238, 0); 100 | border-bottom-color: #eee; 101 | border-width: 11px; 102 | margin-left: -11px; } 103 | .popover-list { 104 | padding: 0; 105 | margin: 0; 106 | list-style: none; } 107 | .popover-item { 108 | padding: 0; 109 | margin: 0; } 110 | .popover-link { 111 | position: relative; 112 | color: #222; 113 | display: block; 114 | padding: 8px 20px; 115 | border-bottom: 1px solid #eee; 116 | text-decoration: none; 117 | text-transform: uppercase; 118 | font-size: 1.0rem; 119 | font-weight: 600; 120 | text-align: center; 121 | letter-spacing: .1rem; } 122 | .popover-item:first-child .popover-link { 123 | border-radius: 4px 4px 0 0; } 124 | .popover-item:last-child .popover-link { 125 | border-radius: 0 0 4px 4px; 126 | border-bottom-width: 0; } 127 | .popover-link:hover { 128 | color: #fff; 129 | background: #33C3F0; } 130 | .popover-link:hover, 131 | .popover-item:first-child .popover-link:hover:after { 132 | border-bottom-color: #33C3F0; } 133 | } 134 | 135 | #demosNavPopover { 136 | width: 200px; 137 | } 138 | -------------------------------------------------------------------------------- /app/assets/stylesheets/objects/status.scss: -------------------------------------------------------------------------------- 1 | .status { 2 | background: #c1f5d1; 3 | padding: 0.5em 1em; 4 | 5 | .refresh { 6 | float: right; 7 | } 8 | 9 | &.cache { 10 | background: #f7efb7; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /app/assets/stylesheets/objects/toggle.scss: -------------------------------------------------------------------------------- 1 | // From http://callmenick.com/post/css-toggle-switch-examples 2 | 3 | $width: 40px; 4 | $radius: $width / 2; 5 | 6 | $gutter: 2px; 7 | $inner-radius: $radius - ($gutter * 2) - 1; 8 | 9 | $color-disabled: #dddddd; 10 | $color-enabled: #8ce196; 11 | 12 | .cmn-toggle { 13 | position: absolute; 14 | margin-left: -9999px; 15 | visibility: hidden; 16 | + label { 17 | display: block; 18 | position: relative; 19 | cursor: pointer; 20 | outline: none; 21 | user-select: none; 22 | } 23 | } 24 | 25 | input { 26 | &.cmn-toggle-round { 27 | + label { 28 | padding: $gutter; 29 | width: $width; 30 | height: $radius; 31 | background-color: $color-disabled; 32 | border-radius: $radius; 33 | 34 | &:before, 35 | &:after { 36 | display: block; 37 | position: absolute; 38 | top: 1px; 39 | left: 1px; 40 | bottom: 1px; 41 | content: ""; 42 | } 43 | 44 | &:before { 45 | right: 1px; 46 | background-color: #f1f1f1; 47 | border-radius: $radius; 48 | transition: background 0.4s; 49 | } 50 | &:after { 51 | width: $radius - $gutter; 52 | background-color: #fff; 53 | border-radius: 100%; 54 | box-shadow: 0 2px 5px rgba(0, 0, 0, 0.3); 55 | transition: margin 0.4s; 56 | } 57 | } 58 | 59 | &:checked + label:before { 60 | background-color: $color-enabled; 61 | } 62 | &:checked + label:after { 63 | margin-left: $radius; 64 | } 65 | } 66 | 67 | &.cmn-toggle-round-flat { 68 | + label { 69 | padding: $gutter; 70 | width: $width; 71 | height: $radius; 72 | background-color: $color-disabled; 73 | border-radius: $radius; 74 | transition: background 0.4s; 75 | 76 | &:before, 77 | &:after { 78 | display: block; 79 | position: absolute; 80 | content: ""; 81 | } 82 | &:before { 83 | top: $gutter; 84 | left: $gutter; 85 | bottom: $gutter; 86 | right: $gutter; 87 | background-color: #fff; 88 | border-radius: $radius; 89 | transition: background 0.4s; 90 | } 91 | &:after { 92 | top: $gutter * 2; 93 | left: $gutter * 2; 94 | bottom: $gutter * 2; 95 | width: $inner-radius; 96 | background-color: $color-disabled; 97 | border-radius: $inner-radius; 98 | transition: margin 0.4s, background 0.4s; 99 | } 100 | } 101 | 102 | &:checked + label { 103 | background-color: $color-enabled; 104 | } 105 | &:checked + label:after { 106 | margin-left: $radius; 107 | background-color: $color-enabled; 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /app/assets/stylesheets/objects/tweet.scss: -------------------------------------------------------------------------------- 1 | @import "settings"; 2 | 3 | .tweet { 4 | @include callout; 5 | @include clearfix; 6 | 7 | margin-bottom: 1em; 8 | background-color: #f8f8ff; 9 | 10 | .media-item { 11 | margin: 0 0 1em; 12 | } 13 | 14 | .media-extras { 15 | padding: 0 0 1em; 16 | } 17 | 18 | .tweet-img { 19 | width: 100%; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/clients/twitter_client.rb: -------------------------------------------------------------------------------- 1 | class TwitterClient 2 | def client 3 | @client ||= Twitter::REST::Client.new do |config| 4 | config.consumer_key = ENV.fetch("TWITTER_CONSUMER_KEY") 5 | config.consumer_secret = ENV.fetch("TWITTER_CONSUMER_SECRET") 6 | config.access_token = ENV.fetch("TWITTER_ACCESS_TOKEN") 7 | config.access_token_secret = ENV.fetch("TWITTER_ACCESS_SECRET") 8 | end 9 | end 10 | 11 | def search(term) 12 | client.search(term) 13 | end 14 | 15 | def timeline(*args) 16 | return enum_for(:timeline) unless block_given? 17 | 18 | client.home_timeline.each do |tweet| 19 | yield TwitterClient::Tweet.new(tweet) 20 | end 21 | end 22 | 23 | def method_missing(method, *args) 24 | if client.respond_to?(method) 25 | client.send(method, *args) 26 | else 27 | super 28 | end 29 | end 30 | 31 | class Tweet < SimpleDelegator 32 | def body; text; end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /app/clients/webpush_client.rb: -------------------------------------------------------------------------------- 1 | class WebpushClient 2 | def self.public_key 3 | ENV.fetch('VAPID_PUBLIC_KEY') 4 | end 5 | 6 | def self.public_key_bytes 7 | Base64.urlsafe_decode64(public_key).bytes 8 | end 9 | 10 | def self.private_key 11 | ENV.fetch('VAPID_PRIVATE_KEY') 12 | end 13 | 14 | # Send webpush message using subscription parameters 15 | # 16 | # @param message [String] text to encrypt 17 | # @param subscription_params [Hash] 18 | # @option subscription_params [String] :endpoint url to send encrypted message 19 | # @option subscription_params [Hash] :keys auth keys to send with message for decryption 20 | # @return true/false 21 | def send_notification(message, endpoint: "", p256dh: "", auth: "") 22 | raise ArgumentError, ":endpoint param is required" if endpoint.blank? 23 | raise ArgumentError, "subscription :keys are missing" if p256dh.blank? || auth.blank? 24 | 25 | Rails.logger.info("Sending WebPush notification...............") 26 | Rails.logger.info("message: #{message}") 27 | Rails.logger.info("endpoint: #{endpoint}") 28 | Rails.logger.info("p256dh: #{p256dh}") 29 | Rails.logger.info("auth: #{auth}") 30 | 31 | Webpush.payload_send \ 32 | message: message, 33 | endpoint: endpoint, 34 | p256dh: p256dh, 35 | auth: auth, 36 | vapid: { 37 | subject: "mailto:ross@rossta.net", 38 | public_key: public_key, 39 | private_key: private_key 40 | } 41 | end 42 | 43 | def public_key 44 | self.class.public_key 45 | end 46 | 47 | def private_key 48 | self.class.private_key 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | # Prevent CSRF attacks by raising an exception. 3 | # For APIs, you may want to use :null_session instead. 4 | protect_from_forgery with: :exception 5 | 6 | PushNotificationError = Class.new(StandardError) 7 | end 8 | -------------------------------------------------------------------------------- /app/controllers/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rossta/serviceworker-rails-sandbox/3d16b9e7475e56045f97908a25029aa309ed1dea/app/controllers/concerns/.keep -------------------------------------------------------------------------------- /app/controllers/pages_controller.rb: -------------------------------------------------------------------------------- 1 | class PagesController < ApplicationController 2 | include HighVoltage::StaticPage 3 | 4 | before_filter :ensure_trailing_slash 5 | 6 | def ensure_trailing_slash 7 | redirect_to url_for(params.merge(trailing_slash: true)) unless skip_redirect? 8 | end 9 | 10 | def skip_redirect? 11 | trailing_slash? || asset? 12 | end 13 | 14 | def asset? 15 | request.env['REQUEST_URI'].split(".").last =~ %r{^(js|css|json)$} 16 | end 17 | 18 | def trailing_slash? 19 | request.env['REQUEST_URI'].match(/[^\?]+/).to_s.last == '/' 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /app/controllers/push_notifications_controller.rb: -------------------------------------------------------------------------------- 1 | class PushNotificationsController < ApplicationController 2 | def create 3 | Rails.logger.info "Sending push notification from #{push_params.inspect}" 4 | subscription_params = fetch_subscription_params 5 | 6 | WebpushJob.perform_later fetch_message, 7 | endpoint: subscription_params[:endpoint], 8 | p256dh: subscription_params.dig(:keys, :p256dh), 9 | auth: subscription_params.dig(:keys, :auth) 10 | 11 | head :ok 12 | end 13 | 14 | private 15 | 16 | def push_params 17 | params.permit(:message, { subscription: [:endpoint, keys: [:auth, :p256dh]]}) 18 | end 19 | 20 | def fetch_message 21 | push_params.fetch(:message, "Yay, you sent a push notification!") 22 | end 23 | 24 | def fetch_subscription_params 25 | @subscription_params ||= push_params.fetch(:subscription) { extract_session_subscription } 26 | end 27 | 28 | def extract_session_subscription 29 | subscription = session.fetch(:subscription) { raise PushNotificationError, 30 | "Cannot create notification: no :subscription in params or session" } 31 | 32 | JSON.parse(subscription).with_indifferent_access 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /app/controllers/streams_controller.rb: -------------------------------------------------------------------------------- 1 | class StreamsController < ApplicationController 2 | QUERIES = { 3 | cats: "#cats filter:images -RT" 4 | }.with_indifferent_access 5 | 6 | def index 7 | @query = search_query 8 | 9 | respond_to do |format| 10 | format.html 11 | format.json { render json: tweets(@query) } 12 | end 13 | end 14 | 15 | def show 16 | @query = QUERIES.fetch(params[:id]) { raise ActiveRecord::RecordNotFound } 17 | 18 | respond_to do |format| 19 | format.html 20 | format.json { render json: tweets(@query) } 21 | end 22 | end 23 | 24 | private 25 | 26 | def client 27 | @client ||= TwitterClient.new 28 | end 29 | 30 | def search_query 31 | params.fetch(:q, QUERIES[:cats]) 32 | end 33 | 34 | def tweets(query) 35 | client.search(query).take(25).map { |t| Tweet.new(t) } 36 | end 37 | helper_method :tweets 38 | 39 | end 40 | -------------------------------------------------------------------------------- /app/controllers/subscriptions_controller.rb: -------------------------------------------------------------------------------- 1 | class SubscriptionsController < ApplicationController 2 | def create 3 | session[:subscription] = params.fetch(:subscription).to_json 4 | 5 | head :ok 6 | end 7 | 8 | def destroy 9 | session.delete(:subscription) 10 | 11 | head :ok 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /app/controllers/welcome_controller.rb: -------------------------------------------------------------------------------- 1 | class WelcomeController < ApplicationController 2 | def index 3 | end 4 | 5 | def offline 6 | render layout: 'offline' 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/meta_helper.rb: -------------------------------------------------------------------------------- 1 | module MetaHelper 2 | def metatag(attrs) 3 | "" 4 | end 5 | 6 | def site_metatags 7 | Rails.cache.fetch('site_metatags') do 8 | site_metadata.map do |attrs| 9 | metatag(attrs) 10 | end.join("\n").html_safe 11 | end 12 | end 13 | 14 | def site_metadata 15 | [ 16 | { name: "description", content: t('meta.description') }, 17 | { name: "keywords", content: t('meta.keywords') }, 18 | { name: "twitter:card", content: "summary" }, 19 | { name: "twitter:creator", content: t('meta.author') }, 20 | { name: "twitter:description", content: t('meta.description') }, 21 | { name: "twitter:image:src", content: image_path(t('meta.image')) }, 22 | { name: "twitter:title", content: t('meta.title') }, 23 | { property: "og:site_name", content: t('meta.site_name') }, 24 | { property: "og:description", content: t('meta.description') }, 25 | { property: "og:image", content: image_path(t('meta.image')) }, 26 | { property: "og:title", content: t('meta.title') }, 27 | { charset: "utf-8" }, 28 | { "http-equiv" => 'X-UA-Compatible', content: 'IE: edge;chrome: 1' }, 29 | { name: "viewport", content: "width=device-width, initial-scale=1, maximum-scale=1"}, 30 | { name: "mobile-web-app-capable", content: "yes" } 31 | ] 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /app/helpers/nav_helper.rb: -------------------------------------------------------------------------------- 1 | module NavHelper 2 | def demo_navbar 3 | nav_pages t('nav').values.flatten 4 | end 5 | 6 | def demo_index 7 | t('nav').map { |sect, titles| [sect.to_s.titleize, nav_pages(titles)] } 8 | end 9 | 10 | def nav_pages(titles) 11 | titles.map { |t| [t, t.parameterize] } 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /app/helpers/settings_helper.rb: -------------------------------------------------------------------------------- 1 | module SettingsHelper 2 | def settings 3 | Rails.configuration.settings 4 | end 5 | 6 | def google_analytics_tracking_id 7 | settings.google_analytics_tracking_id 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /app/helpers/streams_helper.rb: -------------------------------------------------------------------------------- 1 | module StreamsHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/twitter_helper.rb: -------------------------------------------------------------------------------- 1 | require "addressable/uri" 2 | 3 | module TwitterHelper 4 | def tweet_link_to(text, params = {}) 5 | uri = Addressable::URI.parse("https://twitter.com/intent/tweet") 6 | uri.query_values = params.reverse_merge hashtags: "serviceworker-rails,serviceworker" 7 | link_to text, uri.to_s, target: "_blank" 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /app/jobs/webpush_job.rb: -------------------------------------------------------------------------------- 1 | class WebpushJob < ActiveJob::Base 2 | queue_as :default 3 | 4 | def perform(message, params) 5 | client = WebpushClient.new 6 | 7 | log("sending #{message} to #{params[:endpoint]}") 8 | response = client.send_notification(message, params) 9 | log(response ? "success" : "failed") 10 | log(response.body.inspect) 11 | end 12 | 13 | def log(message) 14 | Rails.logger.info("[WebpushClient] #{message}") 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /app/mailers/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rossta/serviceworker-rails-sandbox/3d16b9e7475e56045f97908a25029aa309ed1dea/app/mailers/.keep -------------------------------------------------------------------------------- /app/models/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rossta/serviceworker-rails-sandbox/3d16b9e7475e56045f97908a25029aa309ed1dea/app/models/.keep -------------------------------------------------------------------------------- /app/models/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rossta/serviceworker-rails-sandbox/3d16b9e7475e56045f97908a25029aa309ed1dea/app/models/concerns/.keep -------------------------------------------------------------------------------- /app/models/tweet.rb: -------------------------------------------------------------------------------- 1 | class Tweet < SimpleDelegator 2 | extend ActiveModel::Naming 3 | include ActiveModel::Conversion 4 | 5 | def persisted? 6 | true 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /app/views/layouts/_analytics.html.erb: -------------------------------------------------------------------------------- 1 | 10 | -------------------------------------------------------------------------------- /app/views/layouts/_footer.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Have a question? Found a bug? <%= tweet_link_to "Tweet at #serviceworkerrails" %> or <%= link_to "report it on GitHub", "https://github.com/rossta/serviceworker-rails-sandbox/issues" %>

4 |

<%= Date.today.to_s(:short) %>

5 |
6 |
7 | -------------------------------------------------------------------------------- /app/views/layouts/_navbar.html.erb: -------------------------------------------------------------------------------- 1 | 17 | -------------------------------------------------------------------------------- /app/views/layouts/_sidenav.html.erb: -------------------------------------------------------------------------------- 1 |
2 | menu 3 |
4 | 31 | -------------------------------------------------------------------------------- /app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Service Worker on Rails 5 | <%= site_metatags %> 6 | 7 | <%= stylesheet_link_tag 'application', media: 'all' %> 8 | 9 | <%= javascript_include_tag 'application' %> 10 | <%= csrf_meta_tags %> 11 | 12 | 13 | 14 | <%= yield :head %> 15 | 16 | 17 |
18 | <%= render "layouts/navbar" %> 19 | 20 | <%= render "layouts/sidenav" %> 21 | 22 |
23 |
24 |

<%= notice %>

25 |

<%= alert %>

26 |
27 |
28 |
29 |
30 |
31 | <%= yield %> 32 |
33 |
34 |
35 |
36 | <%= render "layouts/footer" %> 37 | <%= render "layouts/analytics" %> 38 | 39 | 40 | -------------------------------------------------------------------------------- /app/views/layouts/offline.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Service Workers on Rails: Offline 5 | <%= stylesheet_link_tag 'application', media: 'all' %> 6 | 7 | 8 | 11 |
12 |

<%= notice %>

13 |

<%= alert %>

14 |
15 |
16 | <%= yield %> 17 |
18 |
19 |
20 | 21 | 22 | -------------------------------------------------------------------------------- /app/views/pages/cache-then-network.html.erb: -------------------------------------------------------------------------------- 1 |

Cache then Network

2 | 3 |

4 | Objective: To display cached content while content is fetched from the network. 5 |

6 | 7 |

Once the service worker is installed, it will cache async requests for tweet data. Refresh the page and the cached results will render immediately on page load, eventually updating when and if the network response retrieves new results.

8 | 9 |
10 | 11 | <%= javascript_include_tag 'cache-then-network' %> 12 | -------------------------------------------------------------------------------- /app/views/pages/offline-fallback.html.erb: -------------------------------------------------------------------------------- 1 |

Offline Fallback

2 | 3 |

4 | Objective: To display an offline page when the network can't connect. 5 |

6 | 7 |
8 |

9 | Switch off the network connection and <%= link_to "refresh this page", "?", id: "refresh" %> 10 | to see what it looks like "offline". (Hint: it involves cats). 11 |

12 |

13 | To work offline in Firefox, simply select File > Work Offline. 14 |

15 |

16 | In Chrome, you can simulate a disabled network using the Developer Tools. Under the Network tab, 17 | select Offline (0ms, 0kb/s, 0kb/s). 18 |

19 |

20 | <%= image_tag 'screenshots/developer_tools_network_offline_select.jpg', width: '100%' %> 21 |

22 |
23 | 24 | <%= javascript_include_tag 'offline-fallback' %> 25 | -------------------------------------------------------------------------------- /app/views/pages/push-react.html.erb: -------------------------------------------------------------------------------- 1 |

Push React

2 | 3 |

4 | Objective: Demo of push button toggle to subscribe/unsubscribe from push notifications through Service Worker. Button controls are rendered using React. 5 |

6 | 7 |
8 |
9 |
10 | 11 | 14 | <%= javascript_include_tag 'push-react' %> 15 | -------------------------------------------------------------------------------- /app/views/pages/push-simple.html.erb: -------------------------------------------------------------------------------- 1 |

Push Simple

2 | 3 |

4 | Objective: Simple example of the Web Push API through Service Worker. Send notifications to users even when the page is not open. 5 |

6 | 7 |
8 | 11 |
12 | 13 | 16 | <%= javascript_include_tag 'push-simple' %> 17 | -------------------------------------------------------------------------------- /app/views/streams/_stream.html.erb: -------------------------------------------------------------------------------- 1 | <% cache_if(query, ['tweets', query.parameterize], expires_in: 5.minutes) do %> 2 | <%= render tweets(query) %> 3 | <% end %> 4 | -------------------------------------------------------------------------------- /app/views/streams/index.html.erb: -------------------------------------------------------------------------------- 1 | <%= form_tag "", id: "search", method: :get do %> 2 | <%= text_field_tag :q %> 3 | <%= submit_tag "Search" %> 4 | <% end %> 5 | <%= render "stream", query: @query %> 6 | -------------------------------------------------------------------------------- /app/views/streams/show.html.erb: -------------------------------------------------------------------------------- 1 | <%= render "stream", query: @query %> 2 | -------------------------------------------------------------------------------- /app/views/tweets/_tweet.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 | <%= image_tag tweet.user.profile_image_url_https %> 4 |
5 |
<%= tweet.text %>
6 |
7 |
8 |
9 | <% tweet.media.each do |img| %> 10 | <%= image_tag img.media_url_https, width: "100%" %> 11 | <% end %> 12 |
13 |
14 | -------------------------------------------------------------------------------- /app/views/welcome/index.html.erb: -------------------------------------------------------------------------------- 1 |

Service Worker on Rails

2 | 3 |

4 | The Service Worker Rails Sandbox is a collection of examples using Service Workers on Rails. The sandbox makes use of the 5 | <%= link_to "#" do %> 6 | serviceworker-rails 7 | <% end %> 8 | gem which makes it easier to integrate Service Worker scripts with the Rails asset pipeline. You can find the <%= link_to "source code of the sandbox", "#" %> and the 9 | <%= link_to "#" do %> 10 | serviceworker-rails 11 | <% end %> 12 | gem on GitHub. 13 |

14 | 15 | <% demo_index.each do |section, pages| %> 16 |

<%= section %>

17 |
    18 | <% pages.each do |title, path| %> 19 |
  • 20 | <%= link_to title, page_path(path) %> 21 |
  • 22 | <% end %> 23 |
24 | <% end %> 25 | 26 |

27 | On your mobile device, try adding the sandbox to your homescreen. The turtle icon is by Carla Dias from the Noun Project. 28 |

29 | 30 |

31 | Here are some <%= link_to 'Cats', stream_path('cats') %>. 32 |

33 | -------------------------------------------------------------------------------- /app/views/welcome/offline.html.erb: -------------------------------------------------------------------------------- 1 |

Offline

2 | 3 |

You're offline! There is currently no internet connection, but you can still seem some cats:

4 | 5 |
6 | <%= image_tag "pics/cash-cat.jpg" %> 7 |
8 | -------------------------------------------------------------------------------- /bin/bundle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 3 | load Gem.bin_path('bundler', 'bundle') 4 | -------------------------------------------------------------------------------- /bin/foreman: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | # 4 | # This file was generated by Bundler. 5 | # 6 | # The application 'foreman' is installed as part of a gem, and 7 | # this file is here to facilitate running it. 8 | # 9 | 10 | require "pathname" 11 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", 12 | Pathname.new(__FILE__).realpath) 13 | 14 | require "rubygems" 15 | require "bundler/setup" 16 | 17 | load Gem.bin_path("foreman", "foreman") 18 | -------------------------------------------------------------------------------- /bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | APP_PATH = File.expand_path('../config/application', __dir__) 3 | require_relative '../config/boot' 4 | require 'rails/commands' 5 | -------------------------------------------------------------------------------- /bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require_relative '../config/boot' 3 | require 'rake' 4 | Rake.application.run 5 | -------------------------------------------------------------------------------- /bin/rspec: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # 3 | # This file was generated by Bundler. 4 | # 5 | # The application 'rspec' is installed as part of a gem, and 6 | # this file is here to facilitate running it. 7 | # 8 | 9 | require "pathname" 10 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", 11 | Pathname.new(__FILE__).realpath) 12 | 13 | require "rubygems" 14 | require "bundler/setup" 15 | 16 | load Gem.bin_path("rspec-core", "rspec") 17 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'pathname' 3 | require 'fileutils' 4 | include FileUtils 5 | 6 | # path to your application root. 7 | APP_ROOT = Pathname.new File.expand_path('../../', __FILE__) 8 | 9 | def system!(*args) 10 | system(*args) || abort("\n== Command #{args} failed ==") 11 | end 12 | 13 | chdir APP_ROOT do 14 | # This script is a starting point to setup your application. 15 | # Add necessary setup steps to this file. 16 | 17 | puts '== Installing dependencies ==' 18 | system! 'gem install bundler --conservative' 19 | system('bundle check') || system!('bundle install') 20 | 21 | # puts "\n== Copying sample files ==" 22 | # unless File.exist?('config/database.yml') 23 | # cp 'config/database.yml.sample', 'config/database.yml' 24 | # end 25 | 26 | puts "\n== Preparing database ==" 27 | system! 'bin/rails db:setup' 28 | 29 | puts "\n== Removing old logs and tempfiles ==" 30 | system! 'bin/rails log:clear tmp:clear' 31 | 32 | puts "\n== Restarting application server ==" 33 | system! 'bin/rails restart' 34 | end 35 | -------------------------------------------------------------------------------- /bin/spring: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # This file loads spring without using Bundler, in order to be fast. 4 | # It gets overwritten when you run the `spring binstub` command. 5 | 6 | unless defined?(Spring) 7 | require 'rubygems' 8 | require 'bundler' 9 | 10 | if (match = Bundler.default_lockfile.read.match(/^GEM$.*?^ (?: )*spring \((.*?)\)$.*?^$/m)) 11 | Gem.paths = { 'GEM_PATH' => [Bundler.bundle_path.to_s, *Gem.path].uniq.join(Gem.path_separator) } 12 | gem 'spring', match[1] 13 | require 'spring/binstub' 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /bin/start: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | bundle check || bundle install 4 | 5 | rm ./tmp/pids/server.pid > /dev/null 2>&1 6 | ./bin/rails s -b 0.0.0.0 7 | -------------------------------------------------------------------------------- /bin/update: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'pathname' 3 | require 'fileutils' 4 | include FileUtils 5 | 6 | # path to your application root. 7 | APP_ROOT = Pathname.new File.expand_path('../../', __FILE__) 8 | 9 | def system!(*args) 10 | system(*args) || abort("\n== Command #{args} failed ==") 11 | end 12 | 13 | chdir APP_ROOT do 14 | # This script is a way to update your development environment automatically. 15 | # Add necessary update steps to this file. 16 | 17 | puts '== Installing dependencies ==' 18 | system! 'gem install bundler --conservative' 19 | system('bundle check') || system!('bundle install') 20 | 21 | puts "\n== Updating database ==" 22 | system! 'bin/rails db:migrate' 23 | 24 | puts "\n== Removing old logs and tempfiles ==" 25 | system! 'bin/rails log:clear tmp:clear' 26 | 27 | puts "\n== Restarting application server ==" 28 | system! 'bin/rails restart' 29 | end 30 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require ::File.expand_path('../config/environment', __FILE__) 4 | run Rails.application 5 | -------------------------------------------------------------------------------- /config/application.rb: -------------------------------------------------------------------------------- 1 | require_relative 'boot' 2 | 3 | require 'rails/all' 4 | 5 | # Require the gems listed in Gemfile, including any gems 6 | # you've limited to :test, :development, or :production. 7 | Bundler.require(*Rails.groups) 8 | 9 | module ServiceworkerRailsSandbox 10 | class Application < Rails::Application 11 | # We want to set up a custom logger which logs to STDOUT. 12 | # Docker expects your application to log to STDOUT/STDERR and to be ran 13 | # in the foreground. 14 | config.log_level = :debug 15 | config.log_tags = [:subdomain, :uuid] 16 | config.logger = ActiveSupport::TaggedLogging.new(ActiveSupport::Logger.new(STDOUT)) 17 | 18 | # Settings in config/environments/* take precedence over those specified here. 19 | # Application configuration should go into files in config/initializers 20 | # -- all .rb files in that directory are automatically loaded. 21 | 22 | # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. 23 | # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. 24 | # config.time_zone = 'Central Time (US & Canada)' 25 | 26 | # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. 27 | # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] 28 | # config.i18n.default_locale = :de 29 | 30 | # Do not swallow errors in after_commit/after_rollback callbacks. 31 | config.active_record.raise_in_transactional_callbacks = true 32 | 33 | # Ensure trailing slash to provide service worker scopes at path roots 34 | config.action_controller.default_url_options = { trailing_slash: true } 35 | 36 | # Set ActiveJob queue adapter 37 | config.active_job.queue_adapter = :sucker_punch 38 | 39 | # Custom settings 40 | config.settings = ActiveSupport::OrderedOptions.new 41 | config.settings.google_analytics_tracking_id = 'UA-xxxxxxxx-x' 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /config/boot.rb: -------------------------------------------------------------------------------- 1 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) 2 | 3 | require 'bundler/setup' # Set up gems listed in the Gemfile. 4 | -------------------------------------------------------------------------------- /config/cable.yml: -------------------------------------------------------------------------------- 1 | development: 2 | adapter: async 3 | 4 | test: 5 | adapter: async 6 | 7 | production: 8 | adapter: redis 9 | url: redis://localhost:6379/1 10 | -------------------------------------------------------------------------------- /config/database.yml.ci: -------------------------------------------------------------------------------- 1 | test: 2 | adapter: postgresql 3 | database: circle_ruby_test 4 | username: ubuntu 5 | host: localhost 6 | -------------------------------------------------------------------------------- /config/database.yml.example: -------------------------------------------------------------------------------- 1 | default: &default 2 | adapter: postgresql 3 | encoding: unicode 4 | # For details on connection pooling, see rails configuration guide 5 | # http://guides.rubyonrails.org/configuring.html#database-pooling 6 | pool: 5 7 | min_messages: WARNING 8 | username: postgres 9 | password: postgres 10 | host: data 11 | 12 | development: 13 | <<: *default 14 | database: serviceworker_rails_sandbox_development 15 | 16 | test: 17 | <<: *default 18 | database: serviceworker_rails_sandbox_test 19 | 20 | # On Heroku and other platform providers, you may have a full connection URL 21 | # available as an environment variable. For example: 22 | # 23 | # DATABASE_URL="postgres://myuser:mypass@localhost/somedatabase" 24 | -------------------------------------------------------------------------------- /config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require_relative 'application' 3 | 4 | # Initialize the Rails application. 5 | Rails.application.initialize! 6 | -------------------------------------------------------------------------------- /config/environments/development.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # In the development environment your application's code is reloaded on 5 | # every request. This slows down response time but is perfect for development 6 | # since you don't have to restart the web server when you make code changes. 7 | config.cache_classes = false 8 | 9 | # Do not eager load code on boot. 10 | config.eager_load = false 11 | 12 | # Show full error reports and disable caching. 13 | config.consider_all_requests_local = true 14 | 15 | # Enable/disable caching. By default caching is disabled. 16 | if Rails.root.join('tmp/caching-dev.txt').exist? 17 | config.action_controller.perform_caching = true 18 | 19 | config.cache_store = :memory_store 20 | config.public_file_server.headers = { 21 | 'Cache-Control' => 'public, max-age=172800' 22 | } 23 | else 24 | config.action_controller.perform_caching = false 25 | 26 | config.cache_store = :null_store 27 | end 28 | 29 | # Don't care if the mailer can't send. 30 | config.action_mailer.raise_delivery_errors = false 31 | 32 | config.action_mailer.perform_caching = false 33 | 34 | # Print deprecation notices to the Rails logger. 35 | config.active_support.deprecation = :log 36 | 37 | # Raise an error on page load if there are pending migrations. 38 | config.active_record.migration_error = :page_load 39 | 40 | # Debug mode disables concatenation and preprocessing of assets. 41 | # This option may cause significant delays in view rendering with a large 42 | # number of complex assets. 43 | config.assets.debug = false 44 | 45 | config.assets.quiet = true 46 | 47 | # Raises error for missing translations 48 | # config.action_view.raise_on_missing_translations = true 49 | 50 | # Use an evented file watcher to asynchronously detect changes in source code, 51 | 52 | # routes, locales, etc. This feature depends on the listen gem. 53 | # config.file_watcher = ActiveSupport::EventedFileUpdateChecker 54 | 55 | # Additional gem configuration 56 | 57 | config.web_console.whitelisted_ips = '0.0.0.0' 58 | 59 | config.react.variant = :development 60 | end 61 | -------------------------------------------------------------------------------- /config/environments/production.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # Code is not reloaded between requests. 5 | config.cache_classes = true 6 | 7 | # Eager load code on boot. This eager loads most of Rails and 8 | # your application in memory, allowing both threaded web servers 9 | # and those relying on copy on write to perform better. 10 | # Rake tasks automatically ignore this option for performance. 11 | config.eager_load = true 12 | 13 | # Full error reports are disabled and caching is turned on. 14 | config.consider_all_requests_local = false 15 | config.action_controller.perform_caching = true 16 | 17 | # Disable serving static files from the `/public` folder by default since 18 | # Apache or NGINX already handles this. 19 | config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present? 20 | 21 | # Compress JavaScripts and CSS. 22 | config.assets.js_compressor = :uglifier 23 | # config.assets.css_compressor = :sass 24 | 25 | # Do not fallback to assets pipeline if a precompiled asset is missed. 26 | config.assets.compile = false 27 | 28 | # `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb 29 | 30 | # Enable serving of images, stylesheets, and JavaScripts from an asset server. 31 | # config.action_controller.asset_host = 'http://assets.example.com' 32 | 33 | # Specifies the header that your server uses for sending files. 34 | # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache 35 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX 36 | 37 | # Mount Action Cable outside main process or domain 38 | # config.action_cable.mount_path = nil 39 | # config.action_cable.url = 'wss://example.com/cable' 40 | # config.action_cable.allowed_request_origins = [ 'http://example.com', /http:\/\/example.*/ ] 41 | 42 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 43 | config.force_ssl = true 44 | 45 | # Use the lowest log level to ensure availability of diagnostic information 46 | # when problems arise. 47 | config.log_level = :debug 48 | 49 | # Prepend all log lines with the following tags. 50 | config.log_tags = [ :request_id ] 51 | 52 | # Use a different cache store in production. 53 | # config.cache_store = :mem_cache_store 54 | 55 | # Use a real queuing backend for Active Job (and separate queues per environment) 56 | # config.active_job.queue_adapter = :resque 57 | # config.active_job.queue_name_prefix = "serviceworker_rails_sandbox_#{Rails.env}" 58 | config.action_mailer.perform_caching = false 59 | 60 | # Ignore bad email addresses and do not raise email delivery errors. 61 | # Set this to true and configure the email server for immediate delivery to raise delivery errors. 62 | # config.action_mailer.raise_delivery_errors = false 63 | 64 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 65 | # the I18n.default_locale when a translation cannot be found). 66 | config.i18n.fallbacks = true 67 | 68 | # Send deprecation notices to registered listeners. 69 | config.active_support.deprecation = :notify 70 | 71 | # Use default logging formatter so that PID and timestamp are not suppressed. 72 | config.log_formatter = ::Logger::Formatter.new 73 | 74 | # Use a different logger for distributed setups. 75 | # require 'syslog/logger' 76 | # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name') 77 | 78 | if ENV["RAILS_LOG_TO_STDOUT"].present? 79 | logger = ActiveSupport::Logger.new(STDOUT) 80 | logger.formatter = config.log_formatter 81 | config.logger = ActiveSupport::TaggedLogging.new(logger) 82 | end 83 | 84 | # Do not dump schema after migrations. 85 | config.active_record.dump_schema_after_migration = false 86 | 87 | # Additional configuration 88 | config.settings.google_analytics_tracking_id = 'UA-31058047-2' 89 | 90 | config.react.variant = :production 91 | end 92 | 93 | # Rails 4.2 configuraiton: 94 | # Rails.application.configure do 95 | # # Settings specified here will take precedence over those in config/application.rb. 96 | # 97 | # # Code is not reloaded between requests. 98 | # # config.cache_classes = true 99 | # 100 | # # Eager load code on boot. This eager loads most of Rails and 101 | # # your application in memory, allowing both threaded web servers 102 | # # and those relying on copy on write to perform better. 103 | # # Rake tasks automatically ignore this option for performance. 104 | # config.eager_load = true 105 | # 106 | # # Full error reports are disabled and caching is turned on. 107 | # config.consider_all_requests_local = false 108 | # config.action_controller.perform_caching = true 109 | # 110 | # # Enable Rack::Cache to put a simple HTTP cache in front of your application 111 | # # Add `rack-cache` to your Gemfile before enabling this. 112 | # # For large-scale production use, consider using a caching reverse proxy like 113 | # # NGINX, varnish or squid. 114 | # # config.action_dispatch.rack_cache = true 115 | # 116 | # # Disable serving static files from the `/public` folder by default since 117 | # # Apache or NGINX already handles this. 118 | # config.serve_static_files = ENV['RAILS_SERVE_STATIC_FILES'].present? 119 | # 120 | # # Compress JavaScripts and CSS. 121 | # config.assets.js_compressor = :uglifier 122 | # # config.assets.css_compressor = :sass 123 | # 124 | # # Do not fallback to assets pipeline if a precompiled asset is missed. 125 | # config.assets.compile = false 126 | # 127 | # # Asset digests allow you to set far-future HTTP expiration dates on all assets, 128 | # # yet still be able to expire them through the digest params. 129 | # config.assets.digest = true 130 | # 131 | # # `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb 132 | # 133 | # # Specifies the header that your server uses for sending files. 134 | # # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache 135 | # # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX 136 | # 137 | # # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 138 | # config.force_ssl = true 139 | # 140 | # # Use the lowest log level to ensure availability of diagnostic information 141 | # # when problems arise. 142 | # config.log_level = :debug 143 | # 144 | # # Prepend all log lines with the following tags. 145 | # # config.log_tags = [ :subdomain, :uuid ] 146 | # 147 | # # Use a different logger for distributed setups. 148 | # # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new) 149 | # 150 | # # Use a different cache store in production. 151 | # # config.cache_store = :mem_cache_store 152 | # 153 | # # Enable serving of images, stylesheets, and JavaScripts from an asset server. 154 | # # config.action_controller.asset_host = 'https://assets.example.com' 155 | # 156 | # # Ignore bad email addresses and do not raise email delivery errors. 157 | # # Set this to true and configure the email server for immediate delivery to raise delivery errors. 158 | # # config.action_mailer.raise_delivery_errors = false 159 | # 160 | # # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 161 | # # the I18n.default_locale when a translation cannot be found). 162 | # config.i18n.fallbacks = true 163 | # 164 | # # Send deprecation notices to registered listeners. 165 | # config.active_support.deprecation = :notify 166 | # 167 | # # Use default logging formatter so that PID and timestamp are not suppressed. 168 | # config.log_formatter = ::Logger::Formatter.new 169 | # 170 | # # Do not dump schema after migrations. 171 | # config.active_record.dump_schema_after_migration = false 172 | # 173 | # config.settings.google_analytics_tracking_id = 'UA-31058047-2' 174 | # 175 | # config.react.variant = :production 176 | # end 177 | -------------------------------------------------------------------------------- /config/environments/test.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # The test environment is used exclusively to run your application's 5 | # test suite. You never need to work with it otherwise. Remember that 6 | # your test database is "scratch space" for the test suite and is wiped 7 | # and recreated between test runs. Don't rely on the data there! 8 | config.cache_classes = true 9 | 10 | # Do not eager load code on boot. This avoids loading your whole application 11 | # just for the purpose of running a single test. If you are using a tool that 12 | # preloads Rails for running tests, you may have to set it to true. 13 | config.eager_load = false 14 | 15 | # Configure public file server for tests with Cache-Control for performance. 16 | config.public_file_server.enabled = true 17 | config.public_file_server.headers = { 18 | 'Cache-Control' => 'public, max-age=3600' 19 | } 20 | 21 | # Show full error reports and disable caching. 22 | config.consider_all_requests_local = true 23 | config.action_controller.perform_caching = false 24 | 25 | # Raise exceptions instead of rendering exception templates. 26 | config.action_dispatch.show_exceptions = false 27 | 28 | # Disable request forgery protection in test environment. 29 | config.action_controller.allow_forgery_protection = false 30 | config.action_mailer.perform_caching = false 31 | 32 | # Tell Action Mailer not to deliver emails to the real world. 33 | # The :test delivery method accumulates sent emails in the 34 | # ActionMailer::Base.deliveries array. 35 | config.action_mailer.delivery_method = :test 36 | 37 | # Print deprecation notices to the stderr. 38 | config.active_support.deprecation = :stderr 39 | 40 | # Raises error for missing translations 41 | # config.action_view.raise_on_missing_translations = true 42 | end 43 | -------------------------------------------------------------------------------- /config/initializers/application_controller_renderer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # ApplicationController.renderer.defaults.merge!( 4 | # http_host: 'example.org', 5 | # https: false 6 | # ) 7 | -------------------------------------------------------------------------------- /config/initializers/assets.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Version of your assets, change this if you want to expire all your assets. 4 | # Rails.application.config.assets.version = '1.0' 5 | 6 | # Add additional assets to the asset load path 7 | # Rails.application.config.assets.paths << Emoji.images_path 8 | 9 | # Precompile additional assets. 10 | # application.js, application.css, and all non-JS/CSS in app/assets folder are already added. 11 | 12 | # Rails.application.config.assets.precompile += %w( search.js ) 13 | # 14 | Rails.application.configure do 15 | # Version of your assets, change this if you want to expire all your assets. 16 | config.assets.version = '1.1' 17 | 18 | # Add additional assets to the asset load path 19 | # Rails.application.config.assets.paths << Emoji.images_path 20 | 21 | # Precompile additional assets. 22 | # application.js, application.css, and all non-JS/CSS in app/assets folder are already added. 23 | # Rails.application.config.assets.precompile += %w( search.js ) 24 | config.assets.precompile += %w( 25 | home/serviceworker.js 26 | cache-then-network/serviceworker.js 27 | cache-then-network.js 28 | offline-fallback/serviceworker.js 29 | offline-fallback.js 30 | push-simple/serviceworker.js 31 | push-simple.js 32 | push-simple/manifest.json 33 | push-react.js 34 | push-react/serviceworker.js 35 | push-react/manifest.json 36 | ) 37 | 38 | # Use ES2015 and react in asset pipeline 39 | config.browserify_rails.commandline_options = [ 40 | "-t [ babelify --presets [ es2015 react ] ]", 41 | "--extension=\".jsx\"" 42 | ] 43 | end 44 | 45 | -------------------------------------------------------------------------------- /config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } 5 | 6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. 7 | # Rails.backtrace_cleaner.remove_silencers! 8 | -------------------------------------------------------------------------------- /config/initializers/cookies_serializer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Specify a serializer for the signed and encrypted cookie jars. 4 | # Valid options are :json, :marshal, and :hybrid. 5 | Rails.application.config.action_dispatch.cookies_serializer = :json 6 | -------------------------------------------------------------------------------- /config/initializers/filter_parameter_logging.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Configure sensitive parameters which will be filtered from the log file. 4 | Rails.application.config.filter_parameters += [:password] 5 | -------------------------------------------------------------------------------- /config/initializers/high_voltage.rb: -------------------------------------------------------------------------------- 1 | HighVoltage.configure do |config| 2 | config.routes = false 3 | end 4 | -------------------------------------------------------------------------------- /config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format. Inflections 4 | # are locale specific, and you may define rules for as many different 5 | # locales as you wish. All of these examples are active by default: 6 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 7 | # inflect.plural /^(ox)$/i, '\1en' 8 | # inflect.singular /^(ox)en/i, '\1' 9 | # inflect.irregular 'person', 'people' 10 | # inflect.uncountable %w( fish sheep ) 11 | # end 12 | 13 | # These inflection rules are supported but not enabled by default: 14 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 15 | # inflect.acronym 'RESTful' 16 | # end 17 | -------------------------------------------------------------------------------- /config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new mime types for use in respond_to blocks: 4 | # Mime::Type.register "text/richtext", :rtf 5 | -------------------------------------------------------------------------------- /config/initializers/new_framework_defaults.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | # 3 | # This file contains migration options to ease your Rails 5.0 upgrade. 4 | # 5 | # Once upgraded flip defaults one by one to migrate to the new default. 6 | # 7 | # Read the Rails 5.0 release notes for more info on each option. 8 | 9 | # Enable per-form CSRF tokens. Previous versions had false. 10 | Rails.application.config.action_controller.per_form_csrf_tokens = false 11 | 12 | # Enable origin-checking CSRF mitigation. Previous versions had false. 13 | Rails.application.config.action_controller.forgery_protection_origin_check = false 14 | 15 | # Make Ruby 2.4 preserve the timezone of the receiver when calling `to_time`. 16 | # Previous versions had false. 17 | ActiveSupport.to_time_preserves_timezone = false 18 | 19 | # Require `belongs_to` associations by default. Previous versions had false. 20 | Rails.application.config.active_record.belongs_to_required_by_default = false 21 | 22 | # Do not halt callback chains when a callback returns false. Previous versions had true. 23 | ActiveSupport.halt_callback_chains_on_return_false = true 24 | -------------------------------------------------------------------------------- /config/initializers/serviceworker.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | config.serviceworker.routes.draw do 3 | match "/serviceworker.js" => "home/serviceworker.js" 4 | 5 | match "/*pages/serviceworker.js" => "%{pages}/serviceworker.js" 6 | 7 | match "/*pages/manifest.json" => "%{pages}/manifest.json" 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /config/initializers/session_store.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | Rails.application.config.session_store :cookie_store, key: '_serviceworker_rails_sandbox_session' 4 | -------------------------------------------------------------------------------- /config/initializers/sucker_punch.rb: -------------------------------------------------------------------------------- 1 | require 'sucker_punch/async_syntax' # until Rails 5 2 | -------------------------------------------------------------------------------- /config/initializers/wrap_parameters.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # This file contains settings for ActionController::ParamsWrapper which 4 | # is enabled by default. 5 | 6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. 7 | ActiveSupport.on_load(:action_controller) do 8 | wrap_parameters format: [:json] 9 | end 10 | 11 | # To enable root element in JSON for ActiveRecord objects. 12 | # ActiveSupport.on_load(:active_record) do 13 | # self.include_root_in_json = true 14 | # end 15 | -------------------------------------------------------------------------------- /config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Files in the config/locales directory are used for internationalization 2 | # and are automatically loaded by Rails. If you want to use locales other 3 | # than English, add the necessary files in this directory. 4 | # 5 | # To use the locales, use `I18n.t`: 6 | # 7 | # I18n.t 'hello' 8 | # 9 | # In views, this is aliased to just `t`: 10 | # 11 | # <%= t('hello') %> 12 | # 13 | # To use a different locale, set it with `I18n.locale`: 14 | # 15 | # I18n.locale = :es 16 | # 17 | # This would use the information in config/locales/es.yml. 18 | # 19 | # To learn more, please read the Rails Internationalization guide 20 | # available at http://guides.rubyonrails.org/i18n.html. 21 | 22 | en: 23 | hello: "Hello world" 24 | -------------------------------------------------------------------------------- /config/locales/meta.yml: -------------------------------------------------------------------------------- 1 | en: 2 | meta: 3 | title: "Service Workers on Rails" 4 | description: "Service Workers on Rails is an application that demonstrates the use of the new Service Worker JavaScript API with Ruby on Raill applications using the serviceworker-rails gem" 5 | keywords: "Service Workers, Ruby, Rails, Ruby on Rails, serviceworker-rails gem, GitHub, Ross Kaffenberger" 6 | author: "Ross Kaffenberger" 7 | image: "pics/rubiks-cube.jpeg" 8 | site_name: "serviceworkers-rails-sandbox" 9 | -------------------------------------------------------------------------------- /config/locales/nav.yml: -------------------------------------------------------------------------------- 1 | en: 2 | nav: 3 | caching: 4 | - "Cache then Network" 5 | offline: 6 | - "Offline Fallback" 7 | push: 8 | - "Push Simple" 9 | - "Push React" 10 | -------------------------------------------------------------------------------- /config/puma.rb: -------------------------------------------------------------------------------- 1 | # Puma can serve each request in a thread from an internal thread pool. 2 | # The `threads` method setting takes two numbers a minimum and maximum. 3 | # Any libraries that use thread pools should be configured to match 4 | # the maximum value specified for Puma. Default is set to 5 threads for minimum 5 | # and maximum, this matches the default thread size of Active Record. 6 | # 7 | threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }.to_i 8 | threads threads_count, threads_count 9 | 10 | # Specifies the `port` that Puma will listen on to receive requests, default is 3000. 11 | # 12 | port ENV.fetch("PORT") { 3000 } 13 | 14 | # Specifies the `environment` that Puma will run in. 15 | # 16 | environment ENV.fetch("RAILS_ENV") { "development" } 17 | 18 | # Specifies the number of `workers` to boot in clustered mode. 19 | # Workers are forked webserver processes. If using threads and workers together 20 | # the concurrency of the application would be max `threads` * `workers`. 21 | # Workers do not work on JRuby or Windows (both of which do not support 22 | # processes). 23 | # 24 | # workers ENV.fetch("WEB_CONCURRENCY") { 2 } 25 | 26 | # Use the `preload_app!` method when specifying a `workers` number. 27 | # This directive tells Puma to first boot the application and load code 28 | # before forking the application. This takes advantage of Copy On Write 29 | # process behavior so workers use less memory. If you use this option 30 | # you need to make sure to reconnect any threads in the `on_worker_boot` 31 | # block. 32 | # 33 | # preload_app! 34 | 35 | # The code in the `on_worker_boot` will be called if you are using 36 | # clustered mode by specifying a number of `workers`. After each worker 37 | # process is booted this block will be run, if you are using `preload_app!` 38 | # option you will want to use this block to reconnect to any threads 39 | # or connections that may have been created at application boot, Ruby 40 | # cannot share connections between processes. 41 | # 42 | # on_worker_boot do 43 | # ActiveRecord::Base.establish_connection if defined?(ActiveRecord) 44 | # end 45 | 46 | # Allow puma to be restarted by `rails restart` command. 47 | plugin :tmp_restart 48 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | Rails.application.routes.draw do 2 | root "welcome#index" 3 | 4 | resources :streams 5 | 6 | post "/subscribe" => "subscriptions#create" 7 | delete "/unsubscribe" => "subscriptions#destroy" 8 | post "/push" => "push_notifications#create" 9 | 10 | get "/*path/offline" => "welcome#offline" 11 | 12 | get "/*id" => "pages#show", as: :page, format: false, constraints: { id: /(?!assets).*/ } 13 | 14 | # The priority is based upon order of creation: first created -> highest priority. 15 | # See how all your routes lay out with "rake routes". 16 | 17 | # You can have the root of your site routed with "root" 18 | # root 'welcome#index' 19 | 20 | # Example of regular route: 21 | # get 'products/:id' => 'catalog#view' 22 | 23 | # Example of named route that can be invoked with purchase_url(id: product.id) 24 | # get 'products/:id/purchase' => 'catalog#purchase', as: :purchase 25 | 26 | # Example resource route (maps HTTP verbs to controller actions automatically): 27 | # resources :products 28 | 29 | # Example resource route with options: 30 | # resources :products do 31 | # member do 32 | # get 'short' 33 | # post 'toggle' 34 | # end 35 | # 36 | # collection do 37 | # get 'sold' 38 | # end 39 | # end 40 | 41 | # Example resource route with sub-resources: 42 | # resources :products do 43 | # resources :comments, :sales 44 | # resource :seller 45 | # end 46 | 47 | # Example resource route with more complex sub-resources: 48 | # resources :products do 49 | # resources :comments 50 | # resources :sales do 51 | # get 'recent', on: :collection 52 | # end 53 | # end 54 | 55 | # Example resource route with concerns: 56 | # concern :toggleable do 57 | # post 'toggle' 58 | # end 59 | # resources :posts, concerns: :toggleable 60 | # resources :photos, concerns: :toggleable 61 | 62 | # Example resource route within a namespace: 63 | # namespace :admin do 64 | # # Directs /admin/products/* to Admin::ProductsController 65 | # # (app/controllers/admin/products_controller.rb) 66 | # resources :products 67 | # end 68 | end 69 | -------------------------------------------------------------------------------- /config/secrets.yml: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Your secret key is used for verifying the integrity of signed cookies. 4 | # If you change this key, all old signed cookies will become invalid! 5 | 6 | # Make sure the secret is at least 30 characters and all random, 7 | # no regular words or you'll be exposed to dictionary attacks. 8 | # You can use `rake secret` to generate a secure secret key. 9 | 10 | # Make sure the secrets in this file are kept private 11 | # if you're sharing your code publicly. 12 | 13 | development: &default 14 | secret_key_base: <%= ENV["SECRET_KEY_BASE"] %> 15 | 16 | test: 17 | <<: *default 18 | 19 | # Do not keep production secrets in the repository, 20 | # instead read values from the environment. 21 | production: 22 | <<: *default 23 | -------------------------------------------------------------------------------- /config/spring.rb: -------------------------------------------------------------------------------- 1 | %w( 2 | .ruby-version 3 | .rbenv-vars 4 | tmp/restart.txt 5 | tmp/caching-dev.txt 6 | ).each { |path| Spring.watch(path) } 7 | -------------------------------------------------------------------------------- /db/schema.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | # This file is auto-generated from the current state of the database. Instead 3 | # of editing this file, please use the migrations feature of Active Record to 4 | # incrementally modify your database, and then regenerate this schema definition. 5 | # 6 | # Note that this schema.rb definition is the authoritative source for your 7 | # database schema. If you need to create the application database on another 8 | # system, you should be using db:schema:load, not running all the migrations 9 | # from scratch. The latter is a flawed and unsustainable approach (the more migrations 10 | # you'll amass, the slower it'll run and the greater likelihood for issues). 11 | # 12 | # It's strongly recommended that you check this file into your version control system. 13 | 14 | ActiveRecord::Schema.define(version: 0) do 15 | 16 | # These are extensions that must be enabled in order to support this database 17 | enable_extension "plpgsql" 18 | 19 | end 20 | -------------------------------------------------------------------------------- /db/seeds.rb: -------------------------------------------------------------------------------- 1 | # This file should contain all the record creation needed to seed the database with its default values. 2 | # The data can then be loaded with the rake db:seed (or created alongside the db with db:setup). 3 | # 4 | # Examples: 5 | # 6 | # cities = City.create([{ name: 'Chicago' }, { name: 'Copenhagen' }]) 7 | # Mayor.create(name: 'Emanuel', city: cities.first) 8 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | services: 3 | data: 4 | image: postgres:9.4.5 5 | environment: 6 | POSTGRES_USER: postgres 7 | POSTGRES_PASSWORD: postgres 8 | ports: 9 | - '5432:5432' 10 | volumes: 11 | - /var/lib/postgresql/data 12 | 13 | gems: 14 | image: busybox 15 | volumes: 16 | - /gems 17 | 18 | node: 19 | image: busybox 20 | volumes: 21 | - /node 22 | 23 | web: 24 | build: . 25 | command: bin/start 26 | ports: 27 | - "5000:3000" 28 | links: 29 | - data 30 | volumes: 31 | - .:/app 32 | volumes_from: 33 | - gems 34 | - node 35 | - data 36 | env_file: 37 | - .env.docker 38 | -------------------------------------------------------------------------------- /lib/assets/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rossta/serviceworker-rails-sandbox/3d16b9e7475e56045f97908a25029aa309ed1dea/lib/assets/.keep -------------------------------------------------------------------------------- /lib/tasks/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rossta/serviceworker-rails-sandbox/3d16b9e7475e56045f97908a25029aa309ed1dea/lib/tasks/.keep -------------------------------------------------------------------------------- /lib/templates/erb/scaffold/_form.html.erb: -------------------------------------------------------------------------------- 1 | <%%= simple_form_for(@<%= singular_table_name %>) do |f| %> 2 | <%%= f.error_notification %> 3 | 4 |
5 | <%- attributes.each do |attribute| -%> 6 | <%%= f.<%= attribute.reference? ? :association : :input %> :<%= attribute.name %> %> 7 | <%- end -%> 8 |
9 | 10 |
11 | <%%= f.button :submit %> 12 |
13 | <%% end %> 14 | -------------------------------------------------------------------------------- /log/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rossta/serviceworker-rails-sandbox/3d16b9e7475e56045f97908a25029aa309ed1dea/log/.keep -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "serviceworker-rails-sandbox", 3 | "version": "1.0.0", 4 | "description": "A Service Worker sandbox for Rails apps", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Skip\"" 8 | }, 9 | "author": "Ross Kaffenberger", 10 | "license": "MIT", 11 | "dependencies": { 12 | "babel-preset-es2015": "^6.6.0", 13 | "babel-preset-react": "^6.5.0", 14 | "babelify": "^7.3.0", 15 | "browserify": "^13.0.0", 16 | "browserify-incremental": "^3.1.1", 17 | "lodash": "^4.11.2", 18 | "materialize-css": "^0.97.7", 19 | "react": "^15.0.2", 20 | "react-dom": "^15.0.2", 21 | "reactify": "^1.1.1", 22 | "skel": "0.0.0", 23 | "skel-framework": "github:n33/skel" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The page you were looking for doesn't exist (404) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

The page you were looking for doesn't exist.

62 |

You may have mistyped the address or the page may have moved.

63 |
64 |

If you are the application owner check the logs for more information.

65 |
66 | 67 | 68 | -------------------------------------------------------------------------------- /public/422.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The change you wanted was rejected (422) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

The change you wanted was rejected.

62 |

Maybe you tried to change something you didn't have access to.

63 |
64 |

If you are the application owner check the logs for more information.

65 |
66 | 67 | 68 | -------------------------------------------------------------------------------- /public/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | We're sorry, but something went wrong (500) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

We're sorry, but something went wrong.

62 |
63 |

If you are the application owner check the logs for more information.

64 |
65 | 66 | 67 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rossta/serviceworker-rails-sandbox/3d16b9e7475e56045f97908a25029aa309ed1dea/public/favicon.ico -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 2 | # 3 | # To ban all spiders from the entire site uncomment the next two lines: 4 | # User-agent: * 5 | # Disallow: / 6 | -------------------------------------------------------------------------------- /public/touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rossta/serviceworker-rails-sandbox/3d16b9e7475e56045f97908a25029aa309ed1dea/public/touch-icon.png -------------------------------------------------------------------------------- /spec/clients/twitter_client_spec.rb: -------------------------------------------------------------------------------- 1 | require "rails_helper" 2 | 3 | describe TwitterClient do 4 | let(:client) { TwitterClient.new } 5 | 6 | def token 7 | ENV.fetch('TWITTER_ACCESS_TOKEN') 8 | end 9 | 10 | def secret 11 | ENV.fetch('TWITTER_ACCESS_SECRET') 12 | end 13 | 14 | describe "#client" do 15 | it "wraps the twitter gem rest client" do 16 | twitter = client.client 17 | expect(twitter).to be_instance_of ::Twitter::REST::Client 18 | end 19 | end 20 | 21 | describe "#timeline" do 22 | it "returns twitter home_timeline" do 23 | stub_request(:get, "https://api.twitter.com/1.1/statuses/home_timeline.json"). 24 | with(headers: { "Accept" => "application/json" }). 25 | and_return(body: [ 26 | { 27 | id: 1234, 28 | text: "Waking up" 29 | }, 30 | { 31 | id: 1235, 32 | text: "Drinking coffee" 33 | }, 34 | { 35 | id: 1236, 36 | text: "Walking the dog" 37 | }].to_json, 38 | status: 200) 39 | 40 | expect(client.timeline.map(&:body)).to eq ['Waking up', 'Drinking coffee', 'Walking the dog'] 41 | end 42 | end 43 | end 44 | 45 | -------------------------------------------------------------------------------- /spec/clients/webpush_client_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe WebpushClient do 4 | let(:client) { WebpushClient.new } 5 | 6 | describe "#send_notification" do 7 | it "sends to google endpoint" do 8 | p256dh = SecureRandom.random_bytes(65) 9 | auth = SecureRandom.random_bytes(16) 10 | endpoint = "https://fcm.googleapis.com/gcm/send/subscriptionId" 11 | 12 | allow(WebpushClient).to receive(:public_key).and_return("public_key") 13 | allow(WebpushClient).to receive(:private_key).and_return("private_key") 14 | 15 | expect(Webpush).to receive(:payload_send).with( 16 | message: "Hello World", 17 | endpoint: "https://fcm.googleapis.com/gcm/send/subscriptionId", 18 | p256dh: p256dh, 19 | auth: auth, 20 | vapid: { 21 | subject: "mailto:ross@rossta.net", 22 | public_key: "public_key", 23 | private_key: "private_key" 24 | } 25 | 26 | ) 27 | 28 | client.send_notification("Hello World", endpoint: endpoint, p256dh: p256dh, auth: auth) 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /spec/rails_helper.rb: -------------------------------------------------------------------------------- 1 | # This file is copied to spec/ when you run 'rails generate rspec:install' 2 | ENV['RAILS_ENV'] ||= 'test' 3 | require File.expand_path('../../config/environment', __FILE__) 4 | # Prevent database truncation if the environment is production 5 | abort("The Rails environment is running in production mode!") if Rails.env.production? 6 | require 'spec_helper' 7 | require 'rspec/rails' 8 | # Add additional requires below this line. Rails is not loaded until this point! 9 | require 'webmock/rspec' 10 | 11 | # Requires supporting ruby files with custom matchers and macros, etc, in 12 | # spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are 13 | # run as spec files by default. This means that files in spec/support that end 14 | # in _spec.rb will both be required and run as specs, causing the specs to be 15 | # run twice. It is recommended that you do not name files matching this glob to 16 | # end with _spec.rb. You can configure this pattern with the --pattern 17 | # option on the command line or in ~/.rspec, .rspec or `.rspec-local`. 18 | # 19 | # The following line is provided for convenience purposes. It has the downside 20 | # of increasing the boot-up time by auto-requiring all files in the support 21 | # directory. Alternatively, in the individual `*_spec.rb` files, manually 22 | # require only the support files necessary. 23 | # 24 | # Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f } 25 | 26 | # Checks for pending migration and applies them before tests are run. 27 | # If you are not using ActiveRecord, you can remove this line. 28 | ActiveRecord::Migration.maintain_test_schema! 29 | 30 | RSpec.configure do |config| 31 | # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures 32 | # config.fixture_path = "#{::Rails.root}/spec/fixtures" 33 | 34 | # If you're not using ActiveRecord, or you'd prefer not to run each of your 35 | # examples within a transaction, remove the following line or assign false 36 | # instead of true. 37 | config.use_transactional_fixtures = false 38 | 39 | # RSpec Rails can automatically mix in different behaviours to your tests 40 | # based on their file location, for example enabling you to call `get` and 41 | # `post` in specs under `spec/controllers`. 42 | # 43 | # You can disable this behaviour by removing the line below, and instead 44 | # explicitly tag your specs with their type, e.g.: 45 | # 46 | # RSpec.describe UsersController, :type => :controller do 47 | # # ... 48 | # end 49 | # 50 | # The different available types are documented in the features, such as in 51 | # https://relishapp.com/rspec/rspec-rails/docs 52 | config.infer_spec_type_from_file_location! 53 | 54 | # Filter lines from Rails gems in backtraces. 55 | config.filter_rails_from_backtrace! 56 | # arbitrary gems may also be filtered via: 57 | # config.filter_gems_from_backtrace("gem name") 58 | end 59 | 60 | Dir[Rails.root.join("spec/support/**/*.rb")].each { |f| require f } 61 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # This file was generated by the `rails generate rspec:install` command. Conventionally, all 2 | # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. 3 | # The generated `.rspec` file contains `--require spec_helper` which will cause 4 | # this file to always be loaded, without a need to explicitly require it in any 5 | # files. 6 | # 7 | # Given that it is always loaded, you are encouraged to keep this file as 8 | # light-weight as possible. Requiring heavyweight dependencies from this file 9 | # will add to the boot time of your test suite on EVERY test run, even for an 10 | # individual file that may not need all of that loaded. Instead, consider making 11 | # a separate helper file that requires the additional dependencies and performs 12 | # the additional setup, and require it from the spec files that actually need 13 | # it. 14 | # 15 | # The `.rspec` file also contains a few flags that are not defaults but that 16 | # users commonly want. 17 | # 18 | # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration 19 | RSpec.configure do |config| 20 | # rspec-expectations config goes here. You can use an alternate 21 | # assertion/expectation library such as wrong or the stdlib/minitest 22 | # assertions if you prefer. 23 | config.expect_with :rspec do |expectations| 24 | # This option will default to `true` in RSpec 4. It makes the `description` 25 | # and `failure_message` of custom matchers include text for helper methods 26 | # defined using `chain`, e.g.: 27 | # be_bigger_than(2).and_smaller_than(4).description 28 | # # => "be bigger than 2 and smaller than 4" 29 | # ...rather than: 30 | # # => "be bigger than 2" 31 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true 32 | end 33 | 34 | # rspec-mocks config goes here. You can use an alternate test double 35 | # library (such as bogus or mocha) by changing the `mock_with` option here. 36 | config.mock_with :rspec do |mocks| 37 | # Prevents you from mocking or stubbing a method that does not exist on 38 | # a real object. This is generally recommended, and will default to 39 | # `true` in RSpec 4. 40 | mocks.verify_partial_doubles = true 41 | end 42 | 43 | # The settings below are suggested to provide a good initial experience 44 | # with RSpec, but feel free to customize to your heart's content. 45 | =begin 46 | # These two settings work together to allow you to limit a spec run 47 | # to individual examples or groups you care about by tagging them with 48 | # `:focus` metadata. When nothing is tagged with `:focus`, all examples 49 | # get run. 50 | config.filter_run :focus 51 | config.run_all_when_everything_filtered = true 52 | 53 | # Allows RSpec to persist some state between runs in order to support 54 | # the `--only-failures` and `--next-failure` CLI options. We recommend 55 | # you configure your source control system to ignore this file. 56 | config.example_status_persistence_file_path = "spec/examples.txt" 57 | 58 | # Limits the available syntax to the non-monkey patched syntax that is 59 | # recommended. For more details, see: 60 | # - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/ 61 | # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/ 62 | # - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode 63 | config.disable_monkey_patching! 64 | 65 | # Many RSpec users commonly either run the entire suite or an individual 66 | # file, and it's useful to allow more verbose output when running an 67 | # individual spec file. 68 | if config.files_to_run.one? 69 | # Use the documentation formatter for detailed output, 70 | # unless a formatter has already been configured 71 | # (e.g. via a command-line flag). 72 | config.default_formatter = 'doc' 73 | end 74 | 75 | # Print the 10 slowest examples and example groups at the 76 | # end of the spec run, to help surface which specs are running 77 | # particularly slow. 78 | config.profile_examples = 10 79 | 80 | # Run specs in random order to surface order dependencies. If you find an 81 | # order dependency and want to debug it, you can fix the order by providing 82 | # the seed, which is printed after each run. 83 | # --seed 1234 84 | config.order = :random 85 | 86 | # Seed global randomization in this process using the `--seed` CLI option. 87 | # Setting this allows you to use `--seed` to deterministically reproduce 88 | # test failures related to randomization by passing the same `--seed` value 89 | # as the one that triggered the failure. 90 | Kernel.srand config.seed 91 | =end 92 | end 93 | -------------------------------------------------------------------------------- /spec/support/action_mailer.rb: -------------------------------------------------------------------------------- 1 | RSpec.configure do |config| 2 | config.before(:each) do 3 | ActionMailer::Base.deliveries.clear 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /spec/support/database_cleaner.rb: -------------------------------------------------------------------------------- 1 | RSpec.configure do |config| 2 | config.before(:suite) do 3 | # DatabaseCleaner.clean_with(:truncation) 4 | DatabaseCleaner.clean_with(:deletion) 5 | end 6 | 7 | config.before(:each) do 8 | DatabaseCleaner.strategy = :transaction 9 | end 10 | 11 | config.before(:each, :js => true) do 12 | DatabaseCleaner.strategy = :deletion 13 | end 14 | 15 | config.before(:each) do 16 | DatabaseCleaner.start 17 | end 18 | 19 | config.after(:each) do 20 | DatabaseCleaner.clean 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /spec/support/factory_girl.rb: -------------------------------------------------------------------------------- 1 | RSpec.configure do |config| 2 | config.include FactoryGirl::Syntax::Methods 3 | end 4 | -------------------------------------------------------------------------------- /spec/support/webmock.rb: -------------------------------------------------------------------------------- 1 | WebMock.disable_net_connect!(allow_localhost: true) 2 | -------------------------------------------------------------------------------- /vendor/assets/javascripts/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rossta/serviceworker-rails-sandbox/3d16b9e7475e56045f97908a25029aa309ed1dea/vendor/assets/javascripts/.keep -------------------------------------------------------------------------------- /vendor/assets/stylesheets/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rossta/serviceworker-rails-sandbox/3d16b9e7475e56045f97908a25029aa309ed1dea/vendor/assets/stylesheets/.keep -------------------------------------------------------------------------------- /vendor/assets/stylesheets/Skeleton-2.0.4.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rossta/serviceworker-rails-sandbox/3d16b9e7475e56045f97908a25029aa309ed1dea/vendor/assets/stylesheets/Skeleton-2.0.4.zip -------------------------------------------------------------------------------- /vendor/assets/stylesheets/Skeleton-2.0.4/css/normalize.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v3.0.2 | MIT License | git.io/normalize */ 2 | 3 | /** 4 | * 1. Set default font family to sans-serif. 5 | * 2. Prevent iOS text size adjust after orientation change, without disabling 6 | * user zoom. 7 | */ 8 | 9 | html { 10 | font-family: sans-serif; /* 1 */ 11 | -ms-text-size-adjust: 100%; /* 2 */ 12 | -webkit-text-size-adjust: 100%; /* 2 */ 13 | } 14 | 15 | /** 16 | * Remove default margin. 17 | */ 18 | 19 | body { 20 | margin: 0; 21 | } 22 | 23 | /* HTML5 display definitions 24 | ========================================================================== */ 25 | 26 | /** 27 | * Correct `block` display not defined for any HTML5 element in IE 8/9. 28 | * Correct `block` display not defined for `details` or `summary` in IE 10/11 29 | * and Firefox. 30 | * Correct `block` display not defined for `main` in IE 11. 31 | */ 32 | 33 | article, 34 | aside, 35 | details, 36 | figcaption, 37 | figure, 38 | footer, 39 | header, 40 | hgroup, 41 | main, 42 | menu, 43 | nav, 44 | section, 45 | summary { 46 | display: block; 47 | } 48 | 49 | /** 50 | * 1. Correct `inline-block` display not defined in IE 8/9. 51 | * 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera. 52 | */ 53 | 54 | audio, 55 | canvas, 56 | progress, 57 | video { 58 | display: inline-block; /* 1 */ 59 | vertical-align: baseline; /* 2 */ 60 | } 61 | 62 | /** 63 | * Prevent modern browsers from displaying `audio` without controls. 64 | * Remove excess height in iOS 5 devices. 65 | */ 66 | 67 | audio:not([controls]) { 68 | display: none; 69 | height: 0; 70 | } 71 | 72 | /** 73 | * Address `[hidden]` styling not present in IE 8/9/10. 74 | * Hide the `template` element in IE 8/9/11, Safari, and Firefox < 22. 75 | */ 76 | 77 | [hidden], 78 | template { 79 | display: none; 80 | } 81 | 82 | /* Links 83 | ========================================================================== */ 84 | 85 | /** 86 | * Remove the gray background color from active links in IE 10. 87 | */ 88 | 89 | a { 90 | background-color: transparent; 91 | } 92 | 93 | /** 94 | * Improve readability when focused and also mouse hovered in all browsers. 95 | */ 96 | 97 | a:active, 98 | a:hover { 99 | outline: 0; 100 | } 101 | 102 | /* Text-level semantics 103 | ========================================================================== */ 104 | 105 | /** 106 | * Address styling not present in IE 8/9/10/11, Safari, and Chrome. 107 | */ 108 | 109 | abbr[title] { 110 | border-bottom: 1px dotted; 111 | } 112 | 113 | /** 114 | * Address style set to `bolder` in Firefox 4+, Safari, and Chrome. 115 | */ 116 | 117 | b, 118 | strong { 119 | font-weight: bold; 120 | } 121 | 122 | /** 123 | * Address styling not present in Safari and Chrome. 124 | */ 125 | 126 | dfn { 127 | font-style: italic; 128 | } 129 | 130 | /** 131 | * Address variable `h1` font-size and margin within `section` and `article` 132 | * contexts in Firefox 4+, Safari, and Chrome. 133 | */ 134 | 135 | h1 { 136 | font-size: 2em; 137 | margin: 0.67em 0; 138 | } 139 | 140 | /** 141 | * Address styling not present in IE 8/9. 142 | */ 143 | 144 | mark { 145 | background: #ff0; 146 | color: #000; 147 | } 148 | 149 | /** 150 | * Address inconsistent and variable font size in all browsers. 151 | */ 152 | 153 | small { 154 | font-size: 80%; 155 | } 156 | 157 | /** 158 | * Prevent `sub` and `sup` affecting `line-height` in all browsers. 159 | */ 160 | 161 | sub, 162 | sup { 163 | font-size: 75%; 164 | line-height: 0; 165 | position: relative; 166 | vertical-align: baseline; 167 | } 168 | 169 | sup { 170 | top: -0.5em; 171 | } 172 | 173 | sub { 174 | bottom: -0.25em; 175 | } 176 | 177 | /* Embedded content 178 | ========================================================================== */ 179 | 180 | /** 181 | * Remove border when inside `a` element in IE 8/9/10. 182 | */ 183 | 184 | img { 185 | border: 0; 186 | } 187 | 188 | /** 189 | * Correct overflow not hidden in IE 9/10/11. 190 | */ 191 | 192 | svg:not(:root) { 193 | overflow: hidden; 194 | } 195 | 196 | /* Grouping content 197 | ========================================================================== */ 198 | 199 | /** 200 | * Address margin not present in IE 8/9 and Safari. 201 | */ 202 | 203 | figure { 204 | margin: 1em 40px; 205 | } 206 | 207 | /** 208 | * Address differences between Firefox and other browsers. 209 | */ 210 | 211 | hr { 212 | -moz-box-sizing: content-box; 213 | box-sizing: content-box; 214 | height: 0; 215 | } 216 | 217 | /** 218 | * Contain overflow in all browsers. 219 | */ 220 | 221 | pre { 222 | overflow: auto; 223 | } 224 | 225 | /** 226 | * Address odd `em`-unit font size rendering in all browsers. 227 | */ 228 | 229 | code, 230 | kbd, 231 | pre, 232 | samp { 233 | font-family: monospace, monospace; 234 | font-size: 1em; 235 | } 236 | 237 | /* Forms 238 | ========================================================================== */ 239 | 240 | /** 241 | * Known limitation: by default, Chrome and Safari on OS X allow very limited 242 | * styling of `select`, unless a `border` property is set. 243 | */ 244 | 245 | /** 246 | * 1. Correct color not being inherited. 247 | * Known issue: affects color of disabled elements. 248 | * 2. Correct font properties not being inherited. 249 | * 3. Address margins set differently in Firefox 4+, Safari, and Chrome. 250 | */ 251 | 252 | button, 253 | input, 254 | optgroup, 255 | select, 256 | textarea { 257 | color: inherit; /* 1 */ 258 | font: inherit; /* 2 */ 259 | margin: 0; /* 3 */ 260 | } 261 | 262 | /** 263 | * Address `overflow` set to `hidden` in IE 8/9/10/11. 264 | */ 265 | 266 | button { 267 | overflow: visible; 268 | } 269 | 270 | /** 271 | * Address inconsistent `text-transform` inheritance for `button` and `select`. 272 | * All other form control elements do not inherit `text-transform` values. 273 | * Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera. 274 | * Correct `select` style inheritance in Firefox. 275 | */ 276 | 277 | button, 278 | select { 279 | text-transform: none; 280 | } 281 | 282 | /** 283 | * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` 284 | * and `video` controls. 285 | * 2. Correct inability to style clickable `input` types in iOS. 286 | * 3. Improve usability and consistency of cursor style between image-type 287 | * `input` and others. 288 | */ 289 | 290 | button, 291 | html input[type="button"], /* 1 */ 292 | input[type="reset"], 293 | input[type="submit"] { 294 | -webkit-appearance: button; /* 2 */ 295 | cursor: pointer; /* 3 */ 296 | } 297 | 298 | /** 299 | * Re-set default cursor for disabled elements. 300 | */ 301 | 302 | button[disabled], 303 | html input[disabled] { 304 | cursor: default; 305 | } 306 | 307 | /** 308 | * Remove inner padding and border in Firefox 4+. 309 | */ 310 | 311 | button::-moz-focus-inner, 312 | input::-moz-focus-inner { 313 | border: 0; 314 | padding: 0; 315 | } 316 | 317 | /** 318 | * Address Firefox 4+ setting `line-height` on `input` using `!important` in 319 | * the UA stylesheet. 320 | */ 321 | 322 | input { 323 | line-height: normal; 324 | } 325 | 326 | /** 327 | * It's recommended that you don't attempt to style these elements. 328 | * Firefox's implementation doesn't respect box-sizing, padding, or width. 329 | * 330 | * 1. Address box sizing set to `content-box` in IE 8/9/10. 331 | * 2. Remove excess padding in IE 8/9/10. 332 | */ 333 | 334 | input[type="checkbox"], 335 | input[type="radio"] { 336 | box-sizing: border-box; /* 1 */ 337 | padding: 0; /* 2 */ 338 | } 339 | 340 | /** 341 | * Fix the cursor style for Chrome's increment/decrement buttons. For certain 342 | * `font-size` values of the `input`, it causes the cursor style of the 343 | * decrement button to change from `default` to `text`. 344 | */ 345 | 346 | input[type="number"]::-webkit-inner-spin-button, 347 | input[type="number"]::-webkit-outer-spin-button { 348 | height: auto; 349 | } 350 | 351 | /** 352 | * 1. Address `appearance` set to `searchfield` in Safari and Chrome. 353 | * 2. Address `box-sizing` set to `border-box` in Safari and Chrome 354 | * (include `-moz` to future-proof). 355 | */ 356 | 357 | input[type="search"] { 358 | -webkit-appearance: textfield; /* 1 */ 359 | -moz-box-sizing: content-box; 360 | -webkit-box-sizing: content-box; /* 2 */ 361 | box-sizing: content-box; 362 | } 363 | 364 | /** 365 | * Remove inner padding and search cancel button in Safari and Chrome on OS X. 366 | * Safari (but not Chrome) clips the cancel button when the search input has 367 | * padding (and `textfield` appearance). 368 | */ 369 | 370 | input[type="search"]::-webkit-search-cancel-button, 371 | input[type="search"]::-webkit-search-decoration { 372 | -webkit-appearance: none; 373 | } 374 | 375 | /** 376 | * Define consistent border, margin, and padding. 377 | */ 378 | 379 | fieldset { 380 | border: 1px solid #c0c0c0; 381 | margin: 0 2px; 382 | padding: 0.35em 0.625em 0.75em; 383 | } 384 | 385 | /** 386 | * 1. Correct `color` not being inherited in IE 8/9/10/11. 387 | * 2. Remove padding so people aren't caught out if they zero out fieldsets. 388 | */ 389 | 390 | legend { 391 | border: 0; /* 1 */ 392 | padding: 0; /* 2 */ 393 | } 394 | 395 | /** 396 | * Remove default vertical scrollbar in IE 8/9/10/11. 397 | */ 398 | 399 | textarea { 400 | overflow: auto; 401 | } 402 | 403 | /** 404 | * Don't inherit the `font-weight` (applied by a rule above). 405 | * NOTE: the default cannot safely be changed in Chrome and Safari on OS X. 406 | */ 407 | 408 | optgroup { 409 | font-weight: bold; 410 | } 411 | 412 | /* Tables 413 | ========================================================================== */ 414 | 415 | /** 416 | * Remove most spacing between table cells. 417 | */ 418 | 419 | table { 420 | border-collapse: collapse; 421 | border-spacing: 0; 422 | } 423 | 424 | td, 425 | th { 426 | padding: 0; 427 | } -------------------------------------------------------------------------------- /vendor/assets/stylesheets/Skeleton-2.0.4/css/skeleton.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Skeleton V2.0.4 3 | * Copyright 2014, Dave Gamache 4 | * www.getskeleton.com 5 | * Free to use under the MIT license. 6 | * http://www.opensource.org/licenses/mit-license.php 7 | * 12/29/2014 8 | */ 9 | 10 | 11 | /* Table of contents 12 | –––––––––––––––––––––––––––––––––––––––––––––––––– 13 | - Grid 14 | - Base Styles 15 | - Typography 16 | - Links 17 | - Buttons 18 | - Forms 19 | - Lists 20 | - Code 21 | - Tables 22 | - Spacing 23 | - Utilities 24 | - Clearing 25 | - Media Queries 26 | */ 27 | 28 | 29 | /* Grid 30 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 31 | .container { 32 | position: relative; 33 | width: 100%; 34 | max-width: 960px; 35 | margin: 0 auto; 36 | padding: 0 20px; 37 | box-sizing: border-box; } 38 | .column, 39 | .columns { 40 | width: 100%; 41 | float: left; 42 | box-sizing: border-box; } 43 | 44 | /* For devices larger than 400px */ 45 | @media (min-width: 400px) { 46 | .container { 47 | width: 85%; 48 | padding: 0; } 49 | } 50 | 51 | /* For devices larger than 550px */ 52 | @media (min-width: 550px) { 53 | .container { 54 | width: 80%; } 55 | .column, 56 | .columns { 57 | margin-left: 4%; } 58 | .column:first-child, 59 | .columns:first-child { 60 | margin-left: 0; } 61 | 62 | .one.column, 63 | .one.columns { width: 4.66666666667%; } 64 | .two.columns { width: 13.3333333333%; } 65 | .three.columns { width: 22%; } 66 | .four.columns { width: 30.6666666667%; } 67 | .five.columns { width: 39.3333333333%; } 68 | .six.columns { width: 48%; } 69 | .seven.columns { width: 56.6666666667%; } 70 | .eight.columns { width: 65.3333333333%; } 71 | .nine.columns { width: 74.0%; } 72 | .ten.columns { width: 82.6666666667%; } 73 | .eleven.columns { width: 91.3333333333%; } 74 | .twelve.columns { width: 100%; margin-left: 0; } 75 | 76 | .one-third.column { width: 30.6666666667%; } 77 | .two-thirds.column { width: 65.3333333333%; } 78 | 79 | .one-half.column { width: 48%; } 80 | 81 | /* Offsets */ 82 | .offset-by-one.column, 83 | .offset-by-one.columns { margin-left: 8.66666666667%; } 84 | .offset-by-two.column, 85 | .offset-by-two.columns { margin-left: 17.3333333333%; } 86 | .offset-by-three.column, 87 | .offset-by-three.columns { margin-left: 26%; } 88 | .offset-by-four.column, 89 | .offset-by-four.columns { margin-left: 34.6666666667%; } 90 | .offset-by-five.column, 91 | .offset-by-five.columns { margin-left: 43.3333333333%; } 92 | .offset-by-six.column, 93 | .offset-by-six.columns { margin-left: 52%; } 94 | .offset-by-seven.column, 95 | .offset-by-seven.columns { margin-left: 60.6666666667%; } 96 | .offset-by-eight.column, 97 | .offset-by-eight.columns { margin-left: 69.3333333333%; } 98 | .offset-by-nine.column, 99 | .offset-by-nine.columns { margin-left: 78.0%; } 100 | .offset-by-ten.column, 101 | .offset-by-ten.columns { margin-left: 86.6666666667%; } 102 | .offset-by-eleven.column, 103 | .offset-by-eleven.columns { margin-left: 95.3333333333%; } 104 | 105 | .offset-by-one-third.column, 106 | .offset-by-one-third.columns { margin-left: 34.6666666667%; } 107 | .offset-by-two-thirds.column, 108 | .offset-by-two-thirds.columns { margin-left: 69.3333333333%; } 109 | 110 | .offset-by-one-half.column, 111 | .offset-by-one-half.columns { margin-left: 52%; } 112 | 113 | } 114 | 115 | 116 | /* Base Styles 117 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 118 | /* NOTE 119 | html is set to 62.5% so that all the REM measurements throughout Skeleton 120 | are based on 10px sizing. So basically 1.5rem = 15px :) */ 121 | html { 122 | font-size: 62.5%; } 123 | body { 124 | font-size: 1.5em; /* currently ems cause chrome bug misinterpreting rems on body element */ 125 | line-height: 1.6; 126 | font-weight: 400; 127 | font-family: "Raleway", "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif; 128 | color: #222; } 129 | 130 | 131 | /* Typography 132 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 133 | h1, h2, h3, h4, h5, h6 { 134 | margin-top: 0; 135 | margin-bottom: 2rem; 136 | font-weight: 300; } 137 | h1 { font-size: 4.0rem; line-height: 1.2; letter-spacing: -.1rem;} 138 | h2 { font-size: 3.6rem; line-height: 1.25; letter-spacing: -.1rem; } 139 | h3 { font-size: 3.0rem; line-height: 1.3; letter-spacing: -.1rem; } 140 | h4 { font-size: 2.4rem; line-height: 1.35; letter-spacing: -.08rem; } 141 | h5 { font-size: 1.8rem; line-height: 1.5; letter-spacing: -.05rem; } 142 | h6 { font-size: 1.5rem; line-height: 1.6; letter-spacing: 0; } 143 | 144 | /* Larger than phablet */ 145 | @media (min-width: 550px) { 146 | h1 { font-size: 5.0rem; } 147 | h2 { font-size: 4.2rem; } 148 | h3 { font-size: 3.6rem; } 149 | h4 { font-size: 3.0rem; } 150 | h5 { font-size: 2.4rem; } 151 | h6 { font-size: 1.5rem; } 152 | } 153 | 154 | p { 155 | margin-top: 0; } 156 | 157 | 158 | /* Links 159 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 160 | a { 161 | color: #1EAEDB; } 162 | a:hover { 163 | color: #0FA0CE; } 164 | 165 | 166 | /* Buttons 167 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 168 | .button, 169 | button, 170 | input[type="submit"], 171 | input[type="reset"], 172 | input[type="button"] { 173 | display: inline-block; 174 | height: 38px; 175 | padding: 0 30px; 176 | color: #555; 177 | text-align: center; 178 | font-size: 11px; 179 | font-weight: 600; 180 | line-height: 38px; 181 | letter-spacing: .1rem; 182 | text-transform: uppercase; 183 | text-decoration: none; 184 | white-space: nowrap; 185 | background-color: transparent; 186 | border-radius: 4px; 187 | border: 1px solid #bbb; 188 | cursor: pointer; 189 | box-sizing: border-box; } 190 | .button:hover, 191 | button:hover, 192 | input[type="submit"]:hover, 193 | input[type="reset"]:hover, 194 | input[type="button"]:hover, 195 | .button:focus, 196 | button:focus, 197 | input[type="submit"]:focus, 198 | input[type="reset"]:focus, 199 | input[type="button"]:focus { 200 | color: #333; 201 | border-color: #888; 202 | outline: 0; } 203 | .button.button-primary, 204 | button.button-primary, 205 | input[type="submit"].button-primary, 206 | input[type="reset"].button-primary, 207 | input[type="button"].button-primary { 208 | color: #FFF; 209 | background-color: #33C3F0; 210 | border-color: #33C3F0; } 211 | .button.button-primary:hover, 212 | button.button-primary:hover, 213 | input[type="submit"].button-primary:hover, 214 | input[type="reset"].button-primary:hover, 215 | input[type="button"].button-primary:hover, 216 | .button.button-primary:focus, 217 | button.button-primary:focus, 218 | input[type="submit"].button-primary:focus, 219 | input[type="reset"].button-primary:focus, 220 | input[type="button"].button-primary:focus { 221 | color: #FFF; 222 | background-color: #1EAEDB; 223 | border-color: #1EAEDB; } 224 | 225 | 226 | /* Forms 227 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 228 | input[type="email"], 229 | input[type="number"], 230 | input[type="search"], 231 | input[type="text"], 232 | input[type="tel"], 233 | input[type="url"], 234 | input[type="password"], 235 | textarea, 236 | select { 237 | height: 38px; 238 | padding: 6px 10px; /* The 6px vertically centers text on FF, ignored by Webkit */ 239 | background-color: #fff; 240 | border: 1px solid #D1D1D1; 241 | border-radius: 4px; 242 | box-shadow: none; 243 | box-sizing: border-box; } 244 | /* Removes awkward default styles on some inputs for iOS */ 245 | input[type="email"], 246 | input[type="number"], 247 | input[type="search"], 248 | input[type="text"], 249 | input[type="tel"], 250 | input[type="url"], 251 | input[type="password"], 252 | textarea { 253 | -webkit-appearance: none; 254 | -moz-appearance: none; 255 | appearance: none; } 256 | textarea { 257 | min-height: 65px; 258 | padding-top: 6px; 259 | padding-bottom: 6px; } 260 | input[type="email"]:focus, 261 | input[type="number"]:focus, 262 | input[type="search"]:focus, 263 | input[type="text"]:focus, 264 | input[type="tel"]:focus, 265 | input[type="url"]:focus, 266 | input[type="password"]:focus, 267 | textarea:focus, 268 | select:focus { 269 | border: 1px solid #33C3F0; 270 | outline: 0; } 271 | label, 272 | legend { 273 | display: block; 274 | margin-bottom: .5rem; 275 | font-weight: 600; } 276 | fieldset { 277 | padding: 0; 278 | border-width: 0; } 279 | input[type="checkbox"], 280 | input[type="radio"] { 281 | display: inline; } 282 | label > .label-body { 283 | display: inline-block; 284 | margin-left: .5rem; 285 | font-weight: normal; } 286 | 287 | 288 | /* Lists 289 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 290 | ul { 291 | list-style: circle inside; } 292 | ol { 293 | list-style: decimal inside; } 294 | ol, ul { 295 | padding-left: 0; 296 | margin-top: 0; } 297 | ul ul, 298 | ul ol, 299 | ol ol, 300 | ol ul { 301 | margin: 1.5rem 0 1.5rem 3rem; 302 | font-size: 90%; } 303 | li { 304 | margin-bottom: 1rem; } 305 | 306 | 307 | /* Code 308 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 309 | code { 310 | padding: .2rem .5rem; 311 | margin: 0 .2rem; 312 | font-size: 90%; 313 | white-space: nowrap; 314 | background: #F1F1F1; 315 | border: 1px solid #E1E1E1; 316 | border-radius: 4px; } 317 | pre > code { 318 | display: block; 319 | padding: 1rem 1.5rem; 320 | white-space: pre; } 321 | 322 | 323 | /* Tables 324 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 325 | th, 326 | td { 327 | padding: 12px 15px; 328 | text-align: left; 329 | border-bottom: 1px solid #E1E1E1; } 330 | th:first-child, 331 | td:first-child { 332 | padding-left: 0; } 333 | th:last-child, 334 | td:last-child { 335 | padding-right: 0; } 336 | 337 | 338 | /* Spacing 339 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 340 | button, 341 | .button { 342 | margin-bottom: 1rem; } 343 | input, 344 | textarea, 345 | select, 346 | fieldset { 347 | margin-bottom: 1.5rem; } 348 | pre, 349 | blockquote, 350 | dl, 351 | figure, 352 | table, 353 | p, 354 | ul, 355 | ol, 356 | form { 357 | margin-bottom: 2.5rem; } 358 | 359 | 360 | /* Utilities 361 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 362 | .u-full-width { 363 | width: 100%; 364 | box-sizing: border-box; } 365 | .u-max-full-width { 366 | max-width: 100%; 367 | box-sizing: border-box; } 368 | .u-pull-right { 369 | float: right; } 370 | .u-pull-left { 371 | float: left; } 372 | 373 | 374 | /* Misc 375 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 376 | hr { 377 | margin-top: 3rem; 378 | margin-bottom: 3.5rem; 379 | border-width: 0; 380 | border-top: 1px solid #E1E1E1; } 381 | 382 | 383 | /* Clearing 384 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 385 | 386 | /* Self Clearing Goodness */ 387 | .container:after, 388 | .row:after, 389 | .u-cf { 390 | content: ""; 391 | display: table; 392 | clear: both; } 393 | 394 | 395 | /* Media Queries 396 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 397 | /* 398 | Note: The best way to structure the use of media queries is to create the queries 399 | near the relevant code. For example, if you wanted to change the styles for buttons 400 | on small devices, paste the mobile query code up in the buttons section and style it 401 | there. 402 | */ 403 | 404 | 405 | /* Larger than mobile */ 406 | @media (min-width: 400px) {} 407 | 408 | /* Larger than phablet (also point when grid becomes active) */ 409 | @media (min-width: 550px) {} 410 | 411 | /* Larger than tablet */ 412 | @media (min-width: 750px) {} 413 | 414 | /* Larger than desktop */ 415 | @media (min-width: 1000px) {} 416 | 417 | /* Larger than Desktop HD */ 418 | @media (min-width: 1200px) {} 419 | -------------------------------------------------------------------------------- /vendor/assets/stylesheets/Skeleton-2.0.4/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rossta/serviceworker-rails-sandbox/3d16b9e7475e56045f97908a25029aa309ed1dea/vendor/assets/stylesheets/Skeleton-2.0.4/images/favicon.png -------------------------------------------------------------------------------- /vendor/assets/stylesheets/Skeleton-2.0.4/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | Your page title here :) 9 | 10 | 11 | 12 | 14 | 15 | 16 | 18 | 19 | 20 | 22 | 23 | 24 | 25 | 27 | 28 | 29 | 30 | 31 | 32 | 34 |
35 |
36 |
37 |

Basic Page

38 |

This index.html page is a placeholder with the CSS, font and favicon. It's just waiting for you to add some content! If you need some help hit up the Skeleton documentation.

39 |
40 |
41 |
42 | 43 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /vendor/assets/stylesheets/skeleton/normalize.css: -------------------------------------------------------------------------------- 1 | ../Skeleton-2.0.4/css/normalize.css -------------------------------------------------------------------------------- /vendor/assets/stylesheets/skeleton/skeleton.css: -------------------------------------------------------------------------------- 1 | ../Skeleton-2.0.4/css/skeleton.css --------------------------------------------------------------------------------