From 802d1ff70279fb479bd017a511f09043598eb757 Mon Sep 17 00:00:00 2001 From: Andrew Bone Date: Fri, 19 Jun 2026 14:30:00 +0100 Subject: [PATCH] Move to sidebar layout --- .../admin/src/components/Nav.tsx | 96 +++++ .../admin/src/components/SubNav.tsx | 352 ++++++++++++++++++ .../admin/src/index.ts | 8 +- .../admin/src/pages/App.tsx | 134 ++----- .../admin/src/pages/Root.tsx | 24 +- .../admin/src/utils/getTranslation.ts | 5 + .../admin/test/navigation.spec.ts | 8 +- .../plugin-better-auth-dashboard/package.json | 3 +- 8 files changed, 508 insertions(+), 122 deletions(-) create mode 100644 plugins/plugin-better-auth-dashboard/admin/src/components/Nav.tsx create mode 100644 plugins/plugin-better-auth-dashboard/admin/src/components/SubNav.tsx create mode 100644 plugins/plugin-better-auth-dashboard/admin/src/utils/getTranslation.ts diff --git a/plugins/plugin-better-auth-dashboard/admin/src/components/Nav.tsx b/plugins/plugin-better-auth-dashboard/admin/src/components/Nav.tsx new file mode 100644 index 0000000..f14a377 --- /dev/null +++ b/plugins/plugin-better-auth-dashboard/admin/src/components/Nav.tsx @@ -0,0 +1,96 @@ +import { Divider } from "@strapi/design-system"; +import { useIntl } from "react-intl"; +import { getTranslation } from "../utils/getTranslation"; +import { SubNav } from "./SubNav"; + +const sections = [ + { + id: "api-roles", + intlLabel: { + id: getTranslation("settings.general"), + defaultMessage: "General", + }, + links: [ + { + id: "overview", + intlLabel: { + id: getTranslation("settings.overview"), + defaultMessage: "Overview", + }, + to: "/plugins/better-auth-dashboard/overview", + testid: "nav-overview", + }, + { + id: "users", + intlLabel: { + id: getTranslation("settings.users"), + defaultMessage: "User Management", + }, + to: "/plugins/better-auth-dashboard/users", + testid: "nav-users", + }, + { + id: "organizations", + intlLabel: { + id: getTranslation("settings.organizations"), + defaultMessage: "Organization Management", + }, + to: "/plugins/better-auth-dashboard/organizations", + testid: "nav-organizations", + }, + ], + }, +]; + +type NavProps = { + isFullPage?: boolean; + orgEnabled?: boolean; +}; + +const Nav = ({ isFullPage = false, orgEnabled = false }: NavProps) => { + const { formatMessage } = useIntl(); + + return ( + + {!isFullPage && ( + <> + + + + )} + + {isFullPage && ( + <> + + + + )} + + {sections.map((section) => ( + + {section.links + .filter((link) => orgEnabled || link.id !== "organizations") + .map((link) => { + return ( + + {formatMessage(link.intlLabel)} + + ); + })} + + ))} + + + + ); +}; + +export { Nav }; diff --git a/plugins/plugin-better-auth-dashboard/admin/src/components/SubNav.tsx b/plugins/plugin-better-auth-dashboard/admin/src/components/SubNav.tsx new file mode 100644 index 0000000..df7a56d --- /dev/null +++ b/plugins/plugin-better-auth-dashboard/admin/src/components/SubNav.tsx @@ -0,0 +1,352 @@ +import { + Badge, + Box, + SubNav as DSSubNav, + Flex, + ScrollArea, + Typography, +} from "@strapi/design-system"; +import { ChevronDown } from "@strapi/icons"; +import { useEffect, useId, useRef, useState } from "react"; +import { NavLink } from "react-router-dom"; +import styled from "styled-components"; + +const HEIGHT_TOP_NAVIGATION = "6.4rem"; +const HEIGHT_TOP_NAVIGATION_MEDIUM = "5.6rem"; +const WIDTH_SIDE_NAVIGATION = "23.2rem"; + +const MainSubNav = styled(DSSubNav)` + width: 100%; + height: 100%; + overflow: hidden; + background-color: ${({ theme }) => theme.colors.neutral0}; + display: flex; + flex-direction: column; + border-right: 0; + box-shadow: none; + position: relative; + + ${({ theme }) => theme.breakpoints.medium} { + width: ${WIDTH_SIDE_NAVIGATION}; + position: sticky; + top: 0; + border-right: 1px solid ${({ theme }) => theme.colors.neutral150}; + overscroll-behavior: contain; + } +`; + +type MainProps = { + children: React.ReactNode; + isFullPage?: boolean; +}; + +const Main = ({ children, ...props }: MainProps) => ( + {children} +); + +const StyledLink = styled(NavLink)` + display: flex; + align-items: center; + justify-content: space-between; + text-decoration: none; + height: 32px; + color: ${({ theme }) => theme.colors.neutral800}; + + &.active > div { + background-color: ${({ theme }) => theme.colors.primary100}; + color: ${({ theme }) => theme.colors.primary700}; + font-weight: 500; + } + + &:hover.active > div { + background-color: ${({ theme }) => theme.colors.primary100}; + color: ${({ theme }) => theme.colors.primary700}; + font-weight: 500; + } + + &:hover > div { + background-color: ${({ theme }) => theme.colors.neutral100}; + } + + &:focus-visible { + outline-offset: -2px; + } +`; + +const Link = ( + props: Omit, "label"> & { + label: React.ReactNode; + endAction?: React.ReactNode; + handleClick?: () => void; + }, +) => { + const { to, label, endAction, handleClick, ...rest } = props; + + return ( + + + + + {label} + + {endAction} + + + + ); +}; + +const StyledHeader = styled(Flex)` + flex: 0 0 ${HEIGHT_TOP_NAVIGATION}; + height: ${HEIGHT_TOP_NAVIGATION}; + + ${({ theme }) => theme.breakpoints.medium} { + flex: 0 0 ${HEIGHT_TOP_NAVIGATION_MEDIUM}; + height: ${HEIGHT_TOP_NAVIGATION_MEDIUM}; + } +`; + +const Header = ({ label }: { label: string }) => { + return ( + + + {label} + + + ); +}; + +const Sections = ({ + children, + ...props +}: { + children: React.ReactNode[]; + [key: string]: unknown; +}) => { + return ( + + + {children.map((child, index) => { + return
  • {child}
  • ; + })} +
    +
    + ); +}; + +const Section = ({ + label, + children, + badgeLabel, +}: { + label: string; + children: React.ReactNode[]; + badgeLabel?: string; +}) => { + const listId = useId(); + + return ( + + + + + + + {label} + + + + + {badgeLabel && ( + + {badgeLabel} + + )} + + + + + {children.map((child, index) => { + return
  • {child}
  • ; + })} +
    +
    + ); +}; + +const SubSectionHeader = styled.button` + cursor: pointer; + width: 100%; + border: none; + padding: 0; + background: transparent; + display: flex; + align-items: center; + border-radius: ${({ theme }) => theme.borderRadius}; + padding-left: ${({ theme }) => theme.spaces[3]}; + padding-right: ${({ theme }) => theme.spaces[3]}; + padding-top: ${({ theme }) => theme.spaces[2]}; + padding-bottom: ${({ theme }) => theme.spaces[2]}; + + &:hover { + background-color: ${({ theme }) => theme.colors.neutral100}; + } +`; + +const SubSectionLinkWrapper = styled.li` + ${StyledLink} > div { + padding-left: 36px; + } +`; + +const SubSection = ({ + label, + children, +}: { + label: string; + children: React.ReactNode[]; +}) => { + const [isOpen, setOpenLinks] = useState(true); + const [contentHeight, setContentHeight] = useState(0); + const listId = useId(); + const contentRef = useRef(null); + + useEffect(() => { + if (!children) return; + + if (contentRef.current) { + setContentHeight(contentRef.current.scrollHeight); + } + }, [children]); + + const handleClick = () => { + setOpenLinks((prev) => !prev); + }; + + return ( + + + + + + + {label} + + + + + + {children.map((child, index) => { + return ( + {child} + ); + })} + + + ); +}; + +const PageWrapper = styled(Box)` + width: 100%; + + ${MainSubNav} { + background-color: transparent; + border-right: none; + } + + ${({ theme }) => theme.breakpoints.medium} { + overflow: hidden; + + ${MainSubNav} { + top: 0; + } + } +`; + +const Content = ({ children }: { children: React.ReactNode }) => { + return {children}; +}; + +export const SubNav = { + Main, + Content, + Header, + Link, + Sections, + Section, + SubSection, + PageWrapper, +}; diff --git a/plugins/plugin-better-auth-dashboard/admin/src/index.ts b/plugins/plugin-better-auth-dashboard/admin/src/index.ts index fd9237b..a5c5a13 100644 --- a/plugins/plugin-better-auth-dashboard/admin/src/index.ts +++ b/plugins/plugin-better-auth-dashboard/admin/src/index.ts @@ -44,10 +44,10 @@ export default { Component: async () => import("./pages/Root"), }); - // app.router.addRoute({ - // path: `/plugins/${PLUGIN_ID}`, - // Component: async () => import("./pages/Root"), - // }); + app.router.addRoute({ + path: `plugins/${PLUGIN_ID}/*`, + Component: async () => import("./pages/Root"), + }); }, bootstrap() {}, diff --git a/plugins/plugin-better-auth-dashboard/admin/src/pages/App.tsx b/plugins/plugin-better-auth-dashboard/admin/src/pages/App.tsx index 8306e92..f44af2c 100644 --- a/plugins/plugin-better-auth-dashboard/admin/src/pages/App.tsx +++ b/plugins/plugin-better-auth-dashboard/admin/src/pages/App.tsx @@ -1,51 +1,16 @@ -import { - Alert, - Box, - Flex, - Loader, - Tabs, - Typography, -} from "@strapi/design-system"; +import { Alert, Box, Flex, Loader } from "@strapi/design-system"; +import { BackButton, Layouts, Page } from "@strapi/strapi/admin"; import { useQuery } from "react-query"; -import styled from "styled-components"; +import { Outlet } from "react-router-dom"; import { client } from "../client"; -import { PluginIcon } from "../components/PluginIcon"; +import { Nav } from "../components/Nav"; import { hasPlugin, useDashConfig } from "../hooks/useDashConfig"; -import { OrganizationsPage } from "./Organizations"; -import { OverviewPage } from "./Overview"; -import { UsersPage } from "./Users"; -const Accent = styled.div` - height: 3px; - background: linear-gradient(90deg, #4945ff 0%, #7b79ff 55%, #9593ff 100%); - flex-shrink: 0; -`; - -const BrandIcon = styled.div` - width: 30px; - height: 30px; - background: black; - display: flex; - align-items: center; - justify-content: center; - color: white; - font-size: 12px; - font-weight: 900; - flex-shrink: 0; - letter-spacing: -0.06em; - user-select: none; -`; - -const PathTag = styled.code` - padding: 2px 8px; - background: #f0f0ff; - border: 1px solid #d9d8ff; - border-radius: 4px; - font-size: 11px; - color: #4945ff; - font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace; - font-weight: 500; -`; +const RESPONSIVE_DEFAULT_SPACING = { + initial: 4, + medium: 6, + large: 10, +}; export function App() { const { data: config, isLoading, isError, error } = useDashConfig(); @@ -91,75 +56,22 @@ export function App() { if (!config) return null; return ( - - +
    + }> + Authentication - - - - - - - - - - Better Auth - - - - Authentication Dashboard - - - - - {config.basePath} - - - - Overview - - - Users - - {orgEnabled && ( - - Organizations - - )} - - + - - - - - - - - {orgEnabled && ( - - - - )} - - + + +
    ); } diff --git a/plugins/plugin-better-auth-dashboard/admin/src/pages/Root.tsx b/plugins/plugin-better-auth-dashboard/admin/src/pages/Root.tsx index 14321ce..4c7eac1 100644 --- a/plugins/plugin-better-auth-dashboard/admin/src/pages/Root.tsx +++ b/plugins/plugin-better-auth-dashboard/admin/src/pages/Root.tsx @@ -1,6 +1,21 @@ import { useMemo } from "react"; import { QueryClient, QueryClientProvider } from "react-query"; +import { Navigate, Route, Routes, useOutletContext } from "react-router-dom"; +import type { DashConfig } from "../hooks/useDashConfig"; import { App } from "./App"; +import { OrganizationsPage } from "./Organizations"; +import { OverviewPage } from "./Overview"; +import { UsersPage } from "./Users"; + +function UsersRoute() { + const { config } = useOutletContext<{ config: DashConfig }>(); + return ; +} + +function OrganizationsRoute() { + const { teamsEnabled } = useOutletContext<{ teamsEnabled: boolean }>(); + return ; +} /** * Root component that provides the React Query client. @@ -24,7 +39,14 @@ export function Root() { return ( - + + }> + } /> + } /> + } /> + } /> + + ); } diff --git a/plugins/plugin-better-auth-dashboard/admin/src/utils/getTranslation.ts b/plugins/plugin-better-auth-dashboard/admin/src/utils/getTranslation.ts new file mode 100644 index 0000000..9f11aea --- /dev/null +++ b/plugins/plugin-better-auth-dashboard/admin/src/utils/getTranslation.ts @@ -0,0 +1,5 @@ +import { PLUGIN_ID } from "../pluginId"; + +const getTranslation = (id: string) => `${PLUGIN_ID}.${id}`; + +export { getTranslation }; diff --git a/plugins/plugin-better-auth-dashboard/admin/test/navigation.spec.ts b/plugins/plugin-better-auth-dashboard/admin/test/navigation.spec.ts index 8cdd438..a5e067b 100644 --- a/plugins/plugin-better-auth-dashboard/admin/test/navigation.spec.ts +++ b/plugins/plugin-better-auth-dashboard/admin/test/navigation.spec.ts @@ -12,9 +12,7 @@ test.describe("Navigation", () => { test("shows Better Auth branding header", async ({ page }) => { await expect(page.getByTestId("dashboard-root")).toBeVisible(); - // Use exact: true to avoid matching the hidden SVG Better Auth Logo - await expect(page.getByText("Better Auth", { exact: true })).toBeVisible(); - await expect(page.getByText("Authentication Dashboard")).toBeVisible(); + await expect(page.getByText("Authentication")).toBeVisible(); }); test("shows the main navigation tab list", async ({ page }) => { @@ -66,12 +64,12 @@ test.describe("Navigation", () => { test("tab content area renders for overview tab", async ({ page }) => { await expect(page.getByTestId("dashboard-root")).toBeVisible(); - await expect(page.getByTestId("tab-overview")).toBeVisible(); + await expect(page.getByTestId("overview-page")).toBeVisible(); }); test("tab content area renders for users tab", async ({ page }) => { await expect(page.getByTestId("dashboard-root")).toBeVisible(); await page.getByTestId("nav-users").click(); - await expect(page.getByTestId("tab-users")).toBeVisible(); + await expect(page.getByTestId("users-page")).toBeVisible(); }); }); diff --git a/plugins/plugin-better-auth-dashboard/package.json b/plugins/plugin-better-auth-dashboard/package.json index c119d92..09bfe54 100644 --- a/plugins/plugin-better-auth-dashboard/package.json +++ b/plugins/plugin-better-auth-dashboard/package.json @@ -74,7 +74,8 @@ "react-dom": "^18.0.0", "react-intl": "^6.0.0 || ^7.0.0", "react-query": "^3.0.0", - "styled-components": "^6.0.0" + "styled-components": "^6.0.0", + "react-router-dom": "^6.0.0" }, "strapi": { "kind": "plugin",