├── .gitignore
├── .rspec
├── .ruby-version
├── Capfile
├── Gemfile
├── Gemfile.lock
├── LICENSE
├── README.md
├── Rakefile
├── app
├── assets
│ ├── config
│ │ └── manifest.js
│ ├── images
│ │ └── favicon.png
│ ├── javascripts
│ │ └── application.js
│ └── stylesheets
│ │ ├── _helpers.sass
│ │ └── application.scss
├── channels
│ └── application_cable
│ │ ├── channel.rb
│ │ └── connection.rb
├── controllers
│ ├── application_controller.rb
│ ├── companies_controller.rb
│ ├── concerns
│ │ └── .keep
│ ├── pages_controller.rb
│ ├── placeholders_controller.rb
│ ├── projects_controller.rb
│ ├── tasks_controller.rb
│ └── tenants_controller.rb
├── helpers
│ ├── application_helper.rb
│ ├── flashes_helper.rb
│ ├── placeholder_helper.rb
│ └── tour_helper.rb
├── jobs
│ └── application_job.rb
├── mailers
│ └── application_mailer.rb
├── models
│ ├── application_record.rb
│ ├── company.rb
│ ├── concerns
│ │ └── .keep
│ ├── fragment_explainer.rb
│ ├── project.rb
│ ├── task.rb
│ └── tenant.rb
└── views
│ ├── companies
│ ├── _form.erb
│ ├── edit.erb
│ ├── index.erb
│ ├── new.erb
│ └── show.erb
│ ├── layouts
│ ├── _fragment_explainer.erb
│ ├── _nav.erb
│ └── application.erb
│ ├── pages
│ └── home.erb
│ ├── placeholders
│ ├── _form.erb
│ ├── _index.erb
│ ├── _show.erb
│ ├── _table.erb
│ ├── _table_inner.erb
│ ├── _templates.erb
│ ├── form.erb
│ ├── index.erb
│ ├── show.erb
│ └── table.erb
│ ├── projects
│ ├── _form.erb
│ ├── edit.erb
│ ├── index.erb
│ ├── new.erb
│ ├── show.erb
│ └── suggest_name.erb
│ └── tasks
│ ├── _form.erb
│ ├── _head.erb
│ ├── _task.erb
│ ├── edit.erb
│ ├── index.erb
│ ├── new.erb
│ └── show.erb
├── bin
├── bundle
├── rails
├── rake
├── setup
└── spring
├── config.ru
├── config
├── application.rb
├── boot.rb
├── cable.yml
├── database.yml
├── deploy.rb
├── deploy
│ └── production.rb
├── environment.rb
├── environments
│ ├── development.rb
│ ├── production.rb
│ └── test.rb
├── initializers
│ ├── application_controller_renderer.rb
│ ├── assets.rb
│ ├── backtrace_silencers.rb
│ ├── content_security_policy.rb
│ ├── cookies_serializer.rb
│ ├── ext.rb
│ ├── filter_parameter_logging.rb
│ ├── inflections.rb
│ ├── mime_types.rb
│ └── wrap_parameters.rb
├── locales
│ └── en.yml
├── puma.rb
├── routes.rb
├── schedule.rb
├── secrets.yml
├── spring.rb
└── storage.yml
├── db
├── migrate
│ ├── 20200716071558_add_nested_form_test_records.rb
│ ├── 20201209112615_create_tenants.rb
│ ├── 20210107141505_create_tasks.rb
│ ├── 20210623061414_drop_budgets.rb
│ ├── 20211201123306_index_foreign_keys.rb
│ └── 20241005194107_add_position_to_tasks.rb
└── seeds.rb
├── lib
├── assets
│ └── .keep
├── ext
│ ├── action_view
│ │ └── form_builder
│ │ │ └── error_message.rb
│ └── ruby
│ │ ├── enumerable
│ │ └── enumerable_natural_sort.rb
│ │ └── string
│ │ └── string_to_sort_atoms.rb
└── tasks
│ ├── .keep
│ └── db.rake
├── log
└── .keep
├── public
├── 404.html
├── 422.html
├── 500.html
├── apple-touch-icon-precomposed.png
├── apple-touch-icon.png
├── favicon.ico
└── robots.txt
├── spec
├── spec_helper.rb
├── support
│ └── capybara.rb
└── system
│ └── home_spec.rb
├── storage
└── .keep
├── test
├── application_system_test_case.rb
├── channels
│ └── application_cable
│ │ └── connection_test.rb
├── controllers
│ └── .keep
├── fixtures
│ ├── .keep
│ └── files
│ │ └── .keep
├── helpers
│ └── .keep
├── integration
│ └── .keep
├── mailers
│ └── .keep
├── models
│ └── .keep
├── system
│ └── .keep
└── test_helper.rb
└── vendor
└── asset-libs
├── .keep
├── roboto-mono
├── roboto-mono-v22-latin-regular.eot
├── roboto-mono-v22-latin-regular.svg
├── roboto-mono-v22-latin-regular.ttf
├── roboto-mono-v22-latin-regular.woff
├── roboto-mono-v22-latin-regular.woff2
└── roboto-mono.scss
└── roboto
├── roboto-v30-latin-500.eot
├── roboto-v30-latin-500.svg
├── roboto-v30-latin-500.ttf
├── roboto-v30-latin-500.woff
├── roboto-v30-latin-500.woff2
├── roboto-v30-latin-regular.eot
├── roboto-v30-latin-regular.svg
├── roboto-v30-latin-regular.ttf
├── roboto-v30-latin-regular.woff
├── roboto-v30-latin-regular.woff2
└── roboto.scss
/.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 uploaded files in development.
17 | /storage/*
18 | !/storage/.keep
19 |
20 | /public/assets
21 | .byebug_history
22 |
23 | # Ignore master key for decrypting credentials and more.
24 | /config/master.key
25 |
26 | /public/packs
27 | /public/packs-test
28 | /public/system
29 | /node_modules
30 | /yarn-error.log
31 | yarn-debug.log*
32 | .yarn-integrity
33 |
34 | /.idea
35 | /db/schema.rb
36 | /db/*.sqlite3
37 |
38 | /coverage
39 |
40 | # LibreOffice lock file
41 | .~lock.*xlsx#
42 |
--------------------------------------------------------------------------------
/.rspec:
--------------------------------------------------------------------------------
1 | --require spec_helper
2 |
--------------------------------------------------------------------------------
/.ruby-version:
--------------------------------------------------------------------------------
1 | 2.6.6
2 |
--------------------------------------------------------------------------------
/Capfile:
--------------------------------------------------------------------------------
1 | # Load DSL and set up stages
2 | require "capistrano/setup"
3 |
4 | # Include default deployment tasks
5 | require "capistrano/deploy"
6 |
7 | # Load the SCM plugin appropriate to your project:
8 | #
9 | # require "capistrano/scm/hg"
10 | # install_plugin Capistrano::SCM::Hg
11 | # or
12 | # require "capistrano/scm/svn"
13 | # install_plugin Capistrano::SCM::Svn
14 | # or
15 | require "capistrano/scm/git"
16 | install_plugin Capistrano::SCM::Git
17 |
18 | # Include tasks from other gems included in your Gemfile
19 | #
20 | # For documentation on these, see for example:
21 | #
22 | # https://github.com/capistrano/rvm
23 | # https://github.com/capistrano/rbenv
24 | # https://github.com/capistrano/chruby
25 | # https://github.com/capistrano/bundler
26 | # https://github.com/capistrano/rails
27 | # https://github.com/capistrano/passenger
28 | #
29 | # require "capistrano/rvm"
30 | # require "capistrano/rbenv"
31 | # require "capistrano/chruby"
32 | require "capistrano/bundler"
33 | require "capistrano/rails/assets"
34 | require "capistrano/rails/migrations"
35 | require 'whenever/capistrano'
36 | require "capistrano/opscomplete"
37 | require "capistrano/passenger"
38 |
39 | # Load custom tasks from `lib/capistrano/tasks` if you have any defined
40 | Dir.glob("lib/capistrano/tasks/*.rake").each { |r| import r }
41 |
42 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 | git_source(:github) { |repo| "https://github.com/#{repo}.git" }
3 |
4 | # Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
5 | gem 'rails', '~> 6.0'
6 | gem 'pg'
7 | gem 'sqlite3'
8 | # Use Puma as the app server
9 | gem 'puma'
10 | # Use SCSS for stylesheets
11 | gem 'sass-rails'
12 |
13 | # Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder
14 | gem 'jbuilder'
15 |
16 | gem 'coffee-script'
17 |
18 | gem 'faker'
19 |
20 | # Reduces boot times through caching; required in config/boot.rb
21 | gem 'bootsnap', '>= 1.4.2', require: false
22 |
23 | # gem 'unpoly-rails', path: '../unpoly-rails'
24 | gem 'unpoly-rails', '~> 3.10'
25 |
26 | gem 'bootstrap', '>=5.0.0'
27 |
28 | gem 'minidusen'
29 |
30 | gem 'has_defaults'
31 |
32 | gem 'database_cleaner'
33 |
34 | gem 'whenever', require: false
35 |
36 | gem 'terser'
37 |
38 | group :development, :test do
39 | # Call 'byebug' anywhere in the code to stop execution and get a debugger console
40 | gem 'byebug', platforms: [:mri, :mingw, :x64_mingw]
41 | end
42 |
43 | group :development do
44 | # Access an interactive console on exception pages or by calling 'console' anywhere in the code.
45 | gem 'web-console', '>= 3.3.0'
46 | gem 'listen', '~> 3.2'
47 | # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring
48 | gem 'spring'
49 | gem 'spring-watcher-listen', '~> 2.0.0'
50 |
51 | gem 'capistrano', '~> 3.10', require: false
52 | gem 'capistrano-rails', '~> 1.3', require: false
53 | gem 'capistrano-passenger', require: false
54 | gem 'capistrano-opscomplete', require: false
55 | end
56 |
57 | group :test do
58 | gem 'rspec-rails'
59 | gem 'capybara'
60 | gem 'selenium-webdriver'
61 | end
62 |
63 |
64 | # group :test do
65 | # # Adds support for Capybara system testing and selenium driver
66 | # gem 'capybara', '>= 2.15'
67 | # gem 'selenium-webdriver'
68 | # # Easy installation and use of web drivers to run system tests with browsers
69 | # gem 'webdrivers'
70 | # end
71 |
72 | # Windows does not include zoneinfo files, so bundle the tzinfo-data gem
73 | gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]
74 |
75 |
--------------------------------------------------------------------------------
/Gemfile.lock:
--------------------------------------------------------------------------------
1 | GEM
2 | remote: https://rubygems.org/
3 | specs:
4 | actioncable (6.1.7.2)
5 | actionpack (= 6.1.7.2)
6 | activesupport (= 6.1.7.2)
7 | nio4r (~> 2.0)
8 | websocket-driver (>= 0.6.1)
9 | actionmailbox (6.1.7.2)
10 | actionpack (= 6.1.7.2)
11 | activejob (= 6.1.7.2)
12 | activerecord (= 6.1.7.2)
13 | activestorage (= 6.1.7.2)
14 | activesupport (= 6.1.7.2)
15 | mail (>= 2.7.1)
16 | actionmailer (6.1.7.2)
17 | actionpack (= 6.1.7.2)
18 | actionview (= 6.1.7.2)
19 | activejob (= 6.1.7.2)
20 | activesupport (= 6.1.7.2)
21 | mail (~> 2.5, >= 2.5.4)
22 | rails-dom-testing (~> 2.0)
23 | actionpack (6.1.7.2)
24 | actionview (= 6.1.7.2)
25 | activesupport (= 6.1.7.2)
26 | rack (~> 2.0, >= 2.0.9)
27 | rack-test (>= 0.6.3)
28 | rails-dom-testing (~> 2.0)
29 | rails-html-sanitizer (~> 1.0, >= 1.2.0)
30 | actiontext (6.1.7.2)
31 | actionpack (= 6.1.7.2)
32 | activerecord (= 6.1.7.2)
33 | activestorage (= 6.1.7.2)
34 | activesupport (= 6.1.7.2)
35 | nokogiri (>= 1.8.5)
36 | actionview (6.1.7.2)
37 | activesupport (= 6.1.7.2)
38 | builder (~> 3.1)
39 | erubi (~> 1.4)
40 | rails-dom-testing (~> 2.0)
41 | rails-html-sanitizer (~> 1.1, >= 1.2.0)
42 | activejob (6.1.7.2)
43 | activesupport (= 6.1.7.2)
44 | globalid (>= 0.3.6)
45 | activemodel (6.1.7.2)
46 | activesupport (= 6.1.7.2)
47 | activerecord (6.1.7.2)
48 | activemodel (= 6.1.7.2)
49 | activesupport (= 6.1.7.2)
50 | activestorage (6.1.7.2)
51 | actionpack (= 6.1.7.2)
52 | activejob (= 6.1.7.2)
53 | activerecord (= 6.1.7.2)
54 | activesupport (= 6.1.7.2)
55 | marcel (~> 1.0)
56 | mini_mime (>= 1.1.0)
57 | activesupport (6.1.7.2)
58 | concurrent-ruby (~> 1.0, >= 1.0.2)
59 | i18n (>= 1.6, < 2)
60 | minitest (>= 5.1)
61 | tzinfo (~> 2.0)
62 | zeitwerk (~> 2.3)
63 | addressable (2.7.0)
64 | public_suffix (>= 2.0.2, < 5.0)
65 | airbrussh (1.4.0)
66 | sshkit (>= 1.6.1, != 1.7.0)
67 | autoprefixer-rails (10.4.16.0)
68 | execjs (~> 2)
69 | base64 (0.2.0)
70 | bindex (0.8.1)
71 | bootsnap (1.4.6)
72 | msgpack (~> 1.0)
73 | bootstrap (5.3.3)
74 | autoprefixer-rails (>= 9.1.0)
75 | popper_js (>= 2.11.8, < 3)
76 | builder (3.3.0)
77 | byebug (11.1.3)
78 | capistrano (3.16.0)
79 | airbrussh (>= 1.0.0)
80 | i18n
81 | rake (>= 10.0.0)
82 | sshkit (>= 1.9.0)
83 | capistrano-bundler (2.0.1)
84 | capistrano (~> 3.1)
85 | capistrano-opscomplete (1.0.0)
86 | capistrano (>= 3.0, < 4.0.0)
87 | rake
88 | capistrano-passenger (0.2.1)
89 | capistrano (~> 3.0)
90 | capistrano-rails (1.6.1)
91 | capistrano (~> 3.1)
92 | capistrano-bundler (>= 1.1, < 3)
93 | capybara (3.35.3)
94 | addressable
95 | mini_mime (>= 0.1.3)
96 | nokogiri (~> 1.8)
97 | rack (>= 1.6.0)
98 | rack-test (>= 0.6.3)
99 | regexp_parser (>= 1.5, < 3.0)
100 | xpath (~> 3.2)
101 | childprocess (3.0.0)
102 | chronic (0.10.2)
103 | coffee-script (2.4.1)
104 | coffee-script-source
105 | execjs
106 | coffee-script-source (1.12.2)
107 | concurrent-ruby (1.3.4)
108 | crass (1.0.6)
109 | database_cleaner (1.7.0)
110 | date (3.3.3)
111 | diff-lcs (1.4.4)
112 | edge_rider (2.3.0)
113 | activerecord (>= 3.2)
114 | erubi (1.13.0)
115 | execjs (2.7.0)
116 | faker (2.22.0)
117 | i18n (>= 1.8.11, < 2)
118 | ffi (1.13.1)
119 | globalid (1.1.0)
120 | activesupport (>= 5.0)
121 | has_defaults (0.4.4)
122 | activerecord
123 | i18n (1.14.6)
124 | concurrent-ruby (~> 1.0)
125 | jbuilder (2.10.0)
126 | activesupport (>= 5.0.0)
127 | listen (3.2.1)
128 | rb-fsevent (~> 0.10, >= 0.10.3)
129 | rb-inotify (~> 0.9, >= 0.9.10)
130 | logger (1.7.0)
131 | loofah (2.22.0)
132 | crass (~> 1.0.2)
133 | nokogiri (>= 1.12.0)
134 | mail (2.8.1)
135 | mini_mime (>= 0.1.1)
136 | net-imap
137 | net-pop
138 | net-smtp
139 | marcel (1.0.2)
140 | memoized (1.1.2)
141 | method_source (1.1.0)
142 | mini_mime (1.1.2)
143 | mini_portile2 (2.8.7)
144 | minidusen (0.11.0)
145 | activerecord (>= 3.2)
146 | activesupport (>= 3.2)
147 | edge_rider (>= 0.2.5)
148 | minitest (5.25.1)
149 | msgpack (1.3.3)
150 | net-imap (0.3.4)
151 | date
152 | net-protocol
153 | net-pop (0.1.2)
154 | net-protocol
155 | net-protocol (0.2.1)
156 | timeout
157 | net-scp (4.1.0)
158 | net-ssh (>= 2.6.5, < 8.0.0)
159 | net-sftp (4.0.0)
160 | net-ssh (>= 5.0.0, < 8.0.0)
161 | net-smtp (0.3.3)
162 | net-protocol
163 | net-ssh (7.3.0)
164 | nio4r (2.5.8)
165 | nokogiri (1.13.10)
166 | mini_portile2 (~> 2.8.0)
167 | racc (~> 1.4)
168 | ostruct (0.6.1)
169 | pg (1.2.2)
170 | popper_js (2.11.8)
171 | public_suffix (4.0.6)
172 | puma (4.3.8)
173 | nio4r (~> 2.0)
174 | racc (1.8.1)
175 | rack (2.2.9)
176 | rack-test (2.1.0)
177 | rack (>= 1.3)
178 | rails (6.1.7.2)
179 | actioncable (= 6.1.7.2)
180 | actionmailbox (= 6.1.7.2)
181 | actionmailer (= 6.1.7.2)
182 | actionpack (= 6.1.7.2)
183 | actiontext (= 6.1.7.2)
184 | actionview (= 6.1.7.2)
185 | activejob (= 6.1.7.2)
186 | activemodel (= 6.1.7.2)
187 | activerecord (= 6.1.7.2)
188 | activestorage (= 6.1.7.2)
189 | activesupport (= 6.1.7.2)
190 | bundler (>= 1.15.0)
191 | railties (= 6.1.7.2)
192 | sprockets-rails (>= 2.0.0)
193 | rails-dom-testing (2.2.0)
194 | activesupport (>= 5.0.0)
195 | minitest
196 | nokogiri (>= 1.6)
197 | rails-html-sanitizer (1.5.0)
198 | loofah (~> 2.19, >= 2.19.1)
199 | railties (6.1.7.2)
200 | actionpack (= 6.1.7.2)
201 | activesupport (= 6.1.7.2)
202 | method_source
203 | rake (>= 12.2)
204 | thor (~> 1.0)
205 | rake (13.2.1)
206 | rb-fsevent (0.10.4)
207 | rb-inotify (0.10.1)
208 | ffi (~> 1.0)
209 | regexp_parser (2.1.1)
210 | rspec-core (3.10.1)
211 | rspec-support (~> 3.10.0)
212 | rspec-expectations (3.10.1)
213 | diff-lcs (>= 1.2.0, < 2.0)
214 | rspec-support (~> 3.10.0)
215 | rspec-mocks (3.10.2)
216 | diff-lcs (>= 1.2.0, < 2.0)
217 | rspec-support (~> 3.10.0)
218 | rspec-rails (5.0.1)
219 | actionpack (>= 5.2)
220 | activesupport (>= 5.2)
221 | railties (>= 5.2)
222 | rspec-core (~> 3.10)
223 | rspec-expectations (~> 3.10)
224 | rspec-mocks (~> 3.10)
225 | rspec-support (~> 3.10)
226 | rspec-support (3.10.2)
227 | rubyzip (2.3.0)
228 | sass-rails (6.0.0)
229 | sassc-rails (~> 2.1, >= 2.1.1)
230 | sassc (2.4.0)
231 | ffi (~> 1.9)
232 | sassc-rails (2.1.2)
233 | railties (>= 4.0.0)
234 | sassc (>= 2.0)
235 | sprockets (> 3.0)
236 | sprockets-rails
237 | tilt
238 | selenium-webdriver (3.142.7)
239 | childprocess (>= 0.5, < 4.0)
240 | rubyzip (>= 1.2.2)
241 | spring (2.1.0)
242 | spring-watcher-listen (2.0.1)
243 | listen (>= 2.7, < 4.0)
244 | spring (>= 1.2, < 3.0)
245 | sprockets (4.2.0)
246 | concurrent-ruby (~> 1.0)
247 | rack (>= 2.2.4, < 4)
248 | sprockets-rails (3.4.2)
249 | actionpack (>= 5.2)
250 | activesupport (>= 5.2)
251 | sprockets (>= 3.0.0)
252 | sqlite3 (1.4.2)
253 | sshkit (1.24.0)
254 | base64
255 | logger
256 | net-scp (>= 1.1.2)
257 | net-sftp (>= 2.1.2)
258 | net-ssh (>= 2.8.0)
259 | ostruct
260 | terser (1.1.3)
261 | execjs (>= 0.3.0, < 3)
262 | thor (1.3.2)
263 | tilt (2.0.10)
264 | timeout (0.3.2)
265 | tzinfo (2.0.6)
266 | concurrent-ruby (~> 1.0)
267 | unpoly-rails (3.10.2)
268 | actionpack (>= 3.2)
269 | activesupport (>= 3.2)
270 | memoized
271 | railties (>= 3.2)
272 | web-console (4.2.1)
273 | actionview (>= 6.0.0)
274 | activemodel (>= 6.0.0)
275 | bindex (>= 0.4.0)
276 | railties (>= 6.0.0)
277 | websocket-driver (0.7.5)
278 | websocket-extensions (>= 0.1.0)
279 | websocket-extensions (0.1.5)
280 | whenever (1.0.0)
281 | chronic (>= 0.6.3)
282 | xpath (3.2.0)
283 | nokogiri (~> 1.8)
284 | zeitwerk (2.6.18)
285 |
286 | PLATFORMS
287 | ruby
288 |
289 | DEPENDENCIES
290 | bootsnap (>= 1.4.2)
291 | bootstrap (>= 5.0.0)
292 | byebug
293 | capistrano (~> 3.10)
294 | capistrano-opscomplete
295 | capistrano-passenger
296 | capistrano-rails (~> 1.3)
297 | capybara
298 | coffee-script
299 | database_cleaner
300 | faker
301 | has_defaults
302 | jbuilder
303 | listen (~> 3.2)
304 | minidusen
305 | pg
306 | puma
307 | rails (~> 6.0)
308 | rspec-rails
309 | sass-rails
310 | selenium-webdriver
311 | spring
312 | spring-watcher-listen (~> 2.0.0)
313 | sqlite3
314 | terser
315 | tzinfo-data
316 | unpoly-rails (~> 3.10)
317 | web-console (>= 3.3.0)
318 | whenever
319 |
320 | BUNDLED WITH
321 | 2.4.22
322 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2021 Henning Koch
2 |
3 | MIT License
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining
6 | a copy of this software and associated documentation files (the
7 | "Software"), to deal in the Software without restriction, including
8 | without limitation the rights to use, copy, modify, merge, publish,
9 | distribute, sublicense, and/or sell copies of the Software, and to
10 | permit persons to whom the Software is furnished to do so, subject to
11 | the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be
14 | included in all copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # README
2 |
3 | This README would normally document whatever steps are necessary to get the
4 | application up and running.
5 |
6 | Things you may want to cover:
7 |
8 | * Ruby version
9 |
10 | * System dependencies
11 |
12 | * Configuration
13 |
14 | * Database creation
15 |
16 | * Database initialization
17 |
18 | * How to run the test suite
19 |
20 | * Services (job queues, cache servers, search engines, etc.)
21 |
22 | * Deployment instructions
23 |
24 | * ...
25 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | # Add your own tasks in files placed in lib/tasks ending in .rake,
2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
3 |
4 | require_relative 'config/application'
5 |
6 | Rails.application.load_tasks
7 |
--------------------------------------------------------------------------------
/app/assets/config/manifest.js:
--------------------------------------------------------------------------------
1 | //= link_tree ../images
2 | //= link_directory ../stylesheets .css
3 | //= link application.js
4 | //= link application.css
5 |
6 |
--------------------------------------------------------------------------------
/app/assets/images/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/unpoly/unpoly-demo/HEAD/app/assets/images/favicon.png
--------------------------------------------------------------------------------
/app/assets/javascripts/application.js:
--------------------------------------------------------------------------------
1 | // Unpoly //////////////////////////////////////////////////////////////////////////////////////////////////////////////
2 |
3 | //= require unpoly
4 | //= require unpoly-bootstrap5
5 |
6 | // Accelerate all links so the demo feels snappy.
7 | // We still assign out [up-follow] manually so there's less magic for demo viewers.
8 | up.link.config.instantSelectors.unshift('a[href]')
9 | up.link.config.preloadSelectors.unshift('a[href]')
10 |
11 | // We use a .form-group to contain each (label, input, error, help) tuple.
12 | // Unpoly can use this to only update the affected a group when validating.
13 | up.form.config.groupSelectors.unshift('.form-group')
14 |
15 | // Since we're rendering instant loading state for all interactions in the demo,
16 | // wait longer until we show the progress bar.
17 | up.network.config.lateTime = 1250
18 |
19 | // Enable more logging for curious users.
20 | up.log.enable()
21 |
22 |
23 | // Tour /////////////////////////////////////////////////////////////////////////////////////////////////////////////////
24 |
25 | // Gray out tour dots once clicked.
26 | up.on('up:link:follow', '.tour-dot', (event, element) => { element.classList.add('viewed') })
27 |
28 |
29 | // Fragment explainer //////////////////////////////////////////////////////////////////////////////////////////////////
30 |
31 | up.compiler('.fragment-explainer', function(container) {
32 | let lastFragment = document.documentElement
33 |
34 | let targetExplainer = container.querySelector('.fragment-explainer--target')
35 | let revealTarget = container.querySelector('.fragment-explainer--reveal')
36 | let requestExplainer = container.querySelector('.fragment-explainer--request')
37 | let rttExplainer = container.querySelector('.fragment-explainer--rtt')
38 |
39 | return [
40 | up.on('up:fragment:inserted', (_event, fragment) => {
41 | if (fragment.matches('[up-hungry]')) return
42 | if (fragment.querySelector('.placeholder')) return
43 | if (fragment.matches('up-modal, up-modal main, up-drawer, up-drawer main, up-popup, up-popup main')) fragment = up.layer.current.getBoxElement()
44 | lastFragment = fragment
45 | targetExplainer.innerText = up.fragment.toTarget(fragment, { verify: false })
46 | if (config.showFragments.checked) showInsertedFlash(lastFragment)
47 | }),
48 |
49 | up.on(revealTarget, 'click', (event) => {
50 | up.event.halt(event)
51 | showInsertedFlash(lastFragment)
52 | }),
53 |
54 | up.on('up:link:follow up:form:submit', ({ renderOptions }) => {
55 | let method = up.util.normalizeMethod(renderOptions.method)
56 | requestExplainer.innerText = `${method} ${renderOptions.url}`
57 | }),
58 |
59 | up.on('up:fragment:loaded', ({ request, response, revalidating }) => {
60 | if (revalidating) return
61 |
62 | let rtt = Math.max(response.loadedAt - request.builtAt, 0)
63 | let info = `${rtt} ms`
64 | if (request.fromCache) {
65 | info += ' (cache) '
66 | }
67 | rttExplainer.innerHTML = info
68 | })
69 | ]
70 | })
71 |
72 | up.compiler('form#config', function(form) {
73 | return [
74 | up.on('up:link:follow up:form:submit', function(event) {
75 | if (form.disableCache.checked) {
76 | event.renderOptions.cache = false
77 | }
78 | if (form.noPreviews.checked) {
79 | Object.assign(event.renderOptions, up.RenderOptions.NO_PREVIEWS)
80 | }
81 | if (form.noMotion.checked) {
82 | Object.assign(event.renderOptions, up.RenderOptions.NO_MOTION)
83 | }
84 | }),
85 |
86 | up.on('up:link:preload', (event) => {
87 | if (form.disableCache.checked) {
88 | event.preventDefault()
89 | }
90 | }),
91 |
92 | up.on('up:request:load', ({ request }) => {
93 | if (form.extraLatency.checked) {
94 | request.headers['X-Extra-Latency'] = 'true'
95 | }
96 | }),
97 |
98 | up.on('up:click', { capture: true }, (event) => {
99 | if (form.showClicks.checked) {
100 | let style = {
101 | top: event.clientY + "px",
102 | left: event.clientX + "px"
103 | }
104 | let bubble = up.element.affix(document.body, '.click-bubble', { style })
105 | bubble.addEventListener('animationend', () => bubble.remove());
106 | }
107 | })
108 | ]
109 | })
110 |
111 | function showInsertedFlash(fragment) {
112 | fragment = up.fragment.get(fragment)
113 | fragment.classList.add('new-fragment', 'inserted')
114 | up.util.timer(0, () => fragment.classList.remove('inserted'))
115 | up.util.timer(750, () => fragment.classList.remove('new-fragment'))
116 | }
117 |
118 |
119 | // Notifications ///////////////////////////////////////////////////////////////////////////////////////////////////////
120 |
121 | // Prompt user to reload when frontend assets changeds on the server.
122 | up.on('up:assets:changed', function() {
123 | up.element.show(document.querySelector('#new-version'))
124 | })
125 |
126 | // Remove notification flashes after a few seconds.
127 | up.compiler('.alert', function(alert) {
128 | up.util.timer(4000, () => up.destroy(alert, { animation: 'move-to-top' }))
129 | })
130 |
131 |
132 | // Loading state ///////////////////////////////////////////////////////////////////////////////////////////////////////
133 |
134 | function findButton(origin) {
135 | if (origin.matches('.btn')) {
136 | return origin
137 | } else {
138 | let form = origin.closest('form')
139 | return up.form.submitButtons(form)[0]
140 | }
141 | }
142 |
143 | // Show a spinning wheel inside the button.
144 | up.preview('btn-spinner', function(preview) {
145 | let button = findButton(preview.origin)
146 | // Keep the button dimensions while we're hiding its content.
147 | preview.setStyle(button, { height: button.offsetHeight + 'px', width: button.offsetWidth + 'px' })
148 | preview.swapContent(button, ' ')
149 | })
150 |
151 | // Show a spinning wheel inside a loading popup overlay
152 | up.preview('popup-spinner', function(preview) {
153 | preview.showPlaceholder('
')
154 | })
155 |
156 | // Show a spinning wheel inside a loading main element
157 | up.preview('main-spinner', function(preview) {
158 | let main = up.fragment.closest(preview.fragment, ':main')
159 | preview.insert(main, 'afterbegin', '
')
160 | })
161 |
162 | // Shorten placeholder table to the maximum number of rows given
163 | up.compiler('.placeholder-wave', function(placeholder, { rows = 15 }) {
164 | let trs = placeholder.querySelectorAll('tr')
165 | for (let i = 0; i < trs.length - rows; i++) {
166 | trs[i].remove()
167 | }
168 | })
169 |
170 |
171 | // Templates with simple {{variable}} substitution /////////////////////////////////////////////////////////////////////
172 |
173 | up.on('up:template:clone', '[type="text/minimustache"]', function(event) {
174 | let template = event.target.innerHTML
175 | let filled = template.replace(/{{(\w+)}}/g, (_match, variable) => up.util.escapeHTML(event.data[variable]))
176 | event.nodes = up.element.createNodesFromHTML(filled)
177 | })
178 |
179 |
180 | // Tasks ///////////////////////////////////////////////////////////////////////////////////////////////////////////////
181 |
182 | // Setting a `-done` class will line-through the task text.
183 | up.preview('finish-task', function(preview) {
184 | preview.addClass('-done')
185 | })
186 |
187 | // Removing a `-done` class will remove the line-through decoration from the task text.
188 | up.preview('unfinish-task', function(preview) {
189 | preview.removeClass('-done')
190 | })
191 |
192 | up.preview('add-task', function(preview) {
193 | let form = preview.origin.closest('form')
194 | let text = preview.params.get('task[text]')
195 | let newItem = up.template.clone('#task-preview', { text })
196 | preview.insert(form, 'afterend', newItem)
197 | form.reset()
198 | })
199 |
200 | up.preview('clear-tasks', function(preview) {
201 | let doneTasks = up.fragment.all('.task.-done')
202 | for (let doneTask of doneTasks) {
203 | preview.hide(doneTask)
204 | }
205 | })
206 |
207 | up.compiler('#tasks', function(container) {
208 | let draggingItem
209 |
210 | up.on(container, 'dragstart', '.task-item', function(event, item) {
211 | draggingItem = item
212 | item.classList.add('-dragging')
213 | })
214 |
215 | up.on(container, 'dragover', '.task', function(event, item) {
216 | event.preventDefault() // prime drop event
217 | item.classList.add('-dropping')
218 | })
219 |
220 | up.on(container, 'dragleave', '.task', function(event, item) {
221 | item.classList.remove('-dropping')
222 | })
223 |
224 | up.on(container, 'drop', '.task', function(event, dropItem) {
225 | if (!draggingItem) return
226 | event.preventDefault()
227 |
228 | draggingItem.classList.remove('-dragging')
229 | dropItem.classList.remove('-dropping')
230 |
231 | if (draggingItem !== dropItem) {
232 | up.render({
233 | target: '#tasks',
234 | method: 'patch',
235 | url: `/tasks/${draggingItem.dataset.id}/move`,
236 | params: { reference: dropItem.dataset.id },
237 | fallback: true,
238 | preview: (preview) => preview.insert(dropItem, 'afterend', draggingItem)
239 | })
240 | }
241 | })
242 |
243 | })
244 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/_helpers.sass:
--------------------------------------------------------------------------------
1 | @function spacer($length)
2 | @return map-get($spacers, $length)
3 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/application.scss:
--------------------------------------------------------------------------------
1 | //= require unpoly
2 | //= require unpoly-bootstrap5
3 | //= require roboto
4 | //= require roboto-mono
5 |
6 |
7 | // Theme ///////////////////////////////////////////////////////////////////////////////////////////////////////////////
8 |
9 | @import 'bootstrap';
10 | @import 'helpers';
11 |
12 | :root {
13 | --bs-font-sans-serif: 'Roboto', sans-serif;
14 | --bs-font-monospace: 'Roboto Mono', monospace;
15 | }
16 |
17 | a {
18 | --bs-link-decoration: none;
19 | }
20 |
21 | .navbar {
22 | --bs-navbar-padding-x: 1rem;
23 | }
24 |
25 | body {
26 | margin: 5.5rem spacer(4) 7rem spacer(4);
27 | overflow-y: scroll;
28 | }
29 |
30 | .container {
31 | max-width: 1100px;
32 | }
33 |
34 | p {
35 | margin-bottom: 0.8rem;
36 | }
37 |
38 | b, strong, th {
39 | font-weight: 500;
40 | }
41 |
42 | .field_with_errors {
43 | display: contents;
44 |
45 | // For some reason display: contents does not work here.
46 | .input-group & {
47 | display: inline-block;
48 | flex: 1 1 auto;
49 | }
50 |
51 | &:not(form.up-active &) {
52 |
53 | label {
54 | color: $danger;
55 | }
56 |
57 | input,
58 | textarea,
59 | select {
60 | border-color: $danger;
61 | }
62 |
63 | }
64 |
65 | }
66 |
67 | .form-error {
68 | &:not(form.up-active &) {
69 | color: $danger;
70 | }
71 | }
72 |
73 |
74 | .form-group {
75 | margin-bottom: spacer(3);
76 | }
77 |
78 | .btn {
79 | text-align: center;
80 |
81 | &.btn-sm {
82 | min-width: 50px;
83 | }
84 |
85 | &:not(.btn-sm) {
86 | min-width: 75px;
87 | }
88 | }
89 |
90 | $popup-spacing: 12px;
91 |
92 | up-popup {
93 | z-index: 1070;
94 | }
95 |
96 | up-popup.tour-hint {
97 | margin: $popup-spacing;
98 |
99 | code {
100 | white-space: nowrap
101 | }
102 | }
103 |
104 | up-popup-content {
105 | line-height: 1.3;
106 |
107 | p {
108 | margin-bottom: 0.7rem;
109 | }
110 |
111 | :not(.popup-spinner):last-child {
112 | margin-bottom: 0;
113 | }
114 | }
115 |
116 | up-modal-box {
117 | padding: 1.8rem 2rem;
118 | }
119 |
120 | up-modal-content {
121 | :last-child {
122 | margin-bottom: 0;
123 | }
124 |
125 | >*:last-child {
126 | margin-bottom: 0;
127 | }
128 | }
129 |
130 |
131 | // Tasks /////////////////////////////////////////////////////////////////////////////////////////////
132 |
133 | #tasks {
134 | max-width: 650px;
135 | margin: 0 auto;
136 | }
137 |
138 | .task {
139 | position: relative;
140 | display: flex;
141 | align-items: center;
142 | gap: spacer(3);
143 | border: $border-width $border-style $border-color;
144 | padding: ($spacer * 0.5) $spacer;
145 |
146 | &:first-child {
147 | border-top-left-radius: $border-radius;
148 | border-top-right-radius: $border-radius;
149 | }
150 |
151 | &:last-child {
152 | border-bottom-left-radius: $border-radius;
153 | border-bottom-right-radius: $border-radius;
154 | }
155 |
156 | &+& {
157 | border-top: none;
158 | }
159 |
160 | }
161 |
162 | .task-item {
163 | &.-done {
164 | .task-item--text {
165 | text-decoration: line-through;
166 | }
167 |
168 | .task-item--toggle:after {
169 | content: '✔';
170 | position: absolute;
171 | width: 100%;
172 | height: 100%;
173 | font-size: 1.2em;
174 | line-height: 1.3em;
175 | text-align: center;
176 | }
177 | }
178 |
179 | &.-previewing {
180 | color: $gray-600
181 | }
182 |
183 | &[draggable=true] {
184 | cursor: move;
185 | }
186 |
187 | }
188 |
189 | .task-item--toggle {
190 | flex: 0 0 auto;
191 | width: 1.5em;
192 | height: 1.5em;
193 | display: inline-block;
194 | border-radius: 4em;
195 | border: 1px solid $gray-500;
196 | position: relative;
197 | }
198 |
199 | .task-item--text {
200 | flex: 1 1 0;
201 | padding: $btn-padding-y 0;
202 | }
203 |
204 | .task-item--edit {
205 | flex: 0 0 auto;
206 | }
207 |
208 | .task-item.-dragging {
209 | cursor: move;
210 | cursor: -webkit-grabbing;
211 | cursor: -moz-grabbing;
212 | }
213 |
214 | .task.-dropping {
215 | border-bottom-color: $primary !important;
216 | border-bottom-width: 3px;
217 | margin-bottom: -2px;
218 | }
219 |
220 | .task-form {
221 | gap: 0;
222 | display: grid;
223 | grid-template-columns: 1fr auto;
224 | grid-template-rows: auto;
225 | grid-template-areas:
226 | "input save"
227 | "error error";
228 | align-items: center;
229 |
230 |
231 | column-gap: spacer(3);
232 |
233 | &:focus {
234 | outline: none !important;
235 | }
236 | }
237 |
238 | .task-form--input {
239 | grid-area: input;
240 | &:focus {
241 | box-shadow: none !important;
242 | }
243 | }
244 |
245 | .task-form--save {
246 | grid-area: save;
247 | }
248 |
249 | .task-form--error {
250 | margin-top: spacer(1);
251 | grid-area: error;
252 | }
253 |
254 |
255 | // Fragment explainer //////////////////////////////////////////////////////////////////////////////////////////////////
256 |
257 | .fragment-explainer {
258 | background-color: white;
259 | padding: 0.95rem 1.2rem 1.0rem;
260 | position: fixed;
261 | z-index: 2500;
262 | bottom: 0;
263 | left: 0;
264 | width: 100vw;
265 | border-top: 1px solid $gray-500;
266 | box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.1);
267 | display: flex;
268 | gap: 0.6rem 1.7rem;
269 | flex-wrap: wrap;
270 | line-height: 1.35;
271 | justify-content: flex-start;
272 | align-items: center;
273 | }
274 |
275 | .fragment-explainer--cell {
276 | flex: 0 1 auto;
277 | min-width: calc(min(100px, 13vw));
278 |
279 | a {
280 | text-decoration: underline;
281 | }
282 | }
283 |
284 | .fragment-explainer--label {
285 | margin-bottom: 4px;
286 | font-size: 0.8em;
287 | font-weight: 500;
288 | text-transform: uppercase;
289 | color: #444;
290 | }
291 |
292 | #config {
293 | display: grid;
294 | grid-template-columns: auto auto auto;
295 | column-gap: 1em;
296 | }
297 |
298 | .new-fragment {
299 | $new-fragment-color: #fc2;
300 | $new-fragment-opacity: 0.6;
301 |
302 | position: relative;
303 |
304 | &:after {
305 | transition: all 750ms ease-out;
306 | content: '';
307 | position: absolute;
308 | left: 0;
309 | top: 0;
310 | right: 0;
311 | bottom: 0;
312 | background-color: rgba($new-fragment-color, 0);
313 | outline: 10px solid rgba($new-fragment-color, 0);
314 | z-index: 1;
315 | }
316 |
317 | &.inserted:after {
318 | background-color: rgba($new-fragment-color, $new-fragment-opacity);
319 | outline: 10px solid rgba($new-fragment-color, $new-fragment-opacity);
320 | }
321 | }
322 |
323 | .click-bubble {
324 | box-sizing: border-box;
325 | position: fixed;
326 | border-style: solid;
327 | border-color: #ff2299;
328 | border-radius: 50%;
329 | animation: click-bubble 0.3s ease-out;
330 | z-index: 999999;
331 | }
332 |
333 | @keyframes click-bubble {
334 | 0% {
335 | opacity: 0.8;
336 | width:0.5em; height: 0.5em;
337 | margin: -0.25em;
338 | border-width: 0.5em;
339 | }
340 | 100% {
341 | opacity: 0.1;
342 | width: 6em;
343 | height: 6em;
344 | margin: -3em;
345 | border-width: 0.2em;
346 | }
347 | }
348 |
349 |
350 | // Tour ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
351 |
352 | $tour-dot-color: $green;
353 |
354 | .tour-dot {
355 | margin: 0 0.4rem;
356 | display: inline-block;
357 | vertical-align: middle;
358 | width: 10px;
359 | height: 10px;
360 | border-radius: 50%;
361 | background: $tour-dot-color;
362 | cursor: pointer;
363 | box-shadow: 0 0 0 4px rgba($tour-dot-color, 0.3);
364 | font-size: 0;
365 |
366 | &:hover {
367 | animation: none;
368 | }
369 | &[overlay-only] {
370 | display: none;
371 |
372 | up-modal &,
373 | up-drawer & {
374 | display: inline-block;
375 | }
376 | }
377 | &.viewed {
378 | animation: none;
379 | filter: grayscale(100%);
380 | opacity: 0.6;
381 | }
382 | }
383 |
384 |
385 | // Notifications ////////////////////////////////////////////////////////////////////////////////////////////////////////
386 |
387 | #new-version {
388 | position: fixed;
389 | top: 0.75rem;
390 | right: 1rem;
391 | z-index: 1050;
392 | }
393 |
394 | [up-flashes], .flashes-explainer {
395 | position: fixed;
396 | top: 0.4rem;
397 | right: 1rem;
398 | z-index: 1050;
399 |
400 | .alert {
401 | padding: 0.5rem 0.7rem;
402 | margin: 0;
403 | }
404 | }
405 |
406 | .flashes-explainer {
407 | padding: 0.5rem 0.7rem;
408 | border: 1px solid rgba(0, 0, 0, 0.1);
409 | color: #666;
410 | border-radius: 0.25rem;
411 |
412 | #new-version:not([hidden]) + & {
413 | display: none;
414 | }
415 | }
416 |
417 |
418 | // Loading state ///////////////////////////////////////////////////////////////////////////////////////////////////////
419 |
420 | .placeholder {
421 | opacity: 0.1;
422 | border-radius: 15px;
423 | }
424 |
425 | @mixin spinner($size: 1em, $color: $info, $thickness: 2px) {
426 | display: inline-block;
427 | vertical-align: middle;
428 | width: $size;
429 | aspect-ratio: 1;
430 | position: relative;
431 |
432 | // Have the rotating square in an :after element so the
433 | // bounding box of the actual element remains a steady rectangle.
434 | &:after {
435 | content: '';
436 | position: absolute;
437 | left: 0;
438 | top: 0;
439 | right: 0;
440 | bottom: 0;
441 | border: $thickness solid rgba($color, 1.0);
442 | border-bottom-color: rgba($color, 0.2);
443 | border-radius: 50%;
444 | box-sizing: border-box;
445 | animation: rotation 1s linear infinite;
446 | opacity: inherit;
447 | @content;
448 | }
449 |
450 | }
451 |
452 | .btn-spinner {
453 | @include spinner(1em, white);
454 | margin-top: -0.15em;
455 |
456 | .btn:has(&) {
457 | text-align: center;
458 | }
459 | }
460 |
461 | .popup-spinner {
462 | @include spinner($size: 2.5rem, $color: $info);
463 | margin: 0.5rem 3rem 6rem 0.5rem;
464 | }
465 |
466 | .main-spinner {
467 | @include spinner($size: 6rem, $color: $info, $thickness: 3px) {
468 | transition: opacity 0.3s ease-in;
469 | opacity: 1.0;
470 | @starting-style {
471 | opacity: 0.1;
472 | }
473 | }
474 |
475 | position: absolute;
476 | top: 50%;
477 | left: 50%;
478 | transform: translate(-50%, -50%);
479 |
480 | main:has(>&) {
481 | position: relative;
482 | opacity: 1;
483 | filter: grayscale(0%);
484 |
485 | & > *:not(.main-spinner) {
486 | transition: opacity 0.3s ease-out, filter 0.3s ease-out;
487 | opacity: 0.4;
488 | filter: grayscale(100%);
489 | }
490 | }
491 | }
492 |
493 | .placeholder-wave {
494 | transition: opacity 0.2s ease-in;
495 | opacity: 1.0;
496 |
497 | @starting-style {
498 | opacity: 0.5;
499 | }
500 | }
501 |
502 | @keyframes rotation {
503 | 0% {
504 | transform: rotate(0deg);
505 | }
506 | 100% {
507 | transform: rotate(360deg);
508 | }
509 | }
510 |
511 | $search-active-stripe-color: rgba(200, 230, 255, 0.3);
512 |
513 | input[type=search].up-active {
514 | background-image:
515 | repeating-linear-gradient(
516 | -45deg,
517 | transparent,
518 | transparent 1rem,
519 | $search-active-stripe-color 1rem,
520 | $search-active-stripe-color 2rem
521 | );
522 | background-size: 2000% 2000%;
523 | animation: barberpole 200s linear infinite;
524 | }
525 |
526 | @keyframes barberpole {
527 | 100% {
528 | background-position: -1000% 1000%;
529 | }
530 | }
531 |
532 |
--------------------------------------------------------------------------------
/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/controllers/application_controller.rb:
--------------------------------------------------------------------------------
1 | class ApplicationController < ActionController::Base
2 | include Memoized
3 |
4 | before_action :reset_faker
5 | before_action :create_missing_tenant
6 | after_action :emulate_latency
7 |
8 | private
9 |
10 | def create_missing_tenant
11 | unless current_tenant
12 | tenant = Tenant.create!
13 | session[:tenant_id] = tenant.id
14 | session[:return_to] = request.original_url
15 |
16 | # This action checks if the user has cookies enables,
17 | # then redirects back to the current poath.
18 | redirect_to verify_tenant_path
19 | end
20 | end
21 |
22 | helper_method memoize def current_tenant
23 | if (tenant_id = session[:tenant_id].presence)
24 | Tenant.for_current_schema.where(id: tenant_id).first
25 | end
26 | end
27 |
28 | helper_method memoize def fragment_explainer
29 | FragmentExplainer.new(self)
30 | end
31 |
32 | def reset_faker
33 | # The model is asking Faker to produce unique names in Tenent.create, Project.suggest_name, etc.
34 | # We must periodically release the list of used names or Faker will eventually throw errors.
35 | # See https://github.com/faker-ruby/faker#ensuring-unique-values
36 | Faker::UniqueGenerator.clear
37 | end
38 |
39 | def emulate_latency
40 | if up? && request.headers['X-Extra-Latency'].present? && !response.redirect?
41 | sleep 1.0
42 | end
43 | end
44 |
45 | end
46 |
--------------------------------------------------------------------------------
/app/controllers/companies_controller.rb:
--------------------------------------------------------------------------------
1 | class CompaniesController < ApplicationController
2 |
3 | def new
4 | build_company
5 | end
6 |
7 | def create
8 | build_company
9 | save_company(form: 'new')
10 | end
11 |
12 | def edit
13 | load_company
14 | build_company
15 | end
16 |
17 | def update
18 | load_company
19 | build_company
20 | save_company(form: 'edit')
21 | end
22 |
23 | def show
24 | load_company
25 | end
26 |
27 | def index
28 | load_companies
29 | end
30 |
31 | def destroy
32 | load_company
33 | if @company.destroy
34 | up.layer.emit('company:destroyed')
35 | redirect_to companies_path
36 | else
37 | redirect_to @company, alert: 'Could not delete company'
38 | end
39 | end
40 |
41 | private
42 |
43 | def build_company
44 | @company ||= company_scope.build
45 | @company.attributes = company_attributes
46 | end
47 |
48 | def load_company
49 | @company ||= company_scope.find(params[:id])
50 | end
51 |
52 | def save_company(form:)
53 | if up.validate?
54 | @company.valid? # run validations
55 | render form
56 | elsif @company.save
57 | redirect_to @company, notice: 'Company saved successfully'
58 | else
59 | render form, status: :bad_request
60 | end
61 | end
62 |
63 | def load_companies
64 | @companies = company_scope
65 | .search(params[:query])
66 | .order(:name)
67 | .to_a
68 | end
69 |
70 | def company_scope
71 | current_tenant.companies
72 | end
73 |
74 | def company_attributes
75 | if (attrs = params[:company])
76 | attrs.permit(:name, :address)
77 | else
78 | {}
79 | end
80 | end
81 |
82 | end
83 |
--------------------------------------------------------------------------------
/app/controllers/concerns/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/controllers/pages_controller.rb:
--------------------------------------------------------------------------------
1 | class PagesController < ApplicationController
2 | def home
3 | end
4 | end
5 |
--------------------------------------------------------------------------------
/app/controllers/placeholders_controller.rb:
--------------------------------------------------------------------------------
1 | class PlaceholdersController < ApplicationController
2 |
3 | def index
4 | end
5 |
6 | def show
7 | end
8 |
9 | def form
10 | end
11 |
12 | end
13 |
--------------------------------------------------------------------------------
/app/controllers/projects_controller.rb:
--------------------------------------------------------------------------------
1 | class ProjectsController < ApplicationController
2 |
3 | def new
4 | build_project
5 | end
6 |
7 | def create
8 | build_project
9 | save_project(form: 'new')
10 | end
11 |
12 | def edit
13 | load_project
14 | build_project
15 | end
16 |
17 | def update
18 | load_project
19 | build_project
20 | save_project(form: 'edit')
21 | end
22 |
23 | def show
24 | load_project
25 | end
26 |
27 | def index
28 | load_projects
29 | end
30 |
31 | def destroy
32 | load_project
33 | if @project.destroy
34 | up.layer.emit('project:destroyed')
35 | redirect_to projects_path
36 | else
37 | redirect_to @project, alert: 'Could not delete project'
38 | end
39 | end
40 |
41 | def suggest_name
42 | @names = (1..10).map { Project.suggest_name }
43 | end
44 |
45 | private
46 |
47 | def build_project
48 | @project ||= project_scope.build
49 | @project.attributes = project_attributes
50 | end
51 |
52 | def load_project
53 | @project ||= project_scope.find(params[:id])
54 | end
55 |
56 | def save_project(form:)
57 | if up.validate?
58 | @project.valid? # run validations
59 | render form
60 | elsif @project.save
61 | up.layer.emit('project:saved')
62 | redirect_to @project, notice: 'Project saved successfully'
63 | else
64 | render form, status: :bad_request
65 | end
66 | end
67 |
68 | def load_projects
69 | @projects = project_scope.includes(:company).order(:name).to_a
70 | end
71 |
72 | def project_scope
73 | current_tenant.projects
74 | end
75 |
76 | def project_attributes
77 | if (attrs = params[:project])
78 | attrs.permit(:name, :company_id, :budget)
79 | else
80 | {}
81 | end
82 | end
83 |
84 | end
85 |
--------------------------------------------------------------------------------
/app/controllers/tasks_controller.rb:
--------------------------------------------------------------------------------
1 | class TasksController < ApplicationController
2 |
3 | def new
4 | build_task
5 | end
6 |
7 | def create
8 | build_task
9 |
10 | if @task.save
11 | redirect_to tasks_path, notice: 'Task added'
12 | else
13 | render 'new', status: :bad_request
14 | end
15 | end
16 |
17 | def edit
18 | load_task
19 | build_task
20 | end
21 |
22 | def update
23 | load_task
24 | build_task
25 |
26 | if @task.save
27 | redirect_to @task, notice: 'Task updated'
28 | else
29 | render 'edit', status: :bad_request
30 | end
31 | end
32 |
33 | def show
34 | load_task
35 | end
36 |
37 | def index
38 | render_tasks
39 | end
40 |
41 | def clear_done
42 | task_scope.clear_done!
43 | redirect_to tasks_path
44 | end
45 |
46 | def finish
47 | load_task
48 | @task.finish!
49 | redirect_to @task
50 | end
51 |
52 | def unfinish
53 | load_task
54 | @task.unfinish!
55 | redirect_to @task
56 | end
57 |
58 | def move
59 | load_task
60 | reference = params[:reference]
61 | if reference == 'start'
62 | task_scope.move_to_start!(@task)
63 | else
64 | reference_task = find_task(reference)
65 | task_scope.move_after!(reference_task, @task)
66 | end
67 |
68 | render_tasks
69 | end
70 |
71 | private
72 |
73 | def render_tasks
74 | load_tasks
75 | render :index
76 | end
77 |
78 | def load_tasks
79 | @tasks ||= task_scope.to_a
80 | end
81 |
82 | def task_scope
83 | current_tenant.tasks.by_position
84 | end
85 |
86 | def load_task
87 | @task ||= find_task(params[:id])
88 | end
89 |
90 | def find_task(id)
91 | task_scope.find(id)
92 | end
93 |
94 | def build_task
95 | @task ||= task_scope.build
96 | @task.attributes = task_attributes
97 | end
98 |
99 | def task_attributes
100 | if (attrs = params[:task])
101 | attrs.permit(:text, :done)
102 | else
103 | {}
104 | end
105 | end
106 |
107 | helper_method def open_task_count
108 | load_tasks
109 | @tasks.select(&:open?).size
110 | end
111 |
112 | end
113 |
--------------------------------------------------------------------------------
/app/controllers/tenants_controller.rb:
--------------------------------------------------------------------------------
1 | class TenantsController < ApplicationController
2 | skip_before_action :create_missing_tenant
3 |
4 | def verify
5 | if current_tenant
6 | return_to = session[:return_to].presence || root_url
7 | redirect_to return_to
8 | else
9 | render text: 'This demo requires cookies'
10 | end
11 |
12 | end
13 |
14 | end
15 |
--------------------------------------------------------------------------------
/app/helpers/application_helper.rb:
--------------------------------------------------------------------------------
1 | module ApplicationHelper
2 |
3 | # def button_to(name = nil, options = nil, html_options = nil, &block)
4 | # up_html_options, html_options = html_options.partition { |key, _value| key.to_s.start_with?('up-') }.map(&:to_h)
5 | # html_options[:form] ||= {}
6 | # html_options[:form].merge!(up_html_options)
7 | # super(name, options, html_options, &block)
8 | # end
9 |
10 | end
11 |
--------------------------------------------------------------------------------
/app/helpers/flashes_helper.rb:
--------------------------------------------------------------------------------
1 | module FlashesHelper
2 |
3 | def flashes
4 | html = ''.html_safe
5 | flash.map do |type, message|
6 | html << content_tag(:div, message, class: bootstrap_flash_classes(type), role: 'alert')
7 | end
8 | html
9 | end
10 |
11 | def bootstrap_flash_type(type)
12 | case type.to_s
13 | when 'notice'
14 | 'success'
15 | when 'error', 'alert'
16 | 'danger'
17 | else
18 | 'info'
19 | end
20 | end
21 |
22 | def bootstrap_flash_classes(type)
23 | "alert alert-#{bootstrap_flash_type(type)}"
24 | end
25 |
26 | end
27 |
--------------------------------------------------------------------------------
/app/helpers/placeholder_helper.rb:
--------------------------------------------------------------------------------
1 | module PlaceholderHelper
2 |
3 | def placeholder(*content, &block)
4 | content_tag(:span, *content, class: 'placeholder', &block)
5 | end
6 |
7 | end
8 |
--------------------------------------------------------------------------------
/app/helpers/tour_helper.rb:
--------------------------------------------------------------------------------
1 | module TourHelper
2 |
3 | def tour_dot(options = {}, &block)
4 | html = capture(&block).strip.html_safe
5 |
6 | # The margin looks nicer when the text is wrapped in tags.
7 | # Wrap the given HTML in a
if the callerr hasn't already done so.
8 | unless html.starts_with?('<')
9 | html = content_tag(:p, html)
10 | end
11 |
12 | # Add a button to close the hint popup.
13 | html << <<~HTML.html_safe
14 |
15 | OK
16 |
17 | HTML
18 |
19 | if strip_tags(html).size > 400
20 | size = 'large'
21 | else
22 | size = 'medium'
23 | end
24 |
25 | # The hint is just an Unpoly popup.
26 | attrs = {
27 | class: 'tour-dot',
28 | href: '#',
29 | 'up-layer': 'new popup',
30 | 'up-content': '' + html, # force-escape the HTML string by making it unsafe
31 | 'up-position': options.fetch(:position, 'right'),
32 | 'up-align': options.fetch(:align, 'top'),
33 | 'up-class': 'tour-hint',
34 | 'up-size': size
35 | }
36 |
37 | # This is a hint to hide this dot on the root layer (see application.sass).
38 | if options[:overlay_only]
39 | attrs['overlay-only'] = ''
40 | end
41 |
42 | if (size = options[:size])
43 | attrs['up-size'] = size
44 | end
45 |
46 | content_tag(:a, '', attrs)
47 | end
48 |
49 | end
50 |
--------------------------------------------------------------------------------
/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/mailers/application_mailer.rb:
--------------------------------------------------------------------------------
1 | class ApplicationMailer < ActionMailer::Base
2 | default from: 'from@example.com'
3 | layout 'mailer'
4 | end
5 |
--------------------------------------------------------------------------------
/app/models/application_record.rb:
--------------------------------------------------------------------------------
1 | class ApplicationRecord < ActiveRecord::Base
2 | self.abstract_class = true
3 | end
4 |
--------------------------------------------------------------------------------
/app/models/company.rb:
--------------------------------------------------------------------------------
1 | class Company < ActiveRecord::Base
2 | belongs_to :tenant
3 | has_many :projects, dependent: :destroy
4 |
5 | validates :name, presence: true, uniqueness: { case_sensitive: false, scope: :tenant_id }
6 | validates :address, presence: true
7 |
8 | def self.search(query)
9 | if query.present?
10 | where_like([:name, :address] => query)
11 | else
12 | all
13 | end
14 | end
15 |
16 | end
17 |
--------------------------------------------------------------------------------
/app/models/concerns/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/models/fragment_explainer.rb:
--------------------------------------------------------------------------------
1 | class FragmentExplainer
2 |
3 | def initialize(controller)
4 | @controller = controller
5 | end
6 |
7 | def controller_short_path
8 | "#{controller_name}_controller.rb"
9 | end
10 |
11 | def controller_path
12 | "app/controllers/#{controller_short_path}"
13 | end
14 |
15 | def controller_github_url
16 | github_url(controller_path)
17 | end
18 |
19 | def view_short_path
20 | "#{controller_name}/#{view_name}.erb"
21 | end
22 |
23 | def view_path
24 | "app/views/#{view_short_path}"
25 | end
26 |
27 | def view_github_url
28 | github_url(view_path)
29 | end
30 |
31 | def layout_short_path
32 | "application.html.erb"
33 | end
34 |
35 | def layout_path
36 | "app/views/layouts/#{layout_short_path}"
37 | end
38 |
39 | def layout_github_url
40 | github_url(layout_path)
41 | end
42 |
43 | # def updated_selector
44 | # selector = if response.status.to_s.starts_with?('2')
45 | # up.target
46 | # else
47 | # up.fail_target
48 | # end
49 | #
50 | # selector = selector.gsub(', [up-flashes=""]', '')
51 | # selector = selector.gsub(', .fragment-explainer', '')
52 | # selector = selector.gsub('[up-main~=modal]', 'main')
53 | # selector = selector.gsub('[up-main~=popup]', 'main')
54 | #
55 | # selector
56 | # end
57 |
58 | delegate :controller_name, :action_name, :response, :up, :up?, to: :controller
59 |
60 | private
61 |
62 | def github_url(path)
63 | "https://github.com/unpoly/unpoly-demo/blob/master/#{path}"
64 | end
65 |
66 | def view_name
67 | name = action_name
68 | name = name.sub(/^create$/, 'new')
69 | name = name.sub(/^update$/, 'edit')
70 | name
71 | end
72 |
73 | attr_reader :controller
74 |
75 | # def relative_to_root(path)
76 | # path.sub(Rails.root.to_s + '/', '')
77 | # end
78 |
79 | end
80 |
81 | # def track_view_path
82 | # subscriber = ActiveSupport::Notifications.subscribe "render_template.action_view" do |name, started, finished, unique_id, data|
83 | # @view_path = relative_to_root(data[:identifier])
84 | # @layout_path = relative_to_root(data[:layout])
85 | # end
86 | #
87 | #
88 | # @controller_path = "app/controllers/" + controller_name + "_controller.rb"
89 | # @view_path = "app/views/" + controller_name + "/" + action_name.sub('create', 'new').sub('update', 'edit') + ".html.erb"
90 | #
91 | # yield
92 | # ensure
93 | # ActiveSupport::Notifications.unsubscribe(subscriber)
94 | # end
95 | #
96 |
--------------------------------------------------------------------------------
/app/models/project.rb:
--------------------------------------------------------------------------------
1 | class Project < ActiveRecord::Base
2 | belongs_to :tenant
3 | belongs_to :company
4 |
5 | validates :name, presence: true, uniqueness: { case_sensitive: false, scope: :tenant_id }
6 | validates :company_id, presence: true
7 | validates :budget, numericality: { greater_than_or_equal_to: 100 }
8 |
9 | def self.suggest_name
10 | Faker::App.unique.name
11 | end
12 |
13 | end
14 |
--------------------------------------------------------------------------------
/app/models/task.rb:
--------------------------------------------------------------------------------
1 | class Task < ApplicationRecord
2 | belongs_to :tenant
3 | validates :text, presence: true, uniqueness: { case_sensitive: false, scope: :tenant_id }
4 |
5 | has_defaults done: false
6 |
7 | scope :by_position, ->{ order(Arel.sql('COALESCE(position, -id)')) }
8 |
9 | def open?
10 | !done?
11 | end
12 |
13 | def finish!
14 | update!(done: true)
15 | end
16 |
17 | def unfinish!
18 | update!(done: false)
19 | end
20 |
21 | class << self
22 |
23 | def clear_done!
24 | where(done: true).destroy_all
25 | end
26 |
27 | def move_to_start!(task)
28 | old_list = by_position.to_a
29 | new_list = [task, *old_list.without(task)]
30 | rewrite_positions!(new_list)
31 | end
32 |
33 | def move_after!(reference, task)
34 | old_list = by_position.to_a
35 | new_list = old_list.without(task)
36 | reference_position = new_list.index(reference)
37 | new_list.insert(reference_position + 1, task)
38 | rewrite_positions!(new_list)
39 | end
40 |
41 | private
42 |
43 | def rewrite_positions!(list)
44 | Rails.logger.info "rewrite_positions: #{list.inspect}"
45 | list.each_with_index do |item, index|
46 | item.update_attribute(:position, index)
47 | end
48 | end
49 |
50 | end
51 |
52 | end
53 |
--------------------------------------------------------------------------------
/app/models/tenant.rb:
--------------------------------------------------------------------------------
1 | class Tenant < ApplicationRecord
2 |
3 | has_many :companies, dependent: :delete_all
4 | has_many :projects, dependent: :delete_all
5 | has_many :tasks, dependent: :delete_all
6 |
7 | after_create :create_sample_records
8 |
9 | # If we change the schema we recreate new tenants
10 | has_defaults schema_version: -> { self.class.schema_version }
11 |
12 | scope :for_current_schema, -> { where(schema_version: schema_version)}
13 |
14 | def self.schema_version
15 | connection.migration_context.current_version
16 | end
17 |
18 | def self.clean!
19 | old_tenants = where('created_at < ?', 7.days.ago)
20 | incompatible_tenants = where('schema_version != ?', schema_version)
21 | cleanable_tenants = old_tenants.or(incompatible_tenants)
22 | cleanable_tenants.find_each(&:destroy)
23 | end
24 |
25 | private
26 |
27 | def create_sample_records
28 | 5.times do
29 | company = companies.create!(
30 | name: Faker::Company.unique.name,
31 | address: sample_address
32 | )
33 |
34 | 3.times do
35 | projects.create!(
36 | company: company,
37 | name: Project.suggest_name,
38 | budget: ((1..150).to_a.sample * 1000),
39 | )
40 | end
41 | end
42 |
43 | 5.times do
44 | tasks.create!(
45 | text: Faker::Lorem.unique.sentence(word_count: 2, random_words_to_add: 3).chop,
46 | created_at: rand(100).hours.ago
47 | )
48 | end
49 | end
50 |
51 | def sample_address
52 | <<~ADDRESS
53 | #{Faker::Address.street_name} #{rand(100) + 1}
54 | #{Faker::Address.postcode} #{Faker::Address.city}
55 | #{Faker::Address.country}
56 | ADDRESS
57 | end
58 |
59 | end
60 |
61 |
--------------------------------------------------------------------------------
/app/views/companies/_form.erb:
--------------------------------------------------------------------------------
1 | <%= form_for @company, html: {
2 | 'up-submit': true,
3 | 'up-validate': true,
4 | 'up-disable': true,
5 | 'up-preview': 'btn-spinner',
6 | } do |form| %>
7 |
8 |
17 |
18 |
19 | <%= form.label :address %>
20 | <%= form.text_area :address, class: 'form-control', rows: 6 %>
21 | <%= form.error_message :address %>
22 |
23 |
24 |
25 |
26 | <%= form.button "Save", class: 'btn btn-primary' %>
27 |
28 | <% if @company.new_record? %>
29 | <%= tour_dot(overlay_only: true) do %>
30 |
Saving an invalid form will re-render validation errors in the same overlay.
31 |
Links and forms always render in the same layer unless another layer is targeted explicitly.
32 | <% end %>
33 | <% end %>
34 |
35 |
36 |
37 | <% if local_assigns[:cancel_path] %>
38 | <%= link_to 'Cancel', cancel_path, class: 'btn btn-outline-secondary',
39 | 'up-follow': true,
40 | 'up-preview': 'main-spinner'
41 | %>
42 | <% end %>
43 |
44 |
45 |
46 | <% end %>
47 |
--------------------------------------------------------------------------------
/app/views/companies/edit.erb:
--------------------------------------------------------------------------------
1 | Edit company <%= @company.id %>
2 |
3 | <%= render 'form', cancel_path: @company %>
4 |
--------------------------------------------------------------------------------
/app/views/companies/index.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
All companies
4 |
5 |
6 |
7 | <%= tour_dot(position: 'left') do %>
8 |
9 | The search form has an [up-target="#companies"] attribute.
10 | This causes the submission results to update the <div id="companies"> element below.
11 |
12 |
13 |
14 | The form also has an [up-autosubmit] attribute.
15 | This automatically submits the user while the user is typing.
16 |
17 |
18 |
19 | Forms and links assigned an .up-active class while loading.
20 | We use this class for the stripe animation on the search field.
21 |
22 | <% end %>
23 |
24 | <%= form_tag companies_path, method: :get, class: 'd-inline-block',
25 | 'up-target': '#companies',
26 | 'up-autosubmit': true do
27 | %>
28 |
29 |
30 | <% end %>
31 |
32 |
33 |
34 | <%= tour_dot(position: 'left') do %>
35 |
36 | The New company button has an [up-layer=new] attribute.
37 | This causes the link destination to open in an overlay.
38 |
39 |
40 | The button also defines a close condition using an [up-accept-location] attribute.
41 | This causes the overlay to automatically close when it reaches the URL of a newly created company.
42 |
43 |
44 | The [up-on-accepted] attribute causes the company list below to reload when the overlay closes.
45 |
46 | <% end %>
47 |
48 | <%= link_to 'New company', new_company_path, class: 'btn btn-primary',
49 | 'up-layer': 'new',
50 | 'up-placeholder': '#form-placeholder',
51 | 'up-accept-location': company_path('$id'),
52 | 'up-on-accepted': "up.reload('#companies', { placeholder: '#table-placeholder { rows: 5 }' })"
53 | %>
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 | Name
62 |
63 |
64 |
65 |
66 | <% @companies.each_with_index do |company, i| %>
67 |
68 |
69 | <%= link_to company.name, company,
70 | 'up-layer': 'new',
71 | 'up-placeholder': '#show-placeholder',
72 | 'up-dismiss-event': 'company:destroyed',
73 | 'up-on-dismissed': "up.reload('#companies', { placeholder: '#table-placeholder { rows: 5 }' })"
74 | %>
75 |
76 | <%= tour_dot do %>
77 | The link to the company details has an [up-layer=new] attribute.
78 | This causes the link destination to open in an overlay.
79 | <% end if i == 0 %>
80 |
81 |
82 | <% end %>
83 |
84 |
85 |
86 |
--------------------------------------------------------------------------------
/app/views/companies/new.erb:
--------------------------------------------------------------------------------
1 | New company
2 |
3 | <%= render 'form' %>
4 |
--------------------------------------------------------------------------------
/app/views/companies/show.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Company #<%= @company.id %>
4 |
5 |
6 |
7 | <%= tour_dot(overlay_only: true) do %>
8 |
Note how the Edit link renders within this overlay.
9 |
Links and forms always update their own layer unless another layer is targeted explicitly.
10 | <% end %>
11 |
12 | <%= link_to 'Edit', [:edit, @company],
13 | class: 'btn btn-primary',
14 | 'up-follow': true,
15 | 'up-preview': 'main-spinner'
16 | %>
17 |
18 | <%= link_to 'Delete', @company, method: :delete,
19 | class: 'btn btn-danger',
20 | 'up-follow': true,
21 | 'up-confirm': 'Really delete?',
22 | 'up-preview': 'btn-spinner'
23 | %>
24 |
25 |
26 |
27 |
28 | Name
29 | <%= @company.name %>
30 | Address
31 |
32 | <% if @company.address.present? %>
33 | <%= safe_join(@company.address.lines, ' '.html_safe) %>
34 | <% else %>
35 | —
36 | <% end %>
37 |
38 |
39 |
40 |
41 |
42 |
Projects
43 |
44 |
45 |
46 |
47 | <%= tour_dot(position: 'left') do %>
48 |
49 | This button re-uses the project form that we already built for the Projects tab.
50 |
51 |
52 |
53 | When a new project was created, the overlay closes and the project list is reloaded.
54 |
55 | <% end %>
56 | <%= link_to 'Add project', new_project_path(project: { company_id: @company.id }), class: 'btn btn-primary btn-sm',
57 | 'up-layer': 'new',
58 | 'up-placeholder': '#form-placeholder',
59 | 'up-accept-location': project_path('$id'),
60 | 'up-on-accepted': "up.reload('#projects', { placeholder: '#table-placeholder { rows: 5 }' })",
61 | 'up-context': { from_company: true }.to_json
62 | %>
63 |
64 |
65 |
66 |
67 |
68 | <% if @company.projects.present? %>
69 |
70 |
71 | Name
72 | Budget
73 |
74 |
75 | <% @company.projects.natural_sort_by(&:name).each_with_index do |project, i| %>
76 |
77 |
78 | <%= link_to project.name, project,
79 | 'up-layer': 'new',
80 | 'up-placeholder': '#show-placeholder',
81 | 'up-dismiss-event': 'project:destroyed',
82 | 'up-on-dismissed': "up.reload('#projects', { placeholder: '#table-placeholder { rows: 5 }' })",
83 | 'up-context': { from_company: true }.to_json
84 | %>
85 | <%= tour_dot(overlay_only: true) do %>
86 | Opening the project will open another modal over this one.
87 | Overlays can be stacked infinitely.
88 | <% end if i == 0%>
89 |
90 |
91 | <%= number_to_currency(project.budget, unit: '€') %>
92 |
93 |
94 | <% end %>
95 |
96 |
97 | <% else %>
98 |
99 | This company has no projects yet.
100 |
101 | <% end %>
102 |
103 |
--------------------------------------------------------------------------------
/app/views/layouts/_fragment_explainer.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
Last fragment:
5 |
html
6 | (
reveal )
7 |
8 |
9 |
10 |
Server-side code:
11 |
16 |
17 |
18 |
19 |
Last request:
20 |
GET <%= request.path %>
21 |
22 |
23 |
24 |
Round-trip:
25 |
26 | (initial load)
27 |
28 |
29 |
30 |
58 |
59 |
60 |
--------------------------------------------------------------------------------
/app/views/layouts/_nav.erb:
--------------------------------------------------------------------------------
1 |
2 | <%= link_to 'Unpoly Demo', root_path, class: 'navbar-brand', 'up-follow': true, 'up-placeholder': '#index-placeholder' %>
3 |
4 |
5 | <%= tour_dot do %>
6 | Navigation links have the [up-follow] attribute. Clicking such links only updates a page fragment . The remaining DOM is not changed.
7 | Unpoly links can update arbitrary fragments, identified by CSS selectors. The default selector is the <main> element.
8 | <% end %>
9 |
10 | <%= link_to 'Companies', companies_path, class: 'nav-item nav-link', 'up-follow': true, 'up-alias' => company_path('*'), 'up-placeholder': '#index-placeholder { rows: 5 }' %>
11 | <%= link_to 'Projects', projects_path, class: 'nav-item nav-link', 'up-follow': true, 'up-alias' => project_path('*'), 'up-placeholder': '#index-placeholder' %>
12 | <%= link_to 'Tasks', tasks_path, class: 'nav-item nav-link', 'up-follow': true, 'up-alias' => task_path('*'), 'up-placeholder': '#index-placeholder' %>
13 |
14 |
15 |
--------------------------------------------------------------------------------
/app/views/layouts/application.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Unpoly Demo
5 | <%= csrf_meta_tags %>
6 | <%= csp_meta_tag %>
7 |
8 | <%= javascript_include_tag 'application', defer: true %>
9 | <%= stylesheet_link_tag 'application', media: 'all' %>
10 | <%= favicon_link_tag("favicon.png") %>
11 |
12 |
13 |
14 | <%= render 'layouts/nav' %>
15 |
16 |
17 | A newer version is available!
18 |
Reload
19 |
20 |
21 |
22 | <%= tour_dot(position: 'left') do %>
23 |
24 | The element after this one uses the [up-flashes] attribute
25 | to extract notification messages from any response, even
26 | if this container wasn't targeted.
27 |
28 |
29 |
30 | A compiler removes any <div class="alert">
31 | after some seconds.
32 |
33 | <% end %>
34 | Notifications go here
35 |
36 |
37 |
38 | <%= flashes %>
39 |
40 |
41 |
42 |
43 | <%= yield %>
44 |
45 |
46 |
47 | <%= render 'layouts/fragment_explainer' %>
48 |
49 | <%= render 'placeholders/templates' unless up? %>
50 |
51 |
52 |
53 |
--------------------------------------------------------------------------------
/app/views/pages/home.erb:
--------------------------------------------------------------------------------
1 | Unpoly Demo
2 |
3 |
4 | This is a small CRUD
5 | application to show off Unpoly .
6 |
7 |
8 |
9 |
10 | We recommend viewing this demo on a PC or tablet.
11 |
12 |
13 |
14 | Like all Unpoly applications, this demo is a classic web app that renders HTML on the server.
15 | The app is enhanced to feel like a single-page app
16 | by adding [up] attributes to links and forms. Aside from these attributes the server does not need to know about Unpoly in the frontend.
17 |
18 |
19 | Fragments
20 |
21 |
22 | Much of Unpoly's magic lies in updating page fragments instead of making full page loads.
23 | The server still renders full HTML pages , but we only use
24 | the targeted fragments and discard the rest.
25 |
26 |
27 |
28 | Under this model all DOM state outside the updated fragment is preserved.
29 | Following links and forms feels smooth, without seeing flashes of incomplete content or losing scroll positions or incomplete form data.
30 | Pages also load much faster since the DOM, CSS and JavaScript environments do not need to be
31 | destroyed and recreated for every request.
32 |
33 |
34 |
35 | To get a better understanding which parts of the page were updated, the bar at the bottom shows which selectors were
36 | targeted. Click on reveal to highlight the fragment in the page.
37 |
38 |
39 |
40 | The demo app
41 |
42 |
43 | The top navigation divides the app is into three sections:
44 |
45 |
46 |
47 | Companies is a CRUD interface for company records. A company can be assigned one or more projects.
48 | Projects is a CRUD interface for project records. Some of its templates are reused in the Companies section.
49 | Tasks is a simple TODO list, not connected to companies or projects.
50 |
51 |
52 | Exploring the app
53 |
54 |
55 | Click on everything! All data is scoped to your session, so feel free to change and delete records.
56 |
57 |
58 |
59 | Throughout the app you will see green dots <%= tour_dot { 'This is a hint!' } %>.
60 | Clicking on a dot will reveal a hint about what you can discover here.
61 |
62 |
63 | Source code
64 |
65 |
66 | We encourage you to use your browser's DevTools to inspect the HTML. Take a closer look at links, forms and their [up] attributes.
67 |
68 |
69 |
70 | The bar at the bottom of the screen shows links to the server-side code that rendered the last HTML response.
71 | The entire application code can be cloned from the unpoly-demo repository.
72 |
73 |
74 |
75 | The backend is written in Ruby, but don't let that scare you if you're using another language.
76 | The backend only makes a few SQL queries and renders HTML using a template language similar to PHP, Moustache or Pug.
77 | The community has also provided a Python re-implementation of this demo using Django.
78 |
79 |
80 |
81 |
82 | The frontend contains no significant JavaScript
83 | aside from Unpoly itself. The CSS is just Bootstrap 4
84 | with minimal tweaks.
85 |
86 |
--------------------------------------------------------------------------------
/app/views/placeholders/_form.erb:
--------------------------------------------------------------------------------
1 |
2 |
<%= placeholder 'Placeholder #123' %>
3 |
4 |
20 |
21 |
22 |
23 | Save
24 |
25 |
26 |
27 | Cancel
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/app/views/placeholders/_index.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
<%= placeholder 'All records' %>
5 |
6 |
7 | New record
8 |
9 |
10 |
11 | <%= render 'placeholders/table_inner' %>
12 |
13 |
--------------------------------------------------------------------------------
/app/views/placeholders/_show.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
<%= placeholder 'Placeholder #123' %>
5 |
6 |
7 |
8 | Edit
9 | Delete
10 |
11 |
12 |
13 |
14 | <%= placeholder 'Name' %>
15 | <%= placeholder 'Acme Corporation' %>
16 |
17 | <%= placeholder 'Address' %>
18 |
19 | <%= placeholder do %>
20 | Footaster Street 123
21 |
22 | 12345 Fooville
23 | <% end %>
24 |
25 |
26 | <%= placeholder 'Budget' %>
27 | <%= placeholder '€ 1234' %>
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/app/views/placeholders/_table.erb:
--------------------------------------------------------------------------------
1 |
2 | <%= render 'placeholders/table_inner' %>
3 |
4 |
--------------------------------------------------------------------------------
/app/views/placeholders/_table_inner.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | <%= placeholder 'Name' %>
5 | <%= placeholder 'Company' %>
6 |
7 |
8 |
9 |
10 | <% 15.times do%>
11 |
12 | <%= placeholder Faker::App.name %>
13 | <%= placeholder Faker::Company.name %>
14 |
15 | <% end %>
16 |
17 |
18 |
--------------------------------------------------------------------------------
/app/views/placeholders/_templates.erb:
--------------------------------------------------------------------------------
1 |
2 | <%= render 'placeholders/index' %>
3 |
4 |
5 |
6 | <%= render 'placeholders/table' %>
7 |
8 |
9 |
10 | <%= render 'placeholders/show' %>
11 |
12 |
13 |
14 | <%= render 'placeholders/form' %>
15 |
16 |
--------------------------------------------------------------------------------
/app/views/placeholders/form.erb:
--------------------------------------------------------------------------------
1 | <%= render partial: 'form' %>
2 |
--------------------------------------------------------------------------------
/app/views/placeholders/index.erb:
--------------------------------------------------------------------------------
1 | <%= render partial: 'index' %>
2 |
--------------------------------------------------------------------------------
/app/views/placeholders/show.erb:
--------------------------------------------------------------------------------
1 | <%= render partial: 'show' %>
2 |
3 |
--------------------------------------------------------------------------------
/app/views/placeholders/table.erb:
--------------------------------------------------------------------------------
1 | <%= render partial: 'table' %>
2 |
--------------------------------------------------------------------------------
/app/views/projects/_form.erb:
--------------------------------------------------------------------------------
1 | <%= form_for @project, html: {
2 | 'up-submit': true,
3 | 'up-validate': true,
4 | 'up-disable': true,
5 | 'up-preview': 'btn-spinner',
6 | } do |form| %>
7 |
8 |
39 |
40 |
74 |
75 |
85 |
86 |
87 |
88 | <%= form.button "Save", class: 'btn btn-primary' %>
89 |
90 | <% if @project.new_record? %>
91 | <%= tour_dot(overlay_only: true) do %>
92 |
Saving an invalid form will re-render validation errors in the same overlay.
93 |
Links and forms always render in the same layer unless another layer is targeted explicitly.
94 | <% end %>
95 | <% end %>
96 |
97 |
98 |
99 | <% if local_assigns[:cancel_path] %>
100 | <%= link_to 'Cancel', cancel_path,
101 | class: 'btn btn-outline-secondary',
102 | 'up-follow': true,
103 | 'up-preview': 'main-spinner'
104 | %>
105 | <% end %>
106 |
107 |
108 |
109 | <% end %>
110 |
--------------------------------------------------------------------------------
/app/views/projects/edit.erb:
--------------------------------------------------------------------------------
1 | Edit project <%= @project.id %>
2 |
3 | <%= render 'form', cancel_path: @project %>
4 |
--------------------------------------------------------------------------------
/app/views/projects/index.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
All projects
4 |
5 |
6 |
7 | <%= tour_dot(position: 'left') do %>
8 |
9 | The New project button has an [up-layer=new] attribute.
10 | This causes the link destination to open in an overlay.
11 |
12 |
13 | The button also defines a close condition using an [up-accept-location] attribute.
14 | This causes the overlay to automatically close when it reaches the URL of a newly created project.
15 |
16 |
17 | The [up-on-accepted] attribute causes the project list below to reload when the overlay closes.
18 |
19 | <% end %>
20 |
21 | <%= link_to 'New project', new_project_path, class: 'btn btn-primary',
22 | 'up-placeholder': '#form-placeholder',
23 | 'up-layer': 'new',
24 | 'up-accept-location': project_path('$id'),
25 | 'up-on-accepted': "up.reload('#projects', { placeholder: '#table-placeholder' })"
26 | %>
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 | Name
35 | Company
36 |
37 |
38 |
39 |
40 | <% @projects.each do |project| %>
41 |
42 |
43 | <%= link_to project.name, project,
44 | 'up-layer': 'new',
45 | 'up-accept-event': 'project:destroyed project:updated',
46 | 'up-on-accepted': "up.reload('#projects', { placeholder: '#table-placeholder' })",
47 | 'up-placeholder': '#show-placeholder'
48 | %>
49 |
50 |
51 | <%= project.company.name %>
52 |
53 |
54 | <% end %>
55 |
56 |
57 |
58 |
--------------------------------------------------------------------------------
/app/views/projects/new.erb:
--------------------------------------------------------------------------------
1 |
2 | New project
3 |
4 | <% if up.context[:from_company] %>
5 | <%= tour_dot do %>
6 |
7 | This overlay reuses the form template that is also used for the
8 | <%= link_to 'Projects', projects_path, 'up-layer': 'root' %> section
9 | in the main navigation.
10 |
11 | <% end %>
12 | <% end %>
13 |
14 |
15 | <%= render 'form' %>
16 |
--------------------------------------------------------------------------------
/app/views/projects/show.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Project #<%= @project.id %>
5 |
6 | <% if up.context[:from_company] %>
7 | <%= tour_dot do %>
8 |
9 | This overlay reuses the template that is also used for the
10 | <%= link_to 'Projects', projects_path, 'up-layer': 'root' %> section
11 | in the main navigation.
12 |
13 | <% end %>
14 | <% end %>
15 |
16 |
17 |
18 |
19 | <%= tour_dot(overlay_only: true) do %>
20 |
Note how the Edit link renders within this overlay.
21 |
Links and forms always update their own layer unless another layer is targeted explicitly.
22 | <% end %>
23 |
24 | <%= link_to 'Edit', [:edit, @project],
25 | class: 'btn btn-primary',
26 | 'up-follow': true,
27 | 'up-preview': 'main-spinner'
28 | %>
29 |
30 | <%= link_to 'Delete', @project, method: :delete,
31 | class: 'btn btn-danger mr-3',
32 | 'up-follow': true,
33 | 'up-preview': 'btn-spinner',
34 | 'up-confirm': 'Really delete?'
35 | %>
36 |
37 |
38 |
39 |
40 | Name
41 | <%= @project.name %>
42 |
43 | Company
44 |
45 | <%= link_to @project.company.name, @project.company, 'up-layer': 'swap', 'up-dismiss-location': projects_path %>
46 | <%= tour_dot do %>
47 | This link uses an [up-layer=swap] attribute to replace this overlay instead of stacking on top of it.
48 | <% end %>
49 |
50 |
51 | Budget
52 | <%= number_to_currency(@project.budget, unit: '€') %>
53 |
54 |
--------------------------------------------------------------------------------
/app/views/projects/suggest_name.erb:
--------------------------------------------------------------------------------
1 | <% @names.each_with_index do |name, i| %>
2 |
3 | <%= tour_dot do %>
4 |
5 | Each suggestion button has an [up-emit="name:select"] attribute.
6 | When clicked it emits an name:select event on
7 | the popup element.
8 |
9 |
10 | The link that opened this popup is awaiting this event
11 | via its [up-accept-event] attribute. When the element
12 | is observed, the popup is automatically closed and the name copied
13 | into the project form.
14 |
15 | <% end if i == 0 %>
16 |
17 |
21 | <%= name %>
22 |
23 |
24 | <% end %>
25 |
--------------------------------------------------------------------------------
/app/views/tasks/_form.erb:
--------------------------------------------------------------------------------
1 | <% task = local_assigns.fetch(:task) %>
2 | <% target = task.new_record? ? '#tasks' : '.task' %>
3 | <% autofocus = task.new_record? ? '#tasks' : '.task' %>
4 | <% preview = task.new_record? ? 'add-task, btn-spinner' : 'btn-spinner' %>
5 |
6 | <%= form_for task, html: {
7 | class: 'task task-form',
8 | 'autocomplete': 'off',
9 | 'data-id': 'start',
10 | 'up-target': target,
11 | 'up-preview': preview,
12 | } do |form| %>
13 | <%= form.text_field :text, class: 'task-form--input form-control', placeholder: 'What needs to be done?', autofocus: autofocus, required: true %>
14 | <%= form.error_message :text, class: 'task-form--error' %>
15 | <%= form.button 'Save', class: 'task-form--save btn btn-primary btn-sm' %>
16 | <% end %>
17 |
--------------------------------------------------------------------------------
/app/views/tasks/_head.erb:
--------------------------------------------------------------------------------
1 | <% show_tour = action_name == 'index' %>
2 |
3 |
4 |
5 |
6 | <%= title %>
7 | <%= tour_dot do %>
8 |
9 | This screen shows many task boxes with the same selector
10 | (<div class="task">).
11 |
12 |
13 | Note how changing a task will only update a single task box.
14 |
15 | <% end if show_tour %>
16 |
17 |
18 |
19 |
20 |
21 | <%= pluralize(open_task_count, 'task', 'tasks') %> left!
22 |
23 |
24 |
25 | <%= tour_dot(position: 'left') do %>
26 |
27 | The Clear done button has an [up-target=".tasks"] attribute.
28 | Clicking the link will only update the <div class="tasks"> element.
29 |
30 |
31 | Other HTML from the server response is discarded.
32 |
33 | <% end if show_tour %>
34 |
35 | <%= link_to 'Clear done', clear_done_tasks_path, method: :delete,
36 | class: 'btn btn-secondary',
37 | 'up-target': '.tasks',
38 | 'up-preview': 'btn-spinner, clear-tasks'
39 | %>
40 |
41 |
42 |
--------------------------------------------------------------------------------
/app/views/tasks/_task.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 | <% if task.done? %>
4 | <%= link_to '', [:unfinish, task], method: :patch, class: 'task-item--toggle',
5 | 'up-target': '.task',
6 | 'up-preview': 'unfinish-task'
7 | %>
8 | <% else %>
9 | <%= link_to '', [:finish, task], method: :patch, class: 'task-item--toggle',
10 | 'up-target': '.task',
11 | 'up-preview': 'finish-task'
12 | %>
13 | <% end %>
14 |
15 |
16 | <%= task.text %>
17 |
18 | <%= link_to 'Edit', [:edit, task], class: "task-item--edit btn btn-sm btn-secondary",
19 | 'up-preview': 'btn-spinner',
20 | 'up-target': '.task'
21 | %>
22 |
23 |
--------------------------------------------------------------------------------
/app/views/tasks/edit.erb:
--------------------------------------------------------------------------------
1 | <%= render 'tasks/head', title: "Edit task ##{@task.id}" %>
2 |
3 | <%= render 'form', task: @task %>
4 |
--------------------------------------------------------------------------------
/app/views/tasks/index.erb:
--------------------------------------------------------------------------------
1 | <%= render 'tasks/head', title: 'Tasks' %>
2 |
3 |
4 | <%= render 'tasks/form', task: Task.new %>
5 |
6 | <% @tasks.each do |task| %>
7 | <%= render 'tasks/task', task: task %>
8 | <% end %>
9 |
10 |
11 |
17 |
--------------------------------------------------------------------------------
/app/views/tasks/new.erb:
--------------------------------------------------------------------------------
1 | <%= render 'tasks/head', title: 'New task' %>
2 |
3 | <%= render 'form', task: @task %>
4 |
--------------------------------------------------------------------------------
/app/views/tasks/show.erb:
--------------------------------------------------------------------------------
1 | <%= render 'tasks/head', title: "Task ##{@task.id}" %>
2 |
3 | <%= render @task %>
4 |
--------------------------------------------------------------------------------
/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_version
64 | @bundler_version ||=
65 | env_var_version || cli_arg_version ||
66 | lockfile_version
67 | end
68 |
69 | def bundler_requirement
70 | return "#{Gem::Requirement.default}.a" unless bundler_version
71 |
72 | bundler_gem_version = Gem::Version.new(bundler_version)
73 |
74 | requirement = bundler_gem_version.approximate_recommendation
75 |
76 | return requirement unless Gem::Version.new(Gem::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/rails:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | begin
3 | load File.expand_path('../spring', __FILE__)
4 | rescue LoadError => e
5 | raise unless e.message.include?('spring')
6 | end
7 | APP_PATH = File.expand_path('../config/application', __dir__)
8 | require_relative '../config/boot'
9 | require 'rails/commands'
10 |
--------------------------------------------------------------------------------
/bin/rake:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | begin
3 | load File.expand_path('../spring', __FILE__)
4 | rescue LoadError => e
5 | raise unless e.message.include?('spring')
6 | end
7 | require_relative '../config/boot'
8 | require 'rake'
9 | Rake.application.run
10 |
--------------------------------------------------------------------------------
/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 setup or update your development environment automatically.
13 | # This script is idempotent, so that you can run it at anytime and get an expectable outcome.
14 | # Add necessary setup steps to this file.
15 |
16 | puts '== Installing dependencies =='
17 | system! 'gem install bundler --conservative'
18 | system('bundle check') || system!('bundle install')
19 |
20 | # Install JavaScript dependencies
21 | # system('bin/yarn')
22 |
23 | # puts "\n== Copying sample files =="
24 | # unless File.exist?('config/database.yml')
25 | # FileUtils.cp 'config/database.yml.sample', 'config/database.yml'
26 | # end
27 |
28 | puts "\n== Preparing database =="
29 | system! 'bin/rails db:prepare'
30 |
31 | puts "\n== Removing old logs and tempfiles =="
32 | system! 'bin/rails log:clear tmp:clear'
33 |
34 | puts "\n== Restarting application server =="
35 | system! 'bin/rails restart'
36 | end
37 |
--------------------------------------------------------------------------------
/bin/spring:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | # This file loads Spring without using Bundler, in order to be fast.
4 | # It gets overwritten when you run the `spring binstub` command.
5 |
6 | unless defined?(Spring)
7 | require 'rubygems'
8 | require 'bundler'
9 |
10 | lockfile = Bundler::LockfileParser.new(Bundler.default_lockfile.read)
11 | spring = lockfile.specs.detect { |spec| spec.name == 'spring' }
12 | if spring
13 | Gem.use_paths Gem.dir, Bundler.bundle_path.to_s, *Gem.path
14 | gem 'spring', spring.version
15 | require 'spring/binstub'
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/config/application.rb:
--------------------------------------------------------------------------------
1 | require_relative 'boot'
2 |
3 | require 'rails/all'
4 |
5 | # Require the gems listed in Gemfile, including any gems
6 | # you've limited to :test, :development, or :production.
7 | Bundler.require(*Rails.groups)
8 |
9 | module UnpolyDemoLayers
10 | class Application < Rails::Application
11 | # Initialize configuration defaults for originally generated Rails version.
12 | config.load_defaults 6.0
13 |
14 | # Settings in config/environments/* take precedence over those specified here.
15 | # Application configuration can go into files in config/initializers
16 | # -- all .rb files in that directory are automatically loaded after loading
17 | # the framework and any gems in your application.
18 |
19 | config.read_encrypted_secrets = false
20 | config.require_master_key = false
21 | end
22 | end
23 |
--------------------------------------------------------------------------------
/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: async
3 |
4 | test:
5 | adapter: test
6 |
7 | production:
8 | adapter: redis
9 | url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %>
10 | channel_prefix: unpoly_demo_layers_production
11 |
--------------------------------------------------------------------------------
/config/database.yml:
--------------------------------------------------------------------------------
1 | default: &default
2 | adapter: postgresql
3 | encoding: unicode
4 | # For details on connection pooling, see Rails configuration guide
5 | # https://guides.rubyonrails.org/configuring.html#database-pooling
6 | pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
7 |
8 | development:
9 | <<: *default
10 | database: unpoly_demo_development
11 | host: localhost
12 |
13 | test:
14 | <<: *default
15 | database: unpoly_demo_test<%= ENV['TEST_ENV_NUMBER'] %>
16 |
--------------------------------------------------------------------------------
/config/deploy.rb:
--------------------------------------------------------------------------------
1 | # config valid for current version and patch releases of Capistrano
2 | lock "~> 3.16.0"
3 |
4 | set :repo_url, 'git@github.com:unpoly/unpoly-demo.git'
5 | set :application, "unpoly-demo"
6 | set :scm, :git
7 |
8 | # Default value for :log_level is :debug
9 | set :log_level, :info # %i(debug info error), default: :debug
10 |
11 | # Default value for :linked_files is []
12 | set :linked_files, %w(config/database.yml config/secrets.yml)
13 |
14 | # Default value for linked_dirs is []
15 | set :linked_dirs, %w(log public/system tmp/pids)
16 |
17 | # Default value for keep_releases is 5
18 | set :keep_releases, 10
19 |
20 | set :ssh_options, {
21 | forward_agent: true
22 | }
23 |
24 | set :whenever_roles, [:cron]
25 |
26 | # Install new Ruby version if not already installed
27 | after 'deploy:updating', 'opscomplete:ruby:ensure'
28 |
--------------------------------------------------------------------------------
/config/deploy/production.rb:
--------------------------------------------------------------------------------
1 | set :stage, :production
2 |
3 | set :deploy_to, '/var/www/demo.upjs.io/'
4 | set :rails_env, 'production'
5 | set :branch, 'master'
6 |
7 | server 'app01-prod.makandra.makandra.de', user: 'deploy-demo_upjs', roles: %w(app web db) # first is primary
8 | server 'app02-prod.makandra.makandra.de', user: 'deploy-demo_upjs', roles: %w(app web cron)
9 |
--------------------------------------------------------------------------------
/config/environment.rb:
--------------------------------------------------------------------------------
1 | # Load the Rails application.
2 | require_relative 'application'
3 |
4 | # Initialize the Rails application.
5 | Rails.application.initialize!
6 |
--------------------------------------------------------------------------------
/config/environments/development.rb:
--------------------------------------------------------------------------------
1 | Rails.application.configure do
2 | # Settings specified here will take precedence over those in config/application.rb.
3 |
4 | # In the development environment your application's code is reloaded on
5 | # every request. This slows down response time but is perfect for development
6 | # since you don't have to restart the web server when you make code changes.
7 | config.cache_classes = false
8 |
9 | # Do not eager load code on boot.
10 | config.eager_load = false
11 |
12 | # Show full error reports.
13 | config.consider_all_requests_local = true
14 |
15 | # Enable/disable caching. By default caching is disabled.
16 | # Run rails dev:cache to toggle caching.
17 | if Rails.root.join('tmp', 'caching-dev.txt').exist?
18 | config.action_controller.perform_caching = true
19 | config.action_controller.enable_fragment_cache_logging = true
20 |
21 | config.cache_store = :memory_store
22 | config.public_file_server.headers = {
23 | 'Cache-Control' => "public, max-age=#{2.days.to_i}"
24 | }
25 | else
26 | config.action_controller.perform_caching = false
27 |
28 | config.cache_store = :null_store
29 | end
30 |
31 | # Store uploaded files on the local file system (see config/storage.yml for options).
32 | config.active_storage.service = :local
33 |
34 | # Don't care if the mailer can't send.
35 | config.action_mailer.raise_delivery_errors = false
36 |
37 | config.action_mailer.perform_caching = false
38 |
39 | # Print deprecation notices to the Rails logger.
40 | config.active_support.deprecation = :log
41 |
42 | # Raise an error on page load if there are pending migrations.
43 | config.active_record.migration_error = :page_load
44 |
45 | # Highlight code that triggered database queries in logs.
46 | config.active_record.verbose_query_logs = true
47 |
48 | # Debug mode disables concatenation and preprocessing of assets.
49 | # This option may cause significant delays in view rendering with a large
50 | # number of complex assets.
51 | config.assets.debug = true
52 |
53 | # Suppress logger output for asset requests.
54 | config.assets.quiet = true
55 |
56 | # Raises error for missing translations.
57 | # config.action_view.raise_on_missing_translations = true
58 |
59 | # Use an evented file watcher to asynchronously detect changes in source code,
60 | # routes, locales, etc. This feature depends on the listen gem.
61 | config.file_watcher = ActiveSupport::EventedFileUpdateChecker
62 | end
63 |
--------------------------------------------------------------------------------
/config/environments/production.rb:
--------------------------------------------------------------------------------
1 | Rails.application.configure do
2 | # Settings specified here will take precedence over those in config/application.rb.
3 |
4 | # Code is not reloaded between requests.
5 | config.cache_classes = true
6 |
7 | # Eager load code on boot. This eager loads most of Rails and
8 | # your application in memory, allowing both threaded web servers
9 | # and those relying on copy on write to perform better.
10 | # Rake tasks automatically ignore this option for performance.
11 | config.eager_load = true
12 |
13 | # Full error reports are disabled and caching is turned on.
14 | config.consider_all_requests_local = false
15 | config.action_controller.perform_caching = true
16 |
17 | # Ensures that a master key has been made available in either ENV["RAILS_MASTER_KEY"]
18 | # or in config/master.key. This key is used to decrypt credentials (and other encrypted files).
19 | # config.require_master_key = true
20 |
21 | # Disable serving static files from the `/public` folder by default since
22 | # Apache or NGINX already handles this.
23 | config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present?
24 |
25 | # Compress CSS using a preprocessor.
26 | # config.assets.css_compressor = :sass
27 |
28 | # Do not fallback to assets pipeline if a precompiled asset is missed.
29 | config.assets.compile = false
30 |
31 | # Enable serving of images, stylesheets, and JavaScripts from an asset server.
32 | # config.action_controller.asset_host = 'http://assets.example.com'
33 |
34 | # Specifies the header that your server uses for sending files.
35 | # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache
36 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX
37 |
38 | # Store uploaded files on the local file system (see config/storage.yml for options).
39 | config.active_storage.service = :local
40 |
41 | # Mount Action Cable outside main process or domain.
42 | # config.action_cable.mount_path = nil
43 | # config.action_cable.url = 'wss://example.com/cable'
44 | # config.action_cable.allowed_request_origins = [ 'http://example.com', /http:\/\/example.*/ ]
45 |
46 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
47 | # config.force_ssl = true
48 |
49 | # Use the lowest log level to ensure availability of diagnostic information
50 | # when problems arise.
51 | config.log_level = :debug
52 |
53 | # Prepend all log lines with the following tags.
54 | config.log_tags = [ :request_id ]
55 |
56 | # Use a different cache store in production.
57 | # config.cache_store = :mem_cache_store
58 |
59 | # Use a real queuing backend for Active Job (and separate queues per environment).
60 | # config.active_job.queue_adapter = :resque
61 | # config.active_job.queue_name_prefix = "unpoly_demo_layers_production"
62 |
63 | config.action_mailer.perform_caching = false
64 |
65 | # Ignore bad email addresses and do not raise email delivery errors.
66 | # Set this to true and configure the email server for immediate delivery to raise delivery errors.
67 | # config.action_mailer.raise_delivery_errors = false
68 |
69 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to
70 | # the I18n.default_locale when a translation cannot be found).
71 | config.i18n.fallbacks = true
72 |
73 | # Send deprecation notices to registered listeners.
74 | config.active_support.deprecation = :notify
75 |
76 | # Use default logging formatter so that PID and timestamp are not suppressed.
77 | config.log_formatter = ::Logger::Formatter.new
78 |
79 | # Use a different logger for distributed setups.
80 | # require 'syslog/logger'
81 | # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name')
82 |
83 | if ENV["RAILS_LOG_TO_STDOUT"].present?
84 | logger = ActiveSupport::Logger.new(STDOUT)
85 | logger.formatter = config.log_formatter
86 | config.logger = ActiveSupport::TaggedLogging.new(logger)
87 | end
88 |
89 | # Do not dump schema after migrations.
90 | config.active_record.dump_schema_after_migration = false
91 |
92 | # Inserts middleware to perform automatic connection switching.
93 | # The `database_selector` hash is used to pass options to the DatabaseSelector
94 | # middleware. The `delay` is used to determine how long to wait after a write
95 | # to send a subsequent read to the primary.
96 | #
97 | # The `database_resolver` class is used by the middleware to determine which
98 | # database is appropriate to use based on the time delay.
99 | #
100 | # The `database_resolver_context` class is used by the middleware to set
101 | # timestamps for the last write to the primary. The resolver uses the context
102 | # class timestamps to determine how long to wait before reading from the
103 | # replica.
104 | #
105 | # By default Rails will store a last write timestamp in the session. The
106 | # DatabaseSelector middleware is designed as such you can define your own
107 | # strategy for connection switching and pass that into the middleware through
108 | # these configuration options.
109 | # config.active_record.database_selector = { delay: 2.seconds }
110 | # config.active_record.database_resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver
111 | # config.active_record.database_resolver_context = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session
112 |
113 | Sprockets.register_compressor 'application/javascript', :terser, Terser::Compressor
114 | config.assets.js_compressor = :terser
115 |
116 | end
117 |
--------------------------------------------------------------------------------
/config/environments/test.rb:
--------------------------------------------------------------------------------
1 | # The test environment is used exclusively to run your application's
2 | # test suite. You never need to work with it otherwise. Remember that
3 | # your test database is "scratch space" for the test suite and is wiped
4 | # and recreated between test runs. Don't rely on the data there!
5 |
6 | Rails.application.configure do
7 | # Settings specified here will take precedence over those in config/application.rb.
8 |
9 | config.cache_classes = false
10 | config.action_view.cache_template_loading = true
11 |
12 | # Do not eager load code on boot. This avoids loading your whole application
13 | # just for the purpose of running a single test. If you are using a tool that
14 | # preloads Rails for running tests, you may have to set it to true.
15 | config.eager_load = false
16 |
17 | # Configure public file server for tests with Cache-Control for performance.
18 | config.public_file_server.enabled = true
19 | config.public_file_server.headers = {
20 | 'Cache-Control' => "public, max-age=#{1.hour.to_i}"
21 | }
22 |
23 | # Show full error reports and disable caching.
24 | config.consider_all_requests_local = true
25 | config.action_controller.perform_caching = false
26 | config.cache_store = :null_store
27 |
28 | # Raise exceptions instead of rendering exception templates.
29 | config.action_dispatch.show_exceptions = false
30 |
31 | # Disable request forgery protection in test environment.
32 | config.action_controller.allow_forgery_protection = false
33 |
34 | # Store uploaded files on the local file system in a temporary directory.
35 | config.active_storage.service = :test
36 |
37 | config.action_mailer.perform_caching = false
38 |
39 | # Tell Action Mailer not to deliver emails to the real world.
40 | # The :test delivery method accumulates sent emails in the
41 | # ActionMailer::Base.deliveries array.
42 | config.action_mailer.delivery_method = :test
43 |
44 | # Print deprecation notices to the stderr.
45 | config.active_support.deprecation = :stderr
46 |
47 | # Raises error for missing translations.
48 | # config.action_view.raise_on_missing_translations = true
49 | end
50 |
--------------------------------------------------------------------------------
/config/initializers/application_controller_renderer.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # ActiveSupport::Reloader.to_prepare do
4 | # ApplicationController.renderer.defaults.merge!(
5 | # http_host: 'example.org',
6 | # https: false
7 | # )
8 | # end
9 |
--------------------------------------------------------------------------------
/config/initializers/assets.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Version of your assets, change this if you want to expire all your assets.
4 | Rails.application.config.assets.version = '1.0'
5 |
6 | # Add additional assets to the asset load path.
7 | # Rails.application.config.assets.paths << Emoji.images_path
8 | # Add Yarn node_modules folder to the asset load path.
9 | Rails.application.config.assets.paths << Rails.root.join('node_modules')
10 |
11 | # Precompile additional assets.
12 | # application.js, application.css, and all non-JS/CSS in the app/assets
13 | # folder are already added.
14 | # Rails.application.config.assets.precompile += %w( admin.js admin.css )
15 |
16 | # See https://makandracards.com/makandra/29567-managing-vendor-libraries-with-the-rails-asset-pipeline
17 | Rails.application.config.assets.paths += Dir["#{Rails.root}/vendor/asset-libs/*"].sort_by { |dir| -dir.size }
18 |
--------------------------------------------------------------------------------
/config/initializers/backtrace_silencers.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces.
4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ }
5 |
6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code.
7 | # Rails.backtrace_cleaner.remove_silencers!
8 |
--------------------------------------------------------------------------------
/config/initializers/content_security_policy.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Define an application-wide content security policy
4 | # For further information see the following documentation
5 | # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy
6 |
7 | # Rails.application.config.content_security_policy do |policy|
8 | # policy.default_src :self, :https
9 | # policy.font_src :self, :https, :data
10 | # policy.img_src :self, :https, :data
11 | # policy.object_src :none
12 | # policy.script_src :self, :https
13 | # policy.style_src :self, :https
14 | # # If you are using webpack-dev-server then specify webpack-dev-server host
15 | # policy.connect_src :self, :https, "http://localhost:3035", "ws://localhost:3035" if Rails.env.development?
16 |
17 | # # Specify URI for violation reports
18 | # # policy.report_uri "/csp-violation-report-endpoint"
19 | # end
20 |
21 | # If you are using UJS then enable automatic nonce generation
22 | # Rails.application.config.content_security_policy_nonce_generator = -> request { SecureRandom.base64(16) }
23 |
24 | # Set the nonce only to specific directives
25 | # Rails.application.config.content_security_policy_nonce_directives = %w(script-src)
26 |
27 | # Report CSP violations to a specified URI
28 | # For further information see the following documentation:
29 | # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only
30 | # Rails.application.config.content_security_policy_report_only = true
31 |
--------------------------------------------------------------------------------
/config/initializers/cookies_serializer.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Specify a serializer for the signed and encrypted cookie jars.
4 | # Valid options are :json, :marshal, and :hybrid.
5 | Rails.application.config.action_dispatch.cookies_serializer = :json
6 |
--------------------------------------------------------------------------------
/config/initializers/ext.rb:
--------------------------------------------------------------------------------
1 | Dir.glob(Rails.root.join('lib/ext/**/*.rb')).sort.each do |filename|
2 | require filename
3 | end
4 |
--------------------------------------------------------------------------------
/config/initializers/filter_parameter_logging.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Configure sensitive parameters which will be filtered from the log file.
4 | Rails.application.config.filter_parameters += [:password]
5 |
--------------------------------------------------------------------------------
/config/initializers/inflections.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Add new inflection rules using the following format. Inflections
4 | # are locale specific, and you may define rules for as many different
5 | # locales as you wish. All of these examples are active by default:
6 | # ActiveSupport::Inflector.inflections(:en) do |inflect|
7 | # inflect.plural /^(ox)$/i, '\1en'
8 | # inflect.singular /^(ox)en/i, '\1'
9 | # inflect.irregular 'person', 'people'
10 | # inflect.uncountable %w( fish sheep )
11 | # end
12 |
13 | # These inflection rules are supported but not enabled by default:
14 | # ActiveSupport::Inflector.inflections(:en) do |inflect|
15 | # inflect.acronym 'RESTful'
16 | # end
17 |
--------------------------------------------------------------------------------
/config/initializers/mime_types.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Add new mime types for use in respond_to blocks:
4 | # Mime::Type.register "text/richtext", :rtf
5 |
--------------------------------------------------------------------------------
/config/initializers/wrap_parameters.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # This file contains settings for ActionController::ParamsWrapper which
4 | # is enabled by default.
5 |
6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array.
7 | ActiveSupport.on_load(:action_controller) do
8 | wrap_parameters format: [:json]
9 | end
10 |
11 | # To enable root element in JSON for ActiveRecord objects.
12 | # ActiveSupport.on_load(:active_record) do
13 | # self.include_root_in_json = true
14 | # end
15 |
--------------------------------------------------------------------------------
/config/locales/en.yml:
--------------------------------------------------------------------------------
1 | # Files in the config/locales directory are used for internationalization
2 | # and are automatically loaded by Rails. If you want to use locales other
3 | # than English, add the necessary files in this directory.
4 | #
5 | # To use the locales, use `I18n.t`:
6 | #
7 | # I18n.t 'hello'
8 | #
9 | # In views, this is aliased to just `t`:
10 | #
11 | # <%= t('hello') %>
12 | #
13 | # To use a different locale, set it with `I18n.locale`:
14 | #
15 | # I18n.locale = :es
16 | #
17 | # This would use the information in config/locales/es.yml.
18 | #
19 | # The following keys must be escaped otherwise they will not be retrieved by
20 | # the default I18n backend:
21 | #
22 | # true, false, on, off, yes, no
23 | #
24 | # Instead, surround them with single quotes.
25 | #
26 | # en:
27 | # 'true': 'foo'
28 | #
29 | # To learn more, please read the Rails Internationalization guide
30 | # available at https://guides.rubyonrails.org/i18n.html.
31 |
32 | en:
33 | hello: "Hello world"
34 |
--------------------------------------------------------------------------------
/config/puma.rb:
--------------------------------------------------------------------------------
1 | # Puma can serve each request in a thread from an internal thread pool.
2 | # The `threads` method setting takes two numbers: a minimum and maximum.
3 | # Any libraries that use thread pools should be configured to match
4 | # the maximum value specified for Puma. Default is set to 5 threads for minimum
5 | # and maximum; this matches the default thread size of Active Record.
6 | #
7 | max_threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }
8 | min_threads_count = ENV.fetch("RAILS_MIN_THREADS") { max_threads_count }
9 | threads min_threads_count, max_threads_count
10 |
11 | # Specifies the `port` that Puma will listen on to receive requests; default is 3000.
12 | #
13 | port ENV.fetch("PORT") { 3000 }
14 |
15 | # Specifies the `environment` that Puma will run in.
16 | #
17 | environment ENV.fetch("RAILS_ENV") { "development" }
18 |
19 | # Specifies the `pidfile` that Puma will use.
20 | pidfile ENV.fetch("PIDFILE") { "tmp/pids/server.pid" }
21 |
22 | # Specifies the number of `workers` to boot in clustered mode.
23 | # Workers are forked web server processes. If using threads and workers together
24 | # the concurrency of the application would be max `threads` * `workers`.
25 | # Workers do not work on JRuby or Windows (both of which do not support
26 | # processes).
27 | #
28 | # workers ENV.fetch("WEB_CONCURRENCY") { 2 }
29 |
30 | # Use the `preload_app!` method when specifying a `workers` number.
31 | # This directive tells Puma to first boot the application and load code
32 | # before forking the application. This takes advantage of Copy On Write
33 | # process behavior so workers use less memory.
34 | #
35 | # preload_app!
36 |
37 | # Allow puma to be restarted by `rails restart` command.
38 | plugin :tmp_restart
39 |
--------------------------------------------------------------------------------
/config/routes.rb:
--------------------------------------------------------------------------------
1 | Rails.application.routes.draw do
2 | root to: 'pages#home'
3 |
4 | resources :projects do
5 | collection do
6 | get :suggest_name
7 | end
8 | end
9 |
10 | resources :companies
11 |
12 | resources :tasks do
13 | collection do
14 | delete :clear_done
15 | end
16 | member do
17 | patch :finish
18 | patch :unfinish
19 | patch :move
20 | end
21 | end
22 |
23 | get 'placeholders/index', to: 'placeholders#index'
24 | get 'placeholders/table', to: 'placeholders#table'
25 | get 'placeholders/show', to: 'placeholders#show'
26 | get 'placeholders/form', to: 'placeholders#form'
27 |
28 | get 'verify_tenant', to: 'tenants#verify', as: :verify_tenant
29 | end
30 |
--------------------------------------------------------------------------------
/config/schedule.rb:
--------------------------------------------------------------------------------
1 | # Use this file to define all cron jobs that are installed by Capistrano.
2 | # Learn more: http://github.com/javan/whenever
3 |
4 | every 1.day, at: '03:53 am' do
5 | runner 'Tenant.clean!'
6 | end
7 |
--------------------------------------------------------------------------------
/config/secrets.yml:
--------------------------------------------------------------------------------
1 | development:
2 | secret_key_base: 4e5cae0bc066b3a4e01abf54bf1792d5878873a4a60337c65c95d6e6f4c1f86fe42d15f5bb3576e7387a88a9c4b6d397390af37b9bf20c345a0a944e2bbaf702
3 |
4 | test:
5 | secret_key_base: 4b5da351a7f48d8617f249a7fda00c77836d189161ae934f1ba3fc01fb9b779fe92d22a25cee50e14f0f4b11d73a58dcdea9c9eea4f31636d2a73299e9f8bdf0
6 |
--------------------------------------------------------------------------------
/config/spring.rb:
--------------------------------------------------------------------------------
1 | Spring.watch(
2 | ".ruby-version",
3 | ".rbenv-vars",
4 | "tmp/restart.txt",
5 | "tmp/caching-dev.txt"
6 | )
7 |
--------------------------------------------------------------------------------
/config/storage.yml:
--------------------------------------------------------------------------------
1 | test:
2 | service: Disk
3 | root: <%= Rails.root.join("tmp/storage") %>
4 |
5 | local:
6 | service: Disk
7 | root: <%= Rails.root.join("storage") %>
8 |
9 | # Use rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key)
10 | # amazon:
11 | # service: S3
12 | # access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %>
13 | # secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %>
14 | # region: us-east-1
15 | # bucket: your_own_bucket
16 |
17 | # Remember not to checkin your GCS keyfile to a repository
18 | # google:
19 | # service: GCS
20 | # project: your_project
21 | # credentials: <%= Rails.root.join("path/to/gcs.keyfile") %>
22 | # bucket: your_own_bucket
23 |
24 | # Use rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key)
25 | # microsoft:
26 | # service: AzureStorage
27 | # storage_account_name: your_account_name
28 | # storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %>
29 | # container: your_container_name
30 |
31 | # mirror:
32 | # service: Mirror
33 | # primary: local
34 | # mirrors: [ amazon, google, microsoft ]
35 |
--------------------------------------------------------------------------------
/db/migrate/20200716071558_add_nested_form_test_records.rb:
--------------------------------------------------------------------------------
1 | class AddNestedFormTestRecords < ActiveRecord::Migration[4.2]
2 | def change
3 | create_table :projects do |t|
4 | t.string :name
5 | t.integer :company_id
6 | t.timestamps
7 | end
8 |
9 | create_table :budgets do |t|
10 | t.string :name
11 | t.integer :amount
12 | t.integer :project_id
13 | t.timestamps
14 | end
15 |
16 | create_table :companies do |t|
17 | t.string :name
18 | t.text :address
19 | t.timestamps
20 | end
21 | end
22 | end
23 |
--------------------------------------------------------------------------------
/db/migrate/20201209112615_create_tenants.rb:
--------------------------------------------------------------------------------
1 | class CreateTenants < ActiveRecord::Migration[6.0]
2 | def change
3 | create_table :tenants do |t|
4 | t.bigint :schema_version
5 | t.timestamps
6 | end
7 |
8 | add_column :companies, :tenant_id, :integer
9 | add_column :projects, :tenant_id, :integer
10 | add_column :budgets, :tenant_id, :integer
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/db/migrate/20210107141505_create_tasks.rb:
--------------------------------------------------------------------------------
1 | class CreateTasks < ActiveRecord::Migration[6.0]
2 | def change
3 | create_table :tasks do |t|
4 | t.string :text
5 | t.boolean :done
6 | t.integer :tenant_id
7 | t.timestamps
8 | end
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/db/migrate/20210623061414_drop_budgets.rb:
--------------------------------------------------------------------------------
1 | class DropBudgets < ActiveRecord::Migration[6.0]
2 | def up
3 | drop_table :budgets
4 |
5 | add_column :projects, :budget, :integer
6 | end
7 |
8 | def down
9 | remove_column :projects, :budgets
10 |
11 | create_table "budgets", id: :serial, force: :cascade do |t|
12 | t.string "name"
13 | t.integer "amount"
14 | t.integer "project_id"
15 | t.datetime "created_at"
16 | t.datetime "updated_at"
17 | t.integer "tenant_id"
18 | end
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/db/migrate/20211201123306_index_foreign_keys.rb:
--------------------------------------------------------------------------------
1 | class IndexForeignKeys < ActiveRecord::Migration[6.0]
2 | def change
3 | add_index :companies, [:tenant_id, :name] # companies index
4 | add_index :projects, [:tenant_id, :name] # projects index
5 | add_index :projects, :company_id # company has_many projects
6 | add_index :tasks, [:tenant_id, :created_at] # tasks index
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/db/migrate/20241005194107_add_position_to_tasks.rb:
--------------------------------------------------------------------------------
1 | class AddPositionToTasks < ActiveRecord::Migration[6.1]
2 | def change
3 | add_column :tasks, :position, :integer
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/db/seeds.rb:
--------------------------------------------------------------------------------
1 | # This file should contain all the record creation needed to seed the database with its default values.
2 | # The data can then be loaded with the rails db:seed command (or created alongside the database with db:setup).
3 | #
4 | # Examples:
5 | #
6 | # movies = Movie.create([{ name: 'Star Wars' }, { name: 'Lord of the Rings' }])
7 | # Character.create(name: 'Luke', movie: movies.first)
8 |
--------------------------------------------------------------------------------
/lib/assets/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/lib/ext/action_view/form_builder/error_message.rb:
--------------------------------------------------------------------------------
1 | ActionView::Helpers::FormBuilder.class_eval do
2 |
3 | def error_message(field, **options)
4 | errors = object.errors[field]
5 |
6 | options[:class] = "form-text form-error #{options[:class]}"
7 |
8 | if errors.present?
9 | @template.content_tag(:small, errors.first, **options)
10 | end
11 | end
12 |
13 | end
14 |
--------------------------------------------------------------------------------
/lib/ext/ruby/enumerable/enumerable_natural_sort.rb:
--------------------------------------------------------------------------------
1 | module Enumerable
2 |
3 | def natural_sort
4 | natural_sort_by
5 | end
6 |
7 | def natural_sort_by(&stringifier)
8 | sort_by do |element|
9 | element = stringifier.call(element) if stringifier
10 | element = element.to_s unless element.respond_to?(:to_sort_atoms)
11 | element.to_sort_atoms
12 | end
13 | end
14 |
15 | end
16 |
--------------------------------------------------------------------------------
/lib/ext/ruby/string/string_to_sort_atoms.rb:
--------------------------------------------------------------------------------
1 | class SmartSortAtom
2 |
3 | attr_reader :value
4 |
5 | def initialize(value)
6 | @value = value
7 | end
8 |
9 | def <=>(other)
10 | other.is_a?(self.class) or raise "Can only smart compare with other SmartSortAtom"
11 | left_value = value
12 | right_value = other.value
13 | if left_value.class == right_value.class
14 | left_value <=> right_value
15 | elsif left_value.is_a?(Float)
16 | -1
17 | else
18 | 1
19 | end
20 | end
21 |
22 | def self.parse(string)
23 | # Loosely based on http://stackoverflow.com/a/4079031
24 | string.scan(/[^\d\.]+|[\d\.]+/).collect do |atom|
25 | if atom.match(/\d+(\.\d+)?/)
26 | atom = atom.to_f
27 | else
28 | atom = normalize_string(atom)
29 | end
30 | new(atom)
31 | end
32 |
33 | end
34 |
35 | private
36 |
37 | def self.normalize_string(string)
38 | string = ActiveSupport::Inflector.transliterate(string)
39 | string = string.downcase
40 | string
41 | end
42 |
43 | end
44 |
45 | String.class_eval do
46 |
47 | def to_sort_atoms
48 | SmartSortAtom.parse(self)
49 | end
50 |
51 | end
52 |
53 |
--------------------------------------------------------------------------------
/lib/tasks/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/lib/tasks/db.rake:
--------------------------------------------------------------------------------
1 | namespace :db do
2 | desc 'Deletes all rows in all tables'
3 | task :clean => :environment do
4 | DatabaseCleaner.clean_with(:deletion)
5 | end
6 | end
--------------------------------------------------------------------------------
/log/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/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:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/apple-touch-icon.png:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
2 |
--------------------------------------------------------------------------------
/spec/spec_helper.rb:
--------------------------------------------------------------------------------
1 | ENV['RAILS_ENV'] ||= 'test'
2 | require File.expand_path('../config/environment', __dir__)
3 | # Prevent database truncation if the environment is production
4 | abort("The Rails environment is running in production mode!") if Rails.env.production?
5 | require 'rspec/rails'
6 |
7 | Dir[Rails.root.join('spec', 'support', '**', '*.rb')].sort.each { |f| require f }
8 |
9 | # Add additional requires below this line. Rails is not loaded until this point!
10 |
11 | # Requires supporting ruby files with custom matchers and macros, etc, in
12 | # spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are
13 | # run as spec files by default. This means that files in spec/support that end
14 | # in _spec.rb will both be required and run as specs, causing the specs to be
15 | # run twice. It is recommended that you do not name files matching this glob to
16 | # end with _spec.rb. You can configure this pattern with the --pattern
17 | # option on the command line or in ~/.rspec, .rspec or `.rspec-local`.
18 | #
19 | # The following line is provided for convenience purposes. It has the downside
20 | # of increasing the boot-up time by auto-requiring all files in the support
21 | # directory. Alternatively, in the individual `*_spec.rb` files, manually
22 | # require only the support files necessary.
23 | #
24 | # Dir[Rails.root.join('spec', 'support', '**', '*.rb')].sort.each { |f| require f }
25 |
26 | # Checks for pending migrations and applies them before tests are run.
27 | # If you are not using ActiveRecord, you can remove these lines.
28 | begin
29 | ActiveRecord::Migration.maintain_test_schema!
30 | rescue ActiveRecord::PendingMigrationError => e
31 | puts e.to_s.strip
32 | exit 1
33 | end
34 | RSpec.configure do |config|
35 |
36 | # RSpec Rails can automatically mix in different behaviours to your tests
37 | # based on their file location, for example enabling you to call `get` and
38 | # `post` in specs under `spec/controllers`.
39 | #
40 | # You can disable this behaviour by removing the line below, and instead
41 | # explicitly tag your specs with their type, e.g.:
42 | #
43 | # RSpec.describe UsersController, type: :controller do
44 | # # ...
45 | # end
46 | #
47 | # The different available types are documented in the features, such as in
48 | # https://relishapp.com/rspec/rspec-rails/docs
49 | config.infer_spec_type_from_file_location!
50 |
51 | config.expose_dsl_globally = true
52 |
53 | # rspec-expectations config goes here. You can use an alternate
54 | # assertion/expectation library such as wrong or the stdlib/minitest
55 | # assertions if you prefer.
56 | config.expect_with :rspec do |expectations|
57 | # This option will default to `true` in RSpec 4. It makes the `description`
58 | # and `failure_message` of custom matchers include text for helper methods
59 | # defined using `chain`, e.g.:
60 | # be_bigger_than(2).and_smaller_than(4).description
61 | # # => "be bigger than 2 and smaller than 4"
62 | # ...rather than:
63 | # # => "be bigger than 2"
64 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true
65 | end
66 |
67 | # rspec-mocks config goes here. You can use an alternate test double
68 | # library (such as bogus or mocha) by changing the `mock_with` option here.
69 | config.mock_with :rspec do |mocks|
70 | # Prevents you from mocking or stubbing a method that does not exist on
71 | # a real object. This is generally recommended, and will default to
72 | # `true` in RSpec 4.
73 | mocks.verify_partial_doubles = true
74 | end
75 |
76 | # This option will default to `:apply_to_host_groups` in RSpec 4 (and will
77 | # have no way to turn it off -- the option exists only for backwards
78 | # compatibility in RSpec 3). It causes shared context metadata to be
79 | # inherited by the metadata hash of host groups and examples, rather than
80 | # triggering implicit auto-inclusion in groups with matching metadata.
81 | config.shared_context_metadata_behavior = :apply_to_host_groups
82 |
83 | # The settings below are suggested to provide a good initial experience
84 | # with RSpec, but feel free to customize to your heart's content.
85 | =begin
86 | # This allows you to limit a spec run to individual examples or groups
87 | # you care about by tagging them with `:focus` metadata. When nothing
88 | # is tagged with `:focus`, all examples get run. RSpec also provides
89 | # aliases for `it`, `describe`, and `context` that include `:focus`
90 | # metadata: `fit`, `fdescribe` and `fcontext`, respectively.
91 | config.filter_run_when_matching :focus
92 |
93 | # Allows RSpec to persist some state between runs in order to support
94 | # the `--only-failures` and `--next-failure` CLI options. We recommend
95 | # you configure your source control system to ignore this file.
96 | config.example_status_persistence_file_path = "spec/examples.txt"
97 |
98 | # Limits the available syntax to the non-monkey patched syntax that is
99 | # recommended. For more details, see:
100 | # - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/
101 | # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/
102 | # - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode
103 | config.disable_monkey_patching!
104 |
105 | # Many RSpec users commonly either run the entire suite or an individual
106 | # file, and it's useful to allow more verbose output when running an
107 | # individual spec file.
108 | if config.files_to_run.one?
109 | # Use the documentation formatter for detailed output,
110 | # unless a formatter has already been configured
111 | # (e.g. via a command-line flag).
112 | config.default_formatter = "doc"
113 | end
114 |
115 | # Print the 10 slowest examples and example groups at the
116 | # end of the spec run, to help surface which specs are running
117 | # particularly slow.
118 | config.profile_examples = 10
119 |
120 | # Run specs in random order to surface order dependencies. If you find an
121 | # order dependency and want to debug it, you can fix the order by providing
122 | # the seed, which is printed after each run.
123 | # --seed 1234
124 | config.order = :random
125 |
126 | # Seed global randomization in this process using the `--seed` CLI option.
127 | # Setting this allows you to use `--seed` to deterministically reproduce
128 | # test failures related to randomization by passing the same `--seed` value
129 | # as the one that triggered the failure.
130 | Kernel.srand config.seed
131 | =end
132 |
133 | # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures
134 | config.fixture_path = "#{::Rails.root}/spec/fixtures"
135 |
136 | # If you're not using ActiveRecord, or you'd prefer not to run each of your
137 | # examples within a transaction, remove the following line or assign false
138 | # instead of true.
139 | config.use_transactional_fixtures = true
140 |
141 | # You can uncomment this line to turn off ActiveRecord support entirely.
142 | # config.use_active_record = false
143 |
144 | # Filter lines from Rails gems in backtraces.
145 | config.filter_rails_from_backtrace!
146 | # arbitrary gems may also be filtered via:
147 | # config.filter_gems_from_backtrace("gem name")
148 | #
149 | #
150 | end
151 |
--------------------------------------------------------------------------------
/spec/support/capybara.rb:
--------------------------------------------------------------------------------
1 | RSpec.configure do |config|
2 | config.before(:each, type: :system) do
3 | driven_by :rack_test
4 | end
5 |
6 | config.before(:each, type: :system, js: true) do
7 | driven_by :selenium_chrome_headless, screen_size: [1280, 960]
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/spec/system/home_spec.rb:
--------------------------------------------------------------------------------
1 | describe "Home page" do
2 | scenario "User opens the home page and sees instructions", js: true do
3 | visit root_path
4 | expect(page).to have_text("Unpoly Demo")
5 | expect(page).to have_text("Click on everything!")
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/storage/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/application_system_test_case.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 |
3 | class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
4 | driven_by :selenium, using: :chrome, screen_size: [1400, 1400]
5 | end
6 |
--------------------------------------------------------------------------------
/test/channels/application_cable/connection_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 |
3 | class ApplicationCable::ConnectionTest < ActionCable::Connection::TestCase
4 | # test "connects with cookies" do
5 | # cookies.signed[:user_id] = 42
6 | #
7 | # connect
8 | #
9 | # assert_equal connection.user_id, "42"
10 | # end
11 | end
12 |
--------------------------------------------------------------------------------
/test/controllers/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/fixtures/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/fixtures/files/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/helpers/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/integration/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/mailers/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/models/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/system/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/test_helper.rb:
--------------------------------------------------------------------------------
1 | ENV['RAILS_ENV'] ||= 'test'
2 | require_relative '../config/environment'
3 | require 'rails/test_help'
4 |
5 | class ActiveSupport::TestCase
6 | # Run tests in parallel with specified workers
7 | parallelize(workers: :number_of_processors)
8 |
9 | # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order.
10 | fixtures :all
11 |
12 | # Add more helper methods to be used by all tests here...
13 | end
14 |
--------------------------------------------------------------------------------
/vendor/asset-libs/.keep:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/vendor/asset-libs/roboto-mono/roboto-mono-v22-latin-regular.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/unpoly/unpoly-demo/HEAD/vendor/asset-libs/roboto-mono/roboto-mono-v22-latin-regular.eot
--------------------------------------------------------------------------------
/vendor/asset-libs/roboto-mono/roboto-mono-v22-latin-regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/unpoly/unpoly-demo/HEAD/vendor/asset-libs/roboto-mono/roboto-mono-v22-latin-regular.ttf
--------------------------------------------------------------------------------
/vendor/asset-libs/roboto-mono/roboto-mono-v22-latin-regular.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/unpoly/unpoly-demo/HEAD/vendor/asset-libs/roboto-mono/roboto-mono-v22-latin-regular.woff
--------------------------------------------------------------------------------
/vendor/asset-libs/roboto-mono/roboto-mono-v22-latin-regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/unpoly/unpoly-demo/HEAD/vendor/asset-libs/roboto-mono/roboto-mono-v22-latin-regular.woff2
--------------------------------------------------------------------------------
/vendor/asset-libs/roboto-mono/roboto-mono.scss:
--------------------------------------------------------------------------------
1 | /* roboto-mono-regular - latin */
2 | @font-face {
3 | font-family: 'Roboto Mono';
4 | font-style: normal;
5 | font-weight: 400;
6 | src: asset-url('roboto-mono-v22-latin-regular.eot'); /* IE9 Compat Modes */
7 | src: local(''),
8 | asset-url('roboto-mono-v22-latin-regular.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
9 | asset-url('roboto-mono-v22-latin-regular.woff2') format('woff2'), /* Super Modern Browsers */
10 | asset-url('roboto-mono-v22-latin-regular.woff') format('woff'), /* Modern Browsers */
11 | asset-url('roboto-mono-v22-latin-regular.ttf') format('truetype'), /* Safari, Android, iOS */
12 | asset-url('roboto-mono-v22-latin-regular.svg#RobotoMono') format('svg'); /* Legacy iOS */
13 | }
14 |
--------------------------------------------------------------------------------
/vendor/asset-libs/roboto/roboto-v30-latin-500.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/unpoly/unpoly-demo/HEAD/vendor/asset-libs/roboto/roboto-v30-latin-500.eot
--------------------------------------------------------------------------------
/vendor/asset-libs/roboto/roboto-v30-latin-500.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
12 |
13 |
14 |
15 |
16 |
18 |
21 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
34 |
35 |
36 |
38 |
39 |
41 |
43 |
44 |
46 |
48 |
49 |
50 |
51 |
52 |
53 |
55 |
58 |
59 |
61 |
63 |
64 |
65 |
66 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
77 |
78 |
80 |
81 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
98 |
100 |
102 |
103 |
105 |
106 |
108 |
109 |
110 |
111 |
112 |
113 |
115 |
116 |
118 |
120 |
121 |
122 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
139 |
141 |
143 |
144 |
145 |
148 |
149 |
152 |
154 |
155 |
156 |
157 |
160 |
161 |
162 |
163 |
164 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
174 |
175 |
176 |
178 |
181 |
183 |
184 |
185 |
186 |
188 |
190 |
192 |
193 |
195 |
196 |
197 |
198 |
200 |
201 |
202 |
203 |
204 |
205 |
207 |
209 |
211 |
213 |
216 |
219 |
220 |
222 |
223 |
224 |
225 |
227 |
228 |
229 |
231 |
233 |
235 |
237 |
240 |
243 |
246 |
249 |
251 |
253 |
255 |
257 |
259 |
260 |
261 |
262 |
263 |
265 |
267 |
269 |
271 |
273 |
276 |
278 |
280 |
282 |
283 |
284 |
285 |
287 |
288 |
290 |
292 |
293 |
294 |
295 |
296 |
297 |
298 |
299 |
300 |
301 |
302 |
303 |
304 |
305 |
306 |
--------------------------------------------------------------------------------
/vendor/asset-libs/roboto/roboto-v30-latin-500.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/unpoly/unpoly-demo/HEAD/vendor/asset-libs/roboto/roboto-v30-latin-500.ttf
--------------------------------------------------------------------------------
/vendor/asset-libs/roboto/roboto-v30-latin-500.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/unpoly/unpoly-demo/HEAD/vendor/asset-libs/roboto/roboto-v30-latin-500.woff
--------------------------------------------------------------------------------
/vendor/asset-libs/roboto/roboto-v30-latin-500.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/unpoly/unpoly-demo/HEAD/vendor/asset-libs/roboto/roboto-v30-latin-500.woff2
--------------------------------------------------------------------------------
/vendor/asset-libs/roboto/roboto-v30-latin-regular.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/unpoly/unpoly-demo/HEAD/vendor/asset-libs/roboto/roboto-v30-latin-regular.eot
--------------------------------------------------------------------------------
/vendor/asset-libs/roboto/roboto-v30-latin-regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/unpoly/unpoly-demo/HEAD/vendor/asset-libs/roboto/roboto-v30-latin-regular.ttf
--------------------------------------------------------------------------------
/vendor/asset-libs/roboto/roboto-v30-latin-regular.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/unpoly/unpoly-demo/HEAD/vendor/asset-libs/roboto/roboto-v30-latin-regular.woff
--------------------------------------------------------------------------------
/vendor/asset-libs/roboto/roboto-v30-latin-regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/unpoly/unpoly-demo/HEAD/vendor/asset-libs/roboto/roboto-v30-latin-regular.woff2
--------------------------------------------------------------------------------
/vendor/asset-libs/roboto/roboto.scss:
--------------------------------------------------------------------------------
1 | /* roboto-regular - latin */
2 | @font-face {
3 | font-family: 'Roboto';
4 | font-style: normal;
5 | font-weight: 400;
6 | src: asset-url('roboto-v30-latin-regular.eot'); /* IE9 Compat Modes */
7 | src: local(''),
8 | asset-url('roboto-v30-latin-regular.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
9 | asset-url('roboto-v30-latin-regular.woff2') format('woff2'), /* Super Modern Browsers */
10 | asset-url('roboto-v30-latin-regular.woff') format('woff'), /* Modern Browsers */
11 | asset-url('roboto-v30-latin-regular.ttf') format('truetype'), /* Safari, Android, iOS */
12 | asset-url('roboto-v30-latin-regular.svg#Roboto') format('svg'); /* Legacy iOS */
13 | }
14 | /* roboto-500 - latin */
15 | @font-face {
16 | font-family: 'Roboto';
17 | font-style: normal;
18 | font-weight: 500;
19 | src: asset-url('roboto-v30-latin-500.eot'); /* IE9 Compat Modes */
20 | src: local(''),
21 | asset-url('roboto-v30-latin-500.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
22 | asset-url('roboto-v30-latin-500.woff2') format('woff2'), /* Super Modern Browsers */
23 | asset-url('roboto-v30-latin-500.woff') format('woff'), /* Modern Browsers */
24 | asset-url('roboto-v30-latin-500.ttf') format('truetype'), /* Safari, Android, iOS */
25 | asset-url('roboto-v30-latin-500.svg#Roboto') format('svg'); /* Legacy iOS */
26 | }
27 |
--------------------------------------------------------------------------------