From 5be5c4a6cbef759682716a4d97c938b92801d224 Mon Sep 17 00:00:00 2001 From: Nikita Date: Thu, 4 Dec 2025 11:08:44 +0200 Subject: [PATCH 01/26] feat: embed modal sizing --- .../[id]/components/question_embed_modal.tsx | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/front_end/src/app/(main)/questions/[id]/components/question_embed_modal.tsx b/front_end/src/app/(main)/questions/[id]/components/question_embed_modal.tsx index 91b0e7b249..c65f85c29e 100644 --- a/front_end/src/app/(main)/questions/[id]/components/question_embed_modal.tsx +++ b/front_end/src/app/(main)/questions/[id]/components/question_embed_modal.tsx @@ -2,9 +2,11 @@ import { FC } from "react"; import EmbedModal from "@/components/embed_modal"; +import { ContinuousQuestionTypes } from "@/constants/questions"; import useEmbedModalContext from "@/contexts/embed_modal_context"; import { useEmbedUrl } from "@/hooks/share"; import { QuestionType } from "@/types/question"; + type Props = { postId: number; postTitle?: string; @@ -19,12 +21,20 @@ const QuestionEmbedModal: FC = ({ postId, postTitle, questionType }) => { return null; } + const isBinaryOrContinuous = + !!questionType && + (questionType === QuestionType.Binary || + ContinuousQuestionTypes.some((type) => type === questionType)); + + const embedWidth = 550; + const embedHeight = isBinaryOrContinuous ? 360 : 270; + return ( Date: Thu, 4 Dec 2025 16:48:51 +0200 Subject: [PATCH 02/26] feat: set up embed screen --- .../questions/assets/metaculus-dark.png | Bin 0 -> 2990 bytes .../questions/assets/metaculus-light.png | Bin 0 -> 3021 bytes .../questions/components/embed_screen.tsx | 179 ++++++++++++++++++ .../app/(embed)/questions/embed/[id]/page.tsx | 94 ++------- .../questions/[id]/image-preview/route.ts | 2 +- 5 files changed, 195 insertions(+), 80 deletions(-) create mode 100644 front_end/src/app/(embed)/questions/assets/metaculus-dark.png create mode 100644 front_end/src/app/(embed)/questions/assets/metaculus-light.png create mode 100644 front_end/src/app/(embed)/questions/components/embed_screen.tsx diff --git a/front_end/src/app/(embed)/questions/assets/metaculus-dark.png b/front_end/src/app/(embed)/questions/assets/metaculus-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..4c6ee1b192ae20d22a169ddc62233929c941d5ea GIT binary patch literal 2990 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 literal 0 HcmV?d00001 diff --git a/front_end/src/app/(embed)/questions/assets/metaculus-light.png b/front_end/src/app/(embed)/questions/assets/metaculus-light.png new file mode 100644 index 0000000000000000000000000000000000000000..a31595da2118e05b3c5f068f3e7d4723d694d11a GIT binary patch literal 3021 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 550) { + return { width: 550, height: 360 }; + } + if (containerWidth >= 440) { + return { width: 440, height: 360 }; + } + return { width: 360, height: 360 }; +} + +function getOtherSize(containerWidth: number): EmbedSize { + if (containerWidth >= 550) { + return { width: 550, height: 270 }; + } + if (containerWidth >= 401) { + return { width: 401, height: 270 }; + } + if (containerWidth >= 360) { + return { width: 400, height: 360 }; + } + return { width: 360, height: 360 }; +} + +function getSizeForPost(post: PostWithForecasts, containerWidth: number) { + const isBinaryOrContinuous = + !!post.question && + (post.question.type === QuestionType.Binary || + ContinuousQuestionTypes.some((t) => t === post.question?.type)); + + return isBinaryOrContinuous + ? getBinaryContinuousSize(containerWidth) + : getOtherSize(containerWidth); +} + +const EmbedScreen: React.FC = ({ post, targetWidth, targetHeight }) => { + const frameRef = useRef(null); + + const [size, setSize] = useState(() => + getSizeForPost(post, MIN_EMBED_WIDTH) + ); + + useEffect(() => { + if (!frameRef.current) return; + + const el = frameRef.current; + + const observer = new ResizeObserver((entries) => { + const entry = entries[0]; + const rawWidth = entry?.contentRect.width ?? MIN_EMBED_WIDTH; + const effectiveWidth = Math.max(rawWidth, MIN_EMBED_WIDTH); + + setSize(getSizeForPost(post, effectiveWidth)); + }); + + observer.observe(el); + + return () => observer.disconnect(); + }, [post]); + + const ogMode = + typeof targetWidth === "number" && typeof targetHeight === "number"; + + const baseWidth = size.width || MIN_EMBED_WIDTH; + const baseHeight = size.height || MIN_EMBED_WIDTH; + + const scale = ogMode && baseHeight > 0 ? targetHeight / baseHeight : 1; + + const frameStyle: React.CSSProperties = ogMode + ? { + width: targetWidth, + height: targetHeight, + minWidth: MIN_EMBED_WIDTH, + boxSizing: "border-box", + display: "flex", + alignItems: "center", + justifyContent: "center", + overflow: "hidden", + } + : { + width: baseWidth, + height: baseHeight, + minWidth: MIN_EMBED_WIDTH, + minHeight: baseHeight, + boxSizing: "border-box", + }; + + return ( +
+
+
+
+
+
+ + +
+ +
+ Metaculus Logo + Metaculus Logo +
+
+
+
+
+ ); +}; + +export default EmbedScreen; diff --git a/front_end/src/app/(embed)/questions/embed/[id]/page.tsx b/front_end/src/app/(embed)/questions/embed/[id]/page.tsx index 8831fcc583..a786c045b5 100644 --- a/front_end/src/app/(embed)/questions/embed/[id]/page.tsx +++ b/front_end/src/app/(embed)/questions/embed/[id]/page.tsx @@ -1,21 +1,12 @@ -import Link from "next/link"; -import { getTranslations } from "next-intl/server"; - -import ForecastCard from "@/components/forecast_card"; -import CommunityDisclaimer from "@/components/post_card/community_disclaimer"; -import { - CHART_TYPE_PARAM, - EMBED_QUESTION_TITLE, - GRAPH_ZOOM_PARAM, - HIDE_ZOOM_PICKER, -} from "@/constants/global_search_params"; import ServerPostsApi from "@/services/api/posts/posts.server"; -import { EmbedChartType, TimelineChartZoomOption } from "@/types/charts"; import { SearchParams } from "@/types/navigation"; + +import EmbedScreen from "../../components/embed_screen"; + import "./styles.scss"; -import { TournamentType } from "@/types/projects"; -import { getEmbedTheme } from "../../helpers/embed_theme"; +const OG_WIDTH = 1200; +const OG_HEIGHT = 630; export default async function GenerateQuestionPreview(props: { params: Promise<{ id: number }>; @@ -23,76 +14,21 @@ export default async function GenerateQuestionPreview(props: { }) { const searchParams = await props.searchParams; const params = await props.params; - const t = await getTranslations(); const post = await ServerPostsApi.getPostAnonymous(params.id); if (!post) { return null; } - const isCommunityQuestion = - post.projects.default_project.type === TournamentType.Community; + const isOgCapture = searchParams["og"] === "1"; - const embedTheme = getEmbedTheme( - searchParams["embed_theme"], - searchParams["css_variables"] - ); - const nonInteractiveParam = searchParams["non-interactive"]; - - const chartZoomParam = searchParams[GRAPH_ZOOM_PARAM]; - let chartZoom: TimelineChartZoomOption | undefined = undefined; - if (typeof chartZoomParam === "string") { - chartZoom = - (chartZoomParam as TimelineChartZoomOption) ?? - TimelineChartZoomOption.TwoMonths; - } - - const hideZoomPickerParam = searchParams[HIDE_ZOOM_PICKER]; - const embedTitle = searchParams[EMBED_QUESTION_TITLE] as string | undefined; - const chartTypeParam = searchParams[CHART_TYPE_PARAM] as - | EmbedChartType - | undefined; - - return ( -
- {isCommunityQuestion && ( - - )} - -
-

- {t("forecastDisclaimer", { - predictionCount: post.forecasts_count ?? 0, - forecasterCount: post.nr_forecasters, - })} -

- - {t("metaculus")} - -
-
- ); + ); + } + + return ; } diff --git a/front_end/src/app/(main)/questions/[id]/image-preview/route.ts b/front_end/src/app/(main)/questions/[id]/image-preview/route.ts index 1121596741..663dba5573 100644 --- a/front_end/src/app/(main)/questions/[id]/image-preview/route.ts +++ b/front_end/src/app/(main)/questions/[id]/image-preview/route.ts @@ -18,7 +18,7 @@ export async function GET( const theme = request.nextUrl.searchParams.get("theme") ?? "dark"; const { PUBLIC_APP_URL } = getPublicSettings(); - const imageUrl = `${PUBLIC_APP_URL}/questions/embed/${id}/?${ENFORCED_THEME_PARAM}=${theme}&${HIDE_ZOOM_PICKER}=true&non-interactive=true`; + const imageUrl = `${PUBLIC_APP_URL}/questions/embed/${id}/?${ENFORCED_THEME_PARAM}=${theme}&${HIDE_ZOOM_PICKER}=true&non-interactive=true&og=1`; const payload = { url: imageUrl, From a47ad4d796ad316a0065dc7cbf6073f3cec33fc3 Mon Sep 17 00:00:00 2001 From: Nikita Date: Fri, 5 Dec 2025 16:46:03 +0200 Subject: [PATCH 03/26] feat: add basic binary question card support --- .../components/embed_question_card.tsx | 26 ++++ .../components/embed_question_footer.tsx | 53 +++++++ .../components/embed_question_header.tsx | 36 +++++ .../components/embed_question_plot.tsx | 13 ++ .../questions/components/embed_screen.tsx | 41 +----- .../components/question_view_mode_context.tsx | 26 ++++ .../components/truncatable_question_title.tsx | 134 ++++++++++++++++++ .../question_view/shared/question_title.tsx | 17 ++- .../src/components/charts/numeric_chart.tsx | 19 ++- .../charts/primitives/chart_container.tsx | 19 ++- .../consumer_post_card/binary_cp_bar.tsx | 25 +++- .../continuous_chart_card.tsx | 8 +- 12 files changed, 355 insertions(+), 62 deletions(-) create mode 100644 front_end/src/app/(embed)/questions/components/embed_question_card.tsx create mode 100644 front_end/src/app/(embed)/questions/components/embed_question_footer.tsx create mode 100644 front_end/src/app/(embed)/questions/components/embed_question_header.tsx create mode 100644 front_end/src/app/(embed)/questions/components/embed_question_plot.tsx create mode 100644 front_end/src/app/(embed)/questions/components/question_view_mode_context.tsx create mode 100644 front_end/src/app/(embed)/questions/components/truncatable_question_title.tsx diff --git a/front_end/src/app/(embed)/questions/components/embed_question_card.tsx b/front_end/src/app/(embed)/questions/components/embed_question_card.tsx new file mode 100644 index 0000000000..8f575119f0 --- /dev/null +++ b/front_end/src/app/(embed)/questions/components/embed_question_card.tsx @@ -0,0 +1,26 @@ +import { Fragment } from "react"; + +import { PostWithForecasts } from "@/types/post"; + +import EmbedQuestionFooter from "./embed_question_footer"; +import EmbedQuestionHeader from "./embed_question_header"; +import EmbedQuestionPlot from "./embed_question_plot"; +import { QuestionViewModeProvider } from "./question_view_mode_context"; + +type Props = { + post: PostWithForecasts; +}; + +const EmbedQuestionCard: React.FC = ({ post }) => { + return ( + + + + + + + + ); +}; + +export default EmbedQuestionCard; diff --git a/front_end/src/app/(embed)/questions/components/embed_question_footer.tsx b/front_end/src/app/(embed)/questions/components/embed_question_footer.tsx new file mode 100644 index 0000000000..e044577464 --- /dev/null +++ b/front_end/src/app/(embed)/questions/components/embed_question_footer.tsx @@ -0,0 +1,53 @@ +"use client"; + +import Image from "next/image"; +import React from "react"; + +import ForecastersCounter from "@/app/(main)/questions/components/forecaster_counter"; +import CommentStatus from "@/components/post_card/basic_post_card/comment_status"; +import { PostWithForecasts } from "@/types/post"; + +import metaculusDarkLogo from "../assets/metaculus-dark.png"; +import metaculusLightLogo from "../assets/metaculus-light.png"; + +type Props = { + post: PostWithForecasts; +}; + +const EmbedQuestionFooter: React.FC = ({ post }) => { + return ( +
+
+ + +
+ +
+ Metaculus Logo + Metaculus Logo +
+
+ ); +}; + +export default EmbedQuestionFooter; diff --git a/front_end/src/app/(embed)/questions/components/embed_question_header.tsx b/front_end/src/app/(embed)/questions/components/embed_question_header.tsx new file mode 100644 index 0000000000..ede70be777 --- /dev/null +++ b/front_end/src/app/(embed)/questions/components/embed_question_header.tsx @@ -0,0 +1,36 @@ +import QuestionHeaderCPStatus from "@/app/(main)/questions/[id]/components/question_view/forecaster_question_view/question_header/question_header_cp_status"; +import { PostWithForecasts } from "@/types/post"; +import { QuestionWithForecasts } from "@/types/question"; +import { + isContinuousQuestion, + isQuestionPost, +} from "@/utils/questions/helpers"; + +import TruncatableQuestionTitle from "./truncatable_question_title"; + +type Props = { + post: PostWithForecasts; +}; + +const EmbedQuestionHeader: React.FC = ({ post }) => { + return ( +
+ + {post.title} + + {isQuestionPost(post) && ( + + )} +
+ ); +}; + +export default EmbedQuestionHeader; diff --git a/front_end/src/app/(embed)/questions/components/embed_question_plot.tsx b/front_end/src/app/(embed)/questions/components/embed_question_plot.tsx new file mode 100644 index 0000000000..dfd54815bb --- /dev/null +++ b/front_end/src/app/(embed)/questions/components/embed_question_plot.tsx @@ -0,0 +1,13 @@ +import DetailedQuestionCard from "@/components/detailed_question_card/detailed_question_card"; +import { PostWithForecasts } from "@/types/post"; +import { isQuestionPost } from "@/utils/questions/helpers"; + +type Props = { + post: PostWithForecasts; +}; + +const EmbedQuestionPlot: React.FC = ({ post }) => { + return <>{isQuestionPost(post) && }; +}; + +export default EmbedQuestionPlot; diff --git a/front_end/src/app/(embed)/questions/components/embed_screen.tsx b/front_end/src/app/(embed)/questions/components/embed_screen.tsx index 76a1210949..cc6b0255d0 100644 --- a/front_end/src/app/(embed)/questions/components/embed_screen.tsx +++ b/front_end/src/app/(embed)/questions/components/embed_screen.tsx @@ -1,17 +1,13 @@ "use client"; -import Image from "next/image"; import React, { useEffect, useRef, useState } from "react"; -import ForecastersCounter from "@/app/(main)/questions/components/forecaster_counter"; -import CommentStatus from "@/components/post_card/basic_post_card/comment_status"; import { ContinuousQuestionTypes } from "@/constants/questions"; import { PostWithForecasts } from "@/types/post"; import { QuestionType } from "@/types/question"; import cn from "@/utils/core/cn"; -import metaculusDarkLogo from "../assets/metaculus-dark.png"; -import metaculusLightLogo from "../assets/metaculus-light.png"; +import EmbedQuestionCard from "./embed_question_card"; type Props = { post: PostWithForecasts; @@ -127,7 +123,7 @@ const EmbedScreen: React.FC = ({ post, targetWidth, targetHeight }) => { style={frameStyle} >
= ({ post, targetWidth, targetHeight }) => { transformOrigin: "center center", }} > -
-
-
- - -
- -
- Metaculus Logo - Metaculus Logo -
-
+
diff --git a/front_end/src/app/(embed)/questions/components/question_view_mode_context.tsx b/front_end/src/app/(embed)/questions/components/question_view_mode_context.tsx new file mode 100644 index 0000000000..77ceca1eda --- /dev/null +++ b/front_end/src/app/(embed)/questions/components/question_view_mode_context.tsx @@ -0,0 +1,26 @@ +"use client"; + +import React, { createContext, useContext } from "react"; + +export type QuestionViewMode = "default" | "embed"; + +const QuestionViewModeContext = createContext("default"); + +type ProviderProps = { + mode?: QuestionViewMode; + children: React.ReactNode; +}; + +export const QuestionViewModeProvider: React.FC = ({ + mode = "default", + children, +}) => ( + + {children} + +); + +export const useQuestionViewMode = () => useContext(QuestionViewModeContext); + +export const useIsEmbedMode = () => + useContext(QuestionViewModeContext) === "embed"; diff --git a/front_end/src/app/(embed)/questions/components/truncatable_question_title.tsx b/front_end/src/app/(embed)/questions/components/truncatable_question_title.tsx new file mode 100644 index 0000000000..5ee3ad1a1e --- /dev/null +++ b/front_end/src/app/(embed)/questions/components/truncatable_question_title.tsx @@ -0,0 +1,134 @@ +"use client"; + +import React, { HTMLAttributes, useEffect, useRef, useState } from "react"; + +import QuestionTitle from "@/app/(main)/questions/[id]/components/question_view/shared/question_title"; +import cn from "@/utils/core/cn"; + +type TruncatableQuestionTitleProps = HTMLAttributes & { + maxLines?: number; + revealOnHoverOrTap?: boolean; +}; + +const TruncatableQuestionTitle: React.FC = ({ + children, + className, + style, + maxLines = 4, + revealOnHoverOrTap = true, + onMouseEnter, + onMouseLeave, + onFocus, + onBlur, + onClick, + ...rest +}) => { + const [expanded, setExpanded] = useState(false); + const [isTruncated, setIsTruncated] = useState(false); + const titleRef = useRef(null); + + const hasClamp = maxLines > 0; + + const clampStyle: React.CSSProperties = hasClamp + ? { + display: "-webkit-box", + WebkitLineClamp: maxLines, + WebkitBoxOrient: "vertical", + overflow: "hidden", + } + : {}; + + useEffect(() => { + const el = titleRef.current; + if (!el || !hasClamp) { + setIsTruncated(false); + return; + } + + const checkTruncation = () => { + const truncated = el.scrollHeight > el.clientHeight + 1; + setIsTruncated(truncated); + }; + + checkTruncation(); + + const observer = new ResizeObserver(() => { + checkTruncation(); + }); + + observer.observe(el); + return () => observer.disconnect(); + }, [children, maxLines, hasClamp]); + + const handleRevealOn = () => { + if (!revealOnHoverOrTap || !isTruncated) return; + setExpanded(true); + }; + + const handleRevealOff = () => { + if (!revealOnHoverOrTap || !isTruncated) return; + setExpanded(false); + }; + + const handleToggleClick = () => { + if (!revealOnHoverOrTap || !isTruncated) return; + setExpanded((prev) => !prev); + }; + + const showOverlay = revealOnHoverOrTap && isTruncated && expanded; + + return ( +
{ + onMouseEnter?.(e); + handleRevealOn(); + }} + onMouseLeave={(e) => { + onMouseLeave?.(e); + handleRevealOff(); + }} + onFocus={(e) => { + onFocus?.(e); + handleRevealOn(); + }} + onBlur={(e) => { + onBlur?.(e); + handleRevealOff(); + }} + onClick={(e) => { + onClick?.(e); + handleToggleClick(); + }} + > + + {children} + + + {showOverlay && ( + <> +
+ + {children} + +
+ +
+ + )} +
+ ); +}; + +export default TruncatableQuestionTitle; diff --git a/front_end/src/app/(main)/questions/[id]/components/question_view/shared/question_title.tsx b/front_end/src/app/(main)/questions/[id]/components/question_view/shared/question_title.tsx index 8556f72292..ac8a343a63 100644 --- a/front_end/src/app/(main)/questions/[id]/components/question_view/shared/question_title.tsx +++ b/front_end/src/app/(main)/questions/[id]/components/question_view/shared/question_title.tsx @@ -1,14 +1,14 @@ -import { HTMLAttributes } from "react"; +import React, { forwardRef, HTMLAttributes } from "react"; import cn from "@/utils/core/cn"; -const QuestionTitle: React.FC> = ({ - children, - className, - ...props -}) => { +const QuestionTitle = forwardRef< + HTMLHeadingElement, + HTMLAttributes +>(({ children, className, ...props }, ref) => { return (

> = ({ {children}

); -}; +}); + +QuestionTitle.displayName = "QuestionTitle"; + export default QuestionTitle; diff --git a/front_end/src/components/charts/numeric_chart.tsx b/front_end/src/components/charts/numeric_chart.tsx index a894ae3989..869893cc0b 100644 --- a/front_end/src/components/charts/numeric_chart.tsx +++ b/front_end/src/components/charts/numeric_chart.tsx @@ -135,6 +135,13 @@ const NumericChart: FC = ({ const [isCursorActive, setIsCursorActive] = useState(false); const { ref: chartContainerRef, width: chartWidth } = useContainerSize(); + + const baseHeight = height ?? 170; + const chartHeight = useMemo(() => { + if (!isEmbedded || !chartWidth) return baseHeight; + const target = chartWidth * 0.4; + return Math.round(Math.max(120, Math.min(178, target))); + }, [isEmbedded, chartWidth, baseHeight]); const { line, area, points, yDomain, xDomain, yScale, xScale } = useMemo( () => buildChartData(chartWidth, zoom), [chartWidth, zoom, buildChartData] @@ -261,7 +268,7 @@ const NumericChart: FC = ({ cursorLabelComponent={ = ({ }, [ defaultCursor, xScale, - height, + chartHeight, hasExternalTheme, getThemeColor, handleCursorChange, @@ -428,7 +435,7 @@ const NumericChart: FC = ({ = ({ = ({ ticks: { stroke: "transparent" }, axis: { stroke: "transparent" }, }} - offsetY={isEmbedded ? 0 : BOTTOM_PADDING} + offsetY={isEmbedded ? BOTTOM_PADDING - 5 : BOTTOM_PADDING} tickValues={xScale.ticks} tickFormat={ hideCP diff --git a/front_end/src/components/charts/primitives/chart_container.tsx b/front_end/src/components/charts/primitives/chart_container.tsx index a7b7c17806..9f82374f58 100644 --- a/front_end/src/components/charts/primitives/chart_container.tsx +++ b/front_end/src/components/charts/primitives/chart_container.tsx @@ -3,6 +3,7 @@ import { Tab, TabGroup, TabList } from "@headlessui/react"; import { isNil } from "lodash"; import { forwardRef, Fragment, PropsWithChildren, useState } from "react"; +import { useIsEmbedMode } from "@/app/(embed)/questions/components/question_view_mode_context"; import { TimelineChartZoomOption } from "@/types/charts"; import { getChartZoomOptions } from "@/utils/charts/helpers"; import cn from "@/utils/core/cn"; @@ -31,12 +32,22 @@ const ChartContainer = forwardRef>( } }; + const isEmbed = useIsEmbedMode(); + return (
{(!!chartTitle || !!zoom) && ( -
+
{!!chartTitle && ( -
+
{chartTitle}
)} @@ -60,7 +71,9 @@ const ChartContainer = forwardRef>( { "bg-gray-300 dark:bg-gray-300-dark": hover || selected, - } + }, + isEmbed && + "uppercase text-gray-600 dark:text-gray-600-dark md:text-xs" )} > {option.label} diff --git a/front_end/src/components/consumer_post_card/binary_cp_bar.tsx b/front_end/src/components/consumer_post_card/binary_cp_bar.tsx index 023e958d60..ff4d279b11 100644 --- a/front_end/src/components/consumer_post_card/binary_cp_bar.tsx +++ b/front_end/src/components/consumer_post_card/binary_cp_bar.tsx @@ -2,6 +2,7 @@ import { isNil } from "lodash"; import { useTranslations } from "next-intl"; import { FC, useId } from "react"; +import { useIsEmbedMode } from "@/app/(embed)/questions/components/question_view_mode_context"; import { useHideCP } from "@/contexts/cp_context"; import { QuestionStatus } from "@/types/post"; import { QuestionType, QuestionWithNumericForecasts } from "@/types/question"; @@ -18,6 +19,7 @@ const BinaryCPBar: FC = ({ question, size = "md", className }) => { const t = useTranslations(); const { hideCP } = useHideCP(); const gradientId = useId(); + const isEmbed = useIsEmbedMode(); const questionCP = question.aggregations[question.default_aggregation_method]?.latest @@ -34,10 +36,10 @@ const BinaryCPBar: FC = ({ question, size = "md", className }) => { ? Math.round((questionCP as number) * 1000) / 10 : null; - const width = 112; - const height = 66; - const strokeWidth = 12; - const strokeCursorWidth = 17; + const width = isEmbed ? 85 : 112; + const height = isEmbed ? 50 : 66; + const strokeWidth = isEmbed ? 8 : 12; + const strokeCursorWidth = isEmbed ? 12 : 17; const radius = (width - strokeWidth) / 2; const arcAngle = Math.PI * 1.1; const center = { x: width / 2, y: height - strokeWidth }; @@ -83,6 +85,7 @@ const BinaryCPBar: FC = ({ question, size = "md", className }) => { "scale-100": size === "md", "mb-4 scale-[1.25]": size === "lg", }, + isEmbed && "scale-100", className )} > @@ -160,15 +163,23 @@ const BinaryCPBar: FC = ({ question, size = "md", className }) => { {cpPercentage != null ? `${cpPercentage}%` : "%"} {t("chance")} diff --git a/front_end/src/components/detailed_question_card/detailed_question_card/continuous_chart_card.tsx b/front_end/src/components/detailed_question_card/detailed_question_card/continuous_chart_card.tsx index c783436e93..1a09813dba 100644 --- a/front_end/src/components/detailed_question_card/detailed_question_card/continuous_chart_card.tsx +++ b/front_end/src/components/detailed_question_card/detailed_question_card/continuous_chart_card.tsx @@ -1,8 +1,9 @@ "use client"; import { isNil } from "lodash"; import { useTranslations } from "next-intl"; -import React, { FC, ReactNode, useCallback, useMemo, useState } from "react"; +import { FC, ReactNode, useCallback, useMemo, useState } from "react"; +import { useIsEmbedMode } from "@/app/(embed)/questions/components/question_view_mode_context"; import QuestionHeaderCPStatus from "@/app/(main)/questions/[id]/components/question_view/forecaster_question_view/question_header/question_header_cp_status"; import NumericTimeline from "@/components/charts/numeric_timeline"; import QuestionPredictionTooltip from "@/components/charts/primitives/question_prediction_tooltip"; @@ -180,6 +181,8 @@ const DetailedContinuousChartCard: FC = ({ question.status, ]); + const isEmbed = useIsEmbedMode(); + return (
= ({ : cursorTooltip } isConsumerView={isConsumerView} + isEmbedded={isEmbed} />
@@ -281,6 +285,7 @@ const DetailedContinuousChartCard: FC = ({ : cursorTooltip } isConsumerView={isConsumerView} + isEmbedded={isEmbed} />
@@ -323,6 +328,7 @@ const DetailedContinuousChartCard: FC = ({ : cursorTooltip } isConsumerView={isConsumerView} + isEmbedded={isEmbed} />
)} From df0203e3ea05b2873eeae0dd04a69d5c9718f576 Mon Sep 17 00:00:00 2001 From: Nikita Date: Fri, 5 Dec 2025 17:37:57 +0200 Subject: [PATCH 04/26] feat: add dynamic chart height adaptation --- .../components/embed_question_card.tsx | 25 ++++++++++++++--- .../components/embed_question_header.tsx | 27 +++++++++++++++++-- .../components/embed_question_plot.tsx | 11 ++++++-- .../src/components/charts/numeric_chart.tsx | 15 +++-------- .../continuous_chart_card.tsx | 7 +++++ .../detailed_question_card/index.tsx | 3 +++ 6 files changed, 70 insertions(+), 18 deletions(-) diff --git a/front_end/src/app/(embed)/questions/components/embed_question_card.tsx b/front_end/src/app/(embed)/questions/components/embed_question_card.tsx index 8f575119f0..a00fb3d4d3 100644 --- a/front_end/src/app/(embed)/questions/components/embed_question_card.tsx +++ b/front_end/src/app/(embed)/questions/components/embed_question_card.tsx @@ -1,4 +1,4 @@ -import { Fragment } from "react"; +import { Fragment, useMemo, useState } from "react"; import { PostWithForecasts } from "@/types/post"; @@ -12,11 +12,30 @@ type Props = { }; const EmbedQuestionCard: React.FC = ({ post }) => { + const [headerHeight, setHeaderHeight] = useState(0); + + const chartHeight = useMemo(() => { + const MIN_HEADER = 50; + const MAX_HEADER = 100; + const MIN_CHART = 120; + const MAX_CHART = 170; + + if (!headerHeight) return MAX_CHART; + + const clampedHeader = Math.min( + MAX_HEADER, + Math.max(MIN_HEADER, headerHeight) + ); + const t = (clampedHeader - MIN_HEADER) / (MAX_HEADER - MIN_HEADER); + + return Math.round(MAX_CHART + 8 - t * (MAX_CHART - MIN_CHART)); + }, [headerHeight]); + return ( - - + + diff --git a/front_end/src/app/(embed)/questions/components/embed_question_header.tsx b/front_end/src/app/(embed)/questions/components/embed_question_header.tsx index ede70be777..eff95a9a93 100644 --- a/front_end/src/app/(embed)/questions/components/embed_question_header.tsx +++ b/front_end/src/app/(embed)/questions/components/embed_question_header.tsx @@ -7,14 +7,37 @@ import { } from "@/utils/questions/helpers"; import TruncatableQuestionTitle from "./truncatable_question_title"; +import { useEffect, useRef } from "react"; type Props = { post: PostWithForecasts; + onHeightChange?: (height: number) => void; }; -const EmbedQuestionHeader: React.FC = ({ post }) => { +const EmbedQuestionHeader: React.FC = ({ post, onHeightChange }) => { + const containerRef = useRef(null); + + useEffect(() => { + if (!onHeightChange) return; + const el = containerRef.current; + if (!el) return; + + const update = () => { + onHeightChange(el.getBoundingClientRect().height); + }; + + update(); + + const observer = new ResizeObserver(() => { + update(); + }); + observer.observe(el); + + return () => observer.disconnect(); + }, [onHeightChange]); + return ( -
+
= ({ post }) => { - return <>{isQuestionPost(post) && }; +const EmbedQuestionPlot: React.FC = ({ post, chartHeight }) => { + return ( + <> + {isQuestionPost(post) && ( + + )} + + ); }; export default EmbedQuestionPlot; diff --git a/front_end/src/components/charts/numeric_chart.tsx b/front_end/src/components/charts/numeric_chart.tsx index 869893cc0b..34d9d60f12 100644 --- a/front_end/src/components/charts/numeric_chart.tsx +++ b/front_end/src/components/charts/numeric_chart.tsx @@ -135,13 +135,6 @@ const NumericChart: FC = ({ const [isCursorActive, setIsCursorActive] = useState(false); const { ref: chartContainerRef, width: chartWidth } = useContainerSize(); - - const baseHeight = height ?? 170; - const chartHeight = useMemo(() => { - if (!isEmbedded || !chartWidth) return baseHeight; - const target = chartWidth * 0.4; - return Math.round(Math.max(120, Math.min(178, target))); - }, [isEmbedded, chartWidth, baseHeight]); const { line, area, points, yDomain, xDomain, yScale, xScale } = useMemo( () => buildChartData(chartWidth, zoom), [chartWidth, zoom, buildChartData] @@ -268,7 +261,7 @@ const NumericChart: FC = ({ cursorLabelComponent={ = ({ }, [ defaultCursor, xScale, - chartHeight, + height, hasExternalTheme, getThemeColor, handleCursorChange, @@ -435,7 +428,7 @@ const NumericChart: FC = ({ = ({ = ({ @@ -43,6 +44,7 @@ const DetailedContinuousChartCard: FC = ({ forecastAvailability, hideTitle, isConsumerView: isConsumerViewProp, + embedChartHeight, }) => { const t = useTranslations(); const { user } = useAuth(); @@ -183,6 +185,8 @@ const DetailedContinuousChartCard: FC = ({ const isEmbed = useIsEmbedMode(); + const chartHeight = embedChartHeight ?? 150; + return (
= ({ } isConsumerView={isConsumerView} isEmbedded={isEmbed} + height={chartHeight} />
@@ -286,6 +291,7 @@ const DetailedContinuousChartCard: FC = ({ } isConsumerView={isConsumerView} isEmbedded={isEmbed} + height={chartHeight} />
@@ -329,6 +335,7 @@ const DetailedContinuousChartCard: FC = ({ } isConsumerView={isConsumerView} isEmbedded={isEmbed} + height={chartHeight} />
)} diff --git a/front_end/src/components/detailed_question_card/detailed_question_card/index.tsx b/front_end/src/components/detailed_question_card/detailed_question_card/index.tsx index 9a28d5dd78..7f8565ab4b 100644 --- a/front_end/src/components/detailed_question_card/detailed_question_card/index.tsx +++ b/front_end/src/components/detailed_question_card/detailed_question_card/index.tsx @@ -16,12 +16,14 @@ type Props = { post: QuestionPost; hideTitle?: boolean; isConsumerView?: boolean; + embedChartHeight?: number; }; const DetailedQuestionCard: FC = ({ post, hideTitle, isConsumerView, + embedChartHeight, }) => { const { question, status, nr_forecasters } = post; const forecastAvailability = getQuestionForecastAvailability(question); @@ -54,6 +56,7 @@ const DetailedQuestionCard: FC = ({ nrForecasters={nr_forecasters} hideTitle={hideTitle} isConsumerView={isConsumerView} + embedChartHeight={embedChartHeight} /> {hideCP && } From ba02dbe1e3314a7c967bf27d89761d3e770d68f2 Mon Sep 17 00:00:00 2001 From: Nikita Date: Fri, 5 Dec 2025 18:12:42 +0200 Subject: [PATCH 05/26] feat: update truncatable text --- .../components/truncatable_question_title.tsx | 32 ++++++++++++++----- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/front_end/src/app/(embed)/questions/components/truncatable_question_title.tsx b/front_end/src/app/(embed)/questions/components/truncatable_question_title.tsx index 5ee3ad1a1e..6468c168f3 100644 --- a/front_end/src/app/(embed)/questions/components/truncatable_question_title.tsx +++ b/front_end/src/app/(embed)/questions/components/truncatable_question_title.tsx @@ -10,6 +10,8 @@ type TruncatableQuestionTitleProps = HTMLAttributes & { revealOnHoverOrTap?: boolean; }; +const GRADIENT_EXTRA_PX = 12; + const TruncatableQuestionTitle: React.FC = ({ children, className, @@ -25,6 +27,8 @@ const TruncatableQuestionTitle: React.FC = ({ }) => { const [expanded, setExpanded] = useState(false); const [isTruncated, setIsTruncated] = useState(false); + const [contentHeight, setContentHeight] = useState(null); + const [clampedHeight, setClampedHeight] = useState(null); const titleRef = useRef(null); const hasClamp = maxLines > 0; @@ -42,20 +46,23 @@ const TruncatableQuestionTitle: React.FC = ({ const el = titleRef.current; if (!el || !hasClamp) { setIsTruncated(false); + setContentHeight(null); + setClampedHeight(null); return; } const checkTruncation = () => { - const truncated = el.scrollHeight > el.clientHeight + 1; - setIsTruncated(truncated); + const client = el.clientHeight; + const scroll = el.scrollHeight; + + setClampedHeight(client); + setContentHeight(scroll); + setIsTruncated(scroll > client + 1); }; checkTruncation(); - const observer = new ResizeObserver(() => { - checkTruncation(); - }); - + const observer = new ResizeObserver(checkTruncation); observer.observe(el); return () => observer.disconnect(); }, [children, maxLines, hasClamp]); @@ -77,6 +84,10 @@ const TruncatableQuestionTitle: React.FC = ({ const showOverlay = revealOnHoverOrTap && isTruncated && expanded; + const baseHeight = showOverlay ? contentHeight : clampedHeight; + const gradientHeight = + baseHeight != null ? baseHeight + GRADIENT_EXTRA_PX : undefined; + return (
= ({ {...rest} ref={titleRef} className={className} - style={{ ...clampStyle, ...style }} + style={{ + ...clampStyle, + ...style, + ...(showOverlay ? { visibility: "hidden" } : {}), + }} > {children} @@ -120,10 +135,11 @@ const TruncatableQuestionTitle: React.FC = ({
)} From 3a73dfd2f7abda4afc13ac5db833ed255b0bd5d8 Mon Sep 17 00:00:00 2001 From: Nikita Date: Mon, 8 Dec 2025 15:18:19 +0200 Subject: [PATCH 06/26] feat: add adaptation for continuous questions --- .../components/embed_question_header.tsx | 11 +++++++++-- ...uestion_header_continuous_resolution_chip.tsx | 4 ++++ .../question_header_cp_status.tsx | 16 +++++++++++++--- .../continuous_chart_card.tsx | 2 +- .../question_tile/continuous_cp_bar.tsx | 7 +++++++ 5 files changed, 34 insertions(+), 6 deletions(-) diff --git a/front_end/src/app/(embed)/questions/components/embed_question_header.tsx b/front_end/src/app/(embed)/questions/components/embed_question_header.tsx index eff95a9a93..42e163acdf 100644 --- a/front_end/src/app/(embed)/questions/components/embed_question_header.tsx +++ b/front_end/src/app/(embed)/questions/components/embed_question_header.tsx @@ -1,13 +1,16 @@ +import { useEffect, useRef } from "react"; + import QuestionHeaderCPStatus from "@/app/(main)/questions/[id]/components/question_view/forecaster_question_view/question_header/question_header_cp_status"; import { PostWithForecasts } from "@/types/post"; import { QuestionWithForecasts } from "@/types/question"; +import cn from "@/utils/core/cn"; import { isContinuousQuestion, isQuestionPost, } from "@/utils/questions/helpers"; +import { useIsEmbedMode } from "./question_view_mode_context"; import TruncatableQuestionTitle from "./truncatable_question_title"; -import { useEffect, useRef } from "react"; type Props = { post: PostWithForecasts; @@ -16,6 +19,7 @@ type Props = { const EmbedQuestionHeader: React.FC = ({ post, onHeightChange }) => { const containerRef = useRef(null); + const isEmbed = useIsEmbedMode(); useEffect(() => { if (!onHeightChange) return; @@ -37,7 +41,10 @@ const EmbedQuestionHeader: React.FC = ({ post, onHeightChange }) => { }, [onHeightChange]); return ( -
+
= ({ isClosed: question.status === QuestionStatus.CLOSED, }); + const isEmbed = useIsEmbedMode(); + return (
= ({ { "text-gray-700 dark:text-gray-700-dark": !successfullyResolved, "text-base": size === "lg", + "mb-0 truncate text-sm md:text-sm": isEmbed, } )} > diff --git a/front_end/src/app/(main)/questions/[id]/components/question_view/forecaster_question_view/question_header/question_header_cp_status.tsx b/front_end/src/app/(main)/questions/[id]/components/question_view/forecaster_question_view/question_header/question_header_cp_status.tsx index 9d74d0a7f7..73c72122fd 100644 --- a/front_end/src/app/(main)/questions/[id]/components/question_view/forecaster_question_view/question_header/question_header_cp_status.tsx +++ b/front_end/src/app/(main)/questions/[id]/components/question_view/forecaster_question_view/question_header/question_header_cp_status.tsx @@ -2,6 +2,7 @@ import { useLocale, useTranslations } from "next-intl"; import React, { FC } from "react"; +import { useIsEmbedMode } from "@/app/(embed)/questions/components/question_view_mode_context"; import QuestionHeaderContinuousResolutionChip from "@/app/(main)/questions/[id]/components/question_view/forecaster_question_view/question_header/question_header_continuous_resolution_chip"; import { getContinuousAreaChartData } from "@/components/charts/continuous_area_chart"; import MinifiedContinuousAreaChart from "@/components/charts/minified_continuous_area_chart"; @@ -46,6 +47,8 @@ const QuestionHeaderCPStatus: FC = ({ QuestionType.Date, ].includes(question.type); + const isEmbed = useIsEmbedMode(); + if (question.status === QuestionStatus.RESOLVED && question.resolution) { // Resolved/Annulled/Ambiguous const formatedResolution = formatResolution({ @@ -93,13 +96,19 @@ const QuestionHeaderCPStatus: FC = ({ "max-w-[130px]": size === "md", "gap-1": !hideLabel && size === "lg", "gap-0": size === "md", // Remove gap for mobile (both hideLabel true/false) - "-gap-2": size === "md" && hideLabel, // More negative gap for mobile continuous questions + "-gap-2": size === "md" && hideLabel, // More negative gap for mobile continuous questions, + "border-[0.5px] border-olive-500 p-3 dark:border-olive-500-dark md:p-3": + isEmbed, } )} >
{!hideLabel && ( -
+