diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/.prettierrc @@ -0,0 +1 @@ +{} diff --git a/example/A.csv b/example/A.csv new file mode 100644 index 0000000..33a9488 --- /dev/null +++ b/example/A.csv @@ -0,0 +1 @@ +example diff --git a/example/B.csv b/example/B.csv new file mode 100644 index 0000000..5224d81 --- /dev/null +++ b/example/B.csv @@ -0,0 +1 @@ +other_file diff --git a/example/imported/A_1.csv b/example/imported/A_1.csv new file mode 100644 index 0000000..33a9488 --- /dev/null +++ b/example/imported/A_1.csv @@ -0,0 +1 @@ +example diff --git a/package.json b/package.json index 00f2789..c2fa8f6 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,8 @@ "drizzle-orm": "^0.44.7", "next": "16.0.6", "react": "19.2.0", - "react-dom": "19.2.0" + "react-dom": "19.2.0", + "react-dropzone": "^14.3.8" }, "devDependencies": { "@tailwindcss/postcss": "^4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2c3991f..75b36e8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,6 +23,9 @@ importers: react-dom: specifier: 19.2.0 version: 19.2.0(react@19.2.0) + react-dropzone: + specifier: ^14.3.8 + version: 14.3.8(react@19.2.0) devDependencies: '@tailwindcss/postcss': specifier: ^4 @@ -1120,6 +1123,10 @@ packages: resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} engines: {node: '>= 0.4'} + attr-accept@2.2.5: + resolution: {integrity: sha512-0bDNnY/u6pPwHDMoF0FieU354oBi0a8rD9FcsLwzcGWbc8KS8KPIi7y+s13OlVY+gMWc/9xEMUgNE6Qm8ZllYQ==} + engines: {node: '>=4'} + available-typed-arrays@1.0.7: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} @@ -1613,6 +1620,10 @@ packages: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} + file-selector@2.1.2: + resolution: {integrity: sha512-QgXo+mXTe8ljeqUFaX3QVHc5osSItJ/Km+xpocx0aSqWGMSCf6qYs/VnzZgS864Pjn5iceMRFigeAV7AfTlaig==} + engines: {node: '>= 12'} + file-uri-to-path@1.0.0: resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} @@ -2238,6 +2249,12 @@ packages: peerDependencies: react: ^19.2.0 + react-dropzone@14.3.8: + resolution: {integrity: sha512-sBgODnq+lcA4P296DY4wacOZz3JFpD99fp+hb//iBO2HHnyeZU3FwWyXJ6salNpqQdsZrgMrotuko/BdJMV8Ug==} + engines: {node: '>= 10.13'} + peerDependencies: + react: '>= 16.8 || 18.0.0' + react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} @@ -3495,6 +3512,8 @@ snapshots: async-function@1.0.0: {} + attr-accept@2.2.5: {} + available-typed-arrays@1.0.7: dependencies: possible-typed-array-names: 1.1.0 @@ -4108,6 +4127,10 @@ snapshots: dependencies: flat-cache: 4.0.1 + file-selector@2.1.2: + dependencies: + tslib: 2.8.1 + file-uri-to-path@1.0.0: optional: true @@ -4740,6 +4763,13 @@ snapshots: react: 19.2.0 scheduler: 0.27.0 + react-dropzone@14.3.8(react@19.2.0): + dependencies: + attr-accept: 2.2.5 + file-selector: 2.1.2 + prop-types: 15.8.1 + react: 19.2.0 + react-is@16.13.1: {} react@19.2.0: {} diff --git a/src/app/UploadFiles.tsx b/src/app/UploadFiles.tsx new file mode 100644 index 0000000..a8b88cc --- /dev/null +++ b/src/app/UploadFiles.tsx @@ -0,0 +1,78 @@ +"use client"; + +import { useState, useCallback } from "react"; +import { useDropzone } from "react-dropzone"; + +type ExtendedFile = File & { + /** can sometimes be an empty string */ + webkitRelativePath?: string; + path?: string; + relativePath?: string; +}; + +type CreatedFile = { + id: number; + path: string; + size: number; +}; + +export function UploadFiles() { + const [createdFiles, setCreatedFiles] = useState([]); + + const onDrop = useCallback(async (acceptedFiles: ExtendedFile[]) => { + if (acceptedFiles.length === 0) return; + + const newCreatedFiles: CreatedFile[] = []; + for (const file of acceptedFiles) { + const path = + file.path || file.relativePath || file.webkitRelativePath || file.name; + const normalizedPath = path.replace(/^\.?\//, ""); // removes leading ./ or / + + const formData = new FormData(); + formData.append("file", file); + formData.append("path", normalizedPath); + + const res = await fetch("/api/upload", { + method: "POST", + body: formData, + }); + + const data = await res.json(); + newCreatedFiles.push(data.createdFile); + } + setCreatedFiles(newCreatedFiles); + }, []); + + const { getRootProps, getInputProps, isDragActive } = useDropzone({ + onDrop, + multiple: true, + }); + + return ( +
+

Upload Files

+
+ + {isDragActive ? ( +

Drop the files here...

+ ) : ( +

Drag & drop files here, or click to select

+ )} +
+ +
+ ); +} diff --git a/src/app/api/upload/route.ts b/src/app/api/upload/route.ts new file mode 100644 index 0000000..e6f5495 --- /dev/null +++ b/src/app/api/upload/route.ts @@ -0,0 +1,48 @@ +import { db, files, users } from "@/db"; +import { NextRequest, NextResponse } from "next/server"; + +const USER_ID = 1; + +export async function POST(request: NextRequest) { + await assertUserExists(); + + const formData = await request.formData(); + const formFile = formData.get("file") as File | null; + const path = formData.get("path") as string; + + if (!formFile) { + return NextResponse.json({ message: "No file uploaded" }, { status: 400 }); + } + + await db + .insert(users) + .values({ + id: 1, + email: "test@test.com", + name: "Test User", + }) + .onConflictDoNothing(); + + const [createdFile] = await db + .insert(files) + .values({ + ownerId: 1, + path, + size: formFile.size, + }) + .returning(); + + return NextResponse.json({ + createdFile, + }); +} + +const assertUserExists = () => + db + .insert(users) + .values({ + id: USER_ID, + email: "test@test.com", + name: "Test User", + }) + .onConflictDoNothing(); diff --git a/src/app/page.tsx b/src/app/page.tsx index c463412..ec6c095 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,3 +1,10 @@ +import { UploadFiles } from "./UploadFiles"; + export default function Home() { - return

Files

; + return ( +
+

Enlaye Files

+ +
+ ); } diff --git a/src/db/schema.ts b/src/db/schema.ts index 5eb6518..29fa4dc 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -1,4 +1,9 @@ -import { sqliteTable, text, integer, foreignKey } from "drizzle-orm/sqlite-core"; +import { + foreignKey, + integer, + sqliteTable, + text, +} from "drizzle-orm/sqlite-core"; export const users = sqliteTable("users", { id: integer().primaryKey({ autoIncrement: true }), @@ -6,13 +11,18 @@ export const users = sqliteTable("users", { email: text().notNull().unique(), }); -export const files = sqliteTable("files", { - id: integer().primaryKey({ autoIncrement: true }), - ownerId: integer().notNull(), - path: text().notNull(), -}, (table) => [ - foreignKey({ - columns: [table.ownerId], - foreignColumns: [users.id], - }), -]); +export const files = sqliteTable( + "files", + { + id: integer().primaryKey({ autoIncrement: true }), + ownerId: integer("owner_id").notNull(), + path: text().notNull(), + size: integer().notNull(), + }, + (table) => [ + foreignKey({ + columns: [table.ownerId], + foreignColumns: [users.id], + }), + ] +);