feat: add flash message functionality

- Implement Stimulus controller for closing flash messages
- Replace inline alerts with a partial for better organization
- Enhance styling for user registration and login forms

This update introduces a new flash message component that allows
for user notifications to be displayed on the screen and closed by
the user. The forms also include improved styles for a better
user experience.
This commit is contained in:
songtianlun 2025-02-11 15:52:58 +08:00
parent cba76e718f
commit 0312383bc8
7 changed files with 238 additions and 86 deletions

View File

@ -0,0 +1,8 @@
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
close(event) {
const target = event.target.closest('.alert')
target.remove()
}
}

View File

@ -3,12 +3,10 @@
// ./bin/rails generate stimulus controllerName
import { application } from "./application"
import HelloController from "./hello_controller"
application.register("hello", HelloController)
import PhotoSwipeLightBoxController from "./photo_swipe_lightbox_controller"
import FlashMessageController from "./flash_controller"
console.log("ready to register photo-swipe")
application.register("hello", HelloController)
application.register("photo-swipe-lightbox", PhotoSwipeLightBoxController)
console.log("successful to register photo-swipe")
application.register("flash", FlashMessageController)

View File

@ -1,43 +1,86 @@
<h2>Edit <%= resource_name.to_s.humanize %></h2>
<div class="min-h-screen container mx-auto px-4 py-8">
<div class="max-w-2xl mx-auto">
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<h2 class="card-title text-2xl mb-6">Account Settings</h2>
<%= form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put }) do |f| %>
<%= render "devise/shared/error_messages", resource: resource %>
<%= form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put, class: "space-y-6" }) do |f| %>
<%= render "devise/shared/error_messages", resource: resource %>
<div class="field">
<%= f.label :email %><br />
<%= f.email_field :email, autofocus: true, autocomplete: "email" %>
<div class="form-control">
<%= f.label :email, class: "label" %>
<%= f.email_field :email,
autofocus: true,
autocomplete: "email",
class: "input input-bordered w-full" %>
</div>
<% if devise_mapping.confirmable? && resource.pending_reconfirmation? %>
<div class="alert alert-info">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-current shrink-0 w-6 h-6"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
<span>Currently waiting confirmation for: <%= resource.unconfirmed_email %></span>
</div>
<% end %>
<div class="divider">Change Password</div>
<div class="form-control">
<%= f.label :password, class: "label" do %>
<span class="label-text">New Password</span>
<span class="label-text-alt text-gray-500">(leave blank if you don't want to change it)</span>
<% end %>
<%= f.password_field :password,
autocomplete: "new-password",
class: "input input-bordered w-full" %>
<% if @minimum_password_length %>
<label class="label">
<span class="label-text-alt text-gray-500"><%= @minimum_password_length %> characters minimum</span>
</label>
<% end %>
</div>
<div class="form-control">
<%= f.label :password_confirmation, "Confirm New Password", class: "label" %>
<%= f.password_field :password_confirmation,
autocomplete: "new-password",
class: "input input-bordered w-full" %>
</div>
<div class="form-control">
<%= f.label :current_password, class: "label" do %>
<span class="label-text">Current Password</span>
<span class="label-text-alt text-gray-500">(required to confirm changes)</span>
<% end %>
<%= f.password_field :current_password,
autocomplete: "current-password",
class: "input input-bordered w-full" %>
</div>
<div class="form-control mt-6">
<%= f.submit "Update Account", class: "btn btn-primary" %>
</div>
<% end %>
<div class="divider">Danger Zone</div>
<div class="bg-error/10 rounded-box p-4">
<h3 class="font-bold text-error mb-2">Delete Account</h3>
<p class="text-sm mb-4">Once you delete your account, there is no going back. Please be certain.</p>
<%= button_to registration_path(resource_name),
class: "btn btn-error",
data: {
confirm: "Are you sure?",
turbo_confirm: "Are you sure you want to delete your account? This action cannot be undone."
},
method: :delete do %>
Delete Account
<% end %>
</div>
<div class="text-center mt-6">
<%= link_to "← Back", :back, class: "btn btn-ghost btn-sm" %>
</div>
</div>
</div>
</div>
<% if devise_mapping.confirmable? && resource.pending_reconfirmation? %>
<div>Currently waiting confirmation for: <%= resource.unconfirmed_email %></div>
<% end %>
<div class="field">
<%= f.label :password %> <i>(leave blank if you don't want to change it)</i><br />
<%= f.password_field :password, autocomplete: "new-password" %>
<% if @minimum_password_length %>
<br />
<em><%= @minimum_password_length %> characters minimum</em>
<% end %>
</div>
<div class="field">
<%= f.label :password_confirmation %><br />
<%= f.password_field :password_confirmation, autocomplete: "new-password" %>
</div>
<div class="field">
<%= f.label :current_password %> <i>(we need your current password to confirm your changes)</i><br />
<%= f.password_field :current_password, autocomplete: "current-password" %>
</div>
<div class="actions">
<%= f.submit "Update" %>
</div>
<% end %>
<h3>Cancel my account</h3>
<div>Unhappy? <%= button_to "Cancel my account", registration_path(resource_name), data: { confirm: "Are you sure?", turbo_confirm: "Are you sure?" }, method: :delete %></div>
<%= link_to "Back", :back %>
</div>

View File

@ -1,26 +1,68 @@
<h2>Log in</h2>
<div class="min-h-screen flex flex-col items-center justify-center px-4">
<div class="card w-full max-w-md bg-base-100 shadow-xl">
<div class="card-body">
<h2 class="card-title text-2xl font-bold text-center mb-6">Sign in to your account</h2>
<%= form_for(resource, as: resource_name, url: session_path(resource_name)) do |f| %>
<div class="field">
<%= f.label :email %><br />
<%= f.email_field :email, autofocus: true, autocomplete: "email" %>
</div>
<%= form_for(resource, as: resource_name, url: session_path(resource_name), html: { class: "space-y-4" }) do |f| %>
<div class="form-control">
<%= f.label :email, class: "label" %>
<%= f.email_field :email,
autofocus: true,
autocomplete: "email",
class: "input input-bordered w-full",
placeholder: "your@email.com" %>
</div>
<div class="field">
<%= f.label :password %><br />
<%= f.password_field :password, autocomplete: "current-password" %>
</div>
<div class="form-control">
<div class="flex justify-between items-center">
<%= f.label :password, class: "label" %>
<% if devise_mapping.recoverable? %>
<%= link_to "Forgot password?", new_password_path(resource_name), class: "label-text-alt link link-primary" %>
<% end %>
</div>
<%= f.password_field :password,
autocomplete: "current-password",
class: "input input-bordered w-full",
placeholder: "••••••••" %>
</div>
<% if devise_mapping.rememberable? %>
<div class="field">
<%= f.check_box :remember_me %>
<%= f.label :remember_me %>
<% if devise_mapping.rememberable? %>
<div class="form-control">
<label class="label cursor-pointer justify-start gap-2">
<%= f.check_box :remember_me, class: "checkbox checkbox-primary" %>
<span class="label-text">Remember me</span>
</label>
</div>
<% end %>
<div class="form-control mt-6">
<%= f.submit "Sign in", class: "btn btn-primary w-full" %>
</div>
<% end %>
<% if devise_mapping.registerable? %>
<div class="divider">OR</div>
<div class="text-center">
<p class="text-sm text-base-content/70 mb-4">
Don't have an account?
<%= link_to "Create an account", new_registration_path(resource_name), class: "link link-primary" %>
</p>
</div>
<% end %>
<% if devise_mapping.omniauthable? %>
<div class="space-y-3">
<%- resource_class.omniauth_providers.each do |provider| %>
<%= button_to omniauth_authorize_path(resource_name, provider),
class: "btn btn-outline w-full",
data: { turbo: false } do %>
<%= image_tag("#{provider}.svg", class: "w-5 h-5 mr-2") if File.exist?(Rails.root.join("app/assets/images/#{provider}.svg")) %>
Sign in with <%= OmniAuth::Utils.camelize(provider) %>
<% end %>
<% end %>
</div>
<% end %>
</div>
<% end %>
<div class="actions">
<%= f.submit "Log in" %>
</div>
<% end %>
<%= render "devise/shared/links" %>
</div>

View File

@ -0,0 +1,37 @@
<div class="fixed top-24 right-4 z-40 w-80 space-y-2" data-controller="flash">
<% if notice %>
<div class="alert alert-success shadow-lg">
<div class="flex justify-between w-full">
<div class="flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current flex-shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span><%= notice %></span>
</div>
<button class="btn btn-ghost btn-sm btn-circle" data-action="flash#close">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
</div>
<% end %>
<% if alert %>
<div class="alert alert-error shadow-lg">
<div class="flex justify-between w-full">
<div class="flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current flex-shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span><%= alert %></span>
</div>
<button class="btn btn-ghost btn-sm btn-circle" data-action="flash#close">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
</div>
<% end %>
</div>

View File

@ -5,9 +5,51 @@
Today AI Weather
<% end %>
</div>
<div class="flex-none">
<div class="flex-none gap-2">
<%= link_to "Cities", cities_path, class: "btn btn-ghost font-sans" %>
<%= link_to "Arts", arts_path, class: "btn btn-ghost font-sans" %>
<% if user_signed_in? %>
<div class="dropdown dropdown-end">
<label tabindex="0" class="btn btn-ghost">
<div class="flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M17.982 18.725A7.488 7.488 0 0012 15.75a7.488 7.488 0 00-5.982 2.975m11.963 0a9 9 0 10-11.963 0m11.963 0A8.966 8.966 0 0112 21a8.966 8.966 0 01-5.982-2.275M15 9.75a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
<span><%= current_user.email %></span>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5" />
</svg>
</div>
</label>
<ul tabindex="0" class="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-52">
<li>
<%= link_to edit_user_registration_path do %>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
<path stroke-linecap="round" stroke-linejoin="round" d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.24-.438.613-.431.992a6.759 6.759 0 010 .255c-.007.378.138.75.43.99l1.005.828c.424.35.534.954.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.57 6.57 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.28c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.02-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.992a6.932 6.932 0 010-.255c.007-.378-.138-.75-.43-.99l-1.004-.828a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.644-.869l.214-1.281z" />
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
Settings
<% end %>
</li>
<li>
<%= button_to destroy_user_session_path, method: :delete, class: "w-full" do %>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 9V5.25A2.25 2.25 0 0013.5 3h-6a2.25 2.25 0 00-2.25 2.25v13.5A2.25 2.25 0 007.5 21h6a2.25 2.25 0 002.25-2.25V15M12 9l-3 3m0 0l3 3m-3-3h12.75" />
</svg>
Sign out
<% end %>
</li>
</ul>
</div>
<% else %>
<%= link_to new_user_session_path, class: "btn btn-primary" do %>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 9V5.25A2.25 2.25 0 0013.5 3h-6a2.25 2.25 0 00-2.25 2.25v13.5A2.25 2.25 0 007.5 21h6a2.25 2.25 0 002.25-2.25V15m3 0l3-3m0 0l-3-3m3 3H9" />
</svg>
Sign in
<% end %>
<% end %>
</div>
</div>
</div>

View File

@ -59,25 +59,7 @@
<!-- 主要内容 -->
<main class="pt-16 relative">
<div class="toast toast-top toast-end z-[100] fixed pt-20">
<% if notice %>
<div class="alert alert-success shadow-lg">
<div>
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current flex-shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
<span><%= notice %></span>
</div>
</div>
<% end %>
<% if alert %>
<div class="alert alert-error shadow-lg">
<div>
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current flex-shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
<span><%= alert %></span>
</div>
</div>
<% end %>
</div>
<%= render 'layouts/flash_message' %>
<%= yield %>
</main>