feat: 交互体验专项优化(7 项)
- 登录回跳贯通:邮箱登录成功后按 ?redirect= 回到原页面(投票/收藏/评论的登录拦截闭环) - 收藏按钮的登录跳转补上 redirect 参数 - 营销路由组 loading.tsx 骨架屏(客户端导航即时反馈) - 投票按钮/收藏爱心按下缩放微交互 - 移动端抽屉滑入动画(keyframes + animate-drawer-in) - 工具列表空状态加'清除搜索与筛选'按钮(en/zh) - 品牌 404 页(图标 + Back to home / Browse tools 双入口)
This commit is contained in:
@@ -44,7 +44,12 @@ export function LoginForm() {
|
||||
return;
|
||||
}
|
||||
|
||||
router.push(`/${locale}/`);
|
||||
// 登录成功后回跳原页面(投票/收藏/评论的登录拦截会带 ?redirect= 参数)
|
||||
const redirect =
|
||||
typeof window !== "undefined"
|
||||
? new URLSearchParams(window.location.search).get("redirect")
|
||||
: null;
|
||||
router.push(redirect || `/${locale}/`);
|
||||
} catch (err) {
|
||||
setError(t('errors.loginFailed'));
|
||||
} finally {
|
||||
|
||||
@@ -53,7 +53,9 @@ export function FavoritesProvider({ children }: { children: React.ReactNode }) {
|
||||
(toolId: string) => {
|
||||
if (!isAuthenticated) {
|
||||
toast(t("loginRequired"));
|
||||
router.push(`/${locale}/login`);
|
||||
router.push(
|
||||
`/${locale}/login?redirect=${encodeURIComponent(window.location.pathname)}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -161,7 +163,7 @@ export function FavoriteButton({
|
||||
ctx.toggle(toolId);
|
||||
}}
|
||||
className={cn(
|
||||
"rounded-full p-2 transition-colors",
|
||||
"rounded-full p-2 transition-all active:scale-125",
|
||||
favorited
|
||||
? "text-red-500 hover:text-red-600"
|
||||
: "text-muted-foreground/50 hover:text-red-500"
|
||||
|
||||
@@ -49,6 +49,7 @@ interface ToolsTranslations {
|
||||
pricingPaid: string;
|
||||
sortLatest: string;
|
||||
sortTrending: string;
|
||||
clearFilters: string;
|
||||
}
|
||||
|
||||
interface UseCaseOption {
|
||||
@@ -351,6 +352,12 @@ export function ToolsList({
|
||||
<p className="text-muted-foreground">
|
||||
{translations.noTools}
|
||||
</p>
|
||||
<button
|
||||
onClick={() => router.push(`/${locale}/tools`)}
|
||||
className="mt-3 inline-flex items-center gap-1 text-sm font-medium text-primary hover:underline"
|
||||
>
|
||||
{translations.clearFilters}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -186,7 +186,7 @@ export function VoteButton({
|
||||
aria-pressed={voted}
|
||||
aria-label={voted ? t("unvote") : t("vote")}
|
||||
className={cn(
|
||||
"flex h-fit min-w-[4.5rem] flex-col items-center gap-0.5 rounded-xl border px-3 py-2.5 transition-colors",
|
||||
"flex h-fit min-w-[4.5rem] flex-col items-center gap-0.5 rounded-xl border px-3 py-2.5 transition-all active:scale-95",
|
||||
voted
|
||||
? "border-primary/60 bg-primary/10 text-primary"
|
||||
: "border-border text-foreground hover:border-primary/40 hover:bg-primary/5"
|
||||
@@ -205,7 +205,7 @@ export function VoteButton({
|
||||
onClick={() => ctx.toggle(toolId)}
|
||||
aria-pressed={voted}
|
||||
className={cn(
|
||||
"flex w-full items-center justify-center gap-2 rounded-lg border px-4 py-2.5 text-sm font-medium transition-colors",
|
||||
"flex w-full items-center justify-center gap-2 rounded-lg border px-4 py-2.5 text-sm font-medium transition-all active:scale-[0.98]",
|
||||
voted
|
||||
? "border-primary/60 bg-primary/10 text-primary"
|
||||
: "border-border text-foreground hover:bg-hover"
|
||||
|
||||
@@ -127,7 +127,7 @@ export function ThemeHeader({
|
||||
|
||||
{/* Mobile 抽屉 */}
|
||||
{open && (
|
||||
<nav className="border-t border-border bg-background px-4 py-3 lg:hidden" aria-label="Mobile">
|
||||
<nav className="animate-drawer-in border-t border-border bg-background px-4 py-3 lg:hidden" aria-label="Mobile">
|
||||
{navItems.map((item) => (
|
||||
<LocaleLink
|
||||
key={item.href}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* # 组件 MarketingLoading
|
||||
* 说明:营销路由组的加载骨架屏 —— 客户端导航时即时反馈,替代"冻结"的旧页面。
|
||||
*/
|
||||
export default function Loading() {
|
||||
return (
|
||||
<div className="mx-auto max-w-7xl px-4 py-16 sm:px-6">
|
||||
<div className="mx-auto mb-4 h-8 w-64 animate-pulse rounded-lg bg-secondary" />
|
||||
<div className="mx-auto mb-10 h-4 w-96 max-w-full animate-pulse rounded bg-secondary" />
|
||||
<div className="grid gap-3 md:grid-cols-2 lg:grid-cols-3">
|
||||
{Array.from({ length: 6 }).map((_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="h-28 animate-pulse rounded-xl border border-border bg-secondary/50"
|
||||
style={{ animationDelay: `${i * 90}ms` }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -92,6 +92,7 @@ export default async function ToolsPage({ searchParams }: ToolsPageProps) {
|
||||
pricingPaid: t("pricing.paid"),
|
||||
sortLatest: t("sort.latest"),
|
||||
sortTrending: t("sort.trending"),
|
||||
clearFilters: t("clearFilters"),
|
||||
};
|
||||
|
||||
const baseUrl = process.env.NEXT_PUBLIC_APP_URL || 'https://toolso.ai';
|
||||
|
||||
@@ -1,16 +1,36 @@
|
||||
import Link from 'next/link';
|
||||
import { defaultLocale } from '@/i18n.config';
|
||||
|
||||
/**
|
||||
* # 组件 NotFound
|
||||
* 说明:品牌化 404 页(图标 + 双入口:首页 / 工具目录)。
|
||||
*/
|
||||
export default function NotFound() {
|
||||
return (
|
||||
<html>
|
||||
<body>
|
||||
<div style={{ textAlign: 'center', padding: '100px' }}>
|
||||
<h1>404 - Page Not Found</h1>
|
||||
<p>The page you are looking for doesn't exist.</p>
|
||||
<Link href={`/${defaultLocale}`}>
|
||||
Go to Homepage
|
||||
</Link>
|
||||
<body style={{ margin: 0, fontFamily: 'system-ui, sans-serif', background: '#fafafa', color: '#0a0a0a' }}>
|
||||
<div style={{ minHeight: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '24px' }}>
|
||||
<div style={{ textAlign: 'center', maxWidth: '420px' }}>
|
||||
<div style={{ width: 64, height: 64, margin: '0 auto 20px', borderRadius: 16, background: '#0a0a0a', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="#fff" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M15 21v-8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v8" />
|
||||
<path d="M3 10a2 2 0 0 1 .709-1.528l7-5.999a2 2 0 0 1 2.582 0l7 5.999A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" />
|
||||
</svg>
|
||||
</div>
|
||||
<p style={{ fontSize: 13, letterSpacing: '0.2em', color: '#737373', margin: '0 0 8px' }}>404</p>
|
||||
<h1 style={{ fontSize: 24, margin: '0 0 8px' }}>Page not found</h1>
|
||||
<p style={{ fontSize: 14, color: '#525252', lineHeight: 1.6, margin: '0 0 24px' }}>
|
||||
The page you are looking for doesn't exist or may have moved.
|
||||
</p>
|
||||
<div style={{ display: 'flex', gap: 12, justifyContent: 'center' }}>
|
||||
<Link href={`/${defaultLocale}`} style={{ display: 'inline-block', padding: '10px 20px', borderRadius: 8, background: '#0a0a0a', color: '#fff', textDecoration: 'none', fontSize: 14 }}>
|
||||
Back to home
|
||||
</Link>
|
||||
<Link href={`/${defaultLocale}/tools`} style={{ display: 'inline-block', padding: '10px 20px', borderRadius: 8, border: '1px solid #e5e5e5', color: '#0a0a0a', textDecoration: 'none', fontSize: 14 }}>
|
||||
Browse tools
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -104,3 +104,18 @@ html {
|
||||
text-wrap: balance;
|
||||
}
|
||||
}
|
||||
|
||||
/* 移动端抽屉滑入动画(交互反馈) */
|
||||
@keyframes bu-drawer-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-8px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
.animate-drawer-in {
|
||||
animation: bu-drawer-in 0.18s ease-out;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user