Skip to content
Merged
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
2 changes: 2 additions & 0 deletions front_end/messages/cs.json
Original file line number Diff line number Diff line change
Expand Up @@ -762,6 +762,8 @@
"nuclearSecurityDescription": "Kvantifikace globálních rizik pro zajištění naší bezpečnosti a ochrany pro prosperující budoucnost",
"climateChange": "změna klimatu",
"climateChangeDescription": "Předpovídání dlouhodobých změn v teplotních a povětrnostních vzorcích způsobených lidskou činností",
"remindMeLater": "Připomeňte mi později",
"onboardingRemindMeLaterDescription": "(Návod můžete vždy najít později v nabídce v horní části každé stránky)",
"onboardingStep1Question1": "Mám si vzít deštník?",
"onboardingStep1Question2": "Směřujeme k recesi?",
"onboardingStep1Question3": "Zvítězí můj tým?",
Expand Down
2 changes: 2 additions & 0 deletions front_end/messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -901,6 +901,8 @@
"onboardingStep1Paragraph2": "Let's make a few quick predictions to get you started.",
"onboardingStep1Paragraph3": "First, pick a topic you care about.",
"skipTutorial": "Skip Tutorial",
"remindMeLater": "Remind me later",
"onboardingRemindMeLaterDescription": "(You can always find the tutorial later in the menu at the top of every page)",
"skipQuestions": "Skip question",
"onboardingStep2Title": "Here's a real Metaculus question about {topicName}:",
"onboardingStep2CommunityThinks": "Other forecasters tend to think this is",
Expand Down
2 changes: 2 additions & 0 deletions front_end/messages/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -775,6 +775,8 @@
"nuclearSecurityDescription": "Cuantificar los riesgos globales para mantenernos seguros y protegidos para un futuro próspero",
"climateChange": "cambio climático",
"climateChangeDescription": "Predecir los cambios a largo plazo en los patrones de temperatura y clima causados por la actividad humana",
"remindMeLater": "Recuérdamelo más tarde",
"onboardingRemindMeLaterDescription": "(Siempre puedes encontrar el tutorial más tarde en el menú en la parte superior de cada página)",
"onboardingStep1Question1": "¿Debería llevar un paraguas?",
"onboardingStep1Question2": "¿Nos dirigimos a una recesión?",
"onboardingStep1Question3": "¿Saldrá mi equipo victorioso?",
Expand Down
2 changes: 2 additions & 0 deletions front_end/messages/pt.json
Original file line number Diff line number Diff line change
Expand Up @@ -885,6 +885,8 @@
"analytics": "Análise",
"helpsImprovePlatform": "Nos ajuda a melhorar a plataforma",
"saveSelected": "Salvar Selecionados",
"remindMeLater": "Lembrar-me mais tarde",
"onboardingRemindMeLaterDescription": "(Você sempre pode encontrar o tutorial mais tarde no menu no topo de cada página)",
"onboardingStep1Question1": "Devo levar um guarda-chuva?",
"onboardingStep1Question2": "Estamos caminhando para uma recessão?",
"onboardingStep1Question3": "Meu time vai ganhar?",
Expand Down
2 changes: 2 additions & 0 deletions front_end/messages/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -753,6 +753,8 @@
"nuclearSecurityDescription": "量化全球風險以確保我們安全並實現繁榮的未來",
"climateChange": "氣候變化",
"climateChangeDescription": "預測由人類活動引起的長期溫度和天氣模式變化",
"remindMeLater": "稍后提醒我",
"onboardingRemindMeLaterDescription": "(您随时可以在每页顶部的菜单中找到教程)",
"onboardingStep1Question1": "我应该带伞吗?",
"onboardingStep1Question2": "我们是否正走向衰退?",
"onboardingStep1Question3": "我的团队会胜出吗?",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ const EmailConfirmation = () => {
useEffect(() => {
if (searchParams.get("event") === "emailConfirmed") {
sendGAEvent("event", "emailConfirmed");
router.replace("/?start_onboarding=true");
}
}, [router, searchParams]);

Expand Down
1 change: 1 addition & 0 deletions front_end/src/app/(main)/accounts/profile/actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ export async function updateProfileAction(
| "unsubscribed_mailing_tags"
| "unsubscribed_preferences_tags"
| "hide_community_prediction"
| "is_onboarding_complete"
>
>
) {
Expand Down
18 changes: 9 additions & 9 deletions front_end/src/app/(main)/components/mobile_menu.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
"use client";
import {
faArrowLeft,
faBars,
faMagnifyingGlass,
faMinus,
faXmark,
faPlus,
faArrowLeft,
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
Expand All @@ -18,7 +17,7 @@ import {
import classNames from "classnames";
import Link from "next/link";
import { useTranslations } from "next-intl";
import { FC, PropsWithChildren, useState, useRef, useEffect } from "react";
import { FC, PropsWithChildren, useEffect, useRef, useState } from "react";

import { LogOut } from "@/app/(main)/accounts/actions";
import LanguageMenu from "@/components/language_menu";
Expand Down Expand Up @@ -153,7 +152,9 @@ const MobileMenu: FC<Props> = ({ community, onClick }) => {
<MenuLink href={"/accounts/settings/"}>
{t("settings")}
</MenuLink>
<MenuLink href={"/?start_onboarding=true"}>
<MenuLink
onClick={() => setCurrentModal({ type: "onboarding" })}
>
{t("tutorial")}
</MenuLink>
{user.is_superuser && (
Expand Down Expand Up @@ -245,7 +246,9 @@ const MobileMenu: FC<Props> = ({ community, onClick }) => {
<MenuLink href={"/accounts/settings/"}>
{t("settings")}
</MenuLink>
<MenuLink href={"/?start_onboarding=true"}>
<MenuLink
onClick={() => setCurrentModal({ type: "onboarding" })}
>
{t("tutorial")}
</MenuLink>
{user.is_superuser && (
Expand Down Expand Up @@ -279,10 +282,7 @@ const MobileMenu: FC<Props> = ({ community, onClick }) => {
ref={searchContainerRef}
className="fixed inset-x-0 top-12 z-40 bg-blue-200-dark p-2 shadow-md"
>
<GlobalSearch
onSubmit={handleSearchSubmit}
isMobile={true}
/>
<GlobalSearch onSubmit={handleSearchSubmit} isMobile={true} />
</div>
)}
</>
Expand Down
2 changes: 2 additions & 0 deletions front_end/src/app/(main)/questions/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Suspense } from "react";

import AwaitedCommunitiesFeed from "@/components/communities_feed";
import OnboardingCheck from "@/components/onboarding/OnboardingCheck";
import AwaitedPostsFeed from "@/components/posts_feed";
import LoadingIndicator from "@/components/ui/loading_indicator";
import { POST_COMMUNITIES_FILTER } from "@/constants/posts_feed";
Expand Down Expand Up @@ -34,6 +35,7 @@ export default async function Questions({
return (
<>
<main className="mx-auto mt-4 min-h-min w-full max-w-5xl flex-auto px-0 sm:px-2 md:px-3">
<OnboardingCheck />
<div className="gap-3 p-0 sm:flex sm:flex-row sm:gap-4">
<QuestionTopics topics={topics} />
<div className="min-h-[calc(100vh-300px)] grow overflow-x-hidden p-2 pt-2.5 no-scrollbar sm:p-0 sm:pt-5">
Expand Down
8 changes: 4 additions & 4 deletions front_end/src/components/auth/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,12 +64,12 @@ const NavUserButton: FC<Props> = ({ btnClassName }) => {
</Link>
</MenuItem>
<MenuItem>
<Link
className="flex items-center justify-center whitespace-nowrap px-6 py-1.5 capitalize no-underline hover:bg-blue-400-dark lg:items-end lg:justify-end lg:text-right lg:hover:bg-blue-200-dark"
href={"/?start_onboarding=true"}
<a
className="flex cursor-pointer items-center justify-center whitespace-nowrap px-6 py-1.5 capitalize no-underline hover:bg-blue-400-dark lg:items-end lg:justify-end lg:text-right lg:hover:bg-blue-200-dark"
onClick={() => setCurrentModal({ type: "onboarding" })}
>
{t("tutorial")}
</Link>
</a>
</MenuItem>
{user.is_superuser && (
<MenuItem>
Expand Down
2 changes: 1 addition & 1 deletion front_end/src/components/base_modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ const BaseModal: FC<PropsWithChildren<Props>> = ({
<Transition appear show={isOpen} as={Fragment}>
<Dialog
as="div"
className="relative z-50"
className="z-100 relative"
onClose={isImmersive ? () => {} : onClose}
onWheel={(e) => isImmersive && e.stopPropagation()}
>
Expand Down
32 changes: 7 additions & 25 deletions front_end/src/components/onboarding/OnboardingCheck.tsx
Original file line number Diff line number Diff line change
@@ -1,39 +1,21 @@
"use client";

import { useSearchParams, useRouter } from "next/navigation";
import { useEffect, useCallback } from "react";
import { useEffect } from "react";

import { useAuth } from "@/contexts/auth_context";
import { useModal } from "@/contexts/modal_context";
import { checkOnboardingAllowed } from "@/utils/onboarding";

const OnboardingCheck: React.FC = () => {
const searchParams = useSearchParams();
const router = useRouter();
const { setCurrentModal } = useModal();
const { user } = useAuth();

const handleOnboarding = useCallback(() => {
const startOnboarding = searchParams.get("start_onboarding");
if (startOnboarding === "true") {
// Remove the query parameter
const newUrl = new URL(window.location.href);
newUrl.searchParams.delete("start_onboarding");
router.replace(newUrl.toString());

// Check if the user is logged in
if (user) {
// Start the onboarding process
setCurrentModal({ type: "onboarding" });
} else {
// Show the registration modal
setCurrentModal({ type: "signup" });
}
}
}, [searchParams, router, user, setCurrentModal]);

useEffect(() => {
handleOnboarding();
}, [handleOnboarding]);
if (checkOnboardingAllowed() && user?.id && !user?.is_onboarding_complete) {
// Start the onboarding process
setCurrentModal({ type: "onboarding" });
}
}, [user?.id]);

return null; // This component doesn't render anything
};
Expand Down
38 changes: 35 additions & 3 deletions front_end/src/components/onboarding/OnboardingModal.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
"use client";

import { useRouter } from "next/navigation";
import React, { useState, useEffect, useRef } from "react";
import React, { useCallback, useEffect, useRef, useState } from "react";

import { updateProfileAction } from "@/app/(main)/accounts/profile/actions";
import { getPost } from "@/app/(main)/questions/actions";
import BaseModal from "@/components/base_modal";
import { useAuth } from "@/contexts/auth_context";
import { PostWithForecasts } from "@/types/post";
import { logError } from "@/utils/errors";
import {
deleteOnboardingStoredState,
getOnboardingStoredState,
setOnboardingStoredState,
} from "@/utils/onboarding";

import { onboardingTopics } from "./OnboardingSettings";
import Step1 from "./steps/Step1";
Expand All @@ -16,8 +26,13 @@ const OnboardingModal: React.FC<{ isOpen: boolean; onClose: () => void }> = ({
isOpen,
onClose,
}) => {
const [currentStep, setCurrentStep] = useState(1);
const [selectedTopic, setSelectedTopic] = useState<number | null>(null);
const { user } = useAuth();
const [currentStep, setCurrentStep] = useState(
() => getOnboardingStoredState().currentStep || 1
);
const [selectedTopic, setSelectedTopic] = useState<number | null>(
getOnboardingStoredState().selectedTopic || null
);
const [questionData, setQuestionData] = useState<PostWithForecasts | null>(
null
);
Expand All @@ -34,6 +49,17 @@ const OnboardingModal: React.FC<{ isOpen: boolean; onClose: () => void }> = ({
}
};

// Save state into local storage
useEffect(() => {
if (!user?.is_onboarding_complete) {
if (currentStep > 1 && currentStep < 5) {
setOnboardingStoredState({ selectedTopic, currentStep });
} else {
deleteOnboardingStoredState();
}
}
}, [user?.is_onboarding_complete, selectedTopic, currentStep]);

useEffect(() => {
async function fetchQuestionData() {
if (
Expand All @@ -59,6 +85,12 @@ const OnboardingModal: React.FC<{ isOpen: boolean; onClose: () => void }> = ({
}, [selectedTopic, currentStep]);

const handleNext = () => {
// Treat tutorial as done when user opens 4th page
if (currentStep == 4 && !user?.is_onboarding_complete) {
// Mark tutorial as complete
updateProfileAction({ is_onboarding_complete: true }).catch(logError);
}

if (currentStep < 5) {
setCurrentStep(currentStep + 1);
scrollToTop();
Expand Down
25 changes: 24 additions & 1 deletion front_end/src/components/onboarding/steps/Step1.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ import { sendGAEvent } from "@next/third-parties/google";
import { useTranslations } from "next-intl";
import React, { useEffect } from "react";

import { updateProfileAction } from "@/app/(main)/accounts/profile/actions";
import { logError } from "@/utils/errors";
import { setOnboardingSuppressed } from "@/utils/onboarding";

import { onboardingTopics } from "../OnboardingSettings";
import { onboardingStyles } from "../OnboardingStyles";

Expand All @@ -21,7 +25,17 @@ const Step1: React.FC<Step1Props> = ({ onTopicSelect, onClose }) => {
}, []);

const handleSkipTutorial = () => {
// Mark tutorial as complete
sendGAEvent({ event: "onboardingSkipped", event_category: "onboarding" });
updateProfileAction({ is_onboarding_complete: true }).catch(logError);
onClose();
};

const handleCloseTutorial = () => {
// Temporarily hide tutorial
sendGAEvent({ event: "onboardingClosed", event_category: "onboarding" });
// Mark as temporarily suppressed
setOnboardingSuppressed();
onClose();
};

Expand Down Expand Up @@ -75,14 +89,23 @@ const Step1: React.FC<Step1Props> = ({ onTopicSelect, onClose }) => {
</button>
))}
</div>
<div className="mt-4 flex w-full justify-start md:mt-8 md:justify-center">
<div className="mt-4 flex w-full justify-center gap-3 md:mt-8">
<button
onClick={handleSkipTutorial}
className="text-base text-blue-700 underline decoration-blue-700/70 underline-offset-4 hover:text-blue-800 hover:decoration-blue-700/90 dark:text-blue-700-dark dark:decoration-blue-700/70 dark:hover:text-blue-800-dark dark:hover:decoration-blue-700-dark/90 "
>
{t("skipTutorial")}
</button>
<button
onClick={handleCloseTutorial}
className="text-base text-blue-700 underline decoration-blue-700/70 underline-offset-4 hover:text-blue-800 hover:decoration-blue-700/90 dark:text-blue-700-dark dark:decoration-blue-700/70 dark:hover:text-blue-800-dark dark:hover:decoration-blue-700-dark/90 "
>
{t("remindMeLater")}
</button>
</div>
<p className="text-center opacity-60">
{t("onboardingRemindMeLaterDescription")}
</p>
</div>
);
};
Expand Down
1 change: 1 addition & 0 deletions front_end/src/services/profile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ class ProfileApi {
website?: string;
unsubscribed_mailing_tags?: SubscriptionEmailType[];
unsubscribed_preference_tags?: ProfilePreferencesType[];
is_onboarding_complete?: boolean;
}) {
return patch<CurrentUser, typeof props>("/users/me/update/", props);
}
Expand Down
4 changes: 4 additions & 0 deletions front_end/src/types/onboarding.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export type OnboardingStoredState = {
selectedTopic?: number | null;
currentStep?: number | null;
};
1 change: 1 addition & 0 deletions front_end/src/types/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export type CurrentUser = User & {
unsubscribed_mailing_tags: SubscriptionEmailType[];
unsubscribed_preferences_tags: ProfilePreferencesType[];
hide_community_prediction: boolean;
is_onboarding_complete: boolean;
};

export enum ProfilePageMode {
Expand Down
44 changes: 44 additions & 0 deletions front_end/src/utils/onboarding.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
"use client";

import { addDays, isAfter } from "date-fns";

import { OnboardingStoredState } from "@/types/onboarding";

const ONBOARDING_SUPPRESSED_KEY = "onboardingSuppressedAt";
const ONBOARDING_STATE_KEY = "onboardingState";

export function checkLocalStorageAvailable() {
return typeof window !== "undefined" && window.localStorage;
}

export function checkOnboardingAllowed() {
const closedAt =
checkLocalStorageAvailable() &&
localStorage.getItem(ONBOARDING_SUPPRESSED_KEY);

return closedAt ? isAfter(new Date(), addDays(new Date(closedAt), 1)) : true;
}

export function setOnboardingSuppressed() {
if (checkLocalStorageAvailable()) {
localStorage.setItem(ONBOARDING_SUPPRESSED_KEY, new Date().toISOString());
}
}

export function getOnboardingStoredState(): OnboardingStoredState {
const storedValue =
checkLocalStorageAvailable() && localStorage.getItem(ONBOARDING_STATE_KEY);
return storedValue ? JSON.parse(storedValue) : {};
}

export function setOnboardingStoredState(data: OnboardingStoredState) {
if (checkLocalStorageAvailable()) {
localStorage.setItem(ONBOARDING_STATE_KEY, JSON.stringify(data));
}
}

export function deleteOnboardingStoredState() {
if (checkLocalStorageAvailable()) {
localStorage.removeItem(ONBOARDING_STATE_KEY);
}
}
Loading