feat: add theme switching and toast notifications

- Implement theme switching functionality with a new ThemeController
- Add ToastController for displaying notifications
- Update various views for improved layout and styling
- Introduce animations for toast notifications

These changes enhance the user experience by allowing users to switch between light and dark themes and receive feedback through toast notifications. The UI has been improved for better accessibility and aesthetics.
This commit is contained in:
songtianlun 2025-01-17 15:02:25 +08:00
parent 42d8d5ce1d
commit 87e0c2eec6
18 changed files with 308 additions and 94 deletions

View File

@ -18,7 +18,7 @@
} }
h1 { h1 {
@apply text-[3em] tracking-[-2px] mb-[30px] text-center; @apply text-[3em] tracking-[-2px] mb-8 mt-8 text-center;
} }
h2 { h2 {

View File

@ -8,5 +8,6 @@ import "@hotwired/turbo-rails"
import "flowbite/dist/flowbite.turbo" import "flowbite/dist/flowbite.turbo"
import "./controllers" // import "./theme_switch"
import "./controllers"

View File

@ -4,3 +4,10 @@
// eagerLoadControllersFrom("controllers", application) // eagerLoadControllersFrom("controllers", application)
import { application } from "./application" import { application } from "./application"
import ThemeController from "./theme_controller"
import ToastController from "./toast_controller"
import HelloController from "./hello_controller"
application.register("hello", HelloController)
application.register("theme", ThemeController)
application.register("toast", ToastController)

View File

@ -0,0 +1,24 @@
import { Controller } from "@hotwired/stimulus"
// Connects to data-controller="theme"
export default class extends Controller {
static targets = ["toggle"]
connect() {
this.initTheme()
}
initTheme() {
const savedTheme = localStorage.getItem('theme')
if(savedTheme) {
document.documentElement.setAttribute('data-theme', savedTheme)
this.toggleTarget.checked = savedTheme === 'dark'
}
}
toggle(event) {
const newTheme = event.target.checked ? 'dark' : 'light'
document.documentElement.setAttribute('data-theme', newTheme)
localStorage.setItem('theme', newTheme)
}
}

View File

@ -0,0 +1,14 @@
import { Controller } from "@hotwired/stimulus"
// Connects to data-controller="toast"
export default class extends Controller {
connect() {
// 3秒后自动隐藏
setTimeout(() => {
this.element.classList.add('animate-fade-out')
setTimeout(() => {
this.element.remove()
}, 500)
}, 3000)
}
}

View File

@ -0,0 +1,22 @@
// 获取切换按钮
const themeToggle = document.querySelector('.theme-controller');
// 监听变化
themeToggle.addEventListener('change', (e) => {
// 切换 HTML data-theme 属性
if(e.target.checked) {
document.documentElement.setAttribute('data-theme', 'dark');
} else {
document.documentElement.setAttribute('data-theme', 'light');
}
// 保存主题设置到 localStorage
localStorage.setItem('theme', e.target.checked ? 'dark' : 'light');
});
// 页面加载时检查之前的主题设置
const savedTheme = localStorage.getItem('theme');
if(savedTheme) {
document.documentElement.setAttribute('data-theme', savedTheme);
themeToggle.checked = savedTheme === 'dark';
}

View File

@ -1,4 +1,4 @@
<footer class="footer footer-center bg-base-200 text-base-content rounded p-10"> <footer class="footer footer-center bg-base-200 text-base-content rounded p-10 mt-8">
<nav class="grid grid-flow-col gap-4"> <nav class="grid grid-flow-col gap-4">
<%= link_to "About", about_url, class: "link link-hover" %> <%= link_to "About", about_url, class: "link link-hover" %>
<%= link_to "Contact", contact_url, class: "link link-hover" %> <%= link_to "Contact", contact_url, class: "link link-hover" %>

View File

@ -1,4 +1,11 @@
<div class="navbar bg-base-100 glass"> <!-- 固定在顶部容器 -->
<div class="fixed top-0 left-0 right-0 z-50">
<!-- 响应式内边距 -->
<!--<div class="container mx-auto px-3 sm:px-6 lg:px-1 py-4">-->
<!-- 顶部菜单栏 -->
<div class="navbar
backdrop-filter backdrop-blur-lg bg-opacity-30 border-b border-gray-200 border-transparent
shadow-md min-h-0 h-12 px-8 lg:px-12">
<div class="navbar-start"> <div class="navbar-start">
<%= link_to "sample app", root_url, id: "logo", class: "btn btn-ghost text-xl" %> <%= link_to "sample app", root_url, id: "logo", class: "btn btn-ghost text-xl" %>
</div> </div>
@ -9,18 +16,20 @@
<li><%= link_to "Help", help_url %></li> <li><%= link_to "Help", help_url %></li>
<% if logged_in? %> <% if logged_in? %>
<li><%= link_to "Users", users_path %></li> <li><%= link_to "Users", users_path %></li>
<li class="dropdown"> <li>
<a href="#" class="dropdown-toggle" data-toggle="dropdown"> <details>
<summary>
Account <b class="caret"></b> Account <b class="caret"></b>
</a> </summary>
<ul class="dropdown-menu"> <ul class="bg-base-100 rounded-t-none p-2">
<li><%= link_to "Profile", current_user %></li> <li><%= link_to "Profile", current_user %></li>
<li><%= link_to "Settings", edit_user_path(current_user) %></li> <li><%= link_to "Settings", edit_user_path(current_user) %></li>
<li class="divider"></li> <div class="divider"></div>
<li> <li>
<%= link_to "Log out", logout_path, data: { turbo_method: :delete } %> <%= link_to "Log out", logout_path, data: { turbo_method: :delete } %>
</li> </li>
</ul> </ul>
</details>
</li> </li>
<% else %> <% else %>
<li><%= link_to "Log in", login_path %></li> <li><%= link_to "Log in", login_path %></li>
@ -67,27 +76,11 @@
<% end %> <% end %>
</ul> </ul>
</details> </details>
<label class="swap swap-rotate"> <%= render "layouts/theme_swap" %>
<!-- this hidden checkbox controls the state -->
<input type="checkbox" class="theme-controller" value="light" />
<!-- sun icon -->
<svg
class="swap-off h-10 w-10 fill-current"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24">
<path
d="M5.64,17l-.71.71a1,1,0,0,0,0,1.41,1,1,0,0,0,1.41,0l.71-.71A1,1,0,0,0,5.64,17ZM5,12a1,1,0,0,0-1-1H3a1,1,0,0,0,0,2H4A1,1,0,0,0,5,12Zm7-7a1,1,0,0,0,1-1V3a1,1,0,0,0-2,0V4A1,1,0,0,0,12,5ZM5.64,7.05a1,1,0,0,0,.7.29,1,1,0,0,0,.71-.29,1,1,0,0,0,0-1.41l-.71-.71A1,1,0,0,0,4.93,6.34Zm12,.29a1,1,0,0,0,.7-.29l.71-.71a1,1,0,1,0-1.41-1.41L17,5.64a1,1,0,0,0,0,1.41A1,1,0,0,0,17.66,7.34ZM21,11H20a1,1,0,0,0,0,2h1a1,1,0,0,0,0-2Zm-9,8a1,1,0,0,0-1,1v1a1,1,0,0,0,2,0V20A1,1,0,0,0,12,19ZM18.36,17A1,1,0,0,0,17,18.36l.71.71a1,1,0,0,0,1.41,0,1,1,0,0,0,0-1.41ZM12,6.5A5.5,5.5,0,1,0,17.5,12,5.51,5.51,0,0,0,12,6.5Zm0,9A3.5,3.5,0,1,1,15.5,12,3.5,3.5,0,0,1,12,15.5Z" />
</svg>
<!-- moon icon -->
<svg
class="swap-on h-10 w-10 fill-current"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24">
<path
d="M21.64,13a1,1,0,0,0-1.05-.14,8.05,8.05,0,0,1-3.37.73A8.15,8.15,0,0,1,9.08,5.49a8.59,8.59,0,0,1,.25-2A1,1,0,0,0,8,2.36,10.14,10.14,0,1,0,22,14.05,1,1,0,0,0,21.64,13Zm-9.5,6.69A8.14,8.14,0,0,1,7.08,5.22v.27A10.15,10.15,0,0,0,17.22,15.63a9.79,9.79,0,0,0,2.1-.22A8.11,8.11,0,0,1,12.14,19.73Z" />
</svg>
</label>
</div> </div>
</div> </div>
<!--</div>-->
</div>
<!-- 添加一个占位 div防止内容被覆盖 -->
<div aria-hidden="true" class="border-none h-12"></div>

View File

@ -0,0 +1,37 @@
<!-- 放在布局文件的 body 标签末尾 -->
<div class="toast toast-end">
<% flash.each do |message_type, message| %>
<% alert_class = case message_type
when "success" then "alert alert-success"
when "danger", "error" then "alert alert-error"
when "warning" then "alert alert-warning"
when "info" then "alert alert-info"
else "alert"
end
%>
<div data-controller="toast" class="<%= alert_class %> animate-slide-in-right">
<div>
<% case message_type %>
<% when "success" %>
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<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>
<% when "error", "danger" %>
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<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>
<% when "warning" %>
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
<% else %>
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<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" />
</svg>
<% end %>
<span><%= message %></span>
</div>
</div>
<% end %>
</div>

View File

@ -0,0 +1,29 @@
<div data-controller="theme">
<label class="swap swap-rotate">
<!-- this hidden checkbox controls the state -->
<input
type="checkbox"
class="theme-controller"
data-theme-target="toggle"
data-action="change->theme#toggle"
/>
<!-- sun icon -->
<svg
class="swap-off h-6 w-6 fill-current"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24">
<path
d="M5.64,17l-.71.71a1,1,0,0,0,0,1.41,1,1,0,0,0,1.41,0l.71-.71A1,1,0,0,0,5.64,17ZM5,12a1,1,0,0,0-1-1H3a1,1,0,0,0,0,2H4A1,1,0,0,0,5,12Zm7-7a1,1,0,0,0,1-1V3a1,1,0,0,0-2,0V4A1,1,0,0,0,12,5ZM5.64,7.05a1,1,0,0,0,.7.29,1,1,0,0,0,.71-.29,1,1,0,0,0,0-1.41l-.71-.71A1,1,0,0,0,4.93,6.34Zm12,.29a1,1,0,0,0,.7-.29l.71-.71a1,1,0,1,0-1.41-1.41L17,5.64a1,1,0,0,0,0,1.41A1,1,0,0,0,17.66,7.34ZM21,11H20a1,1,0,0,0,0,2h1a1,1,0,0,0,0-2Zm-9,8a1,1,0,0,0-1,1v1a1,1,0,0,0,2,0V20A1,1,0,0,0,12,19ZM18.36,17A1,1,0,0,0,17,18.36l.71.71a1,1,0,0,0,1.41,0,1,1,0,0,0,0-1.41ZM12,6.5A5.5,5.5,0,1,0,17.5,12,5.51,5.51,0,0,0,12,6.5Zm0,9A3.5,3.5,0,1,1,15.5,12,3.5,3.5,0,0,1,12,15.5Z" />
</svg>
<!-- moon icon -->
<svg
class="swap-on h-6 w-6 fill-current"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24">
<path
d="M21.64,13a1,1,0,0,0-1.05-.14,8.05,8.05,0,0,1-3.37.73A8.15,8.15,0,0,1,9.08,5.49a8.59,8.59,0,0,1,.25-2A1,1,0,0,0,8,2.36,10.14,10.14,0,1,0,22,14.05,1,1,0,0,0,21.64,13Zm-9.5,6.69A8.14,8.14,0,0,1,7.08,5.22v.27A10.15,10.15,0,0,0,17.22,15.63a9.79,9.79,0,0,0,2.1-.22A8.11,8.11,0,0,1,12.14,19.73Z" />
</svg>
</label>
</div>

View File

@ -12,10 +12,7 @@
<body> <body>
<%= render 'layouts/header' %> <%= render 'layouts/header' %>
<div class=""> <div class="">
<% flash.each do |message_type, message| %> <%= render 'layouts/message' %>
<%= content_tag(:div, message, class: "alert alert-#{message_type}") %>
<!-- <div class="alert alert-<%#= message_type %>"><%#= message %></div>-->
<% end %>
<%= yield %> <%= yield %>
<%= render 'layouts/footer' %> <%= render 'layouts/footer' %>
<%#= debug(params) if Rails.env.development? %> <%#= debug(params) if Rails.env.development? %>

View File

@ -1,24 +1,45 @@
<% provide(:title, "Log in") %> <% provide(:title, "Log in") %>
<h1>Log in</h1> <div class ="container mx-auto px-4">
<div class ="row"> <h1 class="text-3xl font-bold text-center my-8">Log in</h1>
<div class="col-md-6 col-md-offset-3">
<%= form_with(url: login_path, scope: :session, local: true) do |f| %>
<%= f.label :email %>
<%= f.email_field :email, class: 'form-control' %>
<%= f.label :password %> <div class="max-w-md mx-auto">
<%= link_to "(forgot password)", new_password_reset_path %> <%= form_with(url: login_path, scope: :session, local: true, class: "space-y-4") do |f| %>
<%= f.password_field :password, class: 'form-control' %> <div class="form-control">
<%= f.label :email, class: "label" do %>
<span class="label-text">Email</span>
<% end %>
<%= f.email_field :email, class: "input input-bordered w-full" %>
</div>
<%= f.label :remember_me, class: "checkbox inline" do %> <div class="form-control">
<%= f.check_box :remember_me %> <div class="flex justify-between items-center">
<span>Remember me on this computer</span> <%= f.label :password, class: "label" do %>
<span class="label-text">Password</span>
<% end %>
<%= link_to "(forgot password)", new_password_reset_path,
class: "text-sm text-primary hover:text-primary-focus" %>
</div>
<%= f.password_field :password, class: "input input-bordered w-full" %>
</div>
<div class="form-control">
<label class="label cursor-pointer">
<%= f.check_box :remember_me, class: "checkbox checkbox-primary" %>
<span class="label-text ml-2">Remember me on this computer</span>
</label>
</div>
<div class="form-control mt-6">
<%= f.submit "Log in", class: "btn btn-primary w-full" %>
</div>
<% end %> <% end %>
<%= f.submit "Log in", class: "btn btn-primary" %> <div class="text-center mt-6">
<% end %> <p>New user?
<%= link_to "Sign up now!", signup_path,
<p>New user? <%= link_to "Sign up now!", signup_path %></p> class: "text-primary hover:text-primary-focus" %>
</p>
</div>
</div> </div>
</div> </div>

View File

@ -31,6 +31,6 @@
</div> </div>
<div class="form-control mt-6"> <div class="form-control mt-6">
<%= f.submit yield(:button_text), class: "btn btn-primary" %> <%= f.submit yield(:button_text), class: "btn btn-primary mb-6" %>
</div> </div>
<% end %> <% end %>

View File

@ -1,11 +1,24 @@
<li> <div class="card card-compact bg-base-100 shadow-lg hover:shadow-xl transition-shadow">
<%= gravatar_for user, size: 50 %> <div class="card-body flex-row items-center justify-between">
<%= link_to user.name, user %> <!-- 用户基本信息 -->
<div class="flex items-center gap-4">
<%= gravatar_for user, size: 50, class: "rounded-full" %>
<%= link_to user.name, user,
class: "link link-hover text-lg font-medium" %>
</div>
<!-- 管理员操作按钮 -->
<% if current_user.admin? && !current_user?(user) %> <% if current_user.admin? && !current_user?(user) %>
| <%= link_to "delete", user, <%= link_to user,
data: { data: {
turbo_method: :delete, turbo_method: :delete,
confirm: "You sure?" turbo_confirm: "Are you sure you want to delete this user?"
} %> },
class: "btn btn-ghost btn-sm text-error hover:bg-error hover:text-white" do %>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
<% end %> <% end %>
</li> <% end %>
</div>
</div>

View File

@ -1,13 +1,34 @@
<% provide(:title, "Edit user") %> <% provide(:title, "Edit user") %>
<% provide(:button_text, 'Save changes') %> <% provide(:button_text, 'Save changes') %>
<h1>Update your profile</h1>
<div class="row"> <div class="container mx-auto px-4 py-8">
<div class="col-md-6 col-md-offset-3"> <div class="max-w-md mx-auto">
<!-- 标题 -->
<h1 class="text-2xl font-bold text-center mb-8">Update your profile</h1>
<!-- 表单区域 -->
<div class="card bg-base-100 shadow-lg">
<div class="card-body">
<%= render 'form' %> <%= render 'form' %>
<div class="gravatar_edit">
<%= gravatar_for @user %>
<a href="https://gravatar.com/emails" target="_blank">change</a>
</div> </div>
</div> </div>
<!-- 头像编辑区 -->
<div class="card bg-base-100 shadow-lg mb-8">
<div class="card-body items-center text-center">
<%= gravatar_for @user, size: 100, class: "rounded-full ring ring-primary ring-offset-2" %>
<%= link_to "https://gravatar.com/emails",
target: "_blank",
rel: "noopener noreferrer",
class: "btn btn-outline btn-sm gap-2 mt-4" do %>
<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="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12" />
</svg>
Change Avatar
<% end %>
</div>
</div>
</div>
</div> </div>

View File

@ -1,17 +1,15 @@
<% provide(:title, 'All users') %> <% provide(:title, 'All users') %>
<h1>All users</h1> <div class="container mx-auto px-4 py-8">
<h1 class="text-3xl font-bold text-center mb-8">All Users</h1>
<!-- 分页 -->
<div class="flex justify-center mb-6">
<%= paginate @users %> <%= paginate @users %>
</div>
<!-- 用户列表 -->
<ul class="users"> <div class="grid gap-4 max-w-3xl mx-auto">
<%= render @users %> <%= render @users %>
<%# auto loop by rails, like after %> </div>
<%# @users.each do |user| %> </div>
<!-- <li>-->
<%#= gravatar_for user, size: 50 %>
<%#= link_to user.name, user %>
<!-- </li>-->
<%# end %>
</ul>

View File

@ -1,12 +1,25 @@
<% provide(:title, @user.name) %> <% provide(:title, @user.name) %>
<div class="row"> <div class="container mx-auto px-4 py-8">
<aside class="col-md-4"> <aside class="max-w-2xl mx-auto">
<section class="user_info"> <section class="card bg-base-100 shadow-xl">
<h1> <div class="card-body">
<%= gravatar_for @user %> <!-- 头像和用户名布局 -->
<h1 class="flex flex-col sm:flex-row items-center gap-6">
<!-- 头像添加装饰效果 -->
<div class="relative">
<%= gravatar_for @user, size: 120, class: "rounded-full ring-2 ring-primary ring-offset-2 hover:ring-4 transition-all duration-300" %>
<div class="absolute inset-0 rounded-full bg-primary/10 animate-pulse"></div>
</div>
<!-- 用户名样式 -->
<div class="text-center sm:text-left">
<span class="text-2xl font-bold bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent">
<%= @user.name %> <%= @user.name %>
</span>
</div>
</h1> </h1>
</div>
</section> </section>
</aside> </aside>
</div> </div>

View File

@ -13,6 +13,30 @@ module.exports = {
fontFamily: { fontFamily: {
sans: ['Inter var', ...defaultTheme.fontFamily.sans], sans: ['Inter var', ...defaultTheme.fontFamily.sans],
}, },
keyframes: {
'slide-in-right': {
'0%': {
transform: 'translateX(100%)',
opacity: '0'
},
'100%': {
transform: 'translateX(0)',
opacity: '1'
},
},
'fade-out': {
'0%': {
opacity: '1'
},
'100%': {
opacity: '0'
},
}
},
animation: {
'slide-in-right': 'slide-in-right 0.5s ease-out',
'fade-out': 'fade-out 0.5s ease-out'
},
}, },
}, },
plugins: [ plugins: [