Compare commits

...

26 Commits

Author SHA1 Message Date
Daniel e51fba41e7 Merge pull request #563 from linkwarden/hotfix/title-fetching
update version number
2024-04-17 18:07:05 -04:00
daniel31x13 e8edd1c9a0 update version number 2024-04-17 18:06:04 -04:00
Daniel f30c652676 Merge pull request #562 from linkwarden/hotfix/title-fetching
added a new env var + bug fixed
2024-04-17 18:03:36 -04:00
daniel31x13 8cf621bc62 added a new env var + bug fixed 2024-04-17 18:02:54 -04:00
Daniel 87eb2471ff Merge pull request #543 from linkwarden/dev
make the status of the script independent from the app
2024-03-27 19:39:09 +03:30
daniel31x13 58b6f7339c make the status of the script independent from the app 2024-03-27 12:08:19 -04:00
Daniel 5503483502 Merge pull request #542 from linkwarden/dev
Dev
2024-03-27 10:58:27 +03:30
daniel31x13 a6d018fb53 Merge branch 'dev' of https://github.com/linkwarden/linkwarden into dev 2024-03-27 03:28:02 -04:00
daniel31x13 3929f32e63 minor fix 2024-03-27 03:27:59 -04:00
Daniel c08522386b Merge pull request #541 from linkwarden/dev
Dev
2024-03-27 10:52:31 +03:30
Daniel b51a876904 Merge pull request #537 from paulhovey/import_date
Import pinboard description and date
2024-03-27 10:51:39 +03:30
daniel31x13 2e2d7baee1 fix imports 2024-03-27 03:20:00 -04:00
Paul Hovey 495af0a752 adds description and tags parsing for pinboard html import 2024-03-23 14:57:34 -05:00
Daniel 388b9d9184 Merge pull request #531 from linkwarden/dev
added architecture.md file + renamed license file
2024-03-20 17:27:07 +03:30
daniel31x13 e5fcf18fa4 added architecture.md file + renamed license file 2024-03-18 18:36:59 -04:00
Daniel a3d3b353a1 Merge pull request #528 from linkwarden/dev
Dev
2024-03-18 02:41:49 +03:30
daniel31x13 546e216ac9 fix browser extension bug 2024-03-17 19:07:51 -04:00
daniel31x13 ffc037b854 bug fixed 2024-03-16 20:09:58 -04:00
Daniel 53a65774f0 Merge pull request #518 from linkwarden/dev
support for arbitrary values in manual installation
2024-03-13 17:26:53 +03:30
daniel31x13 5990d4ce2d support for arbitrary values in manual installation 2024-03-13 09:56:13 -04:00
Daniel ce2eb8eafb Merge pull request #517 from linkwarden/dev
support for other ports in manual installation
2024-03-13 17:21:07 +03:30
daniel31x13 bae4cf1d4f support for other ports in manual installation 2024-03-13 09:48:16 -04:00
Daniel 4e20d71a41 Merge pull request #509 from linkwarden/dev
improved UX + improved performance
2024-03-10 13:39:04 +03:30
daniel31x13 4a0e75c6e5 improved UX + improved performance 2024-03-10 06:08:28 -04:00
Daniel 9fce74971f Merge pull request #500 from linkwarden/dev
update announcement version number
2024-03-07 02:31:21 +03:30
daniel31x13 3feeecdc1d update announcement version number 2024-03-06 18:00:54 -05:00
34 changed files with 345 additions and 111 deletions
+1
View File
@@ -21,6 +21,7 @@ ARCHIVE_TAKE_COUNT=
BROWSER_TIMEOUT= BROWSER_TIMEOUT=
IGNORE_UNAUTHORIZED_CA= IGNORE_UNAUTHORIZED_CA=
IGNORE_HTTPS_ERRORS= IGNORE_HTTPS_ERRORS=
IGNORE_URL_SIZE_LIMIT=
# AWS S3 Settings # AWS S3 Settings
SPACES_KEY= SPACES_KEY=
+45
View File
@@ -0,0 +1,45 @@
# Architecture
This is a summary of the architecture of Linkwarden. It's intended as a primer for collaborators to get a high-level understanding of the project.
When you start Linkwarden, there are mainly two components that run:
- The NextJS app, This is the main app and it's responsible for serving the frontend and handling the API routes.
- [The Background Worker](https://github.com/linkwarden/linkwarden/blob/main/scripts/worker.ts), This is a separate `ts-node` process that runs in the background and is responsible for archiving links.
## Main Tech Stack
- [NextJS](https://github.com/vercel/next.js)
- [TypeScript](https://github.com/microsoft/TypeScript)
- [Tailwind](https://github.com/tailwindlabs/tailwindcss)
- [DaisyUI](https://github.com/saadeghi/daisyui)
- [Prisma](https://github.com/prisma/prisma)
- [Playwright](https://github.com/microsoft/playwright)
- [Zustand](https://github.com/pmndrs/zustand)
## Folder Structure
Here's a summary of the main files and folders in the project:
```
linkwarden
├── components # React components
├── hooks # React reusable hooks
├── layouts # Layouts for pages
├── lib
│   ├── api # Server-side functions (controllers, etc.)
│   ├── client # Client-side functions
│   └── shared # Shared functions between client and server
├── pages # Pages and API routes
├── prisma # Prisma schema and migrations
├── scripts
│   ├── migration # Scripts for breaking changes
│   └── worker.ts # Background worker for archiving links
├── store # Zustand stores
├── styles # Styles
└── types # TypeScript types
```
## Versioning
We use semantic versioning for the project. You can track the changes from the [Releases](https://github.com/linkwarden/linkwarden/releases).
View File
+2 -2
View File
@@ -12,11 +12,11 @@ export default function AnnouncementBar({ toggleAnnouncementBar }: Props) {
<div className="w-fit font-semibold"> <div className="w-fit font-semibold">
🎉 See what&apos;s new in{" "} 🎉 See what&apos;s new in{" "}
<Link <Link
href="https://blog.linkwarden.app/releases/v2.4" href="https://blog.linkwarden.app/releases/v2.5"
target="_blank" target="_blank"
className="underline hover:opacity-50 duration-100" className="underline hover:opacity-50 duration-100"
> >
Linkwarden v2.4 Linkwarden v2.5
</Link> </Link>
! 🥳 ! 🥳
</div> </div>
+4 -1
View File
@@ -47,7 +47,10 @@ const CollectionListing = () => {
useEffect(() => { useEffect(() => {
if (account.username) { if (account.username) {
if (!account.collectionOrder || account.collectionOrder.length === 0) if (
(!account.collectionOrder || account.collectionOrder.length === 0) &&
collections.length > 0
)
updateAccount({ updateAccount({
...account, ...account,
collectionOrder: collections collectionOrder: collections
+24 -22
View File
@@ -26,7 +26,7 @@ export default function FilterSearchDropdown({
> >
<i className="bi-funnel text-neutral text-2xl"></i> <i className="bi-funnel text-neutral text-2xl"></i>
</div> </div>
<ul className="dropdown-content z-[30] menu shadow bg-base-200 border border-neutral-content rounded-box w-44 mt-1"> <ul className="dropdown-content z-[30] menu shadow bg-base-200 border border-neutral-content rounded-box w-56 mt-1">
<li> <li>
<label <label
className="label cursor-pointer flex justify-start" className="label cursor-pointer flex justify-start"
@@ -84,27 +84,6 @@ export default function FilterSearchDropdown({
<span className="label-text">Description</span> <span className="label-text">Description</span>
</label> </label>
</li> </li>
<li>
<label
className="label cursor-pointer flex justify-start"
tabIndex={0}
role="button"
>
<input
type="checkbox"
name="search-filter-checkbox"
className="checkbox checkbox-primary"
checked={searchFilter.textContent}
onChange={() => {
setSearchFilter({
...searchFilter,
textContent: !searchFilter.textContent,
});
}}
/>
<span className="label-text">Full Content</span>
</label>
</li>
<li> <li>
<label <label
className="label cursor-pointer flex justify-start" className="label cursor-pointer flex justify-start"
@@ -126,6 +105,29 @@ export default function FilterSearchDropdown({
<span className="label-text">Tags</span> <span className="label-text">Tags</span>
</label> </label>
</li> </li>
<li>
<label
className="label cursor-pointer flex justify-between"
tabIndex={0}
role="button"
>
<input
type="checkbox"
name="search-filter-checkbox"
className="checkbox checkbox-primary"
checked={searchFilter.textContent}
onChange={() => {
setSearchFilter({
...searchFilter,
textContent: !searchFilter.textContent,
});
}}
/>
<span className="label-text">Full Content</span>
<div className="ml-auto badge badge-sm badge-neutral">Slower</div>
</label>
</li>
</ul> </ul>
</div> </div>
); );
+13 -2
View File
@@ -1,14 +1,16 @@
import LinkCard from "@/components/LinkViews/LinkCard"; import LinkCard from "@/components/LinkViews/LinkCard";
import { LinkIncludingShortenedCollectionAndTags } from "@/types/global"; import { LinkIncludingShortenedCollectionAndTags } from "@/types/global";
import { link } from "fs";
import { GridLoader } from "react-spinners";
export default function CardView({ export default function CardView({
links, links,
showCheckbox = true,
editMode, editMode,
isLoading,
}: { }: {
links: LinkIncludingShortenedCollectionAndTags[]; links: LinkIncludingShortenedCollectionAndTags[];
showCheckbox?: boolean;
editMode?: boolean; editMode?: boolean;
isLoading?: boolean;
}) { }) {
return ( return (
<div className="grid min-[1900px]:grid-cols-4 xl:grid-cols-3 sm:grid-cols-2 grid-cols-1 gap-5"> <div className="grid min-[1900px]:grid-cols-4 xl:grid-cols-3 sm:grid-cols-2 grid-cols-1 gap-5">
@@ -23,6 +25,15 @@ export default function CardView({
/> />
); );
})} })}
{isLoading && links.length > 0 && (
<GridLoader
color="oklch(var(--p))"
loading={true}
size={20}
className="fixed top-5 right-5 opacity-50 z-30"
/>
)}
</div> </div>
); );
} }
+12
View File
@@ -1,12 +1,15 @@
import LinkList from "@/components/LinkViews/LinkList"; import LinkList from "@/components/LinkViews/LinkList";
import { LinkIncludingShortenedCollectionAndTags } from "@/types/global"; import { LinkIncludingShortenedCollectionAndTags } from "@/types/global";
import { GridLoader } from "react-spinners";
export default function ListView({ export default function ListView({
links, links,
editMode, editMode,
isLoading,
}: { }: {
links: LinkIncludingShortenedCollectionAndTags[]; links: LinkIncludingShortenedCollectionAndTags[];
editMode?: boolean; editMode?: boolean;
isLoading?: boolean;
}) { }) {
return ( return (
<div className="flex gap-1 flex-col"> <div className="flex gap-1 flex-col">
@@ -21,6 +24,15 @@ export default function ListView({
/> />
); );
})} })}
{isLoading && links.length > 0 && (
<GridLoader
color="oklch(var(--p))"
loading={true}
size={20}
className="fixed top-5 right-5 opacity-50 z-30"
/>
)}
</div> </div>
); );
} }
+12 -6
View File
@@ -162,12 +162,18 @@ export default function LinkCard({ link, flipDropdown, editMode }: Props) {
{unescapeString(link.name || link.description) || link.url} {unescapeString(link.name || link.description) || link.url}
</p> </p>
<div title={link.url || ""} className="w-fit"> <Link
<div className="flex gap-1 item-center select-none text-neutral mt-1"> href={link.url || ""}
<i className="bi-link-45deg text-lg mt-[0.15rem] leading-none"></i> target="_blank"
<p className="text-sm truncate">{shortendURL}</p> title={link.url || ""}
</div> onClick={(e) => {
</div> e.stopPropagation();
}}
className="flex gap-1 item-center select-none text-neutral mt-1 hover:opacity-70 duration-100"
>
<i className="bi-link-45deg text-lg mt-[0.10rem] leading-none"></i>
<p className="text-sm truncate">{shortendURL}</p>
</Link>
</div> </div>
<hr className="divider mt-2 mb-1 last:hidden border-t border-neutral-content h-[1px]" /> <hr className="divider mt-2 mb-1 last:hidden border-t border-neutral-content h-[1px]" />
@@ -2,6 +2,7 @@ import {
CollectionIncludingMembersAndLinkCount, CollectionIncludingMembersAndLinkCount,
LinkIncludingShortenedCollectionAndTags, LinkIncludingShortenedCollectionAndTags,
} from "@/types/global"; } from "@/types/global";
import Link from "next/link";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import React from "react"; import React from "react";
@@ -15,12 +16,12 @@ export default function LinkCollection({
const router = useRouter(); const router = useRouter();
return ( return (
<div <Link
href={`/collections/${link.collection.id}`}
onClick={(e) => { onClick={(e) => {
e.preventDefault(); e.stopPropagation();
router.push(`/collections/${link.collection.id}`);
}} }}
className="flex items-center gap-1 max-w-full w-fit hover:opacity-70 duration-100" className="flex items-center gap-1 max-w-full w-fit hover:opacity-70 duration-100 select-none"
title={collection?.name} title={collection?.name}
> >
<i <i
@@ -28,6 +29,6 @@ export default function LinkCollection({
style={{ color: collection?.color }} style={{ color: collection?.color }}
></i> ></i>
<p className="truncate capitalize">{collection?.name}</p> <p className="truncate capitalize">{collection?.name}</p>
</div> </Link>
); );
} }
@@ -6,14 +6,13 @@ export default function LinkDate({
}: { }: {
link: LinkIncludingShortenedCollectionAndTags; link: LinkIncludingShortenedCollectionAndTags;
}) { }) {
const formattedDate = new Date(link.createdAt as string).toLocaleString( const formattedDate = new Date(
"en-US", (link.importDate || link.createdAt) as string
{ ).toLocaleString("en-US", {
year: "numeric", year: "numeric",
month: "short", month: "short",
day: "numeric", day: "numeric",
} });
);
return ( return (
<div className="flex items-center gap-1 text-neutral"> <div className="flex items-center gap-1 text-neutral">
+12 -4
View File
@@ -144,10 +144,18 @@ export default function LinkCardCompact({
<LinkCollection link={link} collection={collection} /> <LinkCollection link={link} collection={collection} />
) : undefined} ) : undefined}
{link.url ? ( {link.url ? (
<div className="flex items-center gap-1 w-fit text-neutral truncate"> <Link
<i className="bi-link-45deg text-lg" /> href={link.url || ""}
<p className="truncate w-full select-none">{shortendURL}</p> target="_blank"
</div> title={link.url || ""}
onClick={(e) => {
e.stopPropagation();
}}
className="flex gap-1 item-center select-none text-neutral mt-1 hover:opacity-70 duration-100"
>
<i className="bi-link-45deg text-lg mt-[0.1rem] leading-none"></i>
<p className="text-sm truncate">{shortendURL}</p>
</Link>
) : ( ) : (
<div className="badge badge-primary badge-sm my-1 select-none"> <div className="badge badge-primary badge-sm my-1 select-none">
{link.type} {link.type}
+1 -1
View File
@@ -65,7 +65,7 @@ export default function Navbar() {
<ToggleDarkMode className="hidden sm:inline-grid" /> <ToggleDarkMode className="hidden sm:inline-grid" />
<div className="dropdown dropdown-end sm:inline-block hidden"> <div className="dropdown dropdown-end sm:inline-block hidden">
<div className="tooltip tooltip-bottom z-10" data-tip="Create New..."> <div className="tooltip tooltip-bottom" data-tip="Create New...">
<div <div
tabIndex={0} tabIndex={0}
role="button" role="button"
+6 -2
View File
@@ -34,6 +34,8 @@ export default function ReadableView({ link }: Props) {
const [imageError, setImageError] = useState<boolean>(false); const [imageError, setImageError] = useState<boolean>(false);
const [colorPalette, setColorPalette] = useState<RGBColor[]>(); const [colorPalette, setColorPalette] = useState<RGBColor[]>();
const [date, setDate] = useState<Date | string>();
const colorThief = new ColorThief(); const colorThief = new ColorThief();
const router = useRouter(); const router = useRouter();
@@ -54,6 +56,8 @@ export default function ReadableView({ link }: Props) {
}; };
fetchLinkContent(); fetchLinkContent();
setDate(link.importDate || link.createdAt);
}, [link]); }, [link]);
useEffect(() => { useEffect(() => {
@@ -211,8 +215,8 @@ export default function ReadableView({ link }: Props) {
</div> </div>
<p className="min-w-fit text-sm text-neutral"> <p className="min-w-fit text-sm text-neutral">
{link?.createdAt {date
? new Date(link?.createdAt).toLocaleString("en-US", { ? new Date(date).toLocaleString("en-US", {
year: "numeric", year: "numeric",
month: "long", month: "long",
day: "numeric", day: "numeric",
+1 -1
View File
@@ -4,7 +4,7 @@ import { useRouter } from "next/router";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
export default function SettingsSidebar({ className }: { className?: string }) { export default function SettingsSidebar({ className }: { className?: string }) {
const LINKWARDEN_VERSION = "v2.5.0"; const LINKWARDEN_VERSION = "v2.5.2";
const { collections } = useCollectionStore(); const { collections } = useCollectionStore();
+9 -1
View File
@@ -1,5 +1,5 @@
import { LinkRequestQuery } from "@/types/global"; import { LinkRequestQuery } from "@/types/global";
import { useEffect } from "react"; import { useEffect, useState } from "react";
import useDetectPageBottom from "./useDetectPageBottom"; import useDetectPageBottom from "./useDetectPageBottom";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import useLinkStore from "@/store/links"; import useLinkStore from "@/store/links";
@@ -22,6 +22,8 @@ export default function useLinks(
useLinkStore(); useLinkStore();
const router = useRouter(); const router = useRouter();
const [isLoading, setIsLoading] = useState(true);
const { reachedBottom, setReachedBottom } = useDetectPageBottom(); const { reachedBottom, setReachedBottom } = useDetectPageBottom();
const getLinks = async (isInitialCall: boolean, cursor?: number) => { const getLinks = async (isInitialCall: boolean, cursor?: number) => {
@@ -61,10 +63,14 @@ export default function useLinks(
basePath = "/api/v1/public/collections/links"; basePath = "/api/v1/public/collections/links";
} else basePath = "/api/v1/links"; } else basePath = "/api/v1/links";
setIsLoading(true);
const response = await fetch(`${basePath}?${queryString}`); const response = await fetch(`${basePath}?${queryString}`);
const data = await response.json(); const data = await response.json();
setIsLoading(false);
if (response.ok) setLinks(data.response, isInitialCall); if (response.ok) setLinks(data.response, isInitialCall);
}; };
@@ -92,4 +98,6 @@ export default function useLinks(
setReachedBottom(false); setReachedBottom(false);
}, [reachedBottom]); }, [reachedBottom]);
return { isLoading };
} }
+7 -6
View File
@@ -32,11 +32,12 @@ export default async function checkSubscriptionByEmail(email: string) {
customer.subscriptions?.data.some((subscription) => { customer.subscriptions?.data.some((subscription) => {
subscription.current_period_end; subscription.current_period_end;
active = subscription.items.data.some( active =
(e) => subscription.items.data.some(
(e.price.id === MONTHLY_PRICE_ID && e.price.active === true) || (e) =>
(e.price.id === YEARLY_PRICE_ID && e.price.active === true) (e.price.id === MONTHLY_PRICE_ID && e.price.active === true) ||
); (e.price.id === YEARLY_PRICE_ID && e.price.active === true)
) || false;
stripeSubscriptionId = subscription.id; stripeSubscriptionId = subscription.id;
currentPeriodStart = subscription.current_period_start * 1000; currentPeriodStart = subscription.current_period_start * 1000;
currentPeriodEnd = subscription.current_period_end * 1000; currentPeriodEnd = subscription.current_period_end * 1000;
@@ -44,7 +45,7 @@ export default async function checkSubscriptionByEmail(email: string) {
}); });
return { return {
active, active: active || false,
stripeSubscriptionId, stripeSubscriptionId,
currentPeriodStart, currentPeriodStart,
currentPeriodEnd, currentPeriodEnd,
+1
View File
@@ -48,6 +48,7 @@ export default async function postLink(
return { response: "Collection is not accessible.", status: 401 }; return { response: "Collection is not accessible.", status: 401 };
link.collection.id = findCollection.id; link.collection.id = findCollection.id;
link.collection.ownerId = findCollection.ownerId;
} else { } else {
const collection = await prisma.collection.create({ const collection = await prisma.collection.create({
data: { data: {
@@ -2,6 +2,7 @@ import { prisma } from "@/lib/api/db";
import createFolder from "@/lib/api/storage/createFolder"; import createFolder from "@/lib/api/storage/createFolder";
import { JSDOM } from "jsdom"; import { JSDOM } from "jsdom";
import { parse, Node, Element, TextNode } from "himalaya"; import { parse, Node, Element, TextNode } from "himalaya";
import { writeFileSync } from "fs";
const MAX_LINKS_PER_USER = Number(process.env.MAX_LINKS_PER_USER) || 30000; const MAX_LINKS_PER_USER = Number(process.env.MAX_LINKS_PER_USER) || 30000;
@@ -36,7 +37,9 @@ export default async function importFromHTMLFile(
const jsonData = parse(document.documentElement.outerHTML); const jsonData = parse(document.documentElement.outerHTML);
for (const item of jsonData) { const processedArray = processNodes(jsonData);
for (const item of processedArray) {
console.log(item); console.log(item);
await processBookmarks(userId, item as Element); await processBookmarks(userId, item as Element);
} }
@@ -74,7 +77,9 @@ async function processBookmarks(
} else if (item.type === "element" && item.tagName === "a") { } else if (item.type === "element" && item.tagName === "a") {
// process link // process link
const linkUrl = item?.attributes.find((e) => e.key === "href")?.value; const linkUrl = item?.attributes.find(
(e) => e.key.toLowerCase() === "href"
)?.value;
const linkName = ( const linkName = (
item?.children.find((e) => e.type === "text") as TextNode item?.children.find((e) => e.type === "text") as TextNode
)?.content; )?.content;
@@ -82,14 +87,33 @@ async function processBookmarks(
.find((e) => e.key === "tags") .find((e) => e.key === "tags")
?.value.split(","); ?.value.split(",");
// set date if available
const linkDateValue = item?.attributes.find(
(e) => e.key.toLowerCase() === "add_date"
)?.value;
const linkDate = linkDateValue
? new Date(Number(linkDateValue) * 1000)
: undefined;
let linkDesc =
(
(
item?.children?.find(
(e) => e.type === "element" && e.tagName === "dd"
) as Element
)?.children[0] as TextNode
)?.content || "";
if (linkUrl && parentCollectionId) { if (linkUrl && parentCollectionId) {
await createLink( await createLink(
userId, userId,
linkUrl, linkUrl,
parentCollectionId, parentCollectionId,
linkName, linkName,
"", linkDesc,
linkTags linkTags,
linkDate
); );
} else if (linkUrl) { } else if (linkUrl) {
// create a collection named "Imported Bookmarks" and add the link to it // create a collection named "Imported Bookmarks" and add the link to it
@@ -100,8 +124,9 @@ async function processBookmarks(
linkUrl, linkUrl,
collectionId, collectionId,
linkName, linkName,
"", linkDesc,
linkTags linkTags,
linkDate
); );
} }
@@ -160,7 +185,8 @@ const createLink = async (
collectionId: number, collectionId: number,
name?: string, name?: string,
description?: string, description?: string,
tags?: string[] tags?: string[],
importDate?: Date
) => { ) => {
await prisma.link.create({ await prisma.link.create({
data: { data: {
@@ -193,6 +219,48 @@ const createLink = async (
}), }),
} }
: undefined, : undefined,
importDate: importDate || undefined,
}, },
}); });
}; };
function processNodes(nodes: Node[]) {
const findAndProcessDL = (node: Node) => {
if (node.type === "element" && node.tagName === "dl") {
processDLChildren(node);
} else if (
node.type === "element" &&
node.children &&
node.children.length
) {
node.children.forEach((child) => findAndProcessDL(child));
}
};
const processDLChildren = (dlNode: Element) => {
dlNode.children.forEach((child, i) => {
if (child.type === "element" && child.tagName === "dt") {
const nextSibling = dlNode.children[i + 1];
if (
nextSibling &&
nextSibling.type === "element" &&
nextSibling.tagName === "dd"
) {
const aElement = child.children.find(
(el) => el.type === "element" && el.tagName === "a"
);
if (aElement && aElement.type === "element") {
// Add the 'dd' element as a child of the 'a' element
aElement.children.push(nextSibling);
// Remove the 'dd' from the parent 'dl' to avoid duplicate processing
dlNode.children.splice(i + 1, 1);
// Adjust the loop counter due to the removal
}
}
}
});
};
nodes.forEach(findAndProcessDL);
return nodes;
}
+20 -2
View File
@@ -1,17 +1,35 @@
import fetch from "node-fetch"; import fetch from "node-fetch";
import https from "https"; import https from "https";
import { SocksProxyAgent } from "socks-proxy-agent";
export default async function validateUrlSize(url: string) { export default async function validateUrlSize(url: string) {
if (process.env.IGNORE_URL_SIZE_LIMIT === "true") return null;
try { try {
const httpsAgent = new https.Agent({ const httpsAgent = new https.Agent({
rejectUnauthorized: rejectUnauthorized:
process.env.IGNORE_UNAUTHORIZED_CA === "true" ? false : true, process.env.IGNORE_UNAUTHORIZED_CA === "true" ? false : true,
}); });
const response = await fetch(url, { let fetchOpts = {
method: "HEAD", method: "HEAD",
agent: httpsAgent, agent: httpsAgent,
}); };
if (process.env.PROXY) {
let proxy = new URL(process.env.PROXY);
if (process.env.PROXY_USERNAME) {
proxy.username = process.env.PROXY_USERNAME;
proxy.password = process.env.PROXY_PASSWORD || "";
}
fetchOpts = {
method: "HEAD",
agent: new SocksProxyAgent(proxy.toString()),
};
}
const response = await fetch(url, fetchOpts);
const totalSizeMB = const totalSizeMB =
Number(response.headers.get("content-length")) / Math.pow(1024, 2); Number(response.headers.get("content-length")) / Math.pow(1024, 2);
+15 -17
View File
@@ -17,15 +17,7 @@ export default async function verifySubscription(
const currentDate = new Date(); const currentDate = new Date();
if ( if (!subscription?.active || currentDate > subscription.currentPeriodEnd) {
subscription &&
currentDate > subscription.currentPeriodEnd &&
!subscription.active
) {
return null;
}
if (!subscription || currentDate > subscription.currentPeriodEnd) {
const { const {
active, active,
stripeSubscriptionId, stripeSubscriptionId,
@@ -59,15 +51,21 @@ export default async function verifySubscription(
}, },
}) })
.catch((err) => console.log(err)); .catch((err) => console.log(err));
} } else if (!active) {
const subscription = await prisma.subscription.findFirst({
where: {
userId: user.id,
},
});
if (!active) { if (subscription)
if (user.username) await prisma.subscription.delete({
// await prisma.user.update({ where: {
// where: { id: user.id }, userId: user.id,
// data: { username: null }, },
// }); });
return null;
return null;
} }
} }
+1 -1
View File
@@ -52,7 +52,7 @@ export default async function verifyUser({
} }
if (STRIPE_SECRET_KEY) { if (STRIPE_SECRET_KEY) {
const subscribedUser = verifySubscription(user); const subscribedUser = await verifySubscription(user);
if (!subscribedUser) { if (!subscribedUser) {
res.status(401).json({ res.status(401).json({
+17 -6
View File
@@ -27,14 +27,25 @@ export default async function getTitle(url: string) {
fetchOpts = { agent: new SocksProxyAgent(proxy.toString()) }; //TODO: add support for http/https proxies fetchOpts = { agent: new SocksProxyAgent(proxy.toString()) }; //TODO: add support for http/https proxies
} }
const response = await fetch(url, fetchOpts); const responsePromise = fetch(url, fetchOpts);
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => {
reject(new Error("Fetch title timeout"));
}, 10 * 1000); // Stop after 10 seconds
});
const text = await response.text(); const response = await Promise.race([responsePromise, timeoutPromise]);
// regular expression to find the <title> tag if ((response as any)?.status) {
let match = text.match(/<title.*>([^<]*)<\/title>/); const text = await (response as any).text();
if (match) return match[1];
else return ""; // regular expression to find the <title> tag
let match = text.match(/<title.*>([^<]*)<\/title>/);
if (match) return match[1];
else return "";
} else {
return "";
}
} catch (err) { } catch (err) {
console.log(err); console.log(err);
} }
+4 -3
View File
@@ -1,6 +1,6 @@
{ {
"name": "linkwarden", "name": "linkwarden",
"version": "2.5.0", "version": "0.0.0",
"main": "index.js", "main": "index.js",
"repository": "https://github.com/linkwarden/linkwarden.git", "repository": "https://github.com/linkwarden/linkwarden.git",
"author": "Daniel31X13 <daniel31x13@gmail.com>", "author": "Daniel31X13 <daniel31x13@gmail.com>",
@@ -10,10 +10,10 @@
"seed": "node ./prisma/seed.js" "seed": "node ./prisma/seed.js"
}, },
"scripts": { "scripts": {
"dev": "concurrently -k \"next dev\" \"yarn worker:dev\"", "dev": "concurrently -k -P \"next dev {@}\" \"yarn worker:dev\" --",
"worker:dev": "nodemon --skip-project scripts/worker.ts", "worker:dev": "nodemon --skip-project scripts/worker.ts",
"worker:prod": "ts-node --transpile-only --skip-project scripts/worker.ts", "worker:prod": "ts-node --transpile-only --skip-project scripts/worker.ts",
"start": "concurrently \"next start\" \"yarn worker:prod\"", "start": "concurrently -P \"next start {@}\" \"yarn worker:prod\" --",
"build": "next build", "build": "next build",
"lint": "next lint", "lint": "next lint",
"format": "prettier --write \"**/*.{ts,tsx,js,json,md}\"" "format": "prettier --write \"**/*.{ts,tsx,js,json,md}\""
@@ -61,6 +61,7 @@
"react-hot-toast": "^2.4.1", "react-hot-toast": "^2.4.1",
"react-image-file-resizer": "^0.4.8", "react-image-file-resizer": "^0.4.8",
"react-select": "^5.7.4", "react-select": "^5.7.4",
"react-spinners": "^0.13.8",
"socks-proxy-agent": "^8.0.2", "socks-proxy-agent": "^8.0.2",
"stripe": "^12.13.0", "stripe": "^12.13.0",
"vaul": "^0.8.8", "vaul": "^0.8.8",
+1 -5
View File
@@ -168,10 +168,7 @@ export default function Dashboard() {
> >
{links[0] ? ( {links[0] ? (
<div className="w-full"> <div className="w-full">
<LinkComponent <LinkComponent links={links.slice(0, showLinks)} />
links={links.slice(0, showLinks)}
showCheckbox={false}
/>
</div> </div>
) : ( ) : (
<div <div
@@ -282,7 +279,6 @@ export default function Dashboard() {
{links.some((e) => e.pinnedBy && e.pinnedBy[0]) ? ( {links.some((e) => e.pinnedBy && e.pinnedBy[0]) ? (
<div className="w-full"> <div className="w-full">
<LinkComponent <LinkComponent
showCheckbox={false}
links={links links={links
.filter((e) => e.pinnedBy && e.pinnedBy[0]) .filter((e) => e.pinnedBy && e.pinnedBy[0])
.slice(0, showLinks)} .slice(0, showLinks)}
+1 -1
View File
@@ -60,8 +60,8 @@ export default function PublicCollections() {
name: true, name: true,
url: true, url: true,
description: true, description: true,
textContent: true,
tags: true, tags: true,
textContent: false,
}); });
const [sortBy, setSortBy] = useState<Sort>(Sort.DateNewestFirst); const [sortBy, setSortBy] = useState<Sort>(Sort.DateNewestFirst);
+21 -7
View File
@@ -5,12 +5,13 @@ import MainLayout from "@/layouts/MainLayout";
import useLinkStore from "@/store/links"; import useLinkStore from "@/store/links";
import { Sort, ViewMode } from "@/types/global"; import { Sort, ViewMode } from "@/types/global";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import React, { useState } from "react"; import React, { useEffect, useState } from "react";
import ViewDropdown from "@/components/ViewDropdown"; import ViewDropdown from "@/components/ViewDropdown";
import CardView from "@/components/LinkViews/Layouts/CardView"; import CardView from "@/components/LinkViews/Layouts/CardView";
// import GridView from "@/components/LinkViews/Layouts/GridView"; // import GridView from "@/components/LinkViews/Layouts/GridView";
import ListView from "@/components/LinkViews/Layouts/ListView"; import ListView from "@/components/LinkViews/Layouts/ListView";
import PageHeader from "@/components/PageHeader"; import PageHeader from "@/components/PageHeader";
import { GridLoader, PropagateLoader } from "react-spinners";
export default function Search() { export default function Search() {
const { links } = useLinkStore(); const { links } = useLinkStore();
@@ -21,8 +22,8 @@ export default function Search() {
name: true, name: true,
url: true, url: true,
description: true, description: true,
textContent: true,
tags: true, tags: true,
textContent: false,
}); });
const [viewMode, setViewMode] = useState<string>( const [viewMode, setViewMode] = useState<string>(
@@ -30,7 +31,7 @@ export default function Search() {
); );
const [sortBy, setSortBy] = useState<Sort>(Sort.DateNewestFirst); const [sortBy, setSortBy] = useState<Sort>(Sort.DateNewestFirst);
useLinks({ const { isLoading } = useLinks({
sort: sortBy, sort: sortBy,
searchQueryString: decodeURIComponent(router.query.q as string), searchQueryString: decodeURIComponent(router.query.q as string),
searchByName: searchFilter.name, searchByName: searchFilter.name,
@@ -40,6 +41,10 @@ export default function Search() {
searchByTags: searchFilter.tags, searchByTags: searchFilter.tags,
}); });
useEffect(() => {
console.log("isLoading", isLoading);
}, [isLoading]);
const linkView = { const linkView = {
[ViewMode.Card]: CardView, [ViewMode.Card]: CardView,
// [ViewMode.Grid]: GridView, // [ViewMode.Grid]: GridView,
@@ -51,7 +56,7 @@ export default function Search() {
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 justify-between"> <div className="flex justify-between">
<PageHeader icon={"bi-search"} title={"Search Results"} /> <PageHeader icon={"bi-search"} title={"Search Results"} />
@@ -67,15 +72,24 @@ export default function Search() {
</div> </div>
</div> </div>
{links[0] ? ( {!isLoading && !links[0] ? (
<LinkComponent links={links} />
) : (
<p> <p>
Nothing found.{" "} Nothing found.{" "}
<span className="font-bold text-xl" title="Shruggie"> <span className="font-bold text-xl" title="Shruggie">
¯\_()_/¯ ¯\_()_/¯
</span> </span>
</p> </p>
) : links[0] ? (
<LinkComponent links={links} isLoading={isLoading} />
) : (
isLoading && (
<GridLoader
color="oklch(var(--p))"
loading={true}
size={20}
className="m-auto py-10"
/>
)
)} )}
</div> </div>
</MainLayout> </MainLayout>
@@ -0,0 +1,5 @@
-- CreateIndex
CREATE INDEX "Collection_ownerId_idx" ON "Collection"("ownerId");
-- CreateIndex
CREATE INDEX "UsersAndCollections_userId_idx" ON "UsersAndCollections"("userId");
@@ -0,0 +1,2 @@
-- CreateIndex
CREATE INDEX "Tag_ownerId_idx" ON "Tag"("ownerId");
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Link" ADD COLUMN "importDate" TIMESTAMP(3);
+5
View File
@@ -93,6 +93,8 @@ model Collection {
links Link[] links Link[]
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt updatedAt DateTime @default(now()) @updatedAt
@@index([ownerId])
} }
model UsersAndCollections { model UsersAndCollections {
@@ -107,6 +109,7 @@ model UsersAndCollections {
updatedAt DateTime @default(now()) @updatedAt updatedAt DateTime @default(now()) @updatedAt
@@id([userId, collectionId]) @@id([userId, collectionId])
@@index([userId])
} }
model Link { model Link {
@@ -125,6 +128,7 @@ model Link {
pdf String? pdf String?
readable String? readable String?
lastPreserved DateTime? lastPreserved DateTime?
importDate DateTime?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt updatedAt DateTime @default(now()) @updatedAt
} }
@@ -139,6 +143,7 @@ model Tag {
updatedAt DateTime @default(now()) @updatedAt updatedAt DateTime @default(now()) @updatedAt
@@unique([name, ownerId]) @@unique([name, ownerId])
@@index([ownerId])
} }
model Subscription { model Subscription {
+1
View File
@@ -13,6 +13,7 @@ declare global {
MAX_LINKS_PER_USER?: string; MAX_LINKS_PER_USER?: string;
ARCHIVE_TAKE_COUNT?: string; ARCHIVE_TAKE_COUNT?: string;
IGNORE_UNAUTHORIZED_CA?: string; IGNORE_UNAUTHORIZED_CA?: string;
IGNORE_URL_SIZE_LIMIT?: string;
SPACES_KEY?: string; SPACES_KEY?: string;
SPACES_SECRET?: string; SPACES_SECRET?: string;
+7 -1
View File
@@ -7,10 +7,16 @@ type OptionalExcluding<T, TRequired extends keyof T> = Partial<T> &
export interface LinkIncludingShortenedCollectionAndTags export interface LinkIncludingShortenedCollectionAndTags
extends Omit< extends Omit<
Link, Link,
"id" | "createdAt" | "collectionId" | "updatedAt" | "lastPreserved" | "id"
| "createdAt"
| "collectionId"
| "updatedAt"
| "lastPreserved"
| "importDate"
> { > {
id?: number; id?: number;
createdAt?: string; createdAt?: string;
importDate?: string;
collectionId?: number; collectionId?: number;
tags: Tag[]; tags: Tag[];
pinnedBy?: { pinnedBy?: {
+5
View File
@@ -5211,6 +5211,11 @@ react-select@^5.7.4:
react-transition-group "^4.3.0" react-transition-group "^4.3.0"
use-isomorphic-layout-effect "^1.1.2" use-isomorphic-layout-effect "^1.1.2"
react-spinners@^0.13.8:
version "0.13.8"
resolved "https://registry.yarnpkg.com/react-spinners/-/react-spinners-0.13.8.tgz#5262571be0f745d86bbd49a1e6b49f9f9cb19acc"
integrity sha512-3e+k56lUkPj0vb5NDXPVFAOkPC//XyhKPJjvcGjyMNPWsBKpplfeyialP74G7H7+It7KzhtET+MvGqbKgAqpZA==
react-style-singleton@^2.2.1: react-style-singleton@^2.2.1:
version "2.2.1" version "2.2.1"
resolved "https://registry.yarnpkg.com/react-style-singleton/-/react-style-singleton-2.2.1.tgz#f99e420492b2d8f34d38308ff660b60d0b1205b4" resolved "https://registry.yarnpkg.com/react-style-singleton/-/react-style-singleton-2.2.1.tgz#f99e420492b2d8f34d38308ff660b60d0b1205b4"