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;
|
description?: string;
|
||||||
stats?: ShareStat[];
|
stats?: ShareStat[];
|
||||||
wechatShareQrEnabled?: boolean;
|
wechatShareQrEnabled?: boolean;
|
||||||
|
variant?: 'default' | 'compact';
|
||||||
}
|
}
|
||||||
|
|
||||||
const { locale, t } = getI18n(Astro);
|
const { locale, t } = getI18n(Astro);
|
||||||
@@ -33,7 +34,9 @@ const {
|
|||||||
: '复制链接、摘要或二维码,方便换设备继续看,也方便直接发给别人。',
|
: '复制链接、摘要或二维码,方便换设备继续看,也方便直接发给别人。',
|
||||||
stats = [],
|
stats = [],
|
||||||
wechatShareQrEnabled = false,
|
wechatShareQrEnabled = false,
|
||||||
|
variant = 'default',
|
||||||
} = Astro.props as Props;
|
} = Astro.props as Props;
|
||||||
|
const isCompact = variant === 'compact';
|
||||||
|
|
||||||
const visibleBadge = badge;
|
const visibleBadge = badge;
|
||||||
const visibleTitle = title;
|
const visibleTitle = title;
|
||||||
@@ -136,10 +139,13 @@ if (wechatShareQrEnabled) {
|
|||||||
---
|
---
|
||||||
|
|
||||||
<section
|
<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}
|
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="space-y-3">
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<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)]">
|
<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>
|
||||||
|
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<h3 class="text-xl font-semibold text-[var(--title-color)]">{visibleTitle}</h3>
|
<h3 class:list={['font-semibold text-[var(--title-color)]', isCompact ? 'text-lg' : 'text-xl']}>
|
||||||
<p class="max-w-3xl text-sm leading-7 text-[var(--text-secondary)]">{visibleDescription}</p>
|
{visibleTitle}
|
||||||
|
</h3>
|
||||||
|
<p class:list={['text-sm text-[var(--text-secondary)]', isCompact ? 'leading-6' : 'max-w-3xl leading-7']}>
|
||||||
|
{visibleDescription}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{stats.length > 0 ? (
|
{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) => (
|
{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="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>
|
<div class="text-[11px] uppercase tracking-[0.18em] text-[var(--text-tertiary)]">{item.label}</div>
|
||||||
@@ -166,9 +176,14 @@ if (wechatShareQrEnabled) {
|
|||||||
) : null}
|
) : null}
|
||||||
</div>
|
</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>
|
<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>
|
||||||
|
|
||||||
<div class="mt-4 flex flex-col gap-3 sm:flex-row sm:flex-wrap sm:items-center">
|
<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)]">
|
<p class="text-[11px] font-semibold uppercase tracking-[0.22em] text-[var(--text-tertiary)]">
|
||||||
{isEnglish ? 'Share channels' : '分享渠道'}
|
{isEnglish ? 'Share channels' : '分享渠道'}
|
||||||
</p>
|
</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>
|
||||||
<div class="flex flex-wrap gap-2">
|
<div class="flex flex-wrap gap-2">
|
||||||
<a
|
<a
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import TerminalWindow from '../components/ui/TerminalWindow.astro';
|
|||||||
import CommandPrompt from '../components/ui/CommandPrompt.astro';
|
import CommandPrompt from '../components/ui/CommandPrompt.astro';
|
||||||
import FilterPill from '../components/ui/FilterPill.astro';
|
import FilterPill from '../components/ui/FilterPill.astro';
|
||||||
import PostCard from '../components/PostCard.astro';
|
import PostCard from '../components/PostCard.astro';
|
||||||
import FriendLinkCard from '../components/FriendLinkCard.astro';
|
|
||||||
import ViewMoreLink from '../components/ui/ViewMoreLink.astro';
|
import ViewMoreLink from '../components/ui/ViewMoreLink.astro';
|
||||||
import StatsList from '../components/StatsList.astro';
|
import StatsList from '../components/StatsList.astro';
|
||||||
import TechStackList from '../components/TechStackList.astro';
|
import TechStackList from '../components/TechStackList.astro';
|
||||||
@@ -28,6 +27,7 @@ const selectedCategory = url.searchParams.get('category') || '';
|
|||||||
const normalizedSelectedTag = selectedTag.trim().toLowerCase();
|
const normalizedSelectedTag = selectedTag.trim().toLowerCase();
|
||||||
const normalizedSelectedCategory = selectedCategory.trim().toLowerCase();
|
const normalizedSelectedCategory = selectedCategory.trim().toLowerCase();
|
||||||
const previewLimit = 5;
|
const previewLimit = 5;
|
||||||
|
const popularPreviewLimit = 4;
|
||||||
const DEFAULT_HOME_RANGE_KEY = '7d';
|
const DEFAULT_HOME_RANGE_KEY = '7d';
|
||||||
|
|
||||||
let siteSettings = DEFAULT_SITE_SETTINGS;
|
let siteSettings = DEFAULT_SITE_SETTINGS;
|
||||||
@@ -166,6 +166,18 @@ const popularRangeOptions = contentRanges.map((range) => ({
|
|||||||
const initialPopularCount = popularRangeCards.filter(
|
const initialPopularCount = popularRangeCards.filter(
|
||||||
({ rangeKey, post }) => rangeKey === activeContentRange.key && matchesSelectedFilters(post),
|
({ rangeKey, post }) => rangeKey === activeContentRange.key && matchesSelectedFilters(post),
|
||||||
).length;
|
).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 = ({
|
const buildHomeUrl = ({
|
||||||
type = selectedType,
|
type = selectedType,
|
||||||
tag = selectedTag,
|
tag = selectedTag,
|
||||||
@@ -210,11 +222,6 @@ const postsPrompt = hasActiveFilters
|
|||||||
? t('home.promptPostsFiltered', { count: previewCount, filters: activeFilterLabels.join(' · ') })
|
? t('home.promptPostsFiltered', { count: previewCount, filters: activeFilterLabels.join(' · ') })
|
||||||
: t('home.promptPostsDefault', { count: previewCount });
|
: t('home.promptPostsDefault', { count: previewCount });
|
||||||
const popularPrompt = t('home.promptPopularRange', { label: activeContentRange.label });
|
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 = [
|
const navLinks = [
|
||||||
{ icon: 'fa-file-code', text: t('nav.articles'), href: '/articles' },
|
{ icon: 'fa-file-code', text: t('nav.articles'), href: '/articles' },
|
||||||
{ icon: 'fa-folder', text: t('nav.categories'), href: '/categories' },
|
{ icon: 'fa-folder', text: t('nav.categories'), href: '/categories' },
|
||||||
@@ -249,11 +256,34 @@ const homeShareCopy = isEnglish
|
|||||||
description:
|
description:
|
||||||
'Use the homepage as the canonical top-level entry for people and AI search to branch into articles, taxonomies, reviews, and profile context.',
|
'Use the homepage as the canonical top-level entry for people and AI search to branch into articles, taxonomies, reviews, and profile context.',
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
badge: '首页',
|
badge: '首页',
|
||||||
title: '分享首页',
|
title: '分享首页',
|
||||||
description: '把首页发给别人,能快速看到文章、分类、评测和个人介绍等主要内容。',
|
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([
|
const homeBriefHighlights = buildDiscoveryHighlights([
|
||||||
siteSettings.siteDescription,
|
siteSettings.siteDescription,
|
||||||
siteSettings.heroSubtitle,
|
siteSettings.heroSubtitle,
|
||||||
@@ -287,7 +317,7 @@ const homeFaqJsonLd = buildFaqJsonLd(homeFaqs);
|
|||||||
jsonLd={[...homeJsonLd, homeFaqJsonLd].filter(Boolean)}
|
jsonLd={[...homeJsonLd, homeFaqJsonLd].filter(Boolean)}
|
||||||
>
|
>
|
||||||
<PageViewTracker pageType="home" entityId="homepage" />
|
<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">
|
<TerminalWindow title={terminalConfig.title} class="w-full">
|
||||||
<div class="mb-5 px-4 overflow-x-auto">
|
<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>
|
<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>
|
</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>
|
</div>
|
||||||
|
|
||||||
{apiError && (
|
{apiError && (
|
||||||
@@ -364,397 +361,431 @@ const homeFaqJsonLd = buildFaqJsonLd(homeFaqs);
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div id="discover" class="mb-6 px-4">
|
<div class="grid gap-6 px-4 xl:grid-cols-[minmax(0,1fr)_320px]">
|
||||||
<CommandPrompt promptId="home-discover-prompt" command={discoverPrompt} />
|
<div class="min-w-0 space-y-6">
|
||||||
<div class="ml-4 terminal-panel home-discovery-shell">
|
<div id="discover">
|
||||||
<div class="home-discovery-head">
|
<CommandPrompt promptId="home-discover-prompt" command={discoverPrompt} />
|
||||||
<div class="home-taxonomy-copy">
|
<div class="ml-4 terminal-panel home-discovery-shell">
|
||||||
<span class="terminal-kicker">
|
<div class="home-discovery-head">
|
||||||
<i class="fas fa-sliders-h"></i>
|
<div class="home-taxonomy-copy">
|
||||||
<span>home filters</span>
|
<span class="terminal-kicker">
|
||||||
</span>
|
<i class="fas fa-sliders-h"></i>
|
||||||
<div>
|
<span>home filters</span>
|
||||||
<h2 class="text-xl font-bold text-[var(--title-color)]">{t('common.browsePosts')}</h2>
|
</span>
|
||||||
<p class="mt-2 text-sm leading-6 text-[var(--text-secondary)]">
|
<div>
|
||||||
{t('common.categoriesCount', { count: categories.length })} · {t('common.tagsCount', { count: tags.length })} · <span id="home-discovery-results">{t('common.resultsCount', { count: filteredPostsCount })}</span>
|
<h2 class="text-xl font-bold text-[var(--title-color)]">{t('common.browsePosts')}</h2>
|
||||||
</p>
|
<p class="mt-2 text-sm leading-6 text-[var(--text-secondary)]">
|
||||||
</div>
|
{t('common.categoriesCount', { count: categories.length })} · {t('common.tagsCount', { count: tags.length })} · <span id="home-discovery-results">{t('common.resultsCount', { count: filteredPostsCount })}</span>
|
||||||
</div>
|
</p>
|
||||||
|
|
||||||
<div class="home-discovery-actions">
|
|
||||||
<a id="home-clear-filters" href="/" class:list={['terminal-link-arrow', !hasActiveFilters && 'hidden']}>
|
|
||||||
<span>{t('common.clearFilters')}</span>
|
|
||||||
<i class="fas fa-rotate-left text-xs"></i>
|
|
||||||
</a>
|
|
||||||
<ViewMoreLink href="/articles" text={t('common.viewAllArticles')} command="cd ~/articles" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="home-filter-toolbar">
|
|
||||||
{postTypeFilters.map(filter => (
|
|
||||||
<FilterPill
|
|
||||||
tone={filter.id === 'all' ? 'neutral' : 'accent'}
|
|
||||||
active={selectedType === filter.id}
|
|
||||||
href={buildHomeUrl({ type: filter.id, category: selectedCategory, tag: selectedTag })}
|
|
||||||
data-home-type-filter={filter.id}
|
|
||||||
style={filter.id === 'all' ? undefined : getAccentVars(getPostTypeTheme(filter.id))}
|
|
||||||
>
|
|
||||||
<i class={`fas ${filter.icon}`}></i>
|
|
||||||
<span>{filter.name}</span>
|
|
||||||
</FilterPill>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="home-active-filters" class:list={['home-active-filter-row', !hasActiveFilters && 'hidden']}>
|
|
||||||
<span id="home-active-type" class:list={['terminal-chip max-w-full min-w-0', selectedType === 'all' && 'hidden']}>
|
|
||||||
<i id="home-active-type-icon" class={`fas ${postTypeFilters.find((item) => item.id === selectedType)?.icon || 'fa-stream'} text-[10px]`}></i>
|
|
||||||
<span id="home-active-type-text" class="min-w-0 truncate">{postTypeFilters.find((item) => item.id === selectedType)?.name || selectedType}</span>
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
id="home-active-category"
|
|
||||||
class:list={['terminal-chip terminal-chip--accent max-w-full min-w-0', !selectedCategory && 'hidden']}
|
|
||||||
style={selectedCategory ? getAccentVars(getCategoryTheme(selectedCategory)) : undefined}
|
|
||||||
>
|
|
||||||
<i class="fas fa-folder-open text-[10px]"></i>
|
|
||||||
<span id="home-active-category-text" class="min-w-0 truncate">{selectedCategory}</span>
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
id="home-active-tag"
|
|
||||||
class:list={['terminal-chip terminal-chip--accent max-w-full min-w-0', !selectedTag && 'hidden']}
|
|
||||||
style={selectedTag ? getAccentVars(getTagTheme(selectedTag)) : undefined}
|
|
||||||
>
|
|
||||||
<i class="fas fa-hashtag text-[10px]"></i>
|
|
||||||
<span id="home-active-tag-text" class="min-w-0 truncate">{selectedTag}</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="home-discovery-grid">
|
|
||||||
<section id="categories" class="home-discovery-panel">
|
|
||||||
<div class="home-discovery-panel__head">
|
|
||||||
<h3 class="text-sm font-semibold text-[var(--title-color)]">{t('nav.categories')}</h3>
|
|
||||||
<span class="terminal-stat-pill">{categories.length}</span>
|
|
||||||
</div>
|
|
||||||
<div class="home-category-grid home-category-grid--compact">
|
|
||||||
{categories.map(category => (
|
|
||||||
<a
|
|
||||||
href={buildHomeUrl({
|
|
||||||
category: normalizedSelectedCategory === category.name.trim().toLowerCase() ? '' : category.name,
|
|
||||||
tag: selectedTag,
|
|
||||||
})}
|
|
||||||
data-home-category-filter={category.name}
|
|
||||||
class:list={[
|
|
||||||
'home-category-card terminal-panel-accent terminal-interactive-card group',
|
|
||||||
normalizedSelectedCategory === category.name.trim().toLowerCase() && 'is-active'
|
|
||||||
]}
|
|
||||||
style={getAccentVars(getCategoryTheme(category.name))}
|
|
||||||
>
|
|
||||||
<div class="home-category-card__icon terminal-accent-icon">
|
|
||||||
<i class="fas fa-folder-open"></i>
|
|
||||||
</div>
|
|
||||||
<div class="min-w-0 flex flex-1 items-center justify-between gap-3">
|
|
||||||
<h3 class="truncate text-sm font-semibold text-[var(--title-color)] transition-colors group-hover:text-[var(--accent-color,var(--primary))]">
|
|
||||||
{category.name}
|
|
||||||
</h3>
|
|
||||||
<span class="terminal-stat-pill terminal-stat-pill--accent shrink-0" style={getAccentVars(getCategoryTheme(category.name))}>
|
|
||||||
<span>{category.count ?? 0}</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section id="tags" class="home-discovery-panel home-tag-shell">
|
|
||||||
<div class="home-discovery-panel__head">
|
|
||||||
<h3 class="text-sm font-semibold text-[var(--title-color)]">{t('nav.tags')}</h3>
|
|
||||||
<span class="terminal-stat-pill">{tags.length}</span>
|
|
||||||
</div>
|
|
||||||
<div class="home-tag-cloud">
|
|
||||||
{tagEntries.map((tag) => (
|
|
||||||
<a
|
|
||||||
href={buildHomeUrl({
|
|
||||||
tag: normalizedSelectedTag === tag.name.trim().toLowerCase() ? '' : tag.name,
|
|
||||||
category: selectedCategory,
|
|
||||||
})}
|
|
||||||
data-home-tag-filter={tag.name}
|
|
||||||
class:list={[
|
|
||||||
'home-tag-cloud__item',
|
|
||||||
normalizedSelectedTag === tag.name.trim().toLowerCase() && 'is-active'
|
|
||||||
]}
|
|
||||||
style={getTagCloudStyle(tag.name)}
|
|
||||||
>
|
|
||||||
<span class="home-tag-cloud__hash">#</span>
|
|
||||||
<span>{tag.name}</span>
|
|
||||||
<span class="home-tag-cloud__count">{tag.count}</span>
|
|
||||||
</a>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{pinnedPost && (
|
|
||||||
<div class="mb-6 px-4">
|
|
||||||
<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))}>
|
|
||||||
{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="text-[var(--text-secondary)]">{pinnedPost.description}</p>
|
|
||||||
<div class="mt-4">
|
|
||||||
<a href={`/articles/${pinnedPost.slug}`} class="terminal-action-button inline-flex">
|
|
||||||
<i class="fas fa-angle-right"></i>
|
|
||||||
<span>{t('common.readMore')}</span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div id="posts" class="mb-10 px-4">
|
|
||||||
<CommandPrompt promptId="home-posts-prompt" command={postsPrompt} />
|
|
||||||
<div class="ml-4">
|
|
||||||
{allPosts.length > 0 ? (
|
|
||||||
<div id="home-posts-list" class="divide-y divide-[var(--border-color)]">
|
|
||||||
{allPosts.map((post) => {
|
|
||||||
const matchesCurrentFilter = matchesSelectedFilters(post);
|
|
||||||
const filteredIndex = matchesCurrentFilter
|
|
||||||
? allPosts.filter(matchesSelectedFilters).findIndex((item) => item.slug === post.slug)
|
|
||||||
: -1;
|
|
||||||
const isVisible = matchesCurrentFilter && filteredIndex < previewLimit;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-home-post-card
|
|
||||||
data-home-type={post.type}
|
|
||||||
data-home-category={post.category?.trim().toLowerCase() || ''}
|
|
||||||
data-home-tags={post.tags.map((tag) => tag.trim().toLowerCase()).join('|')}
|
|
||||||
data-home-pinned={post.pinned ? 'true' : 'false'}
|
|
||||||
data-home-slug={post.slug}
|
|
||||||
class:list={[!isVisible && 'hidden']}
|
|
||||||
>
|
|
||||||
<PostCard post={post} selectedTag={selectedTag} tagHrefPrefix={homeTagHrefPrefix} />
|
|
||||||
</div>
|
</div>
|
||||||
);
|
</div>
|
||||||
})}
|
|
||||||
|
<div class="home-discovery-actions">
|
||||||
|
<a id="home-clear-filters" href="/" class:list={['terminal-link-arrow', !hasActiveFilters && 'hidden']}>
|
||||||
|
<span>{t('common.clearFilters')}</span>
|
||||||
|
<i class="fas fa-rotate-left text-xs"></i>
|
||||||
|
</a>
|
||||||
|
<ViewMoreLink href="/articles" text={t('common.viewAllArticles')} command="cd ~/articles" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="home-filter-toolbar">
|
||||||
|
{postTypeFilters.map(filter => (
|
||||||
|
<FilterPill
|
||||||
|
tone={filter.id === 'all' ? 'neutral' : 'accent'}
|
||||||
|
active={selectedType === filter.id}
|
||||||
|
href={buildHomeUrl({ type: filter.id, category: selectedCategory, tag: selectedTag })}
|
||||||
|
data-home-type-filter={filter.id}
|
||||||
|
style={filter.id === 'all' ? undefined : getAccentVars(getPostTypeTheme(filter.id))}
|
||||||
|
>
|
||||||
|
<i class={`fas ${filter.icon}`}></i>
|
||||||
|
<span>{filter.name}</span>
|
||||||
|
</FilterPill>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="home-active-filters" class:list={['home-active-filter-row', !hasActiveFilters && 'hidden']}>
|
||||||
|
<span id="home-active-type" class:list={['terminal-chip max-w-full min-w-0', selectedType === 'all' && 'hidden']}>
|
||||||
|
<i id="home-active-type-icon" class={`fas ${postTypeFilters.find((item) => item.id === selectedType)?.icon || 'fa-stream'} text-[10px]`}></i>
|
||||||
|
<span id="home-active-type-text" class="min-w-0 truncate">{postTypeFilters.find((item) => item.id === selectedType)?.name || selectedType}</span>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
id="home-active-category"
|
||||||
|
class:list={['terminal-chip terminal-chip--accent max-w-full min-w-0', !selectedCategory && 'hidden']}
|
||||||
|
style={selectedCategory ? getAccentVars(getCategoryTheme(selectedCategory)) : undefined}
|
||||||
|
>
|
||||||
|
<i class="fas fa-folder-open text-[10px]"></i>
|
||||||
|
<span id="home-active-category-text" class="min-w-0 truncate">{selectedCategory}</span>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
id="home-active-tag"
|
||||||
|
class:list={['terminal-chip terminal-chip--accent max-w-full min-w-0', !selectedTag && 'hidden']}
|
||||||
|
style={selectedTag ? getAccentVars(getTagTheme(selectedTag)) : undefined}
|
||||||
|
>
|
||||||
|
<i class="fas fa-hashtag text-[10px]"></i>
|
||||||
|
<span id="home-active-tag-text" class="min-w-0 truncate">{selectedTag}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="home-discovery-grid">
|
||||||
|
<section id="categories" class="home-discovery-panel">
|
||||||
|
<div class="home-discovery-panel__head">
|
||||||
|
<h3 class="text-sm font-semibold text-[var(--title-color)]">{t('nav.categories')}</h3>
|
||||||
|
<span class="terminal-stat-pill">{categories.length}</span>
|
||||||
|
</div>
|
||||||
|
<div class="home-category-grid home-category-grid--compact">
|
||||||
|
{categories.map(category => (
|
||||||
|
<a
|
||||||
|
href={buildHomeUrl({
|
||||||
|
category: normalizedSelectedCategory === category.name.trim().toLowerCase() ? '' : category.name,
|
||||||
|
tag: selectedTag,
|
||||||
|
})}
|
||||||
|
data-home-category-filter={category.name}
|
||||||
|
class:list={[
|
||||||
|
'home-category-card terminal-panel-accent terminal-interactive-card group',
|
||||||
|
normalizedSelectedCategory === category.name.trim().toLowerCase() && 'is-active'
|
||||||
|
]}
|
||||||
|
style={getAccentVars(getCategoryTheme(category.name))}
|
||||||
|
>
|
||||||
|
<div class="home-category-card__icon terminal-accent-icon">
|
||||||
|
<i class="fas fa-folder-open"></i>
|
||||||
|
</div>
|
||||||
|
<div class="min-w-0 flex flex-1 items-center justify-between gap-3">
|
||||||
|
<h3 class="truncate text-sm font-semibold text-[var(--title-color)] transition-colors group-hover:text-[var(--accent-color,var(--primary))]">
|
||||||
|
{category.name}
|
||||||
|
</h3>
|
||||||
|
<span class="terminal-stat-pill terminal-stat-pill--accent shrink-0" style={getAccentVars(getCategoryTheme(category.name))}>
|
||||||
|
<span>{category.count ?? 0}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="tags" class="home-discovery-panel home-tag-shell">
|
||||||
|
<div class="home-discovery-panel__head">
|
||||||
|
<h3 class="text-sm font-semibold text-[var(--title-color)]">{t('nav.tags')}</h3>
|
||||||
|
<span class="terminal-stat-pill">{tags.length}</span>
|
||||||
|
</div>
|
||||||
|
<div class="home-tag-cloud">
|
||||||
|
{tagEntries.map((tag) => (
|
||||||
|
<a
|
||||||
|
href={buildHomeUrl({
|
||||||
|
tag: normalizedSelectedTag === tag.name.trim().toLowerCase() ? '' : tag.name,
|
||||||
|
category: selectedCategory,
|
||||||
|
})}
|
||||||
|
data-home-tag-filter={tag.name}
|
||||||
|
class:list={[
|
||||||
|
'home-tag-cloud__item',
|
||||||
|
normalizedSelectedTag === tag.name.trim().toLowerCase() && 'is-active'
|
||||||
|
]}
|
||||||
|
style={getTagCloudStyle(tag.name)}
|
||||||
|
>
|
||||||
|
<span class="home-tag-cloud__hash">#</span>
|
||||||
|
<span>{tag.name}</span>
|
||||||
|
<span class="home-tag-cloud__count">{tag.count}</span>
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
</div>
|
||||||
<div id="home-posts-empty" class="terminal-empty">
|
|
||||||
<p class="text-base font-semibold text-[var(--title-color)]">{t('articlesPage.emptyTitle')}</p>
|
{pinnedPost && (
|
||||||
<p class="mt-2 text-sm leading-6 text-[var(--text-secondary)]">{t('articlesPage.emptyDescription')}</p>
|
<div>
|
||||||
<div class="mt-4 flex flex-wrap justify-center gap-2">
|
<CommandPrompt command={t('home.promptPinned')} />
|
||||||
<a href="/" class="terminal-subtle-link">
|
<div id="home-pinned-wrap" class:list={['ml-4', !initialPinnedVisible && 'hidden']}>
|
||||||
<i class="fas fa-rotate-left text-[11px]"></i>
|
<div class="terminal-panel terminal-panel-accent terminal-interactive-card p-4" style={getAccentVars(getPostTypeTheme(pinnedPost.type))}>
|
||||||
<span>{t('common.clearFilters')}</span>
|
<div class="mb-2 flex items-center gap-2">
|
||||||
</a>
|
<span class="rounded bg-[var(--primary)] px-2 py-0.5 text-xs font-bold text-[var(--terminal-bg)]">{t('home.pinned')}</span>
|
||||||
<a href="/articles" class="terminal-subtle-link">
|
<span class="terminal-chip terminal-chip--accent px-2 py-1 text-[10px]" style={getAccentVars(getPostTypeTheme(pinnedPost.type))}>
|
||||||
<i class="fas fa-file-code text-[11px]"></i>
|
{pinnedPost.type === 'article' ? t('common.article') : t('common.tweet')}
|
||||||
<span>{t('common.viewAllArticles')}</span>
|
</span>
|
||||||
</a>
|
<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="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">
|
||||||
|
<i class="fas fa-angle-right"></i>
|
||||||
|
<span>{t('common.readMore')}</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{allPosts.length > 0 && (
|
|
||||||
<div id="home-posts-empty" class:list={['terminal-empty', recentPosts.length > 0 && 'hidden']}>
|
<div id="posts">
|
||||||
<p class="text-base font-semibold text-[var(--title-color)]">{t('articlesPage.emptyTitle')}</p>
|
<CommandPrompt promptId="home-posts-prompt" command={postsPrompt} />
|
||||||
<p class="mt-2 text-sm leading-6 text-[var(--text-secondary)]">{t('articlesPage.emptyDescription')}</p>
|
<div class="ml-4">
|
||||||
<div class="mt-4 flex flex-wrap justify-center gap-2">
|
{allPosts.length > 0 ? (
|
||||||
<a href="/" class="terminal-subtle-link">
|
<div id="home-posts-list" class="divide-y divide-[var(--border-color)]">
|
||||||
<i class="fas fa-rotate-left text-[11px]"></i>
|
{allPosts.map((post) => {
|
||||||
<span>{t('common.clearFilters')}</span>
|
const matchesCurrentFilter = matchesSelectedFilters(post);
|
||||||
</a>
|
const filteredIndex = matchesCurrentFilter
|
||||||
<a href="/articles" class="terminal-subtle-link">
|
? allPosts.filter(matchesSelectedFilters).findIndex((item) => item.slug === post.slug)
|
||||||
<i class="fas fa-file-code text-[11px]"></i>
|
: -1;
|
||||||
<span>{t('common.viewAllArticles')}</span>
|
const isVisible = matchesCurrentFilter && filteredIndex < previewLimit;
|
||||||
</a>
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-home-post-card
|
||||||
|
data-home-type={post.type}
|
||||||
|
data-home-category={post.category?.trim().toLowerCase() || ''}
|
||||||
|
data-home-tags={post.tags.map((tag) => tag.trim().toLowerCase()).join('|')}
|
||||||
|
data-home-pinned={post.pinned ? 'true' : 'false'}
|
||||||
|
data-home-slug={post.slug}
|
||||||
|
class:list={[!isVisible && 'hidden']}
|
||||||
|
>
|
||||||
|
<PostCard post={post} selectedTag={selectedTag} tagHrefPrefix={homeTagHrefPrefix} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div id="home-posts-empty" class="terminal-empty">
|
||||||
|
<p class="text-base font-semibold text-[var(--title-color)]">{t('articlesPage.emptyTitle')}</p>
|
||||||
|
<p class="mt-2 text-sm leading-6 text-[var(--text-secondary)]">{t('articlesPage.emptyDescription')}</p>
|
||||||
|
<div class="mt-4 flex flex-wrap justify-center gap-2">
|
||||||
|
<a href="/" class="terminal-subtle-link">
|
||||||
|
<i class="fas fa-rotate-left text-[11px]"></i>
|
||||||
|
<span>{t('common.clearFilters')}</span>
|
||||||
|
</a>
|
||||||
|
<a href="/articles" class="terminal-subtle-link">
|
||||||
|
<i class="fas fa-file-code text-[11px]"></i>
|
||||||
|
<span>{t('common.viewAllArticles')}</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{allPosts.length > 0 && (
|
||||||
|
<div id="home-posts-empty" class:list={['terminal-empty', recentPosts.length > 0 && 'hidden']}>
|
||||||
|
<p class="text-base font-semibold text-[var(--title-color)]">{t('articlesPage.emptyTitle')}</p>
|
||||||
|
<p class="mt-2 text-sm leading-6 text-[var(--text-secondary)]">{t('articlesPage.emptyDescription')}</p>
|
||||||
|
<div class="mt-4 flex flex-wrap justify-center gap-2">
|
||||||
|
<a href="/" class="terminal-subtle-link">
|
||||||
|
<i class="fas fa-rotate-left text-[11px]"></i>
|
||||||
|
<span>{t('common.clearFilters')}</span>
|
||||||
|
</a>
|
||||||
|
<a href="/articles" class="terminal-subtle-link">
|
||||||
|
<i class="fas fa-file-code text-[11px]"></i>
|
||||||
|
<span>{t('common.viewAllArticles')}</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div class="mt-4">
|
||||||
|
<ViewMoreLink href="/articles" text={t('common.viewAllArticles')} command="cd ~/articles" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
<div class="mt-4">
|
|
||||||
<ViewMoreLink href="/articles" text={t('common.viewAllArticles')} command="cd ~/articles" />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="popular" class="mb-10 px-4">
|
<aside class="space-y-4 xl:sticky xl:top-24 xl:self-start">
|
||||||
<CommandPrompt promptId="home-popular-prompt" command={popularPrompt} />
|
|
||||||
<div class="ml-4 grid gap-6 lg:grid-cols-[1.05fr_0.95fr]">
|
|
||||||
<section class="terminal-panel space-y-4">
|
<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>
|
<div>
|
||||||
<h3 class="text-lg font-bold text-[var(--title-color)]">{t('home.hotNow')}</h3>
|
<h3 class="text-lg font-semibold text-[var(--title-color)]">{homeSidebarCopy.popularTitle}</h3>
|
||||||
<p class="mt-2 text-sm leading-6 text-[var(--text-secondary)]">
|
<p class="mt-1 text-sm leading-6 text-[var(--text-secondary)]">{homeSidebarCopy.popularDesc}</p>
|
||||||
{t('home.hotNowDescription')}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<span id="home-popular-count" class="terminal-stat-pill">{initialPopularCount}</span>
|
<span id="home-popular-count" class="terminal-stat-pill">{initialPopularCount}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
<div class="home-popular-sortbar">
|
||||||
<div class="home-popular-sortbar">
|
{popularRangeOptions.map((option) => (
|
||||||
{popularRangeOptions.map((option) => (
|
<button
|
||||||
<button
|
type="button"
|
||||||
type="button"
|
data-home-popular-range={option.key}
|
||||||
data-home-popular-range={option.key}
|
class:list={[
|
||||||
class:list={[
|
'home-popular-sort',
|
||||||
'home-popular-sort',
|
option.key === activeContentRange.key && 'is-active'
|
||||||
option.key === activeContentRange.key && 'is-active'
|
]}
|
||||||
]}
|
>
|
||||||
>
|
<i class="fas fa-clock text-[10px]"></i>
|
||||||
<i class="fas fa-clock text-[10px]"></i>
|
<span>{option.label}</span>
|
||||||
<span>{option.label}</span>
|
</button>
|
||||||
</button>
|
))}
|
||||||
))}
|
|
||||||
</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>
|
||||||
|
|
||||||
<div id="home-popular-list" class="space-y-3">
|
<div id="home-popular-list" class="space-y-3">
|
||||||
{popularRangeCards.map(({ rangeKey, item, post }) => {
|
{popularRangeCards.map(({ rangeKey, item, post }) => (
|
||||||
return (
|
<a
|
||||||
<a
|
href={`/articles/${post.slug}`}
|
||||||
href={`/articles/${post.slug}`}
|
data-home-popular-card
|
||||||
data-home-popular-card
|
data-home-range={rangeKey}
|
||||||
data-home-range={rangeKey}
|
data-home-type={post.type}
|
||||||
data-home-type={post.type}
|
data-home-category={post.category?.trim().toLowerCase() || ''}
|
||||||
data-home-category={post.category?.trim().toLowerCase() || ''}
|
data-home-tags={post.tags.map((tag) => tag.trim().toLowerCase()).join('|')}
|
||||||
data-home-tags={post.tags.map((tag) => tag.trim().toLowerCase()).join('|')}
|
data-home-slug={post.slug}
|
||||||
data-home-slug={post.slug}
|
data-home-popular-views={item.pageViews}
|
||||||
data-home-popular-views={item.pageViews}
|
data-home-popular-completes={item.readCompletes}
|
||||||
data-home-popular-completes={item.readCompletes}
|
data-home-popular-depth={Math.round(item.avgProgressPercent)}
|
||||||
data-home-popular-depth={Math.round(item.avgProgressPercent)}
|
class:list={[
|
||||||
class:list={[
|
'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)]',
|
||||||
'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',
|
!initialPopularVisibleKeys.has(`${rangeKey}:${post.slug}`) && 'hidden'
|
||||||
rangeKey !== activeContentRange.key && 'hidden'
|
]}
|
||||||
]}
|
style={getAccentVars(getPostTypeTheme(post.type))}
|
||||||
style={getAccentVars(getPostTypeTheme(post.type))}
|
>
|
||||||
>
|
<div class="flex items-start gap-3">
|
||||||
<div class="flex flex-wrap items-start justify-between 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)]">
|
||||||
<div class="min-w-0 flex-1">
|
{item.pageViews}
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
</span>
|
||||||
<span class="terminal-chip terminal-chip--accent text-[10px] py-1 px-2" style={getAccentVars(getPostTypeTheme(post.type))}>
|
<div class="min-w-0 flex-1">
|
||||||
{post.type === 'article' ? t('common.article') : t('common.tweet')}
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
</span>
|
<span class="terminal-chip terminal-chip--accent px-2 py-1 text-[10px]" style={getAccentVars(getPostTypeTheme(post.type))}>
|
||||||
<span class="terminal-chip terminal-chip--accent text-[10px] py-1 px-2" style={getAccentVars(getCategoryTheme(post.category))}>
|
{post.type === 'article' ? t('common.article') : t('common.tweet')}
|
||||||
{post.category}
|
</span>
|
||||||
</span>
|
<span class="terminal-chip terminal-chip--accent px-2 py-1 text-[10px]" style={getAccentVars(getCategoryTheme(post.category))}>
|
||||||
</div>
|
{post.category}
|
||||||
<h4 class="mt-3 text-base font-semibold text-[var(--title-color)]">{post.title}</h4>
|
</span>
|
||||||
<p class="mt-2 line-clamp-2 text-sm leading-6 text-[var(--text-secondary)]">
|
|
||||||
{post.description}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-2 text-right text-xs text-[var(--text-tertiary)]">
|
<h4 class="mt-2 line-clamp-2 text-sm font-semibold leading-6 text-[var(--title-color)]">{post.title}</h4>
|
||||||
<div>{post.date}</div>
|
<div class="mt-2 flex flex-wrap gap-2 text-xs text-[var(--text-secondary)]">
|
||||||
<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>
|
|
||||||
<span>{t('home.views')}: {item.pageViews}</span>
|
<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>{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>{t('home.avgProgress')}: {formatProgressPercent(item.avgProgressPercent)}</span>
|
||||||
</span>
|
</div>
|
||||||
<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>
|
||||||
</a>
|
</div>
|
||||||
);
|
</a>
|
||||||
})}
|
))}
|
||||||
</div>
|
</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>
|
<p class="text-sm leading-6 text-[var(--text-secondary)]">{t('home.hotNowEmpty')}</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="terminal-panel space-y-4">
|
<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>
|
<div>
|
||||||
<h3 class="text-lg font-bold text-[var(--title-color)]">{t('home.readingSignals')}</h3>
|
<h3 class="text-lg font-semibold text-[var(--title-color)]">{homeSidebarCopy.statsTitle}</h3>
|
||||||
<p class="mt-2 text-sm leading-6 text-[var(--text-secondary)]">
|
<p class="mt-1 text-sm leading-6 text-[var(--text-secondary)]">{homeSidebarCopy.statsDesc}</p>
|
||||||
{t('home.readingSignalsDescription')}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<span id="home-stats-window-pill" class="terminal-stat-pill">{activeContentRange.label}</span>
|
<span id="home-stats-window-pill" class="terminal-stat-pill">{activeContentRange.label}</span>
|
||||||
</div>
|
</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">
|
<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 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>
|
<p class="mt-2 text-xs text-[var(--text-secondary)]">{t('home.totalViews')}: {contentOverview.totalPageViews}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="rounded-2xl border border-[var(--border-color)] bg-[var(--bg)]/70 p-4">
|
<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 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>
|
<p class="mt-2 text-xs text-[var(--text-secondary)]">{t('home.totalCompletes')}: {contentOverview.totalReadCompletes}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="rounded-2xl border border-[var(--border-color)] bg-[var(--bg)]/70 p-4">
|
<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 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>
|
<p id="home-reading-window-meta" class="mt-2 text-xs text-[var(--text-secondary)]">{t('home.statsWindowLabel', { label: activeContentRange.label })}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="rounded-2xl border border-[var(--border-color)] bg-[var(--bg)]/70 p-4">
|
<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 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 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('home.totalViews')}: {contentOverview.totalPageViews}</p>
|
<p class="mt-2 text-xs text-[var(--text-secondary)]">{t('common.posts')}: {allPosts.length}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<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>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
|
||||||
|
{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="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>
|
||||||
|
|
||||||
<div class="border-t border-[var(--border-color)] my-8"></div>
|
<div class="my-8 border-t border-[var(--border-color)]"></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>
|
|
||||||
<div class="mt-6 ml-4">
|
|
||||||
<ViewMoreLink href="/friends" text={t('common.viewAllLinks')} command="cd ~/friends" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="border-t border-[var(--border-color)] my-8"></div>
|
|
||||||
|
|
||||||
<div id="about" class="px-4">
|
<div id="about" class="px-4">
|
||||||
<CommandPrompt command={t('home.promptAbout')} />
|
<CommandPrompt command={t('home.promptAbout')} />
|
||||||
@@ -775,6 +806,21 @@ const homeFaqJsonLd = buildFaqJsonLd(homeFaqs);
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
</TerminalWindow>
|
||||||
</div>
|
</div>
|
||||||
</BaseLayout>
|
</BaseLayout>
|
||||||
@@ -783,6 +829,7 @@ const homeFaqJsonLd = buildFaqJsonLd(homeFaqs);
|
|||||||
is:inline
|
is:inline
|
||||||
define:vars={{
|
define:vars={{
|
||||||
previewLimit,
|
previewLimit,
|
||||||
|
popularPreviewLimit,
|
||||||
categoryAccentMap,
|
categoryAccentMap,
|
||||||
tagAccentMap,
|
tagAccentMap,
|
||||||
contentRangesPayload: contentRanges.map((range) => ({
|
contentRangesPayload: contentRanges.map((range) => ({
|
||||||
@@ -1097,7 +1144,7 @@ const homeFaqJsonLd = buildFaqJsonLd(homeFaqs);
|
|||||||
});
|
});
|
||||||
const sortedPopular = sortPopularCards(filteredPopular);
|
const sortedPopular = sortPopularCards(filteredPopular);
|
||||||
popularCards.forEach((card) => card.classList.add('hidden'));
|
popularCards.forEach((card) => card.classList.add('hidden'));
|
||||||
sortedPopular.forEach((card) => {
|
sortedPopular.slice(0, popularPreviewLimit).forEach((card) => {
|
||||||
card.classList.remove('hidden');
|
card.classList.remove('hidden');
|
||||||
popularList?.appendChild(card);
|
popularList?.appendChild(card);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user