diff --git a/front_end/messages/cs.json b/front_end/messages/cs.json index 0736849c6f..76d7692695 100644 --- a/front_end/messages/cs.json +++ b/front_end/messages/cs.json @@ -1940,5 +1940,48 @@ "yourOrganization": "Vaše organizace", "yourMessageForm": "Vaše zpráva", "readFullReport": "Přečíst celou zprávu", + "discoverServicesTitle": "Objevte služby", + "discoverServicesSubtitle": "Vyberte typ své organizace a začněte", + "discoverServicesEnterpriseTitle": "Podnikání", + "discoverServicesEnterpriseDescription": "Pro korporace, které spravují tržní rizika, dodavatelské řetězce a strategii.", + "discoverServicesGovernmentTitle": "Vláda", + "discoverServicesGovernmentDescription": "Pro tvůrce politik navigující změny v AI, obraně, zdravotnictví a pracovních trzích.", + "discoverServicesNonProfitTitle": "Nezisková organizace", + "discoverServicesNonProfitDescription": "Pro nadace, nevládní organizace a think tanky usilující o společenský dopad.", + "discoverServicesAcademiaTitle": "Akademická sféra", + "discoverServicesAcademiaDescription": "Pro univerzity a instituce podporující vědecký pokrok.", + "workWithMetaculus": "Práce s Metaculus", + "selectChallengesYouAreCurrentlyFacing": "Vyberte obtíže, kterým aktuálně čelíte", + "typeHere": "Sem napište…", + "continue": "Pokračovat", + "youHaveUnsavedProgress": "Máte neuložený postup.", + "stay": "Zůstat", + "stepsLeft": "{count, plural, one {# krok zbývá} few {# kroky zbývají} other {# kroků zbývá}}", + "howSoonDoYouNeedForecasts": "Jak rychle potřebujete prognózy?", + "soonerThanThreeMonths": "Dříve než za 3 měsíce", + "laterThanThreeMonths": "Později než za 3 měsíce", + "flexibleUnsure": "Flexibilní / zatím si nejsem jistý", + "whoShouldMakeTheForecasts": "Kdo by měl dělat prognózy?", + "metaculusPros": "Odborníci Metaculus", + "clientExperts": "Vaši interní odborníci", + "notSure": "Nejsem si jistý", + "privacyQuestionTitle": "Jak důvěrná je tato práce?", + "semiConfidentiality": "Nějaká důvěrnost", + "fullConfidentiality": "Úplná důvěrnost", + "weHaveReceivedYourRequest": "Obdrželi jsme vaši žádost", + "backToServices": "Zpět ke službám", + "semiConfidentialityDescription": "Organizace skrytá, ale otázky mohou být veřejné", + "fullConfidentialityDescription": "Organizace i otázky skryté", + "submitYourAnswersTitle": "Odešlete své odpovědi a my se ozveme", + "yourNamePlaceholder": "Jan Novák", + "emailAddressPlaceholder": "jan.novak@firma.cz", + "organizationPlaceholder": "Název organizace", + "anyAdditionalComments": "Nějaké další poznámky?", + "fieldRequired": "Toto pole je povinné.", + "invalidEmail": "Zadejte, prosím, platnou e-mailovou adresu.", + "submitting": "Odesílání...", + "ourTeamWillBeInContactSoon": "Náš tým se brzy ozve.", + "automaticRedirectInSeconds": "Automatické přesměrování za {count, plural, one {# sekundu} few {# sekundy} other {# sekund}}", + "errorSubmittingQuiz": "Něco se pokazilo. Zkuste to prosím znovu.", "thousandsOfOpenQuestions": "20 000+ otevřených otázek" } diff --git a/front_end/messages/en.json b/front_end/messages/en.json index 6190b583a5..34d550aff5 100644 --- a/front_end/messages/en.json +++ b/front_end/messages/en.json @@ -1933,5 +1933,48 @@ "yourOrganization": "Your organization", "yourMessageForm": "Your message", "readFullReport": "Read full report", + "discoverServicesTitle": "Discover Services", + "discoverServicesSubtitle": "Select your organization type to start", + "discoverServicesEnterpriseTitle": "Enterprise", + "discoverServicesEnterpriseDescription": "For corporations managing market risk, supply chains, and strategy.", + "discoverServicesGovernmentTitle": "Government", + "discoverServicesGovernmentDescription": "For policymakers navigating shifts in AI, defense, health, and labor markets.", + "discoverServicesNonProfitTitle": "Non-Profit", + "discoverServicesNonProfitDescription": "For foundations, NGOs, and think tanks driving social impact.", + "discoverServicesAcademiaTitle": "Academia", + "discoverServicesAcademiaDescription": "For universities and institutions advancing scientific discovery.", + "workWithMetaculus": "Work with Metaculus", + "selectChallengesYouAreCurrentlyFacing": "Select challenges you are currently facing", + "typeHere": "Type here…", + "continue": "Continue", + "youHaveUnsavedProgress": "You have unsaved progress.", + "stay": "Stay", + "stepsLeft": "{count, plural, one {# step left} other {# steps left}}", + "howSoonDoYouNeedForecasts": "How soon do you need forecasts?", + "soonerThanThreeMonths": "Sooner than 3 months", + "laterThanThreeMonths": "Later than 3 months", + "flexibleUnsure": "Flexible / not sure yet", + "whoShouldMakeTheForecasts": "Who should make the forecasts?", + "metaculusPros": "Metaculus pros", + "clientExperts": "Your internal experts", + "notSure": "Not sure", + "privacyQuestionTitle": "How confidential is this work?", + "semiConfidentiality": "Some confidentiality", + "fullConfidentiality": "Full confidentiality", + "submitYourAnswersTitle": "Submit your answers and we’ll be in touch", + "weHaveReceivedYourRequest": "We’ve received your request", + "ourTeamWillBeInContactSoon": "Our team will be in contact soon.", + "backToServices": "Back to Services", + "semiConfidentialityDescription": "Organization hidden but questions can be public", + "fullConfidentialityDescription": "Both organization and questions hidden", + "yourNamePlaceholder": "John Doe", + "emailAddressPlaceholder": "john.doe@company.com", + "organizationPlaceholder": "Your organization", + "anyAdditionalComments": "Any additional comments?", + "fieldRequired": "This field is required.", + "invalidEmail": "Please enter a valid email address.", + "submitting": "Submitting...", + "automaticRedirectInSeconds": "Automatic redirect in {count, plural, one {# second} other {# seconds}}", + "errorSubmittingQuiz": "Something went wrong. Please try again.", "feedTileSummaryPlaceholder": "Optional: Enter a custom summary text to display on feed tiles (if not provided, a summary will be auto-generated from the notebook content)" } diff --git a/front_end/messages/es.json b/front_end/messages/es.json index 372fbcd21b..5c58dba1ca 100644 --- a/front_end/messages/es.json +++ b/front_end/messages/es.json @@ -1940,5 +1940,48 @@ "yourOrganization": "Tu organización", "yourMessageForm": "Tu mensaje", "readFullReport": "Leer el informe completo", + "discoverServicesTitle": "Descubrir Servicios", + "discoverServicesSubtitle": "Seleccione su tipo de organización para comenzar", + "discoverServicesEnterpriseTitle": "Empresas", + "discoverServicesEnterpriseDescription": "Para corporaciones que gestionan riesgos de mercado, cadenas de suministro y estrategia.", + "discoverServicesGovernmentTitle": "Gobierno", + "discoverServicesGovernmentDescription": "Para responsables políticos que navegan por cambios en IA, defensa, salud y mercados laborales.", + "discoverServicesNonProfitTitle": "Sin Fines de Lucro", + "discoverServicesNonProfitDescription": "Para fundaciones, ONG y think tanks impulsando impacto social.", + "discoverServicesAcademiaTitle": "Academia", + "discoverServicesAcademiaDescription": "Para universidades e instituciones que avanzan en el descubrimiento científico.", + "workWithMetaculus": "Trabaja con Metaculus", + "selectChallengesYouAreCurrentlyFacing": "Selecciona los desafíos que estás enfrentando actualmente", + "typeHere": "Escribe aquí…", + "continue": "Continuar", + "youHaveUnsavedProgress": "Tienes progreso sin guardar.", + "stay": "Quedarse", + "stepsLeft": "{count, plural, one {# paso restante} other {# pasos restantes}}", + "howSoonDoYouNeedForecasts": "¿Cuán pronto necesitas las previsiones?", + "soonerThanThreeMonths": "Antes de 3 meses", + "laterThanThreeMonths": "Después de 3 meses", + "flexibleUnsure": "Flexible / aún no estoy seguro", + "whoShouldMakeTheForecasts": "¿Quién debería hacer las previsiones?", + "metaculusPros": "Profesionales de Metaculus", + "clientExperts": "Tus expertos internos", + "notSure": "No estoy seguro", + "privacyQuestionTitle": "¿Qué tan confidencial es este trabajo?", + "semiConfidentiality": "Algo de confidencialidad", + "fullConfidentiality": "Confidencialidad total", + "weHaveReceivedYourRequest": "Hemos recibido tu solicitud", + "backToServices": "Volver a Servicios", + "semiConfidentialityDescription": "Organización oculta pero las preguntas pueden ser públicas", + "fullConfidentialityDescription": "Tanto la organización como las preguntas están ocultas", + "submitYourAnswersTitle": "Envía tus respuestas y nos pondremos en contacto", + "yourNamePlaceholder": "John Doe", + "emailAddressPlaceholder": "john.doe@company.com", + "organizationPlaceholder": "Nombre de la organización", + "anyAdditionalComments": "¿Algún comentario adicional?", + "fieldRequired": "Este campo es obligatorio.", + "invalidEmail": "Por favor ingrese una dirección de correo electrónico válida.", + "submitting": "Enviando...", + "ourTeamWillBeInContactSoon": "Nuestro equipo se pondrá en contacto pronto.", + "automaticRedirectInSeconds": "Redirección automática en {count, plural, one {# segundo} other {# segundos}}", + "errorSubmittingQuiz": "Algo salió mal. Por favor, inténtalo de nuevo.", "thousandsOfOpenQuestions": "20,000+ preguntas abiertas" } diff --git a/front_end/messages/pt.json b/front_end/messages/pt.json index d6ccef108a..c22a58ed33 100644 --- a/front_end/messages/pt.json +++ b/front_end/messages/pt.json @@ -1938,5 +1938,48 @@ "yourOrganization": "Sua organização", "yourMessageForm": "Sua mensagem", "readFullReport": "Leia o relatório completo", + "discoverServicesTitle": "Descubra Serviços", + "discoverServicesSubtitle": "Selecione o tipo de organização para começar", + "discoverServicesEnterpriseTitle": "Empresa", + "discoverServicesEnterpriseDescription": "Para corporações que gerenciam risco de mercado, cadeias de suprimento e estratégia.", + "discoverServicesGovernmentTitle": "Governo", + "discoverServicesGovernmentDescription": "Para formuladores de políticas que navegam em mudanças na IA, defesa, saúde e mercados de trabalho.", + "discoverServicesNonProfitTitle": "Sem Fins Lucrativos", + "discoverServicesNonProfitDescription": "Para fundações, ONGs e think tanks impulsionando impacto social.", + "discoverServicesAcademiaTitle": "Academia", + "discoverServicesAcademiaDescription": "Para universidades e instituições que promovem a descoberta científica.", + "workWithMetaculus": "Trabalhe com Metaculus", + "selectChallengesYouAreCurrentlyFacing": "Selecione os desafios que você está enfrentando atualmente", + "typeHere": "Digite aqui...", + "continue": "Continuar", + "youHaveUnsavedProgress": "Você tem progresso não salvo.", + "stay": "Ficar", + "stepsLeft": "{count, plural, one {# passo restante} other {# passos restantes}}", + "howSoonDoYouNeedForecasts": "Com que urgência você precisa das previsões?", + "soonerThanThreeMonths": "Mais rápido que 3 meses", + "laterThanThreeMonths": "Mais tarde que 3 meses", + "flexibleUnsure": "Flexível / ainda não tenho certeza", + "whoShouldMakeTheForecasts": "Quem deve fazer as previsões?", + "metaculusPros": "Profissionais da Metaculus", + "clientExperts": "Seus especialistas internos", + "notSure": "Não tenho certeza", + "privacyQuestionTitle": "Qual é a confidencialidade deste trabalho?", + "semiConfidentiality": "Alguma confidencialidade", + "fullConfidentiality": "Confidencialidade total", + "weHaveReceivedYourRequest": "Recebemos seu pedido", + "backToServices": "Voltar para Serviços", + "semiConfidentialityDescription": "Organização oculta, mas questões podem ser públicas", + "fullConfidentialityDescription": "Tanto a organização quanto as questões estão ocultas", + "submitYourAnswersTitle": "Envie suas respostas e entraremos em contato", + "yourNamePlaceholder": "João Silva", + "emailAddressPlaceholder": "joao.silva@empresa.com", + "organizationPlaceholder": "Nome da organização", + "anyAdditionalComments": "Algum comentário adicional?", + "fieldRequired": "Este campo é obrigatório.", + "invalidEmail": "Por favor, insira um endereço de e-mail válido.", + "submitting": "Enviando...", + "ourTeamWillBeInContactSoon": "Nossa equipe entrará em contato em breve.", + "automaticRedirectInSeconds": "Redirecionamento automático em {count, plural, one {# segundo} other {# segundos}}", + "errorSubmittingQuiz": "Algo deu errado. Por favor, tente novamente.", "thousandsOfOpenQuestions": "20.000+ perguntas abertas" } diff --git a/front_end/messages/zh-TW.json b/front_end/messages/zh-TW.json index 6a35eb5fcc..220c80f771 100644 --- a/front_end/messages/zh-TW.json +++ b/front_end/messages/zh-TW.json @@ -1937,5 +1937,48 @@ "yourOrganization": "您的組織", "yourMessageForm": "您的訊息", "readFullReport": "閱讀完整報告", + "discoverServicesTitle": "發現服務", + "discoverServicesSubtitle": "選擇您的組織類型以開始", + "discoverServicesEnterpriseTitle": "企業", + "discoverServicesEnterpriseDescription": "適用於管理市場風險、供應鏈和戰略的公司。", + "discoverServicesGovernmentTitle": "政府", + "discoverServicesGovernmentDescription": "適用於應對人工智慧、防禦、健康和勞動市場轉變的政策制定者。", + "discoverServicesNonProfitTitle": "非營利組織", + "discoverServicesNonProfitDescription": "適用於推動社會影響的基金會、非政府組織和智庫。", + "discoverServicesAcademiaTitle": "學術界", + "discoverServicesAcademiaDescription": "適用於促進科學發現的大學和研究機構。", + "workWithMetaculus": "與 Metaculus 合作", + "selectChallengesYouAreCurrentlyFacing": "選擇您當前面臨的挑戰", + "typeHere": "在此輸入...", + "continue": "繼續", + "youHaveUnsavedProgress": "您有未儲存的進度。", + "stay": "留下", + "stepsLeft": "{count, plural, one {還剩 # 步} other {還剩 # 步}}", + "howSoonDoYouNeedForecasts": "您多快需要預測?", + "soonerThanThreeMonths": "少於 3 個月", + "laterThanThreeMonths": "多於 3 個月", + "flexibleUnsure": "靈活 / 尚不確定", + "whoShouldMakeTheForecasts": "誰應該製作預測?", + "metaculusPros": "Metaculus 專家", + "clientExperts": "您的內部專家", + "notSure": "不確定", + "privacyQuestionTitle": "此項工作具多大保密性?", + "semiConfidentiality": "某些保密性", + "fullConfidentiality": "完全保密", + "weHaveReceivedYourRequest": "我們已收到您的請求", + "backToServices": "返回服務", + "semiConfidentialityDescription": "組織隱藏但問題可以公開", + "fullConfidentialityDescription": "組織和問題均隱藏", + "submitYourAnswersTitle": "提交您的答案,我們將與您聯繫", + "yourNamePlaceholder": "約翰·多伊", + "emailAddressPlaceholder": "john.doe@company.com", + "organizationPlaceholder": "組織名稱", + "anyAdditionalComments": "有任何其他評論?", + "fieldRequired": "這是必填欄位。", + "invalidEmail": "請輸入有效的電子郵件地址。", + "submitting": "提交中...", + "ourTeamWillBeInContactSoon": "我們的團隊將很快與您聯繫。", + "automaticRedirectInSeconds": "{count} 秒後自動重定向", + "errorSubmittingQuiz": "出了點問題,請重試。", "thousandsOfOpenQuestions": "20,000+ 開放問題" } diff --git a/front_end/messages/zh.json b/front_end/messages/zh.json index 4b2c17d4ea..5f74920b0b 100644 --- a/front_end/messages/zh.json +++ b/front_end/messages/zh.json @@ -1942,5 +1942,48 @@ "yourOrganization": "您的组织", "yourMessageForm": "您的信息", "readFullReport": "阅读完整报告", + "discoverServicesTitle": "发现服务", + "discoverServicesSubtitle": "选择您的组织类型以开始", + "discoverServicesEnterpriseTitle": "企业", + "discoverServicesEnterpriseDescription": "适用于管理市场风险、供应链和战略的公司。", + "discoverServicesGovernmentTitle": "政府", + "discoverServicesGovernmentDescription": "适用于在人工智能、防务、健康和劳动力市场中导航政策变化的决策者。", + "discoverServicesNonProfitTitle": "非营利组织", + "discoverServicesNonProfitDescription": "适用于推动社会影响的基金会、非政府组织和智库。", + "discoverServicesAcademiaTitle": "学术机构", + "discoverServicesAcademiaDescription": "适用于推动科学发现的大学和机构。", + "workWithMetaculus": "与 Metaculus 合作", + "selectChallengesYouAreCurrentlyFacing": "选择您目前面临的挑战", + "typeHere": "在这里输入……", + "continue": "继续", + "youHaveUnsavedProgress": "您有未保存的进度。", + "stay": "停留", + "stepsLeft": "{count, plural, one {剩下 # 步} other {剩下 # 步}}", + "howSoonDoYouNeedForecasts": "您需要多快获得预测?", + "soonerThanThreeMonths": "少于 3 个月", + "laterThanThreeMonths": "超过 3 个月", + "flexibleUnsure": "灵活 / 尚不确定", + "whoShouldMakeTheForecasts": "谁应该进行预测?", + "metaculusPros": "Metaculus 专家", + "clientExperts": "您的内部专家", + "notSure": "不确定", + "privacyQuestionTitle": "这项工作有多保密?", + "semiConfidentiality": "一定程度的保密", + "fullConfidentiality": "完全保密", + "weHaveReceivedYourRequest": "我们已收到您的请求", + "backToServices": "返回服务", + "semiConfidentialityDescription": "组织隐藏,但问题可以公开", + "fullConfidentialityDescription": "组织和问题均隐藏", + "submitYourAnswersTitle": "提交您的答案,我们将与您联系", + "yourNamePlaceholder": "约翰·多伊", + "emailAddressPlaceholder": "john.doe@company.com", + "organizationPlaceholder": "组织名称", + "anyAdditionalComments": "还有其他任何评论吗?", + "fieldRequired": "此字段是必填项。", + "invalidEmail": "请输入有效的电子邮件地址。", + "submitting": "正在提交...", + "ourTeamWillBeInContactSoon": "我们的团队将尽快与您联系。", + "automaticRedirectInSeconds": "{count}秒后自动重定向", + "errorSubmittingQuiz": "出了点问题,请重试。", "thousandsOfOpenQuestions": "20,000+ 开放问题" } diff --git a/front_end/src/app/(main)/services/components/case_studies/constants.tsx b/front_end/src/app/(main)/services/components/case_studies/constants.tsx index 9e0a61f985..56a6599689 100644 --- a/front_end/src/app/(main)/services/components/case_studies/constants.tsx +++ b/front_end/src/app/(main)/services/components/case_studies/constants.tsx @@ -95,7 +95,7 @@ export const CASE_STUDIES: TCaseStudyCard[] = [ ], }, aboutInitiative: - "Developed in collaboration with Our World in Data (OWID), this project deployed Metaculus Pro Forecasters to predict key measures of human progress over various time horizons. Thirty OWID-tracked time-series metrics were selected to collectively illuminate what the future might hold according to Metaculus\u2019s most accurate forecasters.", + "Developed in collaboration with Our World in Data (OWID), this project deployed Metaculus Pro Forecasters to predict key measures of human progress over various time horizons. Thirty OWID-tracked time-series metrics were selected to collectively illuminate what the future might hold according to Metaculus's most accurate forecasters.", report: { previewImageSrc: OwidPreview, previewImageAlt: "OWID report preview", diff --git a/front_end/src/app/(main)/services/components/discover_services/constants.ts b/front_end/src/app/(main)/services/components/discover_services/constants.ts new file mode 100644 index 0000000000..fecd990968 --- /dev/null +++ b/front_end/src/app/(main)/services/components/discover_services/constants.ts @@ -0,0 +1,33 @@ +import { + faGraduationCap, + faIndustry, + faLandmark, + faPeopleGroup, +} from "@fortawesome/free-solid-svg-icons"; + +export const DISCOVER_SERVICES_CARDS = [ + { + type: "enterprise", + titleKey: "discoverServicesEnterpriseTitle", + descriptionKey: "discoverServicesEnterpriseDescription", + icon: faIndustry, + }, + { + type: "government", + titleKey: "discoverServicesGovernmentTitle", + descriptionKey: "discoverServicesGovernmentDescription", + icon: faLandmark, + }, + { + type: "non-profit", + titleKey: "discoverServicesNonProfitTitle", + descriptionKey: "discoverServicesNonProfitDescription", + icon: faPeopleGroup, + }, + { + type: "academia", + titleKey: "discoverServicesAcademiaTitle", + descriptionKey: "discoverServicesAcademiaDescription", + icon: faGraduationCap, + }, +] as const; diff --git a/front_end/src/app/(main)/services/components/discover_services/discover_services_block.tsx b/front_end/src/app/(main)/services/components/discover_services/discover_services_block.tsx new file mode 100644 index 0000000000..3027cce5be --- /dev/null +++ b/front_end/src/app/(main)/services/components/discover_services/discover_services_block.tsx @@ -0,0 +1,50 @@ +import { faArrowRight } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import Link from "next/link"; +import { useTranslations } from "next-intl"; + +import cn from "@/utils/core/cn"; + +import { DISCOVER_SERVICES_CARDS } from "./constants"; +import SectionHeading from "../section_heading"; + +const DiscoverServicesBlock: React.FC = () => { + const t = useTranslations(); + + return ( + <> + + +
+ {DISCOVER_SERVICES_CARDS.map((card) => ( + +
+ + +
+ +

+ {t(card.titleKey)} +

+

+ {t(card.descriptionKey)} +

+ + ))} +
+ + ); +}; + +export default DiscoverServicesBlock; diff --git a/front_end/src/app/(main)/services/components/templates/services_page_template.tsx b/front_end/src/app/(main)/services/components/templates/services_page_template.tsx index 754d4ea642..c8d34d4a96 100644 --- a/front_end/src/app/(main)/services/components/templates/services_page_template.tsx +++ b/front_end/src/app/(main)/services/components/templates/services_page_template.tsx @@ -16,6 +16,7 @@ import Button from "../button"; import CaseStudyCard from "../case_studies/case_study_card"; import { TCaseStudyCard } from "../case_studies/types"; import ContactSection from "../contact_section/contact_section"; +import DiscoverServicesBlock from "../discover_services/discover_services_block"; import HeadingBlock from "../heading_block"; import PartnersCarousel from "../partners_carousel"; import SectionHeading from "../section_heading"; @@ -120,6 +121,8 @@ const ServicesPageTemplate: React.FC = async ({ /> + + = ({ isOpen, onClose, tournamentSlug }) => { const { postsLeft, flowType } = usePredictionFlow(); return ( - -
-

- {t("exitPredictionFlow")} -

-

- {t.rich( - isNil(flowType) - ? "thereAreQuestionsYouHaveNotPredicted" - : "thereAreQuestionsThatRequireAttention", - { - count: postsLeft, - bold: (chunks) => {chunks}, - } - )} -

-

- {t("youCanComeBackAnytime")} -

-
- - -
-
-
+ title={t("exitPredictionFlow")} + description={t.rich( + isNil(flowType) + ? "thereAreQuestionsYouHaveNotPredicted" + : "thereAreQuestionsThatRequireAttention", + { + count: postsLeft, + bold: (chunks) => {chunks}, + } + )} + note={t("youCanComeBackAnytime")} + secondaryAction={{ label: t("keepPredicting") }} + primaryAction={{ + label: t("exit"), + onClick: () => router.push(`/tournament/${tournamentSlug}`), + }} + /> ); }; diff --git a/front_end/src/app/(prediction-flow)/components/header.tsx b/front_end/src/app/(prediction-flow)/components/header.tsx index b1505bff38..7224b8455e 100644 --- a/front_end/src/app/(prediction-flow)/components/header.tsx +++ b/front_end/src/app/(prediction-flow)/components/header.tsx @@ -5,8 +5,15 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import Link from "next/link"; import { useRouter } from "next/navigation"; import { useTranslations } from "next-intl"; -import { FC, useState } from "react"; +import React, { FC } from "react"; +import { + FlowHeaderActions, + FlowHeaderBrand, + FlowHeaderRoot, + FlowHeaderTitle, +} from "@/components/flow/flow_header"; +import { useExitGuard } from "@/components/flow/use_exit_guard"; import Button from "@/components/ui/button"; import cn from "@/utils/core/cn"; @@ -25,11 +32,16 @@ const PredictionFlowHeader: FC = ({ const router = useRouter(); const t = useTranslations(); const { postsLeft } = usePredictionFlow(); - const [isModalOpen, setIsModalOpen] = useState(false); + + const { isExitModalOpen, requestExit, closeExitModal } = useExitGuard({ + canExitImmediately: postsLeft === 0, + onExit: () => router.push(`/tournament/${tournamentSlug}`), + }); + return ( <> -
-
+ + = ({ M -
-
- {tournamentName} -
- - -
+ + + + + + + + + + + setIsModalOpen(false)} + isOpen={isExitModalOpen} + onClose={closeExitModal} tournamentSlug={tournamentSlug} /> diff --git a/front_end/src/app/(prediction-flow)/components/progress_section.tsx b/front_end/src/app/(prediction-flow)/components/progress_section.tsx index 910383537f..031c6e7661 100644 --- a/front_end/src/app/(prediction-flow)/components/progress_section.tsx +++ b/front_end/src/app/(prediction-flow)/components/progress_section.tsx @@ -1,18 +1,23 @@ "use client"; -import { faBars, faXmark } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { isNil } from "lodash"; import { useTranslations } from "next-intl"; -import { FC, useMemo } from "react"; +import React, { FC, useMemo } from "react"; -import PostStepButton from "@/app/(prediction-flow)/components/post_step_button"; -import { usePredictionFlow } from "@/app/(prediction-flow)/components/prediction_flow_provider"; -import Button from "@/components/ui/button"; -import cn from "@/utils/core/cn"; +import { + FlowStepperRoot, + FlowStepperHeader, + FlowStepperSegments, + FlowStepperNav, + FlowStepperNavPrev, + FlowStepperNavNext, + FlowStepperMenu, + FlowStep, +} from "@/components/flow/flow_stepper"; import { isPostOpenQuestionPredicted } from "@/utils/forecasts/helpers"; import PredictionFlowMenu from "./prediction_flow_menu"; +import { usePredictionFlow } from "./prediction_flow_provider"; const ProgressSection: FC = () => { const t = useTranslations(); @@ -26,84 +31,74 @@ const ProgressSection: FC = () => { changeActivePost, } = usePredictionFlow(); + const steps: FlowStep[] = useMemo(() => { + return posts.map((post) => ({ + id: post.id, + isDone: isNil(flowType) ? isPostOpenQuestionPredicted(post) : post.isDone, + })); + }, [posts, flowType]); + const currentIndex = useMemo( () => posts.findIndex((post) => post.id === currentPostId), [posts, currentPostId] ); - const handleClick = (isPrevious: boolean) => { - if (isPrevious ? currentIndex > 0 : currentIndex <= posts.length - 1) { - changeActivePost( - posts[isPrevious ? currentIndex - 1 : currentIndex + 1]?.id ?? null - ); - } - }; + const headerLabel = isNil(currentPostId) + ? t("questionsTotal", { count: posts.length }) + : t("questionsLeft", { count: postsLeft }); + + const nextLabel = t( + isNil(flowType) && posts[currentIndex] + ? isPostOpenQuestionPredicted(posts[currentIndex]) + ? "nextQuestion" + : "skipQuestions" + : posts[currentIndex]?.isDone + ? "nextQuestion" + : "skipQuestions" + ); + + const showNav = currentPostId !== null && !isMenuOpen; + const prevDisabled = currentIndex <= 0; + const nextDisabled = currentIndex > posts.length - 1 || currentIndex < 0; return ( -
-
-

- {isNil(currentPostId) - ? t("questionsTotal", { count: posts.length }) - : t("questionsLeft", { count: postsLeft })} -

- -
- {/* Questions bars */} -
- {posts.map((post, index) => ( - - ))} -
- {/* Navigation buttons */} - {!isNil(currentPostId) && !isMenuOpen && ( -
- - -
+ {nextLabel} + + )} - {isMenuOpen && } -
+ + + + + ); }; diff --git a/front_end/src/app/(services-quiz)/append_services_quiz_row.ts b/front_end/src/app/(services-quiz)/append_services_quiz_row.ts new file mode 100644 index 0000000000..cacc40e09e --- /dev/null +++ b/front_end/src/app/(services-quiz)/append_services_quiz_row.ts @@ -0,0 +1,122 @@ +"use server"; + +import { + appendSheetRow, + getSheetFirstRow, + setSheetRow, +} from "@/services/google_spreadsheets"; + +import type { ServicesQuizSubmitPayload } from "./helpers"; + +function mustEnv(name: string) { + const v = process.env[name]; + if (!v) throw new Error(`Missing env var: ${name}`); + return v; +} + +function normalizeWhoForecasts( + v: ServicesQuizSubmitPayload["whoForecasts"] | null | undefined +) { + if (!v) return ""; + if (v.mode === "not_sure") return "not_sure"; + return v.selections.join(","); +} + +const HEADER_ROW = [ + "created_at", + "category", + "challenges", + "notes", + "timing", + "who_forecasts", + "privacy", + "contact_name", + "contact_email", + "contact_organization", + "contact_comments", +] as const; + +async function ensureHeaderRow( + spreadsheetId: string, + credentialsBase64: string, + sheetName: string +) { + const firstRow = await getSheetFirstRow( + spreadsheetId, + credentialsBase64, + sheetName + ); + + const isEmpty = + firstRow.length === 0 || + firstRow.every((cell) => String(cell).trim() === ""); + + if (!isEmpty) return; + + await setSheetRow(spreadsheetId, credentialsBase64, { + sheetName, + row: [...HEADER_ROW], + rowIndex: 1, + valueInputOption: "RAW", + }); +} + +export async function appendServicesQuizRow( + payload: ServicesQuizSubmitPayload +) { + const spreadsheetId = mustEnv("SERVICES_QUIZ_GOOGLE_SHEETS_SPREADSHEET_ID"); + const credentialsBase64 = mustEnv("GOOGLE_CREDEBTIALS_FAB_SHEET_B64"); + const sheetName = "Sheet1"; + + await ensureHeaderRow(spreadsheetId, credentialsBase64, sheetName); + + const row = [ + new Date().toISOString(), + payload.category ?? "", + payload.challenges.join(" | "), + payload.notes ?? "", + payload.timing ?? "", + normalizeWhoForecasts(payload.whoForecasts), + payload.privacy ?? "", + payload.contact.name ?? "", + payload.contact.email ?? "", + payload.contact.organization ?? "", + payload.contact.comments ?? "", + ]; + + await appendSheetRow(spreadsheetId, credentialsBase64, { + sheetName, + row, + valueInputOption: "USER_ENTERED", + }); + + const zapierWebhookUrl = process.env.SERVICES_QUIZ_ZAPIER_WEBHOOK_URL; + if (zapierWebhookUrl) { + try { + const response = await fetch(zapierWebhookUrl, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + created_at: row[0], + category: payload.category, + challenges: payload.challenges.join(" | "), + notes: payload.notes, + timing: payload.timing, + who_forecasts: normalizeWhoForecasts(payload.whoForecasts), + privacy: payload.privacy, + contact_name: payload.contact.name, + contact_email: payload.contact.email, + contact_organization: payload.contact.organization, + contact_comments: payload.contact.comments, + }), + }); + if (!response.ok) { + console.error( + `Zapier webhook failed: ${response.status} ${response.statusText}` + ); + } + } catch (error) { + console.error("Zapier webhook error:", error); + } + } +} diff --git a/front_end/src/app/(services-quiz)/components/fields/services_quiz_notes_input.tsx b/front_end/src/app/(services-quiz)/components/fields/services_quiz_notes_input.tsx new file mode 100644 index 0000000000..b742438bc6 --- /dev/null +++ b/front_end/src/app/(services-quiz)/components/fields/services_quiz_notes_input.tsx @@ -0,0 +1,99 @@ +"use client"; + +import { faPen, faXmark } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { useTranslations } from "next-intl"; +import React, { FC, useCallback, useLayoutEffect, useRef } from "react"; + +import cn from "@/utils/core/cn"; + +type Props = { + value: string; + onChange: (v: string) => void; + placeholder?: string; + disabled?: boolean; + className?: string; + minHeightClassName?: string; + maxHeightPx?: number; +}; + +const ServicesQuizNotesInput: FC = ({ + value, + onChange, + placeholder, + disabled, + className, + minHeightClassName = "min-h-[56px]", + maxHeightPx, +}) => { + const t = useTranslations(); + const hasValue = value.trim().length > 0; + const ref = useRef(null); + + const resize = useCallback(() => { + const el = ref.current; + if (!el) return; + el.style.height = "0px"; + const next = el.scrollHeight; + const capped = + typeof maxHeightPx === "number" ? Math.min(next, maxHeightPx) : next; + + el.style.height = `${capped}px`; + el.style.overflowY = + typeof maxHeightPx === "number" && next > maxHeightPx ? "auto" : "hidden"; + }, [maxHeightPx]); + + useLayoutEffect(() => { + resize(); + }, [resize, value]); + + return ( +
+