@lucifer91299/create-portal-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/dist/cli/index.js +886 -0
- package/package.json +22 -0
|
@@ -0,0 +1,886 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
// src/cli/index.ts
|
|
5
|
+
var import_fs = require("fs");
|
|
6
|
+
var import_path = require("path");
|
|
7
|
+
|
|
8
|
+
// src/cli/prompt.ts
|
|
9
|
+
var import_readline = require("readline");
|
|
10
|
+
var rl = (0, import_readline.createInterface)({ input: process.stdin, output: process.stderr });
|
|
11
|
+
function ask(question, defaultVal = "") {
|
|
12
|
+
return new Promise((resolve) => {
|
|
13
|
+
const display = defaultVal ? `${question} (${defaultVal}): ` : `${question}: `;
|
|
14
|
+
rl.question(display, (ans) => resolve(ans.trim() || defaultVal));
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
async function select(question, options) {
|
|
18
|
+
process.stderr.write(`
|
|
19
|
+
${question}
|
|
20
|
+
`);
|
|
21
|
+
options.forEach((o, i) => {
|
|
22
|
+
const hint = o.hint ? ` \u2014 ${o.hint}` : "";
|
|
23
|
+
process.stderr.write(` ${i + 1}. ${o.label}${hint}
|
|
24
|
+
`);
|
|
25
|
+
});
|
|
26
|
+
const raw = await ask(`Enter number (1\u2013${options.length})`, "1");
|
|
27
|
+
const idx = Math.max(0, Math.min(parseInt(raw, 10) - 1, options.length - 1));
|
|
28
|
+
const chosen = options[isNaN(idx) ? 0 : idx];
|
|
29
|
+
process.stderr.write(` \u2192 ${chosen.label}
|
|
30
|
+
`);
|
|
31
|
+
return chosen.value;
|
|
32
|
+
}
|
|
33
|
+
function closePrompt() {
|
|
34
|
+
rl.close();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// src/templates/index.ts
|
|
38
|
+
function genPackageJson(o) {
|
|
39
|
+
const deps = {
|
|
40
|
+
"@lucifer91299/ui": o.localUiPath ? `file:${o.localUiPath}` : "^1.0.0",
|
|
41
|
+
"next": "^15.3.0",
|
|
42
|
+
"react": "^19.0.0",
|
|
43
|
+
"react-dom": "^19.0.0",
|
|
44
|
+
"framer-motion": "^12.0.0",
|
|
45
|
+
"axios": "^1.7.9",
|
|
46
|
+
"@tanstack/react-query": "^5.64.1",
|
|
47
|
+
"jose": "^5.9.6",
|
|
48
|
+
"clsx": "^2.1.1",
|
|
49
|
+
"tailwind-merge": "^2.5.5",
|
|
50
|
+
"lucide-react": "^0.469.0"
|
|
51
|
+
};
|
|
52
|
+
if (o.stateManagement === "redux-query") {
|
|
53
|
+
deps["@reduxjs/toolkit"] = "^2.5.0";
|
|
54
|
+
deps["react-redux"] = "^9.2.0";
|
|
55
|
+
}
|
|
56
|
+
if (o.authMode === "laravel") {
|
|
57
|
+
deps["laravel-session-sdk"] = "^1.4.9";
|
|
58
|
+
}
|
|
59
|
+
if (o.includeI18n) {
|
|
60
|
+
deps["next-intl"] = "^3.26.3";
|
|
61
|
+
}
|
|
62
|
+
const devDeps = {
|
|
63
|
+
"typescript": "^5.7.3",
|
|
64
|
+
"@types/node": "^22.10.7",
|
|
65
|
+
"@types/react": "^19.0.7",
|
|
66
|
+
"@types/react-dom": "^19.0.3",
|
|
67
|
+
"tailwindcss": "^3.4.17",
|
|
68
|
+
"postcss": "^8.4.49",
|
|
69
|
+
"autoprefixer": "^10.4.20"
|
|
70
|
+
};
|
|
71
|
+
return JSON.stringify({
|
|
72
|
+
name: o.projectName,
|
|
73
|
+
version: "0.1.0",
|
|
74
|
+
private: true,
|
|
75
|
+
description: o.projectDescription,
|
|
76
|
+
scripts: {
|
|
77
|
+
dev: "next dev",
|
|
78
|
+
build: "next build",
|
|
79
|
+
start: "next start",
|
|
80
|
+
lint: "next lint",
|
|
81
|
+
typecheck: "tsc --noEmit"
|
|
82
|
+
},
|
|
83
|
+
dependencies: deps,
|
|
84
|
+
devDependencies: devDeps
|
|
85
|
+
}, null, 2);
|
|
86
|
+
}
|
|
87
|
+
function genTailwindConfig(_o) {
|
|
88
|
+
return `import type { Config } from 'tailwindcss'
|
|
89
|
+
import preset from '@lucifer91299/ui/tailwind/preset'
|
|
90
|
+
|
|
91
|
+
// Brand colors are set via ThemeProvider CSS variables (see src/theme.config.ts).
|
|
92
|
+
// Do NOT override color tokens here \u2014 that would bypass runtime theming.
|
|
93
|
+
const config: Config = {
|
|
94
|
+
presets: [preset],
|
|
95
|
+
content: ['./src/**/*.{ts,tsx}'],
|
|
96
|
+
plugins: [],
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export default config
|
|
100
|
+
`;
|
|
101
|
+
}
|
|
102
|
+
function genThemeConfig(o) {
|
|
103
|
+
return `import { createTheme } from '@lucifer91299/ui'
|
|
104
|
+
|
|
105
|
+
export default createTheme({
|
|
106
|
+
primary: '${o.primaryColor}',
|
|
107
|
+
accent: '${o.accentColor}',
|
|
108
|
+
success: '${o.successColor}',
|
|
109
|
+
logoSrc: '${o.brandLogoPath || "/brand/logo.svg"}',
|
|
110
|
+
poweredByLogoSrc: '/brand/powered-by-logo.svg',
|
|
111
|
+
poweredByText: 'Powered by',
|
|
112
|
+
projectName: '${o.projectName}',
|
|
113
|
+
sidebar: '${o.sidebarStyle}',
|
|
114
|
+
loginStyle: '${o.loginStyle}',
|
|
115
|
+
})
|
|
116
|
+
`;
|
|
117
|
+
}
|
|
118
|
+
function genEnvLocal(o) {
|
|
119
|
+
const lines = [];
|
|
120
|
+
if (o.authMode === "jwt" || o.authMode === "multi-role") {
|
|
121
|
+
lines.push(`NEXT_PUBLIC_API_URL=${o.apiUrl ?? "http://localhost:3000"}`);
|
|
122
|
+
if (o.authMode === "jwt") {
|
|
123
|
+
lines.push(`JWT_SECRET=${o.jwtSecret ?? "change-me-in-production"}`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
if (o.authMode === "laravel") {
|
|
127
|
+
lines.push(`NEXT_PUBLIC_LARAVEL_URL=${o.laravelUrl ?? "http://localhost:8000"}`);
|
|
128
|
+
lines.push(`SESSION_DB_HOST=${o.dbHost ?? "127.0.0.1"}`);
|
|
129
|
+
lines.push(`SESSION_DB_PORT=${o.dbPort ?? "3306"}`);
|
|
130
|
+
lines.push(`SESSION_DB_NAME=${o.dbName ?? "laravel"}`);
|
|
131
|
+
lines.push(`SESSION_DB_USER=${o.dbUser ?? "root"}`);
|
|
132
|
+
lines.push(`SESSION_DB_PASS=${o.dbPassword ?? ""}`);
|
|
133
|
+
}
|
|
134
|
+
return lines.join("\n") + "\n";
|
|
135
|
+
}
|
|
136
|
+
function genGlobalsCSS() {
|
|
137
|
+
return `/* Project-specific utilities \u2014 SDK component styles are in @lucifer91299/ui/styles/components.css */
|
|
138
|
+
@tailwind base;
|
|
139
|
+
@tailwind components;
|
|
140
|
+
@tailwind utilities;
|
|
141
|
+
`;
|
|
142
|
+
}
|
|
143
|
+
function genRootLayout(o) {
|
|
144
|
+
return `import type { Metadata } from 'next'
|
|
145
|
+
import { Inter } from 'next/font/google'
|
|
146
|
+
import '@lucifer91299/ui/styles/components.css'
|
|
147
|
+
import './globals.css'
|
|
148
|
+
import { Providers } from '@/providers'
|
|
149
|
+
|
|
150
|
+
const inter = Inter({ subsets: ['latin'], variable: '--font-sans' })
|
|
151
|
+
|
|
152
|
+
export const metadata: Metadata = {
|
|
153
|
+
title: '${o.projectName}',
|
|
154
|
+
description: '${o.projectDescription}',
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
158
|
+
return (
|
|
159
|
+
<html lang="en" className={inter.variable}>
|
|
160
|
+
<body>
|
|
161
|
+
<Providers>{children}</Providers>
|
|
162
|
+
</body>
|
|
163
|
+
</html>
|
|
164
|
+
)
|
|
165
|
+
}
|
|
166
|
+
`;
|
|
167
|
+
}
|
|
168
|
+
function genProviders(o) {
|
|
169
|
+
const imports = [
|
|
170
|
+
`'use client'`,
|
|
171
|
+
`import { type ReactNode } from 'react'`,
|
|
172
|
+
`import { ThemeProvider } from '@lucifer91299/ui'`,
|
|
173
|
+
`import { QueryClient, QueryClientProvider } from '@tanstack/react-query'`,
|
|
174
|
+
`import theme from '@/theme.config'`
|
|
175
|
+
];
|
|
176
|
+
const setup = [
|
|
177
|
+
`const queryClient = new QueryClient()`
|
|
178
|
+
];
|
|
179
|
+
if (o.stateManagement === "redux-query") {
|
|
180
|
+
imports.push(`import { Provider as ReduxProvider } from 'react-redux'`);
|
|
181
|
+
imports.push(`import { store } from '@/store'`);
|
|
182
|
+
}
|
|
183
|
+
const wrap = (inner) => {
|
|
184
|
+
let result = inner;
|
|
185
|
+
result = `<QueryClientProvider client={queryClient}>${result}</QueryClientProvider>`;
|
|
186
|
+
if (o.stateManagement === "redux-query") {
|
|
187
|
+
result = `<ReduxProvider store={store}>${result}</ReduxProvider>`;
|
|
188
|
+
}
|
|
189
|
+
result = `<ThemeProvider theme={theme}>${result}</ThemeProvider>`;
|
|
190
|
+
return result;
|
|
191
|
+
};
|
|
192
|
+
return `${imports.join("\n")}
|
|
193
|
+
|
|
194
|
+
${setup.join("\n")}
|
|
195
|
+
|
|
196
|
+
export function Providers({ children }: { children: ReactNode }) {
|
|
197
|
+
return (
|
|
198
|
+
${wrap("{children}")}
|
|
199
|
+
)
|
|
200
|
+
}
|
|
201
|
+
`;
|
|
202
|
+
}
|
|
203
|
+
function genLoginPage(o) {
|
|
204
|
+
const isAnimated = o.loginStyle !== "simple";
|
|
205
|
+
const component = isAnimated ? "LoginPage" : "LoginPageSimple";
|
|
206
|
+
const credField = isAnimated ? "identifier" : "email";
|
|
207
|
+
const poweredByProp = isAnimated ? ` poweredBy={{
|
|
208
|
+
logoSrc: "/brand/powered-by-logo.svg",
|
|
209
|
+
text: "Powered by",
|
|
210
|
+
href: "#",
|
|
211
|
+
}}` : "";
|
|
212
|
+
return `'use client'
|
|
213
|
+
|
|
214
|
+
import { ${component} } from '@lucifer91299/ui'
|
|
215
|
+
import { useState } from 'react'
|
|
216
|
+
import { useRouter } from 'next/navigation'
|
|
217
|
+
|
|
218
|
+
export default function Login() {
|
|
219
|
+
const [isLoading, setIsLoading] = useState(false)
|
|
220
|
+
const [error, setError] = useState<string | null>(null)
|
|
221
|
+
const router = useRouter()
|
|
222
|
+
|
|
223
|
+
const handleSubmit = async (creds: { ${credField}: string; password: string }) => {
|
|
224
|
+
setError(null)
|
|
225
|
+
setIsLoading(true)
|
|
226
|
+
try {
|
|
227
|
+
const res = await fetch('/api/auth/login', {
|
|
228
|
+
method: 'POST',
|
|
229
|
+
headers: { 'Content-Type': 'application/json' },
|
|
230
|
+
body: JSON.stringify({ email: creds.${credField}, password: creds.password }),
|
|
231
|
+
credentials: 'include',
|
|
232
|
+
})
|
|
233
|
+
const data = await res.json()
|
|
234
|
+
if (!res.ok) { setError(data.error ?? 'Invalid credentials'); return }
|
|
235
|
+
router.replace('/dashboard')
|
|
236
|
+
} catch {
|
|
237
|
+
setError('Login failed. Please try again.')
|
|
238
|
+
} finally {
|
|
239
|
+
setIsLoading(false)
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return (
|
|
244
|
+
<${component}
|
|
245
|
+
projectName="${o.projectName}"
|
|
246
|
+
projectSubtitle="Sign in to continue"
|
|
247
|
+
logoSrc="/brand/logo.svg"
|
|
248
|
+
onSubmit={handleSubmit}
|
|
249
|
+
isLoading={isLoading}
|
|
250
|
+
error={error}
|
|
251
|
+
${poweredByProp}
|
|
252
|
+
/>
|
|
253
|
+
)
|
|
254
|
+
}
|
|
255
|
+
`;
|
|
256
|
+
}
|
|
257
|
+
function genLoginRoute(o) {
|
|
258
|
+
const cookieName = o.jwtCookieName ?? "access_token";
|
|
259
|
+
return `import { SignJWT } from 'jose'
|
|
260
|
+
import { NextResponse } from 'next/server'
|
|
261
|
+
|
|
262
|
+
// Demo credentials \u2014 replace this block with a real backend call
|
|
263
|
+
const DEMO_EMAIL = 'admin@demo.com'
|
|
264
|
+
const DEMO_PASSWORD = 'password123'
|
|
265
|
+
const DEMO_USER = { id: 1, name: 'Admin User', role: 'Admin', email: DEMO_EMAIL }
|
|
266
|
+
|
|
267
|
+
export async function POST(request: Request) {
|
|
268
|
+
const { email, password } = (await request.json()) as { email?: string; password?: string }
|
|
269
|
+
|
|
270
|
+
if (email !== DEMO_EMAIL || password !== DEMO_PASSWORD) {
|
|
271
|
+
return NextResponse.json({ error: 'Invalid credentials' }, { status: 401 })
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const secret = new TextEncoder().encode(process.env.JWT_SECRET ?? 'change-me-in-production')
|
|
275
|
+
const token = await new SignJWT({ sub: String(DEMO_USER.id), ...DEMO_USER })
|
|
276
|
+
.setProtectedHeader({ alg: 'HS256' })
|
|
277
|
+
.setExpirationTime('7d')
|
|
278
|
+
.sign(secret)
|
|
279
|
+
|
|
280
|
+
const res = NextResponse.json({ ok: true })
|
|
281
|
+
res.cookies.set('${cookieName}', token, {
|
|
282
|
+
httpOnly: true,
|
|
283
|
+
secure: process.env.NODE_ENV === 'production',
|
|
284
|
+
sameSite: 'lax',
|
|
285
|
+
maxAge: 60 * 60 * 24 * 7,
|
|
286
|
+
path: '/',
|
|
287
|
+
})
|
|
288
|
+
return res
|
|
289
|
+
}
|
|
290
|
+
`;
|
|
291
|
+
}
|
|
292
|
+
function genUserRoute(o) {
|
|
293
|
+
const cookieName = o.jwtCookieName ?? "access_token";
|
|
294
|
+
return `import { jwtVerify } from 'jose'
|
|
295
|
+
import { NextResponse } from 'next/server'
|
|
296
|
+
import { cookies } from 'next/headers'
|
|
297
|
+
|
|
298
|
+
export async function GET() {
|
|
299
|
+
const store = await cookies()
|
|
300
|
+
const token = store.get('${cookieName}')?.value
|
|
301
|
+
if (!token) return NextResponse.json({ error: 'Not authenticated' }, { status: 401 })
|
|
302
|
+
|
|
303
|
+
try {
|
|
304
|
+
const secret = new TextEncoder().encode(process.env.JWT_SECRET ?? 'change-me-in-production')
|
|
305
|
+
const { payload } = await jwtVerify(token, secret)
|
|
306
|
+
return NextResponse.json(payload)
|
|
307
|
+
} catch {
|
|
308
|
+
return NextResponse.json({ error: 'Invalid token' }, { status: 401 })
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
`;
|
|
312
|
+
}
|
|
313
|
+
function genMiddleware(o) {
|
|
314
|
+
if (o.authMode === "multi-role") {
|
|
315
|
+
const rolesArr = (o.roles ?? ["admin"]).map((r) => `'${r}'`).join(", ");
|
|
316
|
+
return `import { multiRoleMiddleware } from '@lucifer91299/ui/server'
|
|
317
|
+
|
|
318
|
+
export default multiRoleMiddleware({
|
|
319
|
+
roles: [${rolesArr}],
|
|
320
|
+
cookiePrefix: 'portal_',
|
|
321
|
+
protectedPaths: ['/dashboard'],
|
|
322
|
+
loginPath: '/login',
|
|
323
|
+
})
|
|
324
|
+
|
|
325
|
+
export const config = {
|
|
326
|
+
matcher: ['/((?!_next/static|_next/image|favicon.ico|public).*)'],
|
|
327
|
+
}
|
|
328
|
+
`;
|
|
329
|
+
}
|
|
330
|
+
if (o.authMode === "laravel") {
|
|
331
|
+
return `import { NextResponse } from 'next/server'
|
|
332
|
+
import type { NextRequest } from 'next/server'
|
|
333
|
+
|
|
334
|
+
const PROTECTED = ['/dashboard']
|
|
335
|
+
const LOGIN = '/login'
|
|
336
|
+
|
|
337
|
+
export function middleware(request: NextRequest) {
|
|
338
|
+
const { pathname } = request.nextUrl
|
|
339
|
+
const isProtected = PROTECTED.some((p) => pathname === p || pathname.startsWith(p + '/'))
|
|
340
|
+
if (!isProtected) return NextResponse.next()
|
|
341
|
+
const session = request.cookies.get('laravel_session')?.value
|
|
342
|
+
if (!session) return NextResponse.redirect(new URL(\`\${LOGIN}?redirect=\${pathname}\`, request.url))
|
|
343
|
+
return NextResponse.next()
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
export const config = {
|
|
347
|
+
matcher: ['/((?!_next/static|_next/image|favicon.ico|public).*)'],
|
|
348
|
+
}
|
|
349
|
+
`;
|
|
350
|
+
}
|
|
351
|
+
const cookieName = o.jwtCookieName ?? "access_token";
|
|
352
|
+
return `import { jwtMiddleware } from '@lucifer91299/ui/server'
|
|
353
|
+
|
|
354
|
+
export default jwtMiddleware({
|
|
355
|
+
cookieName: '${cookieName}',
|
|
356
|
+
jwtSecret: process.env.JWT_SECRET!,
|
|
357
|
+
protectedPaths: ['/dashboard'],
|
|
358
|
+
loginPath: '/login',
|
|
359
|
+
})
|
|
360
|
+
|
|
361
|
+
export const config = {
|
|
362
|
+
matcher: ['/((?!_next/static|_next/image|favicon.ico|public).*)'],
|
|
363
|
+
}
|
|
364
|
+
`;
|
|
365
|
+
}
|
|
366
|
+
function genSessionRoute(o) {
|
|
367
|
+
const cookieName = o.jwtCookieName ?? "access_token";
|
|
368
|
+
return `import { sessionRoute } from '@lucifer91299/ui/server'
|
|
369
|
+
export const { POST, DELETE } = sessionRoute({ cookieName: '${cookieName}' })
|
|
370
|
+
`;
|
|
371
|
+
}
|
|
372
|
+
function genLogoutRoute(o) {
|
|
373
|
+
const cookieName = o.jwtCookieName ?? "access_token";
|
|
374
|
+
return `import { logoutRoute } from '@lucifer91299/ui/server'
|
|
375
|
+
export const { POST } = logoutRoute({ cookieName: '${cookieName}' })
|
|
376
|
+
`;
|
|
377
|
+
}
|
|
378
|
+
function genApiClient(o) {
|
|
379
|
+
return `import axios from 'axios'
|
|
380
|
+
|
|
381
|
+
const api = axios.create({
|
|
382
|
+
baseURL: process.env.NEXT_PUBLIC_API_URL ?? '',
|
|
383
|
+
withCredentials: true,
|
|
384
|
+
timeout: 15000,
|
|
385
|
+
})
|
|
386
|
+
|
|
387
|
+
api.interceptors.response.use(
|
|
388
|
+
(r) => r,
|
|
389
|
+
async (error) => {
|
|
390
|
+
if (error.response?.status === 401) {
|
|
391
|
+
await fetch('/api/auth/logout', { method: 'POST' })
|
|
392
|
+
window.location.href = '/login'
|
|
393
|
+
}
|
|
394
|
+
return Promise.reject(error)
|
|
395
|
+
},
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
export default api
|
|
399
|
+
`;
|
|
400
|
+
}
|
|
401
|
+
function genNavConfig(o) {
|
|
402
|
+
return `import { LayoutDashboard, Settings, User } from 'lucide-react'
|
|
403
|
+
import type { NavGroup } from '@lucifer91299/ui'
|
|
404
|
+
|
|
405
|
+
export const navGroups: NavGroup[] = [
|
|
406
|
+
{
|
|
407
|
+
heading: 'Main',
|
|
408
|
+
groupIcon: <LayoutDashboard className="h-3.5 w-3.5" />,
|
|
409
|
+
items: [
|
|
410
|
+
{ label: 'Dashboard', href: '/dashboard', icon: <LayoutDashboard className="h-4 w-4" /> },
|
|
411
|
+
],
|
|
412
|
+
},
|
|
413
|
+
{
|
|
414
|
+
heading: 'Account',
|
|
415
|
+
groupIcon: <User className="h-3.5 w-3.5" />,
|
|
416
|
+
items: [
|
|
417
|
+
{ label: 'Profile', href: '/dashboard/profile', icon: <User className="h-4 w-4" /> },
|
|
418
|
+
{ label: 'Settings', href: '/dashboard/settings', icon: <Settings className="h-4 w-4" /> },
|
|
419
|
+
],
|
|
420
|
+
},
|
|
421
|
+
]
|
|
422
|
+
`;
|
|
423
|
+
}
|
|
424
|
+
function genDashboardLayout(o) {
|
|
425
|
+
return `'use client'
|
|
426
|
+
|
|
427
|
+
import { DashboardLayout } from '@lucifer91299/ui'
|
|
428
|
+
import { use${o.authMode === "multi-role" ? "MultiRoleAuth" : "JwtAuth"} } from '@lucifer91299/ui'
|
|
429
|
+
import { usePathname } from 'next/navigation'
|
|
430
|
+
import { navGroups } from '@/components/layout/nav-config'
|
|
431
|
+
|
|
432
|
+
export default function Layout({ children }: { children: React.ReactNode }) {
|
|
433
|
+
const pathname = usePathname()
|
|
434
|
+
const { user, logout } = use${o.authMode === "multi-role" ? "MultiRoleAuth({ roles: [] })" : "JwtAuth()"}
|
|
435
|
+
|
|
436
|
+
return (
|
|
437
|
+
<DashboardLayout
|
|
438
|
+
navGroups={navGroups}
|
|
439
|
+
sidebar="${o.sidebarStyle}"
|
|
440
|
+
projectName="${o.projectName}"
|
|
441
|
+
logoSrc="/brand/logo.svg"
|
|
442
|
+
user={{ name: (user as any)?.name ?? 'User', role: (user as any)?.role ?? '' }}
|
|
443
|
+
pathname={pathname}
|
|
444
|
+
onLogout={logout}
|
|
445
|
+
>
|
|
446
|
+
{children}
|
|
447
|
+
</DashboardLayout>
|
|
448
|
+
)
|
|
449
|
+
}
|
|
450
|
+
`;
|
|
451
|
+
}
|
|
452
|
+
function genReduxStore() {
|
|
453
|
+
return `import { configureStore } from '@reduxjs/toolkit'
|
|
454
|
+
import authReducer from './auth.slice'
|
|
455
|
+
|
|
456
|
+
export const store = configureStore({
|
|
457
|
+
reducer: {
|
|
458
|
+
auth: authReducer,
|
|
459
|
+
},
|
|
460
|
+
})
|
|
461
|
+
|
|
462
|
+
export type RootState = ReturnType<typeof store.getState>
|
|
463
|
+
export type AppDispatch = typeof store.dispatch
|
|
464
|
+
`;
|
|
465
|
+
}
|
|
466
|
+
function genAuthSlice() {
|
|
467
|
+
return `import { createSlice, type PayloadAction } from '@reduxjs/toolkit'
|
|
468
|
+
|
|
469
|
+
interface AuthState {
|
|
470
|
+
token: string | null
|
|
471
|
+
user: Record<string, unknown> | null
|
|
472
|
+
authenticated: boolean
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
const initialState: AuthState = {
|
|
476
|
+
token: null,
|
|
477
|
+
user: null,
|
|
478
|
+
authenticated: false,
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
const authSlice = createSlice({
|
|
482
|
+
name: 'auth',
|
|
483
|
+
initialState,
|
|
484
|
+
reducers: {
|
|
485
|
+
setCredentials(state, action: PayloadAction<{ token: string; user: Record<string, unknown> }>) {
|
|
486
|
+
state.token = action.payload.token
|
|
487
|
+
state.user = action.payload.user
|
|
488
|
+
state.authenticated = true
|
|
489
|
+
},
|
|
490
|
+
logout(state) {
|
|
491
|
+
state.token = null
|
|
492
|
+
state.user = null
|
|
493
|
+
state.authenticated = false
|
|
494
|
+
},
|
|
495
|
+
},
|
|
496
|
+
})
|
|
497
|
+
|
|
498
|
+
export const { setCredentials, logout } = authSlice.actions
|
|
499
|
+
export default authSlice.reducer
|
|
500
|
+
`;
|
|
501
|
+
}
|
|
502
|
+
function genRootPage() {
|
|
503
|
+
return `import { redirect } from 'next/navigation'
|
|
504
|
+
|
|
505
|
+
export default function Home() {
|
|
506
|
+
redirect('/dashboard')
|
|
507
|
+
}
|
|
508
|
+
`;
|
|
509
|
+
}
|
|
510
|
+
function genDashboardHomePage(o) {
|
|
511
|
+
return `import { TrendingUp, Users, ShoppingCart, Activity } from 'lucide-react'
|
|
512
|
+
|
|
513
|
+
const stats = [
|
|
514
|
+
{ label: 'Total Users', value: '2,847', change: '+12%', icon: Users },
|
|
515
|
+
{ label: 'Revenue', value: '\u20B948,295', change: '+8.2%', icon: TrendingUp },
|
|
516
|
+
{ label: 'Orders', value: '1,429', change: '+5.1%', icon: ShoppingCart },
|
|
517
|
+
{ label: 'Active Now', value: '94', change: '+3', icon: Activity },
|
|
518
|
+
]
|
|
519
|
+
|
|
520
|
+
const activity = [
|
|
521
|
+
{ id: 'ORD-001', user: 'Rahul Sharma', action: 'New order placed', time: '2 min ago', status: 'Pending' },
|
|
522
|
+
{ id: 'ORD-002', user: 'Priya Mehta', action: 'Payment received', time: '14 min ago', status: 'Paid' },
|
|
523
|
+
{ id: 'ORD-003', user: 'Amit Patel', action: 'Order approved', time: '1 hr ago', status: 'Approved' },
|
|
524
|
+
{ id: 'ORD-004', user: 'Sneha Iyer', action: 'Account registered', time: '3 hr ago', status: 'Active' },
|
|
525
|
+
{ id: 'ORD-005', user: 'Vikram Singh', action: 'Order completed', time: 'Yesterday', status: 'Completed' },
|
|
526
|
+
]
|
|
527
|
+
|
|
528
|
+
const statusColors: Record<string, string> = {
|
|
529
|
+
Pending: 'bg-yellow-100 text-yellow-800',
|
|
530
|
+
Paid: 'bg-blue-100 text-blue-800',
|
|
531
|
+
Approved: 'bg-green-100 text-green-800',
|
|
532
|
+
Active: 'bg-purple-100 text-purple-800',
|
|
533
|
+
Completed: 'bg-gray-100 text-gray-700',
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
export default function DashboardHome() {
|
|
537
|
+
return (
|
|
538
|
+
<div className="p-6 space-y-6">
|
|
539
|
+
<div className="page-header">
|
|
540
|
+
<h1 className="page-title">${o.projectName}</h1>
|
|
541
|
+
<p className="text-body text-label-secondary">Welcome back, Admin</p>
|
|
542
|
+
</div>
|
|
543
|
+
|
|
544
|
+
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
|
545
|
+
{stats.map(({ label, value, change, icon: Icon }) => (
|
|
546
|
+
<div key={label} className="card p-5">
|
|
547
|
+
<div className="flex items-center justify-between mb-3">
|
|
548
|
+
<p className="text-subhead text-label-secondary">{label}</p>
|
|
549
|
+
<div className="w-9 h-9 rounded-xl bg-primary/10 flex items-center justify-center">
|
|
550
|
+
<Icon className="w-4 h-4 text-primary" />
|
|
551
|
+
</div>
|
|
552
|
+
</div>
|
|
553
|
+
<p className="text-title1 font-bold text-label-primary">{value}</p>
|
|
554
|
+
<p className="text-footnote text-green-600 mt-1 font-medium">{change} this month</p>
|
|
555
|
+
</div>
|
|
556
|
+
))}
|
|
557
|
+
</div>
|
|
558
|
+
|
|
559
|
+
<div className="card overflow-hidden">
|
|
560
|
+
<div className="px-5 py-4 border-b border-gray-100">
|
|
561
|
+
<h2 className="text-callout font-semibold text-label-primary">Recent Activity</h2>
|
|
562
|
+
</div>
|
|
563
|
+
<table className="w-full text-sm">
|
|
564
|
+
<thead className="bg-gray-50 text-label-secondary text-subhead">
|
|
565
|
+
<tr>
|
|
566
|
+
<th className="px-5 py-3 text-left font-medium">ID</th>
|
|
567
|
+
<th className="px-5 py-3 text-left font-medium">User</th>
|
|
568
|
+
<th className="px-5 py-3 text-left font-medium">Action</th>
|
|
569
|
+
<th className="px-5 py-3 text-left font-medium">Time</th>
|
|
570
|
+
<th className="px-5 py-3 text-left font-medium">Status</th>
|
|
571
|
+
</tr>
|
|
572
|
+
</thead>
|
|
573
|
+
<tbody className="divide-y divide-gray-50">
|
|
574
|
+
{activity.map((row) => (
|
|
575
|
+
<tr key={row.id} className="hover:bg-gray-50 transition-colors">
|
|
576
|
+
<td className="px-5 py-3.5 font-mono text-xs text-label-secondary">{row.id}</td>
|
|
577
|
+
<td className="px-5 py-3.5 font-medium text-label-primary">{row.user}</td>
|
|
578
|
+
<td className="px-5 py-3.5 text-label-secondary">{row.action}</td>
|
|
579
|
+
<td className="px-5 py-3.5 text-label-tertiary">{row.time}</td>
|
|
580
|
+
<td className="px-5 py-3.5">
|
|
581
|
+
<span className={\`inline-flex px-2.5 py-0.5 rounded-full text-xs font-semibold \${statusColors[row.status] ?? ''}\`}>
|
|
582
|
+
{row.status}
|
|
583
|
+
</span>
|
|
584
|
+
</td>
|
|
585
|
+
</tr>
|
|
586
|
+
))}
|
|
587
|
+
</tbody>
|
|
588
|
+
</table>
|
|
589
|
+
</div>
|
|
590
|
+
</div>
|
|
591
|
+
)
|
|
592
|
+
}
|
|
593
|
+
`;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// src/cli/index.ts
|
|
597
|
+
function write(filePath, content) {
|
|
598
|
+
const dir = filePath.substring(0, filePath.lastIndexOf("/"));
|
|
599
|
+
if (dir) (0, import_fs.mkdirSync)(dir, { recursive: true });
|
|
600
|
+
(0, import_fs.writeFileSync)(filePath, content, "utf-8");
|
|
601
|
+
}
|
|
602
|
+
function log(msg) {
|
|
603
|
+
process.stdout.write(msg + "\n");
|
|
604
|
+
}
|
|
605
|
+
function parseArgs(argv) {
|
|
606
|
+
const flags = {};
|
|
607
|
+
const positional = [];
|
|
608
|
+
for (const arg of argv) {
|
|
609
|
+
if (arg.startsWith("--")) {
|
|
610
|
+
const [key, ...rest] = arg.slice(2).split("=");
|
|
611
|
+
flags[key] = rest.length ? rest.join("=") : true;
|
|
612
|
+
} else {
|
|
613
|
+
positional.push(arg);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
return { flags, positional };
|
|
617
|
+
}
|
|
618
|
+
var DEFAULTS = {
|
|
619
|
+
projectDescription: "My portal application",
|
|
620
|
+
authMode: "jwt",
|
|
621
|
+
loginStyle: "animated",
|
|
622
|
+
sidebarStyle: "full",
|
|
623
|
+
primaryColor: "#000080",
|
|
624
|
+
accentColor: "#FF9933",
|
|
625
|
+
successColor: "#138808",
|
|
626
|
+
brandLogoPath: "brand/logo.svg",
|
|
627
|
+
apiUrl: "http://localhost:3000",
|
|
628
|
+
jwtCookieName: "access_token",
|
|
629
|
+
jwtSecret: "change-me-in-production",
|
|
630
|
+
includeI18n: false,
|
|
631
|
+
stateManagement: "redux-query",
|
|
632
|
+
packageManager: "npm"
|
|
633
|
+
};
|
|
634
|
+
async function runPrompts(projectNameArg) {
|
|
635
|
+
process.stderr.write("\n create-portal-app \u2014 Next.js authenticated portal scaffolder\n");
|
|
636
|
+
process.stderr.write(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n");
|
|
637
|
+
const projectName = projectNameArg?.trim() || await ask("Project name", "my-portal");
|
|
638
|
+
const projectDescription = await ask("Project description", DEFAULTS.projectDescription);
|
|
639
|
+
const authMode = await select("Auth mode?", [
|
|
640
|
+
{ value: "jwt", label: "JWT cookie", hint: "NestJS / Express / any backend" },
|
|
641
|
+
{ value: "multi-role", label: "Multi-role JWT", hint: "Separate cookies per role (coach + judge etc.)" },
|
|
642
|
+
{ value: "laravel", label: "Laravel session", hint: "PHP Laravel backend" }
|
|
643
|
+
]);
|
|
644
|
+
let apiUrl = DEFAULTS.apiUrl;
|
|
645
|
+
let jwtCookieName = DEFAULTS.jwtCookieName;
|
|
646
|
+
let jwtSecret = DEFAULTS.jwtSecret;
|
|
647
|
+
let roles;
|
|
648
|
+
let laravelUrl;
|
|
649
|
+
let dbHost;
|
|
650
|
+
let dbPort;
|
|
651
|
+
let dbName;
|
|
652
|
+
let dbUser;
|
|
653
|
+
let dbPassword;
|
|
654
|
+
if (authMode === "jwt") {
|
|
655
|
+
apiUrl = await ask("Backend API URL", DEFAULTS.apiUrl);
|
|
656
|
+
jwtCookieName = await ask("Cookie name", DEFAULTS.jwtCookieName);
|
|
657
|
+
jwtSecret = await ask("JWT secret", DEFAULTS.jwtSecret);
|
|
658
|
+
}
|
|
659
|
+
if (authMode === "multi-role") {
|
|
660
|
+
const raw = await ask("Role names (comma separated)", "coach, judge");
|
|
661
|
+
roles = raw.split(",").map((r) => r.trim()).filter(Boolean);
|
|
662
|
+
if (!roles.length) roles = ["admin"];
|
|
663
|
+
apiUrl = await ask("Backend API URL", DEFAULTS.apiUrl);
|
|
664
|
+
}
|
|
665
|
+
if (authMode === "laravel") {
|
|
666
|
+
laravelUrl = await ask("Laravel URL", "http://localhost:8000");
|
|
667
|
+
dbHost = await ask("Database host", "127.0.0.1");
|
|
668
|
+
dbPort = await ask("Database port", "3306");
|
|
669
|
+
dbName = await ask("Database name", "laravel");
|
|
670
|
+
dbUser = await ask("Database user", "root");
|
|
671
|
+
dbPassword = await ask("Database password", "");
|
|
672
|
+
}
|
|
673
|
+
const loginStyle = await select("Login page style?", [
|
|
674
|
+
{ value: "animated", label: "Animated", hint: "floating orbs + particle background + tricolor bar" },
|
|
675
|
+
{ value: "simple", label: "Simple", hint: "clean gradient card (minimal)" }
|
|
676
|
+
]);
|
|
677
|
+
const sidebarStyle = await select("Sidebar style?", [
|
|
678
|
+
{ value: "full", label: "Full", hint: "wide sidebar with labels and collapsible groups" },
|
|
679
|
+
{ value: "rail", label: "Rail", hint: "icon-only narrow sidebar" },
|
|
680
|
+
{ value: "both", label: "Both", hint: "full on desktop, rail on mobile" }
|
|
681
|
+
]);
|
|
682
|
+
process.stderr.write("\nBrand colours (press Enter to keep default):\n");
|
|
683
|
+
const primaryColor = await ask("Primary colour ", DEFAULTS.primaryColor);
|
|
684
|
+
const accentColor = await ask("Accent colour ", DEFAULTS.accentColor);
|
|
685
|
+
const successColor = await ask("Success colour ", DEFAULTS.successColor);
|
|
686
|
+
const brandLogoPath = await ask("Logo path in public/", DEFAULTS.brandLogoPath);
|
|
687
|
+
const includeI18nRaw = await select("Include i18n (translations)?", [
|
|
688
|
+
{ value: "no", label: "No" },
|
|
689
|
+
{ value: "yes", label: "Yes" }
|
|
690
|
+
]);
|
|
691
|
+
const stateManagement = await select("State management?", [
|
|
692
|
+
{ value: "redux-query", label: "Redux Toolkit + React Query" },
|
|
693
|
+
{ value: "query-only", label: "React Query only" }
|
|
694
|
+
]);
|
|
695
|
+
const packageManager = await select("Package manager?", [
|
|
696
|
+
{ value: "npm", label: "npm" },
|
|
697
|
+
{ value: "pnpm", label: "pnpm" },
|
|
698
|
+
{ value: "yarn", label: "yarn" }
|
|
699
|
+
]);
|
|
700
|
+
closePrompt();
|
|
701
|
+
return {
|
|
702
|
+
projectName,
|
|
703
|
+
projectDescription,
|
|
704
|
+
authMode,
|
|
705
|
+
loginStyle,
|
|
706
|
+
sidebarStyle,
|
|
707
|
+
primaryColor,
|
|
708
|
+
accentColor,
|
|
709
|
+
successColor,
|
|
710
|
+
brandLogoPath,
|
|
711
|
+
apiUrl,
|
|
712
|
+
jwtCookieName,
|
|
713
|
+
jwtSecret,
|
|
714
|
+
roles,
|
|
715
|
+
laravelUrl,
|
|
716
|
+
dbHost,
|
|
717
|
+
dbPort,
|
|
718
|
+
dbName,
|
|
719
|
+
dbUser,
|
|
720
|
+
dbPassword,
|
|
721
|
+
includeI18n: includeI18nRaw === "yes",
|
|
722
|
+
stateManagement,
|
|
723
|
+
packageManager
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
function scaffold(opts) {
|
|
727
|
+
const outDir = (0, import_path.join)(process.cwd(), opts.projectName);
|
|
728
|
+
if ((0, import_fs.existsSync)(outDir)) {
|
|
729
|
+
log(`
|
|
730
|
+
Error: directory "${opts.projectName}" already exists.
|
|
731
|
+
`);
|
|
732
|
+
process.exit(1);
|
|
733
|
+
}
|
|
734
|
+
log(`
|
|
735
|
+
Creating ${opts.projectName}/ ...
|
|
736
|
+
`);
|
|
737
|
+
const f = (rel) => (0, import_path.join)(outDir, rel);
|
|
738
|
+
write(f("package.json"), genPackageJson(opts));
|
|
739
|
+
write(f("tailwind.config.ts"), genTailwindConfig(opts));
|
|
740
|
+
write(f("src/theme.config.ts"), genThemeConfig(opts));
|
|
741
|
+
write(f(".env.local"), genEnvLocal(opts));
|
|
742
|
+
write(f(".env.example"), genEnvLocal(opts).replace(/=.+/g, "="));
|
|
743
|
+
write(f("postcss.config.mjs"), `export default { plugins: { tailwindcss: {}, autoprefixer: {} } }
|
|
744
|
+
`);
|
|
745
|
+
write(f("next.config.ts"), `import type { NextConfig } from 'next'
|
|
746
|
+
const config: NextConfig = {
|
|
747
|
+
transpilePackages: ['@lucifer91299/ui'],
|
|
748
|
+
}
|
|
749
|
+
export default config
|
|
750
|
+
`);
|
|
751
|
+
write(f("tsconfig.json"), JSON.stringify({
|
|
752
|
+
compilerOptions: {
|
|
753
|
+
target: "ES2017",
|
|
754
|
+
lib: ["dom", "dom.iterable", "esnext"],
|
|
755
|
+
allowJs: true,
|
|
756
|
+
skipLibCheck: true,
|
|
757
|
+
strict: true,
|
|
758
|
+
noEmit: true,
|
|
759
|
+
esModuleInterop: true,
|
|
760
|
+
module: "esnext",
|
|
761
|
+
moduleResolution: "bundler",
|
|
762
|
+
resolveJsonModule: true,
|
|
763
|
+
isolatedModules: true,
|
|
764
|
+
jsx: "preserve",
|
|
765
|
+
incremental: true,
|
|
766
|
+
plugins: [{ name: "next" }],
|
|
767
|
+
paths: { "@/*": ["./src/*"] }
|
|
768
|
+
},
|
|
769
|
+
include: ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
|
770
|
+
exclude: ["node_modules"]
|
|
771
|
+
}, null, 2));
|
|
772
|
+
write(f("src/app/globals.css"), genGlobalsCSS());
|
|
773
|
+
write(f("src/app/layout.tsx"), genRootLayout(opts));
|
|
774
|
+
write(f("src/app/page.tsx"), genRootPage());
|
|
775
|
+
write(f("src/app/login/page.tsx"), genLoginPage(opts));
|
|
776
|
+
write(f("src/app/dashboard/layout.tsx"), genDashboardLayout(opts));
|
|
777
|
+
write(f("src/app/dashboard/page.tsx"), genDashboardHomePage(opts));
|
|
778
|
+
write(f("src/app/api/auth/login/route.ts"), genLoginRoute(opts));
|
|
779
|
+
write(f("src/app/api/auth/user/route.ts"), genUserRoute(opts));
|
|
780
|
+
write(f("src/app/api/auth/session/route.ts"), genSessionRoute(opts));
|
|
781
|
+
write(f("src/app/api/auth/logout/route.ts"), genLogoutRoute(opts));
|
|
782
|
+
write(f("src/providers/index.tsx"), genProviders(opts));
|
|
783
|
+
write(f("src/lib/api.ts"), genApiClient(opts));
|
|
784
|
+
write(f("src/components/layout/nav-config.tsx"), genNavConfig(opts));
|
|
785
|
+
write(f("src/middleware.ts"), genMiddleware(opts));
|
|
786
|
+
if (opts.stateManagement === "redux-query") {
|
|
787
|
+
write(f("src/store/index.ts"), genReduxStore());
|
|
788
|
+
write(f("src/store/auth.slice.ts"), genAuthSlice());
|
|
789
|
+
}
|
|
790
|
+
const initial = opts.projectName.charAt(0).toUpperCase();
|
|
791
|
+
write(
|
|
792
|
+
f("public/brand/logo.svg"),
|
|
793
|
+
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><circle cx="32" cy="32" r="32" fill="${opts.primaryColor}"/><text x="32" y="40" font-size="24" text-anchor="middle" fill="#fff" font-family="system-ui">${initial}</text></svg>`
|
|
794
|
+
);
|
|
795
|
+
write(
|
|
796
|
+
f("public/brand/powered-by-logo.svg"),
|
|
797
|
+
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 80 24"><text y="18" font-size="12" font-family="system-ui" fill="#888">powered</text></svg>`
|
|
798
|
+
);
|
|
799
|
+
const cmds = {
|
|
800
|
+
npm: { install: "npm install", dev: "npm run dev" },
|
|
801
|
+
pnpm: { install: "pnpm install", dev: "pnpm dev" },
|
|
802
|
+
yarn: { install: "yarn", dev: "yarn dev" }
|
|
803
|
+
}[opts.packageManager];
|
|
804
|
+
const count = countFiles(outDir);
|
|
805
|
+
log(`\u2714 ${opts.projectName}/ \u2014 ${count} files created`);
|
|
806
|
+
log(``);
|
|
807
|
+
log(` Theme: ${opts.primaryColor} ${opts.accentColor} ${opts.successColor}`);
|
|
808
|
+
log(` Auth: ${opts.authMode}`);
|
|
809
|
+
log(` Login: ${opts.loginStyle}`);
|
|
810
|
+
log(` Sidebar: ${opts.sidebarStyle}`);
|
|
811
|
+
log(``);
|
|
812
|
+
log(`Next steps:`);
|
|
813
|
+
log(` cd ${opts.projectName}`);
|
|
814
|
+
log(` ${cmds.install}`);
|
|
815
|
+
log(` ${cmds.dev}`);
|
|
816
|
+
log(``);
|
|
817
|
+
}
|
|
818
|
+
function countFiles(dir) {
|
|
819
|
+
const { readdirSync, statSync } = require("fs");
|
|
820
|
+
let n = 0;
|
|
821
|
+
for (const e of readdirSync(dir)) {
|
|
822
|
+
const full = (0, import_path.join)(dir, e);
|
|
823
|
+
n += statSync(full).isDirectory() ? countFiles(full) : 1;
|
|
824
|
+
}
|
|
825
|
+
return n;
|
|
826
|
+
}
|
|
827
|
+
async function main() {
|
|
828
|
+
const { flags, positional } = parseArgs(process.argv.slice(2));
|
|
829
|
+
if (flags["help"] || flags["h"]) {
|
|
830
|
+
log("Usage: create-portal-app [project-name] [options]");
|
|
831
|
+
log("");
|
|
832
|
+
log(" Without options \u2192 interactive prompts for all settings");
|
|
833
|
+
log("");
|
|
834
|
+
log(" --yes, -y Skip all prompts, use defaults immediately");
|
|
835
|
+
log(" --auth=jwt|multi-role|laravel");
|
|
836
|
+
log(" --login=animated|simple");
|
|
837
|
+
log(" --sidebar=full|rail|both");
|
|
838
|
+
log(" --primary=#hex Primary brand colour");
|
|
839
|
+
log(" --accent=#hex Accent colour");
|
|
840
|
+
log(" --success=#hex Success colour");
|
|
841
|
+
log(" --api-url=URL Backend API URL");
|
|
842
|
+
log(" --pm=npm|pnpm|yarn");
|
|
843
|
+
log(" --local-ui=PATH Use local @lucifer91299/ui (file: reference, for development)");
|
|
844
|
+
log("");
|
|
845
|
+
log("Examples:");
|
|
846
|
+
log(" create-portal-app");
|
|
847
|
+
log(" create-portal-app my-portal");
|
|
848
|
+
log(" create-portal-app my-portal --yes");
|
|
849
|
+
log(" create-portal-app my-portal --yes --auth=laravel --primary=#E11D48");
|
|
850
|
+
log(" create-portal-app my-portal --yes --local-ui=../../ui");
|
|
851
|
+
process.exit(0);
|
|
852
|
+
}
|
|
853
|
+
const projectNameArg = positional[0];
|
|
854
|
+
if (flags["yes"] || flags["y"]) {
|
|
855
|
+
const name = projectNameArg || "my-portal";
|
|
856
|
+
const opts2 = {
|
|
857
|
+
projectName: name,
|
|
858
|
+
projectDescription: DEFAULTS.projectDescription,
|
|
859
|
+
authMode: flags["auth"] ?? DEFAULTS.authMode,
|
|
860
|
+
loginStyle: flags["login"] ?? DEFAULTS.loginStyle,
|
|
861
|
+
sidebarStyle: flags["sidebar"] ?? DEFAULTS.sidebarStyle,
|
|
862
|
+
primaryColor: flags["primary"] ?? DEFAULTS.primaryColor,
|
|
863
|
+
accentColor: flags["accent"] ?? DEFAULTS.accentColor,
|
|
864
|
+
successColor: flags["success"] ?? DEFAULTS.successColor,
|
|
865
|
+
brandLogoPath: DEFAULTS.brandLogoPath,
|
|
866
|
+
apiUrl: flags["api-url"] ?? DEFAULTS.apiUrl,
|
|
867
|
+
jwtCookieName: DEFAULTS.jwtCookieName,
|
|
868
|
+
jwtSecret: DEFAULTS.jwtSecret,
|
|
869
|
+
includeI18n: false,
|
|
870
|
+
stateManagement: flags["pm"] ?? DEFAULTS.stateManagement,
|
|
871
|
+
packageManager: flags["pm"] ?? DEFAULTS.packageManager,
|
|
872
|
+
localUiPath: flags["local-ui"] ?? void 0
|
|
873
|
+
};
|
|
874
|
+
log("create-portal-app \u2014 using defaults (--yes)");
|
|
875
|
+
scaffold(opts2);
|
|
876
|
+
return;
|
|
877
|
+
}
|
|
878
|
+
const opts = await runPrompts(projectNameArg);
|
|
879
|
+
opts.localUiPath = flags["local-ui"] ?? void 0;
|
|
880
|
+
scaffold(opts);
|
|
881
|
+
}
|
|
882
|
+
main().catch((err) => {
|
|
883
|
+
closePrompt();
|
|
884
|
+
console.error(err instanceof Error ? err.message : err);
|
|
885
|
+
process.exit(1);
|
|
886
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@lucifer91299/create-portal-app",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Scaffold a Next.js authenticated portal with full design system in one command",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Aakash Kanojiya",
|
|
7
|
+
"bin": {
|
|
8
|
+
"create-portal-app": "dist/cli/index.js"
|
|
9
|
+
},
|
|
10
|
+
"main": "dist/cli/index.js",
|
|
11
|
+
"files": ["dist"],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "tsup",
|
|
14
|
+
"dev": "tsup --watch",
|
|
15
|
+
"typecheck": "tsc --noEmit"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@types/node": "^22.10.7",
|
|
19
|
+
"tsup": "^8.3.5",
|
|
20
|
+
"typescript": "^5.7.3"
|
|
21
|
+
}
|
|
22
|
+
}
|