create-stackflow 1.0.4 → 1.0.7

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.
@@ -3,6 +3,7 @@ import fs from "fs-extra";
3
3
  import ora from "ora";
4
4
  import { execa } from "execa";
5
5
  import { jsxExt, writeTemplate } from "../utils/template.js";
6
+ import { chatFrontendFiles, chatNextPages } from "./chat.js";
6
7
 
7
8
  export async function createFrontend(context) {
8
9
  await scaffoldOfficialFrontend(context);
@@ -43,30 +44,6 @@ async function scaffoldOfficialFrontend(context) {
43
44
 
44
45
  async function installFrontendDependencies(context) {
45
46
  await updateFrontendPackage(context);
46
- if (context.skipInstall) return;
47
- const spinner = ora("Installing frontend dependencies").start();
48
- try {
49
- const deps = [
50
- "axios@latest",
51
- "sonner@latest",
52
- "lucide-react@latest",
53
- "clsx@latest",
54
- "tailwind-merge@latest"
55
- ];
56
- if (context.frontend === "react" && context.router) deps.push("react-router-dom@latest");
57
- if (context.state === "zustand") deps.push("zustand@latest");
58
- if (context.state === "redux-toolkit") deps.push("@reduxjs/toolkit@latest", "react-redux@latest");
59
- if (context.form && context.form !== "none") deps.push("react-hook-form@latest", "@hookform/resolvers@latest");
60
- if (context.validation === "zod") deps.push("zod@latest");
61
- if (context.validation === "yup") deps.push("yup@latest");
62
- if (context.tailwind && context.frontend === "react") deps.push("tailwindcss@latest", "@tailwindcss/vite@latest");
63
-
64
- await execa("npm", ["install", ...deps], { cwd: context.frontendDir, stdio: "ignore" });
65
- spinner.succeed("Installing frontend dependencies");
66
- } catch (error) {
67
- spinner.fail("Installing frontend dependencies");
68
- throw error;
69
- }
70
47
  }
71
48
 
72
49
  async function updateFrontendPackage(context) {
@@ -85,6 +62,7 @@ async function updateFrontendPackage(context) {
85
62
  if (context.frontend === "react" && context.router) deps.push("react-router-dom");
86
63
  if (context.state === "zustand") deps.push("zustand");
87
64
  if (context.state === "redux-toolkit") deps.push("@reduxjs/toolkit", "react-redux");
65
+ if (context.socketio) deps.push("socket.io-client");
88
66
  if (context.form && context.form !== "none") deps.push("react-hook-form", "@hookform/resolvers");
89
67
  if (context.validation === "zod") deps.push("zod");
90
68
  if (context.validation === "yup") deps.push("yup");
@@ -95,8 +73,21 @@ async function updateFrontendPackage(context) {
95
73
  pkg.devDependencies["@tailwindcss/vite"] = "latest";
96
74
  }
97
75
 
76
+ const backendName = path.basename(context.backendDir);
77
+ pkg.devDependencies = pkg.devDependencies || {};
78
+ pkg.devDependencies.concurrently = "latest";
79
+
98
80
  if (context.frontend === "react") {
99
- pkg.scripts = { ...(pkg.scripts || {}), build: "vite build" };
81
+ pkg.scripts = {
82
+ ...(pkg.scripts || {}),
83
+ build: "vite build",
84
+ dev: `concurrently -n backend,frontend -c blue,green "npm run dev --prefix ../${backendName}" "vite"`,
85
+ };
86
+ } else {
87
+ pkg.scripts = {
88
+ ...(pkg.scripts || {}),
89
+ dev: `concurrently -n backend,frontend -c blue,green "npm run dev --prefix ../${backendName}" "next dev"`,
90
+ };
100
91
  }
101
92
 
102
93
  await fs.writeJson(packageFile, pkg, { spaces: 2 });
@@ -122,13 +113,21 @@ async function writeReactOverlay(context) {
122
113
  const src = path.join(context.frontendDir, "src");
123
114
  await fs.ensureDir(path.join(src, "components", "ui"));
124
115
  await fs.ensureDir(path.join(src, "features", "auth"));
125
- await fs.ensureDir(path.join(src, "features", "tasks"));
116
+ await fs.ensureDir(path.join(src, "features", "todos"));
117
+ if (context.socketio) await fs.ensureDir(path.join(src, "features", "chat"));
126
118
  await fs.ensureDir(path.join(src, "lib"));
127
119
  await fs.ensureDir(path.join(src, "pages"));
128
120
  await fs.ensureDir(path.join(src, "routes"));
129
- await fs.ensureDir(path.join(src, "store"));
121
+ if (context.state === "redux-toolkit") {
122
+ await fs.ensureDir(path.join(src, "redux", "slices"));
123
+ } else if (context.state !== "none") {
124
+ await fs.ensureDir(path.join(src, "store"));
125
+ }
130
126
 
131
- await writeTemplate(path.join(context.frontendDir, ".env"), "VITE_API_URL=http://localhost:5000/api\n");
127
+ await writeTemplate(
128
+ path.join(context.frontendDir, ".env"),
129
+ `VITE_API_URL=http://localhost:5000/api\n${context.socketio ? "VITE_SOCKET_URL=http://localhost:5000\n" : ""}`,
130
+ );
132
131
  await relaxTypescriptConfig(context, "react");
133
132
  if (context.tailwind) {
134
133
  await writeTemplate(path.join(context.frontendDir, "vite.config." + (context.language === "typescript" ? "ts" : "js")), `import { defineConfig } from "vite";
@@ -145,6 +144,8 @@ export default defineConfig({
145
144
  }
146
145
 
147
146
  const files = reactTemplates(context, x);
147
+ pruneUnusedFrontendFiles(files, context);
148
+
148
149
  await Promise.all(Object.entries(files).map(([file, content]) => writeTemplate(path.join(src, file), content, context)));
149
150
  if (context.language === "typescript") {
150
151
  await writeTemplate(path.join(src, "vite-env.d.ts"), `/// <reference types="vite/client" />
@@ -160,17 +161,59 @@ async function writeNextOverlay(context) {
160
161
  await fs.ensureDir(path.join(src, "app", "register"));
161
162
  await fs.ensureDir(path.join(src, "components", "ui"));
162
163
  await fs.ensureDir(path.join(src, "features", "auth"));
163
- await fs.ensureDir(path.join(src, "features", "tasks"));
164
+ await fs.ensureDir(path.join(src, "features", "todos"));
165
+ if (context.socketio) await fs.ensureDir(path.join(src, "features", "chat"));
164
166
  await fs.ensureDir(path.join(src, "lib"));
165
- await fs.ensureDir(path.join(src, "store"));
167
+ if (context.state === "redux-toolkit") {
168
+ await fs.ensureDir(path.join(src, "redux", "slices"));
169
+ } else if (context.state !== "none") {
170
+ await fs.ensureDir(path.join(src, "store"));
171
+ }
166
172
 
167
173
  await removeNextGeneratedDuplicates(context, x);
168
- await writeTemplate(path.join(context.frontendDir, ".env.local"), "NEXT_PUBLIC_API_URL=http://localhost:5000/api\n");
174
+ await writeTemplate(
175
+ path.join(context.frontendDir, ".env.local"),
176
+ `NEXT_PUBLIC_API_URL=http://localhost:5000/api\n${context.socketio ? "NEXT_PUBLIC_SOCKET_URL=http://localhost:5000\n" : ""}`,
177
+ );
169
178
  await relaxTypescriptConfig(context, "next");
170
179
  const files = nextTemplates(context, x);
180
+ pruneUnusedFrontendFiles(files, context);
181
+
171
182
  await Promise.all(Object.entries(files).map(([file, content]) => writeTemplate(path.join(src, file), content, context)));
172
183
  }
173
184
 
185
+ function pruneUnusedFrontendFiles(files, context) {
186
+ const x = jsxExt(context);
187
+ const isTs = context.language === "typescript";
188
+
189
+ if (context.state === "none") {
190
+ delete files[`store/auth.${x}`];
191
+ delete files[`redux/store.${isTs ? "ts" : "js"}`];
192
+ delete files[`redux/slices/authSlice.${isTs ? "ts" : "js"}`];
193
+ delete files[`redux/slices/todoSlices.${isTs ? "ts" : "js"}`];
194
+ } else if (context.state === "redux-toolkit") {
195
+ delete files[`store/auth.${x}`];
196
+ } else {
197
+ delete files[`redux/store.${isTs ? "ts" : "js"}`];
198
+ delete files[`redux/slices/authSlice.${isTs ? "ts" : "js"}`];
199
+ delete files[`redux/slices/todoSlices.${isTs ? "ts" : "js"}`];
200
+ }
201
+
202
+ if (!isTs) delete files["lib/types.js"];
203
+ if (context.validation === "none") {
204
+ delete files[`lib/validation.${isTs ? "ts" : "js"}`];
205
+ }
206
+
207
+ if (!context.socketio) {
208
+ delete files[`lib/socket.${isTs ? "ts" : "js"}`];
209
+ delete files[`features/chat/chatService.${isTs ? "ts" : "js"}`];
210
+ delete files[`pages/Chat.${x}`];
211
+ for (const key of Object.keys(files)) {
212
+ if (key.startsWith("app/chat/")) delete files[key];
213
+ }
214
+ }
215
+ }
216
+
174
217
  async function removeNextGeneratedDuplicates(context, x) {
175
218
  const appDir = path.join(context.frontendDir, "src", "app");
176
219
  const generated = ["js", "jsx", "ts", "tsx"].filter((candidate) => candidate !== x);
@@ -241,11 +284,11 @@ async function relaxTypescriptConfig(context, framework) {
241
284
  function commonTemplates(context, x) {
242
285
  const isTs = context.language === "typescript";
243
286
  const typeBlock = isTs ? `export type User = { id: string; name: string; email: string };
244
- export type Task = { _id: string; title: string; description?: string; imageUrl?: string; status: "todo" | "in-progress" | "done" };
287
+ export type Todo = { _id: string; title: string; description?: string; imageUrl?: string; status: "todo" | "in-progress" | "done" };
245
288
  ` : "";
246
- const schema = context.validation === "none" || context.validation === "joi"
289
+ const schema = context.validation === "none"
247
290
  ? `export const authSchema = null;
248
- export const taskSchema = null;
291
+ export const todoSchema = null;
249
292
  `
250
293
  : context.validation === "zod"
251
294
  ? `import { z } from "zod";
@@ -256,7 +299,7 @@ export const authSchema = z.object({
256
299
  password: z.string().min(6)
257
300
  });
258
301
 
259
- export const taskSchema = z.object({
302
+ export const todoSchema = z.object({
260
303
  title: z.string().min(2),
261
304
  description: z.string().optional(),
262
305
  status: z.enum(["todo", "in-progress", "done"]).default("todo")
@@ -270,7 +313,7 @@ export const authSchema = yup.object({
270
313
  password: yup.string().min(6).required()
271
314
  });
272
315
 
273
- export const taskSchema = yup.object({
316
+ export const todoSchema = yup.object({
274
317
  title: yup.string().min(2).required(),
275
318
  description: yup.string(),
276
319
  status: yup.string().oneOf(["todo", "in-progress", "done"]).default("todo")
@@ -295,7 +338,43 @@ export const useAuthStore = create((set) => ({
295
338
  }));
296
339
  `
297
340
  : context.state === "redux-toolkit"
298
- ? `import { configureStore, createSlice } from "@reduxjs/toolkit";
341
+ ? ""
342
+ : `import { createContext, useContext, useState } from "react";
343
+
344
+ const AuthContext = createContext(null);
345
+
346
+ export function AuthProvider({ children }) {
347
+ const [session, setSessionState] = useState({ user: null, token: typeof window !== "undefined" ? localStorage.getItem("stackflow_token") : null });
348
+ const setSession = ({ user, token }) => {
349
+ if (token && typeof window !== "undefined") localStorage.setItem("stackflow_token", token);
350
+ setSessionState({ user, token });
351
+ };
352
+ const logout = () => {
353
+ if (typeof window !== "undefined") localStorage.removeItem("stackflow_token");
354
+ setSessionState({ user: null, token: null });
355
+ };
356
+ return <AuthContext.Provider value={{ ...session, setSession, logout }}>{children}</AuthContext.Provider>;
357
+ }
358
+
359
+ export const useAuthContext = () => useContext(AuthContext);
360
+ `;
361
+
362
+ const reduxExt = isTs ? "ts" : "js";
363
+ const reduxFiles =
364
+ context.state === "redux-toolkit"
365
+ ? {
366
+ [`redux/store.${reduxExt}`]: `import { configureStore } from "@reduxjs/toolkit";
367
+ import authReducer from "./slices/authSlice";
368
+ import todoReducer from "./slices/todoSlices";
369
+
370
+ export const store = configureStore({
371
+ reducer: {
372
+ auth: authReducer,
373
+ todos: todoReducer
374
+ }
375
+ });
376
+ `,
377
+ [`redux/slices/authSlice.${reduxExt}`]: `import { createSlice } from "@reduxjs/toolkit";
299
378
 
300
379
  const authSlice = createSlice({
301
380
  name: "auth",
@@ -315,35 +394,44 @@ const authSlice = createSlice({
315
394
  });
316
395
 
317
396
  export const { setSession, logout } = authSlice.actions;
318
- export const store = configureStore({ reducer: { auth: authSlice.reducer } });
319
- `
320
- : `import { createContext, useContext, useState } from "react";
321
-
322
- const AuthContext = createContext(null);
397
+ export default authSlice.reducer;
398
+ `,
399
+ [`redux/slices/todoSlices.${reduxExt}`]: `import { createSlice } from "@reduxjs/toolkit";
323
400
 
324
- export function AuthProvider({ children }) {
325
- const [session, setSessionState] = useState({ user: null, token: typeof window !== "undefined" ? localStorage.getItem("stackflow_token") : null });
326
- const setSession = ({ user, token }) => {
327
- if (token && typeof window !== "undefined") localStorage.setItem("stackflow_token", token);
328
- setSessionState({ user, token });
329
- };
330
- const logout = () => {
331
- if (typeof window !== "undefined") localStorage.removeItem("stackflow_token");
332
- setSessionState({ user: null, token: null });
333
- };
334
- return <AuthContext.Provider value={{ ...session, setSession, logout }}>{children}</AuthContext.Provider>;
335
- }
401
+ const todoSlices = createSlice({
402
+ name: "todos",
403
+ initialState: { items: [] },
404
+ reducers: {
405
+ setTodos: (state, action) => {
406
+ state.items = action.payload;
407
+ },
408
+ addTodo: (state, action) => {
409
+ state.items.unshift(action.payload);
410
+ },
411
+ updateTodo: (state, action) => {
412
+ const index = state.items.findIndex((todo) => todo._id === action.payload._id);
413
+ if (index !== -1) state.items[index] = action.payload;
414
+ },
415
+ removeTodo: (state, action) => {
416
+ state.items = state.items.filter((todo) => todo._id !== action.payload);
417
+ }
418
+ }
419
+ });
336
420
 
337
- export const useAuthContext = () => useContext(AuthContext);
338
- `;
421
+ export const { setTodos, addTodo, updateTodo, removeTodo } = todoSlices.actions;
422
+ export default todoSlices.reducer;
423
+ `,
424
+ }
425
+ : {};
339
426
 
340
427
  return {
341
- [`lib/types.${isTs ? "ts" : "js"}`]: typeBlock || "export {};\n",
428
+ ...(isTs ? { "lib/types.ts": typeBlock } : {}),
429
+ ...reduxFiles,
430
+ ...chatFrontendFiles(context, x),
342
431
  [`lib/api.${isTs ? "ts" : "js"}`]: `import axios from "axios";
343
432
 
344
433
  export const api = axios.create({
345
- baseURL: ${context.frontend === "next" ? "process.env.NEXT_PUBLIC_API_URL" : "import.meta.env.VITE_API_URL"} || "http://localhost:5000/api",
346
- withCredentials: true
434
+ baseURL: ${context.frontend === "next" ? "process.env.NEXT_PUBLIC_API_URL" : "import.meta.env.VITE_API_URL"} || "http://localhost:5000/api"
347
435
  });
348
436
 
349
437
  api.interceptors.request.use((config) => {
@@ -352,24 +440,26 @@ api.interceptors.request.use((config) => {
352
440
  return config;
353
441
  });
354
442
  `,
355
- [`lib/validation.${isTs ? "ts" : "js"}`]: schema,
356
- [`store/auth.${x}`]: authStore,
357
- [`features/auth/auth.service.${isTs ? "ts" : "js"}`]: `import { api } from "../../lib/api";
443
+ ...(context.validation !== "none" ? { [`lib/validation.${isTs ? "ts" : "js"}`]: schema } : {}),
444
+ ...(context.state !== "none" && context.state !== "redux-toolkit"
445
+ ? { [`store/auth.${x}`]: authStore }
446
+ : {}),
447
+ [`features/auth/authService.${isTs ? "ts" : "js"}`]: `import { api } from "../../lib/api";
358
448
 
359
449
  export const authService = {
360
- register: (payload) => api.post("/auth/register", payload).then((res) => res.data),
361
- login: (payload) => api.post("/auth/login", payload).then((res) => res.data),
362
- me: () => api.get("/auth/me").then((res) => res.data),
450
+ register: (payload) => api.post("/auth/register", payload).then((res) => res.data.data),
451
+ login: (payload) => api.post("/auth/login", payload).then((res) => res.data.data),
452
+ me: () => api.get("/auth/me").then((res) => res.data.data),
363
453
  logout: () => api.post("/auth/logout").then((res) => res.data)
364
454
  };
365
455
  `,
366
- [`features/tasks/task.service.${isTs ? "ts" : "js"}`]: `import { api } from "../../lib/api";
456
+ [`features/todos/todoService.${isTs ? "ts" : "js"}`]: `import { api } from "../../lib/api";
367
457
 
368
- export const taskService = {
369
- list: () => api.get("/tasks").then((res) => res.data.tasks),
370
- create: (payload) => api.post("/tasks", payload, multipartConfig(payload)).then((res) => res.data.task),
371
- update: (id, payload) => api.patch(\`/tasks/\${id}\`, payload, multipartConfig(payload)).then((res) => res.data.task),
372
- remove: (id) => api.delete(\`/tasks/\${id}\`).then((res) => res.data)
458
+ export const todoService = {
459
+ list: () => api.get("/todos").then((res) => res.data.data),
460
+ create: (payload) => api.post("/todos", payload, multipartConfig(payload)).then((res) => res.data.data),
461
+ update: (id, payload) => api.put(\`/todos/\${id}\`, payload, multipartConfig(payload)).then((res) => res.data.data),
462
+ remove: (id) => api.delete(\`/todos/\${id}\`).then((res) => res.data)
373
463
  };
374
464
 
375
465
  function multipartConfig(payload) {
@@ -415,7 +505,7 @@ body { margin: 0; font-family: Inter, ui-sans-serif, system-ui, -apple-system, B
415
505
  [`main.${x}`]: `import React from "react";
416
506
  import ReactDOM from "react-dom/client";
417
507
  ${context.router ? "import { BrowserRouter } from \"react-router-dom\";" : ""}
418
- ${context.state === "redux-toolkit" ? "import { Provider } from \"react-redux\";\nimport { store } from \"./store/auth\";" : ""}
508
+ ${context.state === "redux-toolkit" ? "import { Provider } from \"react-redux\";\nimport { store } from \"./redux/store\";" : ""}
419
509
  ${context.state === "context-api" ? "import { AuthProvider } from \"./store/auth\";" : ""}
420
510
  import { Toaster } from "sonner";
421
511
  import App from "./App";
@@ -439,14 +529,17 @@ import { Dashboard } from "./pages/Dashboard";
439
529
  import { Login } from "./pages/Login";
440
530
  import { Register } from "./pages/Register";
441
531
  import { ProtectedRoute } from "./routes/ProtectedRoute";
532
+ ${context.socketio ? 'import { Chat } from "./pages/Chat";' : ""}
442
533
 
443
534
  export default function App() {
444
535
  return (
445
536
  <Routes>
446
- <Route path="/" element={<Navigate to="/dashboard" replace />} />
537
+ <Route path="/" element={<Navigate to="${context.socketio ? "/chat" : "/dashboard"}" replace />} />
447
538
  <Route path="/login" element={<Login />} />
448
539
  <Route path="/register" element={<Register />} />
449
540
  <Route path="/dashboard" element={<ProtectedRoute><Dashboard /></ProtectedRoute>} />
541
+ ${context.socketio ? `<Route path="/chat" element={<ProtectedRoute><Chat /></ProtectedRoute>} />
542
+ <Route path="/chat/:conversationId" element={<ProtectedRoute><Chat /></ProtectedRoute>} />` : ""}
450
543
  </Routes>
451
544
  );
452
545
  }
@@ -457,13 +550,10 @@ export default function App() {
457
550
  }
458
551
  `}`,
459
552
  [`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
553
 
464
554
  export function ProtectedRoute({ children }) {
465
- const token = ${context.state === "zustand" ? "useAuthStore((state) => state.token)" : context.state === "context-api" ? "useAuthContext()?.token" : context.state === "redux-toolkit" ? "useSelector((state) => state.auth.token)" : "localStorage.getItem(\"stackflow_token\")"};
466
- return token ? children : <Navigate to="/login" replace />;
555
+ const isAuthenticated = Boolean(localStorage.getItem("stackflow_token"));
556
+ return isAuthenticated ? children : <Navigate to="/login" replace />;
467
557
  }
468
558
  `,
469
559
  [`pages/Login.${x}`]: authPage("login", context),
@@ -475,6 +565,7 @@ export function ProtectedRoute({ children }) {
475
565
  function nextTemplates(context, x) {
476
566
  return {
477
567
  ...commonTemplates(context, x),
568
+ ...chatNextPages(context, x),
478
569
  "app/globals.css": `@import "tailwindcss";
479
570
  @custom-variant dark (&:where(.dark, .dark *));
480
571
 
@@ -499,7 +590,7 @@ export default function RootLayout({ children }) {
499
590
  [`app/page.${x}`]: `import { redirect } from "next/navigation";
500
591
 
501
592
  export default function Home() {
502
- redirect("/dashboard");
593
+ redirect("${context.socketio ? "/chat" : "/dashboard"}");
503
594
  }
504
595
  `,
505
596
  [`app/login/page.${x}`]: `"use client";
@@ -521,7 +612,9 @@ function authPage(mode, context, next = false) {
521
612
  const isRegister = mode === "register";
522
613
  const navImport = next ? "import { useRouter } from \"next/navigation\";" : "import { Link, useNavigate } from \"react-router-dom\";";
523
614
  const navHook = next ? "const router = useRouter();" : "const navigate = useNavigate();";
524
- const redirect = next ? "router.push(\"/dashboard\");" : "navigate(\"/dashboard\");";
615
+ const redirect = next
616
+ ? `router.push("${context.socketio ? "/chat" : "/dashboard"}");`
617
+ : `navigate("${context.socketio ? "/chat" : "/dashboard"}");`;
525
618
  const link = next
526
619
  ? `<button className="text-sm text-cyan-700" onClick={() => router.push("${isRegister ? "/login" : "/register"}")}>${isRegister ? "Sign in" : "Create account"}</button>`
527
620
  : `<Link className="text-sm text-cyan-700" to="${isRegister ? "/login" : "/register"}">${isRegister ? "Sign in" : "Create account"}</Link>`;
@@ -530,12 +623,20 @@ ${navImport}
530
623
  import { toast } from "sonner";
531
624
  import { Button } from "${next ? "../../components/ui/button" : "../components/ui/button"}";
532
625
  import { Input } from "${next ? "../../components/ui/input" : "../components/ui/input"}";
533
- import { authService } from "${next ? "../../features/auth/auth.service" : "../features/auth/auth.service"}";
626
+ import { authService } from "${next ? "../../features/auth/authService" : "../features/auth/authService"}";
534
627
  ${context.state === "zustand" ? `import { useAuthStore } from "${next ? "../../store/auth" : "../store/auth"}";` : ""}
628
+ ${context.state === "context-api" ? `import { useAuthContext } from "${next ? "../../store/auth" : "../store/auth"}";` : ""}
629
+ ${context.state === "redux-toolkit" ? `import { useDispatch } from "react-redux";\nimport { setSession as setReduxSession } from "${next ? "../../redux/slices/authSlice" : "../redux/slices/authSlice"}";` : ""}
535
630
 
536
631
  export function ${isRegister ? "Register" : "Login"}() {
537
632
  ${navHook}
538
- const setSession = ${context.state === "zustand" ? "useAuthStore((state) => state.setSession)" : "({ user, token }) => localStorage.setItem(\"stackflow_token\", token)"};
633
+ ${context.state === "redux-toolkit" ? "const dispatch = useDispatch();" : ""}
634
+ const setSession = ${
635
+ context.state === "zustand" ? "useAuthStore((state) => state.setSession)" :
636
+ context.state === "context-api" ? "useAuthContext().setSession" :
637
+ context.state === "redux-toolkit" ? "(data) => dispatch(setReduxSession(data))" :
638
+ "({ user, token }) => localStorage.setItem(\"stackflow_token\", token)"
639
+ };
539
640
  const [loading, setLoading] = useState(false);
540
641
  const [form, setForm] = useState({ name: "", email: "", password: "" });
541
642
 
@@ -583,15 +684,23 @@ import { Edit3, ImagePlus, LogOut, Moon, Plus, Save, Sun, Trash2, X } from "luci
583
684
  import { toast } from "sonner";
584
685
  import { Button } from "${next ? "../../components/ui/button" : "../components/ui/button"}";
585
686
  import { Input } from "${next ? "../../components/ui/input" : "../components/ui/input"}";
586
- import { taskService } from "${next ? "../../features/tasks/task.service" : "../features/tasks/task.service"}";
687
+ import { todoService } from "${next ? "../../features/todos/todoService" : "../features/todos/todoService"}";
587
688
  ${context.state === "zustand" ? `import { useAuthStore } from "${next ? "../../store/auth" : "../store/auth"}";` : ""}
689
+ ${context.state === "context-api" ? `import { useAuthContext } from "${next ? "../../store/auth" : "../store/auth"}";` : ""}
690
+ ${context.state === "redux-toolkit" ? `import { useDispatch } from "react-redux";\nimport { logout as reduxLogout } from "${next ? "../../redux/slices/authSlice" : "../redux/slices/authSlice"}";` : ""}
588
691
 
589
692
  ${componentName} {
590
693
  const ${next ? "router" : "navigate"} = ${next ? "useRouter()" : "useNavigate()"};
591
- const logoutStore = ${context.state === "zustand" ? "useAuthStore((state) => state.logout)" : "() => localStorage.removeItem(\"stackflow_token\")"};
694
+ ${context.state === "redux-toolkit" ? "const dispatch = useDispatch();" : ""}
695
+ const logoutStore = ${
696
+ context.state === "zustand" ? "useAuthStore((state) => state.logout)" :
697
+ context.state === "context-api" ? "useAuthContext().logout" :
698
+ context.state === "redux-toolkit" ? "() => dispatch(reduxLogout())" :
699
+ "() => localStorage.removeItem(\"stackflow_token\")"
700
+ };
592
701
  const [dark, setDark] = useState(() => typeof window !== "undefined" && localStorage.getItem("stackflow_theme") === "dark");
593
702
  const [loading, setLoading] = useState(true);
594
- const [tasks, setTasks] = useState([]);
703
+ const [todos, setTodos] = useState([]);
595
704
  const [form, setForm] = useState({ title: "", description: "", status: "todo", image: null });
596
705
  const [editingId, setEditingId] = useState(null);
597
706
 
@@ -601,30 +710,30 @@ ${componentName} {
601
710
  }, [dark]);
602
711
 
603
712
  useEffect(() => {
604
- taskService.list()
605
- .then(setTasks)
606
- .catch(() => toast.error("Could not load tasks"))
713
+ todoService.list()
714
+ .then(setTodos)
715
+ .catch(() => toast.error("Could not load todos"))
607
716
  .finally(() => setLoading(false));
608
717
  }, []);
609
718
 
610
- async function createTask(event) {
719
+ async function createTodo(event) {
611
720
  event.preventDefault();
612
721
  if (!form.title.trim()) return;
613
722
  const payload = toPayload(form);
614
723
  const saved = editingId
615
- ? await taskService.update(editingId, payload)
616
- : await taskService.create(payload);
617
- setTasks(editingId ? tasks.map((task) => task._id === editingId ? saved : task) : [saved, ...tasks]);
724
+ ? await todoService.update(editingId, payload)
725
+ : await todoService.create(payload);
726
+ setTodos(editingId ? todos.map((todo) => todo._id === editingId ? saved : todo) : [saved, ...todos]);
618
727
  resetForm();
619
- toast.success(editingId ? "Task updated" : "Task created");
728
+ toast.success(editingId ? "Todo updated" : "Todo created");
620
729
  }
621
730
 
622
- function startEdit(task) {
623
- setEditingId(task._id);
731
+ function startEdit(todo) {
732
+ setEditingId(todo._id);
624
733
  setForm({
625
- title: task.title || "",
626
- description: task.description || "",
627
- status: task.status || "todo",
734
+ title: todo.title || "",
735
+ description: todo.description || "",
736
+ status: todo.status || "todo",
628
737
  image: null
629
738
  });
630
739
  }
@@ -634,10 +743,10 @@ ${componentName} {
634
743
  setForm({ title: "", description: "", status: "todo", image: null });
635
744
  }
636
745
 
637
- async function removeTask(id) {
638
- await taskService.remove(id);
639
- setTasks(tasks.filter((task) => task._id !== id));
640
- toast.success("Task deleted");
746
+ async function removeTodo(id) {
747
+ await todoService.remove(id);
748
+ setTodos(todos.filter((todo) => todo._id !== id));
749
+ toast.success("Todo deleted");
641
750
  }
642
751
 
643
752
  function logout() {
@@ -675,37 +784,37 @@ ${componentName} {
675
784
  <section className="mx-auto grid max-w-6xl gap-6 px-4 py-8 md:grid-cols-[1fr_320px]">
676
785
  <div className="rounded-lg border border-slate-200 bg-white p-5 dark:border-slate-800 dark:bg-slate-900">
677
786
  <div className="mb-4 flex items-center justify-between">
678
- <h2 className="text-lg font-semibold">Tasks</h2>
679
- <span className="text-sm text-slate-500">{tasks.length} total</span>
787
+ <h2 className="text-lg font-semibold">Todos</h2>
788
+ <span className="text-sm text-slate-500">{todos.length} total</span>
680
789
  </div>
681
- {loading ? <p className="text-sm text-slate-500">Loading tasks...</p> : (
790
+ {loading ? <p className="text-sm text-slate-500">Loading todos...</p> : (
682
791
  <div className="space-y-3">
683
- {tasks.map((task) => (
684
- <div key={task._id} className="grid gap-3 rounded-md border border-slate-200 p-3 dark:border-slate-800 sm:grid-cols-[96px_1fr_auto]">
792
+ {todos.map((todo) => (
793
+ <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
794
  <div className="h-24 overflow-hidden rounded-md bg-slate-100 dark:bg-slate-800">
686
- {task.imageUrl ? <img className="h-full w-full object-cover" src={\`http://localhost:5000\${task.imageUrl}\`} alt={task.title} /> : <div className="grid h-full place-items-center text-slate-400"><ImagePlus size={20} /></div>}
795
+ {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
796
  </div>
688
797
  <div>
689
- <p className="font-medium">{task.title}</p>
690
- <p className="mt-1 text-sm text-slate-500">{task.description || "No description"}</p>
691
- <p className="mt-2 text-xs uppercase tracking-wide text-slate-500">{task.status}</p>
798
+ <p className="font-medium">{todo.title}</p>
799
+ <p className="mt-1 text-sm text-slate-500">{todo.description || "No description"}</p>
800
+ <p className="mt-2 text-xs uppercase tracking-wide text-slate-500">{todo.status}</p>
692
801
  </div>
693
802
  <div className="flex items-start gap-2">
694
- <Button variant="ghost" onClick={() => startEdit(task)} aria-label="Edit task"><Edit3 size={16} /></Button>
695
- <Button variant="ghost" onClick={() => removeTask(task._id)} aria-label="Delete task"><Trash2 size={16} /></Button>
803
+ <Button variant="ghost" onClick={() => startEdit(todo)} aria-label="Edit todo"><Edit3 size={16} /></Button>
804
+ <Button variant="ghost" onClick={() => removeTodo(todo._id)} aria-label="Delete todo"><Trash2 size={16} /></Button>
696
805
  </div>
697
806
  </div>
698
807
  ))}
699
- {!tasks.length && <p className="text-sm text-slate-500">No tasks yet.</p>}
808
+ {!todos.length && <p className="text-sm text-slate-500">No todos yet.</p>}
700
809
  </div>
701
810
  )}
702
811
  </div>
703
- <form onSubmit={createTask} className="h-fit space-y-3 rounded-lg border border-slate-200 bg-white p-5 dark:border-slate-800 dark:bg-slate-900">
812
+ <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
813
  <div className="flex items-center justify-between">
705
- <h2 className="text-lg font-semibold">{editingId ? "Edit task" : "Create task"}</h2>
814
+ <h2 className="text-lg font-semibold">{editingId ? "Edit todo" : "Create todo"}</h2>
706
815
  {editingId && <Button type="button" variant="ghost" onClick={resetForm} aria-label="Cancel edit"><X size={18} /></Button>}
707
816
  </div>
708
- <Input value={form.title} onChange={(event) => setForm({ ...form, title: event.target.value })} placeholder="Task title" />
817
+ <Input value={form.title} onChange={(event) => setForm({ ...form, title: event.target.value })} placeholder="Todo title" />
709
818
  <Input value={form.description} onChange={(event) => setForm({ ...form, description: event.target.value })} placeholder="Description" />
710
819
  <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
820
  <option value="todo">Todo</option>
@@ -713,7 +822,7 @@ ${componentName} {
713
822
  <option value="done">Done</option>
714
823
  </select>
715
824
  ${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 task" : "Add task"}>{editingId ? <Save size={18} /> : <Plus size={18} />}<span className="ml-2">{editingId ? "Save changes" : "Add task"}</span></Button>
825
+ <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
826
  </form>
718
827
  </section>
719
828
  </main>