chore: reorganize project into monorepo
This commit is contained in:
165
frontend/src/pages/articles/[slug].astro
Normal file
165
frontend/src/pages/articles/[slug].astro
Normal file
@@ -0,0 +1,165 @@
|
||||
---
|
||||
import { createMarkdownProcessor } from '@astrojs/markdown-remark';
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import TerminalWindow from '../../components/ui/TerminalWindow.astro';
|
||||
import CommandPrompt from '../../components/ui/CommandPrompt.astro';
|
||||
import TableOfContents from '../../components/TableOfContents.astro';
|
||||
import RelatedPosts from '../../components/RelatedPosts.astro';
|
||||
import ReadingProgress from '../../components/ReadingProgress.astro';
|
||||
import BackToTop from '../../components/BackToTop.astro';
|
||||
import Lightbox from '../../components/Lightbox.astro';
|
||||
import CodeCopyButton from '../../components/CodeCopyButton.astro';
|
||||
import Comments from '../../components/Comments.astro';
|
||||
import { apiClient } from '../../lib/api/client';
|
||||
import { resolveFileRef, getPostTypeColor } from '../../lib/utils';
|
||||
|
||||
export const prerender = false;
|
||||
|
||||
const { slug } = Astro.params;
|
||||
|
||||
let post = null;
|
||||
|
||||
try {
|
||||
post = await apiClient.getPostBySlug(slug ?? '');
|
||||
} catch (error) {
|
||||
console.error('API Error:', error);
|
||||
}
|
||||
|
||||
if (!post) {
|
||||
return new Response(null, { status: 404 });
|
||||
}
|
||||
|
||||
const typeColor = getPostTypeColor(post.type || 'article');
|
||||
const contentText = post.content || post.description || '';
|
||||
const wordCount = contentText.length;
|
||||
const readTimeMinutes = Math.ceil(wordCount / 300);
|
||||
const articleMarkdown = contentText.replace(/^#\s+.+\r?\n+/, '');
|
||||
|
||||
const markdownProcessor = await createMarkdownProcessor();
|
||||
const renderedContent = await markdownProcessor.render(articleMarkdown);
|
||||
---
|
||||
|
||||
<BaseLayout title={`${post.title} - Termi`} description={post.description}>
|
||||
<ReadingProgress />
|
||||
<BackToTop />
|
||||
<Lightbox />
|
||||
<CodeCopyButton />
|
||||
|
||||
<div class="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div class="flex flex-col gap-8 lg:flex-row">
|
||||
<div class="min-w-0 flex-1">
|
||||
<TerminalWindow title={`~/content/posts/${post.slug}.md`} class="w-full">
|
||||
<div class="px-4 pb-2">
|
||||
<div class="terminal-panel ml-4 mt-4 space-y-5">
|
||||
<div class="flex flex-wrap items-start justify-between gap-4">
|
||||
<div class="space-y-4">
|
||||
<a href="/articles" class="terminal-link-arrow">
|
||||
<i class="fas fa-arrow-left"></i>
|
||||
<span>返回文章索引</span>
|
||||
</a>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<span class="terminal-kicker">
|
||||
<i class="fas fa-file-code"></i>
|
||||
document session
|
||||
</span>
|
||||
<span class="terminal-chip">
|
||||
<span class="h-2.5 w-2.5 rounded-full" style={`background-color: ${typeColor}`}></span>
|
||||
{post.type === 'article' ? 'article' : 'tweet'}
|
||||
</span>
|
||||
<span class="terminal-chip">
|
||||
<i class="fas fa-folder-tree text-[var(--primary)]"></i>
|
||||
{post.category}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<span class="terminal-stat-pill">
|
||||
<i class="far fa-calendar text-[var(--primary)]"></i>
|
||||
{post.date}
|
||||
</span>
|
||||
<span class="terminal-stat-pill">
|
||||
<i class="far fa-clock text-[var(--primary)]"></i>
|
||||
{readTimeMinutes} min
|
||||
</span>
|
||||
<span class="terminal-stat-pill">
|
||||
<i class="fas fa-font text-[var(--primary)]"></i>
|
||||
{wordCount} chars
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3">
|
||||
<h1 class="text-3xl font-bold tracking-tight text-[var(--title-color)] sm:text-4xl">{post.title}</h1>
|
||||
<p class="max-w-3xl text-base leading-8 text-[var(--text-secondary)]">{post.description}</p>
|
||||
</div>
|
||||
|
||||
{post.tags?.length > 0 && (
|
||||
<div class="flex flex-wrap gap-2">
|
||||
{post.tags.map(tag => (
|
||||
<a href={`/tags?tag=${encodeURIComponent(tag)}`} class="terminal-filter">
|
||||
<i class="fas fa-hashtag"></i>
|
||||
<span>{tag}</span>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="px-4 pb-2">
|
||||
<CommandPrompt command={`bat --style=plain ${post.slug}.md`} />
|
||||
|
||||
<div class="ml-4 mt-4 space-y-6">
|
||||
{post.image && (
|
||||
<div class="terminal-panel-muted overflow-hidden">
|
||||
<img
|
||||
src={resolveFileRef(post.image)}
|
||||
alt={post.title}
|
||||
class="w-full h-auto rounded-xl border border-[var(--border-color)] cursor-zoom-in"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div class="terminal-document article-content" set:html={renderedContent.code}></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="px-4 py-6">
|
||||
<div class="terminal-panel-muted ml-4 mt-4 flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||
<span class="text-sm text-[var(--text-secondary)]">
|
||||
file://content/posts/{post.slug}.md
|
||||
</span>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<a href="/articles" class="terminal-action-button">
|
||||
<i class="fas fa-list"></i>
|
||||
<span>back to index</span>
|
||||
</a>
|
||||
<button
|
||||
class="terminal-action-button terminal-action-button-primary"
|
||||
onclick={`navigator.clipboard.writeText(window.location.href)`}
|
||||
>
|
||||
<i class="fas fa-link"></i>
|
||||
<span>copy permalink</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TerminalWindow>
|
||||
|
||||
<RelatedPosts
|
||||
currentSlug={post.slug}
|
||||
currentCategory={post.category}
|
||||
currentTags={post.tags}
|
||||
/>
|
||||
|
||||
<section class="mt-8">
|
||||
<Comments postSlug={post.slug} class="terminal-panel" />
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<TableOfContents />
|
||||
</div>
|
||||
</div>
|
||||
</BaseLayout>
|
||||
Reference in New Issue
Block a user