This commit is contained in:
daniel31x13 2023-12-06 16:13:11 -05:00
parent 6f77882ffc
commit 4b1017f45b
12 changed files with 695 additions and 191 deletions

View File

@ -26,6 +26,8 @@ import unescapeString from "@/lib/client/unescapeString";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import EditLinkModal from "./ModalContent/EditLinkModal"; import EditLinkModal from "./ModalContent/EditLinkModal";
import DeleteLinkModal from "./ModalContent/DeleteLinkModal"; import DeleteLinkModal from "./ModalContent/DeleteLinkModal";
import ExpandedLink from "./ModalContent/ExpandedLink";
import PreservedFormatsModal from "./ModalContent/PreservedFormatsModal";
type Props = { type Props = {
link: LinkIncludingShortenedCollectionAndTags; link: LinkIncludingShortenedCollectionAndTags;
@ -126,6 +128,8 @@ export default function LinkCard({ link, count, className }: 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 [expandedLink, setExpandedLink] = useState(false);
return ( return (
<div <div
@ -187,10 +191,11 @@ export default function LinkCard({ link, count, className }: Props) {
tabIndex={0} tabIndex={0}
onClick={() => { onClick={() => {
(document?.activeElement as HTMLElement)?.blur(); (document?.activeElement as HTMLElement)?.blur();
updateArchive(); setPreservedFormatsModal(true);
// updateArchive();
}} }}
> >
Refresh Link Preserved Formats
</div> </div>
</li> </li>
) : undefined} ) : undefined}
@ -212,8 +217,12 @@ export default function LinkCard({ link, count, className }: Props) {
</div> </div>
) : undefined} ) : undefined}
<div <Link
onClick={() => router.push("/links/" + link.id)} href={"/links/" + link.id}
// onClick={
// () => router.push("/links/" + link.id)
// // setExpandedLink(true)
// }
className="flex flex-col justify-between cursor-pointer h-full w-full gap-1 p-3" className="flex flex-col justify-between cursor-pointer h-full w-full gap-1 p-3"
> >
{link.url && url ? ( {link.url && url ? (
@ -232,12 +241,12 @@ export default function LinkCard({ link, count, className }: Props) {
) : link.type === "pdf" ? ( ) : link.type === "pdf" ? (
<FontAwesomeIcon <FontAwesomeIcon
icon={faFilePdf} icon={faFilePdf}
className="absolute h-12 w-12 bg-primary text-primary-content shadow rounded-md p-2 bottom-3 right-3 select-none z-10" className="absolute h-12 w-12 bg-primary/20 text-primary shadow rounded-md p-2 bottom-3 right-3 select-none z-10"
/> />
) : link.type === "image" ? ( ) : link.type === "image" ? (
<FontAwesomeIcon <FontAwesomeIcon
icon={faFileImage} icon={faFileImage}
className="absolute h-12 w-12 bg-primary text-primary-content shadow rounded-md p-2 bottom-3 right-3 select-none z-10" className="absolute h-12 w-12 bg-primary/20 text-primary shadow rounded-md p-2 bottom-3 right-3 select-none z-10"
/> />
) : undefined} ) : undefined}
@ -248,14 +257,14 @@ export default function LinkCard({ link, count, className }: Props) {
</p> </p>
</div> </div>
{link.type === "url" ? ( {link.url ? (
<Link <Link
href={link.url || ""} href={link.url || ""}
target="_blank" target="_blank"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
}} }}
className="flex items-center gap-1 max-w-full w-fit text-neutral hover:opacity-70 duration-100" className="flex items-center gap-1 max-w-full w-fit text-neutral hover:opacity-60 duration-100"
> >
<FontAwesomeIcon icon={faLink} className="mt-1 w-4 h-4" /> <FontAwesomeIcon icon={faLink} className="mt-1 w-4 h-4" />
<p className="truncate w-full">{shortendURL}</p> <p className="truncate w-full">{shortendURL}</p>
@ -304,7 +313,7 @@ export default function LinkCard({ link, count, className }: Props) {
) : ( ) : (
<p className="text-xs mt-2 p-1 font-semibold italic">No Tags</p> <p className="text-xs mt-2 p-1 font-semibold italic">No Tags</p>
)} */} )} */}
</div> </Link>
{editLinkModal ? ( {editLinkModal ? (
<EditLinkModal <EditLinkModal
onClose={() => setEditLinkModal(false)} onClose={() => setEditLinkModal(false)}
@ -317,6 +326,15 @@ export default function LinkCard({ link, count, className }: Props) {
activeLink={link} activeLink={link}
/> />
) : undefined} ) : undefined}
{preservedFormatsModal ? (
<PreservedFormatsModal
onClose={() => setPreservedFormatsModal(false)}
activeLink={link}
/>
) : undefined}
{/* {expandedLink ? (
<ExpandedLink onClose={() => setExpandedLink(false)} link={link} />
) : undefined} */}
</div> </div>
); );
} }

View File

@ -0,0 +1,254 @@
import {
CollectionIncludingMembersAndLinkCount,
LinkIncludingShortenedCollectionAndTags,
} from "@/types/global";
import Image from "next/image";
import ColorThief, { RGBColor } from "colorthief";
import { useEffect, useState } from "react";
import Link from "next/link";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
faArrowUpRightFromSquare,
faBoxArchive,
faCloudArrowDown,
faFolder,
} from "@fortawesome/free-solid-svg-icons";
import useCollectionStore from "@/store/collections";
import {
faCalendarDays,
faFileImage,
faFilePdf,
} from "@fortawesome/free-regular-svg-icons";
import isValidUrl from "@/lib/shared/isValidUrl";
import unescapeString from "@/lib/client/unescapeString";
import useLocalSettingsStore from "@/store/localSettings";
import Modal from "../Modal";
type Props = {
link: LinkIncludingShortenedCollectionAndTags;
onClose: Function;
};
export default function LinkDetails({ link, onClose }: Props) {
const {
settings: { theme },
} = useLocalSettingsStore();
const [imageError, setImageError] = useState<boolean>(false);
const formattedDate = new Date(link.createdAt as string).toLocaleString(
"en-US",
{
year: "numeric",
month: "short",
day: "numeric",
}
);
const { collections } = useCollectionStore();
const [collection, setCollection] =
useState<CollectionIncludingMembersAndLinkCount>(
collections.find(
(e) => e.id === link.collection.id
) as CollectionIncludingMembersAndLinkCount
);
useEffect(() => {
setCollection(
collections.find(
(e) => e.id === link.collection.id
) as CollectionIncludingMembersAndLinkCount
);
}, [collections]);
const [colorPalette, setColorPalette] = useState<RGBColor[]>();
const colorThief = new ColorThief();
const url = link.url && isValidUrl(link.url) ? new URL(link.url) : undefined;
const handleDownload = (format: "png" | "pdf") => {
const path = `/api/v1/archives/${link.collection.id}/${link.id}.${format}`;
fetch(path)
.then((response) => {
if (response.ok) {
// Create a temporary link and click it to trigger the download
const link = document.createElement("a");
link.href = path;
link.download = format === "pdf" ? "PDF" : "Screenshot";
link.click();
} else {
console.error("Failed to download file");
}
})
.catch((error) => {
console.error("Error:", error);
});
};
return (
<Modal toggleModal={onClose}>
<div className={`relative flex gap-5 items-start mr-10`}>
{!imageError && url && (
<Image
src={`https://t2.gstatic.com/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url=${url.origin}&size=32`}
width={42}
height={42}
alt=""
id={"favicon-" + link.id}
className="select-none mt-2 w-10 rounded-md shadow border-[3px] border-white dark:border-neutral-900 bg-white dark:bg-neutral-900 aspect-square"
draggable="false"
onLoad={(e) => {
try {
const color = colorThief.getPalette(
e.target as HTMLImageElement,
4
);
setColorPalette(color);
} catch (err) {
console.log(err);
}
}}
onError={(e) => {
setImageError(true);
}}
/>
)}
<div className="flex w-full flex-col min-h-[3rem] justify-center drop-shadow">
<p className="text-2xl capitalize break-words hyphens-auto">
{unescapeString(link.name)}
</p>
<Link
href={link.url || ""}
target="_blank"
rel="noreferrer"
className={`${
link.name ? "text-sm" : "text-xl"
} text-gray-500 dark:text-gray-300 break-all hover:underline cursor-pointer w-fit`}
>
{url ? url.host : link.url}
</Link>
</div>
</div>
<div className="flex gap-1 items-center flex-wrap">
<Link
href={`/collections/${link.collection.id}`}
className="flex items-center gap-1 cursor-pointer hover:opacity-60 duration-100 mr-2 z-10"
>
<FontAwesomeIcon
icon={faFolder}
className="w-5 h-5 drop-shadow"
style={{ color: collection?.color }}
/>
<p
title={collection?.name}
className="text-lg truncate max-w-[12rem]"
>
{collection?.name}
</p>
</Link>
{link.tags.map((e, i) => (
<Link key={i} href={`/tags/${e.id}`} className="z-10">
<p
title={e.name}
className="px-2 py-1 bg-sky-200 dark:bg-sky-900 text-xs rounded-3xl cursor-pointer hover:opacity-60 duration-100 truncate max-w-[19rem]"
>
{e.name}
</p>
</Link>
))}
</div>
{link.description && (
<>
<div className="max-h-[20rem] my-3 rounded-md overflow-y-auto hyphens-auto">
{unescapeString(link.description)}
</div>
</>
)}
<div className="flex justify-between items-center">
<div className="flex items-center gap-1 text-gray-500 dark:text-gray-300">
<FontAwesomeIcon icon={faBoxArchive} className="w-4 h-4" />
<p>Archived Formats:</p>
</div>
<div
className="flex items-center gap-1 text-gray-500 dark:text-gray-300"
title={"Created at: " + formattedDate}
>
<FontAwesomeIcon icon={faCalendarDays} className="w-4 h-4" />
<p>{formattedDate}</p>
</div>
</div>
<div className="flex flex-col gap-2">
<div className="flex justify-between items-center p-2 border border-sky-100 dark:border-neutral-700 rounded-md">
<div className="flex gap-2 items-center">
<div className="text-white bg-sky-300 dark:bg-sky-600 p-2 rounded-md">
<FontAwesomeIcon icon={faFileImage} className="w-6 h-6" />
</div>
<p>Screenshot</p>
</div>
<div className="flex gap-1">
<Link
href={`/api/v1/archives/${link.collectionId}/${link.id}.png`}
target="_blank"
rel="noreferrer"
className="cursor-pointer hover:bg-slate-200 hover:dark:bg-neutral-700 duration-100 p-2 rounded-md"
>
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="w-5 h-5 text-sky-500 dark:text-sky-500"
/>
</Link>
<div
onClick={() => handleDownload("png")}
className="cursor-pointer hover:bg-slate-200 hover:dark:bg-neutral-700 duration-100 p-2 rounded-md"
>
<FontAwesomeIcon
icon={faCloudArrowDown}
className="w-5 h-5 cursor-pointer text-sky-500 dark:text-sky-500"
/>
</div>
</div>
</div>
<div className="flex justify-between items-center p-2 border border-sky-100 dark:border-neutral-700 rounded-md">
<div className="flex gap-2 items-center">
<div className="text-white bg-sky-300 dark:bg-sky-600 p-2 rounded-md">
<FontAwesomeIcon icon={faFilePdf} className="w-6 h-6" />
</div>
<p>PDF</p>
</div>
<div className="flex gap-1">
<Link
href={`/api/v1/archives/${link.collectionId}/${link.id}.pdf`}
target="_blank"
rel="noreferrer"
className="cursor-pointer hover:bg-slate-200 hover:dark:bg-neutral-700 duration-100 p-2 rounded-md"
>
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="w-5 h-5 text-sky-500 dark:text-sky-500"
/>
</Link>
<div
onClick={() => handleDownload("pdf")}
className="cursor-pointer hover:bg-slate-200 hover:dark:bg-neutral-700 duration-100 p-2 rounded-md"
>
<FontAwesomeIcon
icon={faCloudArrowDown}
className="w-5 h-5 cursor-pointer text-sky-500 dark:text-sky-500"
/>
</div>
</div>
</div>
</div>
</Modal>
);
}

View File

@ -64,7 +64,6 @@ export default function NewLinkModal({ onClose }: Props) {
}; };
useEffect(() => { useEffect(() => {
setOptionsExpanded(false);
if (router.query.id) { if (router.query.id) {
const currentCollection = collections.find( const currentCollection = collections.find(
(e) => e.id == Number(router.query.id) (e) => e.id == Number(router.query.id)

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 useLinkStore from "@/store/links";
import {
ArchivedFormat,
LinkIncludingShortenedCollectionAndTags,
} from "@/types/global";
import toast from "react-hot-toast";
import Link from "next/link";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
faArrowUpRightFromSquare,
faCloudArrowDown,
faLink,
faTrashCan,
faUpRightFromSquare,
} from "@fortawesome/free-solid-svg-icons";
import Modal from "../Modal";
import { faFileImage, faFilePdf } from "@fortawesome/free-regular-svg-icons";
import { useRouter } from "next/router";
import { useSession } from "next-auth/react";
type Props = {
onClose: Function;
activeLink: LinkIncludingShortenedCollectionAndTags;
};
export default function PreservedFormatsModal({ onClose, activeLink }: Props) {
const session = useSession();
const { links, getLink } = useLinkStore();
const [link, setLink] =
useState<LinkIncludingShortenedCollectionAndTags>(activeLink);
const router = useRouter();
useEffect(() => {
let isPublicRoute = router.pathname.startsWith("/public")
? true
: undefined;
(async () => {
const data = await getLink(link.id as number, isPublicRoute);
setLink(
(data as any).response as LinkIncludingShortenedCollectionAndTags
);
})();
let interval: NodeJS.Timer | undefined;
if (link?.screenshotPath === "pending" || link?.pdfPath === "pending") {
interval = setInterval(async () => {
const data = await getLink(link.id as number, isPublicRoute);
setLink(
(data as any).response as LinkIncludingShortenedCollectionAndTags
);
}, 5000);
} else {
if (interval) {
clearInterval(interval);
}
}
return () => {
if (interval) {
clearInterval(interval);
}
};
}, [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 = (format: ArchivedFormat) => {
const path = `/api/v1/archives/${link?.id}?format=${format}`;
fetch(path)
.then((response) => {
if (response.ok) {
// Create a temporary link and click it to trigger the download
const link = document.createElement("a");
link.href = path;
link.download = format === ArchivedFormat.png ? "Screenshot" : "PDF";
link.click();
} else {
console.error("Failed to download file");
}
})
.catch((error) => {
console.error("Error:", error);
});
};
return (
<Modal toggleModal={onClose}>
<p className="text-xl font-thin">Preserved Formats</p>
<div className="divider mb-3 mt-1"></div>
<div className={`flex flex-col gap-3`}>
{link?.screenshotPath && link?.screenshotPath !== "pending" ? (
<div className="flex justify-between items-center pr-1 border border-neutral-content rounded-md">
<div className="flex gap-2 items-center">
<div className="bg-primary text-primary-content p-2 rounded-l-md">
<FontAwesomeIcon icon={faFileImage} className="w-6 h-6" />
</div>
<p>Screenshot</p>
</div>
<div className="flex gap-1">
<div
onClick={() => handleDownload(ArchivedFormat.png)}
className="cursor-pointer hover:opacity-60 duration-100 p-2 rounded-md"
>
<FontAwesomeIcon
icon={faCloudArrowDown}
className="w-5 h-5 cursor-pointer text-neutral"
/>
</div>
<Link
href={`/api/v1/archives/${link?.id}?format=${
link.screenshotPath.endsWith("png")
? ArchivedFormat.png
: ArchivedFormat.jpeg
}`}
target="_blank"
className="cursor-pointer hover:opacity-60 duration-100 p-2 rounded-md"
>
<FontAwesomeIcon
icon={faUpRightFromSquare}
className="w-5 h-5 text-neutral"
/>
</Link>
</div>
</div>
) : undefined}
{link?.pdfPath && link.pdfPath !== "pending" ? (
<div className="flex justify-between items-center pr-1 border border-neutral-content rounded-md">
<div className="flex gap-2 items-center">
<div className="bg-primary text-primary-content p-2 rounded-l-md">
<FontAwesomeIcon icon={faFilePdf} className="w-6 h-6" />
</div>
<p>PDF</p>
</div>
<div className="flex gap-1">
<div
onClick={() => handleDownload(ArchivedFormat.pdf)}
className="cursor-pointer hover:opacity-60 duration-100 p-2 rounded-md"
>
<FontAwesomeIcon
icon={faCloudArrowDown}
className="w-5 h-5 cursor-pointer text-neutral"
/>
</div>
<Link
href={`/api/v1/archives/${link?.id}?format=${ArchivedFormat.pdf}`}
target="_blank"
className="cursor-pointer hover:opacity-60 duration-100 p-2 rounded-md"
>
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="w-5 h-5 text-neutral"
/>
</Link>
</div>
</div>
) : undefined}
<div className="flex flex-col-reverse sm:flex-row sm:gap-3 items-center justify-center">
{link?.collection.ownerId === session.data?.user.id ? (
<div
className={`btn btn-accent text-white ${
link?.pdfPath &&
link?.screenshotPath &&
link?.pdfPath !== "pending" &&
link?.screenshotPath !== "pending"
? "mt-3"
: ""
}`}
onClick={() => updateArchive()}
>
<div>
<p>Update Preserved Formats</p>
<p className="text-xs">(Refresh Link)</p>
</div>
</div>
) : undefined}
<Link
href={`https://web.archive.org/web/${link?.url?.replace(
/(^\w+:|^)\/\//,
""
)}`}
target="_blank"
className={`text-neutral duration-100 hover:opacity-60 flex gap-2 w-fit items-center text-sm ${
link?.pdfPath &&
link?.screenshotPath &&
link?.pdfPath !== "pending" &&
link?.screenshotPath !== "pending"
? "sm:mt-3"
: ""
}`}
>
<p className="whitespace-nowrap">
View Latest Snapshot on archive.org
</p>
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="w-4 h-4"
/>
</Link>
</div>
</div>
</Modal>
);
}

View File

@ -90,7 +90,7 @@ export default function Navbar() {
New Link New Link
</div> </div>
</li> </li>
<li> {/* <li>
<div <div
onClick={() => { onClick={() => {
(document?.activeElement as HTMLElement)?.blur(); (document?.activeElement as HTMLElement)?.blur();
@ -101,7 +101,7 @@ export default function Navbar() {
> >
Upload File Upload File
</div> </div>
</li> </li> */}
<li> <li>
<div <div
onClick={() => { onClick={() => {

View File

@ -21,7 +21,7 @@ import {
} from "@fortawesome/free-brands-svg-icons"; } from "@fortawesome/free-brands-svg-icons";
export default function SettingsSidebar({ className }: { className?: string }) { export default function SettingsSidebar({ className }: { className?: string }) {
const LINKWARDEN_VERSION = "v2.3.0"; const LINKWARDEN_VERSION = "v2.4.0";
const { collections } = useCollectionStore(); const { collections } = useCollectionStore();

View File

@ -19,6 +19,8 @@ import {
} from "@/types/global"; } from "@/types/global";
import { useSession } from "next-auth/react"; import { useSession } from "next-auth/react";
import useCollectionStore from "@/store/collections"; import useCollectionStore from "@/store/collections";
import EditLinkModal from "@/components/ModalContent/EditLinkModal";
import Link from "next/link";
interface Props { interface Props {
children: ReactNode; children: ReactNode;
@ -73,17 +75,17 @@ export default function LinkLayout({ children }: Props) {
setLinkCollection(collections.find((e) => e.id === link?.collection?.id)); setLinkCollection(collections.find((e) => e.id === link?.collection?.id));
}, [link, collections]); }, [link, collections]);
const [editLinkModal, setEditLinkModal] = useState(false);
return ( return (
<> <>
<ModalManagement />
<div className="flex mx-auto"> <div className="flex mx-auto">
{/* <div className="hidden lg:block fixed left-5 h-screen"> {/* <div className="hidden lg:block fixed left-5 h-screen">
<LinkSidebar /> <LinkSidebar />
</div> */} </div> */}
<div className="w-full flex flex-col min-h-screen max-w-screen-md mx-auto p-5"> <div className="w-full flex flex-col min-h-screen max-w-screen-md mx-auto p-5">
<div className="flex gap-3 mb-5 duration-100 items-center justify-between"> <div className="flex gap-3 mb-3 duration-100 items-center justify-between">
{/* <div {/* <div
onClick={toggleSidebar} onClick={toggleSidebar}
className="inline-flex lg:hidden gap-1 items-center select-none cursor-pointer p-2 text-neutral rounded-md duration-100 hover:bg-neutral-content" className="inline-flex lg:hidden gap-1 items-center select-none cursor-pointer p-2 text-neutral rounded-md duration-100 hover:bg-neutral-content"
@ -91,33 +93,25 @@ export default function LinkLayout({ children }: Props) {
<FontAwesomeIcon icon={faBars} className="w-5 h-5" /> <FontAwesomeIcon icon={faBars} className="w-5 h-5" />
</div> */} </div> */}
<div <Link
onClick={() => { href={
if (router.pathname.startsWith("/public")) { router.pathname.startsWith("/public")
router.push( ? `/public/collections/${
`/public/collections/${
linkCollection?.id || link?.collection.id linkCollection?.id || link?.collection.id
}` }`
); : `/dashboard`
} else { }
router.push(`/dashboard`); className="inline-flex gap-1 btn btn-ghost btn-sm text-neutral px-2"
}
}}
className="inline-flex gap-1 hover:opacity-60 items-center select-none cursor-pointer p-2 lg:p-0 lg:px-1 lg:my-2 text-neutral rounded-md duration-100"
> >
<FontAwesomeIcon icon={faChevronLeft} className="w-4 h-4" /> <FontAwesomeIcon icon={faChevronLeft} className="w-4 h-4" />
Back{" "} <span className="capitalize">
<span className="hidden sm:inline-block"> {router.pathname.startsWith("/public")
to{" "} ? linkCollection?.name || link?.collection?.name
<span className="capitalize"> : "Dashboard"}
{router.pathname.startsWith("/public")
? linkCollection?.name || link?.collection?.name
: "Dashboard"}
</span>
</span> </span>
</div> </Link>
<div className="flex gap-5"> <div className="flex gap-3">
{link?.collection?.ownerId === userId || {link?.collection?.ownerId === userId ||
linkCollection?.members.some( linkCollection?.members.some(
(e) => e.userId === userId && e.canUpdate (e) => e.userId === userId && e.canUpdate
@ -125,20 +119,13 @@ export default function LinkLayout({ children }: Props) {
<div <div
title="Edit" title="Edit"
onClick={() => { onClick={() => {
link setEditLinkModal(true);
? setModal({
modal: "LINK",
state: true,
active: link,
method: "UPDATE",
})
: undefined;
}} }}
className={`hover:opacity-60 duration-100 py-2 px-2 cursor-pointer flex items-center gap-4 w-full rounded-md h-8`} className={`btn btn-ghost btn-square btn-sm`}
> >
<FontAwesomeIcon <FontAwesomeIcon
icon={faPen} icon={faPen}
className="w-6 h-6 text-neutral" className="w-4 h-4 text-neutral"
/> />
</div> </div>
) : undefined} ) : undefined}
@ -201,6 +188,12 @@ export default function LinkLayout({ children }: Props) {
</div> </div>
) : null} ) : null}
</div> </div>
{link && editLinkModal ? (
<EditLinkModal
onClose={() => setEditLinkModal(false)}
activeLink={link}
/>
) : undefined}
</div> </div>
</> </>
); );

View File

@ -1,6 +1,6 @@
import { prisma } from "@/lib/api/db"; import { prisma } from "@/lib/api/db";
import { LinkIncludingShortenedCollectionAndTags } from "@/types/global"; import { LinkIncludingShortenedCollectionAndTags } from "@/types/global";
import getTitle from "@/lib/api/getTitle"; import getTitle from "@/lib/shared/getTitle";
import urlHandler from "@/lib/api/urlHandler"; import urlHandler from "@/lib/api/urlHandler";
import { UsersAndCollections } from "@prisma/client"; import { UsersAndCollections } from "@prisma/client";
import getPermission from "@/lib/api/getPermission"; import getPermission from "@/lib/api/getPermission";

View File

@ -62,82 +62,83 @@ export default async function Index(req: NextApiRequest, res: NextApiResponse) {
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) => {
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 {
const fileBuffer = fs.readFileSync(files.file[0].filepath);
const linkStillExists = await prisma.link.findUnique({
where: { id: linkId },
});
if (linkStillExists) {
await createFile({
filePath: `archives/${collectionPermissions?.id}/${
linkId + suffix
}`,
data: fileBuffer,
});
await prisma.link.update({
where: { id: linkId },
data: {
screenshotPath: `archives/${collectionPermissions?.id}/${
linkId + suffix
}`,
lastPreserved: new Date().toISOString(),
},
});
}
fs.unlinkSync(files.file[0].filepath);
}
return res.status(200).json({
response: files,
});
});
} }
// 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) => {
// 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 {
// const fileBuffer = fs.readFileSync(files.file[0].filepath);
// const linkStillExists = await prisma.link.findUnique({
// where: { id: linkId },
// });
// if (linkStillExists) {
// await createFile({
// filePath: `archives/${collectionPermissions?.id}/${
// linkId + suffix
// }`,
// data: fileBuffer,
// });
// await prisma.link.update({
// where: { id: linkId },
// data: {
// screenshotPath: `archives/${collectionPermissions?.id}/${
// linkId + suffix
// }`,
// lastPreserved: new Date().toISOString(),
// },
// });
// }
// fs.unlinkSync(files.file[0].filepath);
// }
// return res.status(200).json({
// response: files,
// });
// });
// }
} }

View File

@ -13,7 +13,11 @@ import unescapeString from "@/lib/client/unescapeString";
import isValidUrl from "@/lib/shared/isValidUrl"; import isValidUrl from "@/lib/shared/isValidUrl";
import DOMPurify from "dompurify"; import DOMPurify from "dompurify";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faBoxesStacked, faFolder } from "@fortawesome/free-solid-svg-icons"; import {
faBoxesStacked,
faFolder,
faLink,
} from "@fortawesome/free-solid-svg-icons";
import useModalStore from "@/store/modals"; import useModalStore from "@/store/modals";
import { useSession } from "next-auth/react"; import { useSession } from "next-auth/react";
import useLocalSettingsStore from "@/store/localSettings"; import useLocalSettingsStore from "@/store/localSettings";
@ -118,19 +122,19 @@ export default function Index() {
if (colorPalette && banner && bannerInner) { if (colorPalette && banner && bannerInner) {
if (colorPalette[0] && colorPalette[1]) { if (colorPalette[0] && colorPalette[1]) {
banner.style.background = `linear-gradient(to right, ${rgbToHex( banner.style.background = `linear-gradient(to bottom, ${rgbToHex(
colorPalette[0][0], colorPalette[0][0],
colorPalette[0][1], colorPalette[0][1],
colorPalette[0][2] colorPalette[0][2]
)}30, ${rgbToHex( )}20, ${rgbToHex(
colorPalette[1][0], colorPalette[1][0],
colorPalette[1][1], colorPalette[1][1],
colorPalette[1][2] colorPalette[1][2]
)}30)`; )}20)`;
} }
if (colorPalette[2] && colorPalette[3]) { if (colorPalette[2] && colorPalette[3]) {
bannerInner.style.background = `linear-gradient(to left, ${rgbToHex( bannerInner.style.background = `linear-gradient(to bottom, ${rgbToHex(
colorPalette[2][0], colorPalette[2][0],
colorPalette[2][1], colorPalette[2][1],
colorPalette[2][2] colorPalette[2][2]
@ -145,19 +149,15 @@ export default function Index() {
return ( return (
<LinkLayout> <LinkLayout>
<div <div className={`flex flex-col max-w-screen-md h-full`}>
className={`flex flex-col max-w-screen-md h-full ${
settings.theme === "dark" ? "banner-dark-mode" : "banner-light-mode"
}`}
>
<div <div
id="link-banner" id="link-banner"
className="link-banner p-5 mb-4 relative bg-opacity-10 border border-solid border-neutral-content shadow-md" className="link-banner relative bg-opacity-10 border-neutral-content"
> >
<div id="link-banner-inner" className="link-banner-inner"></div> {/* <div id="link-banner-inner" className="link-banner-inner"></div> */}
<div className={`relative flex flex-col gap-3 items-start`}> <div className={`relative flex flex-col gap-3 items-start`}>
<div className="flex gap-3 items-end"> <div className="flex gap-3 items-start">
{!imageError && link?.url && ( {!imageError && link?.url && (
<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`}
@ -165,7 +165,7 @@ export default function Index() {
height={42} height={42}
alt="" alt=""
id={"favicon-" + link.id} id={"favicon-" + link.id}
className="bg-white shadow rounded-md p-1 bottom-5 right-5 select-none" className="bg-white shadow rounded-md p-1 select-none mt-1"
draggable="false" draggable="false"
onLoad={(e) => { onLoad={(e) => {
try { try {
@ -184,76 +184,76 @@ export default function Index() {
}} }}
/> />
)} )}
<div className="flex flex-col">
<div className="flex gap-2 text-sm text-neutral"> <p className="text-xl">
<p className=" min-w-fit"> {unescapeString(link?.name || link?.description || "")}
{link?.createdAt
? new Date(link?.createdAt).toLocaleString("en-US", {
year: "numeric",
month: "long",
day: "numeric",
})
: undefined}
</p> </p>
{link?.url ? ( {link?.url ? (
<> <Link
<p></p> href={link?.url || ""}
<Link title={link?.url}
href={link?.url || ""} target="_blank"
title={link?.url} className="hover:opacity-60 duration-100 break-all text-sm flex items-center gap-1 text-neutral w-fit"
target="_blank" >
className="hover:opacity-60 duration-100 break-all" <FontAwesomeIcon icon={faLink} className="w-4 h-4" />
>
{isValidUrl(link?.url || "") {isValidUrl(link?.url || "")
? new URL(link?.url as string).host ? new URL(link?.url as string).host
: undefined} : undefined}
</Link> </Link>
</>
) : undefined} ) : undefined}
</div> </div>
</div> </div>
<div className="flex flex-col gap-2"> <div className="flex gap-1 items-center flex-wrap">
<p className="capitalize text-2xl sm:text-3xl font-thin"> <Link
{unescapeString(link?.name || link?.description || "")} href={`/collections/${link?.collection.id}`}
</p> className="flex items-center gap-1 cursor-pointer hover:opacity-60 duration-100 mr-2 z-10"
>
<div className="flex gap-1 items-center flex-wrap"> <FontAwesomeIcon
<Link icon={faFolder}
href={`/collections/${link?.collection.id}`} className="w-5 h-5 drop-shadow"
className="flex items-center gap-1 cursor-pointer hover:opacity-60 duration-100 mr-2 z-10" style={{ color: link?.collection.color }}
/>
<p
title={link?.collection.name}
className="text-lg truncate max-w-[12rem]"
> >
<FontAwesomeIcon {link?.collection.name}
icon={faFolder} </p>
className="w-5 h-5 drop-shadow" </Link>
style={{ color: link?.collection.color }} {link?.tags.map((e, i) => (
/> <Link key={i} href={`/tags/${e.id}`} className="z-10">
<p <p
title={link?.collection.name} title={e.name}
className="text-lg truncate max-w-[12rem]" className="btn btn-xs btn-ghost truncate max-w-[19rem]"
> >
{link?.collection.name} #{e.name}
</p> </p>
</Link> </Link>
{link?.tags.map((e, i) => ( ))}
<Link key={i} href={`/tags/${e.id}`} className="z-10">
<p
title={e.name}
className="btn btn-xs btn-ghost truncate max-w-[19rem]"
>
#{e.name}
</p>
</Link>
))}
</div>
</div> </div>
<p className="min-w-fit text-sm text-neutral">
{link?.createdAt
? new Date(link?.createdAt).toLocaleString("en-US", {
year: "numeric",
month: "long",
day: "numeric",
})
: undefined}
</p>
{link?.name ? <p>{link?.description}</p> : undefined}
</div> </div>
</div> </div>
<div className="divider"></div>
<div className="flex flex-col gap-5 h-full"> <div className="flex flex-col gap-5 h-full">
{link?.readabilityPath?.startsWith("archives") ? ( {link?.readabilityPath?.startsWith("archives") ? (
<div <div
className="line-break px-3 reader-view" className="line-break px-1 reader-view"
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
__html: DOMPurify.sanitize(linkContent?.content || "") || "", __html: DOMPurify.sanitize(linkContent?.content || "") || "",
}} }}

View File

@ -93,6 +93,8 @@ const useLinkStore = create<LinkStore>()((set) => ({
}; };
} }
}); });
return data;
} }
return { ok: response.ok, data: data.response }; return { ok: response.ok, data: data.response };