Revert "Unify workspace dropdowns; Update layouts (#458)"
This reverts commit 4bc8448a1d.
This commit is contained in:
@@ -55,10 +55,7 @@ create policy "projects_write" on public.projects for all
|
|||||||
|
|
||||||
Use `server-action-builder` skill for detailed patterns.
|
Use `server-action-builder` skill for detailed patterns.
|
||||||
|
|
||||||
**Rule: Services are decoupled from interfaces.** The service is pure logic that receives dependencies (database client,
|
**Rule: Services are decoupled from interfaces.** The service is pure logic that receives dependencies (database client, etc.) as arguments — it never imports framework-specific modules. The server action is a thin adapter that resolves dependencies and calls the service. This means the same service can be called from a server action, an MCP tool, a CLI command, or a unit test with zero changes.
|
||||||
etc.) as arguments — it never imports framework-specific modules. The server action is a thin adapter that resolves
|
|
||||||
dependencies and calls the service. This means the same service can be called from a server action, an MCP tool, a CLI
|
|
||||||
command, or a unit test with zero changes.
|
|
||||||
|
|
||||||
Create in route's `_lib/server/` directory:
|
Create in route's `_lib/server/` directory:
|
||||||
|
|
||||||
|
|||||||
@@ -19,8 +19,8 @@ export class AuthPageObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async signOut() {
|
async signOut() {
|
||||||
await this.page.click('[data-test="workspace-dropdown-trigger"]');
|
await this.page.click('[data-test="account-dropdown-trigger"]');
|
||||||
await this.page.click('[data-test="workspace-sign-out"]');
|
await this.page.click('[data-test="account-dropdown-sign-out"]');
|
||||||
}
|
}
|
||||||
|
|
||||||
async bootstrapUser(params: { email: string; password: string; name: string }) {
|
async bootstrapUser(params: { email: string; password: string; name: string }) {
|
||||||
@@ -47,19 +47,9 @@ export class AuthPageObject {
|
|||||||
## Common Selectors
|
## Common Selectors
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// Workspace dropdown (sidebar header - combined account switcher + user menu)
|
// Account dropdown
|
||||||
'[data-test="workspace-dropdown-trigger"]' // Opens the dropdown
|
'[data-test="account-dropdown-trigger"]'
|
||||||
'[data-test="workspace-switch-submenu"]' // Sub-trigger for workspace switching
|
'[data-test="account-dropdown-sign-out"]'
|
||||||
'[data-test="workspace-switch-content"]' // Sub-menu content with workspace list
|
|
||||||
'[data-test="workspace-team-item"]' // Individual team items in switcher
|
|
||||||
'[data-test="create-team-trigger"]' // Create team button in switcher
|
|
||||||
'[data-test="workspace-sign-out"]' // Sign out button
|
|
||||||
'[data-test="workspace-settings-link"]' // Settings link
|
|
||||||
'[data-test="account-dropdown-display-name"]' // User display name (inside dropdown panel)
|
|
||||||
|
|
||||||
// Opening the workspace switcher (two-step: open dropdown, then submenu)
|
|
||||||
await page.click('[data-test="workspace-dropdown-trigger"]');
|
|
||||||
await page.click('[data-test="workspace-switch-submenu"]');
|
|
||||||
|
|
||||||
// Navigation
|
// Navigation
|
||||||
'[data-test="sidebar-menu"]'
|
'[data-test="sidebar-menu"]'
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ export function CreateEntityForm() {
|
|||||||
reValidateMode: 'onChange',
|
reValidateMode: 'onChange',
|
||||||
});
|
});
|
||||||
|
|
||||||
const onSubmit = (data: z.output<typeof CreateEntitySchema>) => {
|
const onSubmit = (data: z.infer<typeof CreateEntitySchema>) => {
|
||||||
setError(false);
|
setError(false);
|
||||||
|
|
||||||
startTransition(async () => {
|
startTransition(async () => {
|
||||||
@@ -147,7 +147,7 @@ export function CreateEntityForm() {
|
|||||||
<If condition={error}>
|
<If condition={error}>
|
||||||
<Alert variant="destructive">
|
<Alert variant="destructive">
|
||||||
<AlertDescription>
|
<AlertDescription>
|
||||||
<Trans i18nKey="common.errors.generic" />
|
<Trans i18nKey="common:errors.generic" />
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
</If>
|
</If>
|
||||||
@@ -177,9 +177,9 @@ export function CreateEntityForm() {
|
|||||||
data-test="submit-entity-button"
|
data-test="submit-entity-button"
|
||||||
>
|
>
|
||||||
{pending ? (
|
{pending ? (
|
||||||
<Trans i18nKey="common.creating" />
|
<Trans i18nKey="common:creating" />
|
||||||
) : (
|
) : (
|
||||||
<Trans i18nKey="common.create" />
|
<Trans i18nKey="common:create" />
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</Form>
|
</Form>
|
||||||
|
|||||||
@@ -145,7 +145,7 @@ import { toast } from '@kit/ui/sonner';
|
|||||||
<If condition={error}>
|
<If condition={error}>
|
||||||
<Alert variant="destructive">
|
<Alert variant="destructive">
|
||||||
<AlertDescription>
|
<AlertDescription>
|
||||||
<Trans i18nKey="common.errors.generic" />
|
<Trans i18nKey="common:errors.generic" />
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
</If>
|
</If>
|
||||||
@@ -160,9 +160,9 @@ import { toast } from '@kit/ui/sonner';
|
|||||||
data-test="submit-button"
|
data-test="submit-button"
|
||||||
>
|
>
|
||||||
{pending ? (
|
{pending ? (
|
||||||
<Trans i18nKey="common.submitting" />
|
<Trans i18nKey="common:submitting" />
|
||||||
) : (
|
) : (
|
||||||
<Trans i18nKey="common.submit" />
|
<Trans i18nKey="common:submit" />
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
```
|
```
|
||||||
@@ -199,7 +199,7 @@ export function MyForm() {
|
|||||||
mode: 'onChange',
|
mode: 'onChange',
|
||||||
});
|
});
|
||||||
|
|
||||||
const onSubmit = (data: z.output<typeof MySchema>) => {
|
const onSubmit = (data: z.infer<typeof MySchema>) => {
|
||||||
setError(false);
|
setError(false);
|
||||||
|
|
||||||
startTransition(async () => {
|
startTransition(async () => {
|
||||||
@@ -220,7 +220,7 @@ export function MyForm() {
|
|||||||
<If condition={error}>
|
<If condition={error}>
|
||||||
<Alert variant="destructive">
|
<Alert variant="destructive">
|
||||||
<AlertDescription>
|
<AlertDescription>
|
||||||
<Trans i18nKey="common.errors.generic" />
|
<Trans i18nKey="common:errors.generic" />
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
</If>
|
</If>
|
||||||
|
|||||||
@@ -17,21 +17,19 @@ Create validation schema in `_lib/schemas/`:
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// _lib/schemas/feature.schema.ts
|
// _lib/schemas/feature.schema.ts
|
||||||
import * as z from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
export const CreateFeatureSchema = z.object({
|
export const CreateFeatureSchema = z.object({
|
||||||
name: z.string().min(1, 'Name is required'),
|
name: z.string().min(1, 'Name is required'),
|
||||||
accountId: z.string().uuid('Invalid account ID'),
|
accountId: z.string().uuid('Invalid account ID'),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type CreateFeatureInput = z.output<typeof CreateFeatureSchema>;
|
export type CreateFeatureInput = z.infer<typeof CreateFeatureSchema>;
|
||||||
```
|
```
|
||||||
|
|
||||||
### Step 2: Create Service Layer
|
### Step 2: Create Service Layer
|
||||||
|
|
||||||
**North star: services are decoupled from their interface.** The service is pure logic — it receives a database client
|
**North star: services are decoupled from their interface.** The service is pure logic — it receives a database client as a dependency, never imports one. This means the same service works whether called from a server action, an MCP tool, a CLI command, or a plain unit test.
|
||||||
as a dependency, never imports one. This means the same service works whether called from a server action, an MCP tool,
|
|
||||||
a CLI command, or a plain unit test.
|
|
||||||
|
|
||||||
Create service in `_lib/server/`:
|
Create service in `_lib/server/`:
|
||||||
|
|
||||||
@@ -64,13 +62,11 @@ class FeatureService {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
The service never calls `getSupabaseServerClient()` — the caller provides the client. This keeps the service testable (
|
The service never calls `getSupabaseServerClient()` — the caller provides the client. This keeps the service testable (pass a mock client) and reusable (any interface can supply its own client).
|
||||||
pass a mock client) and reusable (any interface can supply its own client).
|
|
||||||
|
|
||||||
### Step 3: Create Server Action (Thin Adapter)
|
### Step 3: Create Server Action (Thin Adapter)
|
||||||
|
|
||||||
The action is a **thin adapter** — it resolves dependencies (client, logger) and delegates to the service. No business
|
The action is a **thin adapter** — it resolves dependencies (client, logger) and delegates to the service. No business logic lives here.
|
||||||
logic lives here.
|
|
||||||
|
|
||||||
Create action in `_lib/server/server-actions.ts`:
|
Create action in `_lib/server/server-actions.ts`:
|
||||||
|
|
||||||
@@ -111,18 +107,13 @@ export const createFeatureAction = enhanceAction(
|
|||||||
|
|
||||||
## Key Patterns
|
## Key Patterns
|
||||||
|
|
||||||
1. **Services are pure, interfaces are thin adapters.** The service contains all business logic. The server action (or
|
1. **Services are pure, interfaces are thin adapters.** The service contains all business logic. The server action (or MCP tool, or CLI command) is glue code that resolves dependencies and calls the service. If an MCP tool and a server action do the same thing, they call the same service function.
|
||||||
MCP tool, or CLI command) is glue code that resolves dependencies and calls the service. If an MCP tool and a server
|
2. **Inject dependencies, don't import them in services.** Services receive their database client, logger, or any I/O capability as constructor arguments — never by importing framework-specific modules. This keeps them testable with stubs and reusable across interfaces.
|
||||||
action do the same thing, they call the same service function.
|
|
||||||
2. **Inject dependencies, don't import them in services.** Services receive their database client, logger, or any I/O
|
|
||||||
capability as constructor arguments — never by importing framework-specific modules. This keeps them testable with
|
|
||||||
stubs and reusable across interfaces.
|
|
||||||
3. **Schema in separate file** - Reusable between client and server
|
3. **Schema in separate file** - Reusable between client and server
|
||||||
4. **Logging** - Always log before and after operations
|
4. **Logging** - Always log before and after operations
|
||||||
5. **Revalidation** - Use `revalidatePath` after mutations
|
5. **Revalidation** - Use `revalidatePath` after mutations
|
||||||
6. **Trust RLS** - Don't add manual auth checks (RLS handles it)
|
6. **Trust RLS** - Don't add manual auth checks (RLS handles it)
|
||||||
7. **Testable in isolation** - Because services accept their dependencies, you can test them with a mock client and no
|
7. **Testable in isolation** - Because services accept their dependencies, you can test them with a mock client and no running infrastructure
|
||||||
running infrastructure
|
|
||||||
|
|
||||||
## File Structure
|
## File Structure
|
||||||
|
|
||||||
|
|||||||
@@ -28,10 +28,10 @@ export const myAction = enhanceAction(
|
|||||||
|
|
||||||
### Handler Parameters
|
### Handler Parameters
|
||||||
|
|
||||||
| Parameter | Type | Description |
|
| Parameter | Type | Description |
|
||||||
|-----------|--------------------|------------------------------------|
|
|-----------|------|-------------|
|
||||||
| `data` | `z.output<Schema>` | Validated input data |
|
| `data` | `z.infer<Schema>` | Validated input data |
|
||||||
| `user` | `User` | Authenticated user (if auth: true) |
|
| `user` | `User` | Authenticated user (if auth: true) |
|
||||||
|
|
||||||
## enhanceRouteHandler API
|
## enhanceRouteHandler API
|
||||||
|
|
||||||
@@ -69,7 +69,7 @@ export const GET = enhanceRouteHandler(
|
|||||||
## Common Zod Patterns
|
## Common Zod Patterns
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import * as z from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
// Basic schema
|
// Basic schema
|
||||||
export const CreateItemSchema = z.object({
|
export const CreateItemSchema = z.object({
|
||||||
|
|||||||
@@ -9,9 +9,7 @@ You are an expert at building pure, testable services that are decoupled from th
|
|||||||
|
|
||||||
## North Star
|
## North Star
|
||||||
|
|
||||||
**Every service is decoupled from its interface (I/O).** A service takes plain data in, does work, and returns plain
|
**Every service is decoupled from its interface (I/O).** A service takes plain data in, does work, and returns plain data out. It has no knowledge of whether it was called from an MCP tool, a server action, a CLI command, a route handler, or a test. The caller is a thin adapter that resolves dependencies and delegates.
|
||||||
data out. It has no knowledge of whether it was called from an MCP tool, a server action, a CLI command, a route
|
|
||||||
handler, or a test. The caller is a thin adapter that resolves dependencies and delegates.
|
|
||||||
|
|
||||||
## Workflow
|
## Workflow
|
||||||
|
|
||||||
@@ -23,7 +21,7 @@ Start with the input/output types. These are plain TypeScript — no framework t
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// _lib/schemas/project.schema.ts
|
// _lib/schemas/project.schema.ts
|
||||||
import * as z from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
export const CreateProjectSchema = z.object({
|
export const CreateProjectSchema = z.object({
|
||||||
name: z.string().min(1),
|
name: z.string().min(1),
|
||||||
@@ -42,8 +40,7 @@ export interface Project {
|
|||||||
|
|
||||||
### Step 2: Build the Service
|
### Step 2: Build the Service
|
||||||
|
|
||||||
The service receives all dependencies through its constructor. It never imports framework-specific modules (
|
The service receives all dependencies through its constructor. It never imports framework-specific modules (`getSupabaseServerClient`, `getLogger`, `revalidatePath`, etc.).
|
||||||
`getSupabaseServerClient`, `getLogger`, `revalidatePath`, etc.).
|
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// _lib/server/project.service.ts
|
// _lib/server/project.service.ts
|
||||||
@@ -98,8 +95,7 @@ class ProjectService {
|
|||||||
|
|
||||||
### Step 3: Write Thin Adapters
|
### Step 3: Write Thin Adapters
|
||||||
|
|
||||||
Each interface is a thin adapter — it resolves dependencies, calls the service, and handles interface-specific
|
Each interface is a thin adapter — it resolves dependencies, calls the service, and handles interface-specific concerns (revalidation, redirects, MCP formatting, CLI output).
|
||||||
concerns (revalidation, redirects, MCP formatting, CLI output).
|
|
||||||
|
|
||||||
**Server Action adapter:**
|
**Server Action adapter:**
|
||||||
|
|
||||||
@@ -238,32 +234,27 @@ describe('ProjectService', () => {
|
|||||||
|
|
||||||
## Rules
|
## Rules
|
||||||
|
|
||||||
1. **Services are pure functions over data.** Plain objects/primitives in, plain objects/primitives out. No `Request`/
|
1. **Services are pure functions over data.** Plain objects/primitives in, plain objects/primitives out. No `Request`/`Response`, no MCP context, no `FormData`.
|
||||||
`Response`, no MCP context, no `FormData`.
|
|
||||||
|
|
||||||
2. **Inject dependencies, never import them.** The service receives its database client, storage client, or any I/O
|
2. **Inject dependencies, never import them.** The service receives its database client, storage client, or any I/O capability as a constructor argument. Never call `getSupabaseServerClient()` inside a service.
|
||||||
capability as a constructor argument. Never call `getSupabaseServerClient()` inside a service.
|
|
||||||
|
|
||||||
3. **Adapters are trivial glue.** A server action resolves the client, calls the service, and handles `revalidatePath`.
|
3. **Adapters are trivial glue.** A server action resolves the client, calls the service, and handles `revalidatePath`. An MCP tool resolves the client, calls the service, and formats the response. No business logic in adapters.
|
||||||
An MCP tool resolves the client, calls the service, and formats the response. No business logic in adapters.
|
|
||||||
|
|
||||||
4. **One service, many callers.** If two interfaces do the same thing, they call the same service function. Duplicating
|
4. **One service, many callers.** If two interfaces do the same thing, they call the same service function. Duplicating logic is a violation.
|
||||||
logic is a violation.
|
|
||||||
|
|
||||||
5. **Testable in isolation.** Pass a mock client, assert the output. If you need a running database to test a service,
|
5. **Testable in isolation.** Pass a mock client, assert the output. If you need a running database to test a service, refactor until you don't.
|
||||||
refactor until you don't.
|
|
||||||
|
|
||||||
## What Goes Where
|
## What Goes Where
|
||||||
|
|
||||||
| Concern | Location | Example |
|
| Concern | Location | Example |
|
||||||
|------------------------|-------------------------------------------|-------------------------------------------|
|
|---------|----------|---------|
|
||||||
| Input validation (Zod) | `_lib/schemas/` | `CreateProjectSchema` |
|
| Input validation (Zod) | `_lib/schemas/` | `CreateProjectSchema` |
|
||||||
| Business logic | `_lib/server/*.service.ts` | `ProjectService.create()` |
|
| Business logic | `_lib/server/*.service.ts` | `ProjectService.create()` |
|
||||||
| Auth check | Adapter (`enhanceAction({ auth: true })`) | Server action wrapper |
|
| Auth check | Adapter (`enhanceAction({ auth: true })`) | Server action wrapper |
|
||||||
| Logging | Adapter | `logger.info()` before/after service call |
|
| Logging | Adapter | `logger.info()` before/after service call |
|
||||||
| Cache revalidation | Adapter | `revalidatePath()` after mutation |
|
| Cache revalidation | Adapter | `revalidatePath()` after mutation |
|
||||||
| Redirect | Adapter | `redirect()` after creation |
|
| Redirect | Adapter | `redirect()` after creation |
|
||||||
| MCP response format | Adapter | Return service result as MCP content |
|
| MCP response format | Adapter | Return service result as MCP content |
|
||||||
|
|
||||||
## File Structure
|
## File Structure
|
||||||
|
|
||||||
@@ -314,5 +305,4 @@ const result = await client.from('projects').insert(...).select().single();
|
|||||||
|
|
||||||
## Reference
|
## Reference
|
||||||
|
|
||||||
See `[Examples](examples.md)` for more patterns including services with multiple dependencies, services that compose
|
See `[Examples](examples.md)` for more patterns including services with multiple dependencies, services that compose other services, and testing strategies.
|
||||||
other services, and testing strategies.
|
|
||||||
|
|||||||
2
.github/workflows/workflow.yml
vendored
2
.github/workflows/workflow.yml
vendored
@@ -3,7 +3,7 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches: [ main ]
|
branches: [ main ]
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [ main, v3 ]
|
branches: [ main ]
|
||||||
jobs:
|
jobs:
|
||||||
typescript:
|
typescript:
|
||||||
name: ʦ TypeScript
|
name: ʦ TypeScript
|
||||||
|
|||||||
@@ -556,8 +556,8 @@ function MyFeaturePage() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<MyFeatureHeader
|
<MyFeatureHeader
|
||||||
title={<Trans i18nKey={'common.routes.myFeature'} />}
|
title={<Trans i18nKey={'common:routes.myFeature'} />}
|
||||||
description={<Trans i18nKey={'common.myFeatureDescription'} />}
|
description={<Trans i18nKey={'common:myFeatureDescription'} />}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<PageBody>
|
<PageBody>
|
||||||
@@ -829,40 +829,40 @@ import { ProfileAvatar } from '@kit/ui/profile-avatar';
|
|||||||
|
|
||||||
## Core Shadcn UI Components
|
## Core Shadcn UI Components
|
||||||
|
|
||||||
| Component | Description | Import Path |
|
| Component | Description | Import Path |
|
||||||
|------------------|-------------------------------------------|-------------------------------------------------------------------------------------------------|
|
|-----------|-------------|-------------|
|
||||||
| `Accordion` | Expandable/collapsible content sections | `@kit/ui/accordion` [accordion.tsx](mdc:packages/ui/src/shadcn/accordion.tsx) |
|
| `Accordion` | Expandable/collapsible content sections | `@kit/ui/accordion` [accordion.tsx](mdc:packages/ui/src/shadcn/accordion.tsx) |
|
||||||
| `AlertDialog` | Modal dialog for important actions | `@kit/ui/alert-dialog` [alert-dialog.tsx](mdc:packages/ui/src/shadcn/alert-dialog.tsx) |
|
| `AlertDialog` | Modal dialog for important actions | `@kit/ui/alert-dialog` [alert-dialog.tsx](mdc:packages/ui/src/shadcn/alert-dialog.tsx) |
|
||||||
| `Alert` | Status/notification messages | `@kit/ui/alert` [alert.tsx](mdc:packages/ui/src/shadcn/alert.tsx) |
|
| `Alert` | Status/notification messages | `@kit/ui/alert` [alert.tsx](mdc:packages/ui/src/shadcn/alert.tsx) |
|
||||||
| `Avatar` | User profile images with fallback | `@kit/ui/avatar` [avatar.tsx](mdc:packages/ui/src/shadcn/avatar.tsx) |
|
| `Avatar` | User profile images with fallback | `@kit/ui/avatar` [avatar.tsx](mdc:packages/ui/src/shadcn/avatar.tsx) |
|
||||||
| `Badge` | Small status indicators | `@kit/ui/badge` [badge.tsx](mdc:packages/ui/src/shadcn/badge.tsx) |
|
| `Badge` | Small status indicators | `@kit/ui/badge` [badge.tsx](mdc:packages/ui/src/shadcn/badge.tsx) |
|
||||||
| `Breadcrumb` | Navigation path indicators | `@kit/ui/breadcrumb` [breadcrumb.tsx](mdc:packages/ui/src/shadcn/breadcrumb.tsx) |
|
| `Breadcrumb` | Navigation path indicators | `@kit/ui/breadcrumb` [breadcrumb.tsx](mdc:packages/ui/src/shadcn/breadcrumb.tsx) |
|
||||||
| `Button` | Clickable action elements | `@kit/ui/button` [button.tsx](mdc:packages/ui/src/shadcn/button.tsx) |
|
| `Button` | Clickable action elements | `@kit/ui/button` [button.tsx](mdc:packages/ui/src/shadcn/button.tsx) |
|
||||||
| `Calendar` | Date picker and date display | `@kit/ui/calendar` [calendar.tsx](mdc:packages/ui/src/shadcn/calendar.tsx) |
|
| `Calendar` | Date picker and date display | `@kit/ui/calendar` [calendar.tsx](mdc:packages/ui/src/shadcn/calendar.tsx) |
|
||||||
| `Card` | Container for grouped content | `@kit/ui/card` [card.tsx](mdc:packages/ui/src/shadcn/card.tsx) |
|
| `Card` | Container for grouped content | `@kit/ui/card` [card.tsx](mdc:packages/ui/src/shadcn/card.tsx) |
|
||||||
| `Checkbox` | Selection input | `@kit/ui/checkbox` [checkbox.tsx](mdc:packages/ui/src/shadcn/checkbox.tsx) |
|
| `Checkbox` | Selection input | `@kit/ui/checkbox` [checkbox.tsx](mdc:packages/ui/src/shadcn/checkbox.tsx) |
|
||||||
| `Command` | Command palette interface | `@kit/ui/command` [command.tsx](mdc:packages/ui/src/shadcn/command.tsx) |
|
| `Command` | Command palette interface | `@kit/ui/command` [command.tsx](mdc:packages/ui/src/shadcn/command.tsx) |
|
||||||
| `DataTable` | Table | `@kit/ui/data-table` [data-table.tsx](mdc:packages/ui/src/shadcn/data-table.tsx) |
|
| `DataTable` | Table | `@kit/ui/data-table` [data-table.tsx](mdc:packages/ui/src/shadcn/data-table.tsx) |
|
||||||
| `Dialog` | Modal window for focused interactions | `@kit/ui/dialog` [dialog.tsx](mdc:packages/ui/src/shadcn/dialog.tsx) |
|
| `Dialog` | Modal window for focused interactions | `@kit/ui/dialog` [dialog.tsx](mdc:packages/ui/src/shadcn/dialog.tsx) |
|
||||||
| `DropdownMenu` | Menu triggered by a button | `@kit/ui/dropdown-menu` [dropdown-menu.tsx](mdc:packages/ui/src/shadcn/dropdown-menu.tsx) |
|
| `DropdownMenu` | Menu triggered by a button | `@kit/ui/dropdown-menu` [dropdown-menu.tsx](mdc:packages/ui/src/shadcn/dropdown-menu.tsx) |
|
||||||
| `Form` | Form components with validation | `@kit/ui/form` [form.tsx](mdc:packages/ui/src/shadcn/form.tsx) |
|
| `Form` | Form components with validation | `@kit/ui/form` [form.tsx](mdc:packages/ui/src/shadcn/form.tsx) |
|
||||||
| `Input` | Text input field | `@kit/ui/input` [input.tsx](mdc:packages/ui/src/shadcn/input.tsx) |
|
| `Input` | Text input field | `@kit/ui/input` [input.tsx](mdc:packages/ui/src/shadcn/input.tsx) |
|
||||||
| `Input OTP` | OTP Text input field | `@kit/ui/input-otp` [input-otp.tsx](mdc:packages/ui/src/shadcn/input-otp.tsx) |
|
| `Input OTP` | OTP Text input field | `@kit/ui/input-otp` [input-otp.tsx](mdc:packages/ui/src/shadcn/input-otp.tsx) |
|
||||||
| `Label` | Text label for form elements | `@kit/ui/label` [label.tsx](mdc:packages/ui/src/shadcn/label.tsx) |
|
| `Label` | Text label for form elements | `@kit/ui/label` [label.tsx](mdc:packages/ui/src/shadcn/label.tsx) |
|
||||||
| `NavigationMenu` | Hierarchical navigation component | `@kit/ui/navigation-menu` [navigation-menu.tsx](mdc:packages/ui/src/shadcn/navigation-menu.tsx) |
|
| `NavigationMenu` | Hierarchical navigation component | `@kit/ui/navigation-menu` [navigation-menu.tsx](mdc:packages/ui/src/shadcn/navigation-menu.tsx) |
|
||||||
| `Popover` | Floating content triggered by interaction | `@kit/ui/popover` [popover.tsx](mdc:packages/ui/src/shadcn/popover.tsx) |
|
| `Popover` | Floating content triggered by interaction | `@kit/ui/popover` [popover.tsx](mdc:packages/ui/src/shadcn/popover.tsx) |
|
||||||
| `RadioGroup` | Radio button selection group | `@kit/ui/radio-group` [radio-group.tsx](mdc:packages/ui/src/shadcn/radio-group.tsx) |
|
| `RadioGroup` | Radio button selection group | `@kit/ui/radio-group` [radio-group.tsx](mdc:packages/ui/src/shadcn/radio-group.tsx) |
|
||||||
| `ScrollArea` | Customizable scrollable area | `@kit/ui/scroll-area` [scroll-area.tsx](mdc:packages/ui/src/shadcn/scroll-area.tsx) |
|
| `ScrollArea` | Customizable scrollable area | `@kit/ui/scroll-area` [scroll-area.tsx](mdc:packages/ui/src/shadcn/scroll-area.tsx) |
|
||||||
| `Select` | Dropdown selection menu | `@kit/ui/select` [select.tsx](mdc:packages/ui/src/shadcn/select.tsx) |
|
| `Select` | Dropdown selection menu | `@kit/ui/select` [select.tsx](mdc:packages/ui/src/shadcn/select.tsx) |
|
||||||
| `Separator` | Visual divider between content | `@kit/ui/separator` [separator.tsx](mdc:packages/ui/src/shadcn/separator.tsx) |
|
| `Separator` | Visual divider between content | `@kit/ui/separator` [separator.tsx](mdc:packages/ui/src/shadcn/separator.tsx) |
|
||||||
| `Sheet` | Sliding panel from screen edge | `@kit/ui/sheet` [sheet.tsx](mdc:packages/ui/src/shadcn/sheet.tsx) |
|
| `Sheet` | Sliding panel from screen edge | `@kit/ui/sheet` [sheet.tsx](mdc:packages/ui/src/shadcn/sheet.tsx) |
|
||||||
| `Sidebar` | Advanced sidebar navigation | `@kit/ui/sidebar` [sidebar.tsx](mdc:packages/ui/src/shadcn/sidebar.tsx) |
|
| `Sidebar` | Advanced sidebar navigation | `@kit/ui/shadcn-sidebar` [sidebar.tsx](mdc:packages/ui/src/shadcn/sidebar.tsx) |
|
||||||
| `Skeleton` | Loading placeholder | `@kit/ui/skeleton` [skeleton.tsx](mdc:packages/ui/src/shadcn/skeleton.tsx) |
|
| `Skeleton` | Loading placeholder | `@kit/ui/skeleton` [skeleton.tsx](mdc:packages/ui/src/shadcn/skeleton.tsx) |
|
||||||
| `Switch` | Toggle control | `@kit/ui/switch` [switch.tsx](mdc:packages/ui/src/shadcn/switch.tsx) |
|
| `Switch` | Toggle control | `@kit/ui/switch` [switch.tsx](mdc:packages/ui/src/shadcn/switch.tsx) |
|
||||||
| `Toast` | Toaster | `@kit/ui/sonner` [sonner.tsx](mdc:packages/ui/src/shadcn/sonner.tsx) |
|
| `Toast` | Toaster | `@kit/ui/sonner` [sonner.tsx](mdc:packages/ui/src/shadcn/sonner.tsx) |
|
||||||
| `Tabs` | Tab-based navigation | `@kit/ui/tabs` [tabs.tsx](mdc:packages/ui/src/shadcn/tabs.tsx) |
|
| `Tabs` | Tab-based navigation | `@kit/ui/tabs` [tabs.tsx](mdc:packages/ui/src/shadcn/tabs.tsx) |
|
||||||
| `Textarea` | Multi-line text input | `@kit/ui/textarea` [textarea.tsx](mdc:packages/ui/src/shadcn/textarea.tsx) |
|
| `Textarea` | Multi-line text input | `@kit/ui/textarea` [textarea.tsx](mdc:packages/ui/src/shadcn/textarea.tsx) |
|
||||||
| `Tooltip` | Contextual information on hover | `@kit/ui/tooltip` [tooltip.tsx](mdc:packages/ui/src/shadcn/tooltip.tsx) |
|
| `Tooltip` | Contextual information on hover | `@kit/ui/tooltip` [tooltip.tsx](mdc:packages/ui/src/shadcn/tooltip.tsx) |
|
||||||
|
|
||||||
## Makerkit-specific Components
|
## Makerkit-specific Components
|
||||||
|
|
||||||
@@ -920,7 +920,7 @@ Zod schemas should be defined in the `schema` folder and exported, so we can reu
|
|||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
// _lib/schema/create-note.schema.ts
|
// _lib/schema/create-note.schema.ts
|
||||||
import * as z from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
export const CreateNoteSchema = z.object({
|
export const CreateNoteSchema = z.object({
|
||||||
title: z.string().min(1),
|
title: z.string().min(1),
|
||||||
@@ -935,7 +935,7 @@ Server Actions [server-actions.mdc](mdc:.cursor/rules/server-actions.mdc) can he
|
|||||||
```tsx
|
```tsx
|
||||||
'use server';
|
'use server';
|
||||||
|
|
||||||
import * as z from 'zod';
|
import { z } from 'zod';
|
||||||
import { enhanceAction } from '@kit/next/actions';
|
import { enhanceAction } from '@kit/next/actions';
|
||||||
import { CreateNoteSchema } from '../schema/create-note.schema';
|
import { CreateNoteSchema } from '../schema/create-note.schema';
|
||||||
|
|
||||||
@@ -965,7 +965,7 @@ Then create a client component to handle the form submission:
|
|||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import * as z from 'zod';
|
import { z } from 'zod';
|
||||||
import { Textarea } from '@kit/ui/textarea';
|
import { Textarea } from '@kit/ui/textarea';
|
||||||
import { Input } from '@kit/ui/input';
|
import { Input } from '@kit/ui/input';
|
||||||
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@kit/ui/form';
|
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@kit/ui/form';
|
||||||
@@ -1436,7 +1436,7 @@ You always must use `(security_invoker = true)` for views.
|
|||||||
```tsx
|
```tsx
|
||||||
'use server';
|
'use server';
|
||||||
|
|
||||||
import * as z from 'zod';
|
import { z } from 'zod';
|
||||||
import { enhanceAction } from '@kit/next/actions';
|
import { enhanceAction } from '@kit/next/actions';
|
||||||
import { EntitySchema } from '../entity.schema.ts`;
|
import { EntitySchema } from '../entity.schema.ts`;
|
||||||
|
|
||||||
@@ -1463,7 +1463,7 @@ export const myServerAction = enhanceAction(
|
|||||||
- To create API routes (route.ts), always use the `enhanceRouteHandler` function from the "@kit/supabase/routes" package. [index.ts](mdc:packages/next/src/routes/index.ts)
|
- To create API routes (route.ts), always use the `enhanceRouteHandler` function from the "@kit/supabase/routes" package. [index.ts](mdc:packages/next/src/routes/index.ts)
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
import * as z from 'zod';
|
import { z } from 'zod';
|
||||||
import { enhanceRouteHandler } from '@kit/next/routes';
|
import { enhanceRouteHandler } from '@kit/next/routes';
|
||||||
import { NextResponse } from 'next/server';
|
import { NextResponse } from 'next/server';
|
||||||
|
|
||||||
|
|||||||
1
.npmrc
1
.npmrc
@@ -3,6 +3,7 @@ dedupe-peer-dependents=true
|
|||||||
use-lockfile-v6=true
|
use-lockfile-v6=true
|
||||||
resolution-mode=highest
|
resolution-mode=highest
|
||||||
package-manager-strict=false
|
package-manager-strict=false
|
||||||
|
public-hoist-pattern[]=*i18next*
|
||||||
public-hoist-pattern[]=*eslint*
|
public-hoist-pattern[]=*eslint*
|
||||||
public-hoist-pattern[]=*prettier*
|
public-hoist-pattern[]=*prettier*
|
||||||
public-hoist-pattern[]=*require-in-the-middle*
|
public-hoist-pattern[]=*require-in-the-middle*
|
||||||
|
|||||||
14
AGENTS.md
14
AGENTS.md
@@ -39,13 +39,13 @@ pnpm format:fix # Format code
|
|||||||
|
|
||||||
## Key Patterns (Quick Reference)
|
## Key Patterns (Quick Reference)
|
||||||
|
|
||||||
| Pattern | Import | Details |
|
| Pattern | Import | Details |
|
||||||
|----------------|--------------------------------------------------------------|-------------------------------|
|
|---------|--------|---------|
|
||||||
| Server Actions | `authActionClient` from `@kit/next/safe-action` | `packages/next/AGENTS.md` |
|
| Server Actions | `enhanceAction` from `@kit/next/actions` | `packages/next/AGENTS.md` |
|
||||||
| Route Handlers | `enhanceRouteHandler` from `@kit/next/routes` | `packages/next/AGENTS.md` |
|
| Route Handlers | `enhanceRouteHandler` from `@kit/next/routes` | `packages/next/AGENTS.md` |
|
||||||
| Server Client | `getSupabaseServerClient` from `@kit/supabase/server-client` | `packages/supabase/AGENTS.md` |
|
| Server Client | `getSupabaseServerClient` from `@kit/supabase/server-client` | `packages/supabase/AGENTS.md` |
|
||||||
| UI Components | `@kit/ui/{component}` | `packages/ui/AGENTS.md` |
|
| UI Components | `@kit/ui/{component}` | `packages/ui/AGENTS.md` |
|
||||||
| Translations | `Trans` from `@kit/ui/trans` | `packages/ui/AGENTS.md` |
|
| Translations | `Trans` from `@kit/ui/trans` | `packages/ui/AGENTS.md` |
|
||||||
|
|
||||||
## Authorization
|
## Authorization
|
||||||
|
|
||||||
|
|||||||
@@ -120,7 +120,9 @@ export function AlertDialogStory() {
|
|||||||
|
|
||||||
const generateCode = () => {
|
const generateCode = () => {
|
||||||
let code = `<AlertDialog>\n`;
|
let code = `<AlertDialog>\n`;
|
||||||
code += ` <AlertDialogTrigger render={<Button variant="${controls.triggerVariant}">${controls.triggerText}</Button>} />\n`;
|
code += ` <AlertDialogTrigger asChild>\n`;
|
||||||
|
code += ` <Button variant="${controls.triggerVariant}">${controls.triggerText}</Button>\n`;
|
||||||
|
code += ` </AlertDialogTrigger>\n`;
|
||||||
code += ` <AlertDialogContent>\n`;
|
code += ` <AlertDialogContent>\n`;
|
||||||
code += ` <AlertDialogHeader>\n`;
|
code += ` <AlertDialogHeader>\n`;
|
||||||
|
|
||||||
@@ -177,14 +179,11 @@ export function AlertDialogStory() {
|
|||||||
const renderPreview = () => {
|
const renderPreview = () => {
|
||||||
return (
|
return (
|
||||||
<AlertDialog>
|
<AlertDialog>
|
||||||
<AlertDialogTrigger
|
<AlertDialogTrigger asChild>
|
||||||
render={
|
<Button variant={controls.triggerVariant}>
|
||||||
<Button variant={controls.triggerVariant}>
|
{controls.triggerText}
|
||||||
{controls.triggerText}
|
</Button>
|
||||||
</Button>
|
</AlertDialogTrigger>
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
{controls.withIcon ? (
|
{controls.withIcon ? (
|
||||||
@@ -342,11 +341,11 @@ export function AlertDialogStory() {
|
|||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
<div className="flex flex-wrap gap-3">
|
<div className="flex flex-wrap gap-3">
|
||||||
<AlertDialog>
|
<AlertDialog>
|
||||||
<AlertDialogTrigger
|
<AlertDialogTrigger asChild>
|
||||||
render={<Button variant="destructive" size="sm" />}
|
<Button variant="destructive" size="sm">
|
||||||
>
|
<Trash2 className="mr-2 h-4 w-4" />
|
||||||
<Trash2 className="mr-2 h-4 w-4" />
|
Delete Item
|
||||||
Delete Item
|
</Button>
|
||||||
</AlertDialogTrigger>
|
</AlertDialogTrigger>
|
||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
@@ -371,9 +370,11 @@ export function AlertDialogStory() {
|
|||||||
</AlertDialog>
|
</AlertDialog>
|
||||||
|
|
||||||
<AlertDialog>
|
<AlertDialog>
|
||||||
<AlertDialogTrigger render={<Button variant="outline" />}>
|
<AlertDialogTrigger asChild>
|
||||||
<LogOut className="mr-2 h-4 w-4" />
|
<Button variant="outline">
|
||||||
Sign Out
|
<LogOut className="mr-2 h-4 w-4" />
|
||||||
|
Sign Out
|
||||||
|
</Button>
|
||||||
</AlertDialogTrigger>
|
</AlertDialogTrigger>
|
||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
@@ -396,9 +397,11 @@ export function AlertDialogStory() {
|
|||||||
</AlertDialog>
|
</AlertDialog>
|
||||||
|
|
||||||
<AlertDialog>
|
<AlertDialog>
|
||||||
<AlertDialogTrigger render={<Button variant="outline" />}>
|
<AlertDialogTrigger asChild>
|
||||||
<UserX className="mr-2 h-4 w-4" />
|
<Button variant="outline">
|
||||||
Remove User
|
<UserX className="mr-2 h-4 w-4" />
|
||||||
|
Remove User
|
||||||
|
</Button>
|
||||||
</AlertDialogTrigger>
|
</AlertDialogTrigger>
|
||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
@@ -435,9 +438,11 @@ export function AlertDialogStory() {
|
|||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
<div className="flex flex-wrap gap-3">
|
<div className="flex flex-wrap gap-3">
|
||||||
<AlertDialog>
|
<AlertDialog>
|
||||||
<AlertDialogTrigger render={<Button variant="outline" />}>
|
<AlertDialogTrigger asChild>
|
||||||
<Archive className="mr-2 h-4 w-4" />
|
<Button variant="outline">
|
||||||
Archive Project
|
<Archive className="mr-2 h-4 w-4" />
|
||||||
|
Archive Project
|
||||||
|
</Button>
|
||||||
</AlertDialogTrigger>
|
</AlertDialogTrigger>
|
||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
@@ -460,9 +465,11 @@ export function AlertDialogStory() {
|
|||||||
</AlertDialog>
|
</AlertDialog>
|
||||||
|
|
||||||
<AlertDialog>
|
<AlertDialog>
|
||||||
<AlertDialogTrigger render={<Button />}>
|
<AlertDialogTrigger asChild>
|
||||||
<Download className="mr-2 h-4 w-4" />
|
<Button>
|
||||||
Export Data
|
<Download className="mr-2 h-4 w-4" />
|
||||||
|
Export Data
|
||||||
|
</Button>
|
||||||
</AlertDialogTrigger>
|
</AlertDialogTrigger>
|
||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
@@ -486,9 +493,11 @@ export function AlertDialogStory() {
|
|||||||
</AlertDialog>
|
</AlertDialog>
|
||||||
|
|
||||||
<AlertDialog>
|
<AlertDialog>
|
||||||
<AlertDialogTrigger render={<Button variant="outline" />}>
|
<AlertDialogTrigger asChild>
|
||||||
<RefreshCw className="mr-2 h-4 w-4" />
|
<Button variant="outline">
|
||||||
Reset Settings
|
<RefreshCw className="mr-2 h-4 w-4" />
|
||||||
|
Reset Settings
|
||||||
|
</Button>
|
||||||
</AlertDialogTrigger>
|
</AlertDialogTrigger>
|
||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
@@ -526,11 +535,11 @@ export function AlertDialogStory() {
|
|||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<h4 className="text-sm font-semibold">Error/Destructive</h4>
|
<h4 className="text-sm font-semibold">Error/Destructive</h4>
|
||||||
<AlertDialog>
|
<AlertDialog>
|
||||||
<AlertDialogTrigger
|
<AlertDialogTrigger asChild>
|
||||||
render={<Button variant="destructive" size="sm" />}
|
<Button variant="destructive" size="sm">
|
||||||
>
|
<Trash2 className="mr-2 h-4 w-4" />
|
||||||
<Trash2 className="mr-2 h-4 w-4" />
|
Delete Forever
|
||||||
Delete Forever
|
</Button>
|
||||||
</AlertDialogTrigger>
|
</AlertDialogTrigger>
|
||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
@@ -558,11 +567,11 @@ export function AlertDialogStory() {
|
|||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<h4 className="text-sm font-semibold">Warning</h4>
|
<h4 className="text-sm font-semibold">Warning</h4>
|
||||||
<AlertDialog>
|
<AlertDialog>
|
||||||
<AlertDialogTrigger
|
<AlertDialogTrigger asChild>
|
||||||
render={<Button variant="outline" size="sm" />}
|
<Button variant="outline" size="sm">
|
||||||
>
|
<AlertTriangle className="mr-2 h-4 w-4" />
|
||||||
<AlertTriangle className="mr-2 h-4 w-4" />
|
Unsaved Changes
|
||||||
Unsaved Changes
|
</Button>
|
||||||
</AlertDialogTrigger>
|
</AlertDialogTrigger>
|
||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
@@ -588,11 +597,11 @@ export function AlertDialogStory() {
|
|||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<h4 className="text-sm font-semibold">Info</h4>
|
<h4 className="text-sm font-semibold">Info</h4>
|
||||||
<AlertDialog>
|
<AlertDialog>
|
||||||
<AlertDialogTrigger
|
<AlertDialogTrigger asChild>
|
||||||
render={<Button variant="outline" size="sm" />}
|
<Button variant="outline" size="sm">
|
||||||
>
|
<Share className="mr-2 h-4 w-4" />
|
||||||
<Share className="mr-2 h-4 w-4" />
|
Share Publicly
|
||||||
Share Publicly
|
</Button>
|
||||||
</AlertDialogTrigger>
|
</AlertDialogTrigger>
|
||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
@@ -618,9 +627,11 @@ export function AlertDialogStory() {
|
|||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<h4 className="text-sm font-semibold">Success</h4>
|
<h4 className="text-sm font-semibold">Success</h4>
|
||||||
<AlertDialog>
|
<AlertDialog>
|
||||||
<AlertDialogTrigger render={<Button size="sm" />}>
|
<AlertDialogTrigger asChild>
|
||||||
<Download className="mr-2 h-4 w-4" />
|
<Button size="sm">
|
||||||
Complete Setup
|
<Download className="mr-2 h-4 w-4" />
|
||||||
|
Complete Setup
|
||||||
|
</Button>
|
||||||
</AlertDialogTrigger>
|
</AlertDialogTrigger>
|
||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ interface ButtonControls {
|
|||||||
loading: boolean;
|
loading: boolean;
|
||||||
withIcon: boolean;
|
withIcon: boolean;
|
||||||
fullWidth: boolean;
|
fullWidth: boolean;
|
||||||
|
asChild: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const variantOptions = [
|
const variantOptions = [
|
||||||
@@ -67,6 +68,7 @@ export function ButtonStory() {
|
|||||||
loading: false,
|
loading: false,
|
||||||
withIcon: false,
|
withIcon: false,
|
||||||
fullWidth: false,
|
fullWidth: false,
|
||||||
|
asChild: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const generateCode = () => {
|
const generateCode = () => {
|
||||||
@@ -75,12 +77,14 @@ export function ButtonStory() {
|
|||||||
variant: controls.variant,
|
variant: controls.variant,
|
||||||
size: controls.size,
|
size: controls.size,
|
||||||
disabled: controls.disabled,
|
disabled: controls.disabled,
|
||||||
|
asChild: controls.asChild,
|
||||||
className: controls.fullWidth ? 'w-full' : '',
|
className: controls.fullWidth ? 'w-full' : '',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
variant: 'default',
|
variant: 'default',
|
||||||
size: 'default',
|
size: 'default',
|
||||||
disabled: false,
|
disabled: false,
|
||||||
|
asChild: false,
|
||||||
className: '',
|
className: '',
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -190,6 +194,15 @@ export function ButtonStory() {
|
|||||||
onCheckedChange={(checked) => updateControl('fullWidth', checked)}
|
onCheckedChange={(checked) => updateControl('fullWidth', checked)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Label htmlFor="asChild">As Child</Label>
|
||||||
|
<Switch
|
||||||
|
id="asChild"
|
||||||
|
checked={controls.asChild}
|
||||||
|
onCheckedChange={(checked) => updateControl('asChild', checked)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -276,11 +276,11 @@ export default function CalendarStory() {
|
|||||||
<Card>
|
<Card>
|
||||||
<CardContent className="flex justify-center pt-6">
|
<CardContent className="flex justify-center pt-6">
|
||||||
<Popover>
|
<Popover>
|
||||||
<PopoverTrigger
|
<PopoverTrigger asChild>
|
||||||
render={<Button variant="outline" className="justify-start" />}
|
<Button variant="outline" className="justify-start">
|
||||||
>
|
<CalendarIcon className="mr-2 h-4 w-4" />
|
||||||
<CalendarIcon className="mr-2 h-4 w-4" />
|
Pick a date
|
||||||
Pick a date
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent className="w-auto p-0" align="start">
|
<PopoverContent className="w-auto p-0" align="start">
|
||||||
<Calendar
|
<Calendar
|
||||||
|
|||||||
@@ -320,12 +320,10 @@ export function CardButtonStory() {
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr className="border-b">
|
<tr className="border-b">
|
||||||
<td className="p-3 font-mono text-sm">render</td>
|
<td className="p-3 font-mono text-sm">asChild</td>
|
||||||
<td className="p-3 font-mono text-sm">
|
<td className="p-3 font-mono text-sm">boolean</td>
|
||||||
React.ReactElement
|
<td className="p-3 font-mono text-sm">false</td>
|
||||||
</td>
|
<td className="p-3">Render as child element</td>
|
||||||
<td className="p-3 font-mono text-sm">-</td>
|
|
||||||
<td className="p-3">Compose with a custom element</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
<tr className="border-b">
|
<tr className="border-b">
|
||||||
<td className="p-3 font-mono text-sm">className</td>
|
<td className="p-3 font-mono text-sm">className</td>
|
||||||
|
|||||||
@@ -139,8 +139,8 @@ export function DialogStory() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
let code = `<Dialog>\n`;
|
let code = `<Dialog>\n`;
|
||||||
code += ` <DialogTrigger render={<Button variant="${controls.triggerVariant}" />}>\n`;
|
code += ` <DialogTrigger asChild>\n`;
|
||||||
code += ` ${controls.triggerText}\n`;
|
code += ` <Button variant="${controls.triggerVariant}">${controls.triggerText}</Button>\n`;
|
||||||
code += ` </DialogTrigger>\n`;
|
code += ` </DialogTrigger>\n`;
|
||||||
code += ` <DialogContent${contentPropsString}>\n`;
|
code += ` <DialogContent${contentPropsString}>\n`;
|
||||||
code += ` <DialogHeader>\n`;
|
code += ` <DialogHeader>\n`;
|
||||||
@@ -182,8 +182,8 @@ export function DialogStory() {
|
|||||||
|
|
||||||
if (controls.withFooter) {
|
if (controls.withFooter) {
|
||||||
code += ` <DialogFooter>\n`;
|
code += ` <DialogFooter>\n`;
|
||||||
code += ` <DialogClose render={<Button variant="outline" />}>\n`;
|
code += ` <DialogClose asChild>\n`;
|
||||||
code += ` Cancel\n`;
|
code += ` <Button variant="outline">Cancel</Button>\n`;
|
||||||
code += ` </DialogClose>\n`;
|
code += ` </DialogClose>\n`;
|
||||||
code += ` <Button>Save Changes</Button>\n`;
|
code += ` <Button>Save Changes</Button>\n`;
|
||||||
code += ` </DialogFooter>\n`;
|
code += ` </DialogFooter>\n`;
|
||||||
@@ -198,8 +198,10 @@ export function DialogStory() {
|
|||||||
const renderPreview = () => {
|
const renderPreview = () => {
|
||||||
return (
|
return (
|
||||||
<Dialog>
|
<Dialog>
|
||||||
<DialogTrigger render={<Button variant={controls.triggerVariant} />}>
|
<DialogTrigger asChild>
|
||||||
{controls.triggerText}
|
<Button variant={controls.triggerVariant}>
|
||||||
|
{controls.triggerText}
|
||||||
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent
|
<DialogContent
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -269,8 +271,8 @@ export function DialogStory() {
|
|||||||
|
|
||||||
{controls.withFooter && (
|
{controls.withFooter && (
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<DialogClose render={<Button variant="outline" />}>
|
<DialogClose asChild>
|
||||||
Cancel
|
<Button variant="outline">Cancel</Button>
|
||||||
</DialogClose>
|
</DialogClose>
|
||||||
<Button>Save Changes</Button>
|
<Button>Save Changes</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
@@ -389,9 +391,11 @@ export function DialogStory() {
|
|||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
<div className="flex flex-wrap gap-3">
|
<div className="flex flex-wrap gap-3">
|
||||||
<Dialog>
|
<Dialog>
|
||||||
<DialogTrigger render={<Button variant="outline" />}>
|
<DialogTrigger asChild>
|
||||||
<Info className="mr-2 h-4 w-4" />
|
<Button variant="outline">
|
||||||
Info Dialog
|
<Info className="mr-2 h-4 w-4" />
|
||||||
|
Info Dialog
|
||||||
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
@@ -408,15 +412,19 @@ export function DialogStory() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<DialogClose render={<Button />}>Got it</DialogClose>
|
<DialogClose asChild>
|
||||||
|
<Button>Got it</Button>
|
||||||
|
</DialogClose>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
<Dialog>
|
<Dialog>
|
||||||
<DialogTrigger render={<Button />}>
|
<DialogTrigger asChild>
|
||||||
<Edit className="mr-2 h-4 w-4" />
|
<Button>
|
||||||
Edit Profile
|
<Edit className="mr-2 h-4 w-4" />
|
||||||
|
Edit Profile
|
||||||
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
@@ -448,8 +456,8 @@ export function DialogStory() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<DialogClose render={<Button variant="outline" />}>
|
<DialogClose asChild>
|
||||||
Cancel
|
<Button variant="outline">Cancel</Button>
|
||||||
</DialogClose>
|
</DialogClose>
|
||||||
<Button>Save Changes</Button>
|
<Button>Save Changes</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
@@ -457,9 +465,11 @@ export function DialogStory() {
|
|||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
<Dialog>
|
<Dialog>
|
||||||
<DialogTrigger render={<Button variant="secondary" />}>
|
<DialogTrigger asChild>
|
||||||
<Settings className="mr-2 h-4 w-4" />
|
<Button variant="secondary">
|
||||||
Settings
|
<Settings className="mr-2 h-4 w-4" />
|
||||||
|
Settings
|
||||||
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
@@ -489,8 +499,8 @@ export function DialogStory() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<DialogClose render={<Button variant="outline" />}>
|
<DialogClose asChild>
|
||||||
Cancel
|
<Button variant="outline">Cancel</Button>
|
||||||
</DialogClose>
|
</DialogClose>
|
||||||
<Button>Save</Button>
|
<Button>Save</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
@@ -508,8 +518,10 @@ export function DialogStory() {
|
|||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
<div className="flex flex-wrap gap-3">
|
<div className="flex flex-wrap gap-3">
|
||||||
<Dialog>
|
<Dialog>
|
||||||
<DialogTrigger render={<Button variant="outline" size="sm" />}>
|
<DialogTrigger asChild>
|
||||||
Small Dialog
|
<Button variant="outline" size="sm">
|
||||||
|
Small Dialog
|
||||||
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-w-md">
|
<DialogContent className="max-w-md">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
@@ -524,14 +536,16 @@ export function DialogStory() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<DialogClose render={<Button />}>Close</DialogClose>
|
<DialogClose asChild>
|
||||||
|
<Button>Close</Button>
|
||||||
|
</DialogClose>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
<Dialog>
|
<Dialog>
|
||||||
<DialogTrigger render={<Button variant="outline" />}>
|
<DialogTrigger asChild>
|
||||||
Large Dialog
|
<Button variant="outline">Large Dialog</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-w-2xl">
|
<DialogContent className="max-w-2xl">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
@@ -557,8 +571,8 @@ export function DialogStory() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<DialogClose render={<Button variant="outline" />}>
|
<DialogClose asChild>
|
||||||
Cancel
|
<Button variant="outline">Cancel</Button>
|
||||||
</DialogClose>
|
</DialogClose>
|
||||||
<Button>Save</Button>
|
<Button>Save</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
@@ -576,9 +590,11 @@ export function DialogStory() {
|
|||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
<div className="flex flex-wrap gap-3">
|
<div className="flex flex-wrap gap-3">
|
||||||
<Dialog>
|
<Dialog>
|
||||||
<DialogTrigger render={<Button variant="outline" />}>
|
<DialogTrigger asChild>
|
||||||
<Image className="mr-2 h-4 w-4" />
|
<Button variant="outline">
|
||||||
Image Gallery
|
<Image className="mr-2 h-4 w-4" />
|
||||||
|
Image Gallery
|
||||||
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-w-2xl">
|
<DialogContent className="max-w-2xl">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
@@ -611,9 +627,11 @@ export function DialogStory() {
|
|||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
<Dialog>
|
<Dialog>
|
||||||
<DialogTrigger render={<Button variant="outline" />}>
|
<DialogTrigger asChild>
|
||||||
<MessageSquare className="mr-2 h-4 w-4" />
|
<Button variant="outline">
|
||||||
Feedback
|
<MessageSquare className="mr-2 h-4 w-4" />
|
||||||
|
Feedback
|
||||||
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
@@ -650,8 +668,8 @@ export function DialogStory() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<DialogClose render={<Button variant="outline" />}>
|
<DialogClose asChild>
|
||||||
Cancel
|
<Button variant="outline">Cancel</Button>
|
||||||
</DialogClose>
|
</DialogClose>
|
||||||
<Button>
|
<Button>
|
||||||
<MessageSquare className="mr-2 h-4 w-4" />
|
<MessageSquare className="mr-2 h-4 w-4" />
|
||||||
@@ -718,8 +736,8 @@ export function DialogStory() {
|
|||||||
<div>
|
<div>
|
||||||
<h4 className="mb-3 text-lg font-semibold">DialogTrigger</h4>
|
<h4 className="mb-3 text-lg font-semibold">DialogTrigger</h4>
|
||||||
<p className="text-muted-foreground mb-3 text-sm">
|
<p className="text-muted-foreground mb-3 text-sm">
|
||||||
The element that opens the dialog. Use the render prop to compose
|
The element that opens the dialog. Use asChild prop to render as
|
||||||
with a custom element.
|
child element.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
import { useRouter, useSearchParams } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
|
|
||||||
import { Code2, FileText, Search } from 'lucide-react';
|
import { Code2, FileText, Search } from 'lucide-react';
|
||||||
|
|
||||||
@@ -35,7 +35,6 @@ export function DocsSidebar({
|
|||||||
selectedCategory,
|
selectedCategory,
|
||||||
}: DocsSidebarProps) {
|
}: DocsSidebarProps) {
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
const searchParams = useSearchParams();
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const filteredComponents = COMPONENTS_REGISTRY.filter((c) =>
|
const filteredComponents = COMPONENTS_REGISTRY.filter((c) =>
|
||||||
@@ -51,21 +50,21 @@ export function DocsSidebar({
|
|||||||
.sort((a, b) => a.name.localeCompare(b.name));
|
.sort((a, b) => a.name.localeCompare(b.name));
|
||||||
|
|
||||||
const onCategorySelect = (category: string | null) => {
|
const onCategorySelect = (category: string | null) => {
|
||||||
const sp = new URLSearchParams(searchParams);
|
const searchParams = new URLSearchParams(window.location.search);
|
||||||
sp.set('category', category || '');
|
searchParams.set('category', category || '');
|
||||||
router.push(`/components?${sp.toString()}`);
|
router.push(`/components?${searchParams.toString()}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onComponentSelect = (component: ComponentInfo) => {
|
const onComponentSelect = (component: ComponentInfo) => {
|
||||||
const sp = new URLSearchParams(searchParams);
|
const searchParams = new URLSearchParams(window.location.search);
|
||||||
sp.set('component', component.name);
|
searchParams.set('component', component.name);
|
||||||
router.push(`/components?${sp.toString()}`);
|
router.push(`/components?${searchParams.toString()}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-muted/30 flex h-screen w-80 flex-col overflow-hidden border-r">
|
<div className="bg-muted/30 flex h-screen w-80 flex-col overflow-hidden border-r">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="shrink-0 border-b p-4">
|
<div className="flex-shrink-0 border-b p-4">
|
||||||
<div className="mb-2 flex items-center gap-2">
|
<div className="mb-2 flex items-center gap-2">
|
||||||
<Code2 className="text-primary h-6 w-6" />
|
<Code2 className="text-primary h-6 w-6" />
|
||||||
|
|
||||||
@@ -78,14 +77,13 @@ export function DocsSidebar({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Controls */}
|
{/* Controls */}
|
||||||
<div className="shrink-0 space-y-2 border-b p-4">
|
<div className="flex-shrink-0 space-y-2 border-b p-4">
|
||||||
{/* Category Select */}
|
{/* Category Select */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Select
|
<Select
|
||||||
defaultValue={selectedCategory || 'all'}
|
value={selectedCategory || 'all'}
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
const category = value === 'all' ? null : value;
|
const category = value === 'all' ? null : value;
|
||||||
|
|
||||||
onCategorySelect(category);
|
onCategorySelect(category);
|
||||||
|
|
||||||
// Select first component in the filtered results
|
// Select first component in the filtered results
|
||||||
@@ -98,12 +96,8 @@ export function DocsSidebar({
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="w-full">
|
<SelectTrigger>
|
||||||
<SelectValue>
|
<SelectValue placeholder={'Select a category'} />
|
||||||
{(category) => {
|
|
||||||
return category === 'all' ? 'All Categories' : category;
|
|
||||||
}}
|
|
||||||
</SelectValue>
|
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
|
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
@@ -160,7 +154,7 @@ export function DocsSidebar({
|
|||||||
|
|
||||||
{/* Components List - Scrollable */}
|
{/* Components List - Scrollable */}
|
||||||
<div className="flex flex-1 flex-col overflow-y-auto">
|
<div className="flex flex-1 flex-col overflow-y-auto">
|
||||||
<div className="shrink-0 p-4 pb-2">
|
<div className="flex-shrink-0 p-4 pb-2">
|
||||||
<h3 className="flex items-center gap-2 text-sm font-semibold">
|
<h3 className="flex items-center gap-2 text-sm font-semibold">
|
||||||
<FileText className="h-4 w-4" />
|
<FileText className="h-4 w-4" />
|
||||||
Components
|
Components
|
||||||
|
|||||||
@@ -101,18 +101,13 @@ const examples = [
|
|||||||
return (
|
return (
|
||||||
<div className="flex min-h-32 items-center justify-center">
|
<div className="flex min-h-32 items-center justify-center">
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger
|
<DropdownMenuTrigger asChild>
|
||||||
render={
|
<Button variant="ghost" className="relative h-8 w-8 rounded-full">
|
||||||
<Button
|
<Avatar className="h-8 w-8">
|
||||||
variant="ghost"
|
<AvatarImage src="/avatars/01.png" alt="@username" />
|
||||||
className="relative h-8 w-8 rounded-full"
|
<AvatarFallback>JD</AvatarFallback>
|
||||||
/>
|
</Avatar>
|
||||||
}
|
</Button>
|
||||||
>
|
|
||||||
<Avatar className="h-8 w-8">
|
|
||||||
<AvatarImage src="/avatars/01.png" alt="@username" />
|
|
||||||
<AvatarFallback>JD</AvatarFallback>
|
|
||||||
</Avatar>
|
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent className="w-56" align="end" forceMount>
|
<DropdownMenuContent className="w-56" align="end" forceMount>
|
||||||
<DropdownMenuLabel className="font-normal">
|
<DropdownMenuLabel className="font-normal">
|
||||||
@@ -190,11 +185,11 @@ const examples = [
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger
|
<DropdownMenuTrigger asChild>
|
||||||
render={<Button variant="ghost" className="h-8 w-8 p-0" />}
|
<Button variant="ghost" className="h-8 w-8 p-0">
|
||||||
>
|
<span className="sr-only">Open menu</span>
|
||||||
<span className="sr-only">Open menu</span>
|
<MoreHorizontal className="h-4 w-4" />
|
||||||
<MoreHorizontal className="h-4 w-4" />
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end" className="w-48">
|
<DropdownMenuContent align="end" className="w-48">
|
||||||
<DropdownMenuItem onClick={() => setSelectedAction('open')}>
|
<DropdownMenuItem onClick={() => setSelectedAction('open')}>
|
||||||
@@ -280,9 +275,11 @@ const examples = [
|
|||||||
return (
|
return (
|
||||||
<div className="flex min-h-48 items-center justify-center">
|
<div className="flex min-h-48 items-center justify-center">
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger render={<Button variant="outline" />}>
|
<DropdownMenuTrigger asChild>
|
||||||
<Plus className="mr-2 h-4 w-4" />
|
<Button variant="outline">
|
||||||
Create New
|
<Plus className="mr-2 h-4 w-4" />
|
||||||
|
Create New
|
||||||
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent className="w-56">
|
<DropdownMenuContent className="w-56">
|
||||||
<DropdownMenuLabel>Create Content</DropdownMenuLabel>
|
<DropdownMenuLabel>Create Content</DropdownMenuLabel>
|
||||||
@@ -396,11 +393,11 @@ const examples = [
|
|||||||
<span className="text-sm">Appearance & Layout</span>
|
<span className="text-sm">Appearance & Layout</span>
|
||||||
|
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger
|
<DropdownMenuTrigger asChild>
|
||||||
render={<Button variant="outline" size="sm" />}
|
<Button variant="outline" size="sm">
|
||||||
>
|
<Settings className="mr-2 h-4 w-4" />
|
||||||
<Settings className="mr-2 h-4 w-4" />
|
Configure
|
||||||
Configure
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent className="w-64" align="end">
|
<DropdownMenuContent className="w-64" align="end">
|
||||||
<DropdownMenuLabel>View Options</DropdownMenuLabel>
|
<DropdownMenuLabel>View Options</DropdownMenuLabel>
|
||||||
@@ -550,10 +547,10 @@ const examples = [
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger
|
<DropdownMenuTrigger asChild>
|
||||||
render={<Button variant="ghost" className="h-8 w-8 p-0" />}
|
<Button variant="ghost" className="h-8 w-8 p-0">
|
||||||
>
|
<MoreHorizontal className="h-4 w-4" />
|
||||||
<MoreHorizontal className="h-4 w-4" />
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end" className="w-48">
|
<DropdownMenuContent align="end" className="w-48">
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
@@ -866,7 +863,7 @@ export default function DropdownMenuStory() {
|
|||||||
modal: controls.modal ? true : undefined,
|
modal: controls.modal ? true : undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
const dropdownStructure = `<DropdownMenu${rootProps}>\n <DropdownMenuTrigger render={<Button variant="outline" />}>\n Open Menu\n </DropdownMenuTrigger>\n <DropdownMenuContent${contentProps}>\n <DropdownMenuItem>\n <User className="mr-2 h-4 w-4" />\n <span>Profile</span>\n </DropdownMenuItem>\n <DropdownMenuItem>\n <Settings className="mr-2 h-4 w-4" />\n <span>Settings</span>\n </DropdownMenuItem>\n <DropdownMenuSeparator />\n <DropdownMenuItem>\n <LogOut className="mr-2 h-4 w-4" />\n <span>Log out</span>\n </DropdownMenuItem>\n </DropdownMenuContent>\n</DropdownMenu>`;
|
const dropdownStructure = `<DropdownMenu${rootProps}>\n <DropdownMenuTrigger asChild>\n <Button variant="outline">Open Menu</Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent${contentProps}>\n <DropdownMenuItem>\n <User className="mr-2 h-4 w-4" />\n <span>Profile</span>\n </DropdownMenuItem>\n <DropdownMenuItem>\n <Settings className="mr-2 h-4 w-4" />\n <span>Settings</span>\n </DropdownMenuItem>\n <DropdownMenuSeparator />\n <DropdownMenuItem>\n <LogOut className="mr-2 h-4 w-4" />\n <span>Log out</span>\n </DropdownMenuItem>\n </DropdownMenuContent>\n</DropdownMenu>`;
|
||||||
|
|
||||||
return `${importStatement}\n${buttonImport}\n${iconImport}\n\n${dropdownStructure}`;
|
return `${importStatement}\n${buttonImport}\n${iconImport}\n\n${dropdownStructure}`;
|
||||||
};
|
};
|
||||||
@@ -974,8 +971,8 @@ export default function DropdownMenuStory() {
|
|||||||
const previewContent = (
|
const previewContent = (
|
||||||
<div className="flex justify-center p-6">
|
<div className="flex justify-center p-6">
|
||||||
<DropdownMenu modal={controls.modal}>
|
<DropdownMenu modal={controls.modal}>
|
||||||
<DropdownMenuTrigger render={<Button variant="outline" />}>
|
<DropdownMenuTrigger asChild>
|
||||||
Open Menu
|
<Button variant="outline">Open Menu</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent
|
<DropdownMenuContent
|
||||||
side={controls.side}
|
side={controls.side}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { useState } from 'react';
|
|||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import * as z from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { Badge } from '@kit/ui/badge';
|
import { Badge } from '@kit/ui/badge';
|
||||||
import { Button } from '@kit/ui/button';
|
import { Button } from '@kit/ui/button';
|
||||||
@@ -119,7 +119,7 @@ export default function FormStory() {
|
|||||||
const formImport = generateImportStatement(formComponents, '@kit/ui/form');
|
const formImport = generateImportStatement(formComponents, '@kit/ui/form');
|
||||||
const inputImport = generateImportStatement(['Input'], '@kit/ui/input');
|
const inputImport = generateImportStatement(['Input'], '@kit/ui/input');
|
||||||
const buttonImport = generateImportStatement(['Button'], '@kit/ui/button');
|
const buttonImport = generateImportStatement(['Button'], '@kit/ui/button');
|
||||||
const hookFormImports = `import { useForm } from 'react-hook-form';\nimport { zodResolver } from '@hookform/resolvers/zod';\nimport * as z from 'zod';`;
|
const hookFormImports = `import { useForm } from 'react-hook-form';\nimport { zodResolver } from '@hookform/resolvers/zod';\nimport { z } from 'zod';`;
|
||||||
|
|
||||||
let schemaCode = '';
|
let schemaCode = '';
|
||||||
let formFieldsCode = '';
|
let formFieldsCode = '';
|
||||||
@@ -130,19 +130,19 @@ export default function FormStory() {
|
|||||||
|
|
||||||
formFieldsCode = ` <FormField\n control={form.control}\n name="username"\n render={({ field }) => (\n <FormItem>\n <FormLabel>Username</FormLabel>\n <FormControl>\n <Input ${controls.disabled ? 'disabled ' : ''}placeholder="Enter username" {...field} />\n </FormControl>${controls.showDescriptions ? '\n <FormDescription>\n Your public display name.\n </FormDescription>' : ''}${controls.showValidation ? '\n <FormMessage />' : ''}\n </FormItem>\n )}\n />\n <FormField\n control={form.control}\n name="email"\n render={({ field }) => (\n <FormItem>\n <FormLabel>Email</FormLabel>\n <FormControl>\n <Input ${controls.disabled ? 'disabled ' : ''}type="email" placeholder="Enter email" {...field} />\n </FormControl>${controls.showDescriptions ? "\n <FormDescription>\n We'll never share your email.\n </FormDescription>" : ''}${controls.showValidation ? '\n <FormMessage />' : ''}\n </FormItem>\n )}\n />`;
|
formFieldsCode = ` <FormField\n control={form.control}\n name="username"\n render={({ field }) => (\n <FormItem>\n <FormLabel>Username</FormLabel>\n <FormControl>\n <Input ${controls.disabled ? 'disabled ' : ''}placeholder="Enter username" {...field} />\n </FormControl>${controls.showDescriptions ? '\n <FormDescription>\n Your public display name.\n </FormDescription>' : ''}${controls.showValidation ? '\n <FormMessage />' : ''}\n </FormItem>\n )}\n />\n <FormField\n control={form.control}\n name="email"\n render={({ field }) => (\n <FormItem>\n <FormLabel>Email</FormLabel>\n <FormControl>\n <Input ${controls.disabled ? 'disabled ' : ''}type="email" placeholder="Enter email" {...field} />\n </FormControl>${controls.showDescriptions ? "\n <FormDescription>\n We'll never share your email.\n </FormDescription>" : ''}${controls.showValidation ? '\n <FormMessage />' : ''}\n </FormItem>\n )}\n />`;
|
||||||
|
|
||||||
onSubmitCode = ` function onSubmit(values: z.output<typeof formSchema>) {\n console.log('Form submitted:', values);\n }`;
|
onSubmitCode = ` function onSubmit(values: z.infer<typeof formSchema>) {\n console.log('Form submitted:', values);\n }`;
|
||||||
} else if (controls.formType === 'advanced') {
|
} else if (controls.formType === 'advanced') {
|
||||||
schemaCode = `const formSchema = z.object({\n firstName: z.string().min(1, 'First name is required.'),\n lastName: z.string().min(1, 'Last name is required.'),\n email: z.string().email('Please enter a valid email address.'),\n});`;
|
schemaCode = `const formSchema = z.object({\n firstName: z.string().min(1, 'First name is required.'),\n lastName: z.string().min(1, 'Last name is required.'),\n email: z.string().email('Please enter a valid email address.'),\n});`;
|
||||||
|
|
||||||
formFieldsCode = ` <FormField\n control={form.control}\n name="firstName"\n render={({ field }) => (\n <FormItem>\n <FormLabel>First Name</FormLabel>\n <FormControl>\n <Input ${controls.disabled ? 'disabled ' : ''}placeholder="John" {...field} />\n </FormControl>${controls.showValidation ? '\n <FormMessage />' : ''}\n </FormItem>\n )}\n />\n <FormField\n control={form.control}\n name="lastName"\n render={({ field }) => (\n <FormItem>\n <FormLabel>Last Name</FormLabel>\n <FormControl>\n <Input ${controls.disabled ? 'disabled ' : ''}placeholder="Doe" {...field} />\n </FormControl>${controls.showValidation ? '\n <FormMessage />' : ''}\n </FormItem>\n )}\n />`;
|
formFieldsCode = ` <FormField\n control={form.control}\n name="firstName"\n render={({ field }) => (\n <FormItem>\n <FormLabel>First Name</FormLabel>\n <FormControl>\n <Input ${controls.disabled ? 'disabled ' : ''}placeholder="John" {...field} />\n </FormControl>${controls.showValidation ? '\n <FormMessage />' : ''}\n </FormItem>\n )}\n />\n <FormField\n control={form.control}\n name="lastName"\n render={({ field }) => (\n <FormItem>\n <FormLabel>Last Name</FormLabel>\n <FormControl>\n <Input ${controls.disabled ? 'disabled ' : ''}placeholder="Doe" {...field} />\n </FormControl>${controls.showValidation ? '\n <FormMessage />' : ''}\n </FormItem>\n )}\n />`;
|
||||||
|
|
||||||
onSubmitCode = ` function onSubmit(values: z.output<typeof formSchema>) {\n console.log('Advanced form submitted:', values);\n }`;
|
onSubmitCode = ` function onSubmit(values: z.infer<typeof formSchema>) {\n console.log('Advanced form submitted:', values);\n }`;
|
||||||
} else {
|
} else {
|
||||||
schemaCode = `const formSchema = z.object({\n password: z.string().min(8, 'Password must be at least 8 characters.'),\n confirmPassword: z.string(),\n}).refine((data) => data.password === data.confirmPassword, {\n message: 'Passwords do not match.',\n path: ['confirmPassword'],\n});`;
|
schemaCode = `const formSchema = z.object({\n password: z.string().min(8, 'Password must be at least 8 characters.'),\n confirmPassword: z.string(),\n}).refine((data) => data.password === data.confirmPassword, {\n message: 'Passwords do not match.',\n path: ['confirmPassword'],\n});`;
|
||||||
|
|
||||||
formFieldsCode = ` <FormField\n control={form.control}\n name="password"\n render={({ field }) => (\n <FormItem>\n <FormLabel>Password</FormLabel>\n <FormControl>\n <Input ${controls.disabled ? 'disabled ' : ''}type="password" {...field} />\n </FormControl>${controls.showValidation ? '\n <FormMessage />' : ''}\n </FormItem>\n )}\n />\n <FormField\n control={form.control}\n name="confirmPassword"\n render={({ field }) => (\n <FormItem>\n <FormLabel>Confirm Password</FormLabel>\n <FormControl>\n <Input ${controls.disabled ? 'disabled ' : ''}type="password" {...field} />\n </FormControl>${controls.showValidation ? '\n <FormMessage />' : ''}\n </FormItem>\n )}\n />`;
|
formFieldsCode = ` <FormField\n control={form.control}\n name="password"\n render={({ field }) => (\n <FormItem>\n <FormLabel>Password</FormLabel>\n <FormControl>\n <Input ${controls.disabled ? 'disabled ' : ''}type="password" {...field} />\n </FormControl>${controls.showValidation ? '\n <FormMessage />' : ''}\n </FormItem>\n )}\n />\n <FormField\n control={form.control}\n name="confirmPassword"\n render={({ field }) => (\n <FormItem>\n <FormLabel>Confirm Password</FormLabel>\n <FormControl>\n <Input ${controls.disabled ? 'disabled ' : ''}type="password" {...field} />\n </FormControl>${controls.showValidation ? '\n <FormMessage />' : ''}\n </FormItem>\n )}\n />`;
|
||||||
|
|
||||||
onSubmitCode = ` function onSubmit(values: z.output<typeof formSchema>) {\n console.log('Validation form submitted:', values);\n }`;
|
onSubmitCode = ` function onSubmit(values: z.infer<typeof formSchema>) {\n console.log('Validation form submitted:', values);\n }`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultValuesCode =
|
const defaultValuesCode =
|
||||||
@@ -152,13 +152,13 @@ export default function FormStory() {
|
|||||||
? ` defaultValues: {\n firstName: '',\n lastName: '',\n email: '',\n },`
|
? ` defaultValues: {\n firstName: '',\n lastName: '',\n email: '',\n },`
|
||||||
: ` defaultValues: {\n password: '',\n confirmPassword: '',\n },`;
|
: ` defaultValues: {\n password: '',\n confirmPassword: '',\n },`;
|
||||||
|
|
||||||
const fullFormCode = `${hookFormImports}\n${formImport}\n${inputImport}\n${buttonImport}\n\n${schemaCode}\n\nfunction MyForm() {\n const form = useForm<z.output<typeof formSchema>>({\n resolver: zodResolver(formSchema),\n${defaultValuesCode}\n });\n\n${onSubmitCode}\n\n return (\n <Form {...form}>\n <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">\n${formFieldsCode}\n <Button type="submit"${controls.disabled ? ' disabled' : ''}>Submit</Button>\n </form>\n </Form>\n );\n}`;
|
const fullFormCode = `${hookFormImports}\n${formImport}\n${inputImport}\n${buttonImport}\n\n${schemaCode}\n\nfunction MyForm() {\n const form = useForm<z.infer<typeof formSchema>>({\n resolver: zodResolver(formSchema),\n${defaultValuesCode}\n });\n\n${onSubmitCode}\n\n return (\n <Form {...form}>\n <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">\n${formFieldsCode}\n <Button type="submit"${controls.disabled ? ' disabled' : ''}>Submit</Button>\n </form>\n </Form>\n );\n}`;
|
||||||
|
|
||||||
return fullFormCode;
|
return fullFormCode;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Basic form
|
// Basic form
|
||||||
const basicForm = useForm<z.output<typeof basicFormSchema>>({
|
const basicForm = useForm<z.infer<typeof basicFormSchema>>({
|
||||||
resolver: zodResolver(basicFormSchema),
|
resolver: zodResolver(basicFormSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
username: '',
|
username: '',
|
||||||
@@ -169,7 +169,7 @@ export default function FormStory() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Advanced form
|
// Advanced form
|
||||||
const advancedForm = useForm<z.output<typeof advancedFormSchema>>({
|
const advancedForm = useForm<z.infer<typeof advancedFormSchema>>({
|
||||||
resolver: zodResolver(advancedFormSchema),
|
resolver: zodResolver(advancedFormSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
firstName: '',
|
firstName: '',
|
||||||
@@ -183,7 +183,7 @@ export default function FormStory() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Validation form
|
// Validation form
|
||||||
const validationForm = useForm<z.output<typeof validationFormSchema>>({
|
const validationForm = useForm<z.infer<typeof validationFormSchema>>({
|
||||||
resolver: zodResolver(validationFormSchema),
|
resolver: zodResolver(validationFormSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
password: '',
|
password: '',
|
||||||
@@ -1056,7 +1056,7 @@ export default function FormStory() {
|
|||||||
<pre className="overflow-x-auto text-sm">
|
<pre className="overflow-x-auto text-sm">
|
||||||
{`import { useForm } from 'react-hook-form';
|
{`import { useForm } from 'react-hook-form';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import * as z from 'zod';
|
import { z } from 'zod';
|
||||||
import { Form, FormField, FormItem, FormLabel, FormControl, FormMessage } from '@kit/ui/form';
|
import { Form, FormField, FormItem, FormLabel, FormControl, FormMessage } from '@kit/ui/form';
|
||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
@@ -1065,7 +1065,7 @@ const formSchema = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
function MyForm() {
|
function MyForm() {
|
||||||
const form = useForm<z.output<typeof formSchema>>({
|
const form = useForm<z.infer<typeof formSchema>>({
|
||||||
resolver: zodResolver(formSchema),
|
resolver: zodResolver(formSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
username: '',
|
username: '',
|
||||||
@@ -1073,7 +1073,7 @@ function MyForm() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
function onSubmit(values: z.output<typeof formSchema>) {
|
function onSubmit(values: z.infer<typeof formSchema>) {
|
||||||
console.log(values);
|
console.log(values);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ export function KbdStory() {
|
|||||||
let snippet = groupLines.join('\n');
|
let snippet = groupLines.join('\n');
|
||||||
|
|
||||||
if (controls.showTooltip) {
|
if (controls.showTooltip) {
|
||||||
snippet = `<TooltipProvider>\n <Tooltip>\n <TooltipTrigger render={<Button variant="outline" />}>\n Command palette\n </TooltipTrigger>\n <TooltipContent className="flex items-center gap-2">\n <span>Press</span>\n ${groupLines.join('\n ')}\n </TooltipContent>\n </Tooltip>\n</TooltipProvider>`;
|
snippet = `<TooltipProvider>\n <Tooltip>\n <TooltipTrigger asChild>\n <Button variant="outline">Command palette</Button>\n </TooltipTrigger>\n <TooltipContent className="flex items-center gap-2">\n <span>Press</span>\n ${groupLines.join('\n ')}\n </TooltipContent>\n </Tooltip>\n</TooltipProvider>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return formatCodeBlock(snippet, [
|
return formatCodeBlock(snippet, [
|
||||||
@@ -115,11 +115,11 @@ export function KbdStory() {
|
|||||||
{controls.showTooltip ? (
|
{controls.showTooltip ? (
|
||||||
<TooltipProvider delayDuration={200}>
|
<TooltipProvider delayDuration={200}>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger
|
<TooltipTrigger asChild>
|
||||||
render={<Button variant="outline" className="gap-2" />}
|
<Button variant="outline" className="gap-2">
|
||||||
>
|
<Command className="h-4 w-4" />
|
||||||
<Command className="h-4 w-4" />
|
Command palette
|
||||||
Command palette
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent className="flex items-center gap-2">
|
<TooltipContent className="flex items-center gap-2">
|
||||||
<span>Press</span>
|
<span>Press</span>
|
||||||
|
|||||||
@@ -136,13 +136,11 @@ export function SimpleDataTableStory() {
|
|||||||
{controls.showActions && (
|
{controls.showActions && (
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger
|
<DropdownMenuTrigger asChild>
|
||||||
render={
|
<Button variant="ghost" className="h-8 w-8 p-0">
|
||||||
<Button variant="ghost" className="h-8 w-8 p-0" />
|
<span className="sr-only">Open menu</span>
|
||||||
}
|
<MoreHorizontal className="h-4 w-4" />
|
||||||
>
|
</Button>
|
||||||
<span className="sr-only">Open menu</span>
|
|
||||||
<MoreHorizontal className="h-4 w-4" />
|
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end">
|
<DropdownMenuContent align="end">
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ export function SwitchStory() {
|
|||||||
className: cn(
|
className: cn(
|
||||||
controls.size === 'sm' && 'h-4 w-7',
|
controls.size === 'sm' && 'h-4 w-7',
|
||||||
controls.size === 'lg' && 'h-6 w-11',
|
controls.size === 'lg' && 'h-6 w-11',
|
||||||
controls.error && 'data-checked:bg-destructive',
|
controls.error && 'data-[state=checked]:bg-destructive',
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -200,7 +200,7 @@ export function SwitchStory() {
|
|||||||
className={cn(
|
className={cn(
|
||||||
controls.size === 'sm' && 'h-4 w-7',
|
controls.size === 'sm' && 'h-4 w-7',
|
||||||
controls.size === 'lg' && 'h-6 w-11',
|
controls.size === 'lg' && 'h-6 w-11',
|
||||||
controls.error && 'data-checked:bg-destructive',
|
controls.error && 'data-[state=checked]:bg-destructive',
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -616,7 +616,7 @@ export function SwitchStory() {
|
|||||||
</Label>
|
</Label>
|
||||||
<Switch
|
<Switch
|
||||||
id="error-switch"
|
id="error-switch"
|
||||||
className="data-checked:bg-destructive"
|
className="data-[state=checked]:bg-destructive"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-destructive text-sm">
|
<p className="text-destructive text-sm">
|
||||||
@@ -642,7 +642,7 @@ export function SwitchStory() {
|
|||||||
<div>
|
<div>
|
||||||
<h4 className="mb-3 text-lg font-semibold">Switch</h4>
|
<h4 className="mb-3 text-lg font-semibold">Switch</h4>
|
||||||
<p className="text-muted-foreground mb-3 text-sm">
|
<p className="text-muted-foreground mb-3 text-sm">
|
||||||
A toggle switch component for boolean states. Built on Base UI
|
A toggle switch component for boolean states. Built on Radix UI
|
||||||
Switch primitive.
|
Switch primitive.
|
||||||
</p>
|
</p>
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
|
|||||||
@@ -62,9 +62,9 @@ interface TabsControlsProps {
|
|||||||
const variantClasses = {
|
const variantClasses = {
|
||||||
default: '',
|
default: '',
|
||||||
pills:
|
pills:
|
||||||
'[&>div]:bg-background [&>div]:border [&>div]:rounded-lg [&>div]:p-1 [&_button]:rounded-md [&_button[data-active]]:bg-primary [&_button[data-active]]:text-primary-foreground',
|
'[&>div]:bg-background [&>div]:border [&>div]:rounded-lg [&>div]:p-1 [&_button]:rounded-md [&_button[data-state=active]]:bg-primary [&_button[data-state=active]]:text-primary-foreground',
|
||||||
underline:
|
underline:
|
||||||
'[&>div]:bg-transparent [&>div]:border-b [&>div]:rounded-none [&_button]:rounded-none [&_button]:border-b-2 [&_button]:border-transparent [&_button[data-active]]:border-primary [&_button[data-active]]:bg-transparent',
|
'[&>div]:bg-transparent [&>div]:border-b [&>div]:rounded-none [&_button]:rounded-none [&_button]:border-b-2 [&_button]:border-transparent [&_button[data-state=active]]:border-primary [&_button[data-state=active]]:bg-transparent',
|
||||||
};
|
};
|
||||||
|
|
||||||
const sizeClasses = {
|
const sizeClasses = {
|
||||||
@@ -683,28 +683,28 @@ function App() {
|
|||||||
<TabsList className="h-auto rounded-none border-b bg-transparent p-0">
|
<TabsList className="h-auto rounded-none border-b bg-transparent p-0">
|
||||||
<TabsTrigger
|
<TabsTrigger
|
||||||
value="overview"
|
value="overview"
|
||||||
className="data-active:border-primary rounded-none border-b-2 border-transparent data-active:bg-transparent"
|
className="data-[state=active]:border-primary rounded-none border-b-2 border-transparent data-[state=active]:bg-transparent"
|
||||||
>
|
>
|
||||||
<BarChart3 className="mr-2 h-4 w-4" />
|
<BarChart3 className="mr-2 h-4 w-4" />
|
||||||
Overview
|
Overview
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger
|
<TabsTrigger
|
||||||
value="users"
|
value="users"
|
||||||
className="data-active:border-primary rounded-none border-b-2 border-transparent data-active:bg-transparent"
|
className="data-[state=active]:border-primary rounded-none border-b-2 border-transparent data-[state=active]:bg-transparent"
|
||||||
>
|
>
|
||||||
<User className="mr-2 h-4 w-4" />
|
<User className="mr-2 h-4 w-4" />
|
||||||
Users
|
Users
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger
|
<TabsTrigger
|
||||||
value="revenue"
|
value="revenue"
|
||||||
className="data-active:border-primary rounded-none border-b-2 border-transparent data-active:bg-transparent"
|
className="data-[state=active]:border-primary rounded-none border-b-2 border-transparent data-[state=active]:bg-transparent"
|
||||||
>
|
>
|
||||||
<CreditCard className="mr-2 h-4 w-4" />
|
<CreditCard className="mr-2 h-4 w-4" />
|
||||||
Revenue
|
Revenue
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger
|
<TabsTrigger
|
||||||
value="reports"
|
value="reports"
|
||||||
className="data-active:border-primary rounded-none border-b-2 border-transparent data-active:bg-transparent"
|
className="data-[state=active]:border-primary rounded-none border-b-2 border-transparent data-[state=active]:bg-transparent"
|
||||||
>
|
>
|
||||||
<FileText className="mr-2 h-4 w-4" />
|
<FileText className="mr-2 h-4 w-4" />
|
||||||
Reports
|
Reports
|
||||||
@@ -905,7 +905,8 @@ const apiReference = {
|
|||||||
{
|
{
|
||||||
name: '...props',
|
name: '...props',
|
||||||
type: 'React.ComponentPropsWithoutRef<typeof TabsPrimitive.Root>',
|
type: 'React.ComponentPropsWithoutRef<typeof TabsPrimitive.Root>',
|
||||||
description: 'All additional props from Base UI Tabs.Root component.',
|
description:
|
||||||
|
'All props from Radix UI Tabs.Root component including asChild, id, etc.',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
examples: [
|
examples: [
|
||||||
|
|||||||
@@ -144,23 +144,22 @@ function TooltipStory() {
|
|||||||
|
|
||||||
let code = `<TooltipProvider${providerPropsString}>\n`;
|
let code = `<TooltipProvider${providerPropsString}>\n`;
|
||||||
code += ` <Tooltip>\n`;
|
code += ` <Tooltip>\n`;
|
||||||
|
code += ` <TooltipTrigger asChild>\n`;
|
||||||
|
|
||||||
if (controls.triggerType === 'button') {
|
if (controls.triggerType === 'button') {
|
||||||
code += ` <TooltipTrigger render={<Button variant="${controls.triggerVariant}" />}>\n`;
|
code += ` <Button variant="${controls.triggerVariant}">Hover me</Button>\n`;
|
||||||
code += ` Hover me\n`;
|
|
||||||
} else if (controls.triggerType === 'icon') {
|
} else if (controls.triggerType === 'icon') {
|
||||||
|
code += ` <Button variant="${controls.triggerVariant}" size="icon">\n`;
|
||||||
const iconName = selectedIconData?.icon.name || 'Info';
|
const iconName = selectedIconData?.icon.name || 'Info';
|
||||||
code += ` <TooltipTrigger render={<Button variant="${controls.triggerVariant}" size="icon" />}>\n`;
|
code += ` <${iconName} className="h-4 w-4" />\n`;
|
||||||
code += ` <${iconName} className="h-4 w-4" />\n`;
|
code += ` </Button>\n`;
|
||||||
} else if (controls.triggerType === 'text') {
|
} else if (controls.triggerType === 'text') {
|
||||||
code += ` <TooltipTrigger render={<span className="cursor-help underline decoration-dotted" />}>\n`;
|
code += ` <span className="cursor-help underline decoration-dotted">Hover me</span>\n`;
|
||||||
code += ` Hover me\n`;
|
|
||||||
} else if (controls.triggerType === 'input') {
|
} else if (controls.triggerType === 'input') {
|
||||||
code += ` <TooltipTrigger render={<Input placeholder="Hover over this input" />} />\n`;
|
code += ` <Input placeholder="Hover over this input" />\n`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (controls.triggerType !== 'input') {
|
code += ` </TooltipTrigger>\n`;
|
||||||
code += ` </TooltipTrigger>\n`;
|
|
||||||
}
|
|
||||||
code += ` <TooltipContent${contentPropsString}>\n`;
|
code += ` <TooltipContent${contentPropsString}>\n`;
|
||||||
code += ` <p>${controls.content}</p>\n`;
|
code += ` <p>${controls.content}</p>\n`;
|
||||||
code += ` </TooltipContent>\n`;
|
code += ` </TooltipContent>\n`;
|
||||||
@@ -171,50 +170,28 @@ function TooltipStory() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const renderPreview = () => {
|
const renderPreview = () => {
|
||||||
const renderTrigger = () => {
|
const trigger = (() => {
|
||||||
switch (controls.triggerType) {
|
switch (controls.triggerType) {
|
||||||
case 'button':
|
case 'button':
|
||||||
return (
|
return <Button variant={controls.triggerVariant}>Hover me</Button>;
|
||||||
<TooltipTrigger
|
|
||||||
render={<Button variant={controls.triggerVariant} />}
|
|
||||||
>
|
|
||||||
Hover me
|
|
||||||
</TooltipTrigger>
|
|
||||||
);
|
|
||||||
case 'icon':
|
case 'icon':
|
||||||
return (
|
return (
|
||||||
<TooltipTrigger
|
<Button variant={controls.triggerVariant} size="icon">
|
||||||
render={<Button variant={controls.triggerVariant} size="icon" />}
|
|
||||||
>
|
|
||||||
<IconComponent className="h-4 w-4" />
|
<IconComponent className="h-4 w-4" />
|
||||||
</TooltipTrigger>
|
</Button>
|
||||||
);
|
);
|
||||||
case 'text':
|
case 'text':
|
||||||
return (
|
return (
|
||||||
<TooltipTrigger
|
<span className="cursor-help underline decoration-dotted">
|
||||||
render={
|
|
||||||
<span className="cursor-help underline decoration-dotted" />
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Hover me
|
Hover me
|
||||||
</TooltipTrigger>
|
</span>
|
||||||
);
|
);
|
||||||
case 'input':
|
case 'input':
|
||||||
return (
|
return <Input placeholder="Hover over this input" />;
|
||||||
<TooltipTrigger
|
|
||||||
render={<Input placeholder="Hover over this input" />}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
default:
|
default:
|
||||||
return (
|
return <Button variant={controls.triggerVariant}>Hover me</Button>;
|
||||||
<TooltipTrigger
|
|
||||||
render={<Button variant={controls.triggerVariant} />}
|
|
||||||
>
|
|
||||||
Hover me
|
|
||||||
</TooltipTrigger>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
};
|
})();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-[200px] items-center justify-center">
|
<div className="flex min-h-[200px] items-center justify-center">
|
||||||
@@ -224,7 +201,7 @@ function TooltipStory() {
|
|||||||
disableHoverableContent={controls.disableHoverableContent}
|
disableHoverableContent={controls.disableHoverableContent}
|
||||||
>
|
>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
{renderTrigger()}
|
<TooltipTrigger asChild>{trigger}</TooltipTrigger>
|
||||||
<TooltipContent
|
<TooltipContent
|
||||||
side={controls.side}
|
side={controls.side}
|
||||||
align={controls.align}
|
align={controls.align}
|
||||||
@@ -399,9 +376,11 @@ function TooltipStory() {
|
|||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<div className="flex flex-wrap gap-4">
|
<div className="flex flex-wrap gap-4">
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger render={<Button variant="outline" />}>
|
<TooltipTrigger asChild>
|
||||||
<Info className="mr-2 h-4 w-4" />
|
<Button variant="outline">
|
||||||
Info Button
|
<Info className="mr-2 h-4 w-4" />
|
||||||
|
Info Button
|
||||||
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
<p>This provides additional information</p>
|
<p>This provides additional information</p>
|
||||||
@@ -409,8 +388,10 @@ function TooltipStory() {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger render={<Button variant="ghost" size="icon" />}>
|
<TooltipTrigger asChild>
|
||||||
<HelpCircle className="h-4 w-4" />
|
<Button variant="ghost" size="icon">
|
||||||
|
<HelpCircle className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
<p>Click for help documentation</p>
|
<p>Click for help documentation</p>
|
||||||
@@ -418,12 +399,10 @@ function TooltipStory() {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger
|
<TooltipTrigger asChild>
|
||||||
render={
|
<span className="cursor-help underline decoration-dotted">
|
||||||
<span className="cursor-help underline decoration-dotted" />
|
Hover for explanation
|
||||||
}
|
</span>
|
||||||
>
|
|
||||||
Hover for explanation
|
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
<p>This term needs clarification for better understanding</p>
|
<p>This term needs clarification for better understanding</p>
|
||||||
@@ -431,9 +410,9 @@ function TooltipStory() {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger
|
<TooltipTrigger asChild>
|
||||||
render={<Input placeholder="Hover me" className="w-48" />}
|
<Input placeholder="Hover me" className="w-48" />
|
||||||
/>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
<p>Enter your email address here</p>
|
<p>Enter your email address here</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
@@ -455,10 +434,10 @@ function TooltipStory() {
|
|||||||
{/* Top Row */}
|
{/* Top Row */}
|
||||||
<div></div>
|
<div></div>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger
|
<TooltipTrigger asChild>
|
||||||
render={<Button variant="outline" size="sm" />}
|
<Button variant="outline" size="sm">
|
||||||
>
|
Top
|
||||||
Top
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent side="top">
|
<TooltipContent side="top">
|
||||||
<p>Tooltip on top</p>
|
<p>Tooltip on top</p>
|
||||||
@@ -468,10 +447,10 @@ function TooltipStory() {
|
|||||||
|
|
||||||
{/* Middle Row */}
|
{/* Middle Row */}
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger
|
<TooltipTrigger asChild>
|
||||||
render={<Button variant="outline" size="sm" />}
|
<Button variant="outline" size="sm">
|
||||||
>
|
Left
|
||||||
Left
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent side="left">
|
<TooltipContent side="left">
|
||||||
<p>Tooltip on left</p>
|
<p>Tooltip on left</p>
|
||||||
@@ -481,10 +460,10 @@ function TooltipStory() {
|
|||||||
<span className="text-muted-foreground text-sm">Center</span>
|
<span className="text-muted-foreground text-sm">Center</span>
|
||||||
</div>
|
</div>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger
|
<TooltipTrigger asChild>
|
||||||
render={<Button variant="outline" size="sm" />}
|
<Button variant="outline" size="sm">
|
||||||
>
|
Right
|
||||||
Right
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent side="right">
|
<TooltipContent side="right">
|
||||||
<p>Tooltip on right</p>
|
<p>Tooltip on right</p>
|
||||||
@@ -494,10 +473,10 @@ function TooltipStory() {
|
|||||||
{/* Bottom Row */}
|
{/* Bottom Row */}
|
||||||
<div></div>
|
<div></div>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger
|
<TooltipTrigger asChild>
|
||||||
render={<Button variant="outline" size="sm" />}
|
<Button variant="outline" size="sm">
|
||||||
>
|
Bottom
|
||||||
Bottom
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent side="bottom">
|
<TooltipContent side="bottom">
|
||||||
<p>Tooltip on bottom</p>
|
<p>Tooltip on bottom</p>
|
||||||
@@ -519,9 +498,11 @@ function TooltipStory() {
|
|||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<div className="flex flex-wrap gap-4">
|
<div className="flex flex-wrap gap-4">
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger render={<Button variant="outline" />}>
|
<TooltipTrigger asChild>
|
||||||
<Star className="mr-2 h-4 w-4" />
|
<Button variant="outline">
|
||||||
Premium Feature
|
<Star className="mr-2 h-4 w-4" />
|
||||||
|
Premium Feature
|
||||||
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent className="max-w-xs">
|
<TooltipContent className="max-w-xs">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
@@ -535,9 +516,11 @@ function TooltipStory() {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger render={<Button variant="outline" />}>
|
<TooltipTrigger asChild>
|
||||||
<Settings className="mr-2 h-4 w-4" />
|
<Button variant="outline">
|
||||||
Advanced Settings
|
<Settings className="mr-2 h-4 w-4" />
|
||||||
|
Advanced Settings
|
||||||
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
@@ -554,9 +537,11 @@ function TooltipStory() {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger render={<Button variant="destructive" />}>
|
<TooltipTrigger asChild>
|
||||||
<AlertCircle className="mr-2 h-4 w-4" />
|
<Button variant="destructive">
|
||||||
Delete Account
|
<AlertCircle className="mr-2 h-4 w-4" />
|
||||||
|
Delete Account
|
||||||
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent className="border-destructive bg-destructive text-destructive-foreground max-w-xs">
|
<TooltipContent className="border-destructive bg-destructive text-destructive-foreground max-w-xs">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
@@ -583,10 +568,10 @@ function TooltipStory() {
|
|||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger
|
<TooltipTrigger asChild>
|
||||||
render={<Button size="icon" variant="ghost" />}
|
<Button size="icon" variant="ghost">
|
||||||
>
|
<Copy className="h-4 w-4" />
|
||||||
<Copy className="h-4 w-4" />
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
<p>Copy to clipboard</p>
|
<p>Copy to clipboard</p>
|
||||||
@@ -594,10 +579,10 @@ function TooltipStory() {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger
|
<TooltipTrigger asChild>
|
||||||
render={<Button size="icon" variant="ghost" />}
|
<Button size="icon" variant="ghost">
|
||||||
>
|
<Download className="h-4 w-4" />
|
||||||
<Download className="h-4 w-4" />
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
<p>Download file</p>
|
<p>Download file</p>
|
||||||
@@ -605,10 +590,10 @@ function TooltipStory() {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger
|
<TooltipTrigger asChild>
|
||||||
render={<Button size="icon" variant="ghost" />}
|
<Button size="icon" variant="ghost">
|
||||||
>
|
<Share className="h-4 w-4" />
|
||||||
<Share className="h-4 w-4" />
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
<p>Share with others</p>
|
<p>Share with others</p>
|
||||||
@@ -620,11 +605,9 @@ function TooltipStory() {
|
|||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="username">Username</Label>
|
<Label htmlFor="username">Username</Label>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger
|
<TooltipTrigger asChild>
|
||||||
render={
|
<Input id="username" placeholder="Enter username" />
|
||||||
<Input id="username" placeholder="Enter username" />
|
</TooltipTrigger>
|
||||||
}
|
|
||||||
/>
|
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
<p>Must be 3-20 characters, letters and numbers only</p>
|
<p>Must be 3-20 characters, letters and numbers only</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
@@ -633,7 +616,9 @@ function TooltipStory() {
|
|||||||
|
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger render={<Checkbox id="terms" />} />
|
<TooltipTrigger asChild>
|
||||||
|
<Checkbox id="terms" />
|
||||||
|
</TooltipTrigger>
|
||||||
<TooltipContent className="max-w-xs">
|
<TooltipContent className="max-w-xs">
|
||||||
<p>
|
<p>
|
||||||
By checking this, you agree to our Terms of Service and
|
By checking this, you agree to our Terms of Service and
|
||||||
@@ -766,7 +751,7 @@ function TooltipStory() {
|
|||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<strong>TooltipTrigger:</strong> Element that triggers the
|
<strong>TooltipTrigger:</strong> Element that triggers the
|
||||||
tooltip (use render prop)
|
tooltip (use asChild prop)
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -492,7 +492,7 @@ export const COMPONENTS_REGISTRY: ComponentInfo[] = [
|
|||||||
status: 'stable',
|
status: 'stable',
|
||||||
component: CardButtonStory,
|
component: CardButtonStory,
|
||||||
sourceFile: '@kit/ui/card-button',
|
sourceFile: '@kit/ui/card-button',
|
||||||
props: ['className', 'children', 'onClick', 'disabled'],
|
props: ['asChild', 'className', 'children', 'onClick', 'disabled'],
|
||||||
icon: MousePointer,
|
icon: MousePointer,
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -950,7 +950,7 @@ export const COMPONENTS_REGISTRY: ComponentInfo[] = [
|
|||||||
status: 'stable',
|
status: 'stable',
|
||||||
component: ItemStory,
|
component: ItemStory,
|
||||||
sourceFile: '@kit/ui/item',
|
sourceFile: '@kit/ui/item',
|
||||||
props: ['variant', 'size', 'className'],
|
props: ['variant', 'size', 'asChild', 'className'],
|
||||||
icon: Layers,
|
icon: Layers,
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -1004,7 +1004,7 @@ export const COMPONENTS_REGISTRY: ComponentInfo[] = [
|
|||||||
status: 'stable',
|
status: 'stable',
|
||||||
component: BreadcrumbStory,
|
component: BreadcrumbStory,
|
||||||
sourceFile: '@kit/ui/breadcrumb',
|
sourceFile: '@kit/ui/breadcrumb',
|
||||||
props: ['separator', 'href', 'className'],
|
props: ['separator', 'asChild', 'href', 'className'],
|
||||||
icon: ChevronRight,
|
icon: ChevronRight,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { withI18n } from '../../lib/i18n/with-i18n';
|
||||||
import { DocsContent } from './components/docs-content';
|
import { DocsContent } from './components/docs-content';
|
||||||
import { DocsHeader } from './components/docs-header';
|
import { DocsHeader } from './components/docs-header';
|
||||||
import { DocsSidebar } from './components/docs-sidebar';
|
import { DocsSidebar } from './components/docs-sidebar';
|
||||||
@@ -28,4 +29,4 @@ async function ComponentDocsPage(props: ComponentDocsPageProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ComponentDocsPage;
|
export default withI18n(ComponentDocsPage);
|
||||||
|
|||||||
@@ -67,10 +67,10 @@ export default async function EmailPage(props: EmailPageProps) {
|
|||||||
Remember that the below is an approximation of the email. Always test
|
Remember that the below is an approximation of the email. Always test
|
||||||
it in your inbox.{' '}
|
it in your inbox.{' '}
|
||||||
<Dialog>
|
<Dialog>
|
||||||
<DialogTrigger
|
<DialogTrigger asChild>
|
||||||
render={<Button variant={'link'} className="p-0 underline" />}
|
<Button variant={'link'} className="p-0 underline">
|
||||||
>
|
Test Email
|
||||||
Test Email
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
|
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import * as z from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
export const EmailTesterFormSchema = z.object({
|
export const EmailTesterFormSchema = z.object({
|
||||||
username: z.string().min(1),
|
username: z.string().min(1),
|
||||||
|
|||||||
@@ -49,16 +49,13 @@ export default async function EmailsPage() {
|
|||||||
|
|
||||||
<div className={'grid grid-cols-1 gap-4 md:grid-cols-4'}>
|
<div className={'grid grid-cols-1 gap-4 md:grid-cols-4'}>
|
||||||
{categoryTemplates.map((template) => (
|
{categoryTemplates.map((template) => (
|
||||||
<CardButton
|
<CardButton key={template.id} asChild>
|
||||||
key={template.id}
|
<Link href={`/emails/${template.id}`}>
|
||||||
render={
|
<CardButtonHeader>
|
||||||
<Link href={`/emails/${template.id}`}>
|
<CardButtonTitle>{template.name}</CardButtonTitle>
|
||||||
<CardButtonHeader>
|
</CardButtonHeader>
|
||||||
<CardButtonTitle>{template.name}</CardButtonTitle>
|
</Link>
|
||||||
</CardButtonHeader>
|
</CardButton>
|
||||||
</Link>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import type { Metadata } from 'next';
|
|||||||
|
|
||||||
import { DevToolLayout } from '@/components/app-layout';
|
import { DevToolLayout } from '@/components/app-layout';
|
||||||
import { RootProviders } from '@/components/root-providers';
|
import { RootProviders } from '@/components/root-providers';
|
||||||
import { getMessages } from 'next-intl/server';
|
|
||||||
|
|
||||||
import '../styles/globals.css';
|
import '../styles/globals.css';
|
||||||
|
|
||||||
@@ -11,17 +10,15 @@ export const metadata: Metadata = {
|
|||||||
description: 'The dev tool for Makerkit',
|
description: 'The dev tool for Makerkit',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default async function RootLayout({
|
export default function RootLayout({
|
||||||
children,
|
children,
|
||||||
}: Readonly<{
|
}: Readonly<{
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}>) {
|
}>) {
|
||||||
const messages = await getMessages();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<body>
|
<body>
|
||||||
<RootProviders messages={messages}>
|
<RootProviders>
|
||||||
<DevToolLayout>{children}</DevToolLayout>
|
<DevToolLayout>{children}</DevToolLayout>
|
||||||
</RootProviders>
|
</RootProviders>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ export default async function DashboardPage() {
|
|||||||
return (
|
return (
|
||||||
<Page style={'custom'}>
|
<Page style={'custom'}>
|
||||||
<PageHeader
|
<PageHeader
|
||||||
|
displaySidebarTrigger={false}
|
||||||
title={'Dev Tool'}
|
title={'Dev Tool'}
|
||||||
description={'Kit MCP status for this workspace'}
|
description={'Kit MCP status for this workspace'}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import * as z from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
export const CreatePRDSchema = z.object({
|
export const CreatePRDSchema = z.object({
|
||||||
title: z
|
title: z
|
||||||
@@ -32,4 +32,4 @@ export const CreatePRDSchema = z.object({
|
|||||||
.min(1, 'At least one success metric is required'),
|
.min(1, 'At least one success metric is required'),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type CreatePRDData = z.output<typeof CreatePRDSchema>;
|
export type CreatePRDData = z.infer<typeof CreatePRDSchema>;
|
||||||
|
|||||||
@@ -131,14 +131,12 @@ export function TranslationsComparison({
|
|||||||
|
|
||||||
<If condition={locales.length > 1}>
|
<If condition={locales.length > 1}>
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger
|
<DropdownMenuTrigger asChild>
|
||||||
render={
|
<Button variant="outline" className="ml-auto">
|
||||||
<Button variant="outline" className="ml-auto">
|
Select Languages
|
||||||
Select Languages
|
<ChevronDownIcon className="ml-2 h-4 w-4" />
|
||||||
<ChevronDownIcon className="ml-2 h-4 w-4" />
|
</Button>
|
||||||
</Button>
|
</DropdownMenuTrigger>
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<DropdownMenuContent align="end" className="w-[200px]">
|
<DropdownMenuContent align="end" className="w-[200px]">
|
||||||
{locales.map((locale) => (
|
{locales.map((locale) => (
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { revalidatePath } from 'next/cache';
|
import { revalidatePath } from 'next/cache';
|
||||||
|
|
||||||
import * as z from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { findWorkspaceRoot } from '@kit/mcp-server/env';
|
import { findWorkspaceRoot } from '@kit/mcp-server/env';
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -731,15 +731,13 @@ function FilterSwitcher(props: {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger
|
<DropdownMenuTrigger asChild>
|
||||||
render={
|
<Button variant="outline" className="font-normal">
|
||||||
<Button variant="outline" className="font-normal">
|
{buttonLabel()}
|
||||||
{buttonLabel()}
|
|
||||||
|
|
||||||
<ChevronsUpDownIcon className="text-muted-foreground ml-1 h-3 w-3" />
|
<ChevronsUpDownIcon className="text-muted-foreground ml-1 h-3 w-3" />
|
||||||
</Button>
|
</Button>
|
||||||
}
|
</DropdownMenuTrigger>
|
||||||
/>
|
|
||||||
|
|
||||||
<DropdownMenuContent>
|
<DropdownMenuContent>
|
||||||
<DropdownMenuCheckboxItem
|
<DropdownMenuCheckboxItem
|
||||||
@@ -888,41 +886,38 @@ function Summary({ appState }: { appState: AppEnvState }) {
|
|||||||
|
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger
|
<TooltipTrigger asChild>
|
||||||
render={
|
<Button
|
||||||
<Button
|
variant="outline"
|
||||||
variant="outline"
|
size={'sm'}
|
||||||
size={'sm'}
|
onClick={() => {
|
||||||
onClick={() => {
|
let data = '';
|
||||||
let data = '';
|
|
||||||
|
|
||||||
const groups = getGroups(appState, () => true);
|
const groups = getGroups(appState, () => true);
|
||||||
|
|
||||||
groups.forEach((group) => {
|
groups.forEach((group) => {
|
||||||
data += `# ${group.category}\n`;
|
data += `# ${group.category}\n`;
|
||||||
|
|
||||||
group.variables.forEach((variable) => {
|
group.variables.forEach((variable) => {
|
||||||
data += `${variable.key}=${variable.effectiveValue}\n`;
|
data += `${variable.key}=${variable.effectiveValue}\n`;
|
||||||
});
|
|
||||||
|
|
||||||
data += '\n';
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const promise = copyToClipboard(data);
|
data += '\n';
|
||||||
|
});
|
||||||
|
|
||||||
toast.promise(promise, {
|
const promise = copyToClipboard(data);
|
||||||
loading: 'Copying environment variables...',
|
|
||||||
success: 'Environment variables copied to clipboard.',
|
toast.promise(promise, {
|
||||||
error:
|
loading: 'Copying environment variables...',
|
||||||
'Failed to copy environment variables to clipboard',
|
success: 'Environment variables copied to clipboard.',
|
||||||
});
|
error: 'Failed to copy environment variables to clipboard',
|
||||||
}}
|
});
|
||||||
>
|
}}
|
||||||
<CopyIcon className={'mr-2 h-4 w-4'} />
|
>
|
||||||
<span>Copy env file to clipboard</span>
|
<CopyIcon className={'mr-2 h-4 w-4'} />
|
||||||
</Button>
|
<span>Copy env file to clipboard</span>
|
||||||
}
|
</Button>
|
||||||
/>
|
</TooltipTrigger>
|
||||||
|
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
Copy environment variables to clipboard. You can place it in your
|
Copy environment variables to clipboard. You can place it in your
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { revalidatePath } from 'next/cache';
|
import { revalidatePath } from 'next/cache';
|
||||||
|
|
||||||
import * as z from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
createKitEnvDeps,
|
createKitEnvDeps,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { DevToolSidebar } from '@/components/app-sidebar';
|
import { DevToolSidebar } from '@/components/app-sidebar';
|
||||||
|
|
||||||
import { SidebarInset, SidebarProvider } from '@kit/ui/sidebar';
|
import { SidebarInset, SidebarProvider } from '@kit/ui/shadcn-sidebar';
|
||||||
|
|
||||||
export function DevToolLayout(props: React.PropsWithChildren) {
|
export function DevToolLayout(props: React.PropsWithChildren) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import {
|
|||||||
SidebarMenuSub,
|
SidebarMenuSub,
|
||||||
SidebarMenuSubButton,
|
SidebarMenuSubButton,
|
||||||
SidebarMenuSubItem,
|
SidebarMenuSubItem,
|
||||||
} from '@kit/ui/sidebar';
|
} from '@kit/ui/shadcn-sidebar';
|
||||||
import { isRouteActive } from '@kit/ui/utils';
|
import { isRouteActive } from '@kit/ui/utils';
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
@@ -92,14 +92,14 @@ export function DevToolSidebar({
|
|||||||
{route.children.map((child) => (
|
{route.children.map((child) => (
|
||||||
<SidebarMenuSubItem key={child.path}>
|
<SidebarMenuSubItem key={child.path}>
|
||||||
<SidebarMenuSubButton
|
<SidebarMenuSubButton
|
||||||
render={
|
asChild
|
||||||
<Link href={child.path}>
|
|
||||||
<child.Icon className="h-4 w-4" />
|
|
||||||
<span>{child.label}</span>
|
|
||||||
</Link>
|
|
||||||
}
|
|
||||||
isActive={isRouteActive(child.path, pathname, false)}
|
isActive={isRouteActive(child.path, pathname, false)}
|
||||||
/>
|
>
|
||||||
|
<Link href={child.path}>
|
||||||
|
<child.Icon className="h-4 w-4" />
|
||||||
|
<span>{child.label}</span>
|
||||||
|
</Link>
|
||||||
|
</SidebarMenuSubButton>
|
||||||
</SidebarMenuSubItem>
|
</SidebarMenuSubItem>
|
||||||
))}
|
))}
|
||||||
</SidebarMenuSub>
|
</SidebarMenuSub>
|
||||||
@@ -107,13 +107,13 @@ export function DevToolSidebar({
|
|||||||
) : (
|
) : (
|
||||||
<SidebarMenuButton
|
<SidebarMenuButton
|
||||||
isActive={isRouteActive(route.path, pathname, false)}
|
isActive={isRouteActive(route.path, pathname, false)}
|
||||||
render={
|
asChild
|
||||||
<Link href={route.path}>
|
>
|
||||||
<route.Icon className="h-4 w-4" />
|
<Link href={route.path}>
|
||||||
<span>{route.label}</span>
|
<route.Icon className="h-4 w-4" />
|
||||||
</Link>
|
<span>{route.label}</span>
|
||||||
}
|
</Link>
|
||||||
/>
|
</SidebarMenuButton>
|
||||||
)}
|
)}
|
||||||
</SidebarMenuItem>
|
</SidebarMenuItem>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -3,18 +3,18 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
import type { AbstractIntlMessages } from 'next-intl';
|
|
||||||
|
|
||||||
import { I18nClientProvider } from '@kit/i18n/provider';
|
import { I18nProvider } from '@kit/i18n/provider';
|
||||||
import { Toaster } from '@kit/ui/sonner';
|
import { Toaster } from '@kit/ui/sonner';
|
||||||
|
|
||||||
export function RootProviders(
|
import { i18nResolver } from '../lib/i18n/i18n.resolver';
|
||||||
props: React.PropsWithChildren<{ messages: AbstractIntlMessages }>,
|
import { getI18nSettings } from '../lib/i18n/i18n.settings';
|
||||||
) {
|
|
||||||
|
export function RootProviders(props: React.PropsWithChildren) {
|
||||||
return (
|
return (
|
||||||
<I18nClientProvider locale="en" messages={props.messages}>
|
<I18nProvider settings={getI18nSettings('en')} resolver={i18nResolver}>
|
||||||
<ReactQueryProvider>{props.children}</ReactQueryProvider>
|
<ReactQueryProvider>{props.children}</ReactQueryProvider>
|
||||||
</I18nClientProvider>
|
</I18nProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ interface ServiceCardProps {
|
|||||||
export const ServiceCard = ({ name, status }: ServiceCardProps) => {
|
export const ServiceCard = ({ name, status }: ServiceCardProps) => {
|
||||||
return (
|
return (
|
||||||
<Card className="w-full max-w-2xl">
|
<Card className="w-full max-w-2xl">
|
||||||
<CardContent>
|
<CardContent className="p-4">
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center space-x-4">
|
<div className="flex items-center space-x-4">
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
import { getRequestConfig } from 'next-intl/server';
|
|
||||||
|
|
||||||
import account from '../../web/i18n/messages/en/account.json';
|
|
||||||
import auth from '../../web/i18n/messages/en/auth.json';
|
|
||||||
import billing from '../../web/i18n/messages/en/billing.json';
|
|
||||||
import common from '../../web/i18n/messages/en/common.json';
|
|
||||||
import marketing from '../../web/i18n/messages/en/marketing.json';
|
|
||||||
import teams from '../../web/i18n/messages/en/teams.json';
|
|
||||||
|
|
||||||
export default getRequestConfig(async () => {
|
|
||||||
return {
|
|
||||||
locale: 'en',
|
|
||||||
messages: {
|
|
||||||
common,
|
|
||||||
auth,
|
|
||||||
account,
|
|
||||||
teams,
|
|
||||||
billing,
|
|
||||||
marketing,
|
|
||||||
},
|
|
||||||
timeZone: 'UTC',
|
|
||||||
getMessageFallback(info) {
|
|
||||||
return info.key;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
13
apps/dev-tool/lib/i18n/with-i18n.tsx
Normal file
13
apps/dev-tool/lib/i18n/with-i18n.tsx
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { createI18nServerInstance } from './i18n.server';
|
||||||
|
|
||||||
|
type LayoutOrPageComponent<Params> = React.ComponentType<Params>;
|
||||||
|
|
||||||
|
export function withI18n<Params extends object>(
|
||||||
|
Component: LayoutOrPageComponent<Params>,
|
||||||
|
) {
|
||||||
|
return async function I18nServerComponentWrapper(params: Params) {
|
||||||
|
await createI18nServerInstance();
|
||||||
|
|
||||||
|
return <Component {...params} />;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,12 +1,8 @@
|
|||||||
import type { NextConfig } from 'next';
|
import type { NextConfig } from 'next';
|
||||||
|
|
||||||
import createNextIntlPlugin from 'next-intl/plugin';
|
|
||||||
|
|
||||||
const withNextIntl = createNextIntlPlugin('./i18n/request.ts');
|
|
||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
reactStrictMode: true,
|
reactStrictMode: true,
|
||||||
transpilePackages: ['@kit/ui', '@kit/shared', '@kit/i18n'],
|
transpilePackages: ['@kit/ui', '@kit/shared'],
|
||||||
reactCompiler: true,
|
reactCompiler: true,
|
||||||
devIndicators: {
|
devIndicators: {
|
||||||
position: 'bottom-right',
|
position: 'bottom-right',
|
||||||
@@ -18,4 +14,4 @@ const nextConfig: NextConfig = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default withNextIntl(nextConfig);
|
export default nextConfig;
|
||||||
|
|||||||
@@ -13,7 +13,6 @@
|
|||||||
"@tanstack/react-query": "catalog:",
|
"@tanstack/react-query": "catalog:",
|
||||||
"lucide-react": "catalog:",
|
"lucide-react": "catalog:",
|
||||||
"next": "catalog:",
|
"next": "catalog:",
|
||||||
"next-intl": "catalog:",
|
|
||||||
"nodemailer": "catalog:",
|
"nodemailer": "catalog:",
|
||||||
"react": "catalog:",
|
"react": "catalog:",
|
||||||
"react-dom": "catalog:",
|
"react-dom": "catalog:",
|
||||||
@@ -36,7 +35,7 @@
|
|||||||
"babel-plugin-react-compiler": "1.0.0",
|
"babel-plugin-react-compiler": "1.0.0",
|
||||||
"pino-pretty": "13.0.0",
|
"pino-pretty": "13.0.0",
|
||||||
"react-hook-form": "catalog:",
|
"react-hook-form": "catalog:",
|
||||||
"recharts": "3.7.0",
|
"recharts": "2.15.3",
|
||||||
"tailwindcss": "catalog:",
|
"tailwindcss": "catalog:",
|
||||||
"tw-animate-css": "catalog:",
|
"tw-animate-css": "catalog:",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
|
|||||||
@@ -66,6 +66,26 @@
|
|||||||
--animate-accordion-down: accordion-down 0.2s ease-out;
|
--animate-accordion-down: accordion-down 0.2s ease-out;
|
||||||
--animate-accordion-up: accordion-up 0.2s ease-out;
|
--animate-accordion-up: accordion-up 0.2s ease-out;
|
||||||
|
|
||||||
|
@keyframes accordion-down {
|
||||||
|
from {
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
height: var(--radix-accordion-content-height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes accordion-up {
|
||||||
|
from {
|
||||||
|
height: var(--radix-accordion-content-height);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes fade-up {
|
@keyframes fade-up {
|
||||||
0% {
|
0% {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"report": "playwright show-report",
|
"report": "playwright show-report",
|
||||||
"test": "playwright test --max-failures=1 --workers=4",
|
"test": "playwright test --max-failures=1",
|
||||||
"test:fast": "playwright test --max-failures=1 --workers=16",
|
"test:fast": "playwright test --max-failures=1 --workers=16",
|
||||||
"test:setup": "playwright test tests/auth.setup.ts",
|
"test:setup": "playwright test tests/auth.setup.ts",
|
||||||
"test:ui": "playwright test --ui"
|
"test:ui": "playwright test --ui"
|
||||||
|
|||||||
@@ -38,8 +38,6 @@ test.describe('Account Settings', () => {
|
|||||||
|
|
||||||
await Promise.all([request, response]);
|
await Promise.all([request, response]);
|
||||||
|
|
||||||
await page.locator('[data-test="workspace-dropdown-trigger"]').click();
|
|
||||||
|
|
||||||
await expect(account.getProfileName()).toHaveText(name);
|
await expect(account.getProfileName()).toHaveText(name);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -34,17 +34,17 @@ test.describe('Admin', () => {
|
|||||||
await page.goto('/admin');
|
await page.goto('/admin');
|
||||||
|
|
||||||
// Check all stat cards are present
|
// Check all stat cards are present
|
||||||
await expect(page.getByText('Users', { exact: true })).toBeVisible();
|
await expect(page.getByRole('heading', { name: 'Users' })).toBeVisible();
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
page.getByText('Team Accounts', { exact: true }),
|
page.getByRole('heading', { name: 'Team Accounts' }),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
page.getByText('Paying Customers', { exact: true }),
|
page.getByRole('heading', { name: 'Paying Customers' }),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
|
|
||||||
await expect(page.getByText('Trials', { exact: true })).toBeVisible();
|
await expect(page.getByRole('heading', { name: 'Trials' })).toBeVisible();
|
||||||
|
|
||||||
// Verify stat values are numbers
|
// Verify stat values are numbers
|
||||||
const stats = await page.$$('.text-3xl.font-bold');
|
const stats = await page.$$('.text-3xl.font-bold');
|
||||||
|
|||||||
@@ -31,17 +31,8 @@ export class AuthPageObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async signOut() {
|
async signOut() {
|
||||||
const trigger = this.page.locator(
|
await this.page.click('[data-test="account-dropdown-trigger"]');
|
||||||
'[data-test="workspace-dropdown-trigger"], [data-test="account-dropdown-trigger"]',
|
await this.page.click('[data-test="account-dropdown-sign-out"]');
|
||||||
);
|
|
||||||
|
|
||||||
await trigger.click();
|
|
||||||
|
|
||||||
const signOutButton = this.page.locator(
|
|
||||||
'[data-test="workspace-sign-out"], [data-test="account-dropdown-sign-out"]',
|
|
||||||
);
|
|
||||||
|
|
||||||
await signOutButton.click();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async signIn(params: { email: string; password: string }) {
|
async signIn(params: { email: string; password: string }) {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { expect, test } from '@playwright/test';
|
|||||||
|
|
||||||
test.describe('Healthcheck endpoint', () => {
|
test.describe('Healthcheck endpoint', () => {
|
||||||
test('returns healthy status', async ({ request }) => {
|
test('returns healthy status', async ({ request }) => {
|
||||||
const response = await request.get('/api/healthcheck');
|
const response = await request.get('/healthcheck');
|
||||||
|
|
||||||
expect(response.status()).toBe(200);
|
expect(response.status()).toBe(200);
|
||||||
|
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ export class InvitationsPageObject {
|
|||||||
`[data-test="invite-member-form-item"]:nth-child(${nth}) [data-test="role-selector-trigger"]`,
|
`[data-test="invite-member-form-item"]:nth-child(${nth}) [data-test="role-selector-trigger"]`,
|
||||||
);
|
);
|
||||||
|
|
||||||
await this.page.getByRole('option', { name: invite.role }).click();
|
await this.page.click(`[data-test="role-option-${invite.role}"]`);
|
||||||
|
|
||||||
if (index < invites.length - 1) {
|
if (index < invites.length - 1) {
|
||||||
await form.locator('[data-test="add-new-invite-button"]').click();
|
await form.locator('[data-test="add-new-invite-button"]').click();
|
||||||
|
|||||||
@@ -36,13 +36,13 @@ export class TeamAccountsPageObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getTeamFromSelector(teamName: string) {
|
getTeamFromSelector(teamName: string) {
|
||||||
return this.page.locator('[data-test="workspace-team-item"]', {
|
return this.page.locator(`[data-test="account-selector-team"]`, {
|
||||||
hasText: teamName,
|
hasText: teamName,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getTeams() {
|
getTeams() {
|
||||||
return this.page.locator('[data-test="workspace-team-item"]');
|
return this.page.locator('[data-test="account-selector-team"]');
|
||||||
}
|
}
|
||||||
|
|
||||||
goToSettings() {
|
goToSettings() {
|
||||||
@@ -83,11 +83,10 @@ export class TeamAccountsPageObject {
|
|||||||
|
|
||||||
openAccountsSelector() {
|
openAccountsSelector() {
|
||||||
return expect(async () => {
|
return expect(async () => {
|
||||||
await this.page.click('[data-test="workspace-dropdown-trigger"]');
|
await this.page.click('[data-test="account-selector-trigger"]');
|
||||||
await this.page.click('[data-test="workspace-switch-submenu"]');
|
|
||||||
|
|
||||||
return expect(
|
return expect(
|
||||||
this.page.locator('[data-test="workspace-switch-content"]'),
|
this.page.locator('[data-test="account-selector-content"]'),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
}).toPass();
|
}).toPass();
|
||||||
}
|
}
|
||||||
@@ -116,7 +115,7 @@ export class TeamAccountsPageObject {
|
|||||||
async createTeam({ teamName, slug } = this.createTeamName()) {
|
async createTeam({ teamName, slug } = this.createTeamName()) {
|
||||||
await this.openAccountsSelector();
|
await this.openAccountsSelector();
|
||||||
|
|
||||||
await this.page.click('[data-test="create-team-trigger"]');
|
await this.page.click('[data-test="create-team-account-trigger"]');
|
||||||
|
|
||||||
await this.page.fill(
|
await this.page.fill(
|
||||||
'[data-test="create-team-form"] [data-test="team-name-input"]',
|
'[data-test="create-team-form"] [data-test="team-name-input"]',
|
||||||
@@ -141,15 +140,14 @@ export class TeamAccountsPageObject {
|
|||||||
await this.openAccountsSelector();
|
await this.openAccountsSelector();
|
||||||
await expect(this.getTeamFromSelector(teamName)).toBeVisible();
|
await expect(this.getTeamFromSelector(teamName)).toBeVisible();
|
||||||
|
|
||||||
// Close the selector (Escape closes submenu, then parent dropdown)
|
// Close the selector
|
||||||
await this.page.keyboard.press('Escape');
|
|
||||||
await this.page.keyboard.press('Escape');
|
await this.page.keyboard.press('Escape');
|
||||||
}
|
}
|
||||||
|
|
||||||
async createTeamWithNonLatinName(teamName: string, slug: string) {
|
async createTeamWithNonLatinName(teamName: string, slug: string) {
|
||||||
await this.openAccountsSelector();
|
await this.openAccountsSelector();
|
||||||
|
|
||||||
await this.page.click('[data-test="create-team-trigger"]');
|
await this.page.click('[data-test="create-team-account-trigger"]');
|
||||||
|
|
||||||
await this.page.fill(
|
await this.page.fill(
|
||||||
'[data-test="create-team-form"] [data-test="team-name-input"]',
|
'[data-test="create-team-form"] [data-test="team-name-input"]',
|
||||||
@@ -179,8 +177,7 @@ export class TeamAccountsPageObject {
|
|||||||
await this.openAccountsSelector();
|
await this.openAccountsSelector();
|
||||||
await expect(this.getTeamFromSelector(teamName)).toBeVisible();
|
await expect(this.getTeamFromSelector(teamName)).toBeVisible();
|
||||||
|
|
||||||
// Close the selector (Escape closes submenu, then parent dropdown)
|
// Close the selector
|
||||||
await this.page.keyboard.press('Escape');
|
|
||||||
await this.page.keyboard.press('Escape');
|
await this.page.keyboard.press('Escape');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -210,10 +207,11 @@ export class TeamAccountsPageObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async deleteAccount(email: string) {
|
async deleteAccount(email: string) {
|
||||||
await this.page.click('[data-test="delete-team-trigger"]');
|
|
||||||
await this.otp.completeOtpVerification(email);
|
|
||||||
|
|
||||||
await expect(async () => {
|
await expect(async () => {
|
||||||
|
await this.page.click('[data-test="delete-team-trigger"]');
|
||||||
|
|
||||||
|
await this.otp.completeOtpVerification(email);
|
||||||
|
|
||||||
const click = this.page.click(
|
const click = this.page.click(
|
||||||
'[data-test="delete-team-form-confirm-button"]',
|
'[data-test="delete-team-form-confirm-button"]',
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ test.describe('Team Accounts', () => {
|
|||||||
await teamAccounts.createTeam();
|
await teamAccounts.createTeam();
|
||||||
|
|
||||||
await teamAccounts.openAccountsSelector();
|
await teamAccounts.openAccountsSelector();
|
||||||
await page.click('[data-test="create-team-trigger"]');
|
await page.click('[data-test="create-team-account-trigger"]');
|
||||||
|
|
||||||
await teamAccounts.tryCreateTeam('billing');
|
await teamAccounts.tryCreateTeam('billing');
|
||||||
|
|
||||||
@@ -202,7 +202,7 @@ test.describe('Team Accounts', () => {
|
|||||||
|
|
||||||
// Use non-Latin name to trigger the slug field visibility
|
// Use non-Latin name to trigger the slug field visibility
|
||||||
await teamAccounts.openAccountsSelector();
|
await teamAccounts.openAccountsSelector();
|
||||||
await page.click('[data-test="create-team-trigger"]');
|
await page.click('[data-test="create-team-account-trigger"]');
|
||||||
|
|
||||||
await page.fill(
|
await page.fill(
|
||||||
'[data-test="create-team-form"] [data-test="team-name-input"]',
|
'[data-test="create-team-form"] [data-test="team-name-input"]',
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_DELETION=true
|
|||||||
NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_BILLING=true
|
NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_BILLING=true
|
||||||
NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS=true
|
NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS=true
|
||||||
NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_CREATION=true
|
NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_CREATION=true
|
||||||
NEXT_PUBLIC_ENABLE_TEAMS_ACCOUNTS_ONLY=false
|
|
||||||
NEXT_PUBLIC_LANGUAGE_PRIORITY=application
|
NEXT_PUBLIC_LANGUAGE_PRIORITY=application
|
||||||
|
|
||||||
# NEXTJS
|
# NEXTJS
|
||||||
|
|||||||
30
apps/web/app/(marketing)/(legal)/cookie-policy/page.tsx
Normal file
30
apps/web/app/(marketing)/(legal)/cookie-policy/page.tsx
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { SitePageHeader } from '~/(marketing)/_components/site-page-header';
|
||||||
|
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
||||||
|
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||||
|
|
||||||
|
export async function generateMetadata() {
|
||||||
|
const { t } = await createI18nServerInstance();
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: t('marketing:cookiePolicy'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function CookiePolicyPage() {
|
||||||
|
const { t } = await createI18nServerInstance();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<SitePageHeader
|
||||||
|
title={t(`marketing:cookiePolicy`)}
|
||||||
|
subtitle={t(`marketing:cookiePolicyDescription`)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className={'container mx-auto py-8'}>
|
||||||
|
<div>Your terms of service content here</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withI18n(CookiePolicyPage);
|
||||||
30
apps/web/app/(marketing)/(legal)/privacy-policy/page.tsx
Normal file
30
apps/web/app/(marketing)/(legal)/privacy-policy/page.tsx
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { SitePageHeader } from '~/(marketing)/_components/site-page-header';
|
||||||
|
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
||||||
|
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||||
|
|
||||||
|
export async function generateMetadata() {
|
||||||
|
const { t } = await createI18nServerInstance();
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: t('marketing:privacyPolicy'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function PrivacyPolicyPage() {
|
||||||
|
const { t } = await createI18nServerInstance();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<SitePageHeader
|
||||||
|
title={t('marketing:privacyPolicy')}
|
||||||
|
subtitle={t('marketing:privacyPolicyDescription')}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className={'container mx-auto py-8'}>
|
||||||
|
<div>Your terms of service content here</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withI18n(PrivacyPolicyPage);
|
||||||
30
apps/web/app/(marketing)/(legal)/terms-of-service/page.tsx
Normal file
30
apps/web/app/(marketing)/(legal)/terms-of-service/page.tsx
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { SitePageHeader } from '~/(marketing)/_components/site-page-header';
|
||||||
|
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
||||||
|
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||||
|
|
||||||
|
export async function generateMetadata() {
|
||||||
|
const { t } = await createI18nServerInstance();
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: t('marketing:termsOfService'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function TermsOfServicePage() {
|
||||||
|
const { t } = await createI18nServerInstance();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<SitePageHeader
|
||||||
|
title={t(`marketing:termsOfService`)}
|
||||||
|
subtitle={t(`marketing:termsOfServiceDescription`)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className={'container mx-auto py-8'}>
|
||||||
|
<div>Your terms of service content here</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withI18n(TermsOfServicePage);
|
||||||
@@ -8,10 +8,10 @@ export function SiteFooter() {
|
|||||||
return (
|
return (
|
||||||
<Footer
|
<Footer
|
||||||
logo={<AppLogo className="w-[85px] md:w-[95px]" />}
|
logo={<AppLogo className="w-[85px] md:w-[95px]" />}
|
||||||
description={<Trans i18nKey="marketing.footerDescription" />}
|
description={<Trans i18nKey="marketing:footerDescription" />}
|
||||||
copyright={
|
copyright={
|
||||||
<Trans
|
<Trans
|
||||||
i18nKey="marketing.copyright"
|
i18nKey="marketing:copyright"
|
||||||
values={{
|
values={{
|
||||||
product: appConfig.name,
|
product: appConfig.name,
|
||||||
year: new Date().getFullYear(),
|
year: new Date().getFullYear(),
|
||||||
@@ -20,35 +20,35 @@ export function SiteFooter() {
|
|||||||
}
|
}
|
||||||
sections={[
|
sections={[
|
||||||
{
|
{
|
||||||
heading: <Trans i18nKey="marketing.about" />,
|
heading: <Trans i18nKey="marketing:about" />,
|
||||||
links: [
|
links: [
|
||||||
{ href: '/blog', label: <Trans i18nKey="marketing.blog" /> },
|
{ href: '/blog', label: <Trans i18nKey="marketing:blog" /> },
|
||||||
{ href: '/contact', label: <Trans i18nKey="marketing.contact" /> },
|
{ href: '/contact', label: <Trans i18nKey="marketing:contact" /> },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
heading: <Trans i18nKey="marketing.product" />,
|
heading: <Trans i18nKey="marketing:product" />,
|
||||||
links: [
|
links: [
|
||||||
{
|
{
|
||||||
href: '/docs',
|
href: '/docs',
|
||||||
label: <Trans i18nKey="marketing.documentation" />,
|
label: <Trans i18nKey="marketing:documentation" />,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
heading: <Trans i18nKey="marketing.legal" />,
|
heading: <Trans i18nKey="marketing:legal" />,
|
||||||
links: [
|
links: [
|
||||||
{
|
{
|
||||||
href: '/terms-of-service',
|
href: '/terms-of-service',
|
||||||
label: <Trans i18nKey="marketing.termsOfService" />,
|
label: <Trans i18nKey="marketing:termsOfService" />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/privacy-policy',
|
href: '/privacy-policy',
|
||||||
label: <Trans i18nKey="marketing.privacyPolicy" />,
|
label: <Trans i18nKey="marketing:privacyPolicy" />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/cookie-policy',
|
href: '/cookie-policy',
|
||||||
label: <Trans i18nKey="marketing.cookiePolicy" />,
|
label: <Trans i18nKey="marketing:cookiePolicy" />,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -31,7 +31,6 @@ const MobileModeToggle = dynamic(
|
|||||||
|
|
||||||
const paths = {
|
const paths = {
|
||||||
home: pathsConfig.app.home,
|
home: pathsConfig.app.home,
|
||||||
profileSettings: pathsConfig.app.personalAccountSettings,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const features = {
|
const features = {
|
||||||
@@ -79,28 +78,26 @@ function AuthButtons() {
|
|||||||
|
|
||||||
<div className={'flex items-center gap-x-2'}>
|
<div className={'flex items-center gap-x-2'}>
|
||||||
<Button
|
<Button
|
||||||
nativeButton={false}
|
|
||||||
className={'hidden md:flex md:text-sm'}
|
className={'hidden md:flex md:text-sm'}
|
||||||
render={
|
asChild
|
||||||
<Link href={pathsConfig.auth.signIn}>
|
|
||||||
<Trans i18nKey={'auth.signIn'} />
|
|
||||||
</Link>
|
|
||||||
}
|
|
||||||
variant={'outline'}
|
variant={'outline'}
|
||||||
size={'sm'}
|
size={'sm'}
|
||||||
/>
|
>
|
||||||
|
<Link href={pathsConfig.auth.signIn}>
|
||||||
|
<Trans i18nKey={'auth:signIn'} />
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
nativeButton={false}
|
asChild
|
||||||
render={
|
|
||||||
<Link href={pathsConfig.auth.signUp}>
|
|
||||||
<Trans i18nKey={'auth.signUp'} />
|
|
||||||
</Link>
|
|
||||||
}
|
|
||||||
className="text-xs md:text-sm"
|
className="text-xs md:text-sm"
|
||||||
variant={'default'}
|
variant={'default'}
|
||||||
size={'sm'}
|
size={'sm'}
|
||||||
/>
|
>
|
||||||
|
<Link href={pathsConfig.auth.signUp}>
|
||||||
|
<Trans i18nKey={'auth:signUp'} />
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -9,7 +9,7 @@ import { SiteNavigation } from './site-navigation';
|
|||||||
export function SiteHeader(props: { user?: JWTUserData | null }) {
|
export function SiteHeader(props: { user?: JWTUserData | null }) {
|
||||||
return (
|
return (
|
||||||
<Header
|
<Header
|
||||||
logo={<AppLogo className="mx-auto sm:mx-0" href="/" />}
|
logo={<AppLogo />}
|
||||||
navigation={<SiteNavigation />}
|
navigation={<SiteNavigation />}
|
||||||
actions={<SiteHeaderAccountSection user={props.user ?? null} />}
|
actions={<SiteHeaderAccountSection user={props.user ?? null} />}
|
||||||
/>
|
/>
|
||||||
@@ -15,23 +15,23 @@ import { SiteNavigationItem } from './site-navigation-item';
|
|||||||
|
|
||||||
const links = {
|
const links = {
|
||||||
Blog: {
|
Blog: {
|
||||||
label: 'marketing.blog',
|
label: 'marketing:blog',
|
||||||
path: '/blog',
|
path: '/blog',
|
||||||
},
|
},
|
||||||
Changelog: {
|
Changelog: {
|
||||||
label: 'marketing.changelog',
|
label: 'marketing:changelog',
|
||||||
path: '/changelog',
|
path: '/changelog',
|
||||||
},
|
},
|
||||||
Docs: {
|
Docs: {
|
||||||
label: 'marketing.documentation',
|
label: 'marketing:documentation',
|
||||||
path: '/docs',
|
path: '/docs',
|
||||||
},
|
},
|
||||||
Pricing: {
|
Pricing: {
|
||||||
label: 'marketing.pricing',
|
label: 'marketing:pricing',
|
||||||
path: '/pricing',
|
path: '/pricing',
|
||||||
},
|
},
|
||||||
FAQ: {
|
FAQ: {
|
||||||
label: 'marketing.faq',
|
label: 'marketing:faq',
|
||||||
path: '/faq',
|
path: '/faq',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -74,14 +74,11 @@ function MobileDropdown() {
|
|||||||
const className = 'flex w-full h-full items-center';
|
const className = 'flex w-full h-full items-center';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem key={item.path} asChild>
|
||||||
key={item.path}
|
<Link className={className} href={item.path}>
|
||||||
render={
|
<Trans i18nKey={item.label} />
|
||||||
<Link className={className} href={item.path}>
|
</Link>
|
||||||
<Trans i18nKey={item.label} />
|
</DropdownMenuItem>
|
||||||
</Link>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
@@ -6,6 +6,8 @@ import { notFound } from 'next/navigation';
|
|||||||
|
|
||||||
import { createCmsClient } from '@kit/cms';
|
import { createCmsClient } from '@kit/cms';
|
||||||
|
|
||||||
|
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||||
|
|
||||||
import { Post } from '../../blog/_components/post';
|
import { Post } from '../../blog/_components/post';
|
||||||
|
|
||||||
interface BlogPageProps {
|
interface BlogPageProps {
|
||||||
@@ -73,4 +75,4 @@ async function BlogPost({ params }: BlogPageProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default BlogPost;
|
export default withI18n(BlogPost);
|
||||||
@@ -25,7 +25,7 @@ export function BlogPagination(props: {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ArrowLeft className={'mr-2 h-4'} />
|
<ArrowLeft className={'mr-2 h-4'} />
|
||||||
<Trans i18nKey={'marketing.blogPaginationPrevious'} />
|
<Trans i18nKey={'marketing:blogPaginationPrevious'} />
|
||||||
</Button>
|
</Button>
|
||||||
</If>
|
</If>
|
||||||
|
|
||||||
@@ -36,7 +36,7 @@ export function BlogPagination(props: {
|
|||||||
navigate(props.currentPage + 1);
|
navigate(props.currentPage + 1);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Trans i18nKey={'marketing.blogPaginationNext'} />
|
<Trans i18nKey={'marketing:blogPaginationNext'} />
|
||||||
<ArrowRight className={'ml-2 h-4'} />
|
<ArrowRight className={'ml-2 h-4'} />
|
||||||
</Button>
|
</Button>
|
||||||
</If>
|
</If>
|
||||||
@@ -2,13 +2,14 @@ import { cache } from 'react';
|
|||||||
|
|
||||||
import type { Metadata } from 'next';
|
import type { Metadata } from 'next';
|
||||||
|
|
||||||
import { getLocale, getTranslations } from 'next-intl/server';
|
|
||||||
|
|
||||||
import { createCmsClient } from '@kit/cms';
|
import { createCmsClient } from '@kit/cms';
|
||||||
import { getLogger } from '@kit/shared/logger';
|
import { getLogger } from '@kit/shared/logger';
|
||||||
import { If } from '@kit/ui/if';
|
import { If } from '@kit/ui/if';
|
||||||
import { Trans } from '@kit/ui/trans';
|
import { Trans } from '@kit/ui/trans';
|
||||||
|
|
||||||
|
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
||||||
|
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||||
|
|
||||||
// local imports
|
// local imports
|
||||||
import { SitePageHeader } from '../_components/site-page-header';
|
import { SitePageHeader } from '../_components/site-page-header';
|
||||||
import { BlogPagination } from './_components/blog-pagination';
|
import { BlogPagination } from './_components/blog-pagination';
|
||||||
@@ -23,8 +24,7 @@ const BLOG_POSTS_PER_PAGE = 10;
|
|||||||
export const generateMetadata = async (
|
export const generateMetadata = async (
|
||||||
props: BlogPageProps,
|
props: BlogPageProps,
|
||||||
): Promise<Metadata> => {
|
): Promise<Metadata> => {
|
||||||
const t = await getTranslations('marketing');
|
const { t, resolvedLanguage } = await createI18nServerInstance();
|
||||||
const resolvedLanguage = await getLocale();
|
|
||||||
const searchParams = await props.searchParams;
|
const searchParams = await props.searchParams;
|
||||||
const limit = BLOG_POSTS_PER_PAGE;
|
const limit = BLOG_POSTS_PER_PAGE;
|
||||||
|
|
||||||
@@ -34,8 +34,8 @@ export const generateMetadata = async (
|
|||||||
const { total } = await getContentItems(resolvedLanguage, limit, offset);
|
const { total } = await getContentItems(resolvedLanguage, limit, offset);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: t('blog'),
|
title: t('marketing:blog'),
|
||||||
description: t('blogSubtitle'),
|
description: t('marketing:blogSubtitle'),
|
||||||
pagination: {
|
pagination: {
|
||||||
previous: page > 0 ? `/blog?page=${page - 1}` : undefined,
|
previous: page > 0 ? `/blog?page=${page - 1}` : undefined,
|
||||||
next: offset + limit < total ? `/blog?page=${page + 1}` : undefined,
|
next: offset + limit < total ? `/blog?page=${page + 1}` : undefined,
|
||||||
@@ -67,8 +67,7 @@ const getContentItems = cache(
|
|||||||
);
|
);
|
||||||
|
|
||||||
async function BlogPage(props: BlogPageProps) {
|
async function BlogPage(props: BlogPageProps) {
|
||||||
const t = await getTranslations('marketing');
|
const { t, resolvedLanguage: language } = await createI18nServerInstance();
|
||||||
const language = await getLocale();
|
|
||||||
const searchParams = await props.searchParams;
|
const searchParams = await props.searchParams;
|
||||||
|
|
||||||
const limit = BLOG_POSTS_PER_PAGE;
|
const limit = BLOG_POSTS_PER_PAGE;
|
||||||
@@ -83,12 +82,15 @@ async function BlogPage(props: BlogPageProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SitePageHeader title={t('blog')} subtitle={t('blogSubtitle')} />
|
<SitePageHeader
|
||||||
|
title={t('marketing:blog')}
|
||||||
|
subtitle={t('marketing:blogSubtitle')}
|
||||||
|
/>
|
||||||
|
|
||||||
<div className={'container flex flex-col space-y-6 py-8'}>
|
<div className={'container flex flex-col space-y-6 py-8'}>
|
||||||
<If
|
<If
|
||||||
condition={posts.length > 0}
|
condition={posts.length > 0}
|
||||||
fallback={<Trans i18nKey="marketing.noPosts" />}
|
fallback={<Trans i18nKey="marketing:noPosts" />}
|
||||||
>
|
>
|
||||||
<PostsGridList>
|
<PostsGridList>
|
||||||
{posts.map((post, idx) => {
|
{posts.map((post, idx) => {
|
||||||
@@ -109,7 +111,7 @@ async function BlogPage(props: BlogPageProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default BlogPage;
|
export default withI18n(BlogPage);
|
||||||
|
|
||||||
function PostsGridList({ children }: React.PropsWithChildren) {
|
function PostsGridList({ children }: React.PropsWithChildren) {
|
||||||
return (
|
return (
|
||||||
@@ -6,6 +6,8 @@ import { notFound } from 'next/navigation';
|
|||||||
|
|
||||||
import { createCmsClient } from '@kit/cms';
|
import { createCmsClient } from '@kit/cms';
|
||||||
|
|
||||||
|
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||||
|
|
||||||
import { ChangelogDetail } from '../_components/changelog-detail';
|
import { ChangelogDetail } from '../_components/changelog-detail';
|
||||||
|
|
||||||
interface ChangelogEntryPageProps {
|
interface ChangelogEntryPageProps {
|
||||||
@@ -105,4 +107,4 @@ async function ChangelogEntryPage({ params }: ChangelogEntryPageProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ChangelogEntryPage;
|
export default withI18n(ChangelogEntryPage);
|
||||||
@@ -22,7 +22,7 @@ export function ChangelogHeader({ entry }: { entry: Cms.ContentItem }) {
|
|||||||
className="text-muted-foreground hover:text-primary flex items-center gap-1.5 text-sm font-medium transition-colors"
|
className="text-muted-foreground hover:text-primary flex items-center gap-1.5 text-sm font-medium transition-colors"
|
||||||
>
|
>
|
||||||
<ChevronLeft className="h-4 w-4" />
|
<ChevronLeft className="h-4 w-4" />
|
||||||
<Trans i18nKey="marketing.changelog" />
|
<Trans i18nKey="marketing:changelog" />
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -24,8 +24,8 @@ function NavLink({ entry, direction }: NavLinkProps) {
|
|||||||
|
|
||||||
const Icon = isPrevious ? ChevronLeft : ChevronRight;
|
const Icon = isPrevious ? ChevronLeft : ChevronRight;
|
||||||
const i18nKey = isPrevious
|
const i18nKey = isPrevious
|
||||||
? 'marketing.changelogNavigationPrevious'
|
? 'marketing:changelogNavigationPrevious'
|
||||||
: 'marketing.changelogNavigationNext';
|
: 'marketing:changelogNavigationNext';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
@@ -22,29 +22,24 @@ export function ChangelogPagination({
|
|||||||
return (
|
return (
|
||||||
<div className="flex justify-end gap-2">
|
<div className="flex justify-end gap-2">
|
||||||
{canGoToPreviousPage && (
|
{canGoToPreviousPage && (
|
||||||
<Button
|
<Button asChild variant="outline" size="sm">
|
||||||
render={<Link href={`/changelog?page=${previousPage}`} />}
|
<Link href={`/changelog?page=${previousPage}`}>
|
||||||
variant="outline"
|
<ArrowLeft className="mr-2 h-3 w-3" />
|
||||||
size="sm"
|
<span>
|
||||||
>
|
<Trans i18nKey="marketing:changelogPaginationPrevious" />
|
||||||
<ArrowLeft className="mr-2 h-3 w-3" />
|
</span>
|
||||||
|
</Link>
|
||||||
<span>
|
|
||||||
<Trans i18nKey="marketing.changelogPaginationPrevious" />
|
|
||||||
</span>
|
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{canGoToNextPage && (
|
{canGoToNextPage && (
|
||||||
<Button
|
<Button asChild variant="outline" size="sm">
|
||||||
render={<Link href={`/changelog?page=${nextPage}`} />}
|
<Link href={`/changelog?page=${nextPage}`}>
|
||||||
variant="outline"
|
<span>
|
||||||
size="sm"
|
<Trans i18nKey="marketing:changelogPaginationNext" />
|
||||||
>
|
</span>
|
||||||
<span>
|
<ArrowRight className="ml-2 h-3 w-3" />
|
||||||
<Trans i18nKey="marketing.changelogPaginationNext" />
|
</Link>
|
||||||
</span>
|
|
||||||
<ArrowRight className="ml-2 h-3 w-3" />
|
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -2,13 +2,14 @@ import { cache } from 'react';
|
|||||||
|
|
||||||
import type { Metadata } from 'next';
|
import type { Metadata } from 'next';
|
||||||
|
|
||||||
import { getLocale, getTranslations } from 'next-intl/server';
|
|
||||||
|
|
||||||
import { createCmsClient } from '@kit/cms';
|
import { createCmsClient } from '@kit/cms';
|
||||||
import { getLogger } from '@kit/shared/logger';
|
import { getLogger } from '@kit/shared/logger';
|
||||||
import { If } from '@kit/ui/if';
|
import { If } from '@kit/ui/if';
|
||||||
import { Trans } from '@kit/ui/trans';
|
import { Trans } from '@kit/ui/trans';
|
||||||
|
|
||||||
|
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
||||||
|
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||||
|
|
||||||
import { SitePageHeader } from '../_components/site-page-header';
|
import { SitePageHeader } from '../_components/site-page-header';
|
||||||
import { ChangelogEntry } from './_components/changelog-entry';
|
import { ChangelogEntry } from './_components/changelog-entry';
|
||||||
import { ChangelogPagination } from './_components/changelog-pagination';
|
import { ChangelogPagination } from './_components/changelog-pagination';
|
||||||
@@ -22,8 +23,7 @@ const CHANGELOG_ENTRIES_PER_PAGE = 50;
|
|||||||
export const generateMetadata = async (
|
export const generateMetadata = async (
|
||||||
props: ChangelogPageProps,
|
props: ChangelogPageProps,
|
||||||
): Promise<Metadata> => {
|
): Promise<Metadata> => {
|
||||||
const t = await getTranslations('marketing');
|
const { t, resolvedLanguage } = await createI18nServerInstance();
|
||||||
const resolvedLanguage = await getLocale();
|
|
||||||
const searchParams = await props.searchParams;
|
const searchParams = await props.searchParams;
|
||||||
const limit = CHANGELOG_ENTRIES_PER_PAGE;
|
const limit = CHANGELOG_ENTRIES_PER_PAGE;
|
||||||
|
|
||||||
@@ -33,8 +33,8 @@ export const generateMetadata = async (
|
|||||||
const { total } = await getContentItems(resolvedLanguage, limit, offset);
|
const { total } = await getContentItems(resolvedLanguage, limit, offset);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: t('changelog'),
|
title: t('marketing:changelog'),
|
||||||
description: t('changelogSubtitle'),
|
description: t('marketing:changelogSubtitle'),
|
||||||
pagination: {
|
pagination: {
|
||||||
previous: page > 0 ? `/changelog?page=${page - 1}` : undefined,
|
previous: page > 0 ? `/changelog?page=${page - 1}` : undefined,
|
||||||
next: offset + limit < total ? `/changelog?page=${page + 1}` : undefined,
|
next: offset + limit < total ? `/changelog?page=${page + 1}` : undefined,
|
||||||
@@ -66,8 +66,7 @@ const getContentItems = cache(
|
|||||||
);
|
);
|
||||||
|
|
||||||
async function ChangelogPage(props: ChangelogPageProps) {
|
async function ChangelogPage(props: ChangelogPageProps) {
|
||||||
const t = await getTranslations('marketing');
|
const { t, resolvedLanguage: language } = await createI18nServerInstance();
|
||||||
const language = await getLocale();
|
|
||||||
const searchParams = await props.searchParams;
|
const searchParams = await props.searchParams;
|
||||||
|
|
||||||
const limit = CHANGELOG_ENTRIES_PER_PAGE;
|
const limit = CHANGELOG_ENTRIES_PER_PAGE;
|
||||||
@@ -83,14 +82,14 @@ async function ChangelogPage(props: ChangelogPageProps) {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SitePageHeader
|
<SitePageHeader
|
||||||
title={t('changelog')}
|
title={t('marketing:changelog')}
|
||||||
subtitle={t('changelogSubtitle')}
|
subtitle={t('marketing:changelogSubtitle')}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="container flex max-w-4xl flex-col space-y-12 py-12">
|
<div className="container flex max-w-4xl flex-col space-y-12 py-12">
|
||||||
<If
|
<If
|
||||||
condition={entries.length > 0}
|
condition={entries.length > 0}
|
||||||
fallback={<Trans i18nKey="marketing.noChangelogEntries" />}
|
fallback={<Trans i18nKey="marketing:noChangelogEntries" />}
|
||||||
>
|
>
|
||||||
<div className="space-y-0">
|
<div className="space-y-0">
|
||||||
{entries.map((entry, index) => {
|
{entries.map((entry, index) => {
|
||||||
@@ -115,4 +114,4 @@ async function ChangelogPage(props: ChangelogPageProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ChangelogPage;
|
export default withI18n(ChangelogPage);
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState, useTransition } from 'react';
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { useAction } from 'next-safe-action/hooks';
|
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
|
|
||||||
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
|
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
|
||||||
@@ -24,20 +23,13 @@ import { ContactEmailSchema } from '~/(marketing)/contact/_lib/contact-email.sch
|
|||||||
import { sendContactEmail } from '~/(marketing)/contact/_lib/server/server-actions';
|
import { sendContactEmail } from '~/(marketing)/contact/_lib/server/server-actions';
|
||||||
|
|
||||||
export function ContactForm() {
|
export function ContactForm() {
|
||||||
|
const [pending, startTransition] = useTransition();
|
||||||
|
|
||||||
const [state, setState] = useState({
|
const [state, setState] = useState({
|
||||||
success: false,
|
success: false,
|
||||||
error: false,
|
error: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { execute, isPending } = useAction(sendContactEmail, {
|
|
||||||
onSuccess: () => {
|
|
||||||
setState({ success: true, error: false });
|
|
||||||
},
|
|
||||||
onError: () => {
|
|
||||||
setState({ error: true, success: false });
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
resolver: zodResolver(ContactEmailSchema),
|
resolver: zodResolver(ContactEmailSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@@ -60,7 +52,15 @@ export function ContactForm() {
|
|||||||
<form
|
<form
|
||||||
className={'flex flex-col space-y-4'}
|
className={'flex flex-col space-y-4'}
|
||||||
onSubmit={form.handleSubmit((data) => {
|
onSubmit={form.handleSubmit((data) => {
|
||||||
execute(data);
|
startTransition(async () => {
|
||||||
|
try {
|
||||||
|
await sendContactEmail(data);
|
||||||
|
|
||||||
|
setState({ success: true, error: false });
|
||||||
|
} catch {
|
||||||
|
setState({ error: true, success: false });
|
||||||
|
}
|
||||||
|
});
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<FormField
|
<FormField
|
||||||
@@ -69,7 +69,7 @@ export function ContactForm() {
|
|||||||
return (
|
return (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>
|
<FormLabel>
|
||||||
<Trans i18nKey={'marketing.contactName'} />
|
<Trans i18nKey={'marketing:contactName'} />
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
|
|
||||||
<FormControl>
|
<FormControl>
|
||||||
@@ -88,7 +88,7 @@ export function ContactForm() {
|
|||||||
return (
|
return (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>
|
<FormLabel>
|
||||||
<Trans i18nKey={'marketing.contactEmail'} />
|
<Trans i18nKey={'marketing:contactEmail'} />
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
|
|
||||||
<FormControl>
|
<FormControl>
|
||||||
@@ -107,7 +107,7 @@ export function ContactForm() {
|
|||||||
return (
|
return (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>
|
<FormLabel>
|
||||||
<Trans i18nKey={'marketing.contactMessage'} />
|
<Trans i18nKey={'marketing:contactMessage'} />
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
|
|
||||||
<FormControl>
|
<FormControl>
|
||||||
@@ -124,8 +124,8 @@ export function ContactForm() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Button disabled={isPending} type={'submit'}>
|
<Button disabled={pending} type={'submit'}>
|
||||||
<Trans i18nKey={'marketing.sendMessage'} />
|
<Trans i18nKey={'marketing:sendMessage'} />
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
@@ -136,11 +136,11 @@ function SuccessAlert() {
|
|||||||
return (
|
return (
|
||||||
<Alert variant={'success'}>
|
<Alert variant={'success'}>
|
||||||
<AlertTitle>
|
<AlertTitle>
|
||||||
<Trans i18nKey={'marketing.contactSuccess'} />
|
<Trans i18nKey={'marketing:contactSuccess'} />
|
||||||
</AlertTitle>
|
</AlertTitle>
|
||||||
|
|
||||||
<AlertDescription>
|
<AlertDescription>
|
||||||
<Trans i18nKey={'marketing.contactSuccessDescription'} />
|
<Trans i18nKey={'marketing:contactSuccessDescription'} />
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
);
|
);
|
||||||
@@ -150,11 +150,11 @@ function ErrorAlert() {
|
|||||||
return (
|
return (
|
||||||
<Alert variant={'destructive'}>
|
<Alert variant={'destructive'}>
|
||||||
<AlertTitle>
|
<AlertTitle>
|
||||||
<Trans i18nKey={'marketing.contactError'} />
|
<Trans i18nKey={'marketing:contactError'} />
|
||||||
</AlertTitle>
|
</AlertTitle>
|
||||||
|
|
||||||
<AlertDescription>
|
<AlertDescription>
|
||||||
<Trans i18nKey={'marketing.contactErrorDescription'} />
|
<Trans i18nKey={'marketing:contactErrorDescription'} />
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
);
|
);
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import * as z from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
export const ContactEmailSchema = z.object({
|
export const ContactEmailSchema = z.object({
|
||||||
name: z.string().min(1).max(200),
|
name: z.string().min(1).max(200),
|
||||||
@@ -1,29 +1,30 @@
|
|||||||
'use server';
|
'use server';
|
||||||
|
|
||||||
import * as z from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { getMailer } from '@kit/mailers';
|
import { getMailer } from '@kit/mailers';
|
||||||
import { publicActionClient } from '@kit/next/safe-action';
|
import { enhanceAction } from '@kit/next/actions';
|
||||||
|
|
||||||
import { ContactEmailSchema } from '../contact-email.schema';
|
import { ContactEmailSchema } from '../contact-email.schema';
|
||||||
|
|
||||||
const contactEmail = z
|
const contactEmail = z
|
||||||
.string({
|
.string({
|
||||||
error:
|
description: `The email where you want to receive the contact form submissions.`,
|
||||||
|
required_error:
|
||||||
'Contact email is required. Please use the environment variable CONTACT_EMAIL.',
|
'Contact email is required. Please use the environment variable CONTACT_EMAIL.',
|
||||||
})
|
})
|
||||||
.parse(process.env.CONTACT_EMAIL);
|
.parse(process.env.CONTACT_EMAIL);
|
||||||
|
|
||||||
const emailFrom = z
|
const emailFrom = z
|
||||||
.string({
|
.string({
|
||||||
error:
|
description: `The email sending address.`,
|
||||||
|
required_error:
|
||||||
'Sender email is required. Please use the environment variable EMAIL_SENDER.',
|
'Sender email is required. Please use the environment variable EMAIL_SENDER.',
|
||||||
})
|
})
|
||||||
.parse(process.env.EMAIL_SENDER);
|
.parse(process.env.EMAIL_SENDER);
|
||||||
|
|
||||||
export const sendContactEmail = publicActionClient
|
export const sendContactEmail = enhanceAction(
|
||||||
.schema(ContactEmailSchema)
|
async (data) => {
|
||||||
.action(async ({ parsedInput: data }) => {
|
|
||||||
const mailer = await getMailer();
|
const mailer = await getMailer();
|
||||||
|
|
||||||
await mailer.sendEmail({
|
await mailer.sendEmail({
|
||||||
@@ -42,4 +43,9 @@ export const sendContactEmail = publicActionClient
|
|||||||
});
|
});
|
||||||
|
|
||||||
return {};
|
return {};
|
||||||
});
|
},
|
||||||
|
{
|
||||||
|
schema: ContactEmailSchema,
|
||||||
|
auth: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
@@ -1,25 +1,28 @@
|
|||||||
import { getTranslations } from 'next-intl/server';
|
|
||||||
|
|
||||||
import { Heading } from '@kit/ui/heading';
|
import { Heading } from '@kit/ui/heading';
|
||||||
import { Trans } from '@kit/ui/trans';
|
import { Trans } from '@kit/ui/trans';
|
||||||
|
|
||||||
import { SitePageHeader } from '~/(marketing)/_components/site-page-header';
|
import { SitePageHeader } from '~/(marketing)/_components/site-page-header';
|
||||||
import { ContactForm } from '~/(marketing)/contact/_components/contact-form';
|
import { ContactForm } from '~/(marketing)/contact/_components/contact-form';
|
||||||
|
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
||||||
|
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||||
|
|
||||||
export async function generateMetadata() {
|
export async function generateMetadata() {
|
||||||
const t = await getTranslations('marketing');
|
const { t } = await createI18nServerInstance();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: t('contact'),
|
title: t('marketing:contact'),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function ContactPage() {
|
async function ContactPage() {
|
||||||
const t = await getTranslations('marketing');
|
const { t } = await createI18nServerInstance();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<SitePageHeader title={t(`contact`)} subtitle={t(`contactDescription`)} />
|
<SitePageHeader
|
||||||
|
title={t(`marketing:contact`)}
|
||||||
|
subtitle={t(`marketing:contactDescription`)}
|
||||||
|
/>
|
||||||
|
|
||||||
<div className={'container mx-auto'}>
|
<div className={'container mx-auto'}>
|
||||||
<div
|
<div
|
||||||
@@ -32,11 +35,11 @@ async function ContactPage() {
|
|||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<Heading level={3}>
|
<Heading level={3}>
|
||||||
<Trans i18nKey={'marketing.contactHeading'} />
|
<Trans i18nKey={'marketing:contactHeading'} />
|
||||||
</Heading>
|
</Heading>
|
||||||
|
|
||||||
<p className={'text-muted-foreground'}>
|
<p className={'text-muted-foreground'}>
|
||||||
<Trans i18nKey={'marketing.contactSubheading'} />
|
<Trans i18nKey={'marketing:contactSubheading'} />
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -48,4 +51,4 @@ async function ContactPage() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ContactPage;
|
export default withI18n(ContactPage);
|
||||||
@@ -7,6 +7,8 @@ import { If } from '@kit/ui/if';
|
|||||||
import { Separator } from '@kit/ui/separator';
|
import { Separator } from '@kit/ui/separator';
|
||||||
import { cn } from '@kit/ui/utils';
|
import { cn } from '@kit/ui/utils';
|
||||||
|
|
||||||
|
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||||
|
|
||||||
// local imports
|
// local imports
|
||||||
import { DocsCards } from '../_components/docs-cards';
|
import { DocsCards } from '../_components/docs-cards';
|
||||||
|
|
||||||
@@ -89,4 +91,4 @@ async function DocumentationPage({ params }: DocumentationPageProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default DocumentationPage;
|
export default withI18n(DocumentationPage);
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { usePathname } from 'next/navigation';
|
import { usePathname } from 'next/navigation';
|
||||||
|
|
||||||
import { SidebarMenuButton, SidebarMenuItem } from '@kit/ui/sidebar';
|
import { SidebarMenuButton, SidebarMenuItem } from '@kit/ui/shadcn-sidebar';
|
||||||
import { cn, isRouteActive } from '@kit/ui/utils';
|
import { cn, isRouteActive } from '@kit/ui/utils';
|
||||||
|
|
||||||
export function DocsNavLink({
|
export function DocsNavLink({
|
||||||
@@ -12,18 +12,20 @@ export function DocsNavLink({
|
|||||||
children,
|
children,
|
||||||
}: React.PropsWithChildren<{ label: string; url: string }>) {
|
}: React.PropsWithChildren<{ label: string; url: string }>) {
|
||||||
const currentPath = usePathname();
|
const currentPath = usePathname();
|
||||||
const isCurrent = isRouteActive(url, currentPath);
|
const isCurrent = isRouteActive(url, currentPath, true);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SidebarMenuItem>
|
<SidebarMenuItem>
|
||||||
<SidebarMenuButton
|
<SidebarMenuButton
|
||||||
render={<Link href={url} />}
|
asChild
|
||||||
isActive={isCurrent}
|
isActive={isCurrent}
|
||||||
className={cn('text-secondary-foreground transition-all')}
|
className={cn('text-secondary-foreground transition-all')}
|
||||||
>
|
>
|
||||||
<span className="block max-w-full truncate">{label}</span>
|
<Link href={url}>
|
||||||
|
<span className="block max-w-full truncate">{label}</span>
|
||||||
|
|
||||||
{children}
|
{children}
|
||||||
|
</Link>
|
||||||
</SidebarMenuButton>
|
</SidebarMenuButton>
|
||||||
</SidebarMenuItem>
|
</SidebarMenuItem>
|
||||||
);
|
);
|
||||||
@@ -16,7 +16,7 @@ export function DocsNavigationCollapsible(
|
|||||||
const prefix = props.prefix;
|
const prefix = props.prefix;
|
||||||
|
|
||||||
const isChildActive = props.node.children.some((child) =>
|
const isChildActive = props.node.children.some((child) =>
|
||||||
isRouteActive(prefix + '/' + child.url, currentPath),
|
isRouteActive(prefix + '/' + child.url, currentPath, false),
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -10,12 +10,12 @@ import {
|
|||||||
SidebarMenuButton,
|
SidebarMenuButton,
|
||||||
SidebarMenuItem,
|
SidebarMenuItem,
|
||||||
SidebarMenuSub,
|
SidebarMenuSub,
|
||||||
} from '@kit/ui/sidebar';
|
} from '@kit/ui/shadcn-sidebar';
|
||||||
|
|
||||||
import { DocsNavLink } from '~/(marketing)/docs/_components/docs-nav-link';
|
import { DocsNavLink } from '~/(marketing)/docs/_components/docs-nav-link';
|
||||||
import { DocsNavigationCollapsible } from '~/(marketing)/docs/_components/docs-navigation-collapsible';
|
import { DocsNavigationCollapsible } from '~/(marketing)/docs/_components/docs-navigation-collapsible';
|
||||||
|
|
||||||
import { FloatingDocumentationNavigationButton } from './floating-docs-navigation-button';
|
import { FloatingDocumentationNavigation } from './floating-docs-navigation';
|
||||||
|
|
||||||
function Node({
|
function Node({
|
||||||
node,
|
node,
|
||||||
@@ -85,11 +85,13 @@ function NodeTrigger({
|
|||||||
}) {
|
}) {
|
||||||
if (node.collapsible) {
|
if (node.collapsible) {
|
||||||
return (
|
return (
|
||||||
<CollapsibleTrigger render={<SidebarMenuItem />}>
|
<CollapsibleTrigger asChild>
|
||||||
<SidebarMenuButton>
|
<SidebarMenuItem>
|
||||||
{label}
|
<SidebarMenuButton>
|
||||||
<ChevronDown className="ml-auto transition-transform group-data-[state=open]/collapsible:rotate-180" />
|
{label}
|
||||||
</SidebarMenuButton>
|
<ChevronDown className="ml-auto transition-transform group-data-[state=open]/collapsible:rotate-180" />
|
||||||
|
</SidebarMenuButton>
|
||||||
|
</SidebarMenuItem>
|
||||||
</CollapsibleTrigger>
|
</CollapsibleTrigger>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -135,10 +137,12 @@ export function DocsNavigation({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Sidebar
|
<Sidebar
|
||||||
variant={'sidebar'}
|
variant={'ghost'}
|
||||||
className={'sticky z-1 mt-4 max-h-full overflow-y-auto pr-4'}
|
className={
|
||||||
|
'border-border/50 sticky z-1 mt-4 max-h-full overflow-y-auto pr-4'
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<SidebarGroup>
|
<SidebarGroup className="p-0">
|
||||||
<SidebarGroupContent>
|
<SidebarGroupContent>
|
||||||
<SidebarMenu className={'pb-48'}>
|
<SidebarMenu className={'pb-48'}>
|
||||||
<Tree pages={pages} level={0} prefix={prefix} />
|
<Tree pages={pages} level={0} prefix={prefix} />
|
||||||
@@ -147,7 +151,17 @@ export function DocsNavigation({
|
|||||||
</SidebarGroup>
|
</SidebarGroup>
|
||||||
</Sidebar>
|
</Sidebar>
|
||||||
|
|
||||||
<FloatingDocumentationNavigationButton />
|
<div className={'lg:hidden'}>
|
||||||
|
<FloatingDocumentationNavigation>
|
||||||
|
<SidebarGroup>
|
||||||
|
<SidebarGroupContent>
|
||||||
|
<SidebarMenu>
|
||||||
|
<Tree pages={pages} level={0} prefix={prefix} />
|
||||||
|
</SidebarMenu>
|
||||||
|
</SidebarGroupContent>
|
||||||
|
</SidebarGroup>
|
||||||
|
</FloatingDocumentationNavigation>
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useEffect, useEffectEvent, useMemo, useState } from 'react';
|
||||||
|
|
||||||
|
import { usePathname } from 'next/navigation';
|
||||||
|
|
||||||
|
import { Menu } from 'lucide-react';
|
||||||
|
|
||||||
|
import { isBrowser } from '@kit/shared/utils';
|
||||||
|
import { Button } from '@kit/ui/button';
|
||||||
|
import { If } from '@kit/ui/if';
|
||||||
|
|
||||||
|
export function FloatingDocumentationNavigation(
|
||||||
|
props: React.PropsWithChildren,
|
||||||
|
) {
|
||||||
|
const activePath = usePathname();
|
||||||
|
|
||||||
|
const body = useMemo(() => {
|
||||||
|
return isBrowser() ? document.body : null;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const [isVisible, setIsVisible] = useState(false);
|
||||||
|
|
||||||
|
const enableScrolling = useEffectEvent(
|
||||||
|
() => body && (body.style.overflowY = ''),
|
||||||
|
);
|
||||||
|
|
||||||
|
const disableScrolling = useEffectEvent(
|
||||||
|
() => body && (body.style.overflowY = 'hidden'),
|
||||||
|
);
|
||||||
|
|
||||||
|
// enable/disable body scrolling when the docs are toggled
|
||||||
|
useEffect(() => {
|
||||||
|
if (isVisible) {
|
||||||
|
disableScrolling();
|
||||||
|
} else {
|
||||||
|
enableScrolling();
|
||||||
|
}
|
||||||
|
}, [isVisible]);
|
||||||
|
|
||||||
|
// hide docs when navigating to another page
|
||||||
|
useEffect(() => {
|
||||||
|
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||||
|
setIsVisible(false);
|
||||||
|
}, [activePath]);
|
||||||
|
|
||||||
|
const onClick = () => {
|
||||||
|
setIsVisible(!isVisible);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<If condition={isVisible}>
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
'fixed top-0 left-0 z-10 h-screen w-full p-4' +
|
||||||
|
' dark:bg-background flex flex-col space-y-4 overflow-auto bg-white'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</div>
|
||||||
|
</If>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
className={'fixed right-5 bottom-5 z-10 h-16 w-16 rounded-full'}
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
<Menu className={'h-8'} />
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { getLocale } from 'next-intl/server';
|
import { SidebarProvider } from '@kit/ui/shadcn-sidebar';
|
||||||
|
|
||||||
import { SidebarProvider } from '@kit/ui/sidebar';
|
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
||||||
|
|
||||||
// local imports
|
// local imports
|
||||||
import { DocsNavigation } from './_components/docs-navigation';
|
import { DocsNavigation } from './_components/docs-navigation';
|
||||||
@@ -8,8 +8,8 @@ import { getDocs } from './_lib/server/docs.loader';
|
|||||||
import { buildDocumentationTree } from './_lib/utils';
|
import { buildDocumentationTree } from './_lib/utils';
|
||||||
|
|
||||||
async function DocsLayout({ children }: React.PropsWithChildren) {
|
async function DocsLayout({ children }: React.PropsWithChildren) {
|
||||||
const locale = await getLocale();
|
const { resolvedLanguage } = await createI18nServerInstance();
|
||||||
const docs = await getDocs(locale);
|
const docs = await getDocs(resolvedLanguage);
|
||||||
const tree = buildDocumentationTree(docs);
|
const tree = buildDocumentationTree(docs);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -1,21 +1,21 @@
|
|||||||
import { getLocale, getTranslations } from 'next-intl/server';
|
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
||||||
|
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||||
|
|
||||||
import { SitePageHeader } from '../_components/site-page-header';
|
import { SitePageHeader } from '../_components/site-page-header';
|
||||||
import { DocsCards } from './_components/docs-cards';
|
import { DocsCards } from './_components/docs-cards';
|
||||||
import { getDocs } from './_lib/server/docs.loader';
|
import { getDocs } from './_lib/server/docs.loader';
|
||||||
|
|
||||||
export const generateMetadata = async () => {
|
export const generateMetadata = async () => {
|
||||||
const t = await getTranslations('marketing');
|
const { t } = await createI18nServerInstance();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: t('documentation'),
|
title: t('marketing:documentation'),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
async function DocsPage() {
|
async function DocsPage() {
|
||||||
const t = await getTranslations('marketing');
|
const { t, resolvedLanguage } = await createI18nServerInstance();
|
||||||
const locale = await getLocale();
|
const items = await getDocs(resolvedLanguage);
|
||||||
const items = await getDocs(locale);
|
|
||||||
|
|
||||||
// Filter out any docs that have a parentId, as these are children of other docs
|
// Filter out any docs that have a parentId, as these are children of other docs
|
||||||
const cards = items.filter((item) => !item.parentId);
|
const cards = items.filter((item) => !item.parentId);
|
||||||
@@ -23,8 +23,8 @@ async function DocsPage() {
|
|||||||
return (
|
return (
|
||||||
<div className={'flex w-full flex-1 flex-col gap-y-6 xl:gap-y-8'}>
|
<div className={'flex w-full flex-1 flex-col gap-y-6 xl:gap-y-8'}>
|
||||||
<SitePageHeader
|
<SitePageHeader
|
||||||
title={t('documentation')}
|
title={t('marketing:documentation')}
|
||||||
subtitle={t('documentationSubtitle')}
|
subtitle={t('marketing:documentationSubtitle')}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className={'relative flex size-full justify-center overflow-y-auto'}>
|
<div className={'relative flex size-full justify-center overflow-y-auto'}>
|
||||||
@@ -34,4 +34,4 @@ async function DocsPage() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default DocsPage;
|
export default withI18n(DocsPage);
|
||||||
@@ -1,30 +1,31 @@
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
import { ArrowRight, ChevronDown } from 'lucide-react';
|
import { ArrowRight, ChevronDown } from 'lucide-react';
|
||||||
import { getTranslations } from 'next-intl/server';
|
|
||||||
|
|
||||||
import { Button } from '@kit/ui/button';
|
import { Button } from '@kit/ui/button';
|
||||||
import { Trans } from '@kit/ui/trans';
|
import { Trans } from '@kit/ui/trans';
|
||||||
|
|
||||||
import { SitePageHeader } from '~/(marketing)/_components/site-page-header';
|
import { SitePageHeader } from '~/(marketing)/_components/site-page-header';
|
||||||
|
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
||||||
|
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||||
|
|
||||||
export const generateMetadata = async () => {
|
export const generateMetadata = async () => {
|
||||||
const t = await getTranslations('marketing');
|
const { t } = await createI18nServerInstance();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: t('faq'),
|
title: t('marketing:faq'),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
async function FAQPage() {
|
async function FAQPage() {
|
||||||
const t = await getTranslations('marketing');
|
const { t } = await createI18nServerInstance();
|
||||||
|
|
||||||
// replace this content with translations
|
// replace this content with translations
|
||||||
const faqItems = [
|
const faqItems = [
|
||||||
{
|
{
|
||||||
// or: t('faq.question1')
|
// or: t('marketing:faq.question1')
|
||||||
question: `Do you offer a free trial?`,
|
question: `Do you offer a free trial?`,
|
||||||
// or: t('faq.answer1')
|
// or: t('marketing:faq.answer1')
|
||||||
answer: `Yes, we offer a 14-day free trial. You can cancel at any time during the trial period and you won't be charged.`,
|
answer: `Yes, we offer a 14-day free trial. You can cancel at any time during the trial period and you won't be charged.`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -73,7 +74,10 @@ async function FAQPage() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<div className={'flex flex-col space-y-4 xl:space-y-8'}>
|
<div className={'flex flex-col space-y-4 xl:space-y-8'}>
|
||||||
<SitePageHeader title={t('faq')} subtitle={t('faqSubtitle')} />
|
<SitePageHeader
|
||||||
|
title={t('marketing:faq')}
|
||||||
|
subtitle={t('marketing:faqSubtitle')}
|
||||||
|
/>
|
||||||
|
|
||||||
<div className={'container flex flex-col items-center space-y-8 pb-16'}>
|
<div className={'container flex flex-col items-center space-y-8 pb-16'}>
|
||||||
<div className="divide-border flex w-full max-w-xl flex-col divide-y divide-dashed rounded-md border">
|
<div className="divide-border flex w-full max-w-xl flex-col divide-y divide-dashed rounded-md border">
|
||||||
@@ -83,16 +87,14 @@ async function FAQPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Button
|
<Button asChild variant={'outline'}>
|
||||||
nativeButton={false}
|
<Link href={'/contact'}>
|
||||||
render={<Link href={'/contact'} />}
|
<span>
|
||||||
variant={'link'}
|
<Trans i18nKey={'marketing:contactFaq'} />
|
||||||
>
|
</span>
|
||||||
<span>
|
|
||||||
<Trans i18nKey={'marketing.contactFaq'} />
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<ArrowRight className={'ml-2 w-4'} />
|
<ArrowRight className={'ml-2 w-4'} />
|
||||||
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -101,7 +103,7 @@ async function FAQPage() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default FAQPage;
|
export default withI18n(FAQPage);
|
||||||
|
|
||||||
function FaqItem({
|
function FaqItem({
|
||||||
item,
|
item,
|
||||||
@@ -3,6 +3,7 @@ import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
|||||||
|
|
||||||
import { SiteFooter } from '~/(marketing)/_components/site-footer';
|
import { SiteFooter } from '~/(marketing)/_components/site-footer';
|
||||||
import { SiteHeader } from '~/(marketing)/_components/site-header';
|
import { SiteHeader } from '~/(marketing)/_components/site-header';
|
||||||
|
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||||
|
|
||||||
async function SiteLayout(props: React.PropsWithChildren) {
|
async function SiteLayout(props: React.PropsWithChildren) {
|
||||||
const client = getSupabaseServerClient();
|
const client = getSupabaseServerClient();
|
||||||
@@ -19,4 +20,4 @@ async function SiteLayout(props: React.PropsWithChildren) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SiteLayout;
|
export default withI18n(SiteLayout);
|
||||||
@@ -20,6 +20,7 @@ import { Trans } from '@kit/ui/trans';
|
|||||||
|
|
||||||
import billingConfig from '~/config/billing.config';
|
import billingConfig from '~/config/billing.config';
|
||||||
import pathsConfig from '~/config/paths.config';
|
import pathsConfig from '~/config/paths.config';
|
||||||
|
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||||
|
|
||||||
function Home() {
|
function Home() {
|
||||||
return (
|
return (
|
||||||
@@ -29,13 +30,11 @@ function Home() {
|
|||||||
pill={
|
pill={
|
||||||
<Pill label={'New'}>
|
<Pill label={'New'}>
|
||||||
<span>The SaaS Starter Kit for ambitious developers</span>
|
<span>The SaaS Starter Kit for ambitious developers</span>
|
||||||
<PillActionButton
|
<PillActionButton asChild>
|
||||||
render={
|
<Link href={'/auth/sign-up'}>
|
||||||
<Link href={'/auth/sign-up'}>
|
<ArrowRightIcon className={'h-4 w-4'} />
|
||||||
<ArrowRightIcon className={'h-4 w-4'} />
|
</Link>
|
||||||
</Link>
|
</PillActionButton>
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Pill>
|
</Pill>
|
||||||
}
|
}
|
||||||
title={
|
title={
|
||||||
@@ -171,7 +170,7 @@ function Home() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Home;
|
export default withI18n(Home);
|
||||||
|
|
||||||
function MainCallToActionButton() {
|
function MainCallToActionButton() {
|
||||||
return (
|
return (
|
||||||
@@ -180,7 +179,7 @@ function MainCallToActionButton() {
|
|||||||
<Link href={'/auth/sign-up'}>
|
<Link href={'/auth/sign-up'}>
|
||||||
<span className={'flex items-center space-x-0.5'}>
|
<span className={'flex items-center space-x-0.5'}>
|
||||||
<span>
|
<span>
|
||||||
<Trans i18nKey={'common.getStarted'} />
|
<Trans i18nKey={'common:getStarted'} />
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<ArrowRightIcon
|
<ArrowRightIcon
|
||||||
@@ -195,7 +194,7 @@ function MainCallToActionButton() {
|
|||||||
|
|
||||||
<CtaButton variant={'link'} className="h-10 text-sm">
|
<CtaButton variant={'link'} className="h-10 text-sm">
|
||||||
<Link href={'/pricing'}>
|
<Link href={'/pricing'}>
|
||||||
<Trans i18nKey={'common.pricing'} />
|
<Trans i18nKey={'common:pricing'} />
|
||||||
</Link>
|
</Link>
|
||||||
</CtaButton>
|
</CtaButton>
|
||||||
</div>
|
</div>
|
||||||
@@ -1,16 +1,16 @@
|
|||||||
import { getTranslations } from 'next-intl/server';
|
|
||||||
|
|
||||||
import { PricingTable } from '@kit/billing-gateway/marketing';
|
import { PricingTable } from '@kit/billing-gateway/marketing';
|
||||||
|
|
||||||
import { SitePageHeader } from '~/(marketing)/_components/site-page-header';
|
import { SitePageHeader } from '~/(marketing)/_components/site-page-header';
|
||||||
import billingConfig from '~/config/billing.config';
|
import billingConfig from '~/config/billing.config';
|
||||||
import pathsConfig from '~/config/paths.config';
|
import pathsConfig from '~/config/paths.config';
|
||||||
|
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
||||||
|
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||||
|
|
||||||
export const generateMetadata = async () => {
|
export const generateMetadata = async () => {
|
||||||
const t = await getTranslations('marketing');
|
const { t } = await createI18nServerInstance();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: t('pricing'),
|
title: t('marketing:pricing'),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -20,11 +20,14 @@ const paths = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
async function PricingPage() {
|
async function PricingPage() {
|
||||||
const t = await getTranslations('marketing');
|
const { t } = await createI18nServerInstance();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'flex flex-col space-y-8'}>
|
<div className={'flex flex-col space-y-8'}>
|
||||||
<SitePageHeader title={t('pricing')} subtitle={t('pricingSubtitle')} />
|
<SitePageHeader
|
||||||
|
title={t('marketing:pricing')}
|
||||||
|
subtitle={t('marketing:pricingSubtitle')}
|
||||||
|
/>
|
||||||
|
|
||||||
<div className={'container mx-auto pb-8 xl:pb-16'}>
|
<div className={'container mx-auto pb-8 xl:pb-16'}>
|
||||||
<PricingTable paths={paths} config={billingConfig} />
|
<PricingTable paths={paths} config={billingConfig} />
|
||||||
@@ -33,4 +36,4 @@ async function PricingPage() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default PricingPage;
|
export default withI18n(PricingPage);
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
import { getTranslations } from 'next-intl/server';
|
|
||||||
|
|
||||||
import { SitePageHeader } from '~/(marketing)/_components/site-page-header';
|
|
||||||
|
|
||||||
export async function generateMetadata() {
|
|
||||||
const t = await getTranslations('marketing');
|
|
||||||
|
|
||||||
return {
|
|
||||||
title: t('cookiePolicy'),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async function CookiePolicyPage() {
|
|
||||||
const t = await getTranslations('marketing');
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<SitePageHeader
|
|
||||||
title={t(`marketing.cookiePolicy`)}
|
|
||||||
subtitle={t(`marketing.cookiePolicyDescription`)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className={'container mx-auto py-8'}>
|
|
||||||
<div>Your terms of service content here</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default CookiePolicyPage;
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user