improved edit view
This commit is contained in:
parent
1a378de267
commit
d20c915970
|
@ -1,25 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
import { useTranslation } from "next-i18next";
|
|
||||||
import clsx from "clsx";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
onClick: Function;
|
|
||||||
className?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
function EditButton({ onClick, className }: Props) {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
onClick={() => onClick()}
|
|
||||||
className={clsx(
|
|
||||||
"group-hover:opacity-100 opacity-0 duration-100 btn-square btn-xs btn btn-ghost absolute bi-pencil-fill text-neutral cursor-pointer -right-7 text-xs",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
title={t("edit")}
|
|
||||||
></span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default EditButton;
|
|
|
@ -26,22 +26,26 @@ import { IconWeight } from "@phosphor-icons/react";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
import EditButton from "./EditButton";
|
|
||||||
import CollectionSelection from "./InputSelect/CollectionSelection";
|
import CollectionSelection from "./InputSelect/CollectionSelection";
|
||||||
import TagSelection from "./InputSelect/TagSelection";
|
import TagSelection from "./InputSelect/TagSelection";
|
||||||
import unescapeString from "@/lib/client/unescapeString";
|
import unescapeString from "@/lib/client/unescapeString";
|
||||||
import IconPopover from "./IconPopover";
|
import IconPopover from "./IconPopover";
|
||||||
|
import TextInput from "./TextInput";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
className?: string;
|
className?: string;
|
||||||
activeLink: LinkIncludingShortenedCollectionAndTags;
|
activeLink: LinkIncludingShortenedCollectionAndTags;
|
||||||
standalone?: boolean;
|
standalone?: boolean;
|
||||||
|
mode?: "view" | "edit";
|
||||||
|
setMode?: Function;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function LinkDetails({
|
export default function LinkDetails({
|
||||||
className,
|
className,
|
||||||
activeLink,
|
activeLink,
|
||||||
standalone,
|
standalone,
|
||||||
|
mode = "view",
|
||||||
|
setMode,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const [link, setLink] =
|
const [link, setLink] =
|
||||||
useState<LinkIncludingShortenedCollectionAndTags>(activeLink);
|
useState<LinkIncludingShortenedCollectionAndTags>(activeLink);
|
||||||
|
@ -144,21 +148,13 @@ export default function LinkDetails({
|
||||||
|
|
||||||
const updateLink = useUpdateLink();
|
const updateLink = useUpdateLink();
|
||||||
|
|
||||||
const [fieldToEdit, setFieldToEdit] = useState<
|
|
||||||
"name" | "collection" | "tags" | "description" | null
|
|
||||||
>(null);
|
|
||||||
|
|
||||||
const submit = async (e?: any) => {
|
const submit = async (e?: any) => {
|
||||||
e?.preventDefault();
|
e?.preventDefault();
|
||||||
|
|
||||||
const { updatedAt: b, ...oldLink } = activeLink;
|
const { updatedAt: b, ...oldLink } = activeLink;
|
||||||
const { updatedAt: a, ...newLink } = link;
|
const { updatedAt: a, ...newLink } = link;
|
||||||
|
|
||||||
console.log(oldLink);
|
|
||||||
console.log(newLink);
|
|
||||||
|
|
||||||
if (JSON.stringify(oldLink) === JSON.stringify(newLink)) {
|
if (JSON.stringify(oldLink) === JSON.stringify(newLink)) {
|
||||||
setFieldToEdit(null);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -172,7 +168,7 @@ export default function LinkDetails({
|
||||||
toast.error(error.message);
|
toast.error(error.message);
|
||||||
} else {
|
} else {
|
||||||
toast.success(t("updated"));
|
toast.success(t("updated"));
|
||||||
setFieldToEdit(null);
|
setMode && setMode("view");
|
||||||
console.log(data);
|
console.log(data);
|
||||||
setLink(data);
|
setLink(data);
|
||||||
}
|
}
|
||||||
|
@ -204,7 +200,7 @@ export default function LinkDetails({
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"overflow-hidden select-none relative h-32 opacity-80 group",
|
"overflow-hidden select-none relative group h-40 opacity-80",
|
||||||
standalone
|
standalone
|
||||||
? "sm:max-w-xl -mx-5 -mt-5 sm:rounded-t-2xl"
|
? "sm:max-w-xl -mx-5 -mt-5 sm:rounded-t-2xl"
|
||||||
: "-mx-4 -mt-4"
|
: "-mx-4 -mt-4"
|
||||||
|
@ -226,120 +222,112 @@ export default function LinkDetails({
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
) : link.preview === "unavailable" ? (
|
) : link.preview === "unavailable" ? (
|
||||||
<div className="bg-gray-50 duration-100 h-32"></div>
|
<div className="bg-gray-50 duration-100 h-40"></div>
|
||||||
) : (
|
) : (
|
||||||
<div className="duration-100 h-32 skeleton rounded-b-none"></div>
|
<div className="duration-100 h-40 skeleton rounded-b-none"></div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="absolute top-0 bottom-0 left-0 right-0 opacity-0 group-hover:opacity-100 duration-100 flex justify-end items-end">
|
{!standalone && (
|
||||||
<label className="btn btn-xs mb-2 mr-12 opacity-50 hover:opacity-100">
|
<div className="absolute top-0 bottom-0 left-0 right-0 opacity-0 group-hover:opacity-100 duration-100 flex justify-end items-end">
|
||||||
{t("upload_preview_image")}
|
<label className="btn btn-xs mb-2 mr-3 opacity-50 hover:opacity-100">
|
||||||
<input
|
{t("upload_preview_image")}
|
||||||
type="file"
|
<input
|
||||||
accept="image/jpg, image/jpeg, image/png"
|
type="file"
|
||||||
onChange={async (e) => {
|
accept="image/jpg, image/jpeg, image/png"
|
||||||
const file = e.target.files?.[0];
|
onChange={async (e) => {
|
||||||
if (!file) return;
|
const file = e.target.files?.[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append("file", file);
|
formData.append("file", file);
|
||||||
|
|
||||||
const load = toast.loading(t("uploading"));
|
const load = toast.loading(t("uploading"));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(
|
const res = await fetch(
|
||||||
`/api/v1/archives/${link.id}/preview`,
|
`/api/v1/archives/${link.id}/preview`,
|
||||||
{
|
{
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: formData,
|
body: formData,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(await res.text());
|
||||||
}
|
}
|
||||||
);
|
|
||||||
|
|
||||||
if (!res.ok) {
|
const data = await res.json();
|
||||||
throw new Error(await res.text());
|
|
||||||
|
setLink({
|
||||||
|
...link,
|
||||||
|
preview: data.preview,
|
||||||
|
});
|
||||||
|
|
||||||
|
toast.success(t("uploaded"));
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
} finally {
|
||||||
|
toast.dismiss(load);
|
||||||
}
|
}
|
||||||
|
}}
|
||||||
const data = await res.json();
|
className="hidden"
|
||||||
|
/>
|
||||||
setLink({
|
</label>
|
||||||
...link,
|
</div>
|
||||||
preview: data.preview,
|
|
||||||
});
|
|
||||||
|
|
||||||
toast.success(t("uploaded"));
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
} finally {
|
|
||||||
toast.dismiss(load);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className="hidden"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="-mt-14 ml-8 relative w-fit pb-2">
|
|
||||||
<div className="tooltip tooltip-bottom" data-tip={t("change_icon")}>
|
|
||||||
<LinkIcon
|
|
||||||
link={link}
|
|
||||||
className="hover:bg-opacity-70 duration-100 cursor-pointer"
|
|
||||||
onClick={() => setIconPopover(true)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{/* #006796 */}
|
|
||||||
{iconPopover && (
|
|
||||||
<IconPopover
|
|
||||||
color={link.color || "#006796"}
|
|
||||||
setColor={(color: string) => setLink({ ...link, color })}
|
|
||||||
weight={(link.iconWeight || "regular") as IconWeight}
|
|
||||||
setWeight={(iconWeight: string) =>
|
|
||||||
setLink({ ...link, iconWeight })
|
|
||||||
}
|
|
||||||
iconName={link.icon as string}
|
|
||||||
setIconName={(icon: string) => setLink({ ...link, icon })}
|
|
||||||
reset={() =>
|
|
||||||
setLink({
|
|
||||||
...link,
|
|
||||||
color: "",
|
|
||||||
icon: "",
|
|
||||||
iconWeight: "",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
className="top-12"
|
|
||||||
onClose={() => {
|
|
||||||
setIconPopover(false);
|
|
||||||
submit();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{standalone ? (
|
||||||
|
<div className="-mt-14 ml-8 relative w-fit pb-2">
|
||||||
|
<LinkIcon link={link} onClick={() => setIconPopover(true)} />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="-mt-14 ml-8 relative w-fit pb-2">
|
||||||
|
<div className="tooltip tooltip-bottom" data-tip={t("change_icon")}>
|
||||||
|
<LinkIcon
|
||||||
|
link={link}
|
||||||
|
className="hover:bg-opacity-70 duration-100 cursor-pointer"
|
||||||
|
onClick={() => setIconPopover(true)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{iconPopover && (
|
||||||
|
<IconPopover
|
||||||
|
color={link.color || "#006796"}
|
||||||
|
setColor={(color: string) => setLink({ ...link, color })}
|
||||||
|
weight={(link.iconWeight || "regular") as IconWeight}
|
||||||
|
setWeight={(iconWeight: string) =>
|
||||||
|
setLink({ ...link, iconWeight })
|
||||||
|
}
|
||||||
|
iconName={link.icon as string}
|
||||||
|
setIconName={(icon: string) => setLink({ ...link, icon })}
|
||||||
|
reset={() =>
|
||||||
|
setLink({
|
||||||
|
...link,
|
||||||
|
color: "",
|
||||||
|
icon: "",
|
||||||
|
iconWeight: "",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className="top-12"
|
||||||
|
onClose={() => {
|
||||||
|
setIconPopover(false);
|
||||||
|
submit();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="max-w-xl sm:px-8 p-5 pb-8 pt-2">
|
<div className="max-w-xl sm:px-8 p-5 pb-8 pt-2">
|
||||||
{fieldToEdit !== "name" ? (
|
{mode === "view" && (
|
||||||
<div className="text-xl mt-2 group pr-7">
|
<div className="text-xl mt-2 pr-7">
|
||||||
<p
|
<p
|
||||||
className={clsx("relative w-fit", !link.name && "text-neutral")}
|
className={clsx("relative w-fit", !link.name && "text-neutral")}
|
||||||
>
|
>
|
||||||
{link.name || t("untitled")}
|
{link.name || t("untitled")}
|
||||||
<EditButton
|
|
||||||
onClick={() => setFieldToEdit("name")}
|
|
||||||
className="top-0"
|
|
||||||
/>
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : fieldToEdit === "name" ? (
|
)}
|
||||||
<form onSubmit={submit} className="flex items-center gap-2">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
autoFocus
|
|
||||||
onBlur={submit}
|
|
||||||
className="text-xl bg-transparent h-9 w-full outline-none border-b border-b-neutral-content"
|
|
||||||
value={link.name}
|
|
||||||
onChange={(e) => setLink({ ...link, name: e.target.value })}
|
|
||||||
/>
|
|
||||||
</form>
|
|
||||||
) : undefined}
|
|
||||||
|
|
||||||
{link.url && (
|
{link.url && (
|
||||||
<>
|
<>
|
||||||
|
@ -360,20 +348,32 @@ export default function LinkDetails({
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{mode === "edit" && (
|
||||||
|
<>
|
||||||
|
<br />
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p className="text-sm mb-2 text-neutral relative w-fit flex justify-between">
|
||||||
|
{t("name")}
|
||||||
|
</p>
|
||||||
|
<TextInput
|
||||||
|
value={link.name}
|
||||||
|
onChange={(e) => setLink({ ...link, name: e.target.value })}
|
||||||
|
placeholder={t("placeholder_example_link")}
|
||||||
|
className="bg-base-200"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
<div className="group relative">
|
<div className="relative">
|
||||||
<p className="text-sm mb-2 text-neutral relative w-fit flex justify-between">
|
<p className="text-sm mb-2 text-neutral relative w-fit flex justify-between">
|
||||||
{t("collection")}
|
{t("collection")}
|
||||||
{fieldToEdit !== "collection" && (
|
|
||||||
<EditButton
|
|
||||||
onClick={() => setFieldToEdit("collection")}
|
|
||||||
className="bottom-0"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{fieldToEdit !== "collection" ? (
|
{mode === "view" ? (
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Link
|
<Link
|
||||||
href={
|
href={
|
||||||
|
@ -404,7 +404,7 @@ export default function LinkDetails({
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
) : fieldToEdit === "collection" ? (
|
) : (
|
||||||
<CollectionSelection
|
<CollectionSelection
|
||||||
onChange={setCollection}
|
onChange={setCollection}
|
||||||
defaultValue={
|
defaultValue={
|
||||||
|
@ -413,26 +413,18 @@ export default function LinkDetails({
|
||||||
: { value: null as unknown as number, label: "Unorganized" }
|
: { value: null as unknown as number, label: "Unorganized" }
|
||||||
}
|
}
|
||||||
creatable={false}
|
creatable={false}
|
||||||
autoFocus
|
|
||||||
onBlur={submit}
|
|
||||||
/>
|
/>
|
||||||
) : undefined}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
<div className="group relative">
|
<div className="relative">
|
||||||
<p className="text-sm mb-2 text-neutral relative w-fit flex justify-between">
|
<p className="text-sm mb-2 text-neutral relative w-fit flex justify-between">
|
||||||
{t("tags")}
|
{t("tags")}
|
||||||
{fieldToEdit !== "tags" && (
|
|
||||||
<EditButton
|
|
||||||
onClick={() => setFieldToEdit("tags")}
|
|
||||||
className="bottom-0"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{fieldToEdit !== "tags" ? (
|
{mode === "view" ? (
|
||||||
<div className="flex gap-2 flex-wrap rounded-md p-2 bg-base-200 border border-base-200 w-full text-xs">
|
<div className="flex gap-2 flex-wrap rounded-md p-2 bg-base-200 border border-base-200 w-full text-xs">
|
||||||
{link.tags[0] ? (
|
{link.tags[0] ? (
|
||||||
link.tags.map((tag) =>
|
link.tags.map((tag) =>
|
||||||
|
@ -464,26 +456,18 @@ export default function LinkDetails({
|
||||||
label: e.name,
|
label: e.name,
|
||||||
value: e.id,
|
value: e.id,
|
||||||
}))}
|
}))}
|
||||||
autoFocus
|
|
||||||
onBlur={submit}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
<div className="relative group">
|
<div className="relative">
|
||||||
<p className="text-sm mb-2 text-neutral relative w-fit flex justify-between">
|
<p className="text-sm mb-2 text-neutral relative w-fit flex justify-between">
|
||||||
{t("description")}
|
{t("description")}
|
||||||
{fieldToEdit !== "description" && (
|
|
||||||
<EditButton
|
|
||||||
onClick={() => setFieldToEdit("description")}
|
|
||||||
className="bottom-0"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{fieldToEdit !== "description" ? (
|
{mode === "view" ? (
|
||||||
<div className="rounded-md p-2 bg-base-200 hyphens-auto">
|
<div className="rounded-md p-2 bg-base-200 hyphens-auto">
|
||||||
{link.description ? (
|
{link.description ? (
|
||||||
<p>{link.description}</p>
|
<p>{link.description}</p>
|
||||||
|
@ -499,134 +483,165 @@ export default function LinkDetails({
|
||||||
}
|
}
|
||||||
placeholder={t("link_description_placeholder")}
|
placeholder={t("link_description_placeholder")}
|
||||||
className="resize-none w-full rounded-md p-2 h-32 border-neutral-content bg-base-200 focus:border-primary border-solid border outline-none duration-100"
|
className="resize-none w-full rounded-md p-2 h-32 border-neutral-content bg-base-200 focus:border-primary border-solid border outline-none duration-100"
|
||||||
autoFocus
|
|
||||||
onBlur={submit}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<br />
|
{mode === "view" && (
|
||||||
|
<div>
|
||||||
|
<br />
|
||||||
|
|
||||||
<p
|
<p
|
||||||
className="text-sm mb-2 text-neutral"
|
className="text-sm mb-2 text-neutral"
|
||||||
title={t("available_formats")}
|
title={t("available_formats")}
|
||||||
>
|
|
||||||
{link.url ? t("preserved_formats") : t("file")}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className={`flex flex-col rounded-md p-3 bg-base-200`}>
|
|
||||||
{monolithAvailable(link) ? (
|
|
||||||
<>
|
|
||||||
<PreservedFormatRow
|
|
||||||
name={t("webpage")}
|
|
||||||
icon={"bi-filetype-html"}
|
|
||||||
format={ArchivedFormat.monolith}
|
|
||||||
link={link}
|
|
||||||
downloadable={true}
|
|
||||||
/>
|
|
||||||
<hr className="m-3 border-t border-neutral-content" />
|
|
||||||
</>
|
|
||||||
) : 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}
|
|
||||||
/>
|
|
||||||
<hr className="m-3 border-t border-neutral-content" />
|
|
||||||
</>
|
|
||||||
) : undefined}
|
|
||||||
|
|
||||||
{pdfAvailable(link) ? (
|
|
||||||
<>
|
|
||||||
<PreservedFormatRow
|
|
||||||
name={t("pdf")}
|
|
||||||
icon={"bi-file-earmark-pdf"}
|
|
||||||
format={ArchivedFormat.pdf}
|
|
||||||
link={link}
|
|
||||||
downloadable={true}
|
|
||||||
/>
|
|
||||||
<hr className="m-3 border-t border-neutral-content" />
|
|
||||||
</>
|
|
||||||
) : undefined}
|
|
||||||
|
|
||||||
{readabilityAvailable(link) ? (
|
|
||||||
<>
|
|
||||||
<PreservedFormatRow
|
|
||||||
name={t("readable")}
|
|
||||||
icon={"bi-file-earmark-text"}
|
|
||||||
format={ArchivedFormat.readability}
|
|
||||||
link={link}
|
|
||||||
/>
|
|
||||||
<hr className="m-3 border-t border-neutral-content" />
|
|
||||||
</>
|
|
||||||
) : undefined}
|
|
||||||
|
|
||||||
{!isReady() && !atLeastOneFormatAvailable() ? (
|
|
||||||
<div
|
|
||||||
className={`w-full h-full flex flex-col justify-center p-10`}
|
|
||||||
>
|
>
|
||||||
<BeatLoader
|
{link.url ? t("preserved_formats") : t("file")}
|
||||||
color="oklch(var(--p))"
|
</p>
|
||||||
className="mx-auto mb-3"
|
|
||||||
size={30}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<p className="text-center text-2xl">
|
<div className={`flex flex-col rounded-md p-3 bg-base-200`}>
|
||||||
{t("preservation_in_queue")}
|
{monolithAvailable(link) ? (
|
||||||
</p>
|
<>
|
||||||
<p className="text-center text-lg">{t("check_back_later")}</p>
|
<PreservedFormatRow
|
||||||
|
name={t("webpage")}
|
||||||
|
icon={"bi-filetype-html"}
|
||||||
|
format={ArchivedFormat.monolith}
|
||||||
|
link={link}
|
||||||
|
downloadable={true}
|
||||||
|
/>
|
||||||
|
<hr className="m-3 border-t border-neutral-content" />
|
||||||
|
</>
|
||||||
|
) : 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}
|
||||||
|
/>
|
||||||
|
<hr className="m-3 border-t border-neutral-content" />
|
||||||
|
</>
|
||||||
|
) : undefined}
|
||||||
|
|
||||||
|
{pdfAvailable(link) ? (
|
||||||
|
<>
|
||||||
|
<PreservedFormatRow
|
||||||
|
name={t("pdf")}
|
||||||
|
icon={"bi-file-earmark-pdf"}
|
||||||
|
format={ArchivedFormat.pdf}
|
||||||
|
link={link}
|
||||||
|
downloadable={true}
|
||||||
|
/>
|
||||||
|
<hr className="m-3 border-t border-neutral-content" />
|
||||||
|
</>
|
||||||
|
) : undefined}
|
||||||
|
|
||||||
|
{readabilityAvailable(link) ? (
|
||||||
|
<>
|
||||||
|
<PreservedFormatRow
|
||||||
|
name={t("readable")}
|
||||||
|
icon={"bi-file-earmark-text"}
|
||||||
|
format={ArchivedFormat.readability}
|
||||||
|
link={link}
|
||||||
|
/>
|
||||||
|
<hr className="m-3 border-t border-neutral-content" />
|
||||||
|
</>
|
||||||
|
) : 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>
|
||||||
) : link.url && !isReady() && atLeastOneFormatAvailable() ? (
|
</div>
|
||||||
<div className={`w-full h-full flex flex-col justify-center p-5`}>
|
)}
|
||||||
<BeatLoader
|
|
||||||
color="oklch(var(--p))"
|
{mode === "view" ? (
|
||||||
className="mx-auto mb-3"
|
<>
|
||||||
size={20}
|
<br />
|
||||||
/>
|
|
||||||
<p className="text-center">{t("there_are_more_formats")}</p>
|
<p className="text-neutral text-xs text-center">
|
||||||
<p className="text-center text-sm">{t("check_back_later")}</p>
|
{t("saved")}{" "}
|
||||||
|
{new Date(link.createdAt || "").toLocaleDateString("en-US", {
|
||||||
|
month: "short",
|
||||||
|
day: "numeric",
|
||||||
|
year: "numeric",
|
||||||
|
})}{" "}
|
||||||
|
at{" "}
|
||||||
|
{new Date(link.createdAt || "").toLocaleTimeString("en-US", {
|
||||||
|
hour: "numeric",
|
||||||
|
minute: "numeric",
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<br />
|
||||||
|
<div className="flex justify-end items-center">
|
||||||
|
<button
|
||||||
|
className={clsx(
|
||||||
|
"btn btn-accent text-white",
|
||||||
|
JSON.stringify(activeLink) === JSON.stringify(link)
|
||||||
|
? "btn-disabled"
|
||||||
|
: "dark:border-violet-400"
|
||||||
|
)}
|
||||||
|
onClick={submit}
|
||||||
|
>
|
||||||
|
{t("save_changes")}
|
||||||
|
</button>
|
||||||
</div>
|
</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>
|
|
||||||
|
|
||||||
<br />
|
|
||||||
|
|
||||||
<p className="text-neutral text-xs text-center">
|
|
||||||
{t("saved")}{" "}
|
|
||||||
{new Date(link.createdAt || "").toLocaleDateString("en-US", {
|
|
||||||
month: "short",
|
|
||||||
day: "numeric",
|
|
||||||
year: "numeric",
|
|
||||||
})}{" "}
|
|
||||||
at{" "}
|
|
||||||
{new Date(link.createdAt || "").toLocaleTimeString("en-US", {
|
|
||||||
hour: "numeric",
|
|
||||||
minute: "numeric",
|
|
||||||
})}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -4,14 +4,13 @@ import {
|
||||||
LinkIncludingShortenedCollectionAndTags,
|
LinkIncludingShortenedCollectionAndTags,
|
||||||
} from "@/types/global";
|
} from "@/types/global";
|
||||||
import usePermissions from "@/hooks/usePermissions";
|
import usePermissions from "@/hooks/usePermissions";
|
||||||
import EditLinkModal from "@/components/ModalContent/EditLinkModal";
|
|
||||||
import DeleteLinkModal from "@/components/ModalContent/DeleteLinkModal";
|
import DeleteLinkModal from "@/components/ModalContent/DeleteLinkModal";
|
||||||
import { dropdownTriggerer } from "@/lib/client/utils";
|
import { dropdownTriggerer } from "@/lib/client/utils";
|
||||||
import { useTranslation } from "next-i18next";
|
import { useTranslation } from "next-i18next";
|
||||||
import { useUser } from "@/hooks/store/user";
|
import { useUser } from "@/hooks/store/user";
|
||||||
import { useDeleteLink, useGetLink, useUpdateLink } from "@/hooks/store/links";
|
import { useDeleteLink, useGetLink, useUpdateLink } from "@/hooks/store/links";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
import LinkDetailModal from "@/components/ModalContent/LinkDetailModal";
|
import LinkModal from "@/components/ModalContent/LinkModal";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
@ -34,7 +33,7 @@ export default function LinkActions({
|
||||||
const getLink = useGetLink();
|
const getLink = useGetLink();
|
||||||
|
|
||||||
const [editLinkModal, setEditLinkModal] = useState(false);
|
const [editLinkModal, setEditLinkModal] = useState(false);
|
||||||
const [linkDetailModal, setLinkDetailModal] = useState(false);
|
const [linkModal, setLinkModal] = useState(false);
|
||||||
const [deleteLinkModal, setDeleteLinkModal] = useState(false);
|
const [deleteLinkModal, setDeleteLinkModal] = useState(false);
|
||||||
|
|
||||||
const { data: user = {} } = useUser();
|
const { data: user = {} } = useUser();
|
||||||
|
@ -96,7 +95,7 @@ export default function LinkActions({
|
||||||
className={`absolute ${position || "top-3 right-3"} ${
|
className={`absolute ${position || "top-3 right-3"} ${
|
||||||
alignToTop ? "" : "dropdown-end"
|
alignToTop ? "" : "dropdown-end"
|
||||||
} z-20`}
|
} z-20`}
|
||||||
onClick={() => setLinkDetailModal(true)}
|
onClick={() => setLinkModal(true)}
|
||||||
>
|
>
|
||||||
<div className="btn btn-ghost btn-sm btn-square text-neutral">
|
<div className="btn btn-ghost btn-sm btn-square text-neutral">
|
||||||
<i title="More" className="bi-three-dots text-xl" />
|
<i title="More" className="bi-three-dots text-xl" />
|
||||||
|
@ -144,7 +143,7 @@ export default function LinkActions({
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
(document?.activeElement as HTMLElement)?.blur();
|
(document?.activeElement as HTMLElement)?.blur();
|
||||||
setLinkDetailModal(true);
|
setLinkModal(true);
|
||||||
}}
|
}}
|
||||||
className="whitespace-nowrap"
|
className="whitespace-nowrap"
|
||||||
>
|
>
|
||||||
|
@ -217,9 +216,13 @@ export default function LinkActions({
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{editLinkModal && (
|
{editLinkModal && (
|
||||||
<EditLinkModal
|
<LinkModal
|
||||||
onClose={() => setEditLinkModal(false)}
|
onClose={() => setEditLinkModal(false)}
|
||||||
activeLink={link}
|
onPin={pinLink}
|
||||||
|
onUpdateArchive={updateArchive}
|
||||||
|
onDelete={() => setDeleteLinkModal(true)}
|
||||||
|
link={link}
|
||||||
|
activeMode="edit"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{deleteLinkModal && (
|
{deleteLinkModal && (
|
||||||
|
@ -228,10 +231,9 @@ export default function LinkActions({
|
||||||
activeLink={link}
|
activeLink={link}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{linkDetailModal && (
|
{linkModal && (
|
||||||
<LinkDetailModal
|
<LinkModal
|
||||||
onClose={() => setLinkDetailModal(false)}
|
onClose={() => setLinkModal(false)}
|
||||||
onEdit={() => setEditLinkModal(true)}
|
|
||||||
onPin={pinLink}
|
onPin={pinLink}
|
||||||
onUpdateArchive={updateArchive}
|
onUpdateArchive={updateArchive}
|
||||||
onDelete={() => setDeleteLinkModal(true)}
|
onDelete={() => setDeleteLinkModal(true)}
|
||||||
|
|
|
@ -1,161 +0,0 @@
|
||||||
import React, { useEffect, useState } from "react";
|
|
||||||
import CollectionSelection from "@/components/InputSelect/CollectionSelection";
|
|
||||||
import TagSelection from "@/components/InputSelect/TagSelection";
|
|
||||||
import TextInput from "@/components/TextInput";
|
|
||||||
import unescapeString from "@/lib/client/unescapeString";
|
|
||||||
import {
|
|
||||||
ArchivedFormat,
|
|
||||||
LinkIncludingShortenedCollectionAndTags,
|
|
||||||
} from "@/types/global";
|
|
||||||
import Link from "next/link";
|
|
||||||
import Modal from "../Modal";
|
|
||||||
import { useTranslation } from "next-i18next";
|
|
||||||
import { useUpdateLink } from "@/hooks/store/links";
|
|
||||||
import toast from "react-hot-toast";
|
|
||||||
import IconPicker from "../IconPicker";
|
|
||||||
import { IconWeight } from "@phosphor-icons/react";
|
|
||||||
import Image from "next/image";
|
|
||||||
import { previewAvailable } from "@/lib/shared/getArchiveValidity";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
onClose: Function;
|
|
||||||
activeLink: LinkIncludingShortenedCollectionAndTags;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function EditLinkModal({ onClose, activeLink }: Props) {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const [link, setLink] =
|
|
||||||
useState<LinkIncludingShortenedCollectionAndTags>(activeLink);
|
|
||||||
|
|
||||||
let shortenedURL;
|
|
||||||
try {
|
|
||||||
shortenedURL = new URL(link.url || "").host.toLowerCase();
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
const [submitLoader, setSubmitLoader] = useState(false);
|
|
||||||
|
|
||||||
const updateLink = useUpdateLink();
|
|
||||||
|
|
||||||
const setCollection = (e: any) => {
|
|
||||||
if (e?.__isNew__) e.value = null;
|
|
||||||
setLink({
|
|
||||||
...link,
|
|
||||||
collection: { id: e?.value, name: e?.label, ownerId: e?.ownerId },
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const setTags = (e: any) => {
|
|
||||||
const tagNames = e.map((e: any) => ({ name: e.label }));
|
|
||||||
setLink({ ...link, tags: tagNames });
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setLink(activeLink);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const submit = async () => {
|
|
||||||
if (!submitLoader) {
|
|
||||||
setSubmitLoader(true);
|
|
||||||
|
|
||||||
const load = toast.loading(t("updating"));
|
|
||||||
|
|
||||||
await updateLink.mutateAsync(link, {
|
|
||||||
onSettled: (data, error) => {
|
|
||||||
toast.dismiss(load);
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
toast.error(error.message);
|
|
||||||
} else {
|
|
||||||
onClose();
|
|
||||||
toast.success(t("updated"));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
setSubmitLoader(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal toggleModal={onClose}>
|
|
||||||
<p className="text-xl font-thin">{t("edit_link")}</p>
|
|
||||||
|
|
||||||
<div className="divider mb-3 mt-1"></div>
|
|
||||||
|
|
||||||
{link.url && (
|
|
||||||
<Link
|
|
||||||
href={link.url}
|
|
||||||
className="truncate text-neutral flex gap-2 mb-5 w-fit max-w-full"
|
|
||||||
title={link.url}
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
<i className="bi-link-45deg text-xl" />
|
|
||||||
<p>{shortenedURL}</p>
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="w-full">
|
|
||||||
<p className="mb-2">{t("name")}</p>
|
|
||||||
<TextInput
|
|
||||||
value={link.name}
|
|
||||||
onChange={(e) => setLink({ ...link, name: e.target.value })}
|
|
||||||
placeholder={t("placeholder_example_link")}
|
|
||||||
className="bg-base-200"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-5">
|
|
||||||
<div className="grid sm:grid-cols-2 gap-3">
|
|
||||||
<div>
|
|
||||||
<p className="mb-2">{t("collection")}</p>
|
|
||||||
{link.collection.name && (
|
|
||||||
<CollectionSelection
|
|
||||||
onChange={setCollection}
|
|
||||||
defaultValue={
|
|
||||||
link.collection.id
|
|
||||||
? { value: link.collection.id, label: link.collection.name }
|
|
||||||
: { value: null as unknown as number, label: "Unorganized" }
|
|
||||||
}
|
|
||||||
creatable={false}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<p className="mb-2">{t("tags")}</p>
|
|
||||||
<TagSelection
|
|
||||||
onChange={setTags}
|
|
||||||
defaultValue={link.tags.map((e) => ({
|
|
||||||
label: e.name,
|
|
||||||
value: e.id,
|
|
||||||
}))}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="sm:col-span-2">
|
|
||||||
<p className="mb-2">{t("description")}</p>
|
|
||||||
<textarea
|
|
||||||
value={unescapeString(link.description) as string}
|
|
||||||
onChange={(e) =>
|
|
||||||
setLink({ ...link, description: e.target.value })
|
|
||||||
}
|
|
||||||
placeholder={t("link_description_placeholder")}
|
|
||||||
className="resize-none w-full rounded-md p-2 h-32 border-neutral-content bg-base-200 focus:border-primary border-solid border outline-none duration-100"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex justify-end items-center mt-5">
|
|
||||||
<button
|
|
||||||
className="btn btn-accent dark:border-violet-400 text-white"
|
|
||||||
onClick={submit}
|
|
||||||
>
|
|
||||||
{t("save_changes")}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,240 +0,0 @@
|
||||||
import React, { useEffect, useState } from "react";
|
|
||||||
import {
|
|
||||||
ArchivedFormat,
|
|
||||||
LinkIncludingShortenedCollectionAndTags,
|
|
||||||
} from "@/types/global";
|
|
||||||
import getPublicUserData from "@/lib/client/getPublicUserData";
|
|
||||||
import { useTranslation } from "next-i18next";
|
|
||||||
import { useUser } from "@/hooks/store/user";
|
|
||||||
import { useDeleteLink, useGetLink, useUpdateLink } 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";
|
|
||||||
import { dropdownTriggerer } from "@/lib/client/utils";
|
|
||||||
import toast from "react-hot-toast";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
onClose: Function;
|
|
||||||
onEdit: Function;
|
|
||||||
onDelete: Function;
|
|
||||||
onUpdateArchive: Function;
|
|
||||||
onPin: Function;
|
|
||||||
link: LinkIncludingShortenedCollectionAndTags;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function LinkDetailModal({
|
|
||||||
onClose,
|
|
||||||
onEdit,
|
|
||||||
onDelete,
|
|
||||||
onUpdateArchive,
|
|
||||||
onPin,
|
|
||||||
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;
|
|
||||||
|
|
||||||
const updateLink = useUpdateLink();
|
|
||||||
const deleteLink = useDeleteLink();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Drawer toggleDrawer={onClose} className="sm:h-screen sm:flex relative">
|
|
||||||
<div
|
|
||||||
className="bi-x text-xl btn btn-sm btn-circle text-base-content opacity-50 hover:opacity-100 absolute top-3 left-3 z-10"
|
|
||||||
onClick={() => onClose()}
|
|
||||||
></div>
|
|
||||||
|
|
||||||
<div className={`dropdown dropdown-end absolute top-3 right-12 z-20`}>
|
|
||||||
<div
|
|
||||||
tabIndex={0}
|
|
||||||
role="button"
|
|
||||||
onMouseDown={dropdownTriggerer}
|
|
||||||
className="btn btn-sm btn-circle text-base-content opacity-50 hover:opacity-100 z-10"
|
|
||||||
>
|
|
||||||
<i title="More" className="bi-three-dots text-xl" />
|
|
||||||
</div>
|
|
||||||
<ul
|
|
||||||
className={`dropdown-content z-[20] menu shadow bg-base-200 border border-neutral-content rounded-box`}
|
|
||||||
>
|
|
||||||
{(permissions === true || permissions?.canUpdate) && (
|
|
||||||
<li>
|
|
||||||
<div
|
|
||||||
role="button"
|
|
||||||
tabIndex={0}
|
|
||||||
onClick={() => {
|
|
||||||
(document?.activeElement as HTMLElement)?.blur();
|
|
||||||
onPin();
|
|
||||||
}}
|
|
||||||
className="whitespace-nowrap"
|
|
||||||
>
|
|
||||||
{link?.pinnedBy && link.pinnedBy[0]
|
|
||||||
? t("unpin")
|
|
||||||
: t("pin_to_dashboard")}
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
)}
|
|
||||||
{(permissions === true || permissions?.canUpdate) && (
|
|
||||||
<li>
|
|
||||||
<div
|
|
||||||
role="button"
|
|
||||||
tabIndex={0}
|
|
||||||
onClick={() => {
|
|
||||||
(document?.activeElement as HTMLElement)?.blur();
|
|
||||||
onEdit();
|
|
||||||
onClose();
|
|
||||||
}}
|
|
||||||
className="whitespace-nowrap"
|
|
||||||
>
|
|
||||||
{t("edit_link")}
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
)}
|
|
||||||
{link.type === "url" && permissions === true && (
|
|
||||||
<li>
|
|
||||||
<div
|
|
||||||
role="button"
|
|
||||||
tabIndex={0}
|
|
||||||
onClick={() => {
|
|
||||||
(document?.activeElement as HTMLElement)?.blur();
|
|
||||||
onUpdateArchive();
|
|
||||||
}}
|
|
||||||
className="whitespace-nowrap"
|
|
||||||
>
|
|
||||||
{t("refresh_preserved_formats")}
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
)}
|
|
||||||
{(permissions === true || permissions?.canDelete) && (
|
|
||||||
<li>
|
|
||||||
<div
|
|
||||||
role="button"
|
|
||||||
tabIndex={0}
|
|
||||||
onClick={async (e) => {
|
|
||||||
(document?.activeElement as HTMLElement)?.blur();
|
|
||||||
console.log(e.shiftKey);
|
|
||||||
if (e.shiftKey) {
|
|
||||||
const load = toast.loading(t("deleting"));
|
|
||||||
|
|
||||||
await deleteLink.mutateAsync(link.id as number, {
|
|
||||||
onSettled: (data, error) => {
|
|
||||||
toast.dismiss(load);
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
toast.error(error.message);
|
|
||||||
} else {
|
|
||||||
toast.success(t("deleted"));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
onClose();
|
|
||||||
} else {
|
|
||||||
onDelete();
|
|
||||||
onClose();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className="whitespace-nowrap"
|
|
||||||
>
|
|
||||||
{t("delete")}
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
)}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<Link
|
|
||||||
href={isPublicRoute ? `/public/links/${link.id}` : `/links/${link.id}`}
|
|
||||||
target="_blank"
|
|
||||||
className="bi-box-arrow-up-right btn-circle text-base-content opacity-50 hover:opacity-100 btn btn-sm absolute top-3 right-3 select-none z-10"
|
|
||||||
></Link>
|
|
||||||
|
|
||||||
<div className="w-full">
|
|
||||||
<LinkDetails activeLink={link} className="sm:mt-0 -mt-11" />
|
|
||||||
</div>
|
|
||||||
</Drawer>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -0,0 +1,265 @@
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import {
|
||||||
|
ArchivedFormat,
|
||||||
|
LinkIncludingShortenedCollectionAndTags,
|
||||||
|
} from "@/types/global";
|
||||||
|
import getPublicUserData from "@/lib/client/getPublicUserData";
|
||||||
|
import { useTranslation } from "next-i18next";
|
||||||
|
import { useUser } from "@/hooks/store/user";
|
||||||
|
import { useDeleteLink, useGetLink, useUpdateLink } 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";
|
||||||
|
import { dropdownTriggerer } from "@/lib/client/utils";
|
||||||
|
import toast from "react-hot-toast";
|
||||||
|
import clsx from "clsx";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
onClose: Function;
|
||||||
|
onDelete: Function;
|
||||||
|
onUpdateArchive: Function;
|
||||||
|
onPin: Function;
|
||||||
|
link: LinkIncludingShortenedCollectionAndTags;
|
||||||
|
activeMode?: "view" | "edit";
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function LinkModal({
|
||||||
|
onClose,
|
||||||
|
onDelete,
|
||||||
|
onUpdateArchive,
|
||||||
|
onPin,
|
||||||
|
link,
|
||||||
|
activeMode,
|
||||||
|
}: 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;
|
||||||
|
|
||||||
|
const deleteLink = useDeleteLink();
|
||||||
|
|
||||||
|
const [mode, setMode] = useState<"view" | "edit">(activeMode || "view");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Drawer
|
||||||
|
toggleDrawer={onClose}
|
||||||
|
className="sm:h-screen items-center relative"
|
||||||
|
>
|
||||||
|
<div className="absolute top-3 left-0 right-0 flex justify-between px-3">
|
||||||
|
<div
|
||||||
|
className="bi-x text-xl btn btn-sm btn-circle text-base-content opacity-50 hover:opacity-100 z-10"
|
||||||
|
onClick={() => onClose()}
|
||||||
|
></div>
|
||||||
|
|
||||||
|
<div className="flex gap-1 h-8 rounded-full bg-neutral-content bg-opacity-50 text-base-content p-1 text-xs duration-100 select-none z-10">
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
"py-1 px-2 cursor-pointer duration-100 rounded-full font-semibold",
|
||||||
|
mode === "view" && "bg-primary bg-opacity-50"
|
||||||
|
)}
|
||||||
|
onClick={() => {
|
||||||
|
setMode("view");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
View
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
"py-1 px-2 cursor-pointer duration-100 rounded-full font-semibold",
|
||||||
|
mode === "edit" && "bg-primary bg-opacity-50"
|
||||||
|
)}
|
||||||
|
onClick={() => {
|
||||||
|
setMode("edit");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Edit
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<div className={`dropdown dropdown-end z-20`}>
|
||||||
|
<div
|
||||||
|
tabIndex={0}
|
||||||
|
role="button"
|
||||||
|
onMouseDown={dropdownTriggerer}
|
||||||
|
className="btn btn-sm btn-circle text-base-content opacity-50 hover:opacity-100 z-10"
|
||||||
|
>
|
||||||
|
<i title="More" className="bi-three-dots text-xl" />
|
||||||
|
</div>
|
||||||
|
<ul
|
||||||
|
className={`dropdown-content z-[20] menu shadow bg-base-200 border border-neutral-content rounded-box`}
|
||||||
|
>
|
||||||
|
{(permissions === true || permissions?.canUpdate) && (
|
||||||
|
<li>
|
||||||
|
<div
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
onClick={() => {
|
||||||
|
(document?.activeElement as HTMLElement)?.blur();
|
||||||
|
onPin();
|
||||||
|
}}
|
||||||
|
className="whitespace-nowrap"
|
||||||
|
>
|
||||||
|
{link?.pinnedBy && link.pinnedBy[0]
|
||||||
|
? t("unpin")
|
||||||
|
: t("pin_to_dashboard")}
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
{link.type === "url" && permissions === true && (
|
||||||
|
<li>
|
||||||
|
<div
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
onClick={() => {
|
||||||
|
(document?.activeElement as HTMLElement)?.blur();
|
||||||
|
onUpdateArchive();
|
||||||
|
}}
|
||||||
|
className="whitespace-nowrap"
|
||||||
|
>
|
||||||
|
{t("refresh_preserved_formats")}
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
{(permissions === true || permissions?.canDelete) && (
|
||||||
|
<li>
|
||||||
|
<div
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
onClick={async (e) => {
|
||||||
|
(document?.activeElement as HTMLElement)?.blur();
|
||||||
|
console.log(e.shiftKey);
|
||||||
|
if (e.shiftKey) {
|
||||||
|
const load = toast.loading(t("deleting"));
|
||||||
|
|
||||||
|
await deleteLink.mutateAsync(link.id as number, {
|
||||||
|
onSettled: (data, error) => {
|
||||||
|
toast.dismiss(load);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
toast.error(error.message);
|
||||||
|
} else {
|
||||||
|
toast.success(t("deleted"));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
onClose();
|
||||||
|
} else {
|
||||||
|
onDelete();
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="whitespace-nowrap"
|
||||||
|
>
|
||||||
|
{t("delete")}
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<Link
|
||||||
|
href={
|
||||||
|
isPublicRoute ? `/public/links/${link.id}` : `/links/${link.id}`
|
||||||
|
}
|
||||||
|
target="_blank"
|
||||||
|
className="bi-box-arrow-up-right btn-circle text-base-content opacity-50 hover:opacity-100 btn btn-sm select-none z-10"
|
||||||
|
></Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="w-full">
|
||||||
|
<LinkDetails
|
||||||
|
activeLink={link}
|
||||||
|
className="sm:mt-0 -mt-11"
|
||||||
|
mode={mode}
|
||||||
|
setMode={(mode: "view" | "edit") => setMode(mode)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Drawer>
|
||||||
|
);
|
||||||
|
}
|
Ŝarĝante…
Reference in New Issue