├── log └── .keep ├── storage └── .keep ├── tmp ├── .keep ├── pids │ └── .keep └── storage │ └── .keep ├── vendor └── .keep ├── lib ├── assets │ └── .keep └── tasks │ ├── .keep │ ├── routes.rake │ └── annotate_rb.rake ├── .ruby-version ├── app ├── models │ ├── concerns │ │ └── .keep │ ├── application_record.rb │ └── click.rb ├── controllers │ ├── concerns │ │ └── .keep │ ├── application_controller.rb │ ├── vue_controller.rb │ ├── statics_controller.rb │ └── clicks_controller.rb ├── views │ ├── layouts │ │ ├── mailer.text.erb │ │ ├── mailer.html.erb │ │ └── application.html.erb │ ├── vue │ │ └── index.html.erb │ └── statics │ │ └── manifest.webmanifest.erb ├── channels │ ├── application_cable │ │ ├── channel.rb │ │ └── connection.rb │ └── clicks_channel.rb ├── mailers │ └── application_mailer.rb ├── javascript │ ├── src │ │ ├── stores │ │ │ ├── flash.ts │ │ │ └── click.ts │ │ ├── components │ │ │ ├── PageTitle.vue │ │ │ ├── LoadingAnimation.vue │ │ │ ├── GitVersion.vue │ │ │ ├── AppBackground.vue │ │ │ ├── AppHeader.vue │ │ │ ├── ClickList.vue │ │ │ ├── ClickButton.vue │ │ │ ├── AppFlash.vue │ │ │ └── AppFooter.vue │ │ ├── plugins │ │ │ └── plausible.js │ │ ├── pages │ │ │ ├── NotFound.vue │ │ │ └── HomePage.vue │ │ ├── shims-vue.d.ts │ │ ├── utils │ │ │ └── metaContent.ts │ │ ├── use │ │ │ ├── online-offline.ts │ │ │ └── fetch.ts │ │ ├── router.ts │ │ └── App.vue │ ├── channels │ │ ├── index.js │ │ └── consumer.ts │ ├── images │ │ └── logo.svg │ └── entrypoints │ │ ├── application.css │ │ └── application.ts ├── jobs │ └── application_job.rb ├── middleware │ └── cloudfront_denier.rb └── helpers │ └── application_helper.rb ├── .rspec ├── .env.test ├── bin ├── dev ├── rake ├── brakeman ├── rails ├── ci ├── bundler-audit ├── rubocop ├── yarn ├── vite ├── rspec ├── setup ├── open └── bundle ├── Procfile.dev ├── docs ├── GTmetrix.png ├── network.png ├── lighthouse.png ├── web-page-test.png ├── check-your-website.png ├── security-headers.png └── mozilla-observatory.png ├── public ├── favicon.ico ├── favicon-16x16.png ├── favicon-32x32.png ├── mstile-70x70.png ├── apple-touch-icon.png ├── mstile-144x144.png ├── mstile-150x150.png ├── mstile-310x150.png ├── mstile-310x310.png ├── maskable_icon_x384.png ├── robots.txt ├── android-chrome-192x192.png ├── android-chrome-512x512.png ├── sw.js ├── safari-pinned-tab.svg ├── 404.html ├── 400.html ├── 406-unsupported-browser.html ├── 500.html └── 422.html ├── Brewfile ├── .prettierignore ├── .yarnrc.yml ├── config ├── rorvswild.yml ├── environment.rb ├── initializers │ ├── mime_types.rb │ ├── sidekiq.rb │ ├── application_controller_renderer.rb │ ├── cookies_serializer.rb │ ├── session_store.rb │ ├── rorvswild.rb │ ├── permissions_policy.rb │ ├── wrap_parameters.rb │ ├── filter_parameter_logging.rb │ ├── backtrace_silencers.rb │ ├── connection_pool_kwargs.rb │ ├── inflections.rb │ ├── rack.rb │ └── content_security_policy.rb ├── cable.yml ├── bundler-audit.yml ├── boot.rb ├── vite.json ├── locales │ └── en.yml ├── ci.rb ├── storage.yml ├── puma.rb ├── application.rb ├── environments │ ├── test.rb │ ├── development.rb │ └── production.rb ├── database.yml └── routes.rb ├── cable └── config.ru ├── spec ├── requests │ ├── statics_spec.rb │ ├── deflater_spec.rb │ └── clicks_spec.rb ├── channels │ └── clicks_channel_spec.rb ├── javascript │ └── src │ │ ├── components │ │ ├── __snapshots__ │ │ │ └── GitVersion.test.ts.snap │ │ └── GitVersion.test.ts │ │ ├── pages │ │ ├── NotFound.test.ts │ │ ├── About.test.ts │ │ ├── __snapshots__ │ │ │ └── NotFound.test.ts.snap │ │ └── Home.test.ts │ │ ├── utils │ │ └── metaContent.test.ts │ │ ├── App.test.ts │ │ ├── stores │ │ └── click.test.ts │ │ └── __snapshots__ │ │ └── App.test.ts.snap ├── support │ └── capybara.rb ├── models │ └── click_spec.rb ├── system │ └── basic_spec.rb ├── middleware │ └── cloudfront_denier_spec.rb ├── rails_helper.rb └── spec_helper.rb ├── Rakefile ├── .prettierrc.json ├── .vscode ├── extensions.json ├── settings.json └── tasks.json ├── .env.development ├── db ├── migrate │ └── 20210522144348_create_clicks.rb ├── seeds.rb └── schema.rb ├── .gitattributes ├── config.ru ├── .env.example ├── .yarnclean ├── Dockerfile ├── .dockerignore ├── .github ├── workflows │ ├── automerge.yml │ ├── security-checks.yml │ ├── dedupe.yml │ └── ci.yml └── dependabot.yml ├── jest.config.mjs ├── docker └── startup.sh ├── vite.config.mts ├── LICENSE ├── .gitignore ├── eslint.config.mjs ├── .annotaterb.yml ├── docker-compose.yml ├── Guardfile ├── package.json ├── .rubocop.yml ├── Gemfile └── tsconfig.json /log/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /storage/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tmp/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vendor/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/assets/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/tasks/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tmp/pids/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tmp/storage/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 3.4.8 2 | -------------------------------------------------------------------------------- /app/models/concerns/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --require rails_helper 2 | -------------------------------------------------------------------------------- /app/controllers/concerns/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.env.test: -------------------------------------------------------------------------------- 1 | APP_HOST=templatus-vue.test 2 | -------------------------------------------------------------------------------- /app/views/layouts/mailer.text.erb: -------------------------------------------------------------------------------- 1 | <%= yield %> 2 | -------------------------------------------------------------------------------- /app/views/vue/index.html.erb: -------------------------------------------------------------------------------- 1 |
2 | -------------------------------------------------------------------------------- /bin/dev: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | overmind start -f Procfile.dev 4 | -------------------------------------------------------------------------------- /Procfile.dev: -------------------------------------------------------------------------------- 1 | vite: bin/vite dev 2 | browser: bin/open && while true; do sleep 60; done 3 | -------------------------------------------------------------------------------- /docs/GTmetrix.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/templatus/templatus-vue/HEAD/docs/GTmetrix.png -------------------------------------------------------------------------------- /docs/network.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/templatus/templatus-vue/HEAD/docs/network.png -------------------------------------------------------------------------------- /docs/lighthouse.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/templatus/templatus-vue/HEAD/docs/lighthouse.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/templatus/templatus-vue/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /Brewfile: -------------------------------------------------------------------------------- 1 | brew 'puma/puma/puma-dev' 2 | brew 'postgresql@17' 3 | brew 'redis' 4 | brew 'overmind' 5 | -------------------------------------------------------------------------------- /docs/web-page-test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/templatus/templatus-vue/HEAD/docs/web-page-test.png -------------------------------------------------------------------------------- /app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | end 3 | -------------------------------------------------------------------------------- /public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/templatus/templatus-vue/HEAD/public/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/templatus/templatus-vue/HEAD/public/favicon-32x32.png -------------------------------------------------------------------------------- /public/mstile-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/templatus/templatus-vue/HEAD/public/mstile-70x70.png -------------------------------------------------------------------------------- /docs/check-your-website.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/templatus/templatus-vue/HEAD/docs/check-your-website.png -------------------------------------------------------------------------------- /docs/security-headers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/templatus/templatus-vue/HEAD/docs/security-headers.png -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/templatus/templatus-vue/HEAD/public/apple-touch-icon.png -------------------------------------------------------------------------------- /public/mstile-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/templatus/templatus-vue/HEAD/public/mstile-144x144.png -------------------------------------------------------------------------------- /public/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/templatus/templatus-vue/HEAD/public/mstile-150x150.png -------------------------------------------------------------------------------- /public/mstile-310x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/templatus/templatus-vue/HEAD/public/mstile-310x150.png -------------------------------------------------------------------------------- /public/mstile-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/templatus/templatus-vue/HEAD/public/mstile-310x310.png -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | db/schema.rb 2 | tsconfig.json 3 | coverage/* 4 | public/* 5 | tmp/* 6 | .yarn/* 7 | .ruby-lsp/* 8 | -------------------------------------------------------------------------------- /app/controllers/vue_controller.rb: -------------------------------------------------------------------------------- 1 | class VueController < ApplicationController 2 | def index 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /app/models/application_record.rb: -------------------------------------------------------------------------------- 1 | class ApplicationRecord < ActiveRecord::Base 2 | primary_abstract_class 3 | end 4 | -------------------------------------------------------------------------------- /bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require_relative '../config/boot' 3 | require 'rake' 4 | Rake.application.run 5 | -------------------------------------------------------------------------------- /docs/mozilla-observatory.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/templatus/templatus-vue/HEAD/docs/mozilla-observatory.png -------------------------------------------------------------------------------- /public/maskable_icon_x384.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/templatus/templatus-vue/HEAD/public/maskable_icon_x384.png -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | enableScripts: false 2 | 3 | nodeLinker: node-modules 4 | 5 | yarnPath: .yarn/releases/yarn-4.12.0.cjs 6 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 2 | -------------------------------------------------------------------------------- /config/rorvswild.yml: -------------------------------------------------------------------------------- 1 | development: 2 | editor_url: <%= ENV.fetch("RORVSWILD_EDITOR_URL", "vscode://file${path}:${line}") %> 3 | -------------------------------------------------------------------------------- /public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/templatus/templatus-vue/HEAD/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/templatus/templatus-vue/HEAD/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /lib/tasks/routes.rake: -------------------------------------------------------------------------------- 1 | task routes: :environment do 2 | # :nocov: 3 | puts `bundle exec rails routes` 4 | # :nocov: 5 | end 6 | -------------------------------------------------------------------------------- /app/channels/application_cable/channel.rb: -------------------------------------------------------------------------------- 1 | module ApplicationCable 2 | class Channel < ActionCable::Channel::Base 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /bin/brakeman: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'rubygems' 3 | require 'bundler/setup' 4 | 5 | load Gem.bin_path('brakeman', 'brakeman') 6 | -------------------------------------------------------------------------------- /app/channels/application_cable/connection.rb: -------------------------------------------------------------------------------- 1 | module ApplicationCable 2 | class Connection < ActionCable::Connection::Base 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /app/mailers/application_mailer.rb: -------------------------------------------------------------------------------- 1 | class ApplicationMailer < ActionMailer::Base 2 | default from: 'from@example.com' 3 | layout 'mailer' 4 | end 5 | -------------------------------------------------------------------------------- /cable/config.ru: -------------------------------------------------------------------------------- 1 | # Standalone Action Cable 2 | require_relative '../config/environment' 3 | Rails.application.eager_load! 4 | 5 | run ActionCable.server 6 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require_relative 'application' 3 | 4 | # Initialize the Rails application. 5 | Rails.application.initialize! 6 | -------------------------------------------------------------------------------- /spec/requests/statics_spec.rb: -------------------------------------------------------------------------------- 1 | describe 'Statics' do 2 | it 'serves a webmanifest file' do 3 | get webmanifest_path 4 | 5 | expect(response).to have_http_status(:success) 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /bin/ci: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require_relative '../config/boot' 3 | require 'active_support/continuous_integration' 4 | 5 | CI = ActiveSupport::ContinuousIntegration 6 | require_relative '../config/ci.rb' 7 | -------------------------------------------------------------------------------- /app/controllers/statics_controller.rb: -------------------------------------------------------------------------------- 1 | class StaticsController < ApplicationController 2 | def manifest 3 | expires_in 1.day 4 | 5 | respond_to { |format| format.webmanifest { render } } 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /app/javascript/src/stores/flash.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia'; 2 | 3 | export const useFlashStore = defineStore('flash', { 4 | state: () => ({ 5 | notice: '', 6 | alert: '', 7 | }), 8 | }); 9 | -------------------------------------------------------------------------------- /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 'application/manifest+json', :webmanifest 5 | -------------------------------------------------------------------------------- /app/channels/clicks_channel.rb: -------------------------------------------------------------------------------- 1 | class ClicksChannel < ApplicationCable::Channel 2 | def subscribed 3 | stream_from 'clicks_channel' 4 | end 5 | 6 | def unsubscribed 7 | stop_all_streams 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /app/javascript/src/components/PageTitle.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /bin/bundler-audit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require_relative '../config/boot' 3 | require 'bundler/audit/cli' 4 | 5 | if ARGV.empty? || ARGV.include?('check') 6 | ARGV.concat %w[--config config/bundler-audit.yml] 7 | end 8 | Bundler::Audit::CLI.start 9 | -------------------------------------------------------------------------------- /config/cable.yml: -------------------------------------------------------------------------------- 1 | development: 2 | adapter: async 3 | 4 | test: 5 | adapter: test 6 | 7 | production: 8 | adapter: redis 9 | url: <%= ENV.fetch('REDIS_URL', 'redis://localhost:6379/1') %> 10 | channel_prefix: templatus_production 11 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require_relative 'config/application' 5 | 6 | Rails.application.load_tasks 7 | -------------------------------------------------------------------------------- /app/javascript/channels/index.js: -------------------------------------------------------------------------------- 1 | // Load all the channels within this directory and all subdirectories. 2 | // Channel files must be named *_channel.js. 3 | const channels = import.meta.glob('./**/*_channel.js'); 4 | 5 | channels.keys().forEach(channels); 6 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["@prettier/plugin-ruby", "prettier-plugin-tailwindcss"], 3 | "trailingComma": "all", 4 | "singleQuote": true, 5 | "rubyPlugins": "plugin/single_quotes,plugin/trailing_comma", 6 | "printWidth": 80, 7 | "tabWidth": 2 8 | } 9 | -------------------------------------------------------------------------------- /config/bundler-audit.yml: -------------------------------------------------------------------------------- 1 | # Audit all gems listed in the Gemfile for known security problems by running bin/bundler-audit. 2 | # CVEs that are not relevant to the application can be enumerated on the ignore list below. 3 | 4 | ignore: 5 | - CVE-THAT-DOES-NOT-APPLY 6 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "bradlc.vscode-tailwindcss", 4 | "esbenp.prettier-vscode", 5 | "orta.vscode-jest", 6 | "dbaeumer.vscode-eslint", 7 | "foxundermoon.shell-format", 8 | "github.copilot" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /.env.development: -------------------------------------------------------------------------------- 1 | # Hostname of the application 2 | APP_HOST=templatus-vue.test 3 | 4 | # Timezone 5 | TIME_ZONE=Berlin 6 | 7 | # Redis, used for Caching, ActionCable and Sidekiq 8 | REDIS_URL=redis://localhost:6379/0 9 | 10 | # Redirect to HTTPS 11 | FORCE_SSL=true 12 | -------------------------------------------------------------------------------- /spec/channels/clicks_channel_spec.rb: -------------------------------------------------------------------------------- 1 | describe ClicksChannel do 2 | it 'successfully subscribes and unsubscribes' do 3 | subscribe 4 | expect(subscription).to be_confirmed 5 | 6 | unsubscribe 7 | expect(subscription).to be_confirmed 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /config/initializers/sidekiq.rb: -------------------------------------------------------------------------------- 1 | Sidekiq.configure_client do |config| 2 | config.redis = { url: ENV.fetch('REDIS_URL', 'redis://localhost:6379/2') } 3 | end 4 | 5 | Sidekiq.configure_server do |config| 6 | config.redis = { url: ENV.fetch('REDIS_URL', 'redis://localhost:6379/2') } 7 | end 8 | -------------------------------------------------------------------------------- /config/initializers/application_controller_renderer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # ActiveSupport::Reloader.to_prepare do 4 | # ApplicationController.renderer.defaults.merge!( 5 | # http_host: 'example.org', 6 | # https: false 7 | # ) 8 | # end 9 | -------------------------------------------------------------------------------- /config/initializers/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 | -------------------------------------------------------------------------------- /lib/tasks/annotate_rb.rake: -------------------------------------------------------------------------------- 1 | # This rake task was added by annotate_rb gem. 2 | 3 | # Can set `ANNOTATERB_SKIP_ON_DB_TASKS` to be anything to skip this 4 | if Rails.env.development? && ENV['ANNOTATERB_SKIP_ON_DB_TASKS'].nil? 5 | require 'annotate_rb' 6 | 7 | AnnotateRb::Core.load_rake_tasks 8 | end 9 | -------------------------------------------------------------------------------- /bin/rubocop: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'rubygems' 3 | require 'bundler/setup' 4 | 5 | # Explicit RuboCop config increases performance slightly while avoiding config confusion. 6 | ARGV.unshift('--config', File.expand_path('../.rubocop.yml', __dir__)) 7 | 8 | load Gem.bin_path('rubocop', 'rubocop') 9 | -------------------------------------------------------------------------------- /app/jobs/application_job.rb: -------------------------------------------------------------------------------- 1 | class ApplicationJob < ActiveJob::Base 2 | # Automatically retry jobs that encountered a deadlock 3 | # retry_on ActiveRecord::Deadlocked 4 | # 5 | # Most jobs are safe to ignore if the underlying records are no longer available 6 | # discard_on ActiveJob::DeserializationError 7 | end 8 | -------------------------------------------------------------------------------- /app/views/layouts/mailer.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | 11 | <%= yield %> 12 | 13 | 14 | -------------------------------------------------------------------------------- /db/migrate/20210522144348_create_clicks.rb: -------------------------------------------------------------------------------- 1 | class CreateClicks < ActiveRecord::Migration[7.0] 2 | def change 3 | create_table :clicks do |t| 4 | t.inet :ip, null: false 5 | t.string :user_agent, null: false 6 | t.datetime :created_at, precision: 6, null: false 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /app/javascript/channels/consumer.ts: -------------------------------------------------------------------------------- 1 | // Action Cable provides the framework to deal with WebSockets in Rails. 2 | // You can generate new channels where WebSocket features live using the `bin/rails generate channel` command. 3 | 4 | import { createConsumer } from '@rails/actioncable'; 5 | 6 | export default createConsumer(); 7 | -------------------------------------------------------------------------------- /spec/javascript/src/components/__snapshots__/GitVersion.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing 2 | 3 | exports[`GitVersion matches snapshot 1`] = `
v0.0.1-123-7654321
`; 4 | -------------------------------------------------------------------------------- /config/initializers/session_store.rb: -------------------------------------------------------------------------------- 1 | Rails.application.config.session_store :cookie_store, 2 | key: 3 | "#{Rails.configuration.force_ssl ? '__Host-' : '_'}templatus_session", 4 | secure: Rails.configuration.force_ssl 5 | -------------------------------------------------------------------------------- /spec/javascript/src/pages/NotFound.test.ts: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils'; 2 | import NotFound from '@/pages/NotFound.vue'; 3 | 4 | describe('NotFound', () => { 5 | test('matches snapshot', () => { 6 | const wrapper = mount(NotFound, {}); 7 | 8 | expect(wrapper.html()).toMatchSnapshot(); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /app/javascript/src/plugins/plausible.js: -------------------------------------------------------------------------------- 1 | import { init } from '@plausible-analytics/tracker'; 2 | 3 | export default { 4 | install: (app, options) => { 5 | init({ 6 | ...options, 7 | autoCapturePageviews: true, 8 | outboundLinks: true, 9 | }); 10 | 11 | app.provide('plausible'); 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /config/initializers/rorvswild.rb: -------------------------------------------------------------------------------- 1 | if Rails.configuration.x.rorvswild.api_key 2 | RorVsWild.start( 3 | api_key: Rails.configuration.x.rorvswild.api_key, 4 | ignored_exceptions: %w[ActionController::RoutingError], 5 | deployment: { 6 | revision: Rails.configuration.x.git.commit_version, 7 | }, 8 | ) 9 | end 10 | -------------------------------------------------------------------------------- /app/javascript/src/pages/NotFound.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 12 | -------------------------------------------------------------------------------- /app/javascript/src/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import type { defineComponent } from 'vue'; 3 | const Component: ReturnType; 4 | export default Component; 5 | } 6 | 7 | declare module '@rails/request.js'; 8 | declare module '@/plugins/plausible'; 9 | 10 | declare module '*.svg' { 11 | export default '' as string; 12 | } 13 | -------------------------------------------------------------------------------- /config/boot.rb: -------------------------------------------------------------------------------- 1 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) 2 | 3 | require 'bundler/setup' # Set up gems listed in the Gemfile. 4 | require 'bootsnap/setup' # Speed up boot time by caching expensive operations. 5 | 6 | # Keyword-argument compatibility for connection_pool must be applied before Rails boots. 7 | require_relative 'initializers/connection_pool_kwargs' 8 | -------------------------------------------------------------------------------- /spec/javascript/src/pages/About.test.ts: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils'; 2 | import AboutPage from '@/pages/AboutPage.vue'; 3 | import * as Vue from 'vue'; 4 | 5 | describe('About', () => { 6 | test('matches snapshot', () => { 7 | const wrapper = mount(AboutPage, {}); 8 | 9 | expect(wrapper.html().replace(Vue.version, '3.x.y')).toMatchSnapshot(); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /config/vite.json: -------------------------------------------------------------------------------- 1 | { 2 | "all": { 3 | "sourceCodeDir": "app/javascript", 4 | "watchAdditionalPaths": [] 5 | }, 6 | "development": { 7 | "autoBuild": true, 8 | "publicOutputDir": "vite-dev", 9 | "host": "vite.templatus-vue.test", 10 | "port": 3036 11 | }, 12 | "test": { 13 | "autoBuild": true, 14 | "publicOutputDir": "vite-test", 15 | "port": 3037 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /public/sw.js: -------------------------------------------------------------------------------- 1 | self.addEventListener('install', (_event) => { 2 | // console.log('sw.js: Service worker has been installed.', event); 3 | }); 4 | 5 | self.addEventListener('activate', (_event) => { 6 | // console.log('sw.js: Service worker has been activated.', event); 7 | }); 8 | 9 | self.addEventListener('fetch', (_event) => { 10 | // console.log('sw.js: Service worker is fetching', event); 11 | }); 12 | -------------------------------------------------------------------------------- /spec/javascript/src/pages/__snapshots__/NotFound.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing 2 | 3 | exports[`NotFound matches snapshot 1`] = ` 4 |

Error 404

5 |
6 |

Page not found

7 |
8 | `; 9 | -------------------------------------------------------------------------------- /app/javascript/src/utils/metaContent.ts: -------------------------------------------------------------------------------- 1 | export function metaContent(name: string): string | undefined { 2 | const element: HTMLMetaElement | null = document.head.querySelector( 3 | `meta[name="${name}"]`, 4 | ); 5 | 6 | if (element) return element.content; 7 | } 8 | 9 | export function assetUrl(fileName: string): string { 10 | const assetHost = metaContent('asset-host') || ''; 11 | 12 | return assetHost + fileName; 13 | } 14 | -------------------------------------------------------------------------------- /spec/support/capybara.rb: -------------------------------------------------------------------------------- 1 | require 'capybara/rspec' 2 | 3 | Capybara.register_driver :my_playwright do |app| 4 | Capybara::Playwright::Driver.new( 5 | app, 6 | browser_type: ENV.fetch('PLAYWRIGHT_BROWSER', 'chromium').to_sym, 7 | headless: ENV['CI'].present? || ENV['PLAYWRIGHT_HEADLESS'].present? 8 | ) 9 | end 10 | 11 | RSpec.configure do |config| 12 | config.before(:each, type: :system) do 13 | driven_by :my_playwright 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # See https://git-scm.com/docs/gitattributes for more about git attribute files. 2 | 3 | # Mark the database schema as having been generated. 4 | db/schema.rb linguist-generated 5 | 6 | # Mark the yarn lockfile as having been generated. 7 | yarn.lock linguist-generated 8 | 9 | # Mark any vendored files as having been vendored. 10 | vendor/* linguist-vendored 11 | config/credentials/*.yml.enc diff=rails_credentials 12 | config/credentials.yml.enc diff=rails_credentials 13 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require_relative 'config/environment' 4 | 5 | if Rails.env.production? && Rails.configuration.x.app_host 6 | # Redirect to a canonical host (except for health check) 7 | use Rack::CanonicalHost, 8 | Rails.configuration.x.app_host, 9 | cache_control: 'no-cache', 10 | ignore: ->(uri) { uri.path == '/up' } 11 | end 12 | 13 | run Rails.application 14 | Rails.application.load_server 15 | -------------------------------------------------------------------------------- /config/initializers/permissions_policy.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Define an application-wide HTTP permissions policy. For further 4 | # information see: https://developers.google.com/web/updates/2018/06/feature-policy 5 | Rails.application.config.permissions_policy do |policy| 6 | policy.camera :none 7 | policy.gyroscope :none 8 | policy.microphone :none 9 | policy.usb :none 10 | policy.fullscreen :self 11 | policy.payment :self 12 | end 13 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # Hostname of the application 2 | APP_HOST=templatus-vue.example.com 3 | 4 | # Timezone 5 | TIME_ZONE=Berlin 6 | 7 | # Redis, used for Caching, ActionCable and Sidekiq 8 | REDIS_URL=redis://localhost:6379/0 9 | 10 | # Redirect to HTTPS 11 | FORCE_SSL=true 12 | 13 | # Optional: Analytics 14 | # PLAUSIBLE_URL=https://plausible.io 15 | 16 | # Optional: Honeybadger.io error tracking 17 | # HONEYBADGER_API_KEY=1234567890 18 | 19 | # Optional: RorVsWild API key 20 | # RORVSWILD_API_KEY=987654321 21 | -------------------------------------------------------------------------------- /app/javascript/images/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/models/click.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: clicks 4 | # 5 | # id :bigint not null, primary key 6 | # ip :inet not null 7 | # user_agent :string not null 8 | # created_at :datetime not null 9 | # 10 | class Click < ApplicationRecord 11 | validates :ip, presence: true 12 | validates :user_agent, presence: true 13 | 14 | # There is no `updated_at` in the database 15 | def updated_at 16 | created_at 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /spec/javascript/src/components/GitVersion.test.ts: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils'; 2 | import GitVersion from '@/components/GitVersion.vue'; 3 | 4 | describe('GitVersion', () => { 5 | test('matches snapshot', () => { 6 | const wrapper = mount(GitVersion, { 7 | props: { 8 | commitVersion: 'v0.0.1-123-7654321', 9 | commitTime: '2021-06-01T12:00:00+02:00', 10 | }, 11 | }); 12 | 13 | expect(wrapper.html()).toMatchSnapshot(); 14 | 15 | wrapper.unmount(); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /spec/javascript/src/utils/metaContent.test.ts: -------------------------------------------------------------------------------- 1 | import { metaContent } from '@/utils/metaContent'; 2 | 3 | describe('metaContent', () => { 4 | beforeAll(() => { 5 | document.head.innerHTML = ''; 6 | }); 7 | 8 | test('returns content when exists', () => { 9 | expect(metaContent('the-name')).toEqual('the-content'); 10 | }); 11 | 12 | test('returns undefined when missing', () => { 13 | expect(metaContent('this-does-not-exist')).toBeUndefined(); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /db/seeds.rb: -------------------------------------------------------------------------------- 1 | # This file should ensure the existence of records required to run the application in every environment (production, 2 | # development, test). The code here should be idempotent so that it can be executed at any point in every environment. 3 | # The data can then be loaded with the bin/rails db:seed command (or created alongside the database with db:setup). 4 | # 5 | # Example: 6 | # 7 | # ["Action", "Comedy", "Drama", "Horror"].each do |genre_name| 8 | # MovieGenre.find_or_create_by!(name: genre_name) 9 | # end 10 | -------------------------------------------------------------------------------- /spec/requests/deflater_spec.rb: -------------------------------------------------------------------------------- 1 | describe 'Rack::Deflater' do 2 | it 'compresses with gzip if requested' do 3 | ['gzip', 'deflate, gzip', 'gzip, deflate'].each do |compression_method| 4 | get root_path, headers: { HTTP_ACCEPT_ENCODING: compression_method } 5 | 6 | expect(response.headers['Content-Encoding']).to eq('gzip') 7 | end 8 | end 9 | 10 | it 'does not compress if not requested' do 11 | get root_path 12 | 13 | expect(response.headers).not_to have_key('Content-Encoding') 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /app/javascript/src/components/LoadingAnimation.vue: -------------------------------------------------------------------------------- 1 | 18 | -------------------------------------------------------------------------------- /spec/models/click_spec.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: clicks 4 | # 5 | # id :bigint not null, primary key 6 | # ip :inet not null 7 | # user_agent :string not null 8 | # created_at :datetime not null 9 | # 10 | describe Click do 11 | it 'saves payload' do 12 | click = described_class.create! ip: '1.2.3.4', user_agent: 'IRB' 13 | 14 | click.reload 15 | expect(click.ip).to eq('1.2.3.4') 16 | expect(click.user_agent).to eq('IRB') 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /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) { wrap_parameters format: [:json] } 8 | 9 | # To enable root element in JSON for ActiveRecord objects. 10 | # ActiveSupport.on_load(:active_record) do 11 | # self.include_root_in_json = true 12 | # end 13 | -------------------------------------------------------------------------------- /config/initializers/filter_parameter_logging.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Configure parameters to be partially matched (e.g. passw matches password) and filtered from the log file. 4 | # Use this to limit dissemination of sensitive information. 5 | # See the ActiveSupport::ParameterFilter documentation for supported notations and behaviors. 6 | Rails.application.config.filter_parameters += %i[ 7 | passw 8 | email 9 | secret 10 | token 11 | _key 12 | crypt 13 | salt 14 | certificate 15 | otp 16 | ssn 17 | cvv 18 | cvc 19 | ] 20 | -------------------------------------------------------------------------------- /config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 4 | # Rails.backtrace_cleaner.add_silencer { |line| /my_noisy_library/.match?(line) } 5 | 6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code 7 | # by setting BACKTRACE=1 before calling your invocation, like "BACKTRACE=1 ./bin/rails runner 'MyClass.perform'". 8 | Rails.backtrace_cleaner.remove_silencers! if ENV['BACKTRACE'] 9 | -------------------------------------------------------------------------------- /config/initializers/connection_pool_kwargs.rb: -------------------------------------------------------------------------------- 1 | # connection_pool 3.x expects keyword arguments, but some callers (e.g. ActiveSupport::Cache::RedisCacheStore) 2 | # still pass a positional Hash. Ruby 3.4 no longer converts that implicitly, so patch in support. 3 | unless defined?(ConnectionPoolKeywordCompat) 4 | require 'connection_pool' 5 | 6 | module ConnectionPoolKeywordCompat 7 | def initialize(*args, **kwargs, &) 8 | kwargs = args.first.merge(kwargs) if args.first.is_a?(Hash) 9 | super(**kwargs, &) 10 | end 11 | end 12 | 13 | ConnectionPool.prepend(ConnectionPoolKeywordCompat) 14 | end 15 | -------------------------------------------------------------------------------- /bin/yarn: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | APP_ROOT = File.expand_path('..', __dir__) 3 | Dir.chdir(APP_ROOT) do 4 | yarn = 5 | ENV['PATH'] 6 | .split(File::PATH_SEPARATOR) 7 | .select { |dir| File.expand_path(dir) != __dir__ } 8 | .product(%w[yarn yarn.cmd yarn.ps1]) 9 | .map { |dir, file| File.expand_path(file, dir) } 10 | .find { |file| File.executable?(file) } 11 | 12 | if yarn 13 | exec yarn, *ARGV 14 | else 15 | $stderr.puts 'Yarn executable was not detected in the system.' 16 | $stderr.puts 'Download Yarn at https://yarnpkg.com/en/docs/install' 17 | exit 1 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /app/middleware/cloudfront_denier.rb: -------------------------------------------------------------------------------- 1 | # Middleware to deny CloudFront requests to non-assets 2 | # http://ricostacruz.com/til/rails-and-cloudfront 3 | 4 | class CloudfrontDenier 5 | def initialize(app, target:) 6 | @app = app 7 | @target = target 8 | end 9 | 10 | def call(env) 11 | if cloudfront?(env) && !asset?(env) 12 | [302, { 'Location' => @target }, []] 13 | else 14 | @app.call(env) 15 | end 16 | end 17 | 18 | def asset?(env) 19 | env['PATH_INFO'] =~ %r{^/assets/} 20 | end 21 | 22 | def cloudfront?(env) 23 | env['HTTP_USER_AGENT'] == 'Amazon CloudFront' 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /.yarnclean: -------------------------------------------------------------------------------- 1 | # test directories 2 | __tests__ 3 | test 4 | tests 5 | powered-test 6 | 7 | # asset directories 8 | docs 9 | doc 10 | website 11 | images 12 | 13 | # examples 14 | example 15 | examples 16 | 17 | # code coverage directories 18 | coverage 19 | .nyc_output 20 | 21 | # build scripts 22 | Makefile 23 | Gulpfile.js 24 | Gruntfile.js 25 | 26 | # configs 27 | appveyor.yml 28 | circle.yml 29 | codeship-services.yml 30 | codeship-steps.yml 31 | wercker.yml 32 | .tern-project 33 | .gitattributes 34 | .editorconfig 35 | .*ignore 36 | .eslintrc 37 | .jshintrc 38 | .flowconfig 39 | .documentup.json 40 | .yarn-metadata.json 41 | .travis.yml 42 | 43 | # misc 44 | *.md 45 | -------------------------------------------------------------------------------- /app/javascript/entrypoints/application.css: -------------------------------------------------------------------------------- 1 | @import 'tailwindcss'; 2 | @source '../../../app/**/*.{html,erb.html,rb}'; 3 | @source '../../../app/javascript/**/*.{vue,js,ts}'; 4 | @plugin '@tailwindcss/forms'; 5 | 6 | @theme { 7 | --color-rails-light: oklch(54.44% 0.2233 29.15); 8 | --color-rails-dark: oklch(32.7% 0.1342 29.23); 9 | --color-vue: oklch(70.25% 0.132 160.37); 10 | --color-rose: oklch(93.56% 0.0101 1.99); 11 | } 12 | 13 | @layer base { 14 | .gradient__rails-light { 15 | stop-color: var(--color-rails-light); 16 | } 17 | 18 | .gradient__rails-dark { 19 | stop-color: var(--color-rails-dark); 20 | } 21 | 22 | .gradient__vue { 23 | stop-color: var(--color-vue); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/javascript/src/use/online-offline.ts: -------------------------------------------------------------------------------- 1 | import { Ref, ref, onMounted, onUnmounted } from 'vue'; 2 | 3 | export default function useOnlineOffline(): { online: Ref } { 4 | const online = ref(navigator.onLine); 5 | 6 | function setOnline() { 7 | online.value = true; 8 | } 9 | 10 | function setOffline() { 11 | online.value = false; 12 | } 13 | 14 | onMounted(() => { 15 | window.addEventListener('online', setOnline); 16 | window.addEventListener('offline', setOffline); 17 | }); 18 | 19 | onUnmounted(() => { 20 | window.removeEventListener('online', setOnline); 21 | window.removeEventListener('offline', setOffline); 22 | }); 23 | 24 | return { 25 | online, 26 | }; 27 | } 28 | -------------------------------------------------------------------------------- /config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format. Inflections 4 | # are locale specific, and you may define rules for as many different 5 | # locales as you wish. All of these examples are active by default: 6 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 7 | # inflect.plural /^(ox)$/i, "\\1en" 8 | # inflect.singular /^(ox)en/i, "\\1" 9 | # inflect.irregular 'person', 'people' 10 | # inflect.uncountable %w( fish sheep ) 11 | # end 12 | 13 | # These inflection rules are supported but not enabled by default: 14 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 15 | # inflect.acronym 'RESTful' 16 | # end 17 | -------------------------------------------------------------------------------- /app/javascript/src/router.ts: -------------------------------------------------------------------------------- 1 | import { createWebHashHistory, createRouter, RouteRecordRaw } from 'vue-router'; 2 | import HomePage from '@/pages/HomePage.vue'; 3 | import AboutPage from '@/pages/AboutPage.vue'; 4 | import NotFound from '@/pages/NotFound.vue'; 5 | 6 | const routes: Array = [ 7 | { 8 | path: '/', 9 | name: 'Home', 10 | component: HomePage, 11 | props: { 12 | name: 'Templatus', 13 | }, 14 | }, 15 | { 16 | path: '/about', 17 | name: 'About', 18 | component: AboutPage, 19 | }, 20 | { path: '/:pathMatch(.*)', component: NotFound }, 21 | ]; 22 | 23 | const router = createRouter({ 24 | history: createWebHashHistory(), 25 | routes, 26 | }); 27 | 28 | export default router; 29 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | # check=error=true 3 | 4 | ARG SKIP_BOOTSNAP_PRECOMPILE=true 5 | 6 | FROM ghcr.io/ledermann/rails-base-builder:3.4.8-alpine AS builder 7 | 8 | # Remove some files not needed in resulting image. 9 | # Because they are required for building the image, they can't be added to .dockerignore 10 | RUN rm -r package.json tsconfig.json vite.config.mts 11 | 12 | FROM ghcr.io/ledermann/rails-base-final:3.4.8-alpine 13 | LABEL maintainer="georg@ledermann.dev" 14 | 15 | USER app 16 | 17 | # Enable YJIT 18 | ENV RUBY_YJIT_ENABLE=1 19 | 20 | # Entrypoint prepares the database. 21 | ENTRYPOINT ["docker/startup.sh"] 22 | 23 | # Start the server by default, this can be overwritten at runtime 24 | CMD ["./bin/rails", "server"] 25 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .byebug_history 2 | .dockerignore 3 | .env* 4 | .eslintignore 5 | .eslintrc.js 6 | .eslintcache 7 | .git 8 | .github 9 | .gitignore 10 | .gitattributes 11 | .rspec 12 | .rubocop.yml 13 | .vscode 14 | .yarnclean 15 | .prettierignore 16 | .prettierrc.json 17 | docs 18 | coverage 19 | **/*.DS_Store 20 | Brewfile 21 | Brewfile.lock.json 22 | Dockerfile 23 | docker-compose.yml 24 | docker-volumes 25 | Procfile* 26 | Guardfile 27 | log/* 28 | node_modules 29 | public/assets 30 | tmp/* 31 | !/tmp/pids/ 32 | !/tmp/pids/.keep 33 | yarn-error.log 34 | config/environments/test.rb 35 | config/environments/development.rb 36 | jest.config.mjs 37 | spec 38 | README.md 39 | bin/bundle 40 | bin/dev 41 | bin/rspec 42 | bin/rubocop 43 | bin/setup 44 | bin/yarn 45 | bin/open 46 | -------------------------------------------------------------------------------- /app/javascript/src/components/GitVersion.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 34 | -------------------------------------------------------------------------------- /app/views/statics/manifest.webmanifest.erb: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Templatus (Vue edition)", 3 | "short_name": "Templatus", 4 | "start_url": "/", 5 | "display": "standalone", 6 | "orientation": "portrait", 7 | "icons": [ 8 | { 9 | "src": "<%= asset_path('/android-chrome-192x192.png') %>", 10 | "sizes": "192x192", 11 | "type": "image/png" 12 | }, 13 | { 14 | "src": "<%= asset_path('/android-chrome-512x512.png') %>", 15 | "sizes": "512x512", 16 | "type": "image/png" 17 | }, 18 | { 19 | "src": "<%= asset_path('/maskable_icon_x384.png') %>", 20 | "sizes": "384x384", 21 | "type": "image/png", 22 | "purpose": "maskable" 23 | } 24 | ], 25 | "theme_color": "#991c1c", 26 | "background_color": "#ffffff" 27 | } 28 | -------------------------------------------------------------------------------- /.github/workflows/automerge.yml: -------------------------------------------------------------------------------- 1 | name: Dependabot auto-merge 2 | on: pull_request_target 3 | 4 | permissions: 5 | contents: write 6 | pull-requests: write 7 | 8 | jobs: 9 | dependabot: 10 | runs-on: ubuntu-latest 11 | if: ${{ github.actor == 'dependabot[bot]' }} 12 | steps: 13 | - name: Dependabot metadata 14 | id: metadata 15 | uses: dependabot/fetch-metadata@v2.4.0 16 | with: 17 | github-token: '${{ secrets.PAT }}' 18 | 19 | - name: Enable auto-merge for Dependabot PRs 20 | if: ${{ steps.metadata.outputs.update-type != 'version-update:semver-major' }} 21 | run: gh pr merge --auto --squash "$PR_URL" 22 | env: 23 | PR_URL: ${{github.event.pull_request.html_url}} 24 | GITHUB_TOKEN: ${{secrets.PAT}} 25 | -------------------------------------------------------------------------------- /jest.config.mjs: -------------------------------------------------------------------------------- 1 | export default { 2 | verbose: true, 3 | roots: ['spec/javascript'], 4 | moduleNameMapper: { 5 | '^@/(.*)$': '/app/javascript/src/$1', 6 | '^@test/(.*)$': '/spec/javascript/src/$1', 7 | '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': 8 | 'jest-transform-stub', 9 | }, 10 | moduleFileExtensions: ['js', 'ts', 'json', 'vue'], 11 | preset: 'ts-jest', 12 | testMatch: ['**/?(*.)+(spec|test).+(ts|tsx)'], 13 | transform: { 14 | '^.+\\.tsx?$': 'ts-jest', 15 | '^.+\\.vue$': '@vue/vue3-jest', 16 | }, 17 | testEnvironment: 'jsdom', 18 | testEnvironmentOptions: { 19 | url: 'https://templatus-vue.test/', 20 | customExportConditions: ['node', 'node-addons'], 21 | }, 22 | snapshotSerializers: ['/node_modules/jest-serializer-vue'], 23 | }; 24 | -------------------------------------------------------------------------------- /bin/vite: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # 5 | # This file was generated by Bundler. 6 | # 7 | # The application 'vite' is installed as part of a gem, and 8 | # this file is here to facilitate running it. 9 | # 10 | 11 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) 12 | 13 | bundle_binstub = File.expand_path('bundle', __dir__) 14 | 15 | if File.file?(bundle_binstub) 16 | if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/ 17 | load(bundle_binstub) 18 | else 19 | abort( 20 | 'Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. 21 | Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.', 22 | ) 23 | end 24 | end 25 | 26 | require 'rubygems' 27 | require 'bundler/setup' 28 | 29 | load Gem.bin_path('vite_ruby', 'vite') 30 | -------------------------------------------------------------------------------- /spec/system/basic_spec.rb: -------------------------------------------------------------------------------- 1 | describe 'Basic navigation and interaction' do 2 | it 'can navigate between pages' do 3 | visit '/' 4 | expect(page).to have_content('Hello') 5 | 6 | click_on 'About' 7 | expect(page).to have_selector('main h1', text: 'About') 8 | 9 | click_on 'Home' 10 | expect(page).to have_selector('main h1', text: 'Hello') 11 | end 12 | 13 | it 'can click button and see updates' do 14 | visit '/' 15 | expect(page).to have_selector('main h1', text: 'Hello, Templatus!') 16 | expect(page).to have_selector('#counter', text: '0') 17 | 18 | click_button 'Click me!' 19 | 20 | expect(page).to have_selector('#counter', text: '1') 21 | expect(page).to have_selector('ul', text: '127.0.0.0') 22 | expect(page).to have_selector('li', count: 1) 23 | expect(page).to have_selector('#flash', text: 'successfully') 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /docker/startup.sh: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | 3 | echo "Starting ..." 4 | echo "Git commit: $COMMIT_VERSION - $COMMIT_TIME" 5 | echo "----------------" 6 | 7 | # Wait for Redis 8 | redis_host=$(echo "$REDIS_URL" | awk -F[/:] '{print $4}') 9 | redis_port=$(echo "$REDIS_URL" | awk -F[/:] '{print $5}') 10 | until nc -z -v -w30 "$redis_host" "$redis_port"; do 11 | echo "Waiting for Redis on $redis_host:$redis_port ..." 12 | sleep 1 13 | done 14 | echo "Redis is up and running!" 15 | 16 | # Wait for PostgreSQL 17 | until nc -z -v -w30 "$DB_HOST" 5432; do 18 | echo "Waiting for PostgreSQL on $DB_HOST:5432 ..." 19 | sleep 1 20 | done 21 | echo "PostgreSQL is up and running!" 22 | 23 | # If running the rails server then create or migrate existing database 24 | if [ "${*}" == "./bin/rails server" ]; then 25 | echo "Preparing database..." 26 | ./bin/rails db:prepare 27 | echo "Database is ready!" 28 | fi 29 | 30 | exec "${@}" 31 | -------------------------------------------------------------------------------- /app/javascript/src/App.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 29 | -------------------------------------------------------------------------------- /app/javascript/src/use/fetch.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import { FetchRequest } from '@rails/request.js'; 3 | import { useFlashStore } from '@/stores/flash'; 4 | 5 | export function withFlash(request: Promise) { 6 | const flash = useFlashStore(); 7 | 8 | return request.then(async (response: { json: any; ok: any }) => { 9 | const body = await response.json; 10 | 11 | if (response.ok) flash.notice = body.notice; 12 | else flash.alert = body.alert; 13 | 14 | setTimeout(() => flash.$reset(), 2000); 15 | 16 | return response; 17 | }); 18 | } 19 | 20 | export function get(url: string, options: any) { 21 | const request = new FetchRequest('get', url, options); 22 | return withFlash(request.perform()); 23 | } 24 | 25 | export function post(url: string, options: any) { 26 | const request = new FetchRequest('post', url, options); 27 | return withFlash(request.perform()); 28 | } 29 | -------------------------------------------------------------------------------- /bin/rspec: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # 5 | # This file was generated by Bundler. 6 | # 7 | # The application 'rspec' is installed as part of a gem, and 8 | # this file is here to facilitate running it. 9 | # 10 | 11 | require 'pathname' 12 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path( 13 | '../../Gemfile', 14 | Pathname.new(__FILE__).realpath, 15 | ) 16 | 17 | bundle_binstub = File.expand_path('../bundle', __FILE__) 18 | 19 | if File.file?(bundle_binstub) 20 | if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/ 21 | load(bundle_binstub) 22 | else 23 | abort( 24 | 'Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. 25 | Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.', 26 | ) 27 | end 28 | end 29 | 30 | require 'rubygems' 31 | require 'bundler/setup' 32 | 33 | load Gem.bin_path('rspec-core', 'rspec') 34 | -------------------------------------------------------------------------------- /app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | def versions 3 | { 4 | alpine: alpine_version, 5 | ruby: ruby_version, 6 | rails: rails_version, 7 | puma: puma_version, 8 | postgres: postgres_version, 9 | redis: redis_version, 10 | sidekiq: sidekiq_version, 11 | } 12 | end 13 | 14 | private 15 | 16 | def alpine_version 17 | return if RUBY_PLATFORM.exclude?('linux') 18 | 19 | `cat /etc/alpine-release 2>/dev/null`.chomp 20 | end 21 | 22 | def ruby_version 23 | RUBY_VERSION 24 | end 25 | 26 | def rails_version 27 | Rails.version 28 | end 29 | 30 | def puma_version 31 | Puma::Const::PUMA_VERSION 32 | end 33 | 34 | def postgres_version 35 | ActiveRecord::Base.connection.select_value('SHOW server_version;') 36 | end 37 | 38 | def redis_version 39 | Redis.new.info['redis_version'] 40 | end 41 | 42 | def sidekiq_version 43 | Sidekiq::VERSION 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /vite.config.mts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import ViteRails from 'vite-plugin-rails'; 3 | import tailwindcss from '@tailwindcss/vite'; 4 | import vue from '@vitejs/plugin-vue'; 5 | import { resolve } from 'path'; 6 | 7 | export default defineConfig({ 8 | build: { 9 | assetsInlineLimit: 0, 10 | rollupOptions: { 11 | output: { 12 | manualChunks(id) { 13 | if (id.includes('node_modules')) { 14 | return 'vendor'; 15 | } 16 | }, 17 | }, 18 | }, 19 | }, 20 | plugins: [ 21 | tailwindcss(), 22 | ViteRails({ 23 | fullReload: { 24 | additionalPaths: ['config/routes.rb', 'app/views/**/*'], 25 | }, 26 | }), 27 | vue(), 28 | ], 29 | resolve: { 30 | alias: { 31 | '@': resolve(__dirname, 'app/javascript/src'), 32 | }, 33 | }, 34 | server: { 35 | hmr: { 36 | host: 'vite.templatus-vue.test', 37 | clientPort: 443, 38 | }, 39 | }, 40 | }); 41 | -------------------------------------------------------------------------------- /config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Files in the config/locales directory are used for internationalization and 2 | # are automatically loaded by Rails. If you want to use locales other than 3 | # 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 about the API, please read the Rails Internationalization guide 20 | # at https://guides.rubyonrails.org/i18n.html. 21 | # 22 | # Be aware that YAML interprets the following case-insensitive strings as 23 | # booleans: `true`, `false`, `on`, `off`, `yes`, `no`. Therefore, these strings 24 | # must be quoted to be interpreted as strings. For example: 25 | # 26 | # en: 27 | # "yes": yup 28 | # enabled: 'ON' 29 | 30 | en: 31 | hello: 'Hello world' 32 | -------------------------------------------------------------------------------- /config/ci.rb: -------------------------------------------------------------------------------- 1 | # Run using bin/ci 2 | 3 | CI.run do 4 | step 'Setup', 'bin/setup --skip-server' 5 | 6 | step 'Style: Ruby', 'bin/rubocop' 7 | step 'Style: JavaScript', 'bin/yarn lint' 8 | step 'Style: TypeScript', 'bin/yarn tsc' 9 | 10 | step 'Security: Brakeman code analysis', 11 | 'bin/brakeman --quiet --no-pager --exit-on-warn --exit-on-error' 12 | step 'Security: Gem audit', 'bin/bundler-audit' 13 | step 'Security: Yarn vulnerability audit', 'bin/yarn npm audit' 14 | 15 | step 'Tests: JavaScript', 'bin/yarn test' 16 | step 'Tests: Rails', 'PLAYWRIGHT_HEADLESS=true bin/rspec' 17 | step 'Tests: Seeds', 'env RAILS_ENV=test bin/rails db:seed:replant' 18 | 19 | # Optional: set a green GitHub commit status to unblock PR merge. 20 | # Requires the `gh` CLI and `gh extension install basecamp/gh-signoff`. 21 | # if success? 22 | # step "Signoff: All systems go. Ready for merge and deploy.", "gh signoff" 23 | # else 24 | # failure "Signoff: CI failed. Do not merge or deploy.", "Fix the issues and try again." 25 | # end 26 | end 27 | -------------------------------------------------------------------------------- /.github/workflows/security-checks.yml: -------------------------------------------------------------------------------- 1 | name: Security Checks 2 | on: 3 | schedule: 4 | # Run daily at 4:00 AM UTC 5 | - cron: "0 4 * * *" 6 | workflow_dispatch: 7 | 8 | concurrency: 9 | group: ${{ github.workflow }}-${{ github.ref }} 10 | cancel-in-progress: true 11 | 12 | jobs: 13 | ruby-security: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v6 18 | 19 | - name: Set up Ruby 20 | uses: ruby/setup-ruby@v1 21 | with: 22 | bundler-cache: true 23 | 24 | - name: Run bundler-audit 25 | run: bin/bundler-audit 26 | 27 | yarn-security: 28 | runs-on: ubuntu-latest 29 | 30 | steps: 31 | - uses: actions/checkout@v6 32 | 33 | - name: Setup Node.js 34 | uses: actions/setup-node@v6 35 | with: 36 | cache: yarn 37 | node-version-file: "package.json" 38 | 39 | - name: Install Yarn packages 40 | run: bin/yarn install --immutable 41 | 42 | - name: Run security audit for Yarn packages 43 | run: bin/yarn npm audit 44 | -------------------------------------------------------------------------------- /db/schema.rb: -------------------------------------------------------------------------------- 1 | # This file is auto-generated from the current state of the database. Instead 2 | # of editing this file, please use the migrations feature of Active Record to 3 | # incrementally modify your database, and then regenerate this schema definition. 4 | # 5 | # This file is the source Rails uses to define your schema when running `bin/rails 6 | # db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to 7 | # be faster and is potentially less error prone than running all of your 8 | # migrations from scratch. Old migrations may fail to apply correctly if those 9 | # migrations use external dependencies or application code. 10 | # 11 | # It's strongly recommended that you check this file into your version control system. 12 | 13 | ActiveRecord::Schema[8.1].define(version: 2021_05_22_144348) do 14 | # These are extensions that must be enabled in order to support this database 15 | enable_extension "pg_catalog.plpgsql" 16 | 17 | create_table "clicks", force: :cascade do |t| 18 | t.datetime "created_at", null: false 19 | t.inet "ip", null: false 20 | t.string "user_agent", null: false 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021-2023 Georg Ledermann 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /config/initializers/rack.rb: -------------------------------------------------------------------------------- 1 | # Remove 'x-runtime' header 2 | Rails.application.config.middleware.delete(Rack::Runtime) 3 | 4 | # Add content_type for some file extension not included in the 5 | # list of defaults: https://github.com/rack/rack/blob/master/lib/rack/mime.rb 6 | Rack::Mime::MIME_TYPES['.webmanifest'] = 'application/manifest+json' 7 | 8 | unless Rails.env.development? 9 | # Enable gzip compression 10 | Rails.application.config.middleware.use Rack::Deflater 11 | end 12 | 13 | if Rails.application.config.x.app_host 14 | # Allow serving of images, stylesheets, and JavaScripts from the app_host only 15 | Rails.application.config.middleware.insert_before 0, Rack::Cors do 16 | allow do 17 | origins Rails.application.config.x.app_host 18 | resource '*', headers: :any, methods: %i[get post options] 19 | end 20 | end 21 | 22 | # CDN: Allow Cloudfront for assets only 23 | if Rails.application.config.asset_host 24 | require './app/middleware/cloudfront_denier' 25 | 26 | Rails.application.config.middleware.use CloudfrontDenier, 27 | target: "https://#{Rails.application.config.x.app_host}" 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /app/controllers/clicks_controller.rb: -------------------------------------------------------------------------------- 1 | class ClicksController < ApplicationController 2 | def index 3 | clicks = Click.order(created_at: :desc).limit(5).to_a 4 | return unless stale?(clicks, template: false, public: true) 5 | 6 | expires_in 0, must_revalidate: true 7 | 8 | respond_to do |format| 9 | format.json { render json: { total: Click.count, items: clicks } } 10 | end 11 | end 12 | 13 | def create 14 | click = 15 | Click.create! user_agent: request.user_agent, 16 | ip: anonymize(request.remote_ip) 17 | ActionCable.server.broadcast 'clicks_channel', click 18 | 19 | render json: { 20 | notice: 'Click was successfully recorded.', 21 | }, 22 | status: :created 23 | rescue StandardError 24 | render json: { 25 | alert: 'Click recording failed!', 26 | }, 27 | status: :unprocessable_content 28 | end 29 | 30 | private 31 | 32 | def anonymize(ip) 33 | addr = IPAddr.new(ip.to_s) 34 | if addr.ipv4? 35 | # set last octet to 0 36 | addr.mask(24) 37 | else 38 | # set last 80 bits to zeros 39 | addr.mask(48) 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.defaultFormatter": "esbenp.prettier-vscode", 3 | "editor.formatOnSave": true, 4 | "editor.tabSize": 2, 5 | 6 | "files.trimTrailingWhitespace": true, 7 | "files.watcherExclude": { 8 | "**/tmp/**": true, 9 | "**/public/vite*": true 10 | }, 11 | "files.exclude": { 12 | "**/docker-volumes": true 13 | }, 14 | 15 | "search.exclude": { 16 | "**/.yarn": true, 17 | "**/tmp": true, 18 | "**/public/vite*": true, 19 | "**/node_modules": true 20 | }, 21 | 22 | "jest.jestCommandLine": "yarn test", 23 | 24 | "eslint.validate": ["javascript", "vue"], 25 | 26 | "ruby.lint": { 27 | "rubocop": { 28 | "forceExclusion": true 29 | } 30 | }, 31 | 32 | "[ruby]": { 33 | "editor.defaultFormatter": "esbenp.prettier-vscode", 34 | "editor.formatOnSave": true 35 | }, 36 | 37 | "[shellscript]": { 38 | "editor.defaultFormatter": "foxundermoon.shell-format" 39 | }, 40 | "[ignore]": { 41 | "editor.defaultFormatter": "foxundermoon.shell-format" 42 | }, 43 | "[dotenv]": { 44 | "editor.defaultFormatter": "foxundermoon.shell-format" 45 | }, 46 | "[vue]": { 47 | "editor.defaultFormatter": "esbenp.prettier-vscode" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /app/javascript/src/components/AppBackground.vue: -------------------------------------------------------------------------------- 1 | 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files for more about ignoring files. 2 | # 3 | # If you find yourself ignoring temporary files generated by your text editor 4 | # or operating system, you probably want to add a global ignore instead: 5 | # git config --global core.excludesfile '~/.gitignore_global' 6 | 7 | # Ignore bundler config. 8 | /.bundle 9 | 10 | # Ignore all logfiles and tempfiles. 11 | /log/* 12 | /tmp/* 13 | !/log/.keep 14 | !/tmp/.keep 15 | 16 | # Ignore pidfiles, but keep the directory. 17 | /tmp/pids/* 18 | !/tmp/pids/ 19 | !/tmp/pids/.keep 20 | 21 | # Ignore uploaded files in development. 22 | /storage/* 23 | !/storage/.keep 24 | /tmp/storage/* 25 | !/tmp/storage/ 26 | !/tmp/storage/.keep 27 | 28 | # Ignore master key for decrypting credentials and more. 29 | /config/master.key 30 | 31 | public/assets 32 | /node_modules 33 | .eslintcache 34 | 35 | # dotenv 36 | .env 37 | 38 | # SimpleCoverage report 39 | /coverage/* 40 | 41 | # Docker volumes 42 | /docker-volumes 43 | !/docker-volumes/.keep 44 | 45 | /public/vite* 46 | 47 | Brewfile.lock.json 48 | 49 | # https://yarnpkg.com/getting-started/qa#which-files-should-be-gitignored 50 | .pnp.* 51 | .yarn/* 52 | !.yarn/patches 53 | !.yarn/plugins 54 | !.yarn/releases 55 | !.yarn/sdks 56 | !.yarn/versions 57 | -------------------------------------------------------------------------------- /config/storage.yml: -------------------------------------------------------------------------------- 1 | test: 2 | service: Disk 3 | root: <%= Rails.root.join("tmp/storage") %> 4 | 5 | local: 6 | service: Disk 7 | root: <%= Rails.root.join("storage") %> 8 | # 9 | # Use bin/rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key) 10 | # amazon: 11 | # service: S3 12 | # access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %> 13 | # secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %> 14 | # region: us-east-1 15 | # bucket: your_own_bucket-<%= Rails.env %> 16 | 17 | # Remember not to checkin your GCS keyfile to a repository 18 | # google: 19 | # service: GCS 20 | # project: your_project 21 | # credentials: <%= Rails.root.join("path/to/gcs.keyfile") %> 22 | # bucket: your_own_bucket-<%= Rails.env %> 23 | 24 | # Use bin/rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key) 25 | # microsoft: 26 | # service: AzureStorage 27 | # storage_account_name: your_account_name 28 | # storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %> 29 | # container: your_container_name-<%= Rails.env %> 30 | 31 | # mirror: 32 | # service: Mirror 33 | # primary: local 34 | # mirrors: [ amazon, google, microsoft ] 35 | -------------------------------------------------------------------------------- /app/javascript/src/components/AppHeader.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 45 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "Development server", 6 | "command": "bin/dev", 7 | "args": [], 8 | "isBackground": true, 9 | "problemMatcher": [] 10 | }, 11 | { 12 | "label": "guard-rspec", 13 | "command": "bundle", 14 | "args": ["exec", "guard"], 15 | "isBackground": true, 16 | "problemMatcher": { 17 | "applyTo": "allDocuments", 18 | "owner": "Ruby", 19 | "fileLocation": ["relative", "${workspaceRoot}"], 20 | "pattern": [ 21 | { 22 | "regexp": "^(Error|Warning|Info):.*$", 23 | "severity": 1 24 | }, 25 | { 26 | "regexp": "^\\s*[^#]+#[^:]+:$", 27 | "message": 0 28 | }, 29 | { 30 | "regexp": "^\\s*([^:]+):(.*)$", 31 | "message": 5 32 | }, 33 | { 34 | "regexp": "^ ([^:]+):(\\d+):in (.+)$", 35 | "file": 1, 36 | "location": 2, 37 | "code": 3 38 | } 39 | ], 40 | "background": { 41 | "activeOnStart": true, 42 | "beginsPattern": "^# Running:$", 43 | "endsPattern": "^\\d+ runs.*$" 44 | } 45 | }, 46 | "group": { 47 | "kind": "test", 48 | "isDefault": true 49 | } 50 | } 51 | ] 52 | } 53 | -------------------------------------------------------------------------------- /spec/javascript/src/App.test.ts: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils'; 2 | import { setActivePinia, createPinia } from 'pinia'; 3 | import { createRouter, createWebHistory } from 'vue-router'; 4 | 5 | import App from '@/App.vue'; 6 | 7 | const router = createRouter({ 8 | history: createWebHistory(), 9 | routes: [ 10 | { 11 | path: '/', 12 | component: { template: 'Home page' }, 13 | }, 14 | { 15 | path: '/about', 16 | component: { template: 'About page' }, 17 | }, 18 | ], 19 | }); 20 | 21 | describe('App', () => { 22 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 23 | let wrapper: any; 24 | 25 | beforeEach(() => { 26 | setActivePinia(createPinia()); 27 | 28 | wrapper = mount(App, { 29 | global: { 30 | plugins: [router], 31 | }, 32 | }); 33 | }); 34 | 35 | afterEach(() => { 36 | wrapper.unmount(); 37 | }); 38 | 39 | test('matches snapshot', () => { 40 | expect(wrapper.html()).toMatchSnapshot(); 41 | }); 42 | 43 | test('recognizes online/offline', async () => { 44 | window.dispatchEvent(new window.Event('offline')); 45 | await wrapper.vm.$nextTick(); 46 | expect(wrapper.html()).toContain('offline'); 47 | 48 | window.dispatchEvent(new window.Event('online')); 49 | await wrapper.vm.$nextTick(); 50 | expect(wrapper.html()).not.toContain('offline'); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'fileutils' 3 | 4 | APP_ROOT = File.expand_path('..', __dir__) 5 | 6 | def system!(*args) 7 | system(*args, exception: true) 8 | end 9 | 10 | FileUtils.chdir APP_ROOT do 11 | # This script is a way to set up or update your development environment automatically. 12 | # This script is idempotent, so that you can run it at any time and get an expectable outcome. 13 | # Add necessary setup steps to this file. 14 | 15 | puts '== Installing dependencies ==' 16 | system('bundle check') || system!('bundle install') 17 | 18 | # Install JavaScript dependencies 19 | system! 'bin/yarn' 20 | 21 | # puts "\n== Copying sample files ==" 22 | # unless File.exist?('config/database.yml') 23 | # FileUtils.cp 'config/database.yml.sample', 'config/database.yml' 24 | # end 25 | 26 | puts "\n== Copying env file ==" 27 | FileUtils.cp '.env.example', '.env' unless File.exist?('.env') 28 | 29 | puts "\n== Preparing database ==" 30 | system! 'bin/rails db:prepare' 31 | system! 'bin/rails db:reset' if ARGV.include?('--reset') 32 | 33 | puts "\n== Removing old logs, tempfiles, and assets ==" 34 | system! 'bin/rails log:clear tmp:clear assets:clobber' 35 | 36 | unless ARGV.include?('--skip-server') 37 | puts "\n== Restarting application server ==" 38 | STDOUT.flush # flush the output before exec(2) so that it displays 39 | exec 'bin/rails restart' 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import eslint from '@eslint/js'; 2 | import tseslint from 'typescript-eslint'; 3 | 4 | import pluginPrettierRecommended from 'eslint-plugin-prettier/recommended'; 5 | import pluginVue from 'eslint-plugin-vue'; 6 | import pluginJest from 'eslint-plugin-jest'; 7 | 8 | import vueTsEslintConfig from '@vue/eslint-config-typescript'; 9 | import prettierConfig from '@vue/eslint-config-prettier'; 10 | 11 | export default [ 12 | eslint.configs.recommended, 13 | ...tseslint.configs.recommended, 14 | pluginPrettierRecommended, 15 | ...pluginVue.configs['flat/recommended'], 16 | ...vueTsEslintConfig(), 17 | { 18 | files: ['spec/javascript/**'], 19 | ...pluginJest.configs['flat/recommended'], 20 | rules: { 21 | ...pluginJest.configs['flat/recommended'].rules, 22 | 'jest/prefer-expect-assertions': 'off', 23 | }, 24 | }, 25 | { 26 | rules: { 27 | '@typescript-eslint/no-unused-vars': [ 28 | 'error', 29 | { 30 | argsIgnorePattern: '^_', 31 | }, 32 | ], 33 | '@typescript-eslint/no-var-requires': 'off', 34 | 'vue/no-v-html': 'off', 35 | }, 36 | }, 37 | { 38 | ignores: [ 39 | '.ruby-lsp/', 40 | '.yarn/', 41 | 'config/', 42 | 'coverage/', 43 | 'db/', 44 | 'log/', 45 | 'node_modules/', 46 | 'public/', 47 | 'tmp/', 48 | 'vendor/', 49 | ], 50 | }, 51 | prettierConfig, 52 | ]; 53 | -------------------------------------------------------------------------------- /spec/middleware/cloudfront_denier_spec.rb: -------------------------------------------------------------------------------- 1 | describe CloudfrontDenier do 2 | subject(:middleware) { described_class.new(app, target: host) } 3 | 4 | let(:host) { 'https://example.com' } 5 | let(:app) { ->(env) { [200, env, 'OK'] } } 6 | 7 | it 'accepts request for asset' do 8 | code, = middleware.call env_for('http://example.com/assets/example.js') 9 | 10 | expect(code).to eq(200) 11 | end 12 | 13 | it 'accepts request for asset from CloudFront' do 14 | code, = 15 | middleware.call env_for( 16 | 'http://example.com/assets/example.js', 17 | 'HTTP_USER_AGENT' => 'Amazon CloudFront', 18 | ) 19 | 20 | expect(code).to eq(200) 21 | end 22 | 23 | it 'accepts request for non-assets from browser' do 24 | code, = 25 | middleware.call env_for( 26 | 'http://example.com/index.html', 27 | 'HTTP_USER_AGENT' => 'Firefox', 28 | ) 29 | 30 | expect(code).to eq(200) 31 | end 32 | 33 | it 'rejects request for non-assets from CloudFront' do 34 | code, env = 35 | middleware.call env_for( 36 | 'http://example.com/index.html', 37 | 'HTTP_USER_AGENT' => 'Amazon CloudFront', 38 | ) 39 | 40 | expect(code).to eq(302) 41 | expect(env['Location']).to eq(host) 42 | end 43 | 44 | def env_for(url, **opts) 45 | Rack::MockRequest.env_for(url, opts) 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /public/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.annotaterb.yml: -------------------------------------------------------------------------------- 1 | --- 2 | :position: before 3 | :position_in_additional_file_patterns: before 4 | :position_in_class: before 5 | :position_in_factory: before 6 | :position_in_fixture: before 7 | :position_in_routes: before 8 | :position_in_serializer: before 9 | :position_in_test: before 10 | :classified_sort: true 11 | :exclude_controllers: true 12 | :exclude_factories: false 13 | :exclude_fixtures: false 14 | :exclude_helpers: true 15 | :exclude_scaffolds: true 16 | :exclude_serializers: false 17 | :exclude_sti_subclasses: false 18 | :exclude_tests: false 19 | :force: false 20 | :format_markdown: false 21 | :format_rdoc: false 22 | :format_yard: false 23 | :frozen: false 24 | :ignore_model_sub_dir: false 25 | :ignore_unknown_models: false 26 | :include_version: false 27 | :show_check_constraints: false 28 | :show_complete_foreign_keys: false 29 | :show_foreign_keys: true 30 | :show_indexes: true 31 | :simple_indexes: false 32 | :sort: false 33 | :timestamp: false 34 | :trace: false 35 | :with_comment: true 36 | :with_column_comments: true 37 | :with_table_comments: true 38 | :active_admin: false 39 | :command: 40 | :debug: false 41 | :hide_default_column_types: '' 42 | :hide_limit_column_types: '' 43 | :ignore_columns: 44 | :ignore_routes: 45 | :models: true 46 | :routes: false 47 | :skip_on_db_migrate: false 48 | :target_action: :do_annotations 49 | :wrapper: 50 | :wrapper_close: 51 | :wrapper_open: 52 | :classes_default_to_s: [] 53 | :additional_file_patterns: [] 54 | :model_dir: 55 | - app/models 56 | :require: [] 57 | :root_dir: 58 | - '' 59 | -------------------------------------------------------------------------------- /app/javascript/src/components/ClickList.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 51 | -------------------------------------------------------------------------------- /app/javascript/src/stores/click.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia'; 2 | import type { Subscription } from '@rails/actioncable'; 3 | import consumer from '../../channels/consumer'; 4 | import { get, post } from '@/use/fetch'; 5 | 6 | export type Click = { 7 | id: number; 8 | created_at: string; 9 | ip: string; 10 | user_agent: string; 11 | }; 12 | 13 | let channel: Subscription | null = null; 14 | 15 | export const useClickStore = defineStore('click', { 16 | state: () => ({ 17 | loaded: false, 18 | subscribed: false, 19 | total: 0, 20 | items: [] as Click[], 21 | }), 22 | 23 | actions: { 24 | sendClick() { 25 | post('/clicks', { responseKind: 'json' }); 26 | }, 27 | 28 | async getClicks() { 29 | const response = await get('/clicks', { 30 | responseKind: 'json', 31 | }); 32 | const json = await response.json; 33 | 34 | this.total = json.total; 35 | this.items = json.items; 36 | this.loaded = true; 37 | }, 38 | 39 | subscribe() { 40 | channel = consumer.subscriptions.create( 41 | { 42 | channel: 'ClicksChannel', 43 | }, 44 | { 45 | received: (click: Click) => { 46 | this.total += 1; 47 | this.items.unshift(click); 48 | }, 49 | }, 50 | ); 51 | this.subscribed = true; 52 | }, 53 | 54 | unsubscribe() { 55 | if (channel) { 56 | channel.unsubscribe(); 57 | } 58 | this.subscribed = false; 59 | }, 60 | }, 61 | }); 62 | -------------------------------------------------------------------------------- /app/javascript/src/pages/HomePage.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 52 | -------------------------------------------------------------------------------- /spec/javascript/src/stores/click.test.ts: -------------------------------------------------------------------------------- 1 | const mockGet = jest.fn().mockResolvedValue({ 2 | json: { 3 | total: 500, 4 | items: [ 5 | { 6 | id: 1, 7 | created_at: '1990-12-25', 8 | ip: '1.2.3.4', 9 | user_agent: 'World Wide Web', 10 | }, 11 | { 12 | id: 2, 13 | created_at: '1994-12-15', 14 | ip: '5.6.7.8', 15 | user_agent: 'Netscape Navigator', 16 | }, 17 | ], 18 | }, 19 | }); 20 | 21 | const mockPost = jest.fn().mockResolvedValue(43); 22 | 23 | jest.mock('@/use/fetch', () => ({ 24 | __esModule: true, 25 | get: mockGet, 26 | post: mockPost, 27 | })); 28 | 29 | import { setActivePinia, createPinia } from 'pinia'; 30 | import { useClickStore } from '@/stores/click'; 31 | 32 | describe('CounterStore', () => { 33 | beforeEach(() => setActivePinia(createPinia())); 34 | 35 | it('can send click', async () => { 36 | const store = useClickStore(); 37 | await store.sendClick(); 38 | 39 | expect(mockPost).toHaveBeenCalled(); 40 | }); 41 | 42 | it('can get clicks', async () => { 43 | const store = useClickStore(); 44 | await store.getClicks(); 45 | 46 | expect(store.total).toEqual(500); 47 | }); 48 | 49 | it('can subscribe', () => { 50 | const store = useClickStore(); 51 | store.subscribe(); 52 | expect(store.subscribed).toEqual(true); 53 | }); 54 | 55 | it('can unsubscribe', () => { 56 | const store = useClickStore(); 57 | store.unsubscribe(); 58 | expect(store.subscribed).toEqual(false); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /bin/open: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Load environment variables 4 | source .env.development 5 | 6 | # Set the protocol based on FORCE_SSL value 7 | if [ "$FORCE_SSL" == "false" ]; then 8 | PROTOCOL="http" 9 | else 10 | PROTOCOL="https" 11 | fi 12 | 13 | # Set the application URL 14 | URL="$PROTOCOL://$APP_HOST" 15 | 16 | # Open the URL in Google Chrome 17 | osascript < 2 | import { useClickStore } from '@/stores/click'; 3 | import LoadingAnimation from '@/components/LoadingAnimation.vue'; 4 | 5 | const store = useClickStore(); 6 | 7 | defineProps({ 8 | enabled: { 9 | type: Boolean, 10 | required: true, 11 | }, 12 | count: { 13 | type: Number, 14 | required: true, 15 | }, 16 | }); 17 | 18 | 19 | 54 | -------------------------------------------------------------------------------- /spec/javascript/src/pages/Home.test.ts: -------------------------------------------------------------------------------- 1 | const mockSendClick = jest.fn(); 2 | const mockUnsubscribe = jest.fn(); 3 | const mockGetClicks = jest.fn(); 4 | const mockSubscribe = jest.fn(); 5 | 6 | jest.mock('@/stores/click', () => ({ 7 | __esModule: true, 8 | useClickStore: () => ({ 9 | loaded: true, 10 | total: 42, 11 | items: [ 12 | { 13 | created_at: '2021-05-23T09:27:21.497Z', 14 | ip: '1.2.3.4', 15 | user_agent: 'Jest', 16 | }, 17 | ], 18 | getClicks: mockGetClicks, 19 | subscribe: mockSubscribe, 20 | unsubscribe: mockUnsubscribe, 21 | sendClick: mockSendClick, 22 | }), 23 | })); 24 | 25 | import { mount } from '@vue/test-utils'; 26 | import Home from '@/pages/HomePage.vue'; 27 | 28 | describe('Home', () => { 29 | const wrapper = mount(Home, { 30 | props: { 31 | name: 'World', 32 | }, 33 | }); 34 | 35 | test('load clicks and subscribes', () => { 36 | expect(mockSubscribe).toHaveBeenCalled(); 37 | expect(mockGetClicks).toHaveBeenCalled(); 38 | }); 39 | 40 | test('renders name', () => { 41 | const title = wrapper.find('h1')?.element?.textContent?.trim(); 42 | expect(title).toEqual('Hello, World!'); 43 | }); 44 | 45 | test('renders text', () => { 46 | expect(wrapper.text()).toContain('Templatus is'); 47 | expect(wrapper.text()).toContain('Latest clicks'); 48 | expect(wrapper.text()).toContain('May 23, 2021'); 49 | expect(wrapper.text()).toContain('Click me!'); 50 | }); 51 | 52 | test('executes sendClick', () => { 53 | const button = wrapper.find('button'); 54 | button.trigger('click'); 55 | 56 | expect(mockSendClick).toHaveBeenCalled(); 57 | }); 58 | 59 | test('unsubscribes on unmount', () => { 60 | wrapper.unmount(); 61 | expect(mockUnsubscribe).toHaveBeenCalled(); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /.github/workflows/dedupe.yml: -------------------------------------------------------------------------------- 1 | name: Dedupe Yarn Dependencies 2 | 3 | on: 4 | pull_request_target: 5 | types: [opened, synchronize] 6 | branches: [main] 7 | paths: 8 | - 'yarn.lock' 9 | 10 | permissions: 11 | contents: write 12 | pull-requests: write 13 | 14 | jobs: 15 | dedupe: 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - uses: actions/checkout@v6 20 | with: 21 | token: ${{ secrets.PAT }} 22 | fetch-depth: 0 23 | ref: ${{ github.head_ref }} 24 | 25 | - name: Setup Node.js 26 | uses: actions/setup-node@v6 27 | with: 28 | node-version-file: 'package.json' 29 | cache: 'yarn' 30 | 31 | - name: Install dependencies (immutable) 32 | run: yarn install --immutable 33 | 34 | - name: Dedupe 35 | run: yarn dedupe 36 | 37 | - name: Commit and push if yarn.lock changed 38 | id: dedupe-check 39 | run: | 40 | git config user.name "ledermann-bot" 41 | git config user.email "ledermann-bot@users.noreply.github.com" 42 | 43 | if ! git diff --quiet; then 44 | git add yarn.lock 45 | git commit -m 'chore: yarn dedupe' 46 | git push origin HEAD:${{ github.head_ref }} 47 | echo "performed=true" >> $GITHUB_OUTPUT 48 | else 49 | echo "performed=false" >> $GITHUB_OUTPUT 50 | fi 51 | 52 | - name: Comment on PR - Dedupe performed 53 | if: steps.dedupe-check.outputs.performed == 'true' 54 | uses: actions/github-script@v8 55 | with: 56 | script: | 57 | github.rest.issues.createComment({ 58 | issue_number: context.issue.number, 59 | owner: context.repo.owner, 60 | repo: context.repo.repo, 61 | body: '🧹 **Yarn dedupe performed** - Duplicate dependencies have been removed from yarn.lock' 62 | }) 63 | -------------------------------------------------------------------------------- /config/puma.rb: -------------------------------------------------------------------------------- 1 | # This configuration file will be evaluated by Puma. The top-level methods that 2 | # are invoked here are part of Puma's configuration DSL. For more information 3 | # about methods provided by the DSL, see https://puma.io/puma/Puma/DSL.html. 4 | # 5 | # Puma starts a configurable number of processes (workers) and each process 6 | # serves each request in a thread from an internal thread pool. 7 | # 8 | # You can control the number of workers using ENV["WEB_CONCURRENCY"]. You 9 | # should only set this value when you want to run 2 or more workers. The 10 | # default is already 1. You can set it to `auto` to automatically start a worker 11 | # for each available processor. 12 | # 13 | # The ideal number of threads per worker depends both on how much time the 14 | # application spends waiting for IO operations and on how much you wish to 15 | # prioritize throughput over latency. 16 | # 17 | # As a rule of thumb, increasing the number of threads will increase how much 18 | # traffic a given process can handle (throughput), but due to CRuby's 19 | # Global VM Lock (GVL) it has diminishing returns and will degrade the 20 | # response time (latency) of the application. 21 | # 22 | # The default is set to 3 threads as it's deemed a decent compromise between 23 | # throughput and latency for the average Rails application. 24 | # 25 | # Any libraries that use a connection pool or another resource pool should 26 | # be configured to provide at least as many connections as the number of 27 | # threads. This includes Active Record's `pool` parameter in `database.yml`. 28 | threads_count = ENV.fetch('RAILS_MAX_THREADS', 3) 29 | threads threads_count, threads_count 30 | 31 | # Specifies the `port` that Puma will listen on to receive requests; default is 3000. 32 | port ENV.fetch('PORT', 3000) 33 | 34 | # Allow puma to be restarted by `bin/rails restart` command. 35 | plugin :tmp_restart 36 | 37 | # Specify the PID file. Defaults to tmp/pids/server.pid in development. 38 | # In other environments, only set the PID file if requested. 39 | pidfile ENV['PIDFILE'] if ENV['PIDFILE'] 40 | -------------------------------------------------------------------------------- /config/application.rb: -------------------------------------------------------------------------------- 1 | require_relative 'boot' 2 | 3 | require 'rails' 4 | # Pick the frameworks you want: 5 | require 'active_record/railtie' 6 | # require 'active_storage/engine' 7 | require 'action_controller/railtie' 8 | require 'action_view/railtie' 9 | require 'action_mailer/railtie' 10 | require 'active_job/railtie' 11 | require 'action_cable/engine' 12 | require 'action_mailbox/engine' 13 | require 'action_text/engine' 14 | require 'rails/test_unit/railtie' 15 | 16 | # Require the gems listed in Gemfile, including any gems 17 | # you've limited to :test, :development, or :production. 18 | Bundler.require(*Rails.groups) 19 | 20 | module Templatus 21 | class Application < Rails::Application 22 | # Initialize configuration defaults for originally generated Rails version. 23 | config.load_defaults 8.1 24 | 25 | # Please, add to the `ignore` list any other `lib` subdirectories that do 26 | # not contain `.rb` files, or that should not be reloaded or eager loaded. 27 | # Common ones are `templates`, `generators`, or `middleware`, for example. 28 | config.autoload_lib(ignore: %w[assets tasks middleware]) 29 | 30 | # Configuration for the application, engines, and railties goes here. 31 | # 32 | # These settings can be overridden in specific environments using the files 33 | # in config/environments, which are processed later. 34 | # 35 | # config.time_zone = "Central Time (US & Canada)" 36 | # config.eager_load_paths << Rails.root.join("extras") 37 | 38 | config.time_zone = ENV.fetch('TIME_ZONE', 'Berlin') 39 | 40 | config.x.app_host = ENV.fetch('APP_HOST', nil) 41 | 42 | config.x.git.commit_version = 43 | ENV.fetch('COMMIT_VERSION') { `git describe --always`.chomp } 44 | 45 | config.x.git.commit_time = 46 | ENV.fetch('COMMIT_TIME') { `git show -s --format=%cI`.chomp } 47 | 48 | config.x.honeybadger.api_key = ENV['HONEYBADGER_API_KEY'].presence 49 | config.x.rorvswild.api_key = ENV['RORVSWILD_API_KEY'].presence 50 | 51 | config.x.plausible_url = ENV.fetch('PLAUSIBLE_URL', nil) 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /app/javascript/src/components/AppFlash.vue: -------------------------------------------------------------------------------- 1 | 52 | 53 | 84 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | x-app-base: &app_base 4 | build: . 5 | links: 6 | - db 7 | - redis 8 | environment: &app_environment 9 | APP_HOST: localhost 10 | ASSET_HOST: '' 11 | FORCE_SSL: 'false' 12 | DB_HOST: db 13 | DB_PASSWORD: my-secret-database-password 14 | DB_USER: postgres 15 | SECRET_KEY_BASE: something-long-and-secret 16 | HONEYBADGER_API_KEY: '' 17 | PLAUSIBLE_URL: '' 18 | 19 | services: 20 | db: 21 | environment: 22 | POSTGRES_PASSWORD: my-secret-database-password 23 | networks: 24 | - internal 25 | image: postgres:17-alpine 26 | healthcheck: 27 | test: ['CMD-SHELL', 'pg_isready -U postgres'] 28 | volumes: 29 | - ./docker-volumes/postgresql:/var/lib/postgresql/data 30 | 31 | redis: 32 | networks: 33 | - internal 34 | image: redis:alpine 35 | healthcheck: 36 | test: ['CMD', 'redis-cli', 'ping'] 37 | volumes: 38 | - ./docker-volumes/redis:/data 39 | 40 | worker: 41 | <<: *app_base 42 | environment: 43 | <<: *app_environment 44 | REDIS_URL: redis://redis:6379/2 45 | networks: 46 | - internal 47 | command: bundle exec sidekiq 48 | healthcheck: 49 | test: 'ps ax | grep -v grep | grep sidekiq' 50 | 51 | cable: 52 | <<: *app_base 53 | environment: 54 | <<: *app_environment 55 | REDIS_URL: redis://redis:6379/1 56 | command: bundle exec puma -p 28080 cable/config.ru 57 | healthcheck: 58 | test: ['CMD-SHELL', 'nc -z 127.0.0.1 28080 || exit 1'] 59 | networks: 60 | - public 61 | - internal 62 | 63 | app: 64 | <<: *app_base 65 | environment: 66 | <<: *app_environment 67 | REDIS_URL: redis://redis:6379/0 68 | ports: 69 | - 3000 70 | healthcheck: 71 | test: ["CMD", "wget", "--spider", "http://127.0.0.1:3000/up"] 72 | interval: 30s 73 | timeout: 10s 74 | retries: 3 75 | start_period: 60s 76 | networks: 77 | - public 78 | - internal 79 | 80 | networks: 81 | public: 82 | external: true 83 | internal: 84 | driver_opts: 85 | encrypted: '' 86 | -------------------------------------------------------------------------------- /app/javascript/entrypoints/application.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue'; 2 | import { createPinia } from 'pinia'; 3 | import { metaContent } from '@/utils/metaContent'; 4 | import router from '@/router'; 5 | import App from '@/App.vue'; 6 | import HoneybadgerVue from '@honeybadger-io/vue'; 7 | import plausible from '@/plugins/plausible'; 8 | import { register } from 'register-service-worker'; 9 | 10 | register('/sw.js', { 11 | registrationOptions: { scope: './' }, 12 | ready(_registration: ServiceWorkerRegistration) { 13 | console.log('Service worker is active.'); 14 | }, 15 | registered(_registration: ServiceWorkerRegistration) { 16 | console.log('Service worker has been registered.'); 17 | }, 18 | cached(_registration: ServiceWorkerRegistration) { 19 | console.log('Content has been cached for offline use.'); 20 | }, 21 | updatefound(_registration: ServiceWorkerRegistration) { 22 | console.log('New content is downloading.'); 23 | }, 24 | updated(_registration: ServiceWorkerRegistration) { 25 | console.log('New content is available; please refresh.'); 26 | }, 27 | offline() { 28 | console.log( 29 | 'No internet connection found. App is running in offline mode.', 30 | ); 31 | }, 32 | error(error) { 33 | console.error('Error during service worker registration:', error); 34 | }, 35 | }); 36 | 37 | document.addEventListener('DOMContentLoaded', () => { 38 | const app = createApp(App); 39 | 40 | const honeybadgerApiKey = metaContent('honeybadger-api-key'); 41 | if (honeybadgerApiKey) { 42 | const gitCommitVersion = metaContent('git-commit-version'); 43 | 44 | app.use(HoneybadgerVue, { 45 | apiKey: honeybadgerApiKey, 46 | environment: 'production', 47 | revision: gitCommitVersion, 48 | }); 49 | } 50 | 51 | app.use(router); 52 | app.use(createPinia()); 53 | 54 | const plausibleUrl = metaContent('plausible-url'); 55 | if (plausibleUrl) 56 | app.use(plausible, { 57 | domain: metaContent('app-host') || window.location.host, 58 | hashBasedRouting: true, 59 | endpoint: `${plausibleUrl}/api/event`, 60 | }); 61 | 62 | app.mount('#vue-app'); 63 | }); 64 | -------------------------------------------------------------------------------- /config/environments/test.rb: -------------------------------------------------------------------------------- 1 | # The test environment is used exclusively to run your application's 2 | # test suite. You never need to work with it otherwise. Remember that 3 | # your test database is "scratch space" for the test suite and is wiped 4 | # and recreated between test runs. Don't rely on the data there! 5 | 6 | Rails.application.configure do 7 | # Settings specified here will take precedence over those in config/application.rb. 8 | 9 | # While tests run files are not watched, reloading is not necessary. 10 | config.enable_reloading = false 11 | 12 | # Eager loading loads your entire application. When running a single test locally, 13 | # this is usually not necessary, and can slow down your test suite. However, it's 14 | # recommended that you enable it in continuous integration systems to ensure eager 15 | # loading is working properly before deploying your code. 16 | config.eager_load = ENV['CI'].present? 17 | 18 | # Configure public file server for tests with cache-control for performance. 19 | config.public_file_server.headers = { 20 | 'cache-control' => 'public, max-age=3600', 21 | } 22 | 23 | # Show full error reports. 24 | config.consider_all_requests_local = true 25 | config.cache_store = :null_store 26 | 27 | # Render exception templates for rescuable exceptions and raise for other exceptions. 28 | config.action_dispatch.show_exceptions = :rescuable 29 | 30 | # Disable request forgery protection in test environment. 31 | config.action_controller.allow_forgery_protection = false 32 | 33 | # Tell Action Mailer not to deliver emails to the real world. 34 | # The :test delivery method accumulates sent emails in the 35 | # ActionMailer::Base.deliveries array. 36 | config.action_mailer.delivery_method = :test 37 | 38 | # Set host to be used by links generated in mailer templates. 39 | config.action_mailer.default_url_options = { host: 'example.com' } 40 | 41 | # Print deprecation notices to the stderr. 42 | config.active_support.deprecation = :stderr 43 | 44 | # Raises error for missing translations. 45 | # config.i18n.raise_on_missing_translations = true 46 | 47 | # Annotate rendered view with file names. 48 | # config.action_view.annotate_rendered_view_with_filenames = true 49 | 50 | # Raise error when a before_action's only/except options reference missing actions. 51 | config.action_controller.raise_on_missing_callback_actions = true 52 | end 53 | -------------------------------------------------------------------------------- /spec/requests/clicks_spec.rb: -------------------------------------------------------------------------------- 1 | describe 'Clicks' do 2 | include ActiveSupport::Testing::TimeHelpers 3 | 4 | let(:user_agent) { 'Netscape Navigator' } 5 | let(:ip) { '1.2.3.4' } 6 | 7 | describe 'POST /clicks' do 8 | let(:user_agent) { 'Netscape Navigator' } 9 | 10 | def call(ip) 11 | post clicks_path, 12 | headers: { 13 | HTTP_USER_AGENT: user_agent, 14 | REMOTE_ADDR: ip, 15 | ACCEPT: 'application/json', 16 | } 17 | end 18 | 19 | context 'when IPv4' do 20 | let(:ipv4) { '1.2.3.4' } 21 | 22 | it 'saves click' do 23 | call(ipv4) 24 | 25 | expect(response).to have_http_status(:created) 26 | expect(response.parsed_body).to include( 27 | { 'notice' => 'Click was successfully recorded.' }, 28 | ) 29 | expect(Click.last.ip).to eq('1.2.3.0') 30 | expect(Click.last.user_agent).to eq(user_agent) 31 | end 32 | end 33 | 34 | context 'when IPv6' do 35 | let(:ipv6) { '2001:db8::1' } 36 | 37 | it 'saves click' do 38 | call(ipv6) 39 | 40 | expect(response).to have_http_status(:created) 41 | expect(response.parsed_body).to include( 42 | { 'notice' => 'Click was successfully recorded.' }, 43 | ) 44 | expect(Click.last.ip).to eq('2001:0db8:0:0:0:0:0:0') 45 | expect(Click.last.user_agent).to eq(user_agent) 46 | end 47 | end 48 | 49 | context 'when saving fails' do 50 | let(:ipv6) { 'invalid' } 51 | 52 | it 'fails and returns http failure' do 53 | call(ipv6) 54 | 55 | expect(response).to have_http_status(:unprocessable_content) 56 | expect(response.parsed_body).to include( 57 | { 'alert' => 'Click recording failed!' }, 58 | ) 59 | end 60 | end 61 | end 62 | 63 | describe 'GET /index' do 64 | before do 65 | freeze_time 66 | 67 | Click.create! ip:, user_agent: 68 | end 69 | 70 | it 'save click and returns http success' do 71 | get '/clicks', headers: { ACCEPT: 'application/json' } 72 | 73 | expect(response).to have_http_status(:success) 74 | expect(response.parsed_body).to match( 75 | 'total' => 1, 76 | 'items' => [ 77 | hash_including( 78 | { 79 | 'created_at' => Time.current.as_json, 80 | 'ip' => ip, 81 | 'user_agent' => user_agent, 82 | }, 83 | ), 84 | ], 85 | ) 86 | end 87 | end 88 | end 89 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: bundler 9 | directory: '/' 10 | schedule: 11 | interval: daily 12 | time: '01:00' 13 | timezone: Europe/Berlin 14 | open-pull-requests-limit: 10 15 | versioning-strategy: lockfile-only 16 | allow: 17 | - dependency-type: direct 18 | - dependency-type: indirect 19 | labels: 20 | - 'dependencies' 21 | - 'ruby' 22 | groups: 23 | rails: 24 | patterns: 25 | - 'actioncable' 26 | - 'actionmailbox' 27 | - 'actionmailer' 28 | - 'actionpack' 29 | - 'actiontext' 30 | - 'actionview' 31 | - 'activejob' 32 | - 'activemodel' 33 | - 'activerecord' 34 | - 'activestorage' 35 | - 'activesupport' 36 | - 'rails' 37 | - 'railties' 38 | - 'globalid' 39 | - 'i18n' 40 | - 'mail' 41 | - 'rack' 42 | - 'rackup' 43 | rubocop: 44 | patterns: 45 | - 'rubocop*' 46 | rspec: 47 | patterns: 48 | - 'rspec*' 49 | - 'factory_bot' 50 | - 'factory_bot_rails' 51 | 52 | - package-ecosystem: npm 53 | directory: '/' 54 | schedule: 55 | interval: daily 56 | time: '01:00' 57 | timezone: Europe/Berlin 58 | open-pull-requests-limit: 10 59 | versioning-strategy: auto 60 | labels: 61 | - 'dependencies' 62 | - 'javascript' 63 | groups: 64 | tailwindcss: 65 | patterns: 66 | - '@tailwindcss/*' 67 | - 'tailwindcss' 68 | - 'prettier-plugin-tailwindcss' 69 | jest: 70 | patterns: 71 | - 'jest*' 72 | - 'ts-jest' 73 | - 'babel-jest' 74 | - 'eslint-plugin-jest' 75 | - 'vue/vue3-jest' 76 | eslint: 77 | patterns: 78 | - 'eslint*' 79 | typescript-eslint: 80 | patterns: 81 | - '@typescript-eslint/*' 82 | - 'typescript-eslint' 83 | 84 | - package-ecosystem: 'github-actions' 85 | directory: '/' 86 | schedule: 87 | interval: 'daily' 88 | time: '01:00' 89 | timezone: Europe/Berlin 90 | labels: 91 | - 'dependencies' 92 | - 'gh-action' 93 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | # A sample Guardfile 2 | # More info at https://github.com/guard/guard#readme 3 | 4 | ## Uncomment and set this to only include directories you want to watch 5 | # directories %w(app lib config test spec features) \ 6 | # .select{|d| Dir.exist?(d) ? d : UI.warning("Directory #{d} does not exist")} 7 | 8 | ## Note: if you are using the `directories` clause above and you are not 9 | ## watching the project directory ('.'), then you will want to move 10 | ## the Guardfile to a watched dir and symlink it back, e.g. 11 | # 12 | # $ mkdir config 13 | # $ mv Guardfile config/ 14 | # $ ln -s config/Guardfile . 15 | # 16 | # and, you'll have to watch "config/Guardfile" instead of "Guardfile" 17 | 18 | # Ignore unneeded folders to prevent high CPU load 19 | # https://stackoverflow.com/a/20543493/57950 20 | ignore( 21 | [ 22 | %r{^bin/*}, 23 | %r{^db/*}, 24 | %r{^log/*}, 25 | %r{^public/*}, 26 | %r{^tmp/*}, 27 | %r{^node_modules/*}, 28 | ], 29 | ) 30 | 31 | # NOTE: The cmd option is now required due to the increasing number of ways 32 | # rspec may be run, below are examples of the most common uses. 33 | # * bundler: 'bundle exec rspec' 34 | # * bundler binstubs: 'bin/rspec' 35 | # * spring: 'bin/rspec' (This will use spring if running and you have 36 | # installed the spring binstubs per the docs) 37 | # * zeus: 'zeus rspec' (requires the server to be started separately) 38 | # * 'just' rspec: 'rspec' 39 | 40 | guard :rspec, cmd: 'bin/rspec --colour --format documentation --fail-fast' do 41 | directories(%w[app config lib spec]) 42 | 43 | require 'guard/rspec/dsl' 44 | dsl = Guard::RSpec::Dsl.new(self) 45 | 46 | # Feel free to open issues for suggestions and improvements 47 | 48 | # RSpec files 49 | rspec = dsl.rspec 50 | watch(rspec.spec_helper) { rspec.spec_dir } 51 | watch(rspec.spec_support) { rspec.spec_dir } 52 | watch(rspec.spec_files) 53 | 54 | # Ruby files 55 | ruby = dsl.ruby 56 | dsl.watch_spec_files_for(ruby.lib_files) 57 | 58 | # Rails files 59 | rails = dsl.rails(view_extensions: %w[erb]) 60 | 61 | dsl.watch_spec_files_for(rails.app_files) 62 | dsl.watch_spec_files_for(rails.views) 63 | 64 | watch(rails.controllers) do |m| 65 | [ 66 | rspec.spec.call("routing/#{m[1]}_routing"), 67 | rspec.spec.call("controllers/#{m[1]}_controller"), 68 | rspec.spec.call("requests/#{m[1]}_request"), 69 | rspec.spec.call("system/#{m[1]}"), 70 | ] 71 | end 72 | 73 | # Rails config changes 74 | watch(rails.spec_helper) { rspec.spec_dir } 75 | watch(rails.routes) { "#{rspec.spec_dir}/routing" } 76 | watch(rails.app_controller) { "#{rspec.spec_dir}/controllers" } 77 | end 78 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "templatus-vue", 3 | "private": true, 4 | "scripts": { 5 | "format": "prettier . --write --plugin=@prettier/plugin-ruby --plugin=prettier-plugin-tailwindcss", 6 | "test": "TZ=UTC jest", 7 | "lint": "eslint --cache .", 8 | "tsc": "vue-tsc --noEmit" 9 | }, 10 | "dependencies": { 11 | "@heroicons/vue": "^2.2.0", 12 | "@honeybadger-io/js": "^6.12.3", 13 | "@honeybadger-io/vue": "^6.2.9", 14 | "@plausible-analytics/tracker": "^0.4.4", 15 | "@rails/actioncable": "^8.1.100", 16 | "@rails/request.js": "^0.0.13", 17 | "@tailwindcss/forms": "^0.5.11", 18 | "@types/rails__actioncable": "^8.0.3", 19 | "pinia": "^3.0.4", 20 | "register-service-worker": "^1.7.2", 21 | "tailwindcss": "^4.1.18", 22 | "timeago.js": "^4.0.2", 23 | "typescript": "^5.9.3", 24 | "vue": "^3.5.26", 25 | "vue-router": "^4.6.4" 26 | }, 27 | "version": "0.1.0", 28 | "devDependencies": { 29 | "@babel/core": "^7.28.5", 30 | "@babel/preset-env": "^7.28.5", 31 | "@prettier/plugin-ruby": "^4.0.4", 32 | "@size-limit/file": "^12.0.0", 33 | "@tailwindcss/vite": "^4.1.18", 34 | "@types/jest": "^30.0.0", 35 | "@typescript-eslint/eslint-plugin": "^8.50.1", 36 | "@typescript-eslint/parser": "^8.50.1", 37 | "@vitejs/plugin-vue": "^6.0.3", 38 | "@vue/eslint-config-prettier": "^10.2.0", 39 | "@vue/eslint-config-typescript": "^14.6.0", 40 | "@vue/test-utils": "^2.4.6", 41 | "@vue/vue3-jest": "^29.2.6", 42 | "babel-jest": "^30.2.0", 43 | "eslint": "^9.39.2", 44 | "eslint-config-prettier": "^10.1.8", 45 | "eslint-plugin-jest": "^29.11.0", 46 | "eslint-plugin-prettier": "^5.5.4", 47 | "eslint-plugin-vue": "^10.6.2", 48 | "jest": "^30.2.0", 49 | "jest-environment-jsdom": "^30.2.0", 50 | "jest-serializer-vue": "^3.1.0", 51 | "jest-transform-stub": "^2.0.0", 52 | "playwright": "^1.57.0", 53 | "prettier": "^3.7.4", 54 | "prettier-plugin-tailwindcss": "^0.7.2", 55 | "rollup": "^4.54.0", 56 | "size-limit": "^12.0.0", 57 | "ts-jest": "^29.4.6", 58 | "tslib": "^2.8.1", 59 | "vite": "^7.3.0", 60 | "vite-plugin-full-reload": "^1.2.0", 61 | "vite-plugin-rails": "^0.5.0", 62 | "vue-tsc": "^3.2.1" 63 | }, 64 | "dependenciesMeta": { 65 | "@tailwindcss/oxide": { 66 | "built": true 67 | }, 68 | "esbuild": { 69 | "built": true 70 | }, 71 | "vite": { 72 | "built": true 73 | } 74 | }, 75 | "size-limit": [ 76 | { 77 | "limit": "80 kB", 78 | "path": "public/{vite,vite-dev,vite-test}/assets/*.js" 79 | } 80 | ], 81 | "engines": { 82 | "node": ">=22" 83 | }, 84 | "packageManager": "yarn@4.12.0" 85 | } 86 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | inherit_from: 2 | - node_modules/@prettier/plugin-ruby/rubocop.yml 3 | 4 | plugins: 5 | - rubocop-performance 6 | - rubocop-rails 7 | - rubocop-rspec 8 | - rubocop-rspec_rails 9 | - rubocop-factory_bot 10 | 11 | AllCops: 12 | TargetRubyVersion: 3.4 13 | Exclude: 14 | - app/javascript/**/* 15 | - bin/**/* 16 | - coverage/**/* 17 | - db/schema.rb 18 | - log/**/* 19 | - node_modules/**/* 20 | - public/**/* 21 | - spec/javascript/**/* 22 | - tmp/**/* 23 | - vendor/**/* 24 | - docker-volumes/**/* 25 | EnabledByDefault: true 26 | 27 | ### Rails 28 | 29 | Rails/SkipsModelValidations: 30 | Enabled: false 31 | 32 | Rails/SaveBang: 33 | Enabled: false 34 | 35 | Rails/FilePath: 36 | EnforcedStyle: arguments 37 | 38 | Rails/ApplicationController: 39 | Enabled: false 40 | 41 | Rails/SchemaComment: 42 | Enabled: false 43 | 44 | Rails/EnvironmentVariableAccess: 45 | AllowReads: true 46 | 47 | Rails/Env: 48 | Enabled: false 49 | 50 | ### Bundler 51 | 52 | Bundler/OrderedGems: 53 | Enabled: false 54 | 55 | Bundler/GemVersion: 56 | Enabled: false 57 | 58 | ### Style 59 | 60 | Style/BlockComments: 61 | Exclude: 62 | - 'spec/spec_helper.rb' 63 | 64 | Style/FrozenStringLiteralComment: 65 | Enabled: false 66 | 67 | Style/Documentation: 68 | Enabled: false 69 | 70 | Style/MethodCallWithArgsParentheses: 71 | Enabled: false 72 | 73 | Style/MissingElse: 74 | Enabled: false 75 | 76 | Style/Copyright: 77 | Enabled: false 78 | 79 | Style/InlineComment: 80 | Enabled: false 81 | 82 | Style/StringHashKeys: 83 | Enabled: false 84 | 85 | Style/DocumentationMethod: 86 | Enabled: false 87 | 88 | Style/IfUnlessModifier: 89 | Enabled: false 90 | 91 | Style/IpAddresses: 92 | Enabled: false 93 | 94 | Style/StringLiterals: 95 | EnforcedStyle: single_quotes 96 | 97 | Style/StringLiteralsInInterpolation: 98 | EnforcedStyle: single_quotes 99 | 100 | Style/RequireOrder: 101 | Enabled: false 102 | 103 | ### Layout 104 | 105 | Layout/LineLength: 106 | Max: 190 107 | AllowedPatterns: ['\A#'] # Allow long comments 108 | 109 | ### Metrics 110 | 111 | Metrics/BlockLength: 112 | Max: 100 113 | 114 | Metrics/MethodLength: 115 | Max: 15 116 | 117 | # Lint 118 | 119 | Lint/ConstantResolution: 120 | Enabled: false 121 | 122 | Lint/NumberConversion: 123 | Enabled: false 124 | 125 | # Performance 126 | 127 | Performance/ChainArrayAllocation: 128 | Enabled: false 129 | 130 | # RSpec 131 | 132 | RSpec/MultipleExpectations: 133 | Enabled: false 134 | 135 | RSpec/ExampleLength: 136 | Max: 20 137 | 138 | RSpec/AlignLeftLetBrace: 139 | Enabled: false 140 | 141 | RSpec/AlignRightLetBrace: 142 | Enabled: false 143 | -------------------------------------------------------------------------------- /app/javascript/src/components/AppFooter.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 69 | -------------------------------------------------------------------------------- /app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Templatus-Vue 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | <% if Rails.configuration.x.honeybadger.api_key %> 23 | 24 | <% end %> 25 | 26 | <% if Rails.configuration.x.plausible_url %> 27 | 28 | <% end %> 29 | <% if Rails.configuration.x.app_host %> 30 | 31 | <% end %> 32 | 33 | <% if Rails.configuration.asset_host.present? %> 34 | 35 | <% end %> 36 | 37 | <% cache Rails.configuration.x.git.commit_version do %> 38 | <% versions.each do |name, version| %> 39 | <%= tag.meta name: "version-#{name}", content: version %> 40 | <% end %> 41 | <% end %> 42 | 43 | <%= csrf_meta_tags %> 44 | <%= action_cable_meta_tag %> 45 | 46 | <%= tag :link, href: asset_path('/apple-touch-icon.png'), rel: 'apple-touch-icon', type: 'image/png', sizes: '180x180' %> 47 | <%= tag :link, href: asset_path('/favicon-32x32.png'), rel: 'icon', type: 'image/png', sizes: '32x32' %> 48 | <%= tag :link, href: asset_path('/favicon-16x16.png'), rel: 'icon', type: 'image/png', sizes: '16x16' %> 49 | <%= tag :link, href: asset_path('/safari-pinned-tab.svg'),rel: 'mask-icon', color: '#d30001' %> 50 | <%= tag :link, href: webmanifest_path, rel: 'manifest' %> 51 | <%= tag :link, href: vite_asset_path('images/logo.svg'), rel: 'preload', as: 'image' %> 52 | 53 | <%= vite_client_tag %> 54 | <%= vite_typescript_tag 'application' %> 55 | <%= vite_stylesheet_tag 'application' %> 56 | 57 | 58 | 59 | 62 | 63 | <%= yield %> 64 | 65 | 66 | -------------------------------------------------------------------------------- /config/database.yml: -------------------------------------------------------------------------------- 1 | # PostgreSQL. Versions 9.3 and up are supported. 2 | # 3 | # Install the pg driver: 4 | # gem install pg 5 | # On macOS with Homebrew: 6 | # gem install pg -- --with-pg-config=/usr/local/bin/pg_config 7 | # On macOS with MacPorts: 8 | # gem install pg -- --with-pg-config=/opt/local/lib/postgresql84/bin/pg_config 9 | # On Windows: 10 | # gem install pg 11 | # Choose the win32 build. 12 | # Install PostgreSQL and put its /bin directory on your path. 13 | # 14 | # Configure Using Gemfile 15 | # gem 'pg' 16 | # 17 | default: &default 18 | adapter: postgresql 19 | encoding: unicode 20 | # For details on connection pooling, see Rails configuration guide 21 | # https://guides.rubyonrails.org/configuring.html#database-pooling 22 | pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> 23 | 24 | # The specified database role being used to connect to postgres. 25 | # To create additional roles in postgres see `$ createuser --help`. 26 | # When left blank, postgres will use the default role. This is 27 | # the same name as the operating system user running Rails. 28 | username: <%= ENV['DB_USER'] %> 29 | 30 | # The password associated with the postgres role (username). 31 | password: <%= ENV['DB_PASSWORD'] %> 32 | 33 | # Connect on a TCP socket. Omitted by default since the client uses a 34 | # domain socket that doesn't need configuration. Windows does not have 35 | # domain sockets, so uncomment these lines. 36 | host: <%= ENV['DB_HOST'] %> 37 | 38 | # The TCP port the server listens on. Defaults to 5432. 39 | # If your server runs on a different port number, change accordingly. 40 | #port: 5432 41 | 42 | # Schema search path. The server defaults to $user,public 43 | #schema_search_path: myapp,sharedapp,public 44 | 45 | # Minimum log levels, in increasing order: 46 | # debug5, debug4, debug3, debug2, debug1, 47 | # log, notice, warning, error, fatal, and panic 48 | # Defaults to warning. 49 | #min_messages: notice 50 | 51 | development: 52 | <<: *default 53 | database: templatus_development 54 | 55 | # Warning: The database defined as "test" will be erased and 56 | # re-generated from your development database when you run "rake". 57 | # Do not set this db to the same as development or production. 58 | test: 59 | <<: *default 60 | database: templatus_test 61 | 62 | # As with config/credentials.yml, you never want to store sensitive information, 63 | # like your database password, in your source code. If your source code is 64 | # ever seen by anyone, they now have access to your database. 65 | # 66 | # Instead, provide the password or a full connection URL as an environment 67 | # variable when you boot the app. For example: 68 | # 69 | # DATABASE_URL="postgres://myuser:mypass@localhost/somedatabase" 70 | # 71 | # If the connection URL is provided in the special DATABASE_URL environment 72 | # variable, Rails will automatically merge its configuration values on top of 73 | # the values provided in this file. Alternatively, you can specify a connection 74 | # URL environment variable explicitly: 75 | # 76 | # production: 77 | # url: <%= ENV['MY_APP_DATABASE_URL'] %> 78 | # 79 | # Read https://guides.rubyonrails.org/configuring.html#configuring-a-database 80 | # for a full overview on how database connection configuration can be specified. 81 | # 82 | production: 83 | <<: *default 84 | database: templatus_production 85 | -------------------------------------------------------------------------------- /config/initializers/content_security_policy.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Define an application-wide content security policy. 4 | # See the Securing Rails Applications Guide for more information: 5 | # https://guides.rubyonrails.org/security.html#content-security-policy-header 6 | 7 | Rails.application.configure do 8 | config.content_security_policy do |policy| 9 | if Rails.env.development? 10 | policy.style_src :self, 11 | # Allow @vite/client to hot reload style changes 12 | :unsafe_inline 13 | 14 | policy.script_src :self, 15 | :unsafe_inline, 16 | # Allow @vite/client to hot reload JavaScript changes 17 | "https://#{ViteRuby.config.host}" 18 | 19 | policy.connect_src :self, 20 | # Allow ActionCable connection 21 | ( 22 | if Rails.configuration.x.app_host 23 | "wss://#{Rails.configuration.x.app_host}" 24 | end 25 | ), 26 | # Allow @vite/client to hot reload CSS changes 27 | "wss://#{ViteRuby.config.host}" 28 | else 29 | policy.default_src :none 30 | policy.font_src( 31 | *[:self, :data, Rails.configuration.asset_host.presence].compact, 32 | ) 33 | policy.img_src( 34 | *[:self, :data, Rails.configuration.asset_host.presence].compact, 35 | ) 36 | policy.object_src :none 37 | policy.script_src( 38 | *[:self, Rails.configuration.asset_host.presence].compact, 39 | ) 40 | policy.style_src( 41 | *[:self, Rails.configuration.asset_host.presence].compact, 42 | ) 43 | policy.frame_src( 44 | *[:self, Rails.configuration.asset_host.presence].compact, 45 | ) 46 | policy.connect_src( 47 | *[ 48 | :self, 49 | ( 50 | if Rails.configuration.x.app_host 51 | "wss://#{Rails.configuration.x.app_host}" 52 | end 53 | ), 54 | ( 55 | if Rails.configuration.x.honeybadger.api_key 56 | 'https://api.honeybadger.io' 57 | end 58 | ), 59 | Rails.configuration.x.plausible_url.presence, 60 | ].compact, 61 | ) 62 | policy.manifest_src :self 63 | policy.frame_ancestors :none 64 | end 65 | policy.base_uri :self 66 | policy.form_action :self 67 | 68 | # Specify URI for violation reports 69 | # policy.report_uri "/csp-violation-report-endpoint" 70 | 71 | # # Generate session nonces for permitted importmap, inline scripts, and inline styles. 72 | # config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s } 73 | # config.content_security_policy_nonce_directives = %w(script-src style-src) 74 | # 75 | # # Automatically add `nonce` to `javascript_tag`, `javascript_include_tag`, and `stylesheet_link_tag` 76 | # # if the corresponding directives are specified in `content_security_policy_nonce_directives`. 77 | # # config.content_security_policy_nonce_auto = true 78 | # 79 | # # Report violations without enforcing the policy. 80 | # # config.content_security_policy_report_only = true 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /spec/rails_helper.rb: -------------------------------------------------------------------------------- 1 | require 'simplecov' 2 | SimpleCov.start 'rails' do 3 | add_filter 'app/jobs/application_job.rb' 4 | add_filter 'app/mailers/application_mailer.rb' 5 | add_filter 'app/channels/application_cable/connection.rb' 6 | add_filter 'app/channels/application_cable/channel.rb' 7 | add_filter 'app/models/application_record.rb' 8 | end 9 | 10 | # This file is copied to spec/ when you run 'rails generate rspec:install' 11 | require 'spec_helper' 12 | ENV['RAILS_ENV'] ||= 'test' 13 | require File.expand_path('../config/environment', __dir__) 14 | # Prevent database truncation if the environment is production 15 | if Rails.env.production? 16 | abort('The Rails environment is running in production mode!') 17 | end 18 | require 'rspec/rails' 19 | # Add additional requires below this line. Rails is not loaded until this point! 20 | 21 | # Requires supporting ruby files with custom matchers and macros, etc, in 22 | # spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are 23 | # run as spec files by default. This means that files in spec/support that end 24 | # in _spec.rb will both be required and run as specs, causing the specs to be 25 | # run twice. It is recommended that you do not name files matching this glob to 26 | # end with _spec.rb. You can configure this pattern with the --pattern 27 | # option on the command line or in ~/.rspec, .rspec or `.rspec-local`. 28 | # 29 | # The following line is provided for convenience purposes. It has the downside 30 | # of increasing the boot-up time by auto-requiring all files in the support 31 | # directory. Alternatively, in the individual `*_spec.rb` files, manually 32 | # require only the support files necessary. 33 | # 34 | Rails.root.glob('spec/support/**/*.rb').each { |f| require f } 35 | 36 | # Checks for pending migrations and applies them before tests are run. 37 | # If you are not using ActiveRecord, you can remove these lines. 38 | begin 39 | ActiveRecord::Migration.maintain_test_schema! 40 | rescue ActiveRecord::PendingMigrationError => e 41 | puts e.to_s.strip 42 | exit 1 43 | end 44 | RSpec.configure do |config| 45 | # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures 46 | # config.fixture_paths = [Rails.root.join('spec', 'fixtures')] 47 | 48 | # If you're not using ActiveRecord, or you'd prefer not to run each of your 49 | # examples within a transaction, remove the following line or assign false 50 | # instead of true. 51 | config.use_transactional_fixtures = true 52 | 53 | # You can uncomment this line to turn off ActiveRecord support entirely. 54 | # config.use_active_record = false 55 | 56 | # RSpec Rails can automatically mix in different behaviours to your tests 57 | # based on their file location, for example enabling you to call `get` and 58 | # `post` in specs under `spec/controllers`. 59 | # 60 | # You can disable this behaviour by removing the line below, and instead 61 | # explicitly tag your specs with their type, e.g.: 62 | # 63 | # RSpec.describe UsersController, type: :controller do 64 | # # ... 65 | # end 66 | # 67 | # The different available types are documented in the features, such as in 68 | # https://relishapp.com/rspec/rspec-rails/docs 69 | config.infer_spec_type_from_file_location! 70 | 71 | # Filter lines from Rails gems in backtraces. 72 | config.filter_rails_from_backtrace! 73 | # arbitrary gems may also be filtered via: 74 | # config.filter_gems_from_backtrace("gem name") 75 | end 76 | -------------------------------------------------------------------------------- /config/environments/development.rb: -------------------------------------------------------------------------------- 1 | require 'active_support/core_ext/integer/time' 2 | 3 | Rails.application.configure do 4 | # Settings specified here will take precedence over those in config/application.rb. 5 | 6 | # Make code changes take effect immediately without server restart. 7 | config.enable_reloading = true 8 | 9 | # Do not eager load code on boot. 10 | config.eager_load = false 11 | 12 | # Show full error reports. 13 | config.consider_all_requests_local = true 14 | 15 | # Enable server timing. 16 | config.server_timing = true 17 | 18 | # Enable/disable Action Controller caching. By default Action Controller caching is disabled. 19 | # Run rails dev:cache to toggle Action Controller caching. 20 | if Rails.root.join('tmp', 'caching-dev.txt').exist? 21 | config.action_controller.perform_caching = true 22 | config.action_controller.enable_fragment_cache_logging = true 23 | config.public_file_server.headers = { 24 | 'cache-control' => "public, max-age=#{2.days.to_i}", 25 | } 26 | else 27 | config.action_controller.perform_caching = false 28 | end 29 | 30 | # Change to :null_store to avoid any caching. 31 | config.cache_store = :memory_store 32 | 33 | # Don't care if the mailer can't send. 34 | config.action_mailer.raise_delivery_errors = false 35 | 36 | # Make template changes take effect immediately. 37 | config.action_mailer.perform_caching = false 38 | 39 | # Set localhost to be used by links generated in mailer templates. 40 | config.action_mailer.default_url_options = { host: 'localhost', port: 3000 } 41 | 42 | # Print deprecation notices to the Rails logger. 43 | config.active_support.deprecation = :log 44 | 45 | # Raise an error on page load if there are pending migrations. 46 | config.active_record.migration_error = :page_load 47 | 48 | # Highlight code that triggered database queries in logs. 49 | config.active_record.verbose_query_logs = true 50 | 51 | # Append comments with runtime information tags to SQL queries in logs. 52 | config.active_record.query_log_tags_enabled = true 53 | 54 | # Highlight code that enqueued background job in logs. 55 | config.active_job.verbose_enqueue_logs = true 56 | 57 | # Highlight code that triggered redirect in logs. 58 | config.action_dispatch.verbose_redirect_logs = true 59 | 60 | # Raises error for missing translations. 61 | # config.i18n.raise_on_missing_translations = true 62 | 63 | # Annotate rendered view with file names. 64 | config.action_view.annotate_rendered_view_with_filenames = true 65 | 66 | # Uncomment if you wish to allow Action Cable access from any origin. 67 | # config.action_cable.disable_request_forgery_protection = true 68 | 69 | # Raise error when a before_action's only/except options reference missing actions. 70 | config.action_controller.raise_on_missing_callback_actions = true 71 | 72 | # Apply autocorrection by RuboCop to files generated by `bin/rails generate`. 73 | # config.generators.apply_rubocop_autocorrect_after_generate! 74 | 75 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 76 | config.force_ssl = 77 | ActiveModel::Type::Boolean.new.cast ENV.fetch('FORCE_SSL', true) 78 | 79 | if config.force_ssl 80 | config.ssl_options = { 81 | # Ensure that http://localhost:3000 redirects to https://{APP_HOST}, 82 | # because there is no https://localhost:3000 83 | redirect: { 84 | host: ENV.fetch('APP_HOST', nil), 85 | port: 80, 86 | }, 87 | # Don't cache the HTTPS redirect to avoid conflicts with other apps 88 | hsts: false, 89 | } 90 | end 91 | 92 | Rails.application.config.hosts << ENV.fetch('APP_HOST', nil) 93 | end 94 | -------------------------------------------------------------------------------- /bin/bundle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # 5 | # This file was generated by Bundler. 6 | # 7 | # The application 'bundle' is installed as part of a gem, and 8 | # this file is here to facilitate running it. 9 | # 10 | 11 | require 'rubygems' 12 | 13 | m = 14 | Module.new do 15 | module_function 16 | 17 | def invoked_as_script? 18 | File.expand_path($0) == File.expand_path(__FILE__) 19 | end 20 | 21 | def env_var_version 22 | ENV['BUNDLER_VERSION'] 23 | end 24 | 25 | def cli_arg_version 26 | return unless invoked_as_script? # don't want to hijack other binstubs 27 | return unless 'update'.start_with?(ARGV.first || ' ') # must be running `bundle update` 28 | bundler_version = nil 29 | update_index = nil 30 | ARGV.each_with_index do |a, i| 31 | if update_index && update_index.succ == i && 32 | a =~ Gem::Version::ANCHORED_VERSION_PATTERN 33 | bundler_version = a 34 | end 35 | unless a =~ /\A--bundler(?:[= ](#{Gem::Version::VERSION_PATTERN}))?\z/ 36 | next 37 | end 38 | bundler_version = $1 39 | update_index = i 40 | end 41 | bundler_version 42 | end 43 | 44 | def gemfile 45 | gemfile = ENV['BUNDLE_GEMFILE'] 46 | return gemfile if gemfile && !gemfile.empty? 47 | 48 | File.expand_path('../../Gemfile', __FILE__) 49 | end 50 | 51 | def lockfile 52 | lockfile = 53 | case File.basename(gemfile) 54 | when 'gems.rb' 55 | gemfile.sub(/\.rb$/, gemfile) 56 | else 57 | "#{gemfile}.lock" 58 | end 59 | File.expand_path(lockfile) 60 | end 61 | 62 | def lockfile_version 63 | return unless File.file?(lockfile) 64 | lockfile_contents = File.read(lockfile) 65 | unless lockfile_contents =~ 66 | /\n\nBUNDLED WITH\n\s{2,}(#{Gem::Version::VERSION_PATTERN})\n/ 67 | return 68 | end 69 | Regexp.last_match(1) 70 | end 71 | 72 | def bundler_requirement 73 | @bundler_requirement ||= 74 | env_var_version || cli_arg_version || 75 | bundler_requirement_for(lockfile_version) 76 | end 77 | 78 | def bundler_requirement_for(version) 79 | return "#{Gem::Requirement.default}.a" unless version 80 | 81 | bundler_gem_version = Gem::Version.new(version) 82 | 83 | requirement = bundler_gem_version.approximate_recommendation 84 | 85 | unless Gem::Version.new(Gem::VERSION) < Gem::Version.new('2.7.0') 86 | return requirement 87 | end 88 | 89 | requirement += '.a' if bundler_gem_version.prerelease? 90 | 91 | requirement 92 | end 93 | 94 | def load_bundler! 95 | ENV['BUNDLE_GEMFILE'] ||= gemfile 96 | 97 | activate_bundler 98 | end 99 | 100 | def activate_bundler 101 | gem_error = 102 | activation_error_handling { gem 'bundler', bundler_requirement } 103 | return if gem_error.nil? 104 | require_error = activation_error_handling { require 'bundler/version' } 105 | if require_error.nil? && 106 | Gem::Requirement.new(bundler_requirement).satisfied_by?( 107 | Gem::Version.new(Bundler::VERSION), 108 | ) 109 | return 110 | end 111 | warn "Activating bundler (#{bundler_requirement}) failed:\n#{gem_error.message}\n\nTo install the version of bundler this project requires, run `gem install bundler -v '#{bundler_requirement}'`" 112 | exit 42 113 | end 114 | 115 | def activation_error_handling 116 | yield 117 | nil 118 | rescue StandardError, LoadError => e 119 | e 120 | end 121 | end 122 | 123 | m.load_bundler! 124 | 125 | load Gem.bin_path('bundler', 'bundle') if m.invoked_as_script? 126 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | ruby file: '.ruby-version' 4 | 5 | # Full-stack web application framework. (https://rubyonrails.org) 6 | gem 'rails', '~> 8.1.0' 7 | 8 | # Use Vite in Rails and bring joy to your JavaScript experience (https://github.com/ElMassimo/vite_ruby) 9 | gem 'vite_rails' 10 | 11 | # Pg is the Ruby interface to the PostgreSQL RDBMS (https://github.com/ged/ruby-pg) 12 | gem 'pg', '~> 1.1' 13 | 14 | # Puma is a simple, fast, threaded, and highly parallel HTTP 1.1 server for Ruby/Rack applications (https://puma.io) 15 | gem 'puma', '>= 5.0' 16 | 17 | # Boot large ruby/rails apps faster (https://github.com/Shopify/bootsnap) 18 | gem 'bootsnap', require: false 19 | 20 | # Timezone Data for TZInfo (https://tzinfo.github.io) 21 | gem 'tzinfo-data', platforms: %i[windows jruby] 22 | 23 | # A Ruby client library for Redis (https://github.com/redis/redis-rb) 24 | gem 'redis', '>= 4.0.1' 25 | 26 | # Tame Rails' multi-line logging into a single line per request (https://github.com/roidrage/lograge) 27 | gem 'lograge' 28 | 29 | # Middleware for enabling Cross-Origin Resource Sharing in Rack apps (https://github.com/cyu/rack-cors) 30 | gem 'rack-cors', require: 'rack/cors' 31 | 32 | # Rack middleware for defining a canonical host name. (https://github.com/tylerhunt/rack-canonical-host) 33 | gem 'rack-canonical-host' 34 | 35 | # Simple, efficient background processing for Ruby (https://sidekiq.org) 36 | gem 'sidekiq' 37 | 38 | # Ruby on Rails applications monitoring (https://www.rorvswild.com) 39 | gem 'rorvswild' 40 | 41 | # Class to build custom data structures, similar to a Hash. (https://github.com/ruby/ostruct) 42 | gem 'ostruct' 43 | 44 | group :development, :test do 45 | # Debugging functionality for Ruby (https://github.com/ruby/debug) 46 | gem 'debug', platforms: %i[mri windows], require: 'debug/prelude' 47 | 48 | # Security vulnerability scanner for Ruby on Rails. (https://brakemanscanner.org) 49 | gem 'brakeman', require: false 50 | 51 | # Patch-level verification for Bundler (https://github.com/rubysec/bundler-audit) 52 | gem 'bundler-audit', require: false 53 | 54 | # Loads environment variables from `.env`. (https://github.com/bkeepers/dotenv) 55 | gem 'dotenv' 56 | 57 | # RSpec for Rails (https://github.com/rspec/rspec-rails) 58 | gem 'rspec-rails' 59 | 60 | # Automatic Ruby code style checking tool. (https://github.com/rubocop/rubocop) 61 | gem 'rubocop', require: false 62 | 63 | # Automatic performance checking tool for Ruby code. (https://github.com/rubocop/rubocop-performance) 64 | gem 'rubocop-performance', require: false 65 | 66 | # Automatic Rails code style checking tool. (https://github.com/rubocop/rubocop-rails) 67 | gem 'rubocop-rails', require: false 68 | 69 | # Code style checking for RSpec files (https://github.com/rubocop/rubocop-rspec) 70 | gem 'rubocop-rspec', require: false 71 | 72 | # Code style checking for factory_bot files (https://github.com/rubocop/rubocop-factory_bot) 73 | gem 'rubocop-factory_bot', require: false 74 | 75 | # Code style checking for RSpec Rails files (https://github.com/rubocop/rubocop-rspec_rails) 76 | gem 'rubocop-rspec_rails', require: false 77 | end 78 | 79 | group :development do 80 | # Guard gem for RSpec (https://github.com/guard/guard-rspec) 81 | gem 'guard-rspec', require: false 82 | 83 | # prettier plugin for the Ruby programming language (https://github.com/prettier/plugin-ruby#readme) 84 | gem 'prettier' 85 | 86 | # A gem for generating annotations for Rails projects. (https://github.com/drwl/annotaterb) 87 | gem 'annotaterb' 88 | end 89 | 90 | group :test do 91 | # Code coverage for Ruby (https://github.com/simplecov-ruby/simplecov) 92 | gem 'simplecov', require: false 93 | 94 | # Integration testing for Rack applications (https://github.com/teamcapybara/capybara) 95 | gem 'capybara' 96 | 97 | # Playwright driver for Capybara (https://github.com/YusukeIwaki/capybara-playwright-driver) 98 | gem 'capybara-playwright-driver' 99 | end 100 | 101 | group :production do 102 | # Error reports you can be happy about. (https://www.honeybadger.io/for/ruby/) 103 | gem 'honeybadger' 104 | end 105 | -------------------------------------------------------------------------------- /config/environments/production.rb: -------------------------------------------------------------------------------- 1 | require 'active_support/core_ext/integer/time' 2 | 3 | Rails.application.configure do 4 | # Settings specified here will take precedence over those in config/application.rb. 5 | 6 | # Code is not reloaded between requests. 7 | config.enable_reloading = false 8 | 9 | # Eager load code on boot for better performance and memory savings (ignored by Rake tasks). 10 | config.eager_load = true 11 | 12 | # Full error reports are disabled. 13 | config.consider_all_requests_local = false 14 | 15 | # Turn on fragment caching in view templates. 16 | config.action_controller.perform_caching = true 17 | 18 | # Cache assets for far-future expiry since they are all digest stamped. 19 | config.public_file_server.headers = { 20 | 'X-Content-Type-Options' => 'nosniff', 21 | 'Cache-Control' => 'public, s-maxage=31536000, max-age=31536000, immutable', 22 | } 23 | 24 | # Enable serving of images, stylesheets, and JavaScripts from an asset server. 25 | config.asset_host = ENV.fetch('ASSET_HOST', nil).presence 26 | 27 | # Assume all access to the app is happening through a SSL-terminating reverse proxy. 28 | config.assume_ssl = 29 | ActiveModel::Type::Boolean.new.cast ENV.fetch('FORCE_SSL', true) 30 | 31 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 32 | config.force_ssl = 33 | ActiveModel::Type::Boolean.new.cast ENV.fetch('FORCE_SSL', true) 34 | 35 | # Skip http-to-https redirect for the default health check endpoint. 36 | config.ssl_options = { 37 | redirect: { 38 | exclude: ->(request) { request.path == '/up' }, 39 | }, 40 | } 41 | 42 | # Log to STDOUT with the current request id and remote_ip as a default log tag. 43 | config.log_tags = %i[remote_ip request_id] 44 | config.logger = ActiveSupport::TaggedLogging.logger($stdout) 45 | 46 | # Change to "debug" to log everything (including potentially personally-identifiable information!). 47 | config.log_level = ENV.fetch('RAILS_LOG_LEVEL', 'info') 48 | 49 | # Prevent health checks from clogging up the logs. 50 | config.silence_healthcheck_path = '/up' 51 | 52 | # Don't log any deprecations. 53 | config.active_support.report_deprecations = false 54 | 55 | # Replace the default in-process memory cache store with a durable alternative. 56 | config.cache_store = 57 | :redis_cache_store, 58 | { url: ENV.fetch('REDIS_URL', 'redis://localhost:6379/0') } 59 | 60 | # Replace the default in-process and non-durable queuing backend for Active Job. 61 | config.active_job.queue_adapter = :sidekiq 62 | 63 | # Ignore bad email addresses and do not raise email delivery errors. 64 | # Set this to true and configure the email server for immediate delivery to raise delivery errors. 65 | # config.action_mailer.raise_delivery_errors = false 66 | 67 | # Set host to be used by links generated in mailer templates. 68 | config.action_mailer.default_url_options = { host: 'example.com' } 69 | 70 | # Specify outgoing SMTP server. Remember to add smtp/* credentials via bin/rails credentials:edit. 71 | # config.action_mailer.smtp_settings = { 72 | # user_name: Rails.application.credentials.dig(:smtp, :user_name), 73 | # password: Rails.application.credentials.dig(:smtp, :password), 74 | # address: "smtp.example.com", 75 | # port: 587, 76 | # authentication: :plain 77 | # } 78 | 79 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 80 | # the I18n.default_locale when a translation cannot be found). 81 | config.i18n.fallbacks = true 82 | 83 | # Do not dump schema after migrations. 84 | config.active_record.dump_schema_after_migration = false 85 | 86 | # Only use :id for inspections in production. 87 | config.active_record.attributes_for_inspect = [:id] 88 | 89 | # Enable DNS rebinding protection and other `Host` header attacks. 90 | # config.hosts = [ 91 | # "example.com", # Allow requests from example.com 92 | # /.*\.example\.com/ # Allow requests from subdomains like `www.example.com` 93 | # ] 94 | config.hosts = [ENV.fetch('APP_HOST', nil), 'localhost', '127.0.0.1'] 95 | 96 | # Skip DNS rebinding protection for the default health check endpoint. 97 | config.host_authorization = { exclude: ->(request) { request.path == '/up' } } 98 | end 99 | -------------------------------------------------------------------------------- /spec/javascript/src/__snapshots__/App.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing 2 | 3 | exports[`App matches snapshot 1`] = ` 4 |
23 |
Logo 24 | 25 |
26 | 27 | 28 | 29 |
30 | 31 |
32 | 47 |
48 | `; 49 | -------------------------------------------------------------------------------- /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 | # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration 16 | RSpec.configure do |config| 17 | # rspec-expectations config goes here. You can use an alternate 18 | # assertion/expectation library such as wrong or the stdlib/minitest 19 | # assertions if you prefer. 20 | config.expect_with :rspec do |expectations| 21 | # This option will default to `true` in RSpec 4. It makes the `description` 22 | # and `failure_message` of custom matchers include text for helper methods 23 | # defined using `chain`, e.g.: 24 | # be_bigger_than(2).and_smaller_than(4).description 25 | # # => "be bigger than 2 and smaller than 4" 26 | # ...rather than: 27 | # # => "be bigger than 2" 28 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true 29 | end 30 | 31 | # rspec-mocks config goes here. You can use an alternate test double 32 | # library (such as bogus or mocha) by changing the `mock_with` option here. 33 | config.mock_with :rspec do |mocks| 34 | # Prevents you from mocking or stubbing a method that does not exist on 35 | # a real object. This is generally recommended, and will default to 36 | # `true` in RSpec 4. 37 | mocks.verify_partial_doubles = true 38 | end 39 | 40 | # This option will default to `:apply_to_host_groups` in RSpec 4 (and will 41 | # have no way to turn it off -- the option exists only for backwards 42 | # compatibility in RSpec 3). It causes shared context metadata to be 43 | # inherited by the metadata hash of host groups and examples, rather than 44 | # triggering implicit auto-inclusion in groups with matching metadata. 45 | config.shared_context_metadata_behavior = :apply_to_host_groups 46 | 47 | # The settings below are suggested to provide a good initial experience 48 | # with RSpec, but feel free to customize to your heart's content. 49 | =begin 50 | # This allows you to limit a spec run to individual examples or groups 51 | # you care about by tagging them with `:focus` metadata. When nothing 52 | # is tagged with `:focus`, all examples get run. RSpec also provides 53 | # aliases for `it`, `describe`, and `context` that include `:focus` 54 | # metadata: `fit`, `fdescribe` and `fcontext`, respectively. 55 | config.filter_run_when_matching :focus 56 | 57 | # Allows RSpec to persist some state between runs in order to support 58 | # the `--only-failures` and `--next-failure` CLI options. We recommend 59 | # you configure your source control system to ignore this file. 60 | config.example_status_persistence_file_path = "spec/examples.txt" 61 | 62 | # Limits the available syntax to the non-monkey patched syntax that is 63 | # recommended. For more details, see: 64 | # - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/ 65 | # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/ 66 | # - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode 67 | config.disable_monkey_patching! 68 | 69 | # Many RSpec users commonly either run the entire suite or an individual 70 | # file, and it's useful to allow more verbose output when running an 71 | # individual spec file. 72 | if config.files_to_run.one? 73 | # Use the documentation formatter for detailed output, 74 | # unless a formatter has already been configured 75 | # (e.g. via a command-line flag). 76 | config.default_formatter = "doc" 77 | end 78 | 79 | # Print the 10 slowest examples and example groups at the 80 | # end of the spec run, to help surface which specs are running 81 | # particularly slow. 82 | config.profile_examples = 10 83 | 84 | # Run specs in random order to surface order dependencies. If you find an 85 | # order dependency and want to debug it, you can fix the order by providing 86 | # the seed, which is printed after each run. 87 | # --seed 1234 88 | config.order = :random 89 | 90 | # Seed global randomization in this process using the `--seed` CLI option. 91 | # Setting this allows you to use `--seed` to deterministically reproduce 92 | # test failures related to randomization by passing the same `--seed` value 93 | # as the one that triggered the failure. 94 | Kernel.srand config.seed 95 | =end 96 | end 97 | -------------------------------------------------------------------------------- /public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | The page you were looking for doesn’t exist (404 Not found) 8 | 9 | 10 | 11 | 12 | 13 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 |
106 |
107 | 108 | 109 | 110 | 111 |
112 |
113 |

The page you were looking for doesn’t exist. You may have mistyped the address or the page may have moved. If you’re the application owner check the logs for more information.

114 |
115 |
116 | 117 | 118 | 119 | 120 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: push 3 | 4 | concurrency: 5 | group: ${{ github.workflow }}-${{ github.ref }} 6 | cancel-in-progress: true 7 | 8 | jobs: 9 | test: 10 | runs-on: ubuntu-latest 11 | 12 | services: 13 | postgres: 14 | image: postgres:17-alpine 15 | ports: ['5432:5432'] 16 | env: 17 | POSTGRES_PASSWORD: postgres 18 | 19 | redis: 20 | image: redis:alpine 21 | ports: ['6379:6379'] 22 | 23 | env: 24 | DB_HOST: localhost 25 | DB_USER: postgres 26 | DB_PASSWORD: postgres 27 | REDIS_URL: redis://localhost:6379/0 28 | RAILS_ENV: test 29 | CI: true 30 | RUBY_YJIT_ENABLE: 1 31 | 32 | steps: 33 | - uses: actions/checkout@v6 34 | 35 | - name: Set up Ruby 36 | uses: ruby/setup-ruby@v1 37 | with: 38 | bundler-cache: true 39 | 40 | - name: Setup Node.js 41 | uses: actions/setup-node@v6 42 | with: 43 | cache: yarn 44 | node-version-file: 'package.json' 45 | 46 | - name: Install Yarn packages 47 | run: bin/yarn install --immutable 48 | 49 | - name: Get Playwright version 50 | id: playwright-version 51 | run: echo "version=$(node -p "require('./node_modules/playwright/package.json').version")" >> $GITHUB_OUTPUT 52 | 53 | - name: Cache Playwright browsers 54 | uses: actions/cache@v5 55 | id: playwright-cache 56 | with: 57 | path: ~/.cache/ms-playwright 58 | key: playwright-browsers-${{ runner.os }}-${{ steps.playwright-version.outputs.version }} 59 | 60 | - name: Install Playwright browsers 61 | if: steps.playwright-cache.outputs.cache-hit != 'true' 62 | run: yarn playwright install --with-deps chromium 63 | 64 | - name: Lint with RuboCop 65 | run: bin/rubocop --parallel 66 | 67 | - name: Scan for common Rails security vulnerabilities using static analysis 68 | run: bin/brakeman --no-pager 69 | 70 | - name: Run ESLint 71 | run: bin/yarn lint 72 | 73 | - name: Check for TypeScript errors 74 | run: bin/yarn tsc 75 | 76 | - name: Setup PostgreSQL 77 | run: bin/rails db:create 78 | 79 | - name: Compile assets 80 | run: bin/rails assets:precompile 81 | 82 | - name: Run Ruby Tests 83 | run: bin/rspec 84 | 85 | - name: Run JavaScript tests 86 | run: bin/yarn test --coverage 87 | 88 | - name: Check JS size limit 89 | run: yarn size-limit 90 | 91 | - uses: actions/upload-artifact@v6 92 | if: failure() 93 | with: 94 | name: Playwright screenshots 95 | path: tmp/capybara/ 96 | if-no-files-found: ignore 97 | 98 | deploy: 99 | runs-on: ubuntu-latest 100 | 101 | if: github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags') 102 | 103 | needs: test 104 | 105 | steps: 106 | - uses: actions/checkout@v6 107 | with: 108 | fetch-depth: 0 109 | 110 | - name: Fetch tag annotations 111 | # https://github.com/actions/checkout/issues/290 112 | run: git fetch --tags --force 113 | 114 | - name: Docker meta 115 | id: meta 116 | uses: docker/metadata-action@v5 117 | with: 118 | # list of Docker images to use as base name for tags 119 | images: | 120 | ghcr.io/templatus/templatus-vue 121 | # generate Docker tags based on the following events/attributes 122 | tags: | 123 | type=ref,event=branch 124 | type=ref,event=pr 125 | type=semver,pattern={{version}} 126 | type=semver,pattern={{major}}.{{minor}} 127 | type=semver,pattern={{major}} 128 | type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'main') }} 129 | 130 | - name: Set up Docker Buildx 131 | uses: docker/setup-buildx-action@v3 132 | 133 | - name: Login to GitHub Container Registry 134 | uses: docker/login-action@v3 135 | with: 136 | registry: ghcr.io 137 | username: ${{ github.repository_owner }} 138 | password: ${{ secrets.GITHUB_TOKEN }} 139 | 140 | - name: Set ENV values 141 | run: | 142 | echo "COMMIT_TIME=$(git show -s --format=%cI $GITHUB_SHA)" >> $GITHUB_ENV 143 | echo "COMMIT_VERSION=$(git describe --always)" >> $GITHUB_ENV 144 | 145 | - name: Build and push 146 | uses: docker/build-push-action@v6 147 | with: 148 | context: . 149 | platforms: linux/amd64 150 | push: true 151 | tags: ${{ steps.meta.outputs.tags }} 152 | labels: ${{ steps.meta.outputs.labels }} 153 | build-args: | 154 | COMMIT_SHA=${{ github.sha }} 155 | COMMIT_TIME=${{ env.COMMIT_TIME }} 156 | COMMIT_VERSION=${{ env.COMMIT_VERSION }} 157 | COMMIT_BRANCH=${{ github.head_ref || github.ref_name }} 158 | cache-from: type=gha 159 | cache-to: type=gha,mode=max 160 | 161 | - name: Send webhook to start deployment 162 | env: 163 | DEPLOY_HOOK: ${{ secrets.DEPLOY_HOOK }} 164 | if: env.DEPLOY_HOOK != null 165 | run: curl -X POST ${{ env.DEPLOY_HOOK }} 166 | 167 | - name: Notify Honeybadger about deployment 168 | env: 169 | DEPLOY_HOOK: ${{ secrets.DEPLOY_HOOK }} 170 | HONEYBADGER_API_KEY: ${{ secrets.HONEYBADGER_API_KEY }} 171 | if: env.DEPLOY_HOOK != null && env.HONEYBADGER_API_KEY != null 172 | uses: honeybadger-io/github-notify-deploy-action@v1 173 | with: 174 | api_key: ${{ secrets.HONEYBADGER_API_KEY }} 175 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | # == Route Map 2 | # 3 | # Prefix Verb URI Pattern Controller#Action 4 | # rails_health_check GET /up(.:format) rails/health#show 5 | # sidekiq_web /sidekiq Sidekiq::Web 6 | # clicks GET /clicks(.:format) clicks#index 7 | # POST /clicks(.:format) clicks#create 8 | # webmanifest GET /manifest.v1.webmanifest(.:format) statics#manifest 9 | # root GET / vue#index 10 | # rails_postmark_inbound_emails POST /rails/action_mailbox/postmark/inbound_emails(.:format) action_mailbox/ingresses/postmark/inbound_emails#create 11 | # rails_relay_inbound_emails POST /rails/action_mailbox/relay/inbound_emails(.:format) action_mailbox/ingresses/relay/inbound_emails#create 12 | # rails_sendgrid_inbound_emails POST /rails/action_mailbox/sendgrid/inbound_emails(.:format) action_mailbox/ingresses/sendgrid/inbound_emails#create 13 | # rails_mandrill_inbound_health_check GET /rails/action_mailbox/mandrill/inbound_emails(.:format) action_mailbox/ingresses/mandrill/inbound_emails#health_check 14 | # rails_mandrill_inbound_emails POST /rails/action_mailbox/mandrill/inbound_emails(.:format) action_mailbox/ingresses/mandrill/inbound_emails#create 15 | # rails_mailgun_inbound_emails POST /rails/action_mailbox/mailgun/inbound_emails/mime(.:format) action_mailbox/ingresses/mailgun/inbound_emails#create 16 | # rails_conductor_inbound_emails GET /rails/conductor/action_mailbox/inbound_emails(.:format) rails/conductor/action_mailbox/inbound_emails#index 17 | # POST /rails/conductor/action_mailbox/inbound_emails(.:format) rails/conductor/action_mailbox/inbound_emails#create 18 | # new_rails_conductor_inbound_email GET /rails/conductor/action_mailbox/inbound_emails/new(.:format) rails/conductor/action_mailbox/inbound_emails#new 19 | # rails_conductor_inbound_email GET /rails/conductor/action_mailbox/inbound_emails/:id(.:format) rails/conductor/action_mailbox/inbound_emails#show 20 | # new_rails_conductor_inbound_email_source GET /rails/conductor/action_mailbox/inbound_emails/sources/new(.:format) rails/conductor/action_mailbox/inbound_emails/sources#new 21 | # rails_conductor_inbound_email_sources POST /rails/conductor/action_mailbox/inbound_emails/sources(.:format) rails/conductor/action_mailbox/inbound_emails/sources#create 22 | # rails_conductor_inbound_email_reroute POST /rails/conductor/action_mailbox/:inbound_email_id/reroute(.:format) rails/conductor/action_mailbox/reroutes#create 23 | # rails_conductor_inbound_email_incinerate POST /rails/conductor/action_mailbox/:inbound_email_id/incinerate(.:format) rails/conductor/action_mailbox/incinerates#create 24 | # rails_service_blob GET /rails/active_storage/blobs/redirect/:signed_id/*filename(.:format) active_storage/blobs/redirect#show 25 | # rails_service_blob_proxy GET /rails/active_storage/blobs/proxy/:signed_id/*filename(.:format) active_storage/blobs/proxy#show 26 | # GET /rails/active_storage/blobs/:signed_id/*filename(.:format) active_storage/blobs/redirect#show 27 | # rails_blob_representation GET /rails/active_storage/representations/redirect/:signed_blob_id/:variation_key/*filename(.:format) active_storage/representations/redirect#show 28 | # rails_blob_representation_proxy GET /rails/active_storage/representations/proxy/:signed_blob_id/:variation_key/*filename(.:format) active_storage/representations/proxy#show 29 | # GET /rails/active_storage/representations/:signed_blob_id/:variation_key/*filename(.:format) active_storage/representations/redirect#show 30 | # rails_disk_service GET /rails/active_storage/disk/:encoded_key/*filename(.:format) active_storage/disk#show 31 | # update_rails_disk_service PUT /rails/active_storage/disk/:encoded_token(.:format) active_storage/disk#update 32 | # rails_direct_uploads POST /rails/active_storage/direct_uploads(.:format) active_storage/direct_uploads#create 33 | 34 | require 'sidekiq/web' 35 | 36 | Rails.application.routes.draw do 37 | # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html 38 | 39 | # Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500. 40 | # Can be used by load balancers and uptime monitors to verify that the app is live. 41 | get 'up' => 'rails/health#show', :as => :rails_health_check 42 | 43 | mount Sidekiq::Web => '/sidekiq' 44 | 45 | resources :clicks, only: %i[index create] 46 | 47 | get '/manifest.v1.webmanifest', to: 'statics#manifest', as: :webmanifest 48 | 49 | root to: 'vue#index' 50 | end 51 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Basic Options */ 6 | // "incremental": true, /* Enable incremental compilation */ 7 | "target": "esnext", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ 8 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ 9 | // "lib": [], /* Specify library files to be included in the compilation. */ 10 | // "allowJs": true, /* Allow javascript files to be compiled. */ 11 | // "checkJs": true, /* Report errors in .js files. */ 12 | "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', 'react', 'react-jsx' or 'react-jsxdev'. */ 13 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 14 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 15 | "sourceMap": true, /* Generates corresponding '.map' file. */ 16 | // "outFile": "./", /* Concatenate and emit output to single file. */ 17 | // "outDir": "./", /* Redirect output structure to the directory. */ 18 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 19 | // "composite": true, /* Enable project compilation */ 20 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 21 | "removeComments": true, /* Do not emit comments to output. */ 22 | "noEmit": true, /* Do not emit outputs. */ 23 | "importHelpers": true, /* Import emit helpers from 'tslib'. */ 24 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 25 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 26 | 27 | /* Strict Type-Checking Options */ 28 | "strict": true, /* Enable all strict type-checking options. */ 29 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 30 | // "strictNullChecks": true, /* Enable strict null checks. */ 31 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 32 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 33 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 34 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 35 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 36 | 37 | /* Additional Checks */ 38 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 39 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 40 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 41 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 42 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 43 | // "noPropertyAccessFromIndexSignature": true, /* Require undeclared properties from index signatures to use element accesses. */ 44 | 45 | /* Module Resolution Options */ 46 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 47 | "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 48 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 49 | "paths": { 50 | "@/*": ["./app/javascript/src/*"], 51 | "@test/*": ["./spec/javascript/src/*"] 52 | }, 53 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 54 | // "typeRoots": [], /* List of folders to include type definitions from. */ 55 | // "types": [], /* Type declaration files to be included in compilation. */ 56 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 57 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 58 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 59 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 60 | 61 | /* Source Map Options */ 62 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 63 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 64 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 65 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 66 | 67 | /* Experimental Options */ 68 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 69 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 70 | 71 | /* Advanced Options */ 72 | "skipLibCheck": true, /* Skip type checking of declaration files. */ 73 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 74 | }, 75 | "include": ["app/javascript/**/*.ts", "app/javascript/**/*.vue", "spec/javascript/**/*.ts"], 76 | "exclude": ["node_modules", "public"] 77 | } 78 | -------------------------------------------------------------------------------- /public/400.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | The server cannot process the request due to a client error (400 Bad Request) 8 | 9 | 10 | 11 | 12 | 13 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 |
106 |
107 | 108 | 109 | 110 | 111 |
112 |
113 |

The server cannot process the request due to a client error. Please check the request and try again. If you’re the application owner check the logs for more information.

114 |
115 |
116 | 117 | 118 | 119 | 120 | -------------------------------------------------------------------------------- /public/406-unsupported-browser.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Your browser is not supported (406 Not Acceptable) 8 | 9 | 10 | 11 | 12 | 13 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 |
106 |
107 | 108 | 109 | 110 | 111 |
112 |
113 |

Your browser is not supported.
Please upgrade your browser to continue.

114 |
115 |
116 | 117 | 118 | 119 | 120 | -------------------------------------------------------------------------------- /public/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | We’re sorry, but something went wrong (500 Internal Server Error) 8 | 9 | 10 | 11 | 12 | 13 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 |
106 |
107 | 108 | 109 | 110 | 111 |
112 |
113 |

We’re sorry, but something went wrong.
If you’re the application owner check the logs for more information.

114 |
115 |
116 | 117 | 118 | 119 | 120 | -------------------------------------------------------------------------------- /public/422.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | The change you wanted was rejected (422 Unprocessable Entity) 8 | 9 | 10 | 11 | 12 | 13 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 |
106 |
107 | 108 | 109 | 110 | 111 |
112 |
113 |

The change you wanted was rejected. Maybe you tried to change something you didn’t have access to. If you’re the application owner check the logs for more information.

114 |
115 |
116 | 117 | 118 | 119 | 120 | --------------------------------------------------------------------------------