From 3ac11aadaab44b15c587e316b96d77099a3368a1 Mon Sep 17 00:00:00 2001 From: Lorenzo Corallo Date: Thu, 4 Jun 2026 19:57:21 +0200 Subject: [PATCH 01/40] style: update (dark) sidebar css variables --- src/index.css | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/index.css b/src/index.css index 41a1b5f..1e1380b 100644 --- a/src/index.css +++ b/src/index.css @@ -92,13 +92,13 @@ --chart-3: oklch(0.72 0.15 58.79); --chart-4: oklch(0.66 0.2 335.4); --chart-5: oklch(0.76 0.12 214.15); - --sidebar: oklch(0.19 0.03 256.45); + --sidebar: oklch(0.24 0.03 259.05); --sidebar-foreground: oklch(0.82 0.02 256.74); - --sidebar-primary: oklch(0.49 0.22 264.38); - --sidebar-primary-foreground: oklch(1 0 0); - --sidebar-accent: oklch(0.27 0.03 254.37); - --sidebar-accent-foreground: oklch(0.97 0 286.38); - --sidebar-border: oklch(0 0 0); + --sidebar-primary: oklch(0.47 0.16 257.42); + --sidebar-primary-foreground: oklch(0.96 0.01 278.64); + --sidebar-accent: oklch(0.3 0.03 254.37); + --sidebar-accent-foreground: oklch(0.96 0.01 292.8); + --sidebar-border: oklch(0.35 0.04 254.63); --sidebar-ring: oklch(0.47 0.16 257.42); --font-sans: Poppins, sans-serif; --font-serif: Libre Baskerville, serif; From aaed723267f01d3b14ff045ad59df8d21acc46e2 Mon Sep 17 00:00:00 2001 From: Lorenzo Corallo Date: Thu, 4 Jun 2026 23:34:59 +0200 Subject: [PATCH 02/40] feat: sidebar boilerplate and breadcrumb header --- package.json | 3 +- pnpm-lock.yaml | 27 + src/app/dashboard/(active)/breadcrumb.tsx | 38 + src/app/dashboard/(active)/layout.tsx | 26 + .../(active)/telegram/groups/page.tsx | 2 +- src/app/dashboard/layout.tsx | 8 +- src/app/layout.tsx | 2 +- src/app/page.tsx | 4 +- src/components/dashboard-sidebar/data.tsx | 169 ++++ src/components/dashboard-sidebar/index.tsx | 38 + src/components/dashboard-sidebar/main-nav.tsx | 97 +++ src/components/ui/sidebar.tsx | 723 ++++++++++++++++++ tsconfig.json | 2 +- 13 files changed, 1126 insertions(+), 13 deletions(-) create mode 100644 src/app/dashboard/(active)/breadcrumb.tsx create mode 100644 src/app/dashboard/(active)/layout.tsx create mode 100644 src/components/dashboard-sidebar/data.tsx create mode 100644 src/components/dashboard-sidebar/index.tsx create mode 100644 src/components/dashboard-sidebar/main-nav.tsx create mode 100644 src/components/ui/sidebar.tsx diff --git a/package.json b/package.json index 9d621bd..4990011 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,8 @@ "tailwind-scrollbar": "^4.0.2", "tailwindcss-animate": "^1.0.7", "tw-animate-css": "^1.4.0", - "zod": "^4.3.5" + "zod": "^4.3.5", + "zustand": "^5.0.14" }, "devDependencies": { "@biomejs/biome": "2.3.10", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3df57b1..623f594 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -116,6 +116,9 @@ importers: zod: specifier: ^4.3.5 version: 4.3.5 + zustand: + specifier: ^5.0.14 + version: 5.0.14(@types/react@18.3.18)(react@18.3.1)(use-sync-external-store@1.6.0(react@18.3.1)) devDependencies: '@biomejs/biome': specifier: 2.3.10 @@ -3457,6 +3460,24 @@ packages: zod@4.3.6: resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} + zustand@5.0.14: + resolution: {integrity: sha512-/8tAspM5LMPr28b3fwLYrtdj77ECpfZviaP75CMTnwO8ISyaE4GDIG/9rDDYq/cH9D2Xw2A2RXglLInmVBQB/g==} + engines: {node: '>=12.20.0'} + peerDependencies: + '@types/react': '>=18.0.0' + immer: '>=9.0.6' + react: '>=18.0.0' + use-sync-external-store: '>=1.2.0' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + use-sync-external-store: + optional: true + snapshots: '@alloc/quick-lru@5.2.0': {} @@ -6685,3 +6706,9 @@ snapshots: zod@4.3.5: {} zod@4.3.6: {} + + zustand@5.0.14(@types/react@18.3.18)(react@18.3.1)(use-sync-external-store@1.6.0(react@18.3.1)): + optionalDependencies: + '@types/react': 18.3.18 + react: 18.3.1 + use-sync-external-store: 1.6.0(react@18.3.1) diff --git a/src/app/dashboard/(active)/breadcrumb.tsx b/src/app/dashboard/(active)/breadcrumb.tsx new file mode 100644 index 0000000..65546a1 --- /dev/null +++ b/src/app/dashboard/(active)/breadcrumb.tsx @@ -0,0 +1,38 @@ +"use client" +import { usePathname } from "next/navigation" +import { Fragment, useMemo } from "react" +import { + BreadcrumbItem, + BreadcrumbLink, + BreadcrumbList, + BreadcrumbPage, + Breadcrumb as BreadcrumbRoot, + BreadcrumbSeparator, +} from "@/components/ui/breadcrumb" +import { getBreadcrumbs } from "@/components/dashboard-sidebar/main-nav" + +export function Breadcrumb() { + const pathname = usePathname() + const items = useMemo(() => getBreadcrumbs(pathname), [pathname]) + + return ( + + + {items.map((item, i) => ( + + + {i === items.length - 1 ? ( + {item.title} + ) : item.url ? ( + {item.title} + ) : ( + item.title + )} + + {i < items.length - 1 && } + + ))} + + + ) +} diff --git a/src/app/dashboard/(active)/layout.tsx b/src/app/dashboard/(active)/layout.tsx new file mode 100644 index 0000000..26541fd --- /dev/null +++ b/src/app/dashboard/(active)/layout.tsx @@ -0,0 +1,26 @@ +import { DashboardSidebar } from "@/components/dashboard-sidebar" +import { Separator } from "@/components/ui/separator" +import { SidebarInset, SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar" +import { Breadcrumb } from "./breadcrumb" + +export default async function AdminLayout({ children }: { children: React.ReactNode }) { + return ( + + + +
+ + + +
+ {children} +
+
+ ) +} diff --git a/src/app/dashboard/(active)/telegram/groups/page.tsx b/src/app/dashboard/(active)/telegram/groups/page.tsx index 00a40eb..95f126e 100644 --- a/src/app/dashboard/(active)/telegram/groups/page.tsx +++ b/src/app/dashboard/(active)/telegram/groups/page.tsx @@ -28,7 +28,7 @@ export default async function TgGroups({ searchParams }: { searchParams: Promise

Title

Tag

Invite Link

-

Actions

+

Hide

{sorted.map((r) => ( diff --git a/src/app/dashboard/layout.tsx b/src/app/dashboard/layout.tsx index d55b79f..54aee61 100644 --- a/src/app/dashboard/layout.tsx +++ b/src/app/dashboard/layout.tsx @@ -1,5 +1,4 @@ import { redirect } from "next/navigation" -import { AdminHeader } from "@/components/admin-header" import { getServerSession } from "@/server/auth" import { trpc } from "@/server/trpc" @@ -21,10 +20,5 @@ export default async function AdminLayout({ children }: { children: React.ReactN if (!roles.includes("owner") && !roles.includes("direttivo") && !roles.includes("president")) redirect("/onboarding/unauthorized") - return ( - <> - - {children} - - ) + return children } diff --git a/src/app/layout.tsx b/src/app/layout.tsx index b503bb8..410fdf9 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -43,7 +43,7 @@ export default function RootLayout({ children }: Readonly<{ children: React.Reac disableTransitionOnChange > -
+
{children}
diff --git a/src/app/page.tsx b/src/app/page.tsx index abdba51..a658b62 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -9,12 +9,12 @@ export default async function IndexPage() { if (session.data?.user) redirect("/dashboard") return ( - <> +
- +
) } diff --git a/src/components/dashboard-sidebar/data.tsx b/src/components/dashboard-sidebar/data.tsx new file mode 100644 index 0000000..b3747e1 --- /dev/null +++ b/src/components/dashboard-sidebar/data.tsx @@ -0,0 +1,169 @@ +export type NavItem = { + title: string + url: string + items?: NavItem[] +} + +export const DSData = { + navMain: [ + { + title: "Telegram", + url: "/dashboard/telegram", + items: [ + { title: "Groups", url: "/dashboard/telegram/groups" }, + { title: "Users", url: "/dashboard/telegram/user-list" }, + ], + }, + { + title: "Getting Started", + url: "#", + items: [ + { + title: "Installation", + url: "#", + }, + { + title: "Project Structure", + url: "#", + }, + ], + }, + { + title: "Build Your Application", + url: "#", + items: [ + { + title: "Routing", + url: "#", + }, + { + title: "Data Fetching", + url: "#", + isActive: true, + }, + { + title: "Rendering", + url: "#", + }, + { + title: "Caching", + url: "#", + }, + { + title: "Styling", + url: "#", + }, + { + title: "Optimizing", + url: "#", + }, + { + title: "Configuring", + url: "#", + }, + { + title: "Testing", + url: "#", + }, + { + title: "Authentication", + url: "#", + }, + { + title: "Deploying", + url: "#", + }, + { + title: "Upgrading", + url: "#", + }, + { + title: "Examples", + url: "#", + }, + ], + }, + { + title: "API Reference", + url: "#", + items: [ + { + title: "Components", + url: "#", + }, + { + title: "File Conventions", + url: "#", + }, + { + title: "Functions", + url: "#", + }, + { + title: "next.config.js Options", + url: "#", + }, + { + title: "CLI", + url: "#", + }, + { + title: "Edge Runtime", + url: "#", + }, + ], + }, + { + title: "Architecture", + url: "#", + items: [ + { + title: "Accessibility", + url: "#", + }, + { + title: "Fast Refresh", + url: "#", + }, + { + title: "Next.js Compiler", + url: "#", + }, + { + title: "Supported Browsers", + url: "#", + }, + { + title: "Turbopack", + url: "#", + }, + ], + }, + { + title: "Community", + url: "#", + items: [ + { + title: "Contribution Guide", + url: "#", + }, + ], + }, + ], +} + +const flattenNavigation = (items: NavItem[]): Record => { + const map: Record = {} + const traverse = (list: NavItem[]) => { + for (const item of list) { + map[item.url] = item.title + if (item.items) traverse(item.items) + } + } + traverse(items) + return map +} + +console.time("navmap") +export const navMap = flattenNavigation(DSData.navMain) +console.timeEnd("navmap") diff --git a/src/components/dashboard-sidebar/index.tsx b/src/components/dashboard-sidebar/index.tsx new file mode 100644 index 0000000..3193a6e --- /dev/null +++ b/src/components/dashboard-sidebar/index.tsx @@ -0,0 +1,38 @@ +"use client" + +import { GalleryVerticalEndIcon } from "lucide-react" +import type * as React from "react" +import { + Sidebar, + SidebarContent, + SidebarHeader, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, +} from "@/components/ui/sidebar" +import { DSMainNav } from "./main-nav" + +export function DashboardSidebar({ ...props }: React.ComponentProps) { + return ( + + + + + }> +
+ +
+
+ Documentation + v1.0.0 +
+
+
+
+
+ + + +
+ ) +} diff --git a/src/components/dashboard-sidebar/main-nav.tsx b/src/components/dashboard-sidebar/main-nav.tsx new file mode 100644 index 0000000..407f54e --- /dev/null +++ b/src/components/dashboard-sidebar/main-nav.tsx @@ -0,0 +1,97 @@ +"use client" +import { usePathname } from "next/navigation" +import { + SidebarGroup, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + SidebarMenuSub, + SidebarMenuSubButton, + SidebarMenuSubItem, +} from "../ui/sidebar" +import { DSData, navMap } from "./data" + +export type BreadcrumbItem = { + title: string + url?: string +} + +console.log(navMap) +export function getBreadcrumbs(pathname: string): BreadcrumbItem[] { + const segments = pathname.split("/").filter(Boolean) + const breadcrumbs: BreadcrumbItem[] = [] + + let currentPath = "" + + for (const segment of segments) { + currentPath += `/${segment}` + let title = navMap[currentPath] + if (!title) { + if (isUUIDorId(segment)) { + title = "Details" + } else { + title = segment.charAt(0).toUpperCase() + segment.slice(1) + } + } + + breadcrumbs.push({ title, url: currentPath }) + } + + return breadcrumbs +} + +function isUUIDorId(segment: string) { + return !isNaN(Number(segment)) || segment.length > 20 // Regex custom a seconda dei tuoi ID +} + +export function DSMainNav() { + return ( + + + {DSData.navMain.map((category) => ( + + }> + {category.title} + + {category.items?.length ? ( + + {category.items.map((item) => ( + + ))} + + ) : null} + + ))} + + + ) +} + +function DSMenuItem({ + parent, + item, +}: { + parent: (typeof DSData)["navMain"][0] + item: (typeof DSData)["navMain"][0]["items"][0] +}) { + const path = usePathname() + // const { setBreadcrumb } = useSidebar() + const isActive = path === item.url + + // useEffect(() => { + // if (isActive) { + // setBreadcrumb([ + // { title: parent.title, url: parent.url }, + // { title: item.title, url: item.url }, + // ]) + // } + // }, [isActive]) + + return ( + + }> + {item.title} + + + ) +} diff --git a/src/components/ui/sidebar.tsx b/src/components/ui/sidebar.tsx new file mode 100644 index 0000000..41b2135 --- /dev/null +++ b/src/components/ui/sidebar.tsx @@ -0,0 +1,723 @@ +"use client" + +import * as React from "react" +import { mergeProps } from "@base-ui/react/merge-props" +import { useRender } from "@base-ui/react/use-render" +import { cva, type VariantProps } from "class-variance-authority" + +import { useIsMobile } from "@/hooks/use-mobile" +import { cn } from "@/lib/utils/shadcn" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { Separator } from "@/components/ui/separator" +import { + Sheet, + SheetContent, + SheetDescription, + SheetHeader, + SheetTitle, +} from "@/components/ui/sheet" +import { Skeleton } from "@/components/ui/skeleton" +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/ui/tooltip" +import { PanelLeftIcon } from "lucide-react" + +const SIDEBAR_COOKIE_NAME = "sidebar_state" +const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7 +const SIDEBAR_WIDTH = "16rem" +const SIDEBAR_WIDTH_MOBILE = "18rem" +const SIDEBAR_WIDTH_ICON = "3rem" +const SIDEBAR_KEYBOARD_SHORTCUT = "b" + +type SidebarContextProps = { + state: "expanded" | "collapsed" + open: boolean + setOpen: (open: boolean) => void + openMobile: boolean + setOpenMobile: (open: boolean) => void + isMobile: boolean + toggleSidebar: () => void +} + +const SidebarContext = React.createContext(null) + +function useSidebar() { + const context = React.useContext(SidebarContext) + if (!context) { + throw new Error("useSidebar must be used within a SidebarProvider.") + } + + return context +} + +function SidebarProvider({ + defaultOpen = true, + open: openProp, + onOpenChange: setOpenProp, + className, + style, + children, + ...props +}: React.ComponentProps<"div"> & { + defaultOpen?: boolean + open?: boolean + onOpenChange?: (open: boolean) => void +}) { + const isMobile = useIsMobile() + const [openMobile, setOpenMobile] = React.useState(false) + + // This is the internal state of the sidebar. + // We use openProp and setOpenProp for control from outside the component. + const [_open, _setOpen] = React.useState(defaultOpen) + const open = openProp ?? _open + const setOpen = React.useCallback( + (value: boolean | ((value: boolean) => boolean)) => { + const openState = typeof value === "function" ? value(open) : value + if (setOpenProp) { + setOpenProp(openState) + } else { + _setOpen(openState) + } + + // This sets the cookie to keep the sidebar state. + document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}` + }, + [setOpenProp, open] + ) + + // Helper to toggle the sidebar. + const toggleSidebar = React.useCallback(() => { + return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open) + }, [isMobile, setOpen, setOpenMobile]) + + // Adds a keyboard shortcut to toggle the sidebar. + React.useEffect(() => { + const handleKeyDown = (event: KeyboardEvent) => { + if ( + event.key === SIDEBAR_KEYBOARD_SHORTCUT && + (event.metaKey || event.ctrlKey) + ) { + event.preventDefault() + toggleSidebar() + } + } + + window.addEventListener("keydown", handleKeyDown) + return () => window.removeEventListener("keydown", handleKeyDown) + }, [toggleSidebar]) + + // We add a state so that we can do data-state="expanded" or "collapsed". + // This makes it easier to style the sidebar with Tailwind classes. + const state = open ? "expanded" : "collapsed" + + const contextValue = React.useMemo( + () => ({ + state, + open, + setOpen, + isMobile, + openMobile, + setOpenMobile, + toggleSidebar, + }), + [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar] + ) + + return ( + +
+ {children} +
+
+ ) +} + +function Sidebar({ + side = "left", + variant = "sidebar", + collapsible = "offcanvas", + className, + children, + dir, + ...props +}: React.ComponentProps<"div"> & { + side?: "left" | "right" + variant?: "sidebar" | "floating" | "inset" + collapsible?: "offcanvas" | "icon" | "none" +}) { + const { isMobile, state, openMobile, setOpenMobile } = useSidebar() + + if (collapsible === "none") { + return ( +
+ {children} +
+ ) + } + + if (isMobile) { + return ( + + + + Sidebar + Displays the mobile sidebar. + +
{children}
+
+
+ ) + } + + return ( +
+ {/* This is what handles the sidebar gap on desktop */} +
+ +
+ ) +} + +function SidebarTrigger({ + className, + onClick, + ...props +}: React.ComponentProps) { + const { toggleSidebar } = useSidebar() + + return ( + + ) +} + +function SidebarRail({ className, ...props }: React.ComponentProps<"button">) { + const { toggleSidebar } = useSidebar() + + return ( + - } - /> - - - Account - - - - Settings - - - { - await signOut() - redirect("/login") - }} - variant="destructive" - > - Logout - - - - - ) : ( -
- ) -} diff --git a/src/components/dashboard-sidebar/user-nav.tsx b/src/components/dashboard-sidebar/user-nav.tsx index 62c7f06..580c2cc 100644 --- a/src/components/dashboard-sidebar/user-nav.tsx +++ b/src/components/dashboard-sidebar/user-nav.tsx @@ -15,7 +15,7 @@ import { DropdownMenuTrigger, } from "@/components/ui/dropdown-menu" import { SidebarMenu, SidebarMenuButton, SidebarMenuItem, useSidebar } from "@/components/ui/sidebar" -import { auth, useSession } from "@/lib/auth" +import { signOut, useSession } from "@/lib/auth" import { getInitials } from "@/lib/utils" import { Skeleton } from "../ui/skeleton" @@ -87,8 +87,9 @@ export function DSUserNav() { - auth.signOut({ + signOut({ fetchOptions: { onSuccess: () => { toast.success("Logged out!") diff --git a/src/index.css b/src/index.css index 1e1380b..307d683 100644 --- a/src/index.css +++ b/src/index.css @@ -82,7 +82,7 @@ --placeholder: oklch(0.5623 0.04 280.47); --accent: oklch(0.35 0.04 254.63); --accent-foreground: oklch(0.96 0.01 292.8); - --destructive: oklch(0.51 0.19 27.08); + --destructive: oklch(0.61 0.19 27.08); --destructive-foreground: oklch(1 0 0); --border: oklch(0.35 0.04 254.63); --input: oklch(0.3961 0.03 259.05); From fc32ad7dc6277a2ee0d65b778d90769626d05602 Mon Sep 17 00:00:00 2001 From: Lorenzo Corallo Date: Fri, 5 Jun 2026 22:54:51 +0200 Subject: [PATCH 14/40] style: container mx-auto by default --- src/index.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/index.css b/src/index.css index 307d683..d2dd450 100644 --- a/src/index.css +++ b/src/index.css @@ -174,3 +174,7 @@ @theme { --breakpoint-xs: 30rem; } + +@utility container { + margin-inline: auto; +} From 51e171edbf454d3789cd9d71bd302d9756e27b8f Mon Sep 17 00:00:00 2001 From: Lorenzo Corallo Date: Fri, 5 Jun 2026 22:55:49 +0200 Subject: [PATCH 15/40] fix: remove category pages, dont create link for category in breadcrumb --- src/app/dashboard/(active)/account/page.tsx | 2 +- src/app/dashboard/(active)/azure/page.tsx | 29 --------------------- src/app/dashboard/(active)/breadcrumb.tsx | 7 ++++- src/components/dashboard-sidebar/data.tsx | 8 +++--- 4 files changed, 11 insertions(+), 35 deletions(-) delete mode 100644 src/app/dashboard/(active)/azure/page.tsx diff --git a/src/app/dashboard/(active)/account/page.tsx b/src/app/dashboard/(active)/account/page.tsx index 0a91c06..e6c00a0 100644 --- a/src/app/dashboard/(active)/account/page.tsx +++ b/src/app/dashboard/(active)/account/page.tsx @@ -22,7 +22,7 @@ export default async function Account() { const { user } = session return ( -
+

Account

diff --git a/src/app/dashboard/(active)/azure/page.tsx b/src/app/dashboard/(active)/azure/page.tsx deleted file mode 100644 index 563c8e3..0000000 --- a/src/app/dashboard/(active)/azure/page.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { UsersRound } from "lucide-react" -import Image from "next/image" -import Link from "next/link" -import azureSvg from "@/assets/svg/azure.svg" -import { Card, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" - -export default function AssocIndex() { - return ( -
-

- azure logo - Azure -

-
- - - - - - Members - - Manage all @polinetwork.org accounts - - - -
-
- ) -} diff --git a/src/app/dashboard/(active)/breadcrumb.tsx b/src/app/dashboard/(active)/breadcrumb.tsx index 7b28fa4..1b757c1 100644 --- a/src/app/dashboard/(active)/breadcrumb.tsx +++ b/src/app/dashboard/(active)/breadcrumb.tsx @@ -47,6 +47,7 @@ function getBreadcrumbs(navMap: Map, pathname: string): Breadcru const breadcrumbs: BreadcrumbItem[] = [] let currentPath = "" + let i = 0 for (const segment of segments) { currentPath += `/${segment}` @@ -59,9 +60,13 @@ function getBreadcrumbs(navMap: Map, pathname: string): Breadcru } } - breadcrumbs.push({ title, url: currentPath }) + // note: at the moment we do not plan to make category pages. + // If such pages are made in the future, this logic can be removed + breadcrumbs.push({ title, url: i !== 1 ? currentPath : undefined }) + i++ } + console.log(breadcrumbs) return breadcrumbs } diff --git a/src/components/dashboard-sidebar/data.tsx b/src/components/dashboard-sidebar/data.tsx index a87d1cc..b946fbe 100644 --- a/src/components/dashboard-sidebar/data.tsx +++ b/src/components/dashboard-sidebar/data.tsx @@ -7,7 +7,6 @@ export const DSData = { mainNav: [ { title: "Telegram", - url: "/dashboard/telegram", icon: telegram logo, items: [ { title: "Grants", url: "/dashboard/telegram/grants", icon: }, @@ -17,7 +16,6 @@ export const DSData = { }, { title: "Azure", - url: "/dashboard/azure", icon: azure logo, items: [{ title: "Members", url: "/dashboard/azure/members", icon: }], }, @@ -32,8 +30,10 @@ const flattenNavigation = (): Map => { map.set(item.url, item.title) } } - Object.entries(DSData).forEach(([_k, items]) => { - traverse(items) + Object.entries(DSData).forEach(([_k, nav]) => { + nav.forEach((category) => { + traverse(category.items) + }) }) return map } From 63625664bd15d90383508fa2a2d75609ad795c09 Mon Sep 17 00:00:00 2001 From: Lorenzo Corallo Date: Fri, 5 Jun 2026 23:13:30 +0200 Subject: [PATCH 16/40] fix: re-render issue with the use-cookie-storage hook --- src/components/dashboard-sidebar/main-nav.tsx | 16 ++++++++++------ src/hooks/use-cookie-storage.tsx | 6 +----- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/components/dashboard-sidebar/main-nav.tsx b/src/components/dashboard-sidebar/main-nav.tsx index d5d3a7c..b429ade 100644 --- a/src/components/dashboard-sidebar/main-nav.tsx +++ b/src/components/dashboard-sidebar/main-nav.tsx @@ -2,7 +2,7 @@ import { ChevronRight } from "lucide-react" import Link from "next/link" import { usePathname } from "next/navigation" -import { useEffect, useState } from "react" +import { useCallback, useEffect, useState } from "react" import { COOKIES } from "@/constants" import { useCookieStorage } from "@/hooks/use-cookie-storage" import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "../ui/collapsible" @@ -32,23 +32,27 @@ export function DSMainNav({ categoryState }: { categoryState: Record(initialOpen ?? pathname.startsWith(category.url)) + const categoryUrl = category.items[0]?.url.split("/").slice(0, 3).join("/") + const [open, setOpen] = useState(initialOpen ?? (categoryUrl ? pathname.startsWith(categoryUrl) : false)) const [_, setState] = useCookieStorage>( COOKIES.SIDEBAR_CATEGORY_STATE, {}, { expires: 60 * 60 * 24 * 7 } ) - useEffect(() => { - if (open !== undefined) + const handleOpenChange = useCallback( + (open: boolean) => { + setOpen(open) setState((state) => { state[category.title] = open return state }) - }, [open, setState, category.title]) + }, + [setState, category.title] + ) return open !== undefined ? ( - } open={open} onOpenChange={setOpen} className="group/collapsible"> + } open={open} onOpenChange={handleOpenChange} className="group/collapsible"> diff --git a/src/hooks/use-cookie-storage.tsx b/src/hooks/use-cookie-storage.tsx index ece3e52..462bb02 100644 --- a/src/hooks/use-cookie-storage.tsx +++ b/src/hooks/use-cookie-storage.tsx @@ -6,7 +6,7 @@ export function useCookieStorage( initialValue: T, options: CookieOptions = {} ): [T, Dispatch>] { - const envOptions = getDefaultCookieOptions() + const envOptions = useMemo(() => getDefaultCookieOptions(), []) const mergedOptions = useMemo(() => ({ ...envOptions, ...options }), [options, envOptions]) const readValue = useCallback((): T => { @@ -45,9 +45,5 @@ export function useCookieStorage( [key, storedValue, mergedOptions] ) - useEffect(() => { - setStoredValue(readValue()) - }, [readValue]) - return [storedValue, setValue] } From 075f156fdf94044d170cf36340fd5ac2b1a7d04f Mon Sep 17 00:00:00 2001 From: Lorenzo Corallo Date: Fri, 5 Jun 2026 23:15:30 +0200 Subject: [PATCH 17/40] fix: same as previous commit, but for use-session-storage --- src/hooks/use-session-storage.tsx | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/hooks/use-session-storage.tsx b/src/hooks/use-session-storage.tsx index 46a7ab8..b1de618 100644 --- a/src/hooks/use-session-storage.tsx +++ b/src/hooks/use-session-storage.tsx @@ -1,4 +1,4 @@ -import { type Dispatch, type SetStateAction, useCallback, useEffect, useState } from "react" +import { type Dispatch, type SetStateAction, useCallback, useState } from "react" export function useSessionStorage(key: string, initialValue: T): [T, Dispatch>] { // Read from sessionStorage on initialization @@ -35,10 +35,5 @@ export function useSessionStorage(key: string, initialValue: T): [T, Dispatch [key, storedValue] ) - // Keep state in sync if key changes - useEffect(() => { - setStoredValue(readValue()) - }, [readValue]) - return [storedValue, setValue] } From 21c64c5be4300ef6bd24476f8af53e35194e9aa5 Mon Sep 17 00:00:00 2001 From: Lorenzo Corallo Date: Fri, 5 Jun 2026 23:23:50 +0200 Subject: [PATCH 18/40] fix: coderabbit suggestion --- src/components/dashboard-sidebar/main-nav.tsx | 44 ++++++++++++------- 1 file changed, 27 insertions(+), 17 deletions(-) diff --git a/src/components/dashboard-sidebar/main-nav.tsx b/src/components/dashboard-sidebar/main-nav.tsx index b429ade..8fde2fb 100644 --- a/src/components/dashboard-sidebar/main-nav.tsx +++ b/src/components/dashboard-sidebar/main-nav.tsx @@ -19,37 +19,47 @@ import { Skeleton } from "../ui/skeleton" import { DSData } from "./data" export function DSMainNav({ categoryState }: { categoryState: Record }) { + const [_, setCategoryState] = useCookieStorage>( + COOKIES.SIDEBAR_CATEGORY_STATE, + {}, + { expires: 60 * 60 * 24 * 7 } + ) + return ( {DSData.mainNav.map((category) => ( - + { + setCategoryState((state) => ({ ...state, [category.title]: open })) + }} + /> ))} ) } -function DSMenuCategory({ category, initialOpen }: { category: (typeof DSData)["mainNav"][0]; initialOpen?: boolean }) { +function DSMenuCategory({ + category, + initialOpen, + onPersistOpen, +}: { + category: (typeof DSData)["mainNav"][0] + initialOpen?: boolean + onPersistOpen: (open: boolean) => void +}) { const pathname = usePathname() const categoryUrl = category.items[0]?.url.split("/").slice(0, 3).join("/") const [open, setOpen] = useState(initialOpen ?? (categoryUrl ? pathname.startsWith(categoryUrl) : false)) - const [_, setState] = useCookieStorage>( - COOKIES.SIDEBAR_CATEGORY_STATE, - {}, - { expires: 60 * 60 * 24 * 7 } - ) - const handleOpenChange = useCallback( - (open: boolean) => { - setOpen(open) - setState((state) => { - state[category.title] = open - return state - }) - }, - [setState, category.title] - ) + function handleOpenChange(open: boolean) { + setOpen(open) + onPersistOpen(open) + } return open !== undefined ? ( } open={open} onOpenChange={handleOpenChange} className="group/collapsible"> From e9fd9e73b1baa8fe2088287acc4a604dc5debe3a Mon Sep 17 00:00:00 2001 From: Lorenzo Corallo Date: Fri, 5 Jun 2026 23:26:11 +0200 Subject: [PATCH 19/40] fix: match also subroutes in category items --- src/app/dashboard/(active)/breadcrumb.tsx | 1 - src/components/dashboard-sidebar/main-nav.tsx | 6 +++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/app/dashboard/(active)/breadcrumb.tsx b/src/app/dashboard/(active)/breadcrumb.tsx index 1b757c1..39b19a8 100644 --- a/src/app/dashboard/(active)/breadcrumb.tsx +++ b/src/app/dashboard/(active)/breadcrumb.tsx @@ -66,7 +66,6 @@ function getBreadcrumbs(navMap: Map, pathname: string): Breadcru i++ } - console.log(breadcrumbs) return breadcrumbs } diff --git a/src/components/dashboard-sidebar/main-nav.tsx b/src/components/dashboard-sidebar/main-nav.tsx index 8fde2fb..a9bfc09 100644 --- a/src/components/dashboard-sidebar/main-nav.tsx +++ b/src/components/dashboard-sidebar/main-nav.tsx @@ -89,7 +89,11 @@ function DSMenuCategory({ function DSMenuItem({ item }: { item: (typeof DSData)["mainNav"][0]["items"][0] }) { const path = usePathname() - const isActive = path === item.url + + // NOTE: as of now, we have only 1 level depth of submenu, so using startsWith to + // match also subroutes is ok. + // If we go with multiple levels of depth it should be changed accordingly. + const isActive = path.startsWith(item.url) return ( From 60d36175765206e42173f2df4707d37dcf50cebe Mon Sep 17 00:00:00 2001 From: Lorenzo Corallo Date: Fri, 5 Jun 2026 23:45:31 +0200 Subject: [PATCH 20/40] fix: catch JSON parse error --- src/app/dashboard/(active)/layout.tsx | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/app/dashboard/(active)/layout.tsx b/src/app/dashboard/(active)/layout.tsx index 23c5c1b..d481f02 100644 --- a/src/app/dashboard/(active)/layout.tsx +++ b/src/app/dashboard/(active)/layout.tsx @@ -5,10 +5,19 @@ import { SidebarInset, SidebarProvider, SidebarTrigger } from "@/components/ui/s import { COOKIES } from "@/constants" import { Breadcrumb } from "./breadcrumb" +function parseCookie(cookie: string) { + try { + const parsed = JSON.parse(cookie) + return parsed + } catch (_e) { + return {} + } +} + export default async function AdminLayout({ children }: { children: React.ReactNode }) { const cookieStore = await cookies() const cookie = cookieStore.get(COOKIES.SIDEBAR_CATEGORY_STATE)?.value - const DSCategoryState = cookie ? JSON.parse(cookie) : {} + const DSCategoryState = cookie ? parseCookie(cookie) : {} return ( Date: Fri, 5 Jun 2026 23:46:52 +0200 Subject: [PATCH 21/40] fix: rename column in groups --- src/app/dashboard/(active)/telegram/groups/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/dashboard/(active)/telegram/groups/page.tsx b/src/app/dashboard/(active)/telegram/groups/page.tsx index 2946745..897bf1b 100644 --- a/src/app/dashboard/(active)/telegram/groups/page.tsx +++ b/src/app/dashboard/(active)/telegram/groups/page.tsx @@ -23,7 +23,7 @@ export default async function TgGroups({ searchParams }: { searchParams: Promise

Title

Tag

Invite Link

-

Hide

+

Actions

{sorted.map((r) => ( From f6e4fa30ea06b0a07a8fbcc4510a6c53dba119d3 Mon Sep 17 00:00:00 2001 From: Lorenzo Corallo Date: Fri, 5 Jun 2026 23:53:26 +0200 Subject: [PATCH 22/40] style: move breadcrumb to center --- src/app/dashboard/(active)/breadcrumb.tsx | 4 ++-- src/app/dashboard/(active)/layout.tsx | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/app/dashboard/(active)/breadcrumb.tsx b/src/app/dashboard/(active)/breadcrumb.tsx index 39b19a8..17052fe 100644 --- a/src/app/dashboard/(active)/breadcrumb.tsx +++ b/src/app/dashboard/(active)/breadcrumb.tsx @@ -16,12 +16,12 @@ export type BreadcrumbItem = { url?: string } -export function Breadcrumb() { +export function Breadcrumb({ className, ...props }: React.ComponentProps<"nav">) { const pathname = usePathname() const items = useMemo(() => getBreadcrumbs(NAV_MAP, pathname), [pathname]) return ( - + {items.map((item, i) => ( diff --git a/src/app/dashboard/(active)/layout.tsx b/src/app/dashboard/(active)/layout.tsx index d481f02..4c1745d 100644 --- a/src/app/dashboard/(active)/layout.tsx +++ b/src/app/dashboard/(active)/layout.tsx @@ -1,6 +1,5 @@ import { cookies } from "next/headers" import { DashboardSidebar } from "@/components/dashboard-sidebar" -import { Separator } from "@/components/ui/separator" import { SidebarInset, SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar" import { COOKIES } from "@/constants" import { Breadcrumb } from "./breadcrumb" @@ -29,13 +28,14 @@ export default async function AdminLayout({ children }: { children: React.ReactN > -
+
- - + +
{children} ) } +// From fd76b346c5f7f44fd4ba57dc7366a604d7b708a8 Mon Sep 17 00:00:00 2001 From: Lorenzo Corallo Date: Fri, 5 Jun 2026 23:59:05 +0200 Subject: [PATCH 23/40] fix: cleanup home --- src/app/dashboard/(active)/page.tsx | 39 ++++++----------------------- src/components/ui/alert.tsx | 1 + 2 files changed, 8 insertions(+), 32 deletions(-) diff --git a/src/app/dashboard/(active)/page.tsx b/src/app/dashboard/(active)/page.tsx index d0256eb..0e13529 100644 --- a/src/app/dashboard/(active)/page.tsx +++ b/src/app/dashboard/(active)/page.tsx @@ -1,8 +1,5 @@ -import Image from "next/image" -import Link from "next/link" -import azureSvg from "@/assets/svg/azure.svg" -import telegramSvg from "@/assets/svg/telegram.svg" -import { Card, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" +import { InfoIcon } from "lucide-react" +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert" import { getServerSession } from "@/server/auth" import { CompleteProfile } from "./complete-profile" @@ -12,33 +9,11 @@ export default async function AdminHome() { session && (
-

Home

- -
- - - - - azure logo - Azure - - Manage Azure related things - - - - - - - - - telegram logo - Telegram - - Manage Telegram related things - - - -
+ + + Page under construction + Use the sidebar to access the sections +
) ) diff --git a/src/components/ui/alert.tsx b/src/components/ui/alert.tsx index e130133..52a36bc 100644 --- a/src/components/ui/alert.tsx +++ b/src/components/ui/alert.tsx @@ -9,6 +9,7 @@ const alertVariants = cva( variants: { variant: { default: "bg-card text-card-foreground", + info: "bg-primary/20 text-primary-foreground", destructive: "bg-card text-destructive *:data-[slot=alert-description]:text-destructive/90 *:[svg]:text-current", }, From d7388b5fb31a14bd712385b4af7dba3b61425eca Mon Sep 17 00:00:00 2001 From: Lorenzo Corallo Date: Sat, 6 Jun 2026 00:05:15 +0200 Subject: [PATCH 24/40] style: button icon svg size --- src/app/dashboard/(active)/layout.tsx | 2 +- src/components/ui/button.tsx | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/app/dashboard/(active)/layout.tsx b/src/app/dashboard/(active)/layout.tsx index 4c1745d..f0b6d5e 100644 --- a/src/app/dashboard/(active)/layout.tsx +++ b/src/app/dashboard/(active)/layout.tsx @@ -29,7 +29,7 @@ export default async function AdminLayout({ children }: { children: React.ReactN
- +
diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx index 598ffa0..e696922 100644 --- a/src/components/ui/button.tsx +++ b/src/components/ui/button.tsx @@ -25,12 +25,12 @@ const buttonVariants = cva( xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3", sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5", lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2", - icon: "size-8", + icon: "size-8 [&_svg:not([class*='size-'])]:size-4.5", "icon-xs": "size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3", "icon-sm": - "size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg", - "icon-lg": "size-9", + "size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-4", + "icon-lg": "size-9 [&_svg:not([class*='size-'])]:size-5", }, }, defaultVariants: { From 57d3a2324f62389946b89f06613dc9eb1be5e0ab Mon Sep 17 00:00:00 2001 From: Lorenzo Corallo Date: Sat, 6 Jun 2026 20:38:54 +0200 Subject: [PATCH 25/40] style: fix padding mismatch between pages and loadings --- src/app/dashboard/(active)/azure/members/loading.tsx | 2 +- src/app/dashboard/(active)/telegram/grants/loading.tsx | 4 ++-- src/app/dashboard/(active)/telegram/grants/page.tsx | 2 +- src/app/dashboard/(active)/telegram/groups/loading.tsx | 2 +- src/app/dashboard/(active)/telegram/groups/page.tsx | 2 +- src/app/dashboard/(active)/telegram/user-list/loading.tsx | 4 ++-- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/app/dashboard/(active)/azure/members/loading.tsx b/src/app/dashboard/(active)/azure/members/loading.tsx index ac43fc0..a6bd30c 100644 --- a/src/app/dashboard/(active)/azure/members/loading.tsx +++ b/src/app/dashboard/(active)/azure/members/loading.tsx @@ -2,7 +2,7 @@ import { SkeletonAssocTable } from "./table" export default function Loading() { return ( -
+
) diff --git a/src/app/dashboard/(active)/telegram/grants/loading.tsx b/src/app/dashboard/(active)/telegram/grants/loading.tsx index f74f889..753f796 100644 --- a/src/app/dashboard/(active)/telegram/grants/loading.tsx +++ b/src/app/dashboard/(active)/telegram/grants/loading.tsx @@ -4,8 +4,8 @@ import { NewGrant } from "./new-grant" export default async function Loading() { return ( -
-
+
+

Telegram Grants

diff --git a/src/app/dashboard/(active)/telegram/grants/page.tsx b/src/app/dashboard/(active)/telegram/grants/page.tsx index dc01bc8..ce41ade 100644 --- a/src/app/dashboard/(active)/telegram/grants/page.tsx +++ b/src/app/dashboard/(active)/telegram/grants/page.tsx @@ -9,7 +9,7 @@ export default async function GrantsPage() { return (
-
+

Telegram Grants

diff --git a/src/app/dashboard/(active)/telegram/groups/loading.tsx b/src/app/dashboard/(active)/telegram/groups/loading.tsx index 9bd5207..77b98c0 100644 --- a/src/app/dashboard/(active)/telegram/groups/loading.tsx +++ b/src/app/dashboard/(active)/telegram/groups/loading.tsx @@ -3,7 +3,7 @@ import { SearchInput } from "./search-input" export default async function Loading() { return ( -
+

Count:

diff --git a/src/app/dashboard/(active)/telegram/groups/page.tsx b/src/app/dashboard/(active)/telegram/groups/page.tsx index 897bf1b..8987115 100644 --- a/src/app/dashboard/(active)/telegram/groups/page.tsx +++ b/src/app/dashboard/(active)/telegram/groups/page.tsx @@ -12,7 +12,7 @@ export default async function TgGroups({ searchParams }: { searchParams: Promise const sorted = rows.sort((a, b) => a.title.localeCompare(b.title)) return ( -
+

Count: {rows.length} diff --git a/src/app/dashboard/(active)/telegram/user-list/loading.tsx b/src/app/dashboard/(active)/telegram/user-list/loading.tsx index 55f8aa6..0b79c1a 100644 --- a/src/app/dashboard/(active)/telegram/user-list/loading.tsx +++ b/src/app/dashboard/(active)/telegram/user-list/loading.tsx @@ -2,8 +2,8 @@ import { Skeleton } from "@/components/ui/skeleton" export default async function Loading() { return ( -

-
+
+

Count:

From 5003e80b1ab63e96819953cbee7703ffdbeb535e Mon Sep 17 00:00:00 2001 From: Lorenzo Corallo Date: Sat, 6 Jun 2026 20:43:06 +0200 Subject: [PATCH 26/40] feat: loading skeleton for account page --- .../dashboard/(active)/account/loading.tsx | 34 +++++++++++++++++++ src/components/dashboard-sidebar/main-nav.tsx | 2 +- src/hooks/use-cookie-storage.tsx | 2 +- 3 files changed, 36 insertions(+), 2 deletions(-) create mode 100644 src/app/dashboard/(active)/account/loading.tsx diff --git a/src/app/dashboard/(active)/account/loading.tsx b/src/app/dashboard/(active)/account/loading.tsx new file mode 100644 index 0000000..f67b7c1 --- /dev/null +++ b/src/app/dashboard/(active)/account/loading.tsx @@ -0,0 +1,34 @@ +import { Skeleton } from "@/components/ui/skeleton" +import { NewPasskeyButton } from "./passkey-button" + +export default function Loading() { + return ( +
+

Account

+
+ + +
+
+ Name: + +
+ +
+ Email: + +
+
+ Telegram: + +
+
+
+
+

Passkeys

+ + +
+
+ ) +} diff --git a/src/components/dashboard-sidebar/main-nav.tsx b/src/components/dashboard-sidebar/main-nav.tsx index a9bfc09..556d708 100644 --- a/src/components/dashboard-sidebar/main-nav.tsx +++ b/src/components/dashboard-sidebar/main-nav.tsx @@ -2,7 +2,7 @@ import { ChevronRight } from "lucide-react" import Link from "next/link" import { usePathname } from "next/navigation" -import { useCallback, useEffect, useState } from "react" +import { useState } from "react" import { COOKIES } from "@/constants" import { useCookieStorage } from "@/hooks/use-cookie-storage" import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "../ui/collapsible" diff --git a/src/hooks/use-cookie-storage.tsx b/src/hooks/use-cookie-storage.tsx index 462bb02..a6339cc 100644 --- a/src/hooks/use-cookie-storage.tsx +++ b/src/hooks/use-cookie-storage.tsx @@ -1,4 +1,4 @@ -import { type Dispatch, type SetStateAction, useCallback, useEffect, useMemo, useState } from "react" +import { type Dispatch, type SetStateAction, useCallback, useMemo, useState } from "react" import { type CookieOptions, deleteCookie, getCookie, getDefaultCookieOptions, setCookie } from "@/utils/cookies" export function useCookieStorage( From 0b1596c723876ae73d2afcd3c4aa8e5f5887bc43 Mon Sep 17 00:00:00 2001 From: Lorenzo Corallo Date: Sat, 6 Jun 2026 20:45:38 +0200 Subject: [PATCH 27/40] perf: make dashboard homepage static for now --- .../dashboard/(active)/complete-profile.tsx | 10 ++++++--- src/app/dashboard/(active)/page.tsx | 22 ++++++++----------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/app/dashboard/(active)/complete-profile.tsx b/src/app/dashboard/(active)/complete-profile.tsx index 8de2c3f..89fc4ac 100644 --- a/src/app/dashboard/(active)/complete-profile.tsx +++ b/src/app/dashboard/(active)/complete-profile.tsx @@ -1,13 +1,17 @@ "use client" -import type { User } from "better-auth" import { UserRoundPenIcon } from "lucide-react" import Link from "next/link" import { Button } from "@/components/ui/button" +import { useSession } from "@/lib/auth" + +export function CompleteProfile() { + const { data, isPending } = useSession() + + if (!data || isPending) return null -export function CompleteProfile({ user }: { user: User }) { return ( - !user.name && ( + !data.user.name && (

Your profile is incomplete, please enter the missing information.

diff --git a/src/app/dashboard/(active)/page.tsx b/src/app/dashboard/(active)/page.tsx index 0e13529..a0cb34b 100644 --- a/src/app/dashboard/(active)/page.tsx +++ b/src/app/dashboard/(active)/page.tsx @@ -1,20 +1,16 @@ import { InfoIcon } from "lucide-react" import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert" -import { getServerSession } from "@/server/auth" import { CompleteProfile } from "./complete-profile" -export default async function AdminHome() { - const { data: session } = await getServerSession() +export default function AdminHome() { return ( - session && ( -
- - - - Page under construction - Use the sidebar to access the sections - -
- ) +
+ + + + Page under construction + Use the sidebar to access the sections + +
) } From 585f6f64f04e8bd7f56c2e4c91c3380d6ac19fc3 Mon Sep 17 00:00:00 2001 From: Lorenzo Corallo Date: Wed, 10 Jun 2026 23:00:36 +0200 Subject: [PATCH 28/40] fix: review --- src/app/dashboard/(active)/layout.tsx | 19 ++++++++++++++----- src/components/dashboard-sidebar/main-nav.tsx | 5 +---- src/components/ui/sidebar.tsx | 12 ++++++++---- src/constants.ts | 1 + tsconfig.json | 11 +---------- 5 files changed, 25 insertions(+), 23 deletions(-) diff --git a/src/app/dashboard/(active)/layout.tsx b/src/app/dashboard/(active)/layout.tsx index f0b6d5e..016881d 100644 --- a/src/app/dashboard/(active)/layout.tsx +++ b/src/app/dashboard/(active)/layout.tsx @@ -14,19 +14,18 @@ function parseCookie(cookie: string) { } export default async function AdminLayout({ children }: { children: React.ReactNode }) { - const cookieStore = await cookies() - const cookie = cookieStore.get(COOKIES.SIDEBAR_CATEGORY_STATE)?.value - const DSCategoryState = cookie ? parseCookie(cookie) : {} + const sidebarCookies = await getSidebarCookies() return ( - +
@@ -38,4 +37,14 @@ export default async function AdminLayout({ children }: { children: React.ReactN ) } -// + +async function getSidebarCookies() { + const cookieStore = await cookies() + + const sidebarCategoryStateCookie = cookieStore.get(COOKIES.SIDEBAR_CATEGORY_STATE)?.value + const sidebarOpenCookie = cookieStore.get(COOKIES.SIDEBAR_OPEN)?.value + const state: Record = sidebarCategoryStateCookie ? parseCookie(sidebarCategoryStateCookie) : {} + const open: boolean = sidebarOpenCookie !== undefined ? parseCookie(sidebarOpenCookie) : true + + return { state, open } +} diff --git a/src/components/dashboard-sidebar/main-nav.tsx b/src/components/dashboard-sidebar/main-nav.tsx index 556d708..7b7acf5 100644 --- a/src/components/dashboard-sidebar/main-nav.tsx +++ b/src/components/dashboard-sidebar/main-nav.tsx @@ -15,7 +15,6 @@ import { SidebarMenuSubButton, SidebarMenuSubItem, } from "../ui/sidebar" -import { Skeleton } from "../ui/skeleton" import { DSData } from "./data" export function DSMainNav({ categoryState }: { categoryState: Record }) { @@ -61,7 +60,7 @@ function DSMenuCategory({ onPersistOpen(open) } - return open !== undefined ? ( + return ( } open={open} onOpenChange={handleOpenChange} className="group/collapsible"> - ) : ( - ) } diff --git a/src/components/ui/sidebar.tsx b/src/components/ui/sidebar.tsx index 41b2135..3205f8a 100644 --- a/src/components/ui/sidebar.tsx +++ b/src/components/ui/sidebar.tsx @@ -24,9 +24,9 @@ import { TooltipTrigger, } from "@/components/ui/tooltip" import { PanelLeftIcon } from "lucide-react" +import { useCookieStorage } from "@/hooks/use-cookie-storage" +import { COOKIES } from "@/constants" -const SIDEBAR_COOKIE_NAME = "sidebar_state" -const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7 const SIDEBAR_WIDTH = "16rem" const SIDEBAR_WIDTH_MOBILE = "18rem" const SIDEBAR_WIDTH_ICON = "3rem" @@ -71,6 +71,11 @@ function SidebarProvider({ // This is the internal state of the sidebar. // We use openProp and setOpenProp for control from outside the component. + const [_, setOpenCookie] = useCookieStorage( + COOKIES.SIDEBAR_OPEN, + false, + { expires: 60 * 60 * 24 * 7 } + ) const [_open, _setOpen] = React.useState(defaultOpen) const open = openProp ?? _open const setOpen = React.useCallback( @@ -82,8 +87,7 @@ function SidebarProvider({ _setOpen(openState) } - // This sets the cookie to keep the sidebar state. - document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}` + setOpenCookie(openState) }, [setOpenProp, open] ) diff --git a/src/constants.ts b/src/constants.ts index 74f37c5..b0d438f 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,3 +1,4 @@ export const COOKIES = { + SIDEBAR_OPEN: "sidebar_open", SIDEBAR_CATEGORY_STATE: "sidebar_category_state", } as const diff --git a/tsconfig.json b/tsconfig.json index b9dad8c..3f6bceb 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -29,15 +29,6 @@ "@/*": ["./src/*"] } }, - "include": [ - ".eslintrc.cjs", - "next-env.d.ts", - "**/*.ts", - "**/*.tsx", - "**/*.cjs", - "**/*.js", - ".next/types/**/*.ts", - "src/components/admin-sidebar" - ], + "include": [".eslintrc.cjs", "next-env.d.ts", "**/*.ts", "**/*.tsx", "**/*.cjs", "**/*.js", ".next/types/**/*.ts"], "exclude": ["node_modules"] } From 4797e4493b8fa7f5b94bcc36ea6212ea96d23992 Mon Sep 17 00:00:00 2001 From: Lorenzo Corallo Date: Wed, 10 Jun 2026 23:28:44 +0200 Subject: [PATCH 29/40] fix: bring back user details page, link from user list --- .../(active)/telegram/grants/delete-grant.tsx | 10 +- .../(active)/telegram/user-details/page.tsx | 137 ------------------ .../{user-details => users/[id]}/add-role.tsx | 16 +- .../[id]}/card-audit-log.tsx | 0 .../[id]}/card-group-admin.tsx | 6 +- .../[id]}/card-message.tsx | 0 .../[id]}/card-user-grant.tsx | 14 +- .../[id]}/card-user-info.tsx | 9 +- .../[id]}/delete-group-admin.tsx | 16 +- .../(active)/telegram/users/[id]/loading.tsx | 28 ++++ .../[id]}/new-group-admin.tsx | 16 +- .../(active)/telegram/users/[id]/page.tsx | 71 +++++++++ .../[id]}/remove-role.tsx | 10 +- .../telegram/{user-list => users}/loading.tsx | 0 .../telegram/{user-list => users}/page.tsx | 15 +- src/components/dashboard-sidebar/data.tsx | 2 +- src/index.css | 1 + src/server/actions/users.ts | 4 +- 18 files changed, 192 insertions(+), 163 deletions(-) delete mode 100644 src/app/dashboard/(active)/telegram/user-details/page.tsx rename src/app/dashboard/(active)/telegram/{user-details => users/[id]}/add-role.tsx (94%) rename src/app/dashboard/(active)/telegram/{user-details => users/[id]}/card-audit-log.tsx (100%) rename src/app/dashboard/(active)/telegram/{user-details => users/[id]}/card-group-admin.tsx (91%) rename src/app/dashboard/(active)/telegram/{user-details => users/[id]}/card-message.tsx (100%) rename src/app/dashboard/(active)/telegram/{user-details => users/[id]}/card-user-grant.tsx (78%) rename src/app/dashboard/(active)/telegram/{user-details => users/[id]}/card-user-info.tsx (89%) rename src/app/dashboard/(active)/telegram/{user-details => users/[id]}/delete-group-admin.tsx (90%) create mode 100644 src/app/dashboard/(active)/telegram/users/[id]/loading.tsx rename src/app/dashboard/(active)/telegram/{user-details => users/[id]}/new-group-admin.tsx (94%) create mode 100644 src/app/dashboard/(active)/telegram/users/[id]/page.tsx rename src/app/dashboard/(active)/telegram/{user-details => users/[id]}/remove-role.tsx (96%) rename src/app/dashboard/(active)/telegram/{user-list => users}/loading.tsx (100%) rename src/app/dashboard/(active)/telegram/{user-list => users}/page.tsx (67%) diff --git a/src/app/dashboard/(active)/telegram/grants/delete-grant.tsx b/src/app/dashboard/(active)/telegram/grants/delete-grant.tsx index 8743e65..c12558b 100644 --- a/src/app/dashboard/(active)/telegram/grants/delete-grant.tsx +++ b/src/app/dashboard/(active)/telegram/grants/delete-grant.tsx @@ -19,7 +19,13 @@ import { import { Button } from "@/components/ui/button" import { interruptGrant } from "@/server/actions/grants" -export function DeleteGrant({ userId, onDelete }: { userId: number; onDelete(): void }) { +export function DeleteGrant({ + userId, + // onDelete +}: { + userId: number + // onDelete(): void +}) { const router = useRouter() const [open, setOpen] = useState(false) @@ -33,7 +39,7 @@ export function DeleteGrant({ userId, onDelete }: { userId: number; onDelete(): else { toast.success("Grant interrupted successfully") router.refresh() - onDelete() + // onDelete() } } catch (err) { toast.error("There was an error") diff --git a/src/app/dashboard/(active)/telegram/user-details/page.tsx b/src/app/dashboard/(active)/telegram/user-details/page.tsx deleted file mode 100644 index 276ba7f..0000000 --- a/src/app/dashboard/(active)/telegram/user-details/page.tsx +++ /dev/null @@ -1,137 +0,0 @@ -"use client" -import { RefreshCcw, Search, X } from "lucide-react" -import { Suspense, useState, useTransition } from "react" -import { Spinner } from "@/components/spinner" -import { Button } from "@/components/ui/button" -import { Input } from "@/components/ui/input" -import { Label } from "@/components/ui/label" -import { searchUser } from "@/server/actions/users" -import { AuditLogCard } from "./card-audit-log" -import { GroupAdminCard } from "./card-group-admin" -import { MessageCard } from "./card-message" -import { UserGrantCard } from "./card-user-grant" -import { UserInfoCard } from "./card-user-info" -import { NewGroupAdmin } from "./new-group-admin" - -type Data = Awaited> - -export default function TgUsers() { - const [username, setUsername] = useState("") - const [data, setData] = useState(null) - const [pending, startTransition] = useTransition() - - async function submit() { - startTransition(async () => { - const user = await searchUser(username) - setData(user) - }) - } - - async function reset() { - setUsername("") - setData(null) - } - - return ( -
- -
- -
- { - setUsername(e.target.value) - }} - value={username} - /> - - {data ? ( - - ) : ( - - )} - {data && ( - - )} -
-
- - -
- - {data && ( - <> -
- - {data.grant && } -
- -
-

Admin in groups:

- g?.group.id ?? 0) ?? []} - onConfirm={submit} - /> -
-
- {data.groupAdmin - .filter((m) => m !== null && m !== undefined) - .map((m) => ( - - ))} - - {data.groupAdmin.length === 0 && ( -

- This user is not group admin in any group. -

- )} -
- -

Last messages (max 12):

-
- {data.messages?.map((m) => ( - - ))} - - {data.messages?.length === 0 && ( -

- No recent messages sent by this user -

- )} -
- -

Audit log:

-
- {data.audits.map((m) => ( - - - - ))} - {data.audits.length === 0 && ( -

- No audit log found for this user -

- )} -
- - )} -
- ) -} diff --git a/src/app/dashboard/(active)/telegram/user-details/add-role.tsx b/src/app/dashboard/(active)/telegram/users/[id]/add-role.tsx similarity index 94% rename from src/app/dashboard/(active)/telegram/user-details/add-role.tsx rename to src/app/dashboard/(active)/telegram/users/[id]/add-role.tsx index dfd5685..361ddc1 100644 --- a/src/app/dashboard/(active)/telegram/user-details/add-role.tsx +++ b/src/app/dashboard/(active)/telegram/users/[id]/add-role.tsx @@ -1,6 +1,7 @@ "use client" import { USER_ROLE } from "@polinetwork/backend" import { Plus } from "lucide-react" +import { useRouter } from "next/navigation" import { useState } from "react" import { toast } from "sonner" import { Spinner } from "@/components/spinner" @@ -29,7 +30,15 @@ const ARRAY_USER_ROLES = [ USER_ROLE.PRESIDENT, ] as const -export function AddRole({ user, alreadyRoles, onAdd }: { user: TgUser; alreadyRoles: TgUserRole[]; onAdd(): void }) { +export function AddRole({ + user, + alreadyRoles, + // onAdd +}: { + user: TgUser + alreadyRoles: TgUserRole[] + // onAdd(): void +}) { const availableRoles = ARRAY_USER_ROLES.filter((r) => !alreadyRoles.includes(r)).map((g) => ({ value: g, label: `${g.slice(0, 1).toUpperCase()}${g.slice(1)}`, @@ -39,6 +48,8 @@ export function AddRole({ user, alreadyRoles, onAdd }: { user: TgUser; alreadyRo const [pending, setPending] = useState(false) const [selectedRole, setSelectedRole] = useState(null) + const router = useRouter() + async function submit() { if (!selectedRole) return toast.warning("No group selected, cannot proceed") if (!user) return toast.warning("Invalid user, try restarting the dialog") @@ -53,7 +64,8 @@ export function AddRole({ user, alreadyRoles, onAdd }: { user: TgUser; alreadyRo else if (error === "UNAUTHORIZED_SELF_ASSIGN") toast.error("You cannot add roles to yourself") else { toast.success(`Role added!`) - onAdd() + router.refresh() + // onAdd() } handleOpenChange(false) } catch (err) { diff --git a/src/app/dashboard/(active)/telegram/user-details/card-audit-log.tsx b/src/app/dashboard/(active)/telegram/users/[id]/card-audit-log.tsx similarity index 100% rename from src/app/dashboard/(active)/telegram/user-details/card-audit-log.tsx rename to src/app/dashboard/(active)/telegram/users/[id]/card-audit-log.tsx diff --git a/src/app/dashboard/(active)/telegram/user-details/card-group-admin.tsx b/src/app/dashboard/(active)/telegram/users/[id]/card-group-admin.tsx similarity index 91% rename from src/app/dashboard/(active)/telegram/user-details/card-group-admin.tsx rename to src/app/dashboard/(active)/telegram/users/[id]/card-group-admin.tsx index e098242..ea70a93 100644 --- a/src/app/dashboard/(active)/telegram/user-details/card-group-admin.tsx +++ b/src/app/dashboard/(active)/telegram/users/[id]/card-group-admin.tsx @@ -10,11 +10,11 @@ type GroupAdminSingle = NonNullable groupAdminInfo: GroupAdminSingle - onDelete(): void + // onDelete(): void }) { return ( @@ -32,7 +32,7 @@ export function GroupAdminCard({

- +
) diff --git a/src/app/dashboard/(active)/telegram/user-details/card-message.tsx b/src/app/dashboard/(active)/telegram/users/[id]/card-message.tsx similarity index 100% rename from src/app/dashboard/(active)/telegram/user-details/card-message.tsx rename to src/app/dashboard/(active)/telegram/users/[id]/card-message.tsx diff --git a/src/app/dashboard/(active)/telegram/user-details/card-user-grant.tsx b/src/app/dashboard/(active)/telegram/users/[id]/card-user-grant.tsx similarity index 78% rename from src/app/dashboard/(active)/telegram/user-details/card-user-grant.tsx rename to src/app/dashboard/(active)/telegram/users/[id]/card-user-grant.tsx index 05a80ab..7dbeb37 100644 --- a/src/app/dashboard/(active)/telegram/user-details/card-user-grant.tsx +++ b/src/app/dashboard/(active)/telegram/users/[id]/card-user-grant.tsx @@ -3,9 +3,17 @@ import { format } from "date-fns" import { Sparkle } from "lucide-react" import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card" import type { TgGrant, TgUser } from "@/server/trpc/types" -import { DeleteGrant } from "../grants/delete-grant" +import { DeleteGrant } from "../../grants/delete-grant" -export function UserGrantCard({ user, grant, onDelete }: { user: TgUser; grant: TgGrant; onDelete(): void }) { +export function UserGrantCard({ + user, + grant, + //onDelete +}: { + user: TgUser + grant: TgGrant + //onDelete(): void +}) { return ( @@ -19,7 +27,7 @@ export function UserGrantCard({ user, grant, onDelete }: { user: TgUser; grant:

End: {format(grant.validUntil, "yyyy/MM/dd HH:mm")}

- +
) diff --git a/src/app/dashboard/(active)/telegram/user-details/card-user-info.tsx b/src/app/dashboard/(active)/telegram/users/[id]/card-user-info.tsx similarity index 89% rename from src/app/dashboard/(active)/telegram/user-details/card-user-info.tsx rename to src/app/dashboard/(active)/telegram/users/[id]/card-user-info.tsx index ca2362a..52741df 100644 --- a/src/app/dashboard/(active)/telegram/user-details/card-user-info.tsx +++ b/src/app/dashboard/(active)/telegram/users/[id]/card-user-info.tsx @@ -1,3 +1,4 @@ +"use client" import { Star } from "lucide-react" import { Badge } from "@/components/ui/badge" import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/components/ui/card" @@ -12,11 +13,11 @@ type UserRoles = ApiOutput["tg"]["permissions"]["getRoles"]["roles"] export function UserInfoCard({ user, roles, - onUpdate, + // onUpdate, }: { user: NonNullable roles: UserRoles - onUpdate(): void + // onUpdate(): void }) { const sesh = useSession() const seshUserId = sesh.data?.user.telegramId @@ -49,8 +50,8 @@ export function UserInfoCard({
- - + + ) diff --git a/src/app/dashboard/(active)/telegram/user-details/delete-group-admin.tsx b/src/app/dashboard/(active)/telegram/users/[id]/delete-group-admin.tsx similarity index 90% rename from src/app/dashboard/(active)/telegram/user-details/delete-group-admin.tsx rename to src/app/dashboard/(active)/telegram/users/[id]/delete-group-admin.tsx index 3a2cdcc..25b0b36 100644 --- a/src/app/dashboard/(active)/telegram/user-details/delete-group-admin.tsx +++ b/src/app/dashboard/(active)/telegram/users/[id]/delete-group-admin.tsx @@ -1,6 +1,7 @@ "use client" import { Trash2, Trash2Icon } from "lucide-react" +import { useRouter } from "next/navigation" import { useState } from "react" import { toast } from "sonner" import { Spinner } from "@/components/spinner" @@ -19,10 +20,20 @@ import { import { Button } from "@/components/ui/button" import { delGroupAdmin } from "@/server/actions/users" -export function DeleteGroupAdmin({ userId, chatId, onDelete }: { userId: number; chatId: number; onDelete(): void }) { +export function DeleteGroupAdmin({ + userId, + chatId, + // onDelete +}: { + userId: number + chatId: number + // onDelete(): void +}) { const [open, setOpen] = useState(false) const [pending, setPending] = useState(false) + const router = useRouter() + async function deleteGroupAdmin() { setPending(true) @@ -35,7 +46,8 @@ export function DeleteGroupAdmin({ userId, chatId, onDelete }: { userId: number; else if (error === "UNAUTHORIZED_SELF_ASSIGN") toast.error("You cannot delete on yourself") else { toast.success("Group Admin deleted!") - onDelete() + router.refresh() + // onDelete() } } catch (err) { toast.error("There was an error") diff --git a/src/app/dashboard/(active)/telegram/users/[id]/loading.tsx b/src/app/dashboard/(active)/telegram/users/[id]/loading.tsx new file mode 100644 index 0000000..e9a6ed7 --- /dev/null +++ b/src/app/dashboard/(active)/telegram/users/[id]/loading.tsx @@ -0,0 +1,28 @@ +import { Skeleton } from "@/components/ui/skeleton" + +export default async function Loading() { + return ( +
+
+ + +
+

Admin in groups:

+
+ + +
+

Last messages (max 12):

+
+ + +
+

Audit log:

+
+ {new Array(9).fill(0).map((_, i) => ( + + ))} +
+
+ ) +} diff --git a/src/app/dashboard/(active)/telegram/user-details/new-group-admin.tsx b/src/app/dashboard/(active)/telegram/users/[id]/new-group-admin.tsx similarity index 94% rename from src/app/dashboard/(active)/telegram/user-details/new-group-admin.tsx rename to src/app/dashboard/(active)/telegram/users/[id]/new-group-admin.tsx index 58763cc..840e20d 100644 --- a/src/app/dashboard/(active)/telegram/user-details/new-group-admin.tsx +++ b/src/app/dashboard/(active)/telegram/users/[id]/new-group-admin.tsx @@ -1,5 +1,6 @@ "use client" import { Plus, Search, X } from "lucide-react" +import { useRouter } from "next/navigation" import { useState } from "react" import { toast } from "sonner" import { Button } from "@/components/ui/button" @@ -23,12 +24,22 @@ import type { ApiOutput } from "@/server/trpc/types" type Groups = ApiOutput["tg"]["groups"]["search"]["groups"] type User = ApiOutput["tg"]["users"]["getByUsername"]["user"] -export function NewGroupAdmin({ user, alreadyIn, onConfirm }: { user: User; alreadyIn: number[]; onConfirm(): void }) { +export function NewGroupAdmin({ + user, + alreadyIn, + // onConfirm +}: { + user: User + alreadyIn: number[] + // onConfirm(): void +}) { const [open, setOpen] = useState(false) const [groupQuery, setGroupQuery] = useState("") const [groups, setGroups] = useState([]) const [selectedGroup, setSelectedGroup] = useState(null) + const router = useRouter() + async function search() { const { groups } = await searchGroup(groupQuery) setGroups(groups.filter((g) => !alreadyIn.includes(g.telegramId))) @@ -44,7 +55,8 @@ export function NewGroupAdmin({ user, alreadyIn, onConfirm }: { user: User; alre toast.error("You don't have enough permissions") } else { toast.info(`Group admin added`) - onConfirm() + router.refresh() + // onConfirm() } } catch (err) { console.error(err) diff --git a/src/app/dashboard/(active)/telegram/users/[id]/page.tsx b/src/app/dashboard/(active)/telegram/users/[id]/page.tsx new file mode 100644 index 0000000..bd82065 --- /dev/null +++ b/src/app/dashboard/(active)/telegram/users/[id]/page.tsx @@ -0,0 +1,71 @@ +import { Suspense } from "react" +import { getUserDetails } from "@/server/actions/users" +import { AuditLogCard } from "./card-audit-log" +import { GroupAdminCard } from "./card-group-admin" +import { MessageCard } from "./card-message" +import { UserGrantCard } from "./card-user-grant" +import { UserInfoCard } from "./card-user-info" +import { NewGroupAdmin } from "./new-group-admin" + +export default async function TgUserDetails({ params }: { params: Promise<{ id: string }> }) { + const { id } = await params + const data = await getUserDetails(parseInt(id, 10)) + + return ( +
+ {data && ( + <> +
+ + {data.grant && } +
+ +
+

Admin in groups:

+ g?.group.id ?? 0) ?? []} /> +
+
+ {data.groupAdmin + .filter((m) => m !== null && m !== undefined) + .map((m) => ( + + ))} + + {data.groupAdmin.length === 0 && ( +

+ This user is not group admin in any group. +

+ )} +
+ +

Last messages (max 12):

+
+ {data.messages?.map((m) => ( + + ))} + + {data.messages?.length === 0 && ( +

+ No recent messages sent by this user +

+ )} +
+ +

Audit log:

+
+ {data.audits.map((m) => ( + + + + ))} + {data.audits.length === 0 && ( +

+ No audit log found for this user +

+ )} +
+ + )} +
+ ) +} diff --git a/src/app/dashboard/(active)/telegram/user-details/remove-role.tsx b/src/app/dashboard/(active)/telegram/users/[id]/remove-role.tsx similarity index 96% rename from src/app/dashboard/(active)/telegram/user-details/remove-role.tsx rename to src/app/dashboard/(active)/telegram/users/[id]/remove-role.tsx index fcf7f23..82ddde0 100644 --- a/src/app/dashboard/(active)/telegram/user-details/remove-role.tsx +++ b/src/app/dashboard/(active)/telegram/users/[id]/remove-role.tsx @@ -1,6 +1,7 @@ "use client" import { USER_ROLE } from "@polinetwork/backend" import { Minus } from "lucide-react" +import { useRouter } from "next/navigation" import { useState } from "react" import { toast } from "sonner" import { Spinner } from "@/components/spinner" @@ -33,11 +34,11 @@ const ARRAY_USER_ROLES = [ export function RemoveRole({ user, alreadyRoles, - onDelete, + // onDelete, }: { user: TgUser alreadyRoles: TgUserRole[] - onDelete(): void + // onDelete(): void }) { const sesh = useSession() const removerId = sesh.data?.user.telegramId @@ -51,6 +52,8 @@ export function RemoveRole({ const [pending, setPending] = useState(false) const [selectedRole, setSelectedRole] = useState(null) + const router = useRouter() + async function submit() { if (!removerId) return toast.warning("Invalid session, try reloading the page") if (!selectedRole) return toast.warning("No group selected, cannot proceed") @@ -66,7 +69,8 @@ export function RemoveRole({ else if (error === "UNAUTHORIZED_SELF_ASSIGN") toast.error("You cannot delete on yourself") else { toast.success("Role removed!") - onDelete() + router.refresh() + // onDelete() } } catch (err) { console.error(err) diff --git a/src/app/dashboard/(active)/telegram/user-list/loading.tsx b/src/app/dashboard/(active)/telegram/users/loading.tsx similarity index 100% rename from src/app/dashboard/(active)/telegram/user-list/loading.tsx rename to src/app/dashboard/(active)/telegram/users/loading.tsx diff --git a/src/app/dashboard/(active)/telegram/user-list/page.tsx b/src/app/dashboard/(active)/telegram/users/page.tsx similarity index 67% rename from src/app/dashboard/(active)/telegram/user-list/page.tsx rename to src/app/dashboard/(active)/telegram/users/page.tsx index 932e4dd..e4b2737 100644 --- a/src/app/dashboard/(active)/telegram/user-list/page.tsx +++ b/src/app/dashboard/(active)/telegram/users/page.tsx @@ -1,3 +1,6 @@ +import { Eye, ViewIcon } from "lucide-react" +import Link from "next/link" +import { Button } from "@/components/ui/button" import { trpc } from "@/server/trpc" import type { ApiOutput } from "@/server/trpc/types" @@ -11,10 +14,11 @@ export default async function TgUsers() { Count: {data.users?.length}

-
+

Telegram ID

Username

Name

+

Actions

{data?.users?.map((r) => ( @@ -26,12 +30,19 @@ export default async function TgUsers() { function UserRow({ row: r }: { row: Users[number] }) { return ( -
+

{r.id}

{r.username ? `@${r.username}` : ``}

{r.firstName ?? ""} {r.lastName ?? ""}

+
+ + + +
) } diff --git a/src/components/dashboard-sidebar/data.tsx b/src/components/dashboard-sidebar/data.tsx index b946fbe..7401a0c 100644 --- a/src/components/dashboard-sidebar/data.tsx +++ b/src/components/dashboard-sidebar/data.tsx @@ -11,7 +11,7 @@ export const DSData = { items: [ { title: "Grants", url: "/dashboard/telegram/grants", icon: }, { title: "Groups", url: "/dashboard/telegram/groups", icon: }, - { title: "Users", url: "/dashboard/telegram/user-list", icon: }, + { title: "Users", url: "/dashboard/telegram/users", icon: }, ], }, { diff --git a/src/index.css b/src/index.css index d2dd450..08c2477 100644 --- a/src/index.css +++ b/src/index.css @@ -177,4 +177,5 @@ @utility container { margin-inline: auto; + @apply pb-8; } diff --git a/src/server/actions/users.ts b/src/server/actions/users.ts index f8c14ca..2cedf54 100644 --- a/src/server/actions/users.ts +++ b/src/server/actions/users.ts @@ -16,8 +16,8 @@ export async function searchUserInfo(username: string) { return (await trpc.tg.users.getByUsername.query({ username })).user ?? null } -export async function searchUser(username: string) { - const user = await searchUserInfo(username) +export async function getUserDetails(userId: number) { + const { user } = await trpc.tg.users.get.query({ userId: userId }) if (!user) return null const { roles, groupAdmin } = await trpc.tg.permissions.getRoles.query({ userId: user.id }) From 61bfddb280eb9d2d205bc4a64c396b88ac08f3c7 Mon Sep 17 00:00:00 2001 From: Lorenzo Corallo Date: Wed, 10 Jun 2026 23:55:43 +0200 Subject: [PATCH 30/40] feat: add back user search, make container flex(col) with pb-6 --- .../(active)/azure/members/table.tsx | 4 +- .../(active)/telegram/grants/grant-list.tsx | 4 +- .../(active)/telegram/grants/loading.tsx | 6 +- .../(active)/telegram/grants/page.tsx | 4 +- .../(active)/telegram/groups/group-row.tsx | 2 +- .../(active)/telegram/groups/loading.tsx | 12 +-- .../(active)/telegram/groups/page.tsx | 12 +-- .../telegram/users/[id]/card-audit-log.tsx | 4 +- .../(active)/telegram/users/[id]/loading.tsx | 35 +++++--- .../(active)/telegram/users/[id]/page.tsx | 83 ++++++++++--------- .../(active)/telegram/users/loading.tsx | 7 +- .../(active)/telegram/users/page.tsx | 24 ++++-- .../groups => components}/search-input.tsx | 0 src/index.css | 2 +- 14 files changed, 113 insertions(+), 86 deletions(-) rename src/{app/dashboard/(active)/telegram/groups => components}/search-input.tsx (100%) diff --git a/src/app/dashboard/(active)/azure/members/table.tsx b/src/app/dashboard/(active)/azure/members/table.tsx index 18b19fe..2a5bf70 100644 --- a/src/app/dashboard/(active)/azure/members/table.tsx +++ b/src/app/dashboard/(active)/azure/members/table.tsx @@ -33,7 +33,7 @@ export function AssocTable({ members }: { members: AzureMember[] }) { const users = sociFilter ? members.filter((v) => v.isMember) : members return ( -
+

Utenti MS @polinetwork.org

+

Utenti MS @polinetwork.org

diff --git a/src/app/dashboard/(active)/telegram/grants/grant-list.tsx b/src/app/dashboard/(active)/telegram/grants/grant-list.tsx index 6f7e02c..aeb93a0 100644 --- a/src/app/dashboard/(active)/telegram/grants/grant-list.tsx +++ b/src/app/dashboard/(active)/telegram/grants/grant-list.tsx @@ -19,7 +19,7 @@ export function GrantList({ grants, isScheduled }: { grants: Grants; isScheduled ))} {grants.length === 0 && ( -
+
There are no {isScheduled ? "scheduled" : "ongoing"} grants
)} @@ -36,7 +36,7 @@ function GrantRow({ row: r }: { row: Grants[number] }) {

{format(r.grant.validSince, "yyyy/MM/dd HH:mm")}

{format(r.grant.validUntil, "yyyy/MM/dd HH:mm")}

- null} /> +
) } diff --git a/src/app/dashboard/(active)/telegram/grants/loading.tsx b/src/app/dashboard/(active)/telegram/grants/loading.tsx index 753f796..3d6aca2 100644 --- a/src/app/dashboard/(active)/telegram/grants/loading.tsx +++ b/src/app/dashboard/(active)/telegram/grants/loading.tsx @@ -5,12 +5,12 @@ import { NewGrant } from "./new-grant" export default async function Loading() { return (
-
+

Telegram Grants

- + All @@ -36,7 +36,7 @@ export default async function Loading() { function Content() { return (
-
+

Telegram ID

Username

Start Date

diff --git a/src/app/dashboard/(active)/telegram/grants/page.tsx b/src/app/dashboard/(active)/telegram/grants/page.tsx index ce41ade..0ca8cb4 100644 --- a/src/app/dashboard/(active)/telegram/grants/page.tsx +++ b/src/app/dashboard/(active)/telegram/grants/page.tsx @@ -9,12 +9,12 @@ export default async function GrantsPage() { return (
-
+

Telegram Grants

- + All diff --git a/src/app/dashboard/(active)/telegram/groups/group-row.tsx b/src/app/dashboard/(active)/telegram/groups/group-row.tsx index f5b6e9a..bfb8006 100644 --- a/src/app/dashboard/(active)/telegram/groups/group-row.tsx +++ b/src/app/dashboard/(active)/telegram/groups/group-row.tsx @@ -57,7 +57,7 @@ export function GroupRow({ row: r }: { row: TgGroup }) {
-
+

{r.hide ? ( diff --git a/src/app/dashboard/(active)/telegram/groups/loading.tsx b/src/app/dashboard/(active)/telegram/groups/loading.tsx index 77b98c0..b66a765 100644 --- a/src/app/dashboard/(active)/telegram/groups/loading.tsx +++ b/src/app/dashboard/(active)/telegram/groups/loading.tsx @@ -1,21 +1,21 @@ +import { SearchInput } from "@/components/search-input" import { Skeleton } from "@/components/ui/skeleton" -import { SearchInput } from "./search-input" export default async function Loading() { return (

-
+

Count:

-
-
-

telegram ID

+
+
+

Telegram ID

Title

Tag

Invite Link

-

Actions

+

Actions

{new Array(12).fill(0).map((_, i) => ( diff --git a/src/app/dashboard/(active)/telegram/groups/page.tsx b/src/app/dashboard/(active)/telegram/groups/page.tsx index 8987115..47ba9cd 100644 --- a/src/app/dashboard/(active)/telegram/groups/page.tsx +++ b/src/app/dashboard/(active)/telegram/groups/page.tsx @@ -1,6 +1,6 @@ +import { SearchInput } from "@/components/search-input" import { trpc } from "@/server/trpc" import { GroupRow } from "./group-row" -import { SearchInput } from "./search-input" export default async function TgGroups({ searchParams }: { searchParams: Promise<{ q?: string }> }) { const { q } = await searchParams @@ -14,16 +14,16 @@ export default async function TgGroups({ searchParams }: { searchParams: Promise return (
-

+

Count: {rows.length}

-
-
-

telegram ID

+
+
+

Telegram ID

Title

Tag

Invite Link

-

Actions

+

Actions

{sorted.map((r) => ( diff --git a/src/app/dashboard/(active)/telegram/users/[id]/card-audit-log.tsx b/src/app/dashboard/(active)/telegram/users/[id]/card-audit-log.tsx index 681216e..4c412d8 100644 --- a/src/app/dashboard/(active)/telegram/users/[id]/card-audit-log.tsx +++ b/src/app/dashboard/(active)/telegram/users/[id]/card-audit-log.tsx @@ -1,8 +1,8 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { fmtUser } from "@/lib/utils/telegram" -import type { searchUser } from "@/server/actions/users" +import type { getUserDetails } from "@/server/actions/users" -type Data = Awaited> +type Data = Awaited> type Log = NonNullable["audits"][number] export function AuditLogCard({ log: m }: { log: Log }) { diff --git a/src/app/dashboard/(active)/telegram/users/[id]/loading.tsx b/src/app/dashboard/(active)/telegram/users/[id]/loading.tsx index e9a6ed7..7b938df 100644 --- a/src/app/dashboard/(active)/telegram/users/[id]/loading.tsx +++ b/src/app/dashboard/(active)/telegram/users/[id]/loading.tsx @@ -7,21 +7,30 @@ export default async function Loading() {
-

Admin in groups:

-
- - + +
+

Admin in groups:

+
+ + +
-

Last messages (max 12):

-
- - + +
+

Last messages (max 12):

+
+ + +
-

Audit log:

-
- {new Array(9).fill(0).map((_, i) => ( - - ))} + +
+

Audit log:

+
+ {new Array(9).fill(0).map((_, i) => ( + + ))} +
) diff --git a/src/app/dashboard/(active)/telegram/users/[id]/page.tsx b/src/app/dashboard/(active)/telegram/users/[id]/page.tsx index bd82065..99d42fa 100644 --- a/src/app/dashboard/(active)/telegram/users/[id]/page.tsx +++ b/src/app/dashboard/(active)/telegram/users/[id]/page.tsx @@ -15,54 +15,61 @@ export default async function TgUserDetails({ params }: { params: Promise<{ id:
{data && ( <> -
+
{data.grant && }
-
-

Admin in groups:

- g?.group.id ?? 0) ?? []} /> -
-
- {data.groupAdmin - .filter((m) => m !== null && m !== undefined) - .map((m) => ( - - ))} +
+
+

Admin in groups:

+ g?.group.id ?? 0) ?? []} /> +
+ +
+ {data.groupAdmin + .filter((m) => m !== null && m !== undefined) + .map((m) => ( + + ))} - {data.groupAdmin.length === 0 && ( -

- This user is not group admin in any group. -

- )} + {data.groupAdmin.length === 0 && ( +

+ This user is not group admin in any group. +

+ )} +
-

Last messages (max 12):

-
- {data.messages?.map((m) => ( - - ))} +
+

Last messages (max 12):

+
+ {data.messages?.map((m) => ( + + ))} - {data.messages?.length === 0 && ( -

- No recent messages sent by this user -

- )} + {data.messages?.length === 0 && ( +

+ No recent messages sent by this user +

+ )} +
-

Audit log:

-
- {data.audits.map((m) => ( - - - - ))} - {data.audits.length === 0 && ( -

- No audit log found for this user -

- )} +
+

Audit log:

+
+ {data.audits.map((m) => ( + + + + ))} + {data.audits.length === 0 && ( +

+ No audit log found for this user +

+ )} +
)} diff --git a/src/app/dashboard/(active)/telegram/users/loading.tsx b/src/app/dashboard/(active)/telegram/users/loading.tsx index 0b79c1a..e289840 100644 --- a/src/app/dashboard/(active)/telegram/users/loading.tsx +++ b/src/app/dashboard/(active)/telegram/users/loading.tsx @@ -1,17 +1,20 @@ +import { SearchInput } from "@/components/search-input" import { Skeleton } from "@/components/ui/skeleton" export default async function Loading() { return (
+

Count:

-
-
+
+

Telegram ID

Username

Name

+

Actions

{new Array(12).fill(0).map((_, i) => ( diff --git a/src/app/dashboard/(active)/telegram/users/page.tsx b/src/app/dashboard/(active)/telegram/users/page.tsx index e4b2737..947c662 100644 --- a/src/app/dashboard/(active)/telegram/users/page.tsx +++ b/src/app/dashboard/(active)/telegram/users/page.tsx @@ -1,28 +1,36 @@ -import { Eye, ViewIcon } from "lucide-react" +import { Eye } from "lucide-react" import Link from "next/link" +import { SearchInput } from "@/components/search-input" import { Button } from "@/components/ui/button" import { trpc } from "@/server/trpc" import type { ApiOutput } from "@/server/trpc/types" type Users = NonNullable -export default async function TgUsers() { - const data = await trpc.tg.users.getAll.query() +export default async function TgUsers({ searchParams }: { searchParams: Promise<{ q?: string }> }) { + const { q } = await searchParams + const { users } = await trpc.tg.users.getAll.query() + + const data = (!q ? users : users?.filter((u) => u.username?.toLowerCase().replace("@", "").startsWith(q))) ?? [] + return (
+

- Count: {data.users?.length} + Count: {users?.length}

-
+

Telegram ID

Username

Name

Actions

- {data?.users?.map((r) => ( - - ))} + {data.length > 0 ? ( + data.map((r) => ) + ) : ( +
No users found
+ )}
) diff --git a/src/app/dashboard/(active)/telegram/groups/search-input.tsx b/src/components/search-input.tsx similarity index 100% rename from src/app/dashboard/(active)/telegram/groups/search-input.tsx rename to src/components/search-input.tsx diff --git a/src/index.css b/src/index.css index 08c2477..d09261d 100644 --- a/src/index.css +++ b/src/index.css @@ -177,5 +177,5 @@ @utility container { margin-inline: auto; - @apply pb-8; + @apply pb-8 flex flex-col items-start justify-start gap-6; } From ce7da3c00343fe6097e63a272468ad15a264e295 Mon Sep 17 00:00:00 2001 From: Lorenzo Corallo Date: Thu, 11 Jun 2026 00:23:07 +0200 Subject: [PATCH 31/40] fix: missing notFound in user-details, loading style --- src/app/dashboard/(active)/telegram/users/[id]/loading.tsx | 6 +++--- src/app/dashboard/(active)/telegram/users/[id]/page.tsx | 3 +++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/app/dashboard/(active)/telegram/users/[id]/loading.tsx b/src/app/dashboard/(active)/telegram/users/[id]/loading.tsx index 7b938df..be3d0ae 100644 --- a/src/app/dashboard/(active)/telegram/users/[id]/loading.tsx +++ b/src/app/dashboard/(active)/telegram/users/[id]/loading.tsx @@ -3,7 +3,7 @@ import { Skeleton } from "@/components/ui/skeleton" export default async function Loading() { return (
-
+
@@ -17,7 +17,7 @@ export default async function Loading() {
-

Last messages (max 12):

+

Last messages (max 12):

@@ -25,7 +25,7 @@ export default async function Loading() {
-

Audit log:

+

Audit log:

{new Array(9).fill(0).map((_, i) => ( diff --git a/src/app/dashboard/(active)/telegram/users/[id]/page.tsx b/src/app/dashboard/(active)/telegram/users/[id]/page.tsx index 99d42fa..cc7b44a 100644 --- a/src/app/dashboard/(active)/telegram/users/[id]/page.tsx +++ b/src/app/dashboard/(active)/telegram/users/[id]/page.tsx @@ -1,3 +1,4 @@ +import { notFound } from "next/navigation" import { Suspense } from "react" import { getUserDetails } from "@/server/actions/users" import { AuditLogCard } from "./card-audit-log" @@ -11,6 +12,8 @@ export default async function TgUserDetails({ params }: { params: Promise<{ id: const { id } = await params const data = await getUserDetails(parseInt(id, 10)) + if (!data) notFound() + return (
{data && ( From 8f351d4ddfd98257c5de23b46dc373a938dcee9c Mon Sep 17 00:00:00 2001 From: Lorenzo Corallo Date: Thu, 11 Jun 2026 00:34:01 +0200 Subject: [PATCH 32/40] fix: not found page --- src/app/dashboard/(active)/not-found.tsx | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 src/app/dashboard/(active)/not-found.tsx diff --git a/src/app/dashboard/(active)/not-found.tsx b/src/app/dashboard/(active)/not-found.tsx new file mode 100644 index 0000000..e962605 --- /dev/null +++ b/src/app/dashboard/(active)/not-found.tsx @@ -0,0 +1,19 @@ +import Link from "next/link" +import { Button } from "@/components/ui/button" + +export default function NotFound() { + return ( +
+
+

404

+

Not found

+

+ The resource you're looking for doesn't exist or has been moved. +

+
+ + + +
+ ) +} From df0ae868e2d0e0381ec4e74cff75d0df42367827 Mon Sep 17 00:00:00 2001 From: Lorenzo Corallo Date: Thu, 11 Jun 2026 00:34:40 +0200 Subject: [PATCH 33/40] fix: add a11y to user details button --- src/app/dashboard/(active)/telegram/users/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/dashboard/(active)/telegram/users/page.tsx b/src/app/dashboard/(active)/telegram/users/page.tsx index 947c662..5a52f6d 100644 --- a/src/app/dashboard/(active)/telegram/users/page.tsx +++ b/src/app/dashboard/(active)/telegram/users/page.tsx @@ -46,7 +46,7 @@ function UserRow({ row: r }: { row: Users[number] }) {

- From e045aeac4b1a4e228c767dc2a0e2b3fbbb23cd5d Mon Sep 17 00:00:00 2001 From: Bianca Date: Thu, 11 Jun 2026 12:27:44 +0200 Subject: [PATCH 34/40] feat: implement project management features with create, edit, and delete functionality --- .../(active)/web/projects/card-project.tsx | 235 ++++++++++++++++++ .../(active)/web/projects/category-menu.tsx | 38 +++ .../(active)/web/projects/constants.ts | 34 +++ .../dashboard/(active)/web/projects/page.tsx | 13 + .../(active)/web/projects/projects-view.tsx | 215 ++++++++++++++++ .../dashboard/(active)/web/projects/types.ts | 13 + src/components/dashboard-sidebar/data.tsx | 7 +- src/components/delete-dialog.tsx | 72 ++++++ src/components/ui/button-group.tsx | 87 +++++++ src/components/ui/button.tsx | 4 + src/components/web-header.tsx | 33 +++ src/server/actions/projects.ts | 32 +++ 12 files changed, 782 insertions(+), 1 deletion(-) create mode 100644 src/app/dashboard/(active)/web/projects/card-project.tsx create mode 100644 src/app/dashboard/(active)/web/projects/category-menu.tsx create mode 100644 src/app/dashboard/(active)/web/projects/constants.ts create mode 100644 src/app/dashboard/(active)/web/projects/page.tsx create mode 100644 src/app/dashboard/(active)/web/projects/projects-view.tsx create mode 100644 src/app/dashboard/(active)/web/projects/types.ts create mode 100644 src/components/delete-dialog.tsx create mode 100644 src/components/ui/button-group.tsx create mode 100644 src/components/web-header.tsx create mode 100644 src/server/actions/projects.ts diff --git a/src/app/dashboard/(active)/web/projects/card-project.tsx b/src/app/dashboard/(active)/web/projects/card-project.tsx new file mode 100644 index 0000000..64e7b9c --- /dev/null +++ b/src/app/dashboard/(active)/web/projects/card-project.tsx @@ -0,0 +1,235 @@ +"use client" + +import { Languages, Link, LucidePencil, Save, Upload, X } from "lucide-react" +import type { ChangeEvent } from "react" +import { useState } from "react" +import { ProjectCategoryMenu } from "@/app/dashboard/(active)/web/projects/category-menu" +import { DeleteDialog } from "@/components/delete-dialog" +import { Badge } from "@/components/ui/badge" +import { Button } from "@/components/ui/button" +import { Card, CardAction, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { Input } from "@/components/ui/input" +import { Textarea } from "@/components/ui/textarea" +import { getInitials } from "@/lib/utils" +import { cn } from "@/lib/utils/shadcn" +import type { Project, ProjectCategory } from "./types" + +export default function CardProject( + item: Project & { + initialEditActive?: boolean + isDraft?: boolean + onCancelCreate?: () => void + onDelete: () => void + onCategoryChange: (category: ProjectCategory) => void + onSave: (values: Project) => boolean | Promise + } +) { + const iconInputId = `project-icon-${item.id}` + const [editActive, setEditActive] = useState(item.initialEditActive ?? false) + const [title, setTitle] = useState(item.title) + const [logo, setLogo] = useState(item.logo) + const [descriptionIt, setDescriptionIt] = useState(item.descriptionIt) + const [descriptionEn, setDescriptionEn] = useState(item.descriptionEn) + const [link, setLinks] = useState(item.link) + const [pending, setPending] = useState(false) + const initials = getInitials(title) + + async function handleIconUpload(event: ChangeEvent) { + const file = event.target.files?.[0] + if (!file) return + setLogo(await file.text()) + } + + // If it's draft, remove the card, otherwise reset the values to the original ones + function handleCancelEdit() { + if (item.isDraft) { + item.onCancelCreate?.() + return + } + + setTitle(item.title) + setLogo(item.logo) + setDescriptionIt(item.descriptionIt) + setDescriptionEn(item.descriptionEn) + setLinks(item.link) + setEditActive(false) + } + + // TODO: forse spostare la cosa salvata per ultima nella lista? Perche poi ordinata per id finisce li + // se gli id sono crescenti. O tipo la creo direttamente ultima e non in cima? Pero poi devi scorrere per editarla + async function saveChanges() { + if (pending) return + + setPending(true) + try { + const saved = await item.onSave({ + id: item.id, + title, + logo, + descriptionIt, + descriptionEn, + link, + category: item.category, + }) + if (saved) setEditActive(false) + } finally { + setPending(false) + } + } + + function renderIcon() { + if (logo) { + return ( +