mirror of
https://github.com/linkwarden/linkwarden.git
synced 2026-03-03 02:37:02 +00:00
Refactor pages to use consistent layout handling (yes, I forgot to do that until now :P)
This commit is contained in:
@@ -36,11 +36,10 @@ const CollectionListing = () => {
|
||||
const updateCollection = useUpdateCollection();
|
||||
const { data: collections = [], isLoading } = useCollections();
|
||||
|
||||
const { data: user, refetch } = useUser();
|
||||
const { data: user } = useUser();
|
||||
const updateUser = useUpdateUser();
|
||||
|
||||
const router = useRouter();
|
||||
const currentPath = router.asPath;
|
||||
|
||||
const [tree, setTree] = useState<TreeData | undefined>();
|
||||
|
||||
@@ -53,7 +52,7 @@ const CollectionListing = () => {
|
||||
user?.collectionOrder
|
||||
);
|
||||
} else return undefined;
|
||||
}, [collections, user, router]);
|
||||
}, [collections, user]);
|
||||
|
||||
useEffect(() => {
|
||||
setTree(initialTree);
|
||||
@@ -281,7 +280,7 @@ const CollectionListing = () => {
|
||||
<Tree
|
||||
tree={tree}
|
||||
renderItem={(itemProps) =>
|
||||
renderItem({ ...itemProps }, currentPath, droppableActive)
|
||||
renderItem({ ...itemProps }, router.asPath, droppableActive)
|
||||
}
|
||||
onExpand={onExpand}
|
||||
onCollapse={onCollapse}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect } from "react";
|
||||
import React, { ReactElement, ReactNode, useEffect } from "react";
|
||||
import "@/styles/globals.css";
|
||||
import "bootstrap-icons/font/bootstrap-icons.css";
|
||||
import { SessionProvider } from "next-auth/react";
|
||||
@@ -13,6 +13,7 @@ import { isPWA } from "@/lib/utils";
|
||||
import { appWithTranslation } from "next-i18next";
|
||||
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { NextPage } from "next";
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
@@ -22,12 +23,19 @@ const queryClient = new QueryClient({
|
||||
},
|
||||
});
|
||||
|
||||
function App({
|
||||
Component,
|
||||
pageProps,
|
||||
}: AppProps<{
|
||||
session: Session;
|
||||
}>) {
|
||||
export type NextPageWithLayout<P = {}, IP = P> = NextPage<P, IP> & {
|
||||
getLayout?: (page: ReactElement) => ReactNode;
|
||||
};
|
||||
|
||||
type PageProps = { session?: Session | null };
|
||||
|
||||
type AppPropsWithLayout = AppProps<PageProps> & {
|
||||
Component: NextPageWithLayout<PageProps>;
|
||||
};
|
||||
|
||||
function App({ Component, pageProps }: AppPropsWithLayout) {
|
||||
const getLayout = Component.getLayout ?? ((page) => page);
|
||||
|
||||
useEffect(() => {
|
||||
if (isPWA()) {
|
||||
const meta = document.createElement("meta");
|
||||
@@ -98,7 +106,7 @@ function App({
|
||||
</ToastBar>
|
||||
)}
|
||||
</Toaster>
|
||||
<Component {...pageProps} />
|
||||
{getLayout(<Component {...pageProps} />)}
|
||||
{/* </GetData> */}
|
||||
</AuthRedirect>
|
||||
</SessionProvider>
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
ViewMode,
|
||||
} from "@linkwarden/types";
|
||||
import { useRouter } from "next/router";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import React, { ReactElement, useEffect, useState } from "react";
|
||||
import MainLayout from "@/layouts/MainLayout";
|
||||
import ProfilePhoto from "@/components/ProfilePhoto";
|
||||
import usePermissions from "@/hooks/usePermissions";
|
||||
@@ -37,8 +37,9 @@ import {
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import DragNDrop from "@/components/DragNDrop";
|
||||
import { NextPageWithLayout } from "../_app";
|
||||
|
||||
export default function Index() {
|
||||
const Page: NextPageWithLayout = () => {
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
|
||||
@@ -117,294 +118,294 @@ export default function Index() {
|
||||
activeLink={activeLink}
|
||||
setActiveLink={setActiveLink}
|
||||
>
|
||||
<MainLayout>
|
||||
<div
|
||||
className="p-5 flex gap-3 flex-col"
|
||||
style={{
|
||||
backgroundImage: `linear-gradient(${activeCollection?.color}20 0%, ${
|
||||
user?.theme === "dark" ? "#262626" : "#f3f4f6"
|
||||
} 13rem, ${user?.theme === "dark" ? "#171717" : "#ffffff"} 100%)`,
|
||||
}}
|
||||
>
|
||||
{activeCollection && (
|
||||
<div className="flex gap-3 items-start justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
{activeCollection.icon ? (
|
||||
<Icon
|
||||
icon={activeCollection.icon}
|
||||
size={45}
|
||||
weight={
|
||||
(activeCollection.iconWeight || "regular") as IconWeight
|
||||
<div
|
||||
className="p-5 flex gap-3 flex-col"
|
||||
style={{
|
||||
backgroundImage: `linear-gradient(${activeCollection?.color}20 0%, ${
|
||||
user?.theme === "dark" ? "#262626" : "#f3f4f6"
|
||||
} 13rem, ${user?.theme === "dark" ? "#171717" : "#ffffff"} 100%)`,
|
||||
}}
|
||||
>
|
||||
{activeCollection && (
|
||||
<div className="flex gap-3 items-start justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
{activeCollection.icon ? (
|
||||
<Icon
|
||||
icon={activeCollection.icon}
|
||||
size={45}
|
||||
weight={
|
||||
(activeCollection.iconWeight || "regular") as IconWeight
|
||||
}
|
||||
color={activeCollection.color}
|
||||
/>
|
||||
) : (
|
||||
<i
|
||||
className="bi-folder-fill text-3xl"
|
||||
style={{ color: activeCollection.color }}
|
||||
/>
|
||||
)}
|
||||
|
||||
<p className="sm:text-3xl text-2xl w-full py-1 break-words hyphens-auto font-thin">
|
||||
{activeCollection?.name}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
asChild
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="mt-2 text-neutral"
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
title={t("more")}
|
||||
>
|
||||
<i className="bi-three-dots text-xl" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
<DropdownMenuContent
|
||||
sideOffset={4}
|
||||
align="end"
|
||||
className="bg-base-200 border border-neutral-content rounded-box p-1"
|
||||
>
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
for (const link of links) {
|
||||
if (link.url) window.open(link.url, "_blank");
|
||||
}
|
||||
color={activeCollection.color}
|
||||
/>
|
||||
) : (
|
||||
<i
|
||||
className="bi-folder-fill text-3xl"
|
||||
style={{ color: activeCollection.color }}
|
||||
/>
|
||||
}}
|
||||
>
|
||||
<i className="bi-box-arrow-up-right" />
|
||||
{t("open_all_links")}
|
||||
</DropdownMenuItem>
|
||||
|
||||
{permissions === true && (
|
||||
<DropdownMenuItem
|
||||
onClick={() => setEditCollectionModal(true)}
|
||||
>
|
||||
<i className="bi-pencil-square" />
|
||||
{t("edit_collection_info")}
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
|
||||
<p className="sm:text-3xl text-2xl w-full py-1 break-words hyphens-auto font-thin">
|
||||
{activeCollection?.name}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
asChild
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="mt-2 text-neutral"
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
title={t("more")}
|
||||
>
|
||||
<i className="bi-three-dots text-xl" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
<DropdownMenuContent
|
||||
sideOffset={4}
|
||||
align="end"
|
||||
className="bg-base-200 border border-neutral-content rounded-box p-1"
|
||||
>
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
for (const link of links) {
|
||||
if (link.url) window.open(link.url, "_blank");
|
||||
}
|
||||
}}
|
||||
>
|
||||
<i className="bi-box-arrow-up-right" />
|
||||
{t("open_all_links")}
|
||||
</DropdownMenuItem>
|
||||
|
||||
{permissions === true && (
|
||||
<DropdownMenuItem
|
||||
onClick={() => setEditCollectionModal(true)}
|
||||
>
|
||||
<i className="bi-pencil-square" />
|
||||
{t("edit_collection_info")}
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
|
||||
<DropdownMenuItem
|
||||
onClick={() => setEditCollectionSharingModal(true)}
|
||||
>
|
||||
<i className="bi-globe" />
|
||||
{permissions === true
|
||||
? t("share_and_collaborate")
|
||||
: t("view_team")}
|
||||
</DropdownMenuItem>
|
||||
|
||||
{permissions === true && (
|
||||
<DropdownMenuItem
|
||||
onClick={() => setNewCollectionModal(true)}
|
||||
>
|
||||
<i className="bi-folder-plus" />
|
||||
{t("create_subcollection")}
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
<DropdownMenuItem
|
||||
onClick={() => setDeleteCollectionModal(true)}
|
||||
className="text-error"
|
||||
>
|
||||
{permissions === true ? (
|
||||
<>
|
||||
<i className="bi-trash" />
|
||||
{t("delete_collection")}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<i className="bi-box-arrow-left" />
|
||||
{t("leave_collection")}
|
||||
</>
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeCollection && (
|
||||
<div className="min-w-[15rem]">
|
||||
<div className="flex gap-1 justify-center sm:justify-end items-center w-fit">
|
||||
<div
|
||||
className="flex items-center px-1 py-1 rounded-full cursor-pointer hover:bg-base-content/20 transition-colors duration-200"
|
||||
<DropdownMenuItem
|
||||
onClick={() => setEditCollectionSharingModal(true)}
|
||||
>
|
||||
{collectionOwner.id && (
|
||||
<ProfilePhoto
|
||||
src={collectionOwner.image || undefined}
|
||||
name={collectionOwner.name}
|
||||
/>
|
||||
)}
|
||||
{activeCollection.members
|
||||
.sort((a, b) => (a.userId as number) - (b.userId as number))
|
||||
.map((e, i) => {
|
||||
return (
|
||||
<ProfilePhoto
|
||||
key={i}
|
||||
src={e.user.image ? e.user.image : undefined}
|
||||
name={e.user.name}
|
||||
className="-ml-3"
|
||||
/>
|
||||
);
|
||||
})
|
||||
.slice(0, 3)}
|
||||
{activeCollection.members.length - 3 > 0 && (
|
||||
<div className={`avatar drop-shadow-md placeholder -ml-3`}>
|
||||
<div className="bg-base-100 text-neutral rounded-full w-8 h-8 ring-2 ring-neutral-content">
|
||||
<span>+{activeCollection.members.length - 3}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<i className="bi-globe" />
|
||||
{permissions === true
|
||||
? t("share_and_collaborate")
|
||||
: t("view_team")}
|
||||
</DropdownMenuItem>
|
||||
|
||||
<p className="text-neutral text-sm ml-2">
|
||||
{activeCollection.members.length > 0
|
||||
? activeCollection.members.length === 1
|
||||
? t("by_author_and_other", {
|
||||
author: collectionOwner.name,
|
||||
count: activeCollection.members.length,
|
||||
})
|
||||
: t("by_author_and_others", {
|
||||
author: collectionOwner.name,
|
||||
count: activeCollection.members.length,
|
||||
})
|
||||
: t("by_author", { author: collectionOwner.name })}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeCollection?.description && (
|
||||
<p>{activeCollection.description}</p>
|
||||
)}
|
||||
|
||||
<Separator />
|
||||
|
||||
{collections.some((e) => e.parentId === activeCollection?.id) && (
|
||||
<>
|
||||
<PageHeader
|
||||
icon="bi-folder"
|
||||
title={t("collections")}
|
||||
description={t(
|
||||
collections.filter((e) => e.parentId === activeCollection?.id)
|
||||
.length === 1
|
||||
? "showing_count_result"
|
||||
: "showing_count_results",
|
||||
{
|
||||
count: collections.filter(
|
||||
(e) => e.parentId === activeCollection?.id
|
||||
).length,
|
||||
}
|
||||
{permissions === true && (
|
||||
<DropdownMenuItem onClick={() => setNewCollectionModal(true)}>
|
||||
<i className="bi-folder-plus" />
|
||||
{t("create_subcollection")}
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
className="scale-90 w-fit"
|
||||
/>
|
||||
<div className="grid 2xl:grid-cols-4 xl:grid-cols-3 sm:grid-cols-2 grid-cols-1 gap-5">
|
||||
{collections
|
||||
.filter((e) => e.parentId === activeCollection?.id)
|
||||
.map((e) => (
|
||||
<CollectionCard key={e.id} collection={e} />
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<LinkListOptions
|
||||
t={t}
|
||||
viewMode={viewMode}
|
||||
setViewMode={setViewMode}
|
||||
sortBy={sortBy}
|
||||
setSortBy={setSortBy}
|
||||
editMode={
|
||||
permissions === true ||
|
||||
permissions?.canUpdate ||
|
||||
permissions?.canDelete
|
||||
? editMode
|
||||
: undefined
|
||||
}
|
||||
setEditMode={
|
||||
permissions === true ||
|
||||
permissions?.canUpdate ||
|
||||
permissions?.canDelete
|
||||
? setEditMode
|
||||
: undefined
|
||||
}
|
||||
links={links}
|
||||
>
|
||||
{collections.some((e) => e.parentId === activeCollection?.id) ? (
|
||||
<PageHeader
|
||||
icon={"bi-link-45deg"}
|
||||
title={t("links")}
|
||||
description={
|
||||
activeCollection?._count?.links === 1
|
||||
? t("showing_count_result", {
|
||||
count: activeCollection?._count?.links,
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
<DropdownMenuItem
|
||||
onClick={() => setDeleteCollectionModal(true)}
|
||||
className="text-error"
|
||||
>
|
||||
{permissions === true ? (
|
||||
<>
|
||||
<i className="bi-trash" />
|
||||
{t("delete_collection")}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<i className="bi-box-arrow-left" />
|
||||
{t("leave_collection")}
|
||||
</>
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeCollection && (
|
||||
<div className="min-w-[15rem]">
|
||||
<div className="flex gap-1 justify-center sm:justify-end items-center w-fit">
|
||||
<div
|
||||
className="flex items-center px-1 py-1 rounded-full cursor-pointer hover:bg-base-content/20 transition-colors duration-200"
|
||||
onClick={() => setEditCollectionSharingModal(true)}
|
||||
>
|
||||
{collectionOwner.id && (
|
||||
<ProfilePhoto
|
||||
src={collectionOwner.image || undefined}
|
||||
name={collectionOwner.name}
|
||||
/>
|
||||
)}
|
||||
{activeCollection.members
|
||||
.sort((a, b) => (a.userId as number) - (b.userId as number))
|
||||
.map((e, i) => {
|
||||
return (
|
||||
<ProfilePhoto
|
||||
key={i}
|
||||
src={e.user.image ? e.user.image : undefined}
|
||||
name={e.user.name}
|
||||
className="-ml-3"
|
||||
/>
|
||||
);
|
||||
})
|
||||
.slice(0, 3)}
|
||||
{activeCollection.members.length - 3 > 0 && (
|
||||
<div className={`avatar drop-shadow-md placeholder -ml-3`}>
|
||||
<div className="bg-base-100 text-neutral rounded-full w-8 h-8 ring-2 ring-neutral-content">
|
||||
<span>+{activeCollection.members.length - 3}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<p className="text-neutral text-sm ml-2">
|
||||
{activeCollection.members.length > 0
|
||||
? activeCollection.members.length === 1
|
||||
? t("by_author_and_other", {
|
||||
author: collectionOwner.name,
|
||||
count: activeCollection.members.length,
|
||||
})
|
||||
: t("showing_count_results", {
|
||||
count: activeCollection?._count?.links,
|
||||
: t("by_author_and_others", {
|
||||
author: collectionOwner.name,
|
||||
count: activeCollection.members.length,
|
||||
})
|
||||
: t("by_author", { author: collectionOwner.name })}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeCollection?.description && <p>{activeCollection.description}</p>}
|
||||
|
||||
<Separator />
|
||||
|
||||
{collections.some((e) => e.parentId === activeCollection?.id) && (
|
||||
<>
|
||||
<PageHeader
|
||||
icon="bi-folder"
|
||||
title={t("collections")}
|
||||
description={t(
|
||||
collections.filter((e) => e.parentId === activeCollection?.id)
|
||||
.length === 1
|
||||
? "showing_count_result"
|
||||
: "showing_count_results",
|
||||
{
|
||||
count: collections.filter(
|
||||
(e) => e.parentId === activeCollection?.id
|
||||
).length,
|
||||
}
|
||||
className="scale-90 w-fit"
|
||||
/>
|
||||
) : (
|
||||
<p>
|
||||
{activeCollection?._count?.links === 1
|
||||
)}
|
||||
className="scale-90 w-fit"
|
||||
/>
|
||||
<div className="grid 2xl:grid-cols-4 xl:grid-cols-3 sm:grid-cols-2 grid-cols-1 gap-5">
|
||||
{collections
|
||||
.filter((e) => e.parentId === activeCollection?.id)
|
||||
.map((e) => (
|
||||
<CollectionCard key={e.id} collection={e} />
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<LinkListOptions
|
||||
t={t}
|
||||
viewMode={viewMode}
|
||||
setViewMode={setViewMode}
|
||||
sortBy={sortBy}
|
||||
setSortBy={setSortBy}
|
||||
editMode={
|
||||
permissions === true ||
|
||||
permissions?.canUpdate ||
|
||||
permissions?.canDelete
|
||||
? editMode
|
||||
: undefined
|
||||
}
|
||||
setEditMode={
|
||||
permissions === true ||
|
||||
permissions?.canUpdate ||
|
||||
permissions?.canDelete
|
||||
? setEditMode
|
||||
: undefined
|
||||
}
|
||||
links={links}
|
||||
>
|
||||
{collections.some((e) => e.parentId === activeCollection?.id) ? (
|
||||
<PageHeader
|
||||
icon={"bi-link-45deg"}
|
||||
title={t("links")}
|
||||
description={
|
||||
activeCollection?._count?.links === 1
|
||||
? t("showing_count_result", {
|
||||
count: activeCollection?._count?.links,
|
||||
})
|
||||
: t("showing_count_results", {
|
||||
count: activeCollection?._count?.links,
|
||||
})}
|
||||
</p>
|
||||
)}
|
||||
</LinkListOptions>
|
||||
})
|
||||
}
|
||||
className="scale-90 w-fit"
|
||||
/>
|
||||
) : (
|
||||
<p>
|
||||
{activeCollection?._count?.links === 1
|
||||
? t("showing_count_result", {
|
||||
count: activeCollection?._count?.links,
|
||||
})
|
||||
: t("showing_count_results", {
|
||||
count: activeCollection?._count?.links,
|
||||
})}
|
||||
</p>
|
||||
)}
|
||||
</LinkListOptions>
|
||||
|
||||
<Links
|
||||
editMode={editMode}
|
||||
links={links}
|
||||
layout={viewMode}
|
||||
useData={data}
|
||||
/>
|
||||
{!data.isLoading && links && !links[0] && <NoLinksFound />}
|
||||
</div>
|
||||
{activeCollection && (
|
||||
<>
|
||||
{editCollectionModal && (
|
||||
<EditCollectionModal
|
||||
onClose={() => setEditCollectionModal(false)}
|
||||
activeCollection={activeCollection}
|
||||
/>
|
||||
)}
|
||||
{editCollectionSharingModal && (
|
||||
<EditCollectionSharingModal
|
||||
onClose={() => setEditCollectionSharingModal(false)}
|
||||
activeCollection={activeCollection}
|
||||
/>
|
||||
)}
|
||||
{newCollectionModal && (
|
||||
<NewCollectionModal
|
||||
onClose={() => setNewCollectionModal(false)}
|
||||
parent={activeCollection}
|
||||
/>
|
||||
)}
|
||||
{deleteCollectionModal && (
|
||||
<DeleteCollectionModal
|
||||
onClose={() => setDeleteCollectionModal(false)}
|
||||
activeCollection={activeCollection}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</MainLayout>
|
||||
<Links
|
||||
editMode={editMode}
|
||||
links={links}
|
||||
layout={viewMode}
|
||||
useData={data}
|
||||
/>
|
||||
{!data.isLoading && links && !links[0] && <NoLinksFound />}
|
||||
</div>
|
||||
{activeCollection && (
|
||||
<>
|
||||
{editCollectionModal && (
|
||||
<EditCollectionModal
|
||||
onClose={() => setEditCollectionModal(false)}
|
||||
activeCollection={activeCollection}
|
||||
/>
|
||||
)}
|
||||
{editCollectionSharingModal && (
|
||||
<EditCollectionSharingModal
|
||||
onClose={() => setEditCollectionSharingModal(false)}
|
||||
activeCollection={activeCollection}
|
||||
/>
|
||||
)}
|
||||
{newCollectionModal && (
|
||||
<NewCollectionModal
|
||||
onClose={() => setNewCollectionModal(false)}
|
||||
parent={activeCollection}
|
||||
/>
|
||||
)}
|
||||
{deleteCollectionModal && (
|
||||
<DeleteCollectionModal
|
||||
onClose={() => setDeleteCollectionModal(false)}
|
||||
activeCollection={activeCollection}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</DragNDrop>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
Page.getLayout = function getLayout(page: ReactElement) {
|
||||
return <MainLayout>{page}</MainLayout>;
|
||||
};
|
||||
|
||||
export default Page;
|
||||
|
||||
export { getServerSideProps };
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import CollectionCard from "@/components/CollectionCard";
|
||||
import { useMemo, useState } from "react";
|
||||
import { ReactElement, useMemo, useState } from "react";
|
||||
import MainLayout from "@/layouts/MainLayout";
|
||||
import { useSession } from "next-auth/react";
|
||||
import SortDropdown from "@/components/SortDropdown";
|
||||
@@ -16,8 +16,9 @@ import {
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { NextPageWithLayout } from "../_app";
|
||||
|
||||
export default function Collections() {
|
||||
const Page: NextPageWithLayout = () => {
|
||||
const { t } = useTranslation();
|
||||
const { data: collections = [], isLoading } = useCollections();
|
||||
const [sortBy, setSortBy] = useState<Sort>(Sort.DateNewestFirst);
|
||||
@@ -53,102 +54,106 @@ export default function Collections() {
|
||||
const [newCollectionModal, setNewCollectionModal] = useState(false);
|
||||
|
||||
return (
|
||||
<MainLayout>
|
||||
<div className="p-5 flex flex-col gap-5 w-full h-full">
|
||||
<div className="flex justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<PageHeader
|
||||
icon={"bi-folder"}
|
||||
title={t("collections")}
|
||||
description={t("collections_you_own")}
|
||||
/>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => setNewCollectionModal(true)}
|
||||
>
|
||||
<i className="bi-plus-lg text-xl text-neutral"></i>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">
|
||||
<p>{t("new_collection")}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
<div className="flex gap-3 justify-end">
|
||||
<div className="relative mt-2">
|
||||
<SortDropdown sortBy={sortBy} setSort={setSortBy} t={t} />
|
||||
</div>
|
||||
<div className="p-5 flex flex-col gap-5 w-full h-full">
|
||||
<div className="flex justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<PageHeader
|
||||
icon={"bi-folder"}
|
||||
title={t("collections")}
|
||||
description={t("collections_you_own")}
|
||||
/>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => setNewCollectionModal(true)}
|
||||
>
|
||||
<i className="bi-plus-lg text-xl text-neutral"></i>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">
|
||||
<p>{t("new_collection")}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
<div className="flex gap-3 justify-end">
|
||||
<div className="relative mt-2">
|
||||
<SortDropdown sortBy={sortBy} setSort={setSortBy} t={t} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!isLoading && collections && !collections[0] ? (
|
||||
<div
|
||||
style={{ flex: "1 1 auto" }}
|
||||
className="flex flex-col gap-2 justify-center h-full w-full mx-auto p-10"
|
||||
{!isLoading && collections && !collections[0] ? (
|
||||
<div
|
||||
style={{ flex: "1 1 auto" }}
|
||||
className="flex flex-col gap-2 justify-center h-full w-full mx-auto p-10"
|
||||
>
|
||||
<p className="text-center text-xl">
|
||||
{t("create_your_first_collection")}
|
||||
</p>
|
||||
<p className="text-center mx-auto max-w-96 w-fit text-neutral text-sm">
|
||||
{t("create_your_first_collection_desc")}
|
||||
</p>
|
||||
<Button
|
||||
className="mx-auto mt-5"
|
||||
variant={"accent"}
|
||||
onClick={() => setNewCollectionModal(true)}
|
||||
>
|
||||
<p className="text-center text-xl">
|
||||
{t("create_your_first_collection")}
|
||||
</p>
|
||||
<p className="text-center mx-auto max-w-96 w-fit text-neutral text-sm">
|
||||
{t("create_your_first_collection_desc")}
|
||||
</p>
|
||||
<Button
|
||||
className="mx-auto mt-5"
|
||||
variant={"accent"}
|
||||
onClick={() => setNewCollectionModal(true)}
|
||||
>
|
||||
<i className="bi-plus-lg text-xl mr-2" />
|
||||
<i className="bi-plus-lg text-xl mr-2" />
|
||||
{t("new_collection")}
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid 2xl:grid-cols-4 xl:grid-cols-3 sm:grid-cols-2 grid-cols-1 gap-5">
|
||||
{sortedCollections
|
||||
.filter((e) => e.ownerId === data?.user.id && e.parentId === null)
|
||||
.map((e) => (
|
||||
<CollectionCard key={e.id} collection={e} />
|
||||
))}
|
||||
|
||||
<div
|
||||
className="card card-compact shadow-md hover:shadow-none duration-200 border border-neutral-content p-5 bg-base-200 self-stretch min-h-[12rem] rounded-xl cursor-pointer flex flex-col gap-4 justify-center items-center group"
|
||||
onClick={() => setNewCollectionModal(true)}
|
||||
>
|
||||
<p className="group-hover:opacity-0 duration-100">
|
||||
{t("new_collection")}
|
||||
</Button>
|
||||
</p>
|
||||
<i className="bi-plus-lg text-5xl group-hover:text-7xl group-hover:-mt-10 text-primary drop-shadow duration-100"></i>
|
||||
</div>
|
||||
) : (
|
||||
</div>
|
||||
)}
|
||||
|
||||
{sortedCollections.filter((e) => e.ownerId !== data?.user.id)[0] && (
|
||||
<>
|
||||
<PageHeader
|
||||
icon={"bi-folder"}
|
||||
title={t("other_collections")}
|
||||
description={t("other_collections_desc")}
|
||||
/>
|
||||
|
||||
<div className="grid 2xl:grid-cols-4 xl:grid-cols-3 sm:grid-cols-2 grid-cols-1 gap-5">
|
||||
{sortedCollections
|
||||
.filter((e) => e.ownerId === data?.user.id && e.parentId === null)
|
||||
.filter((e) => e.ownerId !== data?.user.id)
|
||||
.map((e) => (
|
||||
<CollectionCard key={e.id} collection={e} />
|
||||
))}
|
||||
|
||||
<div
|
||||
className="card card-compact shadow-md hover:shadow-none duration-200 border border-neutral-content p-5 bg-base-200 self-stretch min-h-[12rem] rounded-xl cursor-pointer flex flex-col gap-4 justify-center items-center group"
|
||||
onClick={() => setNewCollectionModal(true)}
|
||||
>
|
||||
<p className="group-hover:opacity-0 duration-100">
|
||||
{t("new_collection")}
|
||||
</p>
|
||||
<i className="bi-plus-lg text-5xl group-hover:text-7xl group-hover:-mt-10 text-primary drop-shadow duration-100"></i>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{sortedCollections.filter((e) => e.ownerId !== data?.user.id)[0] && (
|
||||
<>
|
||||
<PageHeader
|
||||
icon={"bi-folder"}
|
||||
title={t("other_collections")}
|
||||
description={t("other_collections_desc")}
|
||||
/>
|
||||
|
||||
<div className="grid 2xl:grid-cols-4 xl:grid-cols-3 sm:grid-cols-2 grid-cols-1 gap-5">
|
||||
{sortedCollections
|
||||
.filter((e) => e.ownerId !== data?.user.id)
|
||||
.map((e) => (
|
||||
<CollectionCard key={e.id} collection={e} />
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{newCollectionModal && (
|
||||
<NewCollectionModal onClose={() => setNewCollectionModal(false)} />
|
||||
)}
|
||||
</MainLayout>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
Page.getLayout = function getLayout(page: ReactElement) {
|
||||
return <MainLayout>{page}</MainLayout>;
|
||||
};
|
||||
|
||||
export default Page;
|
||||
|
||||
export { getServerSideProps };
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import MainLayout from "@/layouts/MainLayout";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { ReactElement, useEffect, useMemo, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import React from "react";
|
||||
import { toast } from "react-hot-toast";
|
||||
@@ -34,8 +34,9 @@ import { useUpdateLink } from "@linkwarden/router/links";
|
||||
import usePinLink from "@/lib/client/pinLink";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import DragNDrop from "@/components/DragNDrop";
|
||||
import { NextPageWithLayout } from "./_app";
|
||||
|
||||
export default function Dashboard() {
|
||||
const Page: NextPageWithLayout = () => {
|
||||
const { t } = useTranslation();
|
||||
const { data: collections = [] } = useCollections();
|
||||
const {
|
||||
@@ -287,75 +288,77 @@ export default function Dashboard() {
|
||||
activeLink={activeLink}
|
||||
setActiveLink={setActiveLink}
|
||||
>
|
||||
<MainLayout>
|
||||
<div className="p-5 flex flex-col gap-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<i className="bi-house-fill text-primary" />
|
||||
<p className="font-thin">{t("dashboard")}</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<DashboardLayoutDropdown />
|
||||
<ViewDropdown
|
||||
viewMode={viewMode}
|
||||
setViewMode={setViewMode}
|
||||
dashboard
|
||||
/>
|
||||
</div>
|
||||
<div className="p-5 flex flex-col gap-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<i className="bi-house-fill text-primary" />
|
||||
<p className="font-thin">{t("dashboard")}</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<DashboardLayoutDropdown />
|
||||
<ViewDropdown
|
||||
viewMode={viewMode}
|
||||
setViewMode={setViewMode}
|
||||
dashboard
|
||||
/>
|
||||
</div>
|
||||
{orderedSections[0] ? (
|
||||
orderedSections?.map((section, i) => (
|
||||
<Section
|
||||
key={i}
|
||||
sectionData={section}
|
||||
t={t}
|
||||
collection={collections.find(
|
||||
(c) => c.id === section.collectionId
|
||||
)}
|
||||
collectionLinks={
|
||||
section.collectionId
|
||||
? collectionLinks[section.collectionId]
|
||||
: []
|
||||
}
|
||||
links={links}
|
||||
tags={tags}
|
||||
numberOfLinks={numberOfLinks}
|
||||
collectionsLength={collections.length}
|
||||
numberOfPinnedLinks={numberOfPinnedLinks}
|
||||
dashboardData={dashboardData}
|
||||
setNewLinkModal={setNewLinkModal}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<div className="h-full flex flex-col gap-4">
|
||||
<div className="xl:flex flex flex-col sm:grid grid-cols-2 gap-4 xl:flex-row xl:justify-evenly xl:w-full">
|
||||
<div className="skeleton h-20 w-full"></div>
|
||||
<div className="skeleton h-20 w-full"></div>
|
||||
<div className="skeleton h-20 w-full"></div>
|
||||
<div className="skeleton h-20 w-full"></div>
|
||||
</div>
|
||||
<div className="skeleton h-full"></div>
|
||||
<div className="skeleton h-full"></div>
|
||||
<div className="skeleton h-full"></div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{orderedSections[0] ? (
|
||||
orderedSections?.map((section, i) => (
|
||||
<Section
|
||||
key={i}
|
||||
sectionData={section}
|
||||
t={t}
|
||||
collection={collections.find(
|
||||
(c) => c.id === section.collectionId
|
||||
)}
|
||||
collectionLinks={
|
||||
section.collectionId
|
||||
? collectionLinks[section.collectionId]
|
||||
: []
|
||||
}
|
||||
links={links}
|
||||
tags={tags}
|
||||
numberOfLinks={numberOfLinks}
|
||||
collectionsLength={collections.length}
|
||||
numberOfPinnedLinks={numberOfPinnedLinks}
|
||||
dashboardData={dashboardData}
|
||||
setNewLinkModal={setNewLinkModal}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<div className="h-full flex flex-col gap-4">
|
||||
<div className="xl:flex flex flex-col sm:grid grid-cols-2 gap-4 xl:flex-row xl:justify-evenly xl:w-full">
|
||||
<div className="skeleton h-20 w-full"></div>
|
||||
<div className="skeleton h-20 w-full"></div>
|
||||
<div className="skeleton h-20 w-full"></div>
|
||||
<div className="skeleton h-20 w-full"></div>
|
||||
</div>
|
||||
<div className="skeleton h-full"></div>
|
||||
<div className="skeleton h-full"></div>
|
||||
<div className="skeleton h-full"></div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{showSurveyModal && (
|
||||
<SurveyModal
|
||||
submit={submitSurvey}
|
||||
onClose={() => {
|
||||
setShowsSurveyModal(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{newLinkModal && (
|
||||
<NewLinkModal onClose={() => setNewLinkModal(false)} />
|
||||
)}
|
||||
</MainLayout>
|
||||
{showSurveyModal && (
|
||||
<SurveyModal
|
||||
submit={submitSurvey}
|
||||
onClose={() => {
|
||||
setShowsSurveyModal(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{newLinkModal && <NewLinkModal onClose={() => setNewLinkModal(false)} />}
|
||||
</DragNDrop>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
Page.getLayout = function getLayout(page: ReactElement) {
|
||||
return <MainLayout>{page}</MainLayout>;
|
||||
};
|
||||
|
||||
export default Page;
|
||||
|
||||
export { getServerSideProps };
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import NoLinksFound from "@/components/NoLinksFound";
|
||||
import { useLinks, useUpdateLink } from "@linkwarden/router/links";
|
||||
import { useLinks } from "@linkwarden/router/links";
|
||||
import MainLayout from "@/layouts/MainLayout";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import React, { ReactElement, useEffect, useState } from "react";
|
||||
import {
|
||||
LinkIncludingShortenedCollectionAndTags,
|
||||
Sort,
|
||||
@@ -14,8 +14,9 @@ import { useTranslation } from "next-i18next";
|
||||
import Links from "@/components/LinkViews/Links";
|
||||
import clsx from "clsx";
|
||||
import DragNDrop from "@/components/DragNDrop";
|
||||
import { NextPageWithLayout } from "../_app";
|
||||
|
||||
export default function Index() {
|
||||
const Page: NextPageWithLayout = () => {
|
||||
const [activeLink, setActiveLink] =
|
||||
useState<LinkIncludingShortenedCollectionAndTags | null>(null);
|
||||
const { t } = useTranslation();
|
||||
@@ -45,44 +46,46 @@ export default function Index() {
|
||||
activeLink={activeLink}
|
||||
setActiveLink={setActiveLink}
|
||||
>
|
||||
<MainLayout>
|
||||
<div className="p-5 flex flex-col gap-5 w-full h-full">
|
||||
<LinkListOptions
|
||||
t={t}
|
||||
viewMode={viewMode}
|
||||
setViewMode={setViewMode}
|
||||
sortBy={sortBy}
|
||||
setSortBy={setSortBy}
|
||||
editMode={editMode}
|
||||
setEditMode={setEditMode}
|
||||
links={links}
|
||||
>
|
||||
<div className={clsx("flex items-center gap-3")}>
|
||||
<i
|
||||
className={`bi-link-45deg text-primary text-3xl drop-shadow`}
|
||||
></i>
|
||||
<div>
|
||||
<p className="text-2xl capitalize font-thin">
|
||||
{t("all_links")}
|
||||
</p>
|
||||
<p className="text-xs sm:text-sm">{t("all_links_desc")}</p>
|
||||
</div>
|
||||
<div className="p-5 flex flex-col gap-5 w-full h-full">
|
||||
<LinkListOptions
|
||||
t={t}
|
||||
viewMode={viewMode}
|
||||
setViewMode={setViewMode}
|
||||
sortBy={sortBy}
|
||||
setSortBy={setSortBy}
|
||||
editMode={editMode}
|
||||
setEditMode={setEditMode}
|
||||
links={links}
|
||||
>
|
||||
<div className={clsx("flex items-center gap-3")}>
|
||||
<i
|
||||
className={`bi-link-45deg text-primary text-3xl drop-shadow`}
|
||||
></i>
|
||||
<div>
|
||||
<p className="text-2xl capitalize font-thin">{t("all_links")}</p>
|
||||
<p className="text-xs sm:text-sm">{t("all_links_desc")}</p>
|
||||
</div>
|
||||
</LinkListOptions>
|
||||
</div>
|
||||
</LinkListOptions>
|
||||
|
||||
{!data.isLoading && links && !links[0] && (
|
||||
<NoLinksFound text={t("you_have_not_added_any_links")} />
|
||||
)}
|
||||
<Links
|
||||
editMode={editMode}
|
||||
links={links}
|
||||
layout={viewMode}
|
||||
useData={data}
|
||||
/>
|
||||
</div>
|
||||
</MainLayout>
|
||||
{!data.isLoading && links && !links[0] && (
|
||||
<NoLinksFound text={t("you_have_not_added_any_links")} />
|
||||
)}
|
||||
<Links
|
||||
editMode={editMode}
|
||||
links={links}
|
||||
layout={viewMode}
|
||||
useData={data}
|
||||
/>
|
||||
</div>
|
||||
</DragNDrop>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
Page.getLayout = function getLayout(page: ReactElement) {
|
||||
return <MainLayout>{page}</MainLayout>;
|
||||
};
|
||||
|
||||
export default Page;
|
||||
|
||||
export { getServerSideProps };
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import MainLayout from "@/layouts/MainLayout";
|
||||
import React, { useState } from "react";
|
||||
import React, { ReactElement, useState } from "react";
|
||||
import PageHeader from "@/components/PageHeader";
|
||||
import {
|
||||
LinkIncludingShortenedCollectionAndTags,
|
||||
@@ -12,8 +12,9 @@ import LinkListOptions from "@/components/LinkListOptions";
|
||||
import { useLinks } from "@linkwarden/router/links";
|
||||
import Links from "@/components/LinkViews/Links";
|
||||
import DragNDrop from "@/components/DragNDrop";
|
||||
import { NextPageWithLayout } from "../_app";
|
||||
|
||||
export default function PinnedLinks() {
|
||||
const Page: NextPageWithLayout = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [viewMode, setViewMode] = useState<ViewMode>(
|
||||
@@ -38,56 +39,60 @@ export default function PinnedLinks() {
|
||||
activeLink={activeLink}
|
||||
setActiveLink={setActiveLink}
|
||||
>
|
||||
<MainLayout>
|
||||
<div className="p-5 flex flex-col gap-5 w-full h-full">
|
||||
<LinkListOptions
|
||||
t={t}
|
||||
viewMode={viewMode}
|
||||
setViewMode={setViewMode}
|
||||
sortBy={sortBy}
|
||||
setSortBy={setSortBy}
|
||||
editMode={editMode}
|
||||
setEditMode={setEditMode}
|
||||
links={links}
|
||||
>
|
||||
<PageHeader
|
||||
icon={"bi-pin-angle"}
|
||||
title={t("pinned")}
|
||||
description={t("pinned_links_desc")}
|
||||
/>
|
||||
</LinkListOptions>
|
||||
|
||||
{!data.isLoading && links && !links[0] && (
|
||||
<div
|
||||
style={{ flex: "1 1 auto" }}
|
||||
className="flex flex-col gap-2 justify-center h-full w-full mx-auto p-10"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="w-1/4 min-w-[7rem] max-w-[15rem] h-auto mx-auto mb-5 text-primary drop-shadow"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 16 16"
|
||||
>
|
||||
<path d="M4.146.146A.5.5 0 0 1 4.5 0h7a.5.5 0 0 1 .5.5c0 .68-.342 1.174-.646 1.479-.126.125-.25.224-.354.298v4.431l.078.048c.203.127.476.314.751.555C12.36 7.775 13 8.527 13 9.5a.5.5 0 0 1-.5.5h-4v4.5c0 .276-.224 1.5-.5 1.5s-.5-1.224-.5-1.5V10h-4a.5.5 0 0 1-.5-.5c0-.973.64-1.725 1.17-2.189A6 6 0 0 1 5 6.708V2.277a3 3 0 0 1-.354-.298C4.342 1.674 4 1.179 4 .5a.5.5 0 0 1 .146-.354m1.58 1.408-.002-.001zm-.002-.001.002.001A.5.5 0 0 1 6 2v5a.5.5 0 0 1-.276.447h-.002l-.012.007-.054.03a5 5 0 0 0-.827.58c-.318.278-.585.596-.725.936h7.792c-.14-.34-.407-.658-.725-.936a5 5 0 0 0-.881-.61l-.012-.006h-.002A.5.5 0 0 1 10 7V2a.5.5 0 0 1 .295-.458 1.8 1.8 0 0 0 .351-.271c.08-.08.155-.17.214-.271H5.14q.091.15.214.271a1.8 1.8 0 0 0 .37.282" />
|
||||
</svg>
|
||||
<p className="text-center text-xl">
|
||||
{t("pin_favorite_links_here")}
|
||||
</p>
|
||||
<p className="text-center mx-auto max-w-96 w-fit text-neutral text-sm">
|
||||
{t("pin_favorite_links_here_desc")}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<Links
|
||||
editMode={editMode}
|
||||
links={links}
|
||||
layout={viewMode}
|
||||
useData={data}
|
||||
<div className="p-5 flex flex-col gap-5 w-full h-full">
|
||||
<LinkListOptions
|
||||
t={t}
|
||||
viewMode={viewMode}
|
||||
setViewMode={setViewMode}
|
||||
sortBy={sortBy}
|
||||
setSortBy={setSortBy}
|
||||
editMode={editMode}
|
||||
setEditMode={setEditMode}
|
||||
links={links}
|
||||
>
|
||||
<PageHeader
|
||||
icon={"bi-pin-angle"}
|
||||
title={t("pinned")}
|
||||
description={t("pinned_links_desc")}
|
||||
/>
|
||||
</div>
|
||||
</MainLayout>
|
||||
</LinkListOptions>
|
||||
|
||||
{!data.isLoading && links && !links[0] && (
|
||||
<div
|
||||
style={{ flex: "1 1 auto" }}
|
||||
className="flex flex-col gap-2 justify-center h-full w-full mx-auto p-10"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="w-1/4 min-w-[7rem] max-w-[15rem] h-auto mx-auto mb-5 text-primary drop-shadow"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 16 16"
|
||||
>
|
||||
<path d="M4.146.146A.5.5 0 0 1 4.5 0h7a.5.5 0 0 1 .5.5c0 .68-.342 1.174-.646 1.479-.126.125-.25.224-.354.298v4.431l.078.048c.203.127.476.314.751.555C12.36 7.775 13 8.527 13 9.5a.5.5 0 0 1-.5.5h-4v4.5c0 .276-.224 1.5-.5 1.5s-.5-1.224-.5-1.5V10h-4a.5.5 0 0 1-.5-.5c0-.973.64-1.725 1.17-2.189A6 6 0 0 1 5 6.708V2.277a3 3 0 0 1-.354-.298C4.342 1.674 4 1.179 4 .5a.5.5 0 0 1 .146-.354m1.58 1.408-.002-.001zm-.002-.001.002.001A.5.5 0 0 1 6 2v5a.5.5 0 0 1-.276.447h-.002l-.012.007-.054.03a5 5 0 0 0-.827.58c-.318.278-.585.596-.725.936h7.792c-.14-.34-.407-.658-.725-.936a5 5 0 0 0-.881-.61l-.012-.006h-.002A.5.5 0 0 1 10 7V2a.5.5 0 0 1 .295-.458 1.8 1.8 0 0 0 .351-.271c.08-.08.155-.17.214-.271H5.14q.091.15.214.271a1.8 1.8 0 0 0 .37.282" />
|
||||
</svg>
|
||||
<p className="text-center text-xl">
|
||||
{t("pin_favorite_links_here")}
|
||||
</p>
|
||||
<p className="text-center mx-auto max-w-96 w-fit text-neutral text-sm">
|
||||
{t("pin_favorite_links_here_desc")}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<Links
|
||||
editMode={editMode}
|
||||
links={links}
|
||||
layout={viewMode}
|
||||
useData={data}
|
||||
/>
|
||||
</div>
|
||||
</DragNDrop>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
Page.getLayout = function getLayout(page: ReactElement) {
|
||||
return <MainLayout>{page}</MainLayout>;
|
||||
};
|
||||
|
||||
export default Page;
|
||||
|
||||
export { getServerSideProps };
|
||||
|
||||
@@ -6,15 +6,16 @@ import {
|
||||
ViewMode,
|
||||
} from "@linkwarden/types";
|
||||
import { useRouter } from "next/router";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import React, { ReactElement, useEffect, useState } from "react";
|
||||
import PageHeader from "@/components/PageHeader";
|
||||
import LinkListOptions from "@/components/LinkListOptions";
|
||||
import getServerSideProps from "@/lib/client/getServerSideProps";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import Links from "@/components/LinkViews/Links";
|
||||
import DragNDrop from "@/components/DragNDrop";
|
||||
import { NextPageWithLayout } from "./_app";
|
||||
|
||||
export default function Search() {
|
||||
const Page: NextPageWithLayout = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const router = useRouter();
|
||||
@@ -46,32 +47,36 @@ export default function Search() {
|
||||
activeLink={activeLink}
|
||||
setActiveLink={setActiveLink}
|
||||
>
|
||||
<MainLayout>
|
||||
<div className="p-5 flex flex-col gap-5 w-full h-full">
|
||||
<LinkListOptions
|
||||
t={t}
|
||||
viewMode={viewMode}
|
||||
setViewMode={setViewMode}
|
||||
sortBy={sortBy}
|
||||
setSortBy={setSortBy}
|
||||
editMode={editMode}
|
||||
setEditMode={setEditMode}
|
||||
links={links}
|
||||
>
|
||||
<PageHeader icon={"bi-search"} title={t("search_results")} />
|
||||
</LinkListOptions>
|
||||
<div className="p-5 flex flex-col gap-5 w-full h-full">
|
||||
<LinkListOptions
|
||||
t={t}
|
||||
viewMode={viewMode}
|
||||
setViewMode={setViewMode}
|
||||
sortBy={sortBy}
|
||||
setSortBy={setSortBy}
|
||||
editMode={editMode}
|
||||
setEditMode={setEditMode}
|
||||
links={links}
|
||||
>
|
||||
<PageHeader icon={"bi-search"} title={t("search_results")} />
|
||||
</LinkListOptions>
|
||||
|
||||
{!data.isLoading && links && !links[0] && <p>{t("nothing_found")}</p>}
|
||||
<Links
|
||||
editMode={editMode}
|
||||
links={links}
|
||||
layout={viewMode}
|
||||
useData={data}
|
||||
/>
|
||||
</div>
|
||||
</MainLayout>
|
||||
{!data.isLoading && links && !links[0] && <p>{t("nothing_found")}</p>}
|
||||
<Links
|
||||
editMode={editMode}
|
||||
links={links}
|
||||
layout={viewMode}
|
||||
useData={data}
|
||||
/>
|
||||
</div>
|
||||
</DragNDrop>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
Page.getLayout = function getLayout(page: ReactElement) {
|
||||
return <MainLayout>{page}</MainLayout>;
|
||||
};
|
||||
|
||||
export default Page;
|
||||
|
||||
export { getServerSideProps };
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useRouter } from "next/router";
|
||||
import { FormEvent, useEffect, useState } from "react";
|
||||
import { FormEvent, ReactElement, useEffect, useState } from "react";
|
||||
import MainLayout from "@/layouts/MainLayout";
|
||||
import {
|
||||
LinkIncludingShortenedCollectionAndTags,
|
||||
@@ -25,8 +25,9 @@ import {
|
||||
DropdownMenuSeparator,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import DragNDrop from "@/components/DragNDrop";
|
||||
import { NextPageWithLayout } from "../_app";
|
||||
|
||||
export default function Index() {
|
||||
const Page: NextPageWithLayout = () => {
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
|
||||
@@ -150,117 +151,113 @@ export default function Index() {
|
||||
activeLink={activeLink}
|
||||
setActiveLink={setActiveLink}
|
||||
>
|
||||
<MainLayout>
|
||||
<div className="p-5 flex flex-col gap-5 w-full h-full">
|
||||
<LinkListOptions
|
||||
t={t}
|
||||
viewMode={viewMode}
|
||||
setViewMode={setViewMode}
|
||||
sortBy={sortBy}
|
||||
setSortBy={setSortBy}
|
||||
editMode={editMode}
|
||||
setEditMode={setEditMode}
|
||||
links={links}
|
||||
>
|
||||
<div className="flex gap-3 items-center">
|
||||
<div className="flex gap-2 items-center font-thin">
|
||||
<i className="bi-hash text-primary text-3xl" />
|
||||
<div className="p-5 flex flex-col gap-5 w-full h-full">
|
||||
<LinkListOptions
|
||||
t={t}
|
||||
viewMode={viewMode}
|
||||
setViewMode={setViewMode}
|
||||
sortBy={sortBy}
|
||||
setSortBy={setSortBy}
|
||||
editMode={editMode}
|
||||
setEditMode={setEditMode}
|
||||
links={links}
|
||||
>
|
||||
<div className="flex gap-3 items-center">
|
||||
<div className="flex gap-2 items-center font-thin">
|
||||
<i className="bi-hash text-primary text-3xl" />
|
||||
|
||||
{renameTag ? (
|
||||
<form onSubmit={submit} className="flex items-center gap-2">
|
||||
<input
|
||||
type="text"
|
||||
autoFocus
|
||||
className="sm:text-3xl text-xl bg-transparent h-10 w-3/4 outline-none border-b border-b-neutral-content"
|
||||
value={newTagName}
|
||||
onChange={(e) => setNewTagName(e.target.value)}
|
||||
/>
|
||||
<Button variant="ghost" size="icon" onClick={submit}>
|
||||
<i className="bi-check2 text-neutral text-xl" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={cancelUpdateTag}
|
||||
>
|
||||
<i className="bi-x text-neutral text-xl" />
|
||||
</Button>
|
||||
</form>
|
||||
) : (
|
||||
<>
|
||||
<p className="sm:text-3xl text-xl">{activeTag?.name}</p>
|
||||
<div className="relative">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon" title={t("more")}>
|
||||
<i className="bi-three-dots text-xl text-neutral" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
{renameTag ? (
|
||||
<form onSubmit={submit} className="flex items-center gap-2">
|
||||
<input
|
||||
type="text"
|
||||
autoFocus
|
||||
className="sm:text-3xl text-xl bg-transparent h-10 w-3/4 outline-none border-b border-b-neutral-content"
|
||||
value={newTagName}
|
||||
onChange={(e) => setNewTagName(e.target.value)}
|
||||
/>
|
||||
<Button variant="ghost" size="icon" onClick={submit}>
|
||||
<i className="bi-check2 text-neutral text-xl" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" onClick={cancelUpdateTag}>
|
||||
<i className="bi-x text-neutral text-xl" />
|
||||
</Button>
|
||||
</form>
|
||||
) : (
|
||||
<>
|
||||
<p className="sm:text-3xl text-xl">{activeTag?.name}</p>
|
||||
<div className="relative">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon" title={t("more")}>
|
||||
<i className="bi-three-dots text-xl text-neutral" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
<DropdownMenuContent
|
||||
sideOffset={4}
|
||||
align={
|
||||
activeTag?.name.length && activeTag?.name.length > 8
|
||||
? "end"
|
||||
: "start"
|
||||
}
|
||||
className="bg-base-200 border border-neutral-content rounded-box p-1"
|
||||
<DropdownMenuContent
|
||||
sideOffset={4}
|
||||
align={
|
||||
activeTag?.name.length && activeTag?.name.length > 8
|
||||
? "end"
|
||||
: "start"
|
||||
}
|
||||
className="bg-base-200 border border-neutral-content rounded-box p-1"
|
||||
>
|
||||
<DropdownMenuItem onClick={() => setRenameTag(true)}>
|
||||
<i className="bi-pencil-square" />
|
||||
{t("rename_tag")}
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
<DropdownMenuItem
|
||||
onClick={remove}
|
||||
className="text-error"
|
||||
>
|
||||
<DropdownMenuItem onClick={() => setRenameTag(true)}>
|
||||
<i className="bi-pencil-square" />
|
||||
{t("rename_tag")}
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
<DropdownMenuItem
|
||||
onClick={remove}
|
||||
className="text-error"
|
||||
>
|
||||
<i className="bi-trash" />
|
||||
{t("delete_tag")}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<i className="bi-trash" />
|
||||
{t("delete_tag")}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</LinkListOptions>
|
||||
</div>
|
||||
</LinkListOptions>
|
||||
|
||||
<Links
|
||||
editMode={editMode}
|
||||
links={links}
|
||||
layout={viewMode}
|
||||
useData={data}
|
||||
/>
|
||||
<Links
|
||||
editMode={editMode}
|
||||
links={links}
|
||||
layout={viewMode}
|
||||
useData={data}
|
||||
/>
|
||||
|
||||
{!data.isLoading && links && !links[0] && (
|
||||
<div
|
||||
style={{ flex: "1 1 auto" }}
|
||||
className="flex flex-col gap-2 justify-center h-full w-full mx-auto p-10"
|
||||
>
|
||||
<p className="text-center text-xl">
|
||||
{t("this_tag_has_no_links")}
|
||||
</p>
|
||||
<p className="text-center mx-auto max-w-96 w-fit text-neutral text-sm">
|
||||
{t("this_tag_has_no_links_desc")}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{bulkDeleteLinksModal && (
|
||||
<BulkDeleteLinksModal
|
||||
onClose={() => setBulkDeleteLinksModal(false)}
|
||||
/>
|
||||
{!data.isLoading && links && !links[0] && (
|
||||
<div
|
||||
style={{ flex: "1 1 auto" }}
|
||||
className="flex flex-col gap-2 justify-center h-full w-full mx-auto p-10"
|
||||
>
|
||||
<p className="text-center text-xl">{t("this_tag_has_no_links")}</p>
|
||||
<p className="text-center mx-auto max-w-96 w-fit text-neutral text-sm">
|
||||
{t("this_tag_has_no_links_desc")}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{bulkEditLinksModal && (
|
||||
<BulkEditLinksModal onClose={() => setBulkEditLinksModal(false)} />
|
||||
)}
|
||||
</MainLayout>
|
||||
</div>
|
||||
{bulkDeleteLinksModal && (
|
||||
<BulkDeleteLinksModal onClose={() => setBulkDeleteLinksModal(false)} />
|
||||
)}
|
||||
{bulkEditLinksModal && (
|
||||
<BulkEditLinksModal onClose={() => setBulkEditLinksModal(false)} />
|
||||
)}
|
||||
</DragNDrop>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
Page.getLayout = function getLayout(page: ReactElement) {
|
||||
return <MainLayout>{page}</MainLayout>;
|
||||
};
|
||||
|
||||
export default Page;
|
||||
|
||||
export { getServerSideProps };
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
DropdownMenuRadioItem,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useMemo, useState } from "react";
|
||||
import { ReactElement, useMemo, useState } from "react";
|
||||
import NewTagModal from "@/components/ModalContent/NewTagModal";
|
||||
import {
|
||||
Tooltip,
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
} from "@/components/ui/tooltip";
|
||||
import BulkDeleteTagsModal from "@/components/ModalContent/BulkDeleteTagsModal";
|
||||
import MergeTagsModal from "@/components/ModalContent/MergeTagsModal";
|
||||
import { NextPageWithLayout } from "../_app";
|
||||
|
||||
enum TagSort {
|
||||
DateNewestFirst = 0,
|
||||
@@ -32,7 +33,7 @@ enum TagSort {
|
||||
LinkCountLowHigh = 5,
|
||||
}
|
||||
|
||||
export default function Tags() {
|
||||
const Page: NextPageWithLayout = () => {
|
||||
const { t } = useTranslation();
|
||||
const { data: tags = [], isLoading } = useTags();
|
||||
|
||||
@@ -72,194 +73,192 @@ export default function Tags() {
|
||||
const [selectedTags, setSelectedTags] = useState<number[]>([]);
|
||||
|
||||
return (
|
||||
<MainLayout>
|
||||
<div className="p-5 flex flex-col gap-5 w-full h-full">
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex items-center gap-3">
|
||||
<PageHeader icon={"bi-hash"} title={t("tags")} />
|
||||
<div className="p-5 flex flex-col gap-5 w-full h-full">
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex items-center gap-3">
|
||||
<PageHeader icon={"bi-hash"} title={t("tags")} />
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => setNewTagModal(true)}
|
||||
>
|
||||
<i className="bi-plus-lg text-xl text-neutral"></i>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">
|
||||
<p>{t("new_tag")}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3 justify-end">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => {
|
||||
setEditMode(!editMode);
|
||||
setSelectedTags([]);
|
||||
}}
|
||||
className={editMode ? "bg-primary/20 hover:bg-primary/20" : ""}
|
||||
>
|
||||
<i className="bi-pencil-fill text-neutral text-xl" />
|
||||
</Button>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon">
|
||||
<i className="bi-chevron-expand text-neutral text-xl"></i>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
<DropdownMenuContent sideOffset={4} align="end">
|
||||
<DropdownMenuRadioGroup
|
||||
value={sortBy.toString()}
|
||||
onValueChange={(v) => setSortBy(Number(v) as TagSort)}
|
||||
>
|
||||
<DropdownMenuRadioItem
|
||||
value={TagSort.DateNewestFirst.toString()}
|
||||
>
|
||||
{t("date_newest_first")}
|
||||
</DropdownMenuRadioItem>
|
||||
|
||||
<DropdownMenuRadioItem
|
||||
value={TagSort.DateOldestFirst.toString()}
|
||||
>
|
||||
{t("date_oldest_first")}
|
||||
</DropdownMenuRadioItem>
|
||||
|
||||
<DropdownMenuRadioItem value={TagSort.NameAZ.toString()}>
|
||||
{t("name_az")}
|
||||
</DropdownMenuRadioItem>
|
||||
|
||||
<DropdownMenuRadioItem value={TagSort.NameZA.toString()}>
|
||||
{t("name_za")}
|
||||
</DropdownMenuRadioItem>
|
||||
|
||||
<DropdownMenuRadioItem
|
||||
value={TagSort.LinkCountHighLow.toString()}
|
||||
>
|
||||
{t("link_count_high_low")}
|
||||
</DropdownMenuRadioItem>
|
||||
|
||||
<DropdownMenuRadioItem
|
||||
value={TagSort.LinkCountLowHigh.toString()}
|
||||
>
|
||||
{t("link_count_low_high")}
|
||||
</DropdownMenuRadioItem>
|
||||
</DropdownMenuRadioGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{tags && editMode && tags.length > 0 && (
|
||||
<div className="w-full flex justify-between items-center min-h-[32px]">
|
||||
<div className="flex gap-3 ml-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="checkbox checkbox-primary"
|
||||
onChange={() => {
|
||||
if (selectedTags.length === tags.length) setSelectedTags([]);
|
||||
else setSelectedTags(tags.map((t) => t.id));
|
||||
}}
|
||||
checked={selectedTags.length === tags.length && tags.length > 0}
|
||||
/>
|
||||
{selectedTags.length > 0 ? (
|
||||
<span>
|
||||
{selectedTags.length === 1
|
||||
? t("tag_selected")
|
||||
: t("tags_selected", { count: selectedTags.length })}
|
||||
</span>
|
||||
) : (
|
||||
<span>{t("nothing_selected")}</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setMergeTagsModal(true);
|
||||
}}
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => setNewTagModal(true)}
|
||||
disabled={selectedTags.length < 2}
|
||||
>
|
||||
<i className="bi-plus-lg text-xl text-neutral"></i>
|
||||
<i className="bi-intersect" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">
|
||||
<p>{t("new_tag")}</p>
|
||||
<TooltipContent>
|
||||
<p>{t("merge_tags")}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
onClick={(e) => {
|
||||
setBulkDeleteModal(true);
|
||||
}}
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
disabled={selectedTags.length === 0}
|
||||
>
|
||||
<i className="bi-trash text-error" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p> {t("delete")}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3 justify-end">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => {
|
||||
setEditMode(!editMode);
|
||||
setSelectedTags([]);
|
||||
}}
|
||||
className={editMode ? "bg-primary/20 hover:bg-primary/20" : ""}
|
||||
>
|
||||
<i className="bi-pencil-fill text-neutral text-xl" />
|
||||
</Button>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon">
|
||||
<i className="bi-chevron-expand text-neutral text-xl"></i>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
<DropdownMenuContent sideOffset={4} align="end">
|
||||
<DropdownMenuRadioGroup
|
||||
value={sortBy.toString()}
|
||||
onValueChange={(v) => setSortBy(Number(v) as TagSort)}
|
||||
>
|
||||
<DropdownMenuRadioItem
|
||||
value={TagSort.DateNewestFirst.toString()}
|
||||
>
|
||||
{t("date_newest_first")}
|
||||
</DropdownMenuRadioItem>
|
||||
|
||||
<DropdownMenuRadioItem
|
||||
value={TagSort.DateOldestFirst.toString()}
|
||||
>
|
||||
{t("date_oldest_first")}
|
||||
</DropdownMenuRadioItem>
|
||||
|
||||
<DropdownMenuRadioItem value={TagSort.NameAZ.toString()}>
|
||||
{t("name_az")}
|
||||
</DropdownMenuRadioItem>
|
||||
|
||||
<DropdownMenuRadioItem value={TagSort.NameZA.toString()}>
|
||||
{t("name_za")}
|
||||
</DropdownMenuRadioItem>
|
||||
|
||||
<DropdownMenuRadioItem
|
||||
value={TagSort.LinkCountHighLow.toString()}
|
||||
>
|
||||
{t("link_count_high_low")}
|
||||
</DropdownMenuRadioItem>
|
||||
|
||||
<DropdownMenuRadioItem
|
||||
value={TagSort.LinkCountLowHigh.toString()}
|
||||
>
|
||||
{t("link_count_low_high")}
|
||||
</DropdownMenuRadioItem>
|
||||
</DropdownMenuRadioGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{tags && editMode && tags.length > 0 && (
|
||||
<div className="w-full flex justify-between items-center min-h-[32px]">
|
||||
<div className="flex gap-3 ml-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="checkbox checkbox-primary"
|
||||
onChange={() => {
|
||||
if (selectedTags.length === tags.length) setSelectedTags([]);
|
||||
else setSelectedTags(tags.map((t) => t.id));
|
||||
}}
|
||||
checked={selectedTags.length === tags.length && tags.length > 0}
|
||||
/>
|
||||
{selectedTags.length > 0 ? (
|
||||
<span>
|
||||
{selectedTags.length === 1
|
||||
? t("tag_selected")
|
||||
: t("tags_selected", { count: selectedTags.length })}
|
||||
</span>
|
||||
) : (
|
||||
<span>{t("nothing_selected")}</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setMergeTagsModal(true);
|
||||
}}
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
disabled={selectedTags.length < 2}
|
||||
>
|
||||
<i className="bi-intersect" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{t("merge_tags")}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
onClick={(e) => {
|
||||
setBulkDeleteModal(true);
|
||||
}}
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
disabled={selectedTags.length === 0}
|
||||
>
|
||||
<i className="bi-trash text-error" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p> {t("delete")}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid 2xl:grid-cols-6 xl:grid-cols-5 sm:grid-cols-3 grid-cols-2 gap-5">
|
||||
{sortedTags.map((tag: any) => (
|
||||
<TagCard
|
||||
key={tag.id}
|
||||
tag={tag}
|
||||
selected={selectedTags.includes(tag.id)}
|
||||
editMode={editMode}
|
||||
onSelect={(id: number) => {
|
||||
console.log(id);
|
||||
if (selectedTags.includes(id))
|
||||
setSelectedTags((prev) => prev.filter((t) => t !== id));
|
||||
else setSelectedTags((prev) => [...prev, id]);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{!isLoading && tags && !tags[0] && (
|
||||
<div
|
||||
style={{ flex: "1 1 auto" }}
|
||||
className="flex flex-col gap-2 justify-center h-full w-full mx-auto p-10"
|
||||
>
|
||||
<p className="text-center text-xl">{t("create_your_first_tag")}</p>
|
||||
<p className="text-center mx-auto max-w-96 w-fit text-neutral text-sm">
|
||||
{t("create_your_first_tag_desc")}
|
||||
</p>
|
||||
<Button
|
||||
className="mx-auto mt-5"
|
||||
variant={"accent"}
|
||||
onClick={() => setNewTagModal(true)}
|
||||
>
|
||||
<i className="bi-plus-lg text-xl mr-2" />
|
||||
{t("new_tag")}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<div className="grid 2xl:grid-cols-6 xl:grid-cols-5 sm:grid-cols-3 grid-cols-2 gap-5">
|
||||
{sortedTags.map((tag: any) => (
|
||||
<TagCard
|
||||
key={tag.id}
|
||||
tag={tag}
|
||||
selected={selectedTags.includes(tag.id)}
|
||||
editMode={editMode}
|
||||
onSelect={(id: number) => {
|
||||
console.log(id);
|
||||
if (selectedTags.includes(id))
|
||||
setSelectedTags((prev) => prev.filter((t) => t !== id));
|
||||
else setSelectedTags((prev) => [...prev, id]);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{!isLoading && tags && !tags[0] && (
|
||||
<div
|
||||
style={{ flex: "1 1 auto" }}
|
||||
className="flex flex-col gap-2 justify-center h-full w-full mx-auto p-10"
|
||||
>
|
||||
<p className="text-center text-xl">{t("create_your_first_tag")}</p>
|
||||
<p className="text-center mx-auto max-w-96 w-fit text-neutral text-sm">
|
||||
{t("create_your_first_tag_desc")}
|
||||
</p>
|
||||
<Button
|
||||
className="mx-auto mt-5"
|
||||
variant={"accent"}
|
||||
onClick={() => setNewTagModal(true)}
|
||||
>
|
||||
<i className="bi-plus-lg text-xl mr-2" />
|
||||
{t("new_tag")}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{newTagModal && <NewTagModal onClose={() => setNewTagModal(false)} />}
|
||||
{bulkDeleteModal && (
|
||||
<BulkDeleteTagsModal
|
||||
@@ -281,8 +280,14 @@ export default function Tags() {
|
||||
setSelectedTags={setSelectedTags}
|
||||
/>
|
||||
)}
|
||||
</MainLayout>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
Page.getLayout = function getLayout(page: ReactElement) {
|
||||
return <MainLayout>{page}</MainLayout>;
|
||||
};
|
||||
|
||||
export default Page;
|
||||
|
||||
export { getServerSideProps };
|
||||
|
||||
Reference in New Issue
Block a user