commit
8fdc503f55
|
@ -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>
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
|
@ -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>
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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 };
|
||||||
|
}
|
|
@ -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 } }
|
||||||
|
|
|
@ -0,0 +1,118 @@
|
||||||
|
import { prisma } from "@/lib/api/db";
|
||||||
|
import bcrypt from "bcrypt";
|
||||||
|
import removeFolder from "@/lib/api/storage/removeFolder";
|
||||||
|
import Stripe from "stripe";
|
||||||
|
|
||||||
|
type DeleteUserBody = {
|
||||||
|
password: string;
|
||||||
|
cancellation_details?: {
|
||||||
|
comment?: string;
|
||||||
|
feedback?: Stripe.SubscriptionCancelParams.CancellationDetails.Feedback;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function deleteUserById(
|
||||||
|
userId: number,
|
||||||
|
body: DeleteUserBody
|
||||||
|
) {
|
||||||
|
// First, we retrieve the user from the database
|
||||||
|
const user = await prisma.user.findUnique({
|
||||||
|
where: { id: userId },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return {
|
||||||
|
response: "User not found.",
|
||||||
|
status: 404,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then, we check if the provided password matches the one stored in the database
|
||||||
|
const isPasswordValid = bcrypt.compareSync(body.password, user.password);
|
||||||
|
|
||||||
|
if (!isPasswordValid) {
|
||||||
|
return {
|
||||||
|
response: "Invalid password.",
|
||||||
|
status: 401, // Unauthorized
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete the user and all related data within a transaction
|
||||||
|
await prisma.$transaction(async (prisma) => {
|
||||||
|
// Delete whitelisted users
|
||||||
|
await prisma.whitelistedUser.deleteMany({
|
||||||
|
where: { userId },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Delete links
|
||||||
|
await prisma.link.deleteMany({
|
||||||
|
where: { collection: { ownerId: userId } },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Delete tags
|
||||||
|
await prisma.tag.deleteMany({
|
||||||
|
where: { ownerId: userId },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Delete collections
|
||||||
|
const collections = await prisma.collection.findMany({
|
||||||
|
where: { ownerId: userId },
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const collection of collections) {
|
||||||
|
// Delete related users and collections relations
|
||||||
|
await prisma.usersAndCollections.deleteMany({
|
||||||
|
where: { collectionId: collection.id },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Optionally delete archive folders associated with collections
|
||||||
|
removeFolder({ filePath: `archives/${collection.id}` });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete collections after cleaning up related data
|
||||||
|
await prisma.collection.deleteMany({
|
||||||
|
where: { ownerId: userId },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Optionally delete user's avatar
|
||||||
|
removeFolder({ filePath: `uploads/avatar/${userId}.jpg` });
|
||||||
|
|
||||||
|
// Finally, delete the user
|
||||||
|
await prisma.user.delete({
|
||||||
|
where: { id: userId },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (process.env.STRIPE_SECRET_KEY) {
|
||||||
|
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
|
||||||
|
apiVersion: "2022-11-15",
|
||||||
|
});
|
||||||
|
|
||||||
|
const listByEmail = await stripe.customers.list({
|
||||||
|
email: user.email?.toLowerCase(),
|
||||||
|
expand: ["data.subscriptions"],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (listByEmail.data[0].subscriptions?.data[0].id) {
|
||||||
|
const deleted = await stripe.subscriptions.cancel(
|
||||||
|
listByEmail.data[0].subscriptions?.data[0].id,
|
||||||
|
{
|
||||||
|
cancellation_details: {
|
||||||
|
comment: body.cancellation_details?.comment,
|
||||||
|
feedback: body.cancellation_details?.feedback,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
response: deleted,
|
||||||
|
status: 200,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
response: "User account and all related data deleted successfully.",
|
||||||
|
status: 200,
|
||||||
|
};
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
import { prisma } from "@/lib/api/db";
|
import { prisma } from "@/lib/api/db";
|
||||||
|
|
||||||
export default async function getUser(
|
export default async function getPublicUserById(
|
||||||
targetId: number | string,
|
targetId: number | string,
|
||||||
isId: boolean,
|
isId: boolean,
|
||||||
requestingUsername?: string
|
requestingUsername?: string
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { prisma } from "@/lib/api/db";
|
import { prisma } from "@/lib/api/db";
|
||||||
|
|
||||||
export default async function getUser(userId: number) {
|
export default async function getUserById(userId: number) {
|
||||||
const user = await prisma.user.findUnique({
|
const user = await prisma.user.findUnique({
|
||||||
where: {
|
where: {
|
||||||
id: userId,
|
id: userId,
|
||||||
|
|
|
@ -9,7 +9,7 @@ import createFolder from "@/lib/api/storage/createFolder";
|
||||||
const emailEnabled =
|
const emailEnabled =
|
||||||
process.env.EMAIL_FROM && process.env.EMAIL_SERVER ? true : false;
|
process.env.EMAIL_FROM && process.env.EMAIL_SERVER ? true : false;
|
||||||
|
|
||||||
export default async function updateUser(
|
export default async function updateUserById(
|
||||||
sessionUser: {
|
sessionUser: {
|
||||||
id: number;
|
id: number;
|
||||||
username: string;
|
username: string;
|
||||||
|
|
|
@ -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 });
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { authOptions } from "@/pages/api/v1/auth/[...nextauth]";
|
||||||
import getUserById from "@/lib/api/controllers/users/userId/getUserById";
|
import getUserById from "@/lib/api/controllers/users/userId/getUserById";
|
||||||
import getPublicUserById from "@/lib/api/controllers/users/userId/getPublicUserById";
|
import getPublicUserById from "@/lib/api/controllers/users/userId/getPublicUserById";
|
||||||
import updateUserById from "@/lib/api/controllers/users/userId/updateUserById";
|
import updateUserById from "@/lib/api/controllers/users/userId/updateUserById";
|
||||||
|
import deleteUserById from "@/lib/api/controllers/users/userId/deleteUserById";
|
||||||
|
|
||||||
export default async function users(req: NextApiRequest, res: NextApiResponse) {
|
export default async function users(req: NextApiRequest, res: NextApiResponse) {
|
||||||
const session = await getServerSession(req, res, authOptions);
|
const session = await getServerSession(req, res, authOptions);
|
||||||
|
@ -36,5 +37,12 @@ export default async function users(req: NextApiRequest, res: NextApiResponse) {
|
||||||
} else if (req.method === "PUT") {
|
} else if (req.method === "PUT") {
|
||||||
const updated = await updateUserById(session.user, req.body);
|
const updated = await updateUserById(session.user, req.body);
|
||||||
return res.status(updated.status).json({ response: updated.response });
|
return res.status(updated.status).json({ response: updated.response });
|
||||||
|
} else if (
|
||||||
|
req.method === "DELETE" &&
|
||||||
|
session.user.id === Number(req.query.id)
|
||||||
|
) {
|
||||||
|
console.log(req.body);
|
||||||
|
const updated = await deleteUserById(session.user.id, req.body);
|
||||||
|
return res.status(updated.status).json({ response: updated.response });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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, ${
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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 (
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 };
|
||||||
|
|
|
@ -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 = {
|
||||||
|
|
Ŝarĝante…
Reference in New Issue