Compare commits
10 Commits
5ec3e011e1
...
99397e774c
| Author | SHA1 | Date | |
|---|---|---|---|
| 99397e774c | |||
| 792b0eb275 | |||
| 8798764b89 | |||
| f2d2268039 | |||
| ad24c76a70 | |||
| af978c98b8 | |||
| 75d1187255 | |||
| 1d302eb217 | |||
| 344334592e | |||
| 8bc35d979d |
3388
package-lock.json
generated
@ -24,15 +24,22 @@
|
||||
"@auth/prisma-adapter": "^2.7.2",
|
||||
"@prisma/client": "^6.5.0",
|
||||
"@t3-oss/env-nextjs": "^0.12.0",
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"@tanstack/react-query": "^5.69.0",
|
||||
"@trpc/client": "^11.0.0",
|
||||
"@trpc/react-query": "^11.0.0",
|
||||
"@trpc/server": "^11.0.0",
|
||||
"dompurify": "^3.2.5",
|
||||
"mermaid": "^11.6.0",
|
||||
"minio": "^8.0.5",
|
||||
"next": "^15.2.3",
|
||||
"next-auth": "5.0.0-beta.25",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-hot-toast": "^2.5.2",
|
||||
"react-markdown": "^10.1.0",
|
||||
"rehype-raw": "^7.0.0",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"server-only": "^0.0.1",
|
||||
"superjson": "^2.2.1",
|
||||
"zod": "^3.24.2"
|
||||
|
||||
@ -15,17 +15,6 @@ datasource db {
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
model Post {
|
||||
id Int @id @default(autoincrement())
|
||||
name String
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
createdBy User @relation(fields: [createdById], references: [id])
|
||||
createdById String
|
||||
|
||||
@@index([name])
|
||||
}
|
||||
|
||||
// Necessary for Next auth
|
||||
model Account {
|
||||
@ -63,7 +52,6 @@ model User {
|
||||
image String?
|
||||
accounts Account[]
|
||||
sessions Session[]
|
||||
posts Post[]
|
||||
files File[] // Relation to the File model
|
||||
}
|
||||
|
||||
@ -85,4 +73,5 @@ model File {
|
||||
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
|
||||
}
|
||||
|
||||
BIN
public/favicon-old.ico
Normal file
|
After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 5.2 KiB |
4
public/icons/copy.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8 5.00005C7.01165 5.00082 6.49359 5.01338 6.09202 5.21799C5.71569 5.40973 5.40973 5.71569 5.21799 6.09202C5 6.51984 5 7.07989 5 8.2V17.8C5 18.9201 5 19.4802 5.21799 19.908C5.40973 20.2843 5.71569 20.5903 6.09202 20.782C6.51984 21 7.07989 21 8.2 21H15.8C16.9201 21 17.4802 21 17.908 20.782C18.2843 20.5903 18.5903 20.2843 18.782 19.908C19 19.4802 19 18.9201 19 17.8V8.2C19 7.07989 19 6.51984 18.782 6.09202C18.5903 5.71569 18.2843 5.40973 17.908 5.21799C17.5064 5.01338 16.9884 5.00082 16 5.00005M8 5.00005V7H16V5.00005M8 5.00005V4.70711C8 4.25435 8.17986 3.82014 8.5 3.5C8.82014 3.17986 9.25435 3 9.70711 3H14.2929C14.7456 3 15.1799 3.17986 15.5 3.5C15.8201 3.82014 16 4.25435 16 4.70711V5.00005M15 12H12M15 16H12M9 12H9.01M9 16H9.01" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
4
public/icons/delete.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M18 6L17.1991 18.0129C17.129 19.065 17.0939 19.5911 16.8667 19.99C16.6666 20.3412 16.3648 20.6235 16.0011 20.7998C15.588 21 15.0607 21 14.0062 21H9.99377C8.93927 21 8.41202 21 7.99889 20.7998C7.63517 20.6235 7.33339 20.3412 7.13332 19.99C6.90607 19.5911 6.871 19.065 6.80086 18.0129L6 6M4 6H20M16 6L15.7294 5.18807C15.4671 4.40125 15.3359 4.00784 15.0927 3.71698C14.8779 3.46013 14.6021 3.26132 14.2905 3.13878C13.9376 3 13.523 3 12.6936 3H11.3064C10.477 3 10.0624 3 9.70951 3.13878C9.39792 3.26132 9.12208 3.46013 8.90729 3.71698C8.66405 4.00784 8.53292 4.40125 8.27064 5.18807L8 6M14 10V17M10 10V17" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 921 B |
4
public/icons/download.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M17 17H17.01M17.4 14H18C18.9319 14 19.3978 14 19.7654 14.1522C20.2554 14.3552 20.6448 14.7446 20.8478 15.2346C21 15.6022 21 16.0681 21 17C21 17.9319 21 18.3978 20.8478 18.7654C20.6448 19.2554 20.2554 19.6448 19.7654 19.8478C19.3978 20 18.9319 20 18 20H6C5.06812 20 4.60218 20 4.23463 19.8478C3.74458 19.6448 3.35523 19.2554 3.15224 18.7654C3 18.3978 3 17.9319 3 17C3 16.0681 3 15.6022 3.15224 15.2346C3.35523 14.7446 3.74458 14.3552 4.23463 14.1522C4.60218 14 5.06812 14 6 14H6.6M12 15V4M12 15L9 12M12 15L15 12" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 831 B |
4
public/icons/files/archive.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9 14H15M4.6 10H19.4C19.9601 10 20.2401 10 20.454 9.89101C20.6422 9.79513 20.7951 9.64215 20.891 9.45399C21 9.24008 21 8.96005 21 8.4V5.6C21 5.03995 21 4.75992 20.891 4.54601C20.7951 4.35785 20.6422 4.20487 20.454 4.10899C20.2401 4 19.9601 4 19.4 4H4.6C4.03995 4 3.75992 4 3.54601 4.10899C3.35785 4.20487 3.20487 4.35785 3.10899 4.54601C3 4.75992 3 5.03995 3 5.6V8.4C3 8.96005 3 9.24008 3.10899 9.45399C3.20487 9.64215 3.35785 9.79513 3.54601 9.89101C3.75992 10 4.03995 10 4.6 10ZM5 10H19V16.8C19 17.9201 19 18.4802 18.782 18.908C18.5903 19.2843 18.2843 19.5903 17.908 19.782C17.4802 20 16.9201 20 15.8 20H8.2C7.07989 20 6.51984 20 6.09202 19.782C5.71569 19.5903 5.40973 19.2843 5.21799 18.908C5 18.4802 5 17.9201 5 16.8V10Z" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
4
public/icons/files/code.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7 8L3 11.6923L7 16M17 8L21 11.6923L17 16M14 4L10 20" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 373 B |
4
public/icons/files/text-file.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9 17H15M9 13H15M9 9H10M13 3H8.2C7.0799 3 6.51984 3 6.09202 3.21799C5.71569 3.40973 5.40973 3.71569 5.21799 4.09202C5 4.51984 5 5.0799 5 6.2V17.8C5 18.9201 5 19.4802 5.21799 19.908C5.40973 20.2843 5.71569 20.5903 6.09202 20.782C6.51984 21 7.0799 21 8.2 21H15.8C16.9201 21 17.4802 21 17.908 20.782C18.2843 20.5903 18.5903 20.2843 18.782 19.908C19 19.4802 19 18.9201 19 17.8V9M13 3L19 9M13 3V7.4C13 7.96005 13 8.24008 13.109 8.45399C13.2049 8.64215 13.3578 8.79513 13.546 8.89101C13.7599 9 14.0399 9 14.6 9H19" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 828 B |
4
public/icons/home.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14 21.0001V15.0001H10V21.0001M19 9.77818V16.2001C19 17.8802 19 18.7203 18.673 19.362C18.3854 19.9265 17.9265 20.3855 17.362 20.6731C16.7202 21.0001 15.8802 21.0001 14.2 21.0001H9.8C8.11984 21.0001 7.27976 21.0001 6.63803 20.6731C6.07354 20.3855 5.6146 19.9265 5.32698 19.362C5 18.7203 5 17.8802 5 16.2001V9.77753M21 12.0001L15.5668 5.96405C14.3311 4.59129 13.7133 3.9049 12.9856 3.65151C12.3466 3.42894 11.651 3.42899 11.0119 3.65165C10.2843 3.90516 9.66661 4.59163 8.43114 5.96458L3 12.0001" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 813 B |
@ -1,18 +1,21 @@
|
||||
'use client';
|
||||
import {useFileActions} from "~/app/_components/FileActions";
|
||||
"use client";
|
||||
import { useRef, useState } from "react";
|
||||
import { useFileActions } from "~/app/_components/FileActions";
|
||||
|
||||
export function FileActionsContainer({
|
||||
fileId,
|
||||
fileName,
|
||||
fileUrl,
|
||||
isOwner,
|
||||
isPublic,
|
||||
}: {
|
||||
fileId: string;
|
||||
fileName: string;
|
||||
fileUrl: string;
|
||||
isOwner: boolean;
|
||||
isPublic: boolean;
|
||||
}) {
|
||||
const { handleDownload, handleCopyUrl, handleRemove } = useFileActions(() => fileId, (description: string) => {
|
||||
const { handleDownload, handleCopyUrl, handleRemove} = useFileActions(() => fileId, (description: string) => {
|
||||
if (isOwner) {
|
||||
console.log(description);
|
||||
}
|
||||
@ -25,20 +28,7 @@ export function FileActionsContainer({
|
||||
onClick={() => handleDownload(fileId, fileName)}
|
||||
className="flex items-center justify-center rounded-full bg-blue-500 p-2 hover:bg-blue-600"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={2}
|
||||
stroke="currentColor"
|
||||
className="h-5 w-5 text-white"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M4 16v2a2 2 0 002 2h12a2 2 0 002-2v-2M7 10l5 5m0 0l5-5m-5 5V3"
|
||||
/>
|
||||
</svg>
|
||||
<img src="/icons/download.svg" alt="Download" className="w-6 h-6" />
|
||||
</button>
|
||||
|
||||
{/* Copy URL Button */}
|
||||
@ -46,42 +36,89 @@ export function FileActionsContainer({
|
||||
onClick={() => handleCopyUrl(fileUrl)}
|
||||
className="flex items-center justify-center rounded-full bg-green-500 p-2 hover:bg-green-600"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={2}
|
||||
stroke="currentColor"
|
||||
className="h-5 w-5 text-white"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M8 9h8m-6 4h4m-7 8h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v12a2 2 0 002 2z"
|
||||
/>
|
||||
</svg>
|
||||
<img src="/icons/copy.svg" alt="Copy URL" className="w-6 h-6" />
|
||||
</button>
|
||||
|
||||
{/* Remove Button */}
|
||||
{isOwner && (
|
||||
<button
|
||||
onClick={() => handleRemove(fileId)}
|
||||
className="flex items-center justify-center rounded-full bg-red-500 p-2 hover:bg-red-600"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={2}
|
||||
stroke="currentColor"
|
||||
className="h-5 w-5 text-white"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
<img src="/icons/delete.svg" alt="Remove" className="w-6 h-6" />
|
||||
</button>
|
||||
)}
|
||||
{isOwner && (
|
||||
<div className="mt-4 flex items-center gap-2">
|
||||
<label className="text-sm">
|
||||
Public:
|
||||
</label>
|
||||
<label
|
||||
htmlFor={`public-toggle-${fileId}`}
|
||||
className="relative inline-flex items-center cursor-pointer"
|
||||
>
|
||||
<input
|
||||
id={`public-toggle-${fileId}`}
|
||||
type="checkbox"
|
||||
checked={isPublic} // Ensure this reflects the prop
|
||||
onChange={async (e) => {
|
||||
const newIsPublic = e.target.checked;
|
||||
try {
|
||||
const response = await fetch(`/api/files/update-public`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ fileId: fileId, isPublic: newIsPublic }),
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to update public status");
|
||||
}
|
||||
console.log("Public status updated successfully");
|
||||
} catch (err) {
|
||||
console.error("Error updating public status:", err);
|
||||
}
|
||||
}}
|
||||
className="sr-only peer"
|
||||
/>
|
||||
{/* Toggle Background */}
|
||||
<div className="w-10 h-5 bg-gray-300 rounded-full peer-checked:bg-green-500 peer-focus:ring-2 peer-focus:ring-green-300 transition-colors"></div>
|
||||
{/* Toggle Handle */}
|
||||
<div className="absolute top-0.5 left-0.5 w-4 h-4 bg-white rounded-full shadow-md peer-checked:translate-x-5 transition-transform"></div>
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function FileDescriptionContainer({
|
||||
fileId,
|
||||
fileDescription,
|
||||
}: {
|
||||
fileId: string;
|
||||
fileDescription?: string;
|
||||
}) {
|
||||
|
||||
const [description, setDescription] = useState(fileDescription || ""); // Add state for description
|
||||
const { handleDescriptionChange } = useFileActions(() => {}, (description: string) => {
|
||||
setDescription(description);
|
||||
return undefined;
|
||||
}, fileId); // Wrap setDescription in a function
|
||||
const debounceTimer = useRef<NodeJS.Timeout | null>(null); // Initialize debounce timer
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
handleDescriptionChange(e, debounceTimer); // Pass the debounce timer
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex self-center gap-2">
|
||||
<textarea
|
||||
className="w-full h-24 p-2 border rounded-md bg-gray-800 text-white"
|
||||
value={description} // Use state value
|
||||
onChange={handleChange}
|
||||
placeholder="Enter file description..."
|
||||
maxLength={200} // Limit to 200 characters
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -4,7 +4,7 @@ import { notifyClients } from "~/utils/notifyClients";
|
||||
|
||||
export const useFileActions = (
|
||||
setFiles: (callback: (prevFiles: any[]) => any[]) => void,
|
||||
setDescription?: (description: string) => void,
|
||||
setDescription?: (description: string) => undefined,
|
||||
fileId?: string
|
||||
) => {
|
||||
const pageUrl = `${env.NEXT_PUBLIC_PAGE_URL}`;
|
||||
@ -75,7 +75,9 @@ export const useFileActions = (
|
||||
e: React.ChangeEvent<HTMLTextAreaElement>,
|
||||
debounceTimer: React.RefObject<NodeJS.Timeout | null>
|
||||
) => {
|
||||
if (!setDescription) return;
|
||||
if (setDescription === undefined) {console.error("setDescription function is not provided")
|
||||
return;
|
||||
};
|
||||
|
||||
const newDescription = e.target.value;
|
||||
setDescription(newDescription);
|
||||
@ -83,7 +85,9 @@ export const useFileActions = (
|
||||
if (debounceTimer.current) {
|
||||
clearTimeout(debounceTimer.current);
|
||||
}
|
||||
|
||||
debounceTimer.current = setTimeout(() => {
|
||||
console.log("Calling handleDescriptionSave"); // Debug log
|
||||
handleDescriptionSave(newDescription);
|
||||
}, 1000);
|
||||
};
|
||||
@ -99,7 +103,8 @@ export const useFileActions = (
|
||||
const response = await fetch(`/api/files/share?id=${encodeURIComponent(fileId)}`, {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ description }),
|
||||
// pass the fileId and description in the request body
|
||||
body: JSON.stringify({ description, id: fileId }),
|
||||
});
|
||||
|
||||
if (response.status === 403) {
|
||||
|
||||
@ -6,6 +6,7 @@ import { useRouter } from "next/navigation";
|
||||
import { env } from "~/env.js";
|
||||
import { FilePreview } from "~/app/_components/FilePreview";
|
||||
import { useFileActions } from "~/app/_components/FileActions";
|
||||
import { FileActionsContainer } from "./ActionButtons";
|
||||
|
||||
interface FileDetails {
|
||||
id: string;
|
||||
@ -13,6 +14,8 @@ interface FileDetails {
|
||||
url: string;
|
||||
description: string;
|
||||
extension: string;
|
||||
isOwner: boolean; // Indicates if the user owns the file
|
||||
isPublic: boolean; // Indicates if the file is public
|
||||
}
|
||||
|
||||
interface FileGridProps {
|
||||
@ -101,7 +104,7 @@ export default function FileGrid({ session }: FileGridProps) {
|
||||
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"
|
||||
>
|
||||
{fileType !== "unknown" && <div className=" self-center max-w-50"><FilePreview fileId={file.id} fileType={fileType} /></div>}
|
||||
{<div className=" self-center max-w-50"><FilePreview fileId={file.id} fileType={file.extension} /></div>}
|
||||
|
||||
<button onClick={() => router.push(pageUrl + file.url)}>
|
||||
<h3 className="text-2xl font-bold">{file.name}</h3>
|
||||
@ -110,68 +113,13 @@ export default function FileGrid({ session }: FileGridProps) {
|
||||
|
||||
|
||||
<div className="flex self-center gap-2">
|
||||
{/* Download Button */}
|
||||
<button
|
||||
onClick={() => handleDownload(file.id, file.name)}
|
||||
className="flex items-center justify-center rounded-full bg-blue-500 p-2 hover:bg-blue-600"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={2}
|
||||
stroke="currentColor"
|
||||
className="h-5 w-5 text-white"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M4 16v2a2 2 0 002 2h12a2 2 0 002-2v-2M7 10l5 5m0 0l5-5m-5 5V3"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{/* Copy URL Button */}
|
||||
<button
|
||||
onClick={() => handleCopyUrl(file.url)}
|
||||
className="flex items-center justify-center rounded-full bg-green-500 p-2 hover:bg-green-600"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={2}
|
||||
stroke="currentColor"
|
||||
className="h-5 w-5 text-white"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M8 9h8m-6 4h4m-7 8h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v12a2 2 0 002 2z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{/* Remove Button */}
|
||||
<button
|
||||
onClick={() => handleRemove(file.id)}
|
||||
className="flex items-center justify-center rounded-full bg-red-500 p-2 hover:bg-red-600"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={2}
|
||||
stroke="currentColor"
|
||||
className="h-5 w-5 text-white"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<FileActionsContainer
|
||||
fileId={file.id}
|
||||
fileName={file.name}
|
||||
fileUrl={file.url}
|
||||
isOwner={true}
|
||||
isPublic={file.isPublic}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { getFileType } from "~/utils/fileType"; // Adjust the import path as necessary
|
||||
|
||||
interface FilePreviewProps {
|
||||
fileId: string;
|
||||
@ -11,6 +12,8 @@ export function FilePreview({ fileId, fileType }: FilePreviewProps) {
|
||||
const [mediaSrc, setMediaSrc] = useState<string | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
console.log("File Type:", fileType);
|
||||
|
||||
useEffect(() => {
|
||||
if (!fileId) {
|
||||
setError("File ID is required.");
|
||||
@ -74,6 +77,31 @@ export function FilePreview({ fileId, fileType }: FilePreviewProps) {
|
||||
</audio>
|
||||
);
|
||||
}
|
||||
if (fileType.startsWith("image")) {
|
||||
return <img src={mediaSrc} alt="Media preview" className="max-w-full max-h-96 rounded-lg shadow-md" />;
|
||||
}
|
||||
|
||||
return <img src={mediaSrc} alt="Media preview" className="max-w-full max-h-96 rounded-lg shadow-md" />;
|
||||
if (fileType.startsWith("text")) {
|
||||
return (
|
||||
<img src="/icons/files/text.svg" alt="Text file preview" className="max-w-full max-h-96 rounded-lg invert" />
|
||||
);
|
||||
}
|
||||
if (fileType.startsWith("archive")) {
|
||||
return (
|
||||
<img src="/icons/files/archive.svg" alt="Archive file preview" className="max-w-full max-h-96 rounded-lg invert" />
|
||||
);
|
||||
}
|
||||
if (fileType.startsWith("code") || fileType.startsWith("markdown")) {
|
||||
return (
|
||||
<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);
|
||||
|
||||
return;
|
||||
}
|
||||
@ -6,7 +6,7 @@ export function HomeButton() {
|
||||
onClick={() => (window.location.href = "/")}
|
||||
className="rounded-full bg-white/10 px-4 py-2 font-semibold hover:bg-white/20"
|
||||
>
|
||||
Home
|
||||
<img src="/icons/home.svg" alt="Home" className="w-6 h-6" />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@ -1,13 +1,11 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { db } from "~/server/db";
|
||||
import { auth } from "~/server/auth";
|
||||
import path from "path";
|
||||
import { promises as fs } from "fs";
|
||||
import { minioClient } from "~/utils/minioClient";
|
||||
import { notifyClients } from "~/utils/notifyClients";
|
||||
|
||||
export async function DELETE(req: Request) {
|
||||
const session = await auth();
|
||||
|
||||
if (!session?.user) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
@ -18,32 +16,25 @@ export async function DELETE(req: Request) {
|
||||
return NextResponse.json({ error: "Invalid request body" }, { status: 400 });
|
||||
}
|
||||
|
||||
const resource = await db.file.findUnique({
|
||||
where: { id: body.id },
|
||||
});
|
||||
|
||||
if (!resource) {
|
||||
const file = await db.file.findUnique({ where: { id: body.id } });
|
||||
if (!file) {
|
||||
return NextResponse.json({ error: "File not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
if (resource.uploadedById !== session.user.id) {
|
||||
if (file.uploadedById !== session.user.id) {
|
||||
return NextResponse.json({ error: "You are not authorized to delete this file" }, { status: 403 });
|
||||
}
|
||||
|
||||
const filePath = path.join(process.cwd(), "uploads", path.basename(body.id));
|
||||
await fs.unlink(filePath).catch((err) => {
|
||||
console.error("Error deleting file from filesystem:", err);
|
||||
});
|
||||
const objectName = `${file.id}-${file.name}`;
|
||||
await minioClient.removeObject(process.env.MINIO_BUCKET || "file-hosting", objectName);
|
||||
|
||||
await db.file.delete({
|
||||
where: { id: body.id },
|
||||
});
|
||||
await db.file.delete({ where: { id: body.id } });
|
||||
|
||||
notifyClients({ type: "file-removed", fileId: body.id });
|
||||
|
||||
return NextResponse.json({ message: "File deleted successfully" });
|
||||
} catch (error) {
|
||||
console.error("Error deleting file:", error);
|
||||
console.error("Error deleting file from MinIO:", error);
|
||||
return NextResponse.json({ error: "Failed to delete file" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
25
src/app/api/files/search/route.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { db } from "~/server/db";
|
||||
|
||||
export async function GET(req: Request) {
|
||||
const url = new URL(req.url);
|
||||
const query = url.searchParams.get("query") || "";
|
||||
|
||||
try {
|
||||
// if query is empty, return no files
|
||||
const files = await db.file.findMany({
|
||||
where: {
|
||||
OR: [
|
||||
{ name: { contains: query } },
|
||||
{ description: { contains: query } },
|
||||
],
|
||||
public: true,
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json({ files });
|
||||
} catch (error) {
|
||||
console.error("Error fetching files:", error);
|
||||
return NextResponse.json({ error: "Failed to fetch files" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@ -1,7 +1,7 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import path from "path";
|
||||
import { promises as fs } from "fs";
|
||||
import { db } from "~/server/db";
|
||||
import { minioClient } from "~/utils/minioClient";
|
||||
import { getFileType } from "~/utils/fileType";
|
||||
|
||||
export async function GET(req: Request) {
|
||||
const url = new URL(req.url);
|
||||
@ -21,41 +21,31 @@ export async function GET(req: Request) {
|
||||
return NextResponse.json({ error: "File not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
// Construct the file path
|
||||
const filePath = path.join(process.cwd(), "uploads", file.id);
|
||||
const bucketName = process.env.MINIO_BUCKET || "file-hosting";
|
||||
const objectName = `${file.id}-${file.name}`; // Construct the object name in MinIO
|
||||
|
||||
// Read the file from the filesystem
|
||||
const fileBuffer = await fs.readFile(filePath);
|
||||
// Fetch the file from MinIO
|
||||
const stream = await minioClient.getObject(bucketName, objectName);
|
||||
|
||||
const mimeType = file.extension === ".mp4"
|
||||
? "video/mp4"
|
||||
: file.extension === ".webm"
|
||||
? "video/webm"
|
||||
: file.extension === ".ogg"
|
||||
? "video/ogg"
|
||||
: file.extension === ".jpg" || file.extension === ".jpeg"
|
||||
? "image/jpeg"
|
||||
: file.extension === ".png"
|
||||
? "image/png"
|
||||
: file.extension === ".gif"
|
||||
? "image/gif"
|
||||
: file.extension === ".svg"
|
||||
? "image/svg+xml"
|
||||
: file.extension === ".mp3"
|
||||
? "audio/mpeg"
|
||||
: file.extension === ".wav"
|
||||
? "audio/wav"
|
||||
: "application/octet-stream";
|
||||
const mimeType = getFileType(file.name); // Get the MIME type based on the file extension
|
||||
|
||||
// Return the file as a binary response
|
||||
return new Response(fileBuffer, {
|
||||
const readableStream = new ReadableStream({
|
||||
start(controller) {
|
||||
stream.on("data", (chunk) => controller.enqueue(chunk));
|
||||
stream.on("end", () => controller.close());
|
||||
stream.on("error", (err) => controller.error(err));
|
||||
},
|
||||
});
|
||||
|
||||
return new Response(readableStream, {
|
||||
headers: {
|
||||
"Content-Type": mimeType,
|
||||
"Content-Disposition": `inline; filename="${file.name}"`,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error fetching file:", error);
|
||||
console.error("Error fetching file from MinIO:", error);
|
||||
return NextResponse.json({ error: "Failed to fetch file" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@ -32,6 +32,7 @@ export async function GET(req: Request) {
|
||||
type: file.extension,
|
||||
url: file.url,
|
||||
description: file.description,
|
||||
isPublic: file.public, // Ensure this is included
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error fetching file details:", error);
|
||||
@ -48,7 +49,8 @@ export async function PUT(req: Request) {
|
||||
|
||||
try {
|
||||
const body = (await req.json()) as { id: string; description: string } | null;
|
||||
if (!body?.id || !body.description) {
|
||||
if (!body?.id || body.description === undefined) {
|
||||
// Allow empty description but ensure id is present
|
||||
return NextResponse.json({ error: "Invalid request body" }, { status: 400 });
|
||||
}
|
||||
|
||||
|
||||
22
src/app/api/files/update-public/route.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { db } from "~/server/db";
|
||||
|
||||
export async function POST(req: Request) {
|
||||
try {
|
||||
const { fileId, isPublic } = await req.json();
|
||||
|
||||
if (!fileId) {
|
||||
return NextResponse.json({ error: "File ID is required" }, { status: 400 });
|
||||
}
|
||||
|
||||
await db.file.update({
|
||||
where: { id: fileId },
|
||||
data: { public: isPublic },
|
||||
});
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error("Error updating public status:", error);
|
||||
return NextResponse.json({ error: "Failed to update public status" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@ -1,12 +1,10 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { promises as fs } from "fs";
|
||||
import path from "path";
|
||||
import { db } from "~/server/db";
|
||||
import { auth } from "~/server/auth";
|
||||
import Busboy from "busboy";
|
||||
import { Readable } from "stream";
|
||||
import { notifyClients } from "~/utils/notifyClients";
|
||||
import crypto from "crypto";
|
||||
import { db } from "~/server/db";
|
||||
import { auth } from "~/server/auth";
|
||||
import { minioClient, ensureBucketExists } from "~/utils/minioClient";
|
||||
|
||||
export const config = {
|
||||
api: {
|
||||
@ -16,15 +14,12 @@ export const config = {
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const session = await auth();
|
||||
// generate id for the file
|
||||
const guid = crypto.randomUUID();
|
||||
|
||||
if (!session?.user) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
const uploadDir = path.join(process.cwd(), "uploads");
|
||||
await fs.mkdir(uploadDir, { recursive: true });
|
||||
const bucketName = process.env.MINIO_BUCKET || "file-hosting";
|
||||
await ensureBucketExists(bucketName);
|
||||
|
||||
return new Promise<Response>((resolve, reject) => {
|
||||
const busboy = Busboy({ headers: { "content-type": req.headers.get("content-type") ?? "" } });
|
||||
@ -35,57 +30,39 @@ export async function POST(req: Request) {
|
||||
fileName = info.filename || "uploaded-file";
|
||||
const chunks: Buffer[] = [];
|
||||
|
||||
// Check if a file with the same name already exists for the user
|
||||
const existingFile = await db.file.findFirst({
|
||||
where: {
|
||||
name: fileName,
|
||||
uploadedById: session.user.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (existingFile) {
|
||||
// Modify the file name to make it unique
|
||||
const fileExtension = path.extname(fileName);
|
||||
const baseName = path.basename(fileName, fileExtension);
|
||||
fileName = `${baseName}-${Date.now()}${fileExtension}`;
|
||||
}
|
||||
|
||||
file.on("data", (chunk) => {
|
||||
chunks.push(chunk);
|
||||
});
|
||||
|
||||
file.on("end", () => {
|
||||
file.on("end", async () => {
|
||||
fileBuffer = Buffer.concat(chunks);
|
||||
});
|
||||
});
|
||||
|
||||
busboy.on("finish", () => {
|
||||
void (async () => {
|
||||
// Generate a unique ID for the file
|
||||
const fileId = crypto.randomUUID();
|
||||
const objectName = `${fileId}-${fileName}`;
|
||||
|
||||
try {
|
||||
const filePath = path.join(uploadDir, guid);
|
||||
await fs.writeFile(filePath, fileBuffer);
|
||||
// Upload the file to MinIO
|
||||
await minioClient.putObject(bucketName, objectName, fileBuffer);
|
||||
|
||||
// Save file metadata to the database
|
||||
const newFile = await db.file.create({
|
||||
data: {
|
||||
id: guid,
|
||||
url: `/share?id=${guid}`,
|
||||
id: fileId,
|
||||
url: `/share?id=${fileId}`,
|
||||
name: fileName,
|
||||
size: fileBuffer.length,
|
||||
extension: path.extname(fileName),
|
||||
extension: info.mimeType,
|
||||
uploadedById: session.user.id,
|
||||
},
|
||||
});
|
||||
|
||||
// Notify clients about the new file
|
||||
notifyClients({ type: "file-added", file: newFile });
|
||||
|
||||
resolve(NextResponse.json({ message: "File uploaded successfully" }));
|
||||
resolve(NextResponse.json({ message: "File uploaded successfully", file: newFile }));
|
||||
} catch (error) {
|
||||
console.error("Error handling upload:", error);
|
||||
resolve(NextResponse.json({ error: "Failed to upload file" }, { status: 500 }));
|
||||
console.error("Error uploading file to MinIO:", error);
|
||||
reject(new Error("Failed to upload file"));
|
||||
}
|
||||
})();
|
||||
});
|
||||
});
|
||||
|
||||
busboy.on("error", (error: unknown) => {
|
||||
|
||||
@ -8,14 +8,35 @@ import { Toaster } from "react-hot-toast";
|
||||
export default async function Home() {
|
||||
const session = await auth();
|
||||
|
||||
|
||||
return (
|
||||
<HydrateClient>
|
||||
<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">
|
||||
{/* Top-right corner sign-out button */}
|
||||
{session?.user && (
|
||||
<div className="absolute top-4 right-4">
|
||||
<div className="absolute top-4 right-4 flex items-center gap-4">
|
||||
{/* Search Button */}
|
||||
<Link
|
||||
href="/search"
|
||||
className="rounded-full bg-white/10 p-2 transition hover:bg-white/20"
|
||||
aria-label="Search"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-6 w-6 text-white"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M21 21l-4.35-4.35M16.65 10.65a6 6 0 11-12 0 6 6 0 0112 0z"
|
||||
/>
|
||||
</svg>
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href="/api/auth/signout"
|
||||
className="rounded-full bg-white/10 px-4 py-2 font-semibold no-underline transition hover:bg-white/20"
|
||||
@ -33,7 +54,7 @@ export default async function Home() {
|
||||
{session?.user ? (
|
||||
<>
|
||||
<FileGrid session={session} />
|
||||
<UploadForm/>
|
||||
<UploadForm />
|
||||
</>
|
||||
) : (
|
||||
<p className="text-center text-2xl text-white">
|
||||
@ -41,16 +62,16 @@ export default async function Home() {
|
||||
</p>
|
||||
)}
|
||||
{!session?.user && (
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<div className="flex flex-col items-center justify-center gap-4">
|
||||
<Link
|
||||
href={session ? "/api/auth/signout" : "/api/auth/signin"}
|
||||
className="rounded-full bg-white/10 px-10 py-3 font-semibold no-underline transition hover:bg-white/20"
|
||||
>
|
||||
{session ? "Sign out" : "Sign in"}
|
||||
</Link>
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<div className="flex flex-col items-center justify-center gap-4">
|
||||
<Link
|
||||
href={session ? "/api/auth/signout" : "/api/auth/signin"}
|
||||
className="rounded-full bg-white/10 px-10 py-3 font-semibold no-underline transition hover:bg-white/20"
|
||||
>
|
||||
{session ? "Sign out" : "Sign in"}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
121
src/app/search/page.tsx
Normal file
@ -0,0 +1,121 @@
|
||||
"use client";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { env } from "~/env.js";
|
||||
import { FilePreview } from "~/app/_components/FilePreview";
|
||||
import { FileActionsContainer } from "~/app/_components/ActionButtons";
|
||||
import { HomeButton } from "~/app/_components/HomeButton";
|
||||
|
||||
interface FileDetails {
|
||||
name: string;
|
||||
size: number;
|
||||
owner: string;
|
||||
ownerAvatar: string | null;
|
||||
uploadDate: string;
|
||||
id: string;
|
||||
isOwner: boolean;
|
||||
extension: string;
|
||||
url: string;
|
||||
description: string;
|
||||
isPublic: boolean;
|
||||
}
|
||||
|
||||
export default function SearchFile() {
|
||||
const [searchQuery, setSearchQuery] = useState<string>("");
|
||||
const [files, setFiles] = useState<FileDetails[]>([]);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const pageUrl = env.NEXT_PUBLIC_PAGE_URL;
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
const fetchFiles = async () => {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`/api/files/search?query=${encodeURIComponent(searchQuery)}`
|
||||
);
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to fetch files");
|
||||
}
|
||||
const data = await response.json();
|
||||
setFiles(data.files);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
setError("Failed to load files.");
|
||||
}
|
||||
};
|
||||
|
||||
fetchFiles();
|
||||
}, [searchQuery]);
|
||||
|
||||
return (
|
||||
<main className="flex min-h-screen flex-col items-center bg-gradient-to-b from-[#2e026d] to-[#15162c] text-white">
|
||||
<div className="absolute top-4 left-4 z-20">
|
||||
<HomeButton />
|
||||
</div>
|
||||
{/* Search Bar */}
|
||||
<div className="sticky top-0 z-10 w-full bg-[#2e026d] px-4 py-4 shadow-md">
|
||||
<div className="relative w-full max-w-md mx-auto">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search files..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="w-full p-3 pl-12 text-white bg-[#3b0764] rounded-full shadow-md focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent placeholder-gray-400"
|
||||
/>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="absolute left-4 top-1/2 transform -translate-y-1/2 h-6 w-6 text-gray-400"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M21 21l-4.35-4.35M16.65 10.65a6 6 0 11-12 0 6 6 0 0112 0z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* File Grid */}
|
||||
<div className="flex-grow flex flex-col items-center justify-center w-full px-4">
|
||||
{error && <div className="text-red-500">{error}</div>}
|
||||
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 w-full max-w-7xl">
|
||||
{files.map((file) => {
|
||||
return (
|
||||
<div
|
||||
key={file.id}
|
||||
className="flex place-content-end w-xxs flex-col gap-4 rounded-xl bg-white/10 p-4 hover:bg-white/20"
|
||||
>
|
||||
<div className="self-center max-w-100 sm:max-w-50">
|
||||
<FilePreview fileId={file.id} fileType={file.extension} />
|
||||
</div>
|
||||
|
||||
<button onClick={() => router.push(pageUrl + file.url)}>
|
||||
<h3 className="text-2xl font-bold">{file.name}</h3>
|
||||
</button>
|
||||
{file.description && (
|
||||
<p className="text-sm text-gray-400">
|
||||
Description: {file.description}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="flex self-center gap-2">
|
||||
<FileActionsContainer
|
||||
fileId={file.id}
|
||||
fileName={file.name}
|
||||
fileUrl={file.url}
|
||||
isOwner={false} // Check if the user is the owner
|
||||
isPublic={file.isPublic} // Check if the file is public
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@ -2,7 +2,7 @@ import { notFound } from "next/navigation";
|
||||
import { FilePreview } from "~/app/_components/FilePreview";
|
||||
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
|
||||
import { FileActionsContainer, FileDescriptionContainer } from "~/app/_components/ActionButtons"; // Import the client component
|
||||
import Head from "next/head";
|
||||
|
||||
interface FileDetails {
|
||||
@ -16,6 +16,7 @@ interface FileDetails {
|
||||
type: string;
|
||||
url: string;
|
||||
description: string;
|
||||
isPublic: boolean;
|
||||
}
|
||||
|
||||
async function fetchFileDetails(fileId: string): Promise<FileDetails | null> {
|
||||
@ -41,15 +42,17 @@ async function fetchFileDetails(fileId: string): Promise<FileDetails | null> {
|
||||
export default async function FilePreviewContainer({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: { id?: string };
|
||||
searchParams: Promise<{ id?: string }>;
|
||||
}) {
|
||||
const fileId = searchParams.id;
|
||||
const resolvedSearchParams = await searchParams; // Resolve the promise
|
||||
const fileId = resolvedSearchParams.id;
|
||||
|
||||
if (!fileId) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const fileDetails = await fetchFileDetails(fileId);
|
||||
|
||||
|
||||
if (!fileDetails) {
|
||||
return (
|
||||
@ -143,16 +146,17 @@ export default async function FilePreviewContainer({
|
||||
<strong>Upload Date:</strong>{" "}
|
||||
{new Date(fileDetails.uploadDate).toLocaleString()}
|
||||
</p>
|
||||
<p>
|
||||
<div>
|
||||
<strong>Description:</strong>{" "}
|
||||
{fileDetails.description || "No description available"}
|
||||
</p>
|
||||
<FileDescriptionContainer fileId={fileDetails.id} fileDescription={fileDetails.description}/>
|
||||
</div>
|
||||
<div className="mt-4 flex justify-center">
|
||||
<FileActionsContainer
|
||||
fileId={fileDetails.id}
|
||||
fileName={fileDetails.name}
|
||||
fileUrl={fileDetails.url}
|
||||
isOwner={fileDetails.isOwner}
|
||||
isPublic={fileDetails.isPublic}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
11
src/env.js
@ -19,7 +19,11 @@ export const env = createEnv({
|
||||
NODE_ENV: z
|
||||
.enum(["development", "test", "production"])
|
||||
.default("development"),
|
||||
|
||||
MINIO_ENDPOINT: z.string(),
|
||||
MINIO_PORT: z.string(),
|
||||
MINIO_ACCESS_KEY: z.string(),
|
||||
MINIO_SECRET_KEY: z.string(),
|
||||
MINIO_BUCKET: z.string(),
|
||||
},
|
||||
|
||||
/**
|
||||
@ -43,6 +47,11 @@ export const env = createEnv({
|
||||
DATABASE_URL: process.env.DATABASE_URL,
|
||||
NODE_ENV: process.env.NODE_ENV,
|
||||
NEXT_PUBLIC_PAGE_URL: process.env.NEXT_PUBLIC_PAGE_URL,
|
||||
MINIO_ENDPOINT: process.env.MINIO_ENDPOINT,
|
||||
MINIO_PORT: process.env.MINIO_PORT,
|
||||
MINIO_ACCESS_KEY: process.env.MINIO_ACCESS_KEY,
|
||||
MINIO_SECRET_KEY: process.env.MINIO_SECRET_KEY,
|
||||
MINIO_BUCKET: process.env.MINIO_BUCKET,
|
||||
},
|
||||
/**
|
||||
* Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially
|
||||
|
||||
32
src/utils/fileType.ts
Normal file
@ -0,0 +1,32 @@
|
||||
// This function takes a file name as input and returns the file type based on its extension.
|
||||
|
||||
export function getFileType(fileName: string): string {
|
||||
const extension = fileName.split(".").pop()?.toLowerCase();
|
||||
const fileTypes: Record<string, string> = {
|
||||
"mp4": "video/mp4",
|
||||
"webm": "video/webm",
|
||||
"ogg": "video/ogg",
|
||||
"jpg": "image/jpeg",
|
||||
"jpeg": "image/jpeg",
|
||||
"png": "image/png",
|
||||
"gif": "image/gif",
|
||||
"svg": "image/svg+xml",
|
||||
"mp3": "audio/mpeg",
|
||||
"wav": "audio/wav",
|
||||
"zip": "archive/zip",
|
||||
"rar": "archive/rar",
|
||||
"pdf": "text/pdf",
|
||||
"txt": "text/plain",
|
||||
"c": "code/c",
|
||||
"cpp": "code/cpp",
|
||||
"py": "code/python",
|
||||
"js": "code/javascript",
|
||||
"html": "code/html",
|
||||
"css": "code/css",
|
||||
"md": "markdown/markdown",
|
||||
"json": "code/json",
|
||||
"xml": "code/xml",
|
||||
"csv": "code/csv",
|
||||
};
|
||||
return extension ? fileTypes[extension] || "unknown" : "unknown";
|
||||
};
|
||||
19
src/utils/minioClient.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { Client } from "minio";
|
||||
import { env } from "~/env";
|
||||
|
||||
export const minioClient = new Client({
|
||||
endPoint: env.MINIO_ENDPOINT,
|
||||
port: parseInt(env.MINIO_PORT, 10),
|
||||
useSSL: false, // Set to true if using HTTPS
|
||||
accessKey: env.MINIO_ACCESS_KEY,
|
||||
secretKey: env.MINIO_SECRET_KEY,
|
||||
});
|
||||
|
||||
// Ensure the bucket exists
|
||||
export async function ensureBucketExists(bucketName: string) {
|
||||
const exists = await minioClient.bucketExists(bucketName);
|
||||
if (!exists) {
|
||||
await minioClient.makeBucket(bucketName, "us-east-1");
|
||||
console.log(`Bucket "${bucketName}" created.`);
|
||||
}
|
||||
}
|
||||