├── .commitlintrc.js ├── .editorconfig ├── .env ├── .env.example ├── .eslintrc-auto-import.json ├── .eslintrc.js ├── .gitattributes ├── .github └── workflows │ └── node.yml ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .prettierignore ├── .prettierrc.js ├── .stylelintrc.js ├── .vscode ├── i18n-ally-custom-framework.yml ├── settings.json └── unocss.json ├── README.md ├── actions ├── auth.ts ├── paste.ts └── user.ts ├── app ├── [locale] │ ├── (app) │ │ ├── _components │ │ │ ├── home │ │ │ │ ├── Announcement.tsx │ │ │ │ └── form.tsx │ │ │ └── layout │ │ │ │ ├── Header.module.scss │ │ │ │ ├── Header.tsx │ │ │ │ └── header │ │ │ │ └── Navigation.tsx │ │ ├── about │ │ │ └── page.tsx │ │ ├── dashboard │ │ │ ├── _components │ │ │ │ ├── aside-nav.tsx │ │ │ │ ├── header.tsx │ │ │ │ ├── shell.tsx │ │ │ │ └── skeleton.tsx │ │ │ ├── layout.tsx │ │ │ ├── notifications │ │ │ │ └── page.tsx │ │ │ ├── page.tsx │ │ │ ├── settings │ │ │ │ ├── @profile │ │ │ │ │ ├── loading.tsx │ │ │ │ │ └── page.tsx │ │ │ │ ├── _components │ │ │ │ │ ├── app-settings.tsx │ │ │ │ │ ├── button.tsx │ │ │ │ │ ├── form.tsx │ │ │ │ │ ├── modal.tsx │ │ │ │ │ └── profiles.tsx │ │ │ │ ├── layout.tsx │ │ │ │ ├── loading.tsx │ │ │ │ └── page.tsx │ │ │ └── snippets │ │ │ │ ├── _components │ │ │ │ ├── button.tsx │ │ │ │ ├── modal.tsx │ │ │ │ ├── snippet.module.scss │ │ │ │ └── snippet.tsx │ │ │ │ ├── loading.tsx │ │ │ │ └── page.tsx │ │ ├── layout.tsx │ │ ├── page.tsx │ │ └── v │ │ │ └── [cuid] │ │ │ ├── _components │ │ │ ├── CodePreview.module.scss │ │ │ ├── CodePreview.tsx │ │ │ ├── CodePreviewIntlProvider.tsx │ │ │ └── shiki │ │ │ │ ├── Header.tsx │ │ │ │ ├── LineNumbers.tsx │ │ │ │ └── shiki.scss │ │ │ ├── page.tsx │ │ │ └── raw │ │ │ └── route.ts │ ├── (auth) │ │ ├── _components │ │ │ └── layouts │ │ │ │ ├── content.tsx │ │ │ │ ├── go-back-button.tsx │ │ │ │ └── logo.tsx │ │ ├── auth │ │ │ ├── error │ │ │ │ ├── _components │ │ │ │ │ └── preview.tsx │ │ │ │ └── page.tsx │ │ │ ├── password │ │ │ │ └── reset │ │ │ │ │ ├── [token] │ │ │ │ │ ├── _components │ │ │ │ │ │ └── form.tsx │ │ │ │ │ └── page.tsx │ │ │ │ │ ├── _components │ │ │ │ │ └── form.tsx │ │ │ │ │ └── page.tsx │ │ │ ├── signin │ │ │ │ ├── _components │ │ │ │ │ ├── Credentials.tsx │ │ │ │ │ └── OAuthProvider.tsx │ │ │ │ └── page.tsx │ │ │ └── signup │ │ │ │ ├── _components │ │ │ │ └── form.tsx │ │ │ │ └── page.tsx │ │ ├── layout.module.scss │ │ └── layout.tsx │ └── layout.tsx ├── api │ ├── auth │ │ ├── [...nextauth] │ │ │ └── route.ts │ │ └── signup │ │ │ └── _route.ts │ ├── task │ │ └── route.ts │ └── user │ │ └── authenticators │ │ └── route.ts ├── error.tsx ├── favicon.ico ├── layout.tsx └── not-found.tsx ├── auto-imports.d.ts ├── components ├── animated-logo.tsx ├── confirmation.tsx ├── form.tsx ├── navigation-link.tsx ├── provider.tsx ├── server-provider.tsx ├── status.tsx ├── ui │ ├── accordion.tsx │ ├── action-bar.tsx │ ├── alert.tsx │ ├── avatar.tsx │ ├── blockquote.tsx │ ├── breadcrumb.tsx │ ├── button.tsx │ ├── checkbox-card.tsx │ ├── checkbox.tsx │ ├── clipboard.tsx │ ├── close-button.tsx │ ├── color-mode.tsx │ ├── color-picker.tsx │ ├── data-list.tsx │ ├── dialog.tsx │ ├── drawer.tsx │ ├── empty-state.tsx │ ├── field.tsx │ ├── file-upload.tsx │ ├── hover-card.tsx │ ├── input-group.tsx │ ├── link-button.tsx │ ├── menu.tsx │ ├── native-select.tsx │ ├── number-input.tsx │ ├── pagination.tsx │ ├── password-input.tsx │ ├── pin-input.tsx │ ├── popover.tsx │ ├── progress-circle.tsx │ ├── progress.tsx │ ├── prose.tsx │ ├── provider.tsx │ ├── radio-card.tsx │ ├── radio.tsx │ ├── rating.tsx │ ├── segmented-control.tsx │ ├── select.tsx │ ├── skeleton.tsx │ ├── slider.tsx │ ├── stat.tsx │ ├── status.tsx │ ├── stepper-input.tsx │ ├── steps.tsx │ ├── switch.tsx │ ├── tag.tsx │ ├── timeline.tsx │ ├── toaster.tsx │ ├── toggle-tip.tsx │ ├── toggle.tsx │ └── tooltip.tsx └── uno-css-indicator.tsx ├── config ├── app.tsx └── dashboard.tsx ├── enums ├── app.ts ├── paste.ts ├── response.ts └── user.ts ├── env.mjs ├── hooks ├── actions.ts ├── requests │ └── user.ts └── toast.ts ├── libs ├── auth │ ├── config │ │ ├── edge.ts │ │ └── index.ts │ ├── index.ts │ ├── providers.tsx │ └── webauthn.ts ├── framer │ └── shaking.ts ├── i18n.tsx ├── navigation.ts ├── prisma │ └── client.ts ├── requests.ts ├── services │ └── users │ │ └── user.ts ├── shiki │ ├── extends.ts │ └── index.ts └── validation │ ├── auth.ts │ ├── paste.ts │ └── user.ts ├── lint-staged.config.js ├── messages ├── en.yml └── zh-CN.yml ├── middleware.ts ├── next.config.js ├── package.json ├── pnpm-lock.yaml ├── postcss.config.js ├── prisma ├── README.md └── postgresql │ ├── migrations │ ├── 20231019164642_init │ │ └── migration.sql │ ├── 20231021115443_add_paste_field_type │ │ └── migration.sql │ ├── 20231021123408_add_fields │ │ └── migration.sql │ ├── 20231024065713_add_web_authn_fields │ │ └── migration.sql │ ├── 20231026031107_add_authenticator_field_name │ │ └── migration.sql │ ├── 20231026133156_recreate_next_auth_tables │ │ └── migration.sql │ └── migration_lock.toml │ └── schema.prisma ├── public ├── next.svg └── vercel.svg ├── renovate.json ├── scripts ├── prisma │ ├── db.ts │ ├── generate.ts │ └── migrate.ts └── tools │ └── gen-oauth.ts ├── styles └── global.scss ├── tailwind.config.ts ├── tsconfig.json ├── types └── index.d.ts ├── uno.config.ts ├── utils ├── actions.ts ├── app.ts ├── cookies.ts ├── formatter.ts ├── fs.ts ├── helper.ts ├── middlewares.ts ├── response.ts ├── strings.ts ├── types.ts └── user.ts ├── vercel.json └── vite.config.ts /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greenhat616/pastebin/6156c73b4e68a7728f43b0ebd49be9eb146a594e/.env -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.lockb binary diff=lockb 2 | -------------------------------------------------------------------------------- /.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 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 | -------------------------------------------------------------------------------- /.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 -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | pnpm commitlint --edit $1 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | pnpm lint-staged 5 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .next/ 2 | auto-imports.d.ts 3 | pnpm-lock.yaml 4 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | semi: false, 3 | trailingComma: 'none', 4 | singleQuote: true 5 | } 6 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PasteBin 2 | 3 | A lightweight and modern paste bin and url shortener. 4 | 5 | ## Features 6 | 7 | :yum: Next.js 14 with `App Directory` support 8 | 9 | - `RSC` (React Server Component) for global state hold and data fetching 10 | - `React Server Actions` for forms mutation 11 | - A React style full stack solution, a alternative to `tRPC` 12 | 13 | :globe_with_meridians: I18n with `next-intl` 3 14 | 15 | :closed_lock_with_key: Auth with `next-auth` 5, including full OAuth support and basic credentials. 16 | 17 | - `next-auth` with `prisma` adapter, so that it is not support Edge environment in api route. 18 | - Credentials password hashed with `argon2` 19 | 20 | :smirk: Auto Imports with `unplugin-auto-import` and `unplugin-icons` 21 | 22 | - Necessary `Next.js` components, utils, hooks, and icons are auto imported, so that you don't need to import them manually. 23 | 24 | :shield: Validation with `zod` 25 | 26 | :gem: Database ORM with `prisma` 27 | 28 | - Upcoming multi-drivers support, including `PostgreSQL`, `MySQL`, `SQLite`, `SQL Server`, and `MongoDB` 29 | 30 | :atom_symbol: UI with `Chakra UI` 31 | 32 | :gear: CSS utils library ~~`UnoCSS`~~, use `Tailwind CSS` instead. 33 | 34 | - `UnoCSS` is a better choice for `Tailwind CSS`, but there are issues blocked the use in `webpack` or `postcss`, waiting for the fix. 35 | 36 | :screwdriver: Hooks library, provided by `react-use` and `ahooks` 37 | 38 | :package: Package management with `bun` 39 | 40 | :zap: Syntax highlight with `shikiji` 41 | 42 | :nazar_amulet: Environment variables providing and validating with `@t3-oss/env` 43 | 44 | :rainbow: `TypeScript` native support 45 | 46 | :policeman: Lints and CI process with `husky` and `lint-staged`, checking via `eslint`, `tsc`, `prettier`, and `stylelint` 47 | 48 | ## Installation 49 | 50 | You should define `database` related environment variables in `.env.local` file before running the app. 51 | 52 | It is required by `prisma` to generate database schema and types. 53 | 54 | ```bash 55 | bun i # Install dependencies and generate database schema and types 56 | ``` 57 | 58 | ## Development 59 | 60 | ```bash 61 | bun dev 62 | ``` 63 | 64 | ## Build 65 | 66 | ```bash 67 | bun run build 68 | ``` 69 | 70 | ## Preview 71 | 72 | ```bash 73 | bun start 74 | ``` 75 | -------------------------------------------------------------------------------- /actions/paste.ts: -------------------------------------------------------------------------------- 1 | 'use server' 2 | 3 | import { PasteType } from '@/enums/paste' 4 | import { ResponseCode } from '@/enums/response' 5 | import { auth } from '@/libs/auth' 6 | import client from '@/libs/prisma/client' 7 | import { 8 | Content, 9 | CreateNormalSnippetFormSchema, 10 | type CreateNormalSnippetForm 11 | } from '@/libs/validation/paste' 12 | import type { ActionReturn } from '@/utils/actions' 13 | import type { Session } from 'next-auth' 14 | import { isRedirectError } from 'next/dist/client/components/redirect-error' 15 | import { redirect } from 'next/navigation' 16 | 17 | // Type: normal 18 | export async function submitPasteNormalAction( 19 | prevState: T, 20 | formData: FormData 21 | ): Promise> { 22 | const result = await CreateNormalSnippetFormSchema.safeParseAsync({ 23 | syntax: formData.get('syntax'), 24 | content: formData.get('content'), 25 | expiration: formData.get('expiration'), 26 | poster: formData.get('poster'), 27 | redirect: formData.get('redirect') 28 | }) 29 | if (!result.success) 30 | return nok(ResponseCode.ValidationFailed, { 31 | error: wrapTranslationKey( 32 | 'components.code_form.feedback.validation_failed' 33 | ), 34 | issues: result.error.issues 35 | }) 36 | const content = [ 37 | { 38 | type: PasteType.Normal, 39 | syntax: result.data.syntax, 40 | filename: '', 41 | content: result.data.content 42 | } 43 | ] as Content[] 44 | const session = await auth() 45 | try { 46 | const newPaste = await client.paste.create({ 47 | data: { 48 | title: '', 49 | description: '', 50 | syntax: result.data.syntax, 51 | type: PasteType.Normal, 52 | content, 53 | poster: result.data.poster, 54 | // TODO: remove this force cast, waiting for next-auth update 55 | userId: session ? (session as unknown as Session).user.id : null, 56 | expiredAt: 57 | result.data.expiration === -1 // -1 means never expired 58 | ? null 59 | : new Date(Date.now() + result.data.expiration * 1000) 60 | } 61 | }) 62 | if (result.data.redirect) redirect(`/v/${newPaste.id}`) 63 | return ok({ 64 | id: newPaste.id 65 | }) 66 | } catch (error) { 67 | if (isRedirectError(error)) throw error 68 | console.error(error) 69 | return nok(ResponseCode.OperationFailed, { 70 | error: (error as Error).message 71 | }) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/[locale]/(app)/_components/layout/header/Navigation.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import { Link, usePathname } from '@/libs/navigation' 3 | import { Disclosure } from '@headlessui/react' 4 | import { useTranslations } from 'next-intl' 5 | import { useMemo } from 'react' 6 | import styles from '../Header.module.scss' 7 | 8 | const items = [ 9 | { key: 'home', href: '/', current: true }, 10 | { key: 'about', href: '/about', current: false }, 11 | { 12 | key: 'github', 13 | href: 'https://github.com/greenhat616/pastebin', 14 | current: false 15 | } 16 | ] 17 | 18 | type Props = { 19 | className?: string 20 | platform?: 'pc' | 'mobile' 21 | } 22 | 23 | export default function Navigation(props: Props) { 24 | const { platform = 'pc' } = props 25 | const pathname = usePathname() 26 | const t = useTranslations('app.nav.menu') 27 | const filteredItems = useMemo(() => { 28 | return items.map((item) => { 29 | return { 30 | ...item, 31 | current: item.href === pathname 32 | } 33 | }) 34 | }, [pathname]) 35 | 36 | return ( 37 | <> 38 | {filteredItems.map((item) => 39 | platform === 'pc' ? ( 40 | 51 | {t(item.key)} 52 | 53 | ) : ( 54 | 66 | {t(item.key)} 67 | 68 | ) 69 | )} 70 | 71 | ) 72 | } 73 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/[locale]/(app)/dashboard/_components/aside-nav.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import { Link } from '@/libs/navigation' 3 | import { SidebarNavItem } from '@/types' 4 | import { Flex, Text } from '@chakra-ui/react' 5 | import { usePathname } from 'next/navigation' 6 | 7 | // function DropdownItem({ 8 | // children, 9 | // item 10 | // }: { 11 | // children: React.ReactNode 12 | // item: SidebarNavItem 13 | // }) { 14 | // const [isOpen, setIsOpen] = useState(false) 15 | // return ( 16 | // setIsOpen(!isOpen)} 25 | // > 26 | // {children} 27 | // 28 | // ) 29 | // } 30 | 31 | function Item({ item, active }: { item: SidebarNavItem; active?: boolean }) { 32 | // if (item.items) { 33 | // return ( 34 | // 35 | // {item.items.map((subItem, index) => { 36 | // return 37 | // })} 38 | // 39 | // ) 40 | // } 41 | 42 | return ( 43 | 52 | {item.icon && ( 53 | {item.icon} 54 | )} 55 | {item.title} 56 | 57 | ) 58 | } 59 | 60 | export default function AsideNav({ items }: { items: SidebarNavItem[] }) { 61 | const pathname = usePathname() 62 | if (!items.length) return null 63 | 64 | return ( 65 | 72 | ) 73 | } 74 | -------------------------------------------------------------------------------- /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/_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/[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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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)/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/settings/_components/button.tsx: -------------------------------------------------------------------------------- 1 | import { linkAccountAction, unlinkAccountAction } from '@/actions/user' 2 | import { ResponseCode } from '@/enums/response' 3 | import { Button } from '@/components/ui/button' 4 | import { useMemoizedFn } from 'ahooks' 5 | import { useRouter } from 'next/navigation' 6 | import { useTransition, type ReactNode } from 'react' 7 | import { toaster } from '@/components/ui/toaster' 8 | 9 | export type SSO = { 10 | id: string 11 | name: string 12 | icon: ReactNode 13 | } & { 14 | connected: boolean 15 | } 16 | 17 | export function SSOButton({ sso }: { sso: SSO }) { 18 | const router = useRouter() 19 | const [pending, startTransition] = useTransition() 20 | 21 | const onLink = useMemoizedFn(() => { 22 | startTransition(async () => { 23 | try { 24 | const res = await linkAccountAction(sso.id) 25 | if (res.status !== ResponseCode.OK) { 26 | throw new Error(res.error) 27 | } 28 | const win = window.open(res.data!.url, '_blank') 29 | const timer = setInterval(checkWinIsClosed, 500) 30 | function checkWinIsClosed() { 31 | if (win?.closed) { 32 | clearInterval(timer) 33 | router.refresh() 34 | } 35 | } 36 | } catch (e) { 37 | toaster.create({ 38 | title: 'Link account failed.', 39 | description: e instanceof Error ? e.message : 'Unknown error', 40 | type: 'error', 41 | duration: 5000 42 | }) 43 | } 44 | }) 45 | }) 46 | 47 | const onUnlink = useMemoizedFn(() => { 48 | startTransition(async () => { 49 | try { 50 | const res = await unlinkAccountAction(sso.id) 51 | if (res.status !== ResponseCode.OK) { 52 | throw new Error(res.error) 53 | } 54 | toaster.create({ 55 | title: 'Account unlinked.', 56 | type: 'success', 57 | duration: 5000 58 | }) 59 | router.refresh() 60 | } catch (e) { 61 | toaster.create({ 62 | title: 'Link account failed.', 63 | description: e instanceof Error ? e.message : 'Unknown error', 64 | type: 'error', 65 | duration: 5000 66 | }) 67 | } 68 | }) 69 | }) 70 | 71 | const text = `${sso.connected ? 'Unlink' : 'Link'} ${sso.name}` 72 | 73 | return ( 74 | 84 | ) 85 | } 86 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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]/(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]/(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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/[locale]/(app)/dashboard/snippets/_components/snippet.module.scss: -------------------------------------------------------------------------------- 1 | .snippet { 2 | @apply flex; 3 | } 4 | -------------------------------------------------------------------------------- /app/[locale]/(app)/dashboard/snippets/_components/snippet.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { PasteType } from '@/enums/paste' 4 | import { getDisplayNameByLanguageID } from '@/libs/shiki' 5 | import { Card, Flex } from '@chakra-ui/react' 6 | import { Tag } from '@/components/ui/tag' 7 | import { type Paste } from '@prisma/client' 8 | 9 | import { Link } from '@/libs/navigation' 10 | import { motion } from 'framer-motion' 11 | import styles from './snippet.module.scss' 12 | 13 | export type SnippetProps = { 14 | locale: string 15 | timeZone: string 16 | snippet: Paste 17 | } 18 | 19 | export default function Snippet({ locale, timeZone, snippet }: SnippetProps) { 20 | return ( 21 | 26 | {/* eslint-disable-next-line @typescript-eslint/no-explicit-any */} 27 | 28 | 34 | 35 | 36 |

37 | {snippet.title || 'Untitled Snippet'} 38 |

39 |
40 | {formatPasteType(snippet.type as PasteType)} 41 | {(snippet.type as PasteType) === PasteType.Normal && ( 42 | 43 | {getDisplayNameByLanguageID(snippet.syntax || 'text')} 44 | 45 | )} 46 |
47 |
48 |
49 | 50 |

51 | {snippet.description || 'No description provided.'} 52 |

53 |
54 |

55 | Posted on:{' '} 56 | {newDayjs(snippet.createdAt, { 57 | timeZone, 58 | locale 59 | }).fromNow()} 60 |

61 | 62 | {!!snippet.expiredAt && ( 63 | 64 | Expired{' '} 65 | {newDayjs(snippet.expiredAt, { 66 | timeZone, 67 | locale 68 | }).fromNow()} 69 | 70 | )} 71 |
72 |
73 |
74 | 75 |
76 | ) 77 | } 78 | -------------------------------------------------------------------------------- /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)/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/[locale]/(app)/v/[cuid]/_components/CodePreview.module.scss: -------------------------------------------------------------------------------- 1 | .code-preview { 2 | @apply m-0 flex; 3 | 4 | &, 5 | :where(pre, code, kbd, samp) { 6 | @apply font-code; 7 | 8 | font-variant-ligatures: contextual; 9 | } 10 | 11 | .shiki-container { 12 | @apply flex-1 relative overflow-hidden flex; 13 | 14 | .header { 15 | @apply block text-right text-gray-400 select-none absolute top-0 right-0 w-full; 16 | 17 | .language-name { 18 | @apply whitespace-normal absolute right-8 top-1.5 text-xs; 19 | @apply duration-75 transition-all ease-linear opacity-100; 20 | } 21 | 22 | .actions-group { 23 | @apply grid grid-cols-2 gap-2 absolute right-5 top-3 text-xs w-fit h-10; 24 | @apply duration-75 transition-all ease-linear opacity-0 scale-0; 25 | 26 | .share-button, 27 | .copy-button { 28 | @apply flex items-center justify-center text-xs w-10 h-10 rounded-lg border border-solid border-gray-200 cursor-pointer bg-white opacity-80; 29 | } 30 | } 31 | } 32 | 33 | .content { 34 | @apply pl-5 py-3 flex-1 overflow-x-auto; 35 | 36 | scrollbar-width: none; 37 | } 38 | } 39 | 40 | &:hover { 41 | .shiki-container { 42 | .header { 43 | .language-name { 44 | @apply opacity-0; 45 | } 46 | 47 | .actions-group { 48 | @apply opacity-100 scale-100; 49 | } 50 | } 51 | } 52 | } 53 | 54 | .line-numbers-container { 55 | @apply block border-0 border-r border-solid border-gray-200 w-auto py-3; 56 | 57 | .line-number { 58 | @apply block text-right text-gray-400 select-none; 59 | @apply pl-2 pr-3; 60 | 61 | width: var(--line-numbers-width, 2em); 62 | } 63 | } 64 | 65 | // .shiki { 66 | // .line[data-line] { 67 | // &::before { 68 | // @apply inline-block text-right text-gray-400 select-none; 69 | // @apply pl-2 mr-5 pr-3 border-0 border-r-1 border-solid border-gray-200; 70 | 71 | // width: var(--line-numbers-width, 2em); 72 | // content: attr(data-line); 73 | // } 74 | // } 75 | // } 76 | } 77 | -------------------------------------------------------------------------------- /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)/v/[cuid]/_components/shiki/Header.tsx: -------------------------------------------------------------------------------- 1 | import { getDisplayNameByLanguageID } from '@/libs/shiki' 2 | import { Box } from '@chakra-ui/react' 3 | import { motion } from 'framer-motion' 4 | import styles from '../CodePreview.module.scss' 5 | type Props = { 6 | lang: string 7 | onCopyClick?: (e: MouseEvent | PointerEvent | TouchEvent) => void 8 | onShareClick?: (e: MouseEvent | PointerEvent | TouchEvent) => void 9 | } 10 | 11 | export default function ShikiHeader(props: Props) { 12 | return ( 13 | 14 | 15 | {getDisplayNameByLanguageID(props.lang)} 16 | 17 | 18 | { 31 | if (props.onShareClick) props.onShareClick(e) 32 | }} 33 | > 34 | 35 | 36 | { 49 | if (props.onCopyClick) props.onCopyClick(e) 50 | }} 51 | > 52 | 53 | 54 | 55 | 56 | ) 57 | } 58 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/[locale]/(app)/v/[cuid]/_components/shiki/shiki.scss: -------------------------------------------------------------------------------- 1 | .code-preview { 2 | .shiki { 3 | overflow: inherit; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /app/[locale]/(app)/v/[cuid]/raw/route.ts: -------------------------------------------------------------------------------- 1 | import { PasteType } from '@/enums/paste' 2 | import client from '@/libs/prisma/client' 3 | import type { Content } from '@/libs/validation/paste' 4 | import { NextRequest } from 'next/server' 5 | 6 | export async function GET( 7 | req: NextRequest, 8 | { params }: { params: Promise<{ cuid: string }> } 9 | ) { 10 | const { cuid } = await params 11 | if (!cuid) return new Response('Not Found', { status: 404 }) 12 | const result = await client.paste.findUnique({ 13 | where: { id: cuid } 14 | }) 15 | if (!result) return new Response('Not Found', { status: 404 }) 16 | switch (result.type) { 17 | case PasteType.Gist: 18 | const filename = req.nextUrl.searchParams.get('filename') 19 | if (!filename) 20 | return new Response( 21 | `Not supported operation. You might want to view /v/${cuid}?filename=example.txt?`, 22 | { status: 400 } 23 | ) 24 | const content = (result.content as Array).find( 25 | (content) => content.filename === filename 26 | ) as Content | undefined 27 | if (!content) return new Response('Not Found', { status: 404 }) 28 | return new Response(content.content, { 29 | headers: { 30 | 'Content-Type': 'text/plain; charset=utf-8' 31 | } 32 | }) 33 | case PasteType.Normal: 34 | return new Response((result.content as Array)[0].content, { 35 | headers: { 36 | 'Content-Type': 'text/plain; charset=utf-8' 37 | } 38 | }) 39 | default: 40 | return new Response( 41 | `Not Supported paste type. Maybe you want to view /v/${cuid}/raw/filename`, 42 | { status: 400 } 43 | ) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /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]/(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]/(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]/(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 | -------------------------------------------------------------------------------- /app/[locale]/(auth)/auth/error/page.tsx: -------------------------------------------------------------------------------- 1 | import ErrorPreview from './_components/preview' 2 | 3 | export default async function ErrorPage() { 4 | return 5 | } 6 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/[locale]/layout.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | // Global CSS 4 | // import 'uno.css' 5 | import '@/styles/global.scss' 6 | 7 | // Lib 8 | import { Analytics } from '@vercel/analytics/react' 9 | // import { useLocale } from 'next-intl' 10 | import { notFound } from 'next/navigation' 11 | // import { Inter } from 'next/font/google' 12 | 13 | import { Metadata } from 'next' 14 | import { useLocale } from 'next-intl' 15 | import { 16 | getFormatter, 17 | getNow, 18 | getTimeZone, 19 | getTranslations 20 | } from 'next-intl/server' 21 | 22 | import { UnoCSSIndicator } from '@/components/uno-css-indicator' 23 | import { Fira_Code } from 'next/font/google' 24 | import { QueryClientProvider } from '@/components/provider' 25 | import { Toaster } from '@/components/ui/toaster' 26 | import { Provider } from '@/components/ui/provider' 27 | const firaCode = Fira_Code({ 28 | variable: '--font-fira-code', 29 | subsets: ['latin', 'cyrillic'] 30 | }) 31 | 32 | export async function generateMetadata({ 33 | params: { locale } 34 | }: Omit): Promise { 35 | const t = await getTranslations({ 36 | locale: locale, 37 | namespace: 'app' 38 | }) 39 | const formatter = await getFormatter({ locale }) 40 | const now = await getNow({ locale }) 41 | const timeZone = await getTimeZone({ locale }) 42 | 43 | return { 44 | title: t('name'), 45 | description: t('description'), 46 | other: { 47 | currentYear: formatter.dateTime(now, { year: 'numeric' }), 48 | timeZone: timeZone || 'Asia/Shanghai' 49 | } 50 | } 51 | } 52 | 53 | type Props = { 54 | children: React.ReactNode 55 | params: { locale: string } 56 | } 57 | 58 | export default function LocaleLayout({ children, params }: Props) { 59 | const locale = useLocale() 60 | if (params.locale !== locale) notFound() 61 | return ( 62 | 63 | 64 | 65 | 66 | 67 | {children} 68 | 69 | 70 | 71 | 72 | 73 | 74 | ) 75 | } 76 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/api/user/authenticators/route.ts: -------------------------------------------------------------------------------- 1 | import { ResponseCode } from '@/enums/response' 2 | import { auth } from '@/libs/auth' 3 | import client from '@/libs/prisma/client' 4 | import { NextRequest, type NextResponse } from 'next/server' 5 | import { z } from 'zod' 6 | 7 | export type APIAuthenticatorResponse = { 8 | name?: string 9 | credentialID: string 10 | updatedAt: Date 11 | createdAt: Date 12 | } 13 | 14 | export const GET = auth(async function (req): Promise { 15 | if (!req.auth) return fail(ResponseCode.NotAuthorized, {}) 16 | // Check signed token 17 | try { 18 | const signedTwiceConfirmationToken = await getCookie( 19 | 'user.twice_confirmed', 20 | { 21 | signed: true 22 | } 23 | ) 24 | if (!signedTwiceConfirmationToken) throw new Error('No signed token found') 25 | const arr = signedTwiceConfirmationToken.value.split('.') 26 | if (arr.length !== 2) throw new Error('Invalid signed token') 27 | z.string().uuid().parse(arr[0]) // Check token 28 | if (Date.now() - Number(arr[1]) > 1000 * 60 * 15) { 29 | // 15 minutes 30 | throw new Error('Token expired') 31 | } 32 | } catch (e) { 33 | return fail(ResponseCode.NotAuthorized, { 34 | message: (e as Error).message 35 | }) 36 | } 37 | 38 | // Get authenticators 39 | const authenticators = await client.authenticator.findMany({ 40 | where: { userId: req.auth.user.id }, 41 | orderBy: { createdAt: 'desc' } 42 | }) 43 | return success( 44 | authenticators.map( 45 | (authenticator) => 46 | ({ 47 | name: authenticator.name, 48 | credentialID: authenticator.credentialID, 49 | updatedAt: authenticator.updatedAt, // Last used 50 | createdAt: authenticator.createdAt 51 | }) as APIAuthenticatorResponse 52 | ) 53 | ) 54 | }) as unknown as (req: NextRequest) => Promise 55 | -------------------------------------------------------------------------------- /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/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greenhat616/pastebin/6156c73b4e68a7728f43b0ebd49be9eb146a594e/app/favicon.ico -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /components/ui/avatar.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import type { GroupProps, SlotRecipeProps } from '@chakra-ui/react' 4 | import { Avatar as ChakraAvatar, Group } from '@chakra-ui/react' 5 | import * as React from 'react' 6 | 7 | type ImageProps = React.ImgHTMLAttributes 8 | 9 | export interface AvatarProps extends ChakraAvatar.RootProps { 10 | name?: string 11 | src?: string 12 | srcSet?: string 13 | loading?: ImageProps['loading'] 14 | icon?: React.ReactElement 15 | fallback?: React.ReactNode 16 | } 17 | 18 | export const Avatar = React.forwardRef( 19 | function Avatar(props, ref) { 20 | const { name, src, srcSet, loading, icon, fallback, children, ...rest } = 21 | props 22 | return ( 23 | 24 | 25 | {fallback} 26 | 27 | 28 | {children} 29 | 30 | ) 31 | } 32 | ) 33 | 34 | interface AvatarFallbackProps extends ChakraAvatar.FallbackProps { 35 | name?: string 36 | icon?: React.ReactElement 37 | } 38 | 39 | const AvatarFallback = React.forwardRef( 40 | function AvatarFallback(props, ref) { 41 | const { name, icon, children, ...rest } = props 42 | return ( 43 | 44 | {children} 45 | {name != null && children == null && <>{getInitials(name)}} 46 | {name == null && children == null && ( 47 | {icon} 48 | )} 49 | 50 | ) 51 | } 52 | ) 53 | 54 | function getInitials(name: string) { 55 | const names = name.trim().split(' ') 56 | const firstName = names[0] != null ? names[0] : '' 57 | const lastName = names.length > 1 ? names[names.length - 1] : '' 58 | return firstName && lastName 59 | ? `${firstName.charAt(0)}${lastName.charAt(0)}` 60 | : firstName.charAt(0) 61 | } 62 | 63 | interface AvatarGroupProps extends GroupProps, SlotRecipeProps<'avatar'> {} 64 | 65 | export const AvatarGroup = React.forwardRef( 66 | function AvatarGroup(props, ref) { 67 | const { size, variant, borderless, ...rest } = props 68 | return ( 69 | 70 | 71 | 72 | ) 73 | } 74 | ) 75 | -------------------------------------------------------------------------------- /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/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/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/checkbox-card.tsx: -------------------------------------------------------------------------------- 1 | import { CheckboxCard as ChakraCheckboxCard } from '@chakra-ui/react' 2 | import * as React from 'react' 3 | 4 | export interface CheckboxCardProps extends ChakraCheckboxCard.RootProps { 5 | icon?: React.ReactElement 6 | label?: React.ReactNode 7 | description?: React.ReactNode 8 | addon?: React.ReactNode 9 | indicator?: React.ReactNode | null 10 | indicatorPlacement?: 'start' | 'end' | 'inside' 11 | inputProps?: React.InputHTMLAttributes 12 | } 13 | 14 | export const CheckboxCard = React.forwardRef< 15 | HTMLInputElement, 16 | CheckboxCardProps 17 | >(function CheckboxCard(props, ref) { 18 | const { 19 | inputProps, 20 | label, 21 | description, 22 | icon, 23 | addon, 24 | indicator = , 25 | indicatorPlacement = 'end', 26 | ...rest 27 | } = props 28 | 29 | const hasContent = label || description || icon 30 | const ContentWrapper = indicator ? ChakraCheckboxCard.Content : React.Fragment 31 | 32 | return ( 33 | 34 | 35 | 36 | {indicatorPlacement === 'start' && indicator} 37 | {hasContent && ( 38 | 39 | {icon} 40 | {label && ( 41 | {label} 42 | )} 43 | {description && ( 44 | 45 | {description} 46 | 47 | )} 48 | {indicatorPlacement === 'inside' && indicator} 49 | 50 | )} 51 | {indicatorPlacement === 'end' && indicator} 52 | 53 | {addon && {addon}} 54 | 55 | ) 56 | }) 57 | 58 | export const CheckboxCardIndicator = ChakraCheckboxCard.Indicator 59 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /components/ui/color-mode.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import type { IconButtonProps } from '@chakra-ui/react' 4 | import { ClientOnly, IconButton, Skeleton } from '@chakra-ui/react' 5 | import { ThemeProvider, useTheme } from 'next-themes' 6 | import type { ThemeProviderProps } from 'next-themes' 7 | import * as React from 'react' 8 | import { LuMoon, LuSun } from 'react-icons/lu' 9 | 10 | // eslint-disable-next-line @typescript-eslint/no-empty-object-type 11 | export interface ColorModeProviderProps extends ThemeProviderProps {} 12 | 13 | export function ColorModeProvider(props: ColorModeProviderProps) { 14 | return ( 15 | 16 | ) 17 | } 18 | 19 | export function useColorMode() { 20 | const { resolvedTheme, setTheme } = useTheme() 21 | const toggleColorMode = () => { 22 | setTheme(resolvedTheme === 'light' ? 'dark' : 'light') 23 | } 24 | return { 25 | colorMode: resolvedTheme, 26 | setColorMode: setTheme, 27 | toggleColorMode 28 | } 29 | } 30 | 31 | export function useColorModeValue(light: T, dark: T) { 32 | const { colorMode } = useColorMode() 33 | return colorMode === 'light' ? light : dark 34 | } 35 | 36 | export function ColorModeIcon() { 37 | const { colorMode } = useColorMode() 38 | return colorMode === 'light' ? : 39 | } 40 | 41 | // eslint-disable-next-line @typescript-eslint/no-empty-object-type 42 | interface ColorModeButtonProps extends Omit {} 43 | 44 | export const ColorModeButton = React.forwardRef< 45 | HTMLButtonElement, 46 | ColorModeButtonProps 47 | >(function ColorModeButton(props, ref) { 48 | const { toggleColorMode } = useColorMode() 49 | return ( 50 | }> 51 | 65 | 66 | 67 | 68 | ) 69 | }) 70 | -------------------------------------------------------------------------------- /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/dialog.tsx: -------------------------------------------------------------------------------- 1 | import { Dialog as ChakraDialog, Portal } from '@chakra-ui/react' 2 | import { CloseButton } from './close-button' 3 | import * as React from 'react' 4 | 5 | interface DialogContentProps extends ChakraDialog.ContentProps { 6 | portalled?: boolean 7 | portalRef?: React.RefObject 8 | backdrop?: boolean 9 | } 10 | 11 | export const DialogContent = React.forwardRef< 12 | HTMLDivElement, 13 | DialogContentProps 14 | >(function DialogContent(props, ref) { 15 | const { 16 | children, 17 | portalled = true, 18 | portalRef, 19 | backdrop = true, 20 | ...rest 21 | } = props 22 | 23 | return ( 24 | 25 | {backdrop && } 26 | 27 | 28 | {children} 29 | 30 | 31 | 32 | ) 33 | }) 34 | 35 | export const DialogCloseTrigger = React.forwardRef< 36 | HTMLButtonElement, 37 | ChakraDialog.CloseTriggerProps 38 | >(function DialogCloseTrigger(props, ref) { 39 | return ( 40 | 47 | 48 | {props.children} 49 | 50 | 51 | ) 52 | }) 53 | 54 | export const DialogRoot = ChakraDialog.Root 55 | export const DialogFooter = ChakraDialog.Footer 56 | export const DialogHeader = ChakraDialog.Header 57 | export const DialogBody = ChakraDialog.Body 58 | export const DialogBackdrop = ChakraDialog.Backdrop 59 | export const DialogTitle = ChakraDialog.Title 60 | export const DialogDescription = ChakraDialog.Description 61 | export const DialogTrigger = ChakraDialog.Trigger 62 | export const DialogActionTrigger = ChakraDialog.ActionTrigger 63 | -------------------------------------------------------------------------------- /components/ui/drawer.tsx: -------------------------------------------------------------------------------- 1 | import { Drawer as ChakraDrawer, Portal } from '@chakra-ui/react' 2 | import { CloseButton } from './close-button' 3 | import * as React from 'react' 4 | 5 | interface DrawerContentProps extends ChakraDrawer.ContentProps { 6 | portalled?: boolean 7 | portalRef?: React.RefObject 8 | offset?: ChakraDrawer.ContentProps['padding'] 9 | } 10 | 11 | export const DrawerContent = React.forwardRef< 12 | HTMLDivElement, 13 | DrawerContentProps 14 | >(function DrawerContent(props, ref) { 15 | const { children, portalled = true, portalRef, offset, ...rest } = props 16 | return ( 17 | 18 | 19 | 20 | {children} 21 | 22 | 23 | 24 | ) 25 | }) 26 | 27 | export const DrawerCloseTrigger = React.forwardRef< 28 | HTMLButtonElement, 29 | ChakraDrawer.CloseTriggerProps 30 | >(function DrawerCloseTrigger(props, ref) { 31 | return ( 32 | 39 | 40 | 41 | ) 42 | }) 43 | 44 | export const DrawerTrigger = ChakraDrawer.Trigger 45 | export const DrawerRoot = ChakraDrawer.Root 46 | export const DrawerFooter = ChakraDrawer.Footer 47 | export const DrawerHeader = ChakraDrawer.Header 48 | export const DrawerBody = ChakraDrawer.Body 49 | export const DrawerBackdrop = ChakraDrawer.Backdrop 50 | export const DrawerDescription = ChakraDrawer.Description 51 | export const DrawerTitle = ChakraDrawer.Title 52 | export const DrawerActionTrigger = ChakraDrawer.ActionTrigger 53 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /components/ui/input-group.tsx: -------------------------------------------------------------------------------- 1 | import type { BoxProps, InputElementProps } from '@chakra-ui/react' 2 | import { Group, InputElement } from '@chakra-ui/react' 3 | import * as React from 'react' 4 | 5 | export interface InputGroupProps extends BoxProps { 6 | startElementProps?: InputElementProps 7 | endElementProps?: InputElementProps 8 | startElement?: React.ReactNode 9 | endElement?: React.ReactNode 10 | children: React.ReactElement 11 | startOffset?: InputElementProps['paddingStart'] 12 | endOffset?: InputElementProps['paddingEnd'] 13 | } 14 | 15 | export const InputGroup = React.forwardRef( 16 | function InputGroup(props, ref) { 17 | const { 18 | startElement, 19 | startElementProps, 20 | endElement, 21 | endElementProps, 22 | children, 23 | startOffset = '6px', 24 | endOffset = '6px', 25 | ...rest 26 | } = props 27 | 28 | const child = 29 | // @ts-expect-error we cannot handle the upstream error 30 | React.Children.only>(children) 31 | 32 | return ( 33 | 34 | {startElement && ( 35 | 36 | {startElement} 37 | 38 | )} 39 | {React.cloneElement(child, { 40 | ...(startElement && { 41 | ps: `calc(var(--input-height) - ${startOffset})` 42 | }), 43 | ...(endElement && { pe: `calc(var(--input-height) - ${endOffset})` }), 44 | // @ts-expect-error we cannot handle the upstream error 45 | ...children.props 46 | })} 47 | {endElement && ( 48 | 49 | {endElement} 50 | 51 | )} 52 | 53 | ) 54 | } 55 | ) 56 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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/popover.tsx: -------------------------------------------------------------------------------- 1 | import { Popover as ChakraPopover, Portal } from '@chakra-ui/react' 2 | import { CloseButton } from './close-button' 3 | import * as React from 'react' 4 | 5 | interface PopoverContentProps extends ChakraPopover.ContentProps { 6 | portalled?: boolean 7 | portalRef?: React.RefObject 8 | } 9 | 10 | export const PopoverContent = React.forwardRef< 11 | HTMLDivElement, 12 | PopoverContentProps 13 | >(function PopoverContent(props, ref) { 14 | const { portalled = true, portalRef, ...rest } = props 15 | return ( 16 | 17 | 18 | 19 | 20 | 21 | ) 22 | }) 23 | 24 | export const PopoverArrow = React.forwardRef< 25 | HTMLDivElement, 26 | ChakraPopover.ArrowProps 27 | >(function PopoverArrow(props, ref) { 28 | return ( 29 | 30 | 31 | 32 | ) 33 | }) 34 | 35 | export const PopoverCloseTrigger = React.forwardRef< 36 | HTMLButtonElement, 37 | ChakraPopover.CloseTriggerProps 38 | >(function PopoverCloseTrigger(props, ref) { 39 | return ( 40 | 48 | 49 | 50 | ) 51 | }) 52 | 53 | export const PopoverTitle = ChakraPopover.Title 54 | export const PopoverDescription = ChakraPopover.Description 55 | export const PopoverFooter = ChakraPopover.Footer 56 | export const PopoverHeader = ChakraPopover.Header 57 | export const PopoverRoot = ChakraPopover.Root 58 | export const PopoverBody = ChakraPopover.Body 59 | export const PopoverTrigger = ChakraPopover.Trigger 60 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /components/ui/radio-card.tsx: -------------------------------------------------------------------------------- 1 | import { RadioCard } from '@chakra-ui/react' 2 | import * as React from 'react' 3 | 4 | interface RadioCardItemProps extends RadioCard.ItemProps { 5 | icon?: React.ReactElement 6 | label?: React.ReactNode 7 | description?: React.ReactNode 8 | addon?: React.ReactNode 9 | indicator?: React.ReactNode | null 10 | indicatorPlacement?: 'start' | 'end' | 'inside' 11 | inputProps?: React.InputHTMLAttributes 12 | } 13 | 14 | export const RadioCardItem = React.forwardRef< 15 | HTMLInputElement, 16 | RadioCardItemProps 17 | >(function RadioCardItem(props, ref) { 18 | const { 19 | inputProps, 20 | label, 21 | description, 22 | addon, 23 | icon, 24 | indicator = , 25 | indicatorPlacement = 'end', 26 | ...rest 27 | } = props 28 | 29 | const hasContent = label || description || icon 30 | const ContentWrapper = indicator ? RadioCard.ItemContent : React.Fragment 31 | 32 | return ( 33 | 34 | 35 | 36 | {indicatorPlacement === 'start' && indicator} 37 | {hasContent && ( 38 | 39 | {icon} 40 | {label && {label}} 41 | {description && ( 42 | 43 | {description} 44 | 45 | )} 46 | {indicatorPlacement === 'inside' && indicator} 47 | 48 | )} 49 | {indicatorPlacement === 'end' && indicator} 50 | 51 | {addon && {addon}} 52 | 53 | ) 54 | }) 55 | 56 | export const RadioCardRoot = RadioCard.Root 57 | export const RadioCardLabel = RadioCard.Label 58 | export const RadioCardItemIndicator = RadioCard.ItemIndicator 59 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /components/ui/stat.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Badge, 3 | type BadgeProps, 4 | Stat as ChakraStat, 5 | FormatNumber 6 | } from '@chakra-ui/react' 7 | import { InfoTip } from './toggle-tip' 8 | import * as React from 'react' 9 | 10 | interface StatLabelProps extends ChakraStat.LabelProps { 11 | info?: React.ReactNode 12 | } 13 | 14 | export const StatLabel = React.forwardRef( 15 | function StatLabel(props, ref) { 16 | const { info, children, ...rest } = props 17 | return ( 18 | 19 | {children} 20 | {info && {info}} 21 | 22 | ) 23 | } 24 | ) 25 | 26 | interface StatValueTextProps extends ChakraStat.ValueTextProps { 27 | value?: number 28 | formatOptions?: Intl.NumberFormatOptions 29 | } 30 | 31 | export const StatValueText = React.forwardRef< 32 | HTMLDivElement, 33 | StatValueTextProps 34 | >(function StatValueText(props, ref) { 35 | const { value, formatOptions, children, ...rest } = props 36 | return ( 37 | 38 | {children || 39 | (value != null && )} 40 | 41 | ) 42 | }) 43 | 44 | export const StatUpTrend = React.forwardRef( 45 | function StatUpTrend(props, ref) { 46 | return ( 47 | 48 | 49 | {props.children} 50 | 51 | ) 52 | } 53 | ) 54 | 55 | export const StatDownTrend = React.forwardRef( 56 | function StatDownTrend(props, ref) { 57 | return ( 58 | 59 | 60 | {props.children} 61 | 62 | ) 63 | } 64 | ) 65 | 66 | export const StatRoot = ChakraStat.Root 67 | export const StatHelpText = ChakraStat.HelpText 68 | export const StatValueUnit = ChakraStat.ValueUnit 69 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /components/ui/steps.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Steps as ChakraSteps } from '@chakra-ui/react' 2 | import * as React from 'react' 3 | import { LuCheck } from 'react-icons/lu' 4 | 5 | interface StepInfoProps { 6 | title?: React.ReactNode 7 | description?: React.ReactNode 8 | } 9 | 10 | export interface StepsItemProps 11 | extends Omit, 12 | StepInfoProps { 13 | completedIcon?: React.ReactNode 14 | icon?: React.ReactNode 15 | } 16 | 17 | export const StepsItem = React.forwardRef( 18 | function StepsItem(props, ref) { 19 | const { title, description, completedIcon, icon, ...rest } = props 20 | return ( 21 | 22 | 23 | 24 | } 26 | incomplete={icon || } 27 | /> 28 | 29 | 30 | 31 | 32 | 33 | ) 34 | } 35 | ) 36 | 37 | const StepInfo = (props: StepInfoProps) => { 38 | const { title, description } = props 39 | 40 | if (title && description) { 41 | return ( 42 | 43 | {title} 44 | {description} 45 | 46 | ) 47 | } 48 | 49 | return ( 50 | <> 51 | {title && {title}} 52 | {description && ( 53 | {description} 54 | )} 55 | 56 | ) 57 | } 58 | 59 | interface StepsIndicatorProps { 60 | completedIcon: React.ReactNode 61 | icon?: React.ReactNode 62 | } 63 | 64 | export const StepsIndicator = React.forwardRef< 65 | HTMLDivElement, 66 | StepsIndicatorProps 67 | >(function StepsIndicator(props, ref) { 68 | const { icon = , completedIcon } = props 69 | return ( 70 | 71 | 72 | 73 | ) 74 | }) 75 | 76 | export const StepsList = ChakraSteps.List 77 | export const StepsRoot = ChakraSteps.Root 78 | export const StepsContent = ChakraSteps.Content 79 | export const StepsCompletedContent = ChakraSteps.CompletedContent 80 | 81 | export const StepsNextTrigger = ChakraSteps.NextTrigger 82 | export const StepsPrevTrigger = ChakraSteps.PrevTrigger 83 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /components/ui/toggle-tip.tsx: -------------------------------------------------------------------------------- 1 | import { Popover as ChakraPopover, IconButton, Portal } from '@chakra-ui/react' 2 | import * as React from 'react' 3 | import { HiOutlineInformationCircle } from 'react-icons/hi' 4 | 5 | export interface ToggleTipProps extends ChakraPopover.RootProps { 6 | showArrow?: boolean 7 | portalled?: boolean 8 | portalRef?: React.RefObject 9 | content?: React.ReactNode 10 | } 11 | 12 | export const ToggleTip = React.forwardRef( 13 | function ToggleTip(props, ref) { 14 | const { 15 | showArrow, 16 | children, 17 | portalled = true, 18 | content, 19 | portalRef, 20 | ...rest 21 | } = props 22 | 23 | return ( 24 | 28 | {children} 29 | 30 | 31 | 39 | {showArrow && ( 40 | 41 | 42 | 43 | )} 44 | {content} 45 | 46 | 47 | 48 | 49 | ) 50 | } 51 | ) 52 | 53 | export const InfoTip = React.forwardRef< 54 | HTMLDivElement, 55 | Partial 56 | >(function InfoTip(props, ref) { 57 | const { children, ...rest } = props 58 | return ( 59 | 60 | 66 | 67 | 68 | 69 | ) 70 | }) 71 | -------------------------------------------------------------------------------- /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 |