From 205d59c6c1fea86c17df6cfa28e8babfdd90a280 Mon Sep 17 00:00:00 2001 From: Tudor Amariei Date: Fri, 29 Aug 2025 15:22:07 +0300 Subject: [PATCH 01/11] refactor: update the sidebar and remove the topbar --- backend/users/middleware.py | 2 +- backend/users/models.py | 15 ++ backend/users/views/team/listing.py | 24 +-- frontend/package-lock.json | 4 +- frontend/package.json | 4 +- frontend/src/components/hooks/use-sidebar.ts | 15 ++ frontend/src/components/paul/app-sidebar.tsx | 172 ++++++++---------- frontend/src/components/paul/app-topbar.tsx | 157 ---------------- ...{expandable-nav.tsx => nav-expandable.tsx} | 2 +- .../src/components/paul/nav-icon-link.tsx | 36 ++++ frontend/src/components/paul/nav-logo.tsx | 20 ++ frontend/src/components/paul/nav-user.tsx | 130 +++++++++++++ frontend/src/components/paul/single-nav.tsx | 41 ----- .../components/types/sidebarContextProps.tsx | 9 + frontend/src/components/ui/sidebar.tsx | 51 +++--- frontend/src/hooks/use-team-add-user-form.ts | 4 +- frontend/src/layouts/base-layout.tsx | 6 +- frontend/src/pages/users/team/columns.tsx | 4 +- .../users/team/forms/team-add-user-form.tsx | 8 +- .../users/team/team-page-props-struct.ts | 21 +-- frontend/src/types/common-props.ts | 10 +- frontend/src/types/team-add-user-form-data.ts | 4 +- .../src/types/team-remove-user-form-data.ts | 4 +- frontend/src/types/user.ts | 37 ++-- 24 files changed, 378 insertions(+), 402 deletions(-) create mode 100644 frontend/src/components/hooks/use-sidebar.ts delete mode 100644 frontend/src/components/paul/app-topbar.tsx rename frontend/src/components/paul/{expandable-nav.tsx => nav-expandable.tsx} (98%) create mode 100644 frontend/src/components/paul/nav-icon-link.tsx create mode 100644 frontend/src/components/paul/nav-logo.tsx create mode 100644 frontend/src/components/paul/nav-user.tsx delete mode 100644 frontend/src/components/paul/single-nav.tsx create mode 100644 frontend/src/components/types/sidebarContextProps.tsx diff --git a/backend/users/middleware.py b/backend/users/middleware.py index 6ae11df..58bbd46 100644 --- a/backend/users/middleware.py +++ b/backend/users/middleware.py @@ -40,7 +40,7 @@ def middleware(request: HttpRequest) -> HttpResponse: language=_extract_language(request), # Lazily computed properties: isAuthenticated=lambda: request.user.is_authenticated, - user=lambda: User.to_dict(request.user) if request.user.is_authenticated else None, + user=lambda: User.to_json(request.user) if request.user.is_authenticated else None, ) return get_response(request) diff --git a/backend/users/models.py b/backend/users/models.py index 0bcd538..1450f3b 100644 --- a/backend/users/models.py +++ b/backend/users/models.py @@ -10,6 +10,7 @@ from django.forms.models import model_to_dict from django.utils.translation import gettext as _ +from tools.display import format_dates from tools.utils.choices import CommonLabelValueChoices @@ -119,6 +120,20 @@ class Meta: def to_dict(self): return model_to_dict(self, exclude=("password", "is_superuser", "is_staff", "groups", "user_permissions")) + def to_json(self): + return { + "id": self.id, + "firstName": self.first_name, + "lastName": self.last_name, + "fullName": self.name, + "email": self.email, + "roleLabel": RoleChoices(self.main_role).label, + "roleValue": RoleChoices(self.main_role).value, + "addedSince": format_dates.short_date(self.date_joined), + "lastActivity": format_dates.short_datetime(self.last_login), + "ngohubId": self.ngohub_id, + } + def __str__(self): return self.email diff --git a/backend/users/views/team/listing.py b/backend/users/views/team/listing.py index 283486a..f817ffb 100644 --- a/backend/users/views/team/listing.py +++ b/backend/users/views/team/listing.py @@ -13,7 +13,6 @@ from django.views.decorators.cache import cache_control from tools.data_models.filtering import FilterField, FilterItem -from tools.display import format_dates as display_dates from tools.display.url_build import build_ngohub_url from tools.utils.filtering import build_filters_display, build_filters_mapping, filter_qs from tools.utils.pagination import paginate_queryset @@ -27,21 +26,14 @@ def _serialize_users(users_page: Page[User], user_id: int) -> List[Dict]: - return [ - { - "id": user.id, - "firstName": user.first_name, - "lastName": user.last_name, - "email": user.email, - "isCurrentUser": user.id == user_id, - "roleLabel": RoleChoices(user.main_role).label, - "roleValue": RoleChoices(user.main_role).value, - "addedSince": display_dates.short_date(user.date_joined), - "lastActivity": display_dates.short_datetime(user.last_login), - "ngohubId": user.ngohub_id, - } - for user in users_page - ] + users = [] + for user in users_page: + new_user = user.to_json() + new_user["isCurrentUser"] = bool(user.id == user_id) + + users.append(new_user) + + return users def _get_filter_options() -> List[FilterField]: diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 60f19a6..426db2b 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -15,7 +15,7 @@ "@radix-ui/react-avatar": "^1.1.10", "@radix-ui/react-checkbox": "^1.3.2", "@radix-ui/react-collapsible": "^1.1.11", - "@radix-ui/react-dialog": "^1.1.14", + "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.15", "@radix-ui/react-label": "^2.1.7", "@radix-ui/react-popover": "^1.1.14", @@ -24,7 +24,7 @@ "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-switch": "^1.2.5", "@radix-ui/react-tabs": "^1.1.12", - "@radix-ui/react-tooltip": "^1.2.7", + "@radix-ui/react-tooltip": "^1.2.8", "@tailwindcss/vite": "^4.1.10", "@tanstack/react-table": "^8.21.3", "class-variance-authority": "^0.7.1", diff --git a/frontend/package.json b/frontend/package.json index 195835f..79a1f0a 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -24,7 +24,7 @@ "@radix-ui/react-avatar": "^1.1.10", "@radix-ui/react-checkbox": "^1.3.2", "@radix-ui/react-collapsible": "^1.1.11", - "@radix-ui/react-dialog": "^1.1.14", + "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.15", "@radix-ui/react-label": "^2.1.7", "@radix-ui/react-popover": "^1.1.14", @@ -33,7 +33,7 @@ "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-switch": "^1.2.5", "@radix-ui/react-tabs": "^1.1.12", - "@radix-ui/react-tooltip": "^1.2.7", + "@radix-ui/react-tooltip": "^1.2.8", "@tailwindcss/vite": "^4.1.10", "@tanstack/react-table": "^8.21.3", "class-variance-authority": "^0.7.1", diff --git a/frontend/src/components/hooks/use-sidebar.ts b/frontend/src/components/hooks/use-sidebar.ts new file mode 100644 index 0000000..bea4718 --- /dev/null +++ b/frontend/src/components/hooks/use-sidebar.ts @@ -0,0 +1,15 @@ +import type { SidebarContextProps } from "@/components/types/sidebarContextProps"; +import * as React from "react"; + +export 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; +} + +export { useSidebar }; diff --git a/frontend/src/components/paul/app-sidebar.tsx b/frontend/src/components/paul/app-sidebar.tsx index 1d1b414..c5966c4 100644 --- a/frontend/src/components/paul/app-sidebar.tsx +++ b/frontend/src/components/paul/app-sidebar.tsx @@ -1,116 +1,96 @@ -import { ExpandableNav } from "@/components/paul/expandable-nav"; -import { SingleNav } from "@/components/paul/single-nav"; +import { NavExpandable } from "@/components/paul/nav-expandable"; +import { NavLogo } from "@/components/paul/nav-logo"; +import { NavUser } from "@/components/paul/nav-user"; import { Sidebar, SidebarContent, SidebarFooter, SidebarHeader, SidebarRail } from "@/components/ui/sidebar"; -import { apiGetUrls } from "@/constants/api-urls"; +import type { UserProps } from "@/types/user"; import { CircleStackIcon as Database, DocumentChartBarIcon as FileChartLine, - HomeIcon as House, - InformationCircleIcon as Info, SparklesIcon as Zap, TableCellsIcon as Grid2X2Plus, - UsersIcon as UsersRound, } from "@heroicons/react/24/outline"; import * as React from "react"; import { useTranslation } from "react-i18next"; -export function AppSidebar({ ...props }: React.ComponentProps) { +type AppSidebarProps = { + user: UserProps | null | undefined; +} & React.ComponentProps; + +export function AppSidebar({ user, ...props }: AppSidebarProps) { const { t } = useTranslation(); - const data = { - navHome: [ - { - title: t("navigation.home"), - url: "/", - icon: House, - }, - ], - navMain: [ - { - title: t("navigation.datasets"), - url: "#", - icon: Database, - items: [ - { - title: t("navigation.test1"), - url: "#", - }, - { - title: t("navigation.test2"), - url: "#", - }, - ], - }, - { - title: t("navigation.processedData"), - url: "#", - icon: FileChartLine, - items: [ - { - title: t("navigation.test1"), - url: "#", - }, - { - title: t("navigation.test2"), - url: "#", - }, - ], - }, - { - title: t("navigation.actions"), - url: "#", - icon: Zap, - items: [ - { - title: t("navigation.test1"), - url: "#", - }, - { - title: t("navigation.test2"), - url: "#", - }, - ], - }, - { - title: t("navigation.apps"), - url: "#", - icon: Grid2X2Plus, - items: [ - { - title: t("navigation.test1"), - url: "#", - }, - { - title: t("navigation.test2"), - url: "#", - }, - ], - }, - ], - navMore: [ - { - title: t("navigation.team"), - url: apiGetUrls.teamIndex, - icon: UsersRound, - }, - { - title: t("navigation.help"), - url: "#", - icon: Info, - }, - ], - }; + const navMain = [ + { + title: t("navigation.datasets"), + url: "#", + icon: Database, + items: [ + { + title: t("navigation.test1"), + url: "#", + }, + { + title: t("navigation.test2"), + url: "#", + }, + ], + }, + { + title: t("navigation.processedData"), + url: "#", + icon: FileChartLine, + items: [ + { + title: t("navigation.test1"), + url: "#", + }, + { + title: t("navigation.test2"), + url: "#", + }, + ], + }, + { + title: t("navigation.actions"), + url: "#", + icon: Zap, + items: [ + { + title: t("navigation.test1"), + url: "#", + }, + { + title: t("navigation.test2"), + url: "#", + }, + ], + }, + { + title: t("navigation.apps"), + url: "#", + icon: Grid2X2Plus, + items: [ + { + title: t("navigation.test1"), + url: "#", + }, + { + title: t("navigation.test2"), + url: "#", + }, + ], + }, + ]; return ( - + + + - - + - - - + {user && } ); diff --git a/frontend/src/components/paul/app-topbar.tsx b/frontend/src/components/paul/app-topbar.tsx deleted file mode 100644 index 6356057..0000000 --- a/frontend/src/components/paul/app-topbar.tsx +++ /dev/null @@ -1,157 +0,0 @@ -import { - Disclosure, - DisclosureButton, - DisclosurePanel, - Menu, - MenuButton, - MenuItem, - MenuItems, -} from "@headlessui/react"; -import { MagnifyingGlassIcon } from "@heroicons/react/20/solid"; -import { Bars3Icon, BellIcon, XMarkIcon } from "@heroicons/react/24/outline"; -import { useTranslation } from "react-i18next"; -import { apiPostUrls } from "@/constants/api-urls"; -import { Link } from "@inertiajs/react"; - -import logo from "@/assets/paul-logo.svg"; - -export function AppTopbar() { - const { t } = useTranslation(); - - return ( - -
-
-
-
- PAUL -
-
-
-
- -
-
-
- {/* Mobile menu button */} - - - {t("topbar.openMainMenu")} - -
-
- - - {/* Profile dropdown */} - -
- - - {t("topbar.openUserMenu")} - - -
- - - - {t("topbar.yourProfile")} - - - - - {t("topbar.settings")} - - - - - {t("topbar.signOut")} - - - -
-
-
-
- - -
-
-
- -
-
-
user name
-
user@example.com
-
- -
-
- - {t("topbar.yourProfile")} - - - {t("topbar.settings")} - - - {t("topbar.signOut")} - -
-
-
-
- ); -} diff --git a/frontend/src/components/paul/expandable-nav.tsx b/frontend/src/components/paul/nav-expandable.tsx similarity index 98% rename from frontend/src/components/paul/expandable-nav.tsx rename to frontend/src/components/paul/nav-expandable.tsx index 199c45d..804e147 100644 --- a/frontend/src/components/paul/expandable-nav.tsx +++ b/frontend/src/components/paul/nav-expandable.tsx @@ -14,7 +14,7 @@ import { ChevronRightIcon as ChevronRight } from "@heroicons/react/24/outline"; import { Link } from "@inertiajs/react"; import { type LucideIcon } from "lucide-react"; -export function ExpandableNav({ +export function NavExpandable({ items, }: { items: { diff --git a/frontend/src/components/paul/nav-icon-link.tsx b/frontend/src/components/paul/nav-icon-link.tsx new file mode 100644 index 0000000..a3ed60e --- /dev/null +++ b/frontend/src/components/paul/nav-icon-link.tsx @@ -0,0 +1,36 @@ +import { Link } from "@inertiajs/react"; +import { isValidElement } from "react"; +import type { ComponentType, ReactElement, ReactNode } from "react"; +import * as React from "react"; + +export type HttpMethod = "get" | "post" | "put" | "patch" | "delete"; +export type IconComponent = ComponentType>; + +export interface IconLinkProps { + href: string; + method?: HttpMethod; + icon: IconComponent | ReactElement; + label: ReactNode; + className?: string; + iconClassName?: string; +} + +export function NavIconLink({ + href, + method, + icon, + label, + className = "flex items-center gap-2 text-left text-sm", + iconClassName = "size-4", + ...rest +}: IconLinkProps & Omit, "href" | "method" | "children" | "className">) { + const Icon = icon as IconComponent; + const iconNode = isValidElement(icon) ? icon :