create-tigra 2.6.5 → 2.6.8

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.
Files changed (39) hide show
  1. package/package.json +1 -1
  2. package/template/_claude/rules/client/01-project-structure.md +2 -5
  3. package/template/_claude/rules/client/04-design-system.md +48 -43
  4. package/template/_claude/rules/client/core.md +2 -2
  5. package/template/client/src/app/globals.css +12 -12
  6. package/template/client/src/app/layout.tsx +1 -1
  7. package/template/client/src/app/page.tsx +5 -5
  8. package/template/client/src/app/providers.tsx +1 -1
  9. package/template/client/src/components/common/ThemeToggle.tsx +59 -0
  10. package/template/client/src/features/admin/hooks/useAdminSessions.ts +68 -0
  11. package/template/client/src/features/admin/hooks/useAdminStats.ts +27 -0
  12. package/template/client/src/features/admin/hooks/useAdminUsers.ts +132 -0
  13. package/template/client/src/features/admin/services/admin.service.ts +94 -0
  14. package/template/client/src/features/admin/types/admin.types.ts +65 -0
  15. package/template/client/src/features/auth/components/AuthInitializer.tsx +18 -1
  16. package/template/client/src/lib/api/axios.config.ts +20 -1
  17. package/template/client/src/lib/constants/api-endpoints.ts +9 -0
  18. package/template/client/src/lib/constants/app.constants.ts +3 -1
  19. package/template/client/src/lib/constants/routes.ts +6 -0
  20. package/template/client/src/lib/env.ts +35 -0
  21. package/template/client/src/styles/themes/default.css +92 -0
  22. package/template/server/package.json +1 -0
  23. package/template/server/postman/collection.json +168 -50
  24. package/template/server/prisma/schema.prisma +2 -0
  25. package/template/server/src/jobs/cleanup-deleted-accounts.job.ts +14 -4
  26. package/template/server/src/libs/prisma.ts +13 -0
  27. package/template/server/src/modules/admin/admin.controller.ts +130 -1
  28. package/template/server/src/modules/admin/admin.repo.ts +289 -0
  29. package/template/server/src/modules/admin/admin.routes.ts +113 -7
  30. package/template/server/src/modules/admin/admin.schemas.ts +49 -0
  31. package/template/server/src/modules/admin/admin.service.ts +154 -0
  32. package/template/server/src/modules/auth/auth.repo.ts +5 -18
  33. package/template/server/src/modules/auth/auth.service.ts +20 -28
  34. package/template/server/src/modules/auth/session.repo.ts +10 -5
  35. package/template/client/src/components/common/ThemeSwitcher.tsx +0 -112
  36. package/template/client/src/styles/themes/electric-indigo.css +0 -90
  37. package/template/client/src/styles/themes/ocean-teal.css +0 -90
  38. package/template/client/src/styles/themes/rose-pink.css +0 -90
  39. package/template/client/src/styles/themes/warm-orange.css +0 -90
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-tigra",
3
- "version": "2.6.5",
3
+ "version": "2.6.8",
4
4
  "type": "module",
5
5
  "description": "Create a production-ready full-stack app with Next.js 16 + Fastify 5 + Prisma + Redis",
6
6
  "bin": {
@@ -33,11 +33,8 @@ src/
33
33
  │ ├── types/ # <domain>.types.ts
34
34
  │ └── actions/ # <domain>.actions.ts (Server Actions)
35
35
  ├── styles/
36
- │ ├── themes/ # Color theme presets (switch in globals.css import)
37
- │ │ ├── warm-orange.css # Default earthy, warm
38
- │ │ ├── electric-indigo.css
39
- │ │ ├── ocean-teal.css
40
- │ │ └── rose-pink.css
36
+ │ ├── themes/ # Color theme (light/dark mode values)
37
+ │ │ └── default.css # Claude-inspired warm palette (HEX)
41
38
  │ └── fonts/ # Font presets (switch in globals.css import)
42
39
  │ └── inter-jetbrains.css # Default — Inter + JetBrains Mono
43
40
  ├── hooks/ # Global hooks (useDebounce, useLocalStorage, useMediaQuery)
@@ -12,85 +12,89 @@ Clean, airy, "expensive" look inspired by Linear, Vercel, Stripe, Arc. Every vis
12
12
 
13
13
  ## CSS Architecture (Source of Truth)
14
14
 
15
- This project uses **Tailwind CSS v4** with **OKLCH color space** and the `@theme inline` directive (not the legacy `tailwind.config.ts`).
15
+ This project uses **Tailwind CSS v4** with **HEX colors** and the `@theme inline` directive (not the legacy `tailwind.config.ts`).
16
16
 
17
17
  **Key differences from Tailwind v3:**
18
18
  - No `tailwind.config.ts` — all config is CSS-based via `@theme inline`
19
- - Colors use **OKLCH** (perceptually uniform) not HSL
19
+ - Colors use **HEX** values (e.g., `#c15f3c`), with `rgba()` for alpha values
20
20
  - `@custom-variant dark` replaces `darkMode: 'class'`
21
- - No `@layer base { :root { } }` — variables defined on `:root` via theme preset files
21
+ - No `@layer base { :root { } }` — variables defined on `:root` via theme file
22
22
 
23
23
  ---
24
24
 
25
- ## Theme Preset System (Color Management)
25
+ ## Theme System (Color Management)
26
26
 
27
- **ALL color variables live in theme preset files, NOT in `globals.css`.** This is the single source of truth for the entire app's color palette.
27
+ **ALL color variables live in `src/styles/themes/default.css`, NOT in `globals.css` or components.** This is the single source of truth for the entire app's color palette.
28
28
 
29
29
  ### How It Works
30
30
 
31
31
  ```
32
32
  src/
33
- ├── app/globals.css ← imports ONE theme preset (switch here)
33
+ ├── app/globals.css ← imports the theme + defines smooth transitions
34
34
  └── styles/themes/
35
- ├── warm-orange.css Earthy, warm (default)
36
- ├── electric-indigo.css ← Modern, bold, tech-forward
37
- ├── ocean-teal.css ← Calm, professional
38
- └── rose-pink.css ← Elegant, creative
35
+ └── default.css Claude-inspired warm palette (HEX)
39
36
  ```
40
37
 
41
- `globals.css` imports the active theme via a single line:
38
+ `globals.css` imports the theme:
42
39
 
43
40
  ```css
44
- @import "../styles/themes/warm-orange.css";
41
+ @import "../styles/themes/default.css";
45
42
  ```
46
43
 
47
- **To switch the entire palette**: change that ONE import line. That's it. Every color in the app updates instantly — light mode, dark mode, charts, sidebar, everything.
44
+ ### Theme File Structure
48
45
 
49
- ### Theme Preset Structure
50
-
51
- Each preset file defines ALL semantic color variables for both `:root` (light) and `.dark` (dark mode):
46
+ `default.css` defines ALL semantic color variables for both `:root` (light) and `.dark` (dark mode) using HEX:
52
47
 
53
48
  ```css
54
49
  :root {
55
50
  --radius: 0.625rem;
56
- --background: oklch(...);
57
- --foreground: oklch(...);
58
- --primary: oklch(...);
59
- --primary-foreground: oklch(...);
51
+ --background: #f4f3ee;
52
+ --foreground: #1a170f;
53
+ --primary: #c15f3c;
54
+ --primary-foreground: #ffffff;
60
55
  /* ... all ~35 semantic tokens */
61
56
  }
62
57
 
63
58
  .dark {
64
- --background: oklch(...);
65
- --foreground: oklch(...);
66
- --primary: oklch(...);
59
+ --background: #15130d;
60
+ --foreground: #e9e8e3;
61
+ --primary: #d6724f;
67
62
  /* ... dark mode overrides for all tokens */
68
63
  }
69
64
  ```
70
65
 
71
- ### Creating a Custom Theme
66
+ ### Customizing Colors
67
+
68
+ To change the brand palette, edit the HEX values in `default.css`. That's it — every color in the app updates instantly for both light and dark modes.
69
+
70
+ ### Smooth Theme Transitions
71
+
72
+ `globals.css` includes a global transition rule in `@layer base` that smoothly animates color changes when toggling light/dark mode:
72
73
 
73
- 1. Copy any existing preset file (e.g., `warm-orange.css`)
74
- 2. Rename it (e.g., `my-brand.css`)
75
- 3. Edit the OKLCH values to match your brand palette
76
- 4. Update the import in `globals.css`: `@import "../styles/themes/my-brand.css";`
74
+ ```css
75
+ *, *::before, *::after {
76
+ transition-property: background-color, color, border-color, box-shadow;
77
+ transition-duration: 200ms;
78
+ transition-timing-function: ease-out;
79
+ }
80
+ ```
77
81
 
78
- ### Available Presets
82
+ ### Light/Dark Mode Toggle
79
83
 
80
- | Preset | Accent | Vibe | Inspired by |
81
- |--------|--------|------|-------------|
82
- | `warm-orange.css` | Terracotta orange | Earthy, warm, approachable | Claude |
83
- | `electric-indigo.css` | Deep indigo-violet | Modern, bold, tech-forward | Linear, Figma |
84
- | `ocean-teal.css` | Deep teal-cyan | Calm, professional, trustworthy | Stripe, Vercel |
85
- | `rose-pink.css` | Soft rose-magenta | Elegant, creative, premium | Dribbble, Notion |
84
+ - Managed by `next-themes` with `attribute="class"` and `defaultTheme="light"`
85
+ - The `ThemeToggle` component (`components/common/ThemeToggle.tsx`) provides the UI
86
+ - The Header component also includes a sun/moon toggle button
86
87
 
87
88
  ### CRITICAL RULES — Color Management
88
89
 
89
- 1. **NEVER add or modify color variables directly in `globals.css`.** All `:root` and `.dark` color variables belong in the active theme preset file only.
90
- 2. **NEVER hardcode OKLCH/hex/rgb values in components.** Always use semantic tokens (`bg-primary`, `text-foreground`).
91
- 3. **To change the brand palette**: switch the import in `globals.css` or edit the active preset file. Never scatter color values across multiple files.
92
- 4. **New semantic tokens**: If you need a new color token (rare), add it to ALL preset files to keep them in sync.
93
- 5. **The `@theme inline` block in `globals.css` maps CSS vars to Tailwind** it does NOT define colors. Colors come from the preset.
90
+ 1. **NEVER add or modify color variables in `globals.css`.** All `:root` and `.dark` color variables belong in `default.css` only.
91
+ 2. **NEVER hardcode hex/rgb values in components.** Always use semantic tokens (`bg-primary`, `text-foreground`).
92
+ 3. **NEVER use OKLCH color values.** All colors must be HEX (e.g., `#c15f3c`). Use `rgba()` only when alpha transparency is needed.
93
+ 4. **NEVER rename CSS variables.** The variable names (`--primary`, `--background`, `--muted`, etc.) are locked for consistency. Only edit their HEX values.
94
+ 5. **NEVER modify the smooth transition rules in `globals.css`.** The `transition-property`, `transition-duration`, and `transition-timing-function` on `*` are part of the theme system and must not be changed or removed.
95
+ 6. **NEVER modify the `@theme inline` block in `globals.css`.** It maps CSS vars to Tailwind — it does NOT define colors. Colors come from `default.css`.
96
+ 7. **To change the brand palette**: edit the HEX values in `default.css`. Never scatter color values across multiple files.
97
+ 8. **New semantic tokens**: If you need a new token (rare), add it to both `:root` and `.dark` in `default.css`.
94
98
 
95
99
  ---
96
100
 
@@ -248,7 +252,7 @@ Each preset maps raw font variables (set by `next/font/google` in `layout.tsx`)
248
252
  - Hovered/elevated: `shadow-md` to `shadow-lg`
249
253
  - Modals/popovers: `shadow-xl`
250
254
  - **Glassmorphism**: Only on sticky headers, floating toolbars, modal backdrops. Never on content cards.
251
- `backdrop-filter: blur(12px) saturate(1.5); background: oklch(from var(--background) l c h / 0.8);`
255
+ Use `backdrop-blur-md` + `bg-background/80` in Tailwind.
252
256
  - **No pure black/white**: Use `--background` and `--foreground` tokens (already off-pure).
253
257
 
254
258
  ---
@@ -256,7 +260,7 @@ Each preset maps raw font variables (set by `next/font/google` in `layout.tsx`)
256
260
  ## Typography
257
261
 
258
262
  - **Font**: Defined by the active font preset (default: Inter for sans/heading, JetBrains Mono for mono). See "Font Preset System" above for how to switch.
259
- - **Headings**: `text-wrap: balance`, `leading-tight`. Mobile-first responsive sizes:
263
+ - **Headings**: Use `font-heading`. `text-wrap: balance`, `leading-tight`. Mobile-first responsive sizes:
260
264
  - H1: `text-2xl md:text-3xl lg:text-4xl`
261
265
  - H2: `text-xl md:text-2xl`
262
266
  - H3: `text-lg md:text-xl`
@@ -374,7 +378,8 @@ Link: transition-colors duration-150 active:opacity-70 md:hover:text-primary
374
378
 
375
379
  ## Dark Mode
376
380
 
377
- - Use `next-themes` with `attribute="class"`, `defaultTheme="system"`.
381
+ - Use `next-themes` with `attribute="class"`, `defaultTheme="light"`.
382
+ - Smooth transitions handled by the global CSS transition rules in `globals.css` — do NOT add `disableTransitionOnChange` to `ThemeProvider`.
378
383
  - Reduce shadow visibility in dark mode (use subtle light borders instead).
379
384
  - Consider `brightness-90` on images in dark mode.
380
385
  - Add `suppressHydrationWarning` to `<html>` tag.
@@ -9,7 +9,7 @@
9
9
  | Creating files, folders, feature modules | `01-project-structure.md` |
10
10
  | Building components, writing types/interfaces | `02-components-and-types.md` |
11
11
  | Fetching data, managing state, calling APIs, forms | `03-data-and-state.md` |
12
- | Choosing colors, styling, typography, spacing, motion, **theme presets**, **font presets** | `04-design-system.md` |
12
+ | Choosing colors, styling, typography, spacing, motion, **theme colors**, **font presets** | `04-design-system.md` |
13
13
  | Auth tokens, env vars, security headers | `05-security.md` |
14
14
  | UX psychology, cognitive load, a11y, performance | `06-ux-checklist.md` |
15
15
 
@@ -36,7 +36,7 @@ State: Server data (SSR) → Server Components
36
36
  1. **Mobile-first**: All Tailwind classes start at mobile. Desktop is the enhancement (`md:`, `lg:`). Touch targets min 44x44px. No functionality behind hover-only states.
37
37
  2. **Server Components by default.** Only add `'use client'` when you need hooks, state, or event handlers.
38
38
  3. **Component limits**: Max 250 lines, max 5 props, max 3 JSX nesting levels.
39
- 4. **No hardcoded colors**: Use Tailwind semantic tokens (`bg-primary`, `text-foreground`). Never hex/rgb. **All color variables live in theme preset files (`src/styles/themes/*.css`), NOT in `globals.css` or components.** To change the palette, switch the import in `globals.css` or edit the active preset. Read `04-design-system.md` → "Theme Preset System" for details.
39
+ 4. **No hardcoded colors**: Use Tailwind semantic tokens (`bg-primary`, `text-foreground`). Never hardcode hex/rgb in components. **All color variables live in `src/styles/themes/default.css` using HEX values, NOT in `globals.css` or components.** Only edit the HEX values in `default.css` to customize the palette — never rename variables, change the file structure, or move color definitions elsewhere. The smooth transition system in `globals.css` and the variable naming are locked. Read `04-design-system.md` → "Theme System" for details.
40
40
  5. **No hardcoded fonts**: Use Tailwind font classes (`font-sans`, `font-heading`, `font-mono`). Never hardcode `font-family` in components. **Font families are defined in font preset files (`src/styles/fonts/*.css`).** To change fonts, update the `next/font/google` imports in `layout.tsx` and the font preset file. Read `04-design-system.md` → "Font Preset System" for details.
41
41
  6. **No inline styles**: Tailwind only. Use `cn()` for conditional classes.
42
42
  7. **Import order**: React/Next → third-party → UI → local → hooks → services → types → utils.
@@ -4,21 +4,16 @@
4
4
 
5
5
  /*
6
6
  * ============================================================
7
- * THEME PRESETS All themes loaded, switched via data-theme
7
+ * THEME — Single theme with light/dark mode
8
8
  * ============================================================
9
- * warm-orange.css is the default (uses :root / .dark selectors).
10
- * Other presets are scoped to [data-theme="<name>"] and activated
11
- * by the ThemeSwitcher component setting data-theme on <html>.
9
+ * Colors are defined in default.css using :root (light) and
10
+ * .dark (dark mode) selectors. Light/dark toggle is handled
11
+ * by next-themes setting the "dark" class on <html>.
12
12
  *
13
- * To create a custom theme: copy any preset, scope it under
14
- * [data-theme="your-name"] / .dark[data-theme="your-name"],
15
- * import it below, and add it to ThemeSwitcher's PALETTES array.
13
+ * To customize colors: edit src/styles/themes/default.css
16
14
  * ============================================================
17
15
  */
18
- @import "../styles/themes/warm-orange.css";
19
- @import "../styles/themes/electric-indigo.css";
20
- @import "../styles/themes/ocean-teal.css";
21
- @import "../styles/themes/rose-pink.css";
16
+ @import "../styles/themes/default.css";
22
17
 
23
18
  /*
24
19
  * ============================================================
@@ -85,8 +80,13 @@
85
80
  }
86
81
 
87
82
  @layer base {
88
- * {
83
+ *,
84
+ *::before,
85
+ *::after {
89
86
  @apply border-border outline-ring/50;
87
+ transition-property: background-color, color, border-color, box-shadow;
88
+ transition-duration: 200ms;
89
+ transition-timing-function: ease-out;
90
90
  }
91
91
  body {
92
92
  @apply bg-background text-foreground;
@@ -27,7 +27,7 @@ export default function RootLayout({
27
27
  children: React.ReactNode;
28
28
  }>): React.ReactElement {
29
29
  return (
30
- <html lang="en" className="dark" suppressHydrationWarning>
30
+ <html lang="en" suppressHydrationWarning>
31
31
  <body className={`${inter.variable} ${jetbrainsMono.variable} font-sans antialiased`}>
32
32
  <Providers>{children}</Providers>
33
33
  </body>
@@ -3,7 +3,7 @@ import type { Metadata } from 'next';
3
3
 
4
4
  import Image from 'next/image';
5
5
 
6
- import { ThemeSwitcher } from '@/components/common/ThemeSwitcher';
6
+ import { ThemeToggle } from '@/components/common/ThemeToggle';
7
7
 
8
8
  import { APP_NAME } from '@/lib/constants/app.constants';
9
9
 
@@ -46,12 +46,12 @@ export default function WelcomePage(): React.ReactElement {
46
46
  <span className="font-semibold text-foreground">create-tigra</span>
47
47
  </p>
48
48
 
49
- {/* Theme Palette Switcher */}
50
- <div className="mt-8 pb-6">
51
- <ThemeSwitcher />
49
+ {/* Light/Dark mode toggle */}
50
+ <div className="mt-8">
51
+ <ThemeToggle />
52
52
  </div>
53
53
 
54
- <div className="mt-4 flex flex-col gap-3 sm:flex-row sm:gap-4">
54
+ <div className="mt-6 flex flex-col gap-3 sm:flex-row sm:gap-4">
55
55
  <a
56
56
  href="https://github.com/BehzodKarimov/create-tigra"
57
57
  target="_blank"
@@ -29,7 +29,7 @@ export function Providers({ children }: { children: React.ReactNode }): React.Re
29
29
  return (
30
30
  <ReduxProvider store={store}>
31
31
  <QueryClientProvider client={queryClient}>
32
- <ThemeProvider attribute="class" defaultTheme="dark" disableTransitionOnChange>
32
+ <ThemeProvider attribute="class" defaultTheme="light">
33
33
  <AuthInitializer>
34
34
  {children}
35
35
  </AuthInitializer>
@@ -0,0 +1,59 @@
1
+ 'use client';
2
+
3
+ import type React from 'react';
4
+ import { useCallback } from 'react';
5
+
6
+ import { useTheme } from 'next-themes';
7
+ import { Sun, Moon } from 'lucide-react';
8
+
9
+ import { cn } from '@/lib/utils';
10
+
11
+ export function ThemeToggle(): React.ReactElement {
12
+ const { theme, setTheme } = useTheme();
13
+
14
+ const selectLight = useCallback((): void => {
15
+ setTheme('light');
16
+ }, [setTheme]);
17
+
18
+ const selectDark = useCallback((): void => {
19
+ setTheme('dark');
20
+ }, [setTheme]);
21
+
22
+ return (
23
+ <div className="flex flex-col items-center gap-3">
24
+ <p className="text-xs font-medium tracking-wider text-muted-foreground uppercase">
25
+ Appearance
26
+ </p>
27
+ <div className="flex gap-2 rounded-xl border border-border/50 bg-muted/50 p-1.5 transition-none">
28
+ <button
29
+ type="button"
30
+ onClick={selectLight}
31
+ aria-label="Switch to light mode"
32
+ className={cn(
33
+ 'flex min-h-11 items-center gap-2 rounded-lg px-4 py-2 text-sm font-medium transition-transform duration-200 active:scale-[0.97]',
34
+ theme !== 'dark'
35
+ ? 'bg-background text-foreground shadow-sm'
36
+ : 'text-muted-foreground md:hover:text-foreground',
37
+ )}
38
+ >
39
+ <Sun className="h-4 w-4" />
40
+ Light
41
+ </button>
42
+ <button
43
+ type="button"
44
+ onClick={selectDark}
45
+ aria-label="Switch to dark mode"
46
+ className={cn(
47
+ 'flex min-h-11 items-center gap-2 rounded-lg px-4 py-2 text-sm font-medium transition-transform duration-200 active:scale-[0.97]',
48
+ theme === 'dark'
49
+ ? 'bg-background text-foreground shadow-sm'
50
+ : 'text-muted-foreground md:hover:text-foreground',
51
+ )}
52
+ >
53
+ <Moon className="h-4 w-4" />
54
+ Dark
55
+ </button>
56
+ </div>
57
+ </div>
58
+ );
59
+ }
@@ -0,0 +1,68 @@
1
+ 'use client';
2
+
3
+ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
4
+ import { toast } from 'sonner';
5
+
6
+ import { getErrorMessage } from '@/lib/utils/error';
7
+ import { adminService } from '../services/admin.service';
8
+ import { adminKeys } from './useAdminUsers';
9
+
10
+ import type { IAdminSession, IGetSessionsParams } from '../types/admin.types';
11
+
12
+ // ─── Session List ───────────────────────────────────────────────────────────
13
+
14
+ interface UseAdminSessionsReturn {
15
+ sessions: IAdminSession[];
16
+ pagination: {
17
+ page: number;
18
+ limit: number;
19
+ totalItems: number;
20
+ totalPages: number;
21
+ hasNextPage: boolean;
22
+ hasPreviousPage: boolean;
23
+ } | undefined;
24
+ isLoading: boolean;
25
+ error: Error | null;
26
+ }
27
+
28
+ export const useAdminSessions = (params: IGetSessionsParams = {}): UseAdminSessionsReturn => {
29
+ const { data, isLoading, error } = useQuery({
30
+ queryKey: adminKeys.sessionList(params),
31
+ queryFn: () => adminService.getSessions(params),
32
+ });
33
+
34
+ return {
35
+ sessions: data?.items ?? [],
36
+ pagination: data?.pagination,
37
+ isLoading,
38
+ error,
39
+ };
40
+ };
41
+
42
+ // ─── Force Expire Session ───────────────────────────────────────────────────
43
+
44
+ interface UseForceExpireSessionReturn {
45
+ expireSession: (sessionId: string) => void;
46
+ isExpiring: boolean;
47
+ }
48
+
49
+ export const useForceExpireSession = (): UseForceExpireSessionReturn => {
50
+ const queryClient = useQueryClient();
51
+
52
+ const mutation = useMutation({
53
+ mutationFn: (sessionId: string) => adminService.deleteSession(sessionId),
54
+ onSuccess: () => {
55
+ toast.success('Session expired successfully');
56
+ queryClient.invalidateQueries({ queryKey: adminKeys.sessions() });
57
+ queryClient.invalidateQueries({ queryKey: adminKeys.stats() });
58
+ },
59
+ onError: (error) => {
60
+ toast.error(getErrorMessage(error));
61
+ },
62
+ });
63
+
64
+ return {
65
+ expireSession: mutation.mutate,
66
+ isExpiring: mutation.isPending,
67
+ };
68
+ };
@@ -0,0 +1,27 @@
1
+ 'use client';
2
+
3
+ import { useQuery } from '@tanstack/react-query';
4
+
5
+ import { adminService } from '../services/admin.service';
6
+ import { adminKeys } from './useAdminUsers';
7
+
8
+ import type { IDashboardStats } from '../types/admin.types';
9
+
10
+ interface UseAdminStatsReturn {
11
+ stats: IDashboardStats | undefined;
12
+ isLoading: boolean;
13
+ error: Error | null;
14
+ }
15
+
16
+ export const useAdminStats = (): UseAdminStatsReturn => {
17
+ const { data, isLoading, error } = useQuery({
18
+ queryKey: adminKeys.stats(),
19
+ queryFn: () => adminService.getDashboardStats(),
20
+ });
21
+
22
+ return {
23
+ stats: data,
24
+ isLoading,
25
+ error,
26
+ };
27
+ };
@@ -0,0 +1,132 @@
1
+ 'use client';
2
+
3
+ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
4
+ import { toast } from 'sonner';
5
+
6
+ import { getErrorMessage } from '@/lib/utils/error';
7
+ import { adminService } from '../services/admin.service';
8
+
9
+ import type { IAdminUser, IAdminUserDetail, IGetUsersParams, IGetSessionsParams } from '../types/admin.types';
10
+
11
+ // ─── Query Key Factory ──────────────────────────────────────────────────────
12
+
13
+ export const adminKeys = {
14
+ all: ['admin'] as const,
15
+ stats: () => [...adminKeys.all, 'stats'] as const,
16
+ users: () => [...adminKeys.all, 'users'] as const,
17
+ userList: (params: IGetUsersParams) => [...adminKeys.users(), 'list', params] as const,
18
+ userDetail: (userId: string) => [...adminKeys.users(), 'detail', userId] as const,
19
+ sessions: () => [...adminKeys.all, 'sessions'] as const,
20
+ sessionList: (params: IGetSessionsParams) => [...adminKeys.sessions(), 'list', params] as const,
21
+ };
22
+
23
+ // ─── User List ──────────────────────────────────────────────────────────────
24
+
25
+ interface UseAdminUsersReturn {
26
+ users: IAdminUser[];
27
+ pagination: {
28
+ page: number;
29
+ limit: number;
30
+ totalItems: number;
31
+ totalPages: number;
32
+ hasNextPage: boolean;
33
+ hasPreviousPage: boolean;
34
+ } | undefined;
35
+ isLoading: boolean;
36
+ error: Error | null;
37
+ }
38
+
39
+ export const useAdminUsers = (params: IGetUsersParams = {}): UseAdminUsersReturn => {
40
+ const { data, isLoading, error } = useQuery({
41
+ queryKey: adminKeys.userList(params),
42
+ queryFn: () => adminService.getUsers(params),
43
+ });
44
+
45
+ return {
46
+ users: data?.items ?? [],
47
+ pagination: data?.pagination,
48
+ isLoading,
49
+ error,
50
+ };
51
+ };
52
+
53
+ // ─── User Detail ────────────────────────────────────────────────────────────
54
+
55
+ interface UseAdminUserDetailReturn {
56
+ user: IAdminUserDetail | undefined;
57
+ isLoading: boolean;
58
+ error: Error | null;
59
+ }
60
+
61
+ export const useAdminUserDetail = (userId: string): UseAdminUserDetailReturn => {
62
+ const { data, isLoading, error } = useQuery({
63
+ queryKey: adminKeys.userDetail(userId),
64
+ queryFn: () => adminService.getUserDetail(userId),
65
+ enabled: !!userId,
66
+ });
67
+
68
+ return {
69
+ user: data,
70
+ isLoading,
71
+ error,
72
+ };
73
+ };
74
+
75
+ // ─── Update User Status ─────────────────────────────────────────────────────
76
+
77
+ interface UseUpdateUserStatusReturn {
78
+ updateStatus: (params: { userId: string; isActive: boolean }) => void;
79
+ isUpdating: boolean;
80
+ }
81
+
82
+ export const useUpdateUserStatus = (): UseUpdateUserStatusReturn => {
83
+ const queryClient = useQueryClient();
84
+
85
+ const mutation = useMutation({
86
+ mutationFn: ({ userId, isActive }: { userId: string; isActive: boolean }) =>
87
+ adminService.updateUserStatus(userId, { isActive }),
88
+ onSuccess: (_data, variables) => {
89
+ const action = variables.isActive ? 'activated' : 'deactivated';
90
+ toast.success(`User ${action} successfully`);
91
+ queryClient.invalidateQueries({ queryKey: adminKeys.users() });
92
+ queryClient.invalidateQueries({ queryKey: adminKeys.stats() });
93
+ },
94
+ onError: (error) => {
95
+ toast.error(getErrorMessage(error));
96
+ },
97
+ });
98
+
99
+ return {
100
+ updateStatus: mutation.mutate,
101
+ isUpdating: mutation.isPending,
102
+ };
103
+ };
104
+
105
+ // ─── Update User Role ───────────────────────────────────────────────────────
106
+
107
+ interface UseUpdateUserRoleReturn {
108
+ updateRole: (params: { userId: string; role: 'USER' | 'ADMIN' }) => void;
109
+ isUpdating: boolean;
110
+ }
111
+
112
+ export const useUpdateUserRole = (): UseUpdateUserRoleReturn => {
113
+ const queryClient = useQueryClient();
114
+
115
+ const mutation = useMutation({
116
+ mutationFn: ({ userId, role }: { userId: string; role: 'USER' | 'ADMIN' }) =>
117
+ adminService.updateUserRole(userId, { role }),
118
+ onSuccess: () => {
119
+ toast.success('User role updated successfully');
120
+ queryClient.invalidateQueries({ queryKey: adminKeys.users() });
121
+ queryClient.invalidateQueries({ queryKey: adminKeys.stats() });
122
+ },
123
+ onError: (error) => {
124
+ toast.error(getErrorMessage(error));
125
+ },
126
+ });
127
+
128
+ return {
129
+ updateRole: mutation.mutate,
130
+ isUpdating: mutation.isPending,
131
+ };
132
+ };