From 712a1a29375cff8119b1866d484f860be9c7b7ff Mon Sep 17 00:00:00 2001 From: Daniel Schulteis Date: Sat, 31 Jan 2026 01:40:28 +0100 Subject: [PATCH] feat: implement dynamic mobile header with centered page title and adaptive layout --- app/(backend)/layout.tsx | 24 +- .../users/components/UserMobileCard/index.tsx | 62 +++++ app/(backend)/users/page.tsx | 211 ++++++++++------- components/Header/index.tsx | 29 ++- components/ui/calendar.tsx | 220 ++++++++++++++++++ package-lock.json | 45 ++++ package.json | 2 + 7 files changed, 491 insertions(+), 102 deletions(-) create mode 100644 app/(backend)/users/components/UserMobileCard/index.tsx create mode 100644 components/ui/calendar.tsx diff --git a/app/(backend)/layout.tsx b/app/(backend)/layout.tsx index a1d9dc7..798d22a 100644 --- a/app/(backend)/layout.tsx +++ b/app/(backend)/layout.tsx @@ -3,6 +3,7 @@ import Header from "@/components/Header/index"; import Sidebar from "@/components/Sidebar"; import React, { useState } from "react"; +import { usePathname } from "next/navigation"; interface LayoutProps { children: React.ReactNode; @@ -10,6 +11,21 @@ interface LayoutProps { const Layout: React.FC = ({ children }) => { const [sidebarOpen, setSidebarOpen] = useState(false); + const pathname = usePathname(); + + const getPageTitle = (path: string) => { + if (path === "/") return "Overview"; + + const titles: Record = { + "/users": "Users", + "/settings": "Settings", + "/profile": "Profile", + "/songs": "Songs Database", + "/events": "Worship & Events", + }; + + return titles[path] || "Dashboard"; + }; return (
@@ -26,7 +42,11 @@ const Layout: React.FC = ({ children }) => {
-
setSidebarOpen(!sidebarOpen)} /> +
setSidebarOpen(!sidebarOpen)} + pageTitle={getPageTitle(pathname)} + /> +
{children}
@@ -35,4 +55,4 @@ const Layout: React.FC = ({ children }) => { ); }; -export default Layout; +export default Layout; \ No newline at end of file diff --git a/app/(backend)/users/components/UserMobileCard/index.tsx b/app/(backend)/users/components/UserMobileCard/index.tsx new file mode 100644 index 0000000..d608048 --- /dev/null +++ b/app/(backend)/users/components/UserMobileCard/index.tsx @@ -0,0 +1,62 @@ +'use client'; + +import { MoreVertical, Mail, Shield, Calendar } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; + +interface UserMobileCardProps { + name: string; + email: string; + role: string; + joinedAt?: string; + status: 'active' | 'inactive'; +} + +export const UserMobileCard: React.FC = ({ name, email, role, joinedAt, status }) => { + + return ( +
+ {/* Header section */} +
+
+ {/* Avatar will be here */} +
+ + {name.split(" ").map(n => n[0]).join("")} + +
+
+ {name} + + + {email} + +
+
+ + + {status} + +
+ + {/* Footer section: role and options */} +
+
+ + Role: {role} +
+
+ + Joined: {joinedAt} +
+ +
+
+ ) + +} \ No newline at end of file diff --git a/app/(backend)/users/page.tsx b/app/(backend)/users/page.tsx index 8280907..700d0ea 100644 --- a/app/(backend)/users/page.tsx +++ b/app/(backend)/users/page.tsx @@ -3,103 +3,134 @@ import { useState } from "react"; import { Button } from "@/components/ui/button"; import { Search, Plus, MoreVertical, Filter } from "lucide-react"; +import { UserMobileCard } from "./components/UserMobileCard"; + +const tempUsers = [ + { id: 1, name: "John Doe", email: "john@example.com", role: "Admin", joinedAt: "2023-01-01", status: "active" }, + { id: 2, name: "Alice Smith", email: "alice@test.com", role: "Editor", joinedAt: "2023-02-15", status: "active" }, + { id: 3, name: "Bob Johnson", email: "bob@community.com", role: "Member", joinedAt: "2023-03-10", status: "inactive" }, + { id: 4, name: "Charlie Brown", email: "charlie@community.com", role: "Member", joinedAt: "2023-04-20", status: "active" }, +]; export default function UsersPage() { - + const [open, setOpen] = useState(false) - + return (
- {/* Header */} -
-
-

Users

-

Manage your community members and their permissions.

-
- -
+
- {/* Filters & Search */} -
-
- - -
- -
+ {/* Header: Title + Button for Desktop only */} +
+
+

+ Users +

+

+ Manage your community members, roles, and permissions here. +

+
- {/* Users List / Table */} -
- - - - - - - - - - - - {/* Example Row */} - - - - - - - - - - - - - - - -
UserRoleStatusJoined
-
-
JD
-
-
John Doe
-
john@example.com
+ {/* Кнопка: на мобилке круглая, на десктопе — солидная и яркая */} + +
+ + {/* Search + Filter */} +
+
+ + +
+
-
-
- Administrator - - Active - Jan 29, 2026 - -
-
-
JD
-
-
Ivan Ivanowitsch
-
ivan@example.com
-
-
-
- User - - not active - May 20, 2025 - -
+
+ {/* Кнопка: на мобилке круглая, на десктопе — солидная и яркая */} + +
+
+ + {/* --- For Mobile Phones --- */} +
+ {tempUsers.map((user) => ( + + ))} +
+ + {/* Users List / Table */} +
+ + + + + + + + + + + + {/* Example Row */} + {tempUsers.map((user) => ( + + + + + + + + ))} + +
UserRoleStatusJoined
+
+
{user.name.charAt(0)}
+
+
{user.name}
+
{user.email}
+
+
+
+ {user.role} + + + {user.status} + + {user.joinedAt} + +
+
-
- ); + ); } \ No newline at end of file diff --git a/components/Header/index.tsx b/components/Header/index.tsx index 9441938..f077521 100644 --- a/components/Header/index.tsx +++ b/components/Header/index.tsx @@ -3,15 +3,17 @@ import React from "react"; interface HeaderProps { onMenuClick: () => void; + pageTitle?: string; } -const Header: React.FC = ({ onMenuClick }) => { +const Header: React.FC = ({ onMenuClick, pageTitle }) => { return (
-
+ {/* 1. Left: Menu button (only on mobile) */} +
+ {/* 2. Center: Title (only on mobile) */} +
+ + {pageTitle} + +
+ + {/* 3. Desktop: Search */}
= ({ onMenuClick }) => { />
-
+ {/* 4. RIGHT SIDE: Notifications and Profile */} +
-
-

- Community Admin -

+
+

Community Admin

Super Administrator

-
+
@@ -55,4 +64,4 @@ const Header: React.FC = ({ onMenuClick }) => { ); }; -export default Header; +export default Header; \ No newline at end of file diff --git a/components/ui/calendar.tsx b/components/ui/calendar.tsx new file mode 100644 index 0000000..9fb18ca --- /dev/null +++ b/components/ui/calendar.tsx @@ -0,0 +1,220 @@ +"use client" + +import * as React from "react" +import { + ChevronDownIcon, + ChevronLeftIcon, + ChevronRightIcon, +} from "lucide-react" +import { + DayPicker, + getDefaultClassNames, + type DayButton, +} from "react-day-picker" + +import { cn } from "@/lib/utils" +import { Button, buttonVariants } from "@/components/ui/button" + +function Calendar({ + className, + classNames, + showOutsideDays = true, + captionLayout = "label", + buttonVariant = "ghost", + formatters, + components, + ...props +}: React.ComponentProps & { + buttonVariant?: React.ComponentProps["variant"] +}) { + const defaultClassNames = getDefaultClassNames() + + return ( + svg]:rotate-180`, + String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`, + className + )} + captionLayout={captionLayout} + formatters={{ + formatMonthDropdown: (date) => + date.toLocaleString("default", { month: "short" }), + ...formatters, + }} + classNames={{ + root: cn("w-fit", defaultClassNames.root), + months: cn( + "flex gap-4 flex-col md:flex-row relative", + defaultClassNames.months + ), + month: cn("flex flex-col w-full gap-4", defaultClassNames.month), + nav: cn( + "flex items-center gap-1 w-full absolute top-0 inset-x-0 justify-between", + defaultClassNames.nav + ), + button_previous: cn( + buttonVariants({ variant: buttonVariant }), + "size-(--cell-size) aria-disabled:opacity-50 p-0 select-none", + defaultClassNames.button_previous + ), + button_next: cn( + buttonVariants({ variant: buttonVariant }), + "size-(--cell-size) aria-disabled:opacity-50 p-0 select-none", + defaultClassNames.button_next + ), + month_caption: cn( + "flex items-center justify-center h-(--cell-size) w-full px-(--cell-size)", + defaultClassNames.month_caption + ), + dropdowns: cn( + "w-full flex items-center text-sm font-medium justify-center h-(--cell-size) gap-1.5", + defaultClassNames.dropdowns + ), + dropdown_root: cn( + "relative has-focus:border-ring border border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] rounded-md", + defaultClassNames.dropdown_root + ), + dropdown: cn( + "absolute bg-popover inset-0 opacity-0", + defaultClassNames.dropdown + ), + caption_label: cn( + "select-none font-medium", + captionLayout === "label" + ? "text-sm" + : "rounded-md pl-2 pr-1 flex items-center gap-1 text-sm h-8 [&>svg]:text-muted-foreground [&>svg]:size-3.5", + defaultClassNames.caption_label + ), + table: "w-full border-collapse", + weekdays: cn("flex", defaultClassNames.weekdays), + weekday: cn( + "text-muted-foreground rounded-md flex-1 font-normal text-[0.8rem] select-none", + defaultClassNames.weekday + ), + week: cn("flex w-full mt-2", defaultClassNames.week), + week_number_header: cn( + "select-none w-(--cell-size)", + defaultClassNames.week_number_header + ), + week_number: cn( + "text-[0.8rem] select-none text-muted-foreground", + defaultClassNames.week_number + ), + day: cn( + "relative w-full h-full p-0 text-center [&:last-child[data-selected=true]_button]:rounded-r-md group/day aspect-square select-none", + props.showWeekNumber + ? "[&:nth-child(2)[data-selected=true]_button]:rounded-l-md" + : "[&:first-child[data-selected=true]_button]:rounded-l-md", + defaultClassNames.day + ), + range_start: cn( + "rounded-l-md bg-accent", + defaultClassNames.range_start + ), + range_middle: cn("rounded-none", defaultClassNames.range_middle), + range_end: cn("rounded-r-md bg-accent", defaultClassNames.range_end), + today: cn( + "bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none", + defaultClassNames.today + ), + outside: cn( + "text-muted-foreground aria-selected:text-muted-foreground", + defaultClassNames.outside + ), + disabled: cn( + "text-muted-foreground opacity-50", + defaultClassNames.disabled + ), + hidden: cn("invisible", defaultClassNames.hidden), + ...classNames, + }} + components={{ + Root: ({ className, rootRef, ...props }) => { + return ( +
+ ) + }, + Chevron: ({ className, orientation, ...props }) => { + if (orientation === "left") { + return ( + + ) + } + + if (orientation === "right") { + return ( + + ) + } + + return ( + + ) + }, + DayButton: CalendarDayButton, + WeekNumber: ({ children, ...props }) => { + return ( + +
+ {children} +
+ + ) + }, + ...components, + }} + {...props} + /> + ) +} + +function CalendarDayButton({ + className, + day, + modifiers, + ...props +}: React.ComponentProps) { + const defaultClassNames = getDefaultClassNames() + + const ref = React.useRef(null) + React.useEffect(() => { + if (modifiers.focused) ref.current?.focus() + }, [modifiers.focused]) + + return ( +