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.
- checksums.yaml +4 -4
- data/.builders/generators/project-plan.rb +6 -0
- data/CHANGELOG.md +22 -0
- data/after_templates/README.md +9 -1
- data/after_templates/rag_tailwind.rb +2 -4
- data/after_templates/rag_tailwind_daisyui/index.html.erb +181 -0
- data/after_templates/rag_tailwind_daisyui.rb +25 -0
- data/after_templates/rag_tailwind_hotwire_flash.rb +23 -0
- data/after_templates/rag_tailwind_hotwire_form/_contact.html.erb +8 -0
- data/after_templates/rag_tailwind_hotwire_form/_count.html.erb +1 -0
- data/after_templates/rag_tailwind_hotwire_form/_flash.html.erb +6 -0
- data/after_templates/rag_tailwind_hotwire_form/_form.html.erb +16 -0
- data/after_templates/rag_tailwind_hotwire_form/_list.html.erb +19 -0
- data/after_templates/rag_tailwind_hotwire_form/application.html.erb +23 -0
- data/after_templates/rag_tailwind_hotwire_form/application.js +30 -0
- data/after_templates/rag_tailwind_hotwire_form/application.tailwind.css +111 -0
- data/after_templates/rag_tailwind_hotwire_form/application_helper.rb +15 -0
- data/after_templates/rag_tailwind_hotwire_form/contact.rb +5 -0
- data/after_templates/rag_tailwind_hotwire_form/contacts_controller.rb +98 -0
- data/after_templates/rag_tailwind_hotwire_form/create.turbo_stream.erb +4 -0
- data/after_templates/rag_tailwind_hotwire_form/edit.html.erb +12 -0
- data/after_templates/rag_tailwind_hotwire_form/index.html.erb +5 -0
- data/after_templates/rag_tailwind_hotwire_form/new.html.erb +12 -0
- data/after_templates/rag_tailwind_hotwire_form/show.html.erb +16 -0
- data/after_templates/rag_tailwind_hotwire_form/update.turbo_stream.erb +2 -0
- data/after_templates/rag_tailwind_hotwire_form.rb +54 -0
- data/after_templates/rag_tailwind_hotwire_form_search/_contact.html.erb +8 -0
- data/after_templates/rag_tailwind_hotwire_form_search/_count.html.erb +1 -0
- data/after_templates/rag_tailwind_hotwire_form_search/_flash.html.erb +6 -0
- data/after_templates/rag_tailwind_hotwire_form_search/_form.html.erb +16 -0
- data/after_templates/rag_tailwind_hotwire_form_search/_list.html.erb +19 -0
- data/after_templates/rag_tailwind_hotwire_form_search/_theme_changer.html.erb +35 -0
- data/after_templates/rag_tailwind_hotwire_form_search/application.html.erb +25 -0
- data/after_templates/rag_tailwind_hotwire_form_search/application.js +40 -0
- data/after_templates/rag_tailwind_hotwire_form_search/application.tailwind.css +111 -0
- data/after_templates/rag_tailwind_hotwire_form_search/application_helper.rb +13 -0
- data/after_templates/rag_tailwind_hotwire_form_search/contact.rb +13 -0
- data/after_templates/rag_tailwind_hotwire_form_search/contacts_controller.rb +72 -0
- data/after_templates/rag_tailwind_hotwire_form_search/create.turbo_stream.erb +4 -0
- data/after_templates/rag_tailwind_hotwire_form_search/edit.html.erb +12 -0
- data/after_templates/rag_tailwind_hotwire_form_search/index.html.erb +15 -0
- data/after_templates/rag_tailwind_hotwire_form_search/new.html.erb +12 -0
- data/after_templates/rag_tailwind_hotwire_form_search/search_controller.js +16 -0
- data/after_templates/rag_tailwind_hotwire_form_search/show.html.erb +16 -0
- data/after_templates/rag_tailwind_hotwire_form_search/theme_changer_controller.js +13 -0
- data/after_templates/rag_tailwind_hotwire_form_search/update.turbo_stream.erb +2 -0
- data/after_templates/rag_tailwind_hotwire_form_search.rb +77 -0
- data/after_templates/rag_tailwind_style_reuse.rb +23 -0
- data/docs/project-plan/project.drawio +47 -32
- data/docs/project-plan/project_done.svg +1 -1
- data/docs/project-plan/project_in_progress.svg +1 -1
- data/docs/project-plan/project_todo.svg +1 -1
- data/lib/rails_app_generator/app_generator.rb +38 -0
- data/lib/rails_app_generator/diff/open_in_editor.rb +1 -1
- data/lib/rails_app_generator/diff/processor.rb +8 -1
- data/lib/rails_app_generator/version.rb +1 -1
- data/package-lock.json +1019 -85
- data/package.json +4 -1
- data/profiles/rag-tailwind-daisyui.json +10 -0
- data/profiles/rag-tailwind-hotwire-flash.json +12 -0
- data/profiles/rag-tailwind-hotwire-form-search.json +13 -0
- data/profiles/rag-tailwind-hotwire-form.json +13 -0
- data/profiles/rag-tailwind-style-reuse.json +12 -0
- 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,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,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,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
|
+
}
|