├── .env ├── .gitattributes ├── .prettierignore ├── prisma ├── README.md └── postgresql │ └── migrations │ ├── 20231026031107_add_authenticator_field_name │ └── migration.sql │ ├── 20231021115443_add_paste_field_type │ └── migration.sql │ ├── migration_lock.toml │ ├── 20231021123408_add_fields │ └── migration.sql │ ├── 20231024065713_add_web_authn_fields │ └── migration.sql │ └── 20231026133156_recreate_next_auth_tables │ └── migration.sql ├── app ├── favicon.ico ├── [locale] │ ├── (app) │ │ ├── dashboard │ │ │ ├── snippets │ │ │ │ ├── _components │ │ │ │ │ ├── snippet.module.scss │ │ │ │ │ ├── button.tsx │ │ │ │ │ ├── modal.tsx │ │ │ │ │ └── snippet.tsx │ │ │ │ ├── loading.tsx │ │ │ │ └── page.tsx │ │ │ ├── _components │ │ │ │ ├── shell.tsx │ │ │ │ ├── header.tsx │ │ │ │ ├── skeleton.tsx │ │ │ │ └── aside-nav.tsx │ │ │ ├── settings │ │ │ │ ├── page.tsx │ │ │ │ ├── layout.tsx │ │ │ │ ├── loading.tsx │ │ │ │ ├── @profile │ │ │ │ │ ├── page.tsx │ │ │ │ │ └── loading.tsx │ │ │ │ └── _components │ │ │ │ │ ├── app-settings.tsx │ │ │ │ │ └── button.tsx │ │ │ ├── notifications │ │ │ │ └── page.tsx │ │ │ ├── page.tsx │ │ │ └── layout.tsx │ │ ├── v │ │ │ └── [cuid] │ │ │ │ ├── _components │ │ │ │ ├── shiki │ │ │ │ │ ├── shiki.scss │ │ │ │ │ ├── LineNumbers.tsx │ │ │ │ │ └── Header.tsx │ │ │ │ ├── CodePreviewIntlProvider.tsx │ │ │ │ └── CodePreview.module.scss │ │ │ │ └── raw │ │ │ │ └── route.ts │ │ ├── about │ │ │ └── page.tsx │ │ ├── _components │ │ │ ├── home │ │ │ │ ├── form.tsx │ │ │ │ └── Announcement.tsx │ │ │ └── layout │ │ │ │ └── header │ │ │ │ └── Navigation.tsx │ │ ├── page.tsx │ │ └── layout.tsx │ ├── (auth) │ │ ├── auth │ │ │ ├── error │ │ │ │ ├── page.tsx │ │ │ │ └── _components │ │ │ │ │ └── preview.tsx │ │ │ ├── password │ │ │ │ └── reset │ │ │ │ │ ├── page.tsx │ │ │ │ │ ├── _components │ │ │ │ │ └── form.tsx │ │ │ │ │ └── [token] │ │ │ │ │ ├── page.tsx │ │ │ │ │ └── _components │ │ │ │ │ └── form.tsx │ │ │ ├── signin │ │ │ │ └── _components │ │ │ │ │ └── OAuthProvider.tsx │ │ │ └── signup │ │ │ │ └── page.tsx │ │ ├── _components │ │ │ └── layouts │ │ │ │ ├── logo.tsx │ │ │ │ ├── content.tsx │ │ │ │ └── go-back-button.tsx │ │ ├── layout.module.scss │ │ └── layout.tsx │ └── layout.tsx ├── api │ ├── auth │ │ ├── [...nextauth] │ │ │ └── route.ts │ │ └── signup │ │ │ └── _route.ts │ ├── task │ │ └── route.ts │ └── user │ │ └── authenticators │ │ └── route.ts ├── layout.tsx ├── not-found.tsx └── error.tsx ├── .husky ├── pre-commit └── commit-msg ├── enums ├── user.ts ├── paste.ts ├── app.ts └── response.ts ├── .prettierrc.js ├── config ├── app.tsx └── dashboard.tsx ├── libs ├── auth │ ├── index.ts │ ├── providers.tsx │ └── config │ │ └── edge.ts ├── requests.ts ├── framer │ └── shaking.ts ├── validation │ └── user.ts ├── navigation.ts ├── prisma │ └── client.ts ├── i18n.tsx ├── services │ └── users │ │ └── user.ts └── shiki │ └── extends.ts ├── vite.config.ts ├── vercel.json ├── postcss.config.js ├── utils ├── fs.ts ├── user.ts ├── helper.ts ├── middlewares.ts ├── actions.ts ├── formatter.ts ├── cookies.ts ├── types.ts ├── app.ts ├── response.ts └── strings.ts ├── .vscode ├── settings.json ├── i18n-ally-custom-framework.yml └── unocss.json ├── .editorconfig ├── lint-staged.config.js ├── .env.example ├── components ├── ui │ ├── provider.tsx │ ├── link-button.tsx │ ├── close-button.tsx │ ├── timeline.tsx │ ├── radio.tsx │ ├── number-input.tsx │ ├── status.tsx │ ├── checkbox.tsx │ ├── rating.tsx │ ├── blockquote.tsx │ ├── pin-input.tsx │ ├── data-list.tsx │ ├── progress.tsx │ ├── hover-card.tsx │ ├── field.tsx │ ├── tag.tsx │ ├── empty-state.tsx │ ├── button.tsx │ ├── progress-circle.tsx │ ├── toaster.tsx │ ├── action-bar.tsx │ ├── segmented-control.tsx │ ├── breadcrumb.tsx │ ├── skeleton.tsx │ ├── tooltip.tsx │ ├── switch.tsx │ ├── native-select.tsx │ ├── alert.tsx │ ├── accordion.tsx │ ├── stepper-input.tsx │ ├── toggle.tsx │ ├── drawer.tsx │ ├── input-group.tsx │ ├── popover.tsx │ ├── dialog.tsx │ ├── radio-card.tsx │ ├── checkbox-card.tsx │ ├── stat.tsx │ ├── toggle-tip.tsx │ ├── color-mode.tsx │ ├── avatar.tsx │ ├── steps.tsx │ └── slider.tsx ├── provider.tsx ├── server-provider.tsx ├── navigation-link.tsx ├── status.tsx └── uno-css-indicator.tsx ├── .commitlintrc.js ├── types └── index.d.ts ├── public ├── vercel.svg └── next.svg ├── .gitignore ├── tsconfig.json ├── .eslintrc.js ├── renovate.json ├── .stylelintrc.js ├── hooks ├── requests │ └── user.ts ├── toast.ts └── actions.ts ├── scripts ├── tools │ └── gen-oauth.ts └── prisma │ ├── migrate.ts │ ├── db.ts │ └── generate.ts ├── README.md ├── env.mjs ├── uno.config.ts ├── actions └── paste.ts ├── .github └── workflows │ └── node.yml └── styles └── global.scss /.env: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.lockb binary diff=lockb 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .next/ 2 | auto-imports.d.ts 3 | pnpm-lock.yaml 4 | -------------------------------------------------------------------------------- /prisma/README.md: -------------------------------------------------------------------------------- 1 | # Prisma 2 | 3 | 本目录存放相关文件,分目录存放各个数据库的配置文件。 4 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greenhat616/pastebin/HEAD/app/favicon.ico -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | pnpm lint-staged 5 | -------------------------------------------------------------------------------- /enums/user.ts: -------------------------------------------------------------------------------- 1 | export enum Role { 2 | User = 1, // Normal user 3 | Admin = 100 // Admin user 4 | } 5 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | pnpm commitlint --edit $1 5 | -------------------------------------------------------------------------------- /app/[locale]/(app)/dashboard/snippets/_components/snippet.module.scss: -------------------------------------------------------------------------------- 1 | .snippet { 2 | @apply flex; 3 | } 4 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | semi: false, 3 | trailingComma: 'none', 4 | singleQuote: true 5 | } 6 | -------------------------------------------------------------------------------- /enums/paste.ts: -------------------------------------------------------------------------------- 1 | export enum PasteType { 2 | Normal = 1, // Normal mode 3 | Gist = 2 // Gist like mode 4 | } 5 | -------------------------------------------------------------------------------- /app/[locale]/(app)/v/[cuid]/_components/shiki/shiki.scss: -------------------------------------------------------------------------------- 1 | .code-preview { 2 | .shiki { 3 | overflow: inherit; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /config/app.tsx: -------------------------------------------------------------------------------- 1 | export const appConfig = { 2 | i18n: { 3 | en: 'English - en', 4 | 'zh-CN': '简体中文 - zh-CN' 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /prisma/postgresql/migrations/20231026031107_add_authenticator_field_name/migration.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "authenticators" ADD COLUMN "name" TEXT; 2 | -------------------------------------------------------------------------------- /prisma/postgresql/migrations/20231021115443_add_paste_field_type/migration.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "public"."pastes" ADD COLUMN "type" INTEGER NOT NULL DEFAULT 1; 2 | -------------------------------------------------------------------------------- /app/[locale]/(auth)/auth/error/page.tsx: -------------------------------------------------------------------------------- 1 | import ErrorPreview from './_components/preview' 2 | 3 | export default async function ErrorPage() { 4 | return 5 | } 6 | -------------------------------------------------------------------------------- /prisma/postgresql/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "postgresql" -------------------------------------------------------------------------------- /enums/app.ts: -------------------------------------------------------------------------------- 1 | export enum RuntimeMode { 2 | Development = 0, 3 | Production = 1, 4 | Test 5 | } 6 | 7 | export enum CredentialsAuthType { 8 | Password = 0, 9 | WebAuthn = 1 10 | } 11 | -------------------------------------------------------------------------------- /libs/auth/index.ts: -------------------------------------------------------------------------------- 1 | import NextAuth from 'next-auth' 2 | import 'server-only' 3 | import { authConfig } from './config' 4 | 5 | export const { auth, signIn, signOut, handlers } = NextAuth(authConfig) 6 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import tsconfigPaths from 'vite-tsconfig-paths' 3 | 4 | export default defineConfig({ 5 | root: './', 6 | base: './', 7 | plugins: [tsconfigPaths()] 8 | }) 9 | -------------------------------------------------------------------------------- /app/[locale]/(app)/about/page.tsx: -------------------------------------------------------------------------------- 1 | type Props = { 2 | params: Promise<{ locale: string }> 3 | } 4 | 5 | export default function About(props: Props) { 6 | return ( 7 |
8 |

About

9 |
10 | ) 11 | } 12 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://openapi.vercel.sh/vercel.json", 3 | "cleanUrls": true, 4 | "crons": [ 5 | { 6 | "path": "/api/task", 7 | "schedule": "5 0 * * *" 8 | } 9 | ], 10 | "trailingSlash": false 11 | } 12 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | 'postcss-import': {}, 4 | tailwindcss: {}, 5 | // '@unocss/postcss': { 6 | // content: ['**/*.{html,js,ts,jsx,tsx}'] 7 | // }, 8 | autoprefixer: {} 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /prisma/postgresql/migrations/20231021123408_add_fields/migration.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "public"."users" ADD COLUMN "website" TEXT; 2 | ALTER TABLE "public"."users" ADD COLUMN "bio" TEXT; 3 | ALTER TABLE "public"."pastes" ADD COLUMN "syntax" TEXT NOT NULL DEFAULT 'text'; 4 | -------------------------------------------------------------------------------- /utils/fs.ts: -------------------------------------------------------------------------------- 1 | import { readdir } from 'node:fs/promises' 2 | 3 | export async function getDictionaries(path: string) { 4 | return (await readdir(path, { withFileTypes: true })) 5 | .filter((dirent) => dirent.isDirectory()) 6 | .map((dirent) => dirent.name) 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "css.customData": [".vscode/unocss.json"], 3 | "i18n-ally.localesPaths": ["messages"], 4 | "i18n-ally.keystyle": "nested", 5 | "typescript.tsdk": "node_modules/typescript/lib", 6 | "typescript.updateImportsOnFileMove.enabled": "always" 7 | } 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /app/[locale]/(app)/dashboard/_components/shell.tsx: -------------------------------------------------------------------------------- 1 | import { Grid } from '@chakra-ui/react' 2 | 3 | export default function Shell({ children }: { children: React.ReactNode }) { 4 | return ( 5 | 6 | {children} 7 | 8 | ) 9 | } 10 | -------------------------------------------------------------------------------- /app/api/auth/[...nextauth]/route.ts: -------------------------------------------------------------------------------- 1 | // export { GET, POST } from '@/libs/auth' 2 | 3 | import { handlers } from '@/libs/auth' 4 | // import NextAuth from 'next-auth/next' 5 | 6 | // const handler = NextAuth(config) 7 | // export { handler as GET, handler as POST } 8 | export const { GET, POST } = handlers 9 | -------------------------------------------------------------------------------- /lint-staged.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | '**.{ts,tsx}': [ 3 | 'prettier --write', 4 | 'eslint -c .eslintrc.js', 5 | () => 'tsc -p tsconfig.json --noEmit' 6 | ], 7 | '**.{js,jsx}': ['prettier --write', 'eslint -c .eslintrc.js'], 8 | '*.scss,*.css': ['stylelint --config .stylelintrc.json'] 9 | } 10 | -------------------------------------------------------------------------------- /app/[locale]/(auth)/auth/password/reset/page.tsx: -------------------------------------------------------------------------------- 1 | import { Stack } from '@chakra-ui/react' 2 | import { ApplyPasswordResetForm } from './_components/form' 3 | 4 | export default async function PasswordResetPage() { 5 | return ( 6 | 7 | 8 | 9 | ) 10 | } 11 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | ############### 2 | # Database # 3 | ############### 4 | DB_ADAPTER=postgresql 5 | PG_URL= 6 | PG_DIRECT_URL= 7 | 8 | NEXT_PUBLIC_APP_URL=http://localhost:6754 9 | NEXTAUTH_SECRET= 10 | NEXTAUTH_URL=http://localhost:6754 11 | AUTH_GITHUB_ID= 12 | AUTH_GITHUB_SECRET= 13 | 14 | AUTH_GOOGLE_ID= 15 | AUTH_GOOGLE_SECRET= 16 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react' 2 | 3 | type Props = { 4 | children: ReactNode 5 | } 6 | 7 | // Since we have a `not-found.tsx` page on the root, a layout file 8 | // is required, even if it's just passing children through. 9 | export default function RootLayout({ children }: Props) { 10 | return children 11 | } 12 | -------------------------------------------------------------------------------- /libs/auth/providers.tsx: -------------------------------------------------------------------------------- 1 | import GitHubIcon from '~icons/mdi/github' 2 | import GoogleIcon from '~icons/mdi/google' 3 | export const providers = [ 4 | { 5 | id: 'github', 6 | name: 'GitHub', 7 | icon: 8 | }, 9 | { 10 | id: 'google', 11 | name: 'Google', 12 | icon: 13 | } 14 | ] 15 | -------------------------------------------------------------------------------- /app/[locale]/(auth)/_components/layouts/logo.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import AnimatedLogo, { 4 | type AnimatedLogoProps 5 | } from '@/components/animated-logo' 6 | import { usePathname } from 'next/navigation' 7 | 8 | export default function Logo(props: AnimatedLogoProps) { 9 | const pathname = usePathname() 10 | return 11 | } 12 | -------------------------------------------------------------------------------- /app/[locale]/(app)/_components/home/form.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { CreateNormalSnippet } from '@/components/form' 4 | 5 | export function CreateSnippetForm({ 6 | nickname, 7 | className 8 | }: { 9 | nickname?: string 10 | className?: string 11 | }) { 12 | return ( 13 | 14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /components/ui/provider.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { ChakraProvider, defaultSystem } from '@chakra-ui/react' 4 | import { ColorModeProvider, type ColorModeProviderProps } from './color-mode' 5 | 6 | export function Provider(props: ColorModeProviderProps) { 7 | return ( 8 | 9 | 10 | 11 | ) 12 | } 13 | -------------------------------------------------------------------------------- /libs/requests.ts: -------------------------------------------------------------------------------- 1 | import { QueryClient } from '@tanstack/react-query' 2 | import 'client-only' 3 | import { ofetch } from 'ofetch' 4 | // Requests should 5 | 6 | export const ofetchClient = ofetch.create({ 7 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 8 | // @ts-ignore ts(7017) 9 | fetch: globalThis._nextOriginalFetch || globalThis.fetch 10 | }) 11 | 12 | export const queryClient = new QueryClient() 13 | -------------------------------------------------------------------------------- /app/[locale]/(auth)/auth/password/reset/_components/form.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { Button, Input } from '@chakra-ui/react' 4 | 5 | export function ApplyPasswordResetForm() { 6 | return ( 7 | <> 8 | 9 | 12 | 13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /components/provider.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import { queryClient } from '@/libs/requests' 3 | import { QueryClientProvider as ReactQueryClientProvider } from '@tanstack/react-query' 4 | 5 | export function QueryClientProvider({ 6 | children 7 | }: { 8 | children: React.ReactNode 9 | }) { 10 | return ( 11 | 12 | {children} 13 | 14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /.commitlintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@commitlint/config-conventional'], 3 | rules: { 4 | 'type-enum': [ 5 | 2, 6 | 'always', 7 | [ 8 | 'build', 9 | 'ci', 10 | 'chore', 11 | 'docs', 12 | 'feat', 13 | 'fix', 14 | 'perf', 15 | 'refactor', 16 | 'revert', 17 | 'style', 18 | 'test' 19 | ] 20 | ] 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/not-found.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import Error from 'next/error' 4 | 5 | // Render the default Next.js 404 page when a route 6 | // is requested that doesn't match the middleware and 7 | // therefore doesn't have a locale associated with it. 8 | 9 | export default function NotFound() { 10 | return ( 11 | 12 | 13 | 14 | 15 | 16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /app/[locale]/(auth)/auth/password/reset/[token]/page.tsx: -------------------------------------------------------------------------------- 1 | import { Stack } from '@chakra-ui/react' 2 | import { PasswordResetForm } from './_components/form' 3 | 4 | type Props = { 5 | params: Promise<{ 6 | token: string 7 | }> 8 | } 9 | 10 | export default function ResetPasswordPage(props: Props) { 11 | // 1. verify token 12 | // 2. if token is valid, show reset password form 13 | return ( 14 | 15 | 16 | 17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /components/ui/link-button.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import type { HTMLChakraProps, RecipeProps } from '@chakra-ui/react' 4 | import { createRecipeContext } from '@chakra-ui/react' 5 | 6 | export interface LinkButtonProps 7 | extends HTMLChakraProps<'a', RecipeProps<'button'>> {} 8 | 9 | const { withContext } = createRecipeContext({ key: 'button' }) 10 | 11 | // Replace "a" with your framework's link component 12 | export const LinkButton = withContext('a') 13 | -------------------------------------------------------------------------------- /app/[locale]/(auth)/_components/layouts/content.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { motion } from 'framer-motion' 4 | import React from 'react' 5 | 6 | type Props = { 7 | className?: string 8 | children: React.ReactNode 9 | } 10 | 11 | export default function Content(props: Props) { 12 | return ( 13 | 18 | {props.children} 19 | 20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /app/[locale]/(app)/dashboard/settings/page.tsx: -------------------------------------------------------------------------------- 1 | import { auth } from '@/libs/auth' 2 | import { Card, CardBody, Heading } from '@chakra-ui/react' 3 | 4 | import AppSettings from './_components/app-settings' 5 | 6 | export default async function DashboardPage() { 7 | const session = await auth() 8 | return ( 9 | 10 | 11 | Apps 12 | 13 | 14 | 15 | 16 | 17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /app/[locale]/(auth)/auth/password/reset/[token]/_components/form.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { Button, Input } from '@chakra-ui/react' 4 | 5 | export function PasswordResetForm() { 6 | return ( 7 | <> 8 | 9 | 15 | 18 | 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export type NavItem = { 4 | title: string 5 | href: string 6 | disabled?: boolean 7 | } 8 | 9 | export type MainNavItem = NavItem 10 | 11 | export type SidebarNavItem = { 12 | title: string 13 | disabled?: boolean 14 | external?: boolean 15 | icon?: React.ReactElement 16 | } & ( 17 | | { 18 | href: string 19 | items?: never 20 | } 21 | | { 22 | href?: string 23 | items: NavLink[] 24 | } 25 | ) 26 | 27 | export type DashboardConfig = { 28 | sidebarNavItems: SidebarNavItem[] 29 | } 30 | -------------------------------------------------------------------------------- /utils/user.ts: -------------------------------------------------------------------------------- 1 | import { env } from '@/env.mjs' 2 | import { User } from '@prisma/client' 3 | import md5 from 'md5' 4 | import { type Session } from 'next-auth' 5 | 6 | export function getUserAvatar(session: Session | User | undefined | null) { 7 | const user: Partial> = 8 | (session as Session)?.user || (session as User) || undefined 9 | // console.log(user) 10 | return ( 11 | user?.avatar ?? 12 | env.NEXT_PUBLIC_AUTH_GRAVATAR_MIRROR.replace( 13 | '{hash}', 14 | user?.email ? md5(user.email) : '' 15 | ) 16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /components/ui/close-button.tsx: -------------------------------------------------------------------------------- 1 | import type { ButtonProps } from '@chakra-ui/react' 2 | import { IconButton as ChakraIconButton } from '@chakra-ui/react' 3 | import * as React from 'react' 4 | import { LuX } from 'react-icons/lu' 5 | 6 | export type CloseButtonProps = ButtonProps 7 | 8 | export const CloseButton = React.forwardRef< 9 | HTMLButtonElement, 10 | CloseButtonProps 11 | >(function CloseButton(props, ref) { 12 | return ( 13 | 14 | {props.children ?? } 15 | 16 | ) 17 | }) 18 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.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 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env*.local 29 | 30 | # vercel 31 | .vercel 32 | 33 | # typescript 34 | *.tsbuildinfo 35 | next-env.d.ts 36 | 37 | .idea/ 38 | 39 | .eslintcache 40 | .stylelintcache 41 | 42 | tsx-0/ 43 | tsconfig.tsbuildinfo -------------------------------------------------------------------------------- /app/[locale]/(app)/dashboard/_components/header.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default function Header({ 4 | text, 5 | heading, 6 | children 7 | }: { 8 | text?: React.ReactNode 9 | heading: React.ReactNode 10 | children?: React.ReactNode 11 | }) { 12 | return ( 13 |
14 |
15 |

16 | {heading} 17 |

18 | {text &&

{text}

} 19 |
20 | {children} 21 |
22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /app/[locale]/(app)/dashboard/settings/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Grid } from '@chakra-ui/react' 2 | import React from 'react' 3 | import Header from '../_components/header' 4 | import Shell from '../_components/shell' 5 | 6 | type SettingsLayoutProps = { 7 | profile: React.ReactNode 8 | children: React.ReactNode // Site settings 9 | } 10 | 11 | export default async function SettingsLayout(props: SettingsLayoutProps) { 12 | return ( 13 | 14 |
15 | 16 | {props.profile} 17 | {props.children} 18 | 19 | 20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /libs/framer/shaking.ts: -------------------------------------------------------------------------------- 1 | const getRandomTransformOrigin = () => { 2 | const value = (16 + 40 * Math.random()) / 100 3 | const value2 = (15 + 36 * Math.random()) / 100 4 | return { 5 | originX: value, 6 | originY: value2 7 | } 8 | } 9 | 10 | const getRandomDelay = () => -(Math.random() * 0.7 + 0.05) 11 | 12 | const randomDuration = () => Math.random() * 0.07 + 0.23 13 | 14 | export const variants = { 15 | start: (i: number) => ({ 16 | rotate: i % 2 === 0 ? [-1, 1.3, 0] : [1, -1.4, 0], 17 | transition: { 18 | delay: getRandomDelay(), 19 | repeat: Infinity, 20 | duration: randomDuration() 21 | } 22 | }), 23 | reset: { 24 | rotate: 0 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/error.tsx: -------------------------------------------------------------------------------- 1 | 'use client' // Error components must be Client Components 2 | 3 | import { useEffect } from 'react' 4 | 5 | export default function Error({ 6 | error, 7 | reset 8 | }: { 9 | error: Error & { digest?: string } 10 | reset: () => void 11 | }) { 12 | useEffect(() => { 13 | // Log the error to an error reporting service 14 | console.error(error) 15 | }, [error]) 16 | 17 | return ( 18 |
19 |

Something went wrong!

20 | 28 |
29 | ) 30 | } 31 | -------------------------------------------------------------------------------- /app/[locale]/(app)/v/[cuid]/_components/CodePreviewIntlProvider.tsx: -------------------------------------------------------------------------------- 1 | import { IntlClientProvider } from '@/components/server-provider' 2 | import { pick } from 'lodash-es' 3 | import { useLocale, useMessages, type AbstractIntlMessages } from 'next-intl' 4 | import React from 'react' 5 | 6 | type Props = { 7 | children: React.ReactNode 8 | } 9 | 10 | export default function CodePreviewIntlProvider(props: Props) { 11 | const messages = useMessages() 12 | const locale = useLocale() 13 | 14 | return ( 15 | 19 | {props.children} 20 | 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /app/[locale]/(app)/dashboard/notifications/page.tsx: -------------------------------------------------------------------------------- 1 | import Header from '../_components/header' 2 | import Shell from '../_components/shell' 3 | 4 | export default function NotificationsPage() { 5 | return ( 6 | 7 |
11 |
12 |
13 | 14 |
15 | 16 |

17 | No notifications found. 18 |

19 |
20 | 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /utils/helper.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * classNames is a utility function to join class names together. 3 | * It filters out falsy values. 4 | * @param input 5 | * @return {string} 6 | */ 7 | export function classNames( 8 | ...input: Array 9 | ): string { 10 | input = input.filter(Boolean) as string[] 11 | return input.join(' ') 12 | } 13 | 14 | /** 15 | * getLocalePathname return the pathname, which is the rest of the url after the locale. 16 | * @param {string} pathname - Raw pathname 17 | * @return {string} - Pathname without the first '/' 18 | */ 19 | export function getLocalePathname(pathname: string): string { 20 | if (pathname === '/') return pathname 21 | return pathname.replace(/^\/[a-z]{2}\//, '/') 22 | } 23 | -------------------------------------------------------------------------------- /app/[locale]/(app)/dashboard/page.tsx: -------------------------------------------------------------------------------- 1 | import { auth } from '@/libs/auth' 2 | import { useLocale } from 'next-intl' 3 | import { getTimeZone } from 'next-intl/server' 4 | import Header from './_components/header' 5 | import Shell from './_components/shell' 6 | 7 | export default async function DashboardPage() { 8 | const session = await auth() 9 | // eslint-disable-next-line react-hooks/rules-of-hooks 10 | const locale = useLocale() 11 | const timeZone = await getTimeZone({ locale }) 12 | const now = newDayjs(undefined, { locale, timeZone }) 13 | return ( 14 | 15 |
19 | 20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /components/server-provider.tsx: -------------------------------------------------------------------------------- 1 | // Note that it is a Server Component 2 | import { NextIntlClientProvider, type AbstractIntlMessages } from 'next-intl' 3 | import { getNow, getTimeZone } from 'next-intl/server' 4 | import 'server-only' 5 | 6 | type IntlProviderProps = { 7 | locale: string 8 | messages: AbstractIntlMessages 9 | children: React.ReactNode 10 | } 11 | 12 | export async function IntlClientProvider({ 13 | locale, 14 | messages, 15 | children 16 | }: IntlProviderProps) { 17 | const now = await getNow({ locale }) 18 | const timeZone = await getTimeZone({ locale }) 19 | return ( 20 | 21 | {children} 22 | 23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /app/[locale]/(auth)/_components/layouts/go-back-button.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { useRouter } from '@/libs/navigation' 4 | import { Button, type ButtonProps } from '@chakra-ui/react' 5 | import { useMemoizedFn } from 'ahooks' 6 | 7 | export type Props = { 8 | className?: string 9 | children: React.ReactNode 10 | } & ButtonProps 11 | 12 | export default function GoBackButton(props: Props) { 13 | const router = useRouter() 14 | const goBackFn = useMemoizedFn(() => { 15 | router.back() 16 | }) 17 | 18 | return ( 19 | 27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /app/[locale]/(app)/dashboard/layout.tsx: -------------------------------------------------------------------------------- 1 | import { dashboardConfig } from '@/config/dashboard' 2 | import { auth } from '@/libs/auth' 3 | import { redirect } from '@/libs/navigation' 4 | import { Box, Flex } from '@chakra-ui/react' 5 | import AsideNav from './_components/aside-nav' 6 | 7 | type DashboardLayoutProps = { 8 | children: React.ReactNode 9 | } 10 | 11 | export default async function DashboardLayout(props: DashboardLayoutProps) { 12 | const session = await auth() 13 | if (!session) redirect('/auth/signin') 14 | return ( 15 | 16 | 17 | 18 | {props.children} 19 | 20 | 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /components/navigation-link.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { Link, pathnames } from '@/libs/navigation' 4 | import { useSelectedLayoutSegment } from 'next/navigation' 5 | import { ComponentProps } from 'react' 6 | 7 | export default function NavigationLink< 8 | Pathname extends keyof typeof pathnames 9 | >({ href, ...rest }: ComponentProps>) { 10 | const selectedLayoutSegment = useSelectedLayoutSegment() 11 | const pathname = selectedLayoutSegment ? `/${selectedLayoutSegment}` : '/' 12 | const isActive = pathname === href 13 | 14 | return ( 15 | 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /config/dashboard.tsx: -------------------------------------------------------------------------------- 1 | import { DashboardConfig } from '@/types' 2 | 3 | export const dashboardConfig = { 4 | sidebarNavItems: [ 5 | { 6 | title: 'Overview', 7 | href: '/dashboard', 8 | icon: 9 | }, 10 | { 11 | title: 'Snippets', 12 | href: '/dashboard/snippets', 13 | icon: 14 | }, 15 | { 16 | title: 'Notifications', 17 | href: '/dashboard/notifications', 18 | icon: 19 | }, 20 | { 21 | title: 'Settings', 22 | href: '/dashboard/settings', 23 | icon: 24 | } 25 | ] 26 | } satisfies DashboardConfig 27 | -------------------------------------------------------------------------------- /components/status.tsx: -------------------------------------------------------------------------------- 1 | export type EmptyPlaceholderProps = { 2 | text: string 3 | className?: string 4 | iconClassName?: string 5 | textClassName?: string 6 | } 7 | 8 | export function EmptyPlaceholder(props: EmptyPlaceholderProps) { 9 | return ( 10 |
11 |
12 | 17 |
18 | 19 |

25 | {props.text} 26 |

27 |
28 | ) 29 | } 30 | -------------------------------------------------------------------------------- /components/ui/timeline.tsx: -------------------------------------------------------------------------------- 1 | import { Timeline as ChakraTimeline } from '@chakra-ui/react' 2 | import * as React from 'react' 3 | 4 | export const TimelineConnector = React.forwardRef< 5 | HTMLDivElement, 6 | ChakraTimeline.IndicatorProps 7 | >(function TimelineConnector(props, ref) { 8 | return ( 9 | 10 | 11 | 12 | 13 | ) 14 | }) 15 | 16 | export const TimelineRoot = ChakraTimeline.Root 17 | export const TimelineContent = ChakraTimeline.Content 18 | export const TimelineItem = ChakraTimeline.Item 19 | export const TimelineIndicator = ChakraTimeline.Indicator 20 | export const TimelineTitle = ChakraTimeline.Title 21 | export const TimelineDescription = ChakraTimeline.Description 22 | -------------------------------------------------------------------------------- /components/uno-css-indicator.tsx: -------------------------------------------------------------------------------- 1 | import { RuntimeMode } from '@/enums/app' 2 | 3 | const mode = getRuntimeMode() 4 | 5 | export function UnoCSSIndicator() { 6 | if (mode === RuntimeMode.Production) return null 7 | return ( 8 |
9 |
xs
10 |
11 | sm 12 |
13 |
md
14 |
lg
15 |
xl
16 |
2xl
17 |
18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /enums/response.ts: -------------------------------------------------------------------------------- 1 | export enum ResponseCode { 2 | NotInvoke = -1, // 未调用 3 | OK = 0, 4 | InternalError = 50, // 内部发生错误 5 | ValidationFailed = 51, // 数据验证失败 6 | DBOperationError = 52, // 数据库操作错误 7 | InvalidParameters = 53, // 当前操作给定的参数无效 8 | MissingParameter = 54, // 当前操作的参数缺失 9 | InvalidOperation = 55, // 函数不能这样使用 10 | InvalidConfiguration = 56, // 当前操作的配置无效 11 | MissingConfiguration = 57, // 当前操作缺少配置 12 | NotImplemented = 58, // 操作尚未执行 13 | NotSupported = 59, // 操作不支持 14 | OperationFailed = 60, // 我试过了,但是我不能给你想要的 15 | NotAuthorized = 61, // 未授权 16 | SecurityReason = 62, // 由于安全原因,操作被拒绝 17 | ServerBusy = 63, // 服务器繁忙 18 | UnknownError = 64, // 未知错误 19 | NotFound = 65, // 资源不存在 20 | InvalidRequest = 66, // 无效的请求 21 | NecessaryPackageNotImported = 67, // 缺少必要的包 22 | BusinessValidationFailed = 300 // 业务验证失败 23 | } 24 | -------------------------------------------------------------------------------- /app/[locale]/(app)/dashboard/settings/loading.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { Card, CardBody, Heading } from '@chakra-ui/react' 4 | import { FormInputSkeleton } from '../_components/skeleton' 5 | 6 | export default function SettingsLoading() { 7 | return ( 8 | 9 | 10 | Apps 11 | 12 | 13 |
14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 |
24 |
25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /app/[locale]/(auth)/layout.module.scss: -------------------------------------------------------------------------------- 1 | .auth-layout { 2 | @apply h-[100vh] w-full flex z-0 justify-center relative; 3 | 4 | .go-back-button { 5 | @apply absolute top-5 left-5 px-5 py-2 hidden md:block; 6 | } 7 | 8 | &::before { 9 | @apply h-[100vh] w-full absolute top-0 left-0 z-0; 10 | 11 | content: ' '; // Required for pseudo element 12 | background: url('https://bingw.jasonzeng.dev/?resolution=UHD&index=random') 13 | no-repeat center center; // Bing random image 14 | 15 | background-size: cover; 16 | } 17 | 18 | .main { 19 | @apply w-[90%] md:w-1/2 xl:w-1/3 2xl:w-1/4 mx-auto mt-20 md:mt-24 z-[1]; 20 | 21 | .logo { 22 | @apply w-20 h-20 mx-auto mb-10; 23 | 24 | .inner { 25 | @apply w-20 h-20; 26 | } 27 | } 28 | 29 | .container { 30 | @apply w-full bg-white rounded-3xl shadow-lg p-10; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /utils/middlewares.ts: -------------------------------------------------------------------------------- 1 | import { URLPattern } from 'next/server' 2 | 3 | type URLPatternResult = Exclude, null> 4 | 5 | export type URLPatternParser = [ 6 | URLPattern, 7 | (patternResult: URLPatternResult) => Record 8 | ] 9 | 10 | /** 11 | * Parse params from URL. 12 | * Fucking Next.js.... 13 | * @param {string} url 14 | * @return {Record} 15 | */ 16 | export const parseURLParams = ( 17 | url: string, 18 | patterns: URLPatternParser[] 19 | ): Partial => { 20 | const input = url.split('?')[0] 21 | let result = {} 22 | 23 | for (const [pattern, handler] of patterns) { 24 | const patternResult = pattern.exec(input) 25 | if (patternResult !== null && 'pathname' in patternResult) { 26 | result = handler(patternResult) 27 | break 28 | } 29 | } 30 | return result 31 | } 32 | -------------------------------------------------------------------------------- /components/ui/radio.tsx: -------------------------------------------------------------------------------- 1 | import { RadioGroup as ChakraRadioGroup } from '@chakra-ui/react' 2 | import * as React from 'react' 3 | 4 | export interface RadioProps extends ChakraRadioGroup.ItemProps { 5 | rootRef?: React.Ref 6 | inputProps?: React.InputHTMLAttributes 7 | } 8 | 9 | export const Radio = React.forwardRef( 10 | function Radio(props, ref) { 11 | const { children, inputProps, rootRef, ...rest } = props 12 | return ( 13 | 14 | 15 | 16 | {children && ( 17 | {children} 18 | )} 19 | 20 | ) 21 | } 22 | ) 23 | 24 | export const RadioGroup = ChakraRadioGroup.Root 25 | -------------------------------------------------------------------------------- /components/ui/number-input.tsx: -------------------------------------------------------------------------------- 1 | import { NumberInput as ChakraNumberInput } from '@chakra-ui/react' 2 | import * as React from 'react' 3 | 4 | export interface NumberInputProps extends ChakraNumberInput.RootProps {} 5 | 6 | export const NumberInputRoot = React.forwardRef< 7 | HTMLDivElement, 8 | NumberInputProps 9 | >(function NumberInput(props, ref) { 10 | const { children, ...rest } = props 11 | return ( 12 | 13 | {children} 14 | 15 | 16 | 17 | 18 | 19 | ) 20 | }) 21 | 22 | export const NumberInputField = ChakraNumberInput.Input 23 | export const NumberInputScrubber = ChakraNumberInput.Scrubber 24 | export const NumberInputLabel = ChakraNumberInput.Label 25 | -------------------------------------------------------------------------------- /components/ui/status.tsx: -------------------------------------------------------------------------------- 1 | import type { ColorPalette } from '@chakra-ui/react' 2 | import { Status as ChakraStatus } from '@chakra-ui/react' 3 | import * as React from 'react' 4 | 5 | type StatusValue = 'success' | 'error' | 'warning' | 'info' 6 | 7 | export interface StatusProps extends ChakraStatus.RootProps { 8 | value?: StatusValue 9 | } 10 | 11 | const statusMap: Record = { 12 | success: 'green', 13 | error: 'red', 14 | warning: 'orange', 15 | info: 'blue' 16 | } 17 | 18 | export const Status = React.forwardRef( 19 | function Status(props, ref) { 20 | const { children, value = 'info', ...rest } = props 21 | const colorPalette = rest.colorPalette ?? statusMap[value] 22 | return ( 23 | 24 | 25 | {children} 26 | 27 | ) 28 | } 29 | ) 30 | -------------------------------------------------------------------------------- /components/ui/checkbox.tsx: -------------------------------------------------------------------------------- 1 | import { Checkbox as ChakraCheckbox } from '@chakra-ui/react' 2 | import * as React from 'react' 3 | 4 | export interface CheckboxProps extends ChakraCheckbox.RootProps { 5 | icon?: React.ReactNode 6 | inputProps?: React.InputHTMLAttributes 7 | rootRef?: React.Ref 8 | } 9 | 10 | export const Checkbox = React.forwardRef( 11 | function Checkbox(props, ref) { 12 | const { icon, children, inputProps, rootRef, ...rest } = props 13 | return ( 14 | 15 | 16 | 17 | {icon || } 18 | 19 | {children != null && ( 20 | {children} 21 | )} 22 | 23 | ) 24 | } 25 | ) 26 | -------------------------------------------------------------------------------- /app/[locale]/(app)/dashboard/settings/@profile/page.tsx: -------------------------------------------------------------------------------- 1 | import { auth } from '@/libs/auth' 2 | import { providers } from '@/libs/auth/providers' 3 | import client from '@/libs/prisma/client' 4 | import { User } from '@prisma/client' 5 | import Profiles from '../_components/profiles' 6 | 7 | async function getSSOs(userID: string) { 8 | const accounts = await client.account.findMany({ 9 | where: { 10 | userId: userID 11 | } 12 | }) 13 | return providers.map((provider) => { 14 | return { 15 | ...provider, 16 | connected: accounts.some((account) => account.provider === provider.id) 17 | } 18 | }) 19 | } 20 | 21 | export default async function ProfilePage() { 22 | const session = await auth() 23 | if (!session) return null 24 | const user = await client.user.findUnique({ 25 | where: { 26 | id: session.user.id 27 | } 28 | }) 29 | const ssos = await getSSOs(session!.user.id) 30 | return 31 | } 32 | -------------------------------------------------------------------------------- /app/[locale]/(app)/v/[cuid]/_components/shiki/LineNumbers.tsx: -------------------------------------------------------------------------------- 1 | import { attrsToLines } from '@/libs/shiki' 2 | import { Box } from '@chakra-ui/react' 3 | import styles from '../CodePreview.module.scss' 4 | type Props = { 5 | lines: number 6 | highlightLines?: string 7 | } 8 | 9 | export default function LineNumbers(props: Props) { 10 | const { lines, highlightLines: linesAttrs } = props 11 | const highlightLines = linesAttrs ? attrsToLines(linesAttrs) : undefined 12 | const linesArray = Array.from({ length: lines }, (_, i) => i + 1) 13 | return ( 14 | 15 | {linesArray.map((line) => ( 16 | 24 | {line} 25 | 26 | ))} 27 | 28 | ) 29 | } 30 | -------------------------------------------------------------------------------- /components/ui/rating.tsx: -------------------------------------------------------------------------------- 1 | import { RatingGroup } from '@chakra-ui/react' 2 | import * as React from 'react' 3 | 4 | export interface RatingProps extends RatingGroup.RootProps { 5 | icon?: React.ReactElement 6 | count?: number 7 | label?: React.ReactNode 8 | } 9 | 10 | export const Rating = React.forwardRef( 11 | function Rating(props, ref) { 12 | const { icon, count = 5, label, ...rest } = props 13 | return ( 14 | 15 | {label && {label}} 16 | 17 | 18 | {Array.from({ length: count }).map((_, index) => ( 19 | 20 | 21 | 22 | ))} 23 | 24 | 25 | ) 26 | } 27 | ) 28 | -------------------------------------------------------------------------------- /app/[locale]/(auth)/auth/error/_components/preview.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { Alert } from '@/components/ui/alert' 4 | import { Button } from '@/components/ui/button' 5 | import { Stack } from '@chakra-ui/react' 6 | 7 | import { useSearchParams } from 'next/navigation' 8 | 9 | export default function ErrorPreview() { 10 | const searchParams = useSearchParams() 11 | const err = searchParams.get('error') || 'Unknown Error' 12 | return ( 13 | 14 | 26 | {err} 27 | 28 | 29 | 32 | 33 | ) 34 | } 35 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "esModuleInterop": true, 5 | "forceConsistentCasingInFileNames": true, 6 | "incremental": true, 7 | "isolatedModules": true, 8 | "jsx": "preserve", 9 | "lib": ["dom", "dom.iterable", "ESNext"], 10 | "module": "ESNext", 11 | "moduleResolution": "Bundler", 12 | "noEmit": true, 13 | "paths": { 14 | "@/*": ["./*"] 15 | }, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "resolveJsonModule": true, 22 | "skipDefaultLibCheck": true, 23 | "skipLibCheck": true, 24 | "strict": true, 25 | "target": "ESNext", 26 | "types": ["unplugin-icons/types/react"] 27 | }, 28 | "exclude": ["node_modules"], 29 | "include": [ 30 | "next-env.d.ts", 31 | "auto-imports.d.ts", 32 | "**/*.ts", 33 | "**/*.tsx", 34 | ".next/types/**/*.ts", 35 | "app/[locale]/(auth)/_components/layouts/go-back.-buttontsx", 36 | "scripts/tools/gen-oauth.mts" 37 | ] 38 | } 39 | -------------------------------------------------------------------------------- /components/ui/blockquote.tsx: -------------------------------------------------------------------------------- 1 | import { Blockquote as ChakraBlockquote } from '@chakra-ui/react' 2 | import * as React from 'react' 3 | 4 | export interface BlockquoteProps extends ChakraBlockquote.RootProps { 5 | cite?: React.ReactNode 6 | citeUrl?: string 7 | icon?: React.ReactNode 8 | showDash?: boolean 9 | } 10 | 11 | export const Blockquote = React.forwardRef( 12 | function Blockquote(props, ref) { 13 | const { children, cite, citeUrl, showDash, icon, ...rest } = props 14 | 15 | return ( 16 | 17 | {icon} 18 | 19 | {children} 20 | 21 | {cite && ( 22 | 23 | {showDash ? <>— : null} {cite} 24 | 25 | )} 26 | 27 | ) 28 | } 29 | ) 30 | 31 | export const BlockquoteIcon = ChakraBlockquote.Icon 32 | -------------------------------------------------------------------------------- /components/ui/pin-input.tsx: -------------------------------------------------------------------------------- 1 | import { PinInput as ChakraPinInput, Group } from '@chakra-ui/react' 2 | import * as React from 'react' 3 | 4 | export interface PinInputProps extends ChakraPinInput.RootProps { 5 | rootRef?: React.Ref 6 | count?: number 7 | inputProps?: React.InputHTMLAttributes 8 | attached?: boolean 9 | } 10 | 11 | export const PinInput = React.forwardRef( 12 | function PinInput(props, ref) { 13 | const { count = 4, inputProps, rootRef, attached, ...rest } = props 14 | return ( 15 | 16 | 17 | 18 | 19 | {Array.from({ length: count }).map((_, index) => ( 20 | 21 | ))} 22 | 23 | 24 | 25 | ) 26 | } 27 | ) 28 | -------------------------------------------------------------------------------- /components/ui/data-list.tsx: -------------------------------------------------------------------------------- 1 | import { DataList as ChakraDataList } from '@chakra-ui/react' 2 | import { InfoTip } from './toggle-tip' 3 | import * as React from 'react' 4 | 5 | export const DataListRoot = ChakraDataList.Root 6 | 7 | interface ItemProps extends ChakraDataList.ItemProps { 8 | label: React.ReactNode 9 | value: React.ReactNode 10 | info?: React.ReactNode 11 | grow?: boolean 12 | } 13 | 14 | export const DataListItem = React.forwardRef( 15 | function DataListItem(props, ref) { 16 | const { label, info, value, children, grow, ...rest } = props 17 | return ( 18 | 19 | 20 | {label} 21 | {info && {info}} 22 | 23 | 24 | {value} 25 | 26 | {children} 27 | 28 | ) 29 | } 30 | ) 31 | -------------------------------------------------------------------------------- /components/ui/progress.tsx: -------------------------------------------------------------------------------- 1 | import { Progress as ChakraProgress } from '@chakra-ui/react' 2 | import { InfoTip } from './toggle-tip' 3 | import * as React from 'react' 4 | 5 | export const ProgressBar = React.forwardRef< 6 | HTMLDivElement, 7 | ChakraProgress.TrackProps 8 | >(function ProgressBar(props, ref) { 9 | return ( 10 | 11 | 12 | 13 | ) 14 | }) 15 | 16 | export interface ProgressLabelProps extends ChakraProgress.LabelProps { 17 | info?: React.ReactNode 18 | } 19 | 20 | export const ProgressLabel = React.forwardRef< 21 | HTMLDivElement, 22 | ProgressLabelProps 23 | >(function ProgressLabel(props, ref) { 24 | const { children, info, ...rest } = props 25 | return ( 26 | 27 | {children} 28 | {info && {info}} 29 | 30 | ) 31 | }) 32 | 33 | export const ProgressRoot = ChakraProgress.Root 34 | export const ProgressValueText = ChakraProgress.ValueText 35 | -------------------------------------------------------------------------------- /libs/validation/user.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | 3 | export const UserProfileSchema = z.object({ 4 | bio: z.string().optional(), 5 | website: z.string().url().optional(), 6 | name: z.string().min(1, { 7 | message: wrapTranslationKey('validation.user.name_too_short') 8 | }) 9 | }) 10 | 11 | export type UserProfile = z.infer 12 | 13 | export const ChangePasswordSchema = z 14 | .object({ 15 | password: z.string({ 16 | required_error: wrapTranslationKey( 17 | 'components.change_password_form.feedback.password.required' 18 | ) 19 | }), 20 | password_confirmation: z.string({ 21 | required_error: wrapTranslationKey( 22 | 'components.change_password_form.feedback.password_confirmation.required' 23 | ) 24 | }) 25 | }) 26 | .refine((data) => data.password === data.password_confirmation, { 27 | message: wrapTranslationKey( 28 | 'components.change_password_form.feedback.password_confirmation.mismatch' 29 | ), 30 | path: ['password_confirmation'] // path of error 31 | }) 32 | -------------------------------------------------------------------------------- /app/[locale]/(auth)/auth/signin/_components/OAuthProvider.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { Stack } from '@chakra-ui/react' 4 | import { Button } from '@/components/ui/button' 5 | import { signIn } from 'next-auth/react' 6 | import { useTranslations } from 'next-intl' 7 | import React from 'react' 8 | 9 | type OAuthProviderProps = { 10 | providers: Array<{ 11 | id: string 12 | name: string 13 | icon: React.ReactElement 14 | }> 15 | } 16 | 17 | export default function OAuthProvider(props: OAuthProviderProps) { 18 | const t = useTranslations() 19 | return ( 20 | 21 | {props.providers.map((provider) => { 22 | return ( 23 | 35 | ) 36 | })} 37 | 38 | ) 39 | } 40 | -------------------------------------------------------------------------------- /components/ui/hover-card.tsx: -------------------------------------------------------------------------------- 1 | import { HoverCard, Portal } from '@chakra-ui/react' 2 | import * as React from 'react' 3 | 4 | interface HoverCardContentProps extends HoverCard.ContentProps { 5 | portalled?: boolean 6 | portalRef?: React.RefObject 7 | } 8 | 9 | export const HoverCardContent = React.forwardRef< 10 | HTMLDivElement, 11 | HoverCardContentProps 12 | >(function HoverCardContent(props, ref) { 13 | const { portalled = true, portalRef, ...rest } = props 14 | 15 | return ( 16 | 17 | 18 | 19 | 20 | 21 | ) 22 | }) 23 | 24 | export const HoverCardArrow = React.forwardRef< 25 | HTMLDivElement, 26 | HoverCard.ArrowProps 27 | >(function HoverCardArrow(props, ref) { 28 | return ( 29 | 30 | 31 | 32 | ) 33 | }) 34 | 35 | export const HoverCardRoot = HoverCard.Root 36 | export const HoverCardTrigger = HoverCard.Trigger 37 | -------------------------------------------------------------------------------- /app/[locale]/(app)/dashboard/settings/@profile/loading.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { Card, Heading } from '@chakra-ui/react' 4 | import { 5 | BlockButtonSkeleton, 6 | FormInputSkeleton, 7 | XLargeAvatarSkeleton 8 | } from '../../_components/skeleton' 9 | 10 | export default function ProfilesLoading() { 11 | return ( 12 | 13 | 14 | Profiles 15 | 16 | 17 |
18 |
19 | 20 | 21 | 22 | 23 |
24 |
25 | 26 | 27 | 28 | 29 |
30 |
31 |
32 |
33 | ) 34 | } 35 | -------------------------------------------------------------------------------- /components/ui/field.tsx: -------------------------------------------------------------------------------- 1 | import { Field as ChakraField } from '@chakra-ui/react' 2 | import * as React from 'react' 3 | 4 | export interface FieldProps extends Omit { 5 | label?: React.ReactNode 6 | helperText?: React.ReactNode 7 | errorText?: React.ReactNode 8 | optionalText?: React.ReactNode 9 | } 10 | 11 | export const Field = React.forwardRef( 12 | function Field(props, ref) { 13 | const { label, children, helperText, errorText, optionalText, ...rest } = 14 | props 15 | return ( 16 | 17 | {label && ( 18 | 19 | {label} 20 | 21 | 22 | )} 23 | {children} 24 | {helperText && ( 25 | {helperText} 26 | )} 27 | {errorText && ( 28 | {errorText} 29 | )} 30 | 31 | ) 32 | } 33 | ) 34 | -------------------------------------------------------------------------------- /app/[locale]/(app)/_components/home/Announcement.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { 4 | // AlertIcon, 5 | AlertTitle, 6 | AlertDescription, 7 | Box, 8 | useDisclosure 9 | } from '@chakra-ui/react' 10 | import { Alert } from '@/components/ui/alert' 11 | import { CloseButton } from '@/components/ui/close-button' 12 | 13 | export default function Announcement() { 14 | const { 15 | open: isVisible, 16 | onClose, 17 | onOpen 18 | } = useDisclosure({ defaultOpen: true }) 19 | 20 | return ( 21 | isVisible && ( 22 | 23 | 24 | Success! 25 | 26 | Your application has been received. We will review your application 27 | and respond within the next 48 hours. 28 | 29 | 30 | 37 | 38 | ) 39 | ) 40 | } 41 | -------------------------------------------------------------------------------- /components/ui/tag.tsx: -------------------------------------------------------------------------------- 1 | import { Tag as ChakraTag } from '@chakra-ui/react' 2 | import * as React from 'react' 3 | 4 | export interface TagProps extends ChakraTag.RootProps { 5 | startElement?: React.ReactNode 6 | endElement?: React.ReactNode 7 | onClose?: VoidFunction 8 | closable?: boolean 9 | } 10 | 11 | export const Tag = React.forwardRef( 12 | function Tag(props, ref) { 13 | const { 14 | startElement, 15 | endElement, 16 | onClose, 17 | closable = !!onClose, 18 | children, 19 | ...rest 20 | } = props 21 | 22 | return ( 23 | 24 | {startElement && ( 25 | {startElement} 26 | )} 27 | {children} 28 | {endElement && ( 29 | {endElement} 30 | )} 31 | {closable && ( 32 | 33 | 34 | 35 | )} 36 | 37 | ) 38 | } 39 | ) 40 | -------------------------------------------------------------------------------- /app/[locale]/(app)/dashboard/_components/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Skeleton, 3 | SkeletonCircle, 4 | SkeletonText 5 | } from '@/components/ui/skeleton' 6 | 7 | export function H2Skeleton() { 8 | return 9 | } 10 | 11 | export function PSkeleton() { 12 | return 13 | } 14 | 15 | export function TextSkeleton(props: { lines?: number }) { 16 | return 17 | } 18 | 19 | export function BlockButtonSkeleton() { 20 | return ( 21 |
22 | 23 |
24 | ) 25 | } 26 | 27 | export function XLargeAvatarSkeleton() { 28 | return ( 29 |
30 | 31 |
32 | ) 33 | } 34 | 35 | export function FormInputSkeleton() { 36 | return ( 37 |
38 | 39 | 40 | 41 |
42 | ) 43 | } 44 | -------------------------------------------------------------------------------- /prisma/postgresql/migrations/20231024065713_add_web_authn_fields/migration.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "users" ADD COLUMN "extra_fields" JSONB NOT NULL DEFAULT '{}'::jsonb; 2 | 3 | -- CreateTable 4 | CREATE TABLE "authenticators" ( 5 | "id" BIGSERIAL NOT NULL, 6 | "credential_id" TEXT NOT NULL, 7 | "credential_public_key" BYTEA NOT NULL, 8 | "counter" INTEGER NOT NULL DEFAULT 0, 9 | "credential_device_type" TEXT NOT NULL, 10 | "credential_backed_up" BOOLEAN NOT NULL, 11 | "transports" TEXT, 12 | "fmt" TEXT NOT NULL, 13 | "user_id" TEXT NOT NULL, 14 | "updated_at" TIMESTAMP(3) NOT NULL, 15 | "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 16 | 17 | CONSTRAINT "authenticators_pkey" PRIMARY KEY ("id") 18 | ); 19 | 20 | -- CreateIndex 21 | CREATE INDEX "credential_id" ON "authenticators"("credential_id"); 22 | 23 | -- CreateIndex 24 | CREATE INDEX "user_id" ON "authenticators"("user_id"); 25 | 26 | -- CreateIndex 27 | CREATE UNIQUE INDEX "authenticators_id_key" ON "authenticators"("id"); 28 | 29 | -- AddForeignKey 30 | ALTER TABLE "authenticators" ADD CONSTRAINT "authenticators_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 31 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | browser: true, 5 | node: true 6 | }, 7 | extends: [ 8 | 'next/core-web-vitals', 9 | 'plugin:@typescript-eslint/recommended', 10 | 'prettier', 11 | 'plugin:prettier/recommended', 12 | './.eslintrc-auto-import.json' 13 | ], 14 | rules: { 15 | 'react/jsx-no-undef': 'off', 16 | '@typescript-eslint/no-empty-object-type': 'warn', 17 | 'no-console': [ 18 | process.env.NODE_ENV === 'production' ? 'error' : 'off', 19 | { allow: ['warn', 'error'] } 20 | ], 21 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off', 22 | 'prettier/prettier': 'off' // turn off prettier rules due to conflict, and should be handled by prettier itself 23 | }, 24 | overrides: [ 25 | { 26 | files: ['*.ts', '*.tsx'], 27 | parser: '@typescript-eslint/parser', 28 | plugins: ['@typescript-eslint'], 29 | rules: { 30 | 'no-undef': 'off', // should be off for typescript 31 | '@typescript-eslint/no-unused-vars': 'warn' 32 | } 33 | } 34 | ], 35 | settings: { 36 | 'import/parsers': { 37 | '@typescript-eslint/parser': ['.ts', '.tsx'] 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /app/[locale]/(app)/dashboard/settings/_components/app-settings.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { Button } from '@/components/ui/button' 4 | import { Field } from '@/components/ui/field' 5 | import { Select } from 'chakra-react-select' 6 | 7 | export default function AppSettings() { 8 | return ( 9 |
10 |
11 | 15 | 21 | 22 | 26 | 32 | 33 |
34 | 35 | 38 |
39 | ) 40 | } 41 | -------------------------------------------------------------------------------- /app/[locale]/(app)/dashboard/snippets/_components/button.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import { Button, useDisclosure } from '@chakra-ui/react' 3 | import { useMemoizedFn } from 'ahooks' 4 | import { useRouter } from 'next/navigation' 5 | import { CreateSnippetModal } from './modal' 6 | import { toaster } from '@/components/ui/toaster' 7 | import { open } from 'fs' 8 | 9 | export type AddButtonProps = { 10 | username?: string 11 | } 12 | 13 | export function AddButton({ username }: AddButtonProps) { 14 | const { open, onOpen, onClose } = useDisclosure() 15 | const router = useRouter() 16 | const onSuccess = useMemoizedFn((pasteID: string) => { 17 | toaster.create({ 18 | title: 'Snippet created', 19 | description: `Your snippet has been created successfully. You can view it at ${pasteID}`, 20 | type: 'success', 21 | duration: 5000 22 | }) 23 | router.refresh() 24 | onClose() 25 | }) 26 | return ( 27 |
28 | 31 | 37 |
38 | ) 39 | } 40 | -------------------------------------------------------------------------------- /libs/navigation.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createLocalizedPathnamesNavigation, 3 | Pathnames 4 | } from 'next-intl/navigation' 5 | 6 | export const config = { 7 | // A list of all locales that are supported 8 | locales: ['en', 'zh-CN'], 9 | 10 | // If this locale is matched, pathnames work without a prefix (e.g. `/about`) 11 | defaultLocale: 'en' 12 | } 13 | 14 | export const pathnames = { 15 | // If all locales use the same pathname, a 16 | // single external path can be provided. 17 | '/auth/signin': '/auth/signin', 18 | '/auth/signout': '/auth/signout', 19 | '/auth/error': '/auth/error', 20 | '/auth/signup': '/auth/signup', 21 | '/auth/password/reset': '/auth/password/reset', 22 | '/auth/password/reset/[token]': '/auth/password/reset/[token]', 23 | '/': '/', 24 | '/about': '/about', 25 | '/v/[uuid]': '/v/[uuid]', 26 | '/dashboard': '/dashboard', 27 | '/dashboard/notifications': '/dashboard/notifications', 28 | '/dashboard/snippets': '/dashboard/snippets', 29 | '/dashboard/settings': '/dashboard/settings' 30 | } satisfies Pathnames 31 | 32 | export const { Link, redirect, usePathname, useRouter, getPathname } = 33 | createLocalizedPathnamesNavigation({ locales: config.locales, pathnames }) 34 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:base", 5 | "default:automergeMinor", 6 | "default:disableRateLimiting", 7 | "default:rebaseStalePrs", 8 | "default:pinVersions", 9 | "group:monorepos" 10 | ], 11 | "packageRules": [ 12 | { 13 | "groupName": "Prisma packages", 14 | "matchPackagePatterns": ["prisma"] 15 | }, 16 | { 17 | "groupName": "Next.js packages", 18 | "matchPackagePatterns": ["next"] 19 | }, 20 | { 21 | "groupName": "TypeScript", 22 | "matchPackagePatterns": ["typescript"] 23 | }, 24 | { 25 | "groupName": "Lint packages", 26 | "matchPackagePatterns": [ 27 | "eslint", 28 | "prettier", 29 | "commitlint", 30 | "stylelint", 31 | "husky", 32 | "lint-staged" 33 | ] 34 | }, 35 | { 36 | "groupName": "Testing packages", 37 | "matchPackagePatterns": ["vitest", "cypress"] 38 | }, 39 | { 40 | "description": "Ignore nodejs", 41 | "matchPackageNames": ["node"], 42 | "matchManagers": ["npm"], 43 | "matchDepTypes": ["engines"], 44 | "enabled": false 45 | } 46 | ] 47 | } 48 | -------------------------------------------------------------------------------- /components/ui/empty-state.tsx: -------------------------------------------------------------------------------- 1 | import { EmptyState as ChakraEmptyState, VStack } from '@chakra-ui/react' 2 | import * as React from 'react' 3 | 4 | export interface EmptyStateProps extends ChakraEmptyState.RootProps { 5 | title: string 6 | description?: string 7 | icon?: React.ReactNode 8 | } 9 | 10 | export const EmptyState = React.forwardRef( 11 | function EmptyState(props, ref) { 12 | const { title, description, icon, children, ...rest } = props 13 | return ( 14 | 15 | 16 | {icon && ( 17 | {icon} 18 | )} 19 | {description ? ( 20 | 21 | {title} 22 | 23 | {description} 24 | 25 | 26 | ) : ( 27 | {title} 28 | )} 29 | {children} 30 | 31 | 32 | ) 33 | } 34 | ) 35 | -------------------------------------------------------------------------------- /components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import type { ButtonProps as ChakraButtonProps } from '@chakra-ui/react' 2 | import { 3 | AbsoluteCenter, 4 | Button as ChakraButton, 5 | Span, 6 | Spinner 7 | } from '@chakra-ui/react' 8 | import * as React from 'react' 9 | 10 | interface ButtonLoadingProps { 11 | loading?: boolean 12 | loadingText?: React.ReactNode 13 | } 14 | 15 | export interface ButtonProps extends ChakraButtonProps, ButtonLoadingProps {} 16 | 17 | export const Button = React.forwardRef( 18 | function Button(props, ref) { 19 | const { loading, disabled, loadingText, children, ...rest } = props 20 | return ( 21 | 22 | {loading && !loadingText ? ( 23 | <> 24 | 25 | 26 | 27 | {children} 28 | 29 | ) : loading && loadingText ? ( 30 | <> 31 | 32 | {loadingText} 33 | 34 | ) : ( 35 | children 36 | )} 37 | 38 | ) 39 | } 40 | ) 41 | -------------------------------------------------------------------------------- /components/ui/progress-circle.tsx: -------------------------------------------------------------------------------- 1 | import type { SystemStyleObject } from '@chakra-ui/react' 2 | import { 3 | AbsoluteCenter, 4 | ProgressCircle as ChakraProgressCircle 5 | } from '@chakra-ui/react' 6 | import * as React from 'react' 7 | 8 | interface ProgressCircleRingProps extends ChakraProgressCircle.CircleProps { 9 | trackColor?: SystemStyleObject['stroke'] 10 | cap?: SystemStyleObject['strokeLinecap'] 11 | } 12 | 13 | export const ProgressCircleRing = React.forwardRef< 14 | SVGSVGElement, 15 | ProgressCircleRingProps 16 | >(function ProgressCircleRing(props, ref) { 17 | const { trackColor, cap, color, ...rest } = props 18 | return ( 19 | 20 | 21 | 22 | 23 | ) 24 | }) 25 | 26 | export const ProgressCircleValueText = React.forwardRef< 27 | HTMLDivElement, 28 | ChakraProgressCircle.ValueTextProps 29 | >(function ProgressCircleValueText(props, ref) { 30 | return ( 31 | 32 | 33 | 34 | ) 35 | }) 36 | 37 | export const ProgressCircleRoot = ChakraProgressCircle.Root 38 | -------------------------------------------------------------------------------- /libs/prisma/client.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from '@prisma/client' 2 | import { createSoftDeleteExtension } from 'prisma-extension-soft-delete' 3 | const client = new PrismaClient() 4 | 5 | // use soft delete 6 | client.$extends( 7 | createSoftDeleteExtension({ 8 | models: { 9 | User: { 10 | field: 'deletedAt', 11 | createValue: (deleted) => { 12 | if (deleted) return new Date() 13 | return null 14 | } 15 | }, 16 | Announcement: { 17 | field: 'deletedAt', 18 | createValue: (deleted) => { 19 | if (deleted) return new Date() 20 | return null 21 | } 22 | }, 23 | Paste: { 24 | field: 'deletedAt', 25 | createValue: (deleted) => { 26 | if (deleted) return new Date() 27 | return null 28 | } 29 | }, 30 | Comment: { 31 | field: 'deletedAt', 32 | createValue: (deleted) => { 33 | if (deleted) return new Date() 34 | return null 35 | } 36 | }, 37 | Notification: { 38 | field: 'deletedAt', 39 | createValue: (deleted) => { 40 | if (deleted) return new Date() 41 | return null 42 | } 43 | } 44 | } 45 | }) 46 | ) 47 | 48 | export default client 49 | -------------------------------------------------------------------------------- /components/ui/toaster.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { 4 | Toaster as ChakraToaster, 5 | Portal, 6 | Spinner, 7 | Stack, 8 | Toast, 9 | createToaster 10 | } from '@chakra-ui/react' 11 | 12 | export const toaster = createToaster({ 13 | placement: 'bottom-end', 14 | pauseOnPageIdle: true 15 | }) 16 | 17 | export const Toaster = () => { 18 | return ( 19 | 20 | 21 | {(toast) => ( 22 | 23 | {toast.type === 'loading' ? ( 24 | 25 | ) : ( 26 | 27 | )} 28 | 29 | {toast.title && {toast.title}} 30 | {toast.description && ( 31 | {toast.description} 32 | )} 33 | 34 | {toast.action && ( 35 | {toast.action.label} 36 | )} 37 | {toast.meta?.closable && } 38 | 39 | )} 40 | 41 | 42 | ) 43 | } 44 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/[locale]/(app)/dashboard/snippets/loading.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { Card } from '@chakra-ui/react' 4 | import Header from '../_components/header' 5 | import Shell from '../_components/shell' 6 | import { H2Skeleton, PSkeleton, TextSkeleton } from '../_components/skeleton' 7 | 8 | export default function SnippetsLoading() { 9 | return ( 10 | 11 |
12 |
13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 |
22 |
23 |
24 | 25 | 26 | 27 | 28 | 29 |
30 | 31 | 32 |
33 |
34 |
35 |
36 | 37 | ) 38 | } 39 | -------------------------------------------------------------------------------- /app/[locale]/(app)/page.tsx: -------------------------------------------------------------------------------- 1 | import { IntlClientProvider } from '@/components/server-provider' 2 | import { auth } from '@/libs/auth' 3 | import { Flex } from '@chakra-ui/react' 4 | import { pick } from 'lodash-es' 5 | import { useLocale, useMessages, type AbstractIntlMessages } from 'next-intl' 6 | import Announcement from './_components/home/Announcement' 7 | import { CreateSnippetForm } from './_components/home/form' 8 | type Props = { 9 | params: Promise<{ locale: string }> 10 | } 11 | 12 | function CreateSnippetIntlProvider({ 13 | children 14 | }: { 15 | children: React.ReactNode 16 | }) { 17 | const locale = useLocale() 18 | const messages = useMessages() 19 | return ( 20 | 24 | {children} 25 | 26 | ) 27 | } 28 | 29 | // Home Page 30 | export default async function Home(props: Props) { 31 | const session = await auth() 32 | return ( 33 | 34 | 35 | 36 | 40 | 41 | 42 | ) 43 | } 44 | -------------------------------------------------------------------------------- /components/ui/action-bar.tsx: -------------------------------------------------------------------------------- 1 | import { ActionBar, Portal } from '@chakra-ui/react' 2 | import { CloseButton } from './close-button' 3 | import * as React from 'react' 4 | 5 | interface ActionBarContentProps extends ActionBar.ContentProps { 6 | portalled?: boolean 7 | portalRef?: React.RefObject 8 | } 9 | 10 | export const ActionBarContent = React.forwardRef< 11 | HTMLDivElement, 12 | ActionBarContentProps 13 | >(function ActionBarContent(props, ref) { 14 | const { children, portalled = true, portalRef, ...rest } = props 15 | 16 | return ( 17 | 18 | 19 | 20 | {children} 21 | 22 | 23 | 24 | ) 25 | }) 26 | 27 | export const ActionBarCloseTrigger = React.forwardRef< 28 | HTMLButtonElement, 29 | ActionBar.CloseTriggerProps 30 | >(function ActionBarCloseTrigger(props, ref) { 31 | return ( 32 | 33 | 34 | 35 | ) 36 | }) 37 | 38 | export const ActionBarRoot = ActionBar.Root 39 | export const ActionBarSelectionTrigger = ActionBar.SelectionTrigger 40 | export const ActionBarSeparator = ActionBar.Separator 41 | -------------------------------------------------------------------------------- /.stylelintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ 3 | 'stylelint-config-standard', 4 | 'stylelint-config-recess-order', 5 | 'stylelint-config-html/vue' 6 | ], 7 | plugins: [ 8 | 'stylelint-scss', 9 | 'stylelint-order', 10 | 'stylelint-declaration-block-no-ignored-properties' 11 | ], 12 | ignoreFiles: [ 13 | 'node_modules/**/*', 14 | 'dist/**/*', 15 | '.next/**/*', 16 | '**/typings/**/*', 17 | 'public/css/**/*' 18 | ], 19 | rules: { 20 | 'at-rule-no-unknown': [ 21 | true, 22 | { 23 | ignoreAtRules: [ 24 | 'tailwind', 25 | 'unocss', 26 | 'layer', 27 | 'apply', 28 | 'variants', 29 | 'responsive', 30 | 'screen' 31 | ] 32 | } 33 | ] 34 | }, 35 | overrides: [ 36 | { 37 | files: ['**/*.scss', '*.scss'], 38 | customSyntax: require('postcss-scss'), 39 | rules: { 40 | 'at-rule-no-unknown': null, 41 | 'scss/at-rule-no-unknown': [ 42 | true, 43 | { 44 | ignoreAtRules: [ 45 | 'tailwind', 46 | 'unocss', 47 | 'layer', 48 | 'apply', 49 | 'variants', 50 | 'responsive', 51 | 'screen' 52 | ] 53 | } 54 | ] 55 | } 56 | } 57 | ] 58 | } 59 | -------------------------------------------------------------------------------- /components/ui/segmented-control.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { For, SegmentGroup } from '@chakra-ui/react' 4 | import * as React from 'react' 5 | 6 | interface Item { 7 | value: string 8 | label: React.ReactNode 9 | disabled?: boolean 10 | } 11 | 12 | export interface SegmentedControlProps extends SegmentGroup.RootProps { 13 | items: Array 14 | } 15 | 16 | function normalize(items: Array): Item[] { 17 | return items.map((item) => { 18 | if (typeof item === 'string') return { value: item, label: item } 19 | return item 20 | }) 21 | } 22 | 23 | export const SegmentedControl = React.forwardRef< 24 | HTMLDivElement, 25 | SegmentedControlProps 26 | >(function SegmentedControl(props, ref) { 27 | const { items, ...rest } = props 28 | const data = React.useMemo(() => normalize(items), [items]) 29 | 30 | return ( 31 | 32 | 33 | 34 | {(item) => ( 35 | 40 | {item.label} 41 | 42 | 43 | )} 44 | 45 | 46 | ) 47 | }) 48 | -------------------------------------------------------------------------------- /app/[locale]/(auth)/auth/signup/page.tsx: -------------------------------------------------------------------------------- 1 | import { IntlClientProvider } from '@/components/server-provider' 2 | import { Stack } from '@chakra-ui/react' 3 | import { pick } from 'lodash-es' 4 | import { Metadata } from 'next' 5 | import { AbstractIntlMessages, useLocale, useMessages } from 'next-intl' 6 | import { getTranslations } from 'next-intl/server' 7 | import SignUpForm from './_components/form' 8 | 9 | type Props = { 10 | params: Promise<{ 11 | locale: string 12 | }> 13 | } 14 | 15 | export async function generateMetadata({ params }: Props): Promise { 16 | const { locale } = await params 17 | const t = await getTranslations({ locale }) 18 | 19 | return { 20 | title: `${t('auth.signup.title')} - ${t('app.name')}` 21 | } 22 | } 23 | 24 | function SignUpIntlProvider({ children }: { children: React.ReactNode }) { 25 | const messages = useMessages() 26 | const locale = useLocale() 27 | 28 | return ( 29 | 35 | {children} 36 | 37 | ) 38 | } 39 | 40 | export default async function SignUpPage(props: Props) { 41 | return ( 42 | 43 | 44 | 45 | 46 | 47 | ) 48 | } 49 | -------------------------------------------------------------------------------- /components/ui/breadcrumb.tsx: -------------------------------------------------------------------------------- 1 | import { Breadcrumb, type SystemStyleObject } from '@chakra-ui/react' 2 | import * as React from 'react' 3 | 4 | export interface BreadcrumbRootProps extends Breadcrumb.RootProps { 5 | separator?: React.ReactNode 6 | separatorGap?: SystemStyleObject['gap'] 7 | } 8 | 9 | export const BreadcrumbRoot = React.forwardRef< 10 | HTMLDivElement, 11 | BreadcrumbRootProps 12 | >(function BreadcrumbRoot(props, ref) { 13 | const { separator, separatorGap, children, ...rest } = props 14 | 15 | const validChildren = React.Children.toArray(children).filter( 16 | React.isValidElement 17 | ) 18 | 19 | return ( 20 | 21 | 22 | {validChildren.map((child, index) => { 23 | const last = index === validChildren.length - 1 24 | return ( 25 | 26 | {child} 27 | {!last && ( 28 | {separator} 29 | )} 30 | 31 | ) 32 | })} 33 | 34 | 35 | ) 36 | }) 37 | 38 | export const BreadcrumbLink = Breadcrumb.Link 39 | export const BreadcrumbCurrentLink = Breadcrumb.CurrentLink 40 | export const BreadcrumbEllipsis = Breadcrumb.Ellipsis 41 | -------------------------------------------------------------------------------- /components/ui/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import type { 2 | SkeletonProps as ChakraSkeletonProps, 3 | CircleProps 4 | } from '@chakra-ui/react' 5 | import { Skeleton as ChakraSkeleton, Circle, Stack } from '@chakra-ui/react' 6 | import * as React from 'react' 7 | 8 | export interface SkeletonCircleProps extends ChakraSkeletonProps { 9 | size?: CircleProps['size'] 10 | } 11 | 12 | export const SkeletonCircle = React.forwardRef< 13 | HTMLDivElement, 14 | SkeletonCircleProps 15 | >(function SkeletonCircle(props, ref) { 16 | const { size, ...rest } = props 17 | return ( 18 | 19 | 20 | 21 | ) 22 | }) 23 | 24 | export interface SkeletonTextProps extends ChakraSkeletonProps { 25 | noOfLines?: number 26 | } 27 | 28 | export const SkeletonText = React.forwardRef( 29 | function SkeletonText(props, ref) { 30 | const { noOfLines = 3, gap, ...rest } = props 31 | return ( 32 | 33 | {Array.from({ length: noOfLines }).map((_, index) => ( 34 | 41 | ))} 42 | 43 | ) 44 | } 45 | ) 46 | 47 | export const Skeleton = ChakraSkeleton 48 | -------------------------------------------------------------------------------- /utils/actions.ts: -------------------------------------------------------------------------------- 1 | import { ResponseCode } from '@/enums/response' 2 | import { cookies } from 'next/headers' 3 | import { ZodError } from 'zod' 4 | 5 | export interface State { 6 | error?: string 7 | issues?: ZodError['issues'] // Validation issues 8 | status: ResponseCode 9 | data?: E 10 | ts: number 11 | } 12 | 13 | export type ActionReturn = State 14 | 15 | export type BaseOptions = { 16 | cookiesJar?: Map 17 | } 18 | 19 | async function setCookies(cookiesJar: Map) { 20 | const cookies_jar = await cookies() 21 | for (const [key, value] of cookiesJar) { 22 | cookies_jar.set(key, value) 23 | } 24 | } 25 | 26 | export function ok( 27 | data?: E, 28 | options: BaseOptions = {} 29 | ): ActionReturn { 30 | if (options.cookiesJar) { 31 | setCookies(options.cookiesJar) 32 | } 33 | return { 34 | status: ResponseCode.OK, 35 | ts: Date.now(), 36 | data 37 | } 38 | } 39 | 40 | type NOkProps = { 41 | error?: string 42 | issues?: ZodError['issues'] // Validation issues 43 | data?: E 44 | } 45 | 46 | export function nok( 47 | status: ResponseCode, 48 | args: NOkProps = {}, 49 | options: BaseOptions = {} 50 | ): ActionReturn { 51 | if (options.cookiesJar) { 52 | setCookies(options.cookiesJar) 53 | } 54 | return { 55 | status, 56 | ...args, 57 | ts: Date.now() 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /components/ui/tooltip.tsx: -------------------------------------------------------------------------------- 1 | import { Tooltip as ChakraTooltip, Portal } from '@chakra-ui/react' 2 | import * as React from 'react' 3 | 4 | export interface TooltipProps extends ChakraTooltip.RootProps { 5 | showArrow?: boolean 6 | portalled?: boolean 7 | portalRef?: React.RefObject 8 | content: React.ReactNode 9 | contentProps?: ChakraTooltip.ContentProps 10 | disabled?: boolean 11 | } 12 | 13 | export const Tooltip = React.forwardRef( 14 | function Tooltip(props, ref) { 15 | const { 16 | showArrow, 17 | children, 18 | disabled, 19 | portalled, 20 | content, 21 | contentProps, 22 | portalRef, 23 | ...rest 24 | } = props 25 | 26 | if (disabled) return children 27 | 28 | return ( 29 | 30 | {children} 31 | 32 | 33 | 34 | {showArrow && ( 35 | 36 | 37 | 38 | )} 39 | {content} 40 | 41 | 42 | 43 | 44 | ) 45 | } 46 | ) 47 | -------------------------------------------------------------------------------- /utils/formatter.ts: -------------------------------------------------------------------------------- 1 | import dayjs from 'dayjs' 2 | import relativeTime from 'dayjs/plugin/relativeTime' 3 | import timezone from 'dayjs/plugin/timezone' 4 | import utc from 'dayjs/plugin/utc' 5 | import weekday from 'dayjs/plugin/weekday' 6 | 7 | // TODO: load locale files automatically, maybe use a script to generate the list? 8 | import { PasteType } from '@/enums/paste' 9 | import 'dayjs/locale/en' 10 | import 'dayjs/locale/zh-cn' 11 | 12 | dayjs.extend(utc) 13 | dayjs.extend(timezone) 14 | dayjs.extend(relativeTime) 15 | dayjs.extend(weekday) 16 | 17 | export type DayJSOptions = { 18 | locale?: string 19 | timeZone?: string 20 | } 21 | 22 | export function formatDateTime( 23 | date: string | number | Date | dayjs.Dayjs | null | undefined, 24 | { locale = 'zh-CN', timeZone = 'Asia/Shanghai' }: DayJSOptions = {} 25 | ) { 26 | return dayjs(date) 27 | .locale(locale.toLowerCase()) 28 | .tz(timeZone) 29 | .format('YYYY-MM-DD HH:mm:ss') 30 | } 31 | 32 | export function newDayjs( 33 | date: string | number | Date | dayjs.Dayjs | null | undefined, 34 | { locale = 'zh-CN', timeZone = 'Asia/Shanghai' }: DayJSOptions = {} 35 | ) { 36 | return dayjs(date).locale(locale.toLowerCase()).tz(timeZone) 37 | } 38 | 39 | export function formatPasteType(type: PasteType) { 40 | switch (type) { 41 | case PasteType.Normal: 42 | return 'Normal' 43 | case PasteType.Gist: 44 | return 'Gist' 45 | default: 46 | return 'Unknown' 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /app/[locale]/(app)/dashboard/snippets/_components/modal.tsx: -------------------------------------------------------------------------------- 1 | import { CreateNormalSnippet } from '@/components/form' 2 | import { 3 | DialogBody, 4 | DialogCloseTrigger, 5 | DialogContent, 6 | DialogHeader, 7 | DialogRoot 8 | } from '@/components/ui/dialog' 9 | import { CloseButton } from '@/components/ui/close-button' 10 | import 'client-only' 11 | 12 | type ModalProps = { 13 | open: boolean 14 | onClose: () => void 15 | } 16 | 17 | export interface CreateSnippetModalProps extends ModalProps { 18 | nickname?: string 19 | onSuccess?: (pasteID: string) => void 20 | } 21 | 22 | export function CreateSnippetModal({ 23 | onClose, 24 | open: isOpen, 25 | nickname, 26 | onSuccess 27 | }: CreateSnippetModalProps) { 28 | return ( 29 | 34 | 43 | Create snippet 44 | 45 | 46 | 47 | 48 | 52 | 53 | 54 | 55 | ) 56 | } 57 | -------------------------------------------------------------------------------- /components/ui/switch.tsx: -------------------------------------------------------------------------------- 1 | import { Switch as ChakraSwitch } from '@chakra-ui/react' 2 | import * as React from 'react' 3 | 4 | export interface SwitchProps extends ChakraSwitch.RootProps { 5 | inputProps?: React.InputHTMLAttributes 6 | rootRef?: React.Ref 7 | trackLabel?: { on: React.ReactNode; off: React.ReactNode } 8 | thumbLabel?: { on: React.ReactNode; off: React.ReactNode } 9 | } 10 | 11 | export const Switch = React.forwardRef( 12 | function Switch(props, ref) { 13 | const { inputProps, children, rootRef, trackLabel, thumbLabel, ...rest } = 14 | props 15 | 16 | return ( 17 | 18 | 19 | 20 | 21 | {thumbLabel && ( 22 | 23 | {thumbLabel?.on} 24 | 25 | )} 26 | 27 | {trackLabel && ( 28 | 29 | {trackLabel.on} 30 | 31 | )} 32 | 33 | {children != null && ( 34 | {children} 35 | )} 36 | 37 | ) 38 | } 39 | ) 40 | -------------------------------------------------------------------------------- /app/[locale]/(app)/layout.tsx: -------------------------------------------------------------------------------- 1 | import { IntlClientProvider } from '@/components/server-provider' 2 | import { auth } from '@/libs/auth' 3 | import { omit, pick } from 'lodash-es' 4 | import type { Session } from 'next-auth' 5 | import { AbstractIntlMessages, useLocale, useMessages } from 'next-intl' 6 | import { ReactNode } from 'react' 7 | import { Header } from './_components/layout/Header' 8 | 9 | type Props = { 10 | children: ReactNode 11 | } 12 | 13 | function HeaderIntlProvider({ children }: Props) { 14 | const locale = useLocale() 15 | const messages = useMessages() 16 | return ( 17 | 21 | {children} 22 | 23 | ) 24 | } 25 | 26 | export default async function AppLayout({ children }: Props) { 27 | const session = await auth() 28 | // console.log(session) 29 | return ( 30 | <> 31 | 32 |
) 40 | : null 41 | } 42 | /> 43 | 44 |
45 |
46 | {children} 47 |
48 |
49 | 50 | ) 51 | } 52 | -------------------------------------------------------------------------------- /libs/i18n.tsx: -------------------------------------------------------------------------------- 1 | // import YAML from 'yaml' 2 | import { getRequestConfig } from 'next-intl/server' 3 | import { headers } from 'next/headers' 4 | 5 | export default getRequestConfig(async ({ locale }) => { 6 | const header = await headers() 7 | const now = header.get('x-now') 8 | const timeZone = header.get('x-time-zone') ?? 'Asia/Shanghai' 9 | return { 10 | now: now ? new Date(now) : undefined, 11 | timeZone, 12 | // messages: YAML.parse((await import(`./messages/${locale}.yml`)).default) 13 | messages: (await import(`@/messages/${locale}.yml`)).default, 14 | defaultTranslationValues: { 15 | globalString: 'Global string', 16 | highlight: (chunks) => {chunks} 17 | }, 18 | formats: { 19 | dateTime: { 20 | medium: { 21 | dateStyle: 'medium', 22 | timeStyle: 'short', 23 | hour12: false 24 | } 25 | } 26 | }, 27 | onError(error) { 28 | if ( 29 | error.message === 30 | (process.env.NODE_ENV === 'production' 31 | ? 'MISSING_MESSAGE' 32 | : 'MISSING_MESSAGE: Could not resolve `missing` in `Index`.') 33 | ) { 34 | // Do nothing, this error is triggered on purpose 35 | } else { 36 | console.error(JSON.stringify(error.message)) 37 | } 38 | }, 39 | getMessageFallback({ key, namespace }) { 40 | return ( 41 | '`getMessageFallback` called for ' + 42 | [namespace, key].filter((part) => part != null).join('.') 43 | ) 44 | } 45 | } 46 | }) 47 | -------------------------------------------------------------------------------- /app/[locale]/(auth)/layout.tsx: -------------------------------------------------------------------------------- 1 | import { auth } from '@/libs/auth' 2 | import { Box } from '@chakra-ui/react' 3 | import { useTranslations } from 'next-intl' 4 | import { headers } from 'next/headers' 5 | import { redirect } from 'next/navigation' 6 | import Content from './_components/layouts/content' 7 | import GoBackButton from './_components/layouts/go-back-button' 8 | import Logo from './_components/layouts/logo' 9 | import styles from './layout.module.scss' 10 | 11 | type Props = { 12 | children: React.ReactNode 13 | } 14 | 15 | function GoBack() { 16 | const t = useTranslations() 17 | 18 | return ( 19 | 20 | {t('auth.buttons.back')} 21 | 22 | ) 23 | } 24 | 25 | export default async function AuthLayout(props: Props) { 26 | // lazyload image 27 | const header = await headers() 28 | const session = await auth() 29 | // console.log(session) 30 | if (session) { 31 | // if user is logged in, redirect to callback url or home 32 | const urlParams = new URLSearchParams(header.get('x-search-params') || '') 33 | const callbackUrl = urlParams.get('callbackUrl') || '/' 34 | redirect(callbackUrl) 35 | } 36 | 37 | return ( 38 | 39 | 40 | 41 | 42 | {props.children} 43 | 44 | 45 | ) 46 | } 47 | -------------------------------------------------------------------------------- /components/ui/native-select.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { NativeSelect as Select } from '@chakra-ui/react' 4 | import * as React from 'react' 5 | 6 | interface NativeSelectRootProps extends Select.RootProps { 7 | icon?: React.ReactNode 8 | } 9 | 10 | export const NativeSelectRoot = React.forwardRef< 11 | HTMLDivElement, 12 | NativeSelectRootProps 13 | >(function NativeSelect(props, ref) { 14 | const { icon, children, ...rest } = props 15 | return ( 16 | 17 | {children} 18 | {icon} 19 | 20 | ) 21 | }) 22 | 23 | interface NativeSelectItem { 24 | value: string 25 | label: string 26 | disabled?: boolean 27 | } 28 | 29 | interface NativeSelectField extends Select.FieldProps { 30 | items?: Array 31 | } 32 | 33 | export const NativeSelectField = React.forwardRef< 34 | HTMLSelectElement, 35 | NativeSelectField 36 | >(function NativeSelectField(props, ref) { 37 | const { items: itemsProp, children, ...rest } = props 38 | 39 | const items = React.useMemo( 40 | () => 41 | itemsProp?.map((item) => 42 | typeof item === 'string' ? { label: item, value: item } : item 43 | ), 44 | [itemsProp] 45 | ) 46 | 47 | return ( 48 | 49 | {children} 50 | {items?.map((item) => ( 51 | 54 | ))} 55 | 56 | ) 57 | }) 58 | -------------------------------------------------------------------------------- /components/ui/alert.tsx: -------------------------------------------------------------------------------- 1 | import { Alert as ChakraAlert } from '@chakra-ui/react' 2 | import { CloseButton } from './close-button' 3 | import * as React from 'react' 4 | 5 | export interface AlertProps extends Omit { 6 | startElement?: React.ReactNode 7 | endElement?: React.ReactNode 8 | title?: React.ReactNode 9 | icon?: React.ReactElement 10 | closable?: boolean 11 | onClose?: () => void 12 | } 13 | 14 | export const Alert = React.forwardRef( 15 | function Alert(props, ref) { 16 | const { 17 | title, 18 | children, 19 | icon, 20 | closable, 21 | onClose, 22 | startElement, 23 | endElement, 24 | ...rest 25 | } = props 26 | return ( 27 | 28 | {startElement || {icon}} 29 | {children ? ( 30 | 31 | {title} 32 | {children} 33 | 34 | ) : ( 35 | {title} 36 | )} 37 | {endElement} 38 | {closable && ( 39 | 47 | )} 48 | 49 | ) 50 | } 51 | ) 52 | -------------------------------------------------------------------------------- /app/api/task/route.ts: -------------------------------------------------------------------------------- 1 | import { ResponseCode } from '@/enums/response' 2 | import { Role } from '@/enums/user' 3 | import { env } from '@/env.mjs' 4 | import client from '@/libs/prisma/client' 5 | import { NextRequest } from 'next/server' 6 | 7 | export async function GET(req: NextRequest) { 8 | if ( 9 | env.CRON_TASK_TOKEN && 10 | req.headers.get('Authorization') !== `Bearer ${env.CRON_TASK_TOKEN}` 11 | ) { 12 | return fail(ResponseCode.NotAuthorized, {}) 13 | } 14 | try { 15 | // 1. Check if the first user is admin, if not, grant admin role to it. 16 | const firstUser = await client.user.findFirst({ 17 | orderBy: { 18 | createdAt: 'asc' 19 | } 20 | }) 21 | if (firstUser && firstUser.role !== Role.Admin) { 22 | await client.user.update({ 23 | where: { 24 | id: firstUser.id 25 | }, 26 | data: { 27 | role: Role.Admin 28 | } 29 | }) 30 | } 31 | 32 | // 2. check whether the expired date is coming, if so, delete paste records 33 | const expiredPastes = await client.paste.findMany({ 34 | where: { 35 | expiredAt: { 36 | lte: new Date() 37 | } 38 | } 39 | }) 40 | if (expiredPastes && expiredPastes.length > 0) { 41 | await client.paste.deleteMany({ 42 | where: { 43 | id: { 44 | in: expiredPastes.map((paste) => paste.id) 45 | } 46 | } 47 | }) 48 | } 49 | } catch (error) { 50 | console.error(error) 51 | } finally { 52 | // always return success 53 | return success(null) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /components/ui/accordion.tsx: -------------------------------------------------------------------------------- 1 | import { Accordion, HStack } from '@chakra-ui/react' 2 | import * as React from 'react' 3 | import { LuChevronDown } from 'react-icons/lu' 4 | 5 | interface AccordionItemTriggerProps extends Accordion.ItemTriggerProps { 6 | indicatorPlacement?: 'start' | 'end' 7 | } 8 | 9 | export const AccordionItemTrigger = React.forwardRef< 10 | HTMLButtonElement, 11 | AccordionItemTriggerProps 12 | >(function AccordionItemTrigger(props, ref) { 13 | const { children, indicatorPlacement = 'end', ...rest } = props 14 | return ( 15 | 16 | {indicatorPlacement === 'start' && ( 17 | 18 | 19 | 20 | )} 21 | 22 | {children} 23 | 24 | {indicatorPlacement === 'end' && ( 25 | 26 | 27 | 28 | )} 29 | 30 | ) 31 | }) 32 | 33 | interface AccordionItemContentProps extends Accordion.ItemContentProps {} 34 | 35 | export const AccordionItemContent = React.forwardRef< 36 | HTMLDivElement, 37 | AccordionItemContentProps 38 | >(function AccordionItemContent(props, ref) { 39 | return ( 40 | 41 | 42 | 43 | ) 44 | }) 45 | 46 | export const AccordionRoot = Accordion.Root 47 | export const AccordionItem = Accordion.Item 48 | -------------------------------------------------------------------------------- /app/api/auth/signup/_route.ts: -------------------------------------------------------------------------------- 1 | // import { ResponseCode } from '@/enums/response' 2 | // import { Role } from '@/enums/user' 3 | // import client from '@/libs/prisma/client' 4 | // import { createUser } from '@/libs/services/users/user' 5 | // import Joi from 'joi' 6 | // import { NextRequest } from 'next/server' 7 | 8 | // const userSignUpSchema = Joi.object({ 9 | // email: Joi.string().email().required(), 10 | // name: Joi.string().min(2).max(32).required(), 11 | // password: Joi.string().min(8).max(100).required(), 12 | // passwordConfirm: Joi.string().valid(Joi.ref('password')).required() 13 | // }) 14 | 15 | // type UserSignUp = { 16 | // email: string 17 | // name: string 18 | // password: string 19 | // passwordConfirm: string 20 | // } 21 | 22 | // export async function POST(req: NextRequest) { 23 | // const data = (await req.json()) as Partial 24 | // const { error } = userSignUpSchema.validate(data) 25 | // if (error) { 26 | // return fail(ResponseCode.ValidationFailed, { 27 | // data: error.details, 28 | // status: 400 29 | // }) 30 | // } 31 | // const params = data // It just a type assertion 32 | // const user = await client.user.findFirst({ 33 | // where: { 34 | // email: params.email 35 | // } 36 | // }) 37 | // if (user) { 38 | // return fail(ResponseCode.OperationFailed, { 39 | // message: 'email already exists' 40 | // }) 41 | // } 42 | // const newUser = await createUser(params.email, params.password, { 43 | // name: params.name, 44 | // role: Role.User 45 | // }) 46 | // return success(newUser) 47 | // } 48 | -------------------------------------------------------------------------------- /components/ui/stepper-input.tsx: -------------------------------------------------------------------------------- 1 | import { HStack, IconButton, NumberInput } from '@chakra-ui/react' 2 | import * as React from 'react' 3 | import { LuMinus, LuPlus } from 'react-icons/lu' 4 | 5 | export interface StepperInputProps extends NumberInput.RootProps { 6 | label?: React.ReactNode 7 | } 8 | 9 | export const StepperInput = React.forwardRef( 10 | function StepperInput(props, ref) { 11 | const { label, ...rest } = props 12 | return ( 13 | 14 | {label && {label}} 15 | 16 | 17 | 18 | 19 | 20 | 21 | ) 22 | } 23 | ) 24 | 25 | const DecrementTrigger = React.forwardRef< 26 | HTMLButtonElement, 27 | NumberInput.DecrementTriggerProps 28 | >(function DecrementTrigger(props, ref) { 29 | return ( 30 | 31 | 32 | 33 | 34 | 35 | ) 36 | }) 37 | 38 | const IncrementTrigger = React.forwardRef< 39 | HTMLButtonElement, 40 | NumberInput.IncrementTriggerProps 41 | >(function IncrementTrigger(props, ref) { 42 | return ( 43 | 44 | 45 | 46 | 47 | 48 | ) 49 | }) 50 | -------------------------------------------------------------------------------- /.vscode/i18n-ally-custom-framework.yml: -------------------------------------------------------------------------------- 1 | # .vscode/i18n-ally-custom-framework.yml 2 | 3 | # An array of strings which contain Language Ids defined by VS Code 4 | # You can check available language ids here: https://code.visualstudio.com/docs/languages/identifiers 5 | languageIds: 6 | - javascript 7 | - typescript 8 | - javascriptreact 9 | - typescriptreact 10 | 11 | # An array of RegExes to find the key usage. **The key should be captured in the first match group**. 12 | # You should unescape RegEx strings in order to fit in the YAML file 13 | # To help with this, you can use https://www.freeformatter.com/json-escape.html 14 | usageMatchRegex: 15 | # The following example shows how to detect `t("your.i18n.keys")` 16 | # the `{key}` will be placed by a proper keypath matching regex, 17 | # you can ignore it and use your own matching rules as well 18 | - "[^\\w\\d]wrapTranslationKey\\s*\\(\\s*['\"`]({key})['\"`]" 19 | 20 | # A RegEx to set a custom scope range. This scope will be used as a prefix when detecting keys 21 | # and works like how the i18next framework identifies the namespace scope from the 22 | # useTranslation() hook. 23 | # You should unescape RegEx strings in order to fit in the YAML file 24 | # To help with this, you can use https://www.freeformatter.com/json-escape.html 25 | scopeRangeRegex: "useTranslation\\(\\s*\\[?\\s*['\"`](.*?)['\"`]" 26 | 27 | # An array of strings containing refactor templates. 28 | # The "$1" will be replaced by the keypath specified. 29 | # Optional: uncomment the following two lines to use 30 | 31 | # refactorTemplates: 32 | # - i18n.get("$1") 33 | 34 | # If set to true, only enables this custom framework (will disable all built-in frameworks) 35 | monopoly: false 36 | -------------------------------------------------------------------------------- /components/ui/toggle.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import type { ButtonProps } from '@chakra-ui/react' 4 | import { 5 | Button, 6 | Toggle as ChakraToggle, 7 | useToggleContext 8 | } from '@chakra-ui/react' 9 | import * as React from 'react' 10 | 11 | interface ToggleProps extends ChakraToggle.RootProps { 12 | variant?: keyof typeof variantMap 13 | size?: ButtonProps['size'] 14 | } 15 | 16 | const variantMap = { 17 | solid: { on: 'solid', off: 'outline' }, 18 | surface: { on: 'surface', off: 'outline' }, 19 | subtle: { on: 'subtle', off: 'ghost' }, 20 | ghost: { on: 'subtle', off: 'ghost' } 21 | } as const 22 | 23 | export const Toggle = React.forwardRef( 24 | function Toggle(props, ref) { 25 | const { variant = 'subtle', size, children, ...rest } = props 26 | const variantConfig = variantMap[variant] 27 | 28 | return ( 29 | 30 | 31 | {children} 32 | 33 | 34 | ) 35 | } 36 | ) 37 | 38 | interface ToggleBaseButtonProps extends Omit { 39 | variant: Record<'on' | 'off', ButtonProps['variant']> 40 | } 41 | 42 | const ToggleBaseButton = React.forwardRef< 43 | HTMLButtonElement, 44 | ToggleBaseButtonProps 45 | >(function ToggleBaseButton(props, ref) { 46 | const toggle = useToggleContext() 47 | const { variant, ...rest } = props 48 | return ( 49 | 84 | ) 85 | } 86 | -------------------------------------------------------------------------------- /scripts/prisma/migrate.ts: -------------------------------------------------------------------------------- 1 | import { RuntimeMode } from '@/enums/app' 2 | import { getRuntimeMode, loadEnv } from '@/utils/app' 3 | import { getDictionaries } from '@/utils/fs' 4 | import chalk from 'chalk' 5 | import { spawn } from 'node:child_process' 6 | import path from 'path' 7 | 8 | const mode = getRuntimeMode() 9 | const rootDir = path.resolve(__dirname, '../../') 10 | loadEnv() 11 | 12 | async function main() { 13 | // Get all supported drivers 14 | 15 | const drivers = await getDictionaries(path.join(rootDir, './prisma')) 16 | console.log( 17 | `${chalk.green('✓')} Found ${chalk.cyan(drivers.length)} drivers.` 18 | ) 19 | // list all drivers 20 | console.log(chalk.cyan('Drivers:')) 21 | console.log(drivers.map((v) => ` - ` + v).join('\n')) 22 | 23 | // Get configured driver 24 | const configuredDriver = process.env.DB_ADAPTER || '' 25 | if (!drivers.includes(configuredDriver)) { 26 | console.error( 27 | `${chalk.red('✗')} ${chalk.cyan( 28 | configuredDriver 29 | )} is not a supported driver.` 30 | ) 31 | console.error( 32 | `Please set the ${chalk.cyan( 33 | 'DB_ADAPTER' 34 | )} environment variable to one of the following:` 35 | ) 36 | console.error(drivers.map((v) => ` - ` + v).join('\n')) 37 | process.exit(1) 38 | } 39 | 40 | let driversToMigrate = [configuredDriver] 41 | if (mode === RuntimeMode.Development && process.argv.includes('dev')) { 42 | console.log( 43 | `${chalk.yellow('!')} ${chalk.cyan( 44 | 'dev' 45 | )} mode detected, will migrate all drivers.` 46 | ) 47 | driversToMigrate = drivers 48 | } 49 | // run prisma generate, and pipe the output to stdout, stderr, and stdin of the current process 50 | console.log(chalk.cyan('Migrating...')) 51 | const parentArgvs = process.argv.slice(2) 52 | for (const driver of driversToMigrate) { 53 | console.log(chalk.cyan(` - ${driver}`)) 54 | const command = `./node_modules/.bin/prisma migrate ${parentArgvs.join( 55 | ' ' 56 | )} --schema ./prisma/${configuredDriver}/schema.prisma` 57 | console.log(chalk.gray(` $ ${chalk.reset(command)}`)) 58 | await new Promise((resolve, reject) => { 59 | const p = spawn(command, { 60 | shell: true, 61 | stdio: [process.stdin, process.stdout, process.stderr], 62 | cwd: rootDir, 63 | env: process.env 64 | }) 65 | p.on('error', reject) 66 | p.on('exit', (code) => { 67 | if (code === 0) { 68 | resolve() 69 | } else { 70 | reject(new Error(`prisma migrate failed with code ${code}`)) 71 | } 72 | }) 73 | }) 74 | } 75 | } 76 | 77 | main() 78 | .then(() => process.exit(0)) 79 | .catch((err) => { 80 | console.error(err) 81 | process.exit(1) 82 | }) 83 | -------------------------------------------------------------------------------- /libs/shiki/extends.ts: -------------------------------------------------------------------------------- 1 | // extends mean it should do something in hast tree. not inject in hook. 2 | import type { Element, Root } from 'hast' 3 | import { getDisplayNameByLanguageID } from './' 4 | 5 | /** 6 | * This function appends a header to the root node. 7 | * It will add features like language name, copy button, etc. 8 | * Note that this function should be called after the function that wrapper the shiki root node. 9 | * @param {Root} root - RootHastNode 10 | * @param {string} lang - language id 11 | */ 12 | export function appendHeader(root: Root, lang: string) { 13 | // Root -> [ShikiRootWrapper] 14 | // This force type cast is safe because we know the structure of the root node. 15 | ;(root.children[0]).children.unshift({ 16 | type: 'element', 17 | tagName: 'div', 18 | properties: { class: 'header' }, 19 | children: [ 20 | { 21 | type: 'element', 22 | tagName: 'span', 23 | properties: { 24 | class: 'language-name' 25 | }, 26 | children: [{ type: 'text', value: getDisplayNameByLanguageID(lang) }] 27 | }, 28 | { 29 | type: 'element', 30 | tagName: 'div', 31 | properties: { class: 'copy-button' }, 32 | children: [] 33 | } 34 | ] 35 | }) 36 | } 37 | 38 | /** 39 | * This function wraps the root node with a div. 40 | * @param {Root} root - RootHastNode 41 | */ 42 | export function wrapperShikiRoot(root: Root) { 43 | // Root -> pre -> code 44 | // This force type cast is safe because we know the structure of the root node. 45 | const shikiRoot = root.children[0] 46 | root.children[0] = { 47 | type: 'element', 48 | tagName: 'div', 49 | properties: { class: 'shiki-container' }, 50 | children: [shikiRoot] 51 | } 52 | } 53 | 54 | /** 55 | * This function append a lines block to the root node. 56 | * Please note that this function should be called after the functions that append the header and footer. 57 | * @param root RootHastNode 58 | */ 59 | export function appendLineNumbersBlock(root: Root, lines: number) { 60 | root.children.unshift({ 61 | type: 'element', 62 | tagName: 'div', 63 | properties: { class: 'line-numbers-container' }, 64 | children: [ 65 | { 66 | type: 'element', 67 | tagName: 'span', 68 | properties: { class: 'line block relative', 'data-line': ' ' }, 69 | children: new Array(lines).fill(0).map((_, i) => { 70 | return { 71 | type: 'element', 72 | tagName: 'span', 73 | properties: { class: 'line-number' }, 74 | children: [ 75 | { 76 | type: 'text', 77 | value: (i + 1).toString() 78 | } 79 | ] 80 | } 81 | }) 82 | } 83 | ] 84 | }) 85 | } 86 | -------------------------------------------------------------------------------- /components/ui/slider.tsx: -------------------------------------------------------------------------------- 1 | import { Slider as ChakraSlider, For, HStack } from '@chakra-ui/react' 2 | import * as React from 'react' 3 | 4 | export interface SliderProps extends ChakraSlider.RootProps { 5 | marks?: Array 6 | label?: React.ReactNode 7 | showValue?: boolean 8 | } 9 | 10 | export const Slider = React.forwardRef( 11 | function Slider(props, ref) { 12 | const { marks: marksProp, label, showValue, ...rest } = props 13 | const value = props.defaultValue ?? props.value 14 | 15 | const marks = marksProp?.map((mark) => { 16 | if (typeof mark === 'number') return { value: mark, label: undefined } 17 | return mark 18 | }) 19 | 20 | const hasMarkLabel = !!marks?.some((mark) => mark.label) 21 | 22 | return ( 23 | 24 | {label && !showValue && ( 25 | {label} 26 | )} 27 | {label && showValue && ( 28 | 29 | {label} 30 | 31 | 32 | )} 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | ) 42 | } 43 | ) 44 | 45 | function SliderThumbs(props: { value?: number[] }) { 46 | const { value } = props 47 | return ( 48 | 49 | {(_, index) => ( 50 | 51 | 52 | 53 | )} 54 | 55 | ) 56 | } 57 | 58 | interface SliderMarksProps { 59 | marks?: Array 60 | } 61 | 62 | const SliderMarks = React.forwardRef( 63 | function SliderMarks(props, ref) { 64 | const { marks } = props 65 | if (!marks?.length) return null 66 | 67 | return ( 68 | 69 | {marks.map((mark, index) => { 70 | const value = typeof mark === 'number' ? mark : mark.value 71 | const label = typeof mark === 'number' ? undefined : mark.label 72 | return ( 73 | 74 | 75 | {label} 76 | 77 | ) 78 | })} 79 | 80 | ) 81 | } 82 | ) 83 | -------------------------------------------------------------------------------- /scripts/prisma/db.ts: -------------------------------------------------------------------------------- 1 | import { RuntimeMode } from '@/enums/app' 2 | import { getRuntimeMode, loadEnv } from '@/utils/app' 3 | import { getDictionaries } from '@/utils/fs' 4 | import chalk from 'chalk' 5 | import { spawn } from 'node:child_process' 6 | import path from 'path' 7 | 8 | const mode = getRuntimeMode() 9 | const rootDir = path.resolve(__dirname, '../../') 10 | loadEnv() 11 | 12 | async function main() { 13 | // Get all supported drivers 14 | const drivers = await getDictionaries(path.join(rootDir, './prisma')) 15 | console.log( 16 | `${chalk.green('✓')} Found ${chalk.cyan(drivers.length)} drivers.` 17 | ) 18 | // list all drivers 19 | console.log(chalk.cyan('Drivers:')) 20 | console.log(drivers.map((v) => ` - ` + v).join('\n')) 21 | 22 | // Get configured driver 23 | const configuredDriver = process.env.DB_ADAPTER || '' 24 | if (!drivers.includes(configuredDriver)) { 25 | console.error( 26 | `${chalk.red('✗')} ${chalk.cyan( 27 | configuredDriver 28 | )} is not a supported driver.` 29 | ) 30 | console.error( 31 | `Please set the ${chalk.cyan( 32 | 'DB_ADAPTER' 33 | )} environment variable to one of the following:` 34 | ) 35 | console.error(drivers.map((v) => ` - ` + v).join('\n')) 36 | process.exit(1) 37 | } 38 | 39 | let driversToMigrate = [configuredDriver] 40 | if (mode === RuntimeMode.Development && process.argv.includes('-A')) { 41 | console.log( 42 | `${chalk.yellow('!')} ${chalk.cyan( 43 | 'dev' 44 | )} mode detected, will exec db command on all drivers.` 45 | ) 46 | driversToMigrate = drivers 47 | } 48 | // run prisma generate, and pipe the output to stdout, stderr, and stdin of the current process 49 | console.log(chalk.cyan('do db operation...')) 50 | const parentArgvs = process.argv.slice(2).filter((v) => !v.includes('-A')) 51 | for (const driver of driversToMigrate) { 52 | console.log(chalk.cyan(` - ${driver}`)) 53 | const command = `./node_modules/.bin/prisma db ${parentArgvs.join( 54 | ' ' 55 | )} --schema=./prisma/${configuredDriver}/schema.prisma` 56 | console.log(chalk.gray(` $ ${chalk.reset(command)}`)) 57 | await new Promise((resolve, reject) => { 58 | const p = spawn(command, { 59 | shell: true, 60 | stdio: [process.stdin, process.stdout, process.stderr], 61 | cwd: rootDir, 62 | env: process.env 63 | }) 64 | p.on('error', reject) 65 | p.on('exit', (code) => { 66 | if (code === 0) { 67 | resolve() 68 | } else { 69 | reject(new Error(`prisma migrate failed with code ${code}`)) 70 | } 71 | }) 72 | }) 73 | } 74 | } 75 | 76 | main() 77 | .then(() => process.exit(0)) 78 | .catch((err) => { 79 | console.error(err) 80 | process.exit(1) 81 | }) 82 | -------------------------------------------------------------------------------- /app/[locale]/(app)/dashboard/snippets/page.tsx: -------------------------------------------------------------------------------- 1 | import { IntlClientProvider } from '@/components/server-provider' 2 | import { auth } from '@/libs/auth' 3 | import client from '@/libs/prisma/client' 4 | import { Paste } from '@prisma/client' 5 | import { pick } from 'lodash-es' 6 | import { AbstractIntlMessages, useLocale, useMessages } from 'next-intl' 7 | import { getTimeZone } from 'next-intl/server' 8 | import Header from '../_components/header' 9 | import Shell from '../_components/shell' 10 | import { AddButton } from './_components/button' 11 | import Snippet from './_components/snippet' 12 | function getSnippets(userID: string) { 13 | return client.paste.findMany({ 14 | where: { 15 | userId: userID 16 | }, 17 | orderBy: { 18 | createdAt: 'desc' 19 | }, 20 | select: { 21 | id: true, 22 | title: true, 23 | createdAt: true, 24 | expiredAt: true, 25 | description: true, 26 | syntax: true, 27 | type: true, 28 | content: false 29 | } 30 | }) 31 | } 32 | 33 | function AddButtonIntlProvider({ children }: { children: React.ReactNode }) { 34 | const locale = useLocale() 35 | const messages = useMessages() 36 | return ( 37 | 41 | {children} 42 | 43 | ) 44 | } 45 | 46 | export default async function SnippetsPage() { 47 | // eslint-disable-next-line react-hooks/rules-of-hooks 48 | const locale = useLocale() 49 | const timeZone = await getTimeZone({ locale }) 50 | const session = await auth() 51 | const snippets = await getSnippets(session!.user.id) 52 | 53 | return ( 54 | 55 |
56 | 57 | 58 | 59 |
60 | {snippets.length > 0 ? ( 61 |
62 | {snippets.map((snippet) => ( 63 | 69 | ))} 70 |
71 | ) : ( 72 |
73 |
74 | 79 |
80 | 81 |

82 | No snippets found. 83 |

84 |
85 | )} 86 |
87 | ) 88 | } 89 | -------------------------------------------------------------------------------- /.vscode/unocss.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1.1, 3 | "atDirectives": [ 4 | { 5 | "name": "@tailwind", 6 | "description": "Use the `@tailwind` directive to insert Tailwind's `base`, `components`, `utilities` and `screens` styles into your CSS.", 7 | "references": [ 8 | { 9 | "name": "Tailwind Documentation", 10 | "url": "https://tailwindcss.com/docs/functions-and-directives#tailwind" 11 | } 12 | ] 13 | }, 14 | { 15 | "name": "@unocss", 16 | "description": "Use the `@unocss` directive to insert UnoCSS's `base`, `components`, `utilities` and `screens` styles into your CSS.", 17 | "references": [ 18 | { 19 | "name": "UnoCSS Documentation", 20 | "url": "https://unocss.dev/integrations/postcss#unocss" 21 | } 22 | ] 23 | }, 24 | { 25 | "name": "@apply", 26 | "description": "Use the `@apply` directive to inline any existing utility classes into your own custom CSS. This is useful when you find a common utility pattern in your HTML that you’d like to extract to a new component.", 27 | "references": [ 28 | { 29 | "name": "Tailwind Documentation", 30 | "url": "https://tailwindcss.com/docs/functions-and-directives#apply" 31 | } 32 | ] 33 | }, 34 | { 35 | "name": "@responsive", 36 | "description": "You can generate responsive variants of your own classes by wrapping their definitions in the `@responsive` directive:\n```css\n@responsive {\n .alert {\n background-color: #E53E3E;\n }\n}\n```\n", 37 | "references": [ 38 | { 39 | "name": "Tailwind Documentation", 40 | "url": "https://tailwindcss.com/docs/functions-and-directives#responsive" 41 | } 42 | ] 43 | }, 44 | { 45 | "name": "@screen", 46 | "description": "The `@screen` directive allows you to create media queries that reference your breakpoints by **name** instead of duplicating their values in your own CSS:\n```css\n@screen sm {\n /* ... */\n}\n```\n…gets transformed into this:\n```css\n@media (min-width: 640px) {\n /* ... */\n}\n```\n", 47 | "references": [ 48 | { 49 | "name": "Tailwind Documentation", 50 | "url": "https://tailwindcss.com/docs/functions-and-directives#screen" 51 | } 52 | ] 53 | }, 54 | { 55 | "name": "@variants", 56 | "description": "Generate `hover`, `focus`, `active` and other **variants** of your own utilities by wrapping their definitions in the `@variants` directive:\n```css\n@variants hover, focus {\n .btn-brand {\n background-color: #3182CE;\n }\n}\n```\n", 57 | "references": [ 58 | { 59 | "name": "Tailwind Documentation", 60 | "url": "https://tailwindcss.com/docs/functions-and-directives#variants" 61 | } 62 | ] 63 | } 64 | ] 65 | } 66 | -------------------------------------------------------------------------------- /.github/workflows/node.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: 4 | pull_request: 5 | branches: [main] 6 | push: 7 | branches: [main] 8 | jobs: 9 | test: 10 | name: Lint 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | node-version: [20.x] 15 | steps: 16 | - uses: actions/checkout@v4 17 | - name: Use Node.js ${{ matrix.node-version }} 18 | uses: actions/setup-node@v4 19 | with: 20 | node-version: ${{ matrix.node-version }} 21 | - uses: pnpm/action-setup@v2 22 | name: Install pnpm 23 | with: 24 | version: 9.15.9 25 | run_install: false 26 | - name: Get pnpm store directory 27 | shell: bash 28 | run: | 29 | echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV 30 | 31 | - uses: actions/cache@v4 32 | name: Setup pnpm cache 33 | with: 34 | path: ${{ env.STORE_PATH }} 35 | key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} 36 | restore-keys: | 37 | ${{ runner.os }}-pnpm-store- 38 | 39 | - name: Install dependencies 40 | run: DB_ADAPTER=postgresql pnpm install 41 | 42 | - name: Lint 43 | run: pnpm lint 44 | 45 | # build: 46 | # name: Build 47 | # runs-on: ubuntu-latest 48 | # strategy: 49 | # matrix: 50 | # node-version: [20.x] 51 | # steps: 52 | # - uses: actions/checkout@v3 53 | # - name: Use Node.js ${{ matrix.node-version }} 54 | # uses: actions/setup-node@v3 55 | # with: 56 | # node-version: ${{ matrix.node-version }} 57 | # - uses: pnpm/action-setup@v2 58 | # with: 59 | # version: 8 60 | # - name: Get pnpm store directory 61 | # shell: bash 62 | # run: | 63 | # echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV 64 | 65 | # - uses: actions/cache@v3 66 | # name: Setup pnpm cache 67 | # with: 68 | # path: ${{ env.STORE_PATH }} 69 | # key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} 70 | # restore-keys: | 71 | # ${{ runner.os }}-pnpm-store- 72 | 73 | # - name: Install dependencies 74 | # run: pnpm install 75 | # - name: Create .env file 76 | # uses: SpicyPizza/create-envfile@v2.0 77 | # with: 78 | # file_name: .env.production 79 | # envkey_HITOKOTO_COMMON_API_ENDPOINT: ${{ secrets.HITOKOTO_COMMON_API_ENDPOINT }} 80 | # envkey_HITOKOTO_REVIEWER_API_ENDPOINT: ${{ secrets.HITOKOTO_REVIEWER_API_ENDPOINT }} 81 | # envkey_HITOKOTO_SEARCH_API_ENDPOINT: ${{ secrets.HITOKOTO_SEARCH_API_ENDPOINT }} 82 | # envkey_HITOKOTO_SEARCH_API_PUBKEY: ${{ secrets.HITOKOTO_SEARCH_API_PUBKEY }} 83 | # envkey_COOKIES_ENCRYPT_KEY: ${{ secrets.COOKIES_ENCRYPT_KEY }} 84 | # - name: Build 85 | # run: pnpm build 86 | -------------------------------------------------------------------------------- /hooks/actions.ts: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { ResponseCode } from '@/enums/response' 4 | import { ActionReturn, State } from '@/utils/actions' 5 | import { Awaitable } from '@/utils/types' 6 | import { useEffect, useRef, type MutableRefObject } from 'react' 7 | 8 | import { useFormState } from 'react-dom' 9 | 10 | /** 11 | * useSubmitForm is a wrapper to use React Server Actions with callbacks handled correctly. 12 | * Note that {ActionReturn} is a type alias of {State}. 13 | * @param serverAction - The server action to be invoked. 14 | * @param callbacks - Callbacks to be invoked when the server action is done. 15 | * @returns The state of the server action, the form reference and the action to be invoked. 16 | */ 17 | export function useSubmitForm( 18 | serverAction: ( 19 | preState: State, 20 | formData: FormData 21 | ) => Awaitable>, 22 | callbacks?: { 23 | onSuccess?: ( 24 | state: State, 25 | ref: MutableRefObject 26 | ) => void 27 | onError?: ( 28 | state: State, 29 | ref: MutableRefObject 30 | ) => void 31 | } 32 | ): { 33 | state: State 34 | ref: MutableRefObject 35 | action: (formData: FormData) => void 36 | } 37 | 38 | export function useSubmitForm( 39 | serverAction: ( 40 | preState: State, 41 | formData: FormData 42 | ) => Awaitable>, 43 | callbacks: { 44 | onSuccess?: ( 45 | state: State, 46 | ref: MutableRefObject 47 | ) => void 48 | onError?: ( 49 | state: State, 50 | ref: MutableRefObject 51 | ) => void 52 | } = {} 53 | ) { 54 | const { onSuccess = () => {}, onError = () => {} } = callbacks 55 | const ref = useRef(null) as MutableRefObject 56 | 57 | const [state, formAction] = useFormState>( 58 | // TODO: use best practice to fix this type assertion 59 | // This type assertion is intended, because of react server actions is not giving a best practice. 60 | serverAction as unknown as (formData: State) => Promise>, 61 | { 62 | status: ResponseCode.NotInvoke 63 | } as State 64 | ) 65 | 66 | useEffect(() => { 67 | if ( 68 | (state?.status !== ResponseCode.OK && 69 | state?.status !== ResponseCode.NotInvoke) || 70 | state?.error 71 | ) { 72 | if (typeof onError === 'function') { 73 | onError(state, ref) 74 | } 75 | } 76 | 77 | if (state?.status == ResponseCode.OK) { 78 | if (typeof onSuccess === 'function') { 79 | onSuccess(state, ref) 80 | } 81 | } 82 | // eslint-disable-next-line react-hooks/exhaustive-deps 83 | }, [state, ref]) 84 | 85 | return { state, ref, action: formAction } 86 | } 87 | -------------------------------------------------------------------------------- /utils/strings.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * TrimChars returns a slice of the string s with all leading and trailing Unicode code points contained in cutset removed. 3 | * @param str raw string 4 | * @param cutset the char set to trim 5 | * @return {string} 6 | */ 7 | export function trimChars(str: string, cutset: string) { 8 | let start = 0, 9 | end = str.length 10 | 11 | while (start < end && cutset.indexOf(str[start]) >= 0) ++start 12 | 13 | while (end > start && cutset.indexOf(str[end - 1]) >= 0) --end 14 | 15 | return start > 0 || end < str.length ? str.substring(start, end) : str 16 | } 17 | 18 | /** 19 | * TrimCharsLeft returns a slice of the string s with all leading Unicode code points contained in cutset removed. 20 | * @param str raw string 21 | * @param cutset the char set to trim 22 | * @return {string} 23 | */ 24 | export function trimCharsLeft(str: string, cutset: string) { 25 | let start = 0 26 | const end = str.length 27 | 28 | while (start < end && cutset.indexOf(str[start]) >= 0) ++start 29 | 30 | return start > 0 || end < str.length ? str.substring(start, end) : str 31 | } 32 | 33 | /** 34 | * TrimCharsRight returns a slice of the string s, with all trailing Unicode code points contained in cutset removed. 35 | * @param str raw string 36 | * @param cutset the char set to trim 37 | * @return {string} 38 | */ 39 | export function trimCharsRight(str: string, cutset: string) { 40 | let end = str.length 41 | const start = 0 42 | 43 | while (end > start && cutset.indexOf(str[end - 1]) >= 0) --end 44 | 45 | return start > 0 || end < str.length ? str.substring(start, end) : str 46 | } 47 | 48 | /** 49 | * return the number of lines in a string 50 | * @param {string} str - Raw string 51 | * @return {number} - Number of lines 52 | */ 53 | export function getLines(str: string): number { 54 | return str.split('\n').length 55 | } 56 | 57 | export type TranslationKey = `[[${string}]]` 58 | 59 | /** 60 | * Judge whether the string is a translation key 61 | * @param str - Raw string 62 | * @returns {boolean} - Whether the string is a translation key 63 | */ 64 | export function isTranslationKey(str: string): boolean { 65 | return /^\[\[.*\]\]$/.test(str) 66 | } 67 | 68 | /** 69 | * Unwrap the translation key 70 | * @param str - Raw string 71 | * @returns {string} - Unwrapped string 72 | */ 73 | export function unwrapTranslationKey(str: TranslationKey): string { 74 | return str.slice(2, -2) 75 | } 76 | 77 | /** 78 | * Wrap the translation key 79 | * @param str - Raw string 80 | * @returns {TranslationKey} - Wrapped string 81 | */ 82 | export function wrapTranslationKey(str: string): TranslationKey { 83 | return `[[${str}]]` as TranslationKey 84 | } 85 | 86 | // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type 87 | export function translateIfKey(t: Function, str: string): string { 88 | return isTranslationKey(str) 89 | ? t(unwrapTranslationKey(str as TranslationKey)) 90 | : str 91 | } 92 | -------------------------------------------------------------------------------- /scripts/prisma/generate.ts: -------------------------------------------------------------------------------- 1 | import { loadEnv } from '@/utils/app' 2 | import { getDictionaries } from '@/utils/fs' 3 | import chalk from 'chalk' 4 | import { spawn } from 'node:child_process' 5 | import path from 'path' 6 | 7 | const rootDir = path.resolve(__dirname, '../../') 8 | loadEnv() 9 | 10 | async function main() { 11 | // Get all supported drivers 12 | 13 | const drivers = await getDictionaries(path.join(rootDir, './prisma')) 14 | console.log( 15 | `${chalk.green('✓')} Found ${chalk.cyan(drivers.length)} drivers.` 16 | ) 17 | // list all drivers 18 | console.log(chalk.cyan('Drivers:')) 19 | console.log(drivers.map((v) => ` - ` + v).join('\n')) 20 | 21 | // Get configured driver 22 | const configuredDriver = process.env.DB_ADAPTER || 'postgresql' 23 | if (!drivers.includes(configuredDriver)) { 24 | console.error( 25 | `${chalk.red('✗')} ${chalk.cyan( 26 | configuredDriver 27 | )} is not a supported driver.` 28 | ) 29 | console.error( 30 | `Please set the ${chalk.cyan( 31 | 'DB_ADAPTER' 32 | )} environment variable to one of the following:` 33 | ) 34 | console.error(drivers.map((v) => ` - ` + v).join('\n')) 35 | process.exit(1) 36 | } 37 | // run prisma generate, and pipe the output to stdout, stderr, and stdin of the current process 38 | console.log(chalk.cyan('Generating...')) 39 | console.log(chalk.cyan(` - ${configuredDriver}`)) 40 | const command = `./node_modules/.bin/prisma generate --schema=./prisma/${configuredDriver}/schema.prisma` 41 | console.log(chalk.gray(` $ ${chalk.reset(command)}`)) 42 | await new Promise((resolve, reject) => { 43 | const p = spawn(command, { 44 | shell: true, 45 | stdio: [process.stdin, process.stdout, process.stderr], 46 | cwd: rootDir, 47 | env: process.env 48 | }) 49 | p.on('error', reject) 50 | p.on('exit', (code) => { 51 | if (code === 0) { 52 | resolve() 53 | } else { 54 | reject(new Error(`prisma generate failed with code ${code}`)) 55 | } 56 | }) 57 | }) 58 | // for (const driver of drivers) { 59 | // console.log(chalk.cyan(` - ${driver}`)) 60 | // await new Promise((resolve, reject) => { 61 | // const p = spawn( 62 | // `pnpm prisma generate --schema=./prisma/${driver}/schema.prisma`, 63 | // { 64 | // shell: true, 65 | // stdio: [process.stdin, process.stdout, process.stderr], 66 | // cwd: rootDir, 67 | // env: process.env 68 | // } 69 | // ) 70 | // p.on('error', reject) 71 | // p.on('exit', (code) => { 72 | // if (code === 0) { 73 | // resolve() 74 | // } else { 75 | // reject(new Error(`prisma generate failed with code ${code}`)) 76 | // } 77 | // }) 78 | // }) 79 | // } 80 | } 81 | 82 | main() 83 | .then(() => process.exit(0)) 84 | .catch((err) => { 85 | console.error(err) 86 | process.exit(1) 87 | }) 88 | -------------------------------------------------------------------------------- /styles/global.scss: -------------------------------------------------------------------------------- 1 | @import url('../node_modules/@unocss/reset/sanitize/sanitize.css'); 2 | @import url('../node_modules/@unocss/reset/sanitize/assets.css'); 3 | 4 | // /* style.css */ 5 | // @unocss preflights; 6 | // @unocss default; 7 | 8 | // /* 9 | // Fallback layer. It's always recommended to include. 10 | // Only unused layers will be injected here. 11 | // */ 12 | // @unocss; 13 | 14 | @tailwind base; 15 | @tailwind components; 16 | @tailwind utilities; 17 | 18 | @layer base { 19 | :root { 20 | // Background 21 | --background: 0 0% 100%; 22 | --foreground: 222.2 47.4% 11.2%; 23 | // Mute 24 | --muted: 210 40% 96.1%; 25 | --muted-foreground: 215.4 16.3% 46.9%; 26 | // Popover 27 | --popover: 0 0% 100%; 28 | --popover-foreground: 222.2 47.4% 11.2%; 29 | // Border 30 | --border: 214.3 31.8% 91.4%; 31 | --input: 214.3 31.8% 91.4%; 32 | // Card 33 | --card: 0 0% 100%; 34 | --card-foreground: 222.2 47.4% 11.2%; 35 | // Primary 36 | --primary: 222.2 47.4% 11.2%; 37 | --primary-foreground: 210 40% 98%; 38 | // Secondary 39 | --secondary: 210 40% 96.1%; 40 | --secondary-foreground: 222.2 47.4% 11.2%; 41 | // Accent 42 | --accent: 210 40% 96.1%; 43 | --accent-foreground: 222.2 47.4% 11.2%; 44 | // Destructive 45 | --destructive: 0 100% 50%; 46 | --destructive-foreground: 210 40% 98%; 47 | // Ring 48 | --ring: 215 20.2% 65.1%; 49 | // Radius 50 | --radius: 0.5rem; 51 | } 52 | 53 | .dark { 54 | // Background 55 | --background: 224 71% 4%; 56 | --foreground: 213 31% 91%; 57 | // Mute 58 | --muted: 223 47% 11%; 59 | --muted-foreground: 215.4 16.3% 56.9%; 60 | // Popover 61 | --accent: 216 34% 17%; 62 | --accent-foreground: 210 40% 98%; 63 | // Border 64 | --popover: 224 71% 4%; 65 | --popover-foreground: 215 20.2% 65.1%; 66 | // Card 67 | --border: 216 34% 17%; 68 | --input: 216 34% 17%; 69 | // Card 70 | --card: 224 71% 4%; 71 | --card-foreground: 213 31% 91%; 72 | // Primary 73 | --primary: 210 40% 98%; 74 | --primary-foreground: 222.2 47.4% 1.2%; 75 | // Secondary 76 | --secondary: 222.2 47.4% 11.2%; 77 | --secondary-foreground: 210 40% 98%; 78 | // Accent 79 | --destructive: 0 63% 31%; 80 | --destructive-foreground: 210 40% 98%; 81 | // Ring 82 | --ring: 216 34% 17%; 83 | // Radius 84 | --radius: 0.5rem; 85 | } 86 | } 87 | 88 | @layer base { 89 | * { 90 | @apply border-border; 91 | 92 | box-sizing: border-box; 93 | } 94 | 95 | body { 96 | @apply bg-background text-foreground; 97 | 98 | height: 100vh; 99 | font-feature-settings: 100 | 'rlig' 1, 101 | 'calt' 1; 102 | color: rgb(var(--foreground-rgb)); 103 | background: linear-gradient( 104 | to bottom, 105 | transparent, 106 | rgb(var(--background-end-rgb)) 107 | ) 108 | rgb(var(--background-start-rgb)); 109 | } 110 | 111 | button { 112 | @apply cursor-pointer border-0; 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /prisma/postgresql/migrations/20231026133156_recreate_next_auth_tables/migration.sql: -------------------------------------------------------------------------------- 1 | 2 | -- DropForeignKey 3 | ALTER TABLE "public"."accounts" DROP CONSTRAINT "accounts_user_id_fkey"; 4 | 5 | -- DropForeignKey 6 | ALTER TABLE "public"."sessions" DROP CONSTRAINT "sessions_user_id_fkey"; 7 | 8 | -- DropForeignKey 9 | ALTER TABLE "public"."user_password_resets" DROP CONSTRAINT "user_password_resets_user_id_fkey"; 10 | 11 | -- DropTable 12 | DROP TABLE "public"."accounts"; 13 | 14 | -- DropTable 15 | DROP TABLE "public"."sessions"; 16 | 17 | -- DropTable 18 | DROP TABLE "public"."user_password_resets"; 19 | 20 | -- DropTable 21 | DROP TABLE "public"."verification_requests"; 22 | 23 | -- CreateTable 24 | CREATE TABLE "user_verification" ( 25 | "id" TEXT NOT NULL, 26 | "user_id" TEXT NOT NULL, 27 | "token" TEXT NOT NULL, 28 | "type" INTEGER NOT NULL DEFAULT 1, 29 | "expired_at" TIMESTAMP(3) NOT NULL, 30 | "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 31 | "updated_at" TIMESTAMP(3) NOT NULL, 32 | 33 | CONSTRAINT "user_verification_pkey" PRIMARY KEY ("id") 34 | ); 35 | 36 | -- CreateTable 37 | CREATE TABLE "accounts" ( 38 | "id" TEXT NOT NULL, 39 | "user_id" TEXT NOT NULL, 40 | "type" TEXT NOT NULL, 41 | "provider" TEXT NOT NULL, 42 | "provider_account_id" TEXT NOT NULL, 43 | "refresh_token" TEXT, 44 | "access_token" TEXT, 45 | "expires_at" INTEGER, 46 | "token_type" TEXT, 47 | "scope" TEXT, 48 | "id_token" TEXT, 49 | "session_state" TEXT, 50 | 51 | CONSTRAINT "accounts_pkey" PRIMARY KEY ("id") 52 | ); 53 | 54 | -- CreateTable 55 | CREATE TABLE "sessions" ( 56 | "id" TEXT NOT NULL, 57 | "session_token" TEXT NOT NULL, 58 | "user_id" TEXT NOT NULL, 59 | "expires" TIMESTAMP(3) NOT NULL, 60 | 61 | CONSTRAINT "sessions_pkey" PRIMARY KEY ("id") 62 | ); 63 | 64 | -- CreateTable 65 | CREATE TABLE "verification_tokens" ( 66 | "identifier" TEXT NOT NULL, 67 | "token" TEXT NOT NULL, 68 | "expires" TIMESTAMP(3) NOT NULL 69 | ); 70 | 71 | -- CreateIndex 72 | CREATE UNIQUE INDEX "user_verification_token_key" ON "user_verification"("token"); 73 | 74 | -- CreateIndex 75 | CREATE UNIQUE INDEX "accounts_provider_provider_account_id_key" ON "accounts"("provider", "provider_account_id"); 76 | 77 | -- CreateIndex 78 | CREATE UNIQUE INDEX "sessions_session_token_key" ON "sessions"("session_token"); 79 | 80 | -- CreateIndex 81 | CREATE UNIQUE INDEX "verification_tokens_token_key" ON "verification_tokens"("token"); 82 | 83 | -- CreateIndex 84 | CREATE UNIQUE INDEX "verification_tokens_identifier_token_key" ON "verification_tokens"("identifier", "token"); 85 | 86 | -- AddForeignKey 87 | ALTER TABLE "user_verification" ADD CONSTRAINT "user_verification_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 88 | 89 | -- AddForeignKey 90 | ALTER TABLE "accounts" ADD CONSTRAINT "accounts_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE; 91 | 92 | -- AddForeignKey 93 | ALTER TABLE "sessions" ADD CONSTRAINT "sessions_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE; 94 | 95 | --------------------------------------------------------------------------------