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-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")

View file

@ -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": "",

View file

@ -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 = (
<div className="alert alert-info grammarly-advert" role="alert">
<div className="grammarly-advert-container">
<div className="advert-content">
<p>
Love Grammarly? Then you're in luck! Get 25% off Grammarly Premium
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"
const actions = (
<div>
<Button
bsStyle={null}
className="btn-secondary"
onClick={handleClickClaim}
>
Claim my discount
</a>
</div>
<div className="grammarly-notification-close-btn">
<button aria-label="Close" onClick={handleClose}>
<MaterialIcon type="close" />
</button>
</div>
</div>
{t('claim_discount')}
</Button>
<Button className="btn-bg-ghost" bsStyle={null} onClick={handleLater}>
{t('maybe_later')}
</Button>
</div>
)
const promoOffer = (
<div className="alert alert-info grammarly-advert" role="alert">
<div className="grammarly-advert-container">
<div className="advert-content">
return (
<Notification
action={actions}
ariaLive="polite"
className="editor-notification ol-overlay"
content={
<div>
<p>
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.
</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 className="grammarly-notification-close-btn">
<button aria-label="Close" onClick={handleClose}>
<MaterialIcon type="close" />
</button>
</div>
</div>
</div>
)
return promotionEnded ? permanentOffer : promoOffer
}
customIcon={
<div>
<GrammarlyLogo width="50" height="50" background="#fff" />
</div>
}
isActionBelowContent
isDismissible
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 { 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 }) => (
<Component key={path} />
))}
<GrammarlyAdvert />
<CodeMirrorEditor />
</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'] {
z-index: 1;
}
.writefull-notification {
.editor-notification {
margin: 48px 64px;
width: 80%;
max-width: 560px;

View file

@ -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",