uploads yay!
All checks were successful
Deploy to Cloudflare Pages / deploy (push) Successful in 37s

This commit is contained in:
christian 2024-11-16 22:52:33 +01:00
parent 46c419f310
commit 167a23a000
5 changed files with 288 additions and 7 deletions

176
app/components/ui/form.tsx Normal file
View File

@ -0,0 +1,176 @@
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { Slot } from "@radix-ui/react-slot"
import {
Controller,
ControllerProps,
FieldPath,
FieldValues,
FormProvider,
useFormContext,
} from "react-hook-form"
import { cn } from "~/lib/utils"
import { Label } from "~/components/ui/label"
const Form = FormProvider
type FormFieldContextValue<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
> = {
name: TName
}
const FormFieldContext = React.createContext<FormFieldContextValue>(
{} as FormFieldContextValue
)
const FormField = <
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
>({
...props
}: ControllerProps<TFieldValues, TName>) => {
return (
<FormFieldContext.Provider value={{ name: props.name }}>
<Controller {...props} />
</FormFieldContext.Provider>
)
}
const useFormField = () => {
const fieldContext = React.useContext(FormFieldContext)
const itemContext = React.useContext(FormItemContext)
const { getFieldState, formState } = useFormContext()
const fieldState = getFieldState(fieldContext.name, formState)
if (!fieldContext) {
throw new Error("useFormField should be used within <FormField>")
}
const { id } = itemContext
return {
id,
name: fieldContext.name,
formItemId: `${id}-form-item`,
formDescriptionId: `${id}-form-item-description`,
formMessageId: `${id}-form-item-message`,
...fieldState,
}
}
type FormItemContextValue = {
id: string
}
const FormItemContext = React.createContext<FormItemContextValue>(
{} as FormItemContextValue
)
const FormItem = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
const id = React.useId()
return (
<FormItemContext.Provider value={{ id }}>
<div ref={ref} className={cn("space-y-2", className)} {...props} />
</FormItemContext.Provider>
)
})
FormItem.displayName = "FormItem"
const FormLabel = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
>(({ className, ...props }, ref) => {
const { error, formItemId } = useFormField()
return (
<Label
ref={ref}
className={cn(error && "text-destructive", className)}
htmlFor={formItemId}
{...props}
/>
)
})
FormLabel.displayName = "FormLabel"
const FormControl = React.forwardRef<
React.ElementRef<typeof Slot>,
React.ComponentPropsWithoutRef<typeof Slot>
>(({ ...props }, ref) => {
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
return (
<Slot
ref={ref}
id={formItemId}
aria-describedby={
!error
? `${formDescriptionId}`
: `${formDescriptionId} ${formMessageId}`
}
aria-invalid={!!error}
{...props}
/>
)
})
FormControl.displayName = "FormControl"
const FormDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => {
const { formDescriptionId } = useFormField()
return (
<p
ref={ref}
id={formDescriptionId}
className={cn("text-[0.8rem] text-muted-foreground", className)}
{...props}
/>
)
})
FormDescription.displayName = "FormDescription"
const FormMessage = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, children, ...props }, ref) => {
const { error, formMessageId } = useFormField()
const body = error ? String(error?.message) : children
if (!body) {
return null
}
return (
<p
ref={ref}
id={formMessageId}
className={cn("text-[0.8rem] font-medium text-destructive", className)}
{...props}
>
{body}
</p>
)
})
FormMessage.displayName = "FormMessage"
export {
useFormField,
Form,
FormItem,
FormLabel,
FormControl,
FormDescription,
FormMessage,
FormField,
}

View File

@ -10,9 +10,18 @@ type ContainerDimensions = {
width: number; width: number;
}; };
type FileDimensions = {
x: number;
y: number;
};
type UnitType = "pixels" | "percentage"; type UnitType = "pixels" | "percentage";
export default function CropSelector() { interface ComponentProps {
image?: string;
}
export default function CropSelector({ image }: ComponentProps) {
const [firstDragable, setFirstDragable] = useState<Coordinates>({ const [firstDragable, setFirstDragable] = useState<Coordinates>({
x: 0, x: 0,
y: 0, y: 0,
@ -149,8 +158,11 @@ export default function CropSelector() {
}; };
return ( return (
<div className="flex flex-col h-screen w-screen justify-center items-center gap-4"> <div className="flex flex-col w-full p-4 h-fit justify-center items-center gap-4">
<div ref={containerRef} className="relative w-1/3 h-1/3 border-black border"> <div
ref={containerRef}
className="relative w-auto h-fit min-w-40 min-h-40 border-black border"
>
<DragableBox <DragableBox
position={selectorPosition} position={selectorPosition}
mode="box" mode="box"
@ -163,6 +175,7 @@ export default function CropSelector() {
<DragableBox position={firstDragable} coordSetter={setFirstDragable} mode="handle"> <DragableBox position={firstDragable} coordSetter={setFirstDragable} mode="handle">
<div className="bg-red-600 h-2 w-2 rounded-full hover:scale-125" /> <div className="bg-red-600 h-2 w-2 rounded-full hover:scale-125" />
</DragableBox> </DragableBox>
<img src={image} />
<DragableBox position={secondDragable} coordSetter={setSecondDragable} mode="handle"> <DragableBox position={secondDragable} coordSetter={setSecondDragable} mode="handle">
<div className="bg-red-600 h-2 w-2 rounded-full hover:scale-125" /> <div className="bg-red-600 h-2 w-2 rounded-full hover:scale-125" />
</DragableBox> </DragableBox>

View File

@ -1,5 +1,11 @@
import { Input } from "~/components/ui/input";
import type { MetaFunction } from "@remix-run/cloudflare"; import type { MetaFunction } from "@remix-run/cloudflare";
import CropSelector from "./CropSelector"; import CropSelector from "./CropSelector";
import { Button } from "~/components/ui/button";
import { z, ZodError } from "zod";
import { ChangeEvent, Suspense, useEffect, useRef, useState } from "react";
import { useSubmit } from "@remix-run/react";
import { ActionFunctionArgs, Form, json, useFetcher } from "react-router-dom";
export const meta: MetaFunction = () => { export const meta: MetaFunction = () => {
return [ return [
@ -8,7 +14,63 @@ export const meta: MetaFunction = () => {
]; ];
}; };
const MAX_FILE_SIZE = 5 * 1024 * 1024;
const ACCEPTED_IMAGE_TYPES = [
"image/jpeg",
"image/jpg",
"image/png",
"image/webp",
"image/gif",
] as const;
export const imageFileSchema = z.object({
name: z.string(),
size: z.number().max(MAX_FILE_SIZE, "File size must be less than 5MB"),
type: z.enum(ACCEPTED_IMAGE_TYPES, {
errorMap: () => ({ message: "Only .jpg, .jpeg, .png, .webp and .gif files are accepted." }),
}),
});
export default function Index() { export default function Index() {
return ( const [image, setImage] = useState<string>();
<CropSelector />); const imageRef = useRef<HTMLImageElement | null>(null);
const handleFileChange = (e: ChangeEvent<HTMLInputElement>) => {
console.log(e.target.files);
if (!e.target.files) {
return;
} }
setImage(URL.createObjectURL(e.target.files[0]));
};
useEffect(() => {
if (imageRef.current?.complete && image) {
URL.revokeObjectURL(image);
}
});
return (
<div className="w-screen h-screen flex flex-col justify-center items-center">
{image && <img ref={imageRef} src={image} className="h-40 w-40" />}
<Input name="image-upload" type="file" onChange={handleFileChange} />
<CropSelector image={image} />
<Button>Submit</Button>
</div>
);
}
// export async function action({ request }: ActionFunctionArgs) {
// const formData = await request.formData();
// const image = formData.get("image-upload") as File;
// console.log("skibidi action!");
// try {
// const valid_image = imageFileSchema.parse(image);
// return valid_image;
// } catch (e: any) {
// if (e instanceof ZodError) {
// const errors: Record<string, string> = {};
// errors.image = e.message;
// return json({ errors });
// }
// console.log(e);
// }
// }

31
package-lock.json generated
View File

@ -6,6 +6,7 @@
"": { "": {
"name": "frontend", "name": "frontend",
"dependencies": { "dependencies": {
"@hookform/resolvers": "^3.9.1",
"@radix-ui/react-label": "^2.1.0", "@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-radio-group": "^1.2.1", "@radix-ui/react-radio-group": "^1.2.1",
"@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-slot": "^1.1.0",
@ -19,9 +20,11 @@
"lucide-react": "^0.460.0", "lucide-react": "^0.460.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-hook-form": "^7.53.2",
"remix-utils": "^7.7.0", "remix-utils": "^7.7.0",
"tailwind-merge": "^2.5.4", "tailwind-merge": "^2.5.4",
"tailwindcss-animate": "^1.0.7" "tailwindcss-animate": "^1.0.7",
"zod": "^3.23.8"
}, },
"devDependencies": { "devDependencies": {
"@cloudflare/workers-types": "^4.20240512.0", "@cloudflare/workers-types": "^4.20240512.0",
@ -1230,6 +1233,15 @@
"node": ">=14" "node": ">=14"
} }
}, },
"node_modules/@hookform/resolvers": {
"version": "3.9.1",
"resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.9.1.tgz",
"integrity": "sha512-ud2HqmGBM0P0IABqoskKWI6PEf6ZDDBZkFqe2Vnl+mTHCEHzr3ISjjZyCwTjC/qpL25JC9aIDkloQejvMeq0ug==",
"license": "MIT",
"peerDependencies": {
"react-hook-form": "^7.0.0"
}
},
"node_modules/@humanwhocodes/config-array": { "node_modules/@humanwhocodes/config-array": {
"version": "0.13.0", "version": "0.13.0",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz",
@ -9834,6 +9846,22 @@
"react": "^18.3.1" "react": "^18.3.1"
} }
}, },
"node_modules/react-hook-form": {
"version": "7.53.2",
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.53.2.tgz",
"integrity": "sha512-YVel6fW5sOeedd1524pltpHX+jgU2u3DSDtXEaBORNdqiNrsX/nUI/iGXONegttg0mJVnfrIkiV0cmTU6Oo2xw==",
"license": "MIT",
"engines": {
"node": ">=18.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/react-hook-form"
},
"peerDependencies": {
"react": "^16.8.0 || ^17 || ^18 || ^19"
}
},
"node_modules/react-is": { "node_modules/react-is": {
"version": "16.13.1", "version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
@ -13439,7 +13467,6 @@
"version": "3.23.8", "version": "3.23.8",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz",
"integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==",
"devOptional": true,
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"url": "https://github.com/sponsors/colinhacks" "url": "https://github.com/sponsors/colinhacks"

View File

@ -13,6 +13,7 @@
"typegen": "wrangler types" "typegen": "wrangler types"
}, },
"dependencies": { "dependencies": {
"@hookform/resolvers": "^3.9.1",
"@radix-ui/react-label": "^2.1.0", "@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-radio-group": "^1.2.1", "@radix-ui/react-radio-group": "^1.2.1",
"@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-slot": "^1.1.0",
@ -26,9 +27,11 @@
"lucide-react": "^0.460.0", "lucide-react": "^0.460.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-hook-form": "^7.53.2",
"remix-utils": "^7.7.0", "remix-utils": "^7.7.0",
"tailwind-merge": "^2.5.4", "tailwind-merge": "^2.5.4",
"tailwindcss-animate": "^1.0.7" "tailwindcss-animate": "^1.0.7",
"zod": "^3.23.8"
}, },
"devDependencies": { "devDependencies": {
"@cloudflare/workers-types": "^4.20240512.0", "@cloudflare/workers-types": "^4.20240512.0",