diff --git a/next.config.js b/next.config.js index 121c4f4..707e640 100644 --- a/next.config.js +++ b/next.config.js @@ -4,7 +4,9 @@ */ import "./src/env.js"; -/** @type {import("next").NextConfig} */ -const config = {}; +const nextConfig = { + reactStrictMode: true, + experimental: {}, +}; -export default config; +export default nextConfig; diff --git a/package-lock.json b/package-lock.json index fd432cb..6d44376 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,7 @@ "next-auth": "5.0.0-beta.25", "react": "^19.0.0", "react-dom": "^19.0.0", + "react-hot-toast": "^2.5.2", "server-only": "^0.0.1", "superjson": "^2.2.1", "zod": "^3.24.2" @@ -27,6 +28,7 @@ "devDependencies": { "@eslint/eslintrc": "^3.3.1", "@tailwindcss/postcss": "^4.0.15", + "@types/busboy": "^1.5.4", "@types/node": "^20.14.10", "@types/react": "^19.0.0", "@types/react-dom": "^19.0.0", @@ -1836,6 +1838,16 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/busboy": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@types/busboy/-/busboy-1.5.4.tgz", + "integrity": "sha512-kG7WrUuAKK0NoyxfQHsVE6j1m01s6kMma64E+OZenQABMQyTJop1DumUWcLwAQ2JzpefU7PDYoRDKl8uZosFjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/cookie": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", @@ -2880,7 +2892,6 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "dev": true, "license": "MIT" }, "node_modules/damerau-levenshtein": { @@ -4041,6 +4052,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/goober": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.16.tgz", + "integrity": "sha512-erjk19y1U33+XAMe1VTvIONHYoSqE4iS7BYUZfHaqeohLmnC0FdxEh7rQU+6MZ4OajItzjZFSRtVANrQwNq6/g==", + "license": "MIT", + "peerDependencies": { + "csstype": "^3.0.10" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -5842,6 +5862,23 @@ "react": "^19.1.0" } }, + "node_modules/react-hot-toast": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.5.2.tgz", + "integrity": "sha512-Tun3BbCxzmXXM7C+NI4qiv6lT0uwGh4oAfeJyNOjYUejTsm35mK9iCaYLGv8cBz9L5YxZLx/2ii7zsIwPtPUdw==", + "license": "MIT", + "dependencies": { + "csstype": "^3.1.3", + "goober": "^2.1.16" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": ">=16", + "react-dom": ">=16" + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", diff --git a/package.json b/package.json index 90764fa..5d52f01 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "next-auth": "5.0.0-beta.25", "react": "^19.0.0", "react-dom": "^19.0.0", + "react-hot-toast": "^2.5.2", "server-only": "^0.0.1", "superjson": "^2.2.1", "zod": "^3.24.2" @@ -39,6 +40,7 @@ "devDependencies": { "@eslint/eslintrc": "^3.3.1", "@tailwindcss/postcss": "^4.0.15", + "@types/busboy": "^1.5.4", "@types/node": "^20.14.10", "@types/react": "^19.0.0", "@types/react-dom": "^19.0.0", diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 5bfac5a..6bd6093 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -63,6 +63,7 @@ model User { accounts Account[] sessions Session[] posts Post[] + files File[] // Relation to the File model } model VerificationToken { @@ -72,3 +73,15 @@ model VerificationToken { @@unique([identifier, token]) } + +model File { + id String @id @default(cuid()) + url String + name String + size Int // Size in bytes + extension String + uploadDate DateTime @default(now()) + + uploadedBy User? @relation(fields: [uploadedById], references: [id], onDelete: SetNull) + uploadedById String? +} diff --git a/src/app/_components/FileGrid.tsx b/src/app/_components/FileGrid.tsx new file mode 100644 index 0000000..759c680 --- /dev/null +++ b/src/app/_components/FileGrid.tsx @@ -0,0 +1,207 @@ +"use client"; + +import { useEffect, useState } from "react"; +import toast from "react-hot-toast"; +import { useRouter } from "next/navigation"; + +interface File { + id: string; + name: string; + url: string; +} + +interface FileGridProps { + session: { user: { id: string } } | null; +} + +export default function FileGrid({ session }: FileGridProps) { + const [files, setFiles] = useState([]); + const [error, setError] = useState(null); + + + + const fetchFiles = async () => { + try { + const response = await fetch("/api/files"); + if (!response.ok) { + throw new Error("Failed to fetch files"); + } + const data: { files: File[] } = await response.json(); + setFiles(data.files); + } catch (err) { + console.error(err); + setError("Failed to load files."); + } + }; + + const handleDownload = async (fileName: string) => { + try { + const response = await fetch(`/api/files/download?fileName=${encodeURIComponent(fileName)}`); + if (!response.ok) { + throw new Error("Failed to download file"); + } + + const blob = await response.blob(); + const url = window.URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = fileName; + document.body.appendChild(a); + a.click(); + a.remove(); + window.URL.revokeObjectURL(url); + + toast.success(`File "${fileName}" downloaded successfully!`); + } catch (err) { + console.error(err); + toast.error("Failed to download file."); + } + }; + + useEffect(() => { + if (!session?.user) { + setError("You must be logged in to view files."); + return; + } + + // Fetch files initially + void fetchFiles(); + + // Listen for real-time updates via SSE + const eventSource = new EventSource("/api/files/stream"); + + eventSource.onmessage = (event) => { + const data = JSON.parse(event.data); + + if (data.type === "file-added") { + setFiles((prevFiles) => [...prevFiles, data.file]); + toast.success(`File "${data.file.name}" added!`); + } else if (data.type === "file-removed") { + setFiles((prevFiles) => prevFiles.filter((file) => file.id !== data.fileId)); + } + }; + + eventSource.onerror = (err) => { + console.error("SSE error:", err); + eventSource.close(); + }; + + return () => { + eventSource.close(); // Cleanup on unmount + }; + }, [session]); + + const handleCopyUrl = (url: string) => { + navigator.clipboard + .writeText(url) + .then(() => toast.success("File URL copied to clipboard!")) + .catch(() => toast.error("Failed to copy URL.")); + }; + + const handleRemove = async (fileId: string) => { + try { + const response = await fetch(`/api/remove`, { + method: "DELETE", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ id: fileId }), + }); + + if (!response.ok) { + throw new Error("Failed to delete file"); + } + + setFiles((prevFiles) => prevFiles.filter((file) => file.id !== fileId)); + toast.success("File removed successfully!"); + } catch (err) { + console.error(err); + toast.error("Failed to remove file."); + } + }; + + if (error) { + return
{error}
; + } + const router = useRouter(); + + return ( +
+ {files.map((file) => ( +
+ + +
+ {/* Download Button */} + + + {/* Copy URL Button */} + + + {/* Remove Button */} + +
+
+ ))} +
+ ); +} \ No newline at end of file diff --git a/src/app/_components/UploadForm.tsx b/src/app/_components/UploadForm.tsx new file mode 100644 index 0000000..1627bd8 --- /dev/null +++ b/src/app/_components/UploadForm.tsx @@ -0,0 +1,170 @@ +"use client"; + +import { useState, useRef } from "react"; +import toast, { Toaster } from "react-hot-toast"; + +export default function UploadForm() { + const [file, setFile] = useState(null); + const [uploading, setUploading] = useState(false); + const [uploadedFileUrl, setUploadedFileUrl] = useState(null); + const [progress, setProgress] = useState(0); // Track upload progress + const fileInputRef = useRef(null); // Ref for the file input + + const handleFileChange = (e: React.ChangeEvent) => { + if (e.target.files) { + setFile(e.target.files[0] ?? null); + setUploadedFileUrl(null); // Reset the uploaded file URL when a new file is selected + setProgress(0); // Reset progress + setUploading(false); // Reset uploading state + } + }; + + const handleUpload = async () => { + if (!file) return toast.error("Please select a file to upload."); + setUploading(true); + + try { + const formData = new FormData(); + formData.append("file", file); + + const xhr = new XMLHttpRequest(); + xhr.open("POST", "/api/upload", true); + + // Track upload progress + xhr.upload.onprogress = (event) => { + if (event.lengthComputable && event.total > 0) { + const percentComplete = Math.round((event.loaded / event.total) * 100); + setProgress(percentComplete); + } + }; + + xhr.onload = () => { + if (xhr.status === 200) { + const response = JSON.parse(xhr.responseText); + setUploadedFileUrl(response.url); // Assume the API returns the uploaded file URL + toast.success("File uploaded successfully!"); + + // Clear the file input and reset state + setFile(null); + if (fileInputRef.current) { + fileInputRef.current.value = ""; // Clear the file input + } + } else { + console.error("Upload failed:", xhr.responseText); + toast.error("Failed to upload file."); + } + setUploading(false); + }; + + xhr.onerror = () => { + console.error("Upload failed"); + toast.error("Failed to upload file."); + setUploading(false); + }; + + xhr.send(formData); + } catch (error) { + console.error(error); + toast.error("Failed to upload file."); + setUploading(false); + } + }; + + const handleCopyUrl = () => { + if (uploadedFileUrl) { + navigator.clipboard + .writeText(uploadedFileUrl) + .then(() => toast.success("File URL copied to clipboard!")) + .catch(() => toast.error("Failed to copy URL.")); + } + }; + + return ( +
+ {/* Toast container */} + + +
+ {/* Custom file input */} + + + +
+ + {file && uploading && ( +
+
+
+
+
+ )} + + {uploadedFileUrl && ( +
+

{uploadedFileUrl}

+ +
+ )} +
+ ); +} \ No newline at end of file diff --git a/src/app/_components/post.tsx b/src/app/_components/post.tsx deleted file mode 100644 index 5618b40..0000000 --- a/src/app/_components/post.tsx +++ /dev/null @@ -1,50 +0,0 @@ -"use client"; - -import { useState } from "react"; - -import { api } from "~/trpc/react"; - -export function LatestPost() { - const [latestPost] = api.post.getLatest.useSuspenseQuery(); - - const utils = api.useUtils(); - const [name, setName] = useState(""); - const createPost = api.post.create.useMutation({ - onSuccess: async () => { - await utils.post.invalidate(); - setName(""); - }, - }); - - return ( -
- {latestPost ? ( -

Your most recent post: {latestPost.name}

- ) : ( -

You have no posts yet.

- )} -
{ - e.preventDefault(); - createPost.mutate({ name }); - }} - className="flex flex-col gap-2" - > - setName(e.target.value)} - className="w-full rounded-full bg-white/10 px-4 py-2 text-white" - /> - -
-
- ); -} diff --git a/src/app/api/files/download/route.ts b/src/app/api/files/download/route.ts new file mode 100644 index 0000000..18f8114 --- /dev/null +++ b/src/app/api/files/download/route.ts @@ -0,0 +1,27 @@ +import { NextResponse } from "next/server"; +import path from "path"; +import { promises as fs } from "fs"; + +export async function GET(req: Request) { + const url = new URL(req.url); + const fileName = url.searchParams.get("fileName"); + + if (!fileName) { + return NextResponse.json({ error: "File name is required" }, { status: 400 }); + } + + try { + const filePath = path.join(process.cwd(), "uploads", fileName); + const fileBuffer = await fs.readFile(filePath); + + return new Response(fileBuffer, { + headers: { + "Content-Type": "application/octet-stream", + "Content-Disposition": `attachment; filename="${fileName}"`, + }, + }); + } catch (error) { + console.error("Error reading file:", error); + return NextResponse.json({ error: "File not found" }, { status: 404 }); + } +} \ No newline at end of file diff --git a/src/app/api/files/route.ts b/src/app/api/files/route.ts new file mode 100644 index 0000000..1c7f5f3 --- /dev/null +++ b/src/app/api/files/route.ts @@ -0,0 +1,23 @@ +import { NextResponse } from "next/server"; +import { db } from "~/server/db"; // Ensure this points to your Prisma client setup +import { auth } from "~/server/auth"; + +export async function GET() { + const session = await auth(); + + if (!session?.user) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + try { + const files = await db.file.findMany({ + where: { uploadedById: session.user.id }, + orderBy: { uploadDate: "desc" }, // Replace 'uploadDate' with the correct field name from your schema + }); + + return NextResponse.json({ files }); + } catch (error) { + console.error("Error fetching files:", error); + return NextResponse.json({ error: "Failed to fetch files" }, { status: 500 }); + } +} \ No newline at end of file diff --git a/src/app/api/files/stream/route.ts b/src/app/api/files/stream/route.ts new file mode 100644 index 0000000..fde07ac --- /dev/null +++ b/src/app/api/files/stream/route.ts @@ -0,0 +1,56 @@ +import { NextResponse } from "next/server"; + +const clients: Set = new Set(); + +export async function GET() { + const stream = new ReadableStream({ + start(controller) { + const abortController = new AbortController(); + const signal = abortController.signal; + + const client = { + send: (data: string) => { + controller.enqueue(new TextEncoder().encode(`data: ${data}\n\n`)); + }, + close: () => { + controller.close(); + abortController.abort(); + }, + }; + + clients.add(client); + + // Remove the client when the stream is closed + const abortListener = () => { + clients.delete(client); + controller.close(); // Ensure the stream is closed when the client disconnects + }; + signal.addEventListener("abort", abortListener); + + // Cleanup the abort listener when the stream is closed + signal.addEventListener("abort", () => { + signal.removeEventListener("abort", abortListener); + }); + }, + }); + + return new Response(stream, { + headers: { + "Content-Type": "text/event-stream", + "Cache-Control": "no-cache", + Connection: "keep-alive", + }, + }); +} + +// Notify all connected clients about a file change +export function notifyClients(data: any) { + const message = JSON.stringify(data); + clients.forEach((client) => { + try { + client.send(message); + } catch (error) { + console.error("Failed to send message to a client:", error); + } + }); +} \ No newline at end of file diff --git a/src/app/api/remove/route.ts b/src/app/api/remove/route.ts new file mode 100644 index 0000000..83a533a --- /dev/null +++ b/src/app/api/remove/route.ts @@ -0,0 +1,48 @@ +import { NextResponse } from "next/server"; +import { db } from "~/server/db"; +import { auth } from "~/server/auth"; +import path from "path"; +import { promises as fs } from "fs"; +import { notifyClients } from "../files/stream/route"; + +export async function DELETE(req: Request) { + const session = await auth(); + + if (!session?.user) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + try { + const body = await req.json().catch(() => null); // Handle empty or invalid JSON + if (!body || !body.id) { + return NextResponse.json({ error: "Invalid request body" }, { status: 400 }); + } + + const resourceId = body.id; + + const resource = await db.file.findUnique({ + where: { id: resourceId }, + }); + + if (!resource || resource.uploadedById !== session.user.id) { + return NextResponse.json({ error: "Resource not found or unauthorized" }, { status: 404 }); + } + + const filePath = path.join(process.cwd(), "uploads", path.basename(resource.name)); + await fs.unlink(filePath).catch((err) => { + console.error("Error deleting file from filesystem:", err); + }); + + await db.file.delete({ + where: { id: resourceId }, + }); + + // Notify clients about the deleted file + notifyClients({ type: "file-removed", fileId: resourceId }); + + return NextResponse.json({ message: "Resource deleted successfully" }); + } catch (error) { + console.error("Error deleting resource:", error); + return NextResponse.json({ error: "Failed to delete resource" }, { status: 500 }); + } +} \ No newline at end of file diff --git a/src/app/api/serv/route.ts b/src/app/api/serv/route.ts new file mode 100644 index 0000000..c2560c1 --- /dev/null +++ b/src/app/api/serv/route.ts @@ -0,0 +1,41 @@ +import { NextResponse } from "next/server"; +import path from "path"; +import { promises as fs } from "fs"; +import { db } from "~/server/db"; + +export async function GET(req: Request) { + const url = new URL(req.url); + const fileId = url.searchParams.get("id"); // Get the `id` parameter from the query + + if (!fileId) { + return NextResponse.json({ error: "File ID is required" }, { status: 400 }); + } + + try { + // Fetch file metadata from the database + const file = await db.file.findFirst({ + where: { name: fileId }, + }); + + if (!file) { + return NextResponse.json({ error: "File not found" }, { status: 404 }); + } + + // Construct the file path + const filePath = path.join(process.cwd(), "uploads", file.name); + + // Read the file from the filesystem + const fileBuffer = await fs.readFile(filePath); + + // Return the file as a binary response + return new Response(fileBuffer, { + headers: { + "Content-Type": file.extension.startsWith(".png") ? "image/png" : "application/octet-stream", + "Content-Disposition": `inline; filename="${file.name}"`, + }, + }); + } catch (error) { + console.error("Error fetching file:", error); + return NextResponse.json({ error: "Failed to fetch file" }, { status: 500 }); + } +} \ No newline at end of file diff --git a/src/app/api/share/route.ts b/src/app/api/share/route.ts new file mode 100644 index 0000000..6105375 --- /dev/null +++ b/src/app/api/share/route.ts @@ -0,0 +1,40 @@ +import { NextResponse } from "next/server"; +import { db } from "~/server/db"; +import { auth } from "~/server/auth"; + + + +export async function GET(req: Request) { + const session = await auth(); + const url = new URL(req.url); + const fileName = url.searchParams.get("file"); + + if (!fileName) { + return NextResponse.json({ error: "File name is required" }, { status: 400 }); + } + + try { + const file = await db.file.findFirst({ + where: { name: fileName }, + include: { uploadedBy: true }, + }); + + if (!file) { + return NextResponse.json({ error: "File not found" }, { status: 404 }); + } + + return NextResponse.json({ + name: file.name, + size: file.size, + owner: file.uploadedBy?.name || "Unknown", + uploadDate: file.uploadDate, + id: file.id, + isOwner: session?.user?.id === file.uploadedById, // Check if the current user is the owner + type: file.extension, // Add file type + url: file.url, // Add file URL + }); + } catch (error) { + console.error("Error fetching file details:", error); + return NextResponse.json({ error: "Failed to fetch file details" }, { status: 500 }); + } +} \ No newline at end of file diff --git a/src/app/api/upload/route.ts b/src/app/api/upload/route.ts new file mode 100644 index 0000000..3687d19 --- /dev/null +++ b/src/app/api/upload/route.ts @@ -0,0 +1,111 @@ +import { NextResponse } from "next/server"; +import { promises as fs } from "fs"; +import path from "path"; +import { db } from "~/server/db"; +import { auth } from "~/server/auth"; +import Busboy from "busboy"; +import { Readable } from "stream"; +import { notifyClients } from "../files/stream/route"; + +export const config = { + api: { + bodyParser: false, // Disable Next.js body parsing + }, +}; + +export async function POST(req: Request) { + const session = await auth(); + + if (!session?.user) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const uploadDir = path.join(process.cwd(), "uploads"); + await fs.mkdir(uploadDir, { recursive: true }); + + return new Promise((resolve, reject) => { + const busboy = Busboy({ headers: { "content-type": req.headers.get("content-type") ?? "" } }); + let fileName = ""; + let fileBuffer = Buffer.alloc(0); + + busboy.on("file", async (fieldname, file, info) => { + fileName = info.filename || "uploaded-file"; + const chunks: Buffer[] = []; + + // Check if a file with the same name already exists for the user + const existingFile = await db.file.findFirst({ + where: { + name: fileName, + uploadedById: session.user.id, + }, + }); + + if (existingFile) { + // Modify the file name to make it unique + const fileExtension = path.extname(fileName); + const baseName = path.basename(fileName, fileExtension); + fileName = `${baseName}-${Date.now()}${fileExtension}`; + } + + file.on("data", (chunk) => { + chunks.push(chunk); + }); + + file.on("end", () => { + fileBuffer = Buffer.concat(chunks); + }); + }); + + busboy.on("finish", () => { + void (async () => { + try { + const filePath = path.join(uploadDir, fileName); + await fs.writeFile(filePath, fileBuffer); + const pageurl = new URL(req.url); + //get root path of the url + const pagePath = `${pageurl.protocol}//${pageurl.host}`; + + // Save file metadata to the database + const newFile = await db.file.create({ + data: { + url: `${pagePath}/share?file=${fileName}`, + name: fileName, + size: fileBuffer.length, + extension: path.extname(fileName), + uploadedById: session.user.id, + }, + }); + + // Notify clients about the new file + notifyClients({ type: "file-added", file: newFile }); + + resolve(NextResponse.json({ message: "File uploaded successfully" })); + } catch (error) { + console.error("Error handling upload:", error); + resolve(NextResponse.json({ error: "Failed to upload file" }, { status: 500 })); + } + })(); + }); + + busboy.on("error", (error: unknown) => { + console.error("Error parsing form data:", error); + reject(new Error("Failed to parse form data")); + }); + + if (req.body) { + const reader = req.body.getReader(); + const nodeStream = new Readable({ + async read() { + const { done, value } = await reader.read(); + if (done) { + this.push(null); // End the stream + } else { + this.push(value); // Push data to the stream + } + }, + }); + + nodeStream.pipe(busboy); + } + }); +} \ No newline at end of file diff --git a/src/app/layout.tsx b/src/app/layout.tsx index e9c9cbf..5659f14 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -6,8 +6,9 @@ import { Geist } from "next/font/google"; import { TRPCReactProvider } from "~/trpc/react"; export const metadata: Metadata = { - title: "Create T3 App", - description: "Generated by create-t3-app", + title: "File Hosting - Suchodupin", + description: "A simple file hosting service", + authors: [{ name: "Suchodupin" }], icons: [{ rel: "icon", url: "/favicon.ico" }], }; diff --git a/src/app/page.tsx b/src/app/page.tsx index 37cc32f..146ea28 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,57 +1,48 @@ import Link from "next/link"; - -import { LatestPost } from "~/app/_components/post"; import { auth } from "~/server/auth"; -import { api, HydrateClient } from "~/trpc/server"; +import { HydrateClient } from "~/trpc/server"; +import FileGrid from "~/app/_components/FileGrid"; +import UploadForm from "~/app/_components/UploadForm"; +import toast, { Toaster } from "react-hot-toast"; export default async function Home() { - const hello = await api.post.hello({ text: "from tRPC" }); const session = await auth(); - if (session?.user) { - void api.post.getLatest.prefetch(); - } return ( -
-
-

- Create T3 App -

-
+ +
+ {/* Top-right corner sign-out button */} + {session?.user && ( +
-

First Steps →

-
- Just the basics - Everything you need to know to set up your - database and authentication. -
- - -

Documentation →

-
- Learn more about Create T3 App, the libraries it uses, and how - to deploy it. -
+ Sign out
-
-

- {hello ? hello.greeting : "Loading tRPC query..."} -

+ )} +
+

+ File Hosting +

+ {/* Conditionally render FileGrid and UploadForm if the user is logged in */} + {session?.user ? ( + <> + + + + ) : ( +

+ Please log in to upload and view files. +

+ )} + {!session?.user && ( +
-

- {session && Logged in as {session.user?.name}} -

- - {session?.user && } + )}
diff --git a/src/app/share/page.tsx b/src/app/share/page.tsx new file mode 100644 index 0000000..3a10dc5 --- /dev/null +++ b/src/app/share/page.tsx @@ -0,0 +1,248 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { useSearchParams, useRouter } from "next/navigation"; +import toast, { Toaster } from "react-hot-toast"; + +interface FileDetails { + name: string; + size: number; + owner: string; + uploadDate: string; + id: string; // Add an ID for the file + isOwner: boolean; // Add a flag to indicate ownership + type: string; // Add a type field to differentiate between file types + url: string; // Add a URL field for the file +} + +export default function UploadsPage() { + const searchParams = useSearchParams(); + const router = useRouter(); + const fileName = searchParams.get("file"); + const [fileDetails, setFileDetails] = useState(null); + const [error, setError] = useState(null); + + useEffect(() => { + if (!fileName) { + setError("File name is required."); + return; + } + + const fetchFileDetails = async () => { + try { + const response = await fetch(`/api/share?file=${encodeURIComponent(fileName)}`); + if (!response.ok) { + throw new Error("Failed to fetch file details"); + } + + const data = await response.json(); + setFileDetails(data); + } catch (err) { + console.error(err); + setError("Failed to load file details."); + } + }; + + fetchFileDetails(); + }, [fileName]); + + const handleDownload = async () => { + try { + const response = await fetch(`/api/files/download?fileName=${encodeURIComponent(fileName || "")}`); + if (!response.ok) { + throw new Error("Failed to download file"); + } + + const blob = await response.blob(); + const url = window.URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = fileName || "downloaded-file"; + document.body.appendChild(a); + a.click(); + a.remove(); + window.URL.revokeObjectURL(url); + + toast.success("File downloaded successfully!"); + } catch (err) { + console.error(err); + toast.error("Failed to download file."); + } + }; + + const handleShare = () => { + if (fileDetails) { + const shareableLink = `${window.location.origin}/share?id=${fileDetails.name}`; + navigator.clipboard + .writeText(shareableLink) + .then(() => toast.success("Shareable link copied to clipboard!")) + .catch(() => toast.error("Failed to copy link.")); + } + }; + + const handleRemove = async () => { + try { + const response = await fetch(`/api/remove`, { + method: "DELETE", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ id: fileDetails?.id }), // Use the ID of the file + }); + + if (!response.ok) { + throw new Error("Failed to remove file"); + } + + toast.success("File removed successfully!"); + router.push("/"); + } catch (err) { + console.error(err); + toast.error("Failed to remove file."); + } + }; + + if (error) { + return
{error}
; + } + + if (!fileDetails) { + return ( +
+ +
+ +
+
+

+ File Details +

+
+
+ ); + } + + return ( +
+ +
+ +
+
+

+ File Details +

+
+ {(fileDetails.type.startsWith(".png") || fileDetails.type.startsWith(".jpg") || fileDetails.type.startsWith(".jpeg") || fileDetails.type.startsWith(".gif")) && ( + + )} + {(fileDetails.type.startsWith(".mp4") || fileDetails.type.startsWith(".webm") || fileDetails.type.startsWith(".ogg")) && ( + + )} +
+
+

+ Name: {fileDetails.name} +

+

+ Size: {(fileDetails.size / 1024).toFixed(2)} KB +

+

+ Owner: {fileDetails.owner} +

+

+ Upload Date: {new Date(fileDetails.uploadDate).toLocaleString()} +

+
+ {/* Preview Section */} +
+ + {fileDetails.isOwner && ( + <> + + + + )} +
+
+
+ ); +} + +export function SharePage() { + const searchParams = useSearchParams(); + const fileName = searchParams.get("file"); + const [imageSrc, setImageSrc] = useState(null); + const [error, setError] = useState(null); + + useEffect(() => { + + const fetchImage = async () => { + try { + if (!fileName) { + throw new Error("File name is required."); + } + const response = await fetch(`/api/serv?id=${encodeURIComponent(fileName)}`); + if (!response.ok) { + throw new Error("Failed to fetch image"); + } + + const blob = await response.blob(); + const objectUrl = URL.createObjectURL(blob); + setImageSrc(objectUrl); + } catch (err) { + console.error(err); + setError("Failed to load image."); + console.log(err); + } + }; + + fetchImage(); + + return () => { + if (imageSrc) { + URL.revokeObjectURL(imageSrc); // Clean up the object URL + } + }; + }, [fileName]); + + if (error) { + return
{error}
; + } + + if (!imageSrc) { + return
Loading...
; + } + + return ( + + ); +} \ No newline at end of file diff --git a/uploads/Szymanoch-Toyota.zip b/uploads/Szymanoch-Toyota.zip new file mode 100644 index 0000000..39156f2 Binary files /dev/null and b/uploads/Szymanoch-Toyota.zip differ diff --git a/uploads/naturaltanczy.mp4 b/uploads/naturaltanczy.mp4 new file mode 100644 index 0000000..8667f1a Binary files /dev/null and b/uploads/naturaltanczy.mp4 differ