This commit is contained in:
Benjamin Toby 2025-02-04 13:21:44 +01:00
parent f0850146be
commit 442ad5aa32
17 changed files with 207 additions and 79 deletions

BIN
bun.lockb

Binary file not shown.

0
components/lib/editors/AceEditor.tsx Executable file → Normal file
View File

View File

@ -17,7 +17,7 @@ export default function Border({ spacing, ...props }: TWUI_BORDER_PROPS) {
<div <div
{...props} {...props}
className={twMerge( className={twMerge(
"relative flex items-center gap-2 border rounded", "relative flex items-center gap-2 border border-solid rounded",
"border-slate-300 dark:border-white/10", "border-slate-300 dark:border-white/10",
spacing spacing
? spacing == "normal" ? spacing == "normal"

0
components/lib/elements/Breadcrumbs.tsx Executable file → Normal file
View File

View File

@ -1,6 +1,19 @@
import React, { DetailedHTMLProps, HTMLAttributes } from "react"; import React, { DetailedHTMLProps, HTMLAttributes } from "react";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
type Props = DetailedHTMLProps<
HTMLAttributes<HTMLDivElement>,
HTMLDivElement
> & {
variant?: "normal";
href?: string;
linkProps?: DetailedHTMLProps<
React.AnchorHTMLAttributes<HTMLAnchorElement>,
HTMLAnchorElement
>;
noHover?: boolean;
};
/** /**
* # General Card * # General Card
* @className twui-card * @className twui-card
@ -13,22 +26,18 @@ export default function Card({
href, href,
variant, variant,
linkProps, linkProps,
noHover,
...props ...props
}: DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement> & { }: Props) {
variant?: "normal";
href?: string;
linkProps?: DetailedHTMLProps<
React.AnchorHTMLAttributes<HTMLAnchorElement>,
HTMLAnchorElement
>;
}) {
const component = ( const component = (
<div <div
{...props} {...props}
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 noHover
? ""
: href
? "hover:bg-slate-100 dark:hover:bg-white/30 hover:border-slate-400 dark:hover:border-white/20" ? "hover:bg-slate-100 dark:hover:bg-white/30 hover:border-slate-400 dark:hover:border-white/20"
: "", : "",
"twui-card", "twui-card",

View File

@ -24,6 +24,7 @@ export type TWUI_DROPDOWN_PROPS = PropsWithChildren &
>; >;
debounce?: number; debounce?: number;
hoverOpen?: boolean; hoverOpen?: boolean;
above?: boolean;
position?: (typeof TWUIDropdownContentPositions)[number]; position?: (typeof TWUIDropdownContentPositions)[number];
topOffset?: number; topOffset?: number;
externalSetOpen?: React.Dispatch<React.SetStateAction<boolean>>; externalSetOpen?: React.Dispatch<React.SetStateAction<boolean>>;
@ -41,6 +42,7 @@ export default function Dropdown({
contentWrapperProps, contentWrapperProps,
targetWrapperProps, targetWrapperProps,
hoverOpen, hoverOpen,
above,
debounce = 500, debounce = 500,
target, target,
position = "center", position = "center",
@ -122,6 +124,7 @@ export default function Dropdown({
: position == "right" : position == "right"
? "right-0" ? "right-0"
: "", : "",
above ? "-translate-y-[120%]" : "",
open ? "flex" : "hidden", open ? "flex" : "hidden",
"twui-dropdown-content", "twui-dropdown-content",
contentWrapperProps?.className contentWrapperProps?.className

View File

@ -15,6 +15,7 @@ export type TWUI_TOGGLE_PROPS = PropsWithChildren &
> & { > & {
color?: "normal" | "secondary" | "error" | "success" | "gray"; color?: "normal" | "secondary" | "error" | "success" | "gray";
variant?: "normal" | "outlined" | "ghost"; variant?: "normal" | "outlined" | "ghost";
href?: string;
}; };
/** /**
@ -25,13 +26,15 @@ export default function Tag({
color, color,
variant, variant,
children, children,
href,
...props ...props
}: TWUI_TOGGLE_PROPS) { }: TWUI_TOGGLE_PROPS) {
return ( const mainComponent = (
<div <div
{...props} {...props}
className={twMerge( className={twMerge(
"text-xs px-2 py-0.5 rounded-full outline outline-0", "text-xs px-2 py-0.5 rounded-full outline outline-0",
"text-center flex items-center justify-center",
color == "secondary" color == "secondary"
? "bg-violet-600 outline-violet-600" ? "bg-violet-600 outline-violet-600"
: color == "success" : color == "success"
@ -63,7 +66,7 @@ export default function Tag({
: color == "gray" : color == "gray"
? "text-slate-700 dark:text-white/80" ? "text-slate-700 dark:text-white/80"
: "text-blue-600") : "text-blue-600")
: "", : "text-white",
"twui-tag", "twui-tag",
props.className props.className
@ -72,4 +75,14 @@ export default function Tag({
{children} {children}
</div> </div>
); );
if (href) {
return (
<a href={href} className={twMerge("hover:opacity-80")}>
{mainComponent}
</a>
);
}
return mainComponent;
} }

View File

@ -27,6 +27,7 @@ export default function Form<T extends object = { [key: string]: any }>({
const formData = new FormData(formEl); const formData = new FormData(formEl);
const data = Object.fromEntries(formData.entries()) as T; const data = Object.fromEntries(formData.entries()) as T;
props.submitHandler?.(e, data); props.submitHandler?.(e, data);
props.onSubmit?.(e);
}} }}
> >
{props.children} {props.children}

View File

@ -14,7 +14,9 @@ type ImageUploadProps = DetailedHTMLProps<
React.HTMLAttributes<HTMLDivElement>, React.HTMLAttributes<HTMLDivElement>,
HTMLDivElement HTMLDivElement
> & { > & {
onChange?: (imgData: ImageInputToBase64FunctionReturn | undefined) => any; onChangeHandler?: (
imgData: ImageInputToBase64FunctionReturn | undefined
) => any;
fileInputProps?: DetailedHTMLProps< fileInputProps?: DetailedHTMLProps<
React.InputHTMLAttributes<HTMLInputElement>, React.InputHTMLAttributes<HTMLInputElement>,
HTMLInputElement HTMLInputElement
@ -35,8 +37,11 @@ type ImageUploadProps = DetailedHTMLProps<
disablePreview?: boolean; disablePreview?: boolean;
}; };
/**
* @note use the `onChangeHandler` prop to grab the parsed base64 image object
*/
export default function ImageUpload({ export default function ImageUpload({
onChange, onChangeHandler,
fileInputProps, fileInputProps,
placeHolderWrapper, placeHolderWrapper,
previewImageWrapperProps, previewImageWrapperProps,
@ -60,7 +65,7 @@ export default function ImageUpload({
onChange={(e) => { onChange={(e) => {
imageInputToBase64({ imageInput: e.target }).then((res) => { imageInputToBase64({ imageInput: e.target }).then((res) => {
setSrc(res.imageBase64Full); setSrc(res.imageBase64Full);
onChange?.(res); onChangeHandler?.(res);
fileInputProps?.onChange?.(e); fileInputProps?.onChange?.(e);
}); });
}} }}
@ -88,7 +93,7 @@ export default function ImageUpload({
className="absolute p-2 top-2 right-2 z-20" className="absolute p-2 top-2 right-2 z-20"
onClick={(e) => { onClick={(e) => {
setSrc(undefined); setSrc(undefined);
onChange?.(undefined); onChangeHandler?.(undefined);
}} }}
> >
<X className="text-slate-950 dark:text-white" /> <X className="text-slate-950 dark:text-white" />

View File

@ -107,6 +107,7 @@ export type InputProps<KeyType extends string> = DetailedHTMLProps<
validationFunction?: (value: string) => Promise<boolean>; validationFunction?: (value: string) => Promise<boolean>;
autoComplete?: (typeof autocompleteOptions)[number]; autoComplete?: (typeof autocompleteOptions)[number];
name?: KeyType; name?: KeyType;
valueUpdate?: string;
}; };
/** /**
@ -130,11 +131,16 @@ export default function Input<KeyType extends string>({
autoComplete, autoComplete,
validationFunction, validationFunction,
validationRegex, validationRegex,
valueUpdate,
...props ...props
}: InputProps<KeyType>) { }: InputProps<KeyType>) {
const [focus, setFocus] = React.useState(false); const [focus, setFocus] = React.useState(false);
const [value, setValue] = React.useState( const [value, setValue] = React.useState(
props.defaultValue ? String(props.defaultValue) : "" props.value
? String(props.value)
: props.defaultValue
? String(props.defaultValue)
: ""
); );
delete props.defaultValue; delete props.defaultValue;
@ -163,6 +169,11 @@ export default function Input<KeyType extends string>({
} }
}, [value]); }, [value]);
React.useEffect(() => {
if (!props.value) return;
setValue(String(props.value));
}, [props.value]);
const targetComponent = istextarea ? ( const targetComponent = istextarea ? (
<textarea <textarea
{...props} {...props}
@ -189,7 +200,10 @@ export default function Input<KeyType extends string>({
<input <input
{...props} {...props}
className={twMerge( className={twMerge(
"w-full outline-none bg-transparent", "w-full outline-none bg-transparent border-none",
"hover:border-none hover:outline-none focus:border-none focus:outline-none",
"dark:bg-transparent dark:outline-none dark:border-none",
"p-0",
"twui-input", "twui-input",
props.className props.className
)} )}
@ -220,7 +234,7 @@ export default function Input<KeyType extends string>({
: "border-slate-300 dark:border-white/20", : "border-slate-300 dark:border-white/20",
focus && isValid focus && isValid
? "outline-slate-700 dark:outline-white/50" ? "outline-slate-700 dark:outline-white/50"
: "outline-transparent", : "outline-slate-300 dark:outline-white/20",
variant == "warning" && variant == "warning" &&
isValid && 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",
@ -246,6 +260,7 @@ export default function Input<KeyType extends string>({
className={twMerge( className={twMerge(
"text-xs absolute -top-2.5 left-2 text-slate-500 bg-white px-1.5 rounded-t", "text-xs absolute -top-2.5 left-2 text-slate-500 bg-white px-1.5 rounded-t",
"dark:text-white/60 dark:bg-black", "dark:text-white/60 dark:bg-black",
"twui-input-label",
labelProps?.className labelProps?.className
)} )}
> >

View File

@ -16,24 +16,7 @@ type SelectOptionObject = {
default?: boolean; default?: boolean;
}; };
type SelectOption = SelectOptionObject | SelectOptionObject[]; type SelectProps = DetailedHTMLProps<
/**
* # Select Element
* @className twui-select-wrapper
* @className twui-select
* @className twui-select-dropdown-icon
*/
export default function Select({
label,
options,
componentRef,
labelProps,
wrapperProps,
showLabel,
iconProps,
...props
}: DetailedHTMLProps<
SelectHTMLAttributes<HTMLSelectElement>, SelectHTMLAttributes<HTMLSelectElement>,
HTMLSelectElement HTMLSelectElement
> & { > & {
@ -50,7 +33,26 @@ export default function Select({
>; >;
componentRef?: RefObject<HTMLSelectElement>; componentRef?: RefObject<HTMLSelectElement>;
iconProps?: LucideProps; iconProps?: LucideProps;
}) { changeHandler?: (value: SelectProps["options"][number]["value"]) => void;
};
/**
* # Select Element
* @className twui-select-wrapper
* @className twui-select
* @className twui-select-dropdown-icon
*/
export default function Select({
label,
options,
componentRef,
labelProps,
wrapperProps,
showLabel,
iconProps,
changeHandler,
...props
}: SelectProps) {
return ( return (
<div <div
{...wrapperProps} {...wrapperProps}
@ -64,8 +66,9 @@ export default function Select({
htmlFor={props.name} htmlFor={props.name}
{...labelProps} {...labelProps}
className={twMerge( className={twMerge(
"text-xs absolute -top-2 left-4 text-slate-600 bg-white px-2", "text-xs absolute -top-2.5 left-2 text-slate-500 bg-white px-1.5 rounded-t",
"dark:text-white/60 dark:bg-black", "dark:text-white/60 dark:bg-black",
"twui-input-label",
labelProps?.className labelProps?.className
)} )}
> >
@ -90,6 +93,12 @@ export default function Select({
options.flat().find((opt) => opt.default)?.value || options.flat().find((opt) => opt.default)?.value ||
undefined undefined
} }
onChange={(e) => {
changeHandler?.(
e.target.value as (typeof options)[number]["value"]
);
props.onChange?.(e);
}}
> >
{options.flat().map((option, index) => { {options.flat().map((option, index) => {
return ( return (

View File

@ -25,10 +25,9 @@ export const WebSocketEventNames = ["wsDataEvent", "wsMessageEvent"] as const;
* console.log(e.detail.message) // type string * console.log(e.detail.message) // type string
* }) * })
*/ */
export default function useWebSocket<T>({ export default function useWebSocket<
url, T extends { [key: string]: any } = { [key: string]: any }
debounce, >({ url, debounce }: UseWebsocketHookParams) {
}: UseWebsocketHookParams) {
const DEBOUNCE = debounce || 200; const DEBOUNCE = debounce || 200;
const [socket, setSocket] = React.useState<WebSocket | undefined>( const [socket, setSocket] = React.useState<WebSocket | undefined>(
@ -75,6 +74,8 @@ export default function useWebSocket<T>({
ws.onclose = (ev) => { ws.onclose = (ev) => {
console.log("Websocket closed ... Attempting to reconnect ..."); console.log("Websocket closed ... Attempting to reconnect ...");
console.log("URL:", url);
reconnectInterval = setInterval(() => { reconnectInterval = setInterval(() => {
if (tries >= 3) { if (tries >= 3) {
return window.clearInterval(reconnectInterval); return window.clearInterval(reconnectInterval);

View File

@ -0,0 +1,39 @@
import React from "react";
import { WebSocketEventNames } from "./useWebSocket";
type Param = {
listener?: (typeof WebSocketEventNames)[number];
};
/**
* # Use Websocket Data Event Handler Hook
*/
export default function useWebSocketEventHandler<
T extends { [key: string]: any } = { [key: string]: any }
>(param?: Param) {
const [data, setData] = React.useState<T | undefined>(undefined);
const [message, setMessage] = React.useState<string | undefined>(undefined);
React.useEffect(() => {
const dataEventListenerCallback = (e: Event) => {
const customEvent = e as CustomEvent;
const data = customEvent.detail.data as T | undefined;
const message = customEvent.detail.message as string | undefined;
if (data) setData(data);
if (message) setMessage(message);
};
const messageEventName: (typeof WebSocketEventNames)[number] =
param?.listener || "wsDataEvent";
window.addEventListener(messageEventName, dataEventListenerCallback);
return function () {
window.removeEventListener(
messageEventName,
dataEventListenerCallback
);
};
}, []);
return { data, message };
}

View File

@ -8,6 +8,36 @@ import {
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import Loading from "../elements/Loading"; import Loading from "../elements/Loading";
export type TWUIButtonProps = DetailedHTMLProps<
ButtonHTMLAttributes<HTMLButtonElement>,
HTMLButtonElement
> & {
variant?: "normal" | "ghost" | "outlined";
color?:
| "primary"
| "secondary"
| "accent"
| "gray"
| "error"
| "warning"
| "success";
size?: "small" | "smaller" | "normal" | "large" | "larger";
loadingIconSize?: React.ComponentProps<typeof Loading>["size"];
href?: string;
target?: HTMLAttributeAnchorTarget;
loading?: boolean;
linkProps?: DetailedHTMLProps<
AnchorHTMLAttributes<HTMLAnchorElement>,
HTMLAnchorElement
>;
beforeIcon?: React.ReactNode;
afterIcon?: React.ReactNode;
buttonContentProps?: DetailedHTMLProps<
HTMLAttributes<HTMLDivElement>,
HTMLDivElement
>;
};
/** /**
* # Buttons * # Buttons
* @className twui-button-general * @className twui-button-general
@ -36,35 +66,9 @@ export default function Button({
beforeIcon, beforeIcon,
afterIcon, afterIcon,
loading, loading,
loadingIconSize,
...props ...props
}: DetailedHTMLProps< }: TWUIButtonProps) {
ButtonHTMLAttributes<HTMLButtonElement>,
HTMLButtonElement
> & {
variant?: "normal" | "ghost" | "outlined";
color?:
| "primary"
| "secondary"
| "accent"
| "gray"
| "error"
| "warning"
| "success";
size?: "small" | "smaller" | "normal" | "large" | "larger";
href?: string;
target?: HTMLAttributeAnchorTarget;
loading?: boolean;
linkProps?: DetailedHTMLProps<
AnchorHTMLAttributes<HTMLAnchorElement>,
HTMLAnchorElement
>;
beforeIcon?: React.ReactNode;
afterIcon?: React.ReactNode;
buttonContentProps?: DetailedHTMLProps<
HTMLAttributes<HTMLDivElement>,
HTMLDivElement
>;
}) {
const finalClassName: string = (() => { const finalClassName: string = (() => {
if (variant == "normal" || !variant) { if (variant == "normal" || !variant) {
if (color == "primary" || !color) if (color == "primary" || !color)
@ -194,6 +198,7 @@ export default function Button({
<Loading <Loading
className="absolute" className="absolute"
size={(() => { size={(() => {
if (loadingIconSize) return loadingIconSize;
switch (size) { switch (size) {
case "small": case "small":
return "small"; return "small";

26
components/lib/utils/fetch/fetchApi.ts Executable file → Normal file
View File

@ -1,6 +1,6 @@
import _ from "lodash"; import _ from "lodash";
type FetchApiOptions = { type FetchApiOptions<T extends { [k: string]: any } = { [k: string]: any }> = {
method: method:
| "POST" | "POST"
| "GET" | "GET"
@ -12,7 +12,7 @@ type FetchApiOptions = {
| "delete" | "delete"
| "put" | "put"
| "patch"; | "patch";
body?: object | string; body?: T | string;
headers?: FetchHeader; headers?: FetchHeader;
}; };
@ -30,13 +30,23 @@ export type FetchApiReturn = {
/** /**
* # Fetch API * # Fetch API
*/ */
export default async function fetchApi( export default async function fetchApi<
T extends { [k: string]: any } = { [k: string]: any },
R extends any = any
>(
url: string, url: string,
options?: FetchApiOptions, options?: FetchApiOptions<T>,
csrf?: boolean, csrf?: boolean,
/** Key to use to grab local Storage csrf value. */ /**
localStorageCSRFKey?: string * Key to use to grab local Storage csrf value.
): Promise<any> { */
localStorageCSRFKey?: string,
/**
* Key with which to set the request header csrf
* value
*/
csrfHeaderKey?: string
): Promise<R> {
let data; let data;
const csrfValue = localStorage.getItem(localStorageCSRFKey || "csrf"); const csrfValue = localStorage.getItem(localStorageCSRFKey || "csrf");
@ -46,7 +56,7 @@ export default async function fetchApi(
} as FetchHeader; } as FetchHeader;
if (csrf && csrfValue) { if (csrf && csrfValue) {
finalHeaders[`'${csrfValue.replace(/\"/g, "")}'`] = "true"; finalHeaders[csrfHeaderKey || "x-csrf-key"] = csrfValue;
} }
if (typeof options === "string") { if (typeof options === "string") {

View File

@ -88,6 +88,17 @@ export const skills = {
image: "/images/work/devops/server-management.png", image: "/images/work/devops/server-management.png",
technologies: ["Node JS", "Bun JS", "Shell Script", "Python"], technologies: ["Node JS", "Bun JS", "Shell Script", "Python"],
}, },
{
title: "API Development and Integration",
description:
"Developing custom APIs and integrating existing APIs from external services",
technologies: [
"API",
"REST",
"API Development",
"Data Fetching",
],
},
{ {
title: "React JS", title: "React JS",
description: "Development of React JS applications for the web", description: "Development of React JS applications for the web",

View File

@ -45,6 +45,13 @@ export const work = {
"NGINX Reverse Proxy", "NGINX Reverse Proxy",
], ],
}, },
{
title: "Ifuekosa LLC",
description:
"Tax Preparation, Notary and Business Consulting Services in New Jersey",
href: "https://ifuekosallc.com/",
technologies: ["Wordpress", "Docker", "Email Server"],
},
], ],
}, },
Devops: { Devops: {