Merge pull request #678 from IsaacWise06/fixes

General Fixes
This commit is contained in:
Daniel 2024-08-18 16:40:48 -04:00 committed by GitHub
commit c6e3147bb6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
53 changed files with 420 additions and 445 deletions

View File

@ -1,5 +1,8 @@
import Link from "next/link"; import Link from "next/link";
import { CollectionIncludingMembersAndLinkCount } from "@/types/global"; import {
AccountSettings,
CollectionIncludingMembersAndLinkCount,
} from "@/types/global";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import ProfilePhoto from "./ProfilePhoto"; import ProfilePhoto from "./ProfilePhoto";
import usePermissions from "@/hooks/usePermissions"; import usePermissions from "@/hooks/usePermissions";
@ -12,12 +15,11 @@ 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";
type Props = { export default function CollectionCard({
collection,
}: {
collection: CollectionIncludingMembersAndLinkCount; collection: CollectionIncludingMembersAndLinkCount;
className?: string; }) {
};
export default function CollectionCard({ collection, className }: Props) {
const { t } = useTranslation(); const { t } = useTranslation();
const { settings } = useLocalSettingsStore(); const { settings } = useLocalSettingsStore();
const { data: user = {} } = useUser(); const { data: user = {} } = useUser();
@ -33,15 +35,9 @@ export default function CollectionCard({ collection, className }: Props) {
const permissions = usePermissions(collection.id as number); const permissions = usePermissions(collection.id as number);
const [collectionOwner, setCollectionOwner] = useState({ const [collectionOwner, setCollectionOwner] = useState<
id: null as unknown as number, Partial<AccountSettings>
name: "", >({});
username: "",
image: "",
archiveAsScreenshot: undefined as unknown as boolean,
archiveAsMonolith: undefined as unknown as boolean,
archiveAsPDF: undefined as unknown as boolean,
});
useEffect(() => { useEffect(() => {
const fetchOwner = async () => { const fetchOwner = async () => {
@ -132,12 +128,12 @@ export default function CollectionCard({ collection, className }: Props) {
className="flex items-center absolute bottom-3 left-3 z-10 btn px-2 btn-ghost rounded-full" className="flex items-center absolute bottom-3 left-3 z-10 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}
/> />
) : undefined} )}
{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) => {
@ -151,13 +147,13 @@ export default function CollectionCard({ collection, className }: Props) {
); );
}) })
.slice(0, 3)} .slice(0, 3)}
{collection.members.length - 3 > 0 ? ( {collection.members.length - 3 > 0 && (
<div className={`avatar drop-shadow-md placeholder -ml-3`}> <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"> <div className="bg-base-100 text-neutral rounded-full w-8 h-8 ring-2 ring-neutral-content">
<span>+{collection.members.length - 3}</span> <span>+{collection.members.length - 3}</span>
</div> </div>
</div> </div>
) : null} )}
</div> </div>
<Link <Link
href={`/collections/${collection.id}`} href={`/collections/${collection.id}`}
@ -181,12 +177,12 @@ export default function CollectionCard({ collection, className }: Props) {
<div className="flex justify-end items-center"> <div className="flex justify-end items-center">
<div className="text-right"> <div className="text-right">
<div className="font-bold text-sm flex justify-end gap-1 items-center"> <div className="font-bold text-sm flex justify-end gap-1 items-center">
{collection.isPublic ? ( {collection.isPublic && (
<i <i
className="bi-globe2 drop-shadow text-neutral" className="bi-globe2 drop-shadow text-neutral"
title="This collection is being shared publicly." title="This collection is being shared publicly."
></i> ></i>
) : undefined} )}
<i <i
className="bi-link-45deg text-lg text-neutral" className="bi-link-45deg text-lg text-neutral"
title="This collection is being shared publicly." title="This collection is being shared publicly."
@ -206,24 +202,24 @@ export default function CollectionCard({ collection, className }: Props) {
</div> </div>
</div> </div>
</Link> </Link>
{editCollectionModal ? ( {editCollectionModal && (
<EditCollectionModal <EditCollectionModal
onClose={() => setEditCollectionModal(false)} onClose={() => setEditCollectionModal(false)}
activeCollection={collection} activeCollection={collection}
/> />
) : undefined} )}
{editCollectionSharingModal ? ( {editCollectionSharingModal && (
<EditCollectionSharingModal <EditCollectionSharingModal
onClose={() => setEditCollectionSharingModal(false)} onClose={() => setEditCollectionSharingModal(false)}
activeCollection={collection} activeCollection={collection}
/> />
) : undefined} )}
{deleteCollectionModal ? ( {deleteCollectionModal && (
<DeleteCollectionModal <DeleteCollectionModal
onClose={() => setDeleteCollectionModal(false)} onClose={() => setDeleteCollectionModal(false)}
activeCollection={collection} activeCollection={collection}
/> />
) : undefined} )}
</div> </div>
); );
} }

View File

@ -272,12 +272,12 @@ const renderItem = (
></i> ></i>
<p className="truncate w-full">{collection.name}</p> <p className="truncate w-full">{collection.name}</p>
{collection.isPublic ? ( {collection.isPublic && (
<i <i
className="bi-globe2 text-sm text-black/50 dark:text-white/50 drop-shadow" className="bi-globe2 text-sm text-black/50 dark:text-white/50 drop-shadow"
title="This collection is being shared publicly." title="This collection is being shared publicly."
></i> ></i>
) : undefined} )}
<div className="drop-shadow text-neutral text-xs"> <div className="drop-shadow text-neutral text-xs">
{collection._count?.links} {collection._count?.links}
</div> </div>

View File

@ -60,47 +60,49 @@ export default function Dropdown({
} }
}, [points, dropdownHeight]); }, [points, dropdownHeight]);
return !points || pos ? ( return (
<ClickAwayHandler (!points || pos) && (
onMount={(e) => { <ClickAwayHandler
setDropdownHeight(e.height); onMount={(e) => {
setDropdownWidth(e.width); setDropdownHeight(e.height);
}} setDropdownWidth(e.width);
style={ }}
points style={
? { points
position: "fixed", ? {
top: `${pos?.y}px`, position: "fixed",
left: `${pos?.x}px`, top: `${pos?.y}px`,
} left: `${pos?.x}px`,
: undefined }
} : undefined
onClickOutside={onClickOutside} }
className={`${ onClickOutside={onClickOutside}
className || "" className={`${
} py-1 shadow-md border border-neutral-content bg-base-200 rounded-md flex flex-col z-20`} className || ""
> } py-1 shadow-md border border-neutral-content bg-base-200 rounded-md flex flex-col z-20`}
{items.map((e, i) => { >
const inner = e && ( {items.map((e, i) => {
<div className="cursor-pointer rounded-md"> const inner = e && (
<div className="flex items-center gap-2 py-1 px-2 hover:bg-base-100 duration-100"> <div className="cursor-pointer rounded-md">
<p className="select-none">{e.name}</p> <div className="flex items-center gap-2 py-1 px-2 hover:bg-base-100 duration-100">
<p className="select-none">{e.name}</p>
</div>
</div> </div>
</div> );
);
return e && e.href ? ( return e && e.href ? (
<Link key={i} href={e.href}> <Link key={i} href={e.href}>
{inner}
</Link>
) : (
e && (
<div key={i} onClick={e.onClick}>
{inner} {inner}
</div> </Link>
) ) : (
); e && (
})} <div key={i} onClick={e.onClick}>
</ClickAwayHandler> {inner}
) : null; </div>
)
);
})}
</ClickAwayHandler>
)
);
} }

View File

@ -34,7 +34,6 @@ export default function TagSelection({ onChange, defaultValue }: Props) {
options={options} options={options}
styles={styles} styles={styles}
defaultValue={defaultValue} defaultValue={defaultValue}
// menuPosition="fixed"
isMulti isMulti
/> />
); );

View File

@ -136,7 +136,7 @@ export default function LinkActions({
{t("show_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"
@ -150,7 +150,7 @@ export default function LinkActions({
{t("edit_link")} {t("edit_link")}
</div> </div>
</li> </li>
) : undefined} )}
{link.type === "url" && ( {link.type === "url" && (
<li> <li>
<div <div
@ -166,7 +166,7 @@ export default function LinkActions({
</div> </div>
</li> </li>
)} )}
{permissions === true || permissions?.canDelete ? ( {(permissions === true || permissions?.canDelete) && (
<li> <li>
<div <div
role="button" role="button"
@ -196,36 +196,35 @@ export default function LinkActions({
{t("delete")} {t("delete")}
</div> </div>
</li> </li>
) : undefined} )}
</ul> </ul>
</div> </div>
)} )}
{editLinkModal && (
{editLinkModal ? (
<EditLinkModal <EditLinkModal
onClose={() => setEditLinkModal(false)} onClose={() => setEditLinkModal(false)}
activeLink={link} activeLink={link}
/> />
) : undefined} )}
{deleteLinkModal ? ( {deleteLinkModal && (
<DeleteLinkModal <DeleteLinkModal
onClose={() => setDeleteLinkModal(false)} onClose={() => setDeleteLinkModal(false)}
activeLink={link} activeLink={link}
/> />
) : undefined} )}
{preservedFormatsModal ? ( {preservedFormatsModal && (
<PreservedFormatsModal <PreservedFormatsModal
onClose={() => setPreservedFormatsModal(false)} onClose={() => setPreservedFormatsModal(false)}
link={link} link={link}
/> />
) : undefined} )}
{linkDetailModal ? ( {linkDetailModal && (
<LinkDetailModal <LinkDetailModal
onClose={() => setLinkDetailModal(false)} onClose={() => setLinkDetailModal(false)}
onEdit={() => setEditLinkModal(true)} onEdit={() => setEditLinkModal(true)}
link={link} link={link}
/> />
) : undefined} )}
</> </>
); );
} }

View File

@ -96,7 +96,7 @@ export default function LinkCard({ link, flipDropdown, editMode }: Props) {
let isPublic = router.pathname.startsWith("/public") ? true : undefined; let isPublic = router.pathname.startsWith("/public") ? true : undefined;
useEffect(() => { useEffect(() => {
let interval: any; let interval: NodeJS.Timeout | null = null;
if ( if (
isVisible && isVisible &&

View File

@ -127,9 +127,9 @@ export default function LinkCardCompact({
<div className="mt-1 flex flex-col sm:flex-row sm:items-center gap-2 text-xs text-neutral"> <div className="mt-1 flex flex-col sm:flex-row sm:items-center gap-2 text-xs text-neutral">
<div className="flex items-center gap-x-3 text-neutral flex-wrap"> <div className="flex items-center gap-x-3 text-neutral flex-wrap">
{collection ? ( {collection && (
<LinkCollection link={link} collection={collection} /> <LinkCollection link={link} collection={collection} />
) : undefined} )}
{link.name && <LinkTypeBadge link={link} />} {link.name && <LinkTypeBadge link={link} />}
<LinkDate link={link} /> <LinkDate link={link} />
</div> </div>

View File

@ -88,7 +88,7 @@ export default function LinkMasonry({ link, flipDropdown, editMode }: Props) {
const permissions = usePermissions(collection?.id as number); const permissions = usePermissions(collection?.id as number);
useEffect(() => { useEffect(() => {
let interval: any; let interval: NodeJS.Timeout | null = null;
if ( if (
isVisible && isVisible &&

View File

@ -87,15 +87,13 @@ export default function MobileNavigation({}: Props) {
<MobileNavigationButton href={`/collections`} icon={"bi-folder"} /> <MobileNavigationButton href={`/collections`} icon={"bi-folder"} />
</div> </div>
</div> </div>
{newLinkModal ? ( {newLinkModal && <NewLinkModal onClose={() => setNewLinkModal(false)} />}
<NewLinkModal onClose={() => setNewLinkModal(false)} /> {newCollectionModal && (
) : undefined}
{newCollectionModal ? (
<NewCollectionModal onClose={() => setNewCollectionModal(false)} /> <NewCollectionModal onClose={() => setNewCollectionModal(false)} />
) : undefined} )}
{uploadFileModal ? ( {uploadFileModal && (
<UploadFileModal onClose={() => setUploadFileModal(false)} /> <UploadFileModal onClose={() => setUploadFileModal(false)} />
) : undefined} )}
</> </>
); );
} }

View File

@ -1,7 +1,11 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import TextInput from "@/components/TextInput"; import TextInput from "@/components/TextInput";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
import { CollectionIncludingMembersAndLinkCount, Member } from "@/types/global"; import {
AccountSettings,
CollectionIncludingMembersAndLinkCount,
Member,
} from "@/types/global";
import getPublicUserData from "@/lib/client/getPublicUserData"; import getPublicUserData from "@/lib/client/getPublicUserData";
import usePermissions from "@/hooks/usePermissions"; import usePermissions from "@/hooks/usePermissions";
import ProfilePhoto from "../ProfilePhoto"; import ProfilePhoto from "../ProfilePhoto";
@ -65,15 +69,9 @@ export default function EditCollectionSharingModal({
const [memberUsername, setMemberUsername] = useState(""); const [memberUsername, setMemberUsername] = useState("");
const [collectionOwner, setCollectionOwner] = useState({ const [collectionOwner, setCollectionOwner] = useState<
id: null as unknown as number, Partial<AccountSettings>
name: "", >({});
username: "",
image: "",
archiveAsScreenshot: undefined as unknown as boolean,
archiveAsMonolith: undefined as unknown as boolean,
archiveAsPDF: undefined as unknown as boolean,
});
useEffect(() => { useEffect(() => {
const fetchOwner = async () => { const fetchOwner = async () => {
@ -133,7 +131,7 @@ export default function EditCollectionSharingModal({
</div> </div>
)} )}
{collection.isPublic ? ( {collection.isPublic && (
<div> <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">
@ -141,7 +139,7 @@ export default function EditCollectionSharingModal({
<CopyButton text={publicCollectionURL} /> <CopyButton text={publicCollectionURL} />
</div> </div>
</div> </div>
) : null} )}
{permissions === true && <div className="divider my-3"></div>} {permissions === true && <div className="divider my-3"></div>}

View File

@ -77,7 +77,7 @@ export default function EditLinkModal({ onClose, activeLink }: Props) {
<div className="divider mb-3 mt-1"></div> <div className="divider mb-3 mt-1"></div>
{link.url ? ( {link.url && (
<Link <Link
href={link.url} href={link.url}
className="truncate text-neutral flex gap-2 mb-5 w-fit max-w-full" className="truncate text-neutral flex gap-2 mb-5 w-fit max-w-full"
@ -87,7 +87,7 @@ export default function EditLinkModal({ onClose, activeLink }: Props) {
<i className="bi-link-45deg text-xl" /> <i className="bi-link-45deg text-xl" />
<p>{shortenedURL}</p> <p>{shortenedURL}</p>
</Link> </Link>
) : undefined} )}
<div className="w-full"> <div className="w-full">
<p className="mb-2">{t("name")}</p> <p className="mb-2">{t("name")}</p>
@ -103,7 +103,7 @@ export default function EditLinkModal({ onClose, activeLink }: Props) {
<div className="grid sm:grid-cols-2 gap-3"> <div className="grid sm:grid-cols-2 gap-3">
<div> <div>
<p className="mb-2">{t("collection")}</p> <p className="mb-2">{t("collection")}</p>
{link.collection.name ? ( {link.collection.name && (
<CollectionSelection <CollectionSelection
onChange={setCollection} onChange={setCollection}
defaultValue={ defaultValue={
@ -113,7 +113,7 @@ export default function EditLinkModal({ onClose, activeLink }: Props) {
} }
creatable={false} creatable={false}
/> />
) : null} )}
</div> </div>
<div> <div>

View File

@ -124,7 +124,7 @@ export default function NewLinkModal({ onClose }: Props) {
</div> </div>
<div className="sm:col-span-2 col-span-5"> <div className="sm:col-span-2 col-span-5">
<p className="mb-2">{t("collection")}</p> <p className="mb-2">{t("collection")}</p>
{link.collection.name ? ( {link.collection.name && (
<CollectionSelection <CollectionSelection
onChange={setCollection} onChange={setCollection}
defaultValue={{ defaultValue={{
@ -132,11 +132,11 @@ export default function NewLinkModal({ onClose }: Props) {
value: link.collection.id, value: link.collection.id,
}} }}
/> />
) : null} )}
</div> </div>
</div> </div>
<div className={"mt-2"}> <div className={"mt-2"}>
{optionsExpanded ? ( {optionsExpanded && (
<div className="mt-5"> <div className="mt-5">
<div className="grid sm:grid-cols-2 gap-3"> <div className="grid sm:grid-cols-2 gap-3">
<div> <div>
@ -171,7 +171,7 @@ export default function NewLinkModal({ onClose }: Props) {
</div> </div>
</div> </div>
</div> </div>
) : undefined} )}
</div> </div>
<div className="flex justify-between items-center mt-5"> <div className="flex justify-between items-center mt-5">
<div <div

View File

@ -79,7 +79,7 @@ export default function NewUserModal({ onClose }: Props) {
/> />
</div> </div>
{emailEnabled ? ( {emailEnabled && (
<div> <div>
<p className="mb-2">{t("email")}</p> <p className="mb-2">{t("email")}</p>
<TextInput <TextInput
@ -89,7 +89,7 @@ export default function NewUserModal({ onClose }: Props) {
value={form.email} value={form.email}
/> />
</div> </div>
) : undefined} )}
<div> <div>
<p className="mb-2"> <p className="mb-2">

View File

@ -2,6 +2,7 @@ import React, { useEffect, useState } from "react";
import { import {
LinkIncludingShortenedCollectionAndTags, LinkIncludingShortenedCollectionAndTags,
ArchivedFormat, ArchivedFormat,
AccountSettings,
} from "@/types/global"; } from "@/types/global";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
import Link from "next/link"; import Link from "next/link";
@ -35,15 +36,9 @@ export default function PreservedFormatsModal({ onClose, link }: Props) {
let isPublic = router.pathname.startsWith("/public") ? true : undefined; let isPublic = router.pathname.startsWith("/public") ? true : undefined;
const [collectionOwner, setCollectionOwner] = useState({ const [collectionOwner, setCollectionOwner] = useState<
id: null as unknown as number, Partial<AccountSettings>
name: "", >({});
username: "",
image: "",
archiveAsScreenshot: undefined as unknown as boolean,
archiveAsMonolith: undefined as unknown as boolean,
archiveAsPDF: undefined as unknown as boolean,
});
useEffect(() => { useEffect(() => {
const fetchOwner = async () => { const fetchOwner = async () => {
@ -99,7 +94,7 @@ export default function PreservedFormatsModal({ onClose, link }: Props) {
await getLink.mutateAsync({ id: link.id as number }); await getLink.mutateAsync({ id: link.id as number });
})(); })();
let interval: any; let interval: NodeJS.Timeout | null = null;
if (!isReady()) { if (!isReady()) {
interval = setInterval(async () => { interval = setInterval(async () => {
@ -149,7 +144,7 @@ export default function PreservedFormatsModal({ onClose, link }: Props) {
)} )}
<div className={`flex flex-col gap-3`}> <div className={`flex flex-col gap-3`}>
{monolithAvailable(link) ? ( {monolithAvailable(link) && (
<PreservedFormatRow <PreservedFormatRow
name={t("webpage")} name={t("webpage")}
icon={"bi-filetype-html"} icon={"bi-filetype-html"}
@ -157,9 +152,9 @@ export default function PreservedFormatsModal({ onClose, link }: Props) {
link={link} link={link}
downloadable={true} downloadable={true}
/> />
) : undefined} )}
{screenshotAvailable(link) ? ( {screenshotAvailable(link) && (
<PreservedFormatRow <PreservedFormatRow
name={t("screenshot")} name={t("screenshot")}
icon={"bi-file-earmark-image"} icon={"bi-file-earmark-image"}
@ -171,9 +166,9 @@ export default function PreservedFormatsModal({ onClose, link }: Props) {
link={link} link={link}
downloadable={true} downloadable={true}
/> />
) : undefined} )}
{pdfAvailable(link) ? ( {pdfAvailable(link) && (
<PreservedFormatRow <PreservedFormatRow
name={t("pdf")} name={t("pdf")}
icon={"bi-file-earmark-pdf"} icon={"bi-file-earmark-pdf"}
@ -181,16 +176,16 @@ export default function PreservedFormatsModal({ onClose, link }: Props) {
link={link} link={link}
downloadable={true} downloadable={true}
/> />
) : undefined} )}
{readabilityAvailable(link) ? ( {readabilityAvailable(link) && (
<PreservedFormatRow <PreservedFormatRow
name={t("readable")} name={t("readable")}
icon={"bi-file-earmark-text"} icon={"bi-file-earmark-text"}
format={ArchivedFormat.readability} format={ArchivedFormat.readability}
link={link} link={link}
/> />
) : 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`}>
@ -203,17 +198,20 @@ export default function PreservedFormatsModal({ onClose, link }: Props) {
<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>
) : !isReady() && atLeastOneFormatAvailable() ? ( ) : (
<div className={`w-full h-full flex flex-col justify-center p-5`}> !isReady() &&
<BeatLoader atLeastOneFormatAvailable() && (
color="oklch(var(--p))" <div className={`w-full h-full flex flex-col justify-center p-5`}>
className="mx-auto mb-3" <BeatLoader
size={20} color="oklch(var(--p))"
/> className="mx-auto mb-3"
<p className="text-center">{t("there_are_more_formats")}</p> size={20}
<p className="text-center text-sm">{t("check_back_later")}</p> />
</div> <p className="text-center">{t("there_are_more_formats")}</p>
) : undefined} <p className="text-center text-sm">{t("check_back_later")}</p>
</div>
)
)}
<div <div
className={`flex flex-col sm:flex-row gap-3 items-center justify-center ${ className={`flex flex-col sm:flex-row gap-3 items-center justify-center ${

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])}
/> />
@ -163,7 +163,7 @@ export default function UploadFileModal({ onClose }: Props) {
</div> </div>
<div className="sm:col-span-2 col-span-5"> <div className="sm:col-span-2 col-span-5">
<p className="mb-2">{t("collection")}</p> <p className="mb-2">{t("collection")}</p>
{link.collection.name ? ( {link.collection.name && (
<CollectionSelection <CollectionSelection
onChange={setCollection} onChange={setCollection}
defaultValue={{ defaultValue={{
@ -171,10 +171,10 @@ export default function UploadFileModal({ onClose }: Props) {
value: link.collection.id, value: link.collection.id,
}} }}
/> />
) : null} )}
</div> </div>
</div> </div>
{optionsExpanded ? ( {optionsExpanded && (
<div className="mt-5"> <div className="mt-5">
<div className="grid sm:grid-cols-2 gap-3"> <div className="grid sm:grid-cols-2 gap-3">
<div> <div>
@ -209,7 +209,7 @@ export default function UploadFileModal({ onClose }: Props) {
</div> </div>
</div> </div>
</div> </div>
) : undefined} )}
<div className="flex justify-between items-center mt-5"> <div className="flex justify-between items-center mt-5">
<div <div
onClick={() => setOptionsExpanded(!optionsExpanded)} onClick={() => setOptionsExpanded(!optionsExpanded)}

View File

@ -114,7 +114,7 @@ export default function Navbar() {
<MobileNavigation /> <MobileNavigation />
{sidebar ? ( {sidebar && (
<div className="fixed top-0 bottom-0 right-0 left-0 bg-black bg-opacity-10 backdrop-blur-sm flex items-center fade-in z-40"> <div className="fixed top-0 bottom-0 right-0 left-0 bg-black bg-opacity-10 backdrop-blur-sm flex items-center fade-in z-40">
<ClickAwayHandler className="h-full" onClickOutside={toggleSidebar}> <ClickAwayHandler className="h-full" onClickOutside={toggleSidebar}>
<div className="slide-right h-full shadow-lg"> <div className="slide-right h-full shadow-lg">
@ -122,16 +122,14 @@ export default function Navbar() {
</div> </div>
</ClickAwayHandler> </ClickAwayHandler>
</div> </div>
) : null} )}
{newLinkModal ? ( {newLinkModal && <NewLinkModal onClose={() => setNewLinkModal(false)} />}
<NewLinkModal onClose={() => setNewLinkModal(false)} /> {newCollectionModal && (
) : undefined}
{newCollectionModal ? (
<NewCollectionModal onClose={() => setNewCollectionModal(false)} /> <NewCollectionModal onClose={() => setNewCollectionModal(false)} />
) : undefined} )}
{uploadFileModal ? ( {uploadFileModal && (
<UploadFileModal onClose={() => setUploadFileModal(false)} /> <UploadFileModal onClose={() => setUploadFileModal(false)} />
) : undefined} )}
</div> </div>
); );
} }

View File

@ -39,9 +39,7 @@ export default function NoLinksFound({ text }: Props) {
</span> </span>
</div> </div>
</div> </div>
{newLinkModal ? ( {newLinkModal && <NewLinkModal onClose={() => setNewLinkModal(false)} />}
<NewLinkModal onClose={() => setNewLinkModal(false)} />
) : undefined}
</div> </div>
); );
} }

View File

@ -4,7 +4,6 @@ import {
} from "@/types/global"; } from "@/types/global";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { useGetLink } from "@/hooks/store/links";
type Props = { type Props = {
name: string; name: string;
@ -21,8 +20,6 @@ export default function PreservedFormatRow({
link, link,
downloadable, downloadable,
}: Props) { }: Props) {
const getLink = useGetLink();
const router = useRouter(); const router = useRouter();
let isPublic = router.pathname.startsWith("/public") ? true : undefined; let isPublic = router.pathname.startsWith("/public") ? true : undefined;

View File

@ -60,7 +60,7 @@ export default function ProfileDropdown() {
})} })}
</div> </div>
</li> </li>
{isAdmin ? ( {isAdmin && (
<li> <li>
<Link <Link
href="/admin" href="/admin"
@ -72,7 +72,7 @@ export default function ProfileDropdown() {
{t("server_administration")} {t("server_administration")}
</Link> </Link>
</li> </li>
) : null} )}
<li> <li>
<div <div
onClick={() => { onClick={() => {

View File

@ -68,7 +68,7 @@ export default function ReadableView({ link }: Props) {
useEffect(() => { useEffect(() => {
if (link) getLink.mutateAsync({ id: link.id as number }); if (link) getLink.mutateAsync({ id: link.id as number });
let interval: any; let interval: NodeJS.Timeout | null = null;
if ( if (
link && link &&
(link?.image === "pending" || (link?.image === "pending" ||
@ -182,7 +182,7 @@ export default function ReadableView({ link }: Props) {
link?.name || link?.description || link?.url || "" link?.name || link?.description || link?.url || ""
)} )}
</p> </p>
{link?.url ? ( {link?.url && (
<Link <Link
href={link?.url || ""} href={link?.url || ""}
title={link?.url} title={link?.url}
@ -191,11 +191,10 @@ export default function ReadableView({ link }: Props) {
> >
<i className="bi-link-45deg"></i> <i className="bi-link-45deg"></i>
{isValidUrl(link?.url || "") {isValidUrl(link?.url || "") &&
? new URL(link?.url as string).host new URL(link?.url as string).host}
: undefined}
</Link> </Link>
) : undefined} )}
</div> </div>
</div> </div>

View File

@ -1,5 +1,5 @@
import useLocalSettingsStore from "@/store/localSettings"; import useLocalSettingsStore from "@/store/localSettings";
import { useEffect, useState } from "react"; import { useEffect, useState, ChangeEvent } from "react";
import { useTranslation } from "next-i18next"; import { useTranslation } from "next-i18next";
type Props = { type Props = {
@ -10,14 +10,18 @@ export default function ToggleDarkMode({ className }: Props) {
const { t } = useTranslation(); const { t } = useTranslation();
const { settings, updateSettings } = useLocalSettingsStore(); const { settings, updateSettings } = useLocalSettingsStore();
const [theme, setTheme] = useState(localStorage.getItem("theme")); const [theme, setTheme] = useState<string | null>(
localStorage.getItem("theme")
);
const handleToggle = (e: any) => { const handleToggle = (e: ChangeEvent<HTMLInputElement>) => {
setTheme(e.target.checked ? "dark" : "light"); setTheme(e.target.checked ? "dark" : "light");
}; };
useEffect(() => { useEffect(() => {
updateSettings({ theme: theme as string }); if (theme) {
updateSettings({ theme });
}
}, [theme]); }, [theme]);
return ( return (
@ -34,7 +38,7 @@ export default function ToggleDarkMode({ className }: Props) {
type="checkbox" type="checkbox"
onChange={handleToggle} onChange={handleToggle}
className="theme-controller" className="theme-controller"
checked={localStorage.getItem("theme") === "light" ? false : true} checked={theme === "dark"}
/> />
<i className="bi-sun-fill text-xl swap-on"></i> <i className="bi-sun-fill text-xl swap-on"></i>
<i className="bi-moon-fill text-xl swap-off"></i> <i className="bi-moon-fill text-xl swap-off"></i>

View File

@ -74,12 +74,12 @@ const UserListing = (
</tbody> </tbody>
</table> </table>
{deleteUserModal.isOpen && deleteUserModal.userId ? ( {deleteUserModal.isOpen && deleteUserModal.userId && (
<DeleteUserModal <DeleteUserModal
onClose={() => setDeleteUserModal({ isOpen: false, userId: null })} onClose={() => setDeleteUserModal({ isOpen: false, userId: null })}
userId={deleteUserModal.userId} userId={deleteUserModal.userId}
/> />
) : null} )}
</div> </div>
); );
}; };

View File

@ -2,19 +2,17 @@ import axios, { AxiosError } from "axios"
axios.defaults.baseURL = "http://localhost:3000" axios.defaults.baseURL = "http://localhost:3000"
export async function seedUser (username?: string, password?: string, name?: string) { export async function seedUser(username?: string, password?: string, name?: string) {
try { try {
return await axios.post("/api/v1/users", { return await axios.post("/api/v1/users", {
username: username || "test", username: username || "test",
password: password || "password", password: password || "password",
name: name || "Test User", name: name || "Test User",
}) })
} catch (e: any) { } catch (error) {
if (e instanceof AxiosError) { const axiosError = error as AxiosError;
if (e.response?.status === 400) { if (axiosError && axiosError.response?.status === 400) return
return
} throw error
}
throw e
} }
} }

View File

@ -23,7 +23,7 @@ export default function CenteredForm({
data-testid={dataTestId} data-testid={dataTestId}
> >
<div className="m-auto flex flex-col gap-2 w-full"> <div className="m-auto flex flex-col gap-2 w-full">
{settings.theme ? ( {settings.theme && (
<Image <Image
src={`/linkwarden_${ src={`/linkwarden_${
settings.theme === "dark" ? "dark" : "light" settings.theme === "dark" ? "dark" : "light"
@ -33,12 +33,12 @@ export default function CenteredForm({
alt="Linkwarden" alt="Linkwarden"
className="h-12 w-fit mx-auto" className="h-12 w-fit mx-auto"
/> />
) : undefined} )}
{text ? ( {text && (
<p className="text-lg max-w-[30rem] min-w-80 w-full mx-auto font-semibold px-2 text-center"> <p className="text-lg max-w-[30rem] min-w-80 w-full mx-auto font-semibold px-2 text-center">
{text} {text}
</p> </p>
) : undefined} )}
{children} {children}
<p className="text-center text-xs text-neutral mb-5"> <p className="text-center text-xs text-neutral mb-5">
<Trans <Trans

View File

@ -34,9 +34,9 @@ export default function MainLayout({ children }: Props) {
return ( return (
<div className="flex" data-testid="dashboard-wrapper"> <div className="flex" data-testid="dashboard-wrapper">
{showAnnouncement ? ( {showAnnouncement && (
<Announcement toggleAnnouncementBar={toggleAnnouncementBar} /> <Announcement toggleAnnouncementBar={toggleAnnouncementBar} />
) : undefined} )}
<div className="hidden lg:block"> <div className="hidden lg:block">
<Sidebar className={`fixed top-0`} /> <Sidebar className={`fixed top-0`} />
</div> </div>

View File

@ -54,7 +54,7 @@ export default function SettingsLayout({ children }: Props) {
{children} {children}
{sidebar ? ( {sidebar && (
<div className="fixed top-0 bottom-0 right-0 left-0 bg-black bg-opacity-10 backdrop-blur-sm flex items-center fade-in z-30"> <div className="fixed top-0 bottom-0 right-0 left-0 bg-black bg-opacity-10 backdrop-blur-sm flex items-center fade-in z-30">
<ClickAwayHandler <ClickAwayHandler
className="h-full" className="h-full"
@ -65,7 +65,7 @@ export default function SettingsLayout({ children }: Props) {
</div> </div>
</ClickAwayHandler> </ClickAwayHandler>
</div> </div>
) : null} )}
</div> </div>
</div> </div>
</> </>

View File

@ -1,11 +1,11 @@
import { prisma } from "@/lib/api/db"; import { prisma } from "@/lib/api/db";
import { LinkRequestQuery, Sort } from "@/types/global"; import { LinkRequestQuery, Order, Sort } from "@/types/global";
export default async function getDashboardData( export default async function getDashboardData(
userId: number, userId: number,
query: LinkRequestQuery query: LinkRequestQuery
) { ) {
let order: any = { id: "desc" }; let order: Order = { id: "desc" };
if (query.sort === Sort.DateNewestFirst) order = { id: "desc" }; if (query.sort === Sort.DateNewestFirst) order = { id: "desc" };
else if (query.sort === Sort.DateOldestFirst) order = { id: "asc" }; else if (query.sort === Sort.DateOldestFirst) order = { id: "asc" };
else if (query.sort === Sort.NameAZ) order = { name: "asc" }; else if (query.sort === Sort.NameAZ) order = { name: "asc" };

View File

@ -1,5 +1,5 @@
import { prisma } from "@/lib/api/db"; import { prisma } from "@/lib/api/db";
import { LinkRequestQuery, Sort } from "@/types/global"; import { LinkRequestQuery, Order, Sort } from "@/types/global";
type Response<D> = type Response<D> =
| { | {
@ -17,7 +17,7 @@ export default async function getDashboardData(
userId: number, userId: number,
query: LinkRequestQuery query: LinkRequestQuery
): Promise<Response<any>> { ): Promise<Response<any>> {
let order: any; let order: Order = { id: "desc" };
if (query.sort === Sort.DateNewestFirst) order = { id: "desc" }; if (query.sort === Sort.DateNewestFirst) order = { id: "desc" };
else if (query.sort === Sort.DateOldestFirst) order = { id: "asc" }; else if (query.sort === Sort.DateOldestFirst) order = { id: "asc" };
else if (query.sort === Sort.NameAZ) order = { name: "asc" }; else if (query.sort === Sort.NameAZ) order = { name: "asc" };
@ -105,9 +105,8 @@ export default async function getDashboardData(
}); });
const links = [...recentlyAddedLinks, ...pinnedLinks].sort( const links = [...recentlyAddedLinks, ...pinnedLinks].sort(
(a, b) => (new Date(b.id) as any) - (new Date(a.id) as any) (a, b) => new Date(b.id).getTime() - new Date(a.id).getTime()
); );
return { return {
data: { data: {
links, links,

View File

@ -1,11 +1,11 @@
import { prisma } from "@/lib/api/db"; import { prisma } from "@/lib/api/db";
import { LinkRequestQuery, Sort } from "@/types/global"; import { LinkRequestQuery, Order, Sort } from "@/types/global";
export default async function getLink(userId: number, query: LinkRequestQuery) { export default async function getLink(userId: number, query: LinkRequestQuery) {
const POSTGRES_IS_ENABLED = const POSTGRES_IS_ENABLED =
process.env.DATABASE_URL?.startsWith("postgresql"); process.env.DATABASE_URL?.startsWith("postgresql");
let order: any = { id: "desc" }; let order: Order = { id: "desc" };
if (query.sort === Sort.DateNewestFirst) order = { id: "desc" }; if (query.sort === Sort.DateNewestFirst) order = { id: "desc" };
else if (query.sort === Sort.DateOldestFirst) order = { id: "asc" }; else if (query.sort === Sort.DateOldestFirst) order = { id: "asc" };
else if (query.sort === Sort.NameAZ) order = { name: "asc" }; else if (query.sort === Sort.NameAZ) order = { name: "asc" };

View File

@ -22,18 +22,5 @@ export default async function exportData(userId: number) {
const { password, id, ...userData } = user; const { password, id, ...userData } = user;
function redactIds(obj: any) {
if (Array.isArray(obj)) {
obj.forEach((o) => redactIds(o));
} else if (obj !== null && typeof obj === "object") {
delete obj.id;
for (let key in obj) {
redactIds(obj[key]);
}
}
}
redactIds(userData);
return { response: userData, status: 200 }; return { response: userData, status: 200 };
} }

View File

@ -1,5 +1,4 @@
import { prisma } from "@/lib/api/db"; import { prisma } from "@/lib/api/db";
import { Backup } from "@/types/global";
import createFolder from "@/lib/api/storage/createFolder"; import createFolder from "@/lib/api/storage/createFolder";
const MAX_LINKS_PER_USER = Number(process.env.MAX_LINKS_PER_USER) || 30000; const MAX_LINKS_PER_USER = Number(process.env.MAX_LINKS_PER_USER) || 30000;

View File

@ -1,5 +1,5 @@
import { prisma } from "@/lib/api/db"; import { prisma } from "@/lib/api/db";
import { LinkRequestQuery, Sort } from "@/types/global"; import { LinkRequestQuery, Order, Sort } from "@/types/global";
export default async function getLink( export default async function getLink(
query: Omit<LinkRequestQuery, "tagId" | "pinnedOnly"> query: Omit<LinkRequestQuery, "tagId" | "pinnedOnly">
@ -7,7 +7,7 @@ export default async function getLink(
const POSTGRES_IS_ENABLED = const POSTGRES_IS_ENABLED =
process.env.DATABASE_URL?.startsWith("postgresql"); process.env.DATABASE_URL?.startsWith("postgresql");
let order: any; let order: Order = { id: "desc" };
if (query.sort === Sort.DateNewestFirst) order = { id: "desc" }; if (query.sort === Sort.DateNewestFirst) order = { id: "desc" };
else if (query.sort === Sort.DateOldestFirst) order = { id: "asc" }; else if (query.sort === Sort.DateOldestFirst) order = { id: "asc" };
else if (query.sort === Sort.NameAZ) order = { name: "asc" }; else if (query.sort === Sort.NameAZ) order = { name: "asc" };

View File

@ -29,7 +29,7 @@ export default async function createSession(
secret: process.env.NEXTAUTH_SECRET as string, secret: process.env.NEXTAUTH_SECRET as string,
}); });
const createToken = await prisma.accessToken.create({ await prisma.accessToken.create({
data: { data: {
name: sessionName || "Unknown Device", name: sessionName || "Unknown Device",
userId, userId,

View File

@ -24,10 +24,7 @@ export default async function deleteUserById(
if (!isServerAdmin) { if (!isServerAdmin) {
if (user.password) { if (user.password) {
const isPasswordValid = bcrypt.compareSync( const isPasswordValid = bcrypt.compareSync(body.password, user.password);
body.password,
user.password as string
);
if (!isPasswordValid && !isServerAdmin) { if (!isPasswordValid && !isServerAdmin) {
return { return {

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

@ -14,7 +14,7 @@ export default async function moveFile(from: string, to: string) {
}; };
try { try {
s3Client.copyObject(copyParams, async (err: any) => { s3Client.copyObject(copyParams, async (err: unknown) => {
if (err) { if (err) {
console.error("Error copying the object:", err); console.error("Error copying the object:", err);
} else { } else {

View File

@ -9,7 +9,7 @@ export const resizeImage = (file: File): Promise<Blob> =>
"JPEG", // output format "JPEG", // output format
100, // quality 100, // quality
0, // rotation 0, // rotation
(uri: any) => { (uri) => {
resolve(uri as Blob); resolve(uri as Blob);
}, },
"blob" // output type "blob" // output type

View File

@ -7,10 +7,15 @@ export function isPWA() {
} }
export function isIphone() { export function isIphone() {
return /iPhone/.test(navigator.userAgent) && !(window as any).MSStream; return (
/iPhone/.test(navigator.userAgent) &&
!(window as unknown as { MSStream?: any }).MSStream
);
} }
export function dropdownTriggerer(e: any) { export function dropdownTriggerer(
e: React.FocusEvent<HTMLElement> | React.MouseEvent<HTMLElement>
) {
let targetEl = e.currentTarget; let targetEl = e.currentTarget;
if (targetEl && targetEl.matches(":focus")) { if (targetEl && targetEl.matches(":focus")) {
setTimeout(function () { setTimeout(function () {

View File

@ -39,7 +39,9 @@ export function monolithAvailable(
); );
} }
export function previewAvailable(link: any) { export function previewAvailable(
link: LinkIncludingShortenedCollectionAndTags
) {
return ( return (
link && link &&
link.preview && link.preview &&

View File

@ -100,9 +100,7 @@ export default function Admin() {
<p>{t("no_users_found")}</p> <p>{t("no_users_found")}</p>
)} )}
{newUserModal ? ( {newUserModal && <NewUserModal onClose={() => setNewUserModal(false)} />}
<NewUserModal onClose={() => setNewUserModal(false)} />
) : null}
</div> </div>
); );
} }

View File

@ -166,8 +166,12 @@ export default async function Index(req: NextApiRequest, res: NextApiResponse) {
where: { id: linkId }, where: { id: linkId },
}); });
if (linkStillExists && files.file[0].mimetype?.includes("image")) { const { mimetype } = files.file[0];
const collectionId = collectionPermissions.id as number; const isPDF = mimetype?.includes("pdf");
const isImage = mimetype?.includes("image");
if (linkStillExists && isImage) {
const collectionId = collectionPermissions.id;
createFolder({ createFolder({
filePath: `archives/preview/${collectionId}`, filePath: `archives/preview/${collectionId}`,
}); });
@ -184,13 +188,11 @@ export default async function Index(req: NextApiRequest, res: NextApiResponse) {
await prisma.link.update({ await prisma.link.update({
where: { id: linkId }, where: { id: linkId },
data: { data: {
preview: files.file[0].mimetype?.includes("pdf") preview: isPDF ? "unavailable" : undefined,
? "unavailable" image: isImage
: undefined,
image: files.file[0].mimetype?.includes("image")
? `archives/${collectionPermissions.id}/${linkId + suffix}` ? `archives/${collectionPermissions.id}/${linkId + suffix}`
: null, : null,
pdf: files.file[0].mimetype?.includes("pdf") pdf: isPDF
? `archives/${collectionPermissions.id}/${linkId + suffix}` ? `archives/${collectionPermissions.id}/${linkId + suffix}`
: null, : null,
lastPreserved: new Date().toISOString(), lastPreserved: new Date().toISOString(),

View File

@ -1,4 +1,5 @@
import { import {
AccountSettings,
CollectionIncludingMembersAndLinkCount, CollectionIncludingMembersAndLinkCount,
Sort, Sort,
ViewMode, ViewMode,
@ -54,15 +55,9 @@ export default function Index() {
const { data: user = {} } = useUser(); const { data: user = {} } = useUser();
const [collectionOwner, setCollectionOwner] = useState({ const [collectionOwner, setCollectionOwner] = useState<
id: null as unknown as number, Partial<AccountSettings>
name: "", >({});
username: "",
image: "",
archiveAsScreenshot: undefined as unknown as boolean,
archiveAsMonolith: undefined as unknown as boolean,
archiveAsPDF: undefined as unknown as boolean,
});
useEffect(() => { useEffect(() => {
const fetchOwner = async () => { const fetchOwner = async () => {
@ -207,14 +202,14 @@ export default function Index() {
className="flex items-center btn px-2 btn-ghost rounded-full w-fit" className="flex items-center btn px-2 btn-ghost rounded-full w-fit"
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}
/> />
) : undefined} )}
{activeCollection.members {activeCollection.members
.sort((a, b) => (a.userId as number) - (b.userId as number)) .sort((a, b) => a.userId - b.userId)
.map((e, i) => { .map((e, i) => {
return ( return (
<ProfilePhoto <ProfilePhoto
@ -226,13 +221,13 @@ export default function Index() {
); );
}) })
.slice(0, 3)} .slice(0, 3)}
{activeCollection.members.length - 3 > 0 ? ( {activeCollection.members.length - 3 > 0 && (
<div className={`avatar drop-shadow-md placeholder -ml-3`}> <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"> <div className="bg-base-100 text-neutral rounded-full w-8 h-8 ring-2 ring-neutral-content">
<span>+{activeCollection.members.length - 3}</span> <span>+{activeCollection.members.length - 3}</span>
</div> </div>
</div> </div>
) : null} )}
</div> </div>
<p className="text-neutral text-sm"> <p className="text-neutral text-sm">

View File

@ -60,7 +60,7 @@ export default function Collections() {
</div> </div>
</div> </div>
{sortedCollections.filter((e) => e.ownerId !== data?.user.id)[0] ? ( {sortedCollections.filter((e) => e.ownerId !== data?.user.id)[0] && (
<> <>
<PageHeader <PageHeader
icon={"bi-folder"} icon={"bi-folder"}
@ -76,11 +76,11 @@ export default function Collections() {
})} })}
</div> </div>
</> </>
) : undefined} )}
</div> </div>
{newCollectionModal ? ( {newCollectionModal && (
<NewCollectionModal onClose={() => setNewCollectionModal(false)} /> <NewCollectionModal onClose={() => setNewCollectionModal(false)} />
) : undefined} )}
</MainLayout> </MainLayout>
); );
} }

View File

@ -55,11 +55,14 @@ export default function Dashboard() {
handleNumberOfLinksToShow(); handleNumberOfLinksToShow();
}, [width]); }, [width]);
const importBookmarks = async (e: any, format: MigrationFormat) => { const importBookmarks = async (
const file: File = e.target.files[0]; e: React.ChangeEvent<HTMLInputElement>,
format: MigrationFormat
) => {
const file: File | null = e.target.files && e.target.files[0];
if (file) { if (file) {
var reader = new FileReader(); const reader = new FileReader();
reader.readAsText(file, "UTF-8"); reader.readAsText(file, "UTF-8");
reader.onload = async function (e) { reader.onload = async function (e) {
const load = toast.loading("Importing..."); const load = toast.loading("Importing...");
@ -132,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>
@ -339,9 +342,7 @@ export default function Dashboard() {
)} )}
</div> </div>
</div> </div>
{newLinkModal ? ( {newLinkModal && <NewLinkModal onClose={() => setNewLinkModal(false)} />}
<NewLinkModal onClose={() => setNewLinkModal(false)} />
) : undefined}
</MainLayout> </MainLayout>
); );
} }

View File

@ -203,9 +203,9 @@ export default function Login({
{t("login")} {t("login")}
</Button> </Button>
{availableLogins.buttonAuths.length > 0 ? ( {availableLogins.buttonAuths.length > 0 && (
<div className="divider my-1">{t("or_continue_with")}</div> <div className="divider my-1">{t("or_continue_with")}</div>
) : undefined} )}
</> </>
); );
} }
@ -224,9 +224,9 @@ export default function Login({
loading={submitLoader} loading={submitLoader}
> >
{value.name.toLowerCase() === "google" || {value.name.toLowerCase() === "google" ||
value.name.toLowerCase() === "apple" ? ( (value.name.toLowerCase() === "apple" && (
<i className={"bi-" + value.name.toLowerCase()}></i> <i className={"bi-" + value.name.toLowerCase()}></i>
) : undefined} ))}
{value.name} {value.name}
</Button> </Button>
</React.Fragment> </React.Fragment>

View File

@ -1,6 +1,7 @@
"use client"; "use client";
import getPublicCollectionData from "@/lib/client/getPublicCollectionData"; import getPublicCollectionData from "@/lib/client/getPublicCollectionData";
import { import {
AccountSettings,
CollectionIncludingMembersAndLinkCount, CollectionIncludingMembersAndLinkCount,
Sort, Sort,
ViewMode, ViewMode,
@ -29,15 +30,9 @@ export default function PublicCollections() {
const router = useRouter(); const router = useRouter();
const [collectionOwner, setCollectionOwner] = useState({ const [collectionOwner, setCollectionOwner] = useState<
id: null as unknown as number, Partial<AccountSettings>
name: "", >({});
username: "",
image: "",
archiveAsScreenshot: undefined as unknown as boolean,
archiveAsMonolith: undefined as unknown as boolean,
archiveAsPDF: undefined as unknown as boolean,
});
const [searchFilter, setSearchFilter] = useState({ const [searchFilter, setSearchFilter] = useState({
name: true, name: true,
@ -93,160 +88,162 @@ export default function PublicCollections() {
(localStorage.getItem("viewMode") as ViewMode) || ViewMode.Card (localStorage.getItem("viewMode") as ViewMode) || ViewMode.Card
); );
return collection ? ( if (!collection) return <></>;
<div else
className="h-96" return (
style={{ <div
backgroundImage: `linear-gradient(${collection?.color}30 10%, ${ className="h-96"
settings.theme === "dark" ? "#262626" : "#f3f4f6" style={{
} 13rem, ${settings.theme === "dark" ? "#171717" : "#ffffff"} 100%)`, 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> {collection && (
<meta <Head>
property="og:title" <title>{collection.name} | Linkwarden</title>
content={`${collection.name} | Linkwarden`} <meta
key="title" property="og:title"
/> content={`${collection.name} | Linkwarden`}
</Head> key="title"
) : undefined} />
<div className="lg:w-3/4 w-full mx-auto p-5 bg"> </Head>
<div className="flex items-center justify-between"> )}
<p className="text-4xl font-thin mb-2 capitalize mt-10"> <div className="lg:w-3/4 w-full mx-auto p-5 bg">
{collection.name} <div className="flex items-center justify-between">
</p> <p className="text-4xl font-thin mb-2 capitalize mt-10">
<div className="flex gap-2 items-center mt-8 min-w-fit"> {collection.name}
<ToggleDarkMode /> </p>
<div className="flex gap-2 items-center mt-8 min-w-fit">
<ToggleDarkMode />
<Link href="https://linkwarden.app/" target="_blank"> <Link href="https://linkwarden.app/" target="_blank">
<Image <Image
src={`/icon.png`} src={`/icon.png`}
width={551} width={551}
height={551} height={551}
alt="Linkwarden" alt="Linkwarden"
title={t("list_created_with_linkwarden")} title={t("list_created_with_linkwarden")}
className="h-8 w-fit mx-auto rounded" className="h-8 w-fit mx-auto rounded"
/> />
</Link> </Link>
</div>
</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}
/> />
) : undefined} )}
{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>
) : null}
</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}
/>
) : undefined}
</div>
) : (
<></>
);
} }
export { getServerSideProps }; export { getServerSideProps };

View File

@ -133,9 +133,9 @@ export default function Register({
loading={submitLoader} loading={submitLoader}
> >
{value.name.toLowerCase() === "google" || {value.name.toLowerCase() === "google" ||
value.name.toLowerCase() === "apple" ? ( (value.name.toLowerCase() === "apple" && (
<i className={"bi-" + value.name.toLowerCase()}></i> <i className={"bi-" + value.name.toLowerCase()}></i>
) : undefined} ))}
{value.name} {value.name}
</Button> </Button>
</React.Fragment> </React.Fragment>
@ -201,7 +201,7 @@ export default function Register({
</div> </div>
)} )}
{emailEnabled ? ( {emailEnabled && (
<div> <div>
<p className="text-sm w-fit font-semibold mb-1">{t("email")}</p> <p className="text-sm w-fit font-semibold mb-1">{t("email")}</p>
@ -214,7 +214,7 @@ export default function Register({
onChange={(e) => setForm({ ...form, email: e.target.value })} onChange={(e) => setForm({ ...form, email: e.target.value })}
/> />
</div> </div>
) : undefined} )}
<div className="w-full"> <div className="w-full">
<p className="text-sm w-fit font-semibold mb-1"> <p className="text-sm w-fit font-semibold mb-1">
@ -248,7 +248,7 @@ export default function Register({
/> />
</div> </div>
{process.env.NEXT_PUBLIC_STRIPE ? ( {process.env.NEXT_PUBLIC_STRIPE && (
<div className="text-xs text-neutral mb-3"> <div className="text-xs text-neutral mb-3">
<p> <p>
<Trans <Trans
@ -270,7 +270,7 @@ export default function Register({
/> />
</p> </p>
</div> </div>
) : undefined} )}
<Button <Button
type="submit" type="submit"
@ -282,9 +282,9 @@ export default function Register({
{t("sign_up")} {t("sign_up")}
</Button> </Button>
{availableLogins.buttonAuths.length > 0 ? ( {availableLogins.buttonAuths.length > 0 && (
<div className="divider my-1">{t("or_continue_with")}</div> <div className="divider my-1">{t("or_continue_with")}</div>
) : undefined} )}
{displayLoginExternalButton()} {displayLoginExternalButton()}
<div> <div>
@ -298,7 +298,7 @@ export default function Register({
{t("login")} {t("login")}
</Link> </Link>
</div> </div>
{process.env.NEXT_PUBLIC_STRIPE ? ( {process.env.NEXT_PUBLIC_STRIPE && (
<div className="text-neutral text-center flex items-baseline gap-1 justify-center"> <div className="text-neutral text-center flex items-baseline gap-1 justify-center">
<p>{t("need_help")}</p> <p>{t("need_help")}</p>
<Link <Link
@ -309,7 +309,7 @@ export default function Register({
{t("get_in_touch")} {t("get_in_touch")}
</Link> </Link>
</div> </div>
) : undefined} )}
</div> </div>
</div> </div>
</form> </form>

View File

@ -40,7 +40,7 @@ export default function AccessTokens() {
{t("new_token")} {t("new_token")}
</button> </button>
{tokens.length > 0 ? ( {tokens.length > 0 && (
<table className="table mt-2 overflow-x-auto"> <table className="table mt-2 overflow-x-auto">
<thead> <thead>
<tr> <tr>
@ -85,12 +85,12 @@ export default function AccessTokens() {
))} ))}
</tbody> </tbody>
</table> </table>
) : undefined} )}
</div> </div>
{newTokenModal ? ( {newTokenModal && (
<NewTokenModal onClose={() => setNewTokenModal(false)} /> <NewTokenModal onClose={() => setNewTokenModal(false)} />
) : undefined} )}
{revokeTokenModal && selectedToken && ( {revokeTokenModal && selectedToken && (
<RevokeTokenModal <RevokeTokenModal
onClose={() => { onClose={() => {

View File

@ -1,4 +1,4 @@
import { useState, useEffect } from "react"; import { useState, useEffect, ChangeEvent } from "react";
import { AccountSettings } from "@/types/global"; import { AccountSettings } from "@/types/global";
import { toast } from "react-hot-toast"; import { toast } from "react-hot-toast";
import SettingsLayout from "@/layouts/SettingsLayout"; import SettingsLayout from "@/layouts/SettingsLayout";
@ -55,8 +55,10 @@ export default function Account() {
if (!objectIsEmpty(account)) setUser({ ...account }); if (!objectIsEmpty(account)) setUser({ ...account });
}, [account]); }, [account]);
const handleImageUpload = async (e: any) => { const handleImageUpload = async (e: ChangeEvent<HTMLInputElement>) => {
const file: File = e.target.files[0]; const file = e.target.files?.[0];
if (!file) return toast.error(t("image_upload_no_file_error"));
const fileExtension = file.name.split(".").pop()?.toLowerCase(); const fileExtension = file.name.split(".").pop()?.toLowerCase();
const allowedExtensions = ["png", "jpeg", "jpg"]; const allowedExtensions = ["png", "jpeg", "jpg"];
if (allowedExtensions.includes(fileExtension as string)) { if (allowedExtensions.includes(fileExtension as string)) {
@ -114,9 +116,13 @@ export default function Account() {
setSubmitLoader(false); setSubmitLoader(false);
}; };
const importBookmarks = async (e: any, format: MigrationFormat) => { const importBookmarks = async (
e: ChangeEvent<HTMLInputElement>,
format: MigrationFormat
) => {
setSubmitLoader(true); setSubmitLoader(true);
const file: File = e.target.files[0]; const file = e.target.files?.[0];
if (file) { if (file) {
var reader = new FileReader(); var reader = new FileReader();
reader.readAsText(file, "UTF-8"); reader.readAsText(file, "UTF-8");
@ -190,7 +196,7 @@ export default function Account() {
onChange={(e) => setUser({ ...user, username: e.target.value })} onChange={(e) => setUser({ ...user, username: e.target.value })}
/> />
</div> </div>
{emailEnabled ? ( {emailEnabled && (
<div> <div>
<p className="mb-2">{t("email")}</p> <p className="mb-2">{t("email")}</p>
<TextInput <TextInput
@ -199,7 +205,7 @@ export default function Account() {
onChange={(e) => setUser({ ...user, email: e.target.value })} onChange={(e) => setUser({ ...user, email: e.target.value })}
/> />
</div> </div>
) : undefined} )}
<div> <div>
<p className="mb-2">{t("language")}</p> <p className="mb-2">{t("language")}</p>
<select <select
@ -437,9 +443,8 @@ export default function Account() {
<p> <p>
{t("delete_account_warning")} {t("delete_account_warning")}
{process.env.NEXT_PUBLIC_STRIPE {process.env.NEXT_PUBLIC_STRIPE &&
? " " + t("cancel_subscription_notice") " " + t("cancel_subscription_notice")}
: undefined}
</p> </p>
</div> </div>
@ -448,14 +453,14 @@ export default function Account() {
</Link> </Link>
</div> </div>
{emailChangeVerificationModal ? ( {emailChangeVerificationModal && (
<EmailChangeVerificationModal <EmailChangeVerificationModal
onClose={() => setEmailChangeVerificationModal(false)} onClose={() => setEmailChangeVerificationModal(false)}
onSubmit={submit} onSubmit={submit}
oldEmail={account.email || ""} oldEmail={account.email || ""}
newEmail={user.email || ""} newEmail={user.email || ""}
/> />
) : undefined} )}
</SettingsLayout> </SettingsLayout>
); );
} }

View File

@ -83,7 +83,7 @@ export default function Delete() {
/> />
</div> </div>
{process.env.NEXT_PUBLIC_STRIPE ? ( {process.env.NEXT_PUBLIC_STRIPE && (
<fieldset className="border rounded-md p-2 border-primary"> <fieldset className="border rounded-md p-2 border-primary">
<legend className="px-3 py-1 text-sm sm:text-base border rounded-md border-primary"> <legend className="px-3 py-1 text-sm sm:text-base border rounded-md border-primary">
<b>{t("optional")}</b> <i>{t("feedback_help")}</i> <b>{t("optional")}</b> <i>{t("feedback_help")}</i>
@ -123,7 +123,7 @@ export default function Delete() {
/> />
</div> </div>
</fieldset> </fieldset>
) : undefined} )}
<Button <Button
className="mx-auto" className="mx-auto"

View File

@ -117,6 +117,7 @@
"applying_settings": "Applying settings...", "applying_settings": "Applying settings...",
"settings_applied": "Settings Applied!", "settings_applied": "Settings Applied!",
"email_change_request": "Email change request sent. Please verify the new email address.", "email_change_request": "Email change request sent. Please verify the new email address.",
"image_upload_no_file_error": "No file selected. Please choose an image to upload.",
"image_upload_size_error": "Please select a PNG or JPEG file that's less than 1MB.", "image_upload_size_error": "Please select a PNG or JPEG file that's less than 1MB.",
"image_upload_format_error": "Invalid file format.", "image_upload_format_error": "Invalid file format.",
"importing_bookmarks": "Importing bookmarks...", "importing_bookmarks": "Importing bookmarks...",

View File

@ -117,6 +117,7 @@
"applying_settings": "Applicazione delle impostazioni in corso...", "applying_settings": "Applicazione delle impostazioni in corso...",
"settings_applied": "Impostazioni Applicate!", "settings_applied": "Impostazioni Applicate!",
"email_change_request": "Richiesta di cambio email inviata. Per favore verifica il nuovo indirizzo email.", "email_change_request": "Richiesta di cambio email inviata. Per favore verifica il nuovo indirizzo email.",
"image_upload_no_file_error": "Nessun file selezionato. Scegli un'immagine da caricare.",
"image_upload_size_error": "Per favore seleziona un file PNG o JPEG di dimensioni inferiori a 1MB.", "image_upload_size_error": "Per favore seleziona un file PNG o JPEG di dimensioni inferiori a 1MB.",
"image_upload_format_error": "Formato file non valido.", "image_upload_format_error": "Formato file non valido.",
"importing_bookmarks": "Importazione dei segnalibri in corso...", "importing_bookmarks": "Importazione dei segnalibri in corso...",
@ -367,4 +368,4 @@
"server_administration": "Amministrazione Server", "server_administration": "Amministrazione Server",
"all_collections": "Tutte le Collezioni", "all_collections": "Tutte le Collezioni",
"dashboard": "Dashboard" "dashboard": "Dashboard"
} }

View File

@ -80,6 +80,8 @@ export enum Sort {
DescriptionZA, DescriptionZA,
} }
export type Order = { [key: string]: "asc" | "desc" };
export type LinkRequestQuery = { export type LinkRequestQuery = {
sort?: Sort; sort?: Sort;
cursor?: number; cursor?: number;