From 6ba8f6ec8bd7b0e9f024728072d8ccd2de9e0515 Mon Sep 17 00:00:00 2001 From: Amirhossein Khalili Date: Mon, 15 Jun 2026 17:32:03 +0330 Subject: [PATCH] feat(admin-dashboard): sync tabs and filters with URL --- src/views/AdminDashboard.tsx | 95 ++++++++++++++++++++++++++++++------ 1 file changed, 80 insertions(+), 15 deletions(-) diff --git a/src/views/AdminDashboard.tsx b/src/views/AdminDashboard.tsx index 6bd91eb..3658c1e 100644 --- a/src/views/AdminDashboard.tsx +++ b/src/views/AdminDashboard.tsx @@ -1,6 +1,7 @@ "use client"; import * as React from "react"; +import { usePathname, useRouter, useSearchParams } from "next/navigation"; import { useQuery } from "@tanstack/react-query"; import DateObject from "react-date-object"; import persian from "react-date-object/calendars/persian"; @@ -82,6 +83,26 @@ type SectionState = DateRangeState & { eventId?: string | null; }; +type DashboardTab = "users" | "events" | "blog"; + +const DASHBOARD_TABS: DashboardTab[] = ["users", "events", "blog"]; + +function parseTab(value: string | null): DashboardTab { + return DASHBOARD_TABS.includes(value as DashboardTab) ? (value as DashboardTab) : "users"; +} + +function readDateRange(params: Pick, prefix: "users" | "events" | "blog"): DateRangeState { + return { + from: params.get(`${prefix}_from`) || "", + to: params.get(`${prefix}_to`) || "", + }; +} + +function setParam(params: URLSearchParams, key: string, value?: string | null) { + if (value) params.set(key, value); + else params.delete(key); +} + function toApiDate(date: DateObject | null) { if (!date) return ""; const gregorian = date.toDate(); @@ -699,8 +720,13 @@ function TopPostsCard({ posts }: { posts: BlogAnalyticsSchema["top_posts"] }) { ); } -function UsersSection() { - const [filters, setFilters] = React.useState({ from: "", to: "" }); +function UsersSection({ + filters, + onFiltersChange, +}: { + filters: DateRangeState; + onFiltersChange: (filters: DateRangeState) => void; +}) { const query = useQuery({ queryKey: ["admin", "analytics", "users", filters], queryFn: () => api.getAdminUserAnalytics({ date_from: filters.from || undefined, date_to: filters.to || undefined }), @@ -709,7 +735,7 @@ function UsersSection() { return (
- setFilters({ from: "", to: "" })} /> + onFiltersChange({ from: "", to: "" })} /> {query.isLoading ? : null} {query.isError ? : null} @@ -745,8 +771,13 @@ function UsersContent({ data }: { data: UserAnalyticsSchema }) { ); } -function EventsSection() { - const [filters, setFilters] = React.useState({ from: "", to: "", eventId: null }); +function EventsSection({ + filters, + onFiltersChange, +}: { + filters: SectionState; + onFiltersChange: (filters: SectionState) => void; +}) { const query = useQuery({ queryKey: ["admin", "analytics", "events", filters], queryFn: () => @@ -757,7 +788,7 @@ function EventsSection() { }), }); - const reset = () => setFilters({ from: "", to: "", eventId: null }); + const reset = () => onFiltersChange({ from: "", to: "", eventId: null }); return (
@@ -766,7 +797,7 @@ function EventsSection() {
setFilters((current) => ({ ...current, ...next }))} + onChange={(next) => onFiltersChange({ ...filters, ...next })} onReset={reset} />
@@ -774,7 +805,7 @@ function EventsSection() { setFilters((current) => ({ ...current, eventId }))} + onChange={(eventId) => onFiltersChange({ ...filters, eventId })} loadOptions={async ({ search, limit, offset }) => { const data = await api.getAdminDashboardEventOptions({ search, limit, offset }); return { @@ -838,8 +869,13 @@ function EventsContent({ data }: { data: EventAnalyticsSchema }) { ); } -function BlogSection() { - const [filters, setFilters] = React.useState({ from: "", to: "" }); +function BlogSection({ + filters, + onFiltersChange, +}: { + filters: DateRangeState; + onFiltersChange: (filters: DateRangeState) => void; +}) { const query = useQuery({ queryKey: ["admin", "analytics", "blog", filters], queryFn: () => api.getAdminBlogAnalytics({ date_from: filters.from || undefined, date_to: filters.to || undefined }), @@ -848,7 +884,7 @@ function BlogSection() { return (
- setFilters({ from: "", to: "" })} /> + onFiltersChange({ from: "", to: "" })} /> {query.isLoading ? : null} {query.isError ? : null} @@ -882,6 +918,35 @@ function BlogContent({ data }: { data: BlogAnalyticsSchema }) { } export default function AdminDashboard() { + const router = useRouter(); + const pathname = usePathname(); + const searchParams = useSearchParams(); + const [activeTab, setActiveTab] = React.useState(() => parseTab(searchParams.get("tab"))); + const [usersFilters, setUsersFilters] = React.useState(() => readDateRange(searchParams, "users")); + const [eventsFilters, setEventsFilters] = React.useState(() => ({ + ...readDateRange(searchParams, "events"), + eventId: searchParams.get("event_id"), + })); + const [blogFilters, setBlogFilters] = React.useState(() => readDateRange(searchParams, "blog")); + + React.useEffect(() => { + const params = new URLSearchParams(); + params.set("tab", activeTab); + setParam(params, "users_from", usersFilters.from); + setParam(params, "users_to", usersFilters.to); + setParam(params, "events_from", eventsFilters.from); + setParam(params, "events_to", eventsFilters.to); + setParam(params, "event_id", eventsFilters.eventId); + setParam(params, "blog_from", blogFilters.from); + setParam(params, "blog_to", blogFilters.to); + + const nextSearch = params.toString(); + const currentSearch = searchParams.toString(); + if (nextSearch !== currentSearch) { + router.replace(`${pathname}?${nextSearch}`, { scroll: false }); + } + }, [activeTab, blogFilters, eventsFilters, pathname, router, searchParams, usersFilters]); + return (
@@ -893,7 +958,7 @@ export default function AdminDashboard() {
- + setActiveTab(parseTab(value))} className="space-y-6"> @@ -909,13 +974,13 @@ export default function AdminDashboard() { - + - + - +