refactored link state management + a lot of other changes...

This commit is contained in:
daniel31x13 2024-08-13 00:08:57 -04:00
parent a73e5fa6c6
commit 80f366cd7b
58 changed files with 1302 additions and 819 deletions

View File

@ -20,7 +20,7 @@ type Props = {
export default function CollectionCard({ collection, className }: Props) {
const { t } = useTranslation();
const { settings } = useLocalSettingsStore();
const { data: user } = useUser();
const { data: user = {} } = useUser();
const formattedDate = new Date(collection.createdAt as string).toLocaleString(
"en-US",

View File

@ -25,9 +25,9 @@ interface ExtendedTreeItem extends TreeItem {
const CollectionListing = () => {
const { t } = useTranslation();
const updateCollection = useUpdateCollection();
const { data: collections } = useCollections();
const { data: collections = [] } = useCollections();
const { data: user } = useUser();
const { data: user = {} } = useUser();
const updateUser = useUpdateUser();
const router = useRouter();

View File

@ -24,7 +24,7 @@ export default function CollectionSelection({
showDefaultValue = true,
creatable = true,
}: Props) {
const { data: collections } = useCollections();
const { data: collections = [] } = useCollections();
const router = useRouter();

View File

@ -13,7 +13,7 @@ type Props = {
};
export default function TagSelection({ onChange, defaultValue }: Props) {
const { data: tags } = useTags();
const { data: tags = [] } = useTags();
const [options, setOptions] = useState<Options[]>([]);

View File

@ -5,17 +5,17 @@ import ViewDropdown from "./ViewDropdown";
import { TFunction } from "i18next";
import BulkDeleteLinksModal from "./ModalContent/BulkDeleteLinksModal";
import BulkEditLinksModal from "./ModalContent/BulkEditLinksModal";
import toast from "react-hot-toast";
import useCollectivePermissions from "@/hooks/useCollectivePermissions";
import { useRouter } from "next/router";
import useLinkStore from "@/store/links";
import { Sort } from "@/types/global";
import { Sort, ViewMode } from "@/types/global";
import { useBulkDeleteLinks, useLinks } from "@/hooks/store/links";
type Props = {
children: React.ReactNode;
t: TFunction<"translation", undefined>;
viewMode: string;
setViewMode: Dispatch<SetStateAction<string>>;
viewMode: ViewMode;
setViewMode: Dispatch<SetStateAction<ViewMode>>;
searchFilter?: {
name: boolean;
url: boolean;
@ -48,8 +48,11 @@ const LinkListOptions = ({
editMode,
setEditMode,
}: Props) => {
const { links, selectedLinks, setSelectedLinks, deleteLinksById } =
useLinkStore();
const { selectedLinks, setSelectedLinks } = useLinkStore();
const deleteLinksById = useBulkDeleteLinks();
const { links } = useLinks();
const router = useRouter();
@ -73,23 +76,14 @@ const LinkListOptions = ({
};
const bulkDeleteLinks = async () => {
const load = toast.loading(t("deleting_selections"));
const response = await deleteLinksById(
selectedLinks.map((link) => link.id as number)
);
toast.dismiss(load);
if (response.ok) {
toast.success(
selectedLinks.length === 1
? t("link_deleted")
: t("links_deleted", { count: selectedLinks.length })
);
} else {
toast.error(response.data as string);
await deleteLinksById.mutateAsync(
selectedLinks.map((link) => link.id as number),
{
onSuccess: () => {
setSelectedLinks([]);
},
}
);
};
return (
@ -99,7 +93,10 @@ const LinkListOptions = ({
<div className="flex gap-3 items-center justify-end">
<div className="flex gap-2 items-center mt-2">
{links.length > 0 && editMode !== undefined && setEditMode && (
{links &&
links.length > 0 &&
editMode !== undefined &&
setEditMode && (
<div
role="button"
onClick={() => {
@ -121,15 +118,20 @@ const LinkListOptions = ({
setSearchFilter={setSearchFilter}
/>
)}
<SortDropdown sortBy={sortBy} setSort={setSortBy} t={t} />
<SortDropdown
sortBy={sortBy}
setSort={(value) => {
setSortBy(value);
}}
t={t}
/>
<ViewDropdown viewMode={viewMode} setViewMode={setViewMode} />
</div>
</div>
</div>
{editMode && links.length > 0 && (
{links && editMode && links.length > 0 && (
<div className="w-full flex justify-between items-center min-h-[32px]">
{links.length > 0 && (
<div className="flex gap-3 ml-3">
<input
type="checkbox"
@ -149,7 +151,6 @@ const LinkListOptions = ({
<span>{t("nothing_selected")}</span>
)}
</div>
)}
<div className="flex gap-3">
<button
onClick={() => setBulkEditLinksModal(true)}

View File

@ -1,6 +1,8 @@
import LinkCard from "@/components/LinkViews/LinkCard";
import { useLinks } from "@/hooks/store/links";
import { LinkIncludingShortenedCollectionAndTags } from "@/types/global";
import { link } from "fs";
import { useEffect } from "react";
import { useInView } from "react-intersection-observer";
import { GridLoader } from "react-spinners";
export default function CardView({
@ -12,6 +14,16 @@ export default function CardView({
editMode?: boolean;
isLoading?: boolean;
}) {
const { ref, inView } = useInView();
const { data } = useLinks();
useEffect(() => {
if (inView) {
data.fetchNextPage();
}
}, [data.fetchNextPage, inView]);
return (
<div className="grid min-[1901px]:grid-cols-5 min-[1501px]:grid-cols-4 min-[881px]:grid-cols-3 min-[551px]:grid-cols-2 grid-cols-1 gap-5 pb-5">
{links.map((e, i) => {
@ -26,6 +38,15 @@ export default function CardView({
);
})}
{data.hasNextPage && (
<div className="flex flex-col gap-4" ref={ref}>
<div className="skeleton h-32 w-full"></div>
<div className="skeleton h-4 w-28"></div>
<div className="skeleton h-4 w-full"></div>
<div className="skeleton h-4 w-full"></div>
</div>
)}
{isLoading && links.length > 0 && (
<GridLoader
color="oklch(var(--p))"

View File

@ -21,6 +21,7 @@ import LinkTypeBadge from "./LinkComponents/LinkTypeBadge";
import { useTranslation } from "next-i18next";
import { useCollections } from "@/hooks/store/collections";
import { useUser } from "@/hooks/store/user";
import { useGetLink, useLinks } from "@/hooks/store/links";
type Props = {
link: LinkIncludingShortenedCollectionAndTags;
@ -33,12 +34,16 @@ type Props = {
export default function LinkCard({ link, flipDropdown, editMode }: Props) {
const { t } = useTranslation();
const viewMode = localStorage.getItem("viewMode") || "card";
const { data: collections } = useCollections();
const { data: collections = [] } = useCollections();
const { data: user } = useUser();
const { data: user = {} } = useUser();
const { links, getLink, setSelectedLinks, selectedLinks } = useLinkStore();
const { setSelectedLinks, selectedLinks } = useLinkStore();
const {
data: { data: links = [] },
} = useLinks();
const getLink = useGetLink();
useEffect(() => {
if (!editMode) {
@ -94,7 +99,7 @@ export default function LinkCard({ link, flipDropdown, editMode }: Props) {
link.preview !== "unavailable"
) {
interval = setInterval(async () => {
getLink(link.id as number);
getLink.mutateAsync(link.id as number);
}, 5000);
}

View File

@ -7,11 +7,10 @@ import usePermissions from "@/hooks/usePermissions";
import EditLinkModal from "@/components/ModalContent/EditLinkModal";
import DeleteLinkModal from "@/components/ModalContent/DeleteLinkModal";
import PreservedFormatsModal from "@/components/ModalContent/PreservedFormatsModal";
import useLinkStore from "@/store/links";
import { toast } from "react-hot-toast";
import { dropdownTriggerer } from "@/lib/client/utils";
import { useTranslation } from "next-i18next";
import { useUser } from "@/hooks/store/user";
import { useDeleteLink, useUpdateLink } from "@/hooks/store/links";
type Props = {
link: LinkIncludingShortenedCollectionAndTags;
@ -39,41 +38,18 @@ export default function LinkActions({
const [deleteLinkModal, setDeleteLinkModal] = useState(false);
const [preservedFormatsModal, setPreservedFormatsModal] = useState(false);
const { data: user } = useUser();
const { data: user = {} } = useUser();
const { removeLink, updateLink } = useLinkStore();
const updateLink = useUpdateLink();
const deleteLink = useDeleteLink();
const pinLink = async () => {
const isAlreadyPinned = link?.pinnedBy && link.pinnedBy[0];
const load = toast.loading(t("applying"));
const response = await updateLink({
await updateLink.mutateAsync({
...link,
pinnedBy: isAlreadyPinned ? undefined : [{ id: user.id }],
});
toast.dismiss(load);
if (response.ok) {
toast.success(isAlreadyPinned ? t("link_unpinned") : t("link_unpinned"));
} else {
toast.error(response.data as string);
}
};
const deleteLink = async () => {
const load = toast.loading(t("deleting"));
const response = await removeLink(link.id as number);
toast.dismiss(load);
if (response.ok) {
toast.success(t("deleted"));
} else {
toast.error(response.data as string);
}
};
return (
@ -157,9 +133,11 @@ export default function LinkActions({
<div
role="button"
tabIndex={0}
onClick={(e) => {
onClick={async (e) => {
(document?.activeElement as HTMLElement)?.blur();
e.shiftKey ? deleteLink() : setDeleteLinkModal(true);
e.shiftKey
? await deleteLink.mutateAsync(link.id as number)
: setDeleteLinkModal(true);
}}
>
{t("delete")}

View File

@ -3,7 +3,6 @@ import {
LinkIncludingShortenedCollectionAndTags,
} from "@/types/global";
import Link from "next/link";
import { useRouter } from "next/router";
import React from "react";
export default function LinkCollection({
@ -13,9 +12,8 @@ export default function LinkCollection({
link: LinkIncludingShortenedCollectionAndTags;
collection: CollectionIncludingMembersAndLinkCount;
}) {
const router = useRouter();
return (
<>
<Link
href={`/collections/${link.collection.id}`}
onClick={(e) => {
@ -30,5 +28,6 @@ export default function LinkCollection({
></i>
<p className="truncate capitalize">{collection?.name}</p>
</Link>
</>
);
}

View File

@ -17,6 +17,7 @@ import LinkTypeBadge from "./LinkComponents/LinkTypeBadge";
import { useTranslation } from "next-i18next";
import { useCollections } from "@/hooks/store/collections";
import { useUser } from "@/hooks/store/user";
import { useLinks } from "@/hooks/store/links";
type Props = {
link: LinkIncludingShortenedCollectionAndTags;
@ -33,10 +34,12 @@ export default function LinkCardCompact({
}: Props) {
const { t } = useTranslation();
const { data: collections } = useCollections();
const { data: collections = [] } = useCollections();
const { data: user } = useUser();
const { links, setSelectedLinks, selectedLinks } = useLinkStore();
const { data: user = {} } = useUser();
const { setSelectedLinks, selectedLinks } = useLinkStore();
const { links } = useLinks();
useEffect(() => {
if (!editMode) {

View File

@ -21,6 +21,7 @@ import LinkTypeBadge from "./LinkComponents/LinkTypeBadge";
import { useTranslation } from "next-i18next";
import { useCollections } from "@/hooks/store/collections";
import { useUser } from "@/hooks/store/user";
import { useGetLink, useLinks } from "@/hooks/store/links";
type Props = {
link: LinkIncludingShortenedCollectionAndTags;
@ -33,10 +34,13 @@ type Props = {
export default function LinkMasonry({ link, flipDropdown, editMode }: Props) {
const { t } = useTranslation();
const { data: collections } = useCollections();
const { data: user } = useUser();
const { data: collections = [] } = useCollections();
const { data: user = {} } = useUser();
const { links, getLink, setSelectedLinks, selectedLinks } = useLinkStore();
const { setSelectedLinks, selectedLinks } = useLinkStore();
const { links } = useLinks();
const getLink = useGetLink();
useEffect(() => {
if (!editMode) {
@ -92,7 +96,7 @@ export default function LinkMasonry({ link, flipDropdown, editMode }: Props) {
link.preview !== "unavailable"
) {
interval = setInterval(async () => {
getLink(link.id as number);
getLink.mutateAsync(link.id as number);
}, 5000);
}

View File

@ -0,0 +1,226 @@
import LinkCard from "@/components/LinkViews/LinkCard";
import { useLinks } from "@/hooks/store/links";
import {
LinkIncludingShortenedCollectionAndTags,
ViewMode,
} from "@/types/global";
import { useEffect, useState } from "react";
import { useInView } from "react-intersection-observer";
import { GridLoader } from "react-spinners";
import LinkMasonry from "@/components/LinkViews/LinkMasonry";
import Masonry from "react-masonry-css";
import resolveConfig from "tailwindcss/resolveConfig";
import tailwindConfig from "../../tailwind.config.js";
import { useMemo } from "react";
import LinkList from "@/components/LinkViews/LinkList";
export function CardView({
links,
editMode,
isLoading,
placeholders,
hasNextPage,
placeHolderRef,
}: {
links?: LinkIncludingShortenedCollectionAndTags[];
editMode?: boolean;
isLoading?: boolean;
placeholders?: number[];
hasNextPage?: boolean;
placeHolderRef?: any;
}) {
return (
<div className="grid min-[1901px]:grid-cols-5 min-[1501px]:grid-cols-4 min-[881px]:grid-cols-3 min-[551px]:grid-cols-2 grid-cols-1 gap-5 pb-5">
{links?.map((e, i) => {
return (
<LinkCard
key={i}
link={e}
count={i}
flipDropdown={i === links.length - 1}
editMode={editMode}
/>
);
})}
{(hasNextPage || isLoading) &&
placeholders?.map((e, i) => {
return (
<div
className="flex flex-col gap-4"
ref={e === 1 ? placeHolderRef : undefined}
key={i}
>
<div className="skeleton h-40 w-full"></div>
<div className="skeleton h-3 w-2/3"></div>
<div className="skeleton h-3 w-full"></div>
<div className="skeleton h-3 w-full"></div>
<div className="skeleton h-3 w-1/3"></div>
</div>
);
})}
{/* {isLoading && links.length > 0 && (
<GridLoader
color="oklch(var(--p))"
loading={true}
size={20}
className="fixed top-5 right-5 opacity-50 z-30"
/>
)} */}
</div>
);
}
export function ListView({
links,
editMode,
isLoading,
placeholders,
hasNextPage,
placeHolderRef,
}: {
links?: LinkIncludingShortenedCollectionAndTags[];
editMode?: boolean;
isLoading?: boolean;
placeholders?: number[];
hasNextPage?: boolean;
placeHolderRef?: any;
}) {
return (
<div className="flex gap-1 flex-col">
{links?.map((e, i) => {
return (
<LinkList
key={i}
link={e}
count={i}
flipDropdown={i === links.length - 1}
editMode={editMode}
/>
);
})}
{/* {isLoading && links.length > 0 && (
<GridLoader
color="oklch(var(--p))"
loading={true}
size={20}
className="fixed top-5 right-5 opacity-50 z-30"
/>
)} */}
</div>
);
}
export function MasonryView({
links,
editMode,
isLoading,
placeholders,
hasNextPage,
placeHolderRef,
}: {
links?: LinkIncludingShortenedCollectionAndTags[];
editMode?: boolean;
isLoading?: boolean;
placeholders?: number[];
hasNextPage?: boolean;
placeHolderRef?: any;
}) {
const fullConfig = resolveConfig(tailwindConfig as any);
const breakpointColumnsObj = useMemo(() => {
return {
default: 5,
1900: 4,
1500: 3,
880: 2,
550: 1,
};
}, []);
return (
<Masonry
breakpointCols={breakpointColumnsObj}
columnClassName="flex flex-col gap-5 !w-full"
className="grid min-[1901px]:grid-cols-5 min-[1501px]:grid-cols-4 min-[881px]:grid-cols-3 min-[551px]:grid-cols-2 grid-cols-1 gap-5 pb-5"
>
{links?.map((e, i) => {
return (
<LinkMasonry
key={i}
link={e}
count={i}
flipDropdown={i === links.length - 1}
editMode={editMode}
/>
);
})}
{/* {isLoading && links.length > 0 && (
<GridLoader
color="oklch(var(--p))"
loading={true}
size={20}
className="fixed top-5 right-5 opacity-50 z-30"
/>
)} */}
</Masonry>
);
}
export default function Links({
layout,
links,
editMode,
placeholderCount,
useData,
}: {
layout: ViewMode;
links?: LinkIncludingShortenedCollectionAndTags[];
editMode?: boolean;
placeholderCount?: number;
useData?: any;
}) {
const { ref, inView } = useInView();
useEffect(() => {
if (inView && useData?.fetchNextPage && useData?.hasNextPage) {
useData.fetchNextPage();
}
}, [useData, inView]);
if (layout === ViewMode.List) {
return (
<ListView
links={links}
editMode={editMode}
isLoading={useData?.isLoading}
/>
);
} else if (layout === ViewMode.Masonry) {
return (
<MasonryView
links={links}
editMode={editMode}
isLoading={useData?.isLoading}
/>
);
} else {
// Default to card view
return (
<CardView
links={links}
editMode={editMode}
isLoading={useData?.isLoading}
placeholders={placeholderCountToArray(placeholderCount)}
hasNextPage={useData?.hasNextPage}
placeHolderRef={ref}
/>
);
}
}
const placeholderCountToArray = (num?: number) =>
num ? Array.from({ length: num }, (_, i) => i + 1) : [];

View File

@ -1,9 +1,9 @@
import React from "react";
import useLinkStore from "@/store/links";
import toast from "react-hot-toast";
import Modal from "../Modal";
import Button from "../ui/Button";
import { useTranslation } from "next-i18next";
import { useBulkDeleteLinks } from "@/hooks/store/links";
type Props = {
onClose: Function;
@ -11,22 +11,20 @@ type Props = {
export default function BulkDeleteLinksModal({ onClose }: Props) {
const { t } = useTranslation();
const { selectedLinks, setSelectedLinks, deleteLinksById } = useLinkStore();
const { selectedLinks, setSelectedLinks } = useLinkStore();
const deleteLinksById = useBulkDeleteLinks();
const deleteLink = async () => {
const load = toast.loading(t("deleting"));
const response = await deleteLinksById(
selectedLinks.map((link) => link.id as number)
);
toast.dismiss(load);
if (response.ok) {
toast.success(t("deleted"));
await deleteLinksById.mutateAsync(
selectedLinks.map((link) => link.id as number),
{
onSuccess: () => {
setSelectedLinks([]);
onClose();
} else toast.error(response.data as string);
},
}
);
};
return (

View File

@ -1,11 +1,10 @@
import React, { useEffect, useState } from "react";
import useLinkStore from "@/store/links";
import { LinkIncludingShortenedCollectionAndTags } from "@/types/global";
import toast from "react-hot-toast";
import Modal from "../Modal";
import { useRouter } from "next/router";
import Button from "../ui/Button";
import { useTranslation } from "next-i18next";
import { useDeleteLink } from "@/hooks/store/links";
type Props = {
onClose: Function;
@ -16,31 +15,24 @@ export default function DeleteLinkModal({ onClose, activeLink }: Props) {
const { t } = useTranslation();
const [link, setLink] =
useState<LinkIncludingShortenedCollectionAndTags>(activeLink);
const { removeLink } = useLinkStore();
const deleteLink = useDeleteLink();
const router = useRouter();
useEffect(() => {
setLink(activeLink);
}, []);
const deleteLink = async () => {
const load = toast.loading(t("deleting"));
const response = await removeLink(link.id as number);
toast.dismiss(load);
if (response.ok) {
toast.success(t("deleted"));
} else {
toast.error(response.data as string);
}
const submit = async () => {
await deleteLink.mutateAsync(link.id as number, {
onSuccess: () => {
if (router.pathname.startsWith("/links/[id]")) {
router.push("/dashboard");
}
onClose();
},
});
};
return (
@ -61,7 +53,7 @@ export default function DeleteLinkModal({ onClose, activeLink }: Props) {
<p>{t("shift_key_tip")}</p>
<Button className="ml-auto" intent="destructive" onClick={deleteLink}>
<Button className="ml-auto" intent="destructive" onClick={submit}>
<i className="bi-trash text-xl" />
{t("delete")}
</Button>

View File

@ -46,7 +46,7 @@ export default function EditCollectionSharingModal({
}
};
const { data: user } = useUser();
const { data: user = {} } = useUser();
const permissions = usePermissions(collection.id as number);
const currentURL = new URL(document.URL);

View File

@ -3,12 +3,11 @@ import CollectionSelection from "@/components/InputSelect/CollectionSelection";
import TagSelection from "@/components/InputSelect/TagSelection";
import TextInput from "@/components/TextInput";
import unescapeString from "@/lib/client/unescapeString";
import useLinkStore from "@/store/links";
import { LinkIncludingShortenedCollectionAndTags } from "@/types/global";
import toast from "react-hot-toast";
import Link from "next/link";
import Modal from "../Modal";
import { useTranslation } from "next-i18next";
import { useUpdateLink } from "@/hooks/store/links";
type Props = {
onClose: Function;
@ -27,9 +26,10 @@ export default function EditLinkModal({ onClose, activeLink }: Props) {
console.log(error);
}
const { updateLink } = useLinkStore();
const [submitLoader, setSubmitLoader] = useState(false);
const updateLink = useUpdateLink();
const setCollection = (e: any) => {
if (e?.__isNew__) e.value = null;
setLink({
@ -50,19 +50,14 @@ export default function EditLinkModal({ onClose, activeLink }: Props) {
const submit = async () => {
if (!submitLoader) {
setSubmitLoader(true);
const load = toast.loading(t("updating"));
let response = await updateLink(link);
toast.dismiss(load);
if (response.ok) {
toast.success(t("updated"));
await updateLink.mutateAsync(link, {
onSuccess: () => {
onClose();
} else {
toast.error(response.data as string);
}
},
});
setSubmitLoader(false);
return response;
}
};

View File

@ -3,14 +3,13 @@ import CollectionSelection from "@/components/InputSelect/CollectionSelection";
import TagSelection from "@/components/InputSelect/TagSelection";
import TextInput from "@/components/TextInput";
import unescapeString from "@/lib/client/unescapeString";
import useLinkStore from "@/store/links";
import { LinkIncludingShortenedCollectionAndTags } from "@/types/global";
import { useSession } from "next-auth/react";
import { useRouter } from "next/router";
import toast from "react-hot-toast";
import Modal from "../Modal";
import { useTranslation } from "next-i18next";
import { useCollections } from "@/hooks/store/collections";
import { useAddLink } from "@/hooks/store/links";
type Props = {
onClose: Function;
@ -39,11 +38,13 @@ export default function NewLinkModal({ onClose }: Props) {
const [link, setLink] =
useState<LinkIncludingShortenedCollectionAndTags>(initial);
const { addLink } = useLinkStore();
const addLink = useAddLink();
const [submitLoader, setSubmitLoader] = useState(false);
const [optionsExpanded, setOptionsExpanded] = useState(false);
const router = useRouter();
const { data: collections } = useCollections();
const { data: collections = [] } = useCollections();
const setCollection = (e: any) => {
if (e?.__isNew__) e.value = null;
@ -86,15 +87,13 @@ export default function NewLinkModal({ onClose }: Props) {
const submit = async () => {
if (!submitLoader) {
setSubmitLoader(true);
const load = toast.loading(t("creating_link"));
const response = await addLink(link);
toast.dismiss(load);
if (response.ok) {
toast.success(t("link_created"));
await addLink.mutateAsync(link, {
onSuccess: () => {
onClose();
} else {
toast.error(response.data as string);
}
},
});
setSubmitLoader(false);
}
};

View File

@ -1,5 +1,4 @@
import React, { useEffect, useState } from "react";
import useLinkStore from "@/store/links";
import {
LinkIncludingShortenedCollectionAndTags,
ArchivedFormat,
@ -20,6 +19,7 @@ import getPublicUserData from "@/lib/client/getPublicUserData";
import { useTranslation } from "next-i18next";
import { BeatLoader } from "react-spinners";
import { useUser } from "@/hooks/store/user";
import { useGetLink } from "@/hooks/store/links";
type Props = {
onClose: Function;
@ -29,8 +29,8 @@ type Props = {
export default function PreservedFormatsModal({ onClose, activeLink }: Props) {
const { t } = useTranslation();
const session = useSession();
const { getLink } = useLinkStore();
const { data: user } = useUser();
const getLink = useGetLink();
const { data: user = {} } = useUser();
const [link, setLink] =
useState<LinkIncludingShortenedCollectionAndTags>(activeLink);
const router = useRouter();
@ -98,7 +98,7 @@ export default function PreservedFormatsModal({ onClose, activeLink }: Props) {
useEffect(() => {
(async () => {
const data = await getLink(link.id as number, isPublic);
const data = await getLink.mutateAsync(link.id as number);
setLink(
(data as any).response as LinkIncludingShortenedCollectionAndTags
);
@ -108,7 +108,7 @@ export default function PreservedFormatsModal({ onClose, activeLink }: Props) {
if (!isReady()) {
interval = setInterval(async () => {
const data = await getLink(link.id as number, isPublic);
const data = await getLink.mutateAsync(link.id as number);
setLink(
(data as any).response as LinkIncludingShortenedCollectionAndTags
);
@ -137,7 +137,7 @@ export default function PreservedFormatsModal({ onClose, activeLink }: Props) {
toast.dismiss(load);
if (response.ok) {
const newLink = await getLink(link?.id as number);
const newLink = await getLink.mutateAsync(link?.id as number);
setLink(
(newLink as any).response as LinkIncludingShortenedCollectionAndTags
);

View File

@ -3,7 +3,6 @@ import CollectionSelection from "@/components/InputSelect/CollectionSelection";
import TagSelection from "@/components/InputSelect/TagSelection";
import TextInput from "@/components/TextInput";
import unescapeString from "@/lib/client/unescapeString";
import useLinkStore from "@/store/links";
import {
LinkIncludingShortenedCollectionAndTags,
ArchivedFormat,
@ -14,6 +13,7 @@ import toast from "react-hot-toast";
import Modal from "../Modal";
import { useTranslation } from "next-i18next";
import { useCollections } from "@/hooks/store/collections";
import { useUploadFile } from "@/hooks/store/links";
type Props = {
onClose: Function;
@ -45,11 +45,11 @@ export default function UploadFileModal({ onClose }: Props) {
useState<LinkIncludingShortenedCollectionAndTags>(initial);
const [file, setFile] = useState<File>();
const { uploadFile } = useLinkStore();
const uploadFile = useUploadFile();
const [submitLoader, setSubmitLoader] = useState(false);
const [optionsExpanded, setOptionsExpanded] = useState(false);
const router = useRouter();
const { data: collections } = useCollections();
const { data: collections = [] } = useCollections();
const setCollection = (e: any) => {
if (e?.__isNew__) e.value = null;
@ -115,20 +115,17 @@ export default function UploadFileModal({ onClose }: Props) {
// }
setSubmitLoader(true);
const load = toast.loading(t("creating"));
const response = await uploadFile(link, file);
toast.dismiss(load);
if (response.ok) {
toast.success(t("created_success"));
await uploadFile.mutateAsync(
{ link, file },
{
onSuccess: () => {
onClose();
} else {
toast.error(response.data as string);
},
}
);
setSubmitLoader(false);
return response;
}
};

View File

@ -1,13 +1,11 @@
import React, { useEffect, useState } from "react";
import useLinkStore from "@/store/links";
import {
ArchivedFormat,
LinkIncludingShortenedCollectionAndTags,
} from "@/types/global";
import toast from "react-hot-toast";
import Link from "next/link";
import { useRouter } from "next/router";
import { useSession } from "next-auth/react";
import { useGetLink } from "@/hooks/store/links";
type Props = {
name: string;
@ -24,8 +22,7 @@ export default function PreservedFormatRow({
activeLink,
downloadable,
}: Props) {
const session = useSession();
const { getLink } = useLinkStore();
const getLink = useGetLink();
const [link, setLink] =
useState<LinkIncludingShortenedCollectionAndTags>(activeLink);
@ -36,7 +33,7 @@ export default function PreservedFormatRow({
useEffect(() => {
(async () => {
const data = await getLink(link.id as number, isPublic);
const data = await getLink.mutateAsync(link.id as number);
setLink(
(data as any).response as LinkIncludingShortenedCollectionAndTags
);
@ -45,7 +42,7 @@ export default function PreservedFormatRow({
let interval: any;
if (link?.image === "pending" || link?.pdf === "pending") {
interval = setInterval(async () => {
const data = await getLink(link.id as number, isPublic);
const data = await getLink.mutateAsync(link.id as number);
setLink(
(data as any).response as LinkIncludingShortenedCollectionAndTags
);

View File

@ -9,7 +9,7 @@ import { useUser } from "@/hooks/store/user";
export default function ProfileDropdown() {
const { t } = useTranslation();
const { settings, updateSettings } = useLocalSettingsStore();
const { data: user } = useUser();
const { data: user = {} } = useUser();
const isAdmin = user.id === Number(process.env.NEXT_PUBLIC_ADMIN || 1);

View File

@ -1,7 +1,6 @@
import unescapeString from "@/lib/client/unescapeString";
import { readabilityAvailable } from "@/lib/shared/getArchiveValidity";
import isValidUrl from "@/lib/shared/isValidUrl";
import useLinkStore from "@/store/links";
import {
ArchivedFormat,
CollectionIncludingMembersAndLinkCount,
@ -16,6 +15,7 @@ import React, { useEffect, useMemo, useState } from "react";
import LinkActions from "./LinkViews/LinkComponents/LinkActions";
import { useTranslation } from "next-i18next";
import { useCollections } from "@/hooks/store/collections";
import { useGetLink } from "@/hooks/store/links";
type LinkContent = {
title: string;
@ -45,8 +45,8 @@ export default function ReadableView({ link }: Props) {
const router = useRouter();
const { getLink } = useLinkStore();
const { data: collections } = useCollections();
const getLink = useGetLink();
const { data: collections = [] } = useCollections();
const collection = useMemo(() => {
return collections.find(
@ -73,7 +73,7 @@ export default function ReadableView({ link }: Props) {
}, [link]);
useEffect(() => {
if (link) getLink(link?.id as number);
if (link) getLink.mutateAsync(link?.id as number);
let interval: any;
if (
@ -87,7 +87,10 @@ export default function ReadableView({ link }: Props) {
!link?.readable ||
!link?.monolith)
) {
interval = setInterval(() => getLink(link.id as number), 5000);
interval = setInterval(
() => getLink.mutateAsync(link.id as number),
5000
);
} else {
if (interval) {
clearInterval(interval);

View File

@ -24,7 +24,7 @@ export default function Sidebar({ className }: { className?: string }) {
const { data: collections } = useCollections();
const { data: tags } = useTags();
const { data: tags = [] } = useTags();
const [active, setActive] = useState("");
const router = useRouter();

View File

@ -1,7 +1,8 @@
import React, { Dispatch, SetStateAction } from "react";
import React, { Dispatch, SetStateAction, useEffect } from "react";
import { Sort } from "@/types/global";
import { dropdownTriggerer } from "@/lib/client/utils";
import { TFunction } from "i18next";
import useLocalSettingsStore from "@/store/localSettings";
type Props = {
sortBy: Sort;
@ -10,6 +11,12 @@ type Props = {
};
export default function SortDropdown({ sortBy, setSort, t }: Props) {
const { updateSettings } = useLocalSettingsStore();
useEffect(() => {
updateSettings({ sortBy });
}, [sortBy]);
return (
<div className="dropdown dropdown-bottom dropdown-end">
<div

View File

@ -4,8 +4,8 @@ import useLocalSettingsStore from "@/store/localSettings";
import { ViewMode } from "@/types/global";
type Props = {
viewMode: string;
setViewMode: Dispatch<SetStateAction<string>>;
viewMode: ViewMode;
setViewMode: Dispatch<SetStateAction<ViewMode>>;
};
export default function ViewDropdown({ viewMode, setViewMode }: Props) {
@ -19,7 +19,7 @@ export default function ViewDropdown({ viewMode, setViewMode }: Props) {
};
useEffect(() => {
updateSettings({ viewMode: viewMode as ViewMode });
updateSettings({ viewMode });
}, [viewMode]);
return (

View File

@ -11,7 +11,6 @@ const useCollections = () => {
const data = await response.json();
return data.response;
},
initialData: [],
});
};
@ -42,8 +41,6 @@ const useCreateCollection = () => {
onSuccess: (data) => {
toast.success(t("created"));
return queryClient.setQueryData(["collections"], (oldData: any) => {
console.log([...oldData, data]);
return [...oldData, data];
});
},

View File

@ -0,0 +1,16 @@
import { LinkIncludingShortenedCollectionAndTags } from "@/types/global";
import { useQuery } from "@tanstack/react-query";
const useDashboardData = () => {
return useQuery({
queryKey: ["dashboardData"],
queryFn: async (): Promise<LinkIncludingShortenedCollectionAndTags[]> => {
const response = await fetch("/api/v1/dashboard");
const data = await response.json();
return data.response;
},
});
};
export { useDashboardData };

499
hooks/store/links.tsx Normal file
View File

@ -0,0 +1,499 @@
import {
InfiniteData,
useInfiniteQuery,
UseInfiniteQueryResult,
useQueryClient,
useMutation,
} from "@tanstack/react-query";
import { useEffect, useMemo } from "react";
import {
ArchivedFormat,
LinkIncludingShortenedCollectionAndTags,
LinkRequestQuery,
} from "@/types/global";
import toast from "react-hot-toast";
import { useTranslation } from "next-i18next";
import { useRouter } from "next/router";
const useLinks = (params: LinkRequestQuery = {}) => {
const router = useRouter();
const queryParamsObject = {
sort: params.sort ?? Number(window.localStorage.getItem("sortBy")) ?? 0,
collectionId:
params.collectionId ?? router.pathname === "/collections/[id]"
? router.query.id
: undefined,
tagId:
params.tagId ?? router.pathname === "/tags/[id]"
? router.query.id
: undefined,
pinnedOnly:
params.pinnedOnly ?? router.pathname === "/links/pinned"
? true
: undefined,
searchQueryString: params.searchQueryString,
searchByName: params.searchByName,
searchByUrl: params.searchByUrl,
searchByDescription: params.searchByDescription,
searchByTextContent: params.searchByTextContent,
searchByTags: params.searchByTags,
} as LinkRequestQuery;
const queryString = buildQueryString(queryParamsObject);
const { data, ...rest } = useFetchLinks(queryString);
const links = useMemo(() => {
return data?.pages.reduce((acc, page) => {
return [...acc, ...page];
}, []);
}, [data]);
return {
links,
data: { ...data, ...rest },
} as {
links: LinkIncludingShortenedCollectionAndTags[];
data: UseInfiniteQueryResult<InfiniteData<any, unknown>, Error>;
};
};
const useFetchLinks = (params: string) => {
return useInfiniteQuery({
queryKey: ["links", { params }],
queryFn: async (params) => {
const response = await fetch(
"/api/v1/links?cursor=" +
params.pageParam +
((params.queryKey[1] as any).params
? "&" + (params.queryKey[1] as any).params
: "")
);
const data = await response.json();
return data.response;
},
initialPageParam: 0,
refetchOnWindowFocus: false,
getNextPageParam: (lastPage) => {
if (lastPage.length === 0) {
return undefined;
}
return lastPage.at(-1).id;
},
});
};
const buildQueryString = (params: LinkRequestQuery) => {
return Object.keys(params)
.filter((key) => params[key as keyof LinkRequestQuery] !== undefined)
.map(
(key) =>
`${encodeURIComponent(key)}=${encodeURIComponent(
params[key as keyof LinkRequestQuery] as string
)}`
)
.join("&");
};
const useAddLink = () => {
const { t } = useTranslation();
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (link: LinkIncludingShortenedCollectionAndTags) => {
const load = toast.loading(t("creating_link"));
const response = await fetch("/api/v1/links", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(link),
});
toast.dismiss(load);
const data = await response.json();
if (!response.ok) throw new Error(data.response);
return data.response;
},
onSuccess: (data) => {
toast.success(t("link_created"));
queryClient.setQueryData(["dashboardData"], (oldData: any) => {
if (!oldData) return undefined;
return [data, ...oldData];
});
queryClient.setQueryData(["links"], (oldData: any) => {
if (!oldData) return undefined;
return {
pages: [[data, ...oldData?.pages[0]], ...oldData?.pages.slice(1)],
pageParams: oldData?.pageParams,
};
});
queryClient.invalidateQueries({ queryKey: ["collections"] });
queryClient.invalidateQueries({ queryKey: ["tags"] });
queryClient.invalidateQueries({ queryKey: ["publicLinks"] });
},
onError: (error) => {
toast.error(error.message);
},
});
};
const useUpdateLink = () => {
const { t } = useTranslation();
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (link: LinkIncludingShortenedCollectionAndTags) => {
const load = toast.loading(t("updating"));
const response = await fetch(`/api/v1/links/${link.id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(link),
});
toast.dismiss(load);
const data = await response.json();
if (!response.ok) throw new Error(data.response);
return data.response;
},
onSuccess: (data) => {
toast.success(t("updated"));
queryClient.setQueryData(["dashboardData"], (oldData: any) => {
if (!oldData) return undefined;
return oldData.map((e: any) => (e.id === data.id ? data : e));
});
queryClient.setQueryData(["links"], (oldData: any) => {
if (!oldData) return undefined;
return {
pages: [
oldData.pages[0].map((e: any) => (e.id === data.id ? data : e)),
...oldData.pages.slice(1),
],
pageParams: oldData.pageParams,
};
});
queryClient.invalidateQueries({ queryKey: ["collections"] });
queryClient.invalidateQueries({ queryKey: ["tags"] });
queryClient.invalidateQueries({ queryKey: ["publicLinks"] });
},
onError: (error) => {
toast.error(error.message);
},
});
};
const useDeleteLink = () => {
const { t } = useTranslation();
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (id: number) => {
const load = toast.loading(t("deleting"));
const response = await fetch(`/api/v1/links/${id}`, {
method: "DELETE",
});
toast.dismiss(load);
const data = await response.json();
if (!response.ok) throw new Error(data.response);
return data.response;
},
onSuccess: (data) => {
toast.success(t("deleted"));
queryClient.setQueryData(["dashboardData"], (oldData: any) => {
if (!oldData) return undefined;
return oldData.filter((e: any) => e.id !== data.id);
});
queryClient.setQueryData(["links"], (oldData: any) => {
if (!oldData) return undefined;
return {
pages: [
oldData.pages[0].filter((e: any) => e.id !== data.id),
...oldData.pages.slice(1),
],
pageParams: oldData.pageParams,
};
});
queryClient.invalidateQueries({ queryKey: ["collections"] });
queryClient.invalidateQueries({ queryKey: ["tags"] });
queryClient.invalidateQueries({ queryKey: ["publicLinks"] });
},
onError: (error) => {
toast.error(error.message);
},
});
};
const useGetLink = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (id: number) => {
const response = await fetch(`/api/v1/links/${id}`);
const data = await response.json();
if (!response.ok) throw new Error(data.response);
return data.response;
},
onSuccess: (data) => {
queryClient.setQueryData(["dashboardData"], (oldData: any) => {
if (!oldData) return undefined;
return oldData.map((e: any) => (e.id === data.id ? data : e));
});
queryClient.setQueryData(["links"], (oldData: any) => {
if (!oldData) return undefined;
return {
pages: [
oldData.pages[0].map((e: any) => (e.id === data.id ? data : e)),
...oldData.pages.slice(1),
],
pageParams: oldData.pageParams,
};
});
queryClient.invalidateQueries({ queryKey: ["publicLinks"] });
},
});
};
const useBulkDeleteLinks = () => {
const { t } = useTranslation();
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (linkIds: number[]) => {
const load = toast.loading(t("deleting"));
const response = await fetch("/api/v1/links", {
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ linkIds }),
});
toast.dismiss(load);
const data = await response.json();
if (!response.ok) throw new Error(data.response);
return linkIds;
},
onSuccess: (data) => {
toast.success(t("deleted"));
queryClient.setQueryData(["dashboardData"], (oldData: any) => {
if (!oldData) return undefined;
return oldData.filter((e: any) => !data.includes(e.id));
});
queryClient.setQueryData(["links"], (oldData: any) => {
if (!oldData) return undefined;
return {
pages: [
oldData.pages[0].filter((e: any) => !data.includes(e.id)),
...oldData.pages.slice(1),
],
pageParams: oldData.pageParams,
};
});
queryClient.invalidateQueries({ queryKey: ["collections"] });
queryClient.invalidateQueries({ queryKey: ["tags"] });
queryClient.invalidateQueries({ queryKey: ["publicLinks"] });
},
onError: (error) => {
toast.error(error.message);
},
});
};
const useUploadFile = () => {
const { t } = useTranslation();
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ link, file }: any) => {
let fileType: ArchivedFormat | null = null;
let linkType: "url" | "image" | "pdf" | null = null;
if (file?.type === "image/jpg" || file.type === "image/jpeg") {
fileType = ArchivedFormat.jpeg;
linkType = "image";
} else if (file.type === "image/png") {
fileType = ArchivedFormat.png;
linkType = "image";
} else if (file.type === "application/pdf") {
fileType = ArchivedFormat.pdf;
linkType = "pdf";
} else {
return { ok: false, data: "Invalid file type." };
}
const load = toast.loading(t("creating"));
const response = await fetch("/api/v1/links", {
body: JSON.stringify({
...link,
type: linkType,
name: link.name ? link.name : file.name,
}),
headers: {
"Content-Type": "application/json",
},
method: "POST",
});
const data = await response.json();
if (!response.ok) throw new Error(data.response);
if (response.ok) {
const formBody = new FormData();
file && formBody.append("file", file);
await fetch(
`/api/v1/archives/${(data as any).response.id}?format=${fileType}`,
{
body: formBody,
method: "POST",
}
);
}
toast.dismiss(load);
return data.response;
},
onSuccess: (data) => {
toast.success(t("created_success"));
queryClient.setQueryData(["dashboardData"], (oldData: any) => {
if (!oldData) return undefined;
return [data, ...oldData];
});
queryClient.setQueryData(["links"], (oldData: any) => {
if (!oldData) return undefined;
return {
pages: [[data, ...oldData?.pages[0]], ...oldData?.pages.slice(1)],
pageParams: oldData?.pageParams,
};
});
queryClient.invalidateQueries({ queryKey: ["collections"] });
queryClient.invalidateQueries({ queryKey: ["tags"] });
queryClient.invalidateQueries({ queryKey: ["publicLinks"] });
},
onError: (error) => {
toast.error(error.message);
},
});
};
const useBulkEditLinks = () => {
const { t } = useTranslation();
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({
links,
newData,
removePreviousTags,
}: {
links: LinkIncludingShortenedCollectionAndTags[];
newData: Pick<
LinkIncludingShortenedCollectionAndTags,
"tags" | "collectionId"
>;
removePreviousTags: boolean;
}) => {
const load = toast.loading(t("updating"));
const response = await fetch("/api/v1/links", {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ links, newData, removePreviousTags }),
});
toast.dismiss(load);
const data = await response.json();
if (!response.ok) throw new Error(data.response);
return data.response;
},
onSuccess: (data) => {
toast.success(t("updated"));
queryClient.setQueryData(["dashboardData"], (oldData: any) => {
if (!oldData) return undefined;
return oldData.map((e: any) =>
data.find((d: any) => d.id === e.id) ? data : e
);
});
queryClient.setQueryData(["links"], (oldData: any) => {
if (!oldData) return undefined;
return {
pages: [
oldData.pages[0].map((e: any) =>
data.find((d: any) => d.id === e.id) ? data : e
),
...oldData.pages.slice(1),
],
pageParams: oldData.pageParams,
};
});
queryClient.invalidateQueries({ queryKey: ["collections"] });
queryClient.invalidateQueries({ queryKey: ["tags"] });
queryClient.invalidateQueries({ queryKey: ["publicLinks"] });
},
onError: (error) => {
toast.error(error.message);
},
});
};
export {
useLinks,
useAddLink,
useUpdateLink,
useDeleteLink,
useBulkDeleteLinks,
useUploadFile,
useGetLink,
useBulkEditLinks,
};

View File

@ -0,0 +1,93 @@
import {
InfiniteData,
useInfiniteQuery,
UseInfiniteQueryResult,
} from "@tanstack/react-query";
import { useMemo } from "react";
import {
LinkIncludingShortenedCollectionAndTags,
LinkRequestQuery,
} from "@/types/global";
import { useRouter } from "next/router";
const usePublicLinks = (params: LinkRequestQuery = {}) => {
const router = useRouter();
const queryParamsObject = {
sort: params.sort ?? Number(window.localStorage.getItem("sortBy")) ?? 0,
collectionId: params.collectionId ?? router.query.id,
tagId:
params.tagId ?? router.pathname === "/tags/[id]"
? router.query.id
: undefined,
pinnedOnly:
params.pinnedOnly ?? router.pathname === "/links/pinned"
? true
: undefined,
searchQueryString: params.searchQueryString,
searchByName: params.searchByName,
searchByUrl: params.searchByUrl,
searchByDescription: params.searchByDescription,
searchByTextContent: params.searchByTextContent,
searchByTags: params.searchByTags,
} as LinkRequestQuery;
const queryString = buildQueryString(queryParamsObject);
const { data, ...rest } = useFetchLinks(queryString);
const links = useMemo(() => {
return data?.pages.reduce((acc, page) => {
return [...acc, ...page];
}, []);
}, [data]);
return {
links,
data: { ...data, ...rest },
} as {
links: LinkIncludingShortenedCollectionAndTags[];
data: UseInfiniteQueryResult<InfiniteData<any, unknown>, Error>;
};
};
const useFetchLinks = (params: string) => {
return useInfiniteQuery({
queryKey: ["links", { params }],
queryFn: async (params) => {
const response = await fetch(
"/api/v1/public/collections/links?cursor=" +
params.pageParam +
((params.queryKey[1] as any).params
? "&" + (params.queryKey[1] as any).params
: "")
);
const data = await response.json();
return data.response;
},
initialPageParam: 0,
refetchOnWindowFocus: false,
getNextPageParam: (lastPage) => {
if (lastPage.length === 0) {
return undefined;
}
return lastPage.at(-1).id;
},
});
};
const buildQueryString = (params: LinkRequestQuery) => {
return Object.keys(params)
.filter((key) => params[key as keyof LinkRequestQuery] !== undefined)
.map(
(key) =>
`${encodeURIComponent(key)}=${encodeURIComponent(
params[key as keyof LinkRequestQuery] as string
)}`
)
.join("&");
};
export { usePublicLinks };

View File

@ -13,7 +13,6 @@ const useTags = () => {
const data = await response.json();
return data.response;
},
initialData: [],
});
};

View File

@ -14,7 +14,6 @@ const useTokens = () => {
const data = await response.json();
return data.response as AccessToken[];
},
initialData: [],
});
};

View File

@ -19,7 +19,7 @@ const useUser = () => {
return data.response;
},
enabled: !!userId,
initialData: {},
placeholderData: {},
});
};

View File

@ -4,9 +4,9 @@ import { useCollections } from "./store/collections";
import { useUser } from "./store/user";
export default function useCollectivePermissions(collectionIds: number[]) {
const { data: collections } = useCollections();
const { data: collections = [] } = useCollections();
const { data: user } = useUser();
const { data: user = {} } = useUser();
const [permissions, setPermissions] = useState<Member | true>();
useEffect(() => {

View File

@ -6,7 +6,7 @@ import { useUser } from "./store/user";
export default function useInitialData() {
const { status, data } = useSession();
// const { setLinks } = useLinkStore();
const { data: user } = useUser();
const { data: user = {} } = useUser();
const { setSettings } = useLocalSettingsStore();
useEffect(() => {

View File

@ -1,103 +0,0 @@
import { LinkRequestQuery } from "@/types/global";
import { useEffect, useState } from "react";
import useDetectPageBottom from "./useDetectPageBottom";
import { useRouter } from "next/router";
import useLinkStore from "@/store/links";
export default function useLinks(
{
sort,
collectionId,
tagId,
pinnedOnly,
searchQueryString,
searchByName,
searchByUrl,
searchByDescription,
searchByTags,
searchByTextContent,
}: LinkRequestQuery = { sort: 0 }
) {
const { links, setLinks, resetLinks, selectedLinks, setSelectedLinks } =
useLinkStore();
const router = useRouter();
const [isLoading, setIsLoading] = useState(true);
const { reachedBottom, setReachedBottom } = useDetectPageBottom();
const getLinks = async (isInitialCall: boolean, cursor?: number) => {
const params = {
sort,
cursor,
collectionId,
tagId,
pinnedOnly,
searchQueryString,
searchByName,
searchByUrl,
searchByDescription,
searchByTags,
searchByTextContent,
};
const buildQueryString = (params: LinkRequestQuery) => {
return Object.keys(params)
.filter((key) => params[key as keyof LinkRequestQuery] !== undefined)
.map(
(key) =>
`${encodeURIComponent(key)}=${encodeURIComponent(
params[key as keyof LinkRequestQuery] as string
)}`
)
.join("&");
};
let queryString = buildQueryString(params);
let basePath;
if (router.pathname === "/dashboard") basePath = "/api/v1/dashboard";
else if (router.pathname.startsWith("/public/collections/[id]")) {
queryString = queryString + "&collectionId=" + router.query.id;
basePath = "/api/v1/public/collections/links";
} else basePath = "/api/v1/links";
setIsLoading(true);
const response = await fetch(`${basePath}?${queryString}`);
const data = await response.json();
setIsLoading(false);
if (response.ok) setLinks(data.response, isInitialCall);
};
useEffect(() => {
// Save the selected links before resetting the links
// and then restore the selected links after resetting the links
const previouslySelected = selectedLinks;
resetLinks();
setSelectedLinks(previouslySelected);
getLinks(true);
}, [
router,
sort,
searchQueryString,
searchByName,
searchByUrl,
searchByDescription,
searchByTextContent,
searchByTags,
]);
useEffect(() => {
if (reachedBottom) getLinks(false, links?.at(-1)?.id);
setReachedBottom(false);
}, [reachedBottom]);
return { isLoading };
}

View File

@ -4,9 +4,9 @@ import { useCollections } from "./store/collections";
import { useUser } from "./store/user";
export default function usePermissions(collectionId: number) {
const { data: collections } = useCollections();
const { data: collections = [] } = useCollections();
const { data: user } = useUser();
const { data: user = {} } = useUser();
const [permissions, setPermissions] = useState<Member | true>();
useEffect(() => {

View File

@ -14,7 +14,7 @@ export default function AuthRedirect({ children }: Props) {
const router = useRouter();
const { status } = useSession();
const [shouldRenderChildren, setShouldRenderChildren] = useState(false);
const { data: user } = useUser();
const { data: user = {} } = useUser();
useInitialData();

View File

@ -5,7 +5,7 @@ export default async function getDashboardData(
userId: number,
query: LinkRequestQuery
) {
let order: any;
let order: any = { 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" };
@ -42,7 +42,7 @@ export default async function getDashboardData(
select: { id: true },
},
},
orderBy: order || { id: "desc" },
orderBy: order,
});
const recentlyAddedLinks = await prisma.link.findMany({
@ -67,10 +67,18 @@ export default async function getDashboardData(
select: { id: true },
},
},
orderBy: order || { id: "desc" },
orderBy: order,
});
const links = [...recentlyAddedLinks, ...pinnedLinks].sort(
const combinedLinks = [...recentlyAddedLinks, ...pinnedLinks];
const uniqueLinks = Array.from(
combinedLinks
.reduce((map, item) => map.set(item.id, item), new Map())
.values()
);
const links = uniqueLinks.sort(
(a, b) => (new Date(b.id) as any) - (new Date(a.id) as any)
);

View File

@ -5,7 +5,7 @@ export default async function getLink(userId: number, query: LinkRequestQuery) {
const POSTGRES_IS_ENABLED =
process.env.DATABASE_URL?.startsWith("postgresql");
let order: any;
let order: any = { 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" };
@ -146,7 +146,7 @@ export default async function getLink(userId: number, query: LinkRequestQuery) {
select: { id: true },
},
},
orderBy: order || { id: "desc" },
orderBy: order,
});
return { response: links, status: 200 };

View File

@ -69,6 +69,7 @@
"react-hot-toast": "^2.4.1",
"react-i18next": "^14.1.2",
"react-image-file-resizer": "^0.4.8",
"react-intersection-observer": "^9.13.0",
"react-masonry-css": "^1.0.16",
"react-select": "^5.7.4",
"react-spinners": "^0.13.8",

View File

@ -14,7 +14,13 @@ import { appWithTranslation } from "next-i18next";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
const queryClient = new QueryClient();
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 30,
},
},
});
function App({
Component,
@ -105,12 +111,3 @@ function App({
}
export default appWithTranslation(App);
// function GetData({ children }: { children: React.ReactNode }) {
// const status = useInitialData();
// return typeof window !== "undefined" && status !== "loading" ? (
// children
// ) : (
// <></>
// );
// }

View File

@ -21,7 +21,7 @@ type UserModal = {
export default function Admin() {
const { t } = useTranslation();
const { data: users } = useUsers();
const { data: users = [] } = useUsers();
const [searchQuery, setSearchQuery] = useState("");
const [filteredUsers, setFilteredUsers] = useState<User[]>();

View File

@ -1,4 +1,3 @@
import useLinkStore from "@/store/links";
import {
CollectionIncludingMembersAndLinkCount,
Sort,
@ -8,7 +7,6 @@ import { useRouter } from "next/router";
import React, { useEffect, useState } from "react";
import MainLayout from "@/layouts/MainLayout";
import ProfilePhoto from "@/components/ProfilePhoto";
import useLinks from "@/hooks/useLinks";
import usePermissions from "@/hooks/usePermissions";
import NoLinksFound from "@/components/NoLinksFound";
import useLocalSettingsStore from "@/store/localSettings";
@ -16,16 +14,15 @@ import getPublicUserData from "@/lib/client/getPublicUserData";
import EditCollectionModal from "@/components/ModalContent/EditCollectionModal";
import EditCollectionSharingModal from "@/components/ModalContent/EditCollectionSharingModal";
import DeleteCollectionModal from "@/components/ModalContent/DeleteCollectionModal";
import CardView from "@/components/LinkViews/Layouts/CardView";
import ListView from "@/components/LinkViews/Layouts/ListView";
import { dropdownTriggerer } from "@/lib/client/utils";
import NewCollectionModal from "@/components/ModalContent/NewCollectionModal";
import MasonryView from "@/components/LinkViews/Layouts/MasonryView";
import getServerSideProps from "@/lib/client/getServerSideProps";
import { useTranslation } from "next-i18next";
import LinkListOptions from "@/components/LinkListOptions";
import { useCollections } from "@/hooks/store/collections";
import { useUser } from "@/hooks/store/user";
import { useLinks } from "@/hooks/store/links";
import Links from "@/components/LinkViews/Links";
export default function Index() {
const { t } = useTranslation();
@ -33,25 +30,29 @@ export default function Index() {
const router = useRouter();
const { links } = useLinkStore();
const { data: collections } = useCollections();
const { data: collections = [] } = useCollections();
const [sortBy, setSortBy] = useState<Sort>(Sort.DateNewestFirst);
const [sortBy, setSortBy] = useState<Sort>(
Number(localStorage.getItem("sortBy")) ?? Sort.DateNewestFirst
);
const { links, data } = useLinks({
sort: sortBy,
collectionId: Number(router.query.id),
});
const [activeCollection, setActiveCollection] =
useState<CollectionIncludingMembersAndLinkCount>();
const permissions = usePermissions(activeCollection?.id as number);
useLinks({ collectionId: Number(router.query.id), sort: sortBy });
useEffect(() => {
setActiveCollection(
collections.find((e) => e.id === Number(router.query.id))
);
}, [router, collections]);
const { data: user } = useUser();
const { data: user = {} } = useUser();
const [collectionOwner, setCollectionOwner] = useState({
id: null as unknown as number,
@ -97,19 +98,10 @@ export default function Index() {
if (editMode) return setEditMode(false);
}, [router]);
const [viewMode, setViewMode] = useState<string>(
localStorage.getItem("viewMode") || ViewMode.Card
const [viewMode, setViewMode] = useState<ViewMode>(
(localStorage.getItem("viewMode") as ViewMode) || ViewMode.Card
);
const linkView = {
[ViewMode.Card]: CardView,
[ViewMode.List]: ListView,
[ViewMode.Masonry]: MasonryView,
};
// @ts-ignore
const LinkComponent = linkView[viewMode];
return (
<MainLayout>
<div
@ -323,16 +315,14 @@ export default function Index() {
</p>
</LinkListOptions>
{links.some((e) => e.collectionId === Number(router.query.id)) ? (
<LinkComponent
<Links
editMode={editMode}
links={links.filter(
(e) => e.collection.id === activeCollection?.id
)}
links={links}
layout={viewMode}
placeholderCount={1}
useData={data}
/>
) : (
<NoLinksFound />
)}
{!data.isLoading && !links[0] && <NoLinksFound />}
</div>
{activeCollection && (
<>

View File

@ -13,8 +13,10 @@ import { useCollections } from "@/hooks/store/collections";
export default function Collections() {
const { t } = useTranslation();
const { data: collections } = useCollections();
const [sortBy, setSortBy] = useState<Sort>(Sort.DateNewestFirst);
const { data: collections = [] } = useCollections();
const [sortBy, setSortBy] = useState<Sort>(
Number(localStorage.getItem("sortBy")) ?? Sort.DateNewestFirst
);
const [sortedCollections, setSortedCollections] = useState(collections);
const { data } = useSession();

View File

@ -1,7 +1,5 @@
import useLinkStore from "@/store/links";
import MainLayout from "@/layouts/MainLayout";
import { useEffect, useState } from "react";
import useLinks from "@/hooks/useLinks";
import Link from "next/link";
import useWindowDimensions from "@/hooks/useWindowDimensions";
import React from "react";
@ -10,28 +8,25 @@ import { MigrationFormat, MigrationRequest, ViewMode } from "@/types/global";
import DashboardItem from "@/components/DashboardItem";
import NewLinkModal from "@/components/ModalContent/NewLinkModal";
import PageHeader from "@/components/PageHeader";
import CardView from "@/components/LinkViews/Layouts/CardView";
import ListView from "@/components/LinkViews/Layouts/ListView";
import ViewDropdown from "@/components/ViewDropdown";
import { dropdownTriggerer } from "@/lib/client/utils";
import MasonryView from "@/components/LinkViews/Layouts/MasonryView";
import getServerSideProps from "@/lib/client/getServerSideProps";
import { useTranslation } from "next-i18next";
import { useCollections } from "@/hooks/store/collections";
import { useTags } from "@/hooks/store/tags";
import { useDashboardData } from "@/hooks/store/dashboardData";
import Links from "@/components/LinkViews/Links";
export default function Dashboard() {
const { t } = useTranslation();
const { data: collections } = useCollections();
const { links } = useLinkStore();
const { data: tags } = useTags();
const { data: collections = [] } = useCollections();
const dashboardData = useDashboardData();
const { data: tags = [] } = useTags();
const [numberOfLinks, setNumberOfLinks] = useState(0);
const [showLinks, setShowLinks] = useState(3);
useLinks({ pinnedOnly: true, sort: 0 });
useEffect(() => {
setNumberOfLinks(
collections.reduce(
@ -81,7 +76,7 @@ export default function Dashboard() {
body: JSON.stringify(body),
});
const data = await response.json();
await response.json();
toast.dismiss(load);
@ -99,20 +94,10 @@ export default function Dashboard() {
const [newLinkModal, setNewLinkModal] = useState(false);
const [viewMode, setViewMode] = useState<string>(
localStorage.getItem("viewMode") || ViewMode.Card
const [viewMode, setViewMode] = useState<ViewMode>(
(localStorage.getItem("viewMode") as ViewMode) || ViewMode.Card
);
const linkView = {
[ViewMode.Card]: CardView,
// [ViewMode.Grid]: ,
[ViewMode.List]: ListView,
[ViewMode.Masonry]: MasonryView,
};
// @ts-ignore
const LinkComponent = linkView[viewMode];
return (
<MainLayout>
<div style={{ flex: "1 1 auto" }} className="p-5 flex flex-col gap-5">
@ -171,12 +156,30 @@ export default function Dashboard() {
</div>
<div
style={{ flex: links[0] ? "0 1 auto" : "1 1 auto" }}
style={{
flex:
dashboardData.data || dashboardData.isLoading
? "0 1 auto"
: "1 1 auto",
}}
className="flex flex-col 2xl:flex-row items-start 2xl:gap-2"
>
{links[0] ? (
{dashboardData.isLoading ? (
<div className="w-full">
<LinkComponent links={links.slice(0, showLinks)} />
<Links
layout={viewMode}
placeholderCount={showLinks}
useData={dashboardData}
/>
</div>
) : dashboardData.data &&
dashboardData.data[0] &&
!dashboardData.isLoading ? (
<div className="w-full">
<Links
links={dashboardData.data.slice(0, showLinks)}
layout={viewMode}
/>
</div>
) : (
<div className="sky-shadow flex flex-col justify-center h-full border border-solid border-neutral-content w-full mx-auto p-10 rounded-2xl bg-base-200">
@ -300,12 +303,21 @@ export default function Dashboard() {
style={{ flex: "1 1 auto" }}
className="flex flex-col 2xl:flex-row items-start 2xl:gap-2"
>
{links.some((e) => e.pinnedBy && e.pinnedBy[0]) ? (
{dashboardData.isLoading ? (
<div className="w-full">
<LinkComponent
links={links
<Links
layout={viewMode}
placeholderCount={showLinks}
useData={dashboardData}
/>
</div>
) : dashboardData.data?.some((e) => e.pinnedBy && e.pinnedBy[0]) ? (
<div className="w-full">
<Links
links={dashboardData.data
.filter((e) => e.pinnedBy && e.pinnedBy[0])
.slice(0, showLinks)}
layout={viewMode}
/>
</div>
) : (

View File

@ -1,29 +1,28 @@
import NoLinksFound from "@/components/NoLinksFound";
import useLinks from "@/hooks/useLinks";
// import { useLinks } from "@/hooks/store/links";
import { useLinks } from "@/hooks/store/links";
import MainLayout from "@/layouts/MainLayout";
import useLinkStore from "@/store/links";
import React, { useEffect, useState } from "react";
import PageHeader from "@/components/PageHeader";
import { Sort, ViewMode } from "@/types/global";
import CardView from "@/components/LinkViews/Layouts/CardView";
import ListView from "@/components/LinkViews/Layouts/ListView";
import { useRouter } from "next/router";
import MasonryView from "@/components/LinkViews/Layouts/MasonryView";
import LinkListOptions from "@/components/LinkListOptions";
import getServerSideProps from "@/lib/client/getServerSideProps";
import { useTranslation } from "next-i18next";
import Links from "@/components/LinkViews/Links";
export default function Links() {
export default function Index() {
const { t } = useTranslation();
// const { data: links } = useLinks();
const { links } = useLinkStore();
const [viewMode, setViewMode] = useState<string>(
localStorage.getItem("viewMode") || ViewMode.Card
const [viewMode, setViewMode] = useState<ViewMode>(
(localStorage.getItem("viewMode") as ViewMode) || ViewMode.Card
);
const [sortBy, setSortBy] = useState<Sort>(Sort.DateNewestFirst);
const [sortBy, setSortBy] = useState<Sort>(
Number(localStorage.getItem("sortBy")) ?? Sort.DateNewestFirst
);
const { links, data } = useLinks({
sort: sortBy,
});
const router = useRouter();
@ -33,17 +32,6 @@ export default function Links() {
if (editMode) return setEditMode(false);
}, [router]);
useLinks({ sort: sortBy });
const linkView = {
[ViewMode.Card]: CardView,
[ViewMode.List]: ListView,
[ViewMode.Masonry]: MasonryView,
};
// @ts-ignore
const LinkComponent = linkView[viewMode];
return (
<MainLayout>
<div className="p-5 flex flex-col gap-5 w-full h-full">
@ -63,9 +51,14 @@ export default function Links() {
/>
</LinkListOptions>
{links[0] ? (
<LinkComponent editMode={editMode} links={links} />
) : (
<Links
editMode={editMode}
links={links}
layout={viewMode}
placeholderCount={1}
useData={data}
/>
{!data.isLoading && !links[0] && (
<NoLinksFound text={t("you_have_not_added_any_links")} />
)}
</div>

View File

@ -1,45 +1,32 @@
import useLinks from "@/hooks/useLinks";
import MainLayout from "@/layouts/MainLayout";
import useLinkStore from "@/store/links";
import React, { useEffect, useState } from "react";
import PageHeader from "@/components/PageHeader";
import { Sort, ViewMode } from "@/types/global";
import CardView from "@/components/LinkViews/Layouts/CardView";
import ListView from "@/components/LinkViews/Layouts/ListView";
import { useRouter } from "next/router";
import MasonryView from "@/components/LinkViews/Layouts/MasonryView";
import { useTranslation } from "next-i18next";
import getServerSideProps from "@/lib/client/getServerSideProps";
import LinkListOptions from "@/components/LinkListOptions";
import { useLinks } from "@/hooks/store/links";
import Links from "@/components/LinkViews/Links";
export default function PinnedLinks() {
const { t } = useTranslation();
const { links } = useLinkStore();
const [viewMode, setViewMode] = useState<string>(
localStorage.getItem("viewMode") || ViewMode.Card
const [viewMode, setViewMode] = useState<ViewMode>(
(localStorage.getItem("viewMode") as ViewMode) || ViewMode.Card
);
const [sortBy, setSortBy] = useState<Sort>(
Number(localStorage.getItem("sortBy")) ?? Sort.DateNewestFirst
);
const [sortBy, setSortBy] = useState<Sort>(Sort.DateNewestFirst);
useLinks({ sort: sortBy, pinnedOnly: true });
const { links, data } = useLinks({
sort: sortBy,
pinnedOnly: true,
});
const router = useRouter();
const [editMode, setEditMode] = useState(false);
useEffect(() => {
if (editMode) return setEditMode(false);
}, [router]);
const linkView = {
[ViewMode.Card]: CardView,
[ViewMode.List]: ListView,
[ViewMode.Masonry]: MasonryView,
};
// @ts-ignore
const LinkComponent = linkView[viewMode];
return (
<MainLayout>
<div className="p-5 flex flex-col gap-5 w-full h-full">
@ -59,9 +46,14 @@ export default function PinnedLinks() {
/>
</LinkListOptions>
{links.some((e) => e.pinnedBy && e.pinnedBy[0]) ? (
<LinkComponent editMode={editMode} links={links} />
) : (
<Links
editMode={editMode}
links={links}
layout={viewMode}
placeholderCount={1}
useData={data}
/>
{!data.isLoading && !links[0] && (
<div
style={{ flex: "1 1 auto" }}
className="flex flex-col gap-2 justify-center h-full w-full mx-auto p-10"

View File

@ -1,5 +1,4 @@
import React, { useEffect, useState } from "react";
import useLinkStore from "@/store/links";
import { useRouter } from "next/router";
import {
ArchivedFormat,
@ -7,9 +6,12 @@ import {
} from "@/types/global";
import ReadableView from "@/components/ReadableView";
import getServerSideProps from "@/lib/client/getServerSideProps";
import { useGetLink, useLinks } from "@/hooks/store/links";
export default function Index() {
const { links, getLink } = useLinkStore();
const { links } = useLinks();
const getLink = useGetLink();
const [link, setLink] = useState<LinkIncludingShortenedCollectionAndTags>();
@ -18,7 +20,7 @@ export default function Index() {
useEffect(() => {
const fetchLink = async () => {
if (router.query.id) {
await getLink(Number(router.query.id));
await getLink.mutateAsync(Number(router.query.id));
}
};
@ -26,7 +28,8 @@ export default function Index() {
}, []);
useEffect(() => {
if (links[0]) setLink(links.find((e) => e.id === Number(router.query.id)));
if (links && links[0])
setLink(links.find((e) => e.id === Number(router.query.id)));
}, [links]);
return (

View File

@ -8,8 +8,6 @@ import {
import { useRouter } from "next/router";
import React, { useEffect, useState } from "react";
import Head from "next/head";
import useLinks from "@/hooks/useLinks";
import useLinkStore from "@/store/links";
import ProfilePhoto from "@/components/ProfilePhoto";
import ToggleDarkMode from "@/components/ToggleDarkMode";
import getPublicUserData from "@/lib/client/getPublicUserData";
@ -18,21 +16,19 @@ import Link from "next/link";
import useLocalSettingsStore from "@/store/localSettings";
import SearchBar from "@/components/SearchBar";
import EditCollectionSharingModal from "@/components/ModalContent/EditCollectionSharingModal";
import CardView from "@/components/LinkViews/Layouts/CardView";
import ListView from "@/components/LinkViews/Layouts/ListView";
import MasonryView from "@/components/LinkViews/Layouts/MasonryView";
import { useTranslation } from "next-i18next";
import getServerSideProps from "@/lib/client/getServerSideProps";
import LinkListOptions from "@/components/LinkListOptions";
import { useCollections } from "@/hooks/store/collections";
import { usePublicLinks } from "@/hooks/store/publicLinks";
import Links from "@/components/LinkViews/Links";
export default function PublicCollections() {
const { t } = useTranslation();
const { links } = useLinkStore();
const { settings } = useLocalSettingsStore();
const { data: collections } = useCollections();
const { data: collections = [] } = useCollections();
const router = useRouter();
@ -54,9 +50,11 @@ export default function PublicCollections() {
textContent: false,
});
const [sortBy, setSortBy] = useState<Sort>(Sort.DateNewestFirst);
const [sortBy, setSortBy] = useState<Sort>(
Number(localStorage.getItem("sortBy")) ?? Sort.DateNewestFirst
);
useLinks({
const { links, data } = usePublicLinks({
sort: sortBy,
searchQueryString: router.query.q
? decodeURIComponent(router.query.q as string)
@ -91,19 +89,10 @@ export default function PublicCollections() {
const [editCollectionSharingModal, setEditCollectionSharingModal] =
useState(false);
const [viewMode, setViewMode] = useState<string>(
localStorage.getItem("viewMode") || ViewMode.Card
const [viewMode, setViewMode] = useState<ViewMode>(
(localStorage.getItem("viewMode") as ViewMode) || ViewMode.Card
);
const linkView = {
[ViewMode.Card]: CardView,
[ViewMode.List]: ListView,
[ViewMode.Masonry]: MasonryView,
};
// @ts-ignore
const LinkComponent = linkView[viewMode];
return collection ? (
<div
className="h-96"
@ -227,21 +216,21 @@ export default function PublicCollections() {
/>
</LinkListOptions>
{links[0] ? (
<LinkComponent
links={links
.filter((e) => e.collectionId === Number(router.query.id))
.map((e, i) => {
<Links
links={
links?.map((e, i) => {
const linkWithCollectionData = {
...e,
collection: collection, // Append collection data
};
return linkWithCollectionData;
})}
}) as any
}
layout={viewMode}
placeholderCount={1}
useData={data}
/>
) : (
<p>{t("collection_is_empty")}</p>
)}
{!data.isLoading && !links[0] && <p>{t("collection_is_empty")}</p>}
{/* <p className="text-center text-neutral">
List created with <span className="text-black">Linkwarden.</span>

View File

@ -1,5 +1,4 @@
import React, { useEffect, useState } from "react";
import useLinkStore from "@/store/links";
import { useRouter } from "next/router";
import {
ArchivedFormat,
@ -7,20 +6,20 @@ import {
} from "@/types/global";
import ReadableView from "@/components/ReadableView";
import getServerSideProps from "@/lib/client/getServerSideProps";
import { useGetLink, useLinks } from "@/hooks/store/links";
export default function Index() {
const { links, getLink } = useLinkStore();
const { links } = useLinks();
const getLink = useGetLink();
const [link, setLink] = useState<LinkIncludingShortenedCollectionAndTags>();
const router = useRouter();
let isPublic = router.pathname.startsWith("/public") ? true : false;
useEffect(() => {
const fetchLink = async () => {
if (router.query.id) {
await getLink(Number(router.query.id), isPublic);
await getLink.mutateAsync(Number(router.query.id));
}
};
@ -28,7 +27,8 @@ export default function Index() {
}, []);
useEffect(() => {
if (links[0]) setLink(links.find((e) => e.id === Number(router.query.id)));
if (links && links[0])
setLink(links.find((e) => e.id === Number(router.query.id)));
}, [links]);
return (

View File

@ -1,23 +1,17 @@
import useLinks from "@/hooks/useLinks";
import { useLinks } from "@/hooks/store/links";
import MainLayout from "@/layouts/MainLayout";
import useLinkStore from "@/store/links";
import { Sort, ViewMode } from "@/types/global";
import { useRouter } from "next/router";
import React, { useEffect, useState } from "react";
import CardView from "@/components/LinkViews/Layouts/CardView";
import ListView from "@/components/LinkViews/Layouts/ListView";
import PageHeader from "@/components/PageHeader";
import { GridLoader } from "react-spinners";
import MasonryView from "@/components/LinkViews/Layouts/MasonryView";
import LinkListOptions from "@/components/LinkListOptions";
import getServerSideProps from "@/lib/client/getServerSideProps";
import { useTranslation } from "next-i18next";
import Links from "@/components/LinkViews/Links";
export default function Search() {
const { t } = useTranslation();
const { links } = useLinkStore();
const router = useRouter();
const [searchFilter, setSearchFilter] = useState({
@ -28,11 +22,13 @@ export default function Search() {
textContent: false,
});
const [viewMode, setViewMode] = useState<string>(
localStorage.getItem("viewMode") || ViewMode.Card
const [viewMode, setViewMode] = useState<ViewMode>(
(localStorage.getItem("viewMode") as ViewMode) || ViewMode.Card
);
const [sortBy, setSortBy] = useState<Sort>(Sort.DateNewestFirst);
const [sortBy, setSortBy] = useState<Sort>(
Number(localStorage.getItem("sortBy")) ?? Sort.DateNewestFirst
);
const [editMode, setEditMode] = useState(false);
@ -40,7 +36,17 @@ export default function Search() {
if (editMode) return setEditMode(false);
}, [router]);
const { isLoading } = useLinks({
// const { isLoading } = useLink({
// sort: sortBy,
// searchQueryString: decodeURIComponent(router.query.q as string),
// searchByName: searchFilter.name,
// searchByUrl: searchFilter.url,
// searchByDescription: searchFilter.description,
// searchByTextContent: searchFilter.textContent,
// searchByTags: searchFilter.tags,
// });
const { links, data } = useLinks({
sort: sortBy,
searchQueryString: decodeURIComponent(router.query.q as string),
searchByName: searchFilter.name,
@ -50,15 +56,6 @@ export default function Search() {
searchByTags: searchFilter.tags,
});
const linkView = {
[ViewMode.Card]: CardView,
[ViewMode.List]: ListView,
[ViewMode.Masonry]: MasonryView,
};
// @ts-ignore
const LinkComponent = linkView[viewMode];
return (
<MainLayout>
<div className="p-5 flex flex-col gap-5 w-full h-full">
@ -76,7 +73,9 @@ export default function Search() {
<PageHeader icon={"bi-search"} title={"Search Results"} />
</LinkListOptions>
{!isLoading && !links[0] ? (
{/* {
!isLoading &&
!links[0] ? (
<p>{t("nothing_found")}</p>
) : links[0] ? (
<LinkComponent
@ -93,7 +92,15 @@ export default function Search() {
className="m-auto py-10"
/>
)
)}
)} */}
<Links
editMode={editMode}
links={links}
layout={viewMode}
placeholderCount={1}
useData={data}
/>
</div>
</MainLayout>
);

View File

@ -18,7 +18,7 @@ export default function AccessTokens() {
setRevokeTokenModal(true);
};
const { data: tokens } = useTokens();
const { data: tokens = [] } = useTokens();
return (
<SettingsLayout>

View File

@ -20,7 +20,7 @@ export default function Subscribe() {
const router = useRouter();
const { data: user } = useUser();
const { data: user = {} } = useUser();
useEffect(() => {
const hasInactiveSubscription =

View File

@ -1,31 +1,28 @@
import useLinkStore from "@/store/links";
import { useRouter } from "next/router";
import { FormEvent, useEffect, useState } from "react";
import MainLayout from "@/layouts/MainLayout";
import { Sort, TagIncludingLinkCount, ViewMode } from "@/types/global";
import useLinks from "@/hooks/useLinks";
import { toast } from "react-hot-toast";
import CardView from "@/components/LinkViews/Layouts/CardView";
import ListView from "@/components/LinkViews/Layouts/ListView";
import { useLinks } from "@/hooks/store/links";
import { dropdownTriggerer } from "@/lib/client/utils";
import BulkDeleteLinksModal from "@/components/ModalContent/BulkDeleteLinksModal";
import BulkEditLinksModal from "@/components/ModalContent/BulkEditLinksModal";
import MasonryView from "@/components/LinkViews/Layouts/MasonryView";
import { useTranslation } from "next-i18next";
import getServerSideProps from "@/lib/client/getServerSideProps";
import LinkListOptions from "@/components/LinkListOptions";
import { useRemoveTag, useTags, useUpdateTag } from "@/hooks/store/tags";
import Links from "@/components/LinkViews/Links";
export default function Index() {
const { t } = useTranslation();
const router = useRouter();
const { links } = useLinkStore();
const { data: tags } = useTags();
const { data: tags = [] } = useTags();
const updateTag = useUpdateTag();
const removeTag = useRemoveTag();
const [sortBy, setSortBy] = useState<Sort>(Sort.DateNewestFirst);
const [sortBy, setSortBy] = useState<Sort>(
Number(localStorage.getItem("sortBy")) ?? Sort.DateNewestFirst
);
const [renameTag, setRenameTag] = useState(false);
const [newTagName, setNewTagName] = useState<string>();
@ -40,7 +37,10 @@ export default function Index() {
if (editMode) return setEditMode(false);
}, [router]);
useLinks({ tagId: Number(router.query.id), sort: sortBy });
const { links, data } = useLinks({
sort: sortBy,
tagId: Number(router.query.id),
});
useEffect(() => {
const tag = tags.find((e: any) => e.id === Number(router.query.id));
@ -98,19 +98,10 @@ export default function Index() {
setRenameTag(false);
};
const [viewMode, setViewMode] = useState<string>(
localStorage.getItem("viewMode") || ViewMode.Card
const [viewMode, setViewMode] = useState<ViewMode>(
(localStorage.getItem("viewMode") as ViewMode) || ViewMode.Card
);
const linkView = {
[ViewMode.Card]: CardView,
[ViewMode.List]: ListView,
[ViewMode.Masonry]: MasonryView,
};
// @ts-ignore
const LinkComponent = linkView[viewMode];
return (
<MainLayout>
<div className="p-5 flex flex-col gap-5 w-full">
@ -210,11 +201,12 @@ export default function Index() {
</div>
</LinkListOptions>
<LinkComponent
<Links
editMode={editMode}
links={links.filter((e) =>
e.tags.some((e) => e.id === Number(router.query.id))
)}
links={links}
layout={viewMode}
placeholderCount={1}
useData={data}
/>
</div>
{bulkDeleteLinksModal && (

View File

@ -1,8 +1,5 @@
import { create } from "zustand";
import {
ArchivedFormat,
LinkIncludingShortenedCollectionAndTags,
} from "@/types/global";
import { LinkIncludingShortenedCollectionAndTags } from "@/types/global";
type ResponseObject = {
ok: boolean;
@ -10,24 +7,8 @@ type ResponseObject = {
};
type LinkStore = {
links: LinkIncludingShortenedCollectionAndTags[];
selectedLinks: LinkIncludingShortenedCollectionAndTags[];
setLinks: (
data: LinkIncludingShortenedCollectionAndTags[],
isInitialCall: boolean
) => void;
setSelectedLinks: (links: LinkIncludingShortenedCollectionAndTags[]) => void;
addLink: (
body: LinkIncludingShortenedCollectionAndTags
) => Promise<ResponseObject>;
uploadFile: (
link: LinkIncludingShortenedCollectionAndTags,
file: File
) => Promise<ResponseObject>;
getLink: (linkId: number, publicRoute?: boolean) => Promise<ResponseObject>;
updateLink: (
link: LinkIncludingShortenedCollectionAndTags
) => Promise<ResponseObject>;
updateLinks: (
links: LinkIncludingShortenedCollectionAndTags[],
removePreviousTags: boolean,
@ -36,180 +17,11 @@ type LinkStore = {
"tags" | "collectionId"
>
) => Promise<ResponseObject>;
removeLink: (linkId: number) => Promise<ResponseObject>;
deleteLinksById: (linkIds: number[]) => Promise<ResponseObject>;
resetLinks: () => void;
};
const useLinkStore = create<LinkStore>()((set) => ({
links: [],
selectedLinks: [],
setLinks: async (data, isInitialCall) => {
isInitialCall &&
set(() => ({
links: [],
}));
set((state) => ({
// Filter duplicate links by id
links: [...state.links, ...data].reduce(
(links: LinkIncludingShortenedCollectionAndTags[], item) => {
if (!links.some((link) => link.id === item.id)) {
links.push(item);
}
return links;
},
[]
),
}));
},
setSelectedLinks: (links) => set({ selectedLinks: links }),
addLink: async (body) => {
const response = await fetch("/api/v1/links", {
body: JSON.stringify(body),
headers: {
"Content-Type": "application/json",
},
method: "POST",
});
const data = await response.json();
if (response.ok) {
set((state) => ({
links: [data.response, ...state.links],
}));
}
return { ok: response.ok, data: data.response };
},
uploadFile: async (link, file) => {
let fileType: ArchivedFormat | null = null;
let linkType: "url" | "image" | "pdf" | null = null;
if (file?.type === "image/jpg" || file.type === "image/jpeg") {
fileType = ArchivedFormat.jpeg;
linkType = "image";
} else if (file.type === "image/png") {
fileType = ArchivedFormat.png;
linkType = "image";
} else if (file.type === "application/pdf") {
fileType = ArchivedFormat.pdf;
linkType = "pdf";
} else {
return { ok: false, data: "Invalid file type." };
}
const response = await fetch("/api/v1/links", {
body: JSON.stringify({
...link,
type: linkType,
name: link.name ? link.name : file.name,
}),
headers: {
"Content-Type": "application/json",
},
method: "POST",
});
const data = await response.json();
const createdLink: LinkIncludingShortenedCollectionAndTags = data.response;
console.log(data);
if (response.ok) {
const formBody = new FormData();
file && formBody.append("file", file);
await fetch(
`/api/v1/archives/${(data as any).response.id}?format=${fileType}`,
{
body: formBody,
method: "POST",
}
);
// get file extension
const extension = file.name.split(".").pop() || "";
set((state) => ({
links: [
{
...createdLink,
image:
linkType === "image"
? `archives/${createdLink.collectionId}/${
createdLink.id + extension
}`
: null,
pdf:
linkType === "pdf"
? `archives/${createdLink.collectionId}/${
createdLink.id + ".pdf"
}`
: null,
},
...state.links,
],
}));
}
return { ok: response.ok, data: data.response };
},
getLink: async (linkId, publicRoute) => {
const path = publicRoute
? `/api/v1/public/links/${linkId}`
: `/api/v1/links/${linkId}`;
const response = await fetch(path);
const data = await response.json();
if (response.ok) {
set((state) => {
const linkExists = state.links.some(
(link) => link.id === data.response.id
);
if (linkExists) {
return {
links: state.links.map((e) =>
e.id === data.response.id ? data.response : e
),
};
} else {
return {
links: [...state.links, data.response],
};
}
});
return data;
}
return { ok: response.ok, data: data.response };
},
updateLink: async (link) => {
const response = await fetch(`/api/v1/links/${link.id}`, {
body: JSON.stringify(link),
headers: {
"Content-Type": "application/json",
},
method: "PUT",
});
const data = await response.json();
if (response.ok) {
set((state) => ({
links: state.links.map((e) =>
e.id === data.response.id ? data.response : e
),
}));
}
return { ok: response.ok, data: data.response };
},
updateLinks: async (links, removePreviousTags, newData) => {
const response = await fetch("/api/v1/links", {
body: JSON.stringify({ links, removePreviousTags, newData }),
@ -222,65 +34,11 @@ const useLinkStore = create<LinkStore>()((set) => ({
const data = await response.json();
if (response.ok) {
set((state) => ({
links: state.links.map((e) =>
links.some((link) => link.id === e.id)
? {
...e,
collectionId: newData.collectionId ?? e.collectionId,
collection: {
...e.collection,
id: newData.collectionId ?? e.collection.id,
},
tags: removePreviousTags
? [...(newData.tags ?? [])]
: [...e.tags, ...(newData.tags ?? [])],
}
: e
),
}));
// Update the selected links with the new data
}
return { ok: response.ok, data: data.response };
},
removeLink: async (linkId) => {
const response = await fetch(`/api/v1/links/${linkId}`, {
headers: {
"Content-Type": "application/json",
},
method: "DELETE",
});
const data = await response.json();
if (response.ok) {
set((state) => ({
links: state.links.filter((e) => e.id !== linkId),
}));
}
return { ok: response.ok, data: data.response };
},
deleteLinksById: async (linkIds: number[]) => {
const response = await fetch("/api/v1/links", {
body: JSON.stringify({ linkIds }),
headers: {
"Content-Type": "application/json",
},
method: "DELETE",
});
const data = await response.json();
if (response.ok) {
set((state) => ({
links: state.links.filter((e) => !linkIds.includes(e.id as number)),
}));
}
return { ok: response.ok, data: data.response };
},
resetLinks: () => set({ links: [] }),
}));
export default useLinkStore;

View File

@ -1,8 +1,10 @@
import { Sort } from "@/types/global";
import { create } from "zustand";
type LocalSettings = {
theme?: string;
viewMode?: string;
sortBy?: Sort;
};
type LocalSettingsStore = {
@ -15,10 +17,11 @@ const useLocalSettingsStore = create<LocalSettingsStore>((set) => ({
settings: {
theme: "",
viewMode: "",
sortBy: Sort.DateNewestFirst,
},
updateSettings: async (newSettings) => {
if (
newSettings.theme &&
newSettings.theme !== undefined &&
newSettings.theme !== localStorage.getItem("theme")
) {
localStorage.setItem("theme", newSettings.theme);
@ -29,7 +32,7 @@ const useLocalSettingsStore = create<LocalSettingsStore>((set) => ({
}
if (
newSettings.viewMode &&
newSettings.viewMode !== undefined &&
newSettings.viewMode !== localStorage.getItem("viewMode")
) {
localStorage.setItem("viewMode", newSettings.viewMode);
@ -37,6 +40,13 @@ const useLocalSettingsStore = create<LocalSettingsStore>((set) => ({
// const localTheme = localStorage.getItem("viewMode") || "";
}
if (
newSettings.sortBy !== undefined &&
newSettings.sortBy !== Number(localStorage.getItem("sortBy"))
) {
localStorage.setItem("sortBy", newSettings.sortBy.toString());
}
set((state) => ({ settings: { ...state.settings, ...newSettings } }));
},
setSettings: async () => {

View File

@ -67,7 +67,6 @@ export interface PublicCollectionIncludingLinks extends Collection {
export enum ViewMode {
Card = "card",
Grid = "grid",
List = "list",
Masonry = "masonry",
}
@ -82,7 +81,7 @@ export enum Sort {
}
export type LinkRequestQuery = {
sort: Sort;
sort?: Sort;
cursor?: number;
collectionId?: number;
tagId?: number;

View File

@ -5274,6 +5274,11 @@ react-image-file-resizer@^0.4.8:
resolved "https://registry.yarnpkg.com/react-image-file-resizer/-/react-image-file-resizer-0.4.8.tgz#85f4ae4469fd2867d961568af660ef403d7a79af"
integrity sha512-Ue7CfKnSlsfJ//SKzxNMz8avDgDSpWQDOnTKOp/GNRFJv4dO9L5YGHNEnj40peWkXXAK2OK0eRIoXhOYpUzUTQ==
react-intersection-observer@^9.13.0:
version "9.13.0"
resolved "https://registry.yarnpkg.com/react-intersection-observer/-/react-intersection-observer-9.13.0.tgz#ee10827954cf6ccc204d027f8400a6ddb8df163a"
integrity sha512-y0UvBfjDiXqC8h0EWccyaj4dVBWMxgEx0t5RGNzQsvkfvZwugnKwxpu70StY4ivzYuMajavwUDjH4LJyIki9Lw==
react-is@^16.13.1, react-is@^16.7.0:
version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"