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>
<div className="flex flex-col gap-3"> <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; To confirm, type &quot;
<span className="font-bold">{collection.name}</span> <span className="font-bold">{collection.name}</span>
&quot; in the box below: &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 { faPlus } from "@fortawesome/free-solid-svg-icons";
import RequiredBadge from "../../RequiredBadge"; import RequiredBadge from "../../RequiredBadge";
import { useSession } from "next-auth/react"; import { useSession } from "next-auth/react";
// import useCollectionStore from "@/store/collections"; import useCollectionStore from "@/store/collections";
// import { useRouter } from "next/router"; import { useRouter } from "next/router";
import SubmitButton from "../../SubmitButton"; import SubmitButton from "../../SubmitButton";
import { toast } from "react-hot-toast"; import { toast } from "react-hot-toast";
import Link from "next/link"; import Link from "next/link";
@ -55,27 +55,40 @@ export default function AddOrEditLink({
const { updateLink, addLink } = useLinkStore(); 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 (
// if (router.query.id) { currentCollection &&
// const currentCollection = collections.find( currentCollection.ownerId &&
// (e) => e.id == Number(router.query.id) router.asPath.startsWith("/collections/")
// ); )
setLink({
// if (currentCollection && currentCollection.ownerId) ...link,
// setLink({ collection: {
// ...link, id: currentCollection.id,
// collection: { name: currentCollection.name,
// id: currentCollection.id, ownerId: currentCollection.ownerId,
// name: currentCollection.name, },
// ownerId: currentCollection.ownerId, });
// }, } else
// }); setLink({
// } ...link,
// }, []); collection: {
// id: ,
name: "Unorganized",
ownerId: data?.user.id as number,
},
});
}
}, []);
const setTags = (e: any) => { const setTags = (e: any) => {
const tagNames = e.map((e: any) => { const tagNames = e.map((e: any) => {
@ -147,6 +160,7 @@ export default function AddOrEditLink({
<p className="text-sm text-black dark:text-white mb-2"> <p className="text-sm text-black dark:text-white mb-2">
Collection Collection
</p> </p>
{link.collection.name ? (
<CollectionSelection <CollectionSelection
onChange={setCollection} onChange={setCollection}
// defaultValue={{ // defaultValue={{
@ -165,9 +179,10 @@ export default function AddOrEditLink({
} }
} }
/> />
</div>
</div>
) : null} ) : null}
</div>
</div>
) : undefined}
{optionsExpanded ? ( {optionsExpanded ? (
<div> <div>
@ -187,6 +202,7 @@ export default function AddOrEditLink({
<p className="text-sm text-black dark:text-white mb-2"> <p className="text-sm text-black dark:text-white mb-2">
Collection Collection
</p> </p>
{link.collection.name ? (
<CollectionSelection <CollectionSelection
onChange={setCollection} onChange={setCollection}
defaultValue={ defaultValue={
@ -201,6 +217,7 @@ export default function AddOrEditLink({
} }
} }
/> />
) : undefined}
</div> </div>
) : undefined} ) : undefined}

View File

@ -50,7 +50,7 @@ export default function PrivacySettings({
return wordsArray; return wordsArray;
}; };
const postJSONFile = async (e: any) => { const postBookmarkFile = async (e: any) => {
const file: File = e.target.files[0]; const file: File = e.target.files[0];
if (file) { if (file) {
@ -156,9 +156,7 @@ export default function PrivacySettings({
</div> </div>
<div className="mt-5"> <div className="mt-5">
<p className="text-sm text-black dark:text-white mb-2"> <p className="text-sm text-black dark:text-white mb-2">Import Data</p>
Import/Export Data
</p>
<div className="flex gap-2"> <div className="flex gap-2">
<div <div
@ -178,22 +176,22 @@ export default function PrivacySettings({
const target = e.target as HTMLInputElement; const target = e.target as HTMLInputElement;
if (target.id !== "import-dropdown") setImportDropdown(false); 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"> <div className="cursor-pointer rounded-md">
<label <label
htmlFor="import-file" 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" 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 <input
type="file" type="file"
name="photo" name="photo"
id="import-file" id="import-file"
accept=".json" accept=".html"
className="hidden" className="hidden"
onChange={postJSONFile} onChange={postBookmarkFile}
/> />
</label> </label>
</div> </div>
@ -201,11 +199,12 @@ export default function PrivacySettings({
) : null} ) : null}
</div> </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"> <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 Export Data
</div> </div>
</Link> </Link> */}
</div> </div>
</div> </div>

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
import { prisma } from "@/lib/api/db"; 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({ const user = await prisma.user.findUnique({
where: { id: userId }, where: { id: userId },
include: { 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": { "devDependencies": {
"@playwright/test": "^1.35.1", "@playwright/test": "^1.35.1",
"@types/bcrypt": "^5.0.0", "@types/bcrypt": "^5.0.0",
"@types/jsdom": "^21.1.3",
"autoprefixer": "^10.4.14", "autoprefixer": "^10.4.14",
"postcss": "^8.4.26", "postcss": "^8.4.26",
"prisma": "^5.1.0", "prisma": "^5.1.0",

View File

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

View File

@ -222,7 +222,7 @@ export default function Index() {
</div> </div>
</div> </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"> <div className="grid grid-cols-1 2xl:grid-cols-3 xl:grid-cols-2 gap-5">
{links {links
.filter((e) => e.collectionId === Number(router.query.id)) .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" resolved "https://registry.yarnpkg.com/@types/crypto-js/-/crypto-js-4.1.1.tgz#602859584cecc91894eb23a4892f38cfa927890d"
integrity sha512-BG7fQKZ689HIoc5h+6D2Dgq1fABRa0RbBWKBd9SP/MVRVXROflpm5fhwyATX5duFmbStzyzyycPB8qUYKDH3NA== 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": "@types/json5@^0.0.29":
version "0.0.29" version "0.0.29"
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" 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" resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.2.tgz#1a62f89525723dde24ba1b01b092bf5df8ad4d39"
integrity sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew== 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": "@typescript-eslint/parser@^5.42.0":
version "5.49.0" version "5.49.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.49.0.tgz#d699734b2f20e16351e117417d34a2bc9d7c4b90" 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" json-parse-even-better-errors "^2.3.0"
lines-and-columns "^1.1.6" lines-and-columns "^1.1.6"
parse5@^7.1.2: parse5@^7.0.0, parse5@^7.1.2:
version "7.1.2" version "7.1.2"
resolved "https://registry.yarnpkg.com/parse5/-/parse5-7.1.2.tgz#0736bebbfd77793823240a23b7fc5e010b7f8e32" resolved "https://registry.yarnpkg.com/parse5/-/parse5-7.1.2.tgz#0736bebbfd77793823240a23b7fc5e010b7f8e32"
integrity sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw== integrity sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==