create-stackflow 1.0.4 → 1.0.5
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/cli.js +1 -1
- package/package.json +1 -1
- package/src/generators/backend.js +115 -70
- package/src/generators/frontend.js +77 -58
package/cli.js
CHANGED
|
@@ -8,7 +8,7 @@ const program = new Command();
|
|
|
8
8
|
program
|
|
9
9
|
.name("create-stackflow")
|
|
10
10
|
.description("Generate a production-minded MERN starter with frontend, backend, auth, CRUD, and dashboard UI.")
|
|
11
|
-
.version("1.0.
|
|
11
|
+
.version("1.0.5")
|
|
12
12
|
.argument("[project-name]", "project folder name")
|
|
13
13
|
.option("--skip-install", "generate files without installing dependencies")
|
|
14
14
|
.option("--yes", "use recommended defaults")
|
package/package.json
CHANGED
|
@@ -125,6 +125,13 @@ COOKIE_NAME=stackflow_token
|
|
|
125
125
|
if (context.multer) await fs.outputFile(path.join(context.backendDir, "uploads", ".gitkeep"), "");
|
|
126
126
|
|
|
127
127
|
const files = backendTemplates({ ...context, language: backendContext.language, clientUrl }, e);
|
|
128
|
+
|
|
129
|
+
// Filter out unused configuration files
|
|
130
|
+
if (!context.cloudinary) delete files[`src/config/cloudinary.${e}`];
|
|
131
|
+
if (!context.multer) delete files[`src/services/uploadService.${e}`];
|
|
132
|
+
if (!context.winston) delete files[`src/utils/logger.${e}`];
|
|
133
|
+
if (!context.swagger) delete files[`src/config/swagger.${e}`];
|
|
134
|
+
|
|
128
135
|
await Promise.all(Object.entries(files).map(([file, content]) =>
|
|
129
136
|
writeTemplate(path.join(context.backendDir, file), content, context)
|
|
130
137
|
));
|
|
@@ -188,9 +195,9 @@ ${context.rateLimit === false ? "" : "import rateLimit from \"express-rate-limit
|
|
|
188
195
|
${context.hpp ? "import hpp from \"hpp\";" : ""}
|
|
189
196
|
${imports.morgan}
|
|
190
197
|
${imports.swagger}
|
|
191
|
-
import apiRoutes from "./routes/
|
|
192
|
-
import { errorHandler } from "./middleware/
|
|
193
|
-
import { notFound } from "./middleware/
|
|
198
|
+
import apiRoutes from "./routes/mainRoute.js";
|
|
199
|
+
import { errorHandler } from "./middleware/errorMiddleware.js";
|
|
200
|
+
import { notFound } from "./middleware/notFoundMiddleware.js";
|
|
194
201
|
|
|
195
202
|
const app = express();
|
|
196
203
|
|
|
@@ -228,7 +235,7 @@ export async function connectDB() {
|
|
|
228
235
|
${context.winston ? "logger.info(\"MongoDB connected\");" : "console.log(\"MongoDB connected\");"}
|
|
229
236
|
}
|
|
230
237
|
`,
|
|
231
|
-
[`src/models/
|
|
238
|
+
[`src/models/userModel.${e}`]: `import mongoose from "mongoose";
|
|
232
239
|
import bcrypt from "bcryptjs";
|
|
233
240
|
|
|
234
241
|
const userSchema = new mongoose.Schema({
|
|
@@ -248,9 +255,9 @@ userSchema.methods.comparePassword = function(candidatePassword) {
|
|
|
248
255
|
|
|
249
256
|
export const User = mongoose.model("User", userSchema);
|
|
250
257
|
`,
|
|
251
|
-
[`src/models/
|
|
258
|
+
[`src/models/todoModel.${e}`]: `import mongoose from "mongoose";
|
|
252
259
|
|
|
253
|
-
const
|
|
260
|
+
const todoSchema = new mongoose.Schema({
|
|
254
261
|
title: { type: String, required: true, trim: true },
|
|
255
262
|
description: { type: String, default: "" },
|
|
256
263
|
imageUrl: { type: String, default: "" },
|
|
@@ -258,7 +265,7 @@ const taskSchema = new mongoose.Schema({
|
|
|
258
265
|
owner: { type: mongoose.Schema.Types.ObjectId, ref: "User", required: true }
|
|
259
266
|
}, { timestamps: true });
|
|
260
267
|
|
|
261
|
-
export const
|
|
268
|
+
export const Todo = mongoose.model("Todo", todoSchema);
|
|
262
269
|
`,
|
|
263
270
|
[`src/utils/jwt.${e}`]: `import jwt from "jsonwebtoken";
|
|
264
271
|
|
|
@@ -277,8 +284,8 @@ export function setAuthCookie(res, token) {
|
|
|
277
284
|
});
|
|
278
285
|
}
|
|
279
286
|
`,
|
|
280
|
-
[`src/middleware/
|
|
281
|
-
import { User } from "../models/
|
|
287
|
+
[`src/middleware/authMiddleware.${e}`]: `${reqRes}import jwt from "jsonwebtoken";
|
|
288
|
+
import { User } from "../models/userModel.js";
|
|
282
289
|
|
|
283
290
|
export async function protect(req${type("Request & { user?: unknown }")}, res${type("Response")}, next${type("NextFunction")}) {
|
|
284
291
|
try {
|
|
@@ -301,7 +308,7 @@ export async function protect(req${type("Request & { user?: unknown }")}, res${t
|
|
|
301
308
|
}
|
|
302
309
|
}
|
|
303
310
|
`,
|
|
304
|
-
[`src/middleware/
|
|
311
|
+
[`src/middleware/errorMiddleware.${e}`]: `${reqRes}export function errorHandler(error${type("Error & { statusCode?: number }")}, _req${type("Request")}, res${type("Response")}, _next${type("NextFunction")}) {
|
|
305
312
|
const status = error.statusCode || 500;
|
|
306
313
|
res.status(status).json({
|
|
307
314
|
message: error.message || "Server error",
|
|
@@ -309,11 +316,11 @@ export async function protect(req${type("Request & { user?: unknown }")}, res${t
|
|
|
309
316
|
});
|
|
310
317
|
}
|
|
311
318
|
`,
|
|
312
|
-
[`src/middleware/
|
|
319
|
+
[`src/middleware/notFoundMiddleware.${e}`]: `${reqRes}export function notFound(req${type("Request")}, res${type("Response")}) {
|
|
313
320
|
res.status(404).json({ message: \`Route not found: \${req.originalUrl}\` });
|
|
314
321
|
}
|
|
315
322
|
`,
|
|
316
|
-
[`src/controllers/
|
|
323
|
+
[`src/controllers/authController.${e}`]: `import { User } from "../models/userModel.js";
|
|
317
324
|
import { signToken, setAuthCookie } from "../utils/jwt.js";
|
|
318
325
|
|
|
319
326
|
function publicUser(user) {
|
|
@@ -358,56 +365,93 @@ export async function logout(_req, res) {
|
|
|
358
365
|
res.json({ message: "Logged out" });
|
|
359
366
|
}
|
|
360
367
|
`,
|
|
361
|
-
[`src/controllers/
|
|
368
|
+
[`src/controllers/todoController.${e}`]: `import { Todo } from "../models/todoModel.js";
|
|
362
369
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
370
|
+
const todoController = {
|
|
371
|
+
// Get All Todos
|
|
372
|
+
getTodos: async (req, res) => {
|
|
373
|
+
try {
|
|
374
|
+
const todos = await Todo.find({ owner: req.user._id }).sort({ createdAt: -1 });
|
|
375
|
+
res.status(200).json({
|
|
376
|
+
success: true,
|
|
377
|
+
message: "Todos fetched successfully",
|
|
378
|
+
data: todos
|
|
379
|
+
});
|
|
380
|
+
} catch (error) {
|
|
381
|
+
res.status(500).json({
|
|
382
|
+
success: false,
|
|
383
|
+
message: error.message
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
},
|
|
371
387
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
388
|
+
// Create Todo
|
|
389
|
+
createTodo: async (req, res) => {
|
|
390
|
+
try {
|
|
391
|
+
const imageUrl = req.file ? \`/uploads/\${req.file.filename}\` : "";
|
|
392
|
+
const newTodo = await Todo.create({ ...req.body, imageUrl, owner: req.user._id });
|
|
393
|
+
res.status(201).json({
|
|
394
|
+
success: true,
|
|
395
|
+
message: "Todo created successfully",
|
|
396
|
+
data: newTodo
|
|
397
|
+
});
|
|
398
|
+
} catch (error) {
|
|
399
|
+
res.status(500).json({
|
|
400
|
+
success: false,
|
|
401
|
+
message: error.message
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
},
|
|
381
405
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
}
|
|
406
|
+
// Update Todo
|
|
407
|
+
updateTodo: async (req, res) => {
|
|
408
|
+
try {
|
|
409
|
+
const { id } = req.params;
|
|
410
|
+
const updates = { ...req.body };
|
|
411
|
+
if (req.file) updates.imageUrl = \`/uploads/\${req.file.filename}\`;
|
|
412
|
+
const todo = await Todo.findOneAndUpdate(
|
|
413
|
+
{ _id: id, owner: req.user._id },
|
|
414
|
+
updates,
|
|
415
|
+
{ new: true, runValidators: true }
|
|
416
|
+
);
|
|
417
|
+
if (!todo) return res.status(404).json({ success: false, message: "Todo not found" });
|
|
418
|
+
res.status(200).json({
|
|
419
|
+
success: true,
|
|
420
|
+
message: \`Todo \${id} updated successfully\`,
|
|
421
|
+
data: todo
|
|
422
|
+
});
|
|
423
|
+
} catch (error) {
|
|
424
|
+
res.status(500).json({
|
|
425
|
+
success: false,
|
|
426
|
+
message: error.message
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
},
|
|
397
430
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
431
|
+
// Delete Todo
|
|
432
|
+
deleteTodo: async (req, res) => {
|
|
433
|
+
try {
|
|
434
|
+
const { id } = req.params;
|
|
435
|
+
const todo = await Todo.findOneAndDelete({ _id: id, owner: req.user._id });
|
|
436
|
+
if (!todo) return res.status(404).json({ success: false, message: "Todo not found" });
|
|
437
|
+
res.status(200).json({
|
|
438
|
+
success: true,
|
|
439
|
+
message: \`Todo \${id} deleted successfully\`
|
|
440
|
+
});
|
|
441
|
+
} catch (error) {
|
|
442
|
+
res.status(500).json({
|
|
443
|
+
success: false,
|
|
444
|
+
message: error.message
|
|
445
|
+
});
|
|
446
|
+
}
|
|
405
447
|
}
|
|
406
|
-
}
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
export default todoController;
|
|
407
451
|
`,
|
|
408
|
-
[`src/routes/
|
|
409
|
-
import { login, logout, me, register } from "../controllers/
|
|
410
|
-
import { protect } from "../middleware/
|
|
452
|
+
[`src/routes/authRoute.${e}`]: `import { Router } from "express";
|
|
453
|
+
import { login, logout, me, register } from "../controllers/authController.js";
|
|
454
|
+
import { protect } from "../middleware/authMiddleware.js";
|
|
411
455
|
|
|
412
456
|
const router = Router();
|
|
413
457
|
|
|
@@ -418,33 +462,34 @@ router.post("/logout", logout);
|
|
|
418
462
|
|
|
419
463
|
export default router;
|
|
420
464
|
`,
|
|
421
|
-
[`src/routes/
|
|
422
|
-
import authRoutes from "./
|
|
423
|
-
import
|
|
465
|
+
[`src/routes/mainRoute.${e}`]: `import { Router } from "express";
|
|
466
|
+
import authRoutes from "./authRoute.js";
|
|
467
|
+
import todoRoutes from "./todoRoute.js";
|
|
424
468
|
|
|
425
469
|
const router = Router();
|
|
426
470
|
|
|
427
471
|
router.use("/auth", authRoutes);
|
|
428
|
-
router.use("/
|
|
472
|
+
router.use("/todos", todoRoutes);
|
|
429
473
|
|
|
430
474
|
export default router;
|
|
431
475
|
`,
|
|
432
|
-
[`src/routes/
|
|
433
|
-
import
|
|
434
|
-
import { protect } from "../middleware/
|
|
435
|
-
${context.multer ? "import { upload } from \"../services/
|
|
476
|
+
[`src/routes/todoRoute.${e}`]: `import { Router } from "express";
|
|
477
|
+
import todoController from "../controllers/todoController.js";
|
|
478
|
+
import { protect } from "../middleware/authMiddleware.js";
|
|
479
|
+
${context.multer ? "import { upload } from \"../services/uploadService.js\";" : ""}
|
|
436
480
|
|
|
437
481
|
const router = Router();
|
|
438
482
|
|
|
439
483
|
router.use(protect);
|
|
440
|
-
|
|
441
|
-
router.
|
|
442
|
-
router.
|
|
443
|
-
router.
|
|
484
|
+
|
|
485
|
+
router.get("/", todoController.getTodos);
|
|
486
|
+
router.post("/", ${context.multer ? "upload.single(\"image\"), " : ""}todoController.createTodo);
|
|
487
|
+
router.put("/:id", ${context.multer ? "upload.single(\"image\"), " : ""}todoController.updateTodo);
|
|
488
|
+
router.delete("/:id", todoController.deleteTodo);
|
|
444
489
|
|
|
445
490
|
export default router;
|
|
446
491
|
`,
|
|
447
|
-
[`src/services/
|
|
492
|
+
[`src/services/uploadService.${e}`]: context.multer ? `import multer from "multer";
|
|
448
493
|
|
|
449
494
|
const storage = multer.diskStorage({
|
|
450
495
|
destination: "uploads/",
|
|
@@ -456,7 +501,7 @@ const storage = multer.diskStorage({
|
|
|
456
501
|
export const upload = multer({ storage });
|
|
457
502
|
` : `export {};
|
|
458
503
|
`,
|
|
459
|
-
[`src/validations/
|
|
504
|
+
[`src/validations/authValidation.${e}`]: `export const authValidation = {
|
|
460
505
|
register: ["name", "email", "password"],
|
|
461
506
|
login: ["email", "password"]
|
|
462
507
|
};
|
|
@@ -122,11 +122,11 @@ async function writeReactOverlay(context) {
|
|
|
122
122
|
const src = path.join(context.frontendDir, "src");
|
|
123
123
|
await fs.ensureDir(path.join(src, "components", "ui"));
|
|
124
124
|
await fs.ensureDir(path.join(src, "features", "auth"));
|
|
125
|
-
await fs.ensureDir(path.join(src, "features", "
|
|
125
|
+
await fs.ensureDir(path.join(src, "features", "todos"));
|
|
126
126
|
await fs.ensureDir(path.join(src, "lib"));
|
|
127
127
|
await fs.ensureDir(path.join(src, "pages"));
|
|
128
128
|
await fs.ensureDir(path.join(src, "routes"));
|
|
129
|
-
await fs.ensureDir(path.join(src, "store"));
|
|
129
|
+
if (context.state !== "none") await fs.ensureDir(path.join(src, "store"));
|
|
130
130
|
|
|
131
131
|
await writeTemplate(path.join(context.frontendDir, ".env"), "VITE_API_URL=http://localhost:5000/api\n");
|
|
132
132
|
await relaxTypescriptConfig(context, "react");
|
|
@@ -145,6 +145,9 @@ export default defineConfig({
|
|
|
145
145
|
}
|
|
146
146
|
|
|
147
147
|
const files = reactTemplates(context, x);
|
|
148
|
+
if (context.state === "none") delete files[`store/auth.${x}`];
|
|
149
|
+
if (context.validation === "none") delete files[`lib/validation.${context.language === "typescript" ? "ts" : "js"}`];
|
|
150
|
+
|
|
148
151
|
await Promise.all(Object.entries(files).map(([file, content]) => writeTemplate(path.join(src, file), content, context)));
|
|
149
152
|
if (context.language === "typescript") {
|
|
150
153
|
await writeTemplate(path.join(src, "vite-env.d.ts"), `/// <reference types="vite/client" />
|
|
@@ -160,14 +163,17 @@ async function writeNextOverlay(context) {
|
|
|
160
163
|
await fs.ensureDir(path.join(src, "app", "register"));
|
|
161
164
|
await fs.ensureDir(path.join(src, "components", "ui"));
|
|
162
165
|
await fs.ensureDir(path.join(src, "features", "auth"));
|
|
163
|
-
await fs.ensureDir(path.join(src, "features", "
|
|
166
|
+
await fs.ensureDir(path.join(src, "features", "todos"));
|
|
164
167
|
await fs.ensureDir(path.join(src, "lib"));
|
|
165
|
-
await fs.ensureDir(path.join(src, "store"));
|
|
168
|
+
if (context.state !== "none") await fs.ensureDir(path.join(src, "store"));
|
|
166
169
|
|
|
167
170
|
await removeNextGeneratedDuplicates(context, x);
|
|
168
171
|
await writeTemplate(path.join(context.frontendDir, ".env.local"), "NEXT_PUBLIC_API_URL=http://localhost:5000/api\n");
|
|
169
172
|
await relaxTypescriptConfig(context, "next");
|
|
170
173
|
const files = nextTemplates(context, x);
|
|
174
|
+
if (context.state === "none") delete files[`store/auth.${x}`];
|
|
175
|
+
if (context.validation === "none") delete files[`lib/validation.${context.language === "typescript" ? "ts" : "js"}`];
|
|
176
|
+
|
|
171
177
|
await Promise.all(Object.entries(files).map(([file, content]) => writeTemplate(path.join(src, file), content, context)));
|
|
172
178
|
}
|
|
173
179
|
|
|
@@ -241,11 +247,11 @@ async function relaxTypescriptConfig(context, framework) {
|
|
|
241
247
|
function commonTemplates(context, x) {
|
|
242
248
|
const isTs = context.language === "typescript";
|
|
243
249
|
const typeBlock = isTs ? `export type User = { id: string; name: string; email: string };
|
|
244
|
-
export type
|
|
250
|
+
export type Todo = { _id: string; title: string; description?: string; imageUrl?: string; status: "todo" | "in-progress" | "done" };
|
|
245
251
|
` : "";
|
|
246
252
|
const schema = context.validation === "none" || context.validation === "joi"
|
|
247
253
|
? `export const authSchema = null;
|
|
248
|
-
export const
|
|
254
|
+
export const todoSchema = null;
|
|
249
255
|
`
|
|
250
256
|
: context.validation === "zod"
|
|
251
257
|
? `import { z } from "zod";
|
|
@@ -256,7 +262,7 @@ export const authSchema = z.object({
|
|
|
256
262
|
password: z.string().min(6)
|
|
257
263
|
});
|
|
258
264
|
|
|
259
|
-
export const
|
|
265
|
+
export const todoSchema = z.object({
|
|
260
266
|
title: z.string().min(2),
|
|
261
267
|
description: z.string().optional(),
|
|
262
268
|
status: z.enum(["todo", "in-progress", "done"]).default("todo")
|
|
@@ -270,7 +276,7 @@ export const authSchema = yup.object({
|
|
|
270
276
|
password: yup.string().min(6).required()
|
|
271
277
|
});
|
|
272
278
|
|
|
273
|
-
export const
|
|
279
|
+
export const todoSchema = yup.object({
|
|
274
280
|
title: yup.string().min(2).required(),
|
|
275
281
|
description: yup.string(),
|
|
276
282
|
status: yup.string().oneOf(["todo", "in-progress", "done"]).default("todo")
|
|
@@ -354,7 +360,7 @@ api.interceptors.request.use((config) => {
|
|
|
354
360
|
`,
|
|
355
361
|
[`lib/validation.${isTs ? "ts" : "js"}`]: schema,
|
|
356
362
|
[`store/auth.${x}`]: authStore,
|
|
357
|
-
[`features/auth/
|
|
363
|
+
[`features/auth/authService.${isTs ? "ts" : "js"}`]: `import { api } from "../../lib/api";
|
|
358
364
|
|
|
359
365
|
export const authService = {
|
|
360
366
|
register: (payload) => api.post("/auth/register", payload).then((res) => res.data),
|
|
@@ -363,13 +369,13 @@ export const authService = {
|
|
|
363
369
|
logout: () => api.post("/auth/logout").then((res) => res.data)
|
|
364
370
|
};
|
|
365
371
|
`,
|
|
366
|
-
[`features/
|
|
372
|
+
[`features/todos/todoService.${isTs ? "ts" : "js"}`]: `import { api } from "../../lib/api";
|
|
367
373
|
|
|
368
|
-
export const
|
|
369
|
-
list: () => api.get("/
|
|
370
|
-
create: (payload) => api.post("/
|
|
371
|
-
update: (id, payload) => api.
|
|
372
|
-
remove: (id) => api.delete(\`/
|
|
374
|
+
export const todoService = {
|
|
375
|
+
list: () => api.get("/todos").then((res) => res.data.data),
|
|
376
|
+
create: (payload) => api.post("/todos", payload, multipartConfig(payload)).then((res) => res.data.data),
|
|
377
|
+
update: (id, payload) => api.put(\`/todos/\${id}\`, payload, multipartConfig(payload)).then((res) => res.data.data),
|
|
378
|
+
remove: (id) => api.delete(\`/todos/\${id}\`).then((res) => res.data)
|
|
373
379
|
};
|
|
374
380
|
|
|
375
381
|
function multipartConfig(payload) {
|
|
@@ -457,13 +463,10 @@ export default function App() {
|
|
|
457
463
|
}
|
|
458
464
|
`}`,
|
|
459
465
|
[`routes/ProtectedRoute.${x}`]: `import { Navigate } from "react-router-dom";
|
|
460
|
-
${context.state === "zustand" ? "import { useAuthStore } from \"../store/auth\";" : ""}
|
|
461
|
-
${context.state === "context-api" ? "import { useAuthContext } from \"../store/auth\";" : ""}
|
|
462
|
-
${context.state === "redux-toolkit" ? "import { useSelector } from \"react-redux\";" : ""}
|
|
463
466
|
|
|
464
467
|
export function ProtectedRoute({ children }) {
|
|
465
|
-
const
|
|
466
|
-
return
|
|
468
|
+
const isAuthenticated = Boolean(localStorage.getItem("stackflow_token"));
|
|
469
|
+
return isAuthenticated ? children : <Navigate to="/login" replace />;
|
|
467
470
|
}
|
|
468
471
|
`,
|
|
469
472
|
[`pages/Login.${x}`]: authPage("login", context),
|
|
@@ -530,12 +533,20 @@ ${navImport}
|
|
|
530
533
|
import { toast } from "sonner";
|
|
531
534
|
import { Button } from "${next ? "../../components/ui/button" : "../components/ui/button"}";
|
|
532
535
|
import { Input } from "${next ? "../../components/ui/input" : "../components/ui/input"}";
|
|
533
|
-
import { authService } from "${next ? "../../features/auth/
|
|
536
|
+
import { authService } from "${next ? "../../features/auth/authService" : "../features/auth/authService"}";
|
|
534
537
|
${context.state === "zustand" ? `import { useAuthStore } from "${next ? "../../store/auth" : "../store/auth"}";` : ""}
|
|
538
|
+
${context.state === "context-api" ? `import { useAuthContext } from "${next ? "../../store/auth" : "../store/auth"}";` : ""}
|
|
539
|
+
${context.state === "redux-toolkit" ? `import { useDispatch } from "react-redux";\nimport { setSession as setReduxSession } from "${next ? "../../store/auth" : "../store/auth"}";` : ""}
|
|
535
540
|
|
|
536
541
|
export function ${isRegister ? "Register" : "Login"}() {
|
|
537
542
|
${navHook}
|
|
538
|
-
|
|
543
|
+
${context.state === "redux-toolkit" ? "const dispatch = useDispatch();" : ""}
|
|
544
|
+
const setSession = ${
|
|
545
|
+
context.state === "zustand" ? "useAuthStore((state) => state.setSession)" :
|
|
546
|
+
context.state === "context-api" ? "useAuthContext().setSession" :
|
|
547
|
+
context.state === "redux-toolkit" ? "(data) => dispatch(setReduxSession(data))" :
|
|
548
|
+
"({ user, token }) => localStorage.setItem(\"stackflow_token\", token)"
|
|
549
|
+
};
|
|
539
550
|
const [loading, setLoading] = useState(false);
|
|
540
551
|
const [form, setForm] = useState({ name: "", email: "", password: "" });
|
|
541
552
|
|
|
@@ -583,15 +594,23 @@ import { Edit3, ImagePlus, LogOut, Moon, Plus, Save, Sun, Trash2, X } from "luci
|
|
|
583
594
|
import { toast } from "sonner";
|
|
584
595
|
import { Button } from "${next ? "../../components/ui/button" : "../components/ui/button"}";
|
|
585
596
|
import { Input } from "${next ? "../../components/ui/input" : "../components/ui/input"}";
|
|
586
|
-
import {
|
|
597
|
+
import { todoService } from "${next ? "../../features/todos/todoService" : "../features/todos/todoService"}";
|
|
587
598
|
${context.state === "zustand" ? `import { useAuthStore } from "${next ? "../../store/auth" : "../store/auth"}";` : ""}
|
|
599
|
+
${context.state === "context-api" ? `import { useAuthContext } from "${next ? "../../store/auth" : "../store/auth"}";` : ""}
|
|
600
|
+
${context.state === "redux-toolkit" ? `import { useDispatch } from "react-redux";\nimport { logout as reduxLogout } from "${next ? "../../store/auth" : "../store/auth"}";` : ""}
|
|
588
601
|
|
|
589
602
|
${componentName} {
|
|
590
603
|
const ${next ? "router" : "navigate"} = ${next ? "useRouter()" : "useNavigate()"};
|
|
591
|
-
|
|
604
|
+
${context.state === "redux-toolkit" ? "const dispatch = useDispatch();" : ""}
|
|
605
|
+
const logoutStore = ${
|
|
606
|
+
context.state === "zustand" ? "useAuthStore((state) => state.logout)" :
|
|
607
|
+
context.state === "context-api" ? "useAuthContext().logout" :
|
|
608
|
+
context.state === "redux-toolkit" ? "() => dispatch(reduxLogout())" :
|
|
609
|
+
"() => localStorage.removeItem(\"stackflow_token\")"
|
|
610
|
+
};
|
|
592
611
|
const [dark, setDark] = useState(() => typeof window !== "undefined" && localStorage.getItem("stackflow_theme") === "dark");
|
|
593
612
|
const [loading, setLoading] = useState(true);
|
|
594
|
-
const [
|
|
613
|
+
const [todos, setTodos] = useState([]);
|
|
595
614
|
const [form, setForm] = useState({ title: "", description: "", status: "todo", image: null });
|
|
596
615
|
const [editingId, setEditingId] = useState(null);
|
|
597
616
|
|
|
@@ -601,30 +620,30 @@ ${componentName} {
|
|
|
601
620
|
}, [dark]);
|
|
602
621
|
|
|
603
622
|
useEffect(() => {
|
|
604
|
-
|
|
605
|
-
.then(
|
|
606
|
-
.catch(() => toast.error("Could not load
|
|
623
|
+
todoService.list()
|
|
624
|
+
.then(setTodos)
|
|
625
|
+
.catch(() => toast.error("Could not load todos"))
|
|
607
626
|
.finally(() => setLoading(false));
|
|
608
627
|
}, []);
|
|
609
628
|
|
|
610
|
-
async function
|
|
629
|
+
async function createTodo(event) {
|
|
611
630
|
event.preventDefault();
|
|
612
631
|
if (!form.title.trim()) return;
|
|
613
632
|
const payload = toPayload(form);
|
|
614
633
|
const saved = editingId
|
|
615
|
-
? await
|
|
616
|
-
: await
|
|
617
|
-
|
|
634
|
+
? await todoService.update(editingId, payload)
|
|
635
|
+
: await todoService.create(payload);
|
|
636
|
+
setTodos(editingId ? todos.map((todo) => todo._id === editingId ? saved : todo) : [saved, ...todos]);
|
|
618
637
|
resetForm();
|
|
619
|
-
toast.success(editingId ? "
|
|
638
|
+
toast.success(editingId ? "Todo updated" : "Todo created");
|
|
620
639
|
}
|
|
621
640
|
|
|
622
|
-
function startEdit(
|
|
623
|
-
setEditingId(
|
|
641
|
+
function startEdit(todo) {
|
|
642
|
+
setEditingId(todo._id);
|
|
624
643
|
setForm({
|
|
625
|
-
title:
|
|
626
|
-
description:
|
|
627
|
-
status:
|
|
644
|
+
title: todo.title || "",
|
|
645
|
+
description: todo.description || "",
|
|
646
|
+
status: todo.status || "todo",
|
|
628
647
|
image: null
|
|
629
648
|
});
|
|
630
649
|
}
|
|
@@ -634,10 +653,10 @@ ${componentName} {
|
|
|
634
653
|
setForm({ title: "", description: "", status: "todo", image: null });
|
|
635
654
|
}
|
|
636
655
|
|
|
637
|
-
async function
|
|
638
|
-
await
|
|
639
|
-
|
|
640
|
-
toast.success("
|
|
656
|
+
async function removeTodo(id) {
|
|
657
|
+
await todoService.remove(id);
|
|
658
|
+
setTodos(todos.filter((todo) => todo._id !== id));
|
|
659
|
+
toast.success("Todo deleted");
|
|
641
660
|
}
|
|
642
661
|
|
|
643
662
|
function logout() {
|
|
@@ -675,37 +694,37 @@ ${componentName} {
|
|
|
675
694
|
<section className="mx-auto grid max-w-6xl gap-6 px-4 py-8 md:grid-cols-[1fr_320px]">
|
|
676
695
|
<div className="rounded-lg border border-slate-200 bg-white p-5 dark:border-slate-800 dark:bg-slate-900">
|
|
677
696
|
<div className="mb-4 flex items-center justify-between">
|
|
678
|
-
<h2 className="text-lg font-semibold">
|
|
679
|
-
<span className="text-sm text-slate-500">{
|
|
697
|
+
<h2 className="text-lg font-semibold">Todos</h2>
|
|
698
|
+
<span className="text-sm text-slate-500">{todos.length} total</span>
|
|
680
699
|
</div>
|
|
681
|
-
{loading ? <p className="text-sm text-slate-500">Loading
|
|
700
|
+
{loading ? <p className="text-sm text-slate-500">Loading todos...</p> : (
|
|
682
701
|
<div className="space-y-3">
|
|
683
|
-
{
|
|
684
|
-
<div key={
|
|
702
|
+
{todos.map((todo) => (
|
|
703
|
+
<div key={todo._id} className="grid gap-3 rounded-md border border-slate-200 p-3 dark:border-slate-800 sm:grid-cols-[96px_1fr_auto]">
|
|
685
704
|
<div className="h-24 overflow-hidden rounded-md bg-slate-100 dark:bg-slate-800">
|
|
686
|
-
{
|
|
705
|
+
{todo.imageUrl ? <img className="h-full w-full object-cover" src={\`http://localhost:5000\${todo.imageUrl}\`} alt={todo.title} /> : <div className="grid h-full place-items-center text-slate-400"><ImagePlus size={20} /></div>}
|
|
687
706
|
</div>
|
|
688
707
|
<div>
|
|
689
|
-
<p className="font-medium">{
|
|
690
|
-
<p className="mt-1 text-sm text-slate-500">{
|
|
691
|
-
<p className="mt-2 text-xs uppercase tracking-wide text-slate-500">{
|
|
708
|
+
<p className="font-medium">{todo.title}</p>
|
|
709
|
+
<p className="mt-1 text-sm text-slate-500">{todo.description || "No description"}</p>
|
|
710
|
+
<p className="mt-2 text-xs uppercase tracking-wide text-slate-500">{todo.status}</p>
|
|
692
711
|
</div>
|
|
693
712
|
<div className="flex items-start gap-2">
|
|
694
|
-
<Button variant="ghost" onClick={() => startEdit(
|
|
695
|
-
<Button variant="ghost" onClick={() =>
|
|
713
|
+
<Button variant="ghost" onClick={() => startEdit(todo)} aria-label="Edit todo"><Edit3 size={16} /></Button>
|
|
714
|
+
<Button variant="ghost" onClick={() => removeTodo(todo._id)} aria-label="Delete todo"><Trash2 size={16} /></Button>
|
|
696
715
|
</div>
|
|
697
716
|
</div>
|
|
698
717
|
))}
|
|
699
|
-
{!
|
|
718
|
+
{!todos.length && <p className="text-sm text-slate-500">No todos yet.</p>}
|
|
700
719
|
</div>
|
|
701
720
|
)}
|
|
702
721
|
</div>
|
|
703
|
-
<form onSubmit={
|
|
722
|
+
<form onSubmit={createTodo} className="h-fit space-y-3 rounded-lg border border-slate-200 bg-white p-5 dark:border-slate-800 dark:bg-slate-900">
|
|
704
723
|
<div className="flex items-center justify-between">
|
|
705
|
-
<h2 className="text-lg font-semibold">{editingId ? "Edit
|
|
724
|
+
<h2 className="text-lg font-semibold">{editingId ? "Edit todo" : "Create todo"}</h2>
|
|
706
725
|
{editingId && <Button type="button" variant="ghost" onClick={resetForm} aria-label="Cancel edit"><X size={18} /></Button>}
|
|
707
726
|
</div>
|
|
708
|
-
<Input value={form.title} onChange={(event) => setForm({ ...form, title: event.target.value })} placeholder="
|
|
727
|
+
<Input value={form.title} onChange={(event) => setForm({ ...form, title: event.target.value })} placeholder="Todo title" />
|
|
709
728
|
<Input value={form.description} onChange={(event) => setForm({ ...form, description: event.target.value })} placeholder="Description" />
|
|
710
729
|
<select className="h-10 w-full rounded-md border border-slate-300 bg-white px-3 text-sm dark:border-slate-700 dark:bg-slate-900" value={form.status} onChange={(event) => setForm({ ...form, status: event.target.value })}>
|
|
711
730
|
<option value="todo">Todo</option>
|
|
@@ -713,7 +732,7 @@ ${componentName} {
|
|
|
713
732
|
<option value="done">Done</option>
|
|
714
733
|
</select>
|
|
715
734
|
${context.multer ? `<Input type="file" accept="image/*" onChange={(event) => setForm({ ...form, image: event.target.files?.[0] || null })} />` : ""}
|
|
716
|
-
<Button className="w-full" aria-label={editingId ? "Save
|
|
735
|
+
<Button className="w-full" aria-label={editingId ? "Save todo" : "Add todo"}>{editingId ? <Save size={18} /> : <Plus size={18} />}<span className="ml-2">{editingId ? "Save changes" : "Add todo"}</span></Button>
|
|
717
736
|
</form>
|
|
718
737
|
</section>
|
|
719
738
|
</main>
|