Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
85fa1942e9
|
||
|
|
c9274a0caa | ||
|
|
58853f38c4 | ||
|
|
1884189fb2 | ||
|
|
118cadefdf
|
||
|
|
b14acab08f
|
||
|
|
60925ee6ac
|
||
|
|
036f23b777
|
||
|
|
b368473216
|
||
|
|
551cf2e2cb
|
||
|
|
bb89a84ba8
|
||
|
|
90329f6b22
|
||
|
|
d4661dc8e3
|
||
|
|
24d90a9412
|
||
|
|
6115851147
|
||
|
|
011695cf46
|
||
|
|
9f04130726
|
||
|
|
82d1e74af1
|
||
|
|
453264d9cd
|
||
|
|
b63e3ae77f | ||
|
|
99397e774c
|
||
|
|
792b0eb275
|
||
|
|
8798764b89
|
||
|
|
f2d2268039
|
||
|
|
ad24c76a70
|
||
|
|
af978c98b8
|
||
|
|
75d1187255
|
||
|
|
1d302eb217
|
||
|
|
344334592e
|
||
|
|
8bc35d979d
|
@@ -1,29 +1,90 @@
|
|||||||
# Create T3 App
|
# File Hosting App
|
||||||
|
|
||||||
This is a [T3 Stack](https://create.t3.gg/) project bootstrapped with `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.
|
||||||
|
|
||||||
## What's next? How do I make an app with this?
|
## Features
|
||||||
|
|
||||||
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.
|
- User authentication with NextAuth.js
|
||||||
|
- 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
|
||||||
|
|
||||||
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.
|
## Technologies Used
|
||||||
|
|
||||||
- [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)
|
||||||
- [Drizzle](https://orm.drizzle.team)
|
- [MinIO](https://min.io)
|
||||||
- [Tailwind CSS](https://tailwindcss.com)
|
- [Tailwind CSS](https://tailwindcss.com)
|
||||||
- [tRPC](https://trpc.io)
|
- [tRPC](https://trpc.io)
|
||||||
|
|
||||||
## Learn More
|
## API Documentation
|
||||||
|
|
||||||
To learn more about the [T3 Stack](https://create.t3.gg/), take a look at the following resources:
|
### **Download File**
|
||||||
|
|
||||||
- [Documentation](https://create.t3.gg/)
|
**Endpoint**: `/api/files/download`
|
||||||
- [Learn the T3 Stack](https://create.t3.gg/en/faq#what-learning-resources-are-currently-available) — Check out these awesome tutorials
|
|
||||||
|
|
||||||
You can check out the [create-t3-app GitHub repository](https://github.com/t3-oss/create-t3-app) — your feedback and contributions are welcome!
|
**Method**: `GET`
|
||||||
|
|
||||||
## How do I deploy this?
|
**Query Parameters**:
|
||||||
|
- `fileId` (required): The ID of the file to download.
|
||||||
|
- `fileName` (required): The name of the file to download.
|
||||||
|
|
||||||
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.
|
**Response**:
|
||||||
|
- **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
|
||||||
|
|||||||
@@ -23,16 +23,29 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@auth/prisma-adapter": "^2.7.2",
|
"@auth/prisma-adapter": "^2.7.2",
|
||||||
"@prisma/client": "^6.5.0",
|
"@prisma/client": "^6.5.0",
|
||||||
|
"@prisma/extension-accelerate": "^2.0.0",
|
||||||
"@t3-oss/env-nextjs": "^0.12.0",
|
"@t3-oss/env-nextjs": "^0.12.0",
|
||||||
|
"@tailwindcss/typography": "^0.5.16",
|
||||||
"@tanstack/react-query": "^5.69.0",
|
"@tanstack/react-query": "^5.69.0",
|
||||||
"@trpc/client": "^11.0.0",
|
"@trpc/client": "^11.0.0",
|
||||||
"@trpc/react-query": "^11.0.0",
|
"@trpc/react-query": "^11.0.0",
|
||||||
"@trpc/server": "^11.0.0",
|
"@trpc/server": "^11.0.0",
|
||||||
|
"cuid": "^3.0.0",
|
||||||
|
"dompurify": "^3.2.5",
|
||||||
|
"github-markdown-css": "^5.8.1",
|
||||||
|
"gray-matter": "^4.0.3",
|
||||||
|
"mermaid": "^11.6.0",
|
||||||
|
"minio": "^8.0.5",
|
||||||
"next": "^15.2.3",
|
"next": "^15.2.3",
|
||||||
"next-auth": "5.0.0-beta.25",
|
"next-auth": "5.0.0-beta.25",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"react-hot-toast": "^2.5.2",
|
"react-hot-toast": "^2.5.2",
|
||||||
|
"react-markdown": "^10.1.0",
|
||||||
|
"rehype-raw": "^7.0.0",
|
||||||
|
"remark": "^15.0.1",
|
||||||
|
"remark-gfm": "^4.0.1",
|
||||||
|
"remark-html": "^16.0.1",
|
||||||
"server-only": "^0.0.1",
|
"server-only": "^0.0.1",
|
||||||
"superjson": "^2.2.1",
|
"superjson": "^2.2.1",
|
||||||
"zod": "^3.24.2"
|
"zod": "^3.24.2"
|
||||||
@@ -41,6 +54,7 @@
|
|||||||
"@eslint/eslintrc": "^3.3.1",
|
"@eslint/eslintrc": "^3.3.1",
|
||||||
"@tailwindcss/postcss": "^4.0.15",
|
"@tailwindcss/postcss": "^4.0.15",
|
||||||
"@types/busboy": "^1.5.4",
|
"@types/busboy": "^1.5.4",
|
||||||
|
"@types/mime-types": "^2.1.4",
|
||||||
"@types/node": "^20.14.10",
|
"@types/node": "^20.14.10",
|
||||||
"@types/react": "^19.0.0",
|
"@types/react": "^19.0.0",
|
||||||
"@types/react-dom": "^19.0.0",
|
"@types/react-dom": "^19.0.0",
|
||||||
|
|||||||
@@ -4,10 +4,11 @@
|
|||||||
generator client {
|
generator client {
|
||||||
provider = "prisma-client-js"
|
provider = "prisma-client-js"
|
||||||
binaryTargets = ["native", "debian-openssl-3.0.x"]
|
binaryTargets = ["native", "debian-openssl-3.0.x"]
|
||||||
|
output = "/app/generated/prisma-client"
|
||||||
}
|
}
|
||||||
|
|
||||||
datasource db {
|
datasource db {
|
||||||
provider = "mysql"
|
provider = "postgresql"
|
||||||
// NOTE: When using mysql or sqlserver, uncomment the @db.Text annotations in model Account below
|
// NOTE: When using mysql or sqlserver, uncomment the @db.Text annotations in model Account below
|
||||||
// Further reading:
|
// Further reading:
|
||||||
// https://next-auth.js.org/adapters/prisma#create-the-prisma-schema
|
// https://next-auth.js.org/adapters/prisma#create-the-prisma-schema
|
||||||
@@ -15,17 +16,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 {
|
||||||
@@ -34,7 +24,7 @@ model Account {
|
|||||||
type String
|
type String
|
||||||
provider String
|
provider String
|
||||||
providerAccountId String
|
providerAccountId String
|
||||||
refresh_token String? @db.Text
|
refresh_token String? //@db.Text
|
||||||
access_token String? // @db.Text
|
access_token String? // @db.Text
|
||||||
expires_at Int?
|
expires_at Int?
|
||||||
token_type String?
|
token_type String?
|
||||||
@@ -63,7 +53,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 +74,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
|
||||||
}
|
}
|
||||||
|
|||||||
|
After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 5.2 KiB |
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||||
|
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M8 5.00005C7.01165 5.00082 6.49359 5.01338 6.09202 5.21799C5.71569 5.40973 5.40973 5.71569 5.21799 6.09202C5 6.51984 5 7.07989 5 8.2V17.8C5 18.9201 5 19.4802 5.21799 19.908C5.40973 20.2843 5.71569 20.5903 6.09202 20.782C6.51984 21 7.07989 21 8.2 21H15.8C16.9201 21 17.4802 21 17.908 20.782C18.2843 20.5903 18.5903 20.2843 18.782 19.908C19 19.4802 19 18.9201 19 17.8V8.2C19 7.07989 19 6.51984 18.782 6.09202C18.5903 5.71569 18.2843 5.40973 17.908 5.21799C17.5064 5.01338 16.9884 5.00082 16 5.00005M8 5.00005V7H16V5.00005M8 5.00005V4.70711C8 4.25435 8.17986 3.82014 8.5 3.5C8.82014 3.17986 9.25435 3 9.70711 3H14.2929C14.7456 3 15.1799 3.17986 15.5 3.5C15.8201 3.82014 16 4.25435 16 4.70711V5.00005M15 12H12M15 16H12M9 12H9.01M9 16H9.01" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||||
|
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M18 6L17.1991 18.0129C17.129 19.065 17.0939 19.5911 16.8667 19.99C16.6666 20.3412 16.3648 20.6235 16.0011 20.7998C15.588 21 15.0607 21 14.0062 21H9.99377C8.93927 21 8.41202 21 7.99889 20.7998C7.63517 20.6235 7.33339 20.3412 7.13332 19.99C6.90607 19.5911 6.871 19.065 6.80086 18.0129L6 6M4 6H20M16 6L15.7294 5.18807C15.4671 4.40125 15.3359 4.00784 15.0927 3.71698C14.8779 3.46013 14.6021 3.26132 14.2905 3.13878C13.9376 3 13.523 3 12.6936 3H11.3064C10.477 3 10.0624 3 9.70951 3.13878C9.39792 3.26132 9.12208 3.46013 8.90729 3.71698C8.66405 4.00784 8.53292 4.40125 8.27064 5.18807L8 6M14 10V17M10 10V17" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 921 B |
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||||
|
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M17 17H17.01M17.4 14H18C18.9319 14 19.3978 14 19.7654 14.1522C20.2554 14.3552 20.6448 14.7446 20.8478 15.2346C21 15.6022 21 16.0681 21 17C21 17.9319 21 18.3978 20.8478 18.7654C20.6448 19.2554 20.2554 19.6448 19.7654 19.8478C19.3978 20 18.9319 20 18 20H6C5.06812 20 4.60218 20 4.23463 19.8478C3.74458 19.6448 3.35523 19.2554 3.15224 18.7654C3 18.3978 3 17.9319 3 17C3 16.0681 3 15.6022 3.15224 15.2346C3.35523 14.7446 3.74458 14.3552 4.23463 14.1522C4.60218 14 5.06812 14 6 14H6.6M12 15V4M12 15L9 12M12 15L15 12" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 831 B |
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||||
|
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M9 14H15M4.6 10H19.4C19.9601 10 20.2401 10 20.454 9.89101C20.6422 9.79513 20.7951 9.64215 20.891 9.45399C21 9.24008 21 8.96005 21 8.4V5.6C21 5.03995 21 4.75992 20.891 4.54601C20.7951 4.35785 20.6422 4.20487 20.454 4.10899C20.2401 4 19.9601 4 19.4 4H4.6C4.03995 4 3.75992 4 3.54601 4.10899C3.35785 4.20487 3.20487 4.35785 3.10899 4.54601C3 4.75992 3 5.03995 3 5.6V8.4C3 8.96005 3 9.24008 3.10899 9.45399C3.20487 9.64215 3.35785 9.79513 3.54601 9.89101C3.75992 10 4.03995 10 4.6 10ZM5 10H19V16.8C19 17.9201 19 18.4802 18.782 18.908C18.5903 19.2843 18.2843 19.5903 17.908 19.782C17.4802 20 16.9201 20 15.8 20H8.2C7.07989 20 6.51984 20 6.09202 19.782C5.71569 19.5903 5.40973 19.2843 5.21799 18.908C5 18.4802 5 17.9201 5 16.8V10Z" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||||
|
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M7 8L3 11.6923L7 16M17 8L21 11.6923L17 16M14 4L10 20" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 373 B |
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||||
|
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M9 17H15M9 13H15M9 9H10M13 3H8.2C7.0799 3 6.51984 3 6.09202 3.21799C5.71569 3.40973 5.40973 3.71569 5.21799 4.09202C5 4.51984 5 5.0799 5 6.2V17.8C5 18.9201 5 19.4802 5.21799 19.908C5.40973 20.2843 5.71569 20.5903 6.09202 20.782C6.51984 21 7.0799 21 8.2 21H15.8C16.9201 21 17.4802 21 17.908 20.782C18.2843 20.5903 18.5903 20.2843 18.782 19.908C19 19.4802 19 18.9201 19 17.8V9M13 3L19 9M13 3V7.4C13 7.96005 13 8.24008 13.109 8.45399C13.2049 8.64215 13.3578 8.79513 13.546 8.89101C13.7599 9 14.0399 9 14.6 9H19" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 828 B |
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||||
|
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M14 21.0001V15.0001H10V21.0001M19 9.77818V16.2001C19 17.8802 19 18.7203 18.673 19.362C18.3854 19.9265 17.9265 20.3855 17.362 20.6731C16.7202 21.0001 15.8802 21.0001 14.2 21.0001H9.8C8.11984 21.0001 7.27976 21.0001 6.63803 20.6731C6.07354 20.3855 5.6146 19.9265 5.32698 19.362C5 18.7203 5 17.8802 5 16.2001V9.77753M21 12.0001L15.5668 5.96405C14.3311 4.59129 13.7133 3.9049 12.9856 3.65151C12.3466 3.42894 11.651 3.42899 11.0119 3.65165C10.2843 3.90516 9.66661 4.59163 8.43114 5.96458L3 12.0001" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 813 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12,17A2,2 0 0,0 14,15C14,13.89 13.1,13 12,13A2,2 0 0,0 10,15A2,2 0 0,0 12,17M18,8A2,2 0 0,1 20,10V20A2,2 0 0,1 18,22H6A2,2 0 0,1 4,20V10C4,8.89 4.9,8 6,8H7V6A5,5 0 0,1 12,1A5,5 0 0,1 17,6V8H18M12,3A3,3 0 0,0 9,6V8H15V6A3,3 0 0,0 12,3Z" /></svg>
|
||||||
|
After Width: | Height: | Size: 314 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M17.9,17.39C17.64,16.59 16.89,16 16,16H15V13A1,1 0 0,0 14,12H8V10H10A1,1 0 0,0 11,9V7H13A2,2 0 0,0 15,5V4.59C17.93,5.77 20,8.64 20,12C20,14.08 19.2,15.97 17.9,17.39M11,19.93C7.05,19.44 4,16.08 4,12C4,11.38 4.08,10.78 4.21,10.21L9,15V16A2,2 0 0,0 11,18M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z" /></svg>
|
||||||
|
After Width: | Height: | Size: 406 B |
@@ -0,0 +1,70 @@
|
|||||||
|
import React, { Suspense } from "react";
|
||||||
|
import { HomeButton } from "~/app/_components/HomeButton"; // Import the client component
|
||||||
|
import { Toaster } from "react-hot-toast";
|
||||||
|
import {
|
||||||
|
FileActionsContainer,
|
||||||
|
} from "~/app/_components/ActionButtons"; // Import the client component
|
||||||
|
|
||||||
|
const LoadingSkeleton: React.FC = () => (
|
||||||
|
<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">
|
||||||
|
<HomeButton />
|
||||||
|
</div>
|
||||||
|
<Toaster position="top-right" reverseOrder={false} />
|
||||||
|
<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">
|
||||||
|
<svg
|
||||||
|
className="h-6 w-6 animate-spin text-white/70"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<circle
|
||||||
|
className="opacity-25"
|
||||||
|
cx="12"
|
||||||
|
cy="12"
|
||||||
|
r="10"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="4"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
className="opacity-75"
|
||||||
|
fill="currentColor"
|
||||||
|
d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div className="w-full max-w-md rounded-lg bg-white/10 p-6 text-white shadow-md">
|
||||||
|
<p>
|
||||||
|
<strong>Name:</strong> <span className="inline-block h-6 w-24 rounded bg-white/20 animate-pulse align-middle ml-2" />
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>Size:</strong> <span className="inline-block h-6 w-16 rounded bg-white/20 animate-pulse align-middle ml-2" />
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>Owner:</strong> <span className="inline-block h-6 w-20 rounded bg-white/20 animate-pulse align-middle ml-2" />
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>Upload Date:</strong> <span className="inline-block h-6 w-28 rounded bg-white/20 animate-pulse align-middle ml-2" />
|
||||||
|
</p>
|
||||||
|
<div>
|
||||||
|
<strong>Description:</strong> <span className="inline-block h-6 w-40 rounded bg-white/20 animate-pulse align-middle ml-2" />
|
||||||
|
</div>
|
||||||
|
<div className="mt-4 flex justify-center">
|
||||||
|
<FileActionsContainer
|
||||||
|
fileId={""}
|
||||||
|
fileName={""}
|
||||||
|
fileUrl={""}
|
||||||
|
isOwner={false}
|
||||||
|
isPublic={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default LoadingSkeleton;
|
||||||
@@ -1,87 +1,142 @@
|
|||||||
'use client';
|
"use client";
|
||||||
import {useFileActions} from "~/app/_components/FileActions";
|
import { useRef, useState } from "react";
|
||||||
|
import { useFileActions } from "~/app/_components/FileActions";
|
||||||
|
import toast from "react-hot-toast";
|
||||||
|
|
||||||
export function FileActionsContainer({
|
export function FileActionsContainer({
|
||||||
fileId,
|
fileId,
|
||||||
fileName,
|
fileName,
|
||||||
fileUrl,
|
fileUrl,
|
||||||
isOwner,
|
isOwner,
|
||||||
|
isPublic: initialIsPublic, // Rename to avoid conflict with local state
|
||||||
}: {
|
}: {
|
||||||
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 [isPublic, setIsPublic] = useState(initialIsPublic); // Local state for toggle
|
||||||
if (isOwner) {
|
const { handleDownload, handleCopyUrl, handleRemove } = useFileActions(
|
||||||
console.log(description);
|
() => fileId,
|
||||||
|
(description: string) => {
|
||||||
|
if (isOwner) {
|
||||||
|
console.log(description);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex self-center gap-2">
|
<div className="flex gap-2 self-center">
|
||||||
{/* Download Button */}
|
{/* Download Button */}
|
||||||
<button
|
<button
|
||||||
onClick={() => handleDownload(fileId, fileName)}
|
onClick={() => handleDownload(fileId, fileName)}
|
||||||
className="flex items-center justify-center rounded-full bg-blue-500 p-2 hover:bg-blue-600"
|
className="flex items-center justify-center rounded-full bg-blue-500 p-2 hover:bg-blue-600"
|
||||||
>
|
>
|
||||||
<svg
|
<img src="/icons/download.svg" alt="Download" className="h-6 w-6" />
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
strokeWidth={2}
|
|
||||||
stroke="currentColor"
|
|
||||||
className="h-5 w-5 text-white"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
d="M4 16v2a2 2 0 002 2h12a2 2 0 002-2v-2M7 10l5 5m0 0l5-5m-5 5V3"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Copy URL Button */}
|
{/* Copy URL Button */}
|
||||||
<button
|
<button
|
||||||
onClick={() => handleCopyUrl(fileUrl)}
|
onClick={() => {
|
||||||
className="flex items-center justify-center rounded-full bg-green-500 p-2 hover:bg-green-600"
|
handleCopyUrl(fileUrl);
|
||||||
|
toast.success("File URL copied to clipboard!");
|
||||||
|
}}
|
||||||
|
className="flex items-center justify-center rounded-full bg-green-500 p-2 hover:bg-green-600"
|
||||||
>
|
>
|
||||||
<svg
|
<img src="/icons/copy.svg" alt="Copy URL" className="h-6 w-6" />
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
strokeWidth={2}
|
|
||||||
stroke="currentColor"
|
|
||||||
className="h-5 w-5 text-white"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
d="M8 9h8m-6 4h4m-7 8h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v12a2 2 0 002 2z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Remove Button */}
|
{/* Remove Button */}
|
||||||
|
{isOwner && (
|
||||||
<button
|
<button
|
||||||
onClick={() => handleRemove(fileId)}
|
onClick={async () => {
|
||||||
className="flex items-center justify-center rounded-full bg-red-500 p-2 hover:bg-red-600"
|
try {
|
||||||
|
await handleRemove(fileId);
|
||||||
|
toast.success("File removed successfully!");
|
||||||
|
} catch (err) {
|
||||||
|
toast.error("Failed to remove file.");
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="flex items-center justify-center rounded-full bg-red-500 p-2 hover:bg-red-700"
|
||||||
>
|
>
|
||||||
<svg
|
<img src="/icons/delete.svg" alt="Remove" className="h-6 w-6" />
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
strokeWidth={2}
|
|
||||||
stroke="currentColor"
|
|
||||||
className="h-5 w-5 text-white"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
d="M6 18L18 6M6 6l12 12"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Public/Private Toggle */}
|
||||||
|
{isOwner && (
|
||||||
|
<button
|
||||||
|
onClick={async () => {
|
||||||
|
const newIsPublic = !isPublic;
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
setIsPublic(newIsPublic); // Update local state
|
||||||
|
toast.success(
|
||||||
|
`File is now ${newIsPublic ? "Public" : "Private"}!`
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
toast.error("Error updating public status.");
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="flex items-center justify-center rounded-full bg-gray-500 p-2 hover:bg-gray-600 transition-all duration-300"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={isPublic ? "/icons/public.svg" : "/icons/private.svg"}
|
||||||
|
alt={isPublic ? "Public" : "Private"}
|
||||||
|
className={`h-6 w-6 transition-transform duration-300 ${isPublic ? 'rotate-360' : 'rotate-0'}`}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function FileDescriptionContainer({
|
||||||
|
fileId,
|
||||||
|
fileDescription,
|
||||||
|
}: {
|
||||||
|
fileId: string;
|
||||||
|
fileDescription?: string;
|
||||||
|
}) {
|
||||||
|
const [description, setDescription] = useState(fileDescription || ""); // Add state for description
|
||||||
|
const { handleDescriptionChange } = useFileActions(
|
||||||
|
() => {},
|
||||||
|
(description: string) => {
|
||||||
|
setDescription(description);
|
||||||
|
return undefined;
|
||||||
|
},
|
||||||
|
fileId,
|
||||||
|
); // Wrap setDescription in a function
|
||||||
|
const debounceTimer = useRef<NodeJS.Timeout | null>(null); // Initialize debounce timer
|
||||||
|
|
||||||
|
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
|
handleDescriptionChange(e, debounceTimer); // Pass the debounce timer
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex gap-2 self-center">
|
||||||
|
<textarea
|
||||||
|
className="h-24 w-full rounded-md border bg-gray-800 p-2 text-white"
|
||||||
|
value={description} // Use state value
|
||||||
|
onChange={handleChange}
|
||||||
|
placeholder="Enter file description..."
|
||||||
|
maxLength={200} // Limit to 200 characters
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { notifyClients } from "~/utils/notifyClients";
|
|||||||
|
|
||||||
export const useFileActions = (
|
export const useFileActions = (
|
||||||
setFiles: (callback: (prevFiles: any[]) => any[]) => void,
|
setFiles: (callback: (prevFiles: any[]) => any[]) => void,
|
||||||
setDescription?: (description: string) => void,
|
setDescription?: (description: string) => undefined,
|
||||||
fileId?: string
|
fileId?: string
|
||||||
) => {
|
) => {
|
||||||
const pageUrl = `${env.NEXT_PUBLIC_PAGE_URL}`;
|
const pageUrl = `${env.NEXT_PUBLIC_PAGE_URL}`;
|
||||||
@@ -75,7 +75,9 @@ export const useFileActions = (
|
|||||||
e: React.ChangeEvent<HTMLTextAreaElement>,
|
e: React.ChangeEvent<HTMLTextAreaElement>,
|
||||||
debounceTimer: React.RefObject<NodeJS.Timeout | null>
|
debounceTimer: React.RefObject<NodeJS.Timeout | null>
|
||||||
) => {
|
) => {
|
||||||
if (!setDescription) return;
|
if (setDescription === undefined) {console.error("setDescription function is not provided")
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
const newDescription = e.target.value;
|
const newDescription = e.target.value;
|
||||||
setDescription(newDescription);
|
setDescription(newDescription);
|
||||||
@@ -83,7 +85,9 @@ export const useFileActions = (
|
|||||||
if (debounceTimer.current) {
|
if (debounceTimer.current) {
|
||||||
clearTimeout(debounceTimer.current);
|
clearTimeout(debounceTimer.current);
|
||||||
}
|
}
|
||||||
|
|
||||||
debounceTimer.current = setTimeout(() => {
|
debounceTimer.current = setTimeout(() => {
|
||||||
|
console.log("Calling handleDescriptionSave"); // Debug log
|
||||||
handleDescriptionSave(newDescription);
|
handleDescriptionSave(newDescription);
|
||||||
}, 1000);
|
}, 1000);
|
||||||
};
|
};
|
||||||
@@ -99,7 +103,8 @@ export const useFileActions = (
|
|||||||
const response = await fetch(`/api/files/share?id=${encodeURIComponent(fileId)}`, {
|
const response = await fetch(`/api/files/share?id=${encodeURIComponent(fileId)}`, {
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ description }),
|
// pass the fileId and description in the request body
|
||||||
|
body: JSON.stringify({ description, id: fileId }),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.status === 403) {
|
if (response.status === 403) {
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import toast from "react-hot-toast";
|
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { env } from "~/env.js";
|
import { env } from "~/env.js";
|
||||||
import { FilePreview } from "~/app/_components/FilePreview";
|
import { FilePreview } from "~/app/_components/FilePreview";
|
||||||
import { useFileActions } from "~/app/_components/FileActions";
|
import { useFileActions } from "~/app/_components/FileActions";
|
||||||
|
import { FileActionsContainer } from "./ActionButtons";
|
||||||
|
|
||||||
interface FileDetails {
|
interface FileDetails {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -13,6 +13,8 @@ interface FileDetails {
|
|||||||
url: string;
|
url: string;
|
||||||
description: string;
|
description: string;
|
||||||
extension: string;
|
extension: string;
|
||||||
|
isOwner: boolean; // Indicates if the user owns the file
|
||||||
|
public: boolean; // Indicates if the file is public
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FileGridProps {
|
interface FileGridProps {
|
||||||
@@ -69,13 +71,15 @@ export default function FileGrid({ session }: FileGridProps) {
|
|||||||
|
|
||||||
const eventSource = new EventSource("/api/files/stream");
|
const eventSource = new EventSource("/api/files/stream");
|
||||||
eventSource.onmessage = (event) => {
|
eventSource.onmessage = (event) => {
|
||||||
const data: { type: string; file?: FileDetails; fileId?: string } = JSON.parse(event.data);
|
const data: { type: string; fileId?: string } = JSON.parse(event.data);
|
||||||
|
console.log("SSE event:", data);
|
||||||
if (data.type === "file-added" && data.file) {
|
if (data.type === "file-added" && data.fileId) {
|
||||||
setFiles((prevFiles) => (data.file ? [...prevFiles, data.file] : prevFiles));
|
fetchFiles();
|
||||||
toast.success(`File "${data.file.name}" added!`);
|
} else if (data.type === "file-updated" && data.fileId) {
|
||||||
|
// Fetch the updated file details
|
||||||
|
fetchFiles();
|
||||||
} else if (data.type === "file-removed" && data.fileId) {
|
} else if (data.type === "file-removed" && data.fileId) {
|
||||||
setFiles((prevFiles) => prevFiles.filter((file) => file.id !== data.fileId));
|
setFiles((prevFiles => prevFiles.filter(file => file.id !== data.fileId)));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -101,7 +105,7 @@ export default function FileGrid({ session }: FileGridProps) {
|
|||||||
key={file.id}
|
key={file.id}
|
||||||
className="flex place-content-end max-w-xs flex-col gap-4 rounded-xl bg-white/10 p-4 hover:bg-white/20"
|
className="flex place-content-end max-w-xs flex-col gap-4 rounded-xl bg-white/10 p-4 hover:bg-white/20"
|
||||||
>
|
>
|
||||||
{fileType !== "unknown" && <div className=" self-center max-w-50"><FilePreview fileId={file.id} fileType={fileType} /></div>}
|
{<div className=" self-center max-w-50"><FilePreview fileId={file.id} fileType={file.extension} share={false} /></div>}
|
||||||
|
|
||||||
<button onClick={() => router.push(pageUrl + file.url)}>
|
<button onClick={() => router.push(pageUrl + file.url)}>
|
||||||
<h3 className="text-2xl font-bold">{file.name}</h3>
|
<h3 className="text-2xl font-bold">{file.name}</h3>
|
||||||
@@ -110,68 +114,13 @@ export default function FileGrid({ session }: FileGridProps) {
|
|||||||
|
|
||||||
|
|
||||||
<div className="flex self-center gap-2">
|
<div className="flex self-center gap-2">
|
||||||
{/* Download Button */}
|
<FileActionsContainer
|
||||||
<button
|
fileId={file.id}
|
||||||
onClick={() => handleDownload(file.id, file.name)}
|
fileName={file.name}
|
||||||
className="flex items-center justify-center rounded-full bg-blue-500 p-2 hover:bg-blue-600"
|
fileUrl={file.url}
|
||||||
>
|
isOwner={true}
|
||||||
<svg
|
isPublic={file.public}
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
/>
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
strokeWidth={2}
|
|
||||||
stroke="currentColor"
|
|
||||||
className="h-5 w-5 text-white"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
d="M4 16v2a2 2 0 002 2h12a2 2 0 002-2v-2M7 10l5 5m0 0l5-5m-5 5V3"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{/* Copy URL Button */}
|
|
||||||
<button
|
|
||||||
onClick={() => handleCopyUrl(file.url)}
|
|
||||||
className="flex items-center justify-center rounded-full bg-green-500 p-2 hover:bg-green-600"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
strokeWidth={2}
|
|
||||||
stroke="currentColor"
|
|
||||||
className="h-5 w-5 text-white"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
d="M8 9h8m-6 4h4m-7 8h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v12a2 2 0 002 2z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{/* Remove Button */}
|
|
||||||
<button
|
|
||||||
onClick={() => handleRemove(file.id)}
|
|
||||||
className="flex items-center justify-center rounded-full bg-red-500 p-2 hover:bg-red-600"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
strokeWidth={2}
|
|
||||||
stroke="currentColor"
|
|
||||||
className="h-5 w-5 text-white"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
d="M6 18L18 6M6 6l12 12"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,15 +1,24 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
import { remark } from 'remark';
|
||||||
|
import html from 'remark-html';
|
||||||
|
import matter from 'gray-matter';
|
||||||
|
import "github-markdown-css/github-markdown.css";
|
||||||
|
import "../styles/custom.css"; // Adjust the path as necessary
|
||||||
|
import { MarkdownRenderer } from "../../components/MarkdownRenderer";
|
||||||
|
|
||||||
interface FilePreviewProps {
|
interface FilePreviewProps {
|
||||||
fileId: string;
|
fileId: string;
|
||||||
fileType: string; // Pass the file type as a prop
|
fileType: string; // Pass the file type as a prop
|
||||||
}
|
}
|
||||||
|
|
||||||
export function FilePreview({ fileId, fileType }: FilePreviewProps) {
|
export function FilePreview({ fileId, fileType, share }: FilePreviewProps & { share: boolean }) {
|
||||||
const [mediaSrc, setMediaSrc] = useState<string | null>(null);
|
const [mediaSrc, setMediaSrc] = useState<string | null>(null);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [markdownContent, setMarkdownContent] = useState<string | null>(null);
|
||||||
|
|
||||||
|
console.log("File Type:", fileType);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!fileId) {
|
if (!fileId) {
|
||||||
@@ -44,20 +53,53 @@ export function FilePreview({ fileId, fileType }: FilePreviewProps) {
|
|||||||
};
|
};
|
||||||
}, [fileId]);
|
}, [fileId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (fileType.startsWith("markdown")) {
|
||||||
|
const fetchMarkdown = async () => {
|
||||||
|
try {
|
||||||
|
const result = await renderMarkdown({ id: fileId });
|
||||||
|
setMarkdownContent(result.props.postData.contentHtml);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to fetch markdown content:", err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchMarkdown();
|
||||||
|
}
|
||||||
|
}, [fileId, fileType]);
|
||||||
|
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return <div className="text-red-500">{error}</div>;
|
return <div className="text-red-500">{error}</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!mediaSrc) {
|
if (!mediaSrc && !markdownContent) {
|
||||||
return <div>Loading...</div>;
|
return <div>Loading...</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (fileType.startsWith("markdown")) {
|
||||||
|
if (share) {
|
||||||
|
return (
|
||||||
|
<div className="overflow-y-auto max-h-96 rounded-lg shadow-md">
|
||||||
|
{markdownContent ? (
|
||||||
|
<MarkdownRenderer markdownContent={markdownContent} />
|
||||||
|
) : (
|
||||||
|
<div>Loading markdown...</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<img src="/icons/files/code.svg" alt="Code file preview" className="max-w-full max-h-96 rounded-lg invert" />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (fileType.startsWith("video")) {
|
if (fileType.startsWith("video")) {
|
||||||
return (
|
return (
|
||||||
<video
|
<video
|
||||||
controls
|
controls
|
||||||
className="max-w-full max-h-96 rounded-lg shadow-md"
|
className="max-w-full max-h-96 rounded-lg shadow-md"
|
||||||
src={mediaSrc}
|
src={mediaSrc || ""}
|
||||||
>
|
>
|
||||||
Your browser does not support the video tag.
|
Your browser does not support the video tag.
|
||||||
</video>
|
</video>
|
||||||
@@ -68,12 +110,68 @@ export function FilePreview({ fileId, fileType }: FilePreviewProps) {
|
|||||||
<audio
|
<audio
|
||||||
controls
|
controls
|
||||||
className="max-w-full max-h-96 rounded-lg shadow-md"
|
className="max-w-full max-h-96 rounded-lg shadow-md"
|
||||||
src={mediaSrc}
|
src={mediaSrc || ""}
|
||||||
>
|
>
|
||||||
Your browser does not support the audio tag.
|
Your browser does not support the audio tag.
|
||||||
</audio>
|
</audio>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (fileType.startsWith("image")) {
|
||||||
|
return <img src={mediaSrc || ""} alt="Media preview" className="max-w-full max-h-96 rounded-lg shadow-md" />;
|
||||||
|
}
|
||||||
|
|
||||||
return <img src={mediaSrc} alt="Media preview" className="max-w-full max-h-96 rounded-lg shadow-md" />;
|
if (fileType.startsWith("text")) {
|
||||||
|
return (
|
||||||
|
<img src="/icons/files/text.svg" alt="Text file preview" className="max-w-full max-h-96 rounded-lg invert" />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (fileType.startsWith("archive")) {
|
||||||
|
return (
|
||||||
|
<img src="/icons/files/archive.svg" alt="Archive file preview" className="max-w-full max-h-96 rounded-lg invert" />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (fileType.startsWith("code")) {
|
||||||
|
return (
|
||||||
|
<img src="/icons/files/code.svg" alt="Code file preview" className="max-w-full max-h-96 rounded-lg invert" />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Unsupported file type:", fileType);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function rendererMarkdown(id: string) {
|
||||||
|
const fileContents = await fetch(`/api/files/serv?id=${encodeURIComponent(id)}`)
|
||||||
|
.then((res) => res.text())
|
||||||
|
.catch((err) => {
|
||||||
|
console.error("Failed to fetch file contents:", err);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!fileContents) {
|
||||||
|
throw new Error("File contents could not be fetched.");
|
||||||
|
}
|
||||||
|
const matterResult = matter(fileContents);
|
||||||
|
|
||||||
|
const processedContent = await remark()
|
||||||
|
.use(html)
|
||||||
|
.process(matterResult.content);
|
||||||
|
const contentHtml = processedContent.toString();
|
||||||
|
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
contentHtml,
|
||||||
|
...matterResult.data,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function renderMarkdown({ id }: { id: string }) {
|
||||||
|
const postData = await rendererMarkdown(id);
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
postData,
|
||||||
|
},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import type { Metadata } from "next";
|
||||||
|
|
||||||
|
export async function generateMetadata({
|
||||||
|
searchParams,
|
||||||
|
}: {
|
||||||
|
searchParams: { id?: string };
|
||||||
|
}): Promise<Metadata> {
|
||||||
|
const fileId = searchParams.id;
|
||||||
|
|
||||||
|
if (!fileId) {
|
||||||
|
return {
|
||||||
|
title: "File Not Found",
|
||||||
|
description: "The file you are looking for does not exist.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch file details for metadata
|
||||||
|
const response = await fetch(
|
||||||
|
`${process.env.NEXT_PUBLIC_PAGE_URL}/api/files/share?id=${encodeURIComponent(fileId)}`,
|
||||||
|
{ cache: "no-store" },
|
||||||
|
);
|
||||||
|
if (!response.ok) {
|
||||||
|
return {
|
||||||
|
title: "File Not Found",
|
||||||
|
description: "The file you are looking for does not exist.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const fileDetails = await response.json();
|
||||||
|
|
||||||
|
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`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@ export function HomeButton() {
|
|||||||
onClick={() => (window.location.href = "/")}
|
onClick={() => (window.location.href = "/")}
|
||||||
className="rounded-full bg-white/10 px-4 py-2 font-semibold hover:bg-white/20"
|
className="rounded-full bg-white/10 px-4 py-2 font-semibold hover:bg-white/20"
|
||||||
>
|
>
|
||||||
Home
|
<img src="/icons/home.svg" alt="Home" className="w-6 h-6" />
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -10,6 +10,7 @@ export default function UploadForm() {
|
|||||||
const [uploadedFileUrl, setUploadedFileUrl] = useState<string | null>(null);
|
const [uploadedFileUrl, setUploadedFileUrl] = useState<string | null>(null);
|
||||||
const [progress, setProgress] = useState<number>(0); // Track upload progress
|
const [progress, setProgress] = useState<number>(0); // Track upload progress
|
||||||
const fileInputRef = useRef<HTMLInputElement | null>(null); // Ref for the file input
|
const fileInputRef = useRef<HTMLInputElement | null>(null); // Ref for the file input
|
||||||
|
const [isDragActive, setIsDragActive] = useState(false); // Track drag state
|
||||||
|
|
||||||
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
if (e.target.files) {
|
if (e.target.files) {
|
||||||
@@ -20,6 +21,34 @@ export default function UploadForm() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Drag and drop handlers
|
||||||
|
const handleDragOver = (e: React.DragEvent<HTMLDivElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
setIsDragActive(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDragLeave = (e: React.DragEvent<HTMLDivElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
setIsDragActive(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDrop = (e: React.DragEvent<HTMLDivElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
setIsDragActive(false);
|
||||||
|
if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
|
||||||
|
setFile(e.dataTransfer.files[0] ?? null);
|
||||||
|
setUploadedFileUrl(null);
|
||||||
|
setProgress(0);
|
||||||
|
setUploading(false);
|
||||||
|
if (fileInputRef.current) {
|
||||||
|
fileInputRef.current.value = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleUpload = async () => {
|
const handleUpload = async () => {
|
||||||
if (!file) return toast.error("Please select a file to upload.");
|
if (!file) return toast.error("Please select a file to upload.");
|
||||||
setUploading(true);
|
setUploading(true);
|
||||||
@@ -41,15 +70,14 @@ export default function UploadForm() {
|
|||||||
|
|
||||||
xhr.onload = () => {
|
xhr.onload = () => {
|
||||||
if (xhr.status === 200) {
|
if (xhr.status === 200) {
|
||||||
const response: { url: string } = JSON.parse(xhr.responseText); // Explicitly type the response
|
const response = JSON.parse(xhr.responseText);
|
||||||
setUploadedFileUrl(response.url); // Assume the API returns the uploaded file URL
|
setUploadedFileUrl(response.file?.url || null); // Use the new response structure
|
||||||
notifyClients({type: "file-uploaded", fileUrl: response.url}); // Notify other clients about the new file
|
|
||||||
toast.success("File uploaded successfully!");
|
toast.success("File uploaded successfully!");
|
||||||
|
|
||||||
// Clear the file input and reset state
|
// Clear the file input and reset state
|
||||||
setFile(null);
|
setFile(null);
|
||||||
if (fileInputRef.current) {
|
if (fileInputRef.current) {
|
||||||
fileInputRef.current.value = ""; // Clear the file input
|
fileInputRef.current.value = "";
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.error("Upload failed:", xhr.responseText);
|
console.error("Upload failed:", xhr.responseText);
|
||||||
@@ -86,42 +114,48 @@ export default function UploadForm() {
|
|||||||
{/* Toast container */}
|
{/* Toast container */}
|
||||||
<Toaster position="top-right" reverseOrder={false} />
|
<Toaster position="top-right" reverseOrder={false} />
|
||||||
|
|
||||||
<div className="flex flex-row items-center gap-4">
|
{/* Drag and Drop Area */}
|
||||||
{/* Custom file input */}
|
<div
|
||||||
<label
|
className={`w-full max-w-md flex flex-col items-center justify-center border-2 border-dashed rounded-lg p-6 mb-2 transition-colors duration-200 ${isDragActive ? "border-blue-500 bg-blue-100/30" : "border-gray-400 bg-transparent hover:bg-gray-50/10"}`}
|
||||||
htmlFor="file-upload"
|
onDragOver={handleDragOver}
|
||||||
className="cursor-pointer flex items-center gap-2 rounded-full bg-white/10 px-10 py-3 font-semibold no-underline transition hover:bg-white/20"
|
onDragLeave={handleDragLeave}
|
||||||
>
|
onDrop={handleDrop}
|
||||||
{file ? (
|
onClick={() => fileInputRef.current?.click()}
|
||||||
<>
|
style={{ cursor: "pointer" }}
|
||||||
File Selected
|
>
|
||||||
{/* SVG Icon */}
|
{/* Hidden file input for click-to-select */}
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
strokeWidth={2}
|
|
||||||
stroke="currentColor"
|
|
||||||
className="h-5 w-5 text-green-500"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
d="M5 13l4 4L19 7"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
"Select File"
|
|
||||||
)}
|
|
||||||
</label>
|
|
||||||
<input
|
<input
|
||||||
id="file-upload"
|
|
||||||
ref={fileInputRef} // Attach the ref to the file input
|
|
||||||
type="file"
|
type="file"
|
||||||
|
ref={fileInputRef}
|
||||||
|
style={{ display: "none" }}
|
||||||
onChange={handleFileChange}
|
onChange={handleFileChange}
|
||||||
className="hidden" // Hide the default file input
|
|
||||||
/>
|
/>
|
||||||
|
<span className="text-gray-300">
|
||||||
|
{isDragActive ? "Drop your file here" : "Drag & drop a file here, or click to select"}
|
||||||
|
</span>
|
||||||
|
{file && (
|
||||||
|
<div className="mt-2 flex items-center gap-2">
|
||||||
|
<span className="text-green-500 font-semibold">{file.name}</span>
|
||||||
|
{/* Add button to remove file */}
|
||||||
|
<button
|
||||||
|
onClick={e => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setFile(null);
|
||||||
|
if (fileInputRef.current) {
|
||||||
|
fileInputRef.current.value = "";
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="flex items-center justify-center rounded-full bg-red-500 p-2 hover:bg-red-700"
|
||||||
|
style={{ cursor: "pointer" }}
|
||||||
|
>
|
||||||
|
<img src="/icons/delete.svg" alt="Remove" className="h-6 w-6" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{/* Show upload button only when file is selected */}
|
||||||
|
{file && (
|
||||||
|
<div className="flex flex-row items-center gap-4">
|
||||||
<button
|
<button
|
||||||
onClick={handleUpload}
|
onClick={handleUpload}
|
||||||
disabled={uploading || !file}
|
disabled={uploading || !file}
|
||||||
@@ -129,7 +163,7 @@ export default function UploadForm() {
|
|||||||
>
|
>
|
||||||
{uploading ? "Uploading..." : "Upload"}
|
{uploading ? "Uploading..." : "Upload"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>)}
|
||||||
|
|
||||||
{file && uploading && (
|
{file && uploading && (
|
||||||
<div className="w-full max-w-md flex items-center gap-2">
|
<div className="w-full max-w-md flex items-center gap-2">
|
||||||
@@ -142,31 +176,17 @@ export default function UploadForm() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{uploadedFileUrl && (
|
{/* {uploadedFileUrl && file && (
|
||||||
<div className="flex flex-row items-center gap-4">
|
<div className="flex flex-row items-center gap-4">
|
||||||
<p className="text-white">{uploadedFileUrl}</p>
|
<p className="text-white">{file.name}</p>
|
||||||
<button
|
<button
|
||||||
onClick={handleCopyUrl}
|
onClick={handleCopyUrl}
|
||||||
className="flex items-center justify-center rounded-full bg-blue-500 p-2 hover:bg-blue-600"
|
className="flex items-center justify-center rounded-full bg-blue-500 p-2 hover:bg-blue-600"
|
||||||
>
|
>
|
||||||
{/* Copy Icon */}
|
<img src="/icons/copy.svg" alt="Copy URL" className="h-6 w-6" />
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
strokeWidth={2}
|
|
||||||
stroke="currentColor"
|
|
||||||
className="h-5 w-5 text-white"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
d="M8 16h8M8 12h8m-7 8h6a2 2 0 002-2V6a2 2 0 00-2-2H9a2 2 0 00-2 2v12a2 2 0 002 2z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)} */}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
import path from "path";
|
import { db } from "~/server/db";
|
||||||
import { promises as fs } from "fs";
|
import { minioClient } from "~/utils/minioClient";
|
||||||
|
|
||||||
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,22 +10,43 @@ 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 {
|
||||||
const filePath = path.join(process.cwd(), "uploads", fileId);
|
// Fetch file metadata from the database
|
||||||
const fileBuffer = await fs.readFile(filePath);
|
const file = await db.file.findFirst({
|
||||||
|
where: { id: fileId },
|
||||||
|
});
|
||||||
|
|
||||||
return new Response(fileBuffer, {
|
if (!file) {
|
||||||
|
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 reading file:", error);
|
console.error("Error fetching file from MinIO:", error);
|
||||||
return NextResponse.json({ error: "File not found" }, { status: 404 });
|
return NextResponse.json({ error: "Failed to fetch file" }, { status: 500 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,13 +1,11 @@
|
|||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
import { db } from "~/server/db";
|
import { db } from "~/server/db";
|
||||||
import { auth } from "~/server/auth";
|
import { auth } from "~/server/auth";
|
||||||
import path from "path";
|
import { minioClient } from "~/utils/minioClient";
|
||||||
import { promises as fs } from "fs";
|
|
||||||
import { notifyClients } from "~/utils/notifyClients";
|
import { notifyClients } from "~/utils/notifyClients";
|
||||||
|
|
||||||
export async function DELETE(req: Request) {
|
export async function DELETE(req: Request) {
|
||||||
const session = await auth();
|
const session = await auth();
|
||||||
|
|
||||||
if (!session?.user) {
|
if (!session?.user) {
|
||||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||||
}
|
}
|
||||||
@@ -18,32 +16,25 @@ export async function DELETE(req: Request) {
|
|||||||
return NextResponse.json({ error: "Invalid request body" }, { status: 400 });
|
return NextResponse.json({ error: "Invalid request body" }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const resource = await db.file.findUnique({
|
const file = await db.file.findUnique({ where: { id: body.id } });
|
||||||
where: { id: body.id },
|
if (!file) {
|
||||||
});
|
|
||||||
|
|
||||||
if (!resource) {
|
|
||||||
return NextResponse.json({ error: "File not found" }, { status: 404 });
|
return NextResponse.json({ error: "File not found" }, { status: 404 });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (resource.uploadedById !== session.user.id) {
|
if (file.uploadedById !== session.user.id) {
|
||||||
return NextResponse.json({ error: "You are not authorized to delete this file" }, { status: 403 });
|
return NextResponse.json({ error: "You are not authorized to delete this file" }, { status: 403 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const filePath = path.join(process.cwd(), "uploads", path.basename(body.id));
|
const objectName = `${file.id}-${file.name}`;
|
||||||
await fs.unlink(filePath).catch((err) => {
|
await minioClient.removeObject(process.env.MINIO_BUCKET || "file-hosting", objectName);
|
||||||
console.error("Error deleting file from filesystem:", err);
|
|
||||||
});
|
|
||||||
|
|
||||||
await db.file.delete({
|
await db.file.delete({ where: { id: body.id } });
|
||||||
where: { id: body.id },
|
|
||||||
});
|
|
||||||
|
|
||||||
notifyClients({ type: "file-removed", fileId: body.id });
|
notifyClients({ type: "file-removed", fileId: body.id });
|
||||||
|
|
||||||
return NextResponse.json({ message: "File deleted successfully" });
|
return NextResponse.json({ message: "File deleted successfully" });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error deleting file:", error);
|
console.error("Error deleting file from MinIO:", error);
|
||||||
return NextResponse.json({ error: "Failed to delete file" }, { status: 500 });
|
return NextResponse.json({ error: "Failed to delete file" }, { status: 500 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
import path from "path";
|
|
||||||
import { promises as fs } from "fs";
|
|
||||||
import { db } from "~/server/db";
|
import { db } from "~/server/db";
|
||||||
|
import { minioClient } from "~/utils/minioClient";
|
||||||
|
import { getFileType } from "~/utils/fileType";
|
||||||
|
|
||||||
export async function GET(req: Request) {
|
export async function GET(req: Request) {
|
||||||
const url = new URL(req.url);
|
const url = new URL(req.url);
|
||||||
@@ -21,41 +21,31 @@ export async function GET(req: Request) {
|
|||||||
return NextResponse.json({ error: "File not found" }, { status: 404 });
|
return NextResponse.json({ error: "File not found" }, { status: 404 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Construct the file path
|
const bucketName = process.env.MINIO_BUCKET || "file-hosting";
|
||||||
const filePath = path.join(process.cwd(), "uploads", file.id);
|
const objectName = `${file.id}-${file.name}`; // Construct the object name in MinIO
|
||||||
|
|
||||||
// Read the file from the filesystem
|
// Fetch the file from MinIO
|
||||||
const fileBuffer = await fs.readFile(filePath);
|
const stream = await minioClient.getObject(bucketName, objectName);
|
||||||
|
|
||||||
const mimeType = file.extension === ".mp4"
|
const mimeType = getFileType(file.name); // Get the MIME type based on the file extension
|
||||||
? "video/mp4"
|
|
||||||
: file.extension === ".webm"
|
|
||||||
? "video/webm"
|
|
||||||
: file.extension === ".ogg"
|
|
||||||
? "video/ogg"
|
|
||||||
: file.extension === ".jpg" || file.extension === ".jpeg"
|
|
||||||
? "image/jpeg"
|
|
||||||
: file.extension === ".png"
|
|
||||||
? "image/png"
|
|
||||||
: file.extension === ".gif"
|
|
||||||
? "image/gif"
|
|
||||||
: file.extension === ".svg"
|
|
||||||
? "image/svg+xml"
|
|
||||||
: file.extension === ".mp3"
|
|
||||||
? "audio/mpeg"
|
|
||||||
: file.extension === ".wav"
|
|
||||||
? "audio/wav"
|
|
||||||
: "application/octet-stream";
|
|
||||||
|
|
||||||
// Return the file as a binary response
|
// Return the file as a binary response
|
||||||
return new Response(fileBuffer, {
|
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": mimeType,
|
"Content-Type": mimeType,
|
||||||
"Content-Disposition": `inline; filename="${file.name}"`,
|
"Content-Disposition": `inline; filename="${file.name}"`,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching file:", error);
|
console.error("Error fetching file from MinIO:", error);
|
||||||
return NextResponse.json({ error: "Failed to fetch file" }, { status: 500 });
|
return NextResponse.json({ error: "Failed to fetch file" }, { status: 500 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -25,13 +25,15 @@ export async function GET(req: Request) {
|
|||||||
name: file.name,
|
name: file.name,
|
||||||
size: file.size,
|
size: file.size,
|
||||||
owner: file.uploadedBy?.name ?? null,
|
owner: file.uploadedBy?.name ?? null,
|
||||||
|
ownerId: file.uploadedBy?.id ?? null,
|
||||||
ownerAvatar: file.uploadedBy?.image ?? null,
|
ownerAvatar: file.uploadedBy?.image ?? null,
|
||||||
uploadDate: file.uploadDate,
|
uploadDate: file.uploadDate,
|
||||||
id: file.id,
|
id: file.id,
|
||||||
isOwner: session?.user?.id === file.uploadedById,
|
isOwner: null,
|
||||||
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);
|
||||||
@@ -48,7 +50,8 @@ export async function PUT(req: Request) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const body = (await req.json()) as { id: string; description: string } | null;
|
const body = (await req.json()) as { id: string; description: string } | null;
|
||||||
if (!body?.id || !body.description) {
|
if (!body?.id || body.description === undefined) {
|
||||||
|
// Allow empty description but ensure id is present
|
||||||
return NextResponse.json({ error: "Invalid request body" }, { status: 400 });
|
return NextResponse.json({ error: "Invalid request body" }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
import { NextResponse } from "next/server";
|
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 Busboy from "busboy";
|
||||||
import { Readable } from "stream";
|
import { Readable } from "stream";
|
||||||
|
import { db } from "~/server/db";
|
||||||
|
import { auth } from "~/server/auth";
|
||||||
|
import { minioClient, ensureBucketExists } from "~/utils/minioClient";
|
||||||
|
import { getFileType } from "~/utils/fileType";
|
||||||
|
import cuid from 'cuid';
|
||||||
import { notifyClients } from "~/utils/notifyClients";
|
import { notifyClients } from "~/utils/notifyClients";
|
||||||
import crypto from "crypto";
|
|
||||||
|
|
||||||
export const config = {
|
export const config = {
|
||||||
api: {
|
api: {
|
||||||
@@ -16,18 +16,17 @@ export const config = {
|
|||||||
|
|
||||||
export async function POST(req: Request) {
|
export async function POST(req: Request) {
|
||||||
const session = await auth();
|
const session = await auth();
|
||||||
// generate id for the file
|
|
||||||
const guid = crypto.randomUUID();
|
|
||||||
|
|
||||||
if (!session?.user) {
|
if (!session?.user) {
|
||||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const uploadDir = path.join(process.cwd(), "uploads");
|
const bucketName = process.env.MINIO_BUCKET || "file-hosting";
|
||||||
await fs.mkdir(uploadDir, { recursive: true });
|
await ensureBucketExists(bucketName);
|
||||||
|
|
||||||
return new Promise<Response>((resolve, reject) => {
|
return new Promise<Response>((resolve, reject) => {
|
||||||
const busboy = Busboy({ headers: { "content-type": req.headers.get("content-type") ?? "" } });
|
const busboy = Busboy({
|
||||||
|
headers: { "content-type": req.headers.get("content-type") ?? "" },
|
||||||
|
});
|
||||||
let fileName = "";
|
let fileName = "";
|
||||||
let fileBuffer = Buffer.alloc(0);
|
let fileBuffer = Buffer.alloc(0);
|
||||||
|
|
||||||
@@ -35,57 +34,49 @@ export async function POST(req: Request) {
|
|||||||
fileName = info.filename || "uploaded-file";
|
fileName = info.filename || "uploaded-file";
|
||||||
const chunks: Buffer[] = [];
|
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) => {
|
file.on("data", (chunk) => {
|
||||||
chunks.push(chunk);
|
chunks.push(chunk);
|
||||||
});
|
});
|
||||||
|
|
||||||
file.on("end", () => {
|
file.on("end", async () => {
|
||||||
fileBuffer = Buffer.concat(chunks);
|
fileBuffer = Buffer.concat(chunks);
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
busboy.on("finish", () => {
|
// Generate a unique ID for the file
|
||||||
void (async () => {
|
const fileId = session.user.id + "-" + cuid()
|
||||||
|
const objectName = `${fileId}-${fileName}`;
|
||||||
|
// Change UUID to CUID
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const filePath = path.join(uploadDir, guid);
|
// Upload the file to MinIO
|
||||||
await fs.writeFile(filePath, fileBuffer);
|
await minioClient.putObject(bucketName, objectName, fileBuffer);
|
||||||
|
|
||||||
// Save file metadata to the database
|
// Save file metadata to the database
|
||||||
const newFile = await db.file.create({
|
const newFile = await db.file.create({
|
||||||
data: {
|
data: {
|
||||||
id: guid,
|
id: fileId,
|
||||||
url: `/share?id=${guid}`,
|
url: `/share?id=${fileId}`,
|
||||||
name: fileName,
|
name: fileName,
|
||||||
size: fileBuffer.length,
|
size: fileBuffer.length,
|
||||||
extension: path.extname(fileName),
|
extension: getFileType(fileName),
|
||||||
uploadedById: session.user.id,
|
uploadedById: session.user.id,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
notifyClients({ type: "file-added", fileId: fileId });
|
||||||
|
|
||||||
// Notify clients about the new file
|
resolve(
|
||||||
notifyClients({ type: "file-added", file: newFile });
|
NextResponse.json({
|
||||||
|
message: "File uploaded successfully",
|
||||||
resolve(NextResponse.json({ message: "File uploaded successfully" }));
|
file: newFile,
|
||||||
|
fileId: fileId,
|
||||||
|
}),
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error handling upload:", error);
|
console.error("Error uploading file to MinIO:", error);
|
||||||
resolve(NextResponse.json({ error: "Failed to upload file" }, { status: 500 }));
|
reject(new Error("Failed to upload file"));
|
||||||
}
|
}
|
||||||
})();
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
busboy.on("error", (error: unknown) => {
|
busboy.on("error", (error: unknown) => {
|
||||||
@@ -109,4 +100,4 @@ export async function POST(req: Request) {
|
|||||||
nodeStream.pipe(busboy);
|
nodeStream.pipe(busboy);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "./index"
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
|
||||||
|
/* !!! This is code generated by Prisma. Do not edit directly. !!!
|
||||||
|
/* eslint-disable */
|
||||||
|
module.exports = { ...require('.') }
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "./index"
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
|
||||||
|
/* !!! This is code generated by Prisma. Do not edit directly. !!!
|
||||||
|
/* eslint-disable */
|
||||||
|
module.exports = { ...require('.') }
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "./default"
|
||||||
@@ -0,0 +1,225 @@
|
|||||||
|
|
||||||
|
/* !!! This is code generated by Prisma. Do not edit directly. !!!
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
|
||||||
|
const {
|
||||||
|
Decimal,
|
||||||
|
objectEnumValues,
|
||||||
|
makeStrictEnum,
|
||||||
|
Public,
|
||||||
|
getRuntime,
|
||||||
|
skip
|
||||||
|
} = require('./runtime/index-browser.js')
|
||||||
|
|
||||||
|
|
||||||
|
const Prisma = {}
|
||||||
|
|
||||||
|
exports.Prisma = Prisma
|
||||||
|
exports.$Enums = {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prisma Client JS version: 6.8.2
|
||||||
|
* Query Engine version: 2060c79ba17c6bb9f5823312b6f6b7f4a845738e
|
||||||
|
*/
|
||||||
|
Prisma.prismaVersion = {
|
||||||
|
client: "6.8.2",
|
||||||
|
engine: "2060c79ba17c6bb9f5823312b6f6b7f4a845738e"
|
||||||
|
}
|
||||||
|
|
||||||
|
Prisma.PrismaClientKnownRequestError = () => {
|
||||||
|
const runtimeName = getRuntime().prettyName;
|
||||||
|
throw new Error(`PrismaClientKnownRequestError is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
|
||||||
|
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
|
||||||
|
)};
|
||||||
|
Prisma.PrismaClientUnknownRequestError = () => {
|
||||||
|
const runtimeName = getRuntime().prettyName;
|
||||||
|
throw new Error(`PrismaClientUnknownRequestError is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
|
||||||
|
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
|
||||||
|
)}
|
||||||
|
Prisma.PrismaClientRustPanicError = () => {
|
||||||
|
const runtimeName = getRuntime().prettyName;
|
||||||
|
throw new Error(`PrismaClientRustPanicError is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
|
||||||
|
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
|
||||||
|
)}
|
||||||
|
Prisma.PrismaClientInitializationError = () => {
|
||||||
|
const runtimeName = getRuntime().prettyName;
|
||||||
|
throw new Error(`PrismaClientInitializationError is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
|
||||||
|
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
|
||||||
|
)}
|
||||||
|
Prisma.PrismaClientValidationError = () => {
|
||||||
|
const runtimeName = getRuntime().prettyName;
|
||||||
|
throw new Error(`PrismaClientValidationError is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
|
||||||
|
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
|
||||||
|
)}
|
||||||
|
Prisma.Decimal = Decimal
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Re-export of sql-template-tag
|
||||||
|
*/
|
||||||
|
Prisma.sql = () => {
|
||||||
|
const runtimeName = getRuntime().prettyName;
|
||||||
|
throw new Error(`sqltag is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
|
||||||
|
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
|
||||||
|
)}
|
||||||
|
Prisma.empty = () => {
|
||||||
|
const runtimeName = getRuntime().prettyName;
|
||||||
|
throw new Error(`empty is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
|
||||||
|
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
|
||||||
|
)}
|
||||||
|
Prisma.join = () => {
|
||||||
|
const runtimeName = getRuntime().prettyName;
|
||||||
|
throw new Error(`join is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
|
||||||
|
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
|
||||||
|
)}
|
||||||
|
Prisma.raw = () => {
|
||||||
|
const runtimeName = getRuntime().prettyName;
|
||||||
|
throw new Error(`raw is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
|
||||||
|
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
|
||||||
|
)}
|
||||||
|
Prisma.validator = Public.validator
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extensions
|
||||||
|
*/
|
||||||
|
Prisma.getExtensionContext = () => {
|
||||||
|
const runtimeName = getRuntime().prettyName;
|
||||||
|
throw new Error(`Extensions.getExtensionContext is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
|
||||||
|
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
|
||||||
|
)}
|
||||||
|
Prisma.defineExtension = () => {
|
||||||
|
const runtimeName = getRuntime().prettyName;
|
||||||
|
throw new Error(`Extensions.defineExtension is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
|
||||||
|
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
|
||||||
|
)}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shorthand utilities for JSON filtering
|
||||||
|
*/
|
||||||
|
Prisma.DbNull = objectEnumValues.instances.DbNull
|
||||||
|
Prisma.JsonNull = objectEnumValues.instances.JsonNull
|
||||||
|
Prisma.AnyNull = objectEnumValues.instances.AnyNull
|
||||||
|
|
||||||
|
Prisma.NullTypes = {
|
||||||
|
DbNull: objectEnumValues.classes.DbNull,
|
||||||
|
JsonNull: objectEnumValues.classes.JsonNull,
|
||||||
|
AnyNull: objectEnumValues.classes.AnyNull
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enums
|
||||||
|
*/
|
||||||
|
|
||||||
|
exports.Prisma.TransactionIsolationLevel = makeStrictEnum({
|
||||||
|
ReadUncommitted: 'ReadUncommitted',
|
||||||
|
ReadCommitted: 'ReadCommitted',
|
||||||
|
RepeatableRead: 'RepeatableRead',
|
||||||
|
Serializable: 'Serializable'
|
||||||
|
});
|
||||||
|
|
||||||
|
exports.Prisma.AccountScalarFieldEnum = {
|
||||||
|
id: 'id',
|
||||||
|
userId: 'userId',
|
||||||
|
type: 'type',
|
||||||
|
provider: 'provider',
|
||||||
|
providerAccountId: 'providerAccountId',
|
||||||
|
refresh_token: 'refresh_token',
|
||||||
|
access_token: 'access_token',
|
||||||
|
expires_at: 'expires_at',
|
||||||
|
token_type: 'token_type',
|
||||||
|
scope: 'scope',
|
||||||
|
id_token: 'id_token',
|
||||||
|
session_state: 'session_state',
|
||||||
|
refresh_token_expires_in: 'refresh_token_expires_in'
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.Prisma.SessionScalarFieldEnum = {
|
||||||
|
id: 'id',
|
||||||
|
sessionToken: 'sessionToken',
|
||||||
|
userId: 'userId',
|
||||||
|
expires: 'expires'
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.Prisma.UserScalarFieldEnum = {
|
||||||
|
id: 'id',
|
||||||
|
name: 'name',
|
||||||
|
email: 'email',
|
||||||
|
emailVerified: 'emailVerified',
|
||||||
|
image: 'image'
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.Prisma.VerificationTokenScalarFieldEnum = {
|
||||||
|
identifier: 'identifier',
|
||||||
|
token: 'token',
|
||||||
|
expires: 'expires'
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.Prisma.FileScalarFieldEnum = {
|
||||||
|
id: 'id',
|
||||||
|
url: 'url',
|
||||||
|
name: 'name',
|
||||||
|
size: 'size',
|
||||||
|
extension: 'extension',
|
||||||
|
uploadDate: 'uploadDate',
|
||||||
|
description: 'description',
|
||||||
|
uploadedById: 'uploadedById',
|
||||||
|
public: 'public'
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.Prisma.SortOrder = {
|
||||||
|
asc: 'asc',
|
||||||
|
desc: 'desc'
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.Prisma.QueryMode = {
|
||||||
|
default: 'default',
|
||||||
|
insensitive: 'insensitive'
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.Prisma.NullsOrder = {
|
||||||
|
first: 'first',
|
||||||
|
last: 'last'
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
exports.Prisma.ModelName = {
|
||||||
|
Account: 'Account',
|
||||||
|
Session: 'Session',
|
||||||
|
User: 'User',
|
||||||
|
VerificationToken: 'VerificationToken',
|
||||||
|
File: 'File'
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is a stub Prisma Client that will error at runtime if called.
|
||||||
|
*/
|
||||||
|
class PrismaClient {
|
||||||
|
constructor() {
|
||||||
|
return new Proxy(this, {
|
||||||
|
get(target, prop) {
|
||||||
|
let message
|
||||||
|
const runtime = getRuntime()
|
||||||
|
if (runtime.isEdge) {
|
||||||
|
message = `PrismaClient is not configured to run in ${runtime.prettyName}. In order to run Prisma Client on edge runtime, either:
|
||||||
|
- Use Prisma Accelerate: https://pris.ly/d/accelerate
|
||||||
|
- Use Driver Adapters: https://pris.ly/d/driver-adapters
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
message = 'PrismaClient is unable to run in this browser environment, or has been bundled for the browser (running in `' + runtime.prettyName + '`).'
|
||||||
|
}
|
||||||
|
|
||||||
|
message += `
|
||||||
|
If this is unexpected, please open an issue: https://pris.ly/prisma-prisma-bug-report`
|
||||||
|
|
||||||
|
throw new Error(message)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.PrismaClient = PrismaClient
|
||||||
|
|
||||||
|
Object.assign(exports, Prisma)
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
{
|
||||||
|
"name": "prisma-client-08d22f6c857ed95dda3e967008d87d351c30e87c75486e96dad4c24294f1763e",
|
||||||
|
"main": "index.js",
|
||||||
|
"types": "index.d.ts",
|
||||||
|
"browser": "index-browser.js",
|
||||||
|
"exports": {
|
||||||
|
"./client": {
|
||||||
|
"require": {
|
||||||
|
"node": "./index.js",
|
||||||
|
"edge-light": "./wasm.js",
|
||||||
|
"workerd": "./wasm.js",
|
||||||
|
"worker": "./wasm.js",
|
||||||
|
"browser": "./index-browser.js",
|
||||||
|
"default": "./index.js"
|
||||||
|
},
|
||||||
|
"import": {
|
||||||
|
"node": "./index.js",
|
||||||
|
"edge-light": "./wasm.js",
|
||||||
|
"workerd": "./wasm.js",
|
||||||
|
"worker": "./wasm.js",
|
||||||
|
"browser": "./index-browser.js",
|
||||||
|
"default": "./index.js"
|
||||||
|
},
|
||||||
|
"default": "./index.js"
|
||||||
|
},
|
||||||
|
"./package.json": "./package.json",
|
||||||
|
".": {
|
||||||
|
"require": {
|
||||||
|
"node": "./index.js",
|
||||||
|
"edge-light": "./wasm.js",
|
||||||
|
"workerd": "./wasm.js",
|
||||||
|
"worker": "./wasm.js",
|
||||||
|
"browser": "./index-browser.js",
|
||||||
|
"default": "./index.js"
|
||||||
|
},
|
||||||
|
"import": {
|
||||||
|
"node": "./index.js",
|
||||||
|
"edge-light": "./wasm.js",
|
||||||
|
"workerd": "./wasm.js",
|
||||||
|
"worker": "./wasm.js",
|
||||||
|
"browser": "./index-browser.js",
|
||||||
|
"default": "./index.js"
|
||||||
|
},
|
||||||
|
"default": "./index.js"
|
||||||
|
},
|
||||||
|
"./edge": {
|
||||||
|
"types": "./edge.d.ts",
|
||||||
|
"require": "./edge.js",
|
||||||
|
"import": "./edge.js",
|
||||||
|
"default": "./edge.js"
|
||||||
|
},
|
||||||
|
"./react-native": {
|
||||||
|
"types": "./react-native.d.ts",
|
||||||
|
"require": "./react-native.js",
|
||||||
|
"import": "./react-native.js",
|
||||||
|
"default": "./react-native.js"
|
||||||
|
},
|
||||||
|
"./extension": {
|
||||||
|
"types": "./extension.d.ts",
|
||||||
|
"require": "./extension.js",
|
||||||
|
"import": "./extension.js",
|
||||||
|
"default": "./extension.js"
|
||||||
|
},
|
||||||
|
"./index-browser": {
|
||||||
|
"types": "./index.d.ts",
|
||||||
|
"require": "./index-browser.js",
|
||||||
|
"import": "./index-browser.js",
|
||||||
|
"default": "./index-browser.js"
|
||||||
|
},
|
||||||
|
"./index": {
|
||||||
|
"types": "./index.d.ts",
|
||||||
|
"require": "./index.js",
|
||||||
|
"import": "./index.js",
|
||||||
|
"default": "./index.js"
|
||||||
|
},
|
||||||
|
"./wasm": {
|
||||||
|
"types": "./wasm.d.ts",
|
||||||
|
"require": "./wasm.js",
|
||||||
|
"import": "./wasm.mjs",
|
||||||
|
"default": "./wasm.mjs"
|
||||||
|
},
|
||||||
|
"./runtime/client": {
|
||||||
|
"types": "./runtime/client.d.ts",
|
||||||
|
"require": "./runtime/client.js",
|
||||||
|
"import": "./runtime/client.mjs",
|
||||||
|
"default": "./runtime/client.mjs"
|
||||||
|
},
|
||||||
|
"./runtime/library": {
|
||||||
|
"types": "./runtime/library.d.ts",
|
||||||
|
"require": "./runtime/library.js",
|
||||||
|
"import": "./runtime/library.mjs",
|
||||||
|
"default": "./runtime/library.mjs"
|
||||||
|
},
|
||||||
|
"./runtime/binary": {
|
||||||
|
"types": "./runtime/binary.d.ts",
|
||||||
|
"require": "./runtime/binary.js",
|
||||||
|
"import": "./runtime/binary.mjs",
|
||||||
|
"default": "./runtime/binary.mjs"
|
||||||
|
},
|
||||||
|
"./runtime/wasm": {
|
||||||
|
"types": "./runtime/wasm.d.ts",
|
||||||
|
"require": "./runtime/wasm.js",
|
||||||
|
"import": "./runtime/wasm.mjs",
|
||||||
|
"default": "./runtime/wasm.mjs"
|
||||||
|
},
|
||||||
|
"./runtime/edge": {
|
||||||
|
"types": "./runtime/edge.d.ts",
|
||||||
|
"require": "./runtime/edge.js",
|
||||||
|
"import": "./runtime/edge-esm.js",
|
||||||
|
"default": "./runtime/edge-esm.js"
|
||||||
|
},
|
||||||
|
"./runtime/react-native": {
|
||||||
|
"types": "./runtime/react-native.d.ts",
|
||||||
|
"require": "./runtime/react-native.js",
|
||||||
|
"import": "./runtime/react-native.js",
|
||||||
|
"default": "./runtime/react-native.js"
|
||||||
|
},
|
||||||
|
"./generator-build": {
|
||||||
|
"require": "./generator-build/index.js",
|
||||||
|
"import": "./generator-build/index.js",
|
||||||
|
"default": "./generator-build/index.js"
|
||||||
|
},
|
||||||
|
"./sql": {
|
||||||
|
"require": {
|
||||||
|
"types": "./sql.d.ts",
|
||||||
|
"node": "./sql.js",
|
||||||
|
"default": "./sql.js"
|
||||||
|
},
|
||||||
|
"import": {
|
||||||
|
"types": "./sql.d.ts",
|
||||||
|
"node": "./sql.mjs",
|
||||||
|
"default": "./sql.mjs"
|
||||||
|
},
|
||||||
|
"default": "./sql.js"
|
||||||
|
},
|
||||||
|
"./*": "./*"
|
||||||
|
},
|
||||||
|
"version": "6.8.2",
|
||||||
|
"sideEffects": false
|
||||||
|
}
|
||||||
@@ -0,0 +1,370 @@
|
|||||||
|
declare class AnyNull extends NullTypesEnumValue {
|
||||||
|
#private;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare type Args<T, F extends Operation> = T extends {
|
||||||
|
[K: symbol]: {
|
||||||
|
types: {
|
||||||
|
operations: {
|
||||||
|
[K in F]: {
|
||||||
|
args: any;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
} ? T[symbol]['types']['operations'][F]['args'] : any;
|
||||||
|
|
||||||
|
declare class DbNull extends NullTypesEnumValue {
|
||||||
|
#private;
|
||||||
|
}
|
||||||
|
|
||||||
|
export declare function Decimal(n: Decimal.Value): Decimal;
|
||||||
|
|
||||||
|
export declare namespace Decimal {
|
||||||
|
export type Constructor = typeof Decimal;
|
||||||
|
export type Instance = Decimal;
|
||||||
|
export type Rounding = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8;
|
||||||
|
export type Modulo = Rounding | 9;
|
||||||
|
export type Value = string | number | Decimal;
|
||||||
|
|
||||||
|
// http://mikemcl.github.io/decimal.js/#constructor-properties
|
||||||
|
export interface Config {
|
||||||
|
precision?: number;
|
||||||
|
rounding?: Rounding;
|
||||||
|
toExpNeg?: number;
|
||||||
|
toExpPos?: number;
|
||||||
|
minE?: number;
|
||||||
|
maxE?: number;
|
||||||
|
crypto?: boolean;
|
||||||
|
modulo?: Modulo;
|
||||||
|
defaults?: boolean;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export declare class Decimal {
|
||||||
|
readonly d: number[];
|
||||||
|
readonly e: number;
|
||||||
|
readonly s: number;
|
||||||
|
|
||||||
|
constructor(n: Decimal.Value);
|
||||||
|
|
||||||
|
absoluteValue(): Decimal;
|
||||||
|
abs(): Decimal;
|
||||||
|
|
||||||
|
ceil(): Decimal;
|
||||||
|
|
||||||
|
clampedTo(min: Decimal.Value, max: Decimal.Value): Decimal;
|
||||||
|
clamp(min: Decimal.Value, max: Decimal.Value): Decimal;
|
||||||
|
|
||||||
|
comparedTo(n: Decimal.Value): number;
|
||||||
|
cmp(n: Decimal.Value): number;
|
||||||
|
|
||||||
|
cosine(): Decimal;
|
||||||
|
cos(): Decimal;
|
||||||
|
|
||||||
|
cubeRoot(): Decimal;
|
||||||
|
cbrt(): Decimal;
|
||||||
|
|
||||||
|
decimalPlaces(): number;
|
||||||
|
dp(): number;
|
||||||
|
|
||||||
|
dividedBy(n: Decimal.Value): Decimal;
|
||||||
|
div(n: Decimal.Value): Decimal;
|
||||||
|
|
||||||
|
dividedToIntegerBy(n: Decimal.Value): Decimal;
|
||||||
|
divToInt(n: Decimal.Value): Decimal;
|
||||||
|
|
||||||
|
equals(n: Decimal.Value): boolean;
|
||||||
|
eq(n: Decimal.Value): boolean;
|
||||||
|
|
||||||
|
floor(): Decimal;
|
||||||
|
|
||||||
|
greaterThan(n: Decimal.Value): boolean;
|
||||||
|
gt(n: Decimal.Value): boolean;
|
||||||
|
|
||||||
|
greaterThanOrEqualTo(n: Decimal.Value): boolean;
|
||||||
|
gte(n: Decimal.Value): boolean;
|
||||||
|
|
||||||
|
hyperbolicCosine(): Decimal;
|
||||||
|
cosh(): Decimal;
|
||||||
|
|
||||||
|
hyperbolicSine(): Decimal;
|
||||||
|
sinh(): Decimal;
|
||||||
|
|
||||||
|
hyperbolicTangent(): Decimal;
|
||||||
|
tanh(): Decimal;
|
||||||
|
|
||||||
|
inverseCosine(): Decimal;
|
||||||
|
acos(): Decimal;
|
||||||
|
|
||||||
|
inverseHyperbolicCosine(): Decimal;
|
||||||
|
acosh(): Decimal;
|
||||||
|
|
||||||
|
inverseHyperbolicSine(): Decimal;
|
||||||
|
asinh(): Decimal;
|
||||||
|
|
||||||
|
inverseHyperbolicTangent(): Decimal;
|
||||||
|
atanh(): Decimal;
|
||||||
|
|
||||||
|
inverseSine(): Decimal;
|
||||||
|
asin(): Decimal;
|
||||||
|
|
||||||
|
inverseTangent(): Decimal;
|
||||||
|
atan(): Decimal;
|
||||||
|
|
||||||
|
isFinite(): boolean;
|
||||||
|
|
||||||
|
isInteger(): boolean;
|
||||||
|
isInt(): boolean;
|
||||||
|
|
||||||
|
isNaN(): boolean;
|
||||||
|
|
||||||
|
isNegative(): boolean;
|
||||||
|
isNeg(): boolean;
|
||||||
|
|
||||||
|
isPositive(): boolean;
|
||||||
|
isPos(): boolean;
|
||||||
|
|
||||||
|
isZero(): boolean;
|
||||||
|
|
||||||
|
lessThan(n: Decimal.Value): boolean;
|
||||||
|
lt(n: Decimal.Value): boolean;
|
||||||
|
|
||||||
|
lessThanOrEqualTo(n: Decimal.Value): boolean;
|
||||||
|
lte(n: Decimal.Value): boolean;
|
||||||
|
|
||||||
|
logarithm(n?: Decimal.Value): Decimal;
|
||||||
|
log(n?: Decimal.Value): Decimal;
|
||||||
|
|
||||||
|
minus(n: Decimal.Value): Decimal;
|
||||||
|
sub(n: Decimal.Value): Decimal;
|
||||||
|
|
||||||
|
modulo(n: Decimal.Value): Decimal;
|
||||||
|
mod(n: Decimal.Value): Decimal;
|
||||||
|
|
||||||
|
naturalExponential(): Decimal;
|
||||||
|
exp(): Decimal;
|
||||||
|
|
||||||
|
naturalLogarithm(): Decimal;
|
||||||
|
ln(): Decimal;
|
||||||
|
|
||||||
|
negated(): Decimal;
|
||||||
|
neg(): Decimal;
|
||||||
|
|
||||||
|
plus(n: Decimal.Value): Decimal;
|
||||||
|
add(n: Decimal.Value): Decimal;
|
||||||
|
|
||||||
|
precision(includeZeros?: boolean): number;
|
||||||
|
sd(includeZeros?: boolean): number;
|
||||||
|
|
||||||
|
round(): Decimal;
|
||||||
|
|
||||||
|
sine() : Decimal;
|
||||||
|
sin() : Decimal;
|
||||||
|
|
||||||
|
squareRoot(): Decimal;
|
||||||
|
sqrt(): Decimal;
|
||||||
|
|
||||||
|
tangent() : Decimal;
|
||||||
|
tan() : Decimal;
|
||||||
|
|
||||||
|
times(n: Decimal.Value): Decimal;
|
||||||
|
mul(n: Decimal.Value) : Decimal;
|
||||||
|
|
||||||
|
toBinary(significantDigits?: number): string;
|
||||||
|
toBinary(significantDigits: number, rounding: Decimal.Rounding): string;
|
||||||
|
|
||||||
|
toDecimalPlaces(decimalPlaces?: number): Decimal;
|
||||||
|
toDecimalPlaces(decimalPlaces: number, rounding: Decimal.Rounding): Decimal;
|
||||||
|
toDP(decimalPlaces?: number): Decimal;
|
||||||
|
toDP(decimalPlaces: number, rounding: Decimal.Rounding): Decimal;
|
||||||
|
|
||||||
|
toExponential(decimalPlaces?: number): string;
|
||||||
|
toExponential(decimalPlaces: number, rounding: Decimal.Rounding): string;
|
||||||
|
|
||||||
|
toFixed(decimalPlaces?: number): string;
|
||||||
|
toFixed(decimalPlaces: number, rounding: Decimal.Rounding): string;
|
||||||
|
|
||||||
|
toFraction(max_denominator?: Decimal.Value): Decimal[];
|
||||||
|
|
||||||
|
toHexadecimal(significantDigits?: number): string;
|
||||||
|
toHexadecimal(significantDigits: number, rounding: Decimal.Rounding): string;
|
||||||
|
toHex(significantDigits?: number): string;
|
||||||
|
toHex(significantDigits: number, rounding?: Decimal.Rounding): string;
|
||||||
|
|
||||||
|
toJSON(): string;
|
||||||
|
|
||||||
|
toNearest(n: Decimal.Value, rounding?: Decimal.Rounding): Decimal;
|
||||||
|
|
||||||
|
toNumber(): number;
|
||||||
|
|
||||||
|
toOctal(significantDigits?: number): string;
|
||||||
|
toOctal(significantDigits: number, rounding: Decimal.Rounding): string;
|
||||||
|
|
||||||
|
toPower(n: Decimal.Value): Decimal;
|
||||||
|
pow(n: Decimal.Value): Decimal;
|
||||||
|
|
||||||
|
toPrecision(significantDigits?: number): string;
|
||||||
|
toPrecision(significantDigits: number, rounding: Decimal.Rounding): string;
|
||||||
|
|
||||||
|
toSignificantDigits(significantDigits?: number): Decimal;
|
||||||
|
toSignificantDigits(significantDigits: number, rounding: Decimal.Rounding): Decimal;
|
||||||
|
toSD(significantDigits?: number): Decimal;
|
||||||
|
toSD(significantDigits: number, rounding: Decimal.Rounding): Decimal;
|
||||||
|
|
||||||
|
toString(): string;
|
||||||
|
|
||||||
|
truncated(): Decimal;
|
||||||
|
trunc(): Decimal;
|
||||||
|
|
||||||
|
valueOf(): string;
|
||||||
|
|
||||||
|
static abs(n: Decimal.Value): Decimal;
|
||||||
|
static acos(n: Decimal.Value): Decimal;
|
||||||
|
static acosh(n: Decimal.Value): Decimal;
|
||||||
|
static add(x: Decimal.Value, y: Decimal.Value): Decimal;
|
||||||
|
static asin(n: Decimal.Value): Decimal;
|
||||||
|
static asinh(n: Decimal.Value): Decimal;
|
||||||
|
static atan(n: Decimal.Value): Decimal;
|
||||||
|
static atanh(n: Decimal.Value): Decimal;
|
||||||
|
static atan2(y: Decimal.Value, x: Decimal.Value): Decimal;
|
||||||
|
static cbrt(n: Decimal.Value): Decimal;
|
||||||
|
static ceil(n: Decimal.Value): Decimal;
|
||||||
|
static clamp(n: Decimal.Value, min: Decimal.Value, max: Decimal.Value): Decimal;
|
||||||
|
static clone(object?: Decimal.Config): Decimal.Constructor;
|
||||||
|
static config(object: Decimal.Config): Decimal.Constructor;
|
||||||
|
static cos(n: Decimal.Value): Decimal;
|
||||||
|
static cosh(n: Decimal.Value): Decimal;
|
||||||
|
static div(x: Decimal.Value, y: Decimal.Value): Decimal;
|
||||||
|
static exp(n: Decimal.Value): Decimal;
|
||||||
|
static floor(n: Decimal.Value): Decimal;
|
||||||
|
static hypot(...n: Decimal.Value[]): Decimal;
|
||||||
|
static isDecimal(object: any): object is Decimal;
|
||||||
|
static ln(n: Decimal.Value): Decimal;
|
||||||
|
static log(n: Decimal.Value, base?: Decimal.Value): Decimal;
|
||||||
|
static log2(n: Decimal.Value): Decimal;
|
||||||
|
static log10(n: Decimal.Value): Decimal;
|
||||||
|
static max(...n: Decimal.Value[]): Decimal;
|
||||||
|
static min(...n: Decimal.Value[]): Decimal;
|
||||||
|
static mod(x: Decimal.Value, y: Decimal.Value): Decimal;
|
||||||
|
static mul(x: Decimal.Value, y: Decimal.Value): Decimal;
|
||||||
|
static noConflict(): Decimal.Constructor; // Browser only
|
||||||
|
static pow(base: Decimal.Value, exponent: Decimal.Value): Decimal;
|
||||||
|
static random(significantDigits?: number): Decimal;
|
||||||
|
static round(n: Decimal.Value): Decimal;
|
||||||
|
static set(object: Decimal.Config): Decimal.Constructor;
|
||||||
|
static sign(n: Decimal.Value): number;
|
||||||
|
static sin(n: Decimal.Value): Decimal;
|
||||||
|
static sinh(n: Decimal.Value): Decimal;
|
||||||
|
static sqrt(n: Decimal.Value): Decimal;
|
||||||
|
static sub(x: Decimal.Value, y: Decimal.Value): Decimal;
|
||||||
|
static sum(...n: Decimal.Value[]): Decimal;
|
||||||
|
static tan(n: Decimal.Value): Decimal;
|
||||||
|
static tanh(n: Decimal.Value): Decimal;
|
||||||
|
static trunc(n: Decimal.Value): Decimal;
|
||||||
|
|
||||||
|
static readonly default?: Decimal.Constructor;
|
||||||
|
static readonly Decimal?: Decimal.Constructor;
|
||||||
|
|
||||||
|
static readonly precision: number;
|
||||||
|
static readonly rounding: Decimal.Rounding;
|
||||||
|
static readonly toExpNeg: number;
|
||||||
|
static readonly toExpPos: number;
|
||||||
|
static readonly minE: number;
|
||||||
|
static readonly maxE: number;
|
||||||
|
static readonly crypto: boolean;
|
||||||
|
static readonly modulo: Decimal.Modulo;
|
||||||
|
|
||||||
|
static readonly ROUND_UP: 0;
|
||||||
|
static readonly ROUND_DOWN: 1;
|
||||||
|
static readonly ROUND_CEIL: 2;
|
||||||
|
static readonly ROUND_FLOOR: 3;
|
||||||
|
static readonly ROUND_HALF_UP: 4;
|
||||||
|
static readonly ROUND_HALF_DOWN: 5;
|
||||||
|
static readonly ROUND_HALF_EVEN: 6;
|
||||||
|
static readonly ROUND_HALF_CEIL: 7;
|
||||||
|
static readonly ROUND_HALF_FLOOR: 8;
|
||||||
|
static readonly EUCLID: 9;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare type Exact<A, W> = (A extends unknown ? (W extends A ? {
|
||||||
|
[K in keyof A]: Exact<A[K], W[K]>;
|
||||||
|
} : W) : never) | (A extends Narrowable ? A : never);
|
||||||
|
|
||||||
|
export declare function getRuntime(): GetRuntimeOutput;
|
||||||
|
|
||||||
|
declare type GetRuntimeOutput = {
|
||||||
|
id: RuntimeName;
|
||||||
|
prettyName: string;
|
||||||
|
isEdge: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
declare class JsonNull extends NullTypesEnumValue {
|
||||||
|
#private;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates more strict variant of an enum which, unlike regular enum,
|
||||||
|
* throws on non-existing property access. This can be useful in following situations:
|
||||||
|
* - we have an API, that accepts both `undefined` and `SomeEnumType` as an input
|
||||||
|
* - enum values are generated dynamically from DMMF.
|
||||||
|
*
|
||||||
|
* In that case, if using normal enums and no compile-time typechecking, using non-existing property
|
||||||
|
* will result in `undefined` value being used, which will be accepted. Using strict enum
|
||||||
|
* in this case will help to have a runtime exception, telling you that you are probably doing something wrong.
|
||||||
|
*
|
||||||
|
* Note: if you need to check for existence of a value in the enum you can still use either
|
||||||
|
* `in` operator or `hasOwnProperty` function.
|
||||||
|
*
|
||||||
|
* @param definition
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export declare function makeStrictEnum<T extends Record<PropertyKey, string | number>>(definition: T): T;
|
||||||
|
|
||||||
|
declare type Narrowable = string | number | bigint | boolean | [];
|
||||||
|
|
||||||
|
declare class NullTypesEnumValue extends ObjectEnumValue {
|
||||||
|
_getNamespace(): string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base class for unique values of object-valued enums.
|
||||||
|
*/
|
||||||
|
declare abstract class ObjectEnumValue {
|
||||||
|
constructor(arg?: symbol);
|
||||||
|
abstract _getNamespace(): string;
|
||||||
|
_getName(): string;
|
||||||
|
toString(): string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export declare const objectEnumValues: {
|
||||||
|
classes: {
|
||||||
|
DbNull: typeof DbNull;
|
||||||
|
JsonNull: typeof JsonNull;
|
||||||
|
AnyNull: typeof AnyNull;
|
||||||
|
};
|
||||||
|
instances: {
|
||||||
|
DbNull: DbNull;
|
||||||
|
JsonNull: JsonNull;
|
||||||
|
AnyNull: AnyNull;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
declare type Operation = 'findFirst' | 'findFirstOrThrow' | 'findUnique' | 'findUniqueOrThrow' | 'findMany' | 'create' | 'createMany' | 'createManyAndReturn' | 'update' | 'updateMany' | 'updateManyAndReturn' | 'upsert' | 'delete' | 'deleteMany' | 'aggregate' | 'count' | 'groupBy' | '$queryRaw' | '$executeRaw' | '$queryRawUnsafe' | '$executeRawUnsafe' | 'findRaw' | 'aggregateRaw' | '$runCommandRaw';
|
||||||
|
|
||||||
|
declare namespace Public {
|
||||||
|
export {
|
||||||
|
validator
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export { Public }
|
||||||
|
|
||||||
|
declare type RuntimeName = 'workerd' | 'deno' | 'netlify' | 'node' | 'bun' | 'edge-light' | '';
|
||||||
|
|
||||||
|
declare function validator<V>(): <S>(select: Exact<S, V>) => S;
|
||||||
|
|
||||||
|
declare function validator<C, M extends Exclude<keyof C, `$${string}`>, O extends keyof C[M] & Operation>(client: C, model: M, operation: O): <S>(select: Exact<S, Args<C[M], O>>) => S;
|
||||||
|
|
||||||
|
declare function validator<C, M extends Exclude<keyof C, `$${string}`>, O extends keyof C[M] & Operation, P extends keyof Args<C[M], O>>(client: C, model: M, operation: O, prop: P): <S>(select: Exact<S, Args<C[M], O>[P]>) => S;
|
||||||
|
|
||||||
|
export { }
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
// This is your Prisma schema file,
|
||||||
|
// learn more about it in the docs: https://pris.ly/d/prisma-schema
|
||||||
|
|
||||||
|
generator client {
|
||||||
|
provider = "prisma-client-js"
|
||||||
|
binaryTargets = ["native", "debian-openssl-3.0.x"]
|
||||||
|
output = "/app/generated/prisma-client"
|
||||||
|
}
|
||||||
|
|
||||||
|
datasource db {
|
||||||
|
provider = "postgresql"
|
||||||
|
// NOTE: When using mysql or sqlserver, uncomment the @db.Text annotations in model Account below
|
||||||
|
// Further reading:
|
||||||
|
// https://next-auth.js.org/adapters/prisma#create-the-prisma-schema
|
||||||
|
// https://www.prisma.io/docs/reference/api-reference/prisma-schema-reference#string
|
||||||
|
url = env("DATABASE_URL")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Necessary for Next auth
|
||||||
|
model Account {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
userId String
|
||||||
|
type String
|
||||||
|
provider String
|
||||||
|
providerAccountId String
|
||||||
|
refresh_token String? @db.Text
|
||||||
|
access_token String? // @db.Text
|
||||||
|
expires_at Int?
|
||||||
|
token_type String?
|
||||||
|
scope String?
|
||||||
|
id_token String? // @db.Text
|
||||||
|
session_state String?
|
||||||
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
refresh_token_expires_in Int?
|
||||||
|
|
||||||
|
@@unique([provider, providerAccountId])
|
||||||
|
}
|
||||||
|
|
||||||
|
model Session {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
sessionToken String @unique
|
||||||
|
userId String
|
||||||
|
expires DateTime
|
||||||
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
}
|
||||||
|
|
||||||
|
model User {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
name String?
|
||||||
|
email String? @unique
|
||||||
|
emailVerified DateTime?
|
||||||
|
image String?
|
||||||
|
accounts Account[]
|
||||||
|
sessions Session[]
|
||||||
|
files File[] // Relation to the File model
|
||||||
|
}
|
||||||
|
|
||||||
|
model VerificationToken {
|
||||||
|
identifier String
|
||||||
|
token String @unique
|
||||||
|
expires DateTime
|
||||||
|
|
||||||
|
@@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())
|
||||||
|
description String @default("")
|
||||||
|
uploadedBy User? @relation(fields: [uploadedById], references: [id], onDelete: SetNull)
|
||||||
|
uploadedById String?
|
||||||
|
public Boolean @default(false) // Indicates if the file is public or private
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "./index"
|
||||||
@@ -0,0 +1,225 @@
|
|||||||
|
|
||||||
|
/* !!! This is code generated by Prisma. Do not edit directly. !!!
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
|
||||||
|
const {
|
||||||
|
Decimal,
|
||||||
|
objectEnumValues,
|
||||||
|
makeStrictEnum,
|
||||||
|
Public,
|
||||||
|
getRuntime,
|
||||||
|
skip
|
||||||
|
} = require('./runtime/index-browser.js')
|
||||||
|
|
||||||
|
|
||||||
|
const Prisma = {}
|
||||||
|
|
||||||
|
exports.Prisma = Prisma
|
||||||
|
exports.$Enums = {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prisma Client JS version: 6.8.2
|
||||||
|
* Query Engine version: 2060c79ba17c6bb9f5823312b6f6b7f4a845738e
|
||||||
|
*/
|
||||||
|
Prisma.prismaVersion = {
|
||||||
|
client: "6.8.2",
|
||||||
|
engine: "2060c79ba17c6bb9f5823312b6f6b7f4a845738e"
|
||||||
|
}
|
||||||
|
|
||||||
|
Prisma.PrismaClientKnownRequestError = () => {
|
||||||
|
const runtimeName = getRuntime().prettyName;
|
||||||
|
throw new Error(`PrismaClientKnownRequestError is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
|
||||||
|
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
|
||||||
|
)};
|
||||||
|
Prisma.PrismaClientUnknownRequestError = () => {
|
||||||
|
const runtimeName = getRuntime().prettyName;
|
||||||
|
throw new Error(`PrismaClientUnknownRequestError is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
|
||||||
|
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
|
||||||
|
)}
|
||||||
|
Prisma.PrismaClientRustPanicError = () => {
|
||||||
|
const runtimeName = getRuntime().prettyName;
|
||||||
|
throw new Error(`PrismaClientRustPanicError is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
|
||||||
|
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
|
||||||
|
)}
|
||||||
|
Prisma.PrismaClientInitializationError = () => {
|
||||||
|
const runtimeName = getRuntime().prettyName;
|
||||||
|
throw new Error(`PrismaClientInitializationError is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
|
||||||
|
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
|
||||||
|
)}
|
||||||
|
Prisma.PrismaClientValidationError = () => {
|
||||||
|
const runtimeName = getRuntime().prettyName;
|
||||||
|
throw new Error(`PrismaClientValidationError is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
|
||||||
|
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
|
||||||
|
)}
|
||||||
|
Prisma.Decimal = Decimal
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Re-export of sql-template-tag
|
||||||
|
*/
|
||||||
|
Prisma.sql = () => {
|
||||||
|
const runtimeName = getRuntime().prettyName;
|
||||||
|
throw new Error(`sqltag is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
|
||||||
|
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
|
||||||
|
)}
|
||||||
|
Prisma.empty = () => {
|
||||||
|
const runtimeName = getRuntime().prettyName;
|
||||||
|
throw new Error(`empty is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
|
||||||
|
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
|
||||||
|
)}
|
||||||
|
Prisma.join = () => {
|
||||||
|
const runtimeName = getRuntime().prettyName;
|
||||||
|
throw new Error(`join is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
|
||||||
|
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
|
||||||
|
)}
|
||||||
|
Prisma.raw = () => {
|
||||||
|
const runtimeName = getRuntime().prettyName;
|
||||||
|
throw new Error(`raw is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
|
||||||
|
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
|
||||||
|
)}
|
||||||
|
Prisma.validator = Public.validator
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extensions
|
||||||
|
*/
|
||||||
|
Prisma.getExtensionContext = () => {
|
||||||
|
const runtimeName = getRuntime().prettyName;
|
||||||
|
throw new Error(`Extensions.getExtensionContext is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
|
||||||
|
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
|
||||||
|
)}
|
||||||
|
Prisma.defineExtension = () => {
|
||||||
|
const runtimeName = getRuntime().prettyName;
|
||||||
|
throw new Error(`Extensions.defineExtension is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
|
||||||
|
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
|
||||||
|
)}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shorthand utilities for JSON filtering
|
||||||
|
*/
|
||||||
|
Prisma.DbNull = objectEnumValues.instances.DbNull
|
||||||
|
Prisma.JsonNull = objectEnumValues.instances.JsonNull
|
||||||
|
Prisma.AnyNull = objectEnumValues.instances.AnyNull
|
||||||
|
|
||||||
|
Prisma.NullTypes = {
|
||||||
|
DbNull: objectEnumValues.classes.DbNull,
|
||||||
|
JsonNull: objectEnumValues.classes.JsonNull,
|
||||||
|
AnyNull: objectEnumValues.classes.AnyNull
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enums
|
||||||
|
*/
|
||||||
|
|
||||||
|
exports.Prisma.TransactionIsolationLevel = makeStrictEnum({
|
||||||
|
ReadUncommitted: 'ReadUncommitted',
|
||||||
|
ReadCommitted: 'ReadCommitted',
|
||||||
|
RepeatableRead: 'RepeatableRead',
|
||||||
|
Serializable: 'Serializable'
|
||||||
|
});
|
||||||
|
|
||||||
|
exports.Prisma.AccountScalarFieldEnum = {
|
||||||
|
id: 'id',
|
||||||
|
userId: 'userId',
|
||||||
|
type: 'type',
|
||||||
|
provider: 'provider',
|
||||||
|
providerAccountId: 'providerAccountId',
|
||||||
|
refresh_token: 'refresh_token',
|
||||||
|
access_token: 'access_token',
|
||||||
|
expires_at: 'expires_at',
|
||||||
|
token_type: 'token_type',
|
||||||
|
scope: 'scope',
|
||||||
|
id_token: 'id_token',
|
||||||
|
session_state: 'session_state',
|
||||||
|
refresh_token_expires_in: 'refresh_token_expires_in'
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.Prisma.SessionScalarFieldEnum = {
|
||||||
|
id: 'id',
|
||||||
|
sessionToken: 'sessionToken',
|
||||||
|
userId: 'userId',
|
||||||
|
expires: 'expires'
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.Prisma.UserScalarFieldEnum = {
|
||||||
|
id: 'id',
|
||||||
|
name: 'name',
|
||||||
|
email: 'email',
|
||||||
|
emailVerified: 'emailVerified',
|
||||||
|
image: 'image'
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.Prisma.VerificationTokenScalarFieldEnum = {
|
||||||
|
identifier: 'identifier',
|
||||||
|
token: 'token',
|
||||||
|
expires: 'expires'
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.Prisma.FileScalarFieldEnum = {
|
||||||
|
id: 'id',
|
||||||
|
url: 'url',
|
||||||
|
name: 'name',
|
||||||
|
size: 'size',
|
||||||
|
extension: 'extension',
|
||||||
|
uploadDate: 'uploadDate',
|
||||||
|
description: 'description',
|
||||||
|
uploadedById: 'uploadedById',
|
||||||
|
public: 'public'
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.Prisma.SortOrder = {
|
||||||
|
asc: 'asc',
|
||||||
|
desc: 'desc'
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.Prisma.QueryMode = {
|
||||||
|
default: 'default',
|
||||||
|
insensitive: 'insensitive'
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.Prisma.NullsOrder = {
|
||||||
|
first: 'first',
|
||||||
|
last: 'last'
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
exports.Prisma.ModelName = {
|
||||||
|
Account: 'Account',
|
||||||
|
Session: 'Session',
|
||||||
|
User: 'User',
|
||||||
|
VerificationToken: 'VerificationToken',
|
||||||
|
File: 'File'
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is a stub Prisma Client that will error at runtime if called.
|
||||||
|
*/
|
||||||
|
class PrismaClient {
|
||||||
|
constructor() {
|
||||||
|
return new Proxy(this, {
|
||||||
|
get(target, prop) {
|
||||||
|
let message
|
||||||
|
const runtime = getRuntime()
|
||||||
|
if (runtime.isEdge) {
|
||||||
|
message = `PrismaClient is not configured to run in ${runtime.prettyName}. In order to run Prisma Client on edge runtime, either:
|
||||||
|
- Use Prisma Accelerate: https://pris.ly/d/accelerate
|
||||||
|
- Use Driver Adapters: https://pris.ly/d/driver-adapters
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
message = 'PrismaClient is unable to run in this browser environment, or has been bundled for the browser (running in `' + runtime.prettyName + '`).'
|
||||||
|
}
|
||||||
|
|
||||||
|
message += `
|
||||||
|
If this is unexpected, please open an issue: https://pris.ly/prisma-prisma-bug-report`
|
||||||
|
|
||||||
|
throw new Error(message)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.PrismaClient = PrismaClient
|
||||||
|
|
||||||
|
Object.assign(exports, Prisma)
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import LoadingSkeleton from './LoadingSkeleton';
|
||||||
|
|
||||||
|
export default function Loading() {
|
||||||
|
// You can add any UI inside Loading, including a Skeleton.
|
||||||
|
return <LoadingSkeleton />
|
||||||
|
}
|
||||||
@@ -1,21 +1,80 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { auth } from "~/server/auth";
|
import { useEffect, useState } from "react";
|
||||||
import { HydrateClient } from "~/trpc/server";
|
|
||||||
import FileGrid from "~/app/_components/FileGrid";
|
import FileGrid from "~/app/_components/FileGrid";
|
||||||
import UploadForm from "~/app/_components/UploadForm";
|
import UploadForm from "~/app/_components/UploadForm";
|
||||||
import { Toaster } from "react-hot-toast";
|
import { Toaster } from "react-hot-toast";
|
||||||
|
import { Suspense } from "react";
|
||||||
|
import LoadingSkeleton from "./LoadingSkeleton";
|
||||||
|
|
||||||
export default async function Home() {
|
// Custom fallback for FileGrid
|
||||||
const session = await auth();
|
function FileGridFallback() {
|
||||||
|
return (
|
||||||
|
<div className="grid w-full max-w-4xl animate-pulse grid-cols-1 gap-6 sm:grid-cols-2 md:grid-cols-3">
|
||||||
|
{[...Array(6)].map((_, i) => (
|
||||||
|
<div key={i} className="flex flex-col items-center">
|
||||||
|
<span className="mb-2 text-lg text-white/60">Loading</span>
|
||||||
|
<div className="h-32 w-full rounded bg-white/10" />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom fallback for UploadForm
|
||||||
|
function UploadFormFallback() {
|
||||||
|
return (
|
||||||
|
<div className="mt-8 flex w-full max-w-md animate-pulse flex-col gap-4">
|
||||||
|
<div className="h-10 rounded bg-white/20" />
|
||||||
|
<div className="h-10 rounded bg-white/10" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Home() {
|
||||||
|
const [session, setSession] = useState<{ user?: any } | null>(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
useEffect(() => {
|
||||||
|
async function fetchSession() {
|
||||||
|
setLoading(true);
|
||||||
|
const res = await fetch("/api/auth/session");
|
||||||
|
const data = await res.json();
|
||||||
|
setSession(data);
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
fetchSession();
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<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"
|
||||||
@@ -32,28 +91,59 @@ export default async function Home() {
|
|||||||
{/* Conditionally render FileGrid and UploadForm if the user is logged in */}
|
{/* Conditionally render FileGrid and UploadForm if the user is logged in */}
|
||||||
{session?.user ? (
|
{session?.user ? (
|
||||||
<>
|
<>
|
||||||
<FileGrid session={session} />
|
<Suspense fallback={<FileGridFallback />}>
|
||||||
<UploadForm/>
|
<FileGrid session={session as { user: { id: string } }} />
|
||||||
|
</Suspense>
|
||||||
|
<Suspense fallback={<UploadFormFallback />}>
|
||||||
|
<UploadForm />
|
||||||
|
</Suspense>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : !loading ? (
|
||||||
<p className="text-center text-2xl text-white">
|
<p className="text-center text-2xl text-white">
|
||||||
Please log in to upload and view files.
|
Please log in to upload and view files.
|
||||||
</p>
|
</p>
|
||||||
)}
|
) : null}
|
||||||
{!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
|
{!loading ? (
|
||||||
href={session ? "/api/auth/signout" : "/api/auth/signin"}
|
<Link
|
||||||
className="rounded-full bg-white/10 px-10 py-3 font-semibold no-underline transition hover:bg-white/20"
|
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"
|
||||||
{session ? "Sign out" : "Sign in"}
|
>
|
||||||
</Link>
|
{session ? "Sign out" : "Sign in"}
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
<div className="flex h-10 items-center justify-center">
|
||||||
|
<svg
|
||||||
|
className="h-6 w-6 animate-spin text-white/70"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<circle
|
||||||
|
className="opacity-25"
|
||||||
|
cx="12"
|
||||||
|
cy="12"
|
||||||
|
r="10"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="4"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
className="opacity-75"
|
||||||
|
fill="currentColor"
|
||||||
|
d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</HydrateClient>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default Home;
|
||||||
|
|||||||
@@ -0,0 +1,122 @@
|
|||||||
|
"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;
|
||||||
|
ownerId: 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} share={false} />
|
||||||
|
</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}
|
||||||
|
isPublic={file.isPublic} // Check if the file is public
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import React, { Suspense } from "react";
|
||||||
|
import { HomeButton } from "~/app/_components/HomeButton"; // Import the client component
|
||||||
|
import { Toaster } from "react-hot-toast";
|
||||||
|
import {
|
||||||
|
FileActionsContainer,
|
||||||
|
} from "~/app/_components/ActionButtons"; // Import the client component
|
||||||
|
|
||||||
|
const LoadingSkeleton: React.FC = () => (
|
||||||
|
<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">
|
||||||
|
<HomeButton />
|
||||||
|
</div>
|
||||||
|
<Toaster position="top-right" reverseOrder={false} />
|
||||||
|
<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">
|
||||||
|
<svg
|
||||||
|
className="h-6 w-6 animate-spin text-white/70"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<circle
|
||||||
|
className="opacity-25"
|
||||||
|
cx="12"
|
||||||
|
cy="12"
|
||||||
|
r="10"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="4"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
className="opacity-75"
|
||||||
|
fill="currentColor"
|
||||||
|
d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div className="w-full max-w-md rounded-lg bg-white/10 p-6 text-white shadow-md">
|
||||||
|
<p>
|
||||||
|
<strong>Name:</strong> <span className="inline-block h-6 w-24 rounded bg-white/20 animate-pulse align-middle ml-2" />
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>Size:</strong> <span className="inline-block h-6 w-16 rounded bg-white/20 animate-pulse align-middle ml-2" />
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>Owner:</strong> <span className="inline-block h-6 w-20 rounded bg-white/20 animate-pulse align-middle ml-2" />
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>Upload Date:</strong> <span className="inline-block h-6 w-28 rounded bg-white/20 animate-pulse align-middle ml-2" />
|
||||||
|
</p>
|
||||||
|
<div>
|
||||||
|
<strong>Description:</strong> <span className="inline-block h-6 w-40 rounded bg-white/20 animate-pulse align-middle ml-2" />
|
||||||
|
</div>
|
||||||
|
<div className="mt-4 flex justify-center">
|
||||||
|
<FileActionsContainer
|
||||||
|
fileId={""}
|
||||||
|
fileName={""}
|
||||||
|
fileUrl={""}
|
||||||
|
isOwner={false}
|
||||||
|
isPublic={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default LoadingSkeleton;
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import LoadingSkeleton from './LoadingSkeleton';
|
||||||
|
|
||||||
|
export default function Loading() {
|
||||||
|
// You can add any UI inside Loading, including a Skeleton.
|
||||||
|
return <LoadingSkeleton />
|
||||||
|
}
|
||||||
@@ -1,14 +1,21 @@
|
|||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
|
import { Suspense } from "react";
|
||||||
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 { FileActionsContainer } from "~/app/_components/ActionButtons"; // Import the client component
|
import {
|
||||||
import Head from "next/head";
|
FileActionsContainer,
|
||||||
|
FileDescriptionContainer,
|
||||||
|
} from "~/app/_components/ActionButtons"; // Import the client component
|
||||||
|
import type { Metadata } from "next";
|
||||||
|
import { checkOwner } from "~/utils/checkOwner"; // Import the client component
|
||||||
|
import { auth } from "~/server/auth";
|
||||||
|
|
||||||
interface FileDetails {
|
interface FileDetails {
|
||||||
name: string;
|
name: string;
|
||||||
size: number;
|
size: number;
|
||||||
owner: string;
|
owner: string;
|
||||||
|
ownerId: string;
|
||||||
ownerAvatar: string | null;
|
ownerAvatar: string | null;
|
||||||
uploadDate: string;
|
uploadDate: string;
|
||||||
id: string;
|
id: string;
|
||||||
@@ -16,15 +23,57 @@ 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> {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${process.env.NEXT_PUBLIC_PAGE_URL}/api/files/share?id=${encodeURIComponent(
|
`${process.env.NEXT_PUBLIC_PAGE_URL}/api/files/share?id=${encodeURIComponent(
|
||||||
fileId
|
fileId,
|
||||||
)}`,
|
)}`,
|
||||||
{ cache: "no-store" }
|
{ cache: "no-store" },
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
@@ -41,15 +90,17 @@ async function fetchFileDetails(fileId: string): Promise<FileDetails | null> {
|
|||||||
export default async function FilePreviewContainer({
|
export default async function FilePreviewContainer({
|
||||||
searchParams,
|
searchParams,
|
||||||
}: {
|
}: {
|
||||||
searchParams: { id?: string };
|
searchParams: Promise<{ id?: string }>;
|
||||||
}) {
|
}) {
|
||||||
const fileId = searchParams.id;
|
const resolvedSearchParams = await searchParams; // Resolve the promise
|
||||||
|
const fileId = resolvedSearchParams.id;
|
||||||
|
|
||||||
if (!fileId) {
|
if (!fileId) {
|
||||||
notFound();
|
notFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
const fileDetails = await fetchFileDetails(fileId);
|
const fileDetails = await fetchFileDetails(fileId);
|
||||||
|
const session = await auth();
|
||||||
|
|
||||||
if (!fileDetails) {
|
if (!fileDetails) {
|
||||||
return (
|
return (
|
||||||
@@ -68,97 +119,75 @@ 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">
|
||||||
<Head>
|
<div className="absolute top-4 left-4">
|
||||||
<title>{fileDetails.name} - File Details</title>
|
<HomeButton />
|
||||||
<meta
|
</div>
|
||||||
property="og:title"
|
<Toaster position="top-right" reverseOrder={false} />
|
||||||
content={`${fileDetails.name} - File Details`}
|
<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]">
|
||||||
<meta
|
<span className="text-[hsl(280,100%,70%)]">File</span> Details
|
||||||
property="og:description"
|
</h1>
|
||||||
content={`Size: ${
|
<div className="mt-6">
|
||||||
fileDetails.size > 1024 * 1024 * 1024
|
{fileDetails.type !== "unknown" && (
|
||||||
|
<Suspense fallback={<div className="text-white">Loading...</div>}>
|
||||||
|
<FilePreview fileId={fileDetails.id} fileType={fileDetails.type} share={true} />
|
||||||
|
</Suspense>
|
||||||
|
)}
|
||||||
|
</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
|
|
||||||
? (fileDetails.size / 1024).toFixed(2) + " KB"
|
|
||||||
: fileDetails.size + " Bytes"
|
|
||||||
}, Owner: ${fileDetails.owner}, Uploaded on: ${new Date(
|
|
||||||
fileDetails.uploadDate
|
|
||||||
).toLocaleString()}`}
|
|
||||||
/>
|
|
||||||
<meta property="og:type" content="website" />
|
|
||||||
<meta
|
|
||||||
property="og:image"
|
|
||||||
content={`${process.env.NEXT_PUBLIC_PAGE_URL}/api/files/serv?id=${fileId}`}
|
|
||||||
/>
|
|
||||||
<meta property="og:image:alt" content={`${fileDetails.name} preview`} />
|
|
||||||
<meta
|
|
||||||
property="og:url"
|
|
||||||
content={`${process.env.NEXT_PUBLIC_PAGE_URL}/share?id=${fileId}`}
|
|
||||||
/>
|
|
||||||
</Head>
|
|
||||||
<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">
|
|
||||||
<HomeButton /> {/* Use the client component */}
|
|
||||||
</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 className="bg-white/10 shadow-md rounded-lg p-6 w-full max-w-md text-white">
|
|
||||||
<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
|
|
||||||
? (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>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<strong>Owner:</strong>{" "}
|
<strong>Owner:</strong>{" "}
|
||||||
|
<Suspense fallback={<div className="text-white">Loading...</div>}>
|
||||||
<img
|
<img
|
||||||
className="rounded-md inline size-5"
|
className="inline size-5 rounded-md"
|
||||||
src={fileDetails.ownerAvatar || ""}
|
src={fileDetails.ownerAvatar || ""}
|
||||||
alt="Owner avatar"
|
alt="Owner avatar"
|
||||||
/>{" "}
|
/>{" "}
|
||||||
{fileDetails.owner}
|
{fileDetails.owner}
|
||||||
</p>
|
</Suspense>
|
||||||
<p>
|
</p>
|
||||||
<strong>Upload Date:</strong>{" "}
|
<p>
|
||||||
|
<strong>Upload Date:</strong>{" "}
|
||||||
|
<Suspense fallback={<div className="text-white">Loading...</div>}>
|
||||||
{new Date(fileDetails.uploadDate).toLocaleString()}
|
{new Date(fileDetails.uploadDate).toLocaleString()}
|
||||||
</p>
|
</Suspense>
|
||||||
<p>
|
</p>
|
||||||
<strong>Description:</strong>{" "}
|
<div>
|
||||||
{fileDetails.description || "No description available"}
|
<strong>Description:</strong>{" "}
|
||||||
</p>
|
<Suspense fallback={<div className="text-white">Loading...</div>}>
|
||||||
<div className="mt-4 flex justify-center">
|
<FileDescriptionContainer
|
||||||
|
fileId={fileDetails.id}
|
||||||
|
fileDescription={fileDetails.description}
|
||||||
|
/>
|
||||||
|
</Suspense>
|
||||||
|
</div>
|
||||||
|
<div className="mt-4 flex justify-center">
|
||||||
|
<Suspense fallback={<div className="text-white">Loading...</div>}>
|
||||||
<FileActionsContainer
|
<FileActionsContainer
|
||||||
fileId={fileDetails.id}
|
fileId={fileDetails.id}
|
||||||
fileName={fileDetails.name}
|
fileName={fileDetails.name}
|
||||||
fileUrl={fileDetails.url}
|
fileUrl={fileDetails.url}
|
||||||
isOwner={fileDetails.isOwner}
|
isOwner={session?.user?.id ? await checkOwner(fileDetails.ownerId, session.user.id) : false}
|
||||||
|
isPublic={fileDetails.isPublic}
|
||||||
/>
|
/>
|
||||||
</div>
|
</Suspense>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</div>
|
||||||
</>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
.markdown-body ul,
|
||||||
|
.markdown-body ol {
|
||||||
|
list-style: initial; /* Ensures bullets or numbers are displayed */
|
||||||
|
margin-left: 1.5em; /* Adds proper indentation */
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body li {
|
||||||
|
margin-bottom: 0.5em; /* Adds spacing between list items */
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
import "github-markdown-css/github-markdown.css";
|
||||||
|
|
||||||
|
interface MarkdownRendererProps {
|
||||||
|
markdownContent: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MarkdownRenderer({ markdownContent }: MarkdownRendererProps) {
|
||||||
|
useEffect(() => {
|
||||||
|
if (markdownContent) {
|
||||||
|
const markdownContainer = document.querySelector("#markdown-preview");
|
||||||
|
if (!markdownContainer) return;
|
||||||
|
|
||||||
|
const codeBlocks = markdownContainer.querySelectorAll("code");
|
||||||
|
|
||||||
|
codeBlocks.forEach((block) => {
|
||||||
|
// Check if the block is already wrapped
|
||||||
|
if (block.parentElement?.classList.contains("code-wrapper")) return;
|
||||||
|
|
||||||
|
// Check if the code block is multiline
|
||||||
|
const isMultiline = block.textContent?.includes("\n");
|
||||||
|
if (!isMultiline) return;
|
||||||
|
|
||||||
|
// Create a wrapper only if it doesn't already exist
|
||||||
|
const wrapper = document.createElement("div");
|
||||||
|
wrapper.className = "code-wrapper"; // Add a class to identify the wrapper
|
||||||
|
wrapper.style.display = "flex";
|
||||||
|
wrapper.style.alignItems = "flex-start";
|
||||||
|
wrapper.style.justifyContent = "space-between";
|
||||||
|
wrapper.style.gap = "8px";
|
||||||
|
wrapper.style.width = "100%";
|
||||||
|
wrapper.style.position = "relative";
|
||||||
|
|
||||||
|
const codeContainer = document.createElement("div");
|
||||||
|
codeContainer.style.flex = "1";
|
||||||
|
codeContainer.appendChild(block.cloneNode(true));
|
||||||
|
|
||||||
|
const button = document.createElement("button");
|
||||||
|
button.innerHTML = `
|
||||||
|
<img src="/icons/copy.svg" alt="Copy" class="h-4 w-4" style="filter: invert(1) sepia(1) saturate(5) hue-rotate(180deg);"/>
|
||||||
|
`;
|
||||||
|
button.className =
|
||||||
|
"copy-button inline-flex items-center justify-center bg-gray-200 rounded hover:bg-gray-300 dark:bg-gray-700 dark:text-gray-200 dark:hover:bg-gray-600 transition";
|
||||||
|
button.style.marginLeft = "8px";
|
||||||
|
button.style.width = "1.5rem";
|
||||||
|
button.style.height = "1.5rem";
|
||||||
|
|
||||||
|
button.addEventListener("click", () => {
|
||||||
|
navigator.clipboard.writeText(block.textContent || "").then(() => {
|
||||||
|
button.innerHTML = `
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
||||||
|
</svg>
|
||||||
|
`;
|
||||||
|
setTimeout(() => {
|
||||||
|
button.innerHTML = `
|
||||||
|
<img src="/icons/copy.svg" alt="Copy" class="h-4 w-4" style="filter: invert(1) sepia(1) saturate(5) hue-rotate(180deg);"/>
|
||||||
|
`;
|
||||||
|
}, 2000);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
wrapper.appendChild(codeContainer);
|
||||||
|
wrapper.appendChild(button);
|
||||||
|
|
||||||
|
// Replace the original block with the wrapper
|
||||||
|
block.replaceWith(wrapper);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [markdownContent]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="markdown-body max-w-full p-4 pt-0 pb-0" id="markdown-preview">
|
||||||
|
<div dangerouslySetInnerHTML={{ __html: markdownContent }} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -19,7 +19,11 @@ export const env = createEnv({
|
|||||||
NODE_ENV: z
|
NODE_ENV: z
|
||||||
.enum(["development", "test", "production"])
|
.enum(["development", "test", "production"])
|
||||||
.default("development"),
|
.default("development"),
|
||||||
|
MINIO_ENDPOINT: z.string(),
|
||||||
|
MINIO_PORT: z.string(),
|
||||||
|
MINIO_ACCESS_KEY: z.string(),
|
||||||
|
MINIO_SECRET_KEY: z.string(),
|
||||||
|
MINIO_BUCKET: z.string(),
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -43,6 +47,11 @@ export const env = createEnv({
|
|||||||
DATABASE_URL: process.env.DATABASE_URL,
|
DATABASE_URL: process.env.DATABASE_URL,
|
||||||
NODE_ENV: process.env.NODE_ENV,
|
NODE_ENV: process.env.NODE_ENV,
|
||||||
NEXT_PUBLIC_PAGE_URL: process.env.NEXT_PUBLIC_PAGE_URL,
|
NEXT_PUBLIC_PAGE_URL: process.env.NEXT_PUBLIC_PAGE_URL,
|
||||||
|
MINIO_ENDPOINT: process.env.MINIO_ENDPOINT,
|
||||||
|
MINIO_PORT: process.env.MINIO_PORT,
|
||||||
|
MINIO_ACCESS_KEY: process.env.MINIO_ACCESS_KEY,
|
||||||
|
MINIO_SECRET_KEY: process.env.MINIO_SECRET_KEY,
|
||||||
|
MINIO_BUCKET: process.env.MINIO_BUCKET,
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially
|
* Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially
|
||||||
|
|||||||
@@ -18,18 +18,20 @@ 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.post.create({
|
return ctx.db.file.create({
|
||||||
data: {
|
data: {
|
||||||
name: input.name,
|
name: input.name,
|
||||||
createdBy: { connect: { id: ctx.session.user.id } },
|
url: "default-url", // Replace with actual URL logic
|
||||||
|
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.post.findFirst({
|
const post = await ctx.db.file.findFirst({
|
||||||
orderBy: { createdAt: "desc" },
|
orderBy: { uploadDate: "desc" }, // Replace 'createdAt' with the correct field name from your schema
|
||||||
where: { createdBy: { id: ctx.session.user.id } },
|
where: { uploadedById: ctx.session.user.id },
|
||||||
});
|
});
|
||||||
|
|
||||||
return post ?? null;
|
return post ?? null;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { PrismaClient } from "@prisma/client";
|
import { PrismaClient } from "~/app/generated/prisma-client";
|
||||||
|
import { withAccelerate } from '@prisma/extension-accelerate'
|
||||||
|
|
||||||
import { env } from "~/env";
|
import { env } from "~/env";
|
||||||
|
|
||||||
@@ -6,7 +7,7 @@ const createPrismaClient = () =>
|
|||||||
new PrismaClient({
|
new PrismaClient({
|
||||||
log:
|
log:
|
||||||
env.NODE_ENV === "development" ? ["query", "error", "warn"] : ["error"],
|
env.NODE_ENV === "development" ? ["query", "error", "warn"] : ["error"],
|
||||||
});
|
}).$extends(withAccelerate());
|
||||||
|
|
||||||
const globalForPrisma = globalThis as unknown as {
|
const globalForPrisma = globalThis as unknown as {
|
||||||
prisma: ReturnType<typeof createPrismaClient> | undefined;
|
prisma: ReturnType<typeof createPrismaClient> | undefined;
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import type { Session } from "next-auth";
|
||||||
|
|
||||||
|
export const checkOwner = async (id: string , session: string) => {
|
||||||
|
console.log("Session:", session);
|
||||||
|
console.log("File ID:", id);
|
||||||
|
if (session) {
|
||||||
|
console.log("Session User ID:", session);
|
||||||
|
console.log("File Owner ID:", id);
|
||||||
|
return id === session;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
// This function takes a file name as input and returns the file type based on its extension.
|
||||||
|
import mime from "mime-types";
|
||||||
|
|
||||||
|
export function getFileType(fileName: string): string {
|
||||||
|
const extension = fileName.split(".").pop()?.toLowerCase();
|
||||||
|
const fileTypes: Record<string, string> = {
|
||||||
|
// Video
|
||||||
|
"mp4": "video/mp4",
|
||||||
|
"webm": "video/webm",
|
||||||
|
"ogg": "video/ogg",
|
||||||
|
// Image
|
||||||
|
"jpg": "image/jpeg",
|
||||||
|
"jpeg": "image/jpeg",
|
||||||
|
"png": "image/png",
|
||||||
|
"gif": "image/gif",
|
||||||
|
"svg": "image/svg+xml",
|
||||||
|
// Audio
|
||||||
|
"mp3": "audio/mpeg",
|
||||||
|
"wav": "audio/wav",
|
||||||
|
// Archive
|
||||||
|
"zip": "archive/zip",
|
||||||
|
"rar": "archive/rar",
|
||||||
|
"jar": "archive/jar",
|
||||||
|
"iso": "archive/iso",
|
||||||
|
// Text
|
||||||
|
"pdf": "text/pdf",
|
||||||
|
"txt": "text/plain",
|
||||||
|
// Code
|
||||||
|
"c": "code/c",
|
||||||
|
"cpp": "code/cpp",
|
||||||
|
"py": "code/python",
|
||||||
|
"js": "code/javascript",
|
||||||
|
"html": "code/html",
|
||||||
|
"css": "code/css",
|
||||||
|
"json": "code/json",
|
||||||
|
"xml": "code/xml",
|
||||||
|
"csv": "code/csv",
|
||||||
|
// Markdown
|
||||||
|
"md": "markdown/markdown",
|
||||||
|
// Applications
|
||||||
|
"exe": "application/executable",
|
||||||
|
"apk": "application/android",
|
||||||
|
};
|
||||||
|
return extension ? fileTypes[extension] ||
|
||||||
|
//get the file type using the mime type library
|
||||||
|
mime.lookup(extension) || "application/octet-stream" : "application/octet-stream";
|
||||||
|
|
||||||
|
};
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { Client } from "minio";
|
||||||
|
import { env } from "~/env";
|
||||||
|
|
||||||
|
export const minioClient = new Client({
|
||||||
|
endPoint: env.MINIO_ENDPOINT,
|
||||||
|
port: parseInt(env.MINIO_PORT, 10),
|
||||||
|
useSSL: false, // Set to true if using HTTPS
|
||||||
|
accessKey: env.MINIO_ACCESS_KEY,
|
||||||
|
secretKey: env.MINIO_SECRET_KEY,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Ensure the bucket exists
|
||||||
|
export async function ensureBucketExists(bucketName: string) {
|
||||||
|
const exists = await minioClient.bucketExists(bucketName);
|
||||||
|
if (!exists) {
|
||||||
|
await minioClient.makeBucket(bucketName, "us-east-1");
|
||||||
|
console.log(`Bucket "${bucketName}" created.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -34,8 +34,8 @@
|
|||||||
"next-env.d.ts",
|
"next-env.d.ts",
|
||||||
"**/*.ts",
|
"**/*.ts",
|
||||||
"**/*.tsx",
|
"**/*.tsx",
|
||||||
"**/*.cjs",
|
// "**/*.cjs",
|
||||||
"**/*.js",
|
// "**/*.js",
|
||||||
".next/types/**/*.ts"
|
".next/types/**/*.ts"
|
||||||
],
|
],
|
||||||
"exclude": ["node_modules"]
|
"exclude": ["node_modules"]
|
||||||
|
|||||||