rails_app_generator 0.1.2 → 0.1.5

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 (64) hide show
  1. checksums.yaml +4 -4
  2. data/.builders/generators/project-plan.rb +6 -0
  3. data/CHANGELOG.md +22 -0
  4. data/after_templates/README.md +9 -1
  5. data/after_templates/rag_tailwind.rb +2 -4
  6. data/after_templates/rag_tailwind_daisyui/index.html.erb +181 -0
  7. data/after_templates/rag_tailwind_daisyui.rb +25 -0
  8. data/after_templates/rag_tailwind_hotwire_flash.rb +23 -0
  9. data/after_templates/rag_tailwind_hotwire_form/_contact.html.erb +8 -0
  10. data/after_templates/rag_tailwind_hotwire_form/_count.html.erb +1 -0
  11. data/after_templates/rag_tailwind_hotwire_form/_flash.html.erb +6 -0
  12. data/after_templates/rag_tailwind_hotwire_form/_form.html.erb +16 -0
  13. data/after_templates/rag_tailwind_hotwire_form/_list.html.erb +19 -0
  14. data/after_templates/rag_tailwind_hotwire_form/application.html.erb +23 -0
  15. data/after_templates/rag_tailwind_hotwire_form/application.js +30 -0
  16. data/after_templates/rag_tailwind_hotwire_form/application.tailwind.css +111 -0
  17. data/after_templates/rag_tailwind_hotwire_form/application_helper.rb +15 -0
  18. data/after_templates/rag_tailwind_hotwire_form/contact.rb +5 -0
  19. data/after_templates/rag_tailwind_hotwire_form/contacts_controller.rb +98 -0
  20. data/after_templates/rag_tailwind_hotwire_form/create.turbo_stream.erb +4 -0
  21. data/after_templates/rag_tailwind_hotwire_form/edit.html.erb +12 -0
  22. data/after_templates/rag_tailwind_hotwire_form/index.html.erb +5 -0
  23. data/after_templates/rag_tailwind_hotwire_form/new.html.erb +12 -0
  24. data/after_templates/rag_tailwind_hotwire_form/show.html.erb +16 -0
  25. data/after_templates/rag_tailwind_hotwire_form/update.turbo_stream.erb +2 -0
  26. data/after_templates/rag_tailwind_hotwire_form.rb +54 -0
  27. data/after_templates/rag_tailwind_hotwire_form_search/_contact.html.erb +8 -0
  28. data/after_templates/rag_tailwind_hotwire_form_search/_count.html.erb +1 -0
  29. data/after_templates/rag_tailwind_hotwire_form_search/_flash.html.erb +6 -0
  30. data/after_templates/rag_tailwind_hotwire_form_search/_form.html.erb +16 -0
  31. data/after_templates/rag_tailwind_hotwire_form_search/_list.html.erb +19 -0
  32. data/after_templates/rag_tailwind_hotwire_form_search/_theme_changer.html.erb +35 -0
  33. data/after_templates/rag_tailwind_hotwire_form_search/application.html.erb +25 -0
  34. data/after_templates/rag_tailwind_hotwire_form_search/application.js +40 -0
  35. data/after_templates/rag_tailwind_hotwire_form_search/application.tailwind.css +111 -0
  36. data/after_templates/rag_tailwind_hotwire_form_search/application_helper.rb +13 -0
  37. data/after_templates/rag_tailwind_hotwire_form_search/contact.rb +13 -0
  38. data/after_templates/rag_tailwind_hotwire_form_search/contacts_controller.rb +72 -0
  39. data/after_templates/rag_tailwind_hotwire_form_search/create.turbo_stream.erb +4 -0
  40. data/after_templates/rag_tailwind_hotwire_form_search/edit.html.erb +12 -0
  41. data/after_templates/rag_tailwind_hotwire_form_search/index.html.erb +15 -0
  42. data/after_templates/rag_tailwind_hotwire_form_search/new.html.erb +12 -0
  43. data/after_templates/rag_tailwind_hotwire_form_search/search_controller.js +16 -0
  44. data/after_templates/rag_tailwind_hotwire_form_search/show.html.erb +16 -0
  45. data/after_templates/rag_tailwind_hotwire_form_search/theme_changer_controller.js +13 -0
  46. data/after_templates/rag_tailwind_hotwire_form_search/update.turbo_stream.erb +2 -0
  47. data/after_templates/rag_tailwind_hotwire_form_search.rb +77 -0
  48. data/after_templates/rag_tailwind_style_reuse.rb +23 -0
  49. data/docs/project-plan/project.drawio +47 -32
  50. data/docs/project-plan/project_done.svg +1 -1
  51. data/docs/project-plan/project_in_progress.svg +1 -1
  52. data/docs/project-plan/project_todo.svg +1 -1
  53. data/lib/rails_app_generator/app_generator.rb +38 -0
  54. data/lib/rails_app_generator/diff/open_in_editor.rb +1 -1
  55. data/lib/rails_app_generator/diff/processor.rb +8 -1
  56. data/lib/rails_app_generator/version.rb +1 -1
  57. data/package-lock.json +1019 -85
  58. data/package.json +4 -1
  59. data/profiles/rag-tailwind-daisyui.json +10 -0
  60. data/profiles/rag-tailwind-hotwire-flash.json +12 -0
  61. data/profiles/rag-tailwind-hotwire-form-search.json +13 -0
  62. data/profiles/rag-tailwind-hotwire-form.json +13 -0
  63. data/profiles/rag-tailwind-style-reuse.json +12 -0
  64. metadata +50 -2
@@ -0,0 +1,98 @@
1
+ class ContactsController < ApplicationController
2
+ include ActionView::RecordIdentifier
3
+
4
+ before_action :set_contact, only: %i[show edit update destroy]
5
+ before_action :all_contacts #, except: [:new, :create, :index]
6
+
7
+ # GET /contacts or /contacts.json
8
+ def index
9
+ end
10
+
11
+ # GET /contacts/1 or /contacts/1.json
12
+ def show
13
+ end
14
+
15
+ # GET /contacts/new
16
+ def new
17
+ @contact = Contact.new
18
+ end
19
+
20
+ # GET /contacts/1/edit
21
+ def edit
22
+ end
23
+
24
+ # POST /contacts or /contacts.json
25
+ def create
26
+ @contact = Contact.new(contact_params)
27
+
28
+ if @contact.save
29
+ flash[:notice] = 'Contact was successfully created.'
30
+ else
31
+ render :new, status: :unprocessable_entity
32
+ end
33
+ # respond_to do |format|
34
+ # if @contact.save
35
+ # format.html { redirect_to contact_url(@contact), notice: "Contact was successfully created." }
36
+ # format.json { render :show, status: :created, location: @contact }
37
+ # else
38
+ # format.html { render :new, status: :unprocessable_entity }
39
+ # format.json { render json: @contact.errors, status: :unprocessable_entity }
40
+ # end
41
+ # end
42
+ end
43
+
44
+ # PATCH/PUT /contacts/1 or /contacts/1.json
45
+ def update
46
+ if @contact.update(contact_params)
47
+ flash[:notice] = "Contact updated"
48
+ else
49
+ render :edit, status: :unprocessable_entity
50
+ end
51
+ # respond_to do |format|
52
+ # if @contact.update(contact_params)
53
+ # format.html { redirect_to contact_url(@contact), notice: "Contact was successfully updated." }
54
+ # format.json { render :show, status: :ok, location: @contact }
55
+ # else
56
+ # format.html { render :edit, status: :unprocessable_entity }
57
+ # format.json { render json: @contact.errors, status: :unprocessable_entity }
58
+ # end
59
+ # end
60
+ end
61
+
62
+ # DELETE /contacts/1 or /contacts/1.json
63
+ # def destroy
64
+ # @contact.destroy
65
+
66
+ # respond_to do |format|
67
+ # format.html { redirect_to contacts_url, notice: "Contact was successfully destroyed." }
68
+ # format.json { head :no_content }
69
+ # end
70
+ # end
71
+ def destroy
72
+ @contact.destroy
73
+ flash[:notice] = "Contact removed"
74
+ render turbo_stream: [
75
+ turbo_stream.update("flash", partial: "shared/flash"),
76
+ turbo_stream.remove(dom_id(@contact)),
77
+ turbo_stream.update("contacts-count", partial: "contacts/count", locals: { contacts: @contacts })
78
+ ]
79
+ end
80
+
81
+ private
82
+ # Use callbacks to share common setup or constraints between actions.
83
+ def set_contact
84
+ @contact = Contact.find(params.require(:id))
85
+ end
86
+
87
+ def contact_params
88
+ params.require(:contact).permit(:name, :age, :email)
89
+ end
90
+ # def contact_params
91
+ # params.require(:contact).permit!
92
+ # end
93
+
94
+ def all_contacts
95
+ @contacts = Contact.all
96
+ end
97
+
98
+ end
@@ -0,0 +1,4 @@
1
+ <%= turbo_stream.update("flash", partial: "shared/flash") %>
2
+ <%= turbo_stream.append("contacts-list", partial: "contacts/contact", locals: { contact: @contact }) %>
3
+ <%= turbo_stream.update("contacts-count", partial: "contacts/count", locals: { contacts: @contacts }) %>
4
+ <%= turbo_stream.update("contact-form", partial: "contacts/form", locals: { contact: Contact.new }) %>
@@ -0,0 +1,12 @@
1
+ <div class="prose lg:prose-xl">
2
+ <h1>Edit contact</h1>
3
+ </div>
4
+
5
+ <div class="grid grid-cols-2 gap-4 mt-8">
6
+ <div class="bg-gray-100 p-8">
7
+ <%= render partial: "list", locals: { contacts: @contacts } %>
8
+ </div>
9
+ <div class="form-control w-full max-w-xs">
10
+ <%= render partial: "form", locals: { contact: @contact } %>
11
+ </div>
12
+ </div>
@@ -0,0 +1,5 @@
1
+ <div class="prose lg:prose-xl">
2
+ <h1>Contacts list</h1>
3
+
4
+ <%= link_to "Add new contact", new_contact_path, class: "btn btn-primary" %>
5
+ </div>
@@ -0,0 +1,12 @@
1
+ <div class="prose lg:prose-xl">
2
+ <h1>New contact</h1>
3
+ </div>
4
+
5
+ <div class="grid grid-cols-2 gap-4 mt-8">
6
+ <div class="bg-gray-100 p-8">
7
+ <%= render partial: "list", locals: { contacts: @contacts } %>
8
+ </div>
9
+ <div class="form-control w-full max-w-xs" id="contact-form">
10
+ <%= render partial: "form", locals: { contact: @contact } %>
11
+ </div>
12
+ </div>
@@ -0,0 +1,16 @@
1
+ <div class="prose lg:prose-xl">
2
+ <h1>Contact details</h2>
3
+ </div>
4
+
5
+ <div class="w-1/2 mx-auto p-8 border-2 m-8 grid grid-cols-2 gap-4">
6
+ <div>
7
+ <div class="my-4">Name: <%= @contact.name %></div>
8
+ <hr />
9
+ <div class="my-4">Age: <%= @contact.age %></div>
10
+ <hr />
11
+ <div class="my-4">Email: <%= @contact.email %></div>
12
+ </div>
13
+ <div>
14
+ <img src="https://i.pravatar.cc/300" />
15
+ </div>
16
+ </div>
@@ -0,0 +1,2 @@
1
+ <%= turbo_stream.update("flash", partial: "shared/flash") %>
2
+ <%= turbo_stream.replace(dom_id(@contact), partial: "contacts/contact", locals: { contact: @contact }) %>
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ # How to Start a Ruby on Rails 7 App With Hotwire and Tailwind CSS
4
+ # https://www.youtube.com/watch?v=-n7IbUFKjoM
5
+
6
+ require 'pry'
7
+
8
+ self.local_template_path = File.join(File.dirname(__FILE__), 'rag_tailwind_hotwire_form')
9
+
10
+ gac 'base rails 7 image created'
11
+
12
+ route("resources :contacts")
13
+ route("root 'contacts#index'")
14
+
15
+ after_bundle do
16
+ gsub_file 'app/views/layouts/application.html.erb', %(container mx-auto mt-28 px-5 flex), 'container mt-8 mx-auto'
17
+
18
+ # add_scaffold('contact', 'name', 'email')
19
+ add_migration('create_contacts', 'name', 'email', 'age:integer')
20
+ # rails generate migration CreateUsers name:string salary:decimal
21
+
22
+ copy_file 'contact.rb' , 'app/models/contact.rb'
23
+ copy_file 'contacts_controller.rb' , 'app/controllers/contacts_controller.rb'
24
+
25
+ copy_file '_contact.html.erb' , 'app/views/contacts/_contact.html.erb'
26
+ copy_file '_count.html.erb' , 'app/views/contacts/_count.html.erb'
27
+ copy_file '_form.html.erb' , 'app/views/contacts/_form.html.erb'
28
+ copy_file '_list.html.erb' , 'app/views/contacts/_list.html.erb'
29
+ copy_file 'edit.html.erb' , 'app/views/contacts/edit.html.erb'
30
+ copy_file 'index.html.erb' , 'app/views/contacts/index.html.erb'
31
+ copy_file 'new.html.erb' , 'app/views/contacts/new.html.erb'
32
+ copy_file 'show.html.erb' , 'app/views/contacts/show.html.erb'
33
+
34
+ copy_file 'application_helper.rb' , 'app/helpers/application_helper.rb' , force: true
35
+ copy_file 'application.html.erb' , 'app/views/layouts/application.html.erb' , force: true
36
+ copy_file 'create.turbo_stream.erb' , 'app/views/contacts/create.turbo_stream.erb'
37
+ copy_file 'update.turbo_stream.erb' , 'app/views/contacts/update.turbo_stream.erb'
38
+ copy_file 'application.js' , 'app/javascript/application.js' , force: true
39
+
40
+ copy_file '_flash.html.erb' , 'app/views/shared/_flash.html.erb'
41
+
42
+ # Install tailwind directly, instead of via the --css=tailwind option so that we can configure 3rd party plugins
43
+ # gem "jsbundling-rails"
44
+ gem "cssbundling-rails"
45
+
46
+ rails_command('css:install:tailwind')
47
+ run('npm install daisyui')
48
+ run('npm install -D @tailwindcss/typography')
49
+
50
+ gsub_file 'tailwind.config.js', %(]\n}), "],\n plugins: [require(\"daisyui\"), require(\"@tailwindcss/typography\")],\n}"
51
+ copy_file 'application.tailwind.css' , 'app/assets/stylesheets/application.tailwind.css' , force: true
52
+
53
+ rails_command('db:migrate')
54
+ end
@@ -0,0 +1,8 @@
1
+ <tr id="<%= dom_id(contact) %>" data-stream-enter-class="animate-item-in" data-stream-exit-class="animate-item-out">
2
+ <td><%= contact.id %></td>
3
+ <td><%= link_to contact.name, contact_path(contact), data: { turbo_frame: "_top" } %></td>
4
+ <td><%= contact.age %></td>
5
+ <td><%= contact.email %></td>
6
+ <td><%= link_to "Edit", edit_contact_path(contact), class: "btn btn-link btn-sm", data: { turbo_frame: "_top" } %></td>
7
+ <td><%= button_to "Delete", contact_path(contact), method: :delete, class: "btn btn-link btn-sm" %></td>
8
+ </tr>
@@ -0,0 +1 @@
1
+ <span data-stream-enter-class="animate-item-in" data-stream-exit-class="animate-item-out">Showing <%= contacts.length %> <%= "contact".pluralize(contacts.length) %></span>
@@ -0,0 +1,6 @@
1
+ <% flash.each do |type, msg| %>
2
+ <% alert_class = type == "notice" ? "success" : "error" %>
3
+ <div class="alert alert-<%= alert_class %> mb-8 text-white" data-stream-enter-class="animate-in" data-stream-exit-class="animate-out">
4
+ <%= msg %>
5
+ </div>
6
+ <% end %>
@@ -0,0 +1,16 @@
1
+ <%= form_for contact do |f| %>
2
+ <% if contact.errors.any? %>
3
+ <div class="form-errors">
4
+ <div class="alert shadow-lg alert-error text-white">
5
+ Your form's got some errors
6
+ </div>
7
+ </div>
8
+ <% end %>
9
+ <%= f.text_field :name, placeholder: "Your first name", class: "input input-bordered w-full max-w-xs my-5" %>
10
+ <%= inline_error_for(:name, contact) %>
11
+ <%= f.text_field :age, placeholder: "Your age", class: "input input-bordered w-full max-w-xs my-4" %>
12
+ <%= inline_error_for(:age, contact) %>
13
+ <%= f.text_field :email, placeholder: "Your email address", class: "input input-bordered w-full max-w-xs my-4" %>
14
+ <%= inline_error_for(:email, contact) %>
15
+ <%= f.submit "Save", class: "form-control btn btn-primary my-4" %>
16
+ <% end %>
@@ -0,0 +1,19 @@
1
+ <table class="table table-zebra mt-0 w-full">
2
+ <thead>
3
+ <tr class="w-full">
4
+ <th class="text-left">ID</th>
5
+ <th class="text-left">Name</th>
6
+ <th class="text-left">Age</th>
7
+ <th class="text-left">Email</th>
8
+ <th class="text-center" colspan="2">Actions</th>
9
+ </tr>
10
+ </thead>
11
+ <tbody id="contacts-list" data-controller="contacts" data-action="">
12
+ <% contacts.each do |contact| %>
13
+ <%= render partial: "contact", locals: { contact: contact } %>
14
+ <% end %>
15
+ </tbody>
16
+ </table>
17
+ <div class="text-gray-400 mt-4 text-xs text-right" id="contacts-count">
18
+ <%= render partial: "count", locals: { contacts: contacts } %>
19
+ </div>
@@ -0,0 +1,35 @@
1
+ <div class="dropdown">
2
+ <label tabindex="0" class="btn m-1">Select Theme</label>
3
+ <ul tabindex="0" class="dropdown-content menu p-2 shadow bg-base-100 rounded-box w-52" data-controller='theme-changer' >
4
+ <li data-value="" data-action="click->theme-changer#select">Default</li>
5
+ <li data-value="aqua" data-action="click->theme-changer#select">Aqua</li>
6
+ <li data-value="black" data-action="click->theme-changer#select">Black</li>
7
+ <li data-value="bumblebee" data-action="click->theme-changer#select">Bumblebee</li>
8
+ <li data-value="cmyk" data-action="click->theme-changer#select">Cmyk</li>
9
+ <li data-value="corporate" data-action="click->theme-changer#select">Corporate</li>
10
+ <li data-value="cupcake" data-action="click->theme-changer#select">Cupcake</li>
11
+ <li data-value="cyberpunk" data-action="click->theme-changer#select">Cyberpunk</li>
12
+ <li data-value="dark" data-action="click->theme-changer#select">Dark</li>
13
+ <li data-value="dracula" data-action="click->theme-changer#select">Dracula</li>
14
+ <li data-value="emerald" data-action="click->theme-changer#select">Emerald</li>
15
+ <li data-value="fantasy" data-action="click->theme-changer#select">Fantasy</li>
16
+ <li data-value="forest" data-action="click->theme-changer#select">Forest</li>
17
+ <li data-value="garden" data-action="click->theme-changer#select">Garden</li>
18
+ <li data-value="halloween" data-action="click->theme-changer#select">Halloween</li>
19
+ <li data-value="light" data-action="click->theme-changer#select">Light</li>
20
+ <li data-value="lofi" data-action="click->theme-changer#select">Lofi</li>
21
+ <li data-value="luxury" data-action="click->theme-changer#select">Luxury</li>
22
+ <li data-value="pastel" data-action="click->theme-changer#select">Pastel</li>
23
+ <li data-value="retro" data-action="click->theme-changer#select">Retro</li>
24
+ <li data-value="synthwave" data-action="click->theme-changer#select">Synthwave</li>
25
+ <li data-value="valentine" data-action="click->theme-changer#select">Valentine</li>
26
+ <li data-value="wireframe" data-action="click->theme-changer#select">Wireframe</li>
27
+ <li data-value="autumn" data-action="click->theme-changer#select">Autumn</li>
28
+ <li data-value="business" data-action="click->theme-changer#select">Business</li>
29
+ <li data-value="acid" data-action="click->theme-changer#select">Acid</li>
30
+ <li data-value="lemonade" data-action="click->theme-changer#select">Lemonade</li>
31
+ <li data-value="night" data-action="click->theme-changer#select">Night</li>
32
+ <li data-value="coffee" data-action="click->theme-changer#select">Coffee</li>
33
+ <li data-value="winter" data-action="click->theme-changer#select">Winter</li>
34
+ </select>
35
+ </div>
@@ -0,0 +1,25 @@
1
+ <!DOCTYPE html>
2
+ <html data-theme="retro">
3
+ <head>
4
+ <title>Tailwind Hotwire Form + Search</title>
5
+ <meta name="viewport" content="width=device-width,initial-scale=1">
6
+ <%= csrf_meta_tags %>
7
+ <%= csp_meta_tag %>
8
+
9
+ <%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
10
+ <%= javascript_importmap_tags %>
11
+ </head>
12
+
13
+ <body>
14
+ <%= render partial: "shared/theme_changer" %>
15
+
16
+ <main class="container mx-auto">
17
+ <div class="py-8">
18
+ <div id="flash" class="flash">
19
+ <%= render partial: "shared/flash" %>
20
+ </div>
21
+ <%= yield %>
22
+ </div>
23
+ </main>
24
+ </body>
25
+ </html>
@@ -0,0 +1,40 @@
1
+ // Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails
2
+ import "@hotwired/turbo-rails"
3
+ import "controllers"
4
+
5
+ document.addEventListener("turbo:before-stream-render", function(event) {
6
+ // Add a class to an element we are about to add to the page
7
+ // as defined by its "data-stream-enter-class"
8
+ if (event.target.firstElementChild instanceof HTMLTemplateElement) {
9
+ var enterAnimationClass = event.target.templateContent.firstElementChild.dataset.streamEnterClass
10
+ if (enterAnimationClass) {
11
+ event.target.templateElement.content.firstElementChild.classList.add(enterAnimationClass)
12
+ }
13
+ }
14
+
15
+ // Add a class to an element we are about to remove from the page
16
+ // as defined by its "data-stream-exit-class"
17
+ var elementToRemove = document.getElementById(event.target.target)
18
+ if (elementToRemove) {
19
+ var streamExitClass = elementToRemove.dataset.streamExitClass
20
+ if (streamExitClass) {
21
+ // Intercept the removal of the element
22
+ event.preventDefault()
23
+ elementToRemove.classList.add(streamExitClass)
24
+ // Wait for its animation to end before removing the element
25
+ elementToRemove.addEventListener("animationend", function() {
26
+ event.target.performAction()
27
+ })
28
+ }
29
+ }
30
+ })
31
+
32
+ // FIX this using stimulus
33
+ // Add Theme Changer Event Listener
34
+ var items = Array.from(document.querySelector('.dropdown > ul').children);
35
+ items.forEach(item => {
36
+ item.addEventListener('click', event => {
37
+ var html = document.querySelector('html');
38
+ html.setAttribute("data-theme", event.target.getAttribute('data-value'));
39
+ })
40
+ })
@@ -0,0 +1,111 @@
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
4
+
5
+ /*
6
+
7
+ @layer components {
8
+ .btn-primary {
9
+ @apply py-2 px-4 bg-blue-200;
10
+ }
11
+ }
12
+
13
+ */
14
+
15
+ html {
16
+ font-size: 20px
17
+ }
18
+
19
+ .form-errors {
20
+ color: red;
21
+ }
22
+
23
+ .field_with_errors .input-bordered {
24
+ border: 1px solid red;
25
+ margin-bottom: 0;
26
+ }
27
+
28
+ /* Animation */
29
+ /* */
30
+ /* Got it from here: */
31
+ /* https://edforshaw.co.uk/hotwire-turbo-stream-animations */
32
+ /* and here: */
33
+ /* https://stackoverflow.com/a/61306871/4072276 */
34
+
35
+ .animate-in {
36
+ animation: slide-in 0.25s ease-out;
37
+ }
38
+
39
+ .animate-out {
40
+ animation: slide-out 0.25s ease-out;
41
+ }
42
+
43
+ @keyframes slide-in {
44
+ from { transform: translateX(4rem); }
45
+ to { transform: translateX(0); }
46
+ }
47
+
48
+ @keyframes slide-out {
49
+ from { transform: translateX(0); }
50
+ to { transform: translateX(4rem); }
51
+ }
52
+
53
+ .animate-item-in {
54
+ transition-duration: 0.25s;
55
+ animation: addRow 0.25s ease-in;
56
+ transform-origin: top;
57
+ }
58
+
59
+ .animate-item-out {
60
+ transition-duration: 0.25s;
61
+ animation: removeRow 0.25s ease-in;
62
+ transform-origin: bottom;
63
+ }
64
+
65
+ @keyframes addRow {
66
+ 0% {
67
+ transform: scale(1, 0);
68
+ line-height: 0px;
69
+ background-color: #fff;
70
+ visibility: collapse;
71
+ }
72
+ 50% {
73
+ transform: scale(1, 1);
74
+ line-height: 20px;
75
+ visibility: visible;
76
+ }
77
+ 100% {
78
+ }
79
+ }
80
+
81
+ @keyframes removeRow {
82
+ 100% {
83
+ transform: scale(1, 1);
84
+ line-height: 20px;
85
+ visibility: visible;
86
+ }
87
+ 50% {
88
+ transform: scale(1, 0);
89
+ line-height: 0px;
90
+ background-color: #fff;
91
+ visibility: collapse;
92
+ }
93
+ 0% {
94
+ }
95
+ }
96
+ .flash {
97
+ height: 70px;
98
+ margin-bottom: 2rem;
99
+ }
100
+
101
+ .alert-success {
102
+ --tw-bg-opacity: 1;
103
+ background-color: hsl(var(--su)/var(--tw-bg-opacity));
104
+ --tw-text-opacity: 1;
105
+ color: hsl(var(--suc,var(--nc))/var(--tw-text-opacity));
106
+ }
107
+
108
+ .text-white {
109
+ --tw-text-opacity: 1;
110
+ color: rgb(255 255 255/var(--tw-text-opacity));
111
+ }
@@ -0,0 +1,13 @@
1
+ module ApplicationHelper
2
+ def inline_error_for(field, form_obj)
3
+ html = []
4
+
5
+ if form_obj.errors[field].any?
6
+ html << form_obj.errors[field].map do |msg|
7
+ tag.div(msg, class: "text-red-400 text-xs m-0 p-0 text-right mb-2")
8
+ end
9
+ end
10
+
11
+ html.join.html_safe
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ class Contact < ApplicationRecord
2
+ validates :name, presence: true
3
+ validates :age, presence: true, numericality: true, inclusion: { in: 20...100, message: "must be between 20 and 100" }
4
+ validates :email, presence: true, format: { with: /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i }
5
+
6
+ class << self
7
+ def search(params)
8
+ return all if params[:query].blank?
9
+
10
+ where("name LIKE ?", "%#{sanitize_sql_like(params[:query])}%")
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,72 @@
1
+ class ContactsController < ApplicationController
2
+ include ActionView::RecordIdentifier
3
+
4
+ before_action :set_contact, only: %i[show edit update destroy]
5
+ before_action :all_contacts
6
+
7
+ # GET /contacts or /contacts.json
8
+ def index
9
+ @contacts = Contact.search(params)
10
+ end
11
+
12
+ # GET /contacts/1 or /contacts/1.json
13
+ def show
14
+ end
15
+
16
+ # GET /contacts/new
17
+ def new
18
+ @contact = Contact.new
19
+ end
20
+
21
+ # GET /contacts/1/edit
22
+ def edit
23
+ end
24
+
25
+ # POST /contacts or /contacts.json
26
+ def create
27
+ @contact = Contact.new(contact_params)
28
+
29
+ if @contact.save
30
+ flash[:notice] = 'Contact was successfully created.'
31
+ else
32
+ render :new, status: :unprocessable_entity
33
+ end
34
+ end
35
+
36
+ # PATCH/PUT /contacts/1 or /contacts/1.json
37
+ def update
38
+ if @contact.update(contact_params)
39
+ flash[:notice] = "Contact updated"
40
+ else
41
+ render :edit, status: :unprocessable_entity
42
+ end
43
+ end
44
+
45
+ def destroy
46
+ @contact.destroy
47
+ flash[:notice] = "Contact removed"
48
+ render turbo_stream: [
49
+ turbo_stream.update("flash", partial: "shared/flash"),
50
+ turbo_stream.remove(dom_id(@contact)),
51
+ turbo_stream.update("contacts-count", partial: "contacts/count", locals: { contacts: @contacts })
52
+ ]
53
+ end
54
+
55
+ private
56
+ # Use callbacks to share common setup or constraints between actions.
57
+ def set_contact
58
+ @contact = Contact.find(params.require(:id))
59
+ end
60
+
61
+ def contact_params
62
+ params.require(:contact).permit(:name, :age, :email)
63
+ end
64
+ # def contact_params
65
+ # params.require(:contact).permit!
66
+ # end
67
+
68
+ def all_contacts
69
+ @contacts = Contact.all
70
+ end
71
+
72
+ end
@@ -0,0 +1,4 @@
1
+ <%= turbo_stream.update("flash", partial: "shared/flash") %>
2
+ <%= turbo_stream.append("contacts-list", partial: "contacts/contact", locals: { contact: @contact }) %>
3
+ <%= turbo_stream.update("contacts-count", partial: "contacts/count", locals: { contacts: @contacts }) %>
4
+ <%= turbo_stream.update("contact-form", partial: "contacts/form", locals: { contact: Contact.new }) %>
@@ -0,0 +1,12 @@
1
+ <div class="prose lg:prose-xl">
2
+ <h1>Edit contact</h1>
3
+ </div>
4
+
5
+ <div class="grid grid-cols-2 gap-4 mt-8">
6
+ <div class="bg-gray-100 p-8">
7
+ <%= render partial: "list", locals: { contacts: @contacts } %>
8
+ </div>
9
+ <div class="form-control w-full max-w-xs">
10
+ <%= render partial: "form", locals: { contact: @contact } %>
11
+ </div>
12
+ </div>
@@ -0,0 +1,15 @@
1
+ <div class="prose lg:prose-xl">
2
+ <h1>Contacts list</h1>
3
+ <%= form_tag contacts_path, method: :get, data: { turbo_frame: "search-results", turbo_action: "advance", controller: "search", action: "input->search#submit" } do |f| %>
4
+ <%= text_field_tag "query", nil, placeholder: "Search ...", class: "input input.bordered w-full max-w-xs my-5" %>
5
+ <%= submit_tag "Search", class: "btn btn-primary" %>
6
+ <% end %>
7
+ </div>
8
+
9
+ <div class="bg-gray-100 p-8 col-span-5">
10
+ <%= turbo_frame_tag "search-results" do %>
11
+ <%= render partial: "list", locals: { contacts: @contacts } %>
12
+ <% end %>
13
+ </div>
14
+
15
+ <%= link_to "Add new contact", new_contact_path, class: "btn btn-primary mt-4" %>
@@ -0,0 +1,12 @@
1
+ <div class="prose lg:prose-xl">
2
+ <h1>New contact</h1>
3
+ </div>
4
+
5
+ <div class="grid grid-cols-2 gap-4 mt-8">
6
+ <div class="bg-gray-100 p-8">
7
+ <%= render partial: "list", locals: { contacts: @contacts } %>
8
+ </div>
9
+ <div class="form-control w-full max-w-xs" id="contact-form">
10
+ <%= render partial: "form", locals: { contact: @contact } %>
11
+ </div>
12
+ </div>
@@ -0,0 +1,16 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+ import debounce from "debounce";
3
+
4
+ // Connects to data-controller="search"
5
+ export default class extends Controller {
6
+ initialize() {
7
+ this.submit = debounce(this.submit.bind(this), 300);
8
+ }
9
+ connect() {
10
+ console.log('Search');
11
+ }
12
+
13
+ submit(_event) {
14
+ this.element.requestSubmit();
15
+ }
16
+ }