create-modern-react 1.0.0 → 2.0.0
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/README.md +270 -72
- package/bin/index.js +13 -13
- package/lib/install.js +103 -32
- package/lib/prompts.js +152 -179
- package/lib/setup.js +267 -159
- package/package.json +17 -8
- package/templates/base/.env.example +9 -0
- package/templates/base/.eslintrc.cjs +37 -0
- package/templates/base/.prettierrc +11 -0
- package/templates/base/components.json +17 -0
- package/templates/base/index.html +2 -1
- package/templates/base/package.json +33 -14
- package/templates/base/postcss.config.js +6 -0
- package/templates/base/src/App.tsx +5 -18
- package/templates/base/src/components/layout/error-boundary.tsx +60 -0
- package/templates/base/src/components/layout/index.ts +2 -0
- package/templates/base/src/components/layout/root-layout.tsx +36 -0
- package/templates/base/src/components/ui/button.tsx +55 -0
- package/templates/base/src/components/ui/card.tsx +85 -0
- package/templates/base/src/components/ui/index.ts +12 -0
- package/templates/base/src/components/ui/input.tsx +24 -0
- package/templates/base/src/components/ui/separator.tsx +29 -0
- package/templates/base/src/components/ui/skeleton.tsx +15 -0
- package/templates/base/src/hooks/index.ts +3 -0
- package/templates/base/src/hooks/use-cancel-token.ts +63 -0
- package/templates/base/src/hooks/use-debounce.ts +29 -0
- package/templates/base/src/hooks/use-loader.ts +39 -0
- package/templates/base/src/index.css +73 -60
- package/templates/base/src/lib/utils.ts +14 -0
- package/templates/base/src/main.tsx +6 -6
- package/templates/base/src/providers/index.tsx +27 -0
- package/templates/base/src/providers/theme-provider.tsx +92 -0
- package/templates/base/src/routes/index.tsx +40 -0
- package/templates/base/src/routes/routes.ts +36 -0
- package/templates/base/src/screens/home/index.tsx +132 -0
- package/templates/base/src/screens/not-found/index.tsx +29 -0
- package/templates/base/src/services/alertify-services.ts +133 -0
- package/templates/base/src/services/api/api-helpers.ts +130 -0
- package/templates/base/src/services/api/axios-instance.ts +77 -0
- package/templates/base/src/services/api/index.ts +9 -0
- package/templates/base/src/services/index.ts +2 -0
- package/templates/base/src/types/index.ts +55 -0
- package/templates/base/src/vite-env.d.ts +31 -0
- package/templates/base/tailwind.config.js +77 -0
- package/templates/base/tsconfig.json +4 -3
- package/templates/base/tsconfig.node.json +22 -0
- package/templates/base/vite.config.ts +65 -4
- package/templates/optional/antd/config-provider.tsx +33 -0
- package/templates/optional/antd/index.ts +2 -0
- package/templates/optional/antd/styles/antd-overrides.css +104 -0
- package/templates/optional/antd/theme.ts +75 -0
- package/templates/optional/husky/.husky/pre-commit +1 -0
- package/templates/optional/husky/.lintstagedrc.json +6 -0
- package/templates/optional/redux/hooks.ts +17 -0
- package/templates/optional/redux/index.ts +13 -0
- package/templates/optional/redux/provider.tsx +33 -0
- package/templates/optional/redux/store/index.ts +45 -0
- package/templates/optional/redux/store/slices/app-slice.ts +62 -0
- package/templates/base/src/App.css +0 -14
|
@@ -4,25 +4,44 @@
|
|
|
4
4
|
"version": "0.0.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
7
|
-
"dev": "vite",
|
|
8
|
-
"build": "tsc && vite build",
|
|
7
|
+
"dev": "vite --open --port 3000",
|
|
8
|
+
"build": "tsc -b && vite build",
|
|
9
|
+
"preview": "vite preview",
|
|
9
10
|
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
|
10
|
-
"
|
|
11
|
+
"lint:fix": "eslint . --ext ts,tsx --fix",
|
|
12
|
+
"format": "prettier --write \"src/**/*.{ts,tsx,css,json}\"",
|
|
13
|
+
"format:check": "prettier --check \"src/**/*.{ts,tsx,css,json}\""
|
|
11
14
|
},
|
|
12
15
|
"dependencies": {
|
|
13
|
-
"react": "^18.
|
|
14
|
-
"react-dom": "^18.
|
|
16
|
+
"react": "^18.3.1",
|
|
17
|
+
"react-dom": "^18.3.1",
|
|
18
|
+
"wouter": "^3.3.0",
|
|
19
|
+
"axios": "^1.7.0",
|
|
20
|
+
"clsx": "^2.1.1",
|
|
21
|
+
"tailwind-merge": "^2.5.0",
|
|
22
|
+
"class-variance-authority": "^0.7.0",
|
|
23
|
+
"lucide-react": "^0.400.0",
|
|
24
|
+
"react-hot-toast": "^2.4.1",
|
|
25
|
+
"@radix-ui/react-slot": "^1.1.0"
|
|
15
26
|
},
|
|
16
27
|
"devDependencies": {
|
|
17
|
-
"@types/react": "^18.
|
|
18
|
-
"@types/react-dom": "^18.
|
|
19
|
-
"@typescript-eslint/eslint-plugin": "^7.
|
|
20
|
-
"@typescript-eslint/parser": "^7.
|
|
21
|
-
"@vitejs/plugin-react": "^
|
|
28
|
+
"@types/react": "^18.3.3",
|
|
29
|
+
"@types/react-dom": "^18.3.0",
|
|
30
|
+
"@typescript-eslint/eslint-plugin": "^7.15.0",
|
|
31
|
+
"@typescript-eslint/parser": "^7.15.0",
|
|
32
|
+
"@vitejs/plugin-react-swc": "^3.7.0",
|
|
33
|
+
"autoprefixer": "^10.4.19",
|
|
22
34
|
"eslint": "^8.57.0",
|
|
23
|
-
"eslint-plugin-react-hooks": "^4.6.
|
|
24
|
-
"eslint-plugin-react-refresh": "^0.4.
|
|
25
|
-
"
|
|
26
|
-
"
|
|
35
|
+
"eslint-plugin-react-hooks": "^4.6.2",
|
|
36
|
+
"eslint-plugin-react-refresh": "^0.4.7",
|
|
37
|
+
"eslint-plugin-unused-imports": "^3.2.0",
|
|
38
|
+
"postcss": "^8.4.39",
|
|
39
|
+
"prettier": "^3.3.0",
|
|
40
|
+
"prettier-plugin-tailwindcss": "^0.6.5",
|
|
41
|
+
"tailwindcss": "^3.4.4",
|
|
42
|
+
"typescript": "^5.5.0",
|
|
43
|
+
"vite": "^5.4.0",
|
|
44
|
+
"vite-plugin-svgr": "^4.2.0",
|
|
45
|
+
"vite-plugin-compression": "^0.5.1"
|
|
27
46
|
}
|
|
28
47
|
}
|
|
@@ -1,24 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
1
|
+
import { Providers } from '~/providers';
|
|
2
|
+
import { AppRouter } from '~/routes';
|
|
3
3
|
|
|
4
4
|
function App() {
|
|
5
|
-
const [count, setCount] = useState(0);
|
|
6
|
-
|
|
7
5
|
return (
|
|
8
|
-
<
|
|
9
|
-
<
|
|
10
|
-
|
|
11
|
-
<div className="card">
|
|
12
|
-
<button onClick={() => setCount((count) => count + 1)}>
|
|
13
|
-
count is {count}
|
|
14
|
-
</button>
|
|
15
|
-
<p>
|
|
16
|
-
Edit <code>src/App.tsx</code> and save to test HMR
|
|
17
|
-
</p>
|
|
18
|
-
</div>
|
|
19
|
-
<p className="read-the-docs">Click on the React logo to learn more</p>
|
|
20
|
-
</div>
|
|
21
|
-
</div>
|
|
6
|
+
<Providers>
|
|
7
|
+
<AppRouter />
|
|
8
|
+
</Providers>
|
|
22
9
|
);
|
|
23
10
|
}
|
|
24
11
|
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { Component, type ErrorInfo, type ReactNode } from 'react';
|
|
2
|
+
import { AlertTriangle, RefreshCw } from 'lucide-react';
|
|
3
|
+
import { Button } from '~/components/ui';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
children: ReactNode;
|
|
7
|
+
fallback?: ReactNode;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface State {
|
|
11
|
+
hasError: boolean;
|
|
12
|
+
error: Error | null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class ErrorBoundary extends Component<Props, State> {
|
|
16
|
+
public state: State = {
|
|
17
|
+
hasError: false,
|
|
18
|
+
error: null,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
public static getDerivedStateFromError(error: Error): State {
|
|
22
|
+
return { hasError: true, error };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
public componentDidCatch(error: Error, errorInfo: ErrorInfo) {
|
|
26
|
+
console.error('ErrorBoundary caught an error:', error, errorInfo);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
private handleRetry = () => {
|
|
30
|
+
this.setState({ hasError: false, error: null });
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
public render() {
|
|
34
|
+
if (this.state.hasError) {
|
|
35
|
+
if (this.props.fallback) {
|
|
36
|
+
return this.props.fallback;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<div className="flex min-h-[400px] flex-col items-center justify-center gap-4 p-8">
|
|
41
|
+
<div className="flex h-16 w-16 items-center justify-center rounded-full bg-destructive/10">
|
|
42
|
+
<AlertTriangle className="h-8 w-8 text-destructive" />
|
|
43
|
+
</div>
|
|
44
|
+
<div className="text-center">
|
|
45
|
+
<h2 className="text-xl font-semibold">Something went wrong</h2>
|
|
46
|
+
<p className="mt-2 text-sm text-muted-foreground">
|
|
47
|
+
{this.state.error?.message || 'An unexpected error occurred'}
|
|
48
|
+
</p>
|
|
49
|
+
</div>
|
|
50
|
+
<Button onClick={this.handleRetry} variant="outline" className="mt-4">
|
|
51
|
+
<RefreshCw className="mr-2 h-4 w-4" />
|
|
52
|
+
Try again
|
|
53
|
+
</Button>
|
|
54
|
+
</div>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return this.props.children;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Toaster } from 'react-hot-toast';
|
|
2
|
+
|
|
3
|
+
interface RootLayoutProps {
|
|
4
|
+
children: React.ReactNode;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function RootLayout({ children }: RootLayoutProps) {
|
|
8
|
+
return (
|
|
9
|
+
<div className="relative min-h-screen bg-background font-sans antialiased">
|
|
10
|
+
<main className="relative flex min-h-screen flex-col">{children}</main>
|
|
11
|
+
<Toaster
|
|
12
|
+
position="bottom-right"
|
|
13
|
+
toastOptions={{
|
|
14
|
+
duration: 4000,
|
|
15
|
+
style: {
|
|
16
|
+
background: 'hsl(var(--background))',
|
|
17
|
+
color: 'hsl(var(--foreground))',
|
|
18
|
+
border: '1px solid hsl(var(--border))',
|
|
19
|
+
},
|
|
20
|
+
success: {
|
|
21
|
+
iconTheme: {
|
|
22
|
+
primary: 'hsl(142.1 76.2% 36.3%)',
|
|
23
|
+
secondary: 'white',
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
error: {
|
|
27
|
+
iconTheme: {
|
|
28
|
+
primary: 'hsl(var(--destructive))',
|
|
29
|
+
secondary: 'white',
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
}}
|
|
33
|
+
/>
|
|
34
|
+
</div>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { Slot } from '@radix-ui/react-slot';
|
|
3
|
+
import { cva, type VariantProps } from 'class-variance-authority';
|
|
4
|
+
import { cn } from '~/lib/utils';
|
|
5
|
+
|
|
6
|
+
const buttonVariants = cva(
|
|
7
|
+
'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
|
|
8
|
+
{
|
|
9
|
+
variants: {
|
|
10
|
+
variant: {
|
|
11
|
+
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
|
|
12
|
+
destructive:
|
|
13
|
+
'bg-destructive text-destructive-foreground hover:bg-destructive/90',
|
|
14
|
+
outline:
|
|
15
|
+
'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
|
|
16
|
+
secondary:
|
|
17
|
+
'bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
|
18
|
+
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
|
19
|
+
link: 'text-primary underline-offset-4 hover:underline',
|
|
20
|
+
},
|
|
21
|
+
size: {
|
|
22
|
+
default: 'h-10 px-4 py-2',
|
|
23
|
+
sm: 'h-9 rounded-md px-3',
|
|
24
|
+
lg: 'h-11 rounded-md px-8',
|
|
25
|
+
icon: 'h-10 w-10',
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
defaultVariants: {
|
|
29
|
+
variant: 'default',
|
|
30
|
+
size: 'default',
|
|
31
|
+
},
|
|
32
|
+
}
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
export interface ButtonProps
|
|
36
|
+
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
|
37
|
+
VariantProps<typeof buttonVariants> {
|
|
38
|
+
asChild?: boolean;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
42
|
+
({ className, variant, size, asChild = false, ...props }, ref) => {
|
|
43
|
+
const Comp = asChild ? Slot : 'button';
|
|
44
|
+
return (
|
|
45
|
+
<Comp
|
|
46
|
+
className={cn(buttonVariants({ variant, size, className }))}
|
|
47
|
+
ref={ref}
|
|
48
|
+
{...props}
|
|
49
|
+
/>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
);
|
|
53
|
+
Button.displayName = 'Button';
|
|
54
|
+
|
|
55
|
+
export { Button, buttonVariants };
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { cn } from '~/lib/utils';
|
|
3
|
+
|
|
4
|
+
const Card = React.forwardRef<
|
|
5
|
+
HTMLDivElement,
|
|
6
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
7
|
+
>(({ className, ...props }, ref) => (
|
|
8
|
+
<div
|
|
9
|
+
ref={ref}
|
|
10
|
+
className={cn(
|
|
11
|
+
'rounded-lg border bg-card text-card-foreground shadow-sm',
|
|
12
|
+
className
|
|
13
|
+
)}
|
|
14
|
+
{...props}
|
|
15
|
+
/>
|
|
16
|
+
));
|
|
17
|
+
Card.displayName = 'Card';
|
|
18
|
+
|
|
19
|
+
const CardHeader = React.forwardRef<
|
|
20
|
+
HTMLDivElement,
|
|
21
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
22
|
+
>(({ className, ...props }, ref) => (
|
|
23
|
+
<div
|
|
24
|
+
ref={ref}
|
|
25
|
+
className={cn('flex flex-col space-y-1.5 p-6', className)}
|
|
26
|
+
{...props}
|
|
27
|
+
/>
|
|
28
|
+
));
|
|
29
|
+
CardHeader.displayName = 'CardHeader';
|
|
30
|
+
|
|
31
|
+
const CardTitle = React.forwardRef<
|
|
32
|
+
HTMLParagraphElement,
|
|
33
|
+
React.HTMLAttributes<HTMLHeadingElement>
|
|
34
|
+
>(({ className, ...props }, ref) => (
|
|
35
|
+
<h3
|
|
36
|
+
ref={ref}
|
|
37
|
+
className={cn(
|
|
38
|
+
'text-2xl font-semibold leading-none tracking-tight',
|
|
39
|
+
className
|
|
40
|
+
)}
|
|
41
|
+
{...props}
|
|
42
|
+
/>
|
|
43
|
+
));
|
|
44
|
+
CardTitle.displayName = 'CardTitle';
|
|
45
|
+
|
|
46
|
+
const CardDescription = React.forwardRef<
|
|
47
|
+
HTMLParagraphElement,
|
|
48
|
+
React.HTMLAttributes<HTMLParagraphElement>
|
|
49
|
+
>(({ className, ...props }, ref) => (
|
|
50
|
+
<p
|
|
51
|
+
ref={ref}
|
|
52
|
+
className={cn('text-sm text-muted-foreground', className)}
|
|
53
|
+
{...props}
|
|
54
|
+
/>
|
|
55
|
+
));
|
|
56
|
+
CardDescription.displayName = 'CardDescription';
|
|
57
|
+
|
|
58
|
+
const CardContent = React.forwardRef<
|
|
59
|
+
HTMLDivElement,
|
|
60
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
61
|
+
>(({ className, ...props }, ref) => (
|
|
62
|
+
<div ref={ref} className={cn('p-6 pt-0', className)} {...props} />
|
|
63
|
+
));
|
|
64
|
+
CardContent.displayName = 'CardContent';
|
|
65
|
+
|
|
66
|
+
const CardFooter = React.forwardRef<
|
|
67
|
+
HTMLDivElement,
|
|
68
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
69
|
+
>(({ className, ...props }, ref) => (
|
|
70
|
+
<div
|
|
71
|
+
ref={ref}
|
|
72
|
+
className={cn('flex items-center p-6 pt-0', className)}
|
|
73
|
+
{...props}
|
|
74
|
+
/>
|
|
75
|
+
));
|
|
76
|
+
CardFooter.displayName = 'CardFooter';
|
|
77
|
+
|
|
78
|
+
export {
|
|
79
|
+
Card,
|
|
80
|
+
CardHeader,
|
|
81
|
+
CardFooter,
|
|
82
|
+
CardTitle,
|
|
83
|
+
CardDescription,
|
|
84
|
+
CardContent,
|
|
85
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export { Button, buttonVariants, type ButtonProps } from './button';
|
|
2
|
+
export { Input, type InputProps } from './input';
|
|
3
|
+
export {
|
|
4
|
+
Card,
|
|
5
|
+
CardHeader,
|
|
6
|
+
CardFooter,
|
|
7
|
+
CardTitle,
|
|
8
|
+
CardDescription,
|
|
9
|
+
CardContent,
|
|
10
|
+
} from './card';
|
|
11
|
+
export { Skeleton } from './skeleton';
|
|
12
|
+
export { Separator } from './separator';
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { cn } from '~/lib/utils';
|
|
3
|
+
|
|
4
|
+
export interface InputProps
|
|
5
|
+
extends React.InputHTMLAttributes<HTMLInputElement> {}
|
|
6
|
+
|
|
7
|
+
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|
8
|
+
({ className, type, ...props }, ref) => {
|
|
9
|
+
return (
|
|
10
|
+
<input
|
|
11
|
+
type={type}
|
|
12
|
+
className={cn(
|
|
13
|
+
'flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
|
|
14
|
+
className
|
|
15
|
+
)}
|
|
16
|
+
ref={ref}
|
|
17
|
+
{...props}
|
|
18
|
+
/>
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
);
|
|
22
|
+
Input.displayName = 'Input';
|
|
23
|
+
|
|
24
|
+
export { Input };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { cn } from '~/lib/utils';
|
|
3
|
+
|
|
4
|
+
interface SeparatorProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
5
|
+
orientation?: 'horizontal' | 'vertical';
|
|
6
|
+
decorative?: boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const Separator = React.forwardRef<HTMLDivElement, SeparatorProps>(
|
|
10
|
+
(
|
|
11
|
+
{ className, orientation = 'horizontal', decorative = true, ...props },
|
|
12
|
+
ref
|
|
13
|
+
) => (
|
|
14
|
+
<div
|
|
15
|
+
ref={ref}
|
|
16
|
+
role={decorative ? 'none' : 'separator'}
|
|
17
|
+
aria-orientation={decorative ? undefined : orientation}
|
|
18
|
+
className={cn(
|
|
19
|
+
'shrink-0 bg-border',
|
|
20
|
+
orientation === 'horizontal' ? 'h-[1px] w-full' : 'h-full w-[1px]',
|
|
21
|
+
className
|
|
22
|
+
)}
|
|
23
|
+
{...props}
|
|
24
|
+
/>
|
|
25
|
+
)
|
|
26
|
+
);
|
|
27
|
+
Separator.displayName = 'Separator';
|
|
28
|
+
|
|
29
|
+
export { Separator };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { cn } from '~/lib/utils';
|
|
2
|
+
|
|
3
|
+
function Skeleton({
|
|
4
|
+
className,
|
|
5
|
+
...props
|
|
6
|
+
}: React.HTMLAttributes<HTMLDivElement>) {
|
|
7
|
+
return (
|
|
8
|
+
<div
|
|
9
|
+
className={cn('animate-pulse rounded-md bg-muted', className)}
|
|
10
|
+
{...props}
|
|
11
|
+
/>
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export { Skeleton };
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { useRef, useCallback, useEffect } from 'react';
|
|
2
|
+
import axios, { CancelTokenSource } from 'axios';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Hook for managing Axios request cancellation
|
|
6
|
+
* Automatically cancels pending requests on component unmount
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* const { cancelToken, cancel, reset } = useCancelToken();
|
|
10
|
+
*
|
|
11
|
+
* useEffect(() => {
|
|
12
|
+
* getApi('/users', {}, cancelToken)
|
|
13
|
+
* .then(setUsers)
|
|
14
|
+
* .catch(err => {
|
|
15
|
+
* if (!axios.isCancel(err)) {
|
|
16
|
+
* console.error(err);
|
|
17
|
+
* }
|
|
18
|
+
* });
|
|
19
|
+
*
|
|
20
|
+
* return () => cancel(); // Cancel on unmount
|
|
21
|
+
* }, []);
|
|
22
|
+
*/
|
|
23
|
+
export function useCancelToken() {
|
|
24
|
+
const sourceRef = useRef<CancelTokenSource | null>(null);
|
|
25
|
+
|
|
26
|
+
// Create a new cancel token
|
|
27
|
+
const getToken = useCallback(() => {
|
|
28
|
+
// Cancel previous request if exists
|
|
29
|
+
if (sourceRef.current) {
|
|
30
|
+
sourceRef.current.cancel('Operation cancelled due to new request');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
sourceRef.current = axios.CancelToken.source();
|
|
34
|
+
return sourceRef.current.token;
|
|
35
|
+
}, []);
|
|
36
|
+
|
|
37
|
+
// Cancel the current request
|
|
38
|
+
const cancel = useCallback((message?: string) => {
|
|
39
|
+
if (sourceRef.current) {
|
|
40
|
+
sourceRef.current.cancel(message || 'Operation cancelled by user');
|
|
41
|
+
sourceRef.current = null;
|
|
42
|
+
}
|
|
43
|
+
}, []);
|
|
44
|
+
|
|
45
|
+
// Reset the cancel token
|
|
46
|
+
const reset = useCallback(() => {
|
|
47
|
+
sourceRef.current = null;
|
|
48
|
+
}, []);
|
|
49
|
+
|
|
50
|
+
// Cancel on unmount
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
return () => {
|
|
53
|
+
cancel('Component unmounted');
|
|
54
|
+
};
|
|
55
|
+
}, [cancel]);
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
cancelToken: getToken(),
|
|
59
|
+
cancel,
|
|
60
|
+
reset,
|
|
61
|
+
isCancel: axios.isCancel,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Debounces a value by the specified delay
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* const [searchTerm, setSearchTerm] = useState('');
|
|
8
|
+
* const debouncedSearch = useDebounce(searchTerm, 500);
|
|
9
|
+
*
|
|
10
|
+
* useEffect(() => {
|
|
11
|
+
* // This only runs 500ms after user stops typing
|
|
12
|
+
* fetchSearchResults(debouncedSearch);
|
|
13
|
+
* }, [debouncedSearch]);
|
|
14
|
+
*/
|
|
15
|
+
export function useDebounce<T>(value: T, delay: number = 500): T {
|
|
16
|
+
const [debouncedValue, setDebouncedValue] = useState<T>(value);
|
|
17
|
+
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
const timer = setTimeout(() => {
|
|
20
|
+
setDebouncedValue(value);
|
|
21
|
+
}, delay);
|
|
22
|
+
|
|
23
|
+
return () => {
|
|
24
|
+
clearTimeout(timer);
|
|
25
|
+
};
|
|
26
|
+
}, [value, delay]);
|
|
27
|
+
|
|
28
|
+
return debouncedValue;
|
|
29
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Hook for managing loading states
|
|
5
|
+
* Returns a tuple of [isLoading, startLoader, endLoader]
|
|
6
|
+
*
|
|
7
|
+
* @param initialState - Initial loading state (default: false)
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* const [isLoading, startLoader, endLoader] = useLoader(false);
|
|
11
|
+
*
|
|
12
|
+
* const fetchData = async () => {
|
|
13
|
+
* startLoader();
|
|
14
|
+
* try {
|
|
15
|
+
* const data = await getApi('/users');
|
|
16
|
+
* setUsers(data);
|
|
17
|
+
* } finally {
|
|
18
|
+
* endLoader();
|
|
19
|
+
* }
|
|
20
|
+
* };
|
|
21
|
+
*
|
|
22
|
+
* return (
|
|
23
|
+
* <div>
|
|
24
|
+
* <button onClick={fetchData} disabled={isLoading}>
|
|
25
|
+
* {isLoading ? 'Loading...' : 'Fetch Data'}
|
|
26
|
+
* </button>
|
|
27
|
+
* </div>
|
|
28
|
+
* );
|
|
29
|
+
*/
|
|
30
|
+
export const useLoader = (
|
|
31
|
+
initialState: boolean = false
|
|
32
|
+
): [boolean, () => void, () => void] => {
|
|
33
|
+
const [loader, setLoader] = useState(initialState);
|
|
34
|
+
|
|
35
|
+
const startLoader = () => setLoader(true);
|
|
36
|
+
const endLoader = () => setLoader(false);
|
|
37
|
+
|
|
38
|
+
return [loader, startLoader, endLoader];
|
|
39
|
+
};
|