basic upload

This commit is contained in:
konrad-enlaye
2025-12-05 10:02:11 -05:00
parent 3e22e0a168
commit 250952d4f4
10 changed files with 191 additions and 13 deletions

1
.prettierrc Normal file
View File

@@ -0,0 +1 @@
{}

1
example/A.csv Normal file
View File

@@ -0,0 +1 @@
example
1 example

1
example/B.csv Normal file
View File

@@ -0,0 +1 @@
other_file
1 other_file

1
example/imported/A_1.csv Normal file
View File

@@ -0,0 +1 @@
example
1 example

View File

@@ -17,7 +17,8 @@
"drizzle-orm": "^0.44.7", "drizzle-orm": "^0.44.7",
"next": "16.0.6", "next": "16.0.6",
"react": "19.2.0", "react": "19.2.0",
"react-dom": "19.2.0" "react-dom": "19.2.0",
"react-dropzone": "^14.3.8"
}, },
"devDependencies": { "devDependencies": {
"@tailwindcss/postcss": "^4", "@tailwindcss/postcss": "^4",

30
pnpm-lock.yaml generated
View File

@@ -23,6 +23,9 @@ importers:
react-dom: react-dom:
specifier: 19.2.0 specifier: 19.2.0
version: 19.2.0(react@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: devDependencies:
'@tailwindcss/postcss': '@tailwindcss/postcss':
specifier: ^4 specifier: ^4
@@ -1120,6 +1123,10 @@ packages:
resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==}
engines: {node: '>= 0.4'} 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: available-typed-arrays@1.0.7:
resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@@ -1613,6 +1620,10 @@ packages:
resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
engines: {node: '>=16.0.0'} 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: file-uri-to-path@1.0.0:
resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==}
@@ -2238,6 +2249,12 @@ packages:
peerDependencies: peerDependencies:
react: ^19.2.0 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: react-is@16.13.1:
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
@@ -3495,6 +3512,8 @@ snapshots:
async-function@1.0.0: {} async-function@1.0.0: {}
attr-accept@2.2.5: {}
available-typed-arrays@1.0.7: available-typed-arrays@1.0.7:
dependencies: dependencies:
possible-typed-array-names: 1.1.0 possible-typed-array-names: 1.1.0
@@ -4108,6 +4127,10 @@ snapshots:
dependencies: dependencies:
flat-cache: 4.0.1 flat-cache: 4.0.1
file-selector@2.1.2:
dependencies:
tslib: 2.8.1
file-uri-to-path@1.0.0: file-uri-to-path@1.0.0:
optional: true optional: true
@@ -4740,6 +4763,13 @@ snapshots:
react: 19.2.0 react: 19.2.0
scheduler: 0.27.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-is@16.13.1: {}
react@19.2.0: {} react@19.2.0: {}

78
src/app/UploadFiles.tsx Normal file
View File

@@ -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<CreatedFile[]>([]);
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 (
<div>
<h3 className="text-lg font-bold mb-4">Upload Files</h3>
<div
{...getRootProps()}
className={`border-2 border-dashed rounded-lg p-8 text-center cursor-pointer transition-colors ${
isDragActive
? "border-blue-500 bg-blue-50"
: "border-gray-300 hover:border-gray-400"
}`}
>
<input {...getInputProps()} />
{isDragActive ? (
<p>Drop the files here...</p>
) : (
<p>Drag & drop files here, or click to select</p>
)}
</div>
<ul className="list-disc list-inside mt-4">
{createdFiles.map((file) => (
<li key={file.id}>
Created file #{file.id}: {file.path} ({file.size} bytes)
</li>
))}
</ul>
</div>
);
}

View File

@@ -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();

View File

@@ -1,3 +1,10 @@
import { UploadFiles } from "./UploadFiles";
export default function Home() { export default function Home() {
return <div><h1>Files</h1></div>; return (
<div className="container mx-auto px-4 py-10">
<h1 className="text-2xl font-bold mb-6">Enlaye Files</h1>
<UploadFiles />
</div>
);
} }

View File

@@ -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", { export const users = sqliteTable("users", {
id: integer().primaryKey({ autoIncrement: true }), id: integer().primaryKey({ autoIncrement: true }),
@@ -6,13 +11,18 @@ export const users = sqliteTable("users", {
email: text().notNull().unique(), email: text().notNull().unique(),
}); });
export const files = sqliteTable("files", { export const files = sqliteTable(
"files",
{
id: integer().primaryKey({ autoIncrement: true }), id: integer().primaryKey({ autoIncrement: true }),
ownerId: integer().notNull(), ownerId: integer("owner_id").notNull(),
path: text().notNull(), path: text().notNull(),
}, (table) => [ size: integer().notNull(),
},
(table) => [
foreignKey({ foreignKey({
columns: [table.ownerId], columns: [table.ownerId],
foreignColumns: [users.id], foreignColumns: [users.id],
}), }),
]); ]
);