diff --git a/services/web/app/src/Features/Project/ProjectListController.js b/services/web/app/src/Features/Project/ProjectListController.js index 52abb030a0..8dfcf2c226 100644 --- a/services/web/app/src/Features/Project/ProjectListController.js +++ b/services/web/app/src/Features/Project/ProjectListController.js @@ -308,6 +308,22 @@ async function projectListPage(req, res, next) { } } + let welcomePageRedesignAssignment = { variant: 'default' } + + try { + welcomePageRedesignAssignment = + await SplitTestHandler.promises.getAssignment( + req, + res, + 'welcome-page-redesign' + ) + } catch (error) { + logger.error( + { err: error }, + 'failed to get "welcome-page-redesign" split test assignment' + ) + } + const hasPaidAffiliation = userAffiliations.some( affiliation => affiliation.licence && affiliation.licence !== 'free' ) @@ -378,6 +394,7 @@ async function projectListPage(req, res, next) { showWritefullPromoBanner, showINRBanner, projectDashboardReact: true, // used in navbar + welcomePageRedesignVariant: welcomePageRedesignAssignment.variant, }) } diff --git a/services/web/app/views/project/list-react.pug b/services/web/app/views/project/list-react.pug index 2ba0581079..3cd9f7f250 100644 --- a/services/web/app/views/project/list-react.pug +++ b/services/web/app/views/project/list-react.pug @@ -29,6 +29,7 @@ block append meta meta(name="ol-showWritefullPromoBanner" data-type="boolean" content=showWritefullPromoBanner) meta(name="ol-groupsAndEnterpriseBannerVariant" data-type="string" content=groupsAndEnterpriseBannerVariant) meta(name="ol-showINRBanner" data-type="boolean" content=showINRBanner) + meta(name="ol-welcomePageRedesignVariant" data-type="string" content=welcomePageRedesignVariant) block content main.content.content-alt.project-list-react#project-list-root diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json index 3e41611f5e..287cd8bfd1 100644 --- a/services/web/frontend/extracted-translations.json +++ b/services/web/frontend/extracted-translations.json @@ -167,6 +167,7 @@ "coupon_code_is_not_valid_for_selected_plan": "", "coupons_not_included": "", "create": "", + "create_a_new_project": "", "create_first_project": "", "create_new_subscription": "", "create_new_tag": "", @@ -475,6 +476,7 @@ "institution_account": "", "institution_acct_successfully_linked_2": "", "institution_and_role": "", + "institution_templates": "", "institutional_leavers_survey_notification": "", "integrations": "", "interested_in_cheaper_personal_plan": "", diff --git a/services/web/frontend/js/features/project-list/components/new-project-button.tsx b/services/web/frontend/js/features/project-list/components/new-project-button.tsx index 350d4cb89d..fba4f746d6 100644 --- a/services/web/frontend/js/features/project-list/components/new-project-button.tsx +++ b/services/web/frontend/js/features/project-list/components/new-project-button.tsx @@ -20,7 +20,7 @@ type SendTrackingEvent = { } type Segmentation = SendTrackingEvent & { - 'project-dashboard-react': 'enabled' + 'welcome-page-redesign': 'default' } type ModalMenuClickOptions = { @@ -60,7 +60,7 @@ function NewProjectButton({ }: SendTrackingEvent) => { if (trackingKey) { let segmentation: Segmentation = { - 'project-dashboard-react': 'enabled', + 'welcome-page-redesign': 'default', dropdownMenu, dropdownOpen, } diff --git a/services/web/frontend/js/features/project-list/components/project-list-root.tsx b/services/web/frontend/js/features/project-list/components/project-list-root.tsx index 4d6166e59c..b3d38ed69b 100644 --- a/services/web/frontend/js/features/project-list/components/project-list-root.tsx +++ b/services/web/frontend/js/features/project-list/components/project-list-root.tsx @@ -23,6 +23,8 @@ import ProjectListTitle from './title/project-list-title' import Sidebar from './sidebar/sidebar' import LoadMore from './load-more' import { useEffect } from 'react' +import getMeta from '../../../utils/meta' +import WelcomeMessageNew from './welcome-message-new' function ProjectListRoot() { const { isReady } = useWaitForI18n() @@ -51,6 +53,9 @@ function ProjectListPageContent() { } = useProjectListContext() const selectedTag = tags.find(tag => tag._id === selectedTagId) + const welcomePageRedesignVariant = getMeta( + 'ol-welcomePageRedesignVariant' + ) as 'enabled' | 'default' useEffect(() => { eventTracking.sendMB('loads_v2_dash', {}) @@ -159,7 +164,11 @@ function ProjectListPageContent() { - + {welcomePageRedesignVariant === 'enabled' ? ( + + ) : ( + + )} diff --git a/services/web/frontend/js/features/project-list/components/welcome-message-new.tsx b/services/web/frontend/js/features/project-list/components/welcome-message-new.tsx new file mode 100644 index 0000000000..c9c16a1ff0 --- /dev/null +++ b/services/web/frontend/js/features/project-list/components/welcome-message-new.tsx @@ -0,0 +1,57 @@ +import { useState, useCallback } from 'react' +import { useTranslation } from 'react-i18next' +import { sendMB } from '../../../infrastructure/event-tracking' +import NewProjectButtonModal from './new-project-button/new-project-button-modal' +import type { NewProjectButtonModalVariant } from './new-project-button/new-project-button-modal' +import type { Nullable } from '../../../../../types/utils' +import WelcomeMessageLink from './welcome-message-new/welcome-message-link' +import WelcomeMessageCreateNewProjectDropdown from './welcome-message-new/welcome-message-create-new-project-dropdown' + +export default function WelcomeMessageNew() { + const { t } = useTranslation() + const [activeModal, setActiveModal] = + useState>(null) + + const handleTemplatesClick = useCallback(() => { + sendMB('welcome-page-templates-click', { + 'welcome-page-redesign': 'enabled', + }) + }, []) + + const handleLatexHelpClick = useCallback(() => { + sendMB('welcome-page-latex-help-click', { + 'welcome-page-redesign': 'enabled', + }) + }, []) + + return ( + <> +
+
+

{t('welcome_to_sl')}

+
+ setActiveModal(modal)} + /> + + +
+
+
+ setActiveModal(null)} + /> + + ) +} diff --git a/services/web/frontend/js/features/project-list/components/welcome-message-new/welcome-message-create-new-project-dropdown.tsx b/services/web/frontend/js/features/project-list/components/welcome-message-new/welcome-message-create-new-project-dropdown.tsx new file mode 100644 index 0000000000..c63a0b87db --- /dev/null +++ b/services/web/frontend/js/features/project-list/components/welcome-message-new/welcome-message-create-new-project-dropdown.tsx @@ -0,0 +1,160 @@ +import { useCallback, useState } from 'react' +import { useTranslation } from 'react-i18next' +import type { PortalTemplate } from '../../../../../../types/portal-template' +import { sendMB } from '../../../../infrastructure/event-tracking' +import getMeta from '../../../../utils/meta' +import { NewProjectButtonModalVariant } from '../new-project-button/new-project-button-modal' + +type WelcomeMessageCreateNewProjectDropdownProps = { + setActiveModal: (modal: NewProjectButtonModalVariant) => void +} + +function WelcomeMessageCreateNewProjectDropdown({ + setActiveModal, +}: WelcomeMessageCreateNewProjectDropdownProps) { + const [showDropdown, setShowDropdown] = useState(false) + const { t } = useTranslation() + const portalTemplates = getMeta('ol-portalTemplates') as + | PortalTemplate[] + | undefined + + const handleClick = useCallback(() => { + sendMB('welcome-page-create-first-project-click', { + 'welcome-page-redesign': 'enabled', + dropdownOpen: showDropdown, + }) + + // toggle the dropdown + setShowDropdown(!showDropdown) + }, [setShowDropdown, showDropdown]) + + const handleKeyDown = useCallback( + (e: React.KeyboardEvent) => { + if (e.code === 'Enter') { + handleClick() + } else if (e.code === 'Space') { + handleClick() + + // prevent page down when pressing space + e.preventDefault() + } + }, + [handleClick] + ) + + const handleDropdownItemClick = useCallback( + ( + e: React.MouseEvent, + modalVariant: NewProjectButtonModalVariant, + dropdownMenuEvent: string + ) => { + // prevent firing the main dropdown onClick event + e.stopPropagation() + + setShowDropdown(false) + + sendMB('welcome-page-create-first-project-click', { + 'welcome-page-redesign': 'enabled', + dropdownOpen: true, + dropdownMenu: dropdownMenuEvent, + }) + setActiveModal(modalVariant) + }, + [setActiveModal, setShowDropdown] + ) + + const handlePortalTemplateClick = useCallback( + ( + e: React.MouseEvent, + institutionTemplateName: string + ) => { + // prevent firing the main dropdown onClick event + e.stopPropagation() + + setShowDropdown(false) + + sendMB('welcome-page-create-first-project-click', { + 'welcome-page-redesign': 'enabled', + dropdownMenu: 'institution-template', + dropdownOpen: true, + institutionTemplateName, + }) + }, + [setShowDropdown] + ) + + return ( +
+

{t('create_a_new_project')}

+ + {showDropdown ? ( +
+ + + + + {(portalTemplates?.length ?? 0) > 0 ? ( + <> +
+
+ {t('institution_templates')} +
+ {portalTemplates?.map((portalTemplate, index) => ( + + handlePortalTemplateClick(e, portalTemplate.name) + } + > + {portalTemplate.name} + + ))} + + ) : null} +
+ ) : null} +
+ ) +} + +export default WelcomeMessageCreateNewProjectDropdown diff --git a/services/web/frontend/js/features/project-list/components/welcome-message-new/welcome-message-link.tsx b/services/web/frontend/js/features/project-list/components/welcome-message-new/welcome-message-link.tsx new file mode 100644 index 0000000000..17e0d29dd2 --- /dev/null +++ b/services/web/frontend/js/features/project-list/components/welcome-message-new/welcome-message-link.tsx @@ -0,0 +1,29 @@ +type WelcomeMessageLinkProps = { + imgSrc: string + title: string + href: string + onClick?: () => void +} + +export default function WelcomeMessageLink({ + imgSrc, + title, + href, + onClick, +}: WelcomeMessageLinkProps) { + return ( + +

{title}

+ +
+ ) +} diff --git a/services/web/frontend/js/features/project-list/components/welcome-message.tsx b/services/web/frontend/js/features/project-list/components/welcome-message.tsx index b59ceb782f..cea0d0bf49 100644 --- a/services/web/frontend/js/features/project-list/components/welcome-message.tsx +++ b/services/web/frontend/js/features/project-list/components/welcome-message.tsx @@ -9,13 +9,13 @@ export default function WelcomeMessage() { const handleTemplatesClick = useCallback(() => { sendMB('welcome-page-templates-click', { - 'project-dashboard-react': 'enabled', + 'welcome-page-redesign': 'default', }) }, []) const handleLatexHelpClick = useCallback(() => { sendMB('welcome-page-latex-help-click', { - 'project-dashboard-react': 'enabled', + 'welcome-page-redesign': 'default', }) }, []) diff --git a/services/web/frontend/stylesheets/app/project-list-react.less b/services/web/frontend/stylesheets/app/project-list-react.less index ac959bb56f..461490e61c 100644 --- a/services/web/frontend/stylesheets/app/project-list-react.less +++ b/services/web/frontend/stylesheets/app/project-list-react.less @@ -75,6 +75,110 @@ .project-list-welcome-wrapper { width: 100%; + + .welcome-new-wrapper { + max-width: 1080px; + + .welcome-title { + font-size: 32px; + } + + .welcome-message-cards-wrapper { + display: flex; + flex-direction: column; + justify-content: space-between; + align-items: center; + margin-top: 50px; + + @media (min-width: @screen-md-min) { + flex-direction: row; + justify-content: center; + } + } + + .welcome-message-card { + border: 1px solid @neutral-20; + border-radius: 16px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: space-between; + padding: 24px 16px; + margin: @margin-sm 0; + width: 280px; + height: 200px; + position: relative; + cursor: pointer; + + @media (min-width: @screen-md-min) { + margin: 0 15px; + height: 240px; + } + + &:hover { + background-color: @ol-blue-gray-0; + } + + .welcome-message-card-img { + @media (min-width: @screen-md-min) { + margin-bottom: 20px; + } + } + + .create-new-project-dropdown { + border: 1px solid @neutral-20; + position: absolute; + top: 100%; + left: 0; + width: 100%; + padding: 0; + border-radius: 3px; + z-index: 1; + text-align: left; + + button, + a, + .dropdown-header { + padding: 5px 10px; + } + + > button { + background-color: white; + border: none; + box-shadow: none; + display: flex; + align-items: center; + width: 100%; + &:hover { + background-color: @ol-blue-gray-0; + } + } + + > a { + color: @ol-blue-gray-3; + display: block; + + &:hover { + text-decoration: none; + background-color: @ol-blue-gray-0; + } + } + + hr { + margin-top: 6px; + margin-bottom: 6px; + } + } + } + + .welcome-message-card-link { + &, + &:hover { + text-decoration: none; + color: @ol-blue-gray-3; + } + } + } } .project-list-main-react { diff --git a/services/web/locales/en.json b/services/web/locales/en.json index 53c008664b..136e3d739f 100644 --- a/services/web/locales/en.json +++ b/services/web/locales/en.json @@ -292,6 +292,7 @@ "coupons_not_included": "This does not include your current discounts, which will be applied automatically before your next payment", "create": "Create", "create_a_new_password_for_your_account": "Create a new password for your account", + "create_a_new_project": "Create a new project", "create_first_admin_account": "Create the first Admin account", "create_first_project": "Create First Project", "create_new_account": "Create new account", @@ -772,6 +773,7 @@ "institution_acct_successfully_linked_2": "Your <0>__appName__ account was successfully linked to your <0>__institutionName__ institutional account.", "institution_and_role": "Institution and role", "institution_email_new_to_app": "Your __institutionName__ email (__email__) is new to __appName__.", + "institution_templates": "Institution Templates", "institutional": "Institutional", "institutional_leavers_survey_notification": "Provide some quick feedback to receive a 25% discount on an annual subscription!", "institutional_login_not_supported": "Your institution doesn’t support institutional login yet, but you can still register with your institutional email.", diff --git a/services/web/public/img/welcome-page/browse-templates.svg b/services/web/public/img/welcome-page/browse-templates.svg new file mode 100644 index 0000000000..2da927f7ba --- /dev/null +++ b/services/web/public/img/welcome-page/browse-templates.svg @@ -0,0 +1,105 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/services/web/public/img/welcome-page/create-a-new-project.svg b/services/web/public/img/welcome-page/create-a-new-project.svg new file mode 100644 index 0000000000..2460c1fd54 --- /dev/null +++ b/services/web/public/img/welcome-page/create-a-new-project.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/services/web/public/img/welcome-page/learn-latex.svg b/services/web/public/img/welcome-page/learn-latex.svg new file mode 100644 index 0000000000..5aec4b5975 --- /dev/null +++ b/services/web/public/img/welcome-page/learn-latex.svg @@ -0,0 +1,114 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/services/web/test/frontend/features/project-list/components/welcome-message-new.test.tsx b/services/web/test/frontend/features/project-list/components/welcome-message-new.test.tsx new file mode 100644 index 0000000000..d1e0a2ca2b --- /dev/null +++ b/services/web/test/frontend/features/project-list/components/welcome-message-new.test.tsx @@ -0,0 +1,104 @@ +import { fireEvent, render, screen } from '@testing-library/react' +import WelcomeMessageNew from '../../../../../frontend/js/features/project-list/components/welcome-message-new' +import { expect } from 'chai' + +describe('', function () { + afterEach(function () { + window.metaAttributesCache = new Map() + }) + + it('renders welcome page correctly', function () { + render() + + screen.getByText('Welcome to Overleaf!') + screen.getByText('Create a new project') + screen.getByText('Learn LaTeX with a tutorial') + screen.getByText('Browse templates') + }) + + it('shows correct dropdown when clicking create a new project', function () { + render() + + const button = screen.getByRole('button', { + name: 'Create a new project', + }) + + fireEvent.click(button) + + screen.getByText('Blank Project') + screen.getByText('Example Project') + screen.getByText('Upload Project') + screen.getByText('Import from GitHub') + }) + + it('show the correct dropdown menu for affiliated users', function () { + window.metaAttributesCache.set('ol-portalTemplates', [ + { + name: 'Affiliation 1', + url: '/edu/test-new-template', + }, + ]) + + render() + + const button = screen.getByRole('button', { + name: 'Create a new project', + }) + + fireEvent.click(button) + // static menu + screen.getByText('Blank Project') + screen.getByText('Example Project') + screen.getByText('Upload Project') + screen.getByText('Import from GitHub') + + // static text for institution templates + screen.getByText('Institution Templates') + + // dynamic menu based on portalTemplates + const affiliationTemplate = screen.getByRole('link', { + name: 'Affiliation 1', + }) + + expect(affiliationTemplate.getAttribute('href')).to.equal( + '/edu/test-new-template#templates' + ) + }) + + it('shows correct dropdown when clicking create a new project with a portal template', function () { + render() + + const button = screen.getByRole('button', { + name: 'Create a new project', + }) + + fireEvent.click(button) + + screen.getByText('Blank Project') + screen.getByText('Example Project') + screen.getByText('Upload Project') + screen.getByText('Import from GitHub') + }) + + it('shows correct link for latex tutorial menu', function () { + render() + + const link = screen.getByRole('link', { + name: 'Learn LaTeX with a tutorial', + }) + + expect(link.getAttribute('href')).to.equal( + '/learn/latex/Learn_LaTeX_in_30_minutes' + ) + }) + + it('shows correct link for browse templates menu', function () { + render() + + const link = screen.getByRole('link', { + name: 'Browse templates', + }) + + expect(link.getAttribute('href')).to.equal('/templates') + }) +})