Refactor pages to use consistent layout handling (yes, I forgot to do that until now :P)

This commit is contained in:
daniel31x13
2025-12-19 04:59:32 -05:00
parent ff5ba2097d
commit a32934ee9d
10 changed files with 858 additions and 827 deletions

View File

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

View File

@@ -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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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