First Commit

This commit is contained in:
Benjamin Toby
2024-12-09 16:36:17 +01:00
parent f24b493566
commit b037cec100
128 changed files with 3281 additions and 193 deletions
+23
View File
@@ -0,0 +1,23 @@
import { DetailedHTMLProps, FormHTMLAttributes } from "react";
import { twMerge } from "tailwind-merge";
/**
* # Form Element
* @className twui-form
*/
export default function Form({
...props
}: DetailedHTMLProps<FormHTMLAttributes<HTMLFormElement>, HTMLFormElement>) {
return (
<form
{...props}
className={twMerge(
"flex flex-col items-stretch gap-2 w-full bg-transparent",
"twui-form",
props.className
)}
>
{props.children}
</form>
);
}
+109
View File
@@ -0,0 +1,109 @@
import Button from "@/components/lib/layout/Button";
import Stack from "@/components/lib/layout/Stack";
import { ImagePlus, X } from "lucide-react";
import React, { DetailedHTMLProps } from "react";
import Card from "@/components/lib/elements/Card";
import Span from "@/components/lib/layout/Span";
import Center from "@/components/lib/layout/Center";
import imageInputToBase64, {
ImageInputToBase64FunctionReturn,
} from "../utils/form/imageInputToBase64";
import { twMerge } from "tailwind-merge";
type ImageUploadProps = {
onChange?: (imgData: ImageInputToBase64FunctionReturn | undefined) => any;
fileInputProps?: DetailedHTMLProps<
React.InputHTMLAttributes<HTMLInputElement>,
HTMLInputElement
>;
wrapperProps?: DetailedHTMLProps<
React.HTMLAttributes<HTMLDivElement>,
HTMLDivElement
>;
placeHolderWrapper?: DetailedHTMLProps<
React.HTMLAttributes<HTMLDivElement>,
HTMLDivElement
>;
previewImageWrapperProps?: DetailedHTMLProps<
React.HTMLAttributes<HTMLDivElement>,
HTMLDivElement
>;
previewImageProps?: DetailedHTMLProps<
React.ImgHTMLAttributes<HTMLImageElement>,
HTMLImageElement
>;
};
export default function ImageUpload({
onChange,
fileInputProps,
wrapperProps,
placeHolderWrapper,
previewImageWrapperProps,
previewImageProps,
}: ImageUploadProps) {
const [src, setSrc] = React.useState<string | undefined>(undefined);
const inputRef = React.useRef<HTMLInputElement>();
return (
<Stack
className={twMerge("w-full", wrapperProps?.className)}
{...wrapperProps}
>
<input
type="file"
className={twMerge("hidden", fileInputProps?.className)}
{...fileInputProps}
onChange={(e) => {
imageInputToBase64({ imageInput: e.target }).then((res) => {
setSrc(res.imageBase64Full);
onChange?.(res);
fileInputProps?.onChange?.(e);
});
}}
ref={inputRef as any}
/>
{src ? (
<Card className="w-full relative" {...previewImageWrapperProps}>
<img
src={src}
className="w-full h-[300px] object-contain"
{...previewImageProps}
/>
<Button
variant="ghost"
className="absolute p-2 top-2 right-2 z-20"
onClick={(e) => {
setSrc(undefined);
onChange?.(undefined);
}}
>
<X className="text-slate-950" />
</Button>
</Card>
) : (
<Card
className={twMerge(
"w-full h-[300px] cursor-pointer hover:bg-slate-100 dark:hover:bg-white/20",
placeHolderWrapper?.className
)}
onClick={(e) => {
inputRef.current?.click();
placeHolderWrapper?.onClick?.(e);
}}
{...placeHolderWrapper}
>
<Center>
<Stack className="items-center gap-2">
<ImagePlus className="text-slate-400" />
<Span size="smaller" variant="faded">
Click to Upload Image
</Span>
</Stack>
</Center>
</Card>
)}
</Stack>
);
}
+136
View File
@@ -0,0 +1,136 @@
import React, {
DetailedHTMLProps,
InputHTMLAttributes,
LabelHTMLAttributes,
RefObject,
TextareaHTMLAttributes,
} from "react";
import { twMerge } from "tailwind-merge";
export type InputProps = DetailedHTMLProps<
InputHTMLAttributes<HTMLInputElement>,
HTMLInputElement
> &
DetailedHTMLProps<
TextareaHTMLAttributes<HTMLTextAreaElement>,
HTMLTextAreaElement
> & {
label?: string;
variant?: "normal" | "warning" | "error" | "inactive";
prefix?: string | React.ReactNode;
suffix?: string | React.ReactNode;
showLabel?: boolean;
istextarea?: boolean;
wrapperProps?: DetailedHTMLProps<
InputHTMLAttributes<HTMLDivElement>,
HTMLDivElement
>;
labelProps?: DetailedHTMLProps<
LabelHTMLAttributes<HTMLLabelElement>,
HTMLLabelElement
>;
componentRef?: RefObject<any>;
};
/**
* # Input Element
* @className twui-input
*/
export default function Input({
label,
variant,
prefix,
suffix,
componentRef,
labelProps,
wrapperProps,
showLabel,
istextarea,
...props
}: InputProps) {
const [focus, setFocus] = React.useState(false);
const targetComponent = istextarea ? (
<textarea
{...props}
className={twMerge(
"w-full outline-none bg-transparent",
"twui-textarea",
props.className
)}
ref={componentRef}
onFocus={(e) => {
setFocus(true);
props?.onFocus?.(e);
}}
onBlur={(e) => {
setFocus(false);
props?.onBlur?.(e);
}}
/>
) : (
<input
{...props}
className={twMerge(
"w-full outline-none bg-transparent",
"twui-input",
props.className
)}
ref={componentRef}
onFocus={(e) => {
setFocus(true);
props?.onFocus?.(e);
}}
onBlur={(e) => {
setFocus(false);
props?.onBlur?.(e);
}}
/>
);
return (
<div
{...wrapperProps}
className={twMerge(
"relative flex items-center gap-2 border rounded-md px-3 py-2 outline outline-1",
focus
? "border-slate-700 dark:border-white/50"
: "border-slate-300 dark:border-white/20",
focus
? "outline-slate-700 dark:outline-white/50"
: "outline-transparent",
variant == "warning" &&
"border-yellow-500 dark:border-yellow-300 outline-yellow-500 dark:outline-yellow-300",
variant == "error" &&
"border-red-500 dark:border-red-300 outline-red-500 dark:outline-red-300",
variant == "inactive" && "opacity-40 pointer-events-none",
"bg-white dark:bg-black",
wrapperProps?.className
)}
>
{showLabel && (
<label
htmlFor={props.name}
{...labelProps}
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",
labelProps?.className
)}
>
{label || props.placeholder || props.name}
</label>
)}
{prefix && (
<div className="opacity-60 pointer-events-none">{prefix}</div>
)}
{targetComponent}
{suffix && (
<div className="opacity-60 pointer-events-none">{suffix}</div>
)}
</div>
);
}
+117
View File
@@ -0,0 +1,117 @@
import { ChevronDown, LucideProps } from "lucide-react";
import {
DetailedHTMLProps,
ForwardRefExoticComponent,
InputHTMLAttributes,
LabelHTMLAttributes,
RefAttributes,
RefObject,
SelectHTMLAttributes,
} from "react";
import { twMerge } from "tailwind-merge";
type SelectOptionObject = {
title: string;
value: string;
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<
SelectHTMLAttributes<HTMLSelectElement>,
HTMLSelectElement
> & {
options: SelectOptionObject[];
label?: string;
showLabel?: boolean;
wrapperProps?: DetailedHTMLProps<
InputHTMLAttributes<HTMLDivElement>,
HTMLDivElement
>;
labelProps?: DetailedHTMLProps<
LabelHTMLAttributes<HTMLLabelElement>,
HTMLLabelElement
>;
componentRef?: RefObject<HTMLSelectElement>;
iconProps?: LucideProps;
}) {
return (
<div
{...wrapperProps}
className={twMerge(
"relative w-full flex items-center",
wrapperProps?.className
)}
>
{showLabel && (
<label
htmlFor={props.name}
{...labelProps}
className={twMerge(
"text-xs absolute -top-2 left-4 text-slate-600 bg-white px-2",
"dark:text-white/60 dark:bg-black",
labelProps?.className
)}
>
{label || props.name}
</label>
)}
<select
{...props}
className={twMerge(
"w-full pl-3 py-2 border rounded-md appearance-none pr-8",
"border-slate-300 dark:border-white/20",
"focus:border-slate-700 dark:focus:border-white/50",
"outline-slate-300 dark:outline-white/20",
"focus:outline-slate-700 dark:focus:outline-white/50",
"bg-white dark:bg-black",
"twui-select",
props.className
)}
ref={componentRef}
defaultValue={
options.flat().find((opt) => opt.default)?.value ||
undefined
}
>
{options.flat().map((option, index) => {
return (
<option
key={index}
value={option.value}
// selected={option.default || undefined}
>
{option.title}
</option>
);
})}
</select>
<ChevronDown
size={20}
{...iconProps}
className={twMerge(
"absolute right-2 pointer-events-none",
iconProps?.className
)}
/>
</div>
);
}
+9
View File
@@ -0,0 +1,9 @@
import Input, { InputProps } from "./Input";
/**
* # Textarea Component
* @className twui-textarea
*/
export default function Textarea({ componentRef, ...props }: InputProps) {
return <Input istextarea {...props} componentRef={componentRef} />;
}