better looking detail modal

This commit is contained in:
daniel31x13 2024-08-28 20:22:11 -04:00
parent 6498ae794b
commit 2d0e52f65b
9 changed files with 427 additions and 212 deletions

View File

@ -46,7 +46,7 @@ export default function Drawer({
data-testid="mobile-modal-container" data-testid="mobile-modal-container"
> >
<div <div
className="mx-auto w-12 h-1.5 flex-shrink-0 rounded-full bg-neutral mb-5" className="mx-auto w-12 h-1.5 flex-shrink-0 rounded-full bg-neutral mb-5 relative z-20"
data-testid="mobile-modal-slider" data-testid="mobile-modal-slider"
/> />
{children} {children}

View File

@ -10,6 +10,7 @@ import {
readabilityAvailable, readabilityAvailable,
monolithAvailable, monolithAvailable,
screenshotAvailable, screenshotAvailable,
previewAvailable,
} from "@/lib/shared/getArchiveValidity"; } from "@/lib/shared/getArchiveValidity";
import PreservedFormatRow from "@/components/PreserverdFormatRow"; import PreservedFormatRow from "@/components/PreserverdFormatRow";
import getPublicUserData from "@/lib/client/getPublicUserData"; import getPublicUserData from "@/lib/client/getPublicUserData";
@ -22,13 +23,22 @@ import CopyButton from "./CopyButton";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import Icon from "./Icon"; import Icon from "./Icon";
import { IconWeight } from "@phosphor-icons/react"; import { IconWeight } from "@phosphor-icons/react";
import Image from "next/image";
import clsx from "clsx";
type Props = { type Props = {
className?: string; className?: string;
link: LinkIncludingShortenedCollectionAndTags; link: LinkIncludingShortenedCollectionAndTags;
standalone?: boolean;
editMode?: boolean;
}; };
export default function LinkDetails({ className, link }: Props) { export default function LinkDetails({
className,
link,
standalone,
editMode,
}: Props) {
const { t } = useTranslation(); const { t } = useTranslation();
const session = useSession(); const session = useSession();
const getLink = useGetLink(); const getLink = useGetLink();
@ -126,11 +136,46 @@ export default function LinkDetails({ className, link }: Props) {
const isPublicRoute = router.pathname.startsWith("/public") ? true : false; const isPublicRoute = router.pathname.startsWith("/public") ? true : false;
return ( return (
<div className={className} data-vaul-no-drag> <div className={clsx(className)} data-vaul-no-drag>
<div className="mx-auto w-fit"> <div
<LinkIcon link={link} hideBackground /> className={clsx(
standalone && "sm:border sm:border-neutral-content sm:rounded-2xl p-5"
)}
>
<div
className={clsx(
"overflow-hidden select-none relative h-32 opacity-80 bg-opacity-80",
standalone
? "sm:max-w-xl -mx-5 -mt-5 sm:rounded-t-2xl"
: "-mx-4 -mt-4"
)}
>
{previewAvailable(link) ? (
<Image
src={`/api/v1/archives/${link.id}?format=${ArchivedFormat.jpeg}&preview=true`}
width={1280}
height={720}
alt=""
className="object-cover scale-105"
style={{
filter: "blur(1px)",
}}
onError={(e) => {
const target = e.target as HTMLElement;
target.style.display = "none";
}}
/>
) : link.preview === "unavailable" ? (
<div className="bg-gray-50 duration-100 h-32"></div>
) : (
<div className="duration-100 h-32 skeleton rounded-b-none"></div>
)}
<div className="absolute top-0 left-0 right-0 bottom-0 flex items-center justify-center rounded-md">
<LinkIcon link={link} />
</div>
</div> </div>
<div className="max-w-xl sm:px-8 p-5 pb-8 pt-2">
{link.name && <p className="text-xl text-center mt-2">{link.name}</p>} {link.name && <p className="text-xl text-center mt-2">{link.name}</p>}
{link.url && ( {link.url && (
@ -171,7 +216,9 @@ export default function LinkDetails({ className, link }: Props) {
<Icon <Icon
icon={link.collection.icon} icon={link.collection.icon}
size={30} size={30}
weight={(link.collection.iconWeight || "regular") as IconWeight} weight={
(link.collection.iconWeight || "regular") as IconWeight
}
color={link.collection.color || "#0ea5e9"} color={link.collection.color || "#0ea5e9"}
/> />
) : ( ) : (
@ -195,7 +242,10 @@ export default function LinkDetails({ className, link }: Props) {
<div className="flex gap-2 flex-wrap"> <div className="flex gap-2 flex-wrap">
{link.tags.map((tag) => {link.tags.map((tag) =>
isPublicRoute ? ( isPublicRoute ? (
<div key={tag.id} className="rounded-lg px-3 py-1 bg-base-200"> <div
key={tag.id}
className="rounded-lg px-3 py-1 bg-base-200"
>
{tag.name} {tag.name}
</div> </div>
) : ( ) : (
@ -228,7 +278,10 @@ export default function LinkDetails({ className, link }: Props) {
<br /> <br />
<p className="text-sm mb-2 text-neutral" title={t("available_formats")}> <p
className="text-sm mb-2 text-neutral"
title={t("available_formats")}
>
{link.url ? t("preserved_formats") : t("file")} {link.url ? t("preserved_formats") : t("file")}
</p> </p>
@ -277,14 +330,18 @@ export default function LinkDetails({ className, link }: Props) {
) : undefined} ) : undefined}
{!isReady() && !atLeastOneFormatAvailable() ? ( {!isReady() && !atLeastOneFormatAvailable() ? (
<div className={`w-full h-full flex flex-col justify-center p-10`}> <div
className={`w-full h-full flex flex-col justify-center p-10`}
>
<BeatLoader <BeatLoader
color="oklch(var(--p))" color="oklch(var(--p))"
className="mx-auto mb-3" className="mx-auto mb-3"
size={30} size={30}
/> />
<p className="text-center text-2xl">{t("preservation_in_queue")}</p> <p className="text-center text-2xl">
{t("preservation_in_queue")}
</p>
<p className="text-center text-lg">{t("check_back_later")}</p> <p className="text-center text-lg">{t("check_back_later")}</p>
</div> </div>
) : link.url && !isReady() && atLeastOneFormatAvailable() ? ( ) : link.url && !isReady() && atLeastOneFormatAvailable() ? (
@ -314,5 +371,7 @@ export default function LinkDetails({ className, link }: Props) {
)} )}
</div> </div>
</div> </div>
</div>
</div>
); );
} }

View File

@ -222,6 +222,7 @@ export default function LinkActions({
<LinkDetailModal <LinkDetailModal
onClose={() => setLinkDetailModal(false)} onClose={() => setLinkDetailModal(false)}
onEdit={() => setEditLinkModal(true)} onEdit={() => setEditLinkModal(true)}
onDelete={() => setDeleteLinkModal(true)}
link={link} link={link}
/> />
)} )}

View File

@ -172,7 +172,7 @@ export default function LinkCard({ link, flipDropdown, editMode }: Props) {
<div className="duration-100 h-40 bg-opacity-80 skeleton rounded-none"></div> <div className="duration-100 h-40 bg-opacity-80 skeleton rounded-none"></div>
)} )}
{show.icon && ( {show.icon && (
<div className="absolute top-0 left-0 right-0 bottom-0 rounded-t-2xl flex items-center justify-center shadow rounded-md"> <div className="absolute top-0 left-0 right-0 bottom-0 rounded-t-2xl flex items-center justify-center rounded-md">
<LinkIcon link={link} /> <LinkIcon link={link} />
</div> </div>
)} )}

View File

@ -16,7 +16,7 @@ export default function LinkIcon({
hideBackground?: boolean; hideBackground?: boolean;
}) { }) {
let iconClasses: string = clsx( let iconClasses: string = clsx(
"rounded flex item-center justify-center select-none z-10 w-12 h-12", "rounded flex item-center justify-center shadow-md select-none z-10 w-12 h-12",
!hideBackground && "rounded-md bg-white backdrop-blur-lg bg-opacity-50 p-1", !hideBackground && "rounded-md bg-white backdrop-blur-lg bg-opacity-50 p-1",
className className
); );

View File

@ -162,7 +162,7 @@ export default function LinkMasonry({ link, flipDropdown, editMode }: Props) {
<div className="duration-100 h-40 bg-opacity-80 skeleton rounded-none"></div> <div className="duration-100 h-40 bg-opacity-80 skeleton rounded-none"></div>
)} )}
{show.icon && ( {show.icon && (
<div className="absolute top-0 left-0 right-0 bottom-0 rounded-t-2xl flex items-center justify-center shadow rounded-md"> <div className="absolute top-0 left-0 right-0 bottom-0 rounded-t-2xl flex items-center justify-center rounded-md">
<LinkIcon link={link} /> <LinkIcon link={link} />
</div> </div>
)} )}

View File

@ -1,22 +1,38 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { LinkIncludingShortenedCollectionAndTags } from "@/types/global"; import {
ArchivedFormat,
LinkIncludingShortenedCollectionAndTags,
} from "@/types/global";
import getPublicUserData from "@/lib/client/getPublicUserData"; import getPublicUserData from "@/lib/client/getPublicUserData";
import { useTranslation } from "next-i18next"; import { useTranslation } from "next-i18next";
import { useUser } from "@/hooks/store/user"; import { useUser } from "@/hooks/store/user";
import { useGetLink } from "@/hooks/store/links"; import { useDeleteLink, useGetLink, useUpdateLink } from "@/hooks/store/links";
import Drawer from "../Drawer"; import Drawer from "../Drawer";
import LinkDetails from "../LinkDetails"; import LinkDetails from "../LinkDetails";
import Link from "next/link"; import Link from "next/link";
import usePermissions from "@/hooks/usePermissions"; import usePermissions from "@/hooks/usePermissions";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { previewAvailable } from "@/lib/shared/getArchiveValidity";
import Image from "next/image";
import { dropdownTriggerer } from "@/lib/client/utils";
import toast from "react-hot-toast";
import EditLinkModal from "./EditLinkModal";
import DeleteLinkModal from "./DeleteLinkModal";
import PreservedFormatsModal from "./PreservedFormatsModal";
type Props = { type Props = {
onClose: Function; onClose: Function;
onEdit: Function; onEdit: Function;
onDelete: Function;
link: LinkIncludingShortenedCollectionAndTags; link: LinkIncludingShortenedCollectionAndTags;
}; };
export default function LinkDetailModal({ onClose, onEdit, link }: Props) { export default function LinkDetailModal({
onClose,
onEdit,
onDelete,
link,
}: Props) {
const { t } = useTranslation(); const { t } = useTranslation();
const getLink = useGetLink(); const getLink = useGetLink();
const { data: user = {} } = useUser(); const { data: user = {} } = useUser();
@ -105,26 +121,168 @@ export default function LinkDetailModal({ onClose, onEdit, link }: Props) {
const isPublicRoute = router.pathname.startsWith("/public") ? true : false; const isPublicRoute = router.pathname.startsWith("/public") ? true : false;
const updateLink = useUpdateLink();
const deleteLink = useDeleteLink();
const pinLink = async () => {
const isAlreadyPinned = link?.pinnedBy && link.pinnedBy[0] ? true : false;
const load = toast.loading(t("updating"));
await updateLink.mutateAsync(
{
...link,
pinnedBy: isAlreadyPinned ? undefined : [{ id: user.id }],
},
{
onSettled: (data, error) => {
toast.dismiss(load);
if (error) {
toast.error(error.message);
} else {
toast.success(
isAlreadyPinned ? t("link_unpinned") : t("link_pinned")
);
}
},
}
);
};
const updateArchive = async () => {
const load = toast.loading(t("sending_request"));
const response = await fetch(`/api/v1/links/${link?.id}/archive`, {
method: "PUT",
});
const data = await response.json();
toast.dismiss(load);
if (response.ok) {
await getLink.mutateAsync({ id: link.id as number });
toast.success(t("link_being_archived"));
} else toast.error(data.response);
};
const [editLinkModal, setEditLinkModal] = useState(false);
const [deleteLinkModal, setDeleteLinkModal] = useState(false);
const [preservedFormatsModal, setPreservedFormatsModal] = useState(false);
return ( return (
<Drawer toggleDrawer={onClose} className="sm:h-screen sm:flex relative"> <Drawer toggleDrawer={onClose} className="sm:h-screen sm:flex relative">
<div <div
className="bi-x text-xl text-neutral btn btn-sm btn-square btn-ghost hidden sm:block absolute top-3 left-3" 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()} onClick={() => onClose()}
></div> ></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();
pinLink();
}}
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();
setEditLinkModal(true);
}}
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();
updateArchive();
}}
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 <Link
href={isPublicRoute ? `/public/links/${link.id}` : `/links/${link.id}`} href={isPublicRoute ? `/public/links/${link.id}` : `/links/${link.id}`}
target="_blank" target="_blank"
className="bi-box-arrow-up-right text-xl text-neutral btn btn-sm btn-square btn-ghost absolute top-3 right-3 select-none" 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> ></Link>
<div className="sm:m-auto p-10 w-full max-w-xl"> <div className="w-full">
<LinkDetails link={link} /> <LinkDetails link={link} className="sm:mt-0 -mt-11" />
{(permissions === true || permissions?.canUpdate) && (
<>
<br />
<br />
{/* {(permissions === true || permissions?.canUpdate) && (
<div className="mx-auto text-center"> <div className="mx-auto text-center">
<div <div
className="btn btn-sm btn-ghost" className="btn btn-sm btn-ghost"
@ -136,8 +294,7 @@ export default function LinkDetailModal({ onClose, onEdit, link }: Props) {
{t("edit_link")} {t("edit_link")}
</div> </div>
</div> </div>
</> )} */}
)}
</div> </div>
</Drawer> </Drawer>
); );

View File

@ -17,14 +17,13 @@ const Index = () => {
}, []); }, []);
return ( return (
<div className="flex h-screen py-20"> <div className="flex h-screen">
{getLink.data ? ( {getLink.data ? (
<div className="m-auto py-20 w-full">
<LinkDetails <LinkDetails
link={getLink.data} link={getLink.data}
className="max-w-xl mx-auto p-5 w-full" className="sm:max-w-xl sm:m-auto sm:p-5 w-full"
standalone
/> />
</div>
) : ( ) : (
<div className="max-w-xl p-5 m-auto w-full flex flex-col items-center gap-5"> <div className="max-w-xl p-5 m-auto w-full flex flex-col items-center gap-5">
<div className="w-20 h-20 skeleton rounded-xl"></div> <div className="w-20 h-20 skeleton rounded-xl"></div>

View File

@ -17,12 +17,11 @@ const Index = () => {
return ( return (
<div className="flex h-screen"> <div className="flex h-screen">
{getLink.data ? ( {getLink.data ? (
<div className="m-auto py-20 w-full">
<LinkDetails <LinkDetails
link={getLink.data} link={getLink.data}
className="max-w-xl mx-auto p-5 w-full" className="sm:max-w-xl sm:m-auto sm:p-5 w-full"
standalone
/> />
</div>
) : ( ) : (
<div className="max-w-xl p-5 m-auto w-full flex flex-col items-center gap-5"> <div className="max-w-xl p-5 m-auto w-full flex flex-col items-center gap-5">
<div className="w-20 h-20 skeleton rounded-xl"></div> <div className="w-20 h-20 skeleton rounded-xl"></div>