import bookmarks from other platforms + many other improvements

This commit is contained in:
daniel31x13 2023-10-03 14:34:13 +03:30
parent 8fc8874063
commit f15e298cc3
13 changed files with 216 additions and 172 deletions

View File

@ -82,7 +82,7 @@ export default function DeleteCollection({
</div>
<div className="flex flex-col gap-3">
<p className="text-black dark:text-white select-none text-center">
<p className="text-black dark:text-white text-center">
To confirm, type &quot;
<span className="font-bold">{collection.name}</span>
&quot; in the box below:

View File

@ -7,8 +7,8 @@ import useLinkStore from "@/store/links";
import { faPlus } from "@fortawesome/free-solid-svg-icons";
import RequiredBadge from "../../RequiredBadge";
import { useSession } from "next-auth/react";
// import useCollectionStore from "@/store/collections";
// import { useRouter } from "next/router";
import useCollectionStore from "@/store/collections";
import { useRouter } from "next/router";
import SubmitButton from "../../SubmitButton";
import { toast } from "react-hot-toast";
import Link from "next/link";
@ -55,27 +55,40 @@ export default function AddOrEditLink({
const { updateLink, addLink } = useLinkStore();
// const router = useRouter();
const router = useRouter();
const { collections } = useCollectionStore();
// const { collections } = useCollectionStore();
useEffect(() => {
if (method === "CREATE") {
if (router.query.id) {
const currentCollection = collections.find(
(e) => e.id == Number(router.query.id)
);
// useEffect(() => {
// if (router.query.id) {
// const currentCollection = collections.find(
// (e) => e.id == Number(router.query.id)
// );
// if (currentCollection && currentCollection.ownerId)
// setLink({
// ...link,
// collection: {
// id: currentCollection.id,
// name: currentCollection.name,
// ownerId: currentCollection.ownerId,
// },
// });
// }
// }, []);
if (
currentCollection &&
currentCollection.ownerId &&
router.asPath.startsWith("/collections/")
)
setLink({
...link,
collection: {
id: currentCollection.id,
name: currentCollection.name,
ownerId: currentCollection.ownerId,
},
});
} else
setLink({
...link,
collection: {
// id: ,
name: "Unorganized",
ownerId: data?.user.id as number,
},
});
}
}, []);
const setTags = (e: any) => {
const tagNames = e.map((e: any) => {
@ -147,27 +160,29 @@ export default function AddOrEditLink({
<p className="text-sm text-black dark:text-white mb-2">
Collection
</p>
<CollectionSelection
onChange={setCollection}
// defaultValue={{
// label: link.collection.name,
// value: link.collection.id,
// }}
defaultValue={
link.collection.id
? {
value: link.collection.id,
label: link.collection.name,
}
: {
value: null as unknown as number,
label: "Unorganized",
}
}
/>
{link.collection.name ? (
<CollectionSelection
onChange={setCollection}
// defaultValue={{
// label: link.collection.name,
// value: link.collection.id,
// }}
defaultValue={
link.collection.id
? {
value: link.collection.id,
label: link.collection.name,
}
: {
value: null as unknown as number,
label: "Unorganized",
}
}
/>
) : null}
</div>
</div>
) : null}
) : undefined}
{optionsExpanded ? (
<div>
@ -187,20 +202,22 @@ export default function AddOrEditLink({
<p className="text-sm text-black dark:text-white mb-2">
Collection
</p>
<CollectionSelection
onChange={setCollection}
defaultValue={
link.collection.name && link.collection.id
? {
value: link.collection.id,
label: link.collection.name,
}
: {
value: null as unknown as number,
label: "Unorganized",
}
}
/>
{link.collection.name ? (
<CollectionSelection
onChange={setCollection}
defaultValue={
link.collection.name && link.collection.id
? {
value: link.collection.id,
label: link.collection.name,
}
: {
value: null as unknown as number,
label: "Unorganized",
}
}
/>
) : undefined}
</div>
) : undefined}

View File

@ -50,7 +50,7 @@ export default function PrivacySettings({
return wordsArray;
};
const postJSONFile = async (e: any) => {
const postBookmarkFile = async (e: any) => {
const file: File = e.target.files[0];
if (file) {
@ -156,9 +156,7 @@ export default function PrivacySettings({
</div>
<div className="mt-5">
<p className="text-sm text-black dark:text-white mb-2">
Import/Export Data
</p>
<p className="text-sm text-black dark:text-white mb-2">Import Data</p>
<div className="flex gap-2">
<div
@ -178,22 +176,22 @@ export default function PrivacySettings({
const target = e.target as HTMLInputElement;
if (target.id !== "import-dropdown") setImportDropdown(false);
}}
className={`absolute top-7 left-0 w-36 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-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`}
>
<div className="cursor-pointer rounded-md">
<label
htmlFor="import-file"
title="JSON"
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"
>
Linkwarden
Bookmarks HTML file...
<input
type="file"
name="photo"
id="import-file"
accept=".json"
accept=".html"
className="hidden"
onChange={postJSONFile}
onChange={postBookmarkFile}
/>
</label>
</div>
@ -201,11 +199,12 @@ export default function PrivacySettings({
) : null}
</div>
<Link className="w-fit" href="/api/migration">
{/* Commented out for now. */}
{/* <Link className="w-fit" href="/api/migration">
<div className="border border-slate-200 dark:border-neutral-700 rounded-md bg-white dark:bg-neutral-800 px-2 text-center select-none cursor-pointer duration-100 hover:border-sky-300 hover:dark:border-sky-600">
Export Data
</div>
</Link>
</Link> */}
</div>
</div>

View File

@ -5,21 +5,22 @@ const useDetectPageBottom = () => {
useEffect(() => {
const handleScroll = () => {
const offsetHeight = document.documentElement.offsetHeight;
const innerHeight = window.innerHeight;
const scrollTop = document.documentElement.scrollTop;
const totalHeight = document.documentElement.scrollHeight;
const scrolledHeight = window.scrollY + window.innerHeight;
const hasReachedBottom = offsetHeight - (innerHeight + scrollTop) <= 100;
setReachedBottom(hasReachedBottom);
if (scrolledHeight >= totalHeight) {
setReachedBottom(true);
}
};
window.addEventListener("scroll", handleScroll);
return () => window.removeEventListener("scroll", handleScroll);
return () => {
window.removeEventListener("scroll", handleScroll);
};
}, []);
return reachedBottom;
return { reachedBottom, setReachedBottom };
};
export default useDetectPageBottom;

View File

@ -17,7 +17,7 @@ export default function useLinks(
const { links, setLinks, resetLinks } = useLinkStore();
const router = useRouter();
const hasReachedBottom = useDetectPageBottom();
const { reachedBottom, setReachedBottom } = useDetectPageBottom();
const getLinks = async (isInitialCall: boolean, cursor?: number) => {
const requestBody: LinkRequestQuery = {
@ -48,6 +48,8 @@ export default function useLinks(
}, [router, sort, searchFilter]);
useEffect(() => {
if (hasReachedBottom) getLinks(false, links?.at(-1)?.id);
}, [hasReachedBottom]);
if (reachedBottom) getLinks(false, links?.at(-1)?.id);
setReachedBottom(false);
}, [reachedBottom]);
}

View File

@ -8,6 +8,7 @@ export default async function updateLink(
link: LinkIncludingShortenedCollectionAndTags,
userId: number
) {
console.log(link);
if (!link || !link.collection.id)
return {
response: "Please choose a valid link and collection.",

View File

@ -1,6 +1,6 @@
import { prisma } from "@/lib/api/db";
export default async function getData(userId: number) {
export default async function exportData(userId: number) {
const user = await prisma.user.findUnique({
where: { id: userId },
include: {

View File

@ -0,0 +1,97 @@
import { prisma } from "@/lib/api/db";
import { Backup } from "@/types/global";
import createFolder from "@/lib/api/storage/createFolder";
import { JSDOM } from "jsdom";
export default async function importData(userId: number, rawData: string) {
try {
const dom = new JSDOM(rawData);
const document = dom.window.document;
const folders = document.querySelectorAll("H3");
// @ts-ignore
for (const folder of folders) {
const findCollection = await prisma.user.findUnique({
where: {
id: userId,
},
select: {
collections: {
where: {
name: folder.textContent.trim(),
},
},
},
});
const checkIfCollectionExists = findCollection?.collections[0];
let collectionId = findCollection?.collections[0]?.id;
if (!checkIfCollectionExists || !collectionId) {
const newCollection = await prisma.collection.create({
data: {
name: folder.textContent.trim(),
description: "",
color: "#0ea5e9",
isPublic: false,
ownerId: userId,
},
});
createFolder({ filePath: `archives/${newCollection.id}` });
collectionId = newCollection.id;
}
createFolder({ filePath: `archives/${collectionId}` });
const bookmarks = folder.nextElementSibling.querySelectorAll("A");
for (const bookmark of bookmarks) {
await prisma.link.create({
data: {
name: bookmark.textContent.trim(),
url: bookmark.getAttribute("HREF"),
tags: bookmark.getAttribute("TAGS")
? {
connectOrCreate: bookmark
.getAttribute("TAGS")
.split(",")
.map((tag: string) =>
tag
? {
where: {
name_ownerId: {
name: tag.trim(),
ownerId: userId,
},
},
create: {
name: tag.trim(),
owner: {
connect: {
id: userId,
},
},
},
}
: undefined
),
}
: undefined,
description: bookmark.getAttribute("DESCRIPTION")
? bookmark.getAttribute("DESCRIPTION")
: "",
collectionId: collectionId,
createdAt: new Date(),
},
});
}
}
} catch (err) {
console.log(err);
}
return { response: "Success.", status: 200 };
}

View File

@ -1,88 +0,0 @@
import { prisma } from "@/lib/api/db";
import { Backup } from "@/types/global";
import createFolder from "@/lib/api/storage/createFolder";
export default async function getData(userId: number, rawData: any) {
const data: Backup = JSON.parse(rawData);
// Import collections
try {
data.collections.forEach(async (e) => {
e.name = e.name.trim();
const findCollection = await prisma.user.findUnique({
where: {
id: userId,
},
select: {
collections: {
where: {
name: e.name,
},
},
},
});
const checkIfCollectionExists = findCollection?.collections[0];
let collectionId = findCollection?.collections[0]?.id;
if (!checkIfCollectionExists) {
const newCollection = await prisma.collection.create({
data: {
owner: {
connect: {
id: userId,
},
},
name: e.name,
description: e.description,
color: e.color,
},
});
createFolder({ filePath: `archives/${newCollection.id}` });
collectionId = newCollection.id;
}
// Import Links
e.links.forEach(async (e) => {
const newLink = await prisma.link.create({
data: {
url: e.url,
name: e.name,
description: e.description,
collection: {
connect: {
id: collectionId,
},
},
tags: {
connectOrCreate: e.tags.map((tag) => ({
where: {
name_ownerId: {
name: tag.name.trim(),
ownerId: userId,
},
},
create: {
name: tag.name.trim(),
owner: {
connect: {
id: userId,
},
},
},
})),
},
},
});
});
});
} catch (err) {
console.log(err);
}
return { response: "Success.", status: 200 };
}

View File

@ -54,6 +54,7 @@
"devDependencies": {
"@playwright/test": "^1.35.1",
"@types/bcrypt": "^5.0.0",
"@types/jsdom": "^21.1.3",
"autoprefixer": "^10.4.14",
"postcss": "^8.4.26",
"prisma": "^5.1.0",

View File

@ -1,8 +1,8 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { getServerSession } from "next-auth/next";
import { authOptions } from "@/pages/api/auth/[...nextauth]";
import getData from "@/lib/api/controllers/migration/getData";
import postData from "@/lib/api/controllers/migration/postData";
import exportData from "@/lib/api/controllers/migration/exportData";
import importData from "@/lib/api/controllers/migration/importData";
export default async function users(req: NextApiRequest, res: NextApiResponse) {
const session = await getServerSession(req, res, authOptions);
@ -16,7 +16,7 @@ export default async function users(req: NextApiRequest, res: NextApiResponse) {
});
if (req.method === "GET") {
const data = await getData(session.user.id);
const data = await exportData(session.user.id);
if (data.status === 200)
return res
@ -25,7 +25,7 @@ export default async function users(req: NextApiRequest, res: NextApiResponse) {
.status(data.status)
.json(data.response);
} else if (req.method === "POST") {
const data = await postData(session.user.id, req.body);
const data = await importData(session.user.id, req.body);
return res.status(data.status).json({ response: data.response });
}
}

View File

@ -222,7 +222,7 @@ export default function Index() {
</div>
</div>
</div>
{links[0] ? (
{links.some((e) => e.collectionId === Number(router.query.id)) ? (
<div className="grid grid-cols-1 2xl:grid-cols-3 xl:grid-cols-2 gap-5">
{links
.filter((e) => e.collectionId === Number(router.query.id))

View File

@ -1466,6 +1466,15 @@
resolved "https://registry.yarnpkg.com/@types/crypto-js/-/crypto-js-4.1.1.tgz#602859584cecc91894eb23a4892f38cfa927890d"
integrity sha512-BG7fQKZ689HIoc5h+6D2Dgq1fABRa0RbBWKBd9SP/MVRVXROflpm5fhwyATX5duFmbStzyzyycPB8qUYKDH3NA==
"@types/jsdom@^21.1.3":
version "21.1.3"
resolved "https://registry.yarnpkg.com/@types/jsdom/-/jsdom-21.1.3.tgz#a88c5dc65703e1b10b2a7839c12db49662b43ff0"
integrity sha512-1zzqSP+iHJYV4lB3lZhNBa012pubABkj9yG/GuXuf6LZH1cSPIJBqFDrm5JX65HHt6VOnNYdTui/0ySerRbMgA==
dependencies:
"@types/node" "*"
"@types/tough-cookie" "*"
parse5 "^7.0.0"
"@types/json5@^0.0.29":
version "0.0.29"
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
@ -1521,6 +1530,11 @@
resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.2.tgz#1a62f89525723dde24ba1b01b092bf5df8ad4d39"
integrity sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==
"@types/tough-cookie@*":
version "4.0.3"
resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-4.0.3.tgz#3d06b6769518450871fbc40770b7586334bdfd90"
integrity sha512-THo502dA5PzG/sfQH+42Lw3fvmYkceefOspdCwpHRul8ik2Jv1K8I5OZz1AT3/rs46kwgMCe9bSBmDLYkkOMGg==
"@typescript-eslint/parser@^5.42.0":
version "5.49.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.49.0.tgz#d699734b2f20e16351e117417d34a2bc9d7c4b90"
@ -3946,7 +3960,7 @@ parse-json@^5.0.0:
json-parse-even-better-errors "^2.3.0"
lines-and-columns "^1.1.6"
parse5@^7.1.2:
parse5@^7.0.0, parse5@^7.1.2:
version "7.1.2"
resolved "https://registry.yarnpkg.com/parse5/-/parse5-7.1.2.tgz#0736bebbfd77793823240a23b7fc5e010b7f8e32"
integrity sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==