added recent links to dashboard

This commit is contained in:
daniel31x13 2023-10-23 10:45:48 -04:00
parent 697b139493
commit 4252b79586
19 changed files with 461 additions and 53 deletions

View File

@ -240,7 +240,7 @@ export default function LinkCard({ link, count, className }: Props) {
if (target.id !== "expand-dropdown" + link.id) if (target.id !== "expand-dropdown" + link.id)
setExpandDropdown(false); setExpandDropdown(false);
}} }}
className="absolute top-12 right-5 w-fit" className="absolute top-12 right-5 w-40"
/> />
) : null} ) : null}
</div> </div>

View File

@ -11,6 +11,7 @@ import useAccountStore from "@/store/account";
import ProfilePhoto from "@/components/ProfilePhoto"; import ProfilePhoto from "@/components/ProfilePhoto";
import useModalStore from "@/store/modals"; import useModalStore from "@/store/modals";
import { useTheme } from "next-themes"; import { useTheme } from "next-themes";
import useWindowDimensions from "@/hooks/useWindowDimensions";
export default function Navbar() { export default function Navbar() {
const { setModal } = useModalStore(); const { setModal } = useModalStore();
@ -33,7 +34,11 @@ export default function Navbar() {
const [sidebar, setSidebar] = useState(false); const [sidebar, setSidebar] = useState(false);
window.addEventListener("resize", () => setSidebar(false)); const { width } = useWindowDimensions();
useEffect(() => {
setSidebar(false);
}, [width]);
useEffect(() => { useEffect(() => {
setSidebar(false); setSidebar(false);

View File

@ -11,7 +11,7 @@ export default function NoLinksFound({ text }: Props) {
const { setModal } = useModalStore(); const { setModal } = useModalStore();
return ( return (
<div className="border border-solid border-sky-100 dark:border-neutral-700 w-full p-10 rounded-2xl bg-gray-50 dark:bg-neutral-800"> <div className="border border-solid border-sky-100 dark:border-neutral-700 w-full h-full flex flex-col justify-center p-10 rounded-2xl bg-gray-50 dark:bg-neutral-800">
<p className="text-center text-2xl text-black dark:text-white"> <p className="text-center text-2xl text-black dark:text-white">
{text || "You haven't created any Links Here"} {text || "You haven't created any Links Here"}
</p> </p>

View File

@ -2,7 +2,6 @@ import useCollectionStore from "@/store/collections";
import { useEffect } from "react"; import { useEffect } from "react";
import { useSession } from "next-auth/react"; import { useSession } from "next-auth/react";
import useTagStore from "@/store/tags"; import useTagStore from "@/store/tags";
import useLinkStore from "@/store/links";
import useAccountStore from "@/store/account"; import useAccountStore from "@/store/account";
export default function useInitialData() { export default function useInitialData() {

View File

@ -7,11 +7,14 @@ import useLinkStore from "@/store/links";
export default function useLinks( export default function useLinks(
{ {
sort, sort,
searchFilter,
searchQuery,
pinnedOnly,
collectionId, collectionId,
tagId, tagId,
pinnedOnly,
searchQueryString,
searchByName,
searchByUrl,
searchByDescription,
searchByTags,
}: LinkRequestQuery = { sort: 0 } }: LinkRequestQuery = { sort: 0 }
) { ) {
const { links, setLinks, resetLinks } = useLinkStore(); const { links, setLinks, resetLinks } = useLinkStore();
@ -20,20 +23,37 @@ export default function useLinks(
const { reachedBottom, setReachedBottom } = useDetectPageBottom(); const { reachedBottom, setReachedBottom } = useDetectPageBottom();
const getLinks = async (isInitialCall: boolean, cursor?: number) => { const getLinks = async (isInitialCall: boolean, cursor?: number) => {
const requestBody: LinkRequestQuery = { const params = {
cursor,
sort, sort,
searchFilter, cursor,
searchQuery,
pinnedOnly,
collectionId, collectionId,
tagId, tagId,
pinnedOnly,
searchQueryString,
searchByName,
searchByUrl,
searchByDescription,
searchByTags,
}; };
const encodedData = encodeURIComponent(JSON.stringify(requestBody)); 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 queryString = buildQueryString(params);
const response = await fetch( const response = await fetch(
`/api/v1/links?body=${encodeURIComponent(encodedData)}` `/api/v1/${
router.asPath === "/dashboard" ? "dashboard" : "links"
}?${queryString}`
); );
const data = await response.json(); const data = await response.json();
@ -45,7 +65,15 @@ export default function useLinks(
resetLinks(); resetLinks();
getLinks(true); getLinks(true);
}, [router, sort, searchFilter]); }, [
router,
sort,
searchQueryString,
searchByName,
searchByUrl,
searchByDescription,
searchByTags,
]);
useEffect(() => { useEffect(() => {
if (reachedBottom) getLinks(false, links?.at(-1)?.id); if (reachedBottom) getLinks(false, links?.at(-1)?.id);

View File

@ -0,0 +1,25 @@
import React, { useState, useEffect } from "react";
export default function useWindowDimensions() {
const [dimensions, setDimensions] = useState({
height: window.innerHeight,
width: window.innerWidth,
});
useEffect(() => {
function handleResize() {
setDimensions({
height: window.innerHeight,
width: window.innerWidth,
});
}
window.addEventListener("resize", handleResize);
return () => {
window.removeEventListener("resize", handleResize);
};
}, []);
return dimensions;
}

View File

@ -26,7 +26,7 @@ export default function MainLayout({ children }: Props) {
<Sidebar className="fixed top-0" /> <Sidebar className="fixed top-0" />
</div> </div>
<div className="w-full flex flex-col h-screen lg:ml-64 xl:ml-80"> <div className="w-full flex flex-col min-h-screen lg:ml-64 xl:ml-80">
<Navbar /> <Navbar />
{children} {children}
</div> </div>

View File

@ -7,6 +7,7 @@ import ClickAwayHandler from "@/components/ClickAwayHandler";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faBars, faChevronLeft } from "@fortawesome/free-solid-svg-icons"; import { faBars, faChevronLeft } from "@fortawesome/free-solid-svg-icons";
import Link from "next/link"; import Link from "next/link";
import useWindowDimensions from "@/hooks/useWindowDimensions";
interface Props { interface Props {
children: ReactNode; children: ReactNode;
@ -25,7 +26,11 @@ export default function SettingsLayout({ children }: Props) {
const [sidebar, setSidebar] = useState(false); const [sidebar, setSidebar] = useState(false);
window.addEventListener("resize", () => setSidebar(false)); const { width } = useWindowDimensions();
useEffect(() => {
setSidebar(false);
}, [width]);
useEffect(() => { useEffect(() => {
setSidebar(false); setSidebar(false);

View File

@ -0,0 +1,80 @@
import { prisma } from "@/lib/api/db";
import { LinkRequestQuery, Sort } from "@/types/global";
export default async function getDashboardData(
userId: number,
query: LinkRequestQuery
) {
let order: any;
if (query.sort === Sort.DateNewestFirst) order = { createdAt: "desc" };
else if (query.sort === Sort.DateOldestFirst) order = { createdAt: "asc" };
else if (query.sort === Sort.NameAZ) order = { name: "asc" };
else if (query.sort === Sort.NameZA) order = { name: "desc" };
else if (query.sort === Sort.DescriptionAZ) order = { description: "asc" };
else if (query.sort === Sort.DescriptionZA) order = { description: "desc" };
const pinnedLinks = await prisma.link.findMany({
take: Number(process.env.PAGINATION_TAKE_COUNT) || 20,
skip: query.cursor ? 1 : undefined,
cursor: query.cursor ? { id: query.cursor } : undefined,
where: {
AND: [
{
collection: {
OR: [
{ ownerId: userId },
{
members: {
some: { userId },
},
},
],
},
},
{
pinnedBy: { some: { id: userId } },
},
],
},
include: {
tags: true,
collection: true,
pinnedBy: {
where: { id: userId },
select: { id: true },
},
},
orderBy: order || { createdAt: "desc" },
});
const recentlyAddedLinks = await prisma.link.findMany({
take: 6,
where: {
collection: {
OR: [
{ ownerId: userId },
{
members: {
some: { userId },
},
},
],
},
},
include: {
tags: true,
collection: true,
pinnedBy: {
where: { id: userId },
select: { id: true },
},
},
orderBy: order || { createdAt: "desc" },
});
const links = [...recentlyAddedLinks, ...pinnedLinks].sort(
(a, b) => (new Date(b.createdAt) as any) - (new Date(a.createdAt) as any)
);
return { response: links, status: 200 };
}

View File

@ -1,9 +1,7 @@
import { prisma } from "@/lib/api/db"; import { prisma } from "@/lib/api/db";
import { LinkRequestQuery, Sort } from "@/types/global"; import { LinkRequestQuery, Sort } from "@/types/global";
export default async function getLink(userId: number, body: string) { export default async function getLink(userId: number, query: LinkRequestQuery) {
const query: LinkRequestQuery = JSON.parse(decodeURIComponent(body));
const POSTGRES_IS_ENABLED = process.env.DATABASE_URL.startsWith("postgresql"); const POSTGRES_IS_ENABLED = process.env.DATABASE_URL.startsWith("postgresql");
let order: any; let order: any;
@ -16,40 +14,40 @@ export default async function getLink(userId: number, body: string) {
const searchConditions = []; const searchConditions = [];
if (query.searchQuery) { if (query.searchQueryString) {
if (query.searchFilter?.name) { if (query.searchByName) {
searchConditions.push({ searchConditions.push({
name: { name: {
contains: query.searchQuery, contains: query.searchQueryString,
mode: POSTGRES_IS_ENABLED ? "insensitive" : undefined, mode: POSTGRES_IS_ENABLED ? "insensitive" : undefined,
}, },
}); });
} }
if (query.searchFilter?.url) { if (query.searchByUrl) {
searchConditions.push({ searchConditions.push({
url: { url: {
contains: query.searchQuery, contains: query.searchQueryString,
mode: POSTGRES_IS_ENABLED ? "insensitive" : undefined, mode: POSTGRES_IS_ENABLED ? "insensitive" : undefined,
}, },
}); });
} }
if (query.searchFilter?.description) { if (query.searchByDescription) {
searchConditions.push({ searchConditions.push({
description: { description: {
contains: query.searchQuery, contains: query.searchQueryString,
mode: POSTGRES_IS_ENABLED ? "insensitive" : undefined, mode: POSTGRES_IS_ENABLED ? "insensitive" : undefined,
}, },
}); });
} }
if (query.searchFilter?.tags) { if (query.searchByTags) {
searchConditions.push({ searchConditions.push({
tags: { tags: {
some: { some: {
name: { name: {
contains: query.searchQuery, contains: query.searchQueryString,
mode: POSTGRES_IS_ENABLED ? "insensitive" : undefined, mode: POSTGRES_IS_ENABLED ? "insensitive" : undefined,
}, },
OR: [ OR: [
@ -117,7 +115,7 @@ export default async function getLink(userId: number, body: string) {
OR: [ OR: [
...tagCondition, ...tagCondition,
{ {
[query.searchQuery ? "OR" : "AND"]: [ [query.searchQueryString ? "OR" : "AND"]: [
{ {
pinnedBy: query.pinnedOnly pinnedBy: query.pinnedOnly
? { some: { id: userId } } ? { some: { id: userId } }

View File

@ -0,0 +1,27 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { getServerSession } from "next-auth/next";
import { authOptions } from "@/pages/api/v1/auth/[...nextauth]";
import { LinkRequestQuery } from "@/types/global";
import getDashboardData from "@/lib/api/controllers/dashboard/getDashboardData";
export default async function links(req: NextApiRequest, res: NextApiResponse) {
const session = await getServerSession(req, res, authOptions);
if (!session?.user?.id) {
return res.status(401).json({ response: "You must be logged in." });
} else if (session?.user?.isSubscriber === false)
res.status(401).json({
response:
"You are not a subscriber, feel free to reach out to us at support@linkwarden.app in case of any issues.",
});
if (req.method === "GET") {
const convertedData: LinkRequestQuery = {
sort: Number(req.query.sort as string),
cursor: req.query.cursor ? Number(req.query.cursor as string) : undefined,
};
const links = await getDashboardData(session.user.id, convertedData);
return res.status(links.status).json({ response: links.response });
}
}

View File

@ -3,6 +3,7 @@ import { getServerSession } from "next-auth/next";
import { authOptions } from "@/pages/api/v1/auth/[...nextauth]"; import { authOptions } from "@/pages/api/v1/auth/[...nextauth]";
import getLinks from "@/lib/api/controllers/links/getLinks"; import getLinks from "@/lib/api/controllers/links/getLinks";
import postLink from "@/lib/api/controllers/links/postLink"; import postLink from "@/lib/api/controllers/links/postLink";
import { LinkRequestQuery } from "@/types/global";
export default async function links(req: NextApiRequest, res: NextApiResponse) { export default async function links(req: NextApiRequest, res: NextApiResponse) {
const session = await getServerSession(req, res, authOptions); const session = await getServerSession(req, res, authOptions);
@ -16,7 +17,28 @@ export default async function links(req: NextApiRequest, res: NextApiResponse) {
}); });
if (req.method === "GET") { if (req.method === "GET") {
const links = await getLinks(session.user.id, req?.query?.body as string); // Convert the type of the request query to "LinkRequestQuery"
const convertedData: LinkRequestQuery = {
sort: Number(req.query.sort as string),
cursor: req.query.cursor ? Number(req.query.cursor as string) : undefined,
collectionId: req.query.collectionId
? Number(req.query.collectionId as string)
: undefined,
tagId: req.query.tagId ? Number(req.query.tagId as string) : undefined,
pinnedOnly: req.query.pinnedOnly
? req.query.pinnedOnly === "true"
: undefined,
searchQueryString: req.query.searchQueryString
? (req.query.searchQueryString as string)
: undefined,
searchByName: req.query.searchByName === "true" ? true : undefined,
searchByUrl: req.query.searchByUrl === "true" ? true : undefined,
searchByDescription:
req.query.searchByDescription === "true" ? true : undefined,
searchByTags: req.query.searchByTags === "true" ? true : undefined,
};
const links = await getLinks(session.user.id, convertedData);
return res.status(links.status).json({ response: links.response }); return res.status(links.status).json({ response: links.response });
} else if (req.method === "POST") { } else if (req.method === "POST") {
const newlink = await postLink(req.body, session.user.id); const newlink = await postLink(req.body, session.user.id);

View File

@ -52,7 +52,7 @@ export default function Index() {
return ( return (
<MainLayout> <MainLayout>
<div className="p-5 flex flex-col gap-5 w-full"> <div className="p-5 flex flex-col gap-5 w-full h-full">
<div <div
style={{ style={{
backgroundImage: `linear-gradient(-45deg, ${ backgroundImage: `linear-gradient(-45deg, ${

View File

@ -2,6 +2,9 @@ import useCollectionStore from "@/store/collections";
import { import {
faChartSimple, faChartSimple,
faChevronDown, faChevronDown,
faChevronRight,
faClockRotateLeft,
faFileImport,
faThumbTack, faThumbTack,
} from "@fortawesome/free-solid-svg-icons"; } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
@ -11,14 +14,26 @@ import useTagStore from "@/store/tags";
import LinkCard from "@/components/LinkCard"; import LinkCard from "@/components/LinkCard";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import useLinks from "@/hooks/useLinks"; import useLinks from "@/hooks/useLinks";
import Link from "next/link";
import useWindowDimensions from "@/hooks/useWindowDimensions";
import { faPlus } from "@fortawesome/free-solid-svg-icons";
import React from "react";
import useModalStore from "@/store/modals";
import { toast } from "react-hot-toast";
import { MigrationFormat, MigrationRequest } from "@/types/global";
import ClickAwayHandler from "@/components/ClickAwayHandler";
export default function Dashboard() { export default function Dashboard() {
const { collections } = useCollectionStore(); const { collections } = useCollectionStore();
const { links } = useLinkStore(); const { links } = useLinkStore();
const { tags } = useTagStore(); const { tags } = useTagStore();
const { setModal } = useModalStore();
const [numberOfLinks, setNumberOfLinks] = useState(0); const [numberOfLinks, setNumberOfLinks] = useState(0);
const [showRecents, setShowRecents] = useState(3);
const [linkPinDisclosure, setLinkPinDisclosure] = useState<boolean>(() => { const [linkPinDisclosure, setLinkPinDisclosure] = useState<boolean>(() => {
const storedValue = const storedValue =
typeof window !== "undefined" && typeof window !== "undefined" &&
@ -45,6 +60,61 @@ export default function Dashboard() {
); );
}, [linkPinDisclosure]); }, [linkPinDisclosure]);
const handleNumberOfRecents = () => {
if (window.innerWidth > 1550) {
setShowRecents(6);
} else if (window.innerWidth > 1295) {
setShowRecents(4);
} else setShowRecents(3);
};
const { width } = useWindowDimensions();
useEffect(() => {
handleNumberOfRecents();
}, [width]);
const [importDropdown, setImportDropdown] = useState(false);
const importBookmarks = async (e: any, format: MigrationFormat) => {
const file: File = e.target.files[0];
if (file) {
var reader = new FileReader();
reader.readAsText(file, "UTF-8");
reader.onload = async function (e) {
const load = toast.loading("Importing...");
const request: string = e.target?.result as string;
const body: MigrationRequest = {
format,
data: request,
};
const response = await fetch("/api/v1/migration", {
method: "POST",
body: JSON.stringify(body),
});
const data = await response.json();
toast.dismiss(load);
toast.success("Imported the Bookmarks! Reloading the page...");
setImportDropdown(false);
setTimeout(() => {
location.reload();
}, 2000);
};
reader.onerror = function (e) {
console.log("Error:", e);
};
}
};
return ( return (
<MainLayout> <MainLayout>
<div style={{ flex: "1 1 auto" }} className="p-5 flex flex-col gap-5"> <div style={{ flex: "1 1 auto" }} className="p-5 flex flex-col gap-5">
@ -89,6 +159,146 @@ export default function Dashboard() {
</div> </div>
</div> </div>
<div className="flex justify-between items-center">
<div className="flex gap-2 items-center">
<FontAwesomeIcon
icon={faClockRotateLeft}
className="w-5 h-5 text-sky-500 dark:text-sky-500 drop-shadow"
/>
<p className="text-2xl text-black dark:text-white">
Recently Added Links
</p>
</div>
<Link
href="/links"
className="text-black dark:text-white flex items-center gap-2 cursor-pointer"
>
View All
<FontAwesomeIcon
icon={faChevronRight}
className={`w-4 h-4 text-black dark:text-white`}
/>
</Link>
</div>
<div
style={{ flex: "0 1 auto" }}
className="flex flex-col 2xl:flex-row items-start 2xl:gap-2"
>
{links[0] ? (
<div className="w-full">
<div
className={`grid overflow-hidden 2xl:grid-cols-3 xl:grid-cols-2 grid-cols-1 gap-5 w-full`}
>
{links.slice(0, showRecents).map((e, i) => (
<LinkCard key={i} link={e} count={i} />
))}
</div>
</div>
) : (
<div
style={{ flex: "1 1 auto" }}
className="sky-shadow flex flex-col justify-center h-full border border-solid border-sky-100 dark:border-neutral-700 w-full mx-auto p-10 rounded-2xl bg-gray-50 dark:bg-neutral-800"
>
<p className="text-center text-2xl text-black dark:text-white">
View Your Recently Added Links Here!
</p>
<p className="text-center mx-auto max-w-96 w-fit text-gray-500 dark:text-gray-300 text-sm mt-2">
This section will view your latest added Links across every
Collections you have access to.
</p>
<div className="text-center text-black dark:text-white w-full mt-4 flex flex-wrap gap-4 justify-center">
<div
onClick={() => {
setModal({
modal: "LINK",
state: true,
method: "CREATE",
});
}}
className="inline-flex gap-1 relative w-[11.4rem] items-center font-semibold select-none cursor-pointer p-2 px-3 rounded-md dark:hover:bg-sky-600 text-white bg-sky-700 hover:bg-sky-600 duration-100 group"
>
<FontAwesomeIcon
icon={faPlus}
className="w-5 h-5 group-hover:ml-[4.325rem] absolute duration-100"
/>
<span className="group-hover:opacity-0 text-right w-full duration-100">
Create New Link
</span>
</div>
<div className="relative">
<div
onClick={() => setImportDropdown(!importDropdown)}
id="import-dropdown"
className="flex gap-2 select-none text-sm cursor-pointer p-2 px-3 rounded-md border dark:hover:border-sky-600 text-black border-black dark:text-white dark:border-white hover:border-sky-500 hover:dark:border-sky-500 hover:text-sky-500 hover:dark:text-sky-500 duration-100 group"
>
<FontAwesomeIcon
icon={faFileImport}
className="w-5 h-5 duration-100"
id="import-dropdown"
/>
<span
className="text-right w-full duration-100"
id="import-dropdown"
>
Import Your Bookmarks
</span>
</div>
{importDropdown ? (
<ClickAwayHandler
onClickOutside={(e: Event) => {
const target = e.target as HTMLInputElement;
if (target.id !== "import-dropdown")
setImportDropdown(false);
}}
className={`absolute text-black dark:text-white top-10 left-0 w-52 py-1 shadow-md border border-sky-100 dark:border-neutral-700 bg-gray-50 dark:bg-neutral-800 rounded-md flex flex-col z-20`}
>
<div className="cursor-pointer rounded-md">
<label
htmlFor="import-linkwarden-file"
title="JSON File"
className="flex items-center gap-2 py-1 px-2 hover:bg-slate-200 hover:dark:bg-neutral-700 duration-100 cursor-pointer"
>
Linkwarden...
<input
type="file"
name="photo"
id="import-linkwarden-file"
accept=".json"
className="hidden"
onChange={(e) =>
importBookmarks(e, MigrationFormat.linkwarden)
}
/>
</label>
<label
htmlFor="import-html-file"
title="HTML File"
className="flex items-center gap-2 py-1 px-2 hover:bg-slate-200 hover:dark:bg-neutral-700 duration-100 cursor-pointer"
>
Bookmarks HTML file...
<input
type="file"
name="photo"
id="import-html-file"
accept=".html"
className="hidden"
onChange={(e) =>
importBookmarks(e, MigrationFormat.htmlFile)
}
/>
</label>
</div>
</ClickAwayHandler>
) : null}
</div>
</div>
</div>
)}
</div>
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<div className="flex gap-2 items-center"> <div className="flex gap-2 items-center">
<FontAwesomeIcon <FontAwesomeIcon
@ -121,7 +331,7 @@ export default function Dashboard() {
<div className="w-full"> <div className="w-full">
<div <div
className={`grid overflow-hidden 2xl:grid-cols-3 xl:grid-cols-2 grid-cols-1 gap-5 w-full ${ className={`grid overflow-hidden 2xl:grid-cols-3 xl:grid-cols-2 grid-cols-1 gap-5 w-full ${
linkPinDisclosure ? "h-full" : "h-44" linkPinDisclosure ? "h-full" : "h-[22rem]"
}`} }`}
> >
{links {links

View File

@ -19,7 +19,7 @@ export default function Links() {
return ( return (
<MainLayout> <MainLayout>
<div className="p-5 flex flex-col gap-5 w-full"> <div className="p-5 flex flex-col gap-5 w-full h-full">
<div className="flex gap-3 justify-between items-center"> <div className="flex gap-3 justify-between items-center">
<div className="flex gap-2"> <div className="flex gap-2">
<FontAwesomeIcon <FontAwesomeIcon
@ -60,7 +60,7 @@ export default function Links() {
})} })}
</div> </div>
) : ( ) : (
<NoLinksFound /> <NoLinksFound text="You Haven't Created Any Links Yet" />
)} )}
</div> </div>
</MainLayout> </MainLayout>

View File

@ -4,7 +4,7 @@ import SortDropdown from "@/components/SortDropdown";
import useLinks from "@/hooks/useLinks"; import useLinks from "@/hooks/useLinks";
import MainLayout from "@/layouts/MainLayout"; import MainLayout from "@/layouts/MainLayout";
import useLinkStore from "@/store/links"; import useLinkStore from "@/store/links";
import { LinkSearchFilter, Sort } from "@/types/global"; import { Sort } from "@/types/global";
import { faFilter, faSearch, faSort } from "@fortawesome/free-solid-svg-icons"; import { faFilter, faSearch, faSort } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
@ -15,7 +15,7 @@ export default function Links() {
const router = useRouter(); const router = useRouter();
const [searchFilter, setSearchFilter] = useState<LinkSearchFilter>({ const [searchFilter, setSearchFilter] = useState({
name: true, name: true,
url: true, url: true,
description: true, description: true,
@ -27,9 +27,12 @@ export default function Links() {
const [sortBy, setSortBy] = useState<Sort>(Sort.DateNewestFirst); const [sortBy, setSortBy] = useState<Sort>(Sort.DateNewestFirst);
useLinks({ useLinks({
searchFilter: searchFilter,
searchQuery: router.query.query as string,
sort: sortBy, sort: sortBy,
searchQueryString: router.query.query as string,
searchByName: searchFilter.name,
searchByUrl: searchFilter.url,
searchByDescription: searchFilter.description,
searchByTags: searchFilter.tags,
}); });
return ( return (

View File

@ -1,6 +1,6 @@
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faClose, faPenToSquare } from "@fortawesome/free-solid-svg-icons"; import { faClose } from "@fortawesome/free-solid-svg-icons";
import useAccountStore from "@/store/account"; import useAccountStore from "@/store/account";
import { AccountSettings } from "@/types/global"; import { AccountSettings } from "@/types/global";
import { toast } from "react-hot-toast"; import { toast } from "react-hot-toast";
@ -269,7 +269,7 @@ export default function Account() {
Import your data from other platforms. Import your data from other platforms.
</p> </p>
<div <div
onClick={() => setImportDropdown(true)} onClick={() => setImportDropdown(!importDropdown)}
className="w-fit relative" className="w-fit relative"
id="import-dropdown" id="import-dropdown"
> >
@ -286,7 +286,7 @@ export default function Account() {
if (target.id !== "import-dropdown") if (target.id !== "import-dropdown")
setImportDropdown(false); setImportDropdown(false);
}} }}
className={`absolute top-7 left-0 w-48 py-1 shadow-md border border-sky-100 dark:border-neutral-700 bg-gray-50 dark:bg-neutral-800 rounded-md flex flex-col z-20`} className={`absolute top-7 left-0 w-52 py-1 shadow-md border border-sky-100 dark:border-neutral-700 bg-gray-50 dark:bg-neutral-800 rounded-md flex flex-col z-20`}
> >
<div className="cursor-pointer rounded-md"> <div className="cursor-pointer rounded-md">
<label <label

View File

@ -32,7 +32,16 @@ const useLinkStore = create<LinkStore>()((set) => ({
links: [], links: [],
})); }));
set((state) => ({ set((state) => ({
links: [...state.links, ...data], // 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;
},
[]
),
})); }));
}, },
addLink: async (body) => { addLink: async (body) => {
@ -94,6 +103,7 @@ const useLinkStore = create<LinkStore>()((set) => ({
links: state.links.filter((e) => e.id !== linkId), links: state.links.filter((e) => e.id !== linkId),
})); }));
useTagStore.getState().setTags(); useTagStore.getState().setTags();
useCollectionStore.getState().setCollections();
} }
return { ok: response.ok, data: data.response }; return { ok: response.ok, data: data.response };

View File

@ -56,21 +56,17 @@ export enum Sort {
DescriptionZA, DescriptionZA,
} }
export type LinkSearchFilter = {
name: boolean;
url: boolean;
description: boolean;
tags: boolean;
};
export type LinkRequestQuery = { export type LinkRequestQuery = {
sort: Sort;
cursor?: number; cursor?: number;
collectionId?: number; collectionId?: number;
tagId?: number; tagId?: number;
sort: Sort;
searchFilter?: LinkSearchFilter;
searchQuery?: string;
pinnedOnly?: boolean; pinnedOnly?: boolean;
searchQueryString?: string;
searchByName?: boolean;
searchByUrl?: boolean;
searchByDescription?: boolean;
searchByTags?: boolean;
}; };
export type PublicLinkRequestQuery = { export type PublicLinkRequestQuery = {