create-pnpm-custom-app 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +217 -0
- package/bin/cli.js +185 -0
- package/package.json +39 -0
- package/templates/.github/copilot-instructions.md +184 -0
- package/templates/.nvmrc +1 -0
- package/templates/.vscode/settings.json +51 -0
- package/templates/CONTRIBUTING.md +184 -0
- package/templates/LICENSE +21 -0
- package/templates/README.md +324 -0
- package/templates/apps/api/.env.example +36 -0
- package/templates/apps/api/.prettierrc.json +7 -0
- package/templates/apps/api/eslint.config.js +17 -0
- package/templates/apps/api/gitignore +45 -0
- package/templates/apps/api/jest.config.ts +22 -0
- package/templates/apps/api/package.json +49 -0
- package/templates/apps/api/src/app.ts +121 -0
- package/templates/apps/api/src/config/config.ts +38 -0
- package/templates/apps/api/src/config/logger.ts +57 -0
- package/templates/apps/api/src/db/mongo.ts +30 -0
- package/templates/apps/api/src/index.ts +40 -0
- package/templates/apps/api/src/middlewares/middleware.ts +75 -0
- package/templates/apps/api/src/models/example.model.ts +89 -0
- package/templates/apps/api/src/routes/routes.ts +54 -0
- package/templates/apps/api/src/schemas/swagger.schema.ts +58 -0
- package/templates/apps/api/src/services/example.service.ts +63 -0
- package/templates/apps/api/src/tests/health.test.ts +90 -0
- package/templates/apps/api/src/tests/helpers/test-helpers.ts +40 -0
- package/templates/apps/api/src/tests/mocks/mocks.ts +29 -0
- package/templates/apps/api/src/tests/setup.ts +11 -0
- package/templates/apps/api/src/types/fastify.d.ts +44 -0
- package/templates/apps/api/tsconfig.json +24 -0
- package/templates/apps/web/.env.example +25 -0
- package/templates/apps/web/.prettierignore +7 -0
- package/templates/apps/web/.prettierrc +9 -0
- package/templates/apps/web/app/ICONS.md +42 -0
- package/templates/apps/web/app/[locale]/(routes)/layout.tsx +13 -0
- package/templates/apps/web/app/[locale]/(routes)/page.tsx +49 -0
- package/templates/apps/web/app/[locale]/[...not-found]/page.tsx +8 -0
- package/templates/apps/web/app/[locale]/layout.tsx +35 -0
- package/templates/apps/web/app/[locale]/not-found.tsx +12 -0
- package/templates/apps/web/app/components/layout/Footer.component.tsx +30 -0
- package/templates/apps/web/app/components/layout/Nav.component.tsx +34 -0
- package/templates/apps/web/app/components/ui/README.md +39 -0
- package/templates/apps/web/app/components/ui/atoms/README.md +55 -0
- package/templates/apps/web/app/components/ui/molecules/README.md +51 -0
- package/templates/apps/web/app/globals.css +104 -0
- package/templates/apps/web/app/icon.svg +5 -0
- package/templates/apps/web/app/layout.tsx +37 -0
- package/templates/apps/web/app/manifest.json +22 -0
- package/templates/apps/web/app/providers.tsx +25 -0
- package/templates/apps/web/app/robots.ts +12 -0
- package/templates/apps/web/app/sitemap.ts +18 -0
- package/templates/apps/web/eslint.config.mjs +16 -0
- package/templates/apps/web/gitignore +56 -0
- package/templates/apps/web/hooks/README.md +25 -0
- package/templates/apps/web/i18n/config.ts +9 -0
- package/templates/apps/web/i18n/request.ts +15 -0
- package/templates/apps/web/interfaces/README.md +5 -0
- package/templates/apps/web/lib/README.md +45 -0
- package/templates/apps/web/lib/utils.ts +18 -0
- package/templates/apps/web/messages/en.json +34 -0
- package/templates/apps/web/messages/es.json +34 -0
- package/templates/apps/web/next.config.ts +50 -0
- package/templates/apps/web/package.json +34 -0
- package/templates/apps/web/postcss.config.mjs +7 -0
- package/templates/apps/web/proxy.ts +17 -0
- package/templates/apps/web/public/README.md +7 -0
- package/templates/apps/web/tsconfig.json +27 -0
- package/templates/apps/web/types/README.md +3 -0
- package/templates/docs/README.md +13 -0
- package/templates/gitignore-root +51 -0
- package/templates/package.json +30 -0
- package/templates/packages/shared/eslint.config.js +26 -0
- package/templates/packages/shared/package.json +22 -0
- package/templates/packages/shared/src/index.ts +39 -0
- package/templates/packages/shared/tsconfig.json +19 -0
- package/templates/pnpm-workspace.yaml +3 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll, afterAll } from '@jest/globals';
|
|
2
|
+
import { buildApp } from '../app.js';
|
|
3
|
+
// import { connectMongo, disconnectMongo } from '../db/mongo.js';
|
|
4
|
+
import type { FastifyInstance } from 'fastify';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Example Test Suite
|
|
8
|
+
*
|
|
9
|
+
* This file demonstrates how to write tests for your API endpoints.
|
|
10
|
+
*
|
|
11
|
+
* @remarks
|
|
12
|
+
* Testing best practices:
|
|
13
|
+
* - Use descriptive test names
|
|
14
|
+
* - Test both success and error cases
|
|
15
|
+
* - Mock external dependencies
|
|
16
|
+
* - Clean up test data after each test
|
|
17
|
+
* - Use beforeAll/afterAll for setup/teardown
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```typescript
|
|
21
|
+
* describe('User API', () => {
|
|
22
|
+
* let app: FastifyInstance;
|
|
23
|
+
*
|
|
24
|
+
* beforeAll(async () => {
|
|
25
|
+
* await connectMongo(process.env.MONGODB_URI_TEST!);
|
|
26
|
+
* app = await buildApp();
|
|
27
|
+
* });
|
|
28
|
+
*
|
|
29
|
+
* afterAll(async () => {
|
|
30
|
+
* await app.close();
|
|
31
|
+
* await disconnectMongo();
|
|
32
|
+
* });
|
|
33
|
+
*
|
|
34
|
+
* it('should create a new user', async () => {
|
|
35
|
+
* const response = await app.inject({
|
|
36
|
+
* method: 'POST',
|
|
37
|
+
* url: '/users',
|
|
38
|
+
* payload: {
|
|
39
|
+
* email: 'test@example.com',
|
|
40
|
+
* password: 'password123',
|
|
41
|
+
* name: 'Test User',
|
|
42
|
+
* },
|
|
43
|
+
* });
|
|
44
|
+
*
|
|
45
|
+
* expect(response.statusCode).toBe(201);
|
|
46
|
+
* expect(response.json()).toMatchObject({
|
|
47
|
+
* email: 'test@example.com',
|
|
48
|
+
* name: 'Test User',
|
|
49
|
+
* });
|
|
50
|
+
* });
|
|
51
|
+
*
|
|
52
|
+
* it('should return 400 for invalid email', async () => {
|
|
53
|
+
* const response = await app.inject({
|
|
54
|
+
* method: 'POST',
|
|
55
|
+
* url: '/users',
|
|
56
|
+
* payload: {
|
|
57
|
+
* email: 'invalid-email',
|
|
58
|
+
* password: 'password123',
|
|
59
|
+
* },
|
|
60
|
+
* });
|
|
61
|
+
*
|
|
62
|
+
* expect(response.statusCode).toBe(400);
|
|
63
|
+
* });
|
|
64
|
+
* });
|
|
65
|
+
* ```
|
|
66
|
+
*/
|
|
67
|
+
|
|
68
|
+
describe('Health Check API', () => {
|
|
69
|
+
let app: FastifyInstance;
|
|
70
|
+
|
|
71
|
+
beforeAll(async () => {
|
|
72
|
+
app = await buildApp();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
afterAll(async () => {
|
|
76
|
+
await app.close();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('should return health status', async () => {
|
|
80
|
+
const response = await app.inject({
|
|
81
|
+
method: 'GET',
|
|
82
|
+
url: '/health',
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
expect(response.statusCode).toBe(200);
|
|
86
|
+
const body = response.json();
|
|
87
|
+
expect(body).toHaveProperty('status', 'ok');
|
|
88
|
+
expect(body).toHaveProperty('timestamp');
|
|
89
|
+
});
|
|
90
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test Helpers
|
|
3
|
+
*
|
|
4
|
+
* Utility functions to help with testing.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```typescript
|
|
8
|
+
* // Create test data
|
|
9
|
+
* export function createTestUser(overrides = {}) {
|
|
10
|
+
* return {
|
|
11
|
+
* email: 'test@example.com',
|
|
12
|
+
* password: 'password123',
|
|
13
|
+
* name: 'Test User',
|
|
14
|
+
* role: 'user',
|
|
15
|
+
* ...overrides,
|
|
16
|
+
* };
|
|
17
|
+
* }
|
|
18
|
+
*
|
|
19
|
+
* // Generate JWT token for testing
|
|
20
|
+
* export function generateTestToken(userId: string, app: FastifyInstance) {
|
|
21
|
+
* return app.jwt.sign({ id: userId });
|
|
22
|
+
* }
|
|
23
|
+
*
|
|
24
|
+
* // Clean up test database
|
|
25
|
+
* export async function clearDatabase() {
|
|
26
|
+
* const collections = mongoose.connection.collections;
|
|
27
|
+
* for (const key in collections) {
|
|
28
|
+
* await collections[key].deleteMany({});
|
|
29
|
+
* }
|
|
30
|
+
* }
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
export function generateRandomEmail(): string {
|
|
35
|
+
return `test-${Date.now()}-${Math.random().toString(36).substring(7)}@example.com`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function sleep(ms: number): Promise<void> {
|
|
39
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
40
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test Mocks
|
|
3
|
+
*
|
|
4
|
+
* Mock data and functions for testing.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```typescript
|
|
8
|
+
* // Mock user data
|
|
9
|
+
* export const mockUser = {
|
|
10
|
+
* id: '507f1f77bcf86cd799439011',
|
|
11
|
+
* email: 'test@example.com',
|
|
12
|
+
* name: 'Test User',
|
|
13
|
+
* role: 'user',
|
|
14
|
+
* createdAt: new Date('2024-01-01'),
|
|
15
|
+
* updatedAt: new Date('2024-01-01'),
|
|
16
|
+
* };
|
|
17
|
+
*
|
|
18
|
+
* // Mock service response
|
|
19
|
+
* export const mockUserService = {
|
|
20
|
+
* findUserById: jest.fn().mockResolvedValue(mockUser),
|
|
21
|
+
* createUser: jest.fn().mockResolvedValue(mockUser),
|
|
22
|
+
* };
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
export const mockHealthResponse = {
|
|
27
|
+
status: 'ok',
|
|
28
|
+
timestamp: '2024-01-01T00:00:00.000Z',
|
|
29
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Jest Test Setup
|
|
3
|
+
*
|
|
4
|
+
* This file runs before any tests and sets up the test environment.
|
|
5
|
+
* It configures environment variables needed for testing.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// Set test environment variables before any modules are loaded
|
|
9
|
+
process.env.NODE_ENV = 'test';
|
|
10
|
+
process.env.JWT_SECRET = 'test-secret-key-for-testing-only';
|
|
11
|
+
process.env.MONGODB_URI = 'mongodb://localhost:27017/test-db';
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import '@fastify/jwt';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Extend Fastify types with custom properties
|
|
5
|
+
*
|
|
6
|
+
* This file augments Fastify's type definitions to include custom properties
|
|
7
|
+
* like user authentication data from JWT.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```typescript
|
|
11
|
+
* declare module '@fastify/jwt' {
|
|
12
|
+
* interface FastifyJWT {
|
|
13
|
+
* user: {
|
|
14
|
+
* id: string;
|
|
15
|
+
* email: string;
|
|
16
|
+
* role: 'user' | 'admin';
|
|
17
|
+
* };
|
|
18
|
+
* }
|
|
19
|
+
* }
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* This allows TypeScript to recognize `request.user` in route handlers:
|
|
23
|
+
* ```typescript
|
|
24
|
+
* app.get('/profile', {
|
|
25
|
+
* preHandler: [authenticate],
|
|
26
|
+
* handler: async (request) => {
|
|
27
|
+
* // TypeScript knows request.user has id, email, role
|
|
28
|
+
* const userId = request.user.id;
|
|
29
|
+
* return { userId };
|
|
30
|
+
* },
|
|
31
|
+
* });
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
declare module '@fastify/jwt' {
|
|
36
|
+
interface FastifyJWT {
|
|
37
|
+
user: {
|
|
38
|
+
id: string;
|
|
39
|
+
// Add your JWT payload properties here
|
|
40
|
+
// email?: string;
|
|
41
|
+
// role?: string;
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ES2022",
|
|
5
|
+
"lib": ["ES2022"],
|
|
6
|
+
"moduleResolution": "node",
|
|
7
|
+
"outDir": "./dist",
|
|
8
|
+
"rootDir": "./src",
|
|
9
|
+
"strict": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"forceConsistentCasingInFileNames": true,
|
|
13
|
+
"resolveJsonModule": true,
|
|
14
|
+
"declaration": true,
|
|
15
|
+
"declarationMap": true,
|
|
16
|
+
"sourceMap": true,
|
|
17
|
+
"noUnusedLocals": true,
|
|
18
|
+
"noUnusedParameters": true,
|
|
19
|
+
"noImplicitReturns": true,
|
|
20
|
+
"noFallthroughCasesInSwitch": true
|
|
21
|
+
},
|
|
22
|
+
"include": ["src/**/*"],
|
|
23
|
+
"exclude": ["node_modules", "dist", "**/*.test.ts"]
|
|
24
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# ==================================================
|
|
2
|
+
# API CONFIGURATION
|
|
3
|
+
# ==================================================
|
|
4
|
+
# URL of the backend API
|
|
5
|
+
# Development: http://localhost:3002
|
|
6
|
+
# Production: https://your-api-domain.com
|
|
7
|
+
NEXT_PUBLIC_API_URL=http://localhost:3002
|
|
8
|
+
|
|
9
|
+
# ==================================================
|
|
10
|
+
# AUTHENTICATION (if using NextAuth or similar)
|
|
11
|
+
# ==================================================
|
|
12
|
+
# NEXTAUTH_URL=http://localhost:3000
|
|
13
|
+
# NEXTAUTH_SECRET=your-secret-key-change-this
|
|
14
|
+
|
|
15
|
+
# ==================================================
|
|
16
|
+
# FEATURE FLAGS
|
|
17
|
+
# ==================================================
|
|
18
|
+
# NEXT_PUBLIC_FEATURE_ANALYTICS=false
|
|
19
|
+
|
|
20
|
+
# ==================================================
|
|
21
|
+
# THIRD-PARTY SERVICES
|
|
22
|
+
# ==================================================
|
|
23
|
+
# Add your API keys and secrets here
|
|
24
|
+
# NEXT_PUBLIC_GOOGLE_ANALYTICS_ID=
|
|
25
|
+
# NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# App Icons and Metadata
|
|
2
|
+
|
|
3
|
+
This directory contains the app metadata files required by Next.js.
|
|
4
|
+
|
|
5
|
+
## Required Icons
|
|
6
|
+
|
|
7
|
+
Replace the default placeholder icons with your own branded images:
|
|
8
|
+
|
|
9
|
+
### Social Media & SEO Images
|
|
10
|
+
|
|
11
|
+
> The images shown below are NOT included with the boilerplate and must be added for a correct application experience. Maintain sizes.
|
|
12
|
+
|
|
13
|
+
- **opengraph-image.png** (1200 x 630) - Open Graph image for social media sharing
|
|
14
|
+
- **twitter-image.png** (1200 x 600) - Twitter/X card image
|
|
15
|
+
|
|
16
|
+
### App Icons
|
|
17
|
+
|
|
18
|
+
- **icon.svg** - Vector icon (already included, customize as needed)
|
|
19
|
+
|
|
20
|
+
> The icons shown below are NOT included with the boilerplate and must be added for a correct application experience. Maintain sizes.
|
|
21
|
+
|
|
22
|
+
- **icon.png** (512 x 512) - PNG fallback icon
|
|
23
|
+
- **apple-icon.png** (180 x 180) - Apple touch icon for iOS devices
|
|
24
|
+
- **favicon.ico** (48 x 48) - Browser favicon
|
|
25
|
+
|
|
26
|
+
## Current Status
|
|
27
|
+
|
|
28
|
+
The project includes `icon.svg` as a placeholder. You should replace it and add the missing images before deploying to production.
|
|
29
|
+
|
|
30
|
+
## Generating Icons
|
|
31
|
+
|
|
32
|
+
You can use tools like:
|
|
33
|
+
|
|
34
|
+
- [Favicon Generator](https://realfavicongenerator.net/)
|
|
35
|
+
- [PWA Asset Generator](https://github.com/elegantapp/pwa-asset-generator)
|
|
36
|
+
- [Figma Export](https://www.figma.com/) for design assets
|
|
37
|
+
|
|
38
|
+
## Next.js Metadata
|
|
39
|
+
|
|
40
|
+
These files are automatically detected by Next.js and used in the app metadata. See:
|
|
41
|
+
|
|
42
|
+
- <https://nextjs.org/docs/app/api-reference/file-conventions/metadata>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
import { Nav } from '@/app/components/layout/Nav.component';
|
|
3
|
+
import { Footer } from '@/app/components/layout/Footer.component';
|
|
4
|
+
|
|
5
|
+
export default function RoutesLayout({ children }: { children: ReactNode }) {
|
|
6
|
+
return (
|
|
7
|
+
<>
|
|
8
|
+
<Nav />
|
|
9
|
+
<main className="min-h-screen">{children}</main>
|
|
10
|
+
<Footer />
|
|
11
|
+
</>
|
|
12
|
+
);
|
|
13
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { useTranslations } from 'next-intl';
|
|
2
|
+
import Link from 'next/link';
|
|
3
|
+
|
|
4
|
+
export default function HomePage() {
|
|
5
|
+
const t = useTranslations('home');
|
|
6
|
+
|
|
7
|
+
return (
|
|
8
|
+
<div className="container mx-auto px-4 py-16">
|
|
9
|
+
<div className="mx-auto max-w-3xl text-center">
|
|
10
|
+
<h1 className="text-4xl font-bold tracking-tight sm:text-5xl md:text-6xl">
|
|
11
|
+
{t('title')}
|
|
12
|
+
</h1>
|
|
13
|
+
<p className="mt-6 text-lg text-muted-foreground">{t('description')}</p>
|
|
14
|
+
|
|
15
|
+
<div className="mt-10 flex flex-col items-center gap-4 sm:flex-row sm:justify-center">
|
|
16
|
+
<a
|
|
17
|
+
href="https://github.com/{{GITHUB_USER}}/{{PROJECT_NAME}}"
|
|
18
|
+
target="_blank"
|
|
19
|
+
rel="noopener noreferrer"
|
|
20
|
+
className="inline-flex items-center justify-center rounded-md bg-primary px-6 py-3 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90"
|
|
21
|
+
>
|
|
22
|
+
View on GitHub
|
|
23
|
+
</a>
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
<div className="mt-16 grid gap-8 md:grid-cols-3">
|
|
27
|
+
<div className="rounded-lg border p-6">
|
|
28
|
+
<h3 className="text-lg font-semibold">Next.js 16</h3>
|
|
29
|
+
<p className="mt-2 text-sm text-muted-foreground">
|
|
30
|
+
Latest Next.js with App Router and React Server Components
|
|
31
|
+
</p>
|
|
32
|
+
</div>
|
|
33
|
+
<div className="rounded-lg border p-6">
|
|
34
|
+
<h3 className="text-lg font-semibold">Fastify + MongoDB</h3>
|
|
35
|
+
<p className="mt-2 text-sm text-muted-foreground">
|
|
36
|
+
High-performance API with MongoDB database
|
|
37
|
+
</p>
|
|
38
|
+
</div>
|
|
39
|
+
<div className="rounded-lg border p-6">
|
|
40
|
+
<h3 className="text-lg font-semibold">pnpm Monorepo</h3>
|
|
41
|
+
<p className="mt-2 text-sm text-muted-foreground">
|
|
42
|
+
Efficient workspace management with shared packages
|
|
43
|
+
</p>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import React, { ReactNode } from 'react';
|
|
2
|
+
import { notFound } from 'next/navigation';
|
|
3
|
+
import { NextIntlClientProvider } from 'next-intl';
|
|
4
|
+
import { getMessages } from 'next-intl/server';
|
|
5
|
+
import { locales } from '@/i18n/config';
|
|
6
|
+
|
|
7
|
+
export function generateStaticParams() {
|
|
8
|
+
return locales.map((locale) => ({ locale }));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export default async function LocaleLayout({
|
|
12
|
+
children,
|
|
13
|
+
params,
|
|
14
|
+
}: {
|
|
15
|
+
children: ReactNode;
|
|
16
|
+
params: Promise<{ locale: string }>;
|
|
17
|
+
}) {
|
|
18
|
+
// Validate locale
|
|
19
|
+
const resolvedParams = await params;
|
|
20
|
+
if (!locales.includes(resolvedParams.locale as any)) {
|
|
21
|
+
notFound();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const messages = await getMessages();
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<html lang={resolvedParams.locale}>
|
|
28
|
+
<body>
|
|
29
|
+
<NextIntlClientProvider messages={messages}>
|
|
30
|
+
{children}
|
|
31
|
+
</NextIntlClientProvider>
|
|
32
|
+
</body>
|
|
33
|
+
</html>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { useTranslations } from 'next-intl';
|
|
2
|
+
|
|
3
|
+
export default function NotFound() {
|
|
4
|
+
const t = useTranslations();
|
|
5
|
+
|
|
6
|
+
return (
|
|
7
|
+
<div className="flex min-h-screen flex-col items-center justify-center p-4">
|
|
8
|
+
<h1 className="text-4xl font-bold">404</h1>
|
|
9
|
+
<p className="mt-2 text-muted-foreground">Page not found</p>
|
|
10
|
+
</div>
|
|
11
|
+
);
|
|
12
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { useTranslations } from 'next-intl';
|
|
2
|
+
import Link from 'next/link';
|
|
3
|
+
|
|
4
|
+
export function Footer() {
|
|
5
|
+
const t = useTranslations('footer');
|
|
6
|
+
const currentYear = new Date().getFullYear();
|
|
7
|
+
|
|
8
|
+
return (
|
|
9
|
+
<footer className="border-t bg-background">
|
|
10
|
+
<div className="container mx-auto px-4 py-8">
|
|
11
|
+
<div className="flex flex-col items-center justify-between gap-4 md:flex-row">
|
|
12
|
+
<p className="text-sm text-muted-foreground">
|
|
13
|
+
{t('copyright', { year: currentYear })}
|
|
14
|
+
</p>
|
|
15
|
+
|
|
16
|
+
<div className="flex gap-6">
|
|
17
|
+
<a
|
|
18
|
+
href="https://github.com/{{GITHUB_USER}}/{{PROJECT_NAME}}"
|
|
19
|
+
target="_blank"
|
|
20
|
+
rel="noopener noreferrer"
|
|
21
|
+
className="text-sm text-muted-foreground hover:text-foreground"
|
|
22
|
+
>
|
|
23
|
+
GitHub
|
|
24
|
+
</a>
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
</footer>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { useTranslations } from 'next-intl';
|
|
2
|
+
import Link from 'next/link';
|
|
3
|
+
|
|
4
|
+
export function Nav() {
|
|
5
|
+
const t = useTranslations('nav');
|
|
6
|
+
|
|
7
|
+
return (
|
|
8
|
+
<nav className="sticky top-0 z-50 border-b bg-background/95 backdrop-blur supports-backdrop-filter:bg-background/60">
|
|
9
|
+
<div className="container mx-auto flex h-16 items-center justify-between px-4">
|
|
10
|
+
<Link href="/" className="text-xl font-bold">
|
|
11
|
+
{{PROJECT_NAME}}
|
|
12
|
+
</Link>
|
|
13
|
+
|
|
14
|
+
<ul className="flex items-center gap-6">
|
|
15
|
+
<li>
|
|
16
|
+
<Link href="/" className="text-sm font-medium hover:text-primary">
|
|
17
|
+
{t('home')}
|
|
18
|
+
</Link>
|
|
19
|
+
</li>
|
|
20
|
+
<li>
|
|
21
|
+
<Link href="/about" className="text-sm font-medium hover:text-primary">
|
|
22
|
+
{t('about')}
|
|
23
|
+
</Link>
|
|
24
|
+
</li>
|
|
25
|
+
<li>
|
|
26
|
+
<Link href="/contact" className="text-sm font-medium hover:text-primary">
|
|
27
|
+
{t('contact')}
|
|
28
|
+
</Link>
|
|
29
|
+
</li>
|
|
30
|
+
</ul>
|
|
31
|
+
</div>
|
|
32
|
+
</nav>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# UI Components
|
|
2
|
+
|
|
3
|
+
This folder contains reusable UI components following the Atomic Design pattern.
|
|
4
|
+
|
|
5
|
+
## Structure
|
|
6
|
+
|
|
7
|
+
- **atoms/** - Basic building blocks (Button, Input, Typography, etc.)
|
|
8
|
+
- **molecules/** - Combinations of atoms (Card, Modal, Form fields, etc.)
|
|
9
|
+
|
|
10
|
+
## Guidelines
|
|
11
|
+
|
|
12
|
+
1. All components should be in TypeScript
|
|
13
|
+
2. Use Tailwind CSS for styling
|
|
14
|
+
3. Interactive components need `'use client'` directive
|
|
15
|
+
4. Add JSDoc comments for documentation
|
|
16
|
+
5. Export components with `.component.tsx` suffix
|
|
17
|
+
|
|
18
|
+
## Example Atom Component
|
|
19
|
+
|
|
20
|
+
```tsx
|
|
21
|
+
// Button.component.tsx
|
|
22
|
+
interface ButtonProps {
|
|
23
|
+
variant?: 'primary' | 'secondary';
|
|
24
|
+
children: React.ReactNode;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function Button({ variant = 'primary', children }: ButtonProps) {
|
|
28
|
+
const variants = {
|
|
29
|
+
primary: 'bg-primary text-primary-foreground hover:bg-primary/90',
|
|
30
|
+
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/90',
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<button className={`rounded-md px-4 py-2 font-medium transition-colors ${variants[variant]}`}>
|
|
35
|
+
{children}
|
|
36
|
+
</button>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
```
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# UI Atoms
|
|
2
|
+
|
|
3
|
+
Basic, indivisible UI components (Atomic Design pattern).
|
|
4
|
+
|
|
5
|
+
## Purpose
|
|
6
|
+
|
|
7
|
+
Atoms are the smallest building blocks of the UI. They cannot be broken down further without losing their meaning.
|
|
8
|
+
|
|
9
|
+
## Examples
|
|
10
|
+
|
|
11
|
+
- Buttons
|
|
12
|
+
- Input fields
|
|
13
|
+
- Labels
|
|
14
|
+
- Icons
|
|
15
|
+
- Badges
|
|
16
|
+
|
|
17
|
+
## Component Template
|
|
18
|
+
|
|
19
|
+
```tsx
|
|
20
|
+
// Button.component.tsx
|
|
21
|
+
import { forwardRef } from 'react';
|
|
22
|
+
import { cn } from '@/lib/utils';
|
|
23
|
+
|
|
24
|
+
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
25
|
+
variant?: 'primary' | 'secondary' | 'ghost';
|
|
26
|
+
size?: 'sm' | 'md' | 'lg';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
|
30
|
+
({ variant = 'primary', size = 'md', className, children, ...props }, ref) => {
|
|
31
|
+
return (
|
|
32
|
+
<button
|
|
33
|
+
ref={ref}
|
|
34
|
+
className={cn(
|
|
35
|
+
'inline-flex items-center justify-center rounded-md font-medium transition-colors',
|
|
36
|
+
{
|
|
37
|
+
'bg-primary text-primary-foreground hover:bg-primary/90': variant === 'primary',
|
|
38
|
+
'bg-secondary text-secondary-foreground hover:bg-secondary/90': variant === 'secondary',
|
|
39
|
+
'hover:bg-accent hover:text-accent-foreground': variant === 'ghost',
|
|
40
|
+
'h-8 px-3 text-sm': size === 'sm',
|
|
41
|
+
'h-10 px-4': size === 'md',
|
|
42
|
+
'h-12 px-6 text-lg': size === 'lg',
|
|
43
|
+
},
|
|
44
|
+
className
|
|
45
|
+
)}
|
|
46
|
+
{...props}
|
|
47
|
+
>
|
|
48
|
+
{children}
|
|
49
|
+
</button>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
Button.displayName = 'Button';
|
|
55
|
+
```
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# UI Molecules
|
|
2
|
+
|
|
3
|
+
Composite UI components made of atoms (Atomic Design pattern).
|
|
4
|
+
|
|
5
|
+
## Purpose
|
|
6
|
+
|
|
7
|
+
Molecules are groups of atoms that work together as a unit.
|
|
8
|
+
|
|
9
|
+
## Examples
|
|
10
|
+
|
|
11
|
+
- Form fields (label + input + error message)
|
|
12
|
+
- Search bar (input + button)
|
|
13
|
+
- Card header (title + description)
|
|
14
|
+
- Avatar with name
|
|
15
|
+
|
|
16
|
+
## Component Template
|
|
17
|
+
|
|
18
|
+
```tsx
|
|
19
|
+
// FormField.component.tsx
|
|
20
|
+
import { forwardRef } from 'react';
|
|
21
|
+
import { cn } from '@/lib/utils';
|
|
22
|
+
|
|
23
|
+
interface FormFieldProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
|
24
|
+
label: string;
|
|
25
|
+
error?: string;
|
|
26
|
+
helperText?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const FormField = forwardRef<HTMLInputElement, FormFieldProps>(
|
|
30
|
+
({ label, error, helperText, className, ...props }, ref) => {
|
|
31
|
+
return (
|
|
32
|
+
<div className="space-y-2">
|
|
33
|
+
<label className="text-sm font-medium">{label}</label>
|
|
34
|
+
<input
|
|
35
|
+
ref={ref}
|
|
36
|
+
className={cn(
|
|
37
|
+
'w-full rounded-md border px-3 py-2',
|
|
38
|
+
error && 'border-destructive',
|
|
39
|
+
className
|
|
40
|
+
)}
|
|
41
|
+
{...props}
|
|
42
|
+
/>
|
|
43
|
+
{error && <p className="text-sm text-destructive">{error}</p>}
|
|
44
|
+
{helperText && <p className="text-sm text-muted-foreground">{helperText}</p>}
|
|
45
|
+
</div>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
FormField.displayName = 'FormField';
|
|
51
|
+
```
|