├── src
├── templates
│ ├── css-js
│ │ ├── workspace
│ │ │ ├── .keep
│ │ │ └── store
│ │ │ │ └── app
│ │ │ │ ├── views
│ │ │ │ └── products
│ │ │ │ │ └── show.html.erb
│ │ │ │ └── assets
│ │ │ │ └── stylesheets
│ │ │ │ └── application.css
│ │ └── .tk-config.json
│ ├── default
│ │ ├── pgdata
│ │ │ └── .keep
│ │ ├── workspace
│ │ │ └── .keep
│ │ ├── lib
│ │ │ ├── constants.js
│ │ │ ├── patches
│ │ │ │ └── authentication.rb
│ │ │ ├── commands.js
│ │ │ └── database.js
│ │ ├── bin
│ │ │ ├── console
│ │ │ ├── rackup
│ │ │ └── ruby
│ │ ├── scripts
│ │ │ └── createdb.js
│ │ └── package.json
│ ├── testing
│ │ ├── workspace
│ │ │ ├── .keep
│ │ │ └── store
│ │ │ │ └── test
│ │ │ │ ├── fixtures
│ │ │ │ ├── products.yml
│ │ │ │ └── subscribers.yml
│ │ │ │ ├── models
│ │ │ │ └── product_test.rb
│ │ │ │ └── mailers
│ │ │ │ └── product_mailer_test.rb
│ │ └── .tk-config.json
│ ├── crud-products
│ │ ├── workspace
│ │ │ ├── .keep
│ │ │ └── store
│ │ │ │ ├── app
│ │ │ │ ├── views
│ │ │ │ │ └── products
│ │ │ │ │ │ ├── edit.html.erb
│ │ │ │ │ │ ├── new.html.erb
│ │ │ │ │ │ ├── _form.html.erb
│ │ │ │ │ │ ├── show.html.erb
│ │ │ │ │ │ └── index.html.erb
│ │ │ │ └── controllers
│ │ │ │ │ └── products_controller.rb
│ │ │ │ └── config
│ │ │ │ └── routes.rb
│ │ └── .tk-config.json
│ ├── rails-new
│ │ └── workspace
│ │ │ └── store
│ │ │ ├── log
│ │ │ └── .keep
│ │ │ ├── script
│ │ │ └── .keep
│ │ │ ├── storage
│ │ │ └── .keep
│ │ │ ├── tmp
│ │ │ ├── .keep
│ │ │ ├── pids
│ │ │ │ └── .keep
│ │ │ └── storage
│ │ │ │ └── .keep
│ │ │ ├── test
│ │ │ ├── helpers
│ │ │ │ └── .keep
│ │ │ ├── controllers
│ │ │ │ └── .keep
│ │ │ ├── integration
│ │ │ │ └── .keep
│ │ │ └── test_helper.rb
│ │ │ ├── app
│ │ │ ├── assets
│ │ │ │ ├── images
│ │ │ │ │ └── .keep
│ │ │ │ └── stylesheets
│ │ │ │ │ └── application.css
│ │ │ ├── models
│ │ │ │ ├── concerns
│ │ │ │ │ └── .keep
│ │ │ │ └── application_record.rb
│ │ │ ├── views
│ │ │ │ ├── layouts
│ │ │ │ │ ├── mailer.text.erb
│ │ │ │ │ ├── mailer.html.erb
│ │ │ │ │ └── application.html.erb
│ │ │ │ └── pwa
│ │ │ │ │ ├── manifest.json.erb
│ │ │ │ │ └── service-worker.js
│ │ │ ├── helpers
│ │ │ │ └── application_helper.rb
│ │ │ ├── mailers
│ │ │ │ └── application_mailer.rb
│ │ │ ├── javascript
│ │ │ │ ├── application.js
│ │ │ │ └── controllers
│ │ │ │ │ ├── application.js
│ │ │ │ │ └── index.js
│ │ │ ├── controllers
│ │ │ │ └── application_controller.rb
│ │ │ └── jobs
│ │ │ │ └── application_job.rb
│ │ │ ├── .ruby-version
│ │ │ ├── vendor
│ │ │ └── javascripts
│ │ │ │ └── .keep
│ │ │ ├── config
│ │ │ ├── master.key
│ │ │ ├── routes.rb
│ │ │ ├── boot.rb
│ │ │ ├── environment.rb
│ │ │ ├── cable.yml
│ │ │ ├── importmap.rb
│ │ │ ├── initializers
│ │ │ │ ├── assets.rb
│ │ │ │ ├── filter_parameter_logging.rb
│ │ │ │ ├── inflections.rb
│ │ │ │ └── content_security_policy.rb
│ │ │ ├── credentials.yml.enc
│ │ │ ├── database.yml
│ │ │ ├── locales
│ │ │ │ └── en.yml
│ │ │ ├── application.rb
│ │ │ └── storage.yml
│ │ │ ├── bin
│ │ │ ├── importmap
│ │ │ └── rails
│ │ │ ├── public
│ │ │ ├── robots.txt
│ │ │ ├── icon.png
│ │ │ └── icon.svg
│ │ │ ├── config.ru
│ │ │ ├── Rakefile
│ │ │ ├── README.md
│ │ │ └── db
│ │ │ └── seeds.rb
│ ├── stock-notifications
│ │ ├── workspace
│ │ │ ├── .keep
│ │ │ └── store
│ │ │ │ ├── app
│ │ │ │ ├── models
│ │ │ │ │ ├── subscriber.rb
│ │ │ │ │ ├── product.rb
│ │ │ │ │ └── product
│ │ │ │ │ │ └── notifications.rb
│ │ │ │ ├── views
│ │ │ │ │ ├── product_mailer
│ │ │ │ │ │ ├── in_stock.text.erb
│ │ │ │ │ │ └── in_stock.html.erb
│ │ │ │ │ └── products
│ │ │ │ │ │ ├── _inventory.html.erb
│ │ │ │ │ │ ├── show.html.erb
│ │ │ │ │ │ └── _form.html.erb
│ │ │ │ ├── mailers
│ │ │ │ │ └── product_mailer.rb
│ │ │ │ └── controllers
│ │ │ │ │ ├── unsubscribes_controller.rb
│ │ │ │ │ ├── subscribers_controller.rb
│ │ │ │ │ └── products_controller.rb
│ │ │ │ ├── test
│ │ │ │ ├── models
│ │ │ │ │ └── subscriber_test.rb
│ │ │ │ ├── fixtures
│ │ │ │ │ └── subscribers.yml
│ │ │ │ └── mailers
│ │ │ │ │ └── product_mailer_test.rb
│ │ │ │ ├── db
│ │ │ │ ├── migrate
│ │ │ │ │ ├── 20250620213158_add_inventory_count_to_products.rb
│ │ │ │ │ └── 20250620214344_create_subscribers.rb
│ │ │ │ └── seeds.rb
│ │ │ │ └── config
│ │ │ │ └── routes.rb
│ │ └── .tk-config.json
│ ├── authentication
│ │ ├── workspace
│ │ │ └── store
│ │ │ │ ├── test
│ │ │ │ ├── mailers
│ │ │ │ │ ├── .keep
│ │ │ │ │ └── previews
│ │ │ │ │ │ └── passwords_mailer_preview.rb
│ │ │ │ ├── models
│ │ │ │ │ ├── .keep
│ │ │ │ │ └── user_test.rb
│ │ │ │ └── fixtures
│ │ │ │ │ ├── files
│ │ │ │ │ └── .keep
│ │ │ │ │ └── users.yml
│ │ │ │ ├── app
│ │ │ │ ├── controllers
│ │ │ │ │ ├── concerns
│ │ │ │ │ │ └── .keep
│ │ │ │ │ ├── application_controller.rb
│ │ │ │ │ ├── sessions_controller.rb
│ │ │ │ │ ├── products_controller.rb
│ │ │ │ │ └── passwords_controller.rb
│ │ │ │ ├── models
│ │ │ │ │ ├── session.rb
│ │ │ │ │ ├── current.rb
│ │ │ │ │ └── user.rb
│ │ │ │ ├── views
│ │ │ │ │ ├── passwords_mailer
│ │ │ │ │ │ ├── reset.text.erb
│ │ │ │ │ │ └── reset.html.erb
│ │ │ │ │ ├── products
│ │ │ │ │ │ ├── show.html.erb
│ │ │ │ │ │ └── index.html.erb
│ │ │ │ │ ├── passwords
│ │ │ │ │ │ ├── new.html.erb
│ │ │ │ │ │ └── edit.html.erb
│ │ │ │ │ └── sessions
│ │ │ │ │ │ └── new.html.erb
│ │ │ │ ├── mailers
│ │ │ │ │ └── passwords_mailer.rb
│ │ │ │ └── channels
│ │ │ │ │ └── application_cable
│ │ │ │ │ └── connection.rb
│ │ │ │ ├── tmp
│ │ │ │ └── authenticated-user.txt
│ │ │ │ ├── config
│ │ │ │ └── routes.rb
│ │ │ │ └── db
│ │ │ │ ├── migrate
│ │ │ │ ├── 20250618233539_create_sessions.rb
│ │ │ │ └── 20250618233538_create_users.rb
│ │ │ │ └── seeds.rb
│ │ └── .tk-config.json
│ ├── active-storage
│ │ ├── .tk-config.json
│ │ └── workspace
│ │ │ └── store
│ │ │ ├── app
│ │ │ ├── views
│ │ │ │ ├── layouts
│ │ │ │ │ └── action_text
│ │ │ │ │ │ └── contents
│ │ │ │ │ │ └── _content.html.erb
│ │ │ │ ├── products
│ │ │ │ │ ├── show.html.erb
│ │ │ │ │ └── _form.html.erb
│ │ │ │ └── active_storage
│ │ │ │ │ └── blobs
│ │ │ │ │ └── _blob.html.erb
│ │ │ ├── assets
│ │ │ │ └── images
│ │ │ │ │ └── logo.png
│ │ │ ├── models
│ │ │ │ └── product.rb
│ │ │ ├── javascript
│ │ │ │ └── application.js
│ │ │ └── controllers
│ │ │ │ └── products_controller.rb
│ │ │ ├── test
│ │ │ └── fixtures
│ │ │ │ └── action_text
│ │ │ │ └── rich_texts.yml
│ │ │ ├── config
│ │ │ └── importmap.rb
│ │ │ └── db
│ │ │ ├── seeds.rb
│ │ │ └── migrate
│ │ │ └── 20250618002538_create_action_text_tables.action_text.rb
│ ├── create-products
│ │ ├── .tk-config.json
│ │ └── workspace
│ │ │ └── store
│ │ │ ├── app
│ │ │ └── models
│ │ │ │ └── product.rb
│ │ │ ├── db
│ │ │ ├── seeds.rb
│ │ │ ├── migrate
│ │ │ │ └── 20250521010850_create_products.rb
│ │ │ └── schema.rb
│ │ │ └── test
│ │ │ ├── models
│ │ │ └── product_test.rb
│ │ │ └── fixtures
│ │ │ └── products.yml
│ └── products-controller
│ │ ├── .tk-config.json
│ │ └── workspace
│ │ └── store
│ │ ├── app
│ │ ├── helpers
│ │ │ └── products_helper.rb
│ │ ├── views
│ │ │ └── products
│ │ │ │ └── index.html.erb
│ │ └── controllers
│ │ │ └── products_controller.rb
│ │ ├── config
│ │ └── routes.rb
│ │ └── test
│ │ └── controllers
│ │ └── products_controller_test.rb
├── content
│ ├── tutorial
│ │ ├── 15-css-and-js
│ │ │ ├── 3-hotwire
│ │ │ │ ├── _files
│ │ │ │ │ ├── workspace
│ │ │ │ │ │ ├── .keep
│ │ │ │ │ │ └── store
│ │ │ │ │ │ │ └── app
│ │ │ │ │ │ │ ├── views
│ │ │ │ │ │ │ └── products
│ │ │ │ │ │ │ │ └── show.html.erb
│ │ │ │ │ │ │ └── assets
│ │ │ │ │ │ │ └── stylesheets
│ │ │ │ │ │ │ └── application.css
│ │ │ │ │ └── .tk-config.json
│ │ │ │ └── content.md
│ │ │ ├── 1-propshaft
│ │ │ │ └── _files
│ │ │ │ │ ├── workspace
│ │ │ │ │ └── .keep
│ │ │ │ │ └── .tk-config.json
│ │ │ ├── 2-importmap
│ │ │ │ └── _files
│ │ │ │ │ ├── workspace
│ │ │ │ │ ├── .keep
│ │ │ │ │ └── store
│ │ │ │ │ │ └── app
│ │ │ │ │ │ ├── views
│ │ │ │ │ │ └── products
│ │ │ │ │ │ │ └── show.html.erb
│ │ │ │ │ │ └── assets
│ │ │ │ │ │ └── stylesheets
│ │ │ │ │ │ └── application.css
│ │ │ │ │ └── .tk-config.json
│ │ │ └── meta.md
│ │ ├── 16-testing
│ │ │ ├── 1-fixtures
│ │ │ │ └── _files
│ │ │ │ │ ├── workspace
│ │ │ │ │ └── .keep
│ │ │ │ │ └── .tk-config.json
│ │ │ ├── 2-testing-emails
│ │ │ │ └── _files
│ │ │ │ │ ├── workspace
│ │ │ │ │ ├── .keep
│ │ │ │ │ └── store
│ │ │ │ │ │ └── test
│ │ │ │ │ │ └── fixtures
│ │ │ │ │ │ ├── products.yml
│ │ │ │ │ │ └── subscribers.yml
│ │ │ │ │ └── .tk-config.json
│ │ │ └── meta.md
│ │ ├── 6-active-record
│ │ │ ├── 1-intro
│ │ │ │ └── _files
│ │ │ │ │ ├── workspace
│ │ │ │ │ ├── .keep
│ │ │ │ │ └── app
│ │ │ │ │ │ └── models
│ │ │ │ │ │ └── product.rb
│ │ │ │ │ └── .tk-config.json
│ │ │ ├── 8-validations
│ │ │ │ └── _files
│ │ │ │ │ ├── workspace
│ │ │ │ │ ├── .keep
│ │ │ │ │ └── app
│ │ │ │ │ │ └── models
│ │ │ │ │ │ └── product.rb
│ │ │ │ │ └── .tk-config.json
│ │ │ ├── 2-creating-records
│ │ │ │ └── _files
│ │ │ │ │ ├── workspace
│ │ │ │ │ ├── .keep
│ │ │ │ │ └── app
│ │ │ │ │ │ └── models
│ │ │ │ │ │ └── product.rb
│ │ │ │ │ └── .tk-config.json
│ │ │ ├── 3-querying-records
│ │ │ │ └── _files
│ │ │ │ │ ├── workspace
│ │ │ │ │ ├── .keep
│ │ │ │ │ └── app
│ │ │ │ │ │ └── models
│ │ │ │ │ │ └── product.rb
│ │ │ │ │ └── .tk-config.json
│ │ │ ├── 5-finding-records
│ │ │ │ ├── _files
│ │ │ │ │ ├── workspace
│ │ │ │ │ │ ├── .keep
│ │ │ │ │ │ └── app
│ │ │ │ │ │ │ └── models
│ │ │ │ │ │ │ └── product.rb
│ │ │ │ │ └── .tk-config.json
│ │ │ │ └── content.md
│ │ │ ├── 6-updating-records
│ │ │ │ └── _files
│ │ │ │ │ ├── workspace
│ │ │ │ │ ├── .keep
│ │ │ │ │ └── app
│ │ │ │ │ │ └── models
│ │ │ │ │ │ └── product.rb
│ │ │ │ │ └── .tk-config.json
│ │ │ ├── 7-deleting-records
│ │ │ │ ├── _files
│ │ │ │ │ ├── workspace
│ │ │ │ │ │ ├── .keep
│ │ │ │ │ │ └── app
│ │ │ │ │ │ │ └── models
│ │ │ │ │ │ │ └── product.rb
│ │ │ │ │ └── .tk-config.json
│ │ │ │ └── content.md
│ │ │ ├── 4-filtering-records
│ │ │ │ └── _files
│ │ │ │ │ ├── workspace
│ │ │ │ │ ├── .keep
│ │ │ │ │ └── app
│ │ │ │ │ │ └── models
│ │ │ │ │ │ └── product.rb
│ │ │ │ │ └── .tk-config.json
│ │ │ └── meta.md
│ │ ├── 10-caching
│ │ │ ├── 1-caching-products
│ │ │ │ └── _files
│ │ │ │ │ ├── workspace
│ │ │ │ │ └── .keep
│ │ │ │ │ └── .tk-config.json
│ │ │ └── meta.md
│ │ ├── 11-rich-text
│ │ │ ├── 1-action-text
│ │ │ │ └── _files
│ │ │ │ │ ├── workspace
│ │ │ │ │ ├── .keep
│ │ │ │ │ └── store
│ │ │ │ │ │ └── app
│ │ │ │ │ │ └── views
│ │ │ │ │ │ └── products
│ │ │ │ │ │ └── show.html.erb
│ │ │ │ │ └── .tk-config.json
│ │ │ └── meta.md
│ │ ├── 17-whats-next
│ │ │ ├── 1-conclusion
│ │ │ │ └── _files
│ │ │ │ │ ├── workspace
│ │ │ │ │ └── .keep
│ │ │ │ │ └── .tk-config.json
│ │ │ └── meta.md
│ │ ├── 3-hello-rails
│ │ │ ├── 1-rails-server
│ │ │ │ └── _files
│ │ │ │ │ ├── workspace
│ │ │ │ │ ├── .keep
│ │ │ │ │ └── config.ru
│ │ │ │ │ └── .tk-config.json
│ │ │ ├── meta.md
│ │ │ └── 2-autoloading
│ │ │ │ └── content.md
│ │ ├── 4-database
│ │ │ ├── 1-generate-model
│ │ │ │ └── _files
│ │ │ │ │ ├── workspace
│ │ │ │ │ └── .keep
│ │ │ │ │ └── .tk-config.json
│ │ │ ├── 2-database-migrations
│ │ │ │ └── _files
│ │ │ │ │ ├── workspace
│ │ │ │ │ ├── .keep
│ │ │ │ │ └── store
│ │ │ │ │ │ ├── app
│ │ │ │ │ │ └── models
│ │ │ │ │ │ │ └── product.rb
│ │ │ │ │ │ ├── test
│ │ │ │ │ │ ├── models
│ │ │ │ │ │ │ └── product_test.rb
│ │ │ │ │ │ └── fixtures
│ │ │ │ │ │ │ └── products.yml
│ │ │ │ │ │ └── db
│ │ │ │ │ │ └── migrate
│ │ │ │ │ │ └── 20250521010850_create_products.rb
│ │ │ │ │ └── .tk-config.json
│ │ │ ├── 3-running-migrations
│ │ │ │ ├── _files
│ │ │ │ │ ├── workspace
│ │ │ │ │ │ └── .keep
│ │ │ │ │ └── .tk-config.json
│ │ │ │ └── content.md
│ │ │ └── meta.md
│ │ ├── 5-console
│ │ │ ├── 1-rails-console
│ │ │ │ ├── _files
│ │ │ │ │ ├── workspace
│ │ │ │ │ │ └── .keep
│ │ │ │ │ └── .tk-config.json
│ │ │ │ └── content.md
│ │ │ └── meta.md
│ │ ├── 8-controllers
│ │ │ ├── 10-crud-destroy
│ │ │ │ └── _files
│ │ │ │ │ ├── workspace
│ │ │ │ │ ├── .keep
│ │ │ │ │ └── store
│ │ │ │ │ │ ├── app
│ │ │ │ │ │ ├── views
│ │ │ │ │ │ │ └── products
│ │ │ │ │ │ │ │ ├── edit.html.erb
│ │ │ │ │ │ │ │ ├── new.html.erb
│ │ │ │ │ │ │ │ ├── show.html.erb
│ │ │ │ │ │ │ │ ├── _form.html.erb
│ │ │ │ │ │ │ │ └── index.html.erb
│ │ │ │ │ │ └── controllers
│ │ │ │ │ │ │ └── products_controller.rb
│ │ │ │ │ │ └── config
│ │ │ │ │ │ └── routes.rb
│ │ │ │ │ └── .tk-config.json
│ │ │ ├── 4-crud-show
│ │ │ │ └── _files
│ │ │ │ │ ├── workspace
│ │ │ │ │ ├── .keep
│ │ │ │ │ └── store
│ │ │ │ │ │ ├── app
│ │ │ │ │ │ ├── controllers
│ │ │ │ │ │ │ └── products_controller.rb
│ │ │ │ │ │ └── views
│ │ │ │ │ │ │ └── products
│ │ │ │ │ │ │ └── index.html.erb
│ │ │ │ │ │ └── config
│ │ │ │ │ │ └── routes.rb
│ │ │ │ │ └── .tk-config.json
│ │ │ ├── 5-crud-create
│ │ │ │ └── _files
│ │ │ │ │ ├── workspace
│ │ │ │ │ ├── .keep
│ │ │ │ │ └── store
│ │ │ │ │ │ ├── app
│ │ │ │ │ │ ├── views
│ │ │ │ │ │ │ └── products
│ │ │ │ │ │ │ │ ├── show.html.erb
│ │ │ │ │ │ │ │ └── index.html.erb
│ │ │ │ │ │ └── controllers
│ │ │ │ │ │ │ └── products_controller.rb
│ │ │ │ │ │ └── config
│ │ │ │ │ │ └── routes.rb
│ │ │ │ │ └── .tk-config.json
│ │ │ ├── 7-crud-edit
│ │ │ │ └── _files
│ │ │ │ │ ├── workspace
│ │ │ │ │ ├── .keep
│ │ │ │ │ └── store
│ │ │ │ │ │ ├── app
│ │ │ │ │ │ ├── views
│ │ │ │ │ │ │ └── products
│ │ │ │ │ │ │ │ ├── show.html.erb
│ │ │ │ │ │ │ │ ├── new.html.erb
│ │ │ │ │ │ │ │ └── index.html.erb
│ │ │ │ │ │ └── controllers
│ │ │ │ │ │ │ └── products_controller.rb
│ │ │ │ │ │ └── config
│ │ │ │ │ │ └── routes.rb
│ │ │ │ │ └── .tk-config.json
│ │ │ ├── 2-making-requests
│ │ │ │ ├── _files
│ │ │ │ │ ├── workspace
│ │ │ │ │ │ └── .keep
│ │ │ │ │ └── .tk-config.json
│ │ │ │ └── content.md
│ │ │ ├── 3-instance-variables
│ │ │ │ └── _files
│ │ │ │ │ ├── workspace
│ │ │ │ │ ├── .keep
│ │ │ │ │ └── store
│ │ │ │ │ │ └── config
│ │ │ │ │ │ └── routes.rb
│ │ │ │ │ └── .tk-config.json
│ │ │ ├── 6-strong-parameters
│ │ │ │ └── _files
│ │ │ │ │ ├── workspace
│ │ │ │ │ ├── .keep
│ │ │ │ │ └── store
│ │ │ │ │ │ ├── app
│ │ │ │ │ │ ├── views
│ │ │ │ │ │ │ └── products
│ │ │ │ │ │ │ │ ├── show.html.erb
│ │ │ │ │ │ │ │ ├── new.html.erb
│ │ │ │ │ │ │ │ └── index.html.erb
│ │ │ │ │ │ └── controllers
│ │ │ │ │ │ │ └── products_controller.rb
│ │ │ │ │ │ └── config
│ │ │ │ │ │ └── routes.rb
│ │ │ │ │ └── .tk-config.json
│ │ │ ├── 8-before-actions
│ │ │ │ └── _files
│ │ │ │ │ ├── workspace
│ │ │ │ │ ├── .keep
│ │ │ │ │ └── store
│ │ │ │ │ │ ├── config
│ │ │ │ │ │ └── routes.rb
│ │ │ │ │ │ └── app
│ │ │ │ │ │ ├── views
│ │ │ │ │ │ └── products
│ │ │ │ │ │ │ ├── show.html.erb
│ │ │ │ │ │ │ ├── new.html.erb
│ │ │ │ │ │ │ └── index.html.erb
│ │ │ │ │ │ └── controllers
│ │ │ │ │ │ └── products_controller.rb
│ │ │ │ │ └── .tk-config.json
│ │ │ ├── 1-controllers-actions
│ │ │ │ └── _files
│ │ │ │ │ ├── workspace
│ │ │ │ │ ├── .keep
│ │ │ │ │ └── store
│ │ │ │ │ │ └── config
│ │ │ │ │ │ └── routes.rb
│ │ │ │ │ └── .tk-config.json
│ │ │ ├── 9-extracting-partials
│ │ │ │ └── _files
│ │ │ │ │ ├── workspace
│ │ │ │ │ ├── .keep
│ │ │ │ │ └── store
│ │ │ │ │ │ ├── config
│ │ │ │ │ │ └── routes.rb
│ │ │ │ │ │ └── app
│ │ │ │ │ │ ├── views
│ │ │ │ │ │ └── products
│ │ │ │ │ │ │ ├── show.html.erb
│ │ │ │ │ │ │ ├── new.html.erb
│ │ │ │ │ │ │ └── index.html.erb
│ │ │ │ │ │ └── controllers
│ │ │ │ │ │ └── products_controller.rb
│ │ │ │ │ └── .tk-config.json
│ │ │ └── meta.md
│ │ ├── 12-file-uploads
│ │ │ ├── 1-active-storage
│ │ │ │ └── _files
│ │ │ │ │ ├── workspace
│ │ │ │ │ ├── .keep
│ │ │ │ │ └── store
│ │ │ │ │ │ └── app
│ │ │ │ │ │ ├── models
│ │ │ │ │ │ └── product.rb
│ │ │ │ │ │ ├── views
│ │ │ │ │ │ └── products
│ │ │ │ │ │ │ ├── _form.html.erb
│ │ │ │ │ │ │ └── show.html.erb
│ │ │ │ │ │ └── controllers
│ │ │ │ │ │ └── products_controller.rb
│ │ │ │ │ └── .tk-config.json
│ │ │ └── meta.md
│ │ ├── 13-internationalization
│ │ │ ├── 1-i18n
│ │ │ │ └── _files
│ │ │ │ │ ├── workspace
│ │ │ │ │ └── .keep
│ │ │ │ │ └── .tk-config.json
│ │ │ └── meta.md
│ │ ├── 2-rails-new
│ │ │ ├── 3-directory-structure
│ │ │ │ └── _files
│ │ │ │ │ ├── workspace
│ │ │ │ │ └── .keep
│ │ │ │ │ └── .tk-config.json
│ │ │ ├── 2-creating-your-first-rails-app
│ │ │ │ ├── _files
│ │ │ │ │ └── workspace
│ │ │ │ │ │ └── .keep
│ │ │ │ └── content.md
│ │ │ ├── meta.md
│ │ │ ├── 4-mvc
│ │ │ │ └── content.md
│ │ │ └── 1-prerequisites
│ │ │ │ └── content.md
│ │ ├── 7-request-journey
│ │ │ ├── 3-rails-routes
│ │ │ │ └── _files
│ │ │ │ │ ├── workspace
│ │ │ │ │ └── .keep
│ │ │ │ │ └── .tk-config.json
│ │ │ ├── 5-rails-routes-2
│ │ │ │ └── _files
│ │ │ │ │ ├── workspace
│ │ │ │ │ ├── .keep
│ │ │ │ │ └── store
│ │ │ │ │ │ └── config
│ │ │ │ │ │ └── routes.rb
│ │ │ │ │ └── .tk-config.json
│ │ │ ├── 6-crud-routes
│ │ │ │ └── _files
│ │ │ │ │ ├── workspace
│ │ │ │ │ └── .keep
│ │ │ │ │ └── .tk-config.json
│ │ │ ├── meta.md
│ │ │ ├── 1-intro
│ │ │ │ └── content.md
│ │ │ └── 4-rails-routes-explained
│ │ │ │ └── content.md
│ │ ├── 9-authentication
│ │ │ ├── 2-adding-log-out
│ │ │ │ ├── _files
│ │ │ │ │ ├── workspace
│ │ │ │ │ │ ├── .keep
│ │ │ │ │ │ └── store
│ │ │ │ │ │ │ └── app
│ │ │ │ │ │ │ ├── views
│ │ │ │ │ │ │ ├── products
│ │ │ │ │ │ │ │ ├── show.html.erb
│ │ │ │ │ │ │ │ └── index.html.erb
│ │ │ │ │ │ │ └── layouts
│ │ │ │ │ │ │ │ └── application.html.erb
│ │ │ │ │ │ │ └── controllers
│ │ │ │ │ │ │ └── products_controller.rb
│ │ │ │ │ └── .tk-config.json
│ │ │ │ └── content.md
│ │ │ ├── 4-authenticated-links
│ │ │ │ └── _files
│ │ │ │ │ ├── workspace
│ │ │ │ │ ├── .keep
│ │ │ │ │ └── store
│ │ │ │ │ │ └── app
│ │ │ │ │ │ └── views
│ │ │ │ │ │ ├── products
│ │ │ │ │ │ ├── show.html.erb
│ │ │ │ │ │ └── index.html.erb
│ │ │ │ │ │ └── layouts
│ │ │ │ │ │ └── application.html.erb
│ │ │ │ │ └── .tk-config.json
│ │ │ ├── 1-adding-authentication
│ │ │ │ └── _files
│ │ │ │ │ ├── workspace
│ │ │ │ │ └── .keep
│ │ │ │ │ └── .tk-config.json
│ │ │ ├── 3-unauthenticated-access
│ │ │ │ ├── _files
│ │ │ │ │ ├── workspace
│ │ │ │ │ │ ├── .keep
│ │ │ │ │ │ └── store
│ │ │ │ │ │ │ └── app
│ │ │ │ │ │ │ ├── views
│ │ │ │ │ │ │ ├── products
│ │ │ │ │ │ │ │ ├── show.html.erb
│ │ │ │ │ │ │ │ └── index.html.erb
│ │ │ │ │ │ │ └── layouts
│ │ │ │ │ │ │ │ └── application.html.erb
│ │ │ │ │ │ │ └── controllers
│ │ │ │ │ │ │ └── products_controller.rb
│ │ │ │ │ └── .tk-config.json
│ │ │ │ └── content.md
│ │ │ └── meta.md
│ │ ├── 14-in-stock-notifications
│ │ │ ├── 1-inventory-tracking
│ │ │ │ └── _files
│ │ │ │ │ ├── workspace
│ │ │ │ │ └── .keep
│ │ │ │ │ └── .tk-config.json
│ │ │ ├── 2-adding-subscribers
│ │ │ │ └── _files
│ │ │ │ │ ├── workspace
│ │ │ │ │ ├── .keep
│ │ │ │ │ └── store
│ │ │ │ │ │ ├── db
│ │ │ │ │ │ ├── migrate
│ │ │ │ │ │ │ └── 20250620213158_add_inventory_count_to_products.rb
│ │ │ │ │ │ └── seeds.rb
│ │ │ │ │ │ └── app
│ │ │ │ │ │ ├── models
│ │ │ │ │ │ └── product.rb
│ │ │ │ │ │ ├── views
│ │ │ │ │ │ └── products
│ │ │ │ │ │ │ └── _form.html.erb
│ │ │ │ │ │ └── controllers
│ │ │ │ │ │ └── products_controller.rb
│ │ │ │ │ └── .tk-config.json
│ │ │ ├── 4-extracting-a-concern
│ │ │ │ └── _files
│ │ │ │ │ ├── workspace
│ │ │ │ │ ├── .keep
│ │ │ │ │ └── store
│ │ │ │ │ │ ├── app
│ │ │ │ │ │ ├── models
│ │ │ │ │ │ │ ├── subscriber.rb
│ │ │ │ │ │ │ └── product.rb
│ │ │ │ │ │ ├── views
│ │ │ │ │ │ │ ├── product_mailer
│ │ │ │ │ │ │ │ ├── in_stock.text.erb
│ │ │ │ │ │ │ │ └── in_stock.html.erb
│ │ │ │ │ │ │ └── products
│ │ │ │ │ │ │ │ ├── _inventory.html.erb
│ │ │ │ │ │ │ │ ├── show.html.erb
│ │ │ │ │ │ │ │ └── _form.html.erb
│ │ │ │ │ │ ├── mailers
│ │ │ │ │ │ │ └── product_mailer.rb
│ │ │ │ │ │ └── controllers
│ │ │ │ │ │ │ ├── subscribers_controller.rb
│ │ │ │ │ │ │ └── products_controller.rb
│ │ │ │ │ │ ├── test
│ │ │ │ │ │ ├── models
│ │ │ │ │ │ │ └── subscriber_test.rb
│ │ │ │ │ │ ├── fixtures
│ │ │ │ │ │ │ └── subscribers.yml
│ │ │ │ │ │ └── mailers
│ │ │ │ │ │ │ └── product_mailer_test.rb
│ │ │ │ │ │ ├── db
│ │ │ │ │ │ ├── migrate
│ │ │ │ │ │ │ ├── 20250620213158_add_inventory_count_to_products.rb
│ │ │ │ │ │ │ └── 20250620214344_create_subscribers.rb
│ │ │ │ │ │ └── seeds.rb
│ │ │ │ │ │ └── config
│ │ │ │ │ │ └── routes.rb
│ │ │ │ │ └── .tk-config.json
│ │ │ ├── 5-unsubscribe-links
│ │ │ │ └── _files
│ │ │ │ │ ├── workspace
│ │ │ │ │ ├── .keep
│ │ │ │ │ └── store
│ │ │ │ │ │ ├── app
│ │ │ │ │ │ ├── models
│ │ │ │ │ │ │ ├── subscriber.rb
│ │ │ │ │ │ │ ├── product.rb
│ │ │ │ │ │ │ └── product
│ │ │ │ │ │ │ │ └── notifications.rb
│ │ │ │ │ │ ├── views
│ │ │ │ │ │ │ ├── product_mailer
│ │ │ │ │ │ │ │ ├── in_stock.text.erb
│ │ │ │ │ │ │ │ └── in_stock.html.erb
│ │ │ │ │ │ │ └── products
│ │ │ │ │ │ │ │ ├── _inventory.html.erb
│ │ │ │ │ │ │ │ ├── show.html.erb
│ │ │ │ │ │ │ │ └── _form.html.erb
│ │ │ │ │ │ ├── mailers
│ │ │ │ │ │ │ └── product_mailer.rb
│ │ │ │ │ │ └── controllers
│ │ │ │ │ │ │ ├── subscribers_controller.rb
│ │ │ │ │ │ │ └── products_controller.rb
│ │ │ │ │ │ ├── test
│ │ │ │ │ │ ├── models
│ │ │ │ │ │ │ └── subscriber_test.rb
│ │ │ │ │ │ ├── fixtures
│ │ │ │ │ │ │ └── subscribers.yml
│ │ │ │ │ │ └── mailers
│ │ │ │ │ │ │ └── product_mailer_test.rb
│ │ │ │ │ │ ├── db
│ │ │ │ │ │ ├── migrate
│ │ │ │ │ │ │ ├── 20250620213158_add_inventory_count_to_products.rb
│ │ │ │ │ │ │ └── 20250620214344_create_subscribers.rb
│ │ │ │ │ │ └── seeds.rb
│ │ │ │ │ │ └── config
│ │ │ │ │ │ └── routes.rb
│ │ │ │ │ └── .tk-config.json
│ │ │ ├── 3-in-stock-email-notifications
│ │ │ │ └── _files
│ │ │ │ │ ├── workspace
│ │ │ │ │ ├── .keep
│ │ │ │ │ └── store
│ │ │ │ │ │ ├── app
│ │ │ │ │ │ ├── models
│ │ │ │ │ │ │ ├── subscriber.rb
│ │ │ │ │ │ │ └── product.rb
│ │ │ │ │ │ ├── views
│ │ │ │ │ │ │ └── products
│ │ │ │ │ │ │ │ ├── _inventory.html.erb
│ │ │ │ │ │ │ │ ├── show.html.erb
│ │ │ │ │ │ │ │ └── _form.html.erb
│ │ │ │ │ │ └── controllers
│ │ │ │ │ │ │ ├── subscribers_controller.rb
│ │ │ │ │ │ │ └── products_controller.rb
│ │ │ │ │ │ ├── test
│ │ │ │ │ │ ├── models
│ │ │ │ │ │ │ └── subscriber_test.rb
│ │ │ │ │ │ └── fixtures
│ │ │ │ │ │ │ └── subscribers.yml
│ │ │ │ │ │ ├── db
│ │ │ │ │ │ ├── migrate
│ │ │ │ │ │ │ ├── 20250620213158_add_inventory_count_to_products.rb
│ │ │ │ │ │ │ └── 20250620214344_create_subscribers.rb
│ │ │ │ │ │ └── seeds.rb
│ │ │ │ │ │ └── config
│ │ │ │ │ │ └── routes.rb
│ │ │ │ │ └── .tk-config.json
│ │ │ └── meta.md
│ │ ├── 1-introduction
│ │ │ └── meta.md
│ │ └── meta.md
│ └── config.ts
├── env.d.ts
└── plugins
│ └── remarkRailsPathLinks.ts
├── rails-wasm
├── .gitignore
├── .railsrc
├── Gemfile
├── bin
│ └── pack
├── README.md
├── package.json
├── config
│ └── wasmify.yml
└── boot.rb
├── .dockerignore
├── public
└── images
│ └── getting_started
│ ├── routing_dark.jpg
│ ├── routing_light.jpg
│ ├── mvc_architecture_dark.jpg
│ └── mvc_architecture_light.jpg
├── vite.config.js
├── .vscode
├── extensions.json
└── launch.json
├── .gitignore
├── uno.config.ts
├── icons
└── languages
│ ├── html.svg
│ ├── markdown.svg
│ ├── css.svg
│ ├── ruby.svg
│ ├── js.svg
│ ├── ts.svg
│ ├── json.svg
│ ├── sass.svg
│ └── erb.svg
├── tsconfig.json
├── fly.toml
├── .github
└── workflows
│ └── fly-deploy.yml
├── astro.config.ts
├── patches
└── @tutorialkit+theme+1.5.0.patch
└── package.json
/src/templates/css-js/workspace/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/templates/default/pgdata/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/templates/default/workspace/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/templates/testing/workspace/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/templates/crud-products/workspace/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/rails-wasm/.gitignore:
--------------------------------------------------------------------------------
1 | tmp/
2 | build/
3 | rubies/
4 |
--------------------------------------------------------------------------------
/src/templates/rails-new/workspace/store/log/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/templates/rails-new/workspace/store/script/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/templates/rails-new/workspace/store/storage/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/templates/rails-new/workspace/store/tmp/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/templates/stock-notifications/workspace/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/templates/rails-new/workspace/store/test/helpers/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/templates/rails-new/workspace/store/tmp/pids/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/templates/rails-new/workspace/store/tmp/storage/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/templates/authentication/workspace/store/test/mailers/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/templates/authentication/workspace/store/test/models/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/templates/rails-new/workspace/store/app/assets/images/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/templates/rails-new/workspace/store/test/controllers/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/templates/rails-new/workspace/store/test/integration/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/content/tutorial/15-css-and-js/3-hotwire/_files/workspace/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/content/tutorial/16-testing/1-fixtures/_files/workspace/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/content/tutorial/6-active-record/1-intro/_files/workspace/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/templates/rails-new/workspace/store/.ruby-version:
--------------------------------------------------------------------------------
1 | ruby-3.3.6
2 |
--------------------------------------------------------------------------------
/src/templates/rails-new/workspace/store/app/models/concerns/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/templates/rails-new/workspace/store/vendor/javascripts/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/content/tutorial/10-caching/1-caching-products/_files/workspace/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/content/tutorial/11-rich-text/1-action-text/_files/workspace/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/content/tutorial/15-css-and-js/1-propshaft/_files/workspace/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/content/tutorial/15-css-and-js/2-importmap/_files/workspace/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/content/tutorial/16-testing/2-testing-emails/_files/workspace/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/content/tutorial/17-whats-next/1-conclusion/_files/workspace/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/content/tutorial/3-hello-rails/1-rails-server/_files/workspace/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/content/tutorial/4-database/1-generate-model/_files/workspace/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/content/tutorial/5-console/1-rails-console/_files/workspace/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/content/tutorial/6-active-record/8-validations/_files/workspace/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/content/tutorial/8-controllers/10-crud-destroy/_files/workspace/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/content/tutorial/8-controllers/4-crud-show/_files/workspace/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/content/tutorial/8-controllers/5-crud-create/_files/workspace/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/content/tutorial/8-controllers/7-crud-edit/_files/workspace/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/templates/authentication/workspace/store/test/fixtures/files/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/content/tutorial/12-file-uploads/1-active-storage/_files/workspace/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/content/tutorial/13-internationalization/1-i18n/_files/workspace/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/content/tutorial/2-rails-new/3-directory-structure/_files/workspace/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/content/tutorial/4-database/2-database-migrations/_files/workspace/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/content/tutorial/4-database/3-running-migrations/_files/workspace/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/content/tutorial/6-active-record/2-creating-records/_files/workspace/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/content/tutorial/6-active-record/3-querying-records/_files/workspace/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/content/tutorial/6-active-record/5-finding-records/_files/workspace/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/content/tutorial/6-active-record/6-updating-records/_files/workspace/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/content/tutorial/6-active-record/7-deleting-records/_files/workspace/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/content/tutorial/7-request-journey/3-rails-routes/_files/workspace/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/content/tutorial/7-request-journey/5-rails-routes-2/_files/workspace/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/content/tutorial/7-request-journey/6-crud-routes/_files/workspace/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/content/tutorial/8-controllers/2-making-requests/_files/workspace/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/content/tutorial/8-controllers/3-instance-variables/_files/workspace/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/content/tutorial/8-controllers/6-strong-parameters/_files/workspace/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/content/tutorial/8-controllers/8-before-actions/_files/workspace/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/content/tutorial/9-authentication/2-adding-log-out/_files/workspace/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/templates/authentication/workspace/store/app/controllers/concerns/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/templates/testing/.tk-config.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../css-js"
3 | }
4 |
--------------------------------------------------------------------------------
/src/content/tutorial/6-active-record/4-filtering-records/_files/workspace/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/content/tutorial/8-controllers/1-controllers-actions/_files/workspace/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/content/tutorial/8-controllers/9-extracting-partials/_files/workspace/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/content/tutorial/9-authentication/4-authenticated-links/_files/workspace/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/content/tutorial/2-rails-new/2-creating-your-first-rails-app/_files/workspace/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/content/tutorial/9-authentication/1-adding-authentication/_files/workspace/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/content/tutorial/9-authentication/3-unauthenticated-access/_files/workspace/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/content/tutorial/14-in-stock-notifications/1-inventory-tracking/_files/workspace/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/content/tutorial/14-in-stock-notifications/2-adding-subscribers/_files/workspace/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/content/tutorial/14-in-stock-notifications/4-extracting-a-concern/_files/workspace/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/content/tutorial/14-in-stock-notifications/5-unsubscribe-links/_files/workspace/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/templates/active-storage/.tk-config.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../authentication"
3 | }
4 |
--------------------------------------------------------------------------------
/src/templates/authentication/.tk-config.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../crud-products"
3 | }
4 |
--------------------------------------------------------------------------------
/src/templates/authentication/workspace/store/tmp/authenticated-user.txt:
--------------------------------------------------------------------------------
1 | you@example.org
2 |
--------------------------------------------------------------------------------
/src/templates/create-products/.tk-config.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../rails-new"
3 | }
4 |
--------------------------------------------------------------------------------
/src/templates/crud-products/.tk-config.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../create-products"
3 | }
4 |
--------------------------------------------------------------------------------
/src/templates/css-js/.tk-config.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../stock-notifications"
3 | }
4 |
--------------------------------------------------------------------------------
/src/templates/rails-new/workspace/store/app/views/layouts/mailer.text.erb:
--------------------------------------------------------------------------------
1 | <%= yield %>
2 |
--------------------------------------------------------------------------------
/src/templates/rails-new/workspace/store/config/master.key:
--------------------------------------------------------------------------------
1 | 6cca3c78ba2c4c57b341dd7b4cb9b889
--------------------------------------------------------------------------------
/src/content/tutorial/3-hello-rails/meta.md:
--------------------------------------------------------------------------------
1 | ---
2 | type: part
3 | title: Hello, Rails!
4 | ---
5 |
--------------------------------------------------------------------------------
/src/templates/default/lib/constants.js:
--------------------------------------------------------------------------------
1 | export const RAILS_WASM_PACKAGE_VERSION = '8.0.2-rc.1';
2 |
--------------------------------------------------------------------------------
/src/templates/stock-notifications/.tk-config.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../active-storage"
3 | }
4 |
--------------------------------------------------------------------------------
/src/content/tutorial/14-in-stock-notifications/3-in-stock-email-notifications/_files/workspace/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/templates/products-controller/.tk-config.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../create-products"
3 | }
4 |
--------------------------------------------------------------------------------
/src/content/tutorial/2-rails-new/meta.md:
--------------------------------------------------------------------------------
1 | ---
2 | type: part
3 | title: Creating a New Rails App
4 | ---
5 |
--------------------------------------------------------------------------------
/src/templates/rails-new/workspace/store/app/helpers/application_helper.rb:
--------------------------------------------------------------------------------
1 | module ApplicationHelper
2 | end
3 |
--------------------------------------------------------------------------------
/src/templates/products-controller/workspace/store/app/helpers/products_helper.rb:
--------------------------------------------------------------------------------
1 | module ProductsHelper
2 | end
3 |
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | /.git
2 | /node_modules
3 | .dockerignore
4 | .env
5 | Dockerfile
6 | fly.toml
7 | /dist
8 | /rails-wasm
9 |
--------------------------------------------------------------------------------
/src/templates/testing/workspace/store/test/fixtures/products.yml:
--------------------------------------------------------------------------------
1 | tshirt:
2 | name: T-Shirt
3 | inventory_count: 15
4 |
--------------------------------------------------------------------------------
/src/content/tutorial/16-testing/1-fixtures/_files/.tk-config.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../../../../templates/css-js"
3 | }
4 |
--------------------------------------------------------------------------------
/src/content/tutorial/6-active-record/1-intro/_files/workspace/app/models/product.rb:
--------------------------------------------------------------------------------
1 | class Product < ApplicationRecord
2 | end
3 |
--------------------------------------------------------------------------------
/src/content/tutorial/1-introduction/meta.md:
--------------------------------------------------------------------------------
1 | ---
2 | type: part
3 | title: Introduction
4 | editor: false
5 | terminal: false
6 | ---
7 |
--------------------------------------------------------------------------------
/src/content/tutorial/16-testing/2-testing-emails/_files/.tk-config.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../../../../templates/css-js"
3 | }
4 |
--------------------------------------------------------------------------------
/src/content/tutorial/17-whats-next/1-conclusion/_files/.tk-config.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../../../../templates/testing"
3 | }
4 |
--------------------------------------------------------------------------------
/src/content/tutorial/3-hello-rails/1-rails-server/_files/.tk-config.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../../../../templates/rails-new"
3 | }
4 |
--------------------------------------------------------------------------------
/src/content/tutorial/4-database/1-generate-model/_files/.tk-config.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../../../../templates/rails-new"
3 | }
4 |
--------------------------------------------------------------------------------
/src/content/tutorial/6-active-record/8-validations/_files/workspace/app/models/product.rb:
--------------------------------------------------------------------------------
1 | class Product < ApplicationRecord
2 | end
3 |
--------------------------------------------------------------------------------
/src/templates/authentication/workspace/store/app/models/session.rb:
--------------------------------------------------------------------------------
1 | class Session < ApplicationRecord
2 | belongs_to :user
3 | end
4 |
--------------------------------------------------------------------------------
/src/content/tutorial/11-rich-text/1-action-text/_files/.tk-config.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../../../../templates/active-storage"
3 | }
4 |
--------------------------------------------------------------------------------
/src/content/tutorial/15-css-and-js/3-hotwire/_files/.tk-config.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../../../../templates/stock-notifications"
3 | }
4 |
--------------------------------------------------------------------------------
/src/content/tutorial/2-rails-new/3-directory-structure/_files/.tk-config.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../../../../templates/rails-new"
3 | }
4 |
--------------------------------------------------------------------------------
/src/content/tutorial/4-database/2-database-migrations/_files/.tk-config.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../../../../templates/rails-new"
3 | }
4 |
--------------------------------------------------------------------------------
/src/content/tutorial/4-database/3-running-migrations/_files/.tk-config.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../2-database-migrations/_files"
3 | }
4 |
--------------------------------------------------------------------------------
/src/content/tutorial/5-console/1-rails-console/_files/.tk-config.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../../../../templates/create-products"
3 | }
4 |
--------------------------------------------------------------------------------
/src/content/tutorial/6-active-record/1-intro/_files/.tk-config.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../../../../templates/create-products"
3 | }
4 |
--------------------------------------------------------------------------------
/src/content/tutorial/6-active-record/2-creating-records/_files/workspace/app/models/product.rb:
--------------------------------------------------------------------------------
1 | class Product < ApplicationRecord
2 | end
3 |
--------------------------------------------------------------------------------
/src/content/tutorial/6-active-record/3-querying-records/_files/workspace/app/models/product.rb:
--------------------------------------------------------------------------------
1 | class Product < ApplicationRecord
2 | end
3 |
--------------------------------------------------------------------------------
/src/content/tutorial/6-active-record/4-filtering-records/_files/workspace/app/models/product.rb:
--------------------------------------------------------------------------------
1 | class Product < ApplicationRecord
2 | end
3 |
--------------------------------------------------------------------------------
/src/content/tutorial/6-active-record/5-finding-records/_files/workspace/app/models/product.rb:
--------------------------------------------------------------------------------
1 | class Product < ApplicationRecord
2 | end
3 |
--------------------------------------------------------------------------------
/src/content/tutorial/6-active-record/6-updating-records/_files/workspace/app/models/product.rb:
--------------------------------------------------------------------------------
1 | class Product < ApplicationRecord
2 | end
3 |
--------------------------------------------------------------------------------
/src/content/tutorial/6-active-record/7-deleting-records/_files/workspace/app/models/product.rb:
--------------------------------------------------------------------------------
1 | class Product < ApplicationRecord
2 | end
3 |
--------------------------------------------------------------------------------
/src/content/tutorial/10-caching/1-caching-products/_files/.tk-config.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../../../../templates/authentication"
3 | }
4 |
--------------------------------------------------------------------------------
/src/content/tutorial/12-file-uploads/1-active-storage/_files/.tk-config.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../../../../templates/active-storage"
3 | }
4 |
--------------------------------------------------------------------------------
/src/content/tutorial/13-internationalization/1-i18n/_files/.tk-config.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../../../../templates/active-storage"
3 | }
4 |
--------------------------------------------------------------------------------
/src/content/tutorial/15-css-and-js/1-propshaft/_files/.tk-config.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../../../../templates/stock-notifications"
3 | }
4 |
--------------------------------------------------------------------------------
/src/content/tutorial/15-css-and-js/2-importmap/_files/.tk-config.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../../../../templates/stock-notifications"
3 | }
4 |
--------------------------------------------------------------------------------
/src/content/tutorial/4-database/2-database-migrations/_files/workspace/store/app/models/product.rb:
--------------------------------------------------------------------------------
1 | class Product < ApplicationRecord
2 | end
3 |
--------------------------------------------------------------------------------
/src/content/tutorial/6-active-record/8-validations/_files/.tk-config.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../../../../templates/create-products"
3 | }
4 |
--------------------------------------------------------------------------------
/src/content/tutorial/7-request-journey/3-rails-routes/_files/.tk-config.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../../../../templates/create-products"
3 | }
4 |
--------------------------------------------------------------------------------
/src/content/tutorial/7-request-journey/6-crud-routes/_files/.tk-config.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../../../../templates/create-products"
3 | }
4 |
--------------------------------------------------------------------------------
/src/content/tutorial/8-controllers/4-crud-show/_files/.tk-config.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../../../../templates/products-controller"
3 | }
4 |
--------------------------------------------------------------------------------
/src/content/tutorial/8-controllers/5-crud-create/_files/.tk-config.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../../../../templates/products-controller"
3 | }
4 |
--------------------------------------------------------------------------------
/src/content/tutorial/8-controllers/7-crud-edit/_files/.tk-config.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../../../../templates/products-controller"
3 | }
4 |
--------------------------------------------------------------------------------
/src/content/tutorial/9-authentication/2-adding-log-out/_files/.tk-config.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../../../../templates/authentication"
3 | }
4 |
--------------------------------------------------------------------------------
/src/content/tutorial/6-active-record/2-creating-records/_files/.tk-config.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../../../../templates/create-products"
3 | }
4 |
--------------------------------------------------------------------------------
/src/content/tutorial/6-active-record/3-querying-records/_files/.tk-config.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../../../../templates/create-products"
3 | }
4 |
--------------------------------------------------------------------------------
/src/content/tutorial/6-active-record/4-filtering-records/_files/.tk-config.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../../../../templates/create-products"
3 | }
4 |
--------------------------------------------------------------------------------
/src/content/tutorial/6-active-record/5-finding-records/_files/.tk-config.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../../../../templates/create-products"
3 | }
4 |
--------------------------------------------------------------------------------
/src/content/tutorial/6-active-record/6-updating-records/_files/.tk-config.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../../../../templates/create-products"
3 | }
4 |
--------------------------------------------------------------------------------
/src/content/tutorial/6-active-record/7-deleting-records/_files/.tk-config.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../../../../templates/create-products"
3 | }
4 |
--------------------------------------------------------------------------------
/src/content/tutorial/7-request-journey/5-rails-routes-2/_files/.tk-config.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../../../../templates/create-products"
3 | }
4 |
--------------------------------------------------------------------------------
/src/content/tutorial/8-controllers/1-controllers-actions/_files/.tk-config.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../../../../templates/create-products"
3 | }
4 |
--------------------------------------------------------------------------------
/src/content/tutorial/8-controllers/10-crud-destroy/_files/.tk-config.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../../../../templates/products-controller"
3 | }
4 |
--------------------------------------------------------------------------------
/src/content/tutorial/8-controllers/2-making-requests/_files/.tk-config.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../../../../templates/products-controller"
3 | }
4 |
--------------------------------------------------------------------------------
/src/content/tutorial/8-controllers/6-strong-parameters/_files/.tk-config.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../../../../templates/products-controller"
3 | }
4 |
--------------------------------------------------------------------------------
/src/content/tutorial/8-controllers/8-before-actions/_files/.tk-config.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../../../../templates/products-controller"
3 | }
4 |
--------------------------------------------------------------------------------
/src/content/tutorial/9-authentication/4-authenticated-links/_files/.tk-config.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../../../../templates/authentication"
3 | }
4 |
--------------------------------------------------------------------------------
/src/templates/create-products/workspace/store/app/models/product.rb:
--------------------------------------------------------------------------------
1 | class Product < ApplicationRecord
2 | validates :name, presence: true
3 | end
4 |
--------------------------------------------------------------------------------
/public/images/getting_started/routing_dark.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evilmartians/rails-tutorial/HEAD/public/images/getting_started/routing_dark.jpg
--------------------------------------------------------------------------------
/public/images/getting_started/routing_light.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evilmartians/rails-tutorial/HEAD/public/images/getting_started/routing_light.jpg
--------------------------------------------------------------------------------
/src/content/tutorial/16-testing/2-testing-emails/_files/workspace/store/test/fixtures/products.yml:
--------------------------------------------------------------------------------
1 | tshirt:
2 | name: T-Shirt
3 | inventory_count: 15
4 |
--------------------------------------------------------------------------------
/src/content/tutorial/8-controllers/3-instance-variables/_files/.tk-config.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../../../../templates/products-controller"
3 | }
4 |
--------------------------------------------------------------------------------
/src/content/tutorial/8-controllers/9-extracting-partials/_files/.tk-config.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../../../../templates/products-controller"
3 | }
4 |
--------------------------------------------------------------------------------
/src/content/tutorial/9-authentication/1-adding-authentication/_files/.tk-config.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../../../../templates/crud-products"
3 | }
4 |
--------------------------------------------------------------------------------
/src/content/tutorial/9-authentication/3-unauthenticated-access/_files/.tk-config.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../../../../templates/authentication"
3 | }
4 |
--------------------------------------------------------------------------------
/src/templates/rails-new/workspace/store/app/models/application_record.rb:
--------------------------------------------------------------------------------
1 | class ApplicationRecord < ActiveRecord::Base
2 | primary_abstract_class
3 | end
4 |
--------------------------------------------------------------------------------
/src/content/tutorial/14-in-stock-notifications/1-inventory-tracking/_files/.tk-config.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../../../../templates/active-storage"
3 | }
4 |
--------------------------------------------------------------------------------
/src/content/tutorial/14-in-stock-notifications/2-adding-subscribers/_files/.tk-config.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../../../../templates/active-storage"
3 | }
4 |
--------------------------------------------------------------------------------
/src/content/tutorial/14-in-stock-notifications/4-extracting-a-concern/_files/.tk-config.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../../../../templates/active-storage"
3 | }
4 |
--------------------------------------------------------------------------------
/src/content/tutorial/14-in-stock-notifications/5-unsubscribe-links/_files/.tk-config.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../../../../templates/active-storage"
3 | }
4 |
--------------------------------------------------------------------------------
/src/env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 | ///
4 |
--------------------------------------------------------------------------------
/src/templates/active-storage/workspace/store/app/views/layouts/action_text/contents/_content.html.erb:
--------------------------------------------------------------------------------
1 |
2 | <%= yield -%>
3 |
4 |
--------------------------------------------------------------------------------
/src/templates/rails-new/workspace/store/bin/importmap:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | require_relative "../config/application"
4 | require "importmap/commands"
5 |
--------------------------------------------------------------------------------
/src/templates/rails-new/workspace/store/public/robots.txt:
--------------------------------------------------------------------------------
1 | # See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
2 |
--------------------------------------------------------------------------------
/vite.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite';
2 |
3 | export default defineConfig({
4 | preview: {
5 | allowedHosts: ['astro.test']
6 | }
7 | });
--------------------------------------------------------------------------------
/src/content/tutorial/3-hello-rails/1-rails-server/_files/workspace/config.ru:
--------------------------------------------------------------------------------
1 | run ->(env) {
2 | [200, {"Content-Type" => "text/plain"}, ["Hello from Rack!"]]
3 | }
4 |
--------------------------------------------------------------------------------
/src/templates/products-controller/workspace/store/app/views/products/index.html.erb:
--------------------------------------------------------------------------------
1 | Products#index
2 | Find me in app/views/products/index.html.erb
3 |
--------------------------------------------------------------------------------
/public/images/getting_started/mvc_architecture_dark.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evilmartians/rails-tutorial/HEAD/public/images/getting_started/mvc_architecture_dark.jpg
--------------------------------------------------------------------------------
/src/content/tutorial/14-in-stock-notifications/3-in-stock-email-notifications/_files/.tk-config.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../../../../templates/active-storage"
3 | }
4 |
--------------------------------------------------------------------------------
/src/templates/rails-new/workspace/store/config/routes.rb:
--------------------------------------------------------------------------------
1 | Rails.application.routes.draw do
2 | # Defines the root path route ("/")
3 | # root "posts#index"
4 | end
5 |
--------------------------------------------------------------------------------
/src/templates/rails-new/workspace/store/public/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evilmartians/rails-tutorial/HEAD/src/templates/rails-new/workspace/store/public/icon.png
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": ["astro-build.astro-vscode", "StackBlitz.tutorialkit", "unifiedjs.vscode-mdx"],
3 | "unwantedRecommendations": []
4 | }
5 |
--------------------------------------------------------------------------------
/public/images/getting_started/mvc_architecture_light.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evilmartians/rails-tutorial/HEAD/public/images/getting_started/mvc_architecture_light.jpg
--------------------------------------------------------------------------------
/src/templates/products-controller/workspace/store/app/controllers/products_controller.rb:
--------------------------------------------------------------------------------
1 | class ProductsController < ApplicationController
2 | def index
3 | end
4 | end
5 |
--------------------------------------------------------------------------------
/src/content/tutorial/8-controllers/7-crud-edit/_files/workspace/store/app/views/products/show.html.erb:
--------------------------------------------------------------------------------
1 | <%= @product.name %>
2 |
3 | <%= link_to "Back", products_path %>
4 |
--------------------------------------------------------------------------------
/src/content/tutorial/8-controllers/5-crud-create/_files/workspace/store/app/views/products/show.html.erb:
--------------------------------------------------------------------------------
1 | <%= @product.name %>
2 |
3 | <%= link_to "Back", products_path %>
4 |
--------------------------------------------------------------------------------
/src/templates/stock-notifications/workspace/store/app/models/subscriber.rb:
--------------------------------------------------------------------------------
1 | class Subscriber < ApplicationRecord
2 | belongs_to :product
3 | generates_token_for :unsubscribe
4 | end
5 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | dist
2 | .astro
3 | node_modules
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | .env
9 | .env.production
10 | .DS_Store
11 | .idea
12 |
--------------------------------------------------------------------------------
/src/content/tutorial/14-in-stock-notifications/4-extracting-a-concern/_files/workspace/store/app/models/subscriber.rb:
--------------------------------------------------------------------------------
1 | class Subscriber < ApplicationRecord
2 | belongs_to :product
3 | end
4 |
--------------------------------------------------------------------------------
/src/content/tutorial/14-in-stock-notifications/5-unsubscribe-links/_files/workspace/store/app/models/subscriber.rb:
--------------------------------------------------------------------------------
1 | class Subscriber < ApplicationRecord
2 | belongs_to :product
3 | end
4 |
--------------------------------------------------------------------------------
/src/content/tutorial/8-controllers/6-strong-parameters/_files/workspace/store/app/views/products/show.html.erb:
--------------------------------------------------------------------------------
1 | <%= @product.name %>
2 |
3 | <%= link_to "Back", products_path %>
4 |
--------------------------------------------------------------------------------
/src/templates/crud-products/workspace/store/app/views/products/edit.html.erb:
--------------------------------------------------------------------------------
1 | Edit product
2 |
3 | <%= render "form", product: @product %>
4 | <%= link_to "Cancel", @product %>
5 |
--------------------------------------------------------------------------------
/src/templates/crud-products/workspace/store/app/views/products/new.html.erb:
--------------------------------------------------------------------------------
1 | New product
2 |
3 | <%= render "form", product: @product %>
4 | <%= link_to "Cancel", products_path %>
5 |
--------------------------------------------------------------------------------
/src/templates/rails-new/workspace/store/app/mailers/application_mailer.rb:
--------------------------------------------------------------------------------
1 | class ApplicationMailer < ActionMailer::Base
2 | default from: "from@example.com"
3 | layout "mailer"
4 | end
5 |
--------------------------------------------------------------------------------
/src/templates/rails-new/workspace/store/public/icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/uno.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from '@tutorialkit/theme';
2 |
3 | export default defineConfig({
4 | // add your UnoCSS config here: https://unocss.dev/guide/config-file
5 | });
6 |
--------------------------------------------------------------------------------
/src/templates/rails-new/workspace/store/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 |
--------------------------------------------------------------------------------
/src/content/tutorial/14-in-stock-notifications/3-in-stock-email-notifications/_files/workspace/store/app/models/subscriber.rb:
--------------------------------------------------------------------------------
1 | class Subscriber < ApplicationRecord
2 | belongs_to :product
3 | end
4 |
--------------------------------------------------------------------------------
/src/templates/testing/workspace/store/test/fixtures/subscribers.yml:
--------------------------------------------------------------------------------
1 | david:
2 | product: tshirt
3 | email: david@example.org
4 |
5 | chris:
6 | product: tshirt
7 | email: chris@example.org
8 |
--------------------------------------------------------------------------------
/icons/languages/html.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/templates/active-storage/workspace/store/app/assets/images/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evilmartians/rails-tutorial/HEAD/src/templates/active-storage/workspace/store/app/assets/images/logo.png
--------------------------------------------------------------------------------
/src/templates/authentication/workspace/store/app/models/current.rb:
--------------------------------------------------------------------------------
1 | class Current < ActiveSupport::CurrentAttributes
2 | attribute :session
3 | delegate :user, to: :session, allow_nil: true
4 | end
5 |
--------------------------------------------------------------------------------
/src/templates/crud-products/workspace/store/config/routes.rb:
--------------------------------------------------------------------------------
1 | Rails.application.routes.draw do
2 | resources :products
3 |
4 | # Defines the root path route ("/")
5 | root "products#index"
6 | end
7 |
--------------------------------------------------------------------------------
/src/templates/rails-new/workspace/store/bin/rails:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | APP_PATH = File.expand_path("../config/application", __dir__)
3 | require_relative "../config/boot"
4 | require "rails/commands"
5 |
--------------------------------------------------------------------------------
/src/templates/create-products/workspace/store/db/seeds.rb:
--------------------------------------------------------------------------------
1 | # Create products that we should have created in the previous lessons
2 | Product.create(name: "T-Shirt").destroy
3 | Product.create(name: "Pants")
4 |
--------------------------------------------------------------------------------
/src/templates/products-controller/workspace/store/config/routes.rb:
--------------------------------------------------------------------------------
1 | Rails.application.routes.draw do
2 | resources :products
3 |
4 | # Defines the root path route ("/")
5 | # root "posts#index"
6 | end
7 |
--------------------------------------------------------------------------------
/src/templates/rails-new/workspace/store/config/environment.rb:
--------------------------------------------------------------------------------
1 | # Load the Rails application.
2 | require_relative "application"
3 |
4 | # Initialize the Rails application.
5 | Rails.application.initialize!
6 |
--------------------------------------------------------------------------------
/src/content/tutorial/12-file-uploads/1-active-storage/_files/workspace/store/app/models/product.rb:
--------------------------------------------------------------------------------
1 | class Product < ApplicationRecord
2 | has_rich_text :description
3 | validates :name, presence: true
4 | end
5 |
--------------------------------------------------------------------------------
/src/templates/active-storage/workspace/store/test/fixtures/action_text/rich_texts.yml:
--------------------------------------------------------------------------------
1 | # one:
2 | # record: name_of_fixture (ClassOfFixture)
3 | # name: content
4 | # body: In a million stars!
5 |
--------------------------------------------------------------------------------
/src/templates/authentication/workspace/store/test/models/user_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 |
3 | class UserTest < ActiveSupport::TestCase
4 | # test "the truth" do
5 | # assert true
6 | # end
7 | end
8 |
--------------------------------------------------------------------------------
/src/content/tutorial/8-controllers/10-crud-destroy/_files/workspace/store/app/views/products/edit.html.erb:
--------------------------------------------------------------------------------
1 | Edit product
2 |
3 | <%= render "form", product: @product %>
4 | <%= link_to "Cancel", @product %>
5 |
--------------------------------------------------------------------------------
/src/content/tutorial/8-controllers/10-crud-destroy/_files/workspace/store/app/views/products/new.html.erb:
--------------------------------------------------------------------------------
1 | New product
2 |
3 | <%= render "form", product: @product %>
4 | <%= link_to "Cancel", products_path %>
5 |
--------------------------------------------------------------------------------
/src/templates/active-storage/workspace/store/app/models/product.rb:
--------------------------------------------------------------------------------
1 | class Product < ApplicationRecord
2 | has_one_attached :featured_image
3 | has_rich_text :description
4 | validates :name, presence: true
5 | end
6 |
--------------------------------------------------------------------------------
/src/templates/create-products/workspace/store/test/models/product_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 |
3 | class ProductTest < ActiveSupport::TestCase
4 | # test "the truth" do
5 | # assert true
6 | # end
7 | end
8 |
--------------------------------------------------------------------------------
/src/content/tutorial/14-in-stock-notifications/5-unsubscribe-links/_files/workspace/store/app/views/product_mailer/in_stock.text.erb:
--------------------------------------------------------------------------------
1 | Good news!
2 |
3 | <%= @product.name %> is back in stock.
4 | <%= product_url(@product) %>
5 |
--------------------------------------------------------------------------------
/src/content/tutorial/8-controllers/4-crud-show/_files/workspace/store/app/controllers/products_controller.rb:
--------------------------------------------------------------------------------
1 | class ProductsController < ApplicationController
2 | def index
3 | @products = Product.all
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/src/content/tutorial/14-in-stock-notifications/4-extracting-a-concern/_files/workspace/store/app/views/product_mailer/in_stock.text.erb:
--------------------------------------------------------------------------------
1 | Good news!
2 |
3 | <%= @product.name %> is back in stock.
4 | <%= product_url(@product) %>
5 |
--------------------------------------------------------------------------------
/src/content/tutorial/8-controllers/4-crud-show/_files/workspace/store/config/routes.rb:
--------------------------------------------------------------------------------
1 | Rails.application.routes.draw do
2 | resources :products
3 |
4 | # Defines the root path route ("/")
5 | root "products#index"
6 | end
7 |
--------------------------------------------------------------------------------
/src/content/tutorial/8-controllers/7-crud-edit/_files/workspace/store/config/routes.rb:
--------------------------------------------------------------------------------
1 | Rails.application.routes.draw do
2 | resources :products
3 |
4 | # Defines the root path route ("/")
5 | root "products#index"
6 | end
7 |
--------------------------------------------------------------------------------
/src/templates/authentication/workspace/store/app/views/passwords_mailer/reset.text.erb:
--------------------------------------------------------------------------------
1 | You can reset your password within the next 15 minutes on this password reset page:
2 | <%= edit_password_url(@user.password_reset_token) %>
3 |
--------------------------------------------------------------------------------
/src/templates/rails-new/workspace/store/config.ru:
--------------------------------------------------------------------------------
1 | # This file is used by Rack-based servers to start the application.
2 |
3 | require_relative "config/environment"
4 |
5 | run Rails.application
6 | Rails.application.load_server
7 |
--------------------------------------------------------------------------------
/src/templates/stock-notifications/workspace/store/test/models/subscriber_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 |
3 | class SubscriberTest < ActiveSupport::TestCase
4 | # test "the truth" do
5 | # assert true
6 | # end
7 | end
8 |
--------------------------------------------------------------------------------
/icons/languages/markdown.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/content/tutorial/10-caching/meta.md:
--------------------------------------------------------------------------------
1 | ---
2 | type: part
3 | title: Caching
4 | prepareCommands:
5 | - ['npm install', 'Preparing Ruby runtime']
6 | - ['node scripts/rails.js db:prepare', 'Prepare development database']
7 | ---
8 |
--------------------------------------------------------------------------------
/src/content/tutorial/16-testing/2-testing-emails/_files/workspace/store/test/fixtures/subscribers.yml:
--------------------------------------------------------------------------------
1 | david:
2 | product: tshirt
3 | email: david@example.org
4 |
5 | chris:
6 | product: tshirt
7 | email: chris@example.org
8 |
--------------------------------------------------------------------------------
/src/content/tutorial/8-controllers/10-crud-destroy/_files/workspace/store/config/routes.rb:
--------------------------------------------------------------------------------
1 | Rails.application.routes.draw do
2 | resources :products
3 |
4 | # Defines the root path route ("/")
5 | root "products#index"
6 | end
7 |
--------------------------------------------------------------------------------
/src/content/tutorial/8-controllers/5-crud-create/_files/workspace/store/config/routes.rb:
--------------------------------------------------------------------------------
1 | Rails.application.routes.draw do
2 | resources :products
3 |
4 | # Defines the root path route ("/")
5 | root "products#index"
6 | end
7 |
--------------------------------------------------------------------------------
/src/content/tutorial/8-controllers/8-before-actions/_files/workspace/store/config/routes.rb:
--------------------------------------------------------------------------------
1 | Rails.application.routes.draw do
2 | resources :products
3 |
4 | # Defines the root path route ("/")
5 | root "products#index"
6 | end
7 |
--------------------------------------------------------------------------------
/src/templates/create-products/workspace/store/test/fixtures/products.yml:
--------------------------------------------------------------------------------
1 | # Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
2 |
3 | one:
4 | name: MyString
5 |
6 | two:
7 | name: MyString
8 |
--------------------------------------------------------------------------------
/src/content/tutorial/11-rich-text/meta.md:
--------------------------------------------------------------------------------
1 | ---
2 | type: part
3 | title: Rich Text
4 | prepareCommands:
5 | - ['npm install', 'Preparing Ruby runtime']
6 | - ['node scripts/rails.js db:prepare', 'Prepare development database']
7 | ---
8 |
--------------------------------------------------------------------------------
/src/content/tutorial/14-in-stock-notifications/5-unsubscribe-links/_files/workspace/store/app/views/product_mailer/in_stock.html.erb:
--------------------------------------------------------------------------------
1 | Good news!
2 |
3 | <%= link_to @product.name, product_url(@product) %> is back in stock.
4 |
--------------------------------------------------------------------------------
/src/content/tutorial/8-controllers/1-controllers-actions/_files/workspace/store/config/routes.rb:
--------------------------------------------------------------------------------
1 | Rails.application.routes.draw do
2 | resources :products
3 |
4 | # Defines the root path route ("/")
5 | # root "posts#index"
6 | end
7 |
--------------------------------------------------------------------------------
/src/content/tutorial/8-controllers/10-crud-destroy/_files/workspace/store/app/views/products/show.html.erb:
--------------------------------------------------------------------------------
1 | <%= @product.name %>
2 |
3 | <%= link_to "Back", products_path %>
4 | <%= link_to "Edit", edit_product_path(@product) %>
5 |
--------------------------------------------------------------------------------
/src/content/tutorial/8-controllers/3-instance-variables/_files/workspace/store/config/routes.rb:
--------------------------------------------------------------------------------
1 | Rails.application.routes.draw do
2 | resources :products
3 |
4 | # Defines the root path route ("/")
5 | root "products#index"
6 | end
7 |
--------------------------------------------------------------------------------
/src/content/tutorial/8-controllers/6-strong-parameters/_files/workspace/store/config/routes.rb:
--------------------------------------------------------------------------------
1 | Rails.application.routes.draw do
2 | resources :products
3 |
4 | # Defines the root path route ("/")
5 | root "products#index"
6 | end
7 |
--------------------------------------------------------------------------------
/src/content/tutorial/8-controllers/8-before-actions/_files/workspace/store/app/views/products/show.html.erb:
--------------------------------------------------------------------------------
1 | <%= @product.name %>
2 |
3 | <%= link_to "Back", products_path %>
4 | <%= link_to "Edit", edit_product_path(@product) %>
5 |
--------------------------------------------------------------------------------
/src/content/tutorial/8-controllers/9-extracting-partials/_files/workspace/store/config/routes.rb:
--------------------------------------------------------------------------------
1 | Rails.application.routes.draw do
2 | resources :products
3 |
4 | # Defines the root path route ("/")
5 | root "products#index"
6 | end
7 |
--------------------------------------------------------------------------------
/src/templates/rails-new/workspace/store/app/javascript/application.js:
--------------------------------------------------------------------------------
1 | // Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails
2 |
3 | import "@hotwired/turbo-rails"
4 | import "controllers"
5 |
--------------------------------------------------------------------------------
/src/content/tutorial/12-file-uploads/meta.md:
--------------------------------------------------------------------------------
1 | ---
2 | type: part
3 | title: File Uploads
4 | prepareCommands:
5 | - ['npm install', 'Preparing Ruby runtime']
6 | - ['node scripts/rails.js db:prepare', 'Prepare development database']
7 | ---
8 |
--------------------------------------------------------------------------------
/src/content/tutorial/14-in-stock-notifications/4-extracting-a-concern/_files/workspace/store/app/views/product_mailer/in_stock.html.erb:
--------------------------------------------------------------------------------
1 | Good news!
2 |
3 | <%= link_to @product.name, product_url(@product) %> is back in stock.
4 |
--------------------------------------------------------------------------------
/src/content/tutorial/8-controllers/9-extracting-partials/_files/workspace/store/app/views/products/show.html.erb:
--------------------------------------------------------------------------------
1 | <%= @product.name %>
2 |
3 | <%= link_to "Back", products_path %>
4 | <%= link_to "Edit", edit_product_path(@product) %>
5 |
--------------------------------------------------------------------------------
/src/content/tutorial/4-database/2-database-migrations/_files/workspace/store/test/models/product_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 |
3 | class ProductTest < ActiveSupport::TestCase
4 | # test "the truth" do
5 | # assert true
6 | # end
7 | end
8 |
--------------------------------------------------------------------------------
/src/templates/authentication/workspace/store/app/mailers/passwords_mailer.rb:
--------------------------------------------------------------------------------
1 | class PasswordsMailer < ApplicationMailer
2 | def reset(user)
3 | @user = user
4 | mail subject: "Reset your password", to: user.email_address
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/src/templates/authentication/workspace/store/app/models/user.rb:
--------------------------------------------------------------------------------
1 | class User < ApplicationRecord
2 | has_secure_password
3 | has_many :sessions, dependent: :destroy
4 |
5 | normalizes :email_address, with: ->(e) { e.strip.downcase }
6 | end
7 |
--------------------------------------------------------------------------------
/src/content/tutorial/7-request-journey/5-rails-routes-2/_files/workspace/store/config/routes.rb:
--------------------------------------------------------------------------------
1 | Rails.application.routes.draw do
2 | get "/products", to: "products#index"
3 |
4 | # Defines the root path route ("/")
5 | # root "posts#index"
6 | end
7 |
--------------------------------------------------------------------------------
/src/content/tutorial/5-console/meta.md:
--------------------------------------------------------------------------------
1 | ---
2 | type: part
3 | title: Rails Console
4 | editor: false
5 | prepareCommands:
6 | - ['npm install', 'Preparing Ruby runtime']
7 | - ['node scripts/rails.js db:prepare', 'Prepare development database']
8 | ---
9 |
--------------------------------------------------------------------------------
/src/content/tutorial/6-active-record/meta.md:
--------------------------------------------------------------------------------
1 | ---
2 | type: part
3 | title: Active Record Model Basics
4 | prepareCommands:
5 | - ['npm install', 'Preparing Ruby runtime']
6 | - ['node scripts/rails.js db:prepare', 'Prepare development database']
7 | ---
8 |
--------------------------------------------------------------------------------
/src/templates/authentication/workspace/store/app/views/passwords_mailer/reset.html.erb:
--------------------------------------------------------------------------------
1 |
2 | You can reset your password within the next 15 minutes on
3 | <%= link_to "this password reset page", edit_password_url(@user.password_reset_token) %>.
4 |
5 |
--------------------------------------------------------------------------------
/src/templates/products-controller/workspace/store/test/controllers/products_controller_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 |
3 | class ProductsControllerTest < ActionDispatch::IntegrationTest
4 | # test "the truth" do
5 | # assert true
6 | # end
7 | end
8 |
--------------------------------------------------------------------------------
/src/content/tutorial/4-database/2-database-migrations/_files/workspace/store/test/fixtures/products.yml:
--------------------------------------------------------------------------------
1 | # Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
2 |
3 | one:
4 | name: MyString
5 |
6 | two:
7 | name: MyString
8 |
--------------------------------------------------------------------------------
/src/content/tutorial/4-database/meta.md:
--------------------------------------------------------------------------------
1 | ---
2 | type: part
3 | title: Creating a Database Model
4 | prepareCommands:
5 | - ['npm install', 'Preparing Ruby runtime']
6 | - ['node scripts/createdb.js store_development', 'Create development database']
7 | ---
8 |
--------------------------------------------------------------------------------
/rails-wasm/.railsrc:
--------------------------------------------------------------------------------
1 | --skip-bundle
2 | --skip-git
3 | --skip-decrypted-diffs
4 | --skip-bootsnap
5 | --skip-brakeman
6 | --skip-dev-gems
7 | --skip-kamal
8 | --skip-thruster
9 | --skip-docker
10 | --skip-system-test
11 | --skip-dev-gems
12 | --skip-rubocop
13 |
--------------------------------------------------------------------------------
/src/content/tutorial/14-in-stock-notifications/5-unsubscribe-links/_files/workspace/store/test/models/subscriber_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 |
3 | class SubscriberTest < ActiveSupport::TestCase
4 | # test "the truth" do
5 | # assert true
6 | # end
7 | end
8 |
--------------------------------------------------------------------------------
/src/content/tutorial/7-request-journey/meta.md:
--------------------------------------------------------------------------------
1 | ---
2 | type: part
3 | title: A Request's Journey Through Rails
4 | prepareCommands:
5 | - ['npm install', 'Preparing Ruby runtime']
6 | - ['node scripts/rails.js db:prepare', 'Prepare development database']
7 | ---
8 |
--------------------------------------------------------------------------------
/src/templates/authentication/workspace/store/config/routes.rb:
--------------------------------------------------------------------------------
1 | Rails.application.routes.draw do
2 | resource :session
3 | resources :passwords, param: :token
4 | resources :products
5 |
6 | # Defines the root path route ("/")
7 | root "products#index"
8 | end
9 |
--------------------------------------------------------------------------------
/src/content/tutorial/14-in-stock-notifications/4-extracting-a-concern/_files/workspace/store/test/models/subscriber_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 |
3 | class SubscriberTest < ActiveSupport::TestCase
4 | # test "the truth" do
5 | # assert true
6 | # end
7 | end
8 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "0.2.0",
3 | "configurations": [
4 | {
5 | "command": "./node_modules/.bin/astro dev",
6 | "name": "Development Server",
7 | "request": "launch",
8 | "type": "node-terminal"
9 | }
10 | ]
11 | }
12 |
--------------------------------------------------------------------------------
/src/content/tutorial/14-in-stock-notifications/3-in-stock-email-notifications/_files/workspace/store/test/models/subscriber_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 |
3 | class SubscriberTest < ActiveSupport::TestCase
4 | # test "the truth" do
5 | # assert true
6 | # end
7 | end
8 |
--------------------------------------------------------------------------------
/src/templates/default/bin/console:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env -S node --disable-warning=ExperimentalWarning
2 |
3 | import initVM from "../lib/rails.js";
4 | import IRBRepl from "../lib/irb.js";
5 |
6 | const vm = await initVM();
7 | const irb = new IRBRepl(vm);
8 |
9 | irb.start();
10 |
--------------------------------------------------------------------------------
/src/content/tutorial/8-controllers/4-crud-show/_files/workspace/store/app/views/products/index.html.erb:
--------------------------------------------------------------------------------
1 | Products
2 |
3 |
4 | <% @products.each do |product| %>
5 |
6 | <%= product.name %>
7 |
8 | <% end %>
9 |
10 |
--------------------------------------------------------------------------------
/src/templates/rails-new/workspace/store/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: store_production
11 |
--------------------------------------------------------------------------------
/src/templates/stock-notifications/workspace/store/test/fixtures/subscribers.yml:
--------------------------------------------------------------------------------
1 | # Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
2 |
3 | one:
4 | product: one
5 | email: MyString
6 |
7 | two:
8 | product: two
9 | email: MyString
10 |
--------------------------------------------------------------------------------
/src/content/config.ts:
--------------------------------------------------------------------------------
1 | import { contentSchema } from '@tutorialkit/types';
2 | import { defineCollection } from 'astro:content';
3 |
4 | const tutorial = defineCollection({
5 | type: 'content',
6 | schema: contentSchema,
7 | });
8 |
9 | export const collections = { tutorial };
10 |
--------------------------------------------------------------------------------
/src/templates/active-storage/workspace/store/app/javascript/application.js:
--------------------------------------------------------------------------------
1 | // Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails
2 |
3 | import "@hotwired/turbo-rails"
4 | import "controllers"
5 | import "trix"
6 | import "@rails/actiontext"
7 |
--------------------------------------------------------------------------------
/src/templates/create-products/workspace/store/db/migrate/20250521010850_create_products.rb:
--------------------------------------------------------------------------------
1 | class CreateProducts < ActiveRecord::Migration[8.0]
2 | def change
3 | create_table :products do |t|
4 | t.string :name
5 |
6 | t.timestamps
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/src/templates/default/scripts/createdb.js:
--------------------------------------------------------------------------------
1 | import { PGLite4Rails } from "../lib/database.js";
2 |
3 | const pgDataDir = new URL("../pgdata", import.meta.url).pathname;
4 | const pglite = new PGLite4Rails(pgDataDir);
5 |
6 | const dbname = process.argv[2];
7 | pglite.create_interface(dbname);
8 |
--------------------------------------------------------------------------------
/src/templates/stock-notifications/workspace/store/app/views/product_mailer/in_stock.text.erb:
--------------------------------------------------------------------------------
1 | Good news!
2 |
3 | <%= @product.name %> is back in stock.
4 | <%= product_url(@product) %>
5 |
6 | Unsubscribe: <%= unsubscribe_url(token: params[:subscriber].generate_token_for(:unsubscribe)) %>
7 |
--------------------------------------------------------------------------------
/src/templates/stock-notifications/workspace/store/db/migrate/20250620213158_add_inventory_count_to_products.rb:
--------------------------------------------------------------------------------
1 | class AddInventoryCountToProducts < ActiveRecord::Migration[8.0]
2 | def change
3 | add_column :products, :inventory_count, :integer, default: 0, null: false
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/icons/languages/css.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/templates/crud-products/workspace/store/app/views/products/_form.html.erb:
--------------------------------------------------------------------------------
1 | <%= form_with model: product do |form| %>
2 |
3 | <%= form.label :name %>
4 | <%= form.text_field :name %>
5 |
6 |
7 |
8 | <%= form.submit %>
9 |
10 | <% end %>
11 |
--------------------------------------------------------------------------------
/src/templates/rails-new/workspace/store/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 |
--------------------------------------------------------------------------------
/src/templates/rails-new/workspace/store/app/controllers/application_controller.rb:
--------------------------------------------------------------------------------
1 | class ApplicationController < ActionController::Base
2 | # Only allow modern browsers supporting webp images, web push, badges, import maps, CSS nesting, and CSS :has.
3 | allow_browser versions: :modern
4 | end
5 |
--------------------------------------------------------------------------------
/src/templates/crud-products/workspace/store/app/views/products/show.html.erb:
--------------------------------------------------------------------------------
1 | <%= @product.name %>
2 |
3 | <%= link_to "Back", products_path %>
4 | <%= link_to "Edit", edit_product_path(@product) %>
5 | <%= button_to "Delete", @product, method: :delete, data: { turbo_confirm: "Are you sure?" } %>
6 |
--------------------------------------------------------------------------------
/src/content/tutorial/16-testing/meta.md:
--------------------------------------------------------------------------------
1 | ---
2 | type: part
3 | title: Testing
4 | editor:
5 | fileTree:
6 | allowEdits: "/workspace/store/**"
7 | prepareCommands:
8 | - ['npm install', 'Preparing Ruby runtime']
9 | - ['node scripts/rails.js db:prepare', 'Prepare development database']
10 | ---
11 |
--------------------------------------------------------------------------------
/src/content/tutorial/8-controllers/5-crud-create/_files/workspace/store/app/controllers/products_controller.rb:
--------------------------------------------------------------------------------
1 | class ProductsController < ApplicationController
2 | def index
3 | @products = Product.all
4 | end
5 |
6 | def show
7 | @product = Product.find(params[:id])
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/src/templates/stock-notifications/workspace/store/app/views/product_mailer/in_stock.html.erb:
--------------------------------------------------------------------------------
1 | Good news!
2 |
3 | <%= link_to @product.name, product_url(@product) %> is back in stock.
4 |
5 | <%= link_to "Unsubscribe", unsubscribe_url(token: params[:subscriber].generate_token_for(:unsubscribe)) %>
6 |
--------------------------------------------------------------------------------
/src/content/tutorial/17-whats-next/meta.md:
--------------------------------------------------------------------------------
1 | ---
2 | type: part
3 | title: What's Next?
4 | editor:
5 | fileTree:
6 | allowEdits: "/workspace/store/**"
7 | prepareCommands:
8 | - ['npm install', 'Preparing Ruby runtime']
9 | - ['node scripts/rails.js db:prepare', 'Prepare development database']
10 | ---
11 |
--------------------------------------------------------------------------------
/src/content/tutorial/4-database/2-database-migrations/_files/workspace/store/db/migrate/20250521010850_create_products.rb:
--------------------------------------------------------------------------------
1 | class CreateProducts < ActiveRecord::Migration[8.0]
2 | def change
3 | create_table :products do |t|
4 | t.string :name
5 |
6 | t.timestamps
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/src/content/tutorial/8-controllers/5-crud-create/_files/workspace/store/app/views/products/index.html.erb:
--------------------------------------------------------------------------------
1 | Products
2 |
3 |
4 | <% @products.each do |product| %>
5 |
6 | <%= link_to product.name, product_path(product.id) %>
7 |
8 | <% end %>
9 |
10 |
--------------------------------------------------------------------------------
/src/content/tutorial/8-controllers/meta.md:
--------------------------------------------------------------------------------
1 | ---
2 | type: part
3 | title: Controllers
4 | editor:
5 | fileTree:
6 | allowEdits: "/workspace/store/**"
7 | prepareCommands:
8 | - ['npm install', 'Preparing Ruby runtime']
9 | - ['node scripts/rails.js db:prepare', 'Prepare development database']
10 | ---
11 |
--------------------------------------------------------------------------------
/icons/languages/ruby.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/content/tutorial/14-in-stock-notifications/5-unsubscribe-links/_files/workspace/store/test/fixtures/subscribers.yml:
--------------------------------------------------------------------------------
1 | # Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
2 |
3 | one:
4 | product: one
5 | email: MyString
6 |
7 | two:
8 | product: two
9 | email: MyString
10 |
--------------------------------------------------------------------------------
/src/content/tutorial/15-css-and-js/meta.md:
--------------------------------------------------------------------------------
1 | ---
2 | type: part
3 | title: CSS & JavaScript
4 | editor:
5 | fileTree:
6 | allowEdits: "/workspace/store/**"
7 | prepareCommands:
8 | - ['npm install', 'Preparing Ruby runtime']
9 | - ['node scripts/rails.js db:prepare', 'Prepare development database']
10 | ---
11 |
--------------------------------------------------------------------------------
/src/content/tutorial/8-controllers/10-crud-destroy/_files/workspace/store/app/views/products/_form.html.erb:
--------------------------------------------------------------------------------
1 | <%= form_with model: product do |form| %>
2 |
3 | <%= form.label :name %>
4 | <%= form.text_field :name %>
5 |
6 |
7 |
8 | <%= form.submit %>
9 |
10 | <% end %>
11 |
--------------------------------------------------------------------------------
/src/content/tutorial/9-authentication/meta.md:
--------------------------------------------------------------------------------
1 | ---
2 | type: part
3 | title: Authentication
4 | editor:
5 | fileTree:
6 | allowEdits: "/workspace/store/**"
7 | prepareCommands:
8 | - ['npm install', 'Preparing Ruby runtime']
9 | - ['node scripts/rails.js db:prepare', 'Prepare development database']
10 | ---
11 |
--------------------------------------------------------------------------------
/src/content/tutorial/14-in-stock-notifications/2-adding-subscribers/_files/workspace/store/db/migrate/20250620213158_add_inventory_count_to_products.rb:
--------------------------------------------------------------------------------
1 | class AddInventoryCountToProducts < ActiveRecord::Migration[8.0]
2 | def change
3 | add_column :products, :inventory_count, :integer, default: 0, null: false
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/src/content/tutorial/14-in-stock-notifications/4-extracting-a-concern/_files/workspace/store/test/fixtures/subscribers.yml:
--------------------------------------------------------------------------------
1 | # Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
2 |
3 | one:
4 | product: one
5 | email: MyString
6 |
7 | two:
8 | product: two
9 | email: MyString
10 |
--------------------------------------------------------------------------------
/src/content/tutorial/14-in-stock-notifications/5-unsubscribe-links/_files/workspace/store/db/migrate/20250620213158_add_inventory_count_to_products.rb:
--------------------------------------------------------------------------------
1 | class AddInventoryCountToProducts < ActiveRecord::Migration[8.0]
2 | def change
3 | add_column :products, :inventory_count, :integer, default: 0, null: false
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/src/templates/authentication/workspace/store/app/controllers/application_controller.rb:
--------------------------------------------------------------------------------
1 | class ApplicationController < ActionController::Base
2 | include Authentication
3 | # Only allow modern browsers supporting webp images, web push, badges, import maps, CSS nesting, and CSS :has.
4 | allow_browser versions: :modern
5 | end
6 |
--------------------------------------------------------------------------------
/src/templates/authentication/workspace/store/test/fixtures/users.yml:
--------------------------------------------------------------------------------
1 | <% password_digest = BCrypt::Password.create("password") %>
2 |
3 | one:
4 | email_address: one@example.com
5 | password_digest: <%= password_digest %>
6 |
7 | two:
8 | email_address: two@example.com
9 | password_digest: <%= password_digest %>
10 |
--------------------------------------------------------------------------------
/src/content/tutorial/14-in-stock-notifications/4-extracting-a-concern/_files/workspace/store/db/migrate/20250620213158_add_inventory_count_to_products.rb:
--------------------------------------------------------------------------------
1 | class AddInventoryCountToProducts < ActiveRecord::Migration[8.0]
2 | def change
3 | add_column :products, :inventory_count, :integer, default: 0, null: false
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/src/content/tutorial/13-internationalization/meta.md:
--------------------------------------------------------------------------------
1 | ---
2 | type: part
3 | title: Internationalization
4 | editor:
5 | fileTree:
6 | allowEdits: "/workspace/store/**"
7 | prepareCommands:
8 | - ['npm install', 'Preparing Ruby runtime']
9 | - ['node scripts/rails.js db:prepare', 'Prepare development database']
10 | ---
11 |
--------------------------------------------------------------------------------
/src/content/tutorial/14-in-stock-notifications/3-in-stock-email-notifications/_files/workspace/store/db/migrate/20250620213158_add_inventory_count_to_products.rb:
--------------------------------------------------------------------------------
1 | class AddInventoryCountToProducts < ActiveRecord::Migration[8.0]
2 | def change
3 | add_column :products, :inventory_count, :integer, default: 0, null: false
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/src/content/tutorial/14-in-stock-notifications/3-in-stock-email-notifications/_files/workspace/store/test/fixtures/subscribers.yml:
--------------------------------------------------------------------------------
1 | # Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
2 |
3 | one:
4 | product: one
5 | email: MyString
6 |
7 | two:
8 | product: two
9 | email: MyString
10 |
--------------------------------------------------------------------------------
/src/content/tutorial/14-in-stock-notifications/meta.md:
--------------------------------------------------------------------------------
1 | ---
2 | type: part
3 | title: In Stock Notifications
4 | editor:
5 | fileTree:
6 | allowEdits: "/workspace/store/**"
7 | prepareCommands:
8 | - ['npm install', 'Preparing Ruby runtime']
9 | - ['node scripts/rails.js db:prepare', 'Prepare development database']
10 | ---
11 |
--------------------------------------------------------------------------------
/src/content/tutorial/9-authentication/2-adding-log-out/_files/workspace/store/app/views/products/show.html.erb:
--------------------------------------------------------------------------------
1 | <%= @product.name %>
2 |
3 | <%= link_to "Back", products_path %>
4 | <%= link_to "Edit", edit_product_path(@product) %>
5 | <%= button_to "Delete", @product, method: :delete, data: { turbo_confirm: "Are you sure?" } %>
6 |
--------------------------------------------------------------------------------
/src/templates/rails-new/workspace/store/app/javascript/controllers/application.js:
--------------------------------------------------------------------------------
1 | import { Application } from "@hotwired/stimulus"
2 |
3 | const application = Application.start()
4 |
5 | // Configure Stimulus development experience
6 | application.debug = false
7 | window.Stimulus = application
8 |
9 | export { application }
10 |
--------------------------------------------------------------------------------
/src/content/tutorial/14-in-stock-notifications/2-adding-subscribers/_files/workspace/store/app/models/product.rb:
--------------------------------------------------------------------------------
1 | class Product < ApplicationRecord
2 | has_one_attached :featured_image
3 | has_rich_text :description
4 |
5 | validates :name, presence: true
6 | validates :inventory_count, numericality: { greater_than_or_equal_to: 0 }
7 | end
8 |
--------------------------------------------------------------------------------
/src/content/tutorial/9-authentication/3-unauthenticated-access/_files/workspace/store/app/views/products/show.html.erb:
--------------------------------------------------------------------------------
1 | <%= @product.name %>
2 |
3 | <%= link_to "Back", products_path %>
4 | <%= link_to "Edit", edit_product_path(@product) %>
5 | <%= button_to "Delete", @product, method: :delete, data: { turbo_confirm: "Are you sure?" } %>
6 |
--------------------------------------------------------------------------------
/src/content/tutorial/9-authentication/4-authenticated-links/_files/workspace/store/app/views/products/show.html.erb:
--------------------------------------------------------------------------------
1 | <%= @product.name %>
2 |
3 | <%= link_to "Back", products_path %>
4 | <%= link_to "Edit", edit_product_path(@product) %>
5 | <%= button_to "Delete", @product, method: :delete, data: { turbo_confirm: "Are you sure?" } %>
6 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "astro/tsconfigs/strict",
3 | "compilerOptions": {
4 | "jsx": "react-jsx",
5 | "baseUrl": "./",
6 | "jsxImportSource": "react",
7 | "paths": {
8 | "@*": [
9 | "src/*"
10 | ]
11 | }
12 | },
13 | "exclude": [
14 | "dist",
15 | "vendor"
16 | ]
17 | }
18 |
--------------------------------------------------------------------------------
/src/content/tutorial/8-controllers/7-crud-edit/_files/workspace/store/app/views/products/new.html.erb:
--------------------------------------------------------------------------------
1 | New product
2 |
3 | <%= form_with model: @product do |form| %>
4 |
5 | <%= form.label :name %>
6 | <%= form.text_field :name %>
7 |
8 |
9 |
10 | <%= form.submit %>
11 |
12 | <% end %>
13 |
--------------------------------------------------------------------------------
/src/templates/authentication/workspace/store/app/views/products/show.html.erb:
--------------------------------------------------------------------------------
1 | <%= @product.name %>
2 |
3 | <%= link_to "Back", products_path %>
4 | <% if authenticated? %>
5 | <%= link_to "Edit", edit_product_path(@product) %>
6 | <%= button_to "Delete", @product, method: :delete, data: { turbo_confirm: "Are you sure?" } %>
7 | <% end %>
8 |
--------------------------------------------------------------------------------
/src/templates/crud-products/workspace/store/app/views/products/index.html.erb:
--------------------------------------------------------------------------------
1 | Products
2 |
3 | <%= link_to "New product", new_product_path %>
4 |
5 |
6 | <% @products.each do |product| %>
7 |
8 | <%= link_to product.name, product_path(product.id) %>
9 |
10 | <% end %>
11 |
12 |
--------------------------------------------------------------------------------
/src/templates/stock-notifications/workspace/store/app/models/product.rb:
--------------------------------------------------------------------------------
1 | class Product < ApplicationRecord
2 | include Notifications
3 |
4 | has_one_attached :featured_image
5 | has_rich_text :description
6 |
7 | validates :name, presence: true
8 | validates :inventory_count, numericality: { greater_than_or_equal_to: 0 }
9 | end
10 |
--------------------------------------------------------------------------------
/src/content/tutorial/8-controllers/8-before-actions/_files/workspace/store/app/views/products/new.html.erb:
--------------------------------------------------------------------------------
1 | New product
2 |
3 | <%= form_with model: @product do |form| %>
4 |
5 | <%= form.label :name %>
6 | <%= form.text_field :name %>
7 |
8 |
9 |
10 | <%= form.submit %>
11 |
12 | <% end %>
13 |
--------------------------------------------------------------------------------
/src/templates/rails-new/workspace/store/app/javascript/controllers/index.js:
--------------------------------------------------------------------------------
1 | // Import and register all your controllers from the importmap via controllers/**/*_controller
2 | import { application } from "controllers/application"
3 | import { eagerLoadControllersFrom } from "@hotwired/stimulus-loading"
4 | eagerLoadControllersFrom("controllers", application)
5 |
--------------------------------------------------------------------------------
/src/content/tutorial/8-controllers/6-strong-parameters/_files/workspace/store/app/views/products/new.html.erb:
--------------------------------------------------------------------------------
1 | New product
2 |
3 | <%= form_with model: @product do |form| %>
4 |
5 | <%= form.label :name %>
6 | <%= form.text_field :name %>
7 |
8 |
9 |
10 | <%= form.submit %>
11 |
12 | <% end %>
13 |
--------------------------------------------------------------------------------
/src/content/tutorial/8-controllers/9-extracting-partials/_files/workspace/store/app/views/products/new.html.erb:
--------------------------------------------------------------------------------
1 | New product
2 |
3 | <%= form_with model: @product do |form| %>
4 |
5 | <%= form.label :name %>
6 | <%= form.text_field :name %>
7 |
8 |
9 |
10 | <%= form.submit %>
11 |
12 | <% end %>
13 |
--------------------------------------------------------------------------------
/src/templates/rails-new/workspace/store/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 |
--------------------------------------------------------------------------------
/src/templates/rails-new/workspace/store/app/views/layouts/mailer.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
8 |
9 |
10 |
11 | <%= yield %>
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/templates/stock-notifications/workspace/store/db/migrate/20250620214344_create_subscribers.rb:
--------------------------------------------------------------------------------
1 | class CreateSubscribers < ActiveRecord::Migration[8.0]
2 | def change
3 | create_table :subscribers do |t|
4 | t.belongs_to :product, null: false, foreign_key: true
5 | t.string :email
6 |
7 | t.timestamps
8 | end
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/src/templates/authentication/workspace/store/app/views/products/index.html.erb:
--------------------------------------------------------------------------------
1 | Products
2 |
3 | <%= link_to "New product", new_product_path if authenticated? %>
4 |
5 |
6 | <% @products.each do |product| %>
7 |
8 | <%= link_to product.name, product_path(product.id) %>
9 |
10 | <% end %>
11 |
12 |
--------------------------------------------------------------------------------
/src/content/tutorial/8-controllers/7-crud-edit/_files/workspace/store/app/views/products/index.html.erb:
--------------------------------------------------------------------------------
1 | Products
2 |
3 | <%= link_to "New product", new_product_path %>
4 |
5 |
6 | <% @products.each do |product| %>
7 |
8 | <%= link_to product.name, product_path(product.id) %>
9 |
10 | <% end %>
11 |
12 |
--------------------------------------------------------------------------------
/src/content/tutorial/8-controllers/10-crud-destroy/_files/workspace/store/app/views/products/index.html.erb:
--------------------------------------------------------------------------------
1 | Products
2 |
3 | <%= link_to "New product", new_product_path %>
4 |
5 |
6 | <% @products.each do |product| %>
7 |
8 | <%= link_to product.name, product_path(product.id) %>
9 |
10 | <% end %>
11 |
12 |
--------------------------------------------------------------------------------
/src/content/tutorial/8-controllers/8-before-actions/_files/workspace/store/app/views/products/index.html.erb:
--------------------------------------------------------------------------------
1 | Products
2 |
3 | <%= link_to "New product", new_product_path %>
4 |
5 |
6 | <% @products.each do |product| %>
7 |
8 | <%= link_to product.name, product_path(product.id) %>
9 |
10 | <% end %>
11 |
12 |
--------------------------------------------------------------------------------
/src/content/tutorial/14-in-stock-notifications/5-unsubscribe-links/_files/workspace/store/config/routes.rb:
--------------------------------------------------------------------------------
1 | Rails.application.routes.draw do
2 | resource :session
3 | resources :passwords, param: :token
4 | resources :products do
5 | resources :subscribers, only: [ :create ]
6 | end
7 |
8 | # Defines the root path route ("/")
9 | root "products#index"
10 | end
11 |
--------------------------------------------------------------------------------
/src/content/tutorial/8-controllers/6-strong-parameters/_files/workspace/store/app/views/products/index.html.erb:
--------------------------------------------------------------------------------
1 | Products
2 |
3 | <%= link_to "New product", new_product_path %>
4 |
5 |
6 | <% @products.each do |product| %>
7 |
8 | <%= link_to product.name, product_path(product.id) %>
9 |
10 | <% end %>
11 |
12 |
--------------------------------------------------------------------------------
/src/content/tutorial/8-controllers/9-extracting-partials/_files/workspace/store/app/views/products/index.html.erb:
--------------------------------------------------------------------------------
1 | Products
2 |
3 | <%= link_to "New product", new_product_path %>
4 |
5 |
6 | <% @products.each do |product| %>
7 |
8 | <%= link_to product.name, product_path(product.id) %>
9 |
10 | <% end %>
11 |
12 |
--------------------------------------------------------------------------------
/src/content/tutorial/9-authentication/2-adding-log-out/_files/workspace/store/app/views/products/index.html.erb:
--------------------------------------------------------------------------------
1 | Products
2 |
3 | <%= link_to "New product", new_product_path %>
4 |
5 |
6 | <% @products.each do |product| %>
7 |
8 | <%= link_to product.name, product_path(product.id) %>
9 |
10 | <% end %>
11 |
12 |
--------------------------------------------------------------------------------
/src/templates/authentication/workspace/store/db/migrate/20250618233539_create_sessions.rb:
--------------------------------------------------------------------------------
1 | class CreateSessions < ActiveRecord::Migration[8.0]
2 | def change
3 | create_table :sessions do |t|
4 | t.references :user, null: false, foreign_key: true
5 | t.string :ip_address
6 | t.string :user_agent
7 |
8 | t.timestamps
9 | end
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/src/templates/rails-new/workspace/store/config/importmap.rb:
--------------------------------------------------------------------------------
1 | # Pin npm packages by running ./bin/importmap
2 |
3 | pin "application"
4 |
5 | pin "@hotwired/turbo-rails", to: "turbo.min.js"
6 | pin "@hotwired/stimulus", to: "stimulus.min.js"
7 | pin "@hotwired/stimulus-loading", to: "stimulus-loading.js"
8 |
9 | pin_all_from "app/javascript/controllers", under: "controllers"
10 |
--------------------------------------------------------------------------------
/src/templates/rails-new/workspace/store/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 |
--------------------------------------------------------------------------------
/src/content/tutorial/14-in-stock-notifications/4-extracting-a-concern/_files/workspace/store/config/routes.rb:
--------------------------------------------------------------------------------
1 | Rails.application.routes.draw do
2 | resource :session
3 | resources :passwords, param: :token
4 | resources :products do
5 | resources :subscribers, only: [ :create ]
6 | end
7 |
8 | # Defines the root path route ("/")
9 | root "products#index"
10 | end
11 |
--------------------------------------------------------------------------------
/src/content/tutorial/14-in-stock-notifications/5-unsubscribe-links/_files/workspace/store/app/models/product.rb:
--------------------------------------------------------------------------------
1 | class Product < ApplicationRecord
2 | include Notifications
3 |
4 | has_one_attached :featured_image
5 | has_rich_text :description
6 |
7 | validates :name, presence: true
8 | validates :inventory_count, numericality: { greater_than_or_equal_to: 0 }
9 | end
10 |
--------------------------------------------------------------------------------
/src/content/tutorial/9-authentication/3-unauthenticated-access/_files/workspace/store/app/views/products/index.html.erb:
--------------------------------------------------------------------------------
1 | Products
2 |
3 | <%= link_to "New product", new_product_path %>
4 |
5 |
6 | <% @products.each do |product| %>
7 |
8 | <%= link_to product.name, product_path(product.id) %>
9 |
10 | <% end %>
11 |
12 |
--------------------------------------------------------------------------------
/src/content/tutorial/9-authentication/4-authenticated-links/_files/workspace/store/app/views/products/index.html.erb:
--------------------------------------------------------------------------------
1 | Products
2 |
3 | <%= link_to "New product", new_product_path %>
4 |
5 |
6 | <% @products.each do |product| %>
7 |
8 | <%= link_to product.name, product_path(product.id) %>
9 |
10 | <% end %>
11 |
12 |
--------------------------------------------------------------------------------
/src/templates/authentication/workspace/store/test/mailers/previews/passwords_mailer_preview.rb:
--------------------------------------------------------------------------------
1 | # Preview all emails at http://localhost:3000/rails/mailers/passwords_mailer
2 | class PasswordsMailerPreview < ActionMailer::Preview
3 | # Preview this email at http://localhost:3000/rails/mailers/passwords_mailer/reset
4 | def reset
5 | PasswordsMailer.reset(User.take)
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/src/templates/stock-notifications/workspace/store/config/routes.rb:
--------------------------------------------------------------------------------
1 | Rails.application.routes.draw do
2 | resource :session
3 | resources :passwords, param: :token
4 | resources :products do
5 | resources :subscribers, only: [ :create ]
6 | end
7 | resource :unsubscribe, only: [ :show ]
8 |
9 | # Defines the root path route ("/")
10 | root "products#index"
11 | end
12 |
--------------------------------------------------------------------------------
/src/content/tutorial/14-in-stock-notifications/3-in-stock-email-notifications/_files/workspace/store/config/routes.rb:
--------------------------------------------------------------------------------
1 | Rails.application.routes.draw do
2 | resource :session
3 | resources :passwords, param: :token
4 | resources :products do
5 | resources :subscribers, only: [ :create ]
6 | end
7 |
8 | # Defines the root path route ("/")
9 | root "products#index"
10 | end
11 |
--------------------------------------------------------------------------------
/src/content/tutorial/14-in-stock-notifications/3-in-stock-email-notifications/_files/workspace/store/app/models/product.rb:
--------------------------------------------------------------------------------
1 | class Product < ApplicationRecord
2 | has_many :subscribers, dependent: :destroy
3 | has_one_attached :featured_image
4 | has_rich_text :description
5 |
6 | validates :name, presence: true
7 | validates :inventory_count, numericality: { greater_than_or_equal_to: 0 }
8 | end
9 |
--------------------------------------------------------------------------------
/src/content/tutorial/14-in-stock-notifications/5-unsubscribe-links/_files/workspace/store/db/migrate/20250620214344_create_subscribers.rb:
--------------------------------------------------------------------------------
1 | class CreateSubscribers < ActiveRecord::Migration[8.0]
2 | def change
3 | create_table :subscribers do |t|
4 | t.belongs_to :product, null: false, foreign_key: true
5 | t.string :email
6 |
7 | t.timestamps
8 | end
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/src/templates/authentication/workspace/store/db/migrate/20250618233538_create_users.rb:
--------------------------------------------------------------------------------
1 | class CreateUsers < ActiveRecord::Migration[8.0]
2 | def change
3 | create_table :users do |t|
4 | t.string :email_address, null: false
5 | t.string :password_digest, null: false
6 |
7 | t.timestamps
8 | end
9 | add_index :users, :email_address, unique: true
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/src/content/tutorial/14-in-stock-notifications/4-extracting-a-concern/_files/workspace/store/db/migrate/20250620214344_create_subscribers.rb:
--------------------------------------------------------------------------------
1 | class CreateSubscribers < ActiveRecord::Migration[8.0]
2 | def change
3 | create_table :subscribers do |t|
4 | t.belongs_to :product, null: false, foreign_key: true
5 | t.string :email
6 |
7 | t.timestamps
8 | end
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/src/templates/default/bin/rackup:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env -S node --disable-warning=ExperimentalWarning --watch --watch-path=./
2 |
3 | import initVM from "../lib/rails.js";
4 | import { createRackServer } from "../lib/server.js";
5 |
6 | const vm = await initVM();
7 | const server = await createRackServer(vm);
8 |
9 | server.listen(3000, () => {
10 | console.log("Server started on port 3000");
11 | });
12 |
--------------------------------------------------------------------------------
/src/templates/stock-notifications/workspace/store/app/mailers/product_mailer.rb:
--------------------------------------------------------------------------------
1 | class ProductMailer < ApplicationMailer
2 | # Subject can be set in your I18n file at config/locales/en.yml
3 | # with the following lookup:
4 | #
5 | # en.product_mailer.in_stock.subject
6 | #
7 | def in_stock
8 | @product = params[:product]
9 | mail to: params[:subscriber].email
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/src/content/tutorial/14-in-stock-notifications/3-in-stock-email-notifications/_files/workspace/store/db/migrate/20250620214344_create_subscribers.rb:
--------------------------------------------------------------------------------
1 | class CreateSubscribers < ActiveRecord::Migration[8.0]
2 | def change
3 | create_table :subscribers do |t|
4 | t.belongs_to :product, null: false, foreign_key: true
5 | t.string :email
6 |
7 | t.timestamps
8 | end
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/src/content/tutorial/11-rich-text/1-action-text/_files/workspace/store/app/views/products/show.html.erb:
--------------------------------------------------------------------------------
1 | <% cache @product do %>
2 | <%= @product.name %>
3 | <% end %>
4 |
5 | <%= link_to "Back", products_path %>
6 | <% if authenticated? %>
7 | <%= link_to "Edit", edit_product_path(@product) %>
8 | <%= button_to "Delete", @product, method: :delete, data: { turbo_confirm: "Are you sure?" } %>
9 | <% end %>
10 |
--------------------------------------------------------------------------------
/src/templates/authentication/workspace/store/db/seeds.rb:
--------------------------------------------------------------------------------
1 | # Create products that we should have created in the previous lessons
2 | Product.create(name: "T-Shirt").destroy
3 | Product.create(name: "Pants")
4 |
5 | # For authentication
6 | User.create!(
7 | email_address: "you@example.org",
8 | password: "s3cr3t",
9 | password_confirmation: "s3cr3t"
10 | ) unless User.where(email_address: "you@example.org").exists?
11 |
--------------------------------------------------------------------------------
/rails-wasm/Gemfile:
--------------------------------------------------------------------------------
1 | source "https://rubygems.org"
2 |
3 | gem "rails", "~> 8.0.0"
4 | gem "wasmify-rails", "~> 0.4.0"
5 |
6 | # rails new gems
7 | gem "propshaft"
8 | gem "importmap-rails"
9 | gem "turbo-rails"
10 | gem "stimulus-rails"
11 | gem "jbuilder"
12 |
13 | gem "bcrypt", "~> 3.1.7"
14 |
15 | gem "solid_cache"
16 | gem "solid_queue"
17 | gem "solid_cable"
18 |
19 | gem "image_processing", "~> 1.2"
20 |
21 | gem "tzinfo-data"
22 |
--------------------------------------------------------------------------------
/src/content/tutorial/14-in-stock-notifications/5-unsubscribe-links/_files/workspace/store/app/mailers/product_mailer.rb:
--------------------------------------------------------------------------------
1 | class ProductMailer < ApplicationMailer
2 | # Subject can be set in your I18n file at config/locales/en.yml
3 | # with the following lookup:
4 | #
5 | # en.product_mailer.in_stock.subject
6 | #
7 | def in_stock
8 | @product = params[:product]
9 | mail to: params[:subscriber].email
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/src/content/tutorial/14-in-stock-notifications/4-extracting-a-concern/_files/workspace/store/app/mailers/product_mailer.rb:
--------------------------------------------------------------------------------
1 | class ProductMailer < ApplicationMailer
2 | # Subject can be set in your I18n file at config/locales/en.yml
3 | # with the following lookup:
4 | #
5 | # en.product_mailer.in_stock.subject
6 | #
7 | def in_stock
8 | @product = params[:product]
9 | mail to: params[:subscriber].email
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/src/templates/stock-notifications/workspace/store/test/mailers/product_mailer_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 |
3 | class ProductMailerTest < ActionMailer::TestCase
4 | test "in_stock" do
5 | mail = ProductMailer.in_stock
6 | assert_equal "In stock", mail.subject
7 | assert_equal [ "to@example.org" ], mail.to
8 | assert_equal [ "from@example.com" ], mail.from
9 | assert_match "Hi", mail.body.encoded
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/src/content/tutorial/meta.md:
--------------------------------------------------------------------------------
1 | ---
2 | type: tutorial
3 | openInStackBlitz: false
4 | prepareCommands:
5 | - ['npm install', 'Preparing Ruby runtime']
6 | previews: false
7 | filesystem:
8 | watch: ['/*.json', '/workspace/**/*']
9 | terminal:
10 | open: true
11 | activePanel: 0
12 | panels:
13 | - type: terminal
14 | id: 'cmds'
15 | title: 'Command Line'
16 | allowRedirects: true
17 | - ['output', 'Setup Logs']
18 | ---
19 |
--------------------------------------------------------------------------------
/src/templates/active-storage/workspace/store/config/importmap.rb:
--------------------------------------------------------------------------------
1 | # Pin npm packages by running ./bin/importmap
2 |
3 | pin "application"
4 |
5 | pin "@hotwired/turbo-rails", to: "turbo.min.js"
6 | pin "@hotwired/stimulus", to: "stimulus.min.js"
7 | pin "@hotwired/stimulus-loading", to: "stimulus-loading.js"
8 |
9 | pin_all_from "app/javascript/controllers", under: "controllers"
10 |
11 | pin "trix"
12 | pin "@rails/actiontext", to: "actiontext.esm.js"
13 |
--------------------------------------------------------------------------------
/icons/languages/js.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/content/tutorial/12-file-uploads/1-active-storage/_files/workspace/store/app/views/products/_form.html.erb:
--------------------------------------------------------------------------------
1 | <%= form_with model: product do |form| %>
2 |
3 | <%= form.label :name %>
4 | <%= form.text_field :name %>
5 |
6 |
7 |
8 | <%= form.label :description, style: "display: block" %>
9 | <%= form.rich_textarea :description %>
10 |
11 |
12 |
13 | <%= form.submit %>
14 |
15 | <% end %>
16 |
--------------------------------------------------------------------------------
/src/content/tutorial/12-file-uploads/1-active-storage/_files/workspace/store/app/views/products/show.html.erb:
--------------------------------------------------------------------------------
1 | <% cache @product do %>
2 | <%= @product.name %>
3 | <%= @product.description %>
4 | <% end %>
5 |
6 | <%= link_to "Back", products_path %>
7 | <% if authenticated? %>
8 | <%= link_to "Edit", edit_product_path(@product) %>
9 | <%= button_to "Delete", @product, method: :delete, data: { turbo_confirm: "Are you sure?" } %>
10 | <% end %>
11 |
--------------------------------------------------------------------------------
/src/templates/authentication/workspace/store/app/views/passwords/new.html.erb:
--------------------------------------------------------------------------------
1 | Forgot your password?
2 |
3 | <%= tag.div(flash[:alert], style: "color:red") if flash[:alert] %>
4 |
5 | <%= form_with url: passwords_path do |form| %>
6 | <%= form.email_field :email_address, required: true, autofocus: true, autocomplete: "username", placeholder: "Enter your email address", value: params[:email_address] %>
7 | <%= form.submit "Email reset instructions" %>
8 | <% end %>
9 |
--------------------------------------------------------------------------------
/src/templates/testing/workspace/store/test/models/product_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 |
3 | class ProductTest < ActiveSupport::TestCase
4 | include ActionMailer::TestHelper
5 |
6 | test "sends email notifications when back in stock" do
7 | product = products(:tshirt)
8 |
9 | # Set product out of stock
10 | product.update(inventory_count: 0)
11 |
12 | assert_emails 2 do
13 | product.update(inventory_count: 99)
14 | end
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/src/templates/stock-notifications/workspace/store/app/views/products/_inventory.html.erb:
--------------------------------------------------------------------------------
1 | <% if product.inventory_count? %>
2 | <%= product.inventory_count %> in stock
3 | <% else %>
4 | Out of stock
5 | Email me when available.
6 |
7 | <%= form_with model: [product, Subscriber.new] do |form| %>
8 | <%= form.email_field :email, placeholder: "you@example.com", required: true %>
9 | <%= form.submit "Submit" %>
10 | <% end %>
11 | <% end %>
12 |
--------------------------------------------------------------------------------
/src/content/tutorial/14-in-stock-notifications/4-extracting-a-concern/_files/workspace/store/test/mailers/product_mailer_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 |
3 | class ProductMailerTest < ActionMailer::TestCase
4 | test "in_stock" do
5 | mail = ProductMailer.in_stock
6 | assert_equal "In stock", mail.subject
7 | assert_equal [ "to@example.org" ], mail.to
8 | assert_equal [ "from@example.com" ], mail.from
9 | assert_match "Hi", mail.body.encoded
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/src/content/tutorial/14-in-stock-notifications/5-unsubscribe-links/_files/workspace/store/test/mailers/product_mailer_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 |
3 | class ProductMailerTest < ActionMailer::TestCase
4 | test "in_stock" do
5 | mail = ProductMailer.in_stock
6 | assert_equal "In stock", mail.subject
7 | assert_equal [ "to@example.org" ], mail.to
8 | assert_equal [ "from@example.com" ], mail.from
9 | assert_match "Hi", mail.body.encoded
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/src/templates/stock-notifications/workspace/store/app/controllers/unsubscribes_controller.rb:
--------------------------------------------------------------------------------
1 | class UnsubscribesController < ApplicationController
2 | allow_unauthenticated_access
3 | before_action :set_subscriber
4 |
5 | def show
6 | @subscriber&.destroy
7 | redirect_to root_path, notice: "Unsubscribed successfully."
8 | end
9 |
10 | private
11 |
12 | def set_subscriber
13 | @subscriber = Subscriber.find_by_token_for(:unsubscribe, params[:token])
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/rails-wasm/bin/pack:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | require "bundler/setup"
4 |
5 | require "fileutils"
6 | require "wasmify/rails/builder"
7 |
8 | builder = Wasmify::Rails::Builder.new
9 | builder.run(name: "ruby.wasm")
10 |
11 | require "wasmify/rails/packer"
12 |
13 | output_dir = ARGV[0] ? ARGV[0] : Wasmify::Rails.config.output_dir
14 |
15 | packer = Wasmify::Rails::Packer.new(output_dir:)
16 | packer.run(name: "rails.wasm", ruby_wasm_path: File.join(Wasmify::Rails.config.tmp_dir, "ruby.wasm"))
17 |
--------------------------------------------------------------------------------
/src/templates/testing/workspace/store/test/mailers/product_mailer_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 |
3 | class ProductMailerTest < ActionMailer::TestCase
4 | test "in_stock" do
5 | mail = ProductMailer.with(product: products(:tshirt), subscriber: subscribers(:david)).in_stock
6 | assert_equal "In stock", mail.subject
7 | assert_equal [ "david@example.org" ], mail.to
8 | assert_equal [ "from@example.com" ], mail.from
9 | assert_match "Good news!", mail.body.encoded
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/src/templates/active-storage/workspace/store/app/views/products/show.html.erb:
--------------------------------------------------------------------------------
1 | <% cache @product do %>
2 | <%= image_tag @product.featured_image if @product.featured_image.attached? %>
3 | <%= @product.name %>
4 | <%= @product.description %>
5 | <% end %>
6 |
7 | <%= link_to "Back", products_path %>
8 | <% if authenticated? %>
9 | <%= link_to "Edit", edit_product_path(@product) %>
10 | <%= button_to "Delete", @product, method: :delete, data: { turbo_confirm: "Are you sure?" } %>
11 | <% end %>
12 |
--------------------------------------------------------------------------------
/src/content/tutorial/14-in-stock-notifications/4-extracting-a-concern/_files/workspace/store/app/views/products/_inventory.html.erb:
--------------------------------------------------------------------------------
1 | <% if product.inventory_count? %>
2 | <%= product.inventory_count %> in stock
3 | <% else %>
4 | Out of stock
5 | Email me when available.
6 |
7 | <%= form_with model: [product, Subscriber.new] do |form| %>
8 | <%= form.email_field :email, placeholder: "you@example.com", required: true %>
9 | <%= form.submit "Submit" %>
10 | <% end %>
11 | <% end %>
12 |
--------------------------------------------------------------------------------
/src/content/tutorial/14-in-stock-notifications/5-unsubscribe-links/_files/workspace/store/app/views/products/_inventory.html.erb:
--------------------------------------------------------------------------------
1 | <% if product.inventory_count? %>
2 | <%= product.inventory_count %> in stock
3 | <% else %>
4 | Out of stock
5 | Email me when available.
6 |
7 | <%= form_with model: [product, Subscriber.new] do |form| %>
8 | <%= form.email_field :email, placeholder: "you@example.com", required: true %>
9 | <%= form.submit "Submit" %>
10 | <% end %>
11 | <% end %>
12 |
--------------------------------------------------------------------------------
/src/content/tutorial/14-in-stock-notifications/3-in-stock-email-notifications/_files/workspace/store/app/views/products/_inventory.html.erb:
--------------------------------------------------------------------------------
1 | <% if product.inventory_count? %>
2 | <%= product.inventory_count %> in stock
3 | <% else %>
4 | Out of stock
5 | Email me when available.
6 |
7 | <%= form_with model: [product, Subscriber.new] do |form| %>
8 | <%= form.email_field :email, placeholder: "you@example.com", required: true %>
9 | <%= form.submit "Submit" %>
10 | <% end %>
11 | <% end %>
12 |
--------------------------------------------------------------------------------
/src/templates/authentication/workspace/store/app/channels/application_cable/connection.rb:
--------------------------------------------------------------------------------
1 | module ApplicationCable
2 | class Connection < ActionCable::Connection::Base
3 | identified_by :current_user
4 |
5 | def connect
6 | set_current_user || reject_unauthorized_connection
7 | end
8 |
9 | private
10 | def set_current_user
11 | if session = Session.find_by(id: cookies.signed[:session_id])
12 | self.current_user = session.user
13 | end
14 | end
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/src/templates/rails-new/workspace/store/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 |
--------------------------------------------------------------------------------
/src/templates/rails-new/workspace/store/test/test_helper.rb:
--------------------------------------------------------------------------------
1 | ENV["RAILS_ENV"] ||= "test"
2 | require_relative "../config/environment"
3 | require "rails/test_help"
4 |
5 | module ActiveSupport
6 | class TestCase
7 | # Run tests in parallel with specified workers
8 | parallelize(workers: :number_of_processors)
9 |
10 | # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order.
11 | fixtures :all
12 |
13 | # Add more helper methods to be used by all tests here...
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/fly.toml:
--------------------------------------------------------------------------------
1 | # fly.toml app configuration file generated for tutorialkit-rb-playground on 2025-05-02T17:18:25-07:00
2 | #
3 | # See https://fly.io/docs/reference/configuration/ for information about how to use this file.
4 | #
5 |
6 | app = 'tutorialkit-rb-playground'
7 | primary_region = 'sea'
8 |
9 | [build]
10 |
11 | [http_service]
12 | internal_port = 80
13 | force_https = true
14 | auto_stop_machines = 'stop'
15 | auto_start_machines = true
16 | min_machines_running = 0
17 | processes = ['app']
18 |
19 | [[vm]]
20 | size = 'shared-cpu-1x'
21 |
--------------------------------------------------------------------------------
/src/templates/rails-new/workspace/store/app/views/pwa/manifest.json.erb:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Store",
3 | "icons": [
4 | {
5 | "src": "/icon.png",
6 | "type": "image/png",
7 | "sizes": "512x512"
8 | },
9 | {
10 | "src": "/icon.png",
11 | "type": "image/png",
12 | "sizes": "512x512",
13 | "purpose": "maskable"
14 | }
15 | ],
16 | "start_url": "/",
17 | "display": "standalone",
18 | "scope": "/",
19 | "description": "Store.",
20 | "theme_color": "red",
21 | "background_color": "red"
22 | }
23 |
--------------------------------------------------------------------------------
/src/templates/rails-new/workspace/store/config/initializers/filter_parameter_logging.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Configure parameters to be partially matched (e.g. passw matches password) and filtered from the log file.
4 | # Use this to limit dissemination of sensitive information.
5 | # See the ActiveSupport::ParameterFilter documentation for supported notations and behaviors.
6 | Rails.application.config.filter_parameters += [
7 | :passw, :email, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn, :cvv, :cvc
8 | ]
9 |
--------------------------------------------------------------------------------
/src/templates/rails-new/workspace/store/db/seeds.rb:
--------------------------------------------------------------------------------
1 | # This file should ensure the existence of records required to run the application in every environment (production,
2 | # development, test). The code here should be idempotent so that it can be executed at any point in every environment.
3 | # The data can then be loaded with the bin/rails db:seed command (or created alongside the database with db:setup).
4 | #
5 | # Example:
6 | #
7 | # ["Action", "Comedy", "Drama", "Horror"].each do |genre_name|
8 | # MovieGenre.find_or_create_by!(name: genre_name)
9 | # end
10 |
--------------------------------------------------------------------------------
/src/templates/stock-notifications/workspace/store/app/views/products/show.html.erb:
--------------------------------------------------------------------------------
1 | <% cache @product do %>
2 | <%= image_tag @product.featured_image if @product.featured_image.attached? %>
3 | <%= @product.name %>
4 | <%= @product.description %>
5 | <% end %>
6 |
7 | <%= render "inventory", product: @product %>
8 |
9 | <%= link_to "Back", products_path %>
10 | <% if authenticated? %>
11 | <%= link_to "Edit", edit_product_path(@product) %>
12 | <%= button_to "Delete", @product, method: :delete, data: { turbo_confirm: "Are you sure?" } %>
13 | <% end %>
14 |
--------------------------------------------------------------------------------
/icons/languages/ts.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/templates/stock-notifications/workspace/store/app/controllers/subscribers_controller.rb:
--------------------------------------------------------------------------------
1 | class SubscribersController < ApplicationController
2 | allow_unauthenticated_access
3 | before_action :set_product
4 |
5 | def create
6 | @product.subscribers.where(subscriber_params).first_or_create
7 | redirect_to @product, notice: "You are now subscribed."
8 | end
9 |
10 | private
11 |
12 | def set_product
13 | @product = Product.find(params[:product_id])
14 | end
15 |
16 | def subscriber_params
17 | params.expect(subscriber: [ :email ])
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/src/templates/active-storage/workspace/store/app/views/products/_form.html.erb:
--------------------------------------------------------------------------------
1 | <%= form_with model: product do |form| %>
2 |
3 | <%= form.label :name %>
4 | <%= form.text_field :name %>
5 |
6 |
7 |
8 | <%= form.label :description, style: "display: block" %>
9 | <%= form.rich_textarea :description %>
10 |
11 |
12 |
13 | <%= form.label :featured_image, style: "display: block" %>
14 | <%= form.file_field :featured_image, accept: "image/*" %>
15 |
16 |
17 |
18 | <%= form.submit %>
19 |
20 | <% end %>
21 |
--------------------------------------------------------------------------------
/src/templates/rails-new/workspace/store/config/credentials.yml.enc:
--------------------------------------------------------------------------------
1 | +opdmbbIhKjrVxzwTcULNjyc72D92lPMBrS3ITE0htdpvlwr4KZMIzfSPrPhcmpuHtKF3xNkhEW3/MUuQJb+BQ0++W/wmTzP/CJHaiYkidd0a6arzdXLqxKfkTkc3ew6OUCoNUAyAoVJ+ASr/k655xTm77rvSydr0f3syWdXpa/AOzopeSOVuWxr4k0QnSbeOd1/BGVY5p/FzpakfKGlc5TVXTpkvkWqC3z5w0pNJnfRQAxSp0i/0xIk0y9OxJRU3E4Yyu20+O5NQSW4O+kgI6LJEHu1LCrZCXzVO6kz8BktmGajU72pXAaM56oKMvaOWUf+XxJdKC2+6pLMlIw2apkeXA3Gxc0Afl+hSrx62JeqhExbSXccWk9KVgvTHDBnaCgJu/VMCukQELyMqb/Tw9TPjnyMr2WV8v66QPs2gQLg+kGMOZlW6R6ubu0i2BBV+rxQkn1vPOrsgitbK9B6PtPWEgmTdBiu3QcVDFUIomQLTrGqPkkgxReM--H5GpUcrVpHme6TEY--KUIefLaLDjHc8MAEP0yeng==
--------------------------------------------------------------------------------
/.github/workflows/fly-deploy.yml:
--------------------------------------------------------------------------------
1 | # See https://fly.io/docs/app-guides/continuous-deployment-with-github-actions/
2 |
3 | name: Fly Deploy
4 | on:
5 | push:
6 | branches:
7 | - master
8 | jobs:
9 | deploy:
10 | name: Deploy app
11 | runs-on: ubuntu-latest
12 | concurrency: deploy-group # optional: ensure only one action runs at a time
13 | steps:
14 | - uses: actions/checkout@v4
15 | - uses: superfly/flyctl-actions/setup-flyctl@master
16 | - run: flyctl deploy --remote-only
17 | env:
18 | FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
19 |
--------------------------------------------------------------------------------
/src/templates/rails-new/workspace/store/app/assets/stylesheets/application.css:
--------------------------------------------------------------------------------
1 | /*
2 | * This is a manifest file that'll be compiled into application.css.
3 | *
4 | * With Propshaft, assets are served efficiently without preprocessing steps. You can still include
5 | * application-wide styles in this file, but keep in mind that CSS precedence will follow the standard
6 | * cascading order, meaning styles declared later in the document or manifest will override earlier ones,
7 | * depending on specificity.
8 | *
9 | * Consider organizing styles into separate files for maintainability.
10 | */
11 |
--------------------------------------------------------------------------------
/astro.config.ts:
--------------------------------------------------------------------------------
1 | import tutorialkit from '@tutorialkit/astro';
2 | import { defineConfig } from 'astro/config';
3 | import remarkRailsPathLinks from './src/plugins/remarkRailsPathLinks';
4 |
5 | export default defineConfig({
6 | devToolbar: {
7 | enabled: false,
8 | },
9 | integrations: [
10 | tutorialkit({
11 | components: {
12 | TopBar: "./src/components/TopBar.astro",
13 | HeadTags: "./src/components/HeadTags.astro"
14 | },
15 | defaultRoutes: true
16 | })
17 | ],
18 | markdown: {
19 | remarkPlugins: [remarkRailsPathLinks],
20 | }
21 | });
22 |
--------------------------------------------------------------------------------
/src/content/tutorial/14-in-stock-notifications/5-unsubscribe-links/_files/workspace/store/app/views/products/show.html.erb:
--------------------------------------------------------------------------------
1 | <% cache @product do %>
2 | <%= image_tag @product.featured_image if @product.featured_image.attached? %>
3 | <%= @product.name %>
4 | <%= @product.description %>
5 | <% end %>
6 |
7 | <%= render "inventory", product: @product %>
8 |
9 | <%= link_to "Back", products_path %>
10 | <% if authenticated? %>
11 | <%= link_to "Edit", edit_product_path(@product) %>
12 | <%= button_to "Delete", @product, method: :delete, data: { turbo_confirm: "Are you sure?" } %>
13 | <% end %>
14 |
--------------------------------------------------------------------------------
/src/templates/authentication/workspace/store/app/views/passwords/edit.html.erb:
--------------------------------------------------------------------------------
1 | Update your password
2 |
3 | <%= tag.div(flash[:alert], style: "color:red") if flash[:alert] %>
4 |
5 | <%= form_with url: password_path(params[:token]), method: :put do |form| %>
6 | <%= form.password_field :password, required: true, autocomplete: "new-password", placeholder: "Enter new password", maxlength: 72 %>
7 | <%= form.password_field :password_confirmation, required: true, autocomplete: "new-password", placeholder: "Repeat new password", maxlength: 72 %>
8 | <%= form.submit "Save" %>
9 | <% end %>
10 |
--------------------------------------------------------------------------------
/src/templates/stock-notifications/workspace/store/app/models/product/notifications.rb:
--------------------------------------------------------------------------------
1 | module Product::Notifications
2 | extend ActiveSupport::Concern
3 |
4 | included do
5 | has_many :subscribers, dependent: :destroy
6 | after_update_commit :notify_subscribers, if: :back_in_stock?
7 | end
8 |
9 | def back_in_stock?
10 | inventory_count_previously_was&.zero? && inventory_count > 0
11 | end
12 |
13 | def notify_subscribers
14 | subscribers.each do |subscriber|
15 | ProductMailer.with(product: self, subscriber: subscriber).in_stock.deliver_later
16 | end
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/src/content/tutorial/14-in-stock-notifications/4-extracting-a-concern/_files/workspace/store/app/views/products/show.html.erb:
--------------------------------------------------------------------------------
1 | <% cache @product do %>
2 | <%= image_tag @product.featured_image if @product.featured_image.attached? %>
3 | <%= @product.name %>
4 | <%= @product.description %>
5 | <% end %>
6 |
7 | <%= render "inventory", product: @product %>
8 |
9 | <%= link_to "Back", products_path %>
10 | <% if authenticated? %>
11 | <%= link_to "Edit", edit_product_path(@product) %>
12 | <%= button_to "Delete", @product, method: :delete, data: { turbo_confirm: "Are you sure?" } %>
13 | <% end %>
14 |
--------------------------------------------------------------------------------
/src/content/tutorial/14-in-stock-notifications/3-in-stock-email-notifications/_files/workspace/store/app/views/products/show.html.erb:
--------------------------------------------------------------------------------
1 | <% cache @product do %>
2 | <%= image_tag @product.featured_image if @product.featured_image.attached? %>
3 | <%= @product.name %>
4 | <%= @product.description %>
5 | <% end %>
6 |
7 | <%= render "inventory", product: @product %>
8 |
9 | <%= link_to "Back", products_path %>
10 | <% if authenticated? %>
11 | <%= link_to "Edit", edit_product_path(@product) %>
12 | <%= button_to "Delete", @product, method: :delete, data: { turbo_confirm: "Are you sure?" } %>
13 | <% end %>
14 |
--------------------------------------------------------------------------------
/src/content/tutorial/14-in-stock-notifications/5-unsubscribe-links/_files/workspace/store/app/controllers/subscribers_controller.rb:
--------------------------------------------------------------------------------
1 | class SubscribersController < ApplicationController
2 | allow_unauthenticated_access
3 | before_action :set_product
4 |
5 | def create
6 | @product.subscribers.where(subscriber_params).first_or_create
7 | redirect_to @product, notice: "You are now subscribed."
8 | end
9 |
10 | private
11 |
12 | def set_product
13 | @product = Product.find(params[:product_id])
14 | end
15 |
16 | def subscriber_params
17 | params.expect(subscriber: [ :email ])
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/src/content/tutorial/14-in-stock-notifications/4-extracting-a-concern/_files/workspace/store/app/controllers/subscribers_controller.rb:
--------------------------------------------------------------------------------
1 | class SubscribersController < ApplicationController
2 | allow_unauthenticated_access
3 | before_action :set_product
4 |
5 | def create
6 | @product.subscribers.where(subscriber_params).first_or_create
7 | redirect_to @product, notice: "You are now subscribed."
8 | end
9 |
10 | private
11 |
12 | def set_product
13 | @product = Product.find(params[:product_id])
14 | end
15 |
16 | def subscriber_params
17 | params.expect(subscriber: [ :email ])
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/rails-wasm/README.md:
--------------------------------------------------------------------------------
1 | # @rails-tutorial-wasm NPM package
2 |
3 | To create a `rails.wasm` module with Ruby and Rails included, run the following commands:
4 |
5 | ```sh
6 | bundle install
7 | bin/pack
8 | ```
9 |
10 | Then, you can publish the NPM package containing the `rails.wasm` file as follows:
11 |
12 | ```sh
13 | npm publish
14 | ```
15 |
16 | ## Prerequisites
17 |
18 | To build and pack Ruby Wasm modules, you need the following:
19 |
20 | - Rust toolchain:
21 |
22 | ```sh
23 | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
24 | ```
25 |
26 | - [wasi-vfs](https://github.com/kateinoigakukun/wasi-vfs)
27 |
--------------------------------------------------------------------------------
/src/content/tutorial/14-in-stock-notifications/3-in-stock-email-notifications/_files/workspace/store/app/controllers/subscribers_controller.rb:
--------------------------------------------------------------------------------
1 | class SubscribersController < ApplicationController
2 | allow_unauthenticated_access
3 | before_action :set_product
4 |
5 | def create
6 | @product.subscribers.where(subscriber_params).first_or_create
7 | redirect_to @product, notice: "You are now subscribed."
8 | end
9 |
10 | private
11 |
12 | def set_product
13 | @product = Product.find(params[:product_id])
14 | end
15 |
16 | def subscriber_params
17 | params.expect(subscriber: [ :email ])
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/src/content/tutorial/14-in-stock-notifications/5-unsubscribe-links/_files/workspace/store/app/models/product/notifications.rb:
--------------------------------------------------------------------------------
1 | module Product::Notifications
2 | extend ActiveSupport::Concern
3 |
4 | included do
5 | has_many :subscribers, dependent: :destroy
6 | after_update_commit :notify_subscribers, if: :back_in_stock?
7 | end
8 |
9 | def back_in_stock?
10 | inventory_count_previously_was&.zero? && inventory_count > 0
11 | end
12 |
13 | def notify_subscribers
14 | subscribers.each do |subscriber|
15 | ProductMailer.with(product: self, subscriber: subscriber).in_stock.deliver_later
16 | end
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/rails-wasm/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@rails-tutorial/wasm",
3 | "version": "8.0.2-rc.1",
4 | "description": "Prebuilt Wasm module with Ruby 3.3, Rails and default Rails gems",
5 | "exports": {
6 | "./dist/rails.wasm": "./dist/rails.wasm"
7 | },
8 | "files": [
9 | "dist",
10 | "README.md"
11 | ],
12 | "repository": "https://github.com/palkan/rails-tutorial-on-wasm",
13 | "homepage": "https://github.com/palkan/rails-tutorial-on-wasm",
14 | "publishConfig": {
15 | "access": "public"
16 | },
17 | "keywords": [
18 | "wasm",
19 | "webassembly",
20 | "rails"
21 | ],
22 | "license": "MIT",
23 | "dependencies": {}
24 | }
25 |
--------------------------------------------------------------------------------
/src/content/tutorial/8-controllers/7-crud-edit/_files/workspace/store/app/controllers/products_controller.rb:
--------------------------------------------------------------------------------
1 | class ProductsController < ApplicationController
2 | def index
3 | @products = Product.all
4 | end
5 |
6 | def show
7 | @product = Product.find(params[:id])
8 | end
9 |
10 | def new
11 | @product = Product.new
12 | end
13 |
14 | def create
15 | @product = Product.new(product_params)
16 | if @product.save
17 | redirect_to @product
18 | else
19 | render :new, status: :unprocessable_entity
20 | end
21 | end
22 |
23 | private
24 | def product_params
25 | params.expect(product: [ :name ])
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/src/content/tutorial/8-controllers/6-strong-parameters/_files/workspace/store/app/controllers/products_controller.rb:
--------------------------------------------------------------------------------
1 | class ProductsController < ApplicationController
2 | def index
3 | @products = Product.all
4 | end
5 |
6 | def show
7 | @product = Product.find(params[:id])
8 | end
9 |
10 | def new
11 | @product = Product.new
12 | end
13 |
14 | def create
15 | @product = Product.new(product_params)
16 | if @product.save
17 | redirect_to @product
18 | else
19 | render :new, status: :unprocessable_entity
20 | end
21 | end
22 |
23 | private
24 | def product_params
25 | params.expect(product: [ :name ])
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/rails-wasm/config/wasmify.yml:
--------------------------------------------------------------------------------
1 | output_dir: "./dist"
2 | ruby_version: "3.3"
3 |
4 | # Specify the list of directories to be included into the final
5 | # app.wasm file.
6 | pack_directories: []
7 | pack_root: "/rails-vm"
8 | additional_root_files:
9 | - boot.rb
10 | - .railsrc
11 |
12 | # Specify the list of gems to skip during the base module compiliation.
13 | # Usually, you want to specify the gems with native extensions that are
14 | # not currently Wasm-compatible.
15 | exclude_gems:
16 | - nio4r
17 | - io-console
18 | - psych
19 |
20 | # Skip building native extensions for the following gems (and keep their Ruby source)
21 | ignore_gem_extensions:
22 | - bigdecimal
23 | - date
24 |
--------------------------------------------------------------------------------
/src/templates/authentication/workspace/store/app/views/sessions/new.html.erb:
--------------------------------------------------------------------------------
1 | <%= tag.div(flash[:alert], style: "color:red") if flash[:alert] %>
2 | <%= tag.div(flash[:notice], style: "color:green") if flash[:notice] %>
3 |
4 | <%= form_with url: session_path do |form| %>
5 | <%= form.email_field :email_address, required: true, autofocus: true, autocomplete: "username", placeholder: "Enter your email address", value: params[:email_address] %>
6 | <%= form.password_field :password, required: true, autocomplete: "current-password", placeholder: "Enter your password", maxlength: 72 %>
7 | <%= form.submit "Sign in" %>
8 | <% end %>
9 |
10 |
11 | <%= link_to "Forgot password?", new_password_path %>
12 |
--------------------------------------------------------------------------------
/src/templates/css-js/workspace/store/app/views/products/show.html.erb:
--------------------------------------------------------------------------------
1 | <%= link_to "Back", products_path %>
2 |
3 |
4 | <%= image_tag @product.featured_image if @product.featured_image.attached? %>
5 |
6 |
7 | <% cache @product do %>
8 | <%= @product.name %>
9 | <%= @product.description %>
10 | <% end %>
11 |
12 | <%= render "inventory", product: @product %>
13 |
14 | <% if authenticated? %>
15 | <%= link_to "Edit", edit_product_path(@product) %>
16 | <%= button_to "Delete", @product, method: :delete, data: { turbo_confirm: "Are you sure?" } %>
17 | <% end %>
18 |
19 |
20 |
--------------------------------------------------------------------------------
/patches/@tutorialkit+theme+1.5.0.patch:
--------------------------------------------------------------------------------
1 | diff --git a/node_modules/@tutorialkit/theme/dist/index.js b/node_modules/@tutorialkit/theme/dist/index.js
2 | index e6669fb..3939937 100644
3 | --- a/node_modules/@tutorialkit/theme/dist/index.js
4 | +++ b/node_modules/@tutorialkit/theme/dist/index.js
5 | @@ -52,7 +52,7 @@ export function getInlineContentForPackage({ name, pattern, root }) {
6 | }
7 | }
8 | function readCustomIcons() {
9 | - const iconPaths = globSync('./icons/languages/*.svg');
10 | + const iconPaths = globSync('./icons/**/*.svg');
11 | return iconPaths.reduce((acc, iconPath) => {
12 | const collectionName = basename(dirname(iconPath));
13 | const [iconName] = basename(iconPath).split('.');
14 |
--------------------------------------------------------------------------------
/src/templates/css-js/workspace/store/app/assets/stylesheets/application.css:
--------------------------------------------------------------------------------
1 | body {
2 | font-family: Arial, Helvetica, sans-serif;
3 | padding: 1rem;
4 | }
5 |
6 | nav {
7 | justify-content: flex-end;
8 | display: flex;
9 | font-size: 0.875em;
10 | gap: 0.5rem;
11 | max-width: 1024px;
12 | margin: 0 auto;
13 | padding: 1rem;
14 | }
15 |
16 | nav a {
17 | display: inline-block;
18 | }
19 |
20 | main {
21 | max-width: 1024px;
22 | margin: 0 auto;
23 | }
24 |
25 | .notice {
26 | color: green;
27 | }
28 |
29 | section.product {
30 | display: flex;
31 | gap: 1rem;
32 | flex-direction: row;
33 | }
34 |
35 | section.product img {
36 | border-radius: 8px;
37 | flex-basis: 50%;
38 | max-width: 50%;
39 | }
40 |
--------------------------------------------------------------------------------
/src/content/tutorial/15-css-and-js/3-hotwire/_files/workspace/store/app/views/products/show.html.erb:
--------------------------------------------------------------------------------
1 | <%= link_to "Back", products_path %>
2 |
3 |
4 | <%= image_tag @product.featured_image if @product.featured_image.attached? %>
5 |
6 |
7 | <% cache @product do %>
8 | <%= @product.name %>
9 | <%= @product.description %>
10 | <% end %>
11 |
12 | <%= render "inventory", product: @product %>
13 |
14 | <% if authenticated? %>
15 | <%= link_to "Edit", edit_product_path(@product) %>
16 | <%= button_to "Delete", @product, method: :delete, data: { turbo_confirm: "Are you sure?" } %>
17 | <% end %>
18 |
19 |
20 |
--------------------------------------------------------------------------------
/src/templates/active-storage/workspace/store/app/views/active_storage/blobs/_blob.html.erb:
--------------------------------------------------------------------------------
1 | attachment--<%= blob.filename.extension %>">
2 | <% if blob.representable? %>
3 | <%= image_tag blob.representation(resize_to_limit: local_assigns[:in_gallery] ? [ 800, 600 ] : [ 1024, 768 ]) %>
4 | <% end %>
5 |
6 |
7 | <% if caption = blob.try(:caption) %>
8 | <%= caption %>
9 | <% else %>
10 | <%= blob.filename %>
11 | <%= number_to_human_size blob.byte_size %>
12 | <% end %>
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/content/tutorial/15-css-and-js/2-importmap/_files/workspace/store/app/views/products/show.html.erb:
--------------------------------------------------------------------------------
1 | <%= link_to "Back", products_path %>
2 |
3 |
4 | <%= image_tag @product.featured_image if @product.featured_image.attached? %>
5 |
6 |
7 | <% cache @product do %>
8 | <%= @product.name %>
9 | <%= @product.description %>
10 | <% end %>
11 |
12 | <%= render "inventory", product: @product %>
13 |
14 | <% if authenticated? %>
15 | <%= link_to "Edit", edit_product_path(@product) %>
16 | <%= button_to "Delete", @product, method: :delete, data: { turbo_confirm: "Are you sure?" } %>
17 | <% end %>
18 |
19 |
20 |
--------------------------------------------------------------------------------
/src/templates/stock-notifications/workspace/store/app/views/products/_form.html.erb:
--------------------------------------------------------------------------------
1 | <%= form_with model: product do |form| %>
2 |
3 | <%= form.label :name %>
4 | <%= form.text_field :name %>
5 |
6 |
7 |
8 | <%= form.label :description, style: "display: block" %>
9 | <%= form.rich_textarea :description %>
10 |
11 |
12 |
13 | <%= form.label :featured_image, style: "display: block" %>
14 | <%= form.file_field :featured_image, accept: "image/*" %>
15 |
16 |
17 |
18 | <%= form.label :inventory_count, style: "display: block" %>
19 | <%= form.number_field :inventory_count %>
20 |
21 |
22 |
23 | <%= form.submit %>
24 |
25 | <% end %>
26 |
--------------------------------------------------------------------------------
/icons/languages/json.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/templates/default/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vite-project",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "test": "node --disable-warning=ExperimentalWarning ./scripts/test.js",
9 | "test:watch": "node --disable-warning=ExperimentalWarning --watch --watch-path=./workspace ./scripts/test.js",
10 | "postinstall": "chmod +x bin/*"
11 | },
12 | "dependencies": {
13 | "@electric-sql/pglite": "^0.3.1",
14 | "@ruby/wasm-wasi": "^2.7.1",
15 | "@rails-tutorial/wasm": "^8.0.2-rc.1",
16 | "express": "^5.1.0",
17 | "multer": "^2.0.1",
18 | "set-cookie-parser": "^2.7.1"
19 | },
20 | "devDependencies": {
21 | "vite": "^5.2.0"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/templates/authentication/workspace/store/app/controllers/sessions_controller.rb:
--------------------------------------------------------------------------------
1 | class SessionsController < ApplicationController
2 | allow_unauthenticated_access only: %i[ new create ]
3 | rate_limit to: 10, within: 3.minutes, only: :create, with: -> { redirect_to new_session_url, alert: "Try again later." }
4 |
5 | def new
6 | end
7 |
8 | def create
9 | if user = User.authenticate_by(params.permit(:email_address, :password))
10 | start_new_session_for user
11 | redirect_to after_authentication_url
12 | else
13 | redirect_to new_session_path, alert: "Try another email address or password."
14 | end
15 | end
16 |
17 | def destroy
18 | terminate_session
19 | redirect_to new_session_path
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/src/content/tutorial/15-css-and-js/3-hotwire/_files/workspace/store/app/assets/stylesheets/application.css:
--------------------------------------------------------------------------------
1 | body {
2 | font-family: Arial, Helvetica, sans-serif;
3 | padding: 1rem;
4 | }
5 |
6 | nav {
7 | justify-content: flex-end;
8 | display: flex;
9 | font-size: 0.875em;
10 | gap: 0.5rem;
11 | max-width: 1024px;
12 | margin: 0 auto;
13 | padding: 1rem;
14 | }
15 |
16 | nav a {
17 | display: inline-block;
18 | }
19 |
20 | main {
21 | max-width: 1024px;
22 | margin: 0 auto;
23 | }
24 |
25 | .notice {
26 | color: green;
27 | }
28 |
29 | section.product {
30 | display: flex;
31 | gap: 1rem;
32 | flex-direction: row;
33 | }
34 |
35 | section.product img {
36 | border-radius: 8px;
37 | flex-basis: 50%;
38 | max-width: 50%;
39 | }
40 |
--------------------------------------------------------------------------------
/src/content/tutorial/14-in-stock-notifications/4-extracting-a-concern/_files/workspace/store/app/models/product.rb:
--------------------------------------------------------------------------------
1 | class Product < ApplicationRecord
2 | has_many :subscribers, dependent: :destroy
3 | has_one_attached :featured_image
4 | has_rich_text :description
5 |
6 | validates :name, presence: true
7 | validates :inventory_count, numericality: { greater_than_or_equal_to: 0 }
8 |
9 | after_update_commit :notify_subscribers, if: :back_in_stock?
10 |
11 | def back_in_stock?
12 | inventory_count_previously_was&.zero? && inventory_count > 0
13 | end
14 |
15 | def notify_subscribers
16 | subscribers.each do |subscriber|
17 | ProductMailer.with(product: self, subscriber: subscriber).in_stock.deliver_later
18 | end
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/src/content/tutorial/15-css-and-js/2-importmap/_files/workspace/store/app/assets/stylesheets/application.css:
--------------------------------------------------------------------------------
1 | body {
2 | font-family: Arial, Helvetica, sans-serif;
3 | padding: 1rem;
4 | }
5 |
6 | nav {
7 | justify-content: flex-end;
8 | display: flex;
9 | font-size: 0.875em;
10 | gap: 0.5rem;
11 | max-width: 1024px;
12 | margin: 0 auto;
13 | padding: 1rem;
14 | }
15 |
16 | nav a {
17 | display: inline-block;
18 | }
19 |
20 | main {
21 | max-width: 1024px;
22 | margin: 0 auto;
23 | }
24 |
25 | .notice {
26 | color: green;
27 | }
28 |
29 | section.product {
30 | display: flex;
31 | gap: 1rem;
32 | flex-direction: row;
33 | }
34 |
35 | section.product img {
36 | border-radius: 8px;
37 | flex-basis: 50%;
38 | max-width: 50%;
39 | }
40 |
--------------------------------------------------------------------------------
/src/templates/rails-new/workspace/store/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 |
--------------------------------------------------------------------------------
/src/content/tutorial/14-in-stock-notifications/2-adding-subscribers/_files/workspace/store/app/views/products/_form.html.erb:
--------------------------------------------------------------------------------
1 | <%= form_with model: product do |form| %>
2 |
3 | <%= form.label :name %>
4 | <%= form.text_field :name %>
5 |
6 |
7 |
8 | <%= form.label :description, style: "display: block" %>
9 | <%= form.rich_textarea :description %>
10 |
11 |
12 |
13 | <%= form.label :featured_image, style: "display: block" %>
14 | <%= form.file_field :featured_image, accept: "image/*" %>
15 |
16 |
17 |
18 | <%= form.label :inventory_count, style: "display: block" %>
19 | <%= form.number_field :inventory_count %>
20 |
21 |
22 |
23 | <%= form.submit %>
24 |
25 | <% end %>
26 |
--------------------------------------------------------------------------------
/src/content/tutorial/14-in-stock-notifications/5-unsubscribe-links/_files/workspace/store/app/views/products/_form.html.erb:
--------------------------------------------------------------------------------
1 | <%= form_with model: product do |form| %>
2 |
3 | <%= form.label :name %>
4 | <%= form.text_field :name %>
5 |
6 |
7 |
8 | <%= form.label :description, style: "display: block" %>
9 | <%= form.rich_textarea :description %>
10 |
11 |
12 |
13 | <%= form.label :featured_image, style: "display: block" %>
14 | <%= form.file_field :featured_image, accept: "image/*" %>
15 |
16 |
17 |
18 | <%= form.label :inventory_count, style: "display: block" %>
19 | <%= form.number_field :inventory_count %>
20 |
21 |
22 |
23 | <%= form.submit %>
24 |
25 | <% end %>
26 |
--------------------------------------------------------------------------------
/src/content/tutorial/14-in-stock-notifications/4-extracting-a-concern/_files/workspace/store/app/views/products/_form.html.erb:
--------------------------------------------------------------------------------
1 | <%= form_with model: product do |form| %>
2 |
3 | <%= form.label :name %>
4 | <%= form.text_field :name %>
5 |
6 |
7 |
8 | <%= form.label :description, style: "display: block" %>
9 | <%= form.rich_textarea :description %>
10 |
11 |
12 |
13 | <%= form.label :featured_image, style: "display: block" %>
14 | <%= form.file_field :featured_image, accept: "image/*" %>
15 |
16 |
17 |
18 | <%= form.label :inventory_count, style: "display: block" %>
19 | <%= form.number_field :inventory_count %>
20 |
21 |
22 |
23 | <%= form.submit %>
24 |
25 | <% end %>
26 |
--------------------------------------------------------------------------------
/src/content/tutorial/14-in-stock-notifications/3-in-stock-email-notifications/_files/workspace/store/app/views/products/_form.html.erb:
--------------------------------------------------------------------------------
1 | <%= form_with model: product do |form| %>
2 |
3 | <%= form.label :name %>
4 | <%= form.text_field :name %>
5 |
6 |
7 |
8 | <%= form.label :description, style: "display: block" %>
9 | <%= form.rich_textarea :description %>
10 |
11 |
12 |
13 | <%= form.label :featured_image, style: "display: block" %>
14 | <%= form.file_field :featured_image, accept: "image/*" %>
15 |
16 |
17 |
18 | <%= form.label :inventory_count, style: "display: block" %>
19 | <%= form.number_field :inventory_count %>
20 |
21 |
22 |
23 | <%= form.submit %>
24 |
25 | <% end %>
26 |
--------------------------------------------------------------------------------
/rails-wasm/boot.rb:
--------------------------------------------------------------------------------
1 | # Put all the code required to initialize the Rails Wasm environment
2 |
3 | # Common Rails shims
4 | require "wasmify/rails/shim"
5 |
6 | # Load Rails patches
7 | require "wasmify/rails/patches"
8 |
9 | # Setup external commands
10 | require "wasmify/external_commands"
11 |
12 | # Patch Bundler.require to only require precompiled deps
13 | # (We don't want to deal with group: :wasm here)
14 | def Bundler.require(*groups)
15 | %w[
16 | rails
17 | wasmify-rails
18 | propshaft
19 | importmap-rails
20 | turbo-rails
21 | stimulus-rails
22 | jbuilder
23 | bcrypt
24 | solid_cache
25 | solid_queue
26 | solid_cable
27 | image_processing
28 | tzinfo/data
29 | ].each do |gem_name|
30 | Kernel.require gem_name
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/src/templates/default/lib/patches/authentication.rb:
--------------------------------------------------------------------------------
1 | # Automatically authenticate a user of first request if the corresponding file is present.
2 | # This way, we can skip the authentication in the second part of the tutorial
3 | Wasmify::Patcha.on_load("Authentication") do
4 | Authentication.prepend(Module.new do
5 | def find_session_by_cookie
6 | return super if $__pre_authenticated
7 |
8 | $__pre_authenticated = true
9 |
10 | session = super
11 |
12 | return session if session
13 |
14 | return unless Rails.root.join("tmp/authenticated-user.txt").exist?
15 |
16 | $_user_email = Rails.root.join("tmp/authenticated-user.txt").read.chomp
17 |
18 | user = User.find_by(email_address: $_user_email)
19 | return unless user
20 |
21 | start_new_session_for(user)
22 | end
23 | end)
24 | end
25 |
--------------------------------------------------------------------------------
/src/templates/active-storage/workspace/store/db/seeds.rb:
--------------------------------------------------------------------------------
1 | # Create products that we should have created in the previous lessons
2 | Product.create(name: "T-Shirt").destroy
3 | Product.create(name: "Pants")
4 |
5 | # For authentication
6 | User.create!(
7 | email_address: "you@example.org",
8 | password: "s3cr3t",
9 | password_confirmation: "s3cr3t"
10 | ) unless User.where(email_address: "you@example.org").exists?
11 |
12 | # Create a new product with description and logo attached
13 | logo = File.open(Rails.root.join("app/assets/images/logo.png"))
14 |
15 | product = Product.create(name: "Rails Tutorial", featured_image: logo)
16 | product.description = "Rails Tutorial Shirt A high-quality shirt featuring the official Rails logo .
100% cotton Available in multiple sizes Machine washable "
17 | product.save
18 |
--------------------------------------------------------------------------------
/src/content/tutorial/2-rails-new/2-creating-your-first-rails-app/content.md:
--------------------------------------------------------------------------------
1 | ---
2 | type: lesson
3 | title: Creating your first Rails app
4 | editor: false
5 | custom:
6 | shell:
7 | workdir: "/workspace"
8 | ---
9 |
10 | Creating Your First Rails App
11 | -----------------------------
12 |
13 | Rails comes with several commands to make life easier. Run `rails --help` to see
14 | all of the commands.
15 |
16 | `rails new` generates the foundation of a fresh Rails application for you, so
17 | let's start there.
18 |
19 | To create our `store` application, run the following command in your terminal:
20 |
21 | ```bash
22 | $ rails new store
23 | ```
24 |
25 | :::info
26 | You can customize the application Rails generates by using flags. To see
27 | these options, run `rails new --help`.
28 | :::
29 |
30 | After your new application is created, switch to its directory:
31 |
32 | ```bash
33 | $ cd store
34 | ```
35 |
--------------------------------------------------------------------------------
/src/content/tutorial/9-authentication/3-unauthenticated-access/content.md:
--------------------------------------------------------------------------------
1 | ---
2 | type: lesson
3 | title: Allowing Unauthenticated Access
4 | focus: /workspace/store/app/controllers/products_controller.rb
5 | previews:
6 | - 3000
7 | custom:
8 | shell:
9 | workdir: "/workspace/store"
10 | ---
11 |
12 | ### Allowing Unauthenticated Access
13 |
14 | However, our store's product index and show pages should be accessible to
15 | everyone. By default, the Rails authentication generator will restrict all pages
16 | to authenticated users only.
17 |
18 | To allow guests to view products, we can allow unauthenticated access in our
19 | controller.
20 |
21 | ```ruby ins={2}
22 | class ProductsController < ApplicationController
23 | allow_unauthenticated_access only: %i[ index show ]
24 | # ...
25 | end
26 | ```
27 |
28 | Log out and visit the products index and show pages to see they're accessible
29 | without being authenticated.
30 |
--------------------------------------------------------------------------------
/icons/languages/sass.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/templates/stock-notifications/workspace/store/db/seeds.rb:
--------------------------------------------------------------------------------
1 | # Create products that we should have created in the previous lessons
2 | Product.create!(name: "T-Shirt", inventory_count: 2).destroy
3 | Product.create!(name: "Pants", inventory_count: 22)
4 |
5 | # For authentication
6 | User.create!(
7 | email_address: "you@example.org",
8 | password: "s3cr3t",
9 | password_confirmation: "s3cr3t"
10 | ) unless User.where(email_address: "you@example.org").exists?
11 |
12 | # Create a new product with description and logo attached
13 | logo = File.open(Rails.root.join("app/assets/images/logo.png"))
14 |
15 | product = Product.create!(name: "Rails Tutorial", featured_image: logo, inventory_count: 13)
16 | product.description = "Rails Tutorial Shirt A high-quality shirt featuring the official Rails logo .
100% cotton Available in multiple sizes Machine washable "
17 | product.save!
18 |
--------------------------------------------------------------------------------
/src/content/tutorial/14-in-stock-notifications/2-adding-subscribers/_files/workspace/store/db/seeds.rb:
--------------------------------------------------------------------------------
1 | # Create products that we should have created in the previous lessons
2 | Product.create!(name: "T-Shirt", inventory_count: 2).destroy
3 | Product.create!(name: "Pants", inventory_count: 22)
4 |
5 | # For authentication
6 | User.create!(
7 | email_address: "you@example.org",
8 | password: "s3cr3t",
9 | password_confirmation: "s3cr3t"
10 | ) unless User.where(email_address: "you@example.org").exists?
11 |
12 | # Create a new product with description and logo attached
13 | logo = File.open(Rails.root.join("app/assets/images/logo.png"))
14 |
15 | product = Product.create!(name: "Rails Tutorial", featured_image: logo, inventory_count: 13)
16 | product.description = "Rails Tutorial Shirt A high-quality shirt featuring the official Rails logo .
100% cotton Available in multiple sizes Machine washable "
17 | product.save!
18 |
--------------------------------------------------------------------------------
/src/content/tutorial/14-in-stock-notifications/4-extracting-a-concern/_files/workspace/store/db/seeds.rb:
--------------------------------------------------------------------------------
1 | # Create products that we should have created in the previous lessons
2 | Product.create!(name: "T-Shirt", inventory_count: 2).destroy
3 | Product.create!(name: "Pants", inventory_count: 22)
4 |
5 | # For authentication
6 | User.create!(
7 | email_address: "you@example.org",
8 | password: "s3cr3t",
9 | password_confirmation: "s3cr3t"
10 | ) unless User.where(email_address: "you@example.org").exists?
11 |
12 | # Create a new product with description and logo attached
13 | logo = File.open(Rails.root.join("app/assets/images/logo.png"))
14 |
15 | product = Product.create!(name: "Rails Tutorial", featured_image: logo, inventory_count: 13)
16 | product.description = "Rails Tutorial Shirt A high-quality shirt featuring the official Rails logo .
100% cotton Available in multiple sizes Machine washable "
17 | product.save!
18 |
--------------------------------------------------------------------------------
/src/content/tutorial/14-in-stock-notifications/5-unsubscribe-links/_files/workspace/store/db/seeds.rb:
--------------------------------------------------------------------------------
1 | # Create products that we should have created in the previous lessons
2 | Product.create!(name: "T-Shirt", inventory_count: 2).destroy
3 | Product.create!(name: "Pants", inventory_count: 22)
4 |
5 | # For authentication
6 | User.create!(
7 | email_address: "you@example.org",
8 | password: "s3cr3t",
9 | password_confirmation: "s3cr3t"
10 | ) unless User.where(email_address: "you@example.org").exists?
11 |
12 | # Create a new product with description and logo attached
13 | logo = File.open(Rails.root.join("app/assets/images/logo.png"))
14 |
15 | product = Product.create!(name: "Rails Tutorial", featured_image: logo, inventory_count: 13)
16 | product.description = "Rails Tutorial Shirt A high-quality shirt featuring the official Rails logo .
100% cotton Available in multiple sizes Machine washable "
17 | product.save!
18 |
--------------------------------------------------------------------------------
/src/content/tutorial/5-console/1-rails-console/content.md:
--------------------------------------------------------------------------------
1 | ---
2 | type: lesson
3 | title: Rails Console
4 | custom:
5 | shell:
6 | workdir: "/workspace/store"
7 | ---
8 |
9 | Rails Console
10 | -------------------
11 |
12 | Now that we have created our products table, we can interact with it in Rails.
13 | Let's try it out.
14 |
15 | For this, we're going to use a Rails feature called the *console*. The console
16 | is a helpful, interactive tool for testing our code in our Rails application. Run the following command in the terminal:
17 |
18 | ```bash
19 | $ bin/rails console
20 | ```
21 |
22 | You should see a prompt like the following:
23 |
24 | ```irb
25 | Loading development environment (Rails 8.0.2)
26 | store(dev)>
27 | ```
28 |
29 | Now we can type code that will be executed when we hit `Enter`. Try
30 | printing out the Rails version:
31 |
32 | ```irb
33 | store(dev)> Rails.version
34 |
35 | ```
36 |
37 | If the line "8.0.2" appears, it works!
38 |
--------------------------------------------------------------------------------
/src/content/tutorial/14-in-stock-notifications/3-in-stock-email-notifications/_files/workspace/store/db/seeds.rb:
--------------------------------------------------------------------------------
1 | # Create products that we should have created in the previous lessons
2 | Product.create!(name: "T-Shirt", inventory_count: 2).destroy
3 | Product.create!(name: "Pants", inventory_count: 22)
4 |
5 | # For authentication
6 | User.create!(
7 | email_address: "you@example.org",
8 | password: "s3cr3t",
9 | password_confirmation: "s3cr3t"
10 | ) unless User.where(email_address: "you@example.org").exists?
11 |
12 | # Create a new product with description and logo attached
13 | logo = File.open(Rails.root.join("app/assets/images/logo.png"))
14 |
15 | product = Product.create!(name: "Rails Tutorial", featured_image: logo, inventory_count: 13)
16 | product.description = "Rails Tutorial Shirt A high-quality shirt featuring the official Rails logo .
100% cotton Available in multiple sizes Machine washable "
17 | product.save!
18 |
--------------------------------------------------------------------------------
/src/content/tutorial/8-controllers/8-before-actions/_files/workspace/store/app/controllers/products_controller.rb:
--------------------------------------------------------------------------------
1 | class ProductsController < ApplicationController
2 | def index
3 | @products = Product.all
4 | end
5 |
6 | def show
7 | @product = Product.find(params[:id])
8 | end
9 |
10 | def new
11 | @product = Product.new
12 | end
13 |
14 | def create
15 | @product = Product.new(product_params)
16 | if @product.save
17 | redirect_to @product
18 | else
19 | render :new, status: :unprocessable_entity
20 | end
21 | end
22 |
23 | def edit
24 | @product = Product.find(params[:id])
25 | end
26 |
27 | def update
28 | @product = Product.find(params[:id])
29 | if @product.update(product_params)
30 | redirect_to @product
31 | else
32 | render :edit, status: :unprocessable_entity
33 | end
34 | end
35 |
36 | private
37 | def product_params
38 | params.expect(product: [ :name ])
39 | end
40 | end
41 |
--------------------------------------------------------------------------------
/src/content/tutorial/2-rails-new/4-mvc/content.md:
--------------------------------------------------------------------------------
1 | ---
2 | type: lesson
3 | title: MVC Basics
4 | editor: false
5 | custom:
6 | shell:
7 | workdir: "/workspace/store"
8 | ---
9 |
10 | Model-View-Controller Basics
11 | ----------------------------
12 |
13 | Rails code is organized using the Model-View-Controller (MVC) architecture. With
14 | MVC, we have three main concepts where the majority of our code lives:
15 |
16 | * Model - Manages the data in your application. Typically, your database tables.
17 | * View - Handles rendering responses in different formats like HTML, JSON, XML,
18 | etc.
19 | * Controller - Handles user interactions and the logic for each request.
20 |
21 |
22 |
23 |
24 |
25 |
26 | Now that we've got a basic understanding of MVC, let's see how it's used in
27 | Rails.
28 |
--------------------------------------------------------------------------------
/src/content/tutorial/8-controllers/10-crud-destroy/_files/workspace/store/app/controllers/products_controller.rb:
--------------------------------------------------------------------------------
1 | class ProductsController < ApplicationController
2 | before_action :set_product, only: %i[ show edit update ]
3 |
4 | def index
5 | @products = Product.all
6 | end
7 |
8 | def show
9 | end
10 |
11 | def new
12 | @product = Product.new
13 | end
14 |
15 | def create
16 | @product = Product.new(product_params)
17 | if @product.save
18 | redirect_to @product
19 | else
20 | render :new, status: :unprocessable_entity
21 | end
22 | end
23 |
24 | def edit
25 | end
26 |
27 | def update
28 | if @product.update(product_params)
29 | redirect_to @product
30 | else
31 | render :edit, status: :unprocessable_entity
32 | end
33 | end
34 |
35 | private
36 | def set_product
37 | @product = Product.find(params[:id])
38 | end
39 |
40 | def product_params
41 | params.expect(product: [ :name ])
42 | end
43 | end
44 |
--------------------------------------------------------------------------------
/icons/languages/erb.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/content/tutorial/8-controllers/9-extracting-partials/_files/workspace/store/app/controllers/products_controller.rb:
--------------------------------------------------------------------------------
1 | class ProductsController < ApplicationController
2 | before_action :set_product, only: %i[ show edit update ]
3 |
4 | def index
5 | @products = Product.all
6 | end
7 |
8 | def show
9 | end
10 |
11 | def new
12 | @product = Product.new
13 | end
14 |
15 | def create
16 | @product = Product.new(product_params)
17 | if @product.save
18 | redirect_to @product
19 | else
20 | render :new, status: :unprocessable_entity
21 | end
22 | end
23 |
24 | def edit
25 | end
26 |
27 | def update
28 | if @product.update(product_params)
29 | redirect_to @product
30 | else
31 | render :edit, status: :unprocessable_entity
32 | end
33 | end
34 |
35 | private
36 | def set_product
37 | @product = Product.find(params[:id])
38 | end
39 |
40 | def product_params
41 | params.expect(product: [ :name ])
42 | end
43 | end
44 |
--------------------------------------------------------------------------------
/src/content/tutorial/7-request-journey/1-intro/content.md:
--------------------------------------------------------------------------------
1 | ---
2 | type: lesson
3 | title: "HTTP Intro"
4 | editor: false
5 | custom:
6 | shell:
7 | workdir: "/workspace/store"
8 | ---
9 |
10 | A Request's Journey Through Rails
11 | ---------------------------------
12 |
13 | To get Rails saying "Hello", you need to create at minimum a _route_, a
14 | _controller_ with an _action_, and a _view_. A route maps a request to a
15 | controller action. A controller action performs the necessary work to handle the
16 | request, and prepares any data for the view. A view displays data in a desired
17 | format.
18 |
19 | In terms of implementation: Routes are rules written in a Ruby
20 | [DSL (Domain-Specific Language)](https://en.wikipedia.org/wiki/Domain-specific_language).
21 | Controllers are Ruby classes, and their public methods are actions. And views
22 | are templates, usually written in a mixture of HTML and Ruby.
23 |
24 | That's the short of it, but we’re going to walk through each of these steps in
25 | more detail next.
26 |
--------------------------------------------------------------------------------
/src/content/tutorial/4-database/3-running-migrations/content.md:
--------------------------------------------------------------------------------
1 | ---
2 | type: lesson
3 | title: Running Migrations
4 | focus: /workspace/store/db/migrate/20250521010850_create_products.rb
5 | custom:
6 | shell:
7 | workdir: "/workspace/store"
8 | fs:
9 | remove:
10 | - "/workspace/store"
11 | ---
12 |
13 | Running Migrations
14 | -------------------
15 |
16 | Now that you have defined what changes to make to the database, use the
17 | following command to run the migrations:
18 |
19 | ```bash
20 | $ bin/rails db:migrate
21 | ```
22 |
23 | This command checks for any new migrations and applies them to your database.
24 | Its output looks like this:
25 |
26 | ```bash
27 | == 20250521010850 CreateProducts: migrating ===================================
28 | -- create_table(:products)
29 | -> 0.0030s
30 | == 20250521010850 CreateProducts: migrated (0.0031s) ==========================
31 | ```
32 |
33 | :::tip
34 | If you make a mistake, you can run `bin/rails db:rollback` to undo the last
35 | migration.
36 | :::
37 |
--------------------------------------------------------------------------------
/src/templates/rails-new/workspace/store/app/views/pwa/service-worker.js:
--------------------------------------------------------------------------------
1 | // Add a service worker for processing Web Push notifications:
2 | //
3 | // self.addEventListener("push", async (event) => {
4 | // const { title, options } = await event.data.json()
5 | // event.waitUntil(self.registration.showNotification(title, options))
6 | // })
7 | //
8 | // self.addEventListener("notificationclick", function(event) {
9 | // event.notification.close()
10 | // event.waitUntil(
11 | // clients.matchAll({ type: "window" }).then((clientList) => {
12 | // for (let i = 0; i < clientList.length; i++) {
13 | // let client = clientList[i]
14 | // let clientPath = (new URL(client.url)).pathname
15 | //
16 | // if (clientPath == event.notification.data.path && "focus" in client) {
17 | // return client.focus()
18 | // }
19 | // }
20 | //
21 | // if (clients.openWindow) {
22 | // return clients.openWindow(event.notification.data.path)
23 | // }
24 | // })
25 | // )
26 | // })
27 |
--------------------------------------------------------------------------------
/src/content/tutorial/2-rails-new/1-prerequisites/content.md:
--------------------------------------------------------------------------------
1 | ---
2 | type: lesson
3 | title: Prerequisites
4 | editor: false
5 | custom:
6 | shell:
7 | workdir: "/workspace"
8 | ---
9 |
10 | Creating a New Rails App
11 | ------------------------
12 |
13 | We're going to build a project called `store` - a simple e-commerce app that
14 | demonstrates several of Rails' built-in features.
15 |
16 | :::tip
17 | Any commands prefaced with a dollar sign `$` should be run in the terminal.
18 | :::
19 |
20 | ### Prerequisites
21 |
22 | For this project, you will need ~~Ruby 3.2 or newer~~, ~~Rails 8.0.0 or newer~~, ~~a code editor~~... nothing! Everything you need to build your first Rails application is already "installed" in your browser.
23 |
24 | Let's verify the correct version of Rails is installed. To display the current
25 | version, go to the terminal and run the following. You should see a version number
26 | printed out:
27 |
28 | ```bash
29 | $ rails --version
30 | Rails 8.0.2
31 | ```
32 |
33 | The version shown should be Rails 8.0.0 or higher.
34 |
--------------------------------------------------------------------------------
/src/content/tutorial/3-hello-rails/2-autoloading/content.md:
--------------------------------------------------------------------------------
1 | ---
2 | type: lesson
3 | title: Autoloading in Development
4 | editor: false
5 | custom:
6 | shell:
7 | workdir: "/workspace/store"
8 | ---
9 |
10 | Autoloading in Development
11 | ----------------
12 |
13 | Developer happiness is a cornerstone philosophy of Rails and one way of
14 | achieving this is with automatic code reloading in development.
15 |
16 | Once you start the Rails server, new files or changes to existing files are
17 | detected and automatically loaded or reloaded as necessary. This allows you to
18 | focus on building without having to restart your Rails server after every
19 | change.
20 |
21 | You may also notice that Rails applications rarely use `require` statements like
22 | you may have seen in other programming languages. Rails uses naming conventions
23 | to require files automatically so you can focus on writing your application
24 | code.
25 |
26 | See
27 | [Autoloading and Reloading Constants](https://guides.rubyonrails.org/autoloading_and_reloading_constants.html)
28 | for more details.
29 |
--------------------------------------------------------------------------------
/src/templates/rails-new/workspace/store/config/database.yml:
--------------------------------------------------------------------------------
1 | default: &default
2 | adapter: pglite
3 | pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
4 | timeout: 5000
5 |
6 | development:
7 | <<: *default
8 | database: store_development
9 |
10 | # Warning: The database defined as "test" will be erased and
11 | # re-generated from your development database when you run "rake".
12 | # Do not set this db to the same as development or production.
13 | test:
14 | <<: *default
15 | database: store_test
16 |
17 | production:
18 | primary:
19 | <<: *default
20 | # database: path/to/persistent/storage/production.sqlite3
21 | cache:
22 | <<: *default
23 | # database: path/to/persistent/storage/production_cache.sqlite3
24 | migrations_paths: db/cache_migrate
25 | queue:
26 | <<: *default
27 | # database: path/to/persistent/storage/production_queue.sqlite3
28 | migrations_paths: db/queue_migrate
29 | cable:
30 | <<: *default
31 | # database: path/to/persistent/storage/production_cable.sqlite3
32 | migrations_paths: db/cable_migrate
33 |
--------------------------------------------------------------------------------
/src/templates/crud-products/workspace/store/app/controllers/products_controller.rb:
--------------------------------------------------------------------------------
1 | class ProductsController < ApplicationController
2 | before_action :set_product, only: %i[ show edit update destroy]
3 |
4 | def index
5 | @products = Product.all
6 | end
7 |
8 | def show
9 | end
10 |
11 | def new
12 | @product = Product.new
13 | end
14 |
15 | def create
16 | @product = Product.new(product_params)
17 | if @product.save
18 | redirect_to @product
19 | else
20 | render :new, status: :unprocessable_entity
21 | end
22 | end
23 |
24 | def edit
25 | end
26 |
27 | def update
28 | if @product.update(product_params)
29 | redirect_to @product
30 | else
31 | render :edit, status: :unprocessable_entity
32 | end
33 | end
34 |
35 | def destroy
36 | @product.destroy
37 | redirect_to products_path
38 | end
39 |
40 | private
41 | def set_product
42 | @product = Product.find(params[:id])
43 | end
44 |
45 | def product_params
46 | params.expect(product: [ :name ])
47 | end
48 | end
49 |
--------------------------------------------------------------------------------
/src/templates/rails-new/workspace/store/config/locales/en.yml:
--------------------------------------------------------------------------------
1 | # Files in the config/locales directory are used for internationalization and
2 | # are automatically loaded by Rails. If you want to use locales other than
3 | # English, add the necessary files in this directory.
4 | #
5 | # To use the locales, use `I18n.t`:
6 | #
7 | # I18n.t "hello"
8 | #
9 | # In views, this is aliased to just `t`:
10 | #
11 | # <%= t("hello") %>
12 | #
13 | # To use a different locale, set it with `I18n.locale`:
14 | #
15 | # I18n.locale = :es
16 | #
17 | # This would use the information in config/locales/es.yml.
18 | #
19 | # To learn more about the API, please read the Rails Internationalization guide
20 | # at https://guides.rubyonrails.org/i18n.html.
21 | #
22 | # Be aware that YAML interprets the following case-insensitive strings as
23 | # booleans: `true`, `false`, `on`, `off`, `yes`, `no`. Therefore, these strings
24 | # must be quoted to be interpreted as strings. For example:
25 | #
26 | # en:
27 | # "yes": yup
28 | # enabled: "ON"
29 |
30 | en:
31 | hello: "Hello world"
32 |
--------------------------------------------------------------------------------
/src/content/tutorial/9-authentication/2-adding-log-out/_files/workspace/store/app/views/layouts/application.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | <%= content_for(:title) || "Store" %>
5 |
6 |
7 |
8 | <%= csrf_meta_tags %>
9 | <%= csp_meta_tag %>
10 |
11 | <%= yield :head %>
12 |
13 | <%# Enable PWA manifest for installable apps (make sure to enable in config/routes.rb too!) %>
14 | <%#= tag.link rel: "manifest", href: pwa_manifest_path(format: :json) %>
15 |
16 |
17 |
18 |
19 |
20 | <%# Includes all stylesheet files in app/assets/stylesheets %>
21 | <%= stylesheet_link_tag :app, "data-turbo-track": "reload" %>
22 |
23 |
24 |
25 | <%= yield %>
26 |
27 |
28 |
--------------------------------------------------------------------------------
/src/templates/rails-new/workspace/store/app/views/layouts/application.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | <%= content_for(:title) || "Store" %>
5 |
6 |
7 |
8 | <%= csrf_meta_tags %>
9 | <%= csp_meta_tag %>
10 |
11 | <%= yield :head %>
12 |
13 | <%# Enable PWA manifest for installable apps (make sure to enable in config/routes.rb too!) %>
14 | <%#= tag.link rel: "manifest", href: pwa_manifest_path(format: :json) %>
15 |
16 |
17 |
18 |
19 |
20 | <%# Includes all stylesheet files in app/assets/stylesheets %>
21 | <%= stylesheet_link_tag :app, "data-turbo-track": "reload" %>
22 | <%= javascript_importmap_tags %>
23 |
24 |
25 |
26 | <%= yield %>
27 |
28 |
29 |
--------------------------------------------------------------------------------
/src/content/tutorial/9-authentication/2-adding-log-out/_files/workspace/store/app/controllers/products_controller.rb:
--------------------------------------------------------------------------------
1 | class ProductsController < ApplicationController
2 | before_action :set_product, only: %i[ show edit update destroy]
3 |
4 | def index
5 | @products = Product.all
6 | end
7 |
8 | def show
9 | end
10 |
11 | def new
12 | @product = Product.new
13 | end
14 |
15 | def create
16 | @product = Product.new(product_params)
17 | if @product.save
18 | redirect_to @product
19 | else
20 | render :new, status: :unprocessable_entity
21 | end
22 | end
23 |
24 | def edit
25 | end
26 |
27 | def update
28 | if @product.update(product_params)
29 | redirect_to @product
30 | else
31 | render :edit, status: :unprocessable_entity
32 | end
33 | end
34 |
35 | def destroy
36 | @product.destroy
37 | redirect_to products_path
38 | end
39 |
40 | private
41 | def set_product
42 | @product = Product.find(params[:id])
43 | end
44 |
45 | def product_params
46 | params.expect(product: [ :name ])
47 | end
48 | end
49 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "rails-tutorial",
3 | "version": "0.0.1",
4 | "type": "module",
5 | "private": true,
6 | "scripts": {
7 | "astro": "astro",
8 | "build": "astro check && astro build",
9 | "dev": "astro dev",
10 | "preview": "astro preview",
11 | "start": "astro dev",
12 | "postinstall": "patch-package"
13 | },
14 | "dependencies": {
15 | "@codemirror/lang-yaml": "^6.1.2",
16 | "@codemirror/legacy-modes": "^6.5.1",
17 | "@tutorialkit/react": "1.5.0",
18 | "react": "^18.3.1",
19 | "react-dom": "^18.3.1"
20 | },
21 | "devDependencies": {
22 | "@astrojs/check": "^0.7.0",
23 | "@astrojs/react": "^3.6.0",
24 | "@flydotio/dockerfile": "^0.7.10",
25 | "@tutorialkit/astro": "1.5.0",
26 | "@tutorialkit/theme": "1.5.0",
27 | "@tutorialkit/types": "1.5.0",
28 | "@types/mdast": "^4.0.4",
29 | "@types/node": "^20.14.6",
30 | "@types/react": "^18.3.3",
31 | "astro": "^4.15.0",
32 | "patch-package": "^8.0.0",
33 | "prettier-plugin-astro": "^0.14.1",
34 | "typescript": "^5.4.5",
35 | "unist-util-visit": "^5.0.0"
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/content/tutorial/9-authentication/3-unauthenticated-access/_files/workspace/store/app/controllers/products_controller.rb:
--------------------------------------------------------------------------------
1 | class ProductsController < ApplicationController
2 | before_action :set_product, only: %i[ show edit update destroy]
3 |
4 | def index
5 | @products = Product.all
6 | end
7 |
8 | def show
9 | end
10 |
11 | def new
12 | @product = Product.new
13 | end
14 |
15 | def create
16 | @product = Product.new(product_params)
17 | if @product.save
18 | redirect_to @product
19 | else
20 | render :new, status: :unprocessable_entity
21 | end
22 | end
23 |
24 | def edit
25 | end
26 |
27 | def update
28 | if @product.update(product_params)
29 | redirect_to @product
30 | else
31 | render :edit, status: :unprocessable_entity
32 | end
33 | end
34 |
35 | def destroy
36 | @product.destroy
37 | redirect_to products_path
38 | end
39 |
40 | private
41 | def set_product
42 | @product = Product.find(params[:id])
43 | end
44 |
45 | def product_params
46 | params.expect(product: [ :name ])
47 | end
48 | end
49 |
--------------------------------------------------------------------------------
/src/templates/default/lib/commands.js:
--------------------------------------------------------------------------------
1 | import { createRackServer } from "./server.js";
2 | import IRBRepl from "./irb.js";
3 |
4 | export default class ExternalCommands {
5 | constructor() {
6 | this.command = undefined;
7 | }
8 |
9 | server(port) {
10 | this.command = async function(vm) {
11 | const server = await createRackServer(vm, {skipRackup: true});
12 |
13 | server.listen(port, () => {
14 | console.log(`Express.js server started on port ${port}`);
15 | console.log(`Use Ctrl-C to stop`);
16 | });
17 |
18 | // FIXME: doesn't work; do WebContainers/jsh support signals at all?
19 | process.on('exit', async () => {
20 | console.log('Express.js server is shutting down');
21 | await vm.evalAsync(`execute_at_exit_hooks`)
22 | });
23 | }
24 | }
25 |
26 | console() {
27 | this.command = async function(vm) {
28 | const irb = new IRBRepl(vm);
29 | return irb.start();
30 | }
31 | }
32 |
33 | // Invokes a registered command if any
34 | invoke(vm) {
35 | if (!this.command) return;
36 |
37 | return this.command(vm);
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/templates/authentication/workspace/store/app/controllers/products_controller.rb:
--------------------------------------------------------------------------------
1 | class ProductsController < ApplicationController
2 | allow_unauthenticated_access only: %i[ index show ]
3 | before_action :set_product, only: %i[ show edit update destroy]
4 |
5 | def index
6 | @products = Product.all
7 | end
8 |
9 | def show
10 | end
11 |
12 | def new
13 | @product = Product.new
14 | end
15 |
16 | def create
17 | @product = Product.new(product_params)
18 | if @product.save
19 | redirect_to @product
20 | else
21 | render :new, status: :unprocessable_entity
22 | end
23 | end
24 |
25 | def edit
26 | end
27 |
28 | def update
29 | if @product.update(product_params)
30 | redirect_to @product
31 | else
32 | render :edit, status: :unprocessable_entity
33 | end
34 | end
35 |
36 | def destroy
37 | @product.destroy
38 | redirect_to products_path
39 | end
40 |
41 | private
42 | def set_product
43 | @product = Product.find(params[:id])
44 | end
45 |
46 | def product_params
47 | params.expect(product: [ :name ])
48 | end
49 | end
50 |
--------------------------------------------------------------------------------
/src/content/tutorial/6-active-record/5-finding-records/content.md:
--------------------------------------------------------------------------------
1 | ---
2 | type: lesson
3 | title: Finding Records
4 | focus: /workspace/store/app/models/product.rb
5 | custom:
6 | shell:
7 | workdir: "/workspace/store"
8 | ---
9 |
10 | ### Finding Records
11 |
12 | What if we want to find one specific record?
13 |
14 | We can do this by using the `find` class method to look up a single record by
15 | ID. Call the method and pass in the specific ID by using the following code:
16 |
17 | ```irb
18 | store(dev)> Product.find(1)
19 | Product Load (0.2ms) SELECT "products".* FROM "products" WHERE "products"."id" = 1 LIMIT 1 /*application='Store'*/
20 | => #
21 | ```
22 |
23 | This generates a `SELECT` query but specifies a `WHERE` for the `id` column
24 | matching the ID of `1` that was passed in. It also adds a `LIMIT` to only return
25 | a single record.
26 |
27 | This time, we get a `Product` instance instead of an `ActiveRecord::Relation`
28 | since we're only retrieving a single record from the database.
29 |
--------------------------------------------------------------------------------
/src/templates/active-storage/workspace/store/app/controllers/products_controller.rb:
--------------------------------------------------------------------------------
1 | class ProductsController < ApplicationController
2 | allow_unauthenticated_access only: %i[ index show ]
3 | before_action :set_product, only: %i[ show edit update destroy]
4 |
5 | def index
6 | @products = Product.all
7 | end
8 |
9 | def show
10 | end
11 |
12 | def new
13 | @product = Product.new
14 | end
15 |
16 | def create
17 | @product = Product.new(product_params)
18 | if @product.save
19 | redirect_to @product
20 | else
21 | render :new, status: :unprocessable_entity
22 | end
23 | end
24 |
25 | def edit
26 | end
27 |
28 | def update
29 | if @product.update(product_params)
30 | redirect_to @product
31 | else
32 | render :edit, status: :unprocessable_entity
33 | end
34 | end
35 |
36 | def destroy
37 | @product.destroy
38 | redirect_to products_path
39 | end
40 |
41 | private
42 | def set_product
43 | @product = Product.find(params[:id])
44 | end
45 |
46 | def product_params
47 | params.expect(product: [ :name, :description, :featured_image ])
48 | end
49 | end
50 |
--------------------------------------------------------------------------------
/src/templates/authentication/workspace/store/app/controllers/passwords_controller.rb:
--------------------------------------------------------------------------------
1 | class PasswordsController < ApplicationController
2 | allow_unauthenticated_access
3 | before_action :set_user_by_token, only: %i[ edit update ]
4 |
5 | def new
6 | end
7 |
8 | def create
9 | if user = User.find_by(email_address: params[:email_address])
10 | PasswordsMailer.reset(user).deliver_later
11 | end
12 |
13 | redirect_to new_session_path, notice: "Password reset instructions sent (if user with that email address exists)."
14 | end
15 |
16 | def edit
17 | end
18 |
19 | def update
20 | if @user.update(params.permit(:password, :password_confirmation))
21 | redirect_to new_session_path, notice: "Password has been reset."
22 | else
23 | redirect_to edit_password_path(params[:token]), alert: "Passwords did not match."
24 | end
25 | end
26 |
27 | private
28 | def set_user_by_token
29 | @user = User.find_by_password_reset_token!(params[:token])
30 | rescue ActiveSupport::MessageVerifier::InvalidSignature
31 | redirect_to new_password_path, alert: "Password reset link is invalid or has expired."
32 | end
33 | end
34 |
--------------------------------------------------------------------------------
/src/templates/active-storage/workspace/store/db/migrate/20250618002538_create_action_text_tables.action_text.rb:
--------------------------------------------------------------------------------
1 | # This migration comes from action_text (originally 20180528164100)
2 | class CreateActionTextTables < ActiveRecord::Migration[6.0]
3 | def change
4 | # Use Active Record's configured type for primary and foreign keys
5 | primary_key_type, foreign_key_type = primary_and_foreign_key_types
6 |
7 | create_table :action_text_rich_texts, id: primary_key_type do |t|
8 | t.string :name, null: false
9 | t.text :body, size: :long
10 | t.references :record, null: false, polymorphic: true, index: false, type: foreign_key_type
11 |
12 | t.timestamps
13 |
14 | t.index [ :record_type, :record_id, :name ], name: "index_action_text_rich_texts_uniqueness", unique: true
15 | end
16 | end
17 |
18 | private
19 | def primary_and_foreign_key_types
20 | config = Rails.configuration.generators
21 | setting = config.options[config.orm][:primary_key_type]
22 | primary_key_type = setting || :primary_key
23 | foreign_key_type = setting || :bigint
24 | [ primary_key_type, foreign_key_type ]
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/src/content/tutorial/12-file-uploads/1-active-storage/_files/workspace/store/app/controllers/products_controller.rb:
--------------------------------------------------------------------------------
1 | class ProductsController < ApplicationController
2 | allow_unauthenticated_access only: %i[ index show ]
3 | before_action :set_product, only: %i[ show edit update destroy]
4 |
5 | def index
6 | @products = Product.all
7 | end
8 |
9 | def show
10 | end
11 |
12 | def new
13 | @product = Product.new
14 | end
15 |
16 | def create
17 | @product = Product.new(product_params)
18 | if @product.save
19 | redirect_to @product
20 | else
21 | render :new, status: :unprocessable_entity
22 | end
23 | end
24 |
25 | def edit
26 | end
27 |
28 | def update
29 | if @product.update(product_params)
30 | redirect_to @product
31 | else
32 | render :edit, status: :unprocessable_entity
33 | end
34 | end
35 |
36 | def destroy
37 | @product.destroy
38 | redirect_to products_path
39 | end
40 |
41 | private
42 | def set_product
43 | @product = Product.find(params[:id])
44 | end
45 |
46 | def product_params
47 | params.expect(product: [ :name, :description ])
48 | end
49 | end
50 |
--------------------------------------------------------------------------------
/src/content/tutorial/9-authentication/2-adding-log-out/content.md:
--------------------------------------------------------------------------------
1 | ---
2 | type: lesson
3 | title: Adding Log Out
4 | focus: /workspace/store/app/views/layouts/application.html.erb
5 | previews:
6 | - 3000
7 | custom:
8 | shell:
9 | workdir: "/workspace/store"
10 | ---
11 |
12 | ### Adding Log Out
13 |
14 | To log out of the application, we can add a button to the top of
15 | `app/views/layouts/application.html.erb`. This layout is where you put HTML that
16 | you want to include in every page like a header or footer.
17 |
18 | Add a small `` section inside the `` with a link to Home and a Log
19 | out button and wrap `yield` with a `` tag.
20 |
21 | ```erb ins={5-8,10,12}
22 |
23 |
24 |
25 |
26 |
27 | <%= link_to "Home", root_path %>
28 | <%= button_to "Log out", session_path, method: :delete if authenticated? %>
29 |
30 |
31 |
32 | <%= yield %>
33 |
34 |
35 |
36 | ```
37 |
38 | This will display a Log out button only if the user is authenticated. When
39 | clicked, it will send a DELETE request to the session path which will log the
40 | user out.
41 |
--------------------------------------------------------------------------------
/src/templates/stock-notifications/workspace/store/app/controllers/products_controller.rb:
--------------------------------------------------------------------------------
1 | class ProductsController < ApplicationController
2 | allow_unauthenticated_access only: %i[ index show ]
3 | before_action :set_product, only: %i[ show edit update destroy]
4 |
5 | def index
6 | @products = Product.all
7 | end
8 |
9 | def show
10 | end
11 |
12 | def new
13 | @product = Product.new
14 | end
15 |
16 | def create
17 | @product = Product.new(product_params)
18 | if @product.save
19 | redirect_to @product
20 | else
21 | render :new, status: :unprocessable_entity
22 | end
23 | end
24 |
25 | def edit
26 | end
27 |
28 | def update
29 | if @product.update(product_params)
30 | redirect_to @product
31 | else
32 | render :edit, status: :unprocessable_entity
33 | end
34 | end
35 |
36 | def destroy
37 | @product.destroy
38 | redirect_to products_path
39 | end
40 |
41 | private
42 | def set_product
43 | @product = Product.find(params[:id])
44 | end
45 |
46 | def product_params
47 | params.expect(product: [ :name, :description, :featured_image, :inventory_count ])
48 | end
49 | end
50 |
--------------------------------------------------------------------------------
/src/templates/create-products/workspace/store/db/schema.rb:
--------------------------------------------------------------------------------
1 | # This file is auto-generated from the current state of the database. Instead
2 | # of editing this file, please use the migrations feature of Active Record to
3 | # incrementally modify your database, and then regenerate this schema definition.
4 | #
5 | # This file is the source Rails uses to define your schema when running `bin/rails
6 | # db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to
7 | # be faster and is potentially less error prone than running all of your
8 | # migrations from scratch. Old migrations may fail to apply correctly if those
9 | # migrations use external dependencies or application code.
10 | #
11 | # It's strongly recommended that you check this file into your version control system.
12 |
13 | ActiveRecord::Schema[8.0].define(version: 2025_05_21_010850) do
14 | # These are extensions that must be enabled in order to support this database
15 | enable_extension "pg_catalog.plpgsql"
16 |
17 | create_table "products", force: :cascade do |t|
18 | t.string "name"
19 | t.datetime "created_at", precision: nil, null: false
20 | t.datetime "updated_at", precision: nil, null: false
21 | end
22 | end
23 |
--------------------------------------------------------------------------------
/src/content/tutorial/14-in-stock-notifications/5-unsubscribe-links/_files/workspace/store/app/controllers/products_controller.rb:
--------------------------------------------------------------------------------
1 | class ProductsController < ApplicationController
2 | allow_unauthenticated_access only: %i[ index show ]
3 | before_action :set_product, only: %i[ show edit update destroy]
4 |
5 | def index
6 | @products = Product.all
7 | end
8 |
9 | def show
10 | end
11 |
12 | def new
13 | @product = Product.new
14 | end
15 |
16 | def create
17 | @product = Product.new(product_params)
18 | if @product.save
19 | redirect_to @product
20 | else
21 | render :new, status: :unprocessable_entity
22 | end
23 | end
24 |
25 | def edit
26 | end
27 |
28 | def update
29 | if @product.update(product_params)
30 | redirect_to @product
31 | else
32 | render :edit, status: :unprocessable_entity
33 | end
34 | end
35 |
36 | def destroy
37 | @product.destroy
38 | redirect_to products_path
39 | end
40 |
41 | private
42 | def set_product
43 | @product = Product.find(params[:id])
44 | end
45 |
46 | def product_params
47 | params.expect(product: [ :name, :description, :featured_image, :inventory_count ])
48 | end
49 | end
50 |
--------------------------------------------------------------------------------
/src/content/tutorial/14-in-stock-notifications/2-adding-subscribers/_files/workspace/store/app/controllers/products_controller.rb:
--------------------------------------------------------------------------------
1 | class ProductsController < ApplicationController
2 | allow_unauthenticated_access only: %i[ index show ]
3 | before_action :set_product, only: %i[ show edit update destroy]
4 |
5 | def index
6 | @products = Product.all
7 | end
8 |
9 | def show
10 | end
11 |
12 | def new
13 | @product = Product.new
14 | end
15 |
16 | def create
17 | @product = Product.new(product_params)
18 | if @product.save
19 | redirect_to @product
20 | else
21 | render :new, status: :unprocessable_entity
22 | end
23 | end
24 |
25 | def edit
26 | end
27 |
28 | def update
29 | if @product.update(product_params)
30 | redirect_to @product
31 | else
32 | render :edit, status: :unprocessable_entity
33 | end
34 | end
35 |
36 | def destroy
37 | @product.destroy
38 | redirect_to products_path
39 | end
40 |
41 | private
42 | def set_product
43 | @product = Product.find(params[:id])
44 | end
45 |
46 | def product_params
47 | params.expect(product: [ :name, :description, :featured_image, :inventory_count ])
48 | end
49 | end
50 |
--------------------------------------------------------------------------------
/src/content/tutorial/14-in-stock-notifications/4-extracting-a-concern/_files/workspace/store/app/controllers/products_controller.rb:
--------------------------------------------------------------------------------
1 | class ProductsController < ApplicationController
2 | allow_unauthenticated_access only: %i[ index show ]
3 | before_action :set_product, only: %i[ show edit update destroy]
4 |
5 | def index
6 | @products = Product.all
7 | end
8 |
9 | def show
10 | end
11 |
12 | def new
13 | @product = Product.new
14 | end
15 |
16 | def create
17 | @product = Product.new(product_params)
18 | if @product.save
19 | redirect_to @product
20 | else
21 | render :new, status: :unprocessable_entity
22 | end
23 | end
24 |
25 | def edit
26 | end
27 |
28 | def update
29 | if @product.update(product_params)
30 | redirect_to @product
31 | else
32 | render :edit, status: :unprocessable_entity
33 | end
34 | end
35 |
36 | def destroy
37 | @product.destroy
38 | redirect_to products_path
39 | end
40 |
41 | private
42 | def set_product
43 | @product = Product.find(params[:id])
44 | end
45 |
46 | def product_params
47 | params.expect(product: [ :name, :description, :featured_image, :inventory_count ])
48 | end
49 | end
50 |
--------------------------------------------------------------------------------
/src/content/tutorial/7-request-journey/4-rails-routes-explained/content.md:
--------------------------------------------------------------------------------
1 | ---
2 | type: lesson
3 | title: Rails Routes Explained
4 | editor: false
5 | custom:
6 | shell:
7 | workdir: "/workspace/store"
8 | ---
9 |
10 | Rails Routes Explained
11 | ------
12 |
13 | When Rails sees a request that matches, it will send the request to the
14 | `ProductsController` and the `index` action inside of that controller. This is
15 | how Rails will process the request and return a response to the browser.
16 |
17 | You'll notice that we don't need to specify the protocol, domain, or query
18 | params in our routes. That's basically because the protocol and domain make sure
19 | the request reaches your server. From there, Rails picks up the request and
20 | knows which path to use for responding to the request based on what routes are
21 | defined. The query params are like options that Rails can use to apply to the
22 | request, so they are typically used in the controller for filtering the data.
23 |
24 |
25 |
26 |
27 |
28 |
29 | Let's look at more examples.
30 |
--------------------------------------------------------------------------------
/src/plugins/remarkRailsPathLinks.ts:
--------------------------------------------------------------------------------
1 | import type { Root, InlineCode } from 'mdast';
2 | import type { Plugin } from 'unified';
3 | import { visit } from 'unist-util-visit';
4 |
5 | // Rails path patterns
6 | const RAILS_PATH_PATTERN = /^(app|db|config|test)\/.+$/;
7 |
8 | export interface RailsPathLinkOptions {
9 | className?: string;
10 | }
11 |
12 | const remarkRailsPathLinks: Plugin<[RailsPathLinkOptions?], Root> = (options = {}) => {
13 | const className = options.className || 'rails-path-link';
14 |
15 | return (tree, file) => {
16 | visit(tree, 'inlineCode', (node: InlineCode, index, parent) => {
17 | if (!parent || typeof index !== 'number') return;
18 |
19 | // Check if the inline code content matches Rails path pattern
20 | const content = node.value;
21 | if (!RAILS_PATH_PATTERN.test(content)) return;
22 |
23 | // Replace the inline code node with an HTML node containing a link
24 | const htmlNode = {
25 | type: 'html',
26 | value: `${content} `
27 | };
28 |
29 | parent.children[index] = htmlNode as any;
30 | });
31 | };
32 | };
33 |
34 | export default remarkRailsPathLinks;
35 |
--------------------------------------------------------------------------------
/src/templates/rails-new/workspace/store/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 Store
10 | class Application < Rails::Application
11 | # Initialize configuration defaults for originally generated Rails version.
12 | config.load_defaults 8.0
13 |
14 | # Please, add to the `ignore` list any other `lib` subdirectories that do
15 | # not contain `.rb` files, or that should not be reloaded or eager loaded.
16 | # Common ones are `templates`, `generators`, or `middleware`, for example.
17 | config.autoload_lib(ignore: %w[assets tasks])
18 |
19 | # Configuration for the application, engines, and railties goes here.
20 | #
21 | # These settings can be overridden in specific environments using the files
22 | # in config/environments, which are processed later.
23 | #
24 | # config.time_zone = "Central Time (US & Canada)"
25 | # config.eager_load_paths << Rails.root.join("extras")
26 |
27 | # Don't generate system test files.
28 | config.generators.system_tests = nil
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/src/templates/rails-new/workspace/store/config/initializers/content_security_policy.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Define an application-wide content security policy.
4 | # See the Securing Rails Applications Guide for more information:
5 | # https://guides.rubyonrails.org/security.html#content-security-policy-header
6 |
7 | # Rails.application.configure do
8 | # config.content_security_policy do |policy|
9 | # policy.default_src :self, :https
10 | # policy.font_src :self, :https, :data
11 | # policy.img_src :self, :https, :data
12 | # policy.object_src :none
13 | # policy.script_src :self, :https
14 | # policy.style_src :self, :https
15 | # # Specify URI for violation reports
16 | # # policy.report_uri "/csp-violation-report-endpoint"
17 | # end
18 | #
19 | # # Generate session nonces for permitted importmap, inline scripts, and inline styles.
20 | # config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s }
21 | # config.content_security_policy_nonce_directives = %w(script-src style-src)
22 | #
23 | # # Report violations without enforcing the policy.
24 | # # config.content_security_policy_report_only = true
25 | # end
26 |
--------------------------------------------------------------------------------
/src/content/tutorial/14-in-stock-notifications/3-in-stock-email-notifications/_files/workspace/store/app/controllers/products_controller.rb:
--------------------------------------------------------------------------------
1 | class ProductsController < ApplicationController
2 | allow_unauthenticated_access only: %i[ index show ]
3 | before_action :set_product, only: %i[ show edit update destroy]
4 |
5 | def index
6 | @products = Product.all
7 | end
8 |
9 | def show
10 | end
11 |
12 | def new
13 | @product = Product.new
14 | end
15 |
16 | def create
17 | @product = Product.new(product_params)
18 | if @product.save
19 | redirect_to @product
20 | else
21 | render :new, status: :unprocessable_entity
22 | end
23 | end
24 |
25 | def edit
26 | end
27 |
28 | def update
29 | if @product.update(product_params)
30 | redirect_to @product
31 | else
32 | render :edit, status: :unprocessable_entity
33 | end
34 | end
35 |
36 | def destroy
37 | @product.destroy
38 | redirect_to products_path
39 | end
40 |
41 | private
42 | def set_product
43 | @product = Product.find(params[:id])
44 | end
45 |
46 | def product_params
47 | params.expect(product: [ :name, :description, :featured_image, :inventory_count ])
48 | end
49 | end
50 |
--------------------------------------------------------------------------------
/src/templates/default/bin/ruby:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env -S node --disable-warning=ExperimentalWarning
2 |
3 | import initVM from "../lib/rails.js";
4 | import ExternalCommands from "../lib/commands.js";
5 | import util from "node:util";
6 | import fs from "node:fs";
7 |
8 | const commands = new ExternalCommands();
9 | // Make commands accessible from Ruby via global JS object
10 | global.externalCommands = commands;
11 |
12 | const args = process.argv.slice(2);
13 |
14 | if (args[0] && fs.existsSync(process.cwd() + '/' + args[0])) {
15 | const firstLine = fs.readFileSync(process.cwd() + '/' + args[0], 'utf8').split('\n')[0];
16 | // Check if called via shebang
17 | if (firstLine.includes('#!/usr/bin/env ruby')) {
18 | const path = args.shift();
19 | const vm = await initVM({ env: { "HOME": "/rails-vm" } });
20 |
21 | await vm.evalAsync(`
22 | args = ${util.inspect(args)}
23 | ARGV.replace(args)
24 |
25 | begin
26 | load "./${path}"
27 | execute_at_exit_hooks unless Wasmify::ExternalCommands.any?
28 | rescue SystemExit
29 | end
30 | `);
31 |
32 | commands.invoke(vm)
33 | }
34 | } else {
35 | const vm = await initVM({ env: { "HOME": "/rails-vm" }, args, skipRails: true });
36 | commands.invoke(vm)
37 | }
38 |
--------------------------------------------------------------------------------
/src/content/tutorial/15-css-and-js/3-hotwire/content.md:
--------------------------------------------------------------------------------
1 | ---
2 | type: lesson
3 | title: Hotwire
4 | previews:
5 | - 3000
6 | custom:
7 | shell:
8 | workdir: "/workspace/store"
9 | ---
10 |
11 | ### Hotwire
12 |
13 | Hotwire is a JavaScript framework designed to take full advantage of server-side
14 | generated HTML. It is comprised of 3 core components:
15 |
16 | 1. [**Turbo**](https://turbo.hotwired.dev/) handles navigation, form
17 | submissions, page components, and updates without writing any custom
18 | JavaScript.
19 | 2. [**Stimulus**](https://stimulus.hotwired.dev/) provides a framework for when
20 | you need custom JavaScript to add functionality to the page.
21 | 3. [**Native**](https://native.hotwired.dev/) allows you to make hybrid mobile
22 | apps by embedding your web app and progressively enhancing it with native
23 | mobile features.
24 |
25 | We haven't written any JavaScript yet, but we have been using Hotwire on the
26 | frontend. For instance, the form you created to add and edit a product was
27 | powered by Turbo.
28 |
29 | Learn more in the [Asset Pipeline](https://guides.rubyonrails.org/asset_pipeline.html) and
30 | [Working with JavaScript in Rails](https://guides.rubyonrails.org/working_with_javascript_in_rails.html)
31 | guides.
32 |
--------------------------------------------------------------------------------
/src/content/tutorial/8-controllers/2-making-requests/content.md:
--------------------------------------------------------------------------------
1 | ---
2 | type: lesson
3 | title: Making Requests
4 | focus: /workspace/store/app/controllers/products_controller.rb
5 | previews:
6 | - 3000
7 | custom:
8 | shell:
9 | workdir: "/workspace/store"
10 | ---
11 |
12 | ### Making Requests
13 |
14 | Let's see this in our browser.
15 |
16 | First, run `bin/rails server` in your terminal to
17 | start the Rails server. You will see the Rails welcome page in the preview pane.
18 |
19 | Go to http://localhost:3000/products, Rails will render the
20 | products index HTML.
21 |
22 | Our browser requested `/products` and Rails matched this route to
23 | `products#index`. Rails sent the request to the `ProductsController` and called
24 | the `index` action. Since this action was empty, Rails rendered the matching
25 | template at `app/views/products/index.html.erb` and returned that to our
26 | browser. Pretty cool!
27 |
28 | If we open `config/routes.rb`, we can tell Rails the root route should render
29 | the Products index action by adding this line:
30 |
31 | ```ruby
32 | root "products#index"
33 | ```
34 |
35 | Restart the server (by stopping it vis Ctrl+C and starting again), got to http://localhost:3000, and you will see that Rails will render `Products#index` at the home screen.
36 |
--------------------------------------------------------------------------------
/src/content/tutorial/9-authentication/3-unauthenticated-access/_files/workspace/store/app/views/layouts/application.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | <%= content_for(:title) || "Store" %>
5 |
6 |
7 |
8 | <%= csrf_meta_tags %>
9 | <%= csp_meta_tag %>
10 |
11 | <%= yield :head %>
12 |
13 | <%# Enable PWA manifest for installable apps (make sure to enable in config/routes.rb too!) %>
14 | <%#= tag.link rel: "manifest", href: pwa_manifest_path(format: :json) %>
15 |
16 |
17 |
18 |
19 |
20 | <%# Includes all stylesheet files in app/assets/stylesheets %>
21 | <%= stylesheet_link_tag :app, "data-turbo-track": "reload" %>
22 |
23 |
24 |
25 |
26 | <%= link_to "Home", root_path %>
27 | <%= button_to "Log out", session_path, method: :delete if authenticated? %>
28 |
29 |
30 |
31 | <%= yield %>
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/src/content/tutorial/9-authentication/4-authenticated-links/_files/workspace/store/app/views/layouts/application.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | <%= content_for(:title) || "Store" %>
5 |
6 |
7 |
8 | <%= csrf_meta_tags %>
9 | <%= csp_meta_tag %>
10 |
11 | <%= yield :head %>
12 |
13 | <%# Enable PWA manifest for installable apps (make sure to enable in config/routes.rb too!) %>
14 | <%#= tag.link rel: "manifest", href: pwa_manifest_path(format: :json) %>
15 |
16 |
17 |
18 |
19 |
20 | <%# Includes all stylesheet files in app/assets/stylesheets %>
21 | <%= stylesheet_link_tag :app, "data-turbo-track": "reload" %>
22 |
23 |
24 |
25 |
26 | <%= link_to "Home", root_path %>
27 | <%= button_to "Log out", session_path, method: :delete if authenticated? %>
28 |
29 |
30 |
31 | <%= yield %>
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/src/content/tutorial/6-active-record/7-deleting-records/content.md:
--------------------------------------------------------------------------------
1 | ---
2 | type: lesson
3 | title: Deleting Records
4 | focus: /workspace/store/app/models/product.rb
5 | custom:
6 | shell:
7 | workdir: "/workspace/store"
8 | ---
9 |
10 | ### Deleting Records
11 |
12 | The `destroy` method can be used to delete a record from the database.
13 |
14 | ```irb
15 | store(dev)> product.destroy
16 | TRANSACTION (0.1ms) BEGIN immediate TRANSACTION /*application='Store'*/
17 | Product Destroy (0.4ms) DELETE FROM "products" WHERE "products"."id" = 1 /*application='Store'*/
18 | TRANSACTION (0.1ms) COMMIT TRANSACTION /*application='Store'*/
19 | => #
20 | ```
21 |
22 | This deleted the T-Shirt product from our database. We can confirm this with
23 | `Product.all` to see that it only returns Pants.
24 |
25 | ```irb
26 | store(dev)> Product.all
27 | Product Load (1.9ms) SELECT "products".* FROM "products" /* loading for pp */ LIMIT 11 /*application='Store'*/
28 | =>
29 | [#]
34 | ```
35 |
--------------------------------------------------------------------------------
/src/templates/default/lib/database.js:
--------------------------------------------------------------------------------
1 | import { PGlite } from '@electric-sql/pglite'
2 | import { join } from "node:path";
3 |
4 | const MULTILINE_RX = /;\s*(SELECT|INSERT|UPDATE|DELETE|CREATE|ALTER|DROP|TRUNCATE|WITH|EXPLAIN|ANALYZE|VACUUM|GRANT|REVOKE|BEGIN|COMMIT|ROLLBACK)/i
5 |
6 | class ExternalInterface {
7 | constructor(db, identifier) {
8 | this.db = db;
9 | this.identifier = identifier;
10 | }
11 |
12 | async query(sql, params) {
13 | let res;
14 |
15 | if (MULTILINE_RX.test(sql)) {
16 | res = (await this.db.exec(sql, params))[0];
17 | } else {
18 | res = await this.db.query(sql, params);
19 | }
20 |
21 | return res
22 | }
23 | }
24 |
25 | export class PGLite4Rails {
26 | constructor(dataDir) {
27 | // Created databases
28 | this.dbs = {};
29 | // Base directory for all databases
30 | this.dataDir = dataDir;
31 | }
32 |
33 | async create_interface(dbname) {
34 | if (this.dbs[dbname]) return this.dbs[dbname].identifier;
35 |
36 | const dataDir = join(this.dataDir, dbname);
37 |
38 | const db = await PGlite.create({ dataDir })
39 | const ei = new ExternalInterface(db, `pglite4rails_${dbname}`)
40 |
41 | const identifier = ei.identifier
42 | global[identifier] = this.dbs[dbname] = ei
43 |
44 | return identifier
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/templates/rails-new/workspace/store/config/storage.yml:
--------------------------------------------------------------------------------
1 | test:
2 | service: Disk
3 | root: <%= Rails.root.join("tmp/storage") %>
4 |
5 | local:
6 | service: Disk
7 | root: <%= Rails.root.join("storage") %>
8 |
9 | # Use bin/rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key)
10 | # amazon:
11 | # service: S3
12 | # access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %>
13 | # secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %>
14 | # region: us-east-1
15 | # bucket: your_own_bucket-<%= Rails.env %>
16 |
17 | # Remember not to checkin your GCS keyfile to a repository
18 | # google:
19 | # service: GCS
20 | # project: your_project
21 | # credentials: <%= Rails.root.join("path/to/gcs.keyfile") %>
22 | # bucket: your_own_bucket-<%= Rails.env %>
23 |
24 | # Use bin/rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key)
25 | # microsoft:
26 | # service: AzureStorage
27 | # storage_account_name: your_account_name
28 | # storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %>
29 | # container: your_container_name-<%= Rails.env %>
30 |
31 | # mirror:
32 | # service: Mirror
33 | # primary: local
34 | # mirrors: [ amazon, google, microsoft ]
35 |
--------------------------------------------------------------------------------