From 7ee88c802611996f515a1bc665b248af81c3a325 Mon Sep 17 00:00:00 2001 From: oiov Date: Thu, 30 Oct 2025 17:11:49 +0800 Subject: [PATCH] refact: mult levels dashboard sidebar --- app/(protected)/admin/system/app-configs.tsx | 2 +- .../admin/system/domains/loading.tsx | 11 + app/(protected)/admin/system/domains/page.tsx | 38 ++ app/(protected)/admin/system/page.tsx | 22 - .../admin/system/plans/loading.tsx | 11 + app/(protected)/admin/system/plans/page.tsx | 33 ++ app/(protected)/dashboard/scrape/scrapes.tsx | 16 +- .../dashboard/urls/analytics/loading.tsx | 9 + .../dashboard/urls/analytics/page.tsx | 24 ++ .../dashboard/urls/api/loading.tsx | 10 + app/(protected)/dashboard/urls/api/page.tsx | 47 +++ app/(protected)/dashboard/urls/live-logs.tsx | 10 +- .../dashboard/urls/logs/loading.tsx | 11 + app/(protected)/dashboard/urls/logs/page.tsx | 25 ++ app/(protected)/dashboard/urls/url-list.tsx | 23 -- app/manifest.json | 2 +- components/layout/dashboard-sidebar.tsx | 376 +++++++++++++----- components/shared/icons.tsx | 4 + components/ui/modal.tsx | 2 +- config/dashboard.ts | 147 ++++--- locales/en.json | 16 +- locales/zh.json | 22 +- package.json | 2 +- public/manifest.json | 2 +- public/site.webmanifest | 2 +- public/sw.js.map | 2 +- types/index.d.ts | 1 + 27 files changed, 655 insertions(+), 215 deletions(-) create mode 100644 app/(protected)/admin/system/domains/loading.tsx create mode 100644 app/(protected)/admin/system/domains/page.tsx create mode 100644 app/(protected)/admin/system/plans/loading.tsx create mode 100644 app/(protected)/admin/system/plans/page.tsx create mode 100644 app/(protected)/dashboard/urls/analytics/loading.tsx create mode 100644 app/(protected)/dashboard/urls/analytics/page.tsx create mode 100644 app/(protected)/dashboard/urls/api/loading.tsx create mode 100644 app/(protected)/dashboard/urls/api/page.tsx create mode 100644 app/(protected)/dashboard/urls/logs/loading.tsx create mode 100644 app/(protected)/dashboard/urls/logs/page.tsx diff --git a/app/(protected)/admin/system/app-configs.tsx b/app/(protected)/admin/system/app-configs.tsx index 7df27eb..ff1edd3 100644 --- a/app/(protected)/admin/system/app-configs.tsx +++ b/app/(protected)/admin/system/app-configs.tsx @@ -92,7 +92,7 @@ export default function AppConfigs({}: {}) { return ( - +
{t("App Configs")}
diff --git a/app/(protected)/admin/system/domains/loading.tsx b/app/(protected)/admin/system/domains/loading.tsx new file mode 100644 index 0000000..9a926bb --- /dev/null +++ b/app/(protected)/admin/system/domains/loading.tsx @@ -0,0 +1,11 @@ +import { Skeleton } from "@/components/ui/skeleton"; + +export default function SystemSettingsLoading() { + return ( + <> + + + + + ); +} diff --git a/app/(protected)/admin/system/domains/page.tsx b/app/(protected)/admin/system/domains/page.tsx new file mode 100644 index 0000000..98d76bf --- /dev/null +++ b/app/(protected)/admin/system/domains/page.tsx @@ -0,0 +1,38 @@ +import { redirect } from "next/navigation"; + +import { getCurrentUser } from "@/lib/session"; +import { constructMetadata } from "@/lib/utils"; + +import DomainList from "../domain-list"; + +export const metadata = constructMetadata({ + title: "System Settings", + description: "", +}); + +export default async function DashboardPage() { + const user = await getCurrentUser(); + + if (!user?.id) redirect("/login"); + + return ( + <> + +

+ Note: Once the domain is bound to the project, it will + be used as a business domain to provide services, and direct access to + the business domain will redirect to the main site you deploy. +

+ + ); +} diff --git a/app/(protected)/admin/system/page.tsx b/app/(protected)/admin/system/page.tsx index c96dd61..821ac75 100644 --- a/app/(protected)/admin/system/page.tsx +++ b/app/(protected)/admin/system/page.tsx @@ -24,28 +24,6 @@ export default async function DashboardPage() { - - ); } diff --git a/app/(protected)/admin/system/plans/loading.tsx b/app/(protected)/admin/system/plans/loading.tsx new file mode 100644 index 0000000..9a926bb --- /dev/null +++ b/app/(protected)/admin/system/plans/loading.tsx @@ -0,0 +1,11 @@ +import { Skeleton } from "@/components/ui/skeleton"; + +export default function SystemSettingsLoading() { + return ( + <> + + + + + ); +} diff --git a/app/(protected)/admin/system/plans/page.tsx b/app/(protected)/admin/system/plans/page.tsx new file mode 100644 index 0000000..683b039 --- /dev/null +++ b/app/(protected)/admin/system/plans/page.tsx @@ -0,0 +1,33 @@ +import { redirect } from "next/navigation"; + +import { getCurrentUser } from "@/lib/session"; +import { constructMetadata } from "@/lib/utils"; + +import PlanList from "../plan-list"; + +export const metadata = constructMetadata({ + title: "System Settings", + description: "", +}); + +export default async function DashboardPage() { + const user = await getCurrentUser(); + + if (!user?.id) redirect("/login"); + + return ( + <> + + + ); +} diff --git a/app/(protected)/dashboard/scrape/scrapes.tsx b/app/(protected)/dashboard/scrape/scrapes.tsx index 4b34116..e9fb237 100644 --- a/app/(protected)/dashboard/scrape/scrapes.tsx +++ b/app/(protected)/dashboard/scrape/scrapes.tsx @@ -579,21 +579,7 @@ export const CodeLight = ({ content }: { content: string }) => { {i + 1} {/* Code content */} - - {line - .replace( - /function/, - (match) => `${match}`, - ) - .replace( - /"[^"]*"/, - (match) => `${match}`, - ) - .replace( - /console/, - (match) => `${match}`, - )} - + {line} ))} diff --git a/app/(protected)/dashboard/urls/analytics/loading.tsx b/app/(protected)/dashboard/urls/analytics/loading.tsx new file mode 100644 index 0000000..76a24b5 --- /dev/null +++ b/app/(protected)/dashboard/urls/analytics/loading.tsx @@ -0,0 +1,9 @@ +import { Skeleton } from "@/components/ui/skeleton"; + +export default function DashboardUrlsLoading() { + return ( + <> + + + ); +} diff --git a/app/(protected)/dashboard/urls/analytics/page.tsx b/app/(protected)/dashboard/urls/analytics/page.tsx new file mode 100644 index 0000000..b657fb3 --- /dev/null +++ b/app/(protected)/dashboard/urls/analytics/page.tsx @@ -0,0 +1,24 @@ +import { redirect } from "next/navigation"; + +import { getCurrentUser } from "@/lib/session"; +import { constructMetadata } from "@/lib/utils"; +import { DashboardHeader } from "@/components/dashboard/header"; + +import Globe from "../globe"; + +export const metadata = constructMetadata({ + title: "Globe Analytics", + description: "Display link's globe analytics.", +}); + +export default async function DashboardPage() { + const user = await getCurrentUser(); + + if (!user?.id) redirect("/login"); + + return ( + <> + + + ); +} diff --git a/app/(protected)/dashboard/urls/api/loading.tsx b/app/(protected)/dashboard/urls/api/loading.tsx new file mode 100644 index 0000000..d93de10 --- /dev/null +++ b/app/(protected)/dashboard/urls/api/loading.tsx @@ -0,0 +1,10 @@ +import { Skeleton } from "@/components/ui/skeleton"; + +export default function DashboardUrlsLoading() { + return ( + <> + + + + ); +} diff --git a/app/(protected)/dashboard/urls/api/page.tsx b/app/(protected)/dashboard/urls/api/page.tsx new file mode 100644 index 0000000..62f43f4 --- /dev/null +++ b/app/(protected)/dashboard/urls/api/page.tsx @@ -0,0 +1,47 @@ +import { redirect } from "next/navigation"; + +import { getCurrentUser } from "@/lib/session"; +import { constructMetadata } from "@/lib/utils"; +import { DashboardHeader } from "@/components/dashboard/header"; +import ApiReference from "@/components/shared/api-reference"; + +import { CodeLight } from "../../scrape/scrapes"; +import LiveLog from "../live-logs"; + +export const metadata = constructMetadata({ + title: "Live Logs", + description: "Display link's real-time live logs.", +}); + +export default async function DashboardPage() { + const user = await getCurrentUser(); + + if (!user?.id) redirect("/login"); + + return ( + <> + + + + ); +} diff --git a/app/(protected)/dashboard/urls/live-logs.tsx b/app/(protected)/dashboard/urls/live-logs.tsx index 4145ba5..bfb8423 100644 --- a/app/(protected)/dashboard/urls/live-logs.tsx +++ b/app/(protected)/dashboard/urls/live-logs.tsx @@ -47,10 +47,16 @@ export interface LogEntry { isNew?: boolean; // New property to track newly added logs } -export default function LiveLog({ admin = false }: { admin?: boolean }) { +export default function LiveLog({ + admin = false, + live = false, +}: { + admin?: boolean; + live?: boolean; +}) { const { theme } = useTheme(); const { mutate } = useSWRConfig(); - const [isLive, setIsLive] = useState(false); + const [isLive, setIsLive] = useState(live); const [logs, setLogs] = useState([]); const [limitDiplay, setLimitDisplay] = useState(100); const newLogsRef = useRef>(new Set()); // Track new log keys diff --git a/app/(protected)/dashboard/urls/logs/loading.tsx b/app/(protected)/dashboard/urls/logs/loading.tsx new file mode 100644 index 0000000..61916c0 --- /dev/null +++ b/app/(protected)/dashboard/urls/logs/loading.tsx @@ -0,0 +1,11 @@ +import { Skeleton } from "@/components/ui/skeleton"; +import { DashboardHeader } from "@/components/dashboard/header"; + +export default function DashboardUrlsLoading() { + return ( + <> + + + + ); +} diff --git a/app/(protected)/dashboard/urls/logs/page.tsx b/app/(protected)/dashboard/urls/logs/page.tsx new file mode 100644 index 0000000..fbcdbe7 --- /dev/null +++ b/app/(protected)/dashboard/urls/logs/page.tsx @@ -0,0 +1,25 @@ +import { redirect } from "next/navigation"; + +import { getCurrentUser } from "@/lib/session"; +import { constructMetadata } from "@/lib/utils"; +import { DashboardHeader } from "@/components/dashboard/header"; + +import LiveLog from "../live-logs"; + +export const metadata = constructMetadata({ + title: "Live Logs", + description: "Display link's real-time live logs.", +}); + +export default async function DashboardPage() { + const user = await getCurrentUser(); + + if (!user?.id) redirect("/login"); + + return ( + <> + + + + ); +} diff --git a/app/(protected)/dashboard/urls/url-list.tsx b/app/(protected)/dashboard/urls/url-list.tsx index b78d9bd..11208f6 100644 --- a/app/(protected)/dashboard/urls/url-list.tsx +++ b/app/(protected)/dashboard/urls/url-list.tsx @@ -647,17 +647,6 @@ export default function UserUrlsList({ user, action }: UrlListProps) { ); - const rendLogs = () => ( -
- {action.indexOf("admin") > -1 ? : } - -
- ); - return ( <> {/* Grid */} - setCurrentView("Realtime")} - value="Realtime" - > - - {/* Realtime */} - {selectedUrl?.id && ( } {rendeSeachInputs()} {rendeList()} - {rendLogs()} {pathname !== "/dashboard" && } {rendeSeachInputs()} {rendeGrid()} - {rendLogs()} - - - {action.indexOf("admin") > -1 ? : } {selectedUrl?.id && ( diff --git a/app/manifest.json b/app/manifest.json index 52f19b5..730ebfb 100644 --- a/app/manifest.json +++ b/app/manifest.json @@ -3,7 +3,7 @@ "short_name": "WR.DO", "description": "All-in-one domain platform with short links, temp email, subdomain management, file storage, and open APIs.", "appid": "com.wr.do", - "versionName": "1.1.6", + "versionName": "1.1.7", "versionCode": "1", "start_url": "/", "orientation": "portrait", diff --git a/components/layout/dashboard-sidebar.tsx b/components/layout/dashboard-sidebar.tsx index edc6bfe..d9e614f 100644 --- a/components/layout/dashboard-sidebar.tsx +++ b/components/layout/dashboard-sidebar.tsx @@ -3,7 +3,7 @@ import { Fragment, useEffect, useState } from "react"; import Image from "next/image"; import { usePathname } from "next/navigation"; -import { SidebarNavItem } from "@/types"; +import { NavItem, SidebarNavItem } from "@/types"; import { Menu, PanelLeftClose, PanelRightClose } from "lucide-react"; import { useTranslations } from "next-intl"; import { Link } from "next-view-transitions"; @@ -24,6 +24,12 @@ import { } from "@/components/ui/tooltip"; import { Icons } from "@/components/shared/icons"; +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from "../ui/collapsible"; + interface DashboardSidebarProps { links: SidebarNavItem[]; } @@ -34,18 +40,183 @@ export function DashboardSidebar({ links }: DashboardSidebarProps) { const { isTablet } = useMediaQuery(); const [isSidebarExpanded, setIsSidebarExpanded] = useState(!isTablet); + const [openCollapsibles, setOpenCollapsibles] = useState>( + new Set(), + ); const toggleSidebar = () => { setIsSidebarExpanded(!isSidebarExpanded); }; + const toggleCollapsible = (itemTitle: string) => { + setOpenCollapsibles((prev) => { + const newSet = new Set(prev); + if (newSet.has(itemTitle)) { + newSet.delete(itemTitle); + } else { + newSet.add(itemTitle); + } + return newSet; + }); + }; + useEffect(() => { setIsSidebarExpanded(!isTablet); }, [isTablet]); + // Auto-open collapsibles that contain the current path + useEffect(() => { + links.forEach((section) => { + section.items.forEach((item) => { + if (item.items) { + const hasActivePath = item.items.some( + (subItem) => subItem.href === path, + ); + if (hasActivePath) { + setOpenCollapsibles((prev) => new Set(prev).add(item.title)); + } + } + }); + }); + }, [path, links]); + + const renderNavItem = (item: NavItem, isNested = false) => { + const Icon = item.icon ? Icons[item.icon] : () => null; + const hasSubItems = item.items && item.items.length > 0; + const isOpen = openCollapsibles.has(item.title); + + // Item with sub-items (collapsible) + if (hasSubItems) { + return ( + + {isSidebarExpanded ? ( + toggleCollapsible(item.title)} + > + + + {t(item.title)} + + + +
+ {item.items!.map((subItem) => renderNavItem(subItem, true))} +
+
+
+ ) : ( + + +
+ + + +
+
+ +
+ {item.items!.map((subItem) => + subItem.disabled ? ( + + {t(subItem.title)} + + ) : ( + + {t(subItem.title)} + + ), + )} +
+
+
+ )} +
+ ); + } + + // Regular link item + if (item.href) { + return ( + + {isSidebarExpanded ? ( + + + {t(item.title)} + {item.badge && ( + + {item.badge} + + )} + + ) : ( + + + + + + + + + {t(item.title)} + + )} + + ); + } + + return null; + }; + return ( -
+