@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.
Files changed (64) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +204 -0
  3. package/dist/index.js +649 -0
  4. package/package.json +74 -0
  5. package/templates/backend-hono/base/.env.example +7 -0
  6. package/templates/backend-hono/base/Dockerfile +55 -0
  7. package/templates/backend-hono/base/package.json +30 -0
  8. package/templates/backend-hono/base/src/config/env.ts +12 -0
  9. package/templates/backend-hono/base/src/config/logger.ts +52 -0
  10. package/templates/backend-hono/base/src/index.ts +49 -0
  11. package/templates/backend-hono/base/src/lib/__tests__/response.test.ts +66 -0
  12. package/templates/backend-hono/base/src/lib/errors.ts +87 -0
  13. package/templates/backend-hono/base/src/lib/response.ts +72 -0
  14. package/templates/backend-hono/base/src/middleware/__tests__/error.test.ts +86 -0
  15. package/templates/backend-hono/base/src/middleware/cors.middleware.ts +9 -0
  16. package/templates/backend-hono/base/src/middleware/error.middleware.ts +43 -0
  17. package/templates/backend-hono/base/src/middleware/logger.middleware.ts +14 -0
  18. package/templates/backend-hono/base/src/middleware/rate-limit.middleware.ts +135 -0
  19. package/templates/backend-hono/base/src/routes/__tests__/health.test.ts +36 -0
  20. package/templates/backend-hono/base/src/routes/health.route.ts +14 -0
  21. package/templates/backend-hono/base/src/types/app.types.ts +26 -0
  22. package/templates/backend-hono/base/tsconfig.json +10 -0
  23. package/templates/backend-hono/base/vitest.config.ts +19 -0
  24. package/templates/backend-hono/features/mongodb-prisma/env-additions.txt +2 -0
  25. package/templates/backend-hono/features/mongodb-prisma/package-additions.json +14 -0
  26. package/templates/backend-hono/features/mongodb-prisma/prisma/schema.prisma +18 -0
  27. package/templates/backend-hono/features/mongodb-prisma/src/config/database.ts +43 -0
  28. package/templates/backend-hono/features/mongodb-prisma/src/routes/users.route.ts +82 -0
  29. package/templates/backend-hono/features/mongodb-prisma/src/services/user.service.ts +121 -0
  30. package/templates/backend-hono/features/postgres-prisma/env-additions.txt +2 -0
  31. package/templates/backend-hono/features/postgres-prisma/package-additions.json +15 -0
  32. package/templates/backend-hono/features/postgres-prisma/prisma/schema.prisma +18 -0
  33. package/templates/backend-hono/features/postgres-prisma/src/config/database.ts +43 -0
  34. package/templates/backend-hono/features/postgres-prisma/src/routes/users.route.ts +82 -0
  35. package/templates/backend-hono/features/postgres-prisma/src/services/user.service.ts +121 -0
  36. package/templates/backend-hono/features/redis/env-additions.txt +2 -0
  37. package/templates/backend-hono/features/redis/package-additions.json +8 -0
  38. package/templates/backend-hono/features/redis/src/config/redis.ts +32 -0
  39. package/templates/backend-hono/features/redis/src/services/cache.service.ts +107 -0
  40. package/templates/backend-hono/features/smtp/env-additions.txt +7 -0
  41. package/templates/backend-hono/features/smtp/package-additions.json +8 -0
  42. package/templates/backend-hono/features/smtp/src/config/mail.ts +38 -0
  43. package/templates/backend-hono/features/smtp/src/services/email.service.ts +78 -0
  44. package/templates/backend-hono/features/swagger/package-additions.json +6 -0
  45. package/templates/backend-hono/features/swagger/src/lib/openapi.ts +19 -0
  46. package/templates/backend-hono/features/swagger/src/routes/docs.route.ts +18 -0
  47. package/templates/frontend-nextjs/base/.env.example +2 -0
  48. package/templates/frontend-nextjs/base/app/globals.css +59 -0
  49. package/templates/frontend-nextjs/base/app/layout.tsx +22 -0
  50. package/templates/frontend-nextjs/base/app/page.tsx +49 -0
  51. package/templates/frontend-nextjs/base/components.json +18 -0
  52. package/templates/frontend-nextjs/base/lib/api-client.ts +67 -0
  53. package/templates/frontend-nextjs/base/lib/utils.ts +6 -0
  54. package/templates/frontend-nextjs/base/next.config.ts +8 -0
  55. package/templates/frontend-nextjs/base/package.json +33 -0
  56. package/templates/frontend-nextjs/base/postcss.config.mjs +9 -0
  57. package/templates/frontend-nextjs/base/public/.gitkeep +1 -0
  58. package/templates/frontend-nextjs/base/tailwind.config.ts +58 -0
  59. package/templates/frontend-nextjs/base/tsconfig.json +19 -0
  60. package/templates/shared/base/package.json +19 -0
  61. package/templates/shared/base/src/index.ts +5 -0
  62. package/templates/shared/base/src/schemas/user.schema.ts +34 -0
  63. package/templates/shared/base/src/types.ts +46 -0
  64. 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,2 @@
1
+ # Redis Configuration
2
+ REDIS_URL="redis://localhost:6379"
@@ -0,0 +1,8 @@
1
+ {
2
+ "dependencies": {
3
+ "ioredis": "^5.4.1"
4
+ },
5
+ "devDependencies": {
6
+ "@types/ioredis": "^5.0.0"
7
+ }
8
+ }
@@ -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,7 @@
1
+ # SMTP Configuration
2
+ SMTP_HOST="smtp.gmail.com"
3
+ SMTP_PORT="587"
4
+ SMTP_USER="your-email@gmail.com"
5
+ SMTP_PASS="your-app-password"
6
+ SMTP_FROM="noreply@__PROJECT_NAME__.com"
7
+ WEB_URL="http://localhost:__WEB_PORT__"
@@ -0,0 +1,8 @@
1
+ {
2
+ "dependencies": {
3
+ "nodemailer": "^6.9.16"
4
+ },
5
+ "devDependencies": {
6
+ "@types/nodemailer": "^6.4.16"
7
+ }
8
+ }
@@ -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,6 @@
1
+ {
2
+ "dependencies": {
3
+ "@hono/zod-openapi": "^0.18.4",
4
+ "@scalar/hono-api-reference": "^0.5.175"
5
+ }
6
+ }
@@ -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,2 @@
1
+ # API Configuration
2
+ NEXT_PUBLIC_API_URL=__API_URL__
@@ -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
+ }
@@ -0,0 +1,6 @@
1
+ import { type ClassValue, clsx } from 'clsx';
2
+ import { twMerge } from 'tailwind-merge';
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs));
6
+ }