First Commit
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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} />;
|
||||
}
|
||||
Reference in New Issue
Block a user