diff --git a/package-lock.json b/package-lock.json index c10a123..a6360c4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "@radix-ui/react-dropdown-menu": "^2.1.14", "@radix-ui/react-label": "^2.1.6", "@radix-ui/react-navigation-menu": "^1.2.11", + "@radix-ui/react-select": "^2.2.5", "@radix-ui/react-slot": "^1.2.2", "@radix-ui/react-tabs": "^1.1.11", "@t3-oss/env-nextjs": "^0.12.0", @@ -34,6 +35,7 @@ "next-themes": "^0.4.6", "react": "^19.0.0", "react-dom": "^19.0.0", + "react-dropzone": "^14.3.8", "react-hook-form": "^7.56.3", "server-only": "^0.0.1", "sonner": "^2.0.3", @@ -2108,6 +2110,12 @@ "@prisma/debug": "6.7.0" } }, + "node_modules/@radix-ui/number": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", + "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==", + "license": "MIT" + }, "node_modules/@radix-ui/primitive": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.2.tgz", @@ -3041,6 +3049,270 @@ } } }, + "node_modules/@radix-ui/react-select": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.5.tgz", + "integrity": "sha512-HnMTdXEVuuyzx63ME0ut4+sEMYW6oouHWNGUZc7ddvUWIcfCva/AMoqEW/3wnEllriMWBa0RHspCYnfCWJQYmA==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.10", + "@radix-ui/react-focus-guards": "1.1.2", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.7", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-arrow": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", + "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-collection": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", + "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.10.tgz", + "integrity": "sha512-IM1zzRV4W3HtVgftdQiiOmA0AdJlCtMLe00FXaHwgt3rAnNsIyDqshvkIW3hj/iu5hu8ERP7KIYki6NkqDxAwQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", + "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-popper": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.7.tgz", + "integrity": "sha512-IUFAccz1JyKcf/RjB552PlWwxjeCJB8/4KxT7EhBHOJM+mN7LdW+B3kacJXILm32xawcMMjb2i0cIZpo+f9kiQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-rect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-portal": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-visually-hidden": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", + "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-slot": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.2.tgz", @@ -5266,6 +5538,15 @@ "node": ">= 0.4" } }, + "node_modules/attr-accept": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.5.tgz", + "integrity": "sha512-0bDNnY/u6pPwHDMoF0FieU354oBi0a8rD9FcsLwzcGWbc8KS8KPIi7y+s13OlVY+gMWc/9xEMUgNE6Qm8ZllYQ==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/available-typed-arrays": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", @@ -6947,6 +7228,18 @@ "node": ">=16.0.0" } }, + "node_modules/file-selector": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-2.1.2.tgz", + "integrity": "sha512-QgXo+mXTe8ljeqUFaX3QVHc5osSItJ/Km+xpocx0aSqWGMSCf6qYs/VnzZgS864Pjn5iceMRFigeAV7AfTlaig==", + "license": "MIT", + "dependencies": { + "tslib": "^2.7.0" + }, + "engines": { + "node": ">= 12" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -8207,7 +8500,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, "license": "MIT" }, "node_modules/js-yaml": { @@ -8643,7 +8935,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dev": true, "license": "MIT", "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" @@ -9101,7 +9392,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -9632,7 +9922,6 @@ "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "dev": true, "license": "MIT", "dependencies": { "loose-envify": "^1.4.0", @@ -9748,6 +10037,23 @@ "react": "^19.1.0" } }, + "node_modules/react-dropzone": { + "version": "14.3.8", + "resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-14.3.8.tgz", + "integrity": "sha512-sBgODnq+lcA4P296DY4wacOZz3JFpD99fp+hb//iBO2HHnyeZU3FwWyXJ6salNpqQdsZrgMrotuko/BdJMV8Ug==", + "license": "MIT", + "dependencies": { + "attr-accept": "^2.2.4", + "file-selector": "^2.1.0", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">= 10.13" + }, + "peerDependencies": { + "react": ">= 16.8 || 18.0.0" + } + }, "node_modules/react-hook-form": { "version": "7.56.3", "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.56.3.tgz", @@ -9768,7 +10074,6 @@ "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true, "license": "MIT" }, "node_modules/react-refresh": { diff --git a/package.json b/package.json index 4ac7cdd..0818db7 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "@radix-ui/react-dropdown-menu": "^2.1.14", "@radix-ui/react-label": "^2.1.6", "@radix-ui/react-navigation-menu": "^1.2.11", + "@radix-ui/react-select": "^2.2.5", "@radix-ui/react-slot": "^1.2.2", "@radix-ui/react-tabs": "^1.1.11", "@t3-oss/env-nextjs": "^0.12.0", @@ -47,6 +48,7 @@ "next-themes": "^0.4.6", "react": "^19.0.0", "react-dom": "^19.0.0", + "react-dropzone": "^14.3.8", "react-hook-form": "^7.56.3", "server-only": "^0.0.1", "sonner": "^2.0.3", diff --git a/src/app/upload/page.tsx b/src/app/upload/page.tsx new file mode 100644 index 0000000..319bdde --- /dev/null +++ b/src/app/upload/page.tsx @@ -0,0 +1,13 @@ +import UploadNoteForm from "~/components/UploadNoteForm"; +import { api } from "~/trpc/server"; + +export default async function UploadPage() { + const courses = await api.course.fetchCourses(); + + return ( +
+

Upload a Note

+ +
+ ); +} diff --git a/src/components/UploadNoteForm.tsx b/src/components/UploadNoteForm.tsx new file mode 100644 index 0000000..8570a94 --- /dev/null +++ b/src/components/UploadNoteForm.tsx @@ -0,0 +1,192 @@ +"use client"; +import { useState } from "react"; +import { toast } from "sonner"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import * as z from "zod"; +import { Button } from "~/components/ui/button"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "~/components/ui/form"; +import { Input } from "~/components/ui/input"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "~/components/ui/select"; +import { CloudUpload, Paperclip } from "lucide-react"; +import { + FileInput, + FileUploader, + FileUploaderContent, + FileUploaderItem, +} from "~/components/ui/file-upload"; + +import type { Course } from "@prisma/client"; + +const formSchema = z.object({ + noteTitle: z.string().min(1).min(5), + noteCourse: z.string(), + noteFile: z.any(), +}); + +export default function UploadNoteForm({ courses }: { courses: Course[] }) { + const [files, setFiles] = useState(null); + const [fileUrl, setFileUrl] = useState(undefined); + + const dropZoneConfig = { + maxFiles: 1, + maxSize: 1024 * 1024 * 4, + multiple: false, + }; + const form = useForm>({ + resolver: zodResolver(formSchema), + }); + + function onSubmit(values: z.infer) { + try { + console.log(values.noteFile); + console.log(values); + toast( +
+          {JSON.stringify(values, null, 2)}
+        
, + ); + } catch (error) { + console.error("Form submission error", error); + toast.error("Failed to submit the form. Please try again."); + } + } + + return ( +
+ + ( + + Title + + + + Title of the note + + + )} + /> + + ( + + Course + + + Select what course this note belongs to + + + + )} + /> + + ( + + Select File + + { + setFiles(files); + field.onChange(files?.[0] ?? null); + + if (fileUrl) { + URL.revokeObjectURL(fileUrl); + } + + if (files?.[0]) { + const url = URL.createObjectURL(files[0]); + setFileUrl(url); + } else { + setFileUrl(undefined); + } + }} + dropzoneOptions={dropZoneConfig} + className="bg-background relative rounded-lg p-2" + > + +
+ +

+ Click to upload +   or drag and drop +

+

+ PNG, JPG, or PDF +

+
+
+ + {files && + files.length > 0 && + files.map((file, i) => ( + + + {file.name} + + ))} + +
+
+ Select a file to upload. + +
+ )} + /> + {fileUrl && files?.[0] && ( +
+
+ {files[0].name} +
+
+ )} + + + + ); +} diff --git a/src/components/ui/file-upload.tsx b/src/components/ui/file-upload.tsx new file mode 100644 index 0000000..5e08a36 --- /dev/null +++ b/src/components/ui/file-upload.tsx @@ -0,0 +1,363 @@ +"use client"; + +import { Input } from "~/components/ui/input"; +import { cn } from "~/lib/utils"; +import type { Dispatch, SetStateAction } from "react"; +import { + createContext, + forwardRef, + useCallback, + useContext, + useEffect, + useRef, + useState, +} from "react"; +import type { + DropzoneState, + FileRejection, + DropzoneOptions, +} from "react-dropzone"; +import { useDropzone } from "react-dropzone"; +import { toast } from "sonner"; +import { Trash2 as RemoveIcon } from "lucide-react"; +import { buttonVariants } from "~/components/ui/button"; + +type DirectionOptions = "rtl" | "ltr" | undefined; + +type FileUploaderContextType = { + dropzoneState: DropzoneState; + isLOF: boolean; + isFileTooBig: boolean; + removeFileFromSet: (index: number) => void; + activeIndex: number; + setActiveIndex: Dispatch>; + orientation: "horizontal" | "vertical"; + direction: DirectionOptions; +}; + +const FileUploaderContext = createContext(null); + +export const useFileUpload = () => { + const context = useContext(FileUploaderContext); + if (!context) { + throw new Error("useFileUpload must be used within a FileUploaderProvider"); + } + return context; +}; + +type FileUploaderProps = { + value: File[] | null; + reSelect?: boolean; + onValueChange: (value: File[] | null) => void; + dropzoneOptions: DropzoneOptions; + orientation?: "horizontal" | "vertical"; +}; + +/** + * File upload Docs: {@link: https://localhost:3000/docs/file-upload} + */ + +export const FileUploader = forwardRef< + HTMLDivElement, + FileUploaderProps & React.HTMLAttributes +>( + ( + { + className, + dropzoneOptions, + value, + onValueChange, + reSelect, + orientation = "vertical", + children, + dir, + ...props + }, + ref, + ) => { + const [isFileTooBig, setIsFileTooBig] = useState(false); + const [isLOF, setIsLOF] = useState(false); + const [activeIndex, setActiveIndex] = useState(-1); + const { + accept = { + "image/*": [".jpg", ".jpeg", ".png", ".pdf"], + }, + maxFiles = 1, + maxSize = 4 * 1024 * 1024, + multiple = false, + } = dropzoneOptions; + + const reSelectAll = maxFiles === 1 ? true : reSelect; + const direction: DirectionOptions = dir === "rtl" ? "rtl" : "ltr"; + + const removeFileFromSet = useCallback( + (i: number) => { + if (!value) return; + const newFiles = value.filter((_, index) => index !== i); + onValueChange(newFiles); + }, + [value, onValueChange], + ); + + const handleKeyDown = useCallback( + (e: React.KeyboardEvent) => { + e.preventDefault(); + e.stopPropagation(); + + if (!value) return; + + const moveNext = () => { + const nextIndex = activeIndex + 1; + setActiveIndex(nextIndex > value.length - 1 ? 0 : nextIndex); + }; + + const movePrev = () => { + const nextIndex = activeIndex - 1; + setActiveIndex(nextIndex < 0 ? value.length - 1 : nextIndex); + }; + + const prevKey = + orientation === "horizontal" + ? direction === "ltr" + ? "ArrowLeft" + : "ArrowRight" + : "ArrowUp"; + + const nextKey = + orientation === "horizontal" + ? direction === "ltr" + ? "ArrowRight" + : "ArrowLeft" + : "ArrowDown"; + + if (e.key === nextKey) { + moveNext(); + } else if (e.key === prevKey) { + movePrev(); + } else if (e.key === "Enter" || e.key === "Space") { + if (activeIndex === -1) { + dropzoneState.inputRef.current?.click(); + } + } else if (e.key === "Delete" || e.key === "Backspace") { + if (activeIndex !== -1) { + removeFileFromSet(activeIndex); + if (value.length - 1 === 0) { + setActiveIndex(-1); + return; + } + movePrev(); + } + } else if (e.key === "Escape") { + setActiveIndex(-1); + } + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [value, activeIndex, removeFileFromSet], + ); + + const onDrop = useCallback( + (acceptedFiles: File[], rejectedFiles: FileRejection[]) => { + const files = acceptedFiles; + + if (!files) { + toast.error("file error , probably too big"); + return; + } + + const newValues: File[] = value ? [...value] : []; + + if (reSelectAll) { + newValues.splice(0, newValues.length); + } + + files.forEach((file) => { + if (newValues.length < maxFiles) { + newValues.push(file); + } + }); + + onValueChange(newValues); + + if (rejectedFiles.length > 0) { + // eslint-disable-next-line @typescript-eslint/prefer-for-of + for (let i = 0; i < rejectedFiles.length; i++) { + if (rejectedFiles[i]?.errors[0]?.code === "file-too-large") { + toast.error( + `File is too large. Max size is ${maxSize / 1024 / 1024}MB`, + ); + break; + } + if (rejectedFiles[i]?.errors[0]?.message) { + toast.error(rejectedFiles[i]?.errors[0]?.message); + break; + } + } + } + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [reSelectAll, value], + ); + + useEffect(() => { + if (!value) return; + if (value.length === maxFiles) { + setIsLOF(true); + return; + } + setIsLOF(false); + }, [value, maxFiles]); + + const opts = dropzoneOptions + ? dropzoneOptions + : { accept, maxFiles, maxSize, multiple }; + + const dropzoneState = useDropzone({ + ...opts, + onDrop, + onDropRejected: () => setIsFileTooBig(true), + onDropAccepted: () => setIsFileTooBig(false), + }); + + return ( + +
0, + }, + )} + dir={dir} + {...props} + > + {children} +
+
+ ); + }, +); + +FileUploader.displayName = "FileUploader"; + +export const FileUploaderContent = forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ children, className, ...props }, ref) => { + const { orientation } = useFileUpload(); + const containerRef = useRef(null); + + return ( +
+
+ {children} +
+
+ ); +}); + +FileUploaderContent.displayName = "FileUploaderContent"; + +export const FileUploaderItem = forwardRef< + HTMLDivElement, + { index: number } & React.HTMLAttributes +>(({ className, index, children, ...props }, ref) => { + const { removeFileFromSet, activeIndex, direction } = useFileUpload(); + const isSelected = index === activeIndex; + return ( +
+
+ {children} +
+ +
+ ); +}); + +FileUploaderItem.displayName = "FileUploaderItem"; + +export const FileInput = forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, children, ...props }, ref) => { + const { dropzoneState, isFileTooBig, isLOF } = useFileUpload(); + const rootProps = isLOF ? {} : dropzoneState.getRootProps(); + return ( +
+
+ {children} +
+ +
+ ); +}); + +FileInput.displayName = "FileInput"; diff --git a/src/components/ui/hero.tsx b/src/components/ui/hero.tsx index 74e6462..99fb2f5 100644 --- a/src/components/ui/hero.tsx +++ b/src/components/ui/hero.tsx @@ -72,9 +72,11 @@ export const Hero = () => { View Courses - + + + diff --git a/src/components/ui/select.tsx b/src/components/ui/select.tsx new file mode 100644 index 0000000..1d0e3ff --- /dev/null +++ b/src/components/ui/select.tsx @@ -0,0 +1,185 @@ +"use client" + +import * as React from "react" +import * as SelectPrimitive from "@radix-ui/react-select" +import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react" + +import { cn } from "~/lib/utils" + +function Select({ + ...props +}: React.ComponentProps) { + return +} + +function SelectGroup({ + ...props +}: React.ComponentProps) { + return +} + +function SelectValue({ + ...props +}: React.ComponentProps) { + return +} + +function SelectTrigger({ + className, + size = "default", + children, + ...props +}: React.ComponentProps & { + size?: "sm" | "default" +}) { + return ( + + {children} + + + + + ) +} + +function SelectContent({ + className, + children, + position = "popper", + ...props +}: React.ComponentProps) { + return ( + + + + + {children} + + + + + ) +} + +function SelectLabel({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function SelectItem({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ) +} + +function SelectSeparator({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function SelectScrollUpButton({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +function SelectScrollDownButton({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +export { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectLabel, + SelectScrollDownButton, + SelectScrollUpButton, + SelectSeparator, + SelectTrigger, + SelectValue, +} diff --git a/src/env.js b/src/env.js index 78dca1b..c834f64 100644 --- a/src/env.js +++ b/src/env.js @@ -18,6 +18,10 @@ export const env = createEnv({ AUTH_GITHUB_SECRET: z.string(), ADMIN_FER: z.string(), ADMIN_DEV: z.string(), + AWS_BUCKET_NAME: z.string(), + AWS_BUCKET_REGION: z.string(), + AWS_ACCESS_KEY: z.string(), + AWS_SECRET_ACCESS_KEY: z.string(), NODE_ENV: z .enum(["development", "test", "production"]) .default("development"), @@ -46,6 +50,10 @@ export const env = createEnv({ AUTH_GITHUB_SECRET: process.env.AUTH_GITHUB_SECRET, ADMIN_FER: process.env.ADMIN_FER, ADMIN_DEV: process.env.ADMIN_DEV, + AWS_BUCKET_NAME: process.env.AWS_BUCKET_NAME, + AWS_BUCKET_REGION: process.env.AWS_BUCKET_REGION, + AWS_ACCESS_KEY: process.env.AWS_ACCESS_KEY, + AWS_SECRET_ACCESS_KEY: process.env.AWS_SECRET_ACCESS_KEY, }, /** * Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially