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.
Files changed (59) hide show
  1. package/README.md +270 -72
  2. package/bin/index.js +13 -13
  3. package/lib/install.js +103 -32
  4. package/lib/prompts.js +152 -179
  5. package/lib/setup.js +267 -159
  6. package/package.json +17 -8
  7. package/templates/base/.env.example +9 -0
  8. package/templates/base/.eslintrc.cjs +37 -0
  9. package/templates/base/.prettierrc +11 -0
  10. package/templates/base/components.json +17 -0
  11. package/templates/base/index.html +2 -1
  12. package/templates/base/package.json +33 -14
  13. package/templates/base/postcss.config.js +6 -0
  14. package/templates/base/src/App.tsx +5 -18
  15. package/templates/base/src/components/layout/error-boundary.tsx +60 -0
  16. package/templates/base/src/components/layout/index.ts +2 -0
  17. package/templates/base/src/components/layout/root-layout.tsx +36 -0
  18. package/templates/base/src/components/ui/button.tsx +55 -0
  19. package/templates/base/src/components/ui/card.tsx +85 -0
  20. package/templates/base/src/components/ui/index.ts +12 -0
  21. package/templates/base/src/components/ui/input.tsx +24 -0
  22. package/templates/base/src/components/ui/separator.tsx +29 -0
  23. package/templates/base/src/components/ui/skeleton.tsx +15 -0
  24. package/templates/base/src/hooks/index.ts +3 -0
  25. package/templates/base/src/hooks/use-cancel-token.ts +63 -0
  26. package/templates/base/src/hooks/use-debounce.ts +29 -0
  27. package/templates/base/src/hooks/use-loader.ts +39 -0
  28. package/templates/base/src/index.css +73 -60
  29. package/templates/base/src/lib/utils.ts +14 -0
  30. package/templates/base/src/main.tsx +6 -6
  31. package/templates/base/src/providers/index.tsx +27 -0
  32. package/templates/base/src/providers/theme-provider.tsx +92 -0
  33. package/templates/base/src/routes/index.tsx +40 -0
  34. package/templates/base/src/routes/routes.ts +36 -0
  35. package/templates/base/src/screens/home/index.tsx +132 -0
  36. package/templates/base/src/screens/not-found/index.tsx +29 -0
  37. package/templates/base/src/services/alertify-services.ts +133 -0
  38. package/templates/base/src/services/api/api-helpers.ts +130 -0
  39. package/templates/base/src/services/api/axios-instance.ts +77 -0
  40. package/templates/base/src/services/api/index.ts +9 -0
  41. package/templates/base/src/services/index.ts +2 -0
  42. package/templates/base/src/types/index.ts +55 -0
  43. package/templates/base/src/vite-env.d.ts +31 -0
  44. package/templates/base/tailwind.config.js +77 -0
  45. package/templates/base/tsconfig.json +4 -3
  46. package/templates/base/tsconfig.node.json +22 -0
  47. package/templates/base/vite.config.ts +65 -4
  48. package/templates/optional/antd/config-provider.tsx +33 -0
  49. package/templates/optional/antd/index.ts +2 -0
  50. package/templates/optional/antd/styles/antd-overrides.css +104 -0
  51. package/templates/optional/antd/theme.ts +75 -0
  52. package/templates/optional/husky/.husky/pre-commit +1 -0
  53. package/templates/optional/husky/.lintstagedrc.json +6 -0
  54. package/templates/optional/redux/hooks.ts +17 -0
  55. package/templates/optional/redux/index.ts +13 -0
  56. package/templates/optional/redux/provider.tsx +33 -0
  57. package/templates/optional/redux/store/index.ts +45 -0
  58. package/templates/optional/redux/store/slices/app-slice.ts +62 -0
  59. package/templates/base/src/App.css +0 -14
@@ -1,70 +1,83 @@
1
- :root {
2
- font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
3
- line-height: 1.5;
4
- font-weight: 400;
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
5
4
 
6
- color-scheme: light dark;
7
- color: rgba(255, 255, 255, 0.87);
8
- background-color: #242424;
9
-
10
- font-synthesis: none;
11
- text-rendering: optimizeLegibility;
12
- -webkit-font-smoothing: antialiased;
13
- -moz-osx-font-smoothing: grayscale;
14
- -webkit-text-size-adjust: 100%;
15
- }
16
-
17
- a {
18
- font-weight: 500;
19
- color: #646cff;
20
- text-decoration: inherit;
21
- }
22
- a:hover {
23
- color: #535bf2;
24
- }
25
-
26
- body {
27
- margin: 0;
28
- display: flex;
29
- place-items: center;
30
- min-width: 320px;
31
- min-height: 100vh;
32
- }
5
+ @layer base {
6
+ :root {
7
+ --background: 0 0% 100%;
8
+ --foreground: 222.2 84% 4.9%;
9
+ --card: 0 0% 100%;
10
+ --card-foreground: 222.2 84% 4.9%;
11
+ --popover: 0 0% 100%;
12
+ --popover-foreground: 222.2 84% 4.9%;
13
+ --primary: 222.2 47.4% 11.2%;
14
+ --primary-foreground: 210 40% 98%;
15
+ --secondary: 210 40% 96.1%;
16
+ --secondary-foreground: 222.2 47.4% 11.2%;
17
+ --muted: 210 40% 96.1%;
18
+ --muted-foreground: 215.4 16.3% 46.9%;
19
+ --accent: 210 40% 96.1%;
20
+ --accent-foreground: 222.2 47.4% 11.2%;
21
+ --destructive: 0 84.2% 60.2%;
22
+ --destructive-foreground: 210 40% 98%;
23
+ --border: 214.3 31.8% 91.4%;
24
+ --input: 214.3 31.8% 91.4%;
25
+ --ring: 222.2 84% 4.9%;
26
+ --radius: 0.5rem;
27
+ }
33
28
 
34
- h1 {
35
- font-size: 3.2em;
36
- line-height: 1.1;
29
+ .dark {
30
+ --background: 222.2 84% 4.9%;
31
+ --foreground: 210 40% 98%;
32
+ --card: 222.2 84% 4.9%;
33
+ --card-foreground: 210 40% 98%;
34
+ --popover: 222.2 84% 4.9%;
35
+ --popover-foreground: 210 40% 98%;
36
+ --primary: 210 40% 98%;
37
+ --primary-foreground: 222.2 47.4% 11.2%;
38
+ --secondary: 217.2 32.6% 17.5%;
39
+ --secondary-foreground: 210 40% 98%;
40
+ --muted: 217.2 32.6% 17.5%;
41
+ --muted-foreground: 215 20.2% 65.1%;
42
+ --accent: 217.2 32.6% 17.5%;
43
+ --accent-foreground: 210 40% 98%;
44
+ --destructive: 0 62.8% 30.6%;
45
+ --destructive-foreground: 210 40% 98%;
46
+ --border: 217.2 32.6% 17.5%;
47
+ --input: 217.2 32.6% 17.5%;
48
+ --ring: 212.7 26.8% 83.9%;
49
+ }
37
50
  }
38
51
 
39
- button {
40
- border-radius: 8px;
41
- border: 1px solid transparent;
42
- padding: 0.6em 1.2em;
43
- font-size: 1em;
44
- font-weight: 500;
45
- font-family: inherit;
46
- background-color: #1a1a1a;
47
- color: inherit;
48
- cursor: pointer;
49
- transition: border-color 0.25s;
50
- }
51
- button:hover {
52
- border-color: #646cff;
53
- }
54
- button:focus,
55
- button:focus-visible {
56
- outline: 4px auto -webkit-focus-ring-color;
52
+ @layer base {
53
+ * {
54
+ @apply border-border;
55
+ }
56
+ body {
57
+ @apply bg-background text-foreground;
58
+ font-feature-settings:
59
+ 'rlig' 1,
60
+ 'calt' 1;
61
+ }
57
62
  }
58
63
 
59
- @media (prefers-color-scheme: light) {
60
- :root {
61
- color: #213547;
62
- background-color: #ffffff;
64
+ /* Custom scrollbar */
65
+ @layer utilities {
66
+ .scrollbar-thin {
67
+ scrollbar-width: thin;
68
+ }
69
+ .scrollbar-thin::-webkit-scrollbar {
70
+ width: 6px;
71
+ height: 6px;
72
+ }
73
+ .scrollbar-thin::-webkit-scrollbar-track {
74
+ background: transparent;
63
75
  }
64
- a:hover {
65
- color: #747bff;
76
+ .scrollbar-thin::-webkit-scrollbar-thumb {
77
+ background-color: hsl(var(--muted-foreground) / 0.3);
78
+ border-radius: 3px;
66
79
  }
67
- button {
68
- background-color: #f9f9f9;
80
+ .scrollbar-thin::-webkit-scrollbar-thumb:hover {
81
+ background-color: hsl(var(--muted-foreground) / 0.5);
69
82
  }
70
83
  }
@@ -0,0 +1,14 @@
1
+ import { clsx, type ClassValue } from 'clsx';
2
+ import { twMerge } from 'tailwind-merge';
3
+
4
+ /**
5
+ * Combines class names using clsx and tailwind-merge
6
+ * This ensures Tailwind classes are properly merged without conflicts
7
+ *
8
+ * @example
9
+ * cn('px-2 py-1', condition && 'bg-blue-500', 'px-4')
10
+ * // Returns 'py-1 bg-blue-500 px-4' (px-4 overrides px-2)
11
+ */
12
+ export function cn(...inputs: ClassValue[]) {
13
+ return twMerge(clsx(inputs));
14
+ }
@@ -1,10 +1,10 @@
1
- import React from "react";
2
- import ReactDOM from "react-dom/client";
3
- import App from "./App.tsx";
4
- import "./index.css";
1
+ import React from 'react';
2
+ import ReactDOM from 'react-dom/client';
3
+ import App from './App';
4
+ import './index.css';
5
5
 
6
- ReactDOM.createRoot(document.getElementById("root")!).render(
6
+ ReactDOM.createRoot(document.getElementById('root')!).render(
7
7
  <React.StrictMode>
8
8
  <App />
9
- </React.StrictMode>,
9
+ </React.StrictMode>
10
10
  );
@@ -0,0 +1,27 @@
1
+ import { type ReactNode } from 'react';
2
+ import { ThemeProvider } from './theme-provider';
3
+ import { RootLayout } from '~/components/layout';
4
+ import { ErrorBoundary } from '~/components/layout';
5
+
6
+ interface ProvidersProps {
7
+ children: ReactNode;
8
+ }
9
+
10
+ /**
11
+ * Application providers composition
12
+ * Wraps the app with all necessary context providers
13
+ *
14
+ * Order matters! Providers at the top are available to all children.
15
+ * Add new providers here when needed (e.g., Redux, Auth, etc.)
16
+ */
17
+ export function Providers({ children }: ProvidersProps) {
18
+ return (
19
+ <ErrorBoundary>
20
+ <ThemeProvider defaultTheme="system" storageKey="app-theme">
21
+ <RootLayout>{children}</RootLayout>
22
+ </ThemeProvider>
23
+ </ErrorBoundary>
24
+ );
25
+ }
26
+
27
+ export { ThemeProvider, useTheme } from './theme-provider';
@@ -0,0 +1,92 @@
1
+ import {
2
+ createContext,
3
+ useContext,
4
+ useEffect,
5
+ useState,
6
+ type ReactNode,
7
+ } from 'react';
8
+
9
+ type Theme = 'dark' | 'light' | 'system';
10
+
11
+ interface ThemeContextType {
12
+ theme: Theme;
13
+ setTheme: (theme: Theme) => void;
14
+ resolvedTheme: 'dark' | 'light';
15
+ }
16
+
17
+ const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
18
+
19
+ interface ThemeProviderProps {
20
+ children: ReactNode;
21
+ defaultTheme?: Theme;
22
+ storageKey?: string;
23
+ }
24
+
25
+ export function ThemeProvider({
26
+ children,
27
+ defaultTheme = 'system',
28
+ storageKey = 'app-theme',
29
+ }: ThemeProviderProps) {
30
+ const [theme, setThemeState] = useState<Theme>(() => {
31
+ if (typeof window !== 'undefined') {
32
+ return (localStorage.getItem(storageKey) as Theme) || defaultTheme;
33
+ }
34
+ return defaultTheme;
35
+ });
36
+
37
+ const [resolvedTheme, setResolvedTheme] = useState<'dark' | 'light'>('light');
38
+
39
+ useEffect(() => {
40
+ const root = window.document.documentElement;
41
+ root.classList.remove('light', 'dark');
42
+
43
+ let effectiveTheme: 'dark' | 'light';
44
+
45
+ if (theme === 'system') {
46
+ effectiveTheme = window.matchMedia('(prefers-color-scheme: dark)').matches
47
+ ? 'dark'
48
+ : 'light';
49
+ } else {
50
+ effectiveTheme = theme;
51
+ }
52
+
53
+ root.classList.add(effectiveTheme);
54
+ setResolvedTheme(effectiveTheme);
55
+ }, [theme]);
56
+
57
+ // Listen for system theme changes
58
+ useEffect(() => {
59
+ if (theme !== 'system') return;
60
+
61
+ const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
62
+ const handleChange = (e: MediaQueryListEvent) => {
63
+ const root = window.document.documentElement;
64
+ root.classList.remove('light', 'dark');
65
+ const newTheme = e.matches ? 'dark' : 'light';
66
+ root.classList.add(newTheme);
67
+ setResolvedTheme(newTheme);
68
+ };
69
+
70
+ mediaQuery.addEventListener('change', handleChange);
71
+ return () => mediaQuery.removeEventListener('change', handleChange);
72
+ }, [theme]);
73
+
74
+ const setTheme = (newTheme: Theme) => {
75
+ localStorage.setItem(storageKey, newTheme);
76
+ setThemeState(newTheme);
77
+ };
78
+
79
+ return (
80
+ <ThemeContext.Provider value={{ theme, setTheme, resolvedTheme }}>
81
+ {children}
82
+ </ThemeContext.Provider>
83
+ );
84
+ }
85
+
86
+ export function useTheme() {
87
+ const context = useContext(ThemeContext);
88
+ if (context === undefined) {
89
+ throw new Error('useTheme must be used within a ThemeProvider');
90
+ }
91
+ return context;
92
+ }
@@ -0,0 +1,40 @@
1
+ import { Suspense } from 'react';
2
+ import { Route, Switch } from 'wouter';
3
+ import { routes } from './routes';
4
+ import { Skeleton } from '~/components/ui';
5
+
6
+ /**
7
+ * Loading fallback component
8
+ */
9
+ function RouteLoading() {
10
+ return (
11
+ <div className="flex min-h-screen items-center justify-center">
12
+ <div className="flex flex-col items-center gap-4">
13
+ <Skeleton className="h-12 w-12 rounded-full" />
14
+ <Skeleton className="h-4 w-32" />
15
+ </div>
16
+ </div>
17
+ );
18
+ }
19
+
20
+ /**
21
+ * Application router using Wouter
22
+ * - Lightweight (2KB) alternative to React Router
23
+ * - Supports lazy loading with React.Suspense
24
+ * - Simple API with <Route> and <Switch>
25
+ */
26
+ export function AppRouter() {
27
+ return (
28
+ <Suspense fallback={<RouteLoading />}>
29
+ <Switch>
30
+ {routes.map(({ path, component: Component }) => (
31
+ <Route key={path} path={path}>
32
+ <Component />
33
+ </Route>
34
+ ))}
35
+ </Switch>
36
+ </Suspense>
37
+ );
38
+ }
39
+
40
+ export { routes } from './routes';
@@ -0,0 +1,36 @@
1
+ import { lazy } from 'react';
2
+
3
+ // Lazy-loaded screens
4
+ const Home = lazy(() => import('~/screens/home'));
5
+ const NotFound = lazy(() => import('~/screens/not-found'));
6
+
7
+ export interface RouteConfig {
8
+ path: string;
9
+ component: React.LazyExoticComponent<React.ComponentType>;
10
+ title: string;
11
+ isPrivate?: boolean;
12
+ }
13
+
14
+ /**
15
+ * Application routes configuration
16
+ * Add new routes here - they'll automatically be registered in the router
17
+ */
18
+ export const routes: RouteConfig[] = [
19
+ {
20
+ path: '/',
21
+ component: Home,
22
+ title: 'Home',
23
+ },
24
+ {
25
+ path: '*',
26
+ component: NotFound,
27
+ title: 'Not Found',
28
+ },
29
+ ];
30
+
31
+ /**
32
+ * Get route by path
33
+ */
34
+ export function getRouteByPath(path: string): RouteConfig | undefined {
35
+ return routes.find((route) => route.path === path);
36
+ }
@@ -0,0 +1,132 @@
1
+ import { useState } from 'react';
2
+ import { Moon, Sun, Github, Zap } from 'lucide-react';
3
+ import { Button, Card, CardContent, CardHeader, CardTitle } from '~/components/ui';
4
+ import { useTheme } from '~/providers';
5
+
6
+ export default function Home() {
7
+ const { theme, setTheme, resolvedTheme } = useTheme();
8
+ const [count, setCount] = useState(0);
9
+
10
+ const toggleTheme = () => {
11
+ setTheme(resolvedTheme === 'dark' ? 'light' : 'dark');
12
+ };
13
+
14
+ return (
15
+ <div className="flex min-h-screen flex-col items-center justify-center p-8">
16
+ <div className="w-full max-w-2xl space-y-8">
17
+ {/* Header */}
18
+ <div className="text-center">
19
+ <div className="mb-4 flex justify-center">
20
+ <div className="flex h-16 w-16 items-center justify-center rounded-2xl bg-primary text-primary-foreground">
21
+ <Zap className="h-8 w-8" />
22
+ </div>
23
+ </div>
24
+ <h1 className="text-4xl font-bold tracking-tight">
25
+ create-modern-react
26
+ </h1>
27
+ <p className="mt-2 text-muted-foreground">
28
+ Production-ready React + TypeScript + Tailwind in seconds
29
+ </p>
30
+ </div>
31
+
32
+ {/* Counter Card */}
33
+ <Card>
34
+ <CardHeader>
35
+ <CardTitle className="flex items-center justify-between">
36
+ <span>Interactive Counter</span>
37
+ <Button
38
+ variant="ghost"
39
+ size="icon"
40
+ onClick={toggleTheme}
41
+ aria-label="Toggle theme"
42
+ >
43
+ {resolvedTheme === 'dark' ? (
44
+ <Sun className="h-5 w-5" />
45
+ ) : (
46
+ <Moon className="h-5 w-5" />
47
+ )}
48
+ </Button>
49
+ </CardTitle>
50
+ </CardHeader>
51
+ <CardContent className="space-y-4">
52
+ <div className="flex items-center justify-center gap-4">
53
+ <Button
54
+ variant="outline"
55
+ size="lg"
56
+ onClick={() => setCount((c) => c - 1)}
57
+ >
58
+ -
59
+ </Button>
60
+ <span className="min-w-[4rem] text-center text-4xl font-bold tabular-nums">
61
+ {count}
62
+ </span>
63
+ <Button
64
+ variant="outline"
65
+ size="lg"
66
+ onClick={() => setCount((c) => c + 1)}
67
+ >
68
+ +
69
+ </Button>
70
+ </div>
71
+ <p className="text-center text-sm text-muted-foreground">
72
+ Click the buttons to update the count
73
+ </p>
74
+ </CardContent>
75
+ </Card>
76
+
77
+ {/* Features */}
78
+ <div className="grid gap-4 sm:grid-cols-2">
79
+ <FeatureCard
80
+ title="Vite + SWC"
81
+ description="Lightning fast builds with Hot Module Replacement"
82
+ />
83
+ <FeatureCard
84
+ title="TypeScript"
85
+ description="Full type safety with strict mode enabled"
86
+ />
87
+ <FeatureCard
88
+ title="Tailwind CSS"
89
+ description="Utility-first CSS with dark mode support"
90
+ />
91
+ <FeatureCard
92
+ title="Shadcn/ui"
93
+ description="Beautiful, accessible components"
94
+ />
95
+ </div>
96
+
97
+ {/* Footer */}
98
+ <div className="flex justify-center">
99
+ <Button variant="outline" asChild>
100
+ <a
101
+ href="https://github.com"
102
+ target="_blank"
103
+ rel="noopener noreferrer"
104
+ >
105
+ <Github className="mr-2 h-4 w-4" />
106
+ View on GitHub
107
+ </a>
108
+ </Button>
109
+ </div>
110
+
111
+ <p className="text-center text-xs text-muted-foreground">
112
+ Current theme: {theme} (resolved: {resolvedTheme})
113
+ </p>
114
+ </div>
115
+ </div>
116
+ );
117
+ }
118
+
119
+ function FeatureCard({
120
+ title,
121
+ description,
122
+ }: {
123
+ title: string;
124
+ description: string;
125
+ }) {
126
+ return (
127
+ <div className="rounded-lg border bg-card p-4 text-card-foreground">
128
+ <h3 className="font-semibold">{title}</h3>
129
+ <p className="mt-1 text-sm text-muted-foreground">{description}</p>
130
+ </div>
131
+ );
132
+ }
@@ -0,0 +1,29 @@
1
+ import { Link } from 'wouter';
2
+ import { Home, ArrowLeft } from 'lucide-react';
3
+ import { Button } from '~/components/ui';
4
+
5
+ export default function NotFound() {
6
+ return (
7
+ <div className="flex min-h-screen flex-col items-center justify-center p-8">
8
+ <div className="text-center">
9
+ <h1 className="text-9xl font-bold text-muted-foreground/20">404</h1>
10
+ <h2 className="mt-4 text-2xl font-semibold">Page not found</h2>
11
+ <p className="mt-2 text-muted-foreground">
12
+ Sorry, we couldn't find the page you're looking for.
13
+ </p>
14
+ <div className="mt-8 flex flex-col items-center gap-4 sm:flex-row sm:justify-center">
15
+ <Button asChild>
16
+ <Link href="/">
17
+ <Home className="mr-2 h-4 w-4" />
18
+ Go home
19
+ </Link>
20
+ </Button>
21
+ <Button variant="outline" onClick={() => window.history.back()}>
22
+ <ArrowLeft className="mr-2 h-4 w-4" />
23
+ Go back
24
+ </Button>
25
+ </div>
26
+ </div>
27
+ </div>
28
+ );
29
+ }
@@ -0,0 +1,133 @@
1
+ import toast from 'react-hot-toast';
2
+
3
+ type ToastPosition =
4
+ | 'top-left'
5
+ | 'top-center'
6
+ | 'top-right'
7
+ | 'bottom-left'
8
+ | 'bottom-center'
9
+ | 'bottom-right';
10
+
11
+ // Track the current toast ID to dismiss before showing new one
12
+ let currentToastId: string | undefined;
13
+
14
+ /**
15
+ * Alertify Service - Wrapper around react-hot-toast
16
+ * Auto-dismisses previous toast before showing new one
17
+ *
18
+ * @example
19
+ * Alertify.success('User created successfully');
20
+ * Alertify.error('Failed to save changes');
21
+ * Alertify.info('Please wait...');
22
+ */
23
+ export const Alertify = {
24
+ /**
25
+ * Show a success toast
26
+ */
27
+ success(message: string, position: ToastPosition = 'bottom-right') {
28
+ if (currentToastId) {
29
+ toast.dismiss(currentToastId);
30
+ }
31
+ currentToastId = toast.success(message, { position });
32
+ return currentToastId;
33
+ },
34
+
35
+ /**
36
+ * Show an error toast
37
+ */
38
+ error(message: string, position: ToastPosition = 'bottom-right') {
39
+ if (currentToastId) {
40
+ toast.dismiss(currentToastId);
41
+ }
42
+ currentToastId = toast.error(message, { position });
43
+ return currentToastId;
44
+ },
45
+
46
+ /**
47
+ * Show an info toast (default style)
48
+ */
49
+ info(message: string, position: ToastPosition = 'bottom-right') {
50
+ if (currentToastId) {
51
+ toast.dismiss(currentToastId);
52
+ }
53
+ currentToastId = toast(message, {
54
+ position,
55
+ icon: 'ℹ️',
56
+ });
57
+ return currentToastId;
58
+ },
59
+
60
+ /**
61
+ * Show a loading toast (returns a function to update/dismiss it)
62
+ */
63
+ loading(message: string, position: ToastPosition = 'bottom-right') {
64
+ if (currentToastId) {
65
+ toast.dismiss(currentToastId);
66
+ }
67
+ currentToastId = toast.loading(message, { position });
68
+ return {
69
+ id: currentToastId,
70
+ success: (successMessage: string) => {
71
+ toast.success(successMessage, { id: currentToastId });
72
+ },
73
+ error: (errorMessage: string) => {
74
+ toast.error(errorMessage, { id: currentToastId });
75
+ },
76
+ dismiss: () => {
77
+ toast.dismiss(currentToastId);
78
+ },
79
+ };
80
+ },
81
+
82
+ /**
83
+ * Show a custom toast with action button
84
+ */
85
+ withAction(
86
+ message: string,
87
+ actionText: string,
88
+ onAction: () => void,
89
+ position: ToastPosition = 'bottom-right'
90
+ ) {
91
+ if (currentToastId) {
92
+ toast.dismiss(currentToastId);
93
+ }
94
+ currentToastId = toast(
95
+ (t) => (
96
+ <div className="flex items-center gap-2">
97
+ <span>{message}</span>
98
+ <button
99
+ onClick={() => {
100
+ onAction();
101
+ toast.dismiss(t.id);
102
+ }}
103
+ className="rounded bg-primary px-2 py-1 text-xs font-medium text-primary-foreground hover:bg-primary/90"
104
+ >
105
+ {actionText}
106
+ </button>
107
+ </div>
108
+ ),
109
+ { position, duration: 5000 }
110
+ );
111
+ return currentToastId;
112
+ },
113
+
114
+ /**
115
+ * Dismiss all toasts
116
+ */
117
+ dismissAll() {
118
+ toast.dismiss();
119
+ currentToastId = undefined;
120
+ },
121
+
122
+ /**
123
+ * Dismiss a specific toast
124
+ */
125
+ dismiss(toastId?: string) {
126
+ toast.dismiss(toastId || currentToastId);
127
+ if (!toastId || toastId === currentToastId) {
128
+ currentToastId = undefined;
129
+ }
130
+ },
131
+ };
132
+
133
+ export default Alertify;