fix: landing hero UI, auth guard, stats/history toggle

- Redesigned LandingHero with sleek gradient background, search icon, badge
- Auth guard: unauthenticated users see login page instead of My Links
- LinkDetailPage: stats & history hidden by default, toggle via floating buttons
- Mobile responsive: hero adapts to small screens
- CSS: added detail-floating-actions, refined hero styles
This commit is contained in:
Hermes Agent
2026-06-21 09:38:55 +10:00
parent 4690eca75c
commit bd31662ab8
4 changed files with 253 additions and 13 deletions
+3 -1
View File
@@ -116,7 +116,9 @@ export default function App() {
</>
) : null}
{route === 'home' && user ? <PrivateLinksPage /> : null}
{route === 'my-links' ? <PrivateLinksPage /> : null}
{/* 未登录时 My Links 导航显示登录页 */}
{route === 'my-links' && !user ? <LoginPage /> : null}
{route === 'my-links' && user ? <PrivateLinksPage /> : null}
{route === 'admin' ? <AdminReviewPage /> : null}
{route === 'login' ? <LoginPage /> : null}
{route === 'dev-login' ? <DevLoginPage /> : null}
+19 -12
View File
@@ -1,4 +1,5 @@
import { useState } from 'react';
import { ArrowRight, Search } from 'lucide-react';
export default function LandingHero() {
const [query, setQuery] = useState('');
@@ -9,56 +10,62 @@ export default function LandingHero() {
const trimmed = query.trim().toLowerCase();
if (!trimmed) return;
setSearching(true);
// Navigate to the alias — the SPA will handle it via the worker redirect
window.location.href = `/${trimmed}`;
}
return (
<section className="landing-hero">
<div className="hero-content">
<div className="hero-badge"></div>
<h1 className="hero-title">
<span className="hero-brand">Heygo</span>
<span className="hero-suffix"></span>
</h1>
<p className="hero-subtitle"></p>
<p className="hero-description">
访
<br />
</p>
<div className="hero-features">
<div className="feature-pill">
<span className="feature-dot">🔗</span>
<span className="feature-dot" />
</div>
<div className="feature-pill">
<span className="feature-dot">🔒</span>
<span className="feature-dot" />
</div>
<div className="feature-pill">
<span className="feature-dot">🌍</span>
<span className="feature-dot" />
</div>
<div className="feature-pill">
<span className="feature-dot">📱</span>
<span className="feature-dot" />
</div>
</div>
</div>
<form className="hero-search-box" onSubmit={handleSearch}>
<div className="search-row">
<span className="search-prefix-label">heygo.cc/</span>
<div className="search-prefix">
<span className="search-prefix-icon"><Search size={16} /></span>
<span className="search-prefix-text">heygo.cc/</span>
</div>
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="输入短链名称,例如 canva"
placeholder="输入短链名称"
className="search-field"
autoFocus
/>
<button
type="submit"
className="search-go"
disabled={searching || !query.trim()}
>
{searching ? '…' : '→'}
<ArrowRight size={18} />
</button>
</div>
<p className="search-hint">
+15
View File
@@ -111,6 +111,8 @@ export default function LinkDetailPage({ linkId, linkScope, currentUser, linkBas
const [saving, setSaving] = useState(false);
const [deleting, setDeleting] = useState(false);
const [templateValues, setTemplateValues] = useState<Record<string, string>>({});
const [showStats, setShowStats] = useState(false);
const [showHistory, setShowHistory] = useState(false);
const chartRef = useRef<HTMLCanvasElement | null>(null);
const chartInstance = useRef<Chart | null>(null);
@@ -728,6 +730,16 @@ export default function LinkDetailPage({ linkId, linkScope, currentUser, linkBas
</section>
) : null}
<div className="detail-floating-actions">
<button type="button" className={`link-action${showStats ? ' link-action--confirm' : ''}`} onClick={() => setShowStats(v => !v)}>
📊 Stats
</button>
<button type="button" className={`link-action${showHistory ? ' link-action--confirm' : ''}`} onClick={() => setShowHistory(v => !v)}>
📋 History
</button>
</div>
{showStats ? (
<section className="panel" style={{ padding: '1.5rem' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '1.25rem', flexWrap: 'wrap', gap: '0.5rem' }}>
<h2 style={{ fontSize: '1rem', fontWeight: 600, margin: 0, letterSpacing: '-0.01em' }}>
@@ -767,7 +779,9 @@ export default function LinkDetailPage({ linkId, linkScope, currentUser, linkBas
<canvas ref={chartRef} style={{ display: stats.length === 0 || loadingStats ? 'none' : 'block' }} />
</div>
</section>
) : null}
{showHistory ? (
<section className="panel" style={{ padding: '1.5rem' }}>
<h2 style={{ fontSize: '1rem', fontWeight: 600, margin: '0 0 1rem', letterSpacing: '-0.01em' }}>
Change History
@@ -784,6 +798,7 @@ export default function LinkDetailPage({ linkId, linkScope, currentUser, linkBas
</ul>
)}
</section>
) : null}
{showCopyDialog && link ? (
<CopyPublicLinkDialog
+216
View File
@@ -1211,6 +1211,12 @@ button.link-action--confirm:hover { background: var(--accent-soft); }
color: #B91C1C;
}
.detail-floating-actions {
display: flex;
gap: 0.5rem;
justify-content: flex-start;
}
.copy-success-toast {
position: fixed;
top: 5.5rem;
@@ -1961,6 +1967,201 @@ button.link-action--confirm:hover { background: var(--accent-soft); }
}
}
/* ═══════════════════════════════════════════
Landing Hero
════════════════════════════════════════════ */
.landing-hero {
display: flex;
flex-direction: column;
align-items: center;
gap: 2.5rem;
padding: 5rem 0 1rem;
width: 100%;
position: relative;
}
.landing-hero::before {
content: '';
position: absolute;
inset: 0;
background: radial-gradient(ellipse 60% 50% at 50% 0%, rgba(99,102,241,0.06) 0%, transparent 70%);
pointer-events: none;
}
.hero-content {
text-align: center;
max-width: 600px;
position: relative;
}
.hero-badge {
display: inline-flex;
background: linear-gradient(135deg, rgba(99,102,241,0.12), rgba(129,140,248,0.08));
border: 1px solid rgba(99,102,241,0.18);
border-radius: 999px;
padding: 0.35rem 1rem;
font-size: 0.8125rem;
font-weight: 600;
color: var(--accent-text);
margin-bottom: 1.25rem;
letter-spacing: 0.02em;
}
.hero-title {
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
margin: 0 0 0.75rem;
}
.hero-brand {
font-family: 'Syne', ui-sans-serif, sans-serif;
font-size: 4rem;
font-weight: 800;
letter-spacing: -0.04em;
color: var(--text);
line-height: 1;
}
.hero-suffix {
font-size: 1.75rem;
font-weight: 700;
color: var(--accent);
line-height: 1;
}
.hero-description {
font-size: 1.0625rem;
line-height: 1.7;
margin: 0 auto 1.75rem;
max-width: 460px;
color: var(--muted);
}
.hero-features {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
justify-content: center;
}
.feature-pill {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 999px;
display: inline-flex;
align-items: center;
gap: 0.4rem;
font-size: 0.8125rem;
font-weight: 500;
padding: 0.35rem 0.85rem;
color: var(--text);
white-space: nowrap;
box-shadow: 0 1px 2px rgba(15,23,42,0.04);
transition: border-color 0.15s, box-shadow 0.15s;
}
.feature-pill:hover {
border-color: var(--accent);
box-shadow: 0 0 0 3px var(--accent-soft);
}
.feature-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--accent);
flex-shrink: 0;
}
.hero-search-box {
width: 100%;
max-width: 540px;
position: relative;
}
.search-row {
display: flex;
align-items: stretch;
border: 2px solid var(--border-strong);
border-radius: 12px;
overflow: hidden;
background: var(--surface);
transition: border-color 0.2s, box-shadow 0.2s;
}
.search-row:focus-within {
border-color: var(--accent);
box-shadow: 0 0 0 4px var(--accent-soft), 0 4px 20px rgba(99,102,241,0.08);
}
.search-prefix {
display: flex;
align-items: center;
gap: 0.4rem;
background: var(--background);
border-right: 1px solid var(--border);
padding: 0 0.85rem;
flex-shrink: 0;
}
.search-prefix-icon {
color: var(--muted);
display: flex;
}
.search-prefix-text {
color: var(--accent-text);
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 0.875rem;
font-weight: 600;
white-space: nowrap;
}
.search-field {
border: none;
font: inherit;
font-size: 1rem;
padding: 0.85rem 0.85rem;
flex: 1;
min-width: 0;
outline: none;
background: transparent;
color: var(--text);
}
.search-field::placeholder {
color: var(--muted);
opacity: 0.6;
}
.search-go {
background: transparent;
border: none;
border-left: 1px solid var(--border);
color: var(--accent);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
padding: 0 1.1rem;
transition: background 0.15s, color 0.15s;
flex-shrink: 0;
}
.search-go:hover { background: var(--accent); color: white; }
.search-go:disabled { color: var(--muted); cursor: not-allowed; }
.search-go:disabled:hover { background: transparent; color: var(--muted); }
.search-hint {
color: var(--muted);
font-size: 0.8125rem;
margin: 0.75rem 0 0;
text-align: center;
opacity: 0.7;
}
/* ═══════════════════════════════════════════
Responsive
════════════════════════════════════════════ */
@@ -2014,4 +2215,19 @@ button.link-action--confirm:hover { background: var(--accent-soft); }
.link-search__result-url .truncate {
max-width: 100%;
}
/* Landing Hero — 移动端适配 */
.landing-hero { padding: 3rem 0 0.5rem; gap: 2rem; }
.hero-brand { font-size: 2.5rem; }
.hero-suffix { font-size: 1.2rem; }
.hero-description { font-size: 0.9375rem; }
.hero-badge { font-size: 0.75rem; }
.feature-pill { font-size: 0.75rem; padding: 0.25rem 0.65rem; }
.search-row { border-radius: 10px; }
.search-field { font-size: 0.9375rem; padding: 0.75rem 0.65rem; }
.search-prefix { padding: 0 0.65rem; }
.search-prefix-text { font-size: 0.8rem; }
.search-go { padding: 0 0.85rem; }
.search-hint { font-size: 0.75rem; }
.hero-content { max-width: 100%; }
}