commit
c6e3147bb6
|
@ -1,5 +1,8 @@
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { CollectionIncludingMembersAndLinkCount } from "@/types/global";
|
import {
|
||||||
|
AccountSettings,
|
||||||
|
CollectionIncludingMembersAndLinkCount,
|
||||||
|
} from "@/types/global";
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import ProfilePhoto from "./ProfilePhoto";
|
import ProfilePhoto from "./ProfilePhoto";
|
||||||
import usePermissions from "@/hooks/usePermissions";
|
import usePermissions from "@/hooks/usePermissions";
|
||||||
|
@ -12,12 +15,11 @@ import { dropdownTriggerer } from "@/lib/client/utils";
|
||||||
import { useTranslation } from "next-i18next";
|
import { useTranslation } from "next-i18next";
|
||||||
import { useUser } from "@/hooks/store/user";
|
import { useUser } from "@/hooks/store/user";
|
||||||
|
|
||||||
type Props = {
|
export default function CollectionCard({
|
||||||
|
collection,
|
||||||
|
}: {
|
||||||
collection: CollectionIncludingMembersAndLinkCount;
|
collection: CollectionIncludingMembersAndLinkCount;
|
||||||
className?: string;
|
}) {
|
||||||
};
|
|
||||||
|
|
||||||
export default function CollectionCard({ collection, className }: Props) {
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { settings } = useLocalSettingsStore();
|
const { settings } = useLocalSettingsStore();
|
||||||
const { data: user = {} } = useUser();
|
const { data: user = {} } = useUser();
|
||||||
|
@ -33,15 +35,9 @@ export default function CollectionCard({ collection, className }: Props) {
|
||||||
|
|
||||||
const permissions = usePermissions(collection.id as number);
|
const permissions = usePermissions(collection.id as number);
|
||||||
|
|
||||||
const [collectionOwner, setCollectionOwner] = useState({
|
const [collectionOwner, setCollectionOwner] = useState<
|
||||||
id: null as unknown as number,
|
Partial<AccountSettings>
|
||||||
name: "",
|
>({});
|
||||||
username: "",
|
|
||||||
image: "",
|
|
||||||
archiveAsScreenshot: undefined as unknown as boolean,
|
|
||||||
archiveAsMonolith: undefined as unknown as boolean,
|
|
||||||
archiveAsPDF: undefined as unknown as boolean,
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchOwner = async () => {
|
const fetchOwner = async () => {
|
||||||
|
@ -132,12 +128,12 @@ export default function CollectionCard({ collection, className }: Props) {
|
||||||
className="flex items-center absolute bottom-3 left-3 z-10 btn px-2 btn-ghost rounded-full"
|
className="flex items-center absolute bottom-3 left-3 z-10 btn px-2 btn-ghost rounded-full"
|
||||||
onClick={() => setEditCollectionSharingModal(true)}
|
onClick={() => setEditCollectionSharingModal(true)}
|
||||||
>
|
>
|
||||||
{collectionOwner.id ? (
|
{collectionOwner.id && (
|
||||||
<ProfilePhoto
|
<ProfilePhoto
|
||||||
src={collectionOwner.image || undefined}
|
src={collectionOwner.image || undefined}
|
||||||
name={collectionOwner.name}
|
name={collectionOwner.name}
|
||||||
/>
|
/>
|
||||||
) : undefined}
|
)}
|
||||||
{collection.members
|
{collection.members
|
||||||
.sort((a, b) => (a.userId as number) - (b.userId as number))
|
.sort((a, b) => (a.userId as number) - (b.userId as number))
|
||||||
.map((e, i) => {
|
.map((e, i) => {
|
||||||
|
@ -151,13 +147,13 @@ export default function CollectionCard({ collection, className }: Props) {
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
.slice(0, 3)}
|
.slice(0, 3)}
|
||||||
{collection.members.length - 3 > 0 ? (
|
{collection.members.length - 3 > 0 && (
|
||||||
<div className={`avatar drop-shadow-md placeholder -ml-3`}>
|
<div className={`avatar drop-shadow-md placeholder -ml-3`}>
|
||||||
<div className="bg-base-100 text-neutral rounded-full w-8 h-8 ring-2 ring-neutral-content">
|
<div className="bg-base-100 text-neutral rounded-full w-8 h-8 ring-2 ring-neutral-content">
|
||||||
<span>+{collection.members.length - 3}</span>
|
<span>+{collection.members.length - 3}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<Link
|
<Link
|
||||||
href={`/collections/${collection.id}`}
|
href={`/collections/${collection.id}`}
|
||||||
|
@ -181,12 +177,12 @@ export default function CollectionCard({ collection, className }: Props) {
|
||||||
<div className="flex justify-end items-center">
|
<div className="flex justify-end items-center">
|
||||||
<div className="text-right">
|
<div className="text-right">
|
||||||
<div className="font-bold text-sm flex justify-end gap-1 items-center">
|
<div className="font-bold text-sm flex justify-end gap-1 items-center">
|
||||||
{collection.isPublic ? (
|
{collection.isPublic && (
|
||||||
<i
|
<i
|
||||||
className="bi-globe2 drop-shadow text-neutral"
|
className="bi-globe2 drop-shadow text-neutral"
|
||||||
title="This collection is being shared publicly."
|
title="This collection is being shared publicly."
|
||||||
></i>
|
></i>
|
||||||
) : undefined}
|
)}
|
||||||
<i
|
<i
|
||||||
className="bi-link-45deg text-lg text-neutral"
|
className="bi-link-45deg text-lg text-neutral"
|
||||||
title="This collection is being shared publicly."
|
title="This collection is being shared publicly."
|
||||||
|
@ -206,24 +202,24 @@ export default function CollectionCard({ collection, className }: Props) {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
{editCollectionModal ? (
|
{editCollectionModal && (
|
||||||
<EditCollectionModal
|
<EditCollectionModal
|
||||||
onClose={() => setEditCollectionModal(false)}
|
onClose={() => setEditCollectionModal(false)}
|
||||||
activeCollection={collection}
|
activeCollection={collection}
|
||||||
/>
|
/>
|
||||||
) : undefined}
|
)}
|
||||||
{editCollectionSharingModal ? (
|
{editCollectionSharingModal && (
|
||||||
<EditCollectionSharingModal
|
<EditCollectionSharingModal
|
||||||
onClose={() => setEditCollectionSharingModal(false)}
|
onClose={() => setEditCollectionSharingModal(false)}
|
||||||
activeCollection={collection}
|
activeCollection={collection}
|
||||||
/>
|
/>
|
||||||
) : undefined}
|
)}
|
||||||
{deleteCollectionModal ? (
|
{deleteCollectionModal && (
|
||||||
<DeleteCollectionModal
|
<DeleteCollectionModal
|
||||||
onClose={() => setDeleteCollectionModal(false)}
|
onClose={() => setDeleteCollectionModal(false)}
|
||||||
activeCollection={collection}
|
activeCollection={collection}
|
||||||
/>
|
/>
|
||||||
) : undefined}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -272,12 +272,12 @@ const renderItem = (
|
||||||
></i>
|
></i>
|
||||||
<p className="truncate w-full">{collection.name}</p>
|
<p className="truncate w-full">{collection.name}</p>
|
||||||
|
|
||||||
{collection.isPublic ? (
|
{collection.isPublic && (
|
||||||
<i
|
<i
|
||||||
className="bi-globe2 text-sm text-black/50 dark:text-white/50 drop-shadow"
|
className="bi-globe2 text-sm text-black/50 dark:text-white/50 drop-shadow"
|
||||||
title="This collection is being shared publicly."
|
title="This collection is being shared publicly."
|
||||||
></i>
|
></i>
|
||||||
) : undefined}
|
)}
|
||||||
<div className="drop-shadow text-neutral text-xs">
|
<div className="drop-shadow text-neutral text-xs">
|
||||||
{collection._count?.links}
|
{collection._count?.links}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -60,7 +60,8 @@ export default function Dropdown({
|
||||||
}
|
}
|
||||||
}, [points, dropdownHeight]);
|
}, [points, dropdownHeight]);
|
||||||
|
|
||||||
return !points || pos ? (
|
return (
|
||||||
|
(!points || pos) && (
|
||||||
<ClickAwayHandler
|
<ClickAwayHandler
|
||||||
onMount={(e) => {
|
onMount={(e) => {
|
||||||
setDropdownHeight(e.height);
|
setDropdownHeight(e.height);
|
||||||
|
@ -102,5 +103,6 @@ export default function Dropdown({
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</ClickAwayHandler>
|
</ClickAwayHandler>
|
||||||
) : null;
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,7 +34,6 @@ export default function TagSelection({ onChange, defaultValue }: Props) {
|
||||||
options={options}
|
options={options}
|
||||||
styles={styles}
|
styles={styles}
|
||||||
defaultValue={defaultValue}
|
defaultValue={defaultValue}
|
||||||
// menuPosition="fixed"
|
|
||||||
isMulti
|
isMulti
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
@ -136,7 +136,7 @@ export default function LinkActions({
|
||||||
{t("show_link_details")}
|
{t("show_link_details")}
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
{permissions === true || permissions?.canUpdate ? (
|
{(permissions === true || permissions?.canUpdate) && (
|
||||||
<li>
|
<li>
|
||||||
<div
|
<div
|
||||||
role="button"
|
role="button"
|
||||||
|
@ -150,7 +150,7 @@ export default function LinkActions({
|
||||||
{t("edit_link")}
|
{t("edit_link")}
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
) : undefined}
|
)}
|
||||||
{link.type === "url" && (
|
{link.type === "url" && (
|
||||||
<li>
|
<li>
|
||||||
<div
|
<div
|
||||||
|
@ -166,7 +166,7 @@ export default function LinkActions({
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
)}
|
)}
|
||||||
{permissions === true || permissions?.canDelete ? (
|
{(permissions === true || permissions?.canDelete) && (
|
||||||
<li>
|
<li>
|
||||||
<div
|
<div
|
||||||
role="button"
|
role="button"
|
||||||
|
@ -196,36 +196,35 @@ export default function LinkActions({
|
||||||
{t("delete")}
|
{t("delete")}
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
) : undefined}
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{editLinkModal && (
|
||||||
{editLinkModal ? (
|
|
||||||
<EditLinkModal
|
<EditLinkModal
|
||||||
onClose={() => setEditLinkModal(false)}
|
onClose={() => setEditLinkModal(false)}
|
||||||
activeLink={link}
|
activeLink={link}
|
||||||
/>
|
/>
|
||||||
) : undefined}
|
)}
|
||||||
{deleteLinkModal ? (
|
{deleteLinkModal && (
|
||||||
<DeleteLinkModal
|
<DeleteLinkModal
|
||||||
onClose={() => setDeleteLinkModal(false)}
|
onClose={() => setDeleteLinkModal(false)}
|
||||||
activeLink={link}
|
activeLink={link}
|
||||||
/>
|
/>
|
||||||
) : undefined}
|
)}
|
||||||
{preservedFormatsModal ? (
|
{preservedFormatsModal && (
|
||||||
<PreservedFormatsModal
|
<PreservedFormatsModal
|
||||||
onClose={() => setPreservedFormatsModal(false)}
|
onClose={() => setPreservedFormatsModal(false)}
|
||||||
link={link}
|
link={link}
|
||||||
/>
|
/>
|
||||||
) : undefined}
|
)}
|
||||||
{linkDetailModal ? (
|
{linkDetailModal && (
|
||||||
<LinkDetailModal
|
<LinkDetailModal
|
||||||
onClose={() => setLinkDetailModal(false)}
|
onClose={() => setLinkDetailModal(false)}
|
||||||
onEdit={() => setEditLinkModal(true)}
|
onEdit={() => setEditLinkModal(true)}
|
||||||
link={link}
|
link={link}
|
||||||
/>
|
/>
|
||||||
) : undefined}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -96,7 +96,7 @@ export default function LinkCard({ link, flipDropdown, editMode }: Props) {
|
||||||
let isPublic = router.pathname.startsWith("/public") ? true : undefined;
|
let isPublic = router.pathname.startsWith("/public") ? true : undefined;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let interval: any;
|
let interval: NodeJS.Timeout | null = null;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
isVisible &&
|
isVisible &&
|
||||||
|
|
|
@ -127,9 +127,9 @@ export default function LinkCardCompact({
|
||||||
|
|
||||||
<div className="mt-1 flex flex-col sm:flex-row sm:items-center gap-2 text-xs text-neutral">
|
<div className="mt-1 flex flex-col sm:flex-row sm:items-center gap-2 text-xs text-neutral">
|
||||||
<div className="flex items-center gap-x-3 text-neutral flex-wrap">
|
<div className="flex items-center gap-x-3 text-neutral flex-wrap">
|
||||||
{collection ? (
|
{collection && (
|
||||||
<LinkCollection link={link} collection={collection} />
|
<LinkCollection link={link} collection={collection} />
|
||||||
) : undefined}
|
)}
|
||||||
{link.name && <LinkTypeBadge link={link} />}
|
{link.name && <LinkTypeBadge link={link} />}
|
||||||
<LinkDate link={link} />
|
<LinkDate link={link} />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -88,7 +88,7 @@ export default function LinkMasonry({ link, flipDropdown, editMode }: Props) {
|
||||||
const permissions = usePermissions(collection?.id as number);
|
const permissions = usePermissions(collection?.id as number);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let interval: any;
|
let interval: NodeJS.Timeout | null = null;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
isVisible &&
|
isVisible &&
|
||||||
|
|
|
@ -87,15 +87,13 @@ export default function MobileNavigation({}: Props) {
|
||||||
<MobileNavigationButton href={`/collections`} icon={"bi-folder"} />
|
<MobileNavigationButton href={`/collections`} icon={"bi-folder"} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{newLinkModal ? (
|
{newLinkModal && <NewLinkModal onClose={() => setNewLinkModal(false)} />}
|
||||||
<NewLinkModal onClose={() => setNewLinkModal(false)} />
|
{newCollectionModal && (
|
||||||
) : undefined}
|
|
||||||
{newCollectionModal ? (
|
|
||||||
<NewCollectionModal onClose={() => setNewCollectionModal(false)} />
|
<NewCollectionModal onClose={() => setNewCollectionModal(false)} />
|
||||||
) : undefined}
|
)}
|
||||||
{uploadFileModal ? (
|
{uploadFileModal && (
|
||||||
<UploadFileModal onClose={() => setUploadFileModal(false)} />
|
<UploadFileModal onClose={() => setUploadFileModal(false)} />
|
||||||
) : undefined}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,11 @@
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import TextInput from "@/components/TextInput";
|
import TextInput from "@/components/TextInput";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
import { CollectionIncludingMembersAndLinkCount, Member } from "@/types/global";
|
import {
|
||||||
|
AccountSettings,
|
||||||
|
CollectionIncludingMembersAndLinkCount,
|
||||||
|
Member,
|
||||||
|
} from "@/types/global";
|
||||||
import getPublicUserData from "@/lib/client/getPublicUserData";
|
import getPublicUserData from "@/lib/client/getPublicUserData";
|
||||||
import usePermissions from "@/hooks/usePermissions";
|
import usePermissions from "@/hooks/usePermissions";
|
||||||
import ProfilePhoto from "../ProfilePhoto";
|
import ProfilePhoto from "../ProfilePhoto";
|
||||||
|
@ -65,15 +69,9 @@ export default function EditCollectionSharingModal({
|
||||||
|
|
||||||
const [memberUsername, setMemberUsername] = useState("");
|
const [memberUsername, setMemberUsername] = useState("");
|
||||||
|
|
||||||
const [collectionOwner, setCollectionOwner] = useState({
|
const [collectionOwner, setCollectionOwner] = useState<
|
||||||
id: null as unknown as number,
|
Partial<AccountSettings>
|
||||||
name: "",
|
>({});
|
||||||
username: "",
|
|
||||||
image: "",
|
|
||||||
archiveAsScreenshot: undefined as unknown as boolean,
|
|
||||||
archiveAsMonolith: undefined as unknown as boolean,
|
|
||||||
archiveAsPDF: undefined as unknown as boolean,
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchOwner = async () => {
|
const fetchOwner = async () => {
|
||||||
|
@ -133,7 +131,7 @@ export default function EditCollectionSharingModal({
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{collection.isPublic ? (
|
{collection.isPublic && (
|
||||||
<div>
|
<div>
|
||||||
<p className="mb-2">{t("sharable_link_guide")}</p>
|
<p className="mb-2">{t("sharable_link_guide")}</p>
|
||||||
<div className="w-full hide-scrollbar overflow-x-auto whitespace-nowrap rounded-md p-2 bg-base-200 border-neutral-content border-solid border flex items-center gap-2 justify-between">
|
<div className="w-full hide-scrollbar overflow-x-auto whitespace-nowrap rounded-md p-2 bg-base-200 border-neutral-content border-solid border flex items-center gap-2 justify-between">
|
||||||
|
@ -141,7 +139,7 @@ export default function EditCollectionSharingModal({
|
||||||
<CopyButton text={publicCollectionURL} />
|
<CopyButton text={publicCollectionURL} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
)}
|
||||||
|
|
||||||
{permissions === true && <div className="divider my-3"></div>}
|
{permissions === true && <div className="divider my-3"></div>}
|
||||||
|
|
||||||
|
|
|
@ -77,7 +77,7 @@ export default function EditLinkModal({ onClose, activeLink }: Props) {
|
||||||
|
|
||||||
<div className="divider mb-3 mt-1"></div>
|
<div className="divider mb-3 mt-1"></div>
|
||||||
|
|
||||||
{link.url ? (
|
{link.url && (
|
||||||
<Link
|
<Link
|
||||||
href={link.url}
|
href={link.url}
|
||||||
className="truncate text-neutral flex gap-2 mb-5 w-fit max-w-full"
|
className="truncate text-neutral flex gap-2 mb-5 w-fit max-w-full"
|
||||||
|
@ -87,7 +87,7 @@ export default function EditLinkModal({ onClose, activeLink }: Props) {
|
||||||
<i className="bi-link-45deg text-xl" />
|
<i className="bi-link-45deg text-xl" />
|
||||||
<p>{shortenedURL}</p>
|
<p>{shortenedURL}</p>
|
||||||
</Link>
|
</Link>
|
||||||
) : undefined}
|
)}
|
||||||
|
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<p className="mb-2">{t("name")}</p>
|
<p className="mb-2">{t("name")}</p>
|
||||||
|
@ -103,7 +103,7 @@ export default function EditLinkModal({ onClose, activeLink }: Props) {
|
||||||
<div className="grid sm:grid-cols-2 gap-3">
|
<div className="grid sm:grid-cols-2 gap-3">
|
||||||
<div>
|
<div>
|
||||||
<p className="mb-2">{t("collection")}</p>
|
<p className="mb-2">{t("collection")}</p>
|
||||||
{link.collection.name ? (
|
{link.collection.name && (
|
||||||
<CollectionSelection
|
<CollectionSelection
|
||||||
onChange={setCollection}
|
onChange={setCollection}
|
||||||
defaultValue={
|
defaultValue={
|
||||||
|
@ -113,7 +113,7 @@ export default function EditLinkModal({ onClose, activeLink }: Props) {
|
||||||
}
|
}
|
||||||
creatable={false}
|
creatable={false}
|
||||||
/>
|
/>
|
||||||
) : null}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|
|
@ -124,7 +124,7 @@ export default function NewLinkModal({ onClose }: Props) {
|
||||||
</div>
|
</div>
|
||||||
<div className="sm:col-span-2 col-span-5">
|
<div className="sm:col-span-2 col-span-5">
|
||||||
<p className="mb-2">{t("collection")}</p>
|
<p className="mb-2">{t("collection")}</p>
|
||||||
{link.collection.name ? (
|
{link.collection.name && (
|
||||||
<CollectionSelection
|
<CollectionSelection
|
||||||
onChange={setCollection}
|
onChange={setCollection}
|
||||||
defaultValue={{
|
defaultValue={{
|
||||||
|
@ -132,11 +132,11 @@ export default function NewLinkModal({ onClose }: Props) {
|
||||||
value: link.collection.id,
|
value: link.collection.id,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
) : null}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={"mt-2"}>
|
<div className={"mt-2"}>
|
||||||
{optionsExpanded ? (
|
{optionsExpanded && (
|
||||||
<div className="mt-5">
|
<div className="mt-5">
|
||||||
<div className="grid sm:grid-cols-2 gap-3">
|
<div className="grid sm:grid-cols-2 gap-3">
|
||||||
<div>
|
<div>
|
||||||
|
@ -171,7 +171,7 @@ export default function NewLinkModal({ onClose }: Props) {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : undefined}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between items-center mt-5">
|
<div className="flex justify-between items-center mt-5">
|
||||||
<div
|
<div
|
||||||
|
|
|
@ -79,7 +79,7 @@ export default function NewUserModal({ onClose }: Props) {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{emailEnabled ? (
|
{emailEnabled && (
|
||||||
<div>
|
<div>
|
||||||
<p className="mb-2">{t("email")}</p>
|
<p className="mb-2">{t("email")}</p>
|
||||||
<TextInput
|
<TextInput
|
||||||
|
@ -89,7 +89,7 @@ export default function NewUserModal({ onClose }: Props) {
|
||||||
value={form.email}
|
value={form.email}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : undefined}
|
)}
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<p className="mb-2">
|
<p className="mb-2">
|
||||||
|
|
|
@ -2,6 +2,7 @@ import React, { useEffect, useState } from "react";
|
||||||
import {
|
import {
|
||||||
LinkIncludingShortenedCollectionAndTags,
|
LinkIncludingShortenedCollectionAndTags,
|
||||||
ArchivedFormat,
|
ArchivedFormat,
|
||||||
|
AccountSettings,
|
||||||
} from "@/types/global";
|
} from "@/types/global";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
@ -35,15 +36,9 @@ export default function PreservedFormatsModal({ onClose, link }: Props) {
|
||||||
|
|
||||||
let isPublic = router.pathname.startsWith("/public") ? true : undefined;
|
let isPublic = router.pathname.startsWith("/public") ? true : undefined;
|
||||||
|
|
||||||
const [collectionOwner, setCollectionOwner] = useState({
|
const [collectionOwner, setCollectionOwner] = useState<
|
||||||
id: null as unknown as number,
|
Partial<AccountSettings>
|
||||||
name: "",
|
>({});
|
||||||
username: "",
|
|
||||||
image: "",
|
|
||||||
archiveAsScreenshot: undefined as unknown as boolean,
|
|
||||||
archiveAsMonolith: undefined as unknown as boolean,
|
|
||||||
archiveAsPDF: undefined as unknown as boolean,
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchOwner = async () => {
|
const fetchOwner = async () => {
|
||||||
|
@ -99,7 +94,7 @@ export default function PreservedFormatsModal({ onClose, link }: Props) {
|
||||||
await getLink.mutateAsync({ id: link.id as number });
|
await getLink.mutateAsync({ id: link.id as number });
|
||||||
})();
|
})();
|
||||||
|
|
||||||
let interval: any;
|
let interval: NodeJS.Timeout | null = null;
|
||||||
|
|
||||||
if (!isReady()) {
|
if (!isReady()) {
|
||||||
interval = setInterval(async () => {
|
interval = setInterval(async () => {
|
||||||
|
@ -149,7 +144,7 @@ export default function PreservedFormatsModal({ onClose, link }: Props) {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className={`flex flex-col gap-3`}>
|
<div className={`flex flex-col gap-3`}>
|
||||||
{monolithAvailable(link) ? (
|
{monolithAvailable(link) && (
|
||||||
<PreservedFormatRow
|
<PreservedFormatRow
|
||||||
name={t("webpage")}
|
name={t("webpage")}
|
||||||
icon={"bi-filetype-html"}
|
icon={"bi-filetype-html"}
|
||||||
|
@ -157,9 +152,9 @@ export default function PreservedFormatsModal({ onClose, link }: Props) {
|
||||||
link={link}
|
link={link}
|
||||||
downloadable={true}
|
downloadable={true}
|
||||||
/>
|
/>
|
||||||
) : undefined}
|
)}
|
||||||
|
|
||||||
{screenshotAvailable(link) ? (
|
{screenshotAvailable(link) && (
|
||||||
<PreservedFormatRow
|
<PreservedFormatRow
|
||||||
name={t("screenshot")}
|
name={t("screenshot")}
|
||||||
icon={"bi-file-earmark-image"}
|
icon={"bi-file-earmark-image"}
|
||||||
|
@ -171,9 +166,9 @@ export default function PreservedFormatsModal({ onClose, link }: Props) {
|
||||||
link={link}
|
link={link}
|
||||||
downloadable={true}
|
downloadable={true}
|
||||||
/>
|
/>
|
||||||
) : undefined}
|
)}
|
||||||
|
|
||||||
{pdfAvailable(link) ? (
|
{pdfAvailable(link) && (
|
||||||
<PreservedFormatRow
|
<PreservedFormatRow
|
||||||
name={t("pdf")}
|
name={t("pdf")}
|
||||||
icon={"bi-file-earmark-pdf"}
|
icon={"bi-file-earmark-pdf"}
|
||||||
|
@ -181,16 +176,16 @@ export default function PreservedFormatsModal({ onClose, link }: Props) {
|
||||||
link={link}
|
link={link}
|
||||||
downloadable={true}
|
downloadable={true}
|
||||||
/>
|
/>
|
||||||
) : undefined}
|
)}
|
||||||
|
|
||||||
{readabilityAvailable(link) ? (
|
{readabilityAvailable(link) && (
|
||||||
<PreservedFormatRow
|
<PreservedFormatRow
|
||||||
name={t("readable")}
|
name={t("readable")}
|
||||||
icon={"bi-file-earmark-text"}
|
icon={"bi-file-earmark-text"}
|
||||||
format={ArchivedFormat.readability}
|
format={ArchivedFormat.readability}
|
||||||
link={link}
|
link={link}
|
||||||
/>
|
/>
|
||||||
) : undefined}
|
)}
|
||||||
|
|
||||||
{!isReady() && !atLeastOneFormatAvailable() ? (
|
{!isReady() && !atLeastOneFormatAvailable() ? (
|
||||||
<div className={`w-full h-full flex flex-col justify-center p-10`}>
|
<div className={`w-full h-full flex flex-col justify-center p-10`}>
|
||||||
|
@ -203,7 +198,9 @@ export default function PreservedFormatsModal({ onClose, link }: Props) {
|
||||||
<p className="text-center text-2xl">{t("preservation_in_queue")}</p>
|
<p className="text-center text-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() ? (
|
) : (
|
||||||
|
!isReady() &&
|
||||||
|
atLeastOneFormatAvailable() && (
|
||||||
<div className={`w-full h-full flex flex-col justify-center p-5`}>
|
<div className={`w-full h-full flex flex-col justify-center p-5`}>
|
||||||
<BeatLoader
|
<BeatLoader
|
||||||
color="oklch(var(--p))"
|
color="oklch(var(--p))"
|
||||||
|
@ -213,7 +210,8 @@ export default function PreservedFormatsModal({ onClose, link }: Props) {
|
||||||
<p className="text-center">{t("there_are_more_formats")}</p>
|
<p className="text-center">{t("there_are_more_formats")}</p>
|
||||||
<p className="text-center text-sm">{t("check_back_later")}</p>
|
<p className="text-center text-sm">{t("check_back_later")}</p>
|
||||||
</div>
|
</div>
|
||||||
) : undefined}
|
)
|
||||||
|
)}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={`flex flex-col sm:flex-row gap-3 items-center justify-center ${
|
className={`flex flex-col sm:flex-row gap-3 items-center justify-center ${
|
||||||
|
|
|
@ -150,7 +150,7 @@ export default function UploadFileModal({ onClose }: Props) {
|
||||||
<label className="btn h-10 btn-sm w-full border border-neutral-content hover:border-neutral-content flex justify-between">
|
<label className="btn h-10 btn-sm w-full border border-neutral-content hover:border-neutral-content flex justify-between">
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
accept=".pdf,.png,.jpg,.jpeg,.html"
|
accept=".pdf,.png,.jpg,.jpeg"
|
||||||
className="cursor-pointer custom-file-input"
|
className="cursor-pointer custom-file-input"
|
||||||
onChange={(e) => e.target.files && setFile(e.target.files[0])}
|
onChange={(e) => e.target.files && setFile(e.target.files[0])}
|
||||||
/>
|
/>
|
||||||
|
@ -163,7 +163,7 @@ export default function UploadFileModal({ onClose }: Props) {
|
||||||
</div>
|
</div>
|
||||||
<div className="sm:col-span-2 col-span-5">
|
<div className="sm:col-span-2 col-span-5">
|
||||||
<p className="mb-2">{t("collection")}</p>
|
<p className="mb-2">{t("collection")}</p>
|
||||||
{link.collection.name ? (
|
{link.collection.name && (
|
||||||
<CollectionSelection
|
<CollectionSelection
|
||||||
onChange={setCollection}
|
onChange={setCollection}
|
||||||
defaultValue={{
|
defaultValue={{
|
||||||
|
@ -171,10 +171,10 @@ export default function UploadFileModal({ onClose }: Props) {
|
||||||
value: link.collection.id,
|
value: link.collection.id,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
) : null}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{optionsExpanded ? (
|
{optionsExpanded && (
|
||||||
<div className="mt-5">
|
<div className="mt-5">
|
||||||
<div className="grid sm:grid-cols-2 gap-3">
|
<div className="grid sm:grid-cols-2 gap-3">
|
||||||
<div>
|
<div>
|
||||||
|
@ -209,7 +209,7 @@ export default function UploadFileModal({ onClose }: Props) {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : undefined}
|
)}
|
||||||
<div className="flex justify-between items-center mt-5">
|
<div className="flex justify-between items-center mt-5">
|
||||||
<div
|
<div
|
||||||
onClick={() => setOptionsExpanded(!optionsExpanded)}
|
onClick={() => setOptionsExpanded(!optionsExpanded)}
|
||||||
|
|
|
@ -114,7 +114,7 @@ export default function Navbar() {
|
||||||
|
|
||||||
<MobileNavigation />
|
<MobileNavigation />
|
||||||
|
|
||||||
{sidebar ? (
|
{sidebar && (
|
||||||
<div className="fixed top-0 bottom-0 right-0 left-0 bg-black bg-opacity-10 backdrop-blur-sm flex items-center fade-in z-40">
|
<div className="fixed top-0 bottom-0 right-0 left-0 bg-black bg-opacity-10 backdrop-blur-sm flex items-center fade-in z-40">
|
||||||
<ClickAwayHandler className="h-full" onClickOutside={toggleSidebar}>
|
<ClickAwayHandler className="h-full" onClickOutside={toggleSidebar}>
|
||||||
<div className="slide-right h-full shadow-lg">
|
<div className="slide-right h-full shadow-lg">
|
||||||
|
@ -122,16 +122,14 @@ export default function Navbar() {
|
||||||
</div>
|
</div>
|
||||||
</ClickAwayHandler>
|
</ClickAwayHandler>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
)}
|
||||||
{newLinkModal ? (
|
{newLinkModal && <NewLinkModal onClose={() => setNewLinkModal(false)} />}
|
||||||
<NewLinkModal onClose={() => setNewLinkModal(false)} />
|
{newCollectionModal && (
|
||||||
) : undefined}
|
|
||||||
{newCollectionModal ? (
|
|
||||||
<NewCollectionModal onClose={() => setNewCollectionModal(false)} />
|
<NewCollectionModal onClose={() => setNewCollectionModal(false)} />
|
||||||
) : undefined}
|
)}
|
||||||
{uploadFileModal ? (
|
{uploadFileModal && (
|
||||||
<UploadFileModal onClose={() => setUploadFileModal(false)} />
|
<UploadFileModal onClose={() => setUploadFileModal(false)} />
|
||||||
) : undefined}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,9 +39,7 @@ export default function NoLinksFound({ text }: Props) {
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{newLinkModal ? (
|
{newLinkModal && <NewLinkModal onClose={() => setNewLinkModal(false)} />}
|
||||||
<NewLinkModal onClose={() => setNewLinkModal(false)} />
|
|
||||||
) : undefined}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,6 @@ import {
|
||||||
} from "@/types/global";
|
} from "@/types/global";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useGetLink } from "@/hooks/store/links";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
name: string;
|
name: string;
|
||||||
|
@ -21,8 +20,6 @@ export default function PreservedFormatRow({
|
||||||
link,
|
link,
|
||||||
downloadable,
|
downloadable,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const getLink = useGetLink();
|
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
let isPublic = router.pathname.startsWith("/public") ? true : undefined;
|
let isPublic = router.pathname.startsWith("/public") ? true : undefined;
|
||||||
|
|
|
@ -60,7 +60,7 @@ export default function ProfileDropdown() {
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
{isAdmin ? (
|
{isAdmin && (
|
||||||
<li>
|
<li>
|
||||||
<Link
|
<Link
|
||||||
href="/admin"
|
href="/admin"
|
||||||
|
@ -72,7 +72,7 @@ export default function ProfileDropdown() {
|
||||||
{t("server_administration")}
|
{t("server_administration")}
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
) : null}
|
)}
|
||||||
<li>
|
<li>
|
||||||
<div
|
<div
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|
|
@ -68,7 +68,7 @@ export default function ReadableView({ link }: Props) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (link) getLink.mutateAsync({ id: link.id as number });
|
if (link) getLink.mutateAsync({ id: link.id as number });
|
||||||
|
|
||||||
let interval: any;
|
let interval: NodeJS.Timeout | null = null;
|
||||||
if (
|
if (
|
||||||
link &&
|
link &&
|
||||||
(link?.image === "pending" ||
|
(link?.image === "pending" ||
|
||||||
|
@ -182,7 +182,7 @@ export default function ReadableView({ link }: Props) {
|
||||||
link?.name || link?.description || link?.url || ""
|
link?.name || link?.description || link?.url || ""
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
{link?.url ? (
|
{link?.url && (
|
||||||
<Link
|
<Link
|
||||||
href={link?.url || ""}
|
href={link?.url || ""}
|
||||||
title={link?.url}
|
title={link?.url}
|
||||||
|
@ -191,11 +191,10 @@ export default function ReadableView({ link }: Props) {
|
||||||
>
|
>
|
||||||
<i className="bi-link-45deg"></i>
|
<i className="bi-link-45deg"></i>
|
||||||
|
|
||||||
{isValidUrl(link?.url || "")
|
{isValidUrl(link?.url || "") &&
|
||||||
? new URL(link?.url as string).host
|
new URL(link?.url as string).host}
|
||||||
: undefined}
|
|
||||||
</Link>
|
</Link>
|
||||||
) : undefined}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import useLocalSettingsStore from "@/store/localSettings";
|
import useLocalSettingsStore from "@/store/localSettings";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState, ChangeEvent } from "react";
|
||||||
import { useTranslation } from "next-i18next";
|
import { useTranslation } from "next-i18next";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
@ -10,14 +10,18 @@ export default function ToggleDarkMode({ className }: Props) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { settings, updateSettings } = useLocalSettingsStore();
|
const { settings, updateSettings } = useLocalSettingsStore();
|
||||||
|
|
||||||
const [theme, setTheme] = useState(localStorage.getItem("theme"));
|
const [theme, setTheme] = useState<string | null>(
|
||||||
|
localStorage.getItem("theme")
|
||||||
|
);
|
||||||
|
|
||||||
const handleToggle = (e: any) => {
|
const handleToggle = (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
setTheme(e.target.checked ? "dark" : "light");
|
setTheme(e.target.checked ? "dark" : "light");
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
updateSettings({ theme: theme as string });
|
if (theme) {
|
||||||
|
updateSettings({ theme });
|
||||||
|
}
|
||||||
}, [theme]);
|
}, [theme]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -34,7 +38,7 @@ export default function ToggleDarkMode({ className }: Props) {
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
onChange={handleToggle}
|
onChange={handleToggle}
|
||||||
className="theme-controller"
|
className="theme-controller"
|
||||||
checked={localStorage.getItem("theme") === "light" ? false : true}
|
checked={theme === "dark"}
|
||||||
/>
|
/>
|
||||||
<i className="bi-sun-fill text-xl swap-on"></i>
|
<i className="bi-sun-fill text-xl swap-on"></i>
|
||||||
<i className="bi-moon-fill text-xl swap-off"></i>
|
<i className="bi-moon-fill text-xl swap-off"></i>
|
||||||
|
|
|
@ -74,12 +74,12 @@ const UserListing = (
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
{deleteUserModal.isOpen && deleteUserModal.userId ? (
|
{deleteUserModal.isOpen && deleteUserModal.userId && (
|
||||||
<DeleteUserModal
|
<DeleteUserModal
|
||||||
onClose={() => setDeleteUserModal({ isOpen: false, userId: null })}
|
onClose={() => setDeleteUserModal({ isOpen: false, userId: null })}
|
||||||
userId={deleteUserModal.userId}
|
userId={deleteUserModal.userId}
|
||||||
/>
|
/>
|
||||||
) : null}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -2,19 +2,17 @@ import axios, { AxiosError } from "axios"
|
||||||
|
|
||||||
axios.defaults.baseURL = "http://localhost:3000"
|
axios.defaults.baseURL = "http://localhost:3000"
|
||||||
|
|
||||||
export async function seedUser (username?: string, password?: string, name?: string) {
|
export async function seedUser(username?: string, password?: string, name?: string) {
|
||||||
try {
|
try {
|
||||||
return await axios.post("/api/v1/users", {
|
return await axios.post("/api/v1/users", {
|
||||||
username: username || "test",
|
username: username || "test",
|
||||||
password: password || "password",
|
password: password || "password",
|
||||||
name: name || "Test User",
|
name: name || "Test User",
|
||||||
})
|
})
|
||||||
} catch (e: any) {
|
} catch (error) {
|
||||||
if (e instanceof AxiosError) {
|
const axiosError = error as AxiosError;
|
||||||
if (e.response?.status === 400) {
|
if (axiosError && axiosError.response?.status === 400) return
|
||||||
return
|
|
||||||
}
|
throw error
|
||||||
}
|
|
||||||
throw e
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -23,7 +23,7 @@ export default function CenteredForm({
|
||||||
data-testid={dataTestId}
|
data-testid={dataTestId}
|
||||||
>
|
>
|
||||||
<div className="m-auto flex flex-col gap-2 w-full">
|
<div className="m-auto flex flex-col gap-2 w-full">
|
||||||
{settings.theme ? (
|
{settings.theme && (
|
||||||
<Image
|
<Image
|
||||||
src={`/linkwarden_${
|
src={`/linkwarden_${
|
||||||
settings.theme === "dark" ? "dark" : "light"
|
settings.theme === "dark" ? "dark" : "light"
|
||||||
|
@ -33,12 +33,12 @@ export default function CenteredForm({
|
||||||
alt="Linkwarden"
|
alt="Linkwarden"
|
||||||
className="h-12 w-fit mx-auto"
|
className="h-12 w-fit mx-auto"
|
||||||
/>
|
/>
|
||||||
) : undefined}
|
)}
|
||||||
{text ? (
|
{text && (
|
||||||
<p className="text-lg max-w-[30rem] min-w-80 w-full mx-auto font-semibold px-2 text-center">
|
<p className="text-lg max-w-[30rem] min-w-80 w-full mx-auto font-semibold px-2 text-center">
|
||||||
{text}
|
{text}
|
||||||
</p>
|
</p>
|
||||||
) : undefined}
|
)}
|
||||||
{children}
|
{children}
|
||||||
<p className="text-center text-xs text-neutral mb-5">
|
<p className="text-center text-xs text-neutral mb-5">
|
||||||
<Trans
|
<Trans
|
||||||
|
|
|
@ -34,9 +34,9 @@ export default function MainLayout({ children }: Props) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex" data-testid="dashboard-wrapper">
|
<div className="flex" data-testid="dashboard-wrapper">
|
||||||
{showAnnouncement ? (
|
{showAnnouncement && (
|
||||||
<Announcement toggleAnnouncementBar={toggleAnnouncementBar} />
|
<Announcement toggleAnnouncementBar={toggleAnnouncementBar} />
|
||||||
) : undefined}
|
)}
|
||||||
<div className="hidden lg:block">
|
<div className="hidden lg:block">
|
||||||
<Sidebar className={`fixed top-0`} />
|
<Sidebar className={`fixed top-0`} />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -54,7 +54,7 @@ export default function SettingsLayout({ children }: Props) {
|
||||||
|
|
||||||
{children}
|
{children}
|
||||||
|
|
||||||
{sidebar ? (
|
{sidebar && (
|
||||||
<div className="fixed top-0 bottom-0 right-0 left-0 bg-black bg-opacity-10 backdrop-blur-sm flex items-center fade-in z-30">
|
<div className="fixed top-0 bottom-0 right-0 left-0 bg-black bg-opacity-10 backdrop-blur-sm flex items-center fade-in z-30">
|
||||||
<ClickAwayHandler
|
<ClickAwayHandler
|
||||||
className="h-full"
|
className="h-full"
|
||||||
|
@ -65,7 +65,7 @@ export default function SettingsLayout({ children }: Props) {
|
||||||
</div>
|
</div>
|
||||||
</ClickAwayHandler>
|
</ClickAwayHandler>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import { prisma } from "@/lib/api/db";
|
import { prisma } from "@/lib/api/db";
|
||||||
import { LinkRequestQuery, Sort } from "@/types/global";
|
import { LinkRequestQuery, Order, Sort } from "@/types/global";
|
||||||
|
|
||||||
export default async function getDashboardData(
|
export default async function getDashboardData(
|
||||||
userId: number,
|
userId: number,
|
||||||
query: LinkRequestQuery
|
query: LinkRequestQuery
|
||||||
) {
|
) {
|
||||||
let order: any = { id: "desc" };
|
let order: Order = { id: "desc" };
|
||||||
if (query.sort === Sort.DateNewestFirst) order = { id: "desc" };
|
if (query.sort === Sort.DateNewestFirst) order = { id: "desc" };
|
||||||
else if (query.sort === Sort.DateOldestFirst) order = { id: "asc" };
|
else if (query.sort === Sort.DateOldestFirst) order = { id: "asc" };
|
||||||
else if (query.sort === Sort.NameAZ) order = { name: "asc" };
|
else if (query.sort === Sort.NameAZ) order = { name: "asc" };
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { prisma } from "@/lib/api/db";
|
import { prisma } from "@/lib/api/db";
|
||||||
import { LinkRequestQuery, Sort } from "@/types/global";
|
import { LinkRequestQuery, Order, Sort } from "@/types/global";
|
||||||
|
|
||||||
type Response<D> =
|
type Response<D> =
|
||||||
| {
|
| {
|
||||||
|
@ -17,7 +17,7 @@ export default async function getDashboardData(
|
||||||
userId: number,
|
userId: number,
|
||||||
query: LinkRequestQuery
|
query: LinkRequestQuery
|
||||||
): Promise<Response<any>> {
|
): Promise<Response<any>> {
|
||||||
let order: any;
|
let order: Order = { id: "desc" };
|
||||||
if (query.sort === Sort.DateNewestFirst) order = { id: "desc" };
|
if (query.sort === Sort.DateNewestFirst) order = { id: "desc" };
|
||||||
else if (query.sort === Sort.DateOldestFirst) order = { id: "asc" };
|
else if (query.sort === Sort.DateOldestFirst) order = { id: "asc" };
|
||||||
else if (query.sort === Sort.NameAZ) order = { name: "asc" };
|
else if (query.sort === Sort.NameAZ) order = { name: "asc" };
|
||||||
|
@ -105,9 +105,8 @@ export default async function getDashboardData(
|
||||||
});
|
});
|
||||||
|
|
||||||
const links = [...recentlyAddedLinks, ...pinnedLinks].sort(
|
const links = [...recentlyAddedLinks, ...pinnedLinks].sort(
|
||||||
(a, b) => (new Date(b.id) as any) - (new Date(a.id) as any)
|
(a, b) => new Date(b.id).getTime() - new Date(a.id).getTime()
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data: {
|
data: {
|
||||||
links,
|
links,
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import { prisma } from "@/lib/api/db";
|
import { prisma } from "@/lib/api/db";
|
||||||
import { LinkRequestQuery, Sort } from "@/types/global";
|
import { LinkRequestQuery, Order, Sort } from "@/types/global";
|
||||||
|
|
||||||
export default async function getLink(userId: number, query: LinkRequestQuery) {
|
export default async function getLink(userId: number, query: LinkRequestQuery) {
|
||||||
const POSTGRES_IS_ENABLED =
|
const POSTGRES_IS_ENABLED =
|
||||||
process.env.DATABASE_URL?.startsWith("postgresql");
|
process.env.DATABASE_URL?.startsWith("postgresql");
|
||||||
|
|
||||||
let order: any = { id: "desc" };
|
let order: Order = { id: "desc" };
|
||||||
if (query.sort === Sort.DateNewestFirst) order = { id: "desc" };
|
if (query.sort === Sort.DateNewestFirst) order = { id: "desc" };
|
||||||
else if (query.sort === Sort.DateOldestFirst) order = { id: "asc" };
|
else if (query.sort === Sort.DateOldestFirst) order = { id: "asc" };
|
||||||
else if (query.sort === Sort.NameAZ) order = { name: "asc" };
|
else if (query.sort === Sort.NameAZ) order = { name: "asc" };
|
||||||
|
|
|
@ -22,18 +22,5 @@ export default async function exportData(userId: number) {
|
||||||
|
|
||||||
const { password, id, ...userData } = user;
|
const { password, id, ...userData } = user;
|
||||||
|
|
||||||
function redactIds(obj: any) {
|
|
||||||
if (Array.isArray(obj)) {
|
|
||||||
obj.forEach((o) => redactIds(o));
|
|
||||||
} else if (obj !== null && typeof obj === "object") {
|
|
||||||
delete obj.id;
|
|
||||||
for (let key in obj) {
|
|
||||||
redactIds(obj[key]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
redactIds(userData);
|
|
||||||
|
|
||||||
return { response: userData, status: 200 };
|
return { response: userData, status: 200 };
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { prisma } from "@/lib/api/db";
|
import { prisma } from "@/lib/api/db";
|
||||||
import { Backup } from "@/types/global";
|
|
||||||
import createFolder from "@/lib/api/storage/createFolder";
|
import createFolder from "@/lib/api/storage/createFolder";
|
||||||
|
|
||||||
const MAX_LINKS_PER_USER = Number(process.env.MAX_LINKS_PER_USER) || 30000;
|
const MAX_LINKS_PER_USER = Number(process.env.MAX_LINKS_PER_USER) || 30000;
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { prisma } from "@/lib/api/db";
|
import { prisma } from "@/lib/api/db";
|
||||||
import { LinkRequestQuery, Sort } from "@/types/global";
|
import { LinkRequestQuery, Order, Sort } from "@/types/global";
|
||||||
|
|
||||||
export default async function getLink(
|
export default async function getLink(
|
||||||
query: Omit<LinkRequestQuery, "tagId" | "pinnedOnly">
|
query: Omit<LinkRequestQuery, "tagId" | "pinnedOnly">
|
||||||
|
@ -7,7 +7,7 @@ export default async function getLink(
|
||||||
const POSTGRES_IS_ENABLED =
|
const POSTGRES_IS_ENABLED =
|
||||||
process.env.DATABASE_URL?.startsWith("postgresql");
|
process.env.DATABASE_URL?.startsWith("postgresql");
|
||||||
|
|
||||||
let order: any;
|
let order: Order = { id: "desc" };
|
||||||
if (query.sort === Sort.DateNewestFirst) order = { id: "desc" };
|
if (query.sort === Sort.DateNewestFirst) order = { id: "desc" };
|
||||||
else if (query.sort === Sort.DateOldestFirst) order = { id: "asc" };
|
else if (query.sort === Sort.DateOldestFirst) order = { id: "asc" };
|
||||||
else if (query.sort === Sort.NameAZ) order = { name: "asc" };
|
else if (query.sort === Sort.NameAZ) order = { name: "asc" };
|
||||||
|
|
|
@ -29,7 +29,7 @@ export default async function createSession(
|
||||||
secret: process.env.NEXTAUTH_SECRET as string,
|
secret: process.env.NEXTAUTH_SECRET as string,
|
||||||
});
|
});
|
||||||
|
|
||||||
const createToken = await prisma.accessToken.create({
|
await prisma.accessToken.create({
|
||||||
data: {
|
data: {
|
||||||
name: sessionName || "Unknown Device",
|
name: sessionName || "Unknown Device",
|
||||||
userId,
|
userId,
|
||||||
|
|
|
@ -24,10 +24,7 @@ export default async function deleteUserById(
|
||||||
|
|
||||||
if (!isServerAdmin) {
|
if (!isServerAdmin) {
|
||||||
if (user.password) {
|
if (user.password) {
|
||||||
const isPasswordValid = bcrypt.compareSync(
|
const isPasswordValid = bcrypt.compareSync(body.password, user.password);
|
||||||
body.password,
|
|
||||||
user.password as string
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!isPasswordValid && !isServerAdmin) {
|
if (!isPasswordValid && !isServerAdmin) {
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -16,7 +16,7 @@ const generatePreview = async (
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
image.resize(1280, Jimp.AUTO).quality(20);
|
image.resize(1000, Jimp.AUTO).quality(20);
|
||||||
const processedBuffer = await image.getBufferAsync(Jimp.MIME_JPEG);
|
const processedBuffer = await image.getBufferAsync(Jimp.MIME_JPEG);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
|
|
@ -14,7 +14,7 @@ export default async function moveFile(from: string, to: string) {
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
s3Client.copyObject(copyParams, async (err: any) => {
|
s3Client.copyObject(copyParams, async (err: unknown) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
console.error("Error copying the object:", err);
|
console.error("Error copying the object:", err);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -9,7 +9,7 @@ export const resizeImage = (file: File): Promise<Blob> =>
|
||||||
"JPEG", // output format
|
"JPEG", // output format
|
||||||
100, // quality
|
100, // quality
|
||||||
0, // rotation
|
0, // rotation
|
||||||
(uri: any) => {
|
(uri) => {
|
||||||
resolve(uri as Blob);
|
resolve(uri as Blob);
|
||||||
},
|
},
|
||||||
"blob" // output type
|
"blob" // output type
|
||||||
|
|
|
@ -7,10 +7,15 @@ export function isPWA() {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isIphone() {
|
export function isIphone() {
|
||||||
return /iPhone/.test(navigator.userAgent) && !(window as any).MSStream;
|
return (
|
||||||
|
/iPhone/.test(navigator.userAgent) &&
|
||||||
|
!(window as unknown as { MSStream?: any }).MSStream
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function dropdownTriggerer(e: any) {
|
export function dropdownTriggerer(
|
||||||
|
e: React.FocusEvent<HTMLElement> | React.MouseEvent<HTMLElement>
|
||||||
|
) {
|
||||||
let targetEl = e.currentTarget;
|
let targetEl = e.currentTarget;
|
||||||
if (targetEl && targetEl.matches(":focus")) {
|
if (targetEl && targetEl.matches(":focus")) {
|
||||||
setTimeout(function () {
|
setTimeout(function () {
|
||||||
|
|
|
@ -39,7 +39,9 @@ export function monolithAvailable(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function previewAvailable(link: any) {
|
export function previewAvailable(
|
||||||
|
link: LinkIncludingShortenedCollectionAndTags
|
||||||
|
) {
|
||||||
return (
|
return (
|
||||||
link &&
|
link &&
|
||||||
link.preview &&
|
link.preview &&
|
||||||
|
|
|
@ -100,9 +100,7 @@ export default function Admin() {
|
||||||
<p>{t("no_users_found")}</p>
|
<p>{t("no_users_found")}</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{newUserModal ? (
|
{newUserModal && <NewUserModal onClose={() => setNewUserModal(false)} />}
|
||||||
<NewUserModal onClose={() => setNewUserModal(false)} />
|
|
||||||
) : null}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -166,8 +166,12 @@ export default async function Index(req: NextApiRequest, res: NextApiResponse) {
|
||||||
where: { id: linkId },
|
where: { id: linkId },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (linkStillExists && files.file[0].mimetype?.includes("image")) {
|
const { mimetype } = files.file[0];
|
||||||
const collectionId = collectionPermissions.id as number;
|
const isPDF = mimetype?.includes("pdf");
|
||||||
|
const isImage = mimetype?.includes("image");
|
||||||
|
|
||||||
|
if (linkStillExists && isImage) {
|
||||||
|
const collectionId = collectionPermissions.id;
|
||||||
createFolder({
|
createFolder({
|
||||||
filePath: `archives/preview/${collectionId}`,
|
filePath: `archives/preview/${collectionId}`,
|
||||||
});
|
});
|
||||||
|
@ -184,13 +188,11 @@ export default async function Index(req: NextApiRequest, res: NextApiResponse) {
|
||||||
await prisma.link.update({
|
await prisma.link.update({
|
||||||
where: { id: linkId },
|
where: { id: linkId },
|
||||||
data: {
|
data: {
|
||||||
preview: files.file[0].mimetype?.includes("pdf")
|
preview: isPDF ? "unavailable" : undefined,
|
||||||
? "unavailable"
|
image: isImage
|
||||||
: undefined,
|
|
||||||
image: files.file[0].mimetype?.includes("image")
|
|
||||||
? `archives/${collectionPermissions.id}/${linkId + suffix}`
|
? `archives/${collectionPermissions.id}/${linkId + suffix}`
|
||||||
: null,
|
: null,
|
||||||
pdf: files.file[0].mimetype?.includes("pdf")
|
pdf: isPDF
|
||||||
? `archives/${collectionPermissions.id}/${linkId + suffix}`
|
? `archives/${collectionPermissions.id}/${linkId + suffix}`
|
||||||
: null,
|
: null,
|
||||||
lastPreserved: new Date().toISOString(),
|
lastPreserved: new Date().toISOString(),
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import {
|
import {
|
||||||
|
AccountSettings,
|
||||||
CollectionIncludingMembersAndLinkCount,
|
CollectionIncludingMembersAndLinkCount,
|
||||||
Sort,
|
Sort,
|
||||||
ViewMode,
|
ViewMode,
|
||||||
|
@ -54,15 +55,9 @@ export default function Index() {
|
||||||
|
|
||||||
const { data: user = {} } = useUser();
|
const { data: user = {} } = useUser();
|
||||||
|
|
||||||
const [collectionOwner, setCollectionOwner] = useState({
|
const [collectionOwner, setCollectionOwner] = useState<
|
||||||
id: null as unknown as number,
|
Partial<AccountSettings>
|
||||||
name: "",
|
>({});
|
||||||
username: "",
|
|
||||||
image: "",
|
|
||||||
archiveAsScreenshot: undefined as unknown as boolean,
|
|
||||||
archiveAsMonolith: undefined as unknown as boolean,
|
|
||||||
archiveAsPDF: undefined as unknown as boolean,
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchOwner = async () => {
|
const fetchOwner = async () => {
|
||||||
|
@ -207,14 +202,14 @@ export default function Index() {
|
||||||
className="flex items-center btn px-2 btn-ghost rounded-full w-fit"
|
className="flex items-center btn px-2 btn-ghost rounded-full w-fit"
|
||||||
onClick={() => setEditCollectionSharingModal(true)}
|
onClick={() => setEditCollectionSharingModal(true)}
|
||||||
>
|
>
|
||||||
{collectionOwner.id ? (
|
{collectionOwner.id && (
|
||||||
<ProfilePhoto
|
<ProfilePhoto
|
||||||
src={collectionOwner.image || undefined}
|
src={collectionOwner.image || undefined}
|
||||||
name={collectionOwner.name}
|
name={collectionOwner.name}
|
||||||
/>
|
/>
|
||||||
) : undefined}
|
)}
|
||||||
{activeCollection.members
|
{activeCollection.members
|
||||||
.sort((a, b) => (a.userId as number) - (b.userId as number))
|
.sort((a, b) => a.userId - b.userId)
|
||||||
.map((e, i) => {
|
.map((e, i) => {
|
||||||
return (
|
return (
|
||||||
<ProfilePhoto
|
<ProfilePhoto
|
||||||
|
@ -226,13 +221,13 @@ export default function Index() {
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
.slice(0, 3)}
|
.slice(0, 3)}
|
||||||
{activeCollection.members.length - 3 > 0 ? (
|
{activeCollection.members.length - 3 > 0 && (
|
||||||
<div className={`avatar drop-shadow-md placeholder -ml-3`}>
|
<div className={`avatar drop-shadow-md placeholder -ml-3`}>
|
||||||
<div className="bg-base-100 text-neutral rounded-full w-8 h-8 ring-2 ring-neutral-content">
|
<div className="bg-base-100 text-neutral rounded-full w-8 h-8 ring-2 ring-neutral-content">
|
||||||
<span>+{activeCollection.members.length - 3}</span>
|
<span>+{activeCollection.members.length - 3}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="text-neutral text-sm">
|
<p className="text-neutral text-sm">
|
||||||
|
|
|
@ -60,7 +60,7 @@ export default function Collections() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{sortedCollections.filter((e) => e.ownerId !== data?.user.id)[0] ? (
|
{sortedCollections.filter((e) => e.ownerId !== data?.user.id)[0] && (
|
||||||
<>
|
<>
|
||||||
<PageHeader
|
<PageHeader
|
||||||
icon={"bi-folder"}
|
icon={"bi-folder"}
|
||||||
|
@ -76,11 +76,11 @@ export default function Collections() {
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
) : undefined}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{newCollectionModal ? (
|
{newCollectionModal && (
|
||||||
<NewCollectionModal onClose={() => setNewCollectionModal(false)} />
|
<NewCollectionModal onClose={() => setNewCollectionModal(false)} />
|
||||||
) : undefined}
|
)}
|
||||||
</MainLayout>
|
</MainLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -55,11 +55,14 @@ export default function Dashboard() {
|
||||||
handleNumberOfLinksToShow();
|
handleNumberOfLinksToShow();
|
||||||
}, [width]);
|
}, [width]);
|
||||||
|
|
||||||
const importBookmarks = async (e: any, format: MigrationFormat) => {
|
const importBookmarks = async (
|
||||||
const file: File = e.target.files[0];
|
e: React.ChangeEvent<HTMLInputElement>,
|
||||||
|
format: MigrationFormat
|
||||||
|
) => {
|
||||||
|
const file: File | null = e.target.files && e.target.files[0];
|
||||||
|
|
||||||
if (file) {
|
if (file) {
|
||||||
var reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.readAsText(file, "UTF-8");
|
reader.readAsText(file, "UTF-8");
|
||||||
reader.onload = async function (e) {
|
reader.onload = async function (e) {
|
||||||
const load = toast.loading("Importing...");
|
const load = toast.loading("Importing...");
|
||||||
|
@ -132,7 +135,7 @@ export default function Dashboard() {
|
||||||
|
|
||||||
<DashboardItem
|
<DashboardItem
|
||||||
name={tags.length === 1 ? t("tag") : t("tags")}
|
name={tags.length === 1 ? t("tag") : t("tags")}
|
||||||
value={tags.length * numberOfLinks}
|
value={tags.length}
|
||||||
icon={"bi-hash"}
|
icon={"bi-hash"}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -339,9 +342,7 @@ export default function Dashboard() {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{newLinkModal ? (
|
{newLinkModal && <NewLinkModal onClose={() => setNewLinkModal(false)} />}
|
||||||
<NewLinkModal onClose={() => setNewLinkModal(false)} />
|
|
||||||
) : undefined}
|
|
||||||
</MainLayout>
|
</MainLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -203,9 +203,9 @@ export default function Login({
|
||||||
{t("login")}
|
{t("login")}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{availableLogins.buttonAuths.length > 0 ? (
|
{availableLogins.buttonAuths.length > 0 && (
|
||||||
<div className="divider my-1">{t("or_continue_with")}</div>
|
<div className="divider my-1">{t("or_continue_with")}</div>
|
||||||
) : undefined}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -224,9 +224,9 @@ export default function Login({
|
||||||
loading={submitLoader}
|
loading={submitLoader}
|
||||||
>
|
>
|
||||||
{value.name.toLowerCase() === "google" ||
|
{value.name.toLowerCase() === "google" ||
|
||||||
value.name.toLowerCase() === "apple" ? (
|
(value.name.toLowerCase() === "apple" && (
|
||||||
<i className={"bi-" + value.name.toLowerCase()}></i>
|
<i className={"bi-" + value.name.toLowerCase()}></i>
|
||||||
) : undefined}
|
))}
|
||||||
{value.name}
|
{value.name}
|
||||||
</Button>
|
</Button>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
"use client";
|
"use client";
|
||||||
import getPublicCollectionData from "@/lib/client/getPublicCollectionData";
|
import getPublicCollectionData from "@/lib/client/getPublicCollectionData";
|
||||||
import {
|
import {
|
||||||
|
AccountSettings,
|
||||||
CollectionIncludingMembersAndLinkCount,
|
CollectionIncludingMembersAndLinkCount,
|
||||||
Sort,
|
Sort,
|
||||||
ViewMode,
|
ViewMode,
|
||||||
|
@ -29,15 +30,9 @@ export default function PublicCollections() {
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const [collectionOwner, setCollectionOwner] = useState({
|
const [collectionOwner, setCollectionOwner] = useState<
|
||||||
id: null as unknown as number,
|
Partial<AccountSettings>
|
||||||
name: "",
|
>({});
|
||||||
username: "",
|
|
||||||
image: "",
|
|
||||||
archiveAsScreenshot: undefined as unknown as boolean,
|
|
||||||
archiveAsMonolith: undefined as unknown as boolean,
|
|
||||||
archiveAsPDF: undefined as unknown as boolean,
|
|
||||||
});
|
|
||||||
|
|
||||||
const [searchFilter, setSearchFilter] = useState({
|
const [searchFilter, setSearchFilter] = useState({
|
||||||
name: true,
|
name: true,
|
||||||
|
@ -93,7 +88,9 @@ export default function PublicCollections() {
|
||||||
(localStorage.getItem("viewMode") as ViewMode) || ViewMode.Card
|
(localStorage.getItem("viewMode") as ViewMode) || ViewMode.Card
|
||||||
);
|
);
|
||||||
|
|
||||||
return collection ? (
|
if (!collection) return <></>;
|
||||||
|
else
|
||||||
|
return (
|
||||||
<div
|
<div
|
||||||
className="h-96"
|
className="h-96"
|
||||||
style={{
|
style={{
|
||||||
|
@ -102,7 +99,7 @@ export default function PublicCollections() {
|
||||||
} 13rem, ${settings.theme === "dark" ? "#171717" : "#ffffff"} 100%)`,
|
} 13rem, ${settings.theme === "dark" ? "#171717" : "#ffffff"} 100%)`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{collection ? (
|
{collection && (
|
||||||
<Head>
|
<Head>
|
||||||
<title>{collection.name} | Linkwarden</title>
|
<title>{collection.name} | Linkwarden</title>
|
||||||
<meta
|
<meta
|
||||||
|
@ -111,7 +108,7 @@ export default function PublicCollections() {
|
||||||
key="title"
|
key="title"
|
||||||
/>
|
/>
|
||||||
</Head>
|
</Head>
|
||||||
) : undefined}
|
)}
|
||||||
<div className="lg:w-3/4 w-full mx-auto p-5 bg">
|
<div className="lg:w-3/4 w-full mx-auto p-5 bg">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<p className="text-4xl font-thin mb-2 capitalize mt-10">
|
<p className="text-4xl font-thin mb-2 capitalize mt-10">
|
||||||
|
@ -140,12 +137,12 @@ export default function PublicCollections() {
|
||||||
className="flex items-center btn px-2 btn-ghost rounded-full"
|
className="flex items-center btn px-2 btn-ghost rounded-full"
|
||||||
onClick={() => setEditCollectionSharingModal(true)}
|
onClick={() => setEditCollectionSharingModal(true)}
|
||||||
>
|
>
|
||||||
{collectionOwner.id ? (
|
{collectionOwner.id && (
|
||||||
<ProfilePhoto
|
<ProfilePhoto
|
||||||
src={collectionOwner.image || undefined}
|
src={collectionOwner.image || undefined}
|
||||||
name={collectionOwner.name}
|
name={collectionOwner.name}
|
||||||
/>
|
/>
|
||||||
) : undefined}
|
)}
|
||||||
{collection.members
|
{collection.members
|
||||||
.sort((a, b) => (a.userId as number) - (b.userId as number))
|
.sort((a, b) => (a.userId as number) - (b.userId as number))
|
||||||
.map((e, i) => {
|
.map((e, i) => {
|
||||||
|
@ -159,13 +156,13 @@ export default function PublicCollections() {
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
.slice(0, 3)}
|
.slice(0, 3)}
|
||||||
{collection.members.length - 3 > 0 ? (
|
{collection.members.length - 3 > 0 && (
|
||||||
<div className={`avatar drop-shadow-md placeholder -ml-3`}>
|
<div className={`avatar drop-shadow-md placeholder -ml-3`}>
|
||||||
<div className="bg-base-100 text-neutral rounded-full w-8 h-8 ring-2 ring-neutral-content">
|
<div className="bg-base-100 text-neutral rounded-full w-8 h-8 ring-2 ring-neutral-content">
|
||||||
<span>+{collection.members.length - 3}</span>
|
<span>+{collection.members.length - 3}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="text-neutral text-sm">
|
<p className="text-neutral text-sm">
|
||||||
|
@ -230,22 +227,22 @@ export default function PublicCollections() {
|
||||||
placeholderCount={1}
|
placeholderCount={1}
|
||||||
useData={data}
|
useData={data}
|
||||||
/>
|
/>
|
||||||
{!data.isLoading && links && !links[0] && <p>{t("nothing_found")}</p>}
|
{!data.isLoading && links && !links[0] && (
|
||||||
|
<p>{t("nothing_found")}</p>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* <p className="text-center text-neutral">
|
{/* <p className="text-center text-neutral">
|
||||||
List created with <span className="text-black">Linkwarden.</span>
|
List created with <span className="text-black">Linkwarden.</span>
|
||||||
</p> */}
|
</p> */}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{editCollectionSharingModal ? (
|
{editCollectionSharingModal && (
|
||||||
<EditCollectionSharingModal
|
<EditCollectionSharingModal
|
||||||
onClose={() => setEditCollectionSharingModal(false)}
|
onClose={() => setEditCollectionSharingModal(false)}
|
||||||
activeCollection={collection}
|
activeCollection={collection}
|
||||||
/>
|
/>
|
||||||
) : undefined}
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
|
||||||
<></>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -133,9 +133,9 @@ export default function Register({
|
||||||
loading={submitLoader}
|
loading={submitLoader}
|
||||||
>
|
>
|
||||||
{value.name.toLowerCase() === "google" ||
|
{value.name.toLowerCase() === "google" ||
|
||||||
value.name.toLowerCase() === "apple" ? (
|
(value.name.toLowerCase() === "apple" && (
|
||||||
<i className={"bi-" + value.name.toLowerCase()}></i>
|
<i className={"bi-" + value.name.toLowerCase()}></i>
|
||||||
) : undefined}
|
))}
|
||||||
{value.name}
|
{value.name}
|
||||||
</Button>
|
</Button>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
|
@ -201,7 +201,7 @@ export default function Register({
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{emailEnabled ? (
|
{emailEnabled && (
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm w-fit font-semibold mb-1">{t("email")}</p>
|
<p className="text-sm w-fit font-semibold mb-1">{t("email")}</p>
|
||||||
|
|
||||||
|
@ -214,7 +214,7 @@ export default function Register({
|
||||||
onChange={(e) => setForm({ ...form, email: e.target.value })}
|
onChange={(e) => setForm({ ...form, email: e.target.value })}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : undefined}
|
)}
|
||||||
|
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<p className="text-sm w-fit font-semibold mb-1">
|
<p className="text-sm w-fit font-semibold mb-1">
|
||||||
|
@ -248,7 +248,7 @@ export default function Register({
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{process.env.NEXT_PUBLIC_STRIPE ? (
|
{process.env.NEXT_PUBLIC_STRIPE && (
|
||||||
<div className="text-xs text-neutral mb-3">
|
<div className="text-xs text-neutral mb-3">
|
||||||
<p>
|
<p>
|
||||||
<Trans
|
<Trans
|
||||||
|
@ -270,7 +270,7 @@ export default function Register({
|
||||||
/>
|
/>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : undefined}
|
)}
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
|
@ -282,9 +282,9 @@ export default function Register({
|
||||||
{t("sign_up")}
|
{t("sign_up")}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{availableLogins.buttonAuths.length > 0 ? (
|
{availableLogins.buttonAuths.length > 0 && (
|
||||||
<div className="divider my-1">{t("or_continue_with")}</div>
|
<div className="divider my-1">{t("or_continue_with")}</div>
|
||||||
) : undefined}
|
)}
|
||||||
|
|
||||||
{displayLoginExternalButton()}
|
{displayLoginExternalButton()}
|
||||||
<div>
|
<div>
|
||||||
|
@ -298,7 +298,7 @@ export default function Register({
|
||||||
{t("login")}
|
{t("login")}
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
{process.env.NEXT_PUBLIC_STRIPE ? (
|
{process.env.NEXT_PUBLIC_STRIPE && (
|
||||||
<div className="text-neutral text-center flex items-baseline gap-1 justify-center">
|
<div className="text-neutral text-center flex items-baseline gap-1 justify-center">
|
||||||
<p>{t("need_help")}</p>
|
<p>{t("need_help")}</p>
|
||||||
<Link
|
<Link
|
||||||
|
@ -309,7 +309,7 @@ export default function Register({
|
||||||
{t("get_in_touch")}
|
{t("get_in_touch")}
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
) : undefined}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
@ -40,7 +40,7 @@ export default function AccessTokens() {
|
||||||
{t("new_token")}
|
{t("new_token")}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{tokens.length > 0 ? (
|
{tokens.length > 0 && (
|
||||||
<table className="table mt-2 overflow-x-auto">
|
<table className="table mt-2 overflow-x-auto">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -85,12 +85,12 @@ export default function AccessTokens() {
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
) : undefined}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{newTokenModal ? (
|
{newTokenModal && (
|
||||||
<NewTokenModal onClose={() => setNewTokenModal(false)} />
|
<NewTokenModal onClose={() => setNewTokenModal(false)} />
|
||||||
) : undefined}
|
)}
|
||||||
{revokeTokenModal && selectedToken && (
|
{revokeTokenModal && selectedToken && (
|
||||||
<RevokeTokenModal
|
<RevokeTokenModal
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect, ChangeEvent } from "react";
|
||||||
import { AccountSettings } from "@/types/global";
|
import { AccountSettings } from "@/types/global";
|
||||||
import { toast } from "react-hot-toast";
|
import { toast } from "react-hot-toast";
|
||||||
import SettingsLayout from "@/layouts/SettingsLayout";
|
import SettingsLayout from "@/layouts/SettingsLayout";
|
||||||
|
@ -55,8 +55,10 @@ export default function Account() {
|
||||||
if (!objectIsEmpty(account)) setUser({ ...account });
|
if (!objectIsEmpty(account)) setUser({ ...account });
|
||||||
}, [account]);
|
}, [account]);
|
||||||
|
|
||||||
const handleImageUpload = async (e: any) => {
|
const handleImageUpload = async (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
const file: File = e.target.files[0];
|
const file = e.target.files?.[0];
|
||||||
|
if (!file) return toast.error(t("image_upload_no_file_error"));
|
||||||
|
|
||||||
const fileExtension = file.name.split(".").pop()?.toLowerCase();
|
const fileExtension = file.name.split(".").pop()?.toLowerCase();
|
||||||
const allowedExtensions = ["png", "jpeg", "jpg"];
|
const allowedExtensions = ["png", "jpeg", "jpg"];
|
||||||
if (allowedExtensions.includes(fileExtension as string)) {
|
if (allowedExtensions.includes(fileExtension as string)) {
|
||||||
|
@ -114,9 +116,13 @@ export default function Account() {
|
||||||
setSubmitLoader(false);
|
setSubmitLoader(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const importBookmarks = async (e: any, format: MigrationFormat) => {
|
const importBookmarks = async (
|
||||||
|
e: ChangeEvent<HTMLInputElement>,
|
||||||
|
format: MigrationFormat
|
||||||
|
) => {
|
||||||
setSubmitLoader(true);
|
setSubmitLoader(true);
|
||||||
const file: File = e.target.files[0];
|
const file = e.target.files?.[0];
|
||||||
|
|
||||||
if (file) {
|
if (file) {
|
||||||
var reader = new FileReader();
|
var reader = new FileReader();
|
||||||
reader.readAsText(file, "UTF-8");
|
reader.readAsText(file, "UTF-8");
|
||||||
|
@ -190,7 +196,7 @@ export default function Account() {
|
||||||
onChange={(e) => setUser({ ...user, username: e.target.value })}
|
onChange={(e) => setUser({ ...user, username: e.target.value })}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{emailEnabled ? (
|
{emailEnabled && (
|
||||||
<div>
|
<div>
|
||||||
<p className="mb-2">{t("email")}</p>
|
<p className="mb-2">{t("email")}</p>
|
||||||
<TextInput
|
<TextInput
|
||||||
|
@ -199,7 +205,7 @@ export default function Account() {
|
||||||
onChange={(e) => setUser({ ...user, email: e.target.value })}
|
onChange={(e) => setUser({ ...user, email: e.target.value })}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : undefined}
|
)}
|
||||||
<div>
|
<div>
|
||||||
<p className="mb-2">{t("language")}</p>
|
<p className="mb-2">{t("language")}</p>
|
||||||
<select
|
<select
|
||||||
|
@ -437,9 +443,8 @@ export default function Account() {
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
{t("delete_account_warning")}
|
{t("delete_account_warning")}
|
||||||
{process.env.NEXT_PUBLIC_STRIPE
|
{process.env.NEXT_PUBLIC_STRIPE &&
|
||||||
? " " + t("cancel_subscription_notice")
|
" " + t("cancel_subscription_notice")}
|
||||||
: undefined}
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -448,14 +453,14 @@ export default function Account() {
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{emailChangeVerificationModal ? (
|
{emailChangeVerificationModal && (
|
||||||
<EmailChangeVerificationModal
|
<EmailChangeVerificationModal
|
||||||
onClose={() => setEmailChangeVerificationModal(false)}
|
onClose={() => setEmailChangeVerificationModal(false)}
|
||||||
onSubmit={submit}
|
onSubmit={submit}
|
||||||
oldEmail={account.email || ""}
|
oldEmail={account.email || ""}
|
||||||
newEmail={user.email || ""}
|
newEmail={user.email || ""}
|
||||||
/>
|
/>
|
||||||
) : undefined}
|
)}
|
||||||
</SettingsLayout>
|
</SettingsLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -83,7 +83,7 @@ export default function Delete() {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{process.env.NEXT_PUBLIC_STRIPE ? (
|
{process.env.NEXT_PUBLIC_STRIPE && (
|
||||||
<fieldset className="border rounded-md p-2 border-primary">
|
<fieldset className="border rounded-md p-2 border-primary">
|
||||||
<legend className="px-3 py-1 text-sm sm:text-base border rounded-md border-primary">
|
<legend className="px-3 py-1 text-sm sm:text-base border rounded-md border-primary">
|
||||||
<b>{t("optional")}</b> <i>{t("feedback_help")}</i>
|
<b>{t("optional")}</b> <i>{t("feedback_help")}</i>
|
||||||
|
@ -123,7 +123,7 @@ export default function Delete() {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
) : undefined}
|
)}
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
className="mx-auto"
|
className="mx-auto"
|
||||||
|
|
|
@ -117,6 +117,7 @@
|
||||||
"applying_settings": "Applying settings...",
|
"applying_settings": "Applying settings...",
|
||||||
"settings_applied": "Settings Applied!",
|
"settings_applied": "Settings Applied!",
|
||||||
"email_change_request": "Email change request sent. Please verify the new email address.",
|
"email_change_request": "Email change request sent. Please verify the new email address.",
|
||||||
|
"image_upload_no_file_error": "No file selected. Please choose an image to upload.",
|
||||||
"image_upload_size_error": "Please select a PNG or JPEG file that's less than 1MB.",
|
"image_upload_size_error": "Please select a PNG or JPEG file that's less than 1MB.",
|
||||||
"image_upload_format_error": "Invalid file format.",
|
"image_upload_format_error": "Invalid file format.",
|
||||||
"importing_bookmarks": "Importing bookmarks...",
|
"importing_bookmarks": "Importing bookmarks...",
|
||||||
|
|
|
@ -117,6 +117,7 @@
|
||||||
"applying_settings": "Applicazione delle impostazioni in corso...",
|
"applying_settings": "Applicazione delle impostazioni in corso...",
|
||||||
"settings_applied": "Impostazioni Applicate!",
|
"settings_applied": "Impostazioni Applicate!",
|
||||||
"email_change_request": "Richiesta di cambio email inviata. Per favore verifica il nuovo indirizzo email.",
|
"email_change_request": "Richiesta di cambio email inviata. Per favore verifica il nuovo indirizzo email.",
|
||||||
|
"image_upload_no_file_error": "Nessun file selezionato. Scegli un'immagine da caricare.",
|
||||||
"image_upload_size_error": "Per favore seleziona un file PNG o JPEG di dimensioni inferiori a 1MB.",
|
"image_upload_size_error": "Per favore seleziona un file PNG o JPEG di dimensioni inferiori a 1MB.",
|
||||||
"image_upload_format_error": "Formato file non valido.",
|
"image_upload_format_error": "Formato file non valido.",
|
||||||
"importing_bookmarks": "Importazione dei segnalibri in corso...",
|
"importing_bookmarks": "Importazione dei segnalibri in corso...",
|
||||||
|
|
|
@ -80,6 +80,8 @@ export enum Sort {
|
||||||
DescriptionZA,
|
DescriptionZA,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type Order = { [key: string]: "asc" | "desc" };
|
||||||
|
|
||||||
export type LinkRequestQuery = {
|
export type LinkRequestQuery = {
|
||||||
sort?: Sort;
|
sort?: Sort;
|
||||||
cursor?: number;
|
cursor?: number;
|
||||||
|
|
Ŝarĝante…
Reference in New Issue