Compare commits
294 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ea5e0fb096 | |||
| 26f64d043d | |||
| a078c3dcd2 | |||
| 5a50390e3f | |||
| 894d85d059 | |||
| 0ef41d27e7 | |||
| 4200164712 | |||
| 6d4c6980d5 | |||
| f3f36a7146 | |||
| 8de209bff7 | |||
| e9d01be74d | |||
| 66ac2e67ae | |||
| 7ed88f39c1 | |||
| 32d18bfd41 | |||
| a1b32c1ceb | |||
| 9e683b32ba | |||
| b7c947805d | |||
| a76fdfa774 | |||
| ac605eb71f | |||
| 5b343b6181 | |||
| 05562dcf90 | |||
| 0a60388258 | |||
| d794225cb7 | |||
| ec2ed200f5 | |||
| 2f7703ec8e | |||
| 25cbe1146f | |||
| 3db9b6248e | |||
| b1b5eda15b | |||
| e3e13a095f | |||
| 86f921d098 | |||
| eefda8d6e2 | |||
| 2e4ce7bcee | |||
| 5a4c4f498d | |||
| ec30a9adbd | |||
| a6954c4156 | |||
| b887a2f503 | |||
| edc2bc741e | |||
| a1a02dc0db | |||
| cd1dc8b469 | |||
| 459ac5ed45 | |||
| 9c4bb22e87 | |||
| 7fa5459381 | |||
| b77b8fa9d7 | |||
| c6c4a4d6a1 | |||
| e96b1dbab0 | |||
| 1b55d27645 | |||
| bdc246a8a4 | |||
| 5064bfe4e4 | |||
| 36ddbff557 | |||
| 043b42c895 | |||
| c8330b8106 | |||
| 7f20346423 | |||
| 85b3fd7269 | |||
| 649204ceac | |||
| c7a1376872 | |||
| 3323592211 | |||
| fb570a372d | |||
| 184d3a9c49 | |||
| 849eff5715 | |||
| b400f3ee6d | |||
| dedede0be0 | |||
| e96a23c8a3 | |||
| 7efd6c97e4 | |||
| 43e5e4fb26 | |||
| 39da69cf81 | |||
| 8743a6c6b6 | |||
| c635c951d6 | |||
| cb05342a4a | |||
| 59ef7bc6b6 | |||
| cfd19415ea | |||
| 39ca892b4e | |||
| fc2dad8b39 | |||
| d9bbd2e45f | |||
| 75bc039b4b | |||
| e79bd0fa4c | |||
| 65c5af4f63 | |||
| f58c8a7d99 | |||
| 9da06aaa50 | |||
| 508f11e968 | |||
| 2812f61f4f | |||
| 0ee0e95678 | |||
| e940ec06ac | |||
| 1976c62105 | |||
| aedda9d0de | |||
| 38afbba681 | |||
| cd941e64c1 | |||
| eec0ab7584 | |||
| a415a6f0f7 | |||
| 0008709813 | |||
| 8248f0c36f | |||
| 78556ab9ba | |||
| 02fd961a71 | |||
| b054bfa3b6 | |||
| 597ff59e51 | |||
| 3e5af52ae1 | |||
| 698b2a9e25 | |||
| b7477e008d | |||
| caf6166d49 | |||
| 61e07fbdde | |||
| 96f55f10b8 | |||
| 161cd23658 | |||
| 49e3009d7d | |||
| 3094c3d226 | |||
| 6cd84fed80 | |||
| b1a9bb2664 | |||
| 6796ccb76f | |||
| ad45bf5619 | |||
| 9f2e1249d5 | |||
| 12b5f0ed33 | |||
| 2b1841e362 | |||
| 9bd88dec48 | |||
| f0123e6be6 | |||
| 70a5853cd8 | |||
| ffd1c4d535 | |||
| d745cd114f | |||
| d1252d7c50 | |||
| 3277ef2b0a | |||
| ea2ab14d43 | |||
| 4f096a8096 | |||
| 01e764813d | |||
| 76ef3a9b7d | |||
| 5f35439503 | |||
| 598af42e2c | |||
| 3a1868baf5 | |||
| 72d7158034 | |||
| 504eb1dc47 | |||
| 1ff1c7b111 | |||
| 779f54f726 | |||
| a822eddc11 | |||
| 9cae2d2625 | |||
| fee73b4f77 | |||
| 4049eae3c6 | |||
| 874bbe87e7 | |||
| 950eabacfd | |||
| 6fe0a81cd2 | |||
| 8810699b14 | |||
| 7e29dddea3 | |||
| 63cf041067 | |||
| b4b75444e7 | |||
| 4ec33a4c78 | |||
| 5acfb2e7a9 | |||
| b64e97c94f | |||
| f4224c9c46 | |||
| 75dfb5461d | |||
| 1251cc9a9b | |||
| 4c52d1d1cf | |||
| fdc513cefe | |||
| 09f30f93aa | |||
| 6ccbabe788 | |||
| d752438a26 | |||
| 653e7d2031 | |||
| 2750189f66 | |||
| 29f0d9a937 | |||
| 5b5756bb33 | |||
| 10ac6ced32 | |||
| 3b0584ee83 | |||
| 4567cf525d | |||
| 77d56aae0c | |||
| 81c45e6336 | |||
| 00dcf6cbc8 | |||
| eab6dec219 | |||
| ca7309c6eb | |||
| 316010e210 | |||
| d11a3a8607 | |||
| 15b585c51a | |||
| 234f217a1c | |||
| 0267bf7c2f | |||
| 114c3f5a6b | |||
| ce9abed071 | |||
| c24efed184 | |||
| 6b88866307 | |||
| 436fd99b09 | |||
| 44722b6430 | |||
| cbced8df45 | |||
| 99993cd332 | |||
| f5db202b13 | |||
| 1658afc756 | |||
| f26cfbe9fb | |||
| 1fd3ad5805 | |||
| 6ebdcac4fc | |||
| 19d0766487 | |||
| d906885857 | |||
| 74e7c378a1 | |||
| 21c7934aba | |||
| 73ce35a1b9 | |||
| 4c61bff868 | |||
| 7c3df1b0d9 | |||
| ced7e6093a | |||
| 4ce707d55f | |||
| e128447be6 | |||
| f6e4aa8adf | |||
| f4c9f21959 | |||
| 230213853e | |||
| 47053f6875 | |||
| b56a89d0be | |||
| a27ca56c08 | |||
| fdecb49117 | |||
| 63a2f2d4b3 | |||
| 3f77004403 | |||
| a5055bc81c | |||
| 9fadc43657 | |||
| dcd8e0b8a9 | |||
| 0d16f7d78a | |||
| bb0fa71e69 | |||
| a30ba4bc95 | |||
| 06bae4ffaf | |||
| a3979a88a8 | |||
| cfaf732d27 | |||
| 98f0f2357d | |||
| 129b30f956 | |||
| 71741a115a | |||
| 4e29da6196 | |||
| 5da8ea5d0b | |||
| 21536b82fc | |||
| b475f892e2 | |||
| 38114b16da | |||
| 5e33f2c816 | |||
| 56e8aba53a | |||
| c589405b94 | |||
| e70819ec7b | |||
| 3406c29911 | |||
| 1ec784b3a5 | |||
| 810f2b2bd9 | |||
| e802caa412 | |||
| cfa43f22c7 | |||
| 0ac048c11f | |||
| 57ec8944c3 | |||
| aceafaff56 | |||
| 5a7e63a40a | |||
| c4f8d7015b | |||
| 0a3ea728a1 | |||
| 1ce246d0ad | |||
| d165a4beb6 | |||
| 10c1292fc5 | |||
| 27041fa545 | |||
| 54e9c8231f | |||
| 8ee4cd4e0c | |||
| 87ea9a9eba | |||
| 720d944acd | |||
| cfd6555d6d | |||
| 8df3fe624a | |||
| d686129687 | |||
| 2207771540 | |||
| b62ca94458 | |||
| 76a5c07e1f | |||
| 51c6b291d5 | |||
| c5bcd70457 | |||
| 78f1c5f957 | |||
| 1ccdfb5b98 | |||
| dc5e4a1fd2 | |||
| 51cc2f6531 | |||
| 45bb9fddc4 | |||
| a09258b47c | |||
| 3a4c530785 | |||
| 8d22a7bc56 | |||
| 017d40b623 | |||
| a1f5109365 | |||
| 25db8f732d | |||
| 89f2bc9be4 | |||
| 4b8655e267 | |||
| 849dd9948b | |||
| e42d3ab751 | |||
| e7bfb81b48 | |||
| 5df7003482 | |||
| 131330713f | |||
| 6f12edbfc6 | |||
| 0e558c60c3 | |||
| a0504a8e0e | |||
| 36ba63ff10 | |||
| e8dbcab525 | |||
| 2e4f54a06b | |||
| bb27ca5e11 | |||
| 21cae37835 | |||
| 6a4e16b224 | |||
| 1569b59725 | |||
| 6cb0e5a97b | |||
| e7f9579389 | |||
| e8fb0c7a09 | |||
| 90e76df87d | |||
| 0ac5a339f4 | |||
| 69d9e2bcdc | |||
| 102387a004 | |||
| fa404913bd | |||
| bee0c50c4b | |||
| 0b3ebbcfdf | |||
| dc0b3897d0 | |||
| 2cb983af3e | |||
| a0d4d39eab | |||
| c00d3f3199 | |||
| f564f5bd2a | |||
| 62c3c71e9e | |||
| f7c77e2f14 | |||
| 088c08dd63 | |||
| 5168f4f495 |
@@ -1,6 +1,9 @@
|
||||
{
|
||||
"$schema": "https://unpkg.com/@changesets/config@2.3.0/schema.json",
|
||||
"changelog": "@changesets/cli/changelog",
|
||||
"changelog": [
|
||||
"@changesets/changelog-github",
|
||||
{ "repo": "nextui-org/nextui" }
|
||||
],
|
||||
"commit": false,
|
||||
"fixed": [],
|
||||
"linked": [],
|
||||
|
||||
+2
-1
@@ -1,7 +1,7 @@
|
||||
.now/*
|
||||
.next/*
|
||||
*.css
|
||||
examples/*
|
||||
.changeset
|
||||
dist
|
||||
esm/*
|
||||
public/*
|
||||
@@ -22,3 +22,4 @@ build
|
||||
!react-shim.js
|
||||
!tsup.config.ts
|
||||
apps/docs/preinstall.js
|
||||
apps/docs/next-redirect.js
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
name: "Install"
|
||||
description: "Sets up Node.js and runs install"
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Install dependencies
|
||||
uses: pnpm/action-setup@v2.2.4
|
||||
with:
|
||||
version: 8
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Setup Git User
|
||||
shell: bash
|
||||
run: |
|
||||
git config --global user.email "jrgarciadev@gmail.com"
|
||||
git config --global user.name "Junior Garcia"
|
||||
|
||||
- name: Install dependencies
|
||||
shell: bash
|
||||
run: pnpm install
|
||||
@@ -0,0 +1,72 @@
|
||||
name: QA
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout branch
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install
|
||||
uses: ./.github/common-actions/install
|
||||
|
||||
- name: Build packages
|
||||
run: pnpm build
|
||||
|
||||
tests:
|
||||
name: Tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout branch
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install
|
||||
uses: ./.github/common-actions/install
|
||||
|
||||
- name: Run tests
|
||||
run: pnpm test
|
||||
|
||||
eslint:
|
||||
name: ESLint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout branch
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install
|
||||
uses: ./.github/common-actions/install
|
||||
|
||||
- name: Run ESLint
|
||||
run: pnpm lint
|
||||
|
||||
types:
|
||||
name: TypeScript
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout branch
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install
|
||||
uses: ./.github/common-actions/install
|
||||
|
||||
- name: Run typecheck
|
||||
run: pnpm typecheck
|
||||
|
||||
prettier:
|
||||
name: Prettier
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout branch
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install
|
||||
uses: ./.github/common-actions/install
|
||||
|
||||
- name: Run prettier
|
||||
run: pnpm format:check
|
||||
@@ -1,128 +0,0 @@
|
||||
name: CI/CD
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths-ignore: ['**/README.md']
|
||||
pull_request:
|
||||
branches: [main]
|
||||
types: [opened, synchronize]
|
||||
paths-ignore: ['**/README.md']
|
||||
|
||||
jobs:
|
||||
avoid_reduncy:
|
||||
runs-on: ubuntu-20.04
|
||||
if: ${{ github.event_name == 'push' }}
|
||||
steps:
|
||||
- name: Cancel Previous Redundant Builds
|
||||
uses: styfle/cancel-workflow-action@0.9.1
|
||||
with:
|
||||
access_token: ${{ github.token }}
|
||||
|
||||
lint:
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: 'yarn'
|
||||
node-version: '16'
|
||||
- name: Install dependencies
|
||||
run: yarn install --frozen-lockfile
|
||||
- name: Lint
|
||||
run: yarn lint
|
||||
|
||||
build:
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: 'yarn'
|
||||
node-version: '16'
|
||||
- name: Install dependencies
|
||||
run: yarn install --frozen-lockfile
|
||||
- uses: actions/cache@v3
|
||||
with:
|
||||
path: ${{ github.workspace }}/.next/cache
|
||||
# Generate a new cache whenever packages or source files change.
|
||||
key: ${{ runner.os }}-nextjs-${{ hashFiles('**/yarn.lock') }}-${{ hashFiles('**.[jt]s', '**.[jt]sx') }}
|
||||
# If source files changed but packages didn't, rebuild from a prior cache.
|
||||
restore-keys: |
|
||||
${{ runner.os }}-nextjs-${{ hashFiles('**/yarn.lock') }}-
|
||||
- name: Build
|
||||
run: yarn build
|
||||
|
||||
tests:
|
||||
needs: [lint, build]
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: 'yarn'
|
||||
node-version: '16'
|
||||
- name: Install dependencies
|
||||
run: yarn install --frozen-lockfile
|
||||
- name: Test
|
||||
run: yarn coverage:update
|
||||
- uses: codecov/codecov-action@v2
|
||||
with:
|
||||
directory: './packages/react'
|
||||
|
||||
publish:
|
||||
needs: [build, tests]
|
||||
runs-on: ubuntu-20.04
|
||||
if: ${{ github.event_name == 'push' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: 'yarn'
|
||||
node-version: '16'
|
||||
- name: Install dependencies
|
||||
run: yarn install --frozen-lockfile
|
||||
- name: Pre puiblish NextUI package
|
||||
run: yarn pre-publish:nextui
|
||||
- uses: JS-DevTools/npm-publish@v1
|
||||
with:
|
||||
token: ${{ secrets.NPM_TOKEN }}
|
||||
package: './packages/react/lib/package.json'
|
||||
- name: Show published version
|
||||
if: steps.publish.outputs.type != 'none'
|
||||
run: |
|
||||
echo "Version changed: ${{ steps.publish.outputs.old-version }} => ${{ steps.publish.outputs.version }}"
|
||||
- name: Tag new version
|
||||
if: steps.publish.outputs.type != 'none'
|
||||
uses: Klemensas/action-autotag@stable
|
||||
with:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
tag_prefix: "v"
|
||||
package_root: "./packages/react"
|
||||
|
||||
update_algolia_index:
|
||||
needs: [publish]
|
||||
runs-on: ubuntu-20.04
|
||||
if: ${{ github.event_name == 'push' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: 'yarn'
|
||||
node-version: '16'
|
||||
- name: Install dependencies
|
||||
run: yarn install --frozen-lockfile
|
||||
- name: Upload docs on Algolia
|
||||
run: yarn build:docs-meta
|
||||
env:
|
||||
ALGOLIA_APP_ID: ${{ secrets.ALGOLIA_APP_ID }}
|
||||
ALGOLIA_ADMIN_API_KEY: ${{ secrets.ALGOLIA_ADMIN_API_KEY }}
|
||||
ALGOLIA_SEARCH_API_KEY: ${{ secrets.ALGOLIA_SEARCH_API_KEY }}
|
||||
@@ -1,17 +1,51 @@
|
||||
name: Tagged Release
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
paths:
|
||||
- ".changeset/**"
|
||||
- "packages/**"
|
||||
branches:
|
||||
- main
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
|
||||
jobs:
|
||||
tagged-release:
|
||||
name: Tagged Release
|
||||
runs-on: ubuntu-18.04
|
||||
|
||||
release:
|
||||
name: Release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: "marvinpinto/action-automatic-releases@latest"
|
||||
- name: Checkout branch
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install
|
||||
uses: ./.github/common-actions/install
|
||||
|
||||
- name: Build
|
||||
run: pnpm build
|
||||
|
||||
- name: Tests
|
||||
run: pnpm test
|
||||
|
||||
- name: Create Release Pull Request or Publish to NPM
|
||||
id: changesets
|
||||
uses: changesets/action@v1
|
||||
with:
|
||||
repo_token: "${{ github.token }}"
|
||||
prerelease: false
|
||||
publish: pnpm release
|
||||
commit: "ci(changesets): version packages"
|
||||
setupGitUser: false
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Create @dev release
|
||||
if: steps.changesets.outputs.published != 'true'
|
||||
run: |
|
||||
git checkout main
|
||||
pnpm version:dev
|
||||
pnpm release:dev
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -20,6 +20,7 @@ coverage
|
||||
dist/
|
||||
storybook-static
|
||||
/packages/storybook/public/tailwind.css
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env
|
||||
@@ -48,6 +49,8 @@ esm
|
||||
examples/**/yarn.lock
|
||||
examples/**/out
|
||||
examples/**/.next
|
||||
packages/**/*.backup
|
||||
packages/**/*.backup.ts
|
||||
|
||||
.vercel
|
||||
|
||||
@@ -57,3 +60,6 @@ apps/**/sitemap.xml
|
||||
# turbo
|
||||
.turbo
|
||||
packages/**/.turbo
|
||||
|
||||
# content layer
|
||||
.contentlayer
|
||||
|
||||
+1
-1
@@ -1,8 +1,8 @@
|
||||
dist
|
||||
examples
|
||||
node_modules
|
||||
plop
|
||||
coverage
|
||||
.changeset
|
||||
.next
|
||||
build
|
||||
scripts
|
||||
|
||||
@@ -9,7 +9,7 @@ NEXT_PUBLIC_ALGOLIA_SEARCH_API_KEY=
|
||||
# Vercel Env (is used for skipping typescript check)
|
||||
IS_VERCEL_ENV=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
|
||||
@@ -0,0 +1,312 @@
|
||||
[
|
||||
{
|
||||
"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": "https://higherror.github.io/"
|
||||
},
|
||||
{
|
||||
"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/"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,127 @@
|
||||
import type {Metadata} from "next";
|
||||
|
||||
import {notFound} from "next/navigation";
|
||||
import {allBlogPosts} from "contentlayer/generated";
|
||||
import {Link, User} from "@nextui-org/react";
|
||||
import {format, parseISO} from "date-fns";
|
||||
import NextLink from "next/link";
|
||||
import {Balancer} from "react-wrap-balancer";
|
||||
|
||||
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;
|
||||
};
|
||||
}
|
||||
|
||||
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) {
|
||||
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>
|
||||
</h1>
|
||||
<MDXContent code={post.body.code} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import {Image} from "@nextui-org/react";
|
||||
|
||||
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="/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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import {allBlogPosts} from "contentlayer/generated";
|
||||
import {compareDesc} from "date-fns";
|
||||
|
||||
import {BlogPostList} from "@/components/blog-post";
|
||||
|
||||
export default function Blog() {
|
||||
const posts = allBlogPosts.sort((a, b) => compareDesc(new Date(a.date), new Date(b.date)));
|
||||
|
||||
return (
|
||||
<div className="w-full lg:px-16 mt-12">
|
||||
<div className="text-center">
|
||||
<h1 className="mb-2 font-bold text-4xl">NextUI Latest Updates</h1>
|
||||
<h5 className="text-default-500 text-lg">All the latest news about NextUI.</h5>
|
||||
</div>
|
||||
<BlogPostList posts={posts} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
import type {Metadata} from "next";
|
||||
|
||||
import {notFound} from "next/navigation";
|
||||
import {allDocs} from "contentlayer/generated";
|
||||
import {Link} from "@nextui-org/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-4">
|
||||
<DocsToc headings={headings} />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import {Image} from "@nextui-org/react";
|
||||
|
||||
import manifest from "@/config/routes.json";
|
||||
import {DocsSidebar} from "@/components/docs/sidebar";
|
||||
|
||||
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">
|
||||
<div className="grid grid-cols-12">
|
||||
<div className="hidden 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-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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
"use client";
|
||||
|
||||
import {Card, CardBody, CircularProgress} from "@nextui-org/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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Modal,
|
||||
ModalContent,
|
||||
ModalHeader,
|
||||
ModalBody,
|
||||
ModalFooter,
|
||||
Button,
|
||||
useDisclosure,
|
||||
RadioGroup,
|
||||
Radio,
|
||||
ModalProps,
|
||||
} from "@nextui-org/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" onClick={onClose}>
|
||||
Close
|
||||
</Button>
|
||||
<Button color="primary" onPress={onClose}>
|
||||
Action
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</>
|
||||
)}
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Navbar,
|
||||
NavbarBrand,
|
||||
NavbarContent,
|
||||
NavbarItem,
|
||||
Link as NextUiLink,
|
||||
LinkProps,
|
||||
Button,
|
||||
} from "@nextui-org/react";
|
||||
|
||||
const Link = (props: LinkProps) => <NextUiLink {...props} onClick={(e) => e.preventDefault()} />;
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Navbar,
|
||||
NavbarBrand,
|
||||
NavbarContent,
|
||||
NavbarItem,
|
||||
NavbarMenu,
|
||||
NavbarMenuItem,
|
||||
NavbarMenuToggle,
|
||||
Button,
|
||||
Link as NextUiLink,
|
||||
LinkProps,
|
||||
} from "@nextui-org/react";
|
||||
import React from "react";
|
||||
|
||||
const Link = (props: LinkProps) => <NextUiLink {...props} onClick={(e) => e.preventDefault()} />;
|
||||
|
||||
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 isBordered onMenuOpenChange={setIsMenuOpen}>
|
||||
<NavbarContent className="sm:hidden" justify="start">
|
||||
<NavbarMenuToggle aria-label={isMenuOpen ? "Close menu" : "Open menu"} />
|
||||
</NavbarContent>
|
||||
|
||||
<NavbarContent className="sm:hidden pr-3" justify="center">
|
||||
<NavbarBrand>
|
||||
<AcmeLogo />
|
||||
<p className="font-bold text-inherit">ACME</p>
|
||||
</NavbarBrand>
|
||||
</NavbarContent>
|
||||
|
||||
<NavbarContent className="hidden sm:flex gap-4" justify="center">
|
||||
<NavbarBrand>
|
||||
<AcmeLogo />
|
||||
<p className="font-bold text-inherit">ACME</p>
|
||||
</NavbarBrand>
|
||||
<NavbarItem>
|
||||
<Link color="foreground" href="#">
|
||||
Features
|
||||
</Link>
|
||||
</NavbarItem>
|
||||
<NavbarItem isActive>
|
||||
<Link aria-current="page" color="warning" 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="warning" href="#" variant="flat">
|
||||
Sign Up
|
||||
</Button>
|
||||
</NavbarItem>
|
||||
</NavbarContent>
|
||||
|
||||
<NavbarMenu>
|
||||
{menuItems.map((item, index) => (
|
||||
<NavbarMenuItem key={`${item}-${index}`}>
|
||||
<Link
|
||||
className="w-full"
|
||||
color={
|
||||
index === 2 ? "warning" : index === menuItems.length - 1 ? "danger" : "foreground"
|
||||
}
|
||||
href="#"
|
||||
size="lg"
|
||||
>
|
||||
{item}
|
||||
</Link>
|
||||
</NavbarMenuItem>
|
||||
))}
|
||||
</NavbarMenu>
|
||||
</Navbar>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Navbar,
|
||||
NavbarBrand,
|
||||
NavbarContent,
|
||||
NavbarItem,
|
||||
Link as NextUiLink,
|
||||
LinkProps,
|
||||
Button,
|
||||
} from "@nextui-org/react";
|
||||
|
||||
const Link = (props: LinkProps) => <NextUiLink {...props} onClick={(e) => e.preventDefault()} />;
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Navbar,
|
||||
NavbarBrand,
|
||||
NavbarContent,
|
||||
NavbarItem,
|
||||
Button,
|
||||
Link as NextUiLink,
|
||||
LinkProps,
|
||||
NavbarMenu,
|
||||
NavbarMenuItem,
|
||||
NavbarMenuToggle,
|
||||
} from "@nextui-org/react";
|
||||
import React from "react";
|
||||
|
||||
const Link = (props: LinkProps) => <NextUiLink {...props} onClick={(e) => e.preventDefault()} />;
|
||||
|
||||
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 menuItems = [
|
||||
"Profile",
|
||||
"Dashboard",
|
||||
"Activity",
|
||||
"Analytics",
|
||||
"System",
|
||||
"Deployments",
|
||||
"My Settings",
|
||||
"Team Settings",
|
||||
"Help & Feedback",
|
||||
"Log Out",
|
||||
];
|
||||
|
||||
return (
|
||||
<Navbar disableAnimation isBordered>
|
||||
<NavbarContent className="sm:hidden" justify="start">
|
||||
<NavbarMenuToggle />
|
||||
</NavbarContent>
|
||||
|
||||
<NavbarContent className="sm:hidden pr-3" justify="center">
|
||||
<NavbarBrand>
|
||||
<AcmeLogo />
|
||||
<p className="font-bold text-inherit">ACME</p>
|
||||
</NavbarBrand>
|
||||
</NavbarContent>
|
||||
|
||||
<NavbarContent className="hidden sm:flex gap-4" justify="center">
|
||||
<NavbarBrand>
|
||||
<AcmeLogo />
|
||||
<p className="font-bold text-inherit">ACME</p>
|
||||
</NavbarBrand>
|
||||
<NavbarItem>
|
||||
<Link color="foreground" href="#">
|
||||
Features
|
||||
</Link>
|
||||
</NavbarItem>
|
||||
<NavbarItem isActive>
|
||||
<Link aria-current="page" color="warning" 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="warning" href="#" variant="flat">
|
||||
Sign Up
|
||||
</Button>
|
||||
</NavbarItem>
|
||||
</NavbarContent>
|
||||
|
||||
<NavbarMenu>
|
||||
{menuItems.map((item, index) => (
|
||||
<NavbarMenuItem key={`${item}-${index}`}>
|
||||
<Link
|
||||
className="w-full"
|
||||
color={
|
||||
index === 2 ? "warning" : index === menuItems.length - 1 ? "danger" : "foreground"
|
||||
}
|
||||
href="#"
|
||||
size="lg"
|
||||
>
|
||||
{item}
|
||||
</Link>
|
||||
</NavbarMenuItem>
|
||||
))}
|
||||
</NavbarMenu>
|
||||
</Navbar>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Navbar,
|
||||
NavbarBrand,
|
||||
NavbarContent,
|
||||
NavbarItem,
|
||||
Link as NextUiLink,
|
||||
LinkProps,
|
||||
Button,
|
||||
} from "@nextui-org/react";
|
||||
|
||||
const Link = (props: LinkProps) => <NextUiLink {...props} onClick={(e) => e.preventDefault()} />;
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Navbar,
|
||||
NavbarBrand,
|
||||
NavbarContent,
|
||||
NavbarItem,
|
||||
Link as NextUiLink,
|
||||
LinkProps,
|
||||
Button,
|
||||
} from "@nextui-org/react";
|
||||
|
||||
const Link = (props: LinkProps) => <NextUiLink {...props} onClick={(e) => e.preventDefault()} />;
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Navbar,
|
||||
NavbarBrand,
|
||||
NavbarContent,
|
||||
NavbarItem,
|
||||
Link as NextUiLink,
|
||||
LinkProps,
|
||||
Button,
|
||||
} from "@nextui-org/react";
|
||||
|
||||
const Link = (props: LinkProps) => <NextUiLink {...props} onClick={(e) => e.preventDefault()} />;
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Navbar,
|
||||
NavbarBrand,
|
||||
NavbarContent,
|
||||
NavbarItem,
|
||||
Link as NextUiLink,
|
||||
LinkProps,
|
||||
Button,
|
||||
} from "@nextui-org/react";
|
||||
|
||||
const Link = (props: LinkProps) => <NextUiLink {...props} onClick={(e) => e.preventDefault()} />;
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Navbar,
|
||||
NavbarBrand,
|
||||
NavbarContent,
|
||||
NavbarItem,
|
||||
Link as NextUiLink,
|
||||
LinkProps,
|
||||
DropdownItem,
|
||||
DropdownTrigger,
|
||||
Dropdown,
|
||||
DropdownMenu,
|
||||
Avatar,
|
||||
} from "@nextui-org/react";
|
||||
|
||||
const Link = (props: LinkProps) => <NextUiLink {...props} onClick={(e) => e.preventDefault()} />;
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Navbar,
|
||||
NavbarBrand,
|
||||
NavbarContent,
|
||||
NavbarItem,
|
||||
Link as NextUiLink,
|
||||
LinkProps,
|
||||
Button,
|
||||
DropdownItem,
|
||||
DropdownTrigger,
|
||||
Dropdown,
|
||||
DropdownMenu,
|
||||
} from "@nextui-org/react";
|
||||
import {ChevronDown, Lock, Activity, Flash, Server, TagUser, Scale} from "@nextui-org/shared-icons";
|
||||
|
||||
const Link = (props: LinkProps) => <NextUiLink {...props} onClick={(e) => e.preventDefault()} />;
|
||||
|
||||
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"
|
||||
className="w-[340px]"
|
||||
itemClasses={{
|
||||
base: "gap-4",
|
||||
}}
|
||||
>
|
||||
<DropdownItem
|
||||
key="autoscaling"
|
||||
description="ACME scales apps to meet user demand, automagically, based on load."
|
||||
startContent={icons.scale}
|
||||
>
|
||||
Autoscaling
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
key="usage_metrics"
|
||||
description="Real-time metrics to debug issues. Slow query added? We’ll show you exactly where."
|
||||
startContent={icons.activity}
|
||||
>
|
||||
Usage Metrics
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
key="production_ready"
|
||||
description="ACME runs on ACME, join us and others serving requests at web scale."
|
||||
startContent={icons.flash}
|
||||
>
|
||||
Production Ready
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
key="99_uptime"
|
||||
description="Applications stay on the grid with high availability and high uptime guarantees."
|
||||
startContent={icons.server}
|
||||
>
|
||||
+99% Uptime
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
key="supreme_support"
|
||||
description="Overcome any challenge with a supporting 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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Navbar,
|
||||
NavbarBrand,
|
||||
NavbarContent,
|
||||
NavbarItem,
|
||||
Link as NextUiLink,
|
||||
LinkProps,
|
||||
Button,
|
||||
NavbarMenuToggle,
|
||||
NavbarMenu,
|
||||
NavbarMenuItem,
|
||||
} from "@nextui-org/react";
|
||||
import React from "react";
|
||||
|
||||
const Link = (props: LinkProps) => <NextUiLink {...props} onClick={(e) => e.preventDefault()} />;
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Navbar,
|
||||
NavbarBrand,
|
||||
NavbarContent,
|
||||
NavbarItem,
|
||||
Link as NextUiLink,
|
||||
LinkProps,
|
||||
Input,
|
||||
DropdownItem,
|
||||
DropdownTrigger,
|
||||
Dropdown,
|
||||
DropdownMenu,
|
||||
Avatar,
|
||||
} from "@nextui-org/react";
|
||||
|
||||
const Link = (props: LinkProps) => <NextUiLink {...props} onClick={(e) => e.preventDefault()} />;
|
||||
|
||||
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: "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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,259 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
RadioGroup,
|
||||
Radio,
|
||||
Button,
|
||||
Accordion,
|
||||
Tabs,
|
||||
Textarea,
|
||||
Input,
|
||||
Tab,
|
||||
AccordionItem,
|
||||
extendVariants,
|
||||
} from "@nextui-org/react";
|
||||
import {useState} from "react";
|
||||
|
||||
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-unit-6 min-h-unit-6 px-1",
|
||||
input: "text-tiny",
|
||||
},
|
||||
md: {
|
||||
inputWrapper: "h-unit-10 min-h-unit-10",
|
||||
input: "text-small",
|
||||
},
|
||||
xl: {
|
||||
inputWrapper: "h-unit-14 min-h-unit-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: {},
|
||||
});
|
||||
|
||||
export default function NextUIPerf() {
|
||||
const [textA, setTextA] = useState<string>("");
|
||||
const [textB, setTextB] = useState<string>("");
|
||||
const [textC, setTextC] = useState<string>("");
|
||||
|
||||
return (
|
||||
<div className="w-full p-24 gap-4 flex flex-col">
|
||||
<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="foreground">Press Me!</MyButton2>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Table,
|
||||
TableHeader,
|
||||
TableColumn,
|
||||
TableBody,
|
||||
TableRow,
|
||||
TableCell,
|
||||
getKeyValue,
|
||||
Spinner,
|
||||
Pagination,
|
||||
} from "@nextui-org/react";
|
||||
import {useAsyncList} from "@react-stately/data";
|
||||
import {useCallback, useMemo, useState} from "react";
|
||||
|
||||
type SWCharacter = {
|
||||
name: string;
|
||||
height: string;
|
||||
mass: string;
|
||||
birth_year: string;
|
||||
};
|
||||
|
||||
export default function Page() {
|
||||
const [page, setPage] = useState(1);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
const rowsPerPage = 10;
|
||||
|
||||
let list = useAsyncList<SWCharacter>({
|
||||
async load({signal, cursor}) {
|
||||
// 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();
|
||||
|
||||
setTotal(json.count);
|
||||
|
||||
setIsLoading(false);
|
||||
|
||||
return {
|
||||
items: json.results,
|
||||
cursor: json.next,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
const pages = Math.ceil(total / rowsPerPage);
|
||||
|
||||
const items = useMemo(() => {
|
||||
const start = (page - 1) * rowsPerPage;
|
||||
const end = start + rowsPerPage;
|
||||
|
||||
return list.items.slice(start, end);
|
||||
}, [page, list.items?.length]);
|
||||
|
||||
const onPaginationChange = useCallback(
|
||||
(page: number) => {
|
||||
setIsLoading(true);
|
||||
if (page >= list.items.length / rowsPerPage) {
|
||||
list.loadMore();
|
||||
}
|
||||
setPage(page);
|
||||
},
|
||||
[list.items.length],
|
||||
);
|
||||
|
||||
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={onPaginationChange}
|
||||
/>
|
||||
</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 && !items.length}
|
||||
items={items}
|
||||
loadingContent={<Spinner />}
|
||||
>
|
||||
{(item) => (
|
||||
<TableRow key={item.name}>
|
||||
{(columnKey) => <TableCell>{getKeyValue(item, columnKey)}</TableCell>}
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,579 @@
|
||||
/* 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 "@nextui-org/react";
|
||||
import {ChevronDownIcon, SearchIcon} from "@nextui-org/shared-icons";
|
||||
import {useCallback, useMemo, useState} from "react";
|
||||
import {capitalize} from "lodash";
|
||||
|
||||
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",
|
||||
},
|
||||
{
|
||||
key: 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[0];
|
||||
|
||||
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>View</DropdownItem>
|
||||
<DropdownItem>Edit</DropdownItem>
|
||||
<DropdownItem>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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Table,
|
||||
TableHeader,
|
||||
TableColumn,
|
||||
TableBody,
|
||||
TableRow,
|
||||
TableCell,
|
||||
getKeyValue,
|
||||
Spinner,
|
||||
} from "@nextui-org/react";
|
||||
import {useAsyncList} from "@react-stately/data";
|
||||
import {useInfiniteScroll} from "@nextui-org/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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Table,
|
||||
TableHeader,
|
||||
TableColumn,
|
||||
TableBody,
|
||||
TableRow,
|
||||
TableCell,
|
||||
getKeyValue,
|
||||
Spinner,
|
||||
Button,
|
||||
} from "@nextui-org/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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Table,
|
||||
TableHeader,
|
||||
TableColumn,
|
||||
TableBody,
|
||||
TableRow,
|
||||
TableCell,
|
||||
getKeyValue,
|
||||
Spinner,
|
||||
} from "@nextui-org/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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,553 @@
|
||||
/* 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 "@nextui-org/react";
|
||||
import {ChevronDownIcon, SearchIcon} from "@nextui-org/shared-icons";
|
||||
import {useCallback, useMemo, useState} from "react";
|
||||
import {capitalize} from "lodash";
|
||||
|
||||
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",
|
||||
},
|
||||
{
|
||||
key: 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[0];
|
||||
|
||||
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>View</DropdownItem>
|
||||
<DropdownItem>Edit</DropdownItem>
|
||||
<DropdownItem>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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import Rss from "rss";
|
||||
import {allBlogPosts} from "contentlayer/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,
|
||||
author: `${author} <${siteConfig.email}>`,
|
||||
categories: post.tags ?? [],
|
||||
});
|
||||
});
|
||||
|
||||
return new Response(feed.xml(), {
|
||||
headers: {
|
||||
"Content-Type": "application/xml",
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import {Button, Image, Link} from "@nextui-org/react";
|
||||
|
||||
import {Blockquote} from "@/components/docs/components/blockquote";
|
||||
|
||||
export default function FigmaPage() {
|
||||
return (
|
||||
<>
|
||||
<main className="relative container mx-auto max-w-7xl z-10 px-6 min-h-[calc(100vh_-_64px_-_108px)] mb-12 flex-grow">
|
||||
<section className="w-full flex flex-col items-center lg:px-16 mt-12 gap-6">
|
||||
<div className="text-center max-w-xl">
|
||||
<h1 className="mb-2 font-bold text-4xl">NextUI Figma Kit</h1>
|
||||
<h5 className="text-default-500 text-lg">
|
||||
A Figma file that contains the basis of the NextUI design system to help you design
|
||||
your applications.
|
||||
</h5>
|
||||
</div>
|
||||
|
||||
<iframe
|
||||
className="aspect-video w-full border border-transparent dark:border-default-200/50 object-fit rounded-xl shadow-lg"
|
||||
height="600"
|
||||
src="https://embed.figma.com/file/1267584376234254760/hf_embed?community_viewer=true&embed_host=nextui"
|
||||
title="NextUI Figma Kit"
|
||||
width="800"
|
||||
/>
|
||||
<div className="text-center max-w-2xl m-auto">
|
||||
<Button className="max-w-fit" color="default" variant="bordered">
|
||||
<Link
|
||||
isExternal
|
||||
showAnchorIcon
|
||||
className="text-current"
|
||||
href="https://www.figma.com/community/file/1267584376234254760"
|
||||
>
|
||||
Open in Figma
|
||||
</Link>
|
||||
</Button>
|
||||
<Blockquote color="warning">
|
||||
This file is still in development and will be continuously updated.
|
||||
</Blockquote>
|
||||
</div>
|
||||
</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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import "@/styles/globals.css";
|
||||
import "@/styles/sandpack.css";
|
||||
import {Metadata} from "next";
|
||||
import {clsx} from "@nextui-org/shared-utils";
|
||||
import {Analytics} from "@vercel/analytics/react";
|
||||
|
||||
import {Providers} from "./providers";
|
||||
|
||||
import {Cmdk} from "@/components/cmdk";
|
||||
import manifest from "@/config/routes.json";
|
||||
import {siteConfig} from "@/config/site";
|
||||
import {fontSans} from "@/config/fonts";
|
||||
import {Navbar} from "@/components/navbar";
|
||||
import {Footer} from "@/components/footer";
|
||||
import {__PROD__} from "@/utils";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: {
|
||||
default: siteConfig.name,
|
||||
template: `%s | ${siteConfig.name}`,
|
||||
},
|
||||
description: siteConfig.description,
|
||||
keywords: [
|
||||
"React",
|
||||
"Next.js",
|
||||
"Tailwind CSS",
|
||||
"NextUI",
|
||||
"React Aria",
|
||||
"Server Components",
|
||||
"React Components",
|
||||
"UI Components",
|
||||
"UI Kit",
|
||||
"UI Library",
|
||||
"UI Framework",
|
||||
"UI Design System",
|
||||
],
|
||||
themeColor: [
|
||||
{media: "(prefers-color-scheme: light)", color: "white"},
|
||||
{media: "(prefers-color-scheme: dark)", color: "black"},
|
||||
],
|
||||
icons: {
|
||||
icon: "/favicon.ico",
|
||||
shortcut: "/favicon-32x32.png",
|
||||
apple: "/apple-touch-icon.png",
|
||||
},
|
||||
manifest: "/manifest.json",
|
||||
twitter: siteConfig.twitter,
|
||||
openGraph: siteConfig.openGraph,
|
||||
authors: [
|
||||
{
|
||||
name: "jrgarciadev",
|
||||
url: "https://jrgarciadev.com",
|
||||
},
|
||||
],
|
||||
creator: "jrgarciadev",
|
||||
viewport:
|
||||
"viewport-fit=cover, width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0",
|
||||
};
|
||||
|
||||
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", fontSans.variable)}>
|
||||
<Providers themeProps={{attribute: "class", defaultTheme: "dark"}}>
|
||||
<div className="relative flex flex-col" id="app-container">
|
||||
<Navbar mobileRoutes={manifest.mobileRoutes} routes={manifest.routes} />
|
||||
{children}
|
||||
<Footer />
|
||||
</div>
|
||||
<Cmdk />
|
||||
</Providers>
|
||||
{__PROD__ && <Analytics />}
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import {Spacer} from "@nextui-org/spacer";
|
||||
|
||||
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 {getAllSponsors} from "@/utils/get-all-sponsors";
|
||||
|
||||
async function getData() {
|
||||
try {
|
||||
const sponsors = await getAllSponsors();
|
||||
|
||||
return {
|
||||
sponsors,
|
||||
};
|
||||
} catch (error) {
|
||||
throw new Error("Failed to fetch data");
|
||||
}
|
||||
}
|
||||
|
||||
export default async function Home() {
|
||||
const data = await getData();
|
||||
|
||||
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} />
|
||||
<CustomThemes />
|
||||
<A11yOtb />
|
||||
<DarkMode />
|
||||
<Customization />
|
||||
<LastButNotLeast />
|
||||
<Support sponsors={data.sponsors} />
|
||||
<Spacer y={24} />
|
||||
<InstallBanner />
|
||||
<Community />
|
||||
<Spacer y={24} />
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import {NextUIProvider} from "@nextui-org/react";
|
||||
import {ThemeProvider as NextThemesProvider} from "next-themes";
|
||||
import {ThemeProviderProps} from "next-themes/dist/types";
|
||||
|
||||
export interface ProvidersProps {
|
||||
children: React.ReactNode;
|
||||
themeProps?: ThemeProviderProps;
|
||||
}
|
||||
|
||||
export function Providers({children, themeProps}: ProvidersProps) {
|
||||
return (
|
||||
<NextUIProvider>
|
||||
<NextThemesProvider {...themeProps}>{children}</NextThemesProvider>
|
||||
</NextUIProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
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__} 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__) 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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,64 @@
|
||||
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,
|
||||
);
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./carbon-ad";
|
||||
@@ -1,7 +1,5 @@
|
||||
import {ReactNode, FC} from "react";
|
||||
import {Image} from "@nextui-org/react";
|
||||
import {clsx} from "@nextui-org/shared-utils";
|
||||
|
||||
export interface BgGridContainerProps {
|
||||
showGradient?: boolean;
|
||||
children?: ReactNode;
|
||||
@@ -9,41 +7,41 @@ export interface BgGridContainerProps {
|
||||
}
|
||||
|
||||
export const BgGridContainer: FC<BgGridContainerProps> = ({
|
||||
showGradient = true,
|
||||
// showGradient = true,
|
||||
children,
|
||||
className,
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
"relative overflow-y-hidden flex items-center border border-neutral-400/50 dark:border-neutral-100/50 px-2 py-4 rounded-lg",
|
||||
"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",
|
||||
// "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
|
||||
{/* <div
|
||||
className={clsx(
|
||||
"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))]",
|
||||
"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="absolute h-full w-full z-[-1] opacity-40 -top-8 -right-28">
|
||||
<div className="hidden md:block absolute h-full w-full z-[-1] opacity-40 -top-8 -right-28">
|
||||
<Image
|
||||
removeWrapper
|
||||
alt="custom themes background"
|
||||
@@ -51,7 +49,7 @@ export const BgGridContainer: FC<BgGridContainerProps> = ({
|
||||
src="/gradients/blue-purple-1.svg"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
)} */}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
"use client";
|
||||
|
||||
import {BlogPost} from "contentlayer/generated";
|
||||
import {Card, CardFooter, CardBody, CardHeader, Link, Avatar, Image} from "@nextui-org/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 {useIsMounted} from "@/hooks/use-is-mounted";
|
||||
|
||||
const BlogPostCard = (post: BlogPost) => {
|
||||
const isMounted = useIsMounted();
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{isMounted && (
|
||||
<motion.div
|
||||
animate={{opacity: 1, y: 0}}
|
||||
exit={{opacity: 0, y: 5}}
|
||||
initial={{opacity: 0, y: 5}}
|
||||
transition={{duration: 0.3}}
|
||||
>
|
||||
<Card
|
||||
isBlurred
|
||||
as={NextLink}
|
||||
className="p-2 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}
|
||||
>
|
||||
<CardHeader>
|
||||
<Link
|
||||
as={NextLink}
|
||||
className="font-semibold "
|
||||
href={post.url}
|
||||
size="lg"
|
||||
underline="hover"
|
||||
>
|
||||
<Balancer>{post.title}</Balancer>
|
||||
</Link>
|
||||
</CardHeader>
|
||||
<CardBody className="pt-0 px-2 pb-1">
|
||||
<Image className="mb-3" src={post.image} />
|
||||
<p className="font-normal px-1 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")}
|
||||
</time>
|
||||
<Avatar size="sm" src={post.author?.avatar} />
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
};
|
||||
|
||||
export const BlogPostList = ({posts}: {posts: BlogPost[]}) => {
|
||||
return (
|
||||
<div className="mt-10 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{posts.map((post, idx) => (
|
||||
<BlogPostCard key={idx} {...post} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./nextjs-templates";
|
||||
export * from "./video-in-view";
|
||||
@@ -0,0 +1,34 @@
|
||||
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 NextUI (v2) and Tailwind CSS.",
|
||||
icon: <NewNextJSIcon height={40} width={40} />,
|
||||
href: "https://github.com/nextui-org/next-app-template",
|
||||
},
|
||||
{
|
||||
title: "Next.js 13 (Pages) Template",
|
||||
isExternal: true,
|
||||
description:
|
||||
"A Next.js 13 with pages directory template pre-configured with NextUI (v2) and Tailwind CSS.",
|
||||
icon: <NewNextJSIcon height={40} width={40} />,
|
||||
href: "https://github.com/nextui-org/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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,151 @@
|
||||
/* 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 "@nextui-org/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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,430 @@
|
||||
/* 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 "@nextui-org/react";
|
||||
import {CloseIcon} from "@nextui-org/shared-icons";
|
||||
import {tv} from "tailwind-variants";
|
||||
import {usePathname, useRouter} from "next/navigation";
|
||||
import MultiRef from "react-multi-ref";
|
||||
import {clsx} from "@nextui-org/shared-utils";
|
||||
import scrollIntoView from "scroll-into-view-if-needed";
|
||||
import {isAppleDevice, isWebKit} from "@react-aria/utils";
|
||||
import {create} from "zustand";
|
||||
import {intersectionBy, isEmpty} from "lodash";
|
||||
import {writeStorage, useLocalStorage} from "@rehooks/local-storage";
|
||||
|
||||
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 [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 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,
|
||||
}).slice(0, MAX_RESULTS);
|
||||
}
|
||||
|
||||
const matchesForEachWord = words.map((word) =>
|
||||
matchSorter(data, word, {
|
||||
keys: MATCH_KEYS,
|
||||
}),
|
||||
);
|
||||
|
||||
const matches = intersectionBy(...matchesForEachWord, "objectID").slice(0, MAX_RESULTS);
|
||||
|
||||
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();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("keydown", onKeyDown);
|
||||
}, [isOpen]);
|
||||
|
||||
const onItemSelect = useCallback(
|
||||
(item: SearchResultItem) => {
|
||||
onClose();
|
||||
router.push(item.url);
|
||||
addToRecentSearches(item);
|
||||
},
|
||||
[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">
|
||||
{query.length > 0 && (
|
||||
<Command.Empty>
|
||||
<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,3 +1,4 @@
|
||||
"use client";
|
||||
// Inspired by https://github.dev/modulz/stitches-site code demo
|
||||
import React from "react";
|
||||
import rangeParser from "parse-numeric-range";
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
"use client";
|
||||
|
||||
import {FC, useState} from "react";
|
||||
import {
|
||||
Modal,
|
||||
Button,
|
||||
ModalContent,
|
||||
ModalHeader,
|
||||
Link as NextUILink,
|
||||
ModalBody,
|
||||
ModalFooter,
|
||||
Skeleton,
|
||||
} from "@nextui-org/react";
|
||||
import Link from "next/link";
|
||||
@@ -41,7 +45,7 @@ export const DemoCodeModal: FC<DemoCodeModalProps> = ({isOpen, code, title, subt
|
||||
setIsCodeVisible(isOpen);
|
||||
},
|
||||
}}
|
||||
radius={isMobile ? "none" : "2xl"}
|
||||
radius={isMobile ? "none" : "lg"}
|
||||
size={isMobile ? "full" : "2xl"}
|
||||
onClose={onClose}
|
||||
>
|
||||
@@ -61,18 +65,25 @@ export const DemoCodeModal: FC<DemoCodeModalProps> = ({isOpen, code, title, subt
|
||||
)}
|
||||
</p>
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
<Skeleton className="h-[320px] md:h-[60vh] rounded-xl" isLoaded={isCodeVisible}>
|
||||
<ModalBody className="flex-initial md:pb-6">
|
||||
{isCodeVisible ? (
|
||||
<CodeWindow
|
||||
showCopy
|
||||
showWindowIcons
|
||||
className="min-h-[320px] max-h-[80vh] md:max-h-[60vh] h-screen"
|
||||
className="min-h-[320px] !h-[60vh] max-h-full"
|
||||
language="jsx"
|
||||
title={fileName}
|
||||
value={code}
|
||||
/>
|
||||
</Skeleton>
|
||||
) : (
|
||||
<Skeleton className="h-[60vh] rounded-xl" />
|
||||
)}
|
||||
</ModalBody>
|
||||
<ModalFooter className="md:hidden">
|
||||
<Button fullWidth onPress={onClose}>
|
||||
Close
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import {useRef} from "react";
|
||||
import {Button} from "@nextui-org/react";
|
||||
import confetti from "canvas-confetti";
|
||||
|
||||
export const CustomButton = () => {
|
||||
const buttonRef = useRef<HTMLButtonElement | null>(null);
|
||||
|
||||
const handleConfetti = () => {
|
||||
const handleConfetti = async () => {
|
||||
const {clientWidth, clientHeight} = document.documentElement;
|
||||
const boundingBox = buttonRef.current?.getBoundingClientRect?.();
|
||||
|
||||
@@ -14,6 +15,7 @@ export const CustomButton = () => {
|
||||
const targetWidth = boundingBox?.width ?? 0;
|
||||
|
||||
const targetCenterX = targetX + targetWidth / 2;
|
||||
const confetti = (await import("canvas-confetti")).default;
|
||||
|
||||
confetti({
|
||||
zIndex: 999,
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import {Card, CardBody, Button, Image, Progress, CardProps} from "@nextui-org/react";
|
||||
import {useState, FC} from "react";
|
||||
import {clsx} from "@nextui-org/shared-utils";
|
||||
import NextImage from "next/image";
|
||||
|
||||
import {
|
||||
PauseCircleBoldIcon,
|
||||
@@ -19,9 +22,8 @@ export const MusicPlayer: FC<MusicPlayerProps> = ({className, ...otherProps}) =>
|
||||
return (
|
||||
<Card
|
||||
isBlurred
|
||||
className={clsx("border-none dark:bg-black/40 bg-background/60", className)}
|
||||
radius="2xl"
|
||||
shadow="2xl"
|
||||
className={clsx("border-none bg-background/60 dark:bg-default-100/50", className)}
|
||||
shadow="sm"
|
||||
{...otherProps}
|
||||
>
|
||||
<CardBody>
|
||||
@@ -29,27 +31,26 @@ export const MusicPlayer: FC<MusicPlayerProps> = ({className, ...otherProps}) =>
|
||||
<div className="relative col-span-6 md:col-span-4">
|
||||
<Image
|
||||
alt="Album cover"
|
||||
as={NextImage}
|
||||
className="object-cover"
|
||||
classNames={{
|
||||
base: "shadow-black/20",
|
||||
}}
|
||||
height={200}
|
||||
shadow="lg"
|
||||
height={300}
|
||||
shadow="md"
|
||||
src="/images/album-cover.png"
|
||||
width="100%"
|
||||
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">
|
||||
<h3 className="font-semibold text-foreground/90">Daily Mix</h3>
|
||||
<p className="font-semibold text-foreground/90">Daily Mix</p>
|
||||
<p className="text-sm text-foreground/80">12 Tracks</p>
|
||||
<h1 className="text-lg font-medium mt-2">Frontend Radio</h1>
|
||||
<p className="text-lg font-medium mt-2">Frontend Radio</p>
|
||||
</div>
|
||||
<Button
|
||||
isIconOnly
|
||||
className="text-neutral-900/60 data-[hover]:bg-foreground/10 -translate-y-2 translate-x-2"
|
||||
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)}
|
||||
@@ -65,10 +66,10 @@ export const MusicPlayer: FC<MusicPlayerProps> = ({className, ...otherProps}) =>
|
||||
<Progress
|
||||
aria-label="Music progress"
|
||||
classNames={{
|
||||
filler: "bg-neutral-800 dark:bg-white",
|
||||
track: "bg-neutral-500/30",
|
||||
indicator: "bg-default-800 dark:bg-white",
|
||||
track: "bg-default-500/30",
|
||||
}}
|
||||
color="neutral"
|
||||
color="default"
|
||||
size="sm"
|
||||
value={33}
|
||||
/>
|
||||
@@ -81,6 +82,7 @@ export const MusicPlayer: FC<MusicPlayerProps> = ({className, ...otherProps}) =>
|
||||
<div className="flex w-full items-center justify-center">
|
||||
<Button
|
||||
isIconOnly
|
||||
aria-label="Repeat"
|
||||
className="data-[hover]:bg-foreground/10"
|
||||
radius="full"
|
||||
variant="light"
|
||||
@@ -89,6 +91,7 @@ export const MusicPlayer: FC<MusicPlayerProps> = ({className, ...otherProps}) =>
|
||||
</Button>
|
||||
<Button
|
||||
isIconOnly
|
||||
aria-label="Previous"
|
||||
className="data-[hover]:bg-foreground/10"
|
||||
radius="full"
|
||||
variant="light"
|
||||
@@ -97,6 +100,7 @@ export const MusicPlayer: FC<MusicPlayerProps> = ({className, ...otherProps}) =>
|
||||
</Button>
|
||||
<Button
|
||||
isIconOnly
|
||||
aria-label="Play"
|
||||
className="w-auto h-auto data-[hover]:bg-foreground/10"
|
||||
radius="full"
|
||||
variant="light"
|
||||
@@ -105,6 +109,7 @@ export const MusicPlayer: FC<MusicPlayerProps> = ({className, ...otherProps}) =>
|
||||
</Button>
|
||||
<Button
|
||||
isIconOnly
|
||||
aria-label="Next"
|
||||
className="data-[hover]:bg-foreground/10"
|
||||
radius="full"
|
||||
variant="light"
|
||||
@@ -113,6 +118,7 @@ export const MusicPlayer: FC<MusicPlayerProps> = ({className, ...otherProps}) =>
|
||||
</Button>
|
||||
<Button
|
||||
isIconOnly
|
||||
aria-label="Shuffle"
|
||||
className="data-[hover]:bg-foreground/10"
|
||||
radius="full"
|
||||
variant="light"
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
import {
|
||||
tv,
|
||||
VariantProps,
|
||||
CircularProgress,
|
||||
CircularProgressProps,
|
||||
circularProgress,
|
||||
} from "@nextui-org/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" />
|
||||
// );
|
||||
// }
|
||||
@@ -0,0 +1,25 @@
|
||||
/* 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,6 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import {useState, FC} from "react";
|
||||
import {Card, CardHeader, Button, Avatar, CardBody, CardFooter} from "@nextui-org/react";
|
||||
import {clsx} from "@nextui-org/shared-utils";
|
||||
import NextImage from "next/image";
|
||||
|
||||
interface UserTwitterCardProps {
|
||||
className?: string;
|
||||
@@ -13,14 +16,25 @@ export const UserTwitterCard: FC<UserTwitterCardProps> = ({className}) => {
|
||||
<Card className={clsx("max-w-[300px]", className)}>
|
||||
<CardHeader className="justify-between">
|
||||
<div className="flex gap-5">
|
||||
<Avatar isBordered radius="full" size="md" src="/avatars/avatar-1.png" />
|
||||
<Avatar
|
||||
isBordered
|
||||
ImgComponent={NextImage}
|
||||
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-neutral-600">Zoey Lang</h4>
|
||||
<h5 className="text-sm tracking-tight text-neutral-400">@zoeylang</h5>
|
||||
<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-neutral-200" : ""}
|
||||
className={isFollowed ? "bg-transparent text-foreground border-default-200" : ""}
|
||||
color="primary"
|
||||
radius="full"
|
||||
size="sm"
|
||||
@@ -31,7 +45,7 @@ export const UserTwitterCard: FC<UserTwitterCardProps> = ({className}) => {
|
||||
</Button>
|
||||
</CardHeader>
|
||||
<CardBody className="px-3 py-0">
|
||||
<p className="text-sm pl-px text-neutral-400">
|
||||
<p className="text-sm pl-px text-default-400">
|
||||
Full-stack developer, @getnextui lover she/her
|
||||
<span aria-label="confetti" role="img">
|
||||
🎉
|
||||
@@ -40,12 +54,12 @@ export const UserTwitterCard: FC<UserTwitterCardProps> = ({className}) => {
|
||||
</CardBody>
|
||||
<CardFooter className="gap-3">
|
||||
<div className="flex gap-1">
|
||||
<p className="font-semibold text-neutral-400 text-sm">4</p>
|
||||
<p className=" text-neutral-400 text-sm">Following</p>
|
||||
<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-neutral-400 text-sm">97.1K</p>
|
||||
<p className="text-neutral-400 text-sm">Followers</p>
|
||||
<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,12 +1,13 @@
|
||||
"use client";
|
||||
|
||||
import {FC} from "react";
|
||||
import {tv, VariantProps} from "tailwind-variants";
|
||||
|
||||
const blockquote = tv({
|
||||
base: "border pl-4 bg-neutral-50 my-6 py-3 rounded-xl [&>p]:m-0",
|
||||
|
||||
const blockquoteStyles = tv({
|
||||
base: "border px-4 bg-default-50 my-6 py-3 rounded-xl [&>p]:m-0",
|
||||
variants: {
|
||||
color: {
|
||||
neutral: "border-neutral-100 bg-neutral-50/20",
|
||||
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",
|
||||
@@ -15,11 +16,11 @@ const blockquote = tv({
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
color: "neutral",
|
||||
color: "default",
|
||||
},
|
||||
});
|
||||
|
||||
type BlockquoteVariantProps = VariantProps<typeof blockquote>;
|
||||
type BlockquoteVariantProps = VariantProps<typeof blockquoteStyles>;
|
||||
|
||||
export interface BlockquoteProps extends BlockquoteVariantProps {
|
||||
children?: React.ReactNode;
|
||||
@@ -27,7 +28,7 @@ export interface BlockquoteProps extends BlockquoteVariantProps {
|
||||
}
|
||||
|
||||
export const Blockquote: FC<BlockquoteProps> = ({children, color, className, ...props}) => {
|
||||
const styles = blockquote({color, className});
|
||||
const styles = blockquoteStyles({color, className});
|
||||
|
||||
return (
|
||||
<blockquote className={styles} {...props}>
|
||||
|
||||
@@ -1,87 +1,191 @@
|
||||
import React from "react";
|
||||
"use client";
|
||||
|
||||
import React, {useCallback, useMemo, useRef} from "react";
|
||||
import dynamic from "next/dynamic";
|
||||
import {Spinner} from "@nextui-org/react";
|
||||
import {Skeleton, Tab, Tabs} from "@nextui-org/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: () => (
|
||||
<div>
|
||||
<Spinner />
|
||||
</div>
|
||||
),
|
||||
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: () => (
|
||||
<div>
|
||||
<Spinner />
|
||||
</div>
|
||||
),
|
||||
loading: () => <Skeleton className="w-full h-32 rounded-xl" />,
|
||||
});
|
||||
|
||||
interface CodeDemoProps extends UseCodeDemoProps {
|
||||
component?: string;
|
||||
interface CodeDemoProps extends UseCodeDemoProps, WindowResizerProps {
|
||||
title?: string;
|
||||
asIframe?: boolean;
|
||||
showSandpackPreview?: boolean;
|
||||
initialEditorOpen?: boolean;
|
||||
enableResize?: boolean;
|
||||
showTabs?: boolean;
|
||||
showPreview?: boolean;
|
||||
showWindowActions?: boolean;
|
||||
iframeSrc?: string;
|
||||
asIframe?: boolean;
|
||||
iframeInitialWidth?: number;
|
||||
showOpenInCodeSandbox?: boolean;
|
||||
isPreviewCentered?: boolean;
|
||||
resizeEnabled?: boolean;
|
||||
displayMode?: "always" | "visible";
|
||||
isGradientBox?: boolean;
|
||||
gradientColor?: GradientBoxProps["color"];
|
||||
defaultExpanded?: boolean;
|
||||
previewHeight?: string | number;
|
||||
overflow?: "auto" | "visible" | "hidden";
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const CodeDemo: React.FC<CodeDemoProps> = ({
|
||||
files = {},
|
||||
component,
|
||||
title,
|
||||
showEditor = true,
|
||||
showPreview = true,
|
||||
asIframe = false,
|
||||
resizeEnabled = true,
|
||||
showSandpackPreview = false,
|
||||
showWindowActions = false,
|
||||
enableResize = false,
|
||||
isPreviewCentered = false,
|
||||
showOpenInCodeSandbox,
|
||||
isGradientBox = false,
|
||||
defaultExpanded = false,
|
||||
previewHeight = "auto",
|
||||
overflow = "visible",
|
||||
displayMode = "always",
|
||||
showTabs = true,
|
||||
gradientColor,
|
||||
highlightedLines,
|
||||
iframeInitialWidth,
|
||||
iframeSrc,
|
||||
className,
|
||||
}) => {
|
||||
const ref = useRef(null);
|
||||
const isInView = useInView(ref, {
|
||||
once: true,
|
||||
margin: "600px",
|
||||
});
|
||||
|
||||
const {noInline, code} = useCodeDemo({
|
||||
files,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
{showPreview && (
|
||||
<DynamicReactLiveDemo
|
||||
code={code}
|
||||
enableResize={enableResize || asIframe}
|
||||
height={previewHeight}
|
||||
iframeInitialWidth={iframeInitialWidth}
|
||||
iframeSrc={iframeSrc}
|
||||
iframeTitle={component}
|
||||
noInline={noInline}
|
||||
overflow={overflow}
|
||||
showWindowActions={showWindowActions || asIframe}
|
||||
/>
|
||||
)}
|
||||
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
|
||||
iframeHeight={previewHeight}
|
||||
iframeInitialWidth={iframeInitialWidth}
|
||||
iframeSrc={iframeSrc}
|
||||
iframeTitle={title}
|
||||
resizeEnabled={resizeEnabled}
|
||||
/>
|
||||
) : (
|
||||
<DynamicReactLiveDemo
|
||||
className={className}
|
||||
code={code}
|
||||
gradientColor={gradientColor}
|
||||
height={previewHeight}
|
||||
isCentered={isPreviewCentered}
|
||||
isGradientBox={isGradientBox}
|
||||
noInline={noInline}
|
||||
overflow={overflow}
|
||||
/>
|
||||
);
|
||||
|
||||
return renderContent(content);
|
||||
}, [
|
||||
displayMode,
|
||||
isGradientBox,
|
||||
gradientColor,
|
||||
previewHeight,
|
||||
asIframe,
|
||||
showPreview,
|
||||
isInView,
|
||||
className,
|
||||
]);
|
||||
|
||||
const editorContent = useMemo(() => {
|
||||
if (!showEditor) return null;
|
||||
|
||||
const content = (
|
||||
<DynamicSandpack
|
||||
defaultExpanded={defaultExpanded}
|
||||
files={files}
|
||||
highlightedLines={highlightedLines}
|
||||
showEditor={showEditor}
|
||||
showOpenInCodeSandbox={showPreview}
|
||||
showOpenInCodeSandbox={showOpenInCodeSandbox || showPreview}
|
||||
showPreview={showSandpackPreview}
|
||||
/>
|
||||
);
|
||||
|
||||
return renderContent(content);
|
||||
}, [
|
||||
displayMode,
|
||||
showEditor,
|
||||
isInView,
|
||||
files,
|
||||
highlightedLines,
|
||||
defaultExpanded,
|
||||
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,18 +1,19 @@
|
||||
import React from "react";
|
||||
import {LivePreview, LiveProvider, LiveError} from "react-live";
|
||||
import {clsx} from "@nextui-org/shared-utils";
|
||||
import * as Components from "@nextui-org/react";
|
||||
|
||||
import {BgGridContainer} from "@/components/bg-grid-container";
|
||||
import {GradientBox, GradientBoxProps} from "@/components/gradient-box";
|
||||
|
||||
export interface ReactLiveDemoProps {
|
||||
code: string;
|
||||
height?: string | number;
|
||||
noInline?: boolean;
|
||||
showWindowActions?: boolean;
|
||||
iframeSrc?: string;
|
||||
iframeTitle?: string;
|
||||
iframeInitialWidth?: number;
|
||||
enableResize?: boolean;
|
||||
height?: string | number;
|
||||
isCentered?: boolean;
|
||||
isGradientBox?: boolean;
|
||||
className?: string;
|
||||
gradientColor?: GradientBoxProps["color"];
|
||||
overflow?: "auto" | "visible" | "hidden";
|
||||
}
|
||||
|
||||
@@ -20,13 +21,46 @@ export const scope = {
|
||||
...Components,
|
||||
} as Record<string, unknown>;
|
||||
|
||||
export const ReactLiveDemo: React.FC<ReactLiveDemoProps> = ({code, noInline}) => {
|
||||
export const ReactLiveDemo: React.FC<ReactLiveDemoProps> = ({
|
||||
code,
|
||||
isGradientBox,
|
||||
gradientColor = "orange",
|
||||
isCentered = false,
|
||||
height,
|
||||
className,
|
||||
noInline,
|
||||
}) => {
|
||||
const content = (
|
||||
<>
|
||||
<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}>
|
||||
<BgGridContainer>
|
||||
<LivePreview className="live-preview flex w-full not-prose" />
|
||||
<LiveError />
|
||||
</BgGridContainer>
|
||||
{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="max-w-full py-4 px-2 w-full h-full scrollbar-hide overflow-x-scroll">
|
||||
{content}
|
||||
</div>
|
||||
</GradientBox>
|
||||
) : (
|
||||
<BgGridContainer className={className}>{content}</BgGridContainer>
|
||||
)}
|
||||
</LiveProvider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
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;
|
||||
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,
|
||||
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,
|
||||
}}
|
||||
>
|
||||
<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,5 +1,5 @@
|
||||
import React from "react";
|
||||
import {clsx} from "@nextui-org/shared-utils";
|
||||
import React, {forwardRef} from "react";
|
||||
import {clsx, getUniqueID} from "@nextui-org/shared-utils";
|
||||
import BaseHighlight, {Language, PrismTheme, defaultProps} from "prism-react-renderer";
|
||||
|
||||
import defaultTheme from "@/libs/prism-theme";
|
||||
@@ -10,6 +10,8 @@ interface CodeblockProps {
|
||||
metastring?: string;
|
||||
theme?: PrismTheme;
|
||||
showLines?: boolean;
|
||||
removeIndent?: boolean;
|
||||
hideScrollBar?: boolean;
|
||||
className?: string;
|
||||
children?: (props: any) => React.ReactNode;
|
||||
}
|
||||
@@ -39,61 +41,84 @@ const calculateLinesToHighlight = (meta?: string) => {
|
||||
};
|
||||
};
|
||||
|
||||
export function Codeblock({
|
||||
codeString,
|
||||
language,
|
||||
showLines,
|
||||
theme: themeProp,
|
||||
metastring,
|
||||
className: classNameProp,
|
||||
...props
|
||||
}: CodeblockProps) {
|
||||
const theme = themeProp || defaultTheme;
|
||||
const shouldHighlightLine = calculateLinesToHighlight(metastring);
|
||||
const isMultiLine = codeString.split("\n").length > 2;
|
||||
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;
|
||||
|
||||
return (
|
||||
<BaseHighlight {...defaultProps} code={codeString} language={language} theme={theme} {...props}>
|
||||
{({className, style, tokens, getLineProps, getTokenProps}) => (
|
||||
<div className="w-full" data-language={language}>
|
||||
<pre
|
||||
className={clsx(className, classNameProp, "flex max-w-full", {
|
||||
"flex-col": isMultiLine,
|
||||
})}
|
||||
style={style}
|
||||
>
|
||||
{tokens.map((line, i) => {
|
||||
const lineProps = getLineProps({line, key: i});
|
||||
return (
|
||||
<BaseHighlight
|
||||
{...defaultProps}
|
||||
code={codeString}
|
||||
language={language}
|
||||
theme={theme}
|
||||
{...props}
|
||||
>
|
||||
{({className, style, tokens, getLineProps, getTokenProps}) => (
|
||||
<div className="w-full" data-language={language}>
|
||||
<pre
|
||||
ref={ref}
|
||||
className={clsx(className, classNameProp, "flex max-w-full", {
|
||||
"flex-col": isMultiLine,
|
||||
"scrollbar-hide overflow-x-scroll": hideScrollBar,
|
||||
})}
|
||||
style={style}
|
||||
>
|
||||
{tokens.map((line, i) => {
|
||||
const lineProps = getLineProps({line, key: i});
|
||||
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
{...lineProps}
|
||||
className={clsx(
|
||||
lineProps.className,
|
||||
"px-4 relative [&>span]:relative [&>span]:z-10",
|
||||
{
|
||||
"px-2": showLines,
|
||||
},
|
||||
{
|
||||
"before:content-[''] before:w-full before:h-full before:absolute before:z-0 before:left-0 before:bg-gradient-to-r before:from-white/10 before:to-code-background before:border-l-2 border-l-white/80 dark:before:border-l-white/50": shouldHighlightLine(
|
||||
i,
|
||||
),
|
||||
},
|
||||
)}
|
||||
>
|
||||
{showLines && (
|
||||
<span className="select-none text-xs mr-6 opacity-30">{i + 1}</span>
|
||||
)}
|
||||
{line.map((token, key) => (
|
||||
<span key={key} {...getTokenProps({token, key})} className={className} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</BaseHighlight>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div
|
||||
key={`${i}-${getUniqueID("line-wrapper")}`}
|
||||
{...lineProps}
|
||||
className={clsx(
|
||||
lineProps.className,
|
||||
removeIndent ? "pr-4" : "px-4",
|
||||
"relative [&>span]:relative [&>span]:z-10",
|
||||
{
|
||||
"px-2": showLines,
|
||||
},
|
||||
{
|
||||
"before:content-[''] before:w-full before:h-full before:absolute before:z-0 before:left-0 before:bg-gradient-to-r before:from-white/10 before:to-code-background":
|
||||
shouldHighlightLine(i),
|
||||
},
|
||||
)}
|
||||
>
|
||||
{showLines && (
|
||||
<span className="select-none text-xs mr-6 opacity-30">{i + 1}</span>
|
||||
)}
|
||||
{line.map((token, key) => (
|
||||
<span
|
||||
key={`${key}-${getUniqueID("line")}`}
|
||||
{...getTokenProps({token, key})}
|
||||
className={className}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</BaseHighlight>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Codeblock.displayName = "CodeBlock";
|
||||
|
||||
export default Codeblock;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {FeaturesGrid} from "@/components/marketing";
|
||||
import {FeaturesGrid} from "@/components/marketing/features-grid";
|
||||
import {communityAccounts} from "@/libs/constants";
|
||||
|
||||
export const Community = () => {
|
||||
|
||||
@@ -1,83 +1,113 @@
|
||||
import {Button, Link} from "@nextui-org/react";
|
||||
import {Button, ButtonProps, Code, Link, Tooltip} from "@nextui-org/react";
|
||||
import {ReactNode} from "react";
|
||||
import Balancer from "react-wrap-balancer";
|
||||
|
||||
import {GithubIcon, NpmIcon, AdobeIcon, StorybookIcon} from "@/components/icons";
|
||||
import {GithubIcon, NpmIcon, AdobeIcon, StorybookIcon, NextJsIcon} from "@/components/icons";
|
||||
import {COMPONENT_PATH, COMPONENT_THEME_PATH} from "@/libs/github/constants";
|
||||
|
||||
export interface ComponentLinksProps {
|
||||
component: string;
|
||||
styles?: string;
|
||||
storybook?: string;
|
||||
rscCompatible?: boolean;
|
||||
reactAriaHook?: string;
|
||||
}
|
||||
|
||||
export const ComponentLinks = ({component, storybook, reactAriaHook}: ComponentLinksProps) => {
|
||||
const ButtonLink = ({
|
||||
children,
|
||||
href,
|
||||
startContent,
|
||||
tooltip,
|
||||
...props
|
||||
}: ButtonProps & {
|
||||
href: string;
|
||||
tooltip?: string | ReactNode;
|
||||
}) => {
|
||||
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}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Button>
|
||||
);
|
||||
|
||||
return tooltip ? (
|
||||
<Tooltip className="max-w-[230px]" content={tooltip}>
|
||||
{button}
|
||||
</Tooltip>
|
||||
) : (
|
||||
button
|
||||
);
|
||||
};
|
||||
|
||||
export const ComponentLinks = ({
|
||||
component,
|
||||
storybook,
|
||||
styles,
|
||||
rscCompatible,
|
||||
reactAriaHook,
|
||||
}: ComponentLinksProps) => {
|
||||
if (!component) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap gap-3 mt-6">
|
||||
<Button
|
||||
isExternal
|
||||
as={Link}
|
||||
className="text-neutral-700"
|
||||
href={`https://storiesv2.nextui.org/?path=/story/components-${storybook || component}`}
|
||||
radius="md"
|
||||
size="sm"
|
||||
startIcon={<StorybookIcon className="text-lg text-[#ff4785]" />}
|
||||
variant="flat"
|
||||
<ButtonLink
|
||||
href={`https://storybook.nextui.org/?path=/story/components-${storybook || component}`}
|
||||
startContent={<StorybookIcon className="text-lg text-[#ff4785]" />}
|
||||
>
|
||||
Storybook
|
||||
</Button>
|
||||
<Button
|
||||
isExternal
|
||||
as={Link}
|
||||
className="text-neutral-700"
|
||||
</ButtonLink>
|
||||
<ButtonLink
|
||||
href={`https://www.npmjs.com/package/@nextui-org/${component}`}
|
||||
radius="md"
|
||||
size="sm"
|
||||
startIcon={<NpmIcon className="text-2xl text-[#E53E3E]" />}
|
||||
variant="flat"
|
||||
startContent={<NpmIcon className="text-2xl text-[#E53E3E]" />}
|
||||
>
|
||||
{`@nextui-org/${component}`}
|
||||
</Button>
|
||||
</ButtonLink>
|
||||
{reactAriaHook && (
|
||||
<Button
|
||||
isExternal
|
||||
as={Link}
|
||||
className="text-neutral-700"
|
||||
<ButtonLink
|
||||
href={`https://react-spectrum.adobe.com/react-aria/${reactAriaHook}.html`}
|
||||
radius="md"
|
||||
size="sm"
|
||||
startIcon={<AdobeIcon className="text-lg text-[#E1251B]" />}
|
||||
variant="flat"
|
||||
startContent={<AdobeIcon className="text-lg text-[#E1251B]" />}
|
||||
>
|
||||
React Aria
|
||||
</Button>
|
||||
</ButtonLink>
|
||||
)}
|
||||
<Button
|
||||
isExternal
|
||||
as={Link}
|
||||
className="text-neutral-700"
|
||||
href={`${COMPONENT_PATH}/${component}`}
|
||||
radius="md"
|
||||
size="sm"
|
||||
startIcon={<GithubIcon />}
|
||||
variant="flat"
|
||||
>
|
||||
{rscCompatible && (
|
||||
<ButtonLink
|
||||
href="https://nextjs.org/docs/getting-started/react-essentials#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}/${component}`} startContent={<GithubIcon size={20} />}>
|
||||
Source
|
||||
</Button>
|
||||
<Button
|
||||
isExternal
|
||||
as={Link}
|
||||
className="text-neutral-700"
|
||||
href={`${COMPONENT_THEME_PATH}/${component}.ts`}
|
||||
radius="md"
|
||||
size="sm"
|
||||
startIcon={<GithubIcon />}
|
||||
variant="flat"
|
||||
</ButtonLink>
|
||||
<ButtonLink
|
||||
href={`${COMPONENT_THEME_PATH}/${styles || component}.ts`}
|
||||
startContent={<GithubIcon size={20} />}
|
||||
>
|
||||
Styles source
|
||||
</Button>
|
||||
</ButtonLink>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {NewNextJSIcon, ViteIcon, RemixIcon, AstroIcon} from "@/components/icons";
|
||||
import {FeaturesGrid} from "@/components/marketing";
|
||||
import {FeaturesGrid} from "@/components/marketing/features-grid";
|
||||
|
||||
const frameworks = [
|
||||
{
|
||||
@@ -33,7 +33,7 @@ export const Frameworks = () => {
|
||||
<FeaturesGrid
|
||||
classNames={{
|
||||
base: "mt-8 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-2 gap-4",
|
||||
iconWrapper: "bg-neutral-300/20",
|
||||
iconWrapper: "bg-default-300/20",
|
||||
body: "py-0",
|
||||
}}
|
||||
features={frameworks}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import {Tabs, Tab, Snippet} from "@nextui-org/react";
|
||||
|
||||
import Codeblock from "./codeblock";
|
||||
|
||||
type PackageManager = {
|
||||
key: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
const importTabs: PackageManager[] = [
|
||||
{
|
||||
key: "main",
|
||||
name: "Main",
|
||||
},
|
||||
{
|
||||
key: "individual",
|
||||
name: "Individual",
|
||||
},
|
||||
];
|
||||
|
||||
export interface ImportTabsProps {
|
||||
commands: Record<string, string>;
|
||||
}
|
||||
|
||||
export const ImportTabs = ({commands}: ImportTabsProps) => {
|
||||
return (
|
||||
<Tabs
|
||||
disableAnimation
|
||||
aria-label="NextUI import commands"
|
||||
classNames={{
|
||||
base: "group mt-4",
|
||||
tabList: "relative h-10",
|
||||
}}
|
||||
variant="underlined"
|
||||
>
|
||||
{importTabs.map(({key, name}) => {
|
||||
if (!commands[key]) return null;
|
||||
|
||||
return (
|
||||
<Tab key={key} title={name}>
|
||||
<Snippet
|
||||
disableTooltip
|
||||
fullWidth
|
||||
hideSymbol
|
||||
classNames={{
|
||||
base: "bg-code-background text-code-foreground",
|
||||
pre: "font-light text-base",
|
||||
copyButton: "text-lg text-default-400",
|
||||
}}
|
||||
>
|
||||
<Codeblock
|
||||
hideScrollBar
|
||||
removeIndent
|
||||
codeString={commands[key] as string}
|
||||
language="jsx"
|
||||
/>
|
||||
</Snippet>
|
||||
</Tab>
|
||||
);
|
||||
})}
|
||||
</Tabs>
|
||||
);
|
||||
};
|
||||
@@ -4,3 +4,6 @@ export * from "./blockquote";
|
||||
export * from "./community";
|
||||
export * from "./frameworks";
|
||||
export * from "./code-demo";
|
||||
export * from "./import-tabs";
|
||||
export * from "./swatch-colors-set";
|
||||
export {default as Codeblock} from "./codeblock";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {Tabs, TabItem, Snippet} from "@nextui-org/react";
|
||||
import {Tabs, Tab, Snippet} from "@nextui-org/react";
|
||||
|
||||
import {Codeblock} from "./codeblock";
|
||||
import Codeblock from "./codeblock";
|
||||
|
||||
import {YarnIcon, NpmSmallIcon, PnpmIcon} from "@/components/icons";
|
||||
|
||||
@@ -44,7 +44,7 @@ export const PackageManagers = ({commands}: PackageManagersProps) => {
|
||||
if (!commands[name]) return null;
|
||||
|
||||
return (
|
||||
<TabItem
|
||||
<Tab
|
||||
key={name}
|
||||
title={
|
||||
<div className="flex items-center space-x-2">
|
||||
@@ -60,12 +60,12 @@ export const PackageManagers = ({commands}: PackageManagersProps) => {
|
||||
classNames={{
|
||||
base: "bg-code-background text-code-foreground",
|
||||
pre: "font-light text-base",
|
||||
copyButton: "text-lg text-code-foreground/50",
|
||||
copyButton: "text-lg text-zinc-500 mr-2",
|
||||
}}
|
||||
>
|
||||
<Codeblock codeString={commands[name] as string} language="bash" />
|
||||
<Codeblock removeIndent codeString={commands[name] as string} language="bash" />
|
||||
</Snippet>
|
||||
</TabItem>
|
||||
</Tab>
|
||||
);
|
||||
})}
|
||||
</Tabs>
|
||||
|
||||
@@ -0,0 +1,641 @@
|
||||
import {parseToRgba, readableColor} from "color2k";
|
||||
import {Button, Tooltip} from "@nextui-org/react";
|
||||
import {commonColors, semanticColors} from "@nextui-org/theme";
|
||||
import {useClipboard} from "@nextui-org/use-clipboard";
|
||||
import {useState} from "react";
|
||||
import {useTheme} from "next-themes";
|
||||
import {get, isEmpty} from "lodash";
|
||||
|
||||
type ColorsItem = {
|
||||
color: string;
|
||||
scale?: string;
|
||||
className?: string;
|
||||
textClassName?: string;
|
||||
};
|
||||
|
||||
type SwatchColors = {
|
||||
title: string;
|
||||
items: ColorsItem[];
|
||||
};
|
||||
|
||||
type SwatchSetProps = {
|
||||
colors: SwatchColors[];
|
||||
isSematic?: boolean;
|
||||
};
|
||||
|
||||
const scaleIndexMap: Record<number, string> = {
|
||||
0: "50",
|
||||
1: "100",
|
||||
2: "200",
|
||||
3: "300",
|
||||
4: "400",
|
||||
5: "500",
|
||||
6: "600",
|
||||
7: "700",
|
||||
8: "800",
|
||||
9: "900",
|
||||
};
|
||||
|
||||
const Swatch = ({color, scale}: {color: string; scale?: string}) => {
|
||||
const [copied, setCopied] = useState(false);
|
||||
const {copy} = useClipboard();
|
||||
|
||||
const colorText = color
|
||||
? `#${parseToRgba(color)
|
||||
.slice(0, 3)
|
||||
.map((x) => x.toString(16).padStart(2, "0"))
|
||||
.join("")
|
||||
.toUpperCase()}`
|
||||
: "N/A";
|
||||
|
||||
const handleCopy = () => {
|
||||
copy(colorText);
|
||||
|
||||
setCopied(true);
|
||||
|
||||
setTimeout(() => {
|
||||
setCopied(false);
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
return (
|
||||
<Tooltip content="Copied" isOpen={copied}>
|
||||
<Button
|
||||
className="flex flex-col gap-1 items-center justify-center w-24 h-24 m-2 rounded-medium shadow-small"
|
||||
style={{
|
||||
backgroundColor: color,
|
||||
}}
|
||||
onPress={handleCopy}
|
||||
>
|
||||
<span
|
||||
className="text-small font-semibold"
|
||||
style={{
|
||||
color: readableColor(color),
|
||||
}}
|
||||
>
|
||||
{colorText}
|
||||
</span>
|
||||
{scale && (
|
||||
<span
|
||||
className="text-tiny font-semibold"
|
||||
style={{
|
||||
color: readableColor(color),
|
||||
}}
|
||||
>
|
||||
{scale}
|
||||
</span>
|
||||
)}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
const SematicSwatch = ({
|
||||
color,
|
||||
className,
|
||||
textClassName,
|
||||
}: {
|
||||
color: string;
|
||||
className?: string;
|
||||
textClassName?: string;
|
||||
}) => {
|
||||
const [copied, setCopied] = useState(false);
|
||||
const {copy} = useClipboard();
|
||||
|
||||
const {theme} = useTheme();
|
||||
let value: string = "";
|
||||
const [colorName, colorScale] = color.split("-");
|
||||
|
||||
let currentPalette = get(semanticColors, theme ?? "", {});
|
||||
|
||||
if (!colorScale) {
|
||||
value = get(currentPalette, `${colorName}.DEFAULT`, "");
|
||||
} else {
|
||||
value = get(currentPalette, `${colorName}.${colorScale}`, "");
|
||||
}
|
||||
|
||||
const handleCopy = () => {
|
||||
copy(value);
|
||||
|
||||
setCopied(true);
|
||||
|
||||
setTimeout(() => {
|
||||
setCopied(false);
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
return (
|
||||
<Tooltip content="Copied" isOpen={copied}>
|
||||
<Button
|
||||
className={`${className} gap-1 flex flex-col items-center justify-center w-24 h-24 m-2 rounded-medium shadow-small`}
|
||||
onPress={handleCopy}
|
||||
>
|
||||
<span className={`${textClassName} text-tiny font-semibold`}>{color}</span>
|
||||
{!isEmpty(value) && (
|
||||
<span className={`${textClassName} text-tiny font-semibold`}>{value}</span>
|
||||
)}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
const SwatchSet = ({colors, isSematic = false}: SwatchSetProps) => (
|
||||
<div className="flex flex-row flex-wrap items-center justify-center">
|
||||
{colors.map(({title, items}) => (
|
||||
<div key={title} className="flex flex-col items-start w-full h-full">
|
||||
<h2 className="text-xl font-bold text-foreground">{title}</h2>
|
||||
<div className="flex flex-row flex-wrap items-center justify-start w-full h-full px-4 py-1">
|
||||
{items.map((c, index) =>
|
||||
isSematic ? (
|
||||
<SematicSwatch
|
||||
key={`${c.color}-${index}`}
|
||||
className={c.className}
|
||||
color={c.color}
|
||||
textClassName={c.textClassName}
|
||||
/>
|
||||
) : (
|
||||
<Swatch key={`${c.color}-${index}`} color={c.color} scale={c.scale} />
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
const getCommonItems = (colors: string[]) => {
|
||||
return colors.map((color, index, array) => ({
|
||||
color,
|
||||
scale: array.length > 2 ? scaleIndexMap[index] : undefined,
|
||||
}));
|
||||
};
|
||||
|
||||
export const CommonColors = () => {
|
||||
return (
|
||||
<SwatchSet
|
||||
colors={[
|
||||
{
|
||||
title: "White & Black",
|
||||
items: getCommonItems([commonColors.white, commonColors.black]),
|
||||
},
|
||||
{
|
||||
title: "Blue",
|
||||
items: getCommonItems([...Object.values(commonColors.blue)]),
|
||||
},
|
||||
{
|
||||
title: "Purple",
|
||||
items: getCommonItems([...Object.values(commonColors.purple)]),
|
||||
},
|
||||
{
|
||||
title: "Green",
|
||||
items: getCommonItems([...Object.values(commonColors.green)]),
|
||||
},
|
||||
{
|
||||
title: "Red",
|
||||
items: getCommonItems([...Object.values(commonColors.red)]),
|
||||
},
|
||||
{
|
||||
title: "Pink",
|
||||
items: getCommonItems([...Object.values(commonColors.pink)]),
|
||||
},
|
||||
{
|
||||
title: "Yellow",
|
||||
items: getCommonItems([...Object.values(commonColors.yellow)]),
|
||||
},
|
||||
{
|
||||
title: "Cyan",
|
||||
items: getCommonItems([...Object.values(commonColors.cyan)]),
|
||||
},
|
||||
{
|
||||
title: "Zinc",
|
||||
items: getCommonItems([...Object.values(commonColors.zinc)]),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const SemanticColors = () => {
|
||||
return (
|
||||
<SwatchSet
|
||||
isSematic
|
||||
colors={[
|
||||
{
|
||||
title: "Layout",
|
||||
items: [
|
||||
{
|
||||
color: "background",
|
||||
className: "bg-background",
|
||||
textClassName: "text-foreground",
|
||||
},
|
||||
{
|
||||
color: "foreground",
|
||||
className: "bg-foreground",
|
||||
textClassName: "text-background",
|
||||
},
|
||||
{
|
||||
color: "divider",
|
||||
className: "bg-divider",
|
||||
textClassName: "text-foreground",
|
||||
},
|
||||
{
|
||||
color: "focus",
|
||||
className: "bg-focus",
|
||||
textClassName: "text-primary-foreground",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Content",
|
||||
items: [
|
||||
{
|
||||
color: "content1",
|
||||
className: "bg-content1",
|
||||
textClassName: "text-content1-foreground",
|
||||
},
|
||||
{
|
||||
color: "content2",
|
||||
className: "bg-content2",
|
||||
textClassName: "text-content2-foreground",
|
||||
},
|
||||
{
|
||||
color: "content3",
|
||||
className: "bg-content3",
|
||||
textClassName: "text-content3-foreground",
|
||||
},
|
||||
{
|
||||
color: "content4",
|
||||
className: "bg-content4",
|
||||
textClassName: "text-content4-foreground",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Base",
|
||||
items: [
|
||||
{
|
||||
color: "default",
|
||||
className: "bg-default",
|
||||
textClassName: "text-default-foreground",
|
||||
},
|
||||
{
|
||||
color: "primary",
|
||||
className: "bg-primary",
|
||||
textClassName: "text-primary-foreground",
|
||||
},
|
||||
{
|
||||
color: "secondary",
|
||||
className: "bg-secondary",
|
||||
textClassName: "text-secondary-foreground",
|
||||
},
|
||||
{
|
||||
color: "success",
|
||||
className: "bg-success",
|
||||
textClassName: "text-success-foreground",
|
||||
},
|
||||
{
|
||||
color: "warning",
|
||||
className: "bg-warning",
|
||||
textClassName: "text-warning-foreground",
|
||||
},
|
||||
{
|
||||
color: "danger",
|
||||
className: "bg-danger",
|
||||
textClassName: "text-danger-foreground",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Default",
|
||||
items: [
|
||||
{
|
||||
color: "default-50",
|
||||
className: "bg-default-50",
|
||||
textClassName: "text-default-900",
|
||||
},
|
||||
{
|
||||
color: "default-100",
|
||||
className: "bg-default-100",
|
||||
textClassName: "text-default-900",
|
||||
},
|
||||
{
|
||||
color: "default-200",
|
||||
className: "bg-default-200",
|
||||
textClassName: "text-default-800",
|
||||
},
|
||||
{
|
||||
color: "default-300",
|
||||
className: "bg-default-300",
|
||||
textClassName: "text-default-800",
|
||||
},
|
||||
{
|
||||
color: "default-400",
|
||||
className: "bg-default-400",
|
||||
textClassName: "text-default-800",
|
||||
},
|
||||
{
|
||||
color: "default-500",
|
||||
className: "bg-default-500",
|
||||
textClassName: "text-default-foreground",
|
||||
},
|
||||
{
|
||||
color: "default-600",
|
||||
className: "bg-default-600",
|
||||
textClassName: "text-default-50",
|
||||
},
|
||||
{
|
||||
color: "default-700",
|
||||
className: "bg-default-700",
|
||||
textClassName: "text-default-100",
|
||||
},
|
||||
{
|
||||
color: "default-800",
|
||||
className: "bg-default-800",
|
||||
textClassName: "text-default-100",
|
||||
},
|
||||
{
|
||||
color: "default-900",
|
||||
className: "bg-default-900",
|
||||
textClassName: "text-default-100",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Primary",
|
||||
items: [
|
||||
{
|
||||
color: "primary-50",
|
||||
className: "bg-primary-50",
|
||||
textClassName: "text-primary-900",
|
||||
},
|
||||
{
|
||||
color: "primary-100",
|
||||
className: "bg-primary-100",
|
||||
textClassName: "text-primary-900",
|
||||
},
|
||||
{
|
||||
color: "primary-200",
|
||||
className: "bg-primary-200",
|
||||
textClassName: "text-primary-800",
|
||||
},
|
||||
{
|
||||
color: "primary-300",
|
||||
className: "bg-primary-300",
|
||||
textClassName: "text-primary-800",
|
||||
},
|
||||
{
|
||||
color: "primary-400",
|
||||
className: "bg-primary-400",
|
||||
textClassName: "text-primary-800",
|
||||
},
|
||||
{
|
||||
color: "primary-500",
|
||||
className: "bg-primary-500",
|
||||
textClassName: "text-primary-foreground",
|
||||
},
|
||||
{
|
||||
color: "primary-600",
|
||||
className: "bg-primary-600",
|
||||
textClassName: "text-primary-50",
|
||||
},
|
||||
{
|
||||
color: "primary-700",
|
||||
className: "bg-primary-700",
|
||||
textClassName: "text-primary-100",
|
||||
},
|
||||
{
|
||||
color: "primary-800",
|
||||
className: "bg-primary-800",
|
||||
textClassName: "text-primary-100",
|
||||
},
|
||||
{
|
||||
color: "primary-900",
|
||||
className: "bg-primary-900",
|
||||
textClassName: "text-primary-100",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Secondary",
|
||||
items: [
|
||||
{
|
||||
color: "secondary-50",
|
||||
className: "bg-secondary-50",
|
||||
textClassName: "text-secondary-900",
|
||||
},
|
||||
{
|
||||
color: "secondary-100",
|
||||
className: "bg-secondary-100",
|
||||
textClassName: "text-secondary-900",
|
||||
},
|
||||
{
|
||||
color: "secondary-200",
|
||||
className: "bg-secondary-200",
|
||||
textClassName: "text-secondary-800",
|
||||
},
|
||||
{
|
||||
color: "secondary-300",
|
||||
className: "bg-secondary-300",
|
||||
textClassName: "text-secondary-800",
|
||||
},
|
||||
{
|
||||
color: "secondary-400",
|
||||
className: "bg-secondary-400",
|
||||
textClassName: "text-secondary-800",
|
||||
},
|
||||
{
|
||||
color: "secondary-500",
|
||||
className: "bg-secondary-500",
|
||||
textClassName: "text-secondary-foreground",
|
||||
},
|
||||
{
|
||||
color: "secondary-600",
|
||||
className: "bg-secondary-600",
|
||||
textClassName: "text-secondary-50",
|
||||
},
|
||||
{
|
||||
color: "secondary-700",
|
||||
className: "bg-secondary-700",
|
||||
textClassName: "text-secondary-100",
|
||||
},
|
||||
{
|
||||
color: "secondary-800",
|
||||
className: "bg-secondary-800",
|
||||
textClassName: "text-secondary-100",
|
||||
},
|
||||
{
|
||||
color: "secondary-900",
|
||||
className: "bg-secondary-900",
|
||||
textClassName: "text-secondary-100",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Success",
|
||||
items: [
|
||||
{
|
||||
color: "success-50",
|
||||
className: "bg-success-50",
|
||||
textClassName: "text-success-900",
|
||||
},
|
||||
{
|
||||
color: "success-100",
|
||||
className: "bg-success-100",
|
||||
textClassName: "text-success-900",
|
||||
},
|
||||
{
|
||||
color: "success-200",
|
||||
className: "bg-success-200",
|
||||
textClassName: "text-success-800",
|
||||
},
|
||||
{
|
||||
color: "success-300",
|
||||
className: "bg-success-300",
|
||||
textClassName: "text-success-800",
|
||||
},
|
||||
{
|
||||
color: "success-400",
|
||||
className: "bg-success-400",
|
||||
textClassName: "text-success-800",
|
||||
},
|
||||
{
|
||||
color: "success-500",
|
||||
className: "bg-success-500",
|
||||
textClassName: "text-success-foreground",
|
||||
},
|
||||
{
|
||||
color: "success-600",
|
||||
className: "bg-success-600",
|
||||
textClassName: "text-success-50",
|
||||
},
|
||||
{
|
||||
color: "success-700",
|
||||
className: "bg-success-700",
|
||||
textClassName: "text-success-100",
|
||||
},
|
||||
{
|
||||
color: "success-800",
|
||||
className: "bg-success-800",
|
||||
textClassName: "text-success-100",
|
||||
},
|
||||
{
|
||||
color: "success-900",
|
||||
className: "bg-success-900",
|
||||
textClassName: "text-success-100",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Warning",
|
||||
items: [
|
||||
{
|
||||
color: "warning-50",
|
||||
className: "bg-warning-50",
|
||||
textClassName: "text-warning-900",
|
||||
},
|
||||
{
|
||||
color: "warning-100",
|
||||
className: "bg-warning-100",
|
||||
textClassName: "text-warning-900",
|
||||
},
|
||||
{
|
||||
color: "warning-200",
|
||||
className: "bg-warning-200",
|
||||
textClassName: "text-warning-800",
|
||||
},
|
||||
{
|
||||
color: "warning-300",
|
||||
className: "bg-warning-300",
|
||||
textClassName: "text-warning-800",
|
||||
},
|
||||
{
|
||||
color: "warning-400",
|
||||
className: "bg-warning-400",
|
||||
textClassName: "text-warning-800",
|
||||
},
|
||||
{
|
||||
color: "warning-500",
|
||||
className: "bg-warning-500",
|
||||
textClassName: "text-warning-foreground",
|
||||
},
|
||||
{
|
||||
color: "warning-600",
|
||||
className: "bg-warning-600",
|
||||
textClassName: "text-warning-50",
|
||||
},
|
||||
{
|
||||
color: "warning-700",
|
||||
className: "bg-warning-700",
|
||||
textClassName: "text-warning-100",
|
||||
},
|
||||
{
|
||||
color: "warning-800",
|
||||
className: "bg-warning-800",
|
||||
textClassName: "text-warning-100",
|
||||
},
|
||||
{
|
||||
color: "warning-900",
|
||||
className: "bg-warning-900",
|
||||
textClassName: "text-warning-100",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Danger",
|
||||
items: [
|
||||
{
|
||||
color: "danger-50",
|
||||
className: "bg-danger-50",
|
||||
textClassName: "text-danger-900",
|
||||
},
|
||||
{
|
||||
color: "danger-100",
|
||||
className: "bg-danger-100",
|
||||
textClassName: "text-danger-900",
|
||||
},
|
||||
{
|
||||
color: "danger-200",
|
||||
className: "bg-danger-200",
|
||||
textClassName: "text-danger-800",
|
||||
},
|
||||
{
|
||||
color: "danger-300",
|
||||
className: "bg-danger-300",
|
||||
textClassName: "text-danger-800",
|
||||
},
|
||||
{
|
||||
color: "danger-400",
|
||||
className: "bg-danger-400",
|
||||
textClassName: "text-danger-800",
|
||||
},
|
||||
{
|
||||
color: "danger-500",
|
||||
className: "bg-danger-500",
|
||||
textClassName: "text-danger-foreground",
|
||||
},
|
||||
{
|
||||
color: "danger-600",
|
||||
className: "bg-danger-600",
|
||||
textClassName: "text-danger-50",
|
||||
},
|
||||
{
|
||||
color: "danger-700",
|
||||
className: "bg-danger-700",
|
||||
textClassName: "text-danger-100",
|
||||
},
|
||||
{
|
||||
color: "danger-800",
|
||||
className: "bg-danger-800",
|
||||
textClassName: "text-danger-100",
|
||||
},
|
||||
{
|
||||
color: "danger-900",
|
||||
className: "bg-danger-900",
|
||||
textClassName: "text-danger-100",
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
export * from "./sidebar";
|
||||
export * from "./toc";
|
||||
export * from "./footer-nav";
|
||||
export * from "./pager";
|
||||
export * from "./components/code-demo";
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import NextLink from "next/link";
|
||||
import {Link} from "@nextui-org/react";
|
||||
import {ChevronIcon} from "@nextui-org/shared-icons";
|
||||
|
||||
import manifest from "@/config/routes.json";
|
||||
import {removeFromLast} from "@/utils";
|
||||
import {Route, addTagToSlug} from "@/libs/docs/page";
|
||||
import {Route} from "@/libs/docs/page";
|
||||
import {useDocsRoute} from "@/hooks/use-docs-route";
|
||||
|
||||
export interface FooterNavProps {
|
||||
tag?: string;
|
||||
prevRoute?: Route;
|
||||
nextRoute?: Route;
|
||||
currentRoute?: Route;
|
||||
}
|
||||
|
||||
export const FooterNav: React.FC<FooterNavProps> = ({tag, prevRoute, nextRoute}) => {
|
||||
export const DocsPager: React.FC<FooterNavProps> = ({currentRoute}) => {
|
||||
const {prevRoute, nextRoute} = useDocsRoute(manifest.routes, currentRoute);
|
||||
|
||||
return (
|
||||
<div className="flex w-full justify-between py-20">
|
||||
{prevRoute ? (
|
||||
@@ -21,7 +25,7 @@ export const FooterNav: React.FC<FooterNavProps> = ({tag, prevRoute, nextRoute})
|
||||
as={NextLink}
|
||||
className="flex gap-2"
|
||||
color="foreground"
|
||||
href={addTagToSlug(removeFromLast(prevRoute.path || "", "."), tag)}
|
||||
href={removeFromLast(prevRoute.path || "", ".")}
|
||||
>
|
||||
<ChevronIcon className="text-primary" height={20} width={20} />
|
||||
{prevRoute.title}
|
||||
@@ -35,7 +39,7 @@ export const FooterNav: React.FC<FooterNavProps> = ({tag, prevRoute, nextRoute})
|
||||
as={NextLink}
|
||||
className="flex gap-1 items-center"
|
||||
color="foreground"
|
||||
href={addTagToSlug(removeFromLast(nextRoute.path || "", "."), tag)}
|
||||
href={removeFromLast(nextRoute.path || "", ".")}
|
||||
>
|
||||
{nextRoute.title}
|
||||
<ChevronIcon className="rotate-180 text-primary" height={20} width={20} />
|
||||
@@ -1,3 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import {FC} from "react";
|
||||
import {ChevronIcon} from "@nextui-org/shared-icons";
|
||||
import {CollectionBase, Expandable, MultipleSelection, Node, ItemProps} from "@react-types/shared";
|
||||
@@ -8,15 +10,22 @@ import {TreeState, useTreeState} from "@react-stately/tree";
|
||||
import {useSelectableCollection} from "@react-aria/selection";
|
||||
import {usePress} from "@react-aria/interactions";
|
||||
import {clsx, dataAttr} from "@nextui-org/shared-utils";
|
||||
import {SpacerProps, Spacer, Link as NextUILink} from "@nextui-org/react";
|
||||
import {
|
||||
SpacerProps,
|
||||
Spacer,
|
||||
Link as NextUILink,
|
||||
Chip,
|
||||
dataFocusVisibleClasses,
|
||||
} from "@nextui-org/react";
|
||||
import Link from "next/link";
|
||||
import {isEmpty} from "lodash";
|
||||
import {useRouter} from "next/router";
|
||||
import {usePathname, useRouter} from "next/navigation";
|
||||
|
||||
import {getRoutePaths} from "./utils";
|
||||
|
||||
import {Route} from "@/libs/docs/page";
|
||||
import {TreeKeyboardDelegate} from "@/utils/tree-keyboard-delegate";
|
||||
import {useScrollPosition} from "@/hooks/use-scroll-position";
|
||||
|
||||
export interface Props<T> extends Omit<ItemProps<T>, "title">, Route {
|
||||
slug?: string;
|
||||
@@ -49,6 +58,7 @@ function TreeItem<T>(props: TreeItemProps<T>) {
|
||||
const {key, rendered, childNodes} = item;
|
||||
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
|
||||
const paths = item.props.path
|
||||
? getRoutePaths(item.props.path, item.props?.tag)
|
||||
@@ -57,8 +67,14 @@ function TreeItem<T>(props: TreeItemProps<T>) {
|
||||
pathname: "",
|
||||
};
|
||||
|
||||
const isNew = item.props?.newPost;
|
||||
|
||||
const isExpanded = state.expandedKeys.has(key);
|
||||
const isSelected = state.selectionManager.isSelected(key) || paths.pathname === item.props.slug;
|
||||
const isSelected =
|
||||
state.selectionManager.isSelected(key) ||
|
||||
paths.pathname === item.props.slug ||
|
||||
paths.pathname === pathname ||
|
||||
paths.pagePath === pathname;
|
||||
|
||||
const ref = useRef<any>(null);
|
||||
|
||||
@@ -85,14 +101,9 @@ function TreeItem<T>(props: TreeItemProps<T>) {
|
||||
aria-expanded={dataAttr(hasChildNodes ? isExpanded : undefined)}
|
||||
aria-selected={dataAttr(isSelected)}
|
||||
className={clsx(
|
||||
"flex flex-col gap-3 outline-none w-full",
|
||||
"flex flex-col gap-3 outline-none w-full tap-highlight-transparent",
|
||||
// focus ring
|
||||
"data-[focus-visible=true]:outline-none",
|
||||
"data-[focus-visible=true]:ring-2",
|
||||
"data-[focus-visible=true]:ring-primary",
|
||||
"data-[focus-visible=true]:ring-offset-2",
|
||||
"data-[focus-visible=true]:ring-offset-background",
|
||||
"data-[focus-visible=true]:dark:ring-offset-background-dark",
|
||||
...dataFocusVisibleClasses,
|
||||
)}
|
||||
data-focus-visible={isFocusVisible}
|
||||
data-focused={isFocused}
|
||||
@@ -114,16 +125,11 @@ function TreeItem<T>(props: TreeItemProps<T>) {
|
||||
as={Link}
|
||||
className={clsx(
|
||||
"w-full",
|
||||
"opacity-60",
|
||||
"dark:font-light",
|
||||
{
|
||||
"opacity-100": isSelected,
|
||||
"pointer-events-none": item.props?.comingSoon,
|
||||
},
|
||||
"font-normal",
|
||||
"before:mr-4",
|
||||
"before:content-['']",
|
||||
"before:block",
|
||||
"before:bg-neutral-300",
|
||||
"before:bg-default-300",
|
||||
"before:w-1",
|
||||
"before:h-1",
|
||||
"before:rounded-full",
|
||||
@@ -131,7 +137,28 @@ function TreeItem<T>(props: TreeItemProps<T>) {
|
||||
color="foreground"
|
||||
href={paths.pathname}
|
||||
>
|
||||
{rendered}
|
||||
<span
|
||||
className={clsx(
|
||||
isSelected
|
||||
? "text-primary font-medium dark:text-foreground"
|
||||
: "opacity-80 dark:opacity-60",
|
||||
{
|
||||
"pointer-events-none": item.props?.comingSoon,
|
||||
},
|
||||
)}
|
||||
>
|
||||
{rendered}
|
||||
</span>
|
||||
{isNew && (
|
||||
<Chip className="ml-2" color="primary" size="sm" variant="flat">
|
||||
New
|
||||
</Chip>
|
||||
)}
|
||||
{item.props?.comingSoon && (
|
||||
<Chip className="ml-2" color="default" size="sm" variant="flat">
|
||||
Coming soon
|
||||
</Chip>
|
||||
)}
|
||||
</NextUILink>
|
||||
)}
|
||||
</div>
|
||||
@@ -162,14 +189,16 @@ function TreeHeading({item}: {item: any}) {
|
||||
function Tree<T extends object>(props: CollectionBase<T> & Expandable & MultipleSelection) {
|
||||
let state = useTreeState(props);
|
||||
|
||||
let ref = useRef<HTMLUListElement>(null);
|
||||
|
||||
const scrollPosition = useScrollPosition(ref);
|
||||
|
||||
let keyboardDelegate = useMemo(
|
||||
// @ts-expect-error
|
||||
() => new TreeKeyboardDelegate(state.collection, state.disabledKeys),
|
||||
[state.collection, state.disabledKeys],
|
||||
);
|
||||
|
||||
let ref = useRef(null);
|
||||
|
||||
let {collectionProps} = useSelectableCollection({
|
||||
ref,
|
||||
selectionManager: state.selectionManager,
|
||||
@@ -180,8 +209,13 @@ function Tree<T extends object>(props: CollectionBase<T> & Expandable & Multiple
|
||||
<ul
|
||||
{...collectionProps}
|
||||
ref={ref}
|
||||
className="flex flex-col gap-4 scrollbar-hide overflow-y-scroll max-h-[calc(100vh_-_64px)] pb-28"
|
||||
className="flex flex-col gap-4 scrollbar-hide lg:overflow-y-scroll lg:max-h-[calc(100vh_-_64px)] pb-28"
|
||||
role="tree"
|
||||
style={{
|
||||
WebkitMaskImage: `linear-gradient(to top, transparent 0%, #000 100px, #000 ${
|
||||
scrollPosition > 30 ? "90%" : "100%"
|
||||
}, transparent 100%)`,
|
||||
}}
|
||||
>
|
||||
{[...state.collection].map((item) => {
|
||||
if (item.type === "section") {
|
||||
@@ -198,19 +232,20 @@ export interface DocsSidebarProps {
|
||||
routes?: Route[];
|
||||
tag?: string;
|
||||
slug?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const DocsSidebar: FC<DocsSidebarProps> = ({routes, slug, tag}) => {
|
||||
export const DocsSidebar: FC<DocsSidebarProps> = ({routes, slug, tag, className}) => {
|
||||
const expandedKeys = routes?.reduce((keys, route) => {
|
||||
if (route.defaultOpen) {
|
||||
keys.push(route.key);
|
||||
keys.push(route.key as string);
|
||||
}
|
||||
|
||||
return keys;
|
||||
}, [] as string[]);
|
||||
|
||||
return (
|
||||
<div className="sticky top-20 mt-2 z-0 h-[calc(100vh-121px)]">
|
||||
<div className={clsx("lg:fixed lg:top-20 mt-2 z-0 lg:h-[calc(100vh-121px)]", className)}>
|
||||
<Tree defaultExpandedKeys={expandedKeys} items={routes || []}>
|
||||
{(route) => (
|
||||
<Item
|
||||
|
||||
@@ -1,23 +1,31 @@
|
||||
import {FC} from "react";
|
||||
"use client";
|
||||
|
||||
import {FC, useRef, useEffect} from "react";
|
||||
import {clsx} from "@nextui-org/shared-utils";
|
||||
import {Divider, Spacer} from "@nextui-org/react";
|
||||
import {ChevronCircleTopLinearIcon} from "@nextui-org/shared-icons";
|
||||
import scrollIntoView from "scroll-into-view-if-needed";
|
||||
|
||||
import {Heading} from "@/utils/docs-utils";
|
||||
import {Heading} from "@/libs/docs/utils";
|
||||
import {useScrollSpy} from "@/hooks/use-scroll-spy";
|
||||
import {useScrollPosition} from "@/hooks/use-scroll-position";
|
||||
|
||||
export interface DocsTocProps {
|
||||
headings: Heading[];
|
||||
}
|
||||
|
||||
const paddingLeftByLevel: Record<string, string> = {
|
||||
const paddingLeftByLevel: Record<number, string> = {
|
||||
1: "pl-0",
|
||||
2: "pl-3",
|
||||
2: "pl-0",
|
||||
3: "pl-3",
|
||||
4: "pl-3",
|
||||
};
|
||||
|
||||
export const DocsToc: FC<DocsTocProps> = ({headings}) => {
|
||||
const tocRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const scrollPosition = useScrollPosition(tocRef);
|
||||
|
||||
const activeId = useScrollSpy(
|
||||
headings.map(({id}) => `[id="${id}"]`),
|
||||
{
|
||||
@@ -25,39 +33,65 @@ export const DocsToc: FC<DocsTocProps> = ({headings}) => {
|
||||
},
|
||||
);
|
||||
|
||||
if (headings.length <= 0) return null;
|
||||
|
||||
const activeIndex = headings.findIndex(({id}) => id == activeId);
|
||||
const firstId = headings[0].id;
|
||||
|
||||
useEffect(() => {
|
||||
if (!activeId || activeIndex < 2) return;
|
||||
const anchor = tocRef.current?.querySelector(`li > a[href="#${activeId}"]`);
|
||||
|
||||
if (anchor) {
|
||||
scrollIntoView(anchor, {
|
||||
behavior: "smooth",
|
||||
block: "center",
|
||||
inline: "center",
|
||||
scrollMode: "always",
|
||||
boundary: tocRef.current,
|
||||
});
|
||||
}
|
||||
}, [activeId, activeIndex]);
|
||||
|
||||
return (
|
||||
<div className="sticky w-full flex flex-col gap-4 text-left top-20 h-[calc(100vh-121px)] overflow-y-scroll scrollbar-hide">
|
||||
<div
|
||||
ref={tocRef}
|
||||
className="fixed w-full max-w-[12rem] flex flex-col gap-4 text-left top-20 pb-20 h-[calc(100vh-121px)] scrollbar-hide overflow-y-scroll"
|
||||
style={{
|
||||
WebkitMaskImage: `linear-gradient(to top, transparent 0%, #000 100px, #000 ${
|
||||
scrollPosition > 30 ? "90%" : "100%"
|
||||
}, transparent 100%)`,
|
||||
}}
|
||||
>
|
||||
<p className="text-sm">On this page</p>
|
||||
<ul className="scrollbar-hide flex flex-col gap-2">
|
||||
{headings.map((heading, i) => (
|
||||
<li
|
||||
key={i}
|
||||
className={clsx(
|
||||
"flex items-center text-sm font-normal text-foreground/30",
|
||||
"data-[active=true]:text-foreground/80",
|
||||
"data-[active=true]:font-medium",
|
||||
"before:content-['']",
|
||||
"before:opacity-0",
|
||||
"data-[active=true]:before:opacity-100",
|
||||
"before:transition-opacity",
|
||||
"before:mr-3",
|
||||
"before:block",
|
||||
"before:bg-neutral-300",
|
||||
"before:w-1",
|
||||
"before:h-1",
|
||||
"before:rounded-full",
|
||||
paddingLeftByLevel[heading.level],
|
||||
)}
|
||||
data-active={activeId == heading.id}
|
||||
>
|
||||
<a href={`#${heading.id}`}>{heading.text}</a>
|
||||
</li>
|
||||
))}
|
||||
{headings.map(
|
||||
(heading, i) =>
|
||||
heading.level > 1 && (
|
||||
<li
|
||||
key={i}
|
||||
className={clsx(
|
||||
"transition-colors",
|
||||
"font-normal",
|
||||
"flex items-center text-sm font-normal text-default-500 dark:text-default-300",
|
||||
"data-[active=true]:text-foreground",
|
||||
"dark:data-[active=true]:text-foreground",
|
||||
"before:content-['']",
|
||||
"before:opacity-0",
|
||||
"data-[active=true]:before:opacity-100",
|
||||
"before:transition-opacity",
|
||||
"before:-ml-3",
|
||||
"before:absolute",
|
||||
"before:bg-default-400",
|
||||
"before:w-1",
|
||||
"before:h-1",
|
||||
"before:rounded-full",
|
||||
paddingLeftByLevel[heading.level],
|
||||
)}
|
||||
data-active={activeId == heading.id}
|
||||
>
|
||||
<a href={`#${heading.id}`}>{heading.text}</a>
|
||||
</li>
|
||||
),
|
||||
)}
|
||||
<li
|
||||
className="mt-2 opacity-0 data-[visible=true]:opacity-100 transition-opacity"
|
||||
data-visible={activeIndex >= 2}
|
||||
@@ -65,7 +99,7 @@ export const DocsToc: FC<DocsTocProps> = ({headings}) => {
|
||||
<Divider />
|
||||
<Spacer y={2} />
|
||||
<a
|
||||
className="flex gap-2 items-center text-sm text-foreground/30 hover:text-foreground/80 pl-4 transition-opacity"
|
||||
className="flex gap-2 items-center text-sm text-default-500 dark:text-foreground/30 hover:text-foreground/80 pl-4 transition-opacity"
|
||||
href={`#${firstId}`}
|
||||
>
|
||||
Back to top
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
"use client";
|
||||
|
||||
import dynamic from "next/dynamic";
|
||||
import {usePathname} from "next/navigation";
|
||||
|
||||
import {siteConfig} from "@/config/site";
|
||||
|
||||
const Link = dynamic(() => import("@nextui-org/react").then((mod) => mod.Link));
|
||||
const VercelCallout = dynamic(() => import("./vercel-callout").then((mod) => mod.VercelCallout));
|
||||
|
||||
export const Footer = () => {
|
||||
const pathname = usePathname();
|
||||
|
||||
if (pathname.includes("/examples")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<footer className="container mx-auto max-w-7xl pb-12 px-12">
|
||||
<div className="flex flex-col justify-center items-center gap-1">
|
||||
<p className="text-sm text-default-400">
|
||||
Created by
|
||||
<Link isExternal className="text-sm" href={siteConfig.links.portfolio}>
|
||||
Junior Garcia
|
||||
</Link>
|
||||
</p>
|
||||
<VercelCallout />
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
import {IconSvgProps} from "@/types";
|
||||
|
||||
export const DocumentCodeBoldIcon = ({size = 24, width, height, ...props}: IconSvgProps) => (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="none"
|
||||
focusable="false"
|
||||
height={size || height}
|
||||
role="presentation"
|
||||
viewBox="0 0 24 24"
|
||||
width={size || width}
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M16 2H8C4.5 2 3 4 3 7V17C3 20 4.5 22 8 22H16C19.5 22 21 20 21 17V7C21 4 19.5 2 16 2ZM10.53 16.47C10.82 16.76 10.82 17.24 10.53 17.53C10.38 17.68 10.19 17.75 10 17.75C9.81 17.75 9.62 17.68 9.47 17.53L7.47 15.53C7.18 15.24 7.18 14.76 7.47 14.47L9.47 12.47C9.76 12.18 10.24 12.18 10.53 12.47C10.82 12.76 10.82 13.24 10.53 13.53L9.06 15L10.53 16.47ZM16.53 15.53L14.53 17.53C14.38 17.68 14.19 17.75 14 17.75C13.81 17.75 13.62 17.68 13.47 17.53C13.18 17.24 13.18 16.76 13.47 16.47L14.94 15L13.47 13.53C13.18 13.24 13.18 12.76 13.47 12.47C13.76 12.18 14.24 12.18 14.53 12.47L16.53 14.47C16.82 14.76 16.82 15.24 16.53 15.53ZM18.5 9.25H16.5C14.98 9.25 13.75 8.02 13.75 6.5V4.5C13.75 4.09 14.09 3.75 14.5 3.75C14.91 3.75 15.25 4.09 15.25 4.5V6.5C15.25 7.19 15.81 7.75 16.5 7.75H18.5C18.91 7.75 19.25 8.09 19.25 8.5C19.25 8.91 18.91 9.25 18.5 9.25Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
@@ -0,0 +1,23 @@
|
||||
import {IconSvgProps} from "@/types";
|
||||
|
||||
export const HashBoldIcon = ({size = 24, width, height, ...props}: IconSvgProps) => (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="none"
|
||||
focusable="false"
|
||||
height={size || height}
|
||||
role="presentation"
|
||||
viewBox="0 0 24 24"
|
||||
width={size || width}
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M10.4199 13.4181H13.2599L13.5799 10.5781H10.7399L10.4199 13.4181Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M16.19 2H7.81C4.17 2 2 4.17 2 7.81V16.18C2 19.83 4.17 22 7.81 22H16.18C19.82 22 21.99 19.83 21.99 16.19V7.81C22 4.17 19.83 2 16.19 2ZM18.82 10.58H15.05L14.73 13.43H18.1C18.5 13.43 18.83 13.76 18.83 14.16C18.83 14.56 18.5 14.89 18.1 14.89H14.57L14.16 18.55C14.12 18.92 13.8 19.2 13.43 19.2C13.4 19.2 13.38 19.2 13.35 19.2C12.95 19.16 12.66 18.79 12.7 18.39L13.09 14.89H10.25L9.84 18.55C9.8 18.92 9.48 19.2 9.11 19.2C9.08 19.2 9.06 19.2 9.03 19.2C8.63 19.16 8.34 18.79 8.38 18.39L8.77 14.89H5.18C4.78 14.89 4.45 14.56 4.45 14.16C4.45 13.76 4.78 13.43 5.18 13.43H8.95L9.27 10.58H5.9C5.5 10.58 5.17 10.25 5.17 9.85C5.17 9.45 5.5 9.12 5.9 9.12H9.43L9.84 5.46C9.88 5.06 10.25 4.77 10.65 4.81C11.05 4.85 11.34 5.22 11.3 5.62L10.91 9.12H13.75L14.16 5.46C14.21 5.06 14.57 4.77 14.97 4.81C15.37 4.85 15.66 5.22 15.62 5.62L15.23 9.12H18.84C19.24 9.12 19.57 9.45 19.57 9.85C19.57 10.25 19.22 10.58 18.82 10.58Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
@@ -11,3 +11,8 @@ export * from "./previous";
|
||||
export * from "./repeat-one";
|
||||
export * from "./shuffle";
|
||||
export * from "./info";
|
||||
export * from "./document-code";
|
||||
export * from "./hash";
|
||||
export * from "./more-square";
|
||||
export * from "./play";
|
||||
export * from "./pause";
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import {IconSvgProps} from "@/types";
|
||||
|
||||
export const MoreSquareBoldIcon = ({size = 24, width, height, ...props}: IconSvgProps) => (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="none"
|
||||
focusable="false"
|
||||
height={size || height}
|
||||
role="presentation"
|
||||
viewBox="0 0 24 24"
|
||||
width={size || width}
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M16.19 2H7.81C4.17 2 2 4.17 2 7.81V16.18C2 19.83 4.17 22 7.81 22H16.18C19.82 22 21.99 19.83 21.99 16.19V7.81C22 4.17 19.83 2 16.19 2ZM8 13C7.44 13 7 12.55 7 12C7 11.45 7.45 11 8 11C8.55 11 9 11.45 9 12C9 12.55 8.56 13 8 13ZM12 13C11.44 13 11 12.55 11 12C11 11.45 11.45 11 12 11C12.55 11 13 11.45 13 12C13 12.55 12.56 13 12 13ZM16 13C15.44 13 15 12.55 15 12C15 11.45 15.45 11 16 11C16.55 11 17 11.45 17 12C17 12.55 16.56 13 16 13Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
@@ -0,0 +1,23 @@
|
||||
import {IconSvgProps} from "@/types";
|
||||
|
||||
export const PauseBoldIcon = ({size = 24, width, height, ...props}: IconSvgProps) => (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="none"
|
||||
focusable="false"
|
||||
height={size || height}
|
||||
role="presentation"
|
||||
viewBox="0 0 24 24"
|
||||
width={size || width}
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M10.65 19.11V4.89C10.65 3.54 10.08 3 8.64 3H5.01C3.57 3 3 3.54 3 4.89V19.11C3 20.46 3.57 21 5.01 21H8.64C10.08 21 10.65 20.46 10.65 19.11Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M21 19.11V4.89C21 3.54 20.43 3 18.99 3H15.36C13.93 3 13.35 3.54 13.35 4.89V19.11C13.35 20.46 13.92 21 15.36 21H18.99C20.43 21 21 20.46 21 19.11Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
@@ -0,0 +1,19 @@
|
||||
import {IconSvgProps} from "@/types";
|
||||
|
||||
export const PlayBoldIcon = ({size = 24, width, height, ...props}: IconSvgProps) => (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="none"
|
||||
focusable="false"
|
||||
height={size || height}
|
||||
role="presentation"
|
||||
viewBox="0 0 24 24"
|
||||
width={size || width}
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M4 11.9999V8.43989C4 4.01989 7.13 2.2099 10.96 4.4199L14.05 6.1999L17.14 7.9799C20.97 10.1899 20.97 13.8099 17.14 16.0199L14.05 17.7999L10.96 19.5799C7.13 21.7899 4 19.9799 4 15.5599V11.9999Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
@@ -0,0 +1,23 @@
|
||||
import {IconSvgProps} from "@/types";
|
||||
|
||||
export const ChevronRightLinearIcon = ({size = 24, width, height, ...props}: IconSvgProps) => (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="none"
|
||||
focusable="false"
|
||||
height={height || size}
|
||||
role="presentation"
|
||||
viewBox="0 0 24 24"
|
||||
width={width || size}
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M8.91003 19.9201L15.43 13.4001C16.2 12.6301 16.2 11.3701 15.43 10.6001L8.91003 4.08008"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeMiterlimit="10"
|
||||
strokeWidth="1.5"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
@@ -0,0 +1,43 @@
|
||||
import {IconSvgProps} from "@/types";
|
||||
|
||||
export const HashLinearIcon = ({size = 24, width, height, ...props}: IconSvgProps) => (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="none"
|
||||
focusable="false"
|
||||
height={height || size}
|
||||
role="presentation"
|
||||
viewBox="0 0 24 24"
|
||||
width={width || size}
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M10 3L8 21"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="1.5"
|
||||
/>
|
||||
<path
|
||||
d="M16 3L14 21"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="1.5"
|
||||
/>
|
||||
<path
|
||||
d="M3.5 9H21.5"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="1.5"
|
||||
/>
|
||||
<path
|
||||
d="M2.5 15H20.5"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="1.5"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
@@ -15,3 +15,8 @@ export * from "./paperclip";
|
||||
export * from "./link-squared";
|
||||
export * from "./link";
|
||||
export * from "./rotate-right";
|
||||
export * from "./hash";
|
||||
export * from "./chevron-right";
|
||||
export * from "./search";
|
||||
export * from "./simple-grid";
|
||||
export * from "./rotate-left";
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import {IconSvgProps} from "@/types";
|
||||
|
||||
export const RotateLeftLinearIcon = ({size = 24, width, height, ...props}: IconSvgProps) => (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="none"
|
||||
focusable="false"
|
||||
height={size || height}
|
||||
role="presentation"
|
||||
viewBox="0 0 24 24"
|
||||
width={size || width}
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M9.11008 5.0799C9.98008 4.8199 10.9401 4.6499 12.0001 4.6499C16.7901 4.6499 20.6701 8.5299 20.6701 13.3199C20.6701 18.1099 16.7901 21.9899 12.0001 21.9899C7.21008 21.9899 3.33008 18.1099 3.33008 13.3199C3.33008 11.5399 3.87008 9.8799 4.79008 8.4999"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="1.5"
|
||||
/>
|
||||
<path
|
||||
d="M7.87012 5.32L10.7601 2"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="1.5"
|
||||
/>
|
||||
<path
|
||||
d="M7.87012 5.32007L11.2401 7.78007"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="1.5"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
@@ -0,0 +1,35 @@
|
||||
import {IconSvgProps} from "@/types";
|
||||
|
||||
export const SearchLinearIcon = ({
|
||||
size = 24,
|
||||
strokeWidth = 1.5,
|
||||
width,
|
||||
height,
|
||||
...props
|
||||
}: IconSvgProps) => (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="none"
|
||||
focusable="false"
|
||||
height={height || size}
|
||||
role="presentation"
|
||||
viewBox="0 0 24 24"
|
||||
width={width || size}
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M11.5 21C16.7467 21 21 16.7467 21 11.5C21 6.25329 16.7467 2 11.5 2C6.25329 2 2 6.25329 2 11.5C2 16.7467 6.25329 21 11.5 21Z"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={strokeWidth}
|
||||
/>
|
||||
<path
|
||||
d="M22 22L20 20"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={strokeWidth}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
@@ -0,0 +1,29 @@
|
||||
import {IconSvgProps} from "@/types";
|
||||
|
||||
export const SimpleGridIcon = ({size = 24, width, height, ...props}: IconSvgProps) => (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="none"
|
||||
focusable="false"
|
||||
height={size || height}
|
||||
role="presentation"
|
||||
viewBox="0 0 24 24"
|
||||
width={size || width}
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M9 22H15C20 22 22 20 22 15V9C22 4 20 2 15 2H9C4 2 2 4 2 9V15C2 20 4 22 9 22Z"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="1.5"
|
||||
/>
|
||||
<path
|
||||
d="M9 2V22"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="1.5"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
@@ -0,0 +1,19 @@
|
||||
import {IconSvgProps} from "@/types";
|
||||
|
||||
export const VerticalDotsIcon = ({size = 24, width, height, ...props}: IconSvgProps) => (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="none"
|
||||
focusable="false"
|
||||
height={size || height}
|
||||
role="presentation"
|
||||
viewBox="0 0 24 24"
|
||||
width={size || width}
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M12 10c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0-6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 12c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
@@ -1,3 +1,5 @@
|
||||
"use client";
|
||||
|
||||
/* eslint-disable react/display-name */
|
||||
import {
|
||||
Button,
|
||||
@@ -19,12 +21,13 @@ import {
|
||||
DeleteDocumentBulkIcon,
|
||||
} from "@nextui-org/shared-icons";
|
||||
import Link from "next/link";
|
||||
import dynamic from "next/dynamic";
|
||||
import {useEffect, useRef, useState} from "react";
|
||||
|
||||
import {FeaturesGrid} from "./features-grid";
|
||||
|
||||
import landingContent from "@/content/landing";
|
||||
import {GradientBox, DemoCodeModal} from "@/components";
|
||||
import {GradientBox} from "@/components";
|
||||
import {
|
||||
KeyboardBoldIcon,
|
||||
MouseCircleBoldIcon,
|
||||
@@ -37,6 +40,10 @@ import {
|
||||
import {title, subtitle, titleWrapper, sectionWrapper} from "@/components/primitives";
|
||||
import {useIsMobile} from "@/hooks/use-media-query";
|
||||
|
||||
const DemoCodeModal = dynamic(() => import("../demo-code-modal").then((mod) => mod.DemoCodeModal), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
const a11yItems = [
|
||||
{
|
||||
title: "Keyboard navigation",
|
||||
@@ -64,7 +71,7 @@ const a11yItems = [
|
||||
},
|
||||
];
|
||||
|
||||
const iconClasses = "text-2xl text-neutral-500 pointer-events-none flex-shrink-0";
|
||||
const iconClasses = "text-2xl text-default-500 pointer-events-none flex-shrink-0";
|
||||
|
||||
export const A11yOtb = () => {
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
@@ -97,7 +104,7 @@ export const A11yOtb = () => {
|
||||
NextUI components are built on top of
|
||||
<NextUILink
|
||||
isExternal
|
||||
className="text-xl text-neutral-500 font-light [&>svg]:ml-1"
|
||||
className="text-xl text-default-500 font-light [&>svg]:ml-1"
|
||||
href="https://react-spectrum.adobe.com/react-aria/"
|
||||
underline="always"
|
||||
>
|
||||
@@ -112,15 +119,17 @@ export const A11yOtb = () => {
|
||||
classNames={{
|
||||
base: "grid-cols-1 md:grid-cols-2 lg:grid-cols-2 gap-4",
|
||||
header: "pb-3",
|
||||
iconWrapper: "bg-neutral-100 dark:bg-transparent text-neutral-500/50",
|
||||
card: "bg-white dark:bg-default-400/10",
|
||||
iconWrapper: "bg-default-100 dark:bg-transparent text-default-500/50",
|
||||
}}
|
||||
features={a11yItems}
|
||||
/>
|
||||
<Button
|
||||
aria-label="Learn more about accessibility"
|
||||
as={Link}
|
||||
className="max-w-fit"
|
||||
color="success"
|
||||
href="/docs/theme/customize-theme"
|
||||
href="/docs/customization/customize-theme"
|
||||
radius="full"
|
||||
size="sm"
|
||||
variant="flat"
|
||||
@@ -137,6 +146,7 @@ export const A11yOtb = () => {
|
||||
<Tooltip className="text-xs px-2" content="Show code" placement="top">
|
||||
<Button
|
||||
isIconOnly
|
||||
aria-label="Show code"
|
||||
className="absolute top-1 right-1 text-success-50 data-[hover]:bg-foreground/10"
|
||||
radius="full"
|
||||
variant="light"
|
||||
@@ -152,22 +162,22 @@ export const A11yOtb = () => {
|
||||
isOpen={isDropdownOpen}
|
||||
placement="bottom"
|
||||
portalContainer={ref.current}
|
||||
shouldBlockScroll={isMobile}
|
||||
shouldBlockScroll={false}
|
||||
shouldFlip={isMobile}
|
||||
onOpenChange={(open) => setIsDropdownOpen(open)}
|
||||
>
|
||||
<DropdownTrigger>
|
||||
<Button color="success" variant="flat">
|
||||
<Button className="bg-success-50" color="success" variant="flat">
|
||||
{isMobile ? "Click me" : "Actions"}
|
||||
</Button>
|
||||
</DropdownTrigger>
|
||||
<DropdownMenu
|
||||
aria-label="Actions"
|
||||
closeOnSelect={true}
|
||||
color="neutral"
|
||||
color="default"
|
||||
variant="flat"
|
||||
>
|
||||
<DropdownSection title="Actions">
|
||||
<DropdownSection showDivider title="Actions">
|
||||
<DropdownItem
|
||||
key="new"
|
||||
description="Create a new file"
|
||||
|
||||
@@ -2,7 +2,7 @@ import {Spacer} from "@nextui-org/react";
|
||||
|
||||
import {sectionWrapper, titleWrapper, title, subtitle} from "../primitives";
|
||||
|
||||
import {FeaturesGrid} from "@/components/marketing";
|
||||
import {FeaturesGrid} from "@/components/marketing/features-grid";
|
||||
import {communityAccounts} from "@/libs/constants";
|
||||
|
||||
export const Community = () => {
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
"use client";
|
||||
|
||||
/* eslint-disable react/display-name */
|
||||
import {useMemo, useState} from "react";
|
||||
import {Tabs, TabItem, Card, CardBody, Image, Button, RadioGroup, Radio} from "@nextui-org/react";
|
||||
import {Tabs, Tab, Card, CardBody, Image, Button, RadioGroup, Radio} from "@nextui-org/react";
|
||||
import get from "lodash/get";
|
||||
import NextLink from "next/link";
|
||||
import NextImage from "next/image";
|
||||
|
||||
import {shopCartStyles} from "./styles";
|
||||
|
||||
@@ -19,7 +22,7 @@ const themesTabs = (isMobile: boolean) => [
|
||||
icon: () => (
|
||||
<NextUILogo
|
||||
small
|
||||
className="text-neutral-400 group-data-[selected=true]:text-primary"
|
||||
className="text-default-400 group-data-[selected=true]:text-primary"
|
||||
size={isMobile ? 34 : 44}
|
||||
/>
|
||||
),
|
||||
@@ -57,10 +60,10 @@ type Tab = {id: string; title: () => JSX.Element; icon: () => JSX.Element};
|
||||
const itemSizes = ["xs", "s", "m", "l", "xl"];
|
||||
|
||||
const codeHighlights = {
|
||||
nextui: "6-15",
|
||||
modern: "22-31",
|
||||
elegant: "38-47",
|
||||
retro: "54-67",
|
||||
nextui: "6-19",
|
||||
modern: "26-39",
|
||||
elegant: "46-59",
|
||||
retro: "66-84",
|
||||
};
|
||||
|
||||
const CustomThemesExample = ({
|
||||
@@ -90,20 +93,20 @@ const CustomThemesExample = ({
|
||||
<div className="flex flex-col gap-6 ">
|
||||
<Tabs
|
||||
disableAnimation
|
||||
disableCursor
|
||||
disableCursorAnimation
|
||||
aria-label="Custom themes tabs"
|
||||
classNames={{
|
||||
base: "w-full md:max-w-[50%]",
|
||||
tab: "px-0 md:px-3 h-auto data-[selected=true]:bg-transparent",
|
||||
tabList: "w-full md:max-w-1/2 justify-start gap-0 md:gap-4",
|
||||
tabContent: "text-neutral-400 text-base",
|
||||
base: "w-full",
|
||||
tab: "px-0 w-fit h-auto data-[selected=true]:bg-transparent",
|
||||
tabList: "w-full justify-start gap-8",
|
||||
tabContent: "text-default-400 text-base",
|
||||
}}
|
||||
items={tabs}
|
||||
variant="light"
|
||||
onSelectionChange={onSelectionChange}
|
||||
>
|
||||
{(item) => (
|
||||
<TabItem
|
||||
<Tab
|
||||
key={item.id}
|
||||
title={
|
||||
<div className="flex flex-col justify-center items-center gap-2">
|
||||
@@ -114,13 +117,16 @@ const CustomThemesExample = ({
|
||||
/>
|
||||
)}
|
||||
</Tabs>
|
||||
<Card className={slots.wrapper()} radius="2xl">
|
||||
<Card className={slots.wrapper()} radius="lg">
|
||||
<CardBody className="relative flex-col md:flex-row md:items-center gap-4 md:gap-9 overflow-visible">
|
||||
<div className={slots.imageWrapper()}>
|
||||
<Image
|
||||
fill
|
||||
removeWrapper
|
||||
alt="Shoes theme example"
|
||||
as={NextImage}
|
||||
className={slots.img()}
|
||||
sizes="100vw"
|
||||
src="/images/shoes-1.png"
|
||||
/>
|
||||
</div>
|
||||
@@ -146,7 +152,7 @@ const CustomThemesExample = ({
|
||||
classNames={{
|
||||
wrapper: "hidden",
|
||||
labelWrapper: slots.sizeOption(),
|
||||
label: "text-sm font-semibold text-inherit",
|
||||
label: slots.sizeOptionLabel(),
|
||||
}}
|
||||
value={itemSize}
|
||||
>
|
||||
@@ -158,7 +164,6 @@ const CustomThemesExample = ({
|
||||
<Button
|
||||
className={slots.buyButton()}
|
||||
color="primary"
|
||||
radius="xl"
|
||||
variant={selectedTheme === "nextui" ? "shadow" : "solid"}
|
||||
>
|
||||
Buy now
|
||||
@@ -166,7 +171,7 @@ const CustomThemesExample = ({
|
||||
<Button
|
||||
className={slots.addToBagButton()}
|
||||
color="primary"
|
||||
radius="xl"
|
||||
radius="full"
|
||||
variant="bordered"
|
||||
>
|
||||
Add to bag
|
||||
@@ -175,6 +180,7 @@ const CustomThemesExample = ({
|
||||
</div>
|
||||
<Button
|
||||
isIconOnly
|
||||
aria-label="like"
|
||||
className={slots.starButton()}
|
||||
data-liked={liked}
|
||||
radius="full"
|
||||
@@ -186,10 +192,11 @@ const CustomThemesExample = ({
|
||||
</CardBody>
|
||||
</Card>
|
||||
<Button
|
||||
aria-label="Learn more about theme customization"
|
||||
as={NextLink}
|
||||
className="max-w-fit"
|
||||
className={slots.learnMoreButton()}
|
||||
color="primary"
|
||||
href="/docs/theme/customize-theme"
|
||||
href="/docs/customization/customize-theme"
|
||||
radius="full"
|
||||
size="sm"
|
||||
variant="flat"
|
||||
|
||||
@@ -25,7 +25,7 @@ export const shopCartStyles = tv({
|
||||
"before:transition-all",
|
||||
],
|
||||
img: [
|
||||
"overflow-visible",
|
||||
"object-[45%_50%]",
|
||||
"sm:scale-125",
|
||||
"absolute",
|
||||
"z-10",
|
||||
@@ -43,9 +43,9 @@ export const shopCartStyles = tv({
|
||||
],
|
||||
contentWrapper: "flex flex-col justify-center transition-all h-full h-[200px]",
|
||||
title: ["relative", "w-full", "flex-none", "text-xl", "font-semibold", "text-foreground"],
|
||||
description: ["my-2", "w-full", "text-base", "text-neutral-500"],
|
||||
description: ["my-2", "w-full", "text-base", "text-default-500"],
|
||||
price: "relative text-lg font-semibold text-foreground",
|
||||
previousPrice: "relative line-through font-semibold text-neutral-400 ml-3",
|
||||
previousPrice: "relative line-through font-semibold text-default-400 ml-3",
|
||||
percentOff: "relative font-normal text-success ml-3",
|
||||
sizeOption: [
|
||||
"w-8",
|
||||
@@ -56,10 +56,18 @@ export const shopCartStyles = tv({
|
||||
"items-center",
|
||||
"text-sm",
|
||||
"rounded-full",
|
||||
// focus ring
|
||||
"group-data-[focus-visible=true]:outline-none",
|
||||
"group-data-[focus-visible=true]:ring-2",
|
||||
"group-data-[focus-visible=true]:ring-focus",
|
||||
"group-data-[focus-visible=true]:ring-offset-2",
|
||||
"group-data-[focus-visible=true]:ring-offset-background",
|
||||
],
|
||||
sizeOptionLabel: "text-sm font-semibold text-inherit",
|
||||
buyButton: ["text-sm", "font-normal"],
|
||||
addToBagButton: ["text-sm", "font-normal"],
|
||||
starButton: "absolute top-3 right-3 text-neutral-400 data-[liked=true]:text-warning",
|
||||
starButton: "absolute top-3 right-3 text-default-400 data-[liked=true]:text-warning",
|
||||
learnMoreButton: "max-w-fit",
|
||||
},
|
||||
variants: {
|
||||
theme: {
|
||||
@@ -67,9 +75,10 @@ export const shopCartStyles = tv({
|
||||
card: "py-5",
|
||||
imageWrapper: ["before:rounded-2xl", "before:from-[#010187] before:to-[#18000E]"],
|
||||
sizeOption: [
|
||||
"group-data-[checked=true]:bg-primary",
|
||||
"group-data-[checked=true]:text-primary-foreground",
|
||||
"group-data-[selected=true]:bg-primary",
|
||||
"group-data-[selected=true]:text-primary-foreground",
|
||||
],
|
||||
learnMoreButton: "bg-primary/20 text-primary",
|
||||
},
|
||||
modern: {
|
||||
wrapper: "rounded-3xl",
|
||||
@@ -84,25 +93,34 @@ export const shopCartStyles = tv({
|
||||
title: "text-2xl",
|
||||
description: "text-sm",
|
||||
sizeOption: [
|
||||
"group-data-[checked=true]:bg-secondary",
|
||||
"group-data-[checked=true]:text-secondary-foreground",
|
||||
"group-data-[selected=true]:bg-secondary",
|
||||
"group-data-[selected=true]:text-secondary-foreground",
|
||||
"group-data-[focus-visible=true]:ring-secondary",
|
||||
],
|
||||
percentOff: "text-pink-500",
|
||||
buyButton: ["bg-secondary", "text-sm", "font-normal", "rounded-full"],
|
||||
buyButton: [
|
||||
"bg-secondary",
|
||||
"text-sm",
|
||||
"font-normal",
|
||||
"rounded-full",
|
||||
"data-[focus-visible=true]:ring-secondary",
|
||||
],
|
||||
addToBagButton: [
|
||||
"border-secondary",
|
||||
"text-secondary",
|
||||
"text-sm",
|
||||
"font-normal",
|
||||
"rounded-full",
|
||||
"data-[focus-visible=true]:ring-secondary",
|
||||
],
|
||||
learnMoreButton: "bg-secondary/20 text-secondary",
|
||||
},
|
||||
elegant: {
|
||||
wrapper: "rounded-md",
|
||||
wrapper: "rounded-sm",
|
||||
contentWrapper: "ml-3",
|
||||
imageWrapper: [
|
||||
"lg:scale-[1.3]",
|
||||
"before:rounded-md",
|
||||
"before:rounded-sm",
|
||||
"before:from-[#323232] before:to-[#000000]",
|
||||
"shadow-xl",
|
||||
],
|
||||
@@ -110,20 +128,32 @@ export const shopCartStyles = tv({
|
||||
title: "text-xl font-mono font-thin",
|
||||
description: "text-sm font-mono font-light",
|
||||
sizeOption: [
|
||||
"group-data-[checked=true]:bg-foreground",
|
||||
"group-data-[checked=true]:text-background",
|
||||
"rounded-sm w-7 h-7",
|
||||
"group-data-[selected=true]:bg-foreground",
|
||||
"group-data-[selected=true]:text-background",
|
||||
"group-data-[focus-visible=true]:ring-foreground",
|
||||
],
|
||||
sizeOptionLabel: "font-normal",
|
||||
price: "font-mono font-thin",
|
||||
previousPrice: "font-mono font-light ml-2",
|
||||
percentOff: "text-neutral-500 font-mono text-sm",
|
||||
buyButton: ["bg-foreground", "text-background", "text-sm", "font-normal", "rounded-sm"],
|
||||
percentOff: "text-default-500 font-mono text-sm",
|
||||
buyButton: [
|
||||
"bg-foreground",
|
||||
"text-background",
|
||||
"text-sm",
|
||||
"font-normal",
|
||||
"rounded-sm",
|
||||
"data-[focus-visible=true]:ring-foreground",
|
||||
],
|
||||
addToBagButton: [
|
||||
"border-foreground",
|
||||
"text-foreground",
|
||||
"text-sm",
|
||||
"font-normal",
|
||||
"rounded-sm",
|
||||
"data-[focus-visible=true]:ring-foreground",
|
||||
],
|
||||
learnMoreButton: "bg-foreground/10 text-foreground",
|
||||
},
|
||||
retro: {
|
||||
wrapper: "bg-[#F4E8D1] dark:bg-[#E1CA9E] rounded-sm",
|
||||
@@ -145,8 +175,11 @@ export const shopCartStyles = tv({
|
||||
description: "text-[0.7rem] uppercase text-black",
|
||||
sizeOption: [
|
||||
"text-black/80",
|
||||
"group-data-[checked=true]:bg-[#EE457E]",
|
||||
"group-data-[checked=true]:text-white",
|
||||
"group-data-[selected=true]:bg-[#EE457E]",
|
||||
"group-data-[selected=true]:text-white",
|
||||
"group-data-[focus-visible=true]:ring-[#EE457E]",
|
||||
"group-data-[focus-visible=true]:ring-offset-[#F4E8D1]",
|
||||
"group-data-[focus-visible=true]:dark:ring-offset-[#F4E8D1]",
|
||||
],
|
||||
price: "text-black",
|
||||
previousPrice: "text-black/60 ml-2",
|
||||
@@ -158,6 +191,9 @@ export const shopCartStyles = tv({
|
||||
"uppercase",
|
||||
"text-sm",
|
||||
"rounded-sm",
|
||||
"data-[focus-visible=true]:ring-[#EE457E]",
|
||||
"data-[focus-visible=true]:ring-offset-[#F4E8D1]",
|
||||
"data-[focus-visible=true]:dark:ring-offset-[#F4E8D1]",
|
||||
],
|
||||
addToBagButton: [
|
||||
"border-[#FFD34E]",
|
||||
@@ -166,7 +202,11 @@ export const shopCartStyles = tv({
|
||||
"font-medium",
|
||||
"text-sm",
|
||||
"rounded-sm",
|
||||
"data-[focus-visible=true]:ring-[#EE457E]",
|
||||
"data-[focus-visible=true]:ring-offset-[#F4E8D1]",
|
||||
"data-[focus-visible=true]:dark:ring-offset-[#F4E8D1]",
|
||||
],
|
||||
learnMoreButton: "bg-warning/20 text-warning",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"use client";
|
||||
|
||||
/* eslint-disable react/display-name */
|
||||
import {Button, Link} from "@nextui-org/react";
|
||||
import NextLink from "next/link";
|
||||
@@ -5,7 +7,7 @@ import NextLink from "next/link";
|
||||
import {title, subtitle, titleWrapper, sectionWrapper} from "@/components/primitives";
|
||||
import {CodeWindow} from "@/components/code-window";
|
||||
import landingContent from "@/content/landing";
|
||||
import {GradientBox} from "@/components";
|
||||
import {GradientBox} from "@/components/gradient-box";
|
||||
import {CustomButton} from "@/components/demos";
|
||||
|
||||
export const Customization = () => {
|
||||
@@ -23,7 +25,7 @@ export const Customization = () => {
|
||||
NextUI is based on{" "}
|
||||
<Link
|
||||
isExternal
|
||||
className="text-xl text-neutral-500 font-light [&>svg]:ml-1"
|
||||
className="text-xl text-default-500 font-light [&>svg]:ml-1"
|
||||
href="https://tailwind-variants.org"
|
||||
underline="always"
|
||||
>
|
||||
@@ -53,9 +55,10 @@ export const Customization = () => {
|
||||
</div>
|
||||
<div className="flex w-1/2 justify-start">
|
||||
<Button
|
||||
aria-label="Learn more about customization"
|
||||
as={NextLink}
|
||||
className="max-w-fit bg-pink-100 text-pink-500 dark:bg-pink-900 dark:text-pink-300"
|
||||
href="/docs/theme/customize-theme"
|
||||
href="/docs/customization/customize-theme"
|
||||
radius="full"
|
||||
size="sm"
|
||||
variant="flat"
|
||||
|
||||
@@ -1,16 +1,23 @@
|
||||
"use client";
|
||||
|
||||
/* eslint-disable react/display-name */
|
||||
import {Code, Button, Tooltip} from "@nextui-org/react";
|
||||
import {useState} from "react";
|
||||
import NextLink from "next/link";
|
||||
import dynamic from "next/dynamic";
|
||||
|
||||
import {MusicPlayer} from "@/components/demos";
|
||||
import {title, subtitle, titleWrapper, sectionWrapper} from "@/components/primitives";
|
||||
import {ThemeSwitch} from "@/components/theme-switch";
|
||||
import {CodeWindow} from "@/components/code-window";
|
||||
import landingContent from "@/content/landing";
|
||||
import {GradientBox, DemoCodeModal} from "@/components";
|
||||
import {GradientBox} from "@/components";
|
||||
import {InfoBoldIcon} from "@/components/icons";
|
||||
|
||||
const DemoCodeModal = dynamic(() => import("../demo-code-modal").then((mod) => mod.DemoCodeModal), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
export const DarkMode = () => {
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
|
||||
@@ -38,6 +45,7 @@ export const DarkMode = () => {
|
||||
<Tooltip className="text-xs px-2" content="Show code" placement="top">
|
||||
<Button
|
||||
isIconOnly
|
||||
aria-label="Show code"
|
||||
className="text-white/70 dark:text-black/70 data-[hover]:bg-foreground/10"
|
||||
radius="full"
|
||||
variant="light"
|
||||
@@ -48,7 +56,7 @@ export const DarkMode = () => {
|
||||
</Tooltip>
|
||||
<ThemeSwitch
|
||||
classNames={{
|
||||
base: "p-1.5 bg-transparent rounded-xl",
|
||||
base: "mt-1 bg-transparent rounded-xl",
|
||||
wrapper: "!text-white/70 dark:!text-black/70",
|
||||
}}
|
||||
/>
|
||||
@@ -66,10 +74,11 @@ export const DarkMode = () => {
|
||||
</div>
|
||||
<div className="flex w-1/2 justify-start">
|
||||
<Button
|
||||
aria-label="Learn more about dark mode"
|
||||
as={NextLink}
|
||||
className="max-w-fit"
|
||||
color="warning"
|
||||
href="/docs/theme/dark-mode"
|
||||
href="/docs/customization/dark-mode"
|
||||
radius="full"
|
||||
size="sm"
|
||||
variant="flat"
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
import React, {ReactNode, useMemo} from "react";
|
||||
"use client";
|
||||
|
||||
import React, {ReactNode} from "react";
|
||||
import {tv} from "tailwind-variants";
|
||||
import {Card, CardHeader, CardBody, LinkProps, SlotsToClasses} from "@nextui-org/react";
|
||||
import {useRouter} from "next/router";
|
||||
import {useRouter} from "next/navigation";
|
||||
import {LinkIcon} from "@nextui-org/shared-icons";
|
||||
|
||||
const styles = tv({
|
||||
slots: {
|
||||
base: "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4",
|
||||
card:
|
||||
"dark:border-transparent bg-white/5 dark:bg-neutral-400/10 backdrop-blur-lg backdrop-saturate-[1.8]",
|
||||
card: "border-transparent bg-white/5 dark:bg-default-400/10 backdrop-blur-lg backdrop-saturate-[1.8]",
|
||||
header: "gap-2 pb-0",
|
||||
body: "",
|
||||
iconWrapper:
|
||||
"flex justify-center p-2 rounded-full items-center bg-secondary-100/80 text-pink-500",
|
||||
title: "text-base font-semibold",
|
||||
description: "font-light text-base text-neutral-500",
|
||||
description: "font-normal text-base text-default-500",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -34,7 +35,7 @@ interface FeaturesGridProps {
|
||||
export const FeaturesGrid: React.FC<FeaturesGridProps> = ({features, classNames, ...props}) => {
|
||||
const router = useRouter();
|
||||
|
||||
const slots = useMemo(() => styles(), []);
|
||||
const slots = styles();
|
||||
|
||||
const handleClick = (feat: Feature) => {
|
||||
if (!feat.href) {
|
||||
@@ -64,13 +65,13 @@ export const FeaturesGrid: React.FC<FeaturesGridProps> = ({features, classNames,
|
||||
<p className={slots.title({class: classNames?.title})}>{feat.title}</p>
|
||||
{feat.isExternal && <LinkIcon className="text-white" height={18} width={18} />}
|
||||
</CardHeader>
|
||||
{feat.description && (
|
||||
{feat.description ? (
|
||||
<CardBody className={slots.body({class: classNames?.body})}>
|
||||
<p className={slots.description({class: classNames?.description})}>
|
||||
{feat.description}
|
||||
</p>
|
||||
</CardBody>
|
||||
)}
|
||||
) : null}
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -1,235 +0,0 @@
|
||||
import {
|
||||
Button,
|
||||
Input,
|
||||
Tooltip,
|
||||
Image,
|
||||
CardFooter,
|
||||
Card,
|
||||
CardBody,
|
||||
Switch,
|
||||
Spinner,
|
||||
Pagination,
|
||||
Tabs,
|
||||
TabItem,
|
||||
Link,
|
||||
} from "@nextui-org/react";
|
||||
import {clsx} from "@nextui-org/shared-utils";
|
||||
import {ArrowRightIcon, MoonFilledIcon, SunFilledIcon} from "@nextui-org/shared-icons";
|
||||
import {useTheme} from "next-themes";
|
||||
import NextLink from "next/link";
|
||||
|
||||
import {title, subtitle} from "@/components/primitives";
|
||||
import {NextUILogo} from "@/components";
|
||||
import {GithubIcon} from "@/components/icons";
|
||||
import {UserTwitterCard} from "@/components/demos";
|
||||
import {useIsMounted} from "@/hooks/use-is-mounted";
|
||||
import {useMediaQuery} from "@/hooks/use-media-query";
|
||||
|
||||
const FloatingComponents: React.FC<{mounted: boolean}> = ({mounted}) => {
|
||||
const {theme, setTheme} = useTheme();
|
||||
|
||||
const isSelected = theme === "dark" && mounted;
|
||||
|
||||
const isTablet = useMediaQuery(1024);
|
||||
|
||||
const onChange = () => {
|
||||
theme === "light" ? setTheme("dark") : setTheme("light");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="hidden lg:flex flex-col relative w-1/2">
|
||||
<>
|
||||
<Switch
|
||||
classNames={{
|
||||
base:
|
||||
"absolute -top-[220px] -right-[40px] animate-[levitate_13s_ease_infinite_1s_reverse]",
|
||||
wrapper: "shadow-sm",
|
||||
startIcon: "text-white",
|
||||
}}
|
||||
endIcon={<MoonFilledIcon />}
|
||||
isSelected={isSelected}
|
||||
size="xl"
|
||||
startIcon={<SunFilledIcon />}
|
||||
onChange={onChange}
|
||||
/>
|
||||
|
||||
<Input
|
||||
className="absolute -top-[130px] -right-[120px] animate-[levitate_10s_ease_infinite] w-[200px]"
|
||||
color="secondary"
|
||||
defaultValue="NextUI"
|
||||
label="Input"
|
||||
labelPosition="outside"
|
||||
radius="xl"
|
||||
variant="bordered"
|
||||
onClear={() => {}}
|
||||
/>
|
||||
|
||||
<Card
|
||||
isFooterBlurred
|
||||
className="absolute -top-[260px] right-[100px] h-[120px] animate-[levitate_12s_ease_infinite_1s] z-0 max-w-fit"
|
||||
radius="2xl"
|
||||
>
|
||||
<Image
|
||||
alt="Professional camera"
|
||||
className="object-cover -translate-y-12 h-[100%]"
|
||||
src="/images/card-example-6.jpeg"
|
||||
width={120}
|
||||
/>
|
||||
<CardFooter className="before:bg-black/10 before:border before:border-white/20 overflow-hidden justify-between py-2 absolute before:rounded-xl rounded-xl bottom-1 w-[calc(100%_-_8px)] shadow-lg ml-1 z-10">
|
||||
<p className="text-xs font-semibold text-white/80">Camera</p>
|
||||
<p className="text-xs font-semibold text-white/80">$525</p>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
|
||||
<Tabs
|
||||
aria-label="Floating tabs example"
|
||||
className=""
|
||||
classNames={{
|
||||
base: "absolute left-[170px] -top-[160px] h-10 animate-[levitate_17s_ease_infinite_1s]",
|
||||
tabList: "max-w-[200px] bg-content1 shadow-sm",
|
||||
}}
|
||||
radius="full"
|
||||
size="xs"
|
||||
>
|
||||
<TabItem key="notes" title="Notes" />
|
||||
<TabItem key="tasks" title="Tasks" />
|
||||
<TabItem key="files" title="Files" />
|
||||
</Tabs>
|
||||
|
||||
<UserTwitterCard className="absolute left-[80px] -top-[80px] animate-[levitate_16s_ease_infinite] border-none" />
|
||||
|
||||
<Card className="absolute right-[110px] -top-[60px] animate-[levitate_18s_ease_infinite] shadow-lg z-10 max-w-fit border-none">
|
||||
<CardBody>
|
||||
<NextUILogo small size={60} />
|
||||
</CardBody>
|
||||
</Card>
|
||||
|
||||
<div className="absolute z-10 -top-[40px] -right-[230px] animate-[levitate_14s_ease_infinite_1s]">
|
||||
<Pagination
|
||||
isCompact
|
||||
showControls
|
||||
showShadow
|
||||
classNames={{
|
||||
base: "shadow-sm rounded-xl",
|
||||
item: "bg-background dark:bg-content1",
|
||||
prev: "bg-background dark:bg-content1",
|
||||
next: "bg-background dark:bg-content1",
|
||||
}}
|
||||
initialPage={6}
|
||||
total={10}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{mounted && (
|
||||
<Tooltip
|
||||
showArrow
|
||||
className="text-sm animate-[levitate_14s_ease_infinite]"
|
||||
color="secondary"
|
||||
content="Developers love Next.js"
|
||||
isOpen={!isTablet}
|
||||
placement="top"
|
||||
>
|
||||
<Button
|
||||
className="absolute left-[200px] top-[160px] max-w-fit animate-[levitate_14s_ease_infinite_0.5s]"
|
||||
color="secondary"
|
||||
size="sm"
|
||||
variant="flat"
|
||||
>
|
||||
Tooltip
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
<Card className="absolute right-[10px] top-[30px] animate-[levitate_16s_ease_infinite] shadow-lg z-10 max-w-fit border-none">
|
||||
<CardBody>
|
||||
<Spinner color="secondary" size="xl" />
|
||||
</CardBody>
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
isFooterBlurred
|
||||
className="absolute right-[60px] top-[100px] animate-[levitate_12s_ease_infinite_1s] z-0 max-w-fit"
|
||||
radius="2xl"
|
||||
>
|
||||
<Image
|
||||
alt="Woman listing to music"
|
||||
className="object-cover"
|
||||
height={200}
|
||||
src="/images/hero-card.jpeg"
|
||||
width={200}
|
||||
/>
|
||||
<CardFooter className="before:bg-white/10 overflow-hidden justify-between py-2 absolute before:rounded-xl rounded-xl bottom-1 w-[calc(100%_-_8px)] shadow-lg ml-1 z-10">
|
||||
<p className="text-xs text-white/80">Available soon.</p>
|
||||
<Button color="secondary" radius="full" size="xs" variant="flat">
|
||||
Notify me
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const Hero = () => {
|
||||
const isMounted = useIsMounted();
|
||||
|
||||
return (
|
||||
<section className="flex relative w-full flex-nowrap justify-between items-center h-[calc(100vh_-_64px)] 2xl:h-[calc(84vh_-_64px)]">
|
||||
<div className="flex flex-col gap-6 w-full lg:w-1/2 xl:mt-10">
|
||||
<div className="text-center leading-8 md:leading-10 md:text-left">
|
||||
<div className="inline-block">
|
||||
<h1 className={title()}>Make </h1>
|
||||
<h1 className={title({color: "violet"})}>beautiful </h1>
|
||||
</div>
|
||||
<h1 className={title()}>websites regardless of your design experience.</h1>
|
||||
</div>
|
||||
<h4 className={subtitle({fullWidth: true, class: "text-center md:text-left"})}>
|
||||
Beautiful, fast and modern React UI library.
|
||||
</h4>
|
||||
<div className="flex flex-col md:flex-row items-center gap-4">
|
||||
<Button
|
||||
as={NextLink}
|
||||
className="w-full md:w-auto"
|
||||
color="primary"
|
||||
endIcon={
|
||||
<ArrowRightIcon
|
||||
className="group-data-[hover=true]:translate-x-0.5 transition-transform"
|
||||
strokeWidth={2}
|
||||
/>
|
||||
}
|
||||
href="/docs/guide/introduction"
|
||||
radius="full"
|
||||
size="lg"
|
||||
>
|
||||
Get Started
|
||||
</Button>
|
||||
<Button
|
||||
isExternal
|
||||
as={Link}
|
||||
className="w-full md:w-auto"
|
||||
href="https://github.com/nextui-org/nextui"
|
||||
radius="full"
|
||||
size="lg"
|
||||
startIcon={<GithubIcon />}
|
||||
variant="bordered"
|
||||
>
|
||||
Github
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<FloatingComponents mounted={isMounted} />
|
||||
|
||||
<div
|
||||
className={clsx(
|
||||
"absolute -top-20 lg:top-10 w-screen h-screen -z-50 opacity-0",
|
||||
"data-[mounted=true]:opacity-100 transition-opacity",
|
||||
"bg-left bg-no-repeat bg-[url('/gradients/looper-pattern.svg')]",
|
||||
"after:content-[''] after:absolute after:top-0 after:left-0 after:w-full after:h-full after:z-[-1]",
|
||||
"after:bg-gradient-to-r after:from-transparent after:to-white dark:after:to-black after:z-[-1]",
|
||||
)}
|
||||
data-mounted={isMounted}
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
"use client";
|
||||
|
||||
import {clsx} from "@nextui-org/shared-utils";
|
||||
|
||||
import {useIsMounted} from "@/hooks/use-is-mounted";
|
||||
|
||||
export const BgLooper = () => {
|
||||
const isMounted = useIsMounted();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
"absolute -top-20 lg:top-10 w-screen h-screen z-0 opacity-0 overflow-hidden",
|
||||
"data-[mounted=true]:opacity-100 transition-opacity",
|
||||
"bg-left bg-no-repeat bg-[url('/gradients/looper-pattern.svg')]",
|
||||
"after:content-[''] after:absolute after:top-0 after:left-0 after:w-full after:h-full after:z-[-1]",
|
||||
"after:bg-gradient-to-r after:from-transparent after:to-background dark:after:to-black after:z-[-1]",
|
||||
)}
|
||||
data-mounted={isMounted}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,163 @@
|
||||
import {
|
||||
Button,
|
||||
Tooltip,
|
||||
Input,
|
||||
Image,
|
||||
Card,
|
||||
CardBody,
|
||||
CardFooter,
|
||||
Switch,
|
||||
Spinner,
|
||||
Pagination,
|
||||
} from "@nextui-org/react";
|
||||
import {MoonFilledIcon, SunFilledIcon} from "@nextui-org/shared-icons";
|
||||
import {useTheme} from "next-themes";
|
||||
import NextImage from "next/image";
|
||||
|
||||
import {FloatingTabs} from "./floating-tabs";
|
||||
|
||||
import {UserTwitterCard} from "@/components/demos/user-twitter-card";
|
||||
import {useIsMounted} from "@/hooks/use-is-mounted";
|
||||
import {useMediaQuery} from "@/hooks/use-media-query";
|
||||
import {NextUILogo} from "@/components/nextui-logo";
|
||||
|
||||
export const FloatingComponents: React.FC<{}> = () => {
|
||||
const {theme, setTheme} = useTheme();
|
||||
|
||||
const isMounted = useIsMounted();
|
||||
const isSelected = theme === "dark" && isMounted;
|
||||
|
||||
const isTablet = useMediaQuery(1024);
|
||||
|
||||
const onChange = () => {
|
||||
theme === "light" ? setTheme("dark") : setTheme("light");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="hidden lg:flex flex-col relative z-20 w-1/2">
|
||||
<>
|
||||
<Switch
|
||||
classNames={{
|
||||
base: "absolute -top-[220px] -right-[40px] animate-[levitate_13s_ease_infinite_1s_reverse]",
|
||||
wrapper: "shadow-sm",
|
||||
startContent: "text-white",
|
||||
}}
|
||||
endContent={<MoonFilledIcon />}
|
||||
isSelected={isSelected}
|
||||
size="lg"
|
||||
startContent={<SunFilledIcon />}
|
||||
onChange={onChange}
|
||||
/>
|
||||
|
||||
<Input
|
||||
className="absolute -top-[130px] -right-[120px] animate-[levitate_10s_ease_infinite] w-[200px]"
|
||||
color="secondary"
|
||||
defaultValue="NextUI"
|
||||
label="Input"
|
||||
labelPlacement="outside"
|
||||
variant="bordered"
|
||||
onClear={() => {}}
|
||||
/>
|
||||
|
||||
<Card
|
||||
isFooterBlurred
|
||||
className="absolute -top-[260px] right-[100px] h-[120px] animate-[levitate_12s_ease_infinite_1s] z-0 max-w-fit"
|
||||
>
|
||||
<Image
|
||||
alt="Professional camera"
|
||||
as={NextImage}
|
||||
className="object-cover -translate-y-12 h-[100%]"
|
||||
height={120}
|
||||
src="/images/card-example-6.webp"
|
||||
width={120}
|
||||
/>
|
||||
<CardFooter className="before:bg-black/10 border-white/20 border-1 overflow-hidden justify-between py-2 absolute before:rounded-xl rounded-xl bottom-1 w-[calc(100%_-_8px)] shadow-lg ml-1 z-10">
|
||||
<p className="text-xs font-semibold text-white/80">Camera</p>
|
||||
<p className="text-xs font-semibold text-white/80">$525</p>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
|
||||
<FloatingTabs />
|
||||
|
||||
<UserTwitterCard className="absolute left-[80px] -top-[80px] animate-[levitate_16s_ease_infinite] border-none" />
|
||||
|
||||
<Card
|
||||
className="absolute right-[110px] -top-[60px] animate-[levitate_18s_ease_infinite] z-10 max-w-fit border-none"
|
||||
shadow="lg"
|
||||
>
|
||||
<CardBody>
|
||||
<NextUILogo small size={60} />
|
||||
</CardBody>
|
||||
</Card>
|
||||
|
||||
<div className="absolute z-10 -top-[40px] -right-[230px] animate-[levitate_14s_ease_infinite_1s]">
|
||||
<Pagination
|
||||
isCompact
|
||||
showControls
|
||||
showShadow
|
||||
classNames={{
|
||||
base: "rounded-xl",
|
||||
}}
|
||||
initialPage={6}
|
||||
total={10}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{isMounted && (
|
||||
<Tooltip
|
||||
showArrow
|
||||
className="text-sm animate-[levitate_14s_ease_infinite]"
|
||||
color="secondary"
|
||||
content="Developers love Next.js"
|
||||
isOpen={!isTablet}
|
||||
placement="top"
|
||||
>
|
||||
<Button
|
||||
className="absolute left-[200px] top-[160px] max-w-fit animate-[levitate_14s_ease_infinite_0.5s]"
|
||||
color="secondary"
|
||||
size="sm"
|
||||
variant="flat"
|
||||
>
|
||||
Tooltip
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
<Card
|
||||
className="absolute right-[10px] top-[30px] animate-[levitate_16s_ease_infinite] z-10 max-w-fit border-none"
|
||||
shadow="lg"
|
||||
>
|
||||
<CardBody>
|
||||
<Spinner color="secondary" size="lg" />
|
||||
</CardBody>
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
isFooterBlurred
|
||||
className="absolute right-[60px] top-[100px] animate-[levitate_12s_ease_infinite_1s] z-0 max-w-fit"
|
||||
>
|
||||
<Image
|
||||
alt="Woman listing to music"
|
||||
as={NextImage}
|
||||
className="object-cover"
|
||||
height={200}
|
||||
src="/images/hero-card.webp"
|
||||
width={200}
|
||||
/>
|
||||
<CardFooter className="justify-between before:bg-white/10 border-white/20 border-1 overflow-hidden py-1 absolute before:rounded-xl rounded-large bottom-1 w-[calc(100%_-_8px)] shadow-small ml-1 z-10">
|
||||
<p className="text-tiny text-white/80">Available soon.</p>
|
||||
<Button
|
||||
className="text-tiny text-white bg-black/20"
|
||||
color="default"
|
||||
radius="lg"
|
||||
size="sm"
|
||||
variant="flat"
|
||||
>
|
||||
Notify me
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
"use client";
|
||||
|
||||
import {Tab, Tabs} from "@nextui-org/react";
|
||||
|
||||
export const FloatingTabs: React.FC<{}> = () => {
|
||||
return (
|
||||
<Tabs
|
||||
aria-label="Floating tabs example"
|
||||
className=""
|
||||
classNames={{
|
||||
base: "absolute left-[170px] -top-[160px] h-10 animate-[levitate_17s_ease_infinite_1s]",
|
||||
tabList: "max-w-[200px] shadow-sm",
|
||||
}}
|
||||
radius="full"
|
||||
size="sm"
|
||||
>
|
||||
<Tab key="notes" title="Notes" />
|
||||
<Tab key="tasks" title="Tasks" />
|
||||
<Tab key="files" title="Files" />
|
||||
</Tabs>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,70 @@
|
||||
"use client";
|
||||
|
||||
import NextLink from "next/link";
|
||||
import {Button, Link} from "@nextui-org/react";
|
||||
import {ArrowRightIcon} from "@nextui-org/shared-icons";
|
||||
import dynamic from "next/dynamic";
|
||||
|
||||
import {FloatingComponents} from "./floating-components";
|
||||
|
||||
import {title, subtitle} from "@/components/primitives";
|
||||
import {GithubIcon} from "@/components/icons";
|
||||
|
||||
const BgLooper = dynamic(() => import("./bg-looper").then((mod) => mod.BgLooper), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
export const Hero = () => {
|
||||
return (
|
||||
<section className="flex relative overflow-hidden lg:overflow-visible w-full flex-nowrap justify-between items-center h-[calc(100vh_-_64px)] 2xl:h-[calc(84vh_-_64px)]">
|
||||
<div className="flex relative z-20 flex-col gap-6 w-full lg:w-1/2 xl:mt-10">
|
||||
<div className="text-center leading-8 md:leading-10 md:text-left">
|
||||
<div className="inline-block">
|
||||
<h1 className={title()}>Make </h1>
|
||||
<h1 className={title({color: "violet"})}>beautiful </h1>
|
||||
</div>
|
||||
<h1 className={title()}>websites regardless of your design experience.</h1>
|
||||
</div>
|
||||
<h2 className={subtitle({fullWidth: true, class: "text-center md:text-left"})}>
|
||||
Beautiful, fast and modern React UI library.
|
||||
</h2>
|
||||
<div className="flex flex-col md:flex-row items-center gap-4">
|
||||
<Button
|
||||
as={NextLink}
|
||||
className="w-full md:w-auto"
|
||||
color="primary"
|
||||
endContent={
|
||||
<ArrowRightIcon
|
||||
className="group-data-[hover=true]:translate-x-0.5 outline-none transition-transform"
|
||||
strokeWidth={2}
|
||||
/>
|
||||
}
|
||||
href="/docs/guide/introduction"
|
||||
radius="full"
|
||||
size="lg"
|
||||
>
|
||||
Get Started
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
fullWidth
|
||||
isExternal
|
||||
as={Link}
|
||||
className="w-full md:w-auto"
|
||||
href="https://github.com/nextui-org/nextui"
|
||||
radius="full"
|
||||
size="lg"
|
||||
startContent={<GithubIcon />}
|
||||
variant="bordered"
|
||||
>
|
||||
Github
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<FloatingComponents />
|
||||
|
||||
<BgLooper />
|
||||
</section>
|
||||
);
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user