Refactor project structure and update dependencies; improve code formatting and consistency across multiple components and files

This commit is contained in:
KazooTTT
2025-01-08 00:01:03 +08:00
parent 3c8549fd14
commit 11c193786d
16 changed files with 5571 additions and 6577 deletions

11689
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -2,38 +2,38 @@
import ToolSection from './ToolSection.astro' import ToolSection from './ToolSection.astro'
interface Props { interface Props {
title: string title: string
categories: { categories: {
title: string title: string
sections: { sections: {
[key: string]: { [key: string]: {
title: string title: string
tools: { tools: {
name: string name: string
description: string description: string
href?: string href?: string
iconPath?: string iconPath?: string
}[] }[]
} }
} }
}[] }[]
} }
const { title, categories } = Astro.props const { title, categories } = Astro.props
--- ---
<div> <div>
<h2 class='mb-6 text-xl font-bold'>{title}</h2> <h2 class='mb-6 text-xl font-bold'>{title}</h2>
{ {
categories.map((category) => ( categories.map((category) => (
<div class='mb-8'> <div class='mb-8'>
<h3 class='mb-4 text-lg font-semibold'>{category.title}</h3> <h3 class='mb-4 text-lg font-semibold'>{category.title}</h3>
{Object.values(category.sections).map((section) => ( {Object.values(category.sections).map((section) => (
<div class='mb-6'> <div class='mb-6'>
<ToolSection title={section.title} tools={section.tools} /> <ToolSection title={section.title} tools={section.tools} />
</div> </div>
))} ))}
</div> </div>
)) ))
} }
</div> </div>

View File

@ -1,9 +1,6 @@
import GitHubCalendar from "react-github-calendar"; import GitHubCalendar from 'react-github-calendar'
const GithubHotLine = () => { const GithubHotLine = () => {
return ( return <GitHubCalendar username='kazoottt' />
<GitHubCalendar username='kazoottt' /> }
);
};
export default GithubHotLine
export default GithubHotLine

View File

@ -33,17 +33,19 @@ if (imagePath) {
href={href} href={href}
target={target} target={target}
> >
{imageComponent && ( {
<Image imageComponent && (
src={imageComponent()} <Image
alt={altText} src={imageComponent()}
class='h-32 w-full rounded-2xl rounded-bl-none rounded-br-none object-cover' alt={altText}
loading='eager' class='h-32 w-full rounded-2xl rounded-bl-none rounded-br-none object-cover'
/> loading='eager'
)} />
<div class='flex flex-col gap-y-2 flex-1 px-5 py-4 text-center justify-center w-full'> )
}
<div class='flex w-full flex-1 flex-col justify-center gap-y-2 px-5 py-4 text-center'>
<h1 class='text-lg font-medium'>{heading}</h1> <h1 class='text-lg font-medium'>{heading}</h1>
<h2 class='text-muted-foreground line-clamp-2 text-sm'>{subheading}</h2> <h2 class='line-clamp-2 text-sm text-muted-foreground'>{subheading}</h2>
</div> </div>
<slot /> <slot />

View File

@ -13,12 +13,14 @@ const { headings } = Astro.props
const toc = generateToc(headings) const toc = generateToc(headings)
--- ---
<aside class='sticky top-20 order-2 -me-28 hidden h-[calc(100vh-6rem)] basis-60 lg:flex lg:flex-col'> <aside
class='sticky top-20 order-2 -me-28 hidden h-[calc(100vh-6rem)] basis-60 lg:flex lg:flex-col'
>
{ {
toc.length > 0 && ( toc.length > 0 && (
<> <>
<h2 class='mb-4 font-semibold'>TABLE OF CONTENTS</h2> <h2 class='mb-4 font-semibold'>TABLE OF CONTENTS</h2>
<ul class='text-card-foreground overflow-y-auto pr-4 max-h-[calc(100vh-10rem)]'> <ul class='max-h-[calc(100vh-10rem)] overflow-y-auto pr-4 text-card-foreground'>
{toc.map((heading) => ( {toc.map((heading) => (
<TOCHeading heading={heading} /> <TOCHeading heading={heading} />
))} ))}

View File

@ -195,20 +195,22 @@ import { Image } from 'astro:assets'
height='32' height='32'
viewBox='0 0 24 24' viewBox='0 0 24 24'
class='ml-2 h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:hidden dark:-rotate-90 dark:scale-0' class='ml-2 h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:hidden dark:-rotate-90 dark:scale-0'
><path ><path
fill='currentColor' fill='currentColor'
d='M12 15q1.25 0 2.125-.875T15 12q0-1.25-.875-2.125T12 9q-1.25 0-2.125.875T9 12q0 1.25.875 2.125T12 15m0 1q-1.671 0-2.836-1.164T8 12q0-1.671 1.164-2.836T12 8q1.671 0 2.836 1.164T16 12q0 1.671-1.164 2.836T12 16m-7-3.5H1.5v-1H5zm17.5 0H19v-1h3.5zM11.5 5V1.5h1V5zm0 17.5V19h1v3.5zM6.746 7.404l-2.16-2.098l.695-.744l2.111 2.134zM18.72 19.438l-2.117-2.14l.652-.702l2.16 2.098zM16.596 6.746l2.098-2.16l.744.695l-2.134 2.111zM4.562 18.72l2.14-2.117l.663.652l-2.078 2.179zM12 12' d='M12 15q1.25 0 2.125-.875T15 12q0-1.25-.875-2.125T12 9q-1.25 0-2.125.875T9 12q0 1.25.875 2.125T12 15m0 1q-1.671 0-2.836-1.164T8 12q0-1.671 1.164-2.836T12 8q1.671 0 2.836 1.164T16 12q0 1.671-1.164 2.836T12 16m-7-3.5H1.5v-1H5zm17.5 0H19v-1h3.5zM11.5 5V1.5h1V5zm0 17.5V19h1v3.5zM6.746 7.404l-2.16-2.098l.695-.744l2.111 2.134zM18.72 19.438l-2.117-2.14l.652-.702l2.16 2.098zM16.596 6.746l2.098-2.16l.744.695l-2.134 2.111zM4.562 18.72l2.14-2.117l.663.652l-2.078 2.179zM12 12'
></path></svg> ></path></svg
>
<svg <svg
xmlns='http://www.w3.org/2000/svg' xmlns='http://www.w3.org/2000/svg'
width='32' width='32'
height='32' height='32'
viewBox='0 0 24 24' viewBox='0 0 24 24'
class='ml-2 hidden h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:block dark:rotate-0 dark:scale-100' class='ml-2 hidden h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:block dark:rotate-0 dark:scale-100'
><path ><path
fill='currentColor' fill='currentColor'
d='M12.058 20q-3.334 0-5.667-2.333Q4.058 15.333 4.058 12q0-3.038 1.98-5.27Q8.02 4.5 10.942 4.097q.081 0 .159.006t.153.017q-.506.706-.801 1.57q-.295.865-.295 1.811q0 2.667 1.866 4.533q1.867 1.867 4.534 1.867q.952 0 1.813-.295q.862-.295 1.548-.801q.012.075.018.153q.005.078.005.158q-.384 2.923-2.615 4.904T12.057 20' d='M12.058 20q-3.334 0-5.667-2.333Q4.058 15.333 4.058 12q0-3.038 1.98-5.27Q8.02 4.5 10.942 4.097q.081 0 .159.006t.153.017q-.506.706-.801 1.57q-.295.865-.295 1.811q0 2.667 1.866 4.533q1.867 1.867 4.534 1.867q.952 0 1.813-.295q.862-.295 1.548-.801q.012.075.018.153q.005.078.005.158q-.384 2.923-2.615 4.904T12.057 20'
></path></svg> ></path></svg
>
</button> </button>
</div> </div>
</div> </div>
@ -223,7 +225,7 @@ import { Image } from 'astro:assets'
function setupDarkModeToggle() { function setupDarkModeToggle() {
const toggleDarkModeButton = document.getElementById('toggleDarkMode') const toggleDarkModeButton = document.getElementById('toggleDarkMode')
const mobileToggleDarkMode = document.getElementById('mobileToggleDarkMode') const mobileToggleDarkMode = document.getElementById('mobileToggleDarkMode')
function toggleTheme() { function toggleTheme() {
const currentTheme = getCurrentTheme() const currentTheme = getCurrentTheme()
const newTheme = currentTheme === 'dark' ? 'light' : 'dark' const newTheme = currentTheme === 'dark' ? 'light' : 'dark'

View File

@ -23,7 +23,14 @@ const articleDate = date?.toISOString()
const { headings } = await post.render() const { headings } = await post.render()
--- ---
<PageLayout meta={{ articleDate, description: description ?? '', ogImage: socialImage, title }}> <PageLayout
meta={{
articleDate,
description: description ?? '',
ogImage: socialImage,
title
}}
>
<div class='w-full'> <div class='w-full'>
<Button title='Back' href={backHref} style='button'> <Button title='Back' href={backHref} style='button'>
<svg <svg

View File

@ -1,163 +1,176 @@
import type { APIRoute } from 'astro'; import type { APIRoute } from 'astro'
export const GET: APIRoute = async ({ params, locals, request }) => { export const GET: APIRoute = async ({ params, locals, request }) => {
// Handle multiple path segments and decode the URL-encoded slug // Handle multiple path segments and decode the URL-encoded slug
const slugSegments = params.slug?.split('/') || []; const slugSegments = params.slug?.split('/') || []
const slug = decodeURIComponent(slugSegments.join('/')); const slug = decodeURIComponent(slugSegments.join('/'))
if (!slug) {
return new Response(JSON.stringify({ error: 'Slug is required' }), {
status: 400,
headers: { 'Content-Type': 'application/json' },
});
}
// Access runtime environment through context if (!slug) {
const ctx = locals as any; return new Response(JSON.stringify({ error: 'Slug is required' }), {
const env = ctx.env || ctx.runtime?.env; status: 400,
headers: { 'Content-Type': 'application/json' }
console.log('Context:', { })
hasEnv: !!ctx.env, }
hasRuntime: !!ctx.runtime,
envKeys: ctx.env ? Object.keys(ctx.env) : [],
runtimeEnvKeys: ctx.runtime?.env ? Object.keys(ctx.runtime.env) : []
});
const db = env?.DB; // Access runtime environment through context
if (!db) { const ctx = locals as any
console.error('D1 database not available'); const env = ctx.env || ctx.runtime?.env
return new Response(
JSON.stringify({
error: 'Database not configured',
context: {
hasEnv: !!ctx.env,
hasRuntime: !!ctx.runtime,
envKeys: ctx.env ? Object.keys(ctx.env) : [],
runtimeEnvKeys: ctx.runtime?.env ? Object.keys(ctx.runtime.env) : []
}
}), {
status: 500,
headers: { 'Content-Type': 'application/json' },
});
}
try { console.log('Context:', {
// Get current view count hasEnv: !!ctx.env,
const { results } = await db hasRuntime: !!ctx.runtime,
.prepare('SELECT views FROM pageviews WHERE slug = ?') envKeys: ctx.env ? Object.keys(ctx.env) : [],
.bind(slug) runtimeEnvKeys: ctx.runtime?.env ? Object.keys(ctx.runtime.env) : []
.all(); })
const views = results.length > 0 ? results[0].views : 0; const db = env?.DB
if (!db) {
console.error('D1 database not available')
return new Response(
JSON.stringify({
error: 'Database not configured',
context: {
hasEnv: !!ctx.env,
hasRuntime: !!ctx.runtime,
envKeys: ctx.env ? Object.keys(ctx.env) : [],
runtimeEnvKeys: ctx.runtime?.env ? Object.keys(ctx.runtime.env) : []
}
}),
{
status: 500,
headers: { 'Content-Type': 'application/json' }
}
)
}
return new Response(JSON.stringify({ views }), { try {
status: 200, // Get current view count
headers: { const { results } = await db
'Content-Type': 'application/json', .prepare('SELECT views FROM pageviews WHERE slug = ?')
}, .bind(slug)
}); .all()
} catch (error) {
console.error('Error getting page views:', error); const views = results.length > 0 ? results[0].views : 0
return new Response(
JSON.stringify({ return new Response(JSON.stringify({ views }), {
error: 'Failed to get page views', status: 200,
details: error instanceof Error ? error.message : String(error) headers: {
}), { 'Content-Type': 'application/json'
status: 500, }
headers: { })
'Content-Type': 'application/json', } catch (error) {
}, console.error('Error getting page views:', error)
}); return new Response(
} JSON.stringify({
}; error: 'Failed to get page views',
details: error instanceof Error ? error.message : String(error)
}),
{
status: 500,
headers: {
'Content-Type': 'application/json'
}
}
)
}
}
export const POST: APIRoute = async ({ params, locals, request }) => { export const POST: APIRoute = async ({ params, locals, request }) => {
// Handle multiple path segments and decode the URL-encoded slug // Handle multiple path segments and decode the URL-encoded slug
const slugSegments = params.slug?.split('/') || []; const slugSegments = params.slug?.split('/') || []
const slug = decodeURIComponent(slugSegments.join('/')); const slug = decodeURIComponent(slugSegments.join('/'))
if (!slug) {
return new Response(JSON.stringify({ error: 'Slug is required' }), {
status: 400,
headers: { 'Content-Type': 'application/json' },
});
}
// Access runtime environment through context if (!slug) {
const ctx = locals as any; return new Response(JSON.stringify({ error: 'Slug is required' }), {
const env = ctx.env || ctx.runtime?.env; status: 400,
headers: { 'Content-Type': 'application/json' }
console.log('Context:', { })
hasEnv: !!ctx.env, }
hasRuntime: !!ctx.runtime,
envKeys: ctx.env ? Object.keys(ctx.env) : [],
runtimeEnvKeys: ctx.runtime?.env ? Object.keys(ctx.runtime.env) : []
});
const db = env?.DB; // Access runtime environment through context
if (!db) { const ctx = locals as any
console.error('D1 database not available'); const env = ctx.env || ctx.runtime?.env
return new Response(
JSON.stringify({
error: 'Database not configured',
context: {
hasEnv: !!ctx.env,
hasRuntime: !!ctx.runtime,
envKeys: ctx.env ? Object.keys(ctx.env) : [],
runtimeEnvKeys: ctx.runtime?.env ? Object.keys(ctx.runtime.env) : []
}
}), {
status: 500,
headers: { 'Content-Type': 'application/json' },
});
}
try { console.log('Context:', {
// Get request information hasEnv: !!ctx.env,
const ip_address = request.headers.get('cf-connecting-ip') || request.headers.get('x-forwarded-for') || 'unknown'; hasRuntime: !!ctx.runtime,
const user_agent = request.headers.get('user-agent') || 'unknown'; envKeys: ctx.env ? Object.keys(ctx.env) : [],
const referrer = request.headers.get('referer') || 'unknown'; runtimeEnvKeys: ctx.runtime?.env ? Object.keys(ctx.runtime.env) : []
})
// Start a transaction to ensure both operations succeed or fail together const db = env?.DB
const stmt1 = db.prepare( if (!db) {
`INSERT INTO pageviews (slug, views) console.error('D1 database not available')
return new Response(
JSON.stringify({
error: 'Database not configured',
context: {
hasEnv: !!ctx.env,
hasRuntime: !!ctx.runtime,
envKeys: ctx.env ? Object.keys(ctx.env) : [],
runtimeEnvKeys: ctx.runtime?.env ? Object.keys(ctx.runtime.env) : []
}
}),
{
status: 500,
headers: { 'Content-Type': 'application/json' }
}
)
}
try {
// Get request information
const ip_address =
request.headers.get('cf-connecting-ip') || request.headers.get('x-forwarded-for') || 'unknown'
const user_agent = request.headers.get('user-agent') || 'unknown'
const referrer = request.headers.get('referer') || 'unknown'
// Start a transaction to ensure both operations succeed or fail together
const stmt1 = db
.prepare(
`INSERT INTO pageviews (slug, views)
VALUES (?, 1) VALUES (?, 1)
ON CONFLICT(slug) ON CONFLICT(slug)
DO UPDATE SET views = views + 1, DO UPDATE SET views = views + 1,
updated_at = CURRENT_TIMESTAMP` updated_at = CURRENT_TIMESTAMP`
).bind(slug); )
.bind(slug)
const stmt2 = db.prepare( const stmt2 = db
`INSERT INTO pageview_logs (slug, ip_address, user_agent, referrer) .prepare(
`INSERT INTO pageview_logs (slug, ip_address, user_agent, referrer)
VALUES (?, ?, ?, ?)` VALUES (?, ?, ?, ?)`
).bind(slug, ip_address, user_agent, referrer); )
.bind(slug, ip_address, user_agent, referrer)
// Execute both statements in a transaction // Execute both statements in a transaction
await db.batch([stmt1, stmt2]); await db.batch([stmt1, stmt2])
// Get updated view count // Get updated view count
const { results } = await db const { results } = await db
.prepare('SELECT views FROM pageviews WHERE slug = ?') .prepare('SELECT views FROM pageviews WHERE slug = ?')
.bind(slug) .bind(slug)
.all(); .all()
return new Response(JSON.stringify({ views: results[0].views }), { return new Response(JSON.stringify({ views: results[0].views }), {
status: 200, status: 200,
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json'
}, }
}); })
} catch (error) { } catch (error) {
console.error('Error updating page views:', error); console.error('Error updating page views:', error)
return new Response( return new Response(
JSON.stringify({ JSON.stringify({
error: 'Failed to update page views', error: 'Failed to update page views',
details: error instanceof Error ? error.message : String(error) details: error instanceof Error ? error.message : String(error)
}), { }),
status: 500, {
headers: { status: 500,
'Content-Type': 'application/json', headers: {
}, 'Content-Type': 'application/json'
}); }
} }
}; )
}
}

View File

@ -20,7 +20,10 @@ export const getStaticPaths = (async ({ paginate }) => {
const allPostsByDate = sortMDByDate(allPosts) const allPostsByDate = sortMDByDate(allPosts)
const uniqueTags = getUniqueTagsWithCount(allPosts) const uniqueTags = getUniqueTagsWithCount(allPosts)
const uniqueCategories = getUniqueCategoriesWithCount(allPosts) const uniqueCategories = getUniqueCategoriesWithCount(allPosts)
return paginate(allPostsByDate, { pageSize: 50, props: { uniqueTags, uniqueCategories } }) return paginate(allPostsByDate, {
pageSize: 50,
props: { uniqueTags, uniqueCategories }
})
}) satisfies GetStaticPaths }) satisfies GetStaticPaths
interface Props { interface Props {

View File

@ -5,7 +5,6 @@ import type { GetStaticPaths, InferGetStaticPropsType } from 'astro'
import PostLayout from '@/layouts/BlogPost.astro' import PostLayout from '@/layouts/BlogPost.astro'
import { getAllPosts } from '@/utils' import { getAllPosts } from '@/utils'
import GiscusComment from '@/components/GiscusComment'
export const getStaticPaths = (async () => { export const getStaticPaths = (async () => {
const blogEntries = await getAllPosts() const blogEntries = await getAllPosts()
@ -21,7 +20,6 @@ const { entry } = Astro.props
const { Content } = await entry.render() const { Content } = await entry.render()
--- ---
<PostLayout post={entry}> <PostLayout post={entry} backHref='/blog'>
<Content /> <Content />
<GiscusComment client:load />
</PostLayout> </PostLayout>

View File

@ -7,7 +7,7 @@ const allPosts = await getAllPosts()
const allCategories = getUniqueCategoriesWithCount(allPosts) const allCategories = getUniqueCategoriesWithCount(allPosts)
const meta = { const meta = {
description: 'A list of all the topics I\'ve written about in my posts', description: "A list of all the topics I've written about in my posts",
title: 'All Categories' title: 'All Categories'
} }
--- ---

View File

@ -4,7 +4,6 @@ export const prerender = true
import type { GetStaticPaths, InferGetStaticPropsType } from 'astro' import type { GetStaticPaths, InferGetStaticPropsType } from 'astro'
import PostLayout from '@/layouts/BlogPost.astro' import PostLayout from '@/layouts/BlogPost.astro'
import GiscusComment from '@/components/GiscusComment'
import { getallDiaries } from 'src/utils' import { getallDiaries } from 'src/utils'
export const getStaticPaths = (async () => { export const getStaticPaths = (async () => {
@ -23,5 +22,4 @@ const { Content } = await entry.render()
<PostLayout post={entry} simple={true} backHref='/diary'> <PostLayout post={entry} simple={true} backHref='/diary'>
<Content /> <Content />
<GiscusComment client:load />
</PostLayout> </PostLayout>

View File

@ -202,7 +202,7 @@ const projects = [...directProjects, ...mdProjects].slice(0, MAX_PROJECTS)
</div> </div>
<div class='mt-4 flex justify-end'> <div class='mt-4 flex justify-end'>
<a <a
href={`/categories/${encodeURIComponent('项目-已结项')}`} href={`/categories/${encodeURIComponent('项目')}`}
class='flex items-center gap-x-1 text-sm text-muted-foreground transition-colors hover:text-foreground' class='flex items-center gap-x-1 text-sm text-muted-foreground transition-colors hover:text-foreground'
> >
查看更多项目 查看更多项目

View File

@ -1,5 +1,5 @@
import type { SiteConfig } from '@/types'; import type { SiteConfig } from '@/types'
import type { AstroExpressiveCodeOptions } from 'astro-expressive-code'; import type { AstroExpressiveCodeOptions } from 'astro-expressive-code'
export const siteConfig: SiteConfig = { export const siteConfig: SiteConfig = {
// Used as both a meta property (src/components/BaseHead.astro L:31 + L:49) & the generated satori png (src/pages/og-image/[slug].png.ts) // Used as both a meta property (src/components/BaseHead.astro L:31 + L:49) & the generated satori png (src/pages/og-image/[slug].png.ts)
@ -21,7 +21,7 @@ export const siteConfig: SiteConfig = {
year: 'numeric' year: 'numeric'
} }
}, },
site: 'https://blog.kazoottt.top', site: 'https://blog.kazoottt.top'
} }
export const menuLinks: Array<{ title: string; path: string }> = [ export const menuLinks: Array<{ title: string; path: string }> = [

View File

@ -24,10 +24,9 @@ export type SiteMeta = {
articleDate?: string | undefined articleDate?: string | undefined
} }
export interface CategoryHierarchy { export interface CategoryHierarchy {
category: string category: string
fullCategory: string fullCategory: string
children: Record<string, CategoryHierarchy> children: Record<string, CategoryHierarchy>
count: number count: number
} }

View File

@ -3,11 +3,15 @@ export { elementHasClass, rootInDarkMode, toggleClass } from './domElement'
export { generateToc } from './generateToc' export { generateToc } from './generateToc'
export type { TocItem } from './generateToc' export type { TocItem } from './generateToc'
export { export {
getAllCategories, getAllPosts, getAllCategories,
getAllSortedPosts, getUniqueCategories, getAllPosts,
getUniqueCategoriesWithCount, getUniqueTags, getAllSortedPosts,
getUniqueTagsWithCount, getAllDiaries as getallDiaries, getUniqueCategories,
getAllDiariesSorted as getallDiariesSorted, sortMDByDate getUniqueCategoriesWithCount,
getUniqueTags,
getUniqueTagsWithCount,
getAllDiaries as getallDiaries,
getAllDiariesSorted as getallDiariesSorted,
sortMDByDate
} from './post' } from './post'
export { cn } from './tailwind' export { cn } from './tailwind'