@alfatech/livechat 2024.12.3-1.1
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/.github/workflows/release.yml +33 -0
- package/.husky/pre-commit +2 -0
- package/README.md +30 -0
- package/biome.json +55 -0
- package/index.html +20 -0
- package/package.json +43 -0
- package/postcss.config.cjs +5 -0
- package/public/vite.svg +1 -0
- package/src/api/base.api.ts +118 -0
- package/src/api/error.ts +109 -0
- package/src/assets/react.svg +1 -0
- package/src/components/app/WidgetProvider.tsx +80 -0
- package/src/components/app/button/index.tsx +47 -0
- package/src/components/app/button/types.ts +68 -0
- package/src/components/app/card/profile.tsx +33 -0
- package/src/components/app/icon/index.tsx +144 -0
- package/src/components/app/input/checkbox.tsx +115 -0
- package/src/components/app/input/index.tsx +133 -0
- package/src/components/app/input/label.tsx +33 -0
- package/src/components/app/input/radio.tsx +143 -0
- package/src/components/app/input/select.tsx +54 -0
- package/src/components/app/input/textarea.tsx +68 -0
- package/src/components/app/input/toggle.tsx +162 -0
- package/src/components/app/input/verification.tsx +111 -0
- package/src/features/chat/chat.api.ts +30 -0
- package/src/features/chat/chat.types.ts +50 -0
- package/src/features/chat/partials/ChatCreateForm.tsx +103 -0
- package/src/features/chat/partials/ChatMessageFormFooter.tsx +67 -0
- package/src/features/chat/partials/ChatMessageSection.tsx +103 -0
- package/src/features/widget/components/WidgetIcon.tsx +26 -0
- package/src/features/widget/contexts/page-tracker.tsx +74 -0
- package/src/features/widget/hooks/page-tracker.tsx +57 -0
- package/src/features/widget/widget.api.ts +14 -0
- package/src/features/widget/widget.store.ts +16 -0
- package/src/features/widget/widget.types.ts +28 -0
- package/src/index.css +40 -0
- package/src/layouts/auth.tsx +17 -0
- package/src/layouts/chat.tsx +18 -0
- package/src/lib/cdn.tsx +3 -0
- package/src/lib/cookie.tsx +7 -0
- package/src/lib/debounce.tsx +23 -0
- package/src/lib/hooks/cookie.tsx +22 -0
- package/src/lib/hooks/dayjs.tsx +10 -0
- package/src/lib/hooks/dom.tsx +26 -0
- package/src/lib/hooks/form.tsx +26 -0
- package/src/lib/hooks/router.tsx +31 -0
- package/src/lib/list.tsx +14 -0
- package/src/lib/obj.tsx +23 -0
- package/src/lib/phone.tsx +12 -0
- package/src/main.tsx +31 -0
- package/src/pages/auth/registration-view.tsx +67 -0
- package/src/pages/chat/chat-detail-view.tsx +200 -0
- package/src/router/router.tsx +17 -0
- package/src/router/routes/auth-routes.ts +7 -0
- package/src/router/routes/chat-routes.ts +7 -0
- package/src/router/routes/index.ts +8 -0
- package/src/router/routes/routes.types.ts +0 -0
- package/src/types/i18n.ts +11 -0
- package/src/vite-env.d.ts +1 -0
- package/src/widget.tsx +19 -0
- package/src/ws/events.tsx +69 -0
- package/src/ws/guard.tsx +16 -0
- package/src/ws/hooks.tsx +122 -0
- package/tailwind.config.js +27 -0
- package/tsconfig.app.json +29 -0
- package/tsconfig.app.tsbuildinfo +1 -0
- package/tsconfig.json +7 -0
- package/tsconfig.node.json +22 -0
- package/tsconfig.node.tsbuildinfo +1 -0
- package/version.cjs +5 -0
- package/vite.config.ts +14 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { createContext, useContext, useEffect, useState } from "react";
|
|
2
|
+
|
|
3
|
+
enum storageKeys {
|
|
4
|
+
PAGE = "lc-page",
|
|
5
|
+
HISTORY = "lc-p-history",
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
type PageTrackerContext = {
|
|
9
|
+
page: string;
|
|
10
|
+
setPage: (page: string) => void;
|
|
11
|
+
history: string[];
|
|
12
|
+
clearHistory: () => void;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const pageTrackerContext = createContext<PageTrackerContext>(
|
|
16
|
+
{} as PageTrackerContext,
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
const getPageStr = () => `${window.location.pathname}_$$_${document.title}`;
|
|
20
|
+
|
|
21
|
+
export const getPageFromLocalStorage = (): string => {
|
|
22
|
+
const page = localStorage.getItem(storageKeys.PAGE);
|
|
23
|
+
if (!page || page === "") {
|
|
24
|
+
return "/";
|
|
25
|
+
}
|
|
26
|
+
return getPageStr();
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const getHistoryFromLocalStorage = (): string[] => {
|
|
30
|
+
const history = localStorage.getItem(storageKeys.HISTORY);
|
|
31
|
+
if (!history || history === "") {
|
|
32
|
+
return [];
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
return JSON.parse(history);
|
|
36
|
+
} catch (e) {
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export default function PageTrackerProvider({ children }: { children: any }) {
|
|
42
|
+
const [page, setPage] = useState<string>("/");
|
|
43
|
+
const [history, setHistory] = useState<string[]>(
|
|
44
|
+
getHistoryFromLocalStorage(),
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
function clearHistory() {
|
|
48
|
+
setHistory([]);
|
|
49
|
+
localStorage.removeItem(storageKeys.HISTORY);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const onNewPage = (page: string) => {
|
|
53
|
+
setPage(page);
|
|
54
|
+
setHistory((prev) => [...new Set([...prev, page])]);
|
|
55
|
+
localStorage.setItem(storageKeys.PAGE, page);
|
|
56
|
+
localStorage.setItem(storageKeys.HISTORY, JSON.stringify(history));
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
onNewPage(getPageStr());
|
|
61
|
+
}, []);
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<pageTrackerContext.Provider
|
|
65
|
+
value={{ page, setPage: onNewPage, history, clearHistory }}
|
|
66
|
+
>
|
|
67
|
+
{children}
|
|
68
|
+
</pageTrackerContext.Provider>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function usePageTracker() {
|
|
73
|
+
return useContext(pageTrackerContext);
|
|
74
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { useWebsocket } from "@/ws/hooks";
|
|
2
|
+
import {
|
|
3
|
+
getPageFromLocalStorage,
|
|
4
|
+
usePageTracker,
|
|
5
|
+
} from "../contexts/page-tracker";
|
|
6
|
+
import { useCallback, useEffect, useState } from "react";
|
|
7
|
+
import { useListener } from "@/lib/hooks/dom";
|
|
8
|
+
import { SendEvents } from "@/ws/events";
|
|
9
|
+
|
|
10
|
+
export function usePageTracking() {
|
|
11
|
+
const pageTracker = usePageTracker();
|
|
12
|
+
|
|
13
|
+
const onPopState = useCallback(() => {
|
|
14
|
+
const newUrl = window.location.pathname;
|
|
15
|
+
if (newUrl !== pageTracker.page) {
|
|
16
|
+
pageTracker.setPage(newUrl);
|
|
17
|
+
}
|
|
18
|
+
}, [pageTracker.page]);
|
|
19
|
+
|
|
20
|
+
useListener("popstate", onPopState);
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function usePageTrackingWS() {
|
|
25
|
+
const [onced, setOnced] = useState(false);
|
|
26
|
+
const socket = useWebsocket();
|
|
27
|
+
const pageTracker = usePageTracker();
|
|
28
|
+
|
|
29
|
+
const onPopState = useCallback(() => {
|
|
30
|
+
const newUrl = window.location.pathname;
|
|
31
|
+
if (newUrl !== pageTracker.page) {
|
|
32
|
+
if (socket) {
|
|
33
|
+
socket.emit(SendEvents.PageChange, { page: newUrl });
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
pageTracker.setPage(newUrl);
|
|
37
|
+
}
|
|
38
|
+
}, [socket, pageTracker.page]);
|
|
39
|
+
|
|
40
|
+
useListener("popstate", onPopState);
|
|
41
|
+
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
if (onced || !socket) return;
|
|
44
|
+
const timeout = setTimeout(() => {
|
|
45
|
+
const old = getPageFromLocalStorage();
|
|
46
|
+
if (old !== pageTracker.page) {
|
|
47
|
+
socket.emit(SendEvents.PageChange, { page: pageTracker.page });
|
|
48
|
+
setOnced(true);
|
|
49
|
+
}
|
|
50
|
+
}, 1000);
|
|
51
|
+
return () => {
|
|
52
|
+
clearTimeout(timeout);
|
|
53
|
+
};
|
|
54
|
+
}, [socket, pageTracker.page, onced]);
|
|
55
|
+
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { req } from "@/api/base.api";
|
|
2
|
+
import { useQuery } from "@tanstack/react-query";
|
|
3
|
+
import type { WidgetPublicView } from "./widget.types";
|
|
4
|
+
|
|
5
|
+
export const useWidgetView = (link_id: string) => {
|
|
6
|
+
return useQuery({
|
|
7
|
+
queryKey: ["widgets", link_id],
|
|
8
|
+
queryFn: () => getWidgetByLink(link_id),
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const getWidgetByLink = (link_id: string) => {
|
|
13
|
+
return req<WidgetPublicView>(`widgets/with-link/${link_id}`);
|
|
14
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { create } from "zustand";
|
|
2
|
+
import type { WidgetPublicView } from "./widget.types";
|
|
3
|
+
|
|
4
|
+
type WidgetStore = {
|
|
5
|
+
widget: WidgetPublicView | null;
|
|
6
|
+
open: boolean;
|
|
7
|
+
setOpen: (open: boolean) => void;
|
|
8
|
+
setWidget: (widget: WidgetPublicView) => void;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export const useWidgetStore = create<WidgetStore>((set) => ({
|
|
12
|
+
widget: null,
|
|
13
|
+
open: false,
|
|
14
|
+
setOpen: (open: boolean) => set({ open }),
|
|
15
|
+
setWidget: (widget: WidgetPublicView) => set({ widget }),
|
|
16
|
+
}));
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Lang, Timezone } from "@/types/i18n";
|
|
2
|
+
|
|
3
|
+
export type WidgetPublicView = {
|
|
4
|
+
public_id: string;
|
|
5
|
+
link_id: string;
|
|
6
|
+
client_id: string;
|
|
7
|
+
name: string;
|
|
8
|
+
timezone: Timezone;
|
|
9
|
+
lang: Lang;
|
|
10
|
+
image_url: string;
|
|
11
|
+
ticket_proxy_email: string;
|
|
12
|
+
is_browser_notification_enabled: boolean;
|
|
13
|
+
is_hidden_on_offline: boolean;
|
|
14
|
+
is_upload_disabled: boolean;
|
|
15
|
+
writing_timeout?: number;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export function isWidgetPublicView(obj: any): obj is WidgetPublicView {
|
|
19
|
+
return (
|
|
20
|
+
!!obj &&
|
|
21
|
+
typeof obj.public_id === "string" &&
|
|
22
|
+
typeof obj.link_id === "string" &&
|
|
23
|
+
typeof obj.name === "string" &&
|
|
24
|
+
typeof obj.is_browser_notification_enabled === "boolean" &&
|
|
25
|
+
typeof obj.is_hidden_on_offline === "boolean" &&
|
|
26
|
+
typeof obj.is_upload_disabled === "boolean"
|
|
27
|
+
);
|
|
28
|
+
}
|
package/src/index.css
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
@tailwind base;
|
|
2
|
+
@tailwind components;
|
|
3
|
+
@tailwind utilities;
|
|
4
|
+
|
|
5
|
+
#livechat {
|
|
6
|
+
position: fixed;
|
|
7
|
+
bottom: 1rem;
|
|
8
|
+
right: 1rem;
|
|
9
|
+
z-index: 9999;
|
|
10
|
+
background-color: #f8f9fa;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
:root {
|
|
14
|
+
--scroll-bar-bg: #1c2e45;
|
|
15
|
+
--scroll-bar-bg-hover: #1c2e45;
|
|
16
|
+
--scroll-bar-track: #f0f8ff;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
::-webkit-scrollbar,
|
|
20
|
+
.scrollbar::-webkit-scrollbar {
|
|
21
|
+
width: 3px;
|
|
22
|
+
height: 3px;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
::-webkit-scrollbar-thumb,
|
|
26
|
+
.scrollbar::-webkit-scrollbar-thumb {
|
|
27
|
+
background-color: var(--scroll-bar-bg);
|
|
28
|
+
border-radius: 5px;
|
|
29
|
+
transition: background-color 0.2s ease-in-out;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
::-webkit-scrollbar-thumb:hover,
|
|
33
|
+
.scrollbar::-webkit-scrollbar-thumb:hover {
|
|
34
|
+
background-color: var(--scroll-bar-bg-hover);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
::-webkit-scrollbar-track,
|
|
38
|
+
.scrollbar::-webkit-scrollbar-track {
|
|
39
|
+
background-color: var(--scroll-bar-track);
|
|
40
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { useWidgetStore } from "@/features/widget/widget.store";
|
|
2
|
+
import type { PropsWithChildren } from "react";
|
|
3
|
+
|
|
4
|
+
type Props = {};
|
|
5
|
+
|
|
6
|
+
export default function WidgetAuthLayout({
|
|
7
|
+
children,
|
|
8
|
+
}: PropsWithChildren<Props>) {
|
|
9
|
+
const { widget } = useWidgetStore();
|
|
10
|
+
if (!widget) return null;
|
|
11
|
+
return (
|
|
12
|
+
<section className="border h-full w-full rounded-md p-4 relative">
|
|
13
|
+
<div className="-z-10 h-52 bg-zink-600 absolute top-0 left-0 w-full rounded-t-md flex flex-col gap-4 items-center justify-center"></div>
|
|
14
|
+
{children}
|
|
15
|
+
</section>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { useWidgetStore } from "@/features/widget/widget.store";
|
|
2
|
+
import type { PropsWithChildren } from "react";
|
|
3
|
+
|
|
4
|
+
type Props = {
|
|
5
|
+
className?: string;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export default function WidgetChatLayout({
|
|
9
|
+
children,
|
|
10
|
+
}: PropsWithChildren<Props>) {
|
|
11
|
+
const { widget } = useWidgetStore();
|
|
12
|
+
if (!widget) return null;
|
|
13
|
+
return (
|
|
14
|
+
<section className="border h-full w-full rounded-md relative">
|
|
15
|
+
{children}
|
|
16
|
+
</section>
|
|
17
|
+
);
|
|
18
|
+
}
|
package/src/lib/cdn.tsx
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
type Timer = ReturnType<typeof setTimeout>;
|
|
2
|
+
|
|
3
|
+
function debounce<T extends (...args: any[]) => any>(
|
|
4
|
+
func: T,
|
|
5
|
+
delay: number,
|
|
6
|
+
): (...args: Parameters<T>) => void {
|
|
7
|
+
let timer: Timer | undefined;
|
|
8
|
+
let count = 0;
|
|
9
|
+
|
|
10
|
+
return function debouncedFn(...args: Parameters<T>) {
|
|
11
|
+
count++;
|
|
12
|
+
const currentCount = count;
|
|
13
|
+
clearTimeout(timer);
|
|
14
|
+
|
|
15
|
+
timer = setTimeout(() => {
|
|
16
|
+
if (currentCount === count) {
|
|
17
|
+
func(...args);
|
|
18
|
+
}
|
|
19
|
+
}, delay);
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export default debounce;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
|
|
3
|
+
const getCookie = (key: string) => {
|
|
4
|
+
const cookie = document.cookie
|
|
5
|
+
.split(";")
|
|
6
|
+
.find((cookie) => cookie.trim().startsWith(`${key}=`));
|
|
7
|
+
return cookie ? cookie.split("=")[1] : "";
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const useCookieValue = (key: string) => {
|
|
11
|
+
const [cookie, setCookie] = useState(getCookie(key));
|
|
12
|
+
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
setCookie(getCookie(key));
|
|
15
|
+
}, [key]);
|
|
16
|
+
|
|
17
|
+
return cookie;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export const setCookie = (key: string, value: string) => {
|
|
21
|
+
document.cookie = `${key}=${value}`;
|
|
22
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { useEffect } from "react";
|
|
2
|
+
|
|
3
|
+
export const useBodyClassName = (className: string) => {
|
|
4
|
+
useEffect(() => {
|
|
5
|
+
const body = document.body;
|
|
6
|
+
const classNames = className.split(" ");
|
|
7
|
+
body.classList.add(...classNames);
|
|
8
|
+
return () => {
|
|
9
|
+
body.classList.remove(...classNames);
|
|
10
|
+
};
|
|
11
|
+
}, []);
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const useListener = <T extends HTMLElement = HTMLElement>(
|
|
15
|
+
eventName: string,
|
|
16
|
+
handler: (event: Event) => void,
|
|
17
|
+
element?: T,
|
|
18
|
+
) => {
|
|
19
|
+
const el = typeof window !== "undefined" ? element || window : undefined;
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
el!.addEventListener(eventName, handler);
|
|
22
|
+
return () => {
|
|
23
|
+
el!.removeEventListener(eventName, handler, false);
|
|
24
|
+
};
|
|
25
|
+
}, [eventName, handler, el]);
|
|
26
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { useFormik } from "formik";
|
|
2
|
+
import type { FormikValues } from "formik/dist/types";
|
|
3
|
+
|
|
4
|
+
type Props<T = any> = {
|
|
5
|
+
init: T;
|
|
6
|
+
schema?: any;
|
|
7
|
+
action: (values: T) => void | Promise<void>;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const useForm = <T extends FormikValues = any>({
|
|
11
|
+
init,
|
|
12
|
+
schema,
|
|
13
|
+
action,
|
|
14
|
+
}: Props<T>) => {
|
|
15
|
+
return useFormik<T>({
|
|
16
|
+
initialValues: init,
|
|
17
|
+
validationSchema: schema,
|
|
18
|
+
validateOnBlur: false,
|
|
19
|
+
enableReinitialize: true,
|
|
20
|
+
validateOnChange: false,
|
|
21
|
+
validateOnMount: false,
|
|
22
|
+
onSubmit: (values) => {
|
|
23
|
+
action(values);
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { allRoutes } from "@/router/routes";
|
|
2
|
+
import type { AuthRouteNames } from "@/router/routes/auth-routes";
|
|
3
|
+
import type { ChatRouteNames } from "@/router/routes/chat-routes";
|
|
4
|
+
import { type NavigateOptions, useNavigate } from "react-router-dom";
|
|
5
|
+
|
|
6
|
+
export type RouteName = AuthRouteNames | ChatRouteNames;
|
|
7
|
+
|
|
8
|
+
type Result = {
|
|
9
|
+
push: (name: RouteName, q?: string | object) => void;
|
|
10
|
+
path: (name: RouteName, q?: string | object) => string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const useRouter = (opts?: NavigateOptions): Result => {
|
|
14
|
+
const navi = useNavigate();
|
|
15
|
+
|
|
16
|
+
const path = (name: RouteName, q?: string | object) => {
|
|
17
|
+
const p = allRoutes.find((r) => r.name === name)?.path ?? "/";
|
|
18
|
+
if (typeof q === "object" && p.includes(":")) {
|
|
19
|
+
return Object.entries(q).reduce<string>((acc, [key, value]) => {
|
|
20
|
+
return acc.replace(`:${key}`, value as string);
|
|
21
|
+
}, p);
|
|
22
|
+
}
|
|
23
|
+
return q ? `${p}?${q}` : p;
|
|
24
|
+
};
|
|
25
|
+
return {
|
|
26
|
+
push: (name, q) => {
|
|
27
|
+
navi(path(name, q), opts);
|
|
28
|
+
},
|
|
29
|
+
path: path,
|
|
30
|
+
};
|
|
31
|
+
};
|
package/src/lib/list.tsx
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export type ListResult<T> = {
|
|
2
|
+
page: number;
|
|
3
|
+
limit: number;
|
|
4
|
+
list: T[];
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export function isListResult<T = any>(obj: any): obj is ListResult<T> {
|
|
8
|
+
return (
|
|
9
|
+
typeof obj === "object" &&
|
|
10
|
+
typeof obj.page === "number" &&
|
|
11
|
+
typeof obj.limit === "number" &&
|
|
12
|
+
Array.isArray(obj.list)
|
|
13
|
+
);
|
|
14
|
+
}
|
package/src/lib/obj.tsx
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export function checkObjSame(obj1: any, obj2: any, keys?: string[]): boolean {
|
|
2
|
+
let _keys = keys;
|
|
3
|
+
if (!_keys) {
|
|
4
|
+
_keys = Object.keys(obj1);
|
|
5
|
+
}
|
|
6
|
+
for (let key of _keys) {
|
|
7
|
+
if (Array.isArray(obj1[key]) && Array.isArray(obj2[key])) {
|
|
8
|
+
if (obj1[key].length !== obj2[key].length) {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
for (let i = 0; i < obj1[key].length; i++) {
|
|
12
|
+
if (obj1[key][i] !== obj2[key][i]) {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
if (obj1[key] !== obj2[key]) {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export const initPhone = (phone: string) => {
|
|
2
|
+
if (phone.length === 10 && phone.startsWith("5")) {
|
|
3
|
+
return `+90${phone}`;
|
|
4
|
+
}
|
|
5
|
+
if (phone.length === 11 && phone.startsWith("0")) {
|
|
6
|
+
return `+9${phone}`;
|
|
7
|
+
}
|
|
8
|
+
if (phone.length === 12 && phone.startsWith("90")) {
|
|
9
|
+
return `+${phone}`;
|
|
10
|
+
}
|
|
11
|
+
return phone;
|
|
12
|
+
};
|
package/src/main.tsx
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { useEffect } from "react";
|
|
2
|
+
import { useWidgetView } from "./features/widget/widget.api.ts";
|
|
3
|
+
import { isWidgetPublicView } from "./features/widget/widget.types.ts";
|
|
4
|
+
import AppRouter from "./router/router.tsx";
|
|
5
|
+
import { useWidgetStore } from "./features/widget/widget.store.ts";
|
|
6
|
+
import PageTrackerProvider from "./features/widget/contexts/page-tracker.tsx";
|
|
7
|
+
import { WebSocketProvider } from "./ws/hooks.tsx";
|
|
8
|
+
|
|
9
|
+
type Props = {
|
|
10
|
+
link_id: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export default function App({ link_id }: Props) {
|
|
14
|
+
const { setWidget, widget } = useWidgetStore();
|
|
15
|
+
const { data, isLoading } = useWidgetView(link_id);
|
|
16
|
+
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
if (Array.isArray(data) && isWidgetPublicView(data[0])) {
|
|
19
|
+
setWidget(data[0]);
|
|
20
|
+
}
|
|
21
|
+
}, [data]);
|
|
22
|
+
|
|
23
|
+
if (isLoading || !widget) return null;
|
|
24
|
+
return (
|
|
25
|
+
<PageTrackerProvider>
|
|
26
|
+
<WebSocketProvider>
|
|
27
|
+
<AppRouter />
|
|
28
|
+
</WebSocketProvider>
|
|
29
|
+
</PageTrackerProvider>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { isSuccess } from "@/api/base.api";
|
|
2
|
+
import WidgetProvider from "@/components/app/WidgetProvider";
|
|
3
|
+
import { chatCheck } from "@/features/chat/chat.api";
|
|
4
|
+
import ChatCreateForm from "@/features/chat/partials/ChatCreateForm";
|
|
5
|
+
import WidgetIcon from "@/features/widget/components/WidgetIcon";
|
|
6
|
+
import { usePageTracking } from "@/features/widget/hooks/page-tracker";
|
|
7
|
+
import { useWidgetStore } from "@/features/widget/widget.store";
|
|
8
|
+
import WidgetAuthLayout from "@/layouts/auth";
|
|
9
|
+
import { useCookieValue } from "@/lib/hooks/cookie";
|
|
10
|
+
import { useRouter } from "@/lib/hooks/router";
|
|
11
|
+
import { useEffect, useState } from "react";
|
|
12
|
+
|
|
13
|
+
function Tracking() {
|
|
14
|
+
usePageTracking();
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export default function RegistrationView() {
|
|
19
|
+
const { widget } = useWidgetStore();
|
|
20
|
+
const [loading, setLoading] = useState(true);
|
|
21
|
+
const savedEmail = useCookieValue("email");
|
|
22
|
+
const savedName = useCookieValue("name");
|
|
23
|
+
const router = useRouter();
|
|
24
|
+
if (!widget) return null;
|
|
25
|
+
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
chatCheck(widget.client_id)
|
|
28
|
+
.then(([_, status]) => {
|
|
29
|
+
if (isSuccess(status)) {
|
|
30
|
+
router.push("chat");
|
|
31
|
+
}
|
|
32
|
+
})
|
|
33
|
+
.finally(() => {
|
|
34
|
+
setLoading(false);
|
|
35
|
+
});
|
|
36
|
+
}, []);
|
|
37
|
+
|
|
38
|
+
if (loading) return null;
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<>
|
|
42
|
+
<WidgetProvider>
|
|
43
|
+
<WidgetAuthLayout>
|
|
44
|
+
<div className="flex flex-col gap-3 items-center justify-center h-52">
|
|
45
|
+
<WidgetIcon
|
|
46
|
+
name={widget.name}
|
|
47
|
+
image_url={widget.image_url}
|
|
48
|
+
size="size-24"
|
|
49
|
+
/>
|
|
50
|
+
<div className="text-white text-lg font-bold ml-2">
|
|
51
|
+
{widget.name}
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
<ChatCreateForm
|
|
55
|
+
clientId={widget.client_id}
|
|
56
|
+
savedEmail={savedEmail}
|
|
57
|
+
savedName={savedName}
|
|
58
|
+
onOk={() => {
|
|
59
|
+
router.push("chat");
|
|
60
|
+
}}
|
|
61
|
+
/>
|
|
62
|
+
</WidgetAuthLayout>
|
|
63
|
+
</WidgetProvider>
|
|
64
|
+
<Tracking />
|
|
65
|
+
</>
|
|
66
|
+
);
|
|
67
|
+
}
|