This commit is contained in:
daniel31x13 2024-08-18 16:39:43 -04:00
parent 2b83522eaa
commit 1260e8c093
10 changed files with 268 additions and 272 deletions

View File

@ -61,8 +61,7 @@ export default function Dropdown({
}, [points, dropdownHeight]); }, [points, dropdownHeight]);
return ( return (
!points || (!points || pos) && (
(pos && (
<ClickAwayHandler <ClickAwayHandler
onMount={(e) => { onMount={(e) => {
setDropdownHeight(e.height); setDropdownHeight(e.height);
@ -104,6 +103,6 @@ export default function Dropdown({
); );
})} })}
</ClickAwayHandler> </ClickAwayHandler>
)) )
); );
} }

View File

@ -104,7 +104,7 @@ export default function CollectionSelection({
onChange={onChange} onChange={onChange}
options={options} options={options}
styles={styles} styles={styles}
defaultValue={showDefaultValue && defaultValue} defaultValue={showDefaultValue ? defaultValue : null}
components={{ components={{
Option: customOption, Option: customOption,
}} }}
@ -120,7 +120,7 @@ export default function CollectionSelection({
onChange={onChange} onChange={onChange}
options={options} options={options}
styles={styles} styles={styles}
defaultValue={showDefaultValue && defaultValue} defaultValue={showDefaultValue ? defaultValue : null}
components={{ components={{
Option: customOption, Option: customOption,
}} }}

View File

@ -12,22 +12,20 @@ import { useTranslation } from "next-i18next";
import { useUser } from "@/hooks/store/user"; import { useUser } from "@/hooks/store/user";
import { useDeleteLink, useUpdateLink } from "@/hooks/store/links"; import { useDeleteLink, useUpdateLink } from "@/hooks/store/links";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
import LinkDetailModal from "@/components/ModalContent/LinkDetailModal";
import { useRouter } from "next/router";
type Props = { type Props = {
link: LinkIncludingShortenedCollectionAndTags; link: LinkIncludingShortenedCollectionAndTags;
collection: CollectionIncludingMembersAndLinkCount; collection: CollectionIncludingMembersAndLinkCount;
position?: string; position?: string;
toggleShowInfo?: () => void;
linkInfo?: boolean;
alignToTop?: boolean; alignToTop?: boolean;
flipDropdown?: boolean; flipDropdown?: boolean;
}; };
export default function LinkActions({ export default function LinkActions({
link, link,
toggleShowInfo,
position, position,
linkInfo,
alignToTop, alignToTop,
flipDropdown, flipDropdown,
}: Props) { }: Props) {
@ -36,6 +34,7 @@ export default function LinkActions({
const permissions = usePermissions(link.collection.id as number); const permissions = usePermissions(link.collection.id as number);
const [editLinkModal, setEditLinkModal] = useState(false); const [editLinkModal, setEditLinkModal] = useState(false);
const [linkDetailModal, setLinkDetailModal] = useState(false);
const [deleteLinkModal, setDeleteLinkModal] = useState(false); const [deleteLinkModal, setDeleteLinkModal] = useState(false);
const [preservedFormatsModal, setPreservedFormatsModal] = useState(false); const [preservedFormatsModal, setPreservedFormatsModal] = useState(false);
@ -70,120 +69,137 @@ export default function LinkActions({
); );
}; };
const router = useRouter();
const isPublicRoute = router.pathname.startsWith("/public") ? true : false;
return ( return (
<> <>
<div {isPublicRoute ? (
className={`dropdown dropdown-left absolute ${
position || "top-3 right-3"
} ${alignToTop ? "" : "dropdown-end"} z-20`}
>
<div <div
tabIndex={0} className={`absolute ${position || "top-3 right-3"} ${
role="button" alignToTop ? "" : "dropdown-end"
onMouseDown={dropdownTriggerer} } z-20`}
className="btn btn-ghost btn-sm btn-square text-neutral" onClick={() => setLinkDetailModal(true)}
> >
<i title="More" className="bi-three-dots text-xl" /> <div className="btn btn-ghost btn-sm btn-square text-neutral">
<i title="More" className="bi-three-dots text-xl" />
</div>
</div> </div>
<ul ) : (
className={`dropdown-content z-[20] menu shadow bg-base-200 border border-neutral-content rounded-box mr-1 ${ <div
alignToTop ? "" : "translate-y-10" className={`dropdown dropdown-left absolute ${
}`} position || "top-3 right-3"
} ${alignToTop ? "" : "dropdown-end"} z-20`}
> >
<li> <div
<div tabIndex={0}
role="button" role="button"
tabIndex={0} onMouseDown={dropdownTriggerer}
onClick={() => { className="btn btn-ghost btn-sm btn-square text-neutral"
(document?.activeElement as HTMLElement)?.blur(); >
pinLink(); <i title="More" className="bi-three-dots text-xl" />
}} </div>
className="whitespace-nowrap" <ul
> className={`dropdown-content z-[20] menu shadow bg-base-200 border border-neutral-content rounded-box mr-1 ${
{link?.pinnedBy && link.pinnedBy[0] alignToTop ? "" : "translate-y-10"
? t("unpin") }`}
: t("pin_to_dashboard")} >
</div> {permissions === true ||
</li> (permissions?.canUpdate && (
{linkInfo !== undefined && toggleShowInfo && ( <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>
))}
<li> <li>
<div <div
role="button" role="button"
tabIndex={0} tabIndex={0}
onClick={() => { onClick={() => {
(document?.activeElement as HTMLElement)?.blur(); (document?.activeElement as HTMLElement)?.blur();
toggleShowInfo(); setLinkDetailModal(true);
}} }}
className="whitespace-nowrap" className="whitespace-nowrap"
> >
{!linkInfo ? t("show_link_details") : t("hide_link_details")} {t("show_link_details")}
</div> </div>
</li> </li>
)} {(permissions === true || permissions?.canUpdate) && (
{(permissions === true || permissions?.canUpdate) && ( <li>
<li> <div
<div role="button"
role="button" tabIndex={0}
tabIndex={0} onClick={() => {
onClick={() => { (document?.activeElement as HTMLElement)?.blur();
(document?.activeElement as HTMLElement)?.blur(); setEditLinkModal(true);
setEditLinkModal(true); }}
}} className="whitespace-nowrap"
className="whitespace-nowrap" >
> {t("edit_link")}
{t("edit_link")} </div>
</div> </li>
</li> )}
)} {link.type === "url" && (
{link.type === "url" && ( <li>
<li> <div
<div role="button"
role="button" tabIndex={0}
tabIndex={0} onClick={() => {
onClick={() => { (document?.activeElement as HTMLElement)?.blur();
(document?.activeElement as HTMLElement)?.blur(); setPreservedFormatsModal(true);
setPreservedFormatsModal(true); }}
}} className="whitespace-nowrap"
className="whitespace-nowrap" >
> {t("preserved_formats")}
{t("preserved_formats")} </div>
</div> </li>
</li> )}
)} {(permissions === true || permissions?.canDelete) && (
{(permissions === true || permissions?.canDelete) && ( <li>
<li> <div
<div role="button"
role="button" tabIndex={0}
tabIndex={0} onClick={async (e) => {
onClick={async (e) => { (document?.activeElement as HTMLElement)?.blur();
(document?.activeElement as HTMLElement)?.blur(); e.shiftKey
e.shiftKey ? async () => {
? async () => { const load = toast.loading(t("deleting"));
const load = toast.loading(t("deleting"));
await deleteLink.mutateAsync(link.id as number, { await deleteLink.mutateAsync(link.id as number, {
onSettled: (data, error) => { onSettled: (data, error) => {
toast.dismiss(load); toast.dismiss(load);
if (error) {
toast.error(error.message);
} else {
toast.success(t("deleted"));
}
},
});
}
: setDeleteLinkModal(true);
}}
className="whitespace-nowrap"
>
{t("delete")}
</div>
</li>
)}
</ul>
</div>
if (error) {
toast.error(error.message);
} else {
toast.success(t("deleted"));
}
},
});
}
: setDeleteLinkModal(true);
}}
className="whitespace-nowrap"
>
{t("delete")}
</div>
</li>
)}
</ul>
</div>
)}
{editLinkModal && ( {editLinkModal && (
<EditLinkModal <EditLinkModal
onClose={() => setEditLinkModal(false)} onClose={() => setEditLinkModal(false)}
@ -202,6 +218,13 @@ export default function LinkActions({
link={link} link={link}
/> />
)} )}
{linkDetailModal && (
<LinkDetailModal
onClose={() => setLinkDetailModal(false)}
onEdit={() => setEditLinkModal(true)}
link={link}
/>
)}
</> </>
); );
} }

View File

@ -132,7 +132,7 @@ export default function EditCollectionSharingModal({
)} )}
{collection.isPublic && ( {collection.isPublic && (
<div className={permissions === true ? "pl-5" : ""}> <div>
<p className="mb-2">{t("sharable_link_guide")}</p> <p className="mb-2">{t("sharable_link_guide")}</p>
<div className="w-full hide-scrollbar overflow-x-auto whitespace-nowrap rounded-md p-2 bg-base-200 border-neutral-content border-solid border flex items-center gap-2 justify-between"> <div className="w-full hide-scrollbar overflow-x-auto whitespace-nowrap rounded-md p-2 bg-base-200 border-neutral-content border-solid border flex items-center gap-2 justify-between">
{publicCollectionURL} {publicCollectionURL}

View File

@ -150,7 +150,7 @@ export default function UploadFileModal({ onClose }: Props) {
<label className="btn h-10 btn-sm w-full border border-neutral-content hover:border-neutral-content flex justify-between"> <label className="btn h-10 btn-sm w-full border border-neutral-content hover:border-neutral-content flex justify-between">
<input <input
type="file" type="file"
accept=".pdf,.png,.jpg,.jpeg,.html" accept=".pdf,.png,.jpg,.jpeg"
className="cursor-pointer custom-file-input" className="cursor-pointer custom-file-input"
onChange={(e) => e.target.files && setFile(e.target.files[0])} onChange={(e) => e.target.files && setFile(e.target.files[0])}
/> />

View File

@ -21,9 +21,8 @@ export default function ToggleDarkMode({ className }: Props) {
useEffect(() => { useEffect(() => {
if (theme) { if (theme) {
updateSettings({ theme }); updateSettings({ theme });
localStorage.setItem("theme", theme);
} }
}, [theme, updateSettings]); }, [theme]);
return ( return (
<div <div

View File

@ -22,32 +22,5 @@ export default async function exportData(userId: number) {
const { password, id, ...userData } = user; const { password, id, ...userData } = user;
function redactIds(data: object | object[]): void {
if (Array.isArray(data)) {
data.forEach((item) => redactIds(item));
} else if (data !== null && typeof data === "object") {
const fieldsToRedact = ["id", "parentId", "collectionId", "ownerId"];
fieldsToRedact.forEach((field) => {
if (field in data) {
delete (data as any)[field];
}
});
// Recursively call redactIds for each property that is an object or an array
Object.keys(data).forEach((key) => {
const value = (data as any)[key];
if (
value !== null &&
(typeof value === "object" || Array.isArray(value))
) {
redactIds(value);
}
});
}
}
redactIds(userData);
return { response: userData, status: 200 }; return { response: userData, status: 200 };
} }

View File

@ -16,7 +16,7 @@ const generatePreview = async (
return; return;
} }
image.resize(1280, Jimp.AUTO).quality(20); image.resize(1000, Jimp.AUTO).quality(20);
const processedBuffer = await image.getBufferAsync(Jimp.MIME_JPEG); const processedBuffer = await image.getBufferAsync(Jimp.MIME_JPEG);
if ( if (

View File

@ -135,7 +135,7 @@ export default function Dashboard() {
<DashboardItem <DashboardItem
name={tags.length === 1 ? t("tag") : t("tags")} name={tags.length === 1 ? t("tag") : t("tags")}
value={tags.length * numberOfLinks} value={tags.length}
icon={"bi-hash"} icon={"bi-hash"}
/> />
</div> </div>

View File

@ -88,160 +88,162 @@ export default function PublicCollections() {
(localStorage.getItem("viewMode") as ViewMode) || ViewMode.Card (localStorage.getItem("viewMode") as ViewMode) || ViewMode.Card
); );
if (!collection) return null; if (!collection) return <></>;
else
return (
<div
className="h-96"
style={{
backgroundImage: `linear-gradient(${collection?.color}30 10%, ${
settings.theme === "dark" ? "#262626" : "#f3f4f6"
} 13rem, ${settings.theme === "dark" ? "#171717" : "#ffffff"} 100%)`,
}}
>
{collection && (
<Head>
<title>{collection.name} | Linkwarden</title>
<meta
property="og:title"
content={`${collection.name} | Linkwarden`}
key="title"
/>
</Head>
)}
<div className="lg:w-3/4 w-full mx-auto p-5 bg">
<div className="flex items-center justify-between">
<p className="text-4xl font-thin mb-2 capitalize mt-10">
{collection.name}
</p>
<div className="flex gap-2 items-center mt-8 min-w-fit">
<ToggleDarkMode />
return ( <Link href="https://linkwarden.app/" target="_blank">
<div <Image
className="h-96" src={`/icon.png`}
style={{ width={551}
backgroundImage: `linear-gradient(${collection?.color}30 10%, ${ height={551}
settings.theme === "dark" ? "#262626" : "#f3f4f6" alt="Linkwarden"
} 13rem, ${settings.theme === "dark" ? "#171717" : "#ffffff"} 100%)`, title={t("list_created_with_linkwarden")}
}} className="h-8 w-fit mx-auto rounded"
> />
{collection && ( </Link>
<Head> </div>
<title>{collection.name} | Linkwarden</title>
<meta
property="og:title"
content={`${collection.name} | Linkwarden`}
key="title"
/>
</Head>
)}
<div className="lg:w-3/4 w-full mx-auto p-5 bg">
<div className="flex items-center justify-between">
<p className="text-4xl font-thin mb-2 capitalize mt-10">
{collection.name}
</p>
<div className="flex gap-2 items-center mt-8 min-w-fit">
<ToggleDarkMode />
<Link href="https://linkwarden.app/" target="_blank">
<Image
src={`/icon.png`}
width={551}
height={551}
alt="Linkwarden"
title={t("list_created_with_linkwarden")}
className="h-8 w-fit mx-auto rounded"
/>
</Link>
</div> </div>
</div>
<div className="mt-3"> <div className="mt-3">
<div className={`min-w-[15rem]`}> <div className={`min-w-[15rem]`}>
<div className="flex gap-1 justify-center sm:justify-end items-center w-fit"> <div className="flex gap-1 justify-center sm:justify-end items-center w-fit">
<div <div
className="flex items-center btn px-2 btn-ghost rounded-full" className="flex items-center btn px-2 btn-ghost rounded-full"
onClick={() => setEditCollectionSharingModal(true)} onClick={() => setEditCollectionSharingModal(true)}
> >
{collectionOwner.id && ( {collectionOwner.id && (
<ProfilePhoto <ProfilePhoto
src={collectionOwner.image || undefined} src={collectionOwner.image || undefined}
name={collectionOwner.name} name={collectionOwner.name}
/> />
)} )}
{collection.members {collection.members
.sort((a, b) => (a.userId as number) - (b.userId as number)) .sort((a, b) => (a.userId as number) - (b.userId as number))
.map((e, i) => { .map((e, i) => {
return ( return (
<ProfilePhoto <ProfilePhoto
key={i} key={i}
src={e.user.image ? e.user.image : undefined} src={e.user.image ? e.user.image : undefined}
className="-ml-3" className="-ml-3"
name={e.user.name} name={e.user.name}
/> />
); );
})
.slice(0, 3)}
{collection.members.length - 3 > 0 && (
<div className={`avatar drop-shadow-md placeholder -ml-3`}>
<div className="bg-base-100 text-neutral rounded-full w-8 h-8 ring-2 ring-neutral-content">
<span>+{collection.members.length - 3}</span>
</div>
</div>
)}
</div>
<p className="text-neutral text-sm">
{collection.members.length > 0 &&
collection.members.length === 1
? t("by_author_and_other", {
author: collectionOwner.name,
count: collection.members.length,
}) })
: collection.members.length > 0 && .slice(0, 3)}
collection.members.length !== 1 {collection.members.length - 3 > 0 && (
? t("by_author_and_others", { <div className={`avatar drop-shadow-md placeholder -ml-3`}>
<div className="bg-base-100 text-neutral rounded-full w-8 h-8 ring-2 ring-neutral-content">
<span>+{collection.members.length - 3}</span>
</div>
</div>
)}
</div>
<p className="text-neutral text-sm">
{collection.members.length > 0 &&
collection.members.length === 1
? t("by_author_and_other", {
author: collectionOwner.name, author: collectionOwner.name,
count: collection.members.length, count: collection.members.length,
}) })
: t("by_author", { : collection.members.length > 0 &&
author: collectionOwner.name, collection.members.length !== 1
})} ? t("by_author_and_others", {
</p> author: collectionOwner.name,
count: collection.members.length,
})
: t("by_author", {
author: collectionOwner.name,
})}
</p>
</div>
</div> </div>
</div> </div>
</div>
<p className="mt-5">{collection.description}</p> <p className="mt-5">{collection.description}</p>
<div className="divider mt-5 mb-0"></div> <div className="divider mt-5 mb-0"></div>
<div className="flex mb-5 mt-10 flex-col gap-5"> <div className="flex mb-5 mt-10 flex-col gap-5">
<LinkListOptions <LinkListOptions
t={t} t={t}
viewMode={viewMode} viewMode={viewMode}
setViewMode={setViewMode} setViewMode={setViewMode}
sortBy={sortBy} sortBy={sortBy}
setSortBy={setSortBy} setSortBy={setSortBy}
searchFilter={searchFilter} searchFilter={searchFilter}
setSearchFilter={setSearchFilter} setSearchFilter={setSearchFilter}
> >
<SearchBar <SearchBar
placeholder={ placeholder={
collection._count?.links === 1 collection._count?.links === 1
? t("search_count_link", { ? t("search_count_link", {
count: collection._count?.links, count: collection._count?.links,
}) })
: t("search_count_links", { : t("search_count_links", {
count: collection._count?.links, count: collection._count?.links,
}) })
}
/>
</LinkListOptions>
<Links
links={
links?.map((e, i) => {
const linkWithCollectionData = {
...e,
collection: collection, // Append collection data
};
return linkWithCollectionData;
}) as any
} }
layout={viewMode}
placeholderCount={1}
useData={data}
/> />
</LinkListOptions> {!data.isLoading && links && !links[0] && (
<p>{t("nothing_found")}</p>
)}
<Links {/* <p className="text-center text-neutral">
links={
links?.map((e, i) => {
const linkWithCollectionData = {
...e,
collection: collection, // Append collection data
};
return linkWithCollectionData;
}) as any
}
layout={viewMode}
placeholderCount={1}
useData={data}
/>
{!data.isLoading && links && !links[0] && <p>{t("nothing_found")}</p>}
{/* <p className="text-center text-neutral">
List created with <span className="text-black">Linkwarden.</span> List created with <span className="text-black">Linkwarden.</span>
</p> */} </p> */}
</div>
</div> </div>
{editCollectionSharingModal && (
<EditCollectionSharingModal
onClose={() => setEditCollectionSharingModal(false)}
activeCollection={collection}
/>
)}
</div> </div>
{editCollectionSharingModal && ( );
<EditCollectionSharingModal
onClose={() => setEditCollectionSharingModal(false)}
activeCollection={collection}
/>
)}
</div>
);
} }
export { getServerSideProps }; export { getServerSideProps };