create-batman 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/README.md +27 -0
- package/bin/index.js +485 -0
- package/package.json +30 -0
- package/templates/mysql/client/index.html +12 -0
- package/templates/mysql/client/package.json +21 -0
- package/templates/mysql/client/postcss.config.js +6 -0
- package/templates/mysql/client/src/App.jsx +5 -0
- package/templates/mysql/client/src/api/client.js +6 -0
- package/templates/mysql/client/src/components/Navbar.jsx +75 -0
- package/templates/mysql/client/src/components/ProtectedRoute.jsx +10 -0
- package/templates/mysql/client/src/index.css +24 -0
- package/templates/mysql/client/src/layouts/DashboardLayout.jsx +12 -0
- package/templates/mysql/client/src/main.jsx +10 -0
- package/templates/mysql/client/src/pages/Dashboard.jsx +44 -0
- package/templates/mysql/client/src/pages/Login.jsx +121 -0
- package/templates/mysql/client/src/pages/Page1.jsx +101 -0
- package/templates/mysql/client/src/pages/Page2.jsx +82 -0
- package/templates/mysql/client/src/pages/Page3.jsx +49 -0
- package/templates/mysql/client/src/router/index.jsx +43 -0
- package/templates/mysql/client/src/utils/auth.js +16 -0
- package/templates/mysql/client/tailwind.config.js +15 -0
- package/templates/mysql/client/vite.config.js +6 -0
- package/templates/mysql/server/config/db.js +13 -0
- package/templates/mysql/server/controllers/authController.js +66 -0
- package/templates/mysql/server/index.js +39 -0
- package/templates/mysql/server/middleware/authMiddleware.js +7 -0
- package/templates/mysql/server/package.json +18 -0
- package/templates/mysql/server/routes/authRoutes.js +14 -0
- package/templates/sequelize/client/index.html +12 -0
- package/templates/sequelize/client/package.json +21 -0
- package/templates/sequelize/client/postcss.config.js +6 -0
- package/templates/sequelize/client/src/App.jsx +5 -0
- package/templates/sequelize/client/src/api/client.js +6 -0
- package/templates/sequelize/client/src/components/Navbar.jsx +75 -0
- package/templates/sequelize/client/src/components/ProtectedRoute.jsx +10 -0
- package/templates/sequelize/client/src/index.css +24 -0
- package/templates/sequelize/client/src/layouts/DashboardLayout.jsx +12 -0
- package/templates/sequelize/client/src/main.jsx +10 -0
- package/templates/sequelize/client/src/pages/Dashboard.jsx +44 -0
- package/templates/sequelize/client/src/pages/Login.jsx +121 -0
- package/templates/sequelize/client/src/pages/Page1.jsx +101 -0
- package/templates/sequelize/client/src/pages/Page2.jsx +82 -0
- package/templates/sequelize/client/src/pages/Page3.jsx +49 -0
- package/templates/sequelize/client/src/router/index.jsx +43 -0
- package/templates/sequelize/client/src/utils/auth.js +16 -0
- package/templates/sequelize/client/tailwind.config.js +15 -0
- package/templates/sequelize/client/vite.config.js +6 -0
- package/templates/sequelize/server/config/db.js +13 -0
- package/templates/sequelize/server/config/sequelize.js +17 -0
- package/templates/sequelize/server/controllers/authController.js +62 -0
- package/templates/sequelize/server/index.js +42 -0
- package/templates/sequelize/server/middleware/authMiddleware.js +7 -0
- package/templates/sequelize/server/models/User.js +24 -0
- package/templates/sequelize/server/package.json +19 -0
- package/templates/sequelize/server/routes/authRoutes.js +14 -0
package/README.md
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
|
|
2
|
+
# Create Parachute
|
|
3
|
+
|
|
4
|
+
A clean fullstack starter generator for students.
|
|
5
|
+
|
|
6
|
+
## Usage
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
npx create-parachute my-project
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
The generator creates:
|
|
13
|
+
|
|
14
|
+
- Express backend
|
|
15
|
+
- React + Vite frontend
|
|
16
|
+
- Raw MySQL or Sequelize option
|
|
17
|
+
- Session authentication
|
|
18
|
+
- Username login
|
|
19
|
+
- Hashed passwords
|
|
20
|
+
- React Router navigation
|
|
21
|
+
- Protected routes
|
|
22
|
+
- Dashboard + 3 pages
|
|
23
|
+
- `schema.sql`
|
|
24
|
+
- `instructions.md`
|
|
25
|
+
- `tailwind.md`
|
|
26
|
+
- `sql.md`
|
|
27
|
+
- Optional dependency installation
|
package/bin/index.js
ADDED
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from "fs-extra";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import inquirer from "inquirer";
|
|
6
|
+
import chalk from "chalk";
|
|
7
|
+
import { spawnSync } from "child_process";
|
|
8
|
+
import { fileURLToPath } from "url";
|
|
9
|
+
|
|
10
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
11
|
+
const __dirname = path.dirname(__filename);
|
|
12
|
+
|
|
13
|
+
let projectName = process.argv[2];
|
|
14
|
+
|
|
15
|
+
if (!projectName) {
|
|
16
|
+
const projectAnswer = await inquirer.prompt([
|
|
17
|
+
{
|
|
18
|
+
type: "input",
|
|
19
|
+
name: "projectName",
|
|
20
|
+
message: "Project name:",
|
|
21
|
+
validate: (input) => {
|
|
22
|
+
if (!input.trim()) {
|
|
23
|
+
return "Project name is required.";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (!/^[a-zA-Z0-9-_]+$/.test(input)) {
|
|
27
|
+
return "Only letters, numbers, hyphens, and underscores are allowed.";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return true;
|
|
31
|
+
},
|
|
32
|
+
filter: (input) => input.trim()
|
|
33
|
+
}
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
projectName = projectAnswer.projectName;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const answers = await inquirer.prompt([
|
|
40
|
+
{
|
|
41
|
+
type: "input",
|
|
42
|
+
name: "dbName",
|
|
43
|
+
message: "Database name:",
|
|
44
|
+
default: `${projectName.replace(/-/g, "_")}_db`
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
type: "input",
|
|
48
|
+
name: "serverPort",
|
|
49
|
+
message: "Backend port:",
|
|
50
|
+
default: "5000"
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
type: "input",
|
|
54
|
+
name: "clientPort",
|
|
55
|
+
message: "Frontend port:",
|
|
56
|
+
default: "5173"
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
type: "list",
|
|
60
|
+
name: "database",
|
|
61
|
+
message: "Choose database style:",
|
|
62
|
+
choices: [
|
|
63
|
+
{ name: "Raw MySQL", value: "mysql" },
|
|
64
|
+
{ name: "Sequelize", value: "sequelize" }
|
|
65
|
+
]
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
type: "list",
|
|
69
|
+
name: "theme",
|
|
70
|
+
message: "Choose theme:",
|
|
71
|
+
choices: [
|
|
72
|
+
{ name: chalk.blue("Blue"), value: "#2563EB" },
|
|
73
|
+
{ name: chalk.green("Green"), value: "#16A34A" },
|
|
74
|
+
{ name: chalk.red("Red"), value: "#DC2626" },
|
|
75
|
+
{ name: chalk.magenta("Purple"), value: "#9333EA" },
|
|
76
|
+
{ name: chalk.yellow("Yellow"), value: "#CA8A04" },
|
|
77
|
+
{ name: chalk.cyan("Cyan"), value: "#0891B2" },
|
|
78
|
+
{ name: chalk.hex("#800000")("Maroon"), value: "#800000" }
|
|
79
|
+
]
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
type: "confirm",
|
|
83
|
+
name: "installDeps",
|
|
84
|
+
message: "Install backend and frontend dependencies now?",
|
|
85
|
+
default: true
|
|
86
|
+
}
|
|
87
|
+
]);
|
|
88
|
+
|
|
89
|
+
const template = path.join(__dirname, "..", "templates", answers.database);
|
|
90
|
+
const target = path.join(process.cwd(), projectName);
|
|
91
|
+
|
|
92
|
+
if (fs.existsSync(target)) {
|
|
93
|
+
console.log(chalk.red("Folder already exists. Choose another project name."));
|
|
94
|
+
process.exit(1);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
await fs.copy(template, target);
|
|
98
|
+
|
|
99
|
+
const env = `PORT=${answers.serverPort}
|
|
100
|
+
DB_HOST=localhost
|
|
101
|
+
DB_USER=root
|
|
102
|
+
DB_PASSWORD=
|
|
103
|
+
DB_NAME=${answers.dbName}
|
|
104
|
+
CLIENT_URL=http://localhost:${answers.clientPort}
|
|
105
|
+
SESSION_SECRET=batman_secret`;
|
|
106
|
+
|
|
107
|
+
await fs.writeFile(path.join(target, "server/.env"), env);
|
|
108
|
+
|
|
109
|
+
const schema = `CREATE DATABASE IF NOT EXISTS ${answers.dbName};
|
|
110
|
+
USE ${answers.dbName};
|
|
111
|
+
|
|
112
|
+
CREATE TABLE Users(
|
|
113
|
+
UserID INT AUTO_INCREMENT PRIMARY KEY,
|
|
114
|
+
Username VARCHAR(255) UNIQUE NOT NULL,
|
|
115
|
+
Password VARCHAR(255) NOT NULL
|
|
116
|
+
);`;
|
|
117
|
+
|
|
118
|
+
await fs.writeFile(path.join(target, "schema.sql"), schema);
|
|
119
|
+
|
|
120
|
+
const instructions = `# ${projectName}
|
|
121
|
+
|
|
122
|
+
## 1. Run the database file
|
|
123
|
+
|
|
124
|
+
Open MySQL and run schema.sql.
|
|
125
|
+
|
|
126
|
+
Example:
|
|
127
|
+
|
|
128
|
+
SOURCE /full/path/to/${projectName}/schema.sql;
|
|
129
|
+
|
|
130
|
+
## 2. Start backend
|
|
131
|
+
|
|
132
|
+
cd server
|
|
133
|
+
npm run dev
|
|
134
|
+
|
|
135
|
+
## 3. Start frontend
|
|
136
|
+
|
|
137
|
+
cd client
|
|
138
|
+
npm run dev
|
|
139
|
+
|
|
140
|
+
## Notes
|
|
141
|
+
|
|
142
|
+
- Backend runs on port ${answers.serverPort}
|
|
143
|
+
- Frontend runs on port ${answers.clientPort}
|
|
144
|
+
- Auth uses Username and hashed Password
|
|
145
|
+
- Add your own backend features inside controllers and routes
|
|
146
|
+
- Add your own frontend screens inside client/src/pages
|
|
147
|
+
`;
|
|
148
|
+
|
|
149
|
+
await fs.writeFile(path.join(target, "instructions.md"), instructions);
|
|
150
|
+
|
|
151
|
+
const tailwindCheat = `# Tailwind Cheat Sheet
|
|
152
|
+
|
|
153
|
+
## Layout
|
|
154
|
+
|
|
155
|
+
flex
|
|
156
|
+
grid
|
|
157
|
+
block
|
|
158
|
+
hidden
|
|
159
|
+
relative
|
|
160
|
+
absolute
|
|
161
|
+
fixed
|
|
162
|
+
sticky
|
|
163
|
+
|
|
164
|
+
## Flexbox
|
|
165
|
+
|
|
166
|
+
flex
|
|
167
|
+
flex-col
|
|
168
|
+
flex-row
|
|
169
|
+
items-center
|
|
170
|
+
items-start
|
|
171
|
+
items-end
|
|
172
|
+
justify-center
|
|
173
|
+
justify-between
|
|
174
|
+
justify-end
|
|
175
|
+
gap-2
|
|
176
|
+
gap-4
|
|
177
|
+
gap-6
|
|
178
|
+
|
|
179
|
+
## Grid
|
|
180
|
+
|
|
181
|
+
grid
|
|
182
|
+
grid-cols-1
|
|
183
|
+
grid-cols-2
|
|
184
|
+
grid-cols-3
|
|
185
|
+
md:grid-cols-2
|
|
186
|
+
lg:grid-cols-3
|
|
187
|
+
|
|
188
|
+
## Spacing
|
|
189
|
+
|
|
190
|
+
p-2
|
|
191
|
+
p-4
|
|
192
|
+
p-6
|
|
193
|
+
p-8
|
|
194
|
+
px-4
|
|
195
|
+
px-6
|
|
196
|
+
py-2
|
|
197
|
+
py-3
|
|
198
|
+
py-4
|
|
199
|
+
m-2
|
|
200
|
+
m-4
|
|
201
|
+
mt-4
|
|
202
|
+
mb-4
|
|
203
|
+
mx-auto
|
|
204
|
+
|
|
205
|
+
## Width and Height
|
|
206
|
+
|
|
207
|
+
w-full
|
|
208
|
+
w-screen
|
|
209
|
+
max-w-md
|
|
210
|
+
max-w-xl
|
|
211
|
+
max-w-6xl
|
|
212
|
+
min-h-screen
|
|
213
|
+
h-screen
|
|
214
|
+
|
|
215
|
+
## Typography
|
|
216
|
+
|
|
217
|
+
text-sm
|
|
218
|
+
text-base
|
|
219
|
+
text-lg
|
|
220
|
+
text-xl
|
|
221
|
+
text-2xl
|
|
222
|
+
font-medium
|
|
223
|
+
font-semibold
|
|
224
|
+
font-bold
|
|
225
|
+
text-center
|
|
226
|
+
|
|
227
|
+
## Colors
|
|
228
|
+
|
|
229
|
+
bg-white
|
|
230
|
+
bg-black
|
|
231
|
+
bg-gray-50
|
|
232
|
+
bg-gray-100
|
|
233
|
+
text-black
|
|
234
|
+
text-white
|
|
235
|
+
text-gray-500
|
|
236
|
+
text-gray-700
|
|
237
|
+
|
|
238
|
+
## Borders and Radius
|
|
239
|
+
|
|
240
|
+
border
|
|
241
|
+
border-gray-200
|
|
242
|
+
rounded
|
|
243
|
+
rounded-lg
|
|
244
|
+
rounded-xl
|
|
245
|
+
rounded-2xl
|
|
246
|
+
|
|
247
|
+
## Shadows
|
|
248
|
+
|
|
249
|
+
shadow
|
|
250
|
+
shadow-md
|
|
251
|
+
shadow-lg
|
|
252
|
+
|
|
253
|
+
## Buttons
|
|
254
|
+
|
|
255
|
+
bg-black text-white px-4 py-2 rounded-lg
|
|
256
|
+
bg-blue-600 text-white px-4 py-2 rounded-lg
|
|
257
|
+
|
|
258
|
+
## Forms
|
|
259
|
+
|
|
260
|
+
border rounded-lg px-4 py-2 w-full
|
|
261
|
+
focus:outline-none focus:ring-2 focus:ring-blue-500
|
|
262
|
+
|
|
263
|
+
## Responsive Examples
|
|
264
|
+
|
|
265
|
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
266
|
+
</div>
|
|
267
|
+
|
|
268
|
+
<div className="p-4 md:p-8">
|
|
269
|
+
</div>
|
|
270
|
+
`;
|
|
271
|
+
|
|
272
|
+
await fs.writeFile(path.join(target, "tailwind.md"), tailwindCheat);
|
|
273
|
+
|
|
274
|
+
const sqlCheat = `# SQL Cheat Sheet
|
|
275
|
+
|
|
276
|
+
## Create Database
|
|
277
|
+
|
|
278
|
+
CREATE DATABASE school_db;
|
|
279
|
+
USE school_db;
|
|
280
|
+
|
|
281
|
+
## Create Table
|
|
282
|
+
|
|
283
|
+
CREATE TABLE Students(
|
|
284
|
+
StudentID INT AUTO_INCREMENT PRIMARY KEY,
|
|
285
|
+
StudentName VARCHAR(100) NOT NULL,
|
|
286
|
+
Age INT
|
|
287
|
+
);
|
|
288
|
+
|
|
289
|
+
## Insert
|
|
290
|
+
|
|
291
|
+
INSERT INTO Students (StudentName, Age)
|
|
292
|
+
VALUES ('Aime', 20);
|
|
293
|
+
|
|
294
|
+
## Select All
|
|
295
|
+
|
|
296
|
+
SELECT * FROM Students;
|
|
297
|
+
|
|
298
|
+
## Select Specific Columns
|
|
299
|
+
|
|
300
|
+
SELECT StudentName, Age FROM Students;
|
|
301
|
+
|
|
302
|
+
## Filter with WHERE
|
|
303
|
+
|
|
304
|
+
SELECT * FROM Students
|
|
305
|
+
WHERE Age > 18;
|
|
306
|
+
|
|
307
|
+
## Search with LIKE
|
|
308
|
+
|
|
309
|
+
SELECT * FROM Students
|
|
310
|
+
WHERE StudentName LIKE '%ai%';
|
|
311
|
+
|
|
312
|
+
## Sort
|
|
313
|
+
|
|
314
|
+
SELECT * FROM Students
|
|
315
|
+
ORDER BY StudentName ASC;
|
|
316
|
+
|
|
317
|
+
SELECT * FROM Students
|
|
318
|
+
ORDER BY Age DESC;
|
|
319
|
+
|
|
320
|
+
## Limit
|
|
321
|
+
|
|
322
|
+
SELECT * FROM Students
|
|
323
|
+
LIMIT 5;
|
|
324
|
+
|
|
325
|
+
## Update
|
|
326
|
+
|
|
327
|
+
UPDATE Students
|
|
328
|
+
SET Age = 21
|
|
329
|
+
WHERE StudentID = 1;
|
|
330
|
+
|
|
331
|
+
## Delete
|
|
332
|
+
|
|
333
|
+
DELETE FROM Students
|
|
334
|
+
WHERE StudentID = 1;
|
|
335
|
+
|
|
336
|
+
## Count
|
|
337
|
+
|
|
338
|
+
SELECT COUNT(*) AS TotalStudents
|
|
339
|
+
FROM Students;
|
|
340
|
+
|
|
341
|
+
## Sum
|
|
342
|
+
|
|
343
|
+
SELECT SUM(Amount) AS TotalAmount
|
|
344
|
+
FROM Payments;
|
|
345
|
+
|
|
346
|
+
## Average
|
|
347
|
+
|
|
348
|
+
SELECT AVG(Marks) AS AverageMarks
|
|
349
|
+
FROM Results;
|
|
350
|
+
|
|
351
|
+
## Group By
|
|
352
|
+
|
|
353
|
+
SELECT ClassID, COUNT(*) AS TotalStudents
|
|
354
|
+
FROM Students
|
|
355
|
+
GROUP BY ClassID;
|
|
356
|
+
|
|
357
|
+
## Having
|
|
358
|
+
|
|
359
|
+
SELECT ClassID, COUNT(*) AS TotalStudents
|
|
360
|
+
FROM Students
|
|
361
|
+
GROUP BY ClassID
|
|
362
|
+
HAVING COUNT(*) > 5;
|
|
363
|
+
|
|
364
|
+
## Inner Join
|
|
365
|
+
|
|
366
|
+
SELECT Students.StudentName, Classes.ClassName
|
|
367
|
+
FROM Students
|
|
368
|
+
INNER JOIN Classes
|
|
369
|
+
ON Students.ClassID = Classes.ClassID;
|
|
370
|
+
|
|
371
|
+
## Left Join
|
|
372
|
+
|
|
373
|
+
SELECT Students.StudentName, Payments.Amount
|
|
374
|
+
FROM Students
|
|
375
|
+
LEFT JOIN Payments
|
|
376
|
+
ON Students.StudentID = Payments.StudentID;
|
|
377
|
+
|
|
378
|
+
## Multiple Joins
|
|
379
|
+
|
|
380
|
+
SELECT Students.StudentName, Classes.ClassName, Payments.Amount
|
|
381
|
+
FROM Students
|
|
382
|
+
JOIN Classes ON Students.ClassID = Classes.ClassID
|
|
383
|
+
JOIN Payments ON Students.StudentID = Payments.StudentID;
|
|
384
|
+
|
|
385
|
+
## Subquery
|
|
386
|
+
|
|
387
|
+
SELECT * FROM Students
|
|
388
|
+
WHERE ClassID IN (
|
|
389
|
+
SELECT ClassID FROM Classes WHERE ClassName = 'S6'
|
|
390
|
+
);
|
|
391
|
+
|
|
392
|
+
## Between
|
|
393
|
+
|
|
394
|
+
SELECT * FROM Payments
|
|
395
|
+
WHERE Amount BETWEEN 1000 AND 5000;
|
|
396
|
+
|
|
397
|
+
## Dates
|
|
398
|
+
|
|
399
|
+
SELECT * FROM Orders
|
|
400
|
+
WHERE DATE(CreatedAt) = CURDATE();
|
|
401
|
+
|
|
402
|
+
## Beginner Exam CRUD Pattern
|
|
403
|
+
|
|
404
|
+
GET all:
|
|
405
|
+
SELECT * FROM TableName;
|
|
406
|
+
|
|
407
|
+
GET one:
|
|
408
|
+
SELECT * FROM TableName WHERE ID = ?;
|
|
409
|
+
|
|
410
|
+
CREATE:
|
|
411
|
+
INSERT INTO TableName (Column1, Column2) VALUES (?, ?);
|
|
412
|
+
|
|
413
|
+
UPDATE:
|
|
414
|
+
UPDATE TableName SET Column1 = ?, Column2 = ? WHERE ID = ?;
|
|
415
|
+
|
|
416
|
+
DELETE:
|
|
417
|
+
DELETE FROM TableName WHERE ID = ?;
|
|
418
|
+
`;
|
|
419
|
+
|
|
420
|
+
await fs.writeFile(path.join(target, "sql.md"), sqlCheat);
|
|
421
|
+
|
|
422
|
+
async function inject(dir) {
|
|
423
|
+
const files = await fs.readdir(dir);
|
|
424
|
+
|
|
425
|
+
for (const file of files) {
|
|
426
|
+
const full = path.join(dir, file);
|
|
427
|
+
const stat = await fs.stat(full);
|
|
428
|
+
|
|
429
|
+
if (stat.isDirectory()) {
|
|
430
|
+
await inject(full);
|
|
431
|
+
} else {
|
|
432
|
+
try {
|
|
433
|
+
let content = await fs.readFile(full, "utf8");
|
|
434
|
+
|
|
435
|
+
content = content.replaceAll("__THEME__", answers.theme);
|
|
436
|
+
content = content.replaceAll("__PORT__", answers.serverPort);
|
|
437
|
+
|
|
438
|
+
await fs.writeFile(full, content);
|
|
439
|
+
} catch {}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
await inject(target);
|
|
445
|
+
|
|
446
|
+
console.log(chalk.green("\nProject created successfully.\n"));
|
|
447
|
+
|
|
448
|
+
if (answers.installDeps) {
|
|
449
|
+
console.log(chalk.white("Installing backend dependencies..."));
|
|
450
|
+
|
|
451
|
+
const serverInstall = spawnSync("npm", ["install"], {
|
|
452
|
+
cwd: path.join(target, "server"),
|
|
453
|
+
stdio: "inherit",
|
|
454
|
+
shell: true
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
if (serverInstall.status !== 0) {
|
|
458
|
+
console.log(
|
|
459
|
+
chalk.red(
|
|
460
|
+
"Backend dependency installation failed. Run npm install manually inside server."
|
|
461
|
+
)
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
console.log(chalk.white("Installing frontend dependencies..."));
|
|
466
|
+
|
|
467
|
+
const clientInstall = spawnSync("npm", ["install"], {
|
|
468
|
+
cwd: path.join(target, "client"),
|
|
469
|
+
stdio: "inherit",
|
|
470
|
+
shell: true
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
if (clientInstall.status !== 0) {
|
|
474
|
+
console.log(
|
|
475
|
+
chalk.red(
|
|
476
|
+
"Frontend dependency installation failed. Run npm install manually inside client."
|
|
477
|
+
)
|
|
478
|
+
);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
console.log(chalk.cyan("\nNext steps:\n"));
|
|
483
|
+
console.log(chalk.gray(`1. Run schema.sql in MySQL`));
|
|
484
|
+
console.log(chalk.gray(`2. cd ${projectName}/server && npm run dev`));
|
|
485
|
+
console.log(chalk.gray(`3. cd ${projectName}/client && npm run dev\n`));
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-batman",
|
|
3
|
+
"version": "1.0.2",
|
|
4
|
+
"description": "Student-friendly fullstack exam starter generator",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"create-batman": "bin/index.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"start": "node bin/index.js"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"cli",
|
|
14
|
+
"student",
|
|
15
|
+
"exam",
|
|
16
|
+
"react",
|
|
17
|
+
"vite",
|
|
18
|
+
"express",
|
|
19
|
+
"mysql",
|
|
20
|
+
"sequelize",
|
|
21
|
+
"batman"
|
|
22
|
+
],
|
|
23
|
+
"author": "Aime",
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"chalk": "^5.3.0",
|
|
27
|
+
"fs-extra": "^11.2.0",
|
|
28
|
+
"inquirer": "^9.2.23"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Parachute</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<div id="root"></div>
|
|
10
|
+
<script type="module" src="/src/main.jsx"></script>
|
|
11
|
+
</body>
|
|
12
|
+
</html>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"private": true,
|
|
3
|
+
"scripts": {
|
|
4
|
+
"dev": "vite",
|
|
5
|
+
"build": "vite build",
|
|
6
|
+
"preview": "vite preview"
|
|
7
|
+
},
|
|
8
|
+
"dependencies": {
|
|
9
|
+
"axios": "^1.7.2",
|
|
10
|
+
"react": "^18.3.1",
|
|
11
|
+
"react-dom": "^18.3.1",
|
|
12
|
+
"react-router-dom": "^6.30.1"
|
|
13
|
+
},
|
|
14
|
+
"devDependencies": {
|
|
15
|
+
"@vitejs/plugin-react": "^4.3.1",
|
|
16
|
+
"autoprefixer": "^10.4.20",
|
|
17
|
+
"postcss": "^8.4.41",
|
|
18
|
+
"tailwindcss": "^3.4.10",
|
|
19
|
+
"vite": "^5.3.4"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { NavLink, useNavigate } from "react-router-dom";
|
|
2
|
+
import { api } from "../api/client";
|
|
3
|
+
import { clearUser, getUser } from "../utils/auth";
|
|
4
|
+
|
|
5
|
+
const links = [
|
|
6
|
+
{ to: "/", label: "Dashboard" },
|
|
7
|
+
{ to: "/page1", label: "Form" },
|
|
8
|
+
{ to: "/page2", label: "Table" },
|
|
9
|
+
{ to: "/page3", label: "Cards" }
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
export default function Navbar() {
|
|
13
|
+
const navigate = useNavigate();
|
|
14
|
+
const user = getUser();
|
|
15
|
+
|
|
16
|
+
const logout = async () => {
|
|
17
|
+
try {
|
|
18
|
+
await api.post("/auth/logout");
|
|
19
|
+
} catch {}
|
|
20
|
+
|
|
21
|
+
clearUser();
|
|
22
|
+
navigate("/login");
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<header className="sticky top-0 z-50 border-b border-slate-200 bg-white/95 backdrop-blur">
|
|
27
|
+
<div className="mx-auto flex max-w-6xl flex-col gap-4 px-4 py-4 sm:px-6 lg:flex-row lg:items-center lg:justify-between lg:px-8">
|
|
28
|
+
<div className="flex items-center justify-between gap-4">
|
|
29
|
+
<div>
|
|
30
|
+
<p className="text-lg font-black tracking-tight text-slate-950">Parachute</p>
|
|
31
|
+
<p className="text-xs text-slate-500">Full-stack starter</p>
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<button
|
|
35
|
+
onClick={logout}
|
|
36
|
+
className="rounded-xl bg-slate-950 px-4 py-2 text-sm font-bold text-white lg:hidden"
|
|
37
|
+
>
|
|
38
|
+
Logout
|
|
39
|
+
</button>
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
<nav className="flex gap-2 overflow-x-auto rounded-2xl border border-slate-200 bg-slate-50 p-1">
|
|
43
|
+
{links.map((link) => (
|
|
44
|
+
<NavLink
|
|
45
|
+
key={link.to}
|
|
46
|
+
to={link.to}
|
|
47
|
+
end={link.to === "/"}
|
|
48
|
+
className={({ isActive }) =>
|
|
49
|
+
`whitespace-nowrap rounded-xl px-4 py-2 text-sm font-bold transition ${
|
|
50
|
+
isActive
|
|
51
|
+
? "bg-white text-slate-950 shadow-sm"
|
|
52
|
+
: "text-slate-500 hover:bg-white hover:text-slate-900"
|
|
53
|
+
}`
|
|
54
|
+
}
|
|
55
|
+
>
|
|
56
|
+
{link.label}
|
|
57
|
+
</NavLink>
|
|
58
|
+
))}
|
|
59
|
+
</nav>
|
|
60
|
+
|
|
61
|
+
<div className="hidden items-center gap-3 lg:flex">
|
|
62
|
+
<span className="max-w-[180px] truncate text-sm font-semibold text-slate-500">
|
|
63
|
+
{user?.Username || "User"}
|
|
64
|
+
</span>
|
|
65
|
+
<button
|
|
66
|
+
onClick={logout}
|
|
67
|
+
className="rounded-xl bg-slate-950 px-4 py-2 text-sm font-bold text-white transition hover:bg-slate-800"
|
|
68
|
+
>
|
|
69
|
+
Logout
|
|
70
|
+
</button>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
</header>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
@tailwind base;
|
|
2
|
+
@tailwind components;
|
|
3
|
+
@tailwind utilities;
|
|
4
|
+
|
|
5
|
+
* {
|
|
6
|
+
box-sizing: border-box;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
body {
|
|
10
|
+
margin: 0;
|
|
11
|
+
min-width: 320px;
|
|
12
|
+
background: #f8fafc;
|
|
13
|
+
color: #111827;
|
|
14
|
+
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
a {
|
|
18
|
+
color: inherit;
|
|
19
|
+
text-decoration: none;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
input, select, textarea, button {
|
|
23
|
+
font: inherit;
|
|
24
|
+
}
|