├── public ├── TANSHI.jpg ├── user-icon.png ├── favicon │ ├── favicon.ico │ ├── favicon-96x96.png │ ├── apple-touch-icon.png │ ├── web-app-manifest-192x192.png │ ├── web-app-manifest-512x512.png │ ├── site.webmanifest │ └── favicon.svg ├── og │ └── opengraph-image.png ├── dark-logo.svg ├── light-logo.svg ├── grid-black.svg └── grid.svg ├── .prettierignore ├── commitlint.config.mjs ├── postcss.config.mjs ├── app ├── api │ ├── route.ts │ ├── auth │ │ └── [...nextauth] │ │ │ ├── route.ts │ │ │ └── authOptions.ts │ ├── articles │ │ ├── all │ │ │ └── route.ts │ │ ├── delete │ │ │ └── route.ts │ │ ├── details │ │ │ └── route.ts │ │ ├── update │ │ │ └── route.ts │ │ ├── articleCountByMonth │ │ │ └── route.ts │ │ ├── add │ │ │ └── route.ts │ │ └── list │ │ │ └── route.ts │ ├── cache-data │ │ └── route.ts │ ├── sync-data │ │ ├── route.ts │ │ └── juejin-data.ts │ ├── feed.xml │ │ └── route.ts │ └── guestbook │ │ └── route.ts ├── (main) │ ├── home │ │ ├── ContentCard.tsx │ │ ├── TechnologyStack.tsx │ │ ├── GithubProject.tsx │ │ ├── JueJinArticles.tsx │ │ └── UserProfile.tsx │ ├── guestbook │ │ ├── Header.tsx │ │ ├── page.tsx │ │ ├── MessagesList.tsx │ │ └── AddMessage.tsx │ ├── page.tsx │ ├── article │ │ ├── [id] │ │ │ ├── anchor │ │ │ │ ├── index.css │ │ │ │ └── index.tsx │ │ │ └── page.tsx │ │ └── list │ │ │ └── page.tsx │ └── layout.tsx ├── providers.tsx ├── article │ ├── components │ │ ├── publish-dialog │ │ │ ├── form-summary-field.tsx │ │ │ ├── form-category-field.tsx │ │ │ ├── index.tsx │ │ │ ├── form.tsx │ │ │ └── form-cover-upload.tsx │ │ └── header.tsx │ ├── add │ │ └── page.tsx │ └── edit │ │ └── [id] │ │ └── page.tsx ├── not-found.tsx ├── layout.tsx ├── auth │ └── verify-request │ │ └── page.tsx ├── actions │ ├── email.ts │ └── image-kit.ts └── globals.css ├── prisma ├── migrations │ ├── migration_lock.toml │ └── 20250907134927_init │ │ └── migration.sql ├── prisma.config.ts └── schema.prisma ├── lib ├── date.ts ├── routers.ts ├── prisma.ts ├── enums.ts ├── juejin │ ├── fetch-user-info.ts │ └── fetch-user-articles.ts ├── utils.ts ├── getReadingTime.ts ├── github │ ├── user-info.ts │ └── pinned-repos.ts └── cache-data.ts ├── .prettierrc ├── components ├── no-found.tsx ├── bytemd │ ├── viewer.tsx │ ├── editor.scss │ ├── plugins.ts │ ├── editor.tsx │ └── dark-theme.scss ├── ui │ ├── skeleton.tsx │ ├── sonner.tsx │ ├── label.tsx │ ├── textarea.tsx │ ├── input.tsx │ ├── avatar.tsx │ ├── tooltip.tsx │ ├── button.tsx │ ├── card.tsx │ ├── dialog.tsx │ ├── form.tsx │ ├── select.tsx │ └── dropdown-menu.tsx ├── base-loading.tsx ├── screen-loading.tsx ├── blog-logo.tsx ├── login-dialog │ ├── GithubLoginButton.tsx │ ├── index.tsx │ └── LoginForm.tsx ├── theme-switch.tsx └── layout │ ├── email-subscription.tsx │ ├── footer.tsx │ └── header.tsx ├── types ├── next-auth.d.ts └── index.d.ts ├── .github └── workflows │ └── sync-data.yml ├── components.json ├── .vscode └── settings.json ├── eslint.config.mjs ├── .gitignore ├── next.config.ts ├── tsconfig.json ├── .env.example ├── README.md ├── next-sitemap.config.js ├── LICENSE ├── proxy.ts └── package.json /public/TANSHI.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vaebe/blog/HEAD/public/TANSHI.jpg -------------------------------------------------------------------------------- /public/user-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vaebe/blog/HEAD/public/user-icon.png -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | .next 4 | dist 5 | pnpm-lock.yaml 6 | components/ui/* -------------------------------------------------------------------------------- /public/favicon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vaebe/blog/HEAD/public/favicon/favicon.ico -------------------------------------------------------------------------------- /public/og/opengraph-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vaebe/blog/HEAD/public/og/opengraph-image.png -------------------------------------------------------------------------------- /public/favicon/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vaebe/blog/HEAD/public/favicon/favicon-96x96.png -------------------------------------------------------------------------------- /commitlint.config.mjs: -------------------------------------------------------------------------------- 1 | const config = { extends: ['@commitlint/config-conventional'] } 2 | export default config 3 | -------------------------------------------------------------------------------- /public/favicon/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vaebe/blog/HEAD/public/favicon/apple-touch-icon.png -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | const config = { 2 | plugins: { 3 | '@tailwindcss/postcss': {} 4 | } 5 | } 6 | export default config 7 | -------------------------------------------------------------------------------- /public/favicon/web-app-manifest-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vaebe/blog/HEAD/public/favicon/web-app-manifest-192x192.png -------------------------------------------------------------------------------- /public/favicon/web-app-manifest-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vaebe/blog/HEAD/public/favicon/web-app-manifest-512x512.png -------------------------------------------------------------------------------- /app/api/route.ts: -------------------------------------------------------------------------------- 1 | import { sendJson } from '@/lib/utils' 2 | 3 | export async function GET() { 4 | return sendJson({ data: 'hello world' }) 5 | } 6 | -------------------------------------------------------------------------------- /prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (e.g., Git) 3 | provider = "mysql" 4 | -------------------------------------------------------------------------------- /lib/date.ts: -------------------------------------------------------------------------------- 1 | import dayjs from 'dayjs' 2 | import 'dayjs/locale/zh-cn' 3 | import relativeTime from 'dayjs/plugin/relativeTime' 4 | 5 | dayjs.locale('zh-cn') 6 | dayjs.extend(relativeTime) 7 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "singleQuote": true, 6 | "semi": false, 7 | "trailingComma": "none", 8 | "bracketSpacing": true 9 | } 10 | -------------------------------------------------------------------------------- /app/api/auth/[...nextauth]/route.ts: -------------------------------------------------------------------------------- 1 | import NextAuth from 'next-auth' 2 | import { authOptions } from './authOptions' 3 | 4 | const handler = NextAuth(authOptions) 5 | export { handler as GET, handler as POST } 6 | -------------------------------------------------------------------------------- /components/no-found.tsx: -------------------------------------------------------------------------------- 1 | function NoFound() { 2 | return ( 3 |
4 |

这里曾经有一些东西 , 现在不见了!

5 |
6 | ) 7 | } 8 | 9 | export { NoFound } 10 | export default NoFound 11 | -------------------------------------------------------------------------------- /prisma/prisma.config.ts: -------------------------------------------------------------------------------- 1 | import 'dotenv/config' 2 | import { defineConfig, env } from 'prisma/config' 3 | 4 | export default defineConfig({ 5 | schema: 'prisma/schema.prisma', 6 | migrations: { 7 | path: 'prisma/migrations', 8 | }, 9 | datasource: { 10 | url: env('DATABASE_URL'), 11 | }, 12 | }) -------------------------------------------------------------------------------- /components/bytemd/viewer.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { Viewer } from '@bytemd/react' 4 | import plugins from '@/components/bytemd/plugins' 5 | import './editor.scss' 6 | import './dark-theme.scss' 7 | 8 | export function BytemdViewer({ content }: { content: string }) { 9 | return 10 | } 11 | -------------------------------------------------------------------------------- /lib/routers.ts: -------------------------------------------------------------------------------- 1 | export const routerList = [ 2 | { 3 | path: '/', 4 | name: '首页', 5 | icon: 'mdi-light:home' 6 | }, 7 | { 8 | path: '/article/list', 9 | name: '文章', 10 | icon: 'ph:article-light' 11 | }, 12 | { 13 | path: '/guestbook', 14 | name: '留言板', 15 | icon: 'mynaui:message-dots' 16 | } 17 | ] 18 | -------------------------------------------------------------------------------- /components/ui/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils" 2 | 3 | function Skeleton({ className, ...props }: React.ComponentProps<"div">) { 4 | return ( 5 |
10 | ) 11 | } 12 | 13 | export { Skeleton } 14 | -------------------------------------------------------------------------------- /app/(main)/home/ContentCard.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react' 2 | 3 | interface Props { 4 | children: ReactNode 5 | title: string 6 | } 7 | 8 | export function ContentCard({ children, title }: Props) { 9 | return ( 10 |
11 |

{title}

12 | {children} 13 |
14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /app/api/articles/all/route.ts: -------------------------------------------------------------------------------- 1 | import { sendJson } from '@/lib/utils' 2 | import { prisma } from '@/lib/prisma' 3 | 4 | export async function GET() { 5 | try { 6 | const articles = await prisma.article.findMany({ 7 | orderBy: { createdAt: 'desc' } 8 | }) 9 | 10 | return sendJson({ data: articles }) 11 | } catch (error) { 12 | return sendJson({ code: -1, msg: `获取所有文章失败:${error}` }) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /types/next-auth.d.ts: -------------------------------------------------------------------------------- 1 | // next-auth.d.ts 2 | import { DefaultSession } from 'next-auth' 3 | 4 | declare module 'next-auth' { 5 | interface Session { 6 | user: { 7 | id: string 8 | name: string | null 9 | email: string | null 10 | image: string | null 11 | role?: string | null // 扩展 Session 类型,添加 role 属性 12 | } 13 | } 14 | interface User { 15 | role?: string | null 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/(main)/guestbook/Header.tsx: -------------------------------------------------------------------------------- 1 | export function Header() { 2 | return ( 3 |
4 |

欢迎来到我的留言板

5 |

6 | 牢记社会主义核心价值观:富强、民主、文明、和谐,自由、平等、公正、法治,爱国、敬业、诚信、友善🤔! 7 |

8 | 9 |

10 | 在这里你可以留下一些内容、对我说的话、建议、你的想法等等一切不违反中国法律的内容! 11 |

12 |
13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /app/api/articles/delete/route.ts: -------------------------------------------------------------------------------- 1 | import { sendJson } from '@/lib/utils' 2 | import { prisma } from '@/lib/prisma' 3 | 4 | export async function DELETE(req: Request) { 5 | try { 6 | const { id } = await req.json() 7 | 8 | await prisma.article.delete({ 9 | where: { id } 10 | }) 11 | 12 | return sendJson({ msg: 'success' }) 13 | } catch (error) { 14 | return sendJson({ code: -1, msg: `删除文章失败:${error}` }) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.github/workflows/sync-data.yml: -------------------------------------------------------------------------------- 1 | name: Sync Data 2 | 3 | on: 4 | schedule: 5 | # 每天凌晨 2 点运行 6 | - cron: '0 2 * * *' 7 | workflow_dispatch: # 允许手动触发 8 | 9 | jobs: 10 | sync-articles: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Start Sync Data 15 | env: 16 | API_KEY: ${{ secrets.API_KEY }} 17 | run: | 18 | curl -X GET "https://blog.vaebe.cn/api/sync-data" -H "x-api-key: $API_KEY" 19 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "", 8 | "css": "app/globals.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } 22 | -------------------------------------------------------------------------------- /app/(main)/page.tsx: -------------------------------------------------------------------------------- 1 | import { JueJinArticles } from './home/JueJinArticles' 2 | import { GithubProject } from './home/GithubProject' 3 | import { UserProfile } from './home/UserProfile' 4 | import { TechnologyStack } from './home/TechnologyStack' 5 | 6 | export default function About() { 7 | return ( 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /app/providers.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import '@/lib/date' 4 | import { ThemeProvider } from 'next-themes' 5 | import { SessionProvider } from 'next-auth/react' 6 | import { Toaster } from '@/components/ui/sonner' 7 | 8 | export function Providers({ children }: { children: React.ReactNode }) { 9 | return ( 10 | 11 | 12 | {children} 13 | 14 | 15 | 16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /public/favicon/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vaebe blog", 3 | "short_name": "vaebe blog", 4 | "icons": [ 5 | { 6 | "src": "/web-app-manifest-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png", 9 | "purpose": "maskable" 10 | }, 11 | { 12 | "src": "/web-app-manifest-512x512.png", 13 | "sizes": "512x512", 14 | "type": "image/png", 15 | "purpose": "maskable" 16 | } 17 | ], 18 | "theme_color": "#ffffff", 19 | "background_color": "#ffffff", 20 | "display": "standalone" 21 | } 22 | -------------------------------------------------------------------------------- /app/(main)/article/[id]/anchor/index.css: -------------------------------------------------------------------------------- 1 | .anchor ul { 2 | list-style: none; 3 | padding-left: 0; 4 | } 5 | 6 | .anchor li { 7 | margin: 5px 0; 8 | cursor: pointer; 9 | } 10 | 11 | .anchor li.active a { 12 | color: #3b82f6; 13 | font-weight: bold; 14 | } 15 | 16 | .level-h1 { 17 | font-size: 0.9rem; 18 | } 19 | 20 | .level-h2 { 21 | font-size: 0.9rem; 22 | padding-left: 10px; 23 | } 24 | 25 | .level-h3 { 26 | font-size: 0.9rem; 27 | padding-left: 20px; 28 | } 29 | 30 | .level-h4 { 31 | font-size: 0.9rem; 32 | padding-left: 30px; 33 | } 34 | -------------------------------------------------------------------------------- /lib/prisma.ts: -------------------------------------------------------------------------------- 1 | import "dotenv/config"; 2 | import { PrismaMariaDb } from '@prisma/adapter-mariadb'; 3 | import { PrismaClient } from '../generated/prisma/client'; 4 | 5 | const adapter = new PrismaMariaDb({ 6 | host: process.env.DATABASE_HOST, 7 | user: process.env.DATABASE_USER, 8 | password: process.env.DATABASE_PASSWORD, 9 | database: process.env.DATABASE_NAME, 10 | port: parseInt(process.env.DATABASE_PORT || '3306'), 11 | connectionLimit: 5, 12 | }); 13 | 14 | const prisma = new PrismaClient({ 15 | adapter, 16 | log: ['error', 'warn'], 17 | }); 18 | 19 | export { prisma } -------------------------------------------------------------------------------- /lib/enums.ts: -------------------------------------------------------------------------------- 1 | // 技术栈图标映射 2 | export const techIcons: Record = { 3 | TypeScript: 'logos:typescript-icon', 4 | Vue: 'logos:vue', 5 | NuxtJs: 'logos:nuxt', 6 | NextJs: 'logos:nextjs-icon', 7 | NestJs: 'logos:nestjs', 8 | Go: 'logos:go', 9 | Mysql: 'logos:mysql' 10 | } 11 | 12 | export const techStackData = ['TypeScript', 'Vue', 'NuxtJs', 'NextJs', 'NestJs', 'Go', 'Mysql'] 13 | 14 | export const TimeInSeconds = { 15 | oneHour: 3600, // 1小时 = 3600秒 16 | oneDay: 86400, // 1天 = 86400秒 17 | oneWeek: 604800, // 1周 = 604800秒 18 | oneMonth: 2592000 // 1个月 = 2592000秒(以30天计算) 19 | } 20 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, // 启用保存时自动格式化 3 | "editor.defaultFormatter": "esbenp.prettier-vscode", // 使用 Prettier 作为默认格式化工具 4 | "[javascript]": { 5 | "editor.defaultFormatter": "esbenp.prettier-vscode" 6 | }, 7 | "[typescript]": { 8 | "editor.defaultFormatter": "esbenp.prettier-vscode" 9 | }, 10 | "[typescriptreact]": { 11 | "editor.defaultFormatter": "esbenp.prettier-vscode" 12 | }, 13 | "eslint.validate": ["javascript", "javascriptreact", "typescript", "typescriptreact"], 14 | "[prisma]": { 15 | "editor.defaultFormatter": "Prisma.prisma" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/api/articles/details/route.ts: -------------------------------------------------------------------------------- 1 | import { sendJson } from '@/lib/utils' 2 | import { prisma } from '@/lib/prisma' 3 | 4 | export async function GET(req: Request) { 5 | const url = new URL(req.url) 6 | const id = url.searchParams.get('id') // 从查询参数中获取 id 7 | 8 | try { 9 | if (!id) { 10 | return sendJson({ code: -1, msg: 'id 不存在' }) 11 | } 12 | 13 | const article = await prisma.article.findUnique({ 14 | where: { id: id } 15 | }) 16 | 17 | return sendJson({ data: article }) 18 | } catch (error) { 19 | console.error(error) 20 | return sendJson({ code: -1, msg: '获取文章详情失败!' }) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { dirname } from 'path' 2 | import { fileURLToPath } from 'url' 3 | import { FlatCompat } from '@eslint/eslintrc' 4 | 5 | const __filename = fileURLToPath(import.meta.url) 6 | const __dirname = dirname(__filename) 7 | 8 | const compat = new FlatCompat({ 9 | baseDirectory: __dirname 10 | }) 11 | 12 | const eslintConfig = [ 13 | { 14 | ignores: ['node_modules/**', '.next/**', 'out/**', 'build/**', 'next-env.d.ts'] 15 | }, 16 | ...compat.extends('next/core-web-vitals', 'next/typescript', 'prettier'), 17 | { 18 | ignores: ['**/ui/**', '**/artdots.tsx'] 19 | } 20 | ] 21 | 22 | export default eslintConfig 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | .env 31 | 32 | # vercel 33 | .vercel 34 | 35 | # typescript 36 | *.tsbuildinfo 37 | next-env.d.ts 38 | 39 | # sitemap 40 | robots.txt 41 | sitemap.xml 42 | 43 | # 生成的文件 44 | generated 45 | -------------------------------------------------------------------------------- /app/(main)/layout.tsx: -------------------------------------------------------------------------------- 1 | import LayoutHeader from '@/components/layout/header' 2 | import LayoutFooter from '@/components/layout/footer' 3 | import { ThemeSwitch } from '@/components/theme-switch' 4 | 5 | export default function BaseLayout({ children }: { children: React.ReactNode }) { 6 | return ( 7 | <> 8 | 9 |
{children}
10 | 11 | 12 | 13 |
14 | 15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /components/base-loading.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { Icon } from '@iconify/react' 4 | import { cn } from '@/lib/utils' 5 | 6 | interface LoadingProps { 7 | isLoading: boolean 8 | size?: number 9 | className?: string 10 | } 11 | 12 | export function BaseLoading({ isLoading, size = 24, className }: LoadingProps) { 13 | if (!isLoading) return null 14 | 15 | return ( 16 |
17 | 23 |
24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /app/(main)/guestbook/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { useState } from 'react' 4 | import { Header } from './Header' 5 | import { AddMessage } from './AddMessage' 6 | import { MessagesList } from './MessagesList' 7 | import { GuestbookMessage } from '@/types' 8 | 9 | export default function GuestBook() { 10 | const [messages, setMessages] = useState>([]) 11 | 12 | return ( 13 |
14 |
15 | 16 | 17 | 18 | 19 |
20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /components/ui/sonner.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useTheme } from "next-themes" 4 | import { Toaster as Sonner, ToasterProps } from "sonner" 5 | 6 | const Toaster = ({ ...props }: ToasterProps) => { 7 | const { theme = "system" } = useTheme() 8 | 9 | return ( 10 | 22 | ) 23 | } 24 | 25 | export { Toaster } 26 | -------------------------------------------------------------------------------- /app/api/cache-data/route.ts: -------------------------------------------------------------------------------- 1 | import { sendJson } from '@/lib/utils' 2 | import { prisma } from '@/lib/prisma' 3 | 4 | // 这里获取数据仍设置为 api 接口的原因是可以使用 nextjs 的缓存功能 5 | export async function GET(req: Request) { 6 | try { 7 | const { searchParams } = new URL(req.url) 8 | const key = searchParams.get('key') 9 | 10 | if (!key) { 11 | return sendJson({ code: 400, msg: '缓存数据的 key 不能为空!' }) 12 | } 13 | 14 | const cacheData = await prisma.cacheData.findUnique({ 15 | where: { 16 | key: key 17 | } 18 | }) 19 | 20 | return sendJson({ data: cacheData }) 21 | } catch (error) { 22 | return sendJson({ code: -1, msg: `获取缓存数据失败:${error}` }) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as LabelPrimitive from "@radix-ui/react-label" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | function Label({ 9 | className, 10 | ...props 11 | }: React.ComponentProps) { 12 | return ( 13 | 21 | ) 22 | } 23 | 24 | export { Label } 25 | -------------------------------------------------------------------------------- /app/api/articles/update/route.ts: -------------------------------------------------------------------------------- 1 | import { sendJson } from '@/lib/utils' 2 | import { prisma } from '@/lib/prisma' 3 | 4 | export async function PUT(req: Request) { 5 | try { 6 | const body = await req.json() 7 | const { id, title, content, classify, coverImg, summary, status } = body 8 | 9 | const updatedArticle = await prisma.article.update({ 10 | where: { id }, 11 | data: { 12 | title, 13 | content, 14 | classify, 15 | coverImg, 16 | summary, 17 | status 18 | } 19 | }) 20 | 21 | return sendJson({ data: updatedArticle }) 22 | } catch (error) { 23 | console.error(error) 24 | return sendJson({ code: -1, msg: '更新文章数据失败!' }) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/api/articles/articleCountByMonth/route.ts: -------------------------------------------------------------------------------- 1 | import { sendJson } from '@/lib/utils' 2 | import { prisma } from '@/lib/prisma' 3 | 4 | export async function GET() { 5 | try { 6 | const list = await prisma.$queryRaw<{ month: string; count: number }[]>` 7 | SELECT DATE_FORMAT(createdAt, '%Y-%m') AS month, COUNT(*) AS count 8 | FROM article 9 | WHERE isDeleted = 0 10 | GROUP BY month 11 | ORDER BY month ASC; 12 | ` 13 | 14 | const formattedCounts = list.map((item) => ({ 15 | month: item.month, 16 | count: Number(item.count) 17 | })) 18 | 19 | return sendJson({ data: formattedCounts }) 20 | } catch (error) { 21 | return sendJson({ code: -1, msg: `按月查询文章数量失败:${error}` }) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/api/articles/add/route.ts: -------------------------------------------------------------------------------- 1 | import { sendJson, generateUUID } from '@/lib/utils' 2 | import { prisma } from '@/lib/prisma' 3 | 4 | export async function POST(req: Request) { 5 | try { 6 | const body = await req.json() 7 | const { title, content, classify, coverImg, summary } = body 8 | 9 | const newArticle = await prisma.article.create({ 10 | data: { 11 | id: generateUUID(), 12 | title, 13 | content, 14 | classify, 15 | coverImg, 16 | summary, 17 | status: '01', 18 | source: '00', 19 | userId: 1 // 示例,通常从认证信息中获取用户ID 20 | } 21 | }) 22 | return sendJson({ data: newArticle }) 23 | } catch (error) { 24 | sendJson({ code: -1, msg: `创建文章失败:${error}` }) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /components/screen-loading.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import { FC } from 'react' 3 | import ReactDOM from 'react-dom' 4 | 5 | interface Props { 6 | isLoading: boolean 7 | message?: string 8 | } 9 | 10 | export const FullScreenLoading: FC = ({ isLoading, message = 'Loading...' }) => { 11 | if (!isLoading) return null 12 | 13 | return ReactDOM.createPortal( 14 |
15 |
16 |
17 |

{message}

18 |
19 |
, 20 | document.body 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /app/(main)/home/TechnologyStack.tsx: -------------------------------------------------------------------------------- 1 | import { techIcons, techStackData } from '@/lib/enums' 2 | import { Icon } from '@iconify/react' 3 | import { ContentCard } from './ContentCard' 4 | 5 | export function TechnologyStack() { 6 | return ( 7 | 8 |
9 | {techStackData.map((tech) => ( 10 |
14 | 18 | {tech} 19 |
20 | ))} 21 |
22 |
23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /components/ui/textarea.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | function Textarea({ className, ...props }: React.ComponentProps<"textarea">) { 6 | return ( 7 | 26 | ) 27 | } 28 | 29 | // 留言预览组件 30 | const MessagePreview = ({ message }: { message: string }) => { 31 | return ( 32 |
33 | 34 |
35 | ) 36 | } 37 | 38 | interface MessageControlsProps { 39 | messageLength: number 40 | messageView: boolean 41 | onToggleView: () => void 42 | onSendMsg: () => void 43 | } 44 | 45 | function MessageControls({ 46 | messageLength, 47 | messageView, 48 | onToggleView, 49 | onSendMsg 50 | }: MessageControlsProps) { 51 | return ( 52 |
53 |

支持 Markdown 格式

54 |
55 |

{messageLength} / 1000

56 | 57 | 58 | 59 | 65 | 66 | {messageView ? '关闭预览' : '预览一下'} 67 | 68 | 69 | 70 | 71 | 72 | 73 | 79 | 80 | 发送 81 | 82 | 83 |
84 |
85 | ) 86 | } 87 | 88 | interface AddMessageProps { 89 | setMessages: Dispatch> 90 | } 91 | 92 | function AddMessage({ setMessages }: AddMessageProps) { 93 | const [messageView, setMessageView] = useState(false) 94 | const [message, setMessage] = useState('') 95 | const { data: session, status } = useSession() 96 | 97 | const sendMsg = async () => { 98 | if (!message.trim()) { 99 | toast('留言内容不能为空!') 100 | return 101 | } 102 | 103 | const res = await fetch('/api/guestbook', { 104 | method: 'POST', 105 | headers: { 106 | 'Content-Type': 'application/json' 107 | }, 108 | body: JSON.stringify({ 109 | content: message, 110 | userEmail: session?.user?.email 111 | }) 112 | }).then((res) => res.json()) 113 | 114 | if (res.code !== 0) { 115 | toast(res.msg) 116 | return 117 | } 118 | 119 | toast('留言成功!') 120 | 121 | setMessageView(false) 122 | 123 | setMessages((oldData) => [{ ...res.data, author: session?.user }, ...oldData]) 124 | 125 | setMessage('') 126 | } 127 | 128 | function messageChange(value: string) { 129 | if (message.length > 1000) { 130 | return 131 | } 132 | 133 | setMessage(value) 134 | } 135 | 136 | return ( 137 |
138 | {status === 'authenticated' ? ( 139 |
140 | {messageView ? ( 141 | 142 | ) : ( 143 | 144 | )} 145 | 146 | setMessageView(!messageView)} 150 | onSendMsg={sendMsg} 151 | /> 152 |
153 | ) : ( 154 | 155 | 159 | 160 | )} 161 |
162 | ) 163 | } 164 | 165 | export { AddMessage } 166 | export default AddMessage 167 | -------------------------------------------------------------------------------- /app/(main)/home/UserProfile.tsx: -------------------------------------------------------------------------------- 1 | import { Icon } from '@iconify/react' 2 | import Link from 'next/link' 3 | import Image from 'next/image' 4 | import userIcon from '@/public/user-icon.png' 5 | import type { GithubUserInfo } from '@/lib/github/user-info' 6 | import { GithubUserInfoCacheDataKey } from '@/lib/github/user-info' 7 | import { getCacheDataByKey } from '@/lib/cache-data' 8 | import type { JuejinUserInfo } from '@/lib/juejin/fetch-user-info' 9 | import { fetchJuejinUserInfo } from '@/lib/juejin/fetch-user-info' 10 | import { TimeInSeconds } from '@/lib/enums' 11 | 12 | // 统计项组件 13 | const StatItem = ({ icon, label, value }: { icon: string; label: string; value?: number }) => ( 14 |
15 | 16 | {label}: 17 | {value ?? '-'} 18 |
19 | ) 20 | 21 | // 社交媒体链接配置 22 | const SOCIAL_LINKS = { 23 | juejin: { 24 | url: 'https://juejin.cn/user/712139266339694', 25 | icon: 'simple-icons:juejin', 26 | label: '掘金' 27 | }, 28 | github: { 29 | url: 'https://github.com/vaebe', 30 | icon: 'mdi:github', 31 | label: 'GitHub' 32 | } 33 | } as const 34 | 35 | interface SocialStatsSectionProps { 36 | platform: keyof typeof SOCIAL_LINKS 37 | stats: { icon: string; label: string; value?: number }[] 38 | } 39 | 40 | // 社交媒体统计区块 41 | const SocialStatsSection = ({ platform, stats }: SocialStatsSectionProps) => ( 42 |
43 | 49 |

50 | 51 | {SOCIAL_LINKS[platform].label} 52 |

53 | 54 |
55 | {stats.map((stat, index) => ( 56 | 57 | ))} 58 |
59 |
60 | ) 61 | 62 | function GitHubSocialStatsSection({ info }: { info?: GithubUserInfo }) { 63 | return ( 64 | 80 | ) 81 | } 82 | 83 | async function JuejinSocialStatsSection() { 84 | let info: JuejinUserInfo | undefined 85 | 86 | try { 87 | info = await fetchJuejinUserInfo() 88 | } catch { 89 | info = undefined 90 | } 91 | 92 | return ( 93 | 105 | ) 106 | } 107 | 108 | const Userdescription = ` 109 | 我是 Vaebe,我的主要技术栈是 Vue 全家桶,目前也在使用 React 来构建项目,比如这个博客它使用 Next.js。 110 | 我会将自己的实践过程以文章的形式分享在掘金上,并在 GitHub上参与开源项目,不断提升自己的编程技能。 111 | 欢迎访问我的掘金主页和 GitHub主页,了解更多关于我的信息!` 112 | 113 | // 主要用户信息组件 114 | const UserInfo = ({ githubUserInfo }: { githubUserInfo?: GithubUserInfo }) => ( 115 |
116 | {`${githubUserInfo?.login}'s 124 |
125 |

126 | {githubUserInfo?.login ?? 'Loading...'} 127 |

128 |

{Userdescription}

129 |
130 |
131 | ) 132 | 133 | export async function UserProfile() { 134 | let githubUserInfo: GithubUserInfo | undefined 135 | try { 136 | const res = await getCacheDataByKey({ 137 | key: GithubUserInfoCacheDataKey, 138 | next: { revalidate: TimeInSeconds.oneHour } 139 | }) 140 | if (res.code === 0) { 141 | githubUserInfo = res.data 142 | } 143 | } catch { 144 | githubUserInfo = undefined 145 | } 146 | 147 | return ( 148 |
149 | 150 | 151 |
152 | 153 | 154 |
155 |
156 | ) 157 | } 158 | -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @import 'tailwindcss'; 2 | 3 | @plugin "tailwindcss-animate"; 4 | 5 | @custom-variant dark (&:is(.dark *)); 6 | 7 | @theme { 8 | --font-sans: var(--font-geist-sans); 9 | --font-mono: var(--font-geist-mono); 10 | } 11 | 12 | :root { 13 | --radius: 0.625rem; 14 | 15 | --background: oklch(1 0 0); 16 | 17 | --foreground: oklch(0.145 0 0); 18 | 19 | --card: oklch(1 0 0); 20 | 21 | --card-foreground: oklch(0.145 0 0); 22 | 23 | --popover: oklch(1 0 0); 24 | 25 | --popover-foreground: oklch(0.145 0 0); 26 | 27 | --primary: oklch(0.205 0 0); 28 | 29 | --primary-foreground: oklch(0.985 0 0); 30 | 31 | --secondary: oklch(0.97 0 0); 32 | 33 | --secondary-foreground: oklch(0.205 0 0); 34 | 35 | --muted: oklch(0.97 0 0); 36 | 37 | --muted-foreground: oklch(0.556 0 0); 38 | 39 | --accent: oklch(0.97 0 0); 40 | 41 | --accent-foreground: oklch(0.205 0 0); 42 | 43 | --destructive: oklch(0.577 0.245 27.325); 44 | 45 | --border: oklch(0.922 0 0); 46 | 47 | --input: oklch(0.922 0 0); 48 | 49 | --ring: oklch(0.708 0 0); 50 | 51 | --chart-1: oklch(0.646 0.222 41.116); 52 | 53 | --chart-2: oklch(0.6 0.118 184.704); 54 | 55 | --chart-3: oklch(0.398 0.07 227.392); 56 | 57 | --chart-4: oklch(0.828 0.189 84.429); 58 | 59 | --chart-5: oklch(0.769 0.188 70.08); 60 | 61 | --sidebar: oklch(0.985 0 0); 62 | 63 | --sidebar-foreground: oklch(0.145 0 0); 64 | 65 | --sidebar-primary: oklch(0.205 0 0); 66 | 67 | --sidebar-primary-foreground: oklch(0.985 0 0); 68 | 69 | --sidebar-accent: oklch(0.97 0 0); 70 | 71 | --sidebar-accent-foreground: oklch(0.205 0 0); 72 | 73 | --sidebar-border: oklch(0.922 0 0); 74 | 75 | --sidebar-ring: oklch(0.708 0 0); 76 | } 77 | 78 | .dark { 79 | --background: oklch(0.145 0 0); 80 | 81 | --foreground: oklch(0.985 0 0); 82 | 83 | --card: oklch(0.205 0 0); 84 | 85 | --card-foreground: oklch(0.985 0 0); 86 | 87 | --popover: oklch(0.205 0 0); 88 | 89 | --popover-foreground: oklch(0.985 0 0); 90 | 91 | --primary: oklch(0.922 0 0); 92 | 93 | --primary-foreground: oklch(0.205 0 0); 94 | 95 | --secondary: oklch(0.269 0 0); 96 | 97 | --secondary-foreground: oklch(0.985 0 0); 98 | 99 | --muted: oklch(0.269 0 0); 100 | 101 | --muted-foreground: oklch(0.708 0 0); 102 | 103 | --accent: oklch(0.269 0 0); 104 | 105 | --accent-foreground: oklch(0.985 0 0); 106 | 107 | --destructive: oklch(0.704 0.191 22.216); 108 | 109 | --border: oklch(1 0 0 / 10%); 110 | 111 | --input: oklch(1 0 0 / 15%); 112 | 113 | --ring: oklch(0.556 0 0); 114 | 115 | --chart-1: oklch(0.488 0.243 264.376); 116 | 117 | --chart-2: oklch(0.696 0.17 162.48); 118 | 119 | --chart-3: oklch(0.769 0.188 70.08); 120 | 121 | --chart-4: oklch(0.627 0.265 303.9); 122 | 123 | --chart-5: oklch(0.645 0.246 16.439); 124 | 125 | --sidebar: oklch(0.205 0 0); 126 | 127 | --sidebar-foreground: oklch(0.985 0 0); 128 | 129 | --sidebar-primary: oklch(0.488 0.243 264.376); 130 | 131 | --sidebar-primary-foreground: oklch(0.985 0 0); 132 | 133 | --sidebar-accent: oklch(0.269 0 0); 134 | 135 | --sidebar-accent-foreground: oklch(0.985 0 0); 136 | 137 | --sidebar-border: oklch(1 0 0 / 10%); 138 | 139 | --sidebar-ring: oklch(0.556 0 0); 140 | } 141 | 142 | @theme inline { 143 | --radius-sm: calc(var(--radius) - 4px); 144 | 145 | --radius-md: calc(var(--radius) - 2px); 146 | 147 | --radius-lg: var(--radius); 148 | 149 | --radius-xl: calc(var(--radius) + 4px); 150 | 151 | --color-background: var(--background); 152 | 153 | --color-foreground: var(--foreground); 154 | 155 | --color-card: var(--card); 156 | 157 | --color-card-foreground: var(--card-foreground); 158 | 159 | --color-popover: var(--popover); 160 | 161 | --color-popover-foreground: var(--popover-foreground); 162 | 163 | --color-primary: var(--primary); 164 | 165 | --color-primary-foreground: var(--primary-foreground); 166 | 167 | --color-secondary: var(--secondary); 168 | 169 | --color-secondary-foreground: var(--secondary-foreground); 170 | 171 | --color-muted: var(--muted); 172 | 173 | --color-muted-foreground: var(--muted-foreground); 174 | 175 | --color-accent: var(--accent); 176 | 177 | --color-accent-foreground: var(--accent-foreground); 178 | 179 | --color-destructive: var(--destructive); 180 | 181 | --color-border: var(--border); 182 | 183 | --color-input: var(--input); 184 | 185 | --color-ring: var(--ring); 186 | 187 | --color-chart-1: var(--chart-1); 188 | 189 | --color-chart-2: var(--chart-2); 190 | 191 | --color-chart-3: var(--chart-3); 192 | 193 | --color-chart-4: var(--chart-4); 194 | 195 | --color-chart-5: var(--chart-5); 196 | 197 | --color-sidebar: var(--sidebar); 198 | 199 | --color-sidebar-foreground: var(--sidebar-foreground); 200 | 201 | --color-sidebar-primary: var(--sidebar-primary); 202 | 203 | --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); 204 | 205 | --color-sidebar-accent: var(--sidebar-accent); 206 | 207 | --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); 208 | 209 | --color-sidebar-border: var(--sidebar-border); 210 | 211 | --color-sidebar-ring: var(--sidebar-ring); 212 | } 213 | 214 | @layer base { 215 | * { 216 | @apply border-border outline-ring/50; 217 | } 218 | 219 | body { 220 | @apply bg-background text-foreground; 221 | } 222 | } 223 | 224 | /* Chrome, Edge, Safari 和其他 WebKit 内核的浏览器 */ 225 | ::-webkit-scrollbar { 226 | width: 0; 227 | background: transparent; 228 | } 229 | 230 | /* Firefox */ 231 | * { 232 | scrollbar-width: none; 233 | -ms-overflow-style: none; 234 | } 235 | 236 | 237 | @keyframes gradient-x { 238 | 0% { 239 | background-position: 0% 50%; 240 | } 241 | 242 | 50% { 243 | background-position: 100% 50%; 244 | } 245 | 246 | 100% { 247 | background-position: 0% 50%; 248 | } 249 | } 250 | 251 | .gradient-anim { 252 | background-size: 200% 200%; 253 | animation: gradient-x 3s ease infinite; 254 | } -------------------------------------------------------------------------------- /prisma/migrations/20250907134927_init/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE `article` ( 3 | `id` VARCHAR(191) NOT NULL, 4 | `userId` INTEGER NOT NULL, 5 | `title` TEXT NOT NULL, 6 | `content` LONGTEXT NOT NULL, 7 | `classify` TEXT NULL, 8 | `coverImg` TEXT NULL, 9 | `summary` TEXT NOT NULL, 10 | `source` TEXT NULL, 11 | `views` INTEGER NOT NULL DEFAULT 1, 12 | `likes` INTEGER NOT NULL DEFAULT 1, 13 | `favorites` INTEGER NOT NULL DEFAULT 1, 14 | `showNumber` INTEGER NOT NULL DEFAULT 1, 15 | `status` TEXT NOT NULL, 16 | `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), 17 | `updatedAt` DATETIME(3) NULL, 18 | `deletedAt` DATETIME(3) NULL, 19 | `isDeleted` INTEGER NULL DEFAULT 0, 20 | 21 | UNIQUE INDEX `article_id_key`(`id`), 22 | PRIMARY KEY (`id`) 23 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; 24 | 25 | -- CreateTable 26 | CREATE TABLE `user` ( 27 | `id` VARCHAR(191) NOT NULL, 28 | `name` VARCHAR(191) NULL, 29 | `username` VARCHAR(191) NULL, 30 | `email` VARCHAR(191) NULL, 31 | `password` TEXT NULL, 32 | `role` VARCHAR(191) NULL DEFAULT '01', 33 | `emailVerified` DATETIME(3) NULL, 34 | `image` VARCHAR(191) NULL, 35 | `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), 36 | `updatedAt` DATETIME(3) NOT NULL, 37 | 38 | UNIQUE INDEX `user_username_key`(`username`), 39 | UNIQUE INDEX `user_email_key`(`email`), 40 | PRIMARY KEY (`id`) 41 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; 42 | 43 | -- CreateTable 44 | CREATE TABLE `account` ( 45 | `id` VARCHAR(191) NOT NULL, 46 | `userId` VARCHAR(191) NOT NULL, 47 | `type` VARCHAR(191) NOT NULL, 48 | `provider` VARCHAR(191) NOT NULL, 49 | `providerAccountId` VARCHAR(191) NOT NULL, 50 | `refresh_token` TEXT NULL, 51 | `access_token` TEXT NULL, 52 | `expires_at` INTEGER NULL, 53 | `token_type` VARCHAR(191) NULL, 54 | `scope` VARCHAR(191) NULL, 55 | `id_token` TEXT NULL, 56 | `session_state` VARCHAR(191) NULL, 57 | `refresh_token_expires_in` INTEGER NULL, 58 | `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), 59 | `updatedAt` DATETIME(3) NOT NULL, 60 | 61 | UNIQUE INDEX `account_userId_key`(`userId`), 62 | INDEX `account_userId_idx`(`userId`), 63 | UNIQUE INDEX `account_provider_providerAccountId_key`(`provider`, `providerAccountId`), 64 | PRIMARY KEY (`id`) 65 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; 66 | 67 | -- CreateTable 68 | CREATE TABLE `session` ( 69 | `id` VARCHAR(191) NOT NULL, 70 | `sessionToken` VARCHAR(191) NOT NULL, 71 | `userId` VARCHAR(191) NOT NULL, 72 | `expires` DATETIME(3) NOT NULL, 73 | `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), 74 | `updatedAt` DATETIME(3) NOT NULL, 75 | 76 | UNIQUE INDEX `session_sessionToken_key`(`sessionToken`), 77 | INDEX `session_userId_idx`(`userId`), 78 | PRIMARY KEY (`id`) 79 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; 80 | 81 | -- CreateTable 82 | CREATE TABLE `verification_token` ( 83 | `identifier` VARCHAR(191) NOT NULL, 84 | `token` VARCHAR(191) NOT NULL, 85 | `expires` DATETIME(3) NOT NULL, 86 | 87 | UNIQUE INDEX `verification_token_identifier_token_key`(`identifier`, `token`) 88 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; 89 | 90 | -- CreateTable 91 | CREATE TABLE `authenticator` ( 92 | `credentialID` VARCHAR(191) NOT NULL, 93 | `userId` VARCHAR(191) NOT NULL, 94 | `providerAccountId` VARCHAR(191) NOT NULL, 95 | `credentialPublicKey` VARCHAR(191) NOT NULL, 96 | `counter` INTEGER NOT NULL, 97 | `credentialDeviceType` VARCHAR(191) NOT NULL, 98 | `credentialBackedUp` BOOLEAN NOT NULL, 99 | `transports` VARCHAR(191) NULL, 100 | 101 | UNIQUE INDEX `authenticator_credentialID_key`(`credentialID`), 102 | PRIMARY KEY (`userId`, `credentialID`) 103 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; 104 | 105 | -- CreateTable 106 | CREATE TABLE `message` ( 107 | `id` VARCHAR(191) NOT NULL, 108 | `content` VARCHAR(191) NOT NULL, 109 | `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), 110 | `updatedAt` DATETIME(3) NOT NULL, 111 | `deletedAt` DATETIME(3) NULL, 112 | `authorId` VARCHAR(191) NOT NULL, 113 | 114 | PRIMARY KEY (`id`) 115 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; 116 | 117 | -- CreateTable 118 | CREATE TABLE `subscriber` ( 119 | `id` VARCHAR(191) NOT NULL, 120 | `email` VARCHAR(191) NOT NULL, 121 | `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), 122 | `updatedAt` DATETIME(3) NOT NULL, 123 | `deletedAt` DATETIME(3) NULL, 124 | 125 | UNIQUE INDEX `subscriber_email_key`(`email`), 126 | PRIMARY KEY (`id`) 127 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; 128 | 129 | -- CreateTable 130 | CREATE TABLE `cache-data` ( 131 | `id` VARCHAR(191) NOT NULL, 132 | `key` VARCHAR(191) NOT NULL, 133 | `data` LONGTEXT NOT NULL, 134 | `desc` TEXT NOT NULL, 135 | `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), 136 | `updatedAt` DATETIME(3) NOT NULL, 137 | `deletedAt` DATETIME(3) NULL, 138 | 139 | UNIQUE INDEX `cache-data_key_key`(`key`), 140 | PRIMARY KEY (`id`) 141 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; 142 | 143 | -- AddForeignKey 144 | ALTER TABLE `account` ADD CONSTRAINT `account_userId_fkey` FOREIGN KEY (`userId`) REFERENCES `user`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; 145 | 146 | -- AddForeignKey 147 | ALTER TABLE `session` ADD CONSTRAINT `session_userId_fkey` FOREIGN KEY (`userId`) REFERENCES `user`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; 148 | 149 | -- AddForeignKey 150 | ALTER TABLE `authenticator` ADD CONSTRAINT `authenticator_userId_fkey` FOREIGN KEY (`userId`) REFERENCES `user`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; 151 | 152 | -- AddForeignKey 153 | ALTER TABLE `message` ADD CONSTRAINT `message_authorId_fkey` FOREIGN KEY (`authorId`) REFERENCES `user`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; 154 | -------------------------------------------------------------------------------- /components/ui/select.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as SelectPrimitive from "@radix-ui/react-select" 5 | import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | function Select({ 10 | ...props 11 | }: React.ComponentProps) { 12 | return 13 | } 14 | 15 | function SelectGroup({ 16 | ...props 17 | }: React.ComponentProps) { 18 | return 19 | } 20 | 21 | function SelectValue({ 22 | ...props 23 | }: React.ComponentProps) { 24 | return 25 | } 26 | 27 | function SelectTrigger({ 28 | className, 29 | size = "default", 30 | children, 31 | ...props 32 | }: React.ComponentProps & { 33 | size?: "sm" | "default" 34 | }) { 35 | return ( 36 | 45 | {children} 46 | 47 | 48 | 49 | 50 | ) 51 | } 52 | 53 | function SelectContent({ 54 | className, 55 | children, 56 | position = "popper", 57 | ...props 58 | }: React.ComponentProps) { 59 | return ( 60 | 61 | 72 | 73 | 80 | {children} 81 | 82 | 83 | 84 | 85 | ) 86 | } 87 | 88 | function SelectLabel({ 89 | className, 90 | ...props 91 | }: React.ComponentProps) { 92 | return ( 93 | 98 | ) 99 | } 100 | 101 | function SelectItem({ 102 | className, 103 | children, 104 | ...props 105 | }: React.ComponentProps) { 106 | return ( 107 | 115 | 116 | 117 | 118 | 119 | 120 | {children} 121 | 122 | ) 123 | } 124 | 125 | function SelectSeparator({ 126 | className, 127 | ...props 128 | }: React.ComponentProps) { 129 | return ( 130 | 135 | ) 136 | } 137 | 138 | function SelectScrollUpButton({ 139 | className, 140 | ...props 141 | }: React.ComponentProps) { 142 | return ( 143 | 151 | 152 | 153 | ) 154 | } 155 | 156 | function SelectScrollDownButton({ 157 | className, 158 | ...props 159 | }: React.ComponentProps) { 160 | return ( 161 | 169 | 170 | 171 | ) 172 | } 173 | 174 | export { 175 | Select, 176 | SelectContent, 177 | SelectGroup, 178 | SelectItem, 179 | SelectLabel, 180 | SelectScrollDownButton, 181 | SelectScrollUpButton, 182 | SelectSeparator, 183 | SelectTrigger, 184 | SelectValue, 185 | } 186 | -------------------------------------------------------------------------------- /components/ui/dropdown-menu.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" 5 | import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | function DropdownMenu({ 10 | ...props 11 | }: React.ComponentProps) { 12 | return 13 | } 14 | 15 | function DropdownMenuPortal({ 16 | ...props 17 | }: React.ComponentProps) { 18 | return ( 19 | 20 | ) 21 | } 22 | 23 | function DropdownMenuTrigger({ 24 | ...props 25 | }: React.ComponentProps) { 26 | return ( 27 | 31 | ) 32 | } 33 | 34 | function DropdownMenuContent({ 35 | className, 36 | sideOffset = 4, 37 | ...props 38 | }: React.ComponentProps) { 39 | return ( 40 | 41 | 50 | 51 | ) 52 | } 53 | 54 | function DropdownMenuGroup({ 55 | ...props 56 | }: React.ComponentProps) { 57 | return ( 58 | 59 | ) 60 | } 61 | 62 | function DropdownMenuItem({ 63 | className, 64 | inset, 65 | variant = "default", 66 | ...props 67 | }: React.ComponentProps & { 68 | inset?: boolean 69 | variant?: "default" | "destructive" 70 | }) { 71 | return ( 72 | 82 | ) 83 | } 84 | 85 | function DropdownMenuCheckboxItem({ 86 | className, 87 | children, 88 | checked, 89 | ...props 90 | }: React.ComponentProps) { 91 | return ( 92 | 101 | 102 | 103 | 104 | 105 | 106 | {children} 107 | 108 | ) 109 | } 110 | 111 | function DropdownMenuRadioGroup({ 112 | ...props 113 | }: React.ComponentProps) { 114 | return ( 115 | 119 | ) 120 | } 121 | 122 | function DropdownMenuRadioItem({ 123 | className, 124 | children, 125 | ...props 126 | }: React.ComponentProps) { 127 | return ( 128 | 136 | 137 | 138 | 139 | 140 | 141 | {children} 142 | 143 | ) 144 | } 145 | 146 | function DropdownMenuLabel({ 147 | className, 148 | inset, 149 | ...props 150 | }: React.ComponentProps & { 151 | inset?: boolean 152 | }) { 153 | return ( 154 | 163 | ) 164 | } 165 | 166 | function DropdownMenuSeparator({ 167 | className, 168 | ...props 169 | }: React.ComponentProps) { 170 | return ( 171 | 176 | ) 177 | } 178 | 179 | function DropdownMenuShortcut({ 180 | className, 181 | ...props 182 | }: React.ComponentProps<"span">) { 183 | return ( 184 | 192 | ) 193 | } 194 | 195 | function DropdownMenuSub({ 196 | ...props 197 | }: React.ComponentProps) { 198 | return 199 | } 200 | 201 | function DropdownMenuSubTrigger({ 202 | className, 203 | inset, 204 | children, 205 | ...props 206 | }: React.ComponentProps & { 207 | inset?: boolean 208 | }) { 209 | return ( 210 | 219 | {children} 220 | 221 | 222 | ) 223 | } 224 | 225 | function DropdownMenuSubContent({ 226 | className, 227 | ...props 228 | }: React.ComponentProps) { 229 | return ( 230 | 238 | ) 239 | } 240 | 241 | export { 242 | DropdownMenu, 243 | DropdownMenuPortal, 244 | DropdownMenuTrigger, 245 | DropdownMenuContent, 246 | DropdownMenuGroup, 247 | DropdownMenuLabel, 248 | DropdownMenuItem, 249 | DropdownMenuCheckboxItem, 250 | DropdownMenuRadioGroup, 251 | DropdownMenuRadioItem, 252 | DropdownMenuSeparator, 253 | DropdownMenuShortcut, 254 | DropdownMenuSub, 255 | DropdownMenuSubTrigger, 256 | DropdownMenuSubContent, 257 | } 258 | -------------------------------------------------------------------------------- /components/bytemd/dark-theme.scss: -------------------------------------------------------------------------------- 1 | .dark { 2 | .markdown-body { 3 | font-family: 4 | -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif, 5 | 'Apple Color Emoji', 'Segoe UI Emoji'; 6 | color: #ffffff; 7 | // background-color: #0d1117; 8 | font-size: 16px; 9 | line-height: 1.5; 10 | word-wrap: break-word; 11 | } 12 | 13 | .markdown-body::before { 14 | display: table; 15 | content: ''; 16 | } 17 | 18 | .markdown-body::after { 19 | display: table; 20 | clear: both; 21 | content: ''; 22 | } 23 | 24 | .markdown-body > *:first-child { 25 | margin-top: 0 !important; 26 | } 27 | 28 | .markdown-body > *:last-child { 29 | margin-bottom: 0 !important; 30 | } 31 | 32 | .markdown-body a:not([href]) { 33 | color: inherit; 34 | text-decoration: none; 35 | } 36 | 37 | .markdown-body .absent { 38 | color: #f85149; 39 | } 40 | 41 | .markdown-body .anchor { 42 | float: left; 43 | padding-right: 4px; 44 | margin-left: -20px; 45 | line-height: 1; 46 | } 47 | 48 | .markdown-body .anchor:focus { 49 | outline: none; 50 | } 51 | 52 | .markdown-body p, 53 | .markdown-body ul, 54 | .markdown-body ol, 55 | .markdown-body dl, 56 | .markdown-body table, 57 | .markdown-body pre, 58 | .markdown-body details { 59 | margin-top: 0; 60 | margin-bottom: 16px; 61 | } 62 | 63 | .markdown-body hr { 64 | height: 0.25em; 65 | padding: 0; 66 | margin: 24px 0; 67 | background-color: #30363d; 68 | border: 0; 69 | } 70 | 71 | .markdown-body blockquote { 72 | color: #ffffffb3; 73 | padding: 1px 23px; 74 | margin: 22px 0; 75 | border-left: 4px solid #fff3; 76 | background-color: #ffffff1f; 77 | 78 | & p { 79 | margin: 10px 0; 80 | } 81 | } 82 | 83 | .markdown-body h1, 84 | .markdown-body h2, 85 | .markdown-body h3, 86 | .markdown-body h4, 87 | .markdown-body h5, 88 | .markdown-body h6 { 89 | margin-top: 24px; 90 | margin-bottom: 16px; 91 | font-weight: 600; 92 | line-height: 1.25; 93 | } 94 | 95 | .markdown-body h1 .octicon-link, 96 | .markdown-body h2 .octicon-link, 97 | .markdown-body h3 .octicon-link, 98 | .markdown-body h4 .octicon-link, 99 | .markdown-body h5 .octicon-link, 100 | .markdown-body h6 .octicon-link { 101 | color: #c9d1d9; 102 | vertical-align: middle; 103 | visibility: hidden; 104 | } 105 | 106 | .markdown-body h1:hover .anchor, 107 | .markdown-body h2:hover .anchor, 108 | .markdown-body h3:hover .anchor, 109 | .markdown-body h4:hover .anchor, 110 | .markdown-body h5:hover .anchor, 111 | .markdown-body h6:hover .anchor { 112 | text-decoration: none; 113 | } 114 | 115 | .markdown-body h1:hover .anchor .octicon-link, 116 | .markdown-body h2:hover .anchor .octicon-link, 117 | .markdown-body h3:hover .anchor .octicon-link, 118 | .markdown-body h4:hover .anchor .octicon-link, 119 | .markdown-body h5:hover .anchor .octicon-link, 120 | .markdown-body h6:hover .anchor .octicon-link { 121 | visibility: visible; 122 | } 123 | 124 | .markdown-body h1 tt, 125 | .markdown-body h1 code, 126 | .markdown-body h2 tt, 127 | .markdown-body h2 code, 128 | .markdown-body h3 tt, 129 | .markdown-body h3 code, 130 | .markdown-body h4 tt, 131 | .markdown-body h4 code, 132 | .markdown-body h5 tt, 133 | .markdown-body h5 code, 134 | .markdown-body h6 tt, 135 | .markdown-body h6 code { 136 | padding: 0 0.2em; 137 | font-size: inherit; 138 | } 139 | 140 | .markdown-body h1 { 141 | padding-bottom: 0.3em; 142 | font-size: 2em; 143 | border-bottom: 1px solid #21262d; 144 | } 145 | 146 | .markdown-body h2 { 147 | padding-bottom: 0.3em; 148 | font-size: 1.5em; 149 | border-bottom: 1px solid #21262d; 150 | } 151 | 152 | .markdown-body h3 { 153 | font-size: 1.25em; 154 | } 155 | 156 | .markdown-body h4 { 157 | font-size: 1em; 158 | } 159 | 160 | .markdown-body h5 { 161 | font-size: 0.875em; 162 | } 163 | 164 | .markdown-body h6 { 165 | font-size: 0.85em; 166 | color: #8b949e; 167 | } 168 | 169 | .markdown-body ul, 170 | .markdown-body ol { 171 | padding-left: 2em; 172 | } 173 | 174 | .markdown-body ul.no-list, 175 | .markdown-body ol.no-list { 176 | padding: 0; 177 | list-style-type: none; 178 | } 179 | 180 | .markdown-body ul ul, 181 | .markdown-body ul ol, 182 | .markdown-body ol ol, 183 | .markdown-body ol ul { 184 | margin-top: 0; 185 | margin-bottom: 0; 186 | } 187 | 188 | .markdown-body li > p { 189 | margin-top: 16px; 190 | } 191 | 192 | .markdown-body li + li { 193 | margin-top: 0.25em; 194 | } 195 | 196 | .markdown-body dl { 197 | padding: 0; 198 | } 199 | 200 | .markdown-body dl dt { 201 | padding: 0; 202 | margin-top: 16px; 203 | font-size: 1em; 204 | font-style: italic; 205 | font-weight: 600; 206 | } 207 | 208 | .markdown-body dl dd { 209 | padding: 0 16px; 210 | margin-bottom: 16px; 211 | } 212 | 213 | .markdown-body table { 214 | display: block; 215 | width: 100%; 216 | overflow: auto; 217 | } 218 | 219 | .markdown-body table th { 220 | font-weight: 600; 221 | } 222 | 223 | .markdown-body table th, 224 | .markdown-body table td { 225 | padding: 6px 13px; 226 | border: 1px solid #30363d; 227 | } 228 | 229 | .markdown-body table tr { 230 | background-color: #0d1117; 231 | border-top: 1px solid #21262d; 232 | } 233 | 234 | .markdown-body table tr:nth-child(2n) { 235 | background-color: #161b22; 236 | } 237 | 238 | .markdown-body table img { 239 | background-color: transparent; 240 | } 241 | 242 | .markdown-body img { 243 | max-width: 100%; 244 | box-sizing: content-box; 245 | background-color: #0d1117; 246 | } 247 | 248 | .markdown-body img[align='right'] { 249 | padding-left: 20px; 250 | } 251 | 252 | .markdown-body img[align='left'] { 253 | padding-right: 20px; 254 | } 255 | 256 | .markdown-body .emoji { 257 | max-width: none; 258 | vertical-align: text-top; 259 | background-color: transparent; 260 | } 261 | 262 | .markdown-body span.frame { 263 | display: block; 264 | overflow: hidden; 265 | } 266 | 267 | .markdown-body span.frame > span { 268 | display: block; 269 | float: left; 270 | width: auto; 271 | padding: 7px; 272 | margin: 13px 0 0; 273 | overflow: hidden; 274 | border: 1px solid #30363d; 275 | } 276 | 277 | .markdown-body span.frame span img { 278 | display: block; 279 | float: left; 280 | } 281 | 282 | .markdown-body span.frame span span { 283 | display: block; 284 | padding: 5px 0 0; 285 | clear: both; 286 | color: #c9d1d9; 287 | } 288 | 289 | .markdown-body span.align-center { 290 | display: block; 291 | overflow: hidden; 292 | clear: both; 293 | } 294 | 295 | .markdown-body span.align-center > span { 296 | display: block; 297 | margin: 13px auto 0; 298 | overflow: hidden; 299 | text-align: center; 300 | } 301 | 302 | .markdown-body span.align-center span img { 303 | margin: 0 auto; 304 | text-align: center; 305 | } 306 | 307 | .markdown-body span.align-right { 308 | display: block; 309 | overflow: hidden; 310 | clear: both; 311 | } 312 | 313 | .markdown-body span.align-right > span { 314 | display: block; 315 | margin: 13px 0 0; 316 | overflow: hidden; 317 | text-align: right; 318 | } 319 | 320 | .markdown-body span.align-right span img { 321 | margin: 0; 322 | text-align: right; 323 | } 324 | 325 | .markdown-body span.float-left { 326 | display: block; 327 | float: left; 328 | margin-right: 13px; 329 | overflow: hidden; 330 | } 331 | 332 | .markdown-body span.float-left span { 333 | margin: 13px 0 0; 334 | } 335 | 336 | .markdown-body span.float-right { 337 | display: block; 338 | float: right; 339 | margin-left: 13px; 340 | overflow: hidden; 341 | } 342 | 343 | .markdown-body span.float-right > span { 344 | display: block; 345 | margin: 13px auto 0; 346 | overflow: hidden; 347 | text-align: right; 348 | } 349 | 350 | .markdown-body code, 351 | .markdown-body tt { 352 | padding: 0.2em 0.4em; 353 | margin: 0; 354 | font-size: 85%; 355 | background-color: rgba(110, 118, 129, 0.4); 356 | border-radius: 6px; 357 | } 358 | 359 | .markdown-body code br, 360 | .markdown-body tt br { 361 | display: none; 362 | } 363 | 364 | .markdown-body pre > code { 365 | color: #fffc; 366 | } 367 | 368 | .markdown-body del code { 369 | text-decoration: inherit; 370 | } 371 | 372 | .markdown-body pre { 373 | word-wrap: normal; 374 | } 375 | 376 | .markdown-body pre > code { 377 | padding: 0; 378 | margin: 0; 379 | font-size: 100%; 380 | word-break: normal; 381 | white-space: pre; 382 | background: transparent; 383 | border: 0; 384 | } 385 | 386 | .markdown-body .highlight { 387 | margin-bottom: 16px; 388 | } 389 | 390 | .markdown-body .highlight pre { 391 | margin-bottom: 0; 392 | word-break: normal; 393 | } 394 | 395 | .markdown-body .highlight pre, 396 | .markdown-body pre { 397 | padding: 16px; 398 | overflow: auto; 399 | font-size: 85%; 400 | line-height: 1.45; 401 | background-color: #161b22; 402 | border-radius: 6px; 403 | } 404 | 405 | .markdown-body pre code, 406 | .markdown-body pre tt { 407 | display: inline; 408 | max-width: auto; 409 | padding: 0; 410 | margin: 0; 411 | overflow: visible; 412 | line-height: inherit; 413 | word-wrap: normal; 414 | background-color: transparent; 415 | border: 0; 416 | } 417 | 418 | .markdown-body .csv-data td, 419 | .markdown-body .csv-data th { 420 | padding: 5px; 421 | overflow: hidden; 422 | font-size: 12px; 423 | line-height: 1; 424 | text-align: left; 425 | white-space: nowrap; 426 | } 427 | 428 | .markdown-body .csv-data .blob-num { 429 | padding: 10px 8px 9px; 430 | text-align: right; 431 | background: #0d1117; 432 | border: 0; 433 | } 434 | 435 | .markdown-body .csv-data tr { 436 | border-top: 0; 437 | } 438 | 439 | .markdown-body .csv-data th { 440 | font-weight: 600; 441 | background: #161b22; 442 | border-top: 0; 443 | } 444 | 445 | .markdown-body [data-footnote-ref]::before { 446 | content: '['; 447 | } 448 | 449 | .markdown-body [data-footnote-ref]::after { 450 | content: ']'; 451 | } 452 | 453 | .markdown-body .footnotes { 454 | font-size: 12px; 455 | color: #8b949e; 456 | border-top: 1px solid #30363d; 457 | } 458 | 459 | .markdown-body .footnotes ol { 460 | padding-left: 16px; 461 | } 462 | 463 | .markdown-body .footnotes li { 464 | position: relative; 465 | } 466 | 467 | .markdown-body .footnotes li:target::before { 468 | position: absolute; 469 | top: -8px; 470 | right: -8px; 471 | bottom: -8px; 472 | left: -24px; 473 | pointer-events: none; 474 | content: ''; 475 | border: 2px solid #1f6feb; 476 | border-radius: 6px; 477 | } 478 | 479 | .markdown-body .footnotes li:target { 480 | color: #c9d1d9; 481 | } 482 | 483 | .markdown-body .footnotes .data-footnote-backref g-emoji { 484 | font-family: monospace; 485 | } 486 | 487 | .markdown-body .task-list-item { 488 | list-style-type: none; 489 | } 490 | 491 | .markdown-body .task-list-item label { 492 | font-weight: 400; 493 | } 494 | 495 | .markdown-body .task-list-item.enabled label { 496 | cursor: pointer; 497 | } 498 | 499 | .markdown-body .task-list-item + .task-list-item { 500 | margin-top: 3px; 501 | } 502 | 503 | .markdown-body .task-list-item .handle { 504 | display: none; 505 | } 506 | 507 | .markdown-body .task-list-item-checkbox { 508 | margin: 0 0.2em 0.25em -1.6em; 509 | vertical-align: middle; 510 | } 511 | 512 | .markdown-body .contains-task-list:dir(rtl) .task-list-item-checkbox { 513 | margin: 0 -1.6em 0.25em 0.2em; 514 | } 515 | 516 | .markdown-body ::-webkit-calendar-picker-indicator { 517 | filter: invert(50%); 518 | } 519 | } 520 | --------------------------------------------------------------------------------