Skip to content

Commit b98a46f

Browse files
committed
feat: ui channel
1 parent 8eca25c commit b98a46f

37 files changed

+1187
-88
lines changed

client/package-lock.json

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

client/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
"cmdk": "^0.2.1",
5151
"crypto-js": "^4.2.0",
5252
"date-fns": "^3.3.1",
53+
"dayjs": "^1.11.10",
5354
"embla-carousel-react": "^8.0.0-rc23",
5455
"firebase": "^10.8.0",
5556
"i18next": "^23.9.0",

client/src/apis/channel.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { ENDPOINTS } from '@/constants';
2+
import http_client from '@/lib/http-client';
3+
import { TChannelInput } from '@/lib/schema/channel';
4+
import {
5+
TChannelQuery,
6+
TChannelType,
7+
TChannelWithChannelType,
8+
} from '@/types/channel';
9+
import { TBaseResponse, TResPagination } from '@/types/share';
10+
11+
class ChannelApi {
12+
getChannelType(id: string): Promise<TBaseResponse<TChannelType>> {
13+
return http_client.get(`${ENDPOINTS.CHANNEL.TYPES}/${id}`);
14+
}
15+
16+
getChannelTypes(): Promise<TBaseResponse<TChannelType[]>> {
17+
return http_client.get(ENDPOINTS.CHANNEL.TYPES);
18+
}
19+
20+
getChannels(
21+
query?: TChannelQuery
22+
): Promise<TResPagination<TChannelWithChannelType>> {
23+
return http_client.get(ENDPOINTS.CHANNEL.INDEX, {
24+
params: query,
25+
});
26+
}
27+
28+
create(
29+
data: TChannelInput
30+
): Promise<TBaseResponse<TChannelWithChannelType>> {
31+
return http_client.post(ENDPOINTS.CHANNEL.INDEX, data);
32+
}
33+
34+
update(
35+
id: string,
36+
data: TChannelInput
37+
): Promise<TBaseResponse<TChannelWithChannelType>> {
38+
return http_client.put(`${ENDPOINTS.CHANNEL.INDEX}/${id}`, data);
39+
}
40+
41+
delete(id: string): Promise<TBaseResponse<null>> {
42+
return http_client.delete(`${ENDPOINTS.CHANNEL.DELETE}/${id}`);
43+
}
44+
45+
deleteMany(ids: string[]): Promise<TBaseResponse<null>> {
46+
return http_client.delete(ENDPOINTS.CHANNEL.DELETES, {
47+
data: { ids },
48+
});
49+
}
50+
}
51+
52+
export const channelApi = new ChannelApi();
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
import { useForm } from 'react-hook-form';
2+
import {
3+
Form,
4+
FormControl,
5+
FormField,
6+
FormItem,
7+
FormLabel,
8+
FormMessage,
9+
Input,
10+
Label,
11+
Select,
12+
SelectContent,
13+
SelectItem,
14+
SelectTrigger,
15+
SelectValue,
16+
Switch,
17+
} from '../ui';
18+
import { TChannelInput, useChannelSchema } from '@/lib/schema/channel';
19+
import { useErrorsLngChange } from '@/hooks/use-errors-lng-change';
20+
import { useTranslation } from 'react-i18next';
21+
import { useQuery } from '@tanstack/react-query';
22+
import { queryChannelTypesOption } from '@/lib/query-options/channel';
23+
import { zodResolver } from '@hookform/resolvers/zod';
24+
import { ChannelType } from '@/types/channel';
25+
import { useEffect, useMemo } from 'react';
26+
import { useDidUpdate } from '@/hooks/use-did-update';
27+
28+
type Props = {
29+
id?: string;
30+
onSubmit?: (data: TChannelInput) => void;
31+
defaultValues?: TChannelInput;
32+
};
33+
34+
const ChannelForm = ({
35+
id = 'channel-form',
36+
onSubmit,
37+
defaultValues,
38+
}: Props) => {
39+
const { t } = useTranslation('forms');
40+
const schema = useChannelSchema();
41+
const form = useForm<TChannelInput>({
42+
resolver: zodResolver(schema),
43+
mode: 'onChange',
44+
defaultValues,
45+
});
46+
const { data: types } = useQuery(queryChannelTypesOption);
47+
48+
const channelTypeId = form.watch('channelTypeId');
49+
50+
const currentType = useMemo(
51+
() => types?.find((type) => type.id === channelTypeId),
52+
[channelTypeId, types]
53+
);
54+
55+
useErrorsLngChange(form);
56+
57+
const handleSubmit = (data: TChannelInput) => {
58+
if (onSubmit) {
59+
onSubmit(data);
60+
}
61+
};
62+
63+
useDidUpdate(() => {
64+
form.setValue('credentials', {});
65+
}, [currentType, form]);
66+
67+
return (
68+
<Form {...form}>
69+
<form
70+
className="space-y-3"
71+
id={id}
72+
onSubmit={form.handleSubmit(handleSubmit)}
73+
>
74+
<FormField
75+
name="contactId"
76+
control={form.control}
77+
render={({ field }) => (
78+
<FormItem>
79+
<FormLabel>{t('contactId.label')}</FormLabel>
80+
<FormControl>
81+
<Input
82+
{...field}
83+
placeholder={t('contactId.placeholder')}
84+
autoComplete="off"
85+
/>
86+
</FormControl>
87+
<FormMessage />
88+
</FormItem>
89+
)}
90+
/>
91+
<FormField
92+
name="contactName"
93+
control={form.control}
94+
render={({ field }) => (
95+
<FormItem>
96+
<FormLabel>{t('contactName.label')}</FormLabel>
97+
<FormControl>
98+
<Input
99+
{...field}
100+
placeholder={t('contactName.placeholder')}
101+
autoComplete="off"
102+
/>
103+
</FormControl>
104+
<FormMessage />
105+
</FormItem>
106+
)}
107+
/>
108+
<FormField
109+
control={form.control}
110+
name="active"
111+
render={({ field }) => (
112+
<FormItem className="flex flex-row items-center justify-between">
113+
<div className="space-y-0.5">
114+
<FormLabel>
115+
<Label>{t('active.label')}</Label>
116+
</FormLabel>
117+
</div>
118+
<FormControl>
119+
<Switch
120+
checked={field.value}
121+
onCheckedChange={field.onChange}
122+
/>
123+
</FormControl>
124+
</FormItem>
125+
)}
126+
/>
127+
<FormField
128+
name="channelTypeId"
129+
control={form.control}
130+
render={({ field }) => (
131+
<FormItem>
132+
<FormLabel>{t('channelTypeId.label')}</FormLabel>
133+
<Select
134+
onValueChange={field.onChange}
135+
defaultValue={field.value}
136+
>
137+
<FormControl>
138+
<SelectTrigger>
139+
<SelectValue
140+
placeholder={t('channelTypeId.placeholder')}
141+
/>
142+
</SelectTrigger>
143+
</FormControl>
144+
<SelectContent>
145+
{types?.map((type) => (
146+
<SelectItem key={type.id} value={type.id}>
147+
{type.description}
148+
</SelectItem>
149+
))}
150+
</SelectContent>
151+
</Select>
152+
153+
<FormMessage />
154+
</FormItem>
155+
)}
156+
/>
157+
{currentType &&
158+
(currentType.name === ChannelType.MESSENGER ||
159+
currentType.name === ChannelType.LINE) && (
160+
<>
161+
<FormField
162+
name="credentials.pageToken"
163+
control={form.control}
164+
render={({ field }) => (
165+
<FormItem>
166+
<FormLabel>{t('pageToken.label')}</FormLabel>
167+
<FormControl>
168+
<Input
169+
{...field}
170+
placeholder={t('pageToken.placeholder')}
171+
autoComplete="off"
172+
/>
173+
</FormControl>
174+
175+
<FormMessage />
176+
</FormItem>
177+
)}
178+
/>
179+
{currentType.name === ChannelType.MESSENGER && (
180+
<FormField
181+
name="credentials.webhookSecret"
182+
control={form.control}
183+
render={({ field }) => (
184+
<FormItem>
185+
<FormLabel>
186+
{t('webhookSecret.label')}
187+
</FormLabel>
188+
<FormControl>
189+
<Input
190+
{...field}
191+
placeholder={t(
192+
'webhookSecret.placeholder'
193+
)}
194+
autoComplete="off"
195+
/>
196+
</FormControl>
197+
<FormMessage />
198+
</FormItem>
199+
)}
200+
/>
201+
)}
202+
</>
203+
)}
204+
</form>
205+
</Form>
206+
);
207+
};
208+
209+
export default ChannelForm;
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1+
import ButtonLang from '@/components/btn-lang';
12
import { useAppLayoutStore } from '@/store/use-app-layout';
23

34
export const Header = () => {
45
const { title } = useAppLayoutStore();
56
return (
6-
<header className="fixed h-header left-sidebar top-0 right-0 bg-background border-b border-border shadow-sm px-6 flex items-center">
7+
<header className="fixed h-header left-sidebar top-0 right-0 bg-background border-b border-border shadow-sm px-6 flex items-center justify-between z-50">
78
<span className="text-lg font-semibold">{title}</span>
9+
<ButtonLang />
810
</header>
911
);
1012
};

0 commit comments

Comments
 (0)