Refactor App into TS modules; add tsc typecheck

This commit is contained in:
2026-02-07 20:30:56 +01:00
parent 11cfade6dc
commit 684a4dd0b8
22 changed files with 3254 additions and 1664 deletions
+139
View File
@@ -0,0 +1,139 @@
import { Check, ChevronLeft, ChevronRight, Link } from "lucide-react";
import type { KeyboardEventHandler, MouseEventHandler } from "react";
import type { BookMetadata } from "../types";
import { styles } from "../styles";
import { PauseSolid, PlaySolid } from "./Icons";
type Props = {
isPlaying: boolean;
onTogglePlay: () => void;
onBack10: () => void;
onForward10: () => void;
onProgressClick: MouseEventHandler<HTMLDivElement>;
onProgressKeyDown: KeyboardEventHandler<HTMLDivElement>;
progressPercent: number;
currentIndex: number;
wordsLength: number;
linkCopied: boolean;
onCopyLink: () => void;
bookMetadata: BookMetadata | null;
bookStatsText: string | null;
};
export function BottomControls({
isPlaying,
onTogglePlay,
onBack10,
onForward10,
onProgressClick,
onProgressKeyDown,
progressPercent,
currentIndex,
wordsLength,
linkCopied,
onCopyLink,
bookMetadata,
bookStatsText,
}: Props) {
return (
<div style={styles.bottomArea} className="bottom-area">
<div style={styles.controlsRow}>
<button onClick={onBack10} style={styles.skipBtn} title="Back 10 words">
<ChevronLeft size={24} />
<ChevronLeft size={24} style={{ marginLeft: -14 }} />
</button>
<button
onClick={onTogglePlay}
style={styles.playBtn}
className="play-btn"
>
{isPlaying ? <PauseSolid size={32} /> : <PlaySolid size={32} />}
</button>
<button
onClick={onForward10}
style={styles.skipBtn}
title="Forward 10 words"
>
<ChevronRight size={24} />
<ChevronRight size={24} style={{ marginLeft: -14 }} />
</button>
</div>
<div
style={styles.progressContainer}
onClick={onProgressClick}
onKeyDown={onProgressKeyDown}
role="slider"
tabIndex={0}
aria-label="Reading progress"
aria-valuemin={0}
aria-valuemax={100}
aria-valuenow={Math.round(progressPercent)}
aria-valuetext={`${Math.round(progressPercent)}% complete, word ${currentIndex + 1} of ${wordsLength}`}
>
<div style={{ ...styles.progressBar, width: `${progressPercent}%` }} />
</div>
<div style={styles.progressRow}>
<div style={styles.progressText}>
{currentIndex + 1} / {wordsLength} ({Math.round(progressPercent)}%)
</div>
<button
onClick={onCopyLink}
style={styles.linkBtn}
className="icon-btn"
title="Copy link to current position"
>
{linkCopied ? <Check size={14} /> : <Link size={14} />}
<span style={styles.linkBtnText}>
{linkCopied ? "Copied" : "Copy link"}
</span>
</button>
</div>
<div style={styles.hint} className="hint">
<kbd style={styles.kbd}>Space</kbd> play<kbd style={styles.kbd}></kbd>
<kbd style={styles.kbd}></kbd> word<kbd style={styles.kbd}></kbd>
<kbd style={styles.kbd}></kbd> speed<kbd style={styles.kbd}>R</kbd>{" "}
reset
</div>
{bookMetadata && (bookMetadata.title || bookMetadata.cover) && (
<aside
style={styles.bookMetadata}
aria-label="Current book"
className="book-metadata"
>
{bookMetadata.cover && (
<img
src={bookMetadata.cover}
alt={`Cover of ${bookMetadata.title || "current book"}`}
style={styles.bookCover}
className="book-cover"
/>
)}
<div style={styles.bookInfo}>
{bookMetadata.title && (
<h3 style={styles.bookTitle} className="book-title">
{bookMetadata.title}
</h3>
)}
{bookMetadata.author && (
<p style={styles.bookAuthor} className="book-author">
{bookMetadata.author}
</p>
)}
{bookStatsText && (
<p style={styles.bookStats} className="book-stats">
{bookStatsText}
</p>
)}
</div>
</aside>
)}
</div>
);
}