chore: initialize project structure

This commit is contained in:
Tianen Pang
2025-04-07 21:38:31 +08:00
parent 8bf946b185
commit 33233b9753
3155 changed files with 7253 additions and 264021 deletions
+6 -7
View File
@@ -2,7 +2,9 @@
"$schema": "https://unpkg.com/@changesets/config@2.3.0/schema.json",
"changelog": [
"@changesets/changelog-github",
{ "repo": "heroui-inc/heroui" }
{
"repo": "heroui-inc/heroui"
}
],
"commit": false,
"fixed": [],
@@ -10,11 +12,8 @@
"access": "public",
"baseBranch": "canary",
"updateInternalDependencies": "patch",
"ignore": ["@heroui/docs", "@heroui/storybook"],
"___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH": {
"onlyUpdatePeerDependentsWhenOutOfRange": true
},
"ignore": [
"@heroui/docs",
"@heroui/storybook"
]
}
}
}
-22
View File
@@ -1,22 +0,0 @@
language: "en"
early_access: false
reviews:
high_level_summary: true
poem: false
review_status: true
collapse_walkthrough: false
auto_review:
enabled: true
ignore_title_keywords:
- "WIP"
- "DO NOT MERGE"
- 'ci(changesets)'
drafts: false
base_branches:
- "main"
- "canary"
- "fix/.*"
- "chore/.*"
- "feat/.*"
chat:
auto_reply: true
+13 -7
View File
@@ -1,9 +1,15 @@
root = true
root=true
[*]
indent_size = 2
max_line_length = 100
insert_final_newline = true
trim_trailing_whitespace = true
charset = utf-8
indent_style = space
tab_width=2
indent_size=2
charset=utf-8
end_of_line=unset
indent_style=space
max_line_length=100
insert_final_newline=true
trim_trailing_whitespace=true
[*.md]
max_line_length=off
trim_trailing_whitespace=false
-7
View File
@@ -1,7 +0,0 @@
SKIP_PREFLIGHT_CHECK=true
## Algolia client app id
ALGOLIA_APP_ID=
## Algolia client search only api key
ALGOLIA_SEARCH_API_KEY=
## Algolia admin api key
ALGOLIA_ADMIN_API_KEY=
-25
View File
@@ -1,25 +0,0 @@
.now/*
.next/*
*.css
.changeset
dist
esm/*
public/*
tests/*
scripts/*
*.config.js
.DS_Store
node_modules
coverage
.next
build
!.storybook
/**/.storybook/**
!.commitlintrc.cjs
!.lintstagedrc.cjs
!jest.config.js
!plopfile.js
!react-shim.js
!tsup.config.ts
apps/docs/preinstall.js
apps/docs/next-redirect.js
-93
View File
@@ -1,93 +0,0 @@
{
"$schema": "https://json.schemastore.org/eslintrc.json",
"env": {
"browser": false,
"es2021": true,
"node": true
},
"extends": [
"plugin:react/recommended",
"plugin:prettier/recommended",
"plugin:react-hooks/recommended",
"plugin:jsx-a11y/recommended"
],
"plugins": ["react", "unused-imports", "import", "@typescript-eslint", "jsx-a11y", "prettier"],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaFeatures": {
"jsx": true
},
"ecmaVersion": 12,
"sourceType": "module"
},
"settings": {
"react": {
"version": "detect"
}
},
"rules": {
"no-console": "warn",
"react/prop-types": "off",
"react/jsx-uses-react": "off",
"react/react-in-jsx-scope": "off",
"react-hooks/exhaustive-deps": "off",
"jsx-a11y/click-events-have-key-events": "warn",
"jsx-a11y/interactive-supports-focus": "warn",
"prettier/prettier": "warn",
"no-unused-vars": "off",
"unused-imports/no-unused-vars": "off",
"unused-imports/no-unused-imports": "warn",
"@typescript-eslint/no-unused-vars": [
"warn",
{
"args": "after-used",
"ignoreRestSiblings": false,
"argsIgnorePattern": "^_.*?$"
}
],
"import/order": [
"warn",
{
"groups": [
"type",
"builtin",
"object",
"external",
"internal",
"parent",
"sibling",
"index"
],
"pathGroups": [
{
"pattern": "~/**",
"group": "external",
"position": "after"
}
],
"newlines-between": "always"
}
],
"react/self-closing-comp": "warn",
"react/jsx-sort-props": [
"warn",
{
"callbacksLast": true,
"shorthandFirst": true,
"noSortAlphabetically": false,
"reservedFirst": true
}
],
"padding-line-between-statements": [
"warn",
{"blankLine": "always", "prev": "*", "next": "return"},
{"blankLine": "always", "prev": ["const", "let", "var"], "next": "*"},
{
"blankLine": "any",
"prev": ["const", "let", "var"],
"next": ["const", "let", "var"]
}
],
"import/consistent-type-specifier-style": ["error", "prefer-top-level"]
}
}
+24 -7
View File
@@ -4,15 +4,35 @@ description: "Sets up Node.js and runs install"
runs:
using: composite
steps:
- name: Install dependencies
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
run_install: false
- name: Setup Node.js
- name: Setup node
uses: actions/setup-node@v4
with:
node-version-file: ".nvmrc"
cache: 'pnpm'
check-latest: true
node-version-file: '.nvmrc'
registry-url: "https://registry.npmjs.org"
cache: "pnpm"
- name: Get pnpm store directory
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- name: Setup pnpm cache
uses: actions/cache@v4
with:
path: ${{ env.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install dependencies
shell: bash
run: pnpm install --frozen-lockfile
- name: Setup Git User
shell: bash
@@ -20,6 +40,3 @@ runs:
git config --global user.email "jrgarciadev@gmail.com"
git config --global user.name "Junior Garcia"
- name: Install dependencies
shell: bash
run: pnpm install
+52 -44
View File
@@ -1,66 +1,74 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# cache
.turbo
.cache
.swc
# dependencies
node_modules
/.pnp
.pnp
.pnp.js
# testing
/coverage
coverage
__snapshots__
# next.js
/.next/
.next/
/out/
.next
out
.vercel
# production
/build
dist/
storybook-static
/packages/storybook/public/tailwind.css
# misc
.DS_Store
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
.env.production
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
build
dist
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.yarn-integrity
# typescript
*.tsbuildinfo
next-env.d.ts
# envs
.env
.env.local
.env.development
.env.development.local
.env.test
.env.test.local
.env.production
.env.production.local
.env*
.env*.local
.env*.local
.env*.staging
.env*.production
!.env.example
# idea
.idea
.now
dist
esm
examples/**/yarn.lock
examples/**/out
examples/**/.next
packages/**/*.backup
packages/**/*.backup.ts
.vercel
# temporary
.temp
# ignore sitemap
apps/**/sitemap.xml
apps/**/sitemap-0.xml
# turbo
.turbo
packages/**/.turbo
# fumadocs
.source
# content layer
.contentlayer
.content-collections
# sitemaps
sitemap*.xml
# vite
vite.config.ts.timestamp-*
# storybook
*storybook.log
storybook-static
# misc
.DS_Store
*.pem
+1 -4
View File
@@ -1,4 +1 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
pnpm commitlint --config .commitlintrc.cjs --edit ${1}
pnpm commitlint --config commitlint.config.mjs --edit ${1}
+5 -3
View File
@@ -1,5 +1,7 @@
#!/usr/bin/env sh
huskyDir=$(dirname -- "$0")
. "$huskyDir/_/husky.sh"
. "$huskyDir/scripts/update-dep"
husky_dir=$(dirname -- "$0")
. "$husky_dir/_/husky.sh"
. "$husky_dir/scripts/pnpm-install"
+5 -3
View File
@@ -1,5 +1,7 @@
#!/usr/bin/env sh
huskyDir=$(dirname -- "$0")
. "$huskyDir/_/husky.sh"
. "$huskyDir/scripts/update-dep"
husky_dir=$(dirname -- "$0")
. "$husky_dir/_/husky.sh"
. "$husky_dir/scripts/pnpm-install"
+1 -9
View File
@@ -1,9 +1 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
# Avoid excessive outputs
if [ -t 2 ]; then
exec >/dev/tty 2>&1
fi
pnpm lint-staged -c .lintstagedrc.cjs
pnpm lint-staged -c lint-staged.config.mjs
@@ -1,4 +1,5 @@
#!/usr/bin/env sh
changed_files="$(git diff-tree -r --name-only --no-commit-id ORIG_HEAD HEAD)"
check_run() {
@@ -8,4 +9,4 @@ check_run() {
fi
}
check_run pnpm-lock.yaml "pnpm install --color"
check_run "pnpm-lock.yaml" "pnpm install --color"
-26
View File
@@ -1,26 +0,0 @@
const {relative} = require("path");
const {ESLint} = require("eslint");
const removeIgnoredFiles = async (files) => {
const cwd = process.cwd();
const eslint = new ESLint();
const relativePaths = files.map((file) => relative(cwd, file));
const isIgnored = await Promise.all(relativePaths.map((file) => eslint.isPathIgnored(file)));
const filteredFiles = files.filter((_, i) => !isIgnored[i]);
return filteredFiles.join(" ");
};
module.exports = {
"**/*.{js,ts,jsx,tsx}": async (files) => {
const filesToLint = await removeIgnoredFiles(files);
return [`eslint -c .eslintrc.json --max-warnings=0 --fix ${filesToLint}`];
},
"**/*.css": async (files) => {
const filesToLint = await removeIgnoredFiles(files);
return [`prettier --config .prettierrc.json --ignore-path --write ${filesToLint}`];
},
};
+4 -5
View File
@@ -1,6 +1,5 @@
strict-peer-dependencies=false
auto-install-peers=true
enable-pre-post-scripts=true
public-hoist-pattern[]=*tailwind-variants*
public-hoist-pattern[]=*framer-motion*
public-hoist-pattern[]=*@react-aria/interactions*
public-hoist-pattern[]=*@heroui/theme*
lockfile=true
save-exact=true
strict-peer-dependencies=false
+1 -1
View File
@@ -1 +1 @@
v20.16.0
v22.14.0
-16
View File
@@ -1,16 +0,0 @@
dist
node_modules
plop
coverage
.changeset
.next
build
scripts
pnpm-lock.yaml
!.storybook
!.commitlintrc.cjs
!.lintstagedrc.cjs
!jest.config.js
!plopfile.js
!react-shim.js
!tsup.config.ts
-12
View File
@@ -1,12 +0,0 @@
{
"$schema": "https://json.schemastore.org/prettierrc.json",
"tabWidth": 2,
"printWidth": 100,
"semi": true,
"useTabs": false,
"singleQuote": false,
"bracketSpacing": false,
"endOfLine": "auto",
"arrowParens": "always",
"trailingComma": "all"
}
+9
View File
@@ -0,0 +1,9 @@
{
"recommendations": [
"bradlc.vscode-tailwindcss",
"editorconfig.editorconfig",
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"unifiedjs.vscode-mdx"
]
}
+31 -3
View File
@@ -1,14 +1,42 @@
{
"npm.packageManager": "pnpm",
"css.lint.unknownAtRules": "ignore",
"scss.lint.unknownAtRules": "ignore",
"typescript.tsdk": "node_modules/typescript/lib",
"typescript.enablePromptUseWorkspaceTsdk": true,
"eslint.workingDirectories": [{"mode": "auto"}],
"editor.formatOnSave": false,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
},
"tailwindCSS.experimental.configFile": {
"packages/storybook/tailwind.config.js": ["packages/core/theme/**/*", "packages/components/**/*"],
"apps/docs/tailwind.config.js": "apps/docs/**/*"
"files.associations": {
"**/.vscode/*.json": "jsonc"
},
"[html]": {
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[css]": {
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[scss]": {
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[json]": {
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[jsonc]": {
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"tailwindCSS.classAttributes": ["className", "classNames", "clsx", "cn"],
"tailwindCSS.experimental.classRegex": [
["([\"'`][^\"'`]*.*?[\"'`])", "[\"'`]([^\"'`]*).*?[\"'`]"]
],
"tailwindCSS.experimental.configFile": {
"packages/storybook/styles/globals.css": ["packages/react/**", "packages/storybook/**"]
}
}
-24
View File
@@ -1,24 +0,0 @@
## use cache for Github requests like "tags"
USE_CACHE=true/false
## Algolia client app id
NEXT_PUBLIC_ALGOLIA_APP_ID=
## Algolia client search only api key
NEXT_PUBLIC_ALGOLIA_SEARCH_API_KEY=
# Vercel Env (is used for skipping typescript check)
IS_VERCEL_ENV=true/false
IGNORE_BUILD_CHECKS=true/false
IS_PREVIEW=true/false
ANALYZE_BUNDLE=true/false
# Vercel preview env (is used for taking the docs directly from the project files)
NEXT_PUBLIC_PREVIEW=true/false
## Featurebase
NEXT_PUBLIC_FB_FEEDBACK_ORG=
NEXT_PUBLIC_FB_FEEDBACK_URL=
# PostHog
NEXT_PUBLIC_POSTHOG_KEY=your-posthog-key
NEXT_PUBLIC_POSTHOG_HOST=your-posthog-host
-27
View File
@@ -1,27 +0,0 @@
{
"extends": ["../../.eslintrc.json"],
"ignorePatterns": ["!**/*"],
"overrides": [
{
"files": ["*.ts", "*.tsx"],
"parserOptions": {
"project": ["apps/docs/tsconfig(.*)?.json"],
"ecmaFeatures": {
"jsx": true
}
},
"rules": {
"react/no-unknown-property": [
2,
{
"ignore": ["jsx", "global"]
}
]
}
},
{
"files": ["*.ts", "*.tsx"],
"rules": {}
}
]
}
-42
View File
@@ -1,42 +0,0 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
sitemap.xml
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env
.env*.local
.dev.vars
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
# cloudflare
.wrangler
/.open-next/
-332
View File
@@ -1,332 +0,0 @@
[
{
"MemberId": 275354,
"createdAt": "2022-02-18 11:57",
"type": "USER",
"role": "BACKER",
"tier": "Backer 🖤",
"isActive": true,
"totalAmountDonated": 5,
"currency": "USD",
"lastTransactionAt": "2022-02-18 11:57",
"lastTransactionAmount": 5,
"profile": "https://opencollective.com/guest-7dc076fb",
"name": "jorge",
"company": null,
"description": null,
"image": "/sponsors/undefined",
"email": null,
"twitter": null,
"github": null,
"website": null
},
{
"MemberId": 277819,
"createdAt": "2022-03-01 09:13",
"type": "USER",
"role": "BACKER",
"isActive": true,
"totalAmountDonated": 10,
"currency": "USD",
"lastTransactionAt": "2022-03-01 09:13",
"lastTransactionAmount": 10,
"profile": "https://opencollective.com/dhananjay-senday",
"name": "Dhananjay Senday",
"company": null,
"description": null,
"image": "/sponsors/undefined",
"email": null,
"twitter": null,
"github": null,
"website": null
},
{
"MemberId": 292380,
"createdAt": "2022-04-22 07:47",
"type": "USER",
"role": "BACKER",
"tier": "Backer 🖤",
"isActive": true,
"totalAmountDonated": 10,
"currency": "USD",
"lastTransactionAt": "2022-04-22 07:47",
"lastTransactionAmount": 10,
"profile": "https://opencollective.com/manuel5",
"name": "manuel-rw",
"company": null,
"description": null,
"image": "/sponsors/292380.jpg",
"email": null,
"twitter": null,
"github": null,
"website": null
},
{
"MemberId": 327844,
"createdAt": "2022-08-26 21:32",
"type": "ORGANIZATION",
"role": "BACKER",
"tier": "Gold Sponsor 🥇",
"isActive": true,
"totalAmountDonated": 200,
"currency": "USD",
"lastTransactionAt": "2022-12-22 07:19",
"lastTransactionAmount": 100,
"profile": "https://opencollective.com/lowdefy",
"name": "Lowdefy",
"company": null,
"description": "Create custom web apps in minutes",
"image": "/sponsors/327844.jpg",
"twitter": "https://twitter.com/lowdefy",
"github": "https://github.com/lowdefy",
"website": "https://lowdefy.com"
},
{
"MemberId": 347048,
"createdAt": "2022-10-30 06:25",
"type": "USER",
"role": "BACKER",
"tier": "Backer 🖤",
"isActive": true,
"totalAmountDonated": 1,
"currency": "USD",
"lastTransactionAt": "2022-10-30 06:25",
"lastTransactionAmount": 1,
"profile": "https://opencollective.com/william-frank-monroy-mamani",
"name": "William Frank Monroy Mamani",
"company": null,
"description": null,
"image": "/sponsors/347048.jpg",
"email": null,
"twitter": null,
"github": null,
"website": null
},
{
"MemberId": 374896,
"createdAt": "2022-12-22 05:28",
"type": "ORGANIZATION",
"role": "BACKER",
"tier": "bronze sponsor 🥉",
"isActive": true,
"totalAmountDonated": 70,
"currency": "USD",
"lastTransactionAt": "2023-07-02 09:04",
"lastTransactionAmount": 10,
"profile": "https://opencollective.com/chartbrew",
"name": "Chartbrew",
"company": null,
"description": "Open source visualization and reporting platform",
"image": "/sponsors/374896.jpg",
"twitter": null,
"github": null,
"website": "https://chartbrew.com"
},
{
"MemberId": 375034,
"createdAt": "2022-12-22 13:16",
"type": "USER",
"role": "BACKER",
"isActive": true,
"totalAmountDonated": 50,
"currency": "USD",
"lastTransactionAt": "2022-12-22 13:16",
"lastTransactionAmount": 50,
"profile": "https://opencollective.com/jan-wagebach",
"name": "PRISMA European Capacity Platform GmbH",
"company": "PRISMA European Capacity Platform GmbH",
"description": null,
"image": "/sponsors/375034.jpg",
"email": null,
"twitter": null,
"github": null,
"website": null
},
{
"MemberId": 395990,
"createdAt": "2023-02-11 13:12",
"type": "ORGANIZATION",
"role": "BACKER",
"tier": "Gold Sponsor 🥇",
"isActive": true,
"totalAmountDonated": 100,
"currency": "USD",
"lastTransactionAt": "2023-02-11 13:12",
"lastTransactionAmount": 100,
"profile": "https://opencollective.com/likn",
"name": "LIKN",
"company": null,
"description": "LIKN is a powerful, but simple Web3 component and protocol. Connect Web2 content to Web3, and solely mint content NFTs by URL. Share and Trade in different platforms and marketplaces. OpenAI empowers LIKN which can automatically generate NFT metadata. ",
"image": "/sponsors/395990.jpg",
"twitter": "https://twitter.com/oxlikn",
"github": "https://github.com/0xLIKN",
"website": "https://www.likn.co/?ref=opencollective"
},
{
"MemberId": 404415,
"createdAt": "2023-03-01 16:22",
"type": "USER",
"role": "BACKER",
"tier": "Backer 🖤",
"isActive": true,
"totalAmountDonated": 1,
"currency": "USD",
"lastTransactionAt": "2023-03-01 16:22",
"lastTransactionAmount": 1,
"profile": "https://opencollective.com/rharison-lucas-moreira-abreu",
"name": "Rharison Lucas Moreira Abreu",
"company": null,
"description": null,
"image": "/sponsors/404415.jpg",
"email": null,
"twitter": null,
"github": null,
"website": null
},
{
"MemberId": 407510,
"createdAt": "2023-03-07 22:27",
"type": "USER",
"role": "BACKER",
"tier": "Backer 🖤",
"isActive": true,
"totalAmountDonated": 1,
"currency": "USD",
"lastTransactionAt": "2023-03-07 22:27",
"lastTransactionAmount": 1,
"profile": "https://opencollective.com/higherror",
"name": "HighError",
"company": null,
"description": null,
"image": "/sponsors/407510.jpg",
"email": null,
"twitter": null,
"github": "https://github.com/HighError",
"website": null
},
{
"MemberId": 409558,
"createdAt": "2023-03-12 21:41",
"type": "USER",
"role": "BACKER",
"tier": "Backer 🖤",
"isActive": true,
"totalAmountDonated": 1,
"currency": "USD",
"lastTransactionAt": "2023-03-12 21:41",
"lastTransactionAmount": 1,
"profile": "https://opencollective.com/guest-6133a45c",
"name": "Kristof",
"company": null,
"description": null,
"image": "/sponsors/undefined",
"email": null,
"twitter": null,
"github": null,
"website": null
},
{
"MemberId": 438158,
"createdAt": "2023-05-31 22:44",
"type": "USER",
"role": "BACKER",
"tier": "Backer 🖤",
"isActive": true,
"totalAmountDonated": 5,
"currency": "USD",
"lastTransactionAt": "2023-05-31 22:44",
"lastTransactionAmount": 5,
"profile": "https://opencollective.com/elektrnik",
"name": "ELEKTRNIK",
"company": null,
"description": null,
"image": "/sponsors/438158.jpg",
"email": null,
"twitter": null,
"github": null,
"website": null
},
{
"MemberId": 438877,
"createdAt": "2023-06-02 17:56",
"type": "USER",
"role": "BACKER",
"tier": "Backer 🖤",
"isActive": true,
"totalAmountDonated": 1,
"currency": "USD",
"lastTransactionAt": "2023-06-02 17:56",
"lastTransactionAmount": 1,
"profile": "https://opencollective.com/guest-425370ea",
"name": "Gabriel Jiménez",
"company": null,
"description": null,
"image": "/sponsors/undefined",
"email": null,
"twitter": null,
"github": null,
"website": null
},
{
"MemberId": 439182,
"createdAt": "2023-06-03 15:15",
"type": "USER",
"role": "BACKER",
"tier": "Backer 🖤",
"isActive": true,
"totalAmountDonated": 1,
"currency": "USD",
"lastTransactionAt": "2023-06-03 15:15",
"lastTransactionAmount": 1,
"profile": "https://opencollective.com/jay-thanawut1",
"name": "Jay Thanawut",
"company": null,
"description": null,
"image": "/sponsors/439182.jpg",
"email": null,
"twitter": null,
"github": null,
"website": null
},
{
"MemberId": 442878,
"createdAt": "2023-06-13 13:51",
"type": "USER",
"role": "BACKER",
"tier": "Backer 🖤",
"isActive": true,
"totalAmountDonated": 5,
"currency": "USD",
"lastTransactionAt": "2023-06-13 13:51",
"lastTransactionAmount": 5,
"profile": "https://opencollective.com/encurme",
"name": "EncurMe",
"company": "EncurMe",
"description": null,
"image": "/sponsors/442878.jpg",
"email": null,
"twitter": null,
"github": null,
"website": "https://encur.me/"
},
{
"MemberId": 571230,
"createdAt": "2024-06-01 18:47",
"type": "ORGANIZATION",
"role": "BACKER",
"tier": "Gold Sponsor 🥇",
"isActive": true,
"totalAmountDonated": 100,
"currency": "USD",
"lastTransactionAt": "2024-06-01 18:47",
"lastTransactionAmount": 100,
"profile": "https://opencollective.com/coderabbit",
"name": "CodeRabbit",
"company": null,
"description": "CodeRabbit is an AI-driven context-aware code reviewer that provides line-by-line feedback and smart chat. ",
"image": "/sponsors/571230.jpg",
"twitter": null,
"github": null,
"website": "https://coderabbit.ai"
}
]
-21
View File
@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2020 Next UI Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
-36
View File
@@ -1,36 +0,0 @@
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
[http://localhost:3000/api/hello](http://localhost:3000/api/hello) is an endpoint that uses [Route Handlers](https://beta.nextjs.org/docs/routing/route-handlers). This endpoint can be edited in `app/api/hello/route.ts`.
This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
-132
View File
@@ -1,132 +0,0 @@
import type {Metadata} from "next";
import {notFound} from "next/navigation";
import {allBlogPosts} from "contentlayer2/generated";
import {Link, User} from "@heroui/react";
import {format, parseISO} from "date-fns";
import NextLink from "next/link";
import {Balancer} from "react-wrap-balancer";
import {__DEV__, __PREVIEW__} from "@/utils";
import {MDXContent} from "@/components/mdx-content";
import {siteConfig} from "@/config/site";
import {Route} from "@/libs/docs/page";
import {ChevronRightLinearIcon} from "@/components/icons";
interface BlogPostProps {
params: {
slug: string;
};
}
const isDraftVisible = __DEV__ || __PREVIEW__;
async function getBlogPostFromParams({params}: BlogPostProps) {
const slug = params.slug || "";
const post = allBlogPosts.find((post) => post.slugAsParams === slug);
if (!post) {
null;
}
const currentRoute: Route = {
key: post?._id,
title: post?.title,
path: `/${post?._raw?.sourceFilePath}`,
};
return {post, currentRoute};
}
export async function generateMetadata({params}: BlogPostProps): Promise<Metadata> {
const {post} = await getBlogPostFromParams({params});
if (!post) {
return {};
}
return {
title: post.title,
description: post.description,
openGraph: {
title: post.title,
description: post.description,
type: "article",
url: post.url,
images: [
{
url: post.imageAsParams || siteConfig.ogImage,
width: 1200,
height: 630,
alt: post.title || siteConfig.name,
},
],
},
twitter: {
card: "summary_large_image",
title: post.title,
description: post.description,
images: [siteConfig.ogImage],
creator: siteConfig.creator,
},
};
}
export async function generateStaticParams(): Promise<BlogPostProps["params"][]> {
return allBlogPosts.map((doc) => ({
slug: doc.slugAsParams,
}));
}
export default async function DocPage({params}: BlogPostProps) {
const {post} = await getBlogPostFromParams({params});
if (!post || (post.draft && !isDraftVisible)) {
notFound();
}
return (
<div className="w-full mt-12 flex flex-col justify-start items-center prose prose-neutral">
<div className="w-full max-w-4xl">
<Link
isBlock
as={NextLink}
className="mb-8 -ml-3 text-default-500 hover:text-default-900"
color="foreground"
href="/blog"
size="sm"
>
<ChevronRightLinearIcon className="rotate-180 inline-block mr-1" size={15} />
Back to blog
</Link>
<time className="block text-small mb-2 text-default-500" dateTime={post.date}>
{format(parseISO(post.date), "LLLL d, yyyy")}
</time>
<div className="mb-3 flex w-full flex-col items-start">
<User
isExternal
as={Link}
avatarProps={{
className: "w-9 h-9 text-large",
src: post.author?.avatar,
}}
className="hover:opacity-100"
classNames={{
base: "-ml-2 px-2 py-1.5 hover:bg-default-100 dark:hover:bg-default-50 cursor-pointer transition-colors",
name: "text-foreground",
}}
description={post.author?.username}
href={post.author?.link}
name={post.author?.name}
/>
</div>
<h1 className="mb-2 font-bold text-4xl">
<Balancer>{post.title}</Balancer>
<strong className="text-default-300">{post?.draft && " (Draft)"}</strong>
</h1>
<MDXContent code={post.body.code} />
</div>
</div>
);
}
-39
View File
@@ -1,39 +0,0 @@
import {Image} from "@heroui/react";
import {ScriptProviders} from "@/components/scripts/script-providers";
interface DocsLayoutProps {
children: React.ReactNode;
}
export default function DocsLayout({children}: DocsLayoutProps) {
return (
<>
<main className="relative container mx-auto max-w-7xl z-10 px-6 min-h-[calc(100vh_-_64px_-_108px)] mb-12 flex-grow">
{children}
</main>
<div
aria-hidden="true"
className="fixed hidden dark:md:block dark:opacity-70 -bottom-[40%] -left-[20%] z-0"
>
<Image
removeWrapper
alt="docs left background"
src="https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/images/docs-left.png"
/>
</div>
<div
aria-hidden="true"
className="fixed hidden dark:md:block dark:opacity-70 -top-[80%] -right-[60%] 2xl:-top-[60%] 2xl:-right-[45%] z-0 rotate-12"
>
<Image
removeWrapper
alt="docs right background"
src="https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/images/docs-right.png"
/>
</div>
<ScriptProviders />
</>
);
}
-29
View File
@@ -1,29 +0,0 @@
import {allBlogPosts} from "contentlayer2/generated";
import {compareDesc} from "date-fns";
import {BlogPostList} from "@/components/blog-post";
import {__DEV__, __PREVIEW__} from "@/utils";
const isDraftVisible = __DEV__ || __PREVIEW__;
export default function Blog() {
const posts = allBlogPosts
.sort((a, b) => compareDesc(new Date(a.date), new Date(b.date)))
?.filter((post) => {
if (post.draft && !isDraftVisible) {
return false;
}
return true;
});
return (
<div className="w-full lg:px-16 mt-12">
<div className="text-center">
<h1 className="mb-2 font-bold text-4xl">HeroUI Latest Updates</h1>
<h5 className="text-default-500 text-lg">All the latest news about HeroUI.</h5>
</div>
<BlogPostList posts={posts} />
</div>
);
}
-109
View File
@@ -1,109 +0,0 @@
import type {Metadata} from "next";
import {notFound} from "next/navigation";
import {allDocs} from "contentlayer2/generated";
import {Link} from "@heroui/react";
import {MDXContent} from "@/components/mdx-content";
import {siteConfig} from "@/config/site";
import {DocsPager, DocsToc} from "@/components/docs";
import {Route} from "@/libs/docs/page";
import {GITHUB_URL, REPO_NAME} from "@/libs/github/constants";
import {CONTENT_PATH, TAG} from "@/libs/docs/config";
import {getHeadings} from "@/libs/docs/utils";
interface DocPageProps {
params: {
slug: string[];
};
}
async function getDocFromParams({params}: DocPageProps) {
const slug = params.slug?.join("/") || "";
const doc = allDocs.find((doc) => doc.slugAsParams === slug);
if (!doc) {
null;
}
const headings = getHeadings(doc?.body.raw);
const currentRoute: Route = {
key: doc?._id,
title: doc?.title,
path: `/${doc?._raw?.sourceFilePath}`,
};
return {doc, headings, currentRoute};
}
export async function generateMetadata({params}: DocPageProps): Promise<Metadata> {
const {doc} = await getDocFromParams({params});
if (!doc) {
return {};
}
return {
title: doc.title,
description: doc.description,
openGraph: {
title: doc.title,
description: doc.description,
type: "article",
url: doc.url,
images: [
{
url: siteConfig.ogImage,
width: 1200,
height: 630,
alt: siteConfig.name,
},
],
},
twitter: {
card: "summary_large_image",
title: doc.title,
description: doc.description,
images: [siteConfig.ogImage],
creator: siteConfig.creator,
},
};
}
export async function generateStaticParams(): Promise<DocPageProps["params"][]> {
return allDocs.map((doc) => ({
slug: doc.slugAsParams.split("/"),
}));
}
export default async function DocPage({params}: DocPageProps) {
const {doc, headings, currentRoute} = await getDocFromParams({params});
if (!doc) {
notFound();
}
const editUrl = `${GITHUB_URL}/${REPO_NAME}/edit/${TAG}${CONTENT_PATH}${currentRoute?.path}`;
return (
<>
<div className="col-span-12 lg:col-span-10 xl:col-span-8 lg:px-16 mt-10">
<div className="w-full prose prose-neutral">
<MDXContent code={doc.body.code} />
</div>
{currentRoute && <DocsPager currentRoute={currentRoute} />}
<footer>
<Link isExternal showAnchorIcon href={editUrl} size="sm">
Edit this page on GitHub
</Link>
</footer>
</div>
{headings && headings.length > 0 && (
<div className="hidden z-10 xl:flex xl:col-span-2 mt-8 pl-0">
<DocsToc headings={headings} />
</div>
)}
</>
);
}
-46
View File
@@ -1,46 +0,0 @@
import {Image} from "@heroui/react";
import manifest from "@/config/routes.json";
import {DocsSidebar} from "@/components/docs/sidebar";
import {ScriptProviders} from "@/components/scripts/script-providers";
interface DocsLayoutProps {
children: React.ReactNode;
}
export default function DocsLayout({children}: DocsLayoutProps) {
return (
<>
<main className="relative container mx-auto max-w-8xl z-10 px-6 min-h-[calc(100vh_-_64px_-_108px)] mb-12 flex-grow">
<div className="grid grid-cols-12">
<div className="hidden overflow-visible relative z-10 lg:block lg:col-span-2 mt-8 pr-4">
<DocsSidebar routes={manifest.routes} />
</div>
{children}
</div>
</main>
<div
aria-hidden="true"
className="fixed hidden dark:md:block dark:opacity-100 -bottom-[30%] -left-[30%] z-0"
>
<Image
removeWrapper
alt="docs left background"
src="https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/images/docs-left.png"
/>
</div>
<div
aria-hidden="true"
className="fixed hidden dark:md:block dark:opacity-70 -top-[50%] -right-[60%] 2xl:-top-[60%] 2xl:-right-[45%] z-0 rotate-12"
>
<Image
removeWrapper
alt="docs right background"
src="https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/images/docs-right.png"
/>
</div>
<ScriptProviders />
</>
);
}
@@ -1,47 +0,0 @@
/* eslint-disable no-console */
"use client";
import * as React from "react";
import {Autocomplete, AutocompleteItem} from "@heroui/react";
import {useAsyncList} from "@react-stately/data";
type SWCharacter = {
name: string;
height: string;
mass: string;
birth_year: string;
};
export default function Page() {
let list = useAsyncList<SWCharacter>({
async load({signal, filterText}) {
let res = await fetch(`https://swapi.py4e.com/api/people/?search=${filterText}`, {signal});
let json = await res.json();
return {
items: json.results,
};
},
});
return (
<div className="p-6">
<Autocomplete
className="max-w-xs"
inputValue={list.filterText}
isLoading={list.isLoading}
items={list.items}
label="Select a character"
placeholder="Type to search..."
variant="bordered"
onInputChange={list.setFilterText}
>
{(item) => (
<AutocompleteItem key={item.name} className="capitalize">
{item.name}
</AutocompleteItem>
)}
</Autocomplete>
</div>
);
}
@@ -1,113 +0,0 @@
/* eslint-disable no-console */
"use client";
import * as React from "react";
import {Autocomplete, AutocompleteItem} from "@heroui/react";
import {useInfiniteScroll} from "@heroui/use-infinite-scroll";
type Pokemon = {
name: string;
url: string;
};
type UsePokemonListProps = {
/** Delay to wait before fetching more items */
fetchDelay?: number;
};
function usePokemonList({fetchDelay = 0}: UsePokemonListProps = {}) {
const [items, setItems] = React.useState<Pokemon[]>([]);
const [hasMore, setHasMore] = React.useState(true);
const [isLoading, setIsLoading] = React.useState(false);
const [offset, setOffset] = React.useState(0);
const limit = 10; // Number of items per page, adjust as necessary
const loadPokemon = async (currentOffset: number) => {
const controller = new AbortController();
const {signal} = controller;
try {
setIsLoading(true);
if (offset > 0) {
// Delay to simulate network latency
await new Promise((resolve) => setTimeout(resolve, fetchDelay));
}
let res = await fetch(
`https://pokeapi.co/api/v2/pokemon?offset=${currentOffset}&limit=${limit}`,
{signal},
);
if (!res.ok) {
throw new Error("Network response was not ok");
}
let json = await res.json();
setHasMore(json.next !== null);
// Append new results to existing ones
setItems((prevItems) => [...prevItems, ...json.results]);
} catch (error) {
// @ts-ignore
if (error.name === "AbortError") {
console.log("Fetch aborted");
} else {
console.error("There was an error with the fetch operation:", error);
}
} finally {
setIsLoading(false);
}
};
React.useEffect(() => {
loadPokemon(offset);
}, []);
const onLoadMore = () => {
const newOffset = offset + limit;
setOffset(newOffset);
loadPokemon(newOffset);
};
return {
items,
hasMore,
isLoading,
onLoadMore,
};
}
export default function Page() {
const [isOpen, setIsOpen] = React.useState(false);
const {items, hasMore, isLoading, onLoadMore} = usePokemonList({fetchDelay: 1500});
const [, scrollerRef] = useInfiniteScroll({
hasMore,
isEnabled: isOpen,
shouldUseLoader: false, // We don't want to show the loader at the bottom of the list
onLoadMore,
});
return (
<div className="p-6">
<Autocomplete
className="max-w-xs"
defaultItems={items}
isLoading={isLoading}
label="Pick a Pokemon"
placeholder="Select a Pokemon"
scrollRef={scrollerRef}
variant="bordered"
onOpenChange={setIsOpen}
>
{(item) => (
<AutocompleteItem key={item.name} className="capitalize">
{item.name}
</AutocompleteItem>
)}
</Autocomplete>
</div>
);
}
@@ -1,107 +0,0 @@
"use client";
import * as React from "react";
import {Autocomplete, AutocompleteItem, MenuTriggerAction} from "@heroui/react";
import {useFilter} from "@react-aria/i18n";
const animals = [
{label: "Cat", value: "cat", description: "The second most popular pet in the world"},
{label: "Dog", value: "dog", description: "The most popular pet in the world"},
{label: "Elephant", value: "elephant", description: "The largest land animal"},
{label: "Lion", value: "lion", description: "The king of the jungle"},
{label: "Tiger", value: "tiger", description: "The largest cat species"},
{label: "Giraffe", value: "giraffe", description: "The tallest land animal"},
{
label: "Dolphin",
value: "dolphin",
description: "A widely distributed and diverse group of aquatic mammals",
},
{label: "Penguin", value: "penguin", description: "A group of aquatic flightless birds"},
{label: "Zebra", value: "zebra", description: "A several species of African equids"},
{
label: "Shark",
value: "shark",
description: "A group of elasmobranch fish characterized by a cartilaginous skeleton",
},
{
label: "Whale",
value: "whale",
description: "Diverse group of fully aquatic placental marine mammals",
},
{label: "Otter", value: "otter", description: "A carnivorous mammal in the subfamily Lutrinae"},
{label: "Crocodile", value: "crocodile", description: "A large semiaquatic reptile"},
];
type FieldState = {
selectedKey: React.Key | null;
inputValue: string;
items: typeof animals;
};
export default function Page() {
// Store ComboBox input value, selected option, open state, and items
// in a state tracker
const [fieldState, setFieldState] = React.useState<FieldState>({
selectedKey: "",
inputValue: "",
items: animals,
});
// Implement custom filtering logic and control what items are
// available to the Autocomplete.
const {startsWith} = useFilter({sensitivity: "base"});
// Specify how each of the Autocomplete values should change when an
// option is selected from the list box
const onSelectionChange = (key: React.Key | null) => {
setFieldState((prevState) => {
let selectedItem = prevState.items.find((option) => option.value === key);
return {
inputValue: selectedItem?.label || "",
selectedKey: key,
items: animals.filter((item) => startsWith(item.label, selectedItem?.label || "")),
};
});
};
// Specify how each of the Autocomplete values should change when the input
// field is altered by the user
const onInputChange = (value: string) => {
setFieldState((prevState) => ({
inputValue: value,
selectedKey: value === "" ? null : prevState.selectedKey,
items: animals.filter((item) => startsWith(item.label, value)),
}));
};
// Show entire list if user opens the menu manually
const onOpenChange = (isOpen: boolean, menuTrigger: MenuTriggerAction) => {
if (menuTrigger === "manual" && isOpen) {
setFieldState((prevState) => ({
inputValue: prevState.inputValue,
selectedKey: prevState.selectedKey,
items: animals,
}));
}
};
return (
<div className="p-6">
<Autocomplete
className="max-w-xs"
inputValue={fieldState.inputValue}
items={fieldState.items}
label="Favorite Animal"
placeholder="Select an animal"
selectedKey={fieldState.selectedKey}
variant="bordered"
onInputChange={onInputChange}
onOpenChange={onOpenChange}
onSelectionChange={onSelectionChange}
>
{(item) => <AutocompleteItem key={item.value}>{item.label}</AutocompleteItem>}
</Autocomplete>
</div>
);
}
-27
View File
@@ -1,27 +0,0 @@
"use client";
import {Card, CardBody, CircularProgress} from "@heroui/react";
export default function ButtonDemo() {
return (
<main className="dark bg-background text-foreground">
<div className="flex w-screen h-screen items-center justify-center">
<Card className="w-[240px] h-[240px] bg-default-200 dark:bg-default-50">
<CardBody className="justify-center items-center py-0">
<CircularProgress
classNames={{
svg: "w-36 h-36 drop-shadow-md",
indicator: "stroke-foreground",
track: "stroke-foreground/10",
value: "text-3xl font-semibold text-foreground",
}}
showValueLabel={true}
strokeWidth={4}
value={70}
/>
</CardBody>
</Card>
</div>
</main>
);
}
@@ -1,74 +0,0 @@
"use client";
import {
Modal,
ModalContent,
ModalHeader,
ModalBody,
ModalFooter,
Button,
useDisclosure,
RadioGroup,
Radio,
ModalProps,
} from "@heroui/react";
import {useState} from "react";
export default function Page() {
const {isOpen, onOpen, onOpenChange} = useDisclosure();
const [modalPlacement, setModalPlacement] = useState("auto");
return (
<div className="flex px-10 min-h-[80vh] justify-center items-center flex-col gap-4">
<Button className="max-w-fit" onPress={onOpen}>
Open Modal
</Button>
<RadioGroup
label="Select modal placement"
orientation="horizontal"
value={modalPlacement}
onValueChange={setModalPlacement}
>
<Radio value="auto">auto</Radio>
<Radio value="top">top</Radio>
<Radio value="bottom">bottom</Radio>
<Radio value="center">center</Radio>
<Radio value="top-center">top-center</Radio>
<Radio value="bottom-center">bottom-center</Radio>
</RadioGroup>
<Modal
isOpen={isOpen}
placement={modalPlacement as ModalProps["placement"]}
onOpenChange={onOpenChange}
>
<ModalContent>
{(onClose) => (
<>
<ModalHeader className="flex flex-col gap-1">Modal Title</ModalHeader>
<ModalBody>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam pulvinar risus non
risus hendrerit venenatis. Pellentesque sit amet hendrerit risus, sed porttitor
quam.
</p>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam pulvinar risus non
risus hendrerit venenatis. Pellentesque sit amet hendrerit risus, sed porttitor
quam.
</p>
</ModalBody>
<ModalFooter>
<Button color="danger" variant="light" onPress={onClose}>
Close
</Button>
<Button color="primary" onPress={onClose}>
Action
</Button>
</ModalFooter>
</>
)}
</ModalContent>
</Modal>
</div>
);
}
@@ -1,52 +0,0 @@
"use client";
import {Navbar, NavbarBrand, NavbarContent, NavbarItem, Link, Button} from "@heroui/react";
const AcmeLogo = () => (
<svg fill="none" height="36" viewBox="0 0 32 32" width="36">
<path
clipRule="evenodd"
d="M17.6482 10.1305L15.8785 7.02583L7.02979 22.5499H10.5278L17.6482 10.1305ZM19.8798 14.0457L18.11 17.1983L19.394 19.4511H16.8453L15.1056 22.5499H24.7272L19.8798 14.0457Z"
fill="currentColor"
fillRule="evenodd"
/>
</svg>
);
export default function Page() {
return (
<Navbar isBordered>
<NavbarBrand>
<AcmeLogo />
<p className="font-bold text-inherit">ACME</p>
</NavbarBrand>
<NavbarContent className="hidden sm:flex gap-4" justify="center">
<NavbarItem>
<Link color="foreground" href="#">
Features
</Link>
</NavbarItem>
<NavbarItem isActive>
<Link aria-current="page" href="#">
Customers
</Link>
</NavbarItem>
<NavbarItem>
<Link color="foreground" href="#">
Integrations
</Link>
</NavbarItem>
</NavbarContent>
<NavbarContent justify="end">
<NavbarItem className="hidden lg:flex">
<Link href="#">Login</Link>
</NavbarItem>
<NavbarItem>
<Button as={Link} color="primary" href="#" variant="flat">
Sign Up
</Button>
</NavbarItem>
</NavbarContent>
</Navbar>
);
}
@@ -1,101 +0,0 @@
"use client";
import {
Navbar,
NavbarBrand,
NavbarContent,
NavbarItem,
NavbarMenu,
NavbarMenuItem,
NavbarMenuToggle,
Button,
Link,
} from "@heroui/react";
import React from "react";
const AcmeLogo = () => (
<svg fill="none" height="36" viewBox="0 0 32 32" width="36">
<path
clipRule="evenodd"
d="M17.6482 10.1305L15.8785 7.02583L7.02979 22.5499H10.5278L17.6482 10.1305ZM19.8798 14.0457L18.11 17.1983L19.394 19.4511H16.8453L15.1056 22.5499H24.7272L19.8798 14.0457Z"
fill="currentColor"
fillRule="evenodd"
/>
</svg>
);
export default function Page() {
const [isMenuOpen, setIsMenuOpen] = React.useState<boolean | undefined>(false);
const menuItems = [
"Profile",
"Dashboard",
"Activity",
"Analytics",
"System",
"Deployments",
"My Settings",
"Team Settings",
"Help & Feedback",
"Log Out",
];
return (
<Navbar onMenuOpenChange={setIsMenuOpen}>
<NavbarContent>
<NavbarMenuToggle
aria-label={isMenuOpen ? "Close menu" : "Open menu"}
className="sm:hidden"
/>
<NavbarBrand>
<AcmeLogo />
<p className="font-bold text-inherit">ACME</p>
</NavbarBrand>
</NavbarContent>
<NavbarContent className="hidden sm:flex gap-4" justify="center">
<NavbarItem>
<Link color="foreground" href="#">
Features
</Link>
</NavbarItem>
<NavbarItem isActive>
<Link aria-current="page" href="#">
Customers
</Link>
</NavbarItem>
<NavbarItem>
<Link color="foreground" href="#">
Integrations
</Link>
</NavbarItem>
</NavbarContent>
<NavbarContent justify="end">
<NavbarItem className="hidden lg:flex">
<Link href="#">Login</Link>
</NavbarItem>
<NavbarItem>
<Button as={Link} color="primary" href="#" variant="flat">
Sign Up
</Button>
</NavbarItem>
</NavbarContent>
<NavbarMenu>
{menuItems.map((item, index) => (
<NavbarMenuItem key={`${item}-${index}`}>
<Link
className="w-full"
color={
index === 2 ? "primary" : index === menuItems.length - 1 ? "danger" : "foreground"
}
href="#"
size="lg"
>
{item}
</Link>
</NavbarMenuItem>
))}
</NavbarMenu>
</Navbar>
);
}
@@ -1,69 +0,0 @@
"use client";
import {Navbar, NavbarBrand, NavbarContent, NavbarItem, Link, Button} from "@heroui/react";
const AcmeLogo = () => (
<svg fill="none" height="36" viewBox="0 0 32 32" width="36">
<path
clipRule="evenodd"
d="M17.6482 10.1305L15.8785 7.02583L7.02979 22.5499H10.5278L17.6482 10.1305ZM19.8798 14.0457L18.11 17.1983L19.394 19.4511H16.8453L15.1056 22.5499H24.7272L19.8798 14.0457Z"
fill="currentColor"
fillRule="evenodd"
/>
</svg>
);
export default function Page() {
return (
<Navbar
classNames={{
item: [
"flex",
"relative",
"h-full",
"items-center",
"data-[active=true]:after:content-['']",
"data-[active=true]:after:absolute",
"data-[active=true]:after:bottom-0",
"data-[active=true]:after:left-0",
"data-[active=true]:after:right-0",
"data-[active=true]:after:h-[2px]",
"data-[active=true]:after:rounded-[2px]",
"data-[active=true]:after:bg-primary",
],
}}
>
<NavbarBrand>
<AcmeLogo />
<p className="font-bold text-inherit">ACME</p>
</NavbarBrand>
<NavbarContent className="hidden sm:flex gap-4" justify="center">
<NavbarItem>
<Link color="foreground" href="#">
Features
</Link>
</NavbarItem>
<NavbarItem isActive>
<Link aria-current="page" href="#">
Customers
</Link>
</NavbarItem>
<NavbarItem>
<Link color="foreground" href="#">
Integrations
</Link>
</NavbarItem>
</NavbarContent>
<NavbarContent justify="end">
<NavbarItem className="hidden lg:flex">
<Link href="#">Login</Link>
</NavbarItem>
<NavbarItem>
<Button as={Link} color="primary" href="#" variant="flat">
Sign Up
</Button>
</NavbarItem>
</NavbarContent>
</Navbar>
);
}
@@ -1,101 +0,0 @@
"use client";
import {
Navbar,
NavbarBrand,
NavbarContent,
NavbarItem,
Button,
Link,
NavbarMenu,
NavbarMenuItem,
NavbarMenuToggle,
} from "@heroui/react";
import React from "react";
const AcmeLogo = () => (
<svg fill="none" height="36" viewBox="0 0 32 32" width="36">
<path
clipRule="evenodd"
d="M17.6482 10.1305L15.8785 7.02583L7.02979 22.5499H10.5278L17.6482 10.1305ZM19.8798 14.0457L18.11 17.1983L19.394 19.4511H16.8453L15.1056 22.5499H24.7272L19.8798 14.0457Z"
fill="currentColor"
fillRule="evenodd"
/>
</svg>
);
export default function Page() {
const [isMenuOpen, setIsMenuOpen] = React.useState<boolean | undefined>(false);
const menuItems = [
"Profile",
"Dashboard",
"Activity",
"Analytics",
"System",
"Deployments",
"My Settings",
"Team Settings",
"Help & Feedback",
"Log Out",
];
return (
<Navbar disableAnimation onMenuOpenChange={setIsMenuOpen}>
<NavbarContent>
<NavbarMenuToggle
aria-label={isMenuOpen ? "Close menu" : "Open menu"}
className="sm:hidden"
/>
<NavbarBrand>
<AcmeLogo />
<p className="font-bold text-inherit">ACME</p>
</NavbarBrand>
</NavbarContent>
<NavbarContent className="hidden sm:flex gap-4" justify="center">
<NavbarItem>
<Link color="foreground" href="#">
Features
</Link>
</NavbarItem>
<NavbarItem isActive>
<Link aria-current="page" href="#">
Customers
</Link>
</NavbarItem>
<NavbarItem>
<Link color="foreground" href="#">
Integrations
</Link>
</NavbarItem>
</NavbarContent>
<NavbarContent justify="end">
<NavbarItem className="hidden lg:flex">
<Link href="#">Login</Link>
</NavbarItem>
<NavbarItem>
<Button as={Link} color="primary" href="#" variant="flat">
Sign Up
</Button>
</NavbarItem>
</NavbarContent>
<NavbarMenu>
{menuItems.map((item, index) => (
<NavbarMenuItem key={`${item}-${index}`}>
<Link
className="w-full"
color={
index === 2 ? "primary" : index === menuItems.length - 1 ? "danger" : "foreground"
}
href="#"
size="lg"
>
{item}
</Link>
</NavbarMenuItem>
))}
</NavbarMenu>
</Navbar>
);
}
@@ -1,52 +0,0 @@
"use client";
import {Navbar, NavbarBrand, NavbarContent, NavbarItem, Link, Button} from "@heroui/react";
const AcmeLogo = () => (
<svg fill="none" height="36" viewBox="0 0 32 32" width="36">
<path
clipRule="evenodd"
d="M17.6482 10.1305L15.8785 7.02583L7.02979 22.5499H10.5278L17.6482 10.1305ZM19.8798 14.0457L18.11 17.1983L19.394 19.4511H16.8453L15.1056 22.5499H24.7272L19.8798 14.0457Z"
fill="currentColor"
fillRule="evenodd"
/>
</svg>
);
export default function Page() {
return (
<Navbar isBordered isBlurred={false}>
<NavbarBrand>
<AcmeLogo />
<p className="font-bold text-inherit">ACME</p>
</NavbarBrand>
<NavbarContent className="hidden sm:flex gap-4" justify="center">
<NavbarItem>
<Link color="foreground" href="#">
Features
</Link>
</NavbarItem>
<NavbarItem isActive>
<Link aria-current="page" href="#">
Customers
</Link>
</NavbarItem>
<NavbarItem>
<Link color="foreground" href="#">
Integrations
</Link>
</NavbarItem>
</NavbarContent>
<NavbarContent justify="end">
<NavbarItem className="hidden lg:flex">
<Link href="#">Login</Link>
</NavbarItem>
<NavbarItem>
<Button as={Link} color="primary" href="#" variant="flat">
Sign Up
</Button>
</NavbarItem>
</NavbarContent>
</Navbar>
);
}
@@ -1,52 +0,0 @@
"use client";
import {Navbar, NavbarBrand, NavbarContent, NavbarItem, Link, Button} from "@heroui/react";
const AcmeLogo = () => (
<svg fill="none" height="36" viewBox="0 0 32 32" width="36">
<path
clipRule="evenodd"
d="M17.6482 10.1305L15.8785 7.02583L7.02979 22.5499H10.5278L17.6482 10.1305ZM19.8798 14.0457L18.11 17.1983L19.394 19.4511H16.8453L15.1056 22.5499H24.7272L19.8798 14.0457Z"
fill="currentColor"
fillRule="evenodd"
/>
</svg>
);
export default function Page() {
return (
<Navbar shouldHideOnScroll>
<NavbarBrand>
<AcmeLogo />
<p className="font-bold text-inherit">ACME</p>
</NavbarBrand>
<NavbarContent className="hidden sm:flex gap-4" justify="center">
<NavbarItem>
<Link color="foreground" href="#">
Features
</Link>
</NavbarItem>
<NavbarItem isActive>
<Link aria-current="page" href="#">
Customers
</Link>
</NavbarItem>
<NavbarItem>
<Link color="foreground" href="#">
Integrations
</Link>
</NavbarItem>
</NavbarContent>
<NavbarContent justify="end">
<NavbarItem className="hidden lg:flex">
<Link href="#">Login</Link>
</NavbarItem>
<NavbarItem>
<Button as={Link} color="primary" href="#" variant="flat">
Sign Up
</Button>
</NavbarItem>
</NavbarContent>
</Navbar>
);
}
-78
View File
@@ -1,78 +0,0 @@
const Content = () => (
<div className="px-6 flex gap-4 flex-col pb-16 flex-grow">
<h1 className="mt-4 font-bold text-4xl">Lorem ipsum dolor sit amet</h1>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut
labore et dolore magna aliqua. Purus gravida quis blandit turpis. Augue neque gravida in
fermentum et sollicitudin ac orci. Et sollicitudin ac orci phasellus egestas. Elementum tempus
egestas sed sed risus pretium quam vulputate. Interdum velit euismod in pellentesque massa
placerat duis ultricies.
</p>
<p>
Rhoncus mattis rhoncus urna neque viverra justo nec ultrices dui. Praesent semper feugiat nibh
sed pulvinar. Ultrices gravida dictum fusce ut placerat orci nulla pellentesque. Malesuada
proin libero nunc consequat interdum varius sit amet. Lectus quam id leo in vitae. Sed viverra
tellus in hac habitasse platea dictumst. Vivamus at augue eget arcu. Augue mauris augue neque
gravida in.
</p>
<p>
Tincidunt vitae semper quis lectus nulla at volutpat diam. Gravida dictum fusce ut placerat.
Erat velit scelerisque in dictum non. Tempus quam pellentesque nec nam aliquam sem et tortor
consequat. Eu nisl nunc mi ipsum faucibus. Cras fermentum odio eu feugiat pretium nibh. Vel
pharetra vel turpis nunc eget lorem dolor sed viverra. Sollicitudin tempor id eu nisl nunc mi
ipsum faucibus. Sed id semper risus in hendrerit gravida rutrum. Eget nulla facilisi etiam
dignissim. Erat imperdiet sed euismod nisi. Risus in hendrerit gravida rutrum quisque non
tellus orci ac.
</p>
<p>
Tempor orci dapibus ultrices in iaculis nunc sed augue lacus. In pellentesque massa placerat
duis ultricies. Sit amet massa vitae tortor condimentum. Morbi tincidunt augue interdum velit
euismod. Aliquet enim tortor at auctor urna nunc id. A scelerisque purus semper eget. Vitae
justo eget magna fermentum iaculis. Arcu non odio euismod lacinia at quis. Et leo duis ut diam
quam nulla porttitor massa. Eget nunc scelerisque viverra mauris. Suscipit tellus mauris a
diam maecenas sed enim. Cras sed felis eget velit aliquet. Est placerat in egestas erat
imperdiet sed euismod nisi porta. In ante metus dictum at tempor commodo. In cursus turpis
massa tincidunt dui ut ornare lectus. Tempus iaculis urna id volutpat. Iaculis eu non diam
phasellus vestibulum lorem sed risus.
</p>
<p>
Ridiculus mus mauris vitae ultricies leo integer malesuada nunc vel. Imperdiet massa tincidunt
nunc pulvinar sapien et ligula ullamcorper malesuada. Faucibus pulvinar elementum integer enim
neque volutpat. Gravida arcu ac tortor dignissim convallis aenean. Lectus quam id leo in
vitae. Ultricies tristique nulla aliquet enim tortor. Nec tincidunt praesent semper feugiat
nibh sed. Imperdiet proin fermentum leo vel orci porta non pulvinar neque. Praesent semper
feugiat nibh sed pulvinar proin gravida. Dis parturient montes nascetur ridiculus mus mauris.
Rhoncus dolor purus non enim praesent elementum facilisis leo vel. Ut lectus arcu bibendum at.
Integer enim neque volutpat ac. Diam sit amet nisl suscipit. Eros donec ac odio tempor orci
dapibus ultrices in iaculis. Ullamcorper a lacus vestibulum sed arcu non odio euismod. Quis
lectus nulla at volutpat diam ut. Turpis egestas integer eget aliquet. Adipiscing tristique
risus nec feugiat in fermentum posuere. Morbi tempus iaculis urna id. Amet commodo nulla
facilisi nullam vehicula ipsum a arcu.
</p>
<p>
Rhoncus mattis rhoncus urna neque viverra justo nec ultrices dui. Praesent semper feugiat nibh
sed pulvinar. Ultrices gravida dictum fusce ut placerat orci nulla pellentesque. Malesuada
proin libero nunc consequat interdum varius sit amet. Lectus quam id leo in vitae. Sed viverra
tellus in hac habitasse platea dictumst. Vivamus at augue eget arcu. Augue mauris augue neque
gravida in.
</p>
<p>
Tincidunt vitae semper quis lectus nulla at volutpat diam. Gravida dictum fusce ut placerat.
Erat velit scelerisque in dictum non. Tempus quam pellentesque nec nam aliquam sem et tortor
consequat. Eu nisl nunc mi ipsum faucibus. Cras fermentum odio eu feugiat pretium nibh. Vel
pharetra vel turpis nunc eget lorem dolor sed viverra. Sollicitudin tempor id eu nisl nunc mi
ipsum faucibus. Sed id semper risus in hendrerit gravida rutrum. Eget nulla facilisi etiam
dignissim. Erat imperdiet sed euismod nisi. Risus in hendrerit gravida rutrum quisque non
tellus orci ac.
</p>
</div>
);
export default function NavbarExamplesLayout({children}: {children: React.ReactNode}) {
return (
<div className="flex flex-col">
{children}
<Content />
</div>
);
}
@@ -1,52 +0,0 @@
"use client";
import {Navbar, NavbarBrand, NavbarContent, NavbarItem, Link, Button} from "@heroui/react";
const AcmeLogo = () => (
<svg fill="none" height="36" viewBox="0 0 32 32" width="36">
<path
clipRule="evenodd"
d="M17.6482 10.1305L15.8785 7.02583L7.02979 22.5499H10.5278L17.6482 10.1305ZM19.8798 14.0457L18.11 17.1983L19.394 19.4511H16.8453L15.1056 22.5499H24.7272L19.8798 14.0457Z"
fill="currentColor"
fillRule="evenodd"
/>
</svg>
);
export default function Page() {
return (
<Navbar position="static">
<NavbarBrand>
<AcmeLogo />
<p className="font-bold text-inherit">ACME</p>
</NavbarBrand>
<NavbarContent className="hidden sm:flex gap-4" justify="center">
<NavbarItem>
<Link color="foreground" href="#">
Features
</Link>
</NavbarItem>
<NavbarItem isActive>
<Link aria-current="page" href="#">
Customers
</Link>
</NavbarItem>
<NavbarItem>
<Link color="foreground" href="#">
Integrations
</Link>
</NavbarItem>
</NavbarContent>
<NavbarContent justify="end">
<NavbarItem className="hidden lg:flex">
<Link href="#">Login</Link>
</NavbarItem>
<NavbarItem>
<Button as={Link} color="primary" href="#" variant="flat">
Sign Up
</Button>
</NavbarItem>
</NavbarContent>
</Navbar>
);
}
@@ -1,52 +0,0 @@
"use client";
import {Navbar, NavbarBrand, NavbarContent, NavbarItem, Link, Button} from "@heroui/react";
const AcmeLogo = () => (
<svg fill="none" height="36" viewBox="0 0 32 32" width="36">
<path
clipRule="evenodd"
d="M17.6482 10.1305L15.8785 7.02583L7.02979 22.5499H10.5278L17.6482 10.1305ZM19.8798 14.0457L18.11 17.1983L19.394 19.4511H16.8453L15.1056 22.5499H24.7272L19.8798 14.0457Z"
fill="currentColor"
fillRule="evenodd"
/>
</svg>
);
export default function Page() {
return (
<Navbar>
<NavbarBrand>
<AcmeLogo />
<p className="font-bold text-inherit">ACME</p>
</NavbarBrand>
<NavbarContent className="hidden sm:flex gap-4" justify="center">
<NavbarItem>
<Link color="foreground" href="#">
Features
</Link>
</NavbarItem>
<NavbarItem isActive>
<Link aria-current="page" href="#">
Customers
</Link>
</NavbarItem>
<NavbarItem>
<Link color="foreground" href="#">
Integrations
</Link>
</NavbarItem>
</NavbarContent>
<NavbarContent justify="end">
<NavbarItem className="hidden lg:flex">
<Link href="#">Login</Link>
</NavbarItem>
<NavbarItem>
<Button as={Link} color="primary" href="#" variant="flat">
Sign Up
</Button>
</NavbarItem>
</NavbarContent>
</Navbar>
);
}
@@ -1,83 +0,0 @@
"use client";
import {
Navbar,
NavbarBrand,
NavbarContent,
NavbarItem,
Link,
DropdownItem,
DropdownTrigger,
Dropdown,
DropdownMenu,
Avatar,
} from "@heroui/react";
const AcmeLogo = () => (
<svg fill="none" height="36" viewBox="0 0 32 32" width="36">
<path
clipRule="evenodd"
d="M17.6482 10.1305L15.8785 7.02583L7.02979 22.5499H10.5278L17.6482 10.1305ZM19.8798 14.0457L18.11 17.1983L19.394 19.4511H16.8453L15.1056 22.5499H24.7272L19.8798 14.0457Z"
fill="currentColor"
fillRule="evenodd"
/>
</svg>
);
export default function Page() {
return (
<Navbar>
<NavbarBrand>
<AcmeLogo />
<p className="font-bold text-inherit">ACME</p>
</NavbarBrand>
<NavbarContent className="hidden sm:flex gap-4" justify="center">
<NavbarItem>
<Link color="foreground" href="#">
Features
</Link>
</NavbarItem>
<NavbarItem isActive>
<Link aria-current="page" color="secondary" href="#">
Customers
</Link>
</NavbarItem>
<NavbarItem>
<Link color="foreground" href="#">
Integrations
</Link>
</NavbarItem>
</NavbarContent>
<NavbarContent as="div" justify="end">
<Dropdown placement="bottom-end">
<DropdownTrigger>
<Avatar
isBordered
as="button"
className="transition-transform"
color="secondary"
name="Jason Hughes"
size="sm"
src="https://i.pravatar.cc/150?u=a042581f4e29026704d"
/>
</DropdownTrigger>
<DropdownMenu aria-label="Profile Actions" variant="flat">
<DropdownItem key="profile" className="h-14 gap-2">
<p className="font-semibold">Signed in as</p>
<p className="font-semibold">zoey@example.com</p>
</DropdownItem>
<DropdownItem key="settings">My Settings</DropdownItem>
<DropdownItem key="team_settings">Team Settings</DropdownItem>
<DropdownItem key="analytics">Analytics</DropdownItem>
<DropdownItem key="system">System</DropdownItem>
<DropdownItem key="configurations">Configurations</DropdownItem>
<DropdownItem key="help_and_feedback">Help & Feedback</DropdownItem>
<DropdownItem key="logout" color="danger">
Log Out
</DropdownItem>
</DropdownMenu>
</Dropdown>
</NavbarContent>
</Navbar>
);
}
@@ -1,126 +0,0 @@
"use client";
import {
Navbar,
NavbarBrand,
NavbarContent,
NavbarItem,
Link,
Button,
DropdownItem,
DropdownTrigger,
Dropdown,
DropdownMenu,
} from "@heroui/react";
import {ChevronDown, Lock, Activity, Flash, Server, TagUser, Scale} from "@heroui/shared-icons";
const AcmeLogo = () => (
<svg fill="none" height="36" viewBox="0 0 32 32" width="36">
<path
clipRule="evenodd"
d="M17.6482 10.1305L15.8785 7.02583L7.02979 22.5499H10.5278L17.6482 10.1305ZM19.8798 14.0457L18.11 17.1983L19.394 19.4511H16.8453L15.1056 22.5499H24.7272L19.8798 14.0457Z"
fill="currentColor"
fillRule="evenodd"
/>
</svg>
);
export default function Page() {
const icons = {
chevron: <ChevronDown fill="currentColor" size={16} />,
scale: <Scale className="text-warning" fill="currentColor" size={30} />,
lock: <Lock className="text-success" fill="currentColor" size={30} />,
activity: <Activity className="text-secondary" fill="currentColor" size={30} />,
flash: <Flash className="text-primary" fill="currentColor" size={30} />,
server: <Server className="text-success" fill="currentColor" size={30} />,
user: <TagUser className="text-danger" fill="currentColor" size={30} />,
};
return (
<Navbar>
<NavbarBrand>
<AcmeLogo />
<p className="font-bold text-inherit">ACME</p>
</NavbarBrand>
<NavbarContent className="hidden sm:flex gap-4" justify="center">
<Dropdown>
<NavbarItem>
<DropdownTrigger>
<Button
disableRipple
className="p-0 bg-transparent data-[hover=true]:bg-transparent"
endContent={icons.chevron}
radius="sm"
variant="light"
>
Features
</Button>
</DropdownTrigger>
</NavbarItem>
<DropdownMenu
aria-label="ACME features"
itemClasses={{
base: "gap-4",
}}
>
<DropdownItem
key="autoscaling"
description="ACME scales apps based on demand and load"
startContent={icons.scale}
>
Autoscaling
</DropdownItem>
<DropdownItem
key="usage_metrics"
description="Real-time metrics to debug issues"
startContent={icons.activity}
>
Usage Metrics
</DropdownItem>
<DropdownItem
key="production_ready"
description="ACME runs on ACME, join us at web scale"
startContent={icons.flash}
>
Production Ready
</DropdownItem>
<DropdownItem
key="99_uptime"
description="High availability and uptime guarantees"
startContent={icons.server}
>
+99% Uptime
</DropdownItem>
<DropdownItem
key="supreme_support"
description="Support team ready to respond"
startContent={icons.user}
>
+Supreme Support
</DropdownItem>
</DropdownMenu>
</Dropdown>
<NavbarItem isActive>
<Link aria-current="page" href="#">
Customers
</Link>
</NavbarItem>
<NavbarItem>
<Link color="foreground" href="#">
Integrations
</Link>
</NavbarItem>
</NavbarContent>
<NavbarContent justify="end">
<NavbarItem className="hidden lg:flex">
<Link href="#">Login</Link>
</NavbarItem>
<NavbarItem>
<Button as={Link} color="primary" href="#" variant="flat">
Sign Up
</Button>
</NavbarItem>
</NavbarContent>
</Navbar>
);
}
@@ -1,101 +0,0 @@
"use client";
import {
Navbar,
NavbarBrand,
NavbarContent,
NavbarItem,
Link,
Button,
NavbarMenuToggle,
NavbarMenu,
NavbarMenuItem,
} from "@heroui/react";
import React from "react";
const AcmeLogo = () => (
<svg fill="none" height="36" viewBox="0 0 32 32" width="36">
<path
clipRule="evenodd"
d="M17.6482 10.1305L15.8785 7.02583L7.02979 22.5499H10.5278L17.6482 10.1305ZM19.8798 14.0457L18.11 17.1983L19.394 19.4511H16.8453L15.1056 22.5499H24.7272L19.8798 14.0457Z"
fill="currentColor"
fillRule="evenodd"
/>
</svg>
);
export default function Page() {
const [isMenuOpen, setIsMenuOpen] = React.useState<boolean | undefined>(false);
const menuItems = [
"Profile",
"Dashboard",
"Activity",
"Analytics",
"System",
"Deployments",
"My Settings",
"Team Settings",
"Help & Feedback",
"Log Out",
];
return (
<Navbar onMenuOpenChange={setIsMenuOpen}>
<NavbarContent>
<NavbarMenuToggle
aria-label={isMenuOpen ? "Close menu" : "Open menu"}
className="sm:hidden"
/>
<NavbarBrand>
<AcmeLogo />
<p className="font-bold text-inherit">ACME</p>
</NavbarBrand>
</NavbarContent>
<NavbarContent className="hidden sm:flex gap-4" justify="center">
<NavbarItem>
<Link color="foreground" href="#">
Features
</Link>
</NavbarItem>
<NavbarItem isActive>
<Link aria-current="page" href="#">
Customers
</Link>
</NavbarItem>
<NavbarItem>
<Link color="foreground" href="#">
Integrations
</Link>
</NavbarItem>
</NavbarContent>
<NavbarContent justify="end">
<NavbarItem className="hidden lg:flex">
<Link href="#">Login</Link>
</NavbarItem>
<NavbarItem>
<Button as={Link} color="primary" href="#" variant="flat">
Sign Up
</Button>
</NavbarItem>
</NavbarContent>
<NavbarMenu>
{menuItems.map((item, index) => (
<NavbarMenuItem key={`${item}-${index}`}>
<Link
className="w-full"
color={
index === 2 ? "primary" : index === menuItems.length - 1 ? "danger" : "foreground"
}
href="#"
size="lg"
>
{item}
</Link>
</NavbarMenuItem>
))}
</NavbarMenu>
</Navbar>
);
}
@@ -1,101 +0,0 @@
"use client";
import {
Navbar,
NavbarBrand,
NavbarContent,
NavbarItem,
Link,
Input,
DropdownItem,
DropdownTrigger,
Dropdown,
DropdownMenu,
Avatar,
} from "@heroui/react";
import {SearchLinearIcon} from "@/components/icons";
const AcmeLogo = () => (
<svg fill="none" height="36" viewBox="0 0 32 32" width="36">
<path
clipRule="evenodd"
d="M17.6482 10.1305L15.8785 7.02583L7.02979 22.5499H10.5278L17.6482 10.1305ZM19.8798 14.0457L18.11 17.1983L19.394 19.4511H16.8453L15.1056 22.5499H24.7272L19.8798 14.0457Z"
fill="currentColor"
fillRule="evenodd"
/>
</svg>
);
export default function Page() {
return (
<Navbar isBordered>
<NavbarContent justify="start">
<NavbarBrand className="mr-4">
<AcmeLogo />
<p className="hidden sm:block font-bold text-inherit">ACME</p>
</NavbarBrand>
<NavbarContent className="hidden sm:flex gap-3">
<NavbarItem>
<Link color="foreground" href="#">
Features
</Link>
</NavbarItem>
<NavbarItem isActive>
<Link aria-current="page" color="secondary" href="#">
Customers
</Link>
</NavbarItem>
<NavbarItem>
<Link color="foreground" href="#">
Integrations
</Link>
</NavbarItem>
</NavbarContent>
</NavbarContent>
<NavbarContent as="div" className="items-center" justify="end">
<Input
classNames={{
base: "max-w-full sm:max-w-[10rem] h-10",
input: "text-small",
inputWrapper:
"h-full font-normal text-default-500 bg-default-400/20 dark:bg-default-500/20",
}}
placeholder="Type to search..."
startContent={<SearchLinearIcon size={18} />}
type="search"
/>
<Dropdown placement="bottom-end">
<NavbarItem>
<DropdownTrigger>
<Avatar
isBordered
as="button"
className="transition-transform"
color="secondary"
name="Jason Hughes"
size="sm"
src="https://i.pravatar.cc/150?u=a042581f4e29026704d"
/>
</DropdownTrigger>
</NavbarItem>
<DropdownMenu aria-label="Profile Actions" variant="flat">
<DropdownItem key="profile" className="h-14 gap-2">
<p className="font-semibold">Signed in as</p>
<p className="font-semibold">zoey@example.com</p>
</DropdownItem>
<DropdownItem key="settings">My Settings</DropdownItem>
<DropdownItem key="team_settings">Team Settings</DropdownItem>
<DropdownItem key="analytics">Analytics</DropdownItem>
<DropdownItem key="system">System</DropdownItem>
<DropdownItem key="configurations">Configurations</DropdownItem>
<DropdownItem key="help_and_feedback">Help & Feedback</DropdownItem>
<DropdownItem key="logout" color="danger">
Log Out
</DropdownItem>
</DropdownMenu>
</Dropdown>
</NavbarContent>
</Navbar>
);
}
-561
View File
@@ -1,561 +0,0 @@
"use client";
import {
RadioGroup,
Radio,
Button,
Accordion,
Tabs,
Textarea,
Input,
Tab,
Avatar,
Select,
SelectItem,
AccordionItem,
Pagination,
extendVariants,
PaginationItem,
} from "@heroui/react";
import {useFilter} from "@react-aria/i18n";
import {useEffect, useMemo, useRef, useState} from "react";
import {useSearchParams} from "next/navigation";
import {SearchLinearIcon} from "@/components/icons";
const MyRadioGroup = () => {
const [radio, setRadio] = useState("1");
return (
<RadioGroup value={radio} onValueChange={setRadio}>
<Radio value="1">Radio 1</Radio>
<Radio value="2">Radio 2</Radio>
<Radio value="3">Radio 3</Radio>
<Radio value="4">Radio 4</Radio>
<Radio value="5">Radio 5</Radio>
<Radio value="6">Radio 6</Radio>
<Radio value="7">Radio 7</Radio>
<Radio value="8">Radio 8</Radio>
<Radio value="9">Radio 9</Radio>
<Radio value="10">Radio 10</Radio>
<Radio value="11">Radio 11</Radio>
<Radio value="12">Radio 12</Radio>
<Radio value="13">Radio 13</Radio>
<Radio value="14">Radio 14</Radio>
<Radio value="15">Radio 15</Radio>
<Radio value="16">Radio 16</Radio>
<Radio value="17">Radio 17</Radio>
<Radio value="18">Radio 18</Radio>
<Radio value="19">Radio 19</Radio>
<Radio value="20">Radio 20</Radio>
<Radio value="21">Radio 21</Radio>
<Radio value="22">Radio 22</Radio>
<Radio value="23">Radio 23</Radio>
<Radio value="24">Radio 24</Radio>
<Radio value="25">Radio 25</Radio>
<Radio value="26">Radio 26</Radio>
<Radio value="27">Radio 27</Radio>
<Radio value="28">Radio 28</Radio>
<Radio value="29">Radio 29</Radio>
<Radio value="30">Radio 30</Radio>
<Radio value="31">Radio 31</Radio>
<Radio value="32">Radio 32</Radio>
<Radio value="33">Radio 33</Radio>
<Radio value="34">Radio 34</Radio>
<Radio value="35">Radio 35</Radio>
<Radio value="36">Radio 36</Radio>
<Radio value="37">Radio 37</Radio>
<Radio value="38">Radio 38</Radio>
<Radio value="39">Radio 39</Radio>
<Radio value="40">Radio 40</Radio>
<Radio value="41">Radio 41</Radio>
<Radio value="42">Radio 42</Radio>
<Radio value="43">Radio 43</Radio>
<Radio value="44">Radio 44</Radio>
<Radio value="45">Radio 45</Radio>
<Radio value="46">Radio 46</Radio>
<Radio value="47">Radio 47</Radio>
<Radio value="48">Radio 48</Radio>
<Radio value="49">Radio 49</Radio>
<Radio value="50">Radio 50</Radio>
<Radio value="51">Radio 51</Radio>
<Radio value="52">Radio 52</Radio>
<Radio value="53">Radio 53</Radio>
<Radio value="54">Radio 54</Radio>
<Radio value="55">Radio 55</Radio>
<Radio value="56">Radio 56</Radio>
<Radio value="57">Radio 57</Radio>
<Radio value="58">Radio 58</Radio>
<Radio value="59">Radio 59</Radio>
<Radio value="60">Radio 60</Radio>
<Radio value="61">Radio 61</Radio>
<Radio value="62">Radio 62</Radio>
</RadioGroup>
);
};
const MyInput = extendVariants(Input, {
variants: {
color: {
stone: {
inputWrapper: [
"bg-zinc-100",
"border",
"shadow",
"transition-colors",
"focus-within:bg-zinc-100",
"data-[hover=true]:border-zinc-600",
"data-[hover=true]:bg-zinc-100",
"group-data-[focus=true]:border-zinc-600",
// dark theme
"dark:bg-zinc-900",
"dark:border-zinc-800",
"dark:data-[hover=true]:bg-zinc-900",
"dark:focus-within:bg-zinc-900",
],
input: [
"text-zinc-800",
"placeholder:text-zinc-600",
// dark theme
"dark:text-zinc-400",
"dark:placeholder:text-zinc-600",
],
},
},
size: {
xs: {
inputWrapper: "h-6 min-h-6 px-1",
input: "text-tiny",
},
md: {
inputWrapper: "h-10 min-h-10",
input: "text-small",
},
xl: {
inputWrapper: "h-14 min-h-14",
input: "text-medium",
},
},
radius: {
xs: {
inputWrapper: "rounded",
},
sm: {
inputWrapper: "rounded-[4px]",
},
},
textSize: {
base: {
input: "text-base",
},
},
removeLabel: {
true: {
label: "hidden",
},
false: {},
},
},
defaultVariants: {
color: "stone",
textSize: "base",
removeLabel: true,
},
});
const MyButton2 = extendVariants(Button, {
variants: {
color: {
foreground:
"bg-foreground text-background data-[hover=true]:bg-foreground/90 data-[pressed=true]:bg-foreground/80",
},
isScalable: {
true: "scale-125",
false: "",
},
size: {
xl: "size--xl",
"2xl": "size--2xl",
},
mySize: {
lg: "px-12 py-6 text-lg",
xl: "px-12 py-6 text-xl",
},
},
defaultVariants: {
color: "foreground",
},
});
const usersData = [
{
id: 1,
name: "Tony Reichert",
role: "CEO",
team: "Management",
status: "active",
age: "29",
avatar: "https://d2u8k2ocievbld.cloudfront.net/memojis/male/1.png",
email: "tony.reichert@example.com",
},
{
id: 2,
name: "Zoey Lang",
role: "Tech Lead",
team: "Development",
status: "paused",
age: "25",
avatar: "https://d2u8k2ocievbld.cloudfront.net/memojis/female/1.png",
email: "zoey.lang@example.com",
},
{
id: 3,
name: "Jane Fisher",
role: "Sr. Dev",
team: "Development",
status: "active",
age: "22",
avatar: "https://d2u8k2ocievbld.cloudfront.net/memojis/female/2.png",
email: "jane.fisher@example.com",
},
{
id: 4,
name: "William Howard",
role: "C.M.",
team: "Marketing",
status: "vacation",
age: "28",
avatar: "https://d2u8k2ocievbld.cloudfront.net/memojis/male/2.png",
email: "william.howard@example.com",
},
{
id: 5,
name: "Kristen Copper",
role: "S. Manager",
team: "Sales",
status: "active",
age: "24",
avatar: "https://d2u8k2ocievbld.cloudfront.net/memojis/female/3.png",
email: "kristen.cooper@example.com",
},
{
id: 6,
name: "Brian Kim",
role: "P. Manager",
team: "Management",
age: "29",
avatar: "https://d2u8k2ocievbld.cloudfront.net/memojis/male/3.png",
email: "brian.kim@example.com",
status: "active",
},
{
id: 7,
name: "Michael Hunt",
role: "Designer",
team: "Design",
status: "paused",
age: "27",
avatar: "https://d2u8k2ocievbld.cloudfront.net/memojis/male/4.png",
email: "michael.hunt@example.com",
},
{
id: 8,
name: "Samantha Brooks",
role: "HR Manager",
team: "HR",
status: "active",
age: "31",
avatar: "https://d2u8k2ocievbld.cloudfront.net/memojis/female/4.png",
email: "samantha.brooks@example.com",
},
{
id: 9,
name: "Frank Harrison",
role: "F. Manager",
team: "Finance",
status: "vacation",
age: "33",
avatar: "https://d2u8k2ocievbld.cloudfront.net/memojis/male/5.png",
email: "frank.harrison@example.com",
},
{
id: 10,
name: "Emma Adams",
role: "Ops Manager",
team: "Operations",
status: "active",
age: "35",
avatar: "https://d2u8k2ocievbld.cloudfront.net/memojis/female/5.png",
email: "emma.adams@example.com",
},
{
id: 11,
name: "Brandon Stevens",
role: "Jr. Dev",
team: "Development",
status: "active",
age: "22",
avatar: "https://d2u8k2ocievbld.cloudfront.net/memojis/male/7.png",
email: "brandon.stevens@example.com",
},
{
id: 12,
name: "Megan Richards",
role: "P. Manager",
team: "Product",
status: "paused",
age: "28",
avatar: "https://d2u8k2ocievbld.cloudfront.net/memojis/female/7.png",
email: "megan.richards@example.com",
},
{
id: 13,
name: "Oliver Scott",
role: "S. Manager",
team: "Security",
status: "active",
age: "37",
avatar: "https://d2u8k2ocievbld.cloudfront.net/memojis/male/8.png",
email: "oliver.scott@example.com",
},
{
id: 14,
name: "Grace Allen",
role: "M. Specialist",
team: "Marketing",
status: "active",
age: "30",
avatar: "https://d2u8k2ocievbld.cloudfront.net/memojis/female/8.png",
email: "grace.allen@example.com",
},
{
id: 15,
name: "Noah Carter",
role: "IT Specialist",
team: "I. Technology",
status: "paused",
age: "31",
avatar: "https://d2u8k2ocievbld.cloudfront.net/memojis/male/9.png",
email: "noah.carter@example.com",
},
{
id: 16,
name: "Ava Perez",
role: "Manager",
team: "Sales",
status: "active",
age: "29",
avatar: "https://d2u8k2ocievbld.cloudfront.net/memojis/female/9.png",
email: "ava.perez@example.com",
},
{
id: 17,
name: "Liam Johnson",
role: "Data Analyst",
team: "Analysis",
status: "active",
age: "28",
avatar: "https://d2u8k2ocievbld.cloudfront.net/memojis/male/11.png",
email: "liam.johnson@example.com",
},
{
id: 18,
name: "Sophia Taylor",
role: "QA Analyst",
team: "Testing",
status: "active",
age: "27",
avatar: "https://d2u8k2ocievbld.cloudfront.net/memojis/female/11.png",
email: "sophia.taylor@example.com",
},
{
id: 19,
name: "Lucas Harris",
role: "Administrator",
team: "Information Technology",
status: "paused",
age: "32",
avatar: "https://d2u8k2ocievbld.cloudfront.net/memojis/male/12.png",
email: "lucas.harris@example.com",
},
{
id: 20,
name: "Mia Robinson",
role: "Coordinator",
team: "Operations",
status: "active",
age: "26",
avatar: "https://d2u8k2ocievbld.cloudfront.net/memojis/female/12.png",
email: "mia.robinson@example.com",
},
];
export default function HeroUIPerf() {
const [textA, setTextA] = useState<string>("");
const [textB, setTextB] = useState<string>("");
const [textC, setTextC] = useState<string>("");
const [isOpen, setIsOpen] = useState<boolean>(false);
const [inputValue, setInputValue] = useState<string>();
const [selectedKey, setSelectedKey] = useState<string>("");
const searchParams = useSearchParams();
const page = Number(searchParams.get("page"));
let {startsWith} = useFilter({sensitivity: "base"});
const filteredItems = inputValue
? usersData.filter((item) => startsWith(item.name, inputValue))
: usersData;
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
isOpen && inputRef?.current?.focus();
}, [isOpen]);
const handleSelectionChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
setSelectedKey(e.target.value);
};
const topContent = useMemo(() => {
return (
<Input
ref={inputRef}
isClearable
aria-activedescendant={selectedKey}
aria-expanded={isOpen}
aria-label="Search user"
autoComplete="off"
autoCorrect="off"
className="z-10 sticky top-1"
placeholder="Search..."
spellCheck="false"
startContent={<SearchLinearIcon className="text-default-400" size={18} strokeWidth="2" />}
type="text"
onValueChange={setInputValue}
/>
);
}, [inputRef, selectedKey, isOpen]);
return (
<div className="w-full p-24 gap-4 flex flex-col">
<Select
classNames={{
base: "max-w-xs",
listboxWrapper: "scroll-pb-6 scroll-pt-28",
}}
items={filteredItems}
label="Assigned to"
labelPlacement="outside"
listboxProps={{
topContent,
variant: "flat",
classNames: {
base: [
"before:content-[''] before:rounded-t-medium before:fixed before:w-full before:h-14 before:z-10",
"before:top-0 before:left-0 before:bg-gradient-to-b before:from-default-50",
],
},
}}
placeholder="Select a user"
selectedKeys={[selectedKey]}
showScrollIndicators={false}
variant="flat"
onChange={handleSelectionChange}
onOpenChange={setIsOpen}
>
{(item) => (
<SelectItem key={item.id} textValue={item.name}>
<div className="flex gap-2 items-center">
<Avatar alt={item.name} className="flex-shrink-0" size="sm" src={item.avatar} />
<div className="flex flex-col">
<span className="text-small">{item.name}</span>
<span className="text-tiny text-default-400">{item.email}</span>
</div>
</div>
</SelectItem>
)}
</Select>
<Accordion>
<AccordionItem key="1" aria-label="Accordion 1" title="Accordion 1">
Non est aliqua tempor occaecat laborum. Lorem culpa minim irure mollit. Est qui
reprehenderit commodo magna proident anim ipsum ex. Mollit id amet officia nisi excepteur
eu. Commodo elit commodo nisi nisi aute eu aliquip aliquip voluptate exercitation ullamco
ipsum eiusmod veniam. Magna in laborum anim amet anim ex elit aliqua nostrud mollit.
Pariatur ullamco cillum proident aliqua nostrud. Labore ea veniam cillum duis veniam in
cupidatat voluptate eu officia. Ut laborum sunt nostrud magna. Ex magna esse cillum enim
incididunt pariatur qui veniam dolor. Exercitation id culpa et enim mollit duis duis
aliquip. Magna ullamco est cupidatat laboris irure pariatur aliquip duis aute cillum.
Officia irure do laboris ea nisi sunt reprehenderit laboris irure. Ex eiusmod in duis
veniam excepteur. Sunt et et laboris culpa. Mollit excepteur occaecat elit anim officia.
Laborum commodo proident cupidatat pariatur eu veniam id qui do culpa. Quis consectetur
adipisicing anim ex ea velit excepteur. Deserunt laboris ex aute sunt laborum tempor ea
enim dolore ut in. Id aliqua Lorem exercitation qui velit nostrud anim reprehenderit enim.
Nisi elit fugiat deserunt elit. Sit excepteur ipsum enim excepteur irure irure sint veniam
elit consequat ea id. Lorem ea qui sunt enim occaecat excepteur officia ex consequat
nostrud. Tempor sint Lorem est culpa do.
</AccordionItem>
<AccordionItem key="2" aria-label="Accordion 2" title="Accordion 2">
Non est aliqua tempor occaecat laborum. Lorem culpa minim irure mollit. Est qui
reprehenderit commodo magna proident anim ipsum ex. Mollit id amet officia nisi excepteur
eu. Commodo elit commodo nisi nisi aute eu aliquip aliquip voluptate exercitation ullamco
ipsum eiusmod veniam. Magna in laborum anim amet anim ex elit aliqua nostrud mollit.
Pariatur ullamco cillum proident aliqua nostrud. Labore ea veniam cillum duis veniam in
cupidatat voluptate eu officia. Ut laborum sunt nostrud magna. Ex magna esse cillum enim
</AccordionItem>
<AccordionItem key="3" aria-label="Accordion 3" title="Accordion 3">
Non est aliqua tempor occaecat laborum. Lorem culpa minim irure mollit. Est qui
reprehenderit commodo magna proident anim ipsum ex. Mollit id amet officia nisi excepteur
eu. Commodo elit commodo nisi nisi aute eu aliquip aliquip voluptate exercitation ullamco
ipsum eiusmod veniam. Magna in laborum anim amet anim ex elit aliqua nostrud mollit.
Pariatur ullamco cillum proident aliqua nostrud. Labore ea veniam cillum duis veniam in
</AccordionItem>
</Accordion>
<Tabs classNames={{panel: "flex flex-col gap-5"}} variant="underlined">
<Tab title="Test 1">
<Textarea defaultValue="ASdasd" label="Default value (uncontrolled)" />
<Input label="Text B Tab 1" value={textB} onValueChange={setTextB} />
<Textarea label="Text C Tab 1" value={textC} onValueChange={setTextC} />
</Tab>
<Tab title="Test 2">
<Textarea label="Text B Tab 2" value={textB} onValueChange={setTextB} />
<Textarea label="Text C Tab 2" value={textC} onValueChange={setTextC} />
</Tab>
<Tab title="Test 3">
<Textarea label="Text B Tab 3" value={textB} onValueChange={setTextB} />
<Textarea label="Text C Tab 3" value={textC} onValueChange={setTextC} />
</Tab>
</Tabs>
<h2>Outside</h2>
<Textarea label="Text A" placeholder="Text A" value={textA} onValueChange={setTextA} />
<Textarea label="Text B" placeholder="Text B" value={textB} onValueChange={setTextB} />
<Textarea label="Text C" placeholder="Text C" value={textC} onValueChange={setTextC} />
<MyRadioGroup />
<MyInput
isClearable
placeholder="Search..."
radius="md"
size="md"
startContent={<SearchLinearIcon className="text-zinc-500" size={16} />}
/>
<Button>Click Me!</Button>
<MyButton2 color="primary">Press Me!</MyButton2>
<Pagination
showControls
initialPage={page ?? 1}
renderItem={({page, ...itemProps}) => {
return <PaginationItem href={`/examples/perf?page=${page}`} {...itemProps} />;
}}
total={10}
/>
</div>
);
}
-11
View File
@@ -1,11 +0,0 @@
import {Suspense} from "react";
import ClientPage from "./client-page";
export default function PerfPage() {
return (
<Suspense fallback={<div>Loading...</div>}>
<ClientPage />
</Suspense>
);
}
@@ -1,113 +0,0 @@
/* eslint-disable no-console */
"use client";
import {Select, SelectItem} from "@heroui/react";
import {useEffect, useState} from "react";
import {useInfiniteScroll} from "@heroui/use-infinite-scroll";
type Pokemon = {
name: string;
url: string;
};
type UsePokemonListProps = {
/** Delay to wait before fetching more items */
fetchDelay?: number;
};
function usePokemonList({fetchDelay = 0}: UsePokemonListProps = {}) {
const [items, setItems] = useState<Pokemon[]>([]);
const [hasMore, setHasMore] = useState(true);
const [isLoading, setIsLoading] = useState(false);
const [offset, setOffset] = useState(0);
const limit = 10; // Number of items per page, adjust as necessary
const loadPokemon = async (currentOffset: number) => {
const controller = new AbortController();
const {signal} = controller;
try {
setIsLoading(true);
if (offset > 0) {
// Delay to simulate network latency
await new Promise((resolve) => setTimeout(resolve, fetchDelay));
}
let res = await fetch(
`https://pokeapi.co/api/v2/pokemon?offset=${currentOffset}&limit=${limit}`,
{signal},
);
if (!res.ok) {
throw new Error("Network response was not ok");
}
let json = await res.json();
setHasMore(json.next !== null);
// Append new results to existing ones
setItems((prevItems) => [...prevItems, ...json.results]);
} catch (error) {
// @ts-ignore
if (error.name === "AbortError") {
console.log("Fetch aborted");
} else {
console.error("There was an error with the fetch operation:", error);
}
} finally {
setIsLoading(false);
}
};
useEffect(() => {
loadPokemon(offset);
}, []);
const onLoadMore = () => {
const newOffset = offset + limit;
setOffset(newOffset);
loadPokemon(newOffset);
};
return {
items,
hasMore,
isLoading,
onLoadMore,
};
}
export default function Page() {
const [isOpen, setIsOpen] = useState(false);
const {items, hasMore, isLoading, onLoadMore} = usePokemonList({fetchDelay: 1500});
const [, scrollerRef] = useInfiniteScroll({
hasMore,
isEnabled: isOpen,
shouldUseLoader: false, // We don't want to show the loader at the bottom of the list
onLoadMore,
});
return (
<div className="p-6">
<Select
className="max-w-xs"
isLoading={isLoading}
items={items}
label="Pick a Pokemon"
placeholder="Select a Pokemon"
scrollRef={scrollerRef}
selectionMode="single"
onOpenChange={setIsOpen}
>
{(item) => (
<SelectItem key={item.name} className="capitalize">
{item.name}
</SelectItem>
)}
</Select>
</div>
);
}
@@ -1,85 +0,0 @@
"use client";
import {
Table,
TableHeader,
TableColumn,
TableBody,
TableRow,
TableCell,
getKeyValue,
Spinner,
Pagination,
} from "@heroui/react";
import {useMemo, useState} from "react";
import useSWR from "swr";
type SWCharacter = {
name: string;
height: string;
mass: string;
birth_year: string;
};
const fetcher = (...args: Parameters<typeof fetch>) => fetch(...args).then((res) => res.json());
export default function Page() {
const [page, setPage] = useState(1);
const {data, isLoading} = useSWR<{
count: number;
results: SWCharacter[];
}>(`https://swapi.py4e.com/api/people?page=${page}`, fetcher, {
keepPreviousData: true,
});
const rowsPerPage = 10;
const pages = useMemo(() => {
return data?.count ? Math.ceil(data.count / rowsPerPage) : 0;
}, [data?.count, rowsPerPage]);
return (
<div className="p-6">
<Table
aria-label="Example table with client async pagination"
bottomContent={
pages > 0 ? (
<div className="flex w-full justify-center">
<Pagination
isCompact
showControls
showShadow
color="primary"
page={page}
total={pages}
onChange={(page) => setPage(page)}
/>
</div>
) : null
}
classNames={{
table: "min-h-[400px]",
}}
>
<TableHeader>
<TableColumn key="name">Name</TableColumn>
<TableColumn key="height">Height</TableColumn>
<TableColumn key="mass">Mass</TableColumn>
<TableColumn key="birth_year">Birth year</TableColumn>
</TableHeader>
<TableBody
isLoading={isLoading || data?.results.length === 0}
items={data?.results ?? []}
loadingContent={<Spinner />}
>
{(item) => (
<TableRow key={item.name}>
{(columnKey) => <TableCell>{getKeyValue(item, columnKey)}</TableCell>}
</TableRow>
)}
</TableBody>
</Table>
</div>
);
}
@@ -1,579 +0,0 @@
/* eslint-disable jsx-a11y/no-onchange */
"use client";
import {
Table,
TableHeader,
TableColumn,
TableBody,
TableRow,
TableCell,
Input,
Button,
DropdownTrigger,
Dropdown,
DropdownMenu,
DropdownItem,
Selection,
Chip,
User,
ChipProps,
Pagination,
SortDescriptor,
} from "@heroui/react";
import {ChevronDownIcon, SearchIcon} from "@heroui/shared-icons";
import {useCallback, useMemo, useState} from "react";
import {capitalize} from "@heroui/shared-utils";
import {PlusLinearIcon} from "@/components/icons";
import {VerticalDotsIcon} from "@/components/icons/vertical-dots";
const statusColorMap: Record<string, ChipProps["color"]> = {
active: "success",
paused: "danger",
vacation: "warning",
};
const columns = [
{name: "ID", uid: "id", sortable: true},
{name: "NAME", uid: "name", sortable: true},
{name: "AGE", uid: "age", sortable: true},
{name: "ROLE", uid: "role", sortable: true},
{name: "TEAM", uid: "team"},
{name: "EMAIL", uid: "email"},
{name: "STATUS", uid: "status", sortable: true},
{name: "ACTIONS", uid: "actions"},
];
const statusOptions = [
{name: "Active", uid: "active"},
{name: "Paused", uid: "paused"},
{name: "Vacation", uid: "vacation"},
];
const INITIAL_VISIBLE_COLUMNS = ["name", "role", "status", "actions"];
const users = [
{
id: 1,
name: "Tony Reichert",
role: "CEO",
team: "Management",
status: "active",
age: "29",
avatar: "https://i.pravatar.cc/150?u=a042581f4e29026024d",
email: "tony.reichert@example.com",
},
{
id: 2,
name: "Zoey Lang",
role: "Tech Lead",
team: "Development",
status: "paused",
age: "25",
avatar: "https://i.pravatar.cc/150?u=a042581f4e29026704d",
email: "zoey.lang@example.com",
},
{
id: 3,
name: "Jane Fisher",
role: "Sr. Dev",
team: "Development",
status: "active",
age: "22",
avatar: "https://i.pravatar.cc/150?u=a04258114e29026702d",
email: "jane.fisher@example.com",
},
{
id: 4,
name: "William Howard",
role: "C.M.",
team: "Marketing",
status: "vacation",
age: "28",
avatar: "https://i.pravatar.cc/150?u=a048581f4e29026701d",
email: "william.howard@example.com",
},
{
id: 5,
name: "Kristen Copper",
role: "S. Manager",
team: "Sales",
status: "active",
age: "24",
avatar: "https://i.pravatar.cc/150?u=a092581d4ef9026700d",
email: "kristen.cooper@example.com",
},
{
id: 6,
name: "Brian Kim",
role: "P. Manager",
team: "Management",
age: "29",
avatar: "https://i.pravatar.cc/150?u=a042581f4e29026024d",
email: "brian.kim@example.com",
status: "active",
},
{
id: 7,
name: "Michael Hunt",
role: "Designer",
team: "Design",
status: "paused",
age: "27",
avatar: "https://i.pravatar.cc/150?u=a042581f4e29027007d",
email: "michael.hunt@example.com",
},
{
id: 8,
name: "Samantha Brooks",
role: "HR Manager",
team: "HR",
status: "active",
age: "31",
avatar: "https://i.pravatar.cc/150?u=a042581f4e27027008d",
email: "samantha.brooks@example.com",
},
{
id: 9,
name: "Frank Harrison",
role: "F. Manager",
team: "Finance",
status: "vacation",
age: "33",
avatar: "https://i.pravatar.cc/150?img=4",
email: "frank.harrison@example.com",
},
{
id: 10,
name: "Emma Adams",
role: "Ops Manager",
team: "Operations",
status: "active",
age: "35",
avatar: "https://i.pravatar.cc/150?img=5",
email: "emma.adams@example.com",
},
{
id: 11,
name: "Brandon Stevens",
role: "Jr. Dev",
team: "Development",
status: "active",
age: "22",
avatar: "https://i.pravatar.cc/150?img=8",
email: "brandon.stevens@example.com",
},
{
id: 12,
name: "Megan Richards",
role: "P. Manager",
team: "Product",
status: "paused",
age: "28",
avatar: "https://i.pravatar.cc/150?img=10",
email: "megan.richards@example.com",
},
{
id: 13,
name: "Oliver Scott",
role: "S. Manager",
team: "Security",
status: "active",
age: "37",
avatar: "https://i.pravatar.cc/150?img=12",
email: "oliver.scott@example.com",
},
{
id: 14,
name: "Grace Allen",
role: "M. Specialist",
team: "Marketing",
status: "active",
age: "30",
avatar: "https://i.pravatar.cc/150?img=16",
email: "grace.allen@example.com",
},
{
id: 15,
name: "Noah Carter",
role: "IT Specialist",
team: "I. Technology",
status: "paused",
age: "31",
avatar: "https://i.pravatar.cc/150?img=15",
email: "noah.carter@example.com",
},
{
id: 16,
name: "Ava Perez",
role: "Manager",
team: "Sales",
status: "active",
age: "29",
avatar: "https://i.pravatar.cc/150?img=20",
email: "ava.perez@example.com",
},
{
id: 17,
name: "Liam Johnson",
role: "Data Analyst",
team: "Analysis",
status: "active",
age: "28",
avatar: "https://i.pravatar.cc/150?img=33",
email: "liam.johnson@example.com",
},
{
id: 18,
name: "Sophia Taylor",
role: "QA Analyst",
team: "Testing",
status: "active",
age: "27",
avatar: "https://i.pravatar.cc/150?img=29",
email: "sophia.taylor@example.com",
},
{
id: 19,
name: "Lucas Harris",
role: "Administrator",
team: "Information Technology",
status: "paused",
age: "32",
avatar: "https://i.pravatar.cc/150?img=50",
email: "lucas.harris@example.com",
},
{
id: 20,
name: "Mia Robinson",
role: "Coordinator",
team: "Operations",
status: "active",
age: "26",
avatar: "https://i.pravatar.cc/150?img=45",
email: "mia.robinson@example.com",
},
];
type User = (typeof users)[number];
export default function Page() {
const [filterValue, setFilterValue] = useState("");
const [selectedKeys, setSelectedKeys] = useState<Selection>(new Set([]));
const [visibleColumns, setVisibleColumns] = useState<Selection>(new Set(INITIAL_VISIBLE_COLUMNS));
const [statusFilter, setStatusFilter] = useState<Selection>("all");
const [rowsPerPage, setRowsPerPage] = useState(5);
const [sortDescriptor, setSortDescriptor] = useState<SortDescriptor>({
column: "age",
direction: "ascending",
});
const [page, setPage] = useState(1);
const pages = Math.ceil(users.length / rowsPerPage);
const hasSearchFilter = Boolean(filterValue);
const headerColumns = useMemo(() => {
if (visibleColumns === "all") return columns;
return columns.filter((column) => Array.from(visibleColumns).includes(column.uid));
}, [visibleColumns]);
const filteredItems = useMemo(() => {
let filteredUsers = [...users];
if (hasSearchFilter) {
filteredUsers = filteredUsers.filter((user) =>
user.name.toLowerCase().includes(filterValue.toLowerCase()),
);
}
if (statusFilter !== "all" && Array.from(statusFilter).length !== statusOptions.length) {
filteredUsers = filteredUsers.filter((user) =>
Array.from(statusFilter).includes(user.status),
);
}
return filteredUsers;
}, [users, filterValue, statusFilter]);
const items = useMemo(() => {
const start = (page - 1) * rowsPerPage;
const end = start + rowsPerPage;
return filteredItems.slice(start, end);
}, [page, filteredItems, rowsPerPage]);
const sortedItems = useMemo(() => {
return [...items].sort((a: User, b: User) => {
const first = a[sortDescriptor.column as keyof User] as number;
const second = b[sortDescriptor.column as keyof User] as number;
const cmp = first < second ? -1 : first > second ? 1 : 0;
return sortDescriptor.direction === "descending" ? -cmp : cmp;
});
}, [sortDescriptor, items]);
const renderCell = useCallback((user: User, columnKey: React.Key) => {
const cellValue = user[columnKey as keyof User];
switch (columnKey) {
case "name":
return (
<User
avatarProps={{radius: "full", size: "sm", src: user.avatar}}
classNames={{
description: "text-default-500",
}}
description={user.email}
name={cellValue}
>
{user.email}
</User>
);
case "role":
return (
<div className="flex flex-col">
<p className="text-bold text-small capitalize">{cellValue}</p>
<p className="text-bold text-tiny capitalize text-default-500">{user.team}</p>
</div>
);
case "status":
return (
<Chip
className="capitalize border-none gap-1 text-default-600"
color={statusColorMap[user.status]}
size="sm"
variant="dot"
>
{cellValue}
</Chip>
);
case "actions":
return (
<div className="relative flex justify-end items-center gap-2">
<Dropdown className="bg-background border-1 border-default-200">
<DropdownTrigger>
<Button isIconOnly radius="full" size="sm" variant="light">
<VerticalDotsIcon className="text-default-400" />
</Button>
</DropdownTrigger>
<DropdownMenu>
<DropdownItem key="view">View</DropdownItem>
<DropdownItem key="edit">Edit</DropdownItem>
<DropdownItem key="delete">Delete</DropdownItem>
</DropdownMenu>
</Dropdown>
</div>
);
default:
return cellValue;
}
}, []);
const onRowsPerPageChange = useCallback((e: React.ChangeEvent<HTMLSelectElement>) => {
setRowsPerPage(Number(e.target.value));
setPage(1);
}, []);
const onSearchChange = useCallback((value?: string) => {
if (value) {
setFilterValue(value);
setPage(1);
} else {
setFilterValue("");
}
}, []);
const topContent = useMemo(() => {
return (
<div className="flex flex-col gap-4">
<div className="flex justify-between gap-3 items-end">
<Input
isClearable
classNames={{
base: "w-full sm:max-w-[44%]",
inputWrapper: "border-1",
}}
placeholder="Search by name..."
size="sm"
startContent={<SearchIcon className="text-default-300" />}
value={filterValue}
variant="bordered"
onClear={() => setFilterValue("")}
onValueChange={onSearchChange}
/>
<div className="flex gap-3">
<Dropdown>
<DropdownTrigger className="hidden sm:flex">
<Button
endContent={<ChevronDownIcon className="text-small" />}
size="sm"
variant="flat"
>
Status
</Button>
</DropdownTrigger>
<DropdownMenu
disallowEmptySelection
aria-label="Table Columns"
closeOnSelect={false}
selectedKeys={statusFilter}
selectionMode="multiple"
onSelectionChange={setStatusFilter}
>
{statusOptions.map((status) => (
<DropdownItem key={status.uid} className="capitalize">
{capitalize(status.name)}
</DropdownItem>
))}
</DropdownMenu>
</Dropdown>
<Dropdown>
<DropdownTrigger className="hidden sm:flex">
<Button
endContent={<ChevronDownIcon className="text-small" />}
size="sm"
variant="flat"
>
Columns
</Button>
</DropdownTrigger>
<DropdownMenu
disallowEmptySelection
aria-label="Table Columns"
closeOnSelect={false}
selectedKeys={visibleColumns}
selectionMode="multiple"
onSelectionChange={setVisibleColumns}
>
{columns.map((column) => (
<DropdownItem key={column.uid} className="capitalize">
{capitalize(column.name)}
</DropdownItem>
))}
</DropdownMenu>
</Dropdown>
<Button
className="bg-foreground text-background"
endContent={<PlusLinearIcon />}
size="sm"
>
Add New
</Button>
</div>
</div>
<div className="flex justify-between items-center">
<span className="text-default-400 text-small">Total {users.length} users</span>
<label className="flex items-center text-default-400 text-small">
Rows per page:
<select
className="bg-transparent outline-none text-default-400 text-small"
onChange={onRowsPerPageChange}
>
<option value="5">5</option>
<option value="10">10</option>
<option value="15">15</option>
</select>
</label>
</div>
</div>
);
}, [
filterValue,
statusFilter,
visibleColumns,
onSearchChange,
onRowsPerPageChange,
users.length,
hasSearchFilter,
]);
const bottomContent = useMemo(() => {
return (
<div className="py-2 px-2 flex justify-between items-center">
<Pagination
showControls
classNames={{
cursor: "bg-foreground text-background",
}}
color="default"
isDisabled={hasSearchFilter}
page={page}
total={pages}
variant="light"
onChange={setPage}
/>
<span className="text-small text-default-400">
{selectedKeys === "all"
? "All items selected"
: `${selectedKeys.size} of ${items.length} selected`}
</span>
</div>
);
}, [selectedKeys, items.length, page, pages, hasSearchFilter]);
const classNames = useMemo(
() => ({
wrapper: ["max-h-[382px]", "max-w-3xl"],
th: ["bg-transparent", "text-default-500", "border-b", "border-divider"],
td: [
// changing the rows border radius
// first
"group-data-[first=true]:first:before:rounded-none",
"group-data-[first=true]:last:before:rounded-none",
// middle
"group-data-[middle=true]:before:rounded-none",
// last
"group-data-[last=true]:first:before:rounded-none",
"group-data-[last=true]:last:before:rounded-none",
],
}),
[],
);
return (
<div className="p-6">
<Table
isCompact
removeWrapper
aria-label="Example table with custom cells, pagination and sorting"
bottomContent={bottomContent}
bottomContentPlacement="outside"
checkboxesProps={{
classNames: {
wrapper: "after:bg-foreground after:text-background text-background",
},
}}
classNames={classNames}
selectedKeys={selectedKeys}
selectionMode="multiple"
sortDescriptor={sortDescriptor}
topContent={topContent}
topContentPlacement="outside"
onSelectionChange={setSelectedKeys}
onSortChange={setSortDescriptor}
>
<TableHeader columns={headerColumns}>
{(column) => (
<TableColumn
key={column.uid}
align={column.uid === "actions" ? "center" : "start"}
allowsSorting={column.sortable}
>
{column.name}
</TableColumn>
)}
</TableHeader>
<TableBody emptyContent={"No users found"} items={sortedItems}>
{(item) => (
<TableRow key={item.id}>
{(columnKey) => <TableCell>{renderCell(item, columnKey)}</TableCell>}
</TableRow>
)}
</TableBody>
</Table>
</div>
);
}
@@ -1,87 +0,0 @@
"use client";
import {
Table,
TableHeader,
TableColumn,
TableBody,
TableRow,
TableCell,
getKeyValue,
Spinner,
} from "@heroui/react";
import {useAsyncList} from "@react-stately/data";
import {useInfiniteScroll} from "@heroui/use-infinite-scroll";
import {useState} from "react";
type SWCharacter = {
name: string;
height: string;
mass: string;
birth_year: string;
};
export default function Page() {
const [isLoading, setIsLoading] = useState(true);
const [hasMore, setHasMore] = useState(false);
let list = useAsyncList<SWCharacter>({
async load({signal, cursor}) {
if (cursor) {
setIsLoading(false);
}
// If no cursor is available, then we're loading the first page.
// Otherwise, the cursor is the next URL to load, as returned from the previous page.
const res = await fetch(cursor || "https://swapi.py4e.com/api/people/?search=", {signal});
let json = await res.json();
setHasMore(json.next !== null);
return {
items: json.results,
cursor: json.next,
};
},
});
const [loaderRef, scrollerRef] = useInfiniteScroll({hasMore, onLoadMore: list.loadMore});
return (
<div className="p-6">
<Table
isHeaderSticky
aria-label="Example table with infinite pagination"
baseRef={scrollerRef}
bottomContent={
hasMore ? (
<div className="flex w-full justify-center">
<Spinner ref={loaderRef} color="white" />
</div>
) : null
}
classNames={{
base: "max-h-[520px] overflow-scroll",
table: "min-h-[400px]",
}}
>
<TableHeader>
<TableColumn key="name">Name</TableColumn>
<TableColumn key="height">Height</TableColumn>
<TableColumn key="mass">Mass</TableColumn>
<TableColumn key="birth_year">Birth year</TableColumn>
</TableHeader>
<TableBody
isLoading={isLoading}
items={list.items}
loadingContent={<Spinner color="white" />}
>
{(item) => (
<TableRow key={item.name}>
{(columnKey) => <TableCell>{getKeyValue(item, columnKey)}</TableCell>}
</TableRow>
)}
</TableBody>
</Table>
</div>
);
}
@@ -1,92 +0,0 @@
"use client";
import {
Table,
TableHeader,
TableColumn,
TableBody,
TableRow,
TableCell,
getKeyValue,
Spinner,
Button,
} from "@heroui/react";
import {useAsyncList} from "@react-stately/data";
import {useState} from "react";
type SWCharacter = {
name: string;
height: string;
mass: string;
birth_year: string;
};
export default function Page() {
const [page, setPage] = useState(1);
const [isLoading, setIsLoading] = useState(true);
let list = useAsyncList<SWCharacter>({
async load({signal, cursor}) {
if (cursor) {
setPage((prev) => prev + 1);
}
// If no cursor is available, then we're loading the first page.
// Otherwise, the cursor is the next URL to load, as returned from the previous page.
const res = await fetch(cursor || "https://swapi.py4e.com/api/people/?search=", {signal});
let json = await res.json();
if (!cursor) {
setIsLoading(false);
}
return {
items: json.results,
cursor: json.next,
};
},
});
const hasMore = page < 9;
return (
<div className="p-6">
<Table
isHeaderSticky
aria-label="Example table with client side sorting"
bottomContent={
hasMore && !isLoading ? (
<div className="flex w-full justify-center">
<Button isDisabled={list.isLoading} variant="flat" onPress={list.loadMore}>
{list.isLoading && <Spinner color="white" size="sm" />}
Load More
</Button>
</div>
) : null
}
classNames={{
base: "max-h-[520px] overflow-scroll",
table: "min-h-[420px]",
}}
>
<TableHeader>
<TableColumn key="name">Name</TableColumn>
<TableColumn key="height">Height</TableColumn>
<TableColumn key="mass">Mass</TableColumn>
<TableColumn key="birth_year">Birth year</TableColumn>
</TableHeader>
<TableBody
isLoading={isLoading}
items={list.items}
loadingContent={<Spinner label="Loading..." />}
>
{(item) => (
<TableRow key={item.name}>
{(columnKey) => <TableCell>{getKeyValue(item, columnKey)}</TableCell>}
</TableRow>
)}
</TableBody>
</Table>
</div>
);
}
@@ -1,94 +0,0 @@
"use client";
import {
Table,
TableHeader,
TableColumn,
TableBody,
TableRow,
TableCell,
getKeyValue,
Spinner,
} from "@heroui/react";
import {useAsyncList} from "@react-stately/data";
import {useState} from "react";
type SWCharacter = {
name: string;
height: string;
mass: string;
birth_year: string;
};
export default function Page() {
const [isLoading, setIsLoading] = useState(true);
let list = useAsyncList<SWCharacter>({
async load({signal}) {
let res = await fetch(`https://swapi.py4e.com/api/people/?search`, {
signal,
});
let json = await res.json();
setIsLoading(false);
return {
items: json.results,
};
},
async sort({items, sortDescriptor}) {
return {
items: items.sort((a, b) => {
let first = a[sortDescriptor.column as keyof SWCharacter];
let second = b[sortDescriptor.column as keyof SWCharacter];
let cmp = (parseInt(first) || first) < (parseInt(second) || second) ? -1 : 1;
if (sortDescriptor.direction === "descending") {
cmp *= -1;
}
return cmp;
}),
};
},
});
return (
<div className="p-6">
<Table
aria-label="Example table with client side sorting"
classNames={{
table: "min-h-[400px]",
}}
sortDescriptor={list.sortDescriptor}
onSortChange={list.sort}
>
<TableHeader>
<TableColumn key="name" allowsSorting>
Name
</TableColumn>
<TableColumn key="height" allowsSorting>
Height
</TableColumn>
<TableColumn key="mass" allowsSorting>
Mass
</TableColumn>
<TableColumn key="birth_year" allowsSorting>
Birth year
</TableColumn>
</TableHeader>
<TableBody
isLoading={isLoading}
items={list.items}
loadingContent={<Spinner label="Loading..." />}
>
{(item) => (
<TableRow key={item.name}>
{(columnKey) => <TableCell>{getKeyValue(item, columnKey)}</TableCell>}
</TableRow>
)}
</TableBody>
</Table>
</div>
);
}
@@ -1,553 +0,0 @@
/* eslint-disable jsx-a11y/no-onchange */
"use client";
import {
Table,
TableHeader,
TableColumn,
TableBody,
TableRow,
TableCell,
Input,
Button,
DropdownTrigger,
Dropdown,
DropdownMenu,
DropdownItem,
Selection,
Chip,
User,
ChipProps,
Pagination,
SortDescriptor,
} from "@heroui/react";
import {ChevronDownIcon, SearchIcon} from "@heroui/shared-icons";
import {useCallback, useMemo, useState} from "react";
import {capitalize} from "@heroui/shared-utils";
import {PlusLinearIcon} from "@/components/icons";
import {VerticalDotsIcon} from "@/components/icons/vertical-dots";
const statusColorMap: Record<string, ChipProps["color"]> = {
active: "success",
paused: "danger",
vacation: "warning",
};
const columns = [
{name: "ID", uid: "id", sortable: true},
{name: "NAME", uid: "name", sortable: true},
{name: "AGE", uid: "age", sortable: true},
{name: "ROLE", uid: "role", sortable: true},
{name: "TEAM", uid: "team"},
{name: "EMAIL", uid: "email"},
{name: "STATUS", uid: "status", sortable: true},
{name: "ACTIONS", uid: "actions"},
];
const statusOptions = [
{name: "Active", uid: "active"},
{name: "Paused", uid: "paused"},
{name: "Vacation", uid: "vacation"},
];
const INITIAL_VISIBLE_COLUMNS = ["name", "role", "status", "actions"];
const users = [
{
id: 1,
name: "Tony Reichert",
role: "CEO",
team: "Management",
status: "active",
age: "29",
avatar: "https://i.pravatar.cc/150?u=a042581f4e29026024d",
email: "tony.reichert@example.com",
},
{
id: 2,
name: "Zoey Lang",
role: "Tech Lead",
team: "Development",
status: "paused",
age: "25",
avatar: "https://i.pravatar.cc/150?u=a042581f4e29026704d",
email: "zoey.lang@example.com",
},
{
id: 3,
name: "Jane Fisher",
role: "Sr. Dev",
team: "Development",
status: "active",
age: "22",
avatar: "https://i.pravatar.cc/150?u=a04258114e29026702d",
email: "jane.fisher@example.com",
},
{
id: 4,
name: "William Howard",
role: "C.M.",
team: "Marketing",
status: "vacation",
age: "28",
avatar: "https://i.pravatar.cc/150?u=a048581f4e29026701d",
email: "william.howard@example.com",
},
{
id: 5,
name: "Kristen Copper",
role: "S. Manager",
team: "Sales",
status: "active",
age: "24",
avatar: "https://i.pravatar.cc/150?u=a092581d4ef9026700d",
email: "kristen.cooper@example.com",
},
{
id: 6,
name: "Brian Kim",
role: "P. Manager",
team: "Management",
age: "29",
avatar: "https://i.pravatar.cc/150?u=a042581f4e29026024d",
email: "brian.kim@example.com",
status: "active",
},
{
id: 7,
name: "Michael Hunt",
role: "Designer",
team: "Design",
status: "paused",
age: "27",
avatar: "https://i.pravatar.cc/150?u=a042581f4e29027007d",
email: "michael.hunt@example.com",
},
{
id: 8,
name: "Samantha Brooks",
role: "HR Manager",
team: "HR",
status: "active",
age: "31",
avatar: "https://i.pravatar.cc/150?u=a042581f4e27027008d",
email: "samantha.brooks@example.com",
},
{
id: 9,
name: "Frank Harrison",
role: "F. Manager",
team: "Finance",
status: "vacation",
age: "33",
avatar: "https://i.pravatar.cc/150?img=4",
email: "frank.harrison@example.com",
},
{
id: 10,
name: "Emma Adams",
role: "Ops Manager",
team: "Operations",
status: "active",
age: "35",
avatar: "https://i.pravatar.cc/150?img=5",
email: "emma.adams@example.com",
},
{
id: 11,
name: "Brandon Stevens",
role: "Jr. Dev",
team: "Development",
status: "active",
age: "22",
avatar: "https://i.pravatar.cc/150?img=8",
email: "brandon.stevens@example.com",
},
{
id: 12,
name: "Megan Richards",
role: "P. Manager",
team: "Product",
status: "paused",
age: "28",
avatar: "https://i.pravatar.cc/150?img=10",
email: "megan.richards@example.com",
},
{
id: 13,
name: "Oliver Scott",
role: "S. Manager",
team: "Security",
status: "active",
age: "37",
avatar: "https://i.pravatar.cc/150?img=12",
email: "oliver.scott@example.com",
},
{
id: 14,
name: "Grace Allen",
role: "M. Specialist",
team: "Marketing",
status: "active",
age: "30",
avatar: "https://i.pravatar.cc/150?img=16",
email: "grace.allen@example.com",
},
{
id: 15,
name: "Noah Carter",
role: "IT Specialist",
team: "I. Technology",
status: "paused",
age: "31",
avatar: "https://i.pravatar.cc/150?img=15",
email: "noah.carter@example.com",
},
{
id: 16,
name: "Ava Perez",
role: "Manager",
team: "Sales",
status: "active",
age: "29",
avatar: "https://i.pravatar.cc/150?img=20",
email: "ava.perez@example.com",
},
{
id: 17,
name: "Liam Johnson",
role: "Data Analyst",
team: "Analysis",
status: "active",
age: "28",
avatar: "https://i.pravatar.cc/150?img=33",
email: "liam.johnson@example.com",
},
{
id: 18,
name: "Sophia Taylor",
role: "QA Analyst",
team: "Testing",
status: "active",
age: "27",
avatar: "https://i.pravatar.cc/150?img=29",
email: "sophia.taylor@example.com",
},
{
id: 19,
name: "Lucas Harris",
role: "Administrator",
team: "Information Technology",
status: "paused",
age: "32",
avatar: "https://i.pravatar.cc/150?img=50",
email: "lucas.harris@example.com",
},
{
id: 20,
name: "Mia Robinson",
role: "Coordinator",
team: "Operations",
status: "active",
age: "26",
avatar: "https://i.pravatar.cc/150?img=45",
email: "mia.robinson@example.com",
},
];
type User = (typeof users)[number];
export default function Page() {
const [filterValue, setFilterValue] = useState("");
const [selectedKeys, setSelectedKeys] = useState<Selection>(new Set([]));
const [visibleColumns, setVisibleColumns] = useState<Selection>(new Set(INITIAL_VISIBLE_COLUMNS));
const [statusFilter, setStatusFilter] = useState<Selection>("all");
const [rowsPerPage, setRowsPerPage] = useState(5);
const [sortDescriptor, setSortDescriptor] = useState<SortDescriptor>({
column: "age",
direction: "ascending",
});
const [page, setPage] = useState(1);
const hasSearchFilter = Boolean(filterValue);
const headerColumns = useMemo(() => {
if (visibleColumns === "all") return columns;
return columns.filter((column) => Array.from(visibleColumns).includes(column.uid));
}, [visibleColumns]);
const filteredItems = useMemo(() => {
let filteredUsers = [...users];
if (hasSearchFilter) {
filteredUsers = filteredUsers.filter((user) =>
user.name.toLowerCase().includes(filterValue.toLowerCase()),
);
}
if (statusFilter !== "all" && Array.from(statusFilter).length !== statusOptions.length) {
filteredUsers = filteredUsers.filter((user) =>
Array.from(statusFilter).includes(user.status),
);
}
return filteredUsers;
}, [users, filterValue, statusFilter]);
const pages = Math.ceil(filteredItems.length / rowsPerPage);
const items = useMemo(() => {
const start = (page - 1) * rowsPerPage;
const end = start + rowsPerPage;
return filteredItems.slice(start, end);
}, [page, filteredItems, rowsPerPage]);
const sortedItems = useMemo(() => {
return [...items].sort((a: User, b: User) => {
const first = a[sortDescriptor.column as keyof User] as number;
const second = b[sortDescriptor.column as keyof User] as number;
const cmp = first < second ? -1 : first > second ? 1 : 0;
return sortDescriptor.direction === "descending" ? -cmp : cmp;
});
}, [sortDescriptor, items]);
const renderCell = useCallback((user: User, columnKey: React.Key) => {
const cellValue = user[columnKey as keyof User];
switch (columnKey) {
case "name":
return (
<User
avatarProps={{radius: "lg", src: user.avatar}}
description={user.email}
name={cellValue}
>
{user.email}
</User>
);
case "role":
return (
<div className="flex flex-col">
<p className="text-bold text-small capitalize">{cellValue}</p>
<p className="text-bold text-tiny capitalize text-default-400">{user.team}</p>
</div>
);
case "status":
return (
<Chip className="capitalize" color={statusColorMap[user.status]} size="sm" variant="flat">
{cellValue}
</Chip>
);
case "actions":
return (
<div className="relative flex justify-end items-center gap-2">
<Dropdown>
<DropdownTrigger>
<Button isIconOnly size="sm" variant="light">
<VerticalDotsIcon className="text-default-300" />
</Button>
</DropdownTrigger>
<DropdownMenu>
<DropdownItem key="view">View</DropdownItem>
<DropdownItem key="edit">Edit</DropdownItem>
<DropdownItem key="delete">Delete</DropdownItem>
</DropdownMenu>
</Dropdown>
</div>
);
default:
return cellValue;
}
}, []);
const onNextPage = useCallback(() => {
if (page < pages) {
setPage(page + 1);
}
}, [page, pages]);
const onPreviousPage = useCallback(() => {
if (page > 1) {
setPage(page - 1);
}
}, [page]);
const onRowsPerPageChange = useCallback((e: React.ChangeEvent<HTMLSelectElement>) => {
setRowsPerPage(Number(e.target.value));
setPage(1);
}, []);
const onSearchChange = useCallback((value?: string) => {
if (value) {
setFilterValue(value);
setPage(1);
} else {
setFilterValue("");
}
}, []);
const onClear = useCallback(() => {
setFilterValue("");
setPage(1);
}, []);
const topContent = useMemo(() => {
return (
<div className="flex flex-col gap-4">
<div className="flex justify-between gap-3 items-end">
<Input
isClearable
className="w-full sm:max-w-[44%]"
placeholder="Search by name..."
startContent={<SearchIcon />}
value={filterValue}
onClear={() => onClear()}
onValueChange={onSearchChange}
/>
<div className="flex gap-3">
<Dropdown>
<DropdownTrigger className="hidden sm:flex">
<Button endContent={<ChevronDownIcon className="text-small" />} variant="flat">
Status
</Button>
</DropdownTrigger>
<DropdownMenu
disallowEmptySelection
aria-label="Table Columns"
closeOnSelect={false}
selectedKeys={statusFilter}
selectionMode="multiple"
onSelectionChange={setStatusFilter}
>
{statusOptions.map((status) => (
<DropdownItem key={status.uid} className="capitalize">
{capitalize(status.name)}
</DropdownItem>
))}
</DropdownMenu>
</Dropdown>
<Dropdown>
<DropdownTrigger className="hidden sm:flex">
<Button endContent={<ChevronDownIcon className="text-small" />} variant="flat">
Columns
</Button>
</DropdownTrigger>
<DropdownMenu
disallowEmptySelection
aria-label="Table Columns"
closeOnSelect={false}
selectedKeys={visibleColumns}
selectionMode="multiple"
onSelectionChange={setVisibleColumns}
>
{columns.map((column) => (
<DropdownItem key={column.uid} className="capitalize">
{capitalize(column.name)}
</DropdownItem>
))}
</DropdownMenu>
</Dropdown>
<Button color="primary" endContent={<PlusLinearIcon />}>
Add New
</Button>
</div>
</div>
<div className="flex justify-between items-center">
<span className="text-default-400 text-small">Total {users.length} users</span>
<label className="flex items-center text-default-400 text-small">
Rows per page:
<select
className="bg-transparent outline-none text-default-400 text-small"
onChange={onRowsPerPageChange}
>
<option value="5">5</option>
<option value="10">10</option>
<option value="15">15</option>
</select>
</label>
</div>
</div>
);
}, [
filterValue,
statusFilter,
visibleColumns,
onSearchChange,
onRowsPerPageChange,
users.length,
hasSearchFilter,
]);
const bottomContent = useMemo(() => {
return (
<div className="py-2 px-2 flex justify-between items-center">
<span className="w-[30%] text-small text-default-400">
{selectedKeys === "all"
? "All items selected"
: `${selectedKeys.size} of ${filteredItems.length} selected`}
</span>
<Pagination
isCompact
showControls
showShadow
color="primary"
page={page}
total={pages}
onChange={setPage}
/>
<div className="hidden sm:flex w-[30%] justify-end gap-2">
<Button isDisabled={pages === 1} size="sm" variant="flat" onPress={onPreviousPage}>
Previous
</Button>
<Button isDisabled={pages === 1} size="sm" variant="flat" onPress={onNextPage}>
Next
</Button>
</div>
</div>
);
}, [selectedKeys, items.length, page, pages, hasSearchFilter]);
return (
<div className="p-6">
<Table
isHeaderSticky
aria-label="Example table with custom cells, pagination and sorting"
bottomContent={bottomContent}
bottomContentPlacement="outside"
classNames={{
wrapper: "max-h-[382px]",
}}
selectedKeys={selectedKeys}
selectionMode="multiple"
sortDescriptor={sortDescriptor}
topContent={topContent}
topContentPlacement="outside"
onSelectionChange={setSelectedKeys}
onSortChange={setSortDescriptor}
>
<TableHeader columns={headerColumns}>
{(column) => (
<TableColumn
key={column.uid}
align={column.uid === "actions" ? "center" : "start"}
allowsSorting={column.sortable}
>
{column.name}
</TableColumn>
)}
</TableHeader>
<TableBody emptyContent={"No users found"} items={sortedItems}>
{(item) => (
<TableRow key={item.id}>
{(columnKey) => <TableCell>{renderCell(item, columnKey)}</TableCell>}
</TableRow>
)}
</TableBody>
</Table>
</div>
);
}
-38
View File
@@ -1,38 +0,0 @@
import Rss from "rss";
import {allBlogPosts} from "contentlayer2/generated";
import {siteConfig} from "@/config/site";
import {allCoreContent} from "@/libs/contentlayer";
export async function GET() {
const feed = new Rss({
title: siteConfig.name,
description: siteConfig.description,
feed_url: `${siteConfig.siteUrl}/feed.xml`,
site_url: siteConfig.siteUrl,
webMaster: `${siteConfig.author} <${siteConfig.email}>`,
managingEditor: `${siteConfig.author} <${siteConfig.email}>`,
language: "en-US",
});
allCoreContent(allBlogPosts).forEach((post) => {
const author = post.author ? post.author : siteConfig.author;
feed.item({
title: post.title,
description: post.description ?? "",
url: `${siteConfig.siteUrl}/blog/${post.slug}`,
guid: `${siteConfig.siteUrl}/blog/${post.slug}`,
date: post.date,
// @ts-ignore - name does exist
author: `${author.name} <${siteConfig.email}>`,
categories: post.tags ?? [],
});
});
return new Response(feed.xml(), {
headers: {
"Content-Type": "application/xml",
},
});
}
-96
View File
@@ -1,96 +0,0 @@
import "@/styles/globals.css";
import "@/styles/sandpack.css";
import {Metadata, Viewport} from "next";
import {clsx} from "@heroui/shared-utils";
import {Analytics} from "@vercel/analytics/next";
import {Providers} from "./providers";
import {Cmdk} from "@/components/cmdk";
import manifest from "@/config/routes.json";
import {siteConfig} from "@/config/site";
import {fonts} from "@/config/fonts";
import {Navbar} from "@/components/navbar";
import {Footer} from "@/components/footer";
import {HeroUIChatBanner} from "@/components/heroui-chat-banner";
export const metadata: Metadata = {
title: {
default: siteConfig.name,
template: `%s | ${siteConfig.name}`,
},
description: siteConfig.description,
keywords: [
"React",
"Next.js",
"NextUI",
"Tailwind CSS",
"HeroUI",
"React Aria",
"Server Components",
"React Components",
"UI Components",
"UI Kit",
"UI Library",
"UI Framework",
"UI Design System",
],
icons: {
icon: "/favicon.ico",
shortcut: "/favicon-32x32.png",
apple: "/apple-touch-icon.png",
},
manifest: "/manifest.json",
twitter: siteConfig.twitter,
openGraph: siteConfig.openGraph,
authors: [
{
name: "hero_ui",
url: "https://x.com/hero_ui",
},
],
creator: "heroui-inc",
alternates: {
canonical: "https://heroui.com",
types: {
"application/rss+xml": [{url: "https://heroui.com/feed.xml", title: "HeroUI RSS Feed"}],
},
},
};
export const viewport: Viewport = {
themeColor: [
{color: "#f4f4f5", media: "(prefers-color-scheme: light)"},
{color: "#111111", media: "(prefers-color-scheme: dark)"},
],
width: "device-width",
initialScale: 1,
maximumScale: 1,
userScalable: false,
};
export default function RootLayout({children}: {children: React.ReactNode}) {
return (
<html suppressHydrationWarning dir="ltr" lang="en">
<head />
<body
className={clsx(
"min-h-screen bg-background font-sans antialiased",
fonts.sans.variable,
fonts.mono.variable,
)}
>
<Providers themeProps={{attribute: "class", defaultTheme: "dark"}}>
<div className="relative flex flex-col" id="app-container">
<HeroUIChatBanner />
<Navbar mobileRoutes={manifest.mobileRoutes} routes={manifest.routes} />
{children}
<Analytics mode="production" />
<Footer />
</div>
<Cmdk />
</Providers>
</body>
</html>
);
}
-41
View File
@@ -1,41 +0,0 @@
import {Spacer} from "@heroui/spacer";
import {Suspense} from "react";
import {Hero} from "@/components/marketing/hero";
import {FeaturesGrid} from "@/components/marketing/features-grid";
import {CustomThemes} from "@/components/marketing/custom-themes";
import {A11yOtb} from "@/components/marketing/a11y-otb";
import {DarkMode} from "@/components/marketing/dark-mode";
import {Customization} from "@/components/marketing/customization";
import {LastButNotLeast} from "@/components/marketing/last-but-not-least";
import {InstallBanner} from "@/components/marketing/install-banner";
import {Community} from "@/components/marketing/community";
import Support from "@/components/marketing/support";
import landingContent from "@/content/landing";
import {Sponsors} from "@/components/marketing/sponsors";
import {HeroUIProSection} from "@/components/marketing/heroui-pro-section";
export default async function Home() {
return (
<main className="container mx-auto max-w-7xl px-6 flex-grow">
<section className="flex flex-col items-center justify-center">
<Hero />
<FeaturesGrid features={landingContent.topFeatures} />
<Sponsors />
<CustomThemes />
<A11yOtb />
<DarkMode />
<Customization />
<HeroUIProSection />
<LastButNotLeast />
<Suspense fallback={<div>Loading...</div>}>
<Support />
</Suspense>
<Spacer y={24} />
<InstallBanner />
<Community />
<Spacer y={24} />
</section>
</main>
);
}
-33
View File
@@ -1,33 +0,0 @@
import {Image} from "@heroui/react";
import PlaygroundTabs from "./playground-tabs";
export default function FigmaPage() {
return (
<>
<main className="prose prose-neutral relative container mx-auto max-w-3xl z-10 px-6 min-h-[calc(100vh_-_64px_-_108px)] mb-12 flex-grow">
<section className="w-full flex flex-col items-center mt-12 gap-6">
<div className="text-center">
<h1 className="mb-2">Playground</h1>
</div>
</section>
<section>
<h3 className="text-medium text-default-500">Components</h3>
<PlaygroundTabs />
</section>
</main>
<div
aria-hidden="true"
className="fixed hidden dark:md:block dark:opacity-70 -bottom-[40%] -left-[20%] z-0"
>
<Image removeWrapper alt="docs left background" src="/gradients/docs-left.png" />
</div>
<div
aria-hidden="true"
className="fixed hidden dark:md:block dark:opacity-70 -top-[80%] -right-[60%] 2xl:-top-[60%] 2xl:-right-[45%] z-0 rotate-12"
>
<Image removeWrapper alt="docs right background" src="/gradients/docs-right.png" />
</div>
</>
);
}
@@ -1,19 +0,0 @@
"use client";
import {Tab, Tabs} from "@heroui/react";
export default function BlocksTabs() {
return (
<Tabs
classNames={{
cursor: "dark:bg-default-100 bg-default-200",
}}
radius="full"
variant="light"
>
<Tab key="input" title="Inputs" />
<Tab key="button" title="Buttons" />
<Tab key="card" title="Cards" />
</Tabs>
);
}
-51
View File
@@ -1,51 +0,0 @@
"use client";
import * as React from "react";
import {HeroUIProvider} from "@heroui/react";
import {ThemeProvider as NextThemesProvider} from "next-themes";
import {ThemeProviderProps} from "next-themes";
import {useRouter} from "next/navigation";
import {useEffect} from "react";
import posthog from "posthog-js";
import {PostHogProvider} from "posthog-js/react";
import {__PROD__} from "@/utils";
export interface ProvidersProps {
children: React.ReactNode;
themeProps?: ThemeProviderProps;
}
const ProviderWrapper = ({children}: {children: React.ReactElement}) => {
useEffect(() => {
// Initialize PostHog only once when the app starts
if (typeof window !== "undefined" && __PROD__ && !posthog.isFeatureEnabled("capture")) {
posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY!, {
api_host: "/ingest",
person_profiles: "identified_only",
ui_host: process.env.NEXT_PUBLIC_POSTHOG_HOST,
capture_pageview: false,
capture_pageleave: false,
capture_heatmaps: false,
});
}
}, []);
if (__PROD__) {
return <PostHogProvider client={posthog}>{children}</PostHogProvider>;
}
return children;
};
export function Providers({children, themeProps}: ProvidersProps) {
const router = useRouter();
return (
<ProviderWrapper>
<HeroUIProvider navigate={router.push}>
<NextThemesProvider {...themeProps}>{children}</NextThemesProvider>
</HeroUIProvider>
</ProviderWrapper>
);
}
-9
View File
@@ -1,9 +0,0 @@
import {ThemeBuilder} from "@/components/themes";
export default function ThemesPage() {
return (
<div className="flex flex-col md:flex-row gap-6 w-full p-6 py-3 md:pr-[45vw] lg:pr-[30vw] justify-start mt-12 scrollbar-hide">
<ThemeBuilder />
</div>
);
}
@@ -1,118 +0,0 @@
"use client";
import React, {useCallback, useEffect} from "react";
import Script from "next/script";
import carbonOptimize from "./carbon-optimize";
import {loadScript} from "@/utils/scripts";
import {useIsMounted} from "@/hooks/use-is-mounted";
import {__PROD__, __ENABLE_ADS__} from "@/utils";
const EA_PROVIDER_RATIO = 0.85;
export const CarbonAd: React.FC<unknown> = () => {
const carbonRef = React.useRef(null);
const [showEthicalAds, setShowEthicalAds] = React.useState(false);
const isMounted = useIsMounted();
const loadEthicalAds = useCallback(() => {
return new Promise((resolve) => {
const script = document.createElement("script");
script.src = "https://media.ethicalads.io/media/client/ethicalads.min.js";
script.async = true;
script.onload = () => {
// @ts-ignore
resolve(window.ethicalads);
};
script.onerror = () => {
resolve(null);
};
document.body.appendChild(script);
return () => {
document.body.removeChild(script);
};
});
}, []);
useEffect(() => {
const shouldShowEthicalAds = Math.random() < EA_PROVIDER_RATIO;
let loadCarbon: any = null;
const loadCarbonAds = () => {
setShowEthicalAds(false);
// The isolation logic of carbonads is flawed.
// Once the script starts loading, it will asynchronous resolve, with no way to stop it.
// This leads to duplication of the ad. To solve the issue, we debounce the load action.
loadCarbon =
isMounted &&
setTimeout(() => {
const script = loadScript(
"https://cdn.carbonads.com/carbon.js?serve=CESIC53Y&placement=nextuiorg",
carbonRef.current,
);
script.id = "_carbonads_js";
carbonOptimize.init();
});
};
const loadAdProvider = async () => {
if (shouldShowEthicalAds) {
try {
const ethicalads = await loadEthicalAds();
if (!ethicalads) {
loadCarbonAds();
return;
}
// @ts-ignore
ethicalads.wait.then((placements) => {
if (!placements.length) {
loadCarbonAds();
} else {
setShowEthicalAds(true);
}
});
} catch (error) {
loadCarbonAds();
}
} else {
loadCarbonAds();
}
};
loadAdProvider();
return () => {
loadCarbon && clearTimeout(loadCarbon);
};
}, [isMounted]);
if (!__PROD__ || !__ENABLE_ADS__) return null;
return (
<>
<>
<Script async src="https://media.ethicalads.io/media/client/ethicalads.min.js" />
<div
className="ea-container horizontal"
data-ea-campaign-types="paid|publisher-house|community"
data-ea-publisher="nextuiorg"
data-ea-type="image"
style={{display: showEthicalAds ? "block" : "none"}}
/>
</>
<div className="carbon-ad-container" style={{display: showEthicalAds ? "none" : "block"}}>
<span ref={carbonRef} id="carbon-ad" />
</div>
</>
);
};
@@ -1,64 +0,0 @@
export default {
isRefreshAble: function () {
return !(
typeof document.addEventListener === "undefined" || this.browserSupport().hidden === undefined
);
},
browserSupport: function () {
let hidden;
let visibilityChange;
if (typeof document.hidden !== "undefined") {
// Opera 12.10 and Firefox 18 and later support
hidden = "hidden";
visibilityChange = "visibilitychange";
} else if (typeof document.msHidden !== "undefined") {
hidden = "msHidden";
visibilityChange = "msvisibilitychange";
} else if (typeof document.webkitHidden !== "undefined") {
hidden = "webkitHidden";
visibilityChange = "webkitvisibilitychange";
}
return {
hidden: hidden,
visibilityChange: visibilityChange,
};
},
handleVisibilityChange: function () {
const isElementInViewport = function (el) {
let element = document.querySelector(el);
let bounding = element.getBoundingClientRect();
let isVisible;
if (
bounding.top >= 0 &&
bounding.left >= 0 &&
bounding.right <= window.innerWidth &&
bounding.bottom <= window.innerHeight
) {
isVisible = true;
} else {
isVisible = false;
}
return isVisible;
};
if (!document.hidden) {
if (typeof _carbonads !== "undefined" && isElementInViewport("#carbonads")) {
// eslint-disable-next-line no-undef
_carbonads.refresh();
}
}
},
init: function () {
if (this.isRefreshAble()) {
document.addEventListener(
this.browserSupport().visibilityChange,
this.handleVisibilityChange,
false,
);
}
},
};
@@ -1 +0,0 @@
export * from "./carbon-ad";
@@ -1,55 +0,0 @@
import {ReactNode, FC} from "react";
import {clsx} from "@heroui/shared-utils";
export interface BgGridContainerProps {
showGradient?: boolean;
children?: ReactNode;
className?: string;
}
export const BgGridContainer: FC<BgGridContainerProps> = ({
// showGradient = true,
children,
className,
}) => {
return (
<div
className={clsx(
"relative overflow-y-hidden flex items-center border border-default-200 dark:border-default-100 px-2 py-4 rounded-lg",
"overflow-hidden",
// blur effect
// "bg-transparent",
// "before:w-full",
// "before:bg-background/10",
// "before:content-['']",
// "before:block",
// "before:z-[-1]",
// "before:absolute",
// "before:inset-0",
// "before:backdrop-blur-md",
// "before:backdrop-saturate-200",
className,
)}
>
<div className="max-w-full py-4 px-2 w-full h-full scrollbar-hide overflow-x-scroll">
{children}
</div>
{/* <div
className={clsx(
"hidden md:block absolute z-[-1] inset-0 bg-grid-zinc-300/25 [mask-image:linear-gradient(0deg,rgba(255,255,255,0.1),rgba(255,255,255,0.6))]",
"dark:bg-grid-zinc-500/25 dark:[mask-image:linear-gradient(0deg,rgba(255,255,255,0.1),rgba(255,255,255,0.5))]",
)}
style={{backgroundPosition: "10px 10px"}}
/>
{showGradient && (
<div className="hidden md:block absolute h-full w-full z-[-1] opacity-40 -top-8 -right-28">
<Image
removeWrapper
alt="custom themes background"
className="h-full w-full object-cover overflow-visible"
src="/gradients/blue-purple-1.svg"
/>
</div>
)} */}
</div>
);
};
-82
View File
@@ -1,82 +0,0 @@
"use client";
import {BlogPost} from "contentlayer2/generated";
import {Card, CardFooter, CardBody, CardHeader, Link, Avatar, Image} from "@heroui/react";
import Balancer from "react-wrap-balancer";
import {format, parseISO} from "date-fns";
import NextLink from "next/link";
import {AnimatePresence, motion} from "framer-motion";
import {usePostHog} from "posthog-js/react";
import {useIsMounted} from "@/hooks/use-is-mounted";
const BlogPostCard = (post: BlogPost) => {
const isMounted = useIsMounted();
const posthog = usePostHog();
const handlePress = () => {
posthog.capture("BlogPostCard - Selection", {
name: post.title,
action: "click",
category: "blog",
data: post.url ?? "",
});
};
return (
<AnimatePresence>
{isMounted && (
<motion.article
animate={{opacity: 1, y: 0}}
exit={{opacity: 0, y: 5}}
initial={{opacity: 0, y: 5}}
transition={{duration: 0.3}}
>
<Card
disableRipple
isBlurred
as={NextLink}
className="p-2 h-full border-transparent text-start bg-white/5 dark:bg-default-400/10 backdrop-blur-lg backdrop-saturate-[1.8]"
href={post.url}
isPressable={!!post.url}
onPress={handlePress}
>
<CardHeader>
<Link
as={NextLink}
className="font-semibold text-foreground"
href={post.url}
size="lg"
underline="hover"
onPress={handlePress}
>
<Balancer>{post.title}</Balancer>
</Link>
</CardHeader>
<CardBody className="pt-0 px-2 pb-1">
<Image className="mb-4" src={post.image} />
<p className="font-normal w-full text-default-600">{post.description}</p>
</CardBody>
<CardFooter className="flex justify-between items-center">
<time className="block text-small text-default-500" dateTime={post.date}>
{format(parseISO(post.date), "LLLL d, yyyy")} {post?.draft && " (Draft)"}
</time>
<Avatar size="sm" src={post.author?.avatar} />
</CardFooter>
</Card>
</motion.article>
)}
</AnimatePresence>
);
};
export const BlogPostList = ({posts}: {posts: BlogPost[]}) => {
return (
<div className="mt-10 grid gap-4 grid-cols-[repeat(auto-fill,minmax(300px,1fr))]">
{posts.map((post, idx) => (
<BlogPostCard key={idx} {...post} />
))}
</div>
);
};
-2
View File
@@ -1,2 +0,0 @@
export * from "./nextjs-templates";
export * from "./video-in-view";
@@ -1,34 +0,0 @@
import {NewNextJSIcon} from "@/components/icons";
import {FeaturesGrid} from "@/components/marketing/features-grid";
const frameworks = [
{
title: "Next.js 13 (App) Template",
isExternal: true,
description:
"A Next.js 13 with app directory template pre-configured with HeroUI (v2) and Tailwind CSS.",
icon: <NewNextJSIcon height={40} width={40} />,
href: "https://github.com/heroui-inc/next-app-template",
},
{
title: "Next.js 13 (Pages) Template",
isExternal: true,
description:
"A Next.js 13 with pages directory template pre-configured with HeroUI (v2) and Tailwind CSS.",
icon: <NewNextJSIcon height={40} width={40} />,
href: "https://github.com/heroui-inc/next-pages-template",
},
];
export const NextJsTemplates = () => {
return (
<FeaturesGrid
classNames={{
base: "mt-8 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-2 gap-4",
iconWrapper: "bg-default-300/20",
body: "py-0",
}}
features={frameworks}
/>
);
};
-151
View File
@@ -1,151 +0,0 @@
/* eslint-disable jsx-a11y/media-has-caption */
"use client";
import {useInView} from "framer-motion";
import {useRef, FC, useEffect, useState, useCallback} from "react";
import {Button, cn, Spinner, Tooltip} from "@heroui/react";
import {PlayBoldIcon, PauseBoldIcon} from "@/components/icons";
import {RotateLeftLinearIcon} from "@/components/icons";
interface VideoInViewProps {
src: string;
playMode?: "auto" | "manual";
autoPlay?: boolean;
poster?: string;
width?: number;
height?: number;
controls?: boolean;
className?: string;
}
export const VideoInView: FC<VideoInViewProps> = ({
src,
width,
height,
poster,
autoPlay = true,
playMode = "manual",
controls = false,
className,
}) => {
const [isLoading, setIsLoading] = useState(true);
const [isPlaying, setIsPlaying] = useState(false);
const videoRef = useRef<HTMLVideoElement>(null);
const isVisible = useInView(videoRef);
// play video when it is visible and playMode is auto
useEffect(() => {
if (playMode !== "auto") {
return;
}
if (isVisible) {
videoRef.current?.play();
} else {
videoRef.current?.pause();
}
}, [isVisible]);
const handleCanPlay = useCallback(() => {
setIsLoading(false);
}, []);
useEffect(() => {
const videoEl = videoRef.current;
if (videoEl) {
if (videoEl.readyState > 3) {
// HAVE_FUTURE_DATA: enough data to start playing
handleCanPlay();
} else {
videoEl.addEventListener("canplaythrough", handleCanPlay);
}
// Cleanup the event listener
return () => {
videoEl.removeEventListener("canplaythrough", handleCanPlay);
};
}
}, []);
const onRestart = useCallback(() => {
if (videoRef.current) {
videoRef.current.currentTime = 0;
videoRef.current.play();
setIsPlaying(true);
}
}, []);
const onTogglePlay = useCallback(() => {
if (videoRef.current) {
if (!isPlaying) {
videoRef.current.play();
} else {
videoRef.current.pause();
}
setIsPlaying((v) => !v);
}
}, [isPlaying]);
return (
<div
className="relative data-[playing=true]:after:opacity-0 data-[playing=true]:after:z-[-1] after:content-[''] after:absolute after:inset-0 after:bg-black/30 after:z-20 after:transition-opacity"
data-playing={isPlaying}
>
{isLoading && (
<Spinner
className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2"
color="secondary"
size="lg"
/>
)}
<Tooltip content={isPlaying ? "Pause" : "Play"} delay={1000}>
<Button
isIconOnly
className="absolute z-50 right-12 top-3 border-1 border-transparent bg-transparent before:bg-white/10 before:content-[''] before:block before:z-[-1] before:absolute before:inset-0 before:backdrop-blur-md before:backdrop-saturate-100 before:rounded-lg"
size="sm"
variant="bordered"
onPress={onTogglePlay}
>
{isPlaying ? (
<PauseBoldIcon className="text-white" size={16} />
) : (
<PlayBoldIcon className="text-white" size={16} />
)}
</Button>
</Tooltip>
<Tooltip content="Restart" delay={1000}>
<Button
isIconOnly
className="absolute z-50 right-3 top-3 border-1 border-transparent bg-transparent before:bg-white/10 before:content-[''] before:block before:z-[-1] before:absolute before:inset-0 before:backdrop-blur-md before:backdrop-saturate-100 before:rounded-lg"
size="sm"
variant="bordered"
onPress={onRestart}
>
<RotateLeftLinearIcon className="text-white" size={16} />
</Button>
</Tooltip>
<video
ref={videoRef}
loop
muted
autoPlay={autoPlay && playMode === "auto"}
className={cn(
"w-full border border-transparent dark:border-default-200/50 object-fit rounded-xl shadow-lg",
className,
)}
controls={controls}
height={height}
poster={poster}
src={src}
width={width}
onCanPlay={handleCanPlay}
/>
</div>
);
};
-478
View File
@@ -1,478 +0,0 @@
/* eslint-disable jsx-a11y/no-autofocus */
"use client";
import {Command} from "cmdk";
import {useEffect, useState, FC, useMemo, useCallback, useRef} from "react";
import {matchSorter} from "match-sorter";
import {Button, ButtonProps, Kbd, Modal, ModalContent} from "@heroui/react";
import {CloseIcon} from "@heroui/shared-icons";
import {tv} from "tailwind-variants";
import {usePathname, useRouter} from "next/navigation";
import MultiRef from "react-multi-ref";
import {clsx} from "@heroui/shared-utils";
import scrollIntoView from "scroll-into-view-if-needed";
import {isAppleDevice, isWebKit} from "@react-aria/utils";
import {create} from "zustand";
import {isEmpty, intersectionBy} from "@heroui/shared-utils";
import {writeStorage, useLocalStorage} from "@rehooks/local-storage";
import {usePostHog} from "posthog-js/react";
import {
DocumentCodeBoldIcon,
HashBoldIcon,
ChevronRightLinearIcon,
SearchLinearIcon,
} from "./icons";
import searchData from "@/config/search-meta.json";
import {useUpdateEffect} from "@/hooks/use-update-effect";
const hideOnPaths = ["examples"];
export interface CmdkStore {
isOpen: boolean;
onClose: () => void;
onOpen: () => void;
}
export const useCmdkStore = create<CmdkStore>((set) => ({
isOpen: false,
onClose: () => set({isOpen: false}),
onOpen: () => set({isOpen: true}),
}));
const cmdk = tv({
slots: {
base: "max-h-full overflow-y-auto",
header: [
"flex",
"items-center",
"w-full",
"px-4",
"border-b",
"border-default-400/50",
"dark:border-default-100",
],
searchIcon: "text-default-400 text-lg",
input: [
"w-full",
"px-2",
"h-14",
"font-sans",
"text-lg",
"outline-none",
"rounded-none",
"bg-transparent",
"text-default-700",
"placeholder-default-500",
"dark:text-default-500",
"dark:placeholder:text-default-300",
],
list: ["px-4", "mt-2", "pb-4", "overflow-y-auto", "max-h-[50vh]"],
itemWrapper: [
"px-4",
"mt-2",
"group",
"flex",
"h-16",
"justify-between",
"items-center",
"rounded-lg",
"shadow",
"bg-content2/50",
"active:opacity-70",
"cursor-pointer",
"transition-opacity",
"data-[active=true]:bg-primary",
"data-[active=true]:text-primary-foreground",
],
leftWrapper: ["flex", "gap-3", "items-center", "w-full", "max-w-full"],
leftIcon: [
"text-default-500 dark:text-default-300",
"group-data-[active=true]:text-primary-foreground",
],
itemContent: ["flex", "flex-col", "gap-0", "justify-center", "max-w-[80%]"],
itemParentTitle: [
"text-default-400",
"text-xs",
"group-data-[active=true]:text-primary-foreground",
"select-none",
],
itemTitle: [
"truncate",
"text-default-500",
"group-data-[active=true]:text-primary-foreground",
"select-none",
],
emptyWrapper: ["flex", "flex-col", "text-center", "items-center", "justify-center", "h-32"],
},
});
interface SearchResultItem {
content: string;
objectID: string;
url: string;
type: "lvl1" | "lvl2" | "lvl3";
hierarchy: {
lvl1: string | null;
lvl2?: string | null;
lvl3?: string | null;
};
}
const MATCH_KEYS = ["hierarchy.lvl1", "hierarchy.lvl2", "hierarchy.lvl3", "content"];
const RECENT_SEARCHES_KEY = "recent-searches";
const MAX_RECENT_SEARCHES = 10;
const MAX_RESULTS = 20;
export const Cmdk: FC<{}> = () => {
const [query, setQuery] = useState("");
const [activeItem, setActiveItem] = useState(0);
const [menuNodes] = useState(() => new MultiRef<number, HTMLElement>());
const slots = useMemo(() => cmdk(), []);
const pathname = usePathname();
const eventRef = useRef<"mouse" | "keyboard">();
const listRef = useRef<HTMLDivElement>(null);
const router = useRouter();
const {isOpen, onClose, onOpen} = useCmdkStore();
const posthog = usePostHog();
const [recentSearches] = useLocalStorage<SearchResultItem[]>(RECENT_SEARCHES_KEY);
const addToRecentSearches = (item: SearchResultItem) => {
let searches = recentSearches ?? [];
// Avoid adding the same search again
if (!searches.find((i) => i.objectID === item.objectID)) {
writeStorage(RECENT_SEARCHES_KEY, [item, ...searches].slice(0, MAX_RECENT_SEARCHES));
} else {
// Move the search to the top
searches = searches.filter((i) => i.objectID !== item.objectID);
writeStorage(RECENT_SEARCHES_KEY, [item, ...searches].slice(0, MAX_RECENT_SEARCHES));
}
};
const prioritizeFirstLevelItems = (a: SearchResultItem, b: SearchResultItem) => {
if (a.type === "lvl1") {
return -1;
} else if (b.type === "lvl1") {
return 1;
}
return 0;
};
const results = useMemo<SearchResultItem[]>(
function getResults() {
if (query.length < 2) return [];
const data = searchData as SearchResultItem[];
const words = query.split(" ");
if (words.length === 1) {
return matchSorter(data, query, {
keys: MATCH_KEYS,
sorter: (matches) => {
matches.sort((a, b) => prioritizeFirstLevelItems(a.item, b.item));
return matches;
},
}).slice(0, MAX_RESULTS);
}
const matchesForEachWord = words.map((word) =>
matchSorter(data, word, {
keys: MATCH_KEYS,
sorter: (matches) => {
matches.sort((a, b) => prioritizeFirstLevelItems(a.item, b.item));
return matches;
},
}),
);
const matches = intersectionBy(...matchesForEachWord, "objectID").slice(0, MAX_RESULTS);
posthog.capture("Cmdk - Search", {
name: "cmdk - search",
action: "search",
category: "cmdk",
data: {query, words, matches: matches?.map((match) => match.url).join(", ")},
});
return matches;
},
[query],
);
const items = !isEmpty(results) ? results : recentSearches ?? [];
// Toggle the menu when ⌘K / CTRL K is pressed
useEffect(() => {
const onKeyDown = (e: KeyboardEvent) => {
const hotkey = isAppleDevice() ? "metaKey" : "ctrlKey";
if (e?.key?.toLowerCase() === "k" && e[hotkey]) {
e.preventDefault();
isOpen ? onClose() : onOpen();
posthog.capture("Cmdk - Open/Close", {
name: "cmdk - open/close",
action: "keydown",
category: "cmdk",
data: isOpen ? "close" : "open",
});
}
};
document.addEventListener("keydown", onKeyDown);
return () => {
document.removeEventListener("keydown", onKeyDown);
};
}, [isOpen]);
const onItemSelect = useCallback(
(item: SearchResultItem) => {
onClose();
router.push(item.url);
addToRecentSearches(item);
posthog.capture("Cmdk - ItemSelect", {
name: item.content,
action: "click",
category: "cmdk",
data: item.url,
});
},
[router, recentSearches],
);
const onInputKeyDown = useCallback(
(e: React.KeyboardEvent) => {
eventRef.current = "keyboard";
switch (e.key) {
case "ArrowDown": {
e.preventDefault();
if (activeItem + 1 < items.length) {
setActiveItem(activeItem + 1);
}
break;
}
case "ArrowUp": {
e.preventDefault();
if (activeItem - 1 >= 0) {
setActiveItem(activeItem - 1);
}
break;
}
case "Control":
case "Alt":
case "Shift": {
e.preventDefault();
break;
}
case "Enter": {
if (items?.length <= 0) {
break;
}
onItemSelect(items[activeItem]);
break;
}
}
},
[activeItem, items, router],
);
useUpdateEffect(() => {
setActiveItem(0);
}, [query]);
useUpdateEffect(() => {
if (!listRef.current || eventRef.current === "mouse") return;
const node = menuNodes.map.get(activeItem);
if (!node) return;
scrollIntoView(node, {
scrollMode: "if-needed",
behavior: "smooth",
block: "end",
inline: "end",
boundary: listRef.current,
});
}, [activeItem]);
const CloseButton = useCallback(
({
onPress,
className,
}: {
onPress?: ButtonProps["onPress"];
className?: ButtonProps["className"];
}) => {
return (
<Button
isIconOnly
className={clsx(
"border data-[hover=true]:bg-content2 border-default-400 dark:border-default-100",
className,
)}
radius="full"
size="sm"
variant="bordered"
onPress={onPress}
>
<CloseIcon />
</Button>
);
},
[],
);
const renderItem = useCallback(
(item: SearchResultItem, index: number, isRecent = false) => {
const isLvl1 = item.type === "lvl1";
const mainIcon = isRecent ? (
<SearchLinearIcon className={slots.leftIcon()} size={20} strokeWidth={2} />
) : isLvl1 ? (
<DocumentCodeBoldIcon className={slots.leftIcon()} />
) : (
<HashBoldIcon className={slots.leftIcon()} />
);
return (
<Command.Item
key={item.objectID}
ref={menuNodes.ref(index)}
className={slots.itemWrapper()}
data-active={index === activeItem}
value={item.content}
onMouseEnter={() => {
eventRef.current = "mouse";
setActiveItem(index);
}}
onSelect={() => {
if (eventRef.current === "keyboard") {
return;
}
onItemSelect(item);
}}
>
<div className={slots.leftWrapper()}>
{mainIcon}
<div className={slots.itemContent()}>
{!isLvl1 && <span className={slots.itemParentTitle()}>{item.hierarchy.lvl1}</span>}
<p className={slots.itemTitle()}>{item.content}</p>
</div>
</div>
<ChevronRightLinearIcon size={14} />
</Command.Item>
);
},
[activeItem, onItemSelect, CloseButton, slots],
);
const shouldOpen = !hideOnPaths.some((path) => pathname.includes(path));
return (
<Modal
hideCloseButton
backdrop="opaque"
classNames={{
base: [
"mt-[20vh]",
"border-small",
"dark:border-default-100",
"supports-[backdrop-filter]:bg-background/80",
"dark:supports-[backdrop-filter]:bg-background/30",
"supports-[backdrop-filter]:backdrop-blur-md",
"supports-[backdrop-filter]:backdrop-saturate-150",
],
backdrop: ["bg-black/80"],
}}
isOpen={isOpen && shouldOpen}
motionProps={{
onAnimationComplete: () => {
if (!isOpen) {
setQuery("");
}
},
}}
placement="top-center"
scrollBehavior="inside"
size="xl"
onClose={() => onClose()}
>
<ModalContent>
<Command className={slots.base()} label="Quick search command" shouldFilter={false}>
<div className={slots.header()}>
<SearchLinearIcon className={slots.searchIcon()} strokeWidth={2} />
<Command.Input
autoFocus={!isWebKit()}
className={slots.input()}
placeholder="Search documentation"
value={query}
onKeyDown={onInputKeyDown}
onValueChange={setQuery}
/>
{query.length > 0 && <CloseButton onPress={() => setQuery("")} />}
<Kbd className="hidden md:block border-none px-2 py-1 ml-2 font-medium text-[0.6rem]">
ESC
</Kbd>
</div>
<Command.List ref={listRef} className={slots.list()} role="listbox">
<Command.Empty>
{query.length > 0 && (
<div className={slots.emptyWrapper()}>
<div>
<p>No results for &quot;{query}&quot;</p>
{query.length === 1 ? (
<p className="text-default-400">
Try adding more characters to your search term.
</p>
) : (
<p className="text-default-400">Try searching for something else.</p>
)}
</div>
</div>
)}
</Command.Empty>
{isEmpty(query) &&
(isEmpty(recentSearches) ? (
<div className={slots.emptyWrapper()}>
<p className="text-default-400">No recent searches</p>
</div>
) : (
recentSearches &&
recentSearches.length > 0 && (
<Command.Group
heading={
<div className="flex items-center justify-between">
<p className="text-default-600">Recent</p>
</div>
}
>
{recentSearches.map((item, index) => renderItem(item, index, true))}
</Command.Group>
)
))}
{results.map((item, index) => renderItem(item, index))}
</Command.List>
</Command>
</ModalContent>
</Modal>
);
};
@@ -1,165 +0,0 @@
// Inspired by https://github.dev/modulz/stitches-site code demo
import React from "react";
import refractor from "refractor/core";
import js from "refractor/lang/javascript";
import jsx from "refractor/lang/jsx";
import bash from "refractor/lang/bash";
import css from "refractor/lang/css";
import diff from "refractor/lang/diff";
import {toHtml} from "hast-util-to-html";
import rangeParser from "parse-numeric-range";
import {clsx} from "@heroui/shared-utils";
import {Pre} from "./pre";
import {WindowActions} from "./window-actions";
import highlightLine from "@/libs/rehype-highlight-line";
import highlightWord from "@/libs/rehype-highlight-word";
refractor.register(js);
refractor.register(jsx);
refractor.register(bash);
refractor.register(css);
refractor.register(diff);
type PreProps = Omit<React.ComponentProps<typeof Pre>, "css">;
export type CodeBlockProps = PreProps & {
language: "js" | "jsx" | "bash" | "css" | "diff";
title?: string;
value?: string;
highlightLines?: string;
mode?: "static" | "typewriter";
showLineNumbers?: boolean;
showWindowIcons?: boolean;
className?: string;
};
/**
* recursively get all text nodes as an array for a given element
*/
function getTextNodes(node: any): any[] {
let childTextNodes: React.ReactNode[] = [];
if (!node.hasChildNodes()) return [];
const childNodes = node.childNodes;
for (let i = 0; i < childNodes.length; i++) {
if (childNodes[i].nodeType == Node.TEXT_NODE) {
childTextNodes.push(childNodes[i]);
} else if (childNodes[i].nodeType == Node.ELEMENT_NODE) {
Array.prototype.push.apply(childTextNodes, getTextNodes(childNodes[i]));
}
}
return childTextNodes;
}
/**
* given a text node, wrap each character in the
* given tag.
*/
function wrapEachCharacter(textNode: any, tag: string, count: number) {
const text = textNode.nodeValue;
const parent = textNode.parentNode;
const characters = text.split("");
characters.forEach(function (character: any, letterIndex: any) {
const delay = (count + letterIndex) * 50;
var element = document.createElement(tag);
var characterNode = document.createTextNode(character);
element.appendChild(characterNode);
element.style.opacity = "0";
element.style.transition = `all ease 0ms ${delay}ms`;
parent.insertBefore(element, textNode);
// skip a couple of frames to trigger transition
requestAnimationFrame(() => requestAnimationFrame(() => (element.style.opacity = "1")));
});
parent.removeChild(textNode);
}
function CodeTypewriter({value, className, css, ...props}: any) {
const wrapperRef = React.useRef(null);
React.useEffect(() => {
const wrapper = wrapperRef.current as any;
if (wrapper) {
var allTextNodes = getTextNodes(wrapper);
let count = 0;
allTextNodes?.forEach((textNode) => {
wrapEachCharacter(textNode, "span", count);
count = count + textNode.nodeValue.length;
});
wrapper.style.opacity = "1";
}
return () => (wrapper.innerHTML = value);
}, []);
return (
<Pre className={className} css={css} {...props}>
<code
dangerouslySetInnerHTML={{__html: value}}
ref={wrapperRef}
className={className}
style={{opacity: 0}}
/>
</Pre>
);
}
const CodeBlock = React.forwardRef<HTMLPreElement, CodeBlockProps>((_props, forwardedRef) => {
const {
language,
value,
title,
highlightLines = "0",
className = "",
mode,
showLineNumbers,
showWindowIcons,
...props
} = _props;
let result: any = refractor.highlight(value || "", language);
result = highlightLine(result, rangeParser(highlightLines));
result = highlightWord(result);
// convert to html
result = toHtml(result);
// TODO reset theme
const classes = `language-${language}`;
const codeClasses = clsx("absolute w-full px-4 pb-6", showWindowIcons ? "top-10" : "top-0");
if (mode === "typewriter") {
return <CodeTypewriter className={classes} css={css} value={result} {...props} />;
}
return (
<Pre
ref={forwardedRef}
className={clsx("code-block", classes, className)}
data-line-numbers={showLineNumbers}
{...props}
>
{showWindowIcons && <WindowActions title={title} />}
<code dangerouslySetInnerHTML={{__html: result}} className={clsx(classes, codeClasses)} />
</Pre>
);
});
CodeBlock.displayName = "HeroUI - CodeBlock";
export default CodeBlock;
@@ -1,96 +0,0 @@
"use client";
// Inspired by https://github.dev/modulz/stitches-site code demo
import React from "react";
import rangeParser from "parse-numeric-range";
import CodeBlock, {CodeBlockProps} from "./code-block";
import {CopyButton} from "@/components";
export interface CodeWindowProps extends CodeBlockProps {
showCopy?: boolean;
}
export const CodeWindow: React.FC<CodeWindowProps> = ({highlightLines, showCopy, ...props}) => {
const wrapperRef = React.useRef<HTMLPreElement>(null);
React.useEffect(() => {
const pre = wrapperRef.current;
if (!pre) return;
const PADDING = 15;
let codeInner = pre.querySelector("code") ?? null;
const codeBlockHeight = pre.clientHeight - PADDING * 2;
const lines = pre.querySelectorAll<HTMLElement>(".highlight-line");
if (!highlightLines) {
lines.forEach((line) => {
line.classList.remove("off");
});
if (codeInner) {
codeInner.style.transform = `translate3d(0, 0, 0)`;
}
return;
}
const linesToHighlight = rangeParser(highlightLines);
const firstLineNumber = Math.max(0, linesToHighlight[0] - 1);
const lastLineNumber = Math.min(lines.length - 1, [...linesToHighlight].reverse()[0] - 1);
const firstLine = lines[firstLineNumber];
const lastLine = lines[lastLineNumber];
// Prevent errors in case the right line doesn't exist
if (!firstLine || !lastLine) {
// eslint-disable-next-line no-console
console.warn(`CodeWindow: Error finding the right line`);
return;
}
const linesHeight = lastLine.offsetTop - firstLine.offsetTop;
const maxDistance = (codeInner?.clientHeight || 0) - codeBlockHeight;
const codeFits = linesHeight < codeBlockHeight;
const lastLineIsBelow = lastLine.offsetTop > codeBlockHeight - PADDING;
const lastLineIsAbove = !lastLineIsBelow;
let translateY = 0;
if (codeFits && lastLineIsAbove) {
translateY = 0;
} else if (codeFits && lastLineIsBelow) {
const dist = firstLine.offsetTop - (codeBlockHeight - linesHeight) / 2;
translateY = dist > maxDistance ? maxDistance : dist;
} else {
translateY = firstLine.offsetTop;
}
lines.forEach((line, i) => {
const lineIndex = i + 1;
if (linesToHighlight.includes(lineIndex)) {
line.setAttribute("data-highlighted", "true");
} else {
line.setAttribute("data-highlighted", "false");
}
});
requestAnimationFrame(
() => codeInner && (codeInner.style.transform = `translate3d(0, ${-translateY}px, 0)`),
);
}, [highlightLines]);
return (
<div className="relative">
<CodeBlock ref={wrapperRef} {...props} />
{showCopy && <CopyButton className="top-2 absolute right-2" value={props.value} />}
</div>
);
};
@@ -1 +0,0 @@
export * from "./code-window";
-30
View File
@@ -1,30 +0,0 @@
import {forwardRef} from "react";
import {clsx} from "@heroui/shared-utils";
export interface PreProps {
className?: string;
isScrollable?: boolean;
children?: React.ReactNode;
}
export const Pre = forwardRef<HTMLPreElement, PreProps>(
({className = "", children, isScrollable = true, ...props}, forwardedRef) => {
const scrollClass = isScrollable ? "overflow-scroll" : "overflow-hidden";
return (
<pre
ref={forwardedRef}
className={clsx(
"relative w-full h-full box-border shadow-md text-white/80 leading-5 whitespace-pre text-sm font-mono bg-code-background rounded-xl [&>code]:transition-transform",
scrollClass,
className,
)}
{...props}
>
{children}
</pre>
);
},
);
Pre.displayName = "CodeBlock.Pre";
@@ -1,41 +0,0 @@
import React from "react";
import {tv} from "tailwind-variants";
import {clsx} from "@heroui/shared-utils";
export type WindowActionsProps = {
title?: string;
className?: string;
};
const windowIconStyles = tv({
base: "w-3 h-3 rounded-full",
variants: {
color: {
red: "bg-red-500",
yellow: "bg-yellow-500",
green: "bg-green-500",
},
},
});
export const WindowActions: React.FC<WindowActionsProps> = ({title, className, ...props}) => {
return (
<div
className={clsx(
"flex items-center sticky top-0 left-0 px-4 z-10 justify-between h-8 bg-code-background w-full",
className,
)}
{...props}
>
<div className="flex items-center gap-2 basis-1/3">
<div className={windowIconStyles({color: "red"})} />
<div className={windowIconStyles({color: "yellow"})} />
<div className={windowIconStyles({color: "green"})} />
</div>
<div className="flex basis-1/3 h-full justify-center items-center">
{title && <p className="text-white/30 text-xs font-light">{title}</p>}
</div>
<div className="flex basis-1/3" />
</div>
);
};
-37
View File
@@ -1,37 +0,0 @@
import {ButtonProps} from "@heroui/react";
import {useClipboard} from "@heroui/use-clipboard";
import {memo} from "react";
import {PreviewButton} from "./preview-button";
import {CheckLinearIcon, CopyLinearIcon} from "@/components/icons";
export interface CopyButtonProps extends ButtonProps {
value?: string;
}
export const CopyButton = memo<CopyButtonProps>(({value, className, ...buttonProps}) => {
const {copy, copied} = useClipboard();
const icon = copied ? (
<CheckLinearIcon
className="opacity-0 scale-50 data-[visible=true]:opacity-100 data-[visible=true]:scale-100 transition-transform-opacity"
data-visible={copied}
size={16}
/>
) : (
<CopyLinearIcon
className="opacity-0 scale-50 data-[visible=true]:opacity-100 data-[visible=true]:scale-100 transition-transform-opacity"
data-visible={!copied}
size={16}
/>
);
const handleCopy = () => {
copy(value);
};
return <PreviewButton className={className} icon={icon} onPress={handleCopy} {...buttonProps} />;
});
CopyButton.displayName = "CopyButton";
-89
View File
@@ -1,89 +0,0 @@
"use client";
import {FC, useState} from "react";
import {
Modal,
Button,
ModalContent,
ModalHeader,
Link as HeroUILink,
ModalBody,
ModalFooter,
Skeleton,
} from "@heroui/react";
import Link from "next/link";
import {CodeWindow} from "@/components/code-window";
import {useIsMobile} from "@/hooks/use-media-query";
export interface DemoCodeModalProps {
isOpen: boolean;
code: string;
title: string;
subtitle?: string;
onClose: () => void;
}
export const DemoCodeModal: FC<DemoCodeModalProps> = ({isOpen, code, title, subtitle, onClose}) => {
const [isCodeVisible, setIsCodeVisible] = useState(false);
const isMobile = useIsMobile();
const lowerTitle = title.toLowerCase();
const fileName = `${lowerTitle}.tsx`;
return (
<Modal
classNames={{
backdrop: "z-[100002]", // to appear above the navbar
wrapper: "z-[100003]", // to appear above the backdrop
}}
isOpen={isOpen}
motionProps={{
onAnimationComplete: () => {
setIsCodeVisible(isOpen);
},
}}
radius={isMobile ? "none" : "lg"}
size={isMobile ? "full" : "2xl"}
onClose={onClose}
>
<ModalContent>
<ModalHeader className="flex flex-col gap-2">
<h3>{title} code</h3>
<p className="text-base font-normal">
{subtitle || (
<>
This is an example of how to use the {lowerTitle} component, for more information
please visit the&nbsp;
<HeroUILink as={Link} href={`/docs/components/${lowerTitle}`}>
{lowerTitle}
</HeroUILink>
&nbsp;docs.
</>
)}
</p>
</ModalHeader>
<ModalBody className="flex-initial md:pb-6">
{isCodeVisible ? (
<CodeWindow
showCopy
showWindowIcons
className="min-h-[320px] !h-[60vh] max-h-full"
language="jsx"
title={fileName}
value={code}
/>
) : (
<Skeleton className="h-[60vh] rounded-xl" />
)}
</ModalBody>
<ModalFooter className="md:hidden">
<Button fullWidth onPress={onClose}>
Close
</Button>
</ModalFooter>
</ModalContent>
</Modal>
);
};
@@ -1,49 +0,0 @@
"use client";
import {useRef} from "react";
import {Button} from "@heroui/react";
import {usePostHog} from "posthog-js/react";
export const CustomButton = () => {
const buttonRef = useRef<HTMLButtonElement | null>(null);
const posthog = usePostHog();
const handleConfetti = async () => {
const {clientWidth, clientHeight} = document.documentElement;
const boundingBox = buttonRef.current?.getBoundingClientRect?.();
const targetY = boundingBox?.y ?? 0;
const targetX = boundingBox?.x ?? 0;
const targetWidth = boundingBox?.width ?? 0;
const targetCenterX = targetX + targetWidth / 2;
const confetti = (await import("canvas-confetti")).default;
confetti({
zIndex: 999,
particleCount: 100,
spread: 70,
origin: {
y: targetY / clientHeight,
x: targetCenterX / clientWidth,
},
});
posthog.capture("LandingPage - Confetti Button", {
action: "press",
category: "landing-page",
});
};
return (
<Button
ref={buttonRef}
disableRipple
className="relative overflow-visible rounded-full hover:-translate-y-1 px-12 shadow-xl bg-background/30 after:content-[''] after:absolute after:rounded-full after:inset-0 after:bg-background/40 after:z-[-1] after:transition after:!duration-500 hover:after:scale-150 hover:after:opacity-0"
size="lg"
onPress={handleConfetti}
>
Press me
</Button>
);
};
-3
View File
@@ -1,3 +0,0 @@
export * from "./user-twitter-card";
export * from "./music-player";
export * from "./custom-button";
-133
View File
@@ -1,133 +0,0 @@
"use client";
import {Card, CardBody, Button, Image, Slider, CardProps} from "@heroui/react";
import {useState, FC} from "react";
import {clsx} from "@heroui/shared-utils";
import NextImage from "next/image";
import {
PauseCircleBoldIcon,
NextBoldIcon,
PreviousBoldIcon,
RepeatOneBoldIcon,
ShuffleBoldIcon,
HeartLinearIcon,
} from "@/components/icons";
export interface MusicPlayerProps extends CardProps {}
export const MusicPlayer: FC<MusicPlayerProps> = ({className, ...otherProps}) => {
const [liked, setLiked] = useState(false);
return (
<Card
isBlurred
className={clsx("border-none bg-background/60 dark:bg-default-100/50", className)}
shadow="sm"
{...otherProps}
>
<CardBody>
<div className="md:max-h-[200px] grid grid-cols-6 md:grid-cols-12 gap-6 md:gap-4 items-center justify-center">
<div className="relative col-span-6 md:col-span-4">
<Image
alt="Album cover"
as={NextImage}
className="object-cover"
height={160}
shadow="md"
src="/images/album-cover.png"
width={240}
/>
</div>
<div className="flex flex-col col-span-6 md:col-span-8">
<div className="flex justify-between items-start">
<div className="flex flex-col gap-0">
<p className="text-sm font-semibold text-foreground">Daily Mix</p>
<p className="text-xs text-foreground/80">12 Tracks</p>
<p className="text-lg font-medium mt-2">Frontend Radio</p>
</div>
<Button
isIconOnly
aria-label="Like"
className="text-default-900/60 data-[hover]:bg-foreground/10 -translate-y-2 translate-x-2"
radius="full"
variant="light"
onPress={() => setLiked((v) => !v)}
>
<HeartLinearIcon
className={liked ? "[&>path]:stroke-transparent" : ""}
fill={liked ? "currentColor" : "none"}
/>
</Button>
</div>
<div className="flex flex-col mt-3 gap-1">
<Slider
aria-label="Music progress"
classNames={{
track: "bg-default-500/30",
thumb: "w-2 h-2 after:w-2 after:h-2 after:bg-foreground",
}}
color="foreground"
defaultValue={33}
size="sm"
/>
<div className="flex justify-between">
<p className="text-sm">1:23</p>
<p className="text-sm text-foreground/50">4:32</p>
</div>
</div>
<div className="flex w-full items-center justify-center">
<Button
isIconOnly
aria-label="Repeat"
className="data-[hover]:bg-foreground/10"
radius="full"
variant="light"
>
<RepeatOneBoldIcon className="text-foreground/80" />
</Button>
<Button
isIconOnly
aria-label="Previous"
className="data-[hover]:bg-foreground/10"
radius="full"
variant="light"
>
<PreviousBoldIcon />
</Button>
<Button
isIconOnly
aria-label="Play"
className="w-auto h-auto data-[hover]:bg-foreground/10"
radius="full"
variant="light"
>
<PauseCircleBoldIcon size={54} />
</Button>
<Button
isIconOnly
aria-label="Next"
className="data-[hover]:bg-foreground/10"
radius="full"
variant="light"
>
<NextBoldIcon />
</Button>
<Button
isIconOnly
aria-label="Shuffle"
className="data-[hover]:bg-foreground/10"
radius="full"
variant="light"
>
<ShuffleBoldIcon className="text-foreground/80" />
</Button>
</div>
</div>
</div>
</CardBody>
</Card>
);
};
@@ -1,89 +0,0 @@
import {
tv,
VariantProps,
CircularProgress,
CircularProgressProps,
circularProgress,
} from "@heroui/react";
import {FC} from "react";
const speedProgress = tv({
extend: circularProgress,
slots: {
svg: "",
label: "",
value: "",
},
variants: {
color: {
olive: {
svg: "text-[#84cc16]",
},
orange: {
svg: "text-[#ff8c00]",
},
violet: {
svg: "text-[#8b5cf6]",
},
},
size: {
sm: {
svg: "w-10 h-10",
label: "text-small",
value: "text-[0.5rem]",
},
md: {
svg: "w-12 h-12",
label: "text-medium",
value: "text-small",
},
lg: {
svg: "w-14 h-14",
label: "text-medium",
value: "text-[0.6rem]",
},
xl: {
svg: "w-16 h-16",
label: "text-large",
value: "text-small",
},
},
},
defaultVariants: {
color: "olive",
size: "sm",
},
});
type SpeedProgressVariants = VariantProps<typeof speedProgress>;
export interface SpeedProgressProps extends Omit<CircularProgressProps, "color" | "size"> {
color?: SpeedProgressVariants["color"];
size?: SpeedProgressVariants["size"];
}
export const SpeedProgress: FC<SpeedProgressProps> = ({color, size, ...otherProps}) => {
const slots = speedProgress({size, color});
return (
<CircularProgress
classNames={{
svg: slots.svg(),
label: slots.label(),
value: slots.value(),
}}
// color={color} // not needed because is being passed from slots
formatOptions={{style: "unit", unit: "kilometer"}}
label="Speed"
showValueLabel={true}
// size={size}
{...otherProps}
/>
);
};
// const MyApp = () => {
// return (
// <SpeedProgress color="orange" />
// );
// }
-25
View File
@@ -1,25 +0,0 @@
/* eslint-disable react/display-name */
import React from "react";
type Variants = {[K: string]: {[P: string]: string}};
type StringToBoolean<T> = T extends "true" | "false" ? boolean : T;
type StylesProps<V extends Variants> = {
[K in keyof V]?: StringToBoolean<keyof V[K]>;
} & {
variants: V;
[x: string]: any;
};
type ComponentType<P = {}> = React.ComponentType<P> | React.ForwardRefExoticComponent<P>;
export function extendStyles<P extends Variants>(
Component: ComponentType<StylesProps<P>>,
styles: {variants: P},
) {
return React.forwardRef<HTMLButtonElement, StylesProps<P>>((props, ref) => {
return <Component ref={ref} {...styles} {...props} />;
});
}
@@ -1,65 +0,0 @@
"use client";
import {useState} from "react";
import {Card, CardHeader, Button, Avatar, CardBody, CardFooter} from "@heroui/react";
import {clsx} from "@heroui/shared-utils";
interface UserTwitterCardProps {
className?: string;
}
export const UserTwitterCard = ({className}: UserTwitterCardProps) => {
const [isFollowed, setIsFollowed] = useState(false);
return (
<Card className={clsx("max-w-[300px]", className)}>
<CardHeader className="justify-between">
<div className="flex gap-5">
<Avatar
isBordered
alt="Zoey Lang"
imgProps={{
width: 40,
height: 40,
}}
radius="full"
size="md"
src="/avatars/avatar-1.webp"
/>
<div className="flex flex-col items-start justify-center">
<h4 className="text-sm font-semibold leading-none text-default-600">Zoey Lang</h4>
<h5 className="text-sm tracking-tight text-default-400">@zoeylang</h5>
</div>
</div>
<Button
className={isFollowed ? "bg-transparent text-foreground border-default-200" : ""}
color="primary"
radius="full"
size="sm"
variant={isFollowed ? "bordered" : "solid"}
onPress={() => setIsFollowed(!isFollowed)}
>
{isFollowed ? "Unfollow" : "Follow"}
</Button>
</CardHeader>
<CardBody className="px-3 py-0">
<p className="text-sm pl-px text-default-400">
Full-stack developer, @hero_ui lover she/her&nbsp;
<span aria-label="confetti" role="img">
🎉
</span>
</p>
</CardBody>
<CardFooter className="gap-3">
<div className="flex gap-1">
<p className="font-semibold text-default-400 text-sm">4</p>
<p className=" text-default-400 text-sm">Following</p>
</div>
<div className="flex gap-1">
<p className="font-semibold text-default-400 text-sm">97.1K</p>
<p className="text-default-400 text-sm">Followers</p>
</div>
</CardFooter>
</Card>
);
};
@@ -1,38 +0,0 @@
"use client";
import {FC} from "react";
import {tv, VariantProps} from "tailwind-variants";
const blockquoteStyles = tv({
base: "border px-4 bg-default-50 my-6 py-3 rounded-xl [&>p]:m-0",
variants: {
color: {
default: "border-default-200 dark:border-default-100 bg-default-200/20",
primary: "border-primary-100 bg-primary-50/20",
secondary: "border-secondary-100 bg-secondary-50/20",
success: "border-success-100 bg-success-50/20",
warning: "border-warning-100 bg-warning-50/20",
danger: "border-danger-100 bg-danger-50/20",
},
},
defaultVariants: {
color: "default",
},
});
type BlockquoteVariantProps = VariantProps<typeof blockquoteStyles>;
export interface BlockquoteProps extends BlockquoteVariantProps {
children?: React.ReactNode;
className?: string;
}
export const Blockquote: FC<BlockquoteProps> = ({children, color, className, ...props}) => {
const styles = blockquoteStyles({color, className});
return (
<blockquote className={styles} {...props}>
{children}
</blockquote>
);
};
@@ -1,195 +0,0 @@
"use client";
import React, {useCallback, useMemo, useRef} from "react";
import dynamic from "next/dynamic";
import {Skeleton, Tab, Tabs} from "@heroui/react";
import {useInView} from "framer-motion";
import {useCodeDemo, UseCodeDemoProps} from "./use-code-demo";
import WindowResizer, {WindowResizerProps} from "./window-resizer";
import {GradientBoxProps} from "@/components/gradient-box";
const DynamicReactLiveDemo = dynamic(
() => import("./react-live-demo").then((m) => m.ReactLiveDemo),
{
ssr: false,
// eslint-disable-next-line react/display-name
loading: () => <Skeleton className="w-full h-24 rounded-xl" />,
},
);
const DynamicSandpack = dynamic(() => import("../../../sandpack").then((m) => m.Sandpack), {
ssr: false,
// eslint-disable-next-line react/display-name
loading: () => <Skeleton className="w-full h-32 rounded-xl" />,
});
interface CodeDemoProps extends UseCodeDemoProps, WindowResizerProps {
title?: string;
asIframe?: boolean;
showSandpackPreview?: boolean;
initialEditorOpen?: boolean;
enableResize?: boolean;
showPreview?: boolean;
hideWindowActions?: boolean;
showOpenInCodeSandbox?: boolean;
isPreviewCentered?: boolean;
resizeEnabled?: boolean;
typescriptStrict?: boolean;
displayMode?: "always" | "visible";
isGradientBox?: boolean;
gradientColor?: GradientBoxProps["color"];
previewHeight?: string | number;
overflow?: "auto" | "visible" | "hidden";
className?: string;
}
export const CodeDemo: React.FC<CodeDemoProps> = ({
files = {},
title,
showEditor = true,
showPreview = true,
asIframe = false,
showTabs = true,
resizeEnabled = true,
hideWindowActions = false,
showSandpackPreview = false,
isPreviewCentered = false,
// when false .js files will be used
typescriptStrict = false,
showOpenInCodeSandbox = true,
isGradientBox = false,
previewHeight = "auto",
overflow = "visible",
displayMode = "always",
gradientColor,
highlightedLines,
iframeInitialWidth,
iframeSrc,
className,
}) => {
const ref = useRef(null);
const isInView = useInView(ref, {
once: true,
margin: "600px",
});
const {noInline, code} = useCodeDemo({
files,
});
const renderContent = useCallback(
(content: React.ReactNode) => {
if (displayMode === "always") return content;
if (displayMode === "visible") {
if (!isInView) {
return <div style={{height: previewHeight}} />;
}
return content;
}
},
[displayMode, previewHeight, isInView],
);
const previewContent = useMemo(() => {
if (!showPreview) return null;
const content = asIframe ? (
<WindowResizer
hideWindowActions={hideWindowActions}
iframeHeight={previewHeight}
iframeInitialWidth={iframeInitialWidth}
iframeSrc={iframeSrc}
iframeTitle={title}
resizeEnabled={resizeEnabled}
/>
) : (
<DynamicReactLiveDemo
className={className}
code={code}
files={files}
gradientColor={gradientColor}
height={previewHeight}
isCentered={isPreviewCentered}
isGradientBox={isGradientBox}
noInline={noInline}
overflow={overflow}
/>
);
return renderContent(content);
}, [
displayMode,
isGradientBox,
gradientColor,
previewHeight,
hideWindowActions,
asIframe,
showPreview,
isInView,
className,
]);
const editorContent = useMemo(() => {
if (!showEditor) return null;
const content = (
<DynamicSandpack
files={files}
highlightedLines={highlightedLines}
showEditor={showEditor}
showOpenInCodeSandbox={showOpenInCodeSandbox}
showPreview={showSandpackPreview}
typescriptStrict={typescriptStrict}
/>
);
return renderContent(content);
}, [
displayMode,
showEditor,
isInView,
files,
highlightedLines,
showPreview,
showSandpackPreview,
showOpenInCodeSandbox,
]);
const shouldRenderTabs = useMemo(() => {
if (!showTabs) return false;
if (!showPreview) return false;
if (!showEditor) return false;
return true;
}, [showTabs, showPreview, showEditor]);
return (
<div ref={ref} className="flex flex-col gap-2">
{shouldRenderTabs ? (
<Tabs
disableAnimation
aria-label="Code demo tabs"
classNames={{
panel: "pt-0",
}}
variant="underlined"
>
<Tab key="preview" title="Preview">
{previewContent}
</Tab>
<Tab key="code" title="Code">
{editorContent}
</Tab>
</Tabs>
) : (
<>
{previewContent}
{editorContent}
</>
)}
</div>
);
};
@@ -1 +0,0 @@
export * from "./code-demo";
@@ -1,104 +0,0 @@
import React from "react";
import {LivePreview, LiveProvider, LiveError} from "react-live";
import {clsx} from "@heroui/shared-utils";
import * as HeroUI from "@heroui/react";
import * as intlDateUtils from "@internationalized/date";
import * as reactAriaI18n from "@react-aria/i18n";
import * as reactHookFormBase from "react-hook-form";
import {SandpackFiles} from "@codesandbox/sandpack-react/types";
import {BgGridContainer} from "@/components/bg-grid-container";
import {GradientBox, GradientBoxProps} from "@/components/gradient-box";
import {CopyButton} from "@/components/copy-button";
import {StackblitzButton} from "@/components/stackblitz-button";
import {PreviewButton} from "@/components/preview-button";
export interface ReactLiveDemoProps {
code: string;
files: SandpackFiles;
noInline?: boolean;
height?: string | number;
isCentered?: boolean;
isGradientBox?: boolean;
className?: string;
gradientColor?: GradientBoxProps["color"];
overflow?: "auto" | "visible" | "hidden";
typescriptStrict?: boolean;
}
// 🚨 Do not pass react-hook-form to scope, it will break the live preview since
// it also has a "Form" component that will override the one from @heroui/react
const reactHookForm = {
useForm: reactHookFormBase.useForm,
Controller: reactHookFormBase.Controller,
};
export const scope = {
React,
...HeroUI,
...intlDateUtils,
...reactAriaI18n,
...reactHookForm,
} as Record<string, unknown>;
const DEFAULT_FILE = "/App.jsx";
export const ReactLiveDemo: React.FC<ReactLiveDemoProps> = ({
code,
files,
isGradientBox,
gradientColor = "orange",
isCentered = false,
height,
className,
noInline,
typescriptStrict = false,
}) => {
const content = (
<>
{files?.[DEFAULT_FILE] && (
<div className="absolute top-[-26px] right-[3px] z-50 flex items-center">
<StackblitzButton
button={<PreviewButton icon={undefined} />}
className="before:hidden opacity-0 group-hover/code-demo:opacity-100 transition-opacity text-zinc-400"
files={files}
typescriptStrict={typescriptStrict}
/>
<CopyButton
className="before:hidden opacity-0 group-hover/code-demo:opacity-100 transition-opacity text-zinc-400"
value={files?.[DEFAULT_FILE] as string}
/>
</div>
)}
<LivePreview
className={clsx("live-preview flex h-full w-full not-prose ", {
"justify-center items-center": isCentered,
})}
style={{height}}
/>
<LiveError />
</>
);
return (
<LiveProvider code={code} noInline={noInline} scope={scope}>
{isGradientBox ? (
<GradientBox
isCentered
className={clsx(
className,
"relative overflow-y-hidden flex items-center border border-default-200 dark:border-default-100 px-2 py-4 rounded-lg overflow-hidden",
)}
color={gradientColor}
to="top-right"
>
<div className="group/code-demo max-w-full py-4 px-2 w-full h-full scrollbar-hide overflow-x-scroll">
{content}
</div>
</GradientBox>
) : (
<BgGridContainer className={clsx(className, "group/code-demo")}>{content}</BgGridContainer>
)}
</LiveProvider>
);
};
@@ -1,4 +0,0 @@
export type FileCode = {
fileName: string;
code: string;
};
@@ -1,76 +0,0 @@
import {FileCode} from "./types";
import {scope} from "./react-live-demo";
import {transformCode, joinCode, getFileName} from "./utils";
import {SandpackProps} from "@/components/sandpack";
export interface UseCodeDemoProps extends SandpackProps {
code?: string;
}
export const useCodeDemo = ({code: inputCode, files: filesProp}: UseCodeDemoProps) => {
let code = inputCode?.trim();
let noInline = false;
const files = (filesProp || {}) as object;
let filesCode: FileCode[] = [];
//transform scope to key text value
const scopeKeys = Object.keys(scope);
// convert scopeKeys to string values
const scopeValues = scopeKeys.map((key) => {
return {[key]: `${key}`};
});
// add 'React' to scopeValues
scopeValues.push({React: "React"});
// convert scopeValues to object
const imports = Object.assign({}, ...scopeValues);
// if single file
if (Object.keys(files).length === 1) {
// get first item from files
const file = Object.values(files)[0] as string;
code = transformCode(file, imports);
}
// else if multiple files
else {
// get files with its code
Object.entries(files).forEach(([fileName, fileCode]) => {
//only files with .js can processes by react-live
if (!fileName.includes(".js")) {
return;
}
const componentName = getFileName(fileName);
const transformedCode = transformCode(fileCode as string, imports, componentName);
// add to filesCode
filesCode.push({
fileName,
code: transformedCode,
});
});
// sort code by dependency
filesCode = filesCode.sort((a, b) => {
if (a.code.includes(getFileName(b.fileName))) {
return 1;
}
if (b.code.includes(getFileName(a.fileName))) {
return -1;
}
return 0;
});
code = joinCode(filesCode);
}
noInline = code.includes("render");
return {
code,
noInline,
};
};
@@ -1,57 +0,0 @@
import {FileCode} from "./types";
const importRegex = /^(import\s+(?!type\s+\{)[\s\S]*?;)/gm;
const exportDefaultRegex = /export\s+default\s+function\s+\w+\s*\(\s*\)\s*\{/;
export const transformCode = (
code: string,
imports: {[key: string]: any} = {},
compName = "App",
) => {
let cleanedCode = code
.replace(importRegex, (match) => {
// get component name from the match ex. "import { Table } from '@heroui/react'"
const componentName = match.match(/\w+/g)?.[1] || "";
const matchingImport = imports[componentName];
if (matchingImport) {
// remove the matching import
return "";
}
// if match includes './' or '../' then remove it
if (match.includes("./") || match.includes("../")) {
return "";
}
return match;
})
.replace(exportDefaultRegex, () => {
// replace match with const Name = () => (
return `const ${compName} = () => {`;
})
.replace(/export/g, "");
// add render(<App/>) to cleanedCode if has const App = () => {
if (cleanedCode.includes(`const App = () => {`)) {
cleanedCode = `${cleanedCode}\nrender(<${compName}/>);`;
}
// delete comments from the code
cleanedCode = cleanedCode.replace(/\/\*[\s\S]*?\*\/|([^\\:]|^)\/\/.*$/gm, "");
return cleanedCode;
};
export const joinCode = (filesCode: FileCode[]) => {
// join all the code
const code = filesCode.reduce((acc, {code}) => {
return `${acc}${code}`;
}, "");
return code;
};
export const getFileName = (filePath: string) => {
return filePath?.split(".")?.[0]?.replace(/\W/g, "");
};
@@ -1,168 +0,0 @@
import React from "react";
import {motion, useMotionValue, useTransform} from "framer-motion";
import {tv} from "tailwind-variants";
import {useIsMobile} from "@/hooks/use-media-query";
import {useIsomorphicLayoutEffect} from "@/hooks/use-isomorphic-layout-effect";
import {WindowActions} from "@/components/code-window/window-actions";
const resizer = tv({
slots: {
base: "flex items-center justify-end absolute right-[5px] z-10 w-auto xs:hidden",
main: "relative w-full",
barWrapper:
"cursor-ew-resize select-none absolute d-flex justify-center flex items-center w-[10px] h-auto active:opacity-80",
barInner: "relative z-10",
bar: "w-[6px] h-[40px] rounded-full bg-default-400",
iframeWrapper:
"relative z-10 w-full h-full border border-default-200 dark:border-default-100 rounded-lg overflow-hidden",
iframe: "w-full h-[calc(100%_-_2rem)] border-none z-10 overflow-scroll",
},
variants: {
hasInitialWidth: {
true: {
base: "justify-start",
},
},
isMobile: {
true: {
barInner: "hidden",
},
},
enablePointerEvents: {
true: {
iframe: "pointer-events-auto",
iframeWrapper: "pointer-events-auto",
},
false: {
iframe: "pointer-events-none select-none",
iframeWrapper: "pointer-events-none select-none",
},
},
},
defaultVariants: {
hasInitialWidth: false,
isMobile: false,
enablePointerEvents: true,
},
});
export interface WindowResizerProps {
resizeEnabled?: boolean;
hideWindowActions?: boolean;
iframeHeight?: string | number;
iframeMinWidth?: number;
iframeSrc?: string;
iframeInitialWidth?: number;
iframeTitle?: string;
}
const MIN_WIDTH = 320;
const WindowResizer: React.FC<WindowResizerProps> = (props) => {
let constraintsResizerRef = React.useRef<HTMLDivElement>(null);
let resizerRef = React.useRef<HTMLDivElement>(null);
let iframeRef = React.useRef<HTMLIFrameElement>(null);
const [enablePointerEvents, setEnablePointerEvents] = React.useState(true);
const isMobile = useIsMobile();
const {
iframeSrc,
iframeTitle,
resizeEnabled,
hideWindowActions = false,
iframeHeight: height = "420px",
iframeInitialWidth,
iframeMinWidth: minWidth = MIN_WIDTH,
} = props;
const hasInitialWidth = iframeInitialWidth !== undefined;
const {main, base, barInner, barWrapper, bar, iframe, iframeWrapper} = resizer({
hasInitialWidth,
isMobile,
enablePointerEvents,
});
const resizerX = useMotionValue(0);
const browserWidth = useTransform(resizerX, (x) =>
hasInitialWidth ? iframeInitialWidth + x + 14 : `calc(100% + ${x}px - 14px)`,
);
useIsomorphicLayoutEffect(() => {
let observer = new window.ResizeObserver(() => {
if (constraintsResizerRef.current && resizerRef.current) {
let width = constraintsResizerRef.current.offsetWidth - resizerRef.current.offsetWidth;
if (resizerX.get() > width) {
resizerX.set(width);
}
}
});
constraintsResizerRef.current && observer.observe(constraintsResizerRef.current);
return () => {
observer.disconnect();
};
}, []);
React.useEffect(() => {
if (!resizerRef.current) {
return;
}
resizerRef.current.onselectstart = () => false;
}, []);
return (
<div className={main()} style={{height}}>
<motion.div
className={iframeWrapper()}
style={{
width: isMobile ? "100%" : browserWidth,
}}
>
{!hideWindowActions && <WindowActions className="bg-default-100 dark:bg-default-50" />}
<motion.iframe ref={iframeRef} className={iframe()} src={iframeSrc} title={iframeTitle} />
</motion.div>
{resizeEnabled && (
<div
ref={constraintsResizerRef}
className={base({
className: "z-1 top-0 bottom-0 right-0 xs:w-mw-xs",
})}
style={{
width: `calc(100% - ${hasInitialWidth ? iframeInitialWidth : minWidth}px - 20px)`,
}}
>
<motion.div
ref={resizerRef}
_dragX={resizerX}
className={barWrapper()}
drag="x"
dragConstraints={constraintsResizerRef}
dragElastic={0}
dragMomentum={false}
style={{x: resizerX}}
onDragEnd={() => {
document.documentElement.classList.remove("dragging-ew");
iframeRef.current?.classList.remove("dragging-ew");
setEnablePointerEvents(true);
}}
onDragStart={() => {
document.documentElement.classList.add("dragging-ew");
iframeRef.current?.classList.add("dragging-ew");
setEnablePointerEvents(false);
}}
>
<div className={barInner()}>
<div className={bar()} />
</div>
</motion.div>
</div>
)}
</div>
);
};
export default WindowResizer;
@@ -1,311 +0,0 @@
"use client";
import type {Language, PrismTheme} from "prism-react-renderer";
import {useIntersectionObserver} from "usehooks-ts";
import React, {forwardRef, useEffect} from "react";
import {clsx, dataAttr, getUniqueID} from "@heroui/shared-utils";
import BaseHighlight, {defaultProps} from "prism-react-renderer";
import {debounce, omit} from "@heroui/shared-utils";
import {cn} from "@heroui/react";
import defaultTheme from "@/libs/prism-theme";
interface CodeblockProps {
language: Language;
codeString: string;
metastring?: string;
theme?: PrismTheme;
children?: React.ReactNode;
showLines?: boolean;
removeIndent?: boolean;
hideScrollBar?: boolean;
className?: string;
}
type HighlightStyle = "inserted" | "deleted" | undefined;
const cliCommands = [/^init$/, /^add$/, /^upgrade$/, /^remove$/, /^list$/, /^env$/, /^doctor$/];
const highlightStyleToken = [
"bun",
/nextui\s\w+(?=\s?)/,
/^nextui$/,
/heroui\s\w+(?=\s?)/,
/^heroui$/,
"Usage",
...cliCommands,
];
const RE = /{([\d,-]+)}/;
const calculateLinesToHighlight = (meta?: string) => {
if (!meta) {
return () => false;
}
if (!RE.test(meta)) {
return () => false;
}
// @ts-ignore
const lineNumbers = RE.exec(meta)[1]
.split(`,`)
.map((v) => v.split(`-`).map((x) => parseInt(x, 10)));
return (index: number) => {
const lineNumber = index + 1;
const inRange = lineNumbers.some(([start, end]) =>
end ? lineNumber >= start && lineNumber <= end : lineNumber === start,
);
return inRange;
};
};
const calculateHeight = (codeString: string) => {
const lines = codeString.split("\n").length;
return lines * 24;
};
const CodeBlockHighlight = ({
codeString,
language,
codeLang,
theme,
showLines,
removeIndent,
hideScrollBar,
preRef,
isMultiLine,
shouldHighlightLine,
highlightStyle,
className: classNameProp,
...props
}: CodeblockProps & {
codeLang: Language;
isMultiLine: boolean;
shouldHighlightLine: (index: number) => boolean;
highlightStyle: HighlightStyle[];
preRef: React.Ref<HTMLElement>;
}) => {
const height = calculateHeight(codeString);
const [intersectionRef, isVisible] = useIntersectionObserver({
threshold: 0,
});
return (
<div
ref={intersectionRef}
style={{
height: isVisible ? "auto" : `${height}px`,
// due to display: contents on the scrollable child element, this div will also scroll
// this causes the intersection observer to trigger if scrolled far enough horizontally
// set the width to fit-content to prevent this div from going off screen
width: "fit-content",
}}
>
{isVisible ? (
<BaseHighlight
{...defaultProps}
code={codeString}
language={codeLang}
theme={theme}
{...props}
>
{({className, style, tokens, getLineProps, getTokenProps}) => (
<pre
ref={(element) => {
// Merge the refs
if (typeof preRef === "function") {
preRef(element);
} else if (preRef) {
// @ts-ignore
preRef.current = element;
}
}}
className={clsx(
className,
classNameProp,
`language-${codeLang}`,
"max-w-full contents",
{
"flex-col": isMultiLine,
"overflow-x-scroll scrollbar-hide": hideScrollBar,
},
)}
data-language={language}
style={style}
>
{tokens.map((line, i) => {
const lineProps = getLineProps({line, key: i});
return (
<div
{...omit(lineProps, ["key"])}
key={`${i}-${getUniqueID("line-wrapper")}`}
className={clsx(
lineProps.className,
removeIndent ? "pr-4" : "px-4",
"relative [&>span]:relative [&>span]:z-10",
{
"px-2": showLines,
},
{
"before:to-code-background before:absolute before:left-0 before:z-0 before:h-full before:w-full before:bg-gradient-to-r before:from-white/10 before:content-[''] before:pointer-events-none":
shouldHighlightLine(i),
},
)}
data-deleted={dataAttr(highlightStyle?.[i] === "deleted")}
data-inserted={dataAttr(highlightStyle?.[i] === "inserted")}
>
{showLines && (
<span
className={cn(
"mr-6 select-none text-xs opacity-30",
i + 1 >= 10 ? "mr-4" : "",
i + 1 >= 100 ? "mr-2" : "",
i + 1 >= 1000 ? "mr-0" : "",
)}
>
{i + 1}
</span>
)}
{line.map((token, key) => {
const props = getTokenProps({token, key}) || {};
return (
<span
{...omit(props, ["key"])}
key={`${key}-${getUniqueID("line")}`}
className={className}
style={{
...props.style,
...(highlightStyleToken.some((t) => {
const content = token.content.trim();
const regex = t instanceof RegExp ? t : new RegExp(t);
return regex.test(content);
})
? {color: "rgb(var(--code-function))"}
: {}),
}}
/>
);
})}
</div>
);
})}
</pre>
)}
</BaseHighlight>
) : (
<div className={clsx(classNameProp, "w-full bg-code-background rounded-lg")} />
)}
</div>
);
};
const Codeblock = forwardRef<HTMLPreElement, CodeblockProps>(
(
{
codeString,
language,
showLines,
theme: themeProp,
metastring,
hideScrollBar,
removeIndent,
className: classNameProp,
...props
},
ref,
) => {
const theme = themeProp || defaultTheme;
const shouldHighlightLine = calculateLinesToHighlight(metastring);
const isMultiLine = codeString.split("\n").length > 2;
const lastSelectionText = React.useRef<string | null>(null);
const isDiff = language.includes("diff");
const codeLang = isDiff ? (language.split("-")[1] as Language) : language;
let highlightStyle: HighlightStyle[] = [];
if (isDiff) {
let code: string[] = [];
highlightStyle = codeString.split?.("\n").map((line) => {
if (line.startsWith("+")) {
code.push(line.substr(1));
return "inserted";
}
if (line.startsWith("-")) {
code.push(line.substr(1));
return "deleted";
}
code.push(line);
});
codeString = code.join("\n");
}
useEffect(() => {
const handleSelectionChange = () => {
if (!window.getSelection) return;
const el = window.getSelection()?.anchorNode?.parentNode;
if (!el) return;
const selectionText = window.getSelection()?.toString();
if (!selectionText) return;
if (
!selectionText ||
selectionText === lastSelectionText.current ||
!codeString.includes(selectionText)
)
return;
lastSelectionText.current = selectionText;
};
const debouncedHandleSelectionChange = debounce(handleSelectionChange, 1000);
document.addEventListener("selectionchange", debouncedHandleSelectionChange);
return () => {
document.removeEventListener("selectionchange", debouncedHandleSelectionChange);
};
}, []);
return (
<CodeBlockHighlight
className={classNameProp}
codeLang={codeLang}
codeString={codeString}
hideScrollBar={hideScrollBar}
highlightStyle={highlightStyle}
isMultiLine={isMultiLine}
language={language}
preRef={ref}
removeIndent={removeIndent}
shouldHighlightLine={shouldHighlightLine}
showLines={showLines}
theme={theme}
{...props}
/>
);
},
);
Codeblock.displayName = "CodeBlock";
export default Codeblock;
@@ -1,19 +0,0 @@
import {FeaturesGrid} from "@/components/marketing/features-grid";
import {communityAccounts} from "@/libs/constants";
export const Community = () => {
return (
<div className="max-w-4xl flex flex-col gap-8">
<FeaturesGrid
classNames={{
base: "lg:grid-cols-3",
iconWrapper: "bg-transparent",
header: "pt-2",
body: "pt-0 pb-2",
description: "hidden",
}}
features={communityAccounts}
/>
</div>
);
};
@@ -1,136 +0,0 @@
import {Button, ButtonProps, Code, Link, Tooltip} from "@heroui/react";
import {ReactNode} from "react";
import Balancer from "react-wrap-balancer";
import {usePostHog} from "posthog-js/react";
import {GithubIcon, NpmIcon, AdobeIcon, StorybookIcon, NextJsIcon} from "@/components/icons";
import {COMPONENT_PATH, COMPONENT_THEME_PATH} from "@/libs/github/constants";
export interface ComponentLinksProps {
component: string;
npm?: string;
source?: string;
styles?: string;
storybook?: string;
rscCompatible?: boolean;
reactAriaHook?: string;
}
const ButtonLink = ({
children,
href,
startContent,
tooltip,
...props
}: ButtonProps & {
href: string;
tooltip?: string | ReactNode;
}) => {
const posthog = usePostHog();
const handlePress = () => {
if (!href) return;
posthog.capture("ComponentLinks - Click", {
category: "docs",
action: "click",
data: href || "",
});
};
const button = (
<Button
isExternal
as={Link}
className="!text-small py-4 bg-default-100 dark:bg-default-50 text-default-700"
href={href}
size="sm"
startContent={startContent}
onPress={handlePress}
{...props}
>
{children}
</Button>
);
return tooltip ? (
<Tooltip className="max-w-[230px]" content={tooltip}>
{button}
</Tooltip>
) : (
button
);
};
export const ComponentLinks = ({
component,
npm,
source,
storybook,
styles,
rscCompatible,
reactAriaHook,
}: ComponentLinksProps) => {
if (!component) {
return null;
}
return (
<div className="flex flex-wrap gap-3 mt-6">
<ButtonLink
href={`https://storybook.heroui.com/?path=/story/components-${
storybook || component
}--default`}
startContent={<StorybookIcon className="text-lg text-[#ff4785]" />}
>
Storybook
</ButtonLink>
<ButtonLink
href={`https://www.npmjs.com/package/@heroui/${npm || component}`}
startContent={<NpmIcon className="text-2xl text-[#E53E3E]" />}
>
{`@heroui/${npm || component}`}
</ButtonLink>
{reactAriaHook && (
<ButtonLink
href={`https://react-spectrum.adobe.com/react-aria/${reactAriaHook}.html`}
startContent={<AdobeIcon className="text-lg text-[#E1251B]" />}
>
React Aria
</ButtonLink>
)}
{rscCompatible && (
<ButtonLink
href="https://nextjs.org/docs/app/building-your-application/rendering/server-components"
startContent={<NextJsIcon size={18} />}
tooltip={
<p>
<Balancer>
This component doesn&apos;t use the
<Code className="font-normal bg-transparent px-0 py-0 text-code-mdx">
`use client;`
</Code>
directive making it compatible with RSC.
</Balancer>
</p>
}
>
Server component
</ButtonLink>
)}
<ButtonLink
href={`${COMPONENT_PATH}/${source || component}`}
startContent={<GithubIcon size={20} />}
>
Source
</ButtonLink>
<ButtonLink
href={`${COMPONENT_THEME_PATH}/${styles || component}.ts`}
startContent={<GithubIcon size={20} />}
>
Styles source
</ButtonLink>
</div>
);
};
@@ -1,38 +0,0 @@
import {NewNextJSIcon, ViteIcon, RemixIcon, AstroIcon} from "@/components/icons";
import {FeaturesGrid} from "@/components/marketing/features-grid";
const frameworks = [
{
title: "Next.js",
icon: <NewNextJSIcon height={40} width={40} />,
href: "/docs/frameworks/nextjs",
},
{
title: "Vite",
icon: <ViteIcon height={40} width={40} />,
href: "/docs/frameworks/vite",
},
{
title: "Remix",
icon: <RemixIcon className="text-foreground" height={40} width={40} />,
href: "/docs/frameworks/remix",
},
{
title: "Astro",
icon: <AstroIcon className="text-foreground" height={40} width={40} />,
href: "/docs/frameworks/astro",
},
];
export const Frameworks = () => {
return (
<FeaturesGrid
classNames={{
base: "mt-8 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-2 gap-4",
header: "pb-3",
iconWrapper: "bg-default-300/20",
}}
features={frameworks}
/>
);
};
@@ -1,209 +0,0 @@
import type Highlight from "prism-react-renderer";
export type TransformTokens = Parameters<Highlight["props"]["children"]>[0]["tokens"];
export type TransformTokensTypes = TransformTokens[0][0] & {
folderContent?: TransformTokens;
summaryContent?: TransformTokens[0];
class?: string;
index?: number;
open?: boolean;
};
const startFlag = ["{", "["];
const endFlag = ["}", "]"];
const isElementStartRegex = /^\s*</;
const isElementEndRegex = /^\s*<\//;
/**
* Transform tokens from `prism-react-renderer` to wrap them in folder structure
*
* @example
* transformTokens(tokens) -> wrap tokens in folder structure
*/
export function transformTokens(tokens: TransformTokens, folderLine = 10) {
const result: TransformTokens = [];
let lastIndex = 0;
let startElementName = "";
tokens.forEach((token, index) => {
if (index < lastIndex) {
return;
}
token.forEach((t) => {
(t as TransformTokensTypes).index = index;
});
result.push(token);
const lineContent = getLineContent(token);
const {isStartTag, isEndTag} = checkIsElement(lineContent);
// If it has startElementName means it is within the element range
if (startElementName) {
if (isEndTag) {
// Judge whether it is the end tag of the element then reset startElementName
const {endElementName} = getElementName(lineContent);
if (endElementName === startElementName) {
startElementName = "";
}
}
return;
} else if (isStartTag) {
const {startElementName: elementName, endElementName} = getElementName(lineContent);
if (!endElementName) {
startElementName = elementName;
return;
}
}
let startToken: TransformTokens[0][0] = null as any;
token.forEach((t) => {
if (startFlag.includes(t.content)) {
startToken = t;
}
});
const isFolder = checkIsFolder(token);
if (isFolder && startToken) {
const nextLineContent = tokens.slice(index + 1, index + 2).reduce((acc, line) => {
return acc + getLineContent(line);
}, "");
const isNextLineObjectFolder = checkIsObjectContent(nextLineContent);
const isArrayFolder = lineContent.trim().endsWith("[");
if (isNextLineObjectFolder || isArrayFolder) {
const endIndex = findEndIndex(tokens, index + 1);
// Greater than or equal to folderLine then will folder otherwise it will show directly
if (endIndex !== -1 && endIndex - index >= folderLine) {
lastIndex = endIndex;
const folder = tokens.slice(index + 1, endIndex);
const endToken = tokens[endIndex];
(endToken[0] as TransformTokensTypes).class = "first-custom-folder";
const ellipsisToken: TransformTokensTypes = {
types: ["ellipsis"],
content: "",
class: "custom-folder ellipsis-token",
};
const copyContent: TransformTokensTypes = {
types: ["copy"],
content: "",
folderContent: folder,
class: "custom-folder copy-token",
};
endToken.forEach((t, _, arr) => {
let className = (t as TransformTokensTypes).class || "";
className += " custom-folder";
if (t.content.trim() === "" && (arr.length === 3 || arr.length === 4)) {
// Add length check to sure it's added to } token
className += " empty-token";
}
(t as TransformTokensTypes).class = className;
});
startToken.types = ["folderStart"];
(startToken as TransformTokensTypes).folderContent = folder;
(startToken as TransformTokensTypes).summaryContent = [
...token,
ellipsisToken,
copyContent,
...endToken,
];
(startToken as TransformTokensTypes).index = index;
// isShowFolder && ((startToken as TransformTokensTypes).open = true);
result.splice(result.length - 1, 1, [startToken]);
return;
}
}
}
});
return result;
}
function checkIsFolder(token: TransformTokens[0]) {
const stack: string[] = [];
for (const t of token) {
if (startFlag.includes(t.content)) {
stack.push(t.content);
} else if (endFlag.includes(t.content)) {
stack.pop();
}
}
return stack.length !== 0;
}
function findEndIndex(tokens: TransformTokens, startIndex: number) {
const stack: string[] = ["flag"];
for (let i = startIndex; i < tokens.length; i++) {
const token = tokens[i];
for (const line of token) {
const transformLine = line.content.replace(/\$/g, "");
if (startFlag.includes(transformLine)) {
stack.push("flag");
} else if (endFlag.includes(transformLine)) {
stack.pop();
}
if (stack.length === 0) {
return i;
}
}
}
return -1;
}
function checkIsElement(lineContent: string) {
return {
isStartTag: isElementStartRegex.test(lineContent),
isEndTag: isElementEndRegex.test(lineContent),
};
}
function getElementName(lineContent: string) {
const startElementName = lineContent.match(/^\s*<([a-zA-Z.]+)/);
const endElementName = lineContent.match(/^\s*<\/([a-zA-Z.]+)>/);
return {
startElementName: startElementName?.[1] || (lineContent.includes("<>") ? "<>" : ""),
endElementName: endElementName?.[1] || (lineContent.includes("</>") ? "</>" : ""),
};
}
function getLineContent(token: TransformTokens[0]) {
return token.reduce((acc, t) => acc + t.content, "");
}
function checkIsObjectContent(lineContent: string) {
lineContent = lineContent.trim();
// first: match { a }
// second: match { a: b }
// third: match { a (b) }
// fourth: match /** */
const isObjectContent = /^([\w]+,?$)|([\w\[.\]]+:)|([\w]+\s?\(.*?\)$)|(^\/\*\*)/.test(
lineContent,
);
const hasEqual = /\s=\s/.test(lineContent);
const hasFunction = lineContent.includes("function");
const hasVariable = /var|let|const/.test(lineContent);
return isObjectContent && !hasEqual && !hasFunction && !hasVariable;
}

Some files were not shown because too many files have changed in this diff Show More