diff --git a/services/web/app/src/Features/Project/ProjectListController.js b/services/web/app/src/Features/Project/ProjectListController.js
index 24ed4ac0cc..b891f2cbfc 100644
--- a/services/web/app/src/Features/Project/ProjectListController.js
+++ b/services/web/app/src/Features/Project/ProjectListController.js
@@ -21,6 +21,7 @@ const UserPrimaryEmailCheckHandler = require('../User/UserPrimaryEmailCheckHandl
const UserController = require('../User/UserController')
const LimitationsManager = require('../Subscription/LimitationsManager')
const NotificationsBuilder = require('../Notifications/NotificationsBuilder')
+const SplitTestHandler = require('../SplitTests/SplitTestHandler')
/** @typedef {import("./types").GetProjectsRequest} GetProjectsRequest */
/** @typedef {import("./types").GetProjectsResponse} GetProjectsResponse */
@@ -319,6 +320,23 @@ async function projectListPage(req, res, next) {
showGroupsAndEnterpriseBanner &&
_.sample(['did-you-know', 'on-premise', 'people', 'FOMO'])
+ let showWritefullPromoBanner = false
+ if (Features.hasFeature('saas') && !req.session.justRegistered) {
+ try {
+ const { variant } = await SplitTestHandler.promises.getAssignment(
+ req,
+ res,
+ 'writefull-promo-banner'
+ )
+ showWritefullPromoBanner = variant === 'enabled'
+ } catch (error) {
+ logger.warn(
+ { err: error },
+ 'failed to get "writefull-promo-banner" split test assignment'
+ )
+ }
+ }
+
res.render('project/list-react', {
title: 'your_projects',
usersBestSubscription,
@@ -335,6 +353,7 @@ async function projectListPage(req, res, next) {
prefetchedProjectsBlob,
showGroupsAndEnterpriseBanner,
groupsAndEnterpriseBannerVariant,
+ showWritefullPromoBanner,
projectDashboardReact: true, // used in navbar
})
}
diff --git a/services/web/app/views/project/list-react.pug b/services/web/app/views/project/list-react.pug
index ca30d22469..f223e9acf3 100644
--- a/services/web/app/views/project/list-react.pug
+++ b/services/web/app/views/project/list-react.pug
@@ -26,6 +26,7 @@ block append meta
}))
meta(name="ol-currentUrl" data-type="string" content=currentUrl)
meta(name="ol-showGroupsAndEnterpriseBanner" data-type="boolean" content=showGroupsAndEnterpriseBanner)
+ meta(name="ol-showWritefullPromoBanner" data-type="boolean" content=showWritefullPromoBanner)
meta(name="ol-groupsAndEnterpriseBannerVariant" data-type="string" content=groupsAndEnterpriseBannerVariant)
block content
diff --git a/services/web/frontend/js/features/project-list/components/notifications/user-notifications.tsx b/services/web/frontend/js/features/project-list/components/notifications/user-notifications.tsx
index 948e9c0613..72f8cba2d2 100644
--- a/services/web/frontend/js/features/project-list/components/notifications/user-notifications.tsx
+++ b/services/web/frontend/js/features/project-list/components/notifications/user-notifications.tsx
@@ -3,6 +3,7 @@ import Institution from './groups/institution'
import ConfirmEmail from './groups/confirm-email'
import ReconfirmationInfo from './groups/affiliation/reconfirmation-info'
import GroupsAndEnterpriseBanner from './groups-and-enterprise-banner'
+import WritefullPromoBanner from './writefull-promo-banner'
function UserNotifications() {
return (
@@ -13,6 +14,7 @@ function UserNotifications() {
+
)
diff --git a/services/web/frontend/js/features/project-list/components/notifications/writefull-promo-banner.tsx b/services/web/frontend/js/features/project-list/components/notifications/writefull-promo-banner.tsx
new file mode 100644
index 0000000000..6f1eb8cfee
--- /dev/null
+++ b/services/web/frontend/js/features/project-list/components/notifications/writefull-promo-banner.tsx
@@ -0,0 +1,77 @@
+import { memo, useCallback, useState } from 'react'
+import Notification from './notification'
+import { sendMB } from '../../../../infrastructure/event-tracking'
+import getMeta from '../../../../utils/meta'
+import customLocalStorage from '../../../../infrastructure/local-storage'
+
+const STORAGE_KEY = 'has_dismissed_writefull_promo_banner'
+
+const eventSegmentation = {
+ location: 'dashboard-banner',
+ page: '/project',
+ name: 'writefull',
+}
+
+function WritefullPromoBanner() {
+ const [show, setShow] = useState(() => {
+ const show =
+ getMeta('ol-showWritefullPromoBanner') &&
+ !customLocalStorage.getItem(STORAGE_KEY)
+
+ if (show) {
+ sendMB('promo-prompt', eventSegmentation)
+ }
+
+ return show
+ })
+
+ const handleOpenLink = useCallback(() => {
+ sendMB('promo-click', eventSegmentation)
+ }, [])
+
+ const handleClose = useCallback(() => {
+ customLocalStorage.setItem(STORAGE_KEY, new Date())
+ setShow(false)
+ sendMB('promo-dismiss', eventSegmentation)
+ }, [])
+
+ if (!show) {
+ return null
+ }
+
+ return (
+
+
+
+ Get 10% off Writefull premium—AI-based language feedback and
+ TeXGPT to help you write great papers faster. Use code:{' '}
+ OVERLEAF10
+
+
+
+
+
+ Get Writefull for Overleaf
+
+
+
+ )
+}
+
+export default memo(WritefullPromoBanner)
diff --git a/services/web/frontend/stylesheets/app/project-list.less b/services/web/frontend/stylesheets/app/project-list.less
index afffeb558c..ad37c1961d 100644
--- a/services/web/frontend/stylesheets/app/project-list.less
+++ b/services/web/frontend/stylesheets/app/project-list.less
@@ -248,6 +248,9 @@ input.project-list-table-select-item[type='checkbox'] {
flex-wrap: nowrap;
}
}
+ &.centered-alert .alert {
+ align-items: center;
+ }
}
}
.notification-body {