@yoms/create-monorepo 1.0.2
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/LICENSE +21 -0
- package/README.md +204 -0
- package/dist/index.js +649 -0
- package/package.json +74 -0
- package/templates/backend-hono/base/.env.example +7 -0
- package/templates/backend-hono/base/Dockerfile +55 -0
- package/templates/backend-hono/base/package.json +30 -0
- package/templates/backend-hono/base/src/config/env.ts +12 -0
- package/templates/backend-hono/base/src/config/logger.ts +52 -0
- package/templates/backend-hono/base/src/index.ts +49 -0
- package/templates/backend-hono/base/src/lib/__tests__/response.test.ts +66 -0
- package/templates/backend-hono/base/src/lib/errors.ts +87 -0
- package/templates/backend-hono/base/src/lib/response.ts +72 -0
- package/templates/backend-hono/base/src/middleware/__tests__/error.test.ts +86 -0
- package/templates/backend-hono/base/src/middleware/cors.middleware.ts +9 -0
- package/templates/backend-hono/base/src/middleware/error.middleware.ts +43 -0
- package/templates/backend-hono/base/src/middleware/logger.middleware.ts +14 -0
- package/templates/backend-hono/base/src/middleware/rate-limit.middleware.ts +135 -0
- package/templates/backend-hono/base/src/routes/__tests__/health.test.ts +36 -0
- package/templates/backend-hono/base/src/routes/health.route.ts +14 -0
- package/templates/backend-hono/base/src/types/app.types.ts +26 -0
- package/templates/backend-hono/base/tsconfig.json +10 -0
- package/templates/backend-hono/base/vitest.config.ts +19 -0
- package/templates/backend-hono/features/mongodb-prisma/env-additions.txt +2 -0
- package/templates/backend-hono/features/mongodb-prisma/package-additions.json +14 -0
- package/templates/backend-hono/features/mongodb-prisma/prisma/schema.prisma +18 -0
- package/templates/backend-hono/features/mongodb-prisma/src/config/database.ts +43 -0
- package/templates/backend-hono/features/mongodb-prisma/src/routes/users.route.ts +82 -0
- package/templates/backend-hono/features/mongodb-prisma/src/services/user.service.ts +121 -0
- package/templates/backend-hono/features/postgres-prisma/env-additions.txt +2 -0
- package/templates/backend-hono/features/postgres-prisma/package-additions.json +15 -0
- package/templates/backend-hono/features/postgres-prisma/prisma/schema.prisma +18 -0
- package/templates/backend-hono/features/postgres-prisma/src/config/database.ts +43 -0
- package/templates/backend-hono/features/postgres-prisma/src/routes/users.route.ts +82 -0
- package/templates/backend-hono/features/postgres-prisma/src/services/user.service.ts +121 -0
- package/templates/backend-hono/features/redis/env-additions.txt +2 -0
- package/templates/backend-hono/features/redis/package-additions.json +8 -0
- package/templates/backend-hono/features/redis/src/config/redis.ts +32 -0
- package/templates/backend-hono/features/redis/src/services/cache.service.ts +107 -0
- package/templates/backend-hono/features/smtp/env-additions.txt +7 -0
- package/templates/backend-hono/features/smtp/package-additions.json +8 -0
- package/templates/backend-hono/features/smtp/src/config/mail.ts +38 -0
- package/templates/backend-hono/features/smtp/src/services/email.service.ts +78 -0
- package/templates/backend-hono/features/swagger/package-additions.json +6 -0
- package/templates/backend-hono/features/swagger/src/lib/openapi.ts +19 -0
- package/templates/backend-hono/features/swagger/src/routes/docs.route.ts +18 -0
- package/templates/frontend-nextjs/base/.env.example +2 -0
- package/templates/frontend-nextjs/base/app/globals.css +59 -0
- package/templates/frontend-nextjs/base/app/layout.tsx +22 -0
- package/templates/frontend-nextjs/base/app/page.tsx +49 -0
- package/templates/frontend-nextjs/base/components.json +18 -0
- package/templates/frontend-nextjs/base/lib/api-client.ts +67 -0
- package/templates/frontend-nextjs/base/lib/utils.ts +6 -0
- package/templates/frontend-nextjs/base/next.config.ts +8 -0
- package/templates/frontend-nextjs/base/package.json +33 -0
- package/templates/frontend-nextjs/base/postcss.config.mjs +9 -0
- package/templates/frontend-nextjs/base/public/.gitkeep +1 -0
- package/templates/frontend-nextjs/base/tailwind.config.ts +58 -0
- package/templates/frontend-nextjs/base/tsconfig.json +19 -0
- package/templates/shared/base/package.json +19 -0
- package/templates/shared/base/src/index.ts +5 -0
- package/templates/shared/base/src/schemas/user.schema.ts +34 -0
- package/templates/shared/base/src/types.ts +46 -0
- package/templates/shared/base/tsconfig.json +9 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { prisma } from '../config/database.js';
|
|
2
|
+
import { NotFoundError, ConflictError } from '../lib/errors.js';
|
|
3
|
+
import type { CreateUser, UpdateUser, User } from '__PACKAGE_SCOPE__/shared';
|
|
4
|
+
|
|
5
|
+
export class UserService {
|
|
6
|
+
/**
|
|
7
|
+
* Get all users with pagination
|
|
8
|
+
*/
|
|
9
|
+
static async getAll(page: number = 1, limit: number = 10) {
|
|
10
|
+
const skip = (page - 1) * limit;
|
|
11
|
+
|
|
12
|
+
const [users, total] = await Promise.all([
|
|
13
|
+
prisma.user.findMany({
|
|
14
|
+
skip,
|
|
15
|
+
take: limit,
|
|
16
|
+
orderBy: { createdAt: 'desc' },
|
|
17
|
+
}),
|
|
18
|
+
prisma.user.count(),
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
return { users, total, page, limit };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Get user by ID
|
|
26
|
+
*/
|
|
27
|
+
static async getById(id: string): Promise<User> {
|
|
28
|
+
const user = await prisma.user.findUnique({
|
|
29
|
+
where: { id },
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
if (!user) {
|
|
33
|
+
throw new NotFoundError(`User with ID ${id} not found`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return user;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Get user by email
|
|
41
|
+
*/
|
|
42
|
+
static async getByEmail(email: string): Promise<User | null> {
|
|
43
|
+
return prisma.user.findUnique({
|
|
44
|
+
where: { email },
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Create new user
|
|
50
|
+
*/
|
|
51
|
+
static async create(data: CreateUser): Promise<User> {
|
|
52
|
+
// Check if email already exists
|
|
53
|
+
const existing = await this.getByEmail(data.email);
|
|
54
|
+
if (existing) {
|
|
55
|
+
throw new ConflictError(`User with email ${data.email} already exists`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return prisma.user.create({
|
|
59
|
+
data,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Update user
|
|
65
|
+
*/
|
|
66
|
+
static async update(id: string, data: UpdateUser): Promise<User> {
|
|
67
|
+
// Verify user exists
|
|
68
|
+
await this.getById(id);
|
|
69
|
+
|
|
70
|
+
// If email is being updated, check it's not taken
|
|
71
|
+
if (data.email) {
|
|
72
|
+
const existing = await this.getByEmail(data.email);
|
|
73
|
+
if (existing && existing.id !== id) {
|
|
74
|
+
throw new ConflictError(`Email ${data.email} is already taken`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return prisma.user.update({
|
|
79
|
+
where: { id },
|
|
80
|
+
data,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Delete user
|
|
86
|
+
*/
|
|
87
|
+
static async delete(id: string): Promise<void> {
|
|
88
|
+
// Verify user exists
|
|
89
|
+
await this.getById(id);
|
|
90
|
+
|
|
91
|
+
await prisma.user.delete({
|
|
92
|
+
where: { id },
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Search users by name or email
|
|
98
|
+
*/
|
|
99
|
+
static async search(query: string, page: number = 1, limit: number = 10) {
|
|
100
|
+
const skip = (page - 1) * limit;
|
|
101
|
+
|
|
102
|
+
const where = {
|
|
103
|
+
OR: [
|
|
104
|
+
{ name: { contains: query, mode: 'insensitive' as const } },
|
|
105
|
+
{ email: { contains: query, mode: 'insensitive' as const } },
|
|
106
|
+
],
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const [users, total] = await Promise.all([
|
|
110
|
+
prisma.user.findMany({
|
|
111
|
+
where,
|
|
112
|
+
skip,
|
|
113
|
+
take: limit,
|
|
114
|
+
orderBy: { createdAt: 'desc' },
|
|
115
|
+
}),
|
|
116
|
+
prisma.user.count({ where }),
|
|
117
|
+
]);
|
|
118
|
+
|
|
119
|
+
return { users, total, page, limit };
|
|
120
|
+
}
|
|
121
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import Redis from 'ioredis';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { logger } from './logger.js';
|
|
4
|
+
|
|
5
|
+
// Validate Redis environment variables
|
|
6
|
+
const redisEnvSchema = z.object({
|
|
7
|
+
REDIS_URL: z.string().default('redis://localhost:6379'),
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
const redisEnv = redisEnvSchema.parse(process.env);
|
|
11
|
+
|
|
12
|
+
export const redis = new Redis(redisEnv.REDIS_URL, {
|
|
13
|
+
maxRetriesPerRequest: 3,
|
|
14
|
+
retryStrategy: (times) => {
|
|
15
|
+
const delay = Math.min(times * 50, 2000);
|
|
16
|
+
return delay;
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
redis.on('connect', () => {
|
|
21
|
+
logger.info('Redis connected successfully');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
redis.on('error', (error) => {
|
|
25
|
+
logger.error('Redis connection error:', error);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// Graceful shutdown
|
|
29
|
+
process.on('beforeExit', async () => {
|
|
30
|
+
await redis.quit();
|
|
31
|
+
logger.info('Redis disconnected');
|
|
32
|
+
});
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { redis } from '../config/redis.js';
|
|
2
|
+
import { logger } from '../config/logger.js';
|
|
3
|
+
|
|
4
|
+
export class CacheService {
|
|
5
|
+
/**
|
|
6
|
+
* Get cached value
|
|
7
|
+
*/
|
|
8
|
+
static async get<T>(key: string): Promise<T | null> {
|
|
9
|
+
try {
|
|
10
|
+
const value = await redis.get(key);
|
|
11
|
+
if (!value) return null;
|
|
12
|
+
return JSON.parse(value) as T;
|
|
13
|
+
} catch (error) {
|
|
14
|
+
logger.error('Cache get error:', { key, error });
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Set cached value with optional TTL (in seconds)
|
|
21
|
+
*/
|
|
22
|
+
static async set(key: string, value: unknown, ttl?: number): Promise<boolean> {
|
|
23
|
+
try {
|
|
24
|
+
const serialized = JSON.stringify(value);
|
|
25
|
+
if (ttl) {
|
|
26
|
+
await redis.setex(key, ttl, serialized);
|
|
27
|
+
} else {
|
|
28
|
+
await redis.set(key, serialized);
|
|
29
|
+
}
|
|
30
|
+
return true;
|
|
31
|
+
} catch (error) {
|
|
32
|
+
logger.error('Cache set error:', { key, error });
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Delete cached value
|
|
39
|
+
*/
|
|
40
|
+
static async delete(key: string): Promise<boolean> {
|
|
41
|
+
try {
|
|
42
|
+
await redis.del(key);
|
|
43
|
+
return true;
|
|
44
|
+
} catch (error) {
|
|
45
|
+
logger.error('Cache delete error:', { key, error });
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Delete cached values matching pattern using SCAN
|
|
52
|
+
* (Non-blocking alternative to KEYS command)
|
|
53
|
+
*/
|
|
54
|
+
static async deletePattern(pattern: string): Promise<number> {
|
|
55
|
+
try {
|
|
56
|
+
let cursor = '0';
|
|
57
|
+
let deletedCount = 0;
|
|
58
|
+
|
|
59
|
+
do {
|
|
60
|
+
// Use SCAN instead of KEYS to avoid blocking Redis
|
|
61
|
+
const [nextCursor, keys] = await redis.scan(
|
|
62
|
+
cursor,
|
|
63
|
+
'MATCH',
|
|
64
|
+
pattern,
|
|
65
|
+
'COUNT',
|
|
66
|
+
100
|
|
67
|
+
);
|
|
68
|
+
cursor = nextCursor;
|
|
69
|
+
|
|
70
|
+
if (keys.length > 0) {
|
|
71
|
+
deletedCount += await redis.del(...keys);
|
|
72
|
+
}
|
|
73
|
+
} while (cursor !== '0');
|
|
74
|
+
|
|
75
|
+
return deletedCount;
|
|
76
|
+
} catch (error) {
|
|
77
|
+
logger.error('Cache delete pattern error:', { pattern, error });
|
|
78
|
+
return 0;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Check if key exists
|
|
84
|
+
*/
|
|
85
|
+
static async exists(key: string): Promise<boolean> {
|
|
86
|
+
try {
|
|
87
|
+
const result = await redis.exists(key);
|
|
88
|
+
return result === 1;
|
|
89
|
+
} catch (error) {
|
|
90
|
+
logger.error('Cache exists error:', { key, error });
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Set expiry on existing key
|
|
97
|
+
*/
|
|
98
|
+
static async expire(key: string, ttl: number): Promise<boolean> {
|
|
99
|
+
try {
|
|
100
|
+
const result = await redis.expire(key, ttl);
|
|
101
|
+
return result === 1;
|
|
102
|
+
} catch (error) {
|
|
103
|
+
logger.error('Cache expire error:', { key, ttl, error });
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import nodemailer from 'nodemailer';
|
|
2
|
+
import type { Transporter } from 'nodemailer';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { logger } from './logger.js';
|
|
5
|
+
|
|
6
|
+
// Validate SMTP environment variables
|
|
7
|
+
const smtpEnvSchema = z.object({
|
|
8
|
+
SMTP_HOST: z.string().default('localhost'),
|
|
9
|
+
SMTP_PORT: z.coerce.number().default(587),
|
|
10
|
+
SMTP_USER: z.string().optional(),
|
|
11
|
+
SMTP_PASS: z.string().optional(),
|
|
12
|
+
SMTP_FROM: z.string().email().default('noreply@example.com'),
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const smtpEnv = smtpEnvSchema.parse(process.env);
|
|
16
|
+
|
|
17
|
+
export const transporter: Transporter = nodemailer.createTransporter({
|
|
18
|
+
host: smtpEnv.SMTP_HOST,
|
|
19
|
+
port: smtpEnv.SMTP_PORT,
|
|
20
|
+
secure: smtpEnv.SMTP_PORT === 465,
|
|
21
|
+
auth: smtpEnv.SMTP_USER && smtpEnv.SMTP_PASS ? {
|
|
22
|
+
user: smtpEnv.SMTP_USER,
|
|
23
|
+
pass: smtpEnv.SMTP_PASS,
|
|
24
|
+
} : undefined,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// Verify connection
|
|
28
|
+
transporter.verify((error) => {
|
|
29
|
+
if (error) {
|
|
30
|
+
logger.error('SMTP connection failed:', error);
|
|
31
|
+
} else {
|
|
32
|
+
logger.info('SMTP server ready');
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
export const mailConfig = {
|
|
37
|
+
from: smtpEnv.SMTP_FROM,
|
|
38
|
+
};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { transporter, mailConfig } from '../config/mail.js';
|
|
2
|
+
import { logger } from '../config/logger.js';
|
|
3
|
+
|
|
4
|
+
export interface EmailOptions {
|
|
5
|
+
to: string | string[];
|
|
6
|
+
subject: string;
|
|
7
|
+
text?: string;
|
|
8
|
+
html?: string;
|
|
9
|
+
from?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class EmailService {
|
|
13
|
+
/**
|
|
14
|
+
* Send email
|
|
15
|
+
*/
|
|
16
|
+
static async send(options: EmailOptions): Promise<boolean> {
|
|
17
|
+
try {
|
|
18
|
+
const info = await transporter.sendMail({
|
|
19
|
+
from: options.from || mailConfig.from,
|
|
20
|
+
to: Array.isArray(options.to) ? options.to.join(', ') : options.to,
|
|
21
|
+
subject: options.subject,
|
|
22
|
+
text: options.text,
|
|
23
|
+
html: options.html,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
logger.info('Email sent successfully:', {
|
|
27
|
+
messageId: info.messageId,
|
|
28
|
+
to: options.to,
|
|
29
|
+
subject: options.subject,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
return true;
|
|
33
|
+
} catch (error) {
|
|
34
|
+
logger.error('Failed to send email:', {
|
|
35
|
+
error,
|
|
36
|
+
to: options.to,
|
|
37
|
+
subject: options.subject,
|
|
38
|
+
});
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Send welcome email
|
|
45
|
+
*/
|
|
46
|
+
static async sendWelcome(email: string, name: string): Promise<boolean> {
|
|
47
|
+
return this.send({
|
|
48
|
+
to: email,
|
|
49
|
+
subject: 'Welcome to __PROJECT_NAME__!',
|
|
50
|
+
html: `
|
|
51
|
+
<h1>Welcome, ${name}!</h1>
|
|
52
|
+
<p>Thank you for joining __PROJECT_NAME__.</p>
|
|
53
|
+
<p>We're excited to have you on board!</p>
|
|
54
|
+
`,
|
|
55
|
+
text: `Welcome, ${name}! Thank you for joining __PROJECT_NAME__. We're excited to have you on board!`,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Send password reset email
|
|
61
|
+
*/
|
|
62
|
+
static async sendPasswordReset(email: string, resetToken: string): Promise<boolean> {
|
|
63
|
+
const resetUrl = `${process.env.WEB_URL}/reset-password?token=${resetToken}`;
|
|
64
|
+
|
|
65
|
+
return this.send({
|
|
66
|
+
to: email,
|
|
67
|
+
subject: 'Password Reset Request',
|
|
68
|
+
html: `
|
|
69
|
+
<h1>Password Reset</h1>
|
|
70
|
+
<p>You requested a password reset. Click the link below to reset your password:</p>
|
|
71
|
+
<a href="${resetUrl}">${resetUrl}</a>
|
|
72
|
+
<p>This link will expire in 1 hour.</p>
|
|
73
|
+
<p>If you didn't request this, please ignore this email.</p>
|
|
74
|
+
`,
|
|
75
|
+
text: `You requested a password reset. Visit this link to reset your password: ${resetUrl}. This link will expire in 1 hour.`,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { OpenAPIHono } from '@hono/zod-openapi';
|
|
2
|
+
|
|
3
|
+
export const app = new OpenAPIHono();
|
|
4
|
+
|
|
5
|
+
// Add OpenAPI documentation
|
|
6
|
+
app.doc('/openapi.json', {
|
|
7
|
+
openapi: '3.0.0',
|
|
8
|
+
info: {
|
|
9
|
+
version: '0.1.0',
|
|
10
|
+
title: '__PROJECT_NAME__ API',
|
|
11
|
+
description: 'API documentation for __PROJECT_NAME__',
|
|
12
|
+
},
|
|
13
|
+
servers: [
|
|
14
|
+
{
|
|
15
|
+
url: 'http://localhost:__API_PORT__',
|
|
16
|
+
description: 'Development server',
|
|
17
|
+
},
|
|
18
|
+
],
|
|
19
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { OpenAPIHono } from '@hono/zod-openapi';
|
|
2
|
+
import { apiReference } from '@scalar/hono-api-reference';
|
|
3
|
+
|
|
4
|
+
const docs = new OpenAPIHono();
|
|
5
|
+
|
|
6
|
+
// Scalar UI for API documentation
|
|
7
|
+
docs.get(
|
|
8
|
+
'/',
|
|
9
|
+
apiReference({
|
|
10
|
+
spec: {
|
|
11
|
+
url: '/openapi.json',
|
|
12
|
+
},
|
|
13
|
+
theme: 'purple',
|
|
14
|
+
layout: 'modern',
|
|
15
|
+
})
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
export { docs };
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
@tailwind base;
|
|
2
|
+
@tailwind components;
|
|
3
|
+
@tailwind utilities;
|
|
4
|
+
|
|
5
|
+
@layer base {
|
|
6
|
+
:root {
|
|
7
|
+
--background: 0 0% 100%;
|
|
8
|
+
--foreground: 222.2 84% 4.9%;
|
|
9
|
+
--card: 0 0% 100%;
|
|
10
|
+
--card-foreground: 222.2 84% 4.9%;
|
|
11
|
+
--popover: 0 0% 100%;
|
|
12
|
+
--popover-foreground: 222.2 84% 4.9%;
|
|
13
|
+
--primary: 221.2 83.2% 53.3%;
|
|
14
|
+
--primary-foreground: 210 40% 98%;
|
|
15
|
+
--secondary: 210 40% 96.1%;
|
|
16
|
+
--secondary-foreground: 222.2 47.4% 11.2%;
|
|
17
|
+
--muted: 210 40% 96.1%;
|
|
18
|
+
--muted-foreground: 215.4 16.3% 46.9%;
|
|
19
|
+
--accent: 210 40% 96.1%;
|
|
20
|
+
--accent-foreground: 222.2 47.4% 11.2%;
|
|
21
|
+
--destructive: 0 84.2% 60.2%;
|
|
22
|
+
--destructive-foreground: 210 40% 98%;
|
|
23
|
+
--border: 214.3 31.8% 91.4%;
|
|
24
|
+
--input: 214.3 31.8% 91.4%;
|
|
25
|
+
--ring: 221.2 83.2% 53.3%;
|
|
26
|
+
--radius: 0.5rem;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.dark {
|
|
30
|
+
--background: 222.2 84% 4.9%;
|
|
31
|
+
--foreground: 210 40% 98%;
|
|
32
|
+
--card: 222.2 84% 4.9%;
|
|
33
|
+
--card-foreground: 210 40% 98%;
|
|
34
|
+
--popover: 222.2 84% 4.9%;
|
|
35
|
+
--popover-foreground: 210 40% 98%;
|
|
36
|
+
--primary: 217.2 91.2% 59.8%;
|
|
37
|
+
--primary-foreground: 222.2 47.4% 11.2%;
|
|
38
|
+
--secondary: 217.2 32.6% 17.5%;
|
|
39
|
+
--secondary-foreground: 210 40% 98%;
|
|
40
|
+
--muted: 217.2 32.6% 17.5%;
|
|
41
|
+
--muted-foreground: 215 20.2% 65.1%;
|
|
42
|
+
--accent: 217.2 32.6% 17.5%;
|
|
43
|
+
--accent-foreground: 210 40% 98%;
|
|
44
|
+
--destructive: 0 62.8% 30.6%;
|
|
45
|
+
--destructive-foreground: 210 40% 98%;
|
|
46
|
+
--border: 217.2 32.6% 17.5%;
|
|
47
|
+
--input: 217.2 32.6% 17.5%;
|
|
48
|
+
--ring: 224.3 76.3% 48%;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
@layer base {
|
|
53
|
+
* {
|
|
54
|
+
@apply border-border;
|
|
55
|
+
}
|
|
56
|
+
body {
|
|
57
|
+
@apply bg-background text-foreground;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Metadata } from 'next';
|
|
2
|
+
import { Inter } from 'next/font/google';
|
|
3
|
+
import './globals.css';
|
|
4
|
+
|
|
5
|
+
const inter = Inter({ subsets: ['latin'] });
|
|
6
|
+
|
|
7
|
+
export const metadata: Metadata = {
|
|
8
|
+
title: '__PROJECT_NAME__',
|
|
9
|
+
description: 'Generated with create-monorepo',
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export default function RootLayout({
|
|
13
|
+
children,
|
|
14
|
+
}: Readonly<{
|
|
15
|
+
children: React.ReactNode;
|
|
16
|
+
}>) {
|
|
17
|
+
return (
|
|
18
|
+
<html lang="en">
|
|
19
|
+
<body className={inter.className}>{children}</body>
|
|
20
|
+
</html>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export default function Home() {
|
|
2
|
+
return (
|
|
3
|
+
<main className="flex min-h-screen flex-col items-center justify-center p-24">
|
|
4
|
+
<div className="max-w-5xl w-full items-center justify-center font-mono text-sm">
|
|
5
|
+
<h1 className="text-4xl font-bold text-center mb-8">
|
|
6
|
+
Welcome to __PROJECT_NAME__
|
|
7
|
+
</h1>
|
|
8
|
+
<p className="text-center text-muted-foreground mb-8">
|
|
9
|
+
Get started by editing <code className="font-mono font-bold">app/page.tsx</code>
|
|
10
|
+
</p>
|
|
11
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 max-w-2xl mx-auto">
|
|
12
|
+
<a
|
|
13
|
+
href="https://nextjs.org/docs"
|
|
14
|
+
target="_blank"
|
|
15
|
+
rel="noopener noreferrer"
|
|
16
|
+
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 dark:hover:border-neutral-700 dark:hover:bg-neutral-800/30"
|
|
17
|
+
>
|
|
18
|
+
<h2 className="mb-3 text-2xl font-semibold">
|
|
19
|
+
Next.js Docs{' '}
|
|
20
|
+
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
|
|
21
|
+
→
|
|
22
|
+
</span>
|
|
23
|
+
</h2>
|
|
24
|
+
<p className="m-0 max-w-[30ch] text-sm opacity-50">
|
|
25
|
+
Find in-depth information about Next.js features and API.
|
|
26
|
+
</p>
|
|
27
|
+
</a>
|
|
28
|
+
|
|
29
|
+
<a
|
|
30
|
+
href="https://ui.shadcn.com"
|
|
31
|
+
target="_blank"
|
|
32
|
+
rel="noopener noreferrer"
|
|
33
|
+
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 dark:hover:border-neutral-700 dark:hover:bg-neutral-800/30"
|
|
34
|
+
>
|
|
35
|
+
<h2 className="mb-3 text-2xl font-semibold">
|
|
36
|
+
shadcn/ui{' '}
|
|
37
|
+
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
|
|
38
|
+
→
|
|
39
|
+
</span>
|
|
40
|
+
</h2>
|
|
41
|
+
<p className="m-0 max-w-[30ch] text-sm opacity-50">
|
|
42
|
+
Beautifully designed components built with Radix UI and Tailwind CSS.
|
|
43
|
+
</p>
|
|
44
|
+
</a>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
</main>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema.json",
|
|
3
|
+
"style": "new-york",
|
|
4
|
+
"rsc": true,
|
|
5
|
+
"tsx": true,
|
|
6
|
+
"tailwind": {
|
|
7
|
+
"config": "tailwind.config.ts",
|
|
8
|
+
"css": "app/globals.css",
|
|
9
|
+
"baseColor": "slate",
|
|
10
|
+
"cssVariables": true,
|
|
11
|
+
"prefix": ""
|
|
12
|
+
},
|
|
13
|
+
"aliases": {
|
|
14
|
+
"components": "@/components",
|
|
15
|
+
"utils": "@/lib/utils",
|
|
16
|
+
"ui": "@/components/ui"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { ApiResponse } from '__PACKAGE_SCOPE__/shared';
|
|
2
|
+
|
|
3
|
+
const API_URL = process.env.NEXT_PUBLIC_API_URL || '__API_URL__';
|
|
4
|
+
|
|
5
|
+
export class ApiClient {
|
|
6
|
+
private static async request<T>(
|
|
7
|
+
endpoint: string,
|
|
8
|
+
options?: RequestInit
|
|
9
|
+
): Promise<ApiResponse<T>> {
|
|
10
|
+
const url = `${API_URL}${endpoint}`;
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
const response = await fetch(url, {
|
|
14
|
+
...options,
|
|
15
|
+
headers: {
|
|
16
|
+
'Content-Type': 'application/json',
|
|
17
|
+
...options?.headers,
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const data = await response.json();
|
|
22
|
+
|
|
23
|
+
if (!response.ok) {
|
|
24
|
+
return {
|
|
25
|
+
success: false,
|
|
26
|
+
error: data.error || 'Request failed',
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return data;
|
|
31
|
+
} catch (error) {
|
|
32
|
+
return {
|
|
33
|
+
success: false,
|
|
34
|
+
error: error instanceof Error ? error.message : 'Network error',
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
static async get<T>(endpoint: string): Promise<ApiResponse<T>> {
|
|
40
|
+
return this.request<T>(endpoint, { method: 'GET' });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
static async post<T>(endpoint: string, body: unknown): Promise<ApiResponse<T>> {
|
|
44
|
+
return this.request<T>(endpoint, {
|
|
45
|
+
method: 'POST',
|
|
46
|
+
body: JSON.stringify(body),
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
static async put<T>(endpoint: string, body: unknown): Promise<ApiResponse<T>> {
|
|
51
|
+
return this.request<T>(endpoint, {
|
|
52
|
+
method: 'PUT',
|
|
53
|
+
body: JSON.stringify(body),
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
static async patch<T>(endpoint: string, body: unknown): Promise<ApiResponse<T>> {
|
|
58
|
+
return this.request<T>(endpoint, {
|
|
59
|
+
method: 'PATCH',
|
|
60
|
+
body: JSON.stringify(body),
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
static async delete<T>(endpoint: string): Promise<ApiResponse<T>> {
|
|
65
|
+
return this.request<T>(endpoint, { method: 'DELETE' });
|
|
66
|
+
}
|
|
67
|
+
}
|