main
Some checks failed
UI Deploy (Next-Auth Support) 🎨 / build-and-deploy (push) Failing after 2m42s
32
.agent/agents/frontend-developer.md
Normal 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.
|
||||||
194
.agent/agents/nextjs-architecture-expert.md
Normal 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.
|
||||||
479
.agent/commands/nextjs-api-tester.md
Normal 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.
|
||||||
488
.agent/commands/nextjs-component-generator.md
Normal 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>© 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.
|
||||||
42
.agent/skills/frontend-design/SKILL.md
Normal 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.
|
||||||
209
.agent/skills/senior-frontend/SKILL.md
Normal 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
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
103
.agent/skills/senior-frontend/references/react_patterns.md
Normal 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.
|
||||||
114
.agent/skills/senior-frontend/scripts/bundle_analyzer.py
Normal 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()
|
||||||
114
.agent/skills/senior-frontend/scripts/component_generator.py
Normal 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()
|
||||||
114
.agent/skills/senior-frontend/scripts/frontend_scaffolder.py
Normal 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()
|
||||||
209
.agent/skills/senior-qa/SKILL.md
Normal 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
|
||||||
103
.agent/skills/senior-qa/references/qa_best_practices.md
Normal 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.
|
||||||
103
.agent/skills/senior-qa/references/test_automation_patterns.md
Normal 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.
|
||||||
103
.agent/skills/senior-qa/references/testing_strategies.md
Normal 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.
|
||||||
114
.agent/skills/senior-qa/scripts/coverage_analyzer.py
Normal 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()
|
||||||
114
.agent/skills/senior-qa/scripts/e2e_test_scaffolder.py
Normal 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()
|
||||||
114
.agent/skills/senior-qa/scripts/test_suite_generator.py
Normal 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
@@ -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'
|
||||||
37
.gitea/workflows/deploy-ui.yml
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
name: UI Deploy (Next-Auth Support) 🎨
|
||||||
|
run-name: ${{ gitea.actor }} frontend güncelliyor...
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Kodu Çek
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Docker Build
|
||||||
|
# Tarayıcı tarafında (Client-side) lazım olanları build anında veriyoruz
|
||||||
|
run: |
|
||||||
|
docker build \
|
||||||
|
--build-arg NEXT_PUBLIC_API_URL='${{ secrets.NEXT_PUBLIC_API_URL }}' \
|
||||||
|
--build-arg NEXT_PUBLIC_AUTH_REQUIRED='${{ secrets.NEXT_PUBLIC_AUTH_REQUIRED }}' \
|
||||||
|
--build-arg NEXT_PUBLIC_GOOGLE_API_KEY='${{ secrets.NEXT_PUBLIC_GOOGLE_API_KEY }}' \
|
||||||
|
-t frontend-proje:latest .
|
||||||
|
|
||||||
|
- name: Eski Konteyneri Sil
|
||||||
|
run: docker rm -f frontend-container || true
|
||||||
|
|
||||||
|
- name: Yeni Versiyonu Başlat
|
||||||
|
# Sunucu tarafında (Server-side/Auth) lazım olanları run anında veriyoruz
|
||||||
|
run: |
|
||||||
|
docker run -d \
|
||||||
|
--name frontend-container \
|
||||||
|
--restart always \
|
||||||
|
--network gitea-server_gitea \
|
||||||
|
-p 1800:3000 \
|
||||||
|
-e NEXTAUTH_SECRET='${{ secrets.NEXTAUTH_SECRET }}' \
|
||||||
|
-e NEXTAUTH_URL='${{ secrets.NEXTAUTH_URL }}' \
|
||||||
|
frontend-proje:latest
|
||||||
7
.gitignore
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
node_modules
|
||||||
|
|
||||||
|
.next
|
||||||
|
|
||||||
|
certificates
|
||||||
|
|
||||||
|
.env.local
|
||||||
12
.mcp.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"DeepGraph Next.js MCP": {
|
||||||
|
"command": "npx",
|
||||||
|
"args": [
|
||||||
|
"-y",
|
||||||
|
"mcp-code-graph@latest",
|
||||||
|
"vercel/next.js"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
44
Dockerfile
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
# --- 1. Bağımlılık Katmanı ---
|
||||||
|
FROM node:20-alpine AS deps
|
||||||
|
RUN apk add --no-cache libc6-compat
|
||||||
|
WORKDIR /app
|
||||||
|
COPY package.json package-lock.json ./
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
|
# --- 2. Build Katmanı ---
|
||||||
|
FROM node:20-alpine AS builder
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=deps /app/node_modules ./node_modules
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# [DİKKAT] Build anındaki env'leri buraya tanımlıyoruz
|
||||||
|
ARG NEXT_PUBLIC_API_URL
|
||||||
|
ARG NEXT_PUBLIC_AUTH_REQUIRED
|
||||||
|
ARG NEXT_PUBLIC_GOOGLE_API_KEY
|
||||||
|
|
||||||
|
ENV NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL
|
||||||
|
ENV NEXT_PUBLIC_AUTH_REQUIRED=$NEXT_PUBLIC_AUTH_REQUIRED
|
||||||
|
ENV NEXT_PUBLIC_GOOGLE_API_KEY=$NEXT_PUBLIC_GOOGLE_API_KEY
|
||||||
|
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# --- 3. Çalıştırma Katmanı (Runner) ---
|
||||||
|
FROM node:20-alpine AS runner
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
ENV NODE_ENV production
|
||||||
|
# Güvenlik: Root kullanıcı kullanmıyoruz
|
||||||
|
RUN addgroup --system --gid 1001 nodejs
|
||||||
|
RUN adduser --system --uid 1001 nextjs
|
||||||
|
|
||||||
|
COPY --from=builder /app/public ./public
|
||||||
|
# Standalone mode çıktılarını alıyoruz
|
||||||
|
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
||||||
|
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
||||||
|
|
||||||
|
USER nextjs
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
|
ENV PORT 3000
|
||||||
|
# Standalone modda server.js üzerinden çalışır
|
||||||
|
CMD ["node", "server.js"]
|
||||||
341
README.md
Normal file
@@ -0,0 +1,341 @@
|
|||||||
|
# 🚀 Enterprise Next.js Boilerplate (Antigravity Edition)
|
||||||
|
|
||||||
|
[](https://nextjs.org/)
|
||||||
|
[](https://react.dev/)
|
||||||
|
[](https://chakra-ui.com/)
|
||||||
|
[](https://www.typescriptlang.org/)
|
||||||
|
[](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
@@ -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
@@ -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 you’re looking for doesn’t 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
@@ -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
@@ -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.
|
||||||
21
next.config.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import type { NextConfig } from "next";
|
||||||
|
import createNextIntlPlugin from "next-intl/plugin";
|
||||||
|
|
||||||
|
const nextConfig: NextConfig = {
|
||||||
|
output: 'standalone',
|
||||||
|
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);
|
||||||
10341
package-lock.json
generated
Normal file
44
package.json
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
{
|
||||||
|
"name": "test-ui",
|
||||||
|
"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
@@ -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
BIN
public/assets/.DS_Store
vendored
Normal file
BIN
public/assets/img/sign-in-image.png
Normal file
|
After Width: | Height: | Size: 196 KiB |
BIN
public/assets/img/sign-up-image.png
Normal file
|
After Width: | Height: | Size: 181 KiB |
BIN
public/favicon/android-chrome-192x192.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
public/favicon/android-chrome-512x512.png
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
public/favicon/apple-touch-icon.png
Normal file
|
After Width: | Height: | Size: 9.1 KiB |
BIN
public/favicon/favicon-16x16.png
Normal file
|
After Width: | Height: | Size: 558 B |
BIN
public/favicon/favicon-32x32.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
public/favicon/favicon.ico
Normal file
|
After Width: | Height: | Size: 15 KiB |
19
public/favicon/site.webmanifest
Normal 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
BIN
src/app/.DS_Store
vendored
Normal file
15
src/app/[locale]/(auth)/layout.tsx
Normal 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;
|
||||||
231
src/app/[locale]/(auth)/signin/page.tsx
Normal 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;
|
||||||
166
src/app/[locale]/(auth)/signup/page.tsx
Normal 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;
|
||||||
5
src/app/[locale]/(error)/[...slug]/page.tsx
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { notFound } from 'next/navigation';
|
||||||
|
|
||||||
|
export default function CatchAllPage() {
|
||||||
|
notFound();
|
||||||
|
}
|
||||||
7
src/app/[locale]/(site)/about/page.tsx
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
function AboutPage() {
|
||||||
|
return <div>AboutPage</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AboutPage;
|
||||||
14
src/app/[locale]/(site)/home/page.tsx
Normal 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 />;
|
||||||
|
}
|
||||||
21
src/app/[locale]/(site)/layout.tsx
Normal 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
7
src/app/[locale]/global.css
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
html {
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
41
src/app/[locale]/layout.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
30
src/app/[locale]/not-found.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
5
src/app/[locale]/page.tsx
Normal 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
94
src/app/api/auth/[...nextauth]/route.ts
Normal 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
154
src/components/auth/login-modal.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
71
src/components/layout/footer/footer.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
104
src/components/layout/header/header-link.tsx
Normal 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;
|
||||||
229
src/components/layout/header/header.tsx
Normal 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} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
84
src/components/layout/header/mobile-header-link.tsx
Normal 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;
|
||||||
3303
src/components/site/home/home-card.tsx
Normal file
BIN
src/components/ui/.DS_Store
vendored
Normal file
53
src/components/ui/back-to-top.tsx
Normal 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;
|
||||||
33
src/components/ui/buttons/button.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
});
|
||||||
14
src/components/ui/buttons/close-button.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
});
|
||||||
11
src/components/ui/buttons/link-button.tsx
Normal 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');
|
||||||
44
src/components/ui/buttons/toggle.tsx
Normal 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;
|
||||||
91
src/components/ui/collections/combobox.tsx
Normal 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;
|
||||||
28
src/components/ui/collections/listbox.tsx
Normal 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;
|
||||||
118
src/components/ui/collections/select.tsx
Normal 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;
|
||||||
60
src/components/ui/collections/treeview.tsx
Normal 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,
|
||||||
|
};
|
||||||
108
src/components/ui/color-mode.tsx
Normal 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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
26
src/components/ui/data-display/avatar.tsx
Normal 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;
|
||||||
79
src/components/ui/data-display/clipboard.tsx
Normal 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;
|
||||||
26
src/components/ui/data-display/data-list.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
});
|
||||||
20
src/components/ui/data-display/qr-code.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
});
|
||||||
53
src/components/ui/data-display/stat.tsx
Normal 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;
|
||||||
26
src/components/ui/data-display/tag.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
});
|
||||||
25
src/components/ui/data-display/timeline.tsx
Normal 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;
|
||||||
45
src/components/ui/disclosure/accordion.tsx
Normal 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;
|
||||||
35
src/components/ui/disclosure/breadcrumb.tsx
Normal 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;
|
||||||
182
src/components/ui/disclosure/pagination.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
73
src/components/ui/disclosure/steps.tsx
Normal 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;
|
||||||
27
src/components/ui/feedback/alert.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
});
|
||||||
28
src/components/ui/feedback/empty-state.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
});
|
||||||
32
src/components/ui/feedback/progress-circle.tsx
Normal 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;
|
||||||
30
src/components/ui/feedback/progress.tsx
Normal 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;
|
||||||
35
src/components/ui/feedback/skeleton.tsx
Normal 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;
|
||||||
27
src/components/ui/feedback/status.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
});
|
||||||
28
src/components/ui/feedback/toaster.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
49
src/components/ui/forms/checkbox-card.tsx
Normal 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;
|
||||||
19
src/components/ui/forms/checkbox.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
});
|
||||||
174
src/components/ui/forms/color-picker.tsx
Normal 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;
|
||||||