create-vite-redux 1.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 (38) hide show
  1. package/README.md +98 -0
  2. package/index.js +169 -0
  3. package/package.json +30 -0
  4. package/templates/react-redux-tw/.env.example +1 -0
  5. package/templates/react-redux-tw/.prettierignore +2 -0
  6. package/templates/react-redux-tw/.prettierrc +8 -0
  7. package/templates/react-redux-tw/README.md +81 -0
  8. package/templates/react-redux-tw/_gitignore +31 -0
  9. package/templates/react-redux-tw/_package.json +49 -0
  10. package/templates/react-redux-tw/eslint.config.js +25 -0
  11. package/templates/react-redux-tw/index.html +13 -0
  12. package/templates/react-redux-tw/src/App.tsx +15 -0
  13. package/templates/react-redux-tw/src/components/layout/Footer.tsx +11 -0
  14. package/templates/react-redux-tw/src/components/layout/Header.tsx +16 -0
  15. package/templates/react-redux-tw/src/components/ui/button.tsx +54 -0
  16. package/templates/react-redux-tw/src/components/ui/card.tsx +73 -0
  17. package/templates/react-redux-tw/src/components/ui/dialog.tsx +118 -0
  18. package/templates/react-redux-tw/src/components/ui/form.tsx +138 -0
  19. package/templates/react-redux-tw/src/components/ui/input.tsx +18 -0
  20. package/templates/react-redux-tw/src/components/ui/label.tsx +18 -0
  21. package/templates/react-redux-tw/src/components/ui/sonner.tsx +19 -0
  22. package/templates/react-redux-tw/src/features/auth/LoginForm.tsx +92 -0
  23. package/templates/react-redux-tw/src/features/auth/authApi.ts +40 -0
  24. package/templates/react-redux-tw/src/features/auth/authSchema.ts +19 -0
  25. package/templates/react-redux-tw/src/features/auth/authSlice.ts +31 -0
  26. package/templates/react-redux-tw/src/features/counter/Counter.tsx +34 -0
  27. package/templates/react-redux-tw/src/features/counter/counterSlice.ts +32 -0
  28. package/templates/react-redux-tw/src/hooks/redux.ts +5 -0
  29. package/templates/react-redux-tw/src/index.css +123 -0
  30. package/templates/react-redux-tw/src/lib/utils.ts +6 -0
  31. package/templates/react-redux-tw/src/main.tsx +17 -0
  32. package/templates/react-redux-tw/src/router/index.tsx +26 -0
  33. package/templates/react-redux-tw/src/services/api.ts +10 -0
  34. package/templates/react-redux-tw/src/store/index.ts +16 -0
  35. package/templates/react-redux-tw/tsconfig.app.json +32 -0
  36. package/templates/react-redux-tw/tsconfig.json +7 -0
  37. package/templates/react-redux-tw/tsconfig.node.json +24 -0
  38. package/templates/react-redux-tw/vite.config.ts +17 -0
@@ -0,0 +1,73 @@
1
+ import * as React from 'react'
2
+ import { cn } from '@/lib/utils'
3
+
4
+ function Card({ className, ...props }: React.ComponentProps<'div'>) {
5
+ return (
6
+ <div
7
+ data-slot="card"
8
+ className={cn('bg-card text-card-foreground rounded-xl border shadow-sm', className)}
9
+ {...props}
10
+ />
11
+ )
12
+ }
13
+
14
+ function CardHeader({ className, ...props }: React.ComponentProps<'div'>) {
15
+ return (
16
+ <div
17
+ data-slot="card-header"
18
+ className={cn(
19
+ '@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 pt-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6',
20
+ className
21
+ )}
22
+ {...props}
23
+ />
24
+ )
25
+ }
26
+
27
+ function CardTitle({ className, ...props }: React.ComponentProps<'div'>) {
28
+ return (
29
+ <div
30
+ data-slot="card-title"
31
+ className={cn('leading-none font-semibold', className)}
32
+ {...props}
33
+ />
34
+ )
35
+ }
36
+
37
+ function CardDescription({ className, ...props }: React.ComponentProps<'div'>) {
38
+ return (
39
+ <div
40
+ data-slot="card-description"
41
+ className={cn('text-muted-foreground text-sm', className)}
42
+ {...props}
43
+ />
44
+ )
45
+ }
46
+
47
+ function CardAction({ className, ...props }: React.ComponentProps<'div'>) {
48
+ return (
49
+ <div
50
+ data-slot="card-action"
51
+ className={cn('col-start-2 row-span-2 row-start-1 self-start justify-self-end', className)}
52
+ {...props}
53
+ />
54
+ )
55
+ }
56
+
57
+ function CardContent({ className, ...props }: React.ComponentProps<'div'>) {
58
+ return (
59
+ <div data-slot="card-content" className={cn('px-6 pb-6 pt-0', className)} {...props} />
60
+ )
61
+ }
62
+
63
+ function CardFooter({ className, ...props }: React.ComponentProps<'div'>) {
64
+ return (
65
+ <div
66
+ data-slot="card-footer"
67
+ className={cn('flex items-center px-6 pb-6 pt-0', className)}
68
+ {...props}
69
+ />
70
+ )
71
+ }
72
+
73
+ export { Card, CardHeader, CardFooter, CardTitle, CardAction, CardDescription, CardContent }
@@ -0,0 +1,118 @@
1
+ import * as React from 'react'
2
+ import * as DialogPrimitive from '@radix-ui/react-dialog'
3
+ import { XIcon } from 'lucide-react'
4
+ import { cn } from '@/lib/utils'
5
+
6
+ function Dialog({ ...props }: React.ComponentProps<typeof DialogPrimitive.Root>) {
7
+ return <DialogPrimitive.Root data-slot="dialog" {...props} />
8
+ }
9
+
10
+ function DialogTrigger({ ...props }: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
11
+ return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
12
+ }
13
+
14
+ function DialogPortal({ ...props }: React.ComponentProps<typeof DialogPrimitive.Portal>) {
15
+ return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
16
+ }
17
+
18
+ function DialogClose({ ...props }: React.ComponentProps<typeof DialogPrimitive.Close>) {
19
+ return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
20
+ }
21
+
22
+ function DialogOverlay({
23
+ className,
24
+ ...props
25
+ }: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
26
+ return (
27
+ <DialogPrimitive.Overlay
28
+ data-slot="dialog-overlay"
29
+ className={cn(
30
+ 'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50',
31
+ className
32
+ )}
33
+ {...props}
34
+ />
35
+ )
36
+ }
37
+
38
+ function DialogContent({
39
+ className,
40
+ children,
41
+ ...props
42
+ }: React.ComponentProps<typeof DialogPrimitive.Content>) {
43
+ return (
44
+ <DialogPortal>
45
+ <DialogOverlay />
46
+ <DialogPrimitive.Content
47
+ data-slot="dialog-content"
48
+ className={cn(
49
+ 'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-xl border p-6 shadow-lg duration-200 sm:max-w-lg',
50
+ className
51
+ )}
52
+ {...props}
53
+ >
54
+ {children}
55
+ <DialogPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=size-])]:size-4">
56
+ <XIcon />
57
+ <span className="sr-only">Close</span>
58
+ </DialogPrimitive.Close>
59
+ </DialogPrimitive.Content>
60
+ </DialogPortal>
61
+ )
62
+ }
63
+
64
+ function DialogHeader({ className, ...props }: React.ComponentProps<'div'>) {
65
+ return (
66
+ <div
67
+ data-slot="dialog-header"
68
+ className={cn('flex flex-col gap-2 text-center sm:text-left', className)}
69
+ {...props}
70
+ />
71
+ )
72
+ }
73
+
74
+ function DialogFooter({ className, ...props }: React.ComponentProps<'div'>) {
75
+ return (
76
+ <div
77
+ data-slot="dialog-footer"
78
+ className={cn('flex flex-col-reverse gap-2 sm:flex-row sm:justify-end', className)}
79
+ {...props}
80
+ />
81
+ )
82
+ }
83
+
84
+ function DialogTitle({ className, ...props }: React.ComponentProps<typeof DialogPrimitive.Title>) {
85
+ return (
86
+ <DialogPrimitive.Title
87
+ data-slot="dialog-title"
88
+ className={cn('text-lg leading-none font-semibold', className)}
89
+ {...props}
90
+ />
91
+ )
92
+ }
93
+
94
+ function DialogDescription({
95
+ className,
96
+ ...props
97
+ }: React.ComponentProps<typeof DialogPrimitive.Description>) {
98
+ return (
99
+ <DialogPrimitive.Description
100
+ data-slot="dialog-description"
101
+ className={cn('text-muted-foreground text-sm', className)}
102
+ {...props}
103
+ />
104
+ )
105
+ }
106
+
107
+ export {
108
+ Dialog,
109
+ DialogClose,
110
+ DialogContent,
111
+ DialogDescription,
112
+ DialogFooter,
113
+ DialogHeader,
114
+ DialogOverlay,
115
+ DialogPortal,
116
+ DialogTitle,
117
+ DialogTrigger,
118
+ }
@@ -0,0 +1,138 @@
1
+ import * as React from 'react'
2
+ import * as LabelPrimitive from '@radix-ui/react-label'
3
+ import { Slot } from '@radix-ui/react-slot'
4
+ import { Controller, FormProvider, useFormContext, useFormState } from 'react-hook-form'
5
+ import type {
6
+ ControllerProps,
7
+ FieldPath,
8
+ FieldValues,
9
+ } from 'react-hook-form'
10
+ import { cn } from '@/lib/utils'
11
+ import { Label } from '@/components/ui/label'
12
+
13
+ const Form = FormProvider
14
+
15
+ type FormFieldContextValue<
16
+ TFieldValues extends FieldValues = FieldValues,
17
+ TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
18
+ > = {
19
+ name: TName
20
+ }
21
+
22
+ const FormFieldContext = React.createContext<FormFieldContextValue>({} as FormFieldContextValue)
23
+
24
+ function FormField<
25
+ TFieldValues extends FieldValues = FieldValues,
26
+ TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
27
+ >({ ...props }: ControllerProps<TFieldValues, TName>) {
28
+ return (
29
+ <FormFieldContext.Provider value={{ name: props.name }}>
30
+ <Controller {...props} />
31
+ </FormFieldContext.Provider>
32
+ )
33
+ }
34
+
35
+ function useFormField() {
36
+ const fieldContext = React.useContext(FormFieldContext)
37
+ const itemContext = React.useContext(FormItemContext)
38
+ const { getFieldState } = useFormContext()
39
+ const formState = useFormState({ name: fieldContext.name })
40
+ const fieldState = getFieldState(fieldContext.name, formState)
41
+
42
+ if (!fieldContext) {
43
+ throw new Error('useFormField should be used within <FormField>')
44
+ }
45
+
46
+ const { id } = itemContext
47
+
48
+ return {
49
+ id,
50
+ name: fieldContext.name,
51
+ formItemId: `${id}-form-item`,
52
+ formDescriptionId: `${id}-form-item-description`,
53
+ formMessageId: `${id}-form-item-message`,
54
+ ...fieldState,
55
+ }
56
+ }
57
+
58
+ type FormItemContextValue = {
59
+ id: string
60
+ }
61
+
62
+ const FormItemContext = React.createContext<FormItemContextValue>({} as FormItemContextValue)
63
+
64
+ function FormItem({ className, ...props }: React.ComponentProps<'div'>) {
65
+ const id = React.useId()
66
+ return (
67
+ <FormItemContext.Provider value={{ id }}>
68
+ <div data-slot="form-item" className={cn('grid gap-2', className)} {...props} />
69
+ </FormItemContext.Provider>
70
+ )
71
+ }
72
+
73
+ function FormLabel({ className, ...props }: React.ComponentProps<typeof LabelPrimitive.Root>) {
74
+ const { error, formItemId } = useFormField()
75
+ return (
76
+ <Label
77
+ data-slot="form-label"
78
+ data-error={!!error}
79
+ className={cn('data-[error=true]:text-destructive', className)}
80
+ htmlFor={formItemId}
81
+ {...props}
82
+ />
83
+ )
84
+ }
85
+
86
+ function FormControl({ ...props }: React.ComponentProps<typeof Slot>) {
87
+ const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
88
+ return (
89
+ <Slot
90
+ data-slot="form-control"
91
+ id={formItemId}
92
+ aria-describedby={!error ? formDescriptionId : `${formDescriptionId} ${formMessageId}`}
93
+ aria-invalid={!!error}
94
+ {...props}
95
+ />
96
+ )
97
+ }
98
+
99
+ function FormDescription({ className, ...props }: React.ComponentProps<'p'>) {
100
+ const { formDescriptionId } = useFormField()
101
+ return (
102
+ <p
103
+ data-slot="form-description"
104
+ id={formDescriptionId}
105
+ className={cn('text-muted-foreground text-sm', className)}
106
+ {...props}
107
+ />
108
+ )
109
+ }
110
+
111
+ function FormMessage({ className, children, ...props }: React.ComponentProps<'p'>) {
112
+ const { error, formMessageId } = useFormField()
113
+ const body = error ? String(error?.message ?? '') : children
114
+
115
+ if (!body) return null
116
+
117
+ return (
118
+ <p
119
+ data-slot="form-message"
120
+ id={formMessageId}
121
+ className={cn('text-destructive text-sm', className)}
122
+ {...props}
123
+ >
124
+ {body}
125
+ </p>
126
+ )
127
+ }
128
+
129
+ export {
130
+ useFormField,
131
+ Form,
132
+ FormItem,
133
+ FormLabel,
134
+ FormControl,
135
+ FormDescription,
136
+ FormMessage,
137
+ FormField,
138
+ }
@@ -0,0 +1,18 @@
1
+ import * as React from 'react'
2
+ import { cn } from '@/lib/utils'
3
+
4
+ function Input({ className, type, ...props }: React.ComponentProps<'input'>) {
5
+ return (
6
+ <input
7
+ type={type}
8
+ data-slot="input"
9
+ className={cn(
10
+ 'file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 aria-invalid:border-destructive',
11
+ className
12
+ )}
13
+ {...props}
14
+ />
15
+ )
16
+ }
17
+
18
+ export { Input }
@@ -0,0 +1,18 @@
1
+ import * as React from 'react'
2
+ import * as LabelPrimitive from '@radix-ui/react-label'
3
+ import { cn } from '@/lib/utils'
4
+
5
+ function Label({ className, ...props }: React.ComponentProps<typeof LabelPrimitive.Root>) {
6
+ return (
7
+ <LabelPrimitive.Root
8
+ data-slot="label"
9
+ className={cn(
10
+ 'flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50',
11
+ className
12
+ )}
13
+ {...props}
14
+ />
15
+ )
16
+ }
17
+
18
+ export { Label }
@@ -0,0 +1,19 @@
1
+ import { Toaster as Sonner, type ToasterProps } from 'sonner'
2
+
3
+ function Toaster({ ...props }: ToasterProps) {
4
+ return (
5
+ <Sonner
6
+ className="toaster group"
7
+ style={
8
+ {
9
+ '--normal-bg': 'var(--popover)',
10
+ '--normal-text': 'var(--popover-foreground)',
11
+ '--normal-border': 'var(--border)',
12
+ } as React.CSSProperties
13
+ }
14
+ {...props}
15
+ />
16
+ )
17
+ }
18
+
19
+ export { Toaster }
@@ -0,0 +1,92 @@
1
+ import { useForm } from 'react-hook-form'
2
+ import { zodResolver } from '@hookform/resolvers/zod'
3
+ import { toast } from 'sonner'
4
+ import { loginSchema } from './authSchema'
5
+ import type { LoginInput } from './authSchema'
6
+ import { useLoginMutation } from './authApi'
7
+ import { useAppDispatch } from '@/hooks/redux'
8
+ import { setCredentials } from './authSlice'
9
+ import { Button } from '@/components/ui/button'
10
+ import { Input } from '@/components/ui/input'
11
+ import {
12
+ Card,
13
+ CardContent,
14
+ CardDescription,
15
+ CardFooter,
16
+ CardHeader,
17
+ CardTitle,
18
+ } from '@/components/ui/card'
19
+ import {
20
+ Form,
21
+ FormControl,
22
+ FormField,
23
+ FormItem,
24
+ FormLabel,
25
+ FormMessage,
26
+ } from '@/components/ui/form'
27
+
28
+ export function LoginForm() {
29
+ const dispatch = useAppDispatch()
30
+ const [login, { isLoading }] = useLoginMutation()
31
+
32
+ const form = useForm<LoginInput>({
33
+ resolver: zodResolver(loginSchema),
34
+ defaultValues: { email: '', password: '' },
35
+ })
36
+
37
+ async function onSubmit(values: LoginInput) {
38
+ try {
39
+ const result = await login(values).unwrap()
40
+ dispatch(setCredentials(result))
41
+ toast.success('Logged in successfully')
42
+ } catch {
43
+ toast.error('Invalid email or password')
44
+ }
45
+ }
46
+
47
+ return (
48
+ <Card className="w-full max-w-sm">
49
+ <CardHeader>
50
+ <CardTitle>Sign in</CardTitle>
51
+ <CardDescription>Enter your credentials to access your account.</CardDescription>
52
+ </CardHeader>
53
+ <Form {...form}>
54
+ <form onSubmit={form.handleSubmit(onSubmit)}>
55
+ <CardContent className="flex flex-col gap-4">
56
+ <FormField
57
+ control={form.control}
58
+ name="email"
59
+ render={({ field }) => (
60
+ <FormItem>
61
+ <FormLabel>Email</FormLabel>
62
+ <FormControl>
63
+ <Input type="email" placeholder="you@example.com" {...field} />
64
+ </FormControl>
65
+ <FormMessage />
66
+ </FormItem>
67
+ )}
68
+ />
69
+ <FormField
70
+ control={form.control}
71
+ name="password"
72
+ render={({ field }) => (
73
+ <FormItem>
74
+ <FormLabel>Password</FormLabel>
75
+ <FormControl>
76
+ <Input type="password" placeholder="••••••••" {...field} />
77
+ </FormControl>
78
+ <FormMessage />
79
+ </FormItem>
80
+ )}
81
+ />
82
+ </CardContent>
83
+ <CardFooter>
84
+ <Button type="submit" className="w-full" disabled={isLoading}>
85
+ {isLoading ? 'Signing in…' : 'Sign in'}
86
+ </Button>
87
+ </CardFooter>
88
+ </form>
89
+ </Form>
90
+ </Card>
91
+ )
92
+ }
@@ -0,0 +1,40 @@
1
+ import { api } from '@/services/api'
2
+ import { loginSchema, registerSchema } from './authSchema'
3
+ import type { LoginInput, RegisterInput } from './authSchema'
4
+ import { z } from 'zod'
5
+
6
+ const userSchema = z.object({
7
+ id: z.string(),
8
+ name: z.string(),
9
+ email: z.string().email(),
10
+ })
11
+
12
+ const authResponseSchema = z.object({
13
+ user: userSchema,
14
+ token: z.string(),
15
+ })
16
+
17
+ export type AuthResponse = z.infer<typeof authResponseSchema>
18
+
19
+ export const authApi = api.injectEndpoints({
20
+ endpoints: (builder) => ({
21
+ login: builder.mutation<AuthResponse, LoginInput>({
22
+ query: (credentials) => ({
23
+ url: '/auth/login',
24
+ method: 'POST',
25
+ body: loginSchema.parse(credentials),
26
+ }),
27
+ transformResponse: (response) => authResponseSchema.parse(response),
28
+ }),
29
+ register: builder.mutation<AuthResponse, RegisterInput>({
30
+ query: (data) => ({
31
+ url: '/auth/register',
32
+ method: 'POST',
33
+ body: registerSchema.parse(data),
34
+ }),
35
+ transformResponse: (response) => authResponseSchema.parse(response),
36
+ }),
37
+ }),
38
+ })
39
+
40
+ export const { useLoginMutation, useRegisterMutation } = authApi
@@ -0,0 +1,19 @@
1
+ import { z } from 'zod'
2
+
3
+ export const loginSchema = z.object({
4
+ email: z.string().email('Please enter a valid email address'),
5
+ password: z.string().min(8, 'Password must be at least 8 characters'),
6
+ })
7
+
8
+ export const registerSchema = loginSchema
9
+ .extend({
10
+ name: z.string().min(1, 'Name is required'),
11
+ confirmPassword: z.string(),
12
+ })
13
+ .refine((data) => data.password === data.confirmPassword, {
14
+ message: "Passwords don't match",
15
+ path: ['confirmPassword'],
16
+ })
17
+
18
+ export type LoginInput = z.infer<typeof loginSchema>
19
+ export type RegisterInput = z.infer<typeof registerSchema>
@@ -0,0 +1,31 @@
1
+ import { createSlice } from '@reduxjs/toolkit'
2
+ import type { PayloadAction } from '@reduxjs/toolkit'
3
+ import type { AuthResponse } from './authApi'
4
+
5
+ interface AuthState {
6
+ user: AuthResponse['user'] | null
7
+ token: string | null
8
+ }
9
+
10
+ const initialState: AuthState = {
11
+ user: null,
12
+ token: null,
13
+ }
14
+
15
+ export const authSlice = createSlice({
16
+ name: 'auth',
17
+ initialState,
18
+ reducers: {
19
+ setCredentials: (state, action: PayloadAction<AuthResponse>) => {
20
+ state.user = action.payload.user
21
+ state.token = action.payload.token
22
+ },
23
+ logout: (state) => {
24
+ state.user = null
25
+ state.token = null
26
+ },
27
+ },
28
+ })
29
+
30
+ export const { setCredentials, logout } = authSlice.actions
31
+ export default authSlice.reducer
@@ -0,0 +1,34 @@
1
+ import { useAppDispatch, useAppSelector } from '@/hooks/redux'
2
+ import { increment, decrement, reset } from './counterSlice'
3
+ import { Button } from '@/components/ui/button'
4
+ import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'
5
+
6
+ export function Counter() {
7
+ const count = useAppSelector((state) => state.counter.value)
8
+ const dispatch = useAppDispatch()
9
+
10
+ return (
11
+ <Card className="w-full max-w-sm">
12
+ <CardHeader>
13
+ <CardTitle>Redux Counter</CardTitle>
14
+ <CardDescription>
15
+ A simple demo showing Redux Toolkit state management.
16
+ </CardDescription>
17
+ </CardHeader>
18
+ <CardContent className="flex flex-col items-center gap-4">
19
+ <span className="text-5xl font-bold tabular-nums">{count}</span>
20
+ <div className="flex gap-2">
21
+ <Button variant="outline" size="icon" onClick={() => dispatch(decrement())}>
22
+
23
+ </Button>
24
+ <Button variant="outline" size="icon" onClick={() => dispatch(increment())}>
25
+ +
26
+ </Button>
27
+ </div>
28
+ <Button variant="ghost" size="sm" onClick={() => dispatch(reset())}>
29
+ Reset
30
+ </Button>
31
+ </CardContent>
32
+ </Card>
33
+ )
34
+ }
@@ -0,0 +1,32 @@
1
+ import { createSlice } from '@reduxjs/toolkit'
2
+ import type { PayloadAction } from '@reduxjs/toolkit'
3
+
4
+ interface CounterState {
5
+ value: number
6
+ }
7
+
8
+ const initialState: CounterState = {
9
+ value: 0,
10
+ }
11
+
12
+ export const counterSlice = createSlice({
13
+ name: 'counter',
14
+ initialState,
15
+ reducers: {
16
+ increment: (state) => {
17
+ state.value += 1
18
+ },
19
+ decrement: (state) => {
20
+ state.value -= 1
21
+ },
22
+ incrementByAmount: (state, action: PayloadAction<number>) => {
23
+ state.value += action.payload
24
+ },
25
+ reset: (state) => {
26
+ state.value = 0
27
+ },
28
+ },
29
+ })
30
+
31
+ export const { increment, decrement, incrementByAmount, reset } = counterSlice.actions
32
+ export default counterSlice.reducer
@@ -0,0 +1,5 @@
1
+ import { useDispatch, useSelector } from 'react-redux'
2
+ import type { RootState, AppDispatch } from '@/store'
3
+
4
+ export const useAppDispatch = () => useDispatch<AppDispatch>()
5
+ export const useAppSelector = <T>(selector: (state: RootState) => T): T => useSelector(selector)