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-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")
|
||||
|
|
|
@ -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": "",
|
||||
|
|
|
@ -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"
|
||||
>
|
||||
Claim my discount
|
||||
</a>
|
||||
</div>
|
||||
<div className="grammarly-notification-close-btn">
|
||||
<button aria-label="Close" onClick={handleClose}>
|
||||
<MaterialIcon type="close" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
const actions = (
|
||||
<div>
|
||||
<Button
|
||||
bsStyle={null}
|
||||
className="btn-secondary"
|
||||
onClick={handleClickClaim}
|
||||
>
|
||||
{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>
|
||||
}
|
||||
customIcon={
|
||||
<div>
|
||||
<GrammarlyLogo width="50" height="50" background="#fff" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
isActionBelowContent
|
||||
isDismissible
|
||||
onDismiss={handleDismiss}
|
||||
title="Love Grammarly? Then you're in luck!"
|
||||
type="offer"
|
||||
/>
|
||||
)
|
||||
return promotionEnded ? permanentOffer : promoOffer
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
|
|
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'] {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.writefull-notification {
|
||||
.editor-notification {
|
||||
margin: 48px 64px;
|
||||
width: 80%;
|
||||
max-width: 560px;
|
||||
|
|
|
@ -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",
|
||||
|
|
Loading…
Reference in a new issue