Compare commits
	
		
			No commits in common. "bb89a84ba85cf43030b4ae17e88709ed6c352fb4" and "99397e774c41980ef13b3155e13330ad4eaa7753" have entirely different histories.
		
	
	
		
			bb89a84ba8
			...
			99397e774c
		
	
		
							
								
								
									
										87
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										87
									
								
								README.md
									
									
									
									
									
								
							| @ -1,90 +1,29 @@ | |||||||
| # File Hosting App | # Create T3 App | ||||||
| 
 | 
 | ||||||
| This is a [T3 Stack](https://create.t3.gg/) project. It provides a file hosting service where users can upload, download, and manage files. | This is a [T3 Stack](https://create.t3.gg/) project bootstrapped with `create-t3-app`. | ||||||
| 
 | 
 | ||||||
| ## Features | ## What's next? How do I make an app with this? | ||||||
| 
 | 
 | ||||||
| - User authentication with NextAuth.js | We try to keep this project as simple as possible, so you can start with just the scaffolding we set up for you, and add additional things later when they become necessary. | ||||||
| - File upload and download using MinIO |  | ||||||
| - File metadata management with Prisma |  | ||||||
| - Real-time updates using Server-Sent Events (SSE) |  | ||||||
| - Responsive UI built with Tailwind CSS |  | ||||||
| 
 | 
 | ||||||
| ## Technologies Used | If you are not familiar with the different technologies used in this project, please refer to the respective docs. If you still are in the wind, please join our [Discord](https://t3.gg/discord) and ask for help. | ||||||
| 
 | 
 | ||||||
| - [Next.js](https://nextjs.org) | - [Next.js](https://nextjs.org) | ||||||
| - [NextAuth.js](https://next-auth.js.org) | - [NextAuth.js](https://next-auth.js.org) | ||||||
| - [Prisma](https://prisma.io) | - [Prisma](https://prisma.io) | ||||||
| - [MinIO](https://min.io) | - [Drizzle](https://orm.drizzle.team) | ||||||
| - [Tailwind CSS](https://tailwindcss.com) | - [Tailwind CSS](https://tailwindcss.com) | ||||||
| - [tRPC](https://trpc.io) | - [tRPC](https://trpc.io) | ||||||
| 
 | 
 | ||||||
| ## API Documentation | ## Learn More | ||||||
| 
 | 
 | ||||||
| ### **Download File** | To learn more about the [T3 Stack](https://create.t3.gg/), take a look at the following resources: | ||||||
| 
 | 
 | ||||||
| **Endpoint**: `/api/files/download` | - [Documentation](https://create.t3.gg/) | ||||||
|  | - [Learn the T3 Stack](https://create.t3.gg/en/faq#what-learning-resources-are-currently-available) — Check out these awesome tutorials | ||||||
| 
 | 
 | ||||||
| **Method**: `GET` | You can check out the [create-t3-app GitHub repository](https://github.com/t3-oss/create-t3-app) — your feedback and contributions are welcome! | ||||||
| 
 | 
 | ||||||
| **Query Parameters**: | ## How do I deploy this? | ||||||
| - `fileId` (required): The ID of the file to download. |  | ||||||
| - `fileName` (required): The name of the file to download. |  | ||||||
| 
 | 
 | ||||||
| **Response**: | Follow our deployment guides for [Vercel](https://create.t3.gg/en/deployment/vercel), [Netlify](https://create.t3.gg/en/deployment/netlify) and [Docker](https://create.t3.gg/en/deployment/docker) for more information. | ||||||
| - **200 OK**: Returns the file as a binary stream. |  | ||||||
| - **400 Bad Request**: If `fileId` or `fileName` is missing. |  | ||||||
| - **404 Not Found**: If the file does not exist. |  | ||||||
| - **500 Internal Server Error**: If there is an error fetching the file. |  | ||||||
| 
 |  | ||||||
| --- |  | ||||||
| 
 |  | ||||||
| ### **Serve File** |  | ||||||
| 
 |  | ||||||
| **Endpoint**: `/api/files/serv` |  | ||||||
| 
 |  | ||||||
| **Method**: `GET` |  | ||||||
| 
 |  | ||||||
| **Query Parameters**: |  | ||||||
| - `id` (required): The ID of the file to serve. |  | ||||||
| 
 |  | ||||||
| **Response**: |  | ||||||
| - **200 OK**: Returns the file as a binary stream with the appropriate MIME type. |  | ||||||
| - **400 Bad Request**: If `id` is missing. |  | ||||||
| - **404 Not Found**: If the file does not exist. |  | ||||||
| - **500 Internal Server Error**: If there is an error fetching the file. |  | ||||||
| 
 |  | ||||||
| --- |  | ||||||
| 
 |  | ||||||
| ### **List Files** |  | ||||||
| 
 |  | ||||||
| **Endpoint**: `/api/files` |  | ||||||
| 
 |  | ||||||
| **Method**: `GET` |  | ||||||
| 
 |  | ||||||
| **Response**: |  | ||||||
| - **200 OK**: Returns a list of files with metadata. |  | ||||||
| - **500 Internal Server Error**: If there is an error fetching the files. |  | ||||||
| 
 |  | ||||||
| --- |  | ||||||
| 
 |  | ||||||
| ### **Real-Time Updates** |  | ||||||
| 
 |  | ||||||
| **Endpoint**: `/api/files/stream` |  | ||||||
| 
 |  | ||||||
| **Method**: `GET` |  | ||||||
| 
 |  | ||||||
| **Response**: |  | ||||||
| - **200 OK**: Streams real-time updates for file additions and removals. |  | ||||||
| 
 |  | ||||||
| --- |  | ||||||
| 
 |  | ||||||
| ## How to Run Locally |  | ||||||
| 
 |  | ||||||
| 1. Clone the repository: |  | ||||||
|    ```bash |  | ||||||
|    git clone https://github.com/your-username/file-hosting.git |  | ||||||
|    cd file-hosting |  | ||||||
|    npm install |  | ||||||
|    npm run build |  | ||||||
|    npm run start |  | ||||||
|  | |||||||
| @ -15,17 +15,6 @@ datasource db { | |||||||
|     url      = env("DATABASE_URL") |     url      = env("DATABASE_URL") | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| model Post { |  | ||||||
|     id        Int      @id @default(autoincrement()) |  | ||||||
|     name      String |  | ||||||
|     createdAt DateTime @default(now()) |  | ||||||
|     updatedAt DateTime @updatedAt |  | ||||||
| 
 |  | ||||||
|     createdBy   User   @relation(fields: [createdById], references: [id]) |  | ||||||
|     createdById String |  | ||||||
| 
 |  | ||||||
|     @@index([name]) |  | ||||||
| } |  | ||||||
| 
 | 
 | ||||||
| // Necessary for Next auth | // Necessary for Next auth | ||||||
| model Account { | model Account { | ||||||
| @ -63,7 +52,6 @@ model User { | |||||||
|     image         String? |     image         String? | ||||||
|     accounts      Account[] |     accounts      Account[] | ||||||
|     sessions      Session[] |     sessions      Session[] | ||||||
|     posts         Post[] |  | ||||||
|     files         File[]    // Relation to the File model |     files         File[]    // Relation to the File model | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @ -85,4 +73,5 @@ model File { | |||||||
|     description String @default("") |     description String @default("") | ||||||
|     uploadedBy User?    @relation(fields: [uploadedById], references: [id], onDelete: SetNull) |     uploadedBy User?    @relation(fields: [uploadedById], references: [id], onDelete: SetNull) | ||||||
|     uploadedById String? |     uploadedById String? | ||||||
|  |     public     Boolean  @default(false) // Indicates if the file is public or private | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,4 +1,4 @@ | |||||||
| 'use client'; | "use client"; | ||||||
| import { useRef, useState } from "react"; | import { useRef, useState } from "react"; | ||||||
| import { useFileActions } from "~/app/_components/FileActions"; | import { useFileActions } from "~/app/_components/FileActions"; | ||||||
| 
 | 
 | ||||||
| @ -7,11 +7,13 @@ export function FileActionsContainer({ | |||||||
|   fileName, |   fileName, | ||||||
|   fileUrl, |   fileUrl, | ||||||
|   isOwner, |   isOwner, | ||||||
|  |   isPublic, | ||||||
| }: { | }: { | ||||||
|   fileId: string; |   fileId: string; | ||||||
|   fileName: string; |   fileName: string; | ||||||
|   fileUrl: string; |   fileUrl: string; | ||||||
|   isOwner: boolean; |   isOwner: boolean; | ||||||
|  |   isPublic: boolean; | ||||||
| }) { | }) { | ||||||
|   const { handleDownload, handleCopyUrl, handleRemove} = useFileActions(() => fileId, (description: string) => { |   const { handleDownload, handleCopyUrl, handleRemove} = useFileActions(() => fileId, (description: string) => { | ||||||
|     if (isOwner) { |     if (isOwner) { | ||||||
| @ -36,14 +38,55 @@ export function FileActionsContainer({ | |||||||
|       > |       > | ||||||
|         <img src="/icons/copy.svg" alt="Copy URL" className="w-6 h-6" /> |         <img src="/icons/copy.svg" alt="Copy URL" className="w-6 h-6" /> | ||||||
|       </button> |       </button> | ||||||
| 
 |  | ||||||
|       {/* Remove Button */} |       {/* Remove Button */} | ||||||
|  |       {isOwner && ( | ||||||
|       <button |       <button | ||||||
|         onClick={() => handleRemove(fileId)} |         onClick={() => handleRemove(fileId)} | ||||||
|         className="flex items-center justify-center rounded-full bg-red-500 p-2 hover:bg-red-600" |         className="flex items-center justify-center rounded-full bg-red-500 p-2 hover:bg-red-600" | ||||||
|       > |       > | ||||||
|         <img src="/icons/delete.svg" alt="Remove" className="w-6 h-6" /> |         <img src="/icons/delete.svg" alt="Remove" className="w-6 h-6" /> | ||||||
|       </button> |       </button> | ||||||
|  |       )} | ||||||
|  |       {isOwner && ( | ||||||
|  |         <div className="mt-4 flex items-center gap-2"> | ||||||
|  |         <label className="text-sm"> | ||||||
|  |           Public: | ||||||
|  |         </label> | ||||||
|  |         <label | ||||||
|  |           htmlFor={`public-toggle-${fileId}`} | ||||||
|  |           className="relative inline-flex items-center cursor-pointer" | ||||||
|  |         > | ||||||
|  |           <input | ||||||
|  |             id={`public-toggle-${fileId}`} | ||||||
|  |             type="checkbox" | ||||||
|  |             checked={isPublic} // Ensure this reflects the prop
 | ||||||
|  |             onChange={async (e) => { | ||||||
|  |               const newIsPublic = e.target.checked; | ||||||
|  |               try { | ||||||
|  |                 const response = await fetch(`/api/files/update-public`, { | ||||||
|  |                   method: "POST", | ||||||
|  |                   headers: { | ||||||
|  |                     "Content-Type": "application/json", | ||||||
|  |                   }, | ||||||
|  |                   body: JSON.stringify({ fileId: fileId, isPublic: newIsPublic }), | ||||||
|  |                 }); | ||||||
|  |                 if (!response.ok) { | ||||||
|  |                   throw new Error("Failed to update public status"); | ||||||
|  |                 } | ||||||
|  |                 console.log("Public status updated successfully"); | ||||||
|  |               } catch (err) { | ||||||
|  |                 console.error("Error updating public status:", err); | ||||||
|  |               } | ||||||
|  |             }} | ||||||
|  |             className="sr-only peer" | ||||||
|  |           /> | ||||||
|  |           {/* Toggle Background */} | ||||||
|  |           <div className="w-10 h-5 bg-gray-300 rounded-full peer-checked:bg-green-500 peer-focus:ring-2 peer-focus:ring-green-300 transition-colors"></div> | ||||||
|  |           {/* Toggle Handle */} | ||||||
|  |           <div className="absolute top-0.5 left-0.5 w-4 h-4 bg-white rounded-full shadow-md peer-checked:translate-x-5 transition-transform"></div> | ||||||
|  |         </label> | ||||||
|  |       </div> | ||||||
|  |       )} | ||||||
|     </div> |     </div> | ||||||
|   ); |   ); | ||||||
| } | } | ||||||
|  | |||||||
| @ -15,6 +15,7 @@ interface FileDetails { | |||||||
|   description: string; |   description: string; | ||||||
|   extension: string; |   extension: string; | ||||||
|   isOwner: boolean; // Indicates if the user owns the file
 |   isOwner: boolean; // Indicates if the user owns the file
 | ||||||
|  |   isPublic: boolean; // Indicates if the file is public
 | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| interface FileGridProps { | interface FileGridProps { | ||||||
| @ -116,7 +117,8 @@ export default function FileGrid({ session }: FileGridProps) { | |||||||
|                 fileId={file.id} |                 fileId={file.id} | ||||||
|                 fileName={file.name} |                 fileName={file.name} | ||||||
|                 fileUrl={file.url} |                 fileUrl={file.url} | ||||||
|                 isOwner={file.isOwner} |                 isOwner={true} | ||||||
|  |                 isPublic={file.isPublic} | ||||||
|               /> |               /> | ||||||
|             </div> |             </div> | ||||||
|           </div> |           </div> | ||||||
|  | |||||||
| @ -1,6 +1,6 @@ | |||||||
| import { NextResponse } from "next/server"; | import { NextResponse } from "next/server"; | ||||||
| import { db } from "~/server/db"; | import path from "path"; | ||||||
| import { minioClient } from "~/utils/minioClient"; | import { promises as fs } from "fs"; | ||||||
| 
 | 
 | ||||||
| export async function GET(req: Request) { | export async function GET(req: Request) { | ||||||
|   const url = new URL(req.url); |   const url = new URL(req.url); | ||||||
| @ -10,43 +10,22 @@ export async function GET(req: Request) { | |||||||
|   if (!fileId) { |   if (!fileId) { | ||||||
|     return NextResponse.json({ error: "File id is required" }, { status: 400 }); |     return NextResponse.json({ error: "File id is required" }, { status: 400 }); | ||||||
|   } |   } | ||||||
|   if (!fileName) { |   if (!fileName){ | ||||||
|     return NextResponse.json({ error: "File name is required" }, { status: 400 }); |     return NextResponse.json({ error: "File name is required" }, { status: 400 }); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   try { |   try { | ||||||
|     // Fetch file metadata from the database
 |     const filePath = path.join(process.cwd(), "uploads", fileId); | ||||||
|     const file = await db.file.findFirst({ |     const fileBuffer = await fs.readFile(filePath); | ||||||
|       where: { id: fileId }, |  | ||||||
|     }); |  | ||||||
| 
 | 
 | ||||||
|     if (!file) { |     return new Response(fileBuffer, { | ||||||
|       return NextResponse.json({ error: "File not found" }, { status: 404 }); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     const bucketName = process.env.MINIO_BUCKET || "file-hosting"; |  | ||||||
|     const objectName = `${file.id}-${file.name}`; // Construct the object name in MinIO
 |  | ||||||
| 
 |  | ||||||
|     // Fetch the file from MinIO
 |  | ||||||
|     const stream = await minioClient.getObject(bucketName, objectName); |  | ||||||
| 
 |  | ||||||
|     // Return the file as a binary response
 |  | ||||||
|     const readableStream = new ReadableStream({ |  | ||||||
|       start(controller) { |  | ||||||
|         stream.on("data", (chunk) => controller.enqueue(chunk)); |  | ||||||
|         stream.on("end", () => controller.close()); |  | ||||||
|         stream.on("error", (err) => controller.error(err)); |  | ||||||
|       }, |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     return new Response(readableStream, { |  | ||||||
|       headers: { |       headers: { | ||||||
|         "Content-Type": "application/octet-stream", |         "Content-Type": "application/octet-stream", | ||||||
|         "Content-Disposition": `attachment; filename="${fileName}"`, |         "Content-Disposition": `attachment; filename="${fileName}"`, | ||||||
|       }, |       }, | ||||||
|     }); |     }); | ||||||
|   } catch (error) { |   } catch (error) { | ||||||
|     console.error("Error fetching file from MinIO:", error); |     console.error("Error reading file:", error); | ||||||
|     return NextResponse.json({ error: "Failed to fetch file" }, { status: 500 }); |     return NextResponse.json({ error: "File not found" }, { status: 404 }); | ||||||
|   } |   } | ||||||
| } | } | ||||||
							
								
								
									
										25
									
								
								src/app/api/files/search/route.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								src/app/api/files/search/route.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,25 @@ | |||||||
|  | import { NextResponse } from "next/server"; | ||||||
|  | import { db } from "~/server/db"; | ||||||
|  | 
 | ||||||
|  | export async function GET(req: Request) { | ||||||
|  |   const url = new URL(req.url); | ||||||
|  |   const query = url.searchParams.get("query") || ""; | ||||||
|  | 
 | ||||||
|  |   try { | ||||||
|  |     // if query is empty, return no files
 | ||||||
|  |     const files = await db.file.findMany({ | ||||||
|  |       where: { | ||||||
|  |         OR: [ | ||||||
|  |           { name: { contains: query } }, | ||||||
|  |           { description: { contains: query } }, | ||||||
|  |         ], | ||||||
|  |         public: true, | ||||||
|  |       }, | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     return NextResponse.json({ files }); | ||||||
|  |   } catch (error) { | ||||||
|  |     console.error("Error fetching files:", error); | ||||||
|  |     return NextResponse.json({ error: "Failed to fetch files" }, { status: 500 }); | ||||||
|  |   } | ||||||
|  | } | ||||||
| @ -32,6 +32,7 @@ export async function GET(req: Request) { | |||||||
|       type: file.extension, |       type: file.extension, | ||||||
|       url: file.url, |       url: file.url, | ||||||
|       description: file.description, |       description: file.description, | ||||||
|  |       isPublic: file.public, // Ensure this is included
 | ||||||
|     }); |     }); | ||||||
|   } catch (error) { |   } catch (error) { | ||||||
|     console.error("Error fetching file details:", error); |     console.error("Error fetching file details:", error); | ||||||
|  | |||||||
							
								
								
									
										22
									
								
								src/app/api/files/update-public/route.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								src/app/api/files/update-public/route.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,22 @@ | |||||||
|  | import { NextResponse } from "next/server"; | ||||||
|  | import { db } from "~/server/db"; | ||||||
|  | 
 | ||||||
|  | export async function POST(req: Request) { | ||||||
|  |   try { | ||||||
|  |     const { fileId, isPublic } = await req.json(); | ||||||
|  | 
 | ||||||
|  |     if (!fileId) { | ||||||
|  |       return NextResponse.json({ error: "File ID is required" }, { status: 400 }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     await db.file.update({ | ||||||
|  |       where: { id: fileId }, | ||||||
|  |       data: { public: isPublic }, | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     return NextResponse.json({ success: true }); | ||||||
|  |   } catch (error) { | ||||||
|  |     console.error("Error updating public status:", error); | ||||||
|  |     return NextResponse.json({ error: "Failed to update public status" }, { status: 500 }); | ||||||
|  |   } | ||||||
|  | } | ||||||
| @ -8,14 +8,35 @@ import { Toaster } from "react-hot-toast"; | |||||||
| export default async function Home() { | export default async function Home() { | ||||||
|   const session = await auth(); |   const session = await auth(); | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|   return ( |   return ( | ||||||
|     <HydrateClient> |     <HydrateClient> | ||||||
|       <Toaster position="top-right" reverseOrder={false} /> |       <Toaster position="top-right" reverseOrder={false} /> | ||||||
|       <main className="relative flex min-h-screen flex-col items-center justify-center bg-gradient-to-b from-[#2e026d] to-[#15162c] text-white"> |       <main className="relative flex min-h-screen flex-col items-center justify-center bg-gradient-to-b from-[#2e026d] to-[#15162c] text-white"> | ||||||
|         {/* Top-right corner sign-out button */} |         {/* Top-right corner sign-out button */} | ||||||
|         {session?.user && ( |         {session?.user && ( | ||||||
|           <div className="absolute top-4 right-4"> |           <div className="absolute top-4 right-4 flex items-center gap-4"> | ||||||
|  |             {/* Search Button */} | ||||||
|  |             <Link | ||||||
|  |               href="/search" | ||||||
|  |               className="rounded-full bg-white/10 p-2 transition hover:bg-white/20" | ||||||
|  |               aria-label="Search" | ||||||
|  |             > | ||||||
|  |               <svg | ||||||
|  |                 xmlns="http://www.w3.org/2000/svg" | ||||||
|  |                 className="h-6 w-6 text-white" | ||||||
|  |                 fill="none" | ||||||
|  |                 viewBox="0 0 24 24" | ||||||
|  |                 stroke="currentColor" | ||||||
|  |               > | ||||||
|  |                 <path | ||||||
|  |                   strokeLinecap="round" | ||||||
|  |                   strokeLinejoin="round" | ||||||
|  |                   strokeWidth={2} | ||||||
|  |                   d="M21 21l-4.35-4.35M16.65 10.65a6 6 0 11-12 0 6 6 0 0112 0z" | ||||||
|  |                 /> | ||||||
|  |               </svg> | ||||||
|  |             </Link> | ||||||
|  | 
 | ||||||
|             <Link |             <Link | ||||||
|               href="/api/auth/signout" |               href="/api/auth/signout" | ||||||
|               className="rounded-full bg-white/10 px-4 py-2 font-semibold no-underline transition hover:bg-white/20" |               className="rounded-full bg-white/10 px-4 py-2 font-semibold no-underline transition hover:bg-white/20" | ||||||
| @ -33,7 +54,7 @@ export default async function Home() { | |||||||
|           {session?.user ? ( |           {session?.user ? ( | ||||||
|             <> |             <> | ||||||
|               <FileGrid session={session} /> |               <FileGrid session={session} /> | ||||||
|               <UploadForm/> |               <UploadForm /> | ||||||
|             </> |             </> | ||||||
|           ) : ( |           ) : ( | ||||||
|             <p className="text-center text-2xl text-white"> |             <p className="text-center text-2xl text-white"> | ||||||
| @ -41,16 +62,16 @@ export default async function Home() { | |||||||
|             </p> |             </p> | ||||||
|           )} |           )} | ||||||
|           {!session?.user && ( |           {!session?.user && ( | ||||||
|           <div className="flex flex-col items-center gap-2"> |             <div className="flex flex-col items-center gap-2"> | ||||||
|             <div className="flex flex-col items-center justify-center gap-4"> |               <div className="flex flex-col items-center justify-center gap-4"> | ||||||
|               <Link |                 <Link | ||||||
|                 href={session ? "/api/auth/signout" : "/api/auth/signin"} |                   href={session ? "/api/auth/signout" : "/api/auth/signin"} | ||||||
|                 className="rounded-full bg-white/10 px-10 py-3 font-semibold no-underline transition hover:bg-white/20" |                   className="rounded-full bg-white/10 px-10 py-3 font-semibold no-underline transition hover:bg-white/20" | ||||||
|               > |                 > | ||||||
|                 {session ? "Sign out" : "Sign in"} |                   {session ? "Sign out" : "Sign in"} | ||||||
|               </Link> |                 </Link> | ||||||
|  |               </div> | ||||||
|             </div> |             </div> | ||||||
|           </div> |  | ||||||
|           )} |           )} | ||||||
|         </div> |         </div> | ||||||
|       </main> |       </main> | ||||||
|  | |||||||
							
								
								
									
										121
									
								
								src/app/search/page.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										121
									
								
								src/app/search/page.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,121 @@ | |||||||
|  | "use client"; | ||||||
|  | import { useEffect, useState } from "react"; | ||||||
|  | import { useRouter } from "next/navigation"; | ||||||
|  | import { env } from "~/env.js"; | ||||||
|  | import { FilePreview } from "~/app/_components/FilePreview"; | ||||||
|  | import { FileActionsContainer } from "~/app/_components/ActionButtons"; | ||||||
|  | import { HomeButton } from "~/app/_components/HomeButton"; | ||||||
|  | 
 | ||||||
|  | interface FileDetails { | ||||||
|  |   name: string; | ||||||
|  |   size: number; | ||||||
|  |   owner: string; | ||||||
|  |   ownerAvatar: string | null; | ||||||
|  |   uploadDate: string; | ||||||
|  |   id: string; | ||||||
|  |   isOwner: boolean; | ||||||
|  |   extension: string; | ||||||
|  |   url: string; | ||||||
|  |   description: string; | ||||||
|  |   isPublic: boolean; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export default function SearchFile() { | ||||||
|  |   const [searchQuery, setSearchQuery] = useState<string>(""); | ||||||
|  |   const [files, setFiles] = useState<FileDetails[]>([]); | ||||||
|  |   const [error, setError] = useState<string | null>(null); | ||||||
|  |   const pageUrl = env.NEXT_PUBLIC_PAGE_URL; | ||||||
|  |   const router = useRouter(); | ||||||
|  | 
 | ||||||
|  |   useEffect(() => { | ||||||
|  |     const fetchFiles = async () => { | ||||||
|  |       try { | ||||||
|  |         const response = await fetch( | ||||||
|  |           `/api/files/search?query=${encodeURIComponent(searchQuery)}` | ||||||
|  |         ); | ||||||
|  |         if (!response.ok) { | ||||||
|  |           throw new Error("Failed to fetch files"); | ||||||
|  |         } | ||||||
|  |         const data = await response.json(); | ||||||
|  |         setFiles(data.files); | ||||||
|  |       } catch (err) { | ||||||
|  |         console.error(err); | ||||||
|  |         setError("Failed to load files."); | ||||||
|  |       } | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     fetchFiles(); | ||||||
|  |   }, [searchQuery]); | ||||||
|  | 
 | ||||||
|  |   return ( | ||||||
|  |     <main className="flex min-h-screen flex-col items-center bg-gradient-to-b from-[#2e026d] to-[#15162c] text-white"> | ||||||
|  |       <div className="absolute top-4 left-4 z-20"> | ||||||
|  |         <HomeButton /> | ||||||
|  |       </div> | ||||||
|  |       {/* Search Bar */} | ||||||
|  |       <div className="sticky top-0 z-10 w-full bg-[#2e026d] px-4 py-4 shadow-md"> | ||||||
|  |         <div className="relative w-full max-w-md mx-auto"> | ||||||
|  |           <input | ||||||
|  |             type="text" | ||||||
|  |             placeholder="Search files..." | ||||||
|  |             value={searchQuery} | ||||||
|  |             onChange={(e) => setSearchQuery(e.target.value)} | ||||||
|  |             className="w-full p-3 pl-12 text-white bg-[#3b0764] rounded-full shadow-md focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent placeholder-gray-400" | ||||||
|  |           /> | ||||||
|  |           <svg | ||||||
|  |             xmlns="http://www.w3.org/2000/svg" | ||||||
|  |             className="absolute left-4 top-1/2 transform -translate-y-1/2 h-6 w-6 text-gray-400" | ||||||
|  |             fill="none" | ||||||
|  |             viewBox="0 0 24 24" | ||||||
|  |             stroke="currentColor" | ||||||
|  |           > | ||||||
|  |             <path | ||||||
|  |               strokeLinecap="round" | ||||||
|  |               strokeLinejoin="round" | ||||||
|  |               strokeWidth={2} | ||||||
|  |               d="M21 21l-4.35-4.35M16.65 10.65a6 6 0 11-12 0 6 6 0 0112 0z" | ||||||
|  |             /> | ||||||
|  |           </svg> | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|  | 
 | ||||||
|  |       {/* File Grid */} | ||||||
|  |       <div className="flex-grow flex flex-col items-center justify-center w-full px-4"> | ||||||
|  |         {error && <div className="text-red-500">{error}</div>} | ||||||
|  |         <div className="grid grid-cols-1 gap-6 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 w-full max-w-7xl"> | ||||||
|  |           {files.map((file) => { | ||||||
|  |             return ( | ||||||
|  |               <div | ||||||
|  |                 key={file.id} | ||||||
|  |                 className="flex place-content-end w-xxs flex-col gap-4 rounded-xl bg-white/10 p-4 hover:bg-white/20" | ||||||
|  |               > | ||||||
|  |                 <div className="self-center max-w-100 sm:max-w-50"> | ||||||
|  |                   <FilePreview fileId={file.id} fileType={file.extension} /> | ||||||
|  |                 </div> | ||||||
|  | 
 | ||||||
|  |                 <button onClick={() => router.push(pageUrl + file.url)}> | ||||||
|  |                   <h3 className="text-2xl font-bold">{file.name}</h3> | ||||||
|  |                 </button> | ||||||
|  |                 {file.description && ( | ||||||
|  |                   <p className="text-sm text-gray-400"> | ||||||
|  |                     Description: {file.description} | ||||||
|  |                   </p> | ||||||
|  |                 )} | ||||||
|  | 
 | ||||||
|  |                 <div className="flex self-center gap-2"> | ||||||
|  |                   <FileActionsContainer | ||||||
|  |                     fileId={file.id} | ||||||
|  |                     fileName={file.name} | ||||||
|  |                     fileUrl={file.url} | ||||||
|  |                     isOwner={false} // Check if the user is the owner
 | ||||||
|  |                     isPublic={file.isPublic} // Check if the file is public
 | ||||||
|  |                   /> | ||||||
|  |                 </div> | ||||||
|  |               </div> | ||||||
|  |             ); | ||||||
|  |           })} | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|  |     </main> | ||||||
|  |   ); | ||||||
|  | } | ||||||
| @ -2,11 +2,8 @@ import { notFound } from "next/navigation"; | |||||||
| import { FilePreview } from "~/app/_components/FilePreview"; | import { FilePreview } from "~/app/_components/FilePreview"; | ||||||
| import { HomeButton } from "~/app/_components/HomeButton"; // Import the client component
 | import { HomeButton } from "~/app/_components/HomeButton"; // Import the client component
 | ||||||
| import { Toaster } from "react-hot-toast"; | import { Toaster } from "react-hot-toast"; | ||||||
| import { | import { FileActionsContainer, FileDescriptionContainer } from "~/app/_components/ActionButtons"; // Import the client component
 | ||||||
|   FileActionsContainer, | import Head from "next/head"; | ||||||
|   FileDescriptionContainer, |  | ||||||
| } from "~/app/_components/ActionButtons"; // Import the client component
 |  | ||||||
| import type { Metadata } from "next"; |  | ||||||
| 
 | 
 | ||||||
| interface FileDetails { | interface FileDetails { | ||||||
|   name: string; |   name: string; | ||||||
| @ -19,47 +16,7 @@ interface FileDetails { | |||||||
|   type: string; |   type: string; | ||||||
|   url: string; |   url: string; | ||||||
|   description: string; |   description: string; | ||||||
| } |   isPublic: boolean; | ||||||
| 
 |  | ||||||
| export async function generateMetadata({ |  | ||||||
|   searchParams, |  | ||||||
| }: { |  | ||||||
|   searchParams: Promise<{ id?: string }>; |  | ||||||
| }): Promise<Metadata> { |  | ||||||
|   const resolvedSearchParams = await searchParams; // Resolve the promise
 |  | ||||||
|   const fileId = resolvedSearchParams.id; |  | ||||||
| 
 |  | ||||||
|   if (!fileId) { |  | ||||||
|     return { |  | ||||||
|       title: "File Not Found", |  | ||||||
|       description: "The file you are looking for does not exist.", |  | ||||||
|     }; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   const fileDetails = await fetchFileDetails(fileId); |  | ||||||
| 
 |  | ||||||
|   if (!fileDetails) { |  | ||||||
|     return { |  | ||||||
|       title: "File Not Found", |  | ||||||
|       description: "The file you are looking for does not exist.", |  | ||||||
|     }; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   return { |  | ||||||
|     title: fileDetails.name, |  | ||||||
|     description: fileDetails.description || fileDetails.name, |  | ||||||
|     openGraph: { |  | ||||||
|       title: fileDetails.name, |  | ||||||
|       description: fileDetails.description || fileDetails.name, |  | ||||||
|       url: `${process.env.NEXT_PUBLIC_PAGE_URL}/share?id=${fileDetails.id}`, |  | ||||||
|       images: [ |  | ||||||
|         { |  | ||||||
|           url: `${process.env.NEXT_PUBLIC_PAGE_URL}/api/files/serv?id=${fileDetails.id}`, |  | ||||||
|           alt: `${fileDetails.name} preview`, |  | ||||||
|         }, |  | ||||||
|       ], |  | ||||||
|     }, |  | ||||||
|   }; |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| async function fetchFileDetails(fileId: string): Promise<FileDetails | null> { | async function fetchFileDetails(fileId: string): Promise<FileDetails | null> { | ||||||
| @ -95,6 +52,7 @@ export default async function FilePreviewContainer({ | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   const fileDetails = await fetchFileDetails(fileId); |   const fileDetails = await fetchFileDetails(fileId); | ||||||
|  |    | ||||||
| 
 | 
 | ||||||
|   if (!fileDetails) { |   if (!fileDetails) { | ||||||
|     return ( |     return ( | ||||||
| @ -113,64 +71,98 @@ export default async function FilePreviewContainer({ | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   return ( |   return ( | ||||||
|     <main className="flex min-h-screen flex-col items-center justify-center bg-gradient-to-b from-[#2e026d] to-[#15162c] text-white"> |     <> | ||||||
|       <div className="absolute top-4 left-4"> |       <Head> | ||||||
|         <HomeButton /> |         <title>{fileDetails.name} - File Details</title> | ||||||
|       </div> |         <meta | ||||||
|       <Toaster position="top-right" reverseOrder={false} /> |           property="og:title" | ||||||
|       <div className="container flex flex-col items-center gap-12 px-4 py-16"> |           content={`${fileDetails.name} - File Details`} | ||||||
|         <h1 className="text-5xl font-extrabold tracking-tight sm:text-[5rem]"> |         /> | ||||||
|           <span className="text-[hsl(280,100%,70%)]">File</span> Details |         <meta | ||||||
|         </h1> |           property="og:description" | ||||||
|         <div className="mt-6"> |           content={`Size: ${ | ||||||
|           {fileDetails.type !== "unknown" && ( |             fileDetails.size > 1024 * 1024 * 1024 | ||||||
|             <FilePreview fileId={fileDetails.id} fileType={fileDetails.type} /> |  | ||||||
|           )} |  | ||||||
|         </div> |  | ||||||
|         <div className="w-full max-w-md rounded-lg bg-white/10 p-6 text-white shadow-md"> |  | ||||||
|           <p> |  | ||||||
|             <strong>Name:</strong> {fileDetails.name} |  | ||||||
|           </p> |  | ||||||
|           <p> |  | ||||||
|             <strong>Size:</strong>{" "} |  | ||||||
|             {fileDetails.size > 1024 * 1024 * 1024 |  | ||||||
|               ? (fileDetails.size / (1024 * 1024 * 1024)).toFixed(2) + " GB" |               ? (fileDetails.size / (1024 * 1024 * 1024)).toFixed(2) + " GB" | ||||||
|               : fileDetails.size > 1024 * 1024 |               : fileDetails.size > 1024 * 1024 | ||||||
|               ? (fileDetails.size / (1024 * 1024)).toFixed(2) + " MB" |               ? (fileDetails.size / (1024 * 1024)).toFixed(2) + " MB" | ||||||
|               : fileDetails.size > 1024 |               : fileDetails.size > 1024 | ||||||
|               ? (fileDetails.size / 1024).toFixed(2) + " KB" |               ? (fileDetails.size / 1024).toFixed(2) + " KB" | ||||||
|               : fileDetails.size + " Bytes"} |               : fileDetails.size + " Bytes" | ||||||
|           </p> |           }, Owner: ${fileDetails.owner}, Uploaded on: ${new Date( | ||||||
|           <p> |             fileDetails.uploadDate | ||||||
|             <strong>Owner:</strong>{" "} |           ).toLocaleString()}`}
 | ||||||
|             <img |         /> | ||||||
|               className="inline size-5 rounded-md" |         <meta property="og:type" content="website" /> | ||||||
|               src={fileDetails.ownerAvatar || ""} |         <meta | ||||||
|               alt="Owner avatar" |           property="og:image" | ||||||
|             />{" "} |           content={`${process.env.NEXT_PUBLIC_PAGE_URL}/api/files/serv?id=${fileId}`} | ||||||
|             {fileDetails.owner} |         /> | ||||||
|           </p> |         <meta property="og:image:alt" content={`${fileDetails.name} preview`} /> | ||||||
|           <p> |         <meta | ||||||
|             <strong>Upload Date:</strong>{" "} |           property="og:url" | ||||||
|             {new Date(fileDetails.uploadDate).toLocaleString()} |           content={`${process.env.NEXT_PUBLIC_PAGE_URL}/share?id=${fileId}`} | ||||||
|           </p> |         /> | ||||||
|           <div> |       </Head> | ||||||
|             <strong>Description:</strong>{" "} |       <main className="flex min-h-screen flex-col items-center justify-center bg-gradient-to-b from-[#2e026d] to-[#15162c] text-white"> | ||||||
|             <FileDescriptionContainer |         <div className="absolute top-4 left-4"> | ||||||
|               fileId={fileDetails.id} |           <HomeButton /> {/* Use the client component */} | ||||||
|               fileDescription={fileDetails.description} |         </div> | ||||||
|             /> |         <div className="container flex flex-col items-center gap-12 px-4 py-16"> | ||||||
|  |           <h1 className="text-5xl font-extrabold tracking-tight sm:text-[5rem]"> | ||||||
|  |             <span className="text-[hsl(280,100%,70%)]">File</span> Details | ||||||
|  |           </h1> | ||||||
|  |           <div className="mt-6"> | ||||||
|  |             {fileDetails.type !== "unknown" && ( | ||||||
|  |               <FilePreview | ||||||
|  |                 fileId={fileDetails.id} | ||||||
|  |                 fileType={fileDetails.type} | ||||||
|  |               /> | ||||||
|  |             )} | ||||||
|           </div> |           </div> | ||||||
|           <div className="mt-4 flex justify-center"> |           <div className="bg-white/10 shadow-md rounded-lg p-6 w-full max-w-md text-white"> | ||||||
|             <FileActionsContainer |             <p> | ||||||
|               fileId={fileDetails.id} |               <strong>Name:</strong> {fileDetails.name} | ||||||
|               fileName={fileDetails.name} |             </p> | ||||||
|               fileUrl={fileDetails.url} |             <p> | ||||||
|               isOwner={fileDetails.isOwner} |               <strong>Size:</strong>{" "} | ||||||
|             /> |               {fileDetails.size > 1024 * 1024 * 1024 | ||||||
|  |                 ? (fileDetails.size / (1024 * 1024 * 1024)).toFixed(2) + " GB" | ||||||
|  |                 : fileDetails.size > 1024 * 1024 | ||||||
|  |                 ? (fileDetails.size / (1024 * 1024)).toFixed(2) + " MB" | ||||||
|  |                 : fileDetails.size > 1024 | ||||||
|  |                 ? (fileDetails.size / 1024).toFixed(2) + " KB" | ||||||
|  |                 : fileDetails.size + " Bytes"} | ||||||
|  |             </p> | ||||||
|  |             <p> | ||||||
|  |               <strong>Owner:</strong>{" "} | ||||||
|  |               <img | ||||||
|  |                 className="rounded-md inline size-5" | ||||||
|  |                 src={fileDetails.ownerAvatar || ""} | ||||||
|  |                 alt="Owner avatar" | ||||||
|  |               />{" "} | ||||||
|  |               {fileDetails.owner} | ||||||
|  |             </p> | ||||||
|  |             <p> | ||||||
|  |               <strong>Upload Date:</strong>{" "} | ||||||
|  |               {new Date(fileDetails.uploadDate).toLocaleString()} | ||||||
|  |             </p> | ||||||
|  |             <div> | ||||||
|  |               <strong>Description:</strong>{" "} | ||||||
|  |               <FileDescriptionContainer fileId={fileDetails.id} fileDescription={fileDetails.description}/> | ||||||
|  |             </div> | ||||||
|  |             <div className="mt-4 flex justify-center"> | ||||||
|  |               <FileActionsContainer | ||||||
|  |                 fileId={fileDetails.id} | ||||||
|  |                 fileName={fileDetails.name} | ||||||
|  |                 fileUrl={fileDetails.url} | ||||||
|  |                 isOwner={fileDetails.isOwner} | ||||||
|  |                 isPublic={fileDetails.isPublic} | ||||||
|  |               /> | ||||||
|  |             </div> | ||||||
|           </div> |           </div> | ||||||
|         </div> |         </div> | ||||||
|       </div> |       </main> | ||||||
|     </main> |     </> | ||||||
|   ); |   ); | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | |||||||
| @ -18,20 +18,18 @@ export const postRouter = createTRPCRouter({ | |||||||
|   create: protectedProcedure |   create: protectedProcedure | ||||||
|     .input(z.object({ name: z.string().min(1) })) |     .input(z.object({ name: z.string().min(1) })) | ||||||
|     .mutation(async ({ ctx, input }) => { |     .mutation(async ({ ctx, input }) => { | ||||||
|       return ctx.db.file.create({ |       return ctx.db.post.create({ | ||||||
|         data: { |         data: { | ||||||
|           name: input.name, |           name: input.name, | ||||||
|           url: "default-url", // Replace with actual URL logic
 |           createdBy: { connect: { id: ctx.session.user.id } }, | ||||||
|           size: 0, // Replace with actual size logic
 |  | ||||||
|           extension: "txt", // Replace with actual extension logic
 |  | ||||||
|         }, |         }, | ||||||
|       }); |       }); | ||||||
|     }), |     }), | ||||||
| 
 | 
 | ||||||
|   getLatest: protectedProcedure.query(async ({ ctx }) => { |   getLatest: protectedProcedure.query(async ({ ctx }) => { | ||||||
|     const post = await ctx.db.file.findFirst({ |     const post = await ctx.db.post.findFirst({ | ||||||
|       orderBy: { uploadDate: "desc" }, // Replace 'createdAt' with the correct field name from your schema
 |       orderBy: { createdAt: "desc" }, | ||||||
|       where: { uploadedById: ctx.session.user.id }, |       where: { createdBy: { id: ctx.session.user.id } }, | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     return post ?? null; |     return post ?? null; | ||||||
|  | |||||||
| @ -1,6 +0,0 @@ | |||||||
| module.exports = { |  | ||||||
|   content: [ |  | ||||||
|     "./src/**/*.{js,ts,jsx,tsx}", // Ensure your paths are correct
 |  | ||||||
|   ], |  | ||||||
|   plugins: [require("@tailwindcss/typography")], |  | ||||||
| }; |  | ||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user