Merge pull request #509 from linkwarden/dev

improved UX + improved performance
This commit is contained in:
Daniel 2024-03-10 13:39:04 +03:30 committed by GitHub
commit 4e20d71a41
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 135 additions and 57 deletions

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

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>
); );

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>
); );
} }

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>
); );
} }

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]" />

View File

@ -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>
); );
} }

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}

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"

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.1";
const { collections } = useCollectionStore(); const { collections } = useCollectionStore();

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 };
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "linkwarden", "name": "linkwarden",
"version": "2.5.0", "version": "2.5.1",
"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>",
@ -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",

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)}

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);

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>

View File

@ -0,0 +1,5 @@
-- CreateIndex
CREATE INDEX "Collection_ownerId_idx" ON "Collection"("ownerId");
-- CreateIndex
CREATE INDEX "UsersAndCollections_userId_idx" ON "UsersAndCollections"("userId");

View File

@ -0,0 +1,2 @@
-- CreateIndex
CREATE INDEX "Tag_ownerId_idx" ON "Tag"("ownerId");

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 {
@ -139,6 +142,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 {

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"