added details drawer

This commit is contained in:
daniel31x13 2024-08-18 02:55:59 -04:00
parent a40026040c
commit c18a5f4162
25 changed files with 881 additions and 310 deletions

36
components/CopyButton.tsx Normal file
View File

@ -0,0 +1,36 @@
import React from "react";
type Props = {
text: string;
};
const CopyButton = ({ text }: Props) => {
return (
<div
className="bi-copy text-xl text-neutral btn btn-sm btn-square btn-ghost"
onClick={() => {
try {
navigator.clipboard.writeText(text).then(() => {
const copyIcon = document.querySelector(".bi-copy");
if (copyIcon) {
copyIcon.classList.remove("bi-copy");
copyIcon.classList.add("bi-check2");
copyIcon.classList.add("text-success");
}
setTimeout(() => {
if (copyIcon) {
copyIcon.classList.remove("bi-check2");
copyIcon.classList.remove("text-success");
copyIcon.classList.add("bi-copy");
}
}, 1000);
});
} catch (err) {
console.log(err);
}
}}
></div>
);
};
export default CopyButton;

View File

@ -14,7 +14,12 @@ export default function dashboardItem({
</div>
<div className="ml-4 flex flex-col justify-center">
<p className="text-neutral text-xs tracking-wider">{name}</p>
<p className="font-thin text-5xl text-primary mt-0.5">{value}</p>
<p className="font-thin text-5xl text-primary mt-0.5 hidden sm:block md:hidden">
{value < 1000 ? value : (value / 1000).toFixed(1) + "k"}
</p>
<p className="font-thin text-5xl text-primary mt-0.5 sm:hidden md:block">
{value}
</p>
</div>
</div>
);

88
components/Drawer.tsx Normal file
View File

@ -0,0 +1,88 @@
import React, { ReactNode, useEffect } from "react";
import ClickAwayHandler from "@/components/ClickAwayHandler";
import { Drawer as D } from "vaul";
type Props = {
toggleDrawer: Function;
children: ReactNode;
className?: string;
dismissible?: boolean;
};
export default function Drawer({
toggleDrawer,
className,
children,
dismissible = true,
}: Props) {
const [drawerIsOpen, setDrawerIsOpen] = React.useState(true);
useEffect(() => {
if (window.innerWidth >= 640) {
document.body.style.overflow = "hidden";
document.body.style.position = "relative";
return () => {
document.body.style.overflow = "auto";
document.body.style.position = "";
};
}
}, []);
if (window.innerWidth < 640) {
return (
<D.Root
open={drawerIsOpen}
onClose={() => dismissible && setTimeout(() => toggleDrawer(), 350)}
dismissible={dismissible}
>
<D.Portal>
<D.Overlay className="fixed inset-0 bg-black/40" />
<ClickAwayHandler
onClickOutside={() => dismissible && setDrawerIsOpen(false)}
>
<D.Content className="flex flex-col rounded-t-2xl min-h-max mt-24 fixed bottom-0 left-0 right-0 z-30 h-[90%]">
<div
className="p-4 bg-base-100 rounded-t-2xl flex-1 border-neutral-content border-t overflow-y-auto"
data-testid="mobile-modal-container"
>
<div
className="mx-auto w-12 h-1.5 flex-shrink-0 rounded-full bg-neutral mb-5"
data-testid="mobile-modal-slider"
/>
{children}
</div>
</D.Content>
</ClickAwayHandler>
</D.Portal>
</D.Root>
);
} else {
return (
<D.Root
open={drawerIsOpen}
onClose={() => dismissible && setTimeout(() => toggleDrawer(), 350)}
dismissible={dismissible}
direction="right"
>
<D.Portal>
<D.Overlay className="fixed inset-0 bg-black/10 z-20" />
<ClickAwayHandler
onClickOutside={() => dismissible && setDrawerIsOpen(false)}
className="z-30"
>
<D.Content className="bg-white flex flex-col h-full w-2/5 min-w-[30rem] mt-24 fixed bottom-0 right-0 z-40 !select-auto">
<div
className={
"p-4 bg-base-100 flex-1 border-neutral-content border-l overflow-y-auto " +
className
}
>
{children}
</div>
</D.Content>
</ClickAwayHandler>
</D.Portal>
</D.Root>
);
}
}

302
components/LinkDetails.tsx Normal file
View File

@ -0,0 +1,302 @@
import React, { useEffect, useState } from "react";
import {
LinkIncludingShortenedCollectionAndTags,
ArchivedFormat,
} from "@/types/global";
import Link from "next/link";
import { useSession } from "next-auth/react";
import {
pdfAvailable,
readabilityAvailable,
monolithAvailable,
screenshotAvailable,
} from "@/lib/shared/getArchiveValidity";
import PreservedFormatRow from "@/components/PreserverdFormatRow";
import getPublicUserData from "@/lib/client/getPublicUserData";
import { useTranslation } from "next-i18next";
import { BeatLoader } from "react-spinners";
import { useUser } from "@/hooks/store/user";
import { useGetLink } from "@/hooks/store/links";
import LinkIcon from "./LinkViews/LinkComponents/LinkIcon";
import CopyButton from "./CopyButton";
import { useRouter } from "next/router";
type Props = {
className?: string;
link: LinkIncludingShortenedCollectionAndTags;
};
export default function LinkDetails({ className, link }: Props) {
const { t } = useTranslation();
const session = useSession();
const getLink = useGetLink();
const { data: user = {} } = useUser();
const [collectionOwner, setCollectionOwner] = useState({
id: null as unknown as number,
name: "",
username: "",
image: "",
archiveAsScreenshot: undefined as unknown as boolean,
archiveAsMonolith: undefined as unknown as boolean,
archiveAsPDF: undefined as unknown as boolean,
});
useEffect(() => {
const fetchOwner = async () => {
if (link.collection.ownerId !== user.id) {
const owner = await getPublicUserData(
link.collection.ownerId as number
);
setCollectionOwner(owner);
} else if (link.collection.ownerId === user.id) {
setCollectionOwner({
id: user.id as number,
name: user.name,
username: user.username as string,
image: user.image as string,
archiveAsScreenshot: user.archiveAsScreenshot as boolean,
archiveAsMonolith: user.archiveAsScreenshot as boolean,
archiveAsPDF: user.archiveAsPDF as boolean,
});
}
};
fetchOwner();
}, [link.collection.ownerId]);
const isReady = () => {
return (
link &&
(collectionOwner.archiveAsScreenshot === true
? link.pdf && link.pdf !== "pending"
: true) &&
(collectionOwner.archiveAsMonolith === true
? link.monolith && link.monolith !== "pending"
: true) &&
(collectionOwner.archiveAsPDF === true
? link.pdf && link.pdf !== "pending"
: true) &&
link.readable &&
link.readable !== "pending"
);
};
const atLeastOneFormatAvailable = () => {
return (
screenshotAvailable(link) ||
pdfAvailable(link) ||
readabilityAvailable(link) ||
monolithAvailable(link)
);
};
useEffect(() => {
(async () => {
await getLink.mutateAsync({
id: link.id as number,
});
})();
let interval: any;
if (!isReady()) {
interval = setInterval(async () => {
await getLink.mutateAsync({
id: link.id as number,
});
}, 5000);
} else {
if (interval) {
clearInterval(interval);
}
}
return () => {
if (interval) {
clearInterval(interval);
}
};
}, [link?.monolith]);
const router = useRouter();
const isPublicRoute = router.pathname.startsWith("/public") ? true : false;
return (
<div className={className} data-vaul-no-drag>
<LinkIcon link={link} className="mx-auto" />
{link.name && <p className="text-xl text-center mt-2">{link.name}</p>}
{link.url && (
<>
<br />
<p className="text-sm mb-2 text-neutral">{t("link")}</p>
<div className="rounded-lg p-2 bg-base-200 flex justify-between items-center gap-2">
<Link
href={link.url}
title={link.url}
target="_blank"
className="truncate"
>
{link.url}
</Link>
<CopyButton text={link.url} />
</div>
</>
)}
<br />
<p className="text-sm mb-2 text-neutral">{t("collection")}</p>
<Link
href={
isPublicRoute
? `/public/collections/${link.collection.id}`
: `/collections/${link.collection.id}`
}
className="rounded-lg p-2 bg-base-200 flex justify-between items-center gap-2"
>
<p className="truncate">{link.collection.name}</p>
<i
className="bi-folder-fill text-xl"
style={{ color: link.collection.color }}
></i>
</Link>
{link.tags[0] && (
<>
<br />
<div>
<p className="text-sm mb-2 text-neutral">{t("tags")}</p>
</div>
<div className="flex gap-2">
{link.tags.map((tag) =>
isPublicRoute ? (
<div key={tag.id} className="rounded-lg px-3 py-1 bg-base-200">
{tag.name}
</div>
) : (
<Link
href={"/tags/" + tag.id}
key={tag.id}
className="rounded-lg px-3 py-1 bg-base-200"
>
{tag.name}
</Link>
)
)}
</div>
</>
)}
{link.description && (
<>
<br />
<div>
<p className="text-sm mb-2 text-neutral">{t("notes")}</p>
<div className="rounded-lg p-2 bg-base-200 hyphens-auto">
<p>{link.description}</p>
</div>
</div>
</>
)}
<br />
<p className="text-sm mb-2 text-neutral" title={t("available_formats")}>
{link.url ? t("preserved_formats") : t("file")}
</p>
<div className={`flex flex-col gap-3`}>
{monolithAvailable(link) ? (
<PreservedFormatRow
name={t("webpage")}
icon={"bi-filetype-html"}
format={ArchivedFormat.monolith}
link={link}
downloadable={true}
/>
) : undefined}
{screenshotAvailable(link) ? (
<PreservedFormatRow
name={t("screenshot")}
icon={"bi-file-earmark-image"}
format={
link?.image?.endsWith("png")
? ArchivedFormat.png
: ArchivedFormat.jpeg
}
link={link}
downloadable={true}
/>
) : undefined}
{pdfAvailable(link) ? (
<PreservedFormatRow
name={t("pdf")}
icon={"bi-file-earmark-pdf"}
format={ArchivedFormat.pdf}
link={link}
downloadable={true}
/>
) : undefined}
{readabilityAvailable(link) ? (
<PreservedFormatRow
name={t("readable")}
icon={"bi-file-earmark-text"}
format={ArchivedFormat.readability}
link={link}
/>
) : undefined}
{!isReady() && !atLeastOneFormatAvailable() ? (
<div className={`w-full h-full flex flex-col justify-center p-10`}>
<BeatLoader
color="oklch(var(--p))"
className="mx-auto mb-3"
size={30}
/>
<p className="text-center text-2xl">{t("preservation_in_queue")}</p>
<p className="text-center text-lg">{t("check_back_later")}</p>
</div>
) : link.url && !isReady() && atLeastOneFormatAvailable() ? (
<div className={`w-full h-full flex flex-col justify-center p-5`}>
<BeatLoader
color="oklch(var(--p))"
className="mx-auto mb-3"
size={20}
/>
<p className="text-center">{t("there_are_more_formats")}</p>
<p className="text-center text-sm">{t("check_back_later")}</p>
</div>
) : undefined}
{link.url && (
<Link
href={`https://web.archive.org/web/${link?.url?.replace(
/(^\w+:|^)\/\//,
""
)}`}
target="_blank"
className="text-neutral mx-auto duration-100 hover:opacity-60 flex gap-2 w-1/2 justify-center items-center text-sm"
>
<p className="whitespace-nowrap">{t("view_latest_snapshot")}</p>
<i className="bi-box-arrow-up-right" />
</Link>
)}
</div>
</div>
);
}

View File

@ -12,22 +12,20 @@ import { useTranslation } from "next-i18next";
import { useUser } from "@/hooks/store/user";
import { useDeleteLink, useUpdateLink } from "@/hooks/store/links";
import toast from "react-hot-toast";
import LinkDetailModal from "@/components/ModalContent/LinkDetailModal";
import { useRouter } from "next/router";
type Props = {
link: LinkIncludingShortenedCollectionAndTags;
collection: CollectionIncludingMembersAndLinkCount;
position?: string;
toggleShowInfo?: () => void;
linkInfo?: boolean;
alignToTop?: boolean;
flipDropdown?: boolean;
};
export default function LinkActions({
link,
toggleShowInfo,
position,
linkInfo,
alignToTop,
flipDropdown,
}: Props) {
@ -36,6 +34,7 @@ export default function LinkActions({
const permissions = usePermissions(link.collection.id as number);
const [editLinkModal, setEditLinkModal] = useState(false);
const [linkDetailModal, setLinkDetailModal] = useState(false);
const [deleteLinkModal, setDeleteLinkModal] = useState(false);
const [preservedFormatsModal, setPreservedFormatsModal] = useState(false);
@ -70,8 +69,24 @@ export default function LinkActions({
);
};
const router = useRouter();
const isPublicRoute = router.pathname.startsWith("/public") ? true : false;
return (
<>
{isPublicRoute ? (
<div
className={`absolute ${position || "top-3 right-3"} ${
alignToTop ? "" : "dropdown-end"
} z-20`}
onClick={() => setLinkDetailModal(true)}
>
<div className="btn btn-ghost btn-sm btn-square text-neutral">
<i title="More" className="bi-three-dots text-xl" />
</div>
</div>
) : (
<div
className={`dropdown dropdown-left absolute ${
position || "top-3 right-3"
@ -90,6 +105,8 @@ export default function LinkActions({
alignToTop ? "" : "translate-y-10"
}`}
>
{permissions === true ||
(permissions?.canUpdate && (
<li>
<div
role="button"
@ -105,21 +122,20 @@ export default function LinkActions({
: t("pin_to_dashboard")}
</div>
</li>
{linkInfo !== undefined && toggleShowInfo ? (
))}
<li>
<div
role="button"
tabIndex={0}
onClick={() => {
(document?.activeElement as HTMLElement)?.blur();
toggleShowInfo();
setLinkDetailModal(true);
}}
className="whitespace-nowrap"
>
{!linkInfo ? t("show_link_details") : t("hide_link_details")}
{t("show_link_details")}
</div>
</li>
) : undefined}
{permissions === true || permissions?.canUpdate ? (
<li>
<div
@ -183,6 +199,7 @@ export default function LinkActions({
) : undefined}
</ul>
</div>
)}
{editLinkModal ? (
<EditLinkModal
@ -202,9 +219,13 @@ export default function LinkActions({
link={link}
/>
) : undefined}
{/* {expandedLink ? (
<ExpandedLink onClose={() => setExpandedLink(false)} link={link} />
) : undefined} */}
{linkDetailModal ? (
<LinkDetailModal
onClose={() => setLinkDetailModal(false)}
onEdit={() => setEditLinkModal(true)}
link={link}
/>
) : undefined}
</>
);
}

View File

@ -22,6 +22,7 @@ import { useTranslation } from "next-i18next";
import { useCollections } from "@/hooks/store/collections";
import { useUser } from "@/hooks/store/user";
import { useGetLink, useLinks } from "@/hooks/store/links";
import { useRouter } from "next/router";
type Props = {
link: LinkIncludingShortenedCollectionAndTags;
@ -90,6 +91,10 @@ export default function LinkCard({ link, flipDropdown, editMode }: Props) {
const isVisible = useOnScreen(ref);
const permissions = usePermissions(collection?.id as number);
const router = useRouter();
let isPublic = router.pathname.startsWith("/public") ? true : undefined;
useEffect(() => {
let interval: any;
@ -99,7 +104,7 @@ export default function LinkCard({ link, flipDropdown, editMode }: Props) {
link.preview !== "unavailable"
) {
interval = setInterval(async () => {
getLink.mutateAsync(link.id as number);
getLink.mutateAsync({ id: link.id as number, isPublicRoute: isPublic });
}, 5000);
}
@ -110,8 +115,6 @@ export default function LinkCard({ link, flipDropdown, editMode }: Props) {
};
}, [isVisible, link.preview]);
const [showInfo, setShowInfo] = useState(false);
const selectedStyle = selectedLinks.some(
(selectedLink) => selectedLink.id === link.id
)
@ -196,63 +199,10 @@ export default function LinkCard({ link, flipDropdown, editMode }: Props) {
</div>
</div>
{showInfo && (
<div className="p-3 absolute z-30 top-0 left-0 right-0 bottom-0 bg-base-200 rounded-[0.9rem] fade-in overflow-y-auto">
<div
onClick={() => setShowInfo(!showInfo)}
className=" float-right btn btn-sm outline-none btn-circle btn-ghost z-10"
>
<i className="bi-x text-neutral text-2xl"></i>
</div>
<p className="text-neutral text-lg font-semibold">
{t("description")}
</p>
<hr className="divider my-2 border-t border-neutral-content h-[1px]" />
<p>
{link.description ? (
unescapeString(link.description)
) : (
<span className="text-neutral text-sm">
{t("no_description")}
</span>
)}
</p>
{link.tags && link.tags[0] && (
<>
<p className="text-neutral text-lg mt-3 font-semibold">
{t("tags")}
</p>
<hr className="divider my-2 border-t border-neutral-content h-[1px]" />
<div className="flex gap-3 items-center flex-wrap mt-2 truncate relative">
<div className="flex gap-1 items-center flex-wrap">
{link.tags.map((e, i) => (
<Link
href={"/tags/" + e.id}
key={i}
onClick={(e) => {
e.stopPropagation();
}}
className="btn btn-xs btn-ghost truncate max-w-[19rem]"
>
#{e.name}
</Link>
))}
</div>
</div>
</>
)}
</div>
)}
<LinkActions
link={link}
collection={collection}
position="top-[10.75rem] right-3"
toggleShowInfo={() => setShowInfo(!showInfo)}
linkInfo={showInfo}
flipDropdown={flipDropdown}
/>
</div>

View File

@ -80,8 +80,6 @@ export default function LinkCardCompact({
const permissions = usePermissions(collection?.id as number);
const [showInfo, setShowInfo] = useState(false);
const selectedStyle = selectedLinks.some(
(selectedLink) => selectedLink.id === link.id
)
@ -96,7 +94,7 @@ export default function LinkCardCompact({
<>
<div
className={`${selectedStyle} border relative items-center flex ${
!showInfo && !isPWA() ? "hover:bg-base-300 p-3" : "py-3"
!isPWA() ? "hover:bg-base-300 p-3" : "py-3"
} duration-200 rounded-lg w-full`}
onClick={() =>
selectable
@ -106,20 +104,6 @@ export default function LinkCardCompact({
: undefined
}
>
{/* {showCheckbox &&
editMode &&
(permissions === true ||
permissions?.canCreate ||
permissions?.canDelete) && (
<input
type="checkbox"
className="checkbox checkbox-primary my-auto mr-2"
checked={selectedLinks.some(
(selectedLink) => selectedLink.id === link.id
)}
onChange={() => handleCheckboxClick(link)}
/>
)} */}
<div
className="flex items-center cursor-pointer w-full"
onClick={() =>
@ -157,8 +141,6 @@ export default function LinkCardCompact({
collection={collection}
position="top-3 right-3"
flipDropdown={flipDropdown}
// toggleShowInfo={() => setShowInfo(!showInfo)}
// linkInfo={showInfo}
/>
</div>
<div

View File

@ -96,7 +96,7 @@ export default function LinkMasonry({ link, flipDropdown, editMode }: Props) {
link.preview !== "unavailable"
) {
interval = setInterval(async () => {
getLink.mutateAsync(link.id as number);
getLink.mutateAsync({ id: link.id as number });
}, 5000);
}
@ -107,8 +107,6 @@ export default function LinkMasonry({ link, flipDropdown, editMode }: Props) {
};
}, [isVisible, link.preview]);
const [showInfo, setShowInfo] = useState(false);
const selectedStyle = selectedLinks.some(
(selectedLink) => selectedLink.id === link.id
)
@ -207,57 +205,6 @@ export default function LinkMasonry({ link, flipDropdown, editMode }: Props) {
</div>
</div>
{showInfo && (
<div className="p-3 absolute z-30 top-0 left-0 right-0 bottom-0 bg-base-200 rounded-2xl fade-in overflow-y-auto">
<div
onClick={() => setShowInfo(!showInfo)}
className=" float-right btn btn-sm outline-none btn-circle btn-ghost z-10"
>
<i className="bi-x text-neutral text-2xl"></i>
</div>
<p className="text-neutral text-lg font-semibold">
{t("description")}
</p>
<hr className="divider my-2 last:hidden border-t border-neutral-content h-[1px]" />
<p>
{link.description ? (
unescapeString(link.description)
) : (
<span className="text-neutral text-sm">
{t("no_description")}
</span>
)}
</p>
{link.tags && link.tags[0] && (
<>
<p className="text-neutral text-lg mt-3 font-semibold">
{t("tags")}
</p>
<hr className="divider my-2 last:hidden border-t border-neutral-content h-[1px]" />
<div className="flex gap-3 items-center flex-wrap mt-2 truncate relative">
<div className="flex gap-1 items-center flex-wrap">
{link.tags.map((e, i) => (
<Link
href={"/tags/" + e.id}
key={i}
onClick={(e) => {
e.stopPropagation();
}}
className="btn btn-xs btn-ghost truncate max-w-[19rem]"
>
#{e.name}
</Link>
))}
</div>
</div>
</>
)}
</div>
)}
<LinkActions
link={link}
collection={collection}
@ -266,8 +213,6 @@ export default function LinkMasonry({ link, flipDropdown, editMode }: Props) {
? "top-[10.75rem] right-3"
: "top-[.75rem] right-3"
}
toggleShowInfo={() => setShowInfo(!showInfo)}
linkInfo={showInfo}
flipDropdown={flipDropdown}
/>
</div>

View File

@ -32,7 +32,7 @@ export default function Modal({
return (
<Drawer.Root
open={drawerIsOpen}
onClose={() => dismissible && setTimeout(() => toggleModal(), 100)}
onClose={() => dismissible && setTimeout(() => toggleModal(), 350)}
dismissible={dismissible}
>
<Drawer.Portal>

View File

@ -11,6 +11,7 @@ import { dropdownTriggerer } from "@/lib/client/utils";
import { useTranslation } from "next-i18next";
import { useUpdateCollection } from "@/hooks/store/collections";
import { useUser } from "@/hooks/store/user";
import CopyButton from "../CopyButton";
type Props = {
onClose: Function;
@ -133,21 +134,11 @@ export default function EditCollectionSharingModal({
)}
{collection.isPublic ? (
<div className={permissions === true ? "pl-5" : ""}>
<div>
<p className="mb-2">{t("sharable_link_guide")}</p>
<div
onClick={() => {
try {
navigator.clipboard
.writeText(publicCollectionURL)
.then(() => toast.success(t("copied")));
} catch (err) {
console.log(err);
}
}}
className="w-full hide-scrollbar overflow-x-auto whitespace-nowrap rounded-md p-2 bg-base-200 border-neutral-content border-solid border outline-none hover:border-primary dark:hover:border-primary duration-100 cursor-text"
>
<div className="w-full hide-scrollbar overflow-x-auto whitespace-nowrap rounded-md p-2 bg-base-200 border-neutral-content border-solid border flex items-center gap-2 justify-between">
{publicCollectionURL}
<CopyButton text={publicCollectionURL} />
</div>
</div>
) : null}

View File

@ -0,0 +1,145 @@
import React, { useEffect, useState } from "react";
import { LinkIncludingShortenedCollectionAndTags } from "@/types/global";
import getPublicUserData from "@/lib/client/getPublicUserData";
import { useTranslation } from "next-i18next";
import { useUser } from "@/hooks/store/user";
import { useGetLink } from "@/hooks/store/links";
import Drawer from "../Drawer";
import LinkDetails from "../LinkDetails";
import Link from "next/link";
import usePermissions from "@/hooks/usePermissions";
import { useRouter } from "next/router";
type Props = {
onClose: Function;
onEdit: Function;
link: LinkIncludingShortenedCollectionAndTags;
};
export default function LinkDetailModal({ onClose, onEdit, link }: Props) {
const { t } = useTranslation();
const getLink = useGetLink();
const { data: user = {} } = useUser();
const [collectionOwner, setCollectionOwner] = useState({
id: null as unknown as number,
name: "",
username: "",
image: "",
archiveAsScreenshot: undefined as unknown as boolean,
archiveAsMonolith: undefined as unknown as boolean,
archiveAsPDF: undefined as unknown as boolean,
});
useEffect(() => {
const fetchOwner = async () => {
if (link.collection.ownerId !== user.id) {
const owner = await getPublicUserData(
link.collection.ownerId as number
);
setCollectionOwner(owner);
} else if (link.collection.ownerId === user.id) {
setCollectionOwner({
id: user.id as number,
name: user.name,
username: user.username as string,
image: user.image as string,
archiveAsScreenshot: user.archiveAsScreenshot as boolean,
archiveAsMonolith: user.archiveAsScreenshot as boolean,
archiveAsPDF: user.archiveAsPDF as boolean,
});
}
};
fetchOwner();
}, [link.collection.ownerId]);
const isReady = () => {
return (
link &&
(collectionOwner.archiveAsScreenshot === true
? link.pdf && link.pdf !== "pending"
: true) &&
(collectionOwner.archiveAsMonolith === true
? link.monolith && link.monolith !== "pending"
: true) &&
(collectionOwner.archiveAsPDF === true
? link.pdf && link.pdf !== "pending"
: true) &&
link.readable &&
link.readable !== "pending"
);
};
useEffect(() => {
(async () => {
await getLink.mutateAsync({
id: link.id as number,
});
})();
let interval: any;
if (!isReady()) {
interval = setInterval(async () => {
await getLink.mutateAsync({
id: link.id as number,
});
}, 5000);
} else {
if (interval) {
clearInterval(interval);
}
}
return () => {
if (interval) {
clearInterval(interval);
}
};
}, [link?.monolith]);
const permissions = usePermissions(link.collection.id as number);
const router = useRouter();
const isPublicRoute = router.pathname.startsWith("/public") ? true : false;
return (
<Drawer toggleDrawer={onClose} className="sm:h-screen sm:flex relative">
<div
className="bi-x text-xl text-neutral btn btn-sm btn-square btn-ghost hidden sm:block absolute top-3 left-3"
onClick={() => onClose()}
></div>
<Link
href={isPublicRoute ? `/public/links/${link.id}` : `/links/${link.id}`}
target="_blank"
className="bi-box-arrow-up-right text-xl text-neutral btn btn-sm btn-square btn-ghost absolute top-3 right-3 select-none"
></Link>
<div className="sm:m-auto sm:w-5/6">
<LinkDetails link={link} />
{permissions === true ||
(permissions?.canUpdate && (
<>
<br />
<br />
<div className="mx-auto text-center">
<div
className="btn btn-sm btn-ghost"
onClick={() => {
onEdit();
onClose();
}}
>
{t("edit_link")}
</div>
</div>
</>
))}
</div>
</Drawer>
);
}

View File

@ -61,7 +61,7 @@ export default function NewLinkModal({ onClose }: Props) {
};
useEffect(() => {
if (router.query.id) {
if (router.pathname.startsWith("/collections/") && router.query.id) {
const currentCollection = collections.find(
(e) => e.id == Number(router.query.id)
);

View File

@ -96,14 +96,14 @@ export default function PreservedFormatsModal({ onClose, link }: Props) {
useEffect(() => {
(async () => {
await getLink.mutateAsync(link.id as number);
await getLink.mutateAsync({ id: link.id as number });
})();
let interval: any;
if (!isReady()) {
interval = setInterval(async () => {
await getLink.mutateAsync(link.id as number);
await getLink.mutateAsync({ id: link.id as number });
}, 5000);
} else {
if (interval) {
@ -129,7 +129,7 @@ export default function PreservedFormatsModal({ onClose, link }: Props) {
toast.dismiss(load);
if (response.ok) {
await getLink.mutateAsync(link?.id as number);
await getLink.mutateAsync({ id: link.id as number });
toast.success(t("link_being_archived"));
} else toast.error(data.response);

View File

@ -70,7 +70,7 @@ export default function UploadFileModal({ onClose }: Props) {
useEffect(() => {
setOptionsExpanded(false);
if (router.query.id) {
if (router.pathname.startsWith("/collections/") && router.query.id) {
const currentCollection = collections.find(
(e) => e.id == Number(router.query.id)
);

View File

@ -52,11 +52,9 @@ export default function PreservedFormatRow({
};
return (
<div className="flex justify-between items-center pr-1 border border-neutral-content rounded-md">
<div className="flex justify-between items-center rounded-lg p-2 bg-base-200">
<div className="flex gap-2 items-center">
<div className="bg-primary text-primary-content p-2 rounded-l-md">
<i className={`${icon} text-2xl`} />
</div>
<i className={`${icon} text-2xl text-primary`} />
<p>{name}</p>
</div>
@ -64,7 +62,7 @@ export default function PreservedFormatRow({
{downloadable || false ? (
<div
onClick={() => handleDownload()}
className="btn btn-sm btn-square"
className="btn btn-sm btn-square btn-ghost"
>
<i className="bi-cloud-arrow-down text-xl text-neutral" />
</div>
@ -75,9 +73,9 @@ export default function PreservedFormatRow({
isPublic ? "/public" : ""
}/preserved/${link?.id}?format=${format}`}
target="_blank"
className="btn btn-sm btn-square"
className="btn btn-sm btn-square btn-ghost"
>
<i className="bi-box-arrow-up-right text-xl text-neutral" />
<i className="bi-box-arrow-up-right text-lg text-neutral" />
</Link>
</div>
</div>

View File

@ -46,13 +46,6 @@ export default function ReadableView({ link }: Props) {
const router = useRouter();
const getLink = useGetLink();
const { data: collections = [] } = useCollections();
const collection = useMemo(() => {
return collections.find(
(e) => e.id === link.collection.id
) as CollectionIncludingMembersAndLinkCount;
}, [collections, link]);
useEffect(() => {
const fetchLinkContent = async () => {
@ -73,7 +66,7 @@ export default function ReadableView({ link }: Props) {
}, [link]);
useEffect(() => {
if (link) getLink.mutateAsync(link?.id as number);
if (link) getLink.mutateAsync({ id: link.id as number });
let interval: any;
if (
@ -88,7 +81,10 @@ export default function ReadableView({ link }: Props) {
!link?.monolith)
) {
interval = setInterval(
() => getLink.mutateAsync(link.id as number),
() =>
getLink.mutateAsync({
id: link.id as number,
}),
5000
);
} else {
@ -243,13 +239,6 @@ export default function ReadableView({ link }: Props) {
{link?.name ? <p>{unescapeString(link?.description)}</p> : undefined}
</div>
<LinkActions
link={link}
collection={collection}
position="top-3 right-3"
alignToTop
/>
</div>
<div className="flex flex-col gap-5 h-full">

View File

@ -225,9 +225,21 @@ const useDeleteLink = () => {
const useGetLink = () => {
const queryClient = useQueryClient();
const router = useRouter();
return useMutation({
mutationFn: async (id: number) => {
const response = await fetch(`/api/v1/links/${id}`);
mutationFn: async ({
id,
isPublicRoute = router.pathname.startsWith("/public") ? true : undefined,
}: {
id: number;
isPublicRoute?: boolean;
}) => {
const path = isPublicRoute
? `/api/v1/public/links/${id}`
: `/api/v1/links/${id}`;
const response = await fetch(path);
const data = await response.json();
if (!response.ok) throw new Error(data.response);
@ -250,7 +262,20 @@ const useGetLink = () => {
};
});
queryClient.invalidateQueries({ queryKey: ["publicLinks"] });
queryClient.setQueriesData(
{ queryKey: ["publicLinks"] },
(oldData: any) => {
if (!oldData) return undefined;
return {
pages: oldData.pages.map((page: any) =>
page.map((item: any) => (item.id === data.id ? data : item))
),
pageParams: oldData.pageParams,
};
}
);
// queryClient.invalidateQueries({ queryKey: ["publicLinks"] });
},
});
};

View File

@ -79,7 +79,7 @@
"socks-proxy-agent": "^8.0.2",
"stripe": "^12.13.0",
"tailwind-merge": "^2.3.0",
"vaul": "^0.8.8",
"vaul": "^0.9.1",
"zustand": "^4.3.8"
},
"devDependencies": {
@ -95,7 +95,7 @@
"postcss": "^8.4.26",
"prettier": "3.1.1",
"prisma": "^4.16.2",
"tailwindcss": "^3.3.3",
"tailwindcss": "^3.4.10",
"ts-node": "^10.9.2",
"typescript": "4.9.4"
}

View File

@ -111,14 +111,14 @@ export default function Dashboard() {
</div>
<div>
<div className="flex justify-evenly flex-col xl:flex-row xl:items-center gap-2 xl:w-full h-full rounded-2xl p-8 border border-neutral-content bg-base-200">
<div className="flex justify-evenly flex-col items-start sm:flex-row sm:items-center gap-5 xl:w-full h-full rounded-2xl p-5 bg-base-200">
<DashboardItem
name={numberOfLinks === 1 ? t("link") : t("links")}
value={numberOfLinks}
icon={"bi-link-45deg"}
/>
<div className="divider xl:divider-horizontal"></div>
<div className="divider m-0"></div>
<DashboardItem
name={
@ -128,11 +128,11 @@ export default function Dashboard() {
icon={"bi-folder"}
/>
<div className="divider xl:divider-horizontal"></div>
<div className="divider m-0"></div>
<DashboardItem
name={tags.length === 1 ? t("tag") : t("tags")}
value={tags.length}
value={tags.length * numberOfLinks}
icon={"bi-hash"}
/>
</div>

41
pages/links/[id].tsx Normal file
View File

@ -0,0 +1,41 @@
import LinkDetails from "@/components/LinkDetails";
import { useGetLink } from "@/hooks/store/links";
import { useRouter } from "next/router";
import { useEffect, useState } from "react";
import getServerSideProps from "@/lib/client/getServerSideProps";
const Index = () => {
const router = useRouter();
const { id } = router.query;
useState;
const getLink = useGetLink();
useEffect(() => {
getLink.mutate({ id: Number(id) });
}, []);
return (
<div className="flex h-screen">
{getLink.data ? (
<LinkDetails
link={getLink.data}
className="mx-auto max-w-2xl p-5 m-auto w-full"
/>
) : (
<div className="mx-auto max-w-2xl p-5 m-auto w-full flex flex-col items-center gap-5">
<div className="w-20 h-20 skeleton rounded-xl"></div>
<div className="w-full h-10 skeleton rounded-xl"></div>
<div className="w-full h-10 skeleton rounded-xl"></div>
<div className="w-full h-10 skeleton rounded-xl"></div>
<div className="w-full h-10 skeleton rounded-xl"></div>
</div>
)}
</div>
);
};
export default Index;
export { getServerSideProps };

View File

@ -20,7 +20,7 @@ export default function Index() {
useEffect(() => {
const fetchLink = async () => {
if (router.query.id) {
await getLink.mutateAsync(Number(router.query.id));
await getLink.mutateAsync({ id: Number(router.query.id) });
}
};

View File

@ -0,0 +1,41 @@
import LinkDetails from "@/components/LinkDetails";
import { useGetLink } from "@/hooks/store/links";
import { useRouter } from "next/router";
import { useEffect, useState } from "react";
import getServerSideProps from "@/lib/client/getServerSideProps";
const Index = () => {
const router = useRouter();
const { id } = router.query;
useState;
const getLink = useGetLink();
useEffect(() => {
getLink.mutate({ id: Number(id) });
}, []);
return (
<div className="flex h-screen">
{getLink.data ? (
<LinkDetails
link={getLink.data}
className="mx-auto max-w-2xl p-5 m-auto w-full"
/>
) : (
<div className="mx-auto max-w-2xl p-5 m-auto w-full flex flex-col items-center gap-5">
<div className="w-20 h-20 skeleton rounded-xl"></div>
<div className="w-full h-10 skeleton rounded-xl"></div>
<div className="w-full h-10 skeleton rounded-xl"></div>
<div className="w-full h-10 skeleton rounded-xl"></div>
<div className="w-full h-10 skeleton rounded-xl"></div>
</div>
)}
</div>
);
};
export default Index;
export { getServerSideProps };

View File

@ -6,10 +6,9 @@ import {
} from "@/types/global";
import ReadableView from "@/components/ReadableView";
import getServerSideProps from "@/lib/client/getServerSideProps";
import { useGetLink, useLinks } from "@/hooks/store/links";
import { useGetLink } from "@/hooks/store/links";
export default function Index() {
const { links } = useLinks();
const getLink = useGetLink();
const [link, setLink] = useState<LinkIncludingShortenedCollectionAndTags>();
@ -19,18 +18,14 @@ export default function Index() {
useEffect(() => {
const fetchLink = async () => {
if (router.query.id) {
await getLink.mutateAsync(Number(router.query.id));
const get = await getLink.mutateAsync({ id: Number(router.query.id) });
setLink(get);
}
};
fetchLink();
}, []);
useEffect(() => {
if (links && links[0])
setLink(links.find((e) => e.id === Number(router.query.id)));
}, [links]);
return (
<div className="relative">
{/* <div className="fixed left-1/2 transform -translate-x-1/2 w-fit py-1 px-3 bg-base-200 border border-neutral-content rounded-md">
@ -39,6 +34,12 @@ export default function Index() {
{link && Number(router.query.format) === ArchivedFormat.readability && (
<ReadableView link={link} />
)}
{link && Number(router.query.format) === ArchivedFormat.monolith && (
<iframe
src={`/api/v1/archives/${link.id}?format=${ArchivedFormat.monolith}`}
className="w-full h-screen border-none"
></iframe>
)}
{link && Number(router.query.format) === ArchivedFormat.pdf && (
<iframe
src={`/api/v1/archives/${link.id}?format=${ArchivedFormat.pdf}`}

View File

@ -361,7 +361,6 @@
"unpin": "Unpin",
"pin_to_dashboard": "Pin to Dashboard",
"show_link_details": "Show Link Details",
"hide_link_details": "Hide Link Details",
"link_pinned": "Link Pinned!",
"link_unpinned": "Link Unpinned!",
"webpage": "Webpage",
@ -371,5 +370,6 @@
"demo_title": "Demo Only",
"demo_desc": "This is only a demo instance of Linkwarden and uploads are disabled.",
"demo_desc_2": "If you want to try out the full version, you can sign up for a free trial at:",
"demo_button": "Login as demo user"
"demo_button": "Login as demo user",
"notes": "Notes"
}

View File

@ -3379,7 +3379,7 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
fast-glob@^3.2.11, fast-glob@^3.2.12, fast-glob@^3.2.9:
fast-glob@^3.2.11, fast-glob@^3.2.9:
version "3.2.12"
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.12.tgz#7f39ec99c2e6ab030337142da9e0c18f37afae80"
integrity sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==
@ -3390,6 +3390,17 @@ fast-glob@^3.2.11, fast-glob@^3.2.12, fast-glob@^3.2.9:
merge2 "^1.3.0"
micromatch "^4.0.4"
fast-glob@^3.3.0:
version "3.3.2"
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129"
integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==
dependencies:
"@nodelib/fs.stat" "^2.0.2"
"@nodelib/fs.walk" "^1.2.3"
glob-parent "^5.1.2"
merge2 "^1.3.0"
micromatch "^4.0.4"
fast-json-stable-stringify@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633"
@ -4268,10 +4279,10 @@ jimp@^0.22.10:
"@jimp/types" "^0.22.10"
regenerator-runtime "^0.13.3"
jiti@^1.18.2:
version "1.18.2"
resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.18.2.tgz#80c3ef3d486ebf2450d9335122b32d121f2a83cd"
integrity sha512-QAdOptna2NYiSSpv0O/BwoHBSmz4YhpzJHyi+fnMRTXFjp7B8i/YG5Z8IfusxB1ufjcD2Sre1F3R+nX3fvy7gg==
jiti@^1.21.0:
version "1.21.6"
resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.21.6.tgz#6c7f7398dd4b3142767f9a168af2f317a428d268"
integrity sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==
jose@^4.11.1, jose@^4.11.4, jose@^4.14.1:
version "4.14.4"
@ -5876,20 +5887,20 @@ tailwind-merge@^2.3.0:
dependencies:
"@babel/runtime" "^7.24.1"
tailwindcss@^3.3.3:
version "3.3.3"
resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.3.3.tgz#90da807393a2859189e48e9e7000e6880a736daf"
integrity sha512-A0KgSkef7eE4Mf+nKJ83i75TMyq8HqY3qmFIJSWy8bNt0v1lG7jUcpGpoTFxAwYcWOphcTBLPPJg+bDfhDf52w==
tailwindcss@^3.4.10:
version "3.4.10"
resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.4.10.tgz#70442d9aeb78758d1f911af29af8255ecdb8ffef"
integrity sha512-KWZkVPm7yJRhdu4SRSl9d4AK2wM3a50UsvgHZO7xY77NQr2V+fIrEuoDGQcbvswWvFGbS2f6e+jC/6WJm1Dl0w==
dependencies:
"@alloc/quick-lru" "^5.2.0"
arg "^5.0.2"
chokidar "^3.5.3"
didyoumean "^1.2.2"
dlv "^1.1.3"
fast-glob "^3.2.12"
fast-glob "^3.3.0"
glob-parent "^6.0.2"
is-glob "^4.0.3"
jiti "^1.18.2"
jiti "^1.21.0"
lilconfig "^2.1.0"
micromatch "^4.0.5"
normalize-path "^3.0.0"
@ -6254,10 +6265,10 @@ v8-compile-cache-lib@^3.0.1:
resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf"
integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==
vaul@^0.8.8:
version "0.8.8"
resolved "https://registry.yarnpkg.com/vaul/-/vaul-0.8.8.tgz#c5edc041825fdeaddf0a89e326abcc7ac7449a2d"
integrity sha512-Z9K2b90M/LtY/sRyM1yfA8Y4mHC/5WIqhO2u7Byr49r5LQXkLGdVXiehsnjtws9CL+DyknwTuRMJXlCOHTqg/g==
vaul@^0.9.1:
version "0.9.1"
resolved "https://registry.yarnpkg.com/vaul/-/vaul-0.9.1.tgz#3640198e04636b209b1f907fcf3079bec6ecc66b"
integrity sha512-fAhd7i4RNMinx+WEm6pF3nOl78DFkAazcN04ElLPFF9BMCNGbY/kou8UMhIcicm0rJCNePJP0Yyza60gGOD0Jw==
dependencies:
"@radix-ui/react-dialog" "^1.0.4"