feat(admin-dashboard): sync tabs and filters with URL
This commit is contained in:
@@ -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<URLSearchParams, "get">, 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<DateRangeState>({ 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 (
|
||||
<div className="space-y-6" dir="rtl">
|
||||
<FilterCard title="فیلتر کاربران" description="این فیلتر فقط روی کاربران و تاریخ عضویت آنها اعمال میشود.">
|
||||
<DateRangeFilter value={filters} onChange={setFilters} onReset={() => setFilters({ from: "", to: "" })} />
|
||||
<DateRangeFilter value={filters} onChange={onFiltersChange} onReset={() => onFiltersChange({ from: "", to: "" })} />
|
||||
</FilterCard>
|
||||
{query.isLoading ? <SectionLoading /> : null}
|
||||
{query.isError ? <SectionError error={query.error} /> : null}
|
||||
@@ -745,8 +771,13 @@ function UsersContent({ data }: { data: UserAnalyticsSchema }) {
|
||||
);
|
||||
}
|
||||
|
||||
function EventsSection() {
|
||||
const [filters, setFilters] = React.useState<SectionState>({ 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 (
|
||||
<div className="space-y-6">
|
||||
@@ -766,7 +797,7 @@ function EventsSection() {
|
||||
<div>
|
||||
<DateRangeFilter
|
||||
value={filters}
|
||||
onChange={(next) => setFilters((current) => ({ ...current, ...next }))}
|
||||
onChange={(next) => onFiltersChange({ ...filters, ...next })}
|
||||
onReset={reset}
|
||||
/>
|
||||
</div>
|
||||
@@ -774,7 +805,7 @@ function EventsSection() {
|
||||
<Label>رویداد</Label>
|
||||
<AsyncSearchableCombobox
|
||||
value={filters.eventId ?? null}
|
||||
onChange={(eventId) => 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<DateRangeState>({ 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 (
|
||||
<div className="space-y-6">
|
||||
<FilterCard title="فیلتر بلاگ" description="این فیلتر فقط روی نوشتهها و تعاملات بلاگ اعمال میشود و به رویدادها وابسته نیست.">
|
||||
<DateRangeFilter value={filters} onChange={setFilters} onReset={() => setFilters({ from: "", to: "" })} />
|
||||
<DateRangeFilter value={filters} onChange={onFiltersChange} onReset={() => onFiltersChange({ from: "", to: "" })} />
|
||||
</FilterCard>
|
||||
{query.isLoading ? <SectionLoading /> : null}
|
||||
{query.isError ? <SectionError error={query.error} /> : 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<DashboardTab>(() => parseTab(searchParams.get("tab")));
|
||||
const [usersFilters, setUsersFilters] = React.useState<DateRangeState>(() => readDateRange(searchParams, "users"));
|
||||
const [eventsFilters, setEventsFilters] = React.useState<SectionState>(() => ({
|
||||
...readDateRange(searchParams, "events"),
|
||||
eventId: searchParams.get("event_id"),
|
||||
}));
|
||||
const [blogFilters, setBlogFilters] = React.useState<DateRangeState>(() => 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 (
|
||||
<div className="space-y-6" dir="rtl">
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
@@ -893,7 +958,7 @@ export default function AdminDashboard() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Tabs dir="rtl" defaultValue="users" className="space-y-6">
|
||||
<Tabs dir="rtl" value={activeTab} onValueChange={(value) => setActiveTab(parseTab(value))} className="space-y-6">
|
||||
<TabsList className="grid h-auto w-full grid-cols-3 rounded-2xl p-1 sm:w-fit">
|
||||
<TabsTrigger value="users" className="gap-2 rounded-xl py-2">
|
||||
<UsersRound className="h-4 w-4" />
|
||||
@@ -909,13 +974,13 @@ export default function AdminDashboard() {
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="users">
|
||||
<UsersSection />
|
||||
<UsersSection filters={usersFilters} onFiltersChange={setUsersFilters} />
|
||||
</TabsContent>
|
||||
<TabsContent value="events">
|
||||
<EventsSection />
|
||||
<EventsSection filters={eventsFilters} onFiltersChange={setEventsFilters} />
|
||||
</TabsContent>
|
||||
<TabsContent value="blog">
|
||||
<BlogSection />
|
||||
<BlogSection filters={blogFilters} onFiltersChange={setBlogFilters} />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user