Add search, filters, pagination to admin users table
- Add emailVerified filter (true/false/any) to UsersSearch component - Wire emailVerified param through page searchParams, where clause, and pageUrl helper - Preserve emailVerified in pagination links alongside existing search/role/balance params - Update pagination label to "Страница X из Y · Всего: N" Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -8,11 +8,11 @@ import { UsersSearch } from "@/components/admin/users-search";
|
|||||||
const PAGE_SIZE = 20;
|
const PAGE_SIZE = 20;
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
searchParams: Promise<{ search?: string; role?: string; page?: string; balance?: string }>;
|
searchParams: Promise<{ search?: string; role?: string; page?: string; balance?: string; emailVerified?: string }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function UsersPage({ searchParams }: Props) {
|
export default async function UsersPage({ searchParams }: Props) {
|
||||||
const { search = "", role = "", page = "1", balance = "" } = await searchParams;
|
const { search = "", role = "", page = "1", balance = "", emailVerified = "" } = await searchParams;
|
||||||
const currentPage = Math.max(1, parseInt(page) || 1);
|
const currentPage = Math.max(1, parseInt(page) || 1);
|
||||||
const skip = (currentPage - 1) * PAGE_SIZE;
|
const skip = (currentPage - 1) * PAGE_SIZE;
|
||||||
|
|
||||||
@@ -37,6 +37,8 @@ export default async function UsersPage({ searchParams }: Props) {
|
|||||||
}
|
}
|
||||||
: {}),
|
: {}),
|
||||||
...(role ? { role } : {}),
|
...(role ? { role } : {}),
|
||||||
|
...(emailVerified === "true" ? { emailVerified: true } : {}),
|
||||||
|
...(emailVerified === "false" ? { emailVerified: false } : {}),
|
||||||
...(balanceUserIds !== null ? { id: { in: balanceUserIds } } : {}),
|
...(balanceUserIds !== null ? { id: { in: balanceUserIds } } : {}),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -78,6 +80,7 @@ export default async function UsersPage({ searchParams }: Props) {
|
|||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
if (search) params.set("search", search);
|
if (search) params.set("search", search);
|
||||||
if (role) params.set("role", role);
|
if (role) params.set("role", role);
|
||||||
|
if (emailVerified) params.set("emailVerified", emailVerified);
|
||||||
if (balance) params.set("balance", balance);
|
if (balance) params.set("balance", balance);
|
||||||
params.set("page", String(p));
|
params.set("page", String(p));
|
||||||
return `/admin/users?${params.toString()}`;
|
return `/admin/users?${params.toString()}`;
|
||||||
@@ -101,7 +104,7 @@ export default async function UsersPage({ searchParams }: Props) {
|
|||||||
|
|
||||||
{/* Filters */}
|
{/* Filters */}
|
||||||
<Suspense>
|
<Suspense>
|
||||||
<UsersSearch initialSearch={search} initialRole={role} initialBalance={balance} />
|
<UsersSearch initialSearch={search} initialRole={role} initialEmailVerified={emailVerified} initialBalance={balance} />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
|
||||||
<UsersTable users={tableUsers} />
|
<UsersTable users={tableUsers} />
|
||||||
@@ -132,7 +135,7 @@ export default async function UsersPage({ searchParams }: Props) {
|
|||||||
{currentPage < totalPages && (
|
{currentPage < totalPages && (
|
||||||
<Link href={pageUrl(currentPage + 1)} className="btn-aubade px-3 py-1 text-xs">→</Link>
|
<Link href={pageUrl(currentPage + 1)} className="btn-aubade px-3 py-1 text-xs">→</Link>
|
||||||
)}
|
)}
|
||||||
<span className="ml-2 text-xs" style={{ color: "var(--muted-foreground)" }}>стр. {currentPage} из {totalPages}</span>
|
<span className="ml-2 text-xs" style={{ color: "var(--muted-foreground)" }}>Страница {currentPage} из {totalPages} · Всего: {total}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -16,20 +16,23 @@ const inputStyle: React.CSSProperties = {
|
|||||||
export function UsersSearch({
|
export function UsersSearch({
|
||||||
initialSearch,
|
initialSearch,
|
||||||
initialRole,
|
initialRole,
|
||||||
|
initialEmailVerified,
|
||||||
initialBalance,
|
initialBalance,
|
||||||
}: {
|
}: {
|
||||||
initialSearch: string;
|
initialSearch: string;
|
||||||
initialRole: string;
|
initialRole: string;
|
||||||
|
initialEmailVerified: string;
|
||||||
initialBalance: string;
|
initialBalance: string;
|
||||||
}) {
|
}) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const [, startTransition] = useTransition();
|
const [, startTransition] = useTransition();
|
||||||
|
|
||||||
function update(search: string, role: string, balance: string) {
|
function update(search: string, role: string, emailVerified: string, balance: string) {
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
if (search) params.set("search", search);
|
if (search) params.set("search", search);
|
||||||
if (role) params.set("role", role);
|
if (role) params.set("role", role);
|
||||||
|
if (emailVerified) params.set("emailVerified", emailVerified);
|
||||||
if (balance) params.set("balance", balance);
|
if (balance) params.set("balance", balance);
|
||||||
startTransition(() => router.push(`${pathname}?${params.toString()}`));
|
startTransition(() => router.push(`${pathname}?${params.toString()}`));
|
||||||
}
|
}
|
||||||
@@ -45,7 +48,7 @@ export function UsersSearch({
|
|||||||
onFocus={(e) => (e.currentTarget.style.borderColor = "var(--foreground)")}
|
onFocus={(e) => (e.currentTarget.style.borderColor = "var(--foreground)")}
|
||||||
onBlur={(e) => {
|
onBlur={(e) => {
|
||||||
e.currentTarget.style.borderColor = "var(--border)";
|
e.currentTarget.style.borderColor = "var(--border)";
|
||||||
update(e.currentTarget.value.trim(), initialRole, initialBalance);
|
update(e.currentTarget.value.trim(), initialRole, initialEmailVerified, initialBalance);
|
||||||
}}
|
}}
|
||||||
onKeyDown={(e) => { if (e.key === "Enter") e.currentTarget.blur(); }}
|
onKeyDown={(e) => { if (e.key === "Enter") e.currentTarget.blur(); }}
|
||||||
/>
|
/>
|
||||||
@@ -53,7 +56,7 @@ export function UsersSearch({
|
|||||||
|
|
||||||
<select
|
<select
|
||||||
defaultValue={initialRole}
|
defaultValue={initialRole}
|
||||||
onChange={(e) => update(initialSearch, e.target.value, initialBalance)}
|
onChange={(e) => update(initialSearch, e.target.value, initialEmailVerified, initialBalance)}
|
||||||
style={{ ...inputStyle, appearance: "none", cursor: "pointer" }}
|
style={{ ...inputStyle, appearance: "none", cursor: "pointer" }}
|
||||||
onFocus={(e) => (e.currentTarget.style.borderColor = "var(--foreground)")}
|
onFocus={(e) => (e.currentTarget.style.borderColor = "var(--foreground)")}
|
||||||
onBlur={(e) => (e.currentTarget.style.borderColor = "var(--border)")}
|
onBlur={(e) => (e.currentTarget.style.borderColor = "var(--border)")}
|
||||||
@@ -64,9 +67,21 @@ export function UsersSearch({
|
|||||||
<option value="admin">Администраторы</option>
|
<option value="admin">Администраторы</option>
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
<select
|
||||||
|
defaultValue={initialEmailVerified}
|
||||||
|
onChange={(e) => update(initialSearch, initialRole, e.target.value, initialBalance)}
|
||||||
|
style={{ ...inputStyle, appearance: "none", cursor: "pointer" }}
|
||||||
|
onFocus={(e) => (e.currentTarget.style.borderColor = "var(--foreground)")}
|
||||||
|
onBlur={(e) => (e.currentTarget.style.borderColor = "var(--border)")}
|
||||||
|
>
|
||||||
|
<option value="">Любой статус</option>
|
||||||
|
<option value="true">Email подтверждён</option>
|
||||||
|
<option value="false">Не подтверждён</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => update(initialSearch, initialRole, initialBalance === "nonzero" ? "" : "nonzero")}
|
onClick={() => update(initialSearch, initialRole, initialEmailVerified, initialBalance === "nonzero" ? "" : "nonzero")}
|
||||||
className="text-xs px-3"
|
className="text-xs px-3"
|
||||||
style={{
|
style={{
|
||||||
border: "2px solid var(--border)",
|
border: "2px solid var(--border)",
|
||||||
@@ -78,7 +93,7 @@ export function UsersSearch({
|
|||||||
С балансом
|
С балансом
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{(initialSearch || initialRole || initialBalance) && (
|
{(initialSearch || initialRole || initialEmailVerified || initialBalance) && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => startTransition(() => router.push(pathname))}
|
onClick={() => startTransition(() => router.push(pathname))}
|
||||||
|
|||||||
Reference in New Issue
Block a user