diff --git a/components/LinkViews/LinkCard.tsx b/components/LinkViews/LinkCard.tsx index d93904a..9299442 100644 --- a/components/LinkViews/LinkCard.tsx +++ b/components/LinkViews/LinkCard.tsx @@ -19,6 +19,7 @@ import { generateLinkHref } from "@/lib/client/generateLinkHref"; import useAccountStore from "@/store/account"; import usePermissions from "@/hooks/usePermissions"; import toast from "react-hot-toast"; +import LinkTypeBadge from "./LinkComponents/LinkTypeBadge"; type Props = { link: LinkIncludingShortenedCollectionAndTags; @@ -53,7 +54,9 @@ export default function LinkCard({ link, flipDropdown, editMode }: Props) { let shortendURL; try { - shortendURL = new URL(link.url || "").host.toLowerCase(); + if (link.url) { + shortendURL = new URL(link.url).host.toLowerCase(); + } } catch (error) { console.log(error); } @@ -109,7 +112,6 @@ export default function LinkCard({ link, flipDropdown, editMode }: Props) { editMode && (permissions === true || permissions?.canCreate || permissions?.canDelete); - // window.open ('www.yourdomain.com', '_ blank'); return (
- { - e.stopPropagation(); - }} - className="flex gap-1 item-center select-none text-neutral mt-1 hover:opacity-70 duration-100" - > - -

{shortendURL}

- +

diff --git a/components/LinkViews/LinkComponents/LinkActions.tsx b/components/LinkViews/LinkComponents/LinkActions.tsx index d809dfc..6c25093 100644 --- a/components/LinkViews/LinkComponents/LinkActions.tsx +++ b/components/LinkViews/LinkComponents/LinkActions.tsx @@ -122,18 +122,20 @@ export default function LinkActions({ ) : undefined} -
  • -
    { - (document?.activeElement as HTMLElement)?.blur(); - setPreservedFormatsModal(true); - }} - > - Preserved Formats -
    -
  • + {link.type === "url" && ( +
  • +
    { + (document?.activeElement as HTMLElement)?.blur(); + setPreservedFormatsModal(true); + }} + > + Preserved Formats +
    +
  • + )} {permissions === true || permissions?.canDelete ? (
  • (true); return ( <> - {link.url && url && showFavicon ? ( - { - setShowFavicon(false); - }} - /> - ) : showFavicon === false ? ( -
    - -
    + {link.type === "url" && url ? ( + showFavicon ? ( + { + setShowFavicon(false); + }} + /> + ) : ( + + ) ) : link.type === "pdf" ? ( - + ) : link.type === "image" ? ( - + ) : undefined} ); } + +const LinkPlaceholderIcon = ({ + iconClasses, + icon, +}: { + iconClasses: string; + icon: string; +}) => { + return ( +
    + +
    + ); +}; diff --git a/components/LinkViews/LinkComponents/LinkTypeBadge.tsx b/components/LinkViews/LinkComponents/LinkTypeBadge.tsx new file mode 100644 index 0000000..e491330 --- /dev/null +++ b/components/LinkViews/LinkComponents/LinkTypeBadge.tsx @@ -0,0 +1,38 @@ +import { LinkIncludingShortenedCollectionAndTags } from "@/types/global"; +import Link from "next/link"; +import React from "react"; + +export default function LinkTypeBadge({ + link, +}: { + link: LinkIncludingShortenedCollectionAndTags; +}) { + let shortendURL; + + if (link.type === "url" && link.url) { + try { + shortendURL = new URL(link.url).host.toLowerCase(); + } catch (error) { + console.log(error); + } + } + + return link.url && shortendURL ? ( + { + e.stopPropagation(); + }} + className="flex gap-1 item-center select-none text-neutral mt-1 hover:opacity-70 duration-100" + > + +

    {shortendURL}

    + + ) : ( +
    + {link.type} +
    + ); +} diff --git a/components/LinkViews/LinkList.tsx b/components/LinkViews/LinkList.tsx index a535adf..e5bd8bc 100644 --- a/components/LinkViews/LinkList.tsx +++ b/components/LinkViews/LinkList.tsx @@ -16,6 +16,7 @@ import { generateLinkHref } from "@/lib/client/generateLinkHref"; import useAccountStore from "@/store/account"; import usePermissions from "@/hooks/usePermissions"; import toast from "react-hot-toast"; +import LinkTypeBadge from "./LinkComponents/LinkTypeBadge"; type Props = { link: LinkIncludingShortenedCollectionAndTags; @@ -56,14 +57,6 @@ export default function LinkCardCompact({ } }; - let shortendURL; - - try { - shortendURL = new URL(link.url || "").host.toLowerCase(); - } catch (error) { - console.log(error); - } - const [collection, setCollection] = useState( collections.find( @@ -130,7 +123,11 @@ export default function LinkCardCompact({ } >
    - +
    @@ -143,24 +140,7 @@ export default function LinkCardCompact({ {collection ? ( ) : undefined} - {link.url ? ( - { - e.stopPropagation(); - }} - className="flex gap-1 item-center select-none text-neutral mt-1 hover:opacity-70 duration-100" - > - -

    {shortendURL}

    - - ) : ( -
    - {link.type} -
    - )} +
    diff --git a/components/ModalContent/UploadFileModal.tsx b/components/ModalContent/UploadFileModal.tsx index 497bc91..49524aa 100644 --- a/components/ModalContent/UploadFileModal.tsx +++ b/components/ModalContent/UploadFileModal.tsx @@ -43,7 +43,7 @@ export default function UploadFileModal({ onClose }: Props) { const [file, setFile] = useState(); - const { addLink } = useLinkStore(); + const { uploadFile } = useLinkStore(); const [submitLoader, setSubmitLoader] = useState(false); const [optionsExpanded, setOptionsExpanded] = useState(false); @@ -100,56 +100,22 @@ export default function UploadFileModal({ onClose }: Props) { const submit = async () => { if (!submitLoader && file) { - let fileType: ArchivedFormat | null = null; - let linkType: "url" | "image" | "pdf" | null = null; + setSubmitLoader(true); - if (file?.type === "image/jpg" || file.type === "image/jpeg") { - fileType = ArchivedFormat.jpeg; - linkType = "image"; - } else if (file.type === "image/png") { - fileType = ArchivedFormat.png; - linkType = "image"; - } else if (file.type === "application/pdf") { - fileType = ArchivedFormat.pdf; - linkType = "pdf"; - } + const load = toast.loading("Creating..."); - if (fileType !== null && linkType !== null) { - setSubmitLoader(true); + const response = await uploadFile(link, file); - let response; + toast.dismiss(load); - const load = toast.loading("Creating..."); + if (response.ok) { + toast.success(`Created!`); + onClose(); + } else toast.error(response.data as string); - response = await addLink({ - ...link, - type: linkType, - name: link.name ? link.name : file.name.replace(/\.[^/.]+$/, ""), - }); + setSubmitLoader(false); - toast.dismiss(load); - - if (response.ok) { - const formBody = new FormData(); - file && formBody.append("file", 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 response; } }; @@ -238,7 +204,7 @@ export default function UploadFileModal({ onClose }: Props) { className="btn btn-accent dark:border-violet-400 text-white" onClick={submit} > - Create Link + Upload File diff --git a/lib/api/archiveHandler.ts b/lib/api/archiveHandler.ts index 08a35b5..c4b5c5e 100644 --- a/lib/api/archiveHandler.ts +++ b/lib/api/archiveHandler.ts @@ -10,6 +10,7 @@ import validateUrlSize from "./validateUrlSize"; import removeFile from "./storage/removeFile"; import Jimp from "jimp"; import createFolder from "./storage/createFolder"; +import generatePreview from "./generatePreview"; type LinksAndCollectionAndOwner = Link & { collection: Collection & { @@ -175,35 +176,7 @@ export default async function archiveHandler(link: LinksAndCollectionAndOwner) { // Check if imageResponse is not null if (imageResponse && !link.preview?.startsWith("archive")) { const buffer = await imageResponse.body(); - - // Check if buffer is not null - if (buffer) { - // Load the image using Jimp - Jimp.read(buffer, async (err, image) => { - if (image && !err) { - image?.resize(1280, Jimp.AUTO).quality(20); - const processedBuffer = await image?.getBufferAsync( - Jimp.MIME_JPEG - ); - - createFile({ - data: processedBuffer, - filePath: `archives/preview/${link.collectionId}/${link.id}.jpeg`, - }).then(() => { - return prisma.link.update({ - where: { id: link.id }, - data: { - preview: `archives/preview/${link.collectionId}/${link.id}.jpeg`, - }, - }); - }); - } - }).catch((err) => { - console.error("Error processing the image:", err); - }); - } else { - console.log("No image data found."); - } + await generatePreview(buffer, link.collectionId, link.id); } await page.goBack(); diff --git a/lib/api/controllers/links/postLink.ts b/lib/api/controllers/links/postLink.ts index 80205e4..85fd537 100644 --- a/lib/api/controllers/links/postLink.ts +++ b/lib/api/controllers/links/postLink.ts @@ -174,7 +174,7 @@ export default async function postLink( const newLink = await prisma.link.create({ data: { - url: link.url?.trim(), + url: link.url?.trim() || null, name: link.name, description, type: linkType, diff --git a/lib/api/generatePreview.ts b/lib/api/generatePreview.ts new file mode 100644 index 0000000..3c2da0f --- /dev/null +++ b/lib/api/generatePreview.ts @@ -0,0 +1,35 @@ +import Jimp from "jimp"; +import { prisma } from "./db"; +import createFile from "./storage/createFile"; + +const generatePreview = async ( + buffer: Buffer, + collectionId: number, + linkId: number +) => { + if (buffer && collectionId && linkId) { + // Load the image using Jimp + await Jimp.read(buffer, async (err, image) => { + if (image && !err) { + image?.resize(1280, Jimp.AUTO).quality(20); + const processedBuffer = await image?.getBufferAsync(Jimp.MIME_JPEG); + + createFile({ + data: processedBuffer, + filePath: `archives/preview/${collectionId}/${linkId}.jpeg`, + }).then(() => { + return prisma.link.update({ + where: { id: linkId }, + data: { + preview: `archives/preview/${collectionId}/${linkId}.jpeg`, + }, + }); + }); + } + }).catch((err) => { + console.error("Error processing the image:", err); + }); + } +}; + +export default generatePreview; diff --git a/lib/client/generateLinkHref.ts b/lib/client/generateLinkHref.ts index 47c1888..f012ab9 100644 --- a/lib/client/generateLinkHref.ts +++ b/lib/client/generateLinkHref.ts @@ -16,24 +16,30 @@ export const generateLinkHref = ( ): string => { // Return the links href based on the account's preference // If the user's preference is not available, return the original link - switch (account.linksRouteTo) { - case LinksRouteTo.ORIGINAL: - return link.url || ""; - case LinksRouteTo.PDF: - if (!pdfAvailable(link)) return link.url || ""; + if (account.linksRouteTo === LinksRouteTo.ORIGINAL && link.type === "url") { + return link.url || ""; + } else if (account.linksRouteTo === LinksRouteTo.PDF || link.type === "pdf") { + if (!pdfAvailable(link)) return link.url || ""; - return `/preserved/${link?.id}?format=${ArchivedFormat.pdf}`; - case LinksRouteTo.READABLE: - if (!readabilityAvailable(link)) return link.url || ""; + return `/preserved/${link?.id}?format=${ArchivedFormat.pdf}`; + } else if ( + account.linksRouteTo === LinksRouteTo.READABLE && + link.type === "url" + ) { + if (!readabilityAvailable(link)) return link.url || ""; - return `/preserved/${link?.id}?format=${ArchivedFormat.readability}`; - case LinksRouteTo.SCREENSHOT: - if (!screenshotAvailable(link)) return link.url || ""; + return `/preserved/${link?.id}?format=${ArchivedFormat.readability}`; + } else if ( + account.linksRouteTo === LinksRouteTo.SCREENSHOT || + link.type === "image" + ) { + console.log(link); + if (!screenshotAvailable(link)) return link.url || ""; - return `/preserved/${link?.id}?format=${ - link?.image?.endsWith("png") ? ArchivedFormat.png : ArchivedFormat.jpeg - }`; - default: - return link.url || ""; + return `/preserved/${link?.id}?format=${ + link?.image?.endsWith("png") ? ArchivedFormat.png : ArchivedFormat.jpeg + }`; + } else { + return link.url || ""; } }; diff --git a/package.json b/package.json index 4568588..6b03cf3 100644 --- a/package.json +++ b/package.json @@ -79,7 +79,7 @@ "nodemon": "^3.0.2", "postcss": "^8.4.26", "prettier": "3.1.1", - "prisma": "^5.1.0", + "prisma": "^4.16.2", "tailwindcss": "^3.3.3", "ts-node": "^10.9.2", "typescript": "4.9.4" diff --git a/pages/api/v1/archives/[linkId].ts b/pages/api/v1/archives/[linkId].ts index 5e5ada2..554d4b9 100644 --- a/pages/api/v1/archives/[linkId].ts +++ b/pages/api/v1/archives/[linkId].ts @@ -9,6 +9,8 @@ import formidable from "formidable"; import createFile from "@/lib/api/storage/createFile"; import fs from "fs"; import verifyToken from "@/lib/api/verifyToken"; +import Jimp from "jimp"; +import generatePreview from "@/lib/api/generatePreview"; export const config = { api: { @@ -124,6 +126,14 @@ export default async function Index(req: NextApiRequest, res: NextApiResponse) { where: { id: linkId }, }); + if (linkStillExists && files.file[0].mimetype?.includes("image")) { + generatePreview( + fileBuffer, + collectionPermissions?.id as number, + linkId + ); + } + if (linkStillExists) { await createFile({ filePath: `archives/${collectionPermissions?.id}/${ @@ -135,7 +145,15 @@ export default async function Index(req: NextApiRequest, res: NextApiResponse) { await prisma.link.update({ where: { id: linkId }, data: { - image: `archives/${collectionPermissions?.id}/${linkId + suffix}`, + preview: files.file[0].mimetype?.includes("pdf") + ? "unavailable" + : undefined, + image: files.file[0].mimetype?.includes("image") + ? `archives/${collectionPermissions?.id}/${linkId + suffix}` + : null, + pdf: files.file[0].mimetype?.includes("pdf") + ? `archives/${collectionPermissions?.id}/${linkId + suffix}` + : null, lastPreserved: new Date().toISOString(), }, }); diff --git a/store/links.ts b/store/links.ts index 408a3ee..c2c3a8a 100644 --- a/store/links.ts +++ b/store/links.ts @@ -1,5 +1,8 @@ import { create } from "zustand"; -import { LinkIncludingShortenedCollectionAndTags } from "@/types/global"; +import { + ArchivedFormat, + LinkIncludingShortenedCollectionAndTags, +} from "@/types/global"; import useTagStore from "./tags"; import useCollectionStore from "./collections"; @@ -19,6 +22,10 @@ type LinkStore = { addLink: ( body: LinkIncludingShortenedCollectionAndTags ) => Promise; + uploadFile: ( + link: LinkIncludingShortenedCollectionAndTags, + file: File + ) => Promise; getLink: (linkId: number, publicRoute?: boolean) => Promise; updateLink: ( link: LinkIncludingShortenedCollectionAndTags @@ -79,6 +86,82 @@ const useLinkStore = create()((set) => ({ return { ok: response.ok, data: data.response }; }, + uploadFile: async (link, file) => { + let fileType: ArchivedFormat | null = null; + let linkType: "url" | "image" | "pdf" | null = null; + + if (file?.type === "image/jpg" || file.type === "image/jpeg") { + fileType = ArchivedFormat.jpeg; + linkType = "image"; + } else if (file.type === "image/png") { + fileType = ArchivedFormat.png; + linkType = "image"; + } else if (file.type === "application/pdf") { + fileType = ArchivedFormat.pdf; + linkType = "pdf"; + } else { + return { ok: false, data: "Invalid file type." }; + } + + const response = await fetch("/api/v1/links", { + body: JSON.stringify({ + ...link, + type: linkType, + name: link.name ? link.name : file.name, + }), + headers: { + "Content-Type": "application/json", + }, + method: "POST", + }); + + const data = await response.json(); + + const createdLink: LinkIncludingShortenedCollectionAndTags = data.response; + + console.log(data); + + if (response.ok) { + const formBody = new FormData(); + file && formBody.append("file", file); + + await fetch( + `/api/v1/archives/${(data as any).response.id}?format=${fileType}`, + { + body: formBody, + method: "POST", + } + ); + + // get file extension + const extension = file.name.split(".").pop() || ""; + + set((state) => ({ + links: [ + { + ...createdLink, + image: + linkType === "image" + ? `archives/${createdLink.collectionId}/${ + createdLink.id + extension + }` + : null, + pdf: + linkType === "pdf" + ? `archives/${createdLink.collectionId}/${ + createdLink.id + ".pdf" + }` + : null, + }, + ...state.links, + ], + })); + useTagStore.getState().setTags(); + useCollectionStore.getState().setCollections(); + } + + return { ok: response.ok, data: data.response }; + }, getLink: async (linkId, publicRoute) => { const path = publicRoute ? `/api/v1/public/links/${linkId}` diff --git a/yarn.lock b/yarn.lock index 37c6839..d016ae9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1301,10 +1301,10 @@ resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-4.16.1-1.4bc8b6e1b66cb932731fb1bdbbc550d1e010de81.tgz#d3b5dcf95b6d220e258cbf6ae19b06d30a7e9f14" integrity sha512-q617EUWfRIDTriWADZ4YiWRZXCa/WuhNgLTVd+HqWLffjMSPzyM5uOWoauX91wvQClSKZU4pzI4JJLQ9Kl62Qg== -"@prisma/engines@5.1.0": - version "5.1.0" - resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-5.1.0.tgz#4ccf7f344eaeee08ca1e4a1bb2dc14e36ff1d5ec" - integrity sha512-HqaFsnPmZOdMWkPq6tT2eTVTQyaAXEDdKszcZ4yc7DGMBIYRP6j/zAJTtZUG9SsMV8FaucdL5vRyxY/p5Ni28g== +"@prisma/engines@4.16.2": + version "4.16.2" + resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-4.16.2.tgz#5ec8dd672c2173d597e469194916ad4826ce2e5f" + integrity sha512-vx1nxVvN4QeT/cepQce68deh/Turxy5Mr+4L4zClFuK1GlxN3+ivxfuv+ej/gvidWn1cE1uAhW7ALLNlYbRUAw== "@radix-ui/primitive@1.0.1": version "1.0.1" @@ -5038,12 +5038,12 @@ pretty-format@^3.8.0: resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-3.8.0.tgz#bfbed56d5e9a776645f4b1ff7aa1a3ac4fa3c385" integrity sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew== -prisma@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/prisma/-/prisma-5.1.0.tgz#29e316b54844f5694a83017a9781a6d6f7cb99ea" - integrity sha512-wkXvh+6wxk03G8qwpZMOed4Y3j+EQ+bMTlvbDZHeal6k1E8QuGKzRO7DRXlE1NV0WNgOAas8kwZqcLETQ2+BiQ== +prisma@^4.16.2: + version "4.16.2" + resolved "https://registry.yarnpkg.com/prisma/-/prisma-4.16.2.tgz#469e0a0991c6ae5bcde289401726bb012253339e" + integrity sha512-SYCsBvDf0/7XSJyf2cHTLjLeTLVXYfqp7pG5eEVafFLeT0u/hLFz/9W196nDRGUOo1JfPatAEb+uEnTQImQC1g== dependencies: - "@prisma/engines" "5.1.0" + "@prisma/engines" "4.16.2" process@^0.11.10: version "0.11.10"