chore: initialize project structure
This commit is contained in:
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
+8
-2
@@ -1,9 +1,15 @@
|
||||
root=true
|
||||
|
||||
[*]
|
||||
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
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
|
||||
[*.md]
|
||||
max_line_length=off
|
||||
trim_trailing_whitespace=false
|
||||
|
||||
@@ -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=
|
||||
@@ -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
|
||||
@@ -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"]
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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"
|
||||
@@ -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}`];
|
||||
},
|
||||
};
|
||||
@@ -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,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
|
||||
@@ -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"
|
||||
}
|
||||
Vendored
+9
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"bradlc.vscode-tailwindcss",
|
||||
"editorconfig.editorconfig",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"esbenp.prettier-vscode",
|
||||
"unifiedjs.vscode-mdx"
|
||||
]
|
||||
}
|
||||
Vendored
+31
-3
@@ -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/**"]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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/
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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 />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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",
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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 "{query}"</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";
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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";
|
||||
@@ -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
|
||||
<HeroUILink as={Link} href={`/docs/components/${lowerTitle}`}>
|
||||
{lowerTitle}
|
||||
</HeroUILink>
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -1,3 +0,0 @@
|
||||
export * from "./user-twitter-card";
|
||||
export * from "./music-player";
|
||||
export * from "./custom-button";
|
||||
@@ -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" />
|
||||
// );
|
||||
// }
|
||||
@@ -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
|
||||
<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'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
Reference in New Issue
Block a user