Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions docs/openapi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -534,6 +534,40 @@ components:
items:
type: string
description: "List of options for multiple_choice questions"
example:
- "Democratic"
- "Republican"
- "Libertarian"
- "Green"
- "Other"
all_options_ever:
type: array
items:
type: string
description: "List of all options ever for multiple_choice questions"
example:
- "Democratic"
- "Republican"
- "Libertarian"
- "Green"
- "Blue"
- "Other"
options_history:
type: array
description: "List of [iso format time, options] pairs for multiple_choice questions"
items:
type: array
items:
oneOf:
- type: string
description: "ISO 8601 timestamp when the options became active"
- type: array
items:
type: string
description: "Options list active from this timestamp onward"
example:
- ["0001-01-01T00:00:00", ["a", "b", "c", "other"]]
- ["2026-10-22T16:00:00", ["a", "b", "c", "d", "other"]]
status:
type: string
enum: [ upcoming, open, closed, resolved ]
Expand Down Expand Up @@ -1346,6 +1380,7 @@ paths:
actual_close_time: "2020-11-01T00:00:00Z"
type: "numeric"
options: null
options_history: null
status: "resolved"
resolution: "77289125.94957079"
resolution_criteria: "Resolution Criteria Copy"
Expand Down Expand Up @@ -1519,6 +1554,7 @@ paths:
actual_close_time: "2015-12-15T03:34:00Z"
type: "binary"
options: null
options_history: null
status: "resolved"
possibilities:
type: "binary"
Expand Down Expand Up @@ -1588,6 +1624,16 @@ paths:
- "Libertarian"
- "Green"
- "Other"
all_options_ever:
- "Democratic"
- "Republican"
- "Libertarian"
- "Green"
- "Blue"
- "Other"
options_history:
- ["0001-01-01T00:00:00", ["Democratic", "Republican", "Libertarian", "Other"]]
- ["2026-10-22T16:00:00", ["Democratic", "Republican", "Libertarian", "Green", "Other"]]
status: "open"
possibilities: { }
resolution: null
Expand Down
7 changes: 7 additions & 0 deletions front_end/messages/cs.json
Original file line number Diff line number Diff line change
Expand Up @@ -1823,5 +1823,12 @@
"tournamentsInfoTitle": "Jsme <predmarket>nepredikční trh</predmarket>. Můžete se účastnit zdarma a vyhrát peněžní ceny za přesnost.",
"tournamentsInfoScoringLink": "Co jsou předpovídací skóre?",
"tournamentsInfoPrizesLink": "Jak jsou rozdělovány ceny?",
"dismiss": "Zavřít",
"gracePeriodTooltip": "Pokud neaktualizujete své předpovědi před koncem této lhůty, vaše stávající předpovědi budou automaticky staženy.",
"newOptionsAddedPlural": "Tyto možnosti byly nedávno přidány, prosím upravte své předpovědi odpovídajícím způsobem.",
"newOptionsAddedSingular": "Nedávno byla přidána nová možnost, prosím upravte své předpovědi odpovídajícím způsobem.",
"showNewOptions": "Zobrazit nové možnosti",
"showNewOption": "Zobrazit novou možnost",
"timeRemaining": "Zbývající čas",
"othersCount": "Ostatní ({count})"
}
7 changes: 7 additions & 0 deletions front_end/messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,13 @@
"withdraw": "Withdraw",
"withdrawForecast": "Withdraw Forecast",
"withdrawAll": "Withdraw All",
"dismiss": "Dismiss",
"gracePeriodTooltip": "If you don't update your forecasts before the grace period ends, your existing forecasts will be automatically withdrawn.",
"newOptionsAddedPlural": "These options were recently added, please adjust your forecast(s) accordingly.",
"newOptionsAddedSingular": "A new option was recently added, please adjust your forecasts accordingly.",
"showNewOptions": "Show New Options",
"showNewOption": "Show New Option",
"timeRemaining": "Time remaining",
"saveChange": "Save Change",
"reaffirm": "Reaffirm",
"reaffirmAll": "Reaffirm All",
Expand Down
7 changes: 7 additions & 0 deletions front_end/messages/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -1823,5 +1823,12 @@
"tournamentsInfoTitle": "Nosotros <predmarket>no somos un mercado de predicciones</predmarket>. Puedes participar gratis y ganar premios en efectivo por ser preciso.",
"tournamentsInfoScoringLink": "¿Qué son las puntuaciones de predicción?",
"tournamentsInfoPrizesLink": "¿Cómo se distribuyen los premios?",
"dismiss": "Descartar",
"gracePeriodTooltip": "Si no actualiza sus pronósticos antes de que termine el período de gracia, sus pronósticos existentes se retirarán automáticamente.",
"newOptionsAddedPlural": "Estas opciones se añadieron recientemente, por favor ajuste su(s) pronóstico(s) en consecuencia.",
"newOptionsAddedSingular": "Se añadió una nueva opción recientemente, por favor ajuste sus pronósticos en consecuencia.",
"showNewOptions": "Mostrar nuevas opciones",
"showNewOption": "Mostrar nueva opción",
"timeRemaining": "Tiempo restante",
"othersCount": "Otros ({count})"
}
7 changes: 7 additions & 0 deletions front_end/messages/pt.json
Original file line number Diff line number Diff line change
Expand Up @@ -1821,5 +1821,12 @@
"tournamentsInfoTitle": "Nós <predmarket>não somos um mercado de previsões</predmarket>. Você pode participar gratuitamente e ganhar prêmios em dinheiro por ser preciso.",
"tournamentsInfoScoringLink": "O que são pontuações de previsão?",
"tournamentsInfoPrizesLink": "Como os prêmios são distribuídos?",
"dismiss": "Dispensar",
"gracePeriodTooltip": "Se você não atualizar suas previsões antes do término do período de carência, suas previsões existentes serão retiradas automaticamente.",
"newOptionsAddedPlural": "Essas opções foram adicionadas recentemente, por favor ajuste suas previsões de acordo.",
"newOptionsAddedSingular": "Uma nova opção foi adicionada recentemente, por favor ajuste suas previsões de acordo.",
"showNewOptions": "Mostrar Novas Opções",
"showNewOption": "Mostrar Nova Opção",
"timeRemaining": "Tempo restante",
"othersCount": "Outros ({count})"
}
7 changes: 7 additions & 0 deletions front_end/messages/zh-TW.json
Original file line number Diff line number Diff line change
Expand Up @@ -1820,5 +1820,12 @@
"tournamentsInfoTitle": "我們 <predmarket>不是預測市場</predmarket>。您可以免費參加並因精確的預測贏取現金獎勵。",
"tournamentsInfoScoringLink": "什麼是預測得分?",
"tournamentsInfoPrizesLink": "獎品如何分配?",
"dismiss": "關閉",
"gracePeriodTooltip": "如果您在寬限期間結束之前未更新您的預測,您的現有預測將自動撤回。",
"newOptionsAddedPlural": "這些選項最近新增,請相應調整您的預測。",
"newOptionsAddedSingular": "一個新選項最近新增,請相應調整您的預測。",
"showNewOptions": "顯示新選項",
"showNewOption": "顯示新選項",
"timeRemaining": "剩餘時間",
"withdrawAfterPercentSetting2": "問題總生命周期後撤回"
}
7 changes: 7 additions & 0 deletions front_end/messages/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -1825,5 +1825,12 @@
"tournamentsInfoTitle": "我们<predmarket>不是一个预测市场</predmarket>。您可以免费参与,并因精准的预测赢得现金奖品。",
"tournamentsInfoScoringLink": "什么是预测分数?",
"tournamentsInfoPrizesLink": "奖品如何分配?",
"dismiss": "忽略",
"gracePeriodTooltip": "如果您在宽限期结束前没有更新您的预测,现有的预测将自动撤回。",
"newOptionsAddedPlural": "这些选项是最近添加的,请相应调整您的预测。",
"newOptionsAddedSingular": "最近添加了一个新选项,请相应调整您的预测。",
"showNewOptions": "显示新选项",
"showNewOption": "显示新选项",
"timeRemaining": "剩余时间",
"othersCount": "其他({count})"
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,10 @@ import { SearchParams } from "@/types/navigation";
import { Post, PostWithForecasts } from "@/types/post";
import { QuestionType, QuestionWithForecasts } from "@/types/question";
import { logError } from "@/utils/core/errors";
import { parseQuestionId } from "@/utils/questions/helpers";
import {
getAllOptionsHistory,
parseQuestionId,
} from "@/utils/questions/helpers";

import { AggregationWrapper } from "./aggregation_wrapper";
import { AggregationExtraMethod } from "../types";
Expand Down Expand Up @@ -417,8 +420,9 @@ function parseSubQuestions(
},
];
} else if (data.question?.type === QuestionType.MultipleChoice) {
const allOptions = getAllOptionsHistory(data.question);
return (
data.question.options?.map((option) => ({
allOptions?.map((option) => ({
value: option,
label: option,
})) || []
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,16 @@ const QuestionHeaderCPStatus: FC<Props> = ({
const t = useTranslations();
const { hideCP } = useHideCP();
const forecastAvailability = getQuestionForecastAvailability(question);
const continuousAreaChartData = getContinuousAreaChartData({
question,
isClosed: question.status === QuestionStatus.CLOSED,
});
const isContinuous =
question.type === QuestionType.Numeric ||
question.type === QuestionType.Discrete ||
question.type === QuestionType.Date;
const continuousAreaChartData = !isContinuous
? null
: getContinuousAreaChartData({
question,
isClosed: question.status === QuestionStatus.CLOSED,
});

if (question.status === QuestionStatus.RESOLVED && question.resolution) {
// Resolved/Annulled/Ambiguous
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ const HORIZONTAL_PADDING = 10;

type Props = {
question: Question | GraphingQuestionProps;
data: ContinuousAreaGraphInput;
data: ContinuousAreaGraphInput | null;
height?: number;
width?: number;
extraTheme?: VictoryThemeDefinition;
Expand All @@ -81,6 +81,9 @@ const MinifiedContinuousAreaChart: FC<Props> = ({
forceTickCount,
variant = "feed",
}) => {
if (data === null) {
throw new Error("Data for MinifiedContinuousAreaChart is null");
}
const { ref: chartContainerRef, width: containerWidth } =
useContainerSize<HTMLDivElement>();
const chartWidth = width || containerWidth;
Expand Down
72 changes: 66 additions & 6 deletions front_end/src/components/forecast_maker/forecast_choice_option.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,14 @@ import { getForecastPctDisplayValue } from "@/utils/formatters/prediction";
import ForecastTextInput from "./forecast_text_input";
import Tooltip from "../ui/tooltip";

// ============================================
// ANIMATION & OPACITY SETTINGS - ADJUST HERE
// ============================================
const GRADIENT_OPACITY_NORMAL = "1A"; // Normal state: ~10% (hex)
const GRADIENT_OPACITY_HOVER = "2D"; // Hover state: ~18% (hex)
const BORDER_WIDTH = "4px"; // Border width when animating
export const ANIMATION_DURATION_MS = 1500; // Total animation duration in milliseconds

type OptionResolution = {
resolution: Resolution | null;
type: "question" | "group_question";
Expand All @@ -52,6 +60,11 @@ type Props<T> = {
onOptionClick?: (id: T) => void;
withdrawn?: boolean;
withdrawnEndTimeSec?: number | null;
isNewOption?: boolean;
showHighlight?: boolean;
isAnimating?: boolean;
onInteraction?: () => void;
rowRef?: React.RefObject<HTMLTableRowElement | null>;
};

const ForecastChoiceOption = <T = string,>({
Expand All @@ -73,9 +86,20 @@ const ForecastChoiceOption = <T = string,>({
onOptionClick,
withdrawn = false,
withdrawnEndTimeSec = null,
isNewOption = false,
showHighlight = false,
isAnimating = false,
onInteraction,
rowRef,
}: Props<T>) => {
const t = useTranslations();
const locale = useLocale();
const [isHovered, setIsHovered] = useState(false);
const [mounted, setMounted] = useState(false);

useEffect(() => {
setMounted(true);
}, []);

const inputDisplayValue =
withdrawn && !isDirty
Expand Down Expand Up @@ -124,8 +148,9 @@ const ForecastChoiceOption = <T = string,>({
const handleSliderForecastChange = useCallback(
(value: number) => {
onChange(id, value);
onInteraction?.();
},
[id, onChange]
[id, onChange, onInteraction]
);
const handleInputChange = useCallback((value: string) => {
setInputValue(value);
Expand Down Expand Up @@ -181,16 +206,35 @@ const ForecastChoiceOption = <T = string,>({
</div>
);

const gradientColor = getThemeColor(choiceColor);

return (
<>
<tr
className={cn({
ref={rowRef}
className={cn("relative transition-all duration-300 ease-in-out", {
"bg-orange-200 dark:bg-orange-200-dark": isRowDirty,
"bg-blue-200 dark:bg-blue-200-dark": highlightedOptionId === id,
"bg-gradient-to-r from-purple-200 to-gray-0 dark:from-purple-200-dark dark:to-gray-0-dark":
isQuestionResolved || isGroupResolutionHighlighted,
})}
onClick={() => onOptionClick?.(id)}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
style={{
...(mounted &&
showHighlight && {
backgroundImage: `linear-gradient(to right, ${gradientColor}${isHovered ? GRADIENT_OPACITY_HOVER : GRADIENT_OPACITY_NORMAL} 0%, transparent 100%)`,
}),
...(mounted &&
isNewOption && {
outline: isAnimating
? `${BORDER_WIDTH} solid ${gradientColor}`
: "0px solid transparent",
outlineOffset: "-4px",
transition: `outline ${ANIMATION_DURATION_MS * 0.2}ms ease-in-out`,
}),
}}
>
<th className="w-full border-t border-gray-300 px-3 py-2 text-left text-sm font-medium leading-6 dark:border-gray-300-dark sm:w-auto sm:min-w-[10rem] sm:text-base">
<div className="flex gap-2">
Expand Down Expand Up @@ -228,6 +272,7 @@ const ForecastChoiceOption = <T = string,>({
onFocus={() => {
setIsInputFocused(true);
onChange(id, defaultSliderValue);
onInteraction?.();
}}
onBlur={() => setIsInputFocused(false)}
disabled={disabled}
Expand All @@ -249,7 +294,10 @@ const ForecastChoiceOption = <T = string,>({
minValue={inputMin}
maxValue={inputMax}
value={inputValue}
onFocus={() => setIsInputFocused(true)}
onFocus={() => {
setIsInputFocused(true);
onInteraction?.();
}}
onBlur={() => setIsInputFocused(false)}
disabled={disabled}
/>
Expand All @@ -263,10 +311,22 @@ const ForecastChoiceOption = <T = string,>({
</td>
</tr>
<tr
className={cn("sm:hidden", {
"bg-orange-200 dark:bg-orange-200-dark": isRowDirty,
})}
className={cn(
"relative transition-all duration-300 ease-in-out sm:hidden",
{
"bg-orange-200 dark:bg-orange-200-dark": isRowDirty,
}
)}
onClick={() => onOptionClick?.(id)}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
style={
mounted && showHighlight
? {
backgroundImage: `linear-gradient(to right, ${gradientColor}${isHovered ? GRADIENT_OPACITY_HOVER : GRADIENT_OPACITY_NORMAL} 0%, transparent 100%)`,
}
: undefined
}
>
<td
className={cn(
Expand Down
Loading