Merge pull request #16319 from overleaf/jdt-grammarly-ad-redesign

Grammarly Ad redesign

GitOrigin-RevId: 28d0ae871b6303b31aadb59abc80b625d529cc9b
This commit is contained in:
Jimmy Domagala-Tang 2024-01-05 11:06:52 -05:00 committed by Copybot
parent 5c5a01777c
commit 5f38a930a5
8 changed files with 175 additions and 75 deletions

View file

@ -60,20 +60,18 @@ block content
ng-controller="SystemMessageController" ng-controller="SystemMessageController"
ng-hide="hidden" 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(aria-hidden="true") ×
span.sr-only #{translate("close")} span.sr-only #{translate("close")}
.system-message-content .system-message-content
| {{htmlContent}} | {{htmlContent}}
grammarly-advert()
if hasFeature('saas') if hasFeature('saas')
legacy-editor-warning(delay=10000) legacy-editor-warning(delay=10000)
include ./editor/main include ./editor/main
script(type="text/ng-template", id="genericMessageModalTemplate") script(type="text/ng-template" id="genericMessageModalTemplate")
.modal-header .modal-header
button.close( button.close(
type="button" type="button"
@ -87,7 +85,7 @@ block content
.modal-footer .modal-footer
button.btn.btn-info(ng-click="done()") #{translate("ok")} 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 .modal-header
button.close( button.close(
type="button" type="button"
@ -112,7 +110,7 @@ block content
.modal-footer .modal-footer
button.btn.btn-info(ng-click="done()") #{translate("reload_editor")} 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 .modal-header
h3 {{ title }} h3 {{ title }}
.modal-body(ng-bind-html="message") .modal-body(ng-bind-html="message")

View file

@ -149,6 +149,7 @@
"checking_project_github_status": "", "checking_project_github_status": "",
"choose_a_custom_color": "", "choose_a_custom_color": "",
"choose_from_group_members": "", "choose_from_group_members": "",
"claim_discount": "",
"clear_cached_files": "", "clear_cached_files": "",
"clear_search": "", "clear_search": "",
"click_here_to_view_sl_in_lng": "", "click_here_to_view_sl_in_lng": "",

View file

@ -1,91 +1,104 @@
import { useCallback, useEffect, useState } from 'react' 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 * as eventTracking from '../../../infrastructure/event-tracking'
import customLocalStorage from '../../../infrastructure/local-storage'
import useWaitForGrammarlyCheck from '@/shared/hooks/use-wait-for-grammarly-check' import useWaitForGrammarlyCheck from '@/shared/hooks/use-wait-for-grammarly-check'
export default function GrammarlyAdvert() { export default function GrammarlyAdvert() {
const [show, setShow] = useState(false) 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 // grammarly can take some time to load, we should assume its installed and hide until we know for sure
const grammarlyInstalled = useWaitForGrammarlyCheck({ initialState: false }) const grammarlyInstalled = useWaitForGrammarlyCheck({ initialState: false })
useEffect(() => { const { stillDissmissed, remindThemLater, saveDismissed } =
const hasDismissedGrammarlyAdvert = customLocalStorage.getItem( useRemindMeLater('grammarly_advert')
'editor.has_dismissed_grammarly_advert'
)
const showGrammarlyAdvert = useEffect(() => {
grammarlyInstalled && !hasDismissedGrammarlyAdvert const showGrammarlyAdvert = grammarlyInstalled && !stillDissmissed
if (showGrammarlyAdvert) { if (showGrammarlyAdvert) {
eventTracking.sendMB('grammarly-advert-shown') eventTracking.sendMB('grammarly-advert-shown')
setShow(true) setShow(true)
} }
}, [grammarlyInstalled, setShow]) }, [stillDissmissed, grammarlyInstalled, setShow])
const handleClose = useCallback(() => { const handleDismiss = useCallback(() => {
setShow(false) setShow(false)
customLocalStorage.setItem('editor.has_dismissed_grammarly_advert', true) saveDismissed()
eventTracking.sendMB('grammarly-advert-dismissed') 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) { if (!show) {
return null 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 = ( const actions = (
<div className="alert alert-info grammarly-advert" role="alert"> <div>
<div className="grammarly-advert-container"> <Button
<div className="advert-content"> bsStyle={null}
<p> className="btn-secondary"
Love Grammarly? Then you're in luck! Get 25% off Grammarly Premium onClick={handleClickClaim}
with this exclusive offer for Overleaf users.
</p>
<a
className="advert-link"
onClick={() => 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 {t('claim_discount')}
</a> </Button>
</div> <Button className="btn-bg-ghost" bsStyle={null} onClick={handleLater}>
<div className="grammarly-notification-close-btn"> {t('maybe_later')}
<button aria-label="Close" onClick={handleClose}> </Button>
<MaterialIcon type="close" />
</button>
</div>
</div>
</div> </div>
) )
const promoOffer = (
<div className="alert alert-info grammarly-advert" role="alert"> return (
<div className="grammarly-advert-container"> <Notification
<div className="advert-content"> action={actions}
ariaLive="polite"
className="editor-notification ol-overlay"
content={
<div>
<p> <p>
Overleafers get a limited-time 30% discount on Grammarly Premium. Get 25% off Grammarly Premium with this exclusive offer for Overleaf
(Hurry! Offer ends December 15.) users.
</p> </p>
<a
className="advert-link"
onClick={() => 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
</a>
</div> </div>
<div className="grammarly-notification-close-btn"> }
<button aria-label="Close" onClick={handleClose}> customIcon={
<MaterialIcon type="close" /> <div>
</button> <GrammarlyLogo width="50" height="50" background="#fff" />
</div> </div>
</div> }
</div> isActionBelowContent
) isDismissible
return promotionEnded ? permanentOffer : promoOffer onDismiss={handleDismiss}
title="Love Grammarly? Then you're in luck!"
type="offer"
/>
)
} }

View file

@ -3,6 +3,7 @@ import { FullSizeLoadingSpinner } from '../../../shared/components/loading-spinn
import withErrorBoundary from '../../../infrastructure/error-boundary' import withErrorBoundary from '../../../infrastructure/error-boundary'
import { ErrorBoundaryFallback } from '../../../shared/components/error-boundary-fallback' import { ErrorBoundaryFallback } from '../../../shared/components/error-boundary-fallback'
import importOverleafModules from '../../../../macros/import-overleaf-module.macro' import importOverleafModules from '../../../../macros/import-overleaf-module.macro'
import GrammarlyAdvert from './grammarly-advert'
const writefullPromotion = importOverleafModules( const writefullPromotion = importOverleafModules(
'writefullEditorPromotion' 'writefullEditorPromotion'
@ -22,6 +23,7 @@ function SourceEditor() {
{writefullPromotion.map(({ import: { default: Component }, path }) => ( {writefullPromotion.map(({ import: { default: Component }, path }) => (
<Component key={path} /> <Component key={path} />
))} ))}
<GrammarlyAdvert />
<CodeMirrorEditor /> <CodeMirrorEditor />
</Suspense> </Suspense>
) )

View file

@ -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 }
}

File diff suppressed because one or more lines are too long

View file

@ -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'] { grammarly-extension[data-grammarly-shadow-root='true'] {
z-index: 1; z-index: 1;
} }
.writefull-notification { .editor-notification {
margin: 48px 64px; margin: 48px 64px;
width: 80%; width: 80%;
max-width: 560px; max-width: 560px;

View file

@ -239,6 +239,7 @@
"choose_from_group_members": "Choose from group members", "choose_from_group_members": "Choose from group members",
"choose_your_plan": "Choose your plan", "choose_your_plan": "Choose your plan",
"city": "City", "city": "City",
"claim_discount": "Claim discount",
"clear_cached_files": "Clear cached files", "clear_cached_files": "Clear cached files",
"clear_search": "clear search", "clear_search": "clear search",
"clear_sessions": "Clear Sessions", "clear_sessions": "Clear Sessions",