diff --git a/components/lib/Readme.md b/components/lib/Readme.md index f4e40ab..4f0d2ae 100644 --- a/components/lib/Readme.md +++ b/components/lib/Readme.md @@ -8,10 +8,20 @@ You need a couple of packages and settings to integrate this package ### Packages -- React -- React Dom -- Tailwind CSS **version 4** +- React +- React Dom +- Tailwind CSS **version 4** ### CSS Base This package contains a `base.css` file which has all the base css rules required to run. This css file must be imported in your base project, and it can be update in a separate `.css` file. + +### Install packages + +```sh +bun add lucide-react tailwind-merge html-to-react gray-matter mdx typescript lodash react-code-blocks react-responsive-modal next-mdx-remote remark-gfm rehype-prism-plus openai +``` + +```sh +bun add -D @types/ace @types/react @types/react-dom tailwindcss @types/mdx @next/mdx +``` diff --git a/components/lib/form/Input/NumberInputButtons.tsx b/components/lib/form/Input/NumberInputButtons.tsx index 3212408..e9bdf03 100644 --- a/components/lib/form/Input/NumberInputButtons.tsx +++ b/components/lib/form/Input/NumberInputButtons.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useEffect, useRef, useState } from "react"; import Row from "../../layout/Row"; import { Info, Minus, Plus } from "lucide-react"; import twuiNumberfy from "../../utils/numberfy"; @@ -7,10 +7,10 @@ import { InputProps } from "."; let pressInterval: any; let pressTimeout: any; -type Props = Pick, "min" | "max" | "step"> & { +type Props = Pick, "min" | "max" | "step" | "decimal"> & { + value: string; setValue: React.Dispatch>; - getNormalizedValue: (v: string) => void; - buttonDownRef: React.MutableRefObject; + buttonDownRef: React.RefObject; inputRef: React.RefObject; }; @@ -18,21 +18,50 @@ type Props = Pick, "min" | "max" | "step"> & { * # Input Number Text Buttons */ export default function NumberInputButtons({ - getNormalizedValue, + value, setValue, min, max, step, buttonDownRef, inputRef, + decimal, }: Props) { const PRESS_TRIGGER_TIMEOUT = 200; const DEFAULT_STEP = 1; + const [buttonDown, setButtonDown] = useState(false); + + // function getNormalizedValue(value: string) { + // if (numberText) { + // if (props.max && twuiNumberfy(value) > twuiNumberfy(props.max)) + // return getFinalValue(props.max); + + // if (props.min && twuiNumberfy(value) < twuiNumberfy(props.min)) + // return getFinalValue(props.min); + + // return getFinalValue(value); + // } else { + // return value; + // } + // } + + useEffect(() => { + buttonDownRef.current = buttonDown; + if (buttonDown) { + setValue(inputRef.current?.value || ""); + } else { + setTimeout(() => { + setValue(inputRef.current?.value || ""); + }, 50); + } + }, [buttonDown]); + function incrementDownPress() { window.clearTimeout(pressTimeout); + setButtonDown(true); + pressTimeout = setTimeout(() => { - buttonDownRef.current = true; pressInterval = setInterval(() => { increment(); }, 50); @@ -40,14 +69,15 @@ export default function NumberInputButtons({ } function incrementDownCancel() { - buttonDownRef.current = false; + setButtonDown(false); window.clearTimeout(pressTimeout); window.clearInterval(pressInterval); } function decrementDownPress() { + setButtonDown(true); + pressTimeout = setTimeout(() => { - buttonDownRef.current = true; pressInterval = setInterval(() => { decrement(); }, 50); @@ -55,41 +85,51 @@ export default function NumberInputButtons({ } function decrementDownCancel() { - buttonDownRef.current = false; + setButtonDown(false); window.clearTimeout(pressTimeout); window.clearInterval(pressInterval); } function increment() { - const existingValue = inputRef.current?.value; - const existingNumberValue = twuiNumberfy(existingValue); + if (!inputRef.current) return; - if (max && existingNumberValue >= twuiNumberfy(max)) { - return setValue(String(max)); - } else if (min && existingNumberValue < twuiNumberfy(min)) { - return setValue(String(min)); + const existingValue = inputRef.current.value; + const existingNumberValue = twuiNumberfy(existingValue, decimal); + + let new_value = ""; + + if (max && existingNumberValue >= twuiNumberfy(max, decimal)) { + new_value = twuiNumberfy(max, decimal).toLocaleString(); + } else if (min && existingNumberValue < twuiNumberfy(min, decimal)) { + new_value = twuiNumberfy(min, decimal).toLocaleString(); } else { - setValue( - String( - existingNumberValue + twuiNumberfy(step || DEFAULT_STEP), - ), - ); + new_value = ( + existingNumberValue + + twuiNumberfy(step || DEFAULT_STEP, decimal) + ).toLocaleString(); } + + inputRef.current.value = new_value; } function decrement() { - const existingValue = inputRef.current?.value; - const existingNumberValue = twuiNumberfy(existingValue); + if (!inputRef.current) return; - if (min && existingNumberValue <= twuiNumberfy(min)) { - setValue(String(min)); + const existingValue = inputRef.current?.value; + const existingNumberValue = twuiNumberfy(existingValue, decimal); + + let new_value = ""; + + if (min && existingNumberValue <= twuiNumberfy(min, decimal)) { + new_value = twuiNumberfy(min, decimal).toLocaleString(); } else { - setValue( - String( - existingNumberValue - twuiNumberfy(step || DEFAULT_STEP), - ), - ); + new_value = ( + existingNumberValue - + twuiNumberfy(step || DEFAULT_STEP, decimal) + ).toLocaleString(); } + + inputRef.current.value = new_value; } return ( diff --git a/components/lib/form/Input/index.tsx b/components/lib/form/Input/index.tsx index da20b05..8afb635 100644 --- a/components/lib/form/Input/index.tsx +++ b/components/lib/form/Input/index.tsx @@ -6,27 +6,21 @@ import React, { ReactNode, RefObject, TextareaHTMLAttributes, + useRef, } from "react"; import { twMerge } from "tailwind-merge"; import Span from "../../layout/Span"; -import Button from "../../layout/Button"; import { Eye, EyeOff, Info, InfoIcon, X } from "lucide-react"; import { AutocompleteOptions } from "../../types"; import twuiNumberfy from "../../utils/numberfy"; import Dropdown from "../../elements/Dropdown"; -import Card from "../../elements/Card"; import Stack from "../../layout/Stack"; import NumberInputButtons from "./NumberInputButtons"; import twuiSlugToNormalText from "../../utils/slug-to-normal-text"; -import twuiUseReady from "../../hooks/useReady"; import Row from "../../layout/Row"; import Paper from "../../elements/Paper"; import { TWUISelectValidityObject } from "../Select"; -let timeout: any; -let validationFnTimeout: any; -let externalValueChangeTimeout: any; - export type InputProps = Omit< DetailedHTMLProps, HTMLInputElement>, "prefix" | "suffix" @@ -80,6 +74,7 @@ export type InputProps = Omit< React.HTMLAttributes, HTMLDivElement >; + // refreshDefaultValue?: number; }; let refreshes = 0; @@ -121,9 +116,18 @@ export default function Input( validity: existingValidity, clearInputProps, rawNumber, + // refreshDefaultValue, ...props } = inputProps; + const componentRefreshesRef = useRef(0); + let timeoutRef = useRef(null); + let validationFnTimeoutRef = useRef(null); + let externalValueChangeTimeoutRef = useRef(null); + + refreshes++; + componentRefreshesRef.current++; + function getFinalValue(v: any) { if (rawNumber) return twuiNumberfy(v); if (numberText) { @@ -167,21 +171,8 @@ export default function Input( props.placeholder || (props.name ? twuiSlugToNormalText(props.name) : undefined); - function getNormalizedValue(value: string) { - if (numberText) { - if (props.max && twuiNumberfy(value) > twuiNumberfy(props.max)) - return getFinalValue(props.max); - - if (props.min && twuiNumberfy(value) < twuiNumberfy(props.min)) - return getFinalValue(props.min); - - return getFinalValue(value); - } else { - return value; - } - } - React.useEffect(() => { + // if (!existingReady) return; if (!existingValidity) return; setValidity(existingValidity); }, [existingValidity]); @@ -190,8 +181,8 @@ export default function Input( if (buttonDownRef.current) return; if (changeHandler) { - window.clearTimeout(externalValueChangeTimeout); - externalValueChangeTimeout = setTimeout(() => { + window.clearTimeout(externalValueChangeTimeoutRef.current); + externalValueChangeTimeoutRef.current = setTimeout(() => { changeHandler(val); }, finalDebounce); } @@ -208,10 +199,10 @@ export default function Input( return; } - window.clearTimeout(timeout); + window.clearTimeout(timeoutRef.current); if (validationRegex) { - timeout = setTimeout(() => { + timeoutRef.current = setTimeout(() => { setValidity({ isValid: validationRegex.test(val), msg: "Value mismatch", @@ -220,9 +211,9 @@ export default function Input( } if (validationFunction) { - window.clearTimeout(validationFnTimeout); + window.clearTimeout(validationFnTimeoutRef.current); - validationFnTimeout = setTimeout(() => { + validationFnTimeoutRef.current = setTimeout(() => { if (validationRegex && !validationRegex.test(val)) { return; } @@ -235,11 +226,20 @@ export default function Input( }; React.useEffect(() => { + // if (!existingReady) return; if (typeof props.value !== "string" || !props.value.match(/./)) return; setValue(String(props.value)); }, [props.value]); + // React.useEffect(() => { + // if (!refreshDefaultValue) return; + // console.log("Name:", props.title || props.name); + // console.log("props.defaultValue", props.defaultValue); + // // setValue(String(props.defaultValue || "")); + // }, [refreshDefaultValue]); + React.useEffect(() => { + // if (!existingReady) return; if (istextarea && textAreaRef.current) { } else if (inputRef?.current) { inputRef.current.value = getFinalValue(value); @@ -452,11 +452,12 @@ export default function Input( ) : null} diff --git a/components/lib/hooks/useIntersectionObserver.tsx b/components/lib/hooks/useIntersectionObserver.tsx index 5a92478..3ef16d1 100644 --- a/components/lib/hooks/useIntersectionObserver.tsx +++ b/components/lib/hooks/useIntersectionObserver.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useRef } from "react"; type Param = { elementRef?: React.RefObject; @@ -9,8 +9,6 @@ type Param = { delay?: number; }; -let timeout: any; - export default function useIntersectionObserver({ elementRef, className, @@ -19,6 +17,8 @@ export default function useIntersectionObserver({ delay, elId, }: Param) { + let timeoutRef = useRef(null); + const [isIntersecting, setIsIntersecting] = React.useState(false); const [refresh, setRefresh] = React.useState(0); @@ -27,10 +27,10 @@ export default function useIntersectionObserver({ const observerCallback: IntersectionObserverCallback = React.useCallback( (entries, observer) => { const entry = entries[0]; - window.clearTimeout(timeout); + window.clearTimeout(timeoutRef.current); if (entry.isIntersecting) { - timeout = setTimeout(() => { + timeoutRef.current = setTimeout(() => { setIsIntersecting(true); if (removeIntersected) { @@ -41,7 +41,7 @@ export default function useIntersectionObserver({ setIsIntersecting(false); } }, - [] + [], ); React.useEffect(() => { diff --git a/components/lib/hooks/useWebSocket.tsx b/components/lib/hooks/useWebSocket.tsx index edea9e3..965ef39 100644 --- a/components/lib/hooks/useWebSocket.tsx +++ b/components/lib/hooks/useWebSocket.tsx @@ -6,6 +6,7 @@ export type UseWebsocketHookParams = { disableReconnect?: boolean; /** Interval to ping the websocket. So that the connection doesn't go down. Default 30000ms (30 seconds) */ keepAliveDuration?: number; + /** Interval in ms to force-refresh the connection */ refreshConnection?: number; }; @@ -16,10 +17,10 @@ export const WebSocketEventNames = ["wsDataEvent", "wsMessageEvent"] as const; * @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)=>{ + * @example window.addEventListener("wsDataEvent", (e)=>{ * console.log(e.detail.data) // type object * }) - * @example window.addEventLiatener("wsMessageEvent", (e)=>{ + * @example window.addEventListener("wsMessageEvent", (e)=>{ * console.log(e.detail.message) // type string * }) */ @@ -34,22 +35,29 @@ export default function useWebSocket< }: UseWebsocketHookParams) { const DEBOUNCE = debounce || 500; const KEEP_ALIVE_DURATION = keepAliveDuration || 1000 * 30; - const KEEP_ALIVE_TIMEOUT = 1000 * 60 * 3; - const KEEP_ALIVE_MESSAGE = "twui::ping"; - let uptime = 0; - let tries = useRef(0); + const tries = useRef(0); - // const queue: string[] = []; + // Refs to avoid stale closures in callbacks + const urlRef = useRef(url); + const disableReconnectRef = useRef(disableReconnect); + const keepAliveDurationRef = useRef(KEEP_ALIVE_DURATION); + + React.useEffect(() => { + urlRef.current = url; + disableReconnectRef.current = disableReconnect; + keepAliveDurationRef.current = KEEP_ALIVE_DURATION; + }); const msgInterval = useRef(null); const sendInterval = useRef(null); const keepAliveInterval = useRef(null); + const refreshInterval = useRef(null); + const reconnectTimeout = useRef(null); - const [socket, setSocket] = React.useState( - undefined - ); + const [socket, setSocket] = React.useState(undefined); + const socketRef = useRef(undefined); const messageQueueRef = React.useRef([]); const sendMessageQueueRef = React.useRef([]); @@ -74,16 +82,17 @@ export default function useWebSocket< * # Connect to Websocket */ const connect = React.useCallback(() => { + const currentUrl = urlRef.current; const domain = window.location.origin; - const wsURL = url.startsWith(`ws`) - ? url - : domain.replace(/^http/, "ws") + ("/" + url).replace(/\/\//g, "/"); + const wsURL = currentUrl.startsWith("ws") + ? currentUrl + : domain.replace(/^http/, "ws") + ("/" + currentUrl).replace(/\/\//g, "/"); if (!wsURL) return; - let ws = new WebSocket(wsURL); + const ws = new WebSocket(wsURL); - ws.onerror = (ev) => { + ws.onerror = () => { console.log(`Websocket ERROR:`); }; @@ -91,15 +100,17 @@ export default function useWebSocket< messageQueueRef.current.push(ev.data); }; - ws.onopen = (ev) => { + ws.onopen = () => { window.clearInterval(keepAliveInterval.current); keepAliveInterval.current = window.setInterval(() => { if (ws.readyState === WebSocket.OPEN) { ws.send(KEEP_ALIVE_MESSAGE); } - }, KEEP_ALIVE_DURATION); + }, keepAliveDurationRef.current); + tries.current = 0; + socketRef.current = ws; setSocket(ws); console.log(`Websocket connected to ${wsURL}`); }; @@ -111,23 +122,21 @@ export default function useWebSocket< wasClean: ev.wasClean, }); - if (disableReconnect) return; - - console.log("Attempting to reconnect ..."); - console.log("URL:", url); window.clearInterval(keepAliveInterval.current); + socketRef.current = undefined; + setSocket(undefined); - console.log("tries", tries); + if (disableReconnectRef.current) return; if (tries.current >= 3) { + console.log("Max reconnect attempts reached."); return; } - console.log("Attempting to reconnect ..."); - tries.current += 1; - - connect(); + const backoff = Math.min(1000 * 2 ** tries.current, 30000); + console.log(`Attempting to reconnect in ${backoff}ms... (attempt ${tries.current})`); + reconnectTimeout.current = window.setTimeout(connect, backoff); }; }, []); @@ -135,18 +144,40 @@ export default function useWebSocket< * # Initial Connection */ React.useEffect(() => { - if (socket) return; - connect(); + + return () => { + window.clearTimeout(reconnectTimeout.current); + window.clearInterval(keepAliveInterval.current); + window.clearInterval(refreshInterval.current); + socketRef.current?.close(); + }; }, []); + /** + * # Refresh Connection Interval + */ + React.useEffect(() => { + if (!refreshConnection) return; + + refreshInterval.current = window.setInterval(() => { + console.log("Refreshing WebSocket connection..."); + window.clearTimeout(reconnectTimeout.current); + socketRef.current?.close(); + tries.current = 0; + connect(); + }, refreshConnection); + + return () => window.clearInterval(refreshInterval.current); + }, [refreshConnection]); + React.useEffect(() => { if (!socket) return; sendInterval.current = setInterval(handleSendMessageQueue, DEBOUNCE); msgInterval.current = setInterval(handleReceivedMessageQueue, DEBOUNCE); - return function () { + return () => { window.clearInterval(sendInterval.current); window.clearInterval(msgInterval.current); }; @@ -173,7 +204,8 @@ export default function useWebSocket< * Send Message Queue Handler */ const handleSendMessageQueue = React.useCallback(() => { - if (!socket || socket.readyState !== WebSocket.OPEN) { + const ws = socketRef.current; + if (!ws || ws.readyState !== WebSocket.OPEN) { window.clearInterval(sendInterval.current); return; } @@ -181,29 +213,26 @@ export default function useWebSocket< const newMessage = sendMessageQueueRef.current.shift(); if (!newMessage) return; - socket.send(newMessage); - }, [socket]); + ws.send(newMessage); + }, []); /** * # Send Data Function */ - const sendData = React.useCallback( - (data: T) => { - try { - const queueItemJSON = JSON.stringify(data); + const sendData = React.useCallback((data: T) => { + try { + const queueItemJSON = JSON.stringify(data); - const existingQueue = sendMessageQueueRef.current.find( - (q) => q == queueItemJSON - ); - if (!existingQueue) { - sendMessageQueueRef.current.push(queueItemJSON); - } - } catch (error: any) { - console.log("Error Sending socket message", error.message); + const existingQueue = sendMessageQueueRef.current.find( + (q) => q === queueItemJSON + ); + if (!existingQueue) { + sendMessageQueueRef.current.push(queueItemJSON); } - }, - [socket] - ); + } catch (error: any) { + console.log("Error Sending socket message", error.message); + } + }, []); return { socket, sendData }; } diff --git a/components/lib/utils/fetch/fetchApi.ts b/components/lib/utils/fetch/fetchApi.ts index 2f3272e..33c7ea0 100644 --- a/components/lib/utils/fetch/fetchApi.ts +++ b/components/lib/utils/fetch/fetchApi.ts @@ -93,6 +93,7 @@ export default async function fetchApi< options.headers = _.merge(options.headers, finalHeaders); const finalOptions: any = { ...options }; + fetchData = await fetch(finalURL, finalOptions); } else { const finalOptions = {