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.
|
||||
|
||||
**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.
|
||||
**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.
|
||||
|
||||
Create in route's `_lib/server/` directory:
|
||||
|
||||
|
||||
@@ -19,8 +19,8 @@ export class AuthPageObject {
|
||||
}
|
||||
|
||||
async signOut() {
|
||||
await this.page.click('[data-test="workspace-dropdown-trigger"]');
|
||||
await this.page.click('[data-test="workspace-sign-out"]');
|
||||
await this.page.click('[data-test="account-dropdown-trigger"]');
|
||||
await this.page.click('[data-test="account-dropdown-sign-out"]');
|
||||
}
|
||||
|
||||
async bootstrapUser(params: { email: string; password: string; name: string }) {
|
||||
@@ -47,19 +47,9 @@ export class AuthPageObject {
|
||||
## Common Selectors
|
||||
|
||||
```typescript
|
||||
// Workspace dropdown (sidebar header - combined account switcher + user menu)
|
||||
'[data-test="workspace-dropdown-trigger"]' // Opens the dropdown
|
||||
'[data-test="workspace-switch-submenu"]' // Sub-trigger for workspace switching
|
||||
'[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"]');
|
||||
// Account dropdown
|
||||
'[data-test="account-dropdown-trigger"]'
|
||||
'[data-test="account-dropdown-sign-out"]'
|
||||
|
||||
// Navigation
|
||||
'[data-test="sidebar-menu"]'
|
||||
|
||||
@@ -126,7 +126,7 @@ export function CreateEntityForm() {
|
||||
reValidateMode: 'onChange',
|
||||
});
|
||||
|
||||
const onSubmit = (data: z.output<typeof CreateEntitySchema>) => {
|
||||
const onSubmit = (data: z.infer<typeof CreateEntitySchema>) => {
|
||||
setError(false);
|
||||
|
||||
startTransition(async () => {
|
||||
@@ -147,7 +147,7 @@ export function CreateEntityForm() {
|
||||
<If condition={error}>
|
||||
<Alert variant="destructive">
|
||||
<AlertDescription>
|
||||
<Trans i18nKey="common.errors.generic" />
|
||||
<Trans i18nKey="common:errors.generic" />
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</If>
|
||||
@@ -177,9 +177,9 @@ export function CreateEntityForm() {
|
||||
data-test="submit-entity-button"
|
||||
>
|
||||
{pending ? (
|
||||
<Trans i18nKey="common.creating" />
|
||||
<Trans i18nKey="common:creating" />
|
||||
) : (
|
||||
<Trans i18nKey="common.create" />
|
||||
<Trans i18nKey="common:create" />
|
||||
)}
|
||||
</Button>
|
||||
</Form>
|
||||
|
||||
@@ -145,7 +145,7 @@ import { toast } from '@kit/ui/sonner';
|
||||
<If condition={error}>
|
||||
<Alert variant="destructive">
|
||||
<AlertDescription>
|
||||
<Trans i18nKey="common.errors.generic" />
|
||||
<Trans i18nKey="common:errors.generic" />
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</If>
|
||||
@@ -160,9 +160,9 @@ import { toast } from '@kit/ui/sonner';
|
||||
data-test="submit-button"
|
||||
>
|
||||
{pending ? (
|
||||
<Trans i18nKey="common.submitting" />
|
||||
<Trans i18nKey="common:submitting" />
|
||||
) : (
|
||||
<Trans i18nKey="common.submit" />
|
||||
<Trans i18nKey="common:submit" />
|
||||
)}
|
||||
</Button>
|
||||
```
|
||||
@@ -199,7 +199,7 @@ export function MyForm() {
|
||||
mode: 'onChange',
|
||||
});
|
||||
|
||||
const onSubmit = (data: z.output<typeof MySchema>) => {
|
||||
const onSubmit = (data: z.infer<typeof MySchema>) => {
|
||||
setError(false);
|
||||
|
||||
startTransition(async () => {
|
||||
@@ -220,7 +220,7 @@ export function MyForm() {
|
||||
<If condition={error}>
|
||||
<Alert variant="destructive">
|
||||
<AlertDescription>
|
||||
<Trans i18nKey="common.errors.generic" />
|
||||
<Trans i18nKey="common:errors.generic" />
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</If>
|
||||
|
||||
@@ -17,21 +17,19 @@ Create validation schema in `_lib/schemas/`:
|
||||
|
||||
```typescript
|
||||
// _lib/schemas/feature.schema.ts
|
||||
import * as z from 'zod';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const CreateFeatureSchema = z.object({
|
||||
name: z.string().min(1, 'Name is required'),
|
||||
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
|
||||
|
||||
**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.
|
||||
**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.
|
||||
|
||||
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 (
|
||||
pass a mock client) and reusable (any interface can supply its own client).
|
||||
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).
|
||||
|
||||
### 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
|
||||
logic lives here.
|
||||
The action is a **thin adapter** — it resolves dependencies (client, logger) and delegates to the service. No business logic lives here.
|
||||
|
||||
Create action in `_lib/server/server-actions.ts`:
|
||||
|
||||
@@ -111,18 +107,13 @@ export const createFeatureAction = enhanceAction(
|
||||
|
||||
## Key Patterns
|
||||
|
||||
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.
|
||||
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.
|
||||
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.
|
||||
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
|
||||
4. **Logging** - Always log before and after operations
|
||||
5. **Revalidation** - Use `revalidatePath` after mutations
|
||||
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
|
||||
running infrastructure
|
||||
7. **Testable in isolation** - Because services accept their dependencies, you can test them with a mock client and no running infrastructure
|
||||
|
||||
## File Structure
|
||||
|
||||
|
||||
@@ -29,8 +29,8 @@ export const myAction = enhanceAction(
|
||||
### Handler Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|--------------------|------------------------------------|
|
||||
| `data` | `z.output<Schema>` | Validated input data |
|
||||
|-----------|------|-------------|
|
||||
| `data` | `z.infer<Schema>` | Validated input data |
|
||||
| `user` | `User` | Authenticated user (if auth: true) |
|
||||
|
||||
## enhanceRouteHandler API
|
||||
@@ -69,7 +69,7 @@ export const GET = enhanceRouteHandler(
|
||||
## Common Zod Patterns
|
||||
|
||||
```typescript
|
||||
import * as z from 'zod';
|
||||
import { z } from 'zod';
|
||||
|
||||
// Basic schema
|
||||
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
|
||||
|
||||
**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.
|
||||
**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.
|
||||
|
||||
## Workflow
|
||||
|
||||
@@ -23,7 +21,7 @@ Start with the input/output types. These are plain TypeScript — no framework t
|
||||
|
||||
```typescript
|
||||
// _lib/schemas/project.schema.ts
|
||||
import * as z from 'zod';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const CreateProjectSchema = z.object({
|
||||
name: z.string().min(1),
|
||||
@@ -42,8 +40,7 @@ export interface Project {
|
||||
|
||||
### Step 2: Build the Service
|
||||
|
||||
The service receives all dependencies through its constructor. It never imports framework-specific modules (
|
||||
`getSupabaseServerClient`, `getLogger`, `revalidatePath`, etc.).
|
||||
The service receives all dependencies through its constructor. It never imports framework-specific modules (`getSupabaseServerClient`, `getLogger`, `revalidatePath`, etc.).
|
||||
|
||||
```typescript
|
||||
// _lib/server/project.service.ts
|
||||
@@ -98,8 +95,7 @@ class ProjectService {
|
||||
|
||||
### Step 3: Write Thin Adapters
|
||||
|
||||
Each interface is a thin adapter — it resolves dependencies, calls the service, and handles interface-specific
|
||||
concerns (revalidation, redirects, MCP formatting, CLI output).
|
||||
Each interface is a thin adapter — it resolves dependencies, calls the service, and handles interface-specific concerns (revalidation, redirects, MCP formatting, CLI output).
|
||||
|
||||
**Server Action adapter:**
|
||||
|
||||
@@ -238,25 +234,20 @@ describe('ProjectService', () => {
|
||||
|
||||
## Rules
|
||||
|
||||
1. **Services are pure functions over data.** Plain objects/primitives in, plain objects/primitives out. No `Request`/
|
||||
`Response`, no MCP context, no `FormData`.
|
||||
1. **Services are pure functions over data.** Plain objects/primitives in, plain objects/primitives out. No `Request`/`Response`, no MCP context, no `FormData`.
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
4. **One service, many callers.** If two interfaces do the same thing, they call the same service function. Duplicating
|
||||
logic is a violation.
|
||||
4. **One service, many callers.** If two interfaces do the same thing, they call the same service function. Duplicating 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,
|
||||
refactor until you don't.
|
||||
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.
|
||||
|
||||
## What Goes Where
|
||||
|
||||
| Concern | Location | Example |
|
||||
|------------------------|-------------------------------------------|-------------------------------------------|
|
||||
|---------|----------|---------|
|
||||
| Input validation (Zod) | `_lib/schemas/` | `CreateProjectSchema` |
|
||||
| Business logic | `_lib/server/*.service.ts` | `ProjectService.create()` |
|
||||
| Auth check | Adapter (`enhanceAction({ auth: true })`) | Server action wrapper |
|
||||
@@ -314,5 +305,4 @@ const result = await client.from('projects').insert(...).select().single();
|
||||
|
||||
## Reference
|
||||
|
||||
See `[Examples](examples.md)` for more patterns including services with multiple dependencies, services that compose
|
||||
other services, and testing strategies.
|
||||
See `[Examples](examples.md)` for more patterns including services with multiple dependencies, services that compose other services, and testing strategies.
|
||||
|
||||
2
.github/workflows/workflow.yml
vendored
2
.github/workflows/workflow.yml
vendored
@@ -3,7 +3,7 @@ on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
branches: [ main, v3 ]
|
||||
branches: [ main ]
|
||||
jobs:
|
||||
typescript:
|
||||
name: ʦ TypeScript
|
||||
|
||||
@@ -556,8 +556,8 @@ function MyFeaturePage() {
|
||||
return (
|
||||
<>
|
||||
<MyFeatureHeader
|
||||
title={<Trans i18nKey={'common.routes.myFeature'} />}
|
||||
description={<Trans i18nKey={'common.myFeatureDescription'} />}
|
||||
title={<Trans i18nKey={'common:routes.myFeature'} />}
|
||||
description={<Trans i18nKey={'common:myFeatureDescription'} />}
|
||||
/>
|
||||
|
||||
<PageBody>
|
||||
@@ -830,7 +830,7 @@ import { ProfileAvatar } from '@kit/ui/profile-avatar';
|
||||
## Core Shadcn UI Components
|
||||
|
||||
| Component | Description | Import Path |
|
||||
|------------------|-------------------------------------------|-------------------------------------------------------------------------------------------------|
|
||||
|-----------|-------------|-------------|
|
||||
| `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) |
|
||||
| `Alert` | Status/notification messages | `@kit/ui/alert` [alert.tsx](mdc:packages/ui/src/shadcn/alert.tsx) |
|
||||
@@ -856,7 +856,7 @@ import { ProfileAvatar } from '@kit/ui/profile-avatar';
|
||||
| `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) |
|
||||
| `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) |
|
||||
| `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) |
|
||||
@@ -920,7 +920,7 @@ Zod schemas should be defined in the `schema` folder and exported, so we can reu
|
||||
|
||||
```tsx
|
||||
// _lib/schema/create-note.schema.ts
|
||||
import * as z from 'zod';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const CreateNoteSchema = z.object({
|
||||
title: z.string().min(1),
|
||||
@@ -935,7 +935,7 @@ Server Actions [server-actions.mdc](mdc:.cursor/rules/server-actions.mdc) can he
|
||||
```tsx
|
||||
'use server';
|
||||
|
||||
import * as z from 'zod';
|
||||
import { z } from 'zod';
|
||||
import { enhanceAction } from '@kit/next/actions';
|
||||
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 { useForm } from 'react-hook-form';
|
||||
import * as z from 'zod';
|
||||
import { z } from 'zod';
|
||||
import { Textarea } from '@kit/ui/textarea';
|
||||
import { Input } from '@kit/ui/input';
|
||||
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
|
||||
'use server';
|
||||
|
||||
import * as z from 'zod';
|
||||
import { z } from 'zod';
|
||||
import { enhanceAction } from '@kit/next/actions';
|
||||
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)
|
||||
|
||||
```tsx
|
||||
import * as z from 'zod';
|
||||
import { z } from 'zod';
|
||||
import { enhanceRouteHandler } from '@kit/next/routes';
|
||||
import { NextResponse } from 'next/server';
|
||||
|
||||
|
||||
1
.npmrc
1
.npmrc
@@ -3,6 +3,7 @@ dedupe-peer-dependents=true
|
||||
use-lockfile-v6=true
|
||||
resolution-mode=highest
|
||||
package-manager-strict=false
|
||||
public-hoist-pattern[]=*i18next*
|
||||
public-hoist-pattern[]=*eslint*
|
||||
public-hoist-pattern[]=*prettier*
|
||||
public-hoist-pattern[]=*require-in-the-middle*
|
||||
|
||||
@@ -40,8 +40,8 @@ pnpm format:fix # Format code
|
||||
## Key Patterns (Quick Reference)
|
||||
|
||||
| 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` |
|
||||
| Server Client | `getSupabaseServerClient` from `@kit/supabase/server-client` | `packages/supabase/AGENTS.md` |
|
||||
| UI Components | `@kit/ui/{component}` | `packages/ui/AGENTS.md` |
|
||||
|
||||
@@ -120,7 +120,9 @@ export function AlertDialogStory() {
|
||||
|
||||
const generateCode = () => {
|
||||
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 += ` <AlertDialogHeader>\n`;
|
||||
|
||||
@@ -177,14 +179,11 @@ export function AlertDialogStory() {
|
||||
const renderPreview = () => {
|
||||
return (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger
|
||||
render={
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant={controls.triggerVariant}>
|
||||
{controls.triggerText}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
{controls.withIcon ? (
|
||||
@@ -342,11 +341,11 @@ export function AlertDialogStory() {
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger
|
||||
render={<Button variant="destructive" size="sm" />}
|
||||
>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="destructive" size="sm">
|
||||
<Trash2 className="mr-2 h-4 w-4" />
|
||||
Delete Item
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
@@ -371,9 +370,11 @@ export function AlertDialogStory() {
|
||||
</AlertDialog>
|
||||
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger render={<Button variant="outline" />}>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="outline">
|
||||
<LogOut className="mr-2 h-4 w-4" />
|
||||
Sign Out
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
@@ -396,9 +397,11 @@ export function AlertDialogStory() {
|
||||
</AlertDialog>
|
||||
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger render={<Button variant="outline" />}>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="outline">
|
||||
<UserX className="mr-2 h-4 w-4" />
|
||||
Remove User
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
@@ -435,9 +438,11 @@ export function AlertDialogStory() {
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger render={<Button variant="outline" />}>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="outline">
|
||||
<Archive className="mr-2 h-4 w-4" />
|
||||
Archive Project
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
@@ -460,9 +465,11 @@ export function AlertDialogStory() {
|
||||
</AlertDialog>
|
||||
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger render={<Button />}>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button>
|
||||
<Download className="mr-2 h-4 w-4" />
|
||||
Export Data
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
@@ -486,9 +493,11 @@ export function AlertDialogStory() {
|
||||
</AlertDialog>
|
||||
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger render={<Button variant="outline" />}>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="outline">
|
||||
<RefreshCw className="mr-2 h-4 w-4" />
|
||||
Reset Settings
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
@@ -526,11 +535,11 @@ export function AlertDialogStory() {
|
||||
<div className="space-y-3">
|
||||
<h4 className="text-sm font-semibold">Error/Destructive</h4>
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger
|
||||
render={<Button variant="destructive" size="sm" />}
|
||||
>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="destructive" size="sm">
|
||||
<Trash2 className="mr-2 h-4 w-4" />
|
||||
Delete Forever
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
@@ -558,11 +567,11 @@ export function AlertDialogStory() {
|
||||
<div className="space-y-3">
|
||||
<h4 className="text-sm font-semibold">Warning</h4>
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger
|
||||
render={<Button variant="outline" size="sm" />}
|
||||
>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="outline" size="sm">
|
||||
<AlertTriangle className="mr-2 h-4 w-4" />
|
||||
Unsaved Changes
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
@@ -588,11 +597,11 @@ export function AlertDialogStory() {
|
||||
<div className="space-y-3">
|
||||
<h4 className="text-sm font-semibold">Info</h4>
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger
|
||||
render={<Button variant="outline" size="sm" />}
|
||||
>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="outline" size="sm">
|
||||
<Share className="mr-2 h-4 w-4" />
|
||||
Share Publicly
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
@@ -618,9 +627,11 @@ export function AlertDialogStory() {
|
||||
<div className="space-y-3">
|
||||
<h4 className="text-sm font-semibold">Success</h4>
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger render={<Button size="sm" />}>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button size="sm">
|
||||
<Download className="mr-2 h-4 w-4" />
|
||||
Complete Setup
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
|
||||
@@ -33,6 +33,7 @@ interface ButtonControls {
|
||||
loading: boolean;
|
||||
withIcon: boolean;
|
||||
fullWidth: boolean;
|
||||
asChild: boolean;
|
||||
}
|
||||
|
||||
const variantOptions = [
|
||||
@@ -67,6 +68,7 @@ export function ButtonStory() {
|
||||
loading: false,
|
||||
withIcon: false,
|
||||
fullWidth: false,
|
||||
asChild: false,
|
||||
});
|
||||
|
||||
const generateCode = () => {
|
||||
@@ -75,12 +77,14 @@ export function ButtonStory() {
|
||||
variant: controls.variant,
|
||||
size: controls.size,
|
||||
disabled: controls.disabled,
|
||||
asChild: controls.asChild,
|
||||
className: controls.fullWidth ? 'w-full' : '',
|
||||
},
|
||||
{
|
||||
variant: 'default',
|
||||
size: 'default',
|
||||
disabled: false,
|
||||
asChild: false,
|
||||
className: '',
|
||||
},
|
||||
);
|
||||
@@ -190,6 +194,15 @@ export function ButtonStory() {
|
||||
onCheckedChange={(checked) => updateControl('fullWidth', checked)}
|
||||
/>
|
||||
</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>
|
||||
<CardContent className="flex justify-center pt-6">
|
||||
<Popover>
|
||||
<PopoverTrigger
|
||||
render={<Button variant="outline" className="justify-start" />}
|
||||
>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="outline" className="justify-start">
|
||||
<CalendarIcon className="mr-2 h-4 w-4" />
|
||||
Pick a date
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0" align="start">
|
||||
<Calendar
|
||||
|
||||
@@ -320,12 +320,10 @@ export function CardButtonStory() {
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr className="border-b">
|
||||
<td className="p-3 font-mono text-sm">render</td>
|
||||
<td className="p-3 font-mono text-sm">
|
||||
React.ReactElement
|
||||
</td>
|
||||
<td className="p-3 font-mono text-sm">-</td>
|
||||
<td className="p-3">Compose with a custom element</td>
|
||||
<td className="p-3 font-mono text-sm">asChild</td>
|
||||
<td className="p-3 font-mono text-sm">boolean</td>
|
||||
<td className="p-3 font-mono text-sm">false</td>
|
||||
<td className="p-3">Render as child element</td>
|
||||
</tr>
|
||||
<tr className="border-b">
|
||||
<td className="p-3 font-mono text-sm">className</td>
|
||||
|
||||
@@ -139,8 +139,8 @@ export function DialogStory() {
|
||||
});
|
||||
|
||||
let code = `<Dialog>\n`;
|
||||
code += ` <DialogTrigger render={<Button variant="${controls.triggerVariant}" />}>\n`;
|
||||
code += ` ${controls.triggerText}\n`;
|
||||
code += ` <DialogTrigger asChild>\n`;
|
||||
code += ` <Button variant="${controls.triggerVariant}">${controls.triggerText}</Button>\n`;
|
||||
code += ` </DialogTrigger>\n`;
|
||||
code += ` <DialogContent${contentPropsString}>\n`;
|
||||
code += ` <DialogHeader>\n`;
|
||||
@@ -182,8 +182,8 @@ export function DialogStory() {
|
||||
|
||||
if (controls.withFooter) {
|
||||
code += ` <DialogFooter>\n`;
|
||||
code += ` <DialogClose render={<Button variant="outline" />}>\n`;
|
||||
code += ` Cancel\n`;
|
||||
code += ` <DialogClose asChild>\n`;
|
||||
code += ` <Button variant="outline">Cancel</Button>\n`;
|
||||
code += ` </DialogClose>\n`;
|
||||
code += ` <Button>Save Changes</Button>\n`;
|
||||
code += ` </DialogFooter>\n`;
|
||||
@@ -198,8 +198,10 @@ export function DialogStory() {
|
||||
const renderPreview = () => {
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger render={<Button variant={controls.triggerVariant} />}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant={controls.triggerVariant}>
|
||||
{controls.triggerText}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent
|
||||
className={cn(
|
||||
@@ -269,8 +271,8 @@ export function DialogStory() {
|
||||
|
||||
{controls.withFooter && (
|
||||
<DialogFooter>
|
||||
<DialogClose render={<Button variant="outline" />}>
|
||||
Cancel
|
||||
<DialogClose asChild>
|
||||
<Button variant="outline">Cancel</Button>
|
||||
</DialogClose>
|
||||
<Button>Save Changes</Button>
|
||||
</DialogFooter>
|
||||
@@ -389,9 +391,11 @@ export function DialogStory() {
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<Dialog>
|
||||
<DialogTrigger render={<Button variant="outline" />}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline">
|
||||
<Info className="mr-2 h-4 w-4" />
|
||||
Info Dialog
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
@@ -408,15 +412,19 @@ export function DialogStory() {
|
||||
</p>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<DialogClose render={<Button />}>Got it</DialogClose>
|
||||
<DialogClose asChild>
|
||||
<Button>Got it</Button>
|
||||
</DialogClose>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog>
|
||||
<DialogTrigger render={<Button />}>
|
||||
<DialogTrigger asChild>
|
||||
<Button>
|
||||
<Edit className="mr-2 h-4 w-4" />
|
||||
Edit Profile
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
@@ -448,8 +456,8 @@ export function DialogStory() {
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<DialogClose render={<Button variant="outline" />}>
|
||||
Cancel
|
||||
<DialogClose asChild>
|
||||
<Button variant="outline">Cancel</Button>
|
||||
</DialogClose>
|
||||
<Button>Save Changes</Button>
|
||||
</DialogFooter>
|
||||
@@ -457,9 +465,11 @@ export function DialogStory() {
|
||||
</Dialog>
|
||||
|
||||
<Dialog>
|
||||
<DialogTrigger render={<Button variant="secondary" />}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="secondary">
|
||||
<Settings className="mr-2 h-4 w-4" />
|
||||
Settings
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
@@ -489,8 +499,8 @@ export function DialogStory() {
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<DialogClose render={<Button variant="outline" />}>
|
||||
Cancel
|
||||
<DialogClose asChild>
|
||||
<Button variant="outline">Cancel</Button>
|
||||
</DialogClose>
|
||||
<Button>Save</Button>
|
||||
</DialogFooter>
|
||||
@@ -508,8 +518,10 @@ export function DialogStory() {
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<Dialog>
|
||||
<DialogTrigger render={<Button variant="outline" size="sm" />}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline" size="sm">
|
||||
Small Dialog
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-md">
|
||||
<DialogHeader>
|
||||
@@ -524,14 +536,16 @@ export function DialogStory() {
|
||||
</p>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<DialogClose render={<Button />}>Close</DialogClose>
|
||||
<DialogClose asChild>
|
||||
<Button>Close</Button>
|
||||
</DialogClose>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog>
|
||||
<DialogTrigger render={<Button variant="outline" />}>
|
||||
Large Dialog
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline">Large Dialog</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogHeader>
|
||||
@@ -557,8 +571,8 @@ export function DialogStory() {
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<DialogClose render={<Button variant="outline" />}>
|
||||
Cancel
|
||||
<DialogClose asChild>
|
||||
<Button variant="outline">Cancel</Button>
|
||||
</DialogClose>
|
||||
<Button>Save</Button>
|
||||
</DialogFooter>
|
||||
@@ -576,9 +590,11 @@ export function DialogStory() {
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<Dialog>
|
||||
<DialogTrigger render={<Button variant="outline" />}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline">
|
||||
<Image className="mr-2 h-4 w-4" />
|
||||
Image Gallery
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogHeader>
|
||||
@@ -611,9 +627,11 @@ export function DialogStory() {
|
||||
</Dialog>
|
||||
|
||||
<Dialog>
|
||||
<DialogTrigger render={<Button variant="outline" />}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline">
|
||||
<MessageSquare className="mr-2 h-4 w-4" />
|
||||
Feedback
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
@@ -650,8 +668,8 @@ export function DialogStory() {
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<DialogClose render={<Button variant="outline" />}>
|
||||
Cancel
|
||||
<DialogClose asChild>
|
||||
<Button variant="outline">Cancel</Button>
|
||||
</DialogClose>
|
||||
<Button>
|
||||
<MessageSquare className="mr-2 h-4 w-4" />
|
||||
@@ -718,8 +736,8 @@ export function DialogStory() {
|
||||
<div>
|
||||
<h4 className="mb-3 text-lg font-semibold">DialogTrigger</h4>
|
||||
<p className="text-muted-foreground mb-3 text-sm">
|
||||
The element that opens the dialog. Use the render prop to compose
|
||||
with a custom element.
|
||||
The element that opens the dialog. Use asChild prop to render as
|
||||
child element.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
import { Code2, FileText, Search } from 'lucide-react';
|
||||
|
||||
@@ -35,7 +35,6 @@ export function DocsSidebar({
|
||||
selectedCategory,
|
||||
}: DocsSidebarProps) {
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const searchParams = useSearchParams();
|
||||
const router = useRouter();
|
||||
|
||||
const filteredComponents = COMPONENTS_REGISTRY.filter((c) =>
|
||||
@@ -51,21 +50,21 @@ export function DocsSidebar({
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
const onCategorySelect = (category: string | null) => {
|
||||
const sp = new URLSearchParams(searchParams);
|
||||
sp.set('category', category || '');
|
||||
router.push(`/components?${sp.toString()}`);
|
||||
const searchParams = new URLSearchParams(window.location.search);
|
||||
searchParams.set('category', category || '');
|
||||
router.push(`/components?${searchParams.toString()}`);
|
||||
};
|
||||
|
||||
const onComponentSelect = (component: ComponentInfo) => {
|
||||
const sp = new URLSearchParams(searchParams);
|
||||
sp.set('component', component.name);
|
||||
router.push(`/components?${sp.toString()}`);
|
||||
const searchParams = new URLSearchParams(window.location.search);
|
||||
searchParams.set('component', component.name);
|
||||
router.push(`/components?${searchParams.toString()}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-muted/30 flex h-screen w-80 flex-col overflow-hidden border-r">
|
||||
{/* 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">
|
||||
<Code2 className="text-primary h-6 w-6" />
|
||||
|
||||
@@ -78,14 +77,13 @@ export function DocsSidebar({
|
||||
</div>
|
||||
|
||||
{/* 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 */}
|
||||
<div className="space-y-2">
|
||||
<Select
|
||||
defaultValue={selectedCategory || 'all'}
|
||||
value={selectedCategory || 'all'}
|
||||
onValueChange={(value) => {
|
||||
const category = value === 'all' ? null : value;
|
||||
|
||||
onCategorySelect(category);
|
||||
|
||||
// Select first component in the filtered results
|
||||
@@ -98,12 +96,8 @@ export function DocsSidebar({
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue>
|
||||
{(category) => {
|
||||
return category === 'all' ? 'All Categories' : category;
|
||||
}}
|
||||
</SelectValue>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder={'Select a category'} />
|
||||
</SelectTrigger>
|
||||
|
||||
<SelectContent>
|
||||
@@ -160,7 +154,7 @@ export function DocsSidebar({
|
||||
|
||||
{/* Components List - Scrollable */}
|
||||
<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">
|
||||
<FileText className="h-4 w-4" />
|
||||
Components
|
||||
|
||||
@@ -101,18 +101,13 @@ const examples = [
|
||||
return (
|
||||
<div className="flex min-h-32 items-center justify-center">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
render={
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="relative h-8 w-8 rounded-full"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" className="relative h-8 w-8 rounded-full">
|
||||
<Avatar className="h-8 w-8">
|
||||
<AvatarImage src="/avatars/01.png" alt="@username" />
|
||||
<AvatarFallback>JD</AvatarFallback>
|
||||
</Avatar>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-56" align="end" forceMount>
|
||||
<DropdownMenuLabel className="font-normal">
|
||||
@@ -190,11 +185,11 @@ const examples = [
|
||||
</div>
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
render={<Button variant="ghost" className="h-8 w-8 p-0" />}
|
||||
>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" className="h-8 w-8 p-0">
|
||||
<span className="sr-only">Open menu</span>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-48">
|
||||
<DropdownMenuItem onClick={() => setSelectedAction('open')}>
|
||||
@@ -280,9 +275,11 @@ const examples = [
|
||||
return (
|
||||
<div className="flex min-h-48 items-center justify-center">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger render={<Button variant="outline" />}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline">
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
Create New
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-56">
|
||||
<DropdownMenuLabel>Create Content</DropdownMenuLabel>
|
||||
@@ -396,11 +393,11 @@ const examples = [
|
||||
<span className="text-sm">Appearance & Layout</span>
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
render={<Button variant="outline" size="sm" />}
|
||||
>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" size="sm">
|
||||
<Settings className="mr-2 h-4 w-4" />
|
||||
Configure
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-64" align="end">
|
||||
<DropdownMenuLabel>View Options</DropdownMenuLabel>
|
||||
@@ -550,10 +547,10 @@ const examples = [
|
||||
</div>
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
render={<Button variant="ghost" className="h-8 w-8 p-0" />}
|
||||
>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" className="h-8 w-8 p-0">
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-48">
|
||||
<DropdownMenuItem
|
||||
@@ -866,7 +863,7 @@ export default function DropdownMenuStory() {
|
||||
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}`;
|
||||
};
|
||||
@@ -974,8 +971,8 @@ export default function DropdownMenuStory() {
|
||||
const previewContent = (
|
||||
<div className="flex justify-center p-6">
|
||||
<DropdownMenu modal={controls.modal}>
|
||||
<DropdownMenuTrigger render={<Button variant="outline" />}>
|
||||
Open Menu
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline">Open Menu</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
side={controls.side}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useState } from 'react';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import * as z from 'zod';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { Badge } from '@kit/ui/badge';
|
||||
import { Button } from '@kit/ui/button';
|
||||
@@ -119,7 +119,7 @@ export default function FormStory() {
|
||||
const formImport = generateImportStatement(formComponents, '@kit/ui/form');
|
||||
const inputImport = generateImportStatement(['Input'], '@kit/ui/input');
|
||||
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 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 />`;
|
||||
|
||||
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') {
|
||||
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 />`;
|
||||
|
||||
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 {
|
||||
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 />`;
|
||||
|
||||
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 =
|
||||
@@ -152,13 +152,13 @@ export default function FormStory() {
|
||||
? ` defaultValues: {\n firstName: '',\n lastName: '',\n email: '',\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;
|
||||
};
|
||||
|
||||
// Basic form
|
||||
const basicForm = useForm<z.output<typeof basicFormSchema>>({
|
||||
const basicForm = useForm<z.infer<typeof basicFormSchema>>({
|
||||
resolver: zodResolver(basicFormSchema),
|
||||
defaultValues: {
|
||||
username: '',
|
||||
@@ -169,7 +169,7 @@ export default function FormStory() {
|
||||
});
|
||||
|
||||
// Advanced form
|
||||
const advancedForm = useForm<z.output<typeof advancedFormSchema>>({
|
||||
const advancedForm = useForm<z.infer<typeof advancedFormSchema>>({
|
||||
resolver: zodResolver(advancedFormSchema),
|
||||
defaultValues: {
|
||||
firstName: '',
|
||||
@@ -183,7 +183,7 @@ export default function FormStory() {
|
||||
});
|
||||
|
||||
// Validation form
|
||||
const validationForm = useForm<z.output<typeof validationFormSchema>>({
|
||||
const validationForm = useForm<z.infer<typeof validationFormSchema>>({
|
||||
resolver: zodResolver(validationFormSchema),
|
||||
defaultValues: {
|
||||
password: '',
|
||||
@@ -1056,7 +1056,7 @@ export default function FormStory() {
|
||||
<pre className="overflow-x-auto text-sm">
|
||||
{`import { useForm } from 'react-hook-form';
|
||||
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';
|
||||
|
||||
const formSchema = z.object({
|
||||
@@ -1065,7 +1065,7 @@ const formSchema = z.object({
|
||||
});
|
||||
|
||||
function MyForm() {
|
||||
const form = useForm<z.output<typeof formSchema>>({
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
username: '',
|
||||
@@ -1073,7 +1073,7 @@ function MyForm() {
|
||||
},
|
||||
});
|
||||
|
||||
function onSubmit(values: z.output<typeof formSchema>) {
|
||||
function onSubmit(values: z.infer<typeof formSchema>) {
|
||||
console.log(values);
|
||||
}
|
||||
|
||||
|
||||
@@ -99,7 +99,7 @@ export function KbdStory() {
|
||||
let snippet = groupLines.join('\n');
|
||||
|
||||
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, [
|
||||
@@ -115,11 +115,11 @@ export function KbdStory() {
|
||||
{controls.showTooltip ? (
|
||||
<TooltipProvider delayDuration={200}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={<Button variant="outline" className="gap-2" />}
|
||||
>
|
||||
<TooltipTrigger asChild>
|
||||
<Button variant="outline" className="gap-2">
|
||||
<Command className="h-4 w-4" />
|
||||
Command palette
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="flex items-center gap-2">
|
||||
<span>Press</span>
|
||||
|
||||
@@ -136,13 +136,11 @@ export function SimpleDataTableStory() {
|
||||
{controls.showActions && (
|
||||
<TableCell>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
render={
|
||||
<Button variant="ghost" className="h-8 w-8 p-0" />
|
||||
}
|
||||
>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" className="h-8 w-8 p-0">
|
||||
<span className="sr-only">Open menu</span>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem
|
||||
|
||||
@@ -100,7 +100,7 @@ export function SwitchStory() {
|
||||
className: cn(
|
||||
controls.size === 'sm' && 'h-4 w-7',
|
||||
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(
|
||||
controls.size === 'sm' && 'h-4 w-7',
|
||||
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>
|
||||
<Switch
|
||||
id="error-switch"
|
||||
className="data-checked:bg-destructive"
|
||||
className="data-[state=checked]:bg-destructive"
|
||||
/>
|
||||
</div>
|
||||
<p className="text-destructive text-sm">
|
||||
@@ -642,7 +642,7 @@ export function SwitchStory() {
|
||||
<div>
|
||||
<h4 className="mb-3 text-lg font-semibold">Switch</h4>
|
||||
<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.
|
||||
</p>
|
||||
<div className="overflow-x-auto">
|
||||
|
||||
@@ -62,9 +62,9 @@ interface TabsControlsProps {
|
||||
const variantClasses = {
|
||||
default: '',
|
||||
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:
|
||||
'[&>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 = {
|
||||
@@ -683,28 +683,28 @@ function App() {
|
||||
<TabsList className="h-auto rounded-none border-b bg-transparent p-0">
|
||||
<TabsTrigger
|
||||
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" />
|
||||
Overview
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
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" />
|
||||
Users
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
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" />
|
||||
Revenue
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
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" />
|
||||
Reports
|
||||
@@ -905,7 +905,8 @@ const apiReference = {
|
||||
{
|
||||
name: '...props',
|
||||
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: [
|
||||
|
||||
@@ -144,23 +144,22 @@ function TooltipStory() {
|
||||
|
||||
let code = `<TooltipProvider${providerPropsString}>\n`;
|
||||
code += ` <Tooltip>\n`;
|
||||
code += ` <TooltipTrigger asChild>\n`;
|
||||
|
||||
if (controls.triggerType === 'button') {
|
||||
code += ` <TooltipTrigger render={<Button variant="${controls.triggerVariant}" />}>\n`;
|
||||
code += ` Hover me\n`;
|
||||
code += ` <Button variant="${controls.triggerVariant}">Hover me</Button>\n`;
|
||||
} else if (controls.triggerType === 'icon') {
|
||||
code += ` <Button variant="${controls.triggerVariant}" size="icon">\n`;
|
||||
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 += ` </Button>\n`;
|
||||
} else if (controls.triggerType === 'text') {
|
||||
code += ` <TooltipTrigger render={<span className="cursor-help underline decoration-dotted" />}>\n`;
|
||||
code += ` Hover me\n`;
|
||||
code += ` <span className="cursor-help underline decoration-dotted">Hover me</span>\n`;
|
||||
} 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 += ` <TooltipContent${contentPropsString}>\n`;
|
||||
code += ` <p>${controls.content}</p>\n`;
|
||||
code += ` </TooltipContent>\n`;
|
||||
@@ -171,50 +170,28 @@ function TooltipStory() {
|
||||
};
|
||||
|
||||
const renderPreview = () => {
|
||||
const renderTrigger = () => {
|
||||
const trigger = (() => {
|
||||
switch (controls.triggerType) {
|
||||
case 'button':
|
||||
return (
|
||||
<TooltipTrigger
|
||||
render={<Button variant={controls.triggerVariant} />}
|
||||
>
|
||||
Hover me
|
||||
</TooltipTrigger>
|
||||
);
|
||||
return <Button variant={controls.triggerVariant}>Hover me</Button>;
|
||||
case 'icon':
|
||||
return (
|
||||
<TooltipTrigger
|
||||
render={<Button variant={controls.triggerVariant} size="icon" />}
|
||||
>
|
||||
<Button variant={controls.triggerVariant} size="icon">
|
||||
<IconComponent className="h-4 w-4" />
|
||||
</TooltipTrigger>
|
||||
</Button>
|
||||
);
|
||||
case 'text':
|
||||
return (
|
||||
<TooltipTrigger
|
||||
render={
|
||||
<span className="cursor-help underline decoration-dotted" />
|
||||
}
|
||||
>
|
||||
<span className="cursor-help underline decoration-dotted">
|
||||
Hover me
|
||||
</TooltipTrigger>
|
||||
</span>
|
||||
);
|
||||
case 'input':
|
||||
return (
|
||||
<TooltipTrigger
|
||||
render={<Input placeholder="Hover over this input" />}
|
||||
/>
|
||||
);
|
||||
return <Input placeholder="Hover over this input" />;
|
||||
default:
|
||||
return (
|
||||
<TooltipTrigger
|
||||
render={<Button variant={controls.triggerVariant} />}
|
||||
>
|
||||
Hover me
|
||||
</TooltipTrigger>
|
||||
);
|
||||
return <Button variant={controls.triggerVariant}>Hover me</Button>;
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
return (
|
||||
<div className="flex min-h-[200px] items-center justify-center">
|
||||
@@ -224,7 +201,7 @@ function TooltipStory() {
|
||||
disableHoverableContent={controls.disableHoverableContent}
|
||||
>
|
||||
<Tooltip>
|
||||
{renderTrigger()}
|
||||
<TooltipTrigger asChild>{trigger}</TooltipTrigger>
|
||||
<TooltipContent
|
||||
side={controls.side}
|
||||
align={controls.align}
|
||||
@@ -399,9 +376,11 @@ function TooltipStory() {
|
||||
<TooltipProvider>
|
||||
<div className="flex flex-wrap gap-4">
|
||||
<Tooltip>
|
||||
<TooltipTrigger render={<Button variant="outline" />}>
|
||||
<TooltipTrigger asChild>
|
||||
<Button variant="outline">
|
||||
<Info className="mr-2 h-4 w-4" />
|
||||
Info Button
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>This provides additional information</p>
|
||||
@@ -409,8 +388,10 @@ function TooltipStory() {
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger render={<Button variant="ghost" size="icon" />}>
|
||||
<TooltipTrigger asChild>
|
||||
<Button variant="ghost" size="icon">
|
||||
<HelpCircle className="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Click for help documentation</p>
|
||||
@@ -418,12 +399,10 @@ function TooltipStory() {
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={
|
||||
<span className="cursor-help underline decoration-dotted" />
|
||||
}
|
||||
>
|
||||
<TooltipTrigger asChild>
|
||||
<span className="cursor-help underline decoration-dotted">
|
||||
Hover for explanation
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>This term needs clarification for better understanding</p>
|
||||
@@ -431,9 +410,9 @@ function TooltipStory() {
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={<Input placeholder="Hover me" className="w-48" />}
|
||||
/>
|
||||
<TooltipTrigger asChild>
|
||||
<Input placeholder="Hover me" className="w-48" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Enter your email address here</p>
|
||||
</TooltipContent>
|
||||
@@ -455,10 +434,10 @@ function TooltipStory() {
|
||||
{/* Top Row */}
|
||||
<div></div>
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={<Button variant="outline" size="sm" />}
|
||||
>
|
||||
<TooltipTrigger asChild>
|
||||
<Button variant="outline" size="sm">
|
||||
Top
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top">
|
||||
<p>Tooltip on top</p>
|
||||
@@ -468,10 +447,10 @@ function TooltipStory() {
|
||||
|
||||
{/* Middle Row */}
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={<Button variant="outline" size="sm" />}
|
||||
>
|
||||
<TooltipTrigger asChild>
|
||||
<Button variant="outline" size="sm">
|
||||
Left
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="left">
|
||||
<p>Tooltip on left</p>
|
||||
@@ -481,10 +460,10 @@ function TooltipStory() {
|
||||
<span className="text-muted-foreground text-sm">Center</span>
|
||||
</div>
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={<Button variant="outline" size="sm" />}
|
||||
>
|
||||
<TooltipTrigger asChild>
|
||||
<Button variant="outline" size="sm">
|
||||
Right
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">
|
||||
<p>Tooltip on right</p>
|
||||
@@ -494,10 +473,10 @@ function TooltipStory() {
|
||||
{/* Bottom Row */}
|
||||
<div></div>
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={<Button variant="outline" size="sm" />}
|
||||
>
|
||||
<TooltipTrigger asChild>
|
||||
<Button variant="outline" size="sm">
|
||||
Bottom
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">
|
||||
<p>Tooltip on bottom</p>
|
||||
@@ -519,9 +498,11 @@ function TooltipStory() {
|
||||
<TooltipProvider>
|
||||
<div className="flex flex-wrap gap-4">
|
||||
<Tooltip>
|
||||
<TooltipTrigger render={<Button variant="outline" />}>
|
||||
<TooltipTrigger asChild>
|
||||
<Button variant="outline">
|
||||
<Star className="mr-2 h-4 w-4" />
|
||||
Premium Feature
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="max-w-xs">
|
||||
<div className="space-y-1">
|
||||
@@ -535,9 +516,11 @@ function TooltipStory() {
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger render={<Button variant="outline" />}>
|
||||
<TooltipTrigger asChild>
|
||||
<Button variant="outline">
|
||||
<Settings className="mr-2 h-4 w-4" />
|
||||
Advanced Settings
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<div className="space-y-1">
|
||||
@@ -554,9 +537,11 @@ function TooltipStory() {
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger render={<Button variant="destructive" />}>
|
||||
<TooltipTrigger asChild>
|
||||
<Button variant="destructive">
|
||||
<AlertCircle className="mr-2 h-4 w-4" />
|
||||
Delete Account
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="border-destructive bg-destructive text-destructive-foreground max-w-xs">
|
||||
<div className="space-y-1">
|
||||
@@ -583,10 +568,10 @@ function TooltipStory() {
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={<Button size="icon" variant="ghost" />}
|
||||
>
|
||||
<TooltipTrigger asChild>
|
||||
<Button size="icon" variant="ghost">
|
||||
<Copy className="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Copy to clipboard</p>
|
||||
@@ -594,10 +579,10 @@ function TooltipStory() {
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={<Button size="icon" variant="ghost" />}
|
||||
>
|
||||
<TooltipTrigger asChild>
|
||||
<Button size="icon" variant="ghost">
|
||||
<Download className="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Download file</p>
|
||||
@@ -605,10 +590,10 @@ function TooltipStory() {
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={<Button size="icon" variant="ghost" />}
|
||||
>
|
||||
<TooltipTrigger asChild>
|
||||
<Button size="icon" variant="ghost">
|
||||
<Share className="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Share with others</p>
|
||||
@@ -620,11 +605,9 @@ function TooltipStory() {
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="username">Username</Label>
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={
|
||||
<TooltipTrigger asChild>
|
||||
<Input id="username" placeholder="Enter username" />
|
||||
}
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Must be 3-20 characters, letters and numbers only</p>
|
||||
</TooltipContent>
|
||||
@@ -633,7 +616,9 @@ function TooltipStory() {
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<Tooltip>
|
||||
<TooltipTrigger render={<Checkbox id="terms" />} />
|
||||
<TooltipTrigger asChild>
|
||||
<Checkbox id="terms" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="max-w-xs">
|
||||
<p>
|
||||
By checking this, you agree to our Terms of Service and
|
||||
@@ -766,7 +751,7 @@ function TooltipStory() {
|
||||
</li>
|
||||
<li>
|
||||
<strong>TooltipTrigger:</strong> Element that triggers the
|
||||
tooltip (use render prop)
|
||||
tooltip (use asChild prop)
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -492,7 +492,7 @@ export const COMPONENTS_REGISTRY: ComponentInfo[] = [
|
||||
status: 'stable',
|
||||
component: CardButtonStory,
|
||||
sourceFile: '@kit/ui/card-button',
|
||||
props: ['className', 'children', 'onClick', 'disabled'],
|
||||
props: ['asChild', 'className', 'children', 'onClick', 'disabled'],
|
||||
icon: MousePointer,
|
||||
},
|
||||
|
||||
@@ -950,7 +950,7 @@ export const COMPONENTS_REGISTRY: ComponentInfo[] = [
|
||||
status: 'stable',
|
||||
component: ItemStory,
|
||||
sourceFile: '@kit/ui/item',
|
||||
props: ['variant', 'size', 'className'],
|
||||
props: ['variant', 'size', 'asChild', 'className'],
|
||||
icon: Layers,
|
||||
},
|
||||
|
||||
@@ -1004,7 +1004,7 @@ export const COMPONENTS_REGISTRY: ComponentInfo[] = [
|
||||
status: 'stable',
|
||||
component: BreadcrumbStory,
|
||||
sourceFile: '@kit/ui/breadcrumb',
|
||||
props: ['separator', 'href', 'className'],
|
||||
props: ['separator', 'asChild', 'href', 'className'],
|
||||
icon: ChevronRight,
|
||||
},
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { withI18n } from '../../lib/i18n/with-i18n';
|
||||
import { DocsContent } from './components/docs-content';
|
||||
import { DocsHeader } from './components/docs-header';
|
||||
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
|
||||
it in your inbox.{' '}
|
||||
<Dialog>
|
||||
<DialogTrigger
|
||||
render={<Button variant={'link'} className="p-0 underline" />}
|
||||
>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant={'link'} className="p-0 underline">
|
||||
Test Email
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
|
||||
<DialogContent>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as z from 'zod';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const EmailTesterFormSchema = z.object({
|
||||
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'}>
|
||||
{categoryTemplates.map((template) => (
|
||||
<CardButton
|
||||
key={template.id}
|
||||
render={
|
||||
<CardButton key={template.id} asChild>
|
||||
<Link href={`/emails/${template.id}`}>
|
||||
<CardButtonHeader>
|
||||
<CardButtonTitle>{template.name}</CardButtonTitle>
|
||||
</CardButtonHeader>
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
</CardButton>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,6 @@ import type { Metadata } from 'next';
|
||||
|
||||
import { DevToolLayout } from '@/components/app-layout';
|
||||
import { RootProviders } from '@/components/root-providers';
|
||||
import { getMessages } from 'next-intl/server';
|
||||
|
||||
import '../styles/globals.css';
|
||||
|
||||
@@ -11,17 +10,15 @@ export const metadata: Metadata = {
|
||||
description: 'The dev tool for Makerkit',
|
||||
};
|
||||
|
||||
export default async function RootLayout({
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
const messages = await getMessages();
|
||||
|
||||
return (
|
||||
<html lang="en">
|
||||
<body>
|
||||
<RootProviders messages={messages}>
|
||||
<RootProviders>
|
||||
<DevToolLayout>{children}</DevToolLayout>
|
||||
</RootProviders>
|
||||
</body>
|
||||
|
||||
@@ -37,6 +37,7 @@ export default async function DashboardPage() {
|
||||
return (
|
||||
<Page style={'custom'}>
|
||||
<PageHeader
|
||||
displaySidebarTrigger={false}
|
||||
title={'Dev Tool'}
|
||||
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({
|
||||
title: z
|
||||
@@ -32,4 +32,4 @@ export const CreatePRDSchema = z.object({
|
||||
.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}>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
render={
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" className="ml-auto">
|
||||
Select Languages
|
||||
<ChevronDownIcon className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
<DropdownMenuContent align="end" className="w-[200px]">
|
||||
{locales.map((locale) => (
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { revalidatePath } from 'next/cache';
|
||||
|
||||
import * as z from 'zod';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { findWorkspaceRoot } from '@kit/mcp-server/env';
|
||||
import {
|
||||
|
||||
@@ -731,15 +731,13 @@ function FilterSwitcher(props: {
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
render={
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" className="font-normal">
|
||||
{buttonLabel()}
|
||||
|
||||
<ChevronsUpDownIcon className="text-muted-foreground ml-1 h-3 w-3" />
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuCheckboxItem
|
||||
@@ -888,8 +886,7 @@ function Summary({ appState }: { appState: AppEnvState }) {
|
||||
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size={'sm'}
|
||||
@@ -913,16 +910,14 @@ function Summary({ appState }: { appState: AppEnvState }) {
|
||||
toast.promise(promise, {
|
||||
loading: 'Copying environment variables...',
|
||||
success: 'Environment variables copied to clipboard.',
|
||||
error:
|
||||
'Failed to copy environment variables 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>
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
|
||||
<TooltipContent>
|
||||
Copy environment variables to clipboard. You can place it in your
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { revalidatePath } from 'next/cache';
|
||||
|
||||
import * as z from 'zod';
|
||||
import { z } from 'zod';
|
||||
|
||||
import {
|
||||
createKitEnvDeps,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
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) {
|
||||
return (
|
||||
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
SidebarMenuSub,
|
||||
SidebarMenuSubButton,
|
||||
SidebarMenuSubItem,
|
||||
} from '@kit/ui/sidebar';
|
||||
} from '@kit/ui/shadcn-sidebar';
|
||||
import { isRouteActive } from '@kit/ui/utils';
|
||||
|
||||
const routes = [
|
||||
@@ -92,14 +92,14 @@ export function DevToolSidebar({
|
||||
{route.children.map((child) => (
|
||||
<SidebarMenuSubItem key={child.path}>
|
||||
<SidebarMenuSubButton
|
||||
render={
|
||||
asChild
|
||||
isActive={isRouteActive(child.path, pathname, false)}
|
||||
>
|
||||
<Link href={child.path}>
|
||||
<child.Icon className="h-4 w-4" />
|
||||
<span>{child.label}</span>
|
||||
</Link>
|
||||
}
|
||||
isActive={isRouteActive(child.path, pathname, false)}
|
||||
/>
|
||||
</SidebarMenuSubButton>
|
||||
</SidebarMenuSubItem>
|
||||
))}
|
||||
</SidebarMenuSub>
|
||||
@@ -107,13 +107,13 @@ export function DevToolSidebar({
|
||||
) : (
|
||||
<SidebarMenuButton
|
||||
isActive={isRouteActive(route.path, pathname, false)}
|
||||
render={
|
||||
asChild
|
||||
>
|
||||
<Link href={route.path}>
|
||||
<route.Icon className="h-4 w-4" />
|
||||
<span>{route.label}</span>
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
</SidebarMenuButton>
|
||||
)}
|
||||
</SidebarMenuItem>
|
||||
))}
|
||||
|
||||
@@ -3,18 +3,18 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
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';
|
||||
|
||||
export function RootProviders(
|
||||
props: React.PropsWithChildren<{ messages: AbstractIntlMessages }>,
|
||||
) {
|
||||
import { i18nResolver } from '../lib/i18n/i18n.resolver';
|
||||
import { getI18nSettings } from '../lib/i18n/i18n.settings';
|
||||
|
||||
export function RootProviders(props: React.PropsWithChildren) {
|
||||
return (
|
||||
<I18nClientProvider locale="en" messages={props.messages}>
|
||||
<I18nProvider settings={getI18nSettings('en')} resolver={i18nResolver}>
|
||||
<ReactQueryProvider>{props.children}</ReactQueryProvider>
|
||||
</I18nClientProvider>
|
||||
</I18nProvider>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ interface ServiceCardProps {
|
||||
export const ServiceCard = ({ name, status }: ServiceCardProps) => {
|
||||
return (
|
||||
<Card className="w-full max-w-2xl">
|
||||
<CardContent>
|
||||
<CardContent className="p-4">
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<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 createNextIntlPlugin from 'next-intl/plugin';
|
||||
|
||||
const withNextIntl = createNextIntlPlugin('./i18n/request.ts');
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
reactStrictMode: true,
|
||||
transpilePackages: ['@kit/ui', '@kit/shared', '@kit/i18n'],
|
||||
transpilePackages: ['@kit/ui', '@kit/shared'],
|
||||
reactCompiler: true,
|
||||
devIndicators: {
|
||||
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:",
|
||||
"lucide-react": "catalog:",
|
||||
"next": "catalog:",
|
||||
"next-intl": "catalog:",
|
||||
"nodemailer": "catalog:",
|
||||
"react": "catalog:",
|
||||
"react-dom": "catalog:",
|
||||
@@ -36,7 +35,7 @@
|
||||
"babel-plugin-react-compiler": "1.0.0",
|
||||
"pino-pretty": "13.0.0",
|
||||
"react-hook-form": "catalog:",
|
||||
"recharts": "3.7.0",
|
||||
"recharts": "2.15.3",
|
||||
"tailwindcss": "catalog:",
|
||||
"tw-animate-css": "catalog:",
|
||||
"typescript": "^5.9.3",
|
||||
|
||||
@@ -66,6 +66,26 @@
|
||||
--animate-accordion-down: accordion-down 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 {
|
||||
0% {
|
||||
opacity: 0;
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"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:setup": "playwright test tests/auth.setup.ts",
|
||||
"test:ui": "playwright test --ui"
|
||||
|
||||
@@ -38,8 +38,6 @@ test.describe('Account Settings', () => {
|
||||
|
||||
await Promise.all([request, response]);
|
||||
|
||||
await page.locator('[data-test="workspace-dropdown-trigger"]').click();
|
||||
|
||||
await expect(account.getProfileName()).toHaveText(name);
|
||||
});
|
||||
|
||||
|
||||
@@ -34,17 +34,17 @@ test.describe('Admin', () => {
|
||||
await page.goto('/admin');
|
||||
|
||||
// Check all stat cards are present
|
||||
await expect(page.getByText('Users', { exact: true })).toBeVisible();
|
||||
await expect(page.getByRole('heading', { name: 'Users' })).toBeVisible();
|
||||
|
||||
await expect(
|
||||
page.getByText('Team Accounts', { exact: true }),
|
||||
page.getByRole('heading', { name: 'Team Accounts' }),
|
||||
).toBeVisible();
|
||||
|
||||
await expect(
|
||||
page.getByText('Paying Customers', { exact: true }),
|
||||
page.getByRole('heading', { name: 'Paying Customers' }),
|
||||
).toBeVisible();
|
||||
|
||||
await expect(page.getByText('Trials', { exact: true })).toBeVisible();
|
||||
await expect(page.getByRole('heading', { name: 'Trials' })).toBeVisible();
|
||||
|
||||
// Verify stat values are numbers
|
||||
const stats = await page.$$('.text-3xl.font-bold');
|
||||
|
||||
@@ -31,17 +31,8 @@ export class AuthPageObject {
|
||||
}
|
||||
|
||||
async signOut() {
|
||||
const trigger = this.page.locator(
|
||||
'[data-test="workspace-dropdown-trigger"], [data-test="account-dropdown-trigger"]',
|
||||
);
|
||||
|
||||
await trigger.click();
|
||||
|
||||
const signOutButton = this.page.locator(
|
||||
'[data-test="workspace-sign-out"], [data-test="account-dropdown-sign-out"]',
|
||||
);
|
||||
|
||||
await signOutButton.click();
|
||||
await this.page.click('[data-test="account-dropdown-trigger"]');
|
||||
await this.page.click('[data-test="account-dropdown-sign-out"]');
|
||||
}
|
||||
|
||||
async signIn(params: { email: string; password: string }) {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { expect, test } from '@playwright/test';
|
||||
|
||||
test.describe('Healthcheck endpoint', () => {
|
||||
test('returns healthy status', async ({ request }) => {
|
||||
const response = await request.get('/api/healthcheck');
|
||||
const response = await request.get('/healthcheck');
|
||||
|
||||
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"]`,
|
||||
);
|
||||
|
||||
await this.page.getByRole('option', { name: invite.role }).click();
|
||||
await this.page.click(`[data-test="role-option-${invite.role}"]`);
|
||||
|
||||
if (index < invites.length - 1) {
|
||||
await form.locator('[data-test="add-new-invite-button"]').click();
|
||||
|
||||
@@ -36,13 +36,13 @@ export class TeamAccountsPageObject {
|
||||
}
|
||||
|
||||
getTeamFromSelector(teamName: string) {
|
||||
return this.page.locator('[data-test="workspace-team-item"]', {
|
||||
return this.page.locator(`[data-test="account-selector-team"]`, {
|
||||
hasText: teamName,
|
||||
});
|
||||
}
|
||||
|
||||
getTeams() {
|
||||
return this.page.locator('[data-test="workspace-team-item"]');
|
||||
return this.page.locator('[data-test="account-selector-team"]');
|
||||
}
|
||||
|
||||
goToSettings() {
|
||||
@@ -83,11 +83,10 @@ export class TeamAccountsPageObject {
|
||||
|
||||
openAccountsSelector() {
|
||||
return expect(async () => {
|
||||
await this.page.click('[data-test="workspace-dropdown-trigger"]');
|
||||
await this.page.click('[data-test="workspace-switch-submenu"]');
|
||||
await this.page.click('[data-test="account-selector-trigger"]');
|
||||
|
||||
return expect(
|
||||
this.page.locator('[data-test="workspace-switch-content"]'),
|
||||
this.page.locator('[data-test="account-selector-content"]'),
|
||||
).toBeVisible();
|
||||
}).toPass();
|
||||
}
|
||||
@@ -116,7 +115,7 @@ export class TeamAccountsPageObject {
|
||||
async createTeam({ teamName, slug } = this.createTeamName()) {
|
||||
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(
|
||||
'[data-test="create-team-form"] [data-test="team-name-input"]',
|
||||
@@ -141,15 +140,14 @@ export class TeamAccountsPageObject {
|
||||
await this.openAccountsSelector();
|
||||
await expect(this.getTeamFromSelector(teamName)).toBeVisible();
|
||||
|
||||
// Close the selector (Escape closes submenu, then parent dropdown)
|
||||
await this.page.keyboard.press('Escape');
|
||||
// Close the selector
|
||||
await this.page.keyboard.press('Escape');
|
||||
}
|
||||
|
||||
async createTeamWithNonLatinName(teamName: string, slug: string) {
|
||||
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(
|
||||
'[data-test="create-team-form"] [data-test="team-name-input"]',
|
||||
@@ -179,8 +177,7 @@ export class TeamAccountsPageObject {
|
||||
await this.openAccountsSelector();
|
||||
await expect(this.getTeamFromSelector(teamName)).toBeVisible();
|
||||
|
||||
// Close the selector (Escape closes submenu, then parent dropdown)
|
||||
await this.page.keyboard.press('Escape');
|
||||
// Close the selector
|
||||
await this.page.keyboard.press('Escape');
|
||||
}
|
||||
|
||||
@@ -210,10 +207,11 @@ export class TeamAccountsPageObject {
|
||||
}
|
||||
|
||||
async deleteAccount(email: string) {
|
||||
await expect(async () => {
|
||||
await this.page.click('[data-test="delete-team-trigger"]');
|
||||
|
||||
await this.otp.completeOtpVerification(email);
|
||||
|
||||
await expect(async () => {
|
||||
const click = this.page.click(
|
||||
'[data-test="delete-team-form-confirm-button"]',
|
||||
);
|
||||
|
||||
@@ -88,7 +88,7 @@ test.describe('Team Accounts', () => {
|
||||
await teamAccounts.createTeam();
|
||||
|
||||
await teamAccounts.openAccountsSelector();
|
||||
await page.click('[data-test="create-team-trigger"]');
|
||||
await page.click('[data-test="create-team-account-trigger"]');
|
||||
|
||||
await teamAccounts.tryCreateTeam('billing');
|
||||
|
||||
@@ -202,7 +202,7 @@ test.describe('Team Accounts', () => {
|
||||
|
||||
// Use non-Latin name to trigger the slug field visibility
|
||||
await teamAccounts.openAccountsSelector();
|
||||
await page.click('[data-test="create-team-trigger"]');
|
||||
await page.click('[data-test="create-team-account-trigger"]');
|
||||
|
||||
await page.fill(
|
||||
'[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=true
|
||||
NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_CREATION=true
|
||||
NEXT_PUBLIC_ENABLE_TEAMS_ACCOUNTS_ONLY=false
|
||||
NEXT_PUBLIC_LANGUAGE_PRIORITY=application
|
||||
|
||||
# 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 (
|
||||
<Footer
|
||||
logo={<AppLogo className="w-[85px] md:w-[95px]" />}
|
||||
description={<Trans i18nKey="marketing.footerDescription" />}
|
||||
description={<Trans i18nKey="marketing:footerDescription" />}
|
||||
copyright={
|
||||
<Trans
|
||||
i18nKey="marketing.copyright"
|
||||
i18nKey="marketing:copyright"
|
||||
values={{
|
||||
product: appConfig.name,
|
||||
year: new Date().getFullYear(),
|
||||
@@ -20,35 +20,35 @@ export function SiteFooter() {
|
||||
}
|
||||
sections={[
|
||||
{
|
||||
heading: <Trans i18nKey="marketing.about" />,
|
||||
heading: <Trans i18nKey="marketing:about" />,
|
||||
links: [
|
||||
{ href: '/blog', label: <Trans i18nKey="marketing.blog" /> },
|
||||
{ href: '/contact', label: <Trans i18nKey="marketing.contact" /> },
|
||||
{ href: '/blog', label: <Trans i18nKey="marketing:blog" /> },
|
||||
{ href: '/contact', label: <Trans i18nKey="marketing:contact" /> },
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: <Trans i18nKey="marketing.product" />,
|
||||
heading: <Trans i18nKey="marketing:product" />,
|
||||
links: [
|
||||
{
|
||||
href: '/docs',
|
||||
label: <Trans i18nKey="marketing.documentation" />,
|
||||
label: <Trans i18nKey="marketing:documentation" />,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: <Trans i18nKey="marketing.legal" />,
|
||||
heading: <Trans i18nKey="marketing:legal" />,
|
||||
links: [
|
||||
{
|
||||
href: '/terms-of-service',
|
||||
label: <Trans i18nKey="marketing.termsOfService" />,
|
||||
label: <Trans i18nKey="marketing:termsOfService" />,
|
||||
},
|
||||
{
|
||||
href: '/privacy-policy',
|
||||
label: <Trans i18nKey="marketing.privacyPolicy" />,
|
||||
label: <Trans i18nKey="marketing:privacyPolicy" />,
|
||||
},
|
||||
{
|
||||
href: '/cookie-policy',
|
||||
label: <Trans i18nKey="marketing.cookiePolicy" />,
|
||||
label: <Trans i18nKey="marketing:cookiePolicy" />,
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -31,7 +31,6 @@ const MobileModeToggle = dynamic(
|
||||
|
||||
const paths = {
|
||||
home: pathsConfig.app.home,
|
||||
profileSettings: pathsConfig.app.personalAccountSettings,
|
||||
};
|
||||
|
||||
const features = {
|
||||
@@ -79,28 +78,26 @@ function AuthButtons() {
|
||||
|
||||
<div className={'flex items-center gap-x-2'}>
|
||||
<Button
|
||||
nativeButton={false}
|
||||
className={'hidden md:flex md:text-sm'}
|
||||
render={
|
||||
<Link href={pathsConfig.auth.signIn}>
|
||||
<Trans i18nKey={'auth.signIn'} />
|
||||
</Link>
|
||||
}
|
||||
asChild
|
||||
variant={'outline'}
|
||||
size={'sm'}
|
||||
/>
|
||||
>
|
||||
<Link href={pathsConfig.auth.signIn}>
|
||||
<Trans i18nKey={'auth:signIn'} />
|
||||
</Link>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
nativeButton={false}
|
||||
render={
|
||||
<Link href={pathsConfig.auth.signUp}>
|
||||
<Trans i18nKey={'auth.signUp'} />
|
||||
</Link>
|
||||
}
|
||||
asChild
|
||||
className="text-xs md:text-sm"
|
||||
variant={'default'}
|
||||
size={'sm'}
|
||||
/>
|
||||
>
|
||||
<Link href={pathsConfig.auth.signUp}>
|
||||
<Trans i18nKey={'auth:signUp'} />
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -9,7 +9,7 @@ import { SiteNavigation } from './site-navigation';
|
||||
export function SiteHeader(props: { user?: JWTUserData | null }) {
|
||||
return (
|
||||
<Header
|
||||
logo={<AppLogo className="mx-auto sm:mx-0" href="/" />}
|
||||
logo={<AppLogo />}
|
||||
navigation={<SiteNavigation />}
|
||||
actions={<SiteHeaderAccountSection user={props.user ?? null} />}
|
||||
/>
|
||||
@@ -15,23 +15,23 @@ import { SiteNavigationItem } from './site-navigation-item';
|
||||
|
||||
const links = {
|
||||
Blog: {
|
||||
label: 'marketing.blog',
|
||||
label: 'marketing:blog',
|
||||
path: '/blog',
|
||||
},
|
||||
Changelog: {
|
||||
label: 'marketing.changelog',
|
||||
label: 'marketing:changelog',
|
||||
path: '/changelog',
|
||||
},
|
||||
Docs: {
|
||||
label: 'marketing.documentation',
|
||||
label: 'marketing:documentation',
|
||||
path: '/docs',
|
||||
},
|
||||
Pricing: {
|
||||
label: 'marketing.pricing',
|
||||
label: 'marketing:pricing',
|
||||
path: '/pricing',
|
||||
},
|
||||
FAQ: {
|
||||
label: 'marketing.faq',
|
||||
label: 'marketing:faq',
|
||||
path: '/faq',
|
||||
},
|
||||
};
|
||||
@@ -74,14 +74,11 @@ function MobileDropdown() {
|
||||
const className = 'flex w-full h-full items-center';
|
||||
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
key={item.path}
|
||||
render={
|
||||
<DropdownMenuItem key={item.path} asChild>
|
||||
<Link className={className} href={item.path}>
|
||||
<Trans i18nKey={item.label} />
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})}
|
||||
</DropdownMenuContent>
|
||||
@@ -6,6 +6,8 @@ import { notFound } from 'next/navigation';
|
||||
|
||||
import { createCmsClient } from '@kit/cms';
|
||||
|
||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||
|
||||
import { Post } from '../../blog/_components/post';
|
||||
|
||||
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'} />
|
||||
<Trans i18nKey={'marketing.blogPaginationPrevious'} />
|
||||
<Trans i18nKey={'marketing:blogPaginationPrevious'} />
|
||||
</Button>
|
||||
</If>
|
||||
|
||||
@@ -36,7 +36,7 @@ export function BlogPagination(props: {
|
||||
navigate(props.currentPage + 1);
|
||||
}}
|
||||
>
|
||||
<Trans i18nKey={'marketing.blogPaginationNext'} />
|
||||
<Trans i18nKey={'marketing:blogPaginationNext'} />
|
||||
<ArrowRight className={'ml-2 h-4'} />
|
||||
</Button>
|
||||
</If>
|
||||
@@ -2,13 +2,14 @@ import { cache } from 'react';
|
||||
|
||||
import type { Metadata } from 'next';
|
||||
|
||||
import { getLocale, getTranslations } from 'next-intl/server';
|
||||
|
||||
import { createCmsClient } from '@kit/cms';
|
||||
import { getLogger } from '@kit/shared/logger';
|
||||
import { If } from '@kit/ui/if';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
|
||||
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||
|
||||
// local imports
|
||||
import { SitePageHeader } from '../_components/site-page-header';
|
||||
import { BlogPagination } from './_components/blog-pagination';
|
||||
@@ -23,8 +24,7 @@ const BLOG_POSTS_PER_PAGE = 10;
|
||||
export const generateMetadata = async (
|
||||
props: BlogPageProps,
|
||||
): Promise<Metadata> => {
|
||||
const t = await getTranslations('marketing');
|
||||
const resolvedLanguage = await getLocale();
|
||||
const { t, resolvedLanguage } = await createI18nServerInstance();
|
||||
const searchParams = await props.searchParams;
|
||||
const limit = BLOG_POSTS_PER_PAGE;
|
||||
|
||||
@@ -34,8 +34,8 @@ export const generateMetadata = async (
|
||||
const { total } = await getContentItems(resolvedLanguage, limit, offset);
|
||||
|
||||
return {
|
||||
title: t('blog'),
|
||||
description: t('blogSubtitle'),
|
||||
title: t('marketing:blog'),
|
||||
description: t('marketing:blogSubtitle'),
|
||||
pagination: {
|
||||
previous: page > 0 ? `/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) {
|
||||
const t = await getTranslations('marketing');
|
||||
const language = await getLocale();
|
||||
const { t, resolvedLanguage: language } = await createI18nServerInstance();
|
||||
const searchParams = await props.searchParams;
|
||||
|
||||
const limit = BLOG_POSTS_PER_PAGE;
|
||||
@@ -83,12 +82,15 @@ async function BlogPage(props: BlogPageProps) {
|
||||
|
||||
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'}>
|
||||
<If
|
||||
condition={posts.length > 0}
|
||||
fallback={<Trans i18nKey="marketing.noPosts" />}
|
||||
fallback={<Trans i18nKey="marketing:noPosts" />}
|
||||
>
|
||||
<PostsGridList>
|
||||
{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) {
|
||||
return (
|
||||
@@ -6,6 +6,8 @@ import { notFound } from 'next/navigation';
|
||||
|
||||
import { createCmsClient } from '@kit/cms';
|
||||
|
||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||
|
||||
import { ChangelogDetail } from '../_components/changelog-detail';
|
||||
|
||||
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"
|
||||
>
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
<Trans i18nKey="marketing.changelog" />
|
||||
<Trans i18nKey="marketing:changelog" />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
@@ -24,8 +24,8 @@ function NavLink({ entry, direction }: NavLinkProps) {
|
||||
|
||||
const Icon = isPrevious ? ChevronLeft : ChevronRight;
|
||||
const i18nKey = isPrevious
|
||||
? 'marketing.changelogNavigationPrevious'
|
||||
: 'marketing.changelogNavigationNext';
|
||||
? 'marketing:changelogNavigationPrevious'
|
||||
: 'marketing:changelogNavigationNext';
|
||||
|
||||
return (
|
||||
<Link
|
||||
@@ -22,29 +22,24 @@ export function ChangelogPagination({
|
||||
return (
|
||||
<div className="flex justify-end gap-2">
|
||||
{canGoToPreviousPage && (
|
||||
<Button
|
||||
render={<Link href={`/changelog?page=${previousPage}`} />}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
>
|
||||
<Button asChild variant="outline" size="sm">
|
||||
<Link href={`/changelog?page=${previousPage}`}>
|
||||
<ArrowLeft className="mr-2 h-3 w-3" />
|
||||
|
||||
<span>
|
||||
<Trans i18nKey="marketing.changelogPaginationPrevious" />
|
||||
<Trans i18nKey="marketing:changelogPaginationPrevious" />
|
||||
</span>
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{canGoToNextPage && (
|
||||
<Button
|
||||
render={<Link href={`/changelog?page=${nextPage}`} />}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
>
|
||||
<Button asChild variant="outline" size="sm">
|
||||
<Link href={`/changelog?page=${nextPage}`}>
|
||||
<span>
|
||||
<Trans i18nKey="marketing.changelogPaginationNext" />
|
||||
<Trans i18nKey="marketing:changelogPaginationNext" />
|
||||
</span>
|
||||
<ArrowRight className="ml-2 h-3 w-3" />
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
@@ -2,13 +2,14 @@ import { cache } from 'react';
|
||||
|
||||
import type { Metadata } from 'next';
|
||||
|
||||
import { getLocale, getTranslations } from 'next-intl/server';
|
||||
|
||||
import { createCmsClient } from '@kit/cms';
|
||||
import { getLogger } from '@kit/shared/logger';
|
||||
import { If } from '@kit/ui/if';
|
||||
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 { ChangelogEntry } from './_components/changelog-entry';
|
||||
import { ChangelogPagination } from './_components/changelog-pagination';
|
||||
@@ -22,8 +23,7 @@ const CHANGELOG_ENTRIES_PER_PAGE = 50;
|
||||
export const generateMetadata = async (
|
||||
props: ChangelogPageProps,
|
||||
): Promise<Metadata> => {
|
||||
const t = await getTranslations('marketing');
|
||||
const resolvedLanguage = await getLocale();
|
||||
const { t, resolvedLanguage } = await createI18nServerInstance();
|
||||
const searchParams = await props.searchParams;
|
||||
const limit = CHANGELOG_ENTRIES_PER_PAGE;
|
||||
|
||||
@@ -33,8 +33,8 @@ export const generateMetadata = async (
|
||||
const { total } = await getContentItems(resolvedLanguage, limit, offset);
|
||||
|
||||
return {
|
||||
title: t('changelog'),
|
||||
description: t('changelogSubtitle'),
|
||||
title: t('marketing:changelog'),
|
||||
description: t('marketing:changelogSubtitle'),
|
||||
pagination: {
|
||||
previous: page > 0 ? `/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) {
|
||||
const t = await getTranslations('marketing');
|
||||
const language = await getLocale();
|
||||
const { t, resolvedLanguage: language } = await createI18nServerInstance();
|
||||
const searchParams = await props.searchParams;
|
||||
|
||||
const limit = CHANGELOG_ENTRIES_PER_PAGE;
|
||||
@@ -83,14 +82,14 @@ async function ChangelogPage(props: ChangelogPageProps) {
|
||||
return (
|
||||
<>
|
||||
<SitePageHeader
|
||||
title={t('changelog')}
|
||||
subtitle={t('changelogSubtitle')}
|
||||
title={t('marketing:changelog')}
|
||||
subtitle={t('marketing:changelogSubtitle')}
|
||||
/>
|
||||
|
||||
<div className="container flex max-w-4xl flex-col space-y-12 py-12">
|
||||
<If
|
||||
condition={entries.length > 0}
|
||||
fallback={<Trans i18nKey="marketing.noChangelogEntries" />}
|
||||
fallback={<Trans i18nKey="marketing:noChangelogEntries" />}
|
||||
>
|
||||
<div className="space-y-0">
|
||||
{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';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useState, useTransition } from 'react';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useAction } from 'next-safe-action/hooks';
|
||||
import { useForm } from 'react-hook-form';
|
||||
|
||||
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';
|
||||
|
||||
export function ContactForm() {
|
||||
const [pending, startTransition] = useTransition();
|
||||
|
||||
const [state, setState] = useState({
|
||||
success: false,
|
||||
error: false,
|
||||
});
|
||||
|
||||
const { execute, isPending } = useAction(sendContactEmail, {
|
||||
onSuccess: () => {
|
||||
setState({ success: true, error: false });
|
||||
},
|
||||
onError: () => {
|
||||
setState({ error: true, success: false });
|
||||
},
|
||||
});
|
||||
|
||||
const form = useForm({
|
||||
resolver: zodResolver(ContactEmailSchema),
|
||||
defaultValues: {
|
||||
@@ -60,7 +52,15 @@ export function ContactForm() {
|
||||
<form
|
||||
className={'flex flex-col space-y-4'}
|
||||
onSubmit={form.handleSubmit((data) => {
|
||||
execute(data);
|
||||
startTransition(async () => {
|
||||
try {
|
||||
await sendContactEmail(data);
|
||||
|
||||
setState({ success: true, error: false });
|
||||
} catch {
|
||||
setState({ error: true, success: false });
|
||||
}
|
||||
});
|
||||
})}
|
||||
>
|
||||
<FormField
|
||||
@@ -69,7 +69,7 @@ export function ContactForm() {
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
<Trans i18nKey={'marketing.contactName'} />
|
||||
<Trans i18nKey={'marketing:contactName'} />
|
||||
</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
@@ -88,7 +88,7 @@ export function ContactForm() {
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
<Trans i18nKey={'marketing.contactEmail'} />
|
||||
<Trans i18nKey={'marketing:contactEmail'} />
|
||||
</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
@@ -107,7 +107,7 @@ export function ContactForm() {
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
<Trans i18nKey={'marketing.contactMessage'} />
|
||||
<Trans i18nKey={'marketing:contactMessage'} />
|
||||
</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
@@ -124,8 +124,8 @@ export function ContactForm() {
|
||||
}}
|
||||
/>
|
||||
|
||||
<Button disabled={isPending} type={'submit'}>
|
||||
<Trans i18nKey={'marketing.sendMessage'} />
|
||||
<Button disabled={pending} type={'submit'}>
|
||||
<Trans i18nKey={'marketing:sendMessage'} />
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
@@ -136,11 +136,11 @@ function SuccessAlert() {
|
||||
return (
|
||||
<Alert variant={'success'}>
|
||||
<AlertTitle>
|
||||
<Trans i18nKey={'marketing.contactSuccess'} />
|
||||
<Trans i18nKey={'marketing:contactSuccess'} />
|
||||
</AlertTitle>
|
||||
|
||||
<AlertDescription>
|
||||
<Trans i18nKey={'marketing.contactSuccessDescription'} />
|
||||
<Trans i18nKey={'marketing:contactSuccessDescription'} />
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
@@ -150,11 +150,11 @@ function ErrorAlert() {
|
||||
return (
|
||||
<Alert variant={'destructive'}>
|
||||
<AlertTitle>
|
||||
<Trans i18nKey={'marketing.contactError'} />
|
||||
<Trans i18nKey={'marketing:contactError'} />
|
||||
</AlertTitle>
|
||||
|
||||
<AlertDescription>
|
||||
<Trans i18nKey={'marketing.contactErrorDescription'} />
|
||||
<Trans i18nKey={'marketing:contactErrorDescription'} />
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as z from 'zod';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const ContactEmailSchema = z.object({
|
||||
name: z.string().min(1).max(200),
|
||||
@@ -1,29 +1,30 @@
|
||||
'use server';
|
||||
|
||||
import * as z from 'zod';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { getMailer } from '@kit/mailers';
|
||||
import { publicActionClient } from '@kit/next/safe-action';
|
||||
import { enhanceAction } from '@kit/next/actions';
|
||||
|
||||
import { ContactEmailSchema } from '../contact-email.schema';
|
||||
|
||||
const contactEmail = z
|
||||
.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.',
|
||||
})
|
||||
.parse(process.env.CONTACT_EMAIL);
|
||||
|
||||
const emailFrom = z
|
||||
.string({
|
||||
error:
|
||||
description: `The email sending address.`,
|
||||
required_error:
|
||||
'Sender email is required. Please use the environment variable EMAIL_SENDER.',
|
||||
})
|
||||
.parse(process.env.EMAIL_SENDER);
|
||||
|
||||
export const sendContactEmail = publicActionClient
|
||||
.schema(ContactEmailSchema)
|
||||
.action(async ({ parsedInput: data }) => {
|
||||
export const sendContactEmail = enhanceAction(
|
||||
async (data) => {
|
||||
const mailer = await getMailer();
|
||||
|
||||
await mailer.sendEmail({
|
||||
@@ -42,4 +43,9 @@ export const sendContactEmail = publicActionClient
|
||||
});
|
||||
|
||||
return {};
|
||||
});
|
||||
},
|
||||
{
|
||||
schema: ContactEmailSchema,
|
||||
auth: false,
|
||||
},
|
||||
);
|
||||
@@ -1,25 +1,28 @@
|
||||
import { getTranslations } from 'next-intl/server';
|
||||
|
||||
import { Heading } from '@kit/ui/heading';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
|
||||
import { SitePageHeader } from '~/(marketing)/_components/site-page-header';
|
||||
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() {
|
||||
const t = await getTranslations('marketing');
|
||||
const { t } = await createI18nServerInstance();
|
||||
|
||||
return {
|
||||
title: t('contact'),
|
||||
title: t('marketing:contact'),
|
||||
};
|
||||
}
|
||||
|
||||
async function ContactPage() {
|
||||
const t = await getTranslations('marketing');
|
||||
const { t } = await createI18nServerInstance();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<SitePageHeader title={t(`contact`)} subtitle={t(`contactDescription`)} />
|
||||
<SitePageHeader
|
||||
title={t(`marketing:contact`)}
|
||||
subtitle={t(`marketing:contactDescription`)}
|
||||
/>
|
||||
|
||||
<div className={'container mx-auto'}>
|
||||
<div
|
||||
@@ -32,11 +35,11 @@ async function ContactPage() {
|
||||
>
|
||||
<div>
|
||||
<Heading level={3}>
|
||||
<Trans i18nKey={'marketing.contactHeading'} />
|
||||
<Trans i18nKey={'marketing:contactHeading'} />
|
||||
</Heading>
|
||||
|
||||
<p className={'text-muted-foreground'}>
|
||||
<Trans i18nKey={'marketing.contactSubheading'} />
|
||||
<Trans i18nKey={'marketing:contactSubheading'} />
|
||||
</p>
|
||||
</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 { cn } from '@kit/ui/utils';
|
||||
|
||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||
|
||||
// local imports
|
||||
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 { 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';
|
||||
|
||||
export function DocsNavLink({
|
||||
@@ -12,18 +12,20 @@ export function DocsNavLink({
|
||||
children,
|
||||
}: React.PropsWithChildren<{ label: string; url: string }>) {
|
||||
const currentPath = usePathname();
|
||||
const isCurrent = isRouteActive(url, currentPath);
|
||||
const isCurrent = isRouteActive(url, currentPath, true);
|
||||
|
||||
return (
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton
|
||||
render={<Link href={url} />}
|
||||
asChild
|
||||
isActive={isCurrent}
|
||||
className={cn('text-secondary-foreground transition-all')}
|
||||
>
|
||||
<Link href={url}>
|
||||
<span className="block max-w-full truncate">{label}</span>
|
||||
|
||||
{children}
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
);
|
||||
@@ -16,7 +16,7 @@ export function DocsNavigationCollapsible(
|
||||
const prefix = props.prefix;
|
||||
|
||||
const isChildActive = props.node.children.some((child) =>
|
||||
isRouteActive(prefix + '/' + child.url, currentPath),
|
||||
isRouteActive(prefix + '/' + child.url, currentPath, false),
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -10,12 +10,12 @@ import {
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
SidebarMenuSub,
|
||||
} from '@kit/ui/sidebar';
|
||||
} from '@kit/ui/shadcn-sidebar';
|
||||
|
||||
import { DocsNavLink } from '~/(marketing)/docs/_components/docs-nav-link';
|
||||
import { DocsNavigationCollapsible } from '~/(marketing)/docs/_components/docs-navigation-collapsible';
|
||||
|
||||
import { FloatingDocumentationNavigationButton } from './floating-docs-navigation-button';
|
||||
import { FloatingDocumentationNavigation } from './floating-docs-navigation';
|
||||
|
||||
function Node({
|
||||
node,
|
||||
@@ -85,11 +85,13 @@ function NodeTrigger({
|
||||
}) {
|
||||
if (node.collapsible) {
|
||||
return (
|
||||
<CollapsibleTrigger render={<SidebarMenuItem />}>
|
||||
<CollapsibleTrigger asChild>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton>
|
||||
{label}
|
||||
<ChevronDown className="ml-auto transition-transform group-data-[state=open]/collapsible:rotate-180" />
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</CollapsibleTrigger>
|
||||
);
|
||||
}
|
||||
@@ -135,10 +137,12 @@ export function DocsNavigation({
|
||||
return (
|
||||
<>
|
||||
<Sidebar
|
||||
variant={'sidebar'}
|
||||
className={'sticky z-1 mt-4 max-h-full overflow-y-auto pr-4'}
|
||||
variant={'ghost'}
|
||||
className={
|
||||
'border-border/50 sticky z-1 mt-4 max-h-full overflow-y-auto pr-4'
|
||||
}
|
||||
>
|
||||
<SidebarGroup>
|
||||
<SidebarGroup className="p-0">
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu className={'pb-48'}>
|
||||
<Tree pages={pages} level={0} prefix={prefix} />
|
||||
@@ -147,7 +151,17 @@ export function DocsNavigation({
|
||||
</SidebarGroup>
|
||||
</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
|
||||
import { DocsNavigation } from './_components/docs-navigation';
|
||||
@@ -8,8 +8,8 @@ import { getDocs } from './_lib/server/docs.loader';
|
||||
import { buildDocumentationTree } from './_lib/utils';
|
||||
|
||||
async function DocsLayout({ children }: React.PropsWithChildren) {
|
||||
const locale = await getLocale();
|
||||
const docs = await getDocs(locale);
|
||||
const { resolvedLanguage } = await createI18nServerInstance();
|
||||
const docs = await getDocs(resolvedLanguage);
|
||||
const tree = buildDocumentationTree(docs);
|
||||
|
||||
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 { DocsCards } from './_components/docs-cards';
|
||||
import { getDocs } from './_lib/server/docs.loader';
|
||||
|
||||
export const generateMetadata = async () => {
|
||||
const t = await getTranslations('marketing');
|
||||
const { t } = await createI18nServerInstance();
|
||||
|
||||
return {
|
||||
title: t('documentation'),
|
||||
title: t('marketing:documentation'),
|
||||
};
|
||||
};
|
||||
|
||||
async function DocsPage() {
|
||||
const t = await getTranslations('marketing');
|
||||
const locale = await getLocale();
|
||||
const items = await getDocs(locale);
|
||||
const { t, resolvedLanguage } = await createI18nServerInstance();
|
||||
const items = await getDocs(resolvedLanguage);
|
||||
|
||||
// Filter out any docs that have a parentId, as these are children of other docs
|
||||
const cards = items.filter((item) => !item.parentId);
|
||||
@@ -23,8 +23,8 @@ async function DocsPage() {
|
||||
return (
|
||||
<div className={'flex w-full flex-1 flex-col gap-y-6 xl:gap-y-8'}>
|
||||
<SitePageHeader
|
||||
title={t('documentation')}
|
||||
subtitle={t('documentationSubtitle')}
|
||||
title={t('marketing:documentation')}
|
||||
subtitle={t('marketing:documentationSubtitle')}
|
||||
/>
|
||||
|
||||
<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 { ArrowRight, ChevronDown } from 'lucide-react';
|
||||
import { getTranslations } from 'next-intl/server';
|
||||
|
||||
import { Button } from '@kit/ui/button';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
|
||||
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 () => {
|
||||
const t = await getTranslations('marketing');
|
||||
const { t } = await createI18nServerInstance();
|
||||
|
||||
return {
|
||||
title: t('faq'),
|
||||
title: t('marketing:faq'),
|
||||
};
|
||||
};
|
||||
|
||||
async function FAQPage() {
|
||||
const t = await getTranslations('marketing');
|
||||
const { t } = await createI18nServerInstance();
|
||||
|
||||
// replace this content with translations
|
||||
const faqItems = [
|
||||
{
|
||||
// or: t('faq.question1')
|
||||
// or: t('marketing:faq.question1')
|
||||
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.`,
|
||||
},
|
||||
{
|
||||
@@ -73,7 +74,10 @@ async function FAQPage() {
|
||||
/>
|
||||
|
||||
<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="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>
|
||||
<Button
|
||||
nativeButton={false}
|
||||
render={<Link href={'/contact'} />}
|
||||
variant={'link'}
|
||||
>
|
||||
<Button asChild variant={'outline'}>
|
||||
<Link href={'/contact'}>
|
||||
<span>
|
||||
<Trans i18nKey={'marketing.contactFaq'} />
|
||||
<Trans i18nKey={'marketing:contactFaq'} />
|
||||
</span>
|
||||
|
||||
<ArrowRight className={'ml-2 w-4'} />
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -101,7 +103,7 @@ async function FAQPage() {
|
||||
);
|
||||
}
|
||||
|
||||
export default FAQPage;
|
||||
export default withI18n(FAQPage);
|
||||
|
||||
function FaqItem({
|
||||
item,
|
||||
@@ -3,6 +3,7 @@ import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||
|
||||
import { SiteFooter } from '~/(marketing)/_components/site-footer';
|
||||
import { SiteHeader } from '~/(marketing)/_components/site-header';
|
||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||
|
||||
async function SiteLayout(props: React.PropsWithChildren) {
|
||||
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 pathsConfig from '~/config/paths.config';
|
||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||
|
||||
function Home() {
|
||||
return (
|
||||
@@ -29,13 +30,11 @@ function Home() {
|
||||
pill={
|
||||
<Pill label={'New'}>
|
||||
<span>The SaaS Starter Kit for ambitious developers</span>
|
||||
<PillActionButton
|
||||
render={
|
||||
<PillActionButton asChild>
|
||||
<Link href={'/auth/sign-up'}>
|
||||
<ArrowRightIcon className={'h-4 w-4'} />
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
</PillActionButton>
|
||||
</Pill>
|
||||
}
|
||||
title={
|
||||
@@ -171,7 +170,7 @@ function Home() {
|
||||
);
|
||||
}
|
||||
|
||||
export default Home;
|
||||
export default withI18n(Home);
|
||||
|
||||
function MainCallToActionButton() {
|
||||
return (
|
||||
@@ -180,7 +179,7 @@ function MainCallToActionButton() {
|
||||
<Link href={'/auth/sign-up'}>
|
||||
<span className={'flex items-center space-x-0.5'}>
|
||||
<span>
|
||||
<Trans i18nKey={'common.getStarted'} />
|
||||
<Trans i18nKey={'common:getStarted'} />
|
||||
</span>
|
||||
|
||||
<ArrowRightIcon
|
||||
@@ -195,7 +194,7 @@ function MainCallToActionButton() {
|
||||
|
||||
<CtaButton variant={'link'} className="h-10 text-sm">
|
||||
<Link href={'/pricing'}>
|
||||
<Trans i18nKey={'common.pricing'} />
|
||||
<Trans i18nKey={'common:pricing'} />
|
||||
</Link>
|
||||
</CtaButton>
|
||||
</div>
|
||||
@@ -1,16 +1,16 @@
|
||||
import { getTranslations } from 'next-intl/server';
|
||||
|
||||
import { PricingTable } from '@kit/billing-gateway/marketing';
|
||||
|
||||
import { SitePageHeader } from '~/(marketing)/_components/site-page-header';
|
||||
import billingConfig from '~/config/billing.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 () => {
|
||||
const t = await getTranslations('marketing');
|
||||
const { t } = await createI18nServerInstance();
|
||||
|
||||
return {
|
||||
title: t('pricing'),
|
||||
title: t('marketing:pricing'),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -20,11 +20,14 @@ const paths = {
|
||||
};
|
||||
|
||||
async function PricingPage() {
|
||||
const t = await getTranslations('marketing');
|
||||
const { t } = await createI18nServerInstance();
|
||||
|
||||
return (
|
||||
<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'}>
|
||||
<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