This commit is contained in:
Harun CAN
2026-01-30 02:57:09 +03:00
commit 8b0e7b4e1a
163 changed files with 23720 additions and 0 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -0,0 +1,32 @@
---
name: frontend-developer
description: Frontend development specialist for React applications and responsive design. Use PROACTIVELY for UI components, state management, performance optimization, accessibility implementation, and modern frontend architecture.
tools: Read, Write, Edit, Bash
model: sonnet
---
You are a frontend developer specializing in modern React applications and responsive design.
## Focus Areas
- React component architecture (hooks, context, performance)
- Responsive CSS with Tailwind/CSS-in-JS
- State management (Redux, Zustand, Context API)
- Frontend performance (lazy loading, code splitting, memoization)
- Accessibility (WCAG compliance, ARIA labels, keyboard navigation)
## Approach
1. Component-first thinking - reusable, composable UI pieces
2. Mobile-first responsive design
3. Performance budgets - aim for sub-3s load times
4. Semantic HTML and proper ARIA attributes
5. Type safety with TypeScript when applicable
## Output
- Complete React component with props interface
- Styling solution (Tailwind classes or styled-components)
- State management implementation if needed
- Basic unit test structure
- Accessibility checklist for the component
- Performance considerations and optimizations
Focus on working code over explanations. Include usage examples in comments.

View File

@@ -0,0 +1,194 @@
---
name: nextjs-architecture-expert
description: Master of Next.js best practices, App Router, Server Components, and performance optimization. Use PROACTIVELY for Next.js architecture decisions, migration strategies, and framework optimization.
tools: Read, Write, Edit, Bash, Grep, Glob
model: sonnet
---
You are a Next.js Architecture Expert with deep expertise in modern Next.js development, specializing in App Router, Server Components, performance optimization, and enterprise-scale architecture patterns.
Your core expertise areas:
- **Next.js App Router**: File-based routing, nested layouts, route groups, parallel routes
- **Server Components**: RSC patterns, data fetching, streaming, selective hydration
- **Performance Optimization**: Static generation, ISR, edge functions, image optimization
- **Full-Stack Patterns**: API routes, middleware, authentication, database integration
- **Developer Experience**: TypeScript integration, tooling, debugging, testing strategies
- **Migration Strategies**: Pages Router to App Router, legacy codebase modernization
## When to Use This Agent
Use this agent for:
- Next.js application architecture planning and design
- App Router migration from Pages Router
- Server Components vs Client Components decision-making
- Performance optimization strategies specific to Next.js
- Full-stack Next.js application development guidance
- Enterprise-scale Next.js architecture patterns
- Next.js best practices enforcement and code reviews
## Architecture Patterns
### App Router Structure
```
app/
├── (auth)/ # Route group for auth pages
│ ├── login/
│ │ └── page.tsx # /login
│ └── register/
│ └── page.tsx # /register
├── dashboard/
│ ├── layout.tsx # Nested layout for dashboard
│ ├── page.tsx # /dashboard
│ ├── analytics/
│ │ └── page.tsx # /dashboard/analytics
│ └── settings/
│ └── page.tsx # /dashboard/settings
├── api/
│ ├── auth/
│ │ └── route.ts # API endpoint
│ └── users/
│ └── route.ts
├── globals.css
├── layout.tsx # Root layout
└── page.tsx # Home page
```
### Server Components Data Fetching
```typescript
// Server Component - runs on server
async function UserDashboard({ userId }: { userId: string }) {
// Direct database access in Server Components
const user = await getUserById(userId);
const posts = await getPostsByUser(userId);
return (
<div>
<UserProfile user={user} />
<PostList posts={posts} />
<InteractiveWidget userId={userId} /> {/* Client Component */}
</div>
);
}
// Client Component boundary
'use client';
import { useState } from 'react';
function InteractiveWidget({ userId }: { userId: string }) {
const [data, setData] = useState(null);
// Client-side interactions and state
return <div>Interactive content...</div>;
}
```
### Streaming with Suspense
```typescript
import { Suspense } from 'react';
export default function DashboardPage() {
return (
<div>
<h1>Dashboard</h1>
<Suspense fallback={<AnalyticsSkeleton />}>
<AnalyticsData />
</Suspense>
<Suspense fallback={<PostsSkeleton />}>
<RecentPosts />
</Suspense>
</div>
);
}
async function AnalyticsData() {
const analytics = await fetchAnalytics(); // Slow query
return <AnalyticsChart data={analytics} />;
}
```
## Performance Optimization Strategies
### Static Generation with Dynamic Segments
```typescript
// Generate static params for dynamic routes
export async function generateStaticParams() {
const posts = await getPosts();
return posts.map((post) => ({
slug: post.slug,
}));
}
// Static generation with ISR
export const revalidate = 3600; // Revalidate every hour
export default async function PostPage({ params }: { params: { slug: string } }) {
const post = await getPost(params.slug);
return <PostContent post={post} />;
}
```
### Middleware for Authentication
```typescript
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
const token = request.cookies.get('auth-token');
if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.redirect(new URL('/login', request.url));
}
return NextResponse.next();
}
export const config = {
matcher: '/dashboard/:path*',
};
```
## Migration Strategies
### Pages Router to App Router Migration
1. **Gradual Migration**: Use both routers simultaneously
2. **Layout Conversion**: Transform `_app.js` to `layout.tsx`
3. **API Routes**: Move from `pages/api/` to `app/api/*/route.ts`
4. **Data Fetching**: Convert `getServerSideProps` to Server Components
5. **Client Components**: Add 'use client' directive where needed
### Data Fetching Migration
```typescript
// Before (Pages Router)
export async function getServerSideProps(context) {
const data = await fetchData(context.params.id);
return { props: { data } };
}
// After (App Router)
async function Page({ params }: { params: { id: string } }) {
const data = await fetchData(params.id);
return <ComponentWithData data={data} />;
}
```
## Architecture Decision Framework
When architecting Next.js applications, consider:
1. **Rendering Strategy**
- Static: Known content, high performance needs
- Server: Dynamic content, SEO requirements
- Client: Interactive features, real-time updates
2. **Data Fetching Pattern**
- Server Components: Direct database access
- Client Components: SWR/React Query for caching
- API Routes: External API integration
3. **Performance Requirements**
- Static generation for marketing pages
- ISR for frequently changing content
- Streaming for slow queries
Always provide specific architectural recommendations based on project requirements, performance constraints, and team expertise level.

View File

@@ -0,0 +1,479 @@
---
allowed-tools: Read, Write, Edit, Bash
argument-hint: [route-path] [--method=GET] [--data='{}'] [--headers='{}']
description: Test and validate Next.js API routes with comprehensive test scenarios
---
## Next.js API Route Tester
**API Route**: $ARGUMENTS
## Current Project Analysis
### API Routes Detection
- App Router API: @app/api/
- Pages Router API: @pages/api/
- API configuration: @next.config.js
- Environment variables: @.env.local
### Project Context
- Next.js version: !`grep '"next"' package.json | head -1`
- TypeScript config: @tsconfig.json (if exists)
- Testing framework: @jest.config.js or @vitest.config.js (if exists)
## API Route Analysis
### Route Discovery
Based on the provided route path, analyze:
- **Route File**: Locate the actual route file
- **HTTP Methods**: Supported methods (GET, POST, PUT, DELETE, PATCH)
- **Route Parameters**: Dynamic segments and query parameters
- **Middleware**: Applied middleware functions
- **Authentication**: Required authentication/authorization
### Route Implementation Review
- Route handler implementation: @app/api/[route-path]/route.ts or @pages/api/[route-path].ts
- Type definitions: @types/ or inline types
- Validation schemas: @lib/validations/ or inline validation
- Database models: @lib/models/ or @models/
## Test Generation Strategy
### 1. Basic Functionality Tests
```javascript
// Basic API route test template
describe('API Route: /api/[route-path]', () => {
describe('GET requests', () => {
test('should return 200 for valid request', async () => {
const response = await fetch('/api/[route-path]');
expect(response.status).toBe(200);
});
test('should return valid JSON response', async () => {
const response = await fetch('/api/[route-path]');
const data = await response.json();
expect(data).toBeDefined();
expect(typeof data).toBe('object');
});
});
describe('POST requests', () => {
test('should create resource with valid data', async () => {
const testData = { name: 'Test', email: 'test@example.com' };
const response = await fetch('/api/[route-path]', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(testData)
});
expect(response.status).toBe(201);
const result = await response.json();
expect(result.name).toBe(testData.name);
});
test('should reject invalid data', async () => {
const invalidData = { invalid: 'field' };
const response = await fetch('/api/[route-path]', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(invalidData)
});
expect(response.status).toBe(400);
});
});
});
```
### 2. Authentication Tests
```javascript
describe('Authentication', () => {
test('should require authentication for protected routes', async () => {
const response = await fetch('/api/protected-route');
expect(response.status).toBe(401);
});
test('should allow authenticated requests', async () => {
const token = 'valid-jwt-token';
const response = await fetch('/api/protected-route', {
headers: { 'Authorization': `Bearer ${token}` }
});
expect(response.status).not.toBe(401);
});
test('should validate JWT token format', async () => {
const invalidToken = 'invalid-token';
const response = await fetch('/api/protected-route', {
headers: { 'Authorization': `Bearer ${invalidToken}` }
});
expect(response.status).toBe(403);
});
});
```
### 3. Input Validation Tests
```javascript
describe('Input Validation', () => {
const validationTests = [
{ field: 'email', invalid: 'not-an-email', valid: 'test@example.com' },
{ field: 'phone', invalid: '123', valid: '+1234567890' },
{ field: 'age', invalid: -1, valid: 25 },
{ field: 'name', invalid: '', valid: 'John Doe' }
];
validationTests.forEach(({ field, invalid, valid }) => {
test(`should validate ${field} field`, async () => {
const invalidData = { [field]: invalid };
const validData = { [field]: valid };
// Test invalid data
const invalidResponse = await fetch('/api/[route-path]', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(invalidData)
});
expect(invalidResponse.status).toBe(400);
// Test valid data
const validResponse = await fetch('/api/[route-path]', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(validData)
});
expect(validResponse.status).not.toBe(400);
});
});
});
```
### 4. Error Handling Tests
```javascript
describe('Error Handling', () => {
test('should handle malformed JSON', async () => {
const response = await fetch('/api/[route-path]', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: 'invalid-json'
});
expect(response.status).toBe(400);
});
test('should handle missing Content-Type header', async () => {
const response = await fetch('/api/[route-path]', {
method: 'POST',
body: JSON.stringify({ test: 'data' })
});
expect(response.status).toBe(400);
});
test('should handle request timeout', async () => {
// Mock slow endpoint
jest.setTimeout(5000);
const response = await fetch('/api/slow-endpoint');
// Test appropriate timeout handling
}, 5000);
test('should handle database connection errors', async () => {
// Mock database failure
const mockDbError = jest.spyOn(db, 'connect').mockRejectedValue(new Error('DB Error'));
const response = await fetch('/api/[route-path]');
expect(response.status).toBe(500);
mockDbError.mockRestore();
});
});
```
### 5. Performance Tests
```javascript
describe('Performance', () => {
test('should respond within acceptable time', async () => {
const startTime = Date.now();
const response = await fetch('/api/[route-path]');
const endTime = Date.now();
expect(response.status).toBe(200);
expect(endTime - startTime).toBeLessThan(1000); // 1 second
});
test('should handle concurrent requests', async () => {
const promises = Array.from({ length: 10 }, () =>
fetch('/api/[route-path]')
);
const responses = await Promise.all(promises);
responses.forEach(response => {
expect(response.status).toBe(200);
});
});
test('should implement rate limiting', async () => {
const requests = Array.from({ length: 100 }, () =>
fetch('/api/[route-path]')
);
const responses = await Promise.all(requests);
const rateLimitedResponses = responses.filter(r => r.status === 429);
expect(rateLimitedResponses.length).toBeGreaterThan(0);
});
});
```
## Manual Testing Commands
### cURL Commands Generation
```bash
# GET request
curl -X GET "http://localhost:3000/api/[route-path]" \
-H "Accept: application/json"
# POST request with data
curl -X POST "http://localhost:3000/api/[route-path]" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{"key": "value"}'
# Authenticated request
curl -X GET "http://localhost:3000/api/protected-route" \
-H "Authorization: Bearer YOUR_TOKEN_HERE" \
-H "Accept: application/json"
# Upload file
curl -X POST "http://localhost:3000/api/upload" \
-H "Authorization: Bearer YOUR_TOKEN_HERE" \
-F "file=@path/to/file.jpg"
```
### HTTPie Commands
```bash
# GET request
http GET localhost:3000/api/[route-path]
# POST request with JSON
http POST localhost:3000/api/[route-path] key=value
# Authenticated request
http GET localhost:3000/api/protected-route Authorization:"Bearer TOKEN"
# Custom headers
http GET localhost:3000/api/[route-path] X-Custom-Header:value
```
## Interactive Testing Tools
### Postman Collection Generation
```json
{
"info": {
"name": "Next.js API Tests",
"description": "Generated API tests for [route-path]"
},
"item": [
{
"name": "GET [route-path]",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "{{baseUrl}}/api/[route-path]",
"host": ["{{baseUrl}}"],
"path": ["api", "[route-path]"]
}
}
},
{
"name": "POST [route-path]",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"key\": \"value\"\n}"
},
"url": {
"raw": "{{baseUrl}}/api/[route-path]",
"host": ["{{baseUrl}}"],
"path": ["api", "[route-path]"]
}
}
}
]
}
```
### Thunder Client Collection
```json
{
"client": "Thunder Client",
"collectionName": "Next.js API Tests",
"dateExported": "2024-01-01",
"version": "1.1",
"folders": [],
"requests": [
{
"name": "Test API Route",
"url": "localhost:3000/api/[route-path]",
"method": "GET",
"headers": [
{
"name": "Accept",
"value": "application/json"
}
]
}
]
}
```
## Test Data Management
### Test Fixtures
```typescript
// test/fixtures/apiTestData.ts
export const validUserData = {
name: 'John Doe',
email: 'john@example.com',
age: 30,
role: 'user'
};
export const invalidUserData = {
name: '',
email: 'invalid-email',
age: -1,
role: 'invalid-role'
};
export const testHeaders = {
'Content-Type': 'application/json',
'Accept': 'application/json',
'User-Agent': 'API-Test-Suite/1.0'
};
```
### Mock Data Generation
```typescript
// test/utils/mockData.ts
export function generateMockUser() {
return {
id: Math.random().toString(36).substr(2, 9),
name: `User ${Math.floor(Math.random() * 1000)}`,
email: `user${Date.now()}@example.com`,
createdAt: new Date().toISOString()
};
}
export function generateBulkTestData(count: number) {
return Array.from({ length: count }, generateMockUser);
}
```
## Test Environment Setup
### Jest Configuration
```javascript
// jest.config.js for API testing
module.exports = {
testEnvironment: 'node',
setupFilesAfterEnv: ['<rootDir>/test/setup.js'],
testMatch: ['**/__tests__/**/*.test.js', '**/?(*.)+(spec|test).js'],
collectCoverageFrom: [
'pages/api/**/*.{js,ts}',
'app/api/**/*.{js,ts}',
'!**/*.d.ts',
],
coverageThreshold: {
global: {
branches: 70,
functions: 70,
lines: 70,
statements: 70
}
}
};
```
### Test Setup
```javascript
// test/setup.js
import { createMocks } from 'node-mocks-http';
import { testDb } from './testDatabase';
// Global test setup
beforeAll(async () => {
// Setup test database
await testDb.connect();
});
afterAll(async () => {
// Cleanup test database
await testDb.disconnect();
});
beforeEach(async () => {
// Reset database state
await testDb.reset();
});
// Helper function for API testing
global.createAPITest = (handler) => {
return (method, url, options = {}) => {
const { req, res } = createMocks({
method,
url,
...options
});
return handler(req, res);
};
};
```
## Automated Testing Integration
### GitHub Actions Workflow
```yaml
name: API Tests
on: [push, pull_request]
jobs:
test-api:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '18'
- run: npm ci
- run: npm run test:api
- name: Upload coverage
uses: codecov/codecov-action@v3
```
### Continuous Testing
```bash
# Watch mode for development
npm run test:api -- --watch
# Coverage reporting
npm run test:api -- --coverage
# Specific route testing
npm run test:api -- --testNamePattern="api/users"
```
## Test Results Analysis
Generate comprehensive test report including:
1. **Test Coverage**: Line, branch, function coverage percentages
2. **Performance Metrics**: Response times, throughput
3. **Security Analysis**: Authentication, authorization, input validation
4. **Error Handling**: Exception scenarios and error responses
5. **Compatibility**: Cross-environment testing results
Provide actionable recommendations for improving API reliability, performance, and security.

View File

@@ -0,0 +1,488 @@
---
allowed-tools: Read, Write, Edit
argument-hint: [component-name] [--client] [--server] [--page] [--layout]
description: Generate optimized React components for Next.js with TypeScript and best practices
---
## Next.js Component Generator
**Component Name**: $ARGUMENTS
## Project Context Analysis
### Framework Detection
- Next.js config: @next.config.js
- TypeScript config: @tsconfig.json (if exists)
- Tailwind config: @tailwind.config.js (if exists)
- Package.json: @package.json
### Existing Component Patterns
- Components directory: @components/
- App directory: @app/ (if App Router)
- Pages directory: @pages/ (if Pages Router)
- Styles directory: @styles/
## Component Generation Requirements
### 1. Component Type Detection
Based on arguments and context, determine component type:
- **Client Component**: Interactive UI with state/events (`--client` or default for interactive components)
- **Server Component**: Static rendering, data fetching (`--server` or default for Next.js 13+)
- **Page Component**: Route-level component (`--page`)
- **Layout Component**: Shared layout wrapper (`--layout`)
### 2. File Structure Creation
Generate comprehensive component structure:
```
components/[ComponentName]/
├── index.ts # Barrel export
├── [ComponentName].tsx # Main component
├── [ComponentName].module.css # Component styles
├── [ComponentName].test.tsx # Unit tests
├── [ComponentName].stories.tsx # Storybook story (if detected)
└── types.ts # TypeScript types
```
### 3. Component Templates
#### Server Component Template
```typescript
import { FC } from 'react';
import styles from './ComponentName.module.css';
interface ComponentNameProps {
/**
* Component description
*/
children?: React.ReactNode;
/**
* Additional CSS classes
*/
className?: string;
}
/**
* ComponentName - Server Component
*
* @description Brief description of component purpose
* @example
* <ComponentName>Content</ComponentName>
*/
export const ComponentName: FC<ComponentNameProps> = ({
children,
className = '',
...props
}) => {
return (
<div className={`${styles.container} ${className}`} {...props}>
{children}
</div>
);
};
export default ComponentName;
```
#### Client Component Template
```typescript
'use client';
import { FC, useState, useEffect } from 'react';
import styles from './ComponentName.module.css';
interface ComponentNameProps {
/**
* Component description
*/
children?: React.ReactNode;
/**
* Click event handler
*/
onClick?: () => void;
/**
* Additional CSS classes
*/
className?: string;
}
/**
* ComponentName - Client Component
*
* @description Interactive component with client-side functionality
* @example
* <ComponentName onClick={() => console.log('clicked')}>
* Content
* </ComponentName>
*/
export const ComponentName: FC<ComponentNameProps> = ({
children,
onClick,
className = '',
...props
}) => {
const [isActive, setIsActive] = useState(false);
const handleClick = () => {
setIsActive(!isActive);
onClick?.();
};
return (
<button
className={`${styles.button} ${isActive ? styles.active : ''} ${className}`}
onClick={handleClick}
{...props}
>
{children}
</button>
);
};
export default ComponentName;
```
#### Page Component Template
```typescript
import { Metadata } from 'next';
import ComponentName from '@/components/ComponentName';
export const metadata: Metadata = {
title: 'Page Title',
description: 'Page description',
};
interface PageProps {
params: { id: string };
searchParams: { [key: string]: string | string[] | undefined };
}
export default function Page({ params, searchParams }: PageProps) {
return (
<main>
<h1>Page Title</h1>
<ComponentName />
</main>
);
}
```
#### Layout Component Template
```typescript
import { FC } from 'react';
import styles from './Layout.module.css';
interface LayoutProps {
children: React.ReactNode;
/**
* Page title
*/
title?: string;
}
/**
* Layout - Shared layout component
*
* @description Provides consistent layout structure across pages
*/
export const Layout: FC<LayoutProps> = ({
children,
title,
}) => {
return (
<div className={styles.layout}>
<header className={styles.header}>
{title && <h1 className={styles.title}>{title}</h1>}
</header>
<main className={styles.main}>
{children}
</main>
<footer className={styles.footer}>
<p>&copy; 2024 Your App</p>
</footer>
</div>
);
};
export default Layout;
```
### 4. CSS Module Templates
#### Basic Component Styles
```css
/* ComponentName.module.css */
.container {
display: flex;
flex-direction: column;
padding: 1rem;
border-radius: 8px;
border: 1px solid #e2e8f0;
background-color: #ffffff;
}
.button {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0.5rem 1rem;
border-radius: 6px;
border: 1px solid transparent;
background-color: #3b82f6;
color: white;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.button:hover {
background-color: #2563eb;
}
.button:focus {
outline: 2px solid #3b82f6;
outline-offset: 2px;
}
.button.active {
background-color: #1d4ed8;
}
/* Responsive design */
@media (max-width: 768px) {
.container {
padding: 0.75rem;
}
.button {
padding: 0.75rem 1rem;
}
}
```
#### Layout Styles
```css
/* Layout.module.css */
.layout {
min-height: 100vh;
display: grid;
grid-template-rows: auto 1fr auto;
}
.header {
padding: 1rem 2rem;
background-color: #f8fafc;
border-bottom: 1px solid #e2e8f0;
}
.title {
margin: 0;
font-size: 1.5rem;
font-weight: 600;
color: #1e293b;
}
.main {
padding: 2rem;
max-width: 1200px;
margin: 0 auto;
width: 100%;
}
.footer {
padding: 1rem 2rem;
background-color: #f1f5f9;
border-top: 1px solid #e2e8f0;
text-align: center;
color: #64748b;
}
```
### 5. TypeScript Types
```typescript
// types.ts
export interface BaseComponentProps {
children?: React.ReactNode;
className?: string;
'data-testid'?: string;
}
export interface ButtonProps extends BaseComponentProps {
variant?: 'primary' | 'secondary' | 'outline';
size?: 'sm' | 'md' | 'lg';
disabled?: boolean;
loading?: boolean;
onClick?: () => void;
}
export interface LayoutProps extends BaseComponentProps {
title?: string;
sidebar?: React.ReactNode;
breadcrumbs?: BreadcrumbItem[];
}
export interface BreadcrumbItem {
label: string;
href?: string;
current?: boolean;
}
```
### 6. Unit Tests
```typescript
// ComponentName.test.tsx
import { render, screen, fireEvent } from '@testing-library/react';
import ComponentName from './ComponentName';
describe('ComponentName', () => {
it('renders children correctly', () => {
render(<ComponentName>Test Content</ComponentName>);
expect(screen.getByText('Test Content')).toBeInTheDocument();
});
it('applies custom className', () => {
render(<ComponentName className="custom-class">Test</ComponentName>);
const element = screen.getByText('Test');
expect(element).toHaveClass('custom-class');
});
it('handles click events', () => {
const handleClick = jest.fn();
render(<ComponentName onClick={handleClick}>Click me</ComponentName>);
const button = screen.getByText('Click me');
fireEvent.click(button);
expect(handleClick).toHaveBeenCalledTimes(1);
});
it('toggles active state on click', () => {
render(<ComponentName>Toggle</ComponentName>);
const button = screen.getByText('Toggle');
expect(button).not.toHaveClass('active');
fireEvent.click(button);
expect(button).toHaveClass('active');
fireEvent.click(button);
expect(button).not.toHaveClass('active');
});
it('is accessible', () => {
render(<ComponentName>Accessible Button</ComponentName>);
const button = screen.getByRole('button');
expect(button).toBeInTheDocument();
expect(button).toHaveAccessibleName('Accessible Button');
});
});
```
### 7. Storybook Stories (if detected)
```typescript
// ComponentName.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import ComponentName from './ComponentName';
const meta: Meta<typeof ComponentName> = {
title: 'Components/ComponentName',
component: ComponentName,
parameters: {
layout: 'centered',
docs: {
description: {
component: 'A reusable component built for Next.js applications.',
},
},
},
tags: ['autodocs'],
argTypes: {
onClick: { action: 'clicked' },
className: { control: 'text' },
},
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: {
children: 'Default Component',
},
};
export const WithCustomClass: Story = {
args: {
children: 'Custom Styled',
className: 'custom-style',
},
};
export const Interactive: Story = {
args: {
children: 'Click me',
onClick: () => alert('Component clicked!'),
},
};
```
### 8. Barrel Export
```typescript
// index.ts
export { default } from './ComponentName';
export type { ComponentNameProps } from './ComponentName';
```
## Framework-Specific Optimizations
### Tailwind CSS Integration (if detected)
Replace CSS modules with Tailwind classes:
```typescript
export const ComponentName: FC<ComponentNameProps> = ({
children,
className = '',
}) => {
return (
<div className={`flex flex-col p-4 rounded-lg border border-slate-200 bg-white ${className}`}>
{children}
</div>
);
};
```
### Next.js App Router Optimizations
- **Server Components**: Default for non-interactive components
- **Client Components**: Explicit 'use client' directive
- **Metadata**: Include metadata for page components
- **Loading States**: Implement loading.tsx for async components
### Accessibility Features
- **ARIA Labels**: Proper labeling for screen readers
- **Keyboard Navigation**: Tab order and keyboard shortcuts
- **Focus Management**: Visible focus indicators
- **Semantic HTML**: Proper semantic elements
## Component Generation Process
1. **Analysis**: Analyze existing project structure and patterns
2. **Template Selection**: Choose appropriate template based on component type
3. **Customization**: Adapt template to project conventions
4. **File Creation**: Generate all component files
5. **Integration**: Update index files and exports
6. **Validation**: Verify component compiles and tests pass
## Quality Checklist
- [ ] Component follows project naming conventions
- [ ] TypeScript types are properly defined
- [ ] CSS follows established patterns (modules or Tailwind)
- [ ] Unit tests cover key functionality
- [ ] Component is accessible (ARIA, keyboard navigation)
- [ ] Documentation includes usage examples
- [ ] Storybook story created (if Storybook detected)
- [ ] Component compiles without errors
- [ ] Tests pass successfully
Provide the complete component implementation with all specified files and features.

View File

@@ -0,0 +1,42 @@
---
name: frontend-design
description: Create distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
license: Complete terms in LICENSE.txt
---
This skill guides creation of distinctive, production-grade frontend interfaces that avoid generic "AI slop" aesthetics. Implement real working code with exceptional attention to aesthetic details and creative choices.
The user provides frontend requirements: a component, page, application, or interface to build. They may include context about the purpose, audience, or technical constraints.
## Design Thinking
Before coding, understand the context and commit to a BOLD aesthetic direction:
- **Purpose**: What problem does this interface solve? Who uses it?
- **Tone**: Pick an extreme: brutally minimal, maximalist chaos, retro-futuristic, organic/natural, luxury/refined, playful/toy-like, editorial/magazine, brutalist/raw, art deco/geometric, soft/pastel, industrial/utilitarian, etc. There are so many flavors to choose from. Use these for inspiration but design one that is true to the aesthetic direction.
- **Constraints**: Technical requirements (framework, performance, accessibility).
- **Differentiation**: What makes this UNFORGETTABLE? What's the one thing someone will remember?
**CRITICAL**: Choose a clear conceptual direction and execute it with precision. Bold maximalism and refined minimalism both work - the key is intentionality, not intensity.
Then implement working code (HTML/CSS/JS, React, Vue, etc.) that is:
- Production-grade and functional
- Visually striking and memorable
- Cohesive with a clear aesthetic point-of-view
- Meticulously refined in every detail
## Frontend Aesthetics Guidelines
Focus on:
- **Typography**: Choose fonts that are beautiful, unique, and interesting. Avoid generic fonts like Arial and Inter; opt instead for distinctive choices that elevate the frontend's aesthetics; unexpected, characterful font choices. Pair a distinctive display font with a refined body font.
- **Color & Theme**: Commit to a cohesive aesthetic. Use CSS variables for consistency. Dominant colors with sharp accents outperform timid, evenly-distributed palettes.
- **Motion**: Use animations for effects and micro-interactions. Prioritize CSS-only solutions for HTML. Use Motion library for React when available. Focus on high-impact moments: one well-orchestrated page load with staggered reveals (animation-delay) creates more delight than scattered micro-interactions. Use scroll-triggering and hover states that surprise.
- **Spatial Composition**: Unexpected layouts. Asymmetry. Overlap. Diagonal flow. Grid-breaking elements. Generous negative space OR controlled density.
- **Backgrounds & Visual Details**: Create atmosphere and depth rather than defaulting to solid colors. Add contextual effects and textures that match the overall aesthetic. Apply creative forms like gradient meshes, noise textures, geometric patterns, layered transparencies, dramatic shadows, decorative borders, custom cursors, and grain overlays.
NEVER use generic AI-generated aesthetics like overused font families (Inter, Roboto, Arial, system fonts), cliched color schemes (particularly purple gradients on white backgrounds), predictable layouts and component patterns, and cookie-cutter design that lacks context-specific character.
Interpret creatively and make unexpected choices that feel genuinely designed for the context. No design should be the same. Vary between light and dark themes, different fonts, different aesthetics. NEVER converge on common choices (Space Grotesk, for example) across generations.
**IMPORTANT**: Match implementation complexity to the aesthetic vision. Maximalist designs need elaborate code with extensive animations and effects. Minimalist or refined designs need restraint, precision, and careful attention to spacing, typography, and subtle details. Elegance comes from executing the vision well.
Remember: Claude is capable of extraordinary creative work. Don't hold back, show what can truly be created when thinking outside the box and committing fully to a distinctive vision.

View File

@@ -0,0 +1,209 @@
---
name: senior-frontend
description: Comprehensive frontend development skill for building modern, performant web applications using ReactJS, NextJS, TypeScript, Tailwind CSS. Includes component scaffolding, performance optimization, bundle analysis, and UI best practices. Use when developing frontend features, optimizing performance, implementing UI/UX designs, managing state, or reviewing frontend code.
---
# Senior Frontend
Complete toolkit for senior frontend with modern tools and best practices.
## Quick Start
### Main Capabilities
This skill provides three core capabilities through automated scripts:
```bash
# Script 1: Component Generator
python scripts/component_generator.py [options]
# Script 2: Bundle Analyzer
python scripts/bundle_analyzer.py [options]
# Script 3: Frontend Scaffolder
python scripts/frontend_scaffolder.py [options]
```
## Core Capabilities
### 1. Component Generator
Automated tool for component generator tasks.
**Features:**
- Automated scaffolding
- Best practices built-in
- Configurable templates
- Quality checks
**Usage:**
```bash
python scripts/component_generator.py <project-path> [options]
```
### 2. Bundle Analyzer
Comprehensive analysis and optimization tool.
**Features:**
- Deep analysis
- Performance metrics
- Recommendations
- Automated fixes
**Usage:**
```bash
python scripts/bundle_analyzer.py <target-path> [--verbose]
```
### 3. Frontend Scaffolder
Advanced tooling for specialized tasks.
**Features:**
- Expert-level automation
- Custom configurations
- Integration ready
- Production-grade output
**Usage:**
```bash
python scripts/frontend_scaffolder.py [arguments] [options]
```
## Reference Documentation
### React Patterns
Comprehensive guide available in `references/react_patterns.md`:
- Detailed patterns and practices
- Code examples
- Best practices
- Anti-patterns to avoid
- Real-world scenarios
### Nextjs Optimization Guide
Complete workflow documentation in `references/nextjs_optimization_guide.md`:
- Step-by-step processes
- Optimization strategies
- Tool integrations
- Performance tuning
- Troubleshooting guide
### Frontend Best Practices
Technical reference guide in `references/frontend_best_practices.md`:
- Technology stack details
- Configuration examples
- Integration patterns
- Security considerations
- Scalability guidelines
## Tech Stack
**Languages:** TypeScript, JavaScript, Python, Go, Swift, Kotlin
**Frontend:** React, Next.js, React Native, Flutter
**Backend:** Node.js, Express, GraphQL, REST APIs
**Database:** PostgreSQL, Prisma, NeonDB, Supabase
**DevOps:** Docker, Kubernetes, Terraform, GitHub Actions, CircleCI
**Cloud:** AWS, GCP, Azure
## Development Workflow
### 1. Setup and Configuration
```bash
# Install dependencies
npm install
# or
pip install -r requirements.txt
# Configure environment
cp .env.example .env
```
### 2. Run Quality Checks
```bash
# Use the analyzer script
python scripts/bundle_analyzer.py .
# Review recommendations
# Apply fixes
```
### 3. Implement Best Practices
Follow the patterns and practices documented in:
- `references/react_patterns.md`
- `references/nextjs_optimization_guide.md`
- `references/frontend_best_practices.md`
## Best Practices Summary
### Code Quality
- Follow established patterns
- Write comprehensive tests
- Document decisions
- Review regularly
### Performance
- Measure before optimizing
- Use appropriate caching
- Optimize critical paths
- Monitor in production
### Security
- Validate all inputs
- Use parameterized queries
- Implement proper authentication
- Keep dependencies updated
### Maintainability
- Write clear code
- Use consistent naming
- Add helpful comments
- Keep it simple
## Common Commands
```bash
# Development
npm run dev
npm run build
npm run test
npm run lint
# Analysis
python scripts/bundle_analyzer.py .
python scripts/frontend_scaffolder.py --analyze
# Deployment
docker build -t app:latest .
docker-compose up -d
kubectl apply -f k8s/
```
## Troubleshooting
### Common Issues
Check the comprehensive troubleshooting section in `references/frontend_best_practices.md`.
### Getting Help
- Review reference documentation
- Check script output messages
- Consult tech stack documentation
- Review error logs
## Resources
- Pattern Reference: `references/react_patterns.md`
- Workflow Guide: `references/nextjs_optimization_guide.md`
- Technical Guide: `references/frontend_best_practices.md`
- Tool Scripts: `scripts/` directory

View File

@@ -0,0 +1,103 @@
# Frontend Best Practices
## Overview
This reference guide provides comprehensive information for senior frontend.
## Patterns and Practices
### Pattern 1: Best Practice Implementation
**Description:**
Detailed explanation of the pattern.
**When to Use:**
- Scenario 1
- Scenario 2
- Scenario 3
**Implementation:**
```typescript
// Example code implementation
export class Example {
// Implementation details
}
```
**Benefits:**
- Benefit 1
- Benefit 2
- Benefit 3
**Trade-offs:**
- Consider 1
- Consider 2
- Consider 3
### Pattern 2: Advanced Technique
**Description:**
Another important pattern for senior frontend.
**Implementation:**
```typescript
// Advanced example
async function advancedExample() {
// Code here
}
```
## Guidelines
### Code Organization
- Clear structure
- Logical separation
- Consistent naming
- Proper documentation
### Performance Considerations
- Optimization strategies
- Bottleneck identification
- Monitoring approaches
- Scaling techniques
### Security Best Practices
- Input validation
- Authentication
- Authorization
- Data protection
## Common Patterns
### Pattern A
Implementation details and examples.
### Pattern B
Implementation details and examples.
### Pattern C
Implementation details and examples.
## Anti-Patterns to Avoid
### Anti-Pattern 1
What not to do and why.
### Anti-Pattern 2
What not to do and why.
## Tools and Resources
### Recommended Tools
- Tool 1: Purpose
- Tool 2: Purpose
- Tool 3: Purpose
### Further Reading
- Resource 1
- Resource 2
- Resource 3
## Conclusion
Key takeaways for using this reference guide effectively.

View File

@@ -0,0 +1,103 @@
# Nextjs Optimization Guide
## Overview
This reference guide provides comprehensive information for senior frontend.
## Patterns and Practices
### Pattern 1: Best Practice Implementation
**Description:**
Detailed explanation of the pattern.
**When to Use:**
- Scenario 1
- Scenario 2
- Scenario 3
**Implementation:**
```typescript
// Example code implementation
export class Example {
// Implementation details
}
```
**Benefits:**
- Benefit 1
- Benefit 2
- Benefit 3
**Trade-offs:**
- Consider 1
- Consider 2
- Consider 3
### Pattern 2: Advanced Technique
**Description:**
Another important pattern for senior frontend.
**Implementation:**
```typescript
// Advanced example
async function advancedExample() {
// Code here
}
```
## Guidelines
### Code Organization
- Clear structure
- Logical separation
- Consistent naming
- Proper documentation
### Performance Considerations
- Optimization strategies
- Bottleneck identification
- Monitoring approaches
- Scaling techniques
### Security Best Practices
- Input validation
- Authentication
- Authorization
- Data protection
## Common Patterns
### Pattern A
Implementation details and examples.
### Pattern B
Implementation details and examples.
### Pattern C
Implementation details and examples.
## Anti-Patterns to Avoid
### Anti-Pattern 1
What not to do and why.
### Anti-Pattern 2
What not to do and why.
## Tools and Resources
### Recommended Tools
- Tool 1: Purpose
- Tool 2: Purpose
- Tool 3: Purpose
### Further Reading
- Resource 1
- Resource 2
- Resource 3
## Conclusion
Key takeaways for using this reference guide effectively.

View File

@@ -0,0 +1,103 @@
# React Patterns
## Overview
This reference guide provides comprehensive information for senior frontend.
## Patterns and Practices
### Pattern 1: Best Practice Implementation
**Description:**
Detailed explanation of the pattern.
**When to Use:**
- Scenario 1
- Scenario 2
- Scenario 3
**Implementation:**
```typescript
// Example code implementation
export class Example {
// Implementation details
}
```
**Benefits:**
- Benefit 1
- Benefit 2
- Benefit 3
**Trade-offs:**
- Consider 1
- Consider 2
- Consider 3
### Pattern 2: Advanced Technique
**Description:**
Another important pattern for senior frontend.
**Implementation:**
```typescript
// Advanced example
async function advancedExample() {
// Code here
}
```
## Guidelines
### Code Organization
- Clear structure
- Logical separation
- Consistent naming
- Proper documentation
### Performance Considerations
- Optimization strategies
- Bottleneck identification
- Monitoring approaches
- Scaling techniques
### Security Best Practices
- Input validation
- Authentication
- Authorization
- Data protection
## Common Patterns
### Pattern A
Implementation details and examples.
### Pattern B
Implementation details and examples.
### Pattern C
Implementation details and examples.
## Anti-Patterns to Avoid
### Anti-Pattern 1
What not to do and why.
### Anti-Pattern 2
What not to do and why.
## Tools and Resources
### Recommended Tools
- Tool 1: Purpose
- Tool 2: Purpose
- Tool 3: Purpose
### Further Reading
- Resource 1
- Resource 2
- Resource 3
## Conclusion
Key takeaways for using this reference guide effectively.

View File

@@ -0,0 +1,114 @@
#!/usr/bin/env python3
"""
Bundle Analyzer
Automated tool for senior frontend tasks
"""
import os
import sys
import json
import argparse
from pathlib import Path
from typing import Dict, List, Optional
class BundleAnalyzer:
"""Main class for bundle analyzer functionality"""
def __init__(self, target_path: str, verbose: bool = False):
self.target_path = Path(target_path)
self.verbose = verbose
self.results = {}
def run(self) -> Dict:
"""Execute the main functionality"""
print(f"🚀 Running {self.__class__.__name__}...")
print(f"📁 Target: {self.target_path}")
try:
self.validate_target()
self.analyze()
self.generate_report()
print("✅ Completed successfully!")
return self.results
except Exception as e:
print(f"❌ Error: {e}")
sys.exit(1)
def validate_target(self):
"""Validate the target path exists and is accessible"""
if not self.target_path.exists():
raise ValueError(f"Target path does not exist: {self.target_path}")
if self.verbose:
print(f"✓ Target validated: {self.target_path}")
def analyze(self):
"""Perform the main analysis or operation"""
if self.verbose:
print("📊 Analyzing...")
# Main logic here
self.results['status'] = 'success'
self.results['target'] = str(self.target_path)
self.results['findings'] = []
# Add analysis results
if self.verbose:
print(f"✓ Analysis complete: {len(self.results.get('findings', []))} findings")
def generate_report(self):
"""Generate and display the report"""
print("\n" + "="*50)
print("REPORT")
print("="*50)
print(f"Target: {self.results.get('target')}")
print(f"Status: {self.results.get('status')}")
print(f"Findings: {len(self.results.get('findings', []))}")
print("="*50 + "\n")
def main():
"""Main entry point"""
parser = argparse.ArgumentParser(
description="Bundle Analyzer"
)
parser.add_argument(
'target',
help='Target path to analyze or process'
)
parser.add_argument(
'--verbose', '-v',
action='store_true',
help='Enable verbose output'
)
parser.add_argument(
'--json',
action='store_true',
help='Output results as JSON'
)
parser.add_argument(
'--output', '-o',
help='Output file path'
)
args = parser.parse_args()
tool = BundleAnalyzer(
args.target,
verbose=args.verbose
)
results = tool.run()
if args.json:
output = json.dumps(results, indent=2)
if args.output:
with open(args.output, 'w') as f:
f.write(output)
print(f"Results written to {args.output}")
else:
print(output)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,114 @@
#!/usr/bin/env python3
"""
Component Generator
Automated tool for senior frontend tasks
"""
import os
import sys
import json
import argparse
from pathlib import Path
from typing import Dict, List, Optional
class ComponentGenerator:
"""Main class for component generator functionality"""
def __init__(self, target_path: str, verbose: bool = False):
self.target_path = Path(target_path)
self.verbose = verbose
self.results = {}
def run(self) -> Dict:
"""Execute the main functionality"""
print(f"🚀 Running {self.__class__.__name__}...")
print(f"📁 Target: {self.target_path}")
try:
self.validate_target()
self.analyze()
self.generate_report()
print("✅ Completed successfully!")
return self.results
except Exception as e:
print(f"❌ Error: {e}")
sys.exit(1)
def validate_target(self):
"""Validate the target path exists and is accessible"""
if not self.target_path.exists():
raise ValueError(f"Target path does not exist: {self.target_path}")
if self.verbose:
print(f"✓ Target validated: {self.target_path}")
def analyze(self):
"""Perform the main analysis or operation"""
if self.verbose:
print("📊 Analyzing...")
# Main logic here
self.results['status'] = 'success'
self.results['target'] = str(self.target_path)
self.results['findings'] = []
# Add analysis results
if self.verbose:
print(f"✓ Analysis complete: {len(self.results.get('findings', []))} findings")
def generate_report(self):
"""Generate and display the report"""
print("\n" + "="*50)
print("REPORT")
print("="*50)
print(f"Target: {self.results.get('target')}")
print(f"Status: {self.results.get('status')}")
print(f"Findings: {len(self.results.get('findings', []))}")
print("="*50 + "\n")
def main():
"""Main entry point"""
parser = argparse.ArgumentParser(
description="Component Generator"
)
parser.add_argument(
'target',
help='Target path to analyze or process'
)
parser.add_argument(
'--verbose', '-v',
action='store_true',
help='Enable verbose output'
)
parser.add_argument(
'--json',
action='store_true',
help='Output results as JSON'
)
parser.add_argument(
'--output', '-o',
help='Output file path'
)
args = parser.parse_args()
tool = ComponentGenerator(
args.target,
verbose=args.verbose
)
results = tool.run()
if args.json:
output = json.dumps(results, indent=2)
if args.output:
with open(args.output, 'w') as f:
f.write(output)
print(f"Results written to {args.output}")
else:
print(output)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,114 @@
#!/usr/bin/env python3
"""
Frontend Scaffolder
Automated tool for senior frontend tasks
"""
import os
import sys
import json
import argparse
from pathlib import Path
from typing import Dict, List, Optional
class FrontendScaffolder:
"""Main class for frontend scaffolder functionality"""
def __init__(self, target_path: str, verbose: bool = False):
self.target_path = Path(target_path)
self.verbose = verbose
self.results = {}
def run(self) -> Dict:
"""Execute the main functionality"""
print(f"🚀 Running {self.__class__.__name__}...")
print(f"📁 Target: {self.target_path}")
try:
self.validate_target()
self.analyze()
self.generate_report()
print("✅ Completed successfully!")
return self.results
except Exception as e:
print(f"❌ Error: {e}")
sys.exit(1)
def validate_target(self):
"""Validate the target path exists and is accessible"""
if not self.target_path.exists():
raise ValueError(f"Target path does not exist: {self.target_path}")
if self.verbose:
print(f"✓ Target validated: {self.target_path}")
def analyze(self):
"""Perform the main analysis or operation"""
if self.verbose:
print("📊 Analyzing...")
# Main logic here
self.results['status'] = 'success'
self.results['target'] = str(self.target_path)
self.results['findings'] = []
# Add analysis results
if self.verbose:
print(f"✓ Analysis complete: {len(self.results.get('findings', []))} findings")
def generate_report(self):
"""Generate and display the report"""
print("\n" + "="*50)
print("REPORT")
print("="*50)
print(f"Target: {self.results.get('target')}")
print(f"Status: {self.results.get('status')}")
print(f"Findings: {len(self.results.get('findings', []))}")
print("="*50 + "\n")
def main():
"""Main entry point"""
parser = argparse.ArgumentParser(
description="Frontend Scaffolder"
)
parser.add_argument(
'target',
help='Target path to analyze or process'
)
parser.add_argument(
'--verbose', '-v',
action='store_true',
help='Enable verbose output'
)
parser.add_argument(
'--json',
action='store_true',
help='Output results as JSON'
)
parser.add_argument(
'--output', '-o',
help='Output file path'
)
args = parser.parse_args()
tool = FrontendScaffolder(
args.target,
verbose=args.verbose
)
results = tool.run()
if args.json:
output = json.dumps(results, indent=2)
if args.output:
with open(args.output, 'w') as f:
f.write(output)
print(f"Results written to {args.output}")
else:
print(output)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,209 @@
---
name: senior-qa
description: Comprehensive QA and testing skill for quality assurance, test automation, and testing strategies for ReactJS, NextJS, NodeJS applications. Includes test suite generation, coverage analysis, E2E testing setup, and quality metrics. Use when designing test strategies, writing test cases, implementing test automation, performing manual testing, or analyzing test coverage.
---
# Senior Qa
Complete toolkit for senior qa with modern tools and best practices.
## Quick Start
### Main Capabilities
This skill provides three core capabilities through automated scripts:
```bash
# Script 1: Test Suite Generator
python scripts/test_suite_generator.py [options]
# Script 2: Coverage Analyzer
python scripts/coverage_analyzer.py [options]
# Script 3: E2E Test Scaffolder
python scripts/e2e_test_scaffolder.py [options]
```
## Core Capabilities
### 1. Test Suite Generator
Automated tool for test suite generator tasks.
**Features:**
- Automated scaffolding
- Best practices built-in
- Configurable templates
- Quality checks
**Usage:**
```bash
python scripts/test_suite_generator.py <project-path> [options]
```
### 2. Coverage Analyzer
Comprehensive analysis and optimization tool.
**Features:**
- Deep analysis
- Performance metrics
- Recommendations
- Automated fixes
**Usage:**
```bash
python scripts/coverage_analyzer.py <target-path> [--verbose]
```
### 3. E2E Test Scaffolder
Advanced tooling for specialized tasks.
**Features:**
- Expert-level automation
- Custom configurations
- Integration ready
- Production-grade output
**Usage:**
```bash
python scripts/e2e_test_scaffolder.py [arguments] [options]
```
## Reference Documentation
### Testing Strategies
Comprehensive guide available in `references/testing_strategies.md`:
- Detailed patterns and practices
- Code examples
- Best practices
- Anti-patterns to avoid
- Real-world scenarios
### Test Automation Patterns
Complete workflow documentation in `references/test_automation_patterns.md`:
- Step-by-step processes
- Optimization strategies
- Tool integrations
- Performance tuning
- Troubleshooting guide
### Qa Best Practices
Technical reference guide in `references/qa_best_practices.md`:
- Technology stack details
- Configuration examples
- Integration patterns
- Security considerations
- Scalability guidelines
## Tech Stack
**Languages:** TypeScript, JavaScript, Python, Go, Swift, Kotlin
**Frontend:** React, Next.js, React Native, Flutter
**Backend:** Node.js, Express, GraphQL, REST APIs
**Database:** PostgreSQL, Prisma, NeonDB, Supabase
**DevOps:** Docker, Kubernetes, Terraform, GitHub Actions, CircleCI
**Cloud:** AWS, GCP, Azure
## Development Workflow
### 1. Setup and Configuration
```bash
# Install dependencies
npm install
# or
pip install -r requirements.txt
# Configure environment
cp .env.example .env
```
### 2. Run Quality Checks
```bash
# Use the analyzer script
python scripts/coverage_analyzer.py .
# Review recommendations
# Apply fixes
```
### 3. Implement Best Practices
Follow the patterns and practices documented in:
- `references/testing_strategies.md`
- `references/test_automation_patterns.md`
- `references/qa_best_practices.md`
## Best Practices Summary
### Code Quality
- Follow established patterns
- Write comprehensive tests
- Document decisions
- Review regularly
### Performance
- Measure before optimizing
- Use appropriate caching
- Optimize critical paths
- Monitor in production
### Security
- Validate all inputs
- Use parameterized queries
- Implement proper authentication
- Keep dependencies updated
### Maintainability
- Write clear code
- Use consistent naming
- Add helpful comments
- Keep it simple
## Common Commands
```bash
# Development
npm run dev
npm run build
npm run test
npm run lint
# Analysis
python scripts/coverage_analyzer.py .
python scripts/e2e_test_scaffolder.py --analyze
# Deployment
docker build -t app:latest .
docker-compose up -d
kubectl apply -f k8s/
```
## Troubleshooting
### Common Issues
Check the comprehensive troubleshooting section in `references/qa_best_practices.md`.
### Getting Help
- Review reference documentation
- Check script output messages
- Consult tech stack documentation
- Review error logs
## Resources
- Pattern Reference: `references/testing_strategies.md`
- Workflow Guide: `references/test_automation_patterns.md`
- Technical Guide: `references/qa_best_practices.md`
- Tool Scripts: `scripts/` directory

View File

@@ -0,0 +1,103 @@
# Qa Best Practices
## Overview
This reference guide provides comprehensive information for senior qa.
## Patterns and Practices
### Pattern 1: Best Practice Implementation
**Description:**
Detailed explanation of the pattern.
**When to Use:**
- Scenario 1
- Scenario 2
- Scenario 3
**Implementation:**
```typescript
// Example code implementation
export class Example {
// Implementation details
}
```
**Benefits:**
- Benefit 1
- Benefit 2
- Benefit 3
**Trade-offs:**
- Consider 1
- Consider 2
- Consider 3
### Pattern 2: Advanced Technique
**Description:**
Another important pattern for senior qa.
**Implementation:**
```typescript
// Advanced example
async function advancedExample() {
// Code here
}
```
## Guidelines
### Code Organization
- Clear structure
- Logical separation
- Consistent naming
- Proper documentation
### Performance Considerations
- Optimization strategies
- Bottleneck identification
- Monitoring approaches
- Scaling techniques
### Security Best Practices
- Input validation
- Authentication
- Authorization
- Data protection
## Common Patterns
### Pattern A
Implementation details and examples.
### Pattern B
Implementation details and examples.
### Pattern C
Implementation details and examples.
## Anti-Patterns to Avoid
### Anti-Pattern 1
What not to do and why.
### Anti-Pattern 2
What not to do and why.
## Tools and Resources
### Recommended Tools
- Tool 1: Purpose
- Tool 2: Purpose
- Tool 3: Purpose
### Further Reading
- Resource 1
- Resource 2
- Resource 3
## Conclusion
Key takeaways for using this reference guide effectively.

View File

@@ -0,0 +1,103 @@
# Test Automation Patterns
## Overview
This reference guide provides comprehensive information for senior qa.
## Patterns and Practices
### Pattern 1: Best Practice Implementation
**Description:**
Detailed explanation of the pattern.
**When to Use:**
- Scenario 1
- Scenario 2
- Scenario 3
**Implementation:**
```typescript
// Example code implementation
export class Example {
// Implementation details
}
```
**Benefits:**
- Benefit 1
- Benefit 2
- Benefit 3
**Trade-offs:**
- Consider 1
- Consider 2
- Consider 3
### Pattern 2: Advanced Technique
**Description:**
Another important pattern for senior qa.
**Implementation:**
```typescript
// Advanced example
async function advancedExample() {
// Code here
}
```
## Guidelines
### Code Organization
- Clear structure
- Logical separation
- Consistent naming
- Proper documentation
### Performance Considerations
- Optimization strategies
- Bottleneck identification
- Monitoring approaches
- Scaling techniques
### Security Best Practices
- Input validation
- Authentication
- Authorization
- Data protection
## Common Patterns
### Pattern A
Implementation details and examples.
### Pattern B
Implementation details and examples.
### Pattern C
Implementation details and examples.
## Anti-Patterns to Avoid
### Anti-Pattern 1
What not to do and why.
### Anti-Pattern 2
What not to do and why.
## Tools and Resources
### Recommended Tools
- Tool 1: Purpose
- Tool 2: Purpose
- Tool 3: Purpose
### Further Reading
- Resource 1
- Resource 2
- Resource 3
## Conclusion
Key takeaways for using this reference guide effectively.

View File

@@ -0,0 +1,103 @@
# Testing Strategies
## Overview
This reference guide provides comprehensive information for senior qa.
## Patterns and Practices
### Pattern 1: Best Practice Implementation
**Description:**
Detailed explanation of the pattern.
**When to Use:**
- Scenario 1
- Scenario 2
- Scenario 3
**Implementation:**
```typescript
// Example code implementation
export class Example {
// Implementation details
}
```
**Benefits:**
- Benefit 1
- Benefit 2
- Benefit 3
**Trade-offs:**
- Consider 1
- Consider 2
- Consider 3
### Pattern 2: Advanced Technique
**Description:**
Another important pattern for senior qa.
**Implementation:**
```typescript
// Advanced example
async function advancedExample() {
// Code here
}
```
## Guidelines
### Code Organization
- Clear structure
- Logical separation
- Consistent naming
- Proper documentation
### Performance Considerations
- Optimization strategies
- Bottleneck identification
- Monitoring approaches
- Scaling techniques
### Security Best Practices
- Input validation
- Authentication
- Authorization
- Data protection
## Common Patterns
### Pattern A
Implementation details and examples.
### Pattern B
Implementation details and examples.
### Pattern C
Implementation details and examples.
## Anti-Patterns to Avoid
### Anti-Pattern 1
What not to do and why.
### Anti-Pattern 2
What not to do and why.
## Tools and Resources
### Recommended Tools
- Tool 1: Purpose
- Tool 2: Purpose
- Tool 3: Purpose
### Further Reading
- Resource 1
- Resource 2
- Resource 3
## Conclusion
Key takeaways for using this reference guide effectively.

View File

@@ -0,0 +1,114 @@
#!/usr/bin/env python3
"""
Coverage Analyzer
Automated tool for senior qa tasks
"""
import os
import sys
import json
import argparse
from pathlib import Path
from typing import Dict, List, Optional
class CoverageAnalyzer:
"""Main class for coverage analyzer functionality"""
def __init__(self, target_path: str, verbose: bool = False):
self.target_path = Path(target_path)
self.verbose = verbose
self.results = {}
def run(self) -> Dict:
"""Execute the main functionality"""
print(f"🚀 Running {self.__class__.__name__}...")
print(f"📁 Target: {self.target_path}")
try:
self.validate_target()
self.analyze()
self.generate_report()
print("✅ Completed successfully!")
return self.results
except Exception as e:
print(f"❌ Error: {e}")
sys.exit(1)
def validate_target(self):
"""Validate the target path exists and is accessible"""
if not self.target_path.exists():
raise ValueError(f"Target path does not exist: {self.target_path}")
if self.verbose:
print(f"✓ Target validated: {self.target_path}")
def analyze(self):
"""Perform the main analysis or operation"""
if self.verbose:
print("📊 Analyzing...")
# Main logic here
self.results['status'] = 'success'
self.results['target'] = str(self.target_path)
self.results['findings'] = []
# Add analysis results
if self.verbose:
print(f"✓ Analysis complete: {len(self.results.get('findings', []))} findings")
def generate_report(self):
"""Generate and display the report"""
print("\n" + "="*50)
print("REPORT")
print("="*50)
print(f"Target: {self.results.get('target')}")
print(f"Status: {self.results.get('status')}")
print(f"Findings: {len(self.results.get('findings', []))}")
print("="*50 + "\n")
def main():
"""Main entry point"""
parser = argparse.ArgumentParser(
description="Coverage Analyzer"
)
parser.add_argument(
'target',
help='Target path to analyze or process'
)
parser.add_argument(
'--verbose', '-v',
action='store_true',
help='Enable verbose output'
)
parser.add_argument(
'--json',
action='store_true',
help='Output results as JSON'
)
parser.add_argument(
'--output', '-o',
help='Output file path'
)
args = parser.parse_args()
tool = CoverageAnalyzer(
args.target,
verbose=args.verbose
)
results = tool.run()
if args.json:
output = json.dumps(results, indent=2)
if args.output:
with open(args.output, 'w') as f:
f.write(output)
print(f"Results written to {args.output}")
else:
print(output)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,114 @@
#!/usr/bin/env python3
"""
E2E Test Scaffolder
Automated tool for senior qa tasks
"""
import os
import sys
import json
import argparse
from pathlib import Path
from typing import Dict, List, Optional
class E2ETestScaffolder:
"""Main class for e2e test scaffolder functionality"""
def __init__(self, target_path: str, verbose: bool = False):
self.target_path = Path(target_path)
self.verbose = verbose
self.results = {}
def run(self) -> Dict:
"""Execute the main functionality"""
print(f"🚀 Running {self.__class__.__name__}...")
print(f"📁 Target: {self.target_path}")
try:
self.validate_target()
self.analyze()
self.generate_report()
print("✅ Completed successfully!")
return self.results
except Exception as e:
print(f"❌ Error: {e}")
sys.exit(1)
def validate_target(self):
"""Validate the target path exists and is accessible"""
if not self.target_path.exists():
raise ValueError(f"Target path does not exist: {self.target_path}")
if self.verbose:
print(f"✓ Target validated: {self.target_path}")
def analyze(self):
"""Perform the main analysis or operation"""
if self.verbose:
print("📊 Analyzing...")
# Main logic here
self.results['status'] = 'success'
self.results['target'] = str(self.target_path)
self.results['findings'] = []
# Add analysis results
if self.verbose:
print(f"✓ Analysis complete: {len(self.results.get('findings', []))} findings")
def generate_report(self):
"""Generate and display the report"""
print("\n" + "="*50)
print("REPORT")
print("="*50)
print(f"Target: {self.results.get('target')}")
print(f"Status: {self.results.get('status')}")
print(f"Findings: {len(self.results.get('findings', []))}")
print("="*50 + "\n")
def main():
"""Main entry point"""
parser = argparse.ArgumentParser(
description="E2E Test Scaffolder"
)
parser.add_argument(
'target',
help='Target path to analyze or process'
)
parser.add_argument(
'--verbose', '-v',
action='store_true',
help='Enable verbose output'
)
parser.add_argument(
'--json',
action='store_true',
help='Output results as JSON'
)
parser.add_argument(
'--output', '-o',
help='Output file path'
)
args = parser.parse_args()
tool = E2ETestScaffolder(
args.target,
verbose=args.verbose
)
results = tool.run()
if args.json:
output = json.dumps(results, indent=2)
if args.output:
with open(args.output, 'w') as f:
f.write(output)
print(f"Results written to {args.output}")
else:
print(output)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,114 @@
#!/usr/bin/env python3
"""
Test Suite Generator
Automated tool for senior qa tasks
"""
import os
import sys
import json
import argparse
from pathlib import Path
from typing import Dict, List, Optional
class TestSuiteGenerator:
"""Main class for test suite generator functionality"""
def __init__(self, target_path: str, verbose: bool = False):
self.target_path = Path(target_path)
self.verbose = verbose
self.results = {}
def run(self) -> Dict:
"""Execute the main functionality"""
print(f"🚀 Running {self.__class__.__name__}...")
print(f"📁 Target: {self.target_path}")
try:
self.validate_target()
self.analyze()
self.generate_report()
print("✅ Completed successfully!")
return self.results
except Exception as e:
print(f"❌ Error: {e}")
sys.exit(1)
def validate_target(self):
"""Validate the target path exists and is accessible"""
if not self.target_path.exists():
raise ValueError(f"Target path does not exist: {self.target_path}")
if self.verbose:
print(f"✓ Target validated: {self.target_path}")
def analyze(self):
"""Perform the main analysis or operation"""
if self.verbose:
print("📊 Analyzing...")
# Main logic here
self.results['status'] = 'success'
self.results['target'] = str(self.target_path)
self.results['findings'] = []
# Add analysis results
if self.verbose:
print(f"✓ Analysis complete: {len(self.results.get('findings', []))} findings")
def generate_report(self):
"""Generate and display the report"""
print("\n" + "="*50)
print("REPORT")
print("="*50)
print(f"Target: {self.results.get('target')}")
print(f"Status: {self.results.get('status')}")
print(f"Findings: {len(self.results.get('findings', []))}")
print("="*50 + "\n")
def main():
"""Main entry point"""
parser = argparse.ArgumentParser(
description="Test Suite Generator"
)
parser.add_argument(
'target',
help='Target path to analyze or process'
)
parser.add_argument(
'--verbose', '-v',
action='store_true',
help='Enable verbose output'
)
parser.add_argument(
'--json',
action='store_true',
help='Output results as JSON'
)
parser.add_argument(
'--output', '-o',
help='Output file path'
)
args = parser.parse_args()
tool = TestSuiteGenerator(
args.target,
verbose=args.verbose
)
results = tool.run()
if args.json:
output = json.dumps(results, indent=2)
if args.output:
with open(args.output, 'w') as f:
f.write(output)
print(f"Results written to {args.output}")
else:
print(output)
if __name__ == '__main__':
main()

12
.env.example Normal file
View File

@@ -0,0 +1,12 @@
# NextAuth Configuration
# Generate a secret with: openssl rand -base64 32
NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_SECRET=your-secret-key-here
# Backend API URL
NEXT_PUBLIC_API_URL=http://localhost:3001/api
# Auth Mode: true = login required, false = public access with optional login
NEXT_PUBLIC_AUTH_REQUIRED=false
NEXT_PUBLIC_GOOGLE_API_KEY='api-key'

7
.gitignore vendored Normal file
View File

@@ -0,0 +1,7 @@
node_modules
.next
certificates
.env.local

12
.mcp.json Normal file
View File

@@ -0,0 +1,12 @@
{
"mcpServers": {
"DeepGraph Next.js MCP": {
"command": "npx",
"args": [
"-y",
"mcp-code-graph@latest",
"vercel/next.js"
]
}
}
}

341
README.md Normal file
View File

@@ -0,0 +1,341 @@
# 🚀 Enterprise Next.js Boilerplate (Antigravity Edition)
[![Next.js](https://img.shields.io/badge/Next.js%2016-000000?style=for-the-badge&logo=next.js&logoColor=white)](https://nextjs.org/)
[![React](https://img.shields.io/badge/React%2019-61DAFB?style=for-the-badge&logo=react&logoColor=black)](https://react.dev/)
[![Chakra UI](https://img.shields.io/badge/Chakra%20UI%20v3-319795?style=for-the-badge&logo=chakraui&logoColor=white)](https://chakra-ui.com/)
[![TypeScript](https://img.shields.io/badge/TypeScript-3178C6?style=for-the-badge&logo=typescript&logoColor=white)](https://www.typescriptlang.org/)
[![next-intl](https://img.shields.io/badge/next--intl-0070F3?style=for-the-badge&logo=next.js&logoColor=white)](https://next-intl-docs.vercel.app/)
> **FOR AI AGENTS & DEVELOPERS:** This documentation is structured to provide deep context, architectural decisions, and operational details to ensure seamless handover to any AI coding assistant (like Antigravity) or human developer.
---
## 🧠 Project Context & Architecture (Read Me First)
This is an **opinionated, production-ready** frontend boilerplate built with Next.js 16 App Router. It is designed to work seamlessly with the companion **typescript-boilerplate-be** NestJS backend.
### 🏗️ Core Philosophy
- **Type Safety First:** Strict TypeScript configuration. DTOs and typed API responses connect frontend to backend.
- **App Router Native:** Built entirely on Next.js App Router with Server Components and React Server Actions support.
- **i18n Native:** Localization is baked into routing, API calls, and UI components via `next-intl`.
- **Theme-First Design:** Chakra UI v3 with custom theme system for consistent, accessible UI.
- **Auth by Default:** NextAuth.js integration with JWT token management and automatic session handling.
### 📐 Architectural Decision Records (ADR)
_To understand WHY things are the way they are:_
1. **Locale-Based Routing:**
- **Mechanism:** All routes are prefixed with locale (e.g., `/tr/dashboard`, `/en/login`).
- **Implementation:** `next-intl` plugin with `[locale]` dynamic segment in `app/` directory.
- **Config:** `localePrefix: 'always'` ensures consistent URL structure.
2. **API Client with Auto-Auth & Locale:**
- **Location:** `src/lib/api/create-api-client.ts`
- **Feature:** Automatically injects JWT `Authorization` header from NextAuth session.
- **Feature:** Automatically sets `Accept-Language` header based on current locale (cookie or URL path).
- **Auto-Logout:** 401 responses trigger automatic signOut and redirect.
3. **Backend Proxy (CORS Solution):**
- **Problem:** Local development CORS issues when frontend (port 3001) calls backend (port 3000).
- **Solution:** Next.js `rewrites` in `next.config.ts` proxies `/api/backend/*``http://localhost:3000/api/*`.
- **Usage:** API clients use `/api/backend/...` paths in development.
4. **Provider Composition:**
- **Location:** `src/components/ui/provider.tsx`
- **Stack:** `SessionProvider` (NextAuth) → `ChakraProvider` (UI) → `ColorModeProvider` (Dark/Light) → `Toaster` (Notifications)
- **Why:** Single import in layout provides all necessary contexts.
---
## 🚀 Quick Start for AI & Humans
### 1. Prerequisites
- **Node.js:** v20.19+ (LTS)
- **Package Manager:** `npm` (Lockfile: `package-lock.json`)
- **Backend:** Companion NestJS backend running on port 3000 (see `typescript-boilerplate-be`)
### 2. Environment Setup
```bash
cp .env.example .env.local
# Edit .env.local with your configuration
```
**Required Environment Variables:**
| Variable | Description | Example |
| --------------------------- | --------------------------------------------------------------- | --------------------------- |
| `NEXTAUTH_URL` | NextAuth callback URL | `http://localhost:3001` |
| `NEXTAUTH_SECRET` | Authentication secret (generate with `openssl rand -base64 32`) | `abc123...` |
| `NEXT_PUBLIC_API_URL` | Backend API base URL | `http://localhost:3001/api` |
| `NEXT_PUBLIC_AUTH_REQUIRED` | Require login for all pages | `true` or `false` |
### 3. Installation & Running
```bash
# Install dependencies
npm ci
# Development Mode (HTTPS enabled, port 3001)
npm run dev
# Production Build
npm run build
npm run start
```
> **Note:** Dev server runs on port 3001 with experimental HTTPS for secure cookie handling.
---
## 🌍 Internationalization (i18n) Guide
Deep integration with `next-intl` provides locale-aware routing and translations.
### Configuration
- **Supported Locales:** `['en', 'tr']`
- **Default Locale:** `tr`
- **Locale Detection:** Cookie (`NEXT_LOCALE`) → URL path → Default
### File Structure
```
messages/
├── en.json # English translations
└── tr.json # Turkish translations
src/i18n/
├── routing.ts # Locale routing configuration
├── navigation.ts # Typed navigation helpers (Link, useRouter)
└── request.ts # Server-side i18n setup
```
### Usage in Components
```tsx
// Client Component
"use client";
import { useTranslations } from "next-intl";
export function MyComponent() {
const t = useTranslations("common");
return <h1>{t("welcome")}</h1>;
}
// Navigation with Locale
import { Link } from "@/i18n/navigation";
<Link href="/dashboard">Dashboard</Link>; // Auto-prefixes with current locale
```
### Adding a New Locale
1. Add locale code to `src/i18n/routing.ts`:
```ts
export const locales = ["en", "tr", "de"]; // Add 'de'
```
2. Create `messages/de.json` with translations.
3. Update `getLocale()` in `create-api-client.ts` if needed.
---
## 🔐 Authentication System
Built on NextAuth.js with JWT strategy for seamless backend integration.
### How It Works
1. **Login:** User submits credentials → `/api/auth/[...nextauth]` → Backend validates → JWT returned.
2. **Session:** JWT stored in encrypted cookie, accessible via `useSession()` hook.
3. **API Calls:** `createApiClient()` automatically adds `Authorization: Bearer {token}` header.
4. **Auto-Refresh:** Token refreshed before expiry (managed by NextAuth).
### Protected Routes
```tsx
// Use middleware.ts or check session in layout
import { getServerSession } from "next-auth";
export default async function ProtectedLayout({ children }) {
const session = await getServerSession();
if (!session) redirect("/login");
return <>{children}</>;
}
```
### Auth Mode Toggle
Set `NEXT_PUBLIC_AUTH_REQUIRED=true` for mandatory authentication across all pages.
---
## 🎨 UI System (Chakra UI v3)
### Theme Configuration
- **Location:** `src/theme/theme.ts`
- **Font:** Bricolage Grotesque (Google Fonts, variable font)
- **Color Mode:** Light/Dark with system preference detection
### Component Organization
```
src/components/
├── ui/ # Chakra UI wrapper components
│ ├── provider.tsx # Root provider (Session + Chakra + Theme)
│ ├── color-mode.tsx # Dark/Light mode toggle
│ ├── feedback/ # Toaster, alerts, etc.
│ └── ...
├── layout/ # Page layout components (Header, Sidebar, Footer)
├── auth/ # Authentication forms and guards
└── site/ # Site-specific feature components
```
### Toast Notifications
```tsx
import { toaster } from "@/components/ui/feedback/toaster";
// Success toast
toaster.success({
title: "Saved!",
description: "Changes saved successfully.",
});
// Error toast
toaster.error({ title: "Error", description: "Something went wrong." });
```
---
## 📡 API Integration
### Creating API Clients
```tsx
// src/lib/api/auth/login/queries.ts
import { createApiClient } from "../create-api-client";
const api = createApiClient("/api/backend/auth");
export const loginUser = async (data: LoginDto) => {
const response = await api.post("/login", data);
return response.data;
};
```
### React Query Integration
```tsx
import { useQuery, useMutation } from "@tanstack/react-query";
// Query hook
export const useUsers = () =>
useQuery({
queryKey: ["users"],
queryFn: () => apiClient.get("/users"),
});
// Mutation hook
export const useCreateUser = () =>
useMutation({
mutationFn: (data: CreateUserDto) => apiClient.post("/users", data),
});
```
---
## 📂 System Map (Directory Structure)
```
src/
├── app/ # Next.js App Router
│ ├── [locale]/ # Locale-prefixed routes
│ │ ├── (auth)/ # Auth pages (login, register)
│ │ ├── (site)/ # Main site pages
│ │ ├── (error)/ # Error pages
│ │ ├── layout.tsx # Root layout with providers
│ │ └── page.tsx # Home page
│ └── api/ # Next.js API routes
│ └── auth/ # NextAuth endpoints
├── components/ # React components
│ ├── ui/ # Chakra UI wrappers & base components
│ ├── layout/ # Layout components (Header, Sidebar)
│ ├── auth/ # Auth-related components
│ └── site/ # Feature-specific components
├── config/ # App configuration
├── hooks/ # Custom React hooks
├── i18n/ # Internationalization setup
├── lib/ # Utilities & services
│ ├── api/ # API clients (organized by module)
│ │ ├── auth/ # Auth API (login, register)
│ │ ├── admin/ # Admin API (roles, users)
│ │ └── create-api-client.ts # Axios factory with interceptors
│ ├── services/ # Business logic services
│ └── utils/ # Helper functions
├── theme/ # Chakra UI theme configuration
├── types/ # TypeScript type definitions
└── proxy.ts # API proxy utilities
messages/ # Translation JSON files
public/ # Static assets
```
---
## 🧪 Testing & Development
### Available Scripts
```bash
npm run dev # Start dev server (HTTPS, port 3001)
npm run build # Production build
npm run start # Start production server
npm run lint # Run ESLint
```
### Development Tools
- **React Compiler:** Enabled for automatic optimization.
- **Top Loader:** Visual loading indicator for route transitions.
- **ESLint + Prettier:** Consistent code formatting.
---
## 🛠️ Troubleshooting (Known Issues)
**1. HTTPS Certificate Warnings in Development**
- **Context:** Dev server uses `--experimental-https` for secure cookies.
- **Fix:** Accept the self-signed certificate warning in your browser.
**2. Backend Connection Refused**
- **Fix:** Ensure NestJS backend is running on port 3000.
- **Check:** Run `curl http://localhost:3000/api/health` to verify.
**3. Session Not Persisting**
- **Fix:** Ensure `NEXTAUTH_SECRET` is set and consistent between restarts.
- **Fix:** Check that cookies are not blocked by browser settings.
**4. Locale Not Detected**
- **Context:** Default falls back to `tr` if cookie/URL detection fails.
- **Fix:** Clear `NEXT_LOCALE` cookie and refresh.
---
## 🔗 Related Projects
- **Backend:** [typescript-boilerplate-be](../typescript-boilerplate-be) - NestJS backend with Prisma, JWT auth, and i18n.
---
## 📃 License
This project is proprietary and confidential.

25
eslint.config.mjs Normal file
View File

@@ -0,0 +1,25 @@
import { dirname } from 'path';
import { fileURLToPath } from 'url';
import { FlatCompat } from '@eslint/eslintrc';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const compat = new FlatCompat({
baseDirectory: __dirname,
});
const eslintConfig = [
...compat.extends('next/core-web-vitals', 'next/typescript', 'prettier'),
{
ignores: ['node_modules/**', '.next/**', 'out/**', 'build/**', 'next-env.d.ts'],
},
{
rules: {
'@typescript-eslint/no-empty-object-type': 'off',
'@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }],
},
},
];
export default eslintConfig;

32
messages/en.json Normal file
View File

@@ -0,0 +1,32 @@
{
"home": "Home",
"about": "About",
"solutions": "Solutions",
"intelligent-transportation-systems": "Intelligent Transportation Systems",
"artificial-intelligence": "Artificial Intelligence",
"error": {
"not-found": "Oops! The page youre looking for doesnt exist.",
"404": "404",
"back-to-home": "Go back home"
},
"email": "E-Mail",
"password": "Password",
"auth": {
"remember-me": "Remember Me",
"dont-have-account": "Don't have an account?",
"sign-out": "Sign Out",
"sign-up": "Sign Up",
"sign-in": "Sign In",
"welcome-back": "Welcome Back",
"subtitle": "Enter your email and password to sign in",
"already-have-an-account": "Already have an account?",
"create-an-account-now": "Create an account now"
},
"all-right-reserved": "All rights reserved.",
"privacy-policy": "Privacy Policy",
"terms-of-service": "Terms of Service",
"name": "Name",
"low": "Low",
"medium": "Medium",
"high": "High"
}

32
messages/tr.json Normal file
View File

@@ -0,0 +1,32 @@
{
"home": "Anasayfa",
"about": "Hakkımızda",
"solutions": "Çözümler",
"intelligent-transportation-systems": "Akıllı Ulaşım Sistemleri",
"artificial-intelligence": "Yapay Zeka",
"error": {
"not-found": "Üzgünüz, aradığınız sayfa bulunamadı.",
"404": "404",
"back-to-home": "Ana sayfaya dön"
},
"email": "E-Posta",
"password": "Şifre",
"auth": {
"remember-me": "Beni Hatırla",
"dont-have-account": "Hesabınız yok mu?",
"sign-out": ıkış Yap",
"sign-up": "Kaydol",
"sign-in": "Giriş Yap",
"welcome-back": "Hoş Geldiniz",
"subtitle": "Giriş yapmak için e-postanızı ve şifrenizi giriniz",
"already-have-an-account": "Zaten bir hesabınız var mı?",
"create-an-account-now": "Hemen bir hesap oluşturun"
},
"all-right-reserved": "Tüm hakları saklıdır.",
"privacy-policy": "Gizlilik Politikası",
"terms-of-service": "Hizmet Şartları",
"name": "İsim",
"low": "Düşük",
"medium": "Orta",
"high": "Yüksek"
}

6
next-env.d.ts vendored Normal file
View File

@@ -0,0 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

20
next.config.ts Normal file
View File

@@ -0,0 +1,20 @@
import type { NextConfig } from "next";
import createNextIntlPlugin from "next-intl/plugin";
const nextConfig: NextConfig = {
experimental: {
optimizePackageImports: ["@chakra-ui/react"],
},
reactCompiler: true,
async rewrites() {
return [
{
source: "/api/backend/:path*",
destination: "http://localhost:3000/api/:path*",
},
];
},
};
const withNextIntl = createNextIntlPlugin();
export default withNextIntl(nextConfig);

10345
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

44
package.json Normal file
View File

@@ -0,0 +1,44 @@
{
"name": "my-frontend-app",
"version": "0.0.1",
"private": true,
"scripts": {
"dev": "next dev --webpack --experimental-https -p 3001",
"build": "next build --webpack",
"start": "next start",
"lint": "eslint"
},
"dependencies": {
"@chakra-ui/react": "^3.28.0",
"@emotion/react": "^11.14.0",
"@google/genai": "^1.35.0",
"@hookform/resolvers": "^5.2.2",
"@tanstack/react-query": "^5.90.16",
"axios": "^1.13.1",
"i18next": "^25.6.0",
"next": "16.0.0",
"next-auth": "^4.24.13",
"next-intl": "^4.4.0",
"next-themes": "^0.4.6",
"nextjs-toploader": "^3.9.17",
"react": "19.2.0",
"react-dom": "19.2.0",
"react-hook-form": "^7.65.0",
"react-icons": "^5.5.0",
"yup": "^1.7.1"
},
"devDependencies": {
"@chakra-ui/cli": "^3.27.1",
"@eslint/eslintrc": "^3.3.1",
"@types/node": "^20",
"@types/react": "19.2.2",
"@types/react-dom": "19.2.2",
"babel-plugin-react-compiler": "^1.0.0",
"eslint": "^9.37.0",
"eslint-config-next": "16.0.0",
"eslint-config-prettier": "^10.1.8",
"prettier": "^3.6.2",
"typescript": "^5"
},
"description": "Generated by Frontend CLI"
}

110
prompt.md Normal file
View File

@@ -0,0 +1,110 @@
# 🤖 AI Assistant Context - Next.js Frontend
> Bu dosya, AI asistanların (Claude, GPT, Gemini vb.) projeyi hızlıca anlaması için hazırlanmış bir referans dökümanıdır.
---
## 📚 Projeyi Anlamak İçin Önce Oku
1. **README.md** dosyasını oku - Projenin mimarisi, teknoloji stack'i ve kurulum adımlarını içerir.
```
README.md
```
---
## 🎯 Referans Klasörü
`.claude/` klasörü best practice'ler, agent tanımları ve yardımcı scriptler içerir. Görev türüne göre ilgili referansları kullan:
### Skills (Beceri Setleri)
| Beceri | Konum | Ne Zaman Kullan |
| ------------------- | --------------------------------- | ------------------------------------------ |
| **Senior QA** | `.claude/skills/senior-qa/` | Test yazarken, coverage analizi yaparken |
| **Senior Frontend** | `.claude/skills/senior-frontend/` | Component geliştirirken, UI best practices |
| **Frontend Design** | `.claude/skills/frontend-design/` | Tasarım kararları alırken |
### Agents (Roller)
| Agent | Konum | Açıklama |
| ------------------------------- | ---------------------------------------------- | ------------------------- |
| **Frontend Developer** | `.claude/agents/frontend-developer.md` | Genel frontend geliştirme |
| **Next.js Architecture Expert** | `.claude/agents/nextjs-architecture-expert.md` | Mimari kararlar |
### Commands (Komutlar)
| Komut | Konum | Açıklama |
| ----------------------- | ------------------------------------------------ | ------------------------ |
| **Component Generator** | `.claude/commands/nextjs-component-generator.md` | Yeni component oluşturma |
| **API Tester** | `.claude/commands/nextjs-api-tester.md` | API endpoint test etme |
---
## 🔧 Teknoloji Stack'i (Özet)
- **Framework:** Next.js 16 (App Router)
- **UI Library:** Chakra UI v3
- **State Management:** React Query (TanStack)
- **Auth:** NextAuth.js
- **i18n:** next-intl
- **Language:** TypeScript (Strict Mode)
---
## 🏗️ Proje Yapısı Özeti
```
src/
├── app/[locale]/ # Locale-based routing
├── components/ # UI components
├── lib/api/ # API clients
├── i18n/ # Internationalization
└── theme/ # Chakra UI theme
```
---
## ✅ Görev Bazlı Referans Kullanımı
**Test yazarken:**
```
.claude/skills/senior-qa/references/testing_strategies.md
.claude/skills/senior-qa/references/test_automation_patterns.md
```
**Component geliştirirken:**
```
.claude/skills/senior-frontend/SKILL.md
.claude/skills/frontend-design/SKILL.md
```
**Mimari kararlar alırken:**
```
.claude/agents/nextjs-architecture-expert.md
README.md (ADR bölümü)
```
---
## 💡 Örnek Prompt'lar
### Yeni Component Oluşturma
> "`.claude/skills/senior-frontend/` referanslarını kullanarak, reusable bir `DataTable` component'i oluştur."
### Test Yazma
> "`.claude/skills/senior-qa/references/testing_strategies.md` pattern'lerini kullanarak `LoginForm` için unit test yaz."
### Code Review
> "`.claude/skills/senior-frontend/` best practice'lerine göre `src/components/auth/` klasörünü review et."
---
**Backend Projesi:** `../typescript-boilerplate-be/prompt.md`

BIN
public/.DS_Store vendored Normal file

Binary file not shown.

BIN
public/assets/.DS_Store vendored Normal file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 558 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
public/favicon/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,19 @@
{
"name": "",
"short_name": "",
"icons": [
{
"src": "/favicon/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/favicon/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
}

BIN
src/.DS_Store vendored Normal file

Binary file not shown.

BIN
src/app/.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -0,0 +1,15 @@
'use client';
import Footer from '@/components/layout/footer/footer';
import { Box, Flex } from '@chakra-ui/react';
function AuthLayout({ children }: { children: React.ReactNode }) {
return (
<Flex minH='100vh' direction='column'>
<Box as='main'>{children}</Box>
<Footer />
</Flex>
);
}
export default AuthLayout;

View File

@@ -0,0 +1,231 @@
"use client";
import {
Box,
Flex,
Heading,
Input,
Link as ChakraLink,
Text,
ClientOnly,
} from "@chakra-ui/react";
import { Button } from "@/components/ui/buttons/button";
import { Switch } from "@/components/ui/forms/switch";
import { Field } from "@/components/ui/forms/field";
import { useTranslations } from "next-intl";
import signInImage from "/public/assets/img/sign-in-image.png";
import { InputGroup } from "@/components/ui/forms/input-group";
import { BiLock } from "react-icons/bi";
import { useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup";
import { Link, useRouter } from "@/i18n/navigation";
import { MdMail } from "react-icons/md";
import { PasswordInput } from "@/components/ui/forms/password-input";
import { Skeleton } from "@/components/ui/feedback/skeleton";
import { signIn } from "next-auth/react";
import { toaster } from "@/components/ui/feedback/toaster";
import { useState } from "react";
const schema = yup.object({
email: yup.string().email().required(),
password: yup.string().required(),
});
type SignInForm = yup.InferType<typeof schema>;
const defaultValues = {
email: "test@test.com.tr",
password: "test1234",
};
function SignInPage() {
const t = useTranslations();
const router = useRouter();
const [loading, setLoading] = useState(false);
const {
handleSubmit,
register,
formState: { errors },
} = useForm<SignInForm>({
resolver: yupResolver(schema),
mode: "onChange",
defaultValues,
});
const onSubmit = async (formData: SignInForm) => {
try {
setLoading(true);
const res = await signIn("credentials", {
redirect: false,
email: formData.email,
password: formData.password,
});
if (res?.error) {
throw new Error(res.error);
}
router.replace("/home");
} catch (error) {
toaster.error({
title: (error as Error).message || "Giriş yaparken hata oluştu!",
type: "error",
});
} finally {
setLoading(false);
}
};
return (
<Box position="relative">
<Flex
h={{ sm: "initial", md: "75vh", lg: "85vh" }}
w="100%"
maxW="1044px"
mx="auto"
justifyContent="space-between"
mb="30px"
pt={{ sm: "100px", md: "0px" }}
>
<Flex
as="form"
onSubmit={handleSubmit(onSubmit)}
alignItems="center"
justifyContent="start"
style={{ userSelect: "none" }}
w={{ base: "100%", md: "50%", lg: "42%" }}
>
<Flex
direction="column"
w="100%"
background="transparent"
p="10"
mt={{ md: "150px", lg: "80px" }}
>
<Heading
color={{ base: "primary.400", _dark: "primary.200" }}
fontSize="32px"
mb="10px"
fontWeight="bold"
>
{t("auth.welcome-back")}
</Heading>
<Text
mb="36px"
ms="4px"
color={{ base: "gray.400", _dark: "white" }}
fontWeight="bold"
fontSize="14px"
>
{t("auth.subtitle")}
</Text>
<Field
mb="24px"
label={t("email")}
errorText={errors.email?.message}
invalid={!!errors.email}
>
<InputGroup w="full" startElement={<MdMail size="1rem" />}>
<Input
borderRadius="15px"
fontSize="sm"
type="text"
placeholder={t("email")}
size="lg"
{...register("email")}
/>
</InputGroup>
</Field>
<Field
mb="24px"
label={t("password")}
errorText={errors.password?.message}
invalid={!!errors.password}
>
<InputGroup w="full" startElement={<BiLock size="1rem" />}>
<PasswordInput
borderRadius="15px"
fontSize="sm"
placeholder={t("password")}
size="lg"
{...register("password")}
/>
</InputGroup>
</Field>
<Field mb="24px">
<Switch colorPalette="teal" label={t("auth.remember-me")}>
{t("auth.remember-me")}
</Switch>
</Field>
<Field mb="24px">
<ClientOnly fallback={<Skeleton height="45px" width="100%" />}>
<Button
loading={loading}
type="submit"
bg="primary.400"
w="100%"
h="45px"
color="white"
_hover={{
bg: "primary.500",
}}
_active={{
bg: "primary.400",
}}
>
{t("auth.sign-in")}
</Button>
</ClientOnly>
</Field>
<Flex
flexDirection="column"
justifyContent="center"
alignItems="center"
maxW="100%"
>
<Text
color={{ base: "gray.400", _dark: "white" }}
fontWeight="medium"
>
{t("auth.dont-have-account")}
<ChakraLink
as={Link}
href="/signup"
color={{ base: "primary.400", _dark: "primary.200" }}
ms="5px"
fontWeight="bold"
focusRing="none"
>
{t("auth.sign-up")}
</ChakraLink>
</Text>
</Flex>
</Flex>
</Flex>
<Box
display={{ base: "none", md: "block" }}
overflowX="hidden"
h="100%"
w="40vw"
position="absolute"
right="0px"
>
<Box
bgImage={`url(${signInImage.src})`}
w="100%"
h="100%"
bgSize="cover"
bgPos="50%"
position="absolute"
borderBottomLeftRadius="20px"
/>
</Box>
</Flex>
</Box>
);
}
export default SignInPage;

View File

@@ -0,0 +1,166 @@
'use client';
import { Box, Flex, Input, Link as ChakraLink, Text, ClientOnly } from '@chakra-ui/react';
import signUpImage from '/public/assets/img/sign-up-image.png';
import { Button } from '@/components/ui/buttons/button';
import { Field } from '@/components/ui/forms/field';
import { useTranslations } from 'next-intl';
import { useForm } from 'react-hook-form';
import * as yup from 'yup';
import { yupResolver } from '@hookform/resolvers/yup';
import { InputGroup } from '@/components/ui/forms/input-group';
import { BiLock, BiUser } from 'react-icons/bi';
import { Link } from '@/i18n/navigation';
import { MdMail } from 'react-icons/md';
import { useRouter } from 'next/navigation';
import { PasswordInput } from '@/components/ui/forms/password-input';
import { Skeleton } from '@/components/ui/feedback/skeleton';
const schema = yup.object({
name: yup.string().required(),
email: yup.string().email().required(),
password: yup.string().required(),
});
type SignUpForm = yup.InferType<typeof schema>;
function SignUpPage() {
const t = useTranslations();
const router = useRouter();
const {
handleSubmit,
register,
formState: { errors },
} = useForm<SignUpForm>({ resolver: yupResolver(schema), mode: 'onChange' });
const onSubmit = async (formData: SignUpForm) => {
router.replace('/signin');
return formData;
};
return (
<Box>
<Box
position='absolute'
minH={{ base: '70vh', md: '50vh' }}
w={{ md: 'calc(100vw - 50px)' }}
borderRadius={{ md: '15px' }}
left='0'
right='0'
bgRepeat='no-repeat'
overflow='hidden'
zIndex='-1'
top='0'
bgImage={`url(${signUpImage.src})`}
bgSize='cover'
mx={{ md: 'auto' }}
mt={{ md: '14px' }}
/>
<Flex w='full' h='full' direction='column' alignItems='center' justifyContent='center'>
<Text
fontSize={{ base: '2xl', md: '3xl', lg: '4xl' }}
color='white'
fontWeight='bold'
mt={{ base: '2rem', md: '4.5rem', '2xl': '6.5rem' }}
mb={{ base: '2rem', md: '3rem', '2xl': '4rem' }}
>
{t('auth.create-an-account-now')}
</Text>
<Flex
direction='column'
w={{ base: '100%', md: '445px' }}
background='transparent'
borderRadius='15px'
p='10'
mx={{ base: '100px' }}
bg='bg.panel'
boxShadow='0 20px 27px 0 rgb(0 0 0 / 5%)'
mb='8'
>
<Flex
as='form'
onSubmit={handleSubmit(onSubmit)}
flexDirection='column'
alignItems='center'
justifyContent='start'
w='100%'
>
<Field mb='24px' label={t('name')} errorText={errors.name?.message} invalid={!!errors.name}>
<InputGroup w='full' startElement={<BiUser size='1rem' />}>
<Input
borderRadius='15px'
fontSize='sm'
type='text'
placeholder={t('name')}
size='lg'
{...register('name')}
/>
</InputGroup>
</Field>
<Field mb='24px' label={t('email')} errorText={errors.email?.message} invalid={!!errors.email}>
<InputGroup w='full' startElement={<MdMail size='1rem' />}>
<Input
borderRadius='15px'
fontSize='sm'
type='text'
placeholder={t('email')}
size='lg'
{...register('email')}
/>
</InputGroup>
</Field>
<Field mb='24px' label={t('password')} errorText={errors.password?.message} invalid={!!errors.password}>
<InputGroup w='full' startElement={<BiLock size='1rem' />}>
<PasswordInput
borderRadius='15px'
fontSize='sm'
placeholder={t('password')}
size='lg'
{...register('password')}
/>
</InputGroup>
</Field>
<Field mb='24px'>
<ClientOnly fallback={<Skeleton height='45px' width='100%' />}>
<Button
type='submit'
bg='primary.400'
color='white'
fontWeight='bold'
w='100%'
h='45px'
_hover={{
bg: 'primary.500',
}}
_active={{
bg: 'primary.400',
}}
>
{t('auth.sign-up')}
</Button>
</ClientOnly>
</Field>
</Flex>
<Flex flexDirection='column' justifyContent='center' alignItems='center' maxW='100%'>
<Text color={{ base: 'gray.400', _dark: 'white' }} fontWeight='medium'>
{t('auth.already-have-an-account')}
<ChakraLink
as={Link}
color={{ base: 'primary.400', _dark: 'primary.200' }}
ml='2'
href='/signin'
fontWeight='bold'
focusRing='none'
>
{t('auth.sign-in')}
</ChakraLink>
</Text>
</Flex>
</Flex>
</Flex>
</Box>
);
}
export default SignUpPage;

View File

@@ -0,0 +1,5 @@
import { notFound } from 'next/navigation';
export default function CatchAllPage() {
notFound();
}

View File

@@ -0,0 +1,7 @@
import React from 'react';
function AboutPage() {
return <div>AboutPage</div>;
}
export default AboutPage;

View File

@@ -0,0 +1,14 @@
import { getTranslations } from "next-intl/server";
import HomeCard from "@/components/site/home/home-card";
export async function generateMetadata() {
const t = await getTranslations();
return {
title: `${t("home")} | FCS`,
};
}
export default function Home() {
return <HomeCard />;
}

View File

@@ -0,0 +1,21 @@
'use client';
import { Container, Flex } from '@chakra-ui/react';
import Header from '@/components/layout/header/header';
import Footer from '@/components/layout/footer/footer';
import BackToTop from '@/components/ui/back-to-top';
function MainLayout({ children }: { children: React.ReactNode }) {
return (
<Flex minH='100vh' direction='column'>
<Header />
<Container as='main' maxW='8xl' flex='1' py={4}>
{children}
</Container>
<BackToTop />
<Footer />
</Flex>
);
}
export default MainLayout;

BIN
src/app/[locale]/.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -0,0 +1,7 @@
html {
scroll-behavior: smooth;
}
body {
overflow-x: hidden;
}

View File

@@ -0,0 +1,41 @@
import { Provider } from '@/components/ui/provider';
import { Bricolage_Grotesque } from 'next/font/google';
import { hasLocale, NextIntlClientProvider } from 'next-intl';
import { notFound } from 'next/navigation';
import { routing } from '@/i18n/routing';
import { dir } from 'i18next';
import './global.css';
const bricolage = Bricolage_Grotesque({
variable: '--font-bricolage',
subsets: ['latin'],
});
export default async function RootLayout({
children,
params,
}: {
children: React.ReactNode;
params: Promise<{ locale: string }>;
}) {
const { locale } = await params;
if (!hasLocale(routing.locales, locale)) {
notFound();
}
return (
<html lang={locale} dir={dir(locale)} suppressHydrationWarning data-scroll-behavior='smooth'>
<head>
<link rel='apple-touch-icon' sizes='180x180' href='/favicon/apple-touch-icon.png' />
<link rel='icon' type='image/png' sizes='32x32' href='/favicon/favicon-32x32.png' />
<link rel='icon' type='image/png' sizes='16x16' href='/favicon/favicon-16x16.png' />
<link rel='manifest' href='/favicon/site.webmanifest' />
</head>
<body className={bricolage.variable}>
<NextIntlClientProvider>
<Provider>{children}</Provider>
</NextIntlClientProvider>
</body>
</html>
);
}

View File

@@ -0,0 +1,30 @@
import { Link } from '@/i18n/navigation';
import { Flex, Text, Button, VStack, Heading } from '@chakra-ui/react';
import { getTranslations } from 'next-intl/server';
export default async function NotFoundPage() {
const t = await getTranslations();
return (
<Flex h='100vh' alignItems='center' justifyContent='center' textAlign='center' px={6}>
<VStack spaceY={6}>
<Heading
as='h1'
fontSize={{ base: '5xl', md: '6xl' }}
fontWeight='bold'
color={{ base: 'primary.600', _dark: 'primary.400' }}
>
{t('error.404')}
</Heading>
<Text fontSize={{ base: 'md', md: 'lg' }} color={{ base: 'fg.muted', _dark: 'white' }}>
{t('error.not-found')}
</Text>
<Link href='/home' passHref>
<Button size={{ base: 'md', md: 'lg' }} rounded='md'>
{t('error.back-to-home')}
</Button>
</Link>
</VStack>
</Flex>
);
}

View File

@@ -0,0 +1,5 @@
import { redirect } from 'next/navigation';
export default async function Page() {
redirect('/home');
}

BIN
src/app/api/.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -0,0 +1,94 @@
import baseUrl from "@/config/base-url";
import { authService } from "@/lib/api/Example/auth/service";
import NextAuth from "next-auth";
import Credentials from "next-auth/providers/credentials";
function randomToken() {
return Math.random().toString(36).substring(2) + Date.now().toString(36);
}
const isMockMode = process.env.NEXT_PUBLIC_ENABLE_MOCK_MODE === "true";
const handler = NextAuth({
providers: [
Credentials({
name: "Credentials",
credentials: {
email: { label: "Email", type: "text" },
password: { label: "Password", type: "password" },
},
async authorize(credentials) {
console.log("credentials", credentials);
if (!credentials?.email || !credentials?.password) {
throw new Error("Email ve şifre gereklidir.");
}
// Eğer mock mod aktifse backend'e gitme
if (isMockMode) {
return {
id: credentials.email,
name: credentials.email.split("@")[0],
email: credentials.email,
accessToken: randomToken(),
refreshToken: randomToken(),
};
}
// Normal mod: backend'e istek at
const res = await authService.login({
email: credentials.email,
password: credentials.password,
});
console.log("res", res);
const response = res;
// Backend returns ApiResponse<TokenResponseDto>
// Structure: { data: { accessToken, refreshToken, expiresIn, user }, message, statusCode }
if (!res.success || !response?.data?.accessToken) {
throw new Error(response?.message || "Giriş başarısız");
}
const { accessToken, refreshToken, user } = response.data;
return {
id: user.id,
name: user.firstName
? `${user.firstName} ${user.lastName || ""}`.trim()
: user.email.split("@")[0],
email: user.email,
accessToken,
refreshToken,
roles: user.roles || [],
};
},
}),
],
callbacks: {
async jwt({ token, user }: any) {
if (user) {
token.accessToken = user.accessToken;
token.refreshToken = user.refreshToken;
token.id = user.id;
token.roles = user.roles;
}
return token;
},
async session({ session, token }: any) {
session.user.id = token.id;
session.user.roles = token.roles;
session.accessToken = token.accessToken;
session.refreshToken = token.refreshToken;
return session;
},
},
pages: {
signIn: "/signin",
error: "/signin",
},
session: { strategy: "jwt" },
secret: process.env.NEXTAUTH_SECRET,
});
export { handler as GET, handler as POST };

BIN
src/components/.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -0,0 +1,154 @@
"use client";
import { Box, Heading, Input, Text, VStack } from "@chakra-ui/react";
import { Button } from "@/components/ui/buttons/button";
import { Field } from "@/components/ui/forms/field";
import { InputGroup } from "@/components/ui/forms/input-group";
import { PasswordInput } from "@/components/ui/forms/password-input";
import {
DialogBody,
DialogCloseTrigger,
DialogContent,
DialogHeader,
DialogRoot,
DialogTitle,
} from "@/components/ui/overlays/dialog";
import { useTranslations } from "next-intl";
import { useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup";
import { signIn } from "next-auth/react";
import { toaster } from "@/components/ui/feedback/toaster";
import { useState } from "react";
import { MdMail } from "react-icons/md";
import { BiLock } from "react-icons/bi";
import { Link } from "@/i18n/navigation";
const schema = yup.object({
email: yup.string().email().required(),
password: yup.string().min(6).required(),
});
type LoginForm = yup.InferType<typeof schema>;
interface LoginModalProps {
open: boolean;
onOpenChange: (open: boolean) => void;
}
export function LoginModal({ open, onOpenChange }: LoginModalProps) {
const t = useTranslations();
const [loading, setLoading] = useState(false);
const {
handleSubmit,
register,
formState: { errors },
} = useForm<LoginForm>({
resolver: yupResolver(schema),
mode: "onChange",
});
const onSubmit = async (formData: LoginForm) => {
try {
setLoading(true);
const res = await signIn("credentials", {
redirect: false,
email: formData.email,
password: formData.password,
});
if (res?.error) {
throw new Error(res.error);
}
onOpenChange(false);
toaster.success({
title: t("auth.login-success") || "Login successful!",
type: "success",
});
} catch (error) {
toaster.error({
title: (error as Error).message || "Login failed!",
type: "error",
});
} finally {
setLoading(false);
}
};
return (
<DialogRoot open={open} onOpenChange={(e) => onOpenChange(e.open)}>
<DialogContent>
<DialogHeader>
<DialogTitle>
<Heading size="lg" color="primary.500">
{t("auth.sign-in")}
</Heading>
</DialogTitle>
<DialogCloseTrigger />
</DialogHeader>
<DialogBody>
<Box as="form" onSubmit={handleSubmit(onSubmit)}>
<VStack gap={4}>
<Field
label={t("email")}
errorText={errors.email?.message}
invalid={!!errors.email}
>
<InputGroup w="full" startElement={<MdMail size="1rem" />}>
<Input
borderRadius="md"
fontSize="sm"
type="text"
placeholder={t("email")}
{...register("email")}
/>
</InputGroup>
</Field>
<Field
label={t("password")}
errorText={errors.password?.message}
invalid={!!errors.password}
>
<InputGroup w="full" startElement={<BiLock size="1rem" />}>
<PasswordInput
borderRadius="md"
fontSize="sm"
placeholder={t("password")}
{...register("password")}
/>
</InputGroup>
</Field>
<Button
loading={loading}
type="submit"
bg="primary.400"
w="100%"
color="white"
_hover={{ bg: "primary.500" }}
>
{t("auth.sign-in")}
</Button>
<Text fontSize="sm" color="fg.muted">
{t("auth.dont-have-account")}{" "}
<Link
href="/signup"
style={{
color: "var(--chakra-colors-primary-500)",
fontWeight: "bold",
}}
>
{t("auth.sign-up")}
</Link>
</Text>
</VStack>
</Box>
</DialogBody>
</DialogContent>
</DialogRoot>
);
}

View File

@@ -0,0 +1,71 @@
import { Box, Text, HStack, Link as ChakraLink } from "@chakra-ui/react";
import { Link } from "@/i18n/navigation";
import { useTranslations } from "next-intl";
export default function Footer() {
const t = useTranslations();
return (
<Box as="footer" bg="bg.muted" mt="auto">
<HStack
display="flex"
justify={{ base: "center", md: "space-between" }}
alignContent="center"
maxW="8xl"
mx="auto"
wrap="wrap"
px={{ base: 4, md: 8 }}
position="relative"
minH="16"
>
<Text fontSize="sm" color="fg.muted">
© {new Date().getFullYear()}
<ChakraLink
target="_blank"
rel="noopener noreferrer"
href="https://www.fcs.com.tr"
color={{ base: "primary.500", _dark: "primary.300" }}
focusRing="none"
ml="1"
>
{"FCS"}
</ChakraLink>
. {t("all-right-reserved")}
</Text>
<HStack spaceX={4}>
<ChakraLink
as={Link}
href="/privacy"
fontSize="sm"
color="fg.muted"
focusRing="none"
position="relative"
textDecor="none"
transition="color 0.3s ease-in-out"
_hover={{
color: { base: "primary.500", _dark: "primary.300" },
}}
>
{t("privacy-policy")}
</ChakraLink>
<ChakraLink
as={Link}
href="/terms"
fontSize="sm"
color="fg.muted"
focusRing="none"
position="relative"
textDecor="none"
transition="color 0.3s ease-in-out"
_hover={{
color: { base: "primary.500", _dark: "primary.300" },
}}
>
{t("terms-of-service")}
</ChakraLink>
</HStack>
</HStack>
</Box>
);
}

View File

@@ -0,0 +1,104 @@
import { Box, Link as ChakraLink, Text } from '@chakra-ui/react';
import { NavItem } from '@/config/navigation';
import { MenuContent, MenuItem, MenuRoot, MenuTrigger } from '@/components/ui/overlays/menu';
import { RxChevronDown } from 'react-icons/rx';
import { useActiveNavItem } from '@/hooks/useActiveNavItem';
import { Link } from '@/i18n/navigation';
import { useTranslations } from 'next-intl';
import { useRef, useState } from 'react';
function HeaderLink({ item }: { item: NavItem }) {
const t = useTranslations();
const { isActive, isChildActive } = useActiveNavItem(item);
const [open, setOpen] = useState(false);
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
const handleMouseOpen = () => {
if (timeoutRef.current) clearTimeout(timeoutRef.current);
setOpen(true);
};
const handleMouseClose = () => {
if (timeoutRef.current) clearTimeout(timeoutRef.current);
timeoutRef.current = setTimeout(() => setOpen(false), 150);
};
return (
<Box key={item.label}>
{item.children ? (
<Box onMouseEnter={handleMouseOpen} onMouseLeave={handleMouseClose}>
<MenuRoot open={open} onOpenChange={(e) => setOpen(e.open)}>
<MenuTrigger asChild>
<Text
display='inline-flex'
alignItems='center'
gap='1'
cursor='pointer'
color={isActive ? { base: 'primary.500', _dark: 'primary.300' } : 'fg.muted'}
position='relative'
fontWeight='semibold'
>
{t(item.label)}
<RxChevronDown
style={{ transform: open ? 'rotate(-180deg)' : 'rotate(0deg)', transition: 'transform 0.2s' }}
/>
</Text>
</MenuTrigger>
<MenuContent>
{item.children.map((child, index) => {
const isActiveChild = isChildActive(child.href);
return (
<MenuItem key={index} value={child.href}>
<ChakraLink
key={index}
as={Link}
href={child.href}
focusRing='none'
w='full'
color={isActiveChild ? { base: 'primary.500', _dark: 'primary.300' } : 'fg.muted'}
position='relative'
textDecor='none'
fontWeight='semibold'
>
{t(child.label)}
</ChakraLink>
</MenuItem>
);
})}
</MenuContent>
</MenuRoot>
</Box>
) : (
<ChakraLink
as={Link}
href={item.href}
focusRing='none'
color={isActive ? { base: 'primary.500', _dark: 'primary.300' } : 'fg.muted'}
position='relative'
textDecor='none'
fontWeight='semibold'
_after={{
content: "''",
position: 'absolute',
left: 0,
bottom: '-2px',
width: '0%',
height: '1.5px',
bg: { base: 'primary.500', _dark: 'primary.300' },
transition: 'width 0.3s ease-in-out',
}}
_hover={{
_after: {
width: '100%',
},
}}
>
{t(item.label)}
</ChakraLink>
)}
</Box>
);
}
export default HeaderLink;

View File

@@ -0,0 +1,229 @@
"use client";
import {
Box,
Flex,
HStack,
IconButton,
Link as ChakraLink,
Stack,
VStack,
Button,
MenuItem,
ClientOnly,
} from "@chakra-ui/react";
import { Link, useRouter } from "@/i18n/navigation";
import { ColorModeButton } from "@/components/ui/color-mode";
import {
PopoverBody,
PopoverContent,
PopoverRoot,
PopoverTrigger,
} from "@/components/ui/overlays/popover";
import { RxHamburgerMenu } from "react-icons/rx";
import { NAV_ITEMS } from "@/config/navigation";
import HeaderLink from "./header-link";
import MobileHeaderLink from "./mobile-header-link";
import LocaleSwitcher from "@/components/ui/locale-switcher";
import { useEffect, useState } from "react";
import { useTranslations } from "next-intl";
import {
MenuContent,
MenuRoot,
MenuTrigger,
} from "@/components/ui/overlays/menu";
import { Avatar } from "@/components/ui/data-display/avatar";
import { Skeleton } from "@/components/ui/feedback/skeleton";
import { signOut, useSession } from "next-auth/react";
import { authConfig } from "@/config/auth";
import { LoginModal } from "@/components/auth/login-modal";
import { LuLogIn } from "react-icons/lu";
export default function Header() {
const t = useTranslations();
const [isSticky, setIsSticky] = useState(false);
const [loginModalOpen, setLoginModalOpen] = useState(false);
const router = useRouter();
const { data: session, status } = useSession();
const isAuthenticated = !!session;
const isLoading = status === "loading";
useEffect(() => {
const handleScroll = () => {
setIsSticky(window.scrollY >= 10);
};
window.addEventListener("scroll", handleScroll);
return () => {
window.removeEventListener("scroll", handleScroll);
};
}, []);
const handleLogout = async () => {
await signOut({ redirect: false });
if (authConfig.isAuthRequired) {
router.replace("/signin");
}
};
// Render user menu or login button based on auth state
const renderAuthSection = () => {
if (isLoading) {
return <Skeleton boxSize="10" rounded="full" />;
}
if (isAuthenticated) {
return (
<MenuRoot positioning={{ placement: "bottom-start" }}>
<MenuTrigger rounded="full" focusRing="none">
<Avatar name={session?.user?.name || "User"} variant="solid" />
</MenuTrigger>
<MenuContent>
<MenuItem onClick={handleLogout} value="sign-out">
{t("auth.sign-out")}
</MenuItem>
</MenuContent>
</MenuRoot>
);
}
// Not authenticated - show login button
return (
<Button
variant="solid"
colorPalette="primary"
size="sm"
onClick={() => setLoginModalOpen(true)}
>
<LuLogIn />
{t("auth.sign-in")}
</Button>
);
};
// Render mobile auth section
const renderMobileAuthSection = () => {
if (isLoading) {
return <Skeleton height="10" width="full" />;
}
if (isAuthenticated) {
return (
<>
<Avatar name={session?.user?.name || "User"} variant="solid" />
<Button
variant="surface"
size="sm"
width="full"
onClick={handleLogout}
>
{t("auth.sign-out")}
</Button>
</>
);
}
return (
<Button
variant="solid"
colorPalette="primary"
size="sm"
width="full"
onClick={() => setLoginModalOpen(true)}
>
<LuLogIn />
{t("auth.sign-in")}
</Button>
);
};
return (
<>
<Box
as="nav"
bg={isSticky ? "rgba(255, 255, 255, 0.6)" : "white"}
_dark={{
bg: isSticky ? "rgba(1, 1, 1, 0.6)" : "black",
}}
shadow={isSticky ? "sm" : "none"}
backdropFilter="blur(12px) saturate(180%)"
border="1px solid"
borderColor={isSticky ? "whiteAlpha.300" : "transparent"}
borderBottomRadius={isSticky ? "xl" : "none"}
transition="all 0.4s ease-in-out"
px={{ base: 4, md: 8 }}
py="3"
position="sticky"
top={0}
zIndex={10}
w="full"
>
<Flex justify="space-between" align="center" maxW="8xl" mx="auto">
{/* Logo */}
<HStack>
<ChakraLink
as={Link}
href="/home"
fontSize="lg"
fontWeight="bold"
color={{ base: "primary.500", _dark: "primary.300" }}
focusRing="none"
textDecor="none"
transition="all 0.3s ease-in-out"
_hover={{
color: { base: "primary.900", _dark: "primary.50" },
}}
>
{"FCS "}
</ChakraLink>
</HStack>
{/* DESKTOP NAVIGATION */}
<HStack spaceX={4} display={{ base: "none", lg: "flex" }}>
{NAV_ITEMS.map((item, index) => (
<HeaderLink key={index} item={item} />
))}
</HStack>
<HStack>
<ColorModeButton colorPalette="gray" />
<Box display={{ base: "none", lg: "inline-flex" }} gap={2}>
<LocaleSwitcher />
<ClientOnly fallback={<Skeleton boxSize="10" rounded="full" />}>
{renderAuthSection()}
</ClientOnly>
</Box>
{/* MOBILE NAVIGATION */}
<Stack display={{ base: "inline-flex", lg: "none" }}>
<ClientOnly fallback={<Skeleton boxSize="9" />}>
<PopoverRoot>
<PopoverTrigger as="span">
<IconButton aria-label="Open menu" variant="ghost">
<RxHamburgerMenu />
</IconButton>
</PopoverTrigger>
<PopoverContent width={{ base: "xs", sm: "sm", md: "md" }}>
<PopoverBody>
<VStack mt="2" align="start" spaceY="2" w="full">
{NAV_ITEMS.map((item) => (
<MobileHeaderLink key={item.label} item={item} />
))}
<LocaleSwitcher />
{renderMobileAuthSection()}
</VStack>
</PopoverBody>
</PopoverContent>
</PopoverRoot>
</ClientOnly>
</Stack>
</HStack>
</Flex>
</Box>
{/* Login Modal */}
<LoginModal open={loginModalOpen} onOpenChange={setLoginModalOpen} />
</>
);
}

View File

@@ -0,0 +1,84 @@
import { Text, Box, Link as ChakraLink, useDisclosure, VStack } from '@chakra-ui/react';
import { RxChevronDown } from 'react-icons/rx';
import { NavItem } from '@/config/navigation';
import { useActiveNavItem } from '@/hooks/useActiveNavItem';
import { Link } from '@/i18n/navigation';
import { useTranslations } from 'next-intl';
function MobileHeaderLink({ item }: { item: NavItem }) {
const t = useTranslations();
const { isActive, isChildActive } = useActiveNavItem(item);
const { open, onToggle } = useDisclosure();
return (
<Box key={item.label} w='full'>
{item.children ? (
<VStack align='start' w='full' spaceY={0}>
<Text
onClick={onToggle}
display='inline-flex'
alignItems='center'
gap='1'
cursor='pointer'
color={isActive ? { base: 'primary.500', _dark: 'primary.300' } : 'fg.muted'}
textUnderlineOffset='4px'
textUnderlinePosition='from-font'
textDecoration={isActive ? 'underline' : 'none'}
fontWeight='semibold'
fontSize={{ base: 'md', md: 'lg' }}
_hover={{
color: { base: 'primary.500', _dark: 'primary.300' },
transition: 'all 0.2s ease-in-out',
}}
>
{t(item.label)}
<RxChevronDown
style={{ transform: open ? 'rotate(-180deg)' : 'rotate(0deg)', transition: 'transform 0.2s' }}
/>
</Text>
{open && item.children && (
<VStack align='start' pl='4' pt='1' pb='2' w='full' spaceY={1}>
{item.children.map((child, index) => {
const isActiveChild = isChildActive(child.href);
return (
<ChakraLink
key={index}
as={Link}
href={child.href}
focusRing='none'
color={isActiveChild ? { base: 'primary.500', _dark: 'primary.300' } : 'fg.muted'}
textUnderlineOffset='4px'
textUnderlinePosition='from-font'
textDecoration={isActiveChild ? 'underline' : 'none'}
fontWeight='semibold'
fontSize={{ base: 'md', md: 'lg' }}
>
{t(child.label)}
</ChakraLink>
);
})}
</VStack>
)}
</VStack>
) : (
<ChakraLink
as={Link}
href={item.href}
w='full'
focusRing='none'
color={isActive ? { base: 'primary.500', _dark: 'primary.300' } : 'fg.muted'}
textUnderlineOffset='4px'
textUnderlinePosition='from-font'
textDecoration={isActive ? 'underline' : 'none'}
fontWeight='semibold'
fontSize={{ base: 'md', md: 'lg' }}
>
{t(item.label)}
</ChakraLink>
)}
</Box>
);
}
export default MobileHeaderLink;

File diff suppressed because it is too large Load Diff

BIN
src/components/ui/.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -0,0 +1,53 @@
'use client';
import { useEffect, useState } from 'react';
import { Icon, IconButton, Presence } from '@chakra-ui/react';
import { FiChevronUp } from 'react-icons/fi';
const BackToTop = () => {
const [isVisible, setIsVisible] = useState(false);
useEffect(() => {
const handleScroll = () => {
setIsVisible(window.pageYOffset > 300);
};
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);
const scrollToTop = () => {
window.scrollTo({
top: 0,
behavior: 'smooth',
});
};
return (
<Presence
unmountOnExit
present={isVisible}
animationName={{ _open: 'fade-in', _closed: 'fade-out' }}
animationDuration='moderate'
>
<IconButton
variant={{ base: 'solid', _dark: 'subtle' }}
aria-label='Back to top'
position='fixed'
bottom='8'
right='8'
borderRadius='full'
size='lg'
shadow='lg'
zIndex='999'
onClick={scrollToTop}
>
<Icon>
<FiChevronUp />
</Icon>
</IconButton>
</Presence>
);
};
export default BackToTop;

View File

@@ -0,0 +1,33 @@
import type { ButtonProps as ChakraButtonProps } from '@chakra-ui/react';
import { AbsoluteCenter, Button as ChakraButton, Span, Spinner } from '@chakra-ui/react';
import * as React from 'react';
interface ButtonLoadingProps {
loading?: boolean;
loadingText?: React.ReactNode;
}
export interface ButtonProps extends ChakraButtonProps, ButtonLoadingProps {}
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(function Button(props, ref) {
const { loading, disabled, loadingText, children, ...rest } = props;
return (
<ChakraButton disabled={loading || disabled} ref={ref} {...rest}>
{loading && !loadingText ? (
<>
<AbsoluteCenter display='inline-flex'>
<Spinner size='inherit' color='inherit' />
</AbsoluteCenter>
<Span opacity={0}>{children}</Span>
</>
) : loading && loadingText ? (
<>
<Spinner size='inherit' color='inherit' />
{loadingText}
</>
) : (
children
)}
</ChakraButton>
);
});

View File

@@ -0,0 +1,14 @@
import type { ButtonProps } from '@chakra-ui/react';
import { IconButton as ChakraIconButton } from '@chakra-ui/react';
import * as React from 'react';
import { LuX } from 'react-icons/lu';
export type CloseButtonProps = ButtonProps;
export const CloseButton = React.forwardRef<HTMLButtonElement, CloseButtonProps>(function CloseButton(props, ref) {
return (
<ChakraIconButton variant='ghost' aria-label='Close' ref={ref} {...props}>
{props.children ?? <LuX />}
</ChakraIconButton>
);
});

View File

@@ -0,0 +1,11 @@
'use client';
import type { HTMLChakraProps, RecipeProps } from '@chakra-ui/react';
import { createRecipeContext } from '@chakra-ui/react';
export interface LinkButtonProps extends HTMLChakraProps<'a', RecipeProps<'button'>> {}
const { withContext } = createRecipeContext({ key: 'button' });
// Replace "a" with your framework's link component
export const LinkButton = withContext<HTMLAnchorElement, LinkButtonProps>('a');

View File

@@ -0,0 +1,44 @@
'use client';
import type { ButtonProps } from '@chakra-ui/react';
import { Button, Toggle as ChakraToggle, useToggleContext } from '@chakra-ui/react';
import * as React from 'react';
interface ToggleProps extends ChakraToggle.RootProps {
variant?: keyof typeof variantMap;
size?: ButtonProps['size'];
}
const variantMap = {
solid: { on: 'solid', off: 'outline' },
surface: { on: 'surface', off: 'outline' },
subtle: { on: 'subtle', off: 'ghost' },
ghost: { on: 'subtle', off: 'ghost' },
} as const;
export const Toggle = React.forwardRef<HTMLButtonElement, ToggleProps>(function Toggle(props, ref) {
const { variant = 'subtle', size, children, ...rest } = props;
const variantConfig = variantMap[variant];
return (
<ChakraToggle.Root asChild {...rest}>
<ToggleBaseButton size={size} variant={variantConfig} ref={ref}>
{children}
</ToggleBaseButton>
</ChakraToggle.Root>
);
});
interface ToggleBaseButtonProps extends Omit<ButtonProps, 'variant'> {
variant: Record<'on' | 'off', ButtonProps['variant']>;
}
const ToggleBaseButton = React.forwardRef<HTMLButtonElement, ToggleBaseButtonProps>(
function ToggleBaseButton(props, ref) {
const toggle = useToggleContext();
const { variant, ...rest } = props;
return <Button variant={toggle.pressed ? variant.on : variant.off} ref={ref} {...rest} />;
},
);
export const ToggleIndicator = ChakraToggle.Indicator;

View File

@@ -0,0 +1,91 @@
'use client';
import { Combobox as ChakraCombobox, Portal } from '@chakra-ui/react';
import { CloseButton } from '@/components/ui/buttons/close-button';
import * as React from 'react';
interface ComboboxControlProps extends ChakraCombobox.ControlProps {
clearable?: boolean;
}
export const ComboboxControl = React.forwardRef<HTMLDivElement, ComboboxControlProps>(
function ComboboxControl(props, ref) {
const { children, clearable, ...rest } = props;
return (
<ChakraCombobox.Control {...rest} ref={ref}>
{children}
<ChakraCombobox.IndicatorGroup>
{clearable && <ComboboxClearTrigger />}
<ChakraCombobox.Trigger />
</ChakraCombobox.IndicatorGroup>
</ChakraCombobox.Control>
);
},
);
const ComboboxClearTrigger = React.forwardRef<HTMLButtonElement, ChakraCombobox.ClearTriggerProps>(
function ComboboxClearTrigger(props, ref) {
return (
<ChakraCombobox.ClearTrigger asChild {...props} ref={ref}>
<CloseButton size='xs' variant='plain' focusVisibleRing='inside' focusRingWidth='2px' pointerEvents='auto' />
</ChakraCombobox.ClearTrigger>
);
},
);
interface ComboboxContentProps extends ChakraCombobox.ContentProps {
portalled?: boolean;
portalRef?: React.RefObject<HTMLElement | null>;
}
export const ComboboxContent = React.forwardRef<HTMLDivElement, ComboboxContentProps>(
function ComboboxContent(props, ref) {
const { portalled = true, portalRef, ...rest } = props;
return (
<Portal disabled={!portalled} container={portalRef}>
<ChakraCombobox.Positioner>
<ChakraCombobox.Content {...rest} ref={ref} />
</ChakraCombobox.Positioner>
</Portal>
);
},
);
export const ComboboxItem = React.forwardRef<HTMLDivElement, ChakraCombobox.ItemProps>(
function ComboboxItem(props, ref) {
const { item, children, ...rest } = props;
return (
<ChakraCombobox.Item key={item.value} item={item} {...rest} ref={ref}>
{children}
<ChakraCombobox.ItemIndicator />
</ChakraCombobox.Item>
);
},
);
export const ComboboxRoot = React.forwardRef<HTMLDivElement, ChakraCombobox.RootProps>(
function ComboboxRoot(props, ref) {
return <ChakraCombobox.Root {...props} ref={ref} positioning={{ sameWidth: true, ...props.positioning }} />;
},
) as ChakraCombobox.RootComponent;
interface ComboboxItemGroupProps extends ChakraCombobox.ItemGroupProps {
label: React.ReactNode;
}
export const ComboboxItemGroup = React.forwardRef<HTMLDivElement, ComboboxItemGroupProps>(
function ComboboxItemGroup(props, ref) {
const { children, label, ...rest } = props;
return (
<ChakraCombobox.ItemGroup {...rest} ref={ref}>
<ChakraCombobox.ItemGroupLabel>{label}</ChakraCombobox.ItemGroupLabel>
{children}
</ChakraCombobox.ItemGroup>
);
},
);
export const ComboboxLabel = ChakraCombobox.Label;
export const ComboboxInput = ChakraCombobox.Input;
export const ComboboxEmpty = ChakraCombobox.Empty;
export const ComboboxItemText = ChakraCombobox.ItemText;

View File

@@ -0,0 +1,28 @@
'use client';
import { Listbox as ChakraListbox } from '@chakra-ui/react';
import * as React from 'react';
export const ListboxRoot = React.forwardRef<HTMLDivElement, ChakraListbox.RootProps>(function ListboxRoot(props, ref) {
return <ChakraListbox.Root {...props} ref={ref} />;
}) as ChakraListbox.RootComponent;
export const ListboxContent = React.forwardRef<HTMLDivElement, ChakraListbox.ContentProps>(
function ListboxContent(props, ref) {
return <ChakraListbox.Content {...props} ref={ref} />;
},
);
export const ListboxItem = React.forwardRef<HTMLDivElement, ChakraListbox.ItemProps>(function ListboxItem(props, ref) {
const { children, ...rest } = props;
return (
<ChakraListbox.Item {...rest} ref={ref}>
{children}
<ChakraListbox.ItemIndicator />
</ChakraListbox.Item>
);
});
export const ListboxLabel = ChakraListbox.Label;
export const ListboxItemText = ChakraListbox.ItemText;
export const ListboxEmpty = ChakraListbox.Empty;

View File

@@ -0,0 +1,118 @@
'use client';
import type { CollectionItem } from '@chakra-ui/react';
import { Select as ChakraSelect, Portal } from '@chakra-ui/react';
import { CloseButton } from '../buttons/close-button';
import * as React from 'react';
interface SelectTriggerProps extends ChakraSelect.ControlProps {
clearable?: boolean;
}
export const SelectTrigger = React.forwardRef<HTMLButtonElement, SelectTriggerProps>(
function SelectTrigger(props, ref) {
const { children, clearable, ...rest } = props;
return (
<ChakraSelect.Control {...rest}>
<ChakraSelect.Trigger ref={ref}>{children}</ChakraSelect.Trigger>
<ChakraSelect.IndicatorGroup>
{clearable && <SelectClearTrigger />}
<ChakraSelect.Indicator />
</ChakraSelect.IndicatorGroup>
</ChakraSelect.Control>
);
},
);
const SelectClearTrigger = React.forwardRef<HTMLButtonElement, ChakraSelect.ClearTriggerProps>(
function SelectClearTrigger(props, ref) {
return (
<ChakraSelect.ClearTrigger asChild {...props} ref={ref}>
<CloseButton size='xs' variant='plain' focusVisibleRing='inside' focusRingWidth='2px' pointerEvents='auto' />
</ChakraSelect.ClearTrigger>
);
},
);
interface SelectContentProps extends ChakraSelect.ContentProps {
portalled?: boolean;
portalRef?: React.RefObject<HTMLElement | null>;
}
export const SelectContent = React.forwardRef<HTMLDivElement, SelectContentProps>(function SelectContent(props, ref) {
const { portalled = true, portalRef, ...rest } = props;
return (
<Portal disabled={!portalled} container={portalRef}>
<ChakraSelect.Positioner>
<ChakraSelect.Content {...rest} ref={ref} />
</ChakraSelect.Positioner>
</Portal>
);
});
export const SelectItem = React.forwardRef<HTMLDivElement, ChakraSelect.ItemProps>(function SelectItem(props, ref) {
const { item, children, ...rest } = props;
return (
<ChakraSelect.Item key={item.value} item={item} {...rest} ref={ref}>
{children}
<ChakraSelect.ItemIndicator />
</ChakraSelect.Item>
);
});
interface SelectValueTextProps extends Omit<ChakraSelect.ValueTextProps, 'children'> {
children?(items: CollectionItem[]): React.ReactNode;
}
export const SelectValueText = React.forwardRef<HTMLSpanElement, SelectValueTextProps>(
function SelectValueText(props, ref) {
const { children, ...rest } = props;
return (
<ChakraSelect.ValueText {...rest} ref={ref}>
<ChakraSelect.Context>
{(select) => {
const items = select.selectedItems;
if (items.length === 0) return props.placeholder;
if (children) return children(items);
if (items.length === 1) return select.collection.stringifyItem(items[0]);
return `${items.length} selected`;
}}
</ChakraSelect.Context>
</ChakraSelect.ValueText>
);
},
);
export const SelectRoot = React.forwardRef<HTMLDivElement, ChakraSelect.RootProps>(function SelectRoot(props, ref) {
return (
<ChakraSelect.Root {...props} ref={ref} positioning={{ sameWidth: true, ...props.positioning }}>
{props.asChild ? (
props.children
) : (
<>
<ChakraSelect.HiddenSelect />
{props.children}
</>
)}
</ChakraSelect.Root>
);
}) as ChakraSelect.RootComponent;
interface SelectItemGroupProps extends ChakraSelect.ItemGroupProps {
label: React.ReactNode;
}
export const SelectItemGroup = React.forwardRef<HTMLDivElement, SelectItemGroupProps>(
function SelectItemGroup(props, ref) {
const { children, label, ...rest } = props;
return (
<ChakraSelect.ItemGroup {...rest} ref={ref}>
<ChakraSelect.ItemGroupLabel>{label}</ChakraSelect.ItemGroupLabel>
{children}
</ChakraSelect.ItemGroup>
);
},
);
export const SelectLabel = ChakraSelect.Label;
export const SelectItemText = ChakraSelect.ItemText;

View File

@@ -0,0 +1,60 @@
'use client';
import { TreeView as ChakraTreeView } from '@chakra-ui/react';
import * as React from 'react';
export const TreeViewRoot = React.forwardRef<HTMLDivElement, ChakraTreeView.RootProps>(
function TreeViewRoot(props, ref) {
return <ChakraTreeView.Root {...props} ref={ref} />;
},
);
interface TreeViewTreeProps extends ChakraTreeView.TreeProps {}
export const TreeViewTree = React.forwardRef<HTMLDivElement, TreeViewTreeProps>(function TreeViewTree(props, ref) {
const { ...rest } = props;
return <ChakraTreeView.Tree {...rest} ref={ref} />;
});
export const TreeViewBranch = React.forwardRef<HTMLDivElement, ChakraTreeView.BranchProps>(
function TreeViewBranch(props, ref) {
return <ChakraTreeView.Branch {...props} ref={ref} />;
},
);
export const TreeViewBranchControl = React.forwardRef<HTMLDivElement, ChakraTreeView.BranchControlProps>(
function TreeViewBranchControl(props, ref) {
return <ChakraTreeView.BranchControl {...props} ref={ref} />;
},
);
export const TreeViewItem = React.forwardRef<HTMLDivElement, ChakraTreeView.ItemProps>(
function TreeViewItem(props, ref) {
return <ChakraTreeView.Item {...props} ref={ref} />;
},
);
export const TreeViewLabel = ChakraTreeView.Label;
export const TreeViewBranchIndicator = ChakraTreeView.BranchIndicator;
export const TreeViewBranchText = ChakraTreeView.BranchText;
export const TreeViewBranchContent = ChakraTreeView.BranchContent;
export const TreeViewBranchIndentGuide = ChakraTreeView.BranchIndentGuide;
export const TreeViewItemText = ChakraTreeView.ItemText;
export const TreeViewNode = ChakraTreeView.Node;
export const TreeViewNodeProvider = ChakraTreeView.NodeProvider;
export const TreeView = {
Root: TreeViewRoot,
Label: TreeViewLabel,
Tree: TreeViewTree,
Branch: TreeViewBranch,
BranchControl: TreeViewBranchControl,
BranchIndicator: TreeViewBranchIndicator,
BranchText: TreeViewBranchText,
BranchContent: TreeViewBranchContent,
BranchIndentGuide: TreeViewBranchIndentGuide,
Item: TreeViewItem,
ItemText: TreeViewItemText,
Node: TreeViewNode,
NodeProvider: TreeViewNodeProvider,
};

View File

@@ -0,0 +1,108 @@
'use client';
import type { IconButtonProps, SpanProps } from '@chakra-ui/react';
import { ClientOnly, IconButton, Skeleton, Span } from '@chakra-ui/react';
import { ThemeProvider, useTheme } from 'next-themes';
import type { ThemeProviderProps } from 'next-themes';
import * as React from 'react';
import { LuMoon, LuSun } from 'react-icons/lu';
export interface ColorModeProviderProps extends ThemeProviderProps {}
export function ColorModeProvider(props: ColorModeProviderProps) {
return <ThemeProvider attribute='class' disableTransitionOnChange {...props} />;
}
export type ColorMode = 'light' | 'dark';
export interface UseColorModeReturn {
colorMode: ColorMode;
setColorMode: (colorMode: ColorMode) => void;
toggleColorMode: () => void;
}
export function useColorMode(): UseColorModeReturn {
const { resolvedTheme, setTheme, forcedTheme } = useTheme();
const colorMode = forcedTheme || resolvedTheme;
const toggleColorMode = () => {
setTheme(resolvedTheme === 'dark' ? 'light' : 'dark');
};
return {
colorMode: colorMode as ColorMode,
setColorMode: setTheme,
toggleColorMode,
};
}
export function useColorModeValue<T>(light: T, dark: T) {
const { colorMode } = useColorMode();
const [mounted, setMounted] = React.useState(false);
React.useEffect(() => setMounted(true), []);
if (!mounted) {
return light;
}
return colorMode === 'dark' ? dark : light;
}
export function ColorModeIcon() {
const { colorMode } = useColorMode();
return colorMode === 'dark' ? <LuMoon /> : <LuSun />;
}
interface ColorModeButtonProps extends Omit<IconButtonProps, 'aria-label'> {}
export const ColorModeButton = React.forwardRef<HTMLButtonElement, ColorModeButtonProps>(
function ColorModeButton(props, ref) {
const { toggleColorMode } = useColorMode();
return (
<ClientOnly fallback={<Skeleton boxSize='9' />}>
<IconButton
onClick={toggleColorMode}
variant='ghost'
aria-label='Toggle color mode'
size='sm'
ref={ref}
{...props}
css={{
_icon: {
width: '5',
height: '5',
},
}}
>
<ColorModeIcon />
</IconButton>
</ClientOnly>
);
},
);
export const LightMode = React.forwardRef<HTMLSpanElement, SpanProps>(function LightMode(props, ref) {
return (
<Span
color='fg'
display='contents'
className='chakra-theme light'
colorPalette='gray'
colorScheme='light'
ref={ref}
{...props}
/>
);
});
export const DarkMode = React.forwardRef<HTMLSpanElement, SpanProps>(function DarkMode(props, ref) {
return (
<Span
color='fg'
display='contents'
className='chakra-theme dark'
colorPalette='gray'
colorScheme='dark'
ref={ref}
{...props}
/>
);
});

View File

@@ -0,0 +1,26 @@
import { Avatar as ChakraAvatar, AvatarGroup as ChakraAvatarGroup } from '@chakra-ui/react';
import * as React from 'react';
type ImageProps = React.ImgHTMLAttributes<HTMLImageElement>;
export interface AvatarProps extends ChakraAvatar.RootProps {
name?: string;
src?: string;
srcSet?: string;
loading?: ImageProps['loading'];
icon?: React.ReactElement;
fallback?: React.ReactNode;
}
export const Avatar = React.forwardRef<HTMLDivElement, AvatarProps>(function Avatar(props, ref) {
const { name, src, srcSet, loading, icon, fallback, children, ...rest } = props;
return (
<ChakraAvatar.Root ref={ref} {...rest}>
<ChakraAvatar.Fallback name={name}>{icon || fallback}</ChakraAvatar.Fallback>
<ChakraAvatar.Image src={src} srcSet={srcSet} loading={loading} />
{children}
</ChakraAvatar.Root>
);
});
export const AvatarGroup = ChakraAvatarGroup;

View File

@@ -0,0 +1,79 @@
import type { ButtonProps, InputProps } from '@chakra-ui/react';
import { Button, Clipboard as ChakraClipboard, IconButton, Input } from '@chakra-ui/react';
import * as React from 'react';
import { LuCheck, LuClipboard, LuLink } from 'react-icons/lu';
const ClipboardIcon = React.forwardRef<HTMLDivElement, ChakraClipboard.IndicatorProps>(
function ClipboardIcon(props, ref) {
return (
<ChakraClipboard.Indicator copied={<LuCheck />} {...props} ref={ref}>
<LuClipboard />
</ChakraClipboard.Indicator>
);
},
);
const ClipboardCopyText = React.forwardRef<HTMLDivElement, ChakraClipboard.IndicatorProps>(
function ClipboardCopyText(props, ref) {
return (
<ChakraClipboard.Indicator copied='Copied' {...props} ref={ref}>
Copy
</ChakraClipboard.Indicator>
);
},
);
export const ClipboardLabel = React.forwardRef<HTMLLabelElement, ChakraClipboard.LabelProps>(
function ClipboardLabel(props, ref) {
return (
<ChakraClipboard.Label textStyle='sm' fontWeight='medium' display='inline-block' mb='1' {...props} ref={ref} />
);
},
);
export const ClipboardButton = React.forwardRef<HTMLButtonElement, ButtonProps>(function ClipboardButton(props, ref) {
return (
<ChakraClipboard.Trigger asChild>
<Button ref={ref} size='sm' variant='surface' {...props}>
<ClipboardIcon />
<ClipboardCopyText />
</Button>
</ChakraClipboard.Trigger>
);
});
export const ClipboardLink = React.forwardRef<HTMLButtonElement, ButtonProps>(function ClipboardLink(props, ref) {
return (
<ChakraClipboard.Trigger asChild>
<Button unstyled variant='plain' size='xs' display='inline-flex' alignItems='center' gap='2' ref={ref} {...props}>
<LuLink />
<ClipboardCopyText />
</Button>
</ChakraClipboard.Trigger>
);
});
export const ClipboardIconButton = React.forwardRef<HTMLButtonElement, ButtonProps>(
function ClipboardIconButton(props, ref) {
return (
<ChakraClipboard.Trigger asChild>
<IconButton ref={ref} size='xs' variant='subtle' {...props}>
<ClipboardIcon />
<ClipboardCopyText srOnly />
</IconButton>
</ChakraClipboard.Trigger>
);
},
);
export const ClipboardInput = React.forwardRef<HTMLInputElement, InputProps>(
function ClipboardInputElement(props, ref) {
return (
<ChakraClipboard.Input asChild>
<Input ref={ref} {...props} />
</ChakraClipboard.Input>
);
},
);
export const ClipboardRoot = ChakraClipboard.Root;

View File

@@ -0,0 +1,26 @@
import { DataList as ChakraDataList } from '@chakra-ui/react';
import { InfoTip } from '@/components/ui/overlays/toggle-tip';
import * as React from 'react';
export const DataListRoot = ChakraDataList.Root;
interface ItemProps extends ChakraDataList.ItemProps {
label: React.ReactNode;
value: React.ReactNode;
info?: React.ReactNode;
grow?: boolean;
}
export const DataListItem = React.forwardRef<HTMLDivElement, ItemProps>(function DataListItem(props, ref) {
const { label, info, value, children, grow, ...rest } = props;
return (
<ChakraDataList.Item ref={ref} {...rest}>
<ChakraDataList.ItemLabel flex={grow ? '1' : undefined}>
{label}
{info && <InfoTip>{info}</InfoTip>}
</ChakraDataList.ItemLabel>
<ChakraDataList.ItemValue flex={grow ? '1' : undefined}>{value}</ChakraDataList.ItemValue>
{children}
</ChakraDataList.Item>
);
});

View File

@@ -0,0 +1,20 @@
import { QrCode as ChakraQrCode } from '@chakra-ui/react';
import * as React from 'react';
export interface QrCodeProps extends Omit<ChakraQrCode.RootProps, 'fill' | 'overlay'> {
fill?: string;
overlay?: React.ReactNode;
}
export const QrCode = React.forwardRef<HTMLDivElement, QrCodeProps>(function QrCode(props, ref) {
const { children, fill, overlay, ...rest } = props;
return (
<ChakraQrCode.Root ref={ref} {...rest}>
<ChakraQrCode.Frame style={{ fill }}>
<ChakraQrCode.Pattern />
</ChakraQrCode.Frame>
{children}
{overlay && <ChakraQrCode.Overlay>{overlay}</ChakraQrCode.Overlay>}
</ChakraQrCode.Root>
);
});

View File

@@ -0,0 +1,53 @@
import { Badge, type BadgeProps, Stat as ChakraStat, FormatNumber } from '@chakra-ui/react';
import { InfoTip } from '@/components/ui/overlays/toggle-tip';
import * as React from 'react';
interface StatLabelProps extends ChakraStat.LabelProps {
info?: React.ReactNode;
}
export const StatLabel = React.forwardRef<HTMLDivElement, StatLabelProps>(function StatLabel(props, ref) {
const { info, children, ...rest } = props;
return (
<ChakraStat.Label {...rest} ref={ref}>
{children}
{info && <InfoTip>{info}</InfoTip>}
</ChakraStat.Label>
);
});
interface StatValueTextProps extends ChakraStat.ValueTextProps {
value?: number;
formatOptions?: Intl.NumberFormatOptions;
}
export const StatValueText = React.forwardRef<HTMLDivElement, StatValueTextProps>(function StatValueText(props, ref) {
const { value, formatOptions, children, ...rest } = props;
return (
<ChakraStat.ValueText {...rest} ref={ref}>
{children || (value != null && <FormatNumber value={value} {...formatOptions} />)}
</ChakraStat.ValueText>
);
});
export const StatUpTrend = React.forwardRef<HTMLDivElement, BadgeProps>(function StatUpTrend(props, ref) {
return (
<Badge colorPalette='green' gap='0' {...props} ref={ref}>
<ChakraStat.UpIndicator />
{props.children}
</Badge>
);
});
export const StatDownTrend = React.forwardRef<HTMLDivElement, BadgeProps>(function StatDownTrend(props, ref) {
return (
<Badge colorPalette='red' gap='0' {...props} ref={ref}>
<ChakraStat.DownIndicator />
{props.children}
</Badge>
);
});
export const StatRoot = ChakraStat.Root;
export const StatHelpText = ChakraStat.HelpText;
export const StatValueUnit = ChakraStat.ValueUnit;

View File

@@ -0,0 +1,26 @@
import { Tag as ChakraTag } from '@chakra-ui/react';
import * as React from 'react';
export interface TagProps extends ChakraTag.RootProps {
startElement?: React.ReactNode;
endElement?: React.ReactNode;
onClose?: VoidFunction;
closable?: boolean;
}
export const Tag = React.forwardRef<HTMLSpanElement, TagProps>(function Tag(props, ref) {
const { startElement, endElement, onClose, closable = !!onClose, children, ...rest } = props;
return (
<ChakraTag.Root ref={ref} {...rest}>
{startElement && <ChakraTag.StartElement>{startElement}</ChakraTag.StartElement>}
<ChakraTag.Label>{children}</ChakraTag.Label>
{endElement && <ChakraTag.EndElement>{endElement}</ChakraTag.EndElement>}
{closable && (
<ChakraTag.EndElement>
<ChakraTag.CloseTrigger onClick={onClose} />
</ChakraTag.EndElement>
)}
</ChakraTag.Root>
);
});

View File

@@ -0,0 +1,25 @@
import { Timeline as ChakraTimeline } from '@chakra-ui/react';
import * as React from 'react';
interface TimelineConnectorProps extends ChakraTimeline.IndicatorProps {
icon?: React.ReactNode;
}
export const TimelineConnector = React.forwardRef<HTMLDivElement, TimelineConnectorProps>(function TimelineConnector(
{ icon, ...props },
ref,
) {
return (
<ChakraTimeline.Connector ref={ref}>
<ChakraTimeline.Separator />
<ChakraTimeline.Indicator {...props}>{icon}</ChakraTimeline.Indicator>
</ChakraTimeline.Connector>
);
});
export const TimelineRoot = ChakraTimeline.Root;
export const TimelineContent = ChakraTimeline.Content;
export const TimelineItem = ChakraTimeline.Item;
export const TimelineIndicator = ChakraTimeline.Indicator;
export const TimelineTitle = ChakraTimeline.Title;
export const TimelineDescription = ChakraTimeline.Description;

View File

@@ -0,0 +1,45 @@
import { Accordion, HStack } from '@chakra-ui/react';
import * as React from 'react';
import { LuChevronDown } from 'react-icons/lu';
interface AccordionItemTriggerProps extends Accordion.ItemTriggerProps {
indicatorPlacement?: 'start' | 'end';
}
export const AccordionItemTrigger = React.forwardRef<HTMLButtonElement, AccordionItemTriggerProps>(
function AccordionItemTrigger(props, ref) {
const { children, indicatorPlacement = 'end', ...rest } = props;
return (
<Accordion.ItemTrigger {...rest} ref={ref}>
{indicatorPlacement === 'start' && (
<Accordion.ItemIndicator rotate={{ base: '-90deg', _open: '0deg' }}>
<LuChevronDown />
</Accordion.ItemIndicator>
)}
<HStack gap='4' flex='1' textAlign='start' width='full'>
{children}
</HStack>
{indicatorPlacement === 'end' && (
<Accordion.ItemIndicator>
<LuChevronDown />
</Accordion.ItemIndicator>
)}
</Accordion.ItemTrigger>
);
},
);
interface AccordionItemContentProps extends Accordion.ItemContentProps {}
export const AccordionItemContent = React.forwardRef<HTMLDivElement, AccordionItemContentProps>(
function AccordionItemContent(props, ref) {
return (
<Accordion.ItemContent>
<Accordion.ItemBody {...props} ref={ref} />
</Accordion.ItemContent>
);
},
);
export const AccordionRoot = Accordion.Root;
export const AccordionItem = Accordion.Item;

View File

@@ -0,0 +1,35 @@
import { Breadcrumb, type SystemStyleObject } from '@chakra-ui/react';
import * as React from 'react';
export interface BreadcrumbRootProps extends Breadcrumb.RootProps {
separator?: React.ReactNode;
separatorGap?: SystemStyleObject['gap'];
}
export const BreadcrumbRoot = React.forwardRef<HTMLDivElement, BreadcrumbRootProps>(
function BreadcrumbRoot(props, ref) {
const { separator, separatorGap, children, ...rest } = props;
const validChildren = React.Children.toArray(children).filter(React.isValidElement);
return (
<Breadcrumb.Root ref={ref} {...rest}>
<Breadcrumb.List gap={separatorGap}>
{validChildren.map((child, index) => {
const last = index === validChildren.length - 1;
return (
<React.Fragment key={index}>
<Breadcrumb.Item>{child}</Breadcrumb.Item>
{!last && <Breadcrumb.Separator>{separator}</Breadcrumb.Separator>}
</React.Fragment>
);
})}
</Breadcrumb.List>
</Breadcrumb.Root>
);
},
);
export const BreadcrumbLink = Breadcrumb.Link;
export const BreadcrumbCurrentLink = Breadcrumb.CurrentLink;
export const BreadcrumbEllipsis = Breadcrumb.Ellipsis;

View File

@@ -0,0 +1,182 @@
'use client';
import type { ButtonProps, TextProps } from '@chakra-ui/react';
import {
Button,
Pagination as ChakraPagination,
IconButton,
Text,
createContext,
usePaginationContext,
} from '@chakra-ui/react';
import * as React from 'react';
import { HiChevronLeft, HiChevronRight, HiMiniEllipsisHorizontal } from 'react-icons/hi2';
import { LinkButton } from '@/components/ui/buttons/link-button';
interface ButtonVariantMap {
current: ButtonProps['variant'];
default: ButtonProps['variant'];
ellipsis: ButtonProps['variant'];
}
type PaginationVariant = 'outline' | 'solid' | 'subtle';
interface ButtonVariantContext {
size: ButtonProps['size'];
variantMap: ButtonVariantMap;
getHref?: (page: number) => string;
}
const [RootPropsProvider, useRootProps] = createContext<ButtonVariantContext>({
name: 'RootPropsProvider',
});
export interface PaginationRootProps extends Omit<ChakraPagination.RootProps, 'type'> {
size?: ButtonProps['size'];
variant?: PaginationVariant;
getHref?: (page: number) => string;
}
const variantMap: Record<PaginationVariant, ButtonVariantMap> = {
outline: { default: 'ghost', ellipsis: 'plain', current: 'outline' },
solid: { default: 'outline', ellipsis: 'outline', current: 'solid' },
subtle: { default: 'ghost', ellipsis: 'plain', current: 'subtle' },
};
export const PaginationRoot = React.forwardRef<HTMLDivElement, PaginationRootProps>(
function PaginationRoot(props, ref) {
const { size = 'sm', variant = 'outline', getHref, ...rest } = props;
return (
<RootPropsProvider value={{ size, variantMap: variantMap[variant], getHref }}>
<ChakraPagination.Root ref={ref} type={getHref ? 'link' : 'button'} {...rest} />
</RootPropsProvider>
);
},
);
export const PaginationEllipsis = React.forwardRef<HTMLDivElement, ChakraPagination.EllipsisProps>(
function PaginationEllipsis(props, ref) {
const { size, variantMap } = useRootProps();
return (
<ChakraPagination.Ellipsis ref={ref} {...props} asChild>
<Button as='span' variant={variantMap.ellipsis} size={size}>
<HiMiniEllipsisHorizontal />
</Button>
</ChakraPagination.Ellipsis>
);
},
);
export const PaginationItem = React.forwardRef<HTMLButtonElement, ChakraPagination.ItemProps>(
function PaginationItem(props, ref) {
const { page } = usePaginationContext();
const { size, variantMap, getHref } = useRootProps();
const current = page === props.value;
const variant = current ? variantMap.current : variantMap.default;
if (getHref) {
return (
<LinkButton href={getHref(props.value)} variant={variant} size={size}>
{props.value}
</LinkButton>
);
}
return (
<ChakraPagination.Item ref={ref} {...props} asChild>
<Button variant={variant} size={size}>
{props.value}
</Button>
</ChakraPagination.Item>
);
},
);
export const PaginationPrevTrigger = React.forwardRef<HTMLButtonElement, ChakraPagination.PrevTriggerProps>(
function PaginationPrevTrigger(props, ref) {
const { size, variantMap, getHref } = useRootProps();
const { previousPage } = usePaginationContext();
if (getHref) {
return (
<LinkButton
href={previousPage != null ? getHref(previousPage) : undefined}
variant={variantMap.default}
size={size}
>
<HiChevronLeft />
</LinkButton>
);
}
return (
<ChakraPagination.PrevTrigger ref={ref} asChild {...props}>
<IconButton variant={variantMap.default} size={size}>
<HiChevronLeft />
</IconButton>
</ChakraPagination.PrevTrigger>
);
},
);
export const PaginationNextTrigger = React.forwardRef<HTMLButtonElement, ChakraPagination.NextTriggerProps>(
function PaginationNextTrigger(props, ref) {
const { size, variantMap, getHref } = useRootProps();
const { nextPage } = usePaginationContext();
if (getHref) {
return (
<LinkButton href={nextPage != null ? getHref(nextPage) : undefined} variant={variantMap.default} size={size}>
<HiChevronRight />
</LinkButton>
);
}
return (
<ChakraPagination.NextTrigger ref={ref} asChild {...props}>
<IconButton variant={variantMap.default} size={size}>
<HiChevronRight />
</IconButton>
</ChakraPagination.NextTrigger>
);
},
);
export const PaginationItems = (props: React.HTMLAttributes<HTMLElement>) => {
return (
<ChakraPagination.Context>
{({ pages }) =>
pages.map((page, index) => {
return page.type === 'ellipsis' ? (
<PaginationEllipsis key={index} index={index} {...props} />
) : (
<PaginationItem key={index} type='page' value={page.value} {...props} />
);
})
}
</ChakraPagination.Context>
);
};
interface PageTextProps extends TextProps {
format?: 'short' | 'compact' | 'long';
}
export const PaginationPageText = React.forwardRef<HTMLParagraphElement, PageTextProps>(
function PaginationPageText(props, ref) {
const { format = 'compact', ...rest } = props;
const { page, totalPages, pageRange, count } = usePaginationContext();
const content = React.useMemo(() => {
if (format === 'short') return `${page} / ${totalPages}`;
if (format === 'compact') return `${page} of ${totalPages}`;
return `${pageRange.start + 1} - ${Math.min(pageRange.end, count)} of ${count}`;
}, [format, page, totalPages, pageRange, count]);
return (
<Text fontWeight='medium' ref={ref} {...rest}>
{content}
</Text>
);
},
);

View File

@@ -0,0 +1,73 @@
import { Box, Steps as ChakraSteps } from '@chakra-ui/react';
import * as React from 'react';
import { LuCheck } from 'react-icons/lu';
interface StepInfoProps {
title?: React.ReactNode;
description?: React.ReactNode;
}
export interface StepsItemProps extends Omit<ChakraSteps.ItemProps, 'title'>, StepInfoProps {
completedIcon?: React.ReactNode;
icon?: React.ReactNode;
disableTrigger?: boolean;
}
export const StepsItem = React.forwardRef<HTMLDivElement, StepsItemProps>(function StepsItem(props, ref) {
const { title, description, completedIcon, icon, disableTrigger, ...rest } = props;
return (
<ChakraSteps.Item {...rest} ref={ref}>
<ChakraSteps.Trigger disabled={disableTrigger}>
<ChakraSteps.Indicator>
<ChakraSteps.Status complete={completedIcon || <LuCheck />} incomplete={icon || <ChakraSteps.Number />} />
</ChakraSteps.Indicator>
<StepInfo title={title} description={description} />
</ChakraSteps.Trigger>
<ChakraSteps.Separator />
</ChakraSteps.Item>
);
});
const StepInfo = (props: StepInfoProps) => {
const { title, description } = props;
if (title && description) {
return (
<Box>
<ChakraSteps.Title>{title}</ChakraSteps.Title>
<ChakraSteps.Description>{description}</ChakraSteps.Description>
</Box>
);
}
return (
<>
{title && <ChakraSteps.Title>{title}</ChakraSteps.Title>}
{description && <ChakraSteps.Description>{description}</ChakraSteps.Description>}
</>
);
};
interface StepsIndicatorProps {
completedIcon: React.ReactNode;
icon?: React.ReactNode;
}
export const StepsIndicator = React.forwardRef<HTMLDivElement, StepsIndicatorProps>(
function StepsIndicator(props, ref) {
const { icon = <ChakraSteps.Number />, completedIcon } = props;
return (
<ChakraSteps.Indicator ref={ref}>
<ChakraSteps.Status complete={completedIcon} incomplete={icon} />
</ChakraSteps.Indicator>
);
},
);
export const StepsList = ChakraSteps.List;
export const StepsRoot = ChakraSteps.Root;
export const StepsContent = ChakraSteps.Content;
export const StepsCompletedContent = ChakraSteps.CompletedContent;
export const StepsNextTrigger = ChakraSteps.NextTrigger;
export const StepsPrevTrigger = ChakraSteps.PrevTrigger;

View File

@@ -0,0 +1,27 @@
import { Alert as ChakraAlert } from '@chakra-ui/react';
import * as React from 'react';
export interface AlertProps extends Omit<ChakraAlert.RootProps, 'title'> {
startElement?: React.ReactNode;
endElement?: React.ReactNode;
title?: React.ReactNode;
icon?: React.ReactElement;
}
export const Alert = React.forwardRef<HTMLDivElement, AlertProps>(function Alert(props, ref) {
const { title, children, icon, startElement, endElement, ...rest } = props;
return (
<ChakraAlert.Root ref={ref} {...rest}>
{startElement || <ChakraAlert.Indicator>{icon}</ChakraAlert.Indicator>}
{children ? (
<ChakraAlert.Content>
<ChakraAlert.Title>{title}</ChakraAlert.Title>
<ChakraAlert.Description>{children}</ChakraAlert.Description>
</ChakraAlert.Content>
) : (
<ChakraAlert.Title flex='1'>{title}</ChakraAlert.Title>
)}
{endElement}
</ChakraAlert.Root>
);
});

View File

@@ -0,0 +1,28 @@
import { EmptyState as ChakraEmptyState, VStack } from '@chakra-ui/react';
import * as React from 'react';
export interface EmptyStateProps extends ChakraEmptyState.RootProps {
title: string;
description?: string;
icon?: React.ReactNode;
}
export const EmptyState = React.forwardRef<HTMLDivElement, EmptyStateProps>(function EmptyState(props, ref) {
const { title, description, icon, children, ...rest } = props;
return (
<ChakraEmptyState.Root ref={ref} {...rest}>
<ChakraEmptyState.Content>
{icon && <ChakraEmptyState.Indicator>{icon}</ChakraEmptyState.Indicator>}
{description ? (
<VStack textAlign='center'>
<ChakraEmptyState.Title>{title}</ChakraEmptyState.Title>
<ChakraEmptyState.Description>{description}</ChakraEmptyState.Description>
</VStack>
) : (
<ChakraEmptyState.Title>{title}</ChakraEmptyState.Title>
)}
{children}
</ChakraEmptyState.Content>
</ChakraEmptyState.Root>
);
});

View File

@@ -0,0 +1,32 @@
import type { SystemStyleObject } from '@chakra-ui/react';
import { AbsoluteCenter, ProgressCircle as ChakraProgressCircle } from '@chakra-ui/react';
import * as React from 'react';
interface ProgressCircleRingProps extends ChakraProgressCircle.CircleProps {
trackColor?: SystemStyleObject['stroke'];
cap?: SystemStyleObject['strokeLinecap'];
}
export const ProgressCircleRing = React.forwardRef<SVGSVGElement, ProgressCircleRingProps>(
function ProgressCircleRing(props, ref) {
const { trackColor, cap, color, ...rest } = props;
return (
<ChakraProgressCircle.Circle {...rest} ref={ref}>
<ChakraProgressCircle.Track stroke={trackColor} />
<ChakraProgressCircle.Range stroke={color} strokeLinecap={cap} />
</ChakraProgressCircle.Circle>
);
},
);
export const ProgressCircleValueText = React.forwardRef<HTMLDivElement, ChakraProgressCircle.ValueTextProps>(
function ProgressCircleValueText(props, ref) {
return (
<AbsoluteCenter>
<ChakraProgressCircle.ValueText {...props} ref={ref} />
</AbsoluteCenter>
);
},
);
export const ProgressCircleRoot = ChakraProgressCircle.Root;

View File

@@ -0,0 +1,30 @@
import { Progress as ChakraProgress } from '@chakra-ui/react';
import { InfoTip } from '../overlays/toggle-tip';
import * as React from 'react';
export const ProgressBar = React.forwardRef<HTMLDivElement, ChakraProgress.TrackProps>(
function ProgressBar(props, ref) {
return (
<ChakraProgress.Track {...props} ref={ref}>
<ChakraProgress.Range />
</ChakraProgress.Track>
);
},
);
export interface ProgressLabelProps extends ChakraProgress.LabelProps {
info?: React.ReactNode;
}
export const ProgressLabel = React.forwardRef<HTMLDivElement, ProgressLabelProps>(function ProgressLabel(props, ref) {
const { children, info, ...rest } = props;
return (
<ChakraProgress.Label {...rest} ref={ref}>
{children}
{info && <InfoTip>{info}</InfoTip>}
</ChakraProgress.Label>
);
});
export const ProgressRoot = ChakraProgress.Root;
export const ProgressValueText = ChakraProgress.ValueText;

View File

@@ -0,0 +1,35 @@
import type { SkeletonProps as ChakraSkeletonProps, CircleProps } from '@chakra-ui/react';
import { Skeleton as ChakraSkeleton, Circle, Stack } from '@chakra-ui/react';
import * as React from 'react';
export interface SkeletonCircleProps extends ChakraSkeletonProps {
size?: CircleProps['size'];
}
export const SkeletonCircle = React.forwardRef<HTMLDivElement, SkeletonCircleProps>(
function SkeletonCircle(props, ref) {
const { size, ...rest } = props;
return (
<Circle size={size} asChild ref={ref}>
<ChakraSkeleton {...rest} />
</Circle>
);
},
);
export interface SkeletonTextProps extends ChakraSkeletonProps {
noOfLines?: number;
}
export const SkeletonText = React.forwardRef<HTMLDivElement, SkeletonTextProps>(function SkeletonText(props, ref) {
const { noOfLines = 3, gap, ...rest } = props;
return (
<Stack gap={gap} width='full' ref={ref}>
{Array.from({ length: noOfLines }).map((_, index) => (
<ChakraSkeleton height='4' key={index} {...props} _last={{ maxW: '80%' }} {...rest} />
))}
</Stack>
);
});
export const Skeleton = ChakraSkeleton;

View File

@@ -0,0 +1,27 @@
import type { ColorPalette } from '@chakra-ui/react';
import { Status as ChakraStatus } from '@chakra-ui/react';
import * as React from 'react';
type StatusValue = 'success' | 'error' | 'warning' | 'info';
export interface StatusProps extends ChakraStatus.RootProps {
value?: StatusValue;
}
const statusMap: Record<StatusValue, ColorPalette> = {
success: 'green',
error: 'red',
warning: 'orange',
info: 'blue',
};
export const Status = React.forwardRef<HTMLDivElement, StatusProps>(function Status(props, ref) {
const { children, value = 'info', ...rest } = props;
const colorPalette = rest.colorPalette ?? statusMap[value];
return (
<ChakraStatus.Root ref={ref} {...rest} colorPalette={colorPalette}>
<ChakraStatus.Indicator />
{children}
</ChakraStatus.Root>
);
});

View File

@@ -0,0 +1,28 @@
'use client';
import { Toaster as ChakraToaster, Portal, Spinner, Stack, Toast, createToaster } from '@chakra-ui/react';
export const toaster = createToaster({
placement: 'bottom-end',
pauseOnPageIdle: true,
});
export const Toaster = () => {
return (
<Portal>
<ChakraToaster toaster={toaster} insetInline={{ mdDown: '4' }}>
{(toast) => (
<Toast.Root width={{ md: 'sm' }}>
{toast.type === 'loading' ? <Spinner size='sm' color='blue.solid' /> : <Toast.Indicator />}
<Stack gap='1' flex='1' maxWidth='100%'>
{toast.title && <Toast.Title>{toast.title}</Toast.Title>}
{toast.description && <Toast.Description>{toast.description}</Toast.Description>}
</Stack>
{toast.action && <Toast.ActionTrigger>{toast.action.label}</Toast.ActionTrigger>}
{toast.closable && <Toast.CloseTrigger />}
</Toast.Root>
)}
</ChakraToaster>
</Portal>
);
};

View File

@@ -0,0 +1,49 @@
import { CheckboxCard as ChakraCheckboxCard } from '@chakra-ui/react';
import * as React from 'react';
export interface CheckboxCardProps extends ChakraCheckboxCard.RootProps {
icon?: React.ReactElement;
label?: React.ReactNode;
description?: React.ReactNode;
addon?: React.ReactNode;
indicator?: React.ReactNode | null;
indicatorPlacement?: 'start' | 'end' | 'inside';
inputProps?: React.InputHTMLAttributes<HTMLInputElement>;
}
export const CheckboxCard = React.forwardRef<HTMLInputElement, CheckboxCardProps>(function CheckboxCard(props, ref) {
const {
inputProps,
label,
description,
icon,
addon,
indicator = <ChakraCheckboxCard.Indicator />,
indicatorPlacement = 'end',
...rest
} = props;
const hasContent = label || description || icon;
const ContentWrapper = indicator ? ChakraCheckboxCard.Content : React.Fragment;
return (
<ChakraCheckboxCard.Root {...rest}>
<ChakraCheckboxCard.HiddenInput ref={ref} {...inputProps} />
<ChakraCheckboxCard.Control>
{indicatorPlacement === 'start' && indicator}
{hasContent && (
<ContentWrapper>
{icon}
{label && <ChakraCheckboxCard.Label>{label}</ChakraCheckboxCard.Label>}
{description && <ChakraCheckboxCard.Description>{description}</ChakraCheckboxCard.Description>}
{indicatorPlacement === 'inside' && indicator}
</ContentWrapper>
)}
{indicatorPlacement === 'end' && indicator}
</ChakraCheckboxCard.Control>
{addon && <ChakraCheckboxCard.Addon>{addon}</ChakraCheckboxCard.Addon>}
</ChakraCheckboxCard.Root>
);
});
export const CheckboxCardIndicator = ChakraCheckboxCard.Indicator;

View File

@@ -0,0 +1,19 @@
import { Checkbox as ChakraCheckbox } from '@chakra-ui/react';
import * as React from 'react';
export interface CheckboxProps extends ChakraCheckbox.RootProps {
icon?: React.ReactNode;
inputProps?: React.InputHTMLAttributes<HTMLInputElement>;
rootRef?: React.RefObject<HTMLLabelElement | null>;
}
export const Checkbox = React.forwardRef<HTMLInputElement, CheckboxProps>(function Checkbox(props, ref) {
const { icon, children, inputProps, rootRef, ...rest } = props;
return (
<ChakraCheckbox.Root ref={rootRef} {...rest}>
<ChakraCheckbox.HiddenInput ref={ref} {...inputProps} />
<ChakraCheckbox.Control>{icon || <ChakraCheckbox.Indicator />}</ChakraCheckbox.Control>
{children != null && <ChakraCheckbox.Label>{children}</ChakraCheckbox.Label>}
</ChakraCheckbox.Root>
);
});

View File

@@ -0,0 +1,174 @@
import type { IconButtonProps, StackProps } from '@chakra-ui/react';
import { ColorPicker as ChakraColorPicker, For, IconButton, Portal, Span, Stack, Text, VStack } from '@chakra-ui/react';
import * as React from 'react';
import { LuCheck, LuPipette } from 'react-icons/lu';
export const ColorPickerTrigger = React.forwardRef<
HTMLButtonElement,
ChakraColorPicker.TriggerProps & { fitContent?: boolean }
>(function ColorPickerTrigger(props, ref) {
const { fitContent, ...rest } = props;
return (
<ChakraColorPicker.Trigger data-fit-content={fitContent || undefined} ref={ref} {...rest}>
{props.children || <ChakraColorPicker.ValueSwatch />}
</ChakraColorPicker.Trigger>
);
});
export const ColorPickerInput = React.forwardRef<
HTMLInputElement,
Omit<ChakraColorPicker.ChannelInputProps, 'channel'>
>(function ColorHexInput(props, ref) {
return <ChakraColorPicker.ChannelInput channel='hex' ref={ref} {...props} />;
});
interface ColorPickerContentProps extends ChakraColorPicker.ContentProps {
portalled?: boolean;
portalRef?: React.RefObject<HTMLElement | null>;
}
export const ColorPickerContent = React.forwardRef<HTMLDivElement, ColorPickerContentProps>(
function ColorPickerContent(props, ref) {
const { portalled = true, portalRef, ...rest } = props;
return (
<Portal disabled={!portalled} container={portalRef}>
<ChakraColorPicker.Positioner>
<ChakraColorPicker.Content ref={ref} {...rest} />
</ChakraColorPicker.Positioner>
</Portal>
);
},
);
export const ColorPickerInlineContent = React.forwardRef<HTMLDivElement, ChakraColorPicker.ContentProps>(
function ColorPickerInlineContent(props, ref) {
return <ChakraColorPicker.Content animation='none' shadow='none' padding='0' ref={ref} {...props} />;
},
);
export const ColorPickerSliders = React.forwardRef<HTMLDivElement, StackProps>(function ColorPickerSliders(props, ref) {
return (
<Stack gap='1' flex='1' px='1' ref={ref} {...props}>
<ColorPickerChannelSlider channel='hue' />
<ColorPickerChannelSlider channel='alpha' />
</Stack>
);
});
export const ColorPickerArea = React.forwardRef<HTMLDivElement, ChakraColorPicker.AreaProps>(
function ColorPickerArea(props, ref) {
return (
<ChakraColorPicker.Area ref={ref} {...props}>
<ChakraColorPicker.AreaBackground />
<ChakraColorPicker.AreaThumb />
</ChakraColorPicker.Area>
);
},
);
export const ColorPickerEyeDropper = React.forwardRef<HTMLButtonElement, IconButtonProps>(
function ColorPickerEyeDropper(props, ref) {
return (
<ChakraColorPicker.EyeDropperTrigger asChild>
<IconButton size='xs' variant='outline' ref={ref} {...props}>
<LuPipette />
</IconButton>
</ChakraColorPicker.EyeDropperTrigger>
);
},
);
export const ColorPickerChannelSlider = React.forwardRef<HTMLDivElement, ChakraColorPicker.ChannelSliderProps>(
function ColorPickerSlider(props, ref) {
return (
<ChakraColorPicker.ChannelSlider ref={ref} {...props}>
<ChakraColorPicker.TransparencyGrid size='0.6rem' />
<ChakraColorPicker.ChannelSliderTrack />
<ChakraColorPicker.ChannelSliderThumb />
</ChakraColorPicker.ChannelSlider>
);
},
);
export const ColorPickerSwatchTrigger = React.forwardRef<
HTMLButtonElement,
ChakraColorPicker.SwatchTriggerProps & {
swatchSize?: ChakraColorPicker.SwatchTriggerProps['boxSize'];
}
>(function ColorPickerSwatchTrigger(props, ref) {
const { swatchSize, children, ...rest } = props;
return (
<ChakraColorPicker.SwatchTrigger ref={ref} style={{ ['--color' as string]: props.value }} {...rest}>
{children || (
<ChakraColorPicker.Swatch boxSize={swatchSize} value={props.value}>
<ChakraColorPicker.SwatchIndicator>
<LuCheck />
</ChakraColorPicker.SwatchIndicator>
</ChakraColorPicker.Swatch>
)}
</ChakraColorPicker.SwatchTrigger>
);
});
export const ColorPickerRoot = React.forwardRef<HTMLDivElement, ChakraColorPicker.RootProps>(
function ColorPickerRoot(props, ref) {
return (
<ChakraColorPicker.Root ref={ref} {...props}>
{props.children}
<ChakraColorPicker.HiddenInput tabIndex={-1} />
</ChakraColorPicker.Root>
);
},
);
const formatMap = {
rgba: ['red', 'green', 'blue', 'alpha'],
hsla: ['hue', 'saturation', 'lightness', 'alpha'],
hsba: ['hue', 'saturation', 'brightness', 'alpha'],
hexa: ['hex', 'alpha'],
} as const;
export const ColorPickerChannelInputs = React.forwardRef<HTMLDivElement, ChakraColorPicker.ViewProps>(
function ColorPickerChannelInputs(props, ref) {
const channels = formatMap[props.format];
return (
<ChakraColorPicker.View flexDirection='row' ref={ref} {...props}>
{channels.map((channel) => (
<VStack gap='1' key={channel} flex='1'>
<ColorPickerChannelInput channel={channel} px='0' height='7' textStyle='xs' textAlign='center' />
<Text textStyle='xs' color='fg.muted' fontWeight='medium'>
{channel.charAt(0).toUpperCase()}
</Text>
</VStack>
))}
</ChakraColorPicker.View>
);
},
);
export const ColorPickerChannelSliders = React.forwardRef<HTMLDivElement, ChakraColorPicker.ViewProps>(
function ColorPickerChannelSliders(props, ref) {
const channels = formatMap[props.format];
return (
<ChakraColorPicker.View {...props} ref={ref}>
<For each={channels}>
{(channel) => (
<Stack gap='1' key={channel}>
<Span textStyle='xs' minW='5ch' textTransform='capitalize' fontWeight='medium'>
{channel}
</Span>
<ColorPickerChannelSlider channel={channel} />
</Stack>
)}
</For>
</ChakraColorPicker.View>
);
},
);
export const ColorPickerLabel = ChakraColorPicker.Label;
export const ColorPickerControl = ChakraColorPicker.Control;
export const ColorPickerValueText = ChakraColorPicker.ValueText;
export const ColorPickerValueSwatch = ChakraColorPicker.ValueSwatch;
export const ColorPickerChannelInput = ChakraColorPicker.ChannelInput;
export const ColorPickerSwatchGroup = ChakraColorPicker.SwatchGroup;

View File

@@ -0,0 +1,26 @@
import { Field as ChakraField } from '@chakra-ui/react';
import * as React from 'react';
export interface FieldProps extends Omit<ChakraField.RootProps, 'label'> {
label?: React.ReactNode;
helperText?: React.ReactNode;
errorText?: React.ReactNode;
optionalText?: React.ReactNode;
}
export const Field = React.forwardRef<HTMLDivElement, FieldProps>(function Field(props, ref) {
const { label, children, helperText, errorText, optionalText, ...rest } = props;
return (
<ChakraField.Root ref={ref} {...rest}>
{label && (
<ChakraField.Label>
{label}
<ChakraField.RequiredIndicator fallback={optionalText} />
</ChakraField.Label>
)}
{children}
{helperText && <ChakraField.HelperText>{helperText}</ChakraField.HelperText>}
{errorText && <ChakraField.ErrorText>{errorText}</ChakraField.ErrorText>}
</ChakraField.Root>
);
});

View File

@@ -0,0 +1,150 @@
'use client';
import type { ButtonProps, RecipeProps } from '@chakra-ui/react';
import {
Button,
FileUpload as ChakraFileUpload,
Icon,
IconButton,
Span,
Text,
useFileUploadContext,
useRecipe,
} from '@chakra-ui/react';
import * as React from 'react';
import { LuFile, LuUpload, LuX } from 'react-icons/lu';
export interface FileUploadRootProps extends ChakraFileUpload.RootProps {
inputProps?: React.InputHTMLAttributes<HTMLInputElement>;
}
export const FileUploadRoot = React.forwardRef<HTMLInputElement, FileUploadRootProps>(
function FileUploadRoot(props, ref) {
const { children, inputProps, ...rest } = props;
return (
<ChakraFileUpload.Root {...rest}>
<ChakraFileUpload.HiddenInput ref={ref} {...inputProps} />
{children}
</ChakraFileUpload.Root>
);
},
);
export interface FileUploadDropzoneProps extends ChakraFileUpload.DropzoneProps {
label: React.ReactNode;
description?: React.ReactNode;
}
export const FileUploadDropzone = React.forwardRef<HTMLInputElement, FileUploadDropzoneProps>(
function FileUploadDropzone(props, ref) {
const { children, label, description, ...rest } = props;
return (
<ChakraFileUpload.Dropzone ref={ref} {...rest}>
<Icon fontSize='xl' color='fg.muted'>
<LuUpload />
</Icon>
<ChakraFileUpload.DropzoneContent>
<div>{label}</div>
{description && <Text color='fg.muted'>{description}</Text>}
</ChakraFileUpload.DropzoneContent>
{children}
</ChakraFileUpload.Dropzone>
);
},
);
interface VisibilityProps {
showSize?: boolean;
clearable?: boolean;
}
interface FileUploadItemProps extends VisibilityProps {
file: File;
}
const FileUploadItem = React.forwardRef<HTMLLIElement, FileUploadItemProps>(function FileUploadItem(props, ref) {
const { file, showSize, clearable } = props;
return (
<ChakraFileUpload.Item file={file} ref={ref}>
<ChakraFileUpload.ItemPreview asChild>
<Icon fontSize='lg' color='fg.muted'>
<LuFile />
</Icon>
</ChakraFileUpload.ItemPreview>
{showSize ? (
<ChakraFileUpload.ItemContent>
<ChakraFileUpload.ItemName />
<ChakraFileUpload.ItemSizeText />
</ChakraFileUpload.ItemContent>
) : (
<ChakraFileUpload.ItemName flex='1' />
)}
{clearable && (
<ChakraFileUpload.ItemDeleteTrigger asChild>
<IconButton variant='ghost' color='fg.muted' size='xs'>
<LuX />
</IconButton>
</ChakraFileUpload.ItemDeleteTrigger>
)}
</ChakraFileUpload.Item>
);
});
interface FileUploadListProps extends VisibilityProps, ChakraFileUpload.ItemGroupProps {
files?: File[];
}
export const FileUploadList = React.forwardRef<HTMLUListElement, FileUploadListProps>(
function FileUploadList(props, ref) {
const { showSize, clearable, files, ...rest } = props;
const fileUpload = useFileUploadContext();
const acceptedFiles = files ?? fileUpload.acceptedFiles;
if (acceptedFiles.length === 0) return null;
return (
<ChakraFileUpload.ItemGroup ref={ref} {...rest}>
{acceptedFiles.map((file) => (
<FileUploadItem key={file.name} file={file} showSize={showSize} clearable={clearable} />
))}
</ChakraFileUpload.ItemGroup>
);
},
);
type Assign<T, U> = Omit<T, keyof U> & U;
interface FileInputProps extends Assign<ButtonProps, RecipeProps<'input'>> {
placeholder?: React.ReactNode;
}
export const FileInput = React.forwardRef<HTMLButtonElement, FileInputProps>(function FileInput(props, ref) {
const inputRecipe = useRecipe({ key: 'input' });
const [recipeProps, restProps] = inputRecipe.splitVariantProps(props);
const { placeholder = 'Select file(s)', ...rest } = restProps;
return (
<ChakraFileUpload.Trigger asChild>
<Button unstyled py='0' ref={ref} {...rest} css={[inputRecipe(recipeProps), props.css]}>
<ChakraFileUpload.Context>
{({ acceptedFiles }) => {
if (acceptedFiles.length === 1) {
return <span>{acceptedFiles[0].name}</span>;
}
if (acceptedFiles.length > 1) {
return <span>{acceptedFiles.length} files</span>;
}
return <Span color='fg.subtle'>{placeholder}</Span>;
}}
</ChakraFileUpload.Context>
</Button>
</ChakraFileUpload.Trigger>
);
});
export const FileUploadLabel = ChakraFileUpload.Label;
export const FileUploadClearTrigger = ChakraFileUpload.ClearTrigger;
export const FileUploadTrigger = ChakraFileUpload.Trigger;
export const FileUploadFileText = ChakraFileUpload.FileText;

Some files were not shown because too many files have changed in this diff Show More