@tutorialkit-rb/cli 1.5.2-rb.0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +15 -0
  3. package/dist/index.js +1384 -0
  4. package/package.json +66 -0
  5. package/template/.gitignore +13 -0
  6. package/template/.vscode/extensions.json +4 -0
  7. package/template/.vscode/launch.json +11 -0
  8. package/template/README.md +179 -0
  9. package/template/astro.config.ts +21 -0
  10. package/template/bin/build-wasm +30 -0
  11. package/template/icons/languages/css.svg +1 -0
  12. package/template/icons/languages/html.svg +1 -0
  13. package/template/icons/languages/js.svg +1 -0
  14. package/template/icons/languages/json.svg +1 -0
  15. package/template/icons/languages/markdown.svg +1 -0
  16. package/template/icons/languages/ruby.svg +1 -0
  17. package/template/icons/languages/sass.svg +1 -0
  18. package/template/icons/languages/ts.svg +1 -0
  19. package/template/icons/phosphor/file-erb.svg +1 -0
  20. package/template/icons/phosphor/file-rb.svg +5 -0
  21. package/template/package.json +37 -0
  22. package/template/package.json.bak +37 -0
  23. package/template/public/favicon.svg +4 -0
  24. package/template/public/logo-dark.svg +4 -0
  25. package/template/public/logo.svg +4 -0
  26. package/template/ruby-wasm/.railsrc +12 -0
  27. package/template/ruby-wasm/Gemfile +22 -0
  28. package/template/ruby-wasm/Gemfile.lock +292 -0
  29. package/template/ruby-wasm/README.md +19 -0
  30. package/template/ruby-wasm/bin/pack +16 -0
  31. package/template/ruby-wasm/boot.rb +32 -0
  32. package/template/ruby-wasm/config/wasmify.yml +23 -0
  33. package/template/src/components/FileManager.tsx +116 -0
  34. package/template/src/components/GitHubLink.astro +17 -0
  35. package/template/src/components/HeadTags.astro +65 -0
  36. package/template/src/components/HelpDropdown.tsx +72 -0
  37. package/template/src/components/RailsPathLinkHandler.tsx +107 -0
  38. package/template/src/components/ShellConfigurator.tsx +95 -0
  39. package/template/src/components/TopBar.astro +48 -0
  40. package/template/src/content/config.ts +9 -0
  41. package/template/src/content/tutorial/1-getting-started/1-creating-your-first-rails-app/_files/workspace/.keep +0 -0
  42. package/template/src/content/tutorial/1-getting-started/1-creating-your-first-rails-app/content.md +34 -0
  43. package/template/src/content/tutorial/1-getting-started/2-rails-console/_files/.tk-config.json +3 -0
  44. package/template/src/content/tutorial/1-getting-started/2-rails-console/_files/workspace/.keep +0 -0
  45. package/template/src/content/tutorial/1-getting-started/2-rails-console/content.md +37 -0
  46. package/template/src/content/tutorial/1-getting-started/meta.md +4 -0
  47. package/template/src/content/tutorial/2-controllers/2-crud-operations/_files/.tk-config.json +3 -0
  48. package/template/src/content/tutorial/2-controllers/2-crud-operations/_files/workspace/.keep +0 -0
  49. package/template/src/content/tutorial/2-controllers/2-crud-operations/content.md +99 -0
  50. package/template/src/content/tutorial/2-controllers/meta.md +4 -0
  51. package/template/src/content/tutorial/meta.md +18 -0
  52. package/template/src/env.d.ts +3 -0
  53. package/template/src/plugins/remarkRailsPathLinks.ts +39 -0
  54. package/template/src/templates/crud-products/.tk-config.json +3 -0
  55. package/template/src/templates/crud-products/workspace/.keep +0 -0
  56. package/template/src/templates/crud-products/workspace/store/app/controllers/products_controller.rb +48 -0
  57. package/template/src/templates/crud-products/workspace/store/app/models/product.rb +3 -0
  58. package/template/src/templates/crud-products/workspace/store/app/views/products/_form.html.erb +10 -0
  59. package/template/src/templates/crud-products/workspace/store/app/views/products/edit.html.erb +4 -0
  60. package/template/src/templates/crud-products/workspace/store/app/views/products/index.html.erb +11 -0
  61. package/template/src/templates/crud-products/workspace/store/app/views/products/new.html.erb +4 -0
  62. package/template/src/templates/crud-products/workspace/store/app/views/products/show.html.erb +5 -0
  63. package/template/src/templates/crud-products/workspace/store/config/routes.rb +6 -0
  64. package/template/src/templates/crud-products/workspace/store/db/migrate/20250521010850_create_products.rb +9 -0
  65. package/template/src/templates/crud-products/workspace/store/db/schema.rb +22 -0
  66. package/template/src/templates/crud-products/workspace/store/db/seeds.rb +3 -0
  67. package/template/src/templates/crud-products/workspace/store/test/fixtures/products.yml +7 -0
  68. package/template/src/templates/crud-products/workspace/store/test/models/product_test.rb +7 -0
  69. package/template/src/templates/default/bin/console +9 -0
  70. package/template/src/templates/default/bin/rackup +11 -0
  71. package/template/src/templates/default/bin/rails +41 -0
  72. package/template/src/templates/default/bin/ruby +37 -0
  73. package/template/src/templates/default/lib/commands.js +39 -0
  74. package/template/src/templates/default/lib/database.js +46 -0
  75. package/template/src/templates/default/lib/irb.js +110 -0
  76. package/template/src/templates/default/lib/patches/app_generator.rb +43 -0
  77. package/template/src/templates/default/lib/patches/authentication.rb +24 -0
  78. package/template/src/templates/default/lib/rails.js +69 -0
  79. package/template/src/templates/default/lib/server/frame_location_middleware.js +77 -0
  80. package/template/src/templates/default/lib/server.js +307 -0
  81. package/template/src/templates/default/package-lock.json +1830 -0
  82. package/template/src/templates/default/package.json +23 -0
  83. package/template/src/templates/default/pgdata/.keep +0 -0
  84. package/template/src/templates/default/scripts/createdb.js +7 -0
  85. package/template/src/templates/default/scripts/rails.js +52 -0
  86. package/template/src/templates/default/scripts/wait-for-wasm.js +103 -0
  87. package/template/src/templates/default/workspace/.keep +0 -0
  88. package/template/src/templates/rails-app/workspace/store/.ruby-version +1 -0
  89. package/template/src/templates/rails-app/workspace/store/Gemfile +37 -0
  90. package/template/src/templates/rails-app/workspace/store/README.md +24 -0
  91. package/template/src/templates/rails-app/workspace/store/Rakefile +6 -0
  92. package/template/src/templates/rails-app/workspace/store/app/assets/images/.keep +0 -0
  93. package/template/src/templates/rails-app/workspace/store/app/assets/stylesheets/application.css +10 -0
  94. package/template/src/templates/rails-app/workspace/store/app/controllers/application_controller.rb +4 -0
  95. package/template/src/templates/rails-app/workspace/store/app/helpers/application_helper.rb +2 -0
  96. package/template/src/templates/rails-app/workspace/store/app/javascript/application.js +4 -0
  97. package/template/src/templates/rails-app/workspace/store/app/javascript/controllers/application.js +9 -0
  98. package/template/src/templates/rails-app/workspace/store/app/javascript/controllers/index.js +4 -0
  99. package/template/src/templates/rails-app/workspace/store/app/jobs/application_job.rb +7 -0
  100. package/template/src/templates/rails-app/workspace/store/app/mailers/application_mailer.rb +4 -0
  101. package/template/src/templates/rails-app/workspace/store/app/models/application_record.rb +3 -0
  102. package/template/src/templates/rails-app/workspace/store/app/models/concerns/.keep +0 -0
  103. package/template/src/templates/rails-app/workspace/store/app/views/layouts/application.html.erb +28 -0
  104. package/template/src/templates/rails-app/workspace/store/app/views/layouts/mailer.html.erb +13 -0
  105. package/template/src/templates/rails-app/workspace/store/app/views/layouts/mailer.text.erb +1 -0
  106. package/template/src/templates/rails-app/workspace/store/app/views/pwa/manifest.json.erb +22 -0
  107. package/template/src/templates/rails-app/workspace/store/app/views/pwa/service-worker.js +26 -0
  108. package/template/src/templates/rails-app/workspace/store/bin/importmap +4 -0
  109. package/template/src/templates/rails-app/workspace/store/bin/rails +4 -0
  110. package/template/src/templates/rails-app/workspace/store/config/application.rb +30 -0
  111. package/template/src/templates/rails-app/workspace/store/config/boot.rb +3 -0
  112. package/template/src/templates/rails-app/workspace/store/config/cable.yml +10 -0
  113. package/template/src/templates/rails-app/workspace/store/config/credentials.yml.enc +1 -0
  114. package/template/src/templates/rails-app/workspace/store/config/database.yml +32 -0
  115. package/template/src/templates/rails-app/workspace/store/config/environment.rb +5 -0
  116. package/template/src/templates/rails-app/workspace/store/config/environments/development.rb +69 -0
  117. package/template/src/templates/rails-app/workspace/store/config/environments/production.rb +89 -0
  118. package/template/src/templates/rails-app/workspace/store/config/environments/test.rb +53 -0
  119. package/template/src/templates/rails-app/workspace/store/config/importmap.rb +9 -0
  120. package/template/src/templates/rails-app/workspace/store/config/initializers/assets.rb +7 -0
  121. package/template/src/templates/rails-app/workspace/store/config/initializers/content_security_policy.rb +25 -0
  122. package/template/src/templates/rails-app/workspace/store/config/initializers/filter_parameter_logging.rb +8 -0
  123. package/template/src/templates/rails-app/workspace/store/config/initializers/inflections.rb +16 -0
  124. package/template/src/templates/rails-app/workspace/store/config/locales/en.yml +31 -0
  125. package/template/src/templates/rails-app/workspace/store/config/master.key +1 -0
  126. package/template/src/templates/rails-app/workspace/store/config/puma.rb +41 -0
  127. package/template/src/templates/rails-app/workspace/store/config/routes.rb +4 -0
  128. package/template/src/templates/rails-app/workspace/store/config/storage.yml +34 -0
  129. package/template/src/templates/rails-app/workspace/store/config.ru +6 -0
  130. package/template/src/templates/rails-app/workspace/store/db/seeds.rb +9 -0
  131. package/template/src/templates/rails-app/workspace/store/log/.keep +0 -0
  132. package/template/src/templates/rails-app/workspace/store/public/400.html +114 -0
  133. package/template/src/templates/rails-app/workspace/store/public/404.html +114 -0
  134. package/template/src/templates/rails-app/workspace/store/public/406-unsupported-browser.html +114 -0
  135. package/template/src/templates/rails-app/workspace/store/public/422.html +114 -0
  136. package/template/src/templates/rails-app/workspace/store/public/500.html +114 -0
  137. package/template/src/templates/rails-app/workspace/store/public/icon.png +0 -0
  138. package/template/src/templates/rails-app/workspace/store/public/icon.svg +3 -0
  139. package/template/src/templates/rails-app/workspace/store/public/robots.txt +1 -0
  140. package/template/src/templates/rails-app/workspace/store/script/.keep +0 -0
  141. package/template/src/templates/rails-app/workspace/store/storage/.keep +0 -0
  142. package/template/src/templates/rails-app/workspace/store/test/controllers/.keep +0 -0
  143. package/template/src/templates/rails-app/workspace/store/test/helpers/.keep +0 -0
  144. package/template/src/templates/rails-app/workspace/store/test/integration/.keep +0 -0
  145. package/template/src/templates/rails-app/workspace/store/test/test_helper.rb +15 -0
  146. package/template/src/templates/rails-app/workspace/store/tmp/.keep +0 -0
  147. package/template/src/templates/rails-app/workspace/store/tmp/pids/.keep +0 -0
  148. package/template/src/templates/rails-app/workspace/store/tmp/storage/.keep +0 -0
  149. package/template/src/templates/rails-app/workspace/store/vendor/javascripts/.keep +0 -0
  150. package/template/tsconfig.json +16 -0
  151. package/template/uno.config.ts +10 -0
@@ -0,0 +1,107 @@
1
+ import { useStore } from '@nanostores/react';
2
+ import type { PreviewInfo } from '@tutorialkit-rb/runtime';
3
+ import { useEffect } from 'react';
4
+ import tutorialStore from 'tutorialkit:store';
5
+
6
+ const ensureRailsServerStarted = async (preview: PreviewInfo) => {
7
+ if (preview.ready) {
8
+ return;
9
+ }
10
+
11
+ const terminalConfig = tutorialStore.terminalConfig.get();
12
+ const terminal = terminalConfig.panels.find((panel) => panel.type === 'terminal');
13
+
14
+ if (!terminal || !terminal.process) {
15
+ return;
16
+ }
17
+
18
+ terminal.input(`bin/rails s\n`);
19
+
20
+ await new Promise<void>((resolve, reject) => {
21
+ const tid = setTimeout(() => {
22
+ clearInterval(tick);
23
+ reject();
24
+ }, 10000);
25
+
26
+ const tick = setInterval(() => {
27
+ if (preview.ready) {
28
+ clearInterval(tick);
29
+ clearTimeout(tid);
30
+ resolve();
31
+ }
32
+ }, 200);
33
+ });
34
+ };
35
+
36
+ export default function RailsPathLinkHandler() {
37
+ const previews = useStore(tutorialStore.previews);
38
+
39
+ useEffect(() => {
40
+ async function handleClick(event: MouseEvent) {
41
+ const target = event.target as HTMLElement;
42
+ const link = target.closest('.rails-path-link');
43
+
44
+ if (link) {
45
+ event.preventDefault();
46
+
47
+ const railsPath = link.getAttribute('data-rails-path');
48
+
49
+ if (railsPath) {
50
+ tutorialStore.setSelectedFile(`/workspace/store/${railsPath}`);
51
+ }
52
+
53
+ return;
54
+ }
55
+
56
+ if (target.tagName === 'A') {
57
+ const linkTarget = target as HTMLAnchorElement;
58
+
59
+ if (linkTarget.href.startsWith('http://localhost:3000')) {
60
+ event.preventDefault();
61
+
62
+ const railsPreview = previews.find((pr) => pr.port === 3000);
63
+
64
+ if (!railsPreview) {
65
+ return;
66
+ }
67
+
68
+ try {
69
+ await ensureRailsServerStarted(railsPreview);
70
+ } catch (error) {
71
+ console.error('failed to start Rails server', e);
72
+ return;
73
+ }
74
+
75
+ const input = document.querySelector(
76
+ 'input[type="text"][name="tutorialkit-preview-navigation"]',
77
+ ) as HTMLInputElement;
78
+
79
+ if (!input) {
80
+ return;
81
+ }
82
+
83
+ const newPath = linkTarget.href.replace('http://localhost:3000/', '');
84
+
85
+ input.value = newPath;
86
+
87
+ const ev = new KeyboardEvent('keydown', {
88
+ key: 'Enter',
89
+ code: 'Enter',
90
+ keyCode: 13,
91
+ bubbles: true,
92
+ cancelable: true,
93
+ });
94
+ input.dispatchEvent(ev);
95
+ }
96
+ }
97
+ }
98
+
99
+ document.addEventListener('click', handleClick);
100
+
101
+ return () => {
102
+ document.removeEventListener('click', handleClick);
103
+ };
104
+ }, [previews]);
105
+
106
+ return null;
107
+ }
@@ -0,0 +1,95 @@
1
+ import { useStore } from '@nanostores/react';
2
+ import type { WebContainerProcess } from '@webcontainer/api';
3
+ import { useEffect, useState } from 'react';
4
+ import tutorialStore from 'tutorialkit:store';
5
+
6
+ type ShellConfig = Partial<{
7
+ workdir: string;
8
+ }>;
9
+
10
+ let observedProcess: WebContainerProcess | undefined = undefined;
11
+ let currWorkdir = '';
12
+
13
+ export const ShellConfigurator: React.FC = () => {
14
+ const boot = useStore(tutorialStore.bootStatus);
15
+ const storeRef = useStore(tutorialStore.ref);
16
+ const terminalConfig = useStore(tutorialStore.terminalConfig);
17
+ const lessonLoaded = useStore(tutorialStore.lessonFullyLoaded);
18
+ const [state, set] = useState(0);
19
+
20
+ useEffect(() => {
21
+ const unlisten = tutorialStore.terminalConfig.listen(() => {
22
+ set(state + 1);
23
+ });
24
+ return unlisten;
25
+ }, [terminalConfig]);
26
+
27
+ const lesson = tutorialStore.lesson;
28
+ const terminal = terminalConfig.panels.find((panel) => panel.type === 'terminal');
29
+
30
+ useEffect(() => {
31
+ if (boot !== 'booted') {
32
+ return;
33
+ }
34
+
35
+ if (!lessonLoaded) {
36
+ return;
37
+ }
38
+
39
+ if (!lesson) {
40
+ return;
41
+ }
42
+
43
+ if (!terminal) {
44
+ return;
45
+ }
46
+
47
+ const conf = lesson?.data?.custom?.shell as ShellConfig;
48
+
49
+ if (!conf) {
50
+ return;
51
+ }
52
+
53
+ const { workdir } = conf;
54
+
55
+ if (!workdir) {
56
+ return;
57
+ }
58
+
59
+ if (currWorkdir === workdir) {
60
+ return;
61
+ }
62
+
63
+ currWorkdir = workdir;
64
+
65
+ const checkProcess = () => {
66
+ if (terminal.process || observedProcess) {
67
+ if (!observedProcess) {
68
+ observedProcess = terminal.process;
69
+ }
70
+
71
+ terminal.input(`cd /home/tutorial${workdir} && clear\n`);
72
+
73
+ return true;
74
+ }
75
+
76
+ return false;
77
+ };
78
+
79
+ // Check immediately
80
+ if (checkProcess()) {
81
+ return;
82
+ }
83
+
84
+ // Set up interval to wait for process
85
+ const interval = setInterval(() => {
86
+ if (checkProcess()) {
87
+ clearInterval(interval);
88
+ }
89
+ }, 100);
90
+
91
+ return () => clearInterval(interval);
92
+ }, [boot, terminalConfig, storeRef, lessonLoaded]);
93
+
94
+ return null;
95
+ };
@@ -0,0 +1,48 @@
1
+ ---
2
+ import path from 'node:path';
3
+ import RailsPathLinkHandler from './RailsPathLinkHandler.tsx';
4
+ import { ShellConfigurator } from './ShellConfigurator';
5
+ import { FileManager } from './FileManager';
6
+ import { HelpDropdown } from './HelpDropdown';
7
+ import GitHubLink from './GitHubLink.astro';
8
+
9
+ const logo = path.join(import.meta.env.BASE_URL, 'logo.svg');
10
+ const logoDark = path.join(import.meta.env.BASE_URL, 'logo-dark.svg');
11
+ ---
12
+
13
+ <nav
14
+ class="bg-tk-elements-topBar-backgroundColor transition-theme border-b border-tk-elements-app-borderColor flex max-w-full items-center p-3 px-4 min-h-[56px]"
15
+ >
16
+ <div class="flex flex-1">
17
+ <a
18
+ href="/"
19
+ class="flex items-center text-tk-elements-topBar-logo-color hover:text-tk-elements-topBar-logo-colorHover"
20
+ >
21
+ {logo && <img class="h-5 w-auto dark:hidden" src={logo} />}
22
+ {logo && <img class="h-5 w-auto hidden dark:inline-block" src={logoDark} />}
23
+ </a>
24
+ </div>
25
+
26
+ <div class="mr-2">
27
+ <HelpDropdown client:only />
28
+ </div>
29
+ <div class="mr-2">
30
+ <slot name="download-button" />
31
+ </div>
32
+ <div class="mr-2">
33
+ <slot name="open-in-stackblitz-link" />
34
+ </div>
35
+ <div>
36
+ <slot name="theme-switch" />
37
+ </div>
38
+ <div>
39
+ <slot name="login-button" />
40
+ </div>
41
+ <div class="mr-2">
42
+ <GitHubLink />
43
+ </div>
44
+ </nav>
45
+
46
+ <RailsPathLinkHandler client:only />
47
+ <ShellConfigurator client:only />
48
+ <FileManager client:only />
@@ -0,0 +1,9 @@
1
+ import { contentSchema } from '@tutorialkit-rb/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 };
@@ -0,0 +1,34 @@
1
+ ---
2
+ type: lesson
3
+ title: Creating your first Rails app
4
+ editor: false
5
+ custom:
6
+ shell:
7
+ workdir: "/workspace/store"
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
+ ```
@@ -0,0 +1,3 @@
1
+ {
2
+ "extends": "../../../../../templates/rails-app"
3
+ }
@@ -0,0 +1,37 @@
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
+ <!-- hit Enter -->
35
+ ```
36
+
37
+ If the line "8.0.2" appears, it works!
@@ -0,0 +1,4 @@
1
+ ---
2
+ type: part
3
+ title: Getting Started with Rails
4
+ ---
@@ -0,0 +1,3 @@
1
+ {
2
+ "extends": "../../../../../templates/crud-products"
3
+ }
@@ -0,0 +1,99 @@
1
+ ---
2
+ type: lesson
3
+ title: CRUD Operations
4
+ focus: /workspace/store/app/controllers/products_controller.rb
5
+ previews: [3000]
6
+ mainCommand: ['node scripts/rails.js server', 'Starting Rails server']
7
+ prepareCommands:
8
+ - ['npm install', 'Preparing Ruby runtime']
9
+ - ['node scripts/rails.js db:prepare', 'Prepare development database']
10
+ custom:
11
+ shell:
12
+ workdir: '/workspace/store'
13
+ ---
14
+
15
+ # CRUD Operations in Rails
16
+
17
+ **CRUD** stands for Create, Read, Update, and Delete - the four basic operations you can perform on data. Rails makes CRUD operations simple and intuitive.
18
+
19
+ ## The Seven RESTful Actions
20
+
21
+ Rails controllers typically include these seven standard actions:
22
+
23
+ ### Reading Data
24
+ - **`index`** - List all products
25
+ - **`show`** - Display a single product
26
+
27
+ ### Creating Data
28
+ - **`new`** - Show form to create a product
29
+ - **`create`** - Process form submission and save product
30
+
31
+ ### Updating Data
32
+ - **`edit`** - Show form to edit a product
33
+ - **`update`** - Process form submission and update product
34
+
35
+ ### Deleting Data
36
+ - **`destroy`** - Delete a product
37
+
38
+ ## Try the CRUD Operations
39
+
40
+ 1. **View all products** - The home page shows the `index` action
41
+ 2. **Create a new product** - Click "New Product"
42
+ 3. **View a product** - Click on any product name
43
+ 4. **Edit a product** - Click "Edit" on any product
44
+ 5. **Delete a product** - Click "Delete" (with confirmation)
45
+
46
+ ## Routes and Actions
47
+
48
+ Rails automatically creates RESTful routes for your resources:
49
+
50
+ ```ruby
51
+ # In config/routes.rb
52
+ resources :products
53
+ ```
54
+
55
+ This generates these routes:
56
+
57
+ | HTTP Method | URL | Controller Action | Purpose |
58
+ |-------------|-----|-------------------|---------|
59
+ | GET | `/products` | `index` | List all products |
60
+ | GET | `/products/new` | `new` | Show new product form |
61
+ | POST | `/products` | `create` | Create a product |
62
+ | GET | `/products/1` | `show` | Show product #1 |
63
+ | GET | `/products/1/edit` | `edit` | Show edit form for product #1 |
64
+ | PATCH/PUT | `/products/1` | `update` | Update product #1 |
65
+ | DELETE | `/products/1` | `destroy` | Delete product #1 |
66
+
67
+ ## Strong Parameters
68
+
69
+ Notice how the controller uses **strong parameters** for security:
70
+
71
+ ```ruby
72
+ def product_params
73
+ params.expect(product: [ :name, :description, :price ])
74
+ end
75
+ ```
76
+
77
+ This prevents users from submitting malicious data by only allowing specified parameters.
78
+
79
+ ## Forms and Validations
80
+
81
+ Rails forms automatically:
82
+ - Handle CSRF protection
83
+ - Display validation errors
84
+ - Maintain form state on errors
85
+
86
+ Try creating a product with invalid data to see validation in action!
87
+
88
+ :::tip
89
+ Rails follows RESTful conventions, making your applications predictable and maintainable. The seven standard actions cover most use cases for managing resources.
90
+ :::
91
+
92
+ ## Experiment
93
+
94
+ Try modifying the controller:
95
+ 1. Add a search feature to the `index` action
96
+ 2. Add custom validations to the Product model
97
+ 3. Customize the success messages after create/update/delete
98
+
99
+ CRUD operations are the foundation of most web applications!
@@ -0,0 +1,4 @@
1
+ ---
2
+ type: part
3
+ title: Controllers
4
+ ---
@@ -0,0 +1,18 @@
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
+ ---
@@ -0,0 +1,3 @@
1
+ /// <reference path="../.astro/types.d.ts" />
2
+ /// <reference types="@tutorialkit-rb/astro/types" />
3
+ /// <reference types="astro/client" />
@@ -0,0 +1,39 @@
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, _) => {
16
+ visit(tree, 'inlineCode', (node: InlineCode, index, parent) => {
17
+ if (!parent || typeof index !== 'number') {
18
+ return;
19
+ }
20
+
21
+ // check if the inline code content matches Rails path pattern
22
+ const content = node.value;
23
+
24
+ if (!RAILS_PATH_PATTERN.test(content)) {
25
+ return;
26
+ }
27
+
28
+ // replace the inline code node with an HTML node containing a link
29
+ const htmlNode = {
30
+ type: 'html',
31
+ value: `<button class="${className}" data-rails-path="${content}"><code>${content}</code></button>`,
32
+ };
33
+
34
+ parent.children[index] = htmlNode as any;
35
+ });
36
+ };
37
+ };
38
+
39
+ export default remarkRailsPathLinks;
@@ -0,0 +1,3 @@
1
+ {
2
+ "extends": "../rails-app"
3
+ }
@@ -0,0 +1,48 @@
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
@@ -0,0 +1,3 @@
1
+ class Product < ApplicationRecord
2
+ validates :name, presence: true
3
+ end
@@ -0,0 +1,10 @@
1
+ <%= form_with model: product do |form| %>
2
+ <div>
3
+ <%= form.label :name %>
4
+ <%= form.text_field :name %>
5
+ </div>
6
+
7
+ <div>
8
+ <%= form.submit %>
9
+ </div>
10
+ <% end %>
@@ -0,0 +1,4 @@
1
+ <h1>Edit product</h1>
2
+
3
+ <%= render "form", product: @product %>
4
+ <%= link_to "Cancel", @product %>
@@ -0,0 +1,11 @@
1
+ <h1>Products</h1>
2
+
3
+ <%= link_to "New product", new_product_path %>
4
+
5
+ <div id="products">
6
+ <% @products.each do |product| %>
7
+ <div>
8
+ <%= link_to product.name, product_path(product.id) %>
9
+ </div>
10
+ <% end %>
11
+ </div>
@@ -0,0 +1,4 @@
1
+ <h1>New product</h1>
2
+
3
+ <%= render "form", product: @product %>
4
+ <%= link_to "Cancel", products_path %>
@@ -0,0 +1,5 @@
1
+ <h1><%= @product.name %></h1>
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?" } %>
@@ -0,0 +1,6 @@
1
+ Rails.application.routes.draw do
2
+ resources :products
3
+
4
+ # Defines the root path route ("/")
5
+ root "products#index"
6
+ end
@@ -0,0 +1,9 @@
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
@@ -0,0 +1,22 @@
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
@@ -0,0 +1,3 @@
1
+ # Create products that we should have created in the previous lessons
2
+ Product.create(name: "T-Shirt").destroy
3
+ Product.create(name: "Pants")