metabinaries 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/LICENSE +21 -0
- package/README.md +43 -0
- package/index.js +164 -0
- package/package.json +32 -0
- package/src/constants.js +62 -0
- package/src/templates/app.js +527 -0
- package/src/templates/configs.js +303 -0
- package/src/templates/core.js +328 -0
- package/src/templates/folder-structure.js +21 -0
- package/src/templates/layout.js +279 -0
- package/src/templates/misc.js +277 -0
- package/src/templates/packages.js +42 -0
- package/src/templates/ui-2.js +585 -0
- package/src/templates/ui-3.js +606 -0
- package/src/templates/ui-4.js +615 -0
- package/src/templates/ui.js +777 -0
- package/src/utils.js +38 -0
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
export const configTemplates = {
|
|
2
|
+
'.gitattributes': `# Auto detect text files and perform LF normalization
|
|
3
|
+
* text=auto
|
|
4
|
+
|
|
5
|
+
# Force Unix line endings for shell scripts
|
|
6
|
+
*.sh text eol=lf
|
|
7
|
+
.husky/* text eol=lf
|
|
8
|
+
|
|
9
|
+
# Force Windows line endings for batch files
|
|
10
|
+
*.bat text eol=crlf
|
|
11
|
+
|
|
12
|
+
# Binary files
|
|
13
|
+
*.png binary
|
|
14
|
+
*.jpg binary
|
|
15
|
+
*.ico binary
|
|
16
|
+
*.gif binary
|
|
17
|
+
*.woff binary
|
|
18
|
+
*.woff2 binary
|
|
19
|
+
`,
|
|
20
|
+
|
|
21
|
+
'.gitignore': `# dependencies
|
|
22
|
+
/node_modules
|
|
23
|
+
/.pnp
|
|
24
|
+
.pnp.js
|
|
25
|
+
|
|
26
|
+
# testing
|
|
27
|
+
/coverage
|
|
28
|
+
|
|
29
|
+
# next.js
|
|
30
|
+
/.next/
|
|
31
|
+
/out/
|
|
32
|
+
|
|
33
|
+
# production
|
|
34
|
+
/build
|
|
35
|
+
|
|
36
|
+
# debug
|
|
37
|
+
npm-debug.log*
|
|
38
|
+
yarn-debug.log*
|
|
39
|
+
yarn-error.log*
|
|
40
|
+
|
|
41
|
+
# local env files
|
|
42
|
+
.env
|
|
43
|
+
.env.local
|
|
44
|
+
.env.development.local
|
|
45
|
+
.env.test.local
|
|
46
|
+
.env.production.local
|
|
47
|
+
|
|
48
|
+
# vercel
|
|
49
|
+
.vercel
|
|
50
|
+
|
|
51
|
+
# typescript
|
|
52
|
+
*.tsbuildinfo
|
|
53
|
+
next-env.d.ts`,
|
|
54
|
+
|
|
55
|
+
'.env': `NEXT_PUBLIC_API_URL=https://api.example.com
|
|
56
|
+
NEXT_PUBLIC_APP_URL=https://app.example.com
|
|
57
|
+
NEXT_PUBLIC_SITE_URL=https://example.com
|
|
58
|
+
NEXT_PUBLIC_DEFAULT_LOCALE=en`,
|
|
59
|
+
|
|
60
|
+
'.env.local': `NEXT_PUBLIC_API_URL=http://localhost:3000/api
|
|
61
|
+
NEXT_PUBLIC_APP_URL=http://localhost:3000
|
|
62
|
+
NEXT_PUBLIC_SITE_URL=http://localhost:3000
|
|
63
|
+
NEXT_PUBLIC_DEFAULT_LOCALE=en`,
|
|
64
|
+
|
|
65
|
+
'.env.example': `NEXT_PUBLIC_API_URL=
|
|
66
|
+
NEXT_PUBLIC_APP_URL=
|
|
67
|
+
NEXT_PUBLIC_SITE_URL=
|
|
68
|
+
NEXT_PUBLIC_DEFAULT_LOCALE=en`,
|
|
69
|
+
|
|
70
|
+
'api.yml': projectName => `openapi: 3.0.0
|
|
71
|
+
info:
|
|
72
|
+
title: ${projectName} API
|
|
73
|
+
version: 1.0.0
|
|
74
|
+
paths: {}`,
|
|
75
|
+
|
|
76
|
+
'.versionrc.json': `{
|
|
77
|
+
"types": [
|
|
78
|
+
{"type": "feat", "section": "Features"},
|
|
79
|
+
{"type": "fix", "section": "Bug Fixes"}
|
|
80
|
+
]
|
|
81
|
+
}`,
|
|
82
|
+
|
|
83
|
+
'components.json': `{
|
|
84
|
+
"$schema": "https://ui.shadcn.com/schema.json",
|
|
85
|
+
"style": "default",
|
|
86
|
+
"rsc": true,
|
|
87
|
+
"tsx": true,
|
|
88
|
+
"tailwind": {
|
|
89
|
+
"config": "tailwind.config.ts",
|
|
90
|
+
"css": "app/[locale]/globals.css",
|
|
91
|
+
"baseColor": "slate",
|
|
92
|
+
"cssVariables": true,
|
|
93
|
+
"prefix": ""
|
|
94
|
+
},
|
|
95
|
+
"aliases": {
|
|
96
|
+
"components": "@/components",
|
|
97
|
+
"utils": "@/lib/utils"
|
|
98
|
+
}
|
|
99
|
+
}`,
|
|
100
|
+
|
|
101
|
+
'eslint.config.mjs': `export default [];`,
|
|
102
|
+
|
|
103
|
+
'next.config.ts': `import type { NextConfig } from "next";
|
|
104
|
+
import createNextIntlPlugin from "next-intl/plugin";
|
|
105
|
+
|
|
106
|
+
const withNextIntl = createNextIntlPlugin();
|
|
107
|
+
|
|
108
|
+
const nextConfig: NextConfig = {
|
|
109
|
+
|
|
110
|
+
async headers() {
|
|
111
|
+
return [
|
|
112
|
+
{
|
|
113
|
+
source: "/api/(.*)",
|
|
114
|
+
headers: [
|
|
115
|
+
{ key: "Access-Control-Allow-Credentials", value: "true" },
|
|
116
|
+
{ key: "Access-Control-Allow-Origin", value: "*" }, // Recommended: Change to specific domain in production
|
|
117
|
+
{
|
|
118
|
+
key: "Access-Control-Allow-Methods",
|
|
119
|
+
value: "GET, POST, PUT, DELETE, OPTIONS, PATCH",
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
key: "Access-Control-Allow-Headers",
|
|
123
|
+
value: "Content-Type, Authorization, x-user-id, Accept",
|
|
124
|
+
},
|
|
125
|
+
],
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
source: "/(.*)",
|
|
129
|
+
headers: [
|
|
130
|
+
{ key: "X-Frame-Options", value: "DENY" },
|
|
131
|
+
{ key: "X-Content-Type-Options", value: "nosniff" },
|
|
132
|
+
{ key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
|
|
133
|
+
{ key: "Permissions-Policy", value: "camera=(), microphone=(), geolocation=()" },
|
|
134
|
+
{ key: "Strict-Transport-Security", value: "max-age=31536000; includeSubDomains; preload" },
|
|
135
|
+
{ key: "X-XSS-Protection", value: "1; mode=block" },
|
|
136
|
+
],
|
|
137
|
+
},
|
|
138
|
+
];
|
|
139
|
+
},
|
|
140
|
+
|
|
141
|
+
allowedDevOrigins: [
|
|
142
|
+
"http://localhost:3000",
|
|
143
|
+
...(process.env.NEXT_PUBLIC_API_URL
|
|
144
|
+
? [process.env.NEXT_PUBLIC_API_URL]
|
|
145
|
+
: []),
|
|
146
|
+
"https://api-dev.smartbinaries.com/",
|
|
147
|
+
],
|
|
148
|
+
|
|
149
|
+
images: {
|
|
150
|
+
remotePatterns: [
|
|
151
|
+
{
|
|
152
|
+
protocol: "https",
|
|
153
|
+
hostname: "**",
|
|
154
|
+
port: "",
|
|
155
|
+
pathname: "/**",
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
protocol: "http",
|
|
159
|
+
hostname: "**",
|
|
160
|
+
port: "",
|
|
161
|
+
pathname: "/**",
|
|
162
|
+
},
|
|
163
|
+
],
|
|
164
|
+
unoptimized: true,
|
|
165
|
+
formats: ["image/avif", "image/webp"],
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
export default withNextIntl(nextConfig);`,
|
|
170
|
+
|
|
171
|
+
'postcss.config.mjs': `const config = {
|
|
172
|
+
plugins: {
|
|
173
|
+
"@tailwindcss/postcss": {},
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
export default config;`,
|
|
178
|
+
|
|
179
|
+
'tailwind.config.ts': `import type { Config } from "tailwindcss";
|
|
180
|
+
|
|
181
|
+
const config: Config = {
|
|
182
|
+
darkMode: "class",
|
|
183
|
+
content: [
|
|
184
|
+
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
|
|
185
|
+
"./components/**/*.{js,ts,jsx,tsx,mdx}",
|
|
186
|
+
"./app/**/*.{js,ts,jsx,tsx,mdx}",
|
|
187
|
+
],
|
|
188
|
+
theme: {
|
|
189
|
+
extend: {
|
|
190
|
+
colors: {
|
|
191
|
+
background: 'hsl(var(--background))',
|
|
192
|
+
foreground: 'hsl(var(--foreground))',
|
|
193
|
+
card: {
|
|
194
|
+
DEFAULT: 'hsl(var(--card))',
|
|
195
|
+
foreground: 'hsl(var(--card-foreground))'
|
|
196
|
+
},
|
|
197
|
+
popover: {
|
|
198
|
+
DEFAULT: 'hsl(var(--popover))',
|
|
199
|
+
foreground: 'hsl(var(--popover-foreground))'
|
|
200
|
+
},
|
|
201
|
+
primary: {
|
|
202
|
+
DEFAULT: 'hsl(var(--primary))',
|
|
203
|
+
foreground: 'hsl(var(--primary-foreground))'
|
|
204
|
+
},
|
|
205
|
+
secondary: {
|
|
206
|
+
DEFAULT: 'hsl(var(--secondary))',
|
|
207
|
+
foreground: 'hsl(var(--secondary-foreground))'
|
|
208
|
+
},
|
|
209
|
+
muted: {
|
|
210
|
+
DEFAULT: 'hsl(var(--muted))',
|
|
211
|
+
foreground: 'hsl(var(--muted-foreground))'
|
|
212
|
+
},
|
|
213
|
+
accent: {
|
|
214
|
+
DEFAULT: 'hsl(var(--accent))',
|
|
215
|
+
foreground: 'hsl(var(--accent-foreground))'
|
|
216
|
+
},
|
|
217
|
+
destructive: {
|
|
218
|
+
DEFAULT: 'hsl(var(--destructive))',
|
|
219
|
+
foreground: 'hsl(var(--destructive-foreground))'
|
|
220
|
+
},
|
|
221
|
+
border: 'hsl(var(--border))',
|
|
222
|
+
input: 'hsl(var(--input))',
|
|
223
|
+
ring: 'hsl(var(--ring))',
|
|
224
|
+
chart: {
|
|
225
|
+
'1': 'hsl(var(--chart-1))',
|
|
226
|
+
'2': 'hsl(var(--chart-2))',
|
|
227
|
+
'3': 'hsl(var(--chart-3))',
|
|
228
|
+
'4': 'hsl(var(--chart-4))',
|
|
229
|
+
'5': 'hsl(var(--chart-5))'
|
|
230
|
+
}
|
|
231
|
+
},
|
|
232
|
+
borderRadius: {
|
|
233
|
+
lg: 'var(--radius)',
|
|
234
|
+
md: 'calc(var(--radius) - 2px)',
|
|
235
|
+
sm: 'calc(var(--radius) - 4px)'
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
},
|
|
239
|
+
plugins: [require("tailwindcss-animate")],
|
|
240
|
+
};
|
|
241
|
+
export default config;`,
|
|
242
|
+
|
|
243
|
+
'tsconfig.json': `{
|
|
244
|
+
"compilerOptions": {
|
|
245
|
+
"target": "ES2017",
|
|
246
|
+
"lib": ["dom", "dom.iterable", "esnext"],
|
|
247
|
+
"allowJs": true,
|
|
248
|
+
"skipLibCheck": true,
|
|
249
|
+
"strict": true,
|
|
250
|
+
"noEmit": true,
|
|
251
|
+
"esModuleInterop": true,
|
|
252
|
+
"module": "esnext",
|
|
253
|
+
"moduleResolution": "bundler",
|
|
254
|
+
"resolveJsonModule": true,
|
|
255
|
+
"isolatedModules": true,
|
|
256
|
+
"jsx": "react-jsx",
|
|
257
|
+
"incremental": true,
|
|
258
|
+
"plugins": [
|
|
259
|
+
{
|
|
260
|
+
"name": "next"
|
|
261
|
+
}
|
|
262
|
+
],
|
|
263
|
+
"paths": {
|
|
264
|
+
"@/*": ["./*"]
|
|
265
|
+
}
|
|
266
|
+
},
|
|
267
|
+
"include": [
|
|
268
|
+
"next-env.d.ts",
|
|
269
|
+
"**/*.ts",
|
|
270
|
+
"**/*.tsx",
|
|
271
|
+
".next/types/**/*.ts",
|
|
272
|
+
".next/dev/types/**/*.ts",
|
|
273
|
+
"**/*.mts",
|
|
274
|
+
"app/[locale]/(auth)/layout.tsx"
|
|
275
|
+
],
|
|
276
|
+
"exclude": ["node_modules"]
|
|
277
|
+
}`,
|
|
278
|
+
'i18n.ts': `import { getRequestConfig } from 'next-intl/server';
|
|
279
|
+
import { notFound } from 'next/navigation';
|
|
280
|
+
import { routing } from './i18n/routing';
|
|
281
|
+
|
|
282
|
+
export default getRequestConfig(async ({ locale }) => {
|
|
283
|
+
// Validate that the incoming \`locale\` parameter is valid
|
|
284
|
+
if (!locale || !routing.locales.includes(locale as any)) notFound();
|
|
285
|
+
|
|
286
|
+
return {
|
|
287
|
+
locale,
|
|
288
|
+
messages: (await import(\`./messages/\${locale}.json\`)).default,
|
|
289
|
+
};
|
|
290
|
+
});`,
|
|
291
|
+
'messages/en.json': `{
|
|
292
|
+
"HomePage": {
|
|
293
|
+
"title": "Welcome to MetaBinaries",
|
|
294
|
+
"description": "Your CRM-style workspace is ready."
|
|
295
|
+
}
|
|
296
|
+
}`,
|
|
297
|
+
'messages/ar.json': `{
|
|
298
|
+
"HomePage": {
|
|
299
|
+
"title": "مرحباً بك في ميتابيناريز",
|
|
300
|
+
"description": "بيئة عمل CRM الخاصة بك جاهزة."
|
|
301
|
+
}
|
|
302
|
+
}`,
|
|
303
|
+
};
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
export const coreTemplates = {
|
|
2
|
+
'lib/utils.ts': `import { clsx, type ClassValue } from 'clsx';
|
|
3
|
+
import { twMerge } from 'tailwind-merge';
|
|
4
|
+
|
|
5
|
+
export function cn(...inputs: ClassValue[]) {
|
|
6
|
+
return twMerge(clsx(inputs));
|
|
7
|
+
}`,
|
|
8
|
+
|
|
9
|
+
'hooks/use-mobile.tsx': `import * as React from 'react';
|
|
10
|
+
|
|
11
|
+
const MOBILE_BREAKPOINT = 768;
|
|
12
|
+
|
|
13
|
+
export function useIsMobile() {
|
|
14
|
+
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(
|
|
15
|
+
undefined
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
React.useEffect(() => {
|
|
19
|
+
const mql = window.matchMedia(\`(max-width: \${MOBILE_BREAKPOINT - 1}px)\`);
|
|
20
|
+
const onChange = () => {
|
|
21
|
+
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
|
|
22
|
+
};
|
|
23
|
+
mql.addEventListener('change', onChange);
|
|
24
|
+
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
|
|
25
|
+
return () => mql.removeEventListener('change', onChange);
|
|
26
|
+
}, []);
|
|
27
|
+
|
|
28
|
+
return !!isMobile;
|
|
29
|
+
}`,
|
|
30
|
+
|
|
31
|
+
'hooks/use-mobile.ts': `export function useIsMobile() { return false; }`,
|
|
32
|
+
|
|
33
|
+
'lib/rate-limit.ts': `import { NextRequest, NextResponse } from 'next/server';
|
|
34
|
+
|
|
35
|
+
// Rate limiting configuration
|
|
36
|
+
const RATE_LIMIT_CONFIG = {
|
|
37
|
+
windowMs: 15 * 60 * 1000, // 15 minutes
|
|
38
|
+
maxRequests: 5, // Maximum 5 requests per window
|
|
39
|
+
message: 'Too many contact form submissions, please try again later.',
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// Store for rate limiting (in production, use Redis or similar)
|
|
43
|
+
const rateLimitStore = new Map<string, { count: number; resetTime: number }>();
|
|
44
|
+
|
|
45
|
+
export function rateLimitMiddleware(request: NextRequest) {
|
|
46
|
+
const ip =
|
|
47
|
+
request.headers.get('x-forwarded-for') ||
|
|
48
|
+
request.headers.get('x-real-ip') ||
|
|
49
|
+
request.headers.get('cf-connecting-ip') ||
|
|
50
|
+
'unknown';
|
|
51
|
+
const now = Date.now();
|
|
52
|
+
const windowMs = RATE_LIMIT_CONFIG.windowMs;
|
|
53
|
+
|
|
54
|
+
// Clean up expired entries
|
|
55
|
+
for (const [key, value] of rateLimitStore.entries()) {
|
|
56
|
+
if (now > value.resetTime) {
|
|
57
|
+
rateLimitStore.delete(key);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const current = rateLimitStore.get(ip);
|
|
62
|
+
|
|
63
|
+
if (!current) {
|
|
64
|
+
// First request from this IP
|
|
65
|
+
rateLimitStore.set(ip, {
|
|
66
|
+
count: 1,
|
|
67
|
+
resetTime: now + windowMs,
|
|
68
|
+
});
|
|
69
|
+
return null; // No rate limit exceeded
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (now > current.resetTime) {
|
|
73
|
+
// Window has expired, reset
|
|
74
|
+
rateLimitStore.set(ip, {
|
|
75
|
+
count: 1,
|
|
76
|
+
resetTime: now + windowMs,
|
|
77
|
+
});
|
|
78
|
+
return null; // No rate limit exceeded
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (current.count >= RATE_LIMIT_CONFIG.maxRequests) {
|
|
82
|
+
// Rate limit exceeded
|
|
83
|
+
return NextResponse.json(
|
|
84
|
+
{
|
|
85
|
+
success: false,
|
|
86
|
+
error: RATE_LIMIT_CONFIG.message,
|
|
87
|
+
retryAfter: Math.ceil((current.resetTime - now) / 1000),
|
|
88
|
+
},
|
|
89
|
+
{ status: 429 }
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Increment count
|
|
94
|
+
current.count++;
|
|
95
|
+
rateLimitStore.set(ip, current);
|
|
96
|
+
|
|
97
|
+
return null; // No rate limit exceeded
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Alternative implementation using a more sophisticated approach
|
|
101
|
+
export class RateLimiter {
|
|
102
|
+
private store = new Map<string, { count: number; resetTime: number }>();
|
|
103
|
+
|
|
104
|
+
constructor(
|
|
105
|
+
private windowMs: number = 15 * 60 * 1000, // 15 minutes
|
|
106
|
+
private maxRequests: number = 5
|
|
107
|
+
) {}
|
|
108
|
+
|
|
109
|
+
isAllowed(identifier: string): { allowed: boolean; retryAfter?: number } {
|
|
110
|
+
const now = Date.now();
|
|
111
|
+
|
|
112
|
+
// Clean up expired entries
|
|
113
|
+
this.cleanup();
|
|
114
|
+
|
|
115
|
+
const current = this.store.get(identifier);
|
|
116
|
+
|
|
117
|
+
if (!current) {
|
|
118
|
+
this.store.set(identifier, {
|
|
119
|
+
count: 1,
|
|
120
|
+
resetTime: now + this.windowMs,
|
|
121
|
+
});
|
|
122
|
+
return { allowed: true };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (now > current.resetTime) {
|
|
126
|
+
// Window has expired, reset
|
|
127
|
+
this.store.set(identifier, {
|
|
128
|
+
count: 1,
|
|
129
|
+
resetTime: now + this.windowMs,
|
|
130
|
+
});
|
|
131
|
+
return { allowed: true };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (current.count >= this.maxRequests) {
|
|
135
|
+
return {
|
|
136
|
+
allowed: false,
|
|
137
|
+
retryAfter: Math.ceil((current.resetTime - now) / 1000),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Increment count
|
|
142
|
+
current.count++;
|
|
143
|
+
this.store.set(identifier, current);
|
|
144
|
+
|
|
145
|
+
return { allowed: true };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
private cleanup() {
|
|
149
|
+
const now = Date.now();
|
|
150
|
+
for (const [key, value] of this.store.entries()) {
|
|
151
|
+
if (now > value.resetTime) {
|
|
152
|
+
this.store.delete(key);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Get remaining requests for an identifier
|
|
158
|
+
getRemainingRequests(identifier: string): number {
|
|
159
|
+
const current = this.store.get(identifier);
|
|
160
|
+
if (!current) return this.maxRequests;
|
|
161
|
+
|
|
162
|
+
const now = Date.now();
|
|
163
|
+
if (now > current.resetTime) return this.maxRequests;
|
|
164
|
+
|
|
165
|
+
return Math.max(0, this.maxRequests - current.count);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Get reset time for an identifier
|
|
169
|
+
getResetTime(identifier: string): number | null {
|
|
170
|
+
const current = this.store.get(identifier);
|
|
171
|
+
if (!current) return null;
|
|
172
|
+
|
|
173
|
+
const now = Date.now();
|
|
174
|
+
if (now > current.resetTime) return null;
|
|
175
|
+
|
|
176
|
+
return current.resetTime;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Create a global rate limiter instance
|
|
181
|
+
export const contactFormRateLimiter = new RateLimiter(
|
|
182
|
+
15 * 60 * 1000, // 15 minutes
|
|
183
|
+
5 // 5 requests per window
|
|
184
|
+
);`,
|
|
185
|
+
|
|
186
|
+
'lib/features/NAME/NAME.api.ts': `export const nameApi = {};`,
|
|
187
|
+
'lib/features/NAME/NAME.service.ts': `export const nameService = {};`,
|
|
188
|
+
'lib/features/NAME/NAME.slice.ts': `import { createSlice } from '@reduxjs/toolkit';
|
|
189
|
+
export const nameSlice = createSlice({ name: 'name', initialState: {}, reducers: {} });
|
|
190
|
+
export default nameSlice.reducer;`,
|
|
191
|
+
'lib/features/NAME/NAME.types.ts': `export interface NameType {}`,
|
|
192
|
+
'lib/features/NAME/NAME.utils.ts': `export const nameUtils = {};`,
|
|
193
|
+
'lib/features/NAME/useNAME.ts': `export const useName = () => ({});`,
|
|
194
|
+
|
|
195
|
+
'lib/shared/store.ts': `import { configureStore } from '@reduxjs/toolkit';
|
|
196
|
+
export const store = configureStore({ reducer: {} });`,
|
|
197
|
+
|
|
198
|
+
'lib/shared/axios.ts': `import axios from 'axios';
|
|
199
|
+
|
|
200
|
+
const instance = axios.create({
|
|
201
|
+
baseURL: process.env.NEXT_PUBLIC_API_URL || '/api',
|
|
202
|
+
headers: {
|
|
203
|
+
'Content-Type': 'application/json',
|
|
204
|
+
},
|
|
205
|
+
timeout: 10000, // Security: Add timeout to prevent hanging requests
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
// Request Interceptor: Add Authorization header
|
|
209
|
+
instance.interceptors.request.use(
|
|
210
|
+
(config) => {
|
|
211
|
+
// Security: Ensure token is retrieved safely
|
|
212
|
+
const token = typeof window !== 'undefined' ? localStorage.getItem('token') : null;
|
|
213
|
+
if (token) {
|
|
214
|
+
config.headers.Authorization = \`Bearer \${token}\`;
|
|
215
|
+
}
|
|
216
|
+
return config;
|
|
217
|
+
},
|
|
218
|
+
(error) => Promise.reject(error)
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
// Response Interceptor: Handle global errors (e.g., 401)
|
|
222
|
+
instance.interceptors.response.use(
|
|
223
|
+
(response) => response,
|
|
224
|
+
(error) => {
|
|
225
|
+
if (error.response?.status === 401) {
|
|
226
|
+
// Security: Clear sensible data on unauthorized access
|
|
227
|
+
if (typeof window !== 'undefined') {
|
|
228
|
+
localStorage.removeItem('token');
|
|
229
|
+
const locale = window.location.pathname.split('/')[1] || 'en';
|
|
230
|
+
window.location.href = \`/\${locale}/login\`;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Security: Sanitize error responses before logging/showing to users
|
|
235
|
+
const sanitizedError = {
|
|
236
|
+
message: error.response?.data?.message || error.message || 'An unexpected error occurred',
|
|
237
|
+
status: error.response?.status,
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
return Promise.reject(sanitizedError);
|
|
241
|
+
}
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
export default instance;`,
|
|
245
|
+
|
|
246
|
+
'lib/shared/utils/validation/common-rules.ts': `import { z } from 'zod';
|
|
247
|
+
export const commonRules = { email: z.string().email() };`,
|
|
248
|
+
|
|
249
|
+
'lib/shared/utils/validation/NAME.validation.ts': `import { z } from 'zod';
|
|
250
|
+
export const nameValidation = z.object({});`,
|
|
251
|
+
|
|
252
|
+
'i18n/routing.ts': `import { defineRouting } from 'next-intl/routing';
|
|
253
|
+
|
|
254
|
+
export const routing = defineRouting({
|
|
255
|
+
// A list of all locales that are supported
|
|
256
|
+
locales: ['en', 'ar'],
|
|
257
|
+
|
|
258
|
+
// Used when no locale matches
|
|
259
|
+
defaultLocale:
|
|
260
|
+
(process.env.NEXT_PUBLIC_DEFAULT_LOCALE as 'en' | 'ar') || 'en',
|
|
261
|
+
localePrefix: 'always',
|
|
262
|
+
});`,
|
|
263
|
+
|
|
264
|
+
'i18n/navigation.ts': `import { createNavigation } from 'next-intl/navigation';
|
|
265
|
+
import { routing } from './routing';
|
|
266
|
+
|
|
267
|
+
// Lightweight wrappers around Next.js' navigation
|
|
268
|
+
// APIs that consider the routing configuration
|
|
269
|
+
export const { Link, redirect, usePathname, useRouter, getPathname } =
|
|
270
|
+
createNavigation(routing);`,
|
|
271
|
+
|
|
272
|
+
'i18n/request.ts': `import { getRequestConfig } from 'next-intl/server';
|
|
273
|
+
import { hasLocale } from 'next-intl';
|
|
274
|
+
import { routing } from './routing';
|
|
275
|
+
|
|
276
|
+
export default getRequestConfig(async ({ requestLocale }) => {
|
|
277
|
+
// Typically corresponds to the \`[locale]\` segment
|
|
278
|
+
const requested = await requestLocale;
|
|
279
|
+
let locale = hasLocale(routing.locales, requested)
|
|
280
|
+
? requested
|
|
281
|
+
: routing.defaultLocale;
|
|
282
|
+
|
|
283
|
+
// Safety check: ensure locale is never undefined or empty
|
|
284
|
+
if (!locale || !hasLocale(routing.locales, locale)) {
|
|
285
|
+
locale = 'en'; // Fallback to 'en' as the ultimate default
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return {
|
|
289
|
+
locale,
|
|
290
|
+
messages: (await import(\`@/messages/\${locale}.json\`)).default,
|
|
291
|
+
};
|
|
292
|
+
});`,
|
|
293
|
+
|
|
294
|
+
'middleware.ts': `import createMiddleware from 'next-intl/middleware';
|
|
295
|
+
import { routing } from './i18n/routing';
|
|
296
|
+
|
|
297
|
+
export default createMiddleware(routing);
|
|
298
|
+
|
|
299
|
+
export const config = {
|
|
300
|
+
matcher: ['/', '/(ar|en)/:path*']
|
|
301
|
+
};`,
|
|
302
|
+
|
|
303
|
+
'lib/features/notifications/NotificationContext.tsx': `'use client';
|
|
304
|
+
|
|
305
|
+
import React, { createContext, useContext, useState, ReactNode } from 'react';
|
|
306
|
+
|
|
307
|
+
interface NotificationContextType {
|
|
308
|
+
// Add notification state and methods here
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const NotificationContext = createContext<NotificationContextType | undefined>(undefined);
|
|
312
|
+
|
|
313
|
+
export function NotificationProvider({ children }: { children: ReactNode }) {
|
|
314
|
+
return (
|
|
315
|
+
<NotificationContext.Provider value={{}}>
|
|
316
|
+
{children}
|
|
317
|
+
</NotificationContext.Provider>
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
export function useNotifications() {
|
|
322
|
+
const context = useContext(NotificationContext);
|
|
323
|
+
if (context === undefined) {
|
|
324
|
+
throw new Error('useNotifications must be used within a NotificationProvider');
|
|
325
|
+
}
|
|
326
|
+
return context;
|
|
327
|
+
}`,
|
|
328
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export const folderStructure = [
|
|
2
|
+
'.husky',
|
|
3
|
+
'.husky/_',
|
|
4
|
+
'app/[locale]/(auth)/forgot-password',
|
|
5
|
+
'app/[locale]/(auth)/login',
|
|
6
|
+
'app/[locale]/(auth)/sign-up',
|
|
7
|
+
'app/[locale]/(auth)/_components',
|
|
8
|
+
'app/[locale]/(workspace)/(home)/_components',
|
|
9
|
+
'app/[locale]/(workspace)/_Components',
|
|
10
|
+
'components/layout',
|
|
11
|
+
'components/shared',
|
|
12
|
+
'components/ui',
|
|
13
|
+
'docs',
|
|
14
|
+
'hooks',
|
|
15
|
+
'i18n',
|
|
16
|
+
'lib/features/NAME',
|
|
17
|
+
'lib/shared/utils/validation',
|
|
18
|
+
'messages',
|
|
19
|
+
'public/images',
|
|
20
|
+
'public/icons',
|
|
21
|
+
];
|