This commit is contained in:
Isaac Wise 2024-07-27 17:41:13 -05:00
parent 4faf389a2b
commit 94be3a7448
No known key found for this signature in database
GPG Key ID: A02A33A7E2427136
34 changed files with 308 additions and 256 deletions

View File

@ -1,5 +1,8 @@
import Link from "next/link"; import Link from "next/link";
import { AccountSettings, 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";
@ -33,7 +36,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<Partial<AccountSettings>>({}); const [collectionOwner, setCollectionOwner] = useState<
Partial<AccountSettings>
>({});
useEffect(() => { useEffect(() => {
const fetchOwner = async () => { const fetchOwner = async () => {
@ -151,9 +156,11 @@ export default function CollectionCard({ collection, className }: Props) {
<Link <Link
href={`/collections/${collection.id}`} href={`/collections/${collection.id}`}
style={{ style={{
backgroundImage: `linear-gradient(45deg, ${collection.color}30 10%, ${settings.theme === "dark" ? "oklch(var(--b2))" : "oklch(var(--b2))" backgroundImage: `linear-gradient(45deg, ${collection.color}30 10%, ${
} 50%, ${settings.theme === "dark" ? "oklch(var(--b2))" : "oklch(var(--b2))" settings.theme === "dark" ? "oklch(var(--b2))" : "oklch(var(--b2))"
} 100%)`, } 50%, ${
settings.theme === "dark" ? "oklch(var(--b2))" : "oklch(var(--b2))"
} 100%)`,
}} }}
className="card card-compact shadow-md hover:shadow-none duration-200 border border-neutral-content" className="card card-compact shadow-md hover:shadow-none duration-200 border border-neutral-content"
> >

View File

@ -231,10 +231,11 @@ const renderItem = (
return ( return (
<div ref={provided.innerRef} {...provided.draggableProps} className="mb-1"> <div ref={provided.innerRef} {...provided.draggableProps} className="mb-1">
<div <div
className={`${currentPath === `/collections/${collection.id}` className={`${
? "bg-primary/20 is-active" currentPath === `/collections/${collection.id}`
: "hover:bg-neutral/20" ? "bg-primary/20 is-active"
} duration-100 flex gap-1 items-center pr-2 pl-1 rounded-md`} : "hover:bg-neutral/20"
} duration-100 flex gap-1 items-center pr-2 pl-1 rounded-md`}
> >
{Icon(item as ExtendedTreeItem, onExpand, onCollapse)} {Icon(item as ExtendedTreeItem, onExpand, onCollapse)}

View File

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

View File

@ -10,11 +10,11 @@ type Props = {
onChange: (newValue: unknown, actionMeta: ActionMeta<unknown>) => void; onChange: (newValue: unknown, actionMeta: ActionMeta<unknown>) => void;
showDefaultValue?: boolean; showDefaultValue?: boolean;
defaultValue?: defaultValue?:
| { | {
label: string; label: string;
value?: number; value?: number;
} }
| undefined; | undefined;
creatable?: boolean; creatable?: boolean;
}; };
@ -107,7 +107,7 @@ export default function CollectionSelection({
components={{ components={{
Option: customOption, Option: customOption,
}} }}
// menuPosition="fixed" // menuPosition="fixed"
/> />
); );
} else { } else {
@ -123,7 +123,7 @@ export default function CollectionSelection({
components={{ components={{
Option: customOption, Option: customOption,
}} }}
// menuPosition="fixed" // menuPosition="fixed"
/> />
); );
} }

View File

@ -79,8 +79,9 @@ export default function LinkActions({
return ( return (
<> <>
<div <div
className={`dropdown dropdown-left absolute ${position || "top-3 right-3" className={`dropdown dropdown-left absolute ${
} ${alignToTop ? "" : "dropdown-end"} z-20`} position || "top-3 right-3"
} ${alignToTop ? "" : "dropdown-end"} z-20`}
> >
<div <div
tabIndex={0} tabIndex={0}
@ -91,8 +92,9 @@ export default function LinkActions({
<i title="More" className="bi-three-dots text-xl" /> <i title="More" className="bi-three-dots text-xl" />
</div> </div>
<ul <ul
className={`dropdown-content z-[20] menu shadow bg-base-200 border border-neutral-content rounded-box w-44 mr-1 ${alignToTop ? "" : "translate-y-10" className={`dropdown-content z-[20] menu shadow bg-base-200 border border-neutral-content rounded-box w-44 mr-1 ${
}`} alignToTop ? "" : "translate-y-10"
}`}
> >
<li> <li>
<div <div
@ -122,20 +124,21 @@ export default function LinkActions({
</div> </div>
</li> </li>
)} )}
{permissions === true || permissions?.canUpdate && ( {permissions === true ||
<li> (permissions?.canUpdate && (
<div <li>
role="button" <div
tabIndex={0} role="button"
onClick={() => { tabIndex={0}
(document?.activeElement as HTMLElement)?.blur(); onClick={() => {
setEditLinkModal(true); (document?.activeElement as HTMLElement)?.blur();
}} setEditLinkModal(true);
> }}
{t("edit_link")} >
</div> {t("edit_link")}
</li> </div>
)} </li>
))}
{link.type === "url" && ( {link.type === "url" && (
<li> <li>
<div <div
@ -150,20 +153,21 @@ export default function LinkActions({
</div> </div>
</li> </li>
)} )}
{permissions === true || permissions?.canDelete && ( {permissions === true ||
<li> (permissions?.canDelete && (
<div <li>
role="button" <div
tabIndex={0} role="button"
onClick={(e) => { tabIndex={0}
(document?.activeElement as HTMLElement)?.blur(); onClick={(e) => {
e.shiftKey ? deleteLink() : setDeleteLinkModal(true); (document?.activeElement as HTMLElement)?.blur();
}} e.shiftKey ? deleteLink() : setDeleteLinkModal(true);
> }}
{t("delete")} >
</div> {t("delete")}
</li> </div>
)} </li>
))}
</ul> </ul>
</div> </div>

View File

@ -91,8 +91,9 @@ export default function LinkCardCompact({
return ( return (
<> <>
<div <div
className={`${selectedStyle} border relative items-center flex ${!showInfo && !isPWA() ? "hover:bg-base-300 p-3" : "py-3" className={`${selectedStyle} border relative items-center flex ${
} duration-200 rounded-lg w-full`} !showInfo && !isPWA() ? "hover:bg-base-300 p-3" : "py-3"
} duration-200 rounded-lg w-full`}
onClick={() => onClick={() =>
selectable selectable
? handleCheckboxClick(link) ? handleCheckboxClick(link)
@ -152,8 +153,8 @@ export default function LinkCardCompact({
collection={collection} collection={collection}
position="top-3 right-3" position="top-3 right-3"
flipDropdown={flipDropdown} flipDropdown={flipDropdown}
// toggleShowInfo={() => setShowInfo(!showInfo)} // toggleShowInfo={() => setShowInfo(!showInfo)}
// linkInfo={showInfo} // linkInfo={showInfo}
/> />
</div> </div>
<div <div

View File

@ -9,7 +9,7 @@ import { useTranslation } from "next-i18next";
type Props = {}; type Props = {};
export default function MobileNavigation({ }: Props) { export default function MobileNavigation({}: Props) {
const { t } = useTranslation(); const { t } = useTranslation();
const [newLinkModal, setNewLinkModal] = useState(false); const [newLinkModal, setNewLinkModal] = useState(false);
const [newCollectionModal, setNewCollectionModal] = useState(false); const [newCollectionModal, setNewCollectionModal] = useState(false);
@ -21,8 +21,9 @@ export default function MobileNavigation({ }: Props) {
className={`fixed bottom-0 left-0 right-0 z-30 duration-200 sm:hidden`} className={`fixed bottom-0 left-0 right-0 z-30 duration-200 sm:hidden`}
> >
<div <div
className={`w-full flex bg-base-100 ${isIphone() && isPWA() ? "pb-5" : "" className={`w-full flex bg-base-100 ${
} border-solid border-t-neutral-content border-t`} isIphone() && isPWA() ? "pb-5" : ""
} border-solid border-t-neutral-content border-t`}
> >
<MobileNavigationButton href={`/dashboard`} icon={"bi-house"} /> <MobileNavigationButton href={`/dashboard`} icon={"bi-house"} />
<MobileNavigationButton <MobileNavigationButton
@ -83,9 +84,7 @@ 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 && ( {newCollectionModal && (
<NewCollectionModal onClose={() => setNewCollectionModal(false)} /> <NewCollectionModal onClose={() => setNewCollectionModal(false)} />
)} )}

View File

@ -2,7 +2,11 @@ import React, { useEffect, useState } from "react";
import TextInput from "@/components/TextInput"; import TextInput from "@/components/TextInput";
import useCollectionStore from "@/store/collections"; import useCollectionStore from "@/store/collections";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
import { AccountSettings, 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 useAccountStore from "@/store/account"; import useAccountStore from "@/store/account";
import usePermissions from "@/hooks/usePermissions"; import usePermissions from "@/hooks/usePermissions";
@ -62,7 +66,9 @@ export default function EditCollectionSharingModal({
const [memberUsername, setMemberUsername] = useState(""); const [memberUsername, setMemberUsername] = useState("");
const [collectionOwner, setCollectionOwner] = useState<Partial<AccountSettings>>({}); const [collectionOwner, setCollectionOwner] = useState<
Partial<AccountSettings>
>({});
useEffect(() => { useEffect(() => {
const fetchOwner = async () => { const fetchOwner = async () => {

View File

@ -38,7 +38,9 @@ export default function PreservedFormatsModal({ onClose, activeLink }: Props) {
let isPublic = router.pathname.startsWith("/public") ? true : undefined; let isPublic = router.pathname.startsWith("/public") ? true : undefined;
const [collectionOwner, setCollectionOwner] = useState<Partial<AccountSettings>>({}); const [collectionOwner, setCollectionOwner] = useState<
Partial<AccountSettings>
>({});
useEffect(() => { useEffect(() => {
const fetchOwner = async () => { const fetchOwner = async () => {
@ -143,9 +145,9 @@ export default function PreservedFormatsModal({ onClose, activeLink }: Props) {
<p className="text-xl font-thin">{t("preserved_formats")}</p> <p className="text-xl font-thin">{t("preserved_formats")}</p>
<div className="divider mb-2 mt-1"></div> <div className="divider mb-2 mt-1"></div>
{screenshotAvailable(link) || {screenshotAvailable(link) ||
pdfAvailable(link) || pdfAvailable(link) ||
readabilityAvailable(link) || readabilityAvailable(link) ||
monolithAvailable(link) ? ( monolithAvailable(link) ? (
<p className="mb-3">{t("available_formats")}</p> <p className="mb-3">{t("available_formats")}</p>
) : ( ) : (
"" ""
@ -206,21 +208,25 @@ export default function PreservedFormatsModal({ onClose, activeLink }: 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>
<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 ${isReady() ? "sm:mt " : "" className={`flex flex-col sm:flex-row gap-3 items-center justify-center ${
}`} isReady() ? "sm:mt " : ""
}`}
> >
<Link <Link
href={`https://web.archive.org/web/${link?.url?.replace( href={`https://web.archive.org/web/${link?.url?.replace(

View File

@ -120,9 +120,7 @@ export default function Navbar() {
</ClickAwayHandler> </ClickAwayHandler>
</div> </div>
)} )}
{newLinkModal && ( {newLinkModal && <NewLinkModal onClose={() => setNewLinkModal(false)} />}
<NewLinkModal onClose={() => setNewLinkModal(false)} />
)}
{newCollectionModal && ( {newCollectionModal && (
<NewCollectionModal onClose={() => setNewCollectionModal(false)} /> <NewCollectionModal onClose={() => setNewCollectionModal(false)} />
)} )}

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)} />
)}
</div> </div>
); );
} }

View File

@ -90,18 +90,20 @@ export default function PreservedFormatRow({
</div> </div>
<div className="flex gap-1"> <div className="flex gap-1">
{downloadable || false && ( {downloadable ||
<div (false && (
onClick={() => handleDownload()} <div
className="btn btn-sm btn-square" onClick={() => handleDownload()}
> className="btn btn-sm btn-square"
<i className="bi-cloud-arrow-down text-xl text-neutral" /> >
</div> <i className="bi-cloud-arrow-down text-xl text-neutral" />
)} </div>
))}
<Link <Link
href={`${isPublic ? "/public" : "" href={`${
}/preserved/${link?.id}?format=${format}`} isPublic ? "/public" : ""
}/preserved/${link?.id}?format=${format}`}
target="_blank" target="_blank"
className="btn btn-sm btn-square" className="btn btn-sm btn-square"
> >

View File

@ -32,8 +32,9 @@ export default function ProfileDropdown() {
/> />
</div> </div>
<ul <ul
className={`dropdown-content z-[1] menu shadow bg-base-200 border border-neutral-content rounded-box ${isAdmin ? "w-48" : "w-40" className={`dropdown-content z-[1] menu shadow bg-base-200 border border-neutral-content rounded-box ${
} mt-1`} isAdmin ? "w-48" : "w-40"
} mt-1`}
> >
<li> <li>
<Link <Link

View File

@ -192,7 +192,8 @@ export default function ReadableView({ link }: Props) {
> >
<i className="bi-link-45deg"></i> <i className="bi-link-45deg"></i>
{isValidUrl(link?.url || "") && new URL(link?.url as string).host} {isValidUrl(link?.url || "") &&
new URL(link?.url as string).host}
</Link> </Link>
)} )}
</div> </div>
@ -229,10 +230,10 @@ export default function ReadableView({ link }: Props) {
<p className="min-w-fit text-sm text-neutral"> <p className="min-w-fit text-sm text-neutral">
{date {date
? new Date(date).toLocaleString("en-US", { ? new Date(date).toLocaleString("en-US", {
year: "numeric", year: "numeric",
month: "long", month: "long",
day: "numeric", day: "numeric",
}) })
: undefined} : undefined}
</p> </p>
@ -257,8 +258,9 @@ export default function ReadableView({ link }: Props) {
></div> ></div>
) : ( ) : (
<div <div
className={`w-full h-full flex flex-col justify-center p-10 ${link?.readable === "pending" || !link?.readable ? "skeleton" : "" className={`w-full h-full flex flex-col justify-center p-10 ${
}`} link?.readable === "pending" || !link?.readable ? "skeleton" : ""
}`}
> >
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"

View File

@ -10,7 +10,9 @@ export default function ToggleDarkMode({ className }: Props) {
const { t } = useTranslation(); const { t } = useTranslation();
const { settings, updateSettings } = useLocalSettingsStore(); const { settings, updateSettings } = useLocalSettingsStore();
const [theme, setTheme] = useState<string | null>(localStorage.getItem("theme")); const [theme, setTheme] = useState<string | null>(
localStorage.getItem("theme")
);
const handleToggle = (e: ChangeEvent<HTMLInputElement>) => { const handleToggle = (e: ChangeEvent<HTMLInputElement>) => {
setTheme(e.target.checked ? "dark" : "light"); setTheme(e.target.checked ? "dark" : "light");

View File

@ -25,8 +25,9 @@ export default function CenteredForm({
<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_${settings.theme === "dark" ? "dark" : "light" src={`/linkwarden_${
}.png`} settings.theme === "dark" ? "dark" : "light"
}.png`}
width={640} width={640}
height={136} height={136}
alt="Linkwarden" alt="Linkwarden"

View File

@ -3,15 +3,15 @@ import { LinkRequestQuery, Order, Sort } from "@/types/global";
type Response<D> = type Response<D> =
| { | {
data: D; data: D;
message: string; message: string;
status: number; status: number;
} }
| { | {
data: D; data: D;
message: string; message: string;
status: number; status: number;
}; };
export default async function getDashboardData( export default async function getDashboardData(
userId: number, userId: number,

View File

@ -26,7 +26,7 @@ export default async function exportData(userId: number) {
if (Array.isArray(data)) { if (Array.isArray(data)) {
data.forEach((item) => redactIds(item)); data.forEach((item) => redactIds(item));
} else if (data !== null && typeof data === "object") { } else if (data !== null && typeof data === "object") {
const fieldsToRedact = ['id', 'parentId', 'collectionId', 'ownerId']; const fieldsToRedact = ["id", "parentId", "collectionId", "ownerId"];
fieldsToRedact.forEach((field) => { fieldsToRedact.forEach((field) => {
if (field in data) { if (field in data) {
@ -37,7 +37,10 @@ export default async function exportData(userId: number) {
// Recursively call redactIds for each property that is an object or an array // Recursively call redactIds for each property that is an object or an array
Object.keys(data).forEach((key) => { Object.keys(data).forEach((key) => {
const value = (data as any)[key]; const value = (data as any)[key];
if (value !== null && (typeof value === "object" || Array.isArray(value))) { if (
value !== null &&
(typeof value === "object" || Array.isArray(value))
) {
redactIds(value); redactIds(value);
} }
}); });

View File

@ -63,7 +63,8 @@ async function processBookmarks(
) as Element; ) as Element;
if (collectionName) { if (collectionName) {
const collectionNameContent = (collectionName.children[0] as TextNode)?.content; const collectionNameContent = (collectionName.children[0] as TextNode)
?.content;
if (collectionNameContent) { if (collectionNameContent) {
collectionId = await createCollection( collectionId = await createCollection(
userId, userId,
@ -274,4 +275,3 @@ function processNodes(nodes: Node[]) {
nodes.forEach(findAndProcessDL); nodes.forEach(findAndProcessDL);
return nodes; return nodes;
} }

View File

@ -84,23 +84,23 @@ export default async function importFromWallabag(
tags: tags:
link.tags && link.tags[0] link.tags && link.tags[0]
? { ? {
connectOrCreate: link.tags.map((tag) => ({ connectOrCreate: link.tags.map((tag) => ({
where: { where: {
name_ownerId: { name_ownerId: {
name: tag.trim(), name: tag.trim(),
ownerId: userId, ownerId: userId,
},
},
create: {
name: tag.trim(),
owner: {
connect: {
id: userId,
}, },
}, },
}, create: {
})), name: tag.trim(),
} owner: {
connect: {
id: userId,
},
},
},
})),
}
: undefined, : undefined,
}, },
}); });

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
);
if (!isPasswordValid && !isServerAdmin) { if (!isPasswordValid && !isServerAdmin) {
return { return {

View File

@ -28,10 +28,10 @@ export default async function readFile(filePath: string) {
try { try {
let returnObject: let returnObject:
| { | {
file: Buffer | string; file: Buffer | string;
contentType: ReturnContentTypes; contentType: ReturnContentTypes;
status: number; status: number;
} }
| undefined; | undefined;
const headObjectAsync = util.promisify( const headObjectAsync = util.promisify(

View File

@ -7,10 +7,15 @@ export function isPWA() {
} }
export function isIphone() { export function isIphone() {
return /iPhone/.test(navigator.userAgent) && !(window as unknown as { MSStream?: any }).MSStream; return (
/iPhone/.test(navigator.userAgent) &&
!(window as unknown as { MSStream?: any }).MSStream
);
} }
export function dropdownTriggerer(e: React.FocusEvent<HTMLElement> | React.MouseEvent<HTMLElement>) { 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: LinkIncludingShortenedCollectionAndTags) { export function previewAvailable(
link: LinkIncludingShortenedCollectionAndTags
) {
return ( return (
link && link &&
link.preview && link.preview &&

View File

@ -2,7 +2,7 @@
module.exports = { module.exports = {
i18n: { i18n: {
defaultLocale: "en", defaultLocale: "en",
locales: ["en","it"], locales: ["en", "it"],
}, },
reloadOnPrerender: process.env.NODE_ENV === "development", reloadOnPrerender: process.env.NODE_ENV === "development",
}; };

View File

@ -104,9 +104,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)} />
)}
</div> </div>
); );
} }

View File

@ -189,8 +189,12 @@ export default async function Index(req: NextApiRequest, res: NextApiResponse) {
where: { id: linkId }, where: { id: linkId },
data: { data: {
preview: isPDF ? "unavailable" : undefined, preview: isPDF ? "unavailable" : undefined,
image: isImage ? `archives/${collectionPermissions.id}/${linkId + suffix}` : null, image: isImage
pdf: isPDF ? `archives/${collectionPermissions.id}/${linkId + suffix}` : null, ? `archives/${collectionPermissions.id}/${linkId + suffix}`
: null,
pdf: isPDF
? `archives/${collectionPermissions.id}/${linkId + suffix}`
: null,
lastPreserved: new Date().toISOString(), lastPreserved: new Date().toISOString(),
}, },
}); });

View File

@ -54,7 +54,9 @@ export default function Index() {
const { account } = useAccountStore(); const { account } = useAccountStore();
const [collectionOwner, setCollectionOwner] = useState<Partial<AccountSettings>>({}); const [collectionOwner, setCollectionOwner] = useState<
Partial<AccountSettings>
>({});
useEffect(() => { useEffect(() => {
const fetchOwner = async () => { const fetchOwner = async () => {
@ -108,8 +110,9 @@ export default function Index() {
<div <div
className="h-[60rem] p-5 flex gap-3 flex-col" className="h-[60rem] p-5 flex gap-3 flex-col"
style={{ style={{
backgroundImage: `linear-gradient(${activeCollection?.color}20 10%, ${settings.theme === "dark" ? "#262626" : "#f3f4f6" backgroundImage: `linear-gradient(${activeCollection?.color}20 10%, ${
} 13rem, ${settings.theme === "dark" ? "#171717" : "#ffffff"} 100%)`, settings.theme === "dark" ? "#262626" : "#f3f4f6"
} 13rem, ${settings.theme === "dark" ? "#171717" : "#ffffff"} 100%)`,
}} }}
> >
{activeCollection && ( {activeCollection && (
@ -210,7 +213,7 @@ export default function Index() {
/> />
)} )}
{activeCollection.members {activeCollection.members
.sort((a, b) => (a.userId) - (b.userId)) .sort((a, b) => a.userId - b.userId)
.map((e, i) => { .map((e, i) => {
return ( return (
<ProfilePhoto <ProfilePhoto
@ -233,20 +236,20 @@ export default function Index() {
<p className="text-neutral text-sm"> <p className="text-neutral text-sm">
{activeCollection.members.length > 0 && {activeCollection.members.length > 0 &&
activeCollection.members.length === 1 activeCollection.members.length === 1
? t("by_author_and_other", { ? t("by_author_and_other", {
author: collectionOwner.name,
count: activeCollection.members.length,
})
: activeCollection.members.length > 0 &&
activeCollection.members.length !== 1
? t("by_author_and_others", {
author: collectionOwner.name, author: collectionOwner.name,
count: activeCollection.members.length, count: activeCollection.members.length,
}) })
: activeCollection.members.length > 0 &&
activeCollection.members.length !== 1
? t("by_author_and_others", {
author: collectionOwner.name,
count: activeCollection.members.length,
})
: t("by_author", { : t("by_author", {
author: collectionOwner.name, author: collectionOwner.name,
})} })}
</p> </p>
</div> </div>
</div> </div>
@ -291,15 +294,15 @@ export default function Index() {
setSortBy={setSortBy} setSortBy={setSortBy}
editMode={ editMode={
permissions === true || permissions === true ||
permissions?.canUpdate || permissions?.canUpdate ||
permissions?.canDelete permissions?.canDelete
? editMode ? editMode
: undefined : undefined
} }
setEditMode={ setEditMode={
permissions === true || permissions === true ||
permissions?.canUpdate || permissions?.canUpdate ||
permissions?.canDelete permissions?.canDelete
? setEditMode ? setEditMode
: undefined : undefined
} }
@ -307,11 +310,11 @@ export default function Index() {
<p> <p>
{activeCollection?._count?.links === 1 {activeCollection?._count?.links === 1
? t("showing_count_result", { ? t("showing_count_result", {
count: activeCollection?._count?.links, count: activeCollection?._count?.links,
}) })
: t("showing_count_results", { : t("showing_count_results", {
count: activeCollection?._count?.links, count: activeCollection?._count?.links,
})} })}
</p> </p>
</LinkListOptions> </LinkListOptions>

View File

@ -60,7 +60,10 @@ export default function Dashboard() {
handleNumberOfLinksToShow(); handleNumberOfLinksToShow();
}, [width]); }, [width]);
const importBookmarks = async (e: React.ChangeEvent<HTMLInputElement>, format: MigrationFormat) => { const importBookmarks = async (
e: React.ChangeEvent<HTMLInputElement>,
format: MigrationFormat
) => {
const file: File | null = e.target.files && e.target.files[0]; const file: File | null = e.target.files && e.target.files[0];
if (file) { if (file) {
@ -324,9 +327,7 @@ export default function Dashboard() {
)} )}
</div> </div>
</div> </div>
{newLinkModal && ( {newLinkModal && <NewLinkModal onClose={() => setNewLinkModal(false)} />}
<NewLinkModal onClose={() => setNewLinkModal(false)} />
)}
</MainLayout> </MainLayout>
); );
} }

View File

@ -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>
)} ))}
{value.name} {value.name}
</Button> </Button>
</React.Fragment> </React.Fragment>

View File

@ -37,7 +37,9 @@ export default function PublicCollections() {
const router = useRouter(); const router = useRouter();
const [collectionOwner, setCollectionOwner] = useState<Partial<AccountSettings>>({}); const [collectionOwner, setCollectionOwner] = useState<
Partial<AccountSettings>
>({});
const [searchFilter, setSearchFilter] = useState({ const [searchFilter, setSearchFilter] = useState({
name: true, name: true,
@ -103,8 +105,9 @@ export default function PublicCollections() {
<div <div
className="h-96" className="h-96"
style={{ style={{
backgroundImage: `linear-gradient(${collection?.color}30 10%, ${settings.theme === "dark" ? "#262626" : "#f3f4f6" backgroundImage: `linear-gradient(${collection?.color}30 10%, ${
} 13rem, ${settings.theme === "dark" ? "#171717" : "#ffffff"} 100%)`, settings.theme === "dark" ? "#262626" : "#f3f4f6"
} 13rem, ${settings.theme === "dark" ? "#171717" : "#ffffff"} 100%)`,
}} }}
> >
{collection && ( {collection && (
@ -175,20 +178,20 @@ export default function PublicCollections() {
<p className="text-neutral text-sm"> <p className="text-neutral text-sm">
{collection.members.length > 0 && {collection.members.length > 0 &&
collection.members.length === 1 collection.members.length === 1
? t("by_author_and_other", { ? t("by_author_and_other", {
author: collectionOwner.name,
count: collection.members.length,
})
: collection.members.length > 0 &&
collection.members.length !== 1
? t("by_author_and_others", {
author: collectionOwner.name, author: collectionOwner.name,
count: collection.members.length, count: collection.members.length,
}) })
: collection.members.length > 0 &&
collection.members.length !== 1
? t("by_author_and_others", {
author: collectionOwner.name,
count: collection.members.length,
})
: t("by_author", { : t("by_author", {
author: collectionOwner.name, author: collectionOwner.name,
})} })}
</p> </p>
</div> </div>
</div> </div>
@ -212,11 +215,11 @@ export default function PublicCollections() {
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> </LinkListOptions>

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>
)} ))}
{value.name} {value.name}
</Button> </Button>
</React.Fragment> </React.Fragment>
@ -149,8 +149,8 @@ export default function Register({
text={ text={
process.env.NEXT_PUBLIC_STRIPE process.env.NEXT_PUBLIC_STRIPE
? t("trial_offer_desc", { ? t("trial_offer_desc", {
count: Number(process.env.NEXT_PUBLIC_TRIAL_PERIOD_DAYS || 14), count: Number(process.env.NEXT_PUBLIC_TRIAL_PERIOD_DAYS || 14),
}) })
: t("register_desc") : t("register_desc")
} }
data-testid="registration-form" data-testid="registration-form"

View File

@ -29,19 +29,19 @@ export default function Account() {
!objectIsEmpty(account) !objectIsEmpty(account)
? account ? account
: ({ : ({
// @ts-ignore // @ts-ignore
id: null, id: null,
name: "", name: "",
username: "", username: "",
email: "", email: "",
emailVerified: null, emailVerified: null,
password: undefined, password: undefined,
image: "", image: "",
isPrivate: true, isPrivate: true,
// @ts-ignore // @ts-ignore
createdAt: null, createdAt: null,
whitelistedUsers: [], whitelistedUsers: [],
} as unknown as AccountSettings) } as unknown as AccountSettings)
); );
const { t } = useTranslation(); const { t } = useTranslation();
@ -102,7 +102,10 @@ export default function Account() {
setSubmitLoader(false); setSubmitLoader(false);
}; };
const importBookmarks = async (e: ChangeEvent<HTMLInputElement>, format: MigrationFormat) => { const importBookmarks = async (
e: ChangeEvent<HTMLInputElement>,
format: MigrationFormat
) => {
setSubmitLoader(true); setSubmitLoader(true);
const file = e.target.files?.[0]; const file = e.target.files?.[0];
@ -421,7 +424,8 @@ export default function Account() {
<p> <p>
{t("delete_account_warning")} {t("delete_account_warning")}
{process.env.NEXT_PUBLIC_STRIPE && " " + t("cancel_subscription_notice")} {process.env.NEXT_PUBLIC_STRIPE &&
" " + t("cancel_subscription_notice")}
</p> </p>
</div> </div>

View File

@ -81,7 +81,7 @@ export enum Sort {
DescriptionZA, DescriptionZA,
} }
export type Order = { [key: string]: 'asc' | 'desc' }; export type Order = { [key: string]: "asc" | "desc" };
export type LinkRequestQuery = { export type LinkRequestQuery = {
sort: Sort; sort: Sort;