Updates
This commit is contained in:
		
							parent
							
								
									f0850146be
								
							
						
					
					
						commit
						442ad5aa32
					
				
							
								
								
									
										0
									
								
								components/lib/editors/AceEditor.tsx
									
									
									
									
									
										
										
										Executable file → Normal file
									
								
							
							
						
						
									
										0
									
								
								components/lib/editors/AceEditor.tsx
									
									
									
									
									
										
										
										Executable file → Normal file
									
								
							| @ -17,7 +17,7 @@ export default function Border({ spacing, ...props }: TWUI_BORDER_PROPS) { | ||||
|         <div | ||||
|             {...props} | ||||
|             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", | ||||
|                 spacing | ||||
|                     ? spacing == "normal" | ||||
|  | ||||
							
								
								
									
										0
									
								
								components/lib/elements/Breadcrumbs.tsx
									
									
									
									
									
										
										
										Executable file → Normal file
									
								
							
							
						
						
									
										0
									
								
								components/lib/elements/Breadcrumbs.tsx
									
									
									
									
									
										
										
										Executable file → Normal file
									
								
							| @ -1,6 +1,19 @@ | ||||
| import React, { DetailedHTMLProps, HTMLAttributes } from "react"; | ||||
| 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 | ||||
|  * @className twui-card | ||||
| @ -13,22 +26,18 @@ export default function Card({ | ||||
|     href, | ||||
|     variant, | ||||
|     linkProps, | ||||
|     noHover, | ||||
|     ...props | ||||
| }: DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement> & { | ||||
|     variant?: "normal"; | ||||
|     href?: string; | ||||
|     linkProps?: DetailedHTMLProps< | ||||
|         React.AnchorHTMLAttributes<HTMLAnchorElement>, | ||||
|         HTMLAnchorElement | ||||
|     >; | ||||
| }) { | ||||
| }: Props) { | ||||
|     const component = ( | ||||
|         <div | ||||
|             {...props} | ||||
|             className={twMerge( | ||||
|                 "flex flex-row items-center p-4 rounded bg-white dark:bg-white/10", | ||||
|                 "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" | ||||
|                     : "", | ||||
|                 "twui-card", | ||||
|  | ||||
| @ -24,6 +24,7 @@ export type TWUI_DROPDOWN_PROPS = PropsWithChildren & | ||||
|         >; | ||||
|         debounce?: number; | ||||
|         hoverOpen?: boolean; | ||||
|         above?: boolean; | ||||
|         position?: (typeof TWUIDropdownContentPositions)[number]; | ||||
|         topOffset?: number; | ||||
|         externalSetOpen?: React.Dispatch<React.SetStateAction<boolean>>; | ||||
| @ -41,6 +42,7 @@ export default function Dropdown({ | ||||
|     contentWrapperProps, | ||||
|     targetWrapperProps, | ||||
|     hoverOpen, | ||||
|     above, | ||||
|     debounce = 500, | ||||
|     target, | ||||
|     position = "center", | ||||
| @ -122,6 +124,7 @@ export default function Dropdown({ | ||||
|                         : position == "right" | ||||
|                         ? "right-0" | ||||
|                         : "", | ||||
|                     above ? "-translate-y-[120%]" : "", | ||||
|                     open ? "flex" : "hidden", | ||||
|                     "twui-dropdown-content", | ||||
|                     contentWrapperProps?.className | ||||
|  | ||||
| @ -15,6 +15,7 @@ export type TWUI_TOGGLE_PROPS = PropsWithChildren & | ||||
|     > & { | ||||
|         color?: "normal" | "secondary" | "error" | "success" | "gray"; | ||||
|         variant?: "normal" | "outlined" | "ghost"; | ||||
|         href?: string; | ||||
|     }; | ||||
| 
 | ||||
| /** | ||||
| @ -25,13 +26,15 @@ export default function Tag({ | ||||
|     color, | ||||
|     variant, | ||||
|     children, | ||||
|     href, | ||||
|     ...props | ||||
| }: TWUI_TOGGLE_PROPS) { | ||||
|     return ( | ||||
|     const mainComponent = ( | ||||
|         <div | ||||
|             {...props} | ||||
|             className={twMerge( | ||||
|                 "text-xs px-2 py-0.5 rounded-full outline outline-0", | ||||
|                 "text-center flex items-center justify-center", | ||||
|                 color == "secondary" | ||||
|                     ? "bg-violet-600 outline-violet-600" | ||||
|                     : color == "success" | ||||
| @ -63,7 +66,7 @@ export default function Tag({ | ||||
|                           : color == "gray" | ||||
|                           ? "text-slate-700 dark:text-white/80" | ||||
|                           : "text-blue-600") | ||||
|                     : "", | ||||
|                     : "text-white", | ||||
| 
 | ||||
|                 "twui-tag", | ||||
|                 props.className | ||||
| @ -72,4 +75,14 @@ export default function Tag({ | ||||
|             {children} | ||||
|         </div> | ||||
|     ); | ||||
| 
 | ||||
|     if (href) { | ||||
|         return ( | ||||
|             <a href={href} className={twMerge("hover:opacity-80")}> | ||||
|                 {mainComponent} | ||||
|             </a> | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     return mainComponent; | ||||
| } | ||||
|  | ||||
| @ -27,6 +27,7 @@ export default function Form<T extends object = { [key: string]: any }>({ | ||||
|                 const formData = new FormData(formEl); | ||||
|                 const data = Object.fromEntries(formData.entries()) as T; | ||||
|                 props.submitHandler?.(e, data); | ||||
|                 props.onSubmit?.(e); | ||||
|             }} | ||||
|         > | ||||
|             {props.children} | ||||
|  | ||||
| @ -14,7 +14,9 @@ type ImageUploadProps = DetailedHTMLProps< | ||||
|     React.HTMLAttributes<HTMLDivElement>, | ||||
|     HTMLDivElement | ||||
| > & { | ||||
|     onChange?: (imgData: ImageInputToBase64FunctionReturn | undefined) => any; | ||||
|     onChangeHandler?: ( | ||||
|         imgData: ImageInputToBase64FunctionReturn | undefined | ||||
|     ) => any; | ||||
|     fileInputProps?: DetailedHTMLProps< | ||||
|         React.InputHTMLAttributes<HTMLInputElement>, | ||||
|         HTMLInputElement | ||||
| @ -35,8 +37,11 @@ type ImageUploadProps = DetailedHTMLProps< | ||||
|     disablePreview?: boolean; | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * @note use the `onChangeHandler` prop to grab the parsed base64 image object | ||||
|  */ | ||||
| export default function ImageUpload({ | ||||
|     onChange, | ||||
|     onChangeHandler, | ||||
|     fileInputProps, | ||||
|     placeHolderWrapper, | ||||
|     previewImageWrapperProps, | ||||
| @ -60,7 +65,7 @@ export default function ImageUpload({ | ||||
|                 onChange={(e) => { | ||||
|                     imageInputToBase64({ imageInput: e.target }).then((res) => { | ||||
|                         setSrc(res.imageBase64Full); | ||||
|                         onChange?.(res); | ||||
|                         onChangeHandler?.(res); | ||||
|                         fileInputProps?.onChange?.(e); | ||||
|                     }); | ||||
|                 }} | ||||
| @ -88,7 +93,7 @@ export default function ImageUpload({ | ||||
|                         className="absolute p-2 top-2 right-2 z-20" | ||||
|                         onClick={(e) => { | ||||
|                             setSrc(undefined); | ||||
|                             onChange?.(undefined); | ||||
|                             onChangeHandler?.(undefined); | ||||
|                         }} | ||||
|                     > | ||||
|                         <X className="text-slate-950 dark:text-white" /> | ||||
|  | ||||
| @ -107,6 +107,7 @@ export type InputProps<KeyType extends string> = DetailedHTMLProps< | ||||
|         validationFunction?: (value: string) => Promise<boolean>; | ||||
|         autoComplete?: (typeof autocompleteOptions)[number]; | ||||
|         name?: KeyType; | ||||
|         valueUpdate?: string; | ||||
|     }; | ||||
| 
 | ||||
| /** | ||||
| @ -130,11 +131,16 @@ export default function Input<KeyType extends string>({ | ||||
|     autoComplete, | ||||
|     validationFunction, | ||||
|     validationRegex, | ||||
|     valueUpdate, | ||||
|     ...props | ||||
| }: InputProps<KeyType>) { | ||||
|     const [focus, setFocus] = React.useState(false); | ||||
|     const [value, setValue] = React.useState( | ||||
|         props.defaultValue ? String(props.defaultValue) : "" | ||||
|         props.value | ||||
|             ? String(props.value) | ||||
|             : props.defaultValue | ||||
|             ? String(props.defaultValue) | ||||
|             : "" | ||||
|     ); | ||||
| 
 | ||||
|     delete props.defaultValue; | ||||
| @ -163,6 +169,11 @@ export default function Input<KeyType extends string>({ | ||||
|         } | ||||
|     }, [value]); | ||||
| 
 | ||||
|     React.useEffect(() => { | ||||
|         if (!props.value) return; | ||||
|         setValue(String(props.value)); | ||||
|     }, [props.value]); | ||||
| 
 | ||||
|     const targetComponent = istextarea ? ( | ||||
|         <textarea | ||||
|             {...props} | ||||
| @ -189,7 +200,10 @@ export default function Input<KeyType extends string>({ | ||||
|         <input | ||||
|             {...props} | ||||
|             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", | ||||
|                 props.className | ||||
|             )} | ||||
| @ -220,7 +234,7 @@ export default function Input<KeyType extends string>({ | ||||
|                     : "border-slate-300 dark:border-white/20", | ||||
|                 focus && isValid | ||||
|                     ? "outline-slate-700 dark:outline-white/50" | ||||
|                     : "outline-transparent", | ||||
|                     : "outline-slate-300 dark:outline-white/20", | ||||
|                 variant == "warning" && | ||||
|                     isValid && | ||||
|                     "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( | ||||
|                         "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", | ||||
|                         "twui-input-label", | ||||
|                         labelProps?.className | ||||
|                     )} | ||||
|                 > | ||||
|  | ||||
| @ -16,24 +16,7 @@ type SelectOptionObject = { | ||||
|     default?: boolean; | ||||
| }; | ||||
| 
 | ||||
| type SelectOption = SelectOptionObject | SelectOptionObject[]; | ||||
| 
 | ||||
| /** | ||||
|  * # 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< | ||||
| type SelectProps = DetailedHTMLProps< | ||||
|     SelectHTMLAttributes<HTMLSelectElement>, | ||||
|     HTMLSelectElement | ||||
| > & { | ||||
| @ -50,7 +33,26 @@ export default function Select({ | ||||
|     >; | ||||
|     componentRef?: RefObject<HTMLSelectElement>; | ||||
|     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 ( | ||||
|         <div | ||||
|             {...wrapperProps} | ||||
| @ -64,8 +66,9 @@ export default function Select({ | ||||
|                     htmlFor={props.name} | ||||
|                     {...labelProps} | ||||
|                     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", | ||||
|                         "twui-input-label", | ||||
|                         labelProps?.className | ||||
|                     )} | ||||
|                 > | ||||
| @ -90,6 +93,12 @@ export default function Select({ | ||||
|                     options.flat().find((opt) => opt.default)?.value || | ||||
|                     undefined | ||||
|                 } | ||||
|                 onChange={(e) => { | ||||
|                     changeHandler?.( | ||||
|                         e.target.value as (typeof options)[number]["value"] | ||||
|                     ); | ||||
|                     props.onChange?.(e); | ||||
|                 }} | ||||
|             > | ||||
|                 {options.flat().map((option, index) => { | ||||
|                     return ( | ||||
|  | ||||
| @ -25,10 +25,9 @@ export const WebSocketEventNames = ["wsDataEvent", "wsMessageEvent"] as const; | ||||
|  *      console.log(e.detail.message) // type string
 | ||||
|  * }) | ||||
|  */ | ||||
| export default function useWebSocket<T>({ | ||||
|     url, | ||||
|     debounce, | ||||
| }: UseWebsocketHookParams) { | ||||
| export default function useWebSocket< | ||||
|     T extends { [key: string]: any } = { [key: string]: any } | ||||
| >({ url, debounce }: UseWebsocketHookParams) { | ||||
|     const DEBOUNCE = debounce || 200; | ||||
| 
 | ||||
|     const [socket, setSocket] = React.useState<WebSocket | undefined>( | ||||
| @ -75,6 +74,8 @@ export default function useWebSocket<T>({ | ||||
| 
 | ||||
|         ws.onclose = (ev) => { | ||||
|             console.log("Websocket closed ... Attempting to reconnect ..."); | ||||
|             console.log("URL:", url); | ||||
| 
 | ||||
|             reconnectInterval = setInterval(() => { | ||||
|                 if (tries >= 3) { | ||||
|                     return window.clearInterval(reconnectInterval); | ||||
|  | ||||
							
								
								
									
										39
									
								
								components/lib/hooks/useWebSocketEventHandler.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								components/lib/hooks/useWebSocketEventHandler.tsx
									
									
									
									
									
										Normal 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 }; | ||||
| } | ||||
| @ -8,6 +8,36 @@ import { | ||||
| import { twMerge } from "tailwind-merge"; | ||||
| 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 | ||||
|  * @className twui-button-general | ||||
| @ -36,35 +66,9 @@ export default function Button({ | ||||
|     beforeIcon, | ||||
|     afterIcon, | ||||
|     loading, | ||||
|     loadingIconSize, | ||||
|     ...props | ||||
| }: DetailedHTMLProps< | ||||
|     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 | ||||
|     >; | ||||
| }) { | ||||
| }: TWUIButtonProps) { | ||||
|     const finalClassName: string = (() => { | ||||
|         if (variant == "normal" || !variant) { | ||||
|             if (color == "primary" || !color) | ||||
| @ -194,6 +198,7 @@ export default function Button({ | ||||
|                 <Loading | ||||
|                     className="absolute" | ||||
|                     size={(() => { | ||||
|                         if (loadingIconSize) return loadingIconSize; | ||||
|                         switch (size) { | ||||
|                             case "small": | ||||
|                                 return "small"; | ||||
|  | ||||
							
								
								
									
										26
									
								
								components/lib/utils/fetch/fetchApi.ts
									
									
									
									
									
										
										
										Executable file → Normal file
									
								
							
							
						
						
									
										26
									
								
								components/lib/utils/fetch/fetchApi.ts
									
									
									
									
									
										
										
										Executable file → Normal file
									
								
							| @ -1,6 +1,6 @@ | ||||
| import _ from "lodash"; | ||||
| 
 | ||||
| type FetchApiOptions = { | ||||
| type FetchApiOptions<T extends { [k: string]: any } = { [k: string]: any }> = { | ||||
|     method: | ||||
|         | "POST" | ||||
|         | "GET" | ||||
| @ -12,7 +12,7 @@ type FetchApiOptions = { | ||||
|         | "delete" | ||||
|         | "put" | ||||
|         | "patch"; | ||||
|     body?: object | string; | ||||
|     body?: T | string; | ||||
|     headers?: FetchHeader; | ||||
| }; | ||||
| 
 | ||||
| @ -30,13 +30,23 @@ export type FetchApiReturn = { | ||||
| /** | ||||
|  * # 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, | ||||
|     options?: FetchApiOptions, | ||||
|     options?: FetchApiOptions<T>, | ||||
|     csrf?: boolean, | ||||
|     /** Key to use to grab local Storage csrf value. */ | ||||
|     localStorageCSRFKey?: string | ||||
| ): Promise<any> { | ||||
|     /** | ||||
|      * Key to use to grab local Storage csrf value. | ||||
|      */ | ||||
|     localStorageCSRFKey?: string, | ||||
|     /** | ||||
|      * Key with which to set the request header csrf | ||||
|      * value | ||||
|      */ | ||||
|     csrfHeaderKey?: string | ||||
| ): Promise<R> { | ||||
|     let data; | ||||
| 
 | ||||
|     const csrfValue = localStorage.getItem(localStorageCSRFKey || "csrf"); | ||||
| @ -46,7 +56,7 @@ export default async function fetchApi( | ||||
|     } as FetchHeader; | ||||
| 
 | ||||
|     if (csrf && csrfValue) { | ||||
|         finalHeaders[`'${csrfValue.replace(/\"/g, "")}'`] = "true"; | ||||
|         finalHeaders[csrfHeaderKey || "x-csrf-key"] = csrfValue; | ||||
|     } | ||||
| 
 | ||||
|     if (typeof options === "string") { | ||||
|  | ||||
| @ -88,6 +88,17 @@ export const skills = { | ||||
|                 image: "/images/work/devops/server-management.png", | ||||
|                 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", | ||||
|                 description: "Development of React JS applications for the web", | ||||
|  | ||||
| @ -45,6 +45,13 @@ export const work = { | ||||
|                     "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: { | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user
	 Benjamin Toby
						Benjamin Toby