create-tigra 3.0.5 → 3.1.1

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 (85) hide show
  1. package/README.md +4 -2
  2. package/bin/create-tigra.js +5 -19
  3. package/modules/email-verification/client/hooks/useVerification.ts +3 -3
  4. package/package.json +1 -1
  5. package/template/.agents/skills/security-audit/AI-AND-LLM.md +83 -0
  6. package/template/.agents/skills/security-audit/ATTACK-CLASSES.md +130 -0
  7. package/template/.agents/skills/security-audit/CLIENT-SIDE.md +83 -0
  8. package/template/.agents/skills/security-audit/CLOUD-AND-DEPLOYMENT.md +86 -0
  9. package/template/.agents/skills/security-audit/DATA-ISOLATION-AND-LIFECYCLE.md +84 -0
  10. package/template/.agents/skills/security-audit/DESKTOP-MOBILE-AND-LOCAL-IPC.md +89 -0
  11. package/template/.agents/skills/security-audit/HUNTING.md +251 -0
  12. package/template/.agents/skills/security-audit/LICENSE +21 -0
  13. package/template/.agents/skills/security-audit/MEMORY-SAFETY-AND-BINARY.md +101 -0
  14. package/template/.agents/skills/security-audit/PROTOCOLS-RPC-AND-MESSAGING.md +81 -0
  15. package/template/.agents/skills/security-audit/RECONNAISSANCE.md +156 -0
  16. package/template/.agents/skills/security-audit/RESOURCE-EXHAUSTION-AND-AVAILABILITY.md +78 -0
  17. package/template/.agents/skills/security-audit/SKILL.md +192 -0
  18. package/template/.agents/skills/security-audit/SOURCE.md +5 -0
  19. package/template/.agents/skills/security-audit/SUPPLY-CHAIN-AND-RELEASE.md +73 -0
  20. package/template/.agents/skills/security-audit/VALIDATION-AND-REPORTING.md +186 -0
  21. package/template/.agents/skills/security-audit/WEB-PROTOCOL-AND-AUTH.md +105 -0
  22. package/template/.agents/skills/security-audit/report-schema.json +461 -0
  23. package/template/.agents/skills/security-audit/validate-coverage-ledger.cjs +872 -0
  24. package/template/.agents/skills/security-audit/validate-coverage-ledger.test.cjs +740 -0
  25. package/template/.agents/skills/security-audit/validate-findings.cjs +773 -0
  26. package/template/.agents/skills/security-audit/validate-findings.test.cjs +652 -0
  27. package/template/AGENTS.md +46 -0
  28. package/template/client/AGENTS.md +23 -0
  29. package/template/client/package-lock.json +410 -324
  30. package/template/client/package.json +3 -3
  31. package/template/client/src/app/(auth)/layout.tsx +9 -0
  32. package/template/client/src/app/(auth)/loading.tsx +7 -0
  33. package/template/client/src/app/(main)/layout.tsx +11 -0
  34. package/template/client/src/app/(main)/loading.tsx +7 -0
  35. package/template/client/src/app/globals.css +4 -0
  36. package/template/client/src/app/layout.tsx +9 -2
  37. package/template/client/src/app/loading.tsx +2 -6
  38. package/template/client/src/app/not-found.tsx +2 -3
  39. package/template/client/src/app/providers.tsx +6 -3
  40. package/template/client/src/components/common/AppLink.tsx +84 -0
  41. package/template/client/src/components/common/EmptyState.tsx +2 -2
  42. package/template/client/src/components/common/Pagination.tsx +3 -2
  43. package/template/client/src/components/common/RouteLoadingShell.tsx +21 -0
  44. package/template/client/src/components/common/SmoothNavigationProvider.tsx +151 -0
  45. package/template/client/src/components/layout/Header.tsx +12 -12
  46. package/template/client/src/features/admin/hooks/useAdminSessions.ts +2 -2
  47. package/template/client/src/features/admin/hooks/useAdminUsers.ts +3 -3
  48. package/template/client/src/features/auth/components/AuthInitializer.tsx +3 -2
  49. package/template/client/src/features/auth/components/LoginForm.tsx +3 -3
  50. package/template/client/src/features/auth/components/RegisterForm.tsx +3 -3
  51. package/template/client/src/features/auth/hooks/useAuth.ts +2 -2
  52. package/template/client/src/features/auth/hooks/usePasswordReset.ts +2 -2
  53. package/template/client/src/hooks/useAppRouter.ts +40 -0
  54. package/template/client/src/styles/fonts/inter-jetbrains.css +4 -2
  55. package/template/client/src/styles/themes/default.css +1 -1
  56. package/template/gitignore +0 -6
  57. package/template/server/AGENTS.md +28 -0
  58. package/template/server/package-lock.json +671 -522
  59. package/template/server/package.json +8 -8
  60. package/template/_claude/QUICK_REFERENCE.md +0 -193
  61. package/template/_claude/README.md +0 -53
  62. package/template/_claude/commands/create-client.md +0 -878
  63. package/template/_claude/commands/create-server.md +0 -388
  64. package/template/_claude/hooks/restrict-paths.sh +0 -51
  65. package/template/_claude/rules/client/01-project-structure.md +0 -147
  66. package/template/_claude/rules/client/02-components-and-types.md +0 -146
  67. package/template/_claude/rules/client/03-data-and-state.md +0 -195
  68. package/template/_claude/rules/client/04-design-system.md +0 -408
  69. package/template/_claude/rules/client/05-security.md +0 -55
  70. package/template/_claude/rules/client/06-ux-checklist.md +0 -111
  71. package/template/_claude/rules/client/07-deployment.md +0 -99
  72. package/template/_claude/rules/client/08-lockfile-cross-platform.md +0 -79
  73. package/template/_claude/rules/client/core.md +0 -46
  74. package/template/_claude/rules/global/completion-reports.md +0 -178
  75. package/template/_claude/rules/global/core.md +0 -104
  76. package/template/_claude/rules/global/investigation-before-conclusions.md +0 -57
  77. package/template/_claude/rules/server/core.md +0 -52
  78. package/template/_claude/rules/server/database.md +0 -124
  79. package/template/_claude/rules/server/deployment.md +0 -78
  80. package/template/_claude/rules/server/project-conventions.md +0 -254
  81. package/template/_claude/rules/server/response-handling.md +0 -144
  82. package/template/_claude/settings.json +0 -15
  83. package/template/_claude/skills/clean-ui/SKILL.md +0 -63
  84. package/template/_claude/skills/role/SKILL.md +0 -39
  85. package/template/_claude/skills/theme/SKILL.md +0 -109
@@ -16,11 +16,11 @@
16
16
  "@reduxjs/toolkit": "^2.11.2",
17
17
  "@sentry/nextjs": "^10.62.0",
18
18
  "@tanstack/react-query": "^5.90.21",
19
- "axios": "^1.13.5",
19
+ "axios": "^1.20.0",
20
20
  "class-variance-authority": "^0.7.1",
21
21
  "clsx": "^2.1.1",
22
22
  "lucide-react": "^0.563.0",
23
- "next": "16.2.9",
23
+ "next": "16.3.5",
24
24
  "next-themes": "^0.4.6",
25
25
  "radix-ui": "^1.4.3",
26
26
  "react": "19.2.3",
@@ -41,7 +41,7 @@
41
41
  "@types/react": "^19",
42
42
  "@types/react-dom": "^19",
43
43
  "eslint": "^9",
44
- "eslint-config-next": "16.2.9",
44
+ "eslint-config-next": "16.3.5",
45
45
  "shadcn": "^3.8.4",
46
46
  "tailwindcss": "^4",
47
47
  "tw-animate-css": "^1.4.0",
@@ -0,0 +1,9 @@
1
+ import type React from 'react';
2
+
3
+ export default function AuthRouteLayout({
4
+ children,
5
+ }: Readonly<{
6
+ children: React.ReactNode;
7
+ }>): React.ReactElement {
8
+ return <main className="flex min-h-dvh items-center justify-center p-4">{children}</main>;
9
+ }
@@ -0,0 +1,7 @@
1
+ import type React from 'react';
2
+
3
+ import { RouteLoadingShell } from '@/components/common/RouteLoadingShell';
4
+
5
+ export default function AuthRouteLoading(): React.ReactElement {
6
+ return <RouteLoadingShell className="min-h-dvh" />;
7
+ }
@@ -0,0 +1,11 @@
1
+ import type React from 'react';
2
+
3
+ import { MainLayout } from '@/components/layout/MainLayout';
4
+
5
+ export default function MainRouteLayout({
6
+ children,
7
+ }: Readonly<{
8
+ children: React.ReactNode;
9
+ }>): React.ReactElement {
10
+ return <MainLayout>{children}</MainLayout>;
11
+ }
@@ -0,0 +1,7 @@
1
+ import type React from 'react';
2
+
3
+ import { RouteLoadingShell } from '@/components/common/RouteLoadingShell';
4
+
5
+ export default function MainRouteLoading(): React.ReactElement {
6
+ return <RouteLoadingShell />;
7
+ }
@@ -91,6 +91,7 @@
91
91
  html {
92
92
  @apply bg-background;
93
93
  overflow-x: hidden;
94
+ scroll-behavior: smooth;
94
95
  -webkit-text-size-adjust: 100%;
95
96
  text-size-adjust: 100%;
96
97
  }
@@ -100,4 +101,7 @@
100
101
  overflow-x: hidden;
101
102
  touch-action: manipulation;
102
103
  }
104
+ html[data-smooth-navigation="pending"] body {
105
+ min-height: var(--smooth-navigation-min-height);
106
+ }
103
107
  }
@@ -40,8 +40,15 @@ export default async function RootLayout({
40
40
  const nonce = (await headers()).get('x-nonce') ?? undefined;
41
41
 
42
42
  return (
43
- <html lang="en" suppressHydrationWarning>
44
- <body className={`${inter.variable} ${jetbrainsMono.variable} font-sans antialiased`}>
43
+ // The font preset consumes these next/font variables from :root. Keep the
44
+ // generated classes on <html>; variables introduced only on <body> cannot
45
+ // resolve the root-scoped Tailwind font tokens.
46
+ <html
47
+ lang="en"
48
+ className={`${inter.variable} ${jetbrainsMono.variable}`}
49
+ suppressHydrationWarning
50
+ >
51
+ <body className="font-sans antialiased">
45
52
  <Providers nonce={nonce}>{children}</Providers>
46
53
  </body>
47
54
  </html>
@@ -1,11 +1,7 @@
1
1
  import type React from 'react';
2
2
 
3
- import { LoadingSpinner } from '@/components/common/LoadingSpinner';
3
+ import { RouteLoadingShell } from '@/components/common/RouteLoadingShell';
4
4
 
5
5
  export default function Loading(): React.ReactElement {
6
- return (
7
- <div className="flex min-h-dvh items-center justify-center">
8
- <LoadingSpinner size="lg" />
9
- </div>
10
- );
6
+ return <RouteLoadingShell className="min-h-dvh" />;
11
7
  }
@@ -1,10 +1,9 @@
1
1
  import type React from 'react';
2
2
 
3
- import Link from 'next/link';
4
-
5
3
  import { FileQuestion } from 'lucide-react';
6
4
 
7
5
  import { Button } from '@/components/ui/button';
6
+ import { AppLink } from '@/components/common/AppLink';
8
7
  import { ROUTES } from '@/lib/constants/routes';
9
8
 
10
9
  export default function NotFound(): React.ReactElement {
@@ -16,7 +15,7 @@ export default function NotFound(): React.ReactElement {
16
15
  The page you&apos;re looking for doesn&apos;t exist or has been moved.
17
16
  </p>
18
17
  <Button asChild>
19
- <Link href={ROUTES.HOME}>Go home</Link>
18
+ <AppLink href={ROUTES.HOME}>Go home</AppLink>
20
19
  </Button>
21
20
  </div>
22
21
  );
@@ -8,6 +8,7 @@ import { Provider as ReduxProvider } from 'react-redux';
8
8
  import { ThemeProvider } from 'next-themes';
9
9
  import { Toaster } from 'sonner';
10
10
 
11
+ import { SmoothNavigationProvider } from '@/components/common/SmoothNavigationProvider';
11
12
  import { store } from '@/store';
12
13
  import { AuthInitializer } from '@/features/auth/components/AuthInitializer';
13
14
 
@@ -44,9 +45,11 @@ export function Providers({
44
45
  enableColorScheme={false}
45
46
  nonce={nonce}
46
47
  >
47
- <AuthInitializer>
48
- {children}
49
- </AuthInitializer>
48
+ <SmoothNavigationProvider>
49
+ <AuthInitializer>
50
+ {children}
51
+ </AuthInitializer>
52
+ </SmoothNavigationProvider>
50
53
  <Toaster position="top-right" richColors />
51
54
  </ThemeProvider>
52
55
  </QueryClientProvider>
@@ -0,0 +1,84 @@
1
+ 'use client';
2
+
3
+ import type React from 'react';
4
+ import { forwardRef, useRef } from 'react';
5
+
6
+ import NextLink, { type LinkProps as NextLinkProps } from 'next/link';
7
+
8
+ import { useSmoothNavigation } from './SmoothNavigationProvider';
9
+
10
+ type AnchorProps = Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, keyof NextLinkProps>;
11
+
12
+ export type AppLinkProps = Omit<NextLinkProps, 'scroll'> & AnchorProps;
13
+
14
+ export const AppLink = forwardRef<HTMLAnchorElement, AppLinkProps>(
15
+ ({ onClick, onNavigate, ...props }, ref): React.ReactElement => {
16
+ const { startNavigation } = useSmoothNavigation();
17
+ const pendingDestinationRef = useRef<string | null>(null);
18
+
19
+ return (
20
+ <NextLink
21
+ {...props}
22
+ ref={ref}
23
+ scroll={false}
24
+ onClick={(event) => {
25
+ onClick?.(event);
26
+ pendingDestinationRef.current = null;
27
+ if (
28
+ event.defaultPrevented ||
29
+ event.button !== 0 ||
30
+ event.metaKey ||
31
+ event.ctrlKey ||
32
+ event.shiftKey ||
33
+ event.altKey
34
+ ) {
35
+ return;
36
+ }
37
+
38
+ const anchor = event.currentTarget;
39
+ const destination = new URL(anchor.href);
40
+ const current = new URL(window.location.href);
41
+ const opensElsewhere = Boolean(anchor.target && anchor.target !== '_self');
42
+ const isHashOnly =
43
+ destination.pathname === current.pathname &&
44
+ destination.search === current.search &&
45
+ Boolean(destination.hash);
46
+
47
+ if (
48
+ opensElsewhere ||
49
+ anchor.hasAttribute('download') ||
50
+ destination.origin !== current.origin ||
51
+ isHashOnly
52
+ ) {
53
+ return;
54
+ }
55
+
56
+ // onNavigate runs after onClick only for client-side navigations. Wait
57
+ // for it so a consumer can still prevent navigation without briefly
58
+ // showing a loader or starting the scroll animation.
59
+ pendingDestinationRef.current = destination.href;
60
+ }}
61
+ onNavigate={(event) => {
62
+ let navigationPrevented = false;
63
+ onNavigate?.({
64
+ preventDefault: () => {
65
+ navigationPrevented = true;
66
+ event.preventDefault();
67
+ },
68
+ });
69
+ if (navigationPrevented) {
70
+ pendingDestinationRef.current = null;
71
+ return;
72
+ }
73
+
74
+ if (pendingDestinationRef.current) {
75
+ startNavigation(pendingDestinationRef.current);
76
+ pendingDestinationRef.current = null;
77
+ }
78
+ }}
79
+ />
80
+ );
81
+ }
82
+ );
83
+
84
+ AppLink.displayName = 'AppLink';
@@ -1,9 +1,9 @@
1
1
  import type React from 'react';
2
2
 
3
- import Link from 'next/link';
4
3
  import { FileQuestion } from 'lucide-react';
5
4
 
6
5
  import { Button } from '@/components/ui/button';
6
+ import { AppLink } from './AppLink';
7
7
 
8
8
  interface EmptyStateProps {
9
9
  title: string;
@@ -24,7 +24,7 @@ export const EmptyState = ({
24
24
  <p className="mb-4 text-muted-foreground">{description}</p>
25
25
  {actionLabel && actionHref && (
26
26
  <Button asChild>
27
- <Link href={actionHref}>{actionLabel}</Link>
27
+ <AppLink href={actionHref}>{actionLabel}</AppLink>
28
28
  </Button>
29
29
  )}
30
30
  </div>
@@ -3,10 +3,11 @@
3
3
  import type React from 'react';
4
4
  import { Suspense, useCallback } from 'react';
5
5
 
6
- import { useRouter, useSearchParams, usePathname } from 'next/navigation';
6
+ import { useSearchParams, usePathname } from 'next/navigation';
7
7
  import { ChevronLeft, ChevronRight } from 'lucide-react';
8
8
 
9
9
  import { Button } from '@/components/ui/button';
10
+ import { useAppRouter } from '@/hooks/useAppRouter';
10
11
 
11
12
  interface PaginationProps {
12
13
  page: number;
@@ -14,7 +15,7 @@ interface PaginationProps {
14
15
  }
15
16
 
16
17
  const PaginationInner = ({ page, totalPages }: PaginationProps): React.ReactElement => {
17
- const router = useRouter();
18
+ const router = useAppRouter();
18
19
  const pathname = usePathname();
19
20
  const searchParams = useSearchParams();
20
21
 
@@ -0,0 +1,21 @@
1
+ import type React from 'react';
2
+
3
+ import { LoadingSpinner } from './LoadingSpinner';
4
+ import { cn } from '@/lib/utils';
5
+
6
+ /**
7
+ * Shared fallback for route transitions that are waiting on server data.
8
+ * Keep this intentionally generic so feature pages can later replace it with
9
+ * a layout-matching skeleton without changing the navigation contract.
10
+ */
11
+ interface RouteLoadingShellProps {
12
+ className?: string;
13
+ }
14
+
15
+ export const RouteLoadingShell = ({ className }: RouteLoadingShellProps): React.ReactElement => {
16
+ return (
17
+ <div className={cn('flex min-h-[calc(100dvh-8rem)] items-center justify-center p-6', className)}>
18
+ <LoadingSpinner size="lg" />
19
+ </div>
20
+ );
21
+ };
@@ -0,0 +1,151 @@
1
+ 'use client';
2
+
3
+ import type React from 'react';
4
+ import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
5
+
6
+ import { usePathname, useSearchParams } from 'next/navigation';
7
+
8
+ import { RouteLoadingShell } from './RouteLoadingShell';
9
+
10
+ const MAX_SMOOTH_SCROLL_MS = 1500;
11
+ const NAVIGATION_FAILSAFE_MS = 30_000;
12
+
13
+ interface SmoothNavigationContextValue {
14
+ startNavigation: (destination: string) => void;
15
+ }
16
+
17
+ const SmoothNavigationContext = createContext<SmoothNavigationContextValue | null>(null);
18
+
19
+ interface SmoothNavigationProviderProps {
20
+ children: React.ReactNode;
21
+ }
22
+
23
+ export const SmoothNavigationProvider = ({
24
+ children,
25
+ }: SmoothNavigationProviderProps): React.ReactElement => {
26
+ const pathname = usePathname();
27
+ const searchParams = useSearchParams();
28
+ const [isNavigating, setIsNavigating] = useState(false);
29
+ const animationFrameRef = useRef<number | null>(null);
30
+ const destinationRef = useRef<string | null>(null);
31
+ const originRef = useRef<string | null>(null);
32
+ const navigationTimeoutRef = useRef<number | null>(null);
33
+ const timeoutRef = useRef<number | null>(null);
34
+
35
+ const clearTransitionState = useCallback((): void => {
36
+ if (animationFrameRef.current !== null) {
37
+ window.cancelAnimationFrame(animationFrameRef.current);
38
+ animationFrameRef.current = null;
39
+ }
40
+ if (timeoutRef.current !== null) {
41
+ window.clearTimeout(timeoutRef.current);
42
+ timeoutRef.current = null;
43
+ }
44
+
45
+ const root = document.documentElement;
46
+ delete root.dataset.smoothNavigation;
47
+ root.style.removeProperty('--smooth-navigation-min-height');
48
+ }, []);
49
+
50
+ const finishNavigation = useCallback((): void => {
51
+ if (navigationTimeoutRef.current !== null) {
52
+ window.clearTimeout(navigationTimeoutRef.current);
53
+ navigationTimeoutRef.current = null;
54
+ }
55
+ destinationRef.current = null;
56
+ originRef.current = null;
57
+ setIsNavigating(false);
58
+ }, []);
59
+
60
+ const cancelNavigation = useCallback((): void => {
61
+ finishNavigation();
62
+ clearTransitionState();
63
+ }, [clearTransitionState, finishNavigation]);
64
+
65
+ const startNavigation = useCallback((destination: string): void => {
66
+ clearTransitionState();
67
+
68
+ const root = document.documentElement;
69
+ const documentHeight = Math.max(root.scrollHeight, document.body.scrollHeight, root.clientHeight);
70
+
71
+ // The route fallback can be shorter than the outgoing page. Preserve its
72
+ // height until scrolling reaches the top so the browser cannot clamp the
73
+ // scroll position and turn a smooth transition into an instant jump.
74
+ root.dataset.smoothNavigation = 'pending';
75
+ root.style.setProperty('--smooth-navigation-min-height', `${documentHeight}px`);
76
+
77
+ window.scrollTo({ top: 0, left: 0, behavior: 'smooth' });
78
+
79
+ const startedAt = window.performance.now();
80
+ const monitorScroll = (): void => {
81
+ const timedOut = window.performance.now() - startedAt >= MAX_SMOOTH_SCROLL_MS;
82
+ if (Math.abs(window.scrollY) <= 1 || timedOut) {
83
+ clearTransitionState();
84
+ return;
85
+ }
86
+ animationFrameRef.current = window.requestAnimationFrame(monitorScroll);
87
+ };
88
+
89
+ animationFrameRef.current = window.requestAnimationFrame(monitorScroll);
90
+ timeoutRef.current = window.setTimeout(clearTransitionState, MAX_SMOOTH_SCROLL_MS + 100);
91
+
92
+ const destinationUrl = new URL(destination, window.location.href);
93
+ const destinationKey = `${destinationUrl.pathname}${destinationUrl.search}`;
94
+ const currentKey = `${window.location.pathname}${window.location.search}`;
95
+ if (destinationKey === currentKey) {
96
+ return;
97
+ }
98
+
99
+ destinationRef.current = destinationKey;
100
+ originRef.current = currentKey;
101
+ setIsNavigating(true);
102
+ if (navigationTimeoutRef.current !== null) {
103
+ window.clearTimeout(navigationTimeoutRef.current);
104
+ }
105
+ navigationTimeoutRef.current = window.setTimeout(cancelNavigation, NAVIGATION_FAILSAFE_MS);
106
+ }, [cancelNavigation, clearTransitionState]);
107
+
108
+ useEffect(() => {
109
+ const currentKey = `${pathname}${searchParams.size > 0 ? `?${searchParams.toString()}` : ''}`;
110
+ if (
111
+ destinationRef.current === currentKey ||
112
+ (originRef.current !== null && originRef.current !== currentKey)
113
+ ) {
114
+ // The new route can commit before the smooth scroll reaches the top.
115
+ // Hide the overlay without releasing the preserved document height. A
116
+ // changed key also handles redirects whose URL differs from the request.
117
+ finishNavigation();
118
+ }
119
+ }, [finishNavigation, pathname, searchParams]);
120
+
121
+ useEffect(
122
+ () => () => {
123
+ if (navigationTimeoutRef.current !== null) {
124
+ window.clearTimeout(navigationTimeoutRef.current);
125
+ }
126
+ clearTransitionState();
127
+ },
128
+ [clearTransitionState]
129
+ );
130
+
131
+ const value = useMemo(() => ({ startNavigation }), [startNavigation]);
132
+
133
+ return (
134
+ <SmoothNavigationContext.Provider value={value}>
135
+ {children}
136
+ {isNavigating && (
137
+ <div className="fixed inset-0 z-40 overflow-hidden bg-background" aria-busy="true">
138
+ <RouteLoadingShell className="min-h-dvh" />
139
+ </div>
140
+ )}
141
+ </SmoothNavigationContext.Provider>
142
+ );
143
+ };
144
+
145
+ export const useSmoothNavigation = (): SmoothNavigationContextValue => {
146
+ const context = useContext(SmoothNavigationContext);
147
+ if (!context) {
148
+ throw new Error('useSmoothNavigation must be used within SmoothNavigationProvider');
149
+ }
150
+ return context;
151
+ };
@@ -3,11 +3,11 @@
3
3
  import type React from 'react';
4
4
  import { useState, useCallback, useEffect, useRef } from 'react';
5
5
 
6
- import Link from 'next/link';
7
6
  import { useTheme } from 'next-themes';
8
7
  import { Sun, Moon, Menu, X, LogOut } from 'lucide-react';
9
8
 
10
9
  import { Button } from '@/components/ui/button';
10
+ import { AppLink } from '@/components/common/AppLink';
11
11
  import { useAppSelector } from '@/store/hooks';
12
12
  import { useAuth } from '@/features/auth/hooks/useAuth';
13
13
  import { APP_NAME } from '@/lib/constants/app.constants';
@@ -50,18 +50,18 @@ export const Header = (): React.ReactElement => {
50
50
  return (
51
51
  <header className="sticky top-0 z-50 border-b border-border/50 bg-background/80 backdrop-blur-md">
52
52
  <div className="container mx-auto flex h-16 items-center justify-between px-4 md:px-6 lg:px-8">
53
- <Link
53
+ <AppLink
54
54
  href={ROUTES.HOME}
55
55
  className="text-xl font-bold tracking-tight transition-colors duration-150 active:opacity-70 md:hover:text-primary"
56
56
  >
57
57
  {APP_NAME}
58
- </Link>
58
+ </AppLink>
59
59
 
60
60
  <nav className="hidden items-center gap-2 md:flex">
61
61
  {isAuthenticated ? (
62
62
  <>
63
63
  <Button variant="ghost" size="sm" asChild>
64
- <Link href={ROUTES.DASHBOARD}>Dashboard</Link>
64
+ <AppLink href={ROUTES.DASHBOARD}>Dashboard</AppLink>
65
65
  </Button>
66
66
  <span className="text-sm text-muted-foreground">
67
67
  {user?.firstName}
@@ -79,10 +79,10 @@ export const Header = (): React.ReactElement => {
79
79
  ) : (
80
80
  <>
81
81
  <Button variant="ghost" size="sm" asChild>
82
- <Link href={ROUTES.LOGIN}>Sign in</Link>
82
+ <AppLink href={ROUTES.LOGIN}>Sign in</AppLink>
83
83
  </Button>
84
84
  <Button size="sm" asChild>
85
- <Link href={ROUTES.REGISTER}>Get started</Link>
85
+ <AppLink href={ROUTES.REGISTER}>Get started</AppLink>
86
86
  </Button>
87
87
  </>
88
88
  )}
@@ -134,9 +134,9 @@ export const Header = (): React.ReactElement => {
134
134
  {isAuthenticated ? (
135
135
  <>
136
136
  <Button variant="ghost" className="w-full justify-start" asChild>
137
- <Link href={ROUTES.DASHBOARD} onClick={closeMobileMenu}>
137
+ <AppLink href={ROUTES.DASHBOARD} onClick={closeMobileMenu}>
138
138
  Dashboard
139
- </Link>
139
+ </AppLink>
140
140
  </Button>
141
141
  <Button
142
142
  variant="ghost"
@@ -154,14 +154,14 @@ export const Header = (): React.ReactElement => {
154
154
  ) : (
155
155
  <>
156
156
  <Button variant="ghost" className="w-full justify-start" asChild>
157
- <Link href={ROUTES.LOGIN} onClick={closeMobileMenu}>
157
+ <AppLink href={ROUTES.LOGIN} onClick={closeMobileMenu}>
158
158
  Sign in
159
- </Link>
159
+ </AppLink>
160
160
  </Button>
161
161
  <Button className="w-full" asChild>
162
- <Link href={ROUTES.REGISTER} onClick={closeMobileMenu}>
162
+ <AppLink href={ROUTES.REGISTER} onClick={closeMobileMenu}>
163
163
  Get started
164
- </Link>
164
+ </AppLink>
165
165
  </Button>
166
166
  </>
167
167
  )}
@@ -1,9 +1,9 @@
1
1
  'use client';
2
2
 
3
3
  import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
4
- import { useRouter } from 'next/navigation';
5
4
  import { toast } from 'sonner';
6
5
 
6
+ import { useAppRouter } from '@/hooks/useAppRouter';
7
7
  import { getErrorMessage } from '@/lib/utils/error';
8
8
  import { adminService } from '../services/admin.service';
9
9
  import { adminKeys } from './useAdminUsers';
@@ -49,7 +49,7 @@ interface UseForceExpireSessionReturn {
49
49
 
50
50
  export const useForceExpireSession = (): UseForceExpireSessionReturn => {
51
51
  const queryClient = useQueryClient();
52
- const router = useRouter();
52
+ const router = useAppRouter();
53
53
 
54
54
  const mutation = useMutation({
55
55
  mutationFn: (sessionId: string) => adminService.deleteSession(sessionId),
@@ -1,9 +1,9 @@
1
1
  'use client';
2
2
 
3
3
  import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
4
- import { useRouter } from 'next/navigation';
5
4
  import { toast } from 'sonner';
6
5
 
6
+ import { useAppRouter } from '@/hooks/useAppRouter';
7
7
  import { getErrorMessage } from '@/lib/utils/error';
8
8
  import { adminService } from '../services/admin.service';
9
9
 
@@ -82,7 +82,7 @@ interface UseUpdateUserStatusReturn {
82
82
 
83
83
  export const useUpdateUserStatus = (): UseUpdateUserStatusReturn => {
84
84
  const queryClient = useQueryClient();
85
- const router = useRouter();
85
+ const router = useAppRouter();
86
86
 
87
87
  const mutation = useMutation({
88
88
  mutationFn: ({ userId, isActive }: { userId: string; isActive: boolean }) =>
@@ -114,7 +114,7 @@ interface UseUpdateUserRoleReturn {
114
114
 
115
115
  export const useUpdateUserRole = (): UseUpdateUserRoleReturn => {
116
116
  const queryClient = useQueryClient();
117
- const router = useRouter();
117
+ const router = useAppRouter();
118
118
 
119
119
  const mutation = useMutation({
120
120
  mutationFn: ({ userId, role }: { userId: string; role: 'USER' | 'ADMIN' }) =>
@@ -3,11 +3,12 @@
3
3
  import type React from 'react';
4
4
  import { useEffect } from 'react';
5
5
 
6
- import { usePathname, useRouter } from 'next/navigation';
6
+ import { usePathname } from 'next/navigation';
7
7
 
8
8
  import { toast } from 'sonner';
9
9
 
10
10
  import { useAppSelector } from '@/store/hooks';
11
+ import { useAppRouter } from '@/hooks/useAppRouter';
11
12
  import { ROUTES } from '@/lib/constants/routes';
12
13
  import { isErrorCode, ERROR_CODES } from '@/lib/utils/error';
13
14
  import { useCurrentUser } from '../hooks/useCurrentUser';
@@ -37,7 +38,7 @@ interface HttpLikeError {
37
38
 
38
39
  export const AuthInitializer = ({ children }: AuthInitializerProps): React.ReactElement => {
39
40
  const pathname = usePathname();
40
- const router = useRouter();
41
+ const router = useAppRouter();
41
42
  const { isLoggingOut } = useAppSelector((state) => state.auth);
42
43
 
43
44
  // Skip getMe() on auth pages and during logout.
@@ -3,7 +3,6 @@
3
3
  import type React from 'react';
4
4
  import { Suspense } from 'react';
5
5
 
6
- import Link from 'next/link';
7
6
  import { useSearchParams } from 'next/navigation';
8
7
  import { useForm } from 'react-hook-form';
9
8
  import { zodResolver } from '@hookform/resolvers/zod';
@@ -13,6 +12,7 @@ import { Loader2 } from 'lucide-react';
13
12
  import { Button } from '@/components/ui/button';
14
13
  import { Input } from '@/components/ui/input';
15
14
  import { Label } from '@/components/ui/label';
15
+ import { AppLink } from '@/components/common/AppLink';
16
16
  import { useAuth } from '../hooks/useAuth';
17
17
  import { ROUTES } from '@/lib/constants/routes';
18
18
  import { cn } from '@/lib/utils';
@@ -100,12 +100,12 @@ const LoginFormInner = (): React.ReactElement => {
100
100
 
101
101
  <p className="text-center text-sm text-muted-foreground">
102
102
  Don&apos;t have an account?{' '}
103
- <Link
103
+ <AppLink
104
104
  href={ROUTES.REGISTER}
105
105
  className="font-medium text-primary transition-colors duration-150 active:opacity-70 md:hover:text-primary/80"
106
106
  >
107
107
  Create account
108
- </Link>
108
+ </AppLink>
109
109
  </p>
110
110
  </div>
111
111
  );