lulichat 1.0.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/README.md +68 -0
- package/dist/lulichat-support.es.js +1729 -0
- package/dist/lulichat-support.umd.js +30 -0
- package/dist/style.css +1 -0
- package/package.json +57 -0
- package/src/components/ChatIcon.tsx +27 -0
- package/src/components/ChatInterface.tsx +211 -0
- package/src/components/ChatWidget.tsx +198 -0
- package/src/components/ContactForm.tsx +155 -0
- package/src/components/LuliChat.tsx +48 -0
- package/src/components/ui/Button.tsx +44 -0
- package/src/components/ui/Form.tsx +174 -0
- package/src/components/ui/Icons.tsx +443 -0
- package/src/components/ui/Input.tsx +31 -0
- package/src/constants.ts +32 -0
- package/src/hooks/useSocket.ts +45 -0
- package/src/index.css +383 -0
- package/src/index.ts +5 -0
- package/src/lib/socket.ts +96 -0
- package/src/main.tsx +15 -0
- package/src/types/index.ts +93 -0
- package/src/utils/api.ts +59 -0
- package/src/utils/index.ts +5 -0
- package/src/vite-env.d.ts +1 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { ContactInfo } from "@/types";
|
|
2
|
+
import React, { useState } from "react";
|
|
3
|
+
import Input from "./ui/Input";
|
|
4
|
+
import Form from "./ui/Form";
|
|
5
|
+
import { DropdownArrow, Hello, Loader } from "./ui/Icons";
|
|
6
|
+
import Button from "./ui/Button";
|
|
7
|
+
|
|
8
|
+
interface ContactFormProps {
|
|
9
|
+
companyName: string;
|
|
10
|
+
onSubmit: (contactInfo: ContactInfo) => void;
|
|
11
|
+
onSkip?: () => void;
|
|
12
|
+
allowAnonymous: boolean;
|
|
13
|
+
isLoading: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const ContactForm: React.FC<ContactFormProps> = ({
|
|
17
|
+
companyName,
|
|
18
|
+
onSubmit,
|
|
19
|
+
onSkip,
|
|
20
|
+
allowAnonymous,
|
|
21
|
+
isLoading,
|
|
22
|
+
}) => {
|
|
23
|
+
const [open, setOpen] = React.useState(false);
|
|
24
|
+
const [formData, setFormData] = useState<ContactInfo>({
|
|
25
|
+
name: "",
|
|
26
|
+
email: "",
|
|
27
|
+
phone: "",
|
|
28
|
+
company: "",
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const [errors, setErrors] = useState<Partial<ContactInfo>>({});
|
|
32
|
+
const [values, setValues] = React.useState<Record<string, string>>({
|
|
33
|
+
email: "",
|
|
34
|
+
name: "",
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const validateForm = () => {
|
|
38
|
+
const newErrors: Partial<ContactInfo> = {};
|
|
39
|
+
|
|
40
|
+
if (!formData.name.trim()) {
|
|
41
|
+
newErrors.name = "Name is required";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (!formData.email.trim()) {
|
|
45
|
+
newErrors.email = "Email is required";
|
|
46
|
+
} else if (!/\S+@\S+\.\S+/.test(formData.email)) {
|
|
47
|
+
newErrors.email = "Email is invalid";
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
setErrors(newErrors);
|
|
51
|
+
return Object.keys(newErrors).length === 0;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const handleSubmit = (e: React.FormEvent) => {
|
|
55
|
+
e.preventDefault();
|
|
56
|
+
if (validateForm()) {
|
|
57
|
+
onSubmit(formData);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const handleInputChange = (field: keyof ContactInfo, value: string) => {
|
|
62
|
+
setFormData((prev) => ({ ...prev, [field]: value }));
|
|
63
|
+
if (errors[field]) {
|
|
64
|
+
setErrors((prev) => ({ ...prev, [field]: undefined }));
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<Form
|
|
70
|
+
autoComplete="off"
|
|
71
|
+
onSubmit={handleSubmit}
|
|
72
|
+
onValuesChange={setValues}
|
|
73
|
+
className="lulichat-contact-form"
|
|
74
|
+
>
|
|
75
|
+
<div className="lulichat-form-header">
|
|
76
|
+
<div
|
|
77
|
+
style={{
|
|
78
|
+
marginBottom: "20px",
|
|
79
|
+
display: "flex",
|
|
80
|
+
alignItems: "end",
|
|
81
|
+
columnGap: 8,
|
|
82
|
+
}}
|
|
83
|
+
>
|
|
84
|
+
<Hello
|
|
85
|
+
style={{ display: "block" }}
|
|
86
|
+
color="#DEDEDE6A"
|
|
87
|
+
height={40}
|
|
88
|
+
width={50}
|
|
89
|
+
/>
|
|
90
|
+
<h3 style={{ fontWeight: 500 }}>Hello,</h3>
|
|
91
|
+
</div>
|
|
92
|
+
<h3 style={{ marginBottom: 8 }} className="lulichat-title">
|
|
93
|
+
Welcome to {companyName} Live Chat
|
|
94
|
+
</h3>
|
|
95
|
+
<p
|
|
96
|
+
role="button"
|
|
97
|
+
onClickCapture={() => setOpen(!open)}
|
|
98
|
+
style={{
|
|
99
|
+
textDecoration: "underline",
|
|
100
|
+
lineHeight: "1rem",
|
|
101
|
+
cursor: "pointer",
|
|
102
|
+
}}
|
|
103
|
+
>
|
|
104
|
+
Please provide your details for a better support experience{" "}
|
|
105
|
+
<DropdownArrow
|
|
106
|
+
style={{
|
|
107
|
+
display: "inline-block",
|
|
108
|
+
marginBottom: -2,
|
|
109
|
+
}}
|
|
110
|
+
height={20}
|
|
111
|
+
width={20}
|
|
112
|
+
/>
|
|
113
|
+
</p>
|
|
114
|
+
</div>
|
|
115
|
+
<div className={`lulichat-form-group${open ? " open" : ""}`}>
|
|
116
|
+
<Form.Item error={errors.name} label="Name (Optional)" name="name">
|
|
117
|
+
<Input
|
|
118
|
+
id="name"
|
|
119
|
+
placeholder="Enter your name"
|
|
120
|
+
type="text"
|
|
121
|
+
value={formData.name}
|
|
122
|
+
onChange={(e) => handleInputChange("name", e.target.value)}
|
|
123
|
+
className={"transparent" + (errors.name ? " error" : "")}
|
|
124
|
+
/>
|
|
125
|
+
</Form.Item>
|
|
126
|
+
|
|
127
|
+
<Form.Item name="email" error={errors.email} label="Email">
|
|
128
|
+
<Input
|
|
129
|
+
id="email"
|
|
130
|
+
type="email"
|
|
131
|
+
placeholder="Enter your valid email"
|
|
132
|
+
value={formData.email}
|
|
133
|
+
onChange={(e) => handleInputChange("email", e.target.value)}
|
|
134
|
+
className={"transparent" + (errors.name ? " error" : "")}
|
|
135
|
+
/>
|
|
136
|
+
</Form.Item>
|
|
137
|
+
<Button
|
|
138
|
+
type="submit"
|
|
139
|
+
disabled={isLoading || !values.email}
|
|
140
|
+
style={{ width: "100%", marginTop: 10 }}
|
|
141
|
+
className="lulichat-contact-form-btn"
|
|
142
|
+
>
|
|
143
|
+
{isLoading ? (
|
|
144
|
+
<>
|
|
145
|
+
<Loader />
|
|
146
|
+
Submitting...
|
|
147
|
+
</>
|
|
148
|
+
) : (
|
|
149
|
+
"Submit"
|
|
150
|
+
)}
|
|
151
|
+
</Button>
|
|
152
|
+
</div>
|
|
153
|
+
</Form>
|
|
154
|
+
);
|
|
155
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { ChatWidget } from "./ChatWidget";
|
|
3
|
+
import { LuliChatConfig } from "../types";
|
|
4
|
+
|
|
5
|
+
interface LuliChatProps {
|
|
6
|
+
apiKey: string;
|
|
7
|
+
baseUrl?: string;
|
|
8
|
+
position?: "bottom-right" | "bottom-left" | "top-right" | "top-left";
|
|
9
|
+
primaryColor?: string;
|
|
10
|
+
companyName?: string;
|
|
11
|
+
welcomeMessage?: string;
|
|
12
|
+
requireContactInfo?: boolean;
|
|
13
|
+
allowAnonymous?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const LuliChat: React.FC<LuliChatProps> = ({
|
|
17
|
+
apiKey,
|
|
18
|
+
baseUrl,
|
|
19
|
+
position = "bottom-right",
|
|
20
|
+
primaryColor = "#007bff",
|
|
21
|
+
companyName = "Support",
|
|
22
|
+
welcomeMessage = "Hello! How can we help you today?",
|
|
23
|
+
requireContactInfo = true,
|
|
24
|
+
allowAnonymous = true,
|
|
25
|
+
}) => {
|
|
26
|
+
const config: LuliChatConfig = {
|
|
27
|
+
apiKey,
|
|
28
|
+
baseUrl,
|
|
29
|
+
position,
|
|
30
|
+
primaryColor,
|
|
31
|
+
companyName,
|
|
32
|
+
welcomeMessage,
|
|
33
|
+
requireContactInfo,
|
|
34
|
+
allowAnonymous,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
if (!apiKey) {
|
|
38
|
+
console.error("LuliChat: API key is required");
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return <ChatWidget config={config} />;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// Export all types and components for external use
|
|
46
|
+
export * from "../types";
|
|
47
|
+
export { LuliChatAPI } from "../utils/api";
|
|
48
|
+
export { ChatSocket } from "../lib/socket";
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import clsx from "clsx";
|
|
3
|
+
|
|
4
|
+
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
5
|
+
variant?:
|
|
6
|
+
| "primary"
|
|
7
|
+
| "destructive"
|
|
8
|
+
| "outline"
|
|
9
|
+
| "secondary"
|
|
10
|
+
| "ghost"
|
|
11
|
+
| "link";
|
|
12
|
+
size?: "md" | "sm" | "lg" | "icon";
|
|
13
|
+
shape?: "circle" | "rounded" | "none";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
17
|
+
(
|
|
18
|
+
{
|
|
19
|
+
className,
|
|
20
|
+
variant = "outline",
|
|
21
|
+
size = "md",
|
|
22
|
+
shape = "rounded",
|
|
23
|
+
...props
|
|
24
|
+
},
|
|
25
|
+
ref
|
|
26
|
+
) => {
|
|
27
|
+
return (
|
|
28
|
+
<button
|
|
29
|
+
className={clsx(
|
|
30
|
+
"lulichat-btn",
|
|
31
|
+
`lulichat-btn-${variant}`,
|
|
32
|
+
`lulichat-btn-${size}`,
|
|
33
|
+
`lulichat-btn-${shape}`,
|
|
34
|
+
className
|
|
35
|
+
)}
|
|
36
|
+
ref={ref}
|
|
37
|
+
{...props}
|
|
38
|
+
/>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
);
|
|
42
|
+
Button.displayName = "Button";
|
|
43
|
+
|
|
44
|
+
export default Button;
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { cn } from "@/utils";
|
|
2
|
+
import React, { useRef, useImperativeHandle, useCallback } from "react";
|
|
3
|
+
import Input from "./Input";
|
|
4
|
+
|
|
5
|
+
interface FormProps<T> extends React.FormHTMLAttributes<HTMLFormElement> {
|
|
6
|
+
onValuesChange?: (values: T) => void;
|
|
7
|
+
initialValues?: T;
|
|
8
|
+
onFinish?: (values: T) => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface FormRefObject<T> {
|
|
12
|
+
getValues(): T;
|
|
13
|
+
getFieldValue: <K extends keyof T>(fieldName: K) => T[K];
|
|
14
|
+
elementRef: () => HTMLFormElement | null;
|
|
15
|
+
setFieldValue: <K extends keyof T>(fieldName: K, value: T[K]) => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const FormContext = React.createContext<FormRefObject<any> | undefined>(
|
|
19
|
+
undefined
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
type FormComponent = React.ForwardRefExoticComponent<
|
|
23
|
+
FormProps<Record<string, any>> &
|
|
24
|
+
React.RefAttributes<FormRefObject<Record<string, any>>>
|
|
25
|
+
> & {
|
|
26
|
+
Item: typeof FormItem;
|
|
27
|
+
Label: typeof FormLabel;
|
|
28
|
+
useForm: typeof useForm;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const Form: FormComponent = React.forwardRef(
|
|
32
|
+
<T extends Record<string, any>>(
|
|
33
|
+
{ children, onValuesChange, initialValues, ...props }: FormProps<T>,
|
|
34
|
+
ref: React.Ref<FormRefObject<T>>
|
|
35
|
+
) => {
|
|
36
|
+
const values = useRef<T>(initialValues || ({} as T));
|
|
37
|
+
const self = useRef<HTMLFormElement>(null);
|
|
38
|
+
|
|
39
|
+
const setFieldValue = useCallback(
|
|
40
|
+
<K extends keyof T>(fieldName: K, value: T[K]) => {
|
|
41
|
+
values.current[fieldName] = value;
|
|
42
|
+
if (onValuesChange) onValuesChange({ ...values.current });
|
|
43
|
+
},
|
|
44
|
+
[onValuesChange]
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
const methods: FormRefObject<T> = {
|
|
48
|
+
getValues: () => values.current!,
|
|
49
|
+
getFieldValue: (fieldName) => values.current?.[fieldName]!,
|
|
50
|
+
elementRef: () => self.current,
|
|
51
|
+
setFieldValue,
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
useImperativeHandle(ref, () => methods);
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<FormContext.Provider value={methods}>
|
|
58
|
+
<form {...props} ref={self} onSubmit={props.onSubmit}>
|
|
59
|
+
{children}
|
|
60
|
+
</form>
|
|
61
|
+
</FormContext.Provider>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
) as FormComponent;
|
|
65
|
+
|
|
66
|
+
interface FormItemProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
67
|
+
name?: string;
|
|
68
|
+
children?: React.ReactNode;
|
|
69
|
+
label?: string;
|
|
70
|
+
required?: boolean;
|
|
71
|
+
error?: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const FormItem = React.forwardRef<HTMLDivElement, FormItemProps>(
|
|
75
|
+
({ name, children, ...props }, ref) => {
|
|
76
|
+
const context = React.useContext(FormContext)!;
|
|
77
|
+
|
|
78
|
+
const handleChange = function (
|
|
79
|
+
this: React.ReactElement<
|
|
80
|
+
React.InputHTMLAttributes<HTMLInputElement | HTMLTextAreaElement>
|
|
81
|
+
>,
|
|
82
|
+
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
|
|
83
|
+
) {
|
|
84
|
+
if (context && name) {
|
|
85
|
+
context.setFieldValue(name, e.target.value);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
this.props.onChange?.(e);
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
// Clone children and inject value and onChange if name is provided
|
|
92
|
+
const enhancedChildren = React.Children.map(children, (child) => {
|
|
93
|
+
if (React.isValidElement(child) && name && child.type === Input) {
|
|
94
|
+
let _child = child as React.ReactElement<
|
|
95
|
+
React.InputHTMLAttributes<HTMLInputElement | HTMLTextAreaElement>
|
|
96
|
+
>;
|
|
97
|
+
return React.cloneElement(_child, {
|
|
98
|
+
..._child.props,
|
|
99
|
+
name,
|
|
100
|
+
value: context?.getFieldValue(name) ?? "",
|
|
101
|
+
onChange: handleChange.bind(_child),
|
|
102
|
+
required: props.required,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
return child;
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const _props = Object.create(props);
|
|
109
|
+
delete _props.required;
|
|
110
|
+
delete _props.label;
|
|
111
|
+
|
|
112
|
+
return (
|
|
113
|
+
<div className="lulichat-form-item" ref={ref} {..._props}>
|
|
114
|
+
{props.label && (
|
|
115
|
+
<FormLabel htmlFor={name} required={props.required}>
|
|
116
|
+
{props.label}
|
|
117
|
+
</FormLabel>
|
|
118
|
+
)}
|
|
119
|
+
{enhancedChildren}
|
|
120
|
+
{props.error && <p className="error">{props.error}</p>}
|
|
121
|
+
</div>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
interface FormLabelProps extends React.LabelHTMLAttributes<HTMLLabelElement> {
|
|
127
|
+
required?: boolean;
|
|
128
|
+
htmlFor?: string;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const FormLabel = React.forwardRef<HTMLLabelElement, FormLabelProps>(
|
|
132
|
+
(props, ref) => {
|
|
133
|
+
const required = props.required;
|
|
134
|
+
const children = props.children;
|
|
135
|
+
|
|
136
|
+
return (
|
|
137
|
+
<label
|
|
138
|
+
{...props}
|
|
139
|
+
className={cn("lulichat-form-label", props.className)}
|
|
140
|
+
ref={ref}
|
|
141
|
+
>
|
|
142
|
+
{children}
|
|
143
|
+
{required && <b style={{ color: "red" }}>*</b>}
|
|
144
|
+
</label>
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
const useForm = <T,>(initialValues: T) => {
|
|
150
|
+
const values = useRef<T>(initialValues || ({} as T));
|
|
151
|
+
const self = useRef<HTMLFormElement>(null);
|
|
152
|
+
|
|
153
|
+
const setFieldValue = useCallback(
|
|
154
|
+
<K extends keyof T>(fieldName: K, value: T[K]) => {
|
|
155
|
+
values.current[fieldName] = value;
|
|
156
|
+
},
|
|
157
|
+
[]
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
const methods: FormRefObject<T> = {
|
|
161
|
+
getValues: () => values.current!,
|
|
162
|
+
getFieldValue: (fieldName) => values.current?.[fieldName]!,
|
|
163
|
+
elementRef: () => self.current,
|
|
164
|
+
setFieldValue,
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
return [methods];
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
Form.Item = FormItem;
|
|
171
|
+
Form.Label = FormLabel;
|
|
172
|
+
Form.useForm = useForm;
|
|
173
|
+
|
|
174
|
+
export default Form;
|