Просмотр исходного кода

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

Grammarly Ad redesign

GitOrigin-RevId: 28d0ae871b6303b31aadb59abc80b625d529cc9b
Jimmy Domagala-Tang 2 лет назад
Родитель
Сommit
5f38a930a5

+ 4 - 6
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")

+ 1 - 0
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": "",

+ 73 - 60
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 = (
-    <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
 }

+ 2 - 0
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 }) => (
         <Component key={path} />
       ))}
+      <GrammarlyAdvert />
       <CodeMirrorEditor />
     </Suspense>
   )

+ 54 - 0
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 }
+}

Разница между файлами не показана из-за своего большого размера
+ 31 - 0
services/web/frontend/js/shared/svgs/grammarly-logo.tsx


+ 1 - 9
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;

+ 1 - 0
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",

Некоторые файлы не были показаны из-за большого количества измененных файлов