Compare commits
No commits in common. "600692e3e5378e8a44d3ff5b148b70ed614bdc93" and "3dfee64e8e4c26b506fafcc6f4d2154dc0f35c1d" have entirely different histories.
600692e3e5
...
3dfee64e8e
@ -2,55 +2,20 @@
|
|||||||
|
|
||||||
import { AccountInfo } from "@/components/account-info";
|
import { AccountInfo } from "@/components/account-info";
|
||||||
import { UserDataContext } from "@/components/providers/userdata-provider";
|
import { UserDataContext } from "@/components/providers/userdata-provider";
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { useContext } from "react";
|
||||||
import type { IUserData } from "@/interfaces/userdata.interface";
|
|
||||||
import {
|
|
||||||
type Dispatch,
|
|
||||||
type SetStateAction,
|
|
||||||
useContext,
|
|
||||||
useEffect,
|
|
||||||
useState,
|
|
||||||
} from "react";
|
|
||||||
|
|
||||||
export function AccountDialog() {
|
export function AccountDialog() {
|
||||||
const userContext = useContext(UserDataContext);
|
const userContext = useContext(UserDataContext);
|
||||||
const [isLoaded, setIsLoaded] = useState<boolean>(false);
|
|
||||||
|
|
||||||
if (!userContext?.userData) {
|
if (!userContext?.userData) {
|
||||||
userContext?.setUserData({
|
userContext?.setUserData({ name: "Mathis" });
|
||||||
age: 0,
|
return <p>Loading...</p>;
|
||||||
city: "Chambéry",
|
|
||||||
created_at: "jaj",
|
|
||||||
dollarAvailables: 34,
|
|
||||||
email: "mherriot@tutanota.com",
|
|
||||||
id: "",
|
|
||||||
isActive: false,
|
|
||||||
lastName: "Herriot",
|
|
||||||
pseudo: "Avnyr",
|
|
||||||
roleId: "",
|
|
||||||
updated_at: "",
|
|
||||||
firstName: "Mathis",
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
//TODO No account context
|
||||||
if (userContext?.userData) {
|
|
||||||
setIsLoaded(true);
|
|
||||||
}
|
|
||||||
}, [userContext?.userData]);
|
|
||||||
|
|
||||||
if (!isLoaded) {
|
//TODO Loading context
|
||||||
return <Skeleton className="w-14 h-10 rounded" />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
//TODO Account context
|
||||||
<div>
|
return <AccountInfo userData={userContext.userData} />;
|
||||||
<AccountInfo
|
|
||||||
userData={userContext?.userData as IUserData}
|
|
||||||
setUserData={
|
|
||||||
userContext?.setUserData as Dispatch<SetStateAction<IUserData | undefined>>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
@ -1,61 +1,64 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
DialogTrigger,
|
|
||||||
} from "@/components/ui/dialog";
|
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
|
import {
|
||||||
|
Sheet,
|
||||||
|
SheetClose,
|
||||||
|
SheetContent,
|
||||||
|
SheetDescription,
|
||||||
|
SheetFooter,
|
||||||
|
SheetHeader,
|
||||||
|
SheetTitle,
|
||||||
|
SheetTrigger,
|
||||||
|
} from "@/components/ui/sheet";
|
||||||
import type { IUserData } from "@/interfaces/userdata.interface";
|
import type { IUserData } from "@/interfaces/userdata.interface";
|
||||||
|
import { User } from "lucide-react";
|
||||||
|
|
||||||
import { Landmark, Unplug, User, Wallet } from "lucide-react";
|
export function AccountInfo({ userData }: { userData: IUserData }) {
|
||||||
import type React from "react";
|
|
||||||
|
|
||||||
export function AccountInfo({
|
|
||||||
userData,
|
|
||||||
setUserData,
|
|
||||||
}: {
|
|
||||||
userData: IUserData;
|
|
||||||
setUserData: React.Dispatch<React.SetStateAction<IUserData | undefined>>;
|
|
||||||
}) {
|
|
||||||
return (
|
return (
|
||||||
<Dialog>
|
<Sheet>
|
||||||
<DialogTrigger asChild>
|
<SheetTrigger asChild>
|
||||||
<Button variant="outline" className={"gap-2 px-2"}>
|
<Button variant="outline" className={"gap-1"}>
|
||||||
<p>{userData.firstName}</p>
|
|
||||||
<User />
|
<User />
|
||||||
|
{userData?.firstName || "?"}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</SheetTrigger>
|
||||||
<DialogContent className="sm:max-w-[425px]">
|
<SheetContent>
|
||||||
<DialogHeader>
|
<SheetHeader>
|
||||||
<DialogTitle>{`Your account - ${userData.firstName} ${userData.lastName}`}</DialogTitle>
|
<SheetTitle>Edit profile</SheetTitle>
|
||||||
<DialogDescription>{userData.city}</DialogDescription>
|
<SheetDescription>
|
||||||
</DialogHeader>
|
Make changes to your profile here. Click save when you're done.
|
||||||
<div className={"flex flex-col items-center justify-center w-full"}>
|
</SheetDescription>
|
||||||
<div className={"flex flex-row justify-evenly items-center"}>
|
</SheetHeader>
|
||||||
<div className={"flex flex-row gap-1 justify-center items-center mx-auto"}>
|
<div className="grid gap-4 py-4">
|
||||||
<Landmark />
|
<div className="grid grid-cols-4 items-center gap-4">
|
||||||
<p>{userData.dollarAvailables} $</p>
|
<Label htmlFor="name" className="text-right">
|
||||||
</div>
|
Name
|
||||||
<div></div>
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="name"
|
||||||
|
placeholder={userData.firstName}
|
||||||
|
className="col-span-3"
|
||||||
|
onChange={(event) => {
|
||||||
|
console.log(event.target.value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-4 items-center gap-4">
|
||||||
|
<Label htmlFor="username" className="text-right">
|
||||||
|
Username
|
||||||
|
</Label>
|
||||||
|
<Input id="username" value="@peduarte" className="col-span-3" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<DialogFooter>
|
<SheetFooter>
|
||||||
<Button variant={"secondary"} className={"gap-2 px-2"}>
|
<SheetClose asChild>
|
||||||
<Wallet />
|
<Button type="submit">Save changes</Button>
|
||||||
<p>My wallet</p>
|
</SheetClose>
|
||||||
</Button>
|
</SheetFooter>
|
||||||
<Button variant={"destructive"} className={"gap-2 px-2"}>
|
</SheetContent>
|
||||||
<Unplug />
|
</Sheet>
|
||||||
<p>Disconnect</p>
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,23 +0,0 @@
|
|||||||
import { FormLabel } from "@/components/ui/form";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
function AutoFormLabel({
|
|
||||||
label,
|
|
||||||
isRequired,
|
|
||||||
className,
|
|
||||||
}: {
|
|
||||||
label: string;
|
|
||||||
isRequired: boolean;
|
|
||||||
className?: string;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<FormLabel className={cn(className)}>
|
|
||||||
{label}
|
|
||||||
{isRequired && <span className="text-destructive"> *</span>}
|
|
||||||
</FormLabel>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default AutoFormLabel;
|
|
@ -1,13 +0,0 @@
|
|||||||
function AutoFormTooltip({ fieldConfigItem }: { fieldConfigItem: any }) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{fieldConfigItem?.description && (
|
|
||||||
<p className="text-sm text-gray-500 dark:text-white">
|
|
||||||
{fieldConfigItem.description}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default AutoFormTooltip;
|
|
@ -1,35 +0,0 @@
|
|||||||
import AutoFormCheckbox from "./fields/checkbox";
|
|
||||||
import AutoFormDate from "./fields/date";
|
|
||||||
import AutoFormEnum from "./fields/enum";
|
|
||||||
import AutoFormFile from "./fields/file";
|
|
||||||
import AutoFormInput from "./fields/input";
|
|
||||||
import AutoFormNumber from "./fields/number";
|
|
||||||
import AutoFormRadioGroup from "./fields/radio-group";
|
|
||||||
import AutoFormSwitch from "./fields/switch";
|
|
||||||
import AutoFormTextarea from "./fields/textarea";
|
|
||||||
|
|
||||||
export const INPUT_COMPONENTS = {
|
|
||||||
checkbox: AutoFormCheckbox,
|
|
||||||
date: AutoFormDate,
|
|
||||||
select: AutoFormEnum,
|
|
||||||
radio: AutoFormRadioGroup,
|
|
||||||
switch: AutoFormSwitch,
|
|
||||||
textarea: AutoFormTextarea,
|
|
||||||
number: AutoFormNumber,
|
|
||||||
file: AutoFormFile,
|
|
||||||
fallback: AutoFormInput,
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Define handlers for specific Zod types.
|
|
||||||
* You can expand this object to support more types.
|
|
||||||
*/
|
|
||||||
export const DEFAULT_ZOD_HANDLERS: {
|
|
||||||
[key: string]: keyof typeof INPUT_COMPONENTS;
|
|
||||||
} = {
|
|
||||||
ZodBoolean: "checkbox",
|
|
||||||
ZodDate: "date",
|
|
||||||
ZodEnum: "select",
|
|
||||||
ZodNativeEnum: "select",
|
|
||||||
ZodNumber: "number",
|
|
||||||
};
|
|
@ -1,57 +0,0 @@
|
|||||||
import type { FieldValues, UseFormWatch } from "react-hook-form";
|
|
||||||
import type * as z from "zod";
|
|
||||||
import { type Dependency, DependencyType, type EnumValues } from "./types";
|
|
||||||
|
|
||||||
export default function resolveDependencies<
|
|
||||||
SchemaType extends z.infer<z.ZodObject<any, any>>,
|
|
||||||
>(
|
|
||||||
dependencies: Dependency<SchemaType>[],
|
|
||||||
currentFieldName: keyof SchemaType,
|
|
||||||
watch: UseFormWatch<FieldValues>,
|
|
||||||
) {
|
|
||||||
let isDisabled = false;
|
|
||||||
let isHidden = false;
|
|
||||||
let isRequired = false;
|
|
||||||
let overrideOptions: EnumValues | undefined;
|
|
||||||
|
|
||||||
const currentFieldValue = watch(currentFieldName as string);
|
|
||||||
|
|
||||||
const currentFieldDependencies = dependencies.filter(
|
|
||||||
(dependency) => dependency.targetField === currentFieldName,
|
|
||||||
);
|
|
||||||
for (const dependency of currentFieldDependencies) {
|
|
||||||
const watchedValue = watch(dependency.sourceField as string);
|
|
||||||
|
|
||||||
const conditionMet = dependency.when(watchedValue, currentFieldValue);
|
|
||||||
|
|
||||||
switch (dependency.type) {
|
|
||||||
case DependencyType.DISABLES:
|
|
||||||
if (conditionMet) {
|
|
||||||
isDisabled = true;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case DependencyType.REQUIRES:
|
|
||||||
if (conditionMet) {
|
|
||||||
isRequired = true;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case DependencyType.HIDES:
|
|
||||||
if (conditionMet) {
|
|
||||||
isHidden = true;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case DependencyType.SETS_OPTIONS:
|
|
||||||
if (conditionMet) {
|
|
||||||
overrideOptions = dependency.options;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
isDisabled,
|
|
||||||
isHidden,
|
|
||||||
isRequired,
|
|
||||||
overrideOptions,
|
|
||||||
};
|
|
||||||
}
|
|
@ -1,91 +0,0 @@
|
|||||||
import {
|
|
||||||
AccordionContent,
|
|
||||||
AccordionItem,
|
|
||||||
AccordionTrigger,
|
|
||||||
} from "@/components/ui/accordion";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Separator } from "@/components/ui/separator";
|
|
||||||
import { Plus, Trash } from "lucide-react";
|
|
||||||
import { useFieldArray, type useForm } from "react-hook-form";
|
|
||||||
import * as z from "zod";
|
|
||||||
import { beautifyObjectName } from "../utils";
|
|
||||||
import AutoFormObject from "./object";
|
|
||||||
|
|
||||||
function isZodArray(item: z.ZodArray<any> | z.ZodDefault<any>): item is z.ZodArray<any> {
|
|
||||||
return item instanceof z.ZodArray;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isZodDefault(
|
|
||||||
item: z.ZodArray<any> | z.ZodDefault<any>,
|
|
||||||
): item is z.ZodDefault<any> {
|
|
||||||
return item instanceof z.ZodDefault;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function AutoFormArray({
|
|
||||||
name,
|
|
||||||
item,
|
|
||||||
form,
|
|
||||||
path = [],
|
|
||||||
fieldConfig,
|
|
||||||
}: {
|
|
||||||
name: string;
|
|
||||||
item: z.ZodArray<any> | z.ZodDefault<any>;
|
|
||||||
form: ReturnType<typeof useForm>;
|
|
||||||
path?: string[];
|
|
||||||
fieldConfig?: any;
|
|
||||||
}) {
|
|
||||||
const { fields, append, remove } = useFieldArray({
|
|
||||||
control: form.control,
|
|
||||||
name,
|
|
||||||
});
|
|
||||||
const title = item._def.description ?? beautifyObjectName(name);
|
|
||||||
|
|
||||||
const itemDefType = isZodArray(item)
|
|
||||||
? item._def.type
|
|
||||||
: isZodDefault(item)
|
|
||||||
? item._def.innerType._def.type
|
|
||||||
: null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AccordionItem value={name} className="border-none">
|
|
||||||
<AccordionTrigger>{title}</AccordionTrigger>
|
|
||||||
<AccordionContent>
|
|
||||||
{fields.map((_field, index) => {
|
|
||||||
const key = _field.id;
|
|
||||||
return (
|
|
||||||
<div className="mt-4 flex flex-col" key={`${key}`}>
|
|
||||||
<AutoFormObject
|
|
||||||
schema={itemDefType as z.ZodObject<any, any>}
|
|
||||||
form={form}
|
|
||||||
fieldConfig={fieldConfig}
|
|
||||||
path={[...path, index.toString()]}
|
|
||||||
/>
|
|
||||||
<div className="my-4 flex justify-end">
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
size="icon"
|
|
||||||
type="button"
|
|
||||||
className="hover:bg-zinc-300 hover:text-black focus:ring-0 focus:ring-offset-0 focus-visible:ring-0 focus-visible:ring-offset-0 dark:bg-white dark:text-black dark:hover:bg-zinc-300 dark:hover:text-black dark:hover:ring-0 dark:hover:ring-offset-0 dark:focus-visible:ring-0 dark:focus-visible:ring-offset-0"
|
|
||||||
onClick={() => remove(index)}
|
|
||||||
>
|
|
||||||
<Trash className="size-4 " />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Separator />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="secondary"
|
|
||||||
onClick={() => append({})}
|
|
||||||
className="mt-4 flex items-center"
|
|
||||||
>
|
|
||||||
<Plus className="mr-2" size={16} />
|
|
||||||
Add
|
|
||||||
</Button>
|
|
||||||
</AccordionContent>
|
|
||||||
</AccordionItem>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,34 +0,0 @@
|
|||||||
import { Checkbox } from "@/components/ui/checkbox";
|
|
||||||
import { FormControl, FormItem } from "@/components/ui/form";
|
|
||||||
import AutoFormLabel from "../common/label";
|
|
||||||
import AutoFormTooltip from "../common/tooltip";
|
|
||||||
import type { AutoFormInputComponentProps } from "../types";
|
|
||||||
|
|
||||||
export default function AutoFormCheckbox({
|
|
||||||
label,
|
|
||||||
isRequired,
|
|
||||||
field,
|
|
||||||
fieldConfigItem,
|
|
||||||
fieldProps,
|
|
||||||
}: AutoFormInputComponentProps) {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<FormItem>
|
|
||||||
<div className="mb-3 flex items-center gap-3">
|
|
||||||
<FormControl>
|
|
||||||
<Checkbox
|
|
||||||
checked={field.value}
|
|
||||||
onCheckedChange={field.onChange}
|
|
||||||
{...fieldProps}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<AutoFormLabel
|
|
||||||
label={fieldConfigItem?.label || label}
|
|
||||||
isRequired={isRequired}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</FormItem>
|
|
||||||
<AutoFormTooltip fieldConfigItem={fieldConfigItem} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,25 +0,0 @@
|
|||||||
import { DatePicker } from "@/components/ui/date-picker";
|
|
||||||
import { FormControl, FormItem, FormMessage } from "@/components/ui/form";
|
|
||||||
import AutoFormLabel from "../common/label";
|
|
||||||
import AutoFormTooltip from "../common/tooltip";
|
|
||||||
import type { AutoFormInputComponentProps } from "../types";
|
|
||||||
|
|
||||||
export default function AutoFormDate({
|
|
||||||
label,
|
|
||||||
isRequired,
|
|
||||||
field,
|
|
||||||
fieldConfigItem,
|
|
||||||
fieldProps,
|
|
||||||
}: AutoFormInputComponentProps) {
|
|
||||||
return (
|
|
||||||
<FormItem>
|
|
||||||
<AutoFormLabel label={fieldConfigItem?.label || label} isRequired={isRequired} />
|
|
||||||
<FormControl>
|
|
||||||
<DatePicker date={field.value} setDate={field.onChange} {...fieldProps} />
|
|
||||||
</FormControl>
|
|
||||||
<AutoFormTooltip fieldConfigItem={fieldConfigItem} />
|
|
||||||
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,59 +0,0 @@
|
|||||||
import { FormControl, FormItem, FormMessage } from "@/components/ui/form";
|
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from "@/components/ui/select";
|
|
||||||
import type * as z from "zod";
|
|
||||||
import AutoFormLabel from "../common/label";
|
|
||||||
import AutoFormTooltip from "../common/tooltip";
|
|
||||||
import type { AutoFormInputComponentProps } from "../types";
|
|
||||||
import { getBaseSchema } from "../utils";
|
|
||||||
|
|
||||||
export default function AutoFormEnum({
|
|
||||||
label,
|
|
||||||
isRequired,
|
|
||||||
field,
|
|
||||||
fieldConfigItem,
|
|
||||||
zodItem,
|
|
||||||
fieldProps,
|
|
||||||
}: AutoFormInputComponentProps) {
|
|
||||||
const baseValues = (getBaseSchema(zodItem) as unknown as z.ZodEnum<any>)._def.values;
|
|
||||||
|
|
||||||
let values: [string, string][] = [];
|
|
||||||
if (!Array.isArray(baseValues)) {
|
|
||||||
values = Object.entries(baseValues);
|
|
||||||
} else {
|
|
||||||
values = baseValues.map((value) => [value, value]);
|
|
||||||
}
|
|
||||||
|
|
||||||
function findItem(value: any) {
|
|
||||||
return values.find((item) => item[0] === value);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FormItem>
|
|
||||||
<AutoFormLabel label={fieldConfigItem?.label || label} isRequired={isRequired} />
|
|
||||||
<FormControl>
|
|
||||||
<Select onValueChange={field.onChange} defaultValue={field.value} {...fieldProps}>
|
|
||||||
<SelectTrigger className={fieldProps.className}>
|
|
||||||
<SelectValue placeholder={fieldConfigItem.inputProps?.placeholder}>
|
|
||||||
{field.value ? findItem(field.value)?.[1] : "Select an option"}
|
|
||||||
</SelectValue>
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{values.map(([value, label]) => (
|
|
||||||
<SelectItem value={label} key={value}>
|
|
||||||
{label}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
<AutoFormTooltip fieldConfigItem={fieldConfigItem} />
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,64 +0,0 @@
|
|||||||
import { FormControl, FormItem, FormMessage } from "@/components/ui/form";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { Trash2 } from "lucide-react";
|
|
||||||
import { type ChangeEvent, useState } from "react";
|
|
||||||
import AutoFormLabel from "../common/label";
|
|
||||||
import AutoFormTooltip from "../common/tooltip";
|
|
||||||
import type { AutoFormInputComponentProps } from "../types";
|
|
||||||
export default function AutoFormFile({
|
|
||||||
label,
|
|
||||||
isRequired,
|
|
||||||
fieldConfigItem,
|
|
||||||
fieldProps,
|
|
||||||
field,
|
|
||||||
}: AutoFormInputComponentProps) {
|
|
||||||
const { showLabel: _showLabel, ...fieldPropsWithoutShowLabel } = fieldProps;
|
|
||||||
const showLabel = _showLabel === undefined ? true : _showLabel;
|
|
||||||
const [file, setFile] = useState<string | null>(null);
|
|
||||||
const [fileName, setFileName] = useState<string | null>(null);
|
|
||||||
const handleFileChange = (e: ChangeEvent<HTMLInputElement>) => {
|
|
||||||
const file = e.target.files?.[0];
|
|
||||||
|
|
||||||
if (file) {
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onloadend = () => {
|
|
||||||
setFile(reader.result as string);
|
|
||||||
setFileName(file.name);
|
|
||||||
field.onChange(reader.result as string);
|
|
||||||
};
|
|
||||||
reader.readAsDataURL(file);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRemoveClick = () => {
|
|
||||||
setFile(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FormItem>
|
|
||||||
{showLabel && (
|
|
||||||
<AutoFormLabel label={fieldConfigItem?.label || label} isRequired={isRequired} />
|
|
||||||
)}
|
|
||||||
{!file && (
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
type="file"
|
|
||||||
{...fieldPropsWithoutShowLabel}
|
|
||||||
onChange={handleFileChange}
|
|
||||||
value={""}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
{file && (
|
|
||||||
<div className="flex h-[40px] w-full flex-row items-center justify-between space-x-2 rounded-sm border p-2 text-black focus-visible:ring-0 focus-visible:ring-offset-0 dark:bg-white dark:text-black dark:focus-visible:ring-0 dark:focus-visible:ring-offset-0">
|
|
||||||
<p>{fileName}</p>
|
|
||||||
<button onClick={handleRemoveClick} aria-label="Remove image">
|
|
||||||
<Trash2 size={16} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<AutoFormTooltip fieldConfigItem={fieldConfigItem} />
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,34 +0,0 @@
|
|||||||
import { FormControl, FormItem, FormMessage } from "@/components/ui/form";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import AutoFormLabel from "../common/label";
|
|
||||||
import AutoFormTooltip from "../common/tooltip";
|
|
||||||
import type { AutoFormInputComponentProps } from "../types";
|
|
||||||
|
|
||||||
export default function AutoFormInput({
|
|
||||||
label,
|
|
||||||
isRequired,
|
|
||||||
fieldConfigItem,
|
|
||||||
fieldProps,
|
|
||||||
}: AutoFormInputComponentProps) {
|
|
||||||
const { showLabel: _showLabel, ...fieldPropsWithoutShowLabel } = fieldProps;
|
|
||||||
const showLabel = _showLabel === undefined ? true : _showLabel;
|
|
||||||
const type = fieldProps.type || "text";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-row items-center space-x-2">
|
|
||||||
<FormItem className="flex w-full flex-col justify-start">
|
|
||||||
{showLabel && (
|
|
||||||
<AutoFormLabel
|
|
||||||
label={fieldConfigItem?.label || label}
|
|
||||||
isRequired={isRequired}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<FormControl>
|
|
||||||
<Input type={type} {...fieldPropsWithoutShowLabel} />
|
|
||||||
</FormControl>
|
|
||||||
<AutoFormTooltip fieldConfigItem={fieldConfigItem} />
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,28 +0,0 @@
|
|||||||
import { FormControl, FormItem, FormMessage } from "@/components/ui/form";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import AutoFormLabel from "../common/label";
|
|
||||||
import AutoFormTooltip from "../common/tooltip";
|
|
||||||
import type { AutoFormInputComponentProps } from "../types";
|
|
||||||
|
|
||||||
export default function AutoFormNumber({
|
|
||||||
label,
|
|
||||||
isRequired,
|
|
||||||
fieldConfigItem,
|
|
||||||
fieldProps,
|
|
||||||
}: AutoFormInputComponentProps) {
|
|
||||||
const { showLabel: _showLabel, ...fieldPropsWithoutShowLabel } = fieldProps;
|
|
||||||
const showLabel = _showLabel === undefined ? true : _showLabel;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FormItem>
|
|
||||||
{showLabel && (
|
|
||||||
<AutoFormLabel label={fieldConfigItem?.label || label} isRequired={isRequired} />
|
|
||||||
)}
|
|
||||||
<FormControl>
|
|
||||||
<Input type="number" {...fieldPropsWithoutShowLabel} />
|
|
||||||
</FormControl>
|
|
||||||
<AutoFormTooltip fieldConfigItem={fieldConfigItem} />
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,175 +0,0 @@
|
|||||||
import {
|
|
||||||
Accordion,
|
|
||||||
AccordionContent,
|
|
||||||
AccordionItem,
|
|
||||||
AccordionTrigger,
|
|
||||||
} from "@/components/ui/accordion";
|
|
||||||
import { FormField } from "@/components/ui/form";
|
|
||||||
import { type useForm, useFormContext } from "react-hook-form";
|
|
||||||
import * as z from "zod";
|
|
||||||
import { DEFAULT_ZOD_HANDLERS, INPUT_COMPONENTS } from "../config";
|
|
||||||
import resolveDependencies from "../dependencies";
|
|
||||||
import type { Dependency, FieldConfig, FieldConfigItem } from "../types";
|
|
||||||
import {
|
|
||||||
beautifyObjectName,
|
|
||||||
getBaseSchema,
|
|
||||||
getBaseType,
|
|
||||||
zodToHtmlInputProps,
|
|
||||||
} from "../utils";
|
|
||||||
import AutoFormArray from "./array";
|
|
||||||
|
|
||||||
function DefaultParent({ children }: { children: React.ReactNode }) {
|
|
||||||
return <>{children}</>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function AutoFormObject<SchemaType extends z.ZodObject<any, any>>({
|
|
||||||
schema,
|
|
||||||
form,
|
|
||||||
fieldConfig,
|
|
||||||
path = [],
|
|
||||||
dependencies = [],
|
|
||||||
}: {
|
|
||||||
schema: SchemaType | z.ZodEffects<SchemaType>;
|
|
||||||
form: ReturnType<typeof useForm>;
|
|
||||||
fieldConfig?: FieldConfig<z.infer<SchemaType>>;
|
|
||||||
path?: string[];
|
|
||||||
dependencies?: Dependency<z.infer<SchemaType>>[];
|
|
||||||
}) {
|
|
||||||
const { watch } = useFormContext(); // Use useFormContext to access the watch function
|
|
||||||
|
|
||||||
if (!schema) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const { shape } = getBaseSchema<SchemaType>(schema) || {};
|
|
||||||
|
|
||||||
if (!shape) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleIfZodNumber = (item: z.ZodAny) => {
|
|
||||||
const isZodNumber = (item as any)._def.typeName === "ZodNumber";
|
|
||||||
const isInnerZodNumber = (item._def as any).innerType?._def?.typeName === "ZodNumber";
|
|
||||||
|
|
||||||
if (isZodNumber) {
|
|
||||||
(item as any)._def.coerce = true;
|
|
||||||
} else if (isInnerZodNumber) {
|
|
||||||
(item._def as any).innerType._def.coerce = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return item;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Accordion type="multiple" className="space-y-5 border-none">
|
|
||||||
{Object.keys(shape).map((name) => {
|
|
||||||
let item = shape[name] as z.ZodAny;
|
|
||||||
item = handleIfZodNumber(item) as z.ZodAny;
|
|
||||||
const zodBaseType = getBaseType(item);
|
|
||||||
const itemName = item._def.description ?? beautifyObjectName(name);
|
|
||||||
const key = [...path, name].join(".");
|
|
||||||
|
|
||||||
const {
|
|
||||||
isHidden,
|
|
||||||
isDisabled,
|
|
||||||
isRequired: isRequiredByDependency,
|
|
||||||
overrideOptions,
|
|
||||||
} = resolveDependencies(dependencies, name, watch);
|
|
||||||
if (isHidden) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (zodBaseType === "ZodObject") {
|
|
||||||
return (
|
|
||||||
<AccordionItem value={name} key={key} className="border-none">
|
|
||||||
<AccordionTrigger>{itemName}</AccordionTrigger>
|
|
||||||
<AccordionContent className="p-2">
|
|
||||||
<AutoFormObject
|
|
||||||
schema={item as unknown as z.ZodObject<any, any>}
|
|
||||||
form={form}
|
|
||||||
fieldConfig={
|
|
||||||
(fieldConfig?.[name] ?? {}) as FieldConfig<z.infer<typeof item>>
|
|
||||||
}
|
|
||||||
path={[...path, name]}
|
|
||||||
/>
|
|
||||||
</AccordionContent>
|
|
||||||
</AccordionItem>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (zodBaseType === "ZodArray") {
|
|
||||||
return (
|
|
||||||
<AutoFormArray
|
|
||||||
key={key}
|
|
||||||
name={name}
|
|
||||||
item={item as unknown as z.ZodArray<any>}
|
|
||||||
form={form}
|
|
||||||
fieldConfig={fieldConfig?.[name] ?? {}}
|
|
||||||
path={[...path, name]}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const fieldConfigItem: FieldConfigItem = fieldConfig?.[name] ?? {};
|
|
||||||
const zodInputProps = zodToHtmlInputProps(item);
|
|
||||||
const isRequired =
|
|
||||||
isRequiredByDependency ||
|
|
||||||
zodInputProps.required ||
|
|
||||||
fieldConfigItem.inputProps?.required ||
|
|
||||||
false;
|
|
||||||
|
|
||||||
if (overrideOptions) {
|
|
||||||
item = z.enum(overrideOptions) as unknown as z.ZodAny;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name={key}
|
|
||||||
key={key}
|
|
||||||
render={({ field }) => {
|
|
||||||
const inputType =
|
|
||||||
fieldConfigItem.fieldType ??
|
|
||||||
DEFAULT_ZOD_HANDLERS[zodBaseType] ??
|
|
||||||
"fallback";
|
|
||||||
|
|
||||||
const InputComponent =
|
|
||||||
typeof inputType === "function" ? inputType : INPUT_COMPONENTS[inputType];
|
|
||||||
|
|
||||||
const ParentElement = fieldConfigItem.renderParent ?? DefaultParent;
|
|
||||||
|
|
||||||
const defaultValue = fieldConfigItem.inputProps?.defaultValue;
|
|
||||||
const value = field.value ?? defaultValue ?? "";
|
|
||||||
|
|
||||||
const fieldProps = {
|
|
||||||
...zodToHtmlInputProps(item),
|
|
||||||
...field,
|
|
||||||
...fieldConfigItem.inputProps,
|
|
||||||
disabled: fieldConfigItem.inputProps?.disabled || isDisabled,
|
|
||||||
ref: undefined,
|
|
||||||
value: value,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (InputComponent === undefined) {
|
|
||||||
return <></>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ParentElement key={`${key}.parent`}>
|
|
||||||
<InputComponent
|
|
||||||
zodInputProps={zodInputProps}
|
|
||||||
field={field}
|
|
||||||
fieldConfigItem={fieldConfigItem}
|
|
||||||
label={itemName}
|
|
||||||
isRequired={isRequired}
|
|
||||||
zodItem={item}
|
|
||||||
fieldProps={fieldProps}
|
|
||||||
className={fieldProps.className}
|
|
||||||
/>
|
|
||||||
</ParentElement>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Accordion>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,51 +0,0 @@
|
|||||||
import { FormControl, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
|
|
||||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
|
||||||
import type * as z from "zod";
|
|
||||||
import AutoFormLabel from "../common/label";
|
|
||||||
import AutoFormTooltip from "../common/tooltip";
|
|
||||||
import type { AutoFormInputComponentProps } from "../types";
|
|
||||||
import { getBaseSchema } from "../utils";
|
|
||||||
|
|
||||||
export default function AutoFormRadioGroup({
|
|
||||||
label,
|
|
||||||
isRequired,
|
|
||||||
field,
|
|
||||||
zodItem,
|
|
||||||
fieldProps,
|
|
||||||
fieldConfigItem,
|
|
||||||
}: AutoFormInputComponentProps) {
|
|
||||||
const baseValues = (getBaseSchema(zodItem) as unknown as z.ZodEnum<any>)._def.values;
|
|
||||||
|
|
||||||
let values: string[] = [];
|
|
||||||
if (!Array.isArray(baseValues)) {
|
|
||||||
values = Object.entries(baseValues).map((item) => item[0]);
|
|
||||||
} else {
|
|
||||||
values = baseValues;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<FormItem>
|
|
||||||
<AutoFormLabel label={fieldConfigItem?.label || label} isRequired={isRequired} />
|
|
||||||
<FormControl>
|
|
||||||
<RadioGroup
|
|
||||||
onValueChange={field.onChange}
|
|
||||||
defaultValue={field.value}
|
|
||||||
{...fieldProps}
|
|
||||||
>
|
|
||||||
{values?.map((value: any) => (
|
|
||||||
<FormItem key={value} className="mb-2 flex items-center gap-3 space-y-0">
|
|
||||||
<FormControl>
|
|
||||||
<RadioGroupItem value={value} />
|
|
||||||
</FormControl>
|
|
||||||
<FormLabel className="font-normal">{value}</FormLabel>
|
|
||||||
</FormItem>
|
|
||||||
))}
|
|
||||||
</RadioGroup>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
<AutoFormTooltip fieldConfigItem={fieldConfigItem} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,34 +0,0 @@
|
|||||||
import { FormControl, FormItem } from "@/components/ui/form";
|
|
||||||
import { Switch } from "@/components/ui/switch";
|
|
||||||
import AutoFormLabel from "../common/label";
|
|
||||||
import AutoFormTooltip from "../common/tooltip";
|
|
||||||
import type { AutoFormInputComponentProps } from "../types";
|
|
||||||
|
|
||||||
export default function AutoFormSwitch({
|
|
||||||
label,
|
|
||||||
isRequired,
|
|
||||||
field,
|
|
||||||
fieldConfigItem,
|
|
||||||
fieldProps,
|
|
||||||
}: AutoFormInputComponentProps) {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<FormItem>
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<FormControl>
|
|
||||||
<Switch
|
|
||||||
checked={field.value}
|
|
||||||
onCheckedChange={field.onChange}
|
|
||||||
{...fieldProps}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<AutoFormLabel
|
|
||||||
label={fieldConfigItem?.label || label}
|
|
||||||
isRequired={isRequired}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</FormItem>
|
|
||||||
<AutoFormTooltip fieldConfigItem={fieldConfigItem} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,27 +0,0 @@
|
|||||||
import { FormControl, FormItem, FormMessage } from "@/components/ui/form";
|
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
|
||||||
import AutoFormLabel from "../common/label";
|
|
||||||
import AutoFormTooltip from "../common/tooltip";
|
|
||||||
import type { AutoFormInputComponentProps } from "../types";
|
|
||||||
|
|
||||||
export default function AutoFormTextarea({
|
|
||||||
label,
|
|
||||||
isRequired,
|
|
||||||
fieldConfigItem,
|
|
||||||
fieldProps,
|
|
||||||
}: AutoFormInputComponentProps) {
|
|
||||||
const { showLabel: _showLabel, ...fieldPropsWithoutShowLabel } = fieldProps;
|
|
||||||
const showLabel = _showLabel === undefined ? true : _showLabel;
|
|
||||||
return (
|
|
||||||
<FormItem>
|
|
||||||
{showLabel && (
|
|
||||||
<AutoFormLabel label={fieldConfigItem?.label || label} isRequired={isRequired} />
|
|
||||||
)}
|
|
||||||
<FormControl>
|
|
||||||
<Textarea {...fieldPropsWithoutShowLabel} />
|
|
||||||
</FormControl>
|
|
||||||
<AutoFormTooltip fieldConfigItem={fieldConfigItem} />
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,117 +0,0 @@
|
|||||||
"use client";
|
|
||||||
import { Form } from "@/components/ui/form";
|
|
||||||
import type React from "react";
|
|
||||||
import { useEffect } from "react";
|
|
||||||
import { type DefaultValues, type FormState, useForm } from "react-hook-form";
|
|
||||||
import type { z } from "zod";
|
|
||||||
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
|
|
||||||
import AutoFormObject from "@/components/auto-form/fields/object";
|
|
||||||
import type { Dependency, FieldConfig } from "@/components/auto-form/types";
|
|
||||||
import {
|
|
||||||
type ZodObjectOrWrapped,
|
|
||||||
getDefaultValues,
|
|
||||||
getObjectFormSchema,
|
|
||||||
} from "@/components/auto-form/utils";
|
|
||||||
|
|
||||||
export function AutoFormSubmit({
|
|
||||||
children,
|
|
||||||
className,
|
|
||||||
disabled,
|
|
||||||
}: {
|
|
||||||
children?: React.ReactNode;
|
|
||||||
className?: string;
|
|
||||||
disabled?: boolean;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<Button type="submit" disabled={disabled} className={className}>
|
|
||||||
{children ?? "Submit"}
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function AutoForm<SchemaType extends ZodObjectOrWrapped>({
|
|
||||||
formSchema,
|
|
||||||
values: valuesProp,
|
|
||||||
onValuesChange: onValuesChangeProp,
|
|
||||||
onParsedValuesChange,
|
|
||||||
onSubmit: onSubmitProp,
|
|
||||||
fieldConfig,
|
|
||||||
children,
|
|
||||||
className,
|
|
||||||
dependencies,
|
|
||||||
}: {
|
|
||||||
formSchema: SchemaType;
|
|
||||||
values?: Partial<z.infer<SchemaType>>;
|
|
||||||
onValuesChange?: (values: Partial<z.infer<SchemaType>>) => void;
|
|
||||||
onParsedValuesChange?: (values: Partial<z.infer<SchemaType>>) => void;
|
|
||||||
onSubmit?: (values: z.infer<SchemaType>) => void;
|
|
||||||
fieldConfig?: FieldConfig<z.infer<SchemaType>>;
|
|
||||||
children?:
|
|
||||||
| React.ReactNode
|
|
||||||
| ((formState: FormState<z.infer<SchemaType>>) => React.ReactNode);
|
|
||||||
className?: string;
|
|
||||||
dependencies?: Dependency<z.infer<SchemaType>>[];
|
|
||||||
}) {
|
|
||||||
const objectFormSchema = getObjectFormSchema(formSchema);
|
|
||||||
const defaultValues: DefaultValues<z.infer<typeof objectFormSchema>> | null =
|
|
||||||
getDefaultValues(objectFormSchema, fieldConfig);
|
|
||||||
|
|
||||||
const form = useForm<z.infer<typeof objectFormSchema>>({
|
|
||||||
resolver: zodResolver(formSchema),
|
|
||||||
defaultValues: defaultValues ?? undefined,
|
|
||||||
values: valuesProp,
|
|
||||||
});
|
|
||||||
|
|
||||||
function onSubmit(values: z.infer<typeof formSchema>) {
|
|
||||||
const parsedValues = formSchema.safeParse(values);
|
|
||||||
if (parsedValues.success) {
|
|
||||||
onSubmitProp?.(parsedValues.data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const values = form.watch();
|
|
||||||
// valuesString is needed because form.watch() returns a new object every time
|
|
||||||
const valuesString = JSON.stringify(values);
|
|
||||||
|
|
||||||
// biome-ignore lint/correctness/useExhaustiveDependencies: <explanation>
|
|
||||||
useEffect(() => {
|
|
||||||
onValuesChangeProp?.(values);
|
|
||||||
const parsedValues = formSchema.safeParse(values);
|
|
||||||
if (parsedValues.success) {
|
|
||||||
onParsedValuesChange?.(parsedValues.data);
|
|
||||||
}
|
|
||||||
}, [valuesString]);
|
|
||||||
|
|
||||||
const renderChildren =
|
|
||||||
typeof children === "function"
|
|
||||||
? children(form.formState as FormState<z.infer<SchemaType>>)
|
|
||||||
: children;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="w-full">
|
|
||||||
<Form {...form}>
|
|
||||||
<form
|
|
||||||
onSubmit={(e) => {
|
|
||||||
form.handleSubmit(onSubmit)(e);
|
|
||||||
}}
|
|
||||||
className={cn("space-y-5", className)}
|
|
||||||
>
|
|
||||||
<AutoFormObject
|
|
||||||
schema={objectFormSchema}
|
|
||||||
form={form}
|
|
||||||
dependencies={dependencies}
|
|
||||||
fieldConfig={fieldConfig}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{renderChildren}
|
|
||||||
</form>
|
|
||||||
</Form>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default AutoForm;
|
|
@ -1,71 +0,0 @@
|
|||||||
import type React from "react";
|
|
||||||
import type { ControllerRenderProps, FieldValues } from "react-hook-form";
|
|
||||||
import type * as z from "zod";
|
|
||||||
import type { INPUT_COMPONENTS } from "./config";
|
|
||||||
|
|
||||||
export type FieldConfigItem = {
|
|
||||||
description?: React.ReactNode;
|
|
||||||
inputProps?: React.InputHTMLAttributes<HTMLInputElement> & {
|
|
||||||
showLabel?: boolean;
|
|
||||||
};
|
|
||||||
label?: string;
|
|
||||||
fieldType?: keyof typeof INPUT_COMPONENTS | React.FC<AutoFormInputComponentProps>;
|
|
||||||
|
|
||||||
renderParent?: (props: {
|
|
||||||
children: React.ReactNode;
|
|
||||||
}) => React.ReactElement | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type FieldConfig<SchemaType extends z.infer<z.ZodObject<any, any>>> = {
|
|
||||||
// If SchemaType.key is an object, create a nested FieldConfig, otherwise FieldConfigItem
|
|
||||||
[Key in keyof SchemaType]?: SchemaType[Key] extends object
|
|
||||||
? FieldConfig<z.infer<SchemaType[Key]>>
|
|
||||||
: FieldConfigItem;
|
|
||||||
};
|
|
||||||
|
|
||||||
export enum DependencyType {
|
|
||||||
DISABLES = 0,
|
|
||||||
REQUIRES = 1,
|
|
||||||
HIDES = 2,
|
|
||||||
SETS_OPTIONS = 3,
|
|
||||||
}
|
|
||||||
|
|
||||||
type BaseDependency<SchemaType extends z.infer<z.ZodObject<any, any>>> = {
|
|
||||||
sourceField: keyof SchemaType;
|
|
||||||
type: DependencyType;
|
|
||||||
targetField: keyof SchemaType;
|
|
||||||
when: (sourceFieldValue: any, targetFieldValue: any) => boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ValueDependency<SchemaType extends z.infer<z.ZodObject<any, any>>> =
|
|
||||||
BaseDependency<SchemaType> & {
|
|
||||||
type: DependencyType.DISABLES | DependencyType.REQUIRES | DependencyType.HIDES;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type EnumValues = readonly [string, ...string[]];
|
|
||||||
|
|
||||||
export type OptionsDependency<SchemaType extends z.infer<z.ZodObject<any, any>>> =
|
|
||||||
BaseDependency<SchemaType> & {
|
|
||||||
type: DependencyType.SETS_OPTIONS;
|
|
||||||
|
|
||||||
// Partial array of values from sourceField that will trigger the dependency
|
|
||||||
options: EnumValues;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type Dependency<SchemaType extends z.infer<z.ZodObject<any, any>>> =
|
|
||||||
| ValueDependency<SchemaType>
|
|
||||||
| OptionsDependency<SchemaType>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A FormInput component can handle a specific Zod type (e.g. "ZodBoolean")
|
|
||||||
*/
|
|
||||||
export type AutoFormInputComponentProps = {
|
|
||||||
zodInputProps: React.InputHTMLAttributes<HTMLInputElement>;
|
|
||||||
field: ControllerRenderProps<FieldValues, any>;
|
|
||||||
fieldConfigItem: FieldConfigItem;
|
|
||||||
label: string;
|
|
||||||
isRequired: boolean;
|
|
||||||
fieldProps: any;
|
|
||||||
zodItem: z.ZodAny;
|
|
||||||
className?: string;
|
|
||||||
};
|
|
@ -1,167 +0,0 @@
|
|||||||
import type React from "react";
|
|
||||||
import type { DefaultValues } from "react-hook-form";
|
|
||||||
import type { z } from "zod";
|
|
||||||
import type { FieldConfig } from "./types";
|
|
||||||
|
|
||||||
// TODO: This should support recursive ZodEffects but TypeScript doesn't allow circular type definitions.
|
|
||||||
export type ZodObjectOrWrapped =
|
|
||||||
| z.ZodObject<any, any>
|
|
||||||
| z.ZodEffects<z.ZodObject<any, any>>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Beautify a camelCase string.
|
|
||||||
* e.g. "myString" -> "My String"
|
|
||||||
*/
|
|
||||||
export function beautifyObjectName(string: string) {
|
|
||||||
// if numbers only return the string
|
|
||||||
let output = string.replace(/([A-Z])/g, " $1");
|
|
||||||
output = output.charAt(0).toUpperCase() + output.slice(1);
|
|
||||||
return output;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the lowest level Zod type.
|
|
||||||
* This will unpack optionals, refinements, etc.
|
|
||||||
*/
|
|
||||||
export function getBaseSchema<ChildType extends z.ZodAny | z.AnyZodObject = z.ZodAny>(
|
|
||||||
schema: ChildType | z.ZodEffects<ChildType>,
|
|
||||||
): ChildType | null {
|
|
||||||
if (!schema) return null;
|
|
||||||
if ("innerType" in schema._def) {
|
|
||||||
return getBaseSchema(schema._def.innerType as ChildType);
|
|
||||||
}
|
|
||||||
if ("schema" in schema._def) {
|
|
||||||
return getBaseSchema(schema._def.schema as ChildType);
|
|
||||||
}
|
|
||||||
|
|
||||||
return schema as ChildType;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the type name of the lowest level Zod type.
|
|
||||||
* This will unpack optionals, refinements, etc.
|
|
||||||
*/
|
|
||||||
export function getBaseType(schema: z.ZodAny): string {
|
|
||||||
const baseSchema = getBaseSchema(schema);
|
|
||||||
return baseSchema ? baseSchema._def.typeName : "";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Search for a "ZodDefult" in the Zod stack and return its value.
|
|
||||||
*/
|
|
||||||
export function getDefaultValueInZodStack(schema: z.ZodAny): any {
|
|
||||||
const typedSchema = schema as unknown as z.ZodDefault<z.ZodNumber | z.ZodString>;
|
|
||||||
|
|
||||||
if (typedSchema._def.typeName === "ZodDefault") {
|
|
||||||
return typedSchema._def.defaultValue();
|
|
||||||
}
|
|
||||||
|
|
||||||
if ("innerType" in typedSchema._def) {
|
|
||||||
return getDefaultValueInZodStack(typedSchema._def.innerType as unknown as z.ZodAny);
|
|
||||||
}
|
|
||||||
if ("schema" in typedSchema._def) {
|
|
||||||
return getDefaultValueInZodStack((typedSchema._def as any).schema as z.ZodAny);
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all default values from a Zod schema.
|
|
||||||
*/
|
|
||||||
export function getDefaultValues<Schema extends z.ZodObject<any, any>>(
|
|
||||||
schema: Schema,
|
|
||||||
fieldConfig?: FieldConfig<z.infer<Schema>>,
|
|
||||||
) {
|
|
||||||
if (!schema) return null;
|
|
||||||
const { shape } = schema;
|
|
||||||
type DefaultValuesType = DefaultValues<Partial<z.infer<Schema>>>;
|
|
||||||
const defaultValues = {} as DefaultValuesType;
|
|
||||||
if (!shape) return defaultValues;
|
|
||||||
|
|
||||||
for (const key of Object.keys(shape)) {
|
|
||||||
const item = shape[key] as z.ZodAny;
|
|
||||||
|
|
||||||
if (getBaseType(item) === "ZodObject") {
|
|
||||||
const defaultItems = getDefaultValues(
|
|
||||||
getBaseSchema(item) as unknown as z.ZodObject<any, any>,
|
|
||||||
fieldConfig?.[key] as FieldConfig<z.infer<Schema>>,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (defaultItems !== null) {
|
|
||||||
for (const defaultItemKey of Object.keys(defaultItems)) {
|
|
||||||
const pathKey = `${key}.${defaultItemKey}` as keyof DefaultValuesType;
|
|
||||||
defaultValues[pathKey] = defaultItems[defaultItemKey];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let defaultValue = getDefaultValueInZodStack(item);
|
|
||||||
if (
|
|
||||||
(defaultValue === null || defaultValue === "") &&
|
|
||||||
fieldConfig?.[key]?.inputProps
|
|
||||||
) {
|
|
||||||
defaultValue = (fieldConfig?.[key]?.inputProps as unknown as any).defaultValue;
|
|
||||||
}
|
|
||||||
if (defaultValue !== undefined) {
|
|
||||||
defaultValues[key as keyof DefaultValuesType] = defaultValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return defaultValues;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getObjectFormSchema(schema: ZodObjectOrWrapped): z.ZodObject<any, any> {
|
|
||||||
if (schema?._def.typeName === "ZodEffects") {
|
|
||||||
const typedSchema = schema as z.ZodEffects<z.ZodObject<any, any>>;
|
|
||||||
return getObjectFormSchema(typedSchema._def.schema);
|
|
||||||
}
|
|
||||||
return schema as z.ZodObject<any, any>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert a Zod schema to HTML input props to give direct feedback to the user.
|
|
||||||
* Once submitted, the schema will be validated completely.
|
|
||||||
*/
|
|
||||||
export function zodToHtmlInputProps(
|
|
||||||
schema: z.ZodNumber | z.ZodString | z.ZodOptional<z.ZodNumber | z.ZodString> | any,
|
|
||||||
): React.InputHTMLAttributes<HTMLInputElement> {
|
|
||||||
if (["ZodOptional", "ZodNullable"].includes(schema._def.typeName)) {
|
|
||||||
const typedSchema = schema as z.ZodOptional<z.ZodNumber | z.ZodString>;
|
|
||||||
return {
|
|
||||||
...zodToHtmlInputProps(typedSchema._def.innerType),
|
|
||||||
required: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
const typedSchema = schema as z.ZodNumber | z.ZodString;
|
|
||||||
|
|
||||||
if (!("checks" in typedSchema._def))
|
|
||||||
return {
|
|
||||||
required: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
const { checks } = typedSchema._def;
|
|
||||||
const inputProps: React.InputHTMLAttributes<HTMLInputElement> = {
|
|
||||||
required: true,
|
|
||||||
};
|
|
||||||
const type = getBaseType(schema);
|
|
||||||
|
|
||||||
for (const check of checks) {
|
|
||||||
if (check.kind === "min") {
|
|
||||||
if (type === "ZodString") {
|
|
||||||
inputProps.minLength = check.value;
|
|
||||||
} else {
|
|
||||||
inputProps.min = check.value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (check.kind === "max") {
|
|
||||||
if (type === "ZodString") {
|
|
||||||
inputProps.maxLength = check.value;
|
|
||||||
} else {
|
|
||||||
inputProps.max = check.value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return inputProps;
|
|
||||||
}
|
|
@ -10,7 +10,7 @@ export function Header({
|
|||||||
return (
|
return (
|
||||||
<header
|
<header
|
||||||
className={
|
className={
|
||||||
"flex flex-col md:flex-row justify-between items-center w-full p-1 md:px-3 md:py-2 pb-2 border-b-2"
|
"flex flex-col md:flex-row justify-between items-center w-full p-1 md:px-3 md:py-2 border-b-2"
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@ -24,9 +24,7 @@ export function Header({
|
|||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
<div className={"w-1/3 flex flex-row justify-center items-center"}>{children}</div>
|
<div className={"w-1/3 flex flex-row justify-center items-center"}>{children}</div>
|
||||||
<div
|
<div className={"w-1/3 flex flex-row justify-end gap-2 items-center"}>
|
||||||
className={"w-1/3 flex flex-row justify-center md:justify-end gap-2 items-center"}
|
|
||||||
>
|
|
||||||
<AccountDialog />
|
<AccountDialog />
|
||||||
<ThemeBtnSelector />
|
<ThemeBtnSelector />
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,37 +0,0 @@
|
|||||||
"use client";
|
|
||||||
import { format } from "date-fns";
|
|
||||||
import { Calendar as CalendarIcon } from "lucide-react";
|
|
||||||
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Calendar } from "@/components/ui/calendar";
|
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import { forwardRef } from "react";
|
|
||||||
|
|
||||||
export const DatePicker = forwardRef<
|
|
||||||
HTMLDivElement,
|
|
||||||
{
|
|
||||||
date?: Date;
|
|
||||||
setDate: (date?: Date) => void;
|
|
||||||
}
|
|
||||||
>(function DatePickerCmp({ date, setDate }, ref) {
|
|
||||||
return (
|
|
||||||
<Popover>
|
|
||||||
<PopoverTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant={"outline"}
|
|
||||||
className={cn(
|
|
||||||
"w-full justify-start text-left font-normal",
|
|
||||||
!date && "text-muted-foreground",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<CalendarIcon className="mr-2 h-4 w-4" />
|
|
||||||
{date ? format(date, "PPP") : <span>Pick a date</span>}
|
|
||||||
</Button>
|
|
||||||
</PopoverTrigger>
|
|
||||||
<PopoverContent className="w-auto p-0" ref={ref}>
|
|
||||||
<Calendar mode="single" selected={date} onSelect={setDate} initialFocus />
|
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
);
|
|
||||||
});
|
|
@ -1,4 +1,3 @@
|
|||||||
import { ITrade, type IUserWalletCryptos } from "@/interfaces/crypto.interface";
|
|
||||||
import type { IUserData } from "@/interfaces/userdata.interface";
|
import type { IUserData } from "@/interfaces/userdata.interface";
|
||||||
|
|
||||||
// ----- Request -----
|
// ----- Request -----
|
||||||
@ -34,13 +33,3 @@ export interface IApiRegisterRes extends IAbstractApiResponse {
|
|||||||
export interface IApiLoginRes extends IAbstractApiResponse {
|
export interface IApiLoginRes extends IAbstractApiResponse {
|
||||||
access_token?: string;
|
access_token?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IApiUserAssetsRes extends IAbstractApiResponse {
|
|
||||||
firstName?: string;
|
|
||||||
lastName?: string;
|
|
||||||
dollarAvailables?: number;
|
|
||||||
pseudo?: string;
|
|
||||||
UserHasCrypto?: IUserWalletCryptos[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IApiAllTrades extends IAbstractApiResponse {}
|
|
||||||
|
@ -1,45 +0,0 @@
|
|||||||
export interface IUserWalletCryptos {
|
|
||||||
Crypto?: ICryptoInWalletInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ICryptoInWalletInfo {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
value: number;
|
|
||||||
image: string;
|
|
||||||
quantity: number;
|
|
||||||
created_at: string;
|
|
||||||
updated_at: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type IAllTrades = ITrade[];
|
|
||||||
|
|
||||||
export interface ITrade {
|
|
||||||
Giver: ISellerIdentity;
|
|
||||||
Receiver: IBuyerIdentity;
|
|
||||||
Crypto: ITradedCrypto;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ISellerIdentity {
|
|
||||||
firstName: string;
|
|
||||||
lastName: string;
|
|
||||||
pseudo: string;
|
|
||||||
dollarAvailables: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IBuyerIdentity {
|
|
||||||
firstName: string;
|
|
||||||
lastName: string;
|
|
||||||
pseudo: string;
|
|
||||||
dollarAvailables: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ITradedCrypto {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
value: number;
|
|
||||||
image: string;
|
|
||||||
quantity: number;
|
|
||||||
created_at: string;
|
|
||||||
updated_at: string;
|
|
||||||
}
|
|
Loading…
x
Reference in New Issue
Block a user