Эх сурвалжийг харах

Prettier for PUG templates (#26170)

* Setup prettier

* Ignore these pug templates by prettier

* Fix typo

* Fix prettier error

* Add prettier-ignore for quoting of event-segmentation attribute

* Manual tab indentation

* Interpolate

* Remove unbuffered if conditional

* Inline event-segmentation objects and remove prettier-ignore rule

* Fix spacing before interpolation

* Source format

* Source format

GitOrigin-RevId: c30e037f5caf8f91efc1bd9e75f81ae533b5a506
Rebeka Dekany 1 жил өмнө
parent
commit
c40ab3234d
96 өөрчлөгдсөн 2143 нэмэгдсэн , 1761 устгасан
  1. 28 0
      package-lock.json
  2. 25 0
      services/web/.prettierignore
  3. 15 1
      services/web/.prettierrc
  4. 7 0
      services/web/Makefile
  5. 13 5
      services/web/app/views/_cookie_banner.pug
  6. 87 70
      services/web/app/views/_metadata.pug
  7. 3 1
      services/web/app/views/_mixins/back_to_btns.pug
  8. 4 6
      services/web/app/views/_mixins/begin_now_card.pug
  9. 4 4
      services/web/app/views/_mixins/bookmarkable_tabset.pug
  10. 2 2
      services/web/app/views/_mixins/bootstrap_js.pug
  11. 4 4
      services/web/app/views/_mixins/eyebrow.pug
  12. 18 11
      services/web/app/views/_mixins/faq_search-marketing.pug
  13. 10 5
      services/web/app/views/_mixins/foot_scripts.pug
  14. 49 61
      services/web/app/views/_mixins/formMessages.pug
  15. 53 22
      services/web/app/views/_mixins/links.pug
  16. 3 2
      services/web/app/views/_mixins/material_symbol.pug
  17. 7 7
      services/web/app/views/_mixins/navbar.pug
  18. 5 11
      services/web/app/views/_mixins/notification.pug
  19. 21 24
      services/web/app/views/_mixins/pagination.pug
  20. 1 1
      services/web/app/views/_mixins/previous_page_link.pug
  21. 14 14
      services/web/app/views/_mixins/quote.pug
  22. 1 1
      services/web/app/views/_mixins/recaptcha.pug
  23. 15 21
      services/web/app/views/_mixins/reconfirm_affiliation-marketing.pug
  24. 1 1
      services/web/app/views/_mixins/terms_of_service.pug
  25. 44 41
      services/web/app/views/admin/index.pug
  26. 22 27
      services/web/app/views/beta_program/opt_in.pug
  27. 7 7
      services/web/app/views/general/400.pug
  28. 2 2
      services/web/app/views/general/404.pug
  29. 5 5
      services/web/app/views/general/500.pug
  30. 1 1
      services/web/app/views/general/closed.pug
  31. 5 9
      services/web/app/views/general/post-gateway.pug
  32. 5 5
      services/web/app/views/general/unsupported-browser.pug
  33. 89 52
      services/web/app/views/layout-base.pug
  34. 5 5
      services/web/app/views/layout-marketing.pug
  35. 39 31
      services/web/app/views/layout-react.pug
  36. 6 6
      services/web/app/views/layout-website-redesign.pug
  37. 49 18
      services/web/app/views/layout/fat-footer-base.pug
  38. 41 38
      services/web/app/views/layout/fat-footer-website-redesign.pug
  39. 41 38
      services/web/app/views/layout/fat-footer.pug
  40. 19 11
      services/web/app/views/layout/language-picker-bootstrap-5.pug
  41. 15 12
      services/web/app/views/layout/language-picker.pug
  42. 8 6
      services/web/app/views/layout/layout-no-js.pug
  43. 108 96
      services/web/app/views/layout/navbar-marketing-bootstrap-5.pug
  44. 105 86
      services/web/app/views/layout/navbar-marketing.pug
  45. 99 83
      services/web/app/views/layout/navbar-website-redesign.pug
  46. 2 2
      services/web/app/views/layout/thin-footer-bootstrap-5.pug
  47. 2 2
      services/web/app/views/layout/thin-footer.pug
  48. 29 29
      services/web/app/views/project/editor/new_from_template.pug
  49. 1 1
      services/web/app/views/project/ide-react-detached.pug
  50. 7 2
      services/web/app/views/project/ide-react.pug
  51. 3 3
      services/web/app/views/project/invite/not-valid.pug
  52. 8 8
      services/web/app/views/project/invite/show.pug
  53. 77 29
      services/web/app/views/project/list-react.pug
  54. 5 5
      services/web/app/views/project/token/access-react.pug
  55. 4 4
      services/web/app/views/project/token/sharing-updates.pug
  56. 11 7
      services/web/app/views/subscriptions/add-seats.pug
  57. 2 2
      services/web/app/views/subscriptions/canceled-subscription-react.pug
  58. 82 27
      services/web/app/views/subscriptions/dashboard-react.pug
  59. 3 3
      services/web/app/views/subscriptions/manually-collected-subscription.pug
  60. 3 3
      services/web/app/views/subscriptions/missing-billing-information.pug
  61. 29 41
      services/web/app/views/subscriptions/plans/_faq_new.pug
  62. 217 221
      services/web/app/views/subscriptions/plans/_plans_faq_tabs.pug
  63. 8 4
      services/web/app/views/subscriptions/preview-change.pug
  64. 3 3
      services/web/app/views/subscriptions/subtotal-limit-exceeded.pug
  65. 4 4
      services/web/app/views/subscriptions/successful-subscription-react.pug
  66. 3 3
      services/web/app/views/subscriptions/team/group-invites.pug
  67. 15 11
      services/web/app/views/subscriptions/team/invite-managed.pug
  68. 17 9
      services/web/app/views/subscriptions/team/invite.pug
  69. 9 5
      services/web/app/views/subscriptions/team/invite_logged_out.pug
  70. 9 5
      services/web/app/views/subscriptions/upgrade-group-subscription-react.pug
  71. 1 1
      services/web/app/views/user/accountSuspended.pug
  72. 1 1
      services/web/app/views/user/compromised_password.pug
  73. 1 1
      services/web/app/views/user/confirmSecondaryEmail.pug
  74. 24 30
      services/web/app/views/user/confirm_email.pug
  75. 17 17
      services/web/app/views/user/email-preferences.pug
  76. 18 22
      services/web/app/views/user/login.pug
  77. 2 2
      services/web/app/views/user/one_time_login.pug
  78. 30 26
      services/web/app/views/user/passwordReset-bs5.pug
  79. 37 33
      services/web/app/views/user/passwordReset.pug
  80. 19 19
      services/web/app/views/user/primaryEmailCheck-bs5.pug
  81. 27 26
      services/web/app/views/user/reconfirm-bs5.pug
  82. 29 28
      services/web/app/views/user/reconfirm.pug
  83. 3 3
      services/web/app/views/user/register.pug
  84. 14 18
      services/web/app/views/user/sessions.pug
  85. 23 27
      services/web/app/views/user/setPassword-bs5.pug
  86. 34 32
      services/web/app/views/user/setPassword.pug
  87. 54 27
      services/web/app/views/user/settings.pug
  88. 5 5
      services/web/app/views/user_membership/group-managers-react.pug
  89. 28 12
      services/web/app/views/user_membership/group-members-react.pug
  90. 5 5
      services/web/app/views/user_membership/institution-managers-react.pug
  91. 6 13
      services/web/app/views/user_membership/new.pug
  92. 5 5
      services/web/app/views/user_membership/publisher-managers-react.pug
  93. 70 89
      services/web/modules/launchpad/app/views/launchpad.pug
  94. 27 31
      services/web/modules/user-activate/app/views/user/activate.pug
  95. 2 2
      services/web/modules/user-activate/app/views/user/register.pug
  96. 3 0
      services/web/package.json

+ 28 - 0
package-lock.json

@@ -8911,6 +8911,33 @@
         "url": "https://opencollective.com/popperjs"
       }
     },
+    "node_modules/@prettier/plugin-pug": {
+      "version": "3.4.0",
+      "resolved": "https://registry.npmjs.org/@prettier/plugin-pug/-/plugin-pug-3.4.0.tgz",
+      "integrity": "sha512-Jzd5rE/ellJz3vqfxyVewPsCHXw1dmIzJ3AXhAnqVBKQOj2u73ZS2oUacji8CbQSsYyCy7GXFjXWDlDTMG1x2g==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/Shinigami92"
+        },
+        {
+          "type": "paypal",
+          "url": "https://www.paypal.com/donate/?hosted_button_id=L7GY729FBKTZY"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "pug-lexer": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=18.0.0",
+        "npm": ">=9.0.0"
+      },
+      "peerDependencies": {
+        "prettier": "^3.0.0"
+      }
+    },
     "node_modules/@protobufjs/aspromise": {
       "version": "1.1.2",
       "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
@@ -45272,6 +45299,7 @@
         "@pollyjs/adapter-node-http": "^6.0.6",
         "@pollyjs/core": "^6.0.6",
         "@pollyjs/persister-fs": "^6.0.6",
+        "@prettier/plugin-pug": "^3.4.0",
         "@replit/codemirror-emacs": "overleaf/codemirror-emacs#4394c03858f27053f8768258e9493866e06e938e",
         "@replit/codemirror-indentation-markers": "overleaf/codemirror-indentation-markers#78264032eb286bc47871569ae87bff5ca1c6c161",
         "@replit/codemirror-vim": "overleaf/codemirror-vim#1bef138382d948018f3f9b8a4d7a70ab61774e4b",

+ 25 - 0
services/web/.prettierignore

@@ -13,3 +13,28 @@ frontend/js/features/source-editor/lezer-latex/latex.terms.mjs
 frontend/js/features/source-editor/lezer-bibtex/bibtex.mjs
 frontend/js/features/source-editor/lezer-bibtex/bibtex.terms.mjs
 frontend/js/features/source-editor/hunspell/wasm/hunspell.mjs
+
+# complex pages
+app/views/project/editor.pug
+app/views/project/editor/**
+modules/open-in-overleaf/app/views/documentation.pug
+modules/references-search/app/views/project/editor/**
+modules/rich-text/app/views/toolbar.pug
+
+# loops
+app/views/referal/bonus.pug
+modules/templates/app/views/tag.pug
+
+# expressions that could not be formatted correctly
+app/views/_mixins/faq_search.pug
+app/views/external/home/v2.pug
+app/views/project/token/access.pug
+app/views/user/primaryEmailCheck.pug
+app/views/user/restricted.pug
+modules/admin-panel/app/views/project/show.pug
+modules/templates/app/views/project/editor/_left-menu.pug
+modules/two-factor-authentication/app/views/_mixins.pug
+
+# minified files
+app/views/_google_analytics.pug
+app/views/_customer_io.pug

+ 15 - 1
services/web/.prettierrc

@@ -1,9 +1,23 @@
 {
   "arrowParens": "avoid",
   "jsxSingleQuote": false,
+  "pugAttributeSeparator": "as-needed",
+  "pugBracketSpacing": false,
+  "pugClassNotation": "as-is",
+  "pugIdNotation": "as-is",
+  "pugSortAttributesBeginning": ["name", "data-type"],
+  "plugins": ["@prettier/plugin-pug"],
   "semi": false,
   "singleQuote": true,
   "trailingComma": "es5",
   "tabWidth": 2,
-  "useTabs": false
+  "useTabs": false,
+  "overrides": [
+    {
+      "files": "*.pug",
+      "options": {
+        "useTabs": true
+      }
+    }
+  ]
 }

+ 7 - 0
services/web/Makefile

@@ -455,12 +455,19 @@ format: format_styles
 format_styles:
 	npm run --silent format:styles
 
+format: format_pug
+format_pug:
+	npm run --silent format:pug
+
 format_fix:
 	npm run --silent format:fix
 
 format_styles_fix:
 	npm run --silent format:styles:fix
 
+format_pug_fix:
+	npm run --silent format:pug:fix
+
 format_in_docker:
 	$(RUN_LINT_FORMAT) make format -j2 --output-sync
 

+ 13 - 5
services/web/app/views/_cookie_banner.pug

@@ -1,5 +1,13 @@
-section.cookie-banner.hidden-print.hidden(aria-label="Cookie banner")
-    .cookie-banner-content We only use cookies for essential purposes and to improve your experience on our site. You can find out more in our <a href="/legal#Cookies">cookie policy</a>.
-    .cookie-banner-actions
-        button(type="button" class="btn btn-link btn-sm" data-ol-cookie-banner-set-consent="essential") Essential cookies only
-        button(type="button" class="btn btn-primary btn-sm" data-ol-cookie-banner-set-consent="all") Accept all cookies
+section.cookie-banner.hidden-print.hidden(aria-label='Cookie banner')
+	.cookie-banner-content We only use cookies for essential purposes and to improve your experience on our site. You can find out more in our <a href="/legal#Cookies">cookie policy</a>.
+	.cookie-banner-actions
+		button(
+			type='button'
+			class='btn btn-link btn-sm'
+			data-ol-cookie-banner-set-consent='essential'
+		) Essential cookies only
+		button(
+			type='button'
+			class='btn btn-primary btn-sm'
+			data-ol-cookie-banner-set-consent='all'
+		) Accept all cookies

+ 87 - 70
services/web/app/views/_metadata.pug

@@ -1,123 +1,140 @@
-
 //- Title
-if (metadata && metadata.title)
-	title= metadata.title + ' - ' + settings.appName + ', ' + translate("online_latex_editor")
-	meta(name="twitter:title", content=metadata.title)
-	meta(name="og:title", content=metadata.title)
-else if (typeof(title) == "undefined")
-	title= settings.appName + ', '+ translate("online_latex_editor")
-	meta(name="twitter:title", content=settings.appName + ', '+ translate("online_latex_editor"))
-	meta(name="og:title", content=settings.appName + ', '+ translate("online_latex_editor"))
+if metadata && metadata.title
+	title= metadata.title + ' - ' + settings.appName + ', ' + translate('online_latex_editor')
+	meta(name='twitter:title' content=metadata.title)
+	meta(name='og:title' content=metadata.title)
+else if typeof title == 'undefined'
+	title= settings.appName + ', ' + translate('online_latex_editor')
+	meta(
+		name='twitter:title'
+		content=settings.appName + ', ' + translate('online_latex_editor')
+	)
+	meta(
+		name='og:title'
+		content=settings.appName + ', ' + translate('online_latex_editor')
+	)
 else
-	title= translate(title) + ' - ' + settings.appName + ', ' + translate("online_latex_editor")
+	title= translate(title) + ' - ' + settings.appName + ', ' + translate('online_latex_editor')
 	//- to do - not translate?
-	meta(name="twitter:title", content=translate(title))
-	meta(name="og:title", content=translate(title))
+	meta(name='twitter:title' content=translate(title))
+	meta(name='og:title' content=translate(title))
 
 //- Description
-if (metadata && metadata.description)
-	meta(name="description" , content=metadata.description)
-	meta(itemprop="description" , content=metadata.description)
+if metadata && metadata.description
+	meta(name='description' content=metadata.description)
+	meta(itemprop='description' content=metadata.description)
 	//-twitter and og descriptions handeled in their sections below
 else
-	meta(name="description", content=translate("site_description"))
-	meta(itemprop="description", content=translate("site_description"))
+	meta(name='description' content=translate('site_description'))
+	meta(itemprop='description' content=translate('site_description'))
 
 //- Image
-if (metadata && metadata.image && metadata.image.fields)
+if metadata && metadata.image && metadata.image.fields
 	//- from the CMS
-	meta(itemprop="image", content=metadata.image.fields.file.url)
-	meta(name="image", content=metadata.image.fields.file.url)
-else if (metadata && metadata.image_src)
+	meta(itemprop='image' content=metadata.image.fields.file.url)
+	meta(name='image' content=metadata.image.fields.file.url)
+else if metadata && metadata.image_src
 	//- pages with custom metadata images, metadata.image_src is the full image URL
-	meta(itemprop="image", content=metadata.image_src)
-	meta(name="image", content=metadata.image_src)
-else if (settings.overleaf)
+	meta(itemprop='image' content=metadata.image_src)
+	meta(name='image' content=metadata.image_src)
+else if settings.overleaf
 	//- the default image for Overleaf
-	meta(itemprop="image", content=buildImgPath('ol-brand/overleaf_og_logo.png'))
-	meta(name="image", content=buildImgPath('ol-brand/overleaf_og_logo.png'))
+	meta(itemprop='image' content=buildImgPath('ol-brand/overleaf_og_logo.png'))
+	meta(name='image' content=buildImgPath('ol-brand/overleaf_og_logo.png'))
 else
 	//- the default image for Overleaf Community Edition/Server Pro
-	meta(itemprop="image", content='/apple-touch-icon.png')
-	meta(name="image", content='/apple-touch-icon.png')
+	meta(itemprop='image' content='/apple-touch-icon.png')
+	meta(name='image' content='/apple-touch-icon.png')
 
 //- Keywords
-if (metadata && metadata.keywords)
-	meta(name="keywords" content=metadata.keywords)
+if metadata && metadata.keywords
+	meta(name='keywords' content=metadata.keywords)
 
 //- Misc
-meta(itemprop="name", content=settings.appName + ", the Online LaTeX Editor")
+meta(itemprop='name' content=settings.appName + ', the Online LaTeX Editor')
 
-if (metadata && metadata.robotsNoindexNofollow)
-	meta(name="robots" content="noindex, nofollow")
+if metadata && metadata.robotsNoindexNofollow
+	meta(name='robots' content='noindex, nofollow')
 
 //- Twitter
-meta(name="twitter:card", content=metadata && metadata.twitterCardType ? metadata.twitterCardType : 'summary')
-if (settings.social && settings.social.twitter && settings.social.twitter.handle)
-	meta(name="twitter:site", content="@" + settings.social.twitter.handle)
-if (metadata && metadata.twitterDescription)
-	meta(name="twitter:description", content=metadata.twitterDescription)
+meta(
+	name='twitter:card'
+	content=metadata && metadata.twitterCardType ? metadata.twitterCardType : 'summary'
+)
+if settings.social && settings.social.twitter && settings.social.twitter.handle
+	meta(name='twitter:site' content='@' + settings.social.twitter.handle)
+if metadata && metadata.twitterDescription
+	meta(name='twitter:description' content=metadata.twitterDescription)
 else
-	meta(name="twitter:description", content=translate("site_description"))
-if (metadata && metadata.twitterImage && metadata.twitterImage.fields)
+	meta(name='twitter:description' content=translate('site_description'))
+if metadata && metadata.twitterImage && metadata.twitterImage.fields
 	//- from the CMS
-	meta(name="twitter:image", content=metadata.twitterImage.fields.file.url)
-	meta(name="twitter:image:alt", content=metadata.twitterImage.fields.title)
-else if (settings.overleaf)
+	meta(name='twitter:image' content=metadata.twitterImage.fields.file.url)
+	meta(name='twitter:image:alt' content=metadata.twitterImage.fields.title)
+else if settings.overleaf
 	//- the default image for Overleaf
-	meta(name="twitter:image", content=buildImgPath('ol-brand/overleaf_og_logo.png'))
+	meta(
+		name='twitter:image'
+		content=buildImgPath('ol-brand/overleaf_og_logo.png')
+	)
 else
 	//- the default image for Overleaf Community Edition/Server Pro
-	meta(name="twitter:image", content='/apple-touch-icon.png')
+	meta(name='twitter:image' content='/apple-touch-icon.png')
 
 //- Open Graph
 //- to do - add og:url
-if (settings.social && settings.social.facebook && settings.social.facebook.appId)
-	meta(property="fb:app_id", content=settings.social.facebook.appId)
+if settings.social && settings.social.facebook && settings.social.facebook.appId
+	meta(property='fb:app_id' content=settings.social.facebook.appId)
 
-if (metadata && metadata.openGraphDescription)
-	meta(property="og:description", content=metadata.openGraphDescription)
+if metadata && metadata.openGraphDescription
+	meta(property='og:description' content=metadata.openGraphDescription)
 else
-	meta(property="og:description", content=translate("site_description"))
+	meta(property='og:description' content=translate('site_description'))
 
-if (metadata && metadata.openGraphImage && metadata.openGraphImage.fields)
+if metadata && metadata.openGraphImage && metadata.openGraphImage.fields
 	//- from the CMS
-	meta(property="og:image", content=metadata.openGraphImage.fields.file.url)
-else if (settings.overleaf)
+	meta(property='og:image' content=metadata.openGraphImage.fields.file.url)
+else if settings.overleaf
 	//- the default image for Overleaf
-	meta(property="og:image", content=buildImgPath('ol-brand/overleaf_og_logo.png'))
+	meta(
+		property='og:image'
+		content=buildImgPath('ol-brand/overleaf_og_logo.png')
+	)
 else
 	//- the default image for Overleaf Community Edition/Server Pro
-	meta(property="og:image", content='/apple-touch-icon.png')
+	meta(property='og:image' content='/apple-touch-icon.png')
 
-if (metadata && metadata.openGraphType)
-	meta(property="og:type", metadata.openGraphType)
+if metadata && metadata.openGraphType
+	meta(property='og:type' metadata.openGraphType)
 else
-	meta(property="og:type", content="website")
+	meta(property='og:type' content='website')
 
-if (metadata && metadata.openGraphVideo)
+if metadata && metadata.openGraphVideo
 	//- from the CMS
-	meta(property="og:video", content=metadata.openGraphVideo)
+	meta(property='og:video' content=metadata.openGraphVideo)
 
 //- Viewport
 if !metadata || metadata.viewport !== false
-	meta(name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes")
+	meta(
+		name='viewport'
+		content='width=device-width, initial-scale=1.0, user-scalable=yes'
+	)
 
 //- Noindex
 if settings.robotsNoindex
-	meta(name="robots" content="noindex")
+	meta(name='robots' content='noindex')
 
 //- Icons
-link(rel="icon", sizes="32x32", href="/favicon-32x32.png")
-link(rel="icon", sizes="16x16", href="/favicon-16x16.png")
-link(rel="icon", href="/favicon.svg" type="image/svg+xml")
-link(rel="apple-touch-icon", href="/apple-touch-icon.png")
-link(rel="mask-icon", href="/mask-favicon.svg", color="#046530")
+link(rel='icon' sizes='32x32' href='/favicon-32x32.png')
+link(rel='icon' sizes='16x16' href='/favicon-16x16.png')
+link(rel='icon' href='/favicon.svg' type='image/svg+xml')
+link(rel='apple-touch-icon' href='/apple-touch-icon.png')
+link(rel='mask-icon' href='/mask-favicon.svg' color='#046530')
 
 //- Canonical Tag for SEO
-if (metadata && metadata.canonicalURL)
-	link(rel="canonical" href=metadata.canonicalURL)
+if metadata && metadata.canonicalURL
+	link(rel='canonical' href=metadata.canonicalURL)
 
 //- Manifest
 //- Does not currently contain a start_url to prevent browser installation prompts
-link(rel="manifest" href="/web.sitemanifest")
+link(rel='manifest' href='/web.sitemanifest')

+ 3 - 1
services/web/app/views/_mixins/back_to_btns.pug

@@ -1,4 +1,6 @@
 mixin back-to-btns(settingsAnchor)
 	.d-flex.flex-column.flex-sm-row.gap-3
-		a.btn.btn-secondary(href=`/user/settings${settingsAnchor ? '#' + settingsAnchor : '' }`) #{translate('back_to_account_settings')}
+		a.btn.btn-secondary(
+			href=`/user/settings${settingsAnchor ? '#' + settingsAnchor : '' }`
+		) #{translate('back_to_account_settings')}
 		a.btn.btn-secondary(href='/project') #{translate('back_to_your_projects')}

+ 4 - 6
services/web/app/views/_mixins/begin_now_card.pug

@@ -1,10 +1,10 @@
-mixin begin_now_card()
+mixin begin_now_card
 	- var registerURL = '/register'
 	- var plansURL = '/user/subscription/plans'
 	- var isUserLoggedIn = !!getSessionUser()
 
 	.begin-now-card
-		div.card.card-pattern
+		.card.card-pattern
 			.card-body
 				p.dm-mono
 					span.font-size-display-xs
@@ -16,10 +16,8 @@ mixin begin_now_card()
 				p #{translate("discover_why_over_people_worldwide_trust_overleaf", {count: settings.userCountInMillions})}
 				p.card-links
 					if !isUserLoggedIn
-						a.btn.btn-primary.card-link(
-							href=registerURL
-						) #{translate("sign_up_for_free")}
+						a.btn.btn-primary.card-link(href=registerURL) #{translate("sign_up_for_free")}
 					a.btn.card-link(
-						class = isUserLoggedIn ? 'btn-primary' : 'btn-secondary'
+						class=isUserLoggedIn ? 'btn-primary' : 'btn-secondary'
 						href=plansURL
 					) #{translate("explore_all_plans")}

+ 4 - 4
services/web/app/views/_mixins/bookmarkable_tabset.pug

@@ -1,10 +1,10 @@
 mixin bookmarkable-tabset-header(id, title, active)
-	li(role="presentation")
+	li(role='presentation')
 		a.nav-link(
 			href='#' + id
-			class=(active ? 'active' : '')
+			class=active ? 'active' : ''
 			aria-controls=id
-			role="tab"
-			data-toggle="tab"
+			role='tab'
+			data-toggle='tab'
 			data-ol-bookmarkable-tab
 		) #{title}

+ 2 - 2
services/web/app/views/_mixins/bootstrap_js.pug

@@ -1,3 +1,3 @@
 mixin bootstrap-js(bootstrapVersion)
-	each file in (entrypointScripts(bootstrapVersion === 5 ? 'bootstrap-5' : 'bootstrap-3'))
-		script(type="text/javascript", nonce=scriptNonce, src=file)
+	each file in entrypointScripts(bootstrapVersion === 5 ? 'bootstrap-5' : 'bootstrap-3')
+		script(type='text/javascript' nonce=scriptNonce src=file)

+ 4 - 4
services/web/app/views/_mixins/eyebrow.pug

@@ -1,5 +1,5 @@
 mixin eyebrow(text)
-    span.eyebrow-text
-        span(aria-hidden="true") {
-        span #{text}
-        span(aria-hidden="true") }
+	span.eyebrow-text
+		span(aria-hidden='true') {
+		span #{text}
+		span(aria-hidden='true') }

+ 18 - 11
services/web/app/views/_mixins/faq_search-marketing.pug

@@ -1,30 +1,37 @@
 mixin faq_search-marketing(headerText, headerClass)
-	if (typeof(settings.algolia) != "undefined" && typeof(settings.algolia.indexes) != "undefined" && typeof(settings.algolia.indexes.wiki) != "undefined")
+	if typeof settings.algolia != 'undefined' && typeof settings.algolia.indexes != 'undefined' && typeof settings.algolia.indexes.wiki != 'undefined'
 		if headerText
 			div(class=headerClass) #{headerText}
 		.wiki
-			form.project-search.form-horizontal(role="search" data-ol-faq-search)
+			form.project-search.form-horizontal(role='search' data-ol-faq-search)
 				.form-group.has-feedback.has-feedback-left
 					.col-sm-12
-						input.form-control(type='search', placeholder="Search help library…" aria-label="Search help library…")
-						i.fa.fa-search.form-control-feedback-left(aria-hidden="true")
+						input.form-control(
+							type='search'
+							placeholder='Search help library…'
+							aria-label='Search help library…'
+						)
+						i.fa.fa-search.form-control-feedback-left(aria-hidden='true')
 						i.fa.fa-times.form-control-feedback(
-							style="cursor: pointer;",
+							style='cursor: pointer'
 							hidden
 							data-ol-clear-search
-							aria-hidden="true"
+							aria-hidden='true'
 						)
 						button.sr-only(
-							type="button"
+							type='button'
 							hidden
 							data-ol-clear-search
 							aria-label=translate('clear_search')
 						)
 
-				.row(role="region" aria-label="search results")
-					.col-md-12()
+				.row(role='region' aria-label='search results')
+					.col-md-12
 						div(data-ol-search-results-wrapper)
-							span.sr-only(aria-live="polite" data-ol-search-sr-help-message)
+							span.sr-only(aria-live='polite' data-ol-search-sr-help-message)
 							div(data-ol-search-results)
-						.row-spaced-small.search-result.card.card-thin(hidden data-ol-search-no-results)
+						.row-spaced-small.search-result.card.card-thin(
+							hidden
+							data-ol-search-no-results
+						)
 							p #{translate("no_search_results")}

+ 10 - 5
services/web/app/views/_mixins/foot_scripts.pug

@@ -1,6 +1,11 @@
-mixin foot-scripts()
+mixin foot-scripts
 	each file in entrypointScripts(entrypoint)
-		script(type="text/javascript", nonce=scriptNonce, src=file, defer=deferScripts)
-	if (settings.devToolbar.enabled)
-		each file in entrypointScripts("devToolbar")
-			script(type="text/javascript", nonce=scriptNonce, src=file, defer=deferScripts)
+		script(type='text/javascript' nonce=scriptNonce src=file defer=deferScripts)
+	if settings.devToolbar.enabled
+		each file in entrypointScripts('devToolbar')
+			script(
+				type='text/javascript'
+				nonce=scriptNonce
+				src=file
+				defer=deferScripts
+			)

+ 49 - 61
services/web/app/views/_mixins/formMessages.pug

@@ -1,41 +1,35 @@
 include ./material_symbol
 
-mixin formMessages()
-	div(
-		data-ol-form-messages='',
-		role="alert"
-	)
+mixin formMessages
+	div(data-ol-form-messages='' role='alert')
 
 mixin formMessagesNewStyle(extraClass = 'form-messages-bottom-margin')
-	- const attrs = extraClass ? { 'class': extraClass }  : {}
-	div(
-		data-ol-form-messages-new-style='',
-		role="alert"
-	)&attributes(attrs)
+	- const attrs = extraClass ? {class: extraClass} : {}
+	div(data-ol-form-messages-new-style='' role='alert')&attributes(attrs)
 
 mixin customFormMessage(key, kind)
 	if kind === 'success'
-		div.alert.alert-success(
-			hidden,
-			data-ol-custom-form-message=key,
-			role="alert"
-			aria-live="polite"
+		.alert.alert-success(
+			hidden
+			data-ol-custom-form-message=key
+			role='alert'
+			aria-live='polite'
 		)
 			block
 	else if kind === 'danger'
-		div.alert.alert-danger(
-			hidden,
-			data-ol-custom-form-message=key,
-			role="alert"
-			aria-live="assertive"
+		.alert.alert-danger(
+			hidden
+			data-ol-custom-form-message=key
+			role='alert'
+			aria-live='assertive'
 		)
 			block
 	else
-		div.alert.alert-warning(
-			hidden,
-			data-ol-custom-form-message=key,
-			role="alert"
-			aria-live="polite"
+		.alert.alert-warning(
+			hidden
+			data-ol-custom-form-message=key
+			role='alert'
+			aria-live='polite'
 		)
 			block
 
@@ -43,56 +37,50 @@ mixin customFormMessageNewStyle(key, kind, extraClass = 'mb-3')
 	- extraClass = extraClass ? ' ' + extraClass : ''
 	if kind === 'success'
 		div(
-			class="notification notification-type-success" + extraClass,
-			hidden,
-			data-ol-custom-form-message=key,
-			role="alert"
-			aria-live="polite"
+			class='notification notification-type-success' + extraClass
+			hidden
+			data-ol-custom-form-message=key
+			role='alert'
+			aria-live='polite'
 		)
-			div.notification-icon
-				+material-symbol("check_circle")
-			div.notification-content.text-left
+			.notification-icon
+				+material-symbol('check_circle')
+			.notification-content.text-left
 				block
 	else if kind === 'danger'
 		div(
-			class="notification notification-type-error" + extraClass,
-			hidden,
-			data-ol-custom-form-message=key,
-			role="alert"
-			aria-live="polite"
+			class='notification notification-type-error' + extraClass
+			hidden
+			data-ol-custom-form-message=key
+			role='alert'
+			aria-live='polite'
 		)
-			div.notification-icon
-				+material-symbol("error")
-			div.notification-content.text-left
+			.notification-icon
+				+material-symbol('error')
+			.notification-content.text-left
 				block
 	else
 		div(
-			class="notification notification-type-warning" + extraClass,
-			hidden,
-			data-ol-custom-form-message=key,
-			role="alert"
-			aria-live="polite"
+			class='notification notification-type-warning' + extraClass
+			hidden
+			data-ol-custom-form-message=key
+			role='alert'
+			aria-live='polite'
 		)
-			div.notification-icon
-				+material-symbol("warning")
-			div.notification-content.text-left
+			.notification-icon
+				+material-symbol('warning')
+			.notification-content.text-left
 				block
 
 mixin customValidationMessage(key)
-	div.invalid-feedback.mt-2(
-		hidden,
-		data-ol-custom-form-message=key
-	)
-		i.fa.fa-fw.fa-warning.me-1(aria-hidden="true")
+	.invalid-feedback.mt-2(hidden data-ol-custom-form-message=key)
+		i.fa.fa-fw.fa-warning.me-1(aria-hidden='true')
 		div
 			block
 
 mixin customValidationMessageNewStyle(key)
-	div.notification.notification-type-error(
-			hidden,
-			data-ol-custom-form-message=key
-		)
-		div.notification-icon
-			+material-symbol("error")
-		div.notification-content.text-left.small
+	.notification.notification-type-error(hidden data-ol-custom-form-message=key)
+		.notification-icon
+			+material-symbol('error')
+		.notification-content.text-left.small
 			block

+ 53 - 22
services/web/app/views/_mixins/links.pug

@@ -8,7 +8,8 @@ mixin linkAdvisors(linkText, linkClass, track)
 	- var mb = track && track.mb ? 'true' : null
 	- var mbSegmentation = track && track.segmentation ? track.segmentation : null
 	- var trigger = track && track.trigger ? track.trigger : null
-	a(href="/advisors" 
+	a(
+		href='/advisors'
 		class=linkClass ? linkClass : ''
 		event-tracking-ga=gaCategory
 		event-tracking=gaAction
@@ -20,24 +21,36 @@ mixin linkAdvisors(linkText, linkClass, track)
 		span #{linkText ? linkText : 'advisor programme'}
 
 mixin linkBenefits(linkText, linkClass)
-	a(href=(settings.siteUrl ? settings.siteUrl : '') + "/for/authors" class=linkClass ? linkClass : '')
+	a(
+		href=(settings.siteUrl ? settings.siteUrl : '') + '/for/authors'
+		class=linkClass ? linkClass : ''
+	)
 		| #{linkText ? linkText : 'benefits'}
-		
+
 mixin linkBlog(linkText, linkClass, slug)
 	if slug
-		a(href=(settings.siteUrl ? settings.siteUrl : '') + "/blog/" + slug class=linkClass ? linkClass : '')
+		a(
+			href=(settings.siteUrl ? settings.siteUrl : '') + '/blog/' + slug
+			class=linkClass ? linkClass : ''
+		)
 			| #{linkText ? linkText : 'blog'}
 
 mixin linkContact(linkText, linkClass)
-	a(href=(settings.siteUrl ? settings.siteUrl : '') + "/contact" class=linkClass ? linkClass : '')
+	a(
+		href=(settings.siteUrl ? settings.siteUrl : '') + '/contact'
+		class=linkClass ? linkClass : ''
+	)
 		| #{linkText ? linkText : 'contact'}
 
 mixin linkDash(linkText, linkClass)
-	a(href="/project" class=linkClass ? linkClass : '')
+	a(href='/project' class=linkClass ? linkClass : '')
 		| #{linkText ? linkText : 'project dashboard'}
 
 mixin linkEducation(linkText, linkClass)
-	a(href=(settings.siteUrl ? settings.siteUrl : '') + "/for/edu" class=linkClass ? linkClass : '')
+	a(
+		href=(settings.siteUrl ? settings.siteUrl : '') + '/for/edu'
+		class=linkClass ? linkClass : ''
+	)
 		| #{linkText ? linkText : 'teaching toolkit'}
 
 mixin linkInvite(linkText, linkClass, track)
@@ -48,7 +61,8 @@ mixin linkInvite(linkText, linkClass, track)
 	- var mbSegmentation = track && track.segmentation ? track.segmentation : null
 	- var trigger = track && track.trigger ? track.trigger : null
 
-	a(href="/user/bonus"
+	a(
+		href='/user/bonus'
 		class=linkClass ? linkClass : ''
 		event-tracking-ga=gaCategory
 		event-tracking=gaAction
@@ -60,7 +74,7 @@ mixin linkInvite(linkText, linkClass, track)
 		span #{linkText ? linkText : 'invite your friends'}
 
 mixin linkPlansAndPricing(linkText, linkClass)
-	a(href="/user/subscription/plans" class=linkClass ? linkClass : '')
+	a(href='/user/subscription/plans' class=linkClass ? linkClass : '')
 		| #{linkText ? linkText : 'plans and pricing'}
 
 mixin linkPrintNewTab(linkText, linkClass, icon, track)
@@ -71,7 +85,8 @@ mixin linkPrintNewTab(linkText, linkClass, icon, track)
 	- var mbSegmentation = track && track.segmentation ? track.segmentation : null
 	- var trigger = track && track.trigger ? track.trigger : null
 
-	a(href='?media=print'
+	a(
+		href='?media=print'
 		class=linkClass ? linkClass : ''
 		event-tracking-ga=gaCategory
 		event-tracking=gaAction
@@ -79,20 +94,26 @@ mixin linkPrintNewTab(linkText, linkClass, icon, track)
 		event-tracking-trigger=trigger
 		event-tracking-mb=mb
 		event-segmentation=mbSegmentation
-		target="_BLANK",
-		rel="noopener noreferrer"
+		target='_BLANK'
+		rel='noopener noreferrer'
 	)
 		if icon
-			i(class="fa fa-print")
+			i(class='fa fa-print')
 			| &nbsp;
 		span #{linkText ? linkText : 'print'}
 
 mixin linkSignIn(linkText, linkClass, redirect)
-	a(href=`/login${redirect ? '?redir=' + redirect : ''}` class=linkClass ? linkClass : '')
+	a(
+		href=`/login${redirect ? '?redir=' + redirect : ''}`
+		class=linkClass ? linkClass : ''
+	)
 		| #{linkText ? linkText : 'sign in'}
 
 mixin linkSignUp(linkText, linkClass, redirect)
-	a(href=`/register${redirect ? '?redir=' + redirect : ''}` class=linkClass ? linkClass : '')
+	a(
+		href=`/register${redirect ? '?redir=' + redirect : ''}`
+		class=linkClass ? linkClass : ''
+	)
 		| #{linkText ? linkText : 'sign up'}
 
 mixin linkTweet(linkText, linkClass, tweetText, track)
@@ -103,23 +124,33 @@ mixin linkTweet(linkText, linkClass, tweetText, track)
 	- var mb = track && track.mb ? 'true' : null
 	- var mbSegmentation = track && track.segmentation ? track.segmentation : null
 	- var trigger = track && track.trigger ? track.trigger : null
-	a(class="twitter-share-button " + linkClass
+	a(
+		class='twitter-share-button ' + linkClass
 		event-tracking-ga=gaCategory
 		event-tracking=gaAction
 		event-tracking-label=gaLabel
 		event-tracking-trigger=trigger
 		event-tracking-mb=mb
 		event-segmentation=mbSegmentation
-		href="https://twitter.com/intent/tweet?text=" + tweetText
-		target="_BLANK",
-		rel="noopener noreferrer"
+		href='https://twitter.com/intent/tweet?text=' + tweetText
+		target='_BLANK'
+		rel='noopener noreferrer'
 	) #{linkText ? linkText : 'tweet'}
 
 mixin linkUniversities(linkText, linkClass)
-	a(href=(settings.siteUrl ? settings.siteUrl : '') + "/for/universities" class=linkClass ? linkClass : '')
+	a(
+		href=(settings.siteUrl ? settings.siteUrl : '') + '/for/universities'
+		class=linkClass ? linkClass : ''
+	)
 		| #{linkText ? linkText : 'universities'}
 
 mixin linkWithArrow({text, href, eventTracking, eventSegmentation, eventTrackingTrigger})
-	a.link-with-arrow(href=href event-tracking=eventTracking event-segmentation=eventSegmentation, event-tracking-trigger=eventTrackingTrigger event-tracking-mb)
+	a.link-with-arrow(
+		href=href
+		event-tracking=eventTracking
+		event-segmentation=eventSegmentation
+		event-tracking-trigger=eventTrackingTrigger
+		event-tracking-mb
+	)
 		| #{text}
-		+material-symbol("arrow_right_alt")
+		+material-symbol('arrow_right_alt')

+ 3 - 2
services/web/app/views/_mixins/material_symbol.pug

@@ -1,7 +1,8 @@
 mixin material-symbol(icon, extraClass = null)
 	- extraClass = extraClass ? ' ' + extraClass : ''
-	span(aria-hidden="true", translate="no", class="material-symbols" + extraClass)&attributes(attributes) #{icon}
-	
+	span(aria-hidden='true' translate='no' class='material-symbols' + extraClass)&attributes(attributes)
+		| #{icon}
+
 mixin material-symbol-outlined(icon, extraClass = null)
 	- extraClass = extraClass ? ' ' + extraClass : ''
 	+material-symbol(icon, 'material-symbols-outlined' + extraClass)&attributes(attributes)

+ 7 - 7
services/web/app/views/_mixins/navbar.pug

@@ -1,23 +1,23 @@
 mixin nav-item
-	li(role="none")&attributes(attributes)
+	li(role='none')&attributes(attributes)
 		block
 
 mixin nav-link
-	a(role="menuitem").nav-link&attributes(attributes)
+	a.nav-link(role='menuitem')&attributes(attributes)
 		block
 
 mixin dropdown-menu
-	ul(role="menu").dropdown-menu&attributes(attributes)
+	ul.dropdown-menu(role='menu')&attributes(attributes)
 		block
 
 mixin dropdown-menu-item
-	li(role="none")
+	li(role='none')
 		block
 
 mixin dropdown-menu-link-item
 	+dropdown-menu-item
-		a(role="menuitem").dropdown-item&attributes(attributes)
+		a.dropdown-item(role='menuitem')&attributes(attributes)
 			block
-		
+
 mixin dropdown-menu-divider
-	li(role="separator").dropdown-divider.d-none.d-lg-block
+	li.dropdown-divider.d-none.d-lg-block(role='separator')

+ 5 - 11
services/web/app/views/_mixins/notification.pug

@@ -3,25 +3,19 @@ include ./material_symbol
 
 mixin notificationIcon(type)
 	if type === 'info'
-		+material-symbol("info")
+		+material-symbol('info')
 	else if type === 'success'
-		+material-symbol("check_circle")
+		+material-symbol('check_circle')
 	else if type === 'error'
-		+material-symbol("error")
+		+material-symbol('error')
 	else if type === 'warning'
-		+material-symbol("warning")
-
+		+material-symbol('warning')
 
 mixin notification(options)
 	- var {ariaLive, id, type, title, content, disclaimer, className} = options
 	- var classNames = `notification notification-type-${type} ${className ? className : ''} ${isActionBelowContent ? 'notification-cta-below-content' : ''}`
 
-	div(
-		aria-live=ariaLive,
-		role="alert",
-		id=id,
-		class=classNames
-	)
+	div(aria-live=ariaLive role='alert' id=id class=classNames)
 		.notification-icon
 			+notificationIcon(type)
 		.notification-content-and-cta

+ 21 - 24
services/web/app/views/_mixins/pagination.pug

@@ -10,77 +10,74 @@ mixin pagination(pages, page_path, max_btns)
 		- var max_btns = max_btns || 4
 		- var prev_page = Math.max(parseInt(pages.current_page, 10) - max_btns, 1)
 		- var next_page = parseInt(pages.current_page, 10) + 1
-		- var next_index = 0;
-		- var full_page_path = page_path + "/page/"
+		- var next_index = 0
+		- var full_page_path = page_path + '/page/'
 
-		nav(role="navigation" aria-label=(translate("pagination_navigation")))
+		nav(role='navigation' aria-label=translate('pagination_navigation'))
 			ul.pagination
 				if pages.current_page > 1
 					li
-						a(
-							aria-label=translate("go_to_first_page")
-							href=page_path
-						)
-							span(aria-hidden="true") &lt;&lt;
+						a(aria-label=translate('go_to_first_page') href=page_path)
+							span(aria-hidden='true') &lt;&lt;
 							|
 							| First
 					li
 						a(
-							aria-label=translate("go_to_previous_page")
+							aria-label=translate('go_to_previous_page')
 							href=full_page_path + (parseInt(pages.current_page, 10) - 1)
-							rel="prev"
+							rel='prev'
 						)
-							span(aria-hidden="true") &lt;
+							span(aria-hidden='true') &lt;
 							|
 							| Prev
 
 				if pages.current_page - max_btns > 1
-					li(aria-hidden="true")
+					li(aria-hidden='true')
 						span …
 
 				while prev_page < pages.current_page
 					li
 						a(
-							aria-label=translate("go_to_page_x", {page: prev_page})
+							aria-label=translate('go_to_page_x', {page: prev_page})
 							href=full_page_path + prev_page
 						) #{prev_page}
 						- prev_page++
 
-				li(class="active")
+				li(class='active')
 					span(
-						aria-label=translate("current_page_page", {page: pages.current_page})
-						aria-current="true"
+						aria-label=translate('current_page_page', {page: pages.current_page})
+						aria-current='true'
 					) #{pages.current_page}
 
 				if pages.current_page < pages.total_pages
 					while next_page <= pages.total_pages && next_index < max_btns
 						li
 							a(
-								aria-label=translate("go_to_page_x", {page: next_page})
+								aria-label=translate('go_to_page_x', {page: next_page})
 								href=full_page_path + next_page
 							) #{next_page}
 						- next_page++
 						- next_index++
 
-					if next_page <= pages.total_pages 
-						li.ellipses(aria-hidden="true")
+					if next_page <= pages.total_pages
+						li.ellipses(aria-hidden='true')
 							span …
 
 					li
 						a(
-							aria-label=translate("go_to_next_page")
+							aria-label=translate('go_to_next_page')
 							href=full_page_path + (parseInt(pages.current_page, 10) + 1)
-							rel="next"
+							rel='next'
 						)
 							| Next
 							|
-							span(aria-hidden="true") &gt;
+							span(aria-hidden='true') &gt;
 
 					li
 						a(
-							aria-label=translate("go_to_last_page")
+							aria-label=translate('go_to_last_page')
 							href=full_page_path + pages.total_pages
 						)
 							| Last
 							|
-							span(aria-hidden="true") &gt;&gt;
+							span(aria-hidden='true') &gt;&gt;

+ 1 - 1
services/web/app/views/_mixins/previous_page_link.pug

@@ -2,5 +2,5 @@ include ./material_symbol
 
 mixin previous-page-link(href, text)
 	a.previous-page-link(href=href)
-		+material-symbol-rounded("arrow_left_alt")
+		+material-symbol-rounded('arrow_left_alt')
 		| #{text}

+ 14 - 14
services/web/app/views/_mixins/quote.pug

@@ -3,10 +3,10 @@ mixin quoteLargeTextCentered(quote, person, position, affiliation, link, picture
 		.quote !{quote}
 		if pictureUrl
 			.quote-img
-				-var pictureAlt=`Photo of ${person}`
+				- var pictureAlt = `Photo of ${person}`
 				img(src=pictureUrl alt=pictureAlt)
 		footer
-			div.quote-person
+			.quote-person
 				strong #{person}
 			if person && position
 				div #{position}
@@ -29,27 +29,27 @@ mixin quoteLeftGreenBorder({quote, person, position, affiliation, link})
 
 mixin collinsQuote1
 	.card.card-dark-green-bg
-		-var quote = 'Overleaf is indispensable for us. We use it in our research, thesis writing, project proposals, and manuscripts for publication. When it comes to writing, it’s our main tool.'
-		-var quotePerson = 'Christopher Collins'
-		-var quotePersonPosition = 'Associate Professor and Lab Director, Ontario Tech University'
-		-var quotePersonImg = buildImgPath("advocates/collins.jpg")
+		- var quote = 'Overleaf is indispensable for us. We use it in our research, thesis writing, project proposals, and manuscripts for publication. When it comes to writing, it’s our main tool.'
+		- var quotePerson = 'Christopher Collins'
+		- var quotePersonPosition = 'Associate Professor and Lab Director, Ontario Tech University'
+		- var quotePersonImg = buildImgPath('advocates/collins.jpg')
 		.card-body
 			+quoteLargeTextCentered(quote, quotePerson, quotePersonPosition, null, null, quotePersonImg)
 
 mixin collinsQuote2
 	.card.card-dark-green-bg
-		-var quote = 'We are writing collaboratively right up until the last minute. We are faced with deadlines all the time, and Overleaf gives us the ability to polish right up until the last possible second.'
-		-var quotePerson = 'Christopher Collins'
-		-var quotePersonPosition = 'Associate Professor and Lab Director, Ontario Tech University'
-		-var quotePersonImg = buildImgPath("advocates/collins.jpg")
+		- var quote = 'We are writing collaboratively right up until the last minute. We are faced with deadlines all the time, and Overleaf gives us the ability to polish right up until the last possible second.'
+		- var quotePerson = 'Christopher Collins'
+		- var quotePersonPosition = 'Associate Professor and Lab Director, Ontario Tech University'
+		- var quotePersonImg = buildImgPath('advocates/collins.jpg')
 		.card-body
 			+quoteLargeTextCentered(quote, quotePerson, quotePersonPosition, null, null, quotePersonImg)
 
 mixin bennettQuote1
 	.card.card-dark-green-bg
-		-var quote = 'With Overleaf, we now have a process for developing technical documentation which has virtually eliminated the time required to properly format and layout documents.'
-		-var quotePerson = 'Andrew Bennett'
-		-var quotePersonPosition = 'Software Architect, Symplectic'
-		-var quotePersonImg = buildImgPath("advocates/bennett.jpg")
+		- var quote = 'With Overleaf, we now have a process for developing technical documentation which has virtually eliminated the time required to properly format and layout documents.'
+		- var quotePerson = 'Andrew Bennett'
+		- var quotePersonPosition = 'Software Architect, Symplectic'
+		- var quotePersonImg = buildImgPath('advocates/bennett.jpg')
 		.card-body
 			+quoteLargeTextCentered(quote, quotePerson, quotePersonPosition, null, null, quotePersonImg)

+ 1 - 1
services/web/app/views/_mixins/recaptcha.pug

@@ -1,2 +1,2 @@
-mixin recaptchaConditions()
+mixin recaptchaConditions
 	.recaptcha-branding !{translate("recaptcha_conditions", {}, [{}, {name: 'a', attrs: {href: 'https://policies.google.com/privacy', rel: 'noopener noreferrer', target: '_blank'}}, {name: 'a', attrs: {href: 'https://policies.google.com/terms', rel: 'noopener noreferrer', target: '_blank'}}])}

+ 15 - 21
services/web/app/views/_mixins/reconfirm_affiliation-marketing.pug

@@ -1,14 +1,11 @@
 mixin reconfirmAffiliationNotification-marketing(userEmail, location)
-	form(
-		data-ol-async-form
-		action='/user/emails/send-reconfirmation'
-	)
-		input(name="_csrf" type="hidden" value=csrfToken)
-		input(name="email" type="hidden" value=userEmail.email)
-		+formMessages()
+	form(data-ol-async-form action='/user/emails/send-reconfirmation')
+		input(name='_csrf' type='hidden' value=csrfToken)
+		input(name='email' type='hidden' value=userEmail.email)
+		+formMessages
 
 		.reconfirm-notification
-			div(data-ol-not-sent style="width:100%;")
+			div(data-ol-not-sent style='width: 100%')
 				i.fa.fa-warning
 
 				- var ssoEnabled = userEmail.affiliation && userEmail.affiliation.institution && userEmail.affiliation.institution.ssoEnabled
@@ -18,16 +15,16 @@ mixin reconfirmAffiliationNotification-marketing(userEmail, location)
 						data-ol-slow-link
 						href=`${settings.saml.ukamf.initPath}?university_id=${institutionId}&reconfirm=${location}`
 					)
-						span(data-ol-inflight="idle") #{translate("confirm_affiliation")}
-						span(hidden data-ol-inflight="pending") #{translate("pending")}…
+						span(data-ol-inflight='idle') #{translate("confirm_affiliation")}
+						span(hidden data-ol-inflight='pending') #{translate("pending")}…
 
 				else
 					button.btn-reconfirm.btn.btn-sm.btn-info(
-						type="submit"
+						type='submit'
 						data-ol-disabled-inflight
 					)
-						span(data-ol-inflight="idle") #{translate("confirm_affiliation")}
-						span(hidden data-ol-inflight="pending") #{translate("pending")}…
+						span(data-ol-inflight='idle') #{translate("confirm_affiliation")}
+						span(hidden data-ol-inflight='pending') #{translate("pending")}…
 
 				| !{translate("are_you_still_at", {institutionName: userEmail.affiliation.institution.name}, ['strong'])}&nbsp;
 
@@ -39,22 +36,19 @@ mixin reconfirmAffiliationNotification-marketing(userEmail, location)
 					| !{translate("please_reconfirm_institutional_email", {}, [{name: 'a', attrs: {href: '/user/settings?remove=' + userEmail.email}}])}
 
 				| &nbsp;
-				a(href="/learn/how-to/Institutional_Email_Reconfirmation" target="_blank") #{translate("learn_more")}
+				a(href='/learn/how-to/Institutional_Email_Reconfirmation' target='_blank') #{translate("learn_more")}
 
 			div(hidden data-ol-sent)
 				| !{translate("please_check_your_inbox_to_confirm", {institutionName: userEmail.affiliation.institution.name}, ['strong'])}
 				| &nbsp;
-				button.btn-inline-link(
-					type="submit"
-					data-ol-disabled-inflight
-				)
-					span(data-ol-inflight="idle") #{translate('resend_confirmation_email')}
-					span(hidden data-ol-inflight="pending") #{translate("pending")}…
+				button.btn-inline-link(type='submit' data-ol-disabled-inflight)
+					span(data-ol-inflight='idle') #{translate('resend_confirmation_email')}
+					span(hidden data-ol-inflight='pending') #{translate("pending")}…
 
 mixin reconfirmedAffiliationNotification-marketing(userEmail)
 	.alert.alert-info
 		.reconfirm-notification
-			div(style="width:100%;")
+			div(style='width: 100%')
 				//- extra div for flex styling
 				| !{translate("your_affiliation_is_confirmed", {institutionName: userEmail.affiliation.institution.name}, ['strong'])}
 				|

+ 1 - 1
services/web/app/views/_mixins/terms_of_service.pug

@@ -1,3 +1,3 @@
 mixin termsOfServiceAgreement
-	div.tos-agreement-notice
+	.tos-agreement-notice
 		| !{translate("by_registering_you_agree_to_our_terms_of_service", {}, [{name: 'a', attrs: {href: '/legal#Terms', target: '_blank'}}, {name: 'a', attrs: {href: '/legal#Privacy', target: '_blank'}}])}

+ 44 - 41
services/web/app/views/admin/index.pug

@@ -2,7 +2,7 @@ extends ../layout-marketing
 include ../_mixins/bookmarkable_tabset
 
 block content
-	.content.content-alt#main-content
+	#main-content.content.content-alt
 		.container
 			.row
 				.col-sm-12
@@ -12,7 +12,7 @@ block content
 								h1 Admin Panel
 							.ol-tabs(data-ol-bookmarkable-tabset)
 								.nav-tabs-container
-									ul.nav.nav-tabs.align-left(role="tablist")
+									ul.nav.nav-tabs.align-left(role='tablist')
 										+bookmarkable-tabset-header('system-messages', 'System Messages', true)
 										+bookmarkable-tabset-header('open-sockets', 'Open Sockets')
 										+bookmarkable-tabset-header('open-close-editor', 'Open/Close Editor')
@@ -20,29 +20,28 @@ block content
 											+bookmarkable-tabset-header('tpds', 'TPDS/Dropbox Management')
 
 								.tab-content
-									.tab-pane.active(
-										role="tabpanel"
-										id='system-messages'
-									)
+									.tab-pane.active(role='tabpanel' id='system-messages')
 										each message in systemMessages
 											ul.system-messages
 												li.system-message.row-spaced #{message.content}
 										hr
-										form(method='post', action='/admin/messages')
-											input(name="_csrf", type="hidden", value=csrfToken)
+										form(method='post' action='/admin/messages')
+											input(name='_csrf' type='hidden' value=csrfToken)
 											.form-group
-												label.form-label(for="content")
-												input.form-control(name="content", type="text", placeholder="Message…", required)
-											button.btn.btn-primary(type="submit") Post Message
+												label.form-label(for='content')
+												input.form-control(
+													name='content'
+													type='text'
+													placeholder='Message…'
+													required
+												)
+											button.btn.btn-primary(type='submit') Post Message
 										hr
-										form(method='post', action='/admin/messages/clear')
-											input(name="_csrf", type="hidden", value=csrfToken)
-											button.btn.btn-danger(type="submit") Clear all messages
+										form(method='post' action='/admin/messages/clear')
+											input(name='_csrf' type='hidden' value=csrfToken)
+											button.btn.btn-danger(type='submit') Clear all messages
 
-									.tab-pane(
-										role="tabpanel"
-										id='open-sockets'
-									)
+									.tab-pane(role='tabpanel' id='open-sockets')
 										.row-spaced
 											ul
 												each agents, url in openSockets
@@ -51,52 +50,56 @@ block content
 															each agent in agents
 																li #{agent}
 
-									.tab-pane(
-										role="tabpanel"
-										id='open-close-editor'
-									)
+									.tab-pane(role='tabpanel' id='open-close-editor')
 										if hasFeature('saas')
 											| The "Open/Close Editor" feature is not available in SAAS.
 										else
 											.row-spaced
-												form(method='post',action='/admin/closeEditor')
-													input(name="_csrf", type="hidden", value=csrfToken)
-													button.btn.btn-danger(type="submit") Close Editor
+												form(method='post' action='/admin/closeEditor')
+													input(name='_csrf' type='hidden' value=csrfToken)
+													button.btn.btn-danger(type='submit') Close Editor
 												p.small Will stop anyone opening the editor. Will NOT disconnect already connected users.
 
 											.row-spaced
-												form(method='post',action='/admin/disconnectAllUsers')
-													input(name="_csrf", type="hidden", value=csrfToken)
-													button.btn.btn-danger(type="submit") Disconnect all users
+												form(method='post' action='/admin/disconnectAllUsers')
+													input(name='_csrf' type='hidden' value=csrfToken)
+													button.btn.btn-danger(type='submit') Disconnect all users
 												p.small Will force disconnect all users with the editor open. Make sure to close the editor first to avoid them reconnecting.
 
 											.row-spaced
-												form(method='post',action='/admin/openEditor')
-													input(name="_csrf", type="hidden", value=csrfToken)
-													button.btn.btn-danger(type="submit") Reopen Editor
+												form(method='post' action='/admin/openEditor')
+													input(name='_csrf' type='hidden' value=csrfToken)
+													button.btn.btn-danger(type='submit') Reopen Editor
 												p.small Will reopen the editor after closing.
 
 									if hasFeature('saas')
-										.tab-pane(
-											role="tabpanel"
-											id='tpds'
-										)
+										.tab-pane(role='tabpanel' id='tpds')
 											h3 Flush project to TPDS
 											.row
-												form.col-xs-6(method='post',action='/admin/flushProjectToTpds')
-													input(name="_csrf", type="hidden", value=csrfToken)
+												form.col-xs-6(method='post' action='/admin/flushProjectToTpds')
+													input(name='_csrf' type='hidden' value=csrfToken)
 													.form-group
 														label.form-label(for='project_id') project_id
-														input.form-control(type='text',  name='project_id', placeholder='project_id', required)
+														input.form-control(
+															name='project_id'
+															type='text'
+															placeholder='project_id'
+															required
+														)
 													.form-group
 														button.btn-primary.btn(type='submit') Flush
 											hr
 											h3 Poll Dropbox for user
 											.row
-												form.col-xs-6(method='post',action='/admin/pollDropboxForUser')
-													input(name="_csrf", type="hidden", value=csrfToken)
+												form.col-xs-6(method='post' action='/admin/pollDropboxForUser')
+													input(name='_csrf' type='hidden' value=csrfToken)
 													.form-group
 														label.form-label(for='user_id') user_id
-														input.form-control(type='text',  name='user_id', placeholder='user_id', required)
+														input.form-control(
+															name='user_id'
+															type='text'
+															placeholder='user_id'
+															required
+														)
 													.form-group
 														button.btn-primary.btn(type='submit') Poll

+ 22 - 27
services/web/app/views/beta_program/opt_in.pug

@@ -2,14 +2,14 @@ extends ../layout-marketing
 include ../_mixins/back_to_btns
 
 block content
-	main.content.content-alt#main-content
+	main#main-content.content.content-alt
 		.container.beta-opt-in-wrapper
 			.row
 				.col-lg-10.offset-lg-1.col-xl-8.offset-xl-2
 					.card
 						.card-body
 							.page-header
-								h1 
+								h1
 									| #{translate("sharelatex_beta_program")}
 							.beta-opt-in
 								.container-fluid
@@ -28,7 +28,9 @@ block content
 											ul
 												li
 													| #{translate("beta_program_badge_description")}&nbsp;
-													span.badge.bg-warning-light-bg.text-warning(aria-label=translate("beta_feature_badge"))
+													span.badge.bg-warning-light-bg.text-warning(
+														aria-label=translate('beta_feature_badge')
+													)
 														span.badge-content β
 												li !{translate("you_will_be_able_to_contact_us_any_time_to_share_your_feedback", {}, ['strong'])}.
 												li !{translate("we_may_also_contact_you_from_time_to_time_by_email_with_a_survey", {}, ['strong'])}.
@@ -40,37 +42,30 @@ block content
 											if user.betaProgram
 												form(
 													data-ol-regular-form
-													method="post"
-													action="/beta/opt-out"
+													method='post'
+													action='/beta/opt-out'
 													novalidate
 												)
-													input(type="hidden", name="_csrf", value=csrfToken)
+													input(name='_csrf' type='hidden' value=csrfToken)
 													.form-group
-														a(
-															href="https://forms.gle/CFEsmvZQTAwHCd3X9"
-															target="_blank"
-															rel="noopener noreferrer"
-														).btn.btn-primary.btn-lg #{translate("give_feedback")}
+														a.btn.btn-primary.btn-lg(
+															href='https://forms.gle/CFEsmvZQTAwHCd3X9'
+															target='_blank'
+															rel='noopener noreferrer'
+														) #{translate("give_feedback")}
 													.form-group
 														button.btn.btn-secondary-info.btn-secondary.btn-sm(
-															type="submit"
+															type='submit'
 															data-ol-disabled-inflight
 														)
-															span(data-ol-inflight="idle") #{translate("beta_program_opt_out_action")}
-															span(hidden data-ol-inflight="pending") #{translate("processing")}…
+															span(data-ol-inflight='idle') #{translate("beta_program_opt_out_action")}
+															span(hidden data-ol-inflight='pending') #{translate("processing")}…
 											else
-												form(
-													data-ol-regular-form
-													method="post",
-													action="/beta/opt-in"
-												)
-													input(type="hidden", name="_csrf", value=csrfToken)
+												form(data-ol-regular-form method='post' action='/beta/opt-in')
+													input(name='_csrf' type='hidden' value=csrfToken)
 													.form-group
-														button.btn.btn-primary(
-															type="submit"
-															data-ol-disabled-inflight
-														)
-															span(data-ol-inflight="idle") #{translate("beta_program_opt_in_action")}
-															span(hidden data-ol-inflight="pending") #{translate("joining")}…
+														button.btn.btn-primary(type='submit' data-ol-disabled-inflight)
+															span(data-ol-inflight='idle') #{translate("beta_program_opt_in_action")}
+															span(hidden data-ol-inflight='pending') #{translate("joining")}…
 								.page-separator
-								+back-to-btns()
+								+back-to-btns

+ 7 - 7
services/web/app/views/general/400.pug

@@ -1,29 +1,29 @@
 extends ../layout/layout-no-js
 
 block vars
-	- metadata = { title: 'Something went wrong' }
+	- metadata = {title: 'Something went wrong'}
 
 block body
 	body.full-height
-		main.content.content-alt.full-height#main-content
+		main#main-content.content.content-alt.full-height
 			.container.full-height
 				.error-container.full-height
 					.error-details
 						p.error-status Something went wrong, sorry.
 						p.error-description
 							| There was a problem with your request.
-							if(message)
+							if message
 								|
 								| The error is:
-						if(message)
+						if message
 							p.error-box
 								| #{message}
 						p.error-description
 							| Please go back and try again.
 							| If the problem persists, please contact us at
 							|
-							a(href="mailto:" + settings.adminEmail) #{settings.adminEmail}
+							a(href='mailto:' + settings.adminEmail) #{settings.adminEmail}
 							| .
 						p.error-actions
-							a.error-btn(href="javascript:history.back()") Back
-							a.btn.btn-secondary(href="/") Home
+							a.error-btn(href='javascript:history.back()') Back
+							a.btn.btn-secondary(href='/') Home

+ 2 - 2
services/web/app/views/general/404.pug

@@ -1,11 +1,11 @@
 extends ../layout-marketing
 
 block content
-	main.content.content-alt#main-content
+	main#main-content.content.content-alt
 		.container
 			.error-container
 				.error-details
 					p.error-status Not found
 					p.error-description #{translate("cant_find_page")}
 					p.error-actions
-						a.error-btn(href="/") Home
+						a.error-btn(href='/') Home

+ 5 - 5
services/web/app/views/general/500.pug

@@ -1,11 +1,11 @@
 extends ../layout/layout-no-js
 
 block vars
-	- metadata = { title: 'Something went wrong' }
+	- metadata = {title: 'Something went wrong'}
 
 block body
 	body.full-height
-		main.content.content-alt.full-height#main-content
+		main#main-content.content.content-alt.full-height
 			.container.full-height
 				.error-container.full-height
 					.error-details
@@ -13,11 +13,11 @@ block body
 						p.error-description Our staff are probably looking into this, but if it continues, please check our status page at
 							|
 							|
-							a(href="http://" + settings.statusPageUrl) #{settings.statusPageUrl}
+							a(href='http://' + settings.statusPageUrl) #{settings.statusPageUrl}
 							|
 							| or contact us at
 							|
-							a(href="mailto:" + settings.adminEmail) #{settings.adminEmail}
+							a(href='mailto:' + settings.adminEmail) #{settings.adminEmail}
 							| .
 						p.error-actions
-							a.error-btn(href="/") Home
+							a.error-btn(href='/') Home

+ 1 - 1
services/web/app/views/general/closed.pug

@@ -1,7 +1,7 @@
 extends ../layout-marketing
 
 block content
-	main.content#main-content
+	main#main-content.content
 		.container
 			.row
 				.col-lg-8.offset-lg-2.text-center

+ 5 - 9
services/web/app/views/general/post-gateway.pug

@@ -15,12 +15,8 @@ block content
 						.card-body
 							p.text-center #{translate('processing_your_request')}
 
-	form(
-		data-ol-regular-form
-		data-ol-auto-submit
-		method="POST"
-	)
-		input(name="_csrf" type="hidden" value=csrfToken)
-		input(hidden name="viaGateway" type="submit" value="true")
-		for name in Object.keys(form_data)
-			input(name=name type="hidden" value=form_data[name])
+	form(data-ol-regular-form data-ol-auto-submit method='POST')
+		input(name='_csrf' type='hidden' value=csrfToken)
+		input(name='viaGateway' hidden type='submit' value='true')
+		each name in Object.keys(form_data)
+			input(name=name type='hidden' value=form_data[name])

+ 5 - 5
services/web/app/views/general/unsupported-browser.pug

@@ -1,11 +1,11 @@
 extends ../layout/layout-no-js
 
 block vars
-	- metadata = { title: 'Unsupported browser' }
+	- metadata = {title: 'Unsupported browser'}
 
 block body
 	body.full-height
-		main.content.content-alt.full-height#main-content
+		main#main-content.content.content-alt.full-height
 			.container.full-height
 				.error-container.full-height
 					.error-details
@@ -15,7 +15,7 @@ block body
 							br
 							| If you think you're seeing this message in error,
 							|
-							a(href="mailto:" + settings.adminEmail) please let us know
+							a(href='mailto:' + settings.adminEmail) please let us know
 							| .
 
 						if fromURL
@@ -33,7 +33,7 @@ block body
 						p
 							| Support for beta or developer-preview browser versions cannot be guaranteed. Please
 							|
-							a(href="mailto:" + settings.adminEmail) get in touch
+							a(href='mailto:' + settings.adminEmail) get in touch
 							|
 							| if you encounter any issues while using the service with beta or developer-preview releases of supported browsers.
 						p
@@ -41,5 +41,5 @@ block body
 						p
 							| If you cannot upgrade to one of the supported browsers,
 							|
-							a(href="mailto:" + settings.adminEmail) please let us know
+							a(href='mailto:' + settings.adminEmail) please let us know
 							| .

+ 89 - 52
services/web/app/views/layout-base.pug

@@ -2,8 +2,8 @@ include ./_mixins/foot_scripts
 
 doctype html
 html(
-	lang=(currentLngCode || 'en')
-	class=(fixedSizeDocument ? 'fixed-size-document' : undefined)
+	lang=currentLngCode || 'en'
+	class=fixedSizeDocument ? 'fixed-size-document' : undefined
 )
 	- metadata = metadata || {}
 	- let bootstrap5PageStatus = 'enabled' // One of 'disabled' and 'enabled'
@@ -22,91 +22,128 @@ html(
 		include ./_metadata.pug
 
 		- const bootstrapVersion = bootstrap5PageStatus !== 'disabled' && (bootstrap5Override || bootstrap5PageSplitTest === '' || splitTestVariants[bootstrap5PageSplitTest] === 'enabled') ? 5 : 3
-	
+
 		//- Stylesheet
-		link(rel='stylesheet', href=buildCssPath(getCssThemeModifier(userSettings, brandVariation, enableIeeeBranding), bootstrapVersion), id="main-stylesheet")
+		link(
+			rel='stylesheet'
+			href=buildCssPath(getCssThemeModifier(userSettings, brandVariation, enableIeeeBranding), bootstrapVersion)
+			id='main-stylesheet'
+		)
 		block css
 			each file in entrypointStyles(entrypoint)
-				link(rel='stylesheet', href=file)
+				link(rel='stylesheet' href=file)
 
 		block _headLinks
 
-		if (typeof suppressRelAlternateLinks == "undefined")
+		if typeof suppressRelAlternateLinks == 'undefined'
 			if settings.i18n.subdomainLang
 				each subdomainDetails in settings.i18n.subdomainLang
 					if !subdomainDetails.hide
-						link(rel="alternate", href=subdomainDetails.url + currentUrl, hreflang=subdomainDetails.lngCode)
-
-		if (entrypoint !== 'marketing')
-			link(rel="preload", href=buildJsPath(currentLngCode + "-json.js"), as="script", nonce=scriptNonce)
+						link(
+							rel='alternate'
+							href=subdomainDetails.url + currentUrl
+							hreflang=subdomainDetails.lngCode
+						)
+
+		if entrypoint !== 'marketing'
+			link(
+				rel='preload'
+				href=buildJsPath(currentLngCode + '-json.js')
+				as='script'
+				nonce=scriptNonce
+			)
 
 		//- Scripts
-		if (typeof suppressGoogleAnalytics == "undefined")
+		if typeof suppressGoogleAnalytics == 'undefined'
 			include _google_analytics
 
 		block meta
-			meta(name="ol-csrfToken" content=csrfToken)
+			meta(name='ol-csrfToken' content=csrfToken)
 			//- Configure dynamically loaded assets (via webpack) to be downloaded from CDN
 			//- See: https://webpack.js.org/guides/public-path/#on-the-fly
-			meta(name="ol-baseAssetPath" content=buildBaseAssetPath())
-			meta(name="ol-mathJaxPath" content=mathJaxPath)
-			meta(name="ol-dictionariesRoot" content=dictionariesRoot)
+			meta(name='ol-baseAssetPath' content=buildBaseAssetPath())
+			meta(name='ol-mathJaxPath' content=mathJaxPath)
+			meta(name='ol-dictionariesRoot' content=dictionariesRoot)
 
-			meta(name="ol-usersEmail" content=getUserEmail())
-			meta(name="ol-ab" data-type="json" content={})
-			meta(name="ol-user_id" content=getLoggedInUserId())
+			meta(name='ol-usersEmail' content=getUserEmail())
+			meta(name='ol-ab' data-type='json' content={})
+			meta(name='ol-user_id' content=getLoggedInUserId())
 			//- Internationalisation settings
-			meta(name="ol-i18n" data-type="json" content={
-				currentLangCode: currentLngCode
-			})
+			meta(
+				name='ol-i18n'
+				data-type='json'
+				content={
+					currentLangCode: currentLngCode,
+				}
+			)
 			//- Expose some settings globally to the frontend
-			meta(name="ol-ExposedSettings" data-type="json" content=ExposedSettings)
-			meta(name="ol-splitTestVariants" data-type="json" content=splitTestVariants || {})
-			meta(name="ol-splitTestInfo" data-type="json" content=splitTestInfo || {})
-
-			if (typeof settings.algolia != "undefined")
-				meta(name="ol-algolia" data-type="json" content={
-					appId: settings.algolia.app_id,
-					apiKey: settings.algolia.read_only_api_key,
-					indexes: settings.algolia.indexes
-				})
-
-			meta(name="ol-isManagedAccount" data-type="boolean" content=isManagedAccount)
+			meta(name='ol-ExposedSettings' data-type='json' content=ExposedSettings)
+			meta(
+				name='ol-splitTestVariants'
+				data-type='json'
+				content=splitTestVariants || {}
+			)
+			meta(name='ol-splitTestInfo' data-type='json' content=splitTestInfo || {})
+
+			if typeof settings.algolia != 'undefined'
+				meta(
+					name='ol-algolia'
+					data-type='json'
+					content={
+						appId: settings.algolia.app_id,
+						apiKey: settings.algolia.read_only_api_key,
+						indexes: settings.algolia.indexes,
+					}
+				)
+
+			meta(
+				name='ol-isManagedAccount'
+				data-type='boolean'
+				content=isManagedAccount
+			)
 			each restriction in userRestrictions || []
-				meta(name='ol-cannot-' + restriction data-type="boolean" content=true)
-			meta(name="ol-bootstrapVersion" data-type="json" content=bootstrapVersion)
+				meta(name='ol-cannot-' + restriction data-type='boolean' content)
+			meta(name='ol-bootstrapVersion' data-type='json' content=bootstrapVersion)
 
 		block head-scripts
 
-	body(class={
-		'thin-footer': showThinFooter,
-		'website-redesign': isWebsiteRedesign === true || websiteRedesignOverride,
-		'application-page': isApplicationPage
-	}, data-theme="default")
-		if(settings.recaptcha && settings.recaptcha.siteKeyV3)
-			script(type="text/javascript", nonce=scriptNonce, src="https://www.recaptcha.net/recaptcha/api.js?render=" + settings.recaptcha.siteKeyV3, defer=deferScripts)
-
-		if (typeof suppressSkipToContent == "undefined")
-			a(class="skip-to-content" href="#main-content") #{translate('skip_to_content')}
+	body(
+		class={
+			'thin-footer': showThinFooter,
+			'website-redesign': isWebsiteRedesign === true || websiteRedesignOverride,
+			'application-page': isApplicationPage,
+		}
+		data-theme='default'
+	)
+		if settings.recaptcha && settings.recaptcha.siteKeyV3
+			script(
+				type='text/javascript'
+				nonce=scriptNonce
+				src='https://www.recaptcha.net/recaptcha/api.js?render=' + settings.recaptcha.siteKeyV3
+				defer=deferScripts
+			)
+
+		if typeof suppressSkipToContent == 'undefined'
+			a(class='skip-to-content' href='#main-content') #{translate('skip_to_content')}
 
 		block body
 
-		if (settings.devToolbar.enabled)
-			div#dev-toolbar
+		if settings.devToolbar.enabled
+			#dev-toolbar
 
 	block foot-scripts
 		+foot-scripts
 
 	include _customer_io
 
-	script(type="text/javascript", nonce=scriptNonce).
-		window.addEventListener('DOMContentLoaded', function() {
+	script(type='text/javascript' nonce=scriptNonce).
+		window.addEventListener('DOMContentLoaded', function () {
 			//- Look for bundle
 			var cdnBlocked = typeof Frontend === 'undefined'
 			//- Prevent loops
-			var noCdnAlreadyInUrl = window.location.href.indexOf("nocdn=true") != -1
-			if (cdnBlocked && !noCdnAlreadyInUrl && navigator.userAgent.indexOf("Googlebot") == -1) {
+			var noCdnAlreadyInUrl = window.location.href.indexOf('nocdn=true') != -1
+			if (cdnBlocked && !noCdnAlreadyInUrl && navigator.userAgent.indexOf('Googlebot') == -1) {
 				//- Set query param, server will not set CDN url
-				window.location.search += "&nocdn=true";
+				window.location.search += '&nocdn=true'
 			}
 		})

+ 5 - 5
services/web/app/views/layout-marketing.pug

@@ -7,7 +7,7 @@ block entrypointVar
 	- entrypoint = 'marketing'
 
 block body
-	if (typeof suppressNavbar === "undefined")
+	if typeof suppressNavbar === 'undefined'
 		if bootstrapVersion === 5
 			include layout/navbar-marketing-bootstrap-5
 		else
@@ -15,7 +15,7 @@ block body
 
 	block content
 
-	if (typeof suppressFooter === "undefined")
+	if typeof suppressFooter === 'undefined'
 		if showThinFooter
 			if bootstrapVersion === 5
 				include layout/thin-footer-bootstrap-5
@@ -24,13 +24,13 @@ block body
 		else
 			include layout/fat-footer
 
-	if (typeof(suppressCookieBanner) == 'undefined')
+	if typeof suppressCookieBanner == 'undefined'
 		include _cookie_banner
 
 	if bootstrapVersion === 5
-		!= moduleIncludes("contactModal-marketing-bootstrap-5", locals)
+		!= moduleIncludes('contactModal-marketing-bootstrap-5', locals)
 	else
-		!= moduleIncludes("contactModal-marketing", locals)
+		!= moduleIncludes('contactModal-marketing', locals)
 
 block prepend foot-scripts
 	+bootstrap-js(bootstrapVersion)

+ 39 - 31
services/web/app/views/layout-react.pug

@@ -7,10 +7,10 @@ include ./_mixins/bootstrap_js
 
 block entrypointVar
 	- entrypoint = 'marketing'
-		
+
 block isApplicationPageVar
 	- isApplicationPage = true
-		
+
 block append meta
 	- const canDisplayAdminMenu = hasAdminAccess()
 	- const canDisplayAdminRedirect = canRedirectToAdminDomain()
@@ -22,44 +22,52 @@ block append meta
 	- const enableUpgradeButton = projectDashboardReact && usersBestSubscription && (usersBestSubscription.type === 'free' || usersBestSubscription.type === 'standalone-ai-add-on')
 	- const showSignUpLink = hasFeature('registration-page')
 
-	meta(name="ol-navbar" data-type="json" content={
-		customLogo: settings.nav.custom_logo,
-		title: nav.title,
-		canDisplayAdminMenu,
-		canDisplayAdminRedirect,
-		canDisplaySplitTestMenu,
-		canDisplaySurveyMenu,
-		canDisplayScriptLogMenu,
-		enableUpgradeButton,
-		suppressNavbarRight: !!suppressNavbarRight,
-		suppressNavContentLinks: !!suppressNavContentLinks,
-		showSubscriptionLink: nav.showSubscriptionLink,
-		showSignUpLink: showSignUpLink,
-		currentUrl: currentUrl,
-		sessionUser: sessionUser ? { email: sessionUser.email} : undefined,
-		adminUrl: settings.adminUrl,
-		items: cloneAndTranslateText(nav.header_extras)
-	})
-	meta(name="ol-footer" data-type="json" content={
-		showThinFooter: showThinFooter,
-		showPoweredBy: !hasFeature('saas') && !settings.nav.hide_powered_by,
-		subdomainLang: settings.i18n.subdomainLang,
-		translatedLanguages: settings.translatedLanguages,
-		leftItems: cloneAndTranslateText(settings.nav.left_footer),
-		rightItems: settings.nav.right_footer
-	})
+	meta(
+		name='ol-navbar'
+		data-type='json'
+		content={
+			customLogo: settings.nav.custom_logo,
+			title: nav.title,
+			canDisplayAdminMenu,
+			canDisplayAdminRedirect,
+			canDisplaySplitTestMenu,
+			canDisplaySurveyMenu,
+			canDisplayScriptLogMenu,
+			enableUpgradeButton,
+			suppressNavbarRight: !!suppressNavbarRight,
+			suppressNavContentLinks: !!suppressNavContentLinks,
+			showSubscriptionLink: nav.showSubscriptionLink,
+			showSignUpLink: showSignUpLink,
+			currentUrl: currentUrl,
+			sessionUser: sessionUser ? {email: sessionUser.email} : undefined,
+			adminUrl: settings.adminUrl,
+			items: cloneAndTranslateText(nav.header_extras),
+		}
+	)
+	meta(
+		name='ol-footer'
+		data-type='json'
+		content={
+			showThinFooter: showThinFooter,
+			showPoweredBy: !hasFeature('saas') && !settings.nav.hide_powered_by,
+			subdomainLang: settings.i18n.subdomainLang,
+			translatedLanguages: settings.translatedLanguages,
+			leftItems: cloneAndTranslateText(settings.nav.left_footer),
+			rightItems: settings.nav.right_footer,
+		}
+	)
 
 block body
-	if (typeof suppressNavbar === "undefined")
+	if typeof suppressNavbar === 'undefined'
 		include layout/navbar-marketing-react-bootstrap-5
 
 	block content
 
-	if (typeof suppressFooter === "undefined")
+	if typeof suppressFooter === 'undefined'
 		if showThinFooter
 			include layout/thin-footer-bootstrap-5
 		else
 			include layout/fat-footer-react-bootstrap-5
 
-	if (typeof suppressCookieBanner === "undefined")
+	if typeof suppressCookieBanner === 'undefined'
 		include _cookie_banner

+ 6 - 6
services/web/app/views/layout-website-redesign.pug

@@ -7,7 +7,7 @@ block entrypointVar
 	- entrypoint = 'marketing'
 
 block body
-	if (typeof(suppressNavbar) == "undefined")
+	if typeof suppressNavbar == 'undefined'
 		if bootstrapVersion === 5
 			include layout/navbar-marketing-bootstrap-5
 		else
@@ -17,23 +17,23 @@ block body
 
 	//- bootstrapVersion needed here, because plans.pug uses both BS version
 	//- If the `plans-page-bs5` split test has been completed, remove bootstrapVersion logic
-	if (typeof(suppressFooter) == "undefined")
+	if typeof suppressFooter == 'undefined'
 		if showThinFooter
 			if bootstrapVersion === 5
 				include layout/thin-footer-bootstrap-5
-			else 
+			else
 				include layout/thin-footer
 		else
 			include layout/fat-footer-website-redesign
 
-	if (typeof(suppressCookieBanner) == 'undefined')
+	if typeof suppressCookieBanner == 'undefined'
 		include _cookie_banner
 
 	block contactModal
 		if bootstrapVersion === 5
-			!= moduleIncludes("contactModal-marketing-bootstrap-5", locals)
+			!= moduleIncludes('contactModal-marketing-bootstrap-5', locals)
 		else
-			!= moduleIncludes("contactModal-marketing", locals)
+			!= moduleIncludes('contactModal-marketing', locals)
 
 block prepend foot-scripts
 	+bootstrap-js(bootstrapVersion)

+ 49 - 18
services/web/app/views/layout/fat-footer-base.pug

@@ -1,9 +1,9 @@
 .fat-footer-base
 	.fat-footer-base-section.fat-footer-base-meta
-		.fat-footer-base-item 
+		.fat-footer-base-item
 			.fat-footer-base-copyright © #{new Date().getFullYear()} Overleaf
-			a(href="/legal") #{translate('privacy_and_terms')}
-			a(href="https://www.digital-science.com/security-certifications/") #{translate('compliance')}
+			a(href='/legal') #{translate('privacy_and_terms')}
+			a(href='https://www.digital-science.com/security-certifications/') #{translate('compliance')}
 		ul.fat-footer-base-item.list-unstyled.fat-footer-base-language
 			if bootstrapVersion === 5
 				include language-picker-bootstrap-5
@@ -11,22 +11,53 @@
 				include language-picker
 	.fat-footer-base-section.fat-footer-base-social
 		.fat-footer-base-item
-			a.fat-footer-social.x-logo(href="https://x.com/overleaf")
-				svg(xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 1227" height="25")
-					path(d="M714.163 519.284L1160.89 0H1055.03L667.137 450.887L357.328 0H0L468.492 681.821L0 1226.37H105.866L515.491 750.218L842.672 1226.37H1200L714.137 519.284H714.163ZM569.165 687.828L521.697 619.934L144.011 79.6944H306.615L611.412 515.685L658.88 583.579L1055.08 1150.3H892.476L569.165 687.854V687.828Z")
+			a.fat-footer-social.x-logo(href='https://x.com/overleaf')
+				svg(
+					xmlns='http://www.w3.org/2000/svg'
+					viewBox='0 0 1200 1227'
+					height='25'
+				)
+					path(
+						d='M714.163 519.284L1160.89 0H1055.03L667.137 450.887L357.328 0H0L468.492 681.821L0 1226.37H105.866L515.491 750.218L842.672 1226.37H1200L714.137 519.284H714.163ZM569.165 687.828L521.697 619.934L144.011 79.6944H306.615L611.412 515.685L658.88 583.579L1055.08 1150.3H892.476L569.165 687.854V687.828Z'
+					)
 				span.visually-hidden #{translate("app_on_x", {social: "X"})}
-			a.fat-footer-social.facebook-logo(href="https://www.facebook.com/overleaf.editor")
-				svg(xmlns="http://www.w3.org/2000/svg" viewBox="0 0 666.66668 666.66717" height="25")
+			a.fat-footer-social.facebook-logo(
+				href='https://www.facebook.com/overleaf.editor'
+			)
+				svg(
+					xmlns='http://www.w3.org/2000/svg'
+					viewBox='0 0 666.66668 666.66717'
+					height='25'
+				)
 					defs
-						clipPath(id="a" clipPathUnits="userSpaceOnUse")
-							path(d="M0 700h700V0H0Z")
-					g(clip-path="url(#a)" transform="matrix(1.33333 0 0 -1.33333 -133.333 800)")
-						path.background(d="M0 0c0 138.071-111.929 250-250 250S-500 138.071-500 0c0-117.245 80.715-215.622 189.606-242.638v166.242h-51.552V0h51.552v32.919c0 85.092 38.508 124.532 122.048 124.532 15.838 0 43.167-3.105 54.347-6.211V81.986c-5.901.621-16.149.932-28.882.932-40.993 0-56.832-15.528-56.832-55.9V0h81.659l-14.028-76.396h-67.631v-171.773C-95.927-233.218 0-127.818 0 0" fill="#0866ff" transform="translate(600 350)")
-						path.text(d="m0 0 14.029 76.396H-67.63v27.019c0 40.372 15.838 55.899 56.831 55.899 12.733 0 22.981-.31 28.882-.931v69.253c-11.18 3.106-38.509 6.212-54.347 6.212-83.539 0-122.048-39.441-122.048-124.533V76.396h-51.552V0h51.552v-166.242a250.559 250.559 0 0 1 60.394-7.362c10.254 0 20.358.632 30.288 1.831V0Z" fill="#fff" transform="translate(447.918 273.604)")
+						clipPath(id='a' clipPathUnits='userSpaceOnUse')
+							path(d='M0 700h700V0H0Z')
+					g(
+						clip-path='url(#a)'
+						transform='matrix(1.33333 0 0 -1.33333 -133.333 800)'
+					)
+						path.background(
+							d='M0 0c0 138.071-111.929 250-250 250S-500 138.071-500 0c0-117.245 80.715-215.622 189.606-242.638v166.242h-51.552V0h51.552v32.919c0 85.092 38.508 124.532 122.048 124.532 15.838 0 43.167-3.105 54.347-6.211V81.986c-5.901.621-16.149.932-28.882.932-40.993 0-56.832-15.528-56.832-55.9V0h81.659l-14.028-76.396h-67.631v-171.773C-95.927-233.218 0-127.818 0 0'
+							fill='#0866ff'
+							transform='translate(600 350)'
+						)
+						path.text(
+							d='m0 0 14.029 76.396H-67.63v27.019c0 40.372 15.838 55.899 56.831 55.899 12.733 0 22.981-.31 28.882-.931v69.253c-11.18 3.106-38.509 6.212-54.347 6.212-83.539 0-122.048-39.441-122.048-124.533V76.396h-51.552V0h51.552v-166.242a250.559 250.559 0 0 1 60.394-7.362c10.254 0 20.358.632 30.288 1.831V0Z'
+							fill='#fff'
+							transform='translate(447.918 273.604)'
+						)
 				span.visually-hidden #{translate("app_on_x", {social: "Facebook"})}
-			a.fat-footer-social.linkedin-logo(href="https://www.linkedin.com/company/writelatex-limited")
-				svg(xmlns="http://www.w3.org/2000/svg" viewBox="0 0 72 72" height="25")
-					g(fill="none" fill-rule="evenodd")
-						path.background(fill="#0B66C3" d="M8 72h56a8 8 0 0 0 8-8V8a8 8 0 0 0-8-8H8a8 8 0 0 0-8 8v56a8 8 0 0 0 8 8")
-						path.text(fill="#FFF" d="M62 62H51.316V43.802c0-4.99-1.896-7.777-5.845-7.777-4.296 0-6.54 2.901-6.54 7.777V62H28.632V27.333H38.93v4.67s3.096-5.729 10.453-5.729c7.353 0 12.617 4.49 12.617 13.777zM16.35 22.794c-3.508 0-6.35-2.864-6.35-6.397C10 12.864 12.842 10 16.35 10c3.507 0 6.347 2.864 6.347 6.397 0 3.533-2.84 6.397-6.348 6.397ZM11.032 62h10.736V27.333H11.033V62")
+			a.fat-footer-social.linkedin-logo(
+				href='https://www.linkedin.com/company/writelatex-limited'
+			)
+				svg(xmlns='http://www.w3.org/2000/svg' viewBox='0 0 72 72' height='25')
+					g(fill='none' fill-rule='evenodd')
+						path.background(
+							fill='#0B66C3'
+							d='M8 72h56a8 8 0 0 0 8-8V8a8 8 0 0 0-8-8H8a8 8 0 0 0-8 8v56a8 8 0 0 0 8 8'
+						)
+						path.text(
+							fill='#FFF'
+							d='M62 62H51.316V43.802c0-4.99-1.896-7.777-5.845-7.777-4.296 0-6.54 2.901-6.54 7.777V62H28.632V27.333H38.93v4.67s3.096-5.729 10.453-5.729c7.353 0 12.617 4.49 12.617 13.777zM16.35 22.794c-3.508 0-6.35-2.864-6.35-6.397C10 12.864 12.842 10 16.35 10c3.507 0 6.347 2.864 6.347 6.397 0 3.533-2.84 6.397-6.348 6.397ZM11.032 62h10.736V27.333H11.033V62'
+						)
 				span.visually-hidden #{translate("app_on_x", {social: "LinkedIn"})}

+ 41 - 38
services/web/app/views/layout/fat-footer-website-redesign.pug

@@ -1,84 +1,87 @@
 footer.fat-footer.hidden-print.website-redesign-fat-footer
-	.fat-footer-container(role="navigation" aria-label=translate('footer_navigation'))
+	.fat-footer-container(
+		role='navigation'
+		aria-label=translate('footer_navigation')
+	)
 		.fat-footer-sections(class=hideFatFooter ? 'hidden' : undefined)
-			.footer-section#footer-brand
-				a(href='/', aria-label=settings.appName).footer-brand
-					
+			#footer-brand.footer-section
+				a.footer-brand(href='/' aria-label=settings.appName)
+
 			.footer-section
 				h2.footer-section-heading #{translate('About')}
-					
+
 				ul.list-unstyled
 					li
-						a(href="/about") #{translate('footer_about_us')}
+						a(href='/about') #{translate('footer_about_us')}
 					li
-						a(href="/about/values") #{translate('our_values')}
+						a(href='/about/values') #{translate('our_values')}
 					li
-						a(href="https://digitalscience.pinpointhq.com/") #{translate('careers')}
+						a(href='https://digitalscience.pinpointhq.com/') #{translate('careers')}
 					li
-						a(href="/for/press") !{translate('press_and_awards')}
+						a(href='/for/press') !{translate('press_and_awards')}
 					li
-						a(href="/blog") #{translate('blog')}
-						
+						a(href='/blog') #{translate('blog')}
+
 			.footer-section
 				h2.footer-section-heading #{translate('learn')}
-		
+
 				ul.list-unstyled
 					li
-						a(href="/learn/latex/Learn_LaTeX_in_30_minutes") #{translate('latex_in_thirty_minutes')}
+						a(href='/learn/latex/Learn_LaTeX_in_30_minutes') #{translate('latex_in_thirty_minutes')}
 					li
-						a(href="/latex/templates") #{translate('templates')}
+						a(href='/latex/templates') #{translate('templates')}
 					li
-						a(href="/events/webinars") #{translate('webinars')}
+						a(href='/events/webinars') #{translate('webinars')}
 					li
-						a(href="/learn/latex/Tutorials") #{translate('tutorials')}
+						a(href='/learn/latex/Tutorials') #{translate('tutorials')}
 					li
-						a(href="/learn/latex/Inserting_Images") #{translate('how_to_insert_images')}
+						a(href='/learn/latex/Inserting_Images') #{translate('how_to_insert_images')}
 					li
-						a(href="/learn/latex/Tables") #{translate('how_to_create_tables')}
-						
+						a(href='/learn/latex/Tables') #{translate('how_to_create_tables')}
+
 			.footer-section
 				h2.footer-section-heading !{translate('footer_plans_and_pricing')}
-		
+
 				ul.list-unstyled
 					li
-						a(href="/learn/how-to/Overleaf_premium_features") #{translate('premium_features')}
+						a(href='/learn/how-to/Overleaf_premium_features') #{translate('premium_features')}
 					li
-						a(href="/user/subscription/plans?itm_referrer=footer-for-indv-groups") !{translate('for_individuals_and_groups')}
+						a(href='/user/subscription/plans?itm_referrer=footer-for-indv-groups') !{translate('for_individuals_and_groups')}
 					li
-						a(href="/for/enterprises") #{translate('for_business')}
+						a(href='/for/enterprises') #{translate('for_business')}
 					li
-						a(href="/for/universities") #{translate('for_universities')}
+						a(href='/for/universities') #{translate('for_universities')}
 					li
 						a(
 							data-ol-for-students-link
-							href="/user/subscription/plans?itm_referrer=footer-for-students#student-annual"
+							href='/user/subscription/plans?itm_referrer=footer-for-students#student-annual'
 						) #{translate('for_students')}
 					li
-						a(href="/for/government") #{translate('for_government')}
-						
+						a(href='/for/government') #{translate('for_government')}
+
 			.footer-section
 				h2.footer-section-heading #{translate('get_involved')}
-		
+
 				ul.list-unstyled
 					li
-						a(href="/for/community/advisors") #{translate('become_an_advisor')}
+						a(href='/for/community/advisors') #{translate('become_an_advisor')}
 					li
-						a(href="https://forms.gle/67PSpN1bLnjGCmPQ9") #{translate('let_us_know_what_you_think')}
+						a(href='https://forms.gle/67PSpN1bLnjGCmPQ9') #{translate('let_us_know_what_you_think')}
 					if user
 						li
-							a(href="/beta/participate") #{translate('join_beta_program')}
-		
+							a(href='/beta/participate') #{translate('join_beta_program')}
+
 			.footer-section
 				h2.footer-section-heading #{translate('help')}
-		
+
 				ul.list-unstyled
 					li
-						a(href="/about/why-latex") #{translate('why_latex')}
+						a(href='/about/why-latex') #{translate('why_latex')}
 					li
-						a(href="/learn") #{translate('Documentation')} 
+						a(href='/learn') #{translate('Documentation')} 
 					li
-						a(href="/contact") #{translate('footer_contact_us')}  
+						a(href='/contact') #{translate('footer_contact_us')} 
 					li
-						a(href="https://status.overleaf.com/") #{translate('website_status')}
-					
+						a(href='https://status.overleaf.com/') #{translate('website_status')}
+
 		include fat-footer-base

+ 41 - 38
services/web/app/views/layout/fat-footer.pug

@@ -1,84 +1,87 @@
 footer.fat-footer.hidden-print
-	.fat-footer-container(role="navigation" aria-label=translate('footer_navigation'))
+	.fat-footer-container(
+		role='navigation'
+		aria-label=translate('footer_navigation')
+	)
 		.fat-footer-sections(class=hideFatFooter ? 'hidden' : undefined)
-			.footer-section#footer-brand
-				a(href='/', aria-label=settings.appName).footer-brand
-					
+			#footer-brand.footer-section
+				a.footer-brand(href='/' aria-label=settings.appName)
+
 			.footer-section
 				h2.footer-section-heading #{translate('About')}
-					
+
 				ul.list-unstyled
 					li
-						a(href="/about") #{translate('footer_about_us')}
+						a(href='/about') #{translate('footer_about_us')}
 					li
-						a(href="/about/values") #{translate('our_values')}
+						a(href='/about/values') #{translate('our_values')}
 					li
-						a(href="https://digitalscience.pinpointhq.com/") #{translate('careers')}
+						a(href='https://digitalscience.pinpointhq.com/') #{translate('careers')}
 					li
-						a(href="/for/press") !{translate('press_and_awards')}
+						a(href='/for/press') !{translate('press_and_awards')}
 					li
-						a(href="/blog") #{translate('blog')}
-						
+						a(href='/blog') #{translate('blog')}
+
 			.footer-section
 				h2.footer-section-heading #{translate('learn')}
-		
+
 				ul.list-unstyled
 					li
-						a(href="/learn/latex/Learn_LaTeX_in_30_minutes") #{translate('latex_in_thirty_minutes')}
+						a(href='/learn/latex/Learn_LaTeX_in_30_minutes') #{translate('latex_in_thirty_minutes')}
 					li
-						a(href="/latex/templates") #{translate('templates')}
+						a(href='/latex/templates') #{translate('templates')}
 					li
-						a(href="/events/webinars") #{translate('webinars')}
+						a(href='/events/webinars') #{translate('webinars')}
 					li
-						a(href="/learn/latex/Tutorials") #{translate('tutorials')}
+						a(href='/learn/latex/Tutorials') #{translate('tutorials')}
 					li
-						a(href="/learn/latex/Inserting_Images") #{translate('how_to_insert_images')}
+						a(href='/learn/latex/Inserting_Images') #{translate('how_to_insert_images')}
 					li
-						a(href="/learn/latex/Tables") #{translate('how_to_create_tables')}
-						
+						a(href='/learn/latex/Tables') #{translate('how_to_create_tables')}
+
 			.footer-section
 				h2.footer-section-heading !{translate('footer_plans_and_pricing')}
-		
+
 				ul.list-unstyled
 					li
-						a(href="/learn/how-to/Overleaf_premium_features") #{translate('premium_features')}
+						a(href='/learn/how-to/Overleaf_premium_features') #{translate('premium_features')}
 					li
-						a(href="/user/subscription/plans?itm_referrer=footer-for-indv-groups") !{translate('for_individuals_and_groups')}
+						a(href='/user/subscription/plans?itm_referrer=footer-for-indv-groups') !{translate('for_individuals_and_groups')}
 					li
-						a(href="/for/enterprises") #{translate('for_enterprise')}
+						a(href='/for/enterprises') #{translate('for_enterprise')}
 					li
-						a(href="/for/universities") #{translate('for_universities')}
+						a(href='/for/universities') #{translate('for_universities')}
 					li
 						a(
 							data-ol-for-students-link
-							href="/user/subscription/plans?itm_referrer=footer-for-students#student-annual"
+							href='/user/subscription/plans?itm_referrer=footer-for-students#student-annual'
 						) #{translate('for_students')}
 					li
-						a(href="/for/government") #{translate('for_government')}
-						
+						a(href='/for/government') #{translate('for_government')}
+
 			.footer-section
 				h2.footer-section-heading #{translate('get_involved')}
-		
+
 				ul.list-unstyled
 					li
-						a(href="/for/community/advisors") #{translate('become_an_advisor')}
+						a(href='/for/community/advisors') #{translate('become_an_advisor')}
 					li
-						a(href="https://forms.gle/67PSpN1bLnjGCmPQ9") #{translate('let_us_know_what_you_think')}
+						a(href='https://forms.gle/67PSpN1bLnjGCmPQ9') #{translate('let_us_know_what_you_think')}
 					if user
 						li
-							a(href="/beta/participate") #{translate('join_beta_program')}
-		
+							a(href='/beta/participate') #{translate('join_beta_program')}
+
 			.footer-section
 				h2.footer-section-heading #{translate('help')}
-		
+
 				ul.list-unstyled
 					li
-						a(href="/about/why-latex") #{translate('why_latex')}
+						a(href='/about/why-latex') #{translate('why_latex')}
 					li
-						a(href="/learn") #{translate('Documentation')} 
+						a(href='/learn') #{translate('Documentation')} 
 					li
-						a(href="/contact") #{translate('footer_contact_us')}  
+						a(href='/contact') #{translate('footer_contact_us')} 
 					li
-						a(href="https://status.overleaf.com/") #{translate('website_status')}
-					
+						a(href='https://status.overleaf.com/') #{translate('website_status')}
+
 		include fat-footer-base

+ 19 - 11
services/web/app/views/layout/language-picker-bootstrap-5.pug

@@ -1,27 +1,35 @@
 include ../_mixins/material_symbol
 
-li.dropdown.dropup.subdued(dropdown).language-picker
+li.dropdown.dropup.subdued.language-picker(dropdown)
 	button#language-picker-toggle.btn.btn-link.btn-inline-link(
-		dropdown-toggle,
-		data-ol-lang-selector-tooltip,
-		data-bs-toggle="dropdown",
-		aria-haspopup="true",
-		aria-expanded="false",
-		aria-label="Select " + translate('language'),
+		dropdown-toggle
+		data-ol-lang-selector-tooltip
+		data-bs-toggle='dropdown'
+		aria-haspopup='true'
+		aria-expanded='false'
+		aria-label='Select ' + translate('language')
 		tooltip=translate('language')
 		title=translate('language')
 	)
-		+material-symbol("translate")
+		+material-symbol('translate')
 		| &nbsp;
 		span.language-picker-text #{settings.translatedLanguages[currentLngCode]}
 
-	ul.dropdown-menu.dropdown-menu-sm-width(role="menu" aria-labelledby="language-picker-toggle")
+	ul.dropdown-menu.dropdown-menu-sm-width(
+		role='menu'
+		aria-labelledby='language-picker-toggle'
+	)
 		li.dropdown-header #{translate("language")}
 		each subdomainDetails, subdomain in settings.i18n.subdomainLang
 			if !subdomainDetails.hide
 				- let isActive = subdomainDetails.lngCode === currentLngCode
 				li.lng-option
-					a.menu-indent(href=subdomainDetails.url+currentUrlWithQueryParams, role="menuitem", class=isActive ? 'dropdown-item active' : 'dropdown-item', aria-selected=isActive ? 'true' : 'false')
+					a.menu-indent(
+						href=subdomainDetails.url + currentUrlWithQueryParams
+						role='menuitem'
+						class=isActive ? 'dropdown-item active' : 'dropdown-item'
+						aria-selected=isActive ? 'true' : 'false'
+					)
 						| #{settings.translatedLanguages[subdomainDetails.lngCode]}
 						if subdomainDetails.lngCode === currentLngCode
-							+material-symbol("check", "dropdown-item-trailing-icon")
+							+material-symbol('check', 'dropdown-item-trailing-icon')

+ 15 - 12
services/web/app/views/layout/language-picker.pug

@@ -1,13 +1,13 @@
-li.dropdown.dropup.subdued(dropdown).language-picker
-	a.dropdown-toggle#language-picker-toggle(
-		href="#",
-		dropdown-toggle,
-		data-ol-lang-selector-tooltip,
-		data-toggle="dropdown",
-		role="button"
-		aria-haspopup="true",
-		aria-expanded="false",
-		aria-label="Select " + translate('language'),
+li.dropdown.dropup.subdued.language-picker(dropdown)
+	a#language-picker-toggle.dropdown-toggle(
+		href='#'
+		dropdown-toggle
+		data-ol-lang-selector-tooltip
+		data-toggle='dropdown'
+		role='button'
+		aria-haspopup='true'
+		aria-expanded='false'
+		aria-label='Select ' + translate('language')
 		tooltip=translate('language')
 		title=translate('language')
 	)
@@ -15,10 +15,13 @@ li.dropdown.dropup.subdued(dropdown).language-picker
 		|
 		| #{settings.translatedLanguages[currentLngCode]}
 
-	ul.dropdown-menu(role="menu" aria-labelledby="language-picker-toggle")
+	ul.dropdown-menu(role='menu' aria-labelledby='language-picker-toggle')
 		li.dropdown-header #{translate("language")}
 		each subdomainDetails, subdomain in settings.i18n.subdomainLang
 			if !subdomainDetails.hide
 				li.lng-option
-					a.menu-indent(href=subdomainDetails.url+currentUrlWithQueryParams role="menuitem")
+					a.menu-indent(
+						href=subdomainDetails.url + currentUrlWithQueryParams
+						role='menuitem'
+					)
 						| #{settings.translatedLanguages[subdomainDetails.lngCode]}

+ 8 - 6
services/web/app/views/layout/layout-no-js.pug

@@ -1,18 +1,20 @@
 doctype html
-html(lang="en")
-
+html(lang='en')
 	- metadata = metadata || {}
 	block vars
 
 	head
-		if (metadata && metadata.title)
+		if metadata && metadata.title
 			title= metadata.title
 		if metadata && metadata.viewport
-			meta(name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes")
+			meta(
+				name='viewport'
+				content='width=device-width, initial-scale=1.0, user-scalable=yes'
+			)
 
-		link(rel="icon", href="/favicon.ico")
+		link(rel='icon' href='/favicon.ico')
 
 		if buildCssPath
-			link(rel="stylesheet", href=buildCssPath('', 5))
+			link(rel='stylesheet' href=buildCssPath('', 5))
 
 block body

+ 108 - 96
services/web/app/views/layout/navbar-marketing-bootstrap-5.pug

@@ -1,26 +1,32 @@
 include ../_mixins/navbar
 
-nav.navbar.navbar-default.navbar-main.navbar-expand-lg(class={
-	'website-redesign-navbar': isWebsiteRedesign
-})
+nav.navbar.navbar-default.navbar-main.navbar-expand-lg(
+	class={
+		'website-redesign-navbar': isWebsiteRedesign,
+	}
+)
 	.container-fluid.navbar-container
 		.navbar-header
 			if settings.nav.custom_logo
-				a(href='/', aria-label=settings.appName, style='background-image:url("'+settings.nav.custom_logo+'")').navbar-brand
-			else if (nav.title)
-				a(href='/', aria-label=settings.appName).navbar-title #{nav.title}
+				a.navbar-brand(
+					href='/'
+					aria-label=settings.appName
+					style='background-image:url("' + settings.nav.custom_logo + '")'
+				)
+			else if nav.title
+				a.navbar-title(href='/' aria-label=settings.appName) #{nav.title}
 			else
-				a(href='/', aria-label=settings.appName).navbar-brand
+				a.navbar-brand(href='/' aria-label=settings.appName)
 
 			- var enableUpgradeButton = projectDashboardReact && usersBestSubscription && (usersBestSubscription.type === 'free' || usersBestSubscription.type === 'standalone-ai-add-on')
-			if (enableUpgradeButton)
+			if enableUpgradeButton
 				a.btn.btn-primary.me-2.d-md-none(
-					href="/user/subscription/plans"
-					event-tracking="upgrade-button-click"
-					event-tracking-mb="true"
-					event-tracking-label="upgrade"
-					event-tracking-trigger="click"
-					event-segmentation='{"source": "dashboard-top", "project-dashboard-react": "enabled", "is-dashboard-sidebar-hidden": "true", "is-screen-width-less-than-768px": "true"}'
+					href='/user/subscription/plans'
+					event-tracking='upgrade-button-click'
+					event-tracking-mb='true'
+					event-tracking-label='upgrade'
+					event-tracking-trigger='click'
+					event-segmentation={source: 'dashboard-top', projectDashboardReact: 'enabled', isDashboardSidebarHidden: 'true', isScreenWidthLessThan768px: 'true'}
 				) #{translate("upgrade")}
 
 		- var canDisplayAdminMenu = hasAdminAccess()
@@ -29,45 +35,45 @@ nav.navbar.navbar-default.navbar-main.navbar-expand-lg(class={
 		- var canDisplaySurveyMenu = hasFeature('saas') && canDisplayAdminMenu
 		- var canDisplayScriptLogMenu = hasFeature('saas') && canDisplayAdminMenu
 
-		if (typeof suppressNavbarRight === "undefined")
+		if typeof suppressNavbarRight === 'undefined'
 			button.navbar-toggler.collapsed(
-				type="button",
-				data-bs-toggle="collapse",
-				data-bs-target="#navbar-main-collapse"
-				aria-controls="navbar-main-collapse"
-				aria-expanded="false"
-				aria-label="Toggle " + translate('navigation')
+				type='button'
+				data-bs-toggle='collapse'
+				data-bs-target='#navbar-main-collapse'
+				aria-controls='navbar-main-collapse'
+				aria-expanded='false'
+				aria-label='Toggle ' + translate('navigation')
 			)
-				i.fa.fa-bars(aria-hidden="true")
+				i.fa.fa-bars(aria-hidden='true')
 
-			.navbar-collapse.collapse#navbar-main-collapse
-				ul.nav.navbar-nav.navbar-right.ms-auto(role="menubar")
-					if (canDisplayAdminMenu || canDisplayAdminRedirect || canDisplaySplitTestMenu)
+			#navbar-main-collapse.navbar-collapse.collapse
+				ul.nav.navbar-nav.navbar-right.ms-auto(role='menubar')
+					if canDisplayAdminMenu || canDisplayAdminRedirect || canDisplaySplitTestMenu
 						+nav-item.dropdown.subdued
 							button.dropdown-toggle(
-								aria-haspopup="true",
-								aria-expanded="false",
-								data-bs-toggle="dropdown"
-								role="menuitem"
-								event-tracking="menu-expand"
-								event-tracking-mb="true"
-								event-tracking-trigger="click"
-								event-segmentation={"item": "admin", "location": "top-menu"}
+								aria-haspopup='true'
+								aria-expanded='false'
+								data-bs-toggle='dropdown'
+								role='menuitem'
+								event-tracking='menu-expand'
+								event-tracking-mb='true'
+								event-tracking-trigger='click'
+								event-segmentation={item: 'admin', location: 'top-menu'}
 							)
 								| Admin
 							+dropdown-menu.dropdown-menu-end
 								if canDisplayAdminMenu
-									+dropdown-menu-link-item()(href="/admin") Manage Site
-									+dropdown-menu-link-item()(href="/admin/user") Manage Users
-									+dropdown-menu-link-item()(href="/admin/project") Project URL Lookup
+									+dropdown-menu-link-item(href='/admin') Manage Site
+									+dropdown-menu-link-item(href='/admin/user') Manage Users
+									+dropdown-menu-link-item(href='/admin/project') Project URL Lookup
 								if canDisplayAdminRedirect
-									+dropdown-menu-link-item()(href=settings.adminUrl) Switch to Admin
+									+dropdown-menu-link-item(href=settings.adminUrl) Switch to Admin
 								if canDisplaySplitTestMenu
-									+dropdown-menu-link-item()(href="/admin/split-test") Manage Feature Flags
+									+dropdown-menu-link-item(href='/admin/split-test') Manage Feature Flags
 								if canDisplaySurveyMenu
-									+dropdown-menu-link-item()(href="/admin/survey") Manage Surveys
+									+dropdown-menu-link-item(href='/admin/survey') Manage Surveys
 								if canDisplayScriptLogMenu
-									+dropdown-menu-link-item()(href="/admin/script-logs") View Script Logs
+									+dropdown-menu-link-item(href='/admin/script-logs') View Script Logs
 
 					// loop over header_extras
 					each item in nav.header_extras
@@ -86,14 +92,14 @@ nav.navbar.navbar-default.navbar-main.navbar-expand-lg(class={
 							if item.dropdown
 								+nav-item.dropdown(class=item.class)
 									button.dropdown-toggle(
-										aria-haspopup="true",
-										aria-expanded="false",
-										data-bs-toggle="dropdown"
-										role="menuitem"
-										event-tracking="menu-expand"
-										event-tracking-mb="true"
-										event-tracking-trigger="click"
-										event-segmentation={"item": item.trackingKey, "location": "top-menu"}
+										aria-haspopup='true'
+										aria-expanded='false'
+										data-bs-toggle='dropdown'
+										role='menuitem'
+										event-tracking='menu-expand'
+										event-tracking-mb='true'
+										event-tracking-trigger='click'
+										event-segmentation={item: item.trackingKey, location: 'top-menu'}
 									)
 										| !{translate(item.text)}
 									+dropdown-menu.dropdown-menu-end
@@ -101,31 +107,41 @@ nav.navbar.navbar-default.navbar-main.navbar-expand-lg(class={
 											if child.divider
 												+dropdown-menu-divider
 											else if child.isContactUs
-												+dropdown-menu-link-item()(data-ol-open-contact-form-modal="contact-us" data-bs-target="#contactUsModal" href data-bs-toggle="modal" event-tracking="menu-click" event-tracking-mb="true" event-tracking-trigger="click" event-segmentation={"item": "contact", "location": "top-menu"})
+												+dropdown-menu-link-item(
+													data-ol-open-contact-form-modal='contact-us'
+													data-bs-target='#contactUsModal'
+													href
+													data-bs-toggle='modal'
+													event-tracking='menu-click'
+													event-tracking-mb='true'
+													event-tracking-trigger='click'
+													event-segmentation={item: 'contact', location: 'top-menu'}
+												)
 													span
 														| #{translate("contact_us")}
 											else
 												if child.url
-													+dropdown-menu-link-item()(
-														href=child.url,
-														class=child.class,
-														event-tracking="menu-click"
-														event-tracking-mb="true"
-														event-tracking-trigger="click"
-														event-segmentation={ item: child.trackingKey, location: 'top-menu' }
+													+dropdown-menu-link-item(
+														href=child.url
+														class=child.class
+														event-tracking='menu-click'
+														event-tracking-mb='true'
+														event-tracking-trigger='click'
+														event-segmentation={item: child.trackingKey, location: 'top-menu'}
 													) !{translate(child.text)}
 												else
-													+dropdown-menu-item !{translate(child.text)}
+													+dropdown-menu-item
+														| !{translate(child.text)}
 							else
 								+nav-item(class=item.class)
 									if item.url
 										+nav-link(
-											href=item.url,
-											class=item.class,
-											event-tracking="menu-click"
-											event-tracking-mb="true"
-											event-tracking-trigger="click"
-											event-segmentation={ item: item.trackingKey, location: 'top-menu' }
+											href=item.url
+											class=item.class
+											event-tracking='menu-click'
+											event-tracking-mb='true'
+											event-tracking-trigger='click'
+											event-segmentation={item: item.trackingKey, location: 'top-menu'}
 										) !{translate(item.text)}
 									else
 										| !{translate(item.text)}
@@ -136,48 +152,48 @@ nav.navbar.navbar-default.navbar-main.navbar-expand-lg(class={
 						if hasFeature('registration-page')
 							+nav-item.primary
 								+nav-link(
-									href="/register"
-									event-tracking="menu-click"
-									event-tracking-action="clicked"
-									event-tracking-trigger="click"
-									event-tracking-mb="true"
-									event-segmentation={ page: currentUrl, item: 'register', location: 'top-menu' }
+									href='/register'
+									event-tracking='menu-click'
+									event-tracking-action='clicked'
+									event-tracking-trigger='click'
+									event-tracking-mb='true'
+									event-segmentation={page: currentUrl, item: 'register', location: 'top-menu'}
 								) #{translate('sign_up')}
 
 						// login link
 						+nav-item
 							+nav-link(
-								href="/login"
-								event-tracking="menu-click"
-								event-tracking-action="clicked"
-								event-tracking-trigger="click"
-								event-tracking-mb="true"
-								event-segmentation={ page: currentUrl, item: 'login', location: 'top-menu' }
+								href='/login'
+								event-tracking='menu-click'
+								event-tracking-action='clicked'
+								event-tracking-trigger='click'
+								event-tracking-mb='true'
+								event-segmentation={page: currentUrl, item: 'login', location: 'top-menu'}
 							) #{translate('log_in')}
 
 					// projects link and account menu
 					if getSessionUser()
 						+nav-item
-							+nav-link(href="/project") #{translate('Projects')}
+							+nav-link(href='/project') #{translate('Projects')}
 						+nav-item.dropdown
 							button.dropdown-toggle(
-								aria-haspopup="true",
-								aria-expanded="false",
-								data-bs-toggle="dropdown"
-								role="menuitem"
-								event-tracking="menu-expand"
-								event-tracking-mb="true"
-								event-tracking-trigger="click"
-								event-segmentation={"item": "account", "location": "top-menu"}
+								aria-haspopup='true'
+								aria-expanded='false'
+								data-bs-toggle='dropdown'
+								role='menuitem'
+								event-tracking='menu-expand'
+								event-tracking-mb='true'
+								event-tracking-trigger='click'
+								event-segmentation={item: 'account', location: 'top-menu'}
 							)
 								| #{translate('Account')}
 							+dropdown-menu.dropdown-menu-end
 								+dropdown-menu-item
-									div.disabled.dropdown-item #{getSessionUser().email}
+									.disabled.dropdown-item #{getSessionUser().email}
 								+dropdown-menu-divider
-								+dropdown-menu-link-item()(href="/user/settings") #{translate('account_settings')}
+								+dropdown-menu-link-item(href='/user/settings') #{translate('account_settings')}
 								if nav.showSubscriptionLink
-									+dropdown-menu-link-item()(href="/user/subscription") #{translate('subscription')}
+									+dropdown-menu-link-item(href='/user/subscription') #{translate('subscription')}
 								+dropdown-menu-divider
 								+dropdown-menu-item
 									//-
@@ -185,14 +201,10 @@ nav.navbar.navbar-default.navbar-main.navbar-expand-lg(class={
 										this is that if the button is inside the form, screen readers will not count it in the total
 										number of menu items.
 									button.btn-link.text-left.dropdown-menu-button.dropdown-item(
-										role="menuitem",
-										tabindex="-1"
-										form="logOutForm"
+										role='menuitem'
+										tabindex='-1'
+										form='logOutForm'
 									)
 										| #{translate('log_out')}
-									form(
-										method="POST",
-										action="/logout",
-										id="logOutForm"
-									)
-										input(name='_csrf', type='hidden', value=csrfToken)
+									form(method='POST' action='/logout' id='logOutForm')
+										input(name='_csrf' type='hidden' value=csrfToken)

+ 105 - 86
services/web/app/views/layout/navbar-marketing.pug

@@ -1,32 +1,44 @@
-nav.navbar.navbar-default.navbar-main(class={
-	'website-redesign-navbar': isWebsiteRedesign
-})
+nav.navbar.navbar-default.navbar-main(
+	class={
+		'website-redesign-navbar': isWebsiteRedesign,
+	}
+)
 	.container-fluid
 		.navbar-header
-			if (typeof(suppressNavbarRight) == "undefined")
+			if typeof suppressNavbarRight == 'undefined'
 				button.navbar-toggle.collapsed(
-					type="button",
-					data-toggle="collapse",
-					data-target="#navbar-main-collapse"
-					aria-label="Toggle " + translate('navigation')
+					type='button'
+					data-toggle='collapse'
+					data-target='#navbar-main-collapse'
+					aria-label='Toggle ' + translate('navigation')
 				)
-					i.fa.fa-bars(aria-hidden="true")
+					i.fa.fa-bars(aria-hidden='true')
 			- var enableUpgradeButton = projectDashboardReact && usersBestSubscription && (usersBestSubscription.type === 'free' || usersBestSubscription.type === 'standalone-ai-add-on')
-			if (enableUpgradeButton)
+			if enableUpgradeButton
+				//- prettier-ignore
 				a.btn.btn-primary.pull-right.me-2.visible-xs(
-					href="/user/subscription/plans"
-					event-tracking="upgrade-button-click"
-					event-tracking-mb="true"
-					event-tracking-label="upgrade"
-					event-tracking-trigger="click"
-					event-segmentation='{"source": "dashboard-top", "project-dashboard-react": "enabled", "is-dashboard-sidebar-hidden": "true", "is-screen-width-less-than-768px": "true"}'
+					href='/user/subscription/plans'
+					event-tracking='upgrade-button-click'
+					event-tracking-mb='true'
+					event-tracking-label='upgrade'
+					event-tracking-trigger='click'
+					event-segmentation={
+						source: "dashboard-top",
+						"project-dashboard-react": "enabled",
+						"is-dashboard-sidebar-hidden": "true",
+						"is-screen-width-less-than-768px": "true"
+					}
 				) #{translate("upgrade")}
 			if settings.nav.custom_logo
-				a(href='/', aria-label=settings.appName, style='background-image:url("'+settings.nav.custom_logo+'")').navbar-brand
-			else if (nav.title)
-				a(href='/', aria-label=settings.appName).navbar-title #{nav.title}
+				a.navbar-brand(
+					href='/'
+					aria-label=settings.appName
+					style='background-image:url("' + settings.nav.custom_logo + '")'
+				)
+			else if nav.title
+				a.navbar-title(href='/' aria-label=settings.appName) #{nav.title}
 			else
-				a(href='/', aria-label=settings.appName).navbar-brand
+				a.navbar-brand(href='/' aria-label=settings.appName)
 
 		- var canDisplayAdminMenu = hasAdminAccess()
 		- var canDisplayAdminRedirect = canRedirectToAdminDomain()
@@ -34,44 +46,44 @@ nav.navbar.navbar-default.navbar-main(class={
 		- var canDisplaySurveyMenu = hasFeature('saas') && canDisplayAdminMenu
 		- var canDisplayScriptLogMenu = hasFeature('saas') && canDisplayAdminMenu
 
-		if (typeof(suppressNavbarRight) == "undefined")
-			.navbar-collapse.collapse#navbar-main-collapse
+		if typeof suppressNavbarRight == 'undefined'
+			#navbar-main-collapse.navbar-collapse.collapse
 				ul.nav.navbar-nav.navbar-right
-					if (canDisplayAdminMenu || canDisplayAdminRedirect || canDisplaySplitTestMenu)
+					if canDisplayAdminMenu || canDisplayAdminRedirect || canDisplaySplitTestMenu
 						li.dropdown.subdued
 							a.dropdown-toggle(
-								href="#",
-								role="button",
-								aria-haspopup="true",
-								aria-expanded="false",
-								data-toggle="dropdown"
-								event-tracking="menu-expand"
-								event-tracking-mb="true"
-								event-tracking-trigger="click"
-								event-segmentation={"item": "admin", "location": "top-menu"}
+								href='#'
+								role='button'
+								aria-haspopup='true'
+								aria-expanded='false'
+								data-toggle='dropdown'
+								event-tracking='menu-expand'
+								event-tracking-mb='true'
+								event-tracking-trigger='click'
+								event-segmentation={item: 'admin', location: 'top-menu'}
 							)
 								| Admin
 								span.caret
 							ul.dropdown-menu
 								if canDisplayAdminMenu
 									li
-										a(href="/admin") Manage Site
+										a(href='/admin') Manage Site
 									li
-										a(href="/admin/user") Manage Users
+										a(href='/admin/user') Manage Users
 									li
-										a(href="/admin/project") Project URL Lookup
+										a(href='/admin/project') Project URL Lookup
 								if canDisplayAdminRedirect
 									li
 										a(href=settings.adminUrl) Switch to Admin
 								if canDisplaySplitTestMenu
 									li
-										a(href="/admin/split-test") Manage Feature Flags
+										a(href='/admin/split-test') Manage Feature Flags
 								if canDisplaySurveyMenu
 									li
-										a(href="/admin/survey") Manage Surveys
+										a(href='/admin/survey') Manage Surveys
 								if canDisplayScriptLogMenu
 									li
-										a(href="/admin/script-logs") View Script Logs
+										a(href='/admin/script-logs') View Script Logs
 
 					// loop over header_extras
 					each item in nav.header_extras
@@ -90,15 +102,15 @@ nav.navbar.navbar-default.navbar-main(class={
 							if item.dropdown
 								li.dropdown(class=item.class)
 									a.dropdown-toggle(
-										href="#",
-										role="button",
-										aria-haspopup="true",
-										aria-expanded="false",
-										data-toggle="dropdown"
-										event-tracking="menu-expand"
-										event-tracking-mb="true"
-										event-tracking-trigger="click"
-										event-segmentation={"item": item.trackingKey, "location": "top-menu"}
+										href='#'
+										role='button'
+										aria-haspopup='true'
+										aria-expanded='false'
+										data-toggle='dropdown'
+										event-tracking='menu-expand'
+										event-tracking-mb='true'
+										event-tracking-trigger='click'
+										event-segmentation={item: item.trackingKey, location: 'top-menu'}
 									)
 										| !{translate(item.text)}
 										span.caret
@@ -108,18 +120,25 @@ nav.navbar.navbar-default.navbar-main(class={
 												li.divider
 											else if child.isContactUs
 												li
-													a(data-ol-open-contact-form-modal="contact-us" href event-tracking="menu-click" event-tracking-mb="true" event-tracking-trigger="click" event-segmentation={"item": "contact", "location": "top-menu"})
+													a(
+														data-ol-open-contact-form-modal='contact-us'
+														href
+														event-tracking='menu-click'
+														event-tracking-mb='true'
+														event-tracking-trigger='click'
+														event-segmentation={item: 'contact', location: 'top-menu'}
+													)
 														span
 															| #{translate("contact_us")}
 											else
 												li
 													if child.url
 														a(
-															href=child.url,
-															class=child.class,
-															event-tracking="menu-click"
-															event-tracking-mb="true"
-															event-tracking-trigger="click"
+															href=child.url
+															class=child.class
+															event-tracking='menu-click'
+															event-tracking-mb='true'
+															event-tracking-trigger='click'
 															event-segmentation={item: item.trackingKey, location: 'top-menu'}
 														) !{translate(child.text)}
 													else
@@ -128,12 +147,12 @@ nav.navbar.navbar-default.navbar-main(class={
 								li(class=item.class)
 									if item.url
 										a(
-											href=item.url,
-											class=item.class,
-											event-tracking="menu-click"
-											event-tracking-mb="true"
-											event-tracking-trigger="click"
-											event-segmentation={ item: item.trackingKey, location: 'top-menu' }
+											href=item.url
+											class=item.class
+											event-tracking='menu-click'
+											event-tracking-mb='true'
+											event-tracking-trigger='click'
+											event-segmentation={item: item.trackingKey, location: 'top-menu'}
 										) !{translate(item.text)}
 									else
 										| !{translate(item.text)}
@@ -144,54 +163,54 @@ nav.navbar.navbar-default.navbar-main(class={
 						if hasFeature('registration-page')
 							li.primary
 								a(
-									href="/register"
-									event-tracking="menu-click"
-									event-tracking-action="clicked"
-									event-tracking-trigger="click"
-									event-tracking-mb="true"
-									event-segmentation={ page: currentUrl, item: 'register', location: 'top-menu' }
+									href='/register'
+									event-tracking='menu-click'
+									event-tracking-action='clicked'
+									event-tracking-trigger='click'
+									event-tracking-mb='true'
+									event-segmentation={page: currentUrl, item: 'register', location: 'top-menu'}
 								) #{translate('sign_up')}
 
 						// login link
 						li
 							a(
-								href="/login"
-								event-tracking="menu-click"
-								event-tracking-action="clicked"
-								event-tracking-trigger="click"
-								event-tracking-mb="true"
-								event-segmentation={ page: currentUrl, item: 'login', location: 'top-menu' }
+								href='/login'
+								event-tracking='menu-click'
+								event-tracking-action='clicked'
+								event-tracking-trigger='click'
+								event-tracking-mb='true'
+								event-segmentation={page: currentUrl, item: 'login', location: 'top-menu'}
 							) #{translate('log_in')}
 
 					// projects link and account menu
 					if getSessionUser()
 						li
-							a(href="/project") #{translate('Projects')}
+							a(href='/project') #{translate('Projects')}
 						li.dropdown
 							a.dropdown-toggle(
-								href="#",
-								role="button",
-								aria-haspopup="true",
-								aria-expanded="false",
-								data-toggle="dropdown"
-								event-tracking="menu-expand"
-								event-tracking-mb="true"
-								event-tracking-trigger="click"
-								event-segmentation={"item": "account", "location": "top-menu"}
+								href='#'
+								role='button'
+								aria-haspopup='true'
+								aria-expanded='false'
+								data-toggle='dropdown'
+								event-tracking='menu-expand'
+								event-tracking-mb='true'
+								event-tracking-trigger='click'
+								event-segmentation={item: 'account', location: 'top-menu'}
 							)
 								| #{translate('Account')}
 								span.caret
 							ul.dropdown-menu
 								li
-									div.subdued #{getSessionUser().email}
+									.subdued #{getSessionUser().email}
 								li.divider.hidden-xs.hidden-sm
 								li
-									a(href="/user/settings") #{translate('account_settings')}
+									a(href='/user/settings') #{translate('account_settings')}
 								if nav.showSubscriptionLink
 									li
-										a(href="/user/subscription") #{translate('subscription')}
+										a(href='/user/subscription') #{translate('subscription')}
 								li.divider.hidden-xs.hidden-sm
 								li
-									form(method="POST" action="/logout")
-										input(name='_csrf', type='hidden', value=csrfToken)
+									form(method='POST' action='/logout')
+										input(name='_csrf' type='hidden' value=csrfToken)
 										button.btn-link.text-left.dropdown-menu-button #{translate('log_out')}

+ 99 - 83
services/web/app/views/layout/navbar-website-redesign.pug

@@ -1,30 +1,39 @@
 nav.navbar.navbar-default.navbar-main.website-redesign-navbar
 	.container-fluid
 		.navbar-header
-			if (typeof(suppressNavbarRight) == "undefined")
+			if typeof suppressNavbarRight == 'undefined'
 				button.navbar-toggle.collapsed(
-					type="button",
-					data-toggle="collapse",
-					data-target="#navbar-main-collapse"
-					aria-label="Toggle " + translate('navigation')
+					type='button'
+					data-toggle='collapse'
+					data-target='#navbar-main-collapse'
+					aria-label='Toggle ' + translate('navigation')
 				)
-					i.fa.fa-bars(aria-hidden="true")
+					i.fa.fa-bars(aria-hidden='true')
 			- var enableUpgradeButton = projectDashboardReact && usersBestSubscription && (usersBestSubscription.type === 'free' || usersBestSubscription.type === 'standalone-ai-add-on')
-			if (enableUpgradeButton)
+			if enableUpgradeButton
 				a.btn.btn-primary.pull-right.me-2.visible-xs(
-					href="/user/subscription/plans"
-					event-tracking="upgrade-button-click"
-					event-tracking-mb="true"
-					event-tracking-label="upgrade"
-					event-tracking-trigger="click"
-					event-segmentation='{"source": "dashboard-top", "project-dashboard-react": "enabled", "is-dashboard-sidebar-hidden": "true", "is-screen-width-less-than-768px": "true"}'
+					href='/user/subscription/plans'
+					event-tracking='upgrade-button-click'
+					event-tracking-mb='true'
+					event-tracking-label='upgrade'
+					event-tracking-trigger='click'
+					event-segmentation={
+						source: 'dashboard-top',
+						projectDashboardReact: 'enabled',
+						isDashboardSidebarHidden: 'true',
+						isScreenWidthLessThan768px: 'true',
+					}
 				) #{translate("upgrade")}
 			if settings.nav.custom_logo
-				a(href='/', aria-label=settings.appName, style='background-image:url("'+settings.nav.custom_logo+'")').navbar-brand
-			else if (nav.title)
-				a(href='/', aria-label=settings.appName).navbar-title #{nav.title}
+				a.navbar-brand(
+					href='/'
+					aria-label=settings.appName
+					style='background-image:url("' + settings.nav.custom_logo + '")'
+				)
+			else if nav.title
+				a.navbar-title(href='/' aria-label=settings.appName) #{nav.title}
 			else
-				a(href='/', aria-label=settings.appName).navbar-brand
+				a.navbar-brand(href='/' aria-label=settings.appName)
 
 		- var canDisplayAdminMenu = hasAdminAccess()
 		- var canDisplayAdminRedirect = canRedirectToAdminDomain()
@@ -32,44 +41,44 @@ nav.navbar.navbar-default.navbar-main.website-redesign-navbar
 		- var canDisplaySurveyMenu = hasFeature('saas') && canDisplayAdminMenu
 		- var canDisplayScriptLogMenu = hasFeature('saas') && canDisplayAdminMenu
 
-		if (typeof(suppressNavbarRight) == "undefined")
-			.navbar-collapse.collapse#navbar-main-collapse
+		if typeof suppressNavbarRight == 'undefined'
+			#navbar-main-collapse.navbar-collapse.collapse
 				ul.nav.navbar-nav.navbar-right
-					if (canDisplayAdminMenu || canDisplayAdminRedirect || canDisplaySplitTestMenu)
+					if canDisplayAdminMenu || canDisplayAdminRedirect || canDisplaySplitTestMenu
 						li.dropdown.subdued
 							a.dropdown-toggle(
-								href="#",
-								role="button",
-								aria-haspopup="true",
-								aria-expanded="false",
-								data-toggle="dropdown"
-								event-tracking="menu-expand"
-								event-tracking-mb="true"
-								event-tracking-trigger="click"
-								event-segmentation={"item": "admin", "location": "top-menu"}
+								href='#'
+								role='button'
+								aria-haspopup='true'
+								aria-expanded='false'
+								data-toggle='dropdown'
+								event-tracking='menu-expand'
+								event-tracking-mb='true'
+								event-tracking-trigger='click'
+								event-segmentation={item: 'admin', location: 'top-menu'}
 							)
 								| Admin
 								span.caret
 							ul.dropdown-menu
 								if canDisplayAdminMenu
 									li
-										a(href="/admin") Manage Site
+										a(href='/admin') Manage Site
 									li
-										a(href="/admin/user") Manage Users
+										a(href='/admin/user') Manage Users
 									li
-										a(href="/admin/project") Project URL Lookup
+										a(href='/admin/project') Project URL Lookup
 								if canDisplayAdminRedirect
 									li
 										a(href=settings.adminUrl) Switch to Admin
 								if canDisplaySplitTestMenu
 									li
-										a(href="/admin/split-test") Manage Feature Flags
+										a(href='/admin/split-test') Manage Feature Flags
 								if canDisplaySurveyMenu
 									li
-										a(href="/admin/survey") Manage Surveys
+										a(href='/admin/survey') Manage Surveys
 								if canDisplayScriptLogMenu
 									li
-										a(href="/admin/script-logs") View Script Logs
+										a(href='/admin/script-logs') View Script Logs
 
 					// loop over header_extras
 					each item in nav.header_extras
@@ -88,15 +97,15 @@ nav.navbar.navbar-default.navbar-main.website-redesign-navbar
 							if item.dropdown
 								li.dropdown(class=item.class)
 									a.dropdown-toggle(
-										href="#",
-										role="button",
-										aria-haspopup="true",
-										aria-expanded="false",
-										data-toggle="dropdown"
-										event-tracking="menu-expand"
-										event-tracking-mb="true"
-										event-tracking-trigger="click"
-										event-segmentation={"item": item.trackingKey, "location": "top-menu"}
+										href='#'
+										role='button'
+										aria-haspopup='true'
+										aria-expanded='false'
+										data-toggle='dropdown'
+										event-tracking='menu-expand'
+										event-tracking-mb='true'
+										event-tracking-trigger='click'
+										event-segmentation={item: item.trackingKey, location: 'top-menu'}
 									)
 										| !{translate(item.text)}
 										span.caret
@@ -106,18 +115,25 @@ nav.navbar.navbar-default.navbar-main.website-redesign-navbar
 												li.divider
 											else if child.isContactUs
 												li
-													a(data-ol-open-contact-form-modal="contact-us" href event-tracking="menu-click" event-tracking-mb="true" event-tracking-trigger="click" event-segmentation={"item": "contact", "location": "top-menu"})
+													a(
+														data-ol-open-contact-form-modal='contact-us'
+														href
+														event-tracking='menu-click'
+														event-tracking-mb='true'
+														event-tracking-trigger='click'
+														event-segmentation={item: 'contact', location: 'top-menu'}
+													)
 														span
 															| #{translate("contact_us")}
 											else
 												li
 													if child.url
 														a(
-															href=child.url,
-															class=child.class,
-															event-tracking="menu-click"
-															event-tracking-mb="true"
-															event-tracking-trigger="click"
+															href=child.url
+															class=child.class
+															event-tracking='menu-click'
+															event-tracking-mb='true'
+															event-tracking-trigger='click'
 															event-segmentation={item: child.trackingKey, location: 'top-menu'}
 														) !{translate(child.text)}
 													else
@@ -126,12 +142,12 @@ nav.navbar.navbar-default.navbar-main.website-redesign-navbar
 								li(class=item.class)
 									if item.url
 										a(
-											href=item.url,
-											class=item.class,
-											event-tracking="menu-click"
-											event-tracking-mb="true"
-											event-tracking-trigger="click"
-											event-segmentation={ item: item.trackingKey, location: 'top-menu' }
+											href=item.url
+											class=item.class
+											event-tracking='menu-click'
+											event-tracking-mb='true'
+											event-tracking-trigger='click'
+											event-segmentation={item: item.trackingKey, location: 'top-menu'}
 										) !{translate(item.text)}
 									else
 										| !{translate(item.text)}
@@ -142,54 +158,54 @@ nav.navbar.navbar-default.navbar-main.website-redesign-navbar
 						if hasFeature('registration-page')
 							li.primary
 								a(
-									href="/register"
-									event-tracking="menu-click"
-									event-tracking-action="clicked"
-									event-tracking-trigger="click"
-									event-tracking-mb="true"
-									event-segmentation={ page: currentUrl, item: 'register', location: 'top-menu' }
+									href='/register'
+									event-tracking='menu-click'
+									event-tracking-action='clicked'
+									event-tracking-trigger='click'
+									event-tracking-mb='true'
+									event-segmentation={page: currentUrl, item: 'register', location: 'top-menu'}
 								) #{translate('sign_up')}
 
 						// login link
 						li.secondary
 							a(
-								href="/login"
-								event-tracking="menu-click"
-								event-tracking-action="clicked"
-								event-tracking-trigger="click"
-								event-tracking-mb="true"
-								event-segmentation={ page: currentUrl, item: 'login', location: 'top-menu' }
+								href='/login'
+								event-tracking='menu-click'
+								event-tracking-action='clicked'
+								event-tracking-trigger='click'
+								event-tracking-mb='true'
+								event-segmentation={page: currentUrl, item: 'login', location: 'top-menu'}
 							) #{translate('log_in')}
 
 					// projects link and account menu
 					if getSessionUser()
 						li.secondary
-							a(href="/project") #{translate('Projects')}
+							a(href='/project') #{translate('Projects')}
 						li.secondary.dropdown
 							a.dropdown-toggle(
-								href="#",
-								role="button",
-								aria-haspopup="true",
-								aria-expanded="false",
-								data-toggle="dropdown"
-								event-tracking="menu-expand"
-								event-tracking-mb="true"
-								event-tracking-trigger="click"
-								event-segmentation={"item": "account", "location": "top-menu"}
+								href='#'
+								role='button'
+								aria-haspopup='true'
+								aria-expanded='false'
+								data-toggle='dropdown'
+								event-tracking='menu-expand'
+								event-tracking-mb='true'
+								event-tracking-trigger='click'
+								event-segmentation={item: 'account', location: 'top-menu'}
 							)
 								| #{translate('Account')}
 								span.caret
 							ul.dropdown-menu
 								li
-									div.subdued #{getSessionUser().email}
+									.subdued #{getSessionUser().email}
 								li.divider.hidden-xs.hidden-sm
 								li
-									a(href="/user/settings") #{translate('account_settings')}
+									a(href='/user/settings') #{translate('account_settings')}
 								if nav.showSubscriptionLink
 									li
-										a(href="/user/subscription") #{translate('subscription')}
+										a(href='/user/subscription') #{translate('subscription')}
 								li.divider.hidden-xs.hidden-sm
 								li
-									form(method="POST" action="/logout")
-										input(name='_csrf', type='hidden', value=csrfToken)
+									form(method='POST' action='/logout')
+										input(name='_csrf' type='hidden' value=csrfToken)
 										button.btn-link.text-left.dropdown-menu-button #{translate('log_out')}

+ 2 - 2
services/web/app/views/layout/thin-footer-bootstrap-5.pug

@@ -25,7 +25,7 @@ footer.site-footer
 				each item in nav.left_footer
 					li
 						if item.url
-							a(href=item.url, class=item.class) !{translate(item.text)}
+							a(href=item.url class=item.class) !{translate(item.text)}
 						else
 							| !{item.text}
 
@@ -33,6 +33,6 @@ footer.site-footer
 				each item in nav.right_footer
 					li
 						if item.url
-							a(href=item.url, class=item.class, aria-label=item.label) !{item.text}
+							a(href=item.url class=item.class aria-label=item.label) !{item.text}
 						else
 							| !{item.text}

+ 2 - 2
services/web/app/views/layout/thin-footer.pug

@@ -27,7 +27,7 @@ footer.site-footer
 				each item in nav.left_footer
 					li
 						if item.url
-							a(href=item.url, class=item.class) !{translate(item.text)}
+							a(href=item.url class=item.class) !{translate(item.text)}
 						else
 							| !{item.text}
 
@@ -35,6 +35,6 @@ footer.site-footer
 				each item in nav.right_footer
 					li
 						if item.url
-							a(href=item.url, class=item.class, aria-label=item.label) !{item.text}
+							a(href=item.url class=item.class aria-label=item.label) !{item.text}
 						else
 							| !{item.text}

+ 29 - 29
services/web/app/views/project/editor/new_from_template.pug

@@ -1,36 +1,36 @@
 extends ../../layout-marketing
 
 block vars
-  - var suppressFooter = true
-  - var suppressCookieBanner = true
-  - var suppressSkipToContent = true
+	- var suppressFooter = true
+	- var suppressCookieBanner = true
+	- var suppressSkipToContent = true
 
 block content
-  .editor.full-size
-    .loading-screen()
-      .loading-screen-brand-container
-        .loading-screen-brand(
-          style="height: 20%;"
-        )
+	.editor.full-size
+		.loading-screen()
+			.loading-screen-brand-container
+				.loading-screen-brand(
+					style="height: 20%;"
+				)
 
-      h3.loading-screen-label() #{translate("Opening template")}
-        span.loading-screen-ellip .
-        span.loading-screen-ellip .
-        span.loading-screen-ellip .
+			h3.loading-screen-label() #{translate("Opening template")}
+				span.loading-screen-ellip .
+				span.loading-screen-ellip .
+				span.loading-screen-ellip .
 
-  form(
-    data-ol-regular-form
-    data-ol-auto-submit
-    method='POST'
-    action='/project/new/template/'
-  )
-    input(type="hidden", name="_csrf", value=csrfToken)
-    input(type="hidden" name="templateId" value=templateId)
-    input(type="hidden" name="templateVersionId" value=templateVersionId)
-    input(type="hidden" name="templateName" value=name)
-    input(type="hidden" name="compiler" value=compiler)
-    input(type="hidden" name="imageName" value=imageName)
-    input(type="hidden" name="mainFile" value=mainFile)
-    if brandVariationId
-      input(type="hidden" name="brandVariationId" value=brandVariationId)
-    input(hidden type="submit")
+	form(
+		data-ol-regular-form
+		data-ol-auto-submit
+		method='POST'
+		action='/project/new/template/'
+	)
+		input(type="hidden", name="_csrf", value=csrfToken)
+		input(type="hidden" name="templateId" value=templateId)
+		input(type="hidden" name="templateVersionId" value=templateVersionId)
+		input(type="hidden" name="templateName" value=name)
+		input(type="hidden" name="compiler" value=compiler)
+		input(type="hidden" name="imageName" value=imageName)
+		input(type="hidden" name="mainFile" value=mainFile)
+		if brandVariationId
+			input(type="hidden" name="brandVariationId" value=brandVariationId)
+		input(hidden type="submit")

+ 1 - 1
services/web/app/views/project/ide-react-detached.pug

@@ -11,7 +11,7 @@ block vars
 	- metadata.robotsNoindexNofollow = true
 
 block content
-	#pdf-preview-detached-root()
+	#pdf-preview-detached-root
 
 block append meta
 	include editor/_meta

+ 7 - 2
services/web/app/views/project/ide-react.pug

@@ -15,7 +15,7 @@ block content
 	main#ide-root
 		.loading-screen
 			.loading-screen-brand-container
-				.loading-screen-brand(style="height: 20%;")
+				.loading-screen-brand(style='height: 20%')
 			h3.loading-screen-label #{translate("loading")}
 				span.loading-screen-ellip .
 				span.loading-screen-ellip .
@@ -25,4 +25,9 @@ block append meta
 	include editor/_meta
 
 block prepend foot-scripts
-	script(type="text/javascript", nonce=scriptNonce, src=(wsUrl || '/socket.io') + '/socket.io.js', defer=deferScripts)
+	script(
+		type='text/javascript'
+		nonce=scriptNonce
+		src=(wsUrl || '/socket.io') + '/socket.io.js'
+		defer=deferScripts
+	)

+ 3 - 3
services/web/app/views/project/invite/not-valid.pug

@@ -1,18 +1,18 @@
 extends ../../layout-marketing
 
 block content
-	main.content.content-alt#main-content
+	main#main-content.content.content-alt
 		.container
 			.row
 				.col-md-8.col-md-offset-2.offset-md-2
 					.card.project-invite-invalid
 						.card-body
 							.page-header.text-center
-								h1  #{translate("invite_not_valid")}
+								h1 #{translate("invite_not_valid")}
 							.row.text-center
 								.col-12.col-md-12
 									p
 										| #{translate("invite_not_valid_description")}.
 							.row.text-center.actions
 								.col-12.col-md-12
-									a.btn.btn-secondary-info.btn-secondary(href="/project") #{translate("back_to_your_projects")}
+									a.btn.btn-secondary-info.btn-secondary(href='/project') #{translate("back_to_your_projects")}

+ 8 - 8
services/web/app/views/project/invite/show.pug

@@ -1,7 +1,7 @@
 extends ../../layout-marketing
 
 block content
-	main.content.content-alt#main-content
+	main#main-content.content.content-alt
 		.container
 			.row
 				.col-12.col-md-8.col-md-offset-2.offset-md-2
@@ -20,16 +20,16 @@ block content
 								.col-12.col-md-12
 									form.form(
 										data-ol-regular-form
-										method="POST",
-										action="/project/"+invite.projectId+"/invite/token/"+token+"/accept"
+										method='POST'
+										action='/project/' + invite.projectId + '/invite/token/' + token + '/accept'
 									)
-										input(name='_csrf', type='hidden', value=csrfToken)
-										input(name='token', type='hidden', value=token)
+										input(name='_csrf' type='hidden' value=csrfToken)
+										input(name='token' type='hidden' value=token)
 										.form-group.text-center
 											button.btn.btn-lg.btn-primary(
-												type="submit"
+												type='submit'
 												data-ol-disabled-inflight
 											)
-												span(data-ol-inflight="idle") #{translate("join_project")}
-												span(hidden data-ol-inflight="pending") #{translate("joining")}…
+												span(data-ol-inflight='idle') #{translate("join_project")}
+												span(hidden data-ol-inflight='pending') #{translate("joining")}…
 										.form-group.text-center

+ 77 - 29
services/web/app/views/project/list-react.pug

@@ -9,35 +9,83 @@ block vars
 	- const suppressFooter = true
 
 block append meta
-	meta(name="ol-usersBestSubscription" data-type="json" content=usersBestSubscription)
-	meta(name="ol-notifications" data-type="json" content=notifications)
-	meta(name="ol-notificationsInstitution" data-type="json" content=notificationsInstitution)
-	meta(name="ol-userEmails" data-type="json" content=userEmails)
-	meta(name="ol-allInReconfirmNotificationPeriods" data-type="json" content=allInReconfirmNotificationPeriods)
-	meta(name="ol-user" data-type="json" content=user)
-	meta(name="ol-userAffiliations" data-type="json" content=userAffiliations)
-	meta(name="ol-reconfirmedViaSAML" content=reconfirmedViaSAML)
-	meta(name="ol-survey" data-type="json" content=survey)
-	meta(name="ol-tags" data-type="json" content=tags)
-	meta(name="ol-portalTemplates" data-type="json" content=portalTemplates)
-	meta(name="ol-prefetchedProjectsBlob" data-type="json" content=prefetchedProjectsBlob)
-	if (suggestedLanguageSubdomainConfig)
-		meta(name="ol-suggestedLanguage" data-type="json" content=Object.assign(suggestedLanguageSubdomainConfig, {
-			lngName: translate(suggestedLanguageSubdomainConfig.lngCode),
-			imgUrl: buildImgPath("flags/24/" + suggestedLanguageSubdomainConfig.lngCode + ".png")
-		}))
-	meta(name="ol-currentUrl" data-type="string" content=currentUrl)
-	meta(name="ol-showGroupsAndEnterpriseBanner" data-type="boolean" content=showGroupsAndEnterpriseBanner)
-	meta(name="ol-groupsAndEnterpriseBannerVariant" data-type="string" content=groupsAndEnterpriseBannerVariant)
-	meta(name="ol-showInrGeoBanner" data-type="boolean" content=showInrGeoBanner)
-	meta(name="ol-showBrlGeoBanner" data-type="boolean" content=showBrlGeoBanner)
-	meta(name="ol-recommendedCurrency" data-type="string" content=recommendedCurrency)
-	meta(name="ol-showLATAMBanner" data-type="boolean" content=showLATAMBanner)
-	meta(name="ol-groupSubscriptionsPendingEnrollment" data-type="json" content=groupSubscriptionsPendingEnrollment)
-	meta(name="ol-hasIndividualPaidSubscription" data-type="boolean" content=hasIndividualPaidSubscription)
-	meta(name="ol-groupSsoSetupSuccess" data-type="boolean" content=groupSsoSetupSuccess)
-	meta(name="ol-showUSGovBanner" data-type="boolean" content=showUSGovBanner)
-	meta(name="ol-usGovBannerVariant" data-type="string" content=usGovBannerVariant)
+	meta(
+		name='ol-usersBestSubscription'
+		data-type='json'
+		content=usersBestSubscription
+	)
+	meta(name='ol-notifications' data-type='json' content=notifications)
+	meta(
+		name='ol-notificationsInstitution'
+		data-type='json'
+		content=notificationsInstitution
+	)
+	meta(name='ol-userEmails' data-type='json' content=userEmails)
+	meta(
+		name='ol-allInReconfirmNotificationPeriods'
+		data-type='json'
+		content=allInReconfirmNotificationPeriods
+	)
+	meta(name='ol-user' data-type='json' content=user)
+	meta(name='ol-userAffiliations' data-type='json' content=userAffiliations)
+	meta(name='ol-reconfirmedViaSAML' content=reconfirmedViaSAML)
+	meta(name='ol-survey' data-type='json' content=survey)
+	meta(name='ol-tags' data-type='json' content=tags)
+	meta(name='ol-portalTemplates' data-type='json' content=portalTemplates)
+	meta(
+		name='ol-prefetchedProjectsBlob'
+		data-type='json'
+		content=prefetchedProjectsBlob
+	)
+	if suggestedLanguageSubdomainConfig
+		meta(
+			name='ol-suggestedLanguage'
+			data-type='json'
+			content=Object.assign(suggestedLanguageSubdomainConfig, {
+				lngName: translate(suggestedLanguageSubdomainConfig.lngCode),
+				imgUrl: buildImgPath('flags/24/' + suggestedLanguageSubdomainConfig.lngCode + '.png'),
+			})
+		)
+	meta(name='ol-currentUrl' data-type='string' content=currentUrl)
+	meta(
+		name='ol-showGroupsAndEnterpriseBanner'
+		data-type='boolean'
+		content=showGroupsAndEnterpriseBanner
+	)
+	meta(
+		name='ol-groupsAndEnterpriseBannerVariant'
+		data-type='string'
+		content=groupsAndEnterpriseBannerVariant
+	)
+	meta(name='ol-showInrGeoBanner' data-type='boolean' content=showInrGeoBanner)
+	meta(name='ol-showBrlGeoBanner' data-type='boolean' content=showBrlGeoBanner)
+	meta(
+		name='ol-recommendedCurrency'
+		data-type='string'
+		content=recommendedCurrency
+	)
+	meta(name='ol-showLATAMBanner' data-type='boolean' content=showLATAMBanner)
+	meta(
+		name='ol-groupSubscriptionsPendingEnrollment'
+		data-type='json'
+		content=groupSubscriptionsPendingEnrollment
+	)
+	meta(
+		name='ol-hasIndividualPaidSubscription'
+		data-type='boolean'
+		content=hasIndividualPaidSubscription
+	)
+	meta(
+		name='ol-groupSsoSetupSuccess'
+		data-type='boolean'
+		content=groupSsoSetupSuccess
+	)
+	meta(name='ol-showUSGovBanner' data-type='boolean' content=showUSGovBanner)
+	meta(
+		name='ol-usGovBannerVariant'
+		data-type='string'
+		content=usGovBannerVariant
+	)
 
 block content
 	#project-list-root

+ 5 - 5
services/web/app/views/project/token/access-react.pug

@@ -7,11 +7,11 @@ block vars
 	- var suppressFooter = true
 	- var suppressCookieBanner = true
 	- var suppressSkipToContent = true
-		
+
 block append meta
-	meta(name="ol-postUrl" data-type="string" content=postUrl)
-	meta(name="ol-user" data-type="json" content=user)
+	meta(name='ol-postUrl' data-type='string' content=postUrl)
+	meta(name='ol-user' data-type='json' content=user)
 
 block content
-	.content.content-alt#main-content
-		div#token-access-page
+	#main-content.content.content-alt
+		#token-access-page

+ 4 - 4
services/web/app/views/project/token/sharing-updates.pug

@@ -9,9 +9,9 @@ block vars
 	- var suppressSkipToContent = true
 
 block append meta
-	meta(name="ol-user" data-type="json" content=user)
-	meta(name="ol-project_id" data-type="string" content=projectId)
+	meta(name='ol-user' data-type='json' content=user)
+	meta(name='ol-project_id' data-type='string' content=projectId)
 
 block content
-	.content.content-alt#main-content
-		div#sharing-updates-page
+	#main-content.content.content-alt
+		#sharing-updates-page

+ 11 - 7
services/web/app/views/subscriptions/add-seats.pug

@@ -4,13 +4,17 @@ block entrypointVar
 	- entrypoint = 'pages/user/subscription/group-management/add-seats'
 
 block append meta
-	meta(name="ol-user" data-type="json" content=user)
-	meta(name="ol-groupName", data-type="string", content=groupName)
-	meta(name="ol-subscriptionId", data-type="string", content=subscriptionId)
-	meta(name="ol-totalLicenses", data-type="number", content=totalLicenses)
-	meta(name="ol-isProfessional", data-type="boolean", content=isProfessional)
-	meta(name="ol-isCollectionMethodManual", data-type="boolean", content=isCollectionMethodManual)
+	meta(name='ol-user' data-type='json' content=user)
+	meta(name='ol-groupName' data-type='string' content=groupName)
+	meta(name='ol-subscriptionId' data-type='string' content=subscriptionId)
+	meta(name='ol-totalLicenses' data-type='number' content=totalLicenses)
+	meta(name='ol-isProfessional' data-type='boolean' content=isProfessional)
+	meta(
+		name='ol-isCollectionMethodManual'
+		data-type='boolean'
+		content=isCollectionMethodManual
+	)
 
 block content
-	main.content.content-alt#main-content
+	main#main-content.content.content-alt
 		#add-seats-root

+ 2 - 2
services/web/app/views/subscriptions/canceled-subscription-react.pug

@@ -4,7 +4,7 @@ block entrypointVar
 	- entrypoint = 'pages/user/subscription/canceled-subscription'
 
 block append meta
-	meta(name="ol-user" data-type="json" content=user)
+	meta(name='ol-user' data-type='json' content=user)
 
 block content
-	main.content.content-alt#subscription-canceled-root
+	main#subscription-canceled-root.content.content-alt

+ 82 - 27
services/web/app/views/subscriptions/dashboard-react.pug

@@ -1,36 +1,91 @@
 extends ../layout-react
 
 block entrypointVar
-  - entrypoint = 'pages/user/subscription/dashboard'
+	- entrypoint = 'pages/user/subscription/dashboard'
 
 block head-scripts
-  script(type="text/javascript", nonce=scriptNonce, src="https://js.recurly.com/v4/recurly.js")
+	script(
+		type='text/javascript'
+		nonce=scriptNonce
+		src='https://js.recurly.com/v4/recurly.js'
+	)
 
 block append meta
-  meta(name="ol-subscription" data-type="json" content=personalSubscription)
-  meta(name="ol-userCanExtendTrial" data-type="boolean" content=userCanExtendTrial)
-  meta(name="ol-managedGroupSubscriptions" data-type="json" content=managedGroupSubscriptions)
-  meta(name="ol-memberGroupSubscriptions" data-type="json" content=memberGroupSubscriptions)
-  meta(name="ol-managedInstitutions" data-type="json" content=managedInstitutions)
-  meta(name="ol-managedPublishers" data-type="json" content=managedPublishers)
-  meta(name="ol-planCodesChangingAtTermEnd" data-type="json", content=planCodesChangingAtTermEnd)
-  meta(name="ol-currentInstitutionsWithLicence" data-type="json" content=currentInstitutionsWithLicence)
-  meta(name="ol-hasSubscription" data-type="boolean" content=hasSubscription)
-  meta(name="ol-fromPlansPage" data-type="boolean" content=fromPlansPage)
-  meta(name="ol-plans" data-type="json" content=plans)
-  meta(name="ol-groupSettingsAdvertisedFor" data-type="json" content=groupSettingsAdvertisedFor)
-  meta(name="ol-canUseFlexibleLicensing" data-type="boolean", content=canUseFlexibleLicensing)
-  meta(name="ol-showGroupDiscount" data-type="boolean", content=showGroupDiscount)
-  meta(name="ol-groupSettingsEnabledFor" data-type="json" content=groupSettingsEnabledFor)
-  meta(name="ol-hasAiAssistViaWritefull" data-type="boolean", content=hasAiAssistViaWritefull)
-  meta(name="ol-aiAssistViaWritefullSource" data-type="string", content=aiAssistViaWritefullSource)
-  meta(name="ol-user" data-type="json" content=user)
-  if (personalSubscription && personalSubscription.payment)
-    meta(name="ol-recurlyApiKey" content=settings.apis.recurly.publicKey)
-    meta(name="ol-stripeUKApiKey" content=settings.apis.stripeUK.publishableKey)
-    meta(name="ol-recommendedCurrency" content=personalSubscription.payment.currency)
-    meta(name="ol-groupPlans" data-type="json" content=groupPlans)
+	meta(name='ol-subscription' data-type='json' content=personalSubscription)
+	meta(
+		name='ol-userCanExtendTrial'
+		data-type='boolean'
+		content=userCanExtendTrial
+	)
+	meta(
+		name='ol-managedGroupSubscriptions'
+		data-type='json'
+		content=managedGroupSubscriptions
+	)
+	meta(
+		name='ol-memberGroupSubscriptions'
+		data-type='json'
+		content=memberGroupSubscriptions
+	)
+	meta(
+		name='ol-managedInstitutions'
+		data-type='json'
+		content=managedInstitutions
+	)
+	meta(name='ol-managedPublishers' data-type='json' content=managedPublishers)
+	meta(
+		name='ol-planCodesChangingAtTermEnd'
+		data-type='json'
+		content=planCodesChangingAtTermEnd
+	)
+	meta(
+		name='ol-currentInstitutionsWithLicence'
+		data-type='json'
+		content=currentInstitutionsWithLicence
+	)
+	meta(name='ol-hasSubscription' data-type='boolean' content=hasSubscription)
+	meta(name='ol-fromPlansPage' data-type='boolean' content=fromPlansPage)
+	meta(name='ol-plans' data-type='json' content=plans)
+	meta(
+		name='ol-groupSettingsAdvertisedFor'
+		data-type='json'
+		content=groupSettingsAdvertisedFor
+	)
+	meta(
+		name='ol-canUseFlexibleLicensing'
+		data-type='boolean'
+		content=canUseFlexibleLicensing
+	)
+	meta(
+		name='ol-showGroupDiscount'
+		data-type='boolean'
+		content=showGroupDiscount
+	)
+	meta(
+		name='ol-groupSettingsEnabledFor'
+		data-type='json'
+		content=groupSettingsEnabledFor
+	)
+	meta(
+		name='ol-hasAiAssistViaWritefull'
+		data-type='boolean'
+		content=hasAiAssistViaWritefull
+	)
+	meta(
+		name='ol-aiAssistViaWritefullSource'
+		data-type='string'
+		content=aiAssistViaWritefullSource
+	)
+	meta(name='ol-user' data-type='json' content=user)
+	if personalSubscription && personalSubscription.payment
+		meta(name='ol-recurlyApiKey' content=settings.apis.recurly.publicKey)
+		meta(name='ol-stripeUKApiKey' content=settings.apis.stripeUK.publishableKey)
+		meta(
+			name='ol-recommendedCurrency'
+			content=personalSubscription.payment.currency
+		)
+		meta(name='ol-groupPlans' data-type='json' content=groupPlans)
 
 block content
-  main.content.content-alt#main-content
-    #subscription-dashboard-root
+	main#main-content.content.content-alt
+		#subscription-dashboard-root

+ 3 - 3
services/web/app/views/subscriptions/manually-collected-subscription.pug

@@ -4,8 +4,8 @@ block entrypointVar
 	- entrypoint = 'pages/user/subscription/group-management/manually-collected-subscription'
 
 block append meta
-	meta(name="ol-user" data-type="json" content=user)
-	meta(name="ol-groupName", data-type="string", content=groupName)
+	meta(name='ol-user' data-type='json' content=user)
+	meta(name='ol-groupName' data-type='string' content=groupName)
 
 block content
-	main.content.content-alt#manually-collected-subscription-root
+	main#manually-collected-subscription-root.content.content-alt

+ 3 - 3
services/web/app/views/subscriptions/missing-billing-information.pug

@@ -4,8 +4,8 @@ block entrypointVar
 	- entrypoint = 'pages/user/subscription/group-management/missing-billing-information'
 
 block append meta
-	meta(name="ol-user" data-type="json" content=user)
-	meta(name="ol-groupName", data-type="string", content=groupName)
+	meta(name='ol-user' data-type='json' content=user)
+	meta(name='ol-groupName' data-type='string' content=groupName)
 
 block content
-	main.content.content-alt#missing-billing-information-root
+	main#missing-billing-information-root.content.content-alt

+ 29 - 41
services/web/app/views/subscriptions/plans/_faq_new.pug

@@ -9,7 +9,7 @@ include ../../_mixins/material_symbol
 	.row.row-spaced-extra-large
 		.col-md-12.faq-heading-container
 			h2
-				+eyebrow(translate("frequently_asked_questions"))
+				+eyebrow(translate('frequently_asked_questions'))
 				| #{translate("your_questions_answered")}
 
 	.row
@@ -18,74 +18,62 @@ include ../../_mixins/material_symbol
 				class={
 					'plans-faq-tabs': bootstrapVersion === 5,
 					'ol-tabs': bootstrapVersion === 5,
-					'ol-tabs-scrollable': bootstrapVersion === 3
+					'ol-tabs-scrollable': bootstrapVersion === 3,
 				}
 			)
 				.nav-tabs-container
-					ul.nav.nav-tabs(role="tablist")
+					ul.nav.nav-tabs(role='tablist')
 						//- In the bs5 version of plans page, the `active` class need to be added to the `a` tag instead of the parent `li` tag
 						//- If the `plans-page-bs5` split test has been completed, remove the `active` class from the `li` tag since we're not using it anymore
 						//- If the `plans-page-bs5` split test has been completed, remove the `data-toggle` because it is not needed anymore (bs5 uses `data-bs-toggle`)
-						li(
-							role="presentation"
-							class={
-								active: bootstrapVersion === 3
-							}
-						)
+						li(role='presentation' class={
+							active: bootstrapVersion === 3,
+						})
 							a(
-								role="tab"
-								data-toggle="tab"
-								data-bs-toggle="tab"
+								role='tab'
+								data-toggle='tab'
+								data-bs-toggle='tab'
 								href='#' + managingYourSubscription
 								aria-controls=managingYourSubscription
 								class={
-									active: bootstrapVersion === 5
+									active: bootstrapVersion === 5,
 								}
 							)
 								| #{translate('managing_your_subscription')}
-						li(role="presentation")
+						li(role='presentation')
 							a(
-								role="tab"
-								data-toggle="tab"
-								data-bs-toggle="tab"
+								role='tab'
+								data-toggle='tab'
+								data-bs-toggle='tab'
 								href='#' + overleafIndividualPlans
 								aria-controls=overleafIndividualPlans
 							)
 								| #{translate('overleaf_individual_plans')}
-						li(role="presentation")
+						li(role='presentation')
 							a(
-								role="tab"
-								data-toggle="tab"
-								data-bs-toggle="tab"
+								role='tab'
+								data-toggle='tab'
+								data-bs-toggle='tab'
 								href='#' + overleafGroupPlans
 								aria-controls=overleafGroupPlans
 							)
 								| #{translate('overleaf_group_plans')}
 
 				.tab-content
-					.tab-pane.active(
-						role="tabpanel"
-						id=managingYourSubscription
-					)
-						+managingYourSubscription()
-					.tab-pane(
-						role="tabpanel"
-						id=overleafIndividualPlans
-					)
-						+overleafIndividualPlans()
-					.tab-pane(
-						role="tabpanel"
-						id=overleafGroupPlans
-					)
-						+overleafGroupPlans()
+					.tab-pane.active(role='tabpanel' id=managingYourSubscription)
+						+managingYourSubscription
+					.tab-pane(role='tabpanel' id=overleafIndividualPlans)
+						+overleafIndividualPlans
+					.tab-pane(role='tabpanel' id=overleafGroupPlans)
+						+overleafGroupPlans
 
 	.row
 		.col-xs-12.plans-faq-support
 			span #{translate('still_have_questions')}
 			button(
-				data-ol-open-contact-form-modal="general"
-				data-bs-toggle=bootstrapVersion === 5 ? "modal" : undefined
-				data-bs-target=bootstrapVersion === 5 ? "#contactUsModal" : undefined
+				data-ol-open-contact-form-modal='general'
+				data-bs-toggle=bootstrapVersion === 5 ? 'modal' : undefined
+				data-bs-target=bootstrapVersion === 5 ? '#contactUsModal' : undefined
 			)
-				span(style="margin-right: 4px") #{translate('contact_support')}
-				+material-symbol-rounded("arrow_right_alt", "icon-md")
+				span(style='margin-right: 4px') #{translate('contact_support')}
+				+material-symbol-rounded('arrow_right_alt', 'icon-md')

+ 217 - 221
services/web/app/views/subscriptions/plans/_plans_faq_tabs.pug

@@ -1,357 +1,353 @@
 //- If the `plans-page-bs5` split test has been completed, remove the `data-toggle` and `data-target` because it is not needed anymore (bs5 uses `data-bs-toggle` and `data-bs-target`)
 include ../../_mixins/material_symbol
-	
-mixin managingYourSubscription()
+
+mixin managingYourSubscription
 	.ol-accordions-container
 		.custom-accordion-item
 			button.custom-accordion-header.collapsed(
-				type="button"
-				data-toggle="collapse"
-				data-target="#managingYourSubscriptionQ1"
-				data-bs-toggle="collapse"
-				data-bs-target="#managingYourSubscriptionQ1"
-				aria-expanded="false"
-				aria-controls="managingYourSubscriptionQ1"
+				type='button'
+				data-toggle='collapse'
+				data-target='#managingYourSubscriptionQ1'
+				data-bs-toggle='collapse'
+				data-bs-target='#managingYourSubscriptionQ1'
+				aria-expanded='false'
+				aria-controls='managingYourSubscriptionQ1'
 			)
 				| Can I change plans or cancel later?
 				span.custom-accordion-icon
-					+material-symbol-outlined("keyboard_arrow_down")
-			.collapse(id="managingYourSubscriptionQ1")
+					+material-symbol-outlined('keyboard_arrow_down')
+			.collapse(id='managingYourSubscriptionQ1')
 				.custom-accordion-body
-					span Yes, you can do this at any time by going to 
-					strong Account > Subscription 
+					span Yes, you can do this at any time by going to
+					strong Account > Subscription
 					span when logged in to Overleaf. You can change plans, switch between monthly and annual billing options, or cancel to downgrade to the free plan. When canceling, your subscription will continue until the end of the billing period.
 		.custom-accordion-item
 			button.custom-accordion-header.collapsed(
-				type="button"
-				data-toggle="collapse"
-				data-target="#managingYourSubscriptionQ2"
-				data-bs-toggle="collapse"
-				data-bs-target="#managingYourSubscriptionQ2"
-				aria-expanded="false"
-				aria-controls="managingYourSubscriptionQ2"
+				type='button'
+				data-toggle='collapse'
+				data-target='#managingYourSubscriptionQ2'
+				data-bs-toggle='collapse'
+				data-bs-target='#managingYourSubscriptionQ2'
+				aria-expanded='false'
+				aria-controls='managingYourSubscriptionQ2'
 			)
 				| If I change or cancel my Overleaf plan, will I lose my projects?
 				span.custom-accordion-icon
-					+material-symbol-outlined("keyboard_arrow_down")
-			.collapse(id="managingYourSubscriptionQ2")
+					+material-symbol-outlined('keyboard_arrow_down')
+			.collapse(id='managingYourSubscriptionQ2')
 				.custom-accordion-body
 					| No. Changing or canceling your plan won’t affect your projects, the only change will be to the features available to you. You can see which features are available only on paid plans in the comparison table.
 		.custom-accordion-item
 			button.custom-accordion-header.collapsed(
-				type="button"
-				data-toggle="collapse"
-				data-target="#managingYourSubscriptionQ3"
-				data-bs-toggle="collapse"
-				data-bs-target="#managingYourSubscriptionQ3"
-				aria-expanded="false"
-				aria-controls="managingYourSubscriptionQ3"
+				type='button'
+				data-toggle='collapse'
+				data-target='#managingYourSubscriptionQ3'
+				data-bs-toggle='collapse'
+				data-bs-target='#managingYourSubscriptionQ3'
+				aria-expanded='false'
+				aria-controls='managingYourSubscriptionQ3'
 			)
 				| Can I pay by invoice or purchase order?
 				span.custom-accordion-icon
-					+material-symbol-outlined("keyboard_arrow_down")
-			.collapse(id="managingYourSubscriptionQ3")
+					+material-symbol-outlined('keyboard_arrow_down')
+			.collapse(id='managingYourSubscriptionQ3')
 				.custom-accordion-body
 					| This is possible when you’re purchasing a group subscription for five or more people, or a site license. For individual subscriptions, we can only accept payment online via credit card, debit card, or PayPal.
 		.custom-accordion-item
 			button.custom-accordion-header.collapsed(
-				type="button"
-				data-toggle="collapse"
-				data-target="#managingYourSubscriptionQ4"
-				data-bs-toggle="collapse"
-				data-bs-target="#managingYourSubscriptionQ4"
-				aria-expanded="false"
-				aria-controls="managingYourSubscriptionQ4"
+				type='button'
+				data-toggle='collapse'
+				data-target='#managingYourSubscriptionQ4'
+				data-bs-toggle='collapse'
+				data-bs-target='#managingYourSubscriptionQ4'
+				aria-expanded='false'
+				aria-controls='managingYourSubscriptionQ4'
 			)
 				| How do I view/update the credit card being charged for my subscription?
 				span.custom-accordion-icon
-					+material-symbol-outlined("keyboard_arrow_down")
-			.collapse(id="managingYourSubscriptionQ4")
+					+material-symbol-outlined('keyboard_arrow_down')
+			.collapse(id='managingYourSubscriptionQ4')
 				.custom-accordion-body
-					| You can view and update the card on file by going to Account > 
+					| You can view and update the card on file by going to Account >
 					a.inline-green-link(
-						target="_blank"
-						href="/user/subscription"
-						event-tracking="plans-page-click"
-						event-tracking-mb="true"
-						event-tracking-trigger="click"
-						event-segmentation={ button: 'contact', location: 'faq' }
+						target='_blank'
+						href='/user/subscription'
+						event-tracking='plans-page-click'
+						event-tracking-mb='true'
+						event-tracking-trigger='click'
+						event-segmentation={button: 'contact', location: 'faq'}
 					)
 						span Subscription
 					| .
 
-
-
-
-mixin overleafIndividualPlans()
+mixin overleafIndividualPlans
 	.ol-accordions-container
 		.custom-accordion-item
 			button.custom-accordion-header.collapsed(
-				type="button"
-				data-toggle="collapse"
-				data-target="#overleafIndividualPlansQ1"
-				data-bs-toggle="collapse"
-				data-bs-target="#overleafIndividualPlansQ1"
-				aria-expanded="false"
-				aria-controls="overleafIndividualPlansQ1"
+				type='button'
+				data-toggle='collapse'
+				data-target='#overleafIndividualPlansQ1'
+				data-bs-toggle='collapse'
+				data-bs-target='#overleafIndividualPlansQ1'
+				aria-expanded='false'
+				aria-controls='overleafIndividualPlansQ1'
 			)
 				| How does the free trial work?
 				span.custom-accordion-icon
-					+material-symbol-outlined("keyboard_arrow_down")
-			.collapse(id="overleafIndividualPlansQ1")
+					+material-symbol-outlined('keyboard_arrow_down')
+			.collapse(id='overleafIndividualPlansQ1')
 				.custom-accordion-body
-					span You get full access to your chosen plan during your 7-day free trial, and there’s no obligation to continue beyond the trial. Your card will be charged at the end of your trial unless you cancel before then. To cancel, go to 
-					strong Account > 
+					span You get full access to your chosen plan during your 7-day free trial, and there’s no obligation to continue beyond the trial. Your card will be charged at the end of your trial unless you cancel before then. To cancel, go to
+					strong Account >
+						|
 						a.inline-green-link(
-							target="_blank"
-							href="/user/subscription"
-							event-tracking="plans-page-click"
-							event-tracking-mb="true"
-							event-tracking-trigger="click"
-							event-segmentation={ button: 'contact', location: 'faq' }
+							target='_blank'
+							href='/user/subscription'
+							event-tracking='plans-page-click'
+							event-tracking-mb='true'
+							event-tracking-trigger='click'
+							event-segmentation={button: 'contact', location: 'faq'}
 						)
 							span Subscription
-					span  when logged in to Overleaf (the trial will continue for the full 7 days).
+					span when logged in to Overleaf (the trial will continue for the full 7 days).
 
 		.custom-accordion-item
 			button.custom-accordion-header.collapsed(
-				type="button"
-				data-toggle="collapse"
-				data-target="#overleafIndividualPlansQ2"
-				data-bs-toggle="collapse"
-				data-bs-target="#overleafIndividualPlansQ2"
-				aria-expanded="false"
-				aria-controls="overleafIndividualPlansQ2"
+				type='button'
+				data-toggle='collapse'
+				data-target='#overleafIndividualPlansQ2'
+				data-bs-toggle='collapse'
+				data-bs-target='#overleafIndividualPlansQ2'
+				aria-expanded='false'
+				aria-controls='overleafIndividualPlansQ2'
 			)
 				| What’s a collaborator on an Overleaf individual subscription?
 				span.custom-accordion-icon
-					+material-symbol-outlined("keyboard_arrow_down")
-			.collapse(id="overleafIndividualPlansQ2")
+					+material-symbol-outlined('keyboard_arrow_down')
+			.collapse(id='overleafIndividualPlansQ2')
 				.custom-accordion-body
-					| A collaborator is someone you invite to work with you on a project. So, for example, on our Standard plan you can have up to 10 people collaborating with you on any given project. 
+					| A collaborator is someone you invite to work with you on a project. So, for example, on our Standard plan you can have up to 10 people collaborating with you on any given project.
 
 		.custom-accordion-item
 			button.custom-accordion-header.collapsed(
-				type="button"
-				data-toggle="collapse"
-				data-target="#overleafIndividualPlansQ3"
-				data-bs-toggle="collapse"
-				data-bs-target="#overleafIndividualPlansQ3"
-				aria-expanded="false"
-				aria-controls="overleafIndividualPlansQ3"
+				type='button'
+				data-toggle='collapse'
+				data-target='#overleafIndividualPlansQ3'
+				data-bs-toggle='collapse'
+				data-bs-target='#overleafIndividualPlansQ3'
+				aria-expanded='false'
+				aria-controls='overleafIndividualPlansQ3'
 			)
 				| The individual Standard plan has 10 project collaborators, does it mean that 10 people will be upgraded?
 				span.custom-accordion-icon
-					+material-symbol-outlined("keyboard_arrow_down")
-			.collapse(id="overleafIndividualPlansQ3")
+					+material-symbol-outlined('keyboard_arrow_down')
+			.collapse(id='overleafIndividualPlansQ3')
 				.custom-accordion-body
-					span No. Only the subscriber’s account will be upgraded. An individual Standard subscription allows you to invite 10 people per project to edit the project with you. Your collaborators can access features such as the full document history and extended compile time, but 
-					strong only 
+					span No. Only the subscriber’s account will be upgraded. An individual Standard subscription allows you to invite 10 people per project to edit the project with you. Your collaborators can access features such as the full document history and extended compile time, but
+					strong only
 					span for the project(s) they’re working on with you. If your collaborators want access to those features on their own projects, they will need to purchase their own subscription. (If you work with the same people regularly, you might find a group subscription more cost effective.)
 		.custom-accordion-item
 			button.custom-accordion-header.collapsed(
-				type="button"
-				data-toggle="collapse"
-				data-target="#overleafIndividualPlansQ4"
-				data-bs-toggle="collapse"
-				data-bs-target="#overleafIndividualPlansQ4"
-				aria-expanded="false"
-				aria-controls="overleafIndividualPlansQ4"
+				type='button'
+				data-toggle='collapse'
+				data-target='#overleafIndividualPlansQ4'
+				data-bs-toggle='collapse'
+				data-bs-target='#overleafIndividualPlansQ4'
+				aria-expanded='false'
+				aria-controls='overleafIndividualPlansQ4'
 			)
 				| Do collaborators also have access to the editing and collaboration features I’ve paid for?
 				span.custom-accordion-icon
-					+material-symbol-outlined("keyboard_arrow_down")
-			.collapse(id="overleafIndividualPlansQ4")
+					+material-symbol-outlined('keyboard_arrow_down')
+			.collapse(id='overleafIndividualPlansQ4')
 				.custom-accordion-body
-					span If you have an Overleaf subscription, then your project collaborators will have access to features like real-time track changes and document history, but 
-					strong only 
+					span If you have an Overleaf subscription, then your project collaborators will have access to features like real-time track changes and document history, but
+					strong only
 					span for the project(s) they’re working on with you. If your collaborators want access to those features on their own projects, they will need to purchase their own subscription. (If you work with the same people regularly, you might find a group subscription more cost effective.)
 		.custom-accordion-item
 			button.custom-accordion-header.collapsed(
-				type="button"
-				data-toggle="collapse"
-				data-target="#overleafIndividualPlansQ5"
-				data-bs-toggle="collapse"
-				data-bs-target="#overleafIndividualPlansQ5"
-				aria-expanded="false"
-				aria-controls="overleafIndividualPlansQ5"
+				type='button'
+				data-toggle='collapse'
+				data-target='#overleafIndividualPlansQ5'
+				data-bs-toggle='collapse'
+				data-bs-target='#overleafIndividualPlansQ5'
+				aria-expanded='false'
+				aria-controls='overleafIndividualPlansQ5'
 			)
 				| Can I purchase an individual plan on behalf of someone else?
 				span.custom-accordion-icon
-					+material-symbol-outlined("keyboard_arrow_down")
-			.collapse(id="overleafIndividualPlansQ5")
+					+material-symbol-outlined('keyboard_arrow_down')
+			.collapse(id='overleafIndividualPlansQ5')
 				.custom-accordion-body
-					| Individual subscriptions must be purchased by the account that will be the end user. If you want to purchase a plan for someone else, you’ll need to provide them with relevant payment details to enable them to make the purchase. 
+					| Individual subscriptions must be purchased by the account that will be the end user. If you want to purchase a plan for someone else, you’ll need to provide them with relevant payment details to enable them to make the purchase.
 		.custom-accordion-item
 			button.custom-accordion-header.collapsed(
-				type="button"
-				data-toggle="collapse"
-				data-target="#overleafIndividualPlansQ6"
-				data-bs-toggle="collapse"
-				data-bs-target="#overleafIndividualPlansQ6"
-				aria-expanded="false"
-				aria-controls="overleafIndividualPlansQ6"
+				type='button'
+				data-toggle='collapse'
+				data-target='#overleafIndividualPlansQ6'
+				data-bs-toggle='collapse'
+				data-bs-target='#overleafIndividualPlansQ6'
+				aria-expanded='false'
+				aria-controls='overleafIndividualPlansQ6'
 			)
 				| Who is eligible for the Student plan?
 				span.custom-accordion-icon
-					+material-symbol-outlined("keyboard_arrow_down")
-			.collapse(id="overleafIndividualPlansQ6")
+					+material-symbol-outlined('keyboard_arrow_down')
+			.collapse(id='overleafIndividualPlansQ6')
 				.custom-accordion-body
 					| As the name suggests, the Student plan is only for students at educational institutions. This includes graduate students.
 		.custom-accordion-item
 			button.custom-accordion-header.collapsed(
-				type="button"
-				data-toggle="collapse"
-				data-target="#overleafIndividualPlansQ7"
-				data-bs-toggle="collapse"
-				data-bs-target="#overleafIndividualPlansQ7"
-				aria-expanded="false"
-				aria-controls="overleafIndividualPlansQ7"
+				type='button'
+				data-toggle='collapse'
+				data-target='#overleafIndividualPlansQ7'
+				data-bs-toggle='collapse'
+				data-bs-target='#overleafIndividualPlansQ7'
+				aria-expanded='false'
+				aria-controls='overleafIndividualPlansQ7'
 			)
 				| Can I transfer an individual subscription to someone else?
 				span.custom-accordion-icon
-					+material-symbol-outlined("keyboard_arrow_down")
-			.collapse(id="overleafIndividualPlansQ7")
+					+material-symbol-outlined('keyboard_arrow_down')
+			.collapse(id='overleafIndividualPlansQ7')
 				.custom-accordion-body
-					| No. Individual plans can’t be transferred. 
-
-
-
-
+					| No. Individual plans can’t be transferred.
 
-mixin overleafGroupPlans()
+mixin overleafGroupPlans
 	.ol-accordions-container
 		.custom-accordion-item
 			button.custom-accordion-header.collapsed(
-				type="button"
-				data-toggle="collapse"
-				data-target="#overleafGroupPlansQ1"
-				data-bs-toggle="collapse"
-				data-bs-target="#overleafGroupPlansQ1"
-				aria-expanded="false"
-				aria-controls="overleafGroupPlansQ1"
+				type='button'
+				data-toggle='collapse'
+				data-target='#overleafGroupPlansQ1'
+				data-bs-toggle='collapse'
+				data-bs-target='#overleafGroupPlansQ1'
+				aria-expanded='false'
+				aria-controls='overleafGroupPlansQ1'
 			)
 				| What’s the difference between users and collaborators on an Overleaf group subscription?
 				span.custom-accordion-icon
-					+material-symbol-outlined("keyboard_arrow_down")
-			.collapse(id="overleafGroupPlansQ1")
+					+material-symbol-outlined('keyboard_arrow_down')
+			.collapse(id='overleafGroupPlansQ1')
 				.custom-accordion-body
-					div On any of our group plans, the number of users refers to the number of people you can invite to join your group. All of these people will have access to the plan’s paid-for features across all their projects, such as real-time track changes and document history. 
-					div.mt-2 Collaborators are people that your group users may invite to work with them on their projects. So, for example, if you have the Group Standard plan, the users in your group can invite up to 10 people to work with them on a project. And if you have the Group Professional plan, your users can invite as many people to work with them as they want.
+					div On any of our group plans, the number of users refers to the number of people you can invite to join your group. All of these people will have access to the plan’s paid-for features across all their projects, such as real-time track changes and document history.
+					.mt-2 Collaborators are people that your group users may invite to work with them on their projects. So, for example, if you have the Group Standard plan, the users in your group can invite up to 10 people to work with them on a project. And if you have the Group Professional plan, your users can invite as many people to work with them as they want.
 		.custom-accordion-item
 			button.custom-accordion-header.collapsed(
-				type="button"
-				data-toggle="collapse"
-				data-target="#overleafGroupPlansQ2"
-				data-bs-toggle="collapse"
-				data-bs-target="#overleafGroupPlansQ2"
-				aria-expanded="false"
-				aria-controls="overleafGroupPlansQ2"
+				type='button'
+				data-toggle='collapse'
+				data-target='#overleafGroupPlansQ2'
+				data-bs-toggle='collapse'
+				data-bs-target='#overleafGroupPlansQ2'
+				aria-expanded='false'
+				aria-controls='overleafGroupPlansQ2'
 			)
 				| What is the benefit of purchasing an Overleaf Group plan?
 				span.custom-accordion-icon
-					+material-symbol-outlined("keyboard_arrow_down")
-			.collapse(id="overleafGroupPlansQ2")
+					+material-symbol-outlined('keyboard_arrow_down')
+			.collapse(id='overleafGroupPlansQ2')
 				.custom-accordion-body
-					| Our Group subscriptions allow you to purchase access to our premium features for multiple people. They’re easy to manage, help save on paperwork, and allow groups of 5 or more to purchase via purchase order (PO). We also offer discounts on purchases of Group subscriptions for more than 20 users; just get in touch with our 
+					| Our Group subscriptions allow you to purchase access to our premium features for multiple people. They’re easy to manage, help save on paperwork, and allow groups of 5 or more to purchase via purchase order (PO). We also offer discounts on purchases of Group subscriptions for more than 20 users; just get in touch with our
 					a.inline-green-link(
-						target="_blank"
-						href="/for/contact-sales"
-						event-tracking="plans-page-click"
-						event-tracking-mb="true"
-						event-tracking-trigger="click"
-						event-segmentation={ button: 'contact', location: 'faq' }
+						target='_blank'
+						href='/for/contact-sales'
+						event-tracking='plans-page-click'
+						event-tracking-mb='true'
+						event-tracking-trigger='click'
+						event-segmentation={button: 'contact', location: 'faq'}
 					)
 						span Sales team
 					| .
 		.custom-accordion-item
 			button.custom-accordion-header.collapsed(
-				type="button"
-				data-toggle="collapse"
-				data-target="#overleafGroupPlansQ3"
-				data-bs-toggle="collapse"
-				data-bs-target="#overleafGroupPlansQ3"
-				aria-expanded="false"
-				aria-controls="overleafGroupPlansQ3"
+				type='button'
+				data-toggle='collapse'
+				data-target='#overleafGroupPlansQ3'
+				data-bs-toggle='collapse'
+				data-bs-target='#overleafGroupPlansQ3'
+				aria-expanded='false'
+				aria-controls='overleafGroupPlansQ3'
 			)
 				| Who is eligible for the educational discount?
 				span.custom-accordion-icon
-					+material-symbol-outlined("keyboard_arrow_down")
-			.collapse(id="overleafGroupPlansQ3")
+					+material-symbol-outlined('keyboard_arrow_down')
+			.collapse(id='overleafGroupPlansQ3')
 				.custom-accordion-body
-					| The educational discount for group subscriptions is for students or faculty who are using Overleaf primarily for teaching. 
+					| The educational discount for group subscriptions is for students or faculty who are using Overleaf primarily for teaching.
 		.custom-accordion-item
 			button.custom-accordion-header.collapsed(
-				type="button"
-				data-toggle="collapse"
-				data-target="#overleafGroupPlansQ4"
-				data-bs-toggle="collapse"
-				data-bs-target="#overleafGroupPlansQ4"
-				aria-expanded="false"
-				aria-controls="overleafGroupPlansQ4"
+				type='button'
+				data-toggle='collapse'
+				data-target='#overleafGroupPlansQ4'
+				data-bs-toggle='collapse'
+				data-bs-target='#overleafGroupPlansQ4'
+				aria-expanded='false'
+				aria-controls='overleafGroupPlansQ4'
 			)
 				| How do I add more licenses to my group subscription, and what will it cost?
 				span.custom-accordion-icon
-					+material-symbol-outlined("keyboard_arrow_down")
-			.collapse(id="overleafGroupPlansQ4")
+					+material-symbol-outlined('keyboard_arrow_down')
+			.collapse(id='overleafGroupPlansQ4')
 				.custom-accordion-body
-					div 
-						| You can add up to 20 licenses using the 
+					div
+						| You can add up to 20 licenses using the
 						a.inline-green-link(
-							target="_blank"
-							href="/user/subscription"
-							event-tracking="plans-page-click"
-							event-tracking-mb="true"
-							event-tracking-trigger="click"
-							event-segmentation={ button: 'contact', location: 'faq' }
+							target='_blank'
+							href='/user/subscription'
+							event-tracking='plans-page-click'
+							event-tracking-mb='true'
+							event-tracking-trigger='click'
+							event-segmentation={button: 'contact', location: 'faq'}
 						)
 							span subscription management page
-						|  accessed by going to Account > 
+						|
+						| accessed by going to Account >
 						a.inline-green-link(
-							target="_blank"
-							href="/user/subscription"
-							event-tracking="plans-page-click"
-							event-tracking-mb="true"
-							event-tracking-trigger="click"
-							event-segmentation={ button: 'contact', location: 'faq' }
+							target='_blank'
+							href='/user/subscription'
+							event-tracking='plans-page-click'
+							event-tracking-mb='true'
+							event-tracking-trigger='click'
+							event-segmentation={button: 'contact', location: 'faq'}
 						)
 							span Subscription
-						|  when logged into Overleaf. The cost per license will be prorated at the current per license rate, and will end with your existing renewal date.
-					div.mt-2 
-						| If you need more than 20 licenses added to your subscription, please 
+						|
+						| when logged into Overleaf. The cost per license will be prorated at the current per license rate, and will end with your existing renewal date.
+					.mt-2
+						| If you need more than 20 licenses added to your subscription, please
 						a.inline-green-link(
-							target="_blank"
-							href="/for/contact-sales"
-							event-tracking="plans-page-click"
-							event-tracking-mb="true"
-							event-tracking-trigger="click"
-							event-segmentation={ button: 'contact', location: 'faq' }
+							target='_blank'
+							href='/for/contact-sales'
+							event-tracking='plans-page-click'
+							event-tracking-mb='true'
+							event-tracking-trigger='click'
+							event-segmentation={button: 'contact', location: 'faq'}
 						)
 							span contact the Sales team
 						| .
 		.custom-accordion-item
 			button.custom-accordion-header.collapsed(
-				type="button"
-				data-toggle="collapse"
-				data-target="#overleafGroupPlansQ5"
-				data-bs-toggle="collapse"
-				data-bs-target="#overleafGroupPlansQ5"
-				aria-expanded="false"
-				aria-controls="overleafGroupPlansQ5"
+				type='button'
+				data-toggle='collapse'
+				data-target='#overleafGroupPlansQ5'
+				data-bs-toggle='collapse'
+				data-bs-target='#overleafGroupPlansQ5'
+				aria-expanded='false'
+				aria-controls='overleafGroupPlansQ5'
 			)
 				| How do I upgrade my plan from Group Standard to Group Professional?
 				span.custom-accordion-icon
-					+material-symbol-outlined("keyboard_arrow_down")
-			.collapse(id="overleafGroupPlansQ5")
+					+material-symbol-outlined('keyboard_arrow_down')
+			.collapse(id='overleafGroupPlansQ5')
 				.custom-accordion-body
-					| You can upgrade your plan from Group Standard to Group Professional on the 
+					| You can upgrade your plan from Group Standard to Group Professional on the
 					a.inline-green-link(
-						target="_blank"
-						href="/user/subscription"
-						event-tracking="plans-page-click"
-						event-tracking-mb="true"
-						event-tracking-trigger="click"
-						event-segmentation={ button: 'contact', location: 'faq' }
+						target='_blank'
+						href='/user/subscription'
+						event-tracking='plans-page-click'
+						event-tracking-mb='true'
+						event-tracking-trigger='click'
+						event-segmentation={button: 'contact', location: 'faq'}
 					)
 						span subscription management page
 					| .

+ 8 - 4
services/web/app/views/subscriptions/preview-change.pug

@@ -4,10 +4,14 @@ block entrypointVar
 	- entrypoint = 'pages/user/subscription/preview-change'
 
 block append meta
-	meta(name="ol-user" data-type="json" content=user)
-	meta(name="ol-subscriptionChangePreview" data-type="json" content=changePreview)
-	meta(name="ol-purchaseReferrer" data-type="string" content=purchaseReferrer)
+	meta(name='ol-user' data-type='json' content=user)
+	meta(
+		name='ol-subscriptionChangePreview'
+		data-type='json'
+		content=changePreview
+	)
+	meta(name='ol-purchaseReferrer' data-type='string' content=purchaseReferrer)
 
 block content
-	main.content.content-alt#main-content
+	main#main-content.content.content-alt
 		#subscription-preview-change

+ 3 - 3
services/web/app/views/subscriptions/subtotal-limit-exceeded.pug

@@ -4,8 +4,8 @@ block entrypointVar
 	- entrypoint = 'pages/user/subscription/group-management/subtotal-limit-exceeded'
 
 block append meta
-	meta(name="ol-user" data-type="json" content=user)
-	meta(name="ol-groupName", data-type="string", content=groupName)
+	meta(name='ol-user' data-type='json' content=user)
+	meta(name='ol-groupName' data-type='string' content=groupName)
 
 block content
-	main.content.content-alt#subtotal-limit-exceeded-root
+	main#subtotal-limit-exceeded-root.content.content-alt

+ 4 - 4
services/web/app/views/subscriptions/successful-subscription-react.pug

@@ -4,9 +4,9 @@ block entrypointVar
 	- entrypoint = 'pages/user/subscription/successful-subscription'
 
 block append meta
-	meta(name="ol-subscription" data-type="json" content=personalSubscription)
-	meta(name="ol-postCheckoutRedirect" content=postCheckoutRedirect)
-	meta(name="ol-user" data-type="json" content=user)
+	meta(name='ol-subscription' data-type='json' content=personalSubscription)
+	meta(name='ol-postCheckoutRedirect' content=postCheckoutRedirect)
+	meta(name='ol-user' data-type='json' content=user)
 
 block content
-	main.content.content-alt#subscription-success-root
+	main#subscription-success-root.content.content-alt

+ 3 - 3
services/web/app/views/subscriptions/team/group-invites.pug

@@ -4,8 +4,8 @@ block entrypointVar
 	- entrypoint = 'pages/user/subscription/group-invites'
 
 block append meta
-	meta(name="ol-teamInvites" data-type="json" content=teamInvites)
-	meta(name="ol-user" data-type="json" content=user)
+	meta(name='ol-teamInvites' data-type='json' content=teamInvites)
+	meta(name='ol-user' data-type='json' content=user)
 
 block content
-	main.content.content-alt.team-invite#group-invites-root
+	main#group-invites-root.content.content-alt.team-invite

+ 15 - 11
services/web/app/views/subscriptions/team/invite-managed.pug

@@ -4,16 +4,20 @@ block entrypointVar
 	- entrypoint = 'pages/user/subscription/invite-managed'
 
 block append meta
-	meta(name="ol-inviteToken" content=inviteToken)
-	meta(name="ol-inviterName" content=inviterName)
-	meta(name="ol-expired" data-type="boolean" content=expired)
-	meta(name="ol-alreadyEnrolled" data-type="boolean" content=alreadyEnrolled)
-	meta(name="ol-validationStatus" data-type="json" content=validationStatus)
-	meta(name="ol-currentManagedUserAdminEmail" data-type="string" content=currentManagedUserAdminEmail)
-	meta(name="ol-groupSSOActive" data-type="boolean" content=groupSSOActive)
-	meta(name="ol-subscriptionId" data-type="string" content=subscriptionId)
-	meta(name="ol-user" data-type="json" content=user)
-	meta(name="ol-usersSubscription" data-type="json" content=usersSubscription)
+	meta(name='ol-inviteToken' content=inviteToken)
+	meta(name='ol-inviterName' content=inviterName)
+	meta(name='ol-expired' data-type='boolean' content=expired)
+	meta(name='ol-alreadyEnrolled' data-type='boolean' content=alreadyEnrolled)
+	meta(name='ol-validationStatus' data-type='json' content=validationStatus)
+	meta(
+		name='ol-currentManagedUserAdminEmail'
+		data-type='string'
+		content=currentManagedUserAdminEmail
+	)
+	meta(name='ol-groupSSOActive' data-type='boolean' content=groupSSOActive)
+	meta(name='ol-subscriptionId' data-type='string' content=subscriptionId)
+	meta(name='ol-user' data-type='json' content=user)
+	meta(name='ol-usersSubscription' data-type='json' content=usersSubscription)
 
 block content
-	main.content.content-alt.team-invite#invite-managed-root
+	main#invite-managed-root.content.content-alt.team-invite

+ 17 - 9
services/web/app/views/subscriptions/team/invite.pug

@@ -4,14 +4,22 @@ block entrypointVar
 	- entrypoint = 'pages/user/subscription/invite'
 
 block append meta
-	meta(name="ol-hasIndividualPaidSubscription" data-type="boolean" content=hasIndividualPaidSubscription)
-	meta(name="ol-inviterName" data-type="string" content=inviterName)
-	meta(name="ol-inviteToken" data-type="string" content=inviteToken)
-	meta(name="ol-currentManagedUserAdminEmail" data-type="string" content=currentManagedUserAdminEmail)
-	meta(name="ol-expired" data-type="boolean" content=expired)
-	meta(name="ol-groupSSOActive" data-type="boolean" content=groupSSOActive)
-	meta(name="ol-subscriptionId" data-type="string" content=subscriptionId)
-	meta(name="ol-user" data-type="json" content=user)
+	meta(
+		name='ol-hasIndividualPaidSubscription'
+		data-type='boolean'
+		content=hasIndividualPaidSubscription
+	)
+	meta(name='ol-inviterName' data-type='string' content=inviterName)
+	meta(name='ol-inviteToken' data-type='string' content=inviteToken)
+	meta(
+		name='ol-currentManagedUserAdminEmail'
+		data-type='string'
+		content=currentManagedUserAdminEmail
+	)
+	meta(name='ol-expired' data-type='boolean' content=expired)
+	meta(name='ol-groupSSOActive' data-type='boolean' content=groupSSOActive)
+	meta(name='ol-subscriptionId' data-type='string' content=subscriptionId)
+	meta(name='ol-user' data-type='json' content=user)
 
 block content
-	main.content.content-alt#invite-root
+	main#invite-root.content.content-alt

+ 9 - 5
services/web/app/views/subscriptions/team/invite_logged_out.pug

@@ -1,12 +1,12 @@
 extends ../../layout-marketing
 
 block append meta
-	meta(name="ol-user" data-type="json" content=user)
+	meta(name='ol-user' data-type='json' content=user)
 
 block content
 	- var colClass = bootstrapVersion === 5 ? 'col-lg-8 m-auto' : 'col-md-8 col-md-offset-2'
 
-	main.content.content-alt.team-invite#main-content
+	main#main-content.content.content-alt.team-invite
 		.container
 			.row
 				div(class=colClass)
@@ -16,15 +16,19 @@ block content
 								// TODO: Remove `team-invite-name` once we fully migrated to Bootstrap 5
 								h1.text-center !{translate("invited_to_group", {inviterName: inviterName, appName: appName }, [{name: 'span', attrs: {class: 'team-invite-name'}}])}
 
-							if (accountExists)
+							if accountExists
 								div
 									p #{translate("invited_to_group_login_benefits", {appName: appName})}
 									p #{translate("invited_to_group_login", {emailAddress: emailAddress})}
 									p
-										a.btn.btn-primary(href=`/login?redir=/subscription/invites/${inviteToken}${groupSSOActive ? "&hide_sso_login=true" : ""}`) #{translate("login_to_accept_invitation")}
+										a.btn.btn-primary(
+											href=`/login?redir=/subscription/invites/${inviteToken}${groupSSOActive ? "&hide_sso_login=true" : ""}`
+										) #{translate("login_to_accept_invitation")}
 							else
 								div
 									p #{translate("invited_to_group_register_benefits", {appName: appName})}
 									p #{translate("invited_to_group_register", {inviterName: inviterName})}
 									p
-										a.btn.btn-primary(href=`/register?redir=/subscription/invites/${inviteToken}${groupSSOActive ? "&hide_sso_login=true" : ""}`) #{translate("register_to_accept_invitation")}
+										a.btn.btn-primary(
+											href=`/register?redir=/subscription/invites/${inviteToken}${groupSSOActive ? "&hide_sso_login=true" : ""}`
+										) #{translate("register_to_accept_invitation")}

+ 9 - 5
services/web/app/views/subscriptions/upgrade-group-subscription-react.pug

@@ -4,10 +4,14 @@ block entrypointVar
 	- entrypoint = 'pages/user/subscription/group-management/upgrade-group-subscription'
 
 block append meta
-	meta(name="ol-user" data-type="json" content=user)
-	meta(name="ol-subscriptionChangePreview" data-type="json" content=changePreview)
-	meta(name="ol-totalLicenses", data-type="number", content=totalLicenses)
-	meta(name="ol-groupName", data-type="string", content=groupName)
+	meta(name='ol-user' data-type='json' content=user)
+	meta(
+		name='ol-subscriptionChangePreview'
+		data-type='json'
+		content=changePreview
+	)
+	meta(name='ol-totalLicenses' data-type='number' content=totalLicenses)
+	meta(name='ol-groupName' data-type='string' content=groupName)
 
 block content
-	main.content.content-alt#upgrade-group-subscription-root
+	main#upgrade-group-subscription-root.content.content-alt

+ 1 - 1
services/web/app/views/user/accountSuspended.pug

@@ -6,7 +6,7 @@ block vars
 	- metadata.robotsNoindexNofollow = true
 
 block content
-	main.content.content-alt#main-content
+	main#main-content.content.content-alt
 		.container-custom-sm.mx-auto
 			.card
 				.card-body

+ 1 - 1
services/web/app/views/user/compromised_password.pug

@@ -9,5 +9,5 @@ block entrypointVar
 	- entrypoint = 'pages/compromised-password'
 
 block content
-	main.content.content-alt#main-content
+	main#main-content.content.content-alt
 		#compromised-password

+ 1 - 1
services/web/app/views/user/confirmSecondaryEmail.pug

@@ -8,7 +8,7 @@ block entrypointVar
 	- entrypoint = 'pages/user/confirm-secondary-email'
 
 block append meta
-	meta(name="ol-email" content=email)
+	meta(name='ol-email' content=email)
 
 block content
 	main.content.content-alt

+ 24 - 30
services/web/app/views/user/confirm_email.pug

@@ -2,56 +2,50 @@ extends ../layout-marketing
 include ../_mixins/notification
 
 block content
-	main.content.content-alt#main-content
+	main#main-content.content.content-alt
 		.container
 			.row
 				.col-lg-8.offset-lg-2.col-xl-6.offset-xl-3
 					.card
 						.card-body
-							.page-header(data-ol-hide-on-error-message="confirm-email-wrong-user")
+							.page-header(data-ol-hide-on-error-message='confirm-email-wrong-user')
 								h1 #{translate("confirm_email")}
+							form(method='POST' action='/logout' id='logoutForm')
+								input(name='_csrf' type='hidden' value=csrfToken)
+								input(name='redirect' type='hidden' value=currentUrlWithQueryParams)
 							form(
-								method="POST"
-								action="/logout"
-								id="logoutForm"
+								name='confirmEmailForm'
+								data-ol-async-form
+								data-ol-auto-submit
+								action='/user/emails/confirm'
+								method='POST'
+								id='confirmEmailForm'
 							)
-								input(type="hidden", name="_csrf", value=csrfToken)
-								input(type="hidden", name="redirect", value=currentUrlWithQueryParams)
-							form(
-								data-ol-async-form,
-								data-ol-auto-submit,
-								name="confirmEmailForm"
-								action="/user/emails/confirm",
-								method="POST",
-								id="confirmEmailForm",
-							)
-								input(type="hidden", name="_csrf", value=csrfToken)
-								input(type="hidden", name="token", value=token)
+								input(name='_csrf' type='hidden' value=csrfToken)
+								input(name='token' type='hidden' value=token)
 
 								div(data-ol-not-sent)
-									+formMessages()
-									div(data-ol-custom-form-message="confirm-email-wrong-user" hidden)
+									+formMessages
+									div(data-ol-custom-form-message='confirm-email-wrong-user' hidden)
 										h1.h3 #{translate("we_cant_confirm_this_email")}
 										p !{translate("to_confirm_email_address_you_must_be_logged_in_with_the_requesting_account")}
 										p !{translate("you_are_currently_logged_in_as", {email: getUserEmail()})}
 										.actions
-											button.btn-primary.btn.w-100(
-												form="logoutForm"
-											) #{translate('log_in_with_a_different_account')}
+											button.btn-primary.btn.w-100(form='logoutForm') #{translate('log_in_with_a_different_account')}
 
 									.actions
 										button.btn-primary.btn.w-100(
-											type='submit',
+											type='submit'
 											data-ol-disabled-inflight
-											data-ol-hide-on-error-message="confirm-email-wrong-user"
+											data-ol-hide-on-error-message='confirm-email-wrong-user'
 										)
-											span(data-ol-inflight="idle")
+											span(data-ol-inflight='idle')
 												| #{translate('confirm')}
-											span(hidden data-ol-inflight="pending")
-												span(role='status').spinner-border.spinner-border-sm.mx-2
+											span(hidden data-ol-inflight='pending')
+												span.spinner-border.spinner-border-sm.mx-2(role='status')
 
 								div(hidden data-ol-sent)
-									+notification({ariaLive: 'polite', type: 'success', className: 'mb-3', content: translate("thank_you_email_confirmed")})
-									div.text-center
-										a.btn.btn-primary(href="/user/settings")
+									+notification({ariaLive: 'polite', type: 'success', className: 'mb-3', content: translate('thank_you_email_confirmed')})
+									.text-center
+										a.btn.btn-primary(href='/user/settings')
 											| #{translate('go_to_account_settings')}

+ 17 - 17
services/web/app/views/user/email-preferences.pug

@@ -2,7 +2,7 @@ extends ../layout-marketing
 include ../_mixins/back_to_btns
 
 block content
-	main.content.content-alt#main-content
+	main#main-content.content.content-alt
 		.container
 			.row
 				.col-lg-10.offset-lg-1.col-xl-8.offset-xl-2
@@ -10,9 +10,9 @@ block content
 						.card-body
 							.page-header
 								h1 #{translate("newsletter_info_title")}
-	
+
 							p #{translate("newsletter_info_summary")}
-	
+
 							- var submitAction
 							if subscribed
 								- submitAction = '/user/newsletter/unsubscribe'
@@ -20,28 +20,28 @@ block content
 							else
 								- submitAction = '/user/newsletter/subscribe'
 								p !{translate("newsletter_info_unsubscribed", {}, ['strong'])}
-	
+
 							form(
+								name='newsletterForm'
 								data-ol-async-form
 								data-ol-reload-on-success
-								name="newsletterForm"
 								action=submitAction
-								method="POST"
+								method='POST'
 							)
-								input(name='_csrf', type='hidden', value=csrfToken)
-								+formMessages()
+								input(name='_csrf' type='hidden' value=csrfToken)
+								+formMessages
 								p.actions.text-center
 									if subscribed
-										button.btn-danger.btn(type='submit', data-ol-disabled-inflight)
-											span(data-ol-inflight="idle") #{translate("unsubscribe")}
-											span(hidden data-ol-inflight="pending") #{translate("saving")}…
+										button.btn-danger.btn(type='submit' data-ol-disabled-inflight)
+											span(data-ol-inflight='idle') #{translate("unsubscribe")}
+											span(hidden data-ol-inflight='pending') #{translate("saving")}…
 									else
-										button.btn-primary.btn(type='submit', data-ol-disabled-inflight)
-											span(data-ol-inflight="idle") #{translate("subscribe")}
-											span(hidden data-ol-inflight="pending") #{translate("saving")}…
-	
+										button.btn-primary.btn(type='submit' data-ol-disabled-inflight)
+											span(data-ol-inflight='idle') #{translate("subscribe")}
+											span(hidden data-ol-inflight='pending') #{translate("saving")}…
+
 							if subscribed
 								p #{translate("newsletter_info_note")}
-	
+
 							.page-separator
-							+back-to-btns()
+							+back-to-btns

+ 18 - 22
services/web/app/views/user/login.pug

@@ -1,7 +1,7 @@
 extends ../layout-marketing
 
 block content
-	main.content.content-alt#main-content
+	main#main-content.content.content-alt
 		.container
 			.row
 				.col-lg-6.offset-lg-3.col-xl-4.offset-xl-4
@@ -11,10 +11,10 @@ block content
 								if login_support_title
 									h1 !{login_support_title}
 								else
-									h1 #{translate("log_in")}					
-							form(data-ol-async-form, name="loginForm", action='/login', method="POST")
-								input(name='_csrf', type='hidden', value=csrfToken)
-								+formMessagesNewStyle()
+									h1 #{translate("log_in")} 
+							form(name='loginForm' data-ol-async-form action='/login' method='POST')
+								input(name='_csrf' type='hidden' value=csrfToken)
+								+formMessagesNewStyle
 								+customFormMessageNewStyle('invalid-password-retry-or-reset', 'danger')
 									| !{translate('email_or_password_wrong_try_again_or_reset', {}, [{ name: 'a', attrs: { href: '/user/password/reset', 'aria-describedby': 'resetPasswordDescription' } }])}
 									span.visually-hidden(id='resetPasswordDescription')
@@ -23,28 +23,24 @@ block content
 									| !{translate('password_compromised_try_again_or_use_known_device_or_reset', {}, [{name: 'a', attrs: {href: 'https://haveibeenpwned.com/passwords', rel: 'noopener noreferrer', target: '_blank'}}, {name: 'a', attrs: {href: '/user/password/reset', target: '_blank'}}])}.
 								.form-group
 									input.form-control(
-										type='email',
-										name='email',
-										required,
-										placeholder='email@example.com',
-										autofocus="true"
+										name='email'
+										type='email'
+										required
+										placeholder='email@example.com'
+										autofocus='true'
 									)
 								.form-group
 									input.form-control(
-										type='password',
-										name='password',
-										required,
-										placeholder='********',
+										name='password'
+										type='password'
+										required
+										placeholder='********'
 									)
 								.actions
-									button.btn-primary.btn(
-										type='submit',
-										data-ol-disabled-inflight
-									)
-										span(data-ol-inflight="idle") #{translate("login")}
-										span(hidden data-ol-inflight="pending") #{translate("logging_in")}…
+									button.btn-primary.btn(type='submit' data-ol-disabled-inflight)
+										span(data-ol-inflight='idle') #{translate("login")}
+										span(hidden data-ol-inflight='pending') #{translate("logging_in")}…
 									a.float-end(href='/user/password/reset') #{translate("forgot_your_password")}?
 								if login_support_text
 									hr
-									p.text-center !{login_support_text}	
-		
+									p.text-center !{login_support_text}

+ 2 - 2
services/web/app/views/user/one_time_login.pug

@@ -1,7 +1,7 @@
 extends ../layout-marketing
 
 block content
-	main.content.content-alt#main-content
+	main#main-content.content.content-alt
 		.container
 			.row
 				.col-lg-6.offset-lg-3.col-xl-4.offset-xl-4
@@ -13,6 +13,6 @@ block content
 							p
 								| Please
 								|
-								a(href="/login") log in
+								a(href='/login') log in
 								|
 								| to continue working on your projects.

+ 30 - 26
services/web/app/views/user/passwordReset-bs5.pug

@@ -11,51 +11,55 @@ block content
 	- var showCaptcha = settings.recaptcha && settings.recaptcha.siteKey && !(settings.recaptcha.disabled && settings.recaptcha.disabled.passwordReset)
 
 	if showCaptcha
-		script(type="text/javascript", nonce=scriptNonce, src="https://www.recaptcha.net/recaptcha/api.js?render=explicit")
+		script(
+			type='text/javascript'
+			nonce=scriptNonce
+			src='https://www.recaptcha.net/recaptcha/api.js?render=explicit'
+		)
 		div(
-			id="recaptcha"
-			class="g-recaptcha"
+			id='recaptcha'
+			class='g-recaptcha'
 			data-sitekey=settings.recaptcha.siteKey
-			data-size="invisible"
-			data-badge="inline"
+			data-size='invisible'
+			data-badge='inline'
 		)
 
-	main#main-content(data-ol-captcha-retry-trigger-area="")
-		a.auth-aux-logo(href="/")
-			img(src=buildImgPath("ol-brand/overleaf-o-dark.svg") alt=settings.appName)
+	main#main-content(data-ol-captcha-retry-trigger-area='')
+		a.auth-aux-logo(href='/')
+			img(src=buildImgPath('ol-brand/overleaf-o-dark.svg') alt=settings.appName)
 		.auth-aux-container
 			form(
+				name='passwordResetForm'
+				captcha-action-name=showCaptcha ? 'passwordReset' : false
 				data-ol-async-form
-				name="passwordResetForm"
-				action="/user/password/reset"
-				method="POST"
-				captcha=(showCaptcha ? '' : false)
-				captcha-action-name=(showCaptcha ? "passwordReset" : false)
+				action='/user/password/reset'
+				method='POST'
+				captcha=showCaptcha ? '' : false
 			)
 				if error === 'password_reset_token_expired'
 					h1.h3.mb-3.mt-0 #{translate("sorry_your_token_expired")}
 					p #{translate('please_request_a_new_password_reset_email_and_follow_the_link')}.
 				else
 					h1.h3.mb-3.mt-0(data-ol-not-sent) #{translate("password_reset_sentence_case")}
-					h1.h3.mb-3.mt-0(hidden data-ol-sent) #{translate("check_your_email")}   
+					h1.h3.mb-3.mt-0(hidden data-ol-sent) #{translate("check_your_email")} 
 					p.mb-3.pb-3(data-ol-not-sent) #{translate("enter_your_email_address_below_and_we_will_send_you_a_link_to_reset_your_password")}.
 
 				div(data-ol-not-sent)
-					+formMessagesNewStyle()
+					+formMessagesNewStyle
 					if error && error !== 'password_reset_token_expired'
 						+notification({ariaLive: 'assertive', type: 'error', className: 'mb-3', content: translate(error)})
 
-					div(data-ol-custom-form-message="no-password-allowed-due-to-sso" hidden)
-						+notification({ariaLive: 'polite', type: 'error', className: 'mb-3', content: translate("you_cant_reset_password_due_to_sso", {}, [{name: 'a', attrs: {href: '/sso-login'}}])})
-					input(type="hidden" name="_csrf" value=csrfToken)
+					div(data-ol-custom-form-message='no-password-allowed-due-to-sso' hidden)
+						+notification({ariaLive: 'polite', type: 'error', className: 'mb-3', content: translate('you_cant_reset_password_due_to_sso', {}, [{name: 'a', attrs: {href: '/sso-login'}}])})
+					input(name='_csrf' type='hidden' value=csrfToken)
 					.form-group.mb-3
 						label.form-label(for='email') #{translate("email")}
-						input.form-control#email(
-							aria-label="email"
-							type='email'
+						input#email.form-control(
 							name='email'
+							aria-label='email'
+							type='email'
 							required
-							autocomplete="username"
+							autocomplete='username'
 							autofocus
 						)
 					.actions
@@ -64,14 +68,14 @@ block content
 							data-ol-disabled-inflight
 							aria-label=translate('reset_password_sentence_case')
 						)
-							span(data-ol-inflight="idle")
+							span(data-ol-inflight='idle')
 								| #{translate("reset_password_sentence_case")}
-							span(hidden data-ol-inflight="pending")
+							span(hidden data-ol-inflight='pending')
 								| #{translate("requesting_password_reset")}…
-						a.btn.btn-ghost.w-100.mb-3(href="/login") #{translate("back_to_log_in")}
+						a.btn.btn-ghost.w-100.mb-3(href='/login') #{translate("back_to_log_in")}
 				div(hidden data-ol-sent)
 					p.mb-4 #{translate('password_reset_email_sent')}
-					a.btn.btn-primary.w-100.mb-3(href="/login") #{translate('back_to_log_in')}
+					a.btn.btn-primary.w-100.mb-3(href='/login') #{translate('back_to_log_in')}
 
 			if showCaptcha
 				+recaptchaConditions

+ 37 - 33
services/web/app/views/user/passwordReset.pug

@@ -9,77 +9,81 @@ block content
 	- var showCaptcha = settings.recaptcha && settings.recaptcha.siteKey && !(settings.recaptcha.disabled && settings.recaptcha.disabled.passwordReset)
 
 	if showCaptcha
-		script(type="text/javascript", nonce=scriptNonce, src="https://www.recaptcha.net/recaptcha/api.js?render=explicit")
+		script(
+			type='text/javascript'
+			nonce=scriptNonce
+			src='https://www.recaptcha.net/recaptcha/api.js?render=explicit'
+		)
 		div(
-			id="recaptcha"
-			class="g-recaptcha"
+			id='recaptcha'
+			class='g-recaptcha'
 			data-sitekey=settings.recaptcha.siteKey
-			data-size="invisible"
-			data-badge="inline"
+			data-size='invisible'
+			data-badge='inline'
 		)
 
-	main.content.content-alt#main-content(data-ol-captcha-retry-trigger-area="")
+	main#main-content.content.content-alt(data-ol-captcha-retry-trigger-area='')
 		.container-custom-sm.mx-auto
 			.card
 				form(
+					name='passwordResetForm'
+					captcha-action-name=showCaptcha ? 'passwordReset' : false
 					data-ol-async-form
-					name="passwordResetForm"
-					action="/user/password/reset",
-					method="POST",
-					captcha=(showCaptcha ? '' : false),
-					captcha-action-name=(showCaptcha ? "passwordReset" : false),
+					action='/user/password/reset'
+					method='POST'
+					captcha=showCaptcha ? '' : false
 				)
 					if error === 'password_reset_token_expired'
 						h3.mt-0.mb-2 #{translate("sorry_your_token_expired")}
 						p #{translate('please_request_a_new_password_reset_email_and_follow_the_link')}.
 					else
 						h3.mt-0.mb-2(data-ol-not-sent) #{translate("password_reset")}
-						h3.mt-0.mb-2(hidden data-ol-sent) #{translate("check_your_email")}	
+						h3.mt-0.mb-2(hidden data-ol-sent) #{translate("check_your_email")}
 						p(data-ol-not-sent) #{translate("enter_your_email_address_below_and_we_will_send_you_a_link_to_reset_your_password")}.
 
 					div(data-ol-not-sent)
-						+formMessages()
+						+formMessages
 						if error && error !== 'password_reset_token_expired'
-							div.alert.alert-danger.mb-2(
-								role="alert"
-								aria-live="assertive"
-							)
+							.alert.alert-danger.mb-2(role='alert' aria-live='assertive')
 								| #{translate(error)}
 
-						div(data-ol-custom-form-message="no-password-allowed-due-to-sso" hidden)
-							.notification.notification-type-error(aria-live="polite" style="margin-bottom: 10px;")
+						div(data-ol-custom-form-message='no-password-allowed-due-to-sso' hidden)
+							.notification.notification-type-error(
+								aria-live='polite'
+								style='margin-bottom: 10px'
+							)
 								.notification-icon
-									+material-symbol-rounded("error")
+									+material-symbol-rounded('error')
 								.notification-content-and-cta
 									.notification-content
 										p
 											| !{translate("you_cant_reset_password_due_to_sso", {}, [{name: 'a', attrs: {href: '/sso-login'}}])}
 
-						input(type="hidden", name="_csrf", value=csrfToken)
+						input(name='_csrf' type='hidden' value=csrfToken)
 						.form-group.mb-3
 							label(for='email') #{translate("email")}
-							input.form-control#email(
-								aria-label="email"
-								type='email',
-								name='email',
-								placeholder=translate("enter_your_email_address"),
-								required,
-								autocomplete="username",
+							input#email.form-control(
+								name='email'
+								aria-label='email'
+								type='email'
+								placeholder=translate('enter_your_email_address')
+								required
+								autocomplete='username'
 								autofocus
 							)
 						.actions
 							button.btn.btn-primary.w-100(
-								type='submit',
-								data-ol-disabled-inflight,
+								type='submit'
+								data-ol-disabled-inflight
 								aria-label=translate('request_password_reset_to_reconfirm')
 							)
-								span(data-ol-inflight="idle")
+								span(data-ol-inflight='idle')
 									| #{translate("request_password_reset")}
-								span(hidden data-ol-inflight="pending")
+								span(hidden data-ol-inflight='pending')
 									| #{translate("requesting_password_reset")}…
 					div(hidden data-ol-sent)
 						p.mb-4 #{translate('password_reset_email_sent')}
-						a(href="/login") #{translate('back_to_log_in')}
+						a(href='/login') #{translate('back_to_log_in')}
 
 			if showCaptcha
 				+recaptchaConditions

+ 19 - 19
services/web/app/views/user/primaryEmailCheck-bs5.pug

@@ -7,36 +7,36 @@ block vars
 block content
 	main#main-content
 		.auth-aux-container
-			img.w-50.d-block(src=buildImgPath("ol-brand/overleaf.svg") alt=settings.appName)
+			img.w-50.d-block(
+				src=buildImgPath('ol-brand/overleaf.svg')
+				alt=settings.appName
+			)
 			h1.h3.mb-3 #{translate("keep_your_account_safe")}
 			div(data-ol-multi-submit)
 				p.small.mb-4
 					| !{translate("primary_email_check_question", { email: getUserEmail() }, ["strong"])}
 				form(
 					data-ol-async-form
-					action="/user/emails/primary-email-check"
-					method="POST"
+					action='/user/emails/primary-email-check'
+					method='POST'
 				)
-					input(name='_csrf', type='hidden', value=csrfToken)
-					+formMessagesNewStyle()
+					input(name='_csrf' type='hidden' value=csrfToken)
+					+formMessagesNewStyle
 
-					button.btn.btn-primary.w-100.mb-3(
-						type='submit'
-						data-ol-disabled-inflight
-					)
-						span(data-ol-inflight="idle") #{translate("yes_that_is_correct")}
-						span(hidden data-ol-inflight="pending") #{translate("confirming")}…
+					button.btn.btn-primary.w-100.mb-3(type='submit' data-ol-disabled-inflight)
+						span(data-ol-inflight='idle') #{translate("yes_that_is_correct")}
+						span(hidden data-ol-inflight='pending') #{translate("confirming")}…
 
 				a.btn.btn-secondary.w-100.mb-4(
-					href="/user/settings#add-email"
+					href='/user/settings#add-email'
 					data-ol-slow-link
-					event-tracking="primary-email-check-change-email"
-					event-tracking-mb="true"
-					event-tracking-trigger="click"
+					event-tracking='primary-email-check-change-email'
+					event-tracking-mb='true'
+					event-tracking-trigger='click'
 				)
-					span(data-ol-inflight="idle") #{translate("no_update_email")}
-					span(hidden data-ol-inflight="pending") #{translate("redirecting")}…  
+					span(data-ol-inflight='idle') #{translate("no_update_email")}
+					span(hidden data-ol-inflight='pending') #{translate("redirecting")}… 
 				p.small.mb-2
-					|  #{translate("keep_your_email_updated")}
+					| #{translate("keep_your_email_updated")}
 				p.small
-					|  !{translate("learn_more_about_emails", {}, [{name: 'a', attrs: {href: '/learn/how-to/Keeping_your_account_secure', 'event-tracking': 'primary-email-check-learn-more', 'event-tracking-mb': 'true', 'event-tracking-trigger': 'click' }}])}
+					| !{translate("learn_more_about_emails", {}, [{name: 'a', attrs: {href: '/learn/how-to/Keeping_your_account_secure', 'event-tracking': 'primary-email-check-learn-more', 'event-tracking-mb': 'true', 'event-tracking-trigger': 'click' }}])}

+ 27 - 26
services/web/app/views/user/reconfirm-bs5.pug

@@ -5,46 +5,50 @@ block vars
 	- isWebsiteRedesign = true
 
 block content
-	- var email = reconfirm_email ? reconfirm_email : ""
+	- var email = reconfirm_email ? reconfirm_email : ''
 	- var showCaptcha = settings.recaptcha && settings.recaptcha.siteKey && !(settings.recaptcha.disabled && settings.recaptcha.disabled.passwordReset)
 
 	if showCaptcha
-		script(type="text/javascript", nonce=scriptNonce, src="https://www.recaptcha.net/recaptcha/api.js?render=explicit")
+		script(
+			type='text/javascript'
+			nonce=scriptNonce
+			src='https://www.recaptcha.net/recaptcha/api.js?render=explicit'
+		)
 		div(
-			id="recaptcha"
-			class="g-recaptcha"
+			id='recaptcha'
+			class='g-recaptcha'
 			data-sitekey=settings.recaptcha.siteKey
-			data-size="invisible"
-			data-badge="inline"
+			data-size='invisible'
+			data-badge='inline'
 		)
 
-	main#main-content(data-ol-captcha-retry-trigger-area="")
-		.container.auth-aux-container(style="max-width: 420px;")
+	main#main-content(data-ol-captcha-retry-trigger-area='')
+		.container.auth-aux-container(style='max-width: 420px')
 			form(
+				name='reconfirmAccountForm'
+				captcha-action-name=showCaptcha ? 'passwordReset' : false
 				data-ol-async-form
-				name="reconfirmAccountForm"
-				action="/user/reconfirm"
-				method="POST"
+				action='/user/reconfirm'
+				method='POST'
 				aria-label=translate('request_reconfirmation_email')
-				captcha=(showCaptcha ? '' : false)
-				captcha-action-name=(showCaptcha ? "passwordReset" : false)
+				captcha=showCaptcha ? '' : false
 			)
 				h1.h5.mb-3 #{translate("reconfirm_account")}
 				p #{translate('reconfirm_explained')}
-					| 
+					|
 					a(href=`mailto:${settings.adminEmail}`) #{settings.adminEmail}
 					| .
-				
+
 				div(data-ol-not-sent)
-					+formMessagesNewStyle()
+					+formMessagesNewStyle
 
-					input(type="hidden" name="_csrf" value=csrfToken)
+					input(name='_csrf' type='hidden' value=csrfToken)
 					.form-group.mb-3
 						label.form-label(for='email') #{translate("please_enter_email")}
 						input.form-control(
-							aria-label="email"
-							type='email'
 							name='email'
+							aria-label='email'
+							type='email'
 							placeholder='email@example.com'
 							required
 							autofocus
@@ -52,20 +56,17 @@ block content
 						)
 					.actions
 						button.btn.btn-primary.w-100(
-							style="white-space: normal;"
+							style='white-space: normal'
 							type='submit'
 							data-ol-disabled-inflight
 							aria-label=translate('request_password_reset_to_reconfirm')
 						)
-							span(data-ol-inflight="idle")
+							span(data-ol-inflight='idle')
 								| #{translate('request_password_reset_to_reconfirm')}
-							span(hidden data-ol-inflight="pending")
+							span(hidden data-ol-inflight='pending')
 								| #{translate('request_password_reset_to_reconfirm')}… 
 				div(hidden data-ol-sent)
-					div.alert.alert-success(
-						role="alert"
-						aria-live="polite"
-					)
+					.alert.alert-success(role='alert' aria-live='polite')
 						span #{translate('password_reset_email_sent')}
 
 			if showCaptcha

+ 29 - 28
services/web/app/views/user/reconfirm.pug

@@ -5,20 +5,24 @@ block vars
 	- bootstrap5PageStatus = 'disabled'
 
 block content
-	- var email = reconfirm_email ? reconfirm_email : ""
+	- var email = reconfirm_email ? reconfirm_email : ''
 	- var showCaptcha = settings.recaptcha && settings.recaptcha.siteKey && !(settings.recaptcha.disabled && settings.recaptcha.disabled.passwordReset)
 
 	if showCaptcha
-		script(type="text/javascript", nonce=scriptNonce, src="https://www.recaptcha.net/recaptcha/api.js?render=explicit")
+		script(
+			type='text/javascript'
+			nonce=scriptNonce
+			src='https://www.recaptcha.net/recaptcha/api.js?render=explicit'
+		)
 		div(
-			id="recaptcha"
-			class="g-recaptcha"
+			id='recaptcha'
+			class='g-recaptcha'
 			data-sitekey=settings.recaptcha.siteKey
-			data-size="invisible"
-			data-badge="inline"
+			data-size='invisible'
+			data-badge='inline'
 		)
 
-	main.content.content-alt#main-content(data-ol-captcha-retry-trigger-area="")
+	main#main-content.content.content-alt(data-ol-captcha-retry-trigger-area='')
 		.container
 			.row
 				.col-sm-12.col-md-6.col-md-offset-3
@@ -28,44 +32,41 @@ block content
 							a(href=`mailto:${settings.adminEmail}`) #{settings.adminEmail}
 							| .
 						form(
+							name='reconfirmAccountForm'
+							captcha-action-name=showCaptcha ? 'passwordReset' : false
 							data-ol-async-form
-							name="reconfirmAccountForm"
-							action="/user/reconfirm",
-							method="POST",
+							action='/user/reconfirm'
+							method='POST'
 							aria-label=translate('request_reconfirmation_email')
-							captcha=(showCaptcha ? '' : false),
-							captcha-action-name=(showCaptcha ? "passwordReset" : false)
+							captcha=showCaptcha ? '' : false
 						)
 							div(data-ol-not-sent)
-								+formMessages()
-	
-								input(type="hidden", name="_csrf", value=csrfToken)
+								+formMessages
+
+								input(name='_csrf' type='hidden' value=csrfToken)
 								.form-group
 									label(for='email') #{translate("please_enter_email")}
 									input.form-control(
-										aria-label="email"
-										type='email',
-										name='email',
-										placeholder='email@example.com',
-										required,
+										name='email'
+										aria-label='email'
+										type='email'
+										placeholder='email@example.com'
+										required
 										autofocus
 										value=email
 									)
 								.actions
 									button.btn.btn-primary(
-										type='submit',
-										data-ol-disabled-inflight,
+										type='submit'
+										data-ol-disabled-inflight
 										aria-label=translate('request_password_reset_to_reconfirm')
 									)
-										span(data-ol-inflight="idle")
+										span(data-ol-inflight='idle')
 											| #{translate('request_password_reset_to_reconfirm')}
-										span(hidden data-ol-inflight="pending")
+										span(hidden data-ol-inflight='pending')
 											| #{translate('request_password_reset_to_reconfirm')}… 
 							div(hidden data-ol-sent)
-								div.alert.alert-success(
-									role="alert"
-									aria-live="polite"
-								)
+								.alert.alert-success(role='alert' aria-live='polite')
 									span #{translate('password_reset_email_sent')}
 			.row
 				.col-sm-12.col-md-6.col-md-offset-3

+ 3 - 3
services/web/app/views/user/register.pug

@@ -4,7 +4,7 @@ block vars
 	- bootstrap5PageStatus = 'disabled'
 
 block content
-	main.content.content-alt#main-content
+	main#main-content.content.content-alt
 		.container
 			.row
 				.registration_message
@@ -15,12 +15,12 @@ block content
 							| #{translate("join_sl_to_view_project")}.
 						div
 							| #{translate("already_have_sl_account")}
-							a(href="/login")  #{translate("login_here")}
+							a(href='/login') #{translate("login_here")}
 					else if newTemplateData.templateName !== undefined
 						h1 #{translate("register_to_edit_template", {templateName:newTemplateData.templateName})}
 
 						div #{translate("already_have_sl_account")}
-							a(href="/login")  #{translate("login_here")}
+							a(href='/login') #{translate("login_here")}
 
 			.row
 				.col-md-8.col-md-offset-2.col-lg-6.col-lg-offset-3

+ 14 - 18
services/web/app/views/user/sessions.pug

@@ -1,15 +1,15 @@
 extends ../layout-marketing
 
 block content
-	main.content.content-alt#main-content
+	main#main-content.content.content-alt
 		.container
 			.row
 				.col-lg-10.offset-lg-1.col-xl-8.offset-xl-2
 					.card.clear-user-sessions
 						.card-body
 							.page-header
-								h1  #{translate("your_sessions")}
-	
+								h1 #{translate("your_sessions")}
+
 							if currentSession.ip_address && currentSession.session_created
 								h3 #{translate("current_session")}
 								div
@@ -21,47 +21,43 @@ block content
 											tr
 												td #{currentSession.ip_address}
 												td #{moment(currentSession.session_created).utc().format('Do MMM YYYY, h:mm a')} UTC
-	
+
 							h3 #{translate("other_sessions")}
 							div
 								p.small
 									| !{translate("clear_sessions_description")}
-	
-							form(
-								data-ol-async-form
-								action='/user/sessions/clear'
-								method='POST'
-							)
+
+							form(data-ol-async-form action='/user/sessions/clear' method='POST')
 								input(name='_csrf' type='hidden' value=csrfToken)
 								div(data-ol-not-sent)
 									if sessions.length == 0
 										p.text-center
 											| #{translate("no_other_sessions")}
-	
+
 									if sessions.length > 0
 										table.table.table-striped
 											thead
 												tr
 													th #{translate("ip_address")}
 													th #{translate("session_created_at")}
-											for session in sessions
+											each session in sessions
 												tr
 													td #{session.ip_address}
 													td #{moment(session.session_created).utc().format('Do MMM YYYY, h:mm a')} UTC
-	
+
 										p.actions
 											.text-center
 												button.btn.btn-lg.btn-primary(
-													type="submit"
+													type='submit'
 													data-ol-disable-inflight
 												)
-													span(data-ol-inflight="idle") #{translate('clear_sessions')}
-													span(hidden data-ol-inflight="pending") #{translate("processing")}…
-	
+													span(data-ol-inflight='idle') #{translate('clear_sessions')}
+													span(hidden data-ol-inflight='pending') #{translate("processing")}…
+
 								div(hidden data-ol-sent)
 									p.text-center
 										| #{translate("no_other_sessions")}
-	
+
 									p.text-success.text-center
 										| #{translate('clear_sessions_success')}
 							.page-separator

+ 23 - 27
services/web/app/views/user/setPassword-bs5.pug

@@ -7,28 +7,25 @@ block vars
 
 block content
 	main#main-content
-		a.auth-aux-logo(href="/")
-			img(src=buildImgPath("ol-brand/overleaf-o-dark.svg") alt=settings.appName)
+		a.auth-aux-logo(href='/')
+			img(src=buildImgPath('ol-brand/overleaf-o-dark.svg') alt=settings.appName)
 		.auth-aux-container
 			form(
+				name='passwordResetForm'
 				data-ol-async-form
-				name="passwordResetForm"
-				action="/user/password/set"
-				method="POST"
-				data-ol-hide-on-error="token-expired"
+				action='/user/password/set'
+				method='POST'
+				data-ol-hide-on-error='token-expired'
 			)
-				div(
-					hidden
-					data-ol-sent
-				)
+				div(hidden data-ol-sent)
 					h1.h3.mb-3.mt-0 #{translate("password_updated")}
 					p.mb-4 #{translate("your_password_has_been_successfully_changed")}.
 					a.btn.btn-primary.w-100(href='/login') #{translate("log_in_now")}
 
 				div(data-ol-not-sent)
 					h1.h3.mb-3.mt-0 #{translate("reset_your_password")}
-					p(data-ol-hide-on-error-message="token-expired") #{translate("create_a_new_password_for_your_account")}.
-					+formMessagesNewStyle()
+					p(data-ol-hide-on-error-message='token-expired') #{translate("create_a_new_password_for_your_account")}.
+					+formMessagesNewStyle
 
 					+customFormMessageNewStyle('password-contains-email', 'danger')
 						| #{translate('invalid_password_contains_email')}.
@@ -41,18 +38,21 @@ block content
 					+customFormMessageNewStyle('token-expired', 'danger')
 						| #{translate('password_reset_token_expired')}
 						br
-						a(href="/user/password/reset")
+						a(href='/user/password/reset')
 							| #{translate('request_new_password_reset_email')}
 
-					input(type="hidden" name="_csrf" value=csrfToken)
-					input(type="text" hidden name="email" autocomplete="username" value=email)
+					input(name='_csrf' type='hidden' value=csrfToken)
+					input(name='email' type='text' hidden autocomplete='username' value=email)
 
 					.form-group.mb-3
-						label.form-label(for='passwordField', data-ol-hide-on-error-message="token-expired") #{translate("new_password")}
-						input.form-control.auth-aux-new-password#passwordField(
-							type='password'
+						label.form-label(
+							for='passwordField'
+							data-ol-hide-on-error-message='token-expired'
+						) #{translate("new_password")}
+						input#passwordField.form-control.auth-aux-new-password(
 							name='password'
-							autocomplete="new-password"
+							type='password'
+							autocomplete='new-password'
 							autofocus
 							required
 							minlength=settings.passwordStrengthOptions.length.min
@@ -68,12 +68,8 @@ block content
 							| !{translate('password_was_detected_on_a_public_list_of_known_compromised_passwords', {}, [{name: 'a', attrs: {href: 'https://haveibeenpwned.com/passwords', rel: 'noopener noreferrer', target: '_blank'}}])}.
 							| #{translate('use_a_different_password')}.
 
-						input(
-							type="hidden"
-							name="passwordResetToken"
-							value=passwordResetToken
-						)
-					div(data-ol-hide-on-error-message="token-expired")
+						input(name='passwordResetToken' type='hidden' value=passwordResetToken)
+					div(data-ol-hide-on-error-message='token-expired')
 						div #{translate('in_order_to_have_a_secure_account_make_sure_your_password')}
 						ul.mb-3.ps-4
 							li #{translate('is_longer_than_n_characters', {n: settings.passwordStrengthOptions.length.min})}
@@ -85,7 +81,7 @@ block content
 							data-ol-disabled-inflight
 							aria-label=translate('set_new_password')
 						) 
-							span(data-ol-inflight="idle")
+							span(data-ol-inflight='idle')
 								| #{translate('set_new_password')}
-							span(hidden data-ol-inflight="pending")
+							span(hidden data-ol-inflight='pending')
 								| #{translate('set_new_password')}…

+ 34 - 32
services/web/app/views/user/setPassword.pug

@@ -4,28 +4,25 @@ block vars
 	- bootstrap5PageStatus = 'disabled'
 
 block content
-	main.content.content-alt#main-content
+	main#main-content.content.content-alt
 		.container-custom-sm.mx-auto
 			.card
 				form(
-					data-ol-async-form,
-					name="passwordResetForm",
-					action="/user/password/set",
-					method="POST",
-					data-ol-hide-on-error="token-expired"
+					name='passwordResetForm'
+					data-ol-async-form
+					action='/user/password/set'
+					method='POST'
+					data-ol-hide-on-error='token-expired'
 				)
-					div(
-						hidden
-						data-ol-sent
-					)
+					div(hidden data-ol-sent)
 						h3.mt-0.mb-2 #{translate("password_updated")}
 						p.mb-4 #{translate("your_password_has_been_successfully_changed")}.
 						a(href='/login') #{translate("log_in_now")}
 
 					div(data-ol-not-sent)
 						h3.mt-0.mb-2 #{translate("reset_your_password")}
-						p(data-ol-hide-on-error-message="token-expired") #{translate("create_a_new_password_for_your_account")}.
-						+formMessages()
+						p(data-ol-hide-on-error-message='token-expired') #{translate("create_a_new_password_for_your_account")}.
+						+formMessages
 
 						+customFormMessage('password-contains-email', 'danger')
 							| #{translate('invalid_password_contains_email')}.
@@ -38,21 +35,30 @@ block content
 						+customFormMessage('token-expired', 'danger')
 							| #{translate('password_reset_token_expired')}
 							br
-							a(href="/user/password/reset")
+							a(href='/user/password/reset')
 								| #{translate('request_new_password_reset_email')}
 
-						input(type="hidden", name="_csrf", value=csrfToken)
-						input(type="text" hidden name="email" autocomplete="username" value=email)
+						input(name='_csrf' type='hidden' value=csrfToken)
+						input(
+							name='email'
+							type='text'
+							hidden
+							autocomplete='username'
+							value=email
+						)
 
 						.form-group
-							label(for='passwordField', data-ol-hide-on-error-message="token-expired") #{translate("new_password")}
-							input.form-control#passwordField(
-								type='password',
-								name='password',
-								placeholder=translate("enter_your_new_password"),
-								autocomplete="new-password",
-								autofocus,
-								required,
+							label(
+								for='passwordField'
+								data-ol-hide-on-error-message='token-expired'
+							) #{translate("new_password")}
+							input#passwordField.form-control(
+								name='password'
+								type='password'
+								placeholder=translate('enter_your_new_password')
+								autocomplete='new-password'
+								autofocus
+								required
 								minlength=settings.passwordStrengthOptions.length.min
 							)
 
@@ -66,12 +72,8 @@ block content
 								| !{translate('password_was_detected_on_a_public_list_of_known_compromised_passwords', {}, [{name: 'a', attrs: {href: 'https://haveibeenpwned.com/passwords', rel: 'noopener noreferrer', target: '_blank'}}])}.
 								| #{translate('use_a_different_password')}.
 
-							input(
-								type="hidden",
-								name="passwordResetToken",
-								value=passwordResetToken
-							)
-						div(data-ol-hide-on-error-message="token-expired")
+							input(name='passwordResetToken' type='hidden' value=passwordResetToken)
+						div(data-ol-hide-on-error-message='token-expired')
 							div #{translate('in_order_to_have_a_secure_account_make_sure_your_password')}
 							ul.mb-4.ps-4
 								li #{translate('is_longer_than_n_characters', {n: settings.passwordStrengthOptions.length.min})}
@@ -79,11 +81,11 @@ block content
 								li #{translate('is_not_used_on_any_other_website')}
 						.actions
 							button.btn.btn-primary.w-100(
-								type='submit',
+								type='submit'
 								data-ol-disabled-inflight
 								aria-label=translate('set_new_password')
 							) 
-								span(data-ol-inflight="idle")
+								span(data-ol-inflight='idle')
 									| #{translate('set_new_password')}
-								span(hidden data-ol-inflight="pending")
+								span(hidden data-ol-inflight='pending')
 									| #{translate('set_new_password')}… 

+ 54 - 27
services/web/app/views/user/settings.pug

@@ -2,38 +2,65 @@ extends ../layout-react
 
 block entrypointVar
 	- entrypoint = 'pages/user/settings'
-		
+
 block vars
 	- isWebsiteRedesign = true
 
 block append meta
-	meta(name="ol-hasPassword" data-type="boolean" content=hasPassword)
-	meta(name="ol-shouldAllowEditingDetails" data-type="boolean" content=shouldAllowEditingDetails)
-	meta(name="ol-oauthProviders", data-type="json", content=oauthProviders)
-	meta(name="ol-institutionLinked", data-type="json", content=institutionLinked)
-	meta(name="ol-samlError", data-type="json", content=samlError)
-	meta(name="ol-institutionEmailNonCanonical", content=institutionEmailNonCanonical)
+	meta(name='ol-hasPassword' data-type='boolean' content=hasPassword)
+	meta(
+		name='ol-shouldAllowEditingDetails'
+		data-type='boolean'
+		content=shouldAllowEditingDetails
+	)
+	meta(name='ol-oauthProviders' data-type='json' content=oauthProviders)
+	meta(name='ol-institutionLinked' data-type='json' content=institutionLinked)
+	meta(name='ol-samlError' data-type='json' content=samlError)
+	meta(
+		name='ol-institutionEmailNonCanonical'
+		content=institutionEmailNonCanonical
+	)
 
-	meta(name="ol-reconfirmedViaSAML", content=reconfirmedViaSAML)
-	meta(name="ol-reconfirmationRemoveEmail", content=reconfirmationRemoveEmail)
-	meta(name="ol-samlBeta", content=samlBeta)
-	meta(name="ol-ssoErrorMessage", content=ssoErrorMessage)
-	meta(name="ol-thirdPartyIds", data-type="json", content=thirdPartyIds || {})
-	meta(name="ol-passwordStrengthOptions", data-type="json", content=settings.passwordStrengthOptions || {})
-	meta(name="ol-isExternalAuthenticationSystemUsed" data-type="boolean" content=externalAuthenticationSystemUsed())
-	meta(name="ol-user" data-type="json" content=user)
-	meta(name="ol-labsExperiments" data-type="json" content=labsExperiments)
-	meta(name="ol-dropbox" data-type="json" content=dropbox)
-	meta(name="ol-github" data-type="json" content=github)
-	meta(name="ol-projectSyncSuccessMessage", content=projectSyncSuccessMessage)
-	meta(name="ol-personalAccessTokens", data-type="json" content=personalAccessTokens)
-	meta(name="ol-emailAddressLimit", data-type="json", content=emailAddressLimit)
-	meta(name="ol-currentManagedUserAdminEmail" data-type="string" content=currentManagedUserAdminEmail)
-	meta(name="ol-gitBridgeEnabled" data-type="boolean" content=gitBridgeEnabled)
-	meta(name="ol-isSaas" data-type="boolean" content=isSaas)
-	meta(name="ol-memberOfSSOEnabledGroups" data-type="json" content=memberOfSSOEnabledGroups)
-	meta(name="ol-capabilities" data-type="json" content=capabilities)
+	meta(name='ol-reconfirmedViaSAML' content=reconfirmedViaSAML)
+	meta(name='ol-reconfirmationRemoveEmail' content=reconfirmationRemoveEmail)
+	meta(name='ol-samlBeta' content=samlBeta)
+	meta(name='ol-ssoErrorMessage' content=ssoErrorMessage)
+	meta(name='ol-thirdPartyIds' data-type='json' content=thirdPartyIds || {})
+	meta(
+		name='ol-passwordStrengthOptions'
+		data-type='json'
+		content=settings.passwordStrengthOptions || {}
+	)
+	meta(
+		name='ol-isExternalAuthenticationSystemUsed'
+		data-type='boolean'
+		content=externalAuthenticationSystemUsed()
+	)
+	meta(name='ol-user' data-type='json' content=user)
+	meta(name='ol-labsExperiments' data-type='json' content=labsExperiments)
+	meta(name='ol-dropbox' data-type='json' content=dropbox)
+	meta(name='ol-github' data-type='json' content=github)
+	meta(name='ol-projectSyncSuccessMessage' content=projectSyncSuccessMessage)
+	meta(
+		name='ol-personalAccessTokens'
+		data-type='json'
+		content=personalAccessTokens
+	)
+	meta(name='ol-emailAddressLimit' data-type='json' content=emailAddressLimit)
+	meta(
+		name='ol-currentManagedUserAdminEmail'
+		data-type='string'
+		content=currentManagedUserAdminEmail
+	)
+	meta(name='ol-gitBridgeEnabled' data-type='boolean' content=gitBridgeEnabled)
+	meta(name='ol-isSaas' data-type='boolean' content=isSaas)
+	meta(
+		name='ol-memberOfSSOEnabledGroups'
+		data-type='json'
+		content=memberOfSSOEnabledGroups
+	)
+	meta(name='ol-capabilities' data-type='json' content=capabilities)
 
 block content
-	main.content.content-alt#main-content
+	main#main-content.content.content-alt
 		#settings-page-root

+ 5 - 5
services/web/app/views/user_membership/group-managers-react.pug

@@ -4,10 +4,10 @@ block entrypointVar
 	- entrypoint = 'pages/user/subscription/group-management/group-managers'
 
 block append meta
-	meta(name="ol-user", data-type="json", content=user)
-	meta(name="ol-users", data-type="json", content=users)
-	meta(name="ol-groupId", data-type="string", content=groupId)
-	meta(name="ol-groupName", data-type="string", content=name)
+	meta(name='ol-user' data-type='json' content=user)
+	meta(name='ol-users' data-type='json' content=users)
+	meta(name='ol-groupId' data-type='string' content=groupId)
+	meta(name='ol-groupName' data-type='string' content=name)
 
 block content
-	main.content.content-alt#subscription-manage-group-root
+	main#subscription-manage-group-root.content.content-alt

+ 28 - 12
services/web/app/views/user_membership/group-members-react.pug

@@ -2,18 +2,34 @@ extends ../layout-react
 
 block entrypointVar
 	- entrypoint = 'pages/user/subscription/group-management/group-members'
-		
+
 block append meta
-	meta(name="ol-user", data-type="json", content=user)
-	meta(name="ol-users", data-type="json", content=users)
-	meta(name="ol-groupId", data-type="string", content=groupId)
-	meta(name="ol-groupName", data-type="string", content=name)
-	meta(name="ol-groupSize", data-type="json", content=groupSize)
-	meta(name="ol-managedUsersActive", data-type="boolean", content=managedUsersActive)
-	meta(name="ol-isUserGroupManager", data-type="boolean", content=isUserGroupManager)
-	meta(name="ol-groupSSOActive", data-type="boolean", content=groupSSOActive)
-	meta(name="ol-canUseFlexibleLicensing", data-type="boolean", content=canUseFlexibleLicensing)
-	meta(name="ol-canUseAddSeatsFeature", data-type="boolean", content=canUseAddSeatsFeature)
+	meta(name='ol-user' data-type='json' content=user)
+	meta(name='ol-users' data-type='json' content=users)
+	meta(name='ol-groupId' data-type='string' content=groupId)
+	meta(name='ol-groupName' data-type='string' content=name)
+	meta(name='ol-groupSize' data-type='json' content=groupSize)
+	meta(
+		name='ol-managedUsersActive'
+		data-type='boolean'
+		content=managedUsersActive
+	)
+	meta(
+		name='ol-isUserGroupManager'
+		data-type='boolean'
+		content=isUserGroupManager
+	)
+	meta(name='ol-groupSSOActive' data-type='boolean' content=groupSSOActive)
+	meta(
+		name='ol-canUseFlexibleLicensing'
+		data-type='boolean'
+		content=canUseFlexibleLicensing
+	)
+	meta(
+		name='ol-canUseAddSeatsFeature'
+		data-type='boolean'
+		content=canUseAddSeatsFeature
+	)
 
 block content
-	main.content.content-alt#subscription-manage-group-root
+	main#subscription-manage-group-root.content.content-alt

+ 5 - 5
services/web/app/views/user_membership/institution-managers-react.pug

@@ -4,10 +4,10 @@ block entrypointVar
 	- entrypoint = 'pages/user/subscription/group-management/institution-managers'
 
 block append meta
-	meta(name="ol-user" data-type="json" content=user)
-	meta(name="ol-users", data-type="json", content=users)
-	meta(name="ol-groupId", data-type="string", content=groupId)
-	meta(name="ol-groupName", data-type="string", content=name)
+	meta(name='ol-user' data-type='json' content=user)
+	meta(name='ol-users' data-type='json' content=users)
+	meta(name='ol-groupId' data-type='string' content=groupId)
+	meta(name='ol-groupName' data-type='string' content=name)
 
 block content
-	main.content.content-alt#subscription-manage-group-root
+	main#subscription-manage-group-root.content.content-alt

+ 6 - 13
services/web/app/views/user_membership/new.pug

@@ -4,20 +4,13 @@ block vars
 	- bootstrap5PageStatus = 'disabled'
 
 block content
-	main.content.content-alt#main-content
+	main#main-content.content.content-alt
 		.container
 			.row
 				.col-md-10.col-md-offset-1
 					h3 #{entityName} "#{entityId}" does not exists in v2
-					form(
-						data-ol-regular-form
-						method='post',
-						action=''
-					)
-						input(name="_csrf", type="hidden", value=csrfToken)
-						button.btn.btn-primary(
-							type="submit",
-							data-ol-disabled-inflight
-						)
-							span(data-ol-inflight="idle") Create #{entityName} in v2
-							span(hidden data-ol-inflight="pending") #{translate("creating")}…
+					form(data-ol-regular-form method='post' action='')
+						input(name='_csrf' type='hidden' value=csrfToken)
+						button.btn.btn-primary(type='submit' data-ol-disabled-inflight)
+							span(data-ol-inflight='idle') Create #{entityName} in v2
+							span(hidden data-ol-inflight='pending') #{translate("creating")}…

+ 5 - 5
services/web/app/views/user_membership/publisher-managers-react.pug

@@ -4,10 +4,10 @@ block entrypointVar
 	- entrypoint = 'pages/user/subscription/group-management/publisher-managers'
 
 block append meta
-	meta(name="ol-user" data-type="json" content=user)
-	meta(name="ol-users", data-type="json", content=users)
-	meta(name="ol-groupId", data-type="string", content=groupId)
-	meta(name="ol-groupName", data-type="string", content=name)
+	meta(name='ol-user' data-type='json' content=user)
+	meta(name='ol-users' data-type='json' content=users)
+	meta(name='ol-groupId' data-type='string' content=groupId)
+	meta(name='ol-groupName' data-type='string' content=name)
 
 block content
-	main.content.content-alt#subscription-manage-group-root
+	main#subscription-manage-group-root.content.content-alt

+ 70 - 89
services/web/modules/launchpad/app/views/launchpad.pug

@@ -2,39 +2,43 @@ extends ../../../../app/views/layout-marketing
 
 mixin launchpad-check(section)
 	div(data-ol-launchpad-check=section)
-		span(data-ol-inflight="pending")
+		span(data-ol-inflight='pending')
 			i.fa.fa-fw.fa-spinner.fa-spin
 			span &nbsp;#{translate('checking')}
-	
-		span(hidden data-ol-inflight="idle")
-			div(data-ol-result="success")
+
+		span(hidden data-ol-inflight='idle')
+			div(data-ol-result='success')
 				i.fa.fa-check
 				span &nbsp;#{translate('ok')}
 				button.btn.btn-inline-link
 					span.text-danger &nbsp;#{translate('retry')}
-			div(hidden data-ol-result="error")
+			div(hidden data-ol-result='error')
 				i.fa.fa-exclamation
 				span &nbsp;#{translate('error')}
 				button.btn.btn-inline-link
 					span.text-danger &nbsp;#{translate('retry')}
-				div.alert.alert-danger
+				.alert.alert-danger
 					span(data-ol-error)
 
 block entrypointVar
 	- entrypoint = 'modules/launchpad/pages/launchpad'
-					
+
 block vars
 	- metadata = metadata || {}
 	- bootstrap5PageStatus = 'disabled'
 
 block append meta
-	meta(name="ol-adminUserExists" data-type="boolean" content=adminUserExists)
-	meta(name="ol-ideJsPath" content=buildJsPath('ide.js'))
+	meta(name='ol-adminUserExists' data-type='boolean' content=adminUserExists)
+	meta(name='ol-ideJsPath' content=buildJsPath('ide.js'))
 
 block content
-	script(type="text/javascript", nonce=scriptNonce, src=(wsUrl || '/socket.io') + '/socket.io.js')
+	script(
+		type='text/javascript'
+		nonce=scriptNonce
+		src=(wsUrl || '/socket.io') + '/socket.io.js'
+	)
 
-	.content.content-alt#main-content
+	#main-content.content.content-alt
 		.container
 			.row
 				.col-md-8.col-md-offset-2
@@ -49,8 +53,6 @@ block content
 						<!-- wrapper -->
 						.row
 							.col-md-8.col-md-offset-2
-
-
 								<!-- create first admin form -->
 								if !adminUserExists
 									.row(data-ol-not-sent)
@@ -62,37 +64,34 @@ block content
 												form(
 													data-ol-async-form
 													data-ol-register-admin
-													action="/launchpad/register_admin"
-													method="POST"
+													action='/launchpad/register_admin'
+													method='POST'
 												)
-													input(name='_csrf', type='hidden', value=csrfToken)
-													+formMessages()
+													input(name='_csrf' type='hidden' value=csrfToken)
+													+formMessages
 													.form-group
 														label(for='email') #{translate("email")}
 														input.form-control(
-															type='email',
-															name='email',
-															placeholder="email@example.com"
-															autocomplete="username"
-															required,
-															autofocus="true"
+															name='email'
+															type='email'
+															placeholder='email@example.com'
+															autocomplete='username'
+															required
+															autofocus='true'
 														)
 													.form-group
 														label(for='password') #{translate("password")}
-														input.form-control#passwordField(
-															type='password',
-															name='password',
-															placeholder="********",
-															autocomplete="new-password"
-															required,
+														input#passwordField.form-control(
+															name='password'
+															type='password'
+															placeholder='********'
+															autocomplete='new-password'
+															required
 														)
 													.actions
-														button.btn-primary.btn(
-															type='submit'
-															data-ol-disabled-inflight
-														)
-															span(data-ol-inflight="idle") #{translate("register")}
-															span(hidden data-ol-inflight="pending") #{translate("registering")}…
+														button.btn-primary.btn(type='submit' data-ol-disabled-inflight)
+															span(data-ol-inflight='idle') #{translate("register")}
+															span(hidden data-ol-inflight='pending') #{translate("registering")}…
 
 											// Ldap Form
 											if authMethod === 'ldap'
@@ -103,28 +102,25 @@ block content
 												form(
 													data-ol-async-form
 													data-ol-register-admin
-													action="/launchpad/register_ldap_admin"
-													method="POST"
+													action='/launchpad/register_ldap_admin'
+													method='POST'
 												)
-													input(name='_csrf', type='hidden', value=csrfToken)
-													+formMessages()
+													input(name='_csrf' type='hidden' value=csrfToken)
+													+formMessages
 													.form-group
 														label(for='email') #{translate("email")}
 														input.form-control(
-															type='email',
-															name='email',
-															placeholder="email@example.com"
-															autocomplete="username"
-															required,
-															autofocus="true"
+															name='email'
+															type='email'
+															placeholder='email@example.com'
+															autocomplete='username'
+															required
+															autofocus='true'
 														)
 													.actions
-														button.btn-primary.btn(
-															type='submit'
-															data-ol-disabled-inflight
-														)
-															span(data-ol-inflight="idle") #{translate("register")}
-															span(hidden data-ol-inflight="pending") #{translate("registering")}…
+														button.btn-primary.btn(type='submit' data-ol-disabled-inflight)
+															span(data-ol-inflight='idle') #{translate("register")}
+															span(hidden data-ol-inflight='pending') #{translate("registering")}…
 
 											// Saml Form
 											if authMethod === 'saml'
@@ -135,28 +131,25 @@ block content
 												form(
 													data-ol-async-form
 													data-ol-register-admin
-													action="/launchpad/register_saml_admin"
-													method="POST"
+													action='/launchpad/register_saml_admin'
+													method='POST'
 												)
-													input(name='_csrf', type='hidden', value=csrfToken)
-													+formMessages()
+													input(name='_csrf' type='hidden' value=csrfToken)
+													+formMessages
 													.form-group
 														label(for='email') #{translate("email")}
 														input.form-control(
-															type='email',
-															name='email',
-															placeholder="email@example.com"
-															autocomplete="username"
-															required,
-															autofocus="true"
+															name='email'
+															type='email'
+															placeholder='email@example.com'
+															autocomplete='username'
+															required
+															autofocus='true'
 														)
 													.actions
-														button.btn-primary.btn(
-															type='submit'
-															data-ol-disabled-inflight
-														)
-															span(data-ol-inflight="idle") #{translate("register")}
-															span(hidden data-ol-inflight="pending") #{translate("registering")}…
+														button.btn-primary.btn(type='submit' data-ol-disabled-inflight)
+															span(data-ol-inflight='idle') #{translate("register")}
+															span(hidden data-ol-inflight='pending') #{translate("registering")}…
 
 									br
 
@@ -164,7 +157,6 @@ block content
 								if adminUserExists
 									.row
 										.col-md-12.status-indicators
-
 											h2 #{translate('status_checks')}
 
 											<!-- websocket -->
@@ -185,42 +177,31 @@ block content
 											h3 #{translate('send_test_email')}
 											form.form(
 												data-ol-async-form
-												action="/launchpad/send_test_email"
-												method="POST"
+												action='/launchpad/send_test_email'
+												method='POST'
 											)
 												.form-group
-													label(for="email") Email
-													input.form-control(
-														type="text"
-														id="email"
-														name="email"
-														required
-													)
-												button.btn-primary.btn(
-													type='submit'
-													data-ol-disabled-inflight
-												)
-													span(data-ol-inflight="idle") #{translate("send")}
-													span(hidden data-ol-inflight="pending") #{translate("sending")}…
+													label(for='email') Email
+													input.form-control(name='email' type='text' id='email' required)
+												button.btn-primary.btn(type='submit' data-ol-disabled-inflight)
+													span(data-ol-inflight='idle') #{translate("send")}
+													span(hidden data-ol-inflight='pending') #{translate("sending")}…
 
 												p
-													+formMessages()
-
-
+													+formMessages
 
 									<!-- break -->
 									hr.thin
 
-
 									<!-- Go to app -->
 									.row
 										.col-md-12
 											.text-center
 												br
 												p
-													a(href="/admin").btn.btn-info
+													a.btn.btn-info(href='/admin')
 														| Go To Admin Panel
 													| &nbsp;
-													a(href="/project").btn.btn-primary
+													a.btn.btn-primary(href='/project')
 														| Start Using #{settings.appName}
 												br

+ 27 - 31
services/web/modules/user-activate/app/views/user/activate.pug

@@ -6,14 +6,14 @@ block vars
 include ../../../../../app/views/_mixins/material_symbol
 
 block content
-	main.content.content-alt#main-content
+	main#main-content.content.content-alt
 		.container
-			div.col-lg-6.col-xl-4.m-auto
+			.col-lg-6.col-xl-4.m-auto
 				.notification-list
-					.notification.notification-type-success(aria-live="off" role="alert")
+					.notification.notification-type-success(aria-live='off' role='alert')
 						.notification-content-and-cta
 							.notification-icon
-								+material-symbol("check_circle")
+								+material-symbol('check_circle')
 							.notification-content
 								p
 									| #{translate("nearly_activated")}
@@ -21,12 +21,12 @@ block content
 				h1.h3 #{translate("please_set_a_password")}
 
 				form(
+					name='activationForm'
 					data-ol-async-form
-					name="activationForm",
-					action="/user/password/set",
-					method="POST",
+					action='/user/password/set'
+					method='POST'
 				)
-					+formMessages()
+					+formMessages
 
 					+customFormMessage('token-expired', 'danger')
 						| #{translate("activation_token_expired")}
@@ -34,43 +34,39 @@ block content
 					+customFormMessage('invalid-password', 'danger')
 						| #{translate('invalid_password')}
 
-					input(name='_csrf', type='hidden', value=csrfToken)
-					input(
-						type="hidden",
-						name="passwordResetToken",
-						value=token
-					)
+					input(name='_csrf' type='hidden' value=csrfToken)
+					input(name='passwordResetToken' type='hidden' value=token)
 
 					.form-group
 						label(for='emailField') #{translate("email")}
-						input.form-control#emailField(
-							aria-label="email",
-							type='email',
-							name='email',
-							placeholder="email@example.com",
-							autocomplete="username"
+						input#emailField.form-control(
+							name='email'
+							aria-label='email'
+							type='email'
+							placeholder='email@example.com'
+							autocomplete='username'
 							value=email
-							required,
+							required
 							disabled
 						)
 					.form-group
 						label(for='passwordField') #{translate("password")}
-						input.form-control#passwordField(
-							type='password',
-							name='password',
-							placeholder="********",
-							autocomplete="new-password",
-							autofocus,
-							required,
+						input#passwordField.form-control(
+							name='password'
+							type='password'
+							placeholder='********'
+							autocomplete='new-password'
+							autofocus
+							required
 							minlength=settings.passwordStrengthOptions.length.min
 						)
 					.actions
 						button.btn.btn-primary(
-							type='submit',
+							type='submit'
 							data-ol-disabled-inflight
 							aria-label=translate('activate')
 						)
-							span(data-ol-inflight="idle")
+							span(data-ol-inflight='idle')
 								| #{translate('activate')}
-							span(hidden data-ol-inflight="pending")
+							span(hidden data-ol-inflight='pending')
 								| #{translate('activating')}…

+ 2 - 2
services/web/modules/user-activate/app/views/user/register.pug

@@ -4,9 +4,9 @@ block entrypointVar
 	- entrypoint = 'modules/user-activate/pages/user-activate-page'
 
 block append meta
-	meta(name="ol-user" data-type="json" content=user)
+	meta(name='ol-user' data-type='json' content=user)
 
 block content
-	.content.content-alt#main-content
+	#main-content.content.content-alt
 		.container
 			#user-activate-register-container

+ 3 - 0
services/web/package.json

@@ -28,6 +28,8 @@
     "format:fix": "prettier --write $PWD/'**/*.{js,jsx,mjs,ts,tsx,json}'",
     "format:styles": "prettier --list-different $PWD/'**/*.{css,less,scss}'",
     "format:styles:fix": "prettier --write $PWD/'**/*.{css,less,scss}'",
+    "format:pug": "prettier --list-different $PWD/'**/*.pug'",
+    "format:pug:fix": "prettier --write $PWD/'**/*.pug'",
     "lint": "eslint --max-warnings 0 --format unix --ext .js,.jsx,.mjs,.ts,.tsx .",
     "lint:fix": "eslint --fix --ext .js,.jsx,.mjs,.ts,.tsx .",
     "lint:styles": "stylelint '**/*.scss'",
@@ -209,6 +211,7 @@
     "@pollyjs/adapter-node-http": "^6.0.6",
     "@pollyjs/core": "^6.0.6",
     "@pollyjs/persister-fs": "^6.0.6",
+    "@prettier/plugin-pug": "^3.4.0",
     "@replit/codemirror-emacs": "overleaf/codemirror-emacs#4394c03858f27053f8768258e9493866e06e938e",
     "@replit/codemirror-indentation-markers": "overleaf/codemirror-indentation-markers#78264032eb286bc47871569ae87bff5ca1c6c161",
     "@replit/codemirror-vim": "overleaf/codemirror-vim#1bef138382d948018f3f9b8a4d7a70ab61774e4b",