Updates
This commit is contained in:
parent
998158369a
commit
5587024789
79
components/lib/editors/TinyMCE/index.tsx
Normal file
79
components/lib/editors/TinyMCE/index.tsx
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { RawEditorOptions, TinyMCE, Editor } from "./tinymce";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
export type TinyMCEEditorProps = {
|
||||||
|
tinyMCE?: TinyMCE | null;
|
||||||
|
options?: RawEditorOptions;
|
||||||
|
editorRef?: React.MutableRefObject<Editor | null>;
|
||||||
|
setEditor?: React.Dispatch<React.SetStateAction<Editor>>;
|
||||||
|
wrapperProps?: React.DetailedHTMLProps<
|
||||||
|
React.HTMLAttributes<HTMLDivElement>,
|
||||||
|
HTMLDivElement
|
||||||
|
>;
|
||||||
|
defaultValue?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
let interval: any;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* # Tiny MCE Editor Component
|
||||||
|
* @className_wrapper twui-rte-wrapper
|
||||||
|
*/
|
||||||
|
export default function TinyMCEEditor({
|
||||||
|
options,
|
||||||
|
editorRef,
|
||||||
|
setEditor,
|
||||||
|
tinyMCE,
|
||||||
|
wrapperProps,
|
||||||
|
defaultValue,
|
||||||
|
}: TinyMCEEditorProps) {
|
||||||
|
const editorComponentRef = React.useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const FINAL_HEIGHT = options?.height || 500;
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!editorComponentRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
tinyMCE?.init({
|
||||||
|
height: FINAL_HEIGHT,
|
||||||
|
menubar: false,
|
||||||
|
plugins: [
|
||||||
|
"advlist lists link image charmap print preview anchor",
|
||||||
|
"searchreplace visualblocks code fullscreen",
|
||||||
|
"insertdatetime media table paste code help wordcount",
|
||||||
|
],
|
||||||
|
toolbar:
|
||||||
|
"undo redo | blocks | bold italic | bullist numlist outdent indent | removeformat",
|
||||||
|
content_style:
|
||||||
|
"body { font-family:Helvetica,Arial,sans-serif; font-size:14px }",
|
||||||
|
init_instance_callback: (editor) => {
|
||||||
|
setEditor?.(editor as any);
|
||||||
|
if (editorRef) editorRef.current = editor as any;
|
||||||
|
if (defaultValue) editor.setContent(defaultValue);
|
||||||
|
},
|
||||||
|
base_url: "https://datasquirel.com/tinymce-public",
|
||||||
|
body_class: "twui-tinymce",
|
||||||
|
...options,
|
||||||
|
license_key: "gpl",
|
||||||
|
target: editorComponentRef.current,
|
||||||
|
});
|
||||||
|
}, [tinyMCE]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
{...wrapperProps}
|
||||||
|
ref={editorComponentRef}
|
||||||
|
style={{
|
||||||
|
height: FINAL_HEIGHT + "px",
|
||||||
|
...wrapperProps?.style,
|
||||||
|
}}
|
||||||
|
className={twMerge(
|
||||||
|
"bg-slate-200 dark:bg-slate-700 rounded-sm",
|
||||||
|
"twui-rte-wrapper"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
3313
components/lib/editors/TinyMCE/tinymce.d.ts
vendored
Normal file
3313
components/lib/editors/TinyMCE/tinymce.d.ts
vendored
Normal file
File diff suppressed because it is too large
Load Diff
34
components/lib/editors/TinyMCE/useTinyMCE.tsx
Normal file
34
components/lib/editors/TinyMCE/useTinyMCE.tsx
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { TinyMCE } from "./tinymce";
|
||||||
|
|
||||||
|
let interval: any;
|
||||||
|
|
||||||
|
export default function useTinyMCE() {
|
||||||
|
const [tinyMCE, setTinyMCE] = React.useState<TinyMCE | null>(null);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
// @ts-ignore
|
||||||
|
if (window.tinymce) {
|
||||||
|
console.log("Tinymce already exists");
|
||||||
|
// @ts-ignore
|
||||||
|
setTinyMCE(window.tinymce);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const script = document.createElement("script");
|
||||||
|
script.src = "https://datasquirel.com/tinymce-public/tinymce.min.js";
|
||||||
|
script.async = true;
|
||||||
|
|
||||||
|
document.head.appendChild(script);
|
||||||
|
|
||||||
|
script.onload = () => {
|
||||||
|
// @ts-ignore
|
||||||
|
if (window.tinymce) {
|
||||||
|
// @ts-ignore
|
||||||
|
setTinyMCE(window.tinymce);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return { tinyMCE };
|
||||||
|
}
|
@ -18,7 +18,7 @@ export default function Border({ spacing, ...props }: TWUI_BORDER_PROPS) {
|
|||||||
{...props}
|
{...props}
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
"relative flex items-center gap-2 border rounded",
|
"relative flex items-center gap-2 border rounded",
|
||||||
"border-slate-300 dark:border-white/20",
|
"border-slate-300 dark:border-white/10",
|
||||||
spacing
|
spacing
|
||||||
? spacing == "normal"
|
? spacing == "normal"
|
||||||
? "px-3 py-2"
|
? "px-3 py-2"
|
||||||
|
@ -23,7 +23,7 @@ export default function Breadcrumbs() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
pathLinks.forEach((linkText, index, array) => {
|
pathLinks.forEach((linkText, index, array) => {
|
||||||
if (!linkText?.match(/./) || index == 1) {
|
if (!linkText?.match(/./)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -56,7 +56,7 @@ export default function Breadcrumbs() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Row className="gap-4">
|
<Row className="gap-4 flex-nowrap whitespace-nowrap overflow-x-auto w-full">
|
||||||
{links.map((linkObject, index, array) => {
|
{links.map((linkObject, index, array) => {
|
||||||
if (index === links.length - 1) {
|
if (index === links.length - 1) {
|
||||||
return (
|
return (
|
||||||
|
@ -3,7 +3,11 @@ import { twMerge } from "tailwind-merge";
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* # General Card
|
* # General Card
|
||||||
* @className_wrapper twui-card
|
* @className twui-card
|
||||||
|
* @className twui-card-link
|
||||||
|
*
|
||||||
|
* @info use the classname `nested-link` to prevent the card from being clickable when
|
||||||
|
* a link (or the target element with this calss) inside the card is clicked.
|
||||||
*/
|
*/
|
||||||
export default function Card({
|
export default function Card({
|
||||||
href,
|
href,
|
||||||
@ -24,7 +28,9 @@ export default function Card({
|
|||||||
className={twMerge(
|
className={twMerge(
|
||||||
"flex flex-row items-center p-4 rounded bg-white dark:bg-white/10",
|
"flex flex-row items-center p-4 rounded bg-white dark:bg-white/10",
|
||||||
"border border-slate-200 dark:border-white/10 border-solid",
|
"border border-slate-200 dark:border-white/10 border-solid",
|
||||||
href ? "hover:bg-slate-100 dark:hover:bg-white/30" : "",
|
href
|
||||||
|
? "hover:bg-slate-100 dark:hover:bg-white/30 hover:border-slate-400 dark:hover:border-white/20"
|
||||||
|
: "",
|
||||||
"twui-card",
|
"twui-card",
|
||||||
props.className
|
props.className
|
||||||
)}
|
)}
|
||||||
@ -35,7 +41,26 @@ export default function Card({
|
|||||||
|
|
||||||
if (href) {
|
if (href) {
|
||||||
return (
|
return (
|
||||||
<a href={href} {...linkProps}>
|
<a
|
||||||
|
href={href}
|
||||||
|
{...linkProps}
|
||||||
|
onClick={(e) => {
|
||||||
|
const targetEl = e.target as HTMLElement;
|
||||||
|
if (targetEl.closest(".nested-link")) {
|
||||||
|
e.preventDefault();
|
||||||
|
} else if (e.ctrlKey) {
|
||||||
|
window.open(href, "_blank");
|
||||||
|
} else {
|
||||||
|
window.location.href = href;
|
||||||
|
}
|
||||||
|
linkProps?.onClick?.(e);
|
||||||
|
}}
|
||||||
|
className={twMerge(
|
||||||
|
"cursor-pointer",
|
||||||
|
"twui-card-link",
|
||||||
|
linkProps?.className
|
||||||
|
)}
|
||||||
|
>
|
||||||
{component}
|
{component}
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
|
146
components/lib/elements/Dropdown.tsx
Normal file
146
components/lib/elements/Dropdown.tsx
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
import React, {
|
||||||
|
DetailedHTMLProps,
|
||||||
|
HTMLAttributes,
|
||||||
|
PropsWithChildren,
|
||||||
|
} from "react";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
export const TWUIDropdownContentPositions = [
|
||||||
|
"left",
|
||||||
|
"right",
|
||||||
|
"center",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export type TWUI_DROPDOWN_PROPS = PropsWithChildren &
|
||||||
|
DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement> & {
|
||||||
|
target: React.ReactNode;
|
||||||
|
contentWrapperProps?: DetailedHTMLProps<
|
||||||
|
HTMLAttributes<HTMLDivElement>,
|
||||||
|
HTMLDivElement
|
||||||
|
>;
|
||||||
|
targetWrapperProps?: DetailedHTMLProps<
|
||||||
|
HTMLAttributes<HTMLDivElement>,
|
||||||
|
HTMLDivElement
|
||||||
|
>;
|
||||||
|
debounce?: number;
|
||||||
|
hoverOpen?: boolean;
|
||||||
|
position?: (typeof TWUIDropdownContentPositions)[number];
|
||||||
|
topOffset?: number;
|
||||||
|
externalSetOpen?: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
};
|
||||||
|
|
||||||
|
let timeout: any;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* # Toggle Component
|
||||||
|
* @className_wrapper twui-dropdown-wrapper
|
||||||
|
* @className_wrapper twui-dropdown-target
|
||||||
|
* @className_wrapper twui-dropdown-content
|
||||||
|
*/
|
||||||
|
export default function Dropdown({
|
||||||
|
contentWrapperProps,
|
||||||
|
targetWrapperProps,
|
||||||
|
hoverOpen,
|
||||||
|
debounce = 500,
|
||||||
|
target,
|
||||||
|
position = "center",
|
||||||
|
topOffset,
|
||||||
|
externalSetOpen,
|
||||||
|
...props
|
||||||
|
}: TWUI_DROPDOWN_PROPS) {
|
||||||
|
const [open, setOpen] = React.useState(false);
|
||||||
|
|
||||||
|
const dropdownRef = React.useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const handleClickOutside = React.useCallback((e: MouseEvent) => {
|
||||||
|
const targetEl = e.target as HTMLElement;
|
||||||
|
const closestWrapper = targetEl.closest(".twui-dropdown-wrapper");
|
||||||
|
|
||||||
|
if (!closestWrapper) {
|
||||||
|
externalSetOpen?.(false);
|
||||||
|
return setOpen(false);
|
||||||
|
}
|
||||||
|
if (closestWrapper && closestWrapper !== dropdownRef.current) {
|
||||||
|
externalSetOpen?.(false);
|
||||||
|
return setOpen(false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
document.addEventListener("click", handleClickOutside);
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener("click", handleClickOutside);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
{...props}
|
||||||
|
className={twMerge(
|
||||||
|
"flex flex-col items-center relative",
|
||||||
|
"twui-dropdown-wrapper",
|
||||||
|
props.className
|
||||||
|
)}
|
||||||
|
onMouseEnter={() => {
|
||||||
|
if (!hoverOpen) return;
|
||||||
|
window.clearTimeout(timeout);
|
||||||
|
externalSetOpen?.(true);
|
||||||
|
setOpen(true);
|
||||||
|
}}
|
||||||
|
onMouseLeave={() => {
|
||||||
|
if (!hoverOpen) return;
|
||||||
|
timeout = setTimeout(() => {
|
||||||
|
externalSetOpen?.(false);
|
||||||
|
setOpen(false);
|
||||||
|
}, debounce);
|
||||||
|
}}
|
||||||
|
onBlur={() => {
|
||||||
|
window.clearTimeout(timeout);
|
||||||
|
}}
|
||||||
|
ref={dropdownRef}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
onClick={() => {
|
||||||
|
externalSetOpen?.(!open);
|
||||||
|
setOpen(!open);
|
||||||
|
}}
|
||||||
|
className={twMerge(
|
||||||
|
"cursor-pointer",
|
||||||
|
"twui-dropdown-target",
|
||||||
|
targetWrapperProps?.className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{target}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
{...contentWrapperProps}
|
||||||
|
className={twMerge(
|
||||||
|
"absolute z-10",
|
||||||
|
position == "left"
|
||||||
|
? "left-0"
|
||||||
|
: position == "right"
|
||||||
|
? "right-0"
|
||||||
|
: "",
|
||||||
|
open ? "flex" : "hidden",
|
||||||
|
"twui-dropdown-content",
|
||||||
|
contentWrapperProps?.className
|
||||||
|
)}
|
||||||
|
onMouseEnter={() => {
|
||||||
|
if (!hoverOpen) return;
|
||||||
|
window.clearTimeout(timeout);
|
||||||
|
}}
|
||||||
|
onBlur={() => {
|
||||||
|
if (!hoverOpen) return;
|
||||||
|
window.clearTimeout(timeout);
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
top: `calc(100% + ${topOffset || 0}px)`,
|
||||||
|
...contentWrapperProps?.style,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -8,6 +8,7 @@ type Props = DetailedHTMLProps<
|
|||||||
HTMLDivElement
|
HTMLDivElement
|
||||||
> & {
|
> & {
|
||||||
target: React.ReactNode;
|
target: React.ReactNode;
|
||||||
|
targetRef?: React.MutableRefObject<HTMLDivElement | undefined>;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -15,11 +16,12 @@ type Props = DetailedHTMLProps<
|
|||||||
* @className_wrapper twui-modal-root
|
* @className_wrapper twui-modal-root
|
||||||
* @className_wrapper twui-modal
|
* @className_wrapper twui-modal
|
||||||
*/
|
*/
|
||||||
export default function Modal({ target, ...props }: Props) {
|
export default function Modal({ target, targetRef, ...props }: Props) {
|
||||||
const [wrapper, setWrapper] = React.useState<HTMLDivElement | null>(null);
|
const [wrapper, setWrapper] = React.useState<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const wrapperEl = document.createElement("div");
|
const wrapperEl = document.createElement("div");
|
||||||
|
|
||||||
wrapperEl.className = twMerge(
|
wrapperEl.className = twMerge(
|
||||||
"fixed z-[200000] top-0 left-0 w-screen h-screen",
|
"fixed z-[200000] top-0 left-0 w-screen h-screen",
|
||||||
"flex flex-col items-center justify-center",
|
"flex flex-col items-center justify-center",
|
||||||
@ -57,6 +59,7 @@ export default function Modal({ target, ...props }: Props) {
|
|||||||
const root = createRoot(wrapper);
|
const root = createRoot(wrapper);
|
||||||
root.render(modalEl);
|
root.render(modalEl);
|
||||||
}}
|
}}
|
||||||
|
ref={targetRef as any}
|
||||||
>
|
>
|
||||||
{target}
|
{target}
|
||||||
</div>
|
</div>
|
||||||
|
@ -20,7 +20,7 @@ export default function Paper({
|
|||||||
<div
|
<div
|
||||||
{...props}
|
{...props}
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
"flex flex-row items-center p-4 rounded bg-white dark:bg-white/10",
|
"flex flex-col items-start p-4 rounded bg-white dark:bg-white/10 gap-4",
|
||||||
"border border-slate-200 dark:border-white/10 border-solid w-full",
|
"border border-slate-200 dark:border-white/10 border-solid w-full",
|
||||||
"twui-paper",
|
"twui-paper",
|
||||||
props.className
|
props.className
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
import Input from "../form/Input";
|
import Input, { InputProps } from "../form/Input";
|
||||||
import Button from "../layout/Button";
|
import Button from "../layout/Button";
|
||||||
import Row from "../layout/Row";
|
import Row from "../layout/Row";
|
||||||
import { Search as SearchIcon } from "lucide-react";
|
import { Search as SearchIcon } from "lucide-react";
|
||||||
@ -11,20 +11,13 @@ import React, {
|
|||||||
|
|
||||||
let timeout: any;
|
let timeout: any;
|
||||||
|
|
||||||
export type SearchProps = DetailedHTMLProps<
|
export type SearchProps<KeyType extends string> = DetailedHTMLProps<
|
||||||
React.HTMLAttributes<HTMLDivElement>,
|
React.HTMLAttributes<HTMLDivElement>,
|
||||||
HTMLDivElement
|
HTMLDivElement
|
||||||
> & {
|
> & {
|
||||||
dispatch?: (value?: string) => void;
|
dispatch?: (value?: string) => void;
|
||||||
delay?: number;
|
delay?: number;
|
||||||
inputProps?: DetailedHTMLProps<
|
inputProps?: InputProps<KeyType>;
|
||||||
InputHTMLAttributes<HTMLInputElement>,
|
|
||||||
HTMLInputElement
|
|
||||||
> &
|
|
||||||
DetailedHTMLProps<
|
|
||||||
TextareaHTMLAttributes<HTMLTextAreaElement>,
|
|
||||||
HTMLTextAreaElement
|
|
||||||
>;
|
|
||||||
buttonProps?: DetailedHTMLProps<
|
buttonProps?: DetailedHTMLProps<
|
||||||
React.ButtonHTMLAttributes<HTMLButtonElement>,
|
React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||||
HTMLButtonElement
|
HTMLButtonElement
|
||||||
@ -37,13 +30,13 @@ export type SearchProps = DetailedHTMLProps<
|
|||||||
* @className_circle twui-search-input
|
* @className_circle twui-search-input
|
||||||
* @className_circle twui-search-button
|
* @className_circle twui-search-button
|
||||||
*/
|
*/
|
||||||
export default function Search({
|
export default function Search<KeyType extends string>({
|
||||||
dispatch,
|
dispatch,
|
||||||
delay = 500,
|
delay = 500,
|
||||||
inputProps,
|
inputProps,
|
||||||
buttonProps,
|
buttonProps,
|
||||||
...props
|
...props
|
||||||
}: SearchProps) {
|
}: SearchProps<KeyType>) {
|
||||||
const [input, setInput] = React.useState("");
|
const [input, setInput] = React.useState("");
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
|
164
components/lib/elements/StarRating.tsx
Normal file
164
components/lib/elements/StarRating.tsx
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
import { LucideProps, Star } from "lucide-react";
|
||||||
|
import React, {
|
||||||
|
DetailedHTMLProps,
|
||||||
|
ForwardRefExoticComponent,
|
||||||
|
HTMLAttributes,
|
||||||
|
RefAttributes,
|
||||||
|
} from "react";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
type StarProps = {
|
||||||
|
total?: number;
|
||||||
|
value?: number;
|
||||||
|
size?: number;
|
||||||
|
starProps?: LucideProps;
|
||||||
|
allowRating?: boolean;
|
||||||
|
setValueExternal?: React.Dispatch<React.SetStateAction<number>>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TWUI_STAR_RATING_PROPS = DetailedHTMLProps<
|
||||||
|
HTMLAttributes<HTMLDivElement>,
|
||||||
|
HTMLDivElement
|
||||||
|
> &
|
||||||
|
StarProps;
|
||||||
|
|
||||||
|
let timeout: any;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* # Star Rating Component
|
||||||
|
* @className_wrapper twui-star-rating
|
||||||
|
*/
|
||||||
|
export default function StarRating({
|
||||||
|
total = 5,
|
||||||
|
value = 0,
|
||||||
|
size,
|
||||||
|
starProps,
|
||||||
|
allowRating,
|
||||||
|
setValueExternal,
|
||||||
|
...props
|
||||||
|
}: TWUI_STAR_RATING_PROPS) {
|
||||||
|
const totalArray = Array(total).fill(null);
|
||||||
|
|
||||||
|
const [finalValue, setFinalValue] = React.useState(value);
|
||||||
|
const [selectedStarValue, setSelectedStarValue] = React.useState(value);
|
||||||
|
|
||||||
|
const starClicked = React.useRef(false);
|
||||||
|
const sectionHovered = React.useRef(false);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
window.clearTimeout(timeout);
|
||||||
|
timeout = setTimeout(() => {
|
||||||
|
setValueExternal?.(finalValue);
|
||||||
|
}, 500);
|
||||||
|
}, [selectedStarValue]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
{...props}
|
||||||
|
className={twMerge(
|
||||||
|
"flex flex-row items-center gap-0 -ml-[2px]",
|
||||||
|
"twui-star-rating",
|
||||||
|
props.className
|
||||||
|
)}
|
||||||
|
onMouseEnter={() => {
|
||||||
|
sectionHovered.current = true;
|
||||||
|
}}
|
||||||
|
onMouseLeave={() => {
|
||||||
|
sectionHovered.current = false;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{totalArray.map((_, index) => {
|
||||||
|
return (
|
||||||
|
<StarComponent
|
||||||
|
{...{
|
||||||
|
total,
|
||||||
|
value,
|
||||||
|
size,
|
||||||
|
starProps,
|
||||||
|
index,
|
||||||
|
allowRating,
|
||||||
|
finalValue,
|
||||||
|
setFinalValue,
|
||||||
|
starClicked,
|
||||||
|
selectedStarValue,
|
||||||
|
sectionHovered,
|
||||||
|
setSelectedStarValue,
|
||||||
|
}}
|
||||||
|
key={index}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function StarComponent({
|
||||||
|
value = 0,
|
||||||
|
size = 20,
|
||||||
|
starProps,
|
||||||
|
index,
|
||||||
|
allowRating,
|
||||||
|
finalValue,
|
||||||
|
setFinalValue,
|
||||||
|
starClicked,
|
||||||
|
sectionHovered,
|
||||||
|
setSelectedStarValue,
|
||||||
|
selectedStarValue,
|
||||||
|
}: StarProps & {
|
||||||
|
index: number;
|
||||||
|
finalValue: number;
|
||||||
|
setFinalValue: React.Dispatch<React.SetStateAction<number>>;
|
||||||
|
setSelectedStarValue: React.Dispatch<React.SetStateAction<number>>;
|
||||||
|
starClicked: React.MutableRefObject<boolean>;
|
||||||
|
sectionHovered: React.MutableRefObject<boolean>;
|
||||||
|
selectedStarValue: number;
|
||||||
|
}) {
|
||||||
|
const isActive = index < finalValue;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={twMerge("p-[2px]", allowRating && "cursor-pointer")}
|
||||||
|
onMouseEnter={() => {
|
||||||
|
if (!allowRating) return;
|
||||||
|
|
||||||
|
setFinalValue(index + 1);
|
||||||
|
}}
|
||||||
|
onMouseLeave={() => {
|
||||||
|
if (!allowRating) return;
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
if (sectionHovered.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!starClicked.current) {
|
||||||
|
setFinalValue(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedStarValue) {
|
||||||
|
setFinalValue(selectedStarValue);
|
||||||
|
}
|
||||||
|
}, 200);
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
if (!allowRating) return;
|
||||||
|
|
||||||
|
starClicked.current = true;
|
||||||
|
setSelectedStarValue(index + 1);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Star
|
||||||
|
size={size}
|
||||||
|
className={twMerge(
|
||||||
|
"text-slate-300 dark:text-white/20",
|
||||||
|
isActive &&
|
||||||
|
"text-orange-500 dark:text-orange-400 fill-orange-500 dark:fill-orange-400",
|
||||||
|
// allowRating &&
|
||||||
|
// "hover:text-orange-500 hover:dark:text-orange-400 hover:fill-orange-500 hover:dark:fill-orange-400",
|
||||||
|
starProps?.className
|
||||||
|
)}
|
||||||
|
{...starProps}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
108
components/lib/elements/Tabs.tsx
Normal file
108
components/lib/elements/Tabs.tsx
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
import React, { DetailedHTMLProps, HTMLAttributes } from "react";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
import Border from "./Border";
|
||||||
|
import Stack from "../layout/Stack";
|
||||||
|
import Row from "../layout/Row";
|
||||||
|
import Span from "../layout/Span";
|
||||||
|
|
||||||
|
export type TWUITabsObject = {
|
||||||
|
title: string;
|
||||||
|
value: string;
|
||||||
|
content: React.ReactNode;
|
||||||
|
defaultActive?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TWUI_TOGGLE_PROPS = React.ComponentProps<typeof Stack> & {
|
||||||
|
tabsContentArray: TWUITabsObject[];
|
||||||
|
tabsBorderProps?: React.ComponentProps<typeof Border>;
|
||||||
|
tabsButtonsWrapperProps?: React.DetailedHTMLProps<
|
||||||
|
React.HTMLAttributes<HTMLDivElement>,
|
||||||
|
HTMLDivElement
|
||||||
|
>;
|
||||||
|
centered?: boolean;
|
||||||
|
debounce?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* # Tabs Component
|
||||||
|
* @className twui-tabs-wrapper
|
||||||
|
* @className twui-tab-buttons
|
||||||
|
* @className twui-tab-button-active
|
||||||
|
* @className twui-tab-buttons-wrapper
|
||||||
|
*/
|
||||||
|
export default function Tabs({
|
||||||
|
tabsContentArray,
|
||||||
|
tabsBorderProps,
|
||||||
|
tabsButtonsWrapperProps,
|
||||||
|
centered,
|
||||||
|
debounce = 100,
|
||||||
|
...props
|
||||||
|
}: TWUI_TOGGLE_PROPS) {
|
||||||
|
const values = tabsContentArray.map((obj) => obj.value);
|
||||||
|
|
||||||
|
const [activeValue, setActiveValue] = React.useState(
|
||||||
|
tabsContentArray.find((ctn) => ctn.defaultActive)?.value ||
|
||||||
|
values[0] ||
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
|
||||||
|
const targetContent = tabsContentArray.find(
|
||||||
|
(ctn) => ctn.value == activeValue
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack
|
||||||
|
{...props}
|
||||||
|
className={twMerge("w-full", "twui-tabs-wrapper", props.className)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
{...tabsButtonsWrapperProps}
|
||||||
|
className={twMerge(
|
||||||
|
"w-full",
|
||||||
|
"twui-tab-buttons-wrapper",
|
||||||
|
tabsButtonsWrapperProps?.className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Border className="p-0 w-full" {...tabsBorderProps}>
|
||||||
|
<Row
|
||||||
|
className={twMerge(
|
||||||
|
"gap-0 items-stretch w-full",
|
||||||
|
centered && "justify-center"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{values.map((value, index) => {
|
||||||
|
const targetObject = tabsContentArray.find(
|
||||||
|
(ctn) => ctn.value == value
|
||||||
|
);
|
||||||
|
|
||||||
|
const isActive = value == activeValue;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className={twMerge(
|
||||||
|
"px-6 py-2 rounded -ml-[1px]",
|
||||||
|
isActive
|
||||||
|
? "bg-blue-500 text-white outline-none twui-tab-button-active"
|
||||||
|
: "text-slate-400 dark:text-white/40 hover:text-slate-800 dark:hover:text-white" +
|
||||||
|
" cursor-pointer",
|
||||||
|
"twui-tab-buttons"
|
||||||
|
)}
|
||||||
|
onClick={() => {
|
||||||
|
setActiveValue(undefined);
|
||||||
|
setTimeout(() => {
|
||||||
|
setActiveValue(value);
|
||||||
|
}, debounce);
|
||||||
|
}}
|
||||||
|
key={index}
|
||||||
|
>
|
||||||
|
{targetObject?.title}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Row>
|
||||||
|
</Border>
|
||||||
|
</div>
|
||||||
|
{targetContent?.content}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
75
components/lib/elements/Tag.tsx
Normal file
75
components/lib/elements/Tag.tsx
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
import React, { PropsWithChildren } from "react";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
export type TWUITabsObject = {
|
||||||
|
title: string;
|
||||||
|
value: string;
|
||||||
|
content: React.ReactNode;
|
||||||
|
defaultActive?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TWUI_TOGGLE_PROPS = PropsWithChildren &
|
||||||
|
React.DetailedHTMLProps<
|
||||||
|
React.HTMLAttributes<HTMLDivElement>,
|
||||||
|
HTMLDivElement
|
||||||
|
> & {
|
||||||
|
color?: "normal" | "secondary" | "error" | "success" | "gray";
|
||||||
|
variant?: "normal" | "outlined" | "ghost";
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* # Tabs Component
|
||||||
|
* @className twui-tag
|
||||||
|
*/
|
||||||
|
export default function Tag({
|
||||||
|
color,
|
||||||
|
variant,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: TWUI_TOGGLE_PROPS) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
{...props}
|
||||||
|
className={twMerge(
|
||||||
|
"text-xs px-2 py-0.5 rounded-full outline outline-0",
|
||||||
|
color == "secondary"
|
||||||
|
? "bg-violet-600 outline-violet-600"
|
||||||
|
: color == "success"
|
||||||
|
? "bg-emerald-700 outline-emerald-700"
|
||||||
|
: color == "error"
|
||||||
|
? "bg-orange-700 outline-orange-700"
|
||||||
|
: color == "gray"
|
||||||
|
? "bg-slate-100 outline-slate-200 dark:bg-white/10 dark:outline-white/20 text-slate-500 dark:text-white"
|
||||||
|
: "bg-blue-600 outline-blue-600",
|
||||||
|
variant == "outlined"
|
||||||
|
? "!bg-transparent outline-1 " +
|
||||||
|
(color == "secondary"
|
||||||
|
? "text-violet-600"
|
||||||
|
: color == "success"
|
||||||
|
? "text-emerald-700 dark:text-emerald-400"
|
||||||
|
: color == "error"
|
||||||
|
? "text-orange-700"
|
||||||
|
: color == "gray"
|
||||||
|
? "text-slate-700 dark:text-white/80"
|
||||||
|
: "text-blue-600")
|
||||||
|
: variant == "ghost"
|
||||||
|
? "!bg-transparent outline-none border-none " +
|
||||||
|
(color == "secondary"
|
||||||
|
? "text-violet-600"
|
||||||
|
: color == "success"
|
||||||
|
? "text-emerald-700 dark:text-emerald-400"
|
||||||
|
: color == "error"
|
||||||
|
? "text-orange-700"
|
||||||
|
: color == "gray"
|
||||||
|
? "text-slate-700 dark:text-white/80"
|
||||||
|
: "text-blue-600")
|
||||||
|
: "",
|
||||||
|
|
||||||
|
"twui-tag",
|
||||||
|
props.className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
100
components/lib/elements/Toast.tsx
Normal file
100
components/lib/elements/Toast.tsx
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
import React, { DetailedHTMLProps, HTMLAttributes } from "react";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
import { createRoot } from "react-dom/client";
|
||||||
|
import Card from "./Card";
|
||||||
|
import { X } from "lucide-react";
|
||||||
|
import Span from "../layout/Span";
|
||||||
|
|
||||||
|
export const ToastStyles = ["normal", "success", "error"] as const;
|
||||||
|
export const ToastColors = ToastStyles;
|
||||||
|
|
||||||
|
type Props = DetailedHTMLProps<
|
||||||
|
HTMLAttributes<HTMLDivElement>,
|
||||||
|
HTMLDivElement
|
||||||
|
> & {
|
||||||
|
open?: boolean;
|
||||||
|
setOpen?: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
closeDelay?: number;
|
||||||
|
color?: (typeof ToastStyles)[number];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* # Toast Component
|
||||||
|
* @className twui-toast-root
|
||||||
|
* @className twui-toast
|
||||||
|
* @className twui-toast-success
|
||||||
|
* @className twui-toast-error
|
||||||
|
*/
|
||||||
|
export default function Toast({
|
||||||
|
open,
|
||||||
|
setOpen,
|
||||||
|
closeDelay = 4000,
|
||||||
|
color,
|
||||||
|
...props
|
||||||
|
}: Props) {
|
||||||
|
if (!open) return null;
|
||||||
|
|
||||||
|
const toastEl = (
|
||||||
|
<Card
|
||||||
|
{...props}
|
||||||
|
className={twMerge(
|
||||||
|
"pl-6 pr-8 py-4 bg-blue-700 dark:bg-blue-800",
|
||||||
|
color == "success"
|
||||||
|
? "bg-emerald-600 dark:bg-emerald-700 twui-toast-success"
|
||||||
|
: color == "error"
|
||||||
|
? "bg-orange-600 dark:bg-orange-700 twui-toast-error"
|
||||||
|
: "",
|
||||||
|
props.className,
|
||||||
|
"twui-toast"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Span
|
||||||
|
className={twMerge(
|
||||||
|
"absolute top-2 right-2 z-[100] cursor-pointer"
|
||||||
|
)}
|
||||||
|
onClick={(e) => {
|
||||||
|
const targetEl = e.target as HTMLElement;
|
||||||
|
const rootWrapperEl = targetEl.closest(".twui-toast-root");
|
||||||
|
|
||||||
|
if (rootWrapperEl) {
|
||||||
|
rootWrapperEl.parentElement?.removeChild(rootWrapperEl);
|
||||||
|
setOpen?.(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<X size={15} />
|
||||||
|
</Span>
|
||||||
|
{props.children}
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const wrapperEl = document.createElement("div");
|
||||||
|
|
||||||
|
wrapperEl.className = twMerge(
|
||||||
|
"fixed z-[200000] bottom-10 right-10",
|
||||||
|
"flex flex-col items-center justify-center",
|
||||||
|
"twui-toast-root"
|
||||||
|
);
|
||||||
|
|
||||||
|
document.body.appendChild(wrapperEl);
|
||||||
|
const root = createRoot(wrapperEl);
|
||||||
|
root.render(toastEl);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
closeToast({ wrapperEl });
|
||||||
|
setOpen?.(false);
|
||||||
|
}, closeDelay);
|
||||||
|
|
||||||
|
return function () {
|
||||||
|
closeToast({ wrapperEl });
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeToast({ wrapperEl }: { wrapperEl: HTMLDivElement | null }) {
|
||||||
|
if (!wrapperEl) return;
|
||||||
|
wrapperEl.parentElement?.removeChild(wrapperEl);
|
||||||
|
}
|
98
components/lib/form/Checkbox.tsx
Normal file
98
components/lib/form/Checkbox.tsx
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
import React, {
|
||||||
|
DetailedHTMLProps,
|
||||||
|
HTMLAttributes,
|
||||||
|
InputHTMLAttributes,
|
||||||
|
ReactNode,
|
||||||
|
} from "react";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
import CheckMarkSVG from "../svgs/CheckMarkSVG";
|
||||||
|
|
||||||
|
export type CheckboxProps = DetailedHTMLProps<
|
||||||
|
InputHTMLAttributes<HTMLInputElement>,
|
||||||
|
HTMLInputElement
|
||||||
|
> & {
|
||||||
|
name: string;
|
||||||
|
wrapperProps?: DetailedHTMLProps<
|
||||||
|
HTMLAttributes<HTMLDivElement>,
|
||||||
|
HTMLDivElement
|
||||||
|
>;
|
||||||
|
label?: string | ReactNode;
|
||||||
|
labelProps?: DetailedHTMLProps<
|
||||||
|
HTMLAttributes<HTMLLabelElement>,
|
||||||
|
HTMLLabelElement
|
||||||
|
>;
|
||||||
|
defaultChecked?: boolean;
|
||||||
|
wrapperClassName?: string;
|
||||||
|
setChecked?: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* # Checkbox Component
|
||||||
|
* @className twui-checkbox
|
||||||
|
* @className twui-checkbox-checked
|
||||||
|
* @className twui-checkbox-unchecked
|
||||||
|
*/
|
||||||
|
export default function Checkbox({
|
||||||
|
wrapperProps,
|
||||||
|
label,
|
||||||
|
labelProps,
|
||||||
|
size,
|
||||||
|
name,
|
||||||
|
wrapperClassName,
|
||||||
|
defaultChecked,
|
||||||
|
setChecked,
|
||||||
|
...props
|
||||||
|
}: CheckboxProps) {
|
||||||
|
const finalSize = size || 20;
|
||||||
|
|
||||||
|
const [internalChecked, setInternalChecked] = React.useState(
|
||||||
|
defaultChecked || false
|
||||||
|
);
|
||||||
|
|
||||||
|
const checkMarkRef = React.useRef<HTMLInputElement>();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
{...wrapperProps}
|
||||||
|
onClick={(e) => {
|
||||||
|
checkMarkRef.current?.click();
|
||||||
|
wrapperProps?.onClick?.(e);
|
||||||
|
}}
|
||||||
|
className={twMerge(
|
||||||
|
"flex items-center gap-2",
|
||||||
|
wrapperClassName,
|
||||||
|
wrapperProps?.className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
{...props}
|
||||||
|
width={finalSize}
|
||||||
|
height={finalSize}
|
||||||
|
className={twMerge("hidden")}
|
||||||
|
name={name}
|
||||||
|
onChange={(e) => {
|
||||||
|
setInternalChecked(e.target.checked);
|
||||||
|
setChecked?.(e.target.checked);
|
||||||
|
}}
|
||||||
|
ref={checkMarkRef as any}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className={twMerge(
|
||||||
|
"flex items-center justify-center p-[3px] rounded",
|
||||||
|
internalChecked
|
||||||
|
? "bg-emerald-700 twui-checkbox-checked"
|
||||||
|
: "outline-slate-600 dark:outline-white/50 outline-2 outline -outline-offset-2 twui-checkbox-unchecked",
|
||||||
|
"twui-checkbox"
|
||||||
|
)}
|
||||||
|
style={{
|
||||||
|
width: finalSize + "px",
|
||||||
|
height: finalSize + "px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{internalChecked && <CheckMarkSVG />}
|
||||||
|
</div>
|
||||||
|
{label && <label>{label}</label>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -1,3 +1,4 @@
|
|||||||
|
import _ from "lodash";
|
||||||
import { DetailedHTMLProps, FormHTMLAttributes } from "react";
|
import { DetailedHTMLProps, FormHTMLAttributes } from "react";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
@ -5,17 +6,28 @@ import { twMerge } from "tailwind-merge";
|
|||||||
* # Form Element
|
* # Form Element
|
||||||
* @className twui-form
|
* @className twui-form
|
||||||
*/
|
*/
|
||||||
export default function Form({
|
export default function Form<T extends object = { [key: string]: any }>({
|
||||||
...props
|
...props
|
||||||
}: DetailedHTMLProps<FormHTMLAttributes<HTMLFormElement>, HTMLFormElement>) {
|
}: DetailedHTMLProps<FormHTMLAttributes<HTMLFormElement>, HTMLFormElement> & {
|
||||||
|
submitHandler?: (e: React.FormEvent<HTMLFormElement>, data: T) => void;
|
||||||
|
}) {
|
||||||
|
const finalProps = _.omit(props, "submitHandler");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form
|
<form
|
||||||
{...props}
|
{...finalProps}
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
"flex flex-col items-stretch gap-2 w-full bg-transparent",
|
"flex flex-col items-stretch gap-2 w-full bg-transparent",
|
||||||
"twui-form",
|
"twui-form",
|
||||||
props.className
|
props.className
|
||||||
)}
|
)}
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const formEl = e.target as HTMLFormElement;
|
||||||
|
const formData = new FormData(formEl);
|
||||||
|
const data = Object.fromEntries(formData.entries()) as T;
|
||||||
|
props.submitHandler?.(e, data);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{props.children}
|
{props.children}
|
||||||
</form>
|
</form>
|
||||||
|
@ -1,25 +1,24 @@
|
|||||||
import Button from "@/components/lib/layout/Button";
|
import Button from "../layout/Button";
|
||||||
import Stack from "@/components/lib/layout/Stack";
|
import Stack from "../layout/Stack";
|
||||||
import { ImagePlus, X } from "lucide-react";
|
import { ImagePlus, X } from "lucide-react";
|
||||||
import React, { DetailedHTMLProps } from "react";
|
import React, { DetailedHTMLProps } from "react";
|
||||||
import Card from "@/components/lib/elements/Card";
|
import Card from "../elements/Card";
|
||||||
import Span from "@/components/lib/layout/Span";
|
import Span from "../layout/Span";
|
||||||
import Center from "@/components/lib/layout/Center";
|
import Center from "../layout/Center";
|
||||||
import imageInputToBase64, {
|
import imageInputToBase64, {
|
||||||
ImageInputToBase64FunctionReturn,
|
ImageInputToBase64FunctionReturn,
|
||||||
} from "../utils/form/imageInputToBase64";
|
} from "../utils/form/imageInputToBase64";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
type ImageUploadProps = {
|
type ImageUploadProps = DetailedHTMLProps<
|
||||||
|
React.HTMLAttributes<HTMLDivElement>,
|
||||||
|
HTMLDivElement
|
||||||
|
> & {
|
||||||
onChange?: (imgData: ImageInputToBase64FunctionReturn | undefined) => any;
|
onChange?: (imgData: ImageInputToBase64FunctionReturn | undefined) => any;
|
||||||
fileInputProps?: DetailedHTMLProps<
|
fileInputProps?: DetailedHTMLProps<
|
||||||
React.InputHTMLAttributes<HTMLInputElement>,
|
React.InputHTMLAttributes<HTMLInputElement>,
|
||||||
HTMLInputElement
|
HTMLInputElement
|
||||||
>;
|
>;
|
||||||
wrapperProps?: DetailedHTMLProps<
|
|
||||||
React.HTMLAttributes<HTMLDivElement>,
|
|
||||||
HTMLDivElement
|
|
||||||
>;
|
|
||||||
placeHolderWrapper?: DetailedHTMLProps<
|
placeHolderWrapper?: DetailedHTMLProps<
|
||||||
React.HTMLAttributes<HTMLDivElement>,
|
React.HTMLAttributes<HTMLDivElement>,
|
||||||
HTMLDivElement
|
HTMLDivElement
|
||||||
@ -32,23 +31,27 @@ type ImageUploadProps = {
|
|||||||
React.ImgHTMLAttributes<HTMLImageElement>,
|
React.ImgHTMLAttributes<HTMLImageElement>,
|
||||||
HTMLImageElement
|
HTMLImageElement
|
||||||
>;
|
>;
|
||||||
|
label?: string;
|
||||||
|
disablePreview?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function ImageUpload({
|
export default function ImageUpload({
|
||||||
onChange,
|
onChange,
|
||||||
fileInputProps,
|
fileInputProps,
|
||||||
wrapperProps,
|
|
||||||
placeHolderWrapper,
|
placeHolderWrapper,
|
||||||
previewImageWrapperProps,
|
previewImageWrapperProps,
|
||||||
previewImageProps,
|
previewImageProps,
|
||||||
|
label,
|
||||||
|
disablePreview,
|
||||||
|
...props
|
||||||
}: ImageUploadProps) {
|
}: ImageUploadProps) {
|
||||||
const [src, setSrc] = React.useState<string | undefined>(undefined);
|
const [src, setSrc] = React.useState<string | undefined>(undefined);
|
||||||
const inputRef = React.useRef<HTMLInputElement>();
|
const inputRef = React.useRef<HTMLInputElement>();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack
|
<Stack
|
||||||
className={twMerge("w-full", wrapperProps?.className)}
|
{...props}
|
||||||
{...wrapperProps}
|
className={twMerge("w-full h-[300px]", props?.className)}
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
@ -65,12 +68,21 @@ export default function ImageUpload({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{src ? (
|
{src ? (
|
||||||
<Card className="w-full relative" {...previewImageWrapperProps}>
|
<Card
|
||||||
<img
|
className="w-full relative h-full items-center justify-center"
|
||||||
src={src}
|
{...previewImageWrapperProps}
|
||||||
className="w-full h-[300px] object-contain"
|
>
|
||||||
{...previewImageProps}
|
{disablePreview ? (
|
||||||
/>
|
<Span className="opacity-50" size="small">
|
||||||
|
Image Uploaded!
|
||||||
|
</Span>
|
||||||
|
) : (
|
||||||
|
<img
|
||||||
|
src={src}
|
||||||
|
className="w-full object-contain"
|
||||||
|
{...previewImageProps}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
className="absolute p-2 top-2 right-2 z-20"
|
className="absolute p-2 top-2 right-2 z-20"
|
||||||
@ -79,13 +91,13 @@ export default function ImageUpload({
|
|||||||
onChange?.(undefined);
|
onChange?.(undefined);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<X className="text-slate-950" />
|
<X className="text-slate-950 dark:text-white" />
|
||||||
</Button>
|
</Button>
|
||||||
</Card>
|
</Card>
|
||||||
) : (
|
) : (
|
||||||
<Card
|
<Card
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
"w-full h-[300px] cursor-pointer hover:bg-slate-100 dark:hover:bg-white/20",
|
"w-full h-full cursor-pointer hover:bg-slate-100 dark:hover:bg-white/20",
|
||||||
placeHolderWrapper?.className
|
placeHolderWrapper?.className
|
||||||
)}
|
)}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
@ -98,7 +110,7 @@ export default function ImageUpload({
|
|||||||
<Stack className="items-center gap-2">
|
<Stack className="items-center gap-2">
|
||||||
<ImagePlus className="text-slate-400" />
|
<ImagePlus className="text-slate-400" />
|
||||||
<Span size="smaller" variant="faded">
|
<Span size="smaller" variant="faded">
|
||||||
Click to Upload Image
|
{label || "Click to Upload Image"}
|
||||||
</Span>
|
</Span>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Center>
|
</Center>
|
||||||
|
@ -6,8 +6,79 @@ import React, {
|
|||||||
TextareaHTMLAttributes,
|
TextareaHTMLAttributes,
|
||||||
} from "react";
|
} from "react";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
|
import Span from "../layout/Span";
|
||||||
|
|
||||||
export type InputProps = DetailedHTMLProps<
|
let timeout: any;
|
||||||
|
|
||||||
|
const autocompleteOptions = [
|
||||||
|
// Personal Information
|
||||||
|
"name",
|
||||||
|
"honorific-prefix",
|
||||||
|
"given-name",
|
||||||
|
"additional-name",
|
||||||
|
"family-name",
|
||||||
|
"honorific-suffix",
|
||||||
|
"nickname",
|
||||||
|
|
||||||
|
// Contact Information
|
||||||
|
"email",
|
||||||
|
"username",
|
||||||
|
"new-password",
|
||||||
|
"current-password",
|
||||||
|
"one-time-code",
|
||||||
|
"organization-title",
|
||||||
|
"organization",
|
||||||
|
|
||||||
|
// Address Fields
|
||||||
|
"street-address",
|
||||||
|
"address-line1",
|
||||||
|
"address-line2",
|
||||||
|
"address-line3",
|
||||||
|
"address-level4",
|
||||||
|
"address-level3",
|
||||||
|
"address-level2",
|
||||||
|
"address-level1",
|
||||||
|
"country",
|
||||||
|
"country-name",
|
||||||
|
"postal-code",
|
||||||
|
|
||||||
|
// Phone Numbers
|
||||||
|
"tel",
|
||||||
|
"tel-country-code",
|
||||||
|
"tel-national",
|
||||||
|
"tel-area-code",
|
||||||
|
"tel-local",
|
||||||
|
"tel-extension",
|
||||||
|
|
||||||
|
// Dates
|
||||||
|
"bday",
|
||||||
|
"bday-day",
|
||||||
|
"bday-month",
|
||||||
|
"bday-year",
|
||||||
|
|
||||||
|
// Payment Information
|
||||||
|
"cc-name",
|
||||||
|
"cc-given-name",
|
||||||
|
"cc-additional-name",
|
||||||
|
"cc-family-name",
|
||||||
|
"cc-number",
|
||||||
|
"cc-exp",
|
||||||
|
"cc-exp-month",
|
||||||
|
"cc-exp-year",
|
||||||
|
"cc-csc",
|
||||||
|
"cc-type",
|
||||||
|
|
||||||
|
// Additional Options
|
||||||
|
"sex",
|
||||||
|
"url",
|
||||||
|
"photo",
|
||||||
|
|
||||||
|
// Special Values
|
||||||
|
"on", // Enables autofill (default)
|
||||||
|
"off", // Disables autofill
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export type InputProps<KeyType extends string> = DetailedHTMLProps<
|
||||||
InputHTMLAttributes<HTMLInputElement>,
|
InputHTMLAttributes<HTMLInputElement>,
|
||||||
HTMLInputElement
|
HTMLInputElement
|
||||||
> &
|
> &
|
||||||
@ -30,13 +101,21 @@ export type InputProps = DetailedHTMLProps<
|
|||||||
HTMLLabelElement
|
HTMLLabelElement
|
||||||
>;
|
>;
|
||||||
componentRef?: RefObject<any>;
|
componentRef?: RefObject<any>;
|
||||||
|
validationRegex?: RegExp;
|
||||||
|
debounce?: number;
|
||||||
|
invalidMessage?: string;
|
||||||
|
validationFunction?: (value: string) => Promise<boolean>;
|
||||||
|
autoComplete?: (typeof autocompleteOptions)[number];
|
||||||
|
name?: KeyType;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* # Input Element
|
* # Input Element
|
||||||
* @className twui-input
|
* @className twui-input
|
||||||
|
* @className twui-input-wrapper
|
||||||
|
* @className twui-input-invalid
|
||||||
*/
|
*/
|
||||||
export default function Input({
|
export default function Input<KeyType extends string>({
|
||||||
label,
|
label,
|
||||||
variant,
|
variant,
|
||||||
prefix,
|
prefix,
|
||||||
@ -46,9 +125,43 @@ export default function Input({
|
|||||||
wrapperProps,
|
wrapperProps,
|
||||||
showLabel,
|
showLabel,
|
||||||
istextarea,
|
istextarea,
|
||||||
|
debounce,
|
||||||
|
invalidMessage,
|
||||||
|
autoComplete,
|
||||||
|
validationFunction,
|
||||||
|
validationRegex,
|
||||||
...props
|
...props
|
||||||
}: InputProps) {
|
}: InputProps<KeyType>) {
|
||||||
const [focus, setFocus] = React.useState(false);
|
const [focus, setFocus] = React.useState(false);
|
||||||
|
const [value, setValue] = React.useState(
|
||||||
|
props.defaultValue ? String(props.defaultValue) : ""
|
||||||
|
);
|
||||||
|
|
||||||
|
delete props.defaultValue;
|
||||||
|
|
||||||
|
const [isValid, setIsValid] = React.useState(true);
|
||||||
|
|
||||||
|
const DEFAULT_DEBOUNCE = 500;
|
||||||
|
const finalDebounce = debounce || DEFAULT_DEBOUNCE;
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!value.match(/./)) return setIsValid(true);
|
||||||
|
window.clearTimeout(timeout);
|
||||||
|
|
||||||
|
if (validationRegex) {
|
||||||
|
timeout = setTimeout(() => {
|
||||||
|
setIsValid(validationRegex.test(value));
|
||||||
|
}, finalDebounce);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (validationFunction) {
|
||||||
|
timeout = setTimeout(() => {
|
||||||
|
validationFunction(value).then((res) => {
|
||||||
|
setIsValid(res);
|
||||||
|
});
|
||||||
|
}, finalDebounce);
|
||||||
|
}
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
const targetComponent = istextarea ? (
|
const targetComponent = istextarea ? (
|
||||||
<textarea
|
<textarea
|
||||||
@ -67,6 +180,10 @@ export default function Input({
|
|||||||
setFocus(false);
|
setFocus(false);
|
||||||
props?.onBlur?.(e);
|
props?.onBlur?.(e);
|
||||||
}}
|
}}
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => setValue(e.target.value)}
|
||||||
|
autoComplete={autoComplete}
|
||||||
|
rows={props.height ? Number(props.height) : 4}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<input
|
<input
|
||||||
@ -85,6 +202,11 @@ export default function Input({
|
|||||||
setFocus(false);
|
setFocus(false);
|
||||||
props?.onBlur?.(e);
|
props?.onBlur?.(e);
|
||||||
}}
|
}}
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => {
|
||||||
|
setValue(e.target.value);
|
||||||
|
props?.onChange?.(e);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -93,18 +215,27 @@ export default function Input({
|
|||||||
{...wrapperProps}
|
{...wrapperProps}
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
"relative flex items-center gap-2 border rounded-md px-3 py-2 outline outline-1",
|
"relative flex items-center gap-2 border rounded-md px-3 py-2 outline outline-1",
|
||||||
focus
|
focus && isValid
|
||||||
? "border-slate-700 dark:border-white/50"
|
? "border-slate-700 dark:border-white/50"
|
||||||
: "border-slate-300 dark:border-white/20",
|
: "border-slate-300 dark:border-white/20",
|
||||||
focus
|
focus && isValid
|
||||||
? "outline-slate-700 dark:outline-white/50"
|
? "outline-slate-700 dark:outline-white/50"
|
||||||
: "outline-transparent",
|
: "outline-transparent",
|
||||||
variant == "warning" &&
|
variant == "warning" &&
|
||||||
|
isValid &&
|
||||||
"border-yellow-500 dark:border-yellow-300 outline-yellow-500 dark:outline-yellow-300",
|
"border-yellow-500 dark:border-yellow-300 outline-yellow-500 dark:outline-yellow-300",
|
||||||
variant == "error" &&
|
variant == "error" &&
|
||||||
|
isValid &&
|
||||||
"border-red-500 dark:border-red-300 outline-red-500 dark:outline-red-300",
|
"border-red-500 dark:border-red-300 outline-red-500 dark:outline-red-300",
|
||||||
variant == "inactive" && "opacity-40 pointer-events-none",
|
variant == "inactive" &&
|
||||||
|
isValid &&
|
||||||
|
"opacity-40 pointer-events-none",
|
||||||
"bg-white dark:bg-black",
|
"bg-white dark:bg-black",
|
||||||
|
isValid
|
||||||
|
? ""
|
||||||
|
: "border-orange-500 outline-orange-500 twui-input-invalid",
|
||||||
|
props.readOnly && "opacity-50 pointer-events-none",
|
||||||
|
"twui-input-wrapper",
|
||||||
wrapperProps?.className
|
wrapperProps?.className
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@ -123,13 +254,22 @@ export default function Input({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{prefix && (
|
{prefix && (
|
||||||
<div className="opacity-60 pointer-events-none">{prefix}</div>
|
<div className="opacity-60 pointer-events-none whitespace-nowrap">
|
||||||
|
{prefix}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{targetComponent}
|
{targetComponent}
|
||||||
|
|
||||||
{suffix && (
|
{suffix && (
|
||||||
<div className="opacity-60 pointer-events-none">{suffix}</div>
|
<div className="opacity-60 pointer-events-none whitespace-nowrap">
|
||||||
|
{suffix}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!isValid && (
|
||||||
|
<Span className="opacity-30 pointer-events-none whitespace-nowrap">
|
||||||
|
{invalidMessage || "Invalid"}
|
||||||
|
</Span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -4,6 +4,9 @@ import Input, { InputProps } from "./Input";
|
|||||||
* # Textarea Component
|
* # Textarea Component
|
||||||
* @className twui-textarea
|
* @className twui-textarea
|
||||||
*/
|
*/
|
||||||
export default function Textarea({ componentRef, ...props }: InputProps) {
|
export default function Textarea<KeyType extends string>({
|
||||||
|
componentRef,
|
||||||
|
...props
|
||||||
|
}: InputProps<KeyType>) {
|
||||||
return <Input istextarea {...props} componentRef={componentRef} />;
|
return <Input istextarea {...props} componentRef={componentRef} />;
|
||||||
}
|
}
|
||||||
|
66
components/lib/hooks/useIntersectionObserver.tsx
Normal file
66
components/lib/hooks/useIntersectionObserver.tsx
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
type Param = {
|
||||||
|
elementRef?: React.MutableRefObject<Element | undefined>;
|
||||||
|
className?: string;
|
||||||
|
options?: IntersectionObserverInit;
|
||||||
|
removeIntersected?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function useIntersectionObserver({
|
||||||
|
elementRef,
|
||||||
|
className,
|
||||||
|
options,
|
||||||
|
removeIntersected,
|
||||||
|
}: Param) {
|
||||||
|
const [isIntersecting, setIsIntersecting] = React.useState(false);
|
||||||
|
const [refresh, setRefresh] = React.useState(0);
|
||||||
|
|
||||||
|
const observerCallback: IntersectionObserverCallback = React.useCallback(
|
||||||
|
(entries, observer) => {
|
||||||
|
const entry = entries[0];
|
||||||
|
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
setIsIntersecting(true);
|
||||||
|
|
||||||
|
if (removeIntersected) {
|
||||||
|
observer.unobserve(entry.target);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setIsIntersecting(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const element = elementRef?.current;
|
||||||
|
const elements = className
|
||||||
|
? document.querySelectorAll(`.${className}`)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if (!element && !className && refresh < 5) {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
setRefresh(refresh + 1);
|
||||||
|
}, 2000);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const observer = new IntersectionObserver(observerCallback, {
|
||||||
|
rootMargin: "0px 0px 0px 0px",
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (elements) {
|
||||||
|
elements.forEach((el) => {
|
||||||
|
observer.observe(el);
|
||||||
|
});
|
||||||
|
} else if (element) {
|
||||||
|
observer.observe(element);
|
||||||
|
}
|
||||||
|
}, [refresh]);
|
||||||
|
|
||||||
|
return { isIntersecting };
|
||||||
|
}
|
25
components/lib/hooks/useLocalUser.tsx
Normal file
25
components/lib/hooks/useLocalUser.tsx
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { LocalStorageDict } from "@/dict/local-storage-dict";
|
||||||
|
import { DATASQUIREL_LoggedInUser } from "@moduletrace/datasquirel/package-shared/types";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
export default function useLocalUser() {
|
||||||
|
const [user, setUser] = React.useState<
|
||||||
|
DATASQUIREL_LoggedInUser | null | undefined
|
||||||
|
>(undefined);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
try {
|
||||||
|
const localUserJSON = localStorage.getItem(
|
||||||
|
LocalStorageDict["User"]
|
||||||
|
);
|
||||||
|
if (localUserJSON) {
|
||||||
|
const localUser = JSON.parse(localUserJSON);
|
||||||
|
setUser(localUser);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
setUser(null);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return { user };
|
||||||
|
}
|
144
components/lib/hooks/useWebSocket.tsx
Normal file
144
components/lib/hooks/useWebSocket.tsx
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
export type UseWebsocketHookParams = {
|
||||||
|
debounce?: number;
|
||||||
|
url: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
let reconnectInterval: any;
|
||||||
|
let msgInterval: any;
|
||||||
|
let sendInterval: any;
|
||||||
|
|
||||||
|
let tries = 0;
|
||||||
|
|
||||||
|
export const WebSocketEventNames = ["wsDataEvent", "wsMessageEvent"] as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* # Use Websocket Hook
|
||||||
|
* @event wsDataEvent Listen for event named `wsDataEvent` on `window` to receive Data events
|
||||||
|
* @event wsMessageEvent Listen for event named `wsMessageEvent` on `window` to receive Message events
|
||||||
|
*
|
||||||
|
* @example window.addEventLiatener("wsDataEvent", (e)=>{
|
||||||
|
* console.log(e.detail.data) // type object
|
||||||
|
* })
|
||||||
|
* @example window.addEventLiatener("wsMessageEvent", (e)=>{
|
||||||
|
* console.log(e.detail.message) // type string
|
||||||
|
* })
|
||||||
|
*/
|
||||||
|
export default function useWebSocket<T>({
|
||||||
|
url,
|
||||||
|
debounce,
|
||||||
|
}: UseWebsocketHookParams) {
|
||||||
|
const DEBOUNCE = debounce || 200;
|
||||||
|
|
||||||
|
const [socket, setSocket] = React.useState<WebSocket | undefined>(
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
|
||||||
|
const messageQueueRef = React.useRef<string[]>([]);
|
||||||
|
const sendMessageQueueRef = React.useRef<string[]>([]);
|
||||||
|
|
||||||
|
// const [message, setMessage] = React.useState<string>("");
|
||||||
|
// const [data, setData] = React.useState<T | null>(null);
|
||||||
|
const [refresh, setRefresh] = React.useState(0);
|
||||||
|
|
||||||
|
const dispatchCustomEvent = React.useCallback(
|
||||||
|
(evtName: (typeof WebSocketEventNames)[number], value: string | T) => {
|
||||||
|
const event = new CustomEvent(evtName, {
|
||||||
|
detail: {
|
||||||
|
data: value,
|
||||||
|
message: value,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
window.dispatchEvent(event);
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const wsURL = url;
|
||||||
|
if (!wsURL) return;
|
||||||
|
|
||||||
|
const ws = new WebSocket(wsURL);
|
||||||
|
|
||||||
|
ws.onopen = (ev) => {
|
||||||
|
window.clearInterval(reconnectInterval);
|
||||||
|
setSocket(ws);
|
||||||
|
tries = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onmessage = (ev) => {
|
||||||
|
window.clearInterval(msgInterval);
|
||||||
|
messageQueueRef.current.push(ev.data);
|
||||||
|
msgInterval = setInterval(handleReceivedMessageQueue, DEBOUNCE);
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onclose = (ev) => {
|
||||||
|
console.log("Websocket closed ... Attempting to reconnect ...");
|
||||||
|
reconnectInterval = setInterval(() => {
|
||||||
|
if (tries >= 3) {
|
||||||
|
return window.clearInterval(reconnectInterval);
|
||||||
|
}
|
||||||
|
console.log("Attempting to reconnect ...");
|
||||||
|
setRefresh(refresh + 1);
|
||||||
|
tries++;
|
||||||
|
}, 1000);
|
||||||
|
};
|
||||||
|
|
||||||
|
return function () {
|
||||||
|
window.clearInterval(reconnectInterval);
|
||||||
|
};
|
||||||
|
}, [refresh]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Received Message Queue Handler
|
||||||
|
*/
|
||||||
|
const handleReceivedMessageQueue = React.useCallback(() => {
|
||||||
|
if (messageQueueRef.current.length > 0) {
|
||||||
|
const newMessage = messageQueueRef.current.shift();
|
||||||
|
if (!newMessage) return;
|
||||||
|
try {
|
||||||
|
const jsonData = JSON.parse(newMessage);
|
||||||
|
// setData(jsonData);
|
||||||
|
|
||||||
|
dispatchCustomEvent("wsMessageEvent", newMessage);
|
||||||
|
dispatchCustomEvent("wsDataEvent", jsonData);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Unable to parse string. Returning string.");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
window.clearInterval(msgInterval);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send Message Queue Handler
|
||||||
|
*/
|
||||||
|
const handleSendMessageQueue = React.useCallback(() => {
|
||||||
|
if (sendMessageQueueRef.current.length > 0) {
|
||||||
|
const newMessage = sendMessageQueueRef.current.shift();
|
||||||
|
if (!newMessage) return;
|
||||||
|
socket?.send(newMessage);
|
||||||
|
} else {
|
||||||
|
window.clearInterval(sendInterval);
|
||||||
|
}
|
||||||
|
}, [socket]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* # Send Data Function
|
||||||
|
*/
|
||||||
|
const sendData = React.useCallback(
|
||||||
|
(data: T) => {
|
||||||
|
try {
|
||||||
|
window.clearInterval(sendInterval);
|
||||||
|
sendMessageQueueRef.current.push(JSON.stringify(data));
|
||||||
|
sendInterval = setInterval(handleSendMessageQueue, DEBOUNCE);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.log("Error Sending socket message", error.message);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[socket]
|
||||||
|
);
|
||||||
|
|
||||||
|
return { socket, sendData };
|
||||||
|
}
|
@ -179,7 +179,7 @@ export default function Button({
|
|||||||
<div
|
<div
|
||||||
{...buttonContentProps}
|
{...buttonContentProps}
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
"flex flex-row items-center gap-2",
|
"flex flex-row items-center gap-2 whitespace-nowrap",
|
||||||
loading ? "opacity-0" : "",
|
loading ? "opacity-0" : "",
|
||||||
"twui-button-content-wrapper",
|
"twui-button-content-wrapper",
|
||||||
buttonContentProps?.className
|
buttonContentProps?.className
|
||||||
@ -190,7 +190,22 @@ export default function Button({
|
|||||||
{afterIcon && afterIcon}
|
{afterIcon && afterIcon}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{loading && <Loading className="absolute" />}
|
{loading && (
|
||||||
|
<Loading
|
||||||
|
className="absolute"
|
||||||
|
size={(() => {
|
||||||
|
switch (size) {
|
||||||
|
case "small":
|
||||||
|
return "small";
|
||||||
|
case "smaller":
|
||||||
|
return "smaller";
|
||||||
|
|
||||||
|
default:
|
||||||
|
return "normal";
|
||||||
|
}
|
||||||
|
})()}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -12,7 +12,7 @@ export default function HR({
|
|||||||
<hr
|
<hr
|
||||||
{...props}
|
{...props}
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
"border-slate-200 dark:border-white/20 w-full my-4",
|
"border-slate-200 dark:border-white/10 w-full my-4",
|
||||||
"twui-hr",
|
"twui-hr",
|
||||||
props.className
|
props.className
|
||||||
)}
|
)}
|
||||||
|
82
components/lib/layout/Img.tsx
Normal file
82
components/lib/layout/Img.tsx
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
import _ from "lodash";
|
||||||
|
import React, { DetailedHTMLProps, ImgHTMLAttributes } from "react";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
export type TWUIImageProps = DetailedHTMLProps<
|
||||||
|
ImgHTMLAttributes<HTMLImageElement>,
|
||||||
|
HTMLImageElement
|
||||||
|
> & {
|
||||||
|
size?: number;
|
||||||
|
circle?: boolean;
|
||||||
|
bgImg?: boolean;
|
||||||
|
backgroundImage?: boolean;
|
||||||
|
fallbackImageSrc?: string;
|
||||||
|
srcLight?: string;
|
||||||
|
srcDark?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* # Image Component
|
||||||
|
* @className twui-img
|
||||||
|
*/
|
||||||
|
export default function Img({ ...props }: TWUIImageProps) {
|
||||||
|
const width = props.size || props.width;
|
||||||
|
const height = props.size || props.height;
|
||||||
|
const sizeRatio = width && height ? Number(width) / Number(height) : 1;
|
||||||
|
|
||||||
|
const finalProps = _.omit(props, [
|
||||||
|
"size",
|
||||||
|
"circle",
|
||||||
|
"bgImg",
|
||||||
|
"backgroundImage",
|
||||||
|
"fallbackImageSrc",
|
||||||
|
"srcLight",
|
||||||
|
"srcDark",
|
||||||
|
]);
|
||||||
|
|
||||||
|
const interpolatedProps: typeof props = {
|
||||||
|
...finalProps,
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
className: twMerge(
|
||||||
|
"object-cover",
|
||||||
|
props.circle && "rounded-full",
|
||||||
|
props.bgImg || props.backgroundImage
|
||||||
|
? "absolute top-0 left-0 w-full h-full object-cover z-0"
|
||||||
|
: "",
|
||||||
|
"twui-img",
|
||||||
|
props.className
|
||||||
|
),
|
||||||
|
onError: (e) => {
|
||||||
|
if (props.fallbackImageSrc) {
|
||||||
|
e.currentTarget.src = props.fallbackImageSrc;
|
||||||
|
}
|
||||||
|
props.onError?.(e);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (props.srcDark && props.srcLight) {
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<img
|
||||||
|
{...interpolatedProps}
|
||||||
|
className={twMerge(
|
||||||
|
"hidden dark:block",
|
||||||
|
interpolatedProps.className
|
||||||
|
)}
|
||||||
|
src={props.srcDark}
|
||||||
|
/>
|
||||||
|
<img
|
||||||
|
{...interpolatedProps}
|
||||||
|
className={twMerge(
|
||||||
|
"block dark:hidden",
|
||||||
|
interpolatedProps.className
|
||||||
|
)}
|
||||||
|
src={props.srcLight}
|
||||||
|
/>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <img {...interpolatedProps} />;
|
||||||
|
}
|
@ -18,6 +18,7 @@ export default function LoadingRectangleBlock({
|
|||||||
{...props}
|
{...props}
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
"flex items-center w-full h-10 animate-pulse bg-slate-200 rounded",
|
"flex items-center w-full h-10 animate-pulse bg-slate-200 rounded",
|
||||||
|
"dark:bg-slate-800",
|
||||||
"twui-loading-rectangle-block",
|
"twui-loading-rectangle-block",
|
||||||
props.className
|
props.className
|
||||||
)}
|
)}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import _ from "lodash";
|
||||||
import { DetailedHTMLProps, HTMLAttributes } from "react";
|
import { DetailedHTMLProps, HTMLAttributes } from "react";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
@ -7,12 +8,16 @@ import { twMerge } from "tailwind-merge";
|
|||||||
*/
|
*/
|
||||||
export default function Stack({
|
export default function Stack({
|
||||||
...props
|
...props
|
||||||
}: DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>) {
|
}: DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement> & {
|
||||||
|
center?: boolean;
|
||||||
|
}) {
|
||||||
|
const finalProps = _.omit(props, "center");
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
{...props}
|
{...finalProps}
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
"flex flex-col items-start gap-4",
|
"flex flex-col items-start gap-4",
|
||||||
|
props.center && "items-center",
|
||||||
"twui-stack",
|
"twui-stack",
|
||||||
props.className
|
props.className
|
||||||
)}
|
)}
|
||||||
|
18
components/lib/svgs/CheckMarkSVG.tsx
Normal file
18
components/lib/svgs/CheckMarkSVG.tsx
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
export default function CheckMarkSVG({ color }: { color?: string }) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="100%"
|
||||||
|
height="100%"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
className="lucide lucide-check"
|
||||||
|
>
|
||||||
|
<path d="M20 6 9 17l-5-5" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
@ -27,26 +27,37 @@ export type FetchApiReturn = {
|
|||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* # Fetch API
|
||||||
|
*/
|
||||||
export default async function fetchApi(
|
export default async function fetchApi(
|
||||||
url: string,
|
url: string,
|
||||||
options?: FetchApiOptions,
|
options?: FetchApiOptions,
|
||||||
csrf?: boolean
|
csrf?: boolean,
|
||||||
|
/** Key to use to grab local Storage csrf value. */
|
||||||
|
localStorageCSRFKey?: string
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
let data;
|
let data;
|
||||||
|
|
||||||
|
const csrfValue = localStorage.getItem(localStorageCSRFKey || "csrf");
|
||||||
|
|
||||||
|
let finalHeaders = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
} as FetchHeader;
|
||||||
|
|
||||||
|
if (csrf && csrfValue) {
|
||||||
|
finalHeaders[`'${csrfValue.replace(/\"/g, "")}'`] = "true";
|
||||||
|
}
|
||||||
|
|
||||||
if (typeof options === "string") {
|
if (typeof options === "string") {
|
||||||
try {
|
try {
|
||||||
let fetchData;
|
let fetchData;
|
||||||
const csrfValue = localStorage.getItem("csrf");
|
|
||||||
|
|
||||||
switch (options) {
|
switch (options) {
|
||||||
case "post":
|
case "post":
|
||||||
fetchData = await fetch(url, {
|
fetchData = await fetch(url, {
|
||||||
method: options,
|
method: options,
|
||||||
headers: {
|
headers: finalHeaders,
|
||||||
"Content-Type": "application/json",
|
|
||||||
"x-csrf-auth": csrf ? csrfValue : "",
|
|
||||||
} as FetchHeader,
|
|
||||||
} as RequestInit);
|
} as RequestInit);
|
||||||
data = fetchData.json();
|
data = fetchData.json();
|
||||||
break;
|
break;
|
||||||
@ -64,26 +75,23 @@ export default async function fetchApi(
|
|||||||
try {
|
try {
|
||||||
let fetchData;
|
let fetchData;
|
||||||
|
|
||||||
const csrfValue = localStorage.getItem("csrf");
|
|
||||||
|
|
||||||
if (options.body && typeof options.body === "object") {
|
if (options.body && typeof options.body === "object") {
|
||||||
let oldOptionsBody = _.cloneDeep(options.body);
|
let oldOptionsBody = _.cloneDeep(options.body);
|
||||||
options.body = JSON.stringify(oldOptionsBody);
|
options.body = JSON.stringify(oldOptionsBody);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.headers) {
|
if (options.headers) {
|
||||||
options.headers["x-csrf-auth"] = csrf ? csrfValue : "";
|
options.headers = _.merge(options.headers, finalHeaders);
|
||||||
|
|
||||||
const finalOptions: any = { ...options };
|
const finalOptions: any = { ...options };
|
||||||
fetchData = await fetch(url, finalOptions);
|
fetchData = await fetch(url, finalOptions);
|
||||||
} else {
|
} else {
|
||||||
fetchData = await fetch(url, {
|
const finalOptions = {
|
||||||
...options,
|
...options,
|
||||||
headers: {
|
headers: finalHeaders,
|
||||||
"Content-Type": "application/json",
|
} as RequestInit;
|
||||||
"x-csrf-auth": csrf ? csrfValue : "",
|
|
||||||
} as FetchHeader,
|
fetchData = await fetch(url, finalOptions);
|
||||||
} as RequestInit);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data = fetchData.json();
|
data = fetchData.json();
|
||||||
@ -94,7 +102,7 @@ export default async function fetchApi(
|
|||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
let fetchData = await fetch(url);
|
let fetchData = await fetch(url);
|
||||||
data = fetchData.json();
|
data = await fetchData.json();
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.log("FetchAPI error #3:", error.message);
|
console.log("FetchAPI error #3:", error.message);
|
||||||
data = null;
|
data = null;
|
||||||
|
12
pages/api/coderank-code-server-auth.ts
Normal file
12
pages/api/coderank-code-server-auth.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
|
||||||
|
type Data = {
|
||||||
|
msg: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function handler(
|
||||||
|
req: NextApiRequest,
|
||||||
|
res: NextApiResponse<Data>
|
||||||
|
) {
|
||||||
|
res.status(200).json({ msg: "Authentication Successful!" });
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user