Merge pull request #31329 from overleaf/mj-dark-mode-notification

[web] Introduce notification for dashboard dark mode

GitOrigin-RevId: da5045d412ddc87a4b18823a4a5fa3192fe15c89
This commit is contained in:
Mathias Jakobsen
2026-02-12 09:05:36 +00:00
committed by Copybot
parent 55c3b6b7ea
commit 498c89c6ed
20 changed files with 369 additions and 89 deletions
@@ -21,6 +21,7 @@ import { DsNavStyleProvider } from '@/features/project-list/components/use-is-ds
import CookieBanner from '@/shared/components/cookie-banner'
import useThemedPage from '@/shared/hooks/use-themed-page'
import { UserSettingsProvider } from '@/shared/context/user-settings-context'
import { TutorialProvider } from '@/shared/context/tutorial-context'
function ProjectListRoot() {
const { isReady } = useWaitForI18n()
@@ -37,9 +38,11 @@ export function ProjectListRootInner() {
<ProjectListProvider>
<ColorPickerProvider>
<SplitTestProvider>
<UserSettingsProvider>
<ProjectListPageContent />
</UserSettingsProvider>
<TutorialProvider>
<UserSettingsProvider>
<ProjectListPageContent />
</UserSettingsProvider>
</TutorialProvider>
</SplitTestProvider>
</ColorPickerProvider>
</ProjectListProvider>
@@ -18,6 +18,8 @@ import { useScrolled } from '@/features/project-list/components/sidebar/use-scro
import { useSendProjectListMB } from '@/features/project-list/components/project-list-events'
import { SurveyWidgetDsNav } from '@/features/project-list/components/survey-widget-ds-nav'
import { useFeatureFlag } from '@/shared/context/split-test-context'
import { ThemedProjectDashboardNotification } from './themed-project-dashboard-notification'
import { useThemedDashboardIntro } from './use-themed-dashboard-intro'
function SidebarDsNav() {
const { t } = useTranslation()
@@ -38,6 +40,12 @@ function SidebarDsNav() {
) as NavbarDropdownItemData
const { containerRef, scrolledUp, scrolledDown } = useScrolled()
const themedDsNav = useFeatureFlag('themed-project-dashboard')
const {
completeThemedDashboardIntro,
dismissThemedDashboardIntro,
targetRef,
showingThemedDashboardIntro,
} = useThemedDashboardIntro()
return (
<div
@@ -131,6 +139,12 @@ function SidebarDsNav() {
item: 'account',
location: 'sidebar',
})
if (showingThemedDashboardIntro) {
completeThemedDashboardIntro({
action: 'complete',
event: 'promo-click',
})
}
}
}}
role="menu"
@@ -144,7 +158,7 @@ function SidebarDsNav() {
}}
hidden={showAccountDropdown}
>
<div>
<div ref={targetRef}>
<User size={24} />
</div>
</OLTooltip>
@@ -166,7 +180,16 @@ function SidebarDsNav() {
/>
</Dropdown.Menu>
</Dropdown>
<UserProvider>{contactUsModal}</UserProvider>
<UserProvider>
{themedDsNav && (
<ThemedProjectDashboardNotification
target={targetRef.current}
show={showingThemedDashboardIntro}
onDismiss={dismissThemedDashboardIntro}
/>
)}
{contactUsModal}
</UserProvider>
</>
)}
</nav>
@@ -0,0 +1,102 @@
import Close from '@/shared/components/close'
import OLButton from '@/shared/components/ol/ol-button'
import { useUserContext } from '@/shared/context/user-context'
import { useUserSettingsContext } from '@/shared/context/user-settings-context'
import { useActiveOverallTheme } from '@/shared/hooks/use-active-overall-theme'
import { Overlay, Popover } from 'react-bootstrap'
import { useTranslation } from 'react-i18next'
// TODO: Update this before release
const NEW_USER_CUTOFF_DATE = new Date('2026-02-15')
type ThemedProjectDashboardNotificationProps = {
target: HTMLElement | null
show: boolean
onDismiss: () => void
}
export const ThemedProjectDashboardNotification = ({
target,
show,
onDismiss,
}: ThemedProjectDashboardNotificationProps) => {
const theme = useActiveOverallTheme('themed-project-dashboard')
const {
userSettings: { overallTheme },
} = useUserSettingsContext()
const { signUpDate: signUpDateString } = useUserContext()
const signUpDate = signUpDateString ? new Date(signUpDateString) : new Date(0)
const isNewUser = signUpDate > NEW_USER_CUTOFF_DATE
const { t } = useTranslation()
if (!target) {
return null
}
if (!show) {
return null
}
let content
switch (true) {
case overallTheme === 'system':
content = {
header: t('a_dashboard_that_follows_your_lead'),
body: t(
'your_dashboard_is_set_to_match_your_system_theme_automatically_want_a_different_look_pick_your_favorite_theme_here'
),
}
break
case isNewUser && theme === 'dark':
content = {
header: t('meet_the_new_dark_dashboard'),
body: t(
'weve_set_your_dashboard_to_dark_mode_to_help_you_stay_focused_if_youre_a_fan_of_a_lighter_look_you_can_easily_switch_themes_here'
),
}
break
case theme === 'dark':
content = {
header: t('welcome_to_the_dark_side'),
body: t(
'weve_given_your_dashboard_a_sleek_new_dark_theme_for_more_comfortable_late_night_research_prefer_the_light_switch_back_anytime_right_here'
),
}
break
case theme === 'light':
content = {
header: t('fancy_going_dark'),
body: t(
'weve_matched_your_dashboard_theme_to_your_editor_preferences_but_you_can_change_that_here_anytime'
),
}
break
default:
content = { header: '', body: '' }
}
if (!content.header || !content.body) {
return null
}
return (
<Overlay show placement="right" target={target} onHide={onDismiss}>
<Popover className="themed-dashboard-intro-popover">
<Popover.Header>
{content.header}
<Close
variant={theme === 'light' ? 'dark' : 'light'}
onDismiss={onDismiss}
/>
</Popover.Header>
<Popover.Body>
{content.body}
<div className="d-flex justify-content-end">
<OLButton onClick={onDismiss} variant="link">
{t('got_it')}
</OLButton>
</div>
</Popover.Body>
</Popover>
</Overlay>
)
}
@@ -0,0 +1,38 @@
import { useFeatureFlag } from '@/shared/context/split-test-context'
import { useTutorialContext } from '@/shared/context/tutorial-context'
import useTutorial from '@/shared/hooks/promotions/use-tutorial'
import { useCallback, useEffect, useRef } from 'react'
const THEMED_DASHBOARD_TUTORIAL_KEY = 'themed-dashboard-intro'
export const useThemedDashboardIntro = () => {
const themedDsNav = useFeatureFlag('themed-project-dashboard')
const targetRef = useRef<HTMLDivElement | null>(null)
const { inactiveTutorials } = useTutorialContext()
const {
tryShowingPopup: tryShowingPopupThemedDashboardIntro,
showPopup: showingThemedDashboardIntro,
completeTutorial: completeThemedDashboardIntro,
dismissTutorial,
} = useTutorial(THEMED_DASHBOARD_TUTORIAL_KEY, {
name: THEMED_DASHBOARD_TUTORIAL_KEY,
})
const dismissThemedDashboardIntro = useCallback(() => {
dismissTutorial()
}, [dismissTutorial])
useEffect(() => {
if (
themedDsNav &&
!inactiveTutorials.includes(THEMED_DASHBOARD_TUTORIAL_KEY)
) {
tryShowingPopupThemedDashboardIntro()
}
}, [inactiveTutorials, tryShowingPopupThemedDashboardIntro, themedDsNav])
return {
targetRef,
showingThemedDashboardIntro,
completeThemedDashboardIntro,
dismissThemedDashboardIntro,
}
}