refactor: streamline homepage layout and enhance sidebar functionality
All checks were successful
docker-images / resolve-build-targets (push) Successful in 6s
ui-regression / playwright-regression (push) Successful in 4m15s
docker-images / build-and-push (admin) (push) Successful in 1m6s
docker-images / build-and-push (backend) (push) Successful in 4s
docker-images / build-and-push (frontend) (push) Successful in 1m19s
docker-images / submit-indexnow (push) Successful in 18s
All checks were successful
docker-images / resolve-build-targets (push) Successful in 6s
ui-regression / playwright-regression (push) Successful in 4m15s
docker-images / build-and-push (admin) (push) Successful in 1m6s
docker-images / build-and-push (backend) (push) Successful in 4s
docker-images / build-and-push (frontend) (push) Successful in 1m19s
docker-images / submit-indexnow (push) Successful in 18s
- Removed unused FriendLinkCard import and adjusted sidebar friend links to display a maximum of three. - Introduced a new popularPreviewLimit constant to limit the number of popular posts displayed. - Enhanced the sidebar with quick links, popular content, and friend links sections, improving overall navigation. - Updated the layout to use a grid system for better responsiveness and organization. - Simplified the popular posts section by removing sorting options and adjusting the display logic. - Improved accessibility and readability of various components, including command prompts and statistics.
This commit is contained in:
@@ -17,6 +17,7 @@ interface Props {
|
||||
description?: string;
|
||||
stats?: ShareStat[];
|
||||
wechatShareQrEnabled?: boolean;
|
||||
variant?: 'default' | 'compact';
|
||||
}
|
||||
|
||||
const { locale, t } = getI18n(Astro);
|
||||
@@ -33,7 +34,9 @@ const {
|
||||
: '复制链接、摘要或二维码,方便换设备继续看,也方便直接发给别人。',
|
||||
stats = [],
|
||||
wechatShareQrEnabled = false,
|
||||
variant = 'default',
|
||||
} = Astro.props as Props;
|
||||
const isCompact = variant === 'compact';
|
||||
|
||||
const visibleBadge = badge;
|
||||
const visibleTitle = title;
|
||||
@@ -136,10 +139,13 @@ if (wechatShareQrEnabled) {
|
||||
---
|
||||
|
||||
<section
|
||||
class="rounded-[28px] border border-[var(--border-color)] bg-[linear-gradient(135deg,rgba(var(--primary-rgb),0.1),rgba(var(--secondary-rgb),0.04)_46%,rgba(var(--bg-rgb),0.92))] p-5 sm:p-6"
|
||||
class:list={[
|
||||
'border border-[var(--border-color)] bg-[linear-gradient(135deg,rgba(var(--primary-rgb),0.1),rgba(var(--secondary-rgb),0.04)_46%,rgba(var(--bg-rgb),0.92))]',
|
||||
isCompact ? 'rounded-[24px] p-4' : 'rounded-[28px] p-5 sm:p-6',
|
||||
]}
|
||||
data-share-panel-id={panelId}
|
||||
>
|
||||
<div class="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
|
||||
<div class:list={['flex flex-col gap-4', !isCompact && 'lg:flex-row lg:items-start lg:justify-between']}>
|
||||
<div class="space-y-3">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<span class="inline-flex items-center gap-2 rounded-full border border-[var(--primary)]/20 bg-[var(--primary)]/10 px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.24em] text-[var(--primary)]">
|
||||
@@ -149,13 +155,17 @@ if (wechatShareQrEnabled) {
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<h3 class="text-xl font-semibold text-[var(--title-color)]">{visibleTitle}</h3>
|
||||
<p class="max-w-3xl text-sm leading-7 text-[var(--text-secondary)]">{visibleDescription}</p>
|
||||
<h3 class:list={['font-semibold text-[var(--title-color)]', isCompact ? 'text-lg' : 'text-xl']}>
|
||||
{visibleTitle}
|
||||
</h3>
|
||||
<p class:list={['text-sm text-[var(--text-secondary)]', isCompact ? 'leading-6' : 'max-w-3xl leading-7']}>
|
||||
{visibleDescription}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{stats.length > 0 ? (
|
||||
<div class="grid gap-3 sm:grid-cols-2 lg:min-w-[16rem]">
|
||||
<div class:list={['grid gap-3 sm:grid-cols-2', !isCompact && 'lg:min-w-[16rem]']}>
|
||||
{stats.slice(0, 4).map((item) => (
|
||||
<div class="rounded-2xl border border-[var(--border-color)]/75 bg-[var(--terminal-bg)]/76 px-4 py-3">
|
||||
<div class="text-[11px] uppercase tracking-[0.18em] text-[var(--text-tertiary)]">{item.label}</div>
|
||||
@@ -166,9 +176,14 @@ if (wechatShareQrEnabled) {
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div class="mt-5 rounded-[24px] border border-[var(--border-color)]/70 bg-[var(--terminal-bg)]/86 p-5 shadow-[0_16px_40px_rgba(15,23,42,0.06)]">
|
||||
<div class:list={[
|
||||
'rounded-[24px] border border-[var(--border-color)]/70 bg-[var(--terminal-bg)]/86 shadow-[0_16px_40px_rgba(15,23,42,0.06)]',
|
||||
isCompact ? 'mt-4 p-4' : 'mt-5 p-5',
|
||||
]}>
|
||||
<div class="text-[11px] uppercase tracking-[0.18em] text-[var(--text-tertiary)]">{copy.summaryTitle}</div>
|
||||
<p class="mt-3 text-base leading-8 text-[var(--title-color)]">{safeSummary}</p>
|
||||
<p class:list={['mt-3 text-[var(--title-color)]', isCompact ? 'text-sm leading-7 line-clamp-4' : 'text-base leading-8']}>
|
||||
{safeSummary}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex flex-col gap-3 sm:flex-row sm:flex-wrap sm:items-center">
|
||||
@@ -217,7 +232,7 @@ if (wechatShareQrEnabled) {
|
||||
<p class="text-[11px] font-semibold uppercase tracking-[0.22em] text-[var(--text-tertiary)]">
|
||||
{isEnglish ? 'Share channels' : '分享渠道'}
|
||||
</p>
|
||||
<p class="text-xs leading-6 text-[var(--text-secondary)]">{visibleDescription}</p>
|
||||
{!isCompact && <p class="text-xs leading-6 text-[var(--text-secondary)]">{visibleDescription}</p>}
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<a
|
||||
|
||||
@@ -7,7 +7,6 @@ import TerminalWindow from '../components/ui/TerminalWindow.astro';
|
||||
import CommandPrompt from '../components/ui/CommandPrompt.astro';
|
||||
import FilterPill from '../components/ui/FilterPill.astro';
|
||||
import PostCard from '../components/PostCard.astro';
|
||||
import FriendLinkCard from '../components/FriendLinkCard.astro';
|
||||
import ViewMoreLink from '../components/ui/ViewMoreLink.astro';
|
||||
import StatsList from '../components/StatsList.astro';
|
||||
import TechStackList from '../components/TechStackList.astro';
|
||||
@@ -28,6 +27,7 @@ const selectedCategory = url.searchParams.get('category') || '';
|
||||
const normalizedSelectedTag = selectedTag.trim().toLowerCase();
|
||||
const normalizedSelectedCategory = selectedCategory.trim().toLowerCase();
|
||||
const previewLimit = 5;
|
||||
const popularPreviewLimit = 4;
|
||||
const DEFAULT_HOME_RANGE_KEY = '7d';
|
||||
|
||||
let siteSettings = DEFAULT_SITE_SETTINGS;
|
||||
@@ -166,6 +166,18 @@ const popularRangeOptions = contentRanges.map((range) => ({
|
||||
const initialPopularCount = popularRangeCards.filter(
|
||||
({ rangeKey, post }) => rangeKey === activeContentRange.key && matchesSelectedFilters(post),
|
||||
).length;
|
||||
const initialPopularVisibleKeys = new Set(
|
||||
popularRangeCards
|
||||
.filter(({ rangeKey, post }) => rangeKey === activeContentRange.key && matchesSelectedFilters(post))
|
||||
.sort((left, right) => {
|
||||
const scoreDiff = right.item.pageViews - left.item.pageViews;
|
||||
if (scoreDiff !== 0) return scoreDiff;
|
||||
return right.item.readCompletes - left.item.readCompletes;
|
||||
})
|
||||
.slice(0, popularPreviewLimit)
|
||||
.map(({ rangeKey, post }) => `${rangeKey}:${post.slug}`),
|
||||
);
|
||||
const sidebarFriendLinks = friendLinks.slice(0, 3);
|
||||
const buildHomeUrl = ({
|
||||
type = selectedType,
|
||||
tag = selectedTag,
|
||||
@@ -210,11 +222,6 @@ const postsPrompt = hasActiveFilters
|
||||
? t('home.promptPostsFiltered', { count: previewCount, filters: activeFilterLabels.join(' · ') })
|
||||
: t('home.promptPostsDefault', { count: previewCount });
|
||||
const popularPrompt = t('home.promptPopularRange', { label: activeContentRange.label });
|
||||
const popularSortOptions = [
|
||||
{ id: 'views', label: t('home.sortByViews'), icon: 'fa-eye' },
|
||||
{ id: 'completes', label: t('home.sortByCompletes'), icon: 'fa-check-double' },
|
||||
{ id: 'depth', label: t('home.sortByDepth'), icon: 'fa-chart-line' },
|
||||
];
|
||||
const navLinks = [
|
||||
{ icon: 'fa-file-code', text: t('nav.articles'), href: '/articles' },
|
||||
{ icon: 'fa-folder', text: t('nav.categories'), href: '/categories' },
|
||||
@@ -254,6 +261,29 @@ const homeShareCopy = isEnglish
|
||||
title: '分享首页',
|
||||
description: '把首页发给别人,能快速看到文章、分类、评测和个人介绍等主要内容。',
|
||||
};
|
||||
const homeSidebarCopy = isEnglish
|
||||
? {
|
||||
quickLinks: 'Quick links',
|
||||
quickLinksDesc: 'Jump to the main sections of the site.',
|
||||
popularTitle: 'Hot now',
|
||||
popularDesc: 'Track the most-read content in the selected window.',
|
||||
friendsTitle: 'Friend links',
|
||||
friendsDesc: 'A few recommended sites from the blogroll.',
|
||||
statsTitle: 'Site stats',
|
||||
statsDesc: 'A compact snapshot of current site scale.',
|
||||
aiBriefTitle: 'AI site brief',
|
||||
}
|
||||
: {
|
||||
quickLinks: '快速入口',
|
||||
quickLinksDesc: '把常用入口收进侧栏,首页阅读流更清爽。',
|
||||
popularTitle: '最近热门',
|
||||
popularDesc: '按当前时间窗口查看最受关注的内容。',
|
||||
friendsTitle: '友情链接',
|
||||
friendsDesc: '先看几个常访问的站点入口。',
|
||||
statsTitle: '站点概览',
|
||||
statsDesc: '快速看一下当前站点规模与内容状态。',
|
||||
aiBriefTitle: '站点摘要',
|
||||
};
|
||||
const homeBriefHighlights = buildDiscoveryHighlights([
|
||||
siteSettings.siteDescription,
|
||||
siteSettings.heroSubtitle,
|
||||
@@ -287,7 +317,7 @@ const homeFaqJsonLd = buildFaqJsonLd(homeFaqs);
|
||||
jsonLd={[...homeJsonLd, homeFaqJsonLd].filter(Boolean)}
|
||||
>
|
||||
<PageViewTracker pageType="home" entityId="homepage" />
|
||||
<div class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
|
||||
<div class="mx-auto max-w-[1480px] px-4 py-6 sm:px-6 lg:px-8">
|
||||
<TerminalWindow title={terminalConfig.title} class="w-full">
|
||||
<div class="mb-5 px-4 overflow-x-auto">
|
||||
<pre class="font-mono text-xs sm:text-sm text-[var(--primary)] whitespace-pre">{terminalConfig.asciiArt}</pre>
|
||||
@@ -321,39 +351,6 @@ const homeFaqJsonLd = buildFaqJsonLd(homeFaqs);
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ml-4 mt-4 home-nav-strip">
|
||||
{navLinks.map(link => (
|
||||
<a href={link.href} class="home-nav-pill">
|
||||
<i class={`fas ${link.icon} text-[11px]`}></i>
|
||||
<span class="min-w-0 truncate">{link.text}</span>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div class="ml-4 mt-4">
|
||||
<SharePanel
|
||||
shareTitle={siteSettings.siteTitle}
|
||||
summary={siteSettings.heroSubtitle || siteSettings.siteDescription}
|
||||
canonicalUrl={siteBaseUrl}
|
||||
badge={homeShareCopy.badge}
|
||||
kicker="geo / homepage"
|
||||
title={homeShareCopy.title}
|
||||
description={homeShareCopy.description}
|
||||
stats={systemStats.slice(0, 4)}
|
||||
wechatShareQrEnabled={siteSettings.seo.wechatShareQrEnabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="ml-4 mt-4">
|
||||
<DiscoveryBrief
|
||||
badge={isEnglish ? 'site brief' : '站点摘要'}
|
||||
kicker="geo / overview"
|
||||
title={isEnglish ? 'AI-readable site brief' : '给 AI 看的站点摘要'}
|
||||
summary={siteSettings.heroSubtitle || siteSettings.siteDescription}
|
||||
highlights={homeBriefHighlights}
|
||||
faqs={homeFaqs}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{apiError && (
|
||||
@@ -364,7 +361,9 @@ const homeFaqJsonLd = buildFaqJsonLd(homeFaqs);
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div id="discover" class="mb-6 px-4">
|
||||
<div class="grid gap-6 px-4 xl:grid-cols-[minmax(0,1fr)_320px]">
|
||||
<div class="min-w-0 space-y-6">
|
||||
<div id="discover">
|
||||
<CommandPrompt promptId="home-discover-prompt" command={discoverPrompt} />
|
||||
<div class="ml-4 terminal-panel home-discovery-shell">
|
||||
<div class="home-discovery-head">
|
||||
@@ -495,20 +494,20 @@ const homeFaqJsonLd = buildFaqJsonLd(homeFaqs);
|
||||
</div>
|
||||
|
||||
{pinnedPost && (
|
||||
<div class="mb-6 px-4">
|
||||
<div>
|
||||
<CommandPrompt command={t('home.promptPinned')} />
|
||||
<div id="home-pinned-wrap" class:list={['ml-4', !initialPinnedVisible && 'hidden']}>
|
||||
<div class="terminal-panel terminal-panel-accent terminal-interactive-card p-4" style={getAccentVars(getPostTypeTheme(pinnedPost.type))}>
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<span class="px-2 py-0.5 text-xs rounded bg-[var(--primary)] text-[var(--terminal-bg)] font-bold">{t('home.pinned')}</span>
|
||||
<span class="terminal-chip terminal-chip--accent text-[10px] py-1 px-2" style={getAccentVars(getPostTypeTheme(pinnedPost.type))}>
|
||||
<div class="mb-2 flex items-center gap-2">
|
||||
<span class="rounded bg-[var(--primary)] px-2 py-0.5 text-xs font-bold text-[var(--terminal-bg)]">{t('home.pinned')}</span>
|
||||
<span class="terminal-chip terminal-chip--accent px-2 py-1 text-[10px]" style={getAccentVars(getPostTypeTheme(pinnedPost.type))}>
|
||||
{pinnedPost.type === 'article' ? t('common.article') : t('common.tweet')}
|
||||
</span>
|
||||
<a href={`/articles/${pinnedPost.slug}`} class="text-lg font-bold text-[var(--title-color)] transition hover:text-[var(--primary)]">
|
||||
{pinnedPost.title}
|
||||
</a>
|
||||
</div>
|
||||
<p class="text-sm text-[var(--text-secondary)] mb-2">{pinnedPost.date} | {t('common.readTime')}: {formatReadTime(locale, pinnedPost.readTime, t)}</p>
|
||||
<p class="mb-2 text-sm text-[var(--text-secondary)]">{pinnedPost.date} | {t('common.readTime')}: {formatReadTime(locale, pinnedPost.readTime, t)}</p>
|
||||
<p class="text-[var(--text-secondary)]">{pinnedPost.description}</p>
|
||||
<div class="mt-4">
|
||||
<a href={`/articles/${pinnedPost.slug}`} class="terminal-action-button inline-flex">
|
||||
@@ -521,7 +520,7 @@ const homeFaqJsonLd = buildFaqJsonLd(homeFaqs);
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div id="posts" class="mb-10 px-4">
|
||||
<div id="posts">
|
||||
<CommandPrompt promptId="home-posts-prompt" command={postsPrompt} />
|
||||
<div class="ml-4">
|
||||
{allPosts.length > 0 ? (
|
||||
@@ -585,22 +584,49 @@ const homeFaqJsonLd = buildFaqJsonLd(homeFaqs);
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="popular" class="mb-10 px-4">
|
||||
<CommandPrompt promptId="home-popular-prompt" command={popularPrompt} />
|
||||
<div class="ml-4 grid gap-6 lg:grid-cols-[1.05fr_0.95fr]">
|
||||
<aside class="space-y-4 xl:sticky xl:top-24 xl:self-start">
|
||||
<section class="terminal-panel space-y-4">
|
||||
<div class="flex flex-wrap items-start justify-between gap-3">
|
||||
<div class="space-y-1">
|
||||
<h3 class="text-lg font-semibold text-[var(--title-color)]">{homeSidebarCopy.quickLinks}</h3>
|
||||
<p class="text-sm leading-6 text-[var(--text-secondary)]">{homeSidebarCopy.quickLinksDesc}</p>
|
||||
</div>
|
||||
<div class="grid gap-2 sm:grid-cols-2 xl:grid-cols-2">
|
||||
{navLinks.map(link => (
|
||||
<a
|
||||
href={link.href}
|
||||
class="group flex min-w-0 items-center gap-2 rounded-2xl border border-[var(--border-color)] bg-[var(--terminal-bg)]/75 px-3 py-3 text-sm text-[var(--title-color)] transition hover:-translate-y-0.5 hover:border-[var(--primary)] hover:text-[var(--primary)]"
|
||||
>
|
||||
<span class="flex h-8 w-8 shrink-0 items-center justify-center rounded-xl bg-[var(--primary)]/10 text-[var(--primary)]">
|
||||
<i class={`fas ${link.icon} text-[11px]`}></i>
|
||||
</span>
|
||||
<span class="min-w-0 truncate font-medium">{link.text}</span>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<SharePanel
|
||||
shareTitle={siteSettings.siteTitle}
|
||||
summary={siteSettings.heroSubtitle || siteSettings.siteDescription}
|
||||
canonicalUrl={siteBaseUrl}
|
||||
badge={homeShareCopy.badge}
|
||||
title={homeShareCopy.title}
|
||||
description={homeShareCopy.description}
|
||||
wechatShareQrEnabled={siteSettings.seo.wechatShareQrEnabled}
|
||||
variant="compact"
|
||||
/>
|
||||
|
||||
<section class="terminal-panel space-y-4">
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<h3 class="text-lg font-bold text-[var(--title-color)]">{t('home.hotNow')}</h3>
|
||||
<p class="mt-2 text-sm leading-6 text-[var(--text-secondary)]">
|
||||
{t('home.hotNowDescription')}
|
||||
</p>
|
||||
<h3 class="text-lg font-semibold text-[var(--title-color)]">{homeSidebarCopy.popularTitle}</h3>
|
||||
<p class="mt-1 text-sm leading-6 text-[var(--text-secondary)]">{homeSidebarCopy.popularDesc}</p>
|
||||
</div>
|
||||
<span id="home-popular-count" class="terminal-stat-pill">{initialPopularCount}</span>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<div class="home-popular-sortbar">
|
||||
{popularRangeOptions.map((option) => (
|
||||
<button
|
||||
@@ -617,26 +643,8 @@ const homeFaqJsonLd = buildFaqJsonLd(homeFaqs);
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div class="home-popular-sortbar">
|
||||
{popularSortOptions.map((option) => (
|
||||
<button
|
||||
type="button"
|
||||
data-home-popular-sort={option.id}
|
||||
class:list={[
|
||||
'home-popular-sort',
|
||||
option.id === 'views' && 'is-active'
|
||||
]}
|
||||
>
|
||||
<i class={`fas ${option.icon} text-[10px]`}></i>
|
||||
<span>{option.label}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="home-popular-list" class="space-y-3">
|
||||
{popularRangeCards.map(({ rangeKey, item, post }) => {
|
||||
return (
|
||||
{popularRangeCards.map(({ rangeKey, item, post }) => (
|
||||
<a
|
||||
href={`/articles/${post.slug}`}
|
||||
data-home-popular-card
|
||||
@@ -649,112 +657,135 @@ const homeFaqJsonLd = buildFaqJsonLd(homeFaqs);
|
||||
data-home-popular-completes={item.readCompletes}
|
||||
data-home-popular-depth={Math.round(item.avgProgressPercent)}
|
||||
class:list={[
|
||||
'block rounded-2xl border border-[var(--border-color)] bg-[var(--terminal-bg)]/70 p-4 transition hover:border-[var(--primary)] hover:-translate-y-0.5',
|
||||
rangeKey !== activeContentRange.key && 'hidden'
|
||||
'block rounded-2xl border border-[var(--border-color)] bg-[var(--terminal-bg)]/72 p-3 transition hover:-translate-y-0.5 hover:border-[var(--primary)]',
|
||||
!initialPopularVisibleKeys.has(`${rangeKey}:${post.slug}`) && 'hidden'
|
||||
]}
|
||||
style={getAccentVars(getPostTypeTheme(post.type))}
|
||||
>
|
||||
<div class="flex flex-wrap items-start justify-between gap-3">
|
||||
<div class="flex items-start gap-3">
|
||||
<span class="mt-0.5 inline-flex h-7 w-7 shrink-0 items-center justify-center rounded-full bg-[var(--primary)]/10 text-[11px] font-semibold text-[var(--primary)]">
|
||||
{item.pageViews}
|
||||
</span>
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<span class="terminal-chip terminal-chip--accent text-[10px] py-1 px-2" style={getAccentVars(getPostTypeTheme(post.type))}>
|
||||
<span class="terminal-chip terminal-chip--accent px-2 py-1 text-[10px]" style={getAccentVars(getPostTypeTheme(post.type))}>
|
||||
{post.type === 'article' ? t('common.article') : t('common.tweet')}
|
||||
</span>
|
||||
<span class="terminal-chip terminal-chip--accent text-[10px] py-1 px-2" style={getAccentVars(getCategoryTheme(post.category))}>
|
||||
<span class="terminal-chip terminal-chip--accent px-2 py-1 text-[10px]" style={getAccentVars(getCategoryTheme(post.category))}>
|
||||
{post.category}
|
||||
</span>
|
||||
</div>
|
||||
<h4 class="mt-3 text-base font-semibold text-[var(--title-color)]">{post.title}</h4>
|
||||
<p class="mt-2 line-clamp-2 text-sm leading-6 text-[var(--text-secondary)]">
|
||||
{post.description}
|
||||
</p>
|
||||
</div>
|
||||
<div class="space-y-2 text-right text-xs text-[var(--text-tertiary)]">
|
||||
<div>{post.date}</div>
|
||||
<div>{t('common.readTime')}: {formatReadTime(locale, post.readTime, t)}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex flex-wrap gap-2">
|
||||
<span class="terminal-stat-pill">
|
||||
<i class="fas fa-eye text-[10px] text-[var(--primary)]"></i>
|
||||
<h4 class="mt-2 line-clamp-2 text-sm font-semibold leading-6 text-[var(--title-color)]">{post.title}</h4>
|
||||
<div class="mt-2 flex flex-wrap gap-2 text-xs text-[var(--text-secondary)]">
|
||||
<span>{t('home.views')}: {item.pageViews}</span>
|
||||
</span>
|
||||
<span class="terminal-stat-pill">
|
||||
<i class="fas fa-check-double text-[10px] text-[var(--primary)]"></i>
|
||||
<span>{t('home.completes')}: {item.readCompletes}</span>
|
||||
</span>
|
||||
<span class="terminal-stat-pill">
|
||||
<i class="fas fa-chart-line text-[10px] text-[var(--primary)]"></i>
|
||||
<span>{t('home.avgProgress')}: {formatProgressPercent(item.avgProgressPercent)}</span>
|
||||
</span>
|
||||
<span class="terminal-stat-pill">
|
||||
<i class="fas fa-stopwatch text-[10px] text-[var(--primary)]"></i>
|
||||
<span>{t('home.avgDuration')}: {formatDurationMs(item.avgDurationMs)}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
);
|
||||
})}
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div id="home-popular-empty" class:list={['terminal-empty', initialPopularCount > 0 && 'hidden']}>
|
||||
<div id="home-popular-empty" class:list={['terminal-empty py-6', initialPopularCount > 0 && 'hidden']}>
|
||||
<p class="text-sm leading-6 text-[var(--text-secondary)]">{t('home.hotNowEmpty')}</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="terminal-panel space-y-4">
|
||||
<div class="flex flex-wrap items-start justify-between gap-3">
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<h3 class="text-lg font-bold text-[var(--title-color)]">{t('home.readingSignals')}</h3>
|
||||
<p class="mt-2 text-sm leading-6 text-[var(--text-secondary)]">
|
||||
{t('home.readingSignalsDescription')}
|
||||
</p>
|
||||
<h3 class="text-lg font-semibold text-[var(--title-color)]">{homeSidebarCopy.statsTitle}</h3>
|
||||
<p class="mt-1 text-sm leading-6 text-[var(--text-secondary)]">{homeSidebarCopy.statsDesc}</p>
|
||||
</div>
|
||||
<span id="home-stats-window-pill" class="terminal-stat-pill">{activeContentRange.label}</span>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-3 sm:grid-cols-2">
|
||||
<div class="grid gap-3 sm:grid-cols-2 xl:grid-cols-2">
|
||||
<div class="rounded-2xl border border-[var(--border-color)] bg-[var(--bg)]/70 p-4">
|
||||
<p class="text-xs uppercase tracking-[0.18em] text-[var(--text-tertiary)]">{t('home.views')}</p>
|
||||
<p id="home-reading-views-value" class="mt-3 text-3xl font-semibold text-[var(--title-color)]">{activeContentRange.overview.pageViews}</p>
|
||||
<p id="home-reading-views-value" class="mt-3 text-2xl font-semibold text-[var(--title-color)]">{activeContentRange.overview.pageViews}</p>
|
||||
<p class="mt-2 text-xs text-[var(--text-secondary)]">{t('home.totalViews')}: {contentOverview.totalPageViews}</p>
|
||||
</div>
|
||||
<div class="rounded-2xl border border-[var(--border-color)] bg-[var(--bg)]/70 p-4">
|
||||
<p class="text-xs uppercase tracking-[0.18em] text-[var(--text-tertiary)]">{t('home.completes')}</p>
|
||||
<p id="home-reading-completes-value" class="mt-3 text-3xl font-semibold text-[var(--title-color)]">{activeContentRange.overview.readCompletes}</p>
|
||||
<p id="home-reading-completes-value" class="mt-3 text-2xl font-semibold text-[var(--title-color)]">{activeContentRange.overview.readCompletes}</p>
|
||||
<p class="mt-2 text-xs text-[var(--text-secondary)]">{t('home.totalCompletes')}: {contentOverview.totalReadCompletes}</p>
|
||||
</div>
|
||||
<div class="rounded-2xl border border-[var(--border-color)] bg-[var(--bg)]/70 p-4">
|
||||
<p class="text-xs uppercase tracking-[0.18em] text-[var(--text-tertiary)]">{t('home.avgProgress')}</p>
|
||||
<p id="home-reading-progress-value" class="mt-3 text-3xl font-semibold text-[var(--title-color)]">{formatProgressPercent(activeContentRange.overview.avgReadProgress)}</p>
|
||||
<p id="home-reading-progress-value" class="mt-3 text-2xl font-semibold text-[var(--title-color)]">{formatProgressPercent(activeContentRange.overview.avgReadProgress)}</p>
|
||||
<p id="home-reading-window-meta" class="mt-2 text-xs text-[var(--text-secondary)]">{t('home.statsWindowLabel', { label: activeContentRange.label })}</p>
|
||||
</div>
|
||||
<div class="rounded-2xl border border-[var(--border-color)] bg-[var(--bg)]/70 p-4">
|
||||
<p class="text-xs uppercase tracking-[0.18em] text-[var(--text-tertiary)]">{t('home.avgDuration')}</p>
|
||||
<p id="home-reading-duration-value" class="mt-3 text-3xl font-semibold text-[var(--title-color)]">{formatDurationMs(activeContentRange.overview.avgReadDurationMs)}</p>
|
||||
<p class="mt-2 text-xs text-[var(--text-secondary)]">{t('home.totalViews')}: {contentOverview.totalPageViews}</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<p id="home-reading-duration-value" class="mt-3 text-2xl font-semibold text-[var(--title-color)]">{formatDurationMs(activeContentRange.overview.avgReadDurationMs)}</p>
|
||||
<p class="mt-2 text-xs text-[var(--text-secondary)]">{t('common.posts')}: {allPosts.length}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="border-t border-[var(--border-color)] my-8"></div>
|
||||
|
||||
<div id="friends" class="mb-8 px-4">
|
||||
<CommandPrompt command={t('home.promptFriends')} />
|
||||
<div class="ml-4 grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{friendLinks.map(friend => (
|
||||
<FriendLinkCard friend={friend} />
|
||||
<div class="grid gap-3 sm:grid-cols-2">
|
||||
{systemStats.slice(0, 4).map((item) => (
|
||||
<div class="rounded-2xl border border-[var(--border-color)]/75 bg-[var(--terminal-bg)]/78 px-4 py-3">
|
||||
<div class="text-[11px] uppercase tracking-[0.18em] text-[var(--text-tertiary)]">{item.label}</div>
|
||||
<div class="mt-2 text-lg font-semibold text-[var(--title-color)]">{item.value}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div class="mt-6 ml-4">
|
||||
<ViewMoreLink href="/friends" text={t('common.viewAllLinks')} command="cd ~/friends" />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{sidebarFriendLinks.length > 0 && (
|
||||
<section class="terminal-panel space-y-4">
|
||||
<div class="space-y-1">
|
||||
<h3 class="text-lg font-semibold text-[var(--title-color)]">{homeSidebarCopy.friendsTitle}</h3>
|
||||
<p class="text-sm leading-6 text-[var(--text-secondary)]">{homeSidebarCopy.friendsDesc}</p>
|
||||
</div>
|
||||
|
||||
<div class="border-t border-[var(--border-color)] my-8"></div>
|
||||
<div class="space-y-3">
|
||||
{sidebarFriendLinks.map((friend) => (
|
||||
<a
|
||||
href={friend.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="group flex items-start gap-3 rounded-2xl border border-[var(--border-color)] bg-[var(--terminal-bg)]/75 p-3 transition hover:-translate-y-0.5 hover:border-[var(--primary)]"
|
||||
>
|
||||
{friend.avatar ? (
|
||||
<img
|
||||
src={friend.avatar}
|
||||
alt={friend.name}
|
||||
class="h-10 w-10 shrink-0 rounded-xl border border-[var(--border-color)] object-cover bg-[var(--code-bg)]"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
) : (
|
||||
<span class="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl border border-[var(--border-color)] bg-[var(--code-bg)] text-sm font-bold text-[var(--primary)]">
|
||||
{friend.name.charAt(0).toUpperCase()}
|
||||
</span>
|
||||
)}
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="truncate text-sm font-semibold text-[var(--title-color)] group-hover:text-[var(--primary)]">
|
||||
{friend.name}
|
||||
</span>
|
||||
<i class="fas fa-arrow-up-right-from-square text-[10px] text-[var(--text-tertiary)] opacity-0 transition-opacity group-hover:opacity-100"></i>
|
||||
</div>
|
||||
{friend.description && (
|
||||
<p class="mt-1 line-clamp-2 text-xs leading-6 text-[var(--text-secondary)]">
|
||||
{friend.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<ViewMoreLink href="/friends" text={t('common.viewAllLinks')} command="cd ~/friends" />
|
||||
</section>
|
||||
)}
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
<div class="my-8 border-t border-[var(--border-color)]"></div>
|
||||
|
||||
<div id="about" class="px-4">
|
||||
<CommandPrompt command={t('home.promptAbout')} />
|
||||
@@ -775,6 +806,21 @@ const homeFaqJsonLd = buildFaqJsonLd(homeFaqs);
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="my-8 border-t border-[var(--border-color)]"></div>
|
||||
|
||||
<div class="px-4 pb-2">
|
||||
<div class="ml-4">
|
||||
<DiscoveryBrief
|
||||
badge={isEnglish ? 'site brief' : homeSidebarCopy.aiBriefTitle}
|
||||
kicker="geo / overview"
|
||||
title={isEnglish ? 'AI-readable site brief' : '给 AI 看的站点摘要'}
|
||||
summary={siteSettings.heroSubtitle || siteSettings.siteDescription}
|
||||
highlights={homeBriefHighlights}
|
||||
faqs={homeFaqs}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</TerminalWindow>
|
||||
</div>
|
||||
</BaseLayout>
|
||||
@@ -783,6 +829,7 @@ const homeFaqJsonLd = buildFaqJsonLd(homeFaqs);
|
||||
is:inline
|
||||
define:vars={{
|
||||
previewLimit,
|
||||
popularPreviewLimit,
|
||||
categoryAccentMap,
|
||||
tagAccentMap,
|
||||
contentRangesPayload: contentRanges.map((range) => ({
|
||||
@@ -1097,7 +1144,7 @@ const homeFaqJsonLd = buildFaqJsonLd(homeFaqs);
|
||||
});
|
||||
const sortedPopular = sortPopularCards(filteredPopular);
|
||||
popularCards.forEach((card) => card.classList.add('hidden'));
|
||||
sortedPopular.forEach((card) => {
|
||||
sortedPopular.slice(0, popularPreviewLimit).forEach((card) => {
|
||||
card.classList.remove('hidden');
|
||||
popularList?.appendChild(card);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user