Polish: homework filters, users search/popup, admin comments

Homework (/curator/homework):
- Search by student name/email
- Filter by status (pending/reviewed) and course
- Server-side pagination (20 per page) with URL params

Users (/admin/users):
- Search by name/email, filter by role
- Hover popup on each row: enrolled courses + expiry dates + email
- Pagination (20 per page) with URL params

Comments (/admin/comments):
- New admin page with all active comments
- Search by author or text content
- One-click delete (soft-delete) from the table
- Pagination (30 per page)
- Added "Комментарии" link to admin nav

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-08 13:00:57 +05:00
parent dd46a10c20
commit d0ba4bf909
9 changed files with 874 additions and 152 deletions
+70
View File
@@ -0,0 +1,70 @@
"use client";
import { useRouter, usePathname } from "next/navigation";
import { useTransition } from "react";
import { Search } from "lucide-react";
const inputStyle: React.CSSProperties = {
border: "2px solid var(--border)",
background: "var(--background)",
outline: "none",
padding: "0.4rem 0.75rem",
fontSize: "0.8rem",
fontFamily: "inherit",
};
export function UsersSearch({ initialSearch, initialRole }: { initialSearch: string; initialRole: string }) {
const router = useRouter();
const pathname = usePathname();
const [, startTransition] = useTransition();
function update(search: string, role: string) {
const params = new URLSearchParams();
if (search) params.set("search", search);
if (role) params.set("role", role);
startTransition(() => router.push(`${pathname}?${params.toString()}`));
}
return (
<div className="flex flex-wrap gap-2 mb-4">
<div className="relative">
<Search size={13} className="absolute left-2.5 top-1/2 -translate-y-1/2" style={{ color: "var(--muted-foreground)" }} />
<input
defaultValue={initialSearch}
placeholder="Поиск по имени или email"
style={{ ...inputStyle, paddingLeft: "2rem", width: 240 }}
onFocus={(e) => (e.currentTarget.style.borderColor = "var(--foreground)")}
onBlur={(e) => {
e.currentTarget.style.borderColor = "var(--border)";
update(e.currentTarget.value.trim(), initialRole);
}}
onKeyDown={(e) => { if (e.key === "Enter") e.currentTarget.blur(); }}
/>
</div>
<select
defaultValue={initialRole}
onChange={(e) => update(initialSearch, e.target.value)}
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="student">Ученики</option>
<option value="curator">Кураторы</option>
<option value="admin">Администраторы</option>
</select>
{(initialSearch || initialRole) && (
<button
type="button"
onClick={() => startTransition(() => router.push(pathname))}
className="text-xs px-3"
style={{ border: "2px solid var(--border)", color: "var(--muted-foreground)" }}
>
Сбросить
</button>
)}
</div>
);
}