refactor: read page data from generated content
This commit is contained in:
parent
d85c8ac85d
commit
7915f8c244
|
|
@ -1,39 +1,35 @@
|
||||||
---
|
---
|
||||||
const heatmapLevels = [
|
import type { GiteaActivityData } from '../data/loaders';
|
||||||
'#0f172a',
|
import { site } from '../config';
|
||||||
'#13233e',
|
|
||||||
'#1d4ed8',
|
|
||||||
'#2563eb',
|
|
||||||
'#22c55e',
|
|
||||||
];
|
|
||||||
|
|
||||||
const heatmap = [
|
interface Props {
|
||||||
0, 1, 2, 0, 3, 1, 4, 2, 0, 1,
|
activity: GiteaActivityData;
|
||||||
1, 3, 0, 2, 1, 4, 2, 1, 0, 3,
|
}
|
||||||
2, 0, 1, 3, 4, 1, 0, 2, 1, 3,
|
|
||||||
0, 1, 4, 2, 0, 3, 1, 2, 4, 1,
|
|
||||||
1, 2, 0, 3, 1, 4, 2, 0, 1, 2,
|
|
||||||
0, 4, 1, 2, 3, 0, 2, 1, 4, 1,
|
|
||||||
2, 1, 3, 0, 2, 1, 4, 2, 0, 1,
|
|
||||||
];
|
|
||||||
|
|
||||||
const recentActivities = [
|
const { activity } = Astro.props;
|
||||||
{
|
|
||||||
title: 'homepage landing',
|
const heatmapLevels = ['#0f172a', '#13233e', '#1d4ed8', '#2563eb', '#22c55e'];
|
||||||
detail: '重建首页骨架与模块层级',
|
const heatmapSize = 70;
|
||||||
time: '今天',
|
const heatmapCounts = activity.days.slice(-heatmapSize).map((day) => day.count);
|
||||||
},
|
const maxCount = Math.max(...heatmapCounts, 0);
|
||||||
{
|
const heatmap = Array.from({ length: heatmapSize }, (_, index) => {
|
||||||
title: 'design spec',
|
const count = heatmapCounts[index] ?? 0;
|
||||||
detail: '完成工程编辑部风格设计文档',
|
|
||||||
time: '今天',
|
if (count <= 0 || maxCount <= 0) {
|
||||||
},
|
return 0;
|
||||||
{
|
}
|
||||||
title: 'content seeds',
|
|
||||||
detail: '补齐示例日志、项目与分享数据',
|
return Math.min(
|
||||||
time: '今天',
|
heatmapLevels.length - 1,
|
||||||
},
|
Math.max(1, Math.ceil((count / maxCount) * (heatmapLevels.length - 1))),
|
||||||
];
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const recentActivities = activity.recent.slice(0, 3);
|
||||||
|
const updatedLabel = activity.updatedAt
|
||||||
|
? new Date(activity.updatedAt).toLocaleString('zh-CN', { hour12: false })
|
||||||
|
: '尚未同步';
|
||||||
|
const isPlaceholder = activity.source === 'placeholder';
|
||||||
---
|
---
|
||||||
|
|
||||||
<section class="panel activity-panel">
|
<section class="panel activity-panel">
|
||||||
|
|
@ -41,17 +37,21 @@ const recentActivities = [
|
||||||
<div>
|
<div>
|
||||||
<span class="eyebrow">Gitea Activity</span>
|
<span class="eyebrow">Gitea Activity</span>
|
||||||
<h2>活跃度概览</h2>
|
<h2>活跃度概览</h2>
|
||||||
<p>首版先用静态面板承载未来的热力图与活动流位置,后续再接真实 API。</p>
|
<p>
|
||||||
|
{isPlaceholder
|
||||||
|
? '当前展示的是同步占位结果;接入 Gitea API 后,这里会显示真实热力图与最近活动。'
|
||||||
|
: '这里展示构建阶段同步下来的 Gitea 热力图与最近活动摘要。'}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<a class="section-link" href="https://gitea.sepcomet.xyz" target="_blank" rel="noreferrer">
|
<a class="section-link" href={site.gitea.baseUrl} target="_blank" rel="noreferrer">
|
||||||
打开 Gitea
|
打开 Gitea
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="activity-grid">
|
<div class="activity-grid">
|
||||||
<div>
|
<div>
|
||||||
<p class="activity-copy">贡献热力图区域预留给未来的客户端渲染模块。当前先用静态密度块确认布局节奏与深色模块层级。</p>
|
<p class="activity-copy">最近同步:{updatedLabel}</p>
|
||||||
<div class="heatmap" aria-label="示例贡献热力图">
|
<div class="heatmap" aria-label="Gitea 贡献热力图">
|
||||||
{
|
{
|
||||||
heatmap.map((level) => (
|
heatmap.map((level) => (
|
||||||
<span
|
<span
|
||||||
|
|
@ -73,15 +73,22 @@ const recentActivities = [
|
||||||
|
|
||||||
<div class="activity-list">
|
<div class="activity-list">
|
||||||
{
|
{
|
||||||
|
recentActivities.length > 0 ? (
|
||||||
recentActivities.map((item) => (
|
recentActivities.map((item) => (
|
||||||
<article class="activity-item">
|
<article class="activity-item">
|
||||||
<strong class="mono">{item.title}</strong>
|
<strong class="mono">{item.repo}</strong>
|
||||||
<p class="activity-copy">{item.detail}</p>
|
<p class="activity-copy">{item.message}</p>
|
||||||
<p class="activity-meta" style="margin-top: 0.55rem;">
|
<p class="activity-meta" style="margin-top: 0.55rem;">
|
||||||
<span>{item.time}</span>
|
<span>{item.time}</span>
|
||||||
</p>
|
</p>
|
||||||
</article>
|
</article>
|
||||||
))
|
))
|
||||||
|
) : (
|
||||||
|
<article class="activity-item">
|
||||||
|
<strong class="mono">暂无同步活动</strong>
|
||||||
|
<p class="activity-copy">先运行一次 content:sync,或等待后续接入真实 Gitea 数据源。</p>
|
||||||
|
</article>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ interface Props {
|
||||||
coverImage: string;
|
coverImage: string;
|
||||||
tags: string[];
|
tags: string[];
|
||||||
repo: string;
|
repo: string;
|
||||||
|
repoUrl?: string;
|
||||||
featured?: boolean;
|
featured?: boolean;
|
||||||
demoVideo?: string;
|
demoVideo?: string;
|
||||||
downloadLink?: string;
|
downloadLink?: string;
|
||||||
|
|
@ -16,10 +17,14 @@ const {
|
||||||
coverImage,
|
coverImage,
|
||||||
tags,
|
tags,
|
||||||
repo,
|
repo,
|
||||||
|
repoUrl,
|
||||||
featured = false,
|
featured = false,
|
||||||
demoVideo = '',
|
demoVideo = '',
|
||||||
downloadLink = '',
|
downloadLink = '',
|
||||||
} = Astro.props;
|
} = Astro.props;
|
||||||
|
|
||||||
|
const fallbackRepoUrl = repo ? `https://gitea.sepcomet.xyz/${repo}` : '';
|
||||||
|
const resolvedRepoUrl = repoUrl || fallbackRepoUrl;
|
||||||
---
|
---
|
||||||
|
|
||||||
<article class="card">
|
<article class="card">
|
||||||
|
|
@ -40,9 +45,11 @@ const {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="project-links">
|
<div class="project-links">
|
||||||
<a class="section-link" href={`https://gitea.sepcomet.xyz/${repo}`} target="_blank" rel="noreferrer">
|
{resolvedRepoUrl ? (
|
||||||
|
<a class="section-link" href={resolvedRepoUrl} target="_blank" rel="noreferrer">
|
||||||
仓库
|
仓库
|
||||||
</a>
|
</a>
|
||||||
|
) : null}
|
||||||
{demoVideo ? (
|
{demoVideo ? (
|
||||||
<a class="section-link" href={demoVideo} target="_blank" rel="noreferrer">
|
<a class="section-link" href={demoVideo} target="_blank" rel="noreferrer">
|
||||||
Demo
|
Demo
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,116 @@
|
||||||
|
import { readFile } from 'node:fs/promises';
|
||||||
|
import path from 'node:path';
|
||||||
|
import process from 'node:process';
|
||||||
|
|
||||||
|
type ProjectSeed = {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
gitea_repo: string;
|
||||||
|
cover_image: string;
|
||||||
|
demo_video?: string;
|
||||||
|
download_link?: string;
|
||||||
|
tags: string[];
|
||||||
|
featured: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ProjectData = ProjectSeed & {
|
||||||
|
repo_url?: string;
|
||||||
|
updated_at?: string;
|
||||||
|
source?: 'seed' | 'gitea' | 'gitea+seafile';
|
||||||
|
};
|
||||||
|
|
||||||
|
type ShareSeed = {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
url: string;
|
||||||
|
time: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ShareData = ShareSeed & {
|
||||||
|
size?: number;
|
||||||
|
source?: 'seed' | 'seafile';
|
||||||
|
updated_at?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ActivityDay = {
|
||||||
|
date: string;
|
||||||
|
count: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RecentActivity = {
|
||||||
|
type: string;
|
||||||
|
repo: string;
|
||||||
|
message: string;
|
||||||
|
url: string;
|
||||||
|
time: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GiteaActivityData = {
|
||||||
|
updatedAt: string;
|
||||||
|
source: 'placeholder' | 'gitea';
|
||||||
|
itemCount: number;
|
||||||
|
days: ActivityDay[];
|
||||||
|
recent: RecentActivity[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const rootDir = process.cwd();
|
||||||
|
const generatedProjectsPath = path.join(rootDir, 'src/data/generated/projects.json');
|
||||||
|
const generatedSharesPath = path.join(rootDir, 'src/data/generated/shares.json');
|
||||||
|
const generatedActivityPath = path.join(rootDir, 'src/data/generated/gitea-activity.json');
|
||||||
|
|
||||||
|
const seedProjectsPath = path.join(rootDir, 'src/content/projects/index.json');
|
||||||
|
const seedSharesPath = path.join(rootDir, 'src/content/shares/index.json');
|
||||||
|
|
||||||
|
const emptyActivity: GiteaActivityData = {
|
||||||
|
updatedAt: '',
|
||||||
|
source: 'placeholder',
|
||||||
|
itemCount: 0,
|
||||||
|
days: [],
|
||||||
|
recent: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function loadProjects(): Promise<ProjectData[]> {
|
||||||
|
return readJsonWithFallback<ProjectData[]>(generatedProjectsPath, seedProjectsPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loadShares(): Promise<ShareData[]> {
|
||||||
|
return readJsonWithFallback<ShareData[]>(generatedSharesPath, seedSharesPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loadGiteaActivity(): Promise<GiteaActivityData> {
|
||||||
|
try {
|
||||||
|
return await readJson<GiteaActivityData>(generatedActivityPath);
|
||||||
|
} catch (error) {
|
||||||
|
if (!isMissingFileError(error)) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return emptyActivity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readJsonWithFallback<T>(preferredPath: string, fallbackPath: string): Promise<T> {
|
||||||
|
try {
|
||||||
|
return await readJson<T>(preferredPath);
|
||||||
|
} catch (error) {
|
||||||
|
if (!isMissingFileError(error)) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return readJson<T>(fallbackPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readJson<T>(filePath: string): Promise<T> {
|
||||||
|
const raw = await readFile(filePath, 'utf-8');
|
||||||
|
return JSON.parse(raw) as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isMissingFileError(error: unknown) {
|
||||||
|
return (
|
||||||
|
typeof error === 'object' &&
|
||||||
|
error !== null &&
|
||||||
|
'code' in error &&
|
||||||
|
error.code === 'ENOENT'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -5,17 +5,21 @@ import LogCard from '../components/LogCard.astro';
|
||||||
import ProjectCard from '../components/ProjectCard.astro';
|
import ProjectCard from '../components/ProjectCard.astro';
|
||||||
import RandomCornerImage from '../components/RandomCornerImage.astro';
|
import RandomCornerImage from '../components/RandomCornerImage.astro';
|
||||||
import SectionTitle from '../components/SectionTitle.astro';
|
import SectionTitle from '../components/SectionTitle.astro';
|
||||||
import Layout from '../layouts/Layout.astro';
|
|
||||||
import { site } from '../config';
|
import { site } from '../config';
|
||||||
import projects from '../content/projects/index.json';
|
import { loadGiteaActivity, loadProjects } from '../data/loaders';
|
||||||
|
import Layout from '../layouts/Layout.astro';
|
||||||
|
|
||||||
const logs = (await getCollection('logs')).sort(
|
const [logs, projects, activity] = await Promise.all([
|
||||||
(a, b) => b.data.date.getTime() - a.data.date.getTime(),
|
getCollection('logs'),
|
||||||
);
|
loadProjects(),
|
||||||
|
loadGiteaActivity(),
|
||||||
|
]);
|
||||||
|
|
||||||
const latestLogs = logs.slice(0, 3);
|
const sortedLogs = logs.sort((a, b) => b.data.date.getTime() - a.data.date.getTime());
|
||||||
|
const latestLogs = sortedLogs.slice(0, 3);
|
||||||
const featuredProjects = projects.filter((project) => project.featured).slice(0, 3);
|
const featuredProjects = projects.filter((project) => project.featured).slice(0, 3);
|
||||||
const latestLog = latestLogs[0];
|
const latestLog = latestLogs[0];
|
||||||
|
const activeRepo = activity.recent[0]?.repo ?? featuredProjects[0]?.gitea_repo ?? 'sepcomet/personal-homepage';
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout title={`${site.name} | ${site.title}`} description={site.description} currentPath="/">
|
<Layout title={`${site.name} | ${site.title}`} description={site.description} currentPath="/">
|
||||||
|
|
@ -56,7 +60,7 @@ const latestLog = latestLogs[0];
|
||||||
</section>
|
</section>
|
||||||
<section class="status-item">
|
<section class="status-item">
|
||||||
<p class="status-item__label">Active repo</p>
|
<p class="status-item__label">Active repo</p>
|
||||||
<p class="status-item__value status-item__mono">sepcomet/personal-homepage</p>
|
<p class="status-item__value status-item__mono">{activeRepo}</p>
|
||||||
</section>
|
</section>
|
||||||
<section class="status-item">
|
<section class="status-item">
|
||||||
<p class="status-item__label">Stack</p>
|
<p class="status-item__label">Stack</p>
|
||||||
|
|
@ -106,6 +110,7 @@ const latestLog = latestLogs[0];
|
||||||
coverImage={project.cover_image}
|
coverImage={project.cover_image}
|
||||||
tags={project.tags}
|
tags={project.tags}
|
||||||
repo={project.gitea_repo}
|
repo={project.gitea_repo}
|
||||||
|
repoUrl={project.repo_url}
|
||||||
featured={project.featured}
|
featured={project.featured}
|
||||||
demoVideo={project.demo_video}
|
demoVideo={project.demo_video}
|
||||||
downloadLink={project.download_link}
|
downloadLink={project.download_link}
|
||||||
|
|
@ -118,7 +123,7 @@ const latestLog = latestLogs[0];
|
||||||
|
|
||||||
<section class="section">
|
<section class="section">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<GiteaActivity />
|
<GiteaActivity activity={activity} />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
---
|
---
|
||||||
import Layout from '../../layouts/Layout.astro';
|
|
||||||
import ProjectCard from '../../components/ProjectCard.astro';
|
import ProjectCard from '../../components/ProjectCard.astro';
|
||||||
import { site } from '../../config';
|
import { site } from '../../config';
|
||||||
import projects from '../../content/projects/index.json';
|
import { loadProjects } from '../../data/loaders';
|
||||||
|
import Layout from '../../layouts/Layout.astro';
|
||||||
|
|
||||||
|
const projects = await loadProjects();
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout
|
<Layout
|
||||||
|
|
@ -15,7 +17,7 @@ import projects from '../../content/projects/index.json';
|
||||||
<div class="page-intro">
|
<div class="page-intro">
|
||||||
<span class="eyebrow">Projects</span>
|
<span class="eyebrow">Projects</span>
|
||||||
<h1>项目展示</h1>
|
<h1>项目展示</h1>
|
||||||
<p>项目数据当前来自本地 JSON 文件,首页与此页面共享同一份结构化数据来源。</p>
|
<p>项目页现在优先读取构建阶段生成的数据;如果尚未同步,则回退到仓库里的种子数据。</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-grid">
|
<div class="card-grid">
|
||||||
|
|
@ -27,6 +29,7 @@ import projects from '../../content/projects/index.json';
|
||||||
coverImage={project.cover_image}
|
coverImage={project.cover_image}
|
||||||
tags={project.tags}
|
tags={project.tags}
|
||||||
repo={project.gitea_repo}
|
repo={project.gitea_repo}
|
||||||
|
repoUrl={project.repo_url}
|
||||||
featured={project.featured}
|
featured={project.featured}
|
||||||
demoVideo={project.demo_video}
|
demoVideo={project.demo_video}
|
||||||
downloadLink={project.download_link}
|
downloadLink={project.download_link}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
---
|
---
|
||||||
import Layout from '../layouts/Layout.astro';
|
|
||||||
import { site } from '../config';
|
import { site } from '../config';
|
||||||
import shares from '../content/shares/index.json';
|
import { loadShares } from '../data/loaders';
|
||||||
|
import Layout from '../layouts/Layout.astro';
|
||||||
|
|
||||||
|
const shares = await loadShares();
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout
|
<Layout
|
||||||
|
|
@ -14,7 +16,7 @@ import shares from '../content/shares/index.json';
|
||||||
<div class="page-intro">
|
<div class="page-intro">
|
||||||
<span class="eyebrow">Shares</span>
|
<span class="eyebrow">Shares</span>
|
||||||
<h1>分享链接</h1>
|
<h1>分享链接</h1>
|
||||||
<p>这里预留给 Seafile 或其他公开资源的索引入口。当前先用静态数据确认列表版式。</p>
|
<p>这里优先展示构建时从 Seafile 同步生成的分享数据;当前未接入时会先回退到静态种子数据。</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-grid" style="grid-template-columns: 1fr;">
|
<div class="card-grid" style="grid-template-columns: 1fr;">
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue