diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f71affc..cdf26fb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,10 +17,9 @@ jobs: --health-timeout 2s --health-retries 5 steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v4 - uses: "ruby/setup-ruby@v1" with: - ruby-version: 3.1.2 bundler-cache: true - name: Install libvips @@ -39,7 +38,7 @@ jobs: bin/setup bin/rails standard test:all - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v4 if: failure() with: name: Capybara screenshots diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 449e9f8..cf427f5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -46,7 +46,7 @@ pick b86afaa Lesson 1: Our first lines of JavaScript pick 2fc0b6b Lesson 2: The Audio Player pick cf2942e Lesson 3: Infinite scroll pick dfaeb73 Lesson 4: Typeahead Search -pick 0d97e49 Update Tutorial Assistant on setup (#18) # <- We need to move this +pick 0d97e49 Update setup script (#18) # <- We need to move this ``` 4. Move the commit to **before** the "Getting Started" commit. @@ -71,7 +71,7 @@ pick 66255b3 Extract `PodcastScoped` concern pick 27e8306 Episode Search pick 4276531 [BACKPORT]: Promote lazily-loaded Turbo Frame navigation pick 304a037 Build assets during setup (#16) -pick 0d97e49 Update Tutorial Assistant on setup (#18) # <- Move it before "Getting Started" +pick 0d97e49 Update setup script (#18) # <- Move it before "Getting Started" pick a3ada0f Getting Started pick b86afaa Lesson 1: Our first lines of JavaScript pick 2fc0b6b Lesson 2: The Audio Player @@ -91,7 +91,7 @@ f805411 (HEAD -> main) Lesson 4: Typeahead Search 83f2ae3 Lesson 2: The Audio Player 45624d1 Lesson 1: Our first lines of JavaScript 14e6e70 Getting Started -0a4b7a9 Update Tutorial Assistant on setup (#18) # <- The commit is in the correct spot +0a4b7a9 Update setup script (#18) # <- The commit is in the correct spot 304a037 Build assets during setup (#16) 4276531 [BACKPORT]: Promote lazily-loaded Turbo Frame navigation 27e8306 Episode Search diff --git a/Gemfile.lock b/Gemfile.lock index 8108a03..8b11547 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -171,6 +171,7 @@ GEM factory_bot_rails (6.4.3) factory_bot (~> 6.4) railties (>= 5.0.0) + ffi (1.17.0) ffi (1.17.0-aarch64-linux-gnu) ffi (1.17.0-arm-linux-gnu) ffi (1.17.0-arm64-darwin) @@ -224,6 +225,7 @@ GEM matrix (0.4.2) mini_magick (4.12.0) mini_mime (1.1.5) + mini_portile2 (2.8.7) minitest (5.23.1) msgpack (1.7.2) net-imap (0.4.12) @@ -236,6 +238,9 @@ GEM net-smtp (0.5.0) net-protocol nio4r (2.7.3) + nokogiri (1.16.5) + mini_portile2 (~> 2.8.2) + racc (~> 1.4) nokogiri (1.16.5-aarch64-linux) racc (~> 1.4) nokogiri (1.16.5-arm-linux) @@ -378,6 +383,7 @@ PLATFORMS aarch64-linux arm-linux arm64-darwin + ruby x86-linux x86_64-darwin x86_64-linux @@ -415,4 +421,4 @@ RUBY VERSION ruby 3.3.0p0 BUNDLED WITH - 2.5.6 + 2.5.18 diff --git a/README.md b/README.md index bc4de7f..8e48e4f 100644 --- a/README.md +++ b/README.md @@ -3,25 +3,44 @@ **Hotwire-powered Podcast Player** 🔌 This course works by introducing a series of failing tests that you must get to -pass by introducing Hotwire patterns into the application. Once the tests pass, -move on to the next lesson. +pass by introducing Hotwire patterns into the application. + +For more Hotwire resources, check out our [blog posts]. + +[blog posts]: https://thoughtbot.com/blog/tags/hotwire ## ⚙️ Setup If this is your first time running the application, run `./bin/setup` to install dependencies and seed the database. -## 🚀 Getting Started +### Troubleshooting -Once you've setup the application locally, you can start the [lesson plan][1]. +If you run into a `Tailwindcss::Commands::ExecutableNotFoundException: Cannot find the +tailwindcss executable` error, you need to add your platform to `Gemfile.lock`. See this +[issue] for more details. -## 🏗 Local Development +[issue]: https://stackoverflow.com/a/70720842 + +## 🏗 Running the application Run `./bin/dev` to start the development server and then navigate to -[http://localhost:3000](http://localhost:3000) +[http://localhost:3000](http://localhost:3000). + +Note: Running `./bin/dev` will execute the background jobs to import the +episodes scheduled in the setup step above. It will take a few minutes. +If you run into issues due to a podcast or episode not being +found, see the jobs statuses on http://localhost:3000/good_job. +Once they are finished, the episodes should be accessible in the UI. + +## 🚀 Getting Started + +Once you've setup the application locally, you are ready to start the [lesson plan][1]. + +Once the tests pass, move on to the next lesson. ## Contributing -[Contributing](./CONTRIBUTING.md) +Please see [Contributing](./CONTRIBUTING.md). [1]: ./lessons/README.md diff --git a/app/views/episodes/podcast/_frame.html.erb b/app/views/episodes/podcast/_frame.html.erb index fa50bbe..aa05c06 100644 --- a/app/views/episodes/podcast/_frame.html.erb +++ b/app/views/episodes/podcast/_frame.html.erb @@ -42,43 +42,6 @@

<%= podcast.description %>

- -
-

- <%= inline_svg_tag "icons/graph.svg", class: "text-indigo-300 h-2.5 w-2.5" %> - - Listen -

- -
- - -
diff --git a/bin/setup b/bin/setup index ca59a43..5af3d2f 100755 --- a/bin/setup +++ b/bin/setup @@ -1,5 +1,6 @@ #!/usr/bin/env ruby require "fileutils" +require 'mkmf' # path to your application root. APP_ROOT = File.expand_path("..", __dir__) @@ -13,24 +14,23 @@ FileUtils.chdir APP_ROOT do # This script is idempotent, so that you can run it at any time and get an expectable outcome. # Add necessary setup steps to this file. + vips = MakeMakefile.find_executable('vips') + if !vips && !ENV["CI"] + puts "VIPS is not installed.\n" + printf "See https://www.libvips.org/install.html for install instructions.\n" + exit 1 + end + puts "== Installing dependencies ==" system! "gem install bundler --conservative" - system("bundle check") || system!("bundle install") - - # puts "\n== Copying sample files ==" - # unless File.exist?("config/database.yml") - # FileUtils.cp "config/database.yml.sample", "config/database.yml" - # end - puts "\n== Installing Tutorial Assistant ==" - FileUtils.remove_dir("ta", force: true) if Dir.exist?("ta") - system! "git clone git@github.com:thoughtbot/tutorial-assistant.git ta/ > /dev/null 2>&1 || true" + system("bundle check") || system!("bundle install") puts "\n== Preparing database ==" system! "bin/rails db:prepare" unless ENV["CI"] - puts "\n== Seeding database ==" + puts "\n== Seeding database. It might take a few minutes... ==" system! "bin/rails db:seed" end @@ -43,3 +43,8 @@ FileUtils.chdir APP_ROOT do puts "\n== Restarting application server ==" system! "bin/rails restart" end + +at_exit do + File.delete('mkmf.log') if File.exist?('mkmf.log') + puts "\nFinished setting up" +end diff --git a/config/routes.rb b/config/routes.rb index 0c04bd2..6fb8132 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -13,4 +13,6 @@ resolve "SearchResult" do |search_result| [search_result.podcast, search_result.episode] end + + mount GoodJob::Engine => "good_job" end diff --git a/db/migrate/20240610180532_create_index_good_jobs_jobs_on_priority_created_at_when_unfinished.rb b/db/migrate/20240610180532_create_index_good_jobs_jobs_on_priority_created_at_when_unfinished.rb index 9a6de61..866b542 100644 --- a/db/migrate/20240610180532_create_index_good_jobs_jobs_on_priority_created_at_when_unfinished.rb +++ b/db/migrate/20240610180532_create_index_good_jobs_jobs_on_priority_created_at_when_unfinished.rb @@ -12,7 +12,7 @@ def change end end - add_index :good_jobs, [:priority, :created_at], order: { priority: "DESC NULLS LAST", created_at: :asc }, + add_index :good_jobs, [:priority, :created_at], order: {priority: "DESC NULLS LAST", created_at: :asc}, where: "finished_at IS NULL", name: :index_good_jobs_jobs_on_priority_created_at_when_unfinished, algorithm: :concurrently end diff --git a/db/migrate/20240610180536_recreate_good_job_cron_indexes_with_conditional.rb b/db/migrate/20240610180536_recreate_good_job_cron_indexes_with_conditional.rb index a2d86be..4798b57 100644 --- a/db/migrate/20240610180536_recreate_good_job_cron_indexes_with_conditional.rb +++ b/db/migrate/20240610180536_recreate_good_job_cron_indexes_with_conditional.rb @@ -8,11 +8,11 @@ def change dir.up do unless connection.index_name_exists?(:good_jobs, :index_good_jobs_on_cron_key_and_created_at_cond) add_index :good_jobs, [:cron_key, :created_at], where: "(cron_key IS NOT NULL)", - name: :index_good_jobs_on_cron_key_and_created_at_cond, algorithm: :concurrently + name: :index_good_jobs_on_cron_key_and_created_at_cond, algorithm: :concurrently end unless connection.index_name_exists?(:good_jobs, :index_good_jobs_on_cron_key_and_cron_at_cond) add_index :good_jobs, [:cron_key, :cron_at], where: "(cron_key IS NOT NULL)", unique: true, - name: :index_good_jobs_on_cron_key_and_cron_at_cond, algorithm: :concurrently + name: :index_good_jobs_on_cron_key_and_cron_at_cond, algorithm: :concurrently end if connection.index_name_exists?(:good_jobs, :index_good_jobs_on_cron_key_and_created_at) @@ -26,11 +26,11 @@ def change dir.down do unless connection.index_name_exists?(:good_jobs, :index_good_jobs_on_cron_key_and_created_at) add_index :good_jobs, [:cron_key, :created_at], - name: :index_good_jobs_on_cron_key_and_created_at, algorithm: :concurrently + name: :index_good_jobs_on_cron_key_and_created_at, algorithm: :concurrently end unless connection.index_name_exists?(:good_jobs, :index_good_jobs_on_cron_key_and_cron_at) add_index :good_jobs, [:cron_key, :cron_at], unique: true, - name: :index_good_jobs_on_cron_key_and_cron_at, algorithm: :concurrently + name: :index_good_jobs_on_cron_key_and_cron_at, algorithm: :concurrently end if connection.index_name_exists?(:good_jobs, :index_good_jobs_on_cron_key_and_created_at_cond) diff --git a/db/migrate/20240610180540_create_index_good_job_jobs_for_candidate_lookup.rb b/db/migrate/20240610180540_create_index_good_job_jobs_for_candidate_lookup.rb index 806707c..ea42a27 100644 --- a/db/migrate/20240610180540_create_index_good_job_jobs_for_candidate_lookup.rb +++ b/db/migrate/20240610180540_create_index_good_job_jobs_for_candidate_lookup.rb @@ -12,7 +12,7 @@ def change end end - add_index :good_jobs, [:priority, :created_at], order: { priority: "ASC NULLS LAST", created_at: :asc }, + add_index :good_jobs, [:priority, :created_at], order: {priority: "ASC NULLS LAST", created_at: :asc}, where: "finished_at IS NULL", name: :index_good_job_jobs_for_candidate_lookup, algorithm: :concurrently end diff --git a/db/migrate/20240610180542_create_good_job_process_lock_ids.rb b/db/migrate/20240610180542_create_good_job_process_lock_ids.rb index a7b3791..b516f08 100644 --- a/db/migrate/20240610180542_create_good_job_process_lock_ids.rb +++ b/db/migrate/20240610180542_create_good_job_process_lock_ids.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + class CreateGoodJobProcessLockIds < ActiveRecord::Migration[8.0] def change reversible do |dir| diff --git a/db/migrate/20240610180543_create_good_job_process_lock_indexes.rb b/db/migrate/20240610180543_create_good_job_process_lock_indexes.rb index acf046c..aa6f633 100644 --- a/db/migrate/20240610180543_create_good_job_process_lock_indexes.rb +++ b/db/migrate/20240610180543_create_good_job_process_lock_indexes.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + class CreateGoodJobProcessLockIndexes < ActiveRecord::Migration[8.0] disable_ddl_transaction! @@ -7,23 +8,23 @@ def change dir.up do unless connection.index_name_exists?(:good_jobs, :index_good_jobs_on_priority_scheduled_at_unfinished_unlocked) add_index :good_jobs, [:priority, :scheduled_at], - order: { priority: "ASC NULLS LAST", scheduled_at: :asc }, - where: "finished_at IS NULL AND locked_by_id IS NULL", - name: :index_good_jobs_on_priority_scheduled_at_unfinished_unlocked, - algorithm: :concurrently + order: {priority: "ASC NULLS LAST", scheduled_at: :asc}, + where: "finished_at IS NULL AND locked_by_id IS NULL", + name: :index_good_jobs_on_priority_scheduled_at_unfinished_unlocked, + algorithm: :concurrently end unless connection.index_name_exists?(:good_jobs, :index_good_jobs_on_locked_by_id) add_index :good_jobs, :locked_by_id, - where: "locked_by_id IS NOT NULL", - name: :index_good_jobs_on_locked_by_id, - algorithm: :concurrently + where: "locked_by_id IS NOT NULL", + name: :index_good_jobs_on_locked_by_id, + algorithm: :concurrently end unless connection.index_name_exists?(:good_job_executions, :index_good_job_executions_on_process_id_and_created_at) add_index :good_job_executions, [:process_id, :created_at], - name: :index_good_job_executions_on_process_id_and_created_at, - algorithm: :concurrently + name: :index_good_job_executions_on_process_id_and_created_at, + algorithm: :concurrently end end diff --git a/lessons/lesson-1.md b/lessons/lesson-1.md index 6b29f94..d20c4cb 100644 --- a/lessons/lesson-1.md +++ b/lessons/lesson-1.md @@ -56,10 +56,10 @@ Now we can build the controller. ```js // app/javascript/controllers/hotkey_controller.js -import ApplicationController from "controllers/application_controller" +import { Controller } from '@hotwired/stimulus' import { install, uninstall } from "@github/hotkey" -export default class extends ApplicationController { +export default class extends Controller { static targets = ["shortcut"] shortcutTargetConnected(target) { @@ -83,7 +83,7 @@ Anytime an element with a `data-hotkey-target="shortcut"` data attribute appears on the page, this controller will enable that element to be accessed via the hotkey. Additionally, we add a `aria-keyshortcuts` attribute to the element and set the value to whatever the `hotkey` attribute is. In this case, that's -"Meta+k". We do this in an effort to exposes the existence of the shortcut to +"Meta+k". We do this in an effort to expose the existence of the shortcut to assistive technologies so the presence of the shortcut can be communicated to its users. @@ -133,6 +133,16 @@ anchor link. The [@github/hotkey][] library works by triggering a focus event on form fields, or a click event on other elements. In this case, hitting `Meta+k` will automatically click the link to the search page. +### Check in + +To complete this lesson: + +- run `./bin/rails test` to verify the tests pass +- press ⌘ k anywhere in the episodes list page to verify the search shortcut works as expected + note: using `Control` instead of ⌘ should work as well for non-Mac users + +When you're ready, move on to the next lesson by running `./ta/start-lesson 2`. + [javascript_importmap_tags]: https://github.com/rails/importmap-rails#preloading-pinned-modules [importmap]: https://github.com/WICG/import-maps [@github/hotkey]: https://github.com/github/hotkey diff --git a/test/integration/episodes_test.rb b/test/integration/episodes_test.rb index 102a4e3..b1dbafb 100644 --- a/test/integration/episodes_test.rb +++ b/test/integration/episodes_test.rb @@ -35,10 +35,6 @@ class IndexTest < ActionDispatch::IntegrationTest within :banner do assert_link episode.podcast.title, href: podcast_episodes_path(episode.podcast) assert_link text: episode.podcast.title, href: podcast_episodes_path(episode.podcast) - assert_link "Spotify" - assert_link "Apple Podcast" - assert_link "Overcast" - assert_link "RSS Feed" assert_selector :section, "About", text: episode.podcast.description, count: 1 end within :contentinfo do @@ -153,10 +149,6 @@ class ShowTest < ActionDispatch::IntegrationTest within :banner do assert_link episode.podcast.title, href: podcast_episodes_path(episode.podcast) assert_link text: episode.podcast.title, href: podcast_episodes_path(episode.podcast) - assert_link "Spotify" - assert_link "Apple Podcast" - assert_link "Overcast" - assert_link "RSS Feed" assert_selector :section, "About", text: episode.podcast.description end within :contentinfo do