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.
- package/package.json +1 -1
- package/template/_claude/rules/client/01-project-structure.md +2 -5
- package/template/_claude/rules/client/04-design-system.md +48 -43
- package/template/_claude/rules/client/core.md +2 -2
- package/template/client/src/app/globals.css +12 -12
- package/template/client/src/app/layout.tsx +1 -1
- package/template/client/src/app/page.tsx +5 -5
- package/template/client/src/app/providers.tsx +1 -1
- package/template/client/src/components/common/ThemeToggle.tsx +59 -0
- package/template/client/src/features/admin/hooks/useAdminSessions.ts +68 -0
- package/template/client/src/features/admin/hooks/useAdminStats.ts +27 -0
- package/template/client/src/features/admin/hooks/useAdminUsers.ts +132 -0
- package/template/client/src/features/admin/services/admin.service.ts +94 -0
- package/template/client/src/features/admin/types/admin.types.ts +65 -0
- package/template/client/src/features/auth/components/AuthInitializer.tsx +18 -1
- package/template/client/src/lib/api/axios.config.ts +20 -1
- package/template/client/src/lib/constants/api-endpoints.ts +9 -0
- package/template/client/src/lib/constants/app.constants.ts +3 -1
- package/template/client/src/lib/constants/routes.ts +6 -0
- package/template/client/src/lib/env.ts +35 -0
- package/template/client/src/styles/themes/default.css +92 -0
- package/template/server/package.json +1 -0
- package/template/server/postman/collection.json +168 -50
- package/template/server/prisma/schema.prisma +2 -0
- package/template/server/src/jobs/cleanup-deleted-accounts.job.ts +14 -4
- package/template/server/src/libs/prisma.ts +13 -0
- package/template/server/src/modules/admin/admin.controller.ts +130 -1
- package/template/server/src/modules/admin/admin.repo.ts +289 -0
- package/template/server/src/modules/admin/admin.routes.ts +113 -7
- package/template/server/src/modules/admin/admin.schemas.ts +49 -0
- package/template/server/src/modules/admin/admin.service.ts +154 -0
- package/template/server/src/modules/auth/auth.repo.ts +5 -18
- package/template/server/src/modules/auth/auth.service.ts +20 -28
- package/template/server/src/modules/auth/session.repo.ts +10 -5
- package/template/client/src/components/common/ThemeSwitcher.tsx +0 -112
- package/template/client/src/styles/themes/electric-indigo.css +0 -90
- package/template/client/src/styles/themes/ocean-teal.css +0 -90
- package/template/client/src/styles/themes/rose-pink.css +0 -90
- package/template/client/src/styles/themes/warm-orange.css +0 -90
package/package.json
CHANGED
|
@@ -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
|
|
37
|
-
│ │
|
|
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 **
|
|
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 **
|
|
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
|
|
21
|
+
- No `@layer base { :root { } }` — variables defined on `:root` via theme file
|
|
22
22
|
|
|
23
23
|
---
|
|
24
24
|
|
|
25
|
-
## Theme
|
|
25
|
+
## Theme System (Color Management)
|
|
26
26
|
|
|
27
|
-
**ALL color variables live in
|
|
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
|
|
33
|
+
├── app/globals.css ← imports the theme + defines smooth transitions
|
|
34
34
|
└── styles/themes/
|
|
35
|
-
|
|
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
|
|
38
|
+
`globals.css` imports the theme:
|
|
42
39
|
|
|
43
40
|
```css
|
|
44
|
-
@import "../styles/themes/
|
|
41
|
+
@import "../styles/themes/default.css";
|
|
45
42
|
```
|
|
46
43
|
|
|
47
|
-
|
|
44
|
+
### Theme File Structure
|
|
48
45
|
|
|
49
|
-
|
|
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:
|
|
57
|
-
--foreground:
|
|
58
|
-
--primary:
|
|
59
|
-
--primary-foreground:
|
|
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:
|
|
65
|
-
--foreground:
|
|
66
|
-
--primary:
|
|
59
|
+
--background: #15130d;
|
|
60
|
+
--foreground: #e9e8e3;
|
|
61
|
+
--primary: #d6724f;
|
|
67
62
|
/* ... dark mode overrides for all tokens */
|
|
68
63
|
}
|
|
69
64
|
```
|
|
70
65
|
|
|
71
|
-
###
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
###
|
|
82
|
+
### Light/Dark Mode Toggle
|
|
79
83
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
|
90
|
-
2. **NEVER hardcode
|
|
91
|
-
3. **
|
|
92
|
-
4. **
|
|
93
|
-
5. **
|
|
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-
|
|
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="
|
|
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
|
|
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
|
|
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
|
|
7
|
+
* THEME — Single theme with light/dark mode
|
|
8
8
|
* ============================================================
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* by
|
|
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
|
|
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/
|
|
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"
|
|
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 {
|
|
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
|
-
{/*
|
|
50
|
-
<div className="mt-8
|
|
51
|
-
<
|
|
49
|
+
{/* Light/Dark mode toggle */}
|
|
50
|
+
<div className="mt-8">
|
|
51
|
+
<ThemeToggle />
|
|
52
52
|
</div>
|
|
53
53
|
|
|
54
|
-
<div className="mt-
|
|
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="
|
|
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
|
+
};
|