diff --git a/package.json b/package.json
index 5e2bf4b..938a9b6 100644
--- a/package.json
+++ b/package.json
@@ -25,6 +25,7 @@
"astro-icon": "^1.1.5",
"astro-robots-txt": "^1.0.0",
"astro-webmanifest": "^1.0.0",
+ "clsx": "^2.1.1",
"cssnano": "^7.0.6",
"hastscript": "^9.0.0",
"markdown-it": "^14.1.0",
@@ -39,6 +40,7 @@
"satori": "0.12.1",
"satori-html": "^0.3.2",
"sharp": "^0.33.5",
+ "tailwind-merge": "^3.0.1",
"unified": "^11.0.5",
"unist-util-visit": "^5.0.0"
},
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 009eb92..12cc441 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -38,6 +38,9 @@ importers:
astro-webmanifest:
specifier: ^1.0.0
version: 1.0.0
+ clsx:
+ specifier: ^2.1.1
+ version: 2.1.1
cssnano:
specifier: ^7.0.6
version: 7.0.6(postcss@8.4.49)
@@ -80,6 +83,9 @@ importers:
sharp:
specifier: ^0.33.5
version: 0.33.5
+ tailwind-merge:
+ specifier: ^3.0.1
+ version: 3.0.1
unified:
specifier: ^11.0.5
version: 11.0.5
@@ -3182,6 +3188,9 @@ packages:
engines: {node: '>=14.0.0'}
hasBin: true
+ tailwind-merge@3.0.1:
+ resolution: {integrity: sha512-AvzE8FmSoXC7nC+oU5GlQJbip2UO7tmOhOfQyOmPhrStOGXHU08j8mZEHZ4BmCqY5dWTCo4ClWkNyRNx1wpT0g==}
+
tailwindcss@4.0.0:
resolution: {integrity: sha512-ULRPI3A+e39T7pSaf1xoi58AqqJxVCLg8F/uM5A3FadUbnyDTgltVnXJvdkTjwCOGA6NazqHVcwPJC5h2vRYVQ==}
@@ -7299,6 +7308,8 @@ snapshots:
csso: 5.0.5
picocolors: 1.1.1
+ tailwind-merge@3.0.1: {}
+
tailwindcss@4.0.0: {}
tailwindcss@4.0.0-beta.8: {}
diff --git a/src/components/SocialList.astro b/src/components/SocialList.astro
index f5063b3..c6b0e3a 100644
--- a/src/components/SocialList.astro
+++ b/src/components/SocialList.astro
@@ -27,6 +27,16 @@ const socialLinks: {
link: "https://x.com/kazoottt",
name: "mdi:twitter",
},
+ {
+ friendlyName: "Photo",
+ link: "https://unsplash.com/@kazoottt",
+ name: "mdi:camera",
+ },
+ {
+ friendlyName: "汇总",
+ link: "https://bento.me/KazooTTT",
+ name: "mdi:link",
+ },
];
---
diff --git a/src/components/componentsBefore/BaseHead.astro b/src/components/componentsBefore/BaseHead.astro
new file mode 100644
index 0000000..cb6dbc1
--- /dev/null
+++ b/src/components/componentsBefore/BaseHead.astro
@@ -0,0 +1,124 @@
+---
+// Import the global.css file here so that it is included on
+// all pages through the use of the component.
+import '../styles/app.css'
+
+import type { SiteMeta } from '@/types'
+import { siteConfig } from '@/site-config'
+
+type Props = SiteMeta
+import { ViewTransitions } from 'astro:transitions'
+const { articleDate, description, banner, title } = Astro.props
+
+const titleSeparator = '•'
+const siteTitle = `${title} ${titleSeparator} ${siteConfig.title}`
+const canonicalURL = new URL(Astro.url.pathname, Astro.site)
+const socialImageURL = new URL(banner ? banner : '/social-card.png', Astro.url).href
+---
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{siteTitle}
+
+{/* Icons / Favicon */}
+
+
+
+
+
+
+
+{/* Font preloads */}
+
+
+{/* Canonical URL */}
+
+
+{/* Primary Meta Tags */}
+
+
+
+
+{/* Theme Colour */}
+
+
+{/* Open Graph / Facebook */}
+
+
+
+
+
+
+
+
+
+{
+ articleDate && (
+ <>
+
+
+ >
+ )
+}
+
+{/* Twitter */}
+
+
+
+
+
+
+{/* Sitemap */}
+
+
+{/* RSS auto-discovery */}
+
+
+
+
+
diff --git a/src/components/componentsBefore/Button.astro b/src/components/componentsBefore/Button.astro
new file mode 100644
index 0000000..4649f2f
--- /dev/null
+++ b/src/components/componentsBefore/Button.astro
@@ -0,0 +1,19 @@
+---
+import { cn } from "@/utils/tailwind";
+const { as: Tag = "a", class: className, title, href, style = "button" } = Astro.props;
+---
+
+
+
+ {title}
+
+
diff --git a/src/components/componentsBefore/Card.astro b/src/components/componentsBefore/Card.astro
new file mode 100644
index 0000000..d3cf64f
--- /dev/null
+++ b/src/components/componentsBefore/Card.astro
@@ -0,0 +1,57 @@
+---
+import { cn } from '@/utils'
+import type { ImageMetadata } from 'astro'
+import { Image } from 'astro:assets'
+
+const {
+ as: Tag = 'div',
+ class: className,
+ href,
+ target,
+ heading,
+ subheading,
+ date,
+ imagePath,
+ altText,
+ imageClass
+} = Astro.props
+
+// If href is provided, use 'a' tag instead of the default or provided tag
+const Component = href ? 'a' : Tag
+
+const images = import.meta.glob<{ default: ImageMetadata }>('/src/assets/*.{jpeg,jpg,png,gif}')
+
+if (imagePath) {
+ if (!images[imagePath])
+ throw new Error(`"${imagePath}" does not exist in glob: "src/assets/*.{jpeg,jpg,png,gif}"`)
+}
+---
+
+
+ {
+ imagePath && (
+
+ )
+ }
+
+
+
{heading}
+ {subheading && {subheading}
}
+ {date && {date}
}
+
+
+
+
diff --git a/src/components/componentsBefore/CategoriesView.tsx b/src/components/componentsBefore/CategoriesView.tsx
new file mode 100644
index 0000000..071764f
--- /dev/null
+++ b/src/components/componentsBefore/CategoriesView.tsx
@@ -0,0 +1,113 @@
+import type { CategoryHierarchy } from '@/types'
+import React, { useState } from 'react'
+
+interface CategoriesViewProps {
+ allCategories: [string, number][]
+ allCategoriesHierarchy: CategoryHierarchy[]
+ defaultViewMode?: 'tree' | 'list'
+}
+
+const CategoriesView: React.FC = ({
+ allCategories,
+ allCategoriesHierarchy,
+ defaultViewMode = 'tree'
+}) => {
+ const [viewMode, setViewMode] = useState<'tree' | 'list'>(defaultViewMode)
+
+ return (
+
+
+
Categories
+
+
+
View:
+
+
+
+
+
+
+
+ {allCategoriesHierarchy.length === 0 &&
No posts yet.
}
+
+ {/* List View */}
+ {viewMode === 'list' && (
+
+ {allCategories.map(([category, count]) => (
+
+ ))}
+
+ )}
+
+ {/* Tree View */}
+ {viewMode === 'tree' && (
+
+ {allCategoriesHierarchy.map((category) => (
+
+ {/* Render root-level categories */}
+
+
+ {/* Render nested categories with indentation */}
+ {category.children && Object.values(category.children).length > 0 && (
+
+ {Object.values(category.children).map((childCategory) => (
+
+ ))}
+
+ )}
+
+ ))}
+
+ )}
+
+ )
+}
+
+export default CategoriesView
diff --git a/src/components/componentsBefore/CategorySection.astro b/src/components/componentsBefore/CategorySection.astro
new file mode 100644
index 0000000..db30155
--- /dev/null
+++ b/src/components/componentsBefore/CategorySection.astro
@@ -0,0 +1,39 @@
+---
+import ToolSection from './ToolSection.astro'
+
+interface Props {
+ title: string
+ categories: {
+ title: string
+ sections: {
+ [key: string]: {
+ title: string
+ tools: {
+ name: string
+ description: string
+ href?: string
+ iconPath?: string
+ }[]
+ }
+ }
+ }[]
+}
+
+const { title, categories } = Astro.props
+---
+
+
+
{title}
+ {
+ categories.map((category) => (
+
+
{category.title}
+ {Object.values(category.sections).map((section) => (
+
+
+
+ ))}
+
+ ))
+ }
+
diff --git a/src/components/componentsBefore/FormattedDate.astro b/src/components/componentsBefore/FormattedDate.astro
new file mode 100644
index 0000000..45429a9
--- /dev/null
+++ b/src/components/componentsBefore/FormattedDate.astro
@@ -0,0 +1,15 @@
+---
+import type { HTMLAttributes } from 'astro/types'
+
+interface Props extends HTMLAttributes<'time'> {
+ date: Date
+}
+
+const { date, ...attrs } = Astro.props
+
+const formattedDate = date.toISOString().slice(0, 10).replace(/-/g, '')
+---
+
+
diff --git a/src/components/componentsBefore/FreeTemplate.astro b/src/components/componentsBefore/FreeTemplate.astro
new file mode 100644
index 0000000..8939f5d
--- /dev/null
+++ b/src/components/componentsBefore/FreeTemplate.astro
@@ -0,0 +1,17 @@
+---
+
+---
+
+
+
+
+
+
+
+ Get free template
+
diff --git a/src/components/componentsBefore/GiscusComment.tsx b/src/components/componentsBefore/GiscusComment.tsx
new file mode 100644
index 0000000..6cdaff6
--- /dev/null
+++ b/src/components/componentsBefore/GiscusComment.tsx
@@ -0,0 +1,62 @@
+'use client'
+
+import React from 'react'
+import Giscus from '@giscus/react'
+
+const id = 'inject-comments'
+
+const GiscusComment = () => {
+ const [theme, setTheme] = React.useState('preferred_color_scheme')
+
+ React.useEffect(() => {
+ // Initial theme setup
+ const savedTheme = localStorage.getItem('theme')
+ if (savedTheme) {
+ setTheme(savedTheme)
+ }
+
+ // Listen for theme changes
+ const handleThemeChange = (e: CustomEvent<{ theme: string }>) => {
+ setTheme(e.detail.theme)
+ }
+
+ document.addEventListener('theme-change', handleThemeChange as EventListener)
+
+ // Listen for astro:after-swap events (view transitions)
+ const handleAfterSwap = () => {
+ const currentTheme = localStorage.getItem('theme')
+ if (currentTheme) {
+ setTheme(currentTheme)
+ }
+ }
+
+ document.addEventListener('astro:after-swap', handleAfterSwap)
+
+ return () => {
+ document.removeEventListener('theme-change', handleThemeChange as EventListener)
+ document.removeEventListener('astro:after-swap', handleAfterSwap)
+ }
+ }, [])
+
+ return (
+
+
+
+ )
+}
+
+export default GiscusComment
diff --git a/src/components/componentsBefore/GithubHotLine.tsx b/src/components/componentsBefore/GithubHotLine.tsx
new file mode 100644
index 0000000..d928fc2
--- /dev/null
+++ b/src/components/componentsBefore/GithubHotLine.tsx
@@ -0,0 +1,6 @@
+import GitHubCalendar from 'react-github-calendar'
+const GithubHotLine = () => {
+ return
+}
+
+export default GithubHotLine
diff --git a/src/components/componentsBefore/Label.astro b/src/components/componentsBefore/Label.astro
new file mode 100644
index 0000000..2c82745
--- /dev/null
+++ b/src/components/componentsBefore/Label.astro
@@ -0,0 +1,18 @@
+---
+import { cn } from '@/utils'
+
+const { class: className, as: Tag = 'div', title, href, ...props } = Astro.props
+---
+
+
+
+ {title}
+
diff --git a/src/components/componentsBefore/PageViews.tsx b/src/components/componentsBefore/PageViews.tsx
new file mode 100644
index 0000000..edb777f
--- /dev/null
+++ b/src/components/componentsBefore/PageViews.tsx
@@ -0,0 +1,36 @@
+'use client'
+
+import { useEffect, useState } from 'react'
+
+interface PageViewsProps {
+ slug: string
+}
+
+export default function PageViews({ slug }: PageViewsProps) {
+ const [views, setViews] = useState(null)
+
+ useEffect(() => {
+ console.log('PageViews component mounted with slug:', slug)
+ async function updatePageView() {
+ try {
+ // First increment the view count
+ const postResponse = await fetch(`/api/pageview/${slug}`, {
+ method: 'POST'
+ })
+ if (!postResponse.ok) throw new Error('Failed to increment view count')
+
+ // Then get the updated count
+ const { views } = await postResponse.json()
+ console.log('Updated views:', views)
+ setViews(views)
+ } catch (error) {
+ console.error('Error updating page views:', error)
+ setViews(null)
+ }
+ }
+
+ updatePageView()
+ }, [slug])
+
+ return Views: {views === null ? '-' : views}
+}
diff --git a/src/components/componentsBefore/Paginator.astro b/src/components/componentsBefore/Paginator.astro
new file mode 100644
index 0000000..a03e0a6
--- /dev/null
+++ b/src/components/componentsBefore/Paginator.astro
@@ -0,0 +1,29 @@
+---
+import type { PaginationLink } from '@/types'
+
+interface Props {
+ nextUrl?: PaginationLink
+ prevUrl?: PaginationLink
+}
+
+const { nextUrl, prevUrl } = Astro.props
+---
+
+{
+ (prevUrl || nextUrl) && (
+
+ )
+}
diff --git a/src/components/componentsBefore/ProjectCard.astro b/src/components/componentsBefore/ProjectCard.astro
new file mode 100644
index 0000000..0608fd3
--- /dev/null
+++ b/src/components/componentsBefore/ProjectCard.astro
@@ -0,0 +1,52 @@
+---
+import { Image } from 'astro:assets'
+import type { ImageMetadata } from 'astro'
+import { cn } from '@/utils'
+
+const {
+ as: Tag = 'a',
+ class: className,
+ href,
+ heading,
+ subheading,
+ imagePath,
+ altText,
+ target
+} = Astro.props
+
+let imageComponent = null
+if (imagePath) {
+ const images = import.meta.glob<{ default: ImageMetadata }>('/src/assets/*.{jpeg,jpg,png,gif}')
+ if (images[imagePath]) {
+ imageComponent = images[imagePath]
+ }
+}
+---
+
+
+ {
+ imageComponent && (
+
+ )
+ }
+
+
{heading}
+ {subheading}
+
+
+
+
diff --git a/src/components/componentsBefore/Section.astro b/src/components/componentsBefore/Section.astro
new file mode 100644
index 0000000..c0a24bc
--- /dev/null
+++ b/src/components/componentsBefore/Section.astro
@@ -0,0 +1,15 @@
+---
+import { cn } from '@/utils'
+
+const { class: className, title, subtitle } = Astro.props
+---
+
+
+
+
{title}
+ {subtitle}
+
+
+
+
+
diff --git a/src/components/componentsBefore/SkillLayout.astro b/src/components/componentsBefore/SkillLayout.astro
new file mode 100644
index 0000000..86e68ed
--- /dev/null
+++ b/src/components/componentsBefore/SkillLayout.astro
@@ -0,0 +1,11 @@
+---
+import Button from './Button.astro'
+const { title, skills } = Astro.props
+---
+
+
+
{title}
+
+ {skills.map((skill: string[]) => )}
+
+
diff --git a/src/components/componentsBefore/ThemeProvider.astro b/src/components/componentsBefore/ThemeProvider.astro
new file mode 100644
index 0000000..79e1ad3
--- /dev/null
+++ b/src/components/componentsBefore/ThemeProvider.astro
@@ -0,0 +1,42 @@
+
diff --git a/src/components/componentsBefore/ToolSection.astro b/src/components/componentsBefore/ToolSection.astro
new file mode 100644
index 0000000..ba1c9c8
--- /dev/null
+++ b/src/components/componentsBefore/ToolSection.astro
@@ -0,0 +1,48 @@
+---
+import { cn } from "@/utils/tailwind";
+import { Icon } from "astro-icon/components";
+
+interface Props {
+ class?: string;
+ title: string;
+ tools: {
+ name: string;
+ description: string;
+ href?: string;
+ iconPath?: string;
+ iconBgColour?: string;
+ }[];
+}
+const { class: className, title, tools, ...props } = Astro.props;
+---
+
+
diff --git a/src/components/componentsBefore/blog/Calendar.tsx b/src/components/componentsBefore/blog/Calendar.tsx
new file mode 100644
index 0000000..ffd17a8
--- /dev/null
+++ b/src/components/componentsBefore/blog/Calendar.tsx
@@ -0,0 +1,197 @@
+import type { CollectionEntry } from 'astro:content'
+import React, { useState } from 'react'
+
+type Post = CollectionEntry<'post'>
+interface Props {
+ posts: Post[]
+ currentDate?: Date
+}
+
+interface DateTimeFormatOptions {
+ year?: 'numeric' | '2-digit'
+ month?: 'numeric' | '2-digit' | 'long' | 'short' | 'narrow'
+ day?: 'numeric' | '2-digit'
+}
+
+const weekDays = ['一', '二', '三', '四', '五', '六', '日']
+
+const getFormattedDate = (date: Date, options: DateTimeFormatOptions): string => {
+ return new Intl.DateTimeFormat('zh-CN', options).format(date)
+}
+
+export const Calendar: React.FC = ({ posts, currentDate: initialDate = new Date() }) => {
+ const [currentDate, setCurrentDate] = useState(new Date(initialDate))
+
+ // 将日记按日期分组
+ const postsByDate = new Map()
+ posts.forEach((post) => {
+ const date = getFormattedDate(new Date(post.data.date), {
+ year: 'numeric',
+ month: '2-digit',
+ day: '2-digit'
+ })
+ if (!postsByDate.has(date)) {
+ postsByDate.set(date, [])
+ }
+ postsByDate.get(date)?.push(post)
+ })
+
+ // 获取所有有日记的月份
+ const months = posts.map((post) => {
+ const date = new Date(post.data.date)
+ return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`
+ })
+ const uniqueMonths = Array.from(new Set(months)).sort()
+
+ const getCurrentMonthStr = (): string => {
+ return `${currentDate.getFullYear()}-${String(currentDate.getMonth() + 1).padStart(2, '0')}`
+ }
+
+ const canNavigateToMonth = (direction: 'prev' | 'next'): boolean => {
+ const currentMonthStr = getCurrentMonthStr()
+ if (direction === 'prev') {
+ return uniqueMonths.some((m) => m < currentMonthStr)
+ } else {
+ const now = new Date()
+ const currentMonth = new Date(currentDate.getFullYear(), currentDate.getMonth())
+ const thisMonth = new Date(now.getFullYear(), now.getMonth())
+ return currentMonth < thisMonth
+ }
+ }
+
+ const renderCalendarDays = () => {
+ // 获取当月的第一天和最后一天
+ const firstDayOfMonth = new Date(currentDate.getFullYear(), currentDate.getMonth(), 1)
+ const lastDayOfMonth = new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, 0)
+
+ // 获取当月第一天是星期几
+ const firstDayWeekday = firstDayOfMonth.getDay()
+ const adjustedFirstDayWeekday = firstDayWeekday === 0 ? 7 : firstDayWeekday
+
+ // 计算日历表格需要显示的天数
+ const daysInPrevMonth = adjustedFirstDayWeekday - 1
+ const daysInCurrentMonth = lastDayOfMonth.getDate()
+ const totalDays = Math.ceil((daysInPrevMonth + daysInCurrentMonth) / 7) * 7
+
+ // 获取上个月的最后几天
+ const lastDayOfPrevMonth = new Date(
+ currentDate.getFullYear(),
+ currentDate.getMonth(),
+ 0
+ ).getDate()
+
+ const days = []
+ for (let i = 0; i < totalDays; i++) {
+ const dayNumber = i - daysInPrevMonth + 1
+ const isCurrentMonth = dayNumber > 0 && dayNumber <= daysInCurrentMonth
+ const displayDay = isCurrentMonth
+ ? dayNumber
+ : dayNumber <= 0
+ ? lastDayOfPrevMonth + dayNumber
+ : dayNumber - daysInCurrentMonth
+
+ const date = new Date(
+ currentDate.getFullYear(),
+ isCurrentMonth
+ ? currentDate.getMonth()
+ : dayNumber <= 0
+ ? currentDate.getMonth() - 1
+ : currentDate.getMonth() + 1,
+ displayDay
+ )
+
+ const formattedDate = getFormattedDate(date, {
+ year: 'numeric',
+ month: '2-digit',
+ day: '2-digit'
+ })
+
+ const postsForDay = Array.from(postsByDate.get(formattedDate) || [])
+ const hasPost = postsForDay.length > 0
+
+ days.push(
+
+
+
+ {displayDay}
+
+
+ {hasPost && (
+
+ )}
+
+ )
+ }
+
+ return days
+ }
+
+ const handlePrevMonth = () => {
+ if (canNavigateToMonth('prev')) {
+ setCurrentDate(new Date(currentDate.getFullYear(), currentDate.getMonth() - 1))
+ }
+ }
+
+ const handleNextMonth = () => {
+ if (canNavigateToMonth('next')) {
+ setCurrentDate(new Date(currentDate.getFullYear(), currentDate.getMonth() + 1))
+ }
+ }
+
+ return (
+
+
+
+
+ {currentDate.getFullYear()}年{currentDate.getMonth() + 1}月
+
+
+
+
+
+ {weekDays.map((day) => (
+
+ {day}
+
+ ))}
+ {renderCalendarDays()}
+
+
+ )
+}
diff --git a/src/components/componentsBefore/blog/Hero.astro b/src/components/componentsBefore/blog/Hero.astro
new file mode 100644
index 0000000..1ff5b77
--- /dev/null
+++ b/src/components/componentsBefore/blog/Hero.astro
@@ -0,0 +1,102 @@
+---
+import { Icon } from 'astro-icon/components'
+import type { CollectionEntry } from 'astro:content'
+import Card from '../Card.astro'
+import FormattedDate from '../FormattedDate.astro'
+import Label from '../Label.astro'
+
+interface Props {
+ content: CollectionEntry<'post'>
+ simple?: boolean
+ socialImage?: string
+}
+
+const {
+ content: { data, render },
+ simple = false,
+ socialImage
+} = Astro.props
+const { remarkPluginFrontmatter } = await render()
+---
+
+{
+ socialImage && (
+
+

+
+ )
+}
+
+
+
+ /{' '}
+ {remarkPluginFrontmatter.minutesRead}
+
+
+ {
+ !simple && (
+ <>
+ {data.date_modified ? (
+
+ ) : null}
+
+ >
+ )
+ }
+
+
+ {data.title}
+
+
+{
+ !simple && !!data.tags?.length && (
+
+
+
+ {data.tags?.map((tag) => (
+
+ {tag}
+
+ ))}
+
+
+ )
+}
+
+{
+ data.description && data.description.trim().length > 0 && (
+
+ {data.description}
+
+ )
+}
diff --git a/src/components/componentsBefore/blog/PostPreview.astro b/src/components/componentsBefore/blog/PostPreview.astro
new file mode 100644
index 0000000..9d56cba
--- /dev/null
+++ b/src/components/componentsBefore/blog/PostPreview.astro
@@ -0,0 +1,39 @@
+---
+import type { HTMLTag, Polymorphic } from 'astro/types'
+import type { CollectionEntry } from 'astro:content'
+
+import FormattedDate from '../FormattedDate.astro'
+
+type Props = Polymorphic<{ as: Tag }> & {
+ post: CollectionEntry<'post'>
+ prefix?: string
+ withDesc?: boolean
+ showYear?: boolean
+}
+
+const { as: Tag = 'div', post, prefix = '/blog/', withDesc = false, showYear = false } = Astro.props
+const postDate = post.data.date
+const year = postDate.getFullYear()
+const coverImage = post.data.coverImage
+---
+
+{showYear && {year}
}
+
+
+
+
+ {post.data.draft && (Draft) }
+
+ {post.data.title}
+
+ {
+ withDesc && (
+ {post.data.description}
+ )
+ }
+
+
diff --git a/src/components/componentsBefore/blog/TOC.astro b/src/components/componentsBefore/blog/TOC.astro
new file mode 100644
index 0000000..fd09e1b
--- /dev/null
+++ b/src/components/componentsBefore/blog/TOC.astro
@@ -0,0 +1,31 @@
+---
+import type { MarkdownHeading } from 'astro'
+import { generateToc } from '@/utils'
+
+import TOCHeading from './TOCHeading.astro'
+
+interface Props {
+ headings: MarkdownHeading[]
+}
+
+const { headings } = Astro.props
+
+const toc = generateToc(headings)
+---
+
+
diff --git a/src/components/componentsBefore/blog/TOCHeading.astro b/src/components/componentsBefore/blog/TOCHeading.astro
new file mode 100644
index 0000000..179f69c
--- /dev/null
+++ b/src/components/componentsBefore/blog/TOCHeading.astro
@@ -0,0 +1,152 @@
+---
+import type { TocItem } from '@/utils'
+
+interface Props {
+ heading: TocItem
+}
+
+const {
+ heading: { depth, slug, subheadings, text }
+} = Astro.props
+---
+
+
+
+ {
+ subheadings.length > 0 && (
+
+ )
+ }
+
{text}
+
+ {
+ !!subheadings.length && (
+
+ {subheadings.map((subheading) => (
+
+ ))}
+
+ )
+ }
+
+
+
+
+
diff --git a/src/components/componentsBefore/layout/Footer.astro b/src/components/componentsBefore/layout/Footer.astro
new file mode 100644
index 0000000..e2c21bd
--- /dev/null
+++ b/src/components/componentsBefore/layout/Footer.astro
@@ -0,0 +1,72 @@
+---
+import { Icon } from 'astro-icon/components'
+import PageViews from '../PageViews'
+
+const pathname = new URL(Astro.request.url).pathname
+const slug = pathname === '/' ? 'home' : pathname.replace(/^\/|\/$/g, '')
+---
+
+
diff --git a/src/components/componentsBefore/layout/Header.astro b/src/components/componentsBefore/layout/Header.astro
new file mode 100644
index 0000000..98888c6
--- /dev/null
+++ b/src/components/componentsBefore/layout/Header.astro
@@ -0,0 +1,277 @@
+---
+import kazootttAvatar from '../../assets/kazoottt-avatar.jpeg'
+import { Image } from 'astro:assets'
+---
+
+
+
+
diff --git a/src/components/tools/index.astro b/src/components/tools/index.astro
new file mode 100644
index 0000000..775bb1a
--- /dev/null
+++ b/src/components/tools/index.astro
@@ -0,0 +1,384 @@
+---
+import CategorySection from "@/components/componentsBefore/CategorySection.astro";
+
+const SOFTWARE_TOOLS = {
+ development: {
+ title: "Development",
+ sections: {
+ ide: {
+ title: "IDE & Editors",
+ tools: [
+ {
+ name: "Cursor",
+ description: "AI-Powered Code Editor",
+ href: "https://cursor.sh/",
+ iconPath: "cursor",
+ },
+ {
+ name: "VS Code",
+ description: "Code Editor",
+ href: "https://code.visualstudio.com/",
+ iconPath: "vscode",
+ },
+ {
+ name: "JetBrains Suite",
+ description: "Professional IDEs",
+ href: "https://www.jetbrains.com/",
+ },
+ ],
+ },
+ },
+ },
+ design: {
+ title: "Design & Creative",
+ sections: {
+ design: {
+ title: "Design Software",
+ tools: [
+ {
+ name: "Figma",
+ description: "Design Tool",
+ href: "https://www.figma.com/",
+ iconPath: "figma",
+ },
+ {
+ name: "Canva",
+ description: "Design Tool",
+ href: "https://www.canva.com/",
+ iconPath: "canva",
+ },
+ ],
+ },
+ recording: {
+ title: "Screen Recording & Screenshots",
+ tools: [
+ {
+ name: "CleanShot X",
+ description: "Screenshot & Recording",
+ href: "https://cleanshot.com/",
+ },
+ {
+ name: "OBS Studio",
+ description: "Streaming & Recording",
+ href: "https://obsproject.com/",
+ },
+ {
+ name: "Picsew",
+ description: "Screenshot Tool",
+ href: "https://apps.apple.com/app/picsew-screenshot-stitching/id1208145167",
+ },
+ {
+ name: "shottr",
+ description: "Screenshot Tool",
+ href: "https://shottr.cc/",
+ },
+ {
+ name: "QuickRecorder",
+ description: "Screen Recording",
+ href: "https://lihaoyun6.github.io/quickrecorder/",
+ },
+ ],
+ },
+ },
+ },
+ productivity: {
+ title: "Productivity",
+ sections: {
+ browser: {
+ title: "Browsers",
+ tools: [
+ {
+ name: "Arc Browser",
+ description: "Modern Browser",
+ href: "https://arc.net/",
+ iconPath: "arc",
+ },
+ {
+ name: "Google Chrome",
+ description: "Web Browser",
+ href: "https://www.google.com/chrome/",
+ },
+ {
+ name: "Microsoft Edge",
+ description: "Web Browser",
+ href: "https://www.microsoft.com/edge",
+ },
+ ],
+ },
+ notes: {
+ title: "Note Taking",
+ tools: [
+ {
+ name: "Obsidian",
+ description: "Note Taking",
+ href: "https://obsidian.md/",
+ iconPath: "obsidian",
+ },
+ {
+ name: "Notion",
+ description: "Note Taking",
+ href: "https://notion.so/",
+ iconPath: "notion",
+ },
+ {
+ name: "Flomo",
+ description: "Quick Notes",
+ href: "https://flomoapp.com/",
+ },
+ ],
+ },
+ tools: {
+ title: "Productivity Tools",
+ tools: [
+ {
+ name: "1Password",
+ description: "Password Manager",
+ href: "https://1password.com/",
+ },
+ {
+ name: "Raindrop.io",
+ description: "Bookmark Manager",
+ href: "https://raindrop.io/",
+ },
+ {
+ name: "n8n",
+ description: "Workflow Automation",
+ href: "https://n8n.io/",
+ },
+ {
+ name: "Follow",
+ description: "RSS Reader",
+ href: "https://app.follow.is/",
+ },
+ {
+ name: "滴答清单",
+ description: "Task Management",
+ href: "https://dida365.com/",
+ },
+ ],
+ },
+ },
+ },
+ ai: {
+ title: "AI Tools",
+ sections: {
+ assistants: {
+ title: "AI Assistants",
+ tools: [
+ {
+ name: "ChatGPT",
+ description: "AI Assistant",
+ href: "https://chat.openai.com/",
+ iconPath: "chatgpt",
+ },
+ {
+ name: "Claude",
+ description: "AI Assistant",
+ href: "https://claude.ai/",
+ },
+ {
+ name: "Poe",
+ description: "AI Platform",
+ href: "https://poe.com/",
+ },
+ {
+ name: "Google Notebook LLM",
+ description: "AI Assistant",
+ href: "https://notebooklm.google.com/",
+ },
+ ],
+ },
+ },
+ },
+ media: {
+ title: "Media & Entertainment",
+ sections: {
+ music: {
+ title: "Music",
+ tools: [
+ {
+ name: "Apple Music",
+ description: "Music Streaming",
+ href: "https://music.apple.com/",
+ },
+ {
+ name: "网易云音乐",
+ description: "Music Platform",
+ href: "https://music.163.com/",
+ },
+ ],
+ },
+ reading: {
+ title: "Reading & Writing",
+ tools: [
+ {
+ name: "微信读书",
+ description: "Reading Platform",
+ href: "https://weread.qq.com/",
+ },
+ {
+ name: "Personal Blog",
+ description: "Built with Astro",
+ href: "/",
+ },
+ {
+ name: "Hashnode",
+ description: "Blog Platform",
+ href: "https://hashnode.com/",
+ },
+ ],
+ },
+ },
+ },
+ health: {
+ title: "Health & Fitness",
+ sections: {
+ tracking: {
+ title: "Health Tracking",
+ tools: [
+ {
+ name: "AutoSleep",
+ description: "Sleep Tracking",
+ href: "https://autosleep.app/",
+ },
+ {
+ name: "Grow",
+ description: "Health Tracking",
+ href: "https://apps.apple.com/cn/app/grow-%E4%BD%A0%E7%9A%84%E5%81%A5%E5%BA%B7%E8%B4%B4%E5%BF%83%E5%A5%BD%E4%BC%99%E4%BC%B4/id1560604814",
+ },
+ {
+ name: "Keep",
+ description: "Fitness App",
+ href: "https://www.gotokeep.com/",
+ },
+ ],
+ },
+ },
+ },
+};
+
+const DEVICES = {
+ computing: {
+ title: "Computing Devices",
+ sections: {
+ devices: {
+ title: "Devices",
+ tools: [
+ {
+ name: "Mac Mini M2 Pro",
+ description: "主力机 (32GB, 512GB)",
+ iconPath: "apple",
+ },
+ {
+ name: "MacBook Air M1",
+ description: "移动办公 (16GB, 256GB)",
+ iconPath: "apple",
+ },
+ {
+ name: "机械师整机",
+ description: "台式机 (i5, 32GB, 512GB)",
+ iconPath: "windows",
+ },
+ ],
+ },
+ },
+ },
+ mobile: {
+ title: "Mobile Devices",
+ sections: {
+ phone: {
+ title: "Phone",
+ tools: [
+ {
+ name: "iPhone 13",
+ description: "手机 (256GB)",
+ iconPath: "apple",
+ },
+ ],
+ },
+ tablet: {
+ title: "Tablet",
+ tools: [
+ {
+ name: "iPad Mini 5",
+ description: "平板",
+ iconPath: "apple",
+ },
+ ],
+ },
+ },
+ },
+ wearables: {
+ title: "Smart Wearables",
+ sections: {
+ watch: {
+ title: "Smart Watch",
+ tools: [
+ {
+ name: "Apple Watch S9",
+ description: "智能手表",
+ iconPath: "apple",
+ },
+ ],
+ },
+ audio: {
+ title: "Audio Devices",
+ tools: [
+ {
+ name: "AirPods Pro 2",
+ description: "无线耳机",
+ iconPath: "apple",
+ },
+ ],
+ },
+ },
+ },
+ imaging: {
+ title: "Imaging Equipment",
+ sections: {
+ cameras: {
+ title: "Camera System",
+ tools: [
+ {
+ name: "松下 GX9",
+ description: "相机",
+ iconPath: "round-photo",
+ },
+ {
+ name: "Panasonic 14-140mm",
+ description: "变焦镜头",
+ iconPath: "tool",
+ },
+ {
+ name: "Panasonic 25mm",
+ description: "定焦镜头",
+ iconPath: "tool",
+ },
+ {
+ name: "Panasonic 100-300mm",
+ description: "长焦镜头",
+ iconPath: "tool",
+ },
+ {
+ name: "DJI Action 5 Pro",
+ description: "运动相机",
+ iconPath: "round-photo",
+ },
+ ],
+ },
+ },
+ },
+};
+---
+
+
+
+
+
Tools & Devices
+
Tools, software, and devices I use daily
+
+
+
+
+
+
diff --git a/src/content/note/2023 bw汇报.md b/src/content/note/2023 bw汇报.md
index 83d429e..313c5f7 100644
--- a/src/content/note/2023 bw汇报.md
+++ b/src/content/note/2023 bw汇报.md
@@ -3,7 +3,7 @@ title: 2023 bw汇报
date: 2024-01-07
author: KazooTTT
tags:
- - '2023'
+ - "2023"
- bw
- hanser
published: true
diff --git a/src/icons/apple.svg b/src/icons/apple.svg
new file mode 100644
index 0000000..36c16e9
--- /dev/null
+++ b/src/icons/apple.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/icons/arc.svg b/src/icons/arc.svg
new file mode 100644
index 0000000..5239e41
--- /dev/null
+++ b/src/icons/arc.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/icons/arrow-right.svg b/src/icons/arrow-right.svg
new file mode 100644
index 0000000..40c155c
--- /dev/null
+++ b/src/icons/arrow-right.svg
@@ -0,0 +1,5 @@
+
+
diff --git a/src/icons/canva.svg b/src/icons/canva.svg
new file mode 100644
index 0000000..d8f9746
--- /dev/null
+++ b/src/icons/canva.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/icons/category.svg b/src/icons/category.svg
new file mode 100644
index 0000000..07a1680
--- /dev/null
+++ b/src/icons/category.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/icons/chatgpt.svg b/src/icons/chatgpt.svg
new file mode 100644
index 0000000..0af9f56
--- /dev/null
+++ b/src/icons/chatgpt.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/icons/cursor.svg b/src/icons/cursor.svg
new file mode 100644
index 0000000..1d82a5c
--- /dev/null
+++ b/src/icons/cursor.svg
@@ -0,0 +1,20 @@
+
+
diff --git a/src/icons/figma.svg b/src/icons/figma.svg
new file mode 100644
index 0000000..90f3193
--- /dev/null
+++ b/src/icons/figma.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/icons/github.svg b/src/icons/github.svg
new file mode 100644
index 0000000..2210301
--- /dev/null
+++ b/src/icons/github.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/icons/link.svg b/src/icons/link.svg
new file mode 100644
index 0000000..5210d86
--- /dev/null
+++ b/src/icons/link.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/icons/mail.svg b/src/icons/mail.svg
new file mode 100644
index 0000000..3f57585
--- /dev/null
+++ b/src/icons/mail.svg
@@ -0,0 +1,4 @@
+
+
diff --git a/src/icons/notion.svg b/src/icons/notion.svg
new file mode 100644
index 0000000..c8dafcb
--- /dev/null
+++ b/src/icons/notion.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/icons/obsidian.svg b/src/icons/obsidian.svg
new file mode 100644
index 0000000..a783084
--- /dev/null
+++ b/src/icons/obsidian.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/icons/progress.svg b/src/icons/progress.svg
new file mode 100644
index 0000000..bc7febd
--- /dev/null
+++ b/src/icons/progress.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/icons/round-photo.svg b/src/icons/round-photo.svg
new file mode 100644
index 0000000..54d1c16
--- /dev/null
+++ b/src/icons/round-photo.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/icons/rss.svg b/src/icons/rss.svg
new file mode 100644
index 0000000..87a927a
--- /dev/null
+++ b/src/icons/rss.svg
@@ -0,0 +1,5 @@
+
diff --git a/src/icons/tiktok.svg b/src/icons/tiktok.svg
new file mode 100644
index 0000000..92ba5e4
--- /dev/null
+++ b/src/icons/tiktok.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/icons/tool.svg b/src/icons/tool.svg
new file mode 100644
index 0000000..ad49c07
--- /dev/null
+++ b/src/icons/tool.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/icons/twitter.svg b/src/icons/twitter.svg
new file mode 100644
index 0000000..04e58cf
--- /dev/null
+++ b/src/icons/twitter.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/icons/vscode.svg b/src/icons/vscode.svg
new file mode 100644
index 0000000..6a6b864
--- /dev/null
+++ b/src/icons/vscode.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/icons/windows.svg b/src/icons/windows.svg
new file mode 100644
index 0000000..265606a
--- /dev/null
+++ b/src/icons/windows.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/pages/about.astro b/src/pages/about.astro
index 463abcc..8213ef5 100644
--- a/src/pages/about.astro
+++ b/src/pages/about.astro
@@ -1,8 +1,8 @@
---
import PageLayout from "@/layouts/Base.astro";
-
+import Tools from "@/components/tools/index.astro";
const meta = {
- description: "I'm a starter theme for Astro.build",
+ description: "introduction for KazooTTT",
title: "About",
};
---
@@ -10,4 +10,14 @@ const meta = {
About
TODO ...
+
+

+
+
+
diff --git a/src/utils/tailwind.ts b/src/utils/tailwind.ts
new file mode 100644
index 0000000..3200be2
--- /dev/null
+++ b/src/utils/tailwind.ts
@@ -0,0 +1,6 @@
+import { clsx, type ClassValue } from "clsx";
+import { twMerge } from "tailwind-merge";
+
+export function cn(...inputs: ClassValue[]) {
+ return twMerge(clsx(inputs));
+}