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
+270 -255
View File
@@ -26,22 +26,26 @@ import { IconWeight } from "@phosphor-icons/react";
import Image from "next/image";
import clsx from "clsx";
import toast from "react-hot-toast";
import EditButton from "./EditButton";
import CollectionSelection from "./InputSelect/CollectionSelection";
import TagSelection from "./InputSelect/TagSelection";
import unescapeString from "@/lib/client/unescapeString";
import IconPopover from "./IconPopover";
import TextInput from "./TextInput";
type Props = {
className?: string;
activeLink: LinkIncludingShortenedCollectionAndTags;
standalone?: boolean;
mode?: "view" | "edit";
setMode?: Function;
};
export default function LinkDetails({
className,
activeLink,
standalone,
mode = "view",
setMode,
}: Props) {
const [link, setLink] =
useState<LinkIncludingShortenedCollectionAndTags>(activeLink);
@@ -144,21 +148,13 @@ export default function LinkDetails({
const updateLink = useUpdateLink();
const [fieldToEdit, setFieldToEdit] = useState<
"name" | "collection" | "tags" | "description" | null
>(null);
const submit = async (e?: any) => {
e?.preventDefault();
const { updatedAt: b, ...oldLink } = activeLink;
const { updatedAt: a, ...newLink } = link;
console.log(oldLink);
console.log(newLink);
if (JSON.stringify(oldLink) === JSON.stringify(newLink)) {
setFieldToEdit(null);
return;
}
@@ -172,7 +168,7 @@ export default function LinkDetails({
toast.error(error.message);
} else {
toast.success(t("updated"));
setFieldToEdit(null);
setMode && setMode("view");
console.log(data);
setLink(data);
}
@@ -204,7 +200,7 @@ export default function LinkDetails({
>
<div
className={clsx(
"overflow-hidden select-none relative h-32 opacity-80 group",
"overflow-hidden select-none relative group h-40 opacity-80",
standalone
? "sm:max-w-xl -mx-5 -mt-5 sm:rounded-t-2xl"
: "-mx-4 -mt-4"
@@ -226,120 +222,112 @@ export default function LinkDetails({
}}
/>
) : 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">
<label className="btn btn-xs mb-2 mr-12 opacity-50 hover:opacity-100">
{t("upload_preview_image")}
<input
type="file"
accept="image/jpg, image/jpeg, image/png"
onChange={async (e) => {
const file = e.target.files?.[0];
if (!file) return;
{!standalone && (
<div className="absolute top-0 bottom-0 left-0 right-0 opacity-0 group-hover:opacity-100 duration-100 flex justify-end items-end">
<label className="btn btn-xs mb-2 mr-3 opacity-50 hover:opacity-100">
{t("upload_preview_image")}
<input
type="file"
accept="image/jpg, image/jpeg, image/png"
onChange={async (e) => {
const file = e.target.files?.[0];
if (!file) return;
const formData = new FormData();
formData.append("file", file);
const formData = new FormData();
formData.append("file", file);
const load = toast.loading(t("uploading"));
const load = toast.loading(t("uploading"));
try {
const res = await fetch(
`/api/v1/archives/${link.id}/preview`,
{
method: "POST",
body: formData,
try {
const res = await fetch(
`/api/v1/archives/${link.id}/preview`,
{
method: "POST",
body: formData,
}
);
if (!res.ok) {
throw new Error(await res.text());
}
);
if (!res.ok) {
throw new Error(await res.text());
const data = await res.json();
setLink({
...link,
preview: data.preview,
});
toast.success(t("uploaded"));
} catch (error) {
console.error(error);
} finally {
toast.dismiss(load);
}
const data = await res.json();
setLink({
...link,
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();
}}
/>
}}
className="hidden"
/>
</label>
</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">
{fieldToEdit !== "name" ? (
<div className="text-xl mt-2 group pr-7">
{mode === "view" && (
<div className="text-xl mt-2 pr-7">
<p
className={clsx("relative w-fit", !link.name && "text-neutral")}
>
{link.name || t("untitled")}
<EditButton
onClick={() => setFieldToEdit("name")}
className="top-0"
/>
</p>
</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 && (
<>
@@ -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 />
<div className="group relative">
<div className="relative">
<p className="text-sm mb-2 text-neutral relative w-fit flex justify-between">
{t("collection")}
{fieldToEdit !== "collection" && (
<EditButton
onClick={() => setFieldToEdit("collection")}
className="bottom-0"
/>
)}
</p>
{fieldToEdit !== "collection" ? (
{mode === "view" ? (
<div className="relative">
<Link
href={
@@ -404,7 +404,7 @@ export default function LinkDetails({
</div>
</Link>
</div>
) : fieldToEdit === "collection" ? (
) : (
<CollectionSelection
onChange={setCollection}
defaultValue={
@@ -413,26 +413,18 @@ export default function LinkDetails({
: { value: null as unknown as number, label: "Unorganized" }
}
creatable={false}
autoFocus
onBlur={submit}
/>
) : undefined}
)}
</div>
<br />
<div className="group relative">
<div className="relative">
<p className="text-sm mb-2 text-neutral relative w-fit flex justify-between">
{t("tags")}
{fieldToEdit !== "tags" && (
<EditButton
onClick={() => setFieldToEdit("tags")}
className="bottom-0"
/>
)}
</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">
{link.tags[0] ? (
link.tags.map((tag) =>
@@ -464,26 +456,18 @@ export default function LinkDetails({
label: e.name,
value: e.id,
}))}
autoFocus
onBlur={submit}
/>
)}
</div>
<br />
<div className="relative group">
<div className="relative">
<p className="text-sm mb-2 text-neutral relative w-fit flex justify-between">
{t("description")}
{fieldToEdit !== "description" && (
<EditButton
onClick={() => setFieldToEdit("description")}
className="bottom-0"
/>
)}
</p>
{fieldToEdit !== "description" ? (
{mode === "view" ? (
<div className="rounded-md p-2 bg-base-200 hyphens-auto">
{link.description ? (
<p>{link.description}</p>
@@ -499,134 +483,165 @@ export default function LinkDetails({
}
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"
autoFocus
onBlur={submit}
/>
)}
</div>
<br />
{mode === "view" && (
<div>
<br />
<p
className="text-sm mb-2 text-neutral"
title={t("available_formats")}
>
{link.url ? t("preserved_formats") : t("file")}
</p>
<div className={`flex flex-col 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`}
<p
className="text-sm mb-2 text-neutral"
title={t("available_formats")}
>
<BeatLoader
color="oklch(var(--p))"
className="mx-auto mb-3"
size={30}
/>
{link.url ? t("preserved_formats") : t("file")}
</p>
<p className="text-center text-2xl">
{t("preservation_in_queue")}
</p>
<p className="text-center text-lg">{t("check_back_later")}</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
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>
) : 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>
)}
{mode === "view" ? (
<>
<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>
</>
) : (
<>
<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>
) : 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>