many bug fixes and improvements
This commit is contained in:
parent
b65787358f
commit
55c43d6f9e
|
@ -16,6 +16,7 @@ NEXT_PUBLIC_CREDENTIALS_ENABLED=
|
||||||
DISABLE_NEW_SSO_USERS=
|
DISABLE_NEW_SSO_USERS=
|
||||||
RE_ARCHIVE_LIMIT=
|
RE_ARCHIVE_LIMIT=
|
||||||
NEXT_PUBLIC_MAX_FILE_SIZE=
|
NEXT_PUBLIC_MAX_FILE_SIZE=
|
||||||
|
MAX_LINKS_PER_USER=
|
||||||
|
|
||||||
# AWS S3 Settings
|
# AWS S3 Settings
|
||||||
SPACES_KEY=
|
SPACES_KEY=
|
||||||
|
|
|
@ -15,7 +15,7 @@ type Props = {
|
||||||
link: LinkIncludingShortenedCollectionAndTags;
|
link: LinkIncludingShortenedCollectionAndTags;
|
||||||
collection: CollectionIncludingMembersAndLinkCount;
|
collection: CollectionIncludingMembersAndLinkCount;
|
||||||
position?: string;
|
position?: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
export default function LinkActions({ link, collection, position }: Props) {
|
export default function LinkActions({ link, collection, position }: Props) {
|
||||||
const permissions = usePermissions(link.collection.id as number);
|
const permissions = usePermissions(link.collection.id as number);
|
||||||
|
@ -23,7 +23,6 @@ export default function LinkActions({ link, collection, position }: Props) {
|
||||||
const [editLinkModal, setEditLinkModal] = useState(false);
|
const [editLinkModal, setEditLinkModal] = useState(false);
|
||||||
const [deleteLinkModal, setDeleteLinkModal] = useState(false);
|
const [deleteLinkModal, setDeleteLinkModal] = useState(false);
|
||||||
const [preservedFormatsModal, setPreservedFormatsModal] = useState(false);
|
const [preservedFormatsModal, setPreservedFormatsModal] = useState(false);
|
||||||
const [expandedLink, setExpandedLink] = useState(false);
|
|
||||||
|
|
||||||
const { account } = useAccountStore();
|
const { account } = useAccountStore();
|
||||||
|
|
||||||
|
@ -42,7 +41,7 @@ export default function LinkActions({ link, collection, position }: Props) {
|
||||||
toast.dismiss(load);
|
toast.dismiss(load);
|
||||||
|
|
||||||
response.ok &&
|
response.ok &&
|
||||||
toast.success(`Link ${isAlreadyPinned ? "Unpinned!" : "Pinned!"}`);
|
toast.success(`Link ${isAlreadyPinned ? "Unpinned!" : "Pinned!"}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteLink = async () => {
|
const deleteLink = async () => {
|
||||||
|
@ -57,85 +56,82 @@ export default function LinkActions({ link, collection, position }: Props) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{permissions === true ||
|
<div
|
||||||
permissions?.canUpdate ||
|
className={`dropdown dropdown-left absolute ${
|
||||||
permissions?.canDelete ? (
|
position || "top-3 right-3"
|
||||||
|
} z-20`}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
className={`dropdown dropdown-left absolute ${
|
tabIndex={0}
|
||||||
position || "top-3 right-3"
|
role="button"
|
||||||
} z-20`}
|
className="btn btn-ghost btn-sm btn-square text-neutral"
|
||||||
>
|
>
|
||||||
<div
|
<i
|
||||||
tabIndex={0}
|
id={"expand-dropdown" + collection.id}
|
||||||
role="button"
|
title="More"
|
||||||
className="btn btn-ghost btn-sm btn-square text-neutral"
|
className="bi-three-dots text-xl"
|
||||||
>
|
/>
|
||||||
<i id={"expand-dropdown" + collection.id} title="More" className="bi-three-dots text-xl"/>
|
|
||||||
</div>
|
|
||||||
<ul
|
|
||||||
className="dropdown-content z-[20] menu shadow bg-base-200 border border-neutral-content rounded-box w-44 mr-1">
|
|
||||||
{permissions === true ? (
|
|
||||||
<li>
|
|
||||||
<div
|
|
||||||
role="button"
|
|
||||||
tabIndex={0}
|
|
||||||
onClick={() => {
|
|
||||||
(document?.activeElement as HTMLElement)?.blur();
|
|
||||||
pinLink();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{link?.pinnedBy && link.pinnedBy[0]
|
|
||||||
? "Unpin"
|
|
||||||
: "Pin to Dashboard"}
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
) : undefined}
|
|
||||||
{permissions === true || permissions?.canUpdate ? (
|
|
||||||
<li>
|
|
||||||
<div
|
|
||||||
role="button"
|
|
||||||
tabIndex={0}
|
|
||||||
onClick={() => {
|
|
||||||
(document?.activeElement as HTMLElement)?.blur();
|
|
||||||
setEditLinkModal(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Edit
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
) : undefined}
|
|
||||||
{permissions === true ? (
|
|
||||||
<li>
|
|
||||||
<div
|
|
||||||
role="button"
|
|
||||||
tabIndex={0}
|
|
||||||
onClick={() => {
|
|
||||||
(document?.activeElement as HTMLElement)?.blur();
|
|
||||||
setPreservedFormatsModal(true);
|
|
||||||
// updateArchive();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Preserved Formats
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
) : undefined}
|
|
||||||
{permissions === true || permissions?.canDelete ? (
|
|
||||||
<li>
|
|
||||||
<div
|
|
||||||
role="button"
|
|
||||||
tabIndex={0}
|
|
||||||
onClick={(e) => {
|
|
||||||
(document?.activeElement as HTMLElement)?.blur();
|
|
||||||
e.shiftKey ? deleteLink() : setDeleteLinkModal(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Delete
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
) : undefined}
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
) : undefined}
|
<ul className="dropdown-content z-[20] menu shadow bg-base-200 border border-neutral-content rounded-box w-44 mr-1">
|
||||||
|
{permissions === true ? (
|
||||||
|
<li>
|
||||||
|
<div
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
onClick={() => {
|
||||||
|
(document?.activeElement as HTMLElement)?.blur();
|
||||||
|
pinLink();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{link?.pinnedBy && link.pinnedBy[0]
|
||||||
|
? "Unpin"
|
||||||
|
: "Pin to Dashboard"}
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
) : undefined}
|
||||||
|
{permissions === true || permissions?.canUpdate ? (
|
||||||
|
<li>
|
||||||
|
<div
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
onClick={() => {
|
||||||
|
(document?.activeElement as HTMLElement)?.blur();
|
||||||
|
setEditLinkModal(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Edit
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
) : undefined}
|
||||||
|
<li>
|
||||||
|
<div
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
onClick={() => {
|
||||||
|
(document?.activeElement as HTMLElement)?.blur();
|
||||||
|
setPreservedFormatsModal(true);
|
||||||
|
// updateArchive();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Preserved Formats
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
{permissions === true || permissions?.canDelete ? (
|
||||||
|
<li>
|
||||||
|
<div
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
onClick={(e) => {
|
||||||
|
(document?.activeElement as HTMLElement)?.blur();
|
||||||
|
e.shiftKey ? deleteLink() : setDeleteLinkModal(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
) : undefined}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
{editLinkModal ? (
|
{editLinkModal ? (
|
||||||
<EditLinkModal
|
<EditLinkModal
|
||||||
|
|
|
@ -34,6 +34,8 @@ export default function PreservedFormatsModal({ onClose, activeLink }: Props) {
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
let isPublic = router.pathname.startsWith("/public") ? true : undefined;
|
||||||
|
|
||||||
const [collectionOwner, setCollectionOwner] = useState({
|
const [collectionOwner, setCollectionOwner] = useState({
|
||||||
id: null as unknown as number,
|
id: null as unknown as number,
|
||||||
name: "",
|
name: "",
|
||||||
|
@ -78,12 +80,8 @@ export default function PreservedFormatsModal({ onClose, activeLink }: Props) {
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let isPublicRoute = router.pathname.startsWith("/public")
|
|
||||||
? true
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
const data = await getLink(link.id as number, isPublicRoute);
|
const data = await getLink(link.id as number, isPublic);
|
||||||
setLink(
|
setLink(
|
||||||
(data as any).response as LinkIncludingShortenedCollectionAndTags
|
(data as any).response as LinkIncludingShortenedCollectionAndTags
|
||||||
);
|
);
|
||||||
|
@ -93,7 +91,7 @@ export default function PreservedFormatsModal({ onClose, activeLink }: Props) {
|
||||||
|
|
||||||
if (!isReady()) {
|
if (!isReady()) {
|
||||||
interval = setInterval(async () => {
|
interval = setInterval(async () => {
|
||||||
const data = await getLink(link.id as number, isPublicRoute);
|
const data = await getLink(link.id as number, isPublic);
|
||||||
setLink(
|
setLink(
|
||||||
(data as any).response as LinkIncludingShortenedCollectionAndTags
|
(data as any).response as LinkIncludingShortenedCollectionAndTags
|
||||||
);
|
);
|
||||||
|
@ -123,8 +121,11 @@ export default function PreservedFormatsModal({ onClose, activeLink }: Props) {
|
||||||
toast.dismiss(load);
|
toast.dismiss(load);
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
|
const newLink = await getLink(link?.id as number);
|
||||||
|
setLink(
|
||||||
|
(newLink as any).response as LinkIncludingShortenedCollectionAndTags
|
||||||
|
);
|
||||||
toast.success(`Link is being archived...`);
|
toast.success(`Link is being archived...`);
|
||||||
await getLink(link?.id as number);
|
|
||||||
} else toast.error(data.response);
|
} else toast.error(data.response);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -148,20 +149,15 @@ export default function PreservedFormatsModal({ onClose, activeLink }: Props) {
|
||||||
<div className={`flex flex-col gap-3`}>
|
<div className={`flex flex-col gap-3`}>
|
||||||
{isReady() ? (
|
{isReady() ? (
|
||||||
<>
|
<>
|
||||||
{readabilityAvailable(link) ? (
|
|
||||||
<PreservedFormatRow
|
|
||||||
name={"Readable"}
|
|
||||||
icon={"bi-file-earmark-text"}
|
|
||||||
format={ArchivedFormat.readability}
|
|
||||||
activeLink={link}
|
|
||||||
/>
|
|
||||||
) : undefined}
|
|
||||||
|
|
||||||
{screenshotAvailable(link) ? (
|
{screenshotAvailable(link) ? (
|
||||||
<PreservedFormatRow
|
<PreservedFormatRow
|
||||||
name={"Screenshot"}
|
name={"Screenshot"}
|
||||||
icon={"bi-file-earmark-image"}
|
icon={"bi-file-earmark-image"}
|
||||||
format={ArchivedFormat.png}
|
format={
|
||||||
|
link?.screenshotPath?.endsWith("png")
|
||||||
|
? ArchivedFormat.png
|
||||||
|
: ArchivedFormat.jpeg
|
||||||
|
}
|
||||||
activeLink={link}
|
activeLink={link}
|
||||||
downloadable={true}
|
downloadable={true}
|
||||||
/>
|
/>
|
||||||
|
@ -176,6 +172,15 @@ export default function PreservedFormatsModal({ onClose, activeLink }: Props) {
|
||||||
downloadable={true}
|
downloadable={true}
|
||||||
/>
|
/>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
|
|
||||||
|
{readabilityAvailable(link) ? (
|
||||||
|
<PreservedFormatRow
|
||||||
|
name={"Readable"}
|
||||||
|
icon={"bi-file-earmark-text"}
|
||||||
|
format={ArchivedFormat.readability}
|
||||||
|
activeLink={link}
|
||||||
|
/>
|
||||||
|
) : undefined}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div
|
<div
|
||||||
|
@ -183,7 +188,7 @@ export default function PreservedFormatsModal({ onClose, activeLink }: Props) {
|
||||||
>
|
>
|
||||||
<i className="bi-stack drop-shadow text-primary text-8xl mx-auto mb-5"></i>
|
<i className="bi-stack drop-shadow text-primary text-8xl mx-auto mb-5"></i>
|
||||||
<p className="text-center text-2xl">
|
<p className="text-center text-2xl">
|
||||||
The Link preservation is in the queue
|
Link preservation is in the queue
|
||||||
</p>
|
</p>
|
||||||
<p className="text-center text-lg">
|
<p className="text-center text-lg">
|
||||||
Please check back later to see the result
|
Please check back later to see the result
|
||||||
|
|
|
@ -32,13 +32,11 @@ export default function PreservedFormatRow({
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
useEffect(() => {
|
let isPublic = router.pathname.startsWith("/public") ? true : undefined;
|
||||||
let isPublicRoute = router.pathname.startsWith("/public")
|
|
||||||
? true
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
const data = await getLink(link.id as number, isPublicRoute);
|
const data = await getLink(link.id as number, isPublic);
|
||||||
setLink(
|
setLink(
|
||||||
(data as any).response as LinkIncludingShortenedCollectionAndTags
|
(data as any).response as LinkIncludingShortenedCollectionAndTags
|
||||||
);
|
);
|
||||||
|
@ -47,7 +45,7 @@ export default function PreservedFormatRow({
|
||||||
let interval: any;
|
let interval: any;
|
||||||
if (link?.screenshotPath === "pending" || link?.pdfPath === "pending") {
|
if (link?.screenshotPath === "pending" || link?.pdfPath === "pending") {
|
||||||
interval = setInterval(async () => {
|
interval = setInterval(async () => {
|
||||||
const data = await getLink(link.id as number, isPublicRoute);
|
const data = await getLink(link.id as number, isPublic);
|
||||||
setLink(
|
setLink(
|
||||||
(data as any).response as LinkIncludingShortenedCollectionAndTags
|
(data as any).response as LinkIncludingShortenedCollectionAndTags
|
||||||
);
|
);
|
||||||
|
@ -65,23 +63,6 @@ export default function PreservedFormatRow({
|
||||||
};
|
};
|
||||||
}, [link?.screenshotPath, link?.pdfPath, link?.readabilityPath]);
|
}, [link?.screenshotPath, link?.pdfPath, link?.readabilityPath]);
|
||||||
|
|
||||||
const updateArchive = async () => {
|
|
||||||
const load = toast.loading("Sending request...");
|
|
||||||
|
|
||||||
const response = await fetch(`/api/v1/links/${link?.id}/archive`, {
|
|
||||||
method: "PUT",
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
toast.dismiss(load);
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
toast.success(`Link is being archived...`);
|
|
||||||
getLink(link?.id as number);
|
|
||||||
} else toast.error(data.response);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDownload = () => {
|
const handleDownload = () => {
|
||||||
const path = `/api/v1/archives/${link?.id}?format=${format}`;
|
const path = `/api/v1/archives/${link?.id}?format=${format}`;
|
||||||
fetch(path)
|
fetch(path)
|
||||||
|
@ -121,7 +102,9 @@ export default function PreservedFormatRow({
|
||||||
) : undefined}
|
) : undefined}
|
||||||
|
|
||||||
<Link
|
<Link
|
||||||
href={`/preserved/${link?.id}?format=${format}`}
|
href={`${isPublic ? "/public" : ""}/preserved/${
|
||||||
|
link?.id
|
||||||
|
}?format=${format}`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
className="btn btn-sm btn-square"
|
className="btn btn-sm btn-square"
|
||||||
>
|
>
|
||||||
|
|
|
@ -4,6 +4,8 @@ import isValidUrl from "@/lib/shared/isValidUrl";
|
||||||
import unescapeString from "@/lib/client/unescapeString";
|
import unescapeString from "@/lib/client/unescapeString";
|
||||||
import { TagIncludingLinkCount } from "@/types/global";
|
import { TagIncludingLinkCount } from "@/types/global";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import { useState } from "react";
|
||||||
|
import PreservedFormatsModal from "../ModalContent/PreservedFormatsModal";
|
||||||
|
|
||||||
interface LinksIncludingTags extends LinkType {
|
interface LinksIncludingTags extends LinkType {
|
||||||
tags: TagIncludingLinkCount[];
|
tags: TagIncludingLinkCount[];
|
||||||
|
@ -25,6 +27,8 @@ export default function LinkCard({ link, count }: Props) {
|
||||||
day: "numeric",
|
day: "numeric",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const [preservedFormatsModal, setPreservedFormatsModal] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="border border-solid border-neutral-content bg-base-200 shadow hover:shadow-none duration-100 rounded-lg p-3 flex items-start relative gap-3 group/item">
|
<div className="border border-solid border-neutral-content bg-base-200 shadow hover:shadow-none duration-100 rounded-lg p-3 flex items-start relative gap-3 group/item">
|
||||||
<div className="flex justify-between items-end gap-5 w-full h-full z-0">
|
<div className="flex justify-between items-end gap-5 w-full h-full z-0">
|
||||||
|
@ -77,15 +81,52 @@ export default function LinkCard({ link, count }: Props) {
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
{unescapeString(link.description)}{" "}
|
{unescapeString(link.description)}{" "}
|
||||||
<Link
|
<Link
|
||||||
href={`/public/links/${link.id}`}
|
href={link.url || ""}
|
||||||
|
target="_blank"
|
||||||
className="flex gap-1 items-center flex-wrap text-sm text-neutral hover:opacity-50 duration-100 min-w-fit float-right mt-1 ml-2"
|
className="flex gap-1 items-center flex-wrap text-sm text-neutral hover:opacity-50 duration-100 min-w-fit float-right mt-1 ml-2"
|
||||||
>
|
>
|
||||||
<p>Read</p>
|
<p>Visit</p>
|
||||||
<i className={"bi-chevron-right"}></i>
|
<i className={"bi-chevron-right"}></i>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
className={`dropdown dropdown-left absolute ${"top-3 right-3"} z-20`}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
tabIndex={0}
|
||||||
|
role="button"
|
||||||
|
className="btn btn-ghost btn-sm btn-square text-neutral"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
id={"expand-dropdown" + link.id}
|
||||||
|
title="More"
|
||||||
|
className="bi-three-dots text-xl"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<ul className="dropdown-content z-[20] menu shadow bg-base-200 border border-neutral-content rounded-box w-44 mr-1">
|
||||||
|
<li>
|
||||||
|
<div
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
onClick={() => {
|
||||||
|
(document?.activeElement as HTMLElement)?.blur();
|
||||||
|
setPreservedFormatsModal(true);
|
||||||
|
// updateArchive();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Preserved Formats
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{preservedFormatsModal ? (
|
||||||
|
<PreservedFormatsModal
|
||||||
|
onClose={() => setPreservedFormatsModal(false)}
|
||||||
|
activeLink={link as any}
|
||||||
|
/>
|
||||||
|
) : undefined}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ import { JSDOM } from "jsdom";
|
||||||
import DOMPurify from "dompurify";
|
import DOMPurify from "dompurify";
|
||||||
import { Collection, Link, User } from "@prisma/client";
|
import { Collection, Link, User } from "@prisma/client";
|
||||||
import validateUrlSize from "./validateUrlSize";
|
import validateUrlSize from "./validateUrlSize";
|
||||||
|
import removeFile from "./storage/removeFile";
|
||||||
|
|
||||||
type LinksAndCollectionAndOwner = Link & {
|
type LinksAndCollectionAndOwner = Link & {
|
||||||
collection: Collection & {
|
collection: Collection & {
|
||||||
|
@ -74,7 +75,7 @@ export default async function archiveHandler(link: LinksAndCollectionAndOwner) {
|
||||||
|
|
||||||
const content = await page.content();
|
const content = await page.content();
|
||||||
|
|
||||||
// TODO Webarchive
|
// TODO single file
|
||||||
// const session = await page.context().newCDPSession(page);
|
// const session = await page.context().newCDPSession(page);
|
||||||
// const doc = await session.send("Page.captureSnapshot", {
|
// const doc = await session.send("Page.captureSnapshot", {
|
||||||
// format: "mhtml",
|
// format: "mhtml",
|
||||||
|
@ -189,6 +190,13 @@ export default async function archiveHandler(link: LinksAndCollectionAndOwner) {
|
||||||
: undefined,
|
: undefined,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
else {
|
||||||
|
removeFile({ filePath: `archives/${link.collectionId}/${link.id}.png` });
|
||||||
|
removeFile({ filePath: `archives/${link.collectionId}/${link.id}.pdf` });
|
||||||
|
removeFile({
|
||||||
|
filePath: `archives/${link.collectionId}/${link.id}_readability.json`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
await browser.close();
|
await browser.close();
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,8 @@ import getPermission from "@/lib/api/getPermission";
|
||||||
import createFolder from "@/lib/api/storage/createFolder";
|
import createFolder from "@/lib/api/storage/createFolder";
|
||||||
import validateUrlSize from "../../validateUrlSize";
|
import validateUrlSize from "../../validateUrlSize";
|
||||||
|
|
||||||
|
const MAX_LINKS_PER_USER = Number(process.env.MAX_LINKS_PER_USER) || 30000;
|
||||||
|
|
||||||
export default async function postLink(
|
export default async function postLink(
|
||||||
link: LinkIncludingShortenedCollectionAndTags,
|
link: LinkIncludingShortenedCollectionAndTags,
|
||||||
userId: number
|
userId: number
|
||||||
|
@ -24,6 +26,20 @@ export default async function postLink(
|
||||||
link.collection.name = "Unorganized";
|
link.collection.name = "Unorganized";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const numberOfLinksTheUserHas = await prisma.link.count({
|
||||||
|
where: {
|
||||||
|
collection: {
|
||||||
|
ownerId: userId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (numberOfLinksTheUserHas + 1 > MAX_LINKS_PER_USER)
|
||||||
|
return {
|
||||||
|
response: `Error: Each user can only have a maximum of ${MAX_LINKS_PER_USER} Links.`,
|
||||||
|
status: 400,
|
||||||
|
};
|
||||||
|
|
||||||
link.collection.name = link.collection.name.trim();
|
link.collection.name = link.collection.name.trim();
|
||||||
|
|
||||||
if (link.collection.id) {
|
if (link.collection.id) {
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import { prisma } from "@/lib/api/db";
|
import { prisma } from "@/lib/api/db";
|
||||||
import { Backup } from "@/types/global";
|
|
||||||
import createFolder from "@/lib/api/storage/createFolder";
|
import createFolder from "@/lib/api/storage/createFolder";
|
||||||
import { JSDOM } from "jsdom";
|
import { JSDOM } from "jsdom";
|
||||||
|
|
||||||
|
const MAX_LINKS_PER_USER = Number(process.env.MAX_LINKS_PER_USER) || 30000;
|
||||||
|
|
||||||
export default async function importFromHTMLFile(
|
export default async function importFromHTMLFile(
|
||||||
userId: number,
|
userId: number,
|
||||||
rawData: string
|
rawData: string
|
||||||
|
@ -10,6 +11,23 @@ export default async function importFromHTMLFile(
|
||||||
const dom = new JSDOM(rawData);
|
const dom = new JSDOM(rawData);
|
||||||
const document = dom.window.document;
|
const document = dom.window.document;
|
||||||
|
|
||||||
|
const bookmarks = document.querySelectorAll("A");
|
||||||
|
const totalImports = bookmarks.length;
|
||||||
|
|
||||||
|
const numberOfLinksTheUserHas = await prisma.link.count({
|
||||||
|
where: {
|
||||||
|
collection: {
|
||||||
|
ownerId: userId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (totalImports + numberOfLinksTheUserHas > MAX_LINKS_PER_USER)
|
||||||
|
return {
|
||||||
|
response: `Error: Each user can only have a maximum of ${MAX_LINKS_PER_USER} Links.`,
|
||||||
|
status: 400,
|
||||||
|
};
|
||||||
|
|
||||||
const folders = document.querySelectorAll("H3");
|
const folders = document.querySelectorAll("H3");
|
||||||
|
|
||||||
await prisma
|
await prisma
|
||||||
|
|
|
@ -2,9 +2,34 @@ import { prisma } from "@/lib/api/db";
|
||||||
import { Backup } from "@/types/global";
|
import { Backup } from "@/types/global";
|
||||||
import createFolder from "@/lib/api/storage/createFolder";
|
import createFolder from "@/lib/api/storage/createFolder";
|
||||||
|
|
||||||
export default async function getData(userId: number, rawData: string) {
|
const MAX_LINKS_PER_USER = Number(process.env.MAX_LINKS_PER_USER) || 30000;
|
||||||
|
|
||||||
|
export default async function importFromLinkwarden(
|
||||||
|
userId: number,
|
||||||
|
rawData: string
|
||||||
|
) {
|
||||||
const data: Backup = JSON.parse(rawData);
|
const data: Backup = JSON.parse(rawData);
|
||||||
|
|
||||||
|
let totalImports = 0;
|
||||||
|
|
||||||
|
data.collections.forEach((collection) => {
|
||||||
|
totalImports += collection.links.length;
|
||||||
|
});
|
||||||
|
|
||||||
|
const numberOfLinksTheUserHas = await prisma.link.count({
|
||||||
|
where: {
|
||||||
|
collection: {
|
||||||
|
ownerId: userId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (totalImports + numberOfLinksTheUserHas > MAX_LINKS_PER_USER)
|
||||||
|
return {
|
||||||
|
response: `Error: Each user can only have a maximum of ${MAX_LINKS_PER_USER} Links.`,
|
||||||
|
status: 400,
|
||||||
|
};
|
||||||
|
|
||||||
await prisma
|
await prisma
|
||||||
.$transaction(
|
.$transaction(
|
||||||
async () => {
|
async () => {
|
||||||
|
|
|
@ -75,7 +75,7 @@ export default async function getPublicUser(
|
||||||
username: lessSensitiveInfo.username,
|
username: lessSensitiveInfo.username,
|
||||||
image: lessSensitiveInfo.image,
|
image: lessSensitiveInfo.image,
|
||||||
archiveAsScreenshot: lessSensitiveInfo.archiveAsScreenshot,
|
archiveAsScreenshot: lessSensitiveInfo.archiveAsScreenshot,
|
||||||
archiveAsPdf: lessSensitiveInfo.archiveAsPDF,
|
archiveAsPDF: lessSensitiveInfo.archiveAsPDF,
|
||||||
};
|
};
|
||||||
|
|
||||||
return { response: data, status: 200 };
|
return { response: data, status: 200 };
|
||||||
|
|
|
@ -46,7 +46,14 @@ export default function Index() {
|
||||||
<img
|
<img
|
||||||
alt=""
|
alt=""
|
||||||
src={`/api/v1/archives/${link.id}?format=${ArchivedFormat.png}`}
|
src={`/api/v1/archives/${link.id}?format=${ArchivedFormat.png}`}
|
||||||
className="object-contain w-full h-screen"
|
className="w-fit mx-auto"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{link && Number(router.query.format) === ArchivedFormat.jpeg && (
|
||||||
|
<img
|
||||||
|
alt=""
|
||||||
|
src={`/api/v1/archives/${link.id}?format=${ArchivedFormat.jpeg}`}
|
||||||
|
className="w-fit mx-auto"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -96,11 +96,11 @@ export default function PublicCollections() {
|
||||||
|
|
||||||
return collection ? (
|
return collection ? (
|
||||||
<div
|
<div
|
||||||
className="h-screen"
|
className="h-96"
|
||||||
style={{
|
style={{
|
||||||
backgroundImage: `linear-gradient(${collection?.color}30 10%, ${
|
backgroundImage: `linear-gradient(${collection?.color}30 10%, ${
|
||||||
settings.theme === "dark" ? "#262626" : "#f3f4f6"
|
settings.theme === "dark" ? "#262626" : "#f3f4f6"
|
||||||
} 18rem, ${settings.theme === "dark" ? "#171717" : "#ffffff"} 100%)`,
|
} 13rem, ${settings.theme === "dark" ? "#171717" : "#ffffff"} 100%)`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{collection ? (
|
{collection ? (
|
||||||
|
@ -208,6 +208,11 @@ export default function PublicCollections() {
|
||||||
{links
|
{links
|
||||||
?.filter((e) => e.collectionId === Number(router.query.id))
|
?.filter((e) => e.collectionId === Number(router.query.id))
|
||||||
.map((e, i) => {
|
.map((e, i) => {
|
||||||
|
const linkWithCollectionData = {
|
||||||
|
...e,
|
||||||
|
collection: collection, // Append collection data
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
key={i}
|
key={i}
|
||||||
|
@ -216,7 +221,10 @@ export default function PublicCollections() {
|
||||||
viewport={{ once: true, amount: 0.8 }}
|
viewport={{ once: true, amount: 0.8 }}
|
||||||
>
|
>
|
||||||
<motion.div variants={cardVariants}>
|
<motion.div variants={cardVariants}>
|
||||||
<PublicLinkCard link={e as any} count={i} />
|
<PublicLinkCard
|
||||||
|
link={linkWithCollectionData as any}
|
||||||
|
count={i}
|
||||||
|
/>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -0,0 +1,63 @@
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import useLinkStore from "@/store/links";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import {
|
||||||
|
ArchivedFormat,
|
||||||
|
LinkIncludingShortenedCollectionAndTags,
|
||||||
|
} from "@/types/global";
|
||||||
|
import ReadableView from "@/components/ReadableView";
|
||||||
|
|
||||||
|
export default function Index() {
|
||||||
|
const { links, getLink } = useLinkStore();
|
||||||
|
|
||||||
|
const [link, setLink] = useState<LinkIncludingShortenedCollectionAndTags>();
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
let isPublic = router.pathname.startsWith("/public") ? true : false;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchLink = async () => {
|
||||||
|
if (router.query.id) {
|
||||||
|
await getLink(Number(router.query.id), isPublic);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchLink();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (links[0]) setLink(links.find((e) => e.id === Number(router.query.id)));
|
||||||
|
}, [links]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative">
|
||||||
|
{/* <div className="fixed left-1/2 transform -translate-x-1/2 w-fit py-1 px-3 bg-base-200 border border-neutral-content rounded-md">
|
||||||
|
Readable
|
||||||
|
</div> */}
|
||||||
|
{link && Number(router.query.format) === ArchivedFormat.readability && (
|
||||||
|
<ReadableView link={link} />
|
||||||
|
)}
|
||||||
|
{link && Number(router.query.format) === ArchivedFormat.pdf && (
|
||||||
|
<iframe
|
||||||
|
src={`/api/v1/archives/${link.id}?format=${ArchivedFormat.pdf}`}
|
||||||
|
className="w-full h-screen border-none"
|
||||||
|
></iframe>
|
||||||
|
)}
|
||||||
|
{link && Number(router.query.format) === ArchivedFormat.png && (
|
||||||
|
<img
|
||||||
|
alt=""
|
||||||
|
src={`/api/v1/archives/${link.id}?format=${ArchivedFormat.png}`}
|
||||||
|
className="w-fit mx-auto"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{link && Number(router.query.format) === ArchivedFormat.jpeg && (
|
||||||
|
<img
|
||||||
|
alt=""
|
||||||
|
src={`/api/v1/archives/${link.id}?format=${ArchivedFormat.jpeg}`}
|
||||||
|
className="w-fit mx-auto"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -10,7 +10,6 @@ import SubmitButton from "@/components/SubmitButton";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { MigrationFormat, MigrationRequest } from "@/types/global";
|
import { MigrationFormat, MigrationRequest } from "@/types/global";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import ClickAwayHandler from "@/components/ClickAwayHandler";
|
|
||||||
import Checkbox from "@/components/Checkbox";
|
import Checkbox from "@/components/Checkbox";
|
||||||
|
|
||||||
export default function Account() {
|
export default function Account() {
|
||||||
|
@ -85,9 +84,9 @@ export default function Account() {
|
||||||
setSubmitLoader(false);
|
setSubmitLoader(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const [importDropdown, setImportDropdown] = useState(false);
|
|
||||||
|
|
||||||
const importBookmarks = async (e: any, format: MigrationFormat) => {
|
const importBookmarks = async (e: any, format: MigrationFormat) => {
|
||||||
|
setSubmitLoader(true);
|
||||||
|
|
||||||
const file: File = e.target.files[0];
|
const file: File = e.target.files[0];
|
||||||
|
|
||||||
if (file) {
|
if (file) {
|
||||||
|
@ -112,18 +111,19 @@ export default function Account() {
|
||||||
|
|
||||||
toast.dismiss(load);
|
toast.dismiss(load);
|
||||||
|
|
||||||
toast.success("Imported the Bookmarks! Reloading the page...");
|
if (response.ok) {
|
||||||
|
toast.success("Imported the Bookmarks! Reloading the page...");
|
||||||
setImportDropdown(false);
|
setTimeout(() => {
|
||||||
|
location.reload();
|
||||||
setTimeout(() => {
|
}, 2000);
|
||||||
location.reload();
|
} else toast.error(data.response as string);
|
||||||
}, 2000);
|
|
||||||
};
|
};
|
||||||
reader.onerror = function (e) {
|
reader.onerror = function (e) {
|
||||||
console.log("Error:", e);
|
console.log("Error:", e);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setSubmitLoader(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const [whitelistedUsersTextbox, setWhiteListedUsersTextbox] = useState("");
|
const [whitelistedUsersTextbox, setWhiteListedUsersTextbox] = useState("");
|
||||||
|
|
|
@ -10,6 +10,7 @@ declare global {
|
||||||
AUTOSCROLL_TIMEOUT?: string;
|
AUTOSCROLL_TIMEOUT?: string;
|
||||||
RE_ARCHIVE_LIMIT?: string;
|
RE_ARCHIVE_LIMIT?: string;
|
||||||
NEXT_PUBLIC_MAX_FILE_SIZE?: string;
|
NEXT_PUBLIC_MAX_FILE_SIZE?: string;
|
||||||
|
MAX_LINKS_PER_USER?: string;
|
||||||
|
|
||||||
SPACES_KEY?: string;
|
SPACES_KEY?: string;
|
||||||
SPACES_SECRET?: string;
|
SPACES_SECRET?: string;
|
||||||
|
|
Ŝarĝante…
Reference in New Issue