-
Mẫu truy vấn:
- {PRESET_QUERIES.map((p, i) => (
-
+
+ {PRESET_GROUPS.map((group) => (
+
+
{group.category}
+
+ {group.queries.map((p, i) => (
+
+ ))}
+
+
))}
diff --git a/2017/src/components/search-form.jsx b/2017/src/components/search-form.jsx
index 8a75269..c8e69a9 100644
--- a/2017/src/components/search-form.jsx
+++ b/2017/src/components/search-form.jsx
@@ -19,13 +19,18 @@ function detectMode(raw) {
: { mode: "name-short", hint: `Cần ít nhất ${MIN_NAME_CHARS} ký tự` };
}
-export function SearchForm({ onSearch, onClear, disabled }) {
- const [query, setQuery] = useState("");
+export function SearchForm({ value = "", onSearch, onClear, disabled }) {
+ const [query, setQuery] = useState(value);
const inputRef = useRef(null);
const timerRef = useRef(null);
const { mode, hint } = detectMode(query);
const canSearch = mode === "sbd" || mode === "name";
+ // Sync from external value (deep-link URL, clear button in parent)
+ useEffect(() => {
+ setQuery(value);
+ }, [value]);
+
// Debounced live search
useEffect(() => {
clearTimeout(timerRef.current);
@@ -33,9 +38,12 @@ export function SearchForm({ onSearch, onClear, disabled }) {
if (mode === "empty") onClear?.();
return;
}
+ // Skip if the query already matches external value (prevents loop when
+ // value flows in from URL hydration)
+ if (query.trim() === value.trim() && query.trim() !== "") return;
timerRef.current = setTimeout(() => onSearch(query.trim()), DEBOUNCE_MS);
return () => clearTimeout(timerRef.current);
- }, [query, canSearch, mode, onSearch, onClear]);
+ }, [query, canSearch, mode, onSearch, onClear, value]);
function handleSubmit(e) {
e.preventDefault();
diff --git a/2017/src/components/student-detail.jsx b/2017/src/components/student-detail.jsx
index 43508a6..4c77359 100644
--- a/2017/src/components/student-detail.jsx
+++ b/2017/src/components/student-detail.jsx
@@ -29,18 +29,56 @@ function fmt(n) {
return n === null || n === undefined ? "—" : Number(n).toFixed(2);
}
+// Visible tier legend — keeps symbol + range + label co-located so users
+// don't have to hover tiles to decode colors.
+const TIER_LEGEND = [
+ { key: "poor", symbol: "▽", range: "< 5", label: "Chưa đạt" },
+ { key: "weak", symbol: "○", range: "5-6.5", label: "Trung bình" },
+ { key: "fair", symbol: "◆", range: "6.5-8", label: "Khá" },
+ { key: "good", symbol: "★", range: "8-9", label: "Giỏi" },
+ { key: "excellent", symbol: "✦", range: "≥ 9", label: "Xuất sắc" },
+];
+
export function StudentDetail({ student }) {
- const [copied, setCopied] = useState(false);
+ const [copied, setCopied] = useState(null); // null | 'sbd' | 'share' | 'url'
const blocks = computeBlocks(student);
const subjects = SUBJECT_ORDER
.filter((k) => student[k] !== null && student[k] !== undefined)
.map((k) => ({ key: k, label: SUBJECT_LABELS[k], score: student[k] }));
+ function flash(kind) {
+ setCopied(kind);
+ setTimeout(() => setCopied(null), 1500);
+ }
+
function copySbd() {
- navigator.clipboard.writeText(student.so_bao_danh).then(() => {
- setCopied(true);
- setTimeout(() => setCopied(false), 1500);
- });
+ navigator.clipboard.writeText(student.so_bao_danh).then(() => flash("sbd"));
+ }
+
+ // Formatted summary suitable for pasting into Zalo / Messenger:
+ // Nguyễn Văn A (SBD 49008235)
+ // Toán 8.50 · Ngữ văn 7.25 · ...
+ // Khối A: 22.75
+ // Xem tại: https://...?q=49008235
+ function shareSummary() {
+ const lines = [];
+ lines.push(`${student.ho_ten} (SBD ${student.so_bao_danh})`);
+ lines.push(subjects.map((s) => `${s.label} ${fmt(s.score)}`).join(" · "));
+ if (blocks.length > 0) {
+ lines.push(
+ blocks.slice(0, 3).map((b) => `Khối ${b.code} ${b.total.toFixed(2)}`).join(" · "),
+ );
+ }
+ lines.push(
+ `${window.location.origin}${window.location.pathname}?q=${student.so_bao_danh}`,
+ );
+ const text = lines.join("\n");
+
+ if (navigator.share) {
+ navigator.share({ text }).catch(() => {});
+ return;
+ }
+ navigator.clipboard.writeText(text).then(() => flash("share"));
}
return (
@@ -59,7 +97,7 @@ export function StudentDetail({ student }) {
onClick={copySbd}
aria-label="Sao chép số báo danh"
>
- {copied ? "✓ Đã chép" : "Chép"}
+ {copied === "sbd" ? "✓ Đã chép" : "Chép"}
@@ -71,10 +109,31 @@ export function StudentDetail({ student }) {
)}