├── .gitattributes ├── .gitignore ├── .ruby-version ├── Brewfile ├── Brewfile.lock.json ├── Gemfile ├── Gemfile.lock ├── Procfile.dev ├── README.md ├── Rakefile ├── app ├── assets │ ├── builds │ │ └── .keep │ ├── images │ │ ├── .keep │ │ ├── favicon.ico │ │ └── mark.svg │ └── stylesheets │ │ └── application.tailwind.css ├── channels │ └── application_cable │ │ ├── channel.rb │ │ └── connection.rb ├── components │ ├── application_component.rb │ ├── bootstrap │ │ └── icon_component.rb │ ├── button_component.rb │ ├── menu_component.rb │ ├── menu_item_component.rb │ └── popover_component.rb ├── controllers │ ├── application_controller.rb │ ├── books_controller.rb │ ├── concerns │ │ └── .keep │ └── views_controller.rb ├── helpers │ ├── application_helper.rb │ ├── books_helper.rb │ ├── filter_helper.rb │ └── search_helper.rb ├── javascript │ ├── application.js │ ├── channels │ │ ├── consumer.js │ │ └── index.js │ ├── config │ │ ├── debounced.js │ │ ├── index.js │ │ └── turbo.js │ └── controllers │ │ ├── application.js │ │ ├── application_controller.js │ │ ├── checkbox_set_controller.js │ │ ├── clearable_controller.js │ │ ├── column_controller.js │ │ ├── details_popover_controller.js │ │ ├── details_set_controller.js │ │ ├── element_controller.js │ │ ├── filter_by_controller.js │ │ ├── groupable_controller.js │ │ ├── index.js │ │ ├── pagy_controller.js │ │ └── sort_by_controller.js ├── jobs │ └── application_job.rb ├── models │ ├── application_record.rb │ ├── author.rb │ ├── book.rb │ ├── book_author.rb │ ├── concerns │ │ └── .keep │ └── view.rb └── views │ ├── books │ ├── _form.html.erb │ ├── form │ │ ├── _batch.html.erb │ │ ├── _fields.html.erb │ │ ├── _filter.html.erb │ │ ├── _sort.html.erb │ │ ├── section.rb │ │ └── section │ │ │ └── heading.rb │ ├── index.rb │ ├── tab.rb │ ├── tab_popover.rb │ └── tabs.rb │ ├── filter │ ├── _attribute_fields.html.erb │ ├── _condition_fields.html.erb │ ├── _grouping_fields.html.erb │ └── _value_fields.html.erb │ ├── layout.rb │ ├── table.rb │ └── table │ ├── column.rb │ ├── column_edit.rb │ ├── column_summary.rb │ ├── footer.rb │ ├── group_header.rb │ ├── head.rb │ ├── header.rb │ └── row.rb ├── bin ├── bundle ├── dev ├── rails ├── rake └── setup ├── config.ru ├── config ├── application.rb ├── boot.rb ├── cable.yml ├── credentials.yml.enc ├── database.yml ├── environment.rb ├── environments │ ├── development.rb │ ├── production.rb │ └── test.rb ├── initializers │ ├── assets.rb │ ├── content_security_policy.rb │ ├── filter_parameter_logging.rb │ ├── inflections.rb │ ├── pagy.rb │ ├── permissions_policy.rb │ └── ransack.rb ├── locales │ └── en.yml ├── puma.rb ├── routes.rb └── storage.yml ├── db ├── books.csv ├── migrate │ ├── 20220917065513_create_authors.rb │ ├── 20220917071354_create_books.rb │ ├── 20220917071558_create_book_authors.rb │ └── 20220918220202_create_views.rb ├── schema.rb └── seeds.rb ├── esbuild.config.js ├── lib ├── assets │ └── .keep └── tasks │ └── .keep ├── log └── .keep ├── package.json ├── public ├── 404.html ├── 422.html ├── 500.html ├── apple-touch-icon-precomposed.png ├── apple-touch-icon.png ├── favicon.ico └── robots.txt ├── screenshots ├── 1.png ├── 2.png ├── 3.png ├── 4.png ├── 5.png ├── 6.png ├── 7.png ├── 8.png └── 9.png ├── storage └── .keep ├── tailwind.config.js ├── tmp ├── .keep ├── pids │ └── .keep └── storage │ └── .keep ├── vendor └── .keep └── yarn.lock /.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 any vendored files as having been vendored. 7 | vendor/* linguist-vendored 8 | -------------------------------------------------------------------------------- /.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 | /public/assets 29 | 30 | # Ignore master key for decrypting credentials and more. 31 | /config/master.key 32 | 33 | /app/assets/builds/* 34 | !/app/assets/builds/.keep 35 | 36 | /node_modules 37 | .DS_Store 38 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 3.1.2 2 | -------------------------------------------------------------------------------- /Brewfile: -------------------------------------------------------------------------------- 1 | brew "postgresql" 2 | -------------------------------------------------------------------------------- /Brewfile.lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "entries": { 3 | "brew": { 4 | "postgres": { 5 | }, 6 | "postgresql": { 7 | "version": "14.5_3", 8 | "bottle": { 9 | "rebuild": 0, 10 | "root_url": "https://ghcr.io/v2/homebrew/core", 11 | "files": { 12 | "arm64_monterey": { 13 | "cellar": "/opt/homebrew/Cellar", 14 | "url": "https://ghcr.io/v2/homebrew/core/postgresql/14/blobs/sha256:75506d966a456a059f493b89cc7f94263240a9545c2315014e7307664cc3c235", 15 | "sha256": "75506d966a456a059f493b89cc7f94263240a9545c2315014e7307664cc3c235" 16 | }, 17 | "arm64_big_sur": { 18 | "cellar": "/opt/homebrew/Cellar", 19 | "url": "https://ghcr.io/v2/homebrew/core/postgresql/14/blobs/sha256:d1781239cf4367b89ecddecd4e7ae435a4ea05515a03efc92d7f5c145154937f", 20 | "sha256": "d1781239cf4367b89ecddecd4e7ae435a4ea05515a03efc92d7f5c145154937f" 21 | }, 22 | "monterey": { 23 | "cellar": "/usr/local/Cellar", 24 | "url": "https://ghcr.io/v2/homebrew/core/postgresql/14/blobs/sha256:4979666427e82ca7e83ec171ed1eec689e3a1f1403fe6f48a3ade861278acc1a", 25 | "sha256": "4979666427e82ca7e83ec171ed1eec689e3a1f1403fe6f48a3ade861278acc1a" 26 | }, 27 | "big_sur": { 28 | "cellar": "/usr/local/Cellar", 29 | "url": "https://ghcr.io/v2/homebrew/core/postgresql/14/blobs/sha256:055199badf88a0bc7b203d8b86d236feeebed6dcf6660885b7bba2748d595c9b", 30 | "sha256": "055199badf88a0bc7b203d8b86d236feeebed6dcf6660885b7bba2748d595c9b" 31 | }, 32 | "catalina": { 33 | "cellar": "/usr/local/Cellar", 34 | "url": "https://ghcr.io/v2/homebrew/core/postgresql/14/blobs/sha256:1f34877b0fe1648b7a1f8493fbd151c7479f9ce237d8170ac6baee77f4083d06", 35 | "sha256": "1f34877b0fe1648b7a1f8493fbd151c7479f9ce237d8170ac6baee77f4083d06" 36 | }, 37 | "x86_64_linux": { 38 | "cellar": "/home/linuxbrew/.linuxbrew/Cellar", 39 | "url": "https://ghcr.io/v2/homebrew/core/postgresql/14/blobs/sha256:82aa0f9ed91993af10830d8b32e5bded5f9d2d6b743c69b8e4a58d02731546e9", 40 | "sha256": "82aa0f9ed91993af10830d8b32e5bded5f9d2d6b743c69b8e4a58d02731546e9" 41 | } 42 | } 43 | } 44 | } 45 | } 46 | }, 47 | "system": { 48 | "macos": { 49 | "monterey": { 50 | "HOMEBREW_VERSION": "3.6.1", 51 | "HOMEBREW_PREFIX": "/opt/homebrew", 52 | "Homebrew/homebrew-core": "8819d6244d3f5f2827b7cb8df93b51e1be4a3f22", 53 | "CLT": "12.5.0.22.9", 54 | "Xcode": "12.5.1", 55 | "macOS": "12.6" 56 | } 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | git_source(:github) { |repo| "https://github.com/#{repo}.git" } 3 | 4 | ruby "3.1.2" 5 | 6 | # Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main" 7 | gem "rails", "~> 7.0.4" 8 | 9 | # The modern asset pipeline for Rails [https://github.com/rails/propshaft] 10 | gem "propshaft" 11 | 12 | # Use postgresql as the database for Active Record 13 | gem "pg", "~> 1.1" 14 | 15 | # Use the Puma web server [https://github.com/puma/puma] 16 | gem "puma", "~> 5.0" 17 | 18 | # Bundle and transpile JavaScript [https://github.com/rails/jsbundling-rails] 19 | gem "jsbundling-rails" 20 | 21 | # Hotwire's SPA-like page accelerator [https://turbo.hotwired.dev] 22 | gem "turbo-rails", "~> 1.3.0" 23 | 24 | # Power-pack for Turbo-Streams 25 | gem "turbo_power", "~> 0.1.2" 26 | 27 | # Hotwire's modest JavaScript framework [https://stimulus.hotwired.dev] 28 | gem "stimulus-rails" 29 | 30 | # Bundle and process CSS [https://github.com/rails/cssbundling-rails] 31 | gem "cssbundling-rails" 32 | 33 | # Use Redis adapter to run Action Cable in production 34 | gem "redis", "~> 4.0" 35 | 36 | # Use Kredis to get higher-level data types in Redis [https://github.com/rails/kredis] 37 | # gem "kredis" 38 | 39 | # Use Active Model has_secure_password [https://guides.rubyonrails.org/active_model_basics.html#securepassword] 40 | # gem "bcrypt", "~> 3.1.7" 41 | 42 | # Windows does not include zoneinfo files, so bundle the tzinfo-data gem 43 | gem "tzinfo-data", platforms: %i[ mingw mswin x64_mingw jruby ] 44 | 45 | # Reduces boot times through caching; required in config/boot.rb 46 | gem "bootsnap", require: false 47 | 48 | # Use Active Storage variants [https://guides.rubyonrails.org/active_storage_overview.html#transforming-images] 49 | # gem "image_processing", "~> 1.2" 50 | 51 | # Object-based searching. 52 | gem "ransack", "~> 3.2" 53 | 54 | # The Best Pagination Ruby Gem 55 | gem "pagy", "~> 5.10" 56 | 57 | # A framework for building view components with a Ruby DSL. 58 | gem "phlex-rails" 59 | 60 | group :development, :test do 61 | # See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem 62 | gem "debug", platforms: %i[ mri mingw x64_mingw ] 63 | end 64 | 65 | group :development do 66 | # Use console on exceptions pages [https://github.com/rails/web-console] 67 | gem "web-console" 68 | 69 | # Add speed badges [https://github.com/MiniProfiler/rack-mini-profiler] 70 | # gem "rack-mini-profiler" 71 | 72 | # Speed up commands on slow machines / big apps [https://github.com/rails/spring] 73 | # gem "spring" 74 | end 75 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | actioncable (7.0.4) 5 | actionpack (= 7.0.4) 6 | activesupport (= 7.0.4) 7 | nio4r (~> 2.0) 8 | websocket-driver (>= 0.6.1) 9 | actionmailbox (7.0.4) 10 | actionpack (= 7.0.4) 11 | activejob (= 7.0.4) 12 | activerecord (= 7.0.4) 13 | activestorage (= 7.0.4) 14 | activesupport (= 7.0.4) 15 | mail (>= 2.7.1) 16 | net-imap 17 | net-pop 18 | net-smtp 19 | actionmailer (7.0.4) 20 | actionpack (= 7.0.4) 21 | actionview (= 7.0.4) 22 | activejob (= 7.0.4) 23 | activesupport (= 7.0.4) 24 | mail (~> 2.5, >= 2.5.4) 25 | net-imap 26 | net-pop 27 | net-smtp 28 | rails-dom-testing (~> 2.0) 29 | actionpack (7.0.4) 30 | actionview (= 7.0.4) 31 | activesupport (= 7.0.4) 32 | rack (~> 2.0, >= 2.2.0) 33 | rack-test (>= 0.6.3) 34 | rails-dom-testing (~> 2.0) 35 | rails-html-sanitizer (~> 1.0, >= 1.2.0) 36 | actiontext (7.0.4) 37 | actionpack (= 7.0.4) 38 | activerecord (= 7.0.4) 39 | activestorage (= 7.0.4) 40 | activesupport (= 7.0.4) 41 | globalid (>= 0.6.0) 42 | nokogiri (>= 1.8.5) 43 | actionview (7.0.4) 44 | activesupport (= 7.0.4) 45 | builder (~> 3.1) 46 | erubi (~> 1.4) 47 | rails-dom-testing (~> 2.0) 48 | rails-html-sanitizer (~> 1.1, >= 1.2.0) 49 | activejob (7.0.4) 50 | activesupport (= 7.0.4) 51 | globalid (>= 0.3.6) 52 | activemodel (7.0.4) 53 | activesupport (= 7.0.4) 54 | activerecord (7.0.4) 55 | activemodel (= 7.0.4) 56 | activesupport (= 7.0.4) 57 | activestorage (7.0.4) 58 | actionpack (= 7.0.4) 59 | activejob (= 7.0.4) 60 | activerecord (= 7.0.4) 61 | activesupport (= 7.0.4) 62 | marcel (~> 1.0) 63 | mini_mime (>= 1.1.0) 64 | activesupport (7.0.4) 65 | concurrent-ruby (~> 1.0, >= 1.0.2) 66 | i18n (>= 1.6, < 2) 67 | minitest (>= 5.1) 68 | tzinfo (~> 2.0) 69 | bindex (0.8.1) 70 | bootsnap (1.15.0) 71 | msgpack (~> 1.2) 72 | builder (3.2.4) 73 | concurrent-ruby (1.1.10) 74 | crass (1.0.6) 75 | cssbundling-rails (1.1.1) 76 | railties (>= 6.0.0) 77 | debug (1.6.3) 78 | irb (>= 1.3.6) 79 | reline (>= 0.3.1) 80 | erubi (1.11.0) 81 | globalid (1.0.0) 82 | activesupport (>= 5.0) 83 | i18n (1.12.0) 84 | concurrent-ruby (~> 1.0) 85 | io-console (0.5.11) 86 | irb (1.5.0) 87 | reline (>= 0.3.0) 88 | jsbundling-rails (1.0.3) 89 | railties (>= 6.0.0) 90 | loofah (2.19.0) 91 | crass (~> 1.0.2) 92 | nokogiri (>= 1.5.9) 93 | mail (2.7.1) 94 | mini_mime (>= 0.1.1) 95 | marcel (1.0.2) 96 | method_source (1.0.0) 97 | mini_mime (1.1.2) 98 | minitest (5.16.3) 99 | msgpack (1.6.0) 100 | net-imap (0.3.1) 101 | net-protocol 102 | net-pop (0.1.2) 103 | net-protocol 104 | net-protocol (0.1.3) 105 | timeout 106 | net-smtp (0.3.3) 107 | net-protocol 108 | nio4r (2.5.8) 109 | nokogiri (1.13.9-arm64-darwin) 110 | racc (~> 1.4) 111 | nokogiri (1.13.9-x86_64-darwin) 112 | racc (~> 1.4) 113 | nokogiri (1.13.9-x86_64-linux) 114 | racc (~> 1.4) 115 | pagy (5.10.1) 116 | activesupport 117 | pg (1.4.5) 118 | phlex (1.0.0.rc1) 119 | zeitwerk (~> 2.6) 120 | phlex-rails (0.3.1) 121 | phlex (>= 1.0.0.rc1, < 2) 122 | rails (>= 6.1, < 8) 123 | zeitwerk (~> 2) 124 | propshaft (0.6.4) 125 | actionpack (>= 7.0.0) 126 | activesupport (>= 7.0.0) 127 | rack 128 | railties (>= 7.0.0) 129 | puma (5.6.5) 130 | nio4r (~> 2.0) 131 | racc (1.6.0) 132 | rack (2.2.4) 133 | rack-test (2.0.2) 134 | rack (>= 1.3) 135 | rails (7.0.4) 136 | actioncable (= 7.0.4) 137 | actionmailbox (= 7.0.4) 138 | actionmailer (= 7.0.4) 139 | actionpack (= 7.0.4) 140 | actiontext (= 7.0.4) 141 | actionview (= 7.0.4) 142 | activejob (= 7.0.4) 143 | activemodel (= 7.0.4) 144 | activerecord (= 7.0.4) 145 | activestorage (= 7.0.4) 146 | activesupport (= 7.0.4) 147 | bundler (>= 1.15.0) 148 | railties (= 7.0.4) 149 | rails-dom-testing (2.0.3) 150 | activesupport (>= 4.2.0) 151 | nokogiri (>= 1.6) 152 | rails-html-sanitizer (1.4.3) 153 | loofah (~> 2.3) 154 | railties (7.0.4) 155 | actionpack (= 7.0.4) 156 | activesupport (= 7.0.4) 157 | method_source 158 | rake (>= 12.2) 159 | thor (~> 1.0) 160 | zeitwerk (~> 2.5) 161 | rake (13.0.6) 162 | ransack (3.2.1) 163 | activerecord (>= 6.1.5) 164 | activesupport (>= 6.1.5) 165 | i18n 166 | redis (4.8.0) 167 | reline (0.3.1) 168 | io-console (~> 0.5) 169 | stimulus-rails (1.1.1) 170 | railties (>= 6.0.0) 171 | thor (1.2.1) 172 | timeout (0.3.0) 173 | turbo-rails (1.3.2) 174 | actionpack (>= 6.0.0) 175 | activejob (>= 6.0.0) 176 | railties (>= 6.0.0) 177 | turbo_power (0.1.6) 178 | turbo-rails (~> 1.3.0) 179 | turbo_ready 180 | turbo_ready (0.1.2) 181 | rails (>= 6.1) 182 | turbo-rails (>= 1.1) 183 | tzinfo (2.0.5) 184 | concurrent-ruby (~> 1.0) 185 | web-console (4.2.0) 186 | actionview (>= 6.0.0) 187 | activemodel (>= 6.0.0) 188 | bindex (>= 0.4.0) 189 | railties (>= 6.0.0) 190 | websocket-driver (0.7.5) 191 | websocket-extensions (>= 0.1.0) 192 | websocket-extensions (0.1.5) 193 | zeitwerk (2.6.6) 194 | 195 | PLATFORMS 196 | arm64-darwin-21 197 | x86_64-darwin-19 198 | x86_64-linux 199 | 200 | DEPENDENCIES 201 | bootsnap 202 | cssbundling-rails 203 | debug 204 | jsbundling-rails 205 | pagy (~> 5.10) 206 | pg (~> 1.1) 207 | phlex-rails 208 | propshaft 209 | puma (~> 5.0) 210 | rails (~> 7.0.4) 211 | ransack (~> 3.2) 212 | redis (~> 4.0) 213 | stimulus-rails 214 | turbo-rails (~> 1.3.0) 215 | turbo_power (~> 0.1.2) 216 | tzinfo-data 217 | web-console 218 | 219 | RUBY VERSION 220 | ruby 3.1.2p20 221 | 222 | BUNDLED WITH 223 | 2.3.7 224 | -------------------------------------------------------------------------------- /Procfile.dev: -------------------------------------------------------------------------------- 1 | web: bin/rails server -p 3000 2 | js: yarn build --watch 3 | css: yarn build:css --watch 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HotTable 2 | 3 | This application was built during the [Rails Hackathon](https://railshackathon.com) 2022. A remote hackathon for Rails developers. 4 | We had 48 hours to build a Ruby on Rails application with the theme: **[Hotwire](https://hotwired.dev) powered Rails apps**. 5 | 6 | ## Team 7 | 8 | * [Stephen Margheim](https://github.com/fractaledmind) 9 | * [Joel Drapper](https://github.com/joeldrapper) 10 | * [Marco Roth](https://github.com/marcoroth) 11 | 12 | On the Rails Hackathon site: https://railshackathon.com/teams/18 13 | 14 | 15 | ## Description 16 | 17 | This is an (minimal) [Airtable](https://airtable.com) clone. We left out any of the schema-building functionality and focused squarely on the data management functionality. Moreover, we pushed hard on using semantic HTML, so the table is an actual ``. Within the space of data management, there are a surprising number of features however: 18 | 19 | * Manage which fields are displayed 20 | * Sort by any field in either direction (with multi-column sorting supported as well) 21 | * Filter by any complex query you can imagine (nested ANDs and ORs supported with a full range of predicates) 22 | * Group by any field (sorting in either direction, collapsing or expanding the grouped rows) 23 | * Select rows (with clear visual indication of selected rows) 24 | * Select all rows in the dataset (with clear visual indication of how many records are selected) 25 | * Bulk export selected rows (with whatever display/sort/filter conditions are set on the view) 26 | * Full pagination functionality (select number of items displayed per page, jump to a specific page, see the details of the pagination window) 27 | * Column summaries (see information about the dataset for particular columns, with calculations based on the data-type of the column) 28 | * X- and Y-axis scrollable table on overflow, with fixed headers, footers, and primary columns 29 | * Quick sort, group, hide options for each column in a column header dropdown 30 | * Inline editing of all table cells 31 | * CRUD of views, saving your configuration of a particular combination of fields, sorts, filters, grouping, and pagination 32 | 33 | ## Built with 34 | 35 | * [Ransack](https://activerecord-hackery.github.io/ransack/) drives the filtering and sorting. We extended Ransack to drive the fields and grouping. 36 | * [Pagy](https://ddnexus.github.io/pagy/) drives the pagination. We applied Tailwind styling to the generated HTML. 37 | * [Stimulus](https://stimulus.hotwired.dev/) drives the "details set" (only one `
` element open at a time), automatic form submission of pagination items, building complex sorts/filters, etc. 38 | * [Turbo Drive](https://turbo.hotwired.dev/handbook/drive) allows all of the "searching" to run without a mess of updating dozens of parts of the page on any change. 39 | * [Turbo Streams](https://turbo.hotwired.dev/handbook/streams) drive the column calculations and inline editing to allow for atomic updates of individual table cells. 40 | * [TurboPower](https://github.com/marcoroth/turbo_power) adds a bunch of additional Turbo Stream actions and drives key parts of the inline editing flow. 41 | * [Phlex](https://phlex.fun/) replaces `ActionView` for the HTML rendering, allowing us to build layouts, pages, and components all in pure Ruby. 42 | * [TailwindCSS](https://tailwindcss.com/) for styling 43 | 44 | ## Demo 45 | 46 | A demo application is running at [http://hottable-rails-hackathon.herokuapp.com](http://hottable-rails-hackathon.herokuapp.com) 47 | 48 | ## Entry 49 | 50 | Our entry on the Rails Hackathon site: https://railshackathon.com/entries/6 51 | 52 | ## Application Screenshots 53 | 54 | ![](./screenshots/1.png) 55 | 56 | ![](./screenshots/2.png) 57 | 58 | ![](./screenshots/3.png) 59 | 60 | ![](./screenshots/4.png) 61 | 62 | ![](./screenshots/6.png) 63 | 64 | ![](./screenshots/7.png) 65 | 66 | ![](./screenshots/8.png) 67 | 68 | ![](./screenshots/9.png) 69 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require_relative "config/application" 5 | 6 | Rails.application.load_tasks 7 | -------------------------------------------------------------------------------- /app/assets/builds/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fractaledmind/hottable/4905781fbd1e214a4da6a3e9b539c52d8d282c55/app/assets/builds/.keep -------------------------------------------------------------------------------- /app/assets/images/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fractaledmind/hottable/4905781fbd1e214a4da6a3e9b539c52d8d282c55/app/assets/images/.keep -------------------------------------------------------------------------------- /app/assets/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fractaledmind/hottable/4905781fbd1e214a4da6a3e9b539c52d8d282c55/app/assets/images/favicon.ico -------------------------------------------------------------------------------- /app/assets/images/mark.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/stylesheets/application.tailwind.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | html, body { height: 100% } 6 | 7 | .marker\:hidden::marker { content: "" } 8 | .marker\:hidden::-webkit-details-marker { display: none } 9 | 10 | .monospace-numbers { 11 | /* enable tabular (monospaced) figures */ 12 | font-feature-settings: "tnum"; 13 | /* activating the set of figures where numbers are all of the same size, allowing them to be easily aligned like in tables */ 14 | font-variant-numeric: tabular-nums; 15 | } 16 | 17 | .has-checked\:bg-blue-100:has(:checked) { 18 | @apply bg-blue-100; 19 | } 20 | .row-group:has(:checked) .row-group-has-checked\:text-blue-900 { 21 | @apply text-blue-900; 22 | } 23 | .row-group:has(:checked) .row-group-has-checked\:bg-blue-100 { 24 | @apply bg-blue-100; 25 | } 26 | .row-group:has(:checked) .row-group-has-checked\:bg-green-100\/50 { 27 | @apply bg-green-100/50; 28 | } 29 | .row-group:has(:checked) .row-group-has-checked\:bg-orange-100\/50 { 30 | @apply bg-orange-100/50; 31 | } 32 | .row-group:has(:checked) .row-group-has-checked\:bg-purple-100\/50 { 33 | @apply bg-purple-100/50; 34 | } 35 | .row-group:hover .row-group-hover\:bg-gray-green-100-mixed { 36 | background-color: #E8F8EF; 37 | } 38 | .row-group:has(:checked) .row-group-has-checked\:bg-blue-green-100-mixed { 39 | background-color: #DCF3F3; 40 | } 41 | .row-group:hover .row-group-hover\:bg-gray-orange-100-mixed { 42 | background-color: #F9F1E6; 43 | } 44 | .row-group:has(:checked) .row-group-has-checked\:bg-blue-orange-100-mixed { 45 | background-color: #EDECEA; 46 | } 47 | .row-group:hover .row-group-hover\:bg-gray-purple-100-mixed { 48 | background-color: #F3EEFB; 49 | } 50 | .row-group:has(:checked) .row-group-has-checked\:bg-blue-purple-100-mixed { 51 | background-color: #E7E9FF; 52 | } 53 | .group:has(.peer:checked) .group-has-peer-checked\:block { 54 | @apply block; 55 | } 56 | .group:has(.peer:checked) .group-has-peer-checked\:hidden { 57 | @apply hidden; 58 | } 59 | 60 | body:has([data-row-checkbox]:checked) .body-has-checked\:visible { 61 | @apply visible; 62 | } 63 | 64 | [aria-expanded="true"] .expanded\:bi-chevron-down::before { 65 | content: "\f282" !important; 66 | } 67 | 68 | td, th { 69 | /* fake border with background-image so "borders" stay put when scrolling */ 70 | background-image: 71 | linear-gradient(to right, #CBD5E0, #CBD5E0), 72 | linear-gradient(to bottom, #CBD5E0, #CBD5E0), 73 | linear-gradient(to left, #CBD5E0, #CBD5E0), 74 | linear-gradient(to top, #CBD5E0, #CBD5E0); 75 | background-origin: border-box; 76 | background-position: top left, top right, bottom right, bottom left; 77 | background-repeat: no-repeat; 78 | } 79 | td, th { 80 | /* only put "borders" on right and bottom */ 81 | background-size: 100% 0.5px, 0.5px 100%, 100% 0.5px, 0.5px 100%; 82 | } 83 | 84 | .pagy-nav { 85 | @apply flex-1 max-w-96 min-w-72 flex items-start justify-between px-4 sm:px-0; 86 | } 87 | .pagy-nav .page.prev, 88 | .pagy-nav .page.next { 89 | @apply -mt-px flex w-0 flex-1; 90 | } 91 | .pagy-nav .page.prev { 92 | @apply justify-start mr-2; 93 | } 94 | .pagy-nav .page.next { 95 | @apply justify-end ml-2; 96 | } 97 | .pagy-nav .page.prev, 98 | .pagy-nav .page.next { 99 | @apply inline-flex items-center border-t-2 border-transparent text-sm font-medium text-gray-500 pt-1 100 | } 101 | .pagy-nav .page.prev:not(.disabled), 102 | .pagy-nav .page.next:not(.disabled) { 103 | @apply hover:border-gray-300 hover:text-gray-700; 104 | } 105 | .pagy-nav .page:not(.prev):not(.next) { 106 | @apply inline-flex items-center border-t-2 px-1; 107 | } 108 | .pagy-nav .page:not(.active):not(.prev):not(.next):not(.gap) { 109 | @apply border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300; 110 | } 111 | .pagy-nav .page.active { 112 | @apply border-indigo-500 text-indigo-600; 113 | } 114 | .pagy-nav .page.gap { 115 | @apply border-transparent; 116 | } 117 | -------------------------------------------------------------------------------- /app/channels/application_cable/channel.rb: -------------------------------------------------------------------------------- 1 | module ApplicationCable 2 | class Channel < ActionCable::Channel::Base 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /app/channels/application_cable/connection.rb: -------------------------------------------------------------------------------- 1 | module ApplicationCable 2 | class Connection < ActionCable::Connection::Base 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /app/components/application_component.rb: -------------------------------------------------------------------------------- 1 | require "phlex/rails" 2 | 3 | class ApplicationComponent < Phlex::HTML 4 | include ActionView::Helpers::AssetUrlHelper 5 | include ActionView::RecordIdentifier 6 | include Rails.application.routes.url_helpers 7 | 8 | delegate :request, to: :@_view_context 9 | delegate :params, to: :@_view_context 10 | 11 | class Struct 12 | private 13 | 14 | def tokens(*tokens, **conditional_tokens) 15 | conditional_tokens.each do |condition, token| 16 | case condition 17 | when Symbol then next unless send(condition) 18 | when Proc then next unless condition.call 19 | else raise ArgumentError, 20 | "The class condition must be a Symbol or a Proc." 21 | end 22 | 23 | case token 24 | when Symbol then tokens << token.name 25 | when String then tokens << token 26 | when Array then tokens.concat(t) 27 | else raise ArgumentError, 28 | "Conditional classes must be Symbols, Strings, or Arrays of Symbols or Strings." 29 | end 30 | end 31 | 32 | tokens.compact.join(" ") 33 | end 34 | end 35 | 36 | def attributify(*attribute_hashes) 37 | flat_attribute_hashes = attribute_hashes.map { flatten_attributes_hash(_1) } 38 | 39 | flat_attribute_hashes.reduce({}) do |memo, attribute_hash| 40 | memo.deep_merge(attribute_hash) do |attribute, oldval, newval| 41 | next newval unless ["class", "data-controller", "data-action"].include?(attribute) 42 | next newval if attribute.end_with?("!") 43 | 44 | [oldval, newval].uniq.join(' ') 45 | end 46 | end 47 | end 48 | 49 | def flatten_attributes_hash(input, keys = [], output = {}) 50 | return output.merge!(keys.join('-') => input) unless input.is_a?(Hash) 51 | 52 | input.each do |key, value| 53 | name = case key 54 | when String 55 | key 56 | when Symbol 57 | key.name.tr("_", "-") 58 | else 59 | key.to_s 60 | end 61 | 62 | flatten_attributes_hash( 63 | value, 64 | keys + Array[name], 65 | output 66 | ) 67 | end 68 | 69 | output 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /app/components/bootstrap/icon_component.rb: -------------------------------------------------------------------------------- 1 | module Bootstrap 2 | class IconComponent < ApplicationComponent 3 | def initialize(name, decorative: true, **attributes) 4 | @name = name 5 | @decorative = decorative 6 | @attributes = attributes 7 | end 8 | 9 | def template 10 | i **attributify(icon_attributes, @attributes) 11 | end 12 | 13 | private 14 | 15 | def icon_attributes 16 | { 17 | class: "bi-#{@name}", 18 | aria: { 19 | hidden: @decorative, 20 | }, 21 | } 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /app/components/button_component.rb: -------------------------------------------------------------------------------- 1 | class ButtonComponent < ApplicationComponent 2 | class Struct < ApplicationComponent::Struct 3 | def initialize(type: nil, primary: false) 4 | @type = type || (:primary if primary) 5 | end 6 | 7 | def base 8 | { 9 | class: tokens( 10 | "cursor-pointer inline-flex items-center rounded-md border border-transparent px-2.5 py-1.5 text-base font-medium gap-2", 11 | primary?: "bg-blue-500 hover:bg-blue-400 text-white", 12 | secondary?: "bg-gray-200 hover:bg-gray-300 text-gray-900", 13 | ), 14 | } 15 | end 16 | 17 | private 18 | 19 | def primary? = @type == :primary 20 | def secondary? = @type == :secondary 21 | end 22 | 23 | def self.struct(*args, **kwargs) 24 | Struct.new(*args, **kwargs) 25 | end 26 | 27 | def initialize(text = nil, as: :button, primary: false, icon: nil, **attributes) 28 | @text = text 29 | @element = as 30 | @primary = primary 31 | @icon = icon 32 | @attributes = attributes 33 | end 34 | 35 | def template(&block) 36 | if block_given? 37 | public_send(@element, **button_attributes, &block) 38 | else 39 | public_send(@element, **button_attributes) do 40 | render Bootstrap::IconComponent.new(@icon) if @icon 41 | text @text 42 | end 43 | end 44 | end 45 | 46 | def struct 47 | self.class.struct(primary: @primary) 48 | end 49 | 50 | private 51 | 52 | def button_attributes 53 | attributify(struct.base, @attributes) 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /app/components/menu_component.rb: -------------------------------------------------------------------------------- 1 | class MenuComponent < ApplicationComponent 2 | class Struct < ApplicationComponent::Struct 3 | def initialize() 4 | end 5 | 6 | def root 7 | { 8 | role: :menu, 9 | class: tokens( 10 | "z-40 group", 11 | ), 12 | } 13 | end 14 | 15 | def trigger 16 | {} 17 | end 18 | 19 | def portal 20 | { 21 | class: "divide-y divide-gray-100 rounded-md bg-white border-2 shadow-lg drop-shadow-lg focus:outline-none w-64 group", 22 | aria: { orientation: "vertical" }, 23 | } 24 | end 25 | end 26 | 27 | def self.struct(*args, **kwargs) 28 | Struct.new(*args, **kwargs) 29 | end 30 | 31 | def initialize(icon: nil, side: :bottom, align: :center, **attributes) 32 | @icon = icon 33 | @side = side 34 | @align = align 35 | @attributes = attributes 36 | end 37 | 38 | def template 39 | render PopoverComponent.new(role: :menu, side: @side, align: @align, **attributify(struct.root, @attributes)) do |popover| 40 | @popover = popover 41 | 42 | yield 43 | end 44 | end 45 | 46 | def trigger(**attributes, &) 47 | @popover.trigger(**attributify(struct.trigger, button.base, attributes), &) 48 | end 49 | 50 | def portal(**attributes, &) 51 | @popover.portal(**attributify(struct.portal, attributes), &) 52 | end 53 | 54 | def group(&) 55 | div(class: "py-1", &) 56 | end 57 | 58 | def struct 59 | self.class.struct() 60 | end 61 | 62 | private 63 | 64 | def button 65 | ButtonComponent.struct 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /app/components/menu_item_component.rb: -------------------------------------------------------------------------------- 1 | class MenuItemComponent < ApplicationComponent 2 | class Struct < ApplicationComponent::Struct 3 | def initialize() 4 | end 5 | 6 | def base 7 | { 8 | role: :menuitem, 9 | class: "group cursor-pointer text-gray-700 flex items-center px-4 py-2 space-x-2 hover:bg-gray-200", 10 | tabindex: -1, 11 | } 12 | end 13 | end 14 | 15 | def self.struct(*args, **kwargs) 16 | Struct.new(*args, **kwargs) 17 | end 18 | 19 | def initialize(as: :a, text: nil, icon: nil, url: nil, **attributes) 20 | @element = as 21 | @url = url 22 | @text = text 23 | @icon = icon 24 | @attributes = attributes 25 | end 26 | 27 | def template 28 | public_send(@element, **menuitem_attributes) do 29 | render Bootstrap::IconComponent.new(@icon) if @icon 30 | @text ? span { @text } : yield 31 | end 32 | end 33 | 34 | def struct 35 | self.class.struct() 36 | end 37 | 38 | private 39 | 40 | def menuitem_attributes 41 | attributify(struct.base, { href: @url }, @attributes) 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /app/components/popover_component.rb: -------------------------------------------------------------------------------- 1 | class PopoverComponent < ApplicationComponent 2 | class InvalidSide < StandardError 3 | def initialize(side) 4 | super("`#{side.inspect}` must be one of `:top`, `:bottom`, `:left`, or `:right`") 5 | end 6 | end 7 | class InvalidAlign < StandardError 8 | def initialize(align) 9 | super("`#{align.inspect}` must be one of `:start`, `:center`, or `:end`") 10 | end 11 | end 12 | class InvalidRole < StandardError 13 | def initialize(role) 14 | super("`#{role.inspect}` must be one of `:menu`, `:listbox`, `:tree`, `:grid`, or `:dialog`") 15 | end 16 | end 17 | 18 | class Struct < ApplicationComponent::Struct 19 | def initialize(role: :dialog, side: :bottom, align: :center) 20 | @role = role 21 | @side = side 22 | @align = align 23 | @popover_id = "popover-#{@role}-#{object_id}" 24 | end 25 | 26 | def root 27 | { 28 | class: "relative group", 29 | data: { controller: "details-popover" } 30 | } 31 | end 32 | 33 | def trigger 34 | { 35 | class: tokens("marker:hidden cursor-pointer h-full"), 36 | aria: { 37 | expanded: "false", 38 | haspopup: @role, 39 | controls: @popover_id, 40 | }, 41 | data: { 42 | action: "click@window->details-popover#hide touchend@window->details-popover#hide", 43 | details_popover_target: "button", 44 | }, 45 | } 46 | end 47 | 48 | def portal 49 | { 50 | id: @popover_id, 51 | role: @role, 52 | tabindex: -1, 53 | data: { 54 | side: @side, 55 | align: @align, 56 | }, 57 | class: tokens( 58 | "absolute z-40", 59 | side_top?: "bottom-full mb-1", 60 | side_bottom?: "top-full mt-1", 61 | vertical_start?: "left-0", 62 | vertical_center?: "left-1/2 -translate-x-1/2", 63 | vertical_end?: "right-0", 64 | side_left?: "right-full mr-1", 65 | side_right?: "left-full ml-1", 66 | horizontal_start?: "top-0", 67 | horizontal_center?: "top-1/2 -translate-y-1/2", 68 | horizontal_end?: "bottom-0 ", 69 | ), 70 | } 71 | end 72 | 73 | private 74 | 75 | def side_top? = @side == :top 76 | def side_bottom? = @side == :bottom 77 | def side_left? = @side == :left 78 | def side_right? = @side == :right 79 | def align_start? = @align == :start 80 | def align_center? = @align == :center 81 | def align_end? = @align == :end 82 | def side_vertical? = side_top? || side_bottom? 83 | def side_horizontal? = side_left? || side_right? 84 | def vertical_start? = side_vertical? && align_start? 85 | def vertical_center? = side_vertical? && align_center? 86 | def vertical_end? = side_vertical? && align_end? 87 | def horizontal_start? = side_horizontal? && align_start? 88 | def horizontal_center? = side_horizontal? && align_center? 89 | def horizontal_end? = side_horizontal? && align_end? 90 | end 91 | 92 | def self.struct(*args, **kwargs) 93 | Struct.new(*args, **kwargs) 94 | end 95 | 96 | def initialize(role: :dialog, side: :bottom, align: :center, **attributes) 97 | @role = role 98 | raise InvalidRole.new(role) unless [:menu, :listbox, :tree, :grid, :dialog].include? role 99 | @side = side 100 | raise InvalidSide.new(side) unless [:top, :bottom, :left, :right].include? side 101 | @align = align 102 | raise InvalidAlign.new(align) unless [:start, :center, :end].include? align 103 | @attributes = attributes 104 | @popover_id = "details-popover-#{@role}-#{object_id}" 105 | end 106 | 107 | def template(&block) 108 | details(**attributify(struct.root, @attributes), &block) 109 | end 110 | 111 | def trigger(icon: true, **attributes) 112 | summary(**struct.trigger) do 113 | div(**attributify({class: "h-full"}, attributes)) do 114 | yield 115 | 116 | render trigger_icon(icon) if icon 117 | end 118 | end 119 | end 120 | 121 | def portal(**attributes, &block) 122 | div(**attributify(struct.portal, attributes), &block) 123 | end 124 | 125 | def struct 126 | self.class.struct(role: @role, side: @side, align: @align) 127 | end 128 | 129 | private 130 | 131 | def trigger_icon(passed_icon = nil) 132 | icon_name = passed_icon == true ? "chevron-#{trigger_icon_direction}" : passed_icon 133 | 134 | Bootstrap::IconComponent.new( 135 | icon_name, 136 | class: "opacity-50 group-hover:opacity-100" 137 | ) 138 | end 139 | 140 | def trigger_icon_direction 141 | case @side 142 | when :top 143 | :up 144 | when :bottom 145 | :down 146 | when :left 147 | :left 148 | when :right 149 | :right 150 | end 151 | end 152 | end 153 | -------------------------------------------------------------------------------- /app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | include Pagy::Backend 3 | include ActionView::RecordIdentifier 4 | end 5 | -------------------------------------------------------------------------------- /app/controllers/books_controller.rb: -------------------------------------------------------------------------------- 1 | require "csv" 2 | 3 | class BooksController < ApplicationController 4 | layout false 5 | before_action :set_data 6 | 7 | # GET /books 8 | def index 9 | render Views::Books::Index.new( 10 | search: @search, 11 | result: @result, 12 | records: @records, 13 | pagy: @pagy 14 | ) 15 | end 16 | 17 | def edit 18 | book = Book.find(params[:id]) 19 | parts = [] 20 | 21 | attribute = params["book_attribute"] 22 | inline_edit = params["book_edit"] == "true" 23 | column_class = inline_edit ? Views::Table::ColumnEdit : Views::Table::Column 24 | 25 | html = column_class.new(book, search: ransack_search, attribute: attribute).call 26 | id = dom_id(book, "column_#{attribute}") 27 | 28 | parts << turbo_stream.replace(id, html) 29 | parts << turbo_stream.set_focus("##{id} input, ##{id} select") if inline_edit 30 | 31 | render turbo_stream: parts.join("") 32 | end 33 | 34 | def update 35 | book = Book.find(params[:id]) 36 | 37 | if book.update(book_params) 38 | parts = [] 39 | 40 | changed_attributes = book.previous_changes.keys - ["updated_at"] 41 | 42 | if changed_attributes.empty? 43 | changed_attributes << params["book_attribute"] 44 | end 45 | 46 | changed_attributes.each do |attribute| 47 | html = Views::Table::Column.new(book, search: ransack_search, attribute: attribute).call 48 | id = dom_id(book, "column_#{attribute}") 49 | 50 | parts << turbo_stream.replace(id, html) 51 | end 52 | 53 | render turbo_stream: parts.join(" ") 54 | end 55 | end 56 | 57 | # GET|POST /books/search 58 | def search 59 | redirect_to books_path( 60 | params.to_unsafe_hash.except(:view_name, :authenticity_token, :action, :controller, :commit, :filter, :batch, :field) 61 | ) 62 | end 63 | 64 | # GET books/export 65 | def summarize 66 | data = @search.result 67 | total = case params[:calculation] 68 | when "" 69 | when "nil" 70 | data.where(params[:attribute] => nil).size 71 | when "not_nil" 72 | data.where.not(params[:attribute] => nil).size 73 | when "unique" 74 | data.select(params[:attribute]).distinct.reorder(nil).size 75 | when "min" 76 | data.select(params[:attribute]).minimum(params[:attribute]) 77 | when "max" 78 | data.select(params[:attribute]).maximum(params[:attribute]) 79 | when "avg" 80 | data.select(params[:attribute]).average(params[:attribute]).round(2) 81 | when "sum" 82 | data.select(params[:attribute]).sum(params[:attribute]) 83 | when "earliest" 84 | data.select(params[:attribute]).reorder(params[:attribute] => :asc).limit(1).pluck(params[:attribute]).first 85 | when "latest" 86 | data.select(params[:attribute]).reorder(params[:attribute] => :desc).limit(1).pluck(params[:attribute]).first 87 | end 88 | 89 | render turbo_stream: turbo_stream.replace([params[:attribute], "summary"].join("_")) { 90 | Views::Table::ColumnSummary.new( 91 | total, 92 | attribute: params[:attribute], 93 | calculation: params[:calculation], 94 | ).call(view_context:).html_safe 95 | } 96 | end 97 | 98 | # GET books/export 99 | def export 100 | data = params[:selectAll].present? ? @search.result : @records.where(id: params.fetch(:select, {}).keys) 101 | 102 | respond_to do |format| 103 | format.csv do 104 | send_data data.to_csv(@search.field_attributes), filename: "books-#{Time.now.utc.to_formatted_s(:number)}.csv", disposition: (Rails.env.development? ? :inline : :attachment) 105 | end 106 | end 107 | end 108 | 109 | private 110 | 111 | def set_data 112 | @search = ransack_search 113 | @result = begin 114 | _result = @search.result 115 | _result = _result.reorder(@search.batch.attr_name => @search.batch.dir) if @search.batch 116 | 117 | _result 118 | end 119 | @pagy, @records = pagy(@result, items: page_items) 120 | end 121 | 122 | def book_params 123 | params.require(:book).permit( 124 | :title, 125 | :average_rating, 126 | :isbn, 127 | :isbn13, 128 | :language_code, 129 | :num_pages, 130 | :ratings_count, 131 | :text_reviews_count, 132 | :published_on, 133 | :publisher, 134 | ) 135 | end 136 | 137 | def ransack_search 138 | @_search ||= begin 139 | _search = Book.order(id: :asc).ransack(search_params) 140 | _search.default_fields = Book.ransortable_attributes 141 | 142 | _search 143 | end 144 | end 145 | 146 | def search_params 147 | q = params.fetch(:q, {}) 148 | q[:f] ||= [] 149 | q[:f].insert(0, Book.primary_attribute) if q[:f].any? 150 | q 151 | end 152 | 153 | def page_items 154 | params.fetch(:page_items, Pagy::DEFAULT[:items]) 155 | end 156 | end 157 | -------------------------------------------------------------------------------- /app/controllers/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fractaledmind/hottable/4905781fbd1e214a4da6a3e9b539c52d8d282c55/app/controllers/concerns/.keep -------------------------------------------------------------------------------- /app/controllers/views_controller.rb: -------------------------------------------------------------------------------- 1 | class ViewsController < ApplicationController 2 | def create 3 | view = View.create( 4 | name: create_view_params[:name], 5 | parameters: view_parameters.merge( 6 | current_view: create_view_params[:name] 7 | ) 8 | ) 9 | 10 | redirect_to books_path( 11 | view.parameters 12 | ) 13 | end 14 | 15 | def update 16 | view = View.find(params[:id]) 17 | view.update( 18 | name: update_view_params[:name], 19 | parameters: view_parameters.merge( 20 | current_view: update_view_params[:name] 21 | ) 22 | ) 23 | 24 | redirect_to books_path( 25 | view_parameters.merge( 26 | current_view: view.name 27 | ) 28 | ) 29 | end 30 | 31 | def destroy 32 | View.find(params[:id]).destroy 33 | redirect_to root_path 34 | end 35 | 36 | private 37 | 38 | def create_view_params 39 | params.require(:views).permit(:id, :name) 40 | end 41 | 42 | def update_view_params 43 | view_params = params.require(:views).to_unsafe_hash 44 | view_params.fetch(params[:id], {}) 45 | end 46 | 47 | def view_parameters 48 | params.to_unsafe_hash.except(:authenticity_token, :controller, :action) 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | include Pagy::Frontend 3 | end 4 | -------------------------------------------------------------------------------- /app/helpers/books_helper.rb: -------------------------------------------------------------------------------- 1 | module BooksHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/filter_helper.rb: -------------------------------------------------------------------------------- 1 | module FilterHelper 2 | def button_to_add_fields(form, type) 3 | new_object = form.object.send("build_#{type}") 4 | name = "#{type}_fields" 5 | fields = form.send(name, new_object, child_index: "new_#{type}") do |builder| 6 | render("filter/#{name}", f: builder) 7 | end 8 | 9 | tag.button(class: ['add_fields space-x-2', button_classes], 10 | data: { field_type: type, action: 'filter-by#addFields', content: "#{fields}" }) do 11 | safe_join([ 12 | tag.i(class: 'bi-plus-lg', aria: { hidden: true }), 13 | tag.span(button_label[type]) 14 | ]) 15 | end 16 | end 17 | 18 | def button_to_remove_fields(labeled: true) 19 | tag.button(class: ['remove_fields space-x-2', button_classes, ('border border-transparent' if labeled)], data: { action: 'filter-by#removeFields' }) do 20 | safe_join([ 21 | tag.i(class: 'bi-trash', aria: { hidden: true }), 22 | (tag.span("Remove") if labeled) 23 | ]) 24 | end 25 | end 26 | 27 | def button_to_nest_fields(type) 28 | tag.button(class: ['nest_fields space-x-2', button_classes], data: { field_type: type, action: 'filter-by#nestFields' }) do 29 | safe_join([ 30 | tag.i(class: 'bi-plus-lg', aria: { hidden: true }), 31 | button_label[type] 32 | ]) 33 | end 34 | end 35 | 36 | def button_label 37 | { 38 | value: 'Add Value', 39 | condition: 'Add Condition', 40 | sort: 'Add another sort', 41 | grouping: 'Add Condition Group' 42 | }.freeze 43 | end 44 | 45 | def button_classes 46 | 'inline-flex items-center rounded bg-black/10 px-2.5 py-1.5 text-sm font-medium whitespace-nowrap text-gray-900 hover:bg-black/20' 47 | end 48 | end -------------------------------------------------------------------------------- /app/helpers/search_helper.rb: -------------------------------------------------------------------------------- 1 | module SearchHelper 2 | class << self 3 | def filter_color = "green" 4 | def sort_color = "orange" 5 | def group_color = "purple" 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /app/javascript/application.js: -------------------------------------------------------------------------------- 1 | import "./controllers" 2 | import "./config" 3 | import "./channels" 4 | -------------------------------------------------------------------------------- /app/javascript/channels/consumer.js: -------------------------------------------------------------------------------- 1 | // Action Cable provides the framework to deal with WebSockets in Rails. 2 | // You can generate new channels where WebSocket features live using the `bin/rails generate channel` command. 3 | 4 | import { createConsumer } from "@rails/actioncable" 5 | 6 | export default createConsumer() 7 | -------------------------------------------------------------------------------- /app/javascript/channels/index.js: -------------------------------------------------------------------------------- 1 | import "./**/*_channel.js" 2 | -------------------------------------------------------------------------------- /app/javascript/config/debounced.js: -------------------------------------------------------------------------------- 1 | import debounced from 'debounced' 2 | 3 | debounced.initialize({ input: { wait: 500 } }) 4 | -------------------------------------------------------------------------------- /app/javascript/config/index.js: -------------------------------------------------------------------------------- 1 | import "./**/*.js" 2 | -------------------------------------------------------------------------------- /app/javascript/config/turbo.js: -------------------------------------------------------------------------------- 1 | import { Turbo } from "@hotwired/turbo-rails" 2 | 3 | import TurboPower from "turbo_power" 4 | TurboPower.initialize(Turbo.StreamActions) 5 | -------------------------------------------------------------------------------- /app/javascript/controllers/application.js: -------------------------------------------------------------------------------- 1 | import { Application } from "@hotwired/stimulus" 2 | 3 | const application = Application.start() 4 | 5 | // Configure Stimulus development experience 6 | application.debug = false 7 | window.Stimulus = application 8 | 9 | export { application } 10 | -------------------------------------------------------------------------------- /app/javascript/controllers/application_controller.js: -------------------------------------------------------------------------------- 1 | import { Controller } from "@hotwired/stimulus" 2 | 3 | export default class extends Controller { 4 | 5 | } 6 | -------------------------------------------------------------------------------- /app/javascript/controllers/checkbox_set_controller.js: -------------------------------------------------------------------------------- 1 | import ApplicationController from "./application_controller" 2 | 3 | export default class extends ApplicationController { 4 | static targets = ['parent', 'child', 'count'] 5 | static values = { 6 | total: Number 7 | } 8 | 9 | connect() { 10 | if (!this.hasParentTarget) return 11 | if (!this.hasCountTarget) return 12 | 13 | const parentTarget = this.parentTarget 14 | const setSelectedCount = this.setSelectedCount.bind(this) 15 | this.childTargets.forEach(checkbox => { 16 | checkbox.addEventListener("change", event => { 17 | if (checkbox.checked) { 18 | setSelectedCount() 19 | } else { 20 | parentTarget.checked = false 21 | setSelectedCount() 22 | } 23 | }) 24 | }) 25 | } 26 | 27 | matchAll(e) { 28 | e.preventDefault() 29 | 30 | this.childTargets.forEach(checkbox => { 31 | checkbox.checked = e.currentTarget.checked 32 | }) 33 | 34 | if (e.currentTarget.checked) { 35 | this.setSelectedCount(true) 36 | } else { 37 | this.setSelectedCount(false) 38 | } 39 | } 40 | 41 | deselectAll(e) { 42 | e.preventDefault() 43 | 44 | this.childTargets.forEach(checkbox => { 45 | checkbox.checked = false 46 | }) 47 | 48 | this.setSelectedCount() 49 | } 50 | 51 | selectAll(e) { 52 | e.preventDefault() 53 | 54 | this.childTargets.forEach(checkbox => { 55 | checkbox.checked = true 56 | }) 57 | 58 | this.setSelectedCount() 59 | } 60 | 61 | setSelectedCount(full = false) { 62 | const selectedCount = Array.from(this.childTargets).filter(checkbox => checkbox.checked).length 63 | const fullCount = this.totalValue 64 | this.countTarget.innerHTML = (full ? fullCount : selectedCount).toLocaleString() 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /app/javascript/controllers/clearable_controller.js: -------------------------------------------------------------------------------- 1 | import ApplicationController from "./application_controller" 2 | 3 | export default class extends ApplicationController { 4 | static targets = ['input'] 5 | 6 | clear(event) { 7 | event.preventDefault() 8 | 9 | this.inputTargets.forEach(input => { 10 | input.value = null 11 | }) 12 | 13 | this.element.closest("form").requestSubmit() 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /app/javascript/controllers/column_controller.js: -------------------------------------------------------------------------------- 1 | import ApplicationController from "./application_controller" 2 | import { get } from "@rails/request.js" 3 | import { createPopper } from '@popperjs/core' 4 | 5 | export default class extends ApplicationController { 6 | static targets = ["tooltip", "tooltipTemplate"] 7 | 8 | async update (event) { 9 | if (event instanceof KeyboardEvent) { 10 | if (event.key !== "Enter") return 11 | } 12 | 13 | const form = (event.target instanceof HTMLFormElement) ? event.target : event.target.closest("form") 14 | await form.requestSubmit() 15 | } 16 | 17 | async abort (event) { 18 | event.preventDefault() 19 | 20 | await this.requestEdit(event.target, { "book_edit": "false" }) 21 | } 22 | 23 | async edit (event) { 24 | this.clearSelection() 25 | 26 | await this.requestEdit(event.target, { "book_edit": "true" }) 27 | } 28 | 29 | async requestEdit(target, query = {}) { 30 | const { editUrl, attribute } = target.closest("td, th").dataset 31 | 32 | await get(editUrl, { 33 | query: { 34 | ...query, 35 | 'book_attribute': attribute, 36 | }, 37 | responseKind: "turbo-stream" 38 | }) 39 | } 40 | 41 | clearSelection() { 42 | if (window.getSelection) { 43 | window.getSelection().removeAllRanges() 44 | } else if (document.selection) { 45 | document.selection.empty() 46 | } 47 | } 48 | 49 | tooltipTargetConnected(target) { 50 | this.popper = createPopper(target, this.tooltipTemplateTarget, { 51 | placement: "top" 52 | }) 53 | } 54 | 55 | tooltipTargetDisconnected(target) { 56 | this.popper.destroy() 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /app/javascript/controllers/details_popover_controller.js: -------------------------------------------------------------------------------- 1 | import ApplicationController from "./application_controller" 2 | 3 | export default class extends ApplicationController { 4 | static targets = ['button'] 5 | 6 | connect() { 7 | this.element.addEventListener("toggle", (event) => { 8 | this.buttonTarget.setAttribute('aria-expanded', event.currentTarget.open) 9 | }); 10 | } 11 | 12 | hide(event) { 13 | if (event && (this.element.contains(event.target))) { 14 | return; 15 | } 16 | 17 | this.buttonTarget.setAttribute('aria-expanded', 'false'); 18 | this.element.removeAttribute('open') 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/javascript/controllers/details_set_controller.js: -------------------------------------------------------------------------------- 1 | import ApplicationController from "./application_controller" 2 | 3 | export default class extends ApplicationController { 4 | static targets = ['child'] 5 | 6 | connect () { 7 | this.childTargets.forEach(details => details.addEventListener('toggle', (event) => { 8 | if (!event.currentTarget.open) return 9 | 10 | this.childTargets.forEach((details) => { 11 | if (details == event.currentTarget) return 12 | 13 | details.removeAttribute('open') 14 | }) 15 | })) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/javascript/controllers/element_controller.js: -------------------------------------------------------------------------------- 1 | import ApplicationController from "./application_controller" 2 | 3 | export default class extends ApplicationController { 4 | static targets = [ "click" ] 5 | 6 | click() { 7 | this.clickTargets.forEach(target => target.click()) 8 | } 9 | } -------------------------------------------------------------------------------- /app/javascript/controllers/filter_by_controller.js: -------------------------------------------------------------------------------- 1 | import ApplicationController from "./application_controller" 2 | 3 | export default class extends ApplicationController { 4 | static targets = [ "template" ] 5 | 6 | connect() { 7 | this.element[this.identifier] = this 8 | } 9 | 10 | addFields(event) { 11 | event.preventDefault() 12 | event.stopPropagation() 13 | 14 | const button = event.currentTarget 15 | const type = button.dataset.fieldType 16 | const content = button.dataset.content 17 | const newId = new Date().getTime() 18 | const regexp = new RegExp('new_' + type, 'g') 19 | const newContent = content.replace(regexp, newId) 20 | 21 | button.insertAdjacentHTML('beforebegin', newContent) 22 | } 23 | 24 | removeFields(event) { 25 | event.preventDefault() 26 | event.stopPropagation() 27 | 28 | const button = event.currentTarget 29 | button.closest('.fields').remove(); 30 | } 31 | 32 | nestFields(event) { 33 | event.preventDefault() 34 | event.stopPropagation() 35 | 36 | const button = event.currentTarget 37 | const type = button.dataset.fieldType 38 | 39 | const newId = new Date().getTime() 40 | const idRegexp = new RegExp('new_' + type, 'g') 41 | const objectName = button.closest('.fields').dataset.objectName 42 | const sanitizedObjectName = objectName.replace(/\]\[|[^-a-zA-Z0-9:.]/g, '_').replace(/_$/, '') 43 | let template = this.templateTarget.innerHTML 44 | template = template.replace(/new_object_name\[/g, objectName + "[") 45 | template = template.replace(/new_object_name_/, sanitizedObjectName + '_') 46 | template = template.replace(idRegexp, newId) 47 | 48 | button.insertAdjacentHTML('beforebegin', template) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /app/javascript/controllers/groupable_controller.js: -------------------------------------------------------------------------------- 1 | import ApplicationController from "./application_controller" 2 | 3 | export default class extends ApplicationController { 4 | static targets = [ "row" ] 5 | 6 | connect() {} 7 | 8 | toggle(event) { 9 | const buttonNode = event.currentTarget 10 | const rowNodes = this.rowTargets.filter(row => this.element.contains(row)) 11 | const colllapsedClassname = 'sr-only' 12 | 13 | if (buttonNode.getAttribute('aria-expanded') == 'true') { 14 | buttonNode.setAttribute('aria-expanded', 'false') 15 | rowNodes.forEach((rowNode) => rowNode.classList.add(colllapsedClassname)) 16 | } else { 17 | buttonNode.setAttribute('aria-expanded', 'true') 18 | rowNodes.forEach((rowNode) => rowNode.classList.remove(colllapsedClassname)) 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /app/javascript/controllers/index.js: -------------------------------------------------------------------------------- 1 | import { application } from "./application" 2 | import controllers from "./**/*_controller.js" 3 | 4 | controllers.forEach((controller) => { 5 | application.register(controller.name, controller.module.default) 6 | }) 7 | -------------------------------------------------------------------------------- /app/javascript/controllers/pagy_controller.js: -------------------------------------------------------------------------------- 1 | import ApplicationController from "./application_controller" 2 | 3 | export default class extends ApplicationController { 4 | static targets = [] 5 | 6 | connect() { 7 | const recordsTotalElement = this.element.querySelector('.pagy-info b:nth-child(2)') 8 | if (recordsTotalElement) { 9 | const recordsTotal = Number(recordsTotalElement.innerText) 10 | recordsTotalElement.innerText = recordsTotal.toLocaleString() 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /app/javascript/controllers/sort_by_controller.js: -------------------------------------------------------------------------------- 1 | import ApplicationController from "./application_controller" 2 | 3 | export default class extends ApplicationController { 4 | static targets = ["items", "field"] 5 | 6 | connect() { 7 | if (this.fieldTargets.length === 0) { 8 | this.appendChild() 9 | } 10 | } 11 | 12 | add(event) { 13 | event.preventDefault() 14 | 15 | this.appendChild() 16 | } 17 | 18 | remove(event) { 19 | event.preventDefault() 20 | event.stopPropagation() 21 | 22 | event.target.closest(".fields").remove() 23 | this.updateIndexes() 24 | } 25 | 26 | appendChild() { 27 | const fields = this.fieldTarget.cloneNode(true) 28 | fields.querySelectorAll("select").forEach(field => field.value = null) 29 | 30 | this.itemsTarget.appendChild(fields) 31 | this.updateIndexes() 32 | } 33 | 34 | updateIndexes() { 35 | this.fieldTargets.forEach(field => { 36 | const [column, direction] = field.querySelectorAll("select") 37 | 38 | column.name = `q[s][${this.fieldTargets.indexOf(field)}][name]` 39 | column.id = `q_s_[${this.fieldTargets.indexOf(field)}]_name` 40 | 41 | direction.name = `q[s][${this.fieldTargets.indexOf(field)}][dir]` 42 | direction.id = `q_s_[${this.fieldTargets.indexOf(field)}]_dir` 43 | }) 44 | } 45 | 46 | clear(event) { 47 | event.preventDefault() 48 | 49 | this.element.querySelectorAll("select").forEach(select => select.value = null) 50 | this.element.closest("form").requestSubmit() 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /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/models/application_record.rb: -------------------------------------------------------------------------------- 1 | class ApplicationRecord < ActiveRecord::Base 2 | primary_abstract_class 3 | end 4 | -------------------------------------------------------------------------------- /app/models/author.rb: -------------------------------------------------------------------------------- 1 | class Author < ApplicationRecord 2 | has_many :book_authors 3 | has_many :books, through: :book_authors 4 | end 5 | -------------------------------------------------------------------------------- /app/models/book.rb: -------------------------------------------------------------------------------- 1 | class Book < ApplicationRecord 2 | has_many :book_authors 3 | has_many :authors, through: :book_authors 4 | 5 | # Whitelist the model attributes for sorting 6 | def self.ransortable_attributes(_auth_object = nil) 7 | column_names - ['id', 'created_at', 'updated_at', 'isbn13'] 8 | end 9 | 10 | # Whitelist the model attributes for search 11 | def self.ransackable_attributes(_auth_object = nil) 12 | ransortable_attributes + _ransackers.keys 13 | end 14 | 15 | def self.primary_attribute 16 | 'title' 17 | end 18 | 19 | def self.attribute_schema 20 | { 21 | title: { type: :text, width: "500px", sort: { asc: "A ⮕ Z", desc: "Z ⮕ A", icon: "alpha" } }, 22 | average_rating: { type: :decimal, width: nil, sort: { asc: "1 ⮕ 9", desc: "9 ⮕ 1", icon: "numeric" } }, 23 | isbn: { type: :string, width: nil, sort: { asc: "first ⮕ last", desc: "last ⮕ first", icon: nil } }, 24 | isbn13: { type: :string, width: nil, sort: { asc: "first ⮕ last", desc: "last ⮕ first", icon: nil } }, 25 | language_code: { type: :enum, width: nil, sort: { asc: "first ⮕ last", desc: "last ⮕ first", icon: nil } }, 26 | num_pages: { type: :numeric, width: nil, sort: { asc: "1 ⮕ 9", desc: "9 ⮕ 1", icon: "numeric" } }, 27 | ratings_count: { type: :numeric, width: nil, sort: { asc: "1 ⮕ 9", desc: "9 ⮕ 1", icon: "numeric" } }, 28 | text_reviews_count: { type: :numeric, width: nil, sort: { asc: "1 ⮕ 9", desc: "9 ⮕ 1", icon: "numeric" } }, 29 | published_on: { type: :date, width: nil, sort: { asc: "1 ⮕ 9", desc: "9 ⮕ 1", icon: "numeric" } }, 30 | publisher: { type: :string, width: nil, sort: { asc: "A ⮕ Z", desc: "Z ⮕ A", icon: "alpha" } }, 31 | created_at: { type: :datetime, width: nil, sort: { asc: "1 ⮕ 9", desc: "9 ⮕ 1", icon: "numeric" } }, 32 | updated_at: { type: :datetime, width: nil, sort: { asc: "1 ⮕ 9", desc: "9 ⮕ 1", icon: "numeric" } } 33 | } 34 | end 35 | 36 | def self.to_csv(csv_attributes = [], include_blank = false) 37 | attributes = if defined?(csv_attributes) && csv_attributes.any? 38 | csv_attributes 39 | else 40 | attribute_names - ["created_at", "updated_at"] 41 | end 42 | 43 | CSV.generate(headers: true, col_sep: ";") do |csv| 44 | csv << attributes 45 | 46 | all.each do |record| 47 | csv << record.attributes.values_at(*attributes) 48 | end 49 | 50 | csv << [nil] * attributes.size if include_blank 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /app/models/book_author.rb: -------------------------------------------------------------------------------- 1 | class BookAuthor < ApplicationRecord 2 | belongs_to :book 3 | belongs_to :author 4 | end 5 | -------------------------------------------------------------------------------- /app/models/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fractaledmind/hottable/4905781fbd1e214a4da6a3e9b539c52d8d282c55/app/models/concerns/.keep -------------------------------------------------------------------------------- /app/models/view.rb: -------------------------------------------------------------------------------- 1 | class View < ApplicationRecord 2 | validates :name, presence: true, uniqueness: true 3 | 4 | serialize :parameters, JSON 5 | end 6 | -------------------------------------------------------------------------------- /app/views/books/_form.html.erb: -------------------------------------------------------------------------------- 1 | <%= search_form_for( 2 | search, 3 | url: search_books_path, 4 | html: { id: "searchForm", method: :post, autocomplete: :off, autocapitalize: :none, class: "", data: { controller: "details-set" }, class: "flex justify-between items-center w-full" } 5 | ) do |f| %> 6 | 7 | 18 | 19 | 20 | 21 |
22 | <%= render "books/form/fields", f: %> 23 | <%= render "books/form/sort", f: %> 24 | <%= render "books/form/filter", f: %> 25 | <%= render "books/form/batch", f: %> 26 |
27 | <% end %> 28 | -------------------------------------------------------------------------------- /app/views/books/form/_batch.html.erb: -------------------------------------------------------------------------------- 1 | <%= render Views::Books::Form::Section.new(id: "detailsBatch", data: { controller: "clearable", 'details-set-target': "child" }) do |section| %> 2 | <%= section.title icon: "card-list", colored: f.object.batch.present?, classes: "bg-purple-300 hover:border-purple-500" do %> 3 | <%= f.object.batch.present? ? "Grouped by #{Book.human_attribute_name(f.object.batch_attribute)}" : "Group" %> 4 | <% end %> 5 | 6 | <%= section.body do %> 7 | <%= render(Views::Books::Form::Section::Heading.new) { "Group by" } %> 8 | 9 | <%= tag.fieldset class: "p-2 pt-0 space-y-2" do %> 10 | <% batch_object = f.object.batch.present? ? f.object.batch : f.object.build_batch %> 11 | <%= f.fields_for(:b, batch_object) do |b| %> 12 |
13 | <%= b.collection_select( 14 | :name, 15 | f.object.default_fields.map { |c| [c.name, Book.human_attribute_name(c.name)] }, 16 | :first, :last, 17 | { include_blank: true }, 18 | "data-clearable-target": "input", 19 | class: "text-sm rounded p-1.5 pr-6 border-gray-400 bg-[right_0.125rem_center]") %> 20 | <%= b.collection_select( 21 | :dir, 22 | b.send(:sort_array), 23 | :first, :last, 24 | { include_blank: true }, 25 | "data-clearable-target": "input", 26 | class: "text-sm rounded p-1.5 pr-6 border-gray-400 bg-[right_0.125rem_center]") %> 27 |
28 | 29 |
30 | Select group behavior 31 | 32 |
33 | <%= b.label :expanded, value: true, class: "flex-1 relative flex cursor-pointer rounded-lg border bg-white px-2.5 py-1.5 shadow-sm focus:outline-none" do %> 34 | <%= b.radio_button :expanded, true, checked: b.object.expanded == true || b.object.expanded.nil?, class: "sr-only peer" %> 35 | 36 | 37 | 38 | Expand all 39 | 40 | 41 | 42 | 45 | 46 | 47 | <% end %> 48 | 49 | <%= b.label :expanded, value: false, class: "flex-1 relative flex cursor-pointer rounded-lg border bg-white px-2.5 py-1.5 shadow-sm focus:outline-none" do %> 50 | <%= b.radio_button :expanded, false, checked: b.object.expanded == false, class: "sr-only peer" %> 51 | 52 | 53 | Collapse all 54 | 55 | 56 | 57 | 60 | 61 | 62 | <% end %> 63 |
64 |
65 | <% end %> 66 | <% end %> 67 | 68 |
69 | <%= render(ButtonComponent.new(data: { action: "click->clearable#clear" })) { "Clear" } %> 70 | <%= render(ButtonComponent.new) { "Cancel" } %> 71 | 72 | <%= render(ButtonComponent.new(as: :input, type: :submit, name: "batch", value: "Group", primary: true)) %> 73 |
74 | <% end %> 75 | <% end %> 76 | -------------------------------------------------------------------------------- /app/views/books/form/_fields.html.erb: -------------------------------------------------------------------------------- 1 | <%= render Views::Books::Form::Section.new(id: "detailsFields", data: { 'details-set-target': "child" }) do |section| %> 2 | <%= section.title icon: "eye-slash", colored: f.object.hidden_fields.any?, classes: "bg-blue-300 hover:border-blue-500" do %> 3 | <%= f.object.hidden_fields.any? ? pluralize(f.object.hidden_fields.size, 'hidden field') : "Hide fields" %> 4 | <% end %> 5 | 6 | <%= section.body do %> 7 |
8 | <%= tag.fieldset class: "p-2" do %> 9 | <%= f.collection_check_boxes(:f, f.object.default_fields.reject {|c| c.name == Book.primary_attribute }.map { |c| [c.name, Book.human_attribute_name(c.name)] }, :first, :last) do |builder| %> 10 | <%= builder.label(class: "cursor-pointer py-1 px-2 flex justify-start items-center gap-2 hover:bg-gray-200") do %> 11 | <%= builder.check_box(checked: f.object.field_attributes.include?(builder.value), class: "rounded", data: {checkbox_set_target: "child"}) %> 12 | 13 | <%= builder.text %> 14 | 15 | <% end %> 16 | <% end %> 17 | <% end %> 18 | 19 |
20 | 23 | 26 |
27 |
28 | 29 |
30 | <%= render(ButtonComponent.new) { "Cancel" } %> 31 | 32 | <%= render(ButtonComponent.new(as: :input, type: :submit, name: "field", value: "Set", primary: true)) %> 33 |
34 | <% end %> 35 | <% end %> 36 | -------------------------------------------------------------------------------- /app/views/books/form/_filter.html.erb: -------------------------------------------------------------------------------- 1 | <%= render Views::Books::Form::Section.new(id: "detailsFilter", data: { 'details-set-target': "child", controller: "filter-by clearable" }) do |section| %> 2 | <%= section.title icon: "filter", colored: f.object.condition_attributes.any?, classes: "bg-green-300 hover:border-green-500" do %> 3 | <%= f.object.condition_attributes.any? ? "Filtered by #{f.object.condition_attributes.map {|a| Book.human_attribute_name(a)}.join(', ')}" : "Filter" %> 4 | <% end %> 5 | 6 | <%= section.body do %> 7 | 12 | 13 |
14 | 15 | In this view, show records where 16 | <%= f.combinator_select({}, class: "rounded py-1") %> 17 | of the following are true… 18 | 19 |
20 | <% condition_object = f.object.conditions.any? ? f.object.conditions : f.object.build_condition %> 21 | <%= f.condition_fields(condition_object) do |c| %> 22 | <%= render 'filter/condition_fields', f: c %> 23 | <% end %> 24 | <%= button_to_add_fields(f, :condition) %> 25 | 26 | <%= f.grouping_fields do |g| %> 27 | <%= render 'filter/grouping_fields', f: g %> 28 | <% end %> 29 | <%= button_to_add_fields(f, :grouping) %> 30 |
31 |
32 | 33 |
34 | <%= render(ButtonComponent.new(data: { action: "click->clearable#clear" })) { "Clear" } %> 35 | <%= render(ButtonComponent.new) { "Cancel" } %> 36 | 37 | <%= render(ButtonComponent.new(as: :input, type: :submit, name: "filter", value: "Filter", primary: true)) %> 38 |
39 | <% end %> 40 | <% end %> 41 | -------------------------------------------------------------------------------- /app/views/books/form/_sort.html.erb: -------------------------------------------------------------------------------- 1 | <%= render Views::Books::Form::Section.new(id: "detailsSort", data: { details_set_target: "child", controller: "sort-by clearable" }) do |section| %> 2 | <%= section.title icon: "arrow-down-up", colored: f.object.sorts.any?, classes: "bg-orange-300 hover:border-orange-500" do %> 3 | <%= f.object.sorts.any? ? "Sorted by #{pluralize(f.object.sorts.count, 'field')}" : "Sort" %> 4 | <% end %> 5 | 6 | <%= section.body do %> 7 | <%= render(Views::Books::Form::Section::Heading.new) { "Sort by" } %> 8 | 9 | <%= tag.fieldset class: "p-2 pt-0 space-y-2", data: { sort_by_target: "items" } do %> 10 | <% sort_object = f.object.sorts.any? ? f.object.sorts : f.object.build_sort %> 11 | <%= f.sort_fields(sort_object) do |s| %> 12 |
13 | <%= s.sort_select({}, class: "text-sm rounded p-1.5 pr-6 border-gray-400 bg-[right_0.125rem_center]", "data-clearable-target": "input") %> 14 | <%= render(ButtonComponent.new(data: { action: "click->sort-by#remove" })) { "Remove" } %> 15 |
16 | <% end %> 17 | <% end %> 18 | 19 |
20 | <%= render ButtonComponent.new(data: { action: "click->sort-by#add" }) do %> 21 | <%= render Bootstrap::IconComponent.new("plus-lg") %> 22 | Add another sort 23 | <% end %> 24 |
25 | 26 |
27 | <%= render(ButtonComponent.new(data: { action: "click->clearable#clear" })) { "Clear" } %> 28 | <%= render(ButtonComponent.new(as: :input, type: :submit, name: "sort", value: "Sort", primary: true)) %> 29 |
30 | <% end %> 31 | <% end %> 32 | -------------------------------------------------------------------------------- /app/views/books/form/section.rb: -------------------------------------------------------------------------------- 1 | module Views 2 | class Books::Form::Section < ApplicationComponent 3 | attr_reader :attributes 4 | 5 | def initialize(id:, type: :button, pinned: :right, **attributes) 6 | @id = id 7 | @type = type 8 | @pinned = pinned 9 | @attributes = attributes 10 | end 11 | 12 | def template 13 | render PopoverComponent.new(align: :end, class: "inline-block text-left z-30", id: @id, **attributes) do |popover| 14 | @popover = popover 15 | 16 | yield 17 | end 18 | end 19 | 20 | def title(icon:, colored: false, classes: {}, &block) 21 | @popover.trigger **classes("inline-flex items-center justify-center gap-2 w-full rounded-md border-2 border-transparent bg-white px-4 py-2 font-medium text-gray-700 focus:ring-offset-gray-100", -> { colored } => classes, -> { !colored } => "group-open:border-gray-200 hover:border-gray-300") do 22 | render Bootstrap::IconComponent.new(icon, class: "text-xl") 23 | 24 | span class: "whitespace-nowrap", &block 25 | end 26 | end 27 | 28 | def body(&block) 29 | @popover.portal **classes("mt-1 divide-y divide-gray-100 rounded-md bg-white border-2 shadow-lg overflow-auto max-h-[calc(100vh-250px)] focus:outline-none", -> { @type == :button } => "min-w-72"), &block 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /app/views/books/form/section/heading.rb: -------------------------------------------------------------------------------- 1 | module Views 2 | class Books::Form::Section::Heading< ApplicationComponent 3 | def template(&block) 4 | h3 class: "border-b mb-2 pb-1 font-bold px-2 py-1", &block 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /app/views/books/index.rb: -------------------------------------------------------------------------------- 1 | module Views 2 | class Books::Index < ApplicationComponent 3 | def initialize(records:, result:, search:, pagy:) 4 | @records = records 5 | @result = result 6 | @search = search 7 | @pagy = pagy 8 | end 9 | 10 | def template 11 | render Layout.new do 12 | div class: "bg-violet-700 flex flex-col flex-nowrap overflow-hidden h-full" do 13 | headline 14 | 15 | div class: "grow bg-violet-800 flex flex-col flex-nowrap overflow-hidden" do 16 | tabs 17 | 18 | div class: "grow bg-violet-100 flex flex-col overflow-hidden", data: { controller: "checkbox-set", checkbox_set_total_value: @result.size } do 19 | div class: "shrink flex justify-between items-center flex-wrap bg-white p-2 min-h-16 border-b" do 20 | render "books/form", search: @search 21 | end 22 | 23 | div class: "grow flex items-start -mt-px overflow-scroll", role: "region", aria: { labelledby: "booksTableCaption" }, tabindex: "0" do 24 | render Views::Table.new(@records, result: @result, search: @search, pagy: @pagy) 25 | end 26 | end 27 | end 28 | end 29 | end 30 | end 31 | 32 | private 33 | 34 | def headline 35 | header class: "shrink text-white px-4 py-2 flex items-center justify-between" do 36 | a class: "min-w-0", href: root_path do 37 | h2(class: "text-xl font-bold leading-7 sm:truncate sm:text-2xl sm:tracking-tight") { "Workspace" } 38 | end 39 | div class: "mt-4 flex items-center md:mt-0 md:ml-4" do 40 | svg width: "40", height: "40", viewBox: "0 0 280 300", fill: "white", xmlns: "http://www.w3.org/2000/svg" do 41 | path d: "m130.17 75-68.56 28.4a4.59 4.59 0 0 0 .07 8.51l68.84 27.3a25.52 25.52 0 0 0 18.83 0l68.84-27.3a4.59 4.59 0 0 0 .07-8.51L149.7 75a25.52 25.52 0 0 0-19.53 0" 42 | path d: "M146 154.12v68.19a4.59 4.59 0 0 0 6.29 4.27L229 196.81a4.6 4.6 0 0 0 2.9-4.27v-68.2a4.6 4.6 0 0 0-6.29-4.27l-76.71 29.78a4.58 4.58 0 0 0-2.9 4.27" 43 | path d: "m128.13 157.64-22.77 11-2.31 1.11-48 23c-3 1.47-6.94-.75-6.94-4.13v-64a4.29 4.29 0 0 1 1.47-3.08 5.33 5.33 0 0 1 1.16-.87 4.9 4.9 0 0 1 4.18-.32l72.88 28.87a4.6 4.6 0 0 1 .38 8.41" 44 | end 45 | span { "HotTable" } 46 | end 47 | end 48 | end 49 | 50 | def tabs 51 | div class: "shrink" do 52 | render Books::Tabs.new(View.all.order(created_at: :asc)) 53 | end 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /app/views/books/tab.rb: -------------------------------------------------------------------------------- 1 | module Views 2 | class Books::Tab < ApplicationComponent 3 | def initialize(view) 4 | @view = view 5 | @view_name = @view.name 6 | @rename_id = "rename_#{@view_name}" 7 | end 8 | 9 | def template 10 | if current_page? 11 | render(Books::TabPopover.new(@view.name, active: current_page?)) do |t| 12 | t.body do 13 | div class: "block group-has-peer-checked:hidden divide-y divide-gray-100" do 14 | div class: "py-1" do 15 | render MenuItemComponent.new(as: :label, for: @rename_id, icon: "pencil") do 16 | span { "Rename view" } 17 | input id: @rename_id, type: "checkbox", checked: false, class: "sr-only peer" 18 | end 19 | render MenuItemComponent.new(as: :button, type: "submit", form: "searchForm", formaction: view_path(@view.id), class: "w-full", icon: "sliders2", text: "Update view") 20 | end 21 | unless @view.name == "Books!" 22 | div class: "py-1" do 23 | a href: view_path(@view.id), class: "text-gray-700 group flex items-center px-4 py-2 space-x-2 hover:bg-red-200 hover:text-red-700", role: "menuitem", tabindex: "-1", data_turbo_method: "delete" do 24 | render Bootstrap::IconComponent.new("trash") 25 | span { "Delete view" } 26 | end 27 | end 28 | end 29 | end 30 | 31 | div class: "hidden group-has-peer-checked:block" do 32 | div class: "p-2" do 33 | label(for: "views_name", class: "block text-sm font-medium text-gray-700") { "Name" } 34 | div class: "mt-1" do 35 | input type: "text", value: @view.name, name: "views[#{@view.id}][name]", form: "searchForm", id: "views_name", class: "block w-full text-gray-900 rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm", placeholder: "e.g. 20th century English novels" 36 | end 37 | end 38 | 39 | div class: "flex items-center justify-end gap-2 py-2 px-4 bg-gray-200" do 40 | input type: "submit", value: "Save", form: "searchForm", formaction: view_path(@view.id), class: "inline-flex items-center rounded-md border border-transparent bg-blue-500 hover:bg-blue-400 text-white px-2.5 py-1.5 text-base font-medium text-gray-900 gap-2" 41 | end 42 | end 43 | end 44 | end 45 | else 46 | a id: tab_id, href: books_path(@view.parameters.merge(current_view: @view.name)), 47 | **classes("relative z-40 group rounded-t inline-flex items-center font-medium text-white border-b border-transparent px-4 py-2 hover:border-white hover:bg-violet-900", 48 | current_page?: "bg-white text-gray-800 hover:bg-gray-100"), 49 | aria_current: tokens(current_page?: "page") do 50 | span { @view.name } 51 | end 52 | end 53 | end 54 | 55 | private 56 | 57 | def tab_id 58 | dom_id(@view, :tab) 59 | rescue 60 | "default_tab" 61 | end 62 | 63 | def current_page? 64 | return true if params[:current_view].nil? && @view.name == "Books" 65 | 66 | params[:current_view] == @view.name 67 | end 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /app/views/books/tab_popover.rb: -------------------------------------------------------------------------------- 1 | module Views 2 | class Books::TabPopover < ApplicationComponent 3 | def initialize(title, icon: nil, active: false) 4 | @title = title 5 | @icon = icon 6 | @active = active 7 | end 8 | 9 | def template 10 | render PopoverComponent.new(role: :menu, align: :start, **details_attributes) do |popover| 11 | @popover = popover 12 | 13 | @popover.trigger class: "px-4 py-2 flex items-center gap-1" do 14 | render Bootstrap::IconComponent.new(@icon) if @icon 15 | text @title 16 | end 17 | 18 | yield 19 | end 20 | end 21 | 22 | def body(&) 23 | @popover.portal(**popover_portal_attributes, &) 24 | end 25 | 26 | private 27 | 28 | def details_attributes 29 | { 30 | class: tokens( 31 | "z-40 border-transparent group rounded-t rounded-b-none inline-flex items-center border-b font-medium", 32 | inactive?: "text-white hover:bg-violet-900 open:bg-violet-900 hover:border-white", 33 | active?: "bg-white text-gray-800 hover:bg-gray-100" 34 | ), 35 | data: { 36 | details_set_target: "child" 37 | } 38 | } 39 | end 40 | 41 | def popover_portal_attributes 42 | { 43 | class: "divide-y divide-gray-100 rounded-md bg-white border-2 shadow-lg drop-shadow-lg focus:outline-none w-64 group", 44 | aria: { orientation: "vertical" }, 45 | } 46 | end 47 | 48 | def current_page? = params.dig(:views, :name) == @view.name 49 | def active? = !!@active 50 | def inactive? = !@active 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /app/views/books/tabs.rb: -------------------------------------------------------------------------------- 1 | module Views 2 | class Books::Tabs < ApplicationComponent 3 | def initialize(views) 4 | @views = views 5 | end 6 | 7 | def template 8 | mobile_select 9 | desktop_tabs 10 | end 11 | 12 | private 13 | 14 | def mobile_select 15 | div class: "sm:hidden px-2 space-y-2 mb-2" do 16 | label(for: "tabs", class: "sr-only"){ "Select a tab" } 17 | select id: "tabs", name: "tabs", class: "block w-full rounded-md border-gray-300 focus:border-blue-500 focus:ring-blue-500" do 18 | @views.each do |view| 19 | option(selected: params[:current_view] == view.name || params[:current_view].nil? && view.name == "Books"){ view.name } 20 | end 21 | end 22 | render ButtonComponent.new("New view", icon: "plus-lg", class: "w-full flex") 23 | end 24 | end 25 | 26 | def desktop_tabs 27 | div class: "hidden sm:block border-b border-gray-200" do 28 | nav id: "book_tabs", class: "-mb-px flex space-x-0.5 px-4", 'aria-label': "Tabs", data_controller: "details-set" do 29 | @views.each do |view| 30 | render Books::Tab.new(view) 31 | end 32 | 33 | div id: "new_tab" 34 | 35 | new_view_tab_popover 36 | end 37 | end 38 | end 39 | 40 | def new_view_tab_popover 41 | render(Books::TabPopover.new("New view", icon: "plus-lg")) do |t| 42 | t.body do 43 | div class: "p-2" do 44 | label(for: "views_name", class: "block text-sm font-medium text-gray-700") { "Name" } 45 | div class: "mt-1" do 46 | input type: "text", name: "views[name]", form: "searchForm", id: "views_name", class: "block w-full text-gray-900 rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm", placeholder: "e.g. 20th century English novels" 47 | end 48 | end 49 | 50 | div class: "flex items-center justify-end gap-2 py-2 px-4 bg-gray-200" do 51 | render ButtonComponent.new(as: :input, type: "submit", value: "Save", form: "searchForm", formaction: views_path, primary: true) 52 | end 53 | end 54 | end 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /app/views/filter/_attribute_fields.html.erb: -------------------------------------------------------------------------------- 1 | 2 | <%= f.attribute_select({}, "data-clearable-target": "input", class: "text-sm border-gray-400 rounded p-1.5 pr-6 bg-[right_0.125rem_center]") %> 3 | 4 | -------------------------------------------------------------------------------- /app/views/filter/_condition_fields.html.erb: -------------------------------------------------------------------------------- 1 | <%= tag.fieldset class: "fields condition flex items-center gap-2", 'data-object-name': f.object_name do %> 2 | 3 | 4 |
5 | <%= f.attribute_fields do |a| %> 6 | <%= render 'filter/attribute_fields', f: a %> 7 | <% end %> 8 | 9 | 10 | <%= f.predicate_select({}, class: "text-sm rounded p-1.5 pr-6 border-gray-400 bg-[right_0.125rem_center]") %> 11 | 12 | 13 | 14 | <%= f.value_fields do |v| %> 15 | <%= render 'filter/value_fields', f: v %> 16 | <% end %> 17 | 18 | <%= button_to_add_fields(f, :value) %> 19 | 20 | 21 | <%= button_to_remove_fields %> 22 |
23 | <% end %> -------------------------------------------------------------------------------- /app/views/filter/_grouping_fields.html.erb: -------------------------------------------------------------------------------- 1 | <%= tag.fieldset class: "fields grouping bg-gray-900/5 px-4 py-2 space-y-2", 'data-object-name': f.object_name do %> 2 | 3 |
4 |
5 | Where 6 | <%= f.combinator_select({}, "data-clearable-target": "input", class: "text-sm rounded p-1 pr-6 bg-[right_0.125rem_center]") %> 7 | of the following are true… 8 |
9 |
10 | <%= button_to_remove_fields %> 11 |
12 |
13 |
14 | 15 | <% condition_object = f.object.conditions.any? ? f.object.conditions : f.object.build_condition %> 16 | <%= f.condition_fields(condition_object) do |c| %> 17 | <%= render 'filter/condition_fields', f: c %> 18 | <% end %> 19 | <%= button_to_add_fields(f, :condition) %> 20 | 21 | <%= f.grouping_fields do |g| %> 22 | <%= render 'filter/grouping_fields', f: g %> 23 | <% end %> 24 | <%= button_to_nest_fields(:grouping) %> 25 | <% end %> 26 | -------------------------------------------------------------------------------- /app/views/filter/_value_fields.html.erb: -------------------------------------------------------------------------------- 1 | 2 | <%= f.text_field(:value, "data-clearable-target": "input", class: "text-sm p-1.5 border-0") %> 3 | <%= button_to_remove_fields(labeled: false) %> 4 | 5 | -------------------------------------------------------------------------------- /app/views/layout.rb: -------------------------------------------------------------------------------- 1 | module Views 2 | class Layout < ApplicationComponent 3 | include Phlex::Rails::Layout 4 | include Propshaft::Helper 5 | include ActionView::Helpers::AssetTagHelper 6 | 7 | def template(&) 8 | doctype 9 | 10 | html do 11 | head do 12 | title { "HotTable" } 13 | meta name: "viewport", content: "width=device-width, initial-scale=1" 14 | csp_meta_tag 15 | csrf_meta_tags 16 | link href: asset_path("favicon.ico"), rel: "icon", type: "image/x-icon" 17 | 18 | link rel: "stylesheet", 19 | href: stylesheet_path("application"), 20 | data: { turbo_track: "reload" } 21 | 22 | link rel: "stylesheet", 23 | href: "https://cdn.jsdelivr.net/npm/bootstrap-icons@1.9.1/font/bootstrap-icons.css" 24 | 25 | script type: "text/javascript", 26 | src: javascript_path("application"), 27 | data: { turbo_track: "reload" }, 28 | defer: "defer" 29 | end 30 | 31 | body(&) 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /app/views/table.rb: -------------------------------------------------------------------------------- 1 | module Views 2 | class Table < ApplicationComponent 3 | def initialize(records, result:, search:, pagy:) 4 | @records = records 5 | @result = result 6 | @search = search 7 | @pagy = pagy 8 | end 9 | 10 | def template 11 | table class: "h-full border-r border-gray-300", id: 'table' do 12 | caption id: "booksTableCaption", class: "h-0 overflow-hidden" do 13 | i class: "bi-table", aria_hidden: "true" 14 | "Main View" 15 | end 16 | 17 | render Views::Table::Head.new(search: @search) 18 | 19 | if @search.batch_attribute.present? 20 | group_counts = @result.reorder('').group(@search.batch_attribute).order("books_#{@search.batch_attribute}" => :desc).count 21 | groups = @records.reorder(@search.batch.attr_name => @search.batch.dir).group_by(& @search.batch_attribute.to_sym) 22 | groups.each do |group_name, group_records| 23 | tbody class: "bg-white", data_controller: "groupable" do 24 | render Views::Table::GroupHeader.new(group_name, group_counts[group_name], search: @search) 25 | 26 | group_records.each do |record| 27 | render Views::Table::Row.new(record, search: @search, expanded: @search.batch.expanded) 28 | end 29 | end 30 | end 31 | tbody class: "bg-white" do 32 | tr aria_hidden: "true", class: "bg-violet-100 h-full", style: tokens(-> {!@search.batch.expanded} => "height: calc(100vh - 313px - (59px * #{groups.count}));") do 33 | td colspan: attributes.size + 1, class: "p-0 bg-none" 34 | end 35 | end 36 | else 37 | tbody class: "bg-white" do 38 | @records.each do |record| 39 | render Views::Table::Row.new(record, search: @search) 40 | end 41 | tr aria_hidden: "true", class: "bg-violet-100 h-full" do 42 | td colspan: attributes.size + 1, class: "p-0 bg-none" 43 | end 44 | end 45 | end 46 | 47 | tfoot class: "sticky bottom-0 z-20 bg-gray-100" do 48 | tr class: "h-12" do 49 | td do 50 | end 51 | attributes.each do |attribute| 52 | render Views::Table::ColumnSummary.new(nil, attribute: attribute, calculation: "") 53 | end 54 | end 55 | end 56 | render Views::Table::Footer.new(@search, pagy: @pagy) 57 | end 58 | end 59 | 60 | private 61 | 62 | def attributes = @search.field_attributes 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /app/views/table/column.rb: -------------------------------------------------------------------------------- 1 | module Views 2 | class Table 3 | class Column < ApplicationComponent 4 | COLORS = [ 5 | "bg-red-300 text-red-800", 6 | "bg-orange-300 text-orange-800", 7 | "bg-amber-300 text-amber-800", 8 | "bg-yellow-300 text-yellow-800", 9 | "bg-lime-300 text-lime-800", 10 | "bg-green-300 text-green-800", 11 | "bg-emerald-300 text-emerald-800", 12 | "bg-teal-300 text-teal-800", 13 | "bg-cyan-300 text-cyan-800", 14 | "bg-sky-300 text-sky-800", 15 | "bg-blue-300 text-blue-800", 16 | "bg-indigo-300 text-indigo-800", 17 | "bg-violet-300 text-violet-800", 18 | "bg-purple-300 text-purple-800", 19 | "bg-fuschia-300 text-fuschia-800", 20 | "bg-pink-300 text-pink-800", 21 | "bg-rose-300 text-rose-800", 22 | ] 23 | include ActionView::Helpers::NumberHelper 24 | 25 | def initialize(record, attribute:, search:) 26 | @record = record 27 | @attribute = attribute 28 | @search = search 29 | end 30 | 31 | def template 32 | cell 33 | end 34 | 35 | private 36 | 37 | def cell 38 | if @attribute.to_s == Book.primary_attribute.to_s 39 | th(scope: "row", 40 | **classes("cursor-pointer text-sm font-medium text-gray-900 text-left sticky left-[calc(3rem+1px)] row-group-has-checked:text-blue-900", 41 | filtered?: "bg-green-100 row-group-hover:bg-gray-green-100-mixed row-group-has-checked:bg-blue-green-100-mixed", 42 | sorted?: "bg-orange-100 row-group-hover:bg-gray-orange-100-mixed row-group-has-checked:bg-blue-orange-100-mixed", 43 | grouped?: "bg-purple-100 row-group-hover:bg-gray-purple-100-mixed row-group-has-checked:bg-blue-purple-100-mixed", 44 | -> { !filtered? && !sorted? && !grouped? } => "bg-white", 45 | -> { [:numeric, :decimal].include? attribute_type } => "text-right", 46 | -> { attribute_type == :enum } => "text-center"), 47 | id: dom_id(@record, "column_#{@attribute}"), 48 | data: { 49 | id: @record.id, 50 | controller: "column", 51 | action: self.class == Views::Table::Column ? "dblclick->column#edit" : "", 52 | attribute: @attribute, 53 | attribute_type: attribute_type, 54 | edit_url: edit_book_path(@record), 55 | }, 56 | style: "max-width: #{attribute_schema.fetch(:width, "initial")}") do 57 | body 58 | end 59 | else 60 | td(**classes("text-sm text-gray-500 cursor-pointer whitespace-nowrap text-ellipsis overflow-hidden", 61 | filtered?: "bg-green-100 row-group-hover:bg-gray-green-100-mixed row-group-has-checked:bg-green-100/50", 62 | sorted?: "bg-orange-100 row-group-hover:bg-gray-orange-100-mixed row-group-has-checked:bg-orange-100/50", 63 | grouped?: "bg-purple-100 row-group-hover:bg-gray-purple-100-mixed row-group-has-checked:bg-purple-100/50", 64 | -> { [:numeric, :decimal].include? attribute_type } => "text-right", 65 | -> { attribute_type == :enum } => "text-center"), 66 | id: dom_id(@record, "column_#{@attribute}"), 67 | data: { 68 | id: @record.id, 69 | controller: "column", 70 | action: self.class == Views::Table::Column ? "dblclick->column#edit" : "", 71 | attribute: @attribute, 72 | attribute_type: attribute_type, 73 | edit_url: edit_book_path(@record), 74 | }, 75 | style: "max-width: #{attribute_schema.fetch(:width, "initial")}") do 76 | body 77 | end 78 | end 79 | end 80 | 81 | def body 82 | return div(class: "px-2 py-2 whitespace-nowrap text-ellipsis overflow-hidden") { number_with_precision(value, precision: Book.columns_hash[@attribute].sql_type_metadata.scale) } if attribute_type == :decimal 83 | return div(class: "px-2 py-2 whitespace-nowrap text-ellipsis overflow-hidden") { value } if attribute_type != :enum 84 | 85 | div(**classes("inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium", tailwind_color_for_enum)) { @record.public_send(@attribute).to_s } 86 | end 87 | 88 | def filtered? = @search.condition_attributes.include? @attribute 89 | def sorted? = @search.sort_attributes.include? @attribute 90 | def grouped? = @search.batch_attribute == @attribute 91 | def attribute_schema = Book.attribute_schema.fetch(@attribute.to_sym) 92 | def attribute_type = attribute_schema[:type] 93 | 94 | def value = @record.public_send(@attribute).to_s 95 | 96 | def tailwind_color_for_enum 97 | index = (Digest::MD5.hexdigest(value).to_i(16) % COLORS.size) - 1 98 | COLORS[index] 99 | end 100 | end 101 | end 102 | end 103 | -------------------------------------------------------------------------------- /app/views/table/column_edit.rb: -------------------------------------------------------------------------------- 1 | module Views 2 | class Table 3 | class ColumnEdit < Column 4 | 5 | private 6 | 7 | def body 8 | form action: book_path(@record), method: "patch", data: { action: "submit->column#update" } do 9 | if attribute_type != :enum 10 | input( 11 | value: value, 12 | data: { action: "keypress->column#update blur->column#abort", column_target: "tooltip" }, 13 | name: "book[#{@attribute}]", 14 | class: "w-full px-2 py-2 text-sm", 15 | type: input_type(attribute_type) 16 | ) 17 | else 18 | select(data: { action: "change->column#update blur->column#abort" }, name: "book[#{@attribute}]") do 19 | option(value: "") { "" } 20 | 21 | Book.all.distinct.pluck(@attribute).sort.each do |code| 22 | option(value: code, selected: code == value) { code } 23 | end 24 | end 25 | end 26 | 27 | div data: { column_target: "tooltipTemplate" }, class: "w-inherit bg-gray-200 p-2 rounded mb-2 mt-2 drop-shadow" do 28 | button class: "bg-red-400 hover:bg-red-500 py-1 px-2 mx-1 text-white rounded-full", data: { action: "click->column#abort" } do 29 | i class: "bi-x-circle-fill" 30 | end 31 | button class: "bg-green-400 hover:bg-green-500 py-1 px-2 mx-1 text-white rounded-full", data: { action: "click->column#update" } do 32 | i class: "bi-check-circle-fill" 33 | end 34 | end 35 | 36 | input type: "hidden", name: "book_attribute", value: @attribute 37 | end 38 | end 39 | 40 | def input_type(type) 41 | case type 42 | when :numeric 43 | "number" 44 | when :date 45 | "date" 46 | else 47 | "text" 48 | end 49 | end 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /app/views/table/column_summary.rb: -------------------------------------------------------------------------------- 1 | module Views 2 | class Table 3 | class ColumnSummary < ApplicationComponent 4 | def initialize(total, attribute:, calculation: nil) 5 | @total = total || nil 6 | @attribute = attribute 7 | @calculation = calculation 8 | end 9 | 10 | def template 11 | td id: [@attribute, "summary"].join("_") do 12 | form action: summarize_books_path, method: "get", accept_charset: "UTF-8", class: "text-right space-y-1 px-2", data: { controller: "element"} do 13 | div class: "whitespace-nowrap" do 14 | input type: "hidden", name: "attribute", value: @attribute 15 | select name: "calculation", id: "calculation", dir: "rtl", class: "bg-transparent border-transparent rounded text-gray-600 hover:bg-gray-300", data: { action: "change->element#click" }, autocomplete: "off" do 16 | option(value: "", selected: (@calculation == "")) { "None" } 17 | option(value: "nil", selected: (@calculation == "nil")) { "Empty" } 18 | option(value: "not_nil", selected: (@calculation == "not_nil")) { "Filled" } 19 | option(value: "unique", selected: (@calculation == "unique")) { "Unique" } 20 | option(value: "min", selected: (@calculation == "min")) { "Min" } if [:numeric, :decimal].include?(attribute_type) 21 | option(value: "max", selected: (@calculation == "max")) { "Max" } if [:numeric, :decimal].include?(attribute_type) 22 | option(value: "avg", selected: (@calculation == "avg")) { "Average" } if [:numeric, :decimal].include?(attribute_type) 23 | option(value: "sum", selected: (@calculation == "sum")) { "Sum" } if [:numeric, :decimal].include?(attribute_type) 24 | option(value: "earliest", selected: (@calculation == "earliest")) { "Earliest Date" } if [:date, :datetime].include?(attribute_type) 25 | option(value: "latest", selected: (@calculation == "latest")) { "Latest Date" } if [:date, :datetime].include?(attribute_type) 26 | end 27 | 28 | output(class: "inline-block text-left ml-2") { @total.to_s } 29 | end 30 | noscript do 31 | div class: "flex justify-end gap-1" do 32 | input type: "reset", value: "Cancel", class: "cursor-pointer inline-flex items-center rounded-md border border-transparent bg-gray-200 px-2 py-1 text-base font-medium text-gray-900 hover:bg-gray-300" 33 | 34 | input type: "submit", value: "Apply", class: "inline-flex items-center rounded-md border border-transparent bg-blue-500 px-2 py-1 text-base font-medium text-white shadow-sm hover:bg-blue-400" 35 | end 36 | end 37 | input type: "submit", name: "pagy", hidden: true, data: { 'element-target': "click" } 38 | authenticity_token_input 39 | end 40 | end 41 | end 42 | 43 | private 44 | 45 | def attribute_schema = Book.attribute_schema.fetch(@attribute.to_sym) 46 | def attribute_type = attribute_schema[:type] 47 | 48 | def authenticity_token_input 49 | input type: "hidden", 50 | name: "authenticity_token", 51 | autocomplete: "off", 52 | value: @_view_context.form_authenticity_token 53 | end 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /app/views/table/footer.rb: -------------------------------------------------------------------------------- 1 | module Views 2 | class Table 3 | class Footer < ApplicationComponent 4 | include Pagy::Frontend 5 | 6 | def initialize(search, pagy:) 7 | @search = search 8 | @pagy = pagy 9 | end 10 | 11 | def template 12 | tfoot class: "sticky bottom-12 z-20 bg-gray-100" do 13 | tr do 14 | td colspan: attributes.size + 1 do 15 | div class: "sticky left-0 flex flex-wrap items-center justify-between py-2 px-4 gap-4 max-w-screen", data_controller: "pagy" do 16 | page_items_form 17 | unsafe_raw pagy_nav(@pagy) 18 | unsafe_raw pagy_info(@pagy) 19 | end 20 | end 21 | end 22 | end 23 | end 24 | 25 | private 26 | 27 | def attributes = @search.field_attributes 28 | 29 | def page_items_form 30 | div data_controller: "element" do 31 | select name: "page_items", form: "searchForm", id: "page_items", class: "rounded-md border-gray-300 py-2 pl-3 pr-10 text-base focus:border-indigo-500 focus:outline-none focus:ring-indigo-500 sm:text-sm", data: { action: "change->element#click" }, autocomplete: "off" do 32 | [10, 20, 50, 100].map do |item| 33 | option(value: item.to_s, selected: Array(params[:page_items]).include?(item.to_s) || Pagy::DEFAULT[:items] == item) { item.to_s } 34 | end 35 | end 36 | noscript do 37 | input type: "submit", 38 | name: "pagy", 39 | value: "Update", 40 | class: "cursor-pointer inline-flex items-center justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 sm:w-auto", 41 | data_disable_with: "Update" 42 | end 43 | input type: "submit", form: "searchForm", name: "pagy", hidden: true, data: { 'element-target': "click" } 44 | end 45 | end 46 | 47 | def authenticity_token_input 48 | input type: "hidden", 49 | name: "authenticity_token", 50 | autocomplete: "off", 51 | value: @_view_context.form_authenticity_token 52 | end 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /app/views/table/group_header.rb: -------------------------------------------------------------------------------- 1 | module Views 2 | class Table 3 | class GroupHeader < ApplicationComponent 4 | def initialize(group_name, group_count, search:) 5 | @group_name = group_name 6 | @group_count = group_count 7 | @search = search 8 | end 9 | 10 | def template 11 | tr class: "bg-gray-100 sticky top-12 z-10 bg-gray-100" do 12 | th colspan: "2", scope: "rowgroup", class: "sticky left-0 p-0 align-middle bg-gray-100" do 13 | button class: "group p-2 w-full flex items-center", 14 | aria_haspopup: "true", 15 | aria_expanded: @search.batch.expanded.to_s, 16 | data_action: "groupable#toggle", 17 | data_groupable_target: "button" do 18 | span class: "mr-6 ml-2 rounded w-6 h-6 group-hover:bg-gray-300" do 19 | i class: "expanded:bi-chevron-down bi-chevron-right" 20 | end 21 | div class: "flex flex-1 items-center justify-between" do 22 | div class: "flex flex-col text-left" do 23 | small(class: "uppercase font-bold text-gray-600") { Book.human_attribute_name(@search.batch_attribute) } 24 | span(class: "h5 mb-0 whitespace-nowrap") { @group_name.to_s } 25 | end 26 | div class: "space-x-1" do 27 | small(class: "font-normal text-gray-500") { "Count" } 28 | span(class: "monospace-numbers inline-flex items-center rounded-full bg-gray-300 px-2.5 py-0.5 text-xs font-medium text-gray-900 monospace-numbers") { @group_count.to_s } 29 | end 30 | end 31 | end 32 | end 33 | if (@search.field_attributes.size - 1) > 0 34 | td colspan: @search.field_attributes.size - 1 35 | end 36 | end 37 | end 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /app/views/table/head.rb: -------------------------------------------------------------------------------- 1 | module Views 2 | class Table 3 | class Head < ApplicationComponent 4 | def initialize(search:) 5 | @search = search 6 | end 7 | 8 | def template 9 | thead class: "bg-gray-50" do 10 | tr class: "h-12" do 11 | select_cell 12 | attributes.each do |attribute| 13 | render Header.new(attribute, search: @search) 14 | end 15 | end 16 | end 17 | end 18 | 19 | private 20 | 21 | def attributes = @search.field_attributes 22 | 23 | def select_cell 24 | th scope: 'col', 25 | class: 'w-12 min-w-12 max-w-12 relative text-center sticky top-0 left-px z-10 bg-gray-50 whitespace-nowrap px-2 py-3.5 text-sm font-semibold text-gray-900', 26 | id: 'column_select' do 27 | label for: "selectAll", class: "absolute inset-0" 28 | input type: "checkbox", 29 | id: "selectAll", 30 | class: "rounded border-gray-300 text-blue-600 focus:ring-blue-500", 31 | name: "selectAll", 32 | form: "searchForm", 33 | data: { 34 | action: "checkbox-set#matchAll", 35 | checkbox_set_target: "parent" 36 | } 37 | end 38 | end 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /app/views/table/header.rb: -------------------------------------------------------------------------------- 1 | module Views 2 | class Table 3 | class Header < ApplicationComponent 4 | delegate :params, to: :@_view_context 5 | 6 | def initialize(attribute, search:) 7 | @attribute = attribute 8 | @search = search 9 | end 10 | 11 | def template 12 | th(**header_attributes) do 13 | render MenuComponent.new(**popover_component_props) do |menu| 14 | menu.trigger(**popover_trigger_attributes) do 15 | column_icon 16 | span { Book.human_attribute_name(@attribute) } 17 | end 18 | menu.portal(**popover_portal_attributes) do 19 | menu.group do 20 | render MenuItemComponent.new(**sort_asc_menu_item_props) 21 | render MenuItemComponent.new(**sort_desc_menu_item_props) 22 | end 23 | menu.group do 24 | render MenuItemComponent.new(**group_menu_item_props) 25 | render MenuItemComponent.new(**hide_field_menu_item_props) 26 | end 27 | end 28 | end 29 | end 30 | end 31 | 32 | private 33 | 34 | def header_attributes 35 | { 36 | scope: :col, 37 | style: "width: #{attribute_schema[:width] || 'initial'}", 38 | class: tokens( 39 | "sticky top-0 z-20 whitespace-nowrap p-0 text-left text-sm font-semibold text-gray-900 space-x-1", 40 | conditionless?: "bg-gray-50", 41 | filtered?: "bg-#{SearchHelper.filter_color}-300", 42 | sorted?: "bg-#{SearchHelper.sort_color}-300", 43 | grouped?: "bg-#{SearchHelper.group_color}-300", 44 | primary_attribute?: "left-[calc(3rem+1px)] z-30" 45 | ), 46 | aria: { 47 | sort: sorted? && (@search.sorts.find { |sort| sort.attr_name == @attribute }.dir + 'ending') 48 | }, 49 | } 50 | end 51 | 52 | def popover_component_props 53 | { 54 | align: :start, 55 | class: "inline-block text-left w-full h-full", 56 | data: { 57 | details_set_target: "child", 58 | }, 59 | } 60 | end 61 | 62 | def popover_trigger_attributes 63 | { 64 | class: "p-2 flex items-center gap-2", 65 | } 66 | end 67 | 68 | def popover_portal_attributes 69 | { 70 | class: "overflow-auto max-h-[calc(100vh-250px)] origin-top-right", 71 | } 72 | end 73 | 74 | def column_icon 75 | return render(Bootstrap::IconComponent.new(attribute_icon)) unless sorted? 76 | 77 | span(**classes("inline-flex flex-col items-center", -> { sort_dir == "asc" } => "-mt-3", -> { sort_dir == "desc" } => "-mb-3")) do 78 | sort_index_indicator 79 | sort_dir_indicator 80 | end 81 | end 82 | 83 | def sort_index = @search.sorts.index { |sort| sort.attr_name == @attribute } + 1 84 | def sort_index_indicator 85 | span(class: "bg-orange-400 h-5 w-5 p-1 text-xs text-center leading-none rounded-full z-50", id: "SortPriorityColTitle", aria_hidden: "true") { sort_index.to_s } 86 | end 87 | 88 | def sort_dir = @search.sorts.find { |sort| sort.attr_name == @attribute }.dir 89 | def sort_dir_indicator 90 | if sort_dir == "asc" 91 | svg class: "h-3 w-3 text-orange-500 order-first", viewBox: "0 0 425 233.7", fill: "currentColor", focusable: "false", aria_hidden: "true" do 92 | path d: "M414.4 223.1L212.5 21.2 10.6 223.1" 93 | end 94 | else 95 | svg class: "h-3 w-3 text-orange-500 order-last", viewBox: "0 0 425 233.7", fill: "currentColor", focusable: "false", aria_hidden: "true" do 96 | path d: "M10.6 10.6l201.9 201.9L414.4 10.6" 97 | end 98 | end 99 | end 100 | 101 | def sort_asc_menu_item_props 102 | { 103 | url: search_and_sort_path(:asc), 104 | text: "Sort #{attribute_schema[:sort][:asc]}", 105 | icon: ["sort", attribute_schema[:sort][:icon], "down"].compact.join("-"), 106 | } 107 | end 108 | 109 | def sort_desc_menu_item_props 110 | { 111 | url: search_and_sort_path(:desc), 112 | text: "Sort #{attribute_schema[:sort][:desc]}", 113 | icon: ["sort", attribute_schema[:sort][:icon], "down-alt"].compact.join("-"), 114 | } 115 | end 116 | 117 | def group_menu_item_props 118 | { 119 | url: search_and_batch_path, 120 | text: "Group by this field", 121 | icon: "card-list", 122 | } 123 | end 124 | 125 | def hide_field_menu_item_props 126 | { 127 | url: search_and_fields_path, 128 | text: "Hide field", 129 | icon: "eye-slash", 130 | } 131 | end 132 | 133 | def filtered? = @search.condition_attributes.include? @attribute 134 | def sorted? = @search.sort_attributes.include? @attribute 135 | def grouped? = @search.batch_attribute == @attribute 136 | def conditionless? = !filtered? && !sorted? && !grouped? 137 | def primary_attribute? = Book.primary_attribute.to_s == @attribute.to_s 138 | def attribute_schema = Book.attribute_schema.fetch(@attribute.to_sym) 139 | def search_params = params.to_unsafe_hash[@search.context.search_key].presence || {} 140 | 141 | def attribute_icon 142 | { 143 | string: "type", 144 | text: "text-paragraph", 145 | date: "calendar", 146 | numeric: "hash", 147 | decimal: "hash", 148 | enum: "usb-c-fill", 149 | datetime: "clock-fill" 150 | }.fetch(attribute_schema[:type]) 151 | end 152 | 153 | def search_and_sort_path(dir) 154 | search_and_sort_params = search_params.merge(s: [@attribute, dir].join(" ")) 155 | url_params = params.to_unsafe_hash.merge(@search.context.search_key => search_and_sort_params) 156 | url_for({only_path: true}.merge(url_params)) 157 | end 158 | 159 | def search_and_batch_path 160 | search_and_batch_params = search_params.merge(b: {name: @attribute, dir: :asc, expanded: true}) 161 | url_params = params.to_unsafe_hash.merge(@search.context.search_key => search_and_batch_params) 162 | url_for({only_path: true}.merge(url_params)) 163 | end 164 | 165 | def search_and_fields_path 166 | search_and_fields_params = search_params.merge(f: @search.field_attributes - [@attribute]) 167 | url_params = params.to_unsafe_hash.merge(@search.context.search_key => search_and_fields_params) 168 | url_for({only_path: true}.merge(url_params)) 169 | end 170 | end 171 | end 172 | end 173 | -------------------------------------------------------------------------------- /app/views/table/row.rb: -------------------------------------------------------------------------------- 1 | module Views 2 | class Table 3 | class Row < ApplicationComponent 4 | def initialize(record, search:, expanded: true, inline_edit: false) 5 | @record = record 6 | @search = search 7 | @expanded = expanded 8 | @inline_edit = inline_edit 9 | end 10 | 11 | def template 12 | tr(**classes("row-group hover:bg-gray-100 has-checked:bg-blue-100", 13 | -> { !@expanded } => "sr-only"), id: dom_id(@record, :row), data_groupable_target: "row") do 14 | select_cell 15 | attributes.each do |attribute| 16 | render column_class.new(@record, attribute:, search: @search) 17 | end 18 | end 19 | end 20 | 21 | private 22 | 23 | def column_class 24 | @inline_edit ? ColumnEdit : Column 25 | end 26 | 27 | def attributes 28 | @search.field_attributes 29 | end 30 | 31 | def select_cell 32 | td(class: "text-center relative sticky left-px bg-white w-12 text-sm") do 33 | input(type: "checkbox", 34 | id: select_identifier, 35 | class: "hidden [.row-group:hover_&]:inline-block checked:inline-block peer rounded border-gray-300 text-blue-600 focus:ring-blue-500", 36 | name: "select[#{@record.id}]", 37 | form: "searchForm", 38 | aria: { 39 | labelledby: "row_1 column_select" 40 | }, 41 | data: { 42 | checkbox_set_target: "child", 43 | row_checkbox: true 44 | }) 45 | span(class: "ml-1 text-gray-600 inline-block [.row-group:hover_&]:hidden peer-checked:hidden") { @record.id.to_s } 46 | label for: select_identifier, class: "absolute inset-0" 47 | div class: "absolute inset-y-0 left-0 w-1 bg-blue-600 hidden peer-checked:block" 48 | end 49 | end 50 | 51 | def select_identifier 52 | dom_id(@record, "select") 53 | end 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /bin/bundle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # 5 | # This file was generated by Bundler. 6 | # 7 | # The application 'bundle' is installed as part of a gem, and 8 | # this file is here to facilitate running it. 9 | # 10 | 11 | require "rubygems" 12 | 13 | m = Module.new do 14 | module_function 15 | 16 | def invoked_as_script? 17 | File.expand_path($0) == File.expand_path(__FILE__) 18 | end 19 | 20 | def env_var_version 21 | ENV["BUNDLER_VERSION"] 22 | end 23 | 24 | def cli_arg_version 25 | return unless invoked_as_script? # don't want to hijack other binstubs 26 | return unless "update".start_with?(ARGV.first || " ") # must be running `bundle update` 27 | bundler_version = nil 28 | update_index = nil 29 | ARGV.each_with_index do |a, i| 30 | if update_index && update_index.succ == i && a =~ Gem::Version::ANCHORED_VERSION_PATTERN 31 | bundler_version = a 32 | end 33 | next unless a =~ /\A--bundler(?:[= ](#{Gem::Version::VERSION_PATTERN}))?\z/ 34 | bundler_version = $1 35 | update_index = i 36 | end 37 | bundler_version 38 | end 39 | 40 | def gemfile 41 | gemfile = ENV["BUNDLE_GEMFILE"] 42 | return gemfile if gemfile && !gemfile.empty? 43 | 44 | File.expand_path("../../Gemfile", __FILE__) 45 | end 46 | 47 | def lockfile 48 | lockfile = 49 | case File.basename(gemfile) 50 | when "gems.rb" then gemfile.sub(/\.rb$/, gemfile) 51 | else "#{gemfile}.lock" 52 | end 53 | File.expand_path(lockfile) 54 | end 55 | 56 | def lockfile_version 57 | return unless File.file?(lockfile) 58 | lockfile_contents = File.read(lockfile) 59 | return unless lockfile_contents =~ /\n\nBUNDLED WITH\n\s{2,}(#{Gem::Version::VERSION_PATTERN})\n/ 60 | Regexp.last_match(1) 61 | end 62 | 63 | def bundler_requirement 64 | @bundler_requirement ||= 65 | env_var_version || cli_arg_version || 66 | bundler_requirement_for(lockfile_version) 67 | end 68 | 69 | def bundler_requirement_for(version) 70 | return "#{Gem::Requirement.default}.a" unless version 71 | 72 | bundler_gem_version = Gem::Version.new(version) 73 | 74 | requirement = bundler_gem_version.approximate_recommendation 75 | 76 | return requirement unless Gem.rubygems_version < Gem::Version.new("2.7.0") 77 | 78 | requirement += ".a" if bundler_gem_version.prerelease? 79 | 80 | requirement 81 | end 82 | 83 | def load_bundler! 84 | ENV["BUNDLE_GEMFILE"] ||= gemfile 85 | 86 | activate_bundler 87 | end 88 | 89 | def activate_bundler 90 | gem_error = activation_error_handling do 91 | gem "bundler", bundler_requirement 92 | end 93 | return if gem_error.nil? 94 | require_error = activation_error_handling do 95 | require "bundler/version" 96 | end 97 | return if require_error.nil? && Gem::Requirement.new(bundler_requirement).satisfied_by?(Gem::Version.new(Bundler::VERSION)) 98 | 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}'`" 99 | exit 42 100 | end 101 | 102 | def activation_error_handling 103 | yield 104 | nil 105 | rescue StandardError, LoadError => e 106 | e 107 | end 108 | end 109 | 110 | m.load_bundler! 111 | 112 | if m.invoked_as_script? 113 | load Gem.bin_path("bundler", "bundle") 114 | end 115 | -------------------------------------------------------------------------------- /bin/dev: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if ! foreman version &> /dev/null 4 | then 5 | echo "Installing foreman..." 6 | gem install foreman 7 | fi 8 | 9 | foreman start -f Procfile.dev "$@" 10 | -------------------------------------------------------------------------------- /bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | APP_PATH = File.expand_path("../config/application", __dir__) 3 | require_relative "../config/boot" 4 | require "rails/commands" 5 | -------------------------------------------------------------------------------- /bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require_relative "../config/boot" 3 | require "rake" 4 | Rake.application.run 5 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require "fileutils" 3 | 4 | # path to your application root. 5 | APP_ROOT = File.expand_path("..", __dir__) 6 | 7 | def system!(*args) 8 | system(*args) || abort("\n== Command #{args} failed ==") 9 | end 10 | 11 | FileUtils.chdir APP_ROOT do 12 | # This script is a way to set up or update your development environment automatically. 13 | # This script is idempotent, so that you can run it at any time and get an expectable outcome. 14 | # Add necessary setup steps to this file. 15 | 16 | puts "== Installing dependencies ==" 17 | system! "gem install bundler --conservative" 18 | system("bundle check") || system!("bundle install") 19 | 20 | # puts "\n== Copying sample files ==" 21 | # unless File.exist?("config/database.yml") 22 | # FileUtils.cp "config/database.yml.sample", "config/database.yml" 23 | # end 24 | 25 | puts "\n== Preparing database ==" 26 | system! "bin/rails db:prepare" 27 | 28 | puts "\n== Removing old logs and tempfiles ==" 29 | system! "bin/rails log:clear tmp:clear" 30 | 31 | puts "\n== Restarting application server ==" 32 | system! "bin/rails restart" 33 | end 34 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require_relative "config/environment" 4 | 5 | run Rails.application 6 | Rails.application.load_server 7 | -------------------------------------------------------------------------------- /config/application.rb: -------------------------------------------------------------------------------- 1 | require_relative "boot" 2 | 3 | require "rails" 4 | # Pick the frameworks you want: 5 | require "active_model/railtie" 6 | require "active_job/railtie" 7 | require "active_record/railtie" 8 | require "active_storage/engine" 9 | require "action_controller/railtie" 10 | # require "action_mailer/railtie" 11 | # require "action_mailbox/engine" 12 | require "action_text/engine" 13 | require "action_view/railtie" 14 | require "action_cable/engine" 15 | # require "rails/test_unit/railtie" 16 | 17 | # Require the gems listed in Gemfile, including any gems 18 | # you've limited to :test, :development, or :production. 19 | Bundler.require(*Rails.groups) 20 | 21 | module Team18 22 | class Application < Rails::Application 23 | # Initialize configuration defaults for originally generated Rails version. 24 | config.load_defaults 7.0 25 | config.autoload_paths << "#{root}/app" 26 | 27 | # Configuration for the application, engines, and railties goes here. 28 | # 29 | # These settings can be overridden in specific environments using the files 30 | # in config/environments, which are processed later. 31 | # 32 | # config.time_zone = "Central Time (US & Canada)" 33 | # config.eager_load_paths << Rails.root.join("extras") 34 | 35 | # Don't generate system test files. 36 | config.generators.system_tests = nil 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /config/boot.rb: -------------------------------------------------------------------------------- 1 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) 2 | 3 | require "bundler/setup" # Set up gems listed in the Gemfile. 4 | require "bootsnap/setup" # Speed up boot time by caching expensive operations. 5 | -------------------------------------------------------------------------------- /config/cable.yml: -------------------------------------------------------------------------------- 1 | development: 2 | adapter: redis 3 | url: redis://localhost:6379/1 4 | 5 | test: 6 | adapter: test 7 | 8 | production: 9 | adapter: redis 10 | url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %> 11 | channel_prefix: team_18_production 12 | -------------------------------------------------------------------------------- /config/credentials.yml.enc: -------------------------------------------------------------------------------- 1 | bncssW4z6OnIrfodiyhpv52SXLX1O1udGWHQIUC/S9L+oYfO5+TqpwKqV9EbOn0lWCIS8HSOi6Z08lr9ripP5pGlo+ZdFXGRHNmFdUVfp5H16oW9UaX40kLiIh5YOL/omKGBPeT/dv+haZwnWXGaVCAeknP23R+DffXzbRIsg4IIRkBHkCLnf7DlX6eFkG2eZ7hsSjWL7goLPPr0ws6XB5jfayjE3X+kBKK+0urfe/viukOA6cp95RVtJnj5VLkVuc2r1/6cX4OPc89OaAWex6KLcLPgw7igf37dfxaQAjzGIjaIzFpLd99WmNjDFEVei9rsmtFvU/R1UpP0hAJEYTFaKsMonNrI5a/O3oS1o6z2IYX2G+5QGblERoQxWa48WXJS7Aytr6a/CdK/CskM0npEHnUOWruC6aZT--ARf/m1ar3KNw6Bxn--pjtywK26dGNj5wu2jnmSfQ== -------------------------------------------------------------------------------- /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 | development: 25 | <<: *default 26 | database: team_18_development 27 | 28 | # The specified database role being used to connect to postgres. 29 | # To create additional roles in postgres see `$ createuser --help`. 30 | # When left blank, postgres will use the default role. This is 31 | # the same name as the operating system user running Rails. 32 | #username: team_18 33 | 34 | # The password associated with the postgres role (username). 35 | #password: 36 | 37 | # Connect on a TCP socket. Omitted by default since the client uses a 38 | # domain socket that doesn't need configuration. Windows does not have 39 | # domain sockets, so uncomment these lines. 40 | #host: localhost 41 | 42 | # The TCP port the server listens on. Defaults to 5432. 43 | # If your server runs on a different port number, change accordingly. 44 | #port: 5432 45 | 46 | # Schema search path. The server defaults to $user,public 47 | #schema_search_path: myapp,sharedapp,public 48 | 49 | # Minimum log levels, in increasing order: 50 | # debug5, debug4, debug3, debug2, debug1, 51 | # log, notice, warning, error, fatal, and panic 52 | # Defaults to warning. 53 | #min_messages: notice 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: team_18_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: team_18_production 85 | username: team_18 86 | password: <%= ENV["TEAM_18_DATABASE_PASSWORD"] %> 87 | -------------------------------------------------------------------------------- /config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require_relative "application" 3 | 4 | # Initialize the Rails application. 5 | Rails.application.initialize! 6 | -------------------------------------------------------------------------------- /config/environments/development.rb: -------------------------------------------------------------------------------- 1 | require "active_support/core_ext/integer/time" 2 | 3 | Rails.application.configure do 4 | # Settings specified here will take precedence over those in config/application.rb. 5 | 6 | # In the development environment your application's code is reloaded any time 7 | # it changes. This slows down response time but is perfect for development 8 | # since you don't have to restart the web server when you make code changes. 9 | config.cache_classes = false 10 | 11 | # Do not eager load code on boot. 12 | config.eager_load = false 13 | 14 | # Show full error reports. 15 | config.consider_all_requests_local = true 16 | 17 | # Enable server timing 18 | config.server_timing = true 19 | 20 | # Enable/disable caching. By default caching is disabled. 21 | # Run rails dev:cache to toggle caching. 22 | if Rails.root.join("tmp/caching-dev.txt").exist? 23 | config.action_controller.perform_caching = true 24 | config.action_controller.enable_fragment_cache_logging = true 25 | 26 | config.cache_store = :memory_store 27 | config.public_file_server.headers = { 28 | "Cache-Control" => "public, max-age=#{2.days.to_i}" 29 | } 30 | else 31 | config.action_controller.perform_caching = false 32 | 33 | config.cache_store = :null_store 34 | end 35 | 36 | # Store uploaded files on the local file system (see config/storage.yml for options). 37 | config.active_storage.service = :local 38 | 39 | # Print deprecation notices to the Rails logger. 40 | config.active_support.deprecation = :log 41 | 42 | # Raise exceptions for disallowed deprecations. 43 | config.active_support.disallowed_deprecation = :raise 44 | 45 | # Tell Active Support which deprecation messages to disallow. 46 | config.active_support.disallowed_deprecation_warnings = [] 47 | 48 | # Raise an error on page load if there are pending migrations. 49 | config.active_record.migration_error = :page_load 50 | 51 | # Highlight code that triggered database queries in logs. 52 | config.active_record.verbose_query_logs = true 53 | 54 | 55 | # Raises error for missing translations. 56 | # config.i18n.raise_on_missing_translations = true 57 | 58 | # Annotate rendered view with file names. 59 | # config.action_view.annotate_rendered_view_with_filenames = true 60 | 61 | # Uncomment if you wish to allow Action Cable access from any origin. 62 | # config.action_cable.disable_request_forgery_protection = true 63 | end 64 | -------------------------------------------------------------------------------- /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.cache_classes = true 8 | 9 | # Eager load code on boot. This eager loads most of Rails and 10 | # your application in memory, allowing both threaded web servers 11 | # and those relying on copy on write to perform better. 12 | # Rake tasks automatically ignore this option for performance. 13 | config.eager_load = true 14 | 15 | # Full error reports are disabled and caching is turned on. 16 | config.consider_all_requests_local = false 17 | config.action_controller.perform_caching = true 18 | 19 | # Ensures that a master key has been made available in either ENV["RAILS_MASTER_KEY"] 20 | # or in config/master.key. This key is used to decrypt credentials (and other encrypted files). 21 | # config.require_master_key = true 22 | 23 | # Disable serving static files from the `/public` folder by default since 24 | # Apache or NGINX already handles this. 25 | config.public_file_server.enabled = ENV["RAILS_SERVE_STATIC_FILES"].present? 26 | 27 | # Enable serving of images, stylesheets, and JavaScripts from an asset server. 28 | # config.asset_host = "http://assets.example.com" 29 | 30 | # Specifies the header that your server uses for sending files. 31 | # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for Apache 32 | # config.action_dispatch.x_sendfile_header = "X-Accel-Redirect" # for NGINX 33 | 34 | # Store uploaded files on the local file system (see config/storage.yml for options). 35 | config.active_storage.service = :local 36 | 37 | # Mount Action Cable outside main process or domain. 38 | # config.action_cable.mount_path = nil 39 | # config.action_cable.url = "wss://example.com/cable" 40 | # config.action_cable.allowed_request_origins = [ "http://example.com", /http:\/\/example.*/ ] 41 | 42 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 43 | # config.force_ssl = true 44 | 45 | # Include generic and useful information about system operation, but avoid logging too much 46 | # information to avoid inadvertent exposure of personally identifiable information (PII). 47 | config.log_level = :info 48 | 49 | # Prepend all log lines with the following tags. 50 | config.log_tags = [ :request_id ] 51 | 52 | # Use a different cache store in production. 53 | # config.cache_store = :mem_cache_store 54 | 55 | # Use a real queuing backend for Active Job (and separate queues per environment). 56 | # config.active_job.queue_adapter = :resque 57 | # config.active_job.queue_name_prefix = "team_18_production" 58 | 59 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 60 | # the I18n.default_locale when a translation cannot be found). 61 | config.i18n.fallbacks = true 62 | 63 | # Don't log any deprecations. 64 | config.active_support.report_deprecations = false 65 | 66 | # Use default logging formatter so that PID and timestamp are not suppressed. 67 | config.log_formatter = ::Logger::Formatter.new 68 | 69 | # Use a different logger for distributed setups. 70 | # require "syslog/logger" 71 | # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new "app-name") 72 | 73 | if ENV["RAILS_LOG_TO_STDOUT"].present? 74 | logger = ActiveSupport::Logger.new(STDOUT) 75 | logger.formatter = config.log_formatter 76 | config.logger = ActiveSupport::TaggedLogging.new(logger) 77 | end 78 | 79 | # Do not dump schema after migrations. 80 | config.active_record.dump_schema_after_migration = false 81 | end 82 | -------------------------------------------------------------------------------- /config/environments/test.rb: -------------------------------------------------------------------------------- 1 | require "active_support/core_ext/integer/time" 2 | 3 | # The test environment is used exclusively to run your application's 4 | # test suite. You never need to work with it otherwise. Remember that 5 | # your test database is "scratch space" for the test suite and is wiped 6 | # and recreated between test runs. Don't rely on the data there! 7 | 8 | Rails.application.configure do 9 | # Settings specified here will take precedence over those in config/application.rb. 10 | 11 | # Turn false under Spring and add config.action_view.cache_template_loading = true. 12 | config.cache_classes = true 13 | 14 | # Eager loading loads your whole application. When running a single test locally, 15 | # this probably isn't necessary. It's a good idea to do in a continuous integration 16 | # system, or in some way before deploying your code. 17 | config.eager_load = ENV["CI"].present? 18 | 19 | # Configure public file server for tests with Cache-Control for performance. 20 | config.public_file_server.enabled = true 21 | config.public_file_server.headers = { 22 | "Cache-Control" => "public, max-age=#{1.hour.to_i}" 23 | } 24 | 25 | # Show full error reports and disable caching. 26 | config.consider_all_requests_local = true 27 | config.action_controller.perform_caching = false 28 | config.cache_store = :null_store 29 | 30 | # Raise exceptions instead of rendering exception templates. 31 | config.action_dispatch.show_exceptions = false 32 | 33 | # Disable request forgery protection in test environment. 34 | config.action_controller.allow_forgery_protection = false 35 | 36 | # Store uploaded files on the local file system in a temporary directory. 37 | config.active_storage.service = :test 38 | 39 | # Print deprecation notices to the stderr. 40 | config.active_support.deprecation = :stderr 41 | 42 | # Raise exceptions for disallowed deprecations. 43 | config.active_support.disallowed_deprecation = :raise 44 | 45 | # Tell Active Support which deprecation messages to disallow. 46 | config.active_support.disallowed_deprecation_warnings = [] 47 | 48 | # Raises error for missing translations. 49 | # config.i18n.raise_on_missing_translations = true 50 | 51 | # Annotate rendered view with file names. 52 | # config.action_view.annotate_rendered_view_with_filenames = true 53 | end 54 | -------------------------------------------------------------------------------- /config/initializers/assets.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Version of your assets, change this if you want to expire all your assets. 4 | Rails.application.config.assets.version = "1.0" 5 | 6 | # Add additional assets to the asset load path. 7 | # Rails.application.config.assets.paths << Emoji.images_path 8 | 9 | # Precompile additional assets. 10 | # application.js, application.css, and all non-JS/CSS in the app/assets 11 | # folder are already added. 12 | # Rails.application.config.assets.precompile += %w( admin.js admin.css ) 13 | -------------------------------------------------------------------------------- /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 | # policy.default_src :self, :https 10 | # policy.font_src :self, :https, :data 11 | # policy.img_src :self, :https, :data 12 | # policy.object_src :none 13 | # policy.script_src :self, :https 14 | # policy.style_src :self, :https 15 | # # Specify URI for violation reports 16 | # # policy.report_uri "/csp-violation-report-endpoint" 17 | # end 18 | # 19 | # # Generate session nonces for permitted importmap and inline scripts 20 | # config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s } 21 | # config.content_security_policy_nonce_directives = %w(script-src) 22 | # 23 | # # Report violations without enforcing the policy. 24 | # # config.content_security_policy_report_only = true 25 | # end 26 | -------------------------------------------------------------------------------- /config/initializers/filter_parameter_logging.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Configure parameters to be filtered from the log file. Use this to limit dissemination of 4 | # sensitive information. See the ActiveSupport::ParameterFilter documentation for supported 5 | # notations and behaviors. 6 | Rails.application.config.filter_parameters += [ 7 | :passw, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn 8 | ] 9 | -------------------------------------------------------------------------------- /config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format. Inflections 4 | # are locale specific, and you may define rules for as many different 5 | # locales as you wish. All of these examples are active by default: 6 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 7 | # inflect.plural /^(ox)$/i, "\\1en" 8 | # inflect.singular /^(ox)en/i, "\\1" 9 | # inflect.irregular "person", "people" 10 | # inflect.uncountable %w( fish sheep ) 11 | # end 12 | 13 | # These inflection rules are supported but not enabled by default: 14 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 15 | # inflect.acronym "RESTful" 16 | # end 17 | -------------------------------------------------------------------------------- /config/initializers/pagy.rb: -------------------------------------------------------------------------------- 1 | Pagy::I18n.load(locale: 'en', filepath: 'config/locales/en.yml') -------------------------------------------------------------------------------- /config/initializers/permissions_policy.rb: -------------------------------------------------------------------------------- 1 | # Define an application-wide HTTP permissions policy. For further 2 | # information see https://developers.google.com/web/updates/2018/06/feature-policy 3 | # 4 | # Rails.application.config.permissions_policy do |f| 5 | # f.camera :none 6 | # f.gyroscope :none 7 | # f.microphone :none 8 | # f.usb :none 9 | # f.fullscreen :self 10 | # f.payment :self, "https://secure.example.com" 11 | # end 12 | -------------------------------------------------------------------------------- /config/initializers/ransack.rb: -------------------------------------------------------------------------------- 1 | module Ransack 2 | module Nodes 3 | class Batch < Sort 4 | attr_reader :expanded 5 | 6 | def build(params) 7 | params.with_indifferent_access.each do |key, value| 8 | send("#{key}=", value) if key.match(/^(name|dir|ransacker_args|expanded)$/) 9 | end 10 | 11 | self 12 | end 13 | 14 | def expanded=(value) 15 | @expanded = value == 'true' ? true : false 16 | end 17 | end 18 | end 19 | 20 | class Search 21 | def build(params) 22 | collapse_multiparameter_attributes!(recursive_compact(params)).each do |key, value| 23 | if key == 's' || key == 'sorts' 24 | send(:sorts=, value) 25 | elsif key == 'f' || key == 'fields' 26 | send(:fields=, value) 27 | elsif key == 'b' || key == 'batch' 28 | next if value["name"].blank? && value["dir"].blank? 29 | 30 | send(:batch=, value) 31 | elsif @context.ransackable_scope?(key, @context.object) 32 | add_scope(key, value) 33 | elsif base.attribute_method?(key) 34 | base.send("#{key}=", value) 35 | elsif !Ransack.options[:ignore_unknown_conditions] || !@ignore_unknown_conditions 36 | raise ArgumentError, "Invalid search term #{key}" 37 | end 38 | end 39 | self 40 | end 41 | 42 | def batch=(args) 43 | @batch ||= nil 44 | 45 | case args 46 | when Hash 47 | @batch = Nodes::Batch.new(@context).build(args) 48 | when Nodes::Batch 49 | @batch = args 50 | end 51 | end 52 | alias_method :b=, :batch= 53 | 54 | def batch 55 | @batch ||= nil 56 | 57 | @batch 58 | end 59 | alias_method :b, :batch 60 | 61 | def build_batch(opts = {}) 62 | new_batch(opts).tap do |batch| 63 | self.batch = batch 64 | end 65 | end 66 | 67 | def new_batch(opts = {}) 68 | Nodes::Batch.new(@context).build(opts) 69 | end 70 | 71 | def default_fields=(args) 72 | @default_fields = args.map { |field| Nodes::Attribute.new(@context, field) } 73 | end 74 | 75 | def default_fields 76 | @default_fields ||= [] 77 | end 78 | 79 | def fields=(args) 80 | @fields ||= default_fields 81 | @fields = [] if args == [] 82 | 83 | args.each do |field| 84 | case field 85 | when Hash 86 | field = Nodes::Attribute.new(@context, field['name']) 87 | when String 88 | next if field == '' 89 | field = Nodes::Attribute.new(@context, field) 90 | end 91 | 92 | fields << field 93 | end 94 | end 95 | alias_method :f=, :fields= 96 | 97 | def fields 98 | @fields ||= default_fields 99 | 100 | @fields 101 | end 102 | alias_method :f, :fields 103 | 104 | def build_field(opts = {}) 105 | new_field(opts).tap do |field| 106 | fields << field 107 | end 108 | end 109 | 110 | def new_field(opts = {}) 111 | Nodes::Attribute.new(@context, opts) 112 | end 113 | 114 | def condition_attributes 115 | groupings_attr_names = groupings.flat_map { |g| g.conditions.flat_map { |c| c.attributes.map(&:attr_name) } } 116 | conditions_attr_names = conditions.flat_map { |c| c.attributes.map(&:attr_name) } 117 | (groupings_attr_names + conditions_attr_names).uniq 118 | end 119 | 120 | def sort_attributes 121 | sorts.map(&:attr_name).uniq 122 | end 123 | 124 | def field_attributes 125 | fields.map(&:name).uniq 126 | end 127 | 128 | def hidden_fields 129 | default_fields - fields 130 | end 131 | 132 | def batch_attribute 133 | batch&.attr_name 134 | end 135 | 136 | def recursive_compact(hash_or_array) 137 | p = proc do |*args| 138 | v = args.last 139 | v.delete_if(&p) if v.respond_to? :delete_if 140 | v.nil? || v.respond_to?(:"empty?") && v.empty? 141 | end 142 | 143 | hash_or_array.delete_if(&p) 144 | end 145 | end 146 | end 147 | -------------------------------------------------------------------------------- /config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Files in the config/locales directory are used for internationalization 2 | # and are automatically loaded by Rails. If you want to use locales other 3 | # than English, add the necessary files in this directory. 4 | # 5 | # To use the locales, use `I18n.t`: 6 | # 7 | # I18n.t "hello" 8 | # 9 | # In views, this is aliased to just `t`: 10 | # 11 | # <%= t("hello") %> 12 | # 13 | # To use a different locale, set it with `I18n.locale`: 14 | # 15 | # I18n.locale = :es 16 | # 17 | # This would use the information in config/locales/es.yml. 18 | # 19 | # The following keys must be escaped otherwise they will not be retrieved by 20 | # the default I18n backend: 21 | # 22 | # true, false, on, off, yes, no 23 | # 24 | # Instead, surround them with single quotes. 25 | # 26 | # en: 27 | # "true": "foo" 28 | # 29 | # To learn more, please read the Rails Internationalization guide 30 | # available at https://guides.rubyonrails.org/i18n.html. 31 | 32 | en: 33 | pagy: 34 | item_name: 35 | one: "item" 36 | other: "items" 37 | nav: 38 | prev: "‹ Prev" 39 | next: "Next ›" 40 | gap: "…" 41 | info: 42 | no_items: "No %{item_name} found" 43 | single_page: "%{count} %{item_name}" 44 | multiple_pages: "%{from}-%{to} of %{count}" 45 | 46 | activerecord: 47 | attributes: 48 | book: 49 | title: "Title" 50 | average_rating: "Avg. Rating" 51 | isbn: "ISBN" 52 | isbn13: "ISBN13" 53 | language_code: "Language" 54 | num_pages: "Pages" 55 | ratings_count: "Ratings" 56 | text_reviews_count: "Reviews" 57 | published_on: "Published" 58 | publisher: "Publisher" 59 | -------------------------------------------------------------------------------- /config/puma.rb: -------------------------------------------------------------------------------- 1 | # Puma can serve each request in a thread from an internal thread pool. 2 | # The `threads` method setting takes two numbers: a minimum and maximum. 3 | # Any libraries that use thread pools should be configured to match 4 | # the maximum value specified for Puma. Default is set to 5 threads for minimum 5 | # and maximum; this matches the default thread size of Active Record. 6 | # 7 | max_threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 } 8 | min_threads_count = ENV.fetch("RAILS_MIN_THREADS") { max_threads_count } 9 | threads min_threads_count, max_threads_count 10 | 11 | # Specifies the `worker_timeout` threshold that Puma will use to wait before 12 | # terminating a worker in development environments. 13 | # 14 | worker_timeout 3600 if ENV.fetch("RAILS_ENV", "development") == "development" 15 | 16 | # Specifies the `port` that Puma will listen on to receive requests; default is 3000. 17 | # 18 | port ENV.fetch("PORT") { 3000 } 19 | 20 | # Specifies the `environment` that Puma will run in. 21 | # 22 | environment ENV.fetch("RAILS_ENV") { "development" } 23 | 24 | # Specifies the `pidfile` that Puma will use. 25 | pidfile ENV.fetch("PIDFILE") { "tmp/pids/server.pid" } 26 | 27 | # Specifies the number of `workers` to boot in clustered mode. 28 | # Workers are forked web server processes. If using threads and workers together 29 | # the concurrency of the application would be max `threads` * `workers`. 30 | # Workers do not work on JRuby or Windows (both of which do not support 31 | # processes). 32 | # 33 | # workers ENV.fetch("WEB_CONCURRENCY") { 2 } 34 | 35 | # Use the `preload_app!` method when specifying a `workers` number. 36 | # This directive tells Puma to first boot the application and load code 37 | # before forking the application. This takes advantage of Copy On Write 38 | # process behavior so workers use less memory. 39 | # 40 | # preload_app! 41 | 42 | # Allow puma to be restarted by `bin/rails restart` command. 43 | plugin :tmp_restart 44 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | Rails.application.routes.draw do 2 | resources :books, only: [:index, :update, :edit] do 3 | collection do 4 | match 'search' => 'books#search', via: [:get, :post], as: :search 5 | get :summarize 6 | get :export 7 | end 8 | end 9 | 10 | resources :views, only: [:create, :destroy] do 11 | member do 12 | post :update 13 | end 14 | end 15 | # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html 16 | 17 | # Defines the root path route ("/") 18 | root "books#index" 19 | end 20 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /db/migrate/20220917065513_create_authors.rb: -------------------------------------------------------------------------------- 1 | class CreateAuthors < ActiveRecord::Migration[7.0] 2 | def change 3 | create_table :authors do |t| 4 | t.string :name, index: { unique: true } 5 | 6 | t.timestamps 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/20220917071354_create_books.rb: -------------------------------------------------------------------------------- 1 | class CreateBooks < ActiveRecord::Migration[7.0] 2 | def change 3 | create_table :books do |t| 4 | t.string :title 5 | t.decimal :average_rating, precision: 3, scale: 2 6 | t.string :isbn, index: { unique: true } 7 | t.string :isbn13 8 | t.string :language_code 9 | t.integer :num_pages 10 | t.integer :ratings_count 11 | t.integer :text_reviews_count 12 | t.date :published_on 13 | t.string :publisher 14 | 15 | t.timestamps 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /db/migrate/20220917071558_create_book_authors.rb: -------------------------------------------------------------------------------- 1 | class CreateBookAuthors < ActiveRecord::Migration[7.0] 2 | def change 3 | create_table :book_authors do |t| 4 | t.belongs_to :book, null: false, foreign_key: true 5 | t.belongs_to :author, null: false, foreign_key: true 6 | 7 | t.timestamps 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /db/migrate/20220918220202_create_views.rb: -------------------------------------------------------------------------------- 1 | class CreateViews < ActiveRecord::Migration[7.0] 2 | def change 3 | create_table :views do |t| 4 | t.string :name 5 | t.string :parameters 6 | t.timestamps 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /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[7.0].define(version: 2022_09_18_220202) do 14 | # These are extensions that must be enabled in order to support this database 15 | enable_extension "plpgsql" 16 | 17 | create_table "authors", force: :cascade do |t| 18 | t.string "name" 19 | t.datetime "created_at", null: false 20 | t.datetime "updated_at", null: false 21 | t.index ["name"], name: "index_authors_on_name", unique: true 22 | end 23 | 24 | create_table "book_authors", force: :cascade do |t| 25 | t.bigint "book_id", null: false 26 | t.bigint "author_id", null: false 27 | t.datetime "created_at", null: false 28 | t.datetime "updated_at", null: false 29 | t.index ["author_id"], name: "index_book_authors_on_author_id" 30 | t.index ["book_id"], name: "index_book_authors_on_book_id" 31 | end 32 | 33 | create_table "books", force: :cascade do |t| 34 | t.string "title" 35 | t.decimal "average_rating", precision: 3, scale: 2 36 | t.string "isbn" 37 | t.string "isbn13" 38 | t.string "language_code" 39 | t.integer "num_pages" 40 | t.integer "ratings_count" 41 | t.integer "text_reviews_count" 42 | t.date "published_on" 43 | t.string "publisher" 44 | t.datetime "created_at", null: false 45 | t.datetime "updated_at", null: false 46 | t.index ["isbn"], name: "index_books_on_isbn", unique: true 47 | end 48 | 49 | create_table "views", force: :cascade do |t| 50 | t.string "name" 51 | t.string "parameters" 52 | t.datetime "created_at", null: false 53 | t.datetime "updated_at", null: false 54 | end 55 | 56 | add_foreign_key "book_authors", "authors" 57 | add_foreign_key "book_authors", "books" 58 | end 59 | -------------------------------------------------------------------------------- /db/seeds.rb: -------------------------------------------------------------------------------- 1 | # This file should contain all the record creation needed to seed the database with its default values. 2 | # The data can then be loaded with the bin/rails db:seed command (or created alongside the database with db:setup). 3 | # 4 | # Examples: 5 | # 6 | # movies = Movie.create([{ name: "Star Wars" }, { name: "Lord of the Rings" }]) 7 | # Character.create(name: "Luke", movie: movies.first) 8 | require 'csv' 9 | 10 | book_data = CSV.read(Rails.root.join('db', 'books.csv'), headers: true, liberal_parsing: true) 11 | authors = book_data.by_col['authors'].flat_map { _1.split('/') }.uniq.map { { name: _1 } } 12 | 13 | Author.upsert_all( 14 | authors, 15 | unique_by: :name 16 | ) 17 | 18 | Book.upsert_all( 19 | book_data.map do |book| 20 | { 21 | title: book['title'], 22 | average_rating: book['average_rating'], 23 | isbn: book['isbn'], 24 | isbn13: book['isbn13'], 25 | language_code: book['language_code'], 26 | num_pages: book['num_pages'], 27 | ratings_count: book['num_pages'], 28 | text_reviews_count: book['text_reviews_count'], 29 | published_on: (book['publication_date'].blank? ? nil : Date.strptime(book['publication_date'], '%m/%d/%Y')), 30 | publisher: book['publisher'] 31 | } 32 | end, 33 | unique_by: :isbn 34 | ) 35 | 36 | author_name_to_id = Author.pluck(:name, :id).to_h 37 | book_isbn_to_id = Book.pluck(:isbn, :id).to_h 38 | 39 | BookAuthor.upsert_all( 40 | book_data.flat_map do |book| 41 | book['authors'].split('/').map do |author_name| 42 | { 43 | book_id: book_isbn_to_id.fetch(book['isbn'], nil), 44 | author_id: author_name_to_id.fetch(author_name, nil) 45 | } 46 | end 47 | end 48 | ) 49 | 50 | View.create!(name: "Books", parameters: {}) 51 | -------------------------------------------------------------------------------- /esbuild.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const rails = require('esbuild-rails') 3 | 4 | require("esbuild").build({ 5 | entryPoints: ["application.js"], 6 | bundle: true, 7 | outdir: path.join(process.cwd(), "app/assets/builds"), 8 | absWorkingDir: path.join(process.cwd(), "app/javascript"), 9 | watch: process.argv.includes("--watch"), 10 | plugins: [rails()], 11 | }).catch(() => process.exit(1)) 12 | -------------------------------------------------------------------------------- /lib/assets/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fractaledmind/hottable/4905781fbd1e214a4da6a3e9b539c52d8d282c55/lib/assets/.keep -------------------------------------------------------------------------------- /lib/tasks/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fractaledmind/hottable/4905781fbd1e214a4da6a3e9b539c52d8d282c55/lib/tasks/.keep -------------------------------------------------------------------------------- /log/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fractaledmind/hottable/4905781fbd1e214a4da6a3e9b539c52d8d282c55/log/.keep -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app", 3 | "private": "true", 4 | "dependencies": { 5 | "@hotwired/stimulus": "^3.1.0", 6 | "@hotwired/turbo-rails": "^7.2.0", 7 | "@popperjs/core": "^2.11.6", 8 | "@rails/actioncable": "^7.0.4", 9 | "@rails/request.js": "^0.0.6", 10 | "@tailwindcss/forms": "^0.5.3", 11 | "autoprefixer": "^10.4.11", 12 | "debounced": "^0.0.5", 13 | "esbuild": "^0.15.7", 14 | "esbuild-rails": "^1.0.3", 15 | "postcss": "^8.4.16", 16 | "tailwindcss": "^3.1.8", 17 | "turbo_power": "^0.1.2" 18 | }, 19 | "scripts": { 20 | "build": "node esbuild.config.js", 21 | "build:css": "tailwindcss -i ./app/assets/stylesheets/application.tailwind.css -o ./app/assets/builds/application.css --minify" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The page you were looking for doesn't exist (404) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

The page you were looking for doesn't exist.

62 |

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

63 |
64 |

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

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

The change you wanted was rejected.

62 |

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

63 |
64 |

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

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

We're sorry, but something went wrong.

62 |
63 |

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

64 |
65 | 66 | 67 | -------------------------------------------------------------------------------- /public/apple-touch-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fractaledmind/hottable/4905781fbd1e214a4da6a3e9b539c52d8d282c55/public/apple-touch-icon-precomposed.png -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fractaledmind/hottable/4905781fbd1e214a4da6a3e9b539c52d8d282c55/public/apple-touch-icon.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fractaledmind/hottable/4905781fbd1e214a4da6a3e9b539c52d8d282c55/public/favicon.ico -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 2 | -------------------------------------------------------------------------------- /screenshots/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fractaledmind/hottable/4905781fbd1e214a4da6a3e9b539c52d8d282c55/screenshots/1.png -------------------------------------------------------------------------------- /screenshots/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fractaledmind/hottable/4905781fbd1e214a4da6a3e9b539c52d8d282c55/screenshots/2.png -------------------------------------------------------------------------------- /screenshots/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fractaledmind/hottable/4905781fbd1e214a4da6a3e9b539c52d8d282c55/screenshots/3.png -------------------------------------------------------------------------------- /screenshots/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fractaledmind/hottable/4905781fbd1e214a4da6a3e9b539c52d8d282c55/screenshots/4.png -------------------------------------------------------------------------------- /screenshots/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fractaledmind/hottable/4905781fbd1e214a4da6a3e9b539c52d8d282c55/screenshots/5.png -------------------------------------------------------------------------------- /screenshots/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fractaledmind/hottable/4905781fbd1e214a4da6a3e9b539c52d8d282c55/screenshots/6.png -------------------------------------------------------------------------------- /screenshots/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fractaledmind/hottable/4905781fbd1e214a4da6a3e9b539c52d8d282c55/screenshots/7.png -------------------------------------------------------------------------------- /screenshots/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fractaledmind/hottable/4905781fbd1e214a4da6a3e9b539c52d8d282c55/screenshots/8.png -------------------------------------------------------------------------------- /screenshots/9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fractaledmind/hottable/4905781fbd1e214a4da6a3e9b539c52d8d282c55/screenshots/9.png -------------------------------------------------------------------------------- /storage/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fractaledmind/hottable/4905781fbd1e214a4da6a3e9b539c52d8d282c55/storage/.keep -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: [ 3 | './app/views/**/*.html.erb', 4 | './app/helpers/**/*.rb', 5 | './app/assets/stylesheets/**/*.css', 6 | './app/javascript/**/*.js', 7 | './app/views/**/*.rb', 8 | './app/components/**/*.rb' 9 | ], 10 | plugins: [ 11 | require('@tailwindcss/forms'), 12 | ], 13 | theme: { 14 | extend: { 15 | minWidth: { 16 | '12': '3rem', /* 48px */ 17 | '72': '18rem', /* 288px */ 18 | '80': '20rem', /* 320px */ 19 | '96': '24rem', /* 384px */ 20 | }, 21 | minHeight: { 22 | '16': '4rem', /* 64px */ 23 | }, 24 | maxWidth: { 25 | '12': '3rem', /* 48px */ 26 | '72': '18rem', /* 288px */ 27 | '80': '20rem', /* 320px */ 28 | '96': '24rem', /* 384px */ 29 | screen: '100vw', 30 | }, 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tmp/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fractaledmind/hottable/4905781fbd1e214a4da6a3e9b539c52d8d282c55/tmp/.keep -------------------------------------------------------------------------------- /tmp/pids/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fractaledmind/hottable/4905781fbd1e214a4da6a3e9b539c52d8d282c55/tmp/pids/.keep -------------------------------------------------------------------------------- /tmp/storage/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fractaledmind/hottable/4905781fbd1e214a4da6a3e9b539c52d8d282c55/tmp/storage/.keep -------------------------------------------------------------------------------- /vendor/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fractaledmind/hottable/4905781fbd1e214a4da6a3e9b539c52d8d282c55/vendor/.keep -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@esbuild/linux-loong64@0.15.7": 6 | version "0.15.7" 7 | resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.15.7.tgz#1ec4af4a16c554cbd402cc557ccdd874e3f7be53" 8 | integrity sha512-IKznSJOsVUuyt7cDzzSZyqBEcZe+7WlBqTVXiF1OXP/4Nm387ToaXZ0fyLwI1iBlI/bzpxVq411QE2/Bt2XWWw== 9 | 10 | "@hotwired/stimulus@^3.1.0": 11 | version "3.1.0" 12 | resolved "https://registry.yarnpkg.com/@hotwired/stimulus/-/stimulus-3.1.0.tgz#20215251e5afe6e0a3787285181ba1bfc9097df0" 13 | integrity sha512-iDMHUhiEJ1xFeicyHcZQQgBzhtk5mPR0QZO3L6wtqzMsJEk2TKECuCQTGKjm+KJTHVY0dKq1dOOAWvODjpd2Mg== 14 | 15 | "@hotwired/turbo-rails@^7.2.0": 16 | version "7.2.0" 17 | resolved "https://registry.yarnpkg.com/@hotwired/turbo-rails/-/turbo-rails-7.2.0.tgz#2081ed4e626fac9fd61ba5a4d1eeb53e6ea93b03" 18 | integrity sha512-RxJJGINeLa2lyI078LLSbqZDI7RarxTDMVlgKnsLvtFEn8Pa7ySEcDUxHp5YiLXGbLacAIH/dcfD0JfCWe5Dqw== 19 | dependencies: 20 | "@hotwired/turbo" "^7.2.0" 21 | "@rails/actioncable" "^7.0" 22 | 23 | "@hotwired/turbo@^7.2.0": 24 | version "7.2.0" 25 | resolved "https://registry.yarnpkg.com/@hotwired/turbo/-/turbo-7.2.0.tgz#4ff90d80fda17e69b04a12bbf0a42f09953504f6" 26 | integrity sha512-CYr6N9NfqsjhmZx1xVQ8zYcDo4hTm7sTUpJydbNgMRyG+YfF/9ADIQQ2TtcBdkl2zi/12a3OTWX0UMzNZnAK9w== 27 | 28 | "@nodelib/fs.scandir@2.1.5": 29 | version "2.1.5" 30 | resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" 31 | integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== 32 | dependencies: 33 | "@nodelib/fs.stat" "2.0.5" 34 | run-parallel "^1.1.9" 35 | 36 | "@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": 37 | version "2.0.5" 38 | resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" 39 | integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== 40 | 41 | "@nodelib/fs.walk@^1.2.3": 42 | version "1.2.8" 43 | resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" 44 | integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== 45 | dependencies: 46 | "@nodelib/fs.scandir" "2.1.5" 47 | fastq "^1.6.0" 48 | 49 | "@popperjs/core@^2.11.6": 50 | version "2.11.6" 51 | resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.6.tgz#cee20bd55e68a1720bdab363ecf0c821ded4cd45" 52 | integrity sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw== 53 | 54 | "@rails/actioncable@^7.0", "@rails/actioncable@^7.0.4": 55 | version "7.0.4" 56 | resolved "https://registry.yarnpkg.com/@rails/actioncable/-/actioncable-7.0.4.tgz#70a3ca56809f7aaabb80af2f9c01ae51e1a8ed41" 57 | integrity sha512-tz4oM+Zn9CYsvtyicsa/AwzKZKL+ITHWkhiu7x+xF77clh2b4Rm+s6xnOgY/sGDWoFWZmtKsE95hxBPkgQQNnQ== 58 | 59 | "@rails/request.js@^0.0.6": 60 | version "0.0.6" 61 | resolved "https://registry.yarnpkg.com/@rails/request.js/-/request.js-0.0.6.tgz#5f0347a9f363e50ec45118c7134080490cda81d8" 62 | integrity sha512-dfFWaQXitYJ4kxrgGJNhDNXX54/v10YgoJqBMVe6lhqs6a4N9WD7goZJEvwin82TtK8MqUNhwfyisgKwM6dMdg== 63 | 64 | "@tailwindcss/forms@^0.5.3": 65 | version "0.5.3" 66 | resolved "https://registry.yarnpkg.com/@tailwindcss/forms/-/forms-0.5.3.tgz#e4d7989686cbcaf416c53f1523df5225332a86e7" 67 | integrity sha512-y5mb86JUoiUgBjY/o6FJSFZSEttfb3Q5gllE4xoKjAAD+vBrnIhE4dViwUuow3va8mpH4s9jyUbUbrRGoRdc2Q== 68 | dependencies: 69 | mini-svg-data-uri "^1.2.3" 70 | 71 | acorn-node@^1.8.2: 72 | version "1.8.2" 73 | resolved "https://registry.yarnpkg.com/acorn-node/-/acorn-node-1.8.2.tgz#114c95d64539e53dede23de8b9d96df7c7ae2af8" 74 | integrity sha512-8mt+fslDufLYntIoPAaIMUe/lrbrehIiwmR3t2k9LljIzoigEPF27eLk2hy8zSGzmR/ogr7zbRKINMo1u0yh5A== 75 | dependencies: 76 | acorn "^7.0.0" 77 | acorn-walk "^7.0.0" 78 | xtend "^4.0.2" 79 | 80 | acorn-walk@^7.0.0: 81 | version "7.2.0" 82 | resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-7.2.0.tgz#0de889a601203909b0fbe07b8938dc21d2e967bc" 83 | integrity sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA== 84 | 85 | acorn@^7.0.0: 86 | version "7.4.1" 87 | resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" 88 | integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== 89 | 90 | anymatch@~3.1.2: 91 | version "3.1.2" 92 | resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716" 93 | integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg== 94 | dependencies: 95 | normalize-path "^3.0.0" 96 | picomatch "^2.0.4" 97 | 98 | arg@^5.0.2: 99 | version "5.0.2" 100 | resolved "https://registry.yarnpkg.com/arg/-/arg-5.0.2.tgz#c81433cc427c92c4dcf4865142dbca6f15acd59c" 101 | integrity sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg== 102 | 103 | autoprefixer@^10.4.11: 104 | version "10.4.11" 105 | resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.11.tgz#835136aff1d9cd43640151e0d2dba00f8eac7c1c" 106 | integrity sha512-5lHp6DgRodxlBLSkzHOTcufWFflH1ewfy2hvFQyjrblBFlP/0Yh4O/Wrg4ow8WRlN3AAUFFLAQwX8hTptzqVHg== 107 | dependencies: 108 | browserslist "^4.21.3" 109 | caniuse-lite "^1.0.30001399" 110 | fraction.js "^4.2.0" 111 | normalize-range "^0.1.2" 112 | picocolors "^1.0.0" 113 | postcss-value-parser "^4.2.0" 114 | 115 | balanced-match@^1.0.0: 116 | version "1.0.2" 117 | resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" 118 | integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== 119 | 120 | binary-extensions@^2.0.0: 121 | version "2.2.0" 122 | resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" 123 | integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== 124 | 125 | brace-expansion@^1.1.7: 126 | version "1.1.11" 127 | resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" 128 | integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== 129 | dependencies: 130 | balanced-match "^1.0.0" 131 | concat-map "0.0.1" 132 | 133 | braces@^3.0.2, braces@~3.0.2: 134 | version "3.0.2" 135 | resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" 136 | integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== 137 | dependencies: 138 | fill-range "^7.0.1" 139 | 140 | browserslist@^4.21.3: 141 | version "4.21.4" 142 | resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.4.tgz#e7496bbc67b9e39dd0f98565feccdcb0d4ff6987" 143 | integrity sha512-CBHJJdDmgjl3daYjN5Cp5kbTf1mUhZoS+beLklHIvkOWscs83YAhLlF3Wsh/lciQYAcbBJgTOD44VtG31ZM4Hw== 144 | dependencies: 145 | caniuse-lite "^1.0.30001400" 146 | electron-to-chromium "^1.4.251" 147 | node-releases "^2.0.6" 148 | update-browserslist-db "^1.0.9" 149 | 150 | camelcase-css@^2.0.1: 151 | version "2.0.1" 152 | resolved "https://registry.yarnpkg.com/camelcase-css/-/camelcase-css-2.0.1.tgz#ee978f6947914cc30c6b44741b6ed1df7f043fd5" 153 | integrity sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA== 154 | 155 | caniuse-lite@^1.0.30001399, caniuse-lite@^1.0.30001400: 156 | version "1.0.30001402" 157 | resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001402.tgz#aa29e1f47f5055b0d0c07696a67b8b08023d14c8" 158 | integrity sha512-Mx4MlhXO5NwuvXGgVb+hg65HZ+bhUYsz8QtDGDo2QmaJS2GBX47Xfi2koL86lc8K+l+htXeTEB/Aeqvezoo6Ew== 159 | 160 | chokidar@^3.5.3: 161 | version "3.5.3" 162 | resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" 163 | integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== 164 | dependencies: 165 | anymatch "~3.1.2" 166 | braces "~3.0.2" 167 | glob-parent "~5.1.2" 168 | is-binary-path "~2.1.0" 169 | is-glob "~4.0.1" 170 | normalize-path "~3.0.0" 171 | readdirp "~3.6.0" 172 | optionalDependencies: 173 | fsevents "~2.3.2" 174 | 175 | color-name@^1.1.4: 176 | version "1.1.4" 177 | resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" 178 | integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== 179 | 180 | concat-map@0.0.1: 181 | version "0.0.1" 182 | resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" 183 | integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== 184 | 185 | cssesc@^3.0.0: 186 | version "3.0.0" 187 | resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" 188 | integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== 189 | 190 | debounced@^0.0.5: 191 | version "0.0.5" 192 | resolved "https://registry.yarnpkg.com/debounced/-/debounced-0.0.5.tgz#e540b6eebfe703d93462711b4f3562ffd101b87f" 193 | integrity sha512-8Bgheu1YxQB7ocqYmK2enbLGVoo4nCtu/V6UD/SMDOeV3g2LocG2CrA5oxudlyl79Ja07UiqGdp9pWZoJn52EQ== 194 | 195 | defined@^1.0.0: 196 | version "1.0.0" 197 | resolved "https://registry.yarnpkg.com/defined/-/defined-1.0.0.tgz#c98d9bcef75674188e110969151199e39b1fa693" 198 | integrity sha512-Y2caI5+ZwS5c3RiNDJ6u53VhQHv+hHKwhkI1iHvceKUHw9Df6EK2zRLfjejRgMuCuxK7PfSWIMwWecceVvThjQ== 199 | 200 | detective@^5.2.1: 201 | version "5.2.1" 202 | resolved "https://registry.yarnpkg.com/detective/-/detective-5.2.1.tgz#6af01eeda11015acb0e73f933242b70f24f91034" 203 | integrity sha512-v9XE1zRnz1wRtgurGu0Bs8uHKFSTdteYZNbIPFVhUZ39L/S79ppMpdmVOZAnoz1jfEFodc48n6MX483Xo3t1yw== 204 | dependencies: 205 | acorn-node "^1.8.2" 206 | defined "^1.0.0" 207 | minimist "^1.2.6" 208 | 209 | didyoumean@^1.2.2: 210 | version "1.2.2" 211 | resolved "https://registry.yarnpkg.com/didyoumean/-/didyoumean-1.2.2.tgz#989346ffe9e839b4555ecf5666edea0d3e8ad037" 212 | integrity sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw== 213 | 214 | dlv@^1.1.3: 215 | version "1.1.3" 216 | resolved "https://registry.yarnpkg.com/dlv/-/dlv-1.1.3.tgz#5c198a8a11453596e751494d49874bc7732f2e79" 217 | integrity sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA== 218 | 219 | electron-to-chromium@^1.4.251: 220 | version "1.4.254" 221 | resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.254.tgz#c6203583890abf88dfc0be046cd72d3b48f8beb6" 222 | integrity sha512-Sh/7YsHqQYkA6ZHuHMy24e6TE4eX6KZVsZb9E/DvU1nQRIrH4BflO/4k+83tfdYvDl+MObvlqHPRICzEdC9c6Q== 223 | 224 | esbuild-android-64@0.15.7: 225 | version "0.15.7" 226 | resolved "https://registry.yarnpkg.com/esbuild-android-64/-/esbuild-android-64-0.15.7.tgz#a521604d8c4c6befc7affedc897df8ccde189bea" 227 | integrity sha512-p7rCvdsldhxQr3YHxptf1Jcd86dlhvc3EQmQJaZzzuAxefO9PvcI0GLOa5nCWem1AJ8iMRu9w0r5TG8pHmbi9w== 228 | 229 | esbuild-android-arm64@0.15.7: 230 | version "0.15.7" 231 | resolved "https://registry.yarnpkg.com/esbuild-android-arm64/-/esbuild-android-arm64-0.15.7.tgz#307b81f1088bf1e81dfe5f3d1d63a2d2a2e3e68e" 232 | integrity sha512-L775l9ynJT7rVqRM5vo+9w5g2ysbOCfsdLV4CWanTZ1k/9Jb3IYlQ06VCI1edhcosTYJRECQFJa3eAvkx72eyQ== 233 | 234 | esbuild-darwin-64@0.15.7: 235 | version "0.15.7" 236 | resolved "https://registry.yarnpkg.com/esbuild-darwin-64/-/esbuild-darwin-64-0.15.7.tgz#270117b0c4ec6bcbc5cf3a297a7d11954f007e11" 237 | integrity sha512-KGPt3r1c9ww009t2xLB6Vk0YyNOXh7hbjZ3EecHoVDxgtbUlYstMPDaReimKe6eOEfyY4hBEEeTvKwPsiH5WZg== 238 | 239 | esbuild-darwin-arm64@0.15.7: 240 | version "0.15.7" 241 | resolved "https://registry.yarnpkg.com/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.15.7.tgz#97851eacd11dacb7719713602e3319e16202fc77" 242 | integrity sha512-kBIHvtVqbSGajN88lYMnR3aIleH3ABZLLFLxwL2stiuIGAjGlQW741NxVTpUHQXUmPzxi6POqc9npkXa8AcSZQ== 243 | 244 | esbuild-freebsd-64@0.15.7: 245 | version "0.15.7" 246 | resolved "https://registry.yarnpkg.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.15.7.tgz#1de15ffaf5ae916aa925800aa6d02579960dd8c4" 247 | integrity sha512-hESZB91qDLV5MEwNxzMxPfbjAhOmtfsr9Wnuci7pY6TtEh4UDuevmGmkUIjX/b+e/k4tcNBMf7SRQ2mdNuK/HQ== 248 | 249 | esbuild-freebsd-arm64@0.15.7: 250 | version "0.15.7" 251 | resolved "https://registry.yarnpkg.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.15.7.tgz#0f160dbf5c9a31a1d8dd87acbbcb1a04b7031594" 252 | integrity sha512-dLFR0ChH5t+b3J8w0fVKGvtwSLWCv7GYT2Y2jFGulF1L5HftQLzVGN+6pi1SivuiVSmTh28FwUhi9PwQicXI6Q== 253 | 254 | esbuild-linux-32@0.15.7: 255 | version "0.15.7" 256 | resolved "https://registry.yarnpkg.com/esbuild-linux-32/-/esbuild-linux-32-0.15.7.tgz#422eb853370a5e40bdce8b39525380de11ccadec" 257 | integrity sha512-v3gT/LsONGUZcjbt2swrMjwxo32NJzk+7sAgtxhGx1+ZmOFaTRXBAi1PPfgpeo/J//Un2jIKm/I+qqeo4caJvg== 258 | 259 | esbuild-linux-64@0.15.7: 260 | version "0.15.7" 261 | resolved "https://registry.yarnpkg.com/esbuild-linux-64/-/esbuild-linux-64-0.15.7.tgz#f89c468453bb3194b14f19dc32e0b99612e81d2b" 262 | integrity sha512-LxXEfLAKwOVmm1yecpMmWERBshl+Kv5YJ/1KnyAr6HRHFW8cxOEsEfisD3sVl/RvHyW//lhYUVSuy9jGEfIRAQ== 263 | 264 | esbuild-linux-arm64@0.15.7: 265 | version "0.15.7" 266 | resolved "https://registry.yarnpkg.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.15.7.tgz#68a79d6eb5e032efb9168a0f340ccfd33d6350a1" 267 | integrity sha512-P3cfhudpzWDkglutWgXcT2S7Ft7o2e3YDMrP1n0z2dlbUZghUkKCyaWw0zhp4KxEEzt/E7lmrtRu/pGWnwb9vw== 268 | 269 | esbuild-linux-arm@0.15.7: 270 | version "0.15.7" 271 | resolved "https://registry.yarnpkg.com/esbuild-linux-arm/-/esbuild-linux-arm-0.15.7.tgz#2b7c784d0b3339878013dfa82bf5eaf82c7ce7d3" 272 | integrity sha512-JKgAHtMR5f75wJTeuNQbyznZZa+pjiUHV7sRZp42UNdyXC6TiUYMW/8z8yIBAr2Fpad8hM1royZKQisqPABPvQ== 273 | 274 | esbuild-linux-mips64le@0.15.7: 275 | version "0.15.7" 276 | resolved "https://registry.yarnpkg.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.15.7.tgz#bb8330a50b14aa84673816cb63cc6c8b9beb62cc" 277 | integrity sha512-T7XKuxl0VpeFLCJXub6U+iybiqh0kM/bWOTb4qcPyDDwNVhLUiPcGdG2/0S7F93czUZOKP57YiLV8YQewgLHKw== 278 | 279 | esbuild-linux-ppc64le@0.15.7: 280 | version "0.15.7" 281 | resolved "https://registry.yarnpkg.com/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.15.7.tgz#52544e7fa992811eb996674090d0bc41f067a14b" 282 | integrity sha512-6mGuC19WpFN7NYbecMIJjeQgvDb5aMuvyk0PDYBJrqAEMkTwg3Z98kEKuCm6THHRnrgsdr7bp4SruSAxEM4eJw== 283 | 284 | esbuild-linux-riscv64@0.15.7: 285 | version "0.15.7" 286 | resolved "https://registry.yarnpkg.com/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.15.7.tgz#a43ae60697992b957e454cbb622f7ee5297e8159" 287 | integrity sha512-uUJsezbswAYo/X7OU/P+PuL/EI9WzxsEQXDekfwpQ23uGiooxqoLFAPmXPcRAt941vjlY9jtITEEikWMBr+F/g== 288 | 289 | esbuild-linux-s390x@0.15.7: 290 | version "0.15.7" 291 | resolved "https://registry.yarnpkg.com/esbuild-linux-s390x/-/esbuild-linux-s390x-0.15.7.tgz#8c76a125dd10a84c166294d77416caaf5e1c7b64" 292 | integrity sha512-+tO+xOyTNMc34rXlSxK7aCwJgvQyffqEM5MMdNDEeMU3ss0S6wKvbBOQfgd5jRPblfwJ6b+bKiz0g5nABpY0QQ== 293 | 294 | esbuild-netbsd-64@0.15.7: 295 | version "0.15.7" 296 | resolved "https://registry.yarnpkg.com/esbuild-netbsd-64/-/esbuild-netbsd-64-0.15.7.tgz#19b2e75449d7d9c32b5d8a222bac2f1e0c3b08fd" 297 | integrity sha512-yVc4Wz+Pu3cP5hzm5kIygNPrjar/v5WCSoRmIjCPWfBVJkZNb5brEGKUlf+0Y759D48BCWa0WHrWXaNy0DULTQ== 298 | 299 | esbuild-openbsd-64@0.15.7: 300 | version "0.15.7" 301 | resolved "https://registry.yarnpkg.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.15.7.tgz#1357b2bf72fd037d9150e751420a1fe4c8618ad7" 302 | integrity sha512-GsimbwC4FSR4lN3wf8XmTQ+r8/0YSQo21rWDL0XFFhLHKlzEA4SsT1Tl8bPYu00IU6UWSJ+b3fG/8SB69rcuEQ== 303 | 304 | esbuild-rails@^1.0.3: 305 | version "1.0.3" 306 | resolved "https://registry.yarnpkg.com/esbuild-rails/-/esbuild-rails-1.0.3.tgz#5b963c4affd9cbac3c3d589141062c4deabd5a19" 307 | integrity sha512-a/bD6R3/VBynPD/ftQ6OpYNpjdeG2W30jZ1eEHd2aTsZBdK8ppuox3pZBv1M3c7ZPf+RicYM8cpx9MV/mVs2BA== 308 | dependencies: 309 | glob "^7.2.0" 310 | 311 | esbuild-sunos-64@0.15.7: 312 | version "0.15.7" 313 | resolved "https://registry.yarnpkg.com/esbuild-sunos-64/-/esbuild-sunos-64-0.15.7.tgz#87ab2c604592a9c3c763e72969da0d72bcde91d2" 314 | integrity sha512-8CDI1aL/ts0mDGbWzjEOGKXnU7p3rDzggHSBtVryQzkSOsjCHRVe0iFYUuhczlxU1R3LN/E7HgUO4NXzGGP/Ag== 315 | 316 | esbuild-windows-32@0.15.7: 317 | version "0.15.7" 318 | resolved "https://registry.yarnpkg.com/esbuild-windows-32/-/esbuild-windows-32-0.15.7.tgz#c81e688c0457665a8d463a669e5bf60870323e99" 319 | integrity sha512-cOnKXUEPS8EGCzRSFa1x6NQjGhGsFlVgjhqGEbLTPsA7x4RRYiy2RKoArNUU4iR2vHmzqS5Gr84MEumO/wxYKA== 320 | 321 | esbuild-windows-64@0.15.7: 322 | version "0.15.7" 323 | resolved "https://registry.yarnpkg.com/esbuild-windows-64/-/esbuild-windows-64-0.15.7.tgz#2421d1ae34b0561a9d6767346b381961266c4eff" 324 | integrity sha512-7MI08Ec2sTIDv+zH6StNBKO+2hGUYIT42GmFyW6MBBWWtJhTcQLinKS6ldIN1d52MXIbiJ6nXyCJ+LpL4jBm3Q== 325 | 326 | esbuild-windows-arm64@0.15.7: 327 | version "0.15.7" 328 | resolved "https://registry.yarnpkg.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.15.7.tgz#7d5e9e060a7b454cb2f57f84a3f3c23c8f30b7d2" 329 | integrity sha512-R06nmqBlWjKHddhRJYlqDd3Fabx9LFdKcjoOy08YLimwmsswlFBJV4rXzZCxz/b7ZJXvrZgj8DDv1ewE9+StMw== 330 | 331 | esbuild@^0.15.7: 332 | version "0.15.7" 333 | resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.15.7.tgz#8a1f1aff58671a3199dd24df95314122fc1ddee8" 334 | integrity sha512-7V8tzllIbAQV1M4QoE52ImKu8hT/NLGlGXkiDsbEU5PS6K8Mn09ZnYoS+dcmHxOS9CRsV4IRAMdT3I67IyUNXw== 335 | optionalDependencies: 336 | "@esbuild/linux-loong64" "0.15.7" 337 | esbuild-android-64 "0.15.7" 338 | esbuild-android-arm64 "0.15.7" 339 | esbuild-darwin-64 "0.15.7" 340 | esbuild-darwin-arm64 "0.15.7" 341 | esbuild-freebsd-64 "0.15.7" 342 | esbuild-freebsd-arm64 "0.15.7" 343 | esbuild-linux-32 "0.15.7" 344 | esbuild-linux-64 "0.15.7" 345 | esbuild-linux-arm "0.15.7" 346 | esbuild-linux-arm64 "0.15.7" 347 | esbuild-linux-mips64le "0.15.7" 348 | esbuild-linux-ppc64le "0.15.7" 349 | esbuild-linux-riscv64 "0.15.7" 350 | esbuild-linux-s390x "0.15.7" 351 | esbuild-netbsd-64 "0.15.7" 352 | esbuild-openbsd-64 "0.15.7" 353 | esbuild-sunos-64 "0.15.7" 354 | esbuild-windows-32 "0.15.7" 355 | esbuild-windows-64 "0.15.7" 356 | esbuild-windows-arm64 "0.15.7" 357 | 358 | escalade@^3.1.1: 359 | version "3.1.1" 360 | resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" 361 | integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== 362 | 363 | fast-glob@^3.2.11: 364 | version "3.2.12" 365 | resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.12.tgz#7f39ec99c2e6ab030337142da9e0c18f37afae80" 366 | integrity sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w== 367 | dependencies: 368 | "@nodelib/fs.stat" "^2.0.2" 369 | "@nodelib/fs.walk" "^1.2.3" 370 | glob-parent "^5.1.2" 371 | merge2 "^1.3.0" 372 | micromatch "^4.0.4" 373 | 374 | fastq@^1.6.0: 375 | version "1.13.0" 376 | resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.13.0.tgz#616760f88a7526bdfc596b7cab8c18938c36b98c" 377 | integrity sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw== 378 | dependencies: 379 | reusify "^1.0.4" 380 | 381 | fill-range@^7.0.1: 382 | version "7.0.1" 383 | resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" 384 | integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== 385 | dependencies: 386 | to-regex-range "^5.0.1" 387 | 388 | fraction.js@^4.2.0: 389 | version "4.2.0" 390 | resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.2.0.tgz#448e5109a313a3527f5a3ab2119ec4cf0e0e2950" 391 | integrity sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA== 392 | 393 | fs.realpath@^1.0.0: 394 | version "1.0.0" 395 | resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" 396 | integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== 397 | 398 | fsevents@~2.3.2: 399 | version "2.3.2" 400 | resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" 401 | integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== 402 | 403 | function-bind@^1.1.1: 404 | version "1.1.1" 405 | resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" 406 | integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== 407 | 408 | glob-parent@^5.1.2, glob-parent@~5.1.2: 409 | version "5.1.2" 410 | resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" 411 | integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== 412 | dependencies: 413 | is-glob "^4.0.1" 414 | 415 | glob-parent@^6.0.2: 416 | version "6.0.2" 417 | resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3" 418 | integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== 419 | dependencies: 420 | is-glob "^4.0.3" 421 | 422 | glob@^7.2.0: 423 | version "7.2.3" 424 | resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" 425 | integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== 426 | dependencies: 427 | fs.realpath "^1.0.0" 428 | inflight "^1.0.4" 429 | inherits "2" 430 | minimatch "^3.1.1" 431 | once "^1.3.0" 432 | path-is-absolute "^1.0.0" 433 | 434 | has@^1.0.3: 435 | version "1.0.3" 436 | resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" 437 | integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== 438 | dependencies: 439 | function-bind "^1.1.1" 440 | 441 | inflight@^1.0.4: 442 | version "1.0.6" 443 | resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" 444 | integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== 445 | dependencies: 446 | once "^1.3.0" 447 | wrappy "1" 448 | 449 | inherits@2: 450 | version "2.0.4" 451 | resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" 452 | integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== 453 | 454 | is-binary-path@~2.1.0: 455 | version "2.1.0" 456 | resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" 457 | integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== 458 | dependencies: 459 | binary-extensions "^2.0.0" 460 | 461 | is-core-module@^2.9.0: 462 | version "2.10.0" 463 | resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.10.0.tgz#9012ede0a91c69587e647514e1d5277019e728ed" 464 | integrity sha512-Erxj2n/LDAZ7H8WNJXd9tw38GYM3dv8rk8Zcs+jJuxYTW7sozH+SS8NtrSjVL1/vpLvWi1hxy96IzjJ3EHTJJg== 465 | dependencies: 466 | has "^1.0.3" 467 | 468 | is-extglob@^2.1.1: 469 | version "2.1.1" 470 | resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" 471 | integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== 472 | 473 | is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: 474 | version "4.0.3" 475 | resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" 476 | integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== 477 | dependencies: 478 | is-extglob "^2.1.1" 479 | 480 | is-number@^7.0.0: 481 | version "7.0.0" 482 | resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" 483 | integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== 484 | 485 | lilconfig@^2.0.5, lilconfig@^2.0.6: 486 | version "2.0.6" 487 | resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.0.6.tgz#32a384558bd58af3d4c6e077dd1ad1d397bc69d4" 488 | integrity sha512-9JROoBW7pobfsx+Sq2JsASvCo6Pfo6WWoUW79HuB1BCoBXD4PLWJPqDF6fNj67pqBYTbAHkE57M1kS/+L1neOg== 489 | 490 | merge2@^1.3.0: 491 | version "1.4.1" 492 | resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" 493 | integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== 494 | 495 | micromatch@^4.0.4: 496 | version "4.0.5" 497 | resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6" 498 | integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA== 499 | dependencies: 500 | braces "^3.0.2" 501 | picomatch "^2.3.1" 502 | 503 | mini-svg-data-uri@^1.2.3: 504 | version "1.4.4" 505 | resolved "https://registry.yarnpkg.com/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz#8ab0aabcdf8c29ad5693ca595af19dd2ead09939" 506 | integrity sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg== 507 | 508 | minimatch@^3.1.1: 509 | version "3.1.2" 510 | resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" 511 | integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== 512 | dependencies: 513 | brace-expansion "^1.1.7" 514 | 515 | minimist@^1.2.6: 516 | version "1.2.6" 517 | resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" 518 | integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== 519 | 520 | nanoid@^3.3.4: 521 | version "3.3.4" 522 | resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab" 523 | integrity sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw== 524 | 525 | node-releases@^2.0.6: 526 | version "2.0.6" 527 | resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.6.tgz#8a7088c63a55e493845683ebf3c828d8c51c5503" 528 | integrity sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg== 529 | 530 | normalize-path@^3.0.0, normalize-path@~3.0.0: 531 | version "3.0.0" 532 | resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" 533 | integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== 534 | 535 | normalize-range@^0.1.2: 536 | version "0.1.2" 537 | resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942" 538 | integrity sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA== 539 | 540 | object-hash@^3.0.0: 541 | version "3.0.0" 542 | resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-3.0.0.tgz#73f97f753e7baffc0e2cc9d6e079079744ac82e9" 543 | integrity sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw== 544 | 545 | once@^1.3.0: 546 | version "1.4.0" 547 | resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" 548 | integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== 549 | dependencies: 550 | wrappy "1" 551 | 552 | path-is-absolute@^1.0.0: 553 | version "1.0.1" 554 | resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" 555 | integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== 556 | 557 | path-parse@^1.0.7: 558 | version "1.0.7" 559 | resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" 560 | integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== 561 | 562 | picocolors@^1.0.0: 563 | version "1.0.0" 564 | resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" 565 | integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== 566 | 567 | picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1: 568 | version "2.3.1" 569 | resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" 570 | integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== 571 | 572 | pify@^2.3.0: 573 | version "2.3.0" 574 | resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" 575 | integrity sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog== 576 | 577 | postcss-import@^14.1.0: 578 | version "14.1.0" 579 | resolved "https://registry.yarnpkg.com/postcss-import/-/postcss-import-14.1.0.tgz#a7333ffe32f0b8795303ee9e40215dac922781f0" 580 | integrity sha512-flwI+Vgm4SElObFVPpTIT7SU7R3qk2L7PyduMcokiaVKuWv9d/U+Gm/QAd8NDLuykTWTkcrjOeD2Pp1rMeBTGw== 581 | dependencies: 582 | postcss-value-parser "^4.0.0" 583 | read-cache "^1.0.0" 584 | resolve "^1.1.7" 585 | 586 | postcss-js@^4.0.0: 587 | version "4.0.0" 588 | resolved "https://registry.yarnpkg.com/postcss-js/-/postcss-js-4.0.0.tgz#31db79889531b80dc7bc9b0ad283e418dce0ac00" 589 | integrity sha512-77QESFBwgX4irogGVPgQ5s07vLvFqWr228qZY+w6lW599cRlK/HmnlivnnVUxkjHnCu4J16PDMHcH+e+2HbvTQ== 590 | dependencies: 591 | camelcase-css "^2.0.1" 592 | 593 | postcss-load-config@^3.1.4: 594 | version "3.1.4" 595 | resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-3.1.4.tgz#1ab2571faf84bb078877e1d07905eabe9ebda855" 596 | integrity sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg== 597 | dependencies: 598 | lilconfig "^2.0.5" 599 | yaml "^1.10.2" 600 | 601 | postcss-nested@5.0.6: 602 | version "5.0.6" 603 | resolved "https://registry.yarnpkg.com/postcss-nested/-/postcss-nested-5.0.6.tgz#466343f7fc8d3d46af3e7dba3fcd47d052a945bc" 604 | integrity sha512-rKqm2Fk0KbA8Vt3AdGN0FB9OBOMDVajMG6ZCf/GoHgdxUJ4sBFp0A/uMIRm+MJUdo33YXEtjqIz8u7DAp8B7DA== 605 | dependencies: 606 | postcss-selector-parser "^6.0.6" 607 | 608 | postcss-selector-parser@^6.0.10, postcss-selector-parser@^6.0.6: 609 | version "6.0.10" 610 | resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz#79b61e2c0d1bfc2602d549e11d0876256f8df88d" 611 | integrity sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w== 612 | dependencies: 613 | cssesc "^3.0.0" 614 | util-deprecate "^1.0.2" 615 | 616 | postcss-value-parser@^4.0.0, postcss-value-parser@^4.2.0: 617 | version "4.2.0" 618 | resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" 619 | integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== 620 | 621 | postcss@^8.4.14, postcss@^8.4.16: 622 | version "8.4.16" 623 | resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.16.tgz#33a1d675fac39941f5f445db0de4db2b6e01d43c" 624 | integrity sha512-ipHE1XBvKzm5xI7hiHCZJCSugxvsdq2mPnsq5+UF+VHCjiBvtDrlxJfMBToWaP9D5XlgNmcFGqoHmUn0EYEaRQ== 625 | dependencies: 626 | nanoid "^3.3.4" 627 | picocolors "^1.0.0" 628 | source-map-js "^1.0.2" 629 | 630 | queue-microtask@^1.2.2: 631 | version "1.2.3" 632 | resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" 633 | integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== 634 | 635 | quick-lru@^5.1.1: 636 | version "5.1.1" 637 | resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932" 638 | integrity sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA== 639 | 640 | read-cache@^1.0.0: 641 | version "1.0.0" 642 | resolved "https://registry.yarnpkg.com/read-cache/-/read-cache-1.0.0.tgz#e664ef31161166c9751cdbe8dbcf86b5fb58f774" 643 | integrity sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA== 644 | dependencies: 645 | pify "^2.3.0" 646 | 647 | readdirp@~3.6.0: 648 | version "3.6.0" 649 | resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" 650 | integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== 651 | dependencies: 652 | picomatch "^2.2.1" 653 | 654 | resolve@^1.1.7, resolve@^1.22.1: 655 | version "1.22.1" 656 | resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177" 657 | integrity sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw== 658 | dependencies: 659 | is-core-module "^2.9.0" 660 | path-parse "^1.0.7" 661 | supports-preserve-symlinks-flag "^1.0.0" 662 | 663 | reusify@^1.0.4: 664 | version "1.0.4" 665 | resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" 666 | integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== 667 | 668 | run-parallel@^1.1.9: 669 | version "1.2.0" 670 | resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" 671 | integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== 672 | dependencies: 673 | queue-microtask "^1.2.2" 674 | 675 | source-map-js@^1.0.2: 676 | version "1.0.2" 677 | resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" 678 | integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== 679 | 680 | supports-preserve-symlinks-flag@^1.0.0: 681 | version "1.0.0" 682 | resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" 683 | integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== 684 | 685 | tailwindcss@^3.1.8: 686 | version "3.1.8" 687 | resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.1.8.tgz#4f8520550d67a835d32f2f4021580f9fddb7b741" 688 | integrity sha512-YSneUCZSFDYMwk+TGq8qYFdCA3yfBRdBlS7txSq0LUmzyeqRe3a8fBQzbz9M3WS/iFT4BNf/nmw9mEzrnSaC0g== 689 | dependencies: 690 | arg "^5.0.2" 691 | chokidar "^3.5.3" 692 | color-name "^1.1.4" 693 | detective "^5.2.1" 694 | didyoumean "^1.2.2" 695 | dlv "^1.1.3" 696 | fast-glob "^3.2.11" 697 | glob-parent "^6.0.2" 698 | is-glob "^4.0.3" 699 | lilconfig "^2.0.6" 700 | normalize-path "^3.0.0" 701 | object-hash "^3.0.0" 702 | picocolors "^1.0.0" 703 | postcss "^8.4.14" 704 | postcss-import "^14.1.0" 705 | postcss-js "^4.0.0" 706 | postcss-load-config "^3.1.4" 707 | postcss-nested "5.0.6" 708 | postcss-selector-parser "^6.0.10" 709 | postcss-value-parser "^4.2.0" 710 | quick-lru "^5.1.1" 711 | resolve "^1.22.1" 712 | 713 | to-regex-range@^5.0.1: 714 | version "5.0.1" 715 | resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" 716 | integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== 717 | dependencies: 718 | is-number "^7.0.0" 719 | 720 | turbo-morph@^0.1.0-beta.3: 721 | version "0.1.0-beta.7" 722 | resolved "https://registry.yarnpkg.com/turbo-morph/-/turbo-morph-0.1.0-beta.7.tgz#3a3839c711c2c32fb9f1c9a163cdc8ebbd700795" 723 | integrity sha512-PeQgyXN9GOtOnXVs9sH+nzEKKZ1h4mBqN538v3F2uivJUU/w+crwObf3e6Q7kj7mUBfDoDDFefEfYwvhNKR1uA== 724 | 725 | turbo_power@^0.1.2: 726 | version "0.1.2" 727 | resolved "https://registry.yarnpkg.com/turbo_power/-/turbo_power-0.1.2.tgz#e13e8c2e7a3fd7ba8fecfd45d4f43f861612a484" 728 | integrity sha512-8/atQyTVXcP1YOay8Sso2aTJy4oSU04jPQrtQ6e+1MYz6h/5sgz108Wu5kX8tIVrGtA+EDmnOqeGzck68b5/rg== 729 | dependencies: 730 | turbo-morph "^0.1.0-beta.3" 731 | turbo_ready "^0.0.6" 732 | 733 | turbo_ready@^0.0.6: 734 | version "0.0.6" 735 | resolved "https://registry.yarnpkg.com/turbo_ready/-/turbo_ready-0.0.6.tgz#7e3f01e4af26e38e5dad3291d6307ba82be220ff" 736 | integrity sha512-cyHh5VZjRxdsGPckRMmYbaDwkKY0O59I0Lp/uWffYNbAU70GofTJ6dUP1mjnkrYNVa5UA7sAUcKWXkz6rQnUYg== 737 | 738 | update-browserslist-db@^1.0.9: 739 | version "1.0.9" 740 | resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.9.tgz#2924d3927367a38d5c555413a7ce138fc95fcb18" 741 | integrity sha512-/xsqn21EGVdXI3EXSum1Yckj3ZVZugqyOZQ/CxYPBD/R+ko9NSUScf8tFF4dOKY+2pvSSJA/S+5B8s4Zr4kyvg== 742 | dependencies: 743 | escalade "^3.1.1" 744 | picocolors "^1.0.0" 745 | 746 | util-deprecate@^1.0.2: 747 | version "1.0.2" 748 | resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" 749 | integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== 750 | 751 | wrappy@1: 752 | version "1.0.2" 753 | resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" 754 | integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== 755 | 756 | xtend@^4.0.2: 757 | version "4.0.2" 758 | resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" 759 | integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== 760 | 761 | yaml@^1.10.2: 762 | version "1.10.2" 763 | resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" 764 | integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== 765 | --------------------------------------------------------------------------------