Files
lms-sb/src/components/admin/csv-exporter.tsx
T
admins c94a8dafa9 style(lms): синхронизировать типографику со шкалой ДС-2 (+2px)
Переопределены токены шкалы 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>
2026-05-18 17:14:16 +05:00

92 lines
3.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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>
);
}