Skip to content

Commit 35b3fa4

Browse files
committed
feat: message dialog
1 parent beac5ad commit 35b3fa4

35 files changed

+2734
-333
lines changed

client/package-lock.json

Lines changed: 1132 additions & 157 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

client/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"@hookform/resolvers": "^3.3.4",
1515
"@mdx-js/react": "^3.0.1",
1616
"@mdx-js/rollup": "^3.0.1",
17+
"@million/lint": "^0.0.73",
1718
"@paralleldrive/cuid2": "^2.2.2",
1819
"@radix-ui/react-accordion": "^1.1.2",
1920
"@radix-ui/react-alert-dialog": "^1.0.5",
@@ -66,6 +67,7 @@
6667
"i18next-localstorage-backend": "^4.2.0",
6768
"lodash": "^4.17.21",
6869
"lucide-react": "^0.335.0",
70+
"million": "^3.0.6",
6971
"next-themes": "^0.2.1",
7072
"react": "^18.2.0",
7173
"react-day-picker": "^8.10.0",
Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
import { TCardInput, useCardInputSchema } from '@/lib/schema/card-input'
2+
import { zodResolver } from '@hookform/resolvers/zod'
3+
import _ from 'lodash'
4+
import { Plus, X } from 'lucide-react'
5+
import { useFieldArray, useForm, useWatch } from 'react-hook-form'
6+
import { useTranslation } from 'react-i18next'
7+
import {
8+
Button,
9+
Form,
10+
FormControl,
11+
FormField,
12+
FormItem,
13+
FormLabel,
14+
FormMessage,
15+
Input,
16+
Select,
17+
SelectContent,
18+
SelectItem,
19+
SelectTrigger,
20+
SelectValue,
21+
} from '../ui'
22+
import InputImage from '../ui/input-image'
23+
24+
type Props = {
25+
id?: string
26+
27+
onSubmit?: (data: TCardInput) => void
28+
defaultValue?: TCardInput
29+
}
30+
31+
export const CardForm = ({ defaultValue, id, onSubmit }: Props) => {
32+
const schema = useCardInputSchema()
33+
const { t } = useTranslation(['forms', 'flowDetail'])
34+
const form = useForm<TCardInput>({
35+
resolver: zodResolver(schema),
36+
mode: 'onChange',
37+
defaultValues: defaultValue,
38+
})
39+
40+
const { fields, append, remove } = useFieldArray({
41+
name: 'buttons',
42+
control: form.control,
43+
})
44+
45+
const buttonsWatch = useWatch({
46+
control: form.control,
47+
name: 'buttons',
48+
})
49+
50+
const handleAddButton = () => {
51+
form.trigger('buttons')
52+
53+
if (
54+
!_.isEmpty(form.formState.errors.buttons) ||
55+
buttonsWatch?.some((field) => !field.label || !field.value)
56+
)
57+
return
58+
59+
append({
60+
label: '',
61+
value: '',
62+
type: 'url',
63+
})
64+
}
65+
66+
const handleSubmit = (data: TCardInput) => {
67+
onSubmit?.(data)
68+
}
69+
70+
return (
71+
<Form {...form}>
72+
<form
73+
className='space-y-3'
74+
id='card-form'
75+
onSubmit={form.handleSubmit(handleSubmit)}
76+
>
77+
<FormField
78+
control={form.control}
79+
name='title'
80+
render={({ field }) => {
81+
return (
82+
<FormItem>
83+
<FormLabel required>{t('card_title.label')}</FormLabel>
84+
<FormControl>
85+
<Input {...field} placeholder={t('card_title.placeholder')} />
86+
</FormControl>
87+
<FormMessage />
88+
</FormItem>
89+
)
90+
}}
91+
/>
92+
<FormField
93+
control={form.control}
94+
name='subtitle'
95+
render={({ field }) => {
96+
return (
97+
<FormItem>
98+
<FormLabel required>{t('card_subtitle.label')}</FormLabel>
99+
<FormControl>
100+
<Input
101+
{...field}
102+
placeholder={t('card_subtitle.placeholder')}
103+
/>
104+
</FormControl>
105+
<FormMessage />
106+
</FormItem>
107+
)
108+
}}
109+
/>
110+
<FormField
111+
control={form.control}
112+
name='imageUrl'
113+
render={({ field }) => {
114+
return (
115+
<FormItem>
116+
<FormLabel>{t('card_image_url.label')}</FormLabel>
117+
<FormControl>
118+
<InputImage
119+
defaultValue={field.value}
120+
value={field.value}
121+
onChange={field.onChange}
122+
toServer
123+
size={80}
124+
/>
125+
</FormControl>
126+
<FormMessage />
127+
</FormItem>
128+
)
129+
}}
130+
/>
131+
132+
<div className='space-y-2'>
133+
<div className='flex items-center justify-between'>
134+
<span>Buttons</span>
135+
<Button
136+
variant='outline'
137+
className='p-0 w-6 h-6'
138+
type='button'
139+
onClick={handleAddButton}
140+
disabled={fields.length >= 3}
141+
>
142+
<Plus className='w-4 h-4' />
143+
</Button>
144+
</div>
145+
{fields.length > 0 ? (
146+
fields.map((field, index) => {
147+
return (
148+
<div key={field.id} className='flex gap-3'>
149+
<FormField
150+
name={`buttons.${index}.label`}
151+
control={form.control}
152+
render={({ field }) => {
153+
return (
154+
<FormItem className='w-full'>
155+
<FormControl>
156+
<Input
157+
{...field}
158+
autoComplete='off'
159+
placeholder={t('button_label.placeholder')}
160+
/>
161+
</FormControl>
162+
<FormMessage />
163+
</FormItem>
164+
)
165+
}}
166+
/>
167+
<FormField
168+
name={`buttons.${index}.value`}
169+
control={form.control}
170+
render={({ field }) => {
171+
return (
172+
<FormItem className='w-full'>
173+
<FormControl>
174+
<Input
175+
{...field}
176+
autoComplete='off'
177+
placeholder={t('button_value.placeholder')}
178+
/>
179+
</FormControl>
180+
<FormMessage />
181+
</FormItem>
182+
)
183+
}}
184+
/>
185+
<FormField
186+
name={`buttons.${index}.type`}
187+
control={form.control}
188+
render={({ field }) => {
189+
return (
190+
<FormItem className='w-24 flex-shrink-0'>
191+
<Select
192+
onValueChange={field.onChange}
193+
defaultValue={field.value}
194+
>
195+
<FormControl>
196+
<SelectTrigger>
197+
<SelectValue />
198+
</SelectTrigger>
199+
</FormControl>
200+
<SelectContent>
201+
{['url', 'postback'].map((type) => (
202+
<SelectItem key={type} value={type}>
203+
{type}
204+
</SelectItem>
205+
))}
206+
</SelectContent>
207+
</Select>
208+
<FormMessage />
209+
</FormItem>
210+
)
211+
}}
212+
/>
213+
<Button
214+
size='icon'
215+
onClick={() => {
216+
remove(index)
217+
}}
218+
variant='destructive'
219+
className='flex-shrink-0'
220+
>
221+
<X />
222+
</Button>
223+
</div>
224+
)
225+
})
226+
) : (
227+
<p className='flex items-center justify-center text-center text-sm text-muted-foreground'>
228+
{t('flowDetail:empty_buttons')}
229+
</p>
230+
)}
231+
</div>
232+
</form>
233+
</Form>
234+
)
235+
}

client/src/components/forms/general-setting.tsx

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
1+
import { LANGS } from '@/constants'
2+
import { useErrorsLngChange } from '@/hooks/use-errors-lng-change'
3+
import { queryChannelsForSelectOption } from '@/lib/query-options/channel'
4+
import { TFlowInput, useFlowInputSchema } from '@/lib/schema/flow-input'
5+
import { zodResolver } from '@hookform/resolvers/zod'
6+
import { useSuspenseQuery } from '@tanstack/react-query'
17
import { useForm } from 'react-hook-form'
8+
import { useTranslation } from 'react-i18next'
9+
import { useParams } from 'react-router-dom'
210
import {
311
Form,
412
FormControl,
@@ -7,7 +15,6 @@ import {
715
FormItem,
816
FormLabel,
917
FormMessage,
10-
Input,
1118
Label,
1219
MultipleSelect,
1320
Select,
@@ -16,19 +23,6 @@ import {
1623
SelectTrigger,
1724
SelectValue,
1825
} from '../ui'
19-
import { useErrorsLngChange } from '@/hooks/use-errors-lng-change'
20-
import { useTranslation } from 'react-i18next'
21-
import { zodResolver } from '@hookform/resolvers/zod'
22-
import { TFlowInput, useFlowInputSchema } from '@/lib/schema/flow-input'
23-
import { queryChannelsForSelectOption } from '@/lib/query-options/channel'
24-
import { useParams } from 'react-router-dom'
25-
import { useSuspenseQuery } from '@tanstack/react-query'
26-
import i18n from '@/i18n'
27-
28-
const LANGS: Record<string, string> = {
29-
vi: i18n.t('common:langs.vi'),
30-
en: i18n.t('common:langs.en'),
31-
}
3226

3327
type Props = {
3428
id?: string

client/src/components/forms/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
export * from './card'
12
export * from './change-pass'
23
export * from './forgot-pass'
34
export * from './general-setting'

0 commit comments

Comments
 (0)