mirror of
https://github.com/supabase/supabase.git
synced 2026-10-12 12:55:06 +03:00
Merge branch 'master' into PLyn/storage-docs-update
This commit is contained in:
2862 files changed
+239140
-152394
No files matched your search
Executable
+7
@@ -0,0 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [ "$CLAUDE_CODE_REMOTE" != "true" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
pnpm install
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"hooks": {
|
||||
"SessionStart": [
|
||||
{
|
||||
"matcher": "startup",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/scripts/install_pkgs.sh"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,267 +0,0 @@
|
||||
---
|
||||
description: Docs GraphQL Architecture
|
||||
globs: apps/docs/resources/**/*.ts
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Docs GraphQL Architecture
|
||||
|
||||
## Overview
|
||||
|
||||
The `/apps/docs/resources` folder contains the GraphQL endpoint architecture for the docs GraphQL endpoint at `/api/graphql`. It follows a modular pattern where each top-level query is organized into its own folder with consistent file structure.
|
||||
|
||||
## Architecture Pattern
|
||||
|
||||
Each GraphQL query follows this structure:
|
||||
|
||||
```
|
||||
resources/
|
||||
├── queryObject/
|
||||
│ ├── queryObjectModel.ts # Data models and business logic
|
||||
│ ├── queryObjectSchema.ts # GraphQL type definitions
|
||||
│ ├── queryObjectResolver.ts # Query resolver and arguments
|
||||
│ ├── queryObjectTypes.ts # TypeScript interfaces (optional)
|
||||
│ └── queryObjectSync.ts # Functions for syncing repo content to the database (optional)
|
||||
├── utils/
|
||||
│ ├── connections.ts # GraphQL connection/pagination utilities
|
||||
│ └── fields.ts # GraphQL field selection utilities
|
||||
├── rootSchema.ts # Main GraphQL schema with all queries
|
||||
└── rootSync.ts # Root sync script for syncing to database
|
||||
```
|
||||
|
||||
## Example queries
|
||||
|
||||
1. **searchDocs** (`globalSearch/`) - Vector-based search across all docs content
|
||||
2. **error** (`error/`) - Error code lookup for Supabase services
|
||||
3. **schema** - GraphQL schema introspection
|
||||
|
||||
## Key Files
|
||||
|
||||
### `rootSchema.ts`
|
||||
- Main GraphQL schema definition
|
||||
- Imports all resolvers and combines them into the root query
|
||||
- Defines the `RootQueryType` with all top-level fields
|
||||
|
||||
### `utils/connections.ts`
|
||||
- Provides `createCollectionType()` for paginated collections
|
||||
- `GraphQLCollectionBuilder` for building collection responses
|
||||
- Standard pagination arguments and edge/node patterns
|
||||
|
||||
### `utils/fields.ts`
|
||||
- `graphQLFields()` utility to analyze requested fields in resolvers
|
||||
- Used for optimizing data fetching based on what fields are actually requested
|
||||
|
||||
## Creating a New Top-Level Query
|
||||
|
||||
To add a new GraphQL query, follow these steps:
|
||||
|
||||
### 1. Create Query Folder Structure
|
||||
```bash
|
||||
mkdir resources/newQuery
|
||||
touch resources/newQuery/newQueryModel.ts
|
||||
touch resources/newQuery/newQuerySchema.ts
|
||||
touch resources/newQuery/newQueryResolver.ts
|
||||
```
|
||||
|
||||
### 2. Define GraphQL Schema (`newQuerySchema.ts`)
|
||||
```typescript
|
||||
import { GraphQLObjectType, GraphQLString } from 'graphql'
|
||||
|
||||
export const GRAPHQL_FIELD_NEW_QUERY = 'newQuery' as const
|
||||
|
||||
export const GraphQLObjectTypeNewQuery = new GraphQLObjectType({
|
||||
name: 'NewQuery',
|
||||
description: 'Description of what this query returns',
|
||||
fields: {
|
||||
id: {
|
||||
type: GraphQLString,
|
||||
description: 'Unique identifier',
|
||||
},
|
||||
// Add other fields...
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
### 3. Create Data Model (`newQueryModel.ts`)
|
||||
|
||||
> [!NOTE]
|
||||
> The data model should be agnostic to GraphQL. It may import argument types
|
||||
> from `~/__generated__/graphql`, but otherwise all functions and classes
|
||||
> should be unaware of whether they are called for GraphQL resolution.
|
||||
|
||||
> [!TIP]
|
||||
> The types in `~/__generated__/graphql` for a new endpoint will not exist
|
||||
> until the code generation is run in the next step.
|
||||
|
||||
```typescript
|
||||
import { type RootQueryTypeNewQueryArgs } from '~/__generated__/graphql'
|
||||
import { convertPostgrestToApiError, type ApiErrorGeneric } from '~/app/api/utils'
|
||||
import { Result } from '~/features/helpers.fn'
|
||||
import { supabase } from '~/lib/supabase'
|
||||
|
||||
export class NewQueryModel {
|
||||
constructor(public readonly data: {
|
||||
id: string
|
||||
// other properties...
|
||||
}) {}
|
||||
|
||||
static async loadData(
|
||||
args: RootQueryTypeNewQueryArgs,
|
||||
requestedFields: Array<string>
|
||||
): Promise<Result<NewQueryModel[], ApiErrorGeneric>> {
|
||||
// Implement data fetching logic
|
||||
const result = new Result(
|
||||
await supabase()
|
||||
.from('your_table')
|
||||
.select('*')
|
||||
// Add filters based on args
|
||||
)
|
||||
.map((data) => data.map((item) => new NewQueryModel(item)))
|
||||
.mapError(convertPostgrestToApiError)
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Create Resolver (`newQueryResolver.ts`)
|
||||
```typescript
|
||||
import { GraphQLError, GraphQLNonNull, GraphQLString, type GraphQLResolveInfo } from 'graphql'
|
||||
import { type RootQueryTypeNewQueryArgs } from '~/__generated__/graphql'
|
||||
import { convertUnknownToApiError } from '~/app/api/utils'
|
||||
import { Result } from '~/features/helpers.fn'
|
||||
import { graphQLFields } from '../utils/fields'
|
||||
import { NewQueryModel } from './newQueryModel'
|
||||
import { GRAPHQL_FIELD_NEW_QUERY, GraphQLObjectTypeNewQuery } from './newQuerySchema'
|
||||
|
||||
async function resolveNewQuery(
|
||||
_parent: unknown,
|
||||
args: RootQueryTypeNewQueryArgs,
|
||||
_context: unknown,
|
||||
info: GraphQLResolveInfo
|
||||
): Promise<NewQueryModel[] | GraphQLError> {
|
||||
return (
|
||||
await Result.tryCatchFlat(
|
||||
resolveNewQueryImpl,
|
||||
convertUnknownToApiError,
|
||||
args,
|
||||
info
|
||||
)
|
||||
).match(
|
||||
(data) => data,
|
||||
(error) => {
|
||||
console.error(`Error resolving ${GRAPHQL_FIELD_NEW_QUERY}:`, error)
|
||||
return new GraphQLError(error.isPrivate() ? 'Internal Server Error' : error.message)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
async function resolveNewQueryImpl(
|
||||
args: RootQueryTypeNewQueryArgs,
|
||||
info: GraphQLResolveInfo
|
||||
): Promise<Result<NewQueryModel[], ApiErrorGeneric>> {
|
||||
const fieldsInfo = graphQLFields(info)
|
||||
const requestedFields = Object.keys(fieldsInfo)
|
||||
return await NewQueryModel.loadData(args, requestedFields)
|
||||
}
|
||||
|
||||
export const newQueryRoot = {
|
||||
[GRAPHQL_FIELD_NEW_QUERY]: {
|
||||
description: 'Description of what this query does',
|
||||
args: {
|
||||
id: {
|
||||
type: new GraphQLNonNull(GraphQLString),
|
||||
description: 'Required argument description',
|
||||
},
|
||||
// Add other arguments...
|
||||
},
|
||||
type: GraphQLObjectTypeNewQuery, // or createCollectionType() for lists
|
||||
resolve: resolveNewQuery,
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Register in Root Schema
|
||||
In `rootSchema.ts`, add your resolver:
|
||||
|
||||
```typescript
|
||||
// Import your resolver
|
||||
import { newQueryRoot } from './newQuery/newQueryResolver'
|
||||
|
||||
// Add to the query fields
|
||||
export const rootGraphQLSchema = new GraphQLSchema({
|
||||
query: new GraphQLObjectType({
|
||||
name: 'RootQueryType',
|
||||
fields: {
|
||||
...introspectRoot,
|
||||
...searchRoot,
|
||||
...errorRoot,
|
||||
...newQueryRoot, // Add this line
|
||||
},
|
||||
}),
|
||||
types: [
|
||||
GraphQLObjectTypeGuide,
|
||||
GraphQLObjectTypeReferenceCLICommand,
|
||||
GraphQLObjectTypeReferenceSDKFunction,
|
||||
GraphQLObjectTypeTroubleshooting,
|
||||
],
|
||||
})
|
||||
```
|
||||
|
||||
### 6. Update TypeScript Types
|
||||
Run the GraphQL codegen to update TypeScript types:
|
||||
```bash
|
||||
pnpm run -F docs codegen:graphql
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Error Handling**: Error handling always uses the Result class, defined in apps/docs/features/helpers.fn.ts
|
||||
2. **Field Optimization**: Use `graphQLFields()` to only fetch requested data
|
||||
3. **Collections**: Use `createCollectionType()` for paginated lists
|
||||
4. **Naming**: Use `GRAPHQL_FIELD_*` constants for field names
|
||||
5. **Documentation**: Add GraphQL descriptions to all fields and types
|
||||
6. **Database**: Use `supabase()` client for database operations with `convertPostgrestToApiError`
|
||||
|
||||
## Testing
|
||||
|
||||
Tests are located in apps/docs/app/api/graphql/tests. Each top-level query
|
||||
should have its own test file, located at <queryName>.test.ts.
|
||||
|
||||
### Test data
|
||||
|
||||
Test data uses a local database, seeded with the file at supabase/seed.sql. Add
|
||||
any data required for running your new query.
|
||||
|
||||
### Integration tests
|
||||
|
||||
Integration tests import the POST function defined in
|
||||
apps/docs/api/graphql/route.ts, then make a request to this function.
|
||||
|
||||
For example:
|
||||
|
||||
```ts
|
||||
import { POST } from '../route'
|
||||
|
||||
it('test name', async () => {
|
||||
const query = `
|
||||
query {
|
||||
...
|
||||
}
|
||||
`
|
||||
const request = new Request('http://localhost/api/graphql', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ query }),
|
||||
})
|
||||
|
||||
const result = await POST(request)
|
||||
})
|
||||
```
|
||||
|
||||
Include at least the following tests:
|
||||
|
||||
1. A test that requests all fields (including nested fields) on the new query
|
||||
object, and asserts that there are no errors, and the requested fields are
|
||||
properly returned.
|
||||
2. A test that triggers and error, and asserts that a GraphQL error is properly
|
||||
returned.
|
||||
@@ -1,71 +0,0 @@
|
||||
---
|
||||
description: Docs Testing Procedure
|
||||
globs: apps/docs/**/*.test.ts
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Docs Test Requirements
|
||||
|
||||
Rules for running tests in the docs application, ensuring proper Supabase setup and test execution.
|
||||
|
||||
<rule>
|
||||
name: docs_test_requirements
|
||||
description: Standards for running tests in the docs application with proper Supabase setup
|
||||
filters:
|
||||
# Match test files in the docs app
|
||||
- type: file_extension
|
||||
pattern: "\\.(test|spec)\\.(ts|tsx)$"
|
||||
- type: path
|
||||
pattern: "^apps/docs/.*"
|
||||
# Match test execution events
|
||||
- type: event
|
||||
pattern: "test_execution"
|
||||
|
||||
actions:
|
||||
- type: suggest
|
||||
message: |
|
||||
Before running tests in the docs app:
|
||||
|
||||
1. Check Supabase status:
|
||||
```bash
|
||||
pnpm supabase status
|
||||
```
|
||||
|
||||
2. If Supabase is not running:
|
||||
```bash
|
||||
pnpm supabase start
|
||||
```
|
||||
|
||||
3. Reset the database to ensure clean state:
|
||||
```bash
|
||||
pnpm supabase db reset --local
|
||||
```
|
||||
|
||||
4. Run the tests:
|
||||
```bash
|
||||
pnpm run -F docs test:local:unwatch
|
||||
```
|
||||
|
||||
Important notes:
|
||||
- Always ensure Supabase is running before tests
|
||||
- Database must be reset to ensure clean state
|
||||
- Use test:local:unwatch to run tests without watch mode
|
||||
- Tests are located in apps/docs/**/*.{test,spec}.{ts,tsx}
|
||||
|
||||
examples:
|
||||
- input: |
|
||||
# Bad: Running tests without proper setup
|
||||
pnpm run -F docs test
|
||||
pnpm run -F docs test:local
|
||||
|
||||
# Good: Proper test execution sequence
|
||||
pnpm supabase status
|
||||
pnpm supabase start # if not running
|
||||
pnpm supabase db reset --local
|
||||
pnpm run -F docs test:local:unwatch
|
||||
output: "Correctly executed docs tests with proper Supabase setup"
|
||||
|
||||
metadata:
|
||||
priority: high
|
||||
version: 1.0
|
||||
</rule>
|
||||
+15
-6
@@ -1,3 +1,10 @@
|
||||
---
|
||||
description: "Docs: embeddings generation pipeline (apps/docs/scripts/search)"
|
||||
globs:
|
||||
- apps/docs/scripts/search/**/*.ts
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Documentation Embeddings Generation System
|
||||
|
||||
## Overview
|
||||
@@ -12,31 +19,34 @@ The documentation embeddings generation system processes various documentation s
|
||||
## Architecture
|
||||
|
||||
### Main Entry Point
|
||||
- `generate-embeddings.ts` - Main script that orchestrates the entire process
|
||||
|
||||
- `apps/docs/scripts/search/generate-embeddings.ts` - Main script that orchestrates the entire process
|
||||
- Supports `--refresh` flag to force regeneration of all content
|
||||
|
||||
### Content Sources (`sources/` directory)
|
||||
|
||||
#### Base Classes
|
||||
|
||||
- `BaseLoader` - Abstract class for loading content from different sources
|
||||
- `BaseSource` - Abstract class for processing and formatting content
|
||||
|
||||
#### Source Types
|
||||
1. **Markdown Sources** (`markdown.ts`)
|
||||
|
||||
1. **Markdown Sources** (`apps/docs/scripts/search/sources/markdown.ts`)
|
||||
- Processes `.mdx` files from guides and documentation
|
||||
- Extracts frontmatter metadata and content sections
|
||||
|
||||
2. **Reference Documentation** (`reference-doc.ts`)
|
||||
2. **Reference Documentation** (`apps/docs/scripts/search/sources/reference-doc.ts`)
|
||||
- **OpenAPI References** - Management API documentation from OpenAPI specs
|
||||
- **Client Library References** - JavaScript, Dart, Python, C#, Swift, Kotlin SDKs
|
||||
- **CLI References** - Command-line interface documentation
|
||||
- Processes YAML/JSON specs and matches with common sections
|
||||
|
||||
3. **GitHub Discussions** (`github-discussion.ts`)
|
||||
3. **GitHub Discussions** (`apps/docs/scripts/search/sources/github-discussion.ts`)
|
||||
- Fetches troubleshooting discussions from GitHub using GraphQL API
|
||||
- Uses GitHub App authentication for access
|
||||
|
||||
4. **Partner Integrations** (`partner-integrations.ts`)
|
||||
4. **Partner Integrations** (`apps/docs/scripts/search/sources/partner-integrations.ts`)
|
||||
- Fetches approved partner integration documentation from Supabase database
|
||||
- Technology integrations only (excludes agencies)
|
||||
|
||||
@@ -56,4 +66,3 @@ The documentation embeddings generation system processes various documentation s
|
||||
|
||||
- **`page`** table: Stores page metadata, content, checksum, version
|
||||
- **`page_section`** table: Stores individual sections with embeddings, token counts
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
---
|
||||
description: "Docs: GraphQL architecture for apps/docs/resources"
|
||||
globs:
|
||||
- apps/docs/resources/**/*.ts
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Docs GraphQL Architecture
|
||||
|
||||
## Overview
|
||||
|
||||
The `apps/docs/resources` folder contains the GraphQL endpoint architecture for the docs GraphQL endpoint at `/api/graphql`. It follows a modular pattern where each top-level query is organized into its own folder with consistent file structure.
|
||||
|
||||
## Architecture Pattern
|
||||
|
||||
Each GraphQL query follows this structure:
|
||||
|
||||
```
|
||||
resources/
|
||||
├── queryObject/
|
||||
│ ├── queryObjectModel.ts # Data models and business logic
|
||||
│ ├── queryObjectSchema.ts # GraphQL type definitions
|
||||
│ ├── queryObjectResolver.ts # Query resolver and arguments
|
||||
│ ├── queryObjectTypes.ts # TypeScript interfaces (optional)
|
||||
│ └── queryObjectSync.ts # Functions for syncing repo content to the database (optional)
|
||||
├── utils/
|
||||
│ ├── connections.ts # GraphQL connection/pagination utilities
|
||||
│ └── fields.ts # GraphQL field selection utilities
|
||||
├── rootSchema.ts # Main GraphQL schema with all queries
|
||||
└── rootSync.ts # Root sync script for syncing to database
|
||||
```
|
||||
|
||||
## Example queries
|
||||
|
||||
1. **searchDocs** (`globalSearch/`) - Vector-based search across all docs content
|
||||
2. **error** (`error/`) - Error code lookup for Supabase services
|
||||
3. **schema** - GraphQL schema introspection
|
||||
|
||||
## Key Files
|
||||
|
||||
### `rootSchema.ts`
|
||||
|
||||
- Main GraphQL schema definition
|
||||
- Imports all resolvers and combines them into the root query
|
||||
- Defines the `RootQueryType` with all top-level fields
|
||||
|
||||
### `utils/connections.ts`
|
||||
|
||||
- Provides `createCollectionType()` for paginated collections
|
||||
- `GraphQLCollectionBuilder` for building collection responses
|
||||
- Standard pagination arguments and edge/node patterns
|
||||
|
||||
### `utils/fields.ts`
|
||||
|
||||
- `graphQLFields()` utility to analyze requested fields in resolvers
|
||||
- Used for optimizing data fetching based on what fields are actually requested
|
||||
|
||||
## Creating a New Top-Level Query
|
||||
|
||||
To add a new GraphQL query, follow these steps:
|
||||
|
||||
### 1. Create Query Folder Structure
|
||||
|
||||
```bash
|
||||
mkdir resources/newQuery
|
||||
touch resources/newQuery/newQueryModel.ts
|
||||
touch resources/newQuery/newQuerySchema.ts
|
||||
touch resources/newQuery/newQueryResolver.ts
|
||||
```
|
||||
|
||||
### 2. Define GraphQL Schema (`newQuerySchema.ts`)
|
||||
|
||||
```typescript
|
||||
import { GraphQLObjectType, GraphQLString } from 'graphql'
|
||||
|
||||
export const GRAPHQL_FIELD_NEW_QUERY = 'newQuery' as const
|
||||
|
||||
export const GraphQLObjectTypeNewQuery = new GraphQLObjectType({
|
||||
name: 'NewQuery',
|
||||
description: 'Description of what this query returns',
|
||||
fields: {
|
||||
id: {
|
||||
type: GraphQLString,
|
||||
description: 'Unique identifier',
|
||||
},
|
||||
// Add other fields...
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
### 3. Create Data Model (`newQueryModel.ts`)
|
||||
|
||||
> [!NOTE]
|
||||
> The data model should be agnostic to GraphQL. It may import argument types
|
||||
> from `~/__generated__/graphql`, but otherwise all functions and classes
|
||||
> should be unaware of whether they are called for GraphQL resolution.
|
||||
|
||||
> [!TIP]
|
||||
> The types in `~/__generated__/graphql` for a new endpoint will not exist
|
||||
> until the code generation is run in the next step.
|
||||
|
||||
```typescript
|
||||
import { type RootQueryTypeNewQueryArgs } from '~/__generated__/graphql'
|
||||
import { convertPostgrestToApiError, type ApiErrorGeneric } from '~/app/api/utils'
|
||||
import { Result } from '~/features/helpers.fn'
|
||||
import { supabase } from '~/lib/supabase'
|
||||
|
||||
export class NewQueryModel {
|
||||
constructor(
|
||||
public readonly data: {
|
||||
id: string
|
||||
// other properties...
|
||||
}
|
||||
) {}
|
||||
|
||||
static async loadData(
|
||||
args: RootQueryTypeNewQueryArgs,
|
||||
requestedFields: Array<string>
|
||||
): Promise<Result<NewQueryModel[], ApiErrorGeneric>> {
|
||||
// Implement data fetching logic
|
||||
const result = new Result(
|
||||
await supabase()
|
||||
.from('your_table')
|
||||
.select('*')
|
||||
// Add filters based on args
|
||||
)
|
||||
.map((data) => data.map((item) => new NewQueryModel(item)))
|
||||
.mapError(convertPostgrestToApiError)
|
||||
return result
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,25 @@
|
||||
---
|
||||
description: "Docs: how to run tests locally (Supabase setup + correct commands)"
|
||||
globs:
|
||||
- apps/docs/**/*.{test,spec}.{ts,tsx}
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Docs test requirements
|
||||
|
||||
Before running tests for `apps/docs`, ensure local Supabase is available and the DB is in a known state.
|
||||
|
||||
## Recommended sequence
|
||||
|
||||
```bash
|
||||
pnpm supabase status
|
||||
pnpm supabase start # if not running
|
||||
pnpm supabase db reset --local
|
||||
pnpm run -F docs test:local:unwatch
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- Always reset the local DB before running docs tests to avoid state leakage.
|
||||
- Prefer `test:local:unwatch` for non-watch CI-like runs.
|
||||
|
||||
@@ -1,262 +0,0 @@
|
||||
---
|
||||
description: How to generate pages and interfaces in Studio, a web interface for managing Supabase projects
|
||||
globs:
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
## Project Structure
|
||||
|
||||
- Next.js app using pages router
|
||||
- Pages go in @apps/studio/pages
|
||||
- Project related pages go in @apps/studio/pages/projects/[ref]
|
||||
- Organization related pages go in @apps/studio/pages/org/[slug]
|
||||
- Studio specific components go in @apps/studio/components
|
||||
- Studio specific generic UI components go in @apps/studio/components/ui
|
||||
- Studio specific components related to individual pages go in @apps/studio/components/interfaces e.g. @apps/studio/components/interfaces/Auth
|
||||
- Generic helper functions go in @apps/studio/lib
|
||||
- Generic hooks go in @apps/studio/hooks
|
||||
|
||||
## Component system
|
||||
|
||||
Our primitive component system is in @packages/ui and is based off shadcn/ui components. These components can be shared across all @apps e.g. studio and docs. Do not introduce new ui components unless asked to.
|
||||
|
||||
- UI components are imported from this package across apps e.g. import { Button, Badge } from 'ui'
|
||||
- Some components have a _Shadcn_ namespace appended to component name e.g. import { Input*Shadcn* } from 'ui'
|
||||
- We should be using _Shadcn_ components where possible
|
||||
- Before composing interfaces, read @packages/ui/index.tsx file for a full list of available components
|
||||
|
||||
## Styling
|
||||
|
||||
We use Tailwind for styling.
|
||||
|
||||
- You should never use tailwind classes for colours and instead use classes we've defined ourselves
|
||||
- Backgrounds // most of the time you will not need to define a background
|
||||
- 'bg' used for main app surface background
|
||||
- 'bg-muted' for elevating content // you can use Card instead
|
||||
- 'bg-warning' for highlighting information that needs to be acted on
|
||||
- 'bg-destructive' for highlighting issues
|
||||
- Text
|
||||
- 'text-foreground' for primary text like headings
|
||||
- 'text-foreground-light' for body text
|
||||
- 'text-foreground-lighter' for subtle text
|
||||
- 'text-warning' for calling out information that needs action
|
||||
- 'text-destructive' for calling out when something went wrong
|
||||
- When needing to apply typography styles, read @apps/studio/styles/typography.scss and use one of the available classes instead of hard coding classes e.g. use "heading-default" instead of "text-sm font-medium"
|
||||
- When applying focus styles for keyboard navigation, read @apps/studio/styles/focus.scss for any appropriate classes for consistency with other focus styles
|
||||
|
||||
## Page structure
|
||||
|
||||
When creating a new page follow these steps:
|
||||
|
||||
- Create the page in @apps/studio/pages
|
||||
- Use the PageLayout component that has the following props
|
||||
|
||||
```jsx
|
||||
export interface NavigationItem {
|
||||
id?: string
|
||||
label: string
|
||||
href?: string
|
||||
icon?: ReactNode
|
||||
onClick?: () => void
|
||||
badge?: string
|
||||
active?: boolean
|
||||
}
|
||||
|
||||
interface PageLayoutProps {
|
||||
children?: ReactNode
|
||||
title?: string | ReactNode
|
||||
subtitle?: string | ReactNode
|
||||
icon?: ReactNode
|
||||
breadcrumbs?: Array<{
|
||||
label?: string
|
||||
href?: string
|
||||
element?: ReactNode
|
||||
}>
|
||||
primaryActions?: ReactNode
|
||||
secondaryActions?: ReactNode
|
||||
navigationItems?: NavigationItem[]
|
||||
className?: string
|
||||
size?: 'default' | 'full' | 'large' | 'small'
|
||||
isCompact?: boolean
|
||||
}
|
||||
```
|
||||
|
||||
- If a page has page related actions, add them to primary and secondary action props e.g. Users page has "Create new user" action
|
||||
- If a page is within an existing section (e.g. Auth), you should use the related layout component e.g. AuthLayout
|
||||
- Create a new component in @apps/studio/components/interfaces for the contents of the page
|
||||
- Use ScaffoldContainer if the page should be center aligned in a container
|
||||
- Use ScaffoldSection, ScaffoldSectionTitle, ScaffoldSectionDescription if the page has multiple sections
|
||||
|
||||
### Page example
|
||||
|
||||
```jsx
|
||||
import { MyPageComponent } from 'components/interfaces/MyPage/MyPageComponent'
|
||||
import AuthLayout from './AuthLayout'
|
||||
import DefaultLayout from 'components/layouts/DefaultLayout'
|
||||
import { ScaffoldContainer } from 'components/layouts/Scaffold'
|
||||
import type { NextPageWithLayout } from 'types'
|
||||
|
||||
const MyPage: NextPageWithLayout = () => {
|
||||
return (
|
||||
<ScaffoldContainer>
|
||||
<MyPageComponent />
|
||||
</ScaffoldContainer>
|
||||
)
|
||||
}
|
||||
|
||||
MyPage.getLayout = (page) => (
|
||||
<DefaultLayout>
|
||||
<AuthLayout>{page}</AuthLayout>
|
||||
</DefaultLayout>
|
||||
)
|
||||
|
||||
export default MyPage
|
||||
|
||||
export const MyPageComponent = () => (
|
||||
<ScaffoldSection isFullWidth>
|
||||
<div>
|
||||
<ScaffoldSectionTitle>My page section</ScaffoldSectionTitle>
|
||||
<ScaffoldSectionDescription>A brief description of the purpose of the page</ScaffoldSectionDescription>
|
||||
</div>
|
||||
// Content goes here
|
||||
</ScaffoldSection>
|
||||
)
|
||||
```
|
||||
|
||||
## Forms
|
||||
|
||||
- Build forms with `react-hook-form` + `zod`.
|
||||
- Use our `_Shadcn_` form primitives from `ui` and prefer `FormItemLayout` with layout="flex-row-reverse" for most controls (see `apps/studio/components/interfaces/Settings/Integrations/GithubIntegration/GitHubIntegrationConnectionForm.tsx`).
|
||||
- Keep imports from `ui` with `_Shadcn_` suffixes.
|
||||
- Forms should generally be wrapped in a Card unless specified
|
||||
- If the submit button is outside the form, add a new variable named formId outside the component, and set it as property id on the form element and formId on the button.
|
||||
|
||||
### Example (single field)
|
||||
|
||||
```tsx
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import * as z from 'zod'
|
||||
|
||||
import { Button, Form_Shadcn_, FormField_Shadcn_, FormControl_Shadcn_, Input_Shadcn_ } from 'ui'
|
||||
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
|
||||
|
||||
const profileSchema = z.object({
|
||||
username: z.string().min(2, 'Username must be at least 2 characters'),
|
||||
})
|
||||
|
||||
const formId = `profile-form`
|
||||
|
||||
export function ProfileForm() {
|
||||
const form = useForm<z.infer<typeof profileSchema>>({
|
||||
resolver: zodResolver(profileSchema),
|
||||
defaultValues: { username: '' },
|
||||
mode: 'onSubmit',
|
||||
reValidateMode: 'onBlur',
|
||||
})
|
||||
|
||||
function onSubmit(values: z.infer<typeof profileSchema>) {
|
||||
// handle values
|
||||
}
|
||||
|
||||
return (
|
||||
<Form_Shadcn_ {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||
<Card>
|
||||
<CardContent className="space-y-6">
|
||||
<FormField_Shadcn_
|
||||
control={form.control}
|
||||
name="username"
|
||||
render={({ field }) => (
|
||||
<FormItemLayout
|
||||
layout="flex-row-reverse"
|
||||
label="Username"
|
||||
description="This is your public display name."
|
||||
>
|
||||
<FormControl_Shadcn_>
|
||||
<Input_Shadcn_ placeholder="shadcn" autoComplete="off" {...field} />
|
||||
</FormControl_Shadcn_>
|
||||
</FormItemLayout>
|
||||
)}
|
||||
/>
|
||||
</CardContent>
|
||||
<CardFooter className="justify-end">
|
||||
<Button type="primary" htmlType="submit">
|
||||
Submit
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</form>
|
||||
</Form_Shadcn_>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Cards
|
||||
|
||||
- Use cards when needing to group related pieces of information
|
||||
- Cards can have sections with CardContent
|
||||
- Use CardFooter for actions
|
||||
- Only use CardHeader and CardTitle if the card content has not been described by the surrounding content e.g. Page title or ScaffoldSectionTitle
|
||||
- Use CardHeader and CardTitle when you are using multiple Cards to group related pieces of content e.g. Primary branch, Persistent branches, Preview branches
|
||||
|
||||
## Sheets
|
||||
|
||||
- Use a sheet when needing to reveal more complicated forms or information relating to an object and context switching away to a new page would be disruptive e.g. we list auth providers, clicking an auth provider opens a sheet with information about that provider and a form to enable, user can close sheet to go back to providers list
|
||||
|
||||
## React Query
|
||||
|
||||
- When doing a mutation, always use the mutate function. Always use onSuccess and onError with a toast.success and toast.error.
|
||||
- Use mutateAsync only if the mutation is part of multiple async actions. Wrap the mutateAsync call with try/catch block and add toast.success and toast.error.
|
||||
|
||||
## Tables
|
||||
|
||||
- Use the generic ui table components for most tables
|
||||
- Tables are generally contained witin a card
|
||||
- If a table has associated actions, they should go above on right hand side
|
||||
- If a table has associated search or filters, they should go above on left hand side
|
||||
- If a table is the main content of a page, and it does not have search or filters, you can add table actions to primary and secondary actions of PageLayout
|
||||
- If a table is the main content of a page section, and it does not have search or filters, you can add table actions to the right of ScaffoldSectionTitle
|
||||
- For simple lists of objects you can use ResourceList with ResourceListItem instead
|
||||
|
||||
### Table example
|
||||
|
||||
```jsx
|
||||
import { Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow } from 'ui'
|
||||
|
||||
;<Table>
|
||||
<TableCaption>A list of your recent invoices.</TableCaption>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[100px]">Invoice</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead>Method</TableHead>
|
||||
<TableHead className="text-right">Amount</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell className="font-medium">INV001</TableCell>
|
||||
<TableCell>Paid</TableCell>
|
||||
<TableCell>Credit Card</TableCell>
|
||||
<TableCell className="text-right">$250.00</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
```
|
||||
|
||||
## Alerts
|
||||
|
||||
- Use Admonition component to alert users of important actions or restrictions in place
|
||||
- Place the Admonition either at the top of the contents of the page (below page title) or at the top of the related ScaffoldSection , below ScaffoldTitle
|
||||
- Use sparingly
|
||||
|
||||
### Alert example
|
||||
|
||||
```jsx
|
||||
<Admonition
|
||||
type="note"
|
||||
title="No authentication logs available for this user"
|
||||
description="Auth events such as logging in will be shown here"
|
||||
/>
|
||||
```
|
||||
@@ -0,0 +1,161 @@
|
||||
---
|
||||
description: Guidelines for using the useStaticEffectEvent hook in Studio - a polyfill for React's useEffectEvent pattern
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# useStaticEffectEvent Hook
|
||||
|
||||
The `useStaticEffectEvent` hook (located at `apps/studio/hooks/useStaticEffectEvent.ts`) is a userland implementation of React's `useEffectEvent` pattern. It solves the stale closure problem by providing a stable callback reference that always accesses the latest props and state values.
|
||||
|
||||
## What Problem Does It Solve?
|
||||
|
||||
When using `useEffect`, you often need to access props or state inside your Effect, but you don't want changes to those values to re-run the Effect. Without `useStaticEffectEvent`, you'd face two bad options:
|
||||
|
||||
1. **Add them to dependencies** - causes unnecessary Effect re-runs (teardown/reconnect cycles)
|
||||
2. **Omit from dependencies** - causes stale closure bugs where your callback uses outdated values
|
||||
|
||||
```tsx
|
||||
// Problem: This Effect re-runs every time `theme` changes, even though
|
||||
// we only want to reconnect when `roomId` changes
|
||||
useEffect(() => {
|
||||
const connection = createConnection(roomId)
|
||||
connection.on('connected', () => {
|
||||
showNotification('Connected!', theme) // `theme` causes unwanted re-runs
|
||||
})
|
||||
return () => connection.disconnect()
|
||||
}, [roomId, theme]) // Adding theme causes unnecessary reconnections
|
||||
```
|
||||
|
||||
## When to Use useStaticEffectEvent
|
||||
|
||||
Use `useStaticEffectEvent` when you need to:
|
||||
|
||||
1. **Read latest state/props inside an Effect without re-triggering it**
|
||||
2. **Create stable callbacks that always use current values**
|
||||
3. **Avoid stale closure bugs in event handlers used within Effects**
|
||||
|
||||
### Pattern 1: Syncing data without re-running on every change
|
||||
|
||||
```tsx
|
||||
// ✅ Good - sync data when status changes, but always read latest state
|
||||
const syncApiPrivileges = useStaticEffectEvent(() => {
|
||||
if (hasLoadedInitialData.current) return
|
||||
if (!apiAccessStatus.isSuccess) return
|
||||
if (!privilegesForTable) return
|
||||
|
||||
hasLoadedInitialData.current = true
|
||||
setPrivileges(privilegesForTable.privileges)
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
syncApiPrivileges()
|
||||
}, [apiAccessStatus.status, syncApiPrivileges])
|
||||
```
|
||||
|
||||
### Pattern 2: Stable callbacks for async operations
|
||||
|
||||
```tsx
|
||||
// ✅ Good - wrap complex async logic that reads many values
|
||||
const exportInternal = useStaticEffectEvent(
|
||||
async ({ bypassConfirmation }: { bypassConfirmation: boolean }): Promise<void> => {
|
||||
if (!params.enabled) return
|
||||
const { projectRef, connectionString, entity, totalRows } = params
|
||||
// ... complex async logic using latest params
|
||||
}
|
||||
)
|
||||
|
||||
// This callback is stable and can be safely used in useCallback
|
||||
const exportInDesiredFormat = useCallback(
|
||||
() => exportInternal({ bypassConfirmation: false }),
|
||||
[exportInternal]
|
||||
)
|
||||
```
|
||||
|
||||
### Pattern 3: Infinite scroll / pagination triggers
|
||||
|
||||
```tsx
|
||||
// ✅ Good - always read latest pagination state when scrolling triggers fetch
|
||||
const fetchNext = useStaticEffectEvent(() => {
|
||||
if (lastItem && lastItem.index >= items.length - 1 && hasNextPage && !isFetchingNextPage) {
|
||||
fetchNextPage()
|
||||
}
|
||||
})
|
||||
|
||||
useEffect(fetchNext, [lastItem, fetchNext])
|
||||
```
|
||||
|
||||
## When NOT to Use useStaticEffectEvent
|
||||
|
||||
### Don't use it to avoid specifying legitimate dependencies
|
||||
|
||||
```tsx
|
||||
// ❌ Bad - hiding the fact that this should re-run when roomId changes
|
||||
const connect = useStaticEffectEvent(() => {
|
||||
const connection = createConnection(roomId)
|
||||
connection.connect()
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
connect() // BUG: Won't reconnect when roomId changes!
|
||||
}, [connect])
|
||||
|
||||
// ✅ Good - roomId is a legitimate dependency
|
||||
useEffect(() => {
|
||||
const connection = createConnection(roomId)
|
||||
connection.connect()
|
||||
return () => connection.disconnect()
|
||||
}, [roomId])
|
||||
```
|
||||
|
||||
### Don't use it for simple event handlers outside Effects
|
||||
|
||||
```tsx
|
||||
// ❌ Unnecessary - not used inside an Effect
|
||||
const handleClick = useStaticEffectEvent(() => {
|
||||
console.log(count)
|
||||
})
|
||||
|
||||
// ✅ Good - regular function or useCallback is fine
|
||||
const handleClick = () => {
|
||||
console.log(count)
|
||||
}
|
||||
```
|
||||
|
||||
## How It Works
|
||||
|
||||
The hook uses a ref to store the latest callback and returns a stable wrapper function:
|
||||
|
||||
```tsx
|
||||
export const useStaticEffectEvent = <Callback extends Function>(callback: Callback) => {
|
||||
const callbackRef = useRef(callback)
|
||||
|
||||
// Update the ref on every render with the latest callback
|
||||
useLayoutEffect(() => {
|
||||
callbackRef.current = callback
|
||||
})
|
||||
|
||||
// Return a stable function that calls the latest callback
|
||||
const eventFn = useCallback((...args: any) => {
|
||||
return callbackRef.current(...args)
|
||||
}, [])
|
||||
|
||||
return eventFn as unknown as Callback
|
||||
}
|
||||
```
|
||||
|
||||
## Relationship to React's useEffectEvent
|
||||
|
||||
This hook is a polyfill for React's experimental `useEffectEvent` (now stable in React 19.2). The core concept is identical:
|
||||
|
||||
- Extract non-reactive logic into a stable function
|
||||
- Always access the latest props/state without adding them as Effect dependencies
|
||||
- Should only be called from within Effects
|
||||
|
||||
When React's `useEffectEvent` becomes widely available, this hook can be replaced with the official API.
|
||||
|
||||
## Rules
|
||||
|
||||
1. **Only call the returned function inside Effects** (useEffect, useLayoutEffect)
|
||||
2. **Don't pass the function to other components or hooks** as a callback prop
|
||||
3. **Use for non-reactive logic only** - logic that reads values but shouldn't trigger re-runs
|
||||
4. **Include it in dependency arrays** when used in useEffect (the function is stable, so it won't cause re-runs)
|
||||
@@ -0,0 +1,33 @@
|
||||
---
|
||||
description: 'Studio: index rule for architecture, style, and UI composition patterns'
|
||||
globs:
|
||||
- apps/studio/**/*.{ts,tsx}
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Studio
|
||||
|
||||
Use the nested rules in this folder for focused guidance while working in `apps/studio/`.
|
||||
|
||||
## Architecture and style
|
||||
|
||||
- `studio/project-structure`
|
||||
- `studio/component-system`
|
||||
- `studio/styling`
|
||||
- `studio/best-practices`
|
||||
|
||||
## UI composition (Design System patterns)
|
||||
|
||||
- `studio/layout`
|
||||
- `studio/forms`
|
||||
- `studio/tables`
|
||||
- `studio/charts`
|
||||
- `studio/empty-states`
|
||||
- `studio/navigation`
|
||||
|
||||
## Common UI building blocks
|
||||
|
||||
- `studio/sheets`
|
||||
- `studio/cards`
|
||||
- `studio/alerts`
|
||||
- `studio/react-query`
|
||||
@@ -0,0 +1,13 @@
|
||||
---
|
||||
description: "Studio: alert/admonition usage and placement"
|
||||
globs:
|
||||
- apps/studio/**/*.{ts,tsx}
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Studio alerts
|
||||
|
||||
- Use `Admonition` to call out important actions, restrictions, or critical context.
|
||||
- Place at the top of a page’s content (below the page title) or at the top of the relevant section (below the section title).
|
||||
- Use sparingly.
|
||||
|
||||
@@ -0,0 +1,433 @@
|
||||
---
|
||||
description: "Studio: React and TypeScript best practices for maintainable Studio code"
|
||||
globs:
|
||||
- apps/studio/**/*.{ts,tsx}
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Studio Best Practices
|
||||
|
||||
## Boolean Handling
|
||||
|
||||
### Assign complex conditions to descriptive variables
|
||||
|
||||
When you have multiple conditions in a single expression, extract them into well-named boolean variables. This improves readability and makes the code self-documenting.
|
||||
|
||||
```tsx
|
||||
// ❌ Bad - complex inline condition
|
||||
{
|
||||
!isSchemaLocked && isTableLike(selectedTable) && canUpdateColumns && !isLoading && (
|
||||
<Button onClick={onAddColumn}>New column</Button>
|
||||
)
|
||||
}
|
||||
|
||||
// ✅ Good - extract to descriptive variables
|
||||
const isTableEntity = isTableLike(selectedTable)
|
||||
const canShowAddButton = !isSchemaLocked && isTableEntity && canUpdateColumns && !isLoading
|
||||
|
||||
{
|
||||
canShowAddButton && <Button onClick={onAddColumn}>New column</Button>
|
||||
}
|
||||
```
|
||||
|
||||
### Use consistent naming conventions for booleans
|
||||
|
||||
- Use `is` prefix for state/identity: `isLoading`, `isPaused`, `isNewRecord`, `isError`
|
||||
- Use `has` prefix for possession: `hasPermission`, `hasShownModal`, `hasData`
|
||||
- Use `can` prefix for capability/permission: `canUpdateColumns`, `canDelete`, `canEdit`
|
||||
- Use `should` prefix for conditional behavior: `shouldFetch`, `shouldRender`, `shouldValidate`
|
||||
|
||||
```tsx
|
||||
// ✅ Good examples from codebase
|
||||
const isNewRecord = column === undefined
|
||||
const isPaused = project?.status === PROJECT_STATUS.INACTIVE
|
||||
const isMatureProject = dayjs(project?.inserted_at).isBefore(dayjs().subtract(10, 'day'))
|
||||
const { can: canUpdateColumns } = useAsyncCheckPermissions(
|
||||
PermissionAction.TENANT_SQL_ADMIN_WRITE,
|
||||
'columns'
|
||||
)
|
||||
```
|
||||
|
||||
### Derive boolean state instead of storing it
|
||||
|
||||
When a boolean can be computed from existing state, derive it rather than storing it separately.
|
||||
|
||||
```tsx
|
||||
// ❌ Bad - storing derived state
|
||||
const [isFormValid, setIsFormValid] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
setIsFormValid(name.length > 0 && email.includes('@'))
|
||||
}, [name, email])
|
||||
|
||||
// ✅ Good - derive from existing state
|
||||
const isFormValid = name.length > 0 && email.includes('@')
|
||||
```
|
||||
|
||||
## Component Structure
|
||||
|
||||
### Break down large components
|
||||
|
||||
Components should ideally be under 200-300 lines. If a component grows larger, consider splitting it.
|
||||
|
||||
**Signs a component should be split:**
|
||||
|
||||
- Multiple distinct UI sections
|
||||
- Complex conditional rendering logic
|
||||
- Multiple useState hooks for unrelated state
|
||||
- Difficult to understand at a glance
|
||||
|
||||
```tsx
|
||||
// ❌ Bad - monolithic component with everything inline
|
||||
const UserDashboard = () => {
|
||||
// 50 lines of hooks and state
|
||||
// 100 lines of handlers
|
||||
// 300 lines of JSX with nested conditions
|
||||
}
|
||||
|
||||
// ✅ Good - split into focused sub-components
|
||||
const UserDashboard = () => {
|
||||
return (
|
||||
<div>
|
||||
<UserHeader />
|
||||
<UserStats />
|
||||
<UserActivitySection />
|
||||
<UserSettingsPanel />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Co-locate related components
|
||||
|
||||
Place sub-components in the same directory as the parent component. Use an index file for cleaner imports.
|
||||
|
||||
```
|
||||
components/interfaces/Auth/Users/
|
||||
├── UserPanel.tsx
|
||||
├── UserOverview.tsx
|
||||
├── UserLogs.tsx
|
||||
├── Users.constants.ts
|
||||
└── index.ts
|
||||
```
|
||||
|
||||
### Extract repeated JSX patterns
|
||||
|
||||
If you find yourself copying similar JSX blocks, extract them into a component.
|
||||
|
||||
```tsx
|
||||
// ❌ Bad - repeated pattern
|
||||
<TabsTrigger_Shadcn_ value="overview" className="px-0 pb-0 h-full text-xs data-[state=active]:bg-transparent !shadow-none">
|
||||
Overview
|
||||
</TabsTrigger_Shadcn_>
|
||||
<TabsTrigger_Shadcn_ value="logs" className="px-0 pb-0 h-full text-xs data-[state=active]:bg-transparent !shadow-none">
|
||||
Logs
|
||||
</TabsTrigger_Shadcn_>
|
||||
|
||||
// ✅ Good - extract to component
|
||||
const PanelTab = ({ value, children }: { value: string; children: ReactNode }) => (
|
||||
<TabsTrigger_Shadcn_
|
||||
value={value}
|
||||
className="px-0 pb-0 h-full text-xs data-[state=active]:bg-transparent !shadow-none"
|
||||
>
|
||||
{children}
|
||||
</TabsTrigger_Shadcn_>
|
||||
)
|
||||
```
|
||||
|
||||
## Loading and Error States
|
||||
|
||||
### Use consistent loading/error/success pattern
|
||||
|
||||
Follow a consistent pattern for handling async states:
|
||||
|
||||
```tsx
|
||||
const { data, error, isLoading, isError, isSuccess } = useQuery()
|
||||
|
||||
// Handle loading state first
|
||||
if (isLoading) {
|
||||
return <GenericSkeletonLoader />
|
||||
}
|
||||
|
||||
// Handle error state
|
||||
if (isError) {
|
||||
return <AlertError error={error} subject="Failed to load data" />
|
||||
}
|
||||
|
||||
// Handle empty state if needed
|
||||
if (isSuccess && data.length === 0) {
|
||||
return <EmptyState />
|
||||
}
|
||||
|
||||
// Render success state
|
||||
return <DataDisplay data={data} />
|
||||
```
|
||||
|
||||
### Use early returns for guard clauses
|
||||
|
||||
Prefer early returns over deeply nested conditionals:
|
||||
|
||||
```tsx
|
||||
// ❌ Bad - deeply nested
|
||||
const Component = () => {
|
||||
if (data) {
|
||||
if (!isError) {
|
||||
if (hasPermission) {
|
||||
return <ActualContent />
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// ✅ Good - early returns
|
||||
const Component = () => {
|
||||
if (!data) return null
|
||||
if (isError) return <ErrorDisplay />
|
||||
if (!hasPermission) return <PermissionDenied />
|
||||
|
||||
return <ActualContent />
|
||||
}
|
||||
```
|
||||
|
||||
## State Management
|
||||
|
||||
### Keep state as local as possible
|
||||
|
||||
Start with local state and lift up only when needed.
|
||||
|
||||
```tsx
|
||||
// ✅ Good - state lives where it's used
|
||||
const SearchableList = () => {
|
||||
const [filterString, setFilterString] = useState('')
|
||||
|
||||
const filteredItems = items.filter((item) => item.name.includes(filterString))
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Input value={filterString} onChange={(e) => setFilterString(e.target.value)} />
|
||||
<List items={filteredItems} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Group related state with objects or reducers
|
||||
|
||||
When you have multiple related pieces of state, consider grouping them:
|
||||
|
||||
```tsx
|
||||
// ❌ Bad - multiple related useState calls
|
||||
const [name, setName] = useState('')
|
||||
const [email, setEmail] = useState('')
|
||||
const [phone, setPhone] = useState('')
|
||||
|
||||
// ✅ Good - grouped state for forms (use react-hook-form)
|
||||
const form = useForm<FormValues>({
|
||||
defaultValues: { name: '', email: '', phone: '' },
|
||||
})
|
||||
```
|
||||
|
||||
## Custom Hooks
|
||||
|
||||
### Extract complex logic into custom hooks
|
||||
|
||||
When logic becomes reusable or complex, extract it:
|
||||
|
||||
```tsx
|
||||
// ✅ Good - extracted to custom hook
|
||||
export function useAsyncCheckPermissions(action: string, resource: string) {
|
||||
const { permissions, isLoading, isSuccess } = useGetProjectPermissions()
|
||||
|
||||
const can = useMemo(() => {
|
||||
if (!IS_PLATFORM) return true
|
||||
if (!isSuccess || !permissions) return false
|
||||
return doPermissionsCheck(permissions, action, resource)
|
||||
}, [isSuccess, permissions, action, resource])
|
||||
|
||||
return { isLoading, isSuccess, can }
|
||||
}
|
||||
|
||||
// Usage
|
||||
const { can: canUpdateColumns } = useAsyncCheckPermissions(
|
||||
PermissionAction.TENANT_SQL_ADMIN_WRITE,
|
||||
'columns'
|
||||
)
|
||||
```
|
||||
|
||||
### Return objects from hooks for better extensibility
|
||||
|
||||
```tsx
|
||||
// ❌ Bad - returning array (hard to extend)
|
||||
const useToggle = () => {
|
||||
const [value, setValue] = useState(false)
|
||||
return [value, () => setValue((v) => !v)]
|
||||
}
|
||||
|
||||
// ✅ Good - returning object (easy to extend)
|
||||
const useToggle = (initial = false) => {
|
||||
const [value, setValue] = useState(initial)
|
||||
return {
|
||||
value,
|
||||
toggle: () => setValue((v) => !v),
|
||||
setTrue: () => setValue(true),
|
||||
setFalse: () => setValue(false),
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Event Handlers
|
||||
|
||||
### Name handlers consistently
|
||||
|
||||
Use `on` prefix for prop callbacks and `handle` prefix for internal handlers:
|
||||
|
||||
```tsx
|
||||
interface Props {
|
||||
onClose: () => void // Callback prop
|
||||
onSave: (data: Data) => void
|
||||
}
|
||||
|
||||
const Component = ({ onClose, onSave }: Props) => {
|
||||
const handleSubmit = () => {
|
||||
// Internal handler
|
||||
// process data
|
||||
onSave(data)
|
||||
}
|
||||
|
||||
const handleCancel = () => {
|
||||
// cleanup
|
||||
onClose()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Avoid inline arrow functions for expensive operations
|
||||
|
||||
```tsx
|
||||
// ❌ Bad - creates new function every render
|
||||
<ExpensiveList items={items} onItemClick={(item) => handleItemClick(item)} />
|
||||
|
||||
// ✅ Good - stable reference with useCallback
|
||||
const handleItemClick = useCallback(
|
||||
(item: Item) => {
|
||||
// handle click
|
||||
},
|
||||
[dependencies]
|
||||
)
|
||||
|
||||
<ExpensiveList items={items} onItemClick={handleItemClick} />
|
||||
```
|
||||
|
||||
## Conditional Rendering
|
||||
|
||||
### Use appropriate patterns for different scenarios
|
||||
|
||||
```tsx
|
||||
// Simple show/hide - use &&
|
||||
{
|
||||
isVisible && <Component />
|
||||
}
|
||||
|
||||
// Binary choice - use ternary
|
||||
{
|
||||
isLoading ? <Spinner /> : <Content />
|
||||
}
|
||||
|
||||
// Multiple conditions - use early returns or extracted component
|
||||
const StatusDisplay = ({ status }: { status: Status }) => {
|
||||
if (status === 'loading') return <Spinner />
|
||||
if (status === 'error') return <ErrorMessage />
|
||||
if (status === 'empty') return <EmptyState />
|
||||
return <DataDisplay />
|
||||
}
|
||||
```
|
||||
|
||||
### Avoid nested ternaries
|
||||
|
||||
```tsx
|
||||
// ❌ Bad - nested ternary
|
||||
{
|
||||
isLoading ? <Spinner /> : isError ? <Error /> : <Content />
|
||||
}
|
||||
|
||||
// ✅ Good - separate conditions or early returns
|
||||
if (isLoading) return <Spinner />
|
||||
if (isError) return <Error />
|
||||
return <Content />
|
||||
```
|
||||
|
||||
## Performance
|
||||
|
||||
### Use useMemo for expensive computations
|
||||
|
||||
```tsx
|
||||
// ✅ Good - memoize expensive filtering
|
||||
const filteredItems = useMemo(
|
||||
() => items.filter((item) => item.name.toLowerCase().includes(searchQuery.toLowerCase())),
|
||||
[items, searchQuery]
|
||||
)
|
||||
```
|
||||
|
||||
### Avoid premature optimization
|
||||
|
||||
Don't wrap everything in useMemo/useCallback. Only optimize when:
|
||||
|
||||
- You have measured a performance problem
|
||||
- The computation is genuinely expensive
|
||||
- The value is passed to memoized children
|
||||
|
||||
## TypeScript
|
||||
|
||||
### Define prop interfaces explicitly
|
||||
|
||||
```tsx
|
||||
interface UserCardProps {
|
||||
user: User
|
||||
onEdit: (user: User) => void
|
||||
onDelete: (userId: string) => void
|
||||
isEditable?: boolean
|
||||
}
|
||||
|
||||
export const UserCard = ({ user, onEdit, onDelete, isEditable = true }: UserCardProps) => {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### Use discriminated unions for complex state
|
||||
|
||||
```tsx
|
||||
type AsyncState<T> =
|
||||
| { status: 'idle' }
|
||||
| { status: 'loading' }
|
||||
| { status: 'success'; data: T }
|
||||
| { status: 'error'; error: Error }
|
||||
```
|
||||
|
||||
### Avoid type casting, prefer validation with zod
|
||||
|
||||
Never use type casting (e.g., `as any`, `as Type`). Instead, validate values at runtime using zod schemas. This ensures type safety and catches runtime errors.
|
||||
|
||||
```tsx
|
||||
// ❌ Bad - type casting bypasses type checking
|
||||
const user = apiResponse as User
|
||||
const data = unknownValue as string
|
||||
|
||||
// ✅ Good - validate with zod schema
|
||||
const userSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
email: z.string().email(),
|
||||
})
|
||||
|
||||
const user = userSchema.parse(apiResponse)
|
||||
const data = z.string().parse(unknownValue)
|
||||
|
||||
// ✅ Good - safe parsing with error handling
|
||||
const result = userSchema.safeParse(apiResponse)
|
||||
if (result.success) {
|
||||
const user = result.data
|
||||
} else {
|
||||
// handle validation errors
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,14 @@
|
||||
---
|
||||
description: "Studio: Card usage for grouping related content and actions"
|
||||
globs:
|
||||
- apps/studio/**/*.{ts,tsx}
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Studio cards
|
||||
|
||||
- Use cards to group related pieces of information.
|
||||
- Use `CardContent` for sections and `CardFooter` for actions.
|
||||
- Only use `CardHeader`/`CardTitle` when the card content is not already described by surrounding content (page title, section title, etc).
|
||||
- Prefer headers/titles when multiple cards represent distinct groups (e.g. multiple settings groups).
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
---
|
||||
description: "Studio: composable chart patterns built on Recharts and our chart presentational components"
|
||||
globs:
|
||||
- apps/studio/**/*.{ts,tsx}
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Studio charts
|
||||
|
||||
Use the Design System UI pattern docs as the source of truth:
|
||||
|
||||
- Documentation: `apps/design-system/content/docs/ui-patterns/charts.mdx`
|
||||
- Demos:
|
||||
- `apps/design-system/__registry__/default/block/chart-composed-demo.tsx`
|
||||
- `apps/design-system/__registry__/default/block/chart-composed-basic.tsx`
|
||||
- `apps/design-system/__registry__/default/block/chart-composed-states.tsx`
|
||||
- `apps/design-system/__registry__/default/block/chart-composed-metrics.tsx`
|
||||
- `apps/design-system/__registry__/default/block/chart-composed-actions.tsx`
|
||||
- `apps/design-system/__registry__/default/block/chart-composed-table.tsx`
|
||||
|
||||
## Best practices
|
||||
|
||||
- Prefer provided chart building blocks over passing raw Recharts components to `ChartContent`.
|
||||
- Use `useChart` context flags for consistent loading/disabled handling.
|
||||
- Keep chart composition straightforward; avoid over-abstraction.
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
---
|
||||
description: 'Studio: UI component system (packages/ui + shadcn primitives)'
|
||||
globs:
|
||||
- apps/studio/**/*.{ts,tsx}
|
||||
- packages/ui/**/*.{ts,tsx}
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Studio component system
|
||||
|
||||
Our primitive component system lives in `packages/ui` and is based on shadcn/ui patterns.
|
||||
|
||||
- Prefer using components exported from `ui` (e.g. `import { Button } from 'ui'`).
|
||||
- Prefer `_Shadcn_`-suffixed components for form components e.g. `Input_Shadcn_`.
|
||||
- Avoid introducing new primitives unless explicitly requested.
|
||||
- Browse available exports in `packages/ui/index.tsx` before composing new UI.
|
||||
@@ -0,0 +1,25 @@
|
||||
---
|
||||
description: 'Studio: empty state patterns (presentational vs informational vs zero-results vs missing route)'
|
||||
globs:
|
||||
- apps/studio/**/*.{ts,tsx}
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Studio empty states
|
||||
|
||||
Use the Design System UI pattern docs as the source of truth:
|
||||
|
||||
- Documentation: `apps/design-system/content/docs/ui-patterns/empty-states.mdx`
|
||||
- Demos:
|
||||
- `apps/design-system/registry/default/example/empty-state-presentational-icon.tsx`
|
||||
- `apps/design-system/registry/default/example/empty-state-initial-state-informational.tsx`
|
||||
- `apps/design-system/registry/default/example/empty-state-zero-items-table.tsx`
|
||||
- `apps/design-system/registry/default/example/data-grid-empty-state.tsx`
|
||||
- `apps/design-system/registry/default/example/empty-state-missing-route.tsx`
|
||||
|
||||
## Quick guidance
|
||||
|
||||
- Initial states: use presentational empty states when onboarding/value prop + a clear next action helps.
|
||||
- Data-heavy lists: prefer informational empty states that match the list/table layout.
|
||||
- Zero results: keep the UI consistent with the data state to avoid jarring transitions.
|
||||
- Missing routes: prefer a centered `Admonition` pattern.
|
||||
@@ -0,0 +1,35 @@
|
||||
---
|
||||
description: "Studio: form patterns (page layouts + side panels) and react-hook-form conventions"
|
||||
globs:
|
||||
- apps/studio/**/*.{ts,tsx}
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Studio forms
|
||||
|
||||
Use the Design System UI pattern docs as the source of truth:
|
||||
|
||||
- Documentation: `apps/design-system/content/docs/ui-patterns/forms.mdx`
|
||||
- Demos:
|
||||
- `apps/design-system/registry/default/example/form-patterns-pagelayout.tsx`
|
||||
- `apps/design-system/registry/default/example/form-patterns-sidepanel.tsx`
|
||||
|
||||
## Requirements
|
||||
|
||||
- Build forms with `react-hook-form` + `zod`.
|
||||
- Use `FormItemLayout` instead of manually composing `FormItem`/`FormLabel`/`FormMessage`/`FormDescription`.
|
||||
- Wrap inputs with `FormControl_Shadcn_`.
|
||||
- Use `_Shadcn_` imports from `ui` for form primitives where available.
|
||||
|
||||
## Layout selection
|
||||
|
||||
- Page layouts: `FormItemLayout layout="flex-row-reverse"` inside `Card` (`CardContent` per field; `CardFooter` for actions).
|
||||
- Side panels (wide): `FormItemLayout layout="horizontal"` inside `SheetSection`.
|
||||
- Side panels (narrow, `size="sm"` or below): `FormItemLayout layout="vertical"`.
|
||||
|
||||
## Actions and state
|
||||
|
||||
- Handle dirty state (`form.formState.isDirty`) to show Cancel and to disable Save.
|
||||
- Show loading on submit buttons via `loading`.
|
||||
- When submit button is outside the `<form>`, set a stable `formId` and use the button’s `form` prop.
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
---
|
||||
description: 'Studio: page layout patterns (PageContainer/PageHeader/PageSection) and sizing guidance. Use to learn how to create or update existing pages in Studio.'
|
||||
globs:
|
||||
- apps/studio/**/*.{ts,tsx}
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Studio layout
|
||||
|
||||
Use the Design System UI pattern docs as the source of truth:
|
||||
|
||||
- Documentation: `apps/design-system/content/docs/ui-patterns/layout.mdx`
|
||||
- Demos:
|
||||
- `apps/design-system/registry/default/example/page-layout-settings.tsx`
|
||||
- `apps/design-system/registry/default/example/page-layout-list.tsx`
|
||||
- `apps/design-system/registry/default/example/page-layout-list-simple.tsx`
|
||||
- `apps/design-system/registry/default/example/page-layout-detail.tsx`
|
||||
|
||||
## Guidelines
|
||||
|
||||
- Build pages using `PageContainer`, `PageHeader`, and `PageSection` for consistent spacing and max-widths.
|
||||
- Choose `size` based on content:
|
||||
- Settings/config: `size="default"`
|
||||
- List/table-heavy: `size="large"`
|
||||
- Full-screen experiences: `size="full"`
|
||||
- For list pages:
|
||||
- If filters/search exist, align table actions with filters (avoid `PageHeaderAside`/`PageSectionAside` for those actions).
|
||||
- If no filters/search, actions can go in `PageHeaderAside` or `PageSectionAside` depending on context.
|
||||
@@ -0,0 +1,19 @@
|
||||
---
|
||||
description: "Studio: navigation patterns (page-level NavMenu + URL-driven navigation)"
|
||||
globs:
|
||||
- apps/studio/**/*.{ts,tsx}
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Studio navigation
|
||||
|
||||
Use the Design System UI pattern docs as the source of truth:
|
||||
|
||||
- Documentation: `apps/design-system/content/docs/ui-patterns/navigation.mdx`
|
||||
|
||||
## NavMenu
|
||||
|
||||
- Use `NavMenu` for a horizontal list of related views within a consistent page layout.
|
||||
- Activating an item should trigger a URL change (no local-only tab state).
|
||||
- See: `apps/design-system/content/docs/components/nav-menu.mdx`
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
---
|
||||
description: "Studio: project structure and where code lives"
|
||||
globs:
|
||||
- apps/studio/**/*.{ts,tsx}
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Studio project structure
|
||||
|
||||
- Studio is a Next.js app using the pages router.
|
||||
- Pages live in `apps/studio/pages`.
|
||||
- Project pages: `apps/studio/pages/projects/[ref]`
|
||||
- Org pages: `apps/studio/pages/org/[slug]`
|
||||
- Studio components live in `apps/studio/components`.
|
||||
- Studio UI helpers: `apps/studio/components/ui`
|
||||
- Interface/page components: `apps/studio/components/interfaces` (e.g. `apps/studio/components/interfaces/Auth`)
|
||||
- Shared hooks: `apps/studio/hooks`
|
||||
- Shared helpers: `apps/studio/lib`
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
---
|
||||
description: 'Studio: data fetching conventions for queries/mutations (React Query hooks)'
|
||||
globs:
|
||||
- apps/studio/data/**/*.{ts,tsx}
|
||||
- apps/studio/pages/**/*.{ts,tsx}
|
||||
- apps/studio/components/**/*.{ts,tsx}
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Studio queries & mutations (React Query)
|
||||
|
||||
Follow the `apps/studio/data/` patterns used by edge functions:
|
||||
|
||||
- Query hook: `apps/studio/data/edge-functions/edge-functions-query.ts`
|
||||
- Mutation hook: `apps/studio/data/edge-functions/edge-functions-update-mutation.ts`
|
||||
- Keys: `apps/studio/data/edge-functions/keys.ts`
|
||||
- Page usage: `apps/studio/pages/project/[ref]/functions/index.tsx`
|
||||
|
||||
## Organize query keys
|
||||
|
||||
- Define a `keys.ts` per domain and export `*Keys` helpers (use array keys with `as const`).
|
||||
- Do not inline query keys in components.
|
||||
|
||||
Example:
|
||||
|
||||
```ts
|
||||
export const edgeFunctionsKeys = {
|
||||
list: (projectRef: string | undefined) => ['projects', projectRef, 'edge-functions'] as const,
|
||||
detail: (projectRef: string | undefined, slug: string | undefined) =>
|
||||
['projects', projectRef, 'edge-function', slug, 'detail'] as const,
|
||||
}
|
||||
```
|
||||
|
||||
## Write a query hook
|
||||
|
||||
- Export `Variables`, `Data`, and `Error` types from the file.
|
||||
- Implement a `getX(variables, signal?)` function that:
|
||||
- throws if required variables are missing
|
||||
- passes the `signal` through to the fetcher for cancellation
|
||||
- calls `handleError(error)` and returns `data`
|
||||
- Wrap it in `useXQuery()` using `useQuery`, `UseCustomQueryOptions`, and a domain key helper.
|
||||
- Gate with `enabled` so the query doesn’t run until required variables exist (and platform-only queries should include `IS_PLATFORM`).
|
||||
|
||||
Template:
|
||||
|
||||
```ts
|
||||
export type XVariables = { projectRef?: string }
|
||||
export type XError = ResponseError
|
||||
|
||||
export async function getX({ projectRef }: XVariables, signal?: AbortSignal) {
|
||||
if (!projectRef) throw new Error('projectRef is required')
|
||||
const { data, error } = await get('/v1/projects/{ref}/x', {
|
||||
params: { path: { ref: projectRef } },
|
||||
signal,
|
||||
})
|
||||
if (error) handleError(error)
|
||||
return data
|
||||
}
|
||||
|
||||
export type XData = Awaited<ReturnType<typeof getX>>
|
||||
|
||||
export const useXQuery = <TData = XData>(
|
||||
{ projectRef }: XVariables,
|
||||
{ enabled = true, ...options }: UseCustomQueryOptions<XData, XError, TData> = {}
|
||||
) =>
|
||||
useQuery<XData, XError, TData>({
|
||||
queryKey: xKeys.list(projectRef),
|
||||
queryFn: ({ signal }) => getX({ projectRef }, signal),
|
||||
enabled: IS_PLATFORM && enabled && typeof projectRef !== 'undefined',
|
||||
...options,
|
||||
})
|
||||
```
|
||||
|
||||
## Write a mutation hook
|
||||
|
||||
- Export a `Variables` type that includes `projectRef`, identifiers (e.g. `slug`), and `payload`.
|
||||
- Implement an `updateX(vars)` function that validates required variables and uses `handleError`.
|
||||
- Prefer a `useXMutation()` wrapper that:
|
||||
- accepts `UseCustomMutationOptions` (omit `mutationFn`)
|
||||
- invalidates the relevant `list()` + `detail()` keys in `onSuccess` and `await`s them via `Promise.all`
|
||||
- defaults to a `toast.error(...)` when `onError` isn’t provided
|
||||
|
||||
Template:
|
||||
|
||||
```ts
|
||||
export const useXUpdateMutation = ({ onSuccess, onError, ...options } = {}) => {
|
||||
const queryClient = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: updateX,
|
||||
async onSuccess(data, variables, context) {
|
||||
await Promise.all([
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: xKeys.detail(variables.projectRef, variables.slug),
|
||||
}),
|
||||
queryClient.invalidateQueries({ queryKey: xKeys.list(variables.projectRef) }),
|
||||
])
|
||||
await onSuccess?.(data, variables, context)
|
||||
},
|
||||
async onError(error, variables, context) {
|
||||
if (onError === undefined) toast.error(`Failed to update: ${error.message}`)
|
||||
else onError(error, variables, context)
|
||||
},
|
||||
...options,
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## Component usage
|
||||
|
||||
- Prefer React Query’s v5 flags:
|
||||
- `isPending` for initial load (often aliased to `isLoading`)
|
||||
- `isFetching` for background refetches
|
||||
- Render states explicitly (pending → error → success), like `apps/studio/pages/project/[ref]/functions/index.tsx`.
|
||||
@@ -0,0 +1,23 @@
|
||||
---
|
||||
description: "Studio: side panels (Sheet) for context-preserving workflows"
|
||||
globs:
|
||||
- apps/studio/**/*.{ts,tsx}
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Studio sheets
|
||||
|
||||
Use a `Sheet` when switching to a new page would be disruptive and the user should keep context (e.g. selecting an item from a list to edit details).
|
||||
|
||||
## Structure
|
||||
|
||||
- Prefer `SheetContent` with `size="lg"` for forms that need horizontal layout.
|
||||
- Use `SheetHeader`, `SheetTitle`, `SheetSection`, and `SheetFooter` for consistent structure.
|
||||
- Place submit/cancel actions in `SheetFooter`.
|
||||
|
||||
## Forms in sheets
|
||||
|
||||
- Prefer `FormItemLayout`:
|
||||
- `layout="horizontal"` for wider sheets
|
||||
- `layout="vertical"` for narrow sheets (`size="sm"` or below)
|
||||
- See `@studio/forms` for the canonical patterns and demos.
|
||||
@@ -0,0 +1,16 @@
|
||||
---
|
||||
description: "Studio: styling rules (Tailwind + semantic tokens + typography/focus utilities)"
|
||||
globs:
|
||||
- apps/studio/**/*.{ts,tsx,scss}
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Studio styling
|
||||
|
||||
- Use Tailwind.
|
||||
- Do not hardcode Tailwind color tokens; use our semantic classes:
|
||||
- backgrounds: `bg`, `bg-muted`, `bg-warning`, `bg-destructive`
|
||||
- text: `text-foreground`, `text-foreground-light`, `text-foreground-lighter`, `text-warning`, `text-destructive`
|
||||
- Use existing typography utilities from `apps/studio/styles/typography.scss` instead of recreating styles.
|
||||
- Use existing focus utilities from `apps/studio/styles/focus.scss` for consistent keyboard focus styling.
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
---
|
||||
description: "Studio: table patterns (Table vs Data Table vs Data Grid) and placement of actions/filters"
|
||||
globs:
|
||||
- apps/studio/**/*.{ts,tsx}
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Studio tables
|
||||
|
||||
Use the Design System UI pattern docs as the source of truth:
|
||||
|
||||
- Documentation: `apps/design-system/content/docs/ui-patterns/tables.mdx`
|
||||
- Demos:
|
||||
- `apps/design-system/registry/default/example/table-demo.tsx`
|
||||
- `apps/design-system/registry/default/example/data-table-demo.tsx`
|
||||
- `apps/design-system/registry/default/example/data-grid-demo.tsx`
|
||||
|
||||
## Choose the right pattern
|
||||
|
||||
- `Table`: simple, static, semantic table display.
|
||||
- Data Table: TanStack-powered pattern for sorting/filtering/pagination; composed per use case.
|
||||
- Data Grid: only when you need virtualization, column resizing, or complex cell editing.
|
||||
|
||||
## Actions and filters placement
|
||||
|
||||
- Actions: above the table, aligned right.
|
||||
- Search/filters: above the table, aligned left.
|
||||
- If the table is the primary page content and has no filters/search, actions can live in the page’s primary/secondary actions area.
|
||||
|
||||
@@ -0,0 +1,324 @@
|
||||
---
|
||||
description: "Testing: Playwright E2E best practices for Studio tests (avoid flake + race conditions)"
|
||||
globs:
|
||||
- e2e/studio/**/*.ts
|
||||
- e2e/studio/**/*.spec.ts
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# E2E Testing Best Practices
|
||||
|
||||
## Getting Context
|
||||
|
||||
Before writing or modifying tests, use the Playwright MCP to understand:
|
||||
|
||||
- Available page elements and their roles/locators
|
||||
- Current page state and network activity
|
||||
- Existing test patterns in the codebase
|
||||
|
||||
Avoid extensive code reading - let Playwright's inspection tools guide your understanding of the UI.
|
||||
|
||||
## Avoiding Race Conditions
|
||||
|
||||
### Set up API waiters BEFORE triggering actions
|
||||
|
||||
The most common source of flaky tests is race conditions between UI actions and API calls. Always create response waiters before clicking buttons or navigating.
|
||||
|
||||
```ts
|
||||
// ❌ Bad - race condition: response might complete before waiter is set up
|
||||
await page.getByRole('button', { name: 'Save' }).click()
|
||||
await waitForApiResponse(page, 'pg-meta', ref, 'query?key=table-create')
|
||||
|
||||
// ✅ Good - waiter is ready before action
|
||||
const apiPromise = waitForApiResponse(page, 'pg-meta', ref, 'query?key=table-create')
|
||||
await page.getByRole('button', { name: 'Save' }).click()
|
||||
await apiPromise
|
||||
```
|
||||
|
||||
### Use `createApiResponseWaiter` for pre-navigation waits
|
||||
|
||||
When you need to wait for a response that happens during navigation:
|
||||
|
||||
```ts
|
||||
// ✅ Good - waiter created before navigation
|
||||
const loadPromise = waitForTableToLoad(page, ref)
|
||||
await page.goto(toUrl(`/project/${ref}/editor?schema=public`))
|
||||
await loadPromise
|
||||
```
|
||||
|
||||
### Wait for multiple related API calls with Promise.all
|
||||
|
||||
When an action triggers multiple API calls, wait for all of them:
|
||||
|
||||
```ts
|
||||
// ✅ Good - wait for all related API calls
|
||||
const createTablePromise = waitForApiResponseWithTimeout(page, (response) =>
|
||||
response.url().includes('query?key=table-create')
|
||||
)
|
||||
const tablesPromise = waitForApiResponseWithTimeout(page, (response) =>
|
||||
response.url().includes('tables?include_columns=true')
|
||||
)
|
||||
const entitiesPromise = waitForApiResponseWithTimeout(page, (response) =>
|
||||
response.url().includes('query?key=entity-types-')
|
||||
)
|
||||
|
||||
await page.getByRole('button', { name: 'Save' }).click()
|
||||
await Promise.all([createTablePromise, tablesPromise, entitiesPromise])
|
||||
```
|
||||
|
||||
## Waiting Strategies
|
||||
|
||||
### Prefer Playwright's built-in auto-waiting
|
||||
|
||||
Playwright automatically waits for elements to be actionable. Use this instead of manual timeouts:
|
||||
|
||||
```ts
|
||||
// ❌ Bad - arbitrary timeout
|
||||
await page.waitForTimeout(2000)
|
||||
await page.getByRole('button', { name: 'Submit' }).click()
|
||||
|
||||
// ✅ Good - auto-waits for element to be visible and enabled
|
||||
await page.getByRole('button', { name: 'Submit' }).click()
|
||||
```
|
||||
|
||||
### Use `expect.poll` for dynamic assertions
|
||||
|
||||
When waiting for state to change:
|
||||
|
||||
```ts
|
||||
// ✅ Good - polls until condition is met
|
||||
await expect
|
||||
.poll(async () => {
|
||||
return await page.getByLabel(`View ${tableName}`).count()
|
||||
})
|
||||
.toBe(0)
|
||||
```
|
||||
|
||||
### Use `waitForSelector` with state for element lifecycle
|
||||
|
||||
```ts
|
||||
// ✅ Good - wait for panel to close
|
||||
await page.waitForSelector('[data-testid="side-panel"]', { state: 'detached' })
|
||||
```
|
||||
|
||||
### Avoid `networkidle` - use specific API waits instead
|
||||
|
||||
```ts
|
||||
// ❌ Bad - unreliable and slow
|
||||
await page.waitForLoadState('networkidle')
|
||||
|
||||
// ✅ Good - wait for specific API response
|
||||
await waitForApiResponse(page, 'pg-meta', ref, 'tables')
|
||||
```
|
||||
|
||||
### Use timeouts sparingly and only for non-API waits
|
||||
|
||||
```ts
|
||||
// ✅ Acceptable - waiting for client-side debounce
|
||||
await page.getByRole('textbox').fill('search term')
|
||||
await page.waitForTimeout(300) // Allow debounce to complete
|
||||
|
||||
// ✅ Acceptable - waiting for clipboard API
|
||||
await page.evaluate(() => navigator.clipboard.readText())
|
||||
await page.waitForTimeout(500)
|
||||
```
|
||||
|
||||
## Test Structure
|
||||
|
||||
### Use the custom test utility
|
||||
|
||||
Always import from the custom test utility for consistent fixtures:
|
||||
|
||||
```ts
|
||||
import { test } from '../utils/test.js'
|
||||
```
|
||||
|
||||
### Use `withFileOnceSetup` for expensive setup
|
||||
|
||||
When setup is expensive (cleanup, seeding), run it once per file:
|
||||
|
||||
```ts
|
||||
test.beforeAll(async ({ browser, ref }) => {
|
||||
await withFileOnceSetup(import.meta.url, async () => {
|
||||
const ctx = await browser.newContext()
|
||||
const page = await ctx.newPage()
|
||||
|
||||
// Expensive setup logic (e.g., cleanup old test data)
|
||||
await deleteTestTables(page, ref)
|
||||
})
|
||||
})
|
||||
|
||||
test.afterAll(async () => {
|
||||
await releaseFileOnceCleanup(import.meta.url)
|
||||
})
|
||||
```
|
||||
|
||||
### Dismiss toasts before interacting with UI
|
||||
|
||||
Toasts can overlay buttons and block interactions:
|
||||
|
||||
```ts
|
||||
const dismissToastsIfAny = async (page: Page) => {
|
||||
const closeButtons = page.getByRole('button', { name: 'Close toast' })
|
||||
const count = await closeButtons.count()
|
||||
for (let i = 0; i < count; i++) {
|
||||
await closeButtons.nth(i).click()
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ Good - dismiss toasts before clicking
|
||||
await dismissToastsIfAny(page)
|
||||
await page.getByRole('button', { name: 'New table' }).click()
|
||||
```
|
||||
|
||||
## Assertions
|
||||
|
||||
### Always include descriptive messages
|
||||
|
||||
```ts
|
||||
// ❌ Bad - no context on failure
|
||||
await expect(page.getByRole('button', { name: 'Save' })).toBeVisible()
|
||||
|
||||
// ✅ Good - clear message on failure
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Save' }),
|
||||
'Save button should be visible after form is filled'
|
||||
).toBeVisible()
|
||||
```
|
||||
|
||||
### Use appropriate timeouts for slow operations
|
||||
|
||||
```ts
|
||||
// ✅ Good - explicit timeout for slow operations
|
||||
await expect(
|
||||
page.getByText(`Table ${tableName} is good to go!`),
|
||||
'Success toast should be visible after table creation'
|
||||
).toBeVisible({ timeout: 50000 })
|
||||
```
|
||||
|
||||
## Locators
|
||||
|
||||
### Prefer role-based locators
|
||||
|
||||
```ts
|
||||
// ✅ Good - semantic and resilient
|
||||
page.getByRole('button', { name: 'Save' })
|
||||
page.getByRole('textbox', { name: 'Username' })
|
||||
page.getByRole('menuitem', { name: 'Delete' })
|
||||
|
||||
// ❌ Avoid - brittle CSS selectors
|
||||
page.locator('.btn-primary')
|
||||
page.locator('#submit-button')
|
||||
```
|
||||
|
||||
### Use test IDs for complex elements
|
||||
|
||||
```ts
|
||||
// ✅ Good - stable identifier for complex elements
|
||||
page.getByTestId('table-editor-side-panel')
|
||||
page.getByTestId('action-bar-save-row')
|
||||
```
|
||||
|
||||
### Use `filter` for finding elements in context
|
||||
|
||||
```ts
|
||||
// ✅ Good - find button within specific row
|
||||
const bucketRow = page.getByRole('row').filter({ hasText: bucketName })
|
||||
await bucketRow.getByRole('button').click()
|
||||
```
|
||||
|
||||
## Helper Functions
|
||||
|
||||
### Extract reusable operations into helpers
|
||||
|
||||
Create helper functions for common operations:
|
||||
|
||||
```ts
|
||||
// e2e/studio/utils/storage-helpers.ts
|
||||
export const createBucket = async (
|
||||
page: Page,
|
||||
ref: string,
|
||||
bucketName: string,
|
||||
isPublic: boolean = false
|
||||
) => {
|
||||
await navigateToStorageFiles(page, ref)
|
||||
|
||||
// Check if already exists
|
||||
const bucketRow = page.getByRole('row').filter({ hasText: bucketName })
|
||||
if ((await bucketRow.count()) > 0) return
|
||||
|
||||
await dismissToastsIfAny(page)
|
||||
|
||||
// Create bucket with proper waits
|
||||
const apiPromise = waitForApiResponse(page, 'storage', ref, 'bucket', { method: 'POST' })
|
||||
await page.getByRole('button', { name: 'New bucket' }).click()
|
||||
await page.getByRole('textbox', { name: 'Bucket name' }).fill(bucketName)
|
||||
await page.getByRole('button', { name: 'Create' }).click()
|
||||
await apiPromise
|
||||
|
||||
await expect(
|
||||
page.getByRole('row').filter({ hasText: bucketName }),
|
||||
`Bucket ${bucketName} should be visible`
|
||||
).toBeVisible()
|
||||
}
|
||||
```
|
||||
|
||||
### Use the existing wait utilities
|
||||
|
||||
```ts
|
||||
import {
|
||||
createApiResponseWaiter,
|
||||
waitForApiResponse,
|
||||
waitForGridDataToLoad,
|
||||
waitForTableToLoad,
|
||||
} from '../utils/wait-for-response.js'
|
||||
```
|
||||
|
||||
## API Mocking
|
||||
|
||||
### Mock APIs for isolated testing
|
||||
|
||||
```ts
|
||||
// ✅ Good - mock API response
|
||||
await page.route('*/**/logs.all*', async (route) => {
|
||||
await route.fulfill({ body: JSON.stringify(mockAPILogs) })
|
||||
})
|
||||
```
|
||||
|
||||
### Use soft waits for optional API calls
|
||||
|
||||
```ts
|
||||
// ✅ Good - don't fail if API doesn't respond
|
||||
await waitForApiResponse(page, 'pg-meta', ref, 'optional-endpoint', {
|
||||
soft: true,
|
||||
fallbackWaitMs: 1000,
|
||||
})
|
||||
```
|
||||
|
||||
## Cleanup
|
||||
|
||||
### Clean up test data in beforeAll/beforeEach
|
||||
|
||||
```ts
|
||||
test.beforeEach(async ({ page, ref }) => {
|
||||
await deleteAllBuckets(page, ref)
|
||||
})
|
||||
```
|
||||
|
||||
### Handle existing state gracefully
|
||||
|
||||
```ts
|
||||
// ✅ Good - check before trying to delete
|
||||
const bucketRow = page.getByRole('row').filter({ hasText: bucketName })
|
||||
if ((await bucketRow.count()) === 0) return
|
||||
// proceed with deletion
|
||||
```
|
||||
|
||||
### Reset local storage when needed
|
||||
|
||||
```ts
|
||||
import { resetLocalStorage } from '../utils/reset-local-storage.js'
|
||||
|
||||
// Clean up after tests that modify local storage
|
||||
await resetLocalStorage(page, ref)
|
||||
```
|
||||
@@ -0,0 +1,10 @@
|
||||
---
|
||||
description: "Testing: unit/integration conventions for Studio test files"
|
||||
globs:
|
||||
- apps/studio/**/*.test.ts
|
||||
- apps/studio/**/*.test.tsx
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
Follow the guidelines in `apps/studio/tests/README.md` when writing tests for Studio.
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
description:
|
||||
globs: apps/studio/**/*.test.ts,apps/studio/**/*.test.tsx
|
||||
alwaysApply: false
|
||||
---
|
||||
Make sure to follow the guidelines in this file to write tests: [README.md](mdc:apps/studio/tests/README.md)
|
||||
@@ -18,3 +18,5 @@
|
||||
/apps/studio/components/interfaces/Billing/Payment @supabase/security
|
||||
/apps/studio/components/interfaces/Organization/Documents/ @supabase/security
|
||||
/apps/studio/pages/new/index.tsx @supabase/security
|
||||
|
||||
/apps/studio/data/sql/queries/ @supabase/postgres @avallete
|
||||
@@ -0,0 +1,50 @@
|
||||
name: Authorize Vercel Deploys
|
||||
|
||||
# This workflow is triggered by the validate-pr workflow. When it's triggered, it will run the
|
||||
# authorize-vercel-deploys.yml in master branch. If you want to change it, you'll have to merge it into master.
|
||||
on:
|
||||
# only run this workflow when the validate-pr workflow completes (successfully or not)
|
||||
workflow_run:
|
||||
workflows: ['Validate pull request']
|
||||
types: [completed]
|
||||
|
||||
# Cancel old builds on new commit for same workflow + branch/PR.
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.workflow_run.head_branch || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
authorize-vercel-deploys:
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
|
||||
steps:
|
||||
# Checkout the master branch from the supabase repo and run that script to authorize Vercel deploys
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
with:
|
||||
ref: master
|
||||
# fetch only the root files and scripts folder
|
||||
sparse-checkout: |
|
||||
scripts
|
||||
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
|
||||
name: Install pnpm
|
||||
with:
|
||||
run_install: false
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'pnpm'
|
||||
- name: Download dependencies
|
||||
run: |
|
||||
pnpm install --frozen-lockfile
|
||||
- name: Authorize Vercel Deploys
|
||||
run: |-
|
||||
pnpm run authorize-vercel-deploys
|
||||
env:
|
||||
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
|
||||
# The SHA of the commit that triggered the validate-pr workflow
|
||||
HEAD_COMMIT_SHA: ${{ github.event.workflow_run.head_sha }}
|
||||
@@ -10,9 +10,6 @@ concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
autofix:
|
||||
runs-on: blacksmith-2vcpu-ubuntu-2404
|
||||
@@ -23,10 +20,18 @@ jobs:
|
||||
- name: Calculate number of commits
|
||||
run: echo "PR_FETCH_DEPTH=$(( ${{ github.event.pull_request.commits }} + 1 ))" >> "${GITHUB_ENV}"
|
||||
|
||||
- name: Generate token
|
||||
id: app-token
|
||||
uses: actions/create-github-app-token@v2
|
||||
with:
|
||||
app-id: ${{ secrets.GH_AUTOFIX_APP_ID }}
|
||||
private-key: ${{ secrets.GH_AUTOFIX_PRIVATE_KEY }}
|
||||
permission-contents: write
|
||||
|
||||
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
with:
|
||||
ref: ${{ github.head_ref }}
|
||||
token: ${{ secrets.PAT_AUTOFIX }}
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
fetch-depth: ${{ env.PR_FETCH_DEPTH }}
|
||||
sparse-checkout: |
|
||||
packages
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
name: Run Braintrust evals
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
pull_request:
|
||||
types: [opened, synchronize, labeled]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
eval:
|
||||
name: Run evals
|
||||
if: github.event_name == 'push' || (github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'run-evals'))
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
|
||||
env:
|
||||
BRAINTRUST_PROJECT_ID: ${{ secrets.BRAINTRUST_PROJECT_ID }}
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
# For PR events, checkout the actual branch so Braintrust can report the correct branch name instead of detached HEAD.
|
||||
# github.head_ref is the PR source branch, github.ref_name is the fallback for push events (e.g., master).
|
||||
ref: ${{ github.head_ref || github.ref_name }}
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
|
||||
with:
|
||||
run_install: false
|
||||
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Install Dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Setup Evals
|
||||
run: cd apps/studio && pnpm evals:setup
|
||||
|
||||
- name: Run Evals
|
||||
uses: braintrustdata/eval-action@v1
|
||||
with:
|
||||
api_key: ${{ secrets.BRAINTRUST_API_KEY }}
|
||||
runtime: node
|
||||
package_manager: pnpm
|
||||
root: apps/studio
|
||||
@@ -54,14 +54,21 @@ jobs:
|
||||
echo "Generating new typespec snapshot for review..."
|
||||
npx vitest run --update ./features/docs/Reference.typeSpec.test.ts
|
||||
|
||||
- name: Generate token
|
||||
id: app-token
|
||||
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf
|
||||
with:
|
||||
app-id: ${{ secrets.GH_AUTOFIX_APP_ID }}
|
||||
private-key: ${{ secrets.GH_AUTOFIX_PRIVATE_KEY }}
|
||||
|
||||
- name: Create pull request
|
||||
uses: peter-evans/create-pull-request@c5a7806660adbe173f04e3e038b0ccdcd758773c # v6.1.0
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
commit-message: 'docs: update js client libraries (${{ github.event.inputs.version }})'
|
||||
title: 'docs: update js client libraries (${{ github.event.inputs.version }})'
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
commit-message: 'docs: update js sdk docs (${{ github.event.inputs.version }})'
|
||||
title: 'docs: update js sdk docs (${{ github.event.inputs.version }})'
|
||||
body: |
|
||||
Updates JS client libraries documentation following stable release.
|
||||
Updates JS sdk documentation following stable release.
|
||||
Ran `make` in apps/docs/spec to regenerate tsdoc files.
|
||||
|
||||
**Details:**
|
||||
@@ -69,6 +76,6 @@ jobs:
|
||||
- **Source:** `${{ github.event.inputs.source }}`
|
||||
- **Changes:** Regenerated tsdoc files from latest spec files
|
||||
|
||||
🤖 Auto-generated from supabase-js-libs stable release.
|
||||
branch: 'gha/update-js-libs-docs-${{ github.run_number }}'
|
||||
🤖 Auto-generated from @supabase/supabase-js stable release.
|
||||
branch: 'gha/update-js-sdk-docs-${{ github.run_number }}'
|
||||
base: 'master'
|
||||
@@ -39,10 +39,17 @@ jobs:
|
||||
working-directory: apps/docs/spec
|
||||
run: make download.api.v1 dereference.api.v1 generate.sections.api.v1 format
|
||||
|
||||
- name: Generate token
|
||||
id: app-token
|
||||
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
|
||||
with:
|
||||
app-id: ${{ secrets.GH_AUTOFIX_APP_ID }}
|
||||
private-key: ${{ secrets.GH_AUTOFIX_PRIVATE_KEY }}
|
||||
|
||||
- name: Create pull request
|
||||
uses: peter-evans/create-pull-request@c5a7806660adbe173f04e3e038b0ccdcd758773c # v6.1.0
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
commit-message: 'feat: update mgmt api docs'
|
||||
title: 'feat: update mgmt api docs'
|
||||
body: 'This PR updates mgmt api docs automatically.'
|
||||
|
||||
@@ -54,3 +54,4 @@ jobs:
|
||||
author: 'github-docs-sync-bot <github-docs-sync-bot@supabase.com>'
|
||||
branch: 'bot/docs-sync-troubleshooting'
|
||||
branch-suffix: 'random'
|
||||
labels: 'documentation'
|
||||
@@ -11,7 +11,6 @@ concurrency:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -21,6 +20,16 @@ jobs:
|
||||
with:
|
||||
ref: master
|
||||
- uses: sobolevn/misspell-fixer-action@06ff0b508d4f4c0ba70d15f9a628232c0aade536 # v0.1.0
|
||||
|
||||
- name: Generate token
|
||||
id: app-token
|
||||
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
|
||||
with:
|
||||
app-id: ${{ secrets.GH_AUTOFIX_APP_ID }}
|
||||
private-key: ${{ secrets.GH_AUTOFIX_PRIVATE_KEY }}
|
||||
permission-contents: write
|
||||
permission-pull-requests: write
|
||||
|
||||
- uses: peter-evans/create-pull-request@c5a7806660adbe173f04e3e038b0ccdcd758773c # v6.1.0
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
@@ -124,6 +124,12 @@ jobs:
|
||||
docker buildx imagetools create -t supabase/studio:latest \
|
||||
supabase/studio@${{ needs.release_x86.outputs.image_digest }} \
|
||||
supabase/studio@${{ needs.release_arm.outputs.image_digest }}
|
||||
echo "Published Registry Images" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Image | Link |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "|-------|------|" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| \`supabase/studio:${{ needs.settings.outputs.image_version }}\` | [View on Docker Hub](https://hub.docker.com/r/supabase/studio/tags?name=${{ needs.settings.outputs.image_version }}) |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| \`supabase/studio:latest\` | [View on Docker Hub](https://hub.docker.com/r/supabase/studio/tags?name=latest) |" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
publish:
|
||||
needs:
|
||||
|
||||
@@ -2,18 +2,7 @@ name: Selfhosted Studio E2E Tests
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
paths:
|
||||
- 'packages/pg-meta/**/*'
|
||||
- 'apps/studio/**'
|
||||
- 'e2e/studio/**'
|
||||
- 'pnpm-lock.yaml'
|
||||
pull_request:
|
||||
paths:
|
||||
- 'packages/pg-meta/**/*'
|
||||
- 'apps/studio/**'
|
||||
- 'e2e/studio/**'
|
||||
- 'pnpm-lock.yaml'
|
||||
- '.github/workflows/studio-e2e-test.yml'
|
||||
|
||||
# Cancel old builds on new commit for same workflow + branch/PR
|
||||
concurrency:
|
||||
@@ -28,33 +17,51 @@ jobs:
|
||||
test:
|
||||
timeout-minutes: 60
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
# Require approval only for pull requests from forks
|
||||
environment: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork && 'Studio E2E Tests' || '' }}
|
||||
|
||||
env:
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
- uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
||||
id: filter
|
||||
with:
|
||||
filters: |
|
||||
studio:
|
||||
- 'packages/pg-meta/**'
|
||||
- 'apps/studio/**'
|
||||
- 'apps/ui-library/**'
|
||||
- 'apps/design-system/**'
|
||||
- 'e2e/studio/**'
|
||||
- 'pnpm-lock.yaml'
|
||||
- '.github/workflows/studio-e2e-test.yml'
|
||||
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
|
||||
if: steps.filter.outputs.studio == 'true'
|
||||
name: Install pnpm
|
||||
with:
|
||||
run_install: false
|
||||
- name: Use Node.js
|
||||
if: steps.filter.outputs.studio == 'true'
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
if: steps.filter.outputs.studio == 'true'
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Install Playwright Browsers
|
||||
if: steps.filter.outputs.studio == 'true'
|
||||
run: pnpm -C e2e/studio exec playwright install chromium --with-deps --only-shell
|
||||
|
||||
- name: 🚀 Run Playwright tests against Vercel Preview
|
||||
if: steps.filter.outputs.studio == 'true'
|
||||
id: playwright
|
||||
run: pnpm e2e
|
||||
|
||||
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
if: always()
|
||||
if: always() && steps.filter.outputs.studio == 'true'
|
||||
with:
|
||||
name: playwright-artifacts
|
||||
path: |
|
||||
@@ -64,13 +71,13 @@ jobs:
|
||||
|
||||
- name: Comment Playwright test results on PR
|
||||
uses: daun/playwright-report-comment@be9e270edd5ad86038604d3caa84a819a6ff6fed # v3.10.0
|
||||
if: always() && github.event_name == 'pull_request' && !github.event.pull_request.head.repo.fork
|
||||
if: always() && steps.filter.outputs.studio == 'true' && github.event_name == 'pull_request' && !github.event.pull_request.head.repo.fork
|
||||
with:
|
||||
report-file: e2e/studio/test-results/test-results.json
|
||||
comment-title: '🎭 Playwright Test Results'
|
||||
|
||||
- name: Fail job if tests failed
|
||||
if: steps.playwright.outcome != 'success' || steps.summarize.outputs.flaky_count > 0
|
||||
if: steps.filter.outputs.studio == 'true' && (steps.playwright.outcome != 'success' || steps.summarize.outputs.flaky_count > 0)
|
||||
run: |
|
||||
echo "E2E tests failed" >&2
|
||||
exit 1
|
||||
@@ -35,9 +35,16 @@ jobs:
|
||||
- name: Install deps
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Generate token
|
||||
id: app-token
|
||||
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
|
||||
with:
|
||||
app-id: ${{ secrets.GH_AUTOFIX_APP_ID }}
|
||||
private-key: ${{ secrets.GH_AUTOFIX_PRIVATE_KEY }}
|
||||
|
||||
- name: Decrease ESLint ratchet baselines and open PR
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GH_TOKEN: ${{ steps.app-token.outputs.token }}
|
||||
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
|
||||
run: |
|
||||
set -eo pipefail
|
||||
|
||||
@@ -16,8 +16,9 @@ permissions:
|
||||
|
||||
jobs:
|
||||
typecheck:
|
||||
# Uses larger hosted runner as it significantly decreases build times
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
runs-on: blacksmith-2vcpu-ubuntu-2404
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=4096
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -38,7 +39,7 @@ jobs:
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Run TypeScript type check
|
||||
run: pnpm exec turbo run typecheck
|
||||
run: pnpm run typecheck
|
||||
|
||||
- name: Run Lint
|
||||
run: pnpm exec turbo run lint
|
||||
run: pnpm run lint
|
||||
@@ -56,11 +56,18 @@ jobs:
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --no-frozen-lockfile
|
||||
|
||||
|
||||
- name: Generate token
|
||||
id: app-token
|
||||
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
|
||||
with:
|
||||
app-id: ${{ secrets.GH_AUTOFIX_APP_ID }}
|
||||
private-key: ${{ secrets.GH_AUTOFIX_PRIVATE_KEY }}
|
||||
|
||||
- name: Create pull request
|
||||
uses: peter-evans/create-pull-request@c5a7806660adbe173f04e3e038b0ccdcd758773c # v6.1.0
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
commit-message: 'feat: update @supabase/*-js libraries to v${{ github.event.inputs.version }}'
|
||||
title: 'feat: update @supabase/*-js libraries to v${{ github.event.inputs.version }}'
|
||||
body: |
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
name: Validate pull request
|
||||
|
||||
# This workflow will trigger the authorize-vercel-deploys workflow when it's finished.
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, labeled, unlabeled, synchronize, ready_for_review]
|
||||
|
||||
jobs:
|
||||
validate-pr:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Tagged with 'do not merge'
|
||||
if: contains( github.event.pull_request.labels.*.name, 'do-not-merge')
|
||||
run: |
|
||||
echo "PR blocked: [tag: do not merge]"
|
||||
exit 1
|
||||
|
||||
- name: All good
|
||||
if: ${{ success() }}
|
||||
run: |
|
||||
echo "All good"
|
||||
+4
-2
@@ -117,7 +117,9 @@ next-env.d.ts
|
||||
.vercel
|
||||
|
||||
# AI assistant local files
|
||||
.serena/
|
||||
.claude/*
|
||||
!.claude/settings.json
|
||||
!.claude/scripts/
|
||||
CLAUDE.md
|
||||
|
||||
#include template .env file for docker-compose
|
||||
@@ -149,4 +151,4 @@ gcloud.json
|
||||
# Sentry CLI config
|
||||
**/.sentryclirc
|
||||
|
||||
keys.json
|
||||
keys.json
|
||||
+1
-1
@@ -192,7 +192,7 @@ We support "federating" docs, meaning doc content can come directly from externa
|
||||
|
||||
Federated docs work using Next.js's build pipeline. We use `getStaticProps()` to fetch remote documentation (ie. markdown) at build time which is processed and passed to the respective page within the docs.
|
||||
|
||||
See the [Vecs Python source code](https://github.com/supabase/supabase/blob/master/apps/docs/pages/guides/ai/python/%5Bslug%5D.tsx) to see how we do this for [`supabase/vecs`](https://github.com/supabase/vecs). Use this as a starting point for federating other docs.
|
||||
See the [Vecs Python source code](https://github.com/supabase/supabase/tree/master/apps/docs/app/guides/ai/python/%5Bslug%5D to see how we do this for [`supabase/vecs`](https://github.com/supabase/vecs). Use this as a starting point for federating other docs.
|
||||
|
||||
Some things to consider:
|
||||
|
||||
|
||||
@@ -96,11 +96,11 @@ Our approach for client libraries is modular. Each sub-library is a standalone i
|
||||
<tr>
|
||||
<td>JavaScript (TypeScript)</td>
|
||||
<td><a href="https://github.com/supabase/supabase-js" target="_blank" rel="noopener noreferrer">supabase-js</a></td>
|
||||
<td><a href="https://github.com/supabase/postgrest-js" target="_blank" rel="noopener noreferrer">postgrest-js</a></td>
|
||||
<td><a href="https://github.com/supabase/gotrue-js" target="_blank" rel="noopener noreferrer">gotrue-js</a></td>
|
||||
<td><a href="https://github.com/supabase/realtime-js" target="_blank" rel="noopener noreferrer">realtime-js</a></td>
|
||||
<td><a href="https://github.com/supabase/storage-js" target="_blank" rel="noopener noreferrer">storage-js</a></td>
|
||||
<td><a href="https://github.com/supabase/functions-js" target="_blank" rel="noopener noreferrer">functions-js</a></td>
|
||||
<td><a href="https://github.com/supabase/supabase-js/tree/master/packages/core/postgrest-js" target="_blank" rel="noopener noreferrer">postgrest-js</a></td>
|
||||
<td><a href="https://github.com/supabase/supabase-js/tree/master/packages/core/auth-js" target="_blank" rel="noopener noreferrer">auth-js</a></td>
|
||||
<td><a href="https://github.com/supabase/supabase-js/tree/master/packages/core/realtime-js" target="_blank" rel="noopener noreferrer">realtime-js</a></td>
|
||||
<td><a href="https://github.com/supabase/supabase-js/tree/master/packages/core/storage-js" target="_blank" rel="noopener noreferrer">storage-js</a></td>
|
||||
<td><a href="https://github.com/supabase/supabase-js/tree/master/packages/core/functions-js" target="_blank" rel="noopener noreferrer">functions-js</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Flutter</td>
|
||||
@@ -189,11 +189,11 @@ Our approach for client libraries is modular. Each sub-library is a standalone i
|
||||
<tr>
|
||||
<td>Godot Engine (GDScript)</td>
|
||||
<td><a href="https://github.com/supabase-community/godot-engine.supabase" target="_blank" rel="noopener noreferrer">supabase-gdscript</a></td>
|
||||
<td><a href="https://github.com/supabase-community/postgrest-gdscript" target="_blank" rel="noopener noreferrer">postgrest-gdscript</a></td>
|
||||
<td><a href="https://github.com/supabase-community/gotrue-gdscript" target="_blank" rel="noopener noreferrer">gotrue-gdscript</a></td>
|
||||
<td><a href="https://github.com/supabase-community/realtime-gdscript" target="_blank" rel="noopener noreferrer">realtime-gdscript</a></td>
|
||||
<td><a href="https://github.com/supabase-community/storage-gdscript" target="_blank" rel="noopener noreferrer">storage-gdscript</a></td>
|
||||
<td><a href="https://github.com/supabase-community/functions-gdscript" target="_blank" rel="noopener noreferrer">functions-gdscript</a></td>
|
||||
<td>-</td>
|
||||
<td>-</td>
|
||||
<td>-</td>
|
||||
<td>-</td>
|
||||
<td>-</td>
|
||||
</tr>
|
||||
<!-- /notranslate -->
|
||||
</table>
|
||||
|
||||
@@ -17,14 +17,14 @@
|
||||
"payload": "cross-env NODE_OPTIONS=--no-deprecation payload",
|
||||
"start": "cross-env NODE_OPTIONS=--no-deprecation next start",
|
||||
"vercel-build": "pnpm migrate && pnpm build",
|
||||
"typecheck_IGNORED": "tsc --noEmit"
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@payloadcms/admin-bar": "^3.52.0",
|
||||
"@payloadcms/db-postgres": "^3.52.0",
|
||||
"@payloadcms/live-preview-react": "^3.52.0",
|
||||
"@payloadcms/next": "3.52.0",
|
||||
"@payloadcms/payload-cloud": "3.52.0",
|
||||
"@payloadcms/payload-cloud": "3.60.0",
|
||||
"@payloadcms/plugin-form-builder": "3.52.0",
|
||||
"@payloadcms/plugin-nested-docs": "3.52.0",
|
||||
"@payloadcms/plugin-seo": "3.52.0",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { BannerBlock as BannerBlockProps } from 'src/payload-types'
|
||||
import type { BannerBlock as BannerBlockProps } from '@/payload-types'
|
||||
|
||||
import { cn } from '@/utilities/ui'
|
||||
import React from 'react'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { QuoteBlock as QuoteBlockProps } from 'src/payload-types'
|
||||
import type { QuoteBlock as QuoteBlockProps } from '@/payload-types'
|
||||
|
||||
import React from 'react'
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { YouTubeBlock as YouTubeBlockProps } from 'src/payload-types'
|
||||
import type { YouTubeBlock as YouTubeBlockProps } from '@/payload-types'
|
||||
|
||||
import { cn } from '@/utilities/ui'
|
||||
import React from 'react'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Config } from 'src/payload-types'
|
||||
import type { Config } from '@/payload-types'
|
||||
|
||||
import configPromise from '@payload-config'
|
||||
import { getPayload } from 'payload'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Config } from 'src/payload-types'
|
||||
import type { Config } from '@/payload-types'
|
||||
|
||||
import configPromise from '@payload-config'
|
||||
import { getPayload } from 'payload'
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
"strict": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noImplicitOverride": true,
|
||||
"baseUrl": ".",
|
||||
"esModuleInterop": true,
|
||||
"target": "ES2022",
|
||||
"lib": ["DOM", "DOM.Iterable", "ES2022"],
|
||||
|
||||
@@ -60,9 +60,9 @@ With that out of the way, there are several parts of this design system that nee
|
||||
- `config/docs.ts`: list of components in the sidebar
|
||||
- `content/docs`: the actual component documentation
|
||||
- `registry/examples.ts`: list of example components
|
||||
- `registry/default/example`: the actual example components
|
||||
- `registry/charts.ts`: chart components
|
||||
- `registry/fragments.ts`: fragment components
|
||||
- `registry/fragments.ts`: list of fragment components
|
||||
- `registry/charts.ts`: list of chart components
|
||||
- `registry/default/example/*`: the actual example components
|
||||
|
||||
You will probably need to rebuild the design system’s registry after making new additions. You can do that via:
|
||||
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { TrendingUp } from 'lucide-react'
|
||||
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from 'recharts'
|
||||
|
||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from 'ui'
|
||||
import { ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent } from 'ui'
|
||||
|
||||
export const description = 'An area chart with axes'
|
||||
|
||||
const chartData = [
|
||||
{ month: 'January', desktop: 186, mobile: 80 },
|
||||
{ month: 'February', desktop: 305, mobile: 200 },
|
||||
{ month: 'March', desktop: 237, mobile: 120 },
|
||||
{ month: 'April', desktop: 73, mobile: 190 },
|
||||
{ month: 'May', desktop: 209, mobile: 130 },
|
||||
{ month: 'June', desktop: 214, mobile: 140 },
|
||||
]
|
||||
|
||||
const chartConfig = {
|
||||
desktop: {
|
||||
label: 'Desktop',
|
||||
color: 'hsl(var(--chart-1))',
|
||||
},
|
||||
mobile: {
|
||||
label: 'Mobile',
|
||||
color: 'hsl(var(--chart-2))',
|
||||
},
|
||||
} satisfies ChartConfig
|
||||
|
||||
export default function Component() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Area Chart - Axes</CardTitle>
|
||||
<CardDescription>Showing total visitors for the last 6 months</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ChartContainer config={chartConfig}>
|
||||
<AreaChart
|
||||
accessibilityLayer
|
||||
data={chartData}
|
||||
margin={{
|
||||
left: -20,
|
||||
right: 12,
|
||||
}}
|
||||
>
|
||||
<CartesianGrid vertical={false} />
|
||||
<XAxis
|
||||
dataKey="month"
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickMargin={8}
|
||||
tickFormatter={(value) => value.slice(0, 3)}
|
||||
/>
|
||||
<YAxis tickLine={false} axisLine={false} tickMargin={8} tickCount={3} />
|
||||
<ChartTooltip cursor={false} content={<ChartTooltipContent />} />
|
||||
<Area
|
||||
dataKey="mobile"
|
||||
type="natural"
|
||||
fill="var(--color-mobile)"
|
||||
fillOpacity={0.4}
|
||||
stroke="var(--color-mobile)"
|
||||
stackId="a"
|
||||
/>
|
||||
<Area
|
||||
dataKey="desktop"
|
||||
type="natural"
|
||||
fill="var(--color-desktop)"
|
||||
fillOpacity={0.4}
|
||||
stroke="var(--color-desktop)"
|
||||
stackId="a"
|
||||
/>
|
||||
</AreaChart>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<div className="flex w-full items-start gap-2 text-sm">
|
||||
<div className="grid gap-2">
|
||||
<div className="flex items-center gap-2 font-medium leading-none">
|
||||
Trending up by 5.2% this month <TrendingUp className="h-4 w-4" />
|
||||
</div>
|
||||
<div className="flex items-center gap-2 leading-none text-muted-foreground">
|
||||
January - June 2024
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { TrendingUp } from 'lucide-react'
|
||||
import { Area, AreaChart, CartesianGrid, XAxis } from 'recharts'
|
||||
|
||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from 'ui'
|
||||
import { ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent } from 'ui'
|
||||
|
||||
export const description = 'A simple area chart'
|
||||
|
||||
const chartData = [
|
||||
{ month: 'January', desktop: 186 },
|
||||
{ month: 'February', desktop: 305 },
|
||||
{ month: 'March', desktop: 237 },
|
||||
{ month: 'April', desktop: 73 },
|
||||
{ month: 'May', desktop: 209 },
|
||||
{ month: 'June', desktop: 214 },
|
||||
]
|
||||
|
||||
const chartConfig = {
|
||||
desktop: {
|
||||
label: 'Desktop',
|
||||
color: 'hsl(var(--chart-1))',
|
||||
},
|
||||
} satisfies ChartConfig
|
||||
|
||||
export default function Component() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Area Chart</CardTitle>
|
||||
<CardDescription>Showing total visitors for the last 6 months</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ChartContainer config={chartConfig}>
|
||||
<AreaChart
|
||||
accessibilityLayer
|
||||
data={chartData}
|
||||
margin={{
|
||||
left: 12,
|
||||
right: 12,
|
||||
}}
|
||||
>
|
||||
<CartesianGrid vertical={false} />
|
||||
<XAxis
|
||||
dataKey="month"
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickMargin={8}
|
||||
tickFormatter={(value) => value.slice(0, 3)}
|
||||
/>
|
||||
<ChartTooltip cursor={false} content={<ChartTooltipContent indicator="line" />} />
|
||||
<Area
|
||||
dataKey="desktop"
|
||||
type="natural"
|
||||
fill="var(--color-desktop)"
|
||||
fillOpacity={0.4}
|
||||
stroke="var(--color-desktop)"
|
||||
/>
|
||||
</AreaChart>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<div className="flex w-full items-start gap-2 text-sm">
|
||||
<div className="grid gap-2">
|
||||
<div className="flex items-center gap-2 font-medium leading-none">
|
||||
Trending up by 5.2% this month <TrendingUp className="h-4 w-4" />
|
||||
</div>
|
||||
<div className="flex items-center gap-2 leading-none text-muted-foreground">
|
||||
January - June 2024
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { TrendingUp } from 'lucide-react'
|
||||
import { Area, AreaChart, CartesianGrid, XAxis } from 'recharts'
|
||||
|
||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from 'ui'
|
||||
import { ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent } from 'ui'
|
||||
|
||||
export const description = 'An area chart with gradient fill'
|
||||
|
||||
const chartData = [
|
||||
{ month: 'January', desktop: 186, mobile: 80 },
|
||||
{ month: 'February', desktop: 305, mobile: 200 },
|
||||
{ month: 'March', desktop: 237, mobile: 120 },
|
||||
{ month: 'April', desktop: 73, mobile: 190 },
|
||||
{ month: 'May', desktop: 209, mobile: 130 },
|
||||
{ month: 'June', desktop: 214, mobile: 140 },
|
||||
]
|
||||
|
||||
const chartConfig = {
|
||||
desktop: {
|
||||
label: 'Desktop',
|
||||
color: 'hsl(var(--chart-1))',
|
||||
},
|
||||
mobile: {
|
||||
label: 'Mobile',
|
||||
color: 'hsl(var(--chart-2))',
|
||||
},
|
||||
} satisfies ChartConfig
|
||||
|
||||
export default function Component() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Area Chart - Gradient</CardTitle>
|
||||
<CardDescription>Showing total visitors for the last 6 months</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ChartContainer config={chartConfig}>
|
||||
<AreaChart
|
||||
accessibilityLayer
|
||||
data={chartData}
|
||||
margin={{
|
||||
left: 12,
|
||||
right: 12,
|
||||
}}
|
||||
>
|
||||
<CartesianGrid vertical={false} />
|
||||
<XAxis
|
||||
dataKey="month"
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickMargin={8}
|
||||
tickFormatter={(value) => value.slice(0, 3)}
|
||||
/>
|
||||
<ChartTooltip cursor={false} content={<ChartTooltipContent />} />
|
||||
<defs>
|
||||
<linearGradient id="fillDesktop" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor="var(--color-desktop)" stopOpacity={0.8} />
|
||||
<stop offset="95%" stopColor="var(--color-desktop)" stopOpacity={0.1} />
|
||||
</linearGradient>
|
||||
<linearGradient id="fillMobile" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor="var(--color-mobile)" stopOpacity={0.8} />
|
||||
<stop offset="95%" stopColor="var(--color-mobile)" stopOpacity={0.1} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<Area
|
||||
dataKey="mobile"
|
||||
type="natural"
|
||||
fill="url(#fillMobile)"
|
||||
fillOpacity={0.4}
|
||||
stroke="var(--color-mobile)"
|
||||
stackId="a"
|
||||
/>
|
||||
<Area
|
||||
dataKey="desktop"
|
||||
type="natural"
|
||||
fill="url(#fillDesktop)"
|
||||
fillOpacity={0.4}
|
||||
stroke="var(--color-desktop)"
|
||||
stackId="a"
|
||||
/>
|
||||
</AreaChart>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<div className="flex w-full items-start gap-2 text-sm">
|
||||
<div className="grid gap-2">
|
||||
<div className="flex items-center gap-2 font-medium leading-none">
|
||||
Trending up by 5.2% this month <TrendingUp className="h-4 w-4" />
|
||||
</div>
|
||||
<div className="flex items-center gap-2 leading-none text-muted-foreground">
|
||||
January - June 2024
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { TrendingDown, TrendingUp } from 'lucide-react'
|
||||
import { Area, AreaChart, CartesianGrid, XAxis } from 'recharts'
|
||||
|
||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from 'ui'
|
||||
import {
|
||||
ChartConfig,
|
||||
ChartContainer,
|
||||
ChartLegend,
|
||||
ChartLegendContent,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
} from 'ui'
|
||||
|
||||
export const description = 'An area chart with icons'
|
||||
|
||||
const chartData = [
|
||||
{ month: 'January', desktop: 186, mobile: 80 },
|
||||
{ month: 'February', desktop: 305, mobile: 200 },
|
||||
{ month: 'March', desktop: 237, mobile: 120 },
|
||||
{ month: 'April', desktop: 73, mobile: 190 },
|
||||
{ month: 'May', desktop: 209, mobile: 130 },
|
||||
{ month: 'June', desktop: 214, mobile: 140 },
|
||||
]
|
||||
|
||||
const chartConfig = {
|
||||
desktop: {
|
||||
label: 'Desktop',
|
||||
color: 'hsl(var(--chart-1))',
|
||||
icon: TrendingDown,
|
||||
},
|
||||
mobile: {
|
||||
label: 'Mobile',
|
||||
color: 'hsl(var(--chart-2))',
|
||||
icon: TrendingUp,
|
||||
},
|
||||
} satisfies ChartConfig
|
||||
|
||||
export default function Component() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Area Chart - Icons</CardTitle>
|
||||
<CardDescription>Showing total visitors for the last 6 months</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ChartContainer config={chartConfig}>
|
||||
<AreaChart
|
||||
accessibilityLayer
|
||||
data={chartData}
|
||||
margin={{
|
||||
left: 12,
|
||||
right: 12,
|
||||
}}
|
||||
>
|
||||
<CartesianGrid vertical={false} />
|
||||
<XAxis
|
||||
dataKey="month"
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickMargin={8}
|
||||
tickFormatter={(value) => value.slice(0, 3)}
|
||||
/>
|
||||
<ChartTooltip cursor={false} content={<ChartTooltipContent indicator="line" />} />
|
||||
<Area
|
||||
dataKey="mobile"
|
||||
type="natural"
|
||||
fill="var(--color-mobile)"
|
||||
fillOpacity={0.4}
|
||||
stroke="var(--color-mobile)"
|
||||
stackId="a"
|
||||
/>
|
||||
<Area
|
||||
dataKey="desktop"
|
||||
type="natural"
|
||||
fill="var(--color-desktop)"
|
||||
fillOpacity={0.4}
|
||||
stroke="var(--color-desktop)"
|
||||
stackId="a"
|
||||
/>
|
||||
<ChartLegend content={<ChartLegendContent />} />
|
||||
</AreaChart>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<div className="flex w-full items-start gap-2 text-sm">
|
||||
<div className="grid gap-2">
|
||||
<div className="flex items-center gap-2 font-medium leading-none">
|
||||
Trending up by 5.2% this month <TrendingUp className="h-4 w-4" />
|
||||
</div>
|
||||
<div className="flex items-center gap-2 leading-none text-muted-foreground">
|
||||
January - June 2024
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -1,238 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import * as React from 'react'
|
||||
import { Area, AreaChart, CartesianGrid, XAxis } from 'recharts'
|
||||
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from 'ui'
|
||||
import {
|
||||
ChartConfig,
|
||||
ChartContainer,
|
||||
ChartLegend,
|
||||
ChartLegendContent,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
} from 'ui'
|
||||
import {
|
||||
Select_Shadcn_,
|
||||
SelectContent_Shadcn_,
|
||||
SelectItem_Shadcn_,
|
||||
SelectTrigger_Shadcn_,
|
||||
SelectValue_Shadcn_,
|
||||
} from 'ui'
|
||||
|
||||
export const description = 'An interactive area chart'
|
||||
|
||||
const chartData = [
|
||||
{ date: '2024-04-01', desktop: 222, mobile: 150 },
|
||||
{ date: '2024-04-02', desktop: 97, mobile: 180 },
|
||||
{ date: '2024-04-03', desktop: 167, mobile: 120 },
|
||||
{ date: '2024-04-04', desktop: 242, mobile: 260 },
|
||||
{ date: '2024-04-05', desktop: 373, mobile: 290 },
|
||||
{ date: '2024-04-06', desktop: 301, mobile: 340 },
|
||||
{ date: '2024-04-07', desktop: 245, mobile: 180 },
|
||||
{ date: '2024-04-08', desktop: 409, mobile: 320 },
|
||||
{ date: '2024-04-09', desktop: 59, mobile: 110 },
|
||||
{ date: '2024-04-10', desktop: 261, mobile: 190 },
|
||||
{ date: '2024-04-11', desktop: 327, mobile: 350 },
|
||||
{ date: '2024-04-12', desktop: 292, mobile: 210 },
|
||||
{ date: '2024-04-13', desktop: 342, mobile: 380 },
|
||||
{ date: '2024-04-14', desktop: 137, mobile: 220 },
|
||||
{ date: '2024-04-15', desktop: 120, mobile: 170 },
|
||||
{ date: '2024-04-16', desktop: 138, mobile: 190 },
|
||||
{ date: '2024-04-17', desktop: 446, mobile: 360 },
|
||||
{ date: '2024-04-18', desktop: 364, mobile: 410 },
|
||||
{ date: '2024-04-19', desktop: 243, mobile: 180 },
|
||||
{ date: '2024-04-20', desktop: 89, mobile: 150 },
|
||||
{ date: '2024-04-21', desktop: 137, mobile: 200 },
|
||||
{ date: '2024-04-22', desktop: 224, mobile: 170 },
|
||||
{ date: '2024-04-23', desktop: 138, mobile: 230 },
|
||||
{ date: '2024-04-24', desktop: 387, mobile: 290 },
|
||||
{ date: '2024-04-25', desktop: 215, mobile: 250 },
|
||||
{ date: '2024-04-26', desktop: 75, mobile: 130 },
|
||||
{ date: '2024-04-27', desktop: 383, mobile: 420 },
|
||||
{ date: '2024-04-28', desktop: 122, mobile: 180 },
|
||||
{ date: '2024-04-29', desktop: 315, mobile: 240 },
|
||||
{ date: '2024-04-30', desktop: 454, mobile: 380 },
|
||||
{ date: '2024-05-01', desktop: 165, mobile: 220 },
|
||||
{ date: '2024-05-02', desktop: 293, mobile: 310 },
|
||||
{ date: '2024-05-03', desktop: 247, mobile: 190 },
|
||||
{ date: '2024-05-04', desktop: 385, mobile: 420 },
|
||||
{ date: '2024-05-05', desktop: 481, mobile: 390 },
|
||||
{ date: '2024-05-06', desktop: 498, mobile: 520 },
|
||||
{ date: '2024-05-07', desktop: 388, mobile: 300 },
|
||||
{ date: '2024-05-08', desktop: 149, mobile: 210 },
|
||||
{ date: '2024-05-09', desktop: 227, mobile: 180 },
|
||||
{ date: '2024-05-10', desktop: 293, mobile: 330 },
|
||||
{ date: '2024-05-11', desktop: 335, mobile: 270 },
|
||||
{ date: '2024-05-12', desktop: 197, mobile: 240 },
|
||||
{ date: '2024-05-13', desktop: 197, mobile: 160 },
|
||||
{ date: '2024-05-14', desktop: 448, mobile: 490 },
|
||||
{ date: '2024-05-15', desktop: 473, mobile: 380 },
|
||||
{ date: '2024-05-16', desktop: 338, mobile: 400 },
|
||||
{ date: '2024-05-17', desktop: 499, mobile: 420 },
|
||||
{ date: '2024-05-18', desktop: 315, mobile: 350 },
|
||||
{ date: '2024-05-19', desktop: 235, mobile: 180 },
|
||||
{ date: '2024-05-20', desktop: 177, mobile: 230 },
|
||||
{ date: '2024-05-21', desktop: 82, mobile: 140 },
|
||||
{ date: '2024-05-22', desktop: 81, mobile: 120 },
|
||||
{ date: '2024-05-23', desktop: 252, mobile: 290 },
|
||||
{ date: '2024-05-24', desktop: 294, mobile: 220 },
|
||||
{ date: '2024-05-25', desktop: 201, mobile: 250 },
|
||||
{ date: '2024-05-26', desktop: 213, mobile: 170 },
|
||||
{ date: '2024-05-27', desktop: 420, mobile: 460 },
|
||||
{ date: '2024-05-28', desktop: 233, mobile: 190 },
|
||||
{ date: '2024-05-29', desktop: 78, mobile: 130 },
|
||||
{ date: '2024-05-30', desktop: 340, mobile: 280 },
|
||||
{ date: '2024-05-31', desktop: 178, mobile: 230 },
|
||||
{ date: '2024-06-01', desktop: 178, mobile: 200 },
|
||||
{ date: '2024-06-02', desktop: 470, mobile: 410 },
|
||||
{ date: '2024-06-03', desktop: 103, mobile: 160 },
|
||||
{ date: '2024-06-04', desktop: 439, mobile: 380 },
|
||||
{ date: '2024-06-05', desktop: 88, mobile: 140 },
|
||||
{ date: '2024-06-06', desktop: 294, mobile: 250 },
|
||||
{ date: '2024-06-07', desktop: 323, mobile: 370 },
|
||||
{ date: '2024-06-08', desktop: 385, mobile: 320 },
|
||||
{ date: '2024-06-09', desktop: 438, mobile: 480 },
|
||||
{ date: '2024-06-10', desktop: 155, mobile: 200 },
|
||||
{ date: '2024-06-11', desktop: 92, mobile: 150 },
|
||||
{ date: '2024-06-12', desktop: 492, mobile: 420 },
|
||||
{ date: '2024-06-13', desktop: 81, mobile: 130 },
|
||||
{ date: '2024-06-14', desktop: 426, mobile: 380 },
|
||||
{ date: '2024-06-15', desktop: 307, mobile: 350 },
|
||||
{ date: '2024-06-16', desktop: 371, mobile: 310 },
|
||||
{ date: '2024-06-17', desktop: 475, mobile: 520 },
|
||||
{ date: '2024-06-18', desktop: 107, mobile: 170 },
|
||||
{ date: '2024-06-19', desktop: 341, mobile: 290 },
|
||||
{ date: '2024-06-20', desktop: 408, mobile: 450 },
|
||||
{ date: '2024-06-21', desktop: 169, mobile: 210 },
|
||||
{ date: '2024-06-22', desktop: 317, mobile: 270 },
|
||||
{ date: '2024-06-23', desktop: 480, mobile: 530 },
|
||||
{ date: '2024-06-24', desktop: 132, mobile: 180 },
|
||||
{ date: '2024-06-25', desktop: 141, mobile: 190 },
|
||||
{ date: '2024-06-26', desktop: 434, mobile: 380 },
|
||||
{ date: '2024-06-27', desktop: 448, mobile: 490 },
|
||||
{ date: '2024-06-28', desktop: 149, mobile: 200 },
|
||||
{ date: '2024-06-29', desktop: 103, mobile: 160 },
|
||||
{ date: '2024-06-30', desktop: 446, mobile: 400 },
|
||||
]
|
||||
|
||||
const chartConfig = {
|
||||
visitors: {
|
||||
label: 'Visitors',
|
||||
},
|
||||
desktop: {
|
||||
label: 'Desktop',
|
||||
color: 'hsl(var(--chart-1))',
|
||||
},
|
||||
mobile: {
|
||||
label: 'Mobile',
|
||||
color: 'hsl(var(--chart-2))',
|
||||
},
|
||||
} satisfies ChartConfig
|
||||
|
||||
export default function Component() {
|
||||
const [timeRange, setTimeRange] = React.useState('90d')
|
||||
|
||||
const filteredData = chartData.filter((item) => {
|
||||
const date = new Date(item.date)
|
||||
const now = new Date()
|
||||
let daysToSubtract = 90
|
||||
if (timeRange === '30d') {
|
||||
daysToSubtract = 30
|
||||
} else if (timeRange === '7d') {
|
||||
daysToSubtract = 7
|
||||
}
|
||||
now.setDate(now.getDate() - daysToSubtract)
|
||||
return date >= now
|
||||
})
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="flex items-center gap-2 space-y-0 border-b py-5 sm:flex-row">
|
||||
<div className="grid flex-1 gap-1 text-center sm:text-left">
|
||||
<CardTitle>Area Chart - Interactive</CardTitle>
|
||||
<CardDescription>Showing total visitors for the last 3 months</CardDescription>
|
||||
</div>
|
||||
<Select_Shadcn_ value={timeRange} onValueChange={setTimeRange}>
|
||||
<SelectTrigger_Shadcn_
|
||||
className="w-[160px] rounded-lg sm:ml-auto"
|
||||
aria-label="Select a value"
|
||||
>
|
||||
<SelectValue_Shadcn_ placeholder="Last 3 months" />
|
||||
</SelectTrigger_Shadcn_>
|
||||
<SelectContent_Shadcn_ className="rounded-xl">
|
||||
<SelectItem_Shadcn_ value="90d" className="rounded-lg">
|
||||
Last 3 months
|
||||
</SelectItem_Shadcn_>
|
||||
<SelectItem_Shadcn_ value="30d" className="rounded-lg">
|
||||
Last 30 days
|
||||
</SelectItem_Shadcn_>
|
||||
<SelectItem_Shadcn_ value="7d" className="rounded-lg">
|
||||
Last 7 days
|
||||
</SelectItem_Shadcn_>
|
||||
</SelectContent_Shadcn_>
|
||||
</Select_Shadcn_>
|
||||
</CardHeader>
|
||||
<CardContent className="px-2 pt-4 sm:px-6 sm:pt-6">
|
||||
<ChartContainer config={chartConfig} className="aspect-auto h-[250px] w-full">
|
||||
<AreaChart data={filteredData}>
|
||||
<defs>
|
||||
<linearGradient id="fillDesktop" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor="var(--color-desktop)" stopOpacity={0.8} />
|
||||
<stop offset="95%" stopColor="var(--color-desktop)" stopOpacity={0.1} />
|
||||
</linearGradient>
|
||||
<linearGradient id="fillMobile" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor="var(--color-mobile)" stopOpacity={0.8} />
|
||||
<stop offset="95%" stopColor="var(--color-mobile)" stopOpacity={0.1} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<CartesianGrid vertical={false} />
|
||||
<XAxis
|
||||
dataKey="date"
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickMargin={8}
|
||||
minTickGap={32}
|
||||
tickFormatter={(value) => {
|
||||
const date = new Date(value)
|
||||
return date.toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
})
|
||||
}}
|
||||
/>
|
||||
<ChartTooltip
|
||||
cursor={false}
|
||||
content={
|
||||
<ChartTooltipContent
|
||||
labelFormatter={(value) => {
|
||||
return new Date(value).toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
})
|
||||
}}
|
||||
indicator="dot"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Area
|
||||
dataKey="mobile"
|
||||
type="natural"
|
||||
fill="url(#fillMobile)"
|
||||
stroke="var(--color-mobile)"
|
||||
stackId="a"
|
||||
/>
|
||||
<Area
|
||||
dataKey="desktop"
|
||||
type="natural"
|
||||
fill="url(#fillDesktop)"
|
||||
stroke="var(--color-desktop)"
|
||||
stackId="a"
|
||||
/>
|
||||
<ChartLegend content={<ChartLegendContent />} />
|
||||
</AreaChart>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { TrendingUp } from 'lucide-react'
|
||||
import { Area, AreaChart, CartesianGrid, XAxis } from 'recharts'
|
||||
|
||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from 'ui'
|
||||
import {
|
||||
ChartConfig,
|
||||
ChartContainer,
|
||||
ChartLegend,
|
||||
ChartLegendContent,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
} from 'ui'
|
||||
|
||||
export const description = 'An area chart with a legend'
|
||||
|
||||
const chartData = [
|
||||
{ month: 'January', desktop: 186, mobile: 80 },
|
||||
{ month: 'February', desktop: 305, mobile: 200 },
|
||||
{ month: 'March', desktop: 237, mobile: 120 },
|
||||
{ month: 'April', desktop: 73, mobile: 190 },
|
||||
{ month: 'May', desktop: 209, mobile: 130 },
|
||||
{ month: 'June', desktop: 214, mobile: 140 },
|
||||
]
|
||||
|
||||
const chartConfig = {
|
||||
desktop: {
|
||||
label: 'Desktop',
|
||||
color: 'hsl(var(--chart-1))',
|
||||
},
|
||||
mobile: {
|
||||
label: 'Mobile',
|
||||
color: 'hsl(var(--chart-2))',
|
||||
},
|
||||
} satisfies ChartConfig
|
||||
|
||||
export default function Component() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Area Chart - Legend</CardTitle>
|
||||
<CardDescription>Showing total visitors for the last 6 months</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ChartContainer config={chartConfig}>
|
||||
<AreaChart
|
||||
accessibilityLayer
|
||||
data={chartData}
|
||||
margin={{
|
||||
left: 12,
|
||||
right: 12,
|
||||
}}
|
||||
>
|
||||
<CartesianGrid vertical={false} />
|
||||
<XAxis
|
||||
dataKey="month"
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickMargin={8}
|
||||
tickFormatter={(value) => value.slice(0, 3)}
|
||||
/>
|
||||
<ChartTooltip cursor={false} content={<ChartTooltipContent indicator="line" />} />
|
||||
<Area
|
||||
dataKey="mobile"
|
||||
type="natural"
|
||||
fill="var(--color-mobile)"
|
||||
fillOpacity={0.4}
|
||||
stroke="var(--color-mobile)"
|
||||
stackId="a"
|
||||
/>
|
||||
<Area
|
||||
dataKey="desktop"
|
||||
type="natural"
|
||||
fill="var(--color-desktop)"
|
||||
fillOpacity={0.4}
|
||||
stroke="var(--color-desktop)"
|
||||
stackId="a"
|
||||
/>
|
||||
<ChartLegend content={<ChartLegendContent />} />
|
||||
</AreaChart>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<div className="flex w-full items-start gap-2 text-sm">
|
||||
<div className="grid gap-2">
|
||||
<div className="flex items-center gap-2 font-medium leading-none">
|
||||
Trending up by 5.2% this month <TrendingUp className="h-4 w-4" />
|
||||
</div>
|
||||
<div className="flex items-center gap-2 leading-none text-muted-foreground">
|
||||
January - June 2024
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { TrendingUp } from 'lucide-react'
|
||||
import { Area, AreaChart, CartesianGrid, XAxis } from 'recharts'
|
||||
|
||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from 'ui'
|
||||
import { ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent } from 'ui'
|
||||
|
||||
export const description = 'A linear area chart'
|
||||
|
||||
const chartData = [
|
||||
{ month: 'January', desktop: 186 },
|
||||
{ month: 'February', desktop: 305 },
|
||||
{ month: 'March', desktop: 237 },
|
||||
{ month: 'April', desktop: 73 },
|
||||
{ month: 'May', desktop: 209 },
|
||||
{ month: 'June', desktop: 214 },
|
||||
]
|
||||
|
||||
const chartConfig = {
|
||||
desktop: {
|
||||
label: 'Desktop',
|
||||
color: 'hsl(var(--chart-1))',
|
||||
},
|
||||
} satisfies ChartConfig
|
||||
|
||||
export default function Component() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Area Chart - Linear</CardTitle>
|
||||
<CardDescription>Showing total visitors for the last 6 months</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ChartContainer config={chartConfig}>
|
||||
<AreaChart
|
||||
accessibilityLayer
|
||||
data={chartData}
|
||||
margin={{
|
||||
left: 12,
|
||||
right: 12,
|
||||
}}
|
||||
>
|
||||
<CartesianGrid vertical={false} />
|
||||
<XAxis
|
||||
dataKey="month"
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickMargin={8}
|
||||
tickFormatter={(value) => value.slice(0, 3)}
|
||||
/>
|
||||
<ChartTooltip
|
||||
cursor={false}
|
||||
content={<ChartTooltipContent indicator="dot" hideLabel />}
|
||||
/>
|
||||
<Area
|
||||
dataKey="desktop"
|
||||
type="linear"
|
||||
fill="var(--color-desktop)"
|
||||
fillOpacity={0.4}
|
||||
stroke="var(--color-desktop)"
|
||||
/>
|
||||
</AreaChart>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<div className="flex w-full items-start gap-2 text-sm">
|
||||
<div className="grid gap-2">
|
||||
<div className="flex items-center gap-2 font-medium leading-none">
|
||||
Trending up by 5.2% this month <TrendingUp className="h-4 w-4" />
|
||||
</div>
|
||||
<div className="flex items-center gap-2 leading-none text-muted-foreground">
|
||||
January - June 2024
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { TrendingUp } from 'lucide-react'
|
||||
import { Area, AreaChart, CartesianGrid, XAxis } from 'recharts'
|
||||
|
||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from 'ui'
|
||||
import { ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent } from 'ui'
|
||||
|
||||
export const description = 'A stacked area chart with expand stacking'
|
||||
|
||||
const chartData = [
|
||||
{ month: 'January', desktop: 186, mobile: 80, other: 45 },
|
||||
{ month: 'February', desktop: 305, mobile: 200, other: 100 },
|
||||
{ month: 'March', desktop: 237, mobile: 120, other: 150 },
|
||||
{ month: 'April', desktop: 73, mobile: 190, other: 50 },
|
||||
{ month: 'May', desktop: 209, mobile: 130, other: 100 },
|
||||
{ month: 'June', desktop: 214, mobile: 140, other: 160 },
|
||||
]
|
||||
|
||||
const chartConfig = {
|
||||
desktop: {
|
||||
label: 'Desktop',
|
||||
color: 'hsl(var(--chart-1))',
|
||||
},
|
||||
mobile: {
|
||||
label: 'Mobile',
|
||||
color: 'hsl(var(--chart-2))',
|
||||
},
|
||||
other: {
|
||||
label: 'Other',
|
||||
color: 'hsl(var(--chart-3))',
|
||||
},
|
||||
} satisfies ChartConfig
|
||||
|
||||
export default function Component() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Area Chart - Stacked Expanded</CardTitle>
|
||||
<CardDescription>Showing total visitors for the last 6months</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ChartContainer config={chartConfig}>
|
||||
<AreaChart
|
||||
accessibilityLayer
|
||||
data={chartData}
|
||||
margin={{
|
||||
left: 12,
|
||||
right: 12,
|
||||
top: 12,
|
||||
}}
|
||||
stackOffset="expand"
|
||||
>
|
||||
<CartesianGrid vertical={false} />
|
||||
<XAxis
|
||||
dataKey="month"
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickMargin={8}
|
||||
tickFormatter={(value) => value.slice(0, 3)}
|
||||
/>
|
||||
<ChartTooltip cursor={false} content={<ChartTooltipContent indicator="line" />} />
|
||||
<Area
|
||||
dataKey="other"
|
||||
type="natural"
|
||||
fill="var(--color-other)"
|
||||
fillOpacity={0.1}
|
||||
stroke="var(--color-other)"
|
||||
stackId="a"
|
||||
/>
|
||||
<Area
|
||||
dataKey="mobile"
|
||||
type="natural"
|
||||
fill="var(--color-mobile)"
|
||||
fillOpacity={0.4}
|
||||
stroke="var(--color-mobile)"
|
||||
stackId="a"
|
||||
/>
|
||||
<Area
|
||||
dataKey="desktop"
|
||||
type="natural"
|
||||
fill="var(--color-desktop)"
|
||||
fillOpacity={0.4}
|
||||
stroke="var(--color-desktop)"
|
||||
stackId="a"
|
||||
/>
|
||||
</AreaChart>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<div className="flex w-full items-start gap-2 text-sm">
|
||||
<div className="grid gap-2">
|
||||
<div className="flex items-center gap-2 font-medium leading-none">
|
||||
Trending up by 5.2% this month <TrendingUp className="h-4 w-4" />
|
||||
</div>
|
||||
<div className="flex items-center gap-2 leading-none text-muted-foreground">
|
||||
January - June 2024
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { TrendingUp } from 'lucide-react'
|
||||
import { Area, AreaChart, CartesianGrid, XAxis } from 'recharts'
|
||||
|
||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from 'ui'
|
||||
import { ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent } from 'ui'
|
||||
|
||||
export const description = 'A stacked area chart'
|
||||
|
||||
const chartData = [
|
||||
{ month: 'January', desktop: 186, mobile: 80 },
|
||||
{ month: 'February', desktop: 305, mobile: 200 },
|
||||
{ month: 'March', desktop: 237, mobile: 120 },
|
||||
{ month: 'April', desktop: 73, mobile: 190 },
|
||||
{ month: 'May', desktop: 209, mobile: 130 },
|
||||
{ month: 'June', desktop: 214, mobile: 140 },
|
||||
]
|
||||
|
||||
const chartConfig = {
|
||||
desktop: {
|
||||
label: 'Desktop',
|
||||
color: 'hsl(var(--chart-1))',
|
||||
},
|
||||
mobile: {
|
||||
label: 'Mobile',
|
||||
color: 'hsl(var(--chart-2))',
|
||||
},
|
||||
} satisfies ChartConfig
|
||||
|
||||
export default function Component() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Area Chart - Stacked</CardTitle>
|
||||
<CardDescription>Showing total visitors for the last 6 months</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ChartContainer config={chartConfig}>
|
||||
<AreaChart
|
||||
accessibilityLayer
|
||||
data={chartData}
|
||||
margin={{
|
||||
left: 12,
|
||||
right: 12,
|
||||
}}
|
||||
>
|
||||
<CartesianGrid vertical={false} />
|
||||
<XAxis
|
||||
dataKey="month"
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickMargin={8}
|
||||
tickFormatter={(value) => value.slice(0, 3)}
|
||||
/>
|
||||
<ChartTooltip cursor={false} content={<ChartTooltipContent indicator="dot" />} />
|
||||
<Area
|
||||
dataKey="mobile"
|
||||
type="natural"
|
||||
fill="var(--color-mobile)"
|
||||
fillOpacity={0.4}
|
||||
stroke="var(--color-mobile)"
|
||||
stackId="a"
|
||||
/>
|
||||
<Area
|
||||
dataKey="desktop"
|
||||
type="natural"
|
||||
fill="var(--color-desktop)"
|
||||
fillOpacity={0.4}
|
||||
stroke="var(--color-desktop)"
|
||||
stackId="a"
|
||||
/>
|
||||
</AreaChart>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<div className="flex w-full items-start gap-2 text-sm">
|
||||
<div className="grid gap-2">
|
||||
<div className="flex items-center gap-2 font-medium leading-none">
|
||||
Trending up by 5.2% this month <TrendingUp className="h-4 w-4" />
|
||||
</div>
|
||||
<div className="flex items-center gap-2 leading-none text-muted-foreground">
|
||||
January - June 2024
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { Activity, TrendingUp } from 'lucide-react'
|
||||
import { Area, AreaChart, CartesianGrid, XAxis } from 'recharts'
|
||||
|
||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from 'ui'
|
||||
import { ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent } from 'ui'
|
||||
|
||||
export const description = 'A step area chart'
|
||||
|
||||
const chartData = [
|
||||
{ month: 'January', desktop: 186 },
|
||||
{ month: 'February', desktop: 305 },
|
||||
{ month: 'March', desktop: 237 },
|
||||
{ month: 'April', desktop: 73 },
|
||||
{ month: 'May', desktop: 209 },
|
||||
{ month: 'June', desktop: 214 },
|
||||
]
|
||||
|
||||
const chartConfig = {
|
||||
desktop: {
|
||||
label: 'Desktop',
|
||||
color: 'hsl(var(--chart-1))',
|
||||
icon: Activity,
|
||||
},
|
||||
} satisfies ChartConfig
|
||||
|
||||
export default function Component() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Area Chart - Step</CardTitle>
|
||||
<CardDescription>Showing total visitors for the last 6 months</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ChartContainer config={chartConfig}>
|
||||
<AreaChart
|
||||
accessibilityLayer
|
||||
data={chartData}
|
||||
margin={{
|
||||
left: 12,
|
||||
right: 12,
|
||||
}}
|
||||
>
|
||||
<CartesianGrid vertical={false} />
|
||||
<XAxis
|
||||
dataKey="month"
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickMargin={8}
|
||||
tickFormatter={(value) => value.slice(0, 3)}
|
||||
/>
|
||||
<ChartTooltip cursor={false} content={<ChartTooltipContent hideLabel />} />
|
||||
<Area
|
||||
dataKey="desktop"
|
||||
type="step"
|
||||
fill="var(--color-desktop)"
|
||||
fillOpacity={0.4}
|
||||
stroke="var(--color-desktop)"
|
||||
/>
|
||||
</AreaChart>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<div className="flex w-full items-start gap-2 text-sm">
|
||||
<div className="grid gap-2">
|
||||
<div className="flex items-center gap-2 font-medium leading-none">
|
||||
Trending up by 5.2% this month <TrendingUp className="h-4 w-4" />
|
||||
</div>
|
||||
<div className="flex items-center gap-2 leading-none text-muted-foreground">
|
||||
January - June 2024
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { TrendingUp } from 'lucide-react'
|
||||
import { Bar, BarChart, CartesianGrid, Rectangle, XAxis } from 'recharts'
|
||||
|
||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from 'ui'
|
||||
import { ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent } from 'ui'
|
||||
|
||||
export const description = 'A bar chart with an active bar'
|
||||
|
||||
const chartData = [
|
||||
{ browser: 'chrome', visitors: 187, fill: 'var(--color-chrome)' },
|
||||
{ browser: 'safari', visitors: 200, fill: 'var(--color-safari)' },
|
||||
{ browser: 'firefox', visitors: 275, fill: 'var(--color-firefox)' },
|
||||
{ browser: 'edge', visitors: 173, fill: 'var(--color-edge)' },
|
||||
{ browser: 'other', visitors: 90, fill: 'var(--color-other)' },
|
||||
]
|
||||
|
||||
const chartConfig = {
|
||||
visitors: {
|
||||
label: 'Visitors',
|
||||
},
|
||||
chrome: {
|
||||
label: 'Chrome',
|
||||
color: 'hsl(var(--chart-1))',
|
||||
},
|
||||
safari: {
|
||||
label: 'Safari',
|
||||
color: 'hsl(var(--chart-2))',
|
||||
},
|
||||
firefox: {
|
||||
label: 'Firefox',
|
||||
color: 'hsl(var(--chart-3))',
|
||||
},
|
||||
edge: {
|
||||
label: 'Edge',
|
||||
color: 'hsl(var(--chart-4))',
|
||||
},
|
||||
other: {
|
||||
label: 'Other',
|
||||
color: 'hsl(var(--chart-5))',
|
||||
},
|
||||
} satisfies ChartConfig
|
||||
|
||||
export default function Component() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Bar Chart - Active</CardTitle>
|
||||
<CardDescription>January - June 2024</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ChartContainer config={chartConfig}>
|
||||
<BarChart accessibilityLayer data={chartData}>
|
||||
<CartesianGrid vertical={false} />
|
||||
<XAxis
|
||||
dataKey="browser"
|
||||
tickLine={false}
|
||||
tickMargin={10}
|
||||
axisLine={false}
|
||||
tickFormatter={(value) => chartConfig[value as keyof typeof chartConfig]?.label}
|
||||
/>
|
||||
<ChartTooltip cursor={false} content={<ChartTooltipContent hideLabel />} />
|
||||
<Bar
|
||||
dataKey="visitors"
|
||||
strokeWidth={2}
|
||||
radius={8}
|
||||
activeIndex={2}
|
||||
activeBar={({ ...props }) => {
|
||||
return (
|
||||
<Rectangle
|
||||
{...props}
|
||||
fillOpacity={0.8}
|
||||
stroke={props.payload.fill}
|
||||
strokeDasharray={4}
|
||||
strokeDashoffset={4}
|
||||
/>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</BarChart>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
<CardFooter className="flex-col items-start gap-2 text-sm">
|
||||
<div className="flex gap-2 font-medium leading-none">
|
||||
Trending up by 5.2% this month <TrendingUp className="h-4 w-4" />
|
||||
</div>
|
||||
<div className="leading-none text-muted-foreground">
|
||||
Showing total visitors for the last 6 months
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { TrendingUp } from 'lucide-react'
|
||||
import { Bar, BarChart, CartesianGrid, XAxis } from 'recharts'
|
||||
|
||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from 'ui'
|
||||
import { ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent } from 'ui'
|
||||
|
||||
export const description = 'A bar chart'
|
||||
|
||||
const chartData = [
|
||||
{ month: 'January', desktop: 186 },
|
||||
{ month: 'February', desktop: 305 },
|
||||
{ month: 'March', desktop: 237 },
|
||||
{ month: 'April', desktop: 73 },
|
||||
{ month: 'May', desktop: 209 },
|
||||
{ month: 'June', desktop: 214 },
|
||||
]
|
||||
|
||||
const chartConfig = {
|
||||
desktop: {
|
||||
label: 'Desktop',
|
||||
color: 'hsl(var(--chart-1))',
|
||||
},
|
||||
} satisfies ChartConfig
|
||||
|
||||
export default function Component() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Bar Chart</CardTitle>
|
||||
<CardDescription>January - June 2024</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ChartContainer config={chartConfig}>
|
||||
<BarChart accessibilityLayer data={chartData}>
|
||||
<CartesianGrid vertical={false} />
|
||||
<XAxis
|
||||
dataKey="month"
|
||||
tickLine={false}
|
||||
tickMargin={10}
|
||||
axisLine={false}
|
||||
tickFormatter={(value) => value.slice(0, 3)}
|
||||
/>
|
||||
<ChartTooltip cursor={false} content={<ChartTooltipContent hideLabel />} />
|
||||
<Bar dataKey="desktop" fill="var(--color-desktop)" radius={8} />
|
||||
</BarChart>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
<CardFooter className="flex-col items-start gap-2 text-sm">
|
||||
<div className="flex gap-2 font-medium leading-none">
|
||||
Trending up by 5.2% this month <TrendingUp className="h-4 w-4" />
|
||||
</div>
|
||||
<div className="leading-none text-muted-foreground">
|
||||
Showing total visitors for the last 6 months
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { TrendingUp } from 'lucide-react'
|
||||
import { Bar, BarChart, XAxis, YAxis } from 'recharts'
|
||||
|
||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from 'ui'
|
||||
import { ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent } from 'ui'
|
||||
|
||||
export const description = 'A horizontal bar chart'
|
||||
|
||||
const chartData = [
|
||||
{ month: 'January', desktop: 186 },
|
||||
{ month: 'February', desktop: 305 },
|
||||
{ month: 'March', desktop: 237 },
|
||||
{ month: 'April', desktop: 73 },
|
||||
{ month: 'May', desktop: 209 },
|
||||
{ month: 'June', desktop: 214 },
|
||||
]
|
||||
|
||||
const chartConfig = {
|
||||
desktop: {
|
||||
label: 'Desktop',
|
||||
color: 'hsl(var(--chart-1))',
|
||||
},
|
||||
} satisfies ChartConfig
|
||||
|
||||
export default function Component() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Bar Chart - Horizontal</CardTitle>
|
||||
<CardDescription>January - June 2024</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ChartContainer config={chartConfig}>
|
||||
<BarChart
|
||||
accessibilityLayer
|
||||
data={chartData}
|
||||
layout="vertical"
|
||||
margin={{
|
||||
left: -20,
|
||||
}}
|
||||
>
|
||||
<XAxis type="number" dataKey="desktop" hide />
|
||||
<YAxis
|
||||
dataKey="month"
|
||||
type="category"
|
||||
tickLine={false}
|
||||
tickMargin={10}
|
||||
axisLine={false}
|
||||
tickFormatter={(value) => value.slice(0, 3)}
|
||||
/>
|
||||
<ChartTooltip cursor={false} content={<ChartTooltipContent hideLabel />} />
|
||||
<Bar dataKey="desktop" fill="var(--color-desktop)" radius={5} />
|
||||
</BarChart>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
<CardFooter className="flex-col items-start gap-2 text-sm">
|
||||
<div className="flex gap-2 font-medium leading-none">
|
||||
Trending up by 5.2% this month <TrendingUp className="h-4 w-4" />
|
||||
</div>
|
||||
<div className="leading-none text-muted-foreground">
|
||||
Showing total visitors for the last 6 months
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { TrendingUp } from 'lucide-react'
|
||||
import { Bar, BarChart, CartesianGrid, LabelList, XAxis, YAxis } from 'recharts'
|
||||
|
||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from 'ui'
|
||||
import { ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent } from 'ui'
|
||||
|
||||
export const description = 'A bar chart with a custom label'
|
||||
|
||||
const chartData = [
|
||||
{ month: 'January', desktop: 186, mobile: 80 },
|
||||
{ month: 'February', desktop: 305, mobile: 200 },
|
||||
{ month: 'March', desktop: 237, mobile: 120 },
|
||||
{ month: 'April', desktop: 73, mobile: 190 },
|
||||
{ month: 'May', desktop: 209, mobile: 130 },
|
||||
{ month: 'June', desktop: 214, mobile: 140 },
|
||||
]
|
||||
|
||||
const chartConfig = {
|
||||
desktop: {
|
||||
label: 'Desktop',
|
||||
color: 'hsl(var(--chart-1))',
|
||||
},
|
||||
mobile: {
|
||||
label: 'Mobile',
|
||||
color: 'hsl(var(--chart-2))',
|
||||
},
|
||||
label: {
|
||||
color: 'hsl(var(--background))',
|
||||
},
|
||||
} satisfies ChartConfig
|
||||
|
||||
export default function Component() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Bar Chart - Custom Label</CardTitle>
|
||||
<CardDescription>January - June 2024</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ChartContainer config={chartConfig}>
|
||||
<BarChart
|
||||
accessibilityLayer
|
||||
data={chartData}
|
||||
layout="vertical"
|
||||
margin={{
|
||||
right: 16,
|
||||
}}
|
||||
>
|
||||
<CartesianGrid horizontal={false} />
|
||||
<YAxis
|
||||
dataKey="month"
|
||||
type="category"
|
||||
tickLine={false}
|
||||
tickMargin={10}
|
||||
axisLine={false}
|
||||
tickFormatter={(value) => value.slice(0, 3)}
|
||||
hide
|
||||
/>
|
||||
<XAxis dataKey="desktop" type="number" hide />
|
||||
<ChartTooltip cursor={false} content={<ChartTooltipContent indicator="line" />} />
|
||||
<Bar dataKey="desktop" layout="vertical" fill="var(--color-desktop)" radius={4}>
|
||||
<LabelList
|
||||
dataKey="month"
|
||||
position="insideLeft"
|
||||
offset={8}
|
||||
className="fill-[--color-label]"
|
||||
fontSize={12}
|
||||
/>
|
||||
<LabelList
|
||||
dataKey="desktop"
|
||||
position="right"
|
||||
offset={8}
|
||||
className="fill-foreground"
|
||||
fontSize={12}
|
||||
/>
|
||||
</Bar>
|
||||
</BarChart>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
<CardFooter className="flex-col items-start gap-2 text-sm">
|
||||
<div className="flex gap-2 font-medium leading-none">
|
||||
Trending up by 5.2% this month <TrendingUp className="h-4 w-4" />
|
||||
</div>
|
||||
<div className="leading-none text-muted-foreground">
|
||||
Showing total visitors for the last 6 months
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { TrendingUp } from 'lucide-react'
|
||||
import { Bar, BarChart, CartesianGrid, LabelList, XAxis } from 'recharts'
|
||||
|
||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from 'ui'
|
||||
import { ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent } from 'ui'
|
||||
|
||||
export const description = 'A bar chart with a label'
|
||||
|
||||
const chartData = [
|
||||
{ month: 'January', desktop: 186 },
|
||||
{ month: 'February', desktop: 305 },
|
||||
{ month: 'March', desktop: 237 },
|
||||
{ month: 'April', desktop: 73 },
|
||||
{ month: 'May', desktop: 209 },
|
||||
{ month: 'June', desktop: 214 },
|
||||
]
|
||||
|
||||
const chartConfig = {
|
||||
desktop: {
|
||||
label: 'Desktop',
|
||||
color: 'hsl(var(--chart-1))',
|
||||
},
|
||||
} satisfies ChartConfig
|
||||
|
||||
export default function Component() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Bar Chart - Label</CardTitle>
|
||||
<CardDescription>January - June 2024</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ChartContainer config={chartConfig}>
|
||||
<BarChart
|
||||
accessibilityLayer
|
||||
data={chartData}
|
||||
margin={{
|
||||
top: 20,
|
||||
}}
|
||||
>
|
||||
<CartesianGrid vertical={false} />
|
||||
<XAxis
|
||||
dataKey="month"
|
||||
tickLine={false}
|
||||
tickMargin={10}
|
||||
axisLine={false}
|
||||
tickFormatter={(value) => value.slice(0, 3)}
|
||||
/>
|
||||
<ChartTooltip cursor={false} content={<ChartTooltipContent hideLabel />} />
|
||||
<Bar dataKey="desktop" fill="var(--color-desktop)" radius={8}>
|
||||
<LabelList position="top" offset={12} className="fill-foreground" fontSize={12} />
|
||||
</Bar>
|
||||
</BarChart>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
<CardFooter className="flex-col items-start gap-2 text-sm">
|
||||
<div className="flex gap-2 font-medium leading-none">
|
||||
Trending up by 5.2% this month <TrendingUp className="h-4 w-4" />
|
||||
</div>
|
||||
<div className="leading-none text-muted-foreground">
|
||||
Showing total visitors for the last 6 months
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { TrendingUp } from 'lucide-react'
|
||||
import { Bar, BarChart, XAxis, YAxis } from 'recharts'
|
||||
|
||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from 'ui'
|
||||
import { ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent } from 'ui'
|
||||
|
||||
export const description = 'A mixed bar chart'
|
||||
|
||||
const chartData = [
|
||||
{ browser: 'chrome', visitors: 275, fill: 'var(--color-chrome)' },
|
||||
{ browser: 'safari', visitors: 200, fill: 'var(--color-safari)' },
|
||||
{ browser: 'firefox', visitors: 187, fill: 'var(--color-firefox)' },
|
||||
{ browser: 'edge', visitors: 173, fill: 'var(--color-edge)' },
|
||||
{ browser: 'other', visitors: 90, fill: 'var(--color-other)' },
|
||||
]
|
||||
|
||||
const chartConfig = {
|
||||
visitors: {
|
||||
label: 'Visitors',
|
||||
},
|
||||
chrome: {
|
||||
label: 'Chrome',
|
||||
color: 'hsl(var(--chart-1))',
|
||||
},
|
||||
safari: {
|
||||
label: 'Safari',
|
||||
color: 'hsl(var(--chart-2))',
|
||||
},
|
||||
firefox: {
|
||||
label: 'Firefox',
|
||||
color: 'hsl(var(--chart-3))',
|
||||
},
|
||||
edge: {
|
||||
label: 'Edge',
|
||||
color: 'hsl(var(--chart-4))',
|
||||
},
|
||||
other: {
|
||||
label: 'Other',
|
||||
color: 'hsl(var(--chart-5))',
|
||||
},
|
||||
} satisfies ChartConfig
|
||||
|
||||
export default function Component() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Bar Chart - Mixed</CardTitle>
|
||||
<CardDescription>January - June 2024</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ChartContainer config={chartConfig}>
|
||||
<BarChart
|
||||
accessibilityLayer
|
||||
data={chartData}
|
||||
layout="vertical"
|
||||
margin={{
|
||||
left: 0,
|
||||
}}
|
||||
>
|
||||
<YAxis
|
||||
dataKey="browser"
|
||||
type="category"
|
||||
tickLine={false}
|
||||
tickMargin={10}
|
||||
axisLine={false}
|
||||
tickFormatter={(value) => chartConfig[value as keyof typeof chartConfig]?.label}
|
||||
/>
|
||||
<XAxis dataKey="visitors" type="number" hide />
|
||||
<ChartTooltip cursor={false} content={<ChartTooltipContent hideLabel />} />
|
||||
<Bar dataKey="visitors" layout="vertical" radius={5} />
|
||||
</BarChart>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
<CardFooter className="flex-col items-start gap-2 text-sm">
|
||||
<div className="flex gap-2 font-medium leading-none">
|
||||
Trending up by 5.2% this month <TrendingUp className="h-4 w-4" />
|
||||
</div>
|
||||
<div className="leading-none text-muted-foreground">
|
||||
Showing total visitors for the last 6 months
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { TrendingUp } from 'lucide-react'
|
||||
import { Bar, BarChart, CartesianGrid, XAxis } from 'recharts'
|
||||
|
||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from 'ui'
|
||||
import { ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent } from 'ui'
|
||||
|
||||
export const description = 'A multiple bar chart'
|
||||
|
||||
const chartData = [
|
||||
{ month: 'January', desktop: 186, mobile: 80 },
|
||||
{ month: 'February', desktop: 305, mobile: 200 },
|
||||
{ month: 'March', desktop: 237, mobile: 120 },
|
||||
{ month: 'April', desktop: 73, mobile: 190 },
|
||||
{ month: 'May', desktop: 209, mobile: 130 },
|
||||
{ month: 'June', desktop: 214, mobile: 140 },
|
||||
]
|
||||
|
||||
const chartConfig = {
|
||||
desktop: {
|
||||
label: 'Desktop',
|
||||
color: 'hsl(var(--chart-1))',
|
||||
},
|
||||
mobile: {
|
||||
label: 'Mobile',
|
||||
color: 'hsl(var(--chart-2))',
|
||||
},
|
||||
} satisfies ChartConfig
|
||||
|
||||
export default function Component() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Bar Chart - Multiple</CardTitle>
|
||||
<CardDescription>January - June 2024</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ChartContainer config={chartConfig}>
|
||||
<BarChart accessibilityLayer data={chartData}>
|
||||
<CartesianGrid vertical={false} />
|
||||
<XAxis
|
||||
dataKey="month"
|
||||
tickLine={false}
|
||||
tickMargin={10}
|
||||
axisLine={false}
|
||||
tickFormatter={(value) => value.slice(0, 3)}
|
||||
/>
|
||||
<ChartTooltip cursor={false} content={<ChartTooltipContent indicator="dashed" />} />
|
||||
<Bar dataKey="desktop" fill="var(--color-desktop)" radius={4} />
|
||||
<Bar dataKey="mobile" fill="var(--color-mobile)" radius={4} />
|
||||
</BarChart>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
<CardFooter className="flex-col items-start gap-2 text-sm">
|
||||
<div className="flex gap-2 font-medium leading-none">
|
||||
Trending up by 5.2% this month <TrendingUp className="h-4 w-4" />
|
||||
</div>
|
||||
<div className="leading-none text-muted-foreground">
|
||||
Showing total visitors for the last 6 months
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { TrendingUp } from 'lucide-react'
|
||||
import { Bar, BarChart, CartesianGrid, Cell, LabelList } from 'recharts'
|
||||
|
||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from 'ui'
|
||||
import { ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent } from 'ui'
|
||||
|
||||
export const description = 'A bar chart with negative values'
|
||||
|
||||
const chartData = [
|
||||
{ month: 'January', visitors: 186 },
|
||||
{ month: 'February', visitors: 205 },
|
||||
{ month: 'March', visitors: -207 },
|
||||
{ month: 'April', visitors: 173 },
|
||||
{ month: 'May', visitors: -209 },
|
||||
{ month: 'June', visitors: 214 },
|
||||
]
|
||||
|
||||
const chartConfig = {
|
||||
visitors: {
|
||||
label: 'Visitors',
|
||||
},
|
||||
} satisfies ChartConfig
|
||||
|
||||
export default function Component() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Bar Chart - Negative</CardTitle>
|
||||
<CardDescription>January - June 2024</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ChartContainer config={chartConfig}>
|
||||
<BarChart accessibilityLayer data={chartData}>
|
||||
<CartesianGrid vertical={false} />
|
||||
<ChartTooltip
|
||||
cursor={false}
|
||||
content={<ChartTooltipContent hideLabel hideIndicator />}
|
||||
/>
|
||||
<Bar dataKey="visitors">
|
||||
<LabelList position="top" dataKey="month" fillOpacity={1} />
|
||||
{chartData.map((item) => (
|
||||
<Cell
|
||||
key={item.month}
|
||||
fill={item.visitors > 0 ? 'hsl(var(--chart-1))' : 'hsl(var(--chart-2))'}
|
||||
/>
|
||||
))}
|
||||
</Bar>
|
||||
</BarChart>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
<CardFooter className="flex-col items-start gap-2 text-sm">
|
||||
<div className="flex gap-2 font-medium leading-none">
|
||||
Trending up by 5.2% this month <TrendingUp className="h-4 w-4" />
|
||||
</div>
|
||||
<div className="leading-none text-muted-foreground">
|
||||
Showing total visitors for the last 6 months
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { TrendingUp } from 'lucide-react'
|
||||
import { Bar, BarChart, CartesianGrid, XAxis } from 'recharts'
|
||||
|
||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from 'ui'
|
||||
import {
|
||||
ChartConfig,
|
||||
ChartContainer,
|
||||
ChartLegend,
|
||||
ChartLegendContent,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
} from 'ui'
|
||||
|
||||
export const description = 'A stacked bar chart with a legend'
|
||||
|
||||
const chartData = [
|
||||
{ month: 'January', desktop: 186, mobile: 80 },
|
||||
{ month: 'February', desktop: 305, mobile: 200 },
|
||||
{ month: 'March', desktop: 237, mobile: 120 },
|
||||
{ month: 'April', desktop: 73, mobile: 190 },
|
||||
{ month: 'May', desktop: 209, mobile: 130 },
|
||||
{ month: 'June', desktop: 214, mobile: 140 },
|
||||
]
|
||||
|
||||
const chartConfig = {
|
||||
desktop: {
|
||||
label: 'Desktop',
|
||||
color: 'hsl(var(--chart-1))',
|
||||
},
|
||||
mobile: {
|
||||
label: 'Mobile',
|
||||
color: 'hsl(var(--chart-2))',
|
||||
},
|
||||
} satisfies ChartConfig
|
||||
|
||||
export default function Component() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Bar Chart - Stacked + Legend</CardTitle>
|
||||
<CardDescription>January - June 2024</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ChartContainer config={chartConfig}>
|
||||
<BarChart accessibilityLayer data={chartData}>
|
||||
<CartesianGrid vertical={false} />
|
||||
<XAxis
|
||||
dataKey="month"
|
||||
tickLine={false}
|
||||
tickMargin={10}
|
||||
axisLine={false}
|
||||
tickFormatter={(value) => value.slice(0, 3)}
|
||||
/>
|
||||
<ChartTooltip content={<ChartTooltipContent hideLabel />} />
|
||||
<ChartLegend content={<ChartLegendContent />} />
|
||||
<Bar dataKey="desktop" stackId="a" fill="var(--color-desktop)" radius={[0, 0, 4, 4]} />
|
||||
<Bar dataKey="mobile" stackId="a" fill="var(--color-mobile)" radius={[4, 4, 0, 0]} />
|
||||
</BarChart>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
<CardFooter className="flex-col items-start gap-2 text-sm">
|
||||
<div className="flex gap-2 font-medium leading-none">
|
||||
Trending up by 5.2% this month <TrendingUp className="h-4 w-4" />
|
||||
</div>
|
||||
<div className="leading-none text-muted-foreground">
|
||||
Showing total visitors for the last 6 months
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
'use client'
|
||||
|
||||
import {
|
||||
Chart,
|
||||
ChartActions,
|
||||
ChartCard,
|
||||
ChartContent,
|
||||
ChartHeader,
|
||||
ChartTitle,
|
||||
ChartEmptyState,
|
||||
ChartLoadingState,
|
||||
ChartBar,
|
||||
ChartLine,
|
||||
} from 'ui-patterns/Chart'
|
||||
import { BarChart2, ExternalLink, LineChart } from 'lucide-react'
|
||||
import { useState, useEffect } from 'react'
|
||||
|
||||
export default function ChartComposedActions() {
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [chartType, setChartType] = useState<'bar' | 'line'>('bar')
|
||||
|
||||
const actions = [
|
||||
{
|
||||
label: 'Open in Logs Explorer',
|
||||
onClick: () => {
|
||||
alert('Opening in Logs Explorer...')
|
||||
},
|
||||
icon: <ExternalLink size={12} />,
|
||||
},
|
||||
{
|
||||
label: 'Toggle Chart Type',
|
||||
onClick: () => {
|
||||
setChartType(chartType === 'bar' ? 'line' : 'bar')
|
||||
},
|
||||
icon: chartType === 'bar' ? <LineChart size={12} /> : <BarChart2 size={12} />,
|
||||
},
|
||||
]
|
||||
|
||||
const data = Array.from({ length: 46 }, (_, i) => {
|
||||
const date = new Date()
|
||||
date.setMinutes(date.getMinutes() - i * 5) // Each point 5 minutes apart
|
||||
|
||||
return {
|
||||
timestamp: date.toISOString(),
|
||||
requests: Math.floor(Math.random() * 100),
|
||||
}
|
||||
}).reverse()
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
setIsLoading(false)
|
||||
}, 1000)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6 w-8/12">
|
||||
<Chart isLoading={isLoading}>
|
||||
<ChartCard>
|
||||
<ChartHeader>
|
||||
<ChartTitle tooltip="This is a tooltip">Total Requests</ChartTitle>
|
||||
<ChartActions actions={actions} />
|
||||
</ChartHeader>
|
||||
<ChartContent
|
||||
isEmpty={data.length === 0}
|
||||
emptyState={
|
||||
<ChartEmptyState
|
||||
icon={<BarChart2 size={16} />}
|
||||
title="No data to show"
|
||||
description="It may take up to 24 hours for data to refresh"
|
||||
/>
|
||||
}
|
||||
loadingState={<ChartLoadingState />}
|
||||
>
|
||||
<div className="h-40">
|
||||
{chartType === 'bar' ? (
|
||||
<ChartBar
|
||||
data={data}
|
||||
dataKey="requests"
|
||||
showGrid={false}
|
||||
showYAxis={true}
|
||||
YAxisProps={{
|
||||
tickFormatter: (value) => `${value}k`,
|
||||
width: 80,
|
||||
}}
|
||||
isFullHeight={true}
|
||||
/>
|
||||
) : (
|
||||
<ChartLine
|
||||
data={data}
|
||||
dataKey="requests"
|
||||
showGrid={true}
|
||||
showYAxis={true}
|
||||
YAxisProps={{
|
||||
tickFormatter: (value) => `${value}k`,
|
||||
width: 80,
|
||||
}}
|
||||
isFullHeight={true}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</ChartContent>
|
||||
</ChartCard>
|
||||
</Chart>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
'use client'
|
||||
|
||||
import {
|
||||
Chart,
|
||||
ChartActions,
|
||||
ChartCard,
|
||||
ChartContent,
|
||||
ChartHeader,
|
||||
ChartTitle,
|
||||
ChartEmptyState,
|
||||
ChartLoadingState,
|
||||
ChartBar,
|
||||
ChartLine,
|
||||
} from 'ui-patterns/Chart'
|
||||
import { BarChart2, ExternalLink } from 'lucide-react'
|
||||
import { useState, useEffect } from 'react'
|
||||
|
||||
export default function ComposedChartBasic() {
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
|
||||
const actions = [
|
||||
{
|
||||
label: 'Open in Logs Explorer',
|
||||
onClick: () => {
|
||||
alert('Ta da! You clicked me! 🎉')
|
||||
},
|
||||
icon: <ExternalLink size={12} />,
|
||||
},
|
||||
]
|
||||
|
||||
const data = Array.from({ length: 40 }, (_, i) => {
|
||||
const date = new Date()
|
||||
date.setMinutes(date.getMinutes() - i * 3) // Each point 3 minutes apart
|
||||
|
||||
const progress = i / 40
|
||||
const standard_score = Math.floor(55 + progress * 55 + (Math.random() - 0.5) * 12)
|
||||
const performance = Math.floor(35 + progress * 35 + (Math.random() - 0.5) * 10)
|
||||
const efficiency = Math.floor(25 + progress * 25 + (Math.random() - 0.5) * 12)
|
||||
|
||||
return {
|
||||
timestamp: date.toISOString(),
|
||||
standard_score: Math.max(0, Math.min(100, standard_score)),
|
||||
performance: Math.max(0, Math.min(100, performance)),
|
||||
efficiency: Math.max(0, Math.min(100, efficiency)),
|
||||
}
|
||||
}).reverse()
|
||||
|
||||
const chartConfig = {
|
||||
standard_score: {
|
||||
label: 'Standard Score',
|
||||
color: 'hsl(var(--brand-default))',
|
||||
},
|
||||
performance: {
|
||||
label: 'Performance',
|
||||
color: 'hsl(var(--chart-2))',
|
||||
},
|
||||
efficiency: {
|
||||
label: 'Efficiency',
|
||||
color: 'hsl(var(--chart-5))',
|
||||
},
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
setIsLoading(false)
|
||||
}, 1000)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6 w-8/12">
|
||||
<Chart isLoading={isLoading}>
|
||||
<ChartCard>
|
||||
<ChartHeader>
|
||||
<ChartTitle tooltip="This is a tooltip">Standard Bar Chart</ChartTitle>
|
||||
<ChartActions actions={actions} />
|
||||
</ChartHeader>
|
||||
<ChartContent
|
||||
isEmpty={data.length === 0}
|
||||
emptyState={
|
||||
<ChartEmptyState
|
||||
icon={<BarChart2 size={16} />}
|
||||
title="No data to show"
|
||||
description="It may take up to 24 hours for data to refresh"
|
||||
/>
|
||||
}
|
||||
loadingState={<ChartLoadingState />}
|
||||
>
|
||||
<div className="h-40">
|
||||
<ChartBar
|
||||
data={data}
|
||||
dataKey="standard_score"
|
||||
showGrid={false}
|
||||
showYAxis={true}
|
||||
YAxisProps={{
|
||||
tickFormatter: (value) => `${value}k`,
|
||||
width: 80,
|
||||
}}
|
||||
isFullHeight={true}
|
||||
/>
|
||||
</div>
|
||||
</ChartContent>
|
||||
</ChartCard>
|
||||
</Chart>
|
||||
|
||||
<Chart isLoading={isLoading}>
|
||||
<ChartCard>
|
||||
<ChartHeader>
|
||||
<ChartTitle tooltip="This is a tooltip">Standard Line Chart</ChartTitle>
|
||||
<ChartActions actions={actions} />
|
||||
</ChartHeader>
|
||||
<ChartContent
|
||||
isEmpty={data.length === 0}
|
||||
emptyState={
|
||||
<ChartEmptyState
|
||||
icon={<BarChart2 size={16} />}
|
||||
title="No data to show"
|
||||
description="It may take up to 24 hours for data to refresh"
|
||||
/>
|
||||
}
|
||||
loadingState={<ChartLoadingState />}
|
||||
>
|
||||
<div className="h-40">
|
||||
<ChartLine
|
||||
data={data}
|
||||
dataKey="standard_score"
|
||||
dataKeys={['standard_score', 'performance', 'efficiency']}
|
||||
config={chartConfig}
|
||||
showGrid={true}
|
||||
showYAxis={true}
|
||||
YAxisProps={{
|
||||
tickFormatter: (value) => `${value}k`,
|
||||
width: 80,
|
||||
}}
|
||||
isFullHeight={true}
|
||||
/>
|
||||
</div>
|
||||
</ChartContent>
|
||||
</ChartCard>
|
||||
</Chart>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
'use client'
|
||||
|
||||
import {
|
||||
Chart,
|
||||
ChartCard,
|
||||
ChartContent,
|
||||
ChartHeader,
|
||||
ChartEmptyState,
|
||||
ChartLoadingState,
|
||||
ChartMetric,
|
||||
} from 'ui-patterns/Chart'
|
||||
import { BarChart2 } from 'lucide-react'
|
||||
import { LogsBarChart } from 'ui-patterns/LogsBarChart'
|
||||
import { useState, useEffect } from 'react'
|
||||
|
||||
export default function ComposedChartDemo() {
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
|
||||
const data = Array.from({ length: 60 }, (_, i) => {
|
||||
const date = new Date()
|
||||
date.setMinutes(date.getMinutes() - i * 5) // Each point 5 minutes apart
|
||||
|
||||
return {
|
||||
timestamp: date.toISOString(),
|
||||
ok_count: Math.floor(Math.random() * 100), // Random value 0-99
|
||||
error_count: Math.floor(Math.random() * 50), // Random value 0-50
|
||||
warning_count: Math.floor(Math.random() * 50), // Random value 0-50
|
||||
}
|
||||
}).reverse()
|
||||
|
||||
const totalUsersValue = 4663
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
setIsLoading(false)
|
||||
}, 1000)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="w-8/12">
|
||||
<Chart isLoading={isLoading}>
|
||||
<ChartCard>
|
||||
<ChartHeader>
|
||||
<ChartMetric
|
||||
label="Total Users"
|
||||
value={totalUsersValue.toLocaleString('en-US')}
|
||||
tooltip="This is a tooltip"
|
||||
/>
|
||||
<div className="flex items-center gap-6">
|
||||
<ChartMetric label="Warn" value="1.2k" status="warning" align="end" />
|
||||
<ChartMetric label="Err" value="736" status="negative" align="end" />
|
||||
</div>
|
||||
</ChartHeader>
|
||||
<ChartContent
|
||||
isEmpty={data.length === 0}
|
||||
emptyState={
|
||||
<ChartEmptyState
|
||||
icon={<BarChart2 size={16} />}
|
||||
title="No data to show"
|
||||
description="It may take up to 24 hours for data to refresh"
|
||||
/>
|
||||
}
|
||||
loadingState={<ChartLoadingState />}
|
||||
>
|
||||
<div className="h-40">
|
||||
<LogsBarChart data={data} isFullHeight={true} />
|
||||
</div>
|
||||
</ChartContent>
|
||||
</ChartCard>
|
||||
</Chart>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
'use client'
|
||||
|
||||
import {
|
||||
Chart,
|
||||
ChartCard,
|
||||
ChartHeader,
|
||||
ChartActions,
|
||||
ChartContent,
|
||||
ChartMetric,
|
||||
ChartSparkline,
|
||||
} from 'ui-patterns/Chart'
|
||||
import { ExternalLink } from 'lucide-react'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Skeleton } from 'ui'
|
||||
|
||||
export default function ChartComposedMetrics() {
|
||||
const [data, setData] = useState<Array<{ value: number; timestamp: string }>>([])
|
||||
|
||||
const actions = [
|
||||
{
|
||||
label: 'Open in Logs Explorer',
|
||||
onClick: () => {
|
||||
alert('Ta da! You clicked me! 🎉')
|
||||
},
|
||||
icon: <ExternalLink size={12} />,
|
||||
},
|
||||
]
|
||||
|
||||
useEffect(() => {
|
||||
const now = new Date()
|
||||
setData(
|
||||
Array.from({ length: 12 }, (_, i) => ({
|
||||
value: Math.floor(4000 + i * 100 + (Math.random() * 2000 - 800)),
|
||||
timestamp: new Date(now.getTime() - (11 - i) * 60 * 60 * 1000).toISOString(),
|
||||
}))
|
||||
)
|
||||
}, [])
|
||||
|
||||
const averageValue = data.reduce((acc, curr) => acc + curr.value, 0) / data.length
|
||||
|
||||
const diff = data[data.length - 1]?.value - data[0]?.value || 0
|
||||
const diffPercentage = (diff / averageValue) * 100
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-2 gap-6 w-full">
|
||||
<Chart isLoading={!data.length}>
|
||||
<ChartCard>
|
||||
<ChartHeader className="pb-4" align="start">
|
||||
<ChartMetric label="Slow Queries" value={0} />
|
||||
<ChartActions actions={actions} />
|
||||
</ChartHeader>
|
||||
</ChartCard>
|
||||
</Chart>
|
||||
<Chart isLoading={!data.length}>
|
||||
<ChartCard>
|
||||
<ChartHeader className="pb-4" align="start">
|
||||
<ChartMetric label="Sign Ups" value={127} />
|
||||
<ChartActions actions={actions} />
|
||||
</ChartHeader>
|
||||
</ChartCard>
|
||||
</Chart>
|
||||
<Chart isLoading={!data.length}>
|
||||
<ChartCard>
|
||||
<ChartHeader align="start">
|
||||
<ChartMetric
|
||||
label="Active Users"
|
||||
value={averageValue.toLocaleString(undefined, { maximumFractionDigits: 0 })}
|
||||
diffValue={diffPercentage.toFixed(2) + '%'}
|
||||
/>
|
||||
<ChartActions actions={actions} />
|
||||
</ChartHeader>
|
||||
<ChartContent
|
||||
className="p-0"
|
||||
loadingState={<Skeleton className="w-full h-[6rem] rounded-none mt-4" />}
|
||||
>
|
||||
<ChartSparkline data={data} dataKey="value" />
|
||||
</ChartContent>
|
||||
</ChartCard>
|
||||
</Chart>
|
||||
<Chart isLoading={!data.length}>
|
||||
<ChartCard>
|
||||
<ChartHeader align="start">
|
||||
<ChartMetric
|
||||
label="Active Users"
|
||||
value={averageValue.toLocaleString(undefined, { maximumFractionDigits: 0 })}
|
||||
diffValue={diffPercentage.toFixed(2) + '%'}
|
||||
/>
|
||||
<ChartActions actions={actions} />
|
||||
</ChartHeader>
|
||||
<ChartContent
|
||||
className="p-0"
|
||||
loadingState={<Skeleton className="w-full h-[6rem] rounded-none mt-4" />}
|
||||
>
|
||||
<ChartSparkline data={data} dataKey="value" />
|
||||
</ChartContent>
|
||||
</ChartCard>
|
||||
</Chart>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
'use client'
|
||||
|
||||
import { BarChart2, ExternalLink } from 'lucide-react'
|
||||
import {
|
||||
Chart,
|
||||
ChartCard,
|
||||
ChartHeader,
|
||||
ChartTitle,
|
||||
ChartActions,
|
||||
ChartContent,
|
||||
ChartEmptyState,
|
||||
ChartLoadingState,
|
||||
ChartDisabledState,
|
||||
} from 'ui-patterns/Chart'
|
||||
import { Badge } from 'ui'
|
||||
|
||||
export default function ChartComposedStates() {
|
||||
const actions = [
|
||||
{
|
||||
label: 'Open in Logs Explorer',
|
||||
onClick: () => {
|
||||
alert('Ta da! You clicked me! 🎉')
|
||||
},
|
||||
icon: <ExternalLink size={12} />,
|
||||
},
|
||||
]
|
||||
|
||||
const disabledActions = [
|
||||
{
|
||||
label: 'Upgrade to Pro',
|
||||
href: '#',
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6 w-8/12">
|
||||
<Chart isLoading={true}>
|
||||
<ChartCard>
|
||||
<ChartHeader>
|
||||
<ChartTitle tooltip="This is a tooltip">Response Errors</ChartTitle>
|
||||
<ChartActions actions={actions} />
|
||||
</ChartHeader>
|
||||
<ChartContent loadingState={<ChartLoadingState />}>My chart here...</ChartContent>
|
||||
</ChartCard>
|
||||
</Chart>
|
||||
|
||||
<Chart>
|
||||
<ChartCard>
|
||||
<ChartHeader>
|
||||
<ChartTitle tooltip="This is a tooltip">Response Errors</ChartTitle>
|
||||
<ChartActions actions={actions} />
|
||||
</ChartHeader>
|
||||
<ChartContent
|
||||
isEmpty={true}
|
||||
emptyState={
|
||||
<ChartEmptyState
|
||||
icon={<BarChart2 size={20} />}
|
||||
title="No data to show"
|
||||
description="It may take up to 24 hours for data to refresh"
|
||||
/>
|
||||
}
|
||||
loadingState={<ChartLoadingState />}
|
||||
>
|
||||
My chart here...
|
||||
</ChartContent>
|
||||
</ChartCard>
|
||||
</Chart>
|
||||
|
||||
<Chart isDisabled={true}>
|
||||
<ChartCard>
|
||||
<ChartHeader>
|
||||
<ChartTitle tooltip="This is a tooltip">Response Errors</ChartTitle>
|
||||
<ChartActions actions={actions} />
|
||||
</ChartHeader>
|
||||
<ChartContent
|
||||
loadingState={<ChartLoadingState />}
|
||||
disabledState={
|
||||
<ChartDisabledState
|
||||
icon={<Badge variant="success">Pro</Badge>}
|
||||
label="API Processing Time"
|
||||
description="This chart is available on the Pro plan and above"
|
||||
actions={disabledActions}
|
||||
/>
|
||||
}
|
||||
>
|
||||
My chart here...
|
||||
</ChartContent>
|
||||
</ChartCard>
|
||||
</Chart>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
'use client'
|
||||
|
||||
import {
|
||||
Chart,
|
||||
ChartActions,
|
||||
ChartCard,
|
||||
ChartContent,
|
||||
ChartHeader,
|
||||
ChartTitle,
|
||||
ChartEmptyState,
|
||||
ChartLoadingState,
|
||||
ChartBar,
|
||||
ChartFooter,
|
||||
} from 'ui-patterns/Chart'
|
||||
import { BarChart2, ExternalLink } from 'lucide-react'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Table, TableHead, TableBody, TableRow, TableCell, TableHeader } from 'ui'
|
||||
import { format } from 'date-fns'
|
||||
|
||||
export default function ChartComposedTable() {
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
|
||||
const actions = [
|
||||
{
|
||||
label: 'Open in Logs Explorer',
|
||||
onClick: () => {
|
||||
alert('Opening in Logs Explorer...')
|
||||
},
|
||||
icon: <ExternalLink size={12} />,
|
||||
},
|
||||
]
|
||||
|
||||
const data = Array.from({ length: 46 }, (_, i) => {
|
||||
const date = new Date()
|
||||
date.setMinutes(date.getMinutes() - i * 5) // Each point 5 minutes apart
|
||||
|
||||
return {
|
||||
timestamp: date.toISOString(),
|
||||
standard_score: Math.floor(Math.random() * 100),
|
||||
}
|
||||
}).reverse()
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
setIsLoading(false)
|
||||
}, 1000)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6 w-8/12">
|
||||
<Chart isLoading={isLoading}>
|
||||
<ChartCard>
|
||||
<ChartHeader>
|
||||
<ChartTitle tooltip="This is a tooltip">Standard Bar Chart</ChartTitle>
|
||||
<ChartActions actions={actions} />
|
||||
</ChartHeader>
|
||||
<ChartContent
|
||||
isEmpty={data.length === 0}
|
||||
emptyState={
|
||||
<ChartEmptyState
|
||||
icon={<BarChart2 size={16} />}
|
||||
title="No data to show"
|
||||
description="It may take up to 24 hours for data to refresh"
|
||||
/>
|
||||
}
|
||||
loadingState={<ChartLoadingState />}
|
||||
>
|
||||
<div className="h-40">
|
||||
<ChartBar
|
||||
data={data}
|
||||
dataKey="standard_score"
|
||||
showGrid={false}
|
||||
showYAxis={true}
|
||||
YAxisProps={{
|
||||
tickFormatter: (value) => `${value}k`,
|
||||
width: 80,
|
||||
}}
|
||||
isFullHeight={true}
|
||||
/>
|
||||
</div>
|
||||
</ChartContent>
|
||||
<ChartFooter>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Timestamp</TableHead>
|
||||
<TableHead className="text-right">Standard Score</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{data.slice(0, 3).map((item) => (
|
||||
<TableRow key={item.timestamp}>
|
||||
<TableCell className="text-foreground-light">
|
||||
{format(new Date(item.timestamp), 'MMM d, yyyy h:mm a')}
|
||||
</TableCell>
|
||||
<TableCell className="text-right tabular-nums">{item.standard_score}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</ChartFooter>
|
||||
</ChartCard>
|
||||
</Chart>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { TrendingUp } from 'lucide-react'
|
||||
import { CartesianGrid, Line, LineChart, XAxis } from 'recharts'
|
||||
|
||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from 'ui'
|
||||
import { ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent } from 'ui'
|
||||
|
||||
export const description = 'A line chart'
|
||||
|
||||
const chartData = [
|
||||
{ month: 'January', desktop: 186 },
|
||||
{ month: 'February', desktop: 305 },
|
||||
{ month: 'March', desktop: 237 },
|
||||
{ month: 'April', desktop: 73 },
|
||||
{ month: 'May', desktop: 209 },
|
||||
{ month: 'June', desktop: 214 },
|
||||
]
|
||||
|
||||
const chartConfig = {
|
||||
desktop: {
|
||||
label: 'Desktop',
|
||||
color: 'hsl(var(--chart-1))',
|
||||
},
|
||||
} satisfies ChartConfig
|
||||
|
||||
export default function Component() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Line Chart</CardTitle>
|
||||
<CardDescription>January - June 2024</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ChartContainer config={chartConfig}>
|
||||
<LineChart
|
||||
accessibilityLayer
|
||||
data={chartData}
|
||||
margin={{
|
||||
left: 12,
|
||||
right: 12,
|
||||
}}
|
||||
>
|
||||
<CartesianGrid vertical={false} />
|
||||
<XAxis
|
||||
dataKey="month"
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickMargin={8}
|
||||
tickFormatter={(value) => value.slice(0, 3)}
|
||||
/>
|
||||
<ChartTooltip cursor={false} content={<ChartTooltipContent hideLabel />} />
|
||||
<Line
|
||||
dataKey="desktop"
|
||||
type="natural"
|
||||
stroke="var(--color-desktop)"
|
||||
strokeWidth={2}
|
||||
dot={false}
|
||||
/>
|
||||
</LineChart>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
<CardFooter className="flex-col items-start gap-2 text-sm">
|
||||
<div className="flex gap-2 font-medium leading-none">
|
||||
Trending up by 5.2% this month <TrendingUp className="h-4 w-4" />
|
||||
</div>
|
||||
<div className="leading-none text-muted-foreground">
|
||||
Showing total visitors for the last 6 months
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { TrendingUp } from 'lucide-react'
|
||||
import { CartesianGrid, Dot, Line, LineChart } from 'recharts'
|
||||
|
||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from 'ui'
|
||||
import { ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent } from 'ui'
|
||||
|
||||
export const description = 'A line chart with dots and colors'
|
||||
|
||||
const chartData = [
|
||||
{ browser: 'chrome', visitors: 275, fill: 'var(--color-chrome)' },
|
||||
{ browser: 'safari', visitors: 200, fill: 'var(--color-safari)' },
|
||||
{ browser: 'firefox', visitors: 187, fill: 'var(--color-firefox)' },
|
||||
{ browser: 'edge', visitors: 173, fill: 'var(--color-edge)' },
|
||||
{ browser: 'other', visitors: 90, fill: 'var(--color-other)' },
|
||||
]
|
||||
|
||||
const chartConfig = {
|
||||
visitors: {
|
||||
label: 'Visitors',
|
||||
color: 'hsl(var(--chart-2))',
|
||||
},
|
||||
chrome: {
|
||||
label: 'Chrome',
|
||||
color: 'hsl(var(--chart-1))',
|
||||
},
|
||||
safari: {
|
||||
label: 'Safari',
|
||||
color: 'hsl(var(--chart-2))',
|
||||
},
|
||||
firefox: {
|
||||
label: 'Firefox',
|
||||
color: 'hsl(var(--chart-3))',
|
||||
},
|
||||
edge: {
|
||||
label: 'Edge',
|
||||
color: 'hsl(var(--chart-4))',
|
||||
},
|
||||
other: {
|
||||
label: 'Other',
|
||||
color: 'hsl(var(--chart-5))',
|
||||
},
|
||||
} satisfies ChartConfig
|
||||
|
||||
export default function Component() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Line Chart - Dots Colors</CardTitle>
|
||||
<CardDescription>January - June 2024</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ChartContainer config={chartConfig}>
|
||||
<LineChart
|
||||
accessibilityLayer
|
||||
data={chartData}
|
||||
margin={{
|
||||
top: 24,
|
||||
left: 24,
|
||||
right: 24,
|
||||
}}
|
||||
>
|
||||
<CartesianGrid vertical={false} />
|
||||
<ChartTooltip
|
||||
cursor={false}
|
||||
content={<ChartTooltipContent indicator="line" nameKey="visitors" hideLabel />}
|
||||
/>
|
||||
<Line
|
||||
dataKey="visitors"
|
||||
type="natural"
|
||||
stroke="var(--color-visitors)"
|
||||
strokeWidth={2}
|
||||
dot={({ payload, ...props }) => {
|
||||
return (
|
||||
<Dot
|
||||
key={payload.browser}
|
||||
r={5}
|
||||
cx={props.cx}
|
||||
cy={props.cy}
|
||||
fill={payload.fill}
|
||||
stroke={payload.fill}
|
||||
/>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</LineChart>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
<CardFooter className="flex-col items-start gap-2 text-sm">
|
||||
<div className="flex gap-2 font-medium leading-none">
|
||||
Trending up by 5.2% this month <TrendingUp className="h-4 w-4" />
|
||||
</div>
|
||||
<div className="leading-none text-muted-foreground">
|
||||
Showing total visitors for the last 6 months
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { GitCommitVertical, TrendingUp } from 'lucide-react'
|
||||
import { CartesianGrid, Line, LineChart, XAxis } from 'recharts'
|
||||
|
||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from 'ui'
|
||||
import { ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent } from 'ui'
|
||||
|
||||
export const description = 'A line chart with custom dots'
|
||||
|
||||
const chartData = [
|
||||
{ month: 'January', desktop: 186, mobile: 80 },
|
||||
{ month: 'February', desktop: 305, mobile: 200 },
|
||||
{ month: 'March', desktop: 237, mobile: 120 },
|
||||
{ month: 'April', desktop: 73, mobile: 190 },
|
||||
{ month: 'May', desktop: 209, mobile: 130 },
|
||||
{ month: 'June', desktop: 214, mobile: 140 },
|
||||
]
|
||||
|
||||
const chartConfig = {
|
||||
desktop: {
|
||||
label: 'Desktop',
|
||||
color: 'hsl(var(--chart-1))',
|
||||
},
|
||||
mobile: {
|
||||
label: 'Mobile',
|
||||
color: 'hsl(var(--chart-2))',
|
||||
},
|
||||
} satisfies ChartConfig
|
||||
|
||||
export default function Component() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Line Chart - Custom Dots</CardTitle>
|
||||
<CardDescription>January - June 2024</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ChartContainer config={chartConfig}>
|
||||
<LineChart
|
||||
accessibilityLayer
|
||||
data={chartData}
|
||||
margin={{
|
||||
left: 12,
|
||||
right: 12,
|
||||
}}
|
||||
>
|
||||
<CartesianGrid vertical={false} />
|
||||
<XAxis
|
||||
dataKey="month"
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickMargin={8}
|
||||
tickFormatter={(value) => value.slice(0, 3)}
|
||||
/>
|
||||
<ChartTooltip cursor={false} content={<ChartTooltipContent hideLabel />} />
|
||||
<Line
|
||||
dataKey="desktop"
|
||||
type="natural"
|
||||
stroke="var(--color-desktop)"
|
||||
strokeWidth={2}
|
||||
dot={({ cx, cy, payload }) => {
|
||||
const r = 24
|
||||
return (
|
||||
<GitCommitVertical
|
||||
key={payload.month}
|
||||
x={cx - r / 2}
|
||||
y={cy - r / 2}
|
||||
width={r}
|
||||
height={r}
|
||||
fill="hsl(var(--background))"
|
||||
stroke="var(--color-desktop)"
|
||||
/>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</LineChart>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
<CardFooter className="flex-col items-start gap-2 text-sm">
|
||||
<div className="flex gap-2 font-medium leading-none">
|
||||
Trending up by 5.2% this month <TrendingUp className="h-4 w-4" />
|
||||
</div>
|
||||
<div className="leading-none text-muted-foreground">
|
||||
Showing total visitors for the last 6 months
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { TrendingUp } from 'lucide-react'
|
||||
import { CartesianGrid, Line, LineChart, XAxis } from 'recharts'
|
||||
|
||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from 'ui'
|
||||
import { ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent } from 'ui'
|
||||
|
||||
export const description = 'A line chart with dots'
|
||||
|
||||
const chartData = [
|
||||
{ month: 'January', desktop: 186, mobile: 80 },
|
||||
{ month: 'February', desktop: 305, mobile: 200 },
|
||||
{ month: 'March', desktop: 237, mobile: 120 },
|
||||
{ month: 'April', desktop: 73, mobile: 190 },
|
||||
{ month: 'May', desktop: 209, mobile: 130 },
|
||||
{ month: 'June', desktop: 214, mobile: 140 },
|
||||
]
|
||||
|
||||
const chartConfig = {
|
||||
desktop: {
|
||||
label: 'Desktop',
|
||||
color: 'hsl(var(--chart-1))',
|
||||
},
|
||||
mobile: {
|
||||
label: 'Mobile',
|
||||
color: 'hsl(var(--chart-2))',
|
||||
},
|
||||
} satisfies ChartConfig
|
||||
|
||||
export default function Component() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Line Chart - Dots</CardTitle>
|
||||
<CardDescription>January - June 2024</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ChartContainer config={chartConfig}>
|
||||
<LineChart
|
||||
accessibilityLayer
|
||||
data={chartData}
|
||||
margin={{
|
||||
left: 12,
|
||||
right: 12,
|
||||
}}
|
||||
>
|
||||
<CartesianGrid vertical={false} />
|
||||
<XAxis
|
||||
dataKey="month"
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickMargin={8}
|
||||
tickFormatter={(value) => value.slice(0, 3)}
|
||||
/>
|
||||
<ChartTooltip cursor={false} content={<ChartTooltipContent hideLabel />} />
|
||||
<Line
|
||||
dataKey="desktop"
|
||||
type="natural"
|
||||
stroke="var(--color-desktop)"
|
||||
strokeWidth={2}
|
||||
dot={{
|
||||
fill: 'var(--color-desktop)',
|
||||
}}
|
||||
activeDot={{
|
||||
r: 6,
|
||||
}}
|
||||
/>
|
||||
</LineChart>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
<CardFooter className="flex-col items-start gap-2 text-sm">
|
||||
<div className="flex gap-2 font-medium leading-none">
|
||||
Trending up by 5.2% this month <TrendingUp className="h-4 w-4" />
|
||||
</div>
|
||||
<div className="leading-none text-muted-foreground">
|
||||
Showing total visitors for the last 6 months
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -1,208 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import * as React from 'react'
|
||||
import { CartesianGrid, Line, LineChart, XAxis } from 'recharts'
|
||||
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from 'ui'
|
||||
import { ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent } from 'ui'
|
||||
|
||||
export const description = 'An interactive line chart'
|
||||
|
||||
const chartData = [
|
||||
{ date: '2024-04-01', desktop: 222, mobile: 150 },
|
||||
{ date: '2024-04-02', desktop: 97, mobile: 180 },
|
||||
{ date: '2024-04-03', desktop: 167, mobile: 120 },
|
||||
{ date: '2024-04-04', desktop: 242, mobile: 260 },
|
||||
{ date: '2024-04-05', desktop: 373, mobile: 290 },
|
||||
{ date: '2024-04-06', desktop: 301, mobile: 340 },
|
||||
{ date: '2024-04-07', desktop: 245, mobile: 180 },
|
||||
{ date: '2024-04-08', desktop: 409, mobile: 320 },
|
||||
{ date: '2024-04-09', desktop: 59, mobile: 110 },
|
||||
{ date: '2024-04-10', desktop: 261, mobile: 190 },
|
||||
{ date: '2024-04-11', desktop: 327, mobile: 350 },
|
||||
{ date: '2024-04-12', desktop: 292, mobile: 210 },
|
||||
{ date: '2024-04-13', desktop: 342, mobile: 380 },
|
||||
{ date: '2024-04-14', desktop: 137, mobile: 220 },
|
||||
{ date: '2024-04-15', desktop: 120, mobile: 170 },
|
||||
{ date: '2024-04-16', desktop: 138, mobile: 190 },
|
||||
{ date: '2024-04-17', desktop: 446, mobile: 360 },
|
||||
{ date: '2024-04-18', desktop: 364, mobile: 410 },
|
||||
{ date: '2024-04-19', desktop: 243, mobile: 180 },
|
||||
{ date: '2024-04-20', desktop: 89, mobile: 150 },
|
||||
{ date: '2024-04-21', desktop: 137, mobile: 200 },
|
||||
{ date: '2024-04-22', desktop: 224, mobile: 170 },
|
||||
{ date: '2024-04-23', desktop: 138, mobile: 230 },
|
||||
{ date: '2024-04-24', desktop: 387, mobile: 290 },
|
||||
{ date: '2024-04-25', desktop: 215, mobile: 250 },
|
||||
{ date: '2024-04-26', desktop: 75, mobile: 130 },
|
||||
{ date: '2024-04-27', desktop: 383, mobile: 420 },
|
||||
{ date: '2024-04-28', desktop: 122, mobile: 180 },
|
||||
{ date: '2024-04-29', desktop: 315, mobile: 240 },
|
||||
{ date: '2024-04-30', desktop: 454, mobile: 380 },
|
||||
{ date: '2024-05-01', desktop: 165, mobile: 220 },
|
||||
{ date: '2024-05-02', desktop: 293, mobile: 310 },
|
||||
{ date: '2024-05-03', desktop: 247, mobile: 190 },
|
||||
{ date: '2024-05-04', desktop: 385, mobile: 420 },
|
||||
{ date: '2024-05-05', desktop: 481, mobile: 390 },
|
||||
{ date: '2024-05-06', desktop: 498, mobile: 520 },
|
||||
{ date: '2024-05-07', desktop: 388, mobile: 300 },
|
||||
{ date: '2024-05-08', desktop: 149, mobile: 210 },
|
||||
{ date: '2024-05-09', desktop: 227, mobile: 180 },
|
||||
{ date: '2024-05-10', desktop: 293, mobile: 330 },
|
||||
{ date: '2024-05-11', desktop: 335, mobile: 270 },
|
||||
{ date: '2024-05-12', desktop: 197, mobile: 240 },
|
||||
{ date: '2024-05-13', desktop: 197, mobile: 160 },
|
||||
{ date: '2024-05-14', desktop: 448, mobile: 490 },
|
||||
{ date: '2024-05-15', desktop: 473, mobile: 380 },
|
||||
{ date: '2024-05-16', desktop: 338, mobile: 400 },
|
||||
{ date: '2024-05-17', desktop: 499, mobile: 420 },
|
||||
{ date: '2024-05-18', desktop: 315, mobile: 350 },
|
||||
{ date: '2024-05-19', desktop: 235, mobile: 180 },
|
||||
{ date: '2024-05-20', desktop: 177, mobile: 230 },
|
||||
{ date: '2024-05-21', desktop: 82, mobile: 140 },
|
||||
{ date: '2024-05-22', desktop: 81, mobile: 120 },
|
||||
{ date: '2024-05-23', desktop: 252, mobile: 290 },
|
||||
{ date: '2024-05-24', desktop: 294, mobile: 220 },
|
||||
{ date: '2024-05-25', desktop: 201, mobile: 250 },
|
||||
{ date: '2024-05-26', desktop: 213, mobile: 170 },
|
||||
{ date: '2024-05-27', desktop: 420, mobile: 460 },
|
||||
{ date: '2024-05-28', desktop: 233, mobile: 190 },
|
||||
{ date: '2024-05-29', desktop: 78, mobile: 130 },
|
||||
{ date: '2024-05-30', desktop: 340, mobile: 280 },
|
||||
{ date: '2024-05-31', desktop: 178, mobile: 230 },
|
||||
{ date: '2024-06-01', desktop: 178, mobile: 200 },
|
||||
{ date: '2024-06-02', desktop: 470, mobile: 410 },
|
||||
{ date: '2024-06-03', desktop: 103, mobile: 160 },
|
||||
{ date: '2024-06-04', desktop: 439, mobile: 380 },
|
||||
{ date: '2024-06-05', desktop: 88, mobile: 140 },
|
||||
{ date: '2024-06-06', desktop: 294, mobile: 250 },
|
||||
{ date: '2024-06-07', desktop: 323, mobile: 370 },
|
||||
{ date: '2024-06-08', desktop: 385, mobile: 320 },
|
||||
{ date: '2024-06-09', desktop: 438, mobile: 480 },
|
||||
{ date: '2024-06-10', desktop: 155, mobile: 200 },
|
||||
{ date: '2024-06-11', desktop: 92, mobile: 150 },
|
||||
{ date: '2024-06-12', desktop: 492, mobile: 420 },
|
||||
{ date: '2024-06-13', desktop: 81, mobile: 130 },
|
||||
{ date: '2024-06-14', desktop: 426, mobile: 380 },
|
||||
{ date: '2024-06-15', desktop: 307, mobile: 350 },
|
||||
{ date: '2024-06-16', desktop: 371, mobile: 310 },
|
||||
{ date: '2024-06-17', desktop: 475, mobile: 520 },
|
||||
{ date: '2024-06-18', desktop: 107, mobile: 170 },
|
||||
{ date: '2024-06-19', desktop: 341, mobile: 290 },
|
||||
{ date: '2024-06-20', desktop: 408, mobile: 450 },
|
||||
{ date: '2024-06-21', desktop: 169, mobile: 210 },
|
||||
{ date: '2024-06-22', desktop: 317, mobile: 270 },
|
||||
{ date: '2024-06-23', desktop: 480, mobile: 530 },
|
||||
{ date: '2024-06-24', desktop: 132, mobile: 180 },
|
||||
{ date: '2024-06-25', desktop: 141, mobile: 190 },
|
||||
{ date: '2024-06-26', desktop: 434, mobile: 380 },
|
||||
{ date: '2024-06-27', desktop: 448, mobile: 490 },
|
||||
{ date: '2024-06-28', desktop: 149, mobile: 200 },
|
||||
{ date: '2024-06-29', desktop: 103, mobile: 160 },
|
||||
{ date: '2024-06-30', desktop: 446, mobile: 400 },
|
||||
]
|
||||
|
||||
const chartConfig = {
|
||||
views: {
|
||||
label: 'Page Views',
|
||||
},
|
||||
desktop: {
|
||||
label: 'Desktop',
|
||||
color: 'hsl(var(--chart-1))',
|
||||
},
|
||||
mobile: {
|
||||
label: 'Mobile',
|
||||
color: 'hsl(var(--chart-2))',
|
||||
},
|
||||
} satisfies ChartConfig
|
||||
|
||||
export default function Component() {
|
||||
const [activeChart, setActiveChart] = React.useState<keyof typeof chartConfig>('desktop')
|
||||
|
||||
const total = React.useMemo(
|
||||
() => ({
|
||||
desktop: chartData.reduce((acc, curr) => acc + curr.desktop, 0),
|
||||
mobile: chartData.reduce((acc, curr) => acc + curr.mobile, 0),
|
||||
}),
|
||||
[]
|
||||
)
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="flex flex-col items-stretch space-y-0 border-b p-0 sm:flex-row">
|
||||
<div className="flex flex-1 flex-col justify-center gap-1 px-6 py-5 sm:py-6">
|
||||
<CardTitle>Line Chart - Interactive</CardTitle>
|
||||
<CardDescription>Showing total visitors for the last 3 months</CardDescription>
|
||||
</div>
|
||||
<div className="flex">
|
||||
{['desktop', 'mobile'].map((key) => {
|
||||
const chart = key as keyof typeof chartConfig
|
||||
return (
|
||||
<button
|
||||
key={chart}
|
||||
data-active={activeChart === chart}
|
||||
className="flex flex-1 flex-col justify-center gap-1 border-t px-6 py-4 text-left even:border-l data-[active=true]:bg-muted/50 sm:border-l sm:border-t-0 sm:px-8 sm:py-6"
|
||||
onClick={() => setActiveChart(chart)}
|
||||
>
|
||||
<span className="text-xs text-muted-foreground">{chartConfig[chart].label}</span>
|
||||
<span className="text-lg font-bold leading-none sm:text-3xl">
|
||||
{total[key as keyof typeof total].toLocaleString()}
|
||||
</span>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="px-2 sm:p-6">
|
||||
<ChartContainer config={chartConfig} className="aspect-auto h-[250px] w-full">
|
||||
<LineChart
|
||||
accessibilityLayer
|
||||
data={chartData}
|
||||
margin={{
|
||||
left: 12,
|
||||
right: 12,
|
||||
}}
|
||||
>
|
||||
<CartesianGrid vertical={false} />
|
||||
<XAxis
|
||||
dataKey="date"
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickMargin={8}
|
||||
minTickGap={32}
|
||||
tickFormatter={(value) => {
|
||||
const date = new Date(value)
|
||||
return date.toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
})
|
||||
}}
|
||||
/>
|
||||
<ChartTooltip
|
||||
content={
|
||||
<ChartTooltipContent
|
||||
className="w-[150px]"
|
||||
nameKey="views"
|
||||
labelFormatter={(value) => {
|
||||
return new Date(value).toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
})
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Line
|
||||
dataKey={activeChart}
|
||||
type="monotone"
|
||||
stroke={`var(--color-${activeChart})`}
|
||||
strokeWidth={2}
|
||||
dot={false}
|
||||
/>
|
||||
</LineChart>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { TrendingUp } from 'lucide-react'
|
||||
import { CartesianGrid, LabelList, Line, LineChart } from 'recharts'
|
||||
|
||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from 'ui'
|
||||
import { ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent } from 'ui'
|
||||
|
||||
export const description = 'A line chart with a custom label'
|
||||
|
||||
const chartData = [
|
||||
{ browser: 'chrome', visitors: 275, fill: 'var(--color-chrome)' },
|
||||
{ browser: 'safari', visitors: 200, fill: 'var(--color-safari)' },
|
||||
{ browser: 'firefox', visitors: 187, fill: 'var(--color-firefox)' },
|
||||
{ browser: 'edge', visitors: 173, fill: 'var(--color-edge)' },
|
||||
{ browser: 'other', visitors: 90, fill: 'var(--color-other)' },
|
||||
]
|
||||
|
||||
const chartConfig = {
|
||||
visitors: {
|
||||
label: 'Visitors',
|
||||
color: 'hsl(var(--chart-2))',
|
||||
},
|
||||
chrome: {
|
||||
label: 'Chrome',
|
||||
color: 'hsl(var(--chart-1))',
|
||||
},
|
||||
safari: {
|
||||
label: 'Safari',
|
||||
color: 'hsl(var(--chart-2))',
|
||||
},
|
||||
firefox: {
|
||||
label: 'Firefox',
|
||||
color: 'hsl(var(--chart-3))',
|
||||
},
|
||||
edge: {
|
||||
label: 'Edge',
|
||||
color: 'hsl(var(--chart-4))',
|
||||
},
|
||||
other: {
|
||||
label: 'Other',
|
||||
color: 'hsl(var(--chart-5))',
|
||||
},
|
||||
} satisfies ChartConfig
|
||||
|
||||
export default function Component() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Line Chart - Custom Label</CardTitle>
|
||||
<CardDescription>January - June 2024</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ChartContainer config={chartConfig}>
|
||||
<LineChart
|
||||
accessibilityLayer
|
||||
data={chartData}
|
||||
margin={{
|
||||
top: 24,
|
||||
left: 24,
|
||||
right: 24,
|
||||
}}
|
||||
>
|
||||
<CartesianGrid vertical={false} />
|
||||
<ChartTooltip
|
||||
cursor={false}
|
||||
content={<ChartTooltipContent indicator="line" nameKey="visitors" hideLabel />}
|
||||
/>
|
||||
<Line
|
||||
dataKey="visitors"
|
||||
type="natural"
|
||||
stroke="var(--color-visitors)"
|
||||
strokeWidth={2}
|
||||
dot={{
|
||||
fill: 'var(--color-visitors)',
|
||||
}}
|
||||
activeDot={{
|
||||
r: 6,
|
||||
}}
|
||||
>
|
||||
<LabelList
|
||||
position="top"
|
||||
offset={12}
|
||||
className="fill-foreground"
|
||||
fontSize={12}
|
||||
dataKey="browser"
|
||||
formatter={(value: keyof typeof chartConfig) => chartConfig[value]?.label}
|
||||
/>
|
||||
</Line>
|
||||
</LineChart>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
<CardFooter className="flex-col items-start gap-2 text-sm">
|
||||
<div className="flex gap-2 font-medium leading-none">
|
||||
Trending up by 5.2% this month <TrendingUp className="h-4 w-4" />
|
||||
</div>
|
||||
<div className="leading-none text-muted-foreground">
|
||||
Showing total visitors for the last 6 months
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { TrendingUp } from 'lucide-react'
|
||||
import { CartesianGrid, LabelList, Line, LineChart, XAxis } from 'recharts'
|
||||
|
||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from 'ui'
|
||||
import { ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent } from 'ui'
|
||||
|
||||
export const description = 'A line chart with a label'
|
||||
|
||||
const chartData = [
|
||||
{ month: 'January', desktop: 186, mobile: 80 },
|
||||
{ month: 'February', desktop: 305, mobile: 200 },
|
||||
{ month: 'March', desktop: 237, mobile: 120 },
|
||||
{ month: 'April', desktop: 73, mobile: 190 },
|
||||
{ month: 'May', desktop: 209, mobile: 130 },
|
||||
{ month: 'June', desktop: 214, mobile: 140 },
|
||||
]
|
||||
|
||||
const chartConfig = {
|
||||
desktop: {
|
||||
label: 'Desktop',
|
||||
color: 'hsl(var(--chart-1))',
|
||||
},
|
||||
mobile: {
|
||||
label: 'Mobile',
|
||||
color: 'hsl(var(--chart-2))',
|
||||
},
|
||||
} satisfies ChartConfig
|
||||
|
||||
export default function Component() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Line Chart - Label</CardTitle>
|
||||
<CardDescription>January - June 2024</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ChartContainer config={chartConfig}>
|
||||
<LineChart
|
||||
accessibilityLayer
|
||||
data={chartData}
|
||||
margin={{
|
||||
top: 20,
|
||||
left: 12,
|
||||
right: 12,
|
||||
}}
|
||||
>
|
||||
<CartesianGrid vertical={false} />
|
||||
<XAxis
|
||||
dataKey="month"
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickMargin={8}
|
||||
tickFormatter={(value) => value.slice(0, 3)}
|
||||
/>
|
||||
<ChartTooltip cursor={false} content={<ChartTooltipContent indicator="line" />} />
|
||||
<Line
|
||||
dataKey="desktop"
|
||||
type="natural"
|
||||
stroke="var(--color-desktop)"
|
||||
strokeWidth={2}
|
||||
dot={{
|
||||
fill: 'var(--color-desktop)',
|
||||
}}
|
||||
activeDot={{
|
||||
r: 6,
|
||||
}}
|
||||
>
|
||||
<LabelList position="top" offset={12} className="fill-foreground" fontSize={12} />
|
||||
</Line>
|
||||
</LineChart>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
<CardFooter className="flex-col items-start gap-2 text-sm">
|
||||
<div className="flex gap-2 font-medium leading-none">
|
||||
Trending up by 5.2% this month <TrendingUp className="h-4 w-4" />
|
||||
</div>
|
||||
<div className="leading-none text-muted-foreground">
|
||||
Showing total visitors for the last 6 months
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { TrendingUp } from 'lucide-react'
|
||||
import { CartesianGrid, Line, LineChart, XAxis } from 'recharts'
|
||||
|
||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from 'ui'
|
||||
import { ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent } from 'ui'
|
||||
|
||||
export const description = 'A linear line chart'
|
||||
|
||||
const chartData = [
|
||||
{ month: 'January', desktop: 186 },
|
||||
{ month: 'February', desktop: 305 },
|
||||
{ month: 'March', desktop: 237 },
|
||||
{ month: 'April', desktop: 73 },
|
||||
{ month: 'May', desktop: 209 },
|
||||
{ month: 'June', desktop: 214 },
|
||||
]
|
||||
|
||||
const chartConfig = {
|
||||
desktop: {
|
||||
label: 'Desktop',
|
||||
color: 'hsl(var(--chart-1))',
|
||||
},
|
||||
} satisfies ChartConfig
|
||||
|
||||
export default function Component() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Line Chart - Linear</CardTitle>
|
||||
<CardDescription>January - June 2024</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ChartContainer config={chartConfig}>
|
||||
<LineChart
|
||||
accessibilityLayer
|
||||
data={chartData}
|
||||
margin={{
|
||||
left: 12,
|
||||
right: 12,
|
||||
}}
|
||||
>
|
||||
<CartesianGrid vertical={false} />
|
||||
<XAxis
|
||||
dataKey="month"
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickMargin={8}
|
||||
tickFormatter={(value) => value.slice(0, 3)}
|
||||
/>
|
||||
<ChartTooltip cursor={false} content={<ChartTooltipContent hideLabel />} />
|
||||
<Line
|
||||
dataKey="desktop"
|
||||
type="linear"
|
||||
stroke="var(--color-desktop)"
|
||||
strokeWidth={2}
|
||||
dot={false}
|
||||
/>
|
||||
</LineChart>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
<CardFooter className="flex-col items-start gap-2 text-sm">
|
||||
<div className="flex gap-2 font-medium leading-none">
|
||||
Trending up by 5.2% this month <TrendingUp className="h-4 w-4" />
|
||||
</div>
|
||||
<div className="leading-none text-muted-foreground">
|
||||
Showing total visitors for the last 6 months
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { TrendingUp } from 'lucide-react'
|
||||
import { CartesianGrid, Line, LineChart, XAxis } from 'recharts'
|
||||
|
||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from 'ui'
|
||||
import { ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent } from 'ui'
|
||||
|
||||
export const description = 'A multiple line chart'
|
||||
|
||||
const chartData = [
|
||||
{ month: 'January', desktop: 186, mobile: 80 },
|
||||
{ month: 'February', desktop: 305, mobile: 200 },
|
||||
{ month: 'March', desktop: 237, mobile: 120 },
|
||||
{ month: 'April', desktop: 73, mobile: 190 },
|
||||
{ month: 'May', desktop: 209, mobile: 130 },
|
||||
{ month: 'June', desktop: 214, mobile: 140 },
|
||||
]
|
||||
|
||||
const chartConfig = {
|
||||
desktop: {
|
||||
label: 'Desktop',
|
||||
color: 'hsl(var(--chart-1))',
|
||||
},
|
||||
mobile: {
|
||||
label: 'Mobile',
|
||||
color: 'hsl(var(--chart-2))',
|
||||
},
|
||||
} satisfies ChartConfig
|
||||
|
||||
export default function Component() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Line Chart - Multiple</CardTitle>
|
||||
<CardDescription>January - June 2024</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ChartContainer config={chartConfig}>
|
||||
<LineChart
|
||||
accessibilityLayer
|
||||
data={chartData}
|
||||
margin={{
|
||||
left: 12,
|
||||
right: 12,
|
||||
}}
|
||||
>
|
||||
<CartesianGrid vertical={false} />
|
||||
<XAxis
|
||||
dataKey="month"
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickMargin={8}
|
||||
tickFormatter={(value) => value.slice(0, 3)}
|
||||
/>
|
||||
<ChartTooltip cursor={false} content={<ChartTooltipContent />} />
|
||||
<Line
|
||||
dataKey="desktop"
|
||||
type="monotone"
|
||||
stroke="var(--color-desktop)"
|
||||
strokeWidth={2}
|
||||
dot={false}
|
||||
/>
|
||||
<Line
|
||||
dataKey="mobile"
|
||||
type="monotone"
|
||||
stroke="var(--color-mobile)"
|
||||
strokeWidth={2}
|
||||
dot={false}
|
||||
/>
|
||||
</LineChart>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<div className="flex w-full items-start gap-2 text-sm">
|
||||
<div className="grid gap-2">
|
||||
<div className="flex items-center gap-2 font-medium leading-none">
|
||||
Trending up by 5.2% this month <TrendingUp className="h-4 w-4" />
|
||||
</div>
|
||||
<div className="flex items-center gap-2 leading-none text-muted-foreground">
|
||||
Showing total visitors for the last 6 months
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { TrendingUp } from 'lucide-react'
|
||||
import { CartesianGrid, Line, LineChart, XAxis } from 'recharts'
|
||||
|
||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from 'ui'
|
||||
import { ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent } from 'ui'
|
||||
|
||||
export const description = 'A line chart with step'
|
||||
|
||||
const chartData = [
|
||||
{ month: 'January', desktop: 186 },
|
||||
{ month: 'February', desktop: 305 },
|
||||
{ month: 'March', desktop: 237 },
|
||||
{ month: 'April', desktop: 73 },
|
||||
{ month: 'May', desktop: 209 },
|
||||
{ month: 'June', desktop: 214 },
|
||||
]
|
||||
|
||||
const chartConfig = {
|
||||
desktop: {
|
||||
label: 'Desktop',
|
||||
color: 'hsl(var(--chart-1))',
|
||||
},
|
||||
} satisfies ChartConfig
|
||||
|
||||
export default function Component() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Line Chart - Step</CardTitle>
|
||||
<CardDescription>January - June 2024</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ChartContainer config={chartConfig}>
|
||||
<LineChart
|
||||
accessibilityLayer
|
||||
data={chartData}
|
||||
margin={{
|
||||
left: 12,
|
||||
right: 12,
|
||||
}}
|
||||
>
|
||||
<CartesianGrid vertical={false} />
|
||||
<XAxis
|
||||
dataKey="month"
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickMargin={8}
|
||||
tickFormatter={(value) => value.slice(0, 3)}
|
||||
/>
|
||||
<ChartTooltip cursor={false} content={<ChartTooltipContent hideLabel />} />
|
||||
<Line
|
||||
dataKey="desktop"
|
||||
type="step"
|
||||
stroke="var(--color-desktop)"
|
||||
strokeWidth={2}
|
||||
dot={false}
|
||||
/>
|
||||
</LineChart>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
<CardFooter className="flex-col items-start gap-2 text-sm">
|
||||
<div className="flex gap-2 font-medium leading-none">
|
||||
Trending up by 5.2% this month <TrendingUp className="h-4 w-4" />
|
||||
</div>
|
||||
<div className="leading-none text-muted-foreground">
|
||||
Showing total visitors for the last 6 months
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { TrendingUp } from 'lucide-react'
|
||||
import { Label, Pie, PieChart, Sector } from 'recharts'
|
||||
import { PieSectorDataItem } from 'recharts/types/polar/Pie'
|
||||
|
||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from 'ui'
|
||||
import { ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent } from 'ui'
|
||||
|
||||
export const description = 'A donut chart with an active sector'
|
||||
|
||||
const chartData = [
|
||||
{ browser: 'chrome', visitors: 275, fill: 'var(--color-chrome)' },
|
||||
{ browser: 'safari', visitors: 200, fill: 'var(--color-safari)' },
|
||||
{ browser: 'firefox', visitors: 187, fill: 'var(--color-firefox)' },
|
||||
{ browser: 'edge', visitors: 173, fill: 'var(--color-edge)' },
|
||||
{ browser: 'other', visitors: 90, fill: 'var(--color-other)' },
|
||||
]
|
||||
|
||||
const chartConfig = {
|
||||
visitors: {
|
||||
label: 'Visitors',
|
||||
},
|
||||
chrome: {
|
||||
label: 'Chrome',
|
||||
color: 'hsl(var(--chart-1))',
|
||||
},
|
||||
safari: {
|
||||
label: 'Safari',
|
||||
color: 'hsl(var(--chart-2))',
|
||||
},
|
||||
firefox: {
|
||||
label: 'Firefox',
|
||||
color: 'hsl(var(--chart-3))',
|
||||
},
|
||||
edge: {
|
||||
label: 'Edge',
|
||||
color: 'hsl(var(--chart-4))',
|
||||
},
|
||||
other: {
|
||||
label: 'Other',
|
||||
color: 'hsl(var(--chart-5))',
|
||||
},
|
||||
} satisfies ChartConfig
|
||||
|
||||
export default function Component() {
|
||||
return (
|
||||
<Card className="flex flex-col">
|
||||
<CardHeader className="items-center pb-0">
|
||||
<CardTitle>Pie Chart - Donut Active</CardTitle>
|
||||
<CardDescription>January - June 2024</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex-1 pb-0">
|
||||
<ChartContainer config={chartConfig} className="mx-auto aspect-square max-h-[250px]">
|
||||
<PieChart>
|
||||
<ChartTooltip cursor={false} content={<ChartTooltipContent hideLabel />} />
|
||||
<Pie
|
||||
data={chartData}
|
||||
dataKey="visitors"
|
||||
nameKey="browser"
|
||||
innerRadius={60}
|
||||
strokeWidth={5}
|
||||
activeIndex={0}
|
||||
activeShape={({ outerRadius = 0, ...props }: PieSectorDataItem) => (
|
||||
<Sector {...props} outerRadius={outerRadius + 10} />
|
||||
)}
|
||||
/>
|
||||
</PieChart>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
<CardFooter className="flex-col gap-2 text-sm">
|
||||
<div className="flex items-center gap-2 font-medium leading-none">
|
||||
Trending up by 5.2% this month <TrendingUp className="h-4 w-4" />
|
||||
</div>
|
||||
<div className="leading-none text-muted-foreground">
|
||||
Showing total visitors for the last 6 months
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -1,111 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import * as React from 'react'
|
||||
import { TrendingUp } from 'lucide-react'
|
||||
import { Label, Pie, PieChart } from 'recharts'
|
||||
|
||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from 'ui'
|
||||
import { ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent } from 'ui'
|
||||
|
||||
export const description = 'A donut chart with text'
|
||||
|
||||
const chartData = [
|
||||
{ browser: 'chrome', visitors: 275, fill: 'var(--color-chrome)' },
|
||||
{ browser: 'safari', visitors: 200, fill: 'var(--color-safari)' },
|
||||
{ browser: 'firefox', visitors: 287, fill: 'var(--color-firefox)' },
|
||||
{ browser: 'edge', visitors: 173, fill: 'var(--color-edge)' },
|
||||
{ browser: 'other', visitors: 190, fill: 'var(--color-other)' },
|
||||
]
|
||||
|
||||
const chartConfig = {
|
||||
visitors: {
|
||||
label: 'Visitors',
|
||||
},
|
||||
chrome: {
|
||||
label: 'Chrome',
|
||||
color: 'hsl(var(--chart-1))',
|
||||
},
|
||||
safari: {
|
||||
label: 'Safari',
|
||||
color: 'hsl(var(--chart-2))',
|
||||
},
|
||||
firefox: {
|
||||
label: 'Firefox',
|
||||
color: 'hsl(var(--chart-3))',
|
||||
},
|
||||
edge: {
|
||||
label: 'Edge',
|
||||
color: 'hsl(var(--chart-4))',
|
||||
},
|
||||
other: {
|
||||
label: 'Other',
|
||||
color: 'hsl(var(--chart-5))',
|
||||
},
|
||||
} satisfies ChartConfig
|
||||
|
||||
export default function Component() {
|
||||
const totalVisitors = React.useMemo(() => {
|
||||
return chartData.reduce((acc, curr) => acc + curr.visitors, 0)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Card className="flex flex-col">
|
||||
<CardHeader className="items-center pb-0">
|
||||
<CardTitle>Pie Chart - Donut with Text</CardTitle>
|
||||
<CardDescription>January - June 2024</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex-1 pb-0">
|
||||
<ChartContainer config={chartConfig} className="mx-auto aspect-square max-h-[250px]">
|
||||
<PieChart>
|
||||
<ChartTooltip cursor={false} content={<ChartTooltipContent hideLabel />} />
|
||||
<Pie
|
||||
data={chartData}
|
||||
dataKey="visitors"
|
||||
nameKey="browser"
|
||||
innerRadius={60}
|
||||
strokeWidth={5}
|
||||
>
|
||||
<Label
|
||||
content={({ viewBox }) => {
|
||||
if (viewBox && 'cx' in viewBox && 'cy' in viewBox) {
|
||||
return (
|
||||
<text
|
||||
x={viewBox.cx}
|
||||
y={viewBox.cy}
|
||||
textAnchor="middle"
|
||||
dominantBaseline="middle"
|
||||
>
|
||||
<tspan
|
||||
x={viewBox.cx}
|
||||
y={viewBox.cy}
|
||||
className="fill-foreground text-3xl font-bold"
|
||||
>
|
||||
{totalVisitors.toLocaleString()}
|
||||
</tspan>
|
||||
<tspan
|
||||
x={viewBox.cx}
|
||||
y={(viewBox.cy || 0) + 24}
|
||||
className="fill-muted-foreground"
|
||||
>
|
||||
Visitors
|
||||
</tspan>
|
||||
</text>
|
||||
)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Pie>
|
||||
</PieChart>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
<CardFooter className="flex-col gap-2 text-sm">
|
||||
<div className="flex items-center gap-2 font-medium leading-none">
|
||||
Trending up by 5.2% this month <TrendingUp className="h-4 w-4" />
|
||||
</div>
|
||||
<div className="leading-none text-muted-foreground">
|
||||
Showing total visitors for the last 6 months
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { TrendingUp } from 'lucide-react'
|
||||
import { Pie, PieChart } from 'recharts'
|
||||
|
||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from 'ui'
|
||||
import { ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent } from 'ui'
|
||||
|
||||
export const description = 'A donut chart'
|
||||
|
||||
const chartData = [
|
||||
{ browser: 'chrome', visitors: 275, fill: 'var(--color-chrome)' },
|
||||
{ browser: 'safari', visitors: 200, fill: 'var(--color-safari)' },
|
||||
{ browser: 'firefox', visitors: 187, fill: 'var(--color-firefox)' },
|
||||
{ browser: 'edge', visitors: 173, fill: 'var(--color-edge)' },
|
||||
{ browser: 'other', visitors: 90, fill: 'var(--color-other)' },
|
||||
]
|
||||
|
||||
const chartConfig = {
|
||||
visitors: {
|
||||
label: 'Visitors',
|
||||
},
|
||||
chrome: {
|
||||
label: 'Chrome',
|
||||
color: 'hsl(var(--chart-1))',
|
||||
},
|
||||
safari: {
|
||||
label: 'Safari',
|
||||
color: 'hsl(var(--chart-2))',
|
||||
},
|
||||
firefox: {
|
||||
label: 'Firefox',
|
||||
color: 'hsl(var(--chart-3))',
|
||||
},
|
||||
edge: {
|
||||
label: 'Edge',
|
||||
color: 'hsl(var(--chart-4))',
|
||||
},
|
||||
other: {
|
||||
label: 'Other',
|
||||
color: 'hsl(var(--chart-5))',
|
||||
},
|
||||
} satisfies ChartConfig
|
||||
|
||||
export default function Component() {
|
||||
return (
|
||||
<Card className="flex flex-col">
|
||||
<CardHeader className="items-center pb-0">
|
||||
<CardTitle>Pie Chart - Donut</CardTitle>
|
||||
<CardDescription>January - June 2024</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex-1 pb-0">
|
||||
<ChartContainer config={chartConfig} className="mx-auto aspect-square max-h-[250px]">
|
||||
<PieChart>
|
||||
<ChartTooltip cursor={false} content={<ChartTooltipContent hideLabel />} />
|
||||
<Pie data={chartData} dataKey="visitors" nameKey="browser" innerRadius={60} />
|
||||
</PieChart>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
<CardFooter className="flex-col gap-2 text-sm">
|
||||
<div className="flex items-center gap-2 font-medium leading-none">
|
||||
Trending up by 5.2% this month <TrendingUp className="h-4 w-4" />
|
||||
</div>
|
||||
<div className="leading-none text-muted-foreground">
|
||||
Showing total visitors for the last 6 months
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -1,170 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import * as React from 'react'
|
||||
import { Label, Pie, PieChart, Sector } from 'recharts'
|
||||
import { PieSectorDataItem } from 'recharts/types/polar/Pie'
|
||||
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from 'ui'
|
||||
import { ChartConfig, ChartContainer, ChartStyle, ChartTooltip, ChartTooltipContent } from 'ui'
|
||||
import {
|
||||
Select_Shadcn_,
|
||||
SelectContent_Shadcn_,
|
||||
SelectItem_Shadcn_,
|
||||
SelectTrigger_Shadcn_,
|
||||
SelectValue_Shadcn_,
|
||||
} from 'ui'
|
||||
|
||||
export const description = 'An interactive pie chart'
|
||||
|
||||
const desktopData = [
|
||||
{ month: 'january', desktop: 186, fill: 'var(--color-january)' },
|
||||
{ month: 'february', desktop: 305, fill: 'var(--color-february)' },
|
||||
{ month: 'march', desktop: 237, fill: 'var(--color-march)' },
|
||||
{ month: 'april', desktop: 173, fill: 'var(--color-april)' },
|
||||
{ month: 'may', desktop: 209, fill: 'var(--color-may)' },
|
||||
]
|
||||
|
||||
const chartConfig = {
|
||||
visitors: {
|
||||
label: 'Visitors',
|
||||
},
|
||||
desktop: {
|
||||
label: 'Desktop',
|
||||
},
|
||||
mobile: {
|
||||
label: 'Mobile',
|
||||
},
|
||||
january: {
|
||||
label: 'January',
|
||||
color: 'hsl(var(--chart-1))',
|
||||
},
|
||||
february: {
|
||||
label: 'February',
|
||||
color: 'hsl(var(--chart-2))',
|
||||
},
|
||||
march: {
|
||||
label: 'March',
|
||||
color: 'hsl(var(--chart-3))',
|
||||
},
|
||||
april: {
|
||||
label: 'April',
|
||||
color: 'hsl(var(--chart-4))',
|
||||
},
|
||||
may: {
|
||||
label: 'May',
|
||||
color: 'hsl(var(--chart-5))',
|
||||
},
|
||||
} satisfies ChartConfig
|
||||
|
||||
export default function Component() {
|
||||
const id = 'pie-interactive'
|
||||
const [activeMonth, setActiveMonth] = React.useState(desktopData[0].month)
|
||||
|
||||
const activeIndex = React.useMemo(
|
||||
() => desktopData.findIndex((item) => item.month === activeMonth),
|
||||
[activeMonth]
|
||||
)
|
||||
const months = React.useMemo(() => desktopData.map((item) => item.month), [])
|
||||
|
||||
return (
|
||||
<Card data-chart={id} className="flex flex-col">
|
||||
<ChartStyle id={id} config={chartConfig} />
|
||||
<CardHeader className="flex-row items-start space-y-0 pb-0">
|
||||
<div className="grid gap-1">
|
||||
<CardTitle>Pie Chart - Interactive</CardTitle>
|
||||
<CardDescription>January - June 2024</CardDescription>
|
||||
</div>
|
||||
<Select_Shadcn_ value={activeMonth} onValueChange={setActiveMonth}>
|
||||
<SelectTrigger_Shadcn_
|
||||
className="ml-auto h-7 w-[130px] rounded-lg pl-2.5"
|
||||
aria-label="Select a value"
|
||||
>
|
||||
<SelectValue_Shadcn_ placeholder="Select month" />
|
||||
</SelectTrigger_Shadcn_>
|
||||
<SelectContent_Shadcn_ align="end" className="rounded-xl">
|
||||
{months.map((key) => {
|
||||
const config = chartConfig[key as keyof typeof chartConfig]
|
||||
|
||||
if (!config) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<SelectItem_Shadcn_ key={key} value={key} className="rounded-lg [&_span]:flex">
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
<span
|
||||
className="flex h-3 w-3 shrink-0 rounded-sm"
|
||||
style={{
|
||||
backgroundColor: `var(--color-${key})`,
|
||||
}}
|
||||
/>
|
||||
{config?.label}
|
||||
</div>
|
||||
</SelectItem_Shadcn_>
|
||||
)
|
||||
})}
|
||||
</SelectContent_Shadcn_>
|
||||
</Select_Shadcn_>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-1 justify-center pb-0">
|
||||
<ChartContainer
|
||||
id={id}
|
||||
config={chartConfig}
|
||||
className="mx-auto aspect-square w-full max-w-[300px]"
|
||||
>
|
||||
<PieChart>
|
||||
<ChartTooltip cursor={false} content={<ChartTooltipContent hideLabel />} />
|
||||
<Pie
|
||||
data={desktopData}
|
||||
dataKey="desktop"
|
||||
nameKey="month"
|
||||
innerRadius={60}
|
||||
strokeWidth={5}
|
||||
activeIndex={activeIndex}
|
||||
activeShape={({ outerRadius = 0, ...props }: PieSectorDataItem) => (
|
||||
<g>
|
||||
<Sector {...props} outerRadius={outerRadius + 10} />
|
||||
<Sector
|
||||
{...props}
|
||||
outerRadius={outerRadius + 25}
|
||||
innerRadius={outerRadius + 12}
|
||||
/>
|
||||
</g>
|
||||
)}
|
||||
>
|
||||
<Label
|
||||
content={({ viewBox }) => {
|
||||
if (viewBox && 'cx' in viewBox && 'cy' in viewBox) {
|
||||
return (
|
||||
<text
|
||||
x={viewBox.cx}
|
||||
y={viewBox.cy}
|
||||
textAnchor="middle"
|
||||
dominantBaseline="middle"
|
||||
>
|
||||
<tspan
|
||||
x={viewBox.cx}
|
||||
y={viewBox.cy}
|
||||
className="fill-foreground text-3xl font-bold"
|
||||
>
|
||||
{desktopData[activeIndex].desktop.toLocaleString()}
|
||||
</tspan>
|
||||
<tspan
|
||||
x={viewBox.cx}
|
||||
y={(viewBox.cy || 0) + 24}
|
||||
className="fill-muted-foreground"
|
||||
>
|
||||
Visitors
|
||||
</tspan>
|
||||
</text>
|
||||
)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Pie>
|
||||
</PieChart>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { TrendingUp } from 'lucide-react'
|
||||
import { Pie, PieChart } from 'recharts'
|
||||
|
||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from 'ui'
|
||||
import { ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent } from 'ui'
|
||||
|
||||
export const description = 'A pie chart with a custom label'
|
||||
|
||||
const chartData = [
|
||||
{ browser: 'chrome', visitors: 275, fill: 'var(--color-chrome)' },
|
||||
{ browser: 'safari', visitors: 200, fill: 'var(--color-safari)' },
|
||||
{ browser: 'firefox', visitors: 187, fill: 'var(--color-firefox)' },
|
||||
{ browser: 'edge', visitors: 173, fill: 'var(--color-edge)' },
|
||||
{ browser: 'other', visitors: 90, fill: 'var(--color-other)' },
|
||||
]
|
||||
|
||||
const chartConfig = {
|
||||
visitors: {
|
||||
label: 'Visitors',
|
||||
},
|
||||
chrome: {
|
||||
label: 'Chrome',
|
||||
color: 'hsl(var(--chart-1))',
|
||||
},
|
||||
safari: {
|
||||
label: 'Safari',
|
||||
color: 'hsl(var(--chart-2))',
|
||||
},
|
||||
firefox: {
|
||||
label: 'Firefox',
|
||||
color: 'hsl(var(--chart-3))',
|
||||
},
|
||||
edge: {
|
||||
label: 'Edge',
|
||||
color: 'hsl(var(--chart-4))',
|
||||
},
|
||||
other: {
|
||||
label: 'Other',
|
||||
color: 'hsl(var(--chart-5))',
|
||||
},
|
||||
} satisfies ChartConfig
|
||||
|
||||
export default function Component() {
|
||||
return (
|
||||
<Card className="flex flex-col">
|
||||
<CardHeader className="items-center pb-0">
|
||||
<CardTitle>Pie Chart - Custom Label</CardTitle>
|
||||
<CardDescription>January - June 2024</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex-1 pb-0">
|
||||
<ChartContainer config={chartConfig} className="mx-auto aspect-square max-h-[250px]">
|
||||
<PieChart>
|
||||
<ChartTooltip content={<ChartTooltipContent nameKey="visitors" hideLabel />} />
|
||||
<Pie
|
||||
data={chartData}
|
||||
dataKey="visitors"
|
||||
labelLine={false}
|
||||
label={({ payload, ...props }) => {
|
||||
return (
|
||||
<text
|
||||
cx={props.cx}
|
||||
cy={props.cy}
|
||||
x={props.x}
|
||||
y={props.y}
|
||||
textAnchor={props.textAnchor}
|
||||
dominantBaseline={props.dominantBaseline}
|
||||
fill="hsla(var(--foreground))"
|
||||
>
|
||||
{`${
|
||||
chartConfig[payload.browser as keyof typeof chartConfig]?.label
|
||||
} (${payload.visitors})`}
|
||||
</text>
|
||||
)
|
||||
}}
|
||||
nameKey="browser"
|
||||
/>
|
||||
</PieChart>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
<CardFooter className="flex-col gap-2 text-sm">
|
||||
<div className="flex items-center gap-2 font-medium leading-none">
|
||||
Trending up by 5.2% this month <TrendingUp className="h-4 w-4" />
|
||||
</div>
|
||||
<div className="leading-none text-muted-foreground">
|
||||
Showing total visitors for the last 6 months
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { TrendingUp } from 'lucide-react'
|
||||
import { LabelList, Pie, PieChart } from 'recharts'
|
||||
|
||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from 'ui'
|
||||
import { ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent } from 'ui'
|
||||
|
||||
export const description = 'A pie chart with a label list'
|
||||
|
||||
const chartData = [
|
||||
{ browser: 'chrome', visitors: 275, fill: 'var(--color-chrome)' },
|
||||
{ browser: 'safari', visitors: 200, fill: 'var(--color-safari)' },
|
||||
{ browser: 'firefox', visitors: 187, fill: 'var(--color-firefox)' },
|
||||
{ browser: 'edge', visitors: 173, fill: 'var(--color-edge)' },
|
||||
{ browser: 'other', visitors: 90, fill: 'var(--color-other)' },
|
||||
]
|
||||
|
||||
const chartConfig = {
|
||||
visitors: {
|
||||
label: 'Visitors',
|
||||
},
|
||||
chrome: {
|
||||
label: 'Chrome',
|
||||
color: 'hsl(var(--chart-1))',
|
||||
},
|
||||
safari: {
|
||||
label: 'Safari',
|
||||
color: 'hsl(var(--chart-2))',
|
||||
},
|
||||
firefox: {
|
||||
label: 'Firefox',
|
||||
color: 'hsl(var(--chart-3))',
|
||||
},
|
||||
edge: {
|
||||
label: 'Edge',
|
||||
color: 'hsl(var(--chart-4))',
|
||||
},
|
||||
other: {
|
||||
label: 'Other',
|
||||
color: 'hsl(var(--chart-5))',
|
||||
},
|
||||
} satisfies ChartConfig
|
||||
|
||||
export default function Component() {
|
||||
return (
|
||||
<Card className="flex flex-col">
|
||||
<CardHeader className="items-center pb-0">
|
||||
<CardTitle>Pie Chart - Label List</CardTitle>
|
||||
<CardDescription>January - June 2024</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex-1 pb-0">
|
||||
<ChartContainer config={chartConfig} className="mx-auto aspect-square max-h-[250px]">
|
||||
<PieChart>
|
||||
<ChartTooltip content={<ChartTooltipContent nameKey="visitors" hideLabel />} />
|
||||
<Pie data={chartData} dataKey="visitors">
|
||||
<LabelList
|
||||
dataKey="browser"
|
||||
className="fill-background"
|
||||
stroke="none"
|
||||
fontSize={12}
|
||||
formatter={(value: keyof typeof chartConfig) => chartConfig[value]?.label}
|
||||
/>
|
||||
</Pie>
|
||||
</PieChart>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
<CardFooter className="flex-col gap-2 text-sm">
|
||||
<div className="flex items-center gap-2 font-medium leading-none">
|
||||
Trending up by 5.2% this month <TrendingUp className="h-4 w-4" />
|
||||
</div>
|
||||
<div className="leading-none text-muted-foreground">
|
||||
Showing total visitors for the last 6 months
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { TrendingUp } from 'lucide-react'
|
||||
import { Pie, PieChart } from 'recharts'
|
||||
|
||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from 'ui'
|
||||
import { ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent } from 'ui'
|
||||
|
||||
export const description = 'A pie chart with a label'
|
||||
|
||||
const chartData = [
|
||||
{ browser: 'chrome', visitors: 275, fill: 'var(--color-chrome)' },
|
||||
{ browser: 'safari', visitors: 200, fill: 'var(--color-safari)' },
|
||||
{ browser: 'firefox', visitors: 187, fill: 'var(--color-firefox)' },
|
||||
{ browser: 'edge', visitors: 173, fill: 'var(--color-edge)' },
|
||||
{ browser: 'other', visitors: 90, fill: 'var(--color-other)' },
|
||||
]
|
||||
|
||||
const chartConfig = {
|
||||
visitors: {
|
||||
label: 'Visitors',
|
||||
},
|
||||
chrome: {
|
||||
label: 'Chrome',
|
||||
color: 'hsl(var(--chart-1))',
|
||||
},
|
||||
safari: {
|
||||
label: 'Safari',
|
||||
color: 'hsl(var(--chart-2))',
|
||||
},
|
||||
firefox: {
|
||||
label: 'Firefox',
|
||||
color: 'hsl(var(--chart-3))',
|
||||
},
|
||||
edge: {
|
||||
label: 'Edge',
|
||||
color: 'hsl(var(--chart-4))',
|
||||
},
|
||||
other: {
|
||||
label: 'Other',
|
||||
color: 'hsl(var(--chart-5))',
|
||||
},
|
||||
} satisfies ChartConfig
|
||||
|
||||
export default function Component() {
|
||||
return (
|
||||
<Card className="flex flex-col">
|
||||
<CardHeader className="items-center pb-0">
|
||||
<CardTitle>Pie Chart - Label</CardTitle>
|
||||
<CardDescription>January - June 2024</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex-1 pb-0">
|
||||
<ChartContainer
|
||||
config={chartConfig}
|
||||
className="mx-auto aspect-square max-h-[250px] pb-0 [&_.recharts-pie-label-text]:fill-foreground"
|
||||
>
|
||||
<PieChart>
|
||||
<ChartTooltip content={<ChartTooltipContent hideLabel />} />
|
||||
<Pie data={chartData} dataKey="visitors" label nameKey="browser" />
|
||||
</PieChart>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
<CardFooter className="flex-col gap-2 text-sm">
|
||||
<div className="flex items-center gap-2 font-medium leading-none">
|
||||
Trending up by 5.2% this month <TrendingUp className="h-4 w-4" />
|
||||
</div>
|
||||
<div className="leading-none text-muted-foreground">
|
||||
Showing total visitors for the last 6 months
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { Pie, PieChart } from 'recharts'
|
||||
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from 'ui'
|
||||
import { ChartConfig, ChartContainer, ChartLegend, ChartLegendContent } from 'ui'
|
||||
|
||||
export const description = 'A pie chart with a legend'
|
||||
|
||||
const chartData = [
|
||||
{ browser: 'chrome', visitors: 275, fill: 'var(--color-chrome)' },
|
||||
{ browser: 'safari', visitors: 200, fill: 'var(--color-safari)' },
|
||||
{ browser: 'firefox', visitors: 187, fill: 'var(--color-firefox)' },
|
||||
{ browser: 'edge', visitors: 173, fill: 'var(--color-edge)' },
|
||||
{ browser: 'other', visitors: 90, fill: 'var(--color-other)' },
|
||||
]
|
||||
|
||||
const chartConfig = {
|
||||
visitors: {
|
||||
label: 'Visitors',
|
||||
},
|
||||
chrome: {
|
||||
label: 'Chrome',
|
||||
color: 'hsl(var(--chart-1))',
|
||||
},
|
||||
safari: {
|
||||
label: 'Safari',
|
||||
color: 'hsl(var(--chart-2))',
|
||||
},
|
||||
firefox: {
|
||||
label: 'Firefox',
|
||||
color: 'hsl(var(--chart-3))',
|
||||
},
|
||||
edge: {
|
||||
label: 'Edge',
|
||||
color: 'hsl(var(--chart-4))',
|
||||
},
|
||||
other: {
|
||||
label: 'Other',
|
||||
color: 'hsl(var(--chart-5))',
|
||||
},
|
||||
} satisfies ChartConfig
|
||||
|
||||
export default function Component() {
|
||||
return (
|
||||
<Card className="flex flex-col">
|
||||
<CardHeader className="items-center pb-0">
|
||||
<CardTitle>Pie Chart - Legend</CardTitle>
|
||||
<CardDescription>January - June 2024</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex-1 pb-0">
|
||||
<ChartContainer config={chartConfig} className="mx-auto aspect-square max-h-[300px]">
|
||||
<PieChart>
|
||||
<Pie data={chartData} dataKey="visitors" />
|
||||
<ChartLegend
|
||||
content={<ChartLegendContent nameKey="browser" />}
|
||||
className="-translate-y-2 flex-wrap gap-2 [&>*]:basis-1/4 [&>*]:justify-center"
|
||||
/>
|
||||
</PieChart>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { TrendingUp } from 'lucide-react'
|
||||
import { Pie, PieChart } from 'recharts'
|
||||
|
||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from 'ui'
|
||||
import { ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent } from 'ui'
|
||||
|
||||
export const description = 'A pie chart with no separator'
|
||||
|
||||
const chartData = [
|
||||
{ browser: 'chrome', visitors: 275, fill: 'var(--color-chrome)' },
|
||||
{ browser: 'safari', visitors: 200, fill: 'var(--color-safari)' },
|
||||
{ browser: 'firefox', visitors: 187, fill: 'var(--color-firefox)' },
|
||||
{ browser: 'edge', visitors: 173, fill: 'var(--color-edge)' },
|
||||
{ browser: 'other', visitors: 90, fill: 'var(--color-other)' },
|
||||
]
|
||||
|
||||
const chartConfig = {
|
||||
visitors: {
|
||||
label: 'Visitors',
|
||||
},
|
||||
chrome: {
|
||||
label: 'Chrome',
|
||||
color: 'hsl(var(--chart-1))',
|
||||
},
|
||||
safari: {
|
||||
label: 'Safari',
|
||||
color: 'hsl(var(--chart-2))',
|
||||
},
|
||||
firefox: {
|
||||
label: 'Firefox',
|
||||
color: 'hsl(var(--chart-3))',
|
||||
},
|
||||
edge: {
|
||||
label: 'Edge',
|
||||
color: 'hsl(var(--chart-4))',
|
||||
},
|
||||
other: {
|
||||
label: 'Other',
|
||||
color: 'hsl(var(--chart-5))',
|
||||
},
|
||||
} satisfies ChartConfig
|
||||
|
||||
export default function Component() {
|
||||
return (
|
||||
<Card className="flex flex-col">
|
||||
<CardHeader className="items-center pb-0">
|
||||
<CardTitle>Pie Chart - Separator None</CardTitle>
|
||||
<CardDescription>January - June 2024</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex-1 pb-0">
|
||||
<ChartContainer config={chartConfig} className="mx-auto aspect-square max-h-[250px]">
|
||||
<PieChart>
|
||||
<ChartTooltip cursor={false} content={<ChartTooltipContent hideLabel />} />
|
||||
<Pie data={chartData} dataKey="visitors" nameKey="browser" stroke="0" />
|
||||
</PieChart>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
<CardFooter className="flex-col gap-2 text-sm">
|
||||
<div className="flex items-center gap-2 font-medium leading-none">
|
||||
Trending up by 5.2% this month <TrendingUp className="h-4 w-4" />
|
||||
</div>
|
||||
<div className="leading-none text-muted-foreground">
|
||||
Showing total visitors for the last 6 months
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { TrendingUp } from 'lucide-react'
|
||||
import { Pie, PieChart } from 'recharts'
|
||||
|
||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from 'ui'
|
||||
import { ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent } from 'ui'
|
||||
|
||||
export const description = 'A simple pie chart'
|
||||
|
||||
const chartData = [
|
||||
{ browser: 'chrome', visitors: 275, fill: 'var(--color-chrome)' },
|
||||
{ browser: 'safari', visitors: 200, fill: 'var(--color-safari)' },
|
||||
{ browser: 'firefox', visitors: 187, fill: 'var(--color-firefox)' },
|
||||
{ browser: 'edge', visitors: 173, fill: 'var(--color-edge)' },
|
||||
{ browser: 'other', visitors: 90, fill: 'var(--color-other)' },
|
||||
]
|
||||
|
||||
const chartConfig = {
|
||||
visitors: {
|
||||
label: 'Visitors',
|
||||
},
|
||||
chrome: {
|
||||
label: 'Chrome',
|
||||
color: 'hsl(var(--chart-1))',
|
||||
},
|
||||
safari: {
|
||||
label: 'Safari',
|
||||
color: 'hsl(var(--chart-2))',
|
||||
},
|
||||
firefox: {
|
||||
label: 'Firefox',
|
||||
color: 'hsl(var(--chart-3))',
|
||||
},
|
||||
edge: {
|
||||
label: 'Edge',
|
||||
color: 'hsl(var(--chart-4))',
|
||||
},
|
||||
other: {
|
||||
label: 'Other',
|
||||
color: 'hsl(var(--chart-5))',
|
||||
},
|
||||
} satisfies ChartConfig
|
||||
|
||||
export default function Component() {
|
||||
return (
|
||||
<Card className="flex flex-col">
|
||||
<CardHeader className="items-center pb-0">
|
||||
<CardTitle>Pie Chart</CardTitle>
|
||||
<CardDescription>January - June 2024</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex-1 pb-0">
|
||||
<ChartContainer config={chartConfig} className="mx-auto aspect-square max-h-[250px]">
|
||||
<PieChart>
|
||||
<ChartTooltip cursor={false} content={<ChartTooltipContent hideLabel />} />
|
||||
<Pie data={chartData} dataKey="visitors" nameKey="browser" />
|
||||
</PieChart>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
<CardFooter className="flex-col gap-2 text-sm">
|
||||
<div className="flex items-center gap-2 font-medium leading-none">
|
||||
Trending up by 5.2% this month <TrendingUp className="h-4 w-4" />
|
||||
</div>
|
||||
<div className="leading-none text-muted-foreground">
|
||||
Showing total visitors for the last 6 months
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import * as React from 'react'
|
||||
import { TrendingUp } from 'lucide-react'
|
||||
import { Label, Pie, PieChart } from 'recharts'
|
||||
|
||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from 'ui'
|
||||
import { ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent } from 'ui'
|
||||
|
||||
export const description = 'A pie chart with stacked sections'
|
||||
|
||||
const desktopData = [
|
||||
{ month: 'january', desktop: 186, fill: 'var(--color-january)' },
|
||||
{ month: 'february', desktop: 305, fill: 'var(--color-february)' },
|
||||
{ month: 'march', desktop: 237, fill: 'var(--color-march)' },
|
||||
{ month: 'april', desktop: 173, fill: 'var(--color-april)' },
|
||||
{ month: 'may', desktop: 209, fill: 'var(--color-may)' },
|
||||
]
|
||||
|
||||
const mobileData = [
|
||||
{ month: 'january', mobile: 80, fill: 'var(--color-january)' },
|
||||
{ month: 'february', mobile: 200, fill: 'var(--color-february)' },
|
||||
{ month: 'march', mobile: 120, fill: 'var(--color-march)' },
|
||||
{ month: 'april', mobile: 190, fill: 'var(--color-april)' },
|
||||
{ month: 'may', mobile: 130, fill: 'var(--color-may)' },
|
||||
]
|
||||
|
||||
const chartConfig = {
|
||||
visitors: {
|
||||
label: 'Visitors',
|
||||
},
|
||||
desktop: {
|
||||
label: 'Desktop',
|
||||
},
|
||||
mobile: {
|
||||
label: 'Mobile',
|
||||
},
|
||||
january: {
|
||||
label: 'January',
|
||||
color: 'hsl(var(--chart-1))',
|
||||
},
|
||||
february: {
|
||||
label: 'February',
|
||||
color: 'hsl(var(--chart-2))',
|
||||
},
|
||||
march: {
|
||||
label: 'March',
|
||||
color: 'hsl(var(--chart-3))',
|
||||
},
|
||||
april: {
|
||||
label: 'April',
|
||||
color: 'hsl(var(--chart-4))',
|
||||
},
|
||||
may: {
|
||||
label: 'May',
|
||||
color: 'hsl(var(--chart-5))',
|
||||
},
|
||||
} satisfies ChartConfig
|
||||
|
||||
export default function Component() {
|
||||
return (
|
||||
<Card className="flex flex-col">
|
||||
<CardHeader className="items-center pb-0">
|
||||
<CardTitle>Pie Chart - Stacked</CardTitle>
|
||||
<CardDescription>January - June 2024</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex-1 pb-0">
|
||||
<ChartContainer config={chartConfig} className="mx-auto aspect-square max-h-[250px]">
|
||||
<PieChart>
|
||||
<ChartTooltip
|
||||
content={
|
||||
<ChartTooltipContent
|
||||
labelKey="visitors"
|
||||
nameKey="month"
|
||||
indicator="line"
|
||||
labelFormatter={(_, payload) => {
|
||||
return chartConfig[payload?.[0].dataKey as keyof typeof chartConfig].label
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Pie data={desktopData} dataKey="desktop" outerRadius={60} />
|
||||
<Pie data={mobileData} dataKey="mobile" innerRadius={70} outerRadius={90} />
|
||||
</PieChart>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
<CardFooter className="flex-col gap-2 text-sm">
|
||||
<div className="flex items-center gap-2 font-medium leading-none">
|
||||
Trending up by 5.2% this month <TrendingUp className="h-4 w-4" />
|
||||
</div>
|
||||
<div className="leading-none text-muted-foreground">
|
||||
Showing total visitors for the last 6 months
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { TrendingUp } from 'lucide-react'
|
||||
import { PolarAngleAxis, PolarGrid, Radar, RadarChart } from 'recharts'
|
||||
|
||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from 'ui'
|
||||
import { ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent } from 'ui'
|
||||
|
||||
export const description = 'A radar chart'
|
||||
|
||||
const chartData = [
|
||||
{ month: 'January', desktop: 186 },
|
||||
{ month: 'February', desktop: 305 },
|
||||
{ month: 'March', desktop: 237 },
|
||||
{ month: 'April', desktop: 273 },
|
||||
{ month: 'May', desktop: 209 },
|
||||
{ month: 'June', desktop: 214 },
|
||||
]
|
||||
|
||||
const chartConfig = {
|
||||
desktop: {
|
||||
label: 'Desktop',
|
||||
color: 'hsl(var(--chart-1))',
|
||||
},
|
||||
} satisfies ChartConfig
|
||||
|
||||
export default function Component() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="items-center pb-4">
|
||||
<CardTitle>Radar Chart</CardTitle>
|
||||
<CardDescription>Showing total visitors for the last 6 months</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="pb-0">
|
||||
<ChartContainer config={chartConfig} className="mx-auto aspect-square max-h-[250px]">
|
||||
<RadarChart data={chartData}>
|
||||
<ChartTooltip cursor={false} content={<ChartTooltipContent />} />
|
||||
<PolarAngleAxis dataKey="month" />
|
||||
<PolarGrid />
|
||||
<Radar dataKey="desktop" fill="var(--color-desktop)" fillOpacity={0.6} />
|
||||
</RadarChart>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
<CardFooter className="flex-col gap-2 text-sm">
|
||||
<div className="flex items-center gap-2 font-medium leading-none">
|
||||
Trending up by 5.2% this month <TrendingUp className="h-4 w-4" />
|
||||
</div>
|
||||
<div className="flex items-center gap-2 leading-none text-muted-foreground">
|
||||
January - June 2024
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { TrendingUp } from 'lucide-react'
|
||||
import { PolarAngleAxis, PolarGrid, Radar, RadarChart } from 'recharts'
|
||||
|
||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from 'ui'
|
||||
import { ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent } from 'ui'
|
||||
|
||||
export const description = 'A radar chart with dots'
|
||||
|
||||
const chartData = [
|
||||
{ month: 'January', desktop: 186 },
|
||||
{ month: 'February', desktop: 305 },
|
||||
{ month: 'March', desktop: 237 },
|
||||
{ month: 'April', desktop: 273 },
|
||||
{ month: 'May', desktop: 209 },
|
||||
{ month: 'June', desktop: 214 },
|
||||
]
|
||||
|
||||
const chartConfig = {
|
||||
desktop: {
|
||||
label: 'Desktop',
|
||||
color: 'hsl(var(--chart-1))',
|
||||
},
|
||||
} satisfies ChartConfig
|
||||
|
||||
export default function Component() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="items-center">
|
||||
<CardTitle>Radar Chart - Dots</CardTitle>
|
||||
<CardDescription>Showing total visitors for the last 6 months</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="pb-0">
|
||||
<ChartContainer config={chartConfig} className="mx-auto aspect-square max-h-[250px]">
|
||||
<RadarChart data={chartData}>
|
||||
<ChartTooltip cursor={false} content={<ChartTooltipContent />} />
|
||||
<PolarAngleAxis dataKey="month" />
|
||||
<PolarGrid />
|
||||
<Radar
|
||||
dataKey="desktop"
|
||||
fill="var(--color-desktop)"
|
||||
fillOpacity={0.6}
|
||||
dot={{
|
||||
r: 4,
|
||||
fillOpacity: 1,
|
||||
}}
|
||||
/>
|
||||
</RadarChart>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
<CardFooter className="flex-col gap-2 text-sm">
|
||||
<div className="flex items-center gap-2 font-medium leading-none">
|
||||
Trending up by 5.2% this month <TrendingUp className="h-4 w-4" />
|
||||
</div>
|
||||
<div className="flex items-center gap-2 leading-none text-muted-foreground">
|
||||
January - June 2024
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
Loaded 100 of 2862 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user