implemented basic support for pdf, png and jpg

This commit is contained in:
daniel31x13 2023-12-03 23:52:32 -05:00
parent 33be9e5d83
commit 9c65e3e215
16 changed files with 443 additions and 69 deletions

View File

@ -13,6 +13,7 @@ STORAGE_FOLDER=
AUTOSCROLL_TIMEOUT=
NEXT_PUBLIC_DISABLE_REGISTRATION=
RE_ARCHIVE_LIMIT=
NEXT_PUBLIC_MAX_UPLOAD_SIZE=
# AWS S3 Settings
SPACES_KEY=

View File

@ -13,14 +13,9 @@ type Props = {
value?: number;
}
| undefined;
id?: string;
};
export default function CollectionSelection({
onChange,
defaultValue,
id,
}: Props) {
export default function CollectionSelection({ onChange, defaultValue }: Props) {
const { collections } = useCollectionStore();
const router = useRouter();
@ -49,7 +44,6 @@ export default function CollectionSelection({
return (
<CreatableSelect
key={id || "key"}
isClearable={false}
className="react-select-container"
classNamePrefix="react-select"

View File

@ -64,10 +64,12 @@ export default function DeleteCollectionModal({
return (
<Modal toggleModal={onClose}>
<p className="text-xl mb-5 font-thin text-red-500">
<p className="text-xl font-thin text-red-500">
{permissions === true ? "Delete" : "Leave"} Collection
</p>
<div className="divider my-3"></div>
<div className="flex flex-col gap-3">
{permissions === true ? (
<>

View File

@ -21,14 +21,6 @@ export default function DeleteLinkModal({ onClose, activeLink }: Props) {
const [link, setLink] =
useState<LinkIncludingShortenedCollectionAndTags>(activeLink);
let shortendURL;
try {
shortendURL = new URL(link.url).host.toLowerCase();
} catch (error) {
console.log(error);
}
const { removeLink } = useLinkStore();
const [submitLoader, setSubmitLoader] = useState(false);
@ -50,7 +42,10 @@ export default function DeleteLinkModal({ onClose, activeLink }: Props) {
return (
<Modal toggleModal={onClose}>
<p className="text-xl mb-5 font-thin text-red-500">Delete Link</p>
<p className="text-xl font-thin text-red-500">Delete Link</p>
<div className="divider my-3"></div>
<div className="flex flex-col gap-3">
<p>Are you sure you want to delete this Link?</p>

View File

@ -49,7 +49,9 @@ export default function EditCollectionModal({
return (
<Modal toggleModal={onClose}>
<p className="text-xl mb-5 font-thin">Edit Collection Info</p>
<p className="text-xl font-thin">Edit Collection Info</p>
<div className="divider my-3"></div>
<div className="flex flex-col gap-3">
<div className="flex flex-col sm:flex-row gap-3">

View File

@ -95,10 +95,12 @@ export default function EditCollectionSharingModal({
return (
<Modal toggleModal={onClose}>
<p className="text-xl font-thin mb-5">
<p className="text-xl font-thin">
{permissions === true ? "Share and Collaborate" : "Team"}
</p>
<div className="divider my-3"></div>
<div className="flex flex-col gap-3">
{permissions === true && (
<div>
@ -178,7 +180,7 @@ export default function EditCollectionSharingModal({
setMemberState
)
}
className="btn btn-primary text-white btn-square"
className="btn btn-primary btn-square btn-sm h-10 w-10"
>
<FontAwesomeIcon icon={faUserPlus} className="w-5 h-5" />
</div>
@ -323,7 +325,7 @@ export default function EditCollectionSharingModal({
}}
/>
<span
className={`peer-checked:bg-primary text-sm ${
className={`peer-checked:bg-primary peer-checked:text-primary-content text-sm ${
permissions === true
? "hover:bg-neutral-content duration-100"
: ""
@ -368,7 +370,7 @@ export default function EditCollectionSharingModal({
}}
/>
<span
className={`peer-checked:bg-primary text-sm ${
className={`peer-checked:bg-primary peer-checked:text-primary-content text-sm ${
permissions === true
? "hover:bg-neutral-content duration-100"
: ""
@ -413,7 +415,7 @@ export default function EditCollectionSharingModal({
}}
/>
<span
className={`peer-checked:bg-primary text-sm ${
className={`peer-checked:bg-primary peer-checked:text-primary-content text-sm ${
permissions === true
? "hover:bg-neutral-content duration-100"
: ""

View File

@ -24,7 +24,7 @@ export default function EditLinkModal({ onClose, activeLink }: Props) {
let shortendURL;
try {
shortendURL = new URL(link.url).host.toLowerCase();
shortendURL = new URL(link.url || "").host.toLowerCase();
} catch (error) {
console.log(error);
}
@ -78,20 +78,24 @@ export default function EditLinkModal({ onClose, activeLink }: Props) {
return (
<Modal toggleModal={onClose}>
<p className="text-xl mb-5 font-thin">Edit Link</p>
<p className="text-xl font-thin">Edit Link</p>
<Link
href={link.url}
className="truncate text-neutral flex gap-2 mb-5 w-fit max-w-full"
title={link.url}
target="_blank"
>
<FontAwesomeIcon
icon={faLink}
className="mt-1 w-5 h-5 min-w-[1.25rem]"
/>
<p>{shortendURL}</p>
</Link>
<div className="divider my-3"></div>
{link.url ? (
<Link
href={link.url}
className="truncate text-neutral flex gap-2 mb-5 w-fit max-w-full"
title={link.url}
target="_blank"
>
<FontAwesomeIcon
icon={faLink}
className="mt-1 w-5 h-5 min-w-[1.25rem]"
/>
<p>{shortendURL}</p>
</Link>
) : undefined}
<div className="w-full">
<p className="mb-2">Name</p>

View File

@ -54,7 +54,9 @@ export default function NewCollectionModal({ onClose }: Props) {
return (
<Modal toggleModal={onClose}>
<p className="text-xl mb-5 font-thin">Create a New Collection</p>
<p className="text-xl font-thin">Create a New Collection</p>
<div className="divider my-3"></div>
<div className="flex flex-col gap-3">
<div className="flex flex-col sm:flex-row gap-3">

View File

@ -41,8 +41,6 @@ export default function NewLinkModal({ onClose }: Props) {
const { addLink } = useLinkStore();
const [submitLoader, setSubmitLoader] = useState(false);
const [resetCollectionSelection, setResetCollectionSelection] = useState("");
const [optionsExpanded, setOptionsExpanded] = useState(false);
const router = useRouter();
@ -66,9 +64,6 @@ export default function NewLinkModal({ onClose }: Props) {
};
useEffect(() => {
setResetCollectionSelection(Date.now().toString());
console.log(link);
setOptionsExpanded(false);
if (router.query.id) {
const currentCollection = collections.find(
@ -123,7 +118,10 @@ export default function NewLinkModal({ onClose }: Props) {
return (
<Modal toggleModal={onClose}>
<p className="text-xl mb-5 font-thin">Create a New Link</p>
<p className="text-xl font-thin">Create a New Link</p>
<div className="divider my-3"></div>
<div className="grid grid-flow-row-dense sm:grid-cols-5 gap-3">
<div className="sm:col-span-3 col-span-5">
<p className="mb-2">Link</p>
@ -143,7 +141,6 @@ export default function NewLinkModal({ onClose }: Props) {
label: link.collection.name,
value: link.collection.id,
}}
id={resetCollectionSelection}
/>
) : null}
</div>

View File

@ -0,0 +1,237 @@
import React, { useEffect, useState } from "react";
import { Toaster } from "react-hot-toast";
import CollectionSelection from "@/components/InputSelect/CollectionSelection";
import TagSelection from "@/components/InputSelect/TagSelection";
import TextInput from "@/components/TextInput";
import unescapeString from "@/lib/client/unescapeString";
import useCollectionStore from "@/store/collections";
import useLinkStore from "@/store/links";
import {
ArchivedFormat,
LinkIncludingShortenedCollectionAndTags,
} from "@/types/global";
import { useSession } from "next-auth/react";
import { useRouter } from "next/router";
import toast from "react-hot-toast";
import Modal from "../Modal";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faQuestion } from "@fortawesome/free-solid-svg-icons";
import { faQuestionCircle } from "@fortawesome/free-regular-svg-icons";
type Props = {
onClose: Function;
};
export default function UploadFileModal({ onClose }: Props) {
const { data } = useSession();
const initial = {
name: "",
url: "",
description: "",
type: "url",
tags: [],
screenshotPath: "",
pdfPath: "",
readabilityPath: "",
textContent: "",
collection: {
name: "",
ownerId: data?.user.id as number,
},
} as LinkIncludingShortenedCollectionAndTags;
const [link, setLink] =
useState<LinkIncludingShortenedCollectionAndTags>(initial);
const [file, setFile] = useState<File>();
const { addLink } = useLinkStore();
const [submitLoader, setSubmitLoader] = useState(false);
const [optionsExpanded, setOptionsExpanded] = useState(false);
const router = useRouter();
const { collections } = useCollectionStore();
const setCollection = (e: any) => {
if (e?.__isNew__) e.value = null;
setLink({
...link,
collection: { id: e?.value, name: e?.label, ownerId: e?.ownerId },
});
};
const setTags = (e: any) => {
const tagNames = e.map((e: any) => {
return { name: e.label };
});
setLink({ ...link, tags: tagNames });
};
useEffect(() => {
setOptionsExpanded(false);
if (router.query.id) {
const currentCollection = collections.find(
(e) => e.id == Number(router.query.id)
);
if (
currentCollection &&
currentCollection.ownerId &&
router.asPath.startsWith("/collections/")
)
setLink({
...initial,
collection: {
id: currentCollection.id,
name: currentCollection.name,
ownerId: currentCollection.ownerId,
},
});
} else
setLink({
...initial,
collection: {
name: "Unorganized",
ownerId: data?.user.id as number,
},
});
}, []);
const submit = async () => {
if (!submitLoader && file) {
let fileType: ArchivedFormat | null = null;
if (file?.type === "image/jpg" || file.type === "image/jpeg")
fileType = ArchivedFormat.jpeg;
else if (file.type === "image/png") fileType = ArchivedFormat.png;
else if (file.type === "application/pdf") fileType = ArchivedFormat.pdf;
console.log(fileType);
if (fileType !== null) {
setSubmitLoader(true);
let response;
const load = toast.loading("Creating...");
response = await addLink(link);
toast.dismiss(load);
if (response.ok) {
const formBody = new FormData();
file && formBody.append("file", file);
console.log(formBody.get("file"));
await fetch(
`/api/v1/archives/${
(response.data as LinkIncludingShortenedCollectionAndTags).id
}?format=${fileType}`,
{
body: formBody,
method: "POST",
}
);
toast.success(`Created!`);
onClose();
} else toast.error(response.data as string);
setSubmitLoader(false);
return response;
}
}
};
return (
<Modal toggleModal={onClose}>
<div className="flex gap-2 items-start">
<p className="text-xl font-thin">Upload File</p>
</div>
<div className="divider my-3"></div>
<div className="grid grid-flow-row-dense sm:grid-cols-5 gap-3">
<div className="sm:col-span-3 col-span-5">
<p className="mb-2">File</p>
<label className="btn h-10 btn-sm w-full border border-neutral-content hover:border-neutral-content flex justify-between">
<input
type="file"
accept=".pdf,.png,.jpg,.jpeg"
className="cursor-pointer custom-file-input"
onChange={(e) => e.target.files && setFile(e.target.files[0])}
/>
</label>
<p className="text-xs font-semibold mt-2">
PDF, PNG, JPG (Up to {process.env.NEXT_PUBLIC_MAX_UPLOAD_SIZE || 30}
MB)
</p>
</div>
<div className="sm:col-span-2 col-span-5">
<p className="mb-2">Collection</p>
{link.collection.name ? (
<CollectionSelection
onChange={setCollection}
defaultValue={{
label: link.collection.name,
value: link.collection.id,
}}
/>
) : null}
</div>
</div>
{optionsExpanded ? (
<div className="mt-5">
{/* <hr className="mb-3 border border-neutral-content" /> */}
<div className="grid sm:grid-cols-2 gap-3">
<div>
<p className="mb-2">Name</p>
<TextInput
value={link.name}
onChange={(e) => setLink({ ...link, name: e.target.value })}
placeholder="e.g. Example Link"
className="bg-base-200"
/>
</div>
<div>
<p className="mb-2">Tags</p>
<TagSelection
onChange={setTags}
defaultValue={link.tags.map((e) => {
return { label: e.name, value: e.id };
})}
/>
</div>
<div className="sm:col-span-2">
<p className="mb-2">Description</p>
<textarea
value={unescapeString(link.description) as string}
onChange={(e) =>
setLink({ ...link, description: e.target.value })
}
placeholder="Will be auto generated if nothing is provided."
className="resize-none w-full rounded-md p-2 border-neutral-content bg-base-200 focus:border-sky-300 dark:focus:border-sky-600 border-solid border outline-none duration-100"
/>
</div>
</div>
</div>
) : undefined}
<div className="flex justify-between items-center mt-5">
<div
onClick={() => setOptionsExpanded(!optionsExpanded)}
className={`rounded-md cursor-pointer btn btn-sm btn-ghost duration-100 flex items-center px-2 w-fit text-sm`}
>
<p>{optionsExpanded ? "Hide" : "More"} Options</p>
</div>
<button className="btn btn-accent" onClick={submit}>
Create Link
</button>
</div>
</Modal>
);
}

View File

@ -14,6 +14,7 @@ import useLocalSettingsStore from "@/store/localSettings";
import NewLinkModal from "./ModalContent/NewLinkModal";
import NewCollectionModal from "./ModalContent/NewCollectionModal";
import Link from "next/link";
import UploadFileModal from "./ModalContent/UploadFileModal";
export default function Navbar() {
const { settings, updateSettings } = useLocalSettingsStore();
@ -48,6 +49,7 @@ export default function Navbar() {
const [newLinkModal, setNewLinkModal] = useState(false);
const [newCollectionModal, setNewCollectionModal] = useState(false);
const [uploadFileModal, setUploadFileModal] = useState(false);
return (
<div className="flex justify-between gap-2 items-center px-4 py-2 border-solid border-b-neutral-content border-b">
@ -88,6 +90,18 @@ export default function Navbar() {
New Link
</div>
</li>
<li>
<div
onClick={() => {
(document?.activeElement as HTMLElement)?.blur();
setUploadFileModal(true);
}}
tabIndex={0}
role="button"
>
Upload File
</div>
</li>
<li>
<div
onClick={() => {
@ -163,6 +177,9 @@ export default function Navbar() {
{newCollectionModal ? (
<NewCollectionModal onClose={() => setNewCollectionModal(false)} />
) : undefined}
{uploadFileModal ? (
<UploadFileModal onClose={() => setUploadFileModal(false)} />
) : undefined}
</div>
);
}

View File

@ -25,6 +25,7 @@
"@prisma/client": "^4.16.2",
"@stripe/stripe-js": "^1.54.1",
"@types/crypto-js": "^4.1.1",
"@types/formidable": "^3.4.5",
"@types/node": "20.4.4",
"@types/nodemailer": "^6.4.8",
"@types/react": "18.2.14",
@ -37,6 +38,7 @@
"dompurify": "^3.0.6",
"eslint": "8.46.0",
"eslint-config-next": "13.4.9",
"formidable": "^3.5.1",
"framer-motion": "^10.16.4",
"jsdom": "^22.1.0",
"lottie-web": "^5.12.2",

View File

@ -3,49 +3,129 @@ import readFile from "@/lib/api/storage/readFile";
import { getToken } from "next-auth/jwt";
import { prisma } from "@/lib/api/db";
import { ArchivedFormat } from "@/types/global";
import verifyUser from "@/lib/api/verifyUser";
import getPermission from "@/lib/api/getPermission";
import { UsersAndCollections } from "@prisma/client";
import formidable from "formidable";
import createFile from "@/lib/api/storage/createFile";
import fs from "fs";
export const config = {
api: {
bodyParser: false,
},
};
export default async function Index(req: NextApiRequest, res: NextApiResponse) {
const linkId = Number(req.query.linkId);
const format = Number(req.query.format);
let suffix;
let suffix: string;
if (format === ArchivedFormat.png) suffix = ".png";
else if (format === ArchivedFormat.jpeg) suffix = ".jpeg";
else if (format === ArchivedFormat.pdf) suffix = ".pdf";
else if (format === ArchivedFormat.readability) suffix = "_readability.json";
//@ts-ignore
if (!linkId || !suffix)
return res.status(401).json({ response: "Invalid parameters." });
const token = await getToken({ req });
const userId = token?.id;
if (req.method === "GET") {
const token = await getToken({ req });
const userId = token?.id;
const collectionIsAccessible = await prisma.collection.findFirst({
where: {
links: {
some: {
id: linkId,
const collectionIsAccessible = await prisma.collection.findFirst({
where: {
links: {
some: {
id: linkId,
},
},
OR: [
{ ownerId: userId || -1 },
{ members: { some: { userId: userId || -1 } } },
{ isPublic: true },
],
},
OR: [
{ ownerId: userId || -1 },
{ members: { some: { userId: userId || -1 } } },
{ isPublic: true },
],
},
});
});
if (!collectionIsAccessible)
return res
.status(401)
.json({ response: "You don't have access to this collection." });
if (!collectionIsAccessible)
return res
.status(401)
.json({ response: "You don't have access to this collection." });
const { file, contentType, status } = await readFile(
`archives/${collectionIsAccessible.id}/${linkId + suffix}`
);
const { file, contentType, status } = await readFile(
`archives/${collectionIsAccessible.id}/${linkId + suffix}`
);
res.setHeader("Content-Type", contentType).status(status as number);
res.setHeader("Content-Type", contentType).status(status as number);
return res.send(file);
return res.send(file);
} else if (req.method === "POST") {
const user = await verifyUser({ req, res });
if (!user) return;
const collectionPermissions = await getPermission({
userId: user.id,
linkId,
});
const memberHasAccess = collectionPermissions?.members.some(
(e: UsersAndCollections) => e.userId === user.id && e.canCreate
);
if (!(collectionPermissions?.ownerId === user.id || memberHasAccess))
return { response: "Collection is not accessible.", status: 401 };
// await uploadHandler(linkId, )
const MAX_UPLOAD_SIZE = Number(process.env.NEXT_PUBLIC_MAX_UPLOAD_SIZE);
const form = formidable({
maxFields: 1,
maxFiles: 1,
maxFileSize: MAX_UPLOAD_SIZE || 30 * 1048576,
});
form.parse(req, async (err, fields, files) => {
console.log(files);
const allowedMIMETypes = [
"application/pdf",
"image/png",
"image/jpg",
"image/jpeg",
];
if (
err ||
!files.file ||
!files.file[0] ||
!allowedMIMETypes.includes(files.file[0].mimetype || "")
) {
// Handle parsing error
return res.status(500).json({
response: `Sorry, we couldn't process your file. Please ensure it's a PDF, PNG, or JPG format and doesn't exceed ${MAX_UPLOAD_SIZE}MB.`,
});
} else {
console.log(files.file[0].mimetype);
const fileBuffer = fs.readFileSync(files.file[0].filepath);
console.log(fileBuffer);
await createFile({
filePath: `archives/${collectionPermissions?.id}/${linkId + suffix}`,
data: fileBuffer,
});
fs.unlinkSync(files.file[0].filepath);
}
return res.status(200).json({
response: files,
});
});
}
}

View File

@ -286,3 +286,7 @@ body {
background-position: 0% 50%;
}
}
.custom-file-input::file-selector-button {
cursor: pointer;
}

View File

@ -9,6 +9,7 @@ declare global {
STORAGE_FOLDER?: string;
AUTOSCROLL_TIMEOUT?: string;
RE_ARCHIVE_LIMIT?: string;
NEXT_PUBLIC_MAX_UPLOAD_SIZE?: string;
SPACES_KEY?: string;
SPACES_SECRET?: string;

View File

@ -1497,6 +1497,13 @@
dependencies:
"@types/trusted-types" "*"
"@types/formidable@^3.4.5":
version "3.4.5"
resolved "https://registry.yarnpkg.com/@types/formidable/-/formidable-3.4.5.tgz#8e45c053cac5868e2b71cc7410e2bd92872f6b9c"
integrity sha512-s7YPsNVfnsng5L8sKnG/Gbb2tiwwJTY1conOkJzTMRvJAlLFW1nEua+ADsJQu8N1c0oTHx9+d5nqg10WuT9gHQ==
dependencies:
"@types/node" "*"
"@types/jsdom@^21.1.3":
version "21.1.3"
resolved "https://registry.yarnpkg.com/@types/jsdom/-/jsdom-21.1.3.tgz#a88c5dc65703e1b10b2a7839c12db49662b43ff0"
@ -1766,6 +1773,11 @@ array.prototype.tosorted@^1.1.1:
es-shim-unscopables "^1.0.0"
get-intrinsic "^1.1.3"
asap@^2.0.0:
version "2.0.6"
resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46"
integrity sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==
asn1@~0.2.3:
version "0.2.6"
resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.6.tgz#0d3a7bb6e64e02a90c0303b31f292868ea09a08d"
@ -2299,6 +2311,14 @@ detect-libc@^2.0.0, detect-libc@^2.0.1:
resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.1.tgz#e1897aa88fa6ad197862937fbc0441ef352ee0cd"
integrity sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==
dezalgo@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/dezalgo/-/dezalgo-1.0.4.tgz#751235260469084c132157dfa857f386d4c33d81"
integrity sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==
dependencies:
asap "^2.0.0"
wrappy "1"
didyoumean@^1.2.2:
version "1.2.2"
resolved "https://registry.yarnpkg.com/didyoumean/-/didyoumean-1.2.2.tgz#989346ffe9e839b4555ecf5666edea0d3e8ad037"
@ -2836,6 +2856,15 @@ form-data@~2.3.2:
combined-stream "^1.0.6"
mime-types "^2.1.12"
formidable@^3.5.1:
version "3.5.1"
resolved "https://registry.yarnpkg.com/formidable/-/formidable-3.5.1.tgz#9360a23a656f261207868b1484624c4c8d06ee1a"
integrity sha512-WJWKelbRHN41m5dumb0/k8TeAx7Id/y3a+Z7QfhxP/htI9Js5zYaEDtG8uMgG0vM0lOlqnmjE99/kfpOYi/0Og==
dependencies:
dezalgo "^1.0.4"
hexoid "^1.0.0"
once "^1.4.0"
fraction.js@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.2.0.tgz#448e5109a313a3527f5a3ab2119ec4cf0e0e2950"
@ -3151,6 +3180,11 @@ has@^1.0.3:
dependencies:
function-bind "^1.1.1"
hexoid@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/hexoid/-/hexoid-1.0.0.tgz#ad10c6573fb907de23d9ec63a711267d9dc9bc18"
integrity sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g==
hoist-non-react-statics@^3.3.1:
version "3.3.2"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"