diff --git a/services/web/app/views/project/editor.pug b/services/web/app/views/project/editor.pug index 2d5543483f..4cc2bd0175 100644 --- a/services/web/app/views/project/editor.pug +++ b/services/web/app/views/project/editor.pug @@ -60,20 +60,18 @@ block content ng-controller="SystemMessageController" ng-hide="hidden" ) - button(ng-hide="protected",ng-click="hide()").close.pull-right + button(ng-hide="protected" ng-click="hide()").close.pull-right span(aria-hidden="true") × span.sr-only #{translate("close")} .system-message-content | {{htmlContent}} - grammarly-advert() - if hasFeature('saas') legacy-editor-warning(delay=10000) include ./editor/main - script(type="text/ng-template", id="genericMessageModalTemplate") + script(type="text/ng-template" id="genericMessageModalTemplate") .modal-header button.close( type="button" @@ -87,7 +85,7 @@ block content .modal-footer button.btn.btn-info(ng-click="done()") #{translate("ok")} - script(type="text/ng-template", id="outOfSyncModalTemplate") + script(type="text/ng-template" id="outOfSyncModalTemplate") .modal-header button.close( type="button" @@ -112,7 +110,7 @@ block content .modal-footer button.btn.btn-info(ng-click="done()") #{translate("reload_editor")} - script(type="text/ng-template", id="lockEditorModalTemplate") + script(type="text/ng-template" id="lockEditorModalTemplate") .modal-header h3 {{ title }} .modal-body(ng-bind-html="message") diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json index 2d90cf658f..723d1f27d0 100644 --- a/services/web/frontend/extracted-translations.json +++ b/services/web/frontend/extracted-translations.json @@ -149,6 +149,7 @@ "checking_project_github_status": "", "choose_a_custom_color": "", "choose_from_group_members": "", + "claim_discount": "", "clear_cached_files": "", "clear_search": "", "click_here_to_view_sl_in_lng": "", diff --git a/services/web/frontend/js/features/source-editor/components/grammarly-advert.tsx b/services/web/frontend/js/features/source-editor/components/grammarly-advert.tsx index 67608144f5..bf795d7d25 100644 --- a/services/web/frontend/js/features/source-editor/components/grammarly-advert.tsx +++ b/services/web/frontend/js/features/source-editor/components/grammarly-advert.tsx @@ -1,91 +1,104 @@ import { useCallback, useEffect, useState } from 'react' -import MaterialIcon from '@/shared/components/material-icon' +import { Button } from 'react-bootstrap' +import { useTranslation } from 'react-i18next' +import Notification from '@/shared/components/notification' +import useRemindMeLater from '@/shared/hooks/use-remind-me-later' +import GrammarlyLogo from '@/shared/svgs/grammarly-logo' import * as eventTracking from '../../../infrastructure/event-tracking' -import customLocalStorage from '../../../infrastructure/local-storage' import useWaitForGrammarlyCheck from '@/shared/hooks/use-wait-for-grammarly-check' + export default function GrammarlyAdvert() { const [show, setShow] = useState(false) + const { t } = useTranslation() // grammarly can take some time to load, we should assume its installed and hide until we know for sure const grammarlyInstalled = useWaitForGrammarlyCheck({ initialState: false }) - useEffect(() => { - const hasDismissedGrammarlyAdvert = customLocalStorage.getItem( - 'editor.has_dismissed_grammarly_advert' - ) + const { stillDissmissed, remindThemLater, saveDismissed } = + useRemindMeLater('grammarly_advert') - const showGrammarlyAdvert = - grammarlyInstalled && !hasDismissedGrammarlyAdvert + useEffect(() => { + const showGrammarlyAdvert = grammarlyInstalled && !stillDissmissed if (showGrammarlyAdvert) { eventTracking.sendMB('grammarly-advert-shown') setShow(true) } - }, [grammarlyInstalled, setShow]) + }, [stillDissmissed, grammarlyInstalled, setShow]) - const handleClose = useCallback(() => { + const handleDismiss = useCallback(() => { setShow(false) - customLocalStorage.setItem('editor.has_dismissed_grammarly_advert', true) + saveDismissed() eventTracking.sendMB('grammarly-advert-dismissed') - }, []) + }, [saveDismissed]) + + const handleClickClaim = useCallback(() => { + eventTracking.sendMB('promo-click', { + location: 'notification', + name: 'grammarly-advert', + type: 'click', + }) + + saveDismissed() + setShow(false) + + window.open( + 'https://grammarly.go2cloud.org/aff_c?offer_id=372&aff_id=142242' + ) + }, [saveDismissed]) + + const handleLater = useCallback(() => { + eventTracking.sendMB('promo-click', { + location: 'notification', + name: 'grammarly-advert', + type: 'pause', + }) + setShow(false) + remindThemLater() + }, [remindThemLater]) if (!show) { return null } - // promotion ends on december 16th, 2023 at 00:00 UTC - const promotionEnded = new Date() > new Date(Date.UTC(2023, 11, 16, 0, 0, 0)) - const permanentOffer = ( -
-
-
-

- Love Grammarly? Then you're in luck! Get 25% off Grammarly Premium - with this exclusive offer for Overleaf users. -

- eventTracking.sendMB('grammarly-advert-clicked')} - href="https://grammarly.go2cloud.org/aff_c?offer_id=373&aff_id=142242" - target="_blank" - rel="noopener" - > - Claim my discount - -
-
- -
-
+ const actions = ( +
+ +
) - const promoOffer = ( -
-
-
+ + return ( +

- Overleafers get a limited-time 30% discount on Grammarly Premium. - (Hurry! Offer ends December 15.) + Get 25% off Grammarly Premium with this exclusive offer for Overleaf + users.

- eventTracking.sendMB('grammarly-advert-clicked')} - href="https://grammarly.go2cloud.org/aff_c?offer_id=372&aff_id=142242" - target="_blank" - rel="noopener" - > - Claim my discount -
-
- + } + customIcon={ +
+
-
-
+ } + isActionBelowContent + isDismissible + onDismiss={handleDismiss} + title="Love Grammarly? Then you're in luck!" + type="offer" + /> ) - return promotionEnded ? permanentOffer : promoOffer } diff --git a/services/web/frontend/js/features/source-editor/components/source-editor.tsx b/services/web/frontend/js/features/source-editor/components/source-editor.tsx index 84d60cdab6..7466912998 100644 --- a/services/web/frontend/js/features/source-editor/components/source-editor.tsx +++ b/services/web/frontend/js/features/source-editor/components/source-editor.tsx @@ -3,6 +3,7 @@ import { FullSizeLoadingSpinner } from '../../../shared/components/loading-spinn import withErrorBoundary from '../../../infrastructure/error-boundary' import { ErrorBoundaryFallback } from '../../../shared/components/error-boundary-fallback' import importOverleafModules from '../../../../macros/import-overleaf-module.macro' +import GrammarlyAdvert from './grammarly-advert' const writefullPromotion = importOverleafModules( 'writefullEditorPromotion' @@ -22,6 +23,7 @@ function SourceEditor() { {writefullPromotion.map(({ import: { default: Component }, path }) => ( ))} + ) diff --git a/services/web/frontend/js/shared/hooks/use-remind-me-later.ts b/services/web/frontend/js/shared/hooks/use-remind-me-later.ts new file mode 100644 index 0000000000..321037ff6b --- /dev/null +++ b/services/web/frontend/js/shared/hooks/use-remind-me-later.ts @@ -0,0 +1,54 @@ +import { useState, useCallback, useEffect } from 'react' +import customLocalStorage from '@/infrastructure/local-storage' +import usePersistedState from '@/shared/hooks/use-persisted-state' + +/** + * @typedef {Object} RemindMeLater + * @property {boolean} stillDissmissed - whether the user has dismissed the notification, or if the notification is still withing the 1 day reminder period + * @property {function} remindThemLater - saves that the user has dismissed the notification for 1 day in local storage + * @property {function} saveDismissed - saves that the user has dismissed the notification in local storage + */ + +/** + * + * @param {string} key the unique key used to keep track of what popup is currently being shown (usually the component name) + * @param {string} notificationLocation what page the notification originates from (eg, the editor page, project page, etc) + * @returns {RemindMeLater} an object containing whether the notification is still dismissed, and functions to remind the user later or save that they have dismissed the notification + */ +export default function useRemindMeLater( + key: string, + notificationLocation: string = 'editor' +) { + const [dismissedUntil, setDismissedUntil] = usePersistedState< + Date | undefined + >(`${notificationLocation}.has_dismissed_${key}_until`) + + const [stillDissmissed, setStillDismissed] = useState(true) + + useEffect(() => { + const alertDismissed = customLocalStorage.getItem( + `${notificationLocation}.has_dismissed_${key}` + ) + + const isStillDismissed = Boolean( + dismissedUntil && new Date(dismissedUntil) > new Date() + ) + + setStillDismissed(alertDismissed || isStillDismissed) + }, [setStillDismissed, dismissedUntil, key, notificationLocation]) + + const remindThemLater = useCallback(() => { + const until = new Date() + until.setDate(until.getDate() + 1) // 1 day + setDismissedUntil(until) + }, [setDismissedUntil]) + + const saveDismissed = useCallback(() => { + customLocalStorage.setItem( + `${notificationLocation}.has_dismissed_${key}`, + true + ) + }, [key, notificationLocation]) + + return { stillDissmissed, remindThemLater, saveDismissed } +} diff --git a/services/web/frontend/js/shared/svgs/grammarly-logo.tsx b/services/web/frontend/js/shared/svgs/grammarly-logo.tsx new file mode 100644 index 0000000000..be48b89cbc --- /dev/null +++ b/services/web/frontend/js/shared/svgs/grammarly-logo.tsx @@ -0,0 +1,39 @@ +function GrammarlyLogo({ width = '40', height = '40', background = 'none' }) { + return ( + + + + + + + + + + ) +} + +export default GrammarlyLogo diff --git a/services/web/frontend/stylesheets/app/editor.less b/services/web/frontend/stylesheets/app/editor.less index d52d726a08..8954496a1d 100644 --- a/services/web/frontend/stylesheets/app/editor.less +++ b/services/web/frontend/stylesheets/app/editor.less @@ -742,19 +742,11 @@ CodeMirror } } -.grammarly-advert-container { - display: flex; - - .grammarly-notification-close-btn > button { - background-color: @ol-blue; - } -} - grammarly-extension[data-grammarly-shadow-root='true'] { z-index: 1; } -.writefull-notification { +.editor-notification { margin: 48px 64px; width: 80%; max-width: 560px; diff --git a/services/web/locales/en.json b/services/web/locales/en.json index e56ea5ccc6..ee18912ecd 100644 --- a/services/web/locales/en.json +++ b/services/web/locales/en.json @@ -239,6 +239,7 @@ "choose_from_group_members": "Choose from group members", "choose_your_plan": "Choose your plan", "city": "City", + "claim_discount": "Claim discount", "clear_cached_files": "Clear cached files", "clear_search": "clear search", "clear_sessions": "Clear Sessions",