basic upload
This commit is contained in:
1
.prettierrc
Normal file
1
.prettierrc
Normal file
@@ -0,0 +1 @@
|
||||
{}
|
||||
1
example/A.csv
Normal file
1
example/A.csv
Normal file
@@ -0,0 +1 @@
|
||||
example
|
||||
|
1
example/B.csv
Normal file
1
example/B.csv
Normal file
@@ -0,0 +1 @@
|
||||
other_file
|
||||
|
1
example/imported/A_1.csv
Normal file
1
example/imported/A_1.csv
Normal file
@@ -0,0 +1 @@
|
||||
example
|
||||
|
@@ -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",
|
||||
|
||||
30
pnpm-lock.yaml
generated
30
pnpm-lock.yaml
generated
@@ -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: {}
|
||||
|
||||
78
src/app/UploadFiles.tsx
Normal file
78
src/app/UploadFiles.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
48
src/app/api/upload/route.ts
Normal file
48
src/app/api/upload/route.ts
Normal 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();
|
||||
@@ -1,3 +1,10 @@
|
||||
import { UploadFiles } from "./UploadFiles";
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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],
|
||||
}),
|
||||
]
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user