From 78810e75460aa1918de01c9927c8a1374222600a Mon Sep 17 00:00:00 2001 From: Tom Kuson Date: Sun, 10 Nov 2024 15:28:04 +0000 Subject: [PATCH 1/4] Enable VCS support with Biome --- biome.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/biome.json b/biome.json index 2eb0751..0eae55e 100644 --- a/biome.json +++ b/biome.json @@ -1,9 +1,10 @@ { "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", "vcs": { - "enabled": false, + "enabled": true, "clientKind": "git", - "useIgnoreFile": false + "useIgnoreFile": true, + "defaultBranch": "main" }, "files": { "ignoreUnknown": false, From 87cdc3548cb7279dda986f23b828778846845c7c Mon Sep 17 00:00:00 2001 From: Tom Kuson Date: Sun, 10 Nov 2024 15:29:23 +0000 Subject: [PATCH 2/4] Use hooks --- src/App.tsx | 172 ++++-------------------------- src/components/ScoreSelection.tsx | 48 ++------- src/hooks/useBinarySearch.ts | 161 ++++++++++++++++++++++++++++ src/hooks/useScoreOptions.ts | 48 +++++++++ 4 files changed, 241 insertions(+), 188 deletions(-) create mode 100644 src/hooks/useBinarySearch.ts create mode 100644 src/hooks/useScoreOptions.ts diff --git a/src/App.tsx b/src/App.tsx index 6a34733..11eb772 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,155 +1,31 @@ -import { useState } from "react"; import "./App.css"; import BracketDescription from "./components/BracketDescription"; import Footer from "./components/Footer"; import ScaleSelection from "./components/ScaleSelection"; import ScoreSelection from "./components/ScoreSelection"; -import { scales } from "./data/scales"; -import type { Bracket, JudgeBracket, ScaleType, SpeakerBracket } from "./types"; +import { useBinarySearch } from "./hooks/useBinarySearch"; import { isSpeakerBracket } from "./utils"; function App() { - const [selectedScale, setSelectedScale] = useState("speaker"); - const [low, setLow] = useState(0); - const [high, setHigh] = useState(scales[selectedScale].length - 1); - const [mid, setMid] = useState( - Math.floor((0 + scales[selectedScale].length - 1) / 2), - ); - const [phase, setPhase] = useState<"search" | "select" | "done" | "between">( - "search", - ); - const [selectedBracketIndex, setSelectedBracketIndex] = useState< - number | null - >(null); - const [exactScore, setExactScore] = useState(null); - const [showScale, setShowScale] = useState(false); - const [betweenBracketIndices, setBetweenBracketIndices] = useState< - [number, number] | null - >(null); - - const currentScale = scales[selectedScale] as Bracket[]; - - const handleScaleChange = (newScale: ScaleType) => { - setSelectedScale(newScale); - setLow(0); - setHigh(scales[newScale].length - 1); - setMid(Math.floor((0 + scales[newScale].length - 1) / 2)); - setPhase("search"); - setSelectedBracketIndex(null); - setExactScore(null); - setBetweenBracketIndices(null); - setShowScale(false); - }; - - const handleBetter = () => { - const newHigh = mid - 1; - if (newHigh < low) { - // No more higher brackets. - if (mid === 0) { - // At the top bracket. - setSelectedBracketIndex(mid); - const bracket = currentScale[mid]; - if (!isSpeakerBracket(bracket)) { - setExactScore((bracket as JudgeBracket).score); - setPhase("done"); - } else { - setPhase("select"); - } - } else { - // Between mid-1 and mid. - setBetweenBracketIndices([mid - 1, mid]); - setPhase("between"); - } - } else { - setHigh(newHigh); - const newMid = Math.floor((low + newHigh) / 2); - setMid(newMid); - } - }; - - const handleWorse = () => { - const newLow = mid + 1; - if (newLow > high) { - // No more lower brackets. - if (mid === currentScale.length - 1) { - // At the lowest bracket. - setSelectedBracketIndex(mid); - const bracket = currentScale[mid]; - if (!isSpeakerBracket(bracket)) { - setExactScore((bracket as JudgeBracket).score); - setPhase("done"); - } else { - setPhase("select"); - } - } else { - // Between mid and mid+1. - setBetweenBracketIndices([mid, mid + 1]); - setPhase("between"); - } - } else { - setLow(newLow); - const newMid = Math.floor((newLow + high) / 2); - setMid(newMid); - } - }; - - const handleMatched = () => { - setSelectedBracketIndex(mid); - const bracket = currentScale[mid]; - if (!isSpeakerBracket(bracket)) { - setExactScore((bracket as JudgeBracket).score); - setPhase("done"); - } else { - setPhase("select"); - } - }; - - const handleSelectScore = (position: string) => { - if (selectedBracketIndex !== null) { - const bracket = currentScale[selectedBracketIndex]; - if (isSpeakerBracket(bracket)) { - let score: number; - - if (position === "lower") { - score = bracket.minScore; - } else if (position === "higher") { - score = bracket.maxScore; - } else { - score = Math.floor((bracket.minScore + bracket.maxScore) / 2); - } - - setExactScore(score); - setPhase("done"); - } - } - }; - - const handleSelectExactScore = (score: number) => { - setExactScore(score); - setPhase("done"); - }; - - const selectBracket = (index: number) => { - setSelectedBracketIndex(index); - const bracket = currentScale[index]; - if (!isSpeakerBracket(bracket)) { - setExactScore((bracket as JudgeBracket).score); - setPhase("done"); - } else { - setPhase("select"); - } - }; - - const reset = () => { - setLow(0); - setHigh(currentScale.length - 1); - setMid(Math.floor((0 + currentScale.length - 1) / 2)); - setPhase("search"); - setSelectedBracketIndex(null); - setExactScore(null); - setBetweenBracketIndices(null); - setShowScale(false); - }; + const { + selectedScale, + handleScaleChange, + mid, + phase, + selectedBracketIndex, + exactScore, + betweenBracketIndices, + showScale, + currentScale, + handleBetter, + handleWorse, + handleMatched, + handleSelectScore, + handleSelectExactScore, + selectBracket, + reset, + toggleShowScale, + } = useBinarySearch(); return (
@@ -228,13 +104,11 @@ function App() { {phase === "select" && selectedBracketIndex !== null && (

Selected Bracket:

- + {isSpeakerBracket(currentScale[selectedBracketIndex]) && ( @@ -262,7 +136,7 @@ function App() {
-
diff --git a/src/components/ScoreSelection.tsx b/src/components/ScoreSelection.tsx index 204506d..63b9c79 100644 --- a/src/components/ScoreSelection.tsx +++ b/src/components/ScoreSelection.tsx @@ -1,55 +1,25 @@ import type React from "react"; -import type { PositionOption, ScoreOption } from "../data/bracketOptions"; -import { - bottomBracketOptions, - defaultBracketOptions, - tenthBracketOptions, - topBracketOptions, -} from "../data/bracketOptions"; -import type { SpeakerBracket } from "../types"; - -interface DisplayOption { - label: string; - onClick: () => void; - key: string | number; -} +import { useScoreOptions } from "../hooks/useScoreOptions"; interface ScoreSelectionProps { selectedBracketIndex: number; - currentScale: SpeakerBracket[]; + scaleLength: number; handleSelectScore: (position: string) => void; handleSelectExactScore: (score: number) => void; } const ScoreSelection: React.FC = ({ selectedBracketIndex, - currentScale, + scaleLength, handleSelectScore, handleSelectExactScore, }) => { - const bracketOptionsMap: { [index: number]: ScoreOption[] } = { - 0: topBracketOptions, - 5: tenthBracketOptions, - [currentScale.length - 1]: bottomBracketOptions, - }; - - const getOptions = (): DisplayOption[] => { - const specialOptions = bracketOptionsMap[selectedBracketIndex]; - if (specialOptions) { - return specialOptions.map((option: ScoreOption) => ({ - label: option.label, - onClick: () => handleSelectExactScore(option.score), - key: option.score, - })); - } - return defaultBracketOptions.map((option: PositionOption) => ({ - label: option.label, - onClick: () => handleSelectScore(option.position), - key: option.position, - })); - }; - - const options = getOptions(); + const options = useScoreOptions({ + selectedBracketIndex, + scaleLength, + handleSelectScore, + handleSelectExactScore, + }); return (
diff --git a/src/hooks/useBinarySearch.ts b/src/hooks/useBinarySearch.ts new file mode 100644 index 0000000..e2c0647 --- /dev/null +++ b/src/hooks/useBinarySearch.ts @@ -0,0 +1,161 @@ +import { useMemo, useState } from "react"; +import { scales } from "../data/scales"; +import type { + Bracket, + JudgeBracket, + ScaleType, + SpeakerBracket, +} from "../types"; +import { isSpeakerBracket } from "../utils"; + +export function useBinarySearch(initialScale: ScaleType = "speaker") { + const [selectedScale, setSelectedScale] = useState(initialScale); + const [low, setLow] = useState(0); + const [high, setHigh] = useState(scales[initialScale].length - 1); + const mid = useMemo(() => Math.floor((low + high) / 2), [low, high]); + const [phase, setPhase] = useState<"search" | "select" | "done" | "between">( + "search", + ); + const [selectedBracketIndex, setSelectedBracketIndex] = useState< + number | null + >(null); + const [exactScore, setExactScore] = useState(null); + const [betweenBracketIndices, setBetweenBracketIndices] = useState< + [number, number] | null + >(null); + const [showScale, setShowScale] = useState(false); + + const currentScale = scales[selectedScale] as Bracket[]; + + const handleScaleChange = (newScale: ScaleType) => { + setSelectedScale(newScale); + setLow(0); + setHigh(scales[newScale].length - 1); + setPhase("search"); + setSelectedBracketIndex(null); + setExactScore(null); + setBetweenBracketIndices(null); + setShowScale(false); + }; + + const handleBetter = () => { + const newHigh = mid - 1; + if (newHigh >= low) { + setHigh(newHigh); + } else if (mid === 0) { + // At the top bracket + setSelectedBracketIndex(mid); + const bracket = currentScale[mid]; + if (!isSpeakerBracket(bracket)) { + setExactScore((bracket as JudgeBracket).score); + setPhase("done"); + } else { + setPhase("select"); + } + } else { + // Between mid - 1 and mid + setBetweenBracketIndices([mid - 1, mid]); + setPhase("between"); + } + }; + + const handleWorse = () => { + const newLow = mid + 1; + if (newLow <= high) { + setLow(newLow); + } else if (mid === currentScale.length - 1) { + // At the lowest bracket + setSelectedBracketIndex(mid); + const bracket = currentScale[mid]; + if (!isSpeakerBracket(bracket)) { + setExactScore((bracket as JudgeBracket).score); + setPhase("done"); + } else { + setPhase("select"); + } + } else { + // Between mid and mid + 1 + setBetweenBracketIndices([mid, mid + 1]); + setPhase("between"); + } + }; + + const handleMatched = () => { + setSelectedBracketIndex(mid); + const bracket = currentScale[mid]; + if (!isSpeakerBracket(bracket)) { + setExactScore((bracket as JudgeBracket).score); + setPhase("done"); + } else { + setPhase("select"); + } + }; + + const handleSelectScore = (position: string) => { + if (selectedBracketIndex == null) return; + + const bracket = currentScale[selectedBracketIndex] as SpeakerBracket; + if (!isSpeakerBracket(bracket)) return; + + let score: number; + if (position === "lower") { + score = bracket.minScore; + } else if (position === "higher") { + score = bracket.maxScore; + } else { + score = Math.floor((bracket.minScore + bracket.maxScore) / 2); + } + setExactScore(score); + setPhase("done"); + }; + + const handleSelectExactScore = (score: number) => { + setExactScore(score); + setPhase("done"); + }; + + const selectBracket = (index: number) => { + setSelectedBracketIndex(index); + const bracket = currentScale[index]; + if (!isSpeakerBracket(bracket)) { + setExactScore((bracket as JudgeBracket).score); + setPhase("done"); + } else { + setPhase("select"); + } + }; + + const reset = () => { + setLow(0); + setHigh(currentScale.length - 1); + setPhase("search"); + setSelectedBracketIndex(null); + setExactScore(null); + setBetweenBracketIndices(null); + setShowScale(false); + }; + + const toggleShowScale = () => { + setShowScale((prev) => !prev); + }; + + return { + selectedScale, + handleScaleChange, + mid, + phase, + selectedBracketIndex, + exactScore, + betweenBracketIndices, + showScale, + currentScale, + handleBetter, + handleWorse, + handleMatched, + handleSelectScore, + handleSelectExactScore, + selectBracket, + reset, + toggleShowScale, + }; +} diff --git a/src/hooks/useScoreOptions.ts b/src/hooks/useScoreOptions.ts new file mode 100644 index 0000000..2b2c5b2 --- /dev/null +++ b/src/hooks/useScoreOptions.ts @@ -0,0 +1,48 @@ +import { + type ScoreOption, + bottomBracketOptions, + defaultBracketOptions, + tenthBracketOptions, + topBracketOptions, +} from "../data/bracketOptions"; + +interface UseScoreOptionsParams { + selectedBracketIndex: number; + scaleLength: number; + handleSelectScore: (position: string) => void; + handleSelectExactScore: (score: number) => void; +} + +interface DisplayOption { + label: string; + onClick: () => void; + key: string | number; +} + +export function useScoreOptions({ + selectedBracketIndex, + scaleLength, + handleSelectScore, + handleSelectExactScore, +}: UseScoreOptionsParams): DisplayOption[] { + const bracketOptionsMap: { [index: number]: ScoreOption[] } = { + 0: topBracketOptions, + 5: tenthBracketOptions, + [scaleLength - 1]: bottomBracketOptions, + }; + + const specialOptions = bracketOptionsMap[selectedBracketIndex]; + + if (specialOptions) { + return specialOptions.map((option) => ({ + label: option.label, + onClick: () => handleSelectExactScore(option.score), + key: option.score, + })); + } + return defaultBracketOptions.map((option) => ({ + label: option.label, + onClick: () => handleSelectScore(option.position), + key: option.position, + })); +} From e172d651d0596c7620974ff3003236ebf72c108f Mon Sep 17 00:00:00 2001 From: Tom Kuson Date: Sun, 10 Nov 2024 16:24:57 +0000 Subject: [PATCH 3/4] Fix type guard to accept unknown and handle undefined --- src/utils.tsx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/utils.tsx b/src/utils.tsx index e7ece15..b2f9345 100644 --- a/src/utils.tsx +++ b/src/utils.tsx @@ -1,7 +1,11 @@ -import type { Bracket, SpeakerBracket } from "./types"; +import type { SpeakerBracket } from "./types"; export const isSpeakerBracket = ( - bracket: Bracket, + bracket: unknown, ): bracket is SpeakerBracket => { - return (bracket as SpeakerBracket).minScore !== undefined; + return ( + typeof bracket === "object" && + bracket !== null && + (bracket as SpeakerBracket).minScore !== undefined + ); }; From 70100d9a5acb3b070735427739c52e3b133a16c8 Mon Sep 17 00:00:00 2001 From: Tom Kuson Date: Sun, 10 Nov 2024 16:28:10 +0000 Subject: [PATCH 4/4] Remove footer id This was from before when there used to be a link to the footer. I removed that long ago, so this shouldn't be here. --- src/components/Footer.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx index 4fbfbcc..7db5afa 100644 --- a/src/components/Footer.tsx +++ b/src/components/Footer.tsx @@ -2,7 +2,7 @@ import type React from "react"; const Footer: React.FC = () => { return ( -