progressed file uploads feature (almost done!)
This commit is contained in:
parent
c659711181
commit
e67fef1d04
|
@ -19,6 +19,7 @@ import { generateLinkHref } from "@/lib/client/generateLinkHref";
|
||||||
import useAccountStore from "@/store/account";
|
import useAccountStore from "@/store/account";
|
||||||
import usePermissions from "@/hooks/usePermissions";
|
import usePermissions from "@/hooks/usePermissions";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
|
import LinkTypeBadge from "./LinkComponents/LinkTypeBadge";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
link: LinkIncludingShortenedCollectionAndTags;
|
link: LinkIncludingShortenedCollectionAndTags;
|
||||||
|
@ -53,7 +54,9 @@ export default function LinkCard({ link, flipDropdown, editMode }: Props) {
|
||||||
let shortendURL;
|
let shortendURL;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
shortendURL = new URL(link.url || "").host.toLowerCase();
|
if (link.url) {
|
||||||
|
shortendURL = new URL(link.url).host.toLowerCase();
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
}
|
}
|
||||||
|
@ -109,7 +112,6 @@ export default function LinkCard({ link, flipDropdown, editMode }: Props) {
|
||||||
editMode &&
|
editMode &&
|
||||||
(permissions === true || permissions?.canCreate || permissions?.canDelete);
|
(permissions === true || permissions?.canCreate || permissions?.canDelete);
|
||||||
|
|
||||||
// window.open ('www.yourdomain.com', '_ blank');
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
|
@ -162,18 +164,7 @@ export default function LinkCard({ link, flipDropdown, editMode }: Props) {
|
||||||
{unescapeString(link.name || link.description) || link.url}
|
{unescapeString(link.name || link.description) || link.url}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<Link
|
<LinkTypeBadge link={link} />
|
||||||
href={link.url || ""}
|
|
||||||
target="_blank"
|
|
||||||
title={link.url || ""}
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
}}
|
|
||||||
className="flex gap-1 item-center select-none text-neutral mt-1 hover:opacity-70 duration-100"
|
|
||||||
>
|
|
||||||
<i className="bi-link-45deg text-lg mt-[0.10rem] leading-none"></i>
|
|
||||||
<p className="text-sm truncate">{shortendURL}</p>
|
|
||||||
</Link>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr className="divider mt-2 mb-1 last:hidden border-t border-neutral-content h-[1px]" />
|
<hr className="divider mt-2 mb-1 last:hidden border-t border-neutral-content h-[1px]" />
|
||||||
|
|
|
@ -122,6 +122,7 @@ export default function LinkActions({
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
|
{link.type === "url" && (
|
||||||
<li>
|
<li>
|
||||||
<div
|
<div
|
||||||
role="button"
|
role="button"
|
||||||
|
@ -134,6 +135,7 @@ export default function LinkActions({
|
||||||
Preserved Formats
|
Preserved Formats
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
|
)}
|
||||||
{permissions === true || permissions?.canDelete ? (
|
{permissions === true || permissions?.canDelete ? (
|
||||||
<li>
|
<li>
|
||||||
<div
|
<div
|
||||||
|
|
|
@ -6,9 +6,11 @@ import React from "react";
|
||||||
export default function LinkIcon({
|
export default function LinkIcon({
|
||||||
link,
|
link,
|
||||||
width,
|
width,
|
||||||
|
className,
|
||||||
}: {
|
}: {
|
||||||
link: LinkIncludingShortenedCollectionAndTags;
|
link: LinkIncludingShortenedCollectionAndTags;
|
||||||
width?: string;
|
width?: string;
|
||||||
|
className?: string;
|
||||||
}) {
|
}) {
|
||||||
const url =
|
const url =
|
||||||
isValidUrl(link.url || "") && link.url ? new URL(link.url) : undefined;
|
isValidUrl(link.url || "") && link.url ? new URL(link.url) : undefined;
|
||||||
|
@ -16,13 +18,16 @@ export default function LinkIcon({
|
||||||
const iconClasses: string =
|
const iconClasses: string =
|
||||||
"bg-white shadow rounded-md border-[2px] flex item-center justify-center border-white select-none z-10" +
|
"bg-white shadow rounded-md border-[2px] flex item-center justify-center border-white select-none z-10" +
|
||||||
" " +
|
" " +
|
||||||
(width || "w-12");
|
(width || "w-12") +
|
||||||
|
" " +
|
||||||
|
(className || "");
|
||||||
|
|
||||||
const [showFavicon, setShowFavicon] = React.useState<boolean>(true);
|
const [showFavicon, setShowFavicon] = React.useState<boolean>(true);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{link.url && url && showFavicon ? (
|
{link.type === "url" && url ? (
|
||||||
|
showFavicon ? (
|
||||||
<Image
|
<Image
|
||||||
src={`https://t2.gstatic.com/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url=${link.url}&size=32`}
|
src={`https://t2.gstatic.com/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url=${link.url}&size=32`}
|
||||||
width={64}
|
width={64}
|
||||||
|
@ -34,15 +39,34 @@ export default function LinkIcon({
|
||||||
setShowFavicon(false);
|
setShowFavicon(false);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
) : showFavicon === false ? (
|
) : (
|
||||||
<div className={iconClasses}>
|
<LinkPlaceholderIcon iconClasses={iconClasses} icon="bi-link-45deg" />
|
||||||
<i className="bi-link-45deg text-4xl text-black"></i>
|
)
|
||||||
</div>
|
|
||||||
) : link.type === "pdf" ? (
|
) : link.type === "pdf" ? (
|
||||||
<i className={`bi-file-earmark-pdf ${iconClasses}`}></i>
|
<LinkPlaceholderIcon
|
||||||
|
iconClasses={iconClasses}
|
||||||
|
icon="bi-file-earmark-pdf"
|
||||||
|
/>
|
||||||
) : link.type === "image" ? (
|
) : link.type === "image" ? (
|
||||||
<i className={`bi-file-earmark-image ${iconClasses}`}></i>
|
<LinkPlaceholderIcon
|
||||||
|
iconClasses={iconClasses}
|
||||||
|
icon="bi-file-earmark-image"
|
||||||
|
/>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const LinkPlaceholderIcon = ({
|
||||||
|
iconClasses,
|
||||||
|
icon,
|
||||||
|
}: {
|
||||||
|
iconClasses: string;
|
||||||
|
icon: string;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div className={`text-4xl text-black aspect-square ${iconClasses}`}>
|
||||||
|
<i className={`${icon} m-auto`}></i>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
|
@ -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 ? (
|
||||||
|
<Link
|
||||||
|
href={link.url || ""}
|
||||||
|
target="_blank"
|
||||||
|
title={link.url || ""}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
className="flex gap-1 item-center select-none text-neutral mt-1 hover:opacity-70 duration-100"
|
||||||
|
>
|
||||||
|
<i className="bi-link-45deg text-lg mt-[0.1rem] leading-none"></i>
|
||||||
|
<p className="text-sm truncate">{shortendURL}</p>
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
<div className="badge badge-primary badge-sm my-1 select-none">
|
||||||
|
{link.type}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -16,6 +16,7 @@ import { generateLinkHref } from "@/lib/client/generateLinkHref";
|
||||||
import useAccountStore from "@/store/account";
|
import useAccountStore from "@/store/account";
|
||||||
import usePermissions from "@/hooks/usePermissions";
|
import usePermissions from "@/hooks/usePermissions";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
|
import LinkTypeBadge from "./LinkComponents/LinkTypeBadge";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
link: LinkIncludingShortenedCollectionAndTags;
|
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] =
|
const [collection, setCollection] =
|
||||||
useState<CollectionIncludingMembersAndLinkCount>(
|
useState<CollectionIncludingMembersAndLinkCount>(
|
||||||
collections.find(
|
collections.find(
|
||||||
|
@ -130,7 +123,11 @@ export default function LinkCardCompact({
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className="shrink-0">
|
<div className="shrink-0">
|
||||||
<LinkIcon link={link} width="sm:w-12 w-8 mt-1 sm:mt-0" />
|
<LinkIcon
|
||||||
|
link={link}
|
||||||
|
width="sm:w-12 w-8"
|
||||||
|
className="mt-1 sm:mt-0"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="w-[calc(100%-56px)] ml-2">
|
<div className="w-[calc(100%-56px)] ml-2">
|
||||||
|
@ -143,24 +140,7 @@ export default function LinkCardCompact({
|
||||||
{collection ? (
|
{collection ? (
|
||||||
<LinkCollection link={link} collection={collection} />
|
<LinkCollection link={link} collection={collection} />
|
||||||
) : undefined}
|
) : undefined}
|
||||||
{link.url ? (
|
<LinkTypeBadge link={link} />
|
||||||
<Link
|
|
||||||
href={link.url || ""}
|
|
||||||
target="_blank"
|
|
||||||
title={link.url || ""}
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
}}
|
|
||||||
className="flex gap-1 item-center select-none text-neutral mt-1 hover:opacity-70 duration-100"
|
|
||||||
>
|
|
||||||
<i className="bi-link-45deg text-lg mt-[0.1rem] leading-none"></i>
|
|
||||||
<p className="text-sm truncate">{shortendURL}</p>
|
|
||||||
</Link>
|
|
||||||
) : (
|
|
||||||
<div className="badge badge-primary badge-sm my-1 select-none">
|
|
||||||
{link.type}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<LinkDate link={link} />
|
<LinkDate link={link} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -43,7 +43,7 @@ export default function UploadFileModal({ onClose }: Props) {
|
||||||
|
|
||||||
const [file, setFile] = useState<File>();
|
const [file, setFile] = useState<File>();
|
||||||
|
|
||||||
const { addLink } = useLinkStore();
|
const { uploadFile } = useLinkStore();
|
||||||
const [submitLoader, setSubmitLoader] = useState(false);
|
const [submitLoader, setSubmitLoader] = useState(false);
|
||||||
|
|
||||||
const [optionsExpanded, setOptionsExpanded] = useState(false);
|
const [optionsExpanded, setOptionsExpanded] = useState(false);
|
||||||
|
@ -100,48 +100,15 @@ export default function UploadFileModal({ onClose }: Props) {
|
||||||
|
|
||||||
const submit = async () => {
|
const submit = async () => {
|
||||||
if (!submitLoader && file) {
|
if (!submitLoader && 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";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fileType !== null && linkType !== null) {
|
|
||||||
setSubmitLoader(true);
|
setSubmitLoader(true);
|
||||||
|
|
||||||
let response;
|
|
||||||
|
|
||||||
const load = toast.loading("Creating...");
|
const load = toast.loading("Creating...");
|
||||||
|
|
||||||
response = await addLink({
|
const response = await uploadFile(link, file);
|
||||||
...link,
|
|
||||||
type: linkType,
|
|
||||||
name: link.name ? link.name : file.name.replace(/\.[^/.]+$/, ""),
|
|
||||||
});
|
|
||||||
|
|
||||||
toast.dismiss(load);
|
toast.dismiss(load);
|
||||||
|
|
||||||
if (response.ok) {
|
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!`);
|
toast.success(`Created!`);
|
||||||
onClose();
|
onClose();
|
||||||
} else toast.error(response.data as string);
|
} else toast.error(response.data as string);
|
||||||
|
@ -150,7 +117,6 @@ export default function UploadFileModal({ onClose }: Props) {
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -238,7 +204,7 @@ export default function UploadFileModal({ onClose }: Props) {
|
||||||
className="btn btn-accent dark:border-violet-400 text-white"
|
className="btn btn-accent dark:border-violet-400 text-white"
|
||||||
onClick={submit}
|
onClick={submit}
|
||||||
>
|
>
|
||||||
Create Link
|
Upload File
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
|
@ -10,6 +10,7 @@ import validateUrlSize from "./validateUrlSize";
|
||||||
import removeFile from "./storage/removeFile";
|
import removeFile from "./storage/removeFile";
|
||||||
import Jimp from "jimp";
|
import Jimp from "jimp";
|
||||||
import createFolder from "./storage/createFolder";
|
import createFolder from "./storage/createFolder";
|
||||||
|
import generatePreview from "./generatePreview";
|
||||||
|
|
||||||
type LinksAndCollectionAndOwner = Link & {
|
type LinksAndCollectionAndOwner = Link & {
|
||||||
collection: Collection & {
|
collection: Collection & {
|
||||||
|
@ -175,35 +176,7 @@ export default async function archiveHandler(link: LinksAndCollectionAndOwner) {
|
||||||
// Check if imageResponse is not null
|
// Check if imageResponse is not null
|
||||||
if (imageResponse && !link.preview?.startsWith("archive")) {
|
if (imageResponse && !link.preview?.startsWith("archive")) {
|
||||||
const buffer = await imageResponse.body();
|
const buffer = await imageResponse.body();
|
||||||
|
await generatePreview(buffer, link.collectionId, link.id);
|
||||||
// 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 page.goBack();
|
await page.goBack();
|
||||||
|
|
|
@ -174,7 +174,7 @@ export default async function postLink(
|
||||||
|
|
||||||
const newLink = await prisma.link.create({
|
const newLink = await prisma.link.create({
|
||||||
data: {
|
data: {
|
||||||
url: link.url?.trim(),
|
url: link.url?.trim() || null,
|
||||||
name: link.name,
|
name: link.name,
|
||||||
description,
|
description,
|
||||||
type: linkType,
|
type: linkType,
|
||||||
|
|
|
@ -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;
|
|
@ -16,24 +16,30 @@ export const generateLinkHref = (
|
||||||
): string => {
|
): string => {
|
||||||
// Return the links href based on the account's preference
|
// Return the links href based on the account's preference
|
||||||
// If the user's preference is not available, return the original link
|
// If the user's preference is not available, return the original link
|
||||||
switch (account.linksRouteTo) {
|
if (account.linksRouteTo === LinksRouteTo.ORIGINAL && link.type === "url") {
|
||||||
case LinksRouteTo.ORIGINAL:
|
|
||||||
return link.url || "";
|
return link.url || "";
|
||||||
case LinksRouteTo.PDF:
|
} else if (account.linksRouteTo === LinksRouteTo.PDF || link.type === "pdf") {
|
||||||
if (!pdfAvailable(link)) return link.url || "";
|
if (!pdfAvailable(link)) return link.url || "";
|
||||||
|
|
||||||
return `/preserved/${link?.id}?format=${ArchivedFormat.pdf}`;
|
return `/preserved/${link?.id}?format=${ArchivedFormat.pdf}`;
|
||||||
case LinksRouteTo.READABLE:
|
} else if (
|
||||||
|
account.linksRouteTo === LinksRouteTo.READABLE &&
|
||||||
|
link.type === "url"
|
||||||
|
) {
|
||||||
if (!readabilityAvailable(link)) return link.url || "";
|
if (!readabilityAvailable(link)) return link.url || "";
|
||||||
|
|
||||||
return `/preserved/${link?.id}?format=${ArchivedFormat.readability}`;
|
return `/preserved/${link?.id}?format=${ArchivedFormat.readability}`;
|
||||||
case LinksRouteTo.SCREENSHOT:
|
} else if (
|
||||||
|
account.linksRouteTo === LinksRouteTo.SCREENSHOT ||
|
||||||
|
link.type === "image"
|
||||||
|
) {
|
||||||
|
console.log(link);
|
||||||
if (!screenshotAvailable(link)) return link.url || "";
|
if (!screenshotAvailable(link)) return link.url || "";
|
||||||
|
|
||||||
return `/preserved/${link?.id}?format=${
|
return `/preserved/${link?.id}?format=${
|
||||||
link?.image?.endsWith("png") ? ArchivedFormat.png : ArchivedFormat.jpeg
|
link?.image?.endsWith("png") ? ArchivedFormat.png : ArchivedFormat.jpeg
|
||||||
}`;
|
}`;
|
||||||
default:
|
} else {
|
||||||
return link.url || "";
|
return link.url || "";
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -79,7 +79,7 @@
|
||||||
"nodemon": "^3.0.2",
|
"nodemon": "^3.0.2",
|
||||||
"postcss": "^8.4.26",
|
"postcss": "^8.4.26",
|
||||||
"prettier": "3.1.1",
|
"prettier": "3.1.1",
|
||||||
"prisma": "^5.1.0",
|
"prisma": "^4.16.2",
|
||||||
"tailwindcss": "^3.3.3",
|
"tailwindcss": "^3.3.3",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"typescript": "4.9.4"
|
"typescript": "4.9.4"
|
||||||
|
|
|
@ -9,6 +9,8 @@ import formidable from "formidable";
|
||||||
import createFile from "@/lib/api/storage/createFile";
|
import createFile from "@/lib/api/storage/createFile";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import verifyToken from "@/lib/api/verifyToken";
|
import verifyToken from "@/lib/api/verifyToken";
|
||||||
|
import Jimp from "jimp";
|
||||||
|
import generatePreview from "@/lib/api/generatePreview";
|
||||||
|
|
||||||
export const config = {
|
export const config = {
|
||||||
api: {
|
api: {
|
||||||
|
@ -124,6 +126,14 @@ export default async function Index(req: NextApiRequest, res: NextApiResponse) {
|
||||||
where: { id: linkId },
|
where: { id: linkId },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (linkStillExists && files.file[0].mimetype?.includes("image")) {
|
||||||
|
generatePreview(
|
||||||
|
fileBuffer,
|
||||||
|
collectionPermissions?.id as number,
|
||||||
|
linkId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (linkStillExists) {
|
if (linkStillExists) {
|
||||||
await createFile({
|
await createFile({
|
||||||
filePath: `archives/${collectionPermissions?.id}/${
|
filePath: `archives/${collectionPermissions?.id}/${
|
||||||
|
@ -135,7 +145,15 @@ export default async function Index(req: NextApiRequest, res: NextApiResponse) {
|
||||||
await prisma.link.update({
|
await prisma.link.update({
|
||||||
where: { id: linkId },
|
where: { id: linkId },
|
||||||
data: {
|
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(),
|
lastPreserved: new Date().toISOString(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
import { create } from "zustand";
|
import { create } from "zustand";
|
||||||
import { LinkIncludingShortenedCollectionAndTags } from "@/types/global";
|
import {
|
||||||
|
ArchivedFormat,
|
||||||
|
LinkIncludingShortenedCollectionAndTags,
|
||||||
|
} from "@/types/global";
|
||||||
import useTagStore from "./tags";
|
import useTagStore from "./tags";
|
||||||
import useCollectionStore from "./collections";
|
import useCollectionStore from "./collections";
|
||||||
|
|
||||||
|
@ -19,6 +22,10 @@ type LinkStore = {
|
||||||
addLink: (
|
addLink: (
|
||||||
body: LinkIncludingShortenedCollectionAndTags
|
body: LinkIncludingShortenedCollectionAndTags
|
||||||
) => Promise<ResponseObject>;
|
) => Promise<ResponseObject>;
|
||||||
|
uploadFile: (
|
||||||
|
link: LinkIncludingShortenedCollectionAndTags,
|
||||||
|
file: File
|
||||||
|
) => Promise<ResponseObject>;
|
||||||
getLink: (linkId: number, publicRoute?: boolean) => Promise<ResponseObject>;
|
getLink: (linkId: number, publicRoute?: boolean) => Promise<ResponseObject>;
|
||||||
updateLink: (
|
updateLink: (
|
||||||
link: LinkIncludingShortenedCollectionAndTags
|
link: LinkIncludingShortenedCollectionAndTags
|
||||||
|
@ -79,6 +86,82 @@ const useLinkStore = create<LinkStore>()((set) => ({
|
||||||
|
|
||||||
return { ok: response.ok, data: data.response };
|
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) => {
|
getLink: async (linkId, publicRoute) => {
|
||||||
const path = publicRoute
|
const path = publicRoute
|
||||||
? `/api/v1/public/links/${linkId}`
|
? `/api/v1/public/links/${linkId}`
|
||||||
|
|
18
yarn.lock
18
yarn.lock
|
@ -1301,10 +1301,10 @@
|
||||||
resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-4.16.1-1.4bc8b6e1b66cb932731fb1bdbbc550d1e010de81.tgz#d3b5dcf95b6d220e258cbf6ae19b06d30a7e9f14"
|
resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-4.16.1-1.4bc8b6e1b66cb932731fb1bdbbc550d1e010de81.tgz#d3b5dcf95b6d220e258cbf6ae19b06d30a7e9f14"
|
||||||
integrity sha512-q617EUWfRIDTriWADZ4YiWRZXCa/WuhNgLTVd+HqWLffjMSPzyM5uOWoauX91wvQClSKZU4pzI4JJLQ9Kl62Qg==
|
integrity sha512-q617EUWfRIDTriWADZ4YiWRZXCa/WuhNgLTVd+HqWLffjMSPzyM5uOWoauX91wvQClSKZU4pzI4JJLQ9Kl62Qg==
|
||||||
|
|
||||||
"@prisma/engines@5.1.0":
|
"@prisma/engines@4.16.2":
|
||||||
version "5.1.0"
|
version "4.16.2"
|
||||||
resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-5.1.0.tgz#4ccf7f344eaeee08ca1e4a1bb2dc14e36ff1d5ec"
|
resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-4.16.2.tgz#5ec8dd672c2173d597e469194916ad4826ce2e5f"
|
||||||
integrity sha512-HqaFsnPmZOdMWkPq6tT2eTVTQyaAXEDdKszcZ4yc7DGMBIYRP6j/zAJTtZUG9SsMV8FaucdL5vRyxY/p5Ni28g==
|
integrity sha512-vx1nxVvN4QeT/cepQce68deh/Turxy5Mr+4L4zClFuK1GlxN3+ivxfuv+ej/gvidWn1cE1uAhW7ALLNlYbRUAw==
|
||||||
|
|
||||||
"@radix-ui/primitive@1.0.1":
|
"@radix-ui/primitive@1.0.1":
|
||||||
version "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"
|
resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-3.8.0.tgz#bfbed56d5e9a776645f4b1ff7aa1a3ac4fa3c385"
|
||||||
integrity sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==
|
integrity sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==
|
||||||
|
|
||||||
prisma@^5.1.0:
|
prisma@^4.16.2:
|
||||||
version "5.1.0"
|
version "4.16.2"
|
||||||
resolved "https://registry.yarnpkg.com/prisma/-/prisma-5.1.0.tgz#29e316b54844f5694a83017a9781a6d6f7cb99ea"
|
resolved "https://registry.yarnpkg.com/prisma/-/prisma-4.16.2.tgz#469e0a0991c6ae5bcde289401726bb012253339e"
|
||||||
integrity sha512-wkXvh+6wxk03G8qwpZMOed4Y3j+EQ+bMTlvbDZHeal6k1E8QuGKzRO7DRXlE1NV0WNgOAas8kwZqcLETQ2+BiQ==
|
integrity sha512-SYCsBvDf0/7XSJyf2cHTLjLeTLVXYfqp7pG5eEVafFLeT0u/hLFz/9W196nDRGUOo1JfPatAEb+uEnTQImQC1g==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@prisma/engines" "5.1.0"
|
"@prisma/engines" "4.16.2"
|
||||||
|
|
||||||
process@^0.11.10:
|
process@^0.11.10:
|
||||||
version "0.11.10"
|
version "0.11.10"
|
||||||
|
|
Ŝarĝante…
Reference in New Issue