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