c94a8dafa9
Переопределены токены шкалы Tailwind (--text-xs…--text-5xl) на +2px, базовый размер body 18px, размеры компонентных классов (.btn-aubade, .tag-aubade, .admin-sidebar-nav-link) и инлайновые fontSize приведены к канону дизайн-системы ДС-2. Rem-база (html 16px) не тронута — спейсинг и сетка не затронуты, растёт только текст. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
92 lines
3.4 KiB
TypeScript
92 lines
3.4 KiB
TypeScript
"use client";
|
||
|
||
import { useState } from "react";
|
||
import { Download } from "lucide-react";
|
||
|
||
type Course = { id: string; title: string };
|
||
|
||
const inputStyle: React.CSSProperties = {
|
||
border: "2px solid var(--border)",
|
||
background: "var(--background)",
|
||
outline: "none",
|
||
width: "100%",
|
||
padding: "0.5rem 0.75rem",
|
||
fontSize: "16px",
|
||
fontFamily: "inherit",
|
||
};
|
||
const focusHandlers = {
|
||
onFocus: (e: React.FocusEvent<HTMLSelectElement>) =>
|
||
(e.currentTarget.style.borderColor = "var(--foreground)"),
|
||
onBlur: (e: React.FocusEvent<HTMLSelectElement>) =>
|
||
(e.currentTarget.style.borderColor = "var(--border)"),
|
||
};
|
||
|
||
export function CsvExporter({ courses }: { courses: Course[] }) {
|
||
const [courseId, setCourseId] = useState("");
|
||
const [encoding, setEncoding] = useState<"utf8" | "win1251">("utf8");
|
||
const [loading, setLoading] = useState(false);
|
||
|
||
async function handleExport() {
|
||
setLoading(true);
|
||
try {
|
||
const params = new URLSearchParams({ encoding });
|
||
if (courseId) params.set("courseId", courseId);
|
||
|
||
const res = await fetch(`/api/admin/export-users?${params}`);
|
||
if (!res.ok) throw new Error("Ошибка сервера");
|
||
|
||
const blob = await res.blob();
|
||
const url = URL.createObjectURL(blob);
|
||
const a = document.createElement("a");
|
||
a.href = url;
|
||
const cd = res.headers.get("content-disposition") ?? "";
|
||
const match = cd.match(/filename="([^"]+)"/);
|
||
a.download = match?.[1] ?? "students.csv";
|
||
a.click();
|
||
URL.revokeObjectURL(url);
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
}
|
||
|
||
return (
|
||
<div className="space-y-5 max-w-md">
|
||
<div className="space-y-1.5">
|
||
<label className="text-xs font-bold uppercase tracking-widest" style={{ color: "var(--muted-foreground)" }}>
|
||
Фильтр по курсу
|
||
</label>
|
||
<select value={courseId} onChange={(e) => setCourseId(e.target.value)} style={{ ...inputStyle, appearance: "none" }} {...focusHandlers}>
|
||
<option value="">Все ученики</option>
|
||
{courses.map((c) => <option key={c.id} value={c.id}>{c.title}</option>)}
|
||
</select>
|
||
</div>
|
||
|
||
<div className="space-y-1.5">
|
||
<label className="text-xs font-bold uppercase tracking-widest" style={{ color: "var(--muted-foreground)" }}>
|
||
Кодировка файла
|
||
</label>
|
||
<select value={encoding} onChange={(e) => setEncoding(e.target.value as "utf8" | "win1251")} style={{ ...inputStyle, appearance: "none" }} {...focusHandlers}>
|
||
<option value="utf8">UTF-8 (универсальная)</option>
|
||
<option value="win1251">Windows-1251 (для Excel на Windows)</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div className="p-4 space-y-1" style={{ border: "2px solid var(--border)" }}>
|
||
<p className="text-xs font-bold uppercase tracking-widest" style={{ color: "var(--muted-foreground)" }}>Поля в файле</p>
|
||
<p className="text-sm">Email · Имя · Телефон · Дата регистрации · Курсы · Прогресс (уроков)</p>
|
||
</div>
|
||
|
||
<button
|
||
type="button"
|
||
onClick={handleExport}
|
||
disabled={loading}
|
||
className="btn-aubade btn-aubade-accent flex items-center gap-2 px-5 py-2 text-sm"
|
||
style={{ opacity: loading ? 0.6 : 1 }}
|
||
>
|
||
<Download size={14} />
|
||
{loading ? "Формирую файл..." : "Скачать CSV"}
|
||
</button>
|
||
</div>
|
||
);
|
||
}
|