From a1c62598c089213dc29cfef30d6cc05d290df374 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 14 Mar 2026 19:31:01 +0000 Subject: [PATCH] Add click-to-edit percentage input on forecast cards Users can now click the percentage display to type a forecast value directly instead of only using the slider. Input is validated to accept numbers between 0 and 100. Works in both EditableForecastCard and CompetitionPropView components. https://claude.ai/code/session_01E9JPD1vMoDMGDDrFMMV7UA --- .../props/[propId]/competition-prop-view.tsx | 70 ++++++++++++++++--- .../forecast-card/editable-forecast-card.tsx | 56 +++++++++++++-- 2 files changed, 113 insertions(+), 13 deletions(-) diff --git a/app/competitions/[competitionId]/props/[propId]/competition-prop-view.tsx b/app/competitions/[competitionId]/props/[propId]/competition-prop-view.tsx index 9351191..1dd1dde 100644 --- a/app/competitions/[competitionId]/props/[propId]/competition-prop-view.tsx +++ b/app/competitions/[competitionId]/props/[propId]/competition-prop-view.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState } from "react"; +import { useState, useRef } from "react"; import { useRouter } from "next/navigation"; import Link from "next/link"; import { ChevronRight, Calendar, CalendarClock, Lock } from "lucide-react"; @@ -123,6 +123,10 @@ export function CompetitionPropView({ const isSubmitting = createForecastAction.isLoading || updateForecastAction.isLoading; + const [isEditingPercent, setIsEditingPercent] = useState(false); + const [percentInputValue, setPercentInputValue] = useState(""); + const percentInputRef = useRef(null); + const hasChanges = localForecast !== prop.user_forecast; const colors = getProbColor(localForecast); const percent = @@ -130,6 +134,30 @@ export function CompetitionPropView({ const relativeDeadline = getRelativeDeadline(prop.prop_forecasts_due_date); + const handlePercentClick = () => { + if (!isForecastingOpen) return; + setPercentInputValue(percent !== null ? String(percent) : ""); + setIsEditingPercent(true); + setTimeout(() => percentInputRef.current?.select(), 0); + }; + + const commitPercentInput = () => { + setIsEditingPercent(false); + const trimmed = percentInputValue.trim(); + if (trimmed === "") return; + const num = Number(trimmed); + if (isNaN(num) || num < 0 || num > 100) return; + setLocalForecast(Math.round(num) / 100); + }; + + const handlePercentKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter") { + commitPercentInput(); + } else if (e.key === "Escape") { + setIsEditingPercent(false); + } + }; + const handleBarClick = (e: React.MouseEvent) => { if (!isForecastingOpen) return; const rect = e.currentTarget.getBoundingClientRect(); @@ -275,14 +303,40 @@ export function CompetitionPropView({
{/* Probability display */}
-
- {percent !== null ? `${percent}%` : "—"} -
-
- {percent !== null ? "Your forecast" : "Not set"} -
+ {isEditingPercent ? ( + <> +
+ setPercentInputValue(e.target.value)} + onBlur={commitPercentInput} + onKeyDown={handlePercentKeyDown} + className="w-16 text-4xl font-bold text-center bg-transparent outline-none border-b-2 border-current" + aria-label="Forecast percentage" + /> + % +
+
Your forecast
+ + ) : ( + <> +
+ {percent !== null ? `${percent}%` : "—"} +
+
+ {percent !== null ? "Your forecast" : "Not set"} +
+ + )}
{/* Slider */} diff --git a/components/forecast-card/editable-forecast-card.tsx b/components/forecast-card/editable-forecast-card.tsx index d904064..1864bbf 100644 --- a/components/forecast-card/editable-forecast-card.tsx +++ b/components/forecast-card/editable-forecast-card.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState } from "react"; +import { useState, useRef } from "react"; import { PropWithUserForecast } from "@/types/db_types"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; @@ -96,11 +96,38 @@ export function EditableForecastCard({ const isSubmitting = createForecastAction.isLoading || updateForecastAction.isLoading; + const [isEditingPercent, setIsEditingPercent] = useState(false); + const [percentInputValue, setPercentInputValue] = useState(""); + const percentInputRef = useRef(null); + const hasChanges = localForecast !== prop.user_forecast; const colors = getProbColor(localForecast); const percent = localForecast !== null ? Math.round(localForecast * 100) : null; + const handlePercentClick = () => { + setPercentInputValue(percent !== null ? String(percent) : ""); + setIsEditingPercent(true); + setTimeout(() => percentInputRef.current?.select(), 0); + }; + + const commitPercentInput = () => { + setIsEditingPercent(false); + const trimmed = percentInputValue.trim(); + if (trimmed === "") return; + const num = Number(trimmed); + if (isNaN(num) || num < 0 || num > 100) return; + setLocalForecast(Math.round(num) / 100); + }; + + const handlePercentKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter") { + commitPercentInput(); + } else if (e.key === "Escape") { + setIsEditingPercent(false); + } + }; + const handleBarClick = (e: React.MouseEvent) => { const rect = e.currentTarget.getBoundingClientRect(); const x = e.clientX - rect.left; @@ -162,11 +189,30 @@ export function EditableForecastCard({
{/* Probability box */}
-
- {percent !== null ? `${percent}%` : "—"} -
+ {isEditingPercent ? ( +
+ setPercentInputValue(e.target.value)} + onBlur={commitPercentInput} + onKeyDown={handlePercentKeyDown} + className="w-12 text-2xl font-bold text-center bg-transparent outline-none border-b-2 border-current" + aria-label="Forecast percentage" + /> + % +
+ ) : ( +
+ {percent !== null ? `${percent}%` : "—"} +
+ )}