mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
Merge pull request #16319 from overleaf/jdt-grammarly-ad-redesign
Grammarly Ad redesign GitOrigin-RevId: 28d0ae871b6303b31aadb59abc80b625d529cc9b
This commit is contained in:
parent
5c5a01777c
commit
5f38a930a5
8 changed files with 175 additions and 75 deletions
|
@ -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")
|
||||||
|
|
|
@ -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": "",
|
||||||
|
|
|
@ -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"
|
||||||
|
/>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
)
|
)
|
||||||
|
|
54
services/web/frontend/js/shared/hooks/use-remind-me-later.ts
Normal file
54
services/web/frontend/js/shared/hooks/use-remind-me-later.ts
Normal 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 }
|
||||||
|
}
|
39
services/web/frontend/js/shared/svgs/grammarly-logo.tsx
Normal file
39
services/web/frontend/js/shared/svgs/grammarly-logo.tsx
Normal file
File diff suppressed because one or more lines are too long
|
@ -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;
|
||||||
|
|
|
@ -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",
|
||||||
|
|
Loading…
Reference in a new issue