Merge pull request #718 from linkwarden/feat/customizable-links
Feat/customizable links
This commit is contained in:
commit
8c738d4a99
|
@ -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;
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
import { icons } from "@/lib/client/icons";
|
||||
import React, { useMemo, useState } from "react";
|
||||
import Fuse from "fuse.js";
|
||||
import TextInput from "./TextInput";
|
||||
|
||||
const fuse = new Fuse(icons, {
|
||||
keys: [{ name: "name", weight: 4 }, "tags", "categories"],
|
||||
threshold: 0.2,
|
||||
useExtendedSearch: true,
|
||||
});
|
||||
|
||||
type Props = {};
|
||||
|
||||
const IconPicker = (props: Props) => {
|
||||
const [query, setQuery] = useState("");
|
||||
|
||||
const filteredQueryResultsSelector = useMemo(() => {
|
||||
if (!query) {
|
||||
return icons;
|
||||
}
|
||||
return fuse.search(query).map((result) => result.item);
|
||||
}, [query]);
|
||||
|
||||
return (
|
||||
<div className="w-fit">
|
||||
<TextInput
|
||||
className="p-2 rounded w-full mb-5"
|
||||
placeholder="Search icons"
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
/>
|
||||
<div className="grid grid-cols-6 gap-5 w-fit">
|
||||
{filteredQueryResultsSelector.map((icon) => {
|
||||
const IconComponent = icon.Icon;
|
||||
return (
|
||||
<div key={icon.name} onClick={() => console.log(icon.name)}>
|
||||
<IconComponent size={32} weight="fill" />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default IconPicker;
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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)
|
||||
);
|
||||
|
|
|
@ -7,6 +7,7 @@ import { dropdownTriggerer } from "@/lib/client/utils";
|
|||
import Button from "../ui/Button";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import { useAddToken } from "@/hooks/store/tokens";
|
||||
import CopyButton from "../CopyButton";
|
||||
|
||||
type Props = {
|
||||
onClose: Function;
|
||||
|
@ -68,21 +69,14 @@ export default function NewTokenModal({ onClose }: Props) {
|
|||
<div className="flex flex-col justify-center space-y-4">
|
||||
<p className="text-xl font-thin">{t("access_token_created")}</p>
|
||||
<p>{t("token_creation_notice")}</p>
|
||||
<TextInput
|
||||
spellCheck={false}
|
||||
value={newToken}
|
||||
onChange={() => {}}
|
||||
className="w-full"
|
||||
/>
|
||||
<button
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(newToken);
|
||||
toast.success(t("copied_to_clipboard"));
|
||||
}}
|
||||
className="btn btn-primary w-fit mx-auto"
|
||||
>
|
||||
{t("copy_to_clipboard")}
|
||||
</button>
|
||||
<div className="relative">
|
||||
<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 pr-14">
|
||||
{newToken}
|
||||
<div className="absolute right-0 px-2 border-neutral-content border-solid border-r bg-base-200">
|
||||
<CopyButton text={newToken} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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)
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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"] });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
import * as Icons from "@phosphor-icons/react";
|
||||
import { icons as iconData } from "@phosphor-icons/core";
|
||||
import { IconEntry as CoreEntry } from "@phosphor-icons/core";
|
||||
|
||||
interface IconEntry extends CoreEntry {
|
||||
Icon: Icons.Icon;
|
||||
}
|
||||
|
||||
export const icons: ReadonlyArray<IconEntry> = iconData.map((entry) => ({
|
||||
...entry,
|
||||
Icon: Icons[entry.pascal_name as keyof typeof Icons] as Icons.Icon,
|
||||
}));
|
||||
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
console.log(`${icons.length} icons`);
|
||||
}
|
||||
|
||||
export const iconCount = Intl.NumberFormat("en-US").format(icons.length * 6);
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "linkwarden",
|
||||
"version": "v2.7.1",
|
||||
"version": "v2.8.0",
|
||||
"main": "index.js",
|
||||
"repository": "https://github.com/linkwarden/linkwarden.git",
|
||||
"author": "Daniel31X13 <daniel31x13@gmail.com>",
|
||||
|
@ -25,6 +25,8 @@
|
|||
"@aws-sdk/client-s3": "^3.379.1",
|
||||
"@headlessui/react": "^1.7.15",
|
||||
"@mozilla/readability": "^0.4.4",
|
||||
"@phosphor-icons/core": "^2.1.1",
|
||||
"@phosphor-icons/react": "^2.1.7",
|
||||
"@prisma/client": "^4.16.2",
|
||||
"@stripe/stripe-js": "^1.54.1",
|
||||
"@tanstack/react-query": "^5.51.15",
|
||||
|
@ -50,6 +52,7 @@
|
|||
"eslint-config-next": "13.4.9",
|
||||
"formidable": "^3.5.1",
|
||||
"framer-motion": "^10.16.4",
|
||||
"fuse.js": "^7.0.0",
|
||||
"handlebars": "^4.7.8",
|
||||
"himalaya": "^1.1.0",
|
||||
"i18next": "^23.11.5",
|
||||
|
@ -76,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": {
|
||||
|
@ -92,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"
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 };
|
|
@ -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) });
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -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 };
|
|
@ -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}`}
|
||||
|
|
|
@ -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"
|
||||
}
|
56
yarn.lock
56
yarn.lock
|
@ -1281,6 +1281,16 @@
|
|||
resolved "https://registry.yarnpkg.com/@panva/hkdf/-/hkdf-1.1.1.tgz#ab9cd8755d1976e72fc77a00f7655a64efe6cd5d"
|
||||
integrity sha512-dhPeilub1NuIG0X5Kvhh9lH4iW3ZsHlnzwgwbOlgwQ2wG1IqFzsgHqmKPk3WzsdWAeaxKJxgM0+W433RmN45GA==
|
||||
|
||||
"@phosphor-icons/core@^2.1.1":
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@phosphor-icons/core/-/core-2.1.1.tgz#62a4cfbec9772f1a613a647da214fbb96f3ad39d"
|
||||
integrity sha512-v4ARvrip4qBCImOE5rmPUylOEK4iiED9ZyKjcvzuezqMaiRASCHKcRIuvvxL/twvLpkfnEODCOJp5dM4eZilxQ==
|
||||
|
||||
"@phosphor-icons/react@^2.1.7":
|
||||
version "2.1.7"
|
||||
resolved "https://registry.yarnpkg.com/@phosphor-icons/react/-/react-2.1.7.tgz#b11a4b25849b7e3849970b688d9fe91e5d4fd8d7"
|
||||
integrity sha512-g2e2eVAn1XG2a+LI09QU3IORLhnFNAFkNbo2iwbX6NOKSLOwvEMmTa7CgOzEbgNWR47z8i8kwjdvYZ5fkGx1mQ==
|
||||
|
||||
"@pkgr/utils@^2.3.1":
|
||||
version "2.3.1"
|
||||
resolved "https://registry.yarnpkg.com/@pkgr/utils/-/utils-2.3.1.tgz#0a9b06ffddee364d6642b3cd562ca76f55b34a03"
|
||||
|
@ -3369,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==
|
||||
|
@ -3380,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"
|
||||
|
@ -3553,6 +3574,11 @@ functions-have-names@^1.2.2:
|
|||
resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834"
|
||||
integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==
|
||||
|
||||
fuse.js@^7.0.0:
|
||||
version "7.0.0"
|
||||
resolved "https://registry.yarnpkg.com/fuse.js/-/fuse.js-7.0.0.tgz#6573c9fcd4c8268e403b4fc7d7131ffcf99a9eb2"
|
||||
integrity sha512-14F4hBIxqKvD4Zz/XjDc3y94mNZN6pRv3U13Udo0lNLCWRBUsrMv2xwcF/y/Z5sV6+FQW+/ow68cHpm4sunt8Q==
|
||||
|
||||
gauge@^3.0.0:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/gauge/-/gauge-3.0.2.tgz#03bf4441c044383908bcfa0656ad91803259b395"
|
||||
|
@ -4253,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"
|
||||
|
@ -5861,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"
|
||||
|
@ -6239,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"
|
||||
|
||||
|
|
Ŝarĝante…
Reference in New Issue