fully added reader view support
This commit is contained in:
parent
ed91c4267b
commit
fb61812356
|
@ -36,10 +36,6 @@ export const styles: StylesConfig = {
|
||||||
...styles,
|
...styles,
|
||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
}),
|
}),
|
||||||
clearIndicator: (styles) => ({
|
|
||||||
...styles,
|
|
||||||
visibility: "hidden",
|
|
||||||
}),
|
|
||||||
placeholder: (styles) => ({
|
placeholder: (styles) => ({
|
||||||
...styles,
|
...styles,
|
||||||
borderColor: "black",
|
borderColor: "black",
|
||||||
|
|
|
@ -21,6 +21,7 @@ import { toast } from "react-hot-toast";
|
||||||
import isValidUrl from "@/lib/client/isValidUrl";
|
import isValidUrl from "@/lib/client/isValidUrl";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import unescapeString from "@/lib/client/unescapeString";
|
import unescapeString from "@/lib/client/unescapeString";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
link: LinkIncludingShortenedCollectionAndTags;
|
link: LinkIncludingShortenedCollectionAndTags;
|
||||||
|
@ -38,6 +39,8 @@ type DropdownTrigger =
|
||||||
export default function LinkCard({ link, count, className }: Props) {
|
export default function LinkCard({ link, count, className }: Props) {
|
||||||
const { setModal } = useModalStore();
|
const { setModal } = useModalStore();
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
const permissions = usePermissions(link.collection.id as number);
|
const permissions = usePermissions(link.collection.id as number);
|
||||||
|
|
||||||
const [expandDropdown, setExpandDropdown] = useState<DropdownTrigger>(false);
|
const [expandDropdown, setExpandDropdown] = useState<DropdownTrigger>(false);
|
||||||
|
@ -159,16 +162,7 @@ export default function LinkCard({ link, count, className }: Props) {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
onClick={() => {
|
onClick={() => router.push("/links/" + link.id)}
|
||||||
setModal({
|
|
||||||
modal: "LINK",
|
|
||||||
state: true,
|
|
||||||
method: "UPDATE",
|
|
||||||
isOwnerOrMod:
|
|
||||||
permissions === true || (permissions?.canUpdate as boolean),
|
|
||||||
active: link,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
className="flex items-start cursor-pointer gap-5 sm:gap-10 h-full w-full p-5"
|
className="flex items-start cursor-pointer gap-5 sm:gap-10 h-full w-full p-5"
|
||||||
>
|
>
|
||||||
{url && (
|
{url && (
|
||||||
|
@ -252,10 +246,7 @@ export default function LinkCard({ link, count, className }: Props) {
|
||||||
modal: "LINK",
|
modal: "LINK",
|
||||||
state: true,
|
state: true,
|
||||||
method: "UPDATE",
|
method: "UPDATE",
|
||||||
isOwnerOrMod:
|
|
||||||
permissions === true || permissions?.canUpdate,
|
|
||||||
active: link,
|
active: link,
|
||||||
defaultIndex: 1,
|
|
||||||
});
|
});
|
||||||
setExpandDropdown(false);
|
setExpandDropdown(false);
|
||||||
},
|
},
|
||||||
|
|
|
@ -0,0 +1,138 @@
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import {
|
||||||
|
faPen,
|
||||||
|
faBoxesStacked,
|
||||||
|
faTrashCan,
|
||||||
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import useModalStore from "@/store/modals";
|
||||||
|
import useLinkStore from "@/store/links";
|
||||||
|
import {
|
||||||
|
CollectionIncludingMembersAndLinkCount,
|
||||||
|
LinkIncludingShortenedCollectionAndTags,
|
||||||
|
} from "@/types/global";
|
||||||
|
import { useSession } from "next-auth/react";
|
||||||
|
import useCollectionStore from "@/store/collections";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
className?: string;
|
||||||
|
onClick?: Function;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function SettingsSidebar({ className, onClick }: Props) {
|
||||||
|
const session = useSession();
|
||||||
|
const userId = session.data?.user.id;
|
||||||
|
|
||||||
|
const { setModal } = useModalStore();
|
||||||
|
|
||||||
|
const { links, removeLink } = useLinkStore();
|
||||||
|
const { collections } = useCollectionStore();
|
||||||
|
|
||||||
|
const [linkCollection, setLinkCollection] =
|
||||||
|
useState<CollectionIncludingMembersAndLinkCount>();
|
||||||
|
|
||||||
|
const [link, setLink] = useState<LinkIncludingShortenedCollectionAndTags>();
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (links) setLink(links.find((e) => e.id === Number(router.query.id)));
|
||||||
|
}, [links]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (link)
|
||||||
|
setLinkCollection(collections.find((e) => e.id === link?.collection.id));
|
||||||
|
}, [link]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`dark:bg-neutral-900 bg-white h-full lg:w-10 w-62 overflow-y-auto lg:p-0 p-5 border-solid border-white border dark:border-neutral-900 dark:lg:border-r-neutral-900 lg:border-r-white border-r-sky-100 dark:border-r-neutral-700 z-20 flex flex-col gap-5 lg:justify-center justify-start ${
|
||||||
|
className || ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex flex-col gap-5">
|
||||||
|
{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;
|
||||||
|
onClick && onClick();
|
||||||
|
}}
|
||||||
|
className={`hover:opacity-50 duration-100 py-2 px-2 cursor-pointer flex items-center gap-4 w-full rounded-md h-8`}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faPen}
|
||||||
|
className="w-6 h-6 text-gray-500 dark:text-gray-300"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<p className="text-black dark:text-white truncate w-full lg:hidden">
|
||||||
|
Edit
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : undefined}
|
||||||
|
|
||||||
|
<div
|
||||||
|
onClick={() => {
|
||||||
|
link
|
||||||
|
? setModal({
|
||||||
|
modal: "LINK",
|
||||||
|
state: true,
|
||||||
|
active: link,
|
||||||
|
method: "FORMATS",
|
||||||
|
})
|
||||||
|
: undefined;
|
||||||
|
onClick && onClick();
|
||||||
|
}}
|
||||||
|
title="Preserved Formats"
|
||||||
|
className={`hover:opacity-50 duration-100 py-2 px-2 cursor-pointer flex items-center gap-4 w-full rounded-md h-8`}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faBoxesStacked}
|
||||||
|
className="w-6 h-6 text-gray-500 dark:text-gray-300"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<p className="text-black dark:text-white truncate w-full lg:hidden">
|
||||||
|
Preserved Formats
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{link?.collection.ownerId === userId ||
|
||||||
|
linkCollection?.members.some(
|
||||||
|
(e) => e.userId === userId && e.canDelete
|
||||||
|
) ? (
|
||||||
|
<div
|
||||||
|
onClick={() => {
|
||||||
|
if (link?.id) {
|
||||||
|
removeLink(link.id);
|
||||||
|
router.back();
|
||||||
|
onClick && onClick();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
title="Delete"
|
||||||
|
className={`hover:opacity-50 duration-100 py-2 px-2 cursor-pointer flex items-center gap-4 w-full rounded-md h-8`}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faTrashCan}
|
||||||
|
className="w-6 h-6 text-gray-500 dark:text-gray-300"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<p className="text-black dark:text-white truncate w-full lg:hidden">
|
||||||
|
Delete
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : undefined}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -4,8 +4,7 @@ import TagSelection from "@/components/InputSelect/TagSelection";
|
||||||
import { LinkIncludingShortenedCollectionAndTags } from "@/types/global";
|
import { LinkIncludingShortenedCollectionAndTags } from "@/types/global";
|
||||||
import { faPenToSquare } from "@fortawesome/free-regular-svg-icons";
|
import { faPenToSquare } from "@fortawesome/free-regular-svg-icons";
|
||||||
import useLinkStore from "@/store/links";
|
import useLinkStore from "@/store/links";
|
||||||
import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
import { faLink, faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||||
import RequiredBadge from "../../RequiredBadge";
|
|
||||||
import { useSession } from "next-auth/react";
|
import { useSession } from "next-auth/react";
|
||||||
import useCollectionStore from "@/store/collections";
|
import useCollectionStore from "@/store/collections";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
@ -14,6 +13,7 @@ import { toast } from "react-hot-toast";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import TextInput from "@/components/TextInput";
|
import TextInput from "@/components/TextInput";
|
||||||
import unescapeString from "@/lib/client/unescapeString";
|
import unescapeString from "@/lib/client/unescapeString";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
type Props =
|
type Props =
|
||||||
| {
|
| {
|
||||||
|
@ -48,6 +48,7 @@ export default function AddOrEditLink({
|
||||||
tags: [],
|
tags: [],
|
||||||
screenshotPath: "",
|
screenshotPath: "",
|
||||||
pdfPath: "",
|
pdfPath: "",
|
||||||
|
readabilityPath: "",
|
||||||
collection: {
|
collection: {
|
||||||
name: "",
|
name: "",
|
||||||
ownerId: data?.user.id as number,
|
ownerId: data?.user.id as number,
|
||||||
|
@ -135,23 +136,22 @@ export default function AddOrEditLink({
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-3 sm:w-[35rem] w-80">
|
<div className="flex flex-col gap-3 sm:w-[35rem] w-80">
|
||||||
{method === "UPDATE" ? (
|
{method === "UPDATE" ? (
|
||||||
<p
|
<div
|
||||||
className="text-gray-500 dark:text-gray-300 text-center truncate w-full"
|
className="text-gray-500 dark:text-gray-300 break-all w-full flex gap-2"
|
||||||
title={link.url}
|
title={link.url}
|
||||||
>
|
>
|
||||||
Editing:{" "}
|
<FontAwesomeIcon icon={faLink} className="w-6 h-6" />
|
||||||
<Link href={link.url} target="_blank">
|
<Link href={link.url} target="_blank" className="w-full">
|
||||||
{link.url}
|
{link.url}
|
||||||
</Link>
|
</Link>
|
||||||
</p>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{method === "CREATE" ? (
|
{method === "CREATE" ? (
|
||||||
<div className="grid grid-flow-row-dense sm:grid-cols-5 gap-3">
|
<div className="grid grid-flow-row-dense sm:grid-cols-5 gap-3">
|
||||||
<div className="sm:col-span-3 col-span-5">
|
<div className="sm:col-span-3 col-span-5">
|
||||||
<p className="text-sm text-black dark:text-white mb-2 font-bold">
|
<p className="text-sm text-black dark:text-white mb-2">
|
||||||
Address (URL)
|
Address (URL)
|
||||||
<RequiredBadge />
|
|
||||||
</p>
|
</p>
|
||||||
<TextInput
|
<TextInput
|
||||||
value={link.url}
|
value={link.url}
|
||||||
|
@ -189,7 +189,7 @@ export default function AddOrEditLink({
|
||||||
|
|
||||||
{optionsExpanded ? (
|
{optionsExpanded ? (
|
||||||
<div>
|
<div>
|
||||||
<hr className="mb-3 border border-sky-100 dark:border-neutral-700" />
|
{/* <hr className="mb-3 border border-sky-100 dark:border-neutral-700" /> */}
|
||||||
<div className="grid sm:grid-cols-2 gap-3">
|
<div className="grid sm:grid-cols-2 gap-3">
|
||||||
<div className={`${method === "UPDATE" ? "sm:col-span-2" : ""}`}>
|
<div className={`${method === "UPDATE" ? "sm:col-span-2" : ""}`}>
|
||||||
<p className="text-sm text-black dark:text-white mb-2">Name</p>
|
<p className="text-sm text-black dark:text-white mb-2">Name</p>
|
||||||
|
|
|
@ -0,0 +1,181 @@
|
||||||
|
import { LinkIncludingShortenedCollectionAndTags } from "@/types/global";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import {
|
||||||
|
faArrowUpRightFromSquare,
|
||||||
|
faCloudArrowDown,
|
||||||
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { faFileImage, faFilePdf } from "@fortawesome/free-regular-svg-icons";
|
||||||
|
import useLinkStore from "@/store/links";
|
||||||
|
import { toast } from "react-hot-toast";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { useSession } from "next-auth/react";
|
||||||
|
|
||||||
|
export default function PreservedFormats() {
|
||||||
|
const session = useSession();
|
||||||
|
const { links, getLink } = useLinkStore();
|
||||||
|
|
||||||
|
const [link, setLink] = useState<LinkIncludingShortenedCollectionAndTags>();
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (links) setLink(links.find((e) => e.id === Number(router.query.id)));
|
||||||
|
}, [links]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let interval: NodeJS.Timer | undefined;
|
||||||
|
if (link?.screenshotPath === "pending" || link?.pdfPath === "pending") {
|
||||||
|
interval = setInterval(() => getLink(link.id as number), 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);
|
||||||
|
};
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<div className={`flex flex-col gap-3 sm:w-[35rem] w-80 pt-3`}>
|
||||||
|
{link?.screenshotPath && link?.screenshotPath !== "pending" ? (
|
||||||
|
<div className="flex justify-between items-center pr-1 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-l-md">
|
||||||
|
<FontAwesomeIcon icon={faFileImage} className="w-6 h-6" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-black dark:text-white">Screenshot</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex text-black dark:text-white gap-1">
|
||||||
|
<div
|
||||||
|
onClick={() => handleDownload("png")}
|
||||||
|
className="cursor-pointer hover:opacity-50 duration-100 p-2 rounded-md"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faCloudArrowDown}
|
||||||
|
className="w-5 h-5 cursor-pointer text-gray-500 dark:text-gray-300"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Link
|
||||||
|
href={`/api/v1/archives/${link.collectionId}/${link.id}.png`}
|
||||||
|
target="_blank"
|
||||||
|
className="cursor-pointer hover:opacity-50 duration-100 p-2 rounded-md"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faArrowUpRightFromSquare}
|
||||||
|
className="w-5 h-5 text-gray-500 dark:text-gray-300"
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : undefined}
|
||||||
|
|
||||||
|
{link?.pdfPath && link.pdfPath !== "pending" ? (
|
||||||
|
<div className="flex justify-between items-center pr-1 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-l-md">
|
||||||
|
<FontAwesomeIcon icon={faFilePdf} className="w-6 h-6" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-black dark:text-white">PDF</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex text-black dark:text-white gap-1">
|
||||||
|
<div
|
||||||
|
onClick={() => handleDownload("pdf")}
|
||||||
|
className="cursor-pointer hover:opacity-50 duration-100 p-2 rounded-md"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faCloudArrowDown}
|
||||||
|
className="w-5 h-5 cursor-pointer text-gray-500 dark:text-gray-300"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Link
|
||||||
|
href={`/api/v1/archives/${link.collectionId}/${link.id}.pdf`}
|
||||||
|
target="_blank"
|
||||||
|
className="cursor-pointer hover:opacity-50 duration-100 p-2 rounded-md"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faArrowUpRightFromSquare}
|
||||||
|
className="w-5 h-5 text-gray-500 dark:text-gray-300"
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : undefined}
|
||||||
|
|
||||||
|
<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-600 p-1 rounded-md cursor-pointer select-none mt-3"
|
||||||
|
onClick={() => updateArchive()}
|
||||||
|
>
|
||||||
|
<p>Update Preserved Formats</p>
|
||||||
|
<p className="text-xs">(re-fetch)</p>
|
||||||
|
</div>
|
||||||
|
) : undefined}
|
||||||
|
<Link
|
||||||
|
href={`https://web.archive.org/web/${link?.url.replace(
|
||||||
|
/(^\w+:|^)\/\//,
|
||||||
|
""
|
||||||
|
)}`}
|
||||||
|
target="_blank"
|
||||||
|
className="sm:mt-3 text-gray-500 dark:text-gray-300 duration-100 hover:opacity-50 flex gap-2 w-fit items-center text-sm"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faArrowUpRightFromSquare}
|
||||||
|
className="w-4 h-4"
|
||||||
|
/>
|
||||||
|
<p className="whitespace-nowrap">
|
||||||
|
View Latest Snapshot on archive.org
|
||||||
|
</p>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,94 +1,68 @@
|
||||||
import { Tab } from "@headlessui/react";
|
|
||||||
import { LinkIncludingShortenedCollectionAndTags } from "@/types/global";
|
import { LinkIncludingShortenedCollectionAndTags } from "@/types/global";
|
||||||
import AddOrEditLink from "./AddOrEditLink";
|
import AddOrEditLink from "./AddOrEditLink";
|
||||||
import LinkDetails from "./LinkDetails";
|
import PreservedFormats from "./PreservedFormats";
|
||||||
|
|
||||||
type Props =
|
type Props =
|
||||||
| {
|
| {
|
||||||
toggleLinkModal: Function;
|
toggleLinkModal: Function;
|
||||||
method: "CREATE";
|
method: "CREATE";
|
||||||
isOwnerOrMod?: boolean;
|
|
||||||
activeLink?: LinkIncludingShortenedCollectionAndTags;
|
activeLink?: LinkIncludingShortenedCollectionAndTags;
|
||||||
defaultIndex?: number;
|
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
toggleLinkModal: Function;
|
toggleLinkModal: Function;
|
||||||
method: "UPDATE";
|
method: "UPDATE";
|
||||||
isOwnerOrMod: boolean;
|
|
||||||
activeLink: LinkIncludingShortenedCollectionAndTags;
|
activeLink: LinkIncludingShortenedCollectionAndTags;
|
||||||
defaultIndex?: number;
|
className?: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
toggleLinkModal: Function;
|
||||||
|
method: "FORMATS";
|
||||||
|
activeLink: LinkIncludingShortenedCollectionAndTags;
|
||||||
className?: string;
|
className?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function LinkModal({
|
export default function LinkModal({
|
||||||
className,
|
className,
|
||||||
defaultIndex,
|
|
||||||
toggleLinkModal,
|
toggleLinkModal,
|
||||||
isOwnerOrMod,
|
|
||||||
activeLink,
|
activeLink,
|
||||||
method,
|
method,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
return (
|
return (
|
||||||
<div className={className}>
|
<div className={className}>
|
||||||
<Tab.Group defaultIndex={defaultIndex}>
|
{/* {method === "CREATE" && (
|
||||||
{method === "CREATE" && (
|
|
||||||
<p className="text-xl text-black dark:text-white text-center">
|
<p className="text-xl text-black dark:text-white text-center">
|
||||||
New Link
|
New Link
|
||||||
</p>
|
</p>
|
||||||
)}
|
)} */}
|
||||||
<Tab.List className="flex justify-center flex-col max-w-[15rem] sm:max-w-[30rem] mx-auto sm:flex-row gap-2 sm:gap-3 mb-5 text-black dark:text-white">
|
|
||||||
{method === "UPDATE" && isOwnerOrMod && (
|
|
||||||
<>
|
|
||||||
<Tab
|
|
||||||
className={({ selected }) =>
|
|
||||||
selected
|
|
||||||
? "px-2 py-1 bg-sky-200 dark:bg-sky-800 duration-100 rounded-md outline-none"
|
|
||||||
: "px-2 py-1 hover:bg-slate-200 hover:dark:bg-neutral-700 rounded-md duration-100 outline-none"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Link Details
|
|
||||||
</Tab>
|
|
||||||
<Tab
|
|
||||||
className={({ selected }) =>
|
|
||||||
selected
|
|
||||||
? "px-2 py-1 bg-sky-200 dark:bg-sky-800 duration-100 rounded-md outline-none"
|
|
||||||
: "px-2 py-1 hover:bg-slate-200 hover:dark:bg-neutral-700 rounded-md duration-100 outline-none"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Edit Link
|
|
||||||
</Tab>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Tab.List>
|
|
||||||
<Tab.Panels>
|
|
||||||
{activeLink && method === "UPDATE" && (
|
|
||||||
<Tab.Panel>
|
|
||||||
<LinkDetails
|
|
||||||
linkId={activeLink.id as number}
|
|
||||||
isOwnerOrMod={isOwnerOrMod}
|
|
||||||
/>
|
|
||||||
</Tab.Panel>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Tab.Panel>
|
|
||||||
{activeLink && method === "UPDATE" ? (
|
{activeLink && method === "UPDATE" ? (
|
||||||
|
<>
|
||||||
|
<p className="ml-10 mt-[0.1rem] text-xl mb-3 font-thin">Edit Link</p>
|
||||||
<AddOrEditLink
|
<AddOrEditLink
|
||||||
toggleLinkModal={toggleLinkModal}
|
toggleLinkModal={toggleLinkModal}
|
||||||
method="UPDATE"
|
method="UPDATE"
|
||||||
activeLink={
|
activeLink={activeLink}
|
||||||
activeLink as LinkIncludingShortenedCollectionAndTags
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
) : (
|
</>
|
||||||
<AddOrEditLink
|
) : undefined}
|
||||||
toggleLinkModal={toggleLinkModal}
|
|
||||||
method="CREATE"
|
{method === "CREATE" ? (
|
||||||
/>
|
<>
|
||||||
)}
|
<p className="ml-10 mt-[0.1rem] text-xl mb-3 font-thin">
|
||||||
</Tab.Panel>
|
Create a New Link
|
||||||
</Tab.Panels>
|
</p>
|
||||||
</Tab.Group>
|
<AddOrEditLink toggleLinkModal={toggleLinkModal} method="CREATE" />
|
||||||
|
</>
|
||||||
|
) : undefined}
|
||||||
|
|
||||||
|
{activeLink && method === "FORMATS" ? (
|
||||||
|
<>
|
||||||
|
<p className="ml-10 mt-[0.1rem] text-xl mb-3 font-thin">
|
||||||
|
Manage Preserved Formats
|
||||||
|
</p>
|
||||||
|
<PreservedFormats activeLink={activeLink} />
|
||||||
|
</>
|
||||||
|
) : undefined}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,8 +27,6 @@ export default function ModalManagement() {
|
||||||
<LinkModal
|
<LinkModal
|
||||||
toggleLinkModal={toggleModal}
|
toggleLinkModal={toggleModal}
|
||||||
method={modal.method}
|
method={modal.method}
|
||||||
isOwnerOrMod={modal.isOwnerOrMod as boolean}
|
|
||||||
defaultIndex={modal.defaultIndex}
|
|
||||||
activeLink={modal.active as LinkIncludingShortenedCollectionAndTags}
|
activeLink={modal.active as LinkIncludingShortenedCollectionAndTags}
|
||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
|
@ -0,0 +1,88 @@
|
||||||
|
import LinkSidebar from "@/components/LinkSidebar";
|
||||||
|
import { ReactNode, useEffect, useState } from "react";
|
||||||
|
import ModalManagement from "@/components/ModalManagement";
|
||||||
|
import useModalStore from "@/store/modals";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import ClickAwayHandler from "@/components/ClickAwayHandler";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { faBars, faChevronLeft } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import Link from "next/link";
|
||||||
|
import useWindowDimensions from "@/hooks/useWindowDimensions";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function LinkLayout({ children }: Props) {
|
||||||
|
const { modal } = useModalStore();
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
modal
|
||||||
|
? (document.body.style.overflow = "hidden")
|
||||||
|
: (document.body.style.overflow = "auto");
|
||||||
|
}, [modal]);
|
||||||
|
|
||||||
|
const [sidebar, setSidebar] = useState(false);
|
||||||
|
|
||||||
|
const { width } = useWindowDimensions();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setSidebar(false);
|
||||||
|
}, [width]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setSidebar(false);
|
||||||
|
}, [router]);
|
||||||
|
|
||||||
|
const toggleSidebar = () => {
|
||||||
|
setSidebar(!sidebar);
|
||||||
|
};
|
||||||
|
|
||||||
|
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">
|
||||||
|
<div
|
||||||
|
onClick={toggleSidebar}
|
||||||
|
className="inline-flex lg:hidden gap-1 items-center select-none cursor-pointer p-2 text-gray-500 dark:text-gray-300 rounded-md duration-100 hover:bg-slate-200 dark:hover:bg-neutral-700"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faBars} className="w-5 h-5" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
onClick={() => router.back()}
|
||||||
|
className="inline-flex gap-1 lg:hover:opacity-50 items-center select-none cursor-pointer p-2 lg:p-0 lg:px-1 lg:my-2 text-gray-500 dark:text-gray-300 rounded-md duration-100 hover:bg-slate-200 dark:hover:bg-neutral-700 lg:hover:bg-transparent lg:dark:hover:bg-transparent"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faChevronLeft} className="w-4 h-4" />
|
||||||
|
Back
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{children}
|
||||||
|
|
||||||
|
{sidebar ? (
|
||||||
|
<div className="fixed top-0 bottom-0 right-0 left-0 bg-gray-500 bg-opacity-10 backdrop-blur-sm flex items-center fade-in z-30">
|
||||||
|
<ClickAwayHandler
|
||||||
|
className="h-full"
|
||||||
|
onClickOutside={toggleSidebar}
|
||||||
|
>
|
||||||
|
<div className="slide-right h-full shadow-lg">
|
||||||
|
<LinkSidebar onClick={() => setSidebar(false)} />
|
||||||
|
</div>
|
||||||
|
</ClickAwayHandler>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
|
@ -85,8 +85,6 @@ export default async function archive(
|
||||||
filePath: `archives/${targetLink.collectionId}/${linkId}_readability.json`,
|
filePath: `archives/${targetLink.collectionId}/${linkId}_readability.json`,
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(JSON.parse(JSON.stringify(article)));
|
|
||||||
|
|
||||||
// Screenshot/PDF
|
// Screenshot/PDF
|
||||||
|
|
||||||
let faulty = true;
|
let faulty = true;
|
||||||
|
|
|
@ -16,7 +16,7 @@ export default async function getLinkById(userId: number, linkId: number) {
|
||||||
| null;
|
| null;
|
||||||
|
|
||||||
const memberHasAccess = collectionIsAccessible?.members.some(
|
const memberHasAccess = collectionIsAccessible?.members.some(
|
||||||
(e: UsersAndCollections) => e.userId === userId && e.canUpdate
|
(e: UsersAndCollections) => e.userId === userId
|
||||||
);
|
);
|
||||||
|
|
||||||
const isCollectionOwner = collectionIsAccessible?.ownerId === userId;
|
const isCollectionOwner = collectionIsAccessible?.ownerId === userId;
|
||||||
|
|
|
@ -5,7 +5,6 @@ import archive from "@/lib/api/archive";
|
||||||
import { prisma } from "@/lib/api/db";
|
import { prisma } from "@/lib/api/db";
|
||||||
|
|
||||||
export default async function links(req: NextApiRequest, res: NextApiResponse) {
|
export default async function links(req: NextApiRequest, res: NextApiResponse) {
|
||||||
console.log("hi");
|
|
||||||
const session = await getServerSession(req, res, authOptions);
|
const session = await getServerSession(req, res, authOptions);
|
||||||
|
|
||||||
if (!session?.user?.id) {
|
if (!session?.user?.id) {
|
||||||
|
|
|
@ -0,0 +1,257 @@
|
||||||
|
import LinkLayout from "@/layouts/LinkLayout";
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import useLinkStore from "@/store/links";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { LinkIncludingShortenedCollectionAndTags } from "@/types/global";
|
||||||
|
import Image from "next/image";
|
||||||
|
import ColorThief, { RGBColor } from "colorthief";
|
||||||
|
import { useTheme } from "next-themes";
|
||||||
|
import unescapeString from "@/lib/client/unescapeString";
|
||||||
|
import isValidUrl from "@/lib/client/isValidUrl";
|
||||||
|
import DOMPurify from "dompurify";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { faBoxesStacked } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import useModalStore from "@/store/modals";
|
||||||
|
import { useSession } from "next-auth/react";
|
||||||
|
|
||||||
|
type LinkContent = {
|
||||||
|
title: string;
|
||||||
|
content: string;
|
||||||
|
textContent: string;
|
||||||
|
length: number;
|
||||||
|
excerpt: string;
|
||||||
|
byline: string;
|
||||||
|
dir: string;
|
||||||
|
siteName: string;
|
||||||
|
lang: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function Index() {
|
||||||
|
const { theme } = useTheme();
|
||||||
|
const { links, getLink } = useLinkStore();
|
||||||
|
const { setModal } = useModalStore();
|
||||||
|
|
||||||
|
const session = useSession();
|
||||||
|
const userId = session.data?.user.id;
|
||||||
|
|
||||||
|
const [link, setLink] = useState<LinkIncludingShortenedCollectionAndTags>();
|
||||||
|
const [linkContent, setLinkContent] = useState<LinkContent>();
|
||||||
|
const [imageError, setImageError] = useState<boolean>(false);
|
||||||
|
const [colorPalette, setColorPalette] = useState<RGBColor[]>();
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchLink = async () => {
|
||||||
|
if (router.query.id) {
|
||||||
|
await getLink(Number(router.query.id));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchLink();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (links[0]) setLink(links.find((e) => e.id === Number(router.query.id)));
|
||||||
|
}, [links]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchLinkContent = async () => {
|
||||||
|
if (
|
||||||
|
router.query.id &&
|
||||||
|
link?.readabilityPath &&
|
||||||
|
link?.readabilityPath !== "pending"
|
||||||
|
) {
|
||||||
|
const response = await fetch(`/api/v1/${link?.readabilityPath}`);
|
||||||
|
|
||||||
|
const data = await response?.json();
|
||||||
|
|
||||||
|
setLinkContent(data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchLinkContent();
|
||||||
|
}, [link]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let interval: NodeJS.Timer | undefined;
|
||||||
|
if (link?.screenshotPath === "pending" || link?.pdfPath === "pending") {
|
||||||
|
interval = setInterval(() => getLink(link.id as number), 5000);
|
||||||
|
} else {
|
||||||
|
if (interval) {
|
||||||
|
clearInterval(interval);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (interval) {
|
||||||
|
clearInterval(interval);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [link?.screenshotPath, link?.pdfPath, link?.readabilityPath]);
|
||||||
|
|
||||||
|
const colorThief = new ColorThief();
|
||||||
|
|
||||||
|
const rgbToHex = (r: number, g: number, b: number): string =>
|
||||||
|
"#" +
|
||||||
|
[r, g, b]
|
||||||
|
.map((x) => {
|
||||||
|
const hex = x.toString(16);
|
||||||
|
return hex.length === 1 ? "0" + hex : hex;
|
||||||
|
})
|
||||||
|
.join("");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const banner = document.getElementById("link-banner");
|
||||||
|
const bannerInner = document.getElementById("link-banner-inner");
|
||||||
|
|
||||||
|
if (colorPalette && banner && bannerInner) {
|
||||||
|
if (colorPalette[0] && colorPalette[1]) {
|
||||||
|
banner.style.background = `linear-gradient(to right, ${rgbToHex(
|
||||||
|
colorPalette[0][0],
|
||||||
|
colorPalette[0][1],
|
||||||
|
colorPalette[0][2]
|
||||||
|
)}30, ${rgbToHex(
|
||||||
|
colorPalette[1][0],
|
||||||
|
colorPalette[1][1],
|
||||||
|
colorPalette[1][2]
|
||||||
|
)}30)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (colorPalette[2] && colorPalette[3]) {
|
||||||
|
bannerInner.style.background = `linear-gradient(to left, ${rgbToHex(
|
||||||
|
colorPalette[2][0],
|
||||||
|
colorPalette[2][1],
|
||||||
|
colorPalette[2][2]
|
||||||
|
)}30, ${rgbToHex(
|
||||||
|
colorPalette[3][0],
|
||||||
|
colorPalette[3][1],
|
||||||
|
colorPalette[3][2]
|
||||||
|
)})30`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [colorPalette, theme]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<LinkLayout>
|
||||||
|
<div
|
||||||
|
className={`flex flex-col max-w-screen-md h-full ${
|
||||||
|
theme === "dark" ? "banner-dark-mode" : "banner-light-mode"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
id="link-banner"
|
||||||
|
className="link-banner p-5 mb-6 relative bg-opacity-10 border border-solid border-sky-100 dark:border-neutral-700 shadow-md"
|
||||||
|
>
|
||||||
|
<div id="link-banner-inner" className="link-banner-inner"></div>
|
||||||
|
|
||||||
|
<div className={`relative flex gap-5 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`}
|
||||||
|
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>
|
||||||
|
<p className="capitalize text-3xl font-thin">
|
||||||
|
{unescapeString(link?.name || link?.description || "")}
|
||||||
|
</p>
|
||||||
|
<div className="flex gap-2 text-sm text-gray-500 dark:text-gray-300">
|
||||||
|
<p className=" min-w-fit">
|
||||||
|
{link?.createdAt
|
||||||
|
? new Date(link?.createdAt).toLocaleString("en-US", {
|
||||||
|
year: "numeric",
|
||||||
|
month: "long",
|
||||||
|
day: "numeric",
|
||||||
|
})
|
||||||
|
: undefined}
|
||||||
|
</p>
|
||||||
|
{link?.url ? (
|
||||||
|
<>
|
||||||
|
<p>•</p>
|
||||||
|
<Link
|
||||||
|
href={link?.url || ""}
|
||||||
|
target="_blank"
|
||||||
|
className="hover:underline break-all"
|
||||||
|
>
|
||||||
|
{isValidUrl(link?.url || "")
|
||||||
|
? new URL(link?.url as string).host
|
||||||
|
: undefined}
|
||||||
|
</Link>
|
||||||
|
</>
|
||||||
|
) : undefined}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-5 h-full">
|
||||||
|
{link &&
|
||||||
|
link?.readabilityPath &&
|
||||||
|
link?.readabilityPath !== "pending" ? (
|
||||||
|
<div
|
||||||
|
className="line-break px-5"
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: DOMPurify.sanitize(linkContent?.content || "") || "",
|
||||||
|
}}
|
||||||
|
></div>
|
||||||
|
) : (
|
||||||
|
<div className="border border-solid border-sky-100 dark:border-neutral-700 w-full h-full flex flex-col justify-center p-10 rounded-2xl bg-gray-50 dark:bg-neutral-800">
|
||||||
|
<p className="text-center text-2xl text-black dark:text-white">
|
||||||
|
There is no reader view for this webpage
|
||||||
|
</p>
|
||||||
|
<p className="text-center text-sm text-black dark:text-white">
|
||||||
|
{link?.collection.ownerId === userId
|
||||||
|
? "You can update (refetch) the preserved formats by managing them below"
|
||||||
|
: "The collections owners can refetch the preserved formats"}
|
||||||
|
</p>
|
||||||
|
{link?.collection.ownerId === userId ? (
|
||||||
|
<div
|
||||||
|
onClick={() =>
|
||||||
|
link
|
||||||
|
? setModal({
|
||||||
|
modal: "LINK",
|
||||||
|
state: true,
|
||||||
|
active: link,
|
||||||
|
method: "FORMATS",
|
||||||
|
})
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
className="mt-4 flex gap-2 w-fit mx-auto relative items-center font-semibold select-none cursor-pointer p-2 px-3 rounded-md dark:hover:bg-sky-600 text-white bg-sky-700 hover:bg-sky-600 duration-100"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faBoxesStacked}
|
||||||
|
className="w-5 h-5 duration-100"
|
||||||
|
/>
|
||||||
|
<p>Manage preserved formats</p>
|
||||||
|
</div>
|
||||||
|
) : undefined}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</LinkLayout>
|
||||||
|
);
|
||||||
|
}
|
|
@ -72,11 +72,23 @@ const useLinkStore = create<LinkStore>()((set) => ({
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
set((state) => ({
|
set((state) => {
|
||||||
|
const linkExists = state.links.some(
|
||||||
|
(link) => link.id === data.response.id
|
||||||
|
);
|
||||||
|
|
||||||
|
if (linkExists) {
|
||||||
|
return {
|
||||||
links: state.links.map((e) =>
|
links: state.links.map((e) =>
|
||||||
e.id === data.response.id ? data.response : e
|
e.id === data.response.id ? data.response : e
|
||||||
),
|
),
|
||||||
}));
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
links: [...state.links, data.response],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return { ok: response.ok, data: data.response };
|
return { ok: response.ok, data: data.response };
|
||||||
|
|
|
@ -9,17 +9,19 @@ type Modal =
|
||||||
modal: "LINK";
|
modal: "LINK";
|
||||||
state: boolean;
|
state: boolean;
|
||||||
method: "CREATE";
|
method: "CREATE";
|
||||||
isOwnerOrMod?: boolean;
|
|
||||||
active?: LinkIncludingShortenedCollectionAndTags;
|
active?: LinkIncludingShortenedCollectionAndTags;
|
||||||
defaultIndex?: number;
|
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
modal: "LINK";
|
modal: "LINK";
|
||||||
state: boolean;
|
state: boolean;
|
||||||
method: "UPDATE";
|
method: "UPDATE";
|
||||||
isOwnerOrMod: boolean;
|
|
||||||
active: LinkIncludingShortenedCollectionAndTags;
|
active: LinkIncludingShortenedCollectionAndTags;
|
||||||
defaultIndex?: number;
|
}
|
||||||
|
| {
|
||||||
|
modal: "LINK";
|
||||||
|
state: boolean;
|
||||||
|
method: "FORMATS";
|
||||||
|
active: LinkIncludingShortenedCollectionAndTags;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
modal: "COLLECTION";
|
modal: "COLLECTION";
|
||||||
|
|
|
@ -129,72 +129,9 @@
|
||||||
|
|
||||||
/* For the Link banner */
|
/* For the Link banner */
|
||||||
.link-banner {
|
.link-banner {
|
||||||
/* box-shadow: inset 0px 10px 20px 20px #ffffff; */
|
|
||||||
opacity: 25%;
|
|
||||||
z-index: 0;
|
z-index: 0;
|
||||||
}
|
border-radius: 1rem;
|
||||||
.link-banner .link-banner-inner {
|
height: fit-content;
|
||||||
/* box-shadow: inset 0px 10px 20px 20px #ffffff; */
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
-webkit-mask: linear-gradient(#fff, transparent);
|
|
||||||
mask: linear-gradient(#fff, transparent);
|
|
||||||
}
|
|
||||||
.link-banner::after {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
z-index: 1;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
pointer-events: none;
|
|
||||||
width: 100%;
|
|
||||||
height: 4rem;
|
|
||||||
}
|
|
||||||
.link-banner::before {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
z-index: 1;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
pointer-events: none;
|
|
||||||
width: 100%;
|
|
||||||
height: 4rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* For light mode */
|
|
||||||
.banner-light-mode .link-banner::after {
|
|
||||||
background-image: linear-gradient(
|
|
||||||
to bottom,
|
|
||||||
rgba(255, 255, 255, 0),
|
|
||||||
rgba(255, 255, 255, 1) 90%
|
|
||||||
);
|
|
||||||
}
|
|
||||||
.banner-light-mode .link-banner::before {
|
|
||||||
background-image: linear-gradient(
|
|
||||||
to top,
|
|
||||||
rgba(255, 255, 255, 0),
|
|
||||||
rgba(255, 255, 255, 1) 90%
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* For dark mode */
|
|
||||||
.banner-dark-mode .link-banner::after {
|
|
||||||
background-image: linear-gradient(
|
|
||||||
to bottom,
|
|
||||||
rgba(255, 255, 255, 0),
|
|
||||||
#171717 90%
|
|
||||||
);
|
|
||||||
}
|
|
||||||
.banner-dark-mode .link-banner::before {
|
|
||||||
background-image: linear-gradient(
|
|
||||||
to top,
|
|
||||||
rgba(255, 255, 255, 0),
|
|
||||||
#171717 90%
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Theme */
|
/* Theme */
|
||||||
|
@ -227,6 +164,12 @@
|
||||||
@apply dark:text-white;
|
@apply dark:text-white;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.react-select__clear-indicator * {
|
||||||
|
display: none;
|
||||||
|
width: 0;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.sky-shadow {
|
.sky-shadow {
|
||||||
box-shadow: 0px 0px 3px #0ea5e9;
|
box-shadow: 0px 0px 3px #0ea5e9;
|
||||||
|
@ -239,3 +182,7 @@
|
||||||
.primary-btn-gradient:hover {
|
.primary-btn-gradient:hover {
|
||||||
box-shadow: inset 0px -15px 10px #059bf8;
|
box-shadow: inset 0px -15px 10px #059bf8;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.line-break * {
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
Ŝarĝante…
Reference in New Issue