Merge pull request #722 from linkwarden/dev

Dev
This commit is contained in:
Daniel 2024-08-18 16:41:48 -04:00 committed by GitHub
commit 9f74f62330
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 { CollectionIncludingMembersAndLinkCount } from "@/types/global";
import {
AccountSettings,
CollectionIncludingMembersAndLinkCount,
} from "@/types/global";
import React, { useEffect, useState } from "react";
import ProfilePhoto from "./ProfilePhoto";
import usePermissions from "@/hooks/usePermissions";
@ -12,12 +15,11 @@ import { dropdownTriggerer } from "@/lib/client/utils";
import { useTranslation } from "next-i18next";
import { useUser } from "@/hooks/store/user";
type Props = {
export default function CollectionCard({
collection,
}: {
collection: CollectionIncludingMembersAndLinkCount;
className?: string;
};
export default function CollectionCard({ collection, className }: Props) {
}) {
const { t } = useTranslation();
const { settings } = useLocalSettingsStore();
const { data: user = {} } = useUser();
@ -33,15 +35,9 @@ export default function CollectionCard({ collection, className }: Props) {
const permissions = usePermissions(collection.id as number);
const [collectionOwner, setCollectionOwner] = useState({
id: null as unknown as number,
name: "",
username: "",
image: "",
archiveAsScreenshot: undefined as unknown as boolean,
archiveAsMonolith: undefined as unknown as boolean,
archiveAsPDF: undefined as unknown as boolean,
});
const [collectionOwner, setCollectionOwner] = useState<
Partial<AccountSettings>
>({});
useEffect(() => {
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"
onClick={() => setEditCollectionSharingModal(true)}
>
{collectionOwner.id ? (
{collectionOwner.id && (
<ProfilePhoto
src={collectionOwner.image || undefined}
name={collectionOwner.name}
/>
) : undefined}
)}
{collection.members
.sort((a, b) => (a.userId as number) - (b.userId as number))
.map((e, i) => {
@ -151,13 +147,13 @@ export default function CollectionCard({ collection, className }: Props) {
);
})
.slice(0, 3)}
{collection.members.length - 3 > 0 ? (
{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>
<Link
href={`/collections/${collection.id}`}
@ -181,12 +177,12 @@ export default function CollectionCard({ collection, className }: Props) {
<div className="flex justify-end items-center">
<div className="text-right">
<div className="font-bold text-sm flex justify-end gap-1 items-center">
{collection.isPublic ? (
{collection.isPublic && (
<i
className="bi-globe2 drop-shadow text-neutral"
title="This collection is being shared publicly."
></i>
) : undefined}
)}
<i
className="bi-link-45deg text-lg text-neutral"
title="This collection is being shared publicly."
@ -206,24 +202,24 @@ export default function CollectionCard({ collection, className }: Props) {
</div>
</div>
</Link>
{editCollectionModal ? (
{editCollectionModal && (
<EditCollectionModal
onClose={() => setEditCollectionModal(false)}
activeCollection={collection}
/>
) : undefined}
{editCollectionSharingModal ? (
)}
{editCollectionSharingModal && (
<EditCollectionSharingModal
onClose={() => setEditCollectionSharingModal(false)}
activeCollection={collection}
/>
) : undefined}
{deleteCollectionModal ? (
)}
{deleteCollectionModal && (
<DeleteCollectionModal
onClose={() => setDeleteCollectionModal(false)}
activeCollection={collection}
/>
) : undefined}
)}
</div>
);
}

View File

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

View File

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

View File

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

View File

@ -136,7 +136,7 @@ export default function LinkActions({
{t("show_link_details")}
</div>
</li>
{permissions === true || permissions?.canUpdate ? (
{(permissions === true || permissions?.canUpdate) && (
<li>
<div
role="button"
@ -150,7 +150,7 @@ export default function LinkActions({
{t("edit_link")}
</div>
</li>
) : undefined}
)}
{link.type === "url" && (
<li>
<div
@ -166,7 +166,7 @@ export default function LinkActions({
</div>
</li>
)}
{permissions === true || permissions?.canDelete ? (
{(permissions === true || permissions?.canDelete) && (
<li>
<div
role="button"
@ -196,36 +196,35 @@ export default function LinkActions({
{t("delete")}
</div>
</li>
) : undefined}
)}
</ul>
</div>
)}
{editLinkModal ? (
{editLinkModal && (
<EditLinkModal
onClose={() => setEditLinkModal(false)}
activeLink={link}
/>
) : undefined}
{deleteLinkModal ? (
)}
{deleteLinkModal && (
<DeleteLinkModal
onClose={() => setDeleteLinkModal(false)}
activeLink={link}
/>
) : undefined}
{preservedFormatsModal ? (
)}
{preservedFormatsModal && (
<PreservedFormatsModal
onClose={() => setPreservedFormatsModal(false)}
link={link}
/>
) : undefined}
{linkDetailModal ? (
)}
{linkDetailModal && (
<LinkDetailModal
onClose={() => setLinkDetailModal(false)}
onEdit={() => setEditLinkModal(true)}
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;
useEffect(() => {
let interval: any;
let interval: NodeJS.Timeout | null = null;
if (
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="flex items-center gap-x-3 text-neutral flex-wrap">
{collection ? (
{collection && (
<LinkCollection link={link} collection={collection} />
) : undefined}
)}
{link.name && <LinkTypeBadge link={link} />}
<LinkDate link={link} />
</div>

View File

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

View File

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

View File

@ -1,7 +1,11 @@
import React, { useEffect, useState } from "react";
import TextInput from "@/components/TextInput";
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 usePermissions from "@/hooks/usePermissions";
import ProfilePhoto from "../ProfilePhoto";
@ -65,15 +69,9 @@ export default function EditCollectionSharingModal({
const [memberUsername, setMemberUsername] = useState("");
const [collectionOwner, setCollectionOwner] = useState({
id: null as unknown as number,
name: "",
username: "",
image: "",
archiveAsScreenshot: undefined as unknown as boolean,
archiveAsMonolith: undefined as unknown as boolean,
archiveAsPDF: undefined as unknown as boolean,
});
const [collectionOwner, setCollectionOwner] = useState<
Partial<AccountSettings>
>({});
useEffect(() => {
const fetchOwner = async () => {
@ -133,7 +131,7 @@ export default function EditCollectionSharingModal({
</div>
)}
{collection.isPublic ? (
{collection.isPublic && (
<div>
<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">
@ -141,7 +139,7 @@ export default function EditCollectionSharingModal({
<CopyButton text={publicCollectionURL} />
</div>
</div>
) : null}
)}
{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>
{link.url ? (
{link.url && (
<Link
href={link.url}
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" />
<p>{shortenedURL}</p>
</Link>
) : undefined}
)}
<div className="w-full">
<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>
<p className="mb-2">{t("collection")}</p>
{link.collection.name ? (
{link.collection.name && (
<CollectionSelection
onChange={setCollection}
defaultValue={
@ -113,7 +113,7 @@ export default function EditLinkModal({ onClose, activeLink }: Props) {
}
creatable={false}
/>
) : null}
)}
</div>
<div>

View File

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

View File

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

View File

@ -2,6 +2,7 @@ import React, { useEffect, useState } from "react";
import {
LinkIncludingShortenedCollectionAndTags,
ArchivedFormat,
AccountSettings,
} from "@/types/global";
import toast from "react-hot-toast";
import Link from "next/link";
@ -35,15 +36,9 @@ export default function PreservedFormatsModal({ onClose, link }: Props) {
let isPublic = router.pathname.startsWith("/public") ? true : undefined;
const [collectionOwner, setCollectionOwner] = useState({
id: null as unknown as number,
name: "",
username: "",
image: "",
archiveAsScreenshot: undefined as unknown as boolean,
archiveAsMonolith: undefined as unknown as boolean,
archiveAsPDF: undefined as unknown as boolean,
});
const [collectionOwner, setCollectionOwner] = useState<
Partial<AccountSettings>
>({});
useEffect(() => {
const fetchOwner = async () => {
@ -99,7 +94,7 @@ export default function PreservedFormatsModal({ onClose, link }: Props) {
await getLink.mutateAsync({ id: link.id as number });
})();
let interval: any;
let interval: NodeJS.Timeout | null = null;
if (!isReady()) {
interval = setInterval(async () => {
@ -149,7 +144,7 @@ export default function PreservedFormatsModal({ onClose, link }: Props) {
)}
<div className={`flex flex-col gap-3`}>
{monolithAvailable(link) ? (
{monolithAvailable(link) && (
<PreservedFormatRow
name={t("webpage")}
icon={"bi-filetype-html"}
@ -157,9 +152,9 @@ export default function PreservedFormatsModal({ onClose, link }: Props) {
link={link}
downloadable={true}
/>
) : undefined}
)}
{screenshotAvailable(link) ? (
{screenshotAvailable(link) && (
<PreservedFormatRow
name={t("screenshot")}
icon={"bi-file-earmark-image"}
@ -171,9 +166,9 @@ export default function PreservedFormatsModal({ onClose, link }: Props) {
link={link}
downloadable={true}
/>
) : undefined}
)}
{pdfAvailable(link) ? (
{pdfAvailable(link) && (
<PreservedFormatRow
name={t("pdf")}
icon={"bi-file-earmark-pdf"}
@ -181,16 +176,16 @@ export default function PreservedFormatsModal({ onClose, link }: Props) {
link={link}
downloadable={true}
/>
) : undefined}
)}
{readabilityAvailable(link) ? (
{readabilityAvailable(link) && (
<PreservedFormatRow
name={t("readable")}
icon={"bi-file-earmark-text"}
format={ArchivedFormat.readability}
link={link}
/>
) : undefined}
)}
{!isReady() && !atLeastOneFormatAvailable() ? (
<div className={`w-full h-full flex flex-col justify-center p-10`}>
@ -203,7 +198,9 @@ export default function PreservedFormatsModal({ onClose, link }: Props) {
<p className="text-center text-2xl">{t("preservation_in_queue")}</p>
<p className="text-center text-lg">{t("check_back_later")}</p>
</div>
) : !isReady() && atLeastOneFormatAvailable() ? (
) : (
!isReady() &&
atLeastOneFormatAvailable() && (
<div className={`w-full h-full flex flex-col justify-center p-5`}>
<BeatLoader
color="oklch(var(--p))"
@ -213,7 +210,8 @@ export default function PreservedFormatsModal({ onClose, link }: Props) {
<p className="text-center">{t("there_are_more_formats")}</p>
<p className="text-center text-sm">{t("check_back_later")}</p>
</div>
) : undefined}
)
)}
<div
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">
<input
type="file"
accept=".pdf,.png,.jpg,.jpeg,.html"
accept=".pdf,.png,.jpg,.jpeg"
className="cursor-pointer custom-file-input"
onChange={(e) => e.target.files && setFile(e.target.files[0])}
/>
@ -163,7 +163,7 @@ export default function UploadFileModal({ onClose }: Props) {
</div>
<div className="sm:col-span-2 col-span-5">
<p className="mb-2">{t("collection")}</p>
{link.collection.name ? (
{link.collection.name && (
<CollectionSelection
onChange={setCollection}
defaultValue={{
@ -171,10 +171,10 @@ export default function UploadFileModal({ onClose }: Props) {
value: link.collection.id,
}}
/>
) : null}
)}
</div>
</div>
{optionsExpanded ? (
{optionsExpanded && (
<div className="mt-5">
<div className="grid sm:grid-cols-2 gap-3">
<div>
@ -209,7 +209,7 @@ export default function UploadFileModal({ onClose }: Props) {
</div>
</div>
</div>
) : undefined}
)}
<div className="flex justify-between items-center mt-5">
<div
onClick={() => setOptionsExpanded(!optionsExpanded)}

View File

@ -114,7 +114,7 @@ export default function Navbar() {
<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">
<ClickAwayHandler className="h-full" onClickOutside={toggleSidebar}>
<div className="slide-right h-full shadow-lg">
@ -122,16 +122,14 @@ export default function Navbar() {
</div>
</ClickAwayHandler>
</div>
) : null}
{newLinkModal ? (
<NewLinkModal onClose={() => setNewLinkModal(false)} />
) : undefined}
{newCollectionModal ? (
)}
{newLinkModal && <NewLinkModal onClose={() => setNewLinkModal(false)} />}
{newCollectionModal && (
<NewCollectionModal onClose={() => setNewCollectionModal(false)} />
) : undefined}
{uploadFileModal ? (
)}
{uploadFileModal && (
<UploadFileModal onClose={() => setUploadFileModal(false)} />
) : undefined}
)}
</div>
);
}

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,5 @@
import useLocalSettingsStore from "@/store/localSettings";
import { useEffect, useState } from "react";
import { useEffect, useState, ChangeEvent } from "react";
import { useTranslation } from "next-i18next";
type Props = {
@ -10,14 +10,18 @@ export default function ToggleDarkMode({ className }: Props) {
const { t } = useTranslation();
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");
};
useEffect(() => {
updateSettings({ theme: theme as string });
if (theme) {
updateSettings({ theme });
}
}, [theme]);
return (
@ -34,7 +38,7 @@ export default function ToggleDarkMode({ className }: Props) {
type="checkbox"
onChange={handleToggle}
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-moon-fill text-xl swap-off"></i>

View File

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

View File

@ -9,12 +9,10 @@ export async function seedUser (username?: string, password?: string, name?: str
password: password || "password",
name: name || "Test User",
})
} catch (e: any) {
if (e instanceof AxiosError) {
if (e.response?.status === 400) {
return
}
}
throw e
} catch (error) {
const axiosError = error as AxiosError;
if (axiosError && axiosError.response?.status === 400) return
throw error
}
}

View File

@ -23,7 +23,7 @@ export default function CenteredForm({
data-testid={dataTestId}
>
<div className="m-auto flex flex-col gap-2 w-full">
{settings.theme ? (
{settings.theme && (
<Image
src={`/linkwarden_${
settings.theme === "dark" ? "dark" : "light"
@ -33,12 +33,12 @@ export default function CenteredForm({
alt="Linkwarden"
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">
{text}
</p>
) : undefined}
)}
{children}
<p className="text-center text-xs text-neutral mb-5">
<Trans

View File

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

View File

@ -54,7 +54,7 @@ export default function SettingsLayout({ children }: Props) {
{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">
<ClickAwayHandler
className="h-full"
@ -65,7 +65,7 @@ export default function SettingsLayout({ children }: Props) {
</div>
</ClickAwayHandler>
</div>
) : null}
)}
</div>
</div>
</>

View File

@ -1,11 +1,11 @@
import { prisma } from "@/lib/api/db";
import { LinkRequestQuery, Sort } from "@/types/global";
import { LinkRequestQuery, Order, Sort } from "@/types/global";
export default async function getDashboardData(
userId: number,
query: LinkRequestQuery
) {
let order: any = { id: "desc" };
let order: 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.NameAZ) order = { name: "asc" };

View File

@ -1,5 +1,5 @@
import { prisma } from "@/lib/api/db";
import { LinkRequestQuery, Sort } from "@/types/global";
import { LinkRequestQuery, Order, Sort } from "@/types/global";
type Response<D> =
| {
@ -17,7 +17,7 @@ export default async function getDashboardData(
userId: number,
query: LinkRequestQuery
): Promise<Response<any>> {
let order: any;
let order: 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.NameAZ) order = { name: "asc" };
@ -105,9 +105,8 @@ export default async function getDashboardData(
});
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 {
data: {
links,

View File

@ -1,11 +1,11 @@
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) {
const POSTGRES_IS_ENABLED =
process.env.DATABASE_URL?.startsWith("postgresql");
let order: any = { id: "desc" };
let order: 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.NameAZ) order = { name: "asc" };

View File

@ -22,18 +22,5 @@ export default async function exportData(userId: number) {
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 };
}

View File

@ -1,5 +1,4 @@
import { prisma } from "@/lib/api/db";
import { Backup } from "@/types/global";
import createFolder from "@/lib/api/storage/createFolder";
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 { LinkRequestQuery, Sort } from "@/types/global";
import { LinkRequestQuery, Order, Sort } from "@/types/global";
export default async function getLink(
query: Omit<LinkRequestQuery, "tagId" | "pinnedOnly">
@ -7,7 +7,7 @@ export default async function getLink(
const POSTGRES_IS_ENABLED =
process.env.DATABASE_URL?.startsWith("postgresql");
let order: any;
let order: 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.NameAZ) order = { name: "asc" };

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -7,10 +7,15 @@ export function isPWA() {
}
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;
if (targetEl && targetEl.matches(":focus")) {
setTimeout(function () {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,7 @@
"use client";
import getPublicCollectionData from "@/lib/client/getPublicCollectionData";
import {
AccountSettings,
CollectionIncludingMembersAndLinkCount,
Sort,
ViewMode,
@ -29,15 +30,9 @@ export default function PublicCollections() {
const router = useRouter();
const [collectionOwner, setCollectionOwner] = useState({
id: null as unknown as number,
name: "",
username: "",
image: "",
archiveAsScreenshot: undefined as unknown as boolean,
archiveAsMonolith: undefined as unknown as boolean,
archiveAsPDF: undefined as unknown as boolean,
});
const [collectionOwner, setCollectionOwner] = useState<
Partial<AccountSettings>
>({});
const [searchFilter, setSearchFilter] = useState({
name: true,
@ -93,7 +88,9 @@ export default function PublicCollections() {
(localStorage.getItem("viewMode") as ViewMode) || ViewMode.Card
);
return collection ? (
if (!collection) return <></>;
else
return (
<div
className="h-96"
style={{
@ -102,7 +99,7 @@ export default function PublicCollections() {
} 13rem, ${settings.theme === "dark" ? "#171717" : "#ffffff"} 100%)`,
}}
>
{collection ? (
{collection && (
<Head>
<title>{collection.name} | Linkwarden</title>
<meta
@ -111,7 +108,7 @@ export default function PublicCollections() {
key="title"
/>
</Head>
) : undefined}
)}
<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">
@ -140,12 +137,12 @@ export default function PublicCollections() {
className="flex items-center btn px-2 btn-ghost rounded-full"
onClick={() => setEditCollectionSharingModal(true)}
>
{collectionOwner.id ? (
{collectionOwner.id && (
<ProfilePhoto
src={collectionOwner.image || undefined}
name={collectionOwner.name}
/>
) : undefined}
)}
{collection.members
.sort((a, b) => (a.userId as number) - (b.userId as number))
.map((e, i) => {
@ -159,13 +156,13 @@ export default function PublicCollections() {
);
})
.slice(0, 3)}
{collection.members.length - 3 > 0 ? (
{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">
@ -230,22 +227,22 @@ export default function PublicCollections() {
placeholderCount={1}
useData={data}
/>
{!data.isLoading && links && !links[0] && <p>{t("nothing_found")}</p>}
{!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>
</p> */}
</div>
</div>
{editCollectionSharingModal ? (
{editCollectionSharingModal && (
<EditCollectionSharingModal
onClose={() => setEditCollectionSharingModal(false)}
activeCollection={collection}
/>
) : undefined}
)}
</div>
) : (
<></>
);
}

View File

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

View File

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

View File

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

View File

@ -83,7 +83,7 @@ export default function Delete() {
/>
</div>
{process.env.NEXT_PUBLIC_STRIPE ? (
{process.env.NEXT_PUBLIC_STRIPE && (
<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">
<b>{t("optional")}</b> <i>{t("feedback_help")}</i>
@ -123,7 +123,7 @@ export default function Delete() {
/>
</div>
</fieldset>
) : undefined}
)}
<Button
className="mx-auto"

View File

@ -117,6 +117,7 @@
"applying_settings": "Applying settings...",
"settings_applied": "Settings Applied!",
"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_format_error": "Invalid file format.",
"importing_bookmarks": "Importing bookmarks...",

View File

@ -117,6 +117,7 @@
"applying_settings": "Applicazione delle impostazioni in corso...",
"settings_applied": "Impostazioni Applicate!",
"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_format_error": "Formato file non valido.",
"importing_bookmarks": "Importazione dei segnalibri in corso...",

View File

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