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 && ( + {altText} + ) + } +
+
+

{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]) => ( +
+ + {category} + + + - {count} post{count > 1 && 's'} + +
+ ))} +
+ )} + + {/* Tree View */} + {viewMode === 'tree' && ( +
+ {allCategoriesHierarchy.map((category) => ( +
+ {/* Render root-level categories */} +
+ + {category.category} + + + - {category.count} post{category.count > 1 && 's'} + +
+ + {/* Render nested categories with indentation */} + {category.children && Object.values(category.children).length > 0 && ( +
+ {Object.values(category.children).map((childCategory) => ( +
+ + {childCategory.category} + + + - {childCategory.count} post{childCategory.count > 1 && 's'} + +
+ ))} +
+ )} +
+ ))} +
+ )} +
+ ) +} + +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 && ( + {altText} + ) + } +
+

{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; +--- + +
+

{title}

+
+ { + tools.map((tool) => ( + + +
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 && ( +
+ {postsForDay.map((post: Post) => ( + + {post.data.title} + + ))} +
+ )} +
+ ) + } + + 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 ...
    +
    + Total time coded since Nov 4 2017 +
    + +
    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)); +}