commit
c6e3147bb6
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -60,47 +60,49 @@ export default function Dropdown({
|
|||
}
|
||||
}, [points, dropdownHeight]);
|
||||
|
||||
return !points || pos ? (
|
||||
<ClickAwayHandler
|
||||
onMount={(e) => {
|
||||
setDropdownHeight(e.height);
|
||||
setDropdownWidth(e.width);
|
||||
}}
|
||||
style={
|
||||
points
|
||||
? {
|
||||
position: "fixed",
|
||||
top: `${pos?.y}px`,
|
||||
left: `${pos?.x}px`,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
onClickOutside={onClickOutside}
|
||||
className={`${
|
||||
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 && (
|
||||
<div className="cursor-pointer rounded-md">
|
||||
<div className="flex items-center gap-2 py-1 px-2 hover:bg-base-100 duration-100">
|
||||
<p className="select-none">{e.name}</p>
|
||||
return (
|
||||
(!points || pos) && (
|
||||
<ClickAwayHandler
|
||||
onMount={(e) => {
|
||||
setDropdownHeight(e.height);
|
||||
setDropdownWidth(e.width);
|
||||
}}
|
||||
style={
|
||||
points
|
||||
? {
|
||||
position: "fixed",
|
||||
top: `${pos?.y}px`,
|
||||
left: `${pos?.x}px`,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
onClickOutside={onClickOutside}
|
||||
className={`${
|
||||
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 && (
|
||||
<div className="cursor-pointer rounded-md">
|
||||
<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>
|
||||
);
|
||||
);
|
||||
|
||||
return e && e.href ? (
|
||||
<Link key={i} href={e.href}>
|
||||
{inner}
|
||||
</Link>
|
||||
) : (
|
||||
e && (
|
||||
<div key={i} onClick={e.onClick}>
|
||||
return e && e.href ? (
|
||||
<Link key={i} href={e.href}>
|
||||
{inner}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
})}
|
||||
</ClickAwayHandler>
|
||||
) : null;
|
||||
</Link>
|
||||
) : (
|
||||
e && (
|
||||
<div key={i} onClick={e.onClick}>
|
||||
{inner}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
})}
|
||||
</ClickAwayHandler>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -34,7 +34,6 @@ export default function TagSelection({ onChange, defaultValue }: Props) {
|
|||
options={options}
|
||||
styles={styles}
|
||||
defaultValue={defaultValue}
|
||||
// menuPosition="fixed"
|
||||
isMulti
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -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}
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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 &&
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 &&
|
||||
|
|
|
@ -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}
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>}
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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,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-lg">{t("check_back_later")}</p>
|
||||
</div>
|
||||
) : !isReady() && atLeastOneFormatAvailable() ? (
|
||||
<div className={`w-full h-full flex flex-col justify-center p-5`}>
|
||||
<BeatLoader
|
||||
color="oklch(var(--p))"
|
||||
className="mx-auto mb-3"
|
||||
size={20}
|
||||
/>
|
||||
<p className="text-center">{t("there_are_more_formats")}</p>
|
||||
<p className="text-center text-sm">{t("check_back_later")}</p>
|
||||
</div>
|
||||
) : undefined}
|
||||
) : (
|
||||
!isReady() &&
|
||||
atLeastOneFormatAvailable() && (
|
||||
<div className={`w-full h-full flex flex-col justify-center p-5`}>
|
||||
<BeatLoader
|
||||
color="oklch(var(--p))"
|
||||
className="mx-auto mb-3"
|
||||
size={20}
|
||||
/>
|
||||
<p className="text-center">{t("there_are_more_formats")}</p>
|
||||
<p className="text-center text-sm">{t("check_back_later")}</p>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
|
||||
<div
|
||||
className={`flex flex-col sm:flex-row gap-3 items-center justify-center ${
|
||||
|
|
|
@ -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)}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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={() => {
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -2,19 +2,17 @@ import axios, { AxiosError } from "axios"
|
|||
|
||||
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 {
|
||||
return await axios.post("/api/v1/users", {
|
||||
username: username || "test",
|
||||
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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
|
|
|
@ -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" };
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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" };
|
||||
|
|
|
@ -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 };
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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" };
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 () {
|
||||
|
|
|
@ -39,7 +39,9 @@ export function monolithAvailable(
|
|||
);
|
||||
}
|
||||
|
||||
export function previewAvailable(link: any) {
|
||||
export function previewAvailable(
|
||||
link: LinkIncludingShortenedCollectionAndTags
|
||||
) {
|
||||
return (
|
||||
link &&
|
||||
link.preview &&
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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" ? (
|
||||
<i className={"bi-" + value.name.toLowerCase()}></i>
|
||||
) : undefined}
|
||||
(value.name.toLowerCase() === "apple" && (
|
||||
<i className={"bi-" + value.name.toLowerCase()}></i>
|
||||
))}
|
||||
{value.name}
|
||||
</Button>
|
||||
</React.Fragment>
|
||||
|
|
|
@ -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,160 +88,162 @@ export default function PublicCollections() {
|
|||
(localStorage.getItem("viewMode") as ViewMode) || ViewMode.Card
|
||||
);
|
||||
|
||||
return collection ? (
|
||||
<div
|
||||
className="h-96"
|
||||
style={{
|
||||
backgroundImage: `linear-gradient(${collection?.color}30 10%, ${
|
||||
settings.theme === "dark" ? "#262626" : "#f3f4f6"
|
||||
} 13rem, ${settings.theme === "dark" ? "#171717" : "#ffffff"} 100%)`,
|
||||
}}
|
||||
>
|
||||
{collection ? (
|
||||
<Head>
|
||||
<title>{collection.name} | Linkwarden</title>
|
||||
<meta
|
||||
property="og:title"
|
||||
content={`${collection.name} | Linkwarden`}
|
||||
key="title"
|
||||
/>
|
||||
</Head>
|
||||
) : 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">
|
||||
{collection.name}
|
||||
</p>
|
||||
<div className="flex gap-2 items-center mt-8 min-w-fit">
|
||||
<ToggleDarkMode />
|
||||
if (!collection) return <></>;
|
||||
else
|
||||
return (
|
||||
<div
|
||||
className="h-96"
|
||||
style={{
|
||||
backgroundImage: `linear-gradient(${collection?.color}30 10%, ${
|
||||
settings.theme === "dark" ? "#262626" : "#f3f4f6"
|
||||
} 13rem, ${settings.theme === "dark" ? "#171717" : "#ffffff"} 100%)`,
|
||||
}}
|
||||
>
|
||||
{collection && (
|
||||
<Head>
|
||||
<title>{collection.name} | Linkwarden</title>
|
||||
<meta
|
||||
property="og:title"
|
||||
content={`${collection.name} | Linkwarden`}
|
||||
key="title"
|
||||
/>
|
||||
</Head>
|
||||
)}
|
||||
<div className="lg:w-3/4 w-full mx-auto p-5 bg">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-4xl font-thin mb-2 capitalize mt-10">
|
||||
{collection.name}
|
||||
</p>
|
||||
<div className="flex gap-2 items-center mt-8 min-w-fit">
|
||||
<ToggleDarkMode />
|
||||
|
||||
<Link href="https://linkwarden.app/" target="_blank">
|
||||
<Image
|
||||
src={`/icon.png`}
|
||||
width={551}
|
||||
height={551}
|
||||
alt="Linkwarden"
|
||||
title={t("list_created_with_linkwarden")}
|
||||
className="h-8 w-fit mx-auto rounded"
|
||||
/>
|
||||
</Link>
|
||||
<Link href="https://linkwarden.app/" target="_blank">
|
||||
<Image
|
||||
src={`/icon.png`}
|
||||
width={551}
|
||||
height={551}
|
||||
alt="Linkwarden"
|
||||
title={t("list_created_with_linkwarden")}
|
||||
className="h-8 w-fit mx-auto rounded"
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-3">
|
||||
<div className={`min-w-[15rem]`}>
|
||||
<div className="flex gap-1 justify-center sm:justify-end items-center w-fit">
|
||||
<div
|
||||
className="flex items-center btn px-2 btn-ghost rounded-full"
|
||||
onClick={() => setEditCollectionSharingModal(true)}
|
||||
>
|
||||
{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) => {
|
||||
return (
|
||||
<ProfilePhoto
|
||||
key={i}
|
||||
src={e.user.image ? e.user.image : undefined}
|
||||
className="-ml-3"
|
||||
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,
|
||||
<div className="mt-3">
|
||||
<div className={`min-w-[15rem]`}>
|
||||
<div className="flex gap-1 justify-center sm:justify-end items-center w-fit">
|
||||
<div
|
||||
className="flex items-center btn px-2 btn-ghost rounded-full"
|
||||
onClick={() => setEditCollectionSharingModal(true)}
|
||||
>
|
||||
{collectionOwner.id && (
|
||||
<ProfilePhoto
|
||||
src={collectionOwner.image || undefined}
|
||||
name={collectionOwner.name}
|
||||
/>
|
||||
)}
|
||||
{collection.members
|
||||
.sort((a, b) => (a.userId as number) - (b.userId as number))
|
||||
.map((e, i) => {
|
||||
return (
|
||||
<ProfilePhoto
|
||||
key={i}
|
||||
src={e.user.image ? e.user.image : undefined}
|
||||
className="-ml-3"
|
||||
name={e.user.name}
|
||||
/>
|
||||
);
|
||||
})
|
||||
: collection.members.length > 0 &&
|
||||
collection.members.length !== 1
|
||||
? t("by_author_and_others", {
|
||||
.slice(0, 3)}
|
||||
{collection.members.length - 3 > 0 && (
|
||||
<div className={`avatar drop-shadow-md placeholder -ml-3`}>
|
||||
<div className="bg-base-100 text-neutral rounded-full w-8 h-8 ring-2 ring-neutral-content">
|
||||
<span>+{collection.members.length - 3}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<p className="text-neutral text-sm">
|
||||
{collection.members.length > 0 &&
|
||||
collection.members.length === 1
|
||||
? t("by_author_and_other", {
|
||||
author: collectionOwner.name,
|
||||
count: collection.members.length,
|
||||
})
|
||||
: t("by_author", {
|
||||
author: collectionOwner.name,
|
||||
})}
|
||||
</p>
|
||||
: collection.members.length > 0 &&
|
||||
collection.members.length !== 1
|
||||
? t("by_author_and_others", {
|
||||
author: collectionOwner.name,
|
||||
count: collection.members.length,
|
||||
})
|
||||
: t("by_author", {
|
||||
author: collectionOwner.name,
|
||||
})}
|
||||
</p>
|
||||
</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">
|
||||
<LinkListOptions
|
||||
t={t}
|
||||
viewMode={viewMode}
|
||||
setViewMode={setViewMode}
|
||||
sortBy={sortBy}
|
||||
setSortBy={setSortBy}
|
||||
searchFilter={searchFilter}
|
||||
setSearchFilter={setSearchFilter}
|
||||
>
|
||||
<SearchBar
|
||||
placeholder={
|
||||
collection._count?.links === 1
|
||||
? t("search_count_link", {
|
||||
count: collection._count?.links,
|
||||
})
|
||||
: t("search_count_links", {
|
||||
count: collection._count?.links,
|
||||
})
|
||||
<div className="flex mb-5 mt-10 flex-col gap-5">
|
||||
<LinkListOptions
|
||||
t={t}
|
||||
viewMode={viewMode}
|
||||
setViewMode={setViewMode}
|
||||
sortBy={sortBy}
|
||||
setSortBy={setSortBy}
|
||||
searchFilter={searchFilter}
|
||||
setSearchFilter={setSearchFilter}
|
||||
>
|
||||
<SearchBar
|
||||
placeholder={
|
||||
collection._count?.links === 1
|
||||
? t("search_count_link", {
|
||||
count: collection._count?.links,
|
||||
})
|
||||
: t("search_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
|
||||
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">
|
||||
{/* <p className="text-center text-neutral">
|
||||
List created with <span className="text-black">Linkwarden.</span>
|
||||
</p> */}
|
||||
</div>
|
||||
</div>
|
||||
{editCollectionSharingModal && (
|
||||
<EditCollectionSharingModal
|
||||
onClose={() => setEditCollectionSharingModal(false)}
|
||||
activeCollection={collection}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{editCollectionSharingModal ? (
|
||||
<EditCollectionSharingModal
|
||||
onClose={() => setEditCollectionSharingModal(false)}
|
||||
activeCollection={collection}
|
||||
/>
|
||||
) : undefined}
|
||||
</div>
|
||||
) : (
|
||||
<></>
|
||||
);
|
||||
);
|
||||
}
|
||||
|
||||
export { getServerSideProps };
|
||||
|
|
|
@ -133,9 +133,9 @@ export default function Register({
|
|||
loading={submitLoader}
|
||||
>
|
||||
{value.name.toLowerCase() === "google" ||
|
||||
value.name.toLowerCase() === "apple" ? (
|
||||
<i className={"bi-" + value.name.toLowerCase()}></i>
|
||||
) : undefined}
|
||||
(value.name.toLowerCase() === "apple" && (
|
||||
<i className={"bi-" + value.name.toLowerCase()}></i>
|
||||
))}
|
||||
{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>
|
||||
|
|
|
@ -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={() => {
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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...",
|
||||
|
|
|
@ -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...",
|
||||
|
@ -367,4 +368,4 @@
|
|||
"server_administration": "Amministrazione Server",
|
||||
"all_collections": "Tutte le Collezioni",
|
||||
"dashboard": "Dashboard"
|
||||
}
|
||||
}
|
|
@ -80,6 +80,8 @@ export enum Sort {
|
|||
DescriptionZA,
|
||||
}
|
||||
|
||||
export type Order = { [key: string]: "asc" | "desc" };
|
||||
|
||||
export type LinkRequestQuery = {
|
||||
sort?: Sort;
|
||||
cursor?: number;
|
||||
|
|
Ŝarĝante…
Reference in New Issue