settings.json
{
"editor.tabSize": 2,
"editor.insertSpaces": false,
"editor.detectIndentation": false
}
route.ts
import mime from "mime";
import { join } from "path";
import { stat, mkdir, writeFile } from "fs/promises";
import * as dateFn from "date-fns";
import { NextRequest, NextResponse } from "next/server";
export async function POST(request: NextRequest) {
const formData = await request.formData();
const file = formData.get("file") as Blob | null;
if (!file) {
return NextResponse.json(
{ error: "File blob is required." },
{ status: 400 }
);
}
const buffer = Buffer.from(await file.arrayBuffer());
const relativeUploadDir = `/uploads/${dateFn.format(Date.now(), "dd-MM-Y")}`;
const uploadDir = join(process.cwd(), "public", relativeUploadDir);
try {
await stat(uploadDir);
} catch (e: any) {
if (e.code === "ENOENT") {
await mkdir(uploadDir, { recursive: true });
} else {
console.error(
"Error while trying to create directory when uploading a file\n",
e
);
return NextResponse.json(
{ error: "Something went wrong." },
{ status: 500 }
);
}
}
try {
const uniqueSuffix = `${Date.now()}-${Math.round(Math.random() * 1e9)}`;
const filename = `${file.name.replace(
/\.[^/.]+$/,
""
)}-${uniqueSuffix}.${mime.getExtension(file.type)}`;
await writeFile(`${uploadDir}/${filename}`, buffer);
return NextResponse.json({ fileUrl: `${relativeUploadDir}/${filename}` });
} catch (e) {
console.error("Error while trying to upload a file\n", e);
return NextResponse.json(
{ error: "Something went wrong." },
{ status: 500 }
);
}
}
globals.css
* {
box-sizing: border-box;
}
layout.tsx
import "./globals.css";
export const metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}
page.module.scss
.container {
margin: 0 auto;
max-width: 480px;
h1 {
padding: 40px 0;
}
}
page.tsx
import { Inter } from "next/font/google";
import styles from "./page.module.scss";
import FileUploader from "@/components/FileUploader";
const inter = Inter({ subsets: ["latin"] });
export default function Home() {
return (
<main>
<div className={`${styles.container} ${inter.className}`}>
<h1>File uploader</h1>
<form>
<div>
<h3>Thumbnail</h3>
<FileUploader />
</div>
</form>
</div>
</main>
);
}
FileUploader.module.scss
.file-uploader {
display: block;
position: relative;
overflow: hidden;
img {
position: absolute;
top: 50%;
left: 0;
width: 100%;
height: 100%;
display: block;
object-fit: cover;
transform: translateY(-50%);
}
}
FileUploader.tsx
"use client";
import Image from "next/image";
import { ChangeEvent, useState } from "react";
import styles from "./FileUploader.module.scss";
export default function FileUploader() {
const [imageUrl, setImageUrl] = useState("/images/placeholder-image.jpg");
const onImageFileChange = async (e: ChangeEvent<HTMLInputElement>) => {
const fileInput = e.target;
if (!fileInput.files) {
console.warn("no file was chosen");
return;
}
if (!fileInput.files || fileInput.files.length === 0) {
console.warn("files list is empty");
return;
}
const file = fileInput.files[0];
const formData = new FormData();
formData.append("file", file);
try {
const res = await fetch("/api/upload", {
method: "POST",
body: formData,
});
if (!res.ok) {
console.error("something went wrong, check your console.");
return;
}
const data: { fileUrl: string } = await res.json();
setImageUrl(data.fileUrl);
} catch (error) {
console.error("something went wrong, check your console.");
}
/** Reset file input */
e.target.type = "text";
e.target.type = "file";
};
return (
<label
className={styles["file-uploader"]}
style={{ paddingTop: `calc(100% * (${446} / ${720}))` }}
>
<Image
src={imageUrl}
alt="uploaded image"
width={720}
height={446}
priority={true}
/>
<input
style={{ display: "none" }}
type="file"
onChange={onImageFileChange}
/>
</label>
);
}
.eslintrc.json
{
"extends": "next/core-web-vitals"
}
next-env.d.ts
/// <reference types="next" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.
next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
appDir: true,
},
}
module.exports = nextConfig
package.json
{
"name": "nextjs_app_dir_file_uploader",
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@types/node": "18.15.11",
"@types/react": "18.0.33",
"@types/react-dom": "18.0.11",
"date-fns": "^2.29.3",
"eslint": "8.37.0",
"eslint-config-next": "13.2.4",
"mime": "^3.0.0",
"next": "13.2.4",
"react": "18.2.0",
"react-dom": "18.2.0",
"typescript": "5.0.3"
},
"devDependencies": {
"@types/mime": "^3.0.1",
"sass": "^1.60.0"
}
}tsconfig.json
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}