Merge pull request #330 from linkwarden/feat/handle-files
Feat/handle files
This commit is contained in:
commit
9cd165f2ce
|
@ -13,6 +13,7 @@ STORAGE_FOLDER=
|
|||
AUTOSCROLL_TIMEOUT=
|
||||
NEXT_PUBLIC_DISABLE_REGISTRATION=
|
||||
RE_ARCHIVE_LIMIT=
|
||||
NEXT_PUBLIC_MAX_UPLOAD_SIZE=
|
||||
|
||||
# AWS S3 Settings
|
||||
SPACES_KEY=
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
{}
|
|
@ -65,7 +65,7 @@ export default function CollectionCard({ collection, className }: Props) {
|
|||
|
||||
return (
|
||||
<div className="relative">
|
||||
<div className="dropdown dropdown-bottom dropdown-end absolute top-3 right-3 z-10">
|
||||
<div className="dropdown dropdown-bottom dropdown-end absolute top-3 right-3 z-20">
|
||||
<div
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
|
@ -121,7 +121,7 @@ export default function CollectionCard({ collection, className }: Props) {
|
|||
{collectionOwner.id ? (
|
||||
<ProfilePhoto
|
||||
src={collectionOwner.image || undefined}
|
||||
dimensionClass="w-7 h-7"
|
||||
name={collectionOwner.name}
|
||||
/>
|
||||
) : undefined}
|
||||
{collection.members
|
||||
|
@ -131,13 +131,14 @@ export default function CollectionCard({ collection, className }: Props) {
|
|||
<ProfilePhoto
|
||||
key={i}
|
||||
src={e.user.image ? e.user.image : undefined}
|
||||
name={e.user.name}
|
||||
className="-ml-3"
|
||||
/>
|
||||
);
|
||||
})
|
||||
.slice(0, 3)}
|
||||
{collection.members.length - 3 > 0 ? (
|
||||
<div className={`avatar placeholder -ml-3`}>
|
||||
<div className={`avatar drop-shadow-md placeholder -ml-3`}>
|
||||
<div className="bg-base-100 text-neutral rounded-full w-8 h-8 ring-2 ring-neutral-content">
|
||||
<span>+{collection.members.length - 3}</span>
|
||||
</div>
|
||||
|
|
|
@ -13,14 +13,9 @@ type Props = {
|
|||
value?: number;
|
||||
}
|
||||
| undefined;
|
||||
id?: string;
|
||||
};
|
||||
|
||||
export default function CollectionSelection({
|
||||
onChange,
|
||||
defaultValue,
|
||||
id,
|
||||
}: Props) {
|
||||
export default function CollectionSelection({ onChange, defaultValue }: Props) {
|
||||
const { collections } = useCollectionStore();
|
||||
const router = useRouter();
|
||||
|
||||
|
@ -49,7 +44,6 @@ export default function CollectionSelection({
|
|||
|
||||
return (
|
||||
<CreatableSelect
|
||||
key={id || "key"}
|
||||
isClearable={false}
|
||||
className="react-select-container"
|
||||
classNamePrefix="react-select"
|
||||
|
|
|
@ -13,7 +13,11 @@ import Image from "next/image";
|
|||
import useLinkStore from "@/store/links";
|
||||
import useCollectionStore from "@/store/collections";
|
||||
import useAccountStore from "@/store/account";
|
||||
import { faCalendarDays } from "@fortawesome/free-regular-svg-icons";
|
||||
import {
|
||||
faCalendarDays,
|
||||
faFileImage,
|
||||
faFilePdf,
|
||||
} from "@fortawesome/free-regular-svg-icons";
|
||||
import usePermissions from "@/hooks/usePermissions";
|
||||
import { toast } from "react-hot-toast";
|
||||
import isValidUrl from "@/lib/shared/isValidUrl";
|
||||
|
@ -22,6 +26,8 @@ import unescapeString from "@/lib/client/unescapeString";
|
|||
import { useRouter } from "next/router";
|
||||
import EditLinkModal from "./ModalContent/EditLinkModal";
|
||||
import DeleteLinkModal from "./ModalContent/DeleteLinkModal";
|
||||
import ExpandedLink from "./ModalContent/ExpandedLink";
|
||||
import PreservedFormatsModal from "./ModalContent/PreservedFormatsModal";
|
||||
|
||||
type Props = {
|
||||
link: LinkIncludingShortenedCollectionAndTags;
|
||||
|
@ -63,7 +69,7 @@ export default function LinkCard({ link, count, className }: Props) {
|
|||
);
|
||||
}, [collections, links]);
|
||||
|
||||
const { removeLink, updateLink, getLink } = useLinkStore();
|
||||
const { removeLink, updateLink } = useLinkStore();
|
||||
|
||||
const pinLink = async () => {
|
||||
const isAlreadyPinned = link?.pinnedBy && link.pinnedBy[0];
|
||||
|
@ -81,23 +87,6 @@ export default function LinkCard({ link, count, className }: Props) {
|
|||
toast.success(`Link ${isAlreadyPinned ? "Unpinned!" : "Pinned!"}`);
|
||||
};
|
||||
|
||||
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 deleteLink = async () => {
|
||||
const load = toast.loading("Deleting...");
|
||||
|
||||
|
@ -122,6 +111,8 @@ export default function LinkCard({ link, count, className }: Props) {
|
|||
|
||||
const [editLinkModal, setEditLinkModal] = useState(false);
|
||||
const [deleteLinkModal, setDeleteLinkModal] = useState(false);
|
||||
const [preservedFormatsModal, setPreservedFormatsModal] = useState(false);
|
||||
const [expandedLink, setExpandedLink] = useState(false);
|
||||
|
||||
return (
|
||||
<div
|
||||
|
@ -183,10 +174,11 @@ export default function LinkCard({ link, count, className }: Props) {
|
|||
tabIndex={0}
|
||||
onClick={() => {
|
||||
(document?.activeElement as HTMLElement)?.blur();
|
||||
updateArchive();
|
||||
setPreservedFormatsModal(true);
|
||||
// updateArchive();
|
||||
}}
|
||||
>
|
||||
Refresh Link
|
||||
Preserved Formats
|
||||
</div>
|
||||
</li>
|
||||
) : undefined}
|
||||
|
@ -208,52 +200,82 @@ export default function LinkCard({ link, count, className }: Props) {
|
|||
</div>
|
||||
) : undefined}
|
||||
|
||||
<div
|
||||
onClick={() => router.push("/links/" + link.id)}
|
||||
className="flex items-start cursor-pointer gap-5 sm:gap-10 h-full w-full p-4"
|
||||
<Link
|
||||
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"
|
||||
>
|
||||
{url && account.displayLinkIcons && (
|
||||
{link.url && url ? (
|
||||
<Image
|
||||
src={`https://t2.gstatic.com/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url=${url.origin}&size=32`}
|
||||
src={`https://t2.gstatic.com/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url=${link.url}&size=32`}
|
||||
width={64}
|
||||
height={64}
|
||||
alt=""
|
||||
className={`${
|
||||
account.blurredFavicons ? "blur-sm " : ""
|
||||
} absolute w-12 duration-100 bg-white rounded-md p-1 bottom-5 right-5 select-none z-10`}
|
||||
className={`absolute w-12 bg-white shadow rounded-md p-1 bottom-3 right-3 select-none z-10`}
|
||||
draggable="false"
|
||||
onError={(e) => {
|
||||
const target = e.target as HTMLElement;
|
||||
target.style.display = "none";
|
||||
}}
|
||||
/>
|
||||
) : link.type === "pdf" ? (
|
||||
<FontAwesomeIcon
|
||||
icon={faFilePdf}
|
||||
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" ? (
|
||||
<FontAwesomeIcon
|
||||
icon={faFileImage}
|
||||
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}
|
||||
|
||||
<div className="flex items-baseline gap-1">
|
||||
<p className="text-sm text-neutral">{count + 1}</p>
|
||||
<p className="text-lg truncate w-full pr-8">
|
||||
{unescapeString(link.name || link.description) || shortendURL}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{link.url ? (
|
||||
<div
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
window.open(link.url || "", "_blank");
|
||||
}}
|
||||
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" />
|
||||
<p className="truncate w-full">{shortendURL}</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="badge badge-primary badge-sm my-1">{link.type}</div>
|
||||
)}
|
||||
|
||||
<div className="flex justify-between gap-5 w-full h-full z-0">
|
||||
<div className="flex flex-col justify-between w-full">
|
||||
<div className="flex items-baseline gap-1">
|
||||
<p className="text-sm text-neutral">{count + 1}</p>
|
||||
<p className="text-lg truncate capitalize w-full pr-8">
|
||||
{unescapeString(link.name || link.description)}
|
||||
</p>
|
||||
</div>
|
||||
<Link
|
||||
href={`/collections/${link.collection.id}`}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
className="flex items-center gap-1 max-w-full w-fit my-1 hover:opacity-70 duration-100"
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faFolder}
|
||||
className="w-4 h-4 mt-1 drop-shadow"
|
||||
style={{ color: collection?.color }}
|
||||
/>
|
||||
<p className="truncate capitalize w-full">{collection?.name}</p>
|
||||
</Link>
|
||||
<div
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
router.push(`/collections/${link.collection.id}`);
|
||||
}}
|
||||
className="flex items-center gap-1 max-w-full w-fit hover:opacity-70 duration-100"
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faFolder}
|
||||
className="w-4 h-4 mt-1 drop-shadow"
|
||||
style={{ color: collection?.color }}
|
||||
/>
|
||||
<p className="truncate capitalize w-full">{collection?.name}</p>
|
||||
</div>
|
||||
|
||||
{/* {link.tags[0] ? (
|
||||
<div className="flex gap-3 items-center flex-wrap my-2 truncate relative">
|
||||
<div className="flex items-center gap-1 text-neutral">
|
||||
<FontAwesomeIcon icon={faCalendarDays} className="w-4 h-4" />
|
||||
<p>{formattedDate}</p>
|
||||
</div>
|
||||
{/* {link.tags[0] ? (
|
||||
<div className="flex gap-3 items-center flex-wrap mt-2 truncate relative">
|
||||
<div className="flex gap-1 items-center flex-nowrap">
|
||||
{link.tags.map((e, i) => (
|
||||
<Link
|
||||
|
@ -262,34 +284,18 @@ export default function LinkCard({ link, count, className }: Props) {
|
|||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
className="btn btn-xs btn-outline truncate max-w-[19rem]"
|
||||
className="btn btn-xs btn-ghost truncate max-w-[19rem]"
|
||||
>
|
||||
{e.name}
|
||||
#{e.name}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
<div className="absolute w-1/2 top-0 bottom-0 right-0 bg-gradient-to-r from-transparent to-base-200 to-35%"></div>
|
||||
</div>
|
||||
) : undefined} */}
|
||||
|
||||
<Link
|
||||
href={link.url || ""}
|
||||
target="_blank"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
className="flex items-center gap-1 max-w-full w-fit text-neutral hover:opacity-70 duration-100"
|
||||
>
|
||||
<FontAwesomeIcon icon={faLink} className="mt-1 w-4 h-4" />
|
||||
<p className="truncate w-full">{shortendURL}</p>
|
||||
</Link>
|
||||
<div className="flex items-center gap-1 text-neutral">
|
||||
<FontAwesomeIcon icon={faCalendarDays} className="w-4 h-4" />
|
||||
<p>{formattedDate}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-xs mt-2 p-1 font-semibold italic">No Tags</p>
|
||||
)} */}
|
||||
</Link>
|
||||
{editLinkModal ? (
|
||||
<EditLinkModal
|
||||
onClose={() => setEditLinkModal(false)}
|
||||
|
@ -302,6 +308,15 @@ export default function LinkCard({ link, count, className }: Props) {
|
|||
activeLink={link}
|
||||
/>
|
||||
) : undefined}
|
||||
{preservedFormatsModal ? (
|
||||
<PreservedFormatsModal
|
||||
onClose={() => setPreservedFormatsModal(false)}
|
||||
activeLink={link}
|
||||
/>
|
||||
) : undefined}
|
||||
{/* {expandedLink ? (
|
||||
<ExpandedLink onClose={() => setExpandedLink(false)} link={link} />
|
||||
) : undefined} */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -11,12 +11,14 @@ type Props = {
|
|||
|
||||
export default function Modal({ toggleModal, className, children }: Props) {
|
||||
return (
|
||||
<div className="overflow-y-auto py-2 fixed top-0 bottom-0 right-0 left-0 bg-black bg-opacity-10 backdrop-blur-sm flex justify-center items-center fade-in z-30">
|
||||
<div className="overflow-y-auto pt-2 sm:py-2 fixed top-0 bottom-0 right-0 left-0 bg-black bg-opacity-10 backdrop-blur-sm flex justify-center items-center fade-in z-30">
|
||||
<ClickAwayHandler
|
||||
onClickOutside={toggleModal}
|
||||
className={`m-auto w-11/12 max-w-2xl ${className || ""}`}
|
||||
className={`w-full mt-auto sm:m-auto sm:w-11/12 sm:max-w-2xl ${
|
||||
className || ""
|
||||
}`}
|
||||
>
|
||||
<div className="slide-up m-auto relative border-neutral-content rounded-2xl border-solid border shadow-2xl p-5 bg-base-100">
|
||||
<div className="slide-up mt-auto sm:m-auto relative border-neutral-content rounded-t-2xl sm:rounded-2xl border-t sm:border shadow-2xl p-5 bg-base-100">
|
||||
<div
|
||||
onClick={toggleModal as MouseEventHandler<HTMLDivElement>}
|
||||
className="absolute top-3 right-3 btn btn-sm outline-none btn-circle btn-ghost"
|
||||
|
|
|
@ -92,7 +92,7 @@ export default function PreservedFormats() {
|
|||
{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="text-white bg-primary p-2 rounded-l-md">
|
||||
<div className="bg-primary text-primary-content p-2 rounded-l-md">
|
||||
<FontAwesomeIcon icon={faFileImage} className="w-6 h-6" />
|
||||
</div>
|
||||
|
||||
|
@ -131,7 +131,7 @@ export default function PreservedFormats() {
|
|||
{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="text-white bg-primary p-2 rounded-l-md">
|
||||
<div className="bg-primary text-primary-content p-2 rounded-l-md">
|
||||
<FontAwesomeIcon icon={faFilePdf} className="w-6 h-6" />
|
||||
</div>
|
||||
|
||||
|
@ -166,7 +166,7 @@ export default function PreservedFormats() {
|
|||
<div className="flex flex-col-reverse sm:flex-row gap-5 items-center justify-center">
|
||||
{link?.collection.ownerId === session.data?.user.id ? (
|
||||
<div
|
||||
className={`w-full text-center bg-sky-700 p-1 rounded-md cursor-pointer select-none hover:bg-sky-600 duration-100 ${
|
||||
className={`btn btn-accent text-white ${
|
||||
link?.pdfPath &&
|
||||
link?.screenshotPath &&
|
||||
link?.pdfPath !== "pending" &&
|
||||
|
@ -176,12 +176,14 @@ export default function PreservedFormats() {
|
|||
}`}
|
||||
onClick={() => updateArchive()}
|
||||
>
|
||||
<p>Update Preserved Formats</p>
|
||||
<p className="text-xs">(Refresh Link)</p>
|
||||
<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(
|
||||
href={`https://web.archive.org/web/${link?.url?.replace(
|
||||
/(^\w+:|^)\/\//,
|
||||
""
|
||||
)}`}
|
||||
|
|
|
@ -64,10 +64,12 @@ export default function DeleteCollectionModal({
|
|||
|
||||
return (
|
||||
<Modal toggleModal={onClose}>
|
||||
<p className="text-xl mb-5 font-thin text-red-500">
|
||||
<p className="text-xl font-thin text-red-500">
|
||||
{permissions === true ? "Delete" : "Leave"} Collection
|
||||
</p>
|
||||
|
||||
<div className="divider mb-3 mt-1"></div>
|
||||
|
||||
<div className="flex flex-col gap-3">
|
||||
{permissions === true ? (
|
||||
<>
|
||||
|
|
|
@ -11,6 +11,7 @@ import Link from "next/link";
|
|||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faLink, faTrashCan } from "@fortawesome/free-solid-svg-icons";
|
||||
import Modal from "../Modal";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
type Props = {
|
||||
onClose: Function;
|
||||
|
@ -21,17 +22,11 @@ export default function DeleteLinkModal({ onClose, activeLink }: Props) {
|
|||
const [link, setLink] =
|
||||
useState<LinkIncludingShortenedCollectionAndTags>(activeLink);
|
||||
|
||||
let shortendURL;
|
||||
|
||||
try {
|
||||
shortendURL = new URL(link.url).host.toLowerCase();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
|
||||
const { removeLink } = useLinkStore();
|
||||
const [submitLoader, setSubmitLoader] = useState(false);
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
setLink(activeLink);
|
||||
}, []);
|
||||
|
@ -45,12 +40,19 @@ export default function DeleteLinkModal({ onClose, activeLink }: Props) {
|
|||
|
||||
response.ok && toast.success(`Link Deleted.`);
|
||||
|
||||
if (router.pathname.startsWith("/links/[id]")) {
|
||||
router.push("/dashboard");
|
||||
}
|
||||
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal toggleModal={onClose}>
|
||||
<p className="text-xl mb-5 font-thin text-red-500">Delete Link</p>
|
||||
<p className="text-xl font-thin text-red-500">Delete Link</p>
|
||||
|
||||
<div className="divider mb-3 mt-1"></div>
|
||||
|
||||
<div className="flex flex-col gap-3">
|
||||
<p>Are you sure you want to delete this Link?</p>
|
||||
|
||||
|
|
|
@ -49,7 +49,9 @@ export default function EditCollectionModal({
|
|||
|
||||
return (
|
||||
<Modal toggleModal={onClose}>
|
||||
<p className="text-xl mb-5 font-thin">Edit Collection Info</p>
|
||||
<p className="text-xl font-thin">Edit Collection Info</p>
|
||||
|
||||
<div className="divider mb-3 mt-1"></div>
|
||||
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex flex-col sm:flex-row gap-3">
|
||||
|
@ -108,7 +110,10 @@ export default function EditCollectionModal({
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<button className="btn btn-accent w-fit ml-auto" onClick={submit}>
|
||||
<button
|
||||
className="btn btn-accent text-white w-fit ml-auto"
|
||||
onClick={submit}
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
@ -95,10 +95,12 @@ export default function EditCollectionSharingModal({
|
|||
|
||||
return (
|
||||
<Modal toggleModal={onClose}>
|
||||
<p className="text-xl font-thin mb-5">
|
||||
<p className="text-xl font-thin">
|
||||
{permissions === true ? "Share and Collaborate" : "Team"}
|
||||
</p>
|
||||
|
||||
<div className="divider mb-3 mt-1"></div>
|
||||
|
||||
<div className="flex flex-col gap-3">
|
||||
{permissions === true && (
|
||||
<div>
|
||||
|
@ -178,7 +180,7 @@ export default function EditCollectionSharingModal({
|
|||
setMemberState
|
||||
)
|
||||
}
|
||||
className="btn btn-primary text-white btn-square"
|
||||
className="btn btn-accent text-white btn-square btn-sm h-10 w-10"
|
||||
>
|
||||
<FontAwesomeIcon icon={faUserPlus} className="w-5 h-5" />
|
||||
</div>
|
||||
|
@ -201,7 +203,7 @@ export default function EditCollectionSharingModal({
|
|||
|
||||
<div className="flex flex-col gap-3 rounded-md">
|
||||
<div
|
||||
className="relative border px-2 rounded-xl border-neutral-content bg-base-200 flex min-h-[7rem] sm:min-h-[5rem] gap-2 justify-between"
|
||||
className="relative border px-2 rounded-xl border-neutral-content bg-base-200 flex min-h-[6rem] sm:min-h-[4.1rem] gap-2 justify-between"
|
||||
title={`@${collectionOwner.username} is the owner of this collection.`}
|
||||
>
|
||||
<div className="flex items-center gap-2 w-full">
|
||||
|
@ -209,6 +211,7 @@ export default function EditCollectionSharingModal({
|
|||
src={
|
||||
collectionOwner.image ? collectionOwner.image : undefined
|
||||
}
|
||||
name={collectionOwner.name}
|
||||
/>
|
||||
<div className="w-full">
|
||||
<div className="flex items-center gap-1 w-full justify-between">
|
||||
|
@ -257,6 +260,7 @@ export default function EditCollectionSharingModal({
|
|||
<div className="flex items-center gap-2">
|
||||
<ProfilePhoto
|
||||
src={e.user.image ? e.user.image : undefined}
|
||||
name={e.user.name}
|
||||
/>
|
||||
<div>
|
||||
<p className="text-sm font-bold">{e.user.name}</p>
|
||||
|
@ -323,7 +327,7 @@ export default function EditCollectionSharingModal({
|
|||
}}
|
||||
/>
|
||||
<span
|
||||
className={`peer-checked:bg-primary text-sm ${
|
||||
className={`peer-checked:bg-primary peer-checked:text-primary-content text-sm ${
|
||||
permissions === true
|
||||
? "hover:bg-neutral-content duration-100"
|
||||
: ""
|
||||
|
@ -368,7 +372,7 @@ export default function EditCollectionSharingModal({
|
|||
}}
|
||||
/>
|
||||
<span
|
||||
className={`peer-checked:bg-primary text-sm ${
|
||||
className={`peer-checked:bg-primary peer-checked:text-primary-content text-sm ${
|
||||
permissions === true
|
||||
? "hover:bg-neutral-content duration-100"
|
||||
: ""
|
||||
|
@ -413,7 +417,7 @@ export default function EditCollectionSharingModal({
|
|||
}}
|
||||
/>
|
||||
<span
|
||||
className={`peer-checked:bg-primary text-sm ${
|
||||
className={`peer-checked:bg-primary peer-checked:text-primary-content text-sm ${
|
||||
permissions === true
|
||||
? "hover:bg-neutral-content duration-100"
|
||||
: ""
|
||||
|
@ -434,7 +438,7 @@ export default function EditCollectionSharingModal({
|
|||
|
||||
{permissions === true && (
|
||||
<button
|
||||
className="btn btn-accent w-fit ml-auto mt-3"
|
||||
className="btn btn-accent text-white w-fit ml-auto mt-3"
|
||||
onClick={submit}
|
||||
>
|
||||
Save
|
||||
|
|
|
@ -24,7 +24,7 @@ export default function EditLinkModal({ onClose, activeLink }: Props) {
|
|||
let shortendURL;
|
||||
|
||||
try {
|
||||
shortendURL = new URL(link.url).host.toLowerCase();
|
||||
shortendURL = new URL(link.url || "").host.toLowerCase();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
|
@ -78,20 +78,24 @@ export default function EditLinkModal({ onClose, activeLink }: Props) {
|
|||
|
||||
return (
|
||||
<Modal toggleModal={onClose}>
|
||||
<p className="text-xl mb-5 font-thin">Edit Link</p>
|
||||
<p className="text-xl font-thin">Edit Link</p>
|
||||
|
||||
<Link
|
||||
href={link.url}
|
||||
className="truncate text-neutral flex gap-2 mb-5 w-fit max-w-full"
|
||||
title={link.url}
|
||||
target="_blank"
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faLink}
|
||||
className="mt-1 w-5 h-5 min-w-[1.25rem]"
|
||||
/>
|
||||
<p>{shortendURL}</p>
|
||||
</Link>
|
||||
<div className="divider mb-3 mt-1"></div>
|
||||
|
||||
{link.url ? (
|
||||
<Link
|
||||
href={link.url}
|
||||
className="truncate text-neutral flex gap-2 mb-5 w-fit max-w-full"
|
||||
title={link.url}
|
||||
target="_blank"
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faLink}
|
||||
className="mt-1 w-5 h-5 min-w-[1.25rem]"
|
||||
/>
|
||||
<p>{shortendURL}</p>
|
||||
</Link>
|
||||
) : undefined}
|
||||
|
||||
<div className="w-full">
|
||||
<p className="mb-2">Name</p>
|
||||
|
@ -155,7 +159,7 @@ export default function EditLinkModal({ onClose, activeLink }: Props) {
|
|||
</div>
|
||||
|
||||
<div className="flex justify-end items-center mt-5">
|
||||
<button className="btn btn-accent" onClick={submit}>
|
||||
<button className="btn btn-accent text-white" onClick={submit}>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -54,7 +54,9 @@ export default function NewCollectionModal({ onClose }: Props) {
|
|||
|
||||
return (
|
||||
<Modal toggleModal={onClose}>
|
||||
<p className="text-xl mb-5 font-thin">Create a New Collection</p>
|
||||
<p className="text-xl font-thin">Create a New Collection</p>
|
||||
|
||||
<div className="divider mb-3 mt-1"></div>
|
||||
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex flex-col sm:flex-row gap-3">
|
||||
|
@ -113,7 +115,10 @@ export default function NewCollectionModal({ onClose }: Props) {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<button className="btn btn-accent w-fit ml-auto" onClick={submit}>
|
||||
<button
|
||||
className="btn btn-accent text-white w-fit ml-auto"
|
||||
onClick={submit}
|
||||
>
|
||||
Create Collection
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
@ -23,6 +23,7 @@ export default function NewLinkModal({ onClose }: Props) {
|
|||
name: "",
|
||||
url: "",
|
||||
description: "",
|
||||
type: "url",
|
||||
tags: [],
|
||||
screenshotPath: "",
|
||||
pdfPath: "",
|
||||
|
@ -32,7 +33,7 @@ export default function NewLinkModal({ onClose }: Props) {
|
|||
name: "",
|
||||
ownerId: data?.user.id as number,
|
||||
},
|
||||
};
|
||||
} as LinkIncludingShortenedCollectionAndTags;
|
||||
|
||||
const [link, setLink] =
|
||||
useState<LinkIncludingShortenedCollectionAndTags>(initial);
|
||||
|
@ -40,8 +41,6 @@ export default function NewLinkModal({ onClose }: Props) {
|
|||
const { addLink } = useLinkStore();
|
||||
const [submitLoader, setSubmitLoader] = useState(false);
|
||||
|
||||
const [resetCollectionSelection, setResetCollectionSelection] = useState("");
|
||||
|
||||
const [optionsExpanded, setOptionsExpanded] = useState(false);
|
||||
|
||||
const router = useRouter();
|
||||
|
@ -65,10 +64,6 @@ export default function NewLinkModal({ onClose }: Props) {
|
|||
};
|
||||
|
||||
useEffect(() => {
|
||||
setResetCollectionSelection(Date.now().toString());
|
||||
console.log(link);
|
||||
|
||||
setOptionsExpanded(false);
|
||||
if (router.query.id) {
|
||||
const currentCollection = collections.find(
|
||||
(e) => e.id == Number(router.query.id)
|
||||
|
@ -122,12 +117,15 @@ export default function NewLinkModal({ onClose }: Props) {
|
|||
|
||||
return (
|
||||
<Modal toggleModal={onClose}>
|
||||
<p className="text-xl mb-5 font-thin">Create a New Link</p>
|
||||
<p className="text-xl font-thin">Create a New Link</p>
|
||||
|
||||
<div className="divider mb-3 mt-1"></div>
|
||||
|
||||
<div className="grid grid-flow-row-dense sm:grid-cols-5 gap-3">
|
||||
<div className="sm:col-span-3 col-span-5">
|
||||
<p className="mb-2">Link</p>
|
||||
<TextInput
|
||||
value={link.url}
|
||||
value={link.url || ""}
|
||||
onChange={(e) => setLink({ ...link, url: e.target.value })}
|
||||
placeholder="e.g. http://example.com/"
|
||||
className="bg-base-200"
|
||||
|
@ -142,7 +140,6 @@ export default function NewLinkModal({ onClose }: Props) {
|
|||
label: link.collection.name,
|
||||
value: link.collection.id,
|
||||
}}
|
||||
id={resetCollectionSelection}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
|
@ -195,7 +192,7 @@ export default function NewLinkModal({ onClose }: Props) {
|
|||
<p>{optionsExpanded ? "Hide" : "More"} Options</p>
|
||||
</div>
|
||||
|
||||
<button className="btn btn-accent" onClick={submit}>
|
||||
<button className="btn btn-accent text-white" onClick={submit}>
|
||||
Create Link
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,246 @@
|
|||
import React, { useEffect, useState } from "react";
|
||||
import { Toaster } from "react-hot-toast";
|
||||
import CollectionSelection from "@/components/InputSelect/CollectionSelection";
|
||||
import TagSelection from "@/components/InputSelect/TagSelection";
|
||||
import TextInput from "@/components/TextInput";
|
||||
import unescapeString from "@/lib/client/unescapeString";
|
||||
import useCollectionStore from "@/store/collections";
|
||||
import useLinkStore from "@/store/links";
|
||||
import {
|
||||
ArchivedFormat,
|
||||
LinkIncludingShortenedCollectionAndTags,
|
||||
} from "@/types/global";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { useRouter } from "next/router";
|
||||
import toast from "react-hot-toast";
|
||||
import Modal from "../Modal";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faQuestion } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faQuestionCircle } from "@fortawesome/free-regular-svg-icons";
|
||||
|
||||
type Props = {
|
||||
onClose: Function;
|
||||
};
|
||||
|
||||
export default function UploadFileModal({ onClose }: Props) {
|
||||
const { data } = useSession();
|
||||
|
||||
const initial = {
|
||||
name: "",
|
||||
url: "",
|
||||
description: "",
|
||||
type: "url",
|
||||
tags: [],
|
||||
screenshotPath: "",
|
||||
pdfPath: "",
|
||||
readabilityPath: "",
|
||||
textContent: "",
|
||||
collection: {
|
||||
name: "",
|
||||
ownerId: data?.user.id as number,
|
||||
},
|
||||
} as LinkIncludingShortenedCollectionAndTags;
|
||||
|
||||
const [link, setLink] =
|
||||
useState<LinkIncludingShortenedCollectionAndTags>(initial);
|
||||
|
||||
const [file, setFile] = useState<File>();
|
||||
|
||||
const { addLink } = useLinkStore();
|
||||
const [submitLoader, setSubmitLoader] = useState(false);
|
||||
|
||||
const [optionsExpanded, setOptionsExpanded] = useState(false);
|
||||
|
||||
const router = useRouter();
|
||||
const { collections } = useCollectionStore();
|
||||
|
||||
const setCollection = (e: any) => {
|
||||
if (e?.__isNew__) e.value = null;
|
||||
|
||||
setLink({
|
||||
...link,
|
||||
collection: { id: e?.value, name: e?.label, ownerId: e?.ownerId },
|
||||
});
|
||||
};
|
||||
|
||||
const setTags = (e: any) => {
|
||||
const tagNames = e.map((e: any) => {
|
||||
return { name: e.label };
|
||||
});
|
||||
|
||||
setLink({ ...link, tags: tagNames });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setOptionsExpanded(false);
|
||||
if (router.query.id) {
|
||||
const currentCollection = collections.find(
|
||||
(e) => e.id == Number(router.query.id)
|
||||
);
|
||||
|
||||
if (
|
||||
currentCollection &&
|
||||
currentCollection.ownerId &&
|
||||
router.asPath.startsWith("/collections/")
|
||||
)
|
||||
setLink({
|
||||
...initial,
|
||||
collection: {
|
||||
id: currentCollection.id,
|
||||
name: currentCollection.name,
|
||||
ownerId: currentCollection.ownerId,
|
||||
},
|
||||
});
|
||||
} else
|
||||
setLink({
|
||||
...initial,
|
||||
collection: {
|
||||
name: "Unorganized",
|
||||
ownerId: data?.user.id as number,
|
||||
},
|
||||
});
|
||||
}, []);
|
||||
|
||||
const submit = async () => {
|
||||
if (!submitLoader && file) {
|
||||
let fileType: ArchivedFormat | null = null;
|
||||
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);
|
||||
|
||||
let response;
|
||||
|
||||
const load = toast.loading("Creating...");
|
||||
|
||||
response = await addLink({
|
||||
...link,
|
||||
type: linkType,
|
||||
name: link.name ? link.name : file.name.replace(/\.[^/.]+$/, ""),
|
||||
});
|
||||
|
||||
toast.dismiss(load);
|
||||
|
||||
if (response.ok) {
|
||||
const formBody = new FormData();
|
||||
file && formBody.append("file", file);
|
||||
|
||||
await fetch(
|
||||
`/api/v1/archives/${
|
||||
(response.data as LinkIncludingShortenedCollectionAndTags).id
|
||||
}?format=${fileType}`,
|
||||
{
|
||||
body: formBody,
|
||||
method: "POST",
|
||||
}
|
||||
);
|
||||
toast.success(`Created!`);
|
||||
onClose();
|
||||
} else toast.error(response.data as string);
|
||||
|
||||
setSubmitLoader(false);
|
||||
|
||||
return response;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal toggleModal={onClose}>
|
||||
<div className="flex gap-2 items-start">
|
||||
<p className="text-xl font-thin">Upload File</p>
|
||||
</div>
|
||||
<div className="divider mb-3 mt-1"></div>
|
||||
<div className="grid grid-flow-row-dense sm:grid-cols-5 gap-3">
|
||||
<div className="sm:col-span-3 col-span-5">
|
||||
<p className="mb-2">File</p>
|
||||
<label className="btn h-10 btn-sm w-full border border-neutral-content hover:border-neutral-content flex justify-between">
|
||||
<input
|
||||
type="file"
|
||||
accept=".pdf,.png,.jpg,.jpeg"
|
||||
className="cursor-pointer custom-file-input"
|
||||
onChange={(e) => e.target.files && setFile(e.target.files[0])}
|
||||
/>
|
||||
</label>
|
||||
<p className="text-xs font-semibold mt-2">
|
||||
PDF, PNG, JPG (Up to {process.env.NEXT_PUBLIC_MAX_UPLOAD_SIZE || 30}
|
||||
MB)
|
||||
</p>
|
||||
</div>
|
||||
<div className="sm:col-span-2 col-span-5">
|
||||
<p className="mb-2">Collection</p>
|
||||
{link.collection.name ? (
|
||||
<CollectionSelection
|
||||
onChange={setCollection}
|
||||
defaultValue={{
|
||||
label: link.collection.name,
|
||||
value: link.collection.id,
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
{optionsExpanded ? (
|
||||
<div className="mt-5">
|
||||
{/* <hr className="mb-3 border border-neutral-content" /> */}
|
||||
<div className="grid sm:grid-cols-2 gap-3">
|
||||
<div>
|
||||
<p className="mb-2">Name</p>
|
||||
<TextInput
|
||||
value={link.name}
|
||||
onChange={(e) => setLink({ ...link, name: e.target.value })}
|
||||
placeholder="e.g. Example Link"
|
||||
className="bg-base-200"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="mb-2">Tags</p>
|
||||
<TagSelection
|
||||
onChange={setTags}
|
||||
defaultValue={link.tags.map((e) => {
|
||||
return { label: e.name, value: e.id };
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="sm:col-span-2">
|
||||
<p className="mb-2">Description</p>
|
||||
<textarea
|
||||
value={unescapeString(link.description) as string}
|
||||
onChange={(e) =>
|
||||
setLink({ ...link, description: e.target.value })
|
||||
}
|
||||
placeholder="Will be auto generated if nothing is provided."
|
||||
className="resize-none w-full rounded-md p-2 border-neutral-content bg-base-200 focus:border-sky-300 dark:focus:border-sky-600 border-solid border outline-none duration-100"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : undefined}
|
||||
<div className="flex justify-between items-center mt-5">
|
||||
<div
|
||||
onClick={() => setOptionsExpanded(!optionsExpanded)}
|
||||
className={`rounded-md cursor-pointer btn btn-sm btn-ghost duration-100 flex items-center px-2 w-fit text-sm`}
|
||||
>
|
||||
<p>{optionsExpanded ? "Hide" : "More"} Options</p>
|
||||
</div>
|
||||
|
||||
<button className="btn btn-accent text-white" onClick={submit}>
|
||||
Create Link
|
||||
</button>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
|
@ -14,6 +14,7 @@ import useLocalSettingsStore from "@/store/localSettings";
|
|||
import NewLinkModal from "./ModalContent/NewLinkModal";
|
||||
import NewCollectionModal from "./ModalContent/NewCollectionModal";
|
||||
import Link from "next/link";
|
||||
import UploadFileModal from "./ModalContent/UploadFileModal";
|
||||
|
||||
export default function Navbar() {
|
||||
const { settings, updateSettings } = useLocalSettingsStore();
|
||||
|
@ -48,6 +49,7 @@ export default function Navbar() {
|
|||
|
||||
const [newLinkModal, setNewLinkModal] = useState(false);
|
||||
const [newCollectionModal, setNewCollectionModal] = useState(false);
|
||||
const [uploadFileModal, setUploadFileModal] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="flex justify-between gap-2 items-center px-4 py-2 border-solid border-b-neutral-content border-b">
|
||||
|
@ -88,6 +90,18 @@ export default function Navbar() {
|
|||
New Link
|
||||
</div>
|
||||
</li>
|
||||
{/* <li>
|
||||
<div
|
||||
onClick={() => {
|
||||
(document?.activeElement as HTMLElement)?.blur();
|
||||
setUploadFileModal(true);
|
||||
}}
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
>
|
||||
Upload File
|
||||
</div>
|
||||
</li> */}
|
||||
<li>
|
||||
<div
|
||||
onClick={() => {
|
||||
|
@ -163,6 +177,9 @@ export default function Navbar() {
|
|||
{newCollectionModal ? (
|
||||
<NewCollectionModal onClose={() => setNewCollectionModal(false)} />
|
||||
) : undefined}
|
||||
{uploadFileModal ? (
|
||||
<UploadFileModal onClose={() => setUploadFileModal(false)} />
|
||||
) : undefined}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -31,7 +31,7 @@ export default function ProfilePhoto({
|
|||
|
||||
return !image ? (
|
||||
<div
|
||||
className={`avatar placeholder ${className || ""} ${
|
||||
className={`avatar drop-shadow-md placeholder ${className || ""} ${
|
||||
dimensionClass || "w-8 h-8 "
|
||||
}`}
|
||||
>
|
||||
|
|
|
@ -57,9 +57,9 @@ export default function LinkCard({ link, count }: Props) {
|
|||
<Link
|
||||
href={"/public/collections/20?q=" + e.name}
|
||||
key={i}
|
||||
className="btn btn-xs btn-outline truncate max-w-[19rem]"
|
||||
className="btn btn-xs btn-ghost truncate max-w-[19rem]"
|
||||
>
|
||||
{e.name}
|
||||
#{e.name}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
@ -21,7 +21,7 @@ import {
|
|||
} from "@fortawesome/free-brands-svg-icons";
|
||||
|
||||
export default function SettingsSidebar({ className }: { className?: string }) {
|
||||
const LINKWARDEN_VERSION = "v2.3.0";
|
||||
const LINKWARDEN_VERSION = "v2.4.0";
|
||||
|
||||
const { collections } = useCollectionStore();
|
||||
|
||||
|
|
|
@ -21,7 +21,7 @@ export default function SubmitButton({
|
|||
return (
|
||||
<button
|
||||
type={type ? type : undefined}
|
||||
className={`btn btn-primary text-white tracking-wider w-fit flex items-center gap-2 ${
|
||||
className={`btn btn-accent text-white tracking-wider w-fit flex items-center gap-2 ${
|
||||
className || ""
|
||||
}`}
|
||||
onClick={() => {
|
||||
|
|
|
@ -19,6 +19,11 @@ import {
|
|||
} from "@/types/global";
|
||||
import { useSession } from "next-auth/react";
|
||||
import useCollectionStore from "@/store/collections";
|
||||
import EditLinkModal from "@/components/ModalContent/EditLinkModal";
|
||||
import Link from "next/link";
|
||||
import PreservedFormatsModal from "@/components/ModalContent/PreservedFormatsModal";
|
||||
import toast from "react-hot-toast";
|
||||
import DeleteLinkModal from "@/components/ModalContent/DeleteLinkModal";
|
||||
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
|
@ -73,17 +78,31 @@ export default function LinkLayout({ children }: Props) {
|
|||
setLinkCollection(collections.find((e) => e.id === link?.collection?.id));
|
||||
}, [link, collections]);
|
||||
|
||||
const deleteLink = async () => {
|
||||
const load = toast.loading("Deleting...");
|
||||
|
||||
const response = await removeLink(link?.id as number);
|
||||
|
||||
toast.dismiss(load);
|
||||
|
||||
response.ok && toast.success(`Link Deleted.`);
|
||||
|
||||
router.push("/dashboard");
|
||||
};
|
||||
|
||||
const [editLinkModal, setEditLinkModal] = useState(false);
|
||||
const [deleteLinkModal, setDeleteLinkModal] = useState(false);
|
||||
const [preservedFormatsModal, setPreservedFormatsModal] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ModalManagement />
|
||||
|
||||
<div className="flex mx-auto">
|
||||
{/* <div className="hidden lg:block fixed left-5 h-screen">
|
||||
<LinkSidebar />
|
||||
</div> */}
|
||||
|
||||
<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
|
||||
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"
|
||||
|
@ -91,75 +110,49 @@ export default function LinkLayout({ children }: Props) {
|
|||
<FontAwesomeIcon icon={faBars} className="w-5 h-5" />
|
||||
</div> */}
|
||||
|
||||
<div
|
||||
onClick={() => {
|
||||
if (router.pathname.startsWith("/public")) {
|
||||
router.push(
|
||||
`/public/collections/${
|
||||
<Link
|
||||
href={
|
||||
router.pathname.startsWith("/public")
|
||||
? `/public/collections/${
|
||||
linkCollection?.id || link?.collection.id
|
||||
}`
|
||||
);
|
||||
} else {
|
||||
router.push(`/dashboard`);
|
||||
}
|
||||
}}
|
||||
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"
|
||||
: `/dashboard`
|
||||
}
|
||||
className="inline-flex gap-1 btn btn-ghost btn-sm text-neutral px-2"
|
||||
>
|
||||
<FontAwesomeIcon icon={faChevronLeft} className="w-4 h-4" />
|
||||
Back{" "}
|
||||
<span className="hidden sm:inline-block">
|
||||
to{" "}
|
||||
<span className="capitalize">
|
||||
{router.pathname.startsWith("/public")
|
||||
? linkCollection?.name || link?.collection?.name
|
||||
: "Dashboard"}
|
||||
</span>
|
||||
<span className="capitalize">
|
||||
{router.pathname.startsWith("/public")
|
||||
? linkCollection?.name || link?.collection?.name
|
||||
: "Dashboard"}
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
<div className="flex gap-5">
|
||||
<div className="flex gap-3">
|
||||
{link?.collection?.ownerId === userId ||
|
||||
linkCollection?.members.some(
|
||||
(e) => e.userId === userId && e.canUpdate
|
||||
) ? (
|
||||
<div
|
||||
title="Edit"
|
||||
onClick={() => {
|
||||
link
|
||||
? 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`}
|
||||
onClick={() => setEditLinkModal(true)}
|
||||
className={`btn btn-ghost btn-square btn-sm`}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faPen}
|
||||
className="w-6 h-6 text-neutral"
|
||||
className="w-4 h-4 text-neutral"
|
||||
/>
|
||||
</div>
|
||||
) : undefined}
|
||||
|
||||
<div
|
||||
onClick={() => {
|
||||
link
|
||||
? setModal({
|
||||
modal: "LINK",
|
||||
state: true,
|
||||
active: link,
|
||||
method: "FORMATS",
|
||||
})
|
||||
: undefined;
|
||||
}}
|
||||
onClick={() => setPreservedFormatsModal(true)}
|
||||
title="Preserved Formats"
|
||||
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
|
||||
icon={faBoxesStacked}
|
||||
className="w-6 h-6 text-neutral"
|
||||
className="w-4 h-4 text-neutral"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
@ -168,18 +161,16 @@ export default function LinkLayout({ children }: Props) {
|
|||
(e) => e.userId === userId && e.canDelete
|
||||
) ? (
|
||||
<div
|
||||
onClick={() => {
|
||||
if (link?.id) {
|
||||
removeLink(link.id);
|
||||
router.back();
|
||||
}
|
||||
onClick={(e) => {
|
||||
(document?.activeElement as HTMLElement)?.blur();
|
||||
e.shiftKey ? deleteLink() : setDeleteLinkModal(true);
|
||||
}}
|
||||
title="Delete"
|
||||
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
|
||||
icon={faTrashCan}
|
||||
className="w-6 h-6 text-neutral"
|
||||
className="w-4 h-4 text-neutral"
|
||||
/>
|
||||
</div>
|
||||
) : undefined}
|
||||
|
@ -201,6 +192,24 @@ export default function LinkLayout({ children }: Props) {
|
|||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
{link && editLinkModal ? (
|
||||
<EditLinkModal
|
||||
onClose={() => setEditLinkModal(false)}
|
||||
activeLink={link}
|
||||
/>
|
||||
) : undefined}
|
||||
{link && deleteLinkModal ? (
|
||||
<DeleteLinkModal
|
||||
onClose={() => setDeleteLinkModal(false)}
|
||||
activeLink={link}
|
||||
/>
|
||||
) : undefined}
|
||||
{link && preservedFormatsModal ? (
|
||||
<PreservedFormatsModal
|
||||
onClose={() => setPreservedFormatsModal(false)}
|
||||
activeLink={link}
|
||||
/>
|
||||
) : undefined}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { prisma } from "@/lib/api/db";
|
||||
import { LinkIncludingShortenedCollectionAndTags } from "@/types/global";
|
||||
import getTitle from "@/lib/api/getTitle";
|
||||
import getTitle from "@/lib/shared/getTitle";
|
||||
import urlHandler from "@/lib/api/urlHandler";
|
||||
import { UsersAndCollections } from "@prisma/client";
|
||||
import getPermission from "@/lib/api/getPermission";
|
||||
|
@ -75,7 +75,6 @@ export default async function postLink(
|
|||
name: link.name,
|
||||
description,
|
||||
type: linkType,
|
||||
readabilityPath: "pending",
|
||||
collection: {
|
||||
connectOrCreate: {
|
||||
where: {
|
||||
|
@ -118,11 +117,33 @@ export default async function postLink(
|
|||
? urlHandler(newLink.id, newLink.url, userId)
|
||||
: undefined;
|
||||
|
||||
linkType === "pdf" ? pdfHandler(newLink.id, newLink.url) : undefined;
|
||||
newLink.url && linkType === "pdf"
|
||||
? pdfHandler(newLink.id, newLink.url)
|
||||
: undefined;
|
||||
|
||||
linkType === "image"
|
||||
newLink.url && linkType === "image"
|
||||
? imageHandler(newLink.id, newLink.url, imageExtension)
|
||||
: undefined;
|
||||
|
||||
!newLink.url && linkType === "pdf"
|
||||
? await prisma.link.update({
|
||||
where: { id: newLink.id },
|
||||
data: {
|
||||
pdfPath: "pending",
|
||||
lastPreserved: new Date().toISOString(),
|
||||
},
|
||||
})
|
||||
: undefined;
|
||||
|
||||
!newLink.url && linkType === "image"
|
||||
? await prisma.link.update({
|
||||
where: { id: newLink.id },
|
||||
data: {
|
||||
screenshotPath: "pending",
|
||||
lastPreserved: new Date().toISOString(),
|
||||
},
|
||||
})
|
||||
: undefined;
|
||||
|
||||
return { response: newLink, status: 200 };
|
||||
}
|
||||
|
|
|
@ -37,6 +37,23 @@ export default async function urlHandler(
|
|||
|
||||
const content = await page.content();
|
||||
|
||||
// TODO
|
||||
// const session = await page.context().newCDPSession(page);
|
||||
|
||||
// const doc = await session.send("Page.captureSnapshot", {
|
||||
// format: "mhtml",
|
||||
// });
|
||||
|
||||
// const saveDocLocally = (doc: any) => {
|
||||
// console.log(doc);
|
||||
// return createFile({
|
||||
// data: doc,
|
||||
// filePath: `archives/${targetLink.collectionId}/${linkId}.mhtml`,
|
||||
// });
|
||||
// };
|
||||
|
||||
// saveDocLocally(doc.data);
|
||||
|
||||
// Readability
|
||||
|
||||
const window = new JSDOM("").window;
|
||||
|
|
|
@ -25,6 +25,7 @@
|
|||
"@prisma/client": "^4.16.2",
|
||||
"@stripe/stripe-js": "^1.54.1",
|
||||
"@types/crypto-js": "^4.1.1",
|
||||
"@types/formidable": "^3.4.5",
|
||||
"@types/node": "20.4.4",
|
||||
"@types/nodemailer": "^6.4.8",
|
||||
"@types/react": "18.2.14",
|
||||
|
@ -37,6 +38,7 @@
|
|||
"dompurify": "^3.0.6",
|
||||
"eslint": "8.46.0",
|
||||
"eslint-config-next": "13.4.9",
|
||||
"formidable": "^3.5.1",
|
||||
"framer-motion": "^10.16.4",
|
||||
"jsdom": "^22.1.0",
|
||||
"lottie-web": "^5.12.2",
|
||||
|
|
|
@ -47,7 +47,7 @@ export default function App({
|
|||
reverseOrder={false}
|
||||
toastOptions={{
|
||||
className:
|
||||
"border border-sky-100 dark:border-neutral-700 dark:bg-neutral-900 dark:text-white",
|
||||
"border border-sky-100 dark:border-neutral-700 dark:bg-neutral-800 dark:text-white",
|
||||
}}
|
||||
/>
|
||||
<Component {...pageProps} />
|
||||
|
|
|
@ -3,49 +3,142 @@ import readFile from "@/lib/api/storage/readFile";
|
|||
import { getToken } from "next-auth/jwt";
|
||||
import { prisma } from "@/lib/api/db";
|
||||
import { ArchivedFormat } from "@/types/global";
|
||||
import verifyUser from "@/lib/api/verifyUser";
|
||||
import getPermission from "@/lib/api/getPermission";
|
||||
import { UsersAndCollections } from "@prisma/client";
|
||||
import formidable from "formidable";
|
||||
import createFile from "@/lib/api/storage/createFile";
|
||||
import fs from "fs";
|
||||
|
||||
export const config = {
|
||||
api: {
|
||||
bodyParser: false,
|
||||
},
|
||||
};
|
||||
|
||||
export default async function Index(req: NextApiRequest, res: NextApiResponse) {
|
||||
const linkId = Number(req.query.linkId);
|
||||
const format = Number(req.query.format);
|
||||
|
||||
let suffix;
|
||||
let suffix: string;
|
||||
|
||||
if (format === ArchivedFormat.png) suffix = ".png";
|
||||
else if (format === ArchivedFormat.jpeg) suffix = ".jpeg";
|
||||
else if (format === ArchivedFormat.pdf) suffix = ".pdf";
|
||||
else if (format === ArchivedFormat.readability) suffix = "_readability.json";
|
||||
|
||||
//@ts-ignore
|
||||
if (!linkId || !suffix)
|
||||
return res.status(401).json({ response: "Invalid parameters." });
|
||||
|
||||
const token = await getToken({ req });
|
||||
const userId = token?.id;
|
||||
if (req.method === "GET") {
|
||||
const token = await getToken({ req });
|
||||
const userId = token?.id;
|
||||
|
||||
const collectionIsAccessible = await prisma.collection.findFirst({
|
||||
where: {
|
||||
links: {
|
||||
some: {
|
||||
id: linkId,
|
||||
const collectionIsAccessible = await prisma.collection.findFirst({
|
||||
where: {
|
||||
links: {
|
||||
some: {
|
||||
id: linkId,
|
||||
},
|
||||
},
|
||||
OR: [
|
||||
{ ownerId: userId || -1 },
|
||||
{ members: { some: { userId: userId || -1 } } },
|
||||
{ isPublic: true },
|
||||
],
|
||||
},
|
||||
OR: [
|
||||
{ ownerId: userId || -1 },
|
||||
{ members: { some: { userId: userId || -1 } } },
|
||||
{ isPublic: true },
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
if (!collectionIsAccessible)
|
||||
return res
|
||||
.status(401)
|
||||
.json({ response: "You don't have access to this collection." });
|
||||
if (!collectionIsAccessible)
|
||||
return res
|
||||
.status(401)
|
||||
.json({ response: "You don't have access to this collection." });
|
||||
|
||||
const { file, contentType, status } = await readFile(
|
||||
`archives/${collectionIsAccessible.id}/${linkId + suffix}`
|
||||
);
|
||||
const { file, contentType, status } = await readFile(
|
||||
`archives/${collectionIsAccessible.id}/${linkId + suffix}`
|
||||
);
|
||||
|
||||
res.setHeader("Content-Type", contentType).status(status as number);
|
||||
res.setHeader("Content-Type", contentType).status(status as number);
|
||||
|
||||
return res.send(file);
|
||||
return res.send(file);
|
||||
}
|
||||
// else if (req.method === "POST") {
|
||||
// const user = await verifyUser({ req, res });
|
||||
// if (!user) return;
|
||||
|
||||
// const collectionPermissions = await getPermission({
|
||||
// userId: user.id,
|
||||
// linkId,
|
||||
// });
|
||||
|
||||
// const memberHasAccess = collectionPermissions?.members.some(
|
||||
// (e: UsersAndCollections) => e.userId === user.id && e.canCreate
|
||||
// );
|
||||
|
||||
// if (!(collectionPermissions?.ownerId === user.id || memberHasAccess))
|
||||
// return { response: "Collection is not accessible.", status: 401 };
|
||||
|
||||
// // await uploadHandler(linkId, )
|
||||
|
||||
// const MAX_UPLOAD_SIZE = Number(process.env.NEXT_PUBLIC_MAX_UPLOAD_SIZE);
|
||||
|
||||
// const form = formidable({
|
||||
// maxFields: 1,
|
||||
// maxFiles: 1,
|
||||
// maxFileSize: MAX_UPLOAD_SIZE || 30 * 1048576,
|
||||
// });
|
||||
|
||||
// form.parse(req, async (err, fields, files) => {
|
||||
// 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,
|
||||
// });
|
||||
// });
|
||||
// }
|
||||
}
|
||||
|
|
|
@ -111,7 +111,10 @@ export default function Index() {
|
|||
onClick={() => setEditCollectionSharingModal(true)}
|
||||
>
|
||||
{collectionOwner.id ? (
|
||||
<ProfilePhoto src={collectionOwner.image || undefined} />
|
||||
<ProfilePhoto
|
||||
src={collectionOwner.image || undefined}
|
||||
name={collectionOwner.name}
|
||||
/>
|
||||
) : undefined}
|
||||
{activeCollection.members
|
||||
.sort((a, b) => (a.userId as number) - (b.userId as number))
|
||||
|
@ -121,19 +124,20 @@ export default function Index() {
|
|||
key={i}
|
||||
src={e.user.image ? e.user.image : undefined}
|
||||
className="-ml-3"
|
||||
name={e.user.name}
|
||||
/>
|
||||
);
|
||||
})
|
||||
.slice(0, 3)}
|
||||
{activeCollection.members.length - 3 > 0 ? (
|
||||
<div className={`avatar placeholder -ml-3`}>
|
||||
<div className={`avatar drop-shadow-md placeholder -ml-3`}>
|
||||
<div className="bg-base-100 text-neutral rounded-full w-8 h-8 ring-2 ring-neutral-content">
|
||||
<span>+{activeCollection.members.length - 3}</span>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<p className="text-neutral text-xs">
|
||||
<p className="text-neutral text-sm font-semibold">
|
||||
By {collectionOwner.name}
|
||||
{activeCollection.members.length > 0
|
||||
? ` and ${activeCollection.members.length} others`
|
||||
|
|
|
@ -44,35 +44,6 @@ export default function Collections() {
|
|||
<p>Collections you own</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative mt-2">
|
||||
<div className="dropdown dropdown-bottom">
|
||||
<div
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
className="btn btn-ghost btn-sm btn-square text-neutral"
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faEllipsis}
|
||||
title="More"
|
||||
className="w-5 h-5"
|
||||
/>
|
||||
</div>
|
||||
<ul className="dropdown-content z-[20] menu shadow bg-base-200 border border-neutral-content rounded-box w-40 mt-1">
|
||||
<li>
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => {
|
||||
(document?.activeElement as HTMLElement)?.blur();
|
||||
setNewCollectionModal(true);
|
||||
}}
|
||||
>
|
||||
New Collection
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative mt-2">
|
||||
|
|
|
@ -13,7 +13,11 @@ import unescapeString from "@/lib/client/unescapeString";
|
|||
import isValidUrl from "@/lib/shared/isValidUrl";
|
||||
import DOMPurify from "dompurify";
|
||||
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 { useSession } from "next-auth/react";
|
||||
import useLocalSettingsStore from "@/store/localSettings";
|
||||
|
@ -118,19 +122,19 @@ export default function Index() {
|
|||
|
||||
if (colorPalette && banner && bannerInner) {
|
||||
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][1],
|
||||
colorPalette[0][2]
|
||||
)}30, ${rgbToHex(
|
||||
)}20, ${rgbToHex(
|
||||
colorPalette[1][0],
|
||||
colorPalette[1][1],
|
||||
colorPalette[1][2]
|
||||
)}30)`;
|
||||
)}20)`;
|
||||
}
|
||||
|
||||
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][1],
|
||||
colorPalette[2][2]
|
||||
|
@ -145,19 +149,15 @@ export default function Index() {
|
|||
|
||||
return (
|
||||
<LinkLayout>
|
||||
<div
|
||||
className={`flex flex-col max-w-screen-md h-full ${
|
||||
settings.theme === "dark" ? "banner-dark-mode" : "banner-light-mode"
|
||||
}`}
|
||||
>
|
||||
<div className={`flex flex-col max-w-screen-md h-full`}>
|
||||
<div
|
||||
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="flex gap-3 items-end">
|
||||
<div className="flex gap-3 items-start">
|
||||
{!imageError && link?.url && (
|
||||
<Image
|
||||
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}
|
||||
alt=""
|
||||
id={"favicon-" + link.id}
|
||||
className="select-none mt-2 w-10 rounded-md shadow border-[3px] border-base-100 bg-base-100 aspect-square"
|
||||
className="bg-white shadow rounded-md p-1 select-none mt-1"
|
||||
draggable="false"
|
||||
onLoad={(e) => {
|
||||
try {
|
||||
|
@ -184,76 +184,76 @@ export default function Index() {
|
|||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="flex gap-2 text-sm text-neutral">
|
||||
<p className=" min-w-fit">
|
||||
{link?.createdAt
|
||||
? new Date(link?.createdAt).toLocaleString("en-US", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
})
|
||||
: undefined}
|
||||
<div className="flex flex-col">
|
||||
<p className="text-xl">
|
||||
{unescapeString(link?.name || link?.description || "")}
|
||||
</p>
|
||||
{link?.url ? (
|
||||
<>
|
||||
<p>•</p>
|
||||
<Link
|
||||
href={link?.url || ""}
|
||||
title={link?.url}
|
||||
target="_blank"
|
||||
className="hover:opacity-60 duration-100 break-all"
|
||||
>
|
||||
{isValidUrl(link?.url || "")
|
||||
? new URL(link?.url as string).host
|
||||
: undefined}
|
||||
</Link>
|
||||
</>
|
||||
<Link
|
||||
href={link?.url || ""}
|
||||
title={link?.url}
|
||||
target="_blank"
|
||||
className="hover:opacity-60 duration-100 break-all text-sm flex items-center gap-1 text-neutral w-fit"
|
||||
>
|
||||
<FontAwesomeIcon icon={faLink} className="w-4 h-4" />
|
||||
|
||||
{isValidUrl(link?.url || "")
|
||||
? new URL(link?.url as string).host
|
||||
: undefined}
|
||||
</Link>
|
||||
) : undefined}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<p className="capitalize text-2xl sm:text-3xl font-thin">
|
||||
{unescapeString(link?.name || link?.description || "")}
|
||||
</p>
|
||||
|
||||
<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"
|
||||
<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: link?.collection.color }}
|
||||
/>
|
||||
<p
|
||||
title={link?.collection.name}
|
||||
className="text-lg truncate max-w-[12rem]"
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faFolder}
|
||||
className="w-5 h-5 drop-shadow"
|
||||
style={{ color: link?.collection.color }}
|
||||
/>
|
||||
{link?.collection.name}
|
||||
</p>
|
||||
</Link>
|
||||
{link?.tags.map((e, i) => (
|
||||
<Link key={i} href={`/tags/${e.id}`} className="z-10">
|
||||
<p
|
||||
title={link?.collection.name}
|
||||
className="text-lg truncate max-w-[12rem]"
|
||||
title={e.name}
|
||||
className="btn btn-xs btn-ghost truncate max-w-[19rem]"
|
||||
>
|
||||
{link?.collection.name}
|
||||
#{e.name}
|
||||
</p>
|
||||
</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-outline truncate max-w-[19rem]"
|
||||
>
|
||||
{e.name}
|
||||
</p>
|
||||
</Link>
|
||||
))}
|
||||
</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 className="divider"></div>
|
||||
|
||||
<div className="flex flex-col gap-5 h-full">
|
||||
{link?.readabilityPath?.startsWith("archives") ? (
|
||||
<div
|
||||
className="line-break px-3 reader-view"
|
||||
className="line-break px-1 reader-view"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: DOMPurify.sanitize(linkContent?.content || "") || "",
|
||||
}}
|
||||
|
|
|
@ -145,7 +145,7 @@ export default function PublicCollections() {
|
|||
{collectionOwner.id ? (
|
||||
<ProfilePhoto
|
||||
src={collectionOwner.image || undefined}
|
||||
dimensionClass="w-7 h-7"
|
||||
name={collectionOwner.name}
|
||||
/>
|
||||
) : undefined}
|
||||
{collection.members
|
||||
|
@ -156,12 +156,13 @@ export default function PublicCollections() {
|
|||
key={i}
|
||||
src={e.user.image ? e.user.image : undefined}
|
||||
className="-ml-3"
|
||||
name={e.user.name}
|
||||
/>
|
||||
);
|
||||
})
|
||||
.slice(0, 3)}
|
||||
{collection.members.length - 3 > 0 ? (
|
||||
<div className={`avatar placeholder -ml-3`}>
|
||||
<div className={`avatar drop-shadow-md placeholder -ml-3`}>
|
||||
<div className="bg-base-100 text-neutral rounded-full w-8 h-8 ring-2 ring-neutral-content">
|
||||
<span>+{collection.members.length - 3}</span>
|
||||
</div>
|
||||
|
@ -169,7 +170,7 @@ export default function PublicCollections() {
|
|||
) : null}
|
||||
</div>
|
||||
|
||||
<p className="text-neutral text-xs">
|
||||
<p className="text-neutral text-sm font-semibold">
|
||||
By {collectionOwner.name}
|
||||
{collection.members.length > 0
|
||||
? ` and ${collection.members.length} others`
|
||||
|
|
|
@ -239,14 +239,10 @@ export default function Index() {
|
|||
<Link
|
||||
key={i}
|
||||
href={"/public/collections/20?q=" + e.name}
|
||||
className="z-10"
|
||||
title={e.name}
|
||||
className="z-10 btn btn-xs btn-ghost truncate max-w-[19rem]"
|
||||
>
|
||||
<p
|
||||
title={e.name}
|
||||
className="btn btn-xs btn-outline truncate max-w-[19rem]"
|
||||
>
|
||||
{e.name}
|
||||
</p>
|
||||
#{e.name}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
@ -99,46 +99,12 @@ export default function Appearance() {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="flex items-center gap-2 w-full rounded-md h-8">
|
||||
<p className="truncate w-full pr-7 text-3xl font-thin">Link Card</p>
|
||||
</div>
|
||||
|
||||
<div className="divider my-3"></div>
|
||||
|
||||
<Checkbox
|
||||
label="Display Icons"
|
||||
state={user.displayLinkIcons}
|
||||
onClick={() =>
|
||||
setUser({ ...user, displayLinkIcons: !user.displayLinkIcons })
|
||||
}
|
||||
/>
|
||||
{user.displayLinkIcons ? (
|
||||
<Checkbox
|
||||
label="Blurred"
|
||||
className="pl-5 mt-1"
|
||||
state={user.blurredFavicons}
|
||||
onClick={() =>
|
||||
setUser({ ...user, blurredFavicons: !user.blurredFavicons })
|
||||
}
|
||||
/>
|
||||
) : undefined}
|
||||
<p className="my-3">Preview:</p>
|
||||
|
||||
<LinkPreview
|
||||
settings={{
|
||||
blurredFavicons: user.blurredFavicons,
|
||||
displayLinkIcons: user.displayLinkIcons,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<SubmitButton
|
||||
{/* <SubmitButton
|
||||
onClick={submit}
|
||||
loading={submitLoader}
|
||||
label="Save"
|
||||
className="mt-2 mx-auto lg:mx-0"
|
||||
/>
|
||||
/> */}
|
||||
</div>
|
||||
</SettingsLayout>
|
||||
);
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the column `blurredFavicons` on the `User` table. All the data in the column will be lost.
|
||||
- You are about to drop the column `displayLinkIcons` on the `User` table. All the data in the column will be lost.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE "User" DROP COLUMN "blurredFavicons",
|
||||
DROP COLUMN "displayLinkIcons";
|
|
@ -45,8 +45,6 @@ model User {
|
|||
archiveAsPDF Boolean @default(true)
|
||||
archiveAsWaybackMachine Boolean @default(false)
|
||||
isPrivate Boolean @default(false)
|
||||
displayLinkIcons Boolean @default(true)
|
||||
blurredFavicons Boolean @default(false)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt @default(now())
|
||||
}
|
||||
|
|
|
@ -93,6 +93,8 @@ const useLinkStore = create<LinkStore>()((set) => ({
|
|||
};
|
||||
}
|
||||
});
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
return { ok: response.ok, data: data.response };
|
||||
|
|
|
@ -286,3 +286,7 @@ body {
|
|||
background-position: 0% 50%;
|
||||
}
|
||||
}
|
||||
|
||||
.custom-file-input::file-selector-button {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
|
|
@ -6,9 +6,9 @@ module.exports = {
|
|||
themes: [
|
||||
{
|
||||
light: {
|
||||
primary: "#0284c7",
|
||||
primary: "#0369a1",
|
||||
secondary: "#0891b2",
|
||||
accent: "#6366f1",
|
||||
accent: "#6d28d9",
|
||||
neutral: "#6b7280",
|
||||
"neutral-content": "#d1d5db",
|
||||
"base-100": "#ffffff",
|
||||
|
@ -22,9 +22,9 @@ module.exports = {
|
|||
},
|
||||
{
|
||||
dark: {
|
||||
primary: "#38bdf8",
|
||||
primary: "#7dd3fc",
|
||||
secondary: "#22d3ee",
|
||||
accent: "#6366f1",
|
||||
accent: "#6d28d9",
|
||||
neutral: "#9ca3af",
|
||||
"neutral-content": "#404040",
|
||||
"base-100": "#171717",
|
||||
|
|
|
@ -9,6 +9,7 @@ declare global {
|
|||
STORAGE_FOLDER?: string;
|
||||
AUTOSCROLL_TIMEOUT?: string;
|
||||
RE_ARCHIVE_LIMIT?: string;
|
||||
NEXT_PUBLIC_MAX_UPLOAD_SIZE?: string;
|
||||
|
||||
SPACES_KEY?: string;
|
||||
SPACES_SECRET?: string;
|
||||
|
|
34
yarn.lock
34
yarn.lock
|
@ -1497,6 +1497,13 @@
|
|||
dependencies:
|
||||
"@types/trusted-types" "*"
|
||||
|
||||
"@types/formidable@^3.4.5":
|
||||
version "3.4.5"
|
||||
resolved "https://registry.yarnpkg.com/@types/formidable/-/formidable-3.4.5.tgz#8e45c053cac5868e2b71cc7410e2bd92872f6b9c"
|
||||
integrity sha512-s7YPsNVfnsng5L8sKnG/Gbb2tiwwJTY1conOkJzTMRvJAlLFW1nEua+ADsJQu8N1c0oTHx9+d5nqg10WuT9gHQ==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/jsdom@^21.1.3":
|
||||
version "21.1.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/jsdom/-/jsdom-21.1.3.tgz#a88c5dc65703e1b10b2a7839c12db49662b43ff0"
|
||||
|
@ -1766,6 +1773,11 @@ array.prototype.tosorted@^1.1.1:
|
|||
es-shim-unscopables "^1.0.0"
|
||||
get-intrinsic "^1.1.3"
|
||||
|
||||
asap@^2.0.0:
|
||||
version "2.0.6"
|
||||
resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46"
|
||||
integrity sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==
|
||||
|
||||
asn1@~0.2.3:
|
||||
version "0.2.6"
|
||||
resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.6.tgz#0d3a7bb6e64e02a90c0303b31f292868ea09a08d"
|
||||
|
@ -2299,6 +2311,14 @@ detect-libc@^2.0.0, detect-libc@^2.0.1:
|
|||
resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.1.tgz#e1897aa88fa6ad197862937fbc0441ef352ee0cd"
|
||||
integrity sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==
|
||||
|
||||
dezalgo@^1.0.4:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/dezalgo/-/dezalgo-1.0.4.tgz#751235260469084c132157dfa857f386d4c33d81"
|
||||
integrity sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==
|
||||
dependencies:
|
||||
asap "^2.0.0"
|
||||
wrappy "1"
|
||||
|
||||
didyoumean@^1.2.2:
|
||||
version "1.2.2"
|
||||
resolved "https://registry.yarnpkg.com/didyoumean/-/didyoumean-1.2.2.tgz#989346ffe9e839b4555ecf5666edea0d3e8ad037"
|
||||
|
@ -2836,6 +2856,15 @@ form-data@~2.3.2:
|
|||
combined-stream "^1.0.6"
|
||||
mime-types "^2.1.12"
|
||||
|
||||
formidable@^3.5.1:
|
||||
version "3.5.1"
|
||||
resolved "https://registry.yarnpkg.com/formidable/-/formidable-3.5.1.tgz#9360a23a656f261207868b1484624c4c8d06ee1a"
|
||||
integrity sha512-WJWKelbRHN41m5dumb0/k8TeAx7Id/y3a+Z7QfhxP/htI9Js5zYaEDtG8uMgG0vM0lOlqnmjE99/kfpOYi/0Og==
|
||||
dependencies:
|
||||
dezalgo "^1.0.4"
|
||||
hexoid "^1.0.0"
|
||||
once "^1.4.0"
|
||||
|
||||
fraction.js@^4.2.0:
|
||||
version "4.2.0"
|
||||
resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.2.0.tgz#448e5109a313a3527f5a3ab2119ec4cf0e0e2950"
|
||||
|
@ -3151,6 +3180,11 @@ has@^1.0.3:
|
|||
dependencies:
|
||||
function-bind "^1.1.1"
|
||||
|
||||
hexoid@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/hexoid/-/hexoid-1.0.0.tgz#ad10c6573fb907de23d9ec63a711267d9dc9bc18"
|
||||
integrity sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g==
|
||||
|
||||
hoist-non-react-statics@^3.3.1:
|
||||
version "3.3.2"
|
||||
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
|
||||
|
|
Ŝarĝante…
Reference in New Issue