feat(admin-dashboard): sync tabs and filters with URL
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import DateObject from "react-date-object";
|
import DateObject from "react-date-object";
|
||||||
import persian from "react-date-object/calendars/persian";
|
import persian from "react-date-object/calendars/persian";
|
||||||
@@ -82,6 +83,26 @@ type SectionState = DateRangeState & {
|
|||||||
eventId?: string | null;
|
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) {
|
function toApiDate(date: DateObject | null) {
|
||||||
if (!date) return "";
|
if (!date) return "";
|
||||||
const gregorian = date.toDate();
|
const gregorian = date.toDate();
|
||||||
@@ -699,8 +720,13 @@ function TopPostsCard({ posts }: { posts: BlogAnalyticsSchema["top_posts"] }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function UsersSection() {
|
function UsersSection({
|
||||||
const [filters, setFilters] = React.useState<DateRangeState>({ from: "", to: "" });
|
filters,
|
||||||
|
onFiltersChange,
|
||||||
|
}: {
|
||||||
|
filters: DateRangeState;
|
||||||
|
onFiltersChange: (filters: DateRangeState) => void;
|
||||||
|
}) {
|
||||||
const query = useQuery({
|
const query = useQuery({
|
||||||
queryKey: ["admin", "analytics", "users", filters],
|
queryKey: ["admin", "analytics", "users", filters],
|
||||||
queryFn: () => api.getAdminUserAnalytics({ date_from: filters.from || undefined, date_to: filters.to || undefined }),
|
queryFn: () => api.getAdminUserAnalytics({ date_from: filters.from || undefined, date_to: filters.to || undefined }),
|
||||||
@@ -709,7 +735,7 @@ function UsersSection() {
|
|||||||
return (
|
return (
|
||||||
<div className="space-y-6" dir="rtl">
|
<div className="space-y-6" dir="rtl">
|
||||||
<FilterCard title="فیلتر کاربران" description="این فیلتر فقط روی کاربران و تاریخ عضویت آنها اعمال میشود.">
|
<FilterCard title="فیلتر کاربران" description="این فیلتر فقط روی کاربران و تاریخ عضویت آنها اعمال میشود.">
|
||||||
<DateRangeFilter value={filters} onChange={setFilters} onReset={() => setFilters({ from: "", to: "" })} />
|
<DateRangeFilter value={filters} onChange={onFiltersChange} onReset={() => onFiltersChange({ from: "", to: "" })} />
|
||||||
</FilterCard>
|
</FilterCard>
|
||||||
{query.isLoading ? <SectionLoading /> : null}
|
{query.isLoading ? <SectionLoading /> : null}
|
||||||
{query.isError ? <SectionError error={query.error} /> : null}
|
{query.isError ? <SectionError error={query.error} /> : null}
|
||||||
@@ -745,8 +771,13 @@ function UsersContent({ data }: { data: UserAnalyticsSchema }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function EventsSection() {
|
function EventsSection({
|
||||||
const [filters, setFilters] = React.useState<SectionState>({ from: "", to: "", eventId: null });
|
filters,
|
||||||
|
onFiltersChange,
|
||||||
|
}: {
|
||||||
|
filters: SectionState;
|
||||||
|
onFiltersChange: (filters: SectionState) => void;
|
||||||
|
}) {
|
||||||
const query = useQuery({
|
const query = useQuery({
|
||||||
queryKey: ["admin", "analytics", "events", filters],
|
queryKey: ["admin", "analytics", "events", filters],
|
||||||
queryFn: () =>
|
queryFn: () =>
|
||||||
@@ -757,7 +788,7 @@ function EventsSection() {
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
const reset = () => setFilters({ from: "", to: "", eventId: null });
|
const reset = () => onFiltersChange({ from: "", to: "", eventId: null });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
@@ -766,7 +797,7 @@ function EventsSection() {
|
|||||||
<div>
|
<div>
|
||||||
<DateRangeFilter
|
<DateRangeFilter
|
||||||
value={filters}
|
value={filters}
|
||||||
onChange={(next) => setFilters((current) => ({ ...current, ...next }))}
|
onChange={(next) => onFiltersChange({ ...filters, ...next })}
|
||||||
onReset={reset}
|
onReset={reset}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -774,7 +805,7 @@ function EventsSection() {
|
|||||||
<Label>رویداد</Label>
|
<Label>رویداد</Label>
|
||||||
<AsyncSearchableCombobox
|
<AsyncSearchableCombobox
|
||||||
value={filters.eventId ?? null}
|
value={filters.eventId ?? null}
|
||||||
onChange={(eventId) => setFilters((current) => ({ ...current, eventId }))}
|
onChange={(eventId) => onFiltersChange({ ...filters, eventId })}
|
||||||
loadOptions={async ({ search, limit, offset }) => {
|
loadOptions={async ({ search, limit, offset }) => {
|
||||||
const data = await api.getAdminDashboardEventOptions({ search, limit, offset });
|
const data = await api.getAdminDashboardEventOptions({ search, limit, offset });
|
||||||
return {
|
return {
|
||||||
@@ -838,8 +869,13 @@ function EventsContent({ data }: { data: EventAnalyticsSchema }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function BlogSection() {
|
function BlogSection({
|
||||||
const [filters, setFilters] = React.useState<DateRangeState>({ from: "", to: "" });
|
filters,
|
||||||
|
onFiltersChange,
|
||||||
|
}: {
|
||||||
|
filters: DateRangeState;
|
||||||
|
onFiltersChange: (filters: DateRangeState) => void;
|
||||||
|
}) {
|
||||||
const query = useQuery({
|
const query = useQuery({
|
||||||
queryKey: ["admin", "analytics", "blog", filters],
|
queryKey: ["admin", "analytics", "blog", filters],
|
||||||
queryFn: () => api.getAdminBlogAnalytics({ date_from: filters.from || undefined, date_to: filters.to || undefined }),
|
queryFn: () => api.getAdminBlogAnalytics({ date_from: filters.from || undefined, date_to: filters.to || undefined }),
|
||||||
@@ -848,7 +884,7 @@ function BlogSection() {
|
|||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<FilterCard title="فیلتر بلاگ" description="این فیلتر فقط روی نوشتهها و تعاملات بلاگ اعمال میشود و به رویدادها وابسته نیست.">
|
<FilterCard title="فیلتر بلاگ" description="این فیلتر فقط روی نوشتهها و تعاملات بلاگ اعمال میشود و به رویدادها وابسته نیست.">
|
||||||
<DateRangeFilter value={filters} onChange={setFilters} onReset={() => setFilters({ from: "", to: "" })} />
|
<DateRangeFilter value={filters} onChange={onFiltersChange} onReset={() => onFiltersChange({ from: "", to: "" })} />
|
||||||
</FilterCard>
|
</FilterCard>
|
||||||
{query.isLoading ? <SectionLoading /> : null}
|
{query.isLoading ? <SectionLoading /> : null}
|
||||||
{query.isError ? <SectionError error={query.error} /> : null}
|
{query.isError ? <SectionError error={query.error} /> : null}
|
||||||
@@ -882,6 +918,35 @@ function BlogContent({ data }: { data: BlogAnalyticsSchema }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function AdminDashboard() {
|
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 (
|
return (
|
||||||
<div className="space-y-6" dir="rtl">
|
<div className="space-y-6" dir="rtl">
|
||||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
<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>
|
||||||
</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">
|
<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">
|
<TabsTrigger value="users" className="gap-2 rounded-xl py-2">
|
||||||
<UsersRound className="h-4 w-4" />
|
<UsersRound className="h-4 w-4" />
|
||||||
@@ -909,13 +974,13 @@ export default function AdminDashboard() {
|
|||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
<TabsContent value="users">
|
<TabsContent value="users">
|
||||||
<UsersSection />
|
<UsersSection filters={usersFilters} onFiltersChange={setUsersFilters} />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="events">
|
<TabsContent value="events">
|
||||||
<EventsSection />
|
<EventsSection filters={eventsFilters} onFiltersChange={setEventsFilters} />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="blog">
|
<TabsContent value="blog">
|
||||||
<BlogSection />
|
<BlogSection filters={blogFilters} onFiltersChange={setBlogFilters} />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user