21 Commits
Author SHA1 Message Date
ZareMateandGitHub 7aeae9020d Fuck git (#17) 2025-05-20 13:14:01 +02:00
zaremate 85fa1942e9 Fuck git 2025-05-20 13:10:37 +02:00
ZareMateandGitHub c9274a0caa Added missing packages (#16) 2025-05-20 12:17:22 +02:00
zaremateandZareMate 58853f38c4 Added missing packages 2025-05-20 10:15:21 +00:00
ZareMateandGitHub 1884189fb2 Prisma postgress fix (#15) 2025-05-20 12:13:56 +02:00
zaremate 118cadefdf Prisma postgress fix 2025-05-20 12:10:52 +02:00
zaremate b14acab08f feat: add loading skeleton components for improved user experience during data fetching 2025-05-19 08:41:09 +02:00
zaremate 60925ee6ac feat: add markdown rendering support and enhance file preview component 2025-05-14 06:02:22 +02:00
zaremate 036f23b777 feat: implement owner check functionality and update file action components 2025-05-12 19:21:57 +02:00
zaremate b368473216 feat: enhance file actions with toast notifications and update public/private toggle logic 2025-05-12 10:11:32 +02:00
zaremate 551cf2e2cb Revert "revert 99397e774c41980ef13b3155e13330ad4eaa7753"
This reverts commit b63e3ae77f.
2025-05-12 09:18:45 +02:00
zaremate bb89a84ba8 docs: update README with additional instructions for running the application 2025-05-12 08:25:08 +02:00
zaremate 90329f6b22 docs: update README to reflect file hosting features and API documentation 2025-05-12 08:15:51 +02:00
zaremate d4661dc8e3 fix: update searchParams type to Promise and resolve before accessing id 2025-05-12 08:07:36 +02:00
zaremate 24d90a9412 fix: remove unused import and clean up file size formatting logic 2025-05-12 07:51:14 +02:00
zaremate 6115851147 feat: implement metadata generation for file details and improve error handling 2025-05-12 07:41:51 +02:00
zaremate 011695cf46 feat: add meta tags for improved SEO on file not found page 2025-05-12 07:25:23 +02:00
zaremate 9f04130726 fix: clean up imports and formatting in FilePreviewContainer 2025-05-12 07:20:03 +02:00
zaremate 82d1e74af1 feat: refactor file download to use MinIO for file retrieval 2025-05-12 07:12:43 +02:00
zaremate 453264d9cd fix: include fileId in Content-Disposition header for file downloads 2025-05-11 03:20:14 +02:00
zaremate b63e3ae77f revert 99397e774c
revert WIP: implement public file visibility feature and enhance search functionality
2025-05-09 08:55:45 +00:00
50 changed files with 15621 additions and 280 deletions
+74 -13
View File
@@ -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
+223
View File
@@ -11,13 +11,17 @@
"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", "@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", "dompurify": "^3.2.5",
"github-markdown-css": "^5.8.1",
"gray-matter": "^4.0.3",
"mermaid": "^11.6.0", "mermaid": "^11.6.0",
"minio": "^8.0.5", "minio": "^8.0.5",
"next": "^15.2.3", "next": "^15.2.3",
@@ -27,7 +31,9 @@
"react-hot-toast": "^2.5.2", "react-hot-toast": "^2.5.2",
"react-markdown": "^10.1.0", "react-markdown": "^10.1.0",
"rehype-raw": "^7.0.0", "rehype-raw": "^7.0.0",
"remark": "^15.0.1",
"remark-gfm": "^4.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"
@@ -36,6 +42,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",
@@ -1541,6 +1548,17 @@
"devOptional": true, "devOptional": true,
"license": "Apache-2.0" "license": "Apache-2.0"
}, },
"node_modules/@prisma/extension-accelerate": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@prisma/extension-accelerate/-/extension-accelerate-2.0.0.tgz",
"integrity": "sha512-8phE1FQ/sqNQM5VRnWog2jp3r+/acffIJR1D7QHPk8d/WHdKUyLhIVSKnd1Gq+5orwzzW2I0O16gvb6Uzp0PRw==",
"engines": {
"node": ">=18"
},
"peerDependencies": {
"@prisma/client": ">=4.16.1"
}
},
"node_modules/@prisma/fetch-engine": { "node_modules/@prisma/fetch-engine": {
"version": "6.6.0", "version": "6.6.0",
"resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.6.0.tgz", "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.6.0.tgz",
@@ -2301,6 +2319,13 @@
"@types/unist": "*" "@types/unist": "*"
} }
}, },
"node_modules/@types/mime-types": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/@types/mime-types/-/mime-types-2.1.4.tgz",
"integrity": "sha512-lfU4b34HOri+kAY5UheuFMWPDOI+OPceBSHZKp69gEyTL/mmJ4cnU6Y/rlme3UL3GyOn6Y42hyIEw0/q8sWx5w==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/ms": { "node_modules/@types/ms": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
@@ -3508,6 +3533,13 @@
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/cuid": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/cuid/-/cuid-3.0.0.tgz",
"integrity": "sha512-WZYYkHdIDnaxdeP8Misq3Lah5vFjJwGuItJuV+tvMafosMzw0nF297T7mrm8IOWiPJkV6gc7sa8pzx27+w25Zg==",
"deprecated": "Cuid and other k-sortable and non-cryptographic ids (Ulid, ObjectId, KSUID, all UUIDs) are all insecure. Use @paralleldrive/cuid2 instead.",
"license": "MIT"
},
"node_modules/cytoscape": { "node_modules/cytoscape": {
"version": "3.32.0", "version": "3.32.0",
"resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.32.0.tgz", "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.32.0.tgz",
@@ -4883,6 +4915,19 @@
"url": "https://opencollective.com/eslint" "url": "https://opencollective.com/eslint"
} }
}, },
"node_modules/esprima": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
"license": "BSD-2-Clause",
"bin": {
"esparse": "bin/esparse.js",
"esvalidate": "bin/esvalidate.js"
},
"engines": {
"node": ">=4"
}
},
"node_modules/esquery": { "node_modules/esquery": {
"version": "1.6.0", "version": "1.6.0",
"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz",
@@ -4957,6 +5002,18 @@
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/extend-shallow": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
"integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==",
"license": "MIT",
"dependencies": {
"is-extendable": "^0.1.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/fast-deep-equal": { "node_modules/fast-deep-equal": {
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@@ -5247,6 +5304,18 @@
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
} }
}, },
"node_modules/github-markdown-css": {
"version": "5.8.1",
"resolved": "https://registry.npmjs.org/github-markdown-css/-/github-markdown-css-5.8.1.tgz",
"integrity": "sha512-8G+PFvqigBQSWLQjyzgpa2ThD9bo7+kDsriUIidGcRhXgmcaAWUIpCZf8DavJgc+xifjbCG+GvMyWr0XMXmc7g==",
"license": "MIT",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/glob-parent": { "node_modules/glob-parent": {
"version": "6.0.2", "version": "6.0.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
@@ -5325,6 +5394,43 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/gray-matter": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz",
"integrity": "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==",
"license": "MIT",
"dependencies": {
"js-yaml": "^3.13.1",
"kind-of": "^6.0.2",
"section-matter": "^1.0.0",
"strip-bom-string": "^1.0.0"
},
"engines": {
"node": ">=6.0"
}
},
"node_modules/gray-matter/node_modules/argparse": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
"license": "MIT",
"dependencies": {
"sprintf-js": "~1.0.2"
}
},
"node_modules/gray-matter/node_modules/js-yaml": {
"version": "3.14.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
"integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
"license": "MIT",
"dependencies": {
"argparse": "^1.0.7",
"esprima": "^4.0.0"
},
"bin": {
"js-yaml": "bin/js-yaml.js"
}
},
"node_modules/hachure-fill": { "node_modules/hachure-fill": {
"version": "0.5.2", "version": "0.5.2",
"resolved": "https://registry.npmjs.org/hachure-fill/-/hachure-fill-0.5.2.tgz", "resolved": "https://registry.npmjs.org/hachure-fill/-/hachure-fill-0.5.2.tgz",
@@ -5479,6 +5585,44 @@
"url": "https://opencollective.com/unified" "url": "https://opencollective.com/unified"
} }
}, },
"node_modules/hast-util-sanitize": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/hast-util-sanitize/-/hast-util-sanitize-5.0.2.tgz",
"integrity": "sha512-3yTWghByc50aGS7JlGhk61SPenfE/p1oaFeNwkOOyrscaOkMGrcW9+Cy/QAIOBpZxP1yqDIzFMR0+Np0i0+usg==",
"license": "MIT",
"dependencies": {
"@types/hast": "^3.0.0",
"@ungap/structured-clone": "^1.0.0",
"unist-util-position": "^5.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/hast-util-to-html": {
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz",
"integrity": "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==",
"license": "MIT",
"dependencies": {
"@types/hast": "^3.0.0",
"@types/unist": "^3.0.0",
"ccount": "^2.0.0",
"comma-separated-tokens": "^2.0.0",
"hast-util-whitespace": "^3.0.0",
"html-void-elements": "^3.0.0",
"mdast-util-to-hast": "^13.0.0",
"property-information": "^7.0.0",
"space-separated-tokens": "^2.0.0",
"stringify-entities": "^4.0.0",
"zwitch": "^2.0.4"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/hast-util-to-jsx-runtime": { "node_modules/hast-util-to-jsx-runtime": {
"version": "2.3.6", "version": "2.3.6",
"resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz",
@@ -5880,6 +6024,15 @@
"url": "https://github.com/sponsors/wooorm" "url": "https://github.com/sponsors/wooorm"
} }
}, },
"node_modules/is-extendable": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz",
"integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/is-extglob": { "node_modules/is-extglob": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
@@ -6315,6 +6468,15 @@
"resolved": "https://registry.npmjs.org/khroma/-/khroma-2.1.0.tgz", "resolved": "https://registry.npmjs.org/khroma/-/khroma-2.1.0.tgz",
"integrity": "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==" "integrity": "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw=="
}, },
"node_modules/kind-of": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
"integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/kolorist": { "node_modules/kolorist": {
"version": "1.8.0", "version": "1.8.0",
"resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.8.0.tgz", "resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.8.0.tgz",
@@ -8716,6 +8878,22 @@
"url": "https://opencollective.com/unified" "url": "https://opencollective.com/unified"
} }
}, },
"node_modules/remark": {
"version": "15.0.1",
"resolved": "https://registry.npmjs.org/remark/-/remark-15.0.1.tgz",
"integrity": "sha512-Eht5w30ruCXgFmxVUSlNWQ9iiimq07URKeFS3hNc8cUWy1llX4KDWfyEDZRycMc+znsN9Ux5/tJ/BFdgdOwA3A==",
"license": "MIT",
"dependencies": {
"@types/mdast": "^4.0.0",
"remark-parse": "^11.0.0",
"remark-stringify": "^11.0.0",
"unified": "^11.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/remark-gfm": { "node_modules/remark-gfm": {
"version": "4.0.1", "version": "4.0.1",
"resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz", "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz",
@@ -8734,6 +8912,23 @@
"url": "https://opencollective.com/unified" "url": "https://opencollective.com/unified"
} }
}, },
"node_modules/remark-html": {
"version": "16.0.1",
"resolved": "https://registry.npmjs.org/remark-html/-/remark-html-16.0.1.tgz",
"integrity": "sha512-B9JqA5i0qZe0Nsf49q3OXyGvyXuZFDzAP2iOFLEumymuYJITVpiH1IgsTEwTpdptDmZlMDMWeDmSawdaJIGCXQ==",
"license": "MIT",
"dependencies": {
"@types/mdast": "^4.0.0",
"hast-util-sanitize": "^5.0.0",
"hast-util-to-html": "^9.0.0",
"mdast-util-to-hast": "^13.0.0",
"unified": "^11.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/remark-parse": { "node_modules/remark-parse": {
"version": "11.0.0", "version": "11.0.0",
"resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz",
@@ -8974,6 +9169,19 @@
"integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/section-matter": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz",
"integrity": "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==",
"license": "MIT",
"dependencies": {
"extend-shallow": "^2.0.1",
"kind-of": "^6.0.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/semver": { "node_modules/semver": {
"version": "7.7.1", "version": "7.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
@@ -9219,6 +9427,12 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/sprintf-js": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
"integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
"license": "BSD-3-Clause"
},
"node_modules/stable-hash": { "node_modules/stable-hash": {
"version": "0.0.5", "version": "0.0.5",
"resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz", "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz",
@@ -9404,6 +9618,15 @@
"node": ">=4" "node": ">=4"
} }
}, },
"node_modules/strip-bom-string": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz",
"integrity": "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/strip-json-comments": { "node_modules/strip-json-comments": {
"version": "3.1.1", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+7
View File
@@ -23,13 +23,17 @@
"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", "@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", "dompurify": "^3.2.5",
"github-markdown-css": "^5.8.1",
"gray-matter": "^4.0.3",
"mermaid": "^11.6.0", "mermaid": "^11.6.0",
"minio": "^8.0.5", "minio": "^8.0.5",
"next": "^15.2.3", "next": "^15.2.3",
@@ -39,7 +43,9 @@
"react-hot-toast": "^2.5.2", "react-hot-toast": "^2.5.2",
"react-markdown": "^10.1.0", "react-markdown": "^10.1.0",
"rehype-raw": "^7.0.0", "rehype-raw": "^7.0.0",
"remark": "^15.0.1",
"remark-gfm": "^4.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"
@@ -48,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",
+3 -2
View File
@@ -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
@@ -23,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?
+1
View File
@@ -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

+1
View File
@@ -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

+70
View File
@@ -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;
+57 -39
View File
@@ -1,13 +1,14 @@
"use client"; "use client";
import { useRef, useState } from "react"; import { useRef, useState } from "react";
import { useFileActions } from "~/app/_components/FileActions"; import { useFileActions } from "~/app/_components/FileActions";
import toast from "react-hot-toast";
export function FileActionsContainer({ export function FileActionsContainer({
fileId, fileId,
fileName, fileName,
fileUrl, fileUrl,
isOwner, isOwner,
isPublic, isPublic: initialIsPublic, // Rename to avoid conflict with local state
}: { }: {
fileId: string; fileId: string;
fileName: string; fileName: string;
@@ -15,77 +16,91 @@ export function FileActionsContainer({
isOwner: boolean; isOwner: boolean;
isPublic: boolean; isPublic: boolean;
}) { }) {
const { handleDownload, handleCopyUrl, handleRemove} = useFileActions(() => fileId, (description: string) => { const [isPublic, setIsPublic] = useState(initialIsPublic); // Local state for toggle
const { handleDownload, handleCopyUrl, handleRemove } = useFileActions(
() => fileId,
(description: string) => {
if (isOwner) { if (isOwner) {
console.log(description); 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"
> >
<img src="/icons/download.svg" alt="Download" className="w-6 h-6" /> <img src="/icons/download.svg" alt="Download" className="h-6 w-6" />
</button> </button>
{/* Copy URL Button */} {/* Copy URL Button */}
<button <button
onClick={() => handleCopyUrl(fileUrl)} onClick={() => {
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" className="flex items-center justify-center rounded-full bg-green-500 p-2 hover:bg-green-600"
> >
<img src="/icons/copy.svg" alt="Copy URL" className="w-6 h-6" /> <img src="/icons/copy.svg" alt="Copy URL" className="h-6 w-6" />
</button> </button>
{/* Remove Button */} {/* Remove Button */}
{isOwner && ( {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"
> >
<img src="/icons/delete.svg" alt="Remove" className="w-6 h-6" /> <img src="/icons/delete.svg" alt="Remove" className="h-6 w-6" />
</button> </button>
)} )}
{/* Public/Private Toggle */}
{isOwner && ( {isOwner && (
<div className="mt-4 flex items-center gap-2"> <button
<label className="text-sm"> onClick={async () => {
Public: const newIsPublic = !isPublic;
</label>
<label
htmlFor={`public-toggle-${fileId}`}
className="relative inline-flex items-center cursor-pointer"
>
<input
id={`public-toggle-${fileId}`}
type="checkbox"
checked={isPublic} // Ensure this reflects the prop
onChange={async (e) => {
const newIsPublic = e.target.checked;
try { try {
const response = await fetch(`/api/files/update-public`, { const response = await fetch(`/api/files/update-public`, {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
body: JSON.stringify({ fileId: fileId, isPublic: newIsPublic }), body: JSON.stringify({
fileId: fileId,
isPublic: newIsPublic,
}),
}); });
if (!response.ok) { if (!response.ok) {
throw new Error("Failed to update public status"); throw new Error("Failed to update public status");
} }
console.log("Public status updated successfully"); setIsPublic(newIsPublic); // Update local state
toast.success(
`File is now ${newIsPublic ? "Public" : "Private"}!`
);
} catch (err) { } catch (err) {
console.error("Error updating public status:", err); toast.error("Error updating public status.");
console.error(err);
} }
}} }}
className="sr-only peer" 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'}`}
/> />
{/* Toggle Background */} </button>
<div className="w-10 h-5 bg-gray-300 rounded-full peer-checked:bg-green-500 peer-focus:ring-2 peer-focus:ring-green-300 transition-colors"></div>
{/* Toggle Handle */}
<div className="absolute top-0.5 left-0.5 w-4 h-4 bg-white rounded-full shadow-md peer-checked:translate-x-5 transition-transform"></div>
</label>
</div>
)} )}
</div> </div>
); );
@@ -98,12 +113,15 @@ export function FileDescriptionContainer({
fileId: string; fileId: string;
fileDescription?: string; fileDescription?: string;
}) { }) {
const [description, setDescription] = useState(fileDescription || ""); // Add state for description const [description, setDescription] = useState(fileDescription || ""); // Add state for description
const { handleDescriptionChange } = useFileActions(() => {}, (description: string) => { const { handleDescriptionChange } = useFileActions(
() => {},
(description: string) => {
setDescription(description); setDescription(description);
return undefined; return undefined;
}, fileId); // Wrap setDescription in a function },
fileId,
); // Wrap setDescription in a function
const debounceTimer = useRef<NodeJS.Timeout | null>(null); // Initialize debounce timer const debounceTimer = useRef<NodeJS.Timeout | null>(null); // Initialize debounce timer
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => { const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
@@ -111,9 +129,9 @@ export function FileDescriptionContainer({
}; };
return ( return (
<div className="flex self-center gap-2"> <div className="flex gap-2 self-center">
<textarea <textarea
className="w-full h-24 p-2 border rounded-md bg-gray-800 text-white" className="h-24 w-full rounded-md border bg-gray-800 p-2 text-white"
value={description} // Use state value value={description} // Use state value
onChange={handleChange} onChange={handleChange}
placeholder="Enter file description..." placeholder="Enter file description..."
+11 -10
View File
@@ -1,7 +1,6 @@
"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";
@@ -15,7 +14,7 @@ interface FileDetails {
description: string; description: string;
extension: string; extension: string;
isOwner: boolean; // Indicates if the user owns the file isOwner: boolean; // Indicates if the user owns the file
isPublic: boolean; // Indicates if the file is public public: boolean; // Indicates if the file is public
} }
interface FileGridProps { interface FileGridProps {
@@ -72,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)));
} }
}; };
@@ -104,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"
> >
{<div className=" self-center max-w-50"><FilePreview fileId={file.id} fileType={file.extension} /></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>
@@ -118,7 +119,7 @@ export default function FileGrid({ session }: FileGridProps) {
fileName={file.name} fileName={file.name}
fileUrl={file.url} fileUrl={file.url}
isOwner={true} isOwner={true}
isPublic={file.isPublic} isPublic={file.public}
/> />
</div> </div>
</div> </div>
+82 -12
View File
@@ -1,16 +1,22 @@
"use client"; "use client";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { getFileType } from "~/utils/fileType"; // Adjust the import path as necessary 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); console.log("File Type:", fileType);
@@ -47,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>
@@ -71,14 +110,14 @@ 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")) { 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")) { if (fileType.startsWith("text")) {
@@ -91,17 +130,48 @@ export function FilePreview({ fileId, fileType }: FilePreviewProps) {
<img src="/icons/files/archive.svg" alt="Archive file preview" className="max-w-full max-h-96 rounded-lg invert" /> <img src="/icons/files/archive.svg" alt="Archive file preview" className="max-w-full max-h-96 rounded-lg invert" />
); );
} }
if (fileType.startsWith("code") || fileType.startsWith("markdown")) { if (fileType.startsWith("code")) {
return ( return (
<img src="/icons/files/code.svg" alt="Code file preview" className="max-w-full max-h-96 rounded-lg invert" /> <img src="/icons/files/code.svg" alt="Code file preview" className="max-w-full max-h-96 rounded-lg invert" />
); );
} }
// if (fileType.startsWith("markdown")) {
// return;
// }
// log file type
console.log("Unsupported file type:", fileType); console.log("Unsupported file type:", fileType);
return; 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,
},
};
} }
+45
View File
@@ -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`,
},
],
},
};
}
+74 -54
View File
@@ -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}
onClick={() => fileInputRef.current?.click()}
style={{ cursor: "pointer" }}
> >
{file ? ( {/* Hidden file input for click-to-select */}
<>
File Selected
{/* SVG Icon */}
<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>
); );
} }
+28 -7
View File
@@ -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);
@@ -15,17 +15,38 @@ export async function GET(req: Request) {
} }
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 });
} }
} }
+2 -1
View File
@@ -25,10 +25,11 @@ 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,
+19 -5
View File
@@ -1,10 +1,12 @@
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
import Busboy from "busboy"; import Busboy from "busboy";
import { Readable } from "stream"; import { Readable } from "stream";
import crypto from "crypto";
import { db } from "~/server/db"; import { db } from "~/server/db";
import { auth } from "~/server/auth"; import { auth } from "~/server/auth";
import { minioClient, ensureBucketExists } from "~/utils/minioClient"; import { minioClient, ensureBucketExists } from "~/utils/minioClient";
import { getFileType } from "~/utils/fileType";
import cuid from 'cuid';
import { notifyClients } from "~/utils/notifyClients";
export const config = { export const config = {
api: { api: {
@@ -22,7 +24,9 @@ export async function POST(req: Request) {
await ensureBucketExists(bucketName); 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);
@@ -38,8 +42,11 @@ export async function POST(req: Request) {
fileBuffer = Buffer.concat(chunks); fileBuffer = Buffer.concat(chunks);
// Generate a unique ID for the file // Generate a unique ID for the file
const fileId = crypto.randomUUID(); const fileId = session.user.id + "-" + cuid()
const objectName = `${fileId}-${fileName}`; const objectName = `${fileId}-${fileName}`;
// Change UUID to CUID
try { try {
// Upload the file to MinIO // Upload the file to MinIO
@@ -52,12 +59,19 @@ export async function POST(req: Request) {
url: `/share?id=${fileId}`, url: `/share?id=${fileId}`,
name: fileName, name: fileName,
size: fileBuffer.length, size: fileBuffer.length,
extension: info.mimeType, extension: getFileType(fileName),
uploadedById: session.user.id, uploadedById: session.user.id,
}, },
}); });
notifyClients({ type: "file-added", fileId: fileId });
resolve(NextResponse.json({ message: "File uploaded successfully", file: newFile })); resolve(
NextResponse.json({
message: "File uploaded successfully",
file: newFile,
fileId: fileId,
}),
);
} catch (error) { } catch (error) {
console.error("Error uploading file to MinIO:", error); console.error("Error uploading file to MinIO:", error);
reject(new Error("Failed to upload file")); reject(new Error("Failed to upload file"));
+1
View File
@@ -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('.') }
+1
View File
@@ -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('.') }
+1
View File
@@ -0,0 +1 @@
export * from "./default"
File diff suppressed because one or more lines are too long
@@ -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)
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
@@ -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
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -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 { }
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -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
}
+1
View File
@@ -0,0 +1 @@
export * from "./index"
+225
View File
@@ -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)
+6
View File
@@ -0,0 +1,6 @@
import LoadingSkeleton from './LoadingSkeleton';
export default function Loading() {
// You can add any UI inside Loading, including a Skeleton.
return <LoadingSkeleton />
}
+78 -9
View File
@@ -1,15 +1,53 @@
"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 */}
@@ -53,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 />}>
<FileGrid session={session as { user: { id: string } }} />
</Suspense>
<Suspense fallback={<UploadFormFallback />}>
<UploadForm /> <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">
{!loading ? (
<Link <Link
href={session ? "/api/auth/signout" : "/api/auth/signin"} href={session ? "/api/auth/signout" : "/api/auth/signin"}
className="rounded-full bg-white/10 px-10 py-3 font-semibold no-underline transition hover:bg-white/20" className="rounded-full bg-white/10 px-10 py-3 font-semibold no-underline transition hover:bg-white/20"
> >
{session ? "Sign out" : "Sign in"} {session ? "Sign out" : "Sign in"}
</Link> </Link>
) : (
<div 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;
+3 -2
View File
@@ -10,6 +10,7 @@ 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;
@@ -90,7 +91,7 @@ export default function SearchFile() {
className="flex place-content-end w-xxs flex-col gap-4 rounded-xl bg-white/10 p-4 hover:bg-white/20" 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"> <div className="self-center max-w-100 sm:max-w-50">
<FilePreview fileId={file.id} fileType={file.extension} /> <FilePreview fileId={file.id} fileType={file.extension} share={false} />
</div> </div>
<button onClick={() => router.push(pageUrl + file.url)}> <button onClick={() => router.push(pageUrl + file.url)}>
@@ -107,7 +108,7 @@ export default function SearchFile() {
fileId={file.id} fileId={file.id}
fileName={file.name} fileName={file.name}
fileUrl={file.url} fileUrl={file.url}
isOwner={false} // Check if the user is the owner isOwner={false}
isPublic={file.isPublic} // Check if the file is public isPublic={file.isPublic} // Check if the file is public
/> />
</div> </div>
+70
View File
@@ -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;
+6
View File
@@ -0,0 +1,6 @@
import LoadingSkeleton from './LoadingSkeleton';
export default function Loading() {
// You can add any UI inside Loading, including a Skeleton.
return <LoadingSkeleton />
}
+73 -48
View File
@@ -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, FileDescriptionContainer } 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;
@@ -19,13 +26,54 @@ interface FileDetails {
isPublic: boolean; 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) {
@@ -52,7 +100,7 @@ export default async function FilePreviewContainer({
} }
const fileDetails = await fetchFileDetails(fileId); const fileDetails = await fetchFileDetails(fileId);
const session = await auth();
if (!fileDetails) { if (!fileDetails) {
return ( return (
@@ -71,55 +119,23 @@ export default async function FilePreviewContainer({
} }
return ( return (
<>
<Head>
<title>{fileDetails.name} - File Details</title>
<meta
property="og:title"
content={`${fileDetails.name} - File Details`}
/>
<meta
property="og:description"
content={`Size: ${
fileDetails.size > 1024 * 1024 * 1024
? (fileDetails.size / (1024 * 1024 * 1024)).toFixed(2) + " GB"
: fileDetails.size > 1024 * 1024
? (fileDetails.size / (1024 * 1024)).toFixed(2) + " MB"
: fileDetails.size > 1024
? (fileDetails.size / 1024).toFixed(2) + " KB"
: fileDetails.size + " Bytes"
}, 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"> <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"> <div className="absolute top-4 left-4">
<HomeButton /> {/* Use the client component */} <HomeButton />
</div> </div>
<Toaster position="top-right" reverseOrder={false} />
<div className="container flex flex-col items-center gap-12 px-4 py-16"> <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]"> <h1 className="text-5xl font-extrabold tracking-tight sm:text-[5rem]">
<span className="text-[hsl(280,100%,70%)]">File</span> Details <span className="text-[hsl(280,100%,70%)]">File</span> Details
</h1> </h1>
<div className="mt-6"> <div className="mt-6">
{fileDetails.type !== "unknown" && ( {fileDetails.type !== "unknown" && (
<FilePreview <Suspense fallback={<div className="text-white">Loading...</div>}>
fileId={fileDetails.id} <FilePreview fileId={fileDetails.id} fileType={fileDetails.type} share={true} />
fileType={fileDetails.type} </Suspense>
/>
)} )}
</div> </div>
<div className="bg-white/10 shadow-md rounded-lg p-6 w-full max-w-md text-white"> <div className="w-full max-w-md rounded-lg bg-white/10 p-6 text-white shadow-md">
<p> <p>
<strong>Name:</strong> {fileDetails.name} <strong>Name:</strong> {fileDetails.name}
</p> </p>
@@ -135,34 +151,43 @@ export default async function FilePreviewContainer({
</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}
</Suspense>
</p> </p>
<p> <p>
<strong>Upload Date:</strong>{" "} <strong>Upload Date:</strong>{" "}
<Suspense fallback={<div className="text-white">Loading...</div>}>
{new Date(fileDetails.uploadDate).toLocaleString()} {new Date(fileDetails.uploadDate).toLocaleString()}
</Suspense>
</p> </p>
<div> <div>
<strong>Description:</strong>{" "} <strong>Description:</strong>{" "}
<FileDescriptionContainer fileId={fileDetails.id} fileDescription={fileDetails.description}/> <Suspense fallback={<div className="text-white">Loading...</div>}>
<FileDescriptionContainer
fileId={fileDetails.id}
fileDescription={fileDetails.description}
/>
</Suspense>
</div> </div>
<div className="mt-4 flex justify-center"> <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} isPublic={fileDetails.isPublic}
/> />
</Suspense>
</div> </div>
</div> </div>
</div> </div>
</main> </main>
</>
); );
} }
+9
View File
@@ -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 */
}
+78
View File
@@ -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>
);
}
View File
+7 -5
View File
@@ -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;
+3 -2
View File
@@ -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;
+12
View File
@@ -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;
}
+18 -2
View File
@@ -1,32 +1,48 @@
// This function takes a file name as input and returns the file type based on its extension. // 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 { export function getFileType(fileName: string): string {
const extension = fileName.split(".").pop()?.toLowerCase(); const extension = fileName.split(".").pop()?.toLowerCase();
const fileTypes: Record<string, string> = { const fileTypes: Record<string, string> = {
// Video
"mp4": "video/mp4", "mp4": "video/mp4",
"webm": "video/webm", "webm": "video/webm",
"ogg": "video/ogg", "ogg": "video/ogg",
// Image
"jpg": "image/jpeg", "jpg": "image/jpeg",
"jpeg": "image/jpeg", "jpeg": "image/jpeg",
"png": "image/png", "png": "image/png",
"gif": "image/gif", "gif": "image/gif",
"svg": "image/svg+xml", "svg": "image/svg+xml",
// Audio
"mp3": "audio/mpeg", "mp3": "audio/mpeg",
"wav": "audio/wav", "wav": "audio/wav",
// Archive
"zip": "archive/zip", "zip": "archive/zip",
"rar": "archive/rar", "rar": "archive/rar",
"jar": "archive/jar",
"iso": "archive/iso",
// Text
"pdf": "text/pdf", "pdf": "text/pdf",
"txt": "text/plain", "txt": "text/plain",
// Code
"c": "code/c", "c": "code/c",
"cpp": "code/cpp", "cpp": "code/cpp",
"py": "code/python", "py": "code/python",
"js": "code/javascript", "js": "code/javascript",
"html": "code/html", "html": "code/html",
"css": "code/css", "css": "code/css",
"md": "markdown/markdown",
"json": "code/json", "json": "code/json",
"xml": "code/xml", "xml": "code/xml",
"csv": "code/csv", "csv": "code/csv",
// Markdown
"md": "markdown/markdown",
// Applications
"exe": "application/executable",
"apk": "application/android",
}; };
return extension ? fileTypes[extension] || "unknown" : "unknown"; return extension ? fileTypes[extension] ||
//get the file type using the mime type library
mime.lookup(extension) || "application/octet-stream" : "application/octet-stream";
}; };
+2 -2
View File
@@ -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"]