improved edit view

This commit is contained in:
daniel31x13 2024-08-30 17:29:15 -04:00
parent 1a378de267
commit d20c915970
6 changed files with 548 additions and 692 deletions

View File

@ -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;

View File

@ -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>

View File

@ -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)}

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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>
);
}