feat(blog): add blog feature with layout, list component and i18n support
- Create BlogLayout for consistent blog page structure - Implement BlogList component with responsive design and line-clamp - Add blog navigation to header with proper routing - Include i18n support for both English and Chinese - Add sample blog pages with mock data
This commit is contained in:
124
src/components/BlogList.tsx
Normal file
124
src/components/BlogList.tsx
Normal file
@@ -0,0 +1,124 @@
|
||||
import React from 'react';
|
||||
import { useTranslations, type Lang } from '../i18n/utils';
|
||||
|
||||
/**
|
||||
* Blog post interface definition
|
||||
*/
|
||||
interface BlogPost {
|
||||
title: string;
|
||||
description: string;
|
||||
image: string;
|
||||
slug: string;
|
||||
tags: string[];
|
||||
date: string;
|
||||
readTime: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Props interface for BlogList component
|
||||
*/
|
||||
interface BlogListProps {
|
||||
posts: BlogPost[];
|
||||
lang: Lang;
|
||||
baseUrl?: string; // Base URL for blog posts, defaults to '/blog/posts/'
|
||||
}
|
||||
|
||||
/**
|
||||
* Reusable blog list component that displays blog posts in a grid layout
|
||||
* @param posts - Array of blog posts to display
|
||||
* @param lang - Current language for internationalization
|
||||
* @param baseUrl - Base URL for blog post links
|
||||
*/
|
||||
export default function BlogList({ posts, lang, baseUrl = '/blog/posts/' }: BlogListProps) {
|
||||
const t = useTranslations(lang);
|
||||
|
||||
// Adjust base URL for Chinese language
|
||||
const postBaseUrl = lang === 'zh' ? '/zh/blog/posts/' : baseUrl;
|
||||
|
||||
// Get localized "Read More" text
|
||||
const readMoreText = lang === 'zh' ? '阅读更多' : 'Read More';
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{posts.map((post, index) => (
|
||||
<article key={post.slug || index} className="group">
|
||||
<div className="bg-card/50 backdrop-blur-sm rounded-2xl overflow-hidden border border-border hover:border-purple-500/50 transition-all duration-300 hover:transform hover:scale-[1.02]">
|
||||
<div className="flex flex-col md:flex-row">
|
||||
{/* Featured Image */}
|
||||
<div className="relative overflow-hidden md:w-80 md:flex-shrink-0">
|
||||
<img
|
||||
src={post.image}
|
||||
alt={post.title}
|
||||
className="w-full h-48 md:h-full object-cover group-hover:scale-110 transition-transform duration-300"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-t md:bg-gradient-to-r from-background/50 to-transparent"></div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="p-6 flex-1 flex flex-col justify-between">
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-card-foreground mb-3 group-hover:text-purple-500 transition-colors duration-200">
|
||||
<a href={`${postBaseUrl}${post.slug}`}>
|
||||
{post.title}
|
||||
</a>
|
||||
</h2>
|
||||
|
||||
<p className="text-muted-foreground mb-4 line-clamp-3">
|
||||
{post.description}
|
||||
</p>
|
||||
|
||||
{/* Tags */}
|
||||
<div className="flex flex-wrap gap-2 mb-4">
|
||||
{post.tags.map((tag, tagIndex) => (
|
||||
<span
|
||||
key={`${tag}-${tagIndex}`}
|
||||
className="px-2 py-1 text-xs bg-muted text-muted-foreground rounded-full"
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Meta Info */}
|
||||
<div className="flex items-center justify-between text-sm text-muted-foreground">
|
||||
<div className="flex items-center space-x-4">
|
||||
<span className="flex items-center">
|
||||
<svg className="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 002 2v12a2 2 0 002 2z"></path>
|
||||
</svg>
|
||||
{post.date}
|
||||
</span>
|
||||
<span>{post.readTime}</span>
|
||||
</div>
|
||||
|
||||
<a
|
||||
href={`${postBaseUrl}${post.slug}`}
|
||||
className="text-purple-500 hover:text-purple-400 font-medium flex items-center group"
|
||||
>
|
||||
{readMoreText}
|
||||
<svg className="w-4 h-4 ml-1 group-hover:translate-x-1 transition-transform duration-200" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M17 8l4 4m0 0l-4 4m4-4H3"></path>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* CSS styles for line clamping (to be included in global styles)
|
||||
*/
|
||||
export const blogListStyles = `
|
||||
.line-clamp-3 {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
`;
|
||||
@@ -1,6 +1,6 @@
|
||||
import ThemeToggle from "./ui/theme-toggle";
|
||||
import { personalInfo } from "@/lib/data";
|
||||
import LanguageSwitcher from "./LanguageSwitcher";
|
||||
import ThemeToggle from "./ui/theme-toggle";
|
||||
import { useTranslations, getLocalizedPath, type Lang } from "@/i18n/utils";
|
||||
import { useState, useEffect } from "react";
|
||||
import { Menu, X } from "lucide-react";
|
||||
@@ -45,10 +45,11 @@ export default function GlassHeader({ lang }: GlassHeaderProps) {
|
||||
{ key: 'nav.about', icon: '👨💻 ', sectionId: 'about' },
|
||||
{ key: 'nav.services', icon: '🛠️ ', sectionId: 'services' },
|
||||
{ key: 'nav.projects', icon: '🚀 ', sectionId: 'projects' },
|
||||
{ key: 'nav.blog', icon: '📝 ', href: getLocalizedPath('/blog', lang) },
|
||||
].map((item) => (
|
||||
<a
|
||||
key={item.key}
|
||||
href={`#${item.sectionId}`}
|
||||
href={item.href || `#${item.sectionId}`}
|
||||
className="transition-colors duration-150 hover:text-foreground/80 text-foreground/60"
|
||||
>
|
||||
{item.icon}
|
||||
@@ -87,10 +88,11 @@ export default function GlassHeader({ lang }: GlassHeaderProps) {
|
||||
{ key: 'nav.about', icon: '👨💻 ', sectionId: 'about' },
|
||||
{ key: 'nav.services', icon: '🛠️ ', sectionId: 'services' },
|
||||
{ key: 'nav.projects', icon: '🚀 ', sectionId: 'projects' },
|
||||
{ key: 'nav.blog', icon: '📝 ', href: getLocalizedPath('/blog', lang) },
|
||||
].map((item) => (
|
||||
<a
|
||||
key={item.key}
|
||||
href={`#${item.sectionId}`}
|
||||
href={item.href || `#${item.sectionId}`}
|
||||
className="transition-colors duration-150 hover:text-foreground/80 text-foreground/60 py-2 block"
|
||||
onClick={toggleMenu}
|
||||
>
|
||||
|
||||
@@ -12,6 +12,7 @@ export const ui = {
|
||||
'nav.about': 'About',
|
||||
'nav.services': 'Services',
|
||||
'nav.projects': 'Projects',
|
||||
'nav.blog': 'Blog',
|
||||
'nav.contact': 'Contact',
|
||||
'site.title': 'Joy Zhao - Full Stack Developer',
|
||||
'site.description': 'Full Stack Developer specializing in React, Node.js, and modern web technologies',
|
||||
@@ -31,6 +32,7 @@ export const ui = {
|
||||
'nav.about': '关于',
|
||||
'nav.services': '服务',
|
||||
'nav.projects': '项目',
|
||||
'nav.blog': '博客',
|
||||
'nav.contact': '联系',
|
||||
'site.title': 'Joy Zhao - 全栈开发者',
|
||||
'site.description': '专注于 React、Node.js 和现代 Web 技术的全栈开发者',
|
||||
|
||||
94
src/layouts/BlogLayout.astro
Normal file
94
src/layouts/BlogLayout.astro
Normal file
@@ -0,0 +1,94 @@
|
||||
---
|
||||
import { type Lang } from '@/i18n/utils';
|
||||
import { defaultLang } from '@/i18n/ui';
|
||||
import GlassHeader from '@/components/GlassHeader';
|
||||
import Footer from '@/components/Footer';
|
||||
import "../styles/global.css";
|
||||
|
||||
export interface Props {
|
||||
title: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
const { title, description = 'Explore my latest thoughts on coding, tech trends, and developer life.' } = Astro.props;
|
||||
const lang = Astro.currentLocale as Lang || defaultLang;
|
||||
---
|
||||
|
||||
<!doctype html>
|
||||
<html lang={lang}>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="description" content={description} />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="generator" content={Astro.generator} />
|
||||
<title>{title}</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
</head>
|
||||
<body class="min-h-screen bg-background font-sans antialiased selection:bg-purple-500/20 selection:text-purple-500">
|
||||
<div
|
||||
class="fixed inset-0 -z-10 h-full w-full bg-background bg-[radial-gradient(ellipse_80%_80%_at_50%_-20%,rgba(120,119,198,0.3),rgba(255,255,255,0))]"
|
||||
>
|
||||
</div>
|
||||
<!-- Glass Header with navigation -->
|
||||
<GlassHeader lang={lang} client:load />
|
||||
|
||||
<!-- Main content with proper spacing for fixed header -->
|
||||
<div class="pt-16">
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<Footer lang={lang} client:load />
|
||||
</body>
|
||||
</html>
|
||||
|
||||
<script is:inline>
|
||||
const getThemePreference = () => {
|
||||
if (typeof localStorage !== "undefined" && localStorage.getItem("theme")) {
|
||||
return localStorage.getItem("theme");
|
||||
}
|
||||
return window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
? "dark"
|
||||
: "light";
|
||||
};
|
||||
const isDark = getThemePreference() === "dark";
|
||||
document.documentElement.classList[isDark ? "add" : "remove"]("dark");
|
||||
|
||||
if (typeof localStorage !== "undefined") {
|
||||
const observer = new MutationObserver(() => {
|
||||
const isDark = document.documentElement.classList.contains("dark");
|
||||
localStorage.setItem("theme", isDark ? "dark" : "light");
|
||||
});
|
||||
observer.observe(document.documentElement, {
|
||||
attributes: true,
|
||||
attributeFilter: ["class"],
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
:root {
|
||||
--transition-standard: 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
body {
|
||||
transition:
|
||||
background-color var(--transition-standard),
|
||||
color var(--transition-standard);
|
||||
}
|
||||
</style>
|
||||
127
src/pages/blog/index.astro
Normal file
127
src/pages/blog/index.astro
Normal file
@@ -0,0 +1,127 @@
|
||||
---
|
||||
import BlogLayout from '../../layouts/BlogLayout.astro';
|
||||
import BlogList from '../../components/BlogList.tsx';
|
||||
|
||||
// Sample blog data - inline for now
|
||||
const blogPosts = [
|
||||
{
|
||||
id: 1,
|
||||
title: "Mastering React Hooks: A Deep Dive",
|
||||
description: "Explore the power of React Hooks to manage state and side effects in functional components, with practical examples and best practices.",
|
||||
image: "https://images.unsplash.com/photo-1633356122544-f134324a6cee?w=400&h=250&fit=crop&crop=center",
|
||||
date: "May 10, 2025",
|
||||
readTime: "5 min read",
|
||||
tags: ["React", "JavaScript", "Frontend"],
|
||||
slug: "mastering-react-hooks"
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "Scaling Node.js Apps with Docker",
|
||||
description: "Learn how to containerize Node.js applications using Docker for seamless deployment and scalability in production environments.",
|
||||
image: "https://images.unsplash.com/photo-1605745341112-85968b19335b?w=400&h=250&fit=crop&crop=center",
|
||||
date: "April 25, 2025",
|
||||
readTime: "7 min read",
|
||||
tags: ["Node.js", "Docker", "DevOps"],
|
||||
slug: "scaling-nodejs-docker"
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: "Building Modern UIs with Tailwind CSS",
|
||||
description: "Discover how to create beautiful, responsive user interfaces using Tailwind CSS utility classes and component patterns.",
|
||||
image: "https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=400&h=250&fit=crop&crop=center",
|
||||
date: "April 15, 2025",
|
||||
readTime: "6 min read",
|
||||
tags: ["CSS", "Tailwind", "UI/UX"],
|
||||
slug: "modern-ui-tailwind"
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: "TypeScript Best Practices for Large Projects",
|
||||
description: "Essential TypeScript patterns and practices for maintaining code quality and developer productivity in enterprise applications.",
|
||||
image: "https://images.unsplash.com/photo-1516321318423-f06f85e504b3?w=400&h=250&fit=crop&crop=center",
|
||||
date: "March 30, 2025",
|
||||
readTime: "8 min read",
|
||||
tags: ["TypeScript", "JavaScript", "Architecture"],
|
||||
slug: "typescript-best-practices"
|
||||
}
|
||||
];
|
||||
|
||||
// Sample categories and tags
|
||||
const categories = ["React", "Node.js", "TailwindCSS", "TypeScript", "DevOps"];
|
||||
const tags = ["#React", "#JavaScript", "#Frontend", "#Node.js", "#Docker", "#DevOps", "#TailwindCSS", "#CSS", "#TypeScript"];
|
||||
|
||||
---
|
||||
|
||||
<BlogLayout title="Blog - Joy Zhao" description="Dive into my thoughts on coding, tech trends, and developer life. Explore my latest posts below.">
|
||||
<main class="min-h-screen">
|
||||
<!-- Header Section -->
|
||||
<div class="container mx-auto px-4 pt-24 pb-12">
|
||||
<div class="text-center mb-16">
|
||||
<h1 class="text-5xl md:text-6xl font-bold bg-gradient-to-r from-foreground via-purple-600 to-purple-800 dark:from-foreground dark:via-purple-200 dark:to-purple-300 bg-clip-text text-transparent mb-6">
|
||||
Our <span class="text-purple-500">Latest</span> Blog
|
||||
</h1>
|
||||
<p class="text-xl text-muted-foreground max-w-3xl mx-auto">
|
||||
Dive into my thoughts on coding, tech trends, and developer life. Explore my latest posts below.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="container mx-auto px-4 pb-20">
|
||||
<div class="grid grid-cols-1 lg:grid-cols-4 gap-8">
|
||||
<!-- Sidebar -->
|
||||
<div class="lg:col-span-1 space-y-8">
|
||||
<!-- Categories -->
|
||||
<div class="bg-card/50 backdrop-blur-sm rounded-2xl p-6 border border-border">
|
||||
<h3 class="text-xl font-semibold text-card-foreground mb-4 flex items-center">
|
||||
<svg class="w-5 h-5 mr-2 text-purple-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14-7l2 2-2 2m2-2H9m10 0V9M5 19l2-2-2-2m2 2H3m2 0v2"></path>
|
||||
</svg>
|
||||
Categories
|
||||
</h3>
|
||||
<div class="space-y-2">
|
||||
{categories.map((category) => (
|
||||
<a href={`/blog/categories/${category.toLowerCase()}`}
|
||||
class="block text-muted-foreground hover:text-purple-500 transition-colors duration-200">
|
||||
{category}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tags -->
|
||||
<div class="bg-card/50 backdrop-blur-sm rounded-2xl p-6 border border-border">
|
||||
<h3 class="text-xl font-semibold text-card-foreground mb-4 flex items-center">
|
||||
<svg class="w-5 h-5 mr-2 text-purple-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z"></path>
|
||||
</svg>
|
||||
# Tags
|
||||
</h3>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
{tags.map((tag) => (
|
||||
<a href={`/blog/tags/${tag.slice(1).toLowerCase()}`}
|
||||
class="inline-block px-3 py-1 text-sm bg-muted text-muted-foreground rounded-full hover:bg-purple-500/20 hover:text-purple-500 transition-all duration-200">
|
||||
{tag}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Blog Posts -->
|
||||
<div class="lg:col-span-3">
|
||||
<BlogList posts={blogPosts} lang="en" client:load />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</BlogLayout>
|
||||
|
||||
<style>
|
||||
.line-clamp-3 {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
127
src/pages/zh/blog/index.astro
Normal file
127
src/pages/zh/blog/index.astro
Normal file
@@ -0,0 +1,127 @@
|
||||
---
|
||||
import BlogLayout from '../../../layouts/BlogLayout.astro';
|
||||
import BlogList from '../../../components/BlogList.tsx';
|
||||
|
||||
// 示例博客数据 - 暂时内联
|
||||
const blogPosts = [
|
||||
{
|
||||
id: 1,
|
||||
title: "精通 React Hooks:深入探索",
|
||||
description: "探索 React Hooks 在函数组件中管理状态和副作用的强大功能,包含实用示例和最佳实践。",
|
||||
image: "https://images.unsplash.com/photo-1633356122544-f134324a6cee?w=400&h=250&fit=crop&crop=center",
|
||||
date: "2025年5月10日",
|
||||
readTime: "5分钟阅读",
|
||||
tags: ["React", "JavaScript", "前端"],
|
||||
slug: "mastering-react-hooks"
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "使用 Docker 扩展 Node.js 应用",
|
||||
description: "学习如何使用 Docker 容器化 Node.js 应用程序,实现生产环境中的无缝部署和可扩展性。",
|
||||
image: "https://images.unsplash.com/photo-1605745341112-85968b19335b?w=400&h=250&fit=crop&crop=center",
|
||||
date: "2025年4月25日",
|
||||
readTime: "7分钟阅读",
|
||||
tags: ["Node.js", "Docker", "DevOps"],
|
||||
slug: "scaling-nodejs-docker"
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: "使用 Tailwind CSS 构建现代 UI",
|
||||
description: "探索如何使用 Tailwind CSS 实用类和组件模式创建美观、响应式的用户界面。",
|
||||
image: "https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=400&h=250&fit=crop&crop=center",
|
||||
date: "2025年4月15日",
|
||||
readTime: "6分钟阅读",
|
||||
tags: ["CSS", "Tailwind", "UI/UX"],
|
||||
slug: "modern-ui-tailwind"
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: "大型项目的 TypeScript 最佳实践",
|
||||
description: "在企业级应用中维护代码质量和开发者生产力的必备 TypeScript 模式和实践。",
|
||||
image: "https://images.unsplash.com/photo-1516321318423-f06f85e504b3?w=400&h=250&fit=crop&crop=center",
|
||||
date: "2025年3月30日",
|
||||
readTime: "8分钟阅读",
|
||||
tags: ["TypeScript", "JavaScript", "架构"],
|
||||
slug: "typescript-best-practices"
|
||||
}
|
||||
];
|
||||
|
||||
// 示例分类和标签
|
||||
const categories = ["React", "Node.js", "TailwindCSS", "TypeScript", "DevOps"];
|
||||
const tags = ["#React", "#JavaScript", "#前端", "#Node.js", "#Docker", "#DevOps", "#TailwindCSS", "#CSS", "#TypeScript"];
|
||||
|
||||
---
|
||||
|
||||
<BlogLayout title="博客 - 赵桂阳" description="深入我对编程、技术趋势和开发者生活的思考。探索我的最新文章。">
|
||||
<main class="min-h-screen">
|
||||
<!-- 头部区域 -->
|
||||
<div class="container mx-auto px-4 pt-24 pb-12">
|
||||
<div class="text-center mb-16">
|
||||
<h1 class="text-5xl md:text-6xl font-bold bg-gradient-to-r from-foreground via-purple-600 to-purple-800 dark:from-foreground dark:via-purple-200 dark:to-purple-300 bg-clip-text text-transparent mb-6">
|
||||
我的 <span class="text-purple-500">最新</span> 博客
|
||||
</h1>
|
||||
<p class="text-xl text-muted-foreground max-w-3xl mx-auto">
|
||||
深入我对编程、技术趋势和开发者生活的思考。探索我的最新文章。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 主要内容 -->
|
||||
<div class="container mx-auto px-4 pb-20">
|
||||
<div class="grid grid-cols-1 lg:grid-cols-4 gap-8">
|
||||
<!-- 侧边栏 -->
|
||||
<div class="lg:col-span-1 space-y-8">
|
||||
<!-- 分类 -->
|
||||
<div class="bg-card/50 backdrop-blur-sm rounded-2xl p-6 border border-border">
|
||||
<h3 class="text-xl font-semibold text-card-foreground mb-4 flex items-center">
|
||||
<svg class="w-5 h-5 mr-2 text-purple-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14-7l2 2-2 2m2-2H9m10 0V9M5 19l2-2-2-2m2 2H3m2 0v2"></path>
|
||||
</svg>
|
||||
分类
|
||||
</h3>
|
||||
<div class="space-y-2">
|
||||
{categories.map((category) => (
|
||||
<a href={`/zh/blog/categories/${category.toLowerCase()}`}
|
||||
class="block text-muted-foreground hover:text-purple-500 transition-colors duration-200">
|
||||
{category}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 标签 -->
|
||||
<div class="bg-card/50 backdrop-blur-sm rounded-2xl p-6 border border-border">
|
||||
<h3 class="text-xl font-semibold text-card-foreground mb-4 flex items-center">
|
||||
<svg class="w-5 h-5 mr-2 text-purple-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z"></path>
|
||||
</svg>
|
||||
# 标签
|
||||
</h3>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
{tags.map((tag) => (
|
||||
<a href={`/zh/blog/tags/${tag.slice(1).toLowerCase()}`}
|
||||
class="inline-block px-3 py-1 text-sm bg-muted text-muted-foreground rounded-full hover:bg-purple-500/20 hover:text-purple-500 transition-all duration-200">
|
||||
{tag}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 博客文章 -->
|
||||
<div class="lg:col-span-3">
|
||||
<BlogList posts={blogPosts} lang="zh" client:load />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</BlogLayout>
|
||||
|
||||
<style>
|
||||
.line-clamp-3 {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
@@ -1,6 +1,14 @@
|
||||
@import "tailwindcss";
|
||||
@import "tw-animate-css";
|
||||
|
||||
/* Blog List Component Styles */
|
||||
.line-clamp-3 {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
:root {
|
||||
|
||||
Reference in New Issue
Block a user