File size: 4,581 Bytes
9205030 4c7b844 9205030 4c7b844 9205030 4c7b844 9205030 4c7b844 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 | import type { RefObject, KeyboardEvent } from "react";
import { MaterialIcon } from "@/components/ui/MaterialIcon";
interface FloatingNavProps {
questions: Array<{ id: string }>;
activeQuestionId: string | null;
answers: Record<string, string>;
markedQuestions: Set<string>;
onGoToQuestion: (qId: string) => void;
onScrollToTop: () => void;
navSliderRef?: RefObject<HTMLDivElement | null>;
}
export function FloatingNav({
questions,
activeQuestionId,
answers,
markedQuestions,
onGoToQuestion,
onScrollToTop,
navSliderRef,
}: FloatingNavProps) {
const isAnswered = (qId: string) => !!answers[qId];
const isMarked = (qId: string) => markedQuestions.has(qId);
const handleKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {
if (questions.length === 0) return;
const activeIdx = questions.findIndex((q) => q.id === activeQuestionId);
if (e.key === "ArrowRight") {
e.preventDefault();
const next = Math.min(questions.length - 1, Math.max(0, activeIdx) + 1);
onGoToQuestion(questions[next].id);
} else if (e.key === "ArrowLeft") {
e.preventDefault();
const prev = Math.max(0, Math.max(0, activeIdx) - 1);
onGoToQuestion(questions[prev].id);
} else if (e.key === "Home") {
e.preventDefault();
onGoToQuestion(questions[0].id);
} else if (e.key === "End") {
e.preventDefault();
onGoToQuestion(questions[questions.length - 1].id);
}
};
return (
<nav
className="fixed bottom-6 left-1/2 -translate-x-1/2 flex items-center gap-2 p-2 bg-[var(--clay-black)]/95 backdrop-blur-md rounded-full shadow-2xl border border-white/10 z-40 max-w-[90vw]"
aria-label="Navigasi soal"
>
<button
onClick={() => {
navSliderRef?.current?.scrollBy({ left: -200, behavior: "smooth" });
}}
className="w-8 h-8 shrink-0 flex items-center justify-center rounded-full bg-white/10 text-white/60 hover:bg-white/20 hover:text-white transition-all"
aria-label="Geser navigasi ke kiri"
>
<MaterialIcon name="chevron_left" className="text-sm" />
</button>
<div
ref={navSliderRef}
className="flex items-center gap-1 overflow-x-auto hide-scrollbar max-w-[60vw] sm:max-w-[50vw] md:max-w-[40vw] lg:max-w-[30vw]"
role="tablist"
aria-label="Daftar soal"
onKeyDown={handleKeyDown}
>
{questions.map((q, gIdx) => {
const answered = isAnswered(q.id);
const marked = isMarked(q.id);
const isActive = q.id === activeQuestionId;
return (
<button
key={q.id}
data-qid={q.id}
onClick={() => onGoToQuestion(q.id)}
role="tab"
aria-selected={isActive}
tabIndex={isActive ? 0 : -1}
aria-controls={`question-panel-${q.id}`}
aria-label={`Soal ${gIdx + 1}${marked ? " (ditandai)" : ""}${answered ? " (sudah dijawab)" : ""}`}
className={`relative w-7 h-7 sm:w-8 sm:h-8 shrink-0 flex items-center justify-center rounded-full font-bold text-[10px] sm:text-xs transition-all ${
answered
? "bg-[var(--matcha-600)] text-[var(--pure-white)]"
: isActive
? "bg-white/30 text-white"
: "bg-white/10 text-white/60 hover:bg-white/20"
}`}
>
{gIdx + 1}
{marked && (
<span className="absolute -top-0.5 -right-0.5 w-2 h-2 bg-[var(--pomegranate-400)] rounded-full border border-[var(--clay-black)]" />
)}
</button>
);
})}
</div>
<button
onClick={() => {
navSliderRef?.current?.scrollBy({ left: 200, behavior: "smooth" });
}}
className="w-8 h-8 shrink-0 flex items-center justify-center rounded-full bg-white/10 text-white/60 hover:bg-white/20 hover:text-white transition-all"
aria-label="Geser navigasi ke kanan"
>
<MaterialIcon name="chevron_right" className="text-sm" />
</button>
<div className="w-px h-6 bg-white/10 shrink-0" />
<button
onClick={onScrollToTop}
className="flex items-center gap-1 px-3 py-2 shrink-0 rounded-full text-white/80 font-semibold text-xs hover:text-white transition-all"
aria-label="Kembali ke atas"
>
<MaterialIcon name="arrow_upward" className="text-sm" />
<span className="hidden sm:inline">Atas</span>
</button>
</nav>
);
}
|