[WEB-4488] feat: brand revamp (#7544)

* chore: empty state asset and theme improvement (#7542)

* chore: empty state asset and theme improvement

* chore: upgrade modal improvement and code refactor

* feat: onboarding revamp and theme changes (#7541)

* refactor: consolidate password strength indicator into shared UI package

* chore: remove old password strength meter implementations

* chore: update package dependencies for password strength refactor

* chore: code refactor

* chore: brand logo added

* chore:  terms and conditions refactor

* chore: auth form refactor

* chore: oauth enhancements and refactor

* chore: plane new logos added

* chore: auth input form field added to ui package

* chore: password input component added

* chore: web auth refactor

* chore: update brand colors and remove onboarding-specific styles

* chore: clean up unused assets

* chore: profile menu text overflow

* chore: theme related changes

* chore: logo spinner updated

* chore: onboarding constant and types updated

* chore: theme changes and code refactor

* feat: onboarding flow revamp

* fix:  build error and code refactoring

* chore: code refactor

* fix: build error

* chore: consent option added to onboarding and code refactor

* fix: build fix

* chore: code refactor

* chore: auth screen revamp and code refactor

* chore: onboarding enhancements

* chore: code refactor

* chore: onboarding logic improvement

* chore: code refactor

* fix: onboarding pre release improvements

* chore: color token updated

* chore: color token updated

* chore: auth screen line height and size improvements

* chore: input height updated

* chore: n-progress theme updated

* chore: theme and logo enhancements

* chore: space auth and code refactor

* chore: update new brand empty states (#7543)

* [WEB-4585]chore: branding updates (#7540)

* chore: updated logo, og image, and loaders

* chore: updated branding colors

* chore: tour modal logo

* chore: updated logo spinner size

* chore: updated email templates logos and colors

* chore: code refactor

* fix: removed conditional hook render

* fix: space app loader

---------

Co-authored-by: Vamsi Krishna <46787868+vamsikrishnamathala@users.noreply.github.com>
Co-authored-by: vamsikrishnamathala <matalav55@gmail.com>
This commit is contained in:
Anmol Singh Bhatia
2025-08-06 22:24:47 +05:30
committed by GitHub
parent 6450793d72
commit 51e146f8ca
345 changed files with 5158 additions and 2515 deletions

View File

@@ -42,7 +42,7 @@ export const AdminSidebarDropdown = observer(() => {
)} )}
> >
<div className="flex flex-col gap-2.5 pb-2"> <div className="flex flex-col gap-2.5 pb-2">
<span className="px-2 text-custom-sidebar-text-200">{currentUser?.email}</span> <span className="px-2 text-custom-sidebar-text-200 truncate">{currentUser?.email}</span>
</div> </div>
<div className="py-2"> <div className="py-2">
<Menu.Item <Menu.Item

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 466 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 761 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 919 B

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -2,8 +2,8 @@
"name": "", "name": "",
"short_name": "", "short_name": "",
"icons": [ "icons": [
{ "src": "/android-chrome-192x192.png", "sizes": "192x192", "type": "image/png" }, { "src": "/favicon/android-chrome-192x192.png", "sizes": "192x192", "type": "image/png" },
{ "src": "/android-chrome-512x512.png", "sizes": "512x512", "type": "image/png" } { "src": "/favicon/android-chrome-512x512.png", "sizes": "512x512", "type": "image/png" }
], ],
"theme_color": "#ffffff", "theme_color": "#ffffff",
"background_color": "#ffffff", "background_color": "#ffffff",

View File

@@ -24,24 +24,24 @@
:root { :root {
color-scheme: light !important; color-scheme: light !important;
--color-primary-10: 236, 241, 255; --color-primary-10: 229, 243, 250;
--color-primary-20: 217, 228, 255; --color-primary-20: 216, 237, 248;
--color-primary-30: 197, 214, 255; --color-primary-30: 199, 229, 244;
--color-primary-40: 178, 200, 255; --color-primary-40: 169, 214, 239;
--color-primary-50: 159, 187, 255; --color-primary-50: 144, 202, 234;
--color-primary-60: 140, 173, 255; --color-primary-60: 109, 186, 227;
--color-primary-70: 121, 159, 255; --color-primary-70: 75, 170, 221;
--color-primary-80: 101, 145, 255; --color-primary-80: 41, 154, 214;
--color-primary-90: 82, 132, 255; --color-primary-90: 34, 129, 180;
--color-primary-100: 63, 118, 255; --color-primary-100: 0, 99, 153;
--color-primary-200: 57, 106, 230; --color-primary-200: 0, 92, 143;
--color-primary-300: 50, 94, 204; --color-primary-300: 0, 86, 133;
--color-primary-400: 44, 83, 179; --color-primary-400: 0, 77, 117;
--color-primary-500: 38, 71, 153; --color-primary-500: 0, 66, 102;
--color-primary-600: 32, 59, 128; --color-primary-600: 0, 53, 82;
--color-primary-700: 25, 47, 102; --color-primary-700: 0, 43, 66;
--color-primary-800: 19, 35, 76; --color-primary-800: 0, 33, 51;
--color-primary-900: 13, 24, 51; --color-primary-900: 0, 23, 36;
--color-background-100: 255, 255, 255; /* primary bg */ --color-background-100: 255, 255, 255; /* primary bg */
--color-background-90: 247, 247, 247; /* secondary bg */ --color-background-90: 247, 247, 247; /* secondary bg */
@@ -197,6 +197,25 @@
[data-theme="dark-contrast"] { [data-theme="dark-contrast"] {
color-scheme: dark !important; color-scheme: dark !important;
--color-primary-10: 8, 31, 43;
--color-primary-20: 10, 37, 51;
--color-primary-30: 13, 49, 69;
--color-primary-40: 16, 58, 81;
--color-primary-50: 18, 68, 94;
--color-primary-60: 23, 86, 120;
--color-primary-70: 28, 104, 146;
--color-primary-80: 31, 116, 163;
--color-primary-90: 34, 129, 180;
--color-primary-100: 40, 146, 204;
--color-primary-200: 41, 154, 214;
--color-primary-300: 75, 170, 221;
--color-primary-400: 109, 186, 227;
--color-primary-500: 144, 202, 234;
--color-primary-600: 169, 214, 239;
--color-primary-700: 199, 229, 244;
--color-primary-800: 216, 237, 248;
--color-primary-900: 229, 243, 250;
--color-background-100: 25, 25, 25; /* primary bg */ --color-background-100: 25, 25, 25; /* primary bg */
--color-background-90: 32, 32, 32; /* secondary bg */ --color-background-90: 32, 32, 32; /* secondary bg */
--color-background-80: 44, 44, 44; /* tertiary bg */ --color-background-80: 44, 44, 44; /* tertiary bg */
@@ -286,25 +305,6 @@
[data-theme="dark"], [data-theme="dark"],
[data-theme="light-contrast"], [data-theme="light-contrast"],
[data-theme="dark-contrast"] { [data-theme="dark-contrast"] {
--color-primary-10: 236, 241, 255;
--color-primary-20: 217, 228, 255;
--color-primary-30: 197, 214, 255;
--color-primary-40: 178, 200, 255;
--color-primary-50: 159, 187, 255;
--color-primary-60: 140, 173, 255;
--color-primary-70: 121, 159, 255;
--color-primary-80: 101, 145, 255;
--color-primary-90: 82, 132, 255;
--color-primary-100: 63, 118, 255;
--color-primary-200: 57, 106, 230;
--color-primary-300: 50, 94, 204;
--color-primary-400: 44, 83, 179;
--color-primary-500: 38, 71, 153;
--color-primary-600: 32, 59, 128;
--color-primary-700: 25, 47, 102;
--color-primary-800: 19, 35, 76;
--color-primary-900: 13, 24, 51;
--color-sidebar-background-100: var(--color-background-100); /* primary sidebar bg */ --color-sidebar-background-100: var(--color-background-100); /* primary sidebar bg */
--color-sidebar-background-90: var(--color-background-90); /* secondary sidebar bg */ --color-sidebar-background-90: var(--color-background-90); /* secondary sidebar bg */
--color-sidebar-background-80: var(--color-background-80); /* tertiary sidebar bg */ --color-sidebar-background-80: var(--color-background-80); /* tertiary sidebar bg */

View File

@@ -8,8 +8,8 @@
<title>Set a new password to your Plane account</title> <title>Set a new password to your Plane account</title>
<style type="text/css" emogrify="no"> #outlook a { padding: 0; } .ExternalClass { width: 100%; } .ExternalClass, .ExternalClass p, .ExternalClass span, .ExternalClass font, .ExternalClass td, .ExternalClass div { line-height: 100%; } table td { border-collapse: collapse; mso-line-height-rule: exactly; } .editable.image { font-size: 0 !important; line-height: 0 !important; } .nl2go_preheader { display: none !important; mso-hide: all !important; mso-line-height-rule: exactly; visibility: hidden !important; line-height: 0px !important; font-size: 0px !important; } body { width: 100% !important; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; margin: 0; padding: 0; } img { outline: none; text-decoration: none; -ms-interpolation-mode: bicubic; } a img { border: none; } table { border-collapse: collapse; mso-table-lspace: 0pt; mso-table-rspace: 0pt; } th { font-weight: normal; text-align: left; } *[class="gmail-fix"] { display: none !important; } </style> <style type="text/css" emogrify="no"> #outlook a { padding: 0; } .ExternalClass { width: 100%; } .ExternalClass, .ExternalClass p, .ExternalClass span, .ExternalClass font, .ExternalClass td, .ExternalClass div { line-height: 100%; } table td { border-collapse: collapse; mso-line-height-rule: exactly; } .editable.image { font-size: 0 !important; line-height: 0 !important; } .nl2go_preheader { display: none !important; mso-hide: all !important; mso-line-height-rule: exactly; visibility: hidden !important; line-height: 0px !important; font-size: 0px !important; } body { width: 100% !important; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; margin: 0; padding: 0; } img { outline: none; text-decoration: none; -ms-interpolation-mode: bicubic; } a img { border: none; } table { border-collapse: collapse; mso-table-lspace: 0pt; mso-table-rspace: 0pt; } th { font-weight: normal; text-align: left; } *[class="gmail-fix"] { display: none !important; } </style>
<style type="text/css" emogrify="no"> @media (max-width: 600px) { .gmx-killpill { content: " \03D1"; } } </style> <style type="text/css" emogrify="no"> @media (max-width: 600px) { .gmx-killpill { content: " \03D1"; } } </style>
<style type="text/css" emogrify="no"> @media (max-width: 600px) { .gmx-killpill { content: " \03D1"; } .r0-o { border-style: solid !important; margin: 0 auto 0 auto !important; width: 320px !important; } .r1-i { background-color: #ffffff !important; } .r2-c { box-sizing: border-box !important; text-align: center !important; valign: top !important; width: 100% !important; } .r3-o { border-style: solid !important; margin: 0 auto 0 auto !important; width: 100% !important; } .r4-i { padding-bottom: 20px !important; padding-left: 15px !important; padding-right: 15px !important; padding-top: 20px !important; } .r5-c { box-sizing: border-box !important; display: block !important; valign: top !important; width: 100% !important; } .r6-o { border-style: solid !important; width: 100% !important; } .r7-i { padding-left: 0px !important; padding-right: 0px !important; } .r8-c { box-sizing: border-box !important; text-align: left !important; valign: top !important; width: 100% !important; } .r9-o { border-style: solid !important; margin: 0 auto 0 0 !important; width: 100% !important; } .r10-i { padding-bottom: 10px !important; padding-left: 0px !important; padding-right: 0px !important; padding-top: 10px !important; } .r11-i { padding-bottom: 15px !important; padding-top: 15px !important; text-align: left !important; } .r12-i { background-color: #ffffff !important; padding-bottom: 15px !important; padding-top: 15px !important; text-align: left !important; } .r13-c { box-sizing: border-box !important; padding: 0 !important; text-align: center !important; valign: top !important; width: 100% !important; } .r14-o { border-style: solid !important; margin: 0 auto 0 auto !important; margin-bottom: 15px !important; margin-top: 15px !important; width: 100% !important; } .r15-i { padding: 0 !important; text-align: center !important; } .r16-r { background-color: #3f76ff !important; border-radius: 4px !important; border-width: 0px !important; box-sizing: border-box; height: initial !important; padding: 0 !important; padding-bottom: 12px !important; padding-top: 12px !important; text-align: center !important; width: 100% !important; } .r17-i { padding-bottom: 0px !important; padding-left: 15px !important; padding-right: 15px !important; padding-top: 0px !important; } .r18-c { box-sizing: border-box !important; text-align: center !important; width: 100% !important; } .r19-i { padding-bottom: 5px !important; padding-top: 5px !important; } .r20-i { padding-bottom: 15px !important; padding-left: 15px !important; padding-right: 15px !important; padding-top: 15px !important; } .r21-o { border-bottom-color: #efefef !important; border-bottom-width: 2px !important; border-left-color: #efefef !important; border-left-width: 2px !important; border-right-color: #efefef !important; border-right-width: 2px !important; border-style: solid !important; border-top-color: #efefef !important; border-top-width: 2px !important; margin: 0 auto 0 0 !important; width: 100% !important; } .r22-i { padding-bottom: 5px !important; padding-left: 5px !important; padding-right: 5px !important; padding-top: 5px !important; text-align: left !important; } .r23-i { padding-bottom: 5px !important; padding-left: 15px !important; padding-right: 15px !important; padding-top: 5px !important; } .r24-c { box-sizing: border-box !important; width: 100% !important; } .r25-i { font-size: 0px !important; padding-bottom: 10px !important; padding-left: 65px !important; padding-right: 65px !important; padding-top: 10px !important; } .r26-c { box-sizing: border-box !important; width: 32px !important; } .r27-o { border-style: solid !important; margin-right: 8px !important; width: 32px !important; } .r28-o { border-style: solid !important; margin-right: 0px !important; width: 32px !important; } .r29-i { padding-bottom: 0px !important; padding-top: 5px !important; text-align: center !important; } body { -webkit-text-size-adjust: none; } .nl2go-responsive-hide { display: none; } .nl2go-body-table { min-width: unset !important; } .mobshow { height: auto !important; overflow: visible !important; max-height: unset !important; visibility: visible !important; border: none !important; } .resp-table { display: inline-table !important; } .magic-resp { display: table-cell !important; } } </style> <style type="text/css" emogrify="no"> @media (max-width: 600px) { .gmx-killpill { content: " \03D1"; } .r0-o { border-style: solid !important; margin: 0 auto 0 auto !important; width: 320px !important; } .r1-i { background-color: #ffffff !important; } .r2-c { box-sizing: border-box !important; text-align: center !important; valign: top !important; width: 100% !important; } .r3-o { border-style: solid !important; margin: 0 auto 0 auto !important; width: 100% !important; } .r4-i { padding-bottom: 20px !important; padding-left: 15px !important; padding-right: 15px !important; padding-top: 20px !important; } .r5-c { box-sizing: border-box !important; display: block !important; valign: top !important; width: 100% !important; } .r6-o { border-style: solid !important; width: 100% !important; } .r7-i { padding-left: 0px !important; padding-right: 0px !important; } .r8-c { box-sizing: border-box !important; text-align: left !important; valign: top !important; width: 100% !important; } .r9-o { border-style: solid !important; margin: 0 auto 0 0 !important; width: 100% !important; } .r10-i { padding-bottom: 10px !important; padding-left: 0px !important; padding-right: 0px !important; padding-top: 10px !important; } .r11-i { padding-bottom: 15px !important; padding-top: 15px !important; text-align: left !important; } .r12-i { background-color: #ffffff !important; padding-bottom: 15px !important; padding-top: 15px !important; text-align: left !important; } .r13-c { box-sizing: border-box !important; padding: 0 !important; text-align: center !important; valign: top !important; width: 100% !important; } .r14-o { border-style: solid !important; margin: 0 auto 0 auto !important; margin-bottom: 15px !important; margin-top: 15px !important; width: 100% !important; } .r15-i { padding: 0 !important; text-align: center !important; } .r16-r { background-color: #006399 !important; border-radius: 4px !important; border-width: 0px !important; box-sizing: border-box; height: initial !important; padding: 0 !important; padding-bottom: 12px !important; padding-top: 12px !important; text-align: center !important; width: 100% !important; } .r17-i { padding-bottom: 0px !important; padding-left: 15px !important; padding-right: 15px !important; padding-top: 0px !important; } .r18-c { box-sizing: border-box !important; text-align: center !important; width: 100% !important; } .r19-i { padding-bottom: 5px !important; padding-top: 5px !important; } .r20-i { padding-bottom: 15px !important; padding-left: 15px !important; padding-right: 15px !important; padding-top: 15px !important; } .r21-o { border-bottom-color: #efefef !important; border-bottom-width: 2px !important; border-left-color: #efefef !important; border-left-width: 2px !important; border-right-color: #efefef !important; border-right-width: 2px !important; border-style: solid !important; border-top-color: #efefef !important; border-top-width: 2px !important; margin: 0 auto 0 0 !important; width: 100% !important; } .r22-i { padding-bottom: 5px !important; padding-left: 5px !important; padding-right: 5px !important; padding-top: 5px !important; text-align: left !important; } .r23-i { padding-bottom: 5px !important; padding-left: 15px !important; padding-right: 15px !important; padding-top: 5px !important; } .r24-c { box-sizing: border-box !important; width: 100% !important; } .r25-i { font-size: 0px !important; padding-bottom: 10px !important; padding-left: 65px !important; padding-right: 65px !important; padding-top: 10px !important; } .r26-c { box-sizing: border-box !important; width: 32px !important; } .r27-o { border-style: solid !important; margin-right: 8px !important; width: 32px !important; } .r28-o { border-style: solid !important; margin-right: 0px !important; width: 32px !important; } .r29-i { padding-bottom: 0px !important; padding-top: 5px !important; text-align: center !important; } body { -webkit-text-size-adjust: none; } .nl2go-responsive-hide { display: none; } .nl2go-body-table { min-width: unset !important; } .mobshow { height: auto !important; overflow: visible !important; max-height: unset !important; visibility: visible !important; border: none !important; } .resp-table { display: inline-table !important; } .magic-resp { display: table-cell !important; } } </style>
<style type="text/css"> p, h1, h2, h3, h4, ol, ul { margin: 0; } a, a:link { color: #3f76ff; text-decoration: underline; } .nl2go-default-textstyle { color: #3b3f44; font-family: georgia, serif; font-size: 16px; line-height: 1.5; word-break: break-word; } .default-button { color: #ffffff; font-family: georgia, serif; font-size: 16px; font-style: normal; font-weight: normal; line-height: 1.15; text-decoration: none; word-break: break-word; } .default-heading1 { color: #1f2d3d; font-family: arial, helvetica, sans-serif; font-size: 36px; word-break: break-word; } .default-heading2 { color: #1f2d3d; font-family: arial, helvetica, sans-serif; font-size: 32px; word-break: break-word; } .default-heading3 { color: #1f2d3d; font-family: arial, helvetica, sans-serif; font-size: 24px; word-break: break-word; } .default-heading4 { color: #1f2d3d; font-family: arial, helvetica, sans-serif; font-size: 18px; word-break: break-word; } a[x-apple-data-detectors] { color: inherit !important; text-decoration: inherit !important; font-size: inherit !important; font-family: inherit !important; font-weight: inherit !important; line-height: inherit !important; } .no-show-for-you { border: none; display: none; float: none; font-size: 0; height: 0; line-height: 0; max-height: 0; mso-hide: all; overflow: hidden; table-layout: fixed; visibility: hidden; width: 0; } </style> <style type="text/css"> p, h1, h2, h3, h4, ol, ul { margin: 0; } a, a:link { color: #006399; text-decoration: underline; } .nl2go-default-textstyle { color: #3b3f44; font-family: georgia, serif; font-size: 16px; line-height: 1.5; word-break: break-word; } .default-button { color: #ffffff; font-family: georgia, serif; font-size: 16px; font-style: normal; font-weight: normal; line-height: 1.15; text-decoration: none; word-break: break-word; } .default-heading1 { color: #1f2d3d; font-family: arial, helvetica, sans-serif; font-size: 36px; word-break: break-word; } .default-heading2 { color: #1f2d3d; font-family: arial, helvetica, sans-serif; font-size: 32px; word-break: break-word; } .default-heading3 { color: #1f2d3d; font-family: arial, helvetica, sans-serif; font-size: 24px; word-break: break-word; } .default-heading4 { color: #1f2d3d; font-family: arial, helvetica, sans-serif; font-size: 18px; word-break: break-word; } a[x-apple-data-detectors] { color: inherit !important; text-decoration: inherit !important; font-size: inherit !important; font-family: inherit !important; font-weight: inherit !important; line-height: inherit !important; } .no-show-for-you { border: none; display: none; float: none; font-size: 0; height: 0; line-height: 0; max-height: 0; mso-hide: all; overflow: hidden; table-layout: fixed; visibility: hidden; width: 0; } </style>
<!--[if mso ]> <!--[if mso ]>
<xml> <xml>
<o:OfficeDocumentSettings> <o:OfficeDocumentSettings>
@@ -18,9 +18,9 @@
</o:OfficeDocumentSettings> </o:OfficeDocumentSettings>
</xml> </xml>
<! [endif]--> <! [endif]-->
<style type="text/css"> a:link { color: #3f76ff; text-decoration: underline; } </style> <style type="text/css"> a:link { color: #006399; text-decoration: underline; } </style>
</head> </head>
<body bgcolor="#ffffff" text="#3b3f44" link="#3f76ff" yahoo="fix" style="background-color: #ffffff" > <body bgcolor="#ffffff" text="#3b3f44" link="#006399" yahoo="fix" style="background-color: #ffffff" >
<table cellspacing="0" cellpadding="0" border="0" role="presentation" class="nl2go-body-table" width="100%" style="background-color: #ffffff; width: 100%" > <table cellspacing="0" cellpadding="0" border="0" role="presentation" class="nl2go-body-table" width="100%" style="background-color: #ffffff; width: 100%" >
<tr> <tr>
<td> <td>
@@ -41,7 +41,7 @@
<td class="r8-c" align="left"> <td class="r8-c" align="left">
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="150" class="r9-o" style=" table-layout: fixed; width: 150px; " > <table cellspacing="0" cellpadding="0" border="0" role="presentation" width="150" class="r9-o" style=" table-layout: fixed; width: 150px; " >
<tr> <tr>
<td style=" font-size: 0px; line-height: 0px; " > <img src="https://img.mailinblue.com/5942152/images/content_library/original/64708f00b503b149db7ff135.png" width="150" border="0" style=" display: block; width: 100%; " /> </td> <td style=" font-size: 0px; line-height: 0px; " > <img src="https://media.docs.plane.so/logo/new-logo-white.png" width="150" border="0" style=" display: block; width: 100%; " /> </td>
</tr> </tr>
</table> </table>
</td> </td>
@@ -94,9 +94,9 @@
</tr> </tr>
<tr> <tr>
<td class="r13-c" align="center" style=" align: center; padding-bottom: 15px; padding-top: 15px; valign: top; " > <td class="r13-c" align="center" style=" align: center; padding-bottom: 15px; padding-top: 15px; valign: top; " >
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="285" class="r14-o" style=" background-color: #3f76ff; border-collapse: separate; border-color: #3f76ff; border-radius: 4px; border-style: solid; border-width: 0px; table-layout: fixed; width: 285px; " > <table cellspacing="0" cellpadding="0" border="0" role="presentation" width="285" class="r14-o" style=" background-color: #006399; border-collapse: separate; border-color: #006399; border-radius: 4px; border-style: solid; border-width: 0px; table-layout: fixed; width: 285px; " >
<tr> <tr>
<td height="18" align="center" valign="top" class="r15-i nl2go-default-textstyle" style=" word-break: break-word; background-color: #3f76ff; border-radius: 4px; color: #ffffff; font-family: georgia, serif; font-size: 16px; font-style: normal; line-height: 1.15; padding-bottom: 12px; padding-top: 12px; text-align: center; " > <a href="{{forgot_password_url}}" class="r16-r default-button" target="_blank" data-btn="1" style=" font-style: normal; font-weight: normal; line-height: 1.15; text-decoration: none; word-break: break-word; word-wrap: break-word; display: block; -webkit-text-size-adjust: none; color: #ffffff; font-family: arial, helvetica, sans-serif; font-size: 16px; " > <span>Reset password</span></a > </td> <td height="18" align="center" valign="top" class="r15-i nl2go-default-textstyle" style=" word-break: break-word; background-color: #006399; border-radius: 4px; color: #ffffff; font-family: georgia, serif; font-size: 16px; font-style: normal; line-height: 1.15; padding-bottom: 12px; padding-top: 12px; text-align: center; " > <a href="{{forgot_password_url}}" class="r16-r default-button" target="_blank" data-btn="1" style=" font-style: normal; font-weight: normal; line-height: 1.15; text-decoration: none; word-break: break-word; word-wrap: break-word; display: block; -webkit-text-size-adjust: none; color: #ffffff; font-family: arial, helvetica, sans-serif; font-size: 16px; " > <span>Reset password</span></a > </td>
</tr> </tr>
</table> </table>
</td> </td>
@@ -187,7 +187,7 @@
<td class="nl2go-responsive-hide" width="2" style=" font-size: 0px; line-height: 1px; background-color: #efefef; " > ­ </td> <td class="nl2go-responsive-hide" width="2" style=" font-size: 0px; line-height: 1px; background-color: #efefef; " > ­ </td>
<td align="left" valign="top" class="r22-i nl2go-default-textstyle" style=" color: #3b3f44; font-family: georgia, serif; font-size: 16px; word-break: break-word; line-height: 1.5; padding-bottom: 5px; padding-left: 5px; padding-right: 5px; padding-top: 5px; text-align: left; " > <td align="left" valign="top" class="r22-i nl2go-default-textstyle" style=" color: #3b3f44; font-family: georgia, serif; font-size: 16px; word-break: break-word; line-height: 1.5; padding-bottom: 5px; padding-left: 5px; padding-right: 5px; padding-top: 5px; text-align: left; " >
<div> <div>
<p style="margin: 0"> <span style="font-size: 13px" >Despite our popularity, we are humbly early-stage. We are shipping fast, so please reach out to us with feature requests, major and minor nits, and anything else you find missing. We read every </span ><a href="https://discord.com/channels/1031547764020084846/1094927053867995176" title="Plane Support on Discod" target="_blank" style=" color: #3f76ff; text-decoration: underline; " ><span style="font-size: 13px" >message</span ></a ><span style="font-size: 13px" >, </span ><a href="http://twitter.com/planepowers" title="@planepowers" target="_blank" style=" color: #3f76ff; text-decoration: underline; " ><span style="font-size: 13px" >tweet</span ></a ><span style="font-size: 13px" >, and </span ><a href="https://github.com/makeplane/plane/issues" title="Plane's GitHub conversations" target="_blank" style=" color: #3f76ff; text-decoration: underline; " ><span style="font-size: 13px" >conversation</span ></a ><span style="font-size: 13px" > and update </span ><a href="https://plane.sh/plane/0b170a1c-0e55-47cb-9307-ea49a05672b5?board=kanban" title="Plane's roadmap" target="_blank" style=" color: #3f76ff; text-decoration: underline; " ><span style="font-size: 13px" >our public roadmap</span ></a ><span style="font-size: 13px" >.</span > </p> <p style="margin: 0"> <span style="font-size: 13px" >Despite our popularity, we are humbly early-stage. We are shipping fast, so please reach out to us with feature requests, major and minor nits, and anything else you find missing. We read every </span ><a href="https://discord.com/channels/1031547764020084846/1094927053867995176" title="Plane Support on Discod" target="_blank" style=" color: #006399; text-decoration: underline; " ><span style="font-size: 13px" >message</span ></a ><span style="font-size: 13px" >, </span ><a href="http://twitter.com/planepowers" title="@planepowers" target="_blank" style=" color: #006399; text-decoration: underline; " ><span style="font-size: 13px" >tweet</span ></a ><span style="font-size: 13px" >, and </span ><a href="https://github.com/makeplane/plane/issues" title="Plane's GitHub conversations" target="_blank" style=" color: #006399; text-decoration: underline; " ><span style="font-size: 13px" >conversation</span ></a ><span style="font-size: 13px" > and update </span ><a href="https://plane.sh/plane/0b170a1c-0e55-47cb-9307-ea49a05672b5?board=kanban" title="Plane's roadmap" target="_blank" style=" color: #006399; text-decoration: underline; " ><span style="font-size: 13px" >our public roadmap</span ></a ><span style="font-size: 13px" >.</span > </p>
</div> </div>
</td> </td>
<td class="nl2go-responsive-hide" width="2" style=" font-size: 0px; line-height: 1px; background-color: #efefef; " > ­ </td> <td class="nl2go-responsive-hide" width="2" style=" font-size: 0px; line-height: 1px; background-color: #efefef; " > ­ </td>
@@ -236,7 +236,7 @@
<th width="40" class="r26-c mobshow resp-table" style=" font-weight: normal; " > <th width="40" class="r26-c mobshow resp-table" style=" font-weight: normal; " >
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" class="r27-o" style=" table-layout: fixed; width: 100%; " > <table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" class="r27-o" style=" table-layout: fixed; width: 100%; " >
<tr> <tr>
<td class="r19-i" style=" font-size: 0px; line-height: 0px; padding-bottom: 5px; padding-top: 5px; " > <a href="https://github.com/makeplane" target="_blank" style=" color: #3f76ff; text-decoration: underline; " > <img src="https://creative-assets.mailinblue.com/editor/social-icons/rounded_colored/github_32px.png" width="32" border="0" style=" display: block; width: 100%; " /></a> </td> <td class="r19-i" style=" font-size: 0px; line-height: 0px; padding-bottom: 5px; padding-top: 5px; " > <a href="https://github.com/makeplane" target="_blank" style=" color: #006399; text-decoration: underline; " > <img src="https://creative-assets.mailinblue.com/editor/social-icons/rounded_colored/github_32px.png" width="32" border="0" style=" display: block; width: 100%; " /></a> </td>
<td class="nl2go-responsive-hide" width="8" style=" font-size: 0px; line-height: 1px; " > ­ </td> <td class="nl2go-responsive-hide" width="8" style=" font-size: 0px; line-height: 1px; " > ­ </td>
</tr> </tr>
</table> </table>
@@ -244,7 +244,7 @@
<th width="40" class="r26-c mobshow resp-table" style=" font-weight: normal; " > <th width="40" class="r26-c mobshow resp-table" style=" font-weight: normal; " >
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" class="r27-o" style=" table-layout: fixed; width: 100%; " > <table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" class="r27-o" style=" table-layout: fixed; width: 100%; " >
<tr> <tr>
<td class="r19-i" style=" font-size: 0px; line-height: 0px; padding-bottom: 5px; padding-top: 5px; " > <a href="https://www.linkedin.com/company/planepowers/" target="_blank" style=" color: #3f76ff; text-decoration: underline; " > <img src="https://creative-assets.mailinblue.com/editor/social-icons/rounded_colored/linkedin_32px.png" width="32" border="0" style=" display: block; width: 100%; " /></a> </td> <td class="r19-i" style=" font-size: 0px; line-height: 0px; padding-bottom: 5px; padding-top: 5px; " > <a href="https://www.linkedin.com/company/planepowers/" target="_blank" style=" color: #006399; text-decoration: underline; " > <img src="https://creative-assets.mailinblue.com/editor/social-icons/rounded_colored/linkedin_32px.png" width="32" border="0" style=" display: block; width: 100%; " /></a> </td>
<td class="nl2go-responsive-hide" width="8" style=" font-size: 0px; line-height: 1px; " > ­ </td> <td class="nl2go-responsive-hide" width="8" style=" font-size: 0px; line-height: 1px; " > ­ </td>
</tr> </tr>
</table> </table>
@@ -252,7 +252,7 @@
<th width="40" class="r26-c mobshow resp-table" style=" font-weight: normal; " > <th width="40" class="r26-c mobshow resp-table" style=" font-weight: normal; " >
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" class="r27-o" style=" table-layout: fixed; width: 100%; " > <table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" class="r27-o" style=" table-layout: fixed; width: 100%; " >
<tr> <tr>
<td class="r19-i" style=" font-size: 0px; line-height: 0px; padding-bottom: 5px; padding-top: 5px; " > <a href="https://twitter.com/planepowers" target="_blank" style=" color: #3f76ff; text-decoration: underline; " > <img src="https://creative-assets.mailinblue.com/editor/social-icons/rounded_colored/twitter_32px.png" width="32" border="0" style=" display: block; width: 100%; " /></a> </td> <td class="r19-i" style=" font-size: 0px; line-height: 0px; padding-bottom: 5px; padding-top: 5px; " > <a href="https://twitter.com/planepowers" target="_blank" style=" color: #006399; text-decoration: underline; " > <img src="https://creative-assets.mailinblue.com/editor/social-icons/rounded_colored/twitter_32px.png" width="32" border="0" style=" display: block; width: 100%; " /></a> </td>
<td class="nl2go-responsive-hide" width="8" style=" font-size: 0px; line-height: 1px; " > ­ </td> <td class="nl2go-responsive-hide" width="8" style=" font-size: 0px; line-height: 1px; " > ­ </td>
</tr> </tr>
</table> </table>
@@ -260,7 +260,7 @@
<th width="32" class="r26-c mobshow resp-table" style=" font-weight: normal; " > <th width="32" class="r26-c mobshow resp-table" style=" font-weight: normal; " >
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" class="r28-o" style=" table-layout: fixed; width: 100%; " > <table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" class="r28-o" style=" table-layout: fixed; width: 100%; " >
<tr> <tr>
<td class="r19-i" style=" font-size: 0px; line-height: 0px; padding-bottom: 5px; padding-top: 5px; " > <a href="https://plane.so/" target="_blank" style=" color: #3f76ff; text-decoration: underline; " > <img src="https://creative-assets.mailinblue.com/editor/social-icons/rounded_colored/website_32px.png" width="32" border="0" style=" display: block; width: 100%; " /></a> </td> <td class="r19-i" style=" font-size: 0px; line-height: 0px; padding-bottom: 5px; padding-top: 5px; " > <a href="https://plane.so/" target="_blank" style=" color: #006399; text-decoration: underline; " > <img src="https://creative-assets.mailinblue.com/editor/social-icons/rounded_colored/website_32px.png" width="32" border="0" style=" display: block; width: 100%; " /></a> </td>
</tr> </tr>
</table> </table>
</th> </th>

View File

@@ -9,7 +9,7 @@
<style type="text/css" emogrify="no"> #outlook a { padding: 0; } .ExternalClass { width: 100%; } .ExternalClass, .ExternalClass p, .ExternalClass span, .ExternalClass font, .ExternalClass td, .ExternalClass div { line-height: 100%; } table td { border-collapse: collapse; mso-line-height-rule: exactly; } .editable.image { font-size: 0 !important; line-height: 0 !important; } .nl2go_preheader { display: none !important; mso-hide: all !important; mso-line-height-rule: exactly; visibility: hidden !important; line-height: 0px !important; font-size: 0px !important; } body { width: 100% !important; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; margin: 0; padding: 0; } img { outline: none; text-decoration: none; -ms-interpolation-mode: bicubic; } a img { border: none; } table { border-collapse: collapse; mso-table-lspace: 0pt; mso-table-rspace: 0pt; } th { font-weight: normal; text-align: left; } *[class="gmail-fix"] { display: none !important; } </style> <style type="text/css" emogrify="no"> #outlook a { padding: 0; } .ExternalClass { width: 100%; } .ExternalClass, .ExternalClass p, .ExternalClass span, .ExternalClass font, .ExternalClass td, .ExternalClass div { line-height: 100%; } table td { border-collapse: collapse; mso-line-height-rule: exactly; } .editable.image { font-size: 0 !important; line-height: 0 !important; } .nl2go_preheader { display: none !important; mso-hide: all !important; mso-line-height-rule: exactly; visibility: hidden !important; line-height: 0px !important; font-size: 0px !important; } body { width: 100% !important; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; margin: 0; padding: 0; } img { outline: none; text-decoration: none; -ms-interpolation-mode: bicubic; } a img { border: none; } table { border-collapse: collapse; mso-table-lspace: 0pt; mso-table-rspace: 0pt; } th { font-weight: normal; text-align: left; } *[class="gmail-fix"] { display: none !important; } </style>
<style type="text/css" emogrify="no"> @media (max-width: 600px) { .gmx-killpill { content: " \03D1"; } } </style> <style type="text/css" emogrify="no"> @media (max-width: 600px) { .gmx-killpill { content: " \03D1"; } } </style>
<style type="text/css" emogrify="no"> @media (max-width: 600px) { .gmx-killpill { content: " \03D1"; } .r0-o { border-style: solid !important; margin: 0 auto 0 auto !important; width: 320px !important; } .r1-i { background-color: #ffffff !important; } .r2-c { box-sizing: border-box !important; text-align: center !important; valign: top !important; width: 100% !important; } .r3-o { border-style: solid !important; margin: 0 auto 0 auto !important; width: 100% !important; } .r4-i { padding-bottom: 20px !important; padding-left: 15px !important; padding-right: 15px !important; padding-top: 20px !important; } .r5-c { box-sizing: border-box !important; display: block !important; valign: top !important; width: 100% !important; } .r6-o { border-style: solid !important; width: 100% !important; } .r7-i { padding-left: 0px !important; padding-right: 0px !important; } .r8-c { box-sizing: border-box !important; text-align: left !important; valign: top !important; width: 100% !important; } .r9-o { border-style: solid !important; margin: 0 auto 0 0 !important; width: 100% !important; } .r10-i { padding-bottom: 15px !important; padding-left: 15px !important; padding-right: 15px !important; padding-top: 15px !important; } .r11-i { padding-bottom: 10px !important; padding-top: 10px !important; text-align: left !important; } .r12-i { padding-bottom: 10px !important; padding-left: 0px !important; padding-right: 0px !important; padding-top: 10px !important; } .r13-o { border-bottom-color: #d9e4ff !important; border-bottom-width: 1px !important; border-left-color: #d9e4ff !important; border-left-width: 1px !important; border-right-color: #d9e4ff !important; border-right-width: 1px !important; border-style: solid !important; border-top-color: #d9e4ff !important; border-top-width: 1px !important; margin: 0 auto 0 0 !important; width: 100% !important; } .r14-i { background-color: #ecf1ff !important; padding-bottom: 10px !important; padding-left: 10px !important; padding-right: 10px !important; padding-top: 10px !important; text-align: left !important; } .r15-o { border-bottom-color: #efefef !important; border-bottom-width: 2px !important; border-left-color: #efefef !important; border-left-width: 2px !important; border-right-color: #efefef !important; border-right-width: 2px !important; border-style: solid !important; border-top-color: #efefef !important; border-top-width: 2px !important; margin: 0 auto 0 0 !important; width: 100% !important; } .r16-i { padding-bottom: 5px !important; padding-left: 5px !important; padding-right: 5px !important; padding-top: 5px !important; text-align: left !important; } .r17-i { padding-bottom: 5px !important; padding-left: 15px !important; padding-right: 15px !important; padding-top: 5px !important; } .r18-c { box-sizing: border-box !important; text-align: center !important; width: 100% !important; } .r19-c { box-sizing: border-box !important; width: 100% !important; } .r20-i { font-size: 0px !important; padding-bottom: 10px !important; padding-left: 65px !important; padding-right: 65px !important; padding-top: 10px !important; } .r21-c { box-sizing: border-box !important; width: 32px !important; } .r22-o { border-style: solid !important; margin-right: 8px !important; width: 32px !important; } .r23-i { padding-bottom: 5px !important; padding-top: 5px !important; } .r24-o { border-style: solid !important; margin-right: 0px !important; width: 32px !important; } .r25-i { padding-bottom: 0px !important; padding-top: 5px !important; text-align: center !important; } body { -webkit-text-size-adjust: none; } .nl2go-responsive-hide { display: none; } .nl2go-body-table { min-width: unset !important; } .mobshow { height: auto !important; overflow: visible !important; max-height: unset !important; visibility: visible !important; border: none !important; } .resp-table { display: inline-table !important; } .magic-resp { display: table-cell !important; } } </style> <style type="text/css" emogrify="no"> @media (max-width: 600px) { .gmx-killpill { content: " \03D1"; } .r0-o { border-style: solid !important; margin: 0 auto 0 auto !important; width: 320px !important; } .r1-i { background-color: #ffffff !important; } .r2-c { box-sizing: border-box !important; text-align: center !important; valign: top !important; width: 100% !important; } .r3-o { border-style: solid !important; margin: 0 auto 0 auto !important; width: 100% !important; } .r4-i { padding-bottom: 20px !important; padding-left: 15px !important; padding-right: 15px !important; padding-top: 20px !important; } .r5-c { box-sizing: border-box !important; display: block !important; valign: top !important; width: 100% !important; } .r6-o { border-style: solid !important; width: 100% !important; } .r7-i { padding-left: 0px !important; padding-right: 0px !important; } .r8-c { box-sizing: border-box !important; text-align: left !important; valign: top !important; width: 100% !important; } .r9-o { border-style: solid !important; margin: 0 auto 0 0 !important; width: 100% !important; } .r10-i { padding-bottom: 15px !important; padding-left: 15px !important; padding-right: 15px !important; padding-top: 15px !important; } .r11-i { padding-bottom: 10px !important; padding-top: 10px !important; text-align: left !important; } .r12-i { padding-bottom: 10px !important; padding-left: 0px !important; padding-right: 0px !important; padding-top: 10px !important; } .r13-o { border-bottom-color: #d9e4ff !important; border-bottom-width: 1px !important; border-left-color: #d9e4ff !important; border-left-width: 1px !important; border-right-color: #d9e4ff !important; border-right-width: 1px !important; border-style: solid !important; border-top-color: #d9e4ff !important; border-top-width: 1px !important; margin: 0 auto 0 0 !important; width: 100% !important; } .r14-i { background-color: #ecf1ff !important; padding-bottom: 10px !important; padding-left: 10px !important; padding-right: 10px !important; padding-top: 10px !important; text-align: left !important; } .r15-o { border-bottom-color: #efefef !important; border-bottom-width: 2px !important; border-left-color: #efefef !important; border-left-width: 2px !important; border-right-color: #efefef !important; border-right-width: 2px !important; border-style: solid !important; border-top-color: #efefef !important; border-top-width: 2px !important; margin: 0 auto 0 0 !important; width: 100% !important; } .r16-i { padding-bottom: 5px !important; padding-left: 5px !important; padding-right: 5px !important; padding-top: 5px !important; text-align: left !important; } .r17-i { padding-bottom: 5px !important; padding-left: 15px !important; padding-right: 15px !important; padding-top: 5px !important; } .r18-c { box-sizing: border-box !important; text-align: center !important; width: 100% !important; } .r19-c { box-sizing: border-box !important; width: 100% !important; } .r20-i { font-size: 0px !important; padding-bottom: 10px !important; padding-left: 65px !important; padding-right: 65px !important; padding-top: 10px !important; } .r21-c { box-sizing: border-box !important; width: 32px !important; } .r22-o { border-style: solid !important; margin-right: 8px !important; width: 32px !important; } .r23-i { padding-bottom: 5px !important; padding-top: 5px !important; } .r24-o { border-style: solid !important; margin-right: 0px !important; width: 32px !important; } .r25-i { padding-bottom: 0px !important; padding-top: 5px !important; text-align: center !important; } body { -webkit-text-size-adjust: none; } .nl2go-responsive-hide { display: none; } .nl2go-body-table { min-width: unset !important; } .mobshow { height: auto !important; overflow: visible !important; max-height: unset !important; visibility: visible !important; border: none !important; } .resp-table { display: inline-table !important; } .magic-resp { display: table-cell !important; } } </style>
<style type="text/css"> p, h1, h2, h3, h4, ol, ul { margin: 0; } a, a:link { color: #3f76ff; text-decoration: underline; } .nl2go-default-textstyle { color: #3b3f44; font-family: georgia, serif; font-size: 16px; line-height: 1.5; word-break: break-word; } .default-button { color: #ffffff; font-family: georgia, serif; font-size: 16px; font-style: normal; font-weight: normal; line-height: 1.15; text-decoration: none; word-break: break-word; } .default-heading1 { color: #1f2d3d; font-family: arial, helvetica, sans-serif; font-size: 36px; word-break: break-word; } .default-heading2 { color: #1f2d3d; font-family: arial, helvetica, sans-serif; font-size: 32px; word-break: break-word; } .default-heading3 { color: #1f2d3d; font-family: arial, helvetica, sans-serif; font-size: 24px; word-break: break-word; } .default-heading4 { color: #1f2d3d; font-family: arial, helvetica, sans-serif; font-size: 18px; word-break: break-word; } a[x-apple-data-detectors] { color: inherit !important; text-decoration: inherit !important; font-size: inherit !important; font-family: inherit !important; font-weight: inherit !important; line-height: inherit !important; } .no-show-for-you { border: none; display: none; float: none; font-size: 0; height: 0; line-height: 0; max-height: 0; mso-hide: all; overflow: hidden; table-layout: fixed; visibility: hidden; width: 0; } </style> <style type="text/css"> p, h1, h2, h3, h4, ol, ul { margin: 0; } a, a:link { color: #006399; text-decoration: underline; } .nl2go-default-textstyle { color: #3b3f44; font-family: georgia, serif; font-size: 16px; line-height: 1.5; word-break: break-word; } .default-button { color: #ffffff; font-family: georgia, serif; font-size: 16px; font-style: normal; font-weight: normal; line-height: 1.15; text-decoration: none; word-break: break-word; } .default-heading1 { color: #1f2d3d; font-family: arial, helvetica, sans-serif; font-size: 36px; word-break: break-word; } .default-heading2 { color: #1f2d3d; font-family: arial, helvetica, sans-serif; font-size: 32px; word-break: break-word; } .default-heading3 { color: #1f2d3d; font-family: arial, helvetica, sans-serif; font-size: 24px; word-break: break-word; } .default-heading4 { color: #1f2d3d; font-family: arial, helvetica, sans-serif; font-size: 18px; word-break: break-word; } a[x-apple-data-detectors] { color: inherit !important; text-decoration: inherit !important; font-size: inherit !important; font-family: inherit !important; font-weight: inherit !important; line-height: inherit !important; } .no-show-for-you { border: none; display: none; float: none; font-size: 0; height: 0; line-height: 0; max-height: 0; mso-hide: all; overflow: hidden; table-layout: fixed; visibility: hidden; width: 0; } </style>
<!--[if mso ]> <!--[if mso ]>
<xml> <xml>
<o:OfficeDocumentSettings> <o:OfficeDocumentSettings>
@@ -18,9 +18,9 @@
</o:OfficeDocumentSettings> </o:OfficeDocumentSettings>
</xml> </xml>
<! [endif]--> <! [endif]-->
<style type="text/css"> a:link { color: #3f76ff; text-decoration: underline; } </style> <style type="text/css"> a:link { color: #006399; text-decoration: underline; } </style>
</head> </head>
<body bgcolor="#ffffff" text="#3b3f44" link="#3f76ff" yahoo="fix" style="background-color: #ffffff" > <body bgcolor="#ffffff" text="#3b3f44" link="#006399" yahoo="fix" style="background-color: #ffffff" >
<table cellspacing="0" cellpadding="0" border="0" role="presentation" class="nl2go-body-table" width="100%" style="background-color: #ffffff; width: 100%" > <table cellspacing="0" cellpadding="0" border="0" role="presentation" class="nl2go-body-table" width="100%" style="background-color: #ffffff; width: 100%" >
<tr> <tr>
<td> <td>
@@ -41,7 +41,7 @@
<td class="r8-c" align="left"> <td class="r8-c" align="left">
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="150" class="r9-o" style=" table-layout: fixed; width: 150px; " > <table cellspacing="0" cellpadding="0" border="0" role="presentation" width="150" class="r9-o" style=" table-layout: fixed; width: 150px; " >
<tr> <tr>
<td style=" font-size: 0px; line-height: 0px; " > <img src="https://img.mailinblue.com/5942152/images/content_library/original/64708f00b503b149db7ff135.png" width="150" border="0" style=" display: block; width: 100%; " /> </td> <td style=" font-size: 0px; line-height: 0px; " > <img src="https://media.docs.plane.so/logo/new-logo-white.png" width="150" border="0" style=" display: block; width: 100%; " /> </td>
</tr> </tr>
</table> </table>
</td> </td>
@@ -80,7 +80,7 @@
</td> </td>
</tr> </tr>
</table> </table>
</td> </td>
</tr> </tr>
</table> </table>
</th> </th>
@@ -145,7 +145,7 @@
<td class="nl2go-responsive-hide" width="2" style=" font-size: 0px; line-height: 1px; background-color: #efefef; " > ­ </td> <td class="nl2go-responsive-hide" width="2" style=" font-size: 0px; line-height: 1px; background-color: #efefef; " > ­ </td>
<td align="left" valign="top" class="r16-i nl2go-default-textstyle" style=" color: #3b3f44; font-family: georgia, serif; font-size: 16px; word-break: break-word; line-height: 1.5; padding-bottom: 5px; padding-left: 5px; padding-right: 5px; padding-top: 5px; text-align: left; " > <td align="left" valign="top" class="r16-i nl2go-default-textstyle" style=" color: #3b3f44; font-family: georgia, serif; font-size: 16px; word-break: break-word; line-height: 1.5; padding-bottom: 5px; padding-left: 5px; padding-right: 5px; padding-top: 5px; text-align: left; " >
<div> <div>
<p style="margin: 0"> <span style="font-size: 13px" >Despite our popularity, we are humbly early-stage. We are shipping fast, so please reach out to us with feature requests, major and minor nits, and anything else you find missing. We read every </span ><a href="https://discord.com/channels/1031547764020084846/1094927053867995176" title="Plane Support on Discod" target="_blank" style=" color: #3f76ff; text-decoration: underline; " ><span style="font-size: 13px" >message</span ></a ><span style="font-size: 13px" >, </span ><a href="http://twitter.com/planepowers" title="@planepowers" target="_blank" style=" color: #3f76ff; text-decoration: underline; " ><span style="font-size: 13px" >tweet</span ></a ><span style="font-size: 13px" >, and </span ><a href="https://github.com/makeplane/plane/issues" title="Plane's GitHub conversations" target="_blank" style=" color: #3f76ff; text-decoration: underline; " ><span style="font-size: 13px" >conversation</span ></a ><span style="font-size: 13px" > and update </span ><a href="https://plane.sh/plane/0b170a1c-0e55-47cb-9307-ea49a05672b5?board=kanban" title="Plane's roadmap" target="_blank" style=" color: #3f76ff; text-decoration: underline; " ><span style="font-size: 13px" >our public roadmap</span ></a ><span style="font-size: 13px" >.</span > </p> <p style="margin: 0"> <span style="font-size: 13px" >Despite our popularity, we are humbly early-stage. We are shipping fast, so please reach out to us with feature requests, major and minor nits, and anything else you find missing. We read every </span ><a href="https://discord.com/channels/1031547764020084846/1094927053867995176" title="Plane Support on Discod" target="_blank" style=" color: #006399; text-decoration: underline; " ><span style="font-size: 13px" >message</span ></a ><span style="font-size: 13px" >, </span ><a href="http://twitter.com/planepowers" title="@planepowers" target="_blank" style=" color: #006399; text-decoration: underline; " ><span style="font-size: 13px" >tweet</span ></a ><span style="font-size: 13px" >, and </span ><a href="https://github.com/makeplane/plane/issues" title="Plane's GitHub conversations" target="_blank" style=" color: #006399; text-decoration: underline; " ><span style="font-size: 13px" >conversation</span ></a ><span style="font-size: 13px" > and update </span ><a href="https://plane.sh/plane/0b170a1c-0e55-47cb-9307-ea49a05672b5?board=kanban" title="Plane's roadmap" target="_blank" style=" color: #006399; text-decoration: underline; " ><span style="font-size: 13px" >our public roadmap</span ></a ><span style="font-size: 13px" >.</span > </p>
</div> </div>
</td> </td>
<td class="nl2go-responsive-hide" width="2" style=" font-size: 0px; line-height: 1px; background-color: #efefef; " > ­ </td> <td class="nl2go-responsive-hide" width="2" style=" font-size: 0px; line-height: 1px; background-color: #efefef; " > ­ </td>
@@ -194,7 +194,7 @@
<th width="40" class="r21-c mobshow resp-table" style=" font-weight: normal; " > <th width="40" class="r21-c mobshow resp-table" style=" font-weight: normal; " >
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" class="r22-o" style=" table-layout: fixed; width: 100%; " > <table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" class="r22-o" style=" table-layout: fixed; width: 100%; " >
<tr> <tr>
<td class="r23-i" style=" font-size: 0px; line-height: 0px; padding-bottom: 5px; padding-top: 5px; " > <a href="https://github.com/makeplane" target="_blank" style=" color: #3f76ff; text-decoration: underline; " > <img src="https://creative-assets.mailinblue.com/editor/social-icons/rounded_colored/github_32px.png" width="32" border="0" style=" display: block; width: 100%; " /></a> </td> <td class="r23-i" style=" font-size: 0px; line-height: 0px; padding-bottom: 5px; padding-top: 5px; " > <a href="https://github.com/makeplane" target="_blank" style=" color: #006399; text-decoration: underline; " > <img src="https://creative-assets.mailinblue.com/editor/social-icons/rounded_colored/github_32px.png" width="32" border="0" style=" display: block; width: 100%; " /></a> </td>
<td class="nl2go-responsive-hide" width="8" style=" font-size: 0px; line-height: 1px; " > ­ </td> <td class="nl2go-responsive-hide" width="8" style=" font-size: 0px; line-height: 1px; " > ­ </td>
</tr> </tr>
</table> </table>
@@ -202,7 +202,7 @@
<th width="40" class="r21-c mobshow resp-table" style=" font-weight: normal; " > <th width="40" class="r21-c mobshow resp-table" style=" font-weight: normal; " >
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" class="r22-o" style=" table-layout: fixed; width: 100%; " > <table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" class="r22-o" style=" table-layout: fixed; width: 100%; " >
<tr> <tr>
<td class="r23-i" style=" font-size: 0px; line-height: 0px; padding-bottom: 5px; padding-top: 5px; " > <a href="https://www.linkedin.com/company/planepowers/" target="_blank" style=" color: #3f76ff; text-decoration: underline; " > <img src="https://creative-assets.mailinblue.com/editor/social-icons/rounded_colored/linkedin_32px.png" width="32" border="0" style=" display: block; width: 100%; " /></a> </td> <td class="r23-i" style=" font-size: 0px; line-height: 0px; padding-bottom: 5px; padding-top: 5px; " > <a href="https://www.linkedin.com/company/planepowers/" target="_blank" style=" color: #006399; text-decoration: underline; " > <img src="https://creative-assets.mailinblue.com/editor/social-icons/rounded_colored/linkedin_32px.png" width="32" border="0" style=" display: block; width: 100%; " /></a> </td>
<td class="nl2go-responsive-hide" width="8" style=" font-size: 0px; line-height: 1px; " > ­ </td> <td class="nl2go-responsive-hide" width="8" style=" font-size: 0px; line-height: 1px; " > ­ </td>
</tr> </tr>
</table> </table>
@@ -210,7 +210,7 @@
<th width="40" class="r21-c mobshow resp-table" style=" font-weight: normal; " > <th width="40" class="r21-c mobshow resp-table" style=" font-weight: normal; " >
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" class="r22-o" style=" table-layout: fixed; width: 100%; " > <table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" class="r22-o" style=" table-layout: fixed; width: 100%; " >
<tr> <tr>
<td class="r23-i" style=" font-size: 0px; line-height: 0px; padding-bottom: 5px; padding-top: 5px; " > <a href="https://twitter.com/planepowers" target="_blank" style=" color: #3f76ff; text-decoration: underline; " > <img src="https://creative-assets.mailinblue.com/editor/social-icons/rounded_colored/twitter_32px.png" width="32" border="0" style=" display: block; width: 100%; " /></a> </td> <td class="r23-i" style=" font-size: 0px; line-height: 0px; padding-bottom: 5px; padding-top: 5px; " > <a href="https://twitter.com/planepowers" target="_blank" style=" color: #006399; text-decoration: underline; " > <img src="https://creative-assets.mailinblue.com/editor/social-icons/rounded_colored/twitter_32px.png" width="32" border="0" style=" display: block; width: 100%; " /></a> </td>
<td class="nl2go-responsive-hide" width="8" style=" font-size: 0px; line-height: 1px; " > ­ </td> <td class="nl2go-responsive-hide" width="8" style=" font-size: 0px; line-height: 1px; " > ­ </td>
</tr> </tr>
</table> </table>
@@ -218,7 +218,7 @@
<th width="32" class="r21-c mobshow resp-table" style=" font-weight: normal; " > <th width="32" class="r21-c mobshow resp-table" style=" font-weight: normal; " >
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" class="r24-o" style=" table-layout: fixed; width: 100%; " > <table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" class="r24-o" style=" table-layout: fixed; width: 100%; " >
<tr> <tr>
<td class="r23-i" style=" font-size: 0px; line-height: 0px; padding-bottom: 5px; padding-top: 5px; " > <a href="https://plane.so/" target="_blank" style=" color: #3f76ff; text-decoration: underline; " > <img src="https://creative-assets.mailinblue.com/editor/social-icons/rounded_colored/website_32px.png" width="32" border="0" style=" display: block; width: 100%; " /></a> </td> <td class="r23-i" style=" font-size: 0px; line-height: 0px; padding-bottom: 5px; padding-top: 5px; " > <a href="https://plane.so/" target="_blank" style=" color: #006399; text-decoration: underline; " > <img src="https://creative-assets.mailinblue.com/editor/social-icons/rounded_colored/website_32px.png" width="32" border="0" style=" display: block; width: 100%; " /></a> </td>
</tr> </tr>
</table> </table>
</th> </th>

View File

@@ -8,7 +8,7 @@
<title> {{ first_name }} invited you to join {{ project_name }} on Plane </title> <title> {{ first_name }} invited you to join {{ project_name }} on Plane </title>
<style type="text/css" emogrify="no"> #outlook a { padding: 0; } .ExternalClass { width: 100%; } .ExternalClass, .ExternalClass p, .ExternalClass span, .ExternalClass font, .ExternalClass td, .ExternalClass div { line-height: 100%; } table td { border-collapse: collapse; mso-line-height-rule: exactly; } .editable.image { font-size: 0 !important; line-height: 0 !important; } .nl2go_preheader { display: none !important; mso-hide: all !important; mso-line-height-rule: exactly; visibility: hidden !important; line-height: 0px !important; font-size: 0px !important; } body { width: 100% !important; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; margin: 0; padding: 0; } img { outline: none; text-decoration: none; -ms-interpolation-mode: bicubic; } a img { border: none; } table { border-collapse: collapse; mso-table-lspace: 0pt; mso-table-rspace: 0pt; } th { font-weight: normal; text-align: left; } *[class="gmail-fix"] { display: none !important; } </style> <style type="text/css" emogrify="no"> #outlook a { padding: 0; } .ExternalClass { width: 100%; } .ExternalClass, .ExternalClass p, .ExternalClass span, .ExternalClass font, .ExternalClass td, .ExternalClass div { line-height: 100%; } table td { border-collapse: collapse; mso-line-height-rule: exactly; } .editable.image { font-size: 0 !important; line-height: 0 !important; } .nl2go_preheader { display: none !important; mso-hide: all !important; mso-line-height-rule: exactly; visibility: hidden !important; line-height: 0px !important; font-size: 0px !important; } body { width: 100% !important; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; margin: 0; padding: 0; } img { outline: none; text-decoration: none; -ms-interpolation-mode: bicubic; } a img { border: none; } table { border-collapse: collapse; mso-table-lspace: 0pt; mso-table-rspace: 0pt; } th { font-weight: normal; text-align: left; } *[class="gmail-fix"] { display: none !important; } </style>
<style type="text/css" emogrify="no"> @media (max-width: 600px) { .gmx-killpill { content: " \03D1"; } } </style> <style type="text/css" emogrify="no"> @media (max-width: 600px) { .gmx-killpill { content: " \03D1"; } } </style>
<style type="text/css" emogrify="no"> @media (max-width: 600px) { .gmx-killpill { content: " \03D1"; } .r0-c { box-sizing: border-box !important; text-align: center !important; valign: top !important; width: 320px !important; } .r1-o { border-style: solid !important; margin: 0 auto 0 auto !important; width: 320px !important; } .r2-i { background-color: #ffffff !important; } .r3-c { box-sizing: border-box !important; text-align: center !important; valign: top !important; width: 100% !important; } .r4-o { border-style: solid !important; margin: 0 auto 0 auto !important; margin-top: 20px !important; width: 100% !important; } .r5-i { background-color: #f8f9fa !important; padding-bottom: 20px !important; padding-left: 10px !important; padding-right: 10px !important; padding-top: 20px !important; } .r6-c { box-sizing: border-box !important; display: block !important; valign: top !important; width: 100% !important; } .r7-o { border-style: solid !important; width: 100% !important; } .r8-i { padding-left: 0px !important; padding-right: 0px !important; } .r9-o { border-style: solid !important; margin: 0 auto 0 auto !important; width: 100% !important; } .r10-i { padding-bottom: 35px !important; padding-top: 15px !important; } .r11-c { box-sizing: border-box !important; text-align: left !important; valign: top !important; width: 100% !important; } .r12-o { border-style: solid !important; margin: 0 auto 0 0 !important; width: 100% !important; } .r13-i { padding-left: 20px !important; padding-right: 20px !important; padding-top: 0px !important; text-align: center !important; } .r14-o { border-style: solid !important; margin: 0 auto 0 auto !important; margin-bottom: 20px !important; margin-top: 20px !important; width: 100% !important; } .r15-i { text-align: center !important; } .r16-r { background-color: #ffffff !important; border-color: #3f76ff !important; border-radius: 4px !important; border-width: 1px !important; box-sizing: border-box; height: initial !important; padding-bottom: 7px !important; padding-left: 20px !important; padding-right: 20px !important; padding-top: 7px !important; text-align: center !important; width: 100% !important; } .r17-i { padding-bottom: 15px !important; padding-left: 20px !important; padding-right: 20px !important; padding-top: 15px !important; text-align: left !important; } .r18-i { background-color: #eff2f7 !important; padding-bottom: 20px !important; padding-left: 15px !important; padding-right: 15px !important; padding-top: 20px !important; } .r19-i { padding-bottom: 15px !important; padding-top: 15px !important; } .r20-i { color: #3b3f44 !important; padding-bottom: 0px !important; padding-top: 0px !important; text-align: center !important; } .r21-c { box-sizing: border-box !important; text-align: center !important; width: 100% !important; } .r22-c { box-sizing: border-box !important; width: 100% !important; } .r23-i { font-size: 0px !important; padding-bottom: 15px !important; padding-left: 65px !important; padding-right: 65px !important; padding-top: 15px !important; } .r24-c { box-sizing: border-box !important; width: 32px !important; } .r25-o { border-style: solid !important; margin-right: 8px !important; width: 32px !important; } .r26-i { padding-bottom: 5px !important; padding-top: 5px !important; } .r27-o { border-style: solid !important; margin-right: 0px !important; width: 32px !important; } .r28-i { color: #3b3f44 !important; padding-bottom: 15px !important; padding-top: 15px !important; text-align: center !important; } .r29-i { padding-bottom: 15px !important; padding-left: 0px !important; padding-right: 0px !important; padding-top: 0px !important; } .r30-c { box-sizing: border-box !important; text-align: center !important; valign: top !important; width: 129px !important; } .r31-o { border-style: solid !important; margin: 0 auto 0 auto !important; width: 129px !important; } body { -webkit-text-size-adjust: none; } .nl2go-responsive-hide { display: none; } .nl2go-body-table { min-width: unset !important; } .mobshow { height: auto !important; overflow: visible !important; max-height: unset !important; visibility: visible !important; border: none !important; } .resp-table { display: inline-table !important; } .magic-resp { display: table-cell !important; } } </style> <style type="text/css" emogrify="no"> @media (max-width: 600px) { .gmx-killpill { content: " \03D1"; } .r0-c { box-sizing: border-box !important; text-align: center !important; valign: top !important; width: 320px !important; } .r1-o { border-style: solid !important; margin: 0 auto 0 auto !important; width: 320px !important; } .r2-i { background-color: #ffffff !important; } .r3-c { box-sizing: border-box !important; text-align: center !important; valign: top !important; width: 100% !important; } .r4-o { border-style: solid !important; margin: 0 auto 0 auto !important; margin-top: 20px !important; width: 100% !important; } .r5-i { background-color: #f8f9fa !important; padding-bottom: 20px !important; padding-left: 10px !important; padding-right: 10px !important; padding-top: 20px !important; } .r6-c { box-sizing: border-box !important; display: block !important; valign: top !important; width: 100% !important; } .r7-o { border-style: solid !important; width: 100% !important; } .r8-i { padding-left: 0px !important; padding-right: 0px !important; } .r9-o { border-style: solid !important; margin: 0 auto 0 auto !important; width: 100% !important; } .r10-i { padding-bottom: 35px !important; padding-top: 15px !important; } .r11-c { box-sizing: border-box !important; text-align: left !important; valign: top !important; width: 100% !important; } .r12-o { border-style: solid !important; margin: 0 auto 0 0 !important; width: 100% !important; } .r13-i { padding-left: 20px !important; padding-right: 20px !important; padding-top: 0px !important; text-align: center !important; } .r14-o { border-style: solid !important; margin: 0 auto 0 auto !important; margin-bottom: 20px !important; margin-top: 20px !important; width: 100% !important; } .r15-i { text-align: center !important; } .r16-r { background-color: #ffffff !important; border-color: #006399 !important; border-radius: 4px !important; border-width: 1px !important; box-sizing: border-box; height: initial !important; padding-bottom: 7px !important; padding-left: 20px !important; padding-right: 20px !important; padding-top: 7px !important; text-align: center !important; width: 100% !important; } .r17-i { padding-bottom: 15px !important; padding-left: 20px !important; padding-right: 20px !important; padding-top: 15px !important; text-align: left !important; } .r18-i { background-color: #eff2f7 !important; padding-bottom: 20px !important; padding-left: 15px !important; padding-right: 15px !important; padding-top: 20px !important; } .r19-i { padding-bottom: 15px !important; padding-top: 15px !important; } .r20-i { color: #3b3f44 !important; padding-bottom: 0px !important; padding-top: 0px !important; text-align: center !important; } .r21-c { box-sizing: border-box !important; text-align: center !important; width: 100% !important; } .r22-c { box-sizing: border-box !important; width: 100% !important; } .r23-i { font-size: 0px !important; padding-bottom: 15px !important; padding-left: 65px !important; padding-right: 65px !important; padding-top: 15px !important; } .r24-c { box-sizing: border-box !important; width: 32px !important; } .r25-o { border-style: solid !important; margin-right: 8px !important; width: 32px !important; } .r26-i { padding-bottom: 5px !important; padding-top: 5px !important; } .r27-o { border-style: solid !important; margin-right: 0px !important; width: 32px !important; } .r28-i { color: #3b3f44 !important; padding-bottom: 15px !important; padding-top: 15px !important; text-align: center !important; } .r29-i { padding-bottom: 15px !important; padding-left: 0px !important; padding-right: 0px !important; padding-top: 0px !important; } .r30-c { box-sizing: border-box !important; text-align: center !important; valign: top !important; width: 129px !important; } .r31-o { border-style: solid !important; margin: 0 auto 0 auto !important; width: 129px !important; } body { -webkit-text-size-adjust: none; } .nl2go-responsive-hide { display: none; } .nl2go-body-table { min-width: unset !important; } .mobshow { height: auto !important; overflow: visible !important; max-height: unset !important; visibility: visible !important; border: none !important; } .resp-table { display: inline-table !important; } .magic-resp { display: table-cell !important; } } </style>
<!--[if !mso]><!--> <!--[if !mso]><!-->
<style type="text/css" emogrify="no"> @import url("https://fonts.googleapis.com/css2?family=Bitter&family=Roboto"); </style> <style type="text/css" emogrify="no"> @import url("https://fonts.googleapis.com/css2?family=Bitter&family=Roboto"); </style>
<!--<![endif]--> <!--<![endif]-->
@@ -58,7 +58,7 @@
<td height="15" style=" font-size: 15px; line-height: 15px; " > ­ </td> <td height="15" style=" font-size: 15px; line-height: 15px; " > ­ </td>
</tr> </tr>
<tr> <tr>
<td class="r10-i" style=" font-size: 0px; line-height: 0px; " > <img src="https://ik.imagekit.io/w2okwbtu2/Plane_Logo_pIhtbyIoa.png?ik-sdk-version=javascript-1.4.3&updatedAt=1670873447444" width="120" border="0" class="" style=" display: block; width: 100%; " /> </td> <td class="r10-i" style=" font-size: 0px; line-height: 0px; " > <img src="https://media.docs.plane.so/logo/new-logo-white.png" width="120" border="0" class="" style=" display: block; width: 100%; " /> </td>
</tr> </tr>
<tr class="nl2go-responsive-hide" > <tr class="nl2go-responsive-hide" >
<td height="35" style=" font-size: 35px; line-height: 35px; " > ­ </td> <td height="35" style=" font-size: 35px; line-height: 35px; " > ­ </td>
@@ -91,17 +91,17 @@
<tr> <tr>
<td height="18" align="center" valign="top" class="r15-i nl2go-default-textstyle" style=" color: #3b3f44; font-family: arial, helvetica, sans-serif; font-size: 16px; line-height: 1.5; " > <td height="18" align="center" valign="top" class="r15-i nl2go-default-textstyle" style=" color: #3b3f44; font-family: arial, helvetica, sans-serif; font-size: 16px; line-height: 1.5; " >
<!--[if mso]> <!--[if mso]>
<v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="urn:schemas-microsoft-com:office:word" href="{{invitation_url}}" style=" v-text-anchor: middle; height: 33px; width: 301px; " arcsize="12%" fillcolor="#ffffff" strokecolor="#3f76ff" strokeweight="1px" data-btn="1" > <v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="urn:schemas-microsoft-com:office:word" href="{{invitation_url}}" style=" v-text-anchor: middle; height: 33px; width: 301px; " arcsize="12%" fillcolor="#ffffff" strokecolor="#006399" strokeweight="1px" data-btn="1" >
<w:anchorlock /> <w:anchorlock />
<div style="display: none" > <div style="display: none" >
<center class="default-button" > <center class="default-button" >
<p> <span style=" color: #3f76ff; " >Accept the invite</span > </p> <p> <span style=" color: #006399; " >Accept the invite</span > </p>
</center> </center>
</div> </div>
</v:roundrect> </v:roundrect>
<![endif]--> <!--[if !mso]><!-- --> <![endif]--> <!--[if !mso]><!-- -->
<a href="{{invitation_url}}" class="r16-r default-button" target="_blank" rel="noopener noreferrer" data-btn="1" style=" font-style: normal; font-weight: bold; line-height: 1.15; text-decoration: none; border-style: solid; word-wrap: break-word; display: inline-block; -webkit-text-size-adjust: none; mso-hide: all; background-color: #ffffff; border-color: #3f76ff; border-radius: 4px; border-width: 1px; color: #ffffff; font-family: arial, helvetica, sans-serif; font-size: 16px; height: 18px; padding-bottom: 7px; padding-left: 20px; padding-right: 20px; padding-top: 7px; width: 258px; " > <a href="{{invitation_url}}" class="r16-r default-button" target="_blank" rel="noopener noreferrer" data-btn="1" style=" font-style: normal; font-weight: bold; line-height: 1.15; text-decoration: none; border-style: solid; word-wrap: break-word; display: inline-block; -webkit-text-size-adjust: none; mso-hide: all; background-color: #ffffff; border-color: #006399; border-radius: 4px; border-width: 1px; color: #ffffff; font-family: arial, helvetica, sans-serif; font-size: 16px; height: 18px; padding-bottom: 7px; padding-left: 20px; padding-right: 20px; padding-top: 7px; width: 258px; " >
<p style="margin: 0"> <span style="color: #3f76ff" >Accept the invite</span > </p> <p style="margin: 0"> <span style="color: #006399" >Accept the invite</span > </p>
</a> </a>
<!--<![endif]--> <!--<![endif]-->
</td> </td>

View File

@@ -8,8 +8,8 @@
<title>{{first_name}} has invited you to join them in {{workspace_name}} on Plane.</title> <title>{{first_name}} has invited you to join them in {{workspace_name}} on Plane.</title>
<style type="text/css" emogrify="no">#outlook a { padding:0; } .ExternalClass { width:100%; } .ExternalClass, .ExternalClass p, .ExternalClass span, .ExternalClass font, .ExternalClass td, .ExternalClass div { line-height: 100%; } table td { border-collapse: collapse; mso-line-height-rule: exactly; } .editable.image { font-size: 0 !important; line-height: 0 !important; } .nl2go_preheader { display: none !important; mso-hide:all !important; mso-line-height-rule: exactly; visibility: hidden !important; line-height: 0px !important; font-size: 0px !important; } body { width:100% !important; -webkit-text-size-adjust:100%; -ms-text-size-adjust:100%; margin:0; padding:0; } img { outline:none; text-decoration:none; -ms-interpolation-mode: bicubic; } a img { border:none; } table { border-collapse:collapse; mso-table-lspace:0pt; mso-table-rspace:0pt; } th { font-weight: normal; text-align: left; } *[class="gmail-fix"] { display: none !important; } </style> <style type="text/css" emogrify="no">#outlook a { padding:0; } .ExternalClass { width:100%; } .ExternalClass, .ExternalClass p, .ExternalClass span, .ExternalClass font, .ExternalClass td, .ExternalClass div { line-height: 100%; } table td { border-collapse: collapse; mso-line-height-rule: exactly; } .editable.image { font-size: 0 !important; line-height: 0 !important; } .nl2go_preheader { display: none !important; mso-hide:all !important; mso-line-height-rule: exactly; visibility: hidden !important; line-height: 0px !important; font-size: 0px !important; } body { width:100% !important; -webkit-text-size-adjust:100%; -ms-text-size-adjust:100%; margin:0; padding:0; } img { outline:none; text-decoration:none; -ms-interpolation-mode: bicubic; } a img { border:none; } table { border-collapse:collapse; mso-table-lspace:0pt; mso-table-rspace:0pt; } th { font-weight: normal; text-align: left; } *[class="gmail-fix"] { display: none !important; } </style>
<style type="text/css" emogrify="no"> @media (max-width: 600px) { .gmx-killpill { content: ' \03D1';} } </style> <style type="text/css" emogrify="no"> @media (max-width: 600px) { .gmx-killpill { content: ' \03D1';} } </style>
<style type="text/css" emogrify="no">@media (max-width: 600px) { .gmx-killpill { content: ' \03D1';} .r0-o { border-style: solid !important; margin: 0 auto 0 auto !important; width: 320px !important } .r1-i { background-color: #ffffff !important } .r2-c { box-sizing: border-box !important; text-align: center !important; valign: top !important; width: 100% !important } .r3-o { border-style: solid !important; margin: 0 auto 0 auto !important; width: 100% !important } .r4-i { padding-bottom: 20px !important; padding-left: 15px !important; padding-right: 15px !important; padding-top: 20px !important } .r5-c { box-sizing: border-box !important; display: block !important; valign: top !important; width: 100% !important } .r6-o { border-style: solid !important; width: 100% !important } .r7-i { padding-left: 0px !important; padding-right: 0px !important } .r8-i { padding-bottom: 20px !important; padding-left: 10px !important; padding-right: 10px !important; padding-top: 20px !important } .r9-c { box-sizing: border-box !important; text-align: left !important; valign: top !important; width: 100% !important } .r10-o { border-style: solid !important; margin: 0 auto 0 0 !important; width: 100% !important } .r11-i { padding-top: 15px !important; text-align: center !important } .r12-c { box-sizing: border-box !important; padding-bottom: 15px !important; padding-top: 15px !important; text-align: left !important; valign: top !important; width: 100% !important } .r13-c { box-sizing: border-box !important; padding: 0 !important; text-align: center !important; valign: top !important; width: 100% !important } .r14-o { border-style: solid !important; margin: 0 auto 0 auto !important; margin-bottom: 15px !important; margin-top: 15px !important; width: 100% !important } .r15-i { padding: 0 !important; text-align: center !important } .r16-r { background-color: #3f76ff !important; border-radius: 4px !important; border-width: 0px !important; box-sizing: border-box; height: initial !important; padding: 0 !important; padding-bottom: 12px !important; padding-left: 5px !important; padding-right: 5px !important; padding-top: 12px !important; text-align: center !important; width: 100% !important } .r17-c { box-sizing: border-box !important; text-align: center !important; width: 100% !important } .r18-c { box-sizing: border-box !important; width: 100% !important } .r19-i { font-size: 0px !important; padding-bottom: 15px !important; padding-left: 65px !important; padding-right: 65px !important; padding-top: 15px !important } .r20-c { box-sizing: border-box !important; width: 32px !important } .r21-o { border-style: solid !important; margin-right: 8px !important; width: 32px !important } .r22-i { padding-bottom: 5px !important; padding-top: 5px !important } .r23-o { border-style: solid !important; margin-right: 0px !important; width: 32px !important } body { -webkit-text-size-adjust: none } .nl2go-responsive-hide { display: none } .nl2go-body-table { min-width: unset !important } .mobshow { height: auto !important; overflow: visible !important; max-height: unset !important; visibility: visible !important; border: none !important } .resp-table { display: inline-table !important } .magic-resp { display: table-cell !important } } </style> <style type="text/css" emogrify="no">@media (max-width: 600px) { .gmx-killpill { content: ' \03D1';} .r0-o { border-style: solid !important; margin: 0 auto 0 auto !important; width: 320px !important } .r1-i { background-color: #ffffff !important } .r2-c { box-sizing: border-box !important; text-align: center !important; valign: top !important; width: 100% !important } .r3-o { border-style: solid !important; margin: 0 auto 0 auto !important; width: 100% !important } .r4-i { padding-bottom: 20px !important; padding-left: 15px !important; padding-right: 15px !important; padding-top: 20px !important } .r5-c { box-sizing: border-box !important; display: block !important; valign: top !important; width: 100% !important } .r6-o { border-style: solid !important; width: 100% !important } .r7-i { padding-left: 0px !important; padding-right: 0px !important } .r8-i { padding-bottom: 20px !important; padding-left: 10px !important; padding-right: 10px !important; padding-top: 20px !important } .r9-c { box-sizing: border-box !important; text-align: left !important; valign: top !important; width: 100% !important } .r10-o { border-style: solid !important; margin: 0 auto 0 0 !important; width: 100% !important } .r11-i { padding-top: 15px !important; text-align: center !important } .r12-c { box-sizing: border-box !important; padding-bottom: 15px !important; padding-top: 15px !important; text-align: left !important; valign: top !important; width: 100% !important } .r13-c { box-sizing: border-box !important; padding: 0 !important; text-align: center !important; valign: top !important; width: 100% !important } .r14-o { border-style: solid !important; margin: 0 auto 0 auto !important; margin-bottom: 15px !important; margin-top: 15px !important; width: 100% !important } .r15-i { padding: 0 !important; text-align: center !important } .r16-r { background-color: #006399 !important; border-radius: 4px !important; border-width: 0px !important; box-sizing: border-box; height: initial !important; padding: 0 !important; padding-bottom: 12px !important; padding-left: 5px !important; padding-right: 5px !important; padding-top: 12px !important; text-align: center !important; width: 100% !important } .r17-c { box-sizing: border-box !important; text-align: center !important; width: 100% !important } .r18-c { box-sizing: border-box !important; width: 100% !important } .r19-i { font-size: 0px !important; padding-bottom: 15px !important; padding-left: 65px !important; padding-right: 65px !important; padding-top: 15px !important } .r20-c { box-sizing: border-box !important; width: 32px !important } .r21-o { border-style: solid !important; margin-right: 8px !important; width: 32px !important } .r22-i { padding-bottom: 5px !important; padding-top: 5px !important } .r23-o { border-style: solid !important; margin-right: 0px !important; width: 32px !important } body { -webkit-text-size-adjust: none } .nl2go-responsive-hide { display: none } .nl2go-body-table { min-width: unset !important } .mobshow { height: auto !important; overflow: visible !important; max-height: unset !important; visibility: visible !important; border: none !important } .resp-table { display: inline-table !important } .magic-resp { display: table-cell !important } } </style>
<style type="text/css">p, h1, h2, h3, h4, ol, ul, li { margin: 0; } a, a:link { color: #3f76ff; text-decoration: underline } .nl2go-default-textstyle { color: #3b3f44; font-family: georgia, serif; font-size: 18px; line-height: 1.5; word-break: break-word } .default-button { color: #ffffff; font-family: georgia, serif; font-size: 16px; font-style: normal; font-weight: bold; line-height: 1.15; text-decoration: none; word-break: break-word } .default-heading1 { color: #1F2D3D; font-family: arial,helvetica,sans-serif; font-size: 36px; word-break: break-word } .default-heading2 { color: #1F2D3D; font-family: arial,helvetica,sans-serif; font-size: 32px; word-break: break-word } .default-heading3 { color: #1F2D3D; font-family: arial,helvetica,sans-serif; font-size: 24px; word-break: break-word } .default-heading4 { color: #1F2D3D; font-family: arial,helvetica,sans-serif; font-size: 18px; word-break: break-word } a[x-apple-data-detectors] { color: inherit !important; text-decoration: inherit !important; font-size: inherit !important; font-family: inherit !important; font-weight: inherit !important; line-height: inherit !important; } .no-show-for-you { border: none; display: none; float: none; font-size: 0; height: 0; line-height: 0; max-height: 0; mso-hide: all; overflow: hidden; table-layout: fixed; visibility: hidden; width: 0; } </style> <style type="text/css">p, h1, h2, h3, h4, ol, ul, li { margin: 0; } a, a:link { color: #006399; text-decoration: underline } .nl2go-default-textstyle { color: #3b3f44; font-family: georgia, serif; font-size: 18px; line-height: 1.5; word-break: break-word } .default-button { color: #ffffff; font-family: georgia, serif; font-size: 16px; font-style: normal; font-weight: bold; line-height: 1.15; text-decoration: none; word-break: break-word } .default-heading1 { color: #1F2D3D; font-family: arial,helvetica,sans-serif; font-size: 36px; word-break: break-word } .default-heading2 { color: #1F2D3D; font-family: arial,helvetica,sans-serif; font-size: 32px; word-break: break-word } .default-heading3 { color: #1F2D3D; font-family: arial,helvetica,sans-serif; font-size: 24px; word-break: break-word } .default-heading4 { color: #1F2D3D; font-family: arial,helvetica,sans-serif; font-size: 18px; word-break: break-word } a[x-apple-data-detectors] { color: inherit !important; text-decoration: inherit !important; font-size: inherit !important; font-family: inherit !important; font-weight: inherit !important; line-height: inherit !important; } .no-show-for-you { border: none; display: none; float: none; font-size: 0; height: 0; line-height: 0; max-height: 0; mso-hide: all; overflow: hidden; table-layout: fixed; visibility: hidden; width: 0; } </style>
<!--[if mso]> <!--[if mso]>
<xml> <xml>
<o:OfficeDocumentSettings> <o:OfficeDocumentSettings>
@@ -18,9 +18,9 @@
</o:OfficeDocumentSettings> </o:OfficeDocumentSettings>
</xml> </xml>
<![endif]--> <![endif]-->
<style type="text/css">a:link{color: #3f76ff; text-decoration: underline;}</style> <style type="text/css">a:link{color: #006399; text-decoration: underline;}</style>
</head> </head>
<body bgcolor="#ffffff" text="#3b3f44" link="#3f76ff" yahoo="fix" style="background-color: #ffffff;"> <body bgcolor="#ffffff" text="#3b3f44" link="#006399" yahoo="fix" style="background-color: #ffffff;">
<table cellspacing="0" cellpadding="0" border="0" role="presentation" class="nl2go-body-table" width="100%" style="background-color: #ffffff; width: 100%;"> <table cellspacing="0" cellpadding="0" border="0" role="presentation" class="nl2go-body-table" width="100%" style="background-color: #ffffff; width: 100%;">
<tr> <tr>
<td> <td>
@@ -41,7 +41,7 @@
<td class="r2-c" align="center"> <td class="r2-c" align="center">
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="200" class="r3-o" style="table-layout: fixed; width: 200px;"> <table cellspacing="0" cellpadding="0" border="0" role="presentation" width="200" class="r3-o" style="table-layout: fixed; width: 200px;">
<tr> <tr>
<td style="font-size: 0px; line-height: 0px;"> <img src="https://img.mailinblue.com/5942152/images/content_library/original/64708f00b503b149db7ff135.png" width="200" border="0" style="display: block; width: 100%;"></td> <td style="font-size: 0px; line-height: 0px;"> <img src="https://media.docs.plane.so/logo/new-logo-white.png" width="200" border="0" style="display: block; width: 100%;"></td>
</tr> </tr>
</table> </table>
</td> </td>
@@ -88,9 +88,9 @@
</tr> </tr>
<tr> <tr>
<td class="r13-c" align="center" style="align: center; padding-bottom: 15px; padding-top: 15px; valign: top;"> <td class="r13-c" align="center" style="align: center; padding-bottom: 15px; padding-top: 15px; valign: top;">
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="300" class="r14-o" style="background-color: #3f76ff; border-collapse: separate; border-color: #3f76ff; border-radius: 4px; border-style: solid; border-width: 0px; table-layout: fixed; width: 300px;"> <table cellspacing="0" cellpadding="0" border="0" role="presentation" width="300" class="r14-o" style="background-color: #006399; border-collapse: separate; border-color: #006399; border-radius: 4px; border-style: solid; border-width: 0px; table-layout: fixed; width: 300px;">
<tr> <tr>
<td height="18" align="center" valign="top" class="r15-i nl2go-default-textstyle" style="word-break: break-word; background-color: #3f76ff; border-radius: 4px; color: #ffffff; font-family: georgia, serif; font-size: 16px; font-style: normal; line-height: 1.15; padding-bottom: 12px; padding-left: 5px; padding-right: 5px; padding-top: 12px; text-align: center;"> <a href="{{abs_url}}" class="r16-r default-button" target="_blank" data-btn="1" style="font-style: normal; font-weight: bold; line-height: 1.15; text-decoration: none; word-break: break-word; word-wrap: break-word; display: block; -webkit-text-size-adjust: none; color: #ffffff; font-family: georgia, serif; font-size: 16px;"> <span><span style="font-family: Arial, helvetica, sans-serif;">Join them on Plane</span></span></a> </td> <td height="18" align="center" valign="top" class="r15-i nl2go-default-textstyle" style="word-break: break-word; background-color: #006399; border-radius: 4px; color: #ffffff; font-family: georgia, serif; font-size: 16px; font-style: normal; line-height: 1.15; padding-bottom: 12px; padding-left: 5px; padding-right: 5px; padding-top: 12px; text-align: center;"> <a href="{{abs_url}}" class="r16-r default-button" target="_blank" data-btn="1" style="font-style: normal; font-weight: bold; line-height: 1.15; text-decoration: none; word-break: break-word; word-wrap: break-word; display: block; -webkit-text-size-adjust: none; color: #ffffff; font-family: georgia, serif; font-size: 16px;"> <span><span style="font-family: Arial, helvetica, sans-serif;">Join them on Plane</span></span></a> </td>
</tr> </tr>
</table> </table>
</td> </td>
@@ -131,7 +131,7 @@
<th width="40" class="r20-c mobshow resp-table" style="font-weight: normal;"> <th width="40" class="r20-c mobshow resp-table" style="font-weight: normal;">
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" class="r21-o" style="table-layout: fixed; width: 100%;"> <table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" class="r21-o" style="table-layout: fixed; width: 100%;">
<tr> <tr>
<td class="r22-i" style="font-size: 0px; line-height: 0px; padding-bottom: 5px; padding-top: 5px;"> <a href="https://github.com/makeplane" target="_blank" style="color: #3f76ff; text-decoration: underline;"> <img src="https://creative-assets.mailinblue.com/editor/social-icons/rounded_colored/github_32px.png" width="32" border="0" style="display: block; width: 100%;"></a> </td> <td class="r22-i" style="font-size: 0px; line-height: 0px; padding-bottom: 5px; padding-top: 5px;"> <a href="https://github.com/makeplane" target="_blank" style="color: #006399; text-decoration: underline;"> <img src="https://creative-assets.mailinblue.com/editor/social-icons/rounded_colored/github_32px.png" width="32" border="0" style="display: block; width: 100%;"></a> </td>
<td class="nl2go-responsive-hide" width="8" style="font-size: 0px; line-height: 1px;">­ </td> <td class="nl2go-responsive-hide" width="8" style="font-size: 0px; line-height: 1px;">­ </td>
</tr> </tr>
</table> </table>
@@ -139,7 +139,7 @@
<th width="40" class="r20-c mobshow resp-table" style="font-weight: normal;"> <th width="40" class="r20-c mobshow resp-table" style="font-weight: normal;">
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" class="r21-o" style="table-layout: fixed; width: 100%;"> <table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" class="r21-o" style="table-layout: fixed; width: 100%;">
<tr> <tr>
<td class="r22-i" style="font-size: 0px; line-height: 0px; padding-bottom: 5px; padding-top: 5px;"> <a href="https://www.linkedin.com/company/planepowers/" target="_blank" style="color: #3f76ff; text-decoration: underline;"> <img src="https://creative-assets.mailinblue.com/editor/social-icons/rounded_colored/linkedin_32px.png" width="32" border="0" style="display: block; width: 100%;"></a> </td> <td class="r22-i" style="font-size: 0px; line-height: 0px; padding-bottom: 5px; padding-top: 5px;"> <a href="https://www.linkedin.com/company/planepowers/" target="_blank" style="color: #006399; text-decoration: underline;"> <img src="https://creative-assets.mailinblue.com/editor/social-icons/rounded_colored/linkedin_32px.png" width="32" border="0" style="display: block; width: 100%;"></a> </td>
<td class="nl2go-responsive-hide" width="8" style="font-size: 0px; line-height: 1px;">­ </td> <td class="nl2go-responsive-hide" width="8" style="font-size: 0px; line-height: 1px;">­ </td>
</tr> </tr>
</table> </table>
@@ -147,7 +147,7 @@
<th width="40" class="r20-c mobshow resp-table" style="font-weight: normal;"> <th width="40" class="r20-c mobshow resp-table" style="font-weight: normal;">
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" class="r21-o" style="table-layout: fixed; width: 100%;"> <table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" class="r21-o" style="table-layout: fixed; width: 100%;">
<tr> <tr>
<td class="r22-i" style="font-size: 0px; line-height: 0px; padding-bottom: 5px; padding-top: 5px;"> <a href="https://twitter.com/planepowers" target="_blank" style="color: #3f76ff; text-decoration: underline;"> <img src="https://creative-assets.mailinblue.com/editor/social-icons/rounded_colored/twitter_32px.png" width="32" border="0" style="display: block; width: 100%;"></a> </td> <td class="r22-i" style="font-size: 0px; line-height: 0px; padding-bottom: 5px; padding-top: 5px;"> <a href="https://twitter.com/planepowers" target="_blank" style="color: #006399; text-decoration: underline;"> <img src="https://creative-assets.mailinblue.com/editor/social-icons/rounded_colored/twitter_32px.png" width="32" border="0" style="display: block; width: 100%;"></a> </td>
<td class="nl2go-responsive-hide" width="8" style="font-size: 0px; line-height: 1px;">­ </td> <td class="nl2go-responsive-hide" width="8" style="font-size: 0px; line-height: 1px;">­ </td>
</tr> </tr>
</table> </table>
@@ -155,7 +155,7 @@
<th width="32" class="r20-c mobshow resp-table" style="font-weight: normal;"> <th width="32" class="r20-c mobshow resp-table" style="font-weight: normal;">
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" class="r23-o" style="table-layout: fixed; width: 100%;"> <table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" class="r23-o" style="table-layout: fixed; width: 100%;">
<tr> <tr>
<td class="r22-i" style="font-size: 0px; line-height: 0px; padding-bottom: 5px; padding-top: 5px;"> <a href="https://plane.so/" target="_blank" style="color: #3f76ff; text-decoration: underline;"> <img src="https://creative-assets.mailinblue.com/editor/social-icons/rounded_colored/website_32px.png" width="32" border="0" style="display: block; width: 100%;"></a> </td> <td class="r22-i" style="font-size: 0px; line-height: 0px; padding-bottom: 5px; padding-top: 5px;"> <a href="https://plane.so/" target="_blank" style="color: #006399; text-decoration: underline;"> <img src="https://creative-assets.mailinblue.com/editor/social-icons/rounded_colored/website_32px.png" width="32" border="0" style="display: block; width: 100%;"></a> </td>
</tr> </tr>
</table> </table>
</th> </th>

View File

@@ -8,14 +8,14 @@
<style> *[class="gmail-fix"] { display: none !important; } </style> <style> *[class="gmail-fix"] { display: none !important; } </style>
<style type="text/css" emogrify="no"> @media (max-width: 600px) { .gmx-killpill { content: " \03D1"; } } </style> <style type="text/css" emogrify="no"> @media (max-width: 600px) { .gmx-killpill { content: " \03D1"; } } </style>
</head> </head>
<body bgcolor="#ffffff" text="#3b3f44" link="#3f76ff" yahoo="fix" style="background-color: #f7f9ff; margin: 20px" > <body bgcolor="#ffffff" text="#3b3f44" link="#006399" yahoo="fix" style="background-color: #f7f9ff; margin: 20px" >
<div style=" width: 600px; table-layout: fixed; height: 100%; margin-left: auto; margin-right: auto; " > <div style=" width: 600px; table-layout: fixed; height: 100%; margin-left: auto; margin-right: auto; " >
<!-- Header --> <!-- Header -->
<div> <div>
<table style="width: 600px" cellspacing="0"> <table style="width: 600px" cellspacing="0">
<tr> <tr>
<td> <td>
<div style="margin-left: 30px; margin-bottom: 20px; margin-top: 20px" > <img src="https://plane-marketing.s3.ap-south-1.amazonaws.com/plane-assets/emails/plane-logo.png" width="130" height="40" border="0" /> </div> <div style="margin-left: 30px; margin-bottom: 20px; margin-top: 20px" > <img src="https://media.docs.plane.so/logo/new-logo-white.png" width="150" border="0" /> </div>
</td> </td>
</tr> </tr>
</table> </table>

View File

@@ -164,7 +164,7 @@
text-align: center !important; text-align: center !important;
} }
.r15-r { .r15-r {
background-color: #3f76ff !important; background-color: #006399 !important;
border-radius: 4px !important; border-radius: 4px !important;
border-width: 0px !important; border-width: 0px !important;
box-sizing: border-box; box-sizing: border-box;
@@ -296,7 +296,7 @@
} }
a, a,
a:link { a:link {
color: #3f76ff; color: #006399;
text-decoration: underline; text-decoration: underline;
} }
.nl2go-default-textstyle { .nl2go-default-textstyle {
@@ -372,7 +372,7 @@
[endif]--> [endif]-->
<style type="text/css"> <style type="text/css">
a:link { a:link {
color: #3f76ff; color: #006399;
text-decoration: underline; text-decoration: underline;
} }
</style> </style>
@@ -380,7 +380,7 @@
<body <body
bgcolor="#ffffff" bgcolor="#ffffff"
text="#3b3f44" text="#3b3f44"
link="#3f76ff" link="#006399"
yahoo="fix" yahoo="fix"
style="background-color: #ffffff" style="background-color: #ffffff"
> >
@@ -483,7 +483,7 @@
" "
> >
<img <img
src="https://img.mailinblue.com/5942152/images/content_library/original/64708f00b503b149db7ff135.png" src="https://media.docs.plane.so/logo/new-logo-white.png"
width="150" width="150"
border="0" border="0"
style=" style="
@@ -672,9 +672,9 @@
width="285" width="285"
class="r13-o" class="r13-o"
style=" style="
background-color: #3f76ff; background-color: #006399;
border-collapse: separate; border-collapse: separate;
border-color: #3f76ff; border-color: #006399;
border-radius: 4px; border-radius: 4px;
border-style: solid; border-style: solid;
border-width: 0px; border-width: 0px;
@@ -690,7 +690,7 @@
class="r14-i nl2go-default-textstyle" class="r14-i nl2go-default-textstyle"
style=" style="
word-break: break-word; word-break: break-word;
background-color: #3f76ff; background-color: #006399;
border-radius: 4px; border-radius: 4px;
color: #ffffff; color: #ffffff;
font-family: georgia, serif; font-family: georgia, serif;
@@ -984,7 +984,7 @@
title="Plane Support on Discod" title="Plane Support on Discod"
target="_blank" target="_blank"
style=" style="
color: #3f76ff; color: #006399;
text-decoration: underline; text-decoration: underline;
" "
><span ><span
@@ -998,7 +998,7 @@
title="@planepowers" title="@planepowers"
target="_blank" target="_blank"
style=" style="
color: #3f76ff; color: #006399;
text-decoration: underline; text-decoration: underline;
" "
><span ><span
@@ -1012,7 +1012,7 @@
title="Plane's GitHub conversations" title="Plane's GitHub conversations"
target="_blank" target="_blank"
style=" style="
color: #3f76ff; color: #006399;
text-decoration: underline; text-decoration: underline;
" "
><span ><span
@@ -1028,7 +1028,7 @@
title="Plane's roadmap" title="Plane's roadmap"
target="_blank" target="_blank"
style=" style="
color: #3f76ff; color: #006399;
text-decoration: underline; text-decoration: underline;
" "
><span ><span
@@ -1248,7 +1248,7 @@
href="https://github.com/makeplane" href="https://github.com/makeplane"
target="_blank" target="_blank"
style=" style="
color: #3f76ff; color: #006399;
text-decoration: underline; text-decoration: underline;
" "
> >
@@ -1308,7 +1308,7 @@
href="https://www.linkedin.com/company/planepowers/" href="https://www.linkedin.com/company/planepowers/"
target="_blank" target="_blank"
style=" style="
color: #3f76ff; color: #006399;
text-decoration: underline; text-decoration: underline;
" "
> >
@@ -1368,7 +1368,7 @@
href="https://twitter.com/planepowers" href="https://twitter.com/planepowers"
target="_blank" target="_blank"
style=" style="
color: #3f76ff; color: #006399;
text-decoration: underline; text-decoration: underline;
" "
> >
@@ -1428,7 +1428,7 @@
href="https://plane.so/" href="https://plane.so/"
target="_blank" target="_blank"
style=" style="
color: #3f76ff; color: #006399;
text-decoration: underline; text-decoration: underline;
" "
> >

View File

@@ -8,8 +8,8 @@
<title>{{ message }}</title> <title>{{ message }}</title>
<style type="text/css" emogrify="no"> #outlook a { padding: 0; } .ExternalClass { width: 100%; } .ExternalClass, .ExternalClass p, .ExternalClass span, .ExternalClass font, .ExternalClass td, .ExternalClass div { line-height: 100%; } table td { border-collapse: collapse; mso-line-height-rule: exactly; } .editable.image { font-size: 0 !important; line-height: 0 !important; } .nl2go_preheader { display: none !important; mso-hide: all !important; mso-line-height-rule: exactly; visibility: hidden !important; line-height: 0px !important; font-size: 0px !important; } body { width: 100% !important; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; margin: 0; padding: 0; } img { outline: none; text-decoration: none; -ms-interpolation-mode: bicubic; } a img { border: none; } table { border-collapse: collapse; mso-table-lspace: 0pt; mso-table-rspace: 0pt; } th { font-weight: normal; text-align: left; } *[class="gmail-fix"] { display: none !important; } </style> <style type="text/css" emogrify="no"> #outlook a { padding: 0; } .ExternalClass { width: 100%; } .ExternalClass, .ExternalClass p, .ExternalClass span, .ExternalClass font, .ExternalClass td, .ExternalClass div { line-height: 100%; } table td { border-collapse: collapse; mso-line-height-rule: exactly; } .editable.image { font-size: 0 !important; line-height: 0 !important; } .nl2go_preheader { display: none !important; mso-hide: all !important; mso-line-height-rule: exactly; visibility: hidden !important; line-height: 0px !important; font-size: 0px !important; } body { width: 100% !important; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; margin: 0; padding: 0; } img { outline: none; text-decoration: none; -ms-interpolation-mode: bicubic; } a img { border: none; } table { border-collapse: collapse; mso-table-lspace: 0pt; mso-table-rspace: 0pt; } th { font-weight: normal; text-align: left; } *[class="gmail-fix"] { display: none !important; } </style>
<style type="text/css" emogrify="no"> @media (max-width: 600px) { .gmx-killpill { content: " \03D1"; } } </style> <style type="text/css" emogrify="no"> @media (max-width: 600px) { .gmx-killpill { content: " \03D1"; } } </style>
<style type="text/css" emogrify="no"> @media (max-width: 600px) { .gmx-killpill { content: " \03D1"; } .r0-o { border-style: solid !important; margin: 0 auto 0 auto !important; width: 320px !important; } .r1-i { background-color: #ffffff !important; } .r2-c { box-sizing: border-box !important; text-align: center !important; valign: top !important; width: 100% !important; } .r3-o { border-style: solid !important; margin: 0 auto 0 auto !important; width: 100% !important; } .r4-i { padding-bottom: 20px !important; padding-left: 15px !important; padding-right: 15px !important; padding-top: 20px !important; } .r5-c { box-sizing: border-box !important; display: block !important; valign: top !important; width: 100% !important; } .r6-o { border-style: solid !important; width: 100% !important; } .r7-i { padding-left: 0px !important; padding-right: 0px !important; } .r8-c { box-sizing: border-box !important; text-align: left !important; valign: top !important; width: 100% !important; } .r9-o { border-style: solid !important; margin: 0 auto 0 0 !important; width: 100% !important; } .r10-i { padding-bottom: 10px !important; padding-left: 0px !important; padding-right: 0px !important; padding-top: 10px !important; } .r11-i { padding-bottom: 15px !important; padding-top: 15px !important; text-align: left !important; } .r12-c { box-sizing: border-box !important; padding: 0 !important; text-align: center !important; valign: top !important; width: 100% !important; } .r13-o { border-style: solid !important; margin: 0 auto 0 auto !important; margin-bottom: 15px !important; margin-top: 15px !important; width: 100% !important; } .r14-i { padding: 0 !important; text-align: center !important; } .r15-r { background-color: #3f76ff !important; border-radius: 4px !important; border-width: 0px !important; box-sizing: border-box; height: initial !important; padding: 0 !important; padding-bottom: 12px !important; padding-top: 12px !important; text-align: center !important; width: 100% !important; } .r16-i { padding-bottom: 0px !important; padding-left: 15px !important; padding-right: 15px !important; padding-top: 0px !important; } .r17-c { box-sizing: border-box !important; text-align: center !important; width: 100% !important; } .r18-i { padding-bottom: 5px !important; padding-top: 5px !important; } .r19-i { padding-bottom: 15px !important; padding-left: 15px !important; padding-right: 15px !important; padding-top: 15px !important; } .r20-o { border-bottom-color: #efefef !important; border-bottom-width: 2px !important; border-left-color: #efefef !important; border-left-width: 2px !important; border-right-color: #efefef !important; border-right-width: 2px !important; border-style: solid !important; border-top-color: #efefef !important; border-top-width: 2px !important; margin: 0 auto 0 0 !important; width: 100% !important; } .r21-i { padding-bottom: 5px !important; padding-left: 5px !important; padding-right: 5px !important; padding-top: 5px !important; text-align: left !important; } .r22-i { padding-bottom: 5px !important; padding-left: 15px !important; padding-right: 15px !important; padding-top: 5px !important; } .r23-c { box-sizing: border-box !important; width: 100% !important; } .r24-i { font-size: 0px !important; padding-bottom: 10px !important; padding-left: 65px !important; padding-right: 65px !important; padding-top: 10px !important; } .r25-c { box-sizing: border-box !important; width: 32px !important; } .r26-o { border-style: solid !important; margin-right: 8px !important; width: 32px !important; } .r27-o { border-style: solid !important; margin-right: 0px !important; width: 32px !important; } .r28-i { padding-bottom: 0px !important; padding-top: 5px !important; text-align: center !important; } body { -webkit-text-size-adjust: none; } .nl2go-responsive-hide { display: none; } .nl2go-body-table { min-width: unset !important; } .mobshow { height: auto !important; overflow: visible !important; max-height: unset !important; visibility: visible !important; border: none !important; } .resp-table { display: inline-table !important; } .magic-resp { display: table-cell !important; } } </style> <style type="text/css" emogrify="no"> @media (max-width: 600px) { .gmx-killpill { content: " \03D1"; } .r0-o { border-style: solid !important; margin: 0 auto 0 auto !important; width: 320px !important; } .r1-i { background-color: #ffffff !important; } .r2-c { box-sizing: border-box !important; text-align: center !important; valign: top !important; width: 100% !important; } .r3-o { border-style: solid !important; margin: 0 auto 0 auto !important; width: 100% !important; } .r4-i { padding-bottom: 20px !important; padding-left: 15px !important; padding-right: 15px !important; padding-top: 20px !important; } .r5-c { box-sizing: border-box !important; display: block !important; valign: top !important; width: 100% !important; } .r6-o { border-style: solid !important; width: 100% !important; } .r7-i { padding-left: 0px !important; padding-right: 0px !important; } .r8-c { box-sizing: border-box !important; text-align: left !important; valign: top !important; width: 100% !important; } .r9-o { border-style: solid !important; margin: 0 auto 0 0 !important; width: 100% !important; } .r10-i { padding-bottom: 10px !important; padding-left: 0px !important; padding-right: 0px !important; padding-top: 10px !important; } .r11-i { padding-bottom: 15px !important; padding-top: 15px !important; text-align: left !important; } .r12-c { box-sizing: border-box !important; padding: 0 !important; text-align: center !important; valign: top !important; width: 100% !important; } .r13-o { border-style: solid !important; margin: 0 auto 0 auto !important; margin-bottom: 15px !important; margin-top: 15px !important; width: 100% !important; } .r14-i { padding: 0 !important; text-align: center !important; } .r15-r { background-color: #006399 !important; border-radius: 4px !important; border-width: 0px !important; box-sizing: border-box; height: initial !important; padding: 0 !important; padding-bottom: 12px !important; padding-top: 12px !important; text-align: center !important; width: 100% !important; } .r16-i { padding-bottom: 0px !important; padding-left: 15px !important; padding-right: 15px !important; padding-top: 0px !important; } .r17-c { box-sizing: border-box !important; text-align: center !important; width: 100% !important; } .r18-i { padding-bottom: 5px !important; padding-top: 5px !important; } .r19-i { padding-bottom: 15px !important; padding-left: 15px !important; padding-right: 15px !important; padding-top: 15px !important; } .r20-o { border-bottom-color: #efefef !important; border-bottom-width: 2px !important; border-left-color: #efefef !important; border-left-width: 2px !important; border-right-color: #efefef !important; border-right-width: 2px !important; border-style: solid !important; border-top-color: #efefef !important; border-top-width: 2px !important; margin: 0 auto 0 0 !important; width: 100% !important; } .r21-i { padding-bottom: 5px !important; padding-left: 5px !important; padding-right: 5px !important; padding-top: 5px !important; text-align: left !important; } .r22-i { padding-bottom: 5px !important; padding-left: 15px !important; padding-right: 15px !important; padding-top: 5px !important; } .r23-c { box-sizing: border-box !important; width: 100% !important; } .r24-i { font-size: 0px !important; padding-bottom: 10px !important; padding-left: 65px !important; padding-right: 65px !important; padding-top: 10px !important; } .r25-c { box-sizing: border-box !important; width: 32px !important; } .r26-o { border-style: solid !important; margin-right: 8px !important; width: 32px !important; } .r27-o { border-style: solid !important; margin-right: 0px !important; width: 32px !important; } .r28-i { padding-bottom: 0px !important; padding-top: 5px !important; text-align: center !important; } body { -webkit-text-size-adjust: none; } .nl2go-responsive-hide { display: none; } .nl2go-body-table { min-width: unset !important; } .mobshow { height: auto !important; overflow: visible !important; max-height: unset !important; visibility: visible !important; border: none !important; } .resp-table { display: inline-table !important; } .magic-resp { display: table-cell !important; } } </style>
<style type="text/css"> p, h1, h2, h3, h4, ol, ul { margin: 0; } a, a:link { color: #3f76ff; text-decoration: underline; } .nl2go-default-textstyle { color: #3b3f44; font-family: georgia, serif; font-size: 16px; line-height: 1.5; word-break: break-word; } .default-button { color: #ffffff; font-family: georgia, serif; font-size: 16px; font-style: normal; font-weight: normal; line-height: 1.15; text-decoration: none; word-break: break-word; } .default-heading1 { color: #1f2d3d; font-family: arial, helvetica, sans-serif; font-size: 36px; word-break: break-word; } .default-heading2 { color: #1f2d3d; font-family: arial, helvetica, sans-serif; font-size: 32px; word-break: break-word; } .default-heading3 { color: #1f2d3d; font-family: arial, helvetica, sans-serif; font-size: 24px; word-break: break-word; } .default-heading4 { color: #1f2d3d; font-family: arial, helvetica, sans-serif; font-size: 18px; word-break: break-word; } a[x-apple-data-detectors] { color: inherit !important; text-decoration: inherit !important; font-size: inherit !important; font-family: inherit !important; font-weight: inherit !important; line-height: inherit !important; } .no-show-for-you { border: none; display: none; float: none; font-size: 0; height: 0; line-height: 0; max-height: 0; mso-hide: all; overflow: hidden; table-layout: fixed; visibility: hidden; width: 0; } </style> <style type="text/css"> p, h1, h2, h3, h4, ol, ul { margin: 0; } a, a:link { color: #006399; text-decoration: underline; } .nl2go-default-textstyle { color: #3b3f44; font-family: georgia, serif; font-size: 16px; line-height: 1.5; word-break: break-word; } .default-button { color: #ffffff; font-family: georgia, serif; font-size: 16px; font-style: normal; font-weight: normal; line-height: 1.15; text-decoration: none; word-break: break-word; } .default-heading1 { color: #1f2d3d; font-family: arial, helvetica, sans-serif; font-size: 36px; word-break: break-word; } .default-heading2 { color: #1f2d3d; font-family: arial, helvetica, sans-serif; font-size: 32px; word-break: break-word; } .default-heading3 { color: #1f2d3d; font-family: arial, helvetica, sans-serif; font-size: 24px; word-break: break-word; } .default-heading4 { color: #1f2d3d; font-family: arial, helvetica, sans-serif; font-size: 18px; word-break: break-word; } a[x-apple-data-detectors] { color: inherit !important; text-decoration: inherit !important; font-size: inherit !important; font-family: inherit !important; font-weight: inherit !important; line-height: inherit !important; } .no-show-for-you { border: none; display: none; float: none; font-size: 0; height: 0; line-height: 0; max-height: 0; mso-hide: all; overflow: hidden; table-layout: fixed; visibility: hidden; width: 0; } </style>
<!--[if mso ]> <!--[if mso ]>
<xml> <xml>
<o:OfficeDocumentSettings> <o:OfficeDocumentSettings>
@@ -18,9 +18,9 @@
</o:OfficeDocumentSettings> </o:OfficeDocumentSettings>
</xml> </xml>
<! [endif]--> <! [endif]-->
<style type="text/css"> a:link { color: #3f76ff; text-decoration: underline; } </style> <style type="text/css"> a:link { color: #006399; text-decoration: underline; } </style>
</head> </head>
<body bgcolor="#ffffff" text="#3b3f44" link="#3f76ff" yahoo="fix" style="background-color: #ffffff" > <body bgcolor="#ffffff" text="#3b3f44" link="#006399" yahoo="fix" style="background-color: #ffffff" >
<table cellspacing="0" cellpadding="0" border="0" role="presentation" class="nl2go-body-table" width="100%" style="background-color: #ffffff; width: 100%" > <table cellspacing="0" cellpadding="0" border="0" role="presentation" class="nl2go-body-table" width="100%" style="background-color: #ffffff; width: 100%" >
<tr> <tr>
<td> <td>
@@ -41,7 +41,7 @@
<td class="r8-c" align="left"> <td class="r8-c" align="left">
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="150" class="r9-o" style=" table-layout: fixed; width: 150px; " > <table cellspacing="0" cellpadding="0" border="0" role="presentation" width="150" class="r9-o" style=" table-layout: fixed; width: 150px; " >
<tr> <tr>
<td style=" font-size: 0px; line-height: 0px; " > <img src="https://img.mailinblue.com/5942152/images/content_library/original/64708f00b503b149db7ff135.png" width="150" border="0" style=" display: block; width: 100%; " /> </td> <td style=" font-size: 0px; line-height: 0px; " > <img src="https://media.docs.plane.so/logo/new-logo-white.png" width="150" border="0" style=" display: block; width: 100%; " /> </td>
</tr> </tr>
</table> </table>
</td> </td>
@@ -80,7 +80,7 @@
</td> </td>
</tr> </tr>
<tr style=" display: flex; align-items: center; width: 100%; justify-content: center; " > <tr style=" display: flex; align-items: center; width: 100%; justify-content: center; " >
<td style=" display: flex; align-items: center; width: 100%; justify-content: center; " > <a href="{{ webhook_url }}" style=" text-decoration: none; display: flex; align-items: center; width: 100%; justify-content: center; " > <span style=" max-width: min-content; white-space: nowrap; background-color: #3f76ff; padding: 10px 15px; border: 1px solid #2f4ba8; border-radius: 4px; margin-top: 15px; cursor: pointer; font-size: 0.8rem; color: #ffffff; display: flex; align-items: center; justify-content: center; " > View webhook </span> </a> </td> <td style=" display: flex; align-items: center; width: 100%; justify-content: center; " > <a href="{{ webhook_url }}" style=" text-decoration: none; display: flex; align-items: center; width: 100%; justify-content: center; " > <span style=" max-width: min-content; white-space: nowrap; background-color: #006399; padding: 10px 15px; border: 1px solid #2f4ba8; border-radius: 4px; margin-top: 15px; cursor: pointer; font-size: 0.8rem; color: #ffffff; display: flex; align-items: center; justify-content: center; " > View webhook </span> </a> </td>
</tr> </tr>
</table> </table>
</td> </td>
@@ -155,7 +155,7 @@
<td class="nl2go-responsive-hide" width="2" style=" font-size: 0px; line-height: 1px; background-color: #efefef; " > ­ </td> <td class="nl2go-responsive-hide" width="2" style=" font-size: 0px; line-height: 1px; background-color: #efefef; " > ­ </td>
<td align="left" valign="top" class="r21-i nl2go-default-textstyle" style=" color: #3b3f44; font-family: georgia, serif; font-size: 16px; word-break: break-word; line-height: 1.5; padding-bottom: 5px; padding-left: 5px; padding-right: 5px; padding-top: 5px; text-align: left; " > <td align="left" valign="top" class="r21-i nl2go-default-textstyle" style=" color: #3b3f44; font-family: georgia, serif; font-size: 16px; word-break: break-word; line-height: 1.5; padding-bottom: 5px; padding-left: 5px; padding-right: 5px; padding-top: 5px; text-align: left; " >
<div> <div>
<p style="margin: 0"> <span style="font-size: 13px" >Despite our popularity, we are humbly early-stage. We are shipping fast, so please reach out to us with feature requests, major and minor nits, and anything else you find missing. We read every </span ><a href="https://discord.com/channels/1031547764020084846/1094927053867995176" title="Plane Support on Discod" target="_blank" style=" color: #3f76ff; text-decoration: underline; " ><span style="font-size: 13px" >message</span ></a ><span style="font-size: 13px" >, </span ><a href="http://twitter.com/planepowers" title="@planepowers" target="_blank" style=" color: #3f76ff; text-decoration: underline; " ><span style="font-size: 13px" >tweet</span ></a ><span style="font-size: 13px" >, and </span ><a href="https://github.com/makeplane/plane/issues" title="Plane's GitHub conversations" target="_blank" style=" color: #3f76ff; text-decoration: underline; " ><span style="font-size: 13px" >conversation</span ></a ><span style="font-size: 13px" > and update </span ><a href="https://plane.sh/plane/0b170a1c-0e55-47cb-9307-ea49a05672b5?board=kanban" title="Plane's roadmap" target="_blank" style=" color: #3f76ff; text-decoration: underline; " ><span style="font-size: 13px" >our public roadmap</span ></a ><span style="font-size: 13px" >.</span > </p> <p style="margin: 0"> <span style="font-size: 13px" >Despite our popularity, we are humbly early-stage. We are shipping fast, so please reach out to us with feature requests, major and minor nits, and anything else you find missing. We read every </span ><a href="https://discord.com/channels/1031547764020084846/1094927053867995176" title="Plane Support on Discod" target="_blank" style=" color: #006399; text-decoration: underline; " ><span style="font-size: 13px" >message</span ></a ><span style="font-size: 13px" >, </span ><a href="http://twitter.com/planepowers" title="@planepowers" target="_blank" style=" color: #006399; text-decoration: underline; " ><span style="font-size: 13px" >tweet</span ></a ><span style="font-size: 13px" >, and </span ><a href="https://github.com/makeplane/plane/issues" title="Plane's GitHub conversations" target="_blank" style=" color: #006399; text-decoration: underline; " ><span style="font-size: 13px" >conversation</span ></a ><span style="font-size: 13px" > and update </span ><a href="https://plane.sh/plane/0b170a1c-0e55-47cb-9307-ea49a05672b5?board=kanban" title="Plane's roadmap" target="_blank" style=" color: #006399; text-decoration: underline; " ><span style="font-size: 13px" >our public roadmap</span ></a ><span style="font-size: 13px" >.</span > </p>
</div> </div>
</td> </td>
<td class="nl2go-responsive-hide" width="2" style=" font-size: 0px; line-height: 1px; background-color: #efefef; " > ­ </td> <td class="nl2go-responsive-hide" width="2" style=" font-size: 0px; line-height: 1px; background-color: #efefef; " > ­ </td>
@@ -204,7 +204,7 @@
<th width="40" class="r25-c mobshow resp-table" style=" font-weight: normal; " > <th width="40" class="r25-c mobshow resp-table" style=" font-weight: normal; " >
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" class="r26-o" style=" table-layout: fixed; width: 100%; " > <table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" class="r26-o" style=" table-layout: fixed; width: 100%; " >
<tr> <tr>
<td class="r18-i" style=" font-size: 0px; line-height: 0px; padding-bottom: 5px; padding-top: 5px; " > <a href="https://github.com/makeplane" target="_blank" style=" color: #3f76ff; text-decoration: underline; " > <img src="https://creative-assets.mailinblue.com/editor/social-icons/rounded_colored/github_32px.png" width="32" border="0" style=" display: block; width: 100%; " /></a> </td> <td class="r18-i" style=" font-size: 0px; line-height: 0px; padding-bottom: 5px; padding-top: 5px; " > <a href="https://github.com/makeplane" target="_blank" style=" color: #006399; text-decoration: underline; " > <img src="https://creative-assets.mailinblue.com/editor/social-icons/rounded_colored/github_32px.png" width="32" border="0" style=" display: block; width: 100%; " /></a> </td>
<td class="nl2go-responsive-hide" width="8" style=" font-size: 0px; line-height: 1px; " > ­ </td> <td class="nl2go-responsive-hide" width="8" style=" font-size: 0px; line-height: 1px; " > ­ </td>
</tr> </tr>
</table> </table>
@@ -212,7 +212,7 @@
<th width="40" class="r25-c mobshow resp-table" style=" font-weight: normal; " > <th width="40" class="r25-c mobshow resp-table" style=" font-weight: normal; " >
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" class="r26-o" style=" table-layout: fixed; width: 100%; " > <table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" class="r26-o" style=" table-layout: fixed; width: 100%; " >
<tr> <tr>
<td class="r18-i" style=" font-size: 0px; line-height: 0px; padding-bottom: 5px; padding-top: 5px; " > <a href="https://www.linkedin.com/company/planepowers/" target="_blank" style=" color: #3f76ff; text-decoration: underline; " > <img src="https://creative-assets.mailinblue.com/editor/social-icons/rounded_colored/linkedin_32px.png" width="32" border="0" style=" display: block; width: 100%; " /></a> </td> <td class="r18-i" style=" font-size: 0px; line-height: 0px; padding-bottom: 5px; padding-top: 5px; " > <a href="https://www.linkedin.com/company/planepowers/" target="_blank" style=" color: #006399; text-decoration: underline; " > <img src="https://creative-assets.mailinblue.com/editor/social-icons/rounded_colored/linkedin_32px.png" width="32" border="0" style=" display: block; width: 100%; " /></a> </td>
<td class="nl2go-responsive-hide" width="8" style=" font-size: 0px; line-height: 1px; " > ­ </td> <td class="nl2go-responsive-hide" width="8" style=" font-size: 0px; line-height: 1px; " > ­ </td>
</tr> </tr>
</table> </table>
@@ -220,7 +220,7 @@
<th width="40" class="r25-c mobshow resp-table" style=" font-weight: normal; " > <th width="40" class="r25-c mobshow resp-table" style=" font-weight: normal; " >
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" class="r26-o" style=" table-layout: fixed; width: 100%; " > <table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" class="r26-o" style=" table-layout: fixed; width: 100%; " >
<tr> <tr>
<td class="r18-i" style=" font-size: 0px; line-height: 0px; padding-bottom: 5px; padding-top: 5px; " > <a href="https://twitter.com/planepowers" target="_blank" style=" color: #3f76ff; text-decoration: underline; " > <img src="https://creative-assets.mailinblue.com/editor/social-icons/rounded_colored/twitter_32px.png" width="32" border="0" style=" display: block; width: 100%; " /></a> </td> <td class="r18-i" style=" font-size: 0px; line-height: 0px; padding-bottom: 5px; padding-top: 5px; " > <a href="https://twitter.com/planepowers" target="_blank" style=" color: #006399; text-decoration: underline; " > <img src="https://creative-assets.mailinblue.com/editor/social-icons/rounded_colored/twitter_32px.png" width="32" border="0" style=" display: block; width: 100%; " /></a> </td>
<td class="nl2go-responsive-hide" width="8" style=" font-size: 0px; line-height: 1px; " > ­ </td> <td class="nl2go-responsive-hide" width="8" style=" font-size: 0px; line-height: 1px; " > ­ </td>
</tr> </tr>
</table> </table>
@@ -228,7 +228,7 @@
<th width="32" class="r25-c mobshow resp-table" style=" font-weight: normal; " > <th width="32" class="r25-c mobshow resp-table" style=" font-weight: normal; " >
<table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" class="r27-o" style=" table-layout: fixed; width: 100%; " > <table cellspacing="0" cellpadding="0" border="0" role="presentation" width="100%" class="r27-o" style=" table-layout: fixed; width: 100%; " >
<tr> <tr>
<td class="r18-i" style=" font-size: 0px; line-height: 0px; padding-bottom: 5px; padding-top: 5px; " > <a href="https://plane.so/" target="_blank" style=" color: #3f76ff; text-decoration: underline; " > <img src="https://creative-assets.mailinblue.com/editor/social-icons/rounded_colored/website_32px.png" width="32" border="0" style=" display: block; width: 100%; " /></a> </td> <td class="r18-i" style=" font-size: 0px; line-height: 0px; padding-bottom: 5px; padding-top: 5px; " > <a href="https://plane.so/" target="_blank" style=" color: #006399; text-decoration: underline; " > <img src="https://creative-assets.mailinblue.com/editor/social-icons/rounded_colored/website_32px.png" width="32" border="0" style=" display: block; width: 100%; " /></a> </td>
</tr> </tr>
</table> </table>
</th> </th>

View File

@@ -173,7 +173,7 @@
text-align: center !important; text-align: center !important;
} }
.r16-r { .r16-r {
background-color: #3f76ff !important; background-color: #006399 !important;
border-radius: 4px !important; border-radius: 4px !important;
border-width: 0px !important; border-width: 0px !important;
box-sizing: border-box; box-sizing: border-box;
@@ -305,7 +305,7 @@
} }
a, a,
a:link { a:link {
color: #3f76ff; color: #006399;
text-decoration: underline; text-decoration: underline;
} }
.nl2go-default-textstyle { .nl2go-default-textstyle {
@@ -382,7 +382,7 @@
<![endif]--> <![endif]-->
<style type="text/css"> <style type="text/css">
a:link { a:link {
color: #3f76ff; color: #006399;
text-decoration: underline; text-decoration: underline;
} }
</style> </style>
@@ -390,7 +390,7 @@
<body <body
bgcolor="#ffffff" bgcolor="#ffffff"
text="#3b3f44" text="#3b3f44"
link="#3f76ff" link="#006399"
yahoo="fix" yahoo="fix"
style="background-color: #ffffff" style="background-color: #ffffff"
> >
@@ -493,7 +493,7 @@
" "
> >
<img <img
src="https://img.mailinblue.com/5942152/images/content_library/original/64708f00b503b149db7ff135.png" src="https://media.docs.plane.so/logo/new-logo-white.png"
width="150" width="150"
border="0" border="0"
style=" style="
@@ -651,9 +651,9 @@
width="285" width="285"
class="r14-o" class="r14-o"
style=" style="
background-color: #3f76ff; background-color: #006399;
border-collapse: separate; border-collapse: separate;
border-color: #3f76ff; border-color: #006399;
border-radius: 4px; border-radius: 4px;
border-style: solid; border-style: solid;
border-width: 0px; border-width: 0px;
@@ -669,7 +669,7 @@
class="r15-i nl2go-default-textstyle" class="r15-i nl2go-default-textstyle"
style=" style="
word-break: break-word; word-break: break-word;
background-color: #3f76ff; background-color: #006399;
border-radius: 4px; border-radius: 4px;
color: #ffffff; color: #ffffff;
font-family: georgia, serif; font-family: georgia, serif;
@@ -963,7 +963,7 @@
title="Plane Support on Discod" title="Plane Support on Discod"
target="_blank" target="_blank"
style=" style="
color: #3f76ff; color: #006399;
text-decoration: underline; text-decoration: underline;
" "
><span ><span
@@ -977,7 +977,7 @@
title="@planepowers" title="@planepowers"
target="_blank" target="_blank"
style=" style="
color: #3f76ff; color: #006399;
text-decoration: underline; text-decoration: underline;
" "
><span ><span
@@ -991,7 +991,7 @@
title="Plane's GitHub conversations" title="Plane's GitHub conversations"
target="_blank" target="_blank"
style=" style="
color: #3f76ff; color: #006399;
text-decoration: underline; text-decoration: underline;
" "
><span ><span
@@ -1007,7 +1007,7 @@
title="Plane's roadmap" title="Plane's roadmap"
target="_blank" target="_blank"
style=" style="
color: #3f76ff; color: #006399;
text-decoration: underline; text-decoration: underline;
" "
><span ><span
@@ -1227,7 +1227,7 @@
href="https://github.com/makeplane" href="https://github.com/makeplane"
target="_blank" target="_blank"
style=" style="
color: #3f76ff; color: #006399;
text-decoration: underline; text-decoration: underline;
" "
> >
@@ -1287,7 +1287,7 @@
href="https://www.linkedin.com/company/planepowers/" href="https://www.linkedin.com/company/planepowers/"
target="_blank" target="_blank"
style=" style="
color: #3f76ff; color: #006399;
text-decoration: underline; text-decoration: underline;
" "
> >
@@ -1347,7 +1347,7 @@
href="https://twitter.com/planepowers" href="https://twitter.com/planepowers"
target="_blank" target="_blank"
style=" style="
color: #3f76ff; color: #006399;
text-decoration: underline; text-decoration: underline;
" "
> >
@@ -1407,7 +1407,7 @@
href="https://plane.so/" href="https://plane.so/"
target="_blank" target="_blank"
style=" style="
color: #3f76ff; color: #006399;
text-decoration: underline; text-decoration: underline;
" "
> >

View File

@@ -173,7 +173,7 @@
text-align: center !important; text-align: center !important;
} }
.r16-r { .r16-r {
background-color: #3f76ff !important; background-color: #006399 !important;
border-radius: 4px !important; border-radius: 4px !important;
border-width: 0px !important; border-width: 0px !important;
box-sizing: border-box; box-sizing: border-box;
@@ -305,7 +305,7 @@
} }
a, a,
a:link { a:link {
color: #3f76ff; color: #006399;
text-decoration: underline; text-decoration: underline;
} }
.nl2go-default-textstyle { .nl2go-default-textstyle {
@@ -382,7 +382,7 @@
<![endif]--> <![endif]-->
<style type="text/css"> <style type="text/css">
a:link { a:link {
color: #3f76ff; color: #006399;
text-decoration: underline; text-decoration: underline;
} }
</style> </style>
@@ -390,7 +390,7 @@
<body <body
bgcolor="#ffffff" bgcolor="#ffffff"
text="#3b3f44" text="#3b3f44"
link="#3f76ff" link="#006399"
yahoo="fix" yahoo="fix"
style="background-color: #ffffff" style="background-color: #ffffff"
> >
@@ -493,7 +493,7 @@
" "
> >
<img <img
src="https://img.mailinblue.com/5942152/images/content_library/original/64708f00b503b149db7ff135.png" src="https://media.docs.plane.so/logo/new-logo-white.png"
width="150" width="150"
border="0" border="0"
style=" style="
@@ -650,9 +650,9 @@
width="285" width="285"
class="r14-o" class="r14-o"
style=" style="
background-color: #3f76ff; background-color: #006399;
border-collapse: separate; border-collapse: separate;
border-color: #3f76ff; border-color: #006399;
border-radius: 4px; border-radius: 4px;
border-style: solid; border-style: solid;
border-width: 0px; border-width: 0px;
@@ -668,7 +668,7 @@
class="r15-i nl2go-default-textstyle" class="r15-i nl2go-default-textstyle"
style=" style="
word-break: break-word; word-break: break-word;
background-color: #3f76ff; background-color: #006399;
border-radius: 4px; border-radius: 4px;
color: #ffffff; color: #ffffff;
font-family: georgia, serif; font-family: georgia, serif;
@@ -964,7 +964,7 @@
title="Plane Support on Discod" title="Plane Support on Discod"
target="_blank" target="_blank"
style=" style="
color: #3f76ff; color: #006399;
text-decoration: underline; text-decoration: underline;
" "
><span ><span
@@ -978,7 +978,7 @@
title="@planepowers" title="@planepowers"
target="_blank" target="_blank"
style=" style="
color: #3f76ff; color: #006399;
text-decoration: underline; text-decoration: underline;
" "
><span ><span
@@ -992,7 +992,7 @@
title="Plane's GitHub conversations" title="Plane's GitHub conversations"
target="_blank" target="_blank"
style=" style="
color: #3f76ff; color: #006399;
text-decoration: underline; text-decoration: underline;
" "
><span ><span
@@ -1008,7 +1008,7 @@
title="Plane's roadmap" title="Plane's roadmap"
target="_blank" target="_blank"
style=" style="
color: #3f76ff; color: #006399;
text-decoration: underline; text-decoration: underline;
" "
><span ><span
@@ -1228,7 +1228,7 @@
href="https://github.com/makeplane" href="https://github.com/makeplane"
target="_blank" target="_blank"
style=" style="
color: #3f76ff; color: #006399;
text-decoration: underline; text-decoration: underline;
" "
> >
@@ -1288,7 +1288,7 @@
href="https://www.linkedin.com/company/planepowers/" href="https://www.linkedin.com/company/planepowers/"
target="_blank" target="_blank"
style=" style="
color: #3f76ff; color: #006399;
text-decoration: underline; text-decoration: underline;
" "
> >
@@ -1348,7 +1348,7 @@
href="https://twitter.com/planepowers" href="https://twitter.com/planepowers"
target="_blank" target="_blank"
style=" style="
color: #3f76ff; color: #006399;
text-decoration: underline; text-decoration: underline;
" "
> >
@@ -1408,7 +1408,7 @@
href="https://plane.so/" href="https://plane.so/"
target="_blank" target="_blank"
style=" style="
color: #3f76ff; color: #006399;
text-decoration: underline; text-decoration: underline;
" "
> >

View File

@@ -39,7 +39,13 @@ export const IssuesClientLayout = observer((props: Props) => {
: null : null
); );
if (!publishSettings && !error) return <LogoSpinner />; if (!publishSettings && !error) {
return (
<div className="flex items-center justify-center h-screen w-full">
<LogoSpinner />
</div>
);
}
if (error) return <SomethingWentWrongError />; if (error) return <SomethingWentWrongError />;

View File

@@ -11,7 +11,13 @@ import { useUser } from "@/hooks/store";
const HomePage = observer(() => { const HomePage = observer(() => {
const { data: currentUser, isAuthenticated, isLoading } = useUser(); const { data: currentUser, isAuthenticated, isLoading } = useUser();
if (isLoading) return <LogoSpinner />;
if (isLoading)
return (
<div className="flex items-center justify-center h-screen w-full">
<LogoSpinner />
</div>
);
if (currentUser && isAuthenticated) return <UserLoggedIn />; if (currentUser && isAuthenticated) return <UserLoggedIn />;

View File

@@ -42,7 +42,13 @@ const IssuesLayout = observer((props: Props) => {
if (error) return <SomethingWentWrongError />; if (error) return <SomethingWentWrongError />;
if (!publishSettings || !viewData) return <LogoSpinner />; if (!publishSettings || !viewData) {
return (
<div className="flex items-center justify-center h-screen w-full">
<LogoSpinner />
</div>
);
}
return ( return (
<div className="relative flex h-screen min-h-[500px] w-screen flex-col overflow-hidden"> <div className="relative flex h-screen min-h-[500px] w-screen flex-col overflow-hidden">

View File

@@ -1,12 +1,11 @@
"use client"; "use client";
import { FC, ReactNode } from "react"; import { FC } from "react";
// helpers // helpers
import { EAuthModes } from "@/types/auth"; import { EAuthModes } from "@/types/auth";
type TAuthHeader = { type TAuthHeader = {
authMode: EAuthModes; authMode: EAuthModes;
children: ReactNode;
}; };
type TAuthHeaderContent = { type TAuthHeaderContent = {
@@ -30,7 +29,7 @@ const Titles: TAuthHeaderDetails = {
}; };
export const AuthHeader: FC<TAuthHeader> = (props) => { export const AuthHeader: FC<TAuthHeader> = (props) => {
const { authMode, children } = props; const { authMode } = props;
const getHeaderSubHeader = (mode: EAuthModes | null): TAuthHeaderContent => { const getHeaderSubHeader = (mode: EAuthModes | null): TAuthHeaderContent => {
if (mode) { if (mode) {
@@ -38,7 +37,7 @@ export const AuthHeader: FC<TAuthHeader> = (props) => {
} }
return { return {
header: "Comment or react to work itemss", header: "Comment or react to work items",
subHeader: "Use plane to add your valuable inputs to features.", subHeader: "Use plane to add your valuable inputs to features.",
}; };
}; };
@@ -47,11 +46,10 @@ export const AuthHeader: FC<TAuthHeader> = (props) => {
return ( return (
<> <>
<div className="space-y-1 text-center"> <div className="flex flex-col gap-1">
<h3 className="text-xl sm:text-2xl md:text-3xl font-bold text-onboarding-text-100">{header}</h3> <span className="text-2xl font-semibold text-custom-text-100 leading-7">{header}</span>
<p className="text-xs sm:text-sm md:text-base font-medium text-onboarding-text-400">{subHeader}</p> <span className="text-2xl font-semibold text-custom-text-400 leading-7">{subHeader}</span>
</div> </div>
{children}
</> </>
); );
}; };

View File

@@ -2,10 +2,14 @@
import React, { FC, useEffect, useState } from "react"; import React, { FC, useEffect, useState } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import Image from "next/image";
import { useSearchParams } from "next/navigation"; import { useSearchParams } from "next/navigation";
import { useTheme } from "next-themes";
// plane imports // plane imports
import { API_BASE_URL } from "@plane/constants";
import { SitesAuthService } from "@plane/services"; import { SitesAuthService } from "@plane/services";
import { IEmailCheckData } from "@plane/types"; import { IEmailCheckData } from "@plane/types";
import { OAuthOptions } from "@plane/ui";
// components // components
import { import {
AuthHeader, AuthHeader,
@@ -13,7 +17,6 @@ import {
AuthEmailForm, AuthEmailForm,
AuthUniqueCodeForm, AuthUniqueCodeForm,
AuthPasswordForm, AuthPasswordForm,
OAuthOptions,
TermsAndConditions, TermsAndConditions,
} from "@/components/account"; } from "@/components/account";
// helpers // helpers
@@ -27,6 +30,11 @@ import {
import { useInstance } from "@/hooks/store"; import { useInstance } from "@/hooks/store";
// types // types
import { EAuthModes, EAuthSteps } from "@/types/auth"; import { EAuthModes, EAuthSteps } from "@/types/auth";
// assets
import GithubLightLogo from "/public/logos/github-black.png";
import GithubDarkLogo from "/public/logos/github-dark.svg";
import GitlabLogo from "/public/logos/gitlab-logo.svg";
import GoogleLogo from "/public/logos/google-logo.svg";
const authService = new SitesAuthService(); const authService = new SitesAuthService();
@@ -36,6 +44,7 @@ export const AuthRoot: FC = observer(() => {
const emailParam = searchParams.get("email") || undefined; const emailParam = searchParams.get("email") || undefined;
const error_code = searchParams.get("error_code") || undefined; const error_code = searchParams.get("error_code") || undefined;
const nextPath = searchParams.get("next_path") || undefined; const nextPath = searchParams.get("next_path") || undefined;
const next_path = searchParams.get("next_path");
// states // states
const [authMode, setAuthMode] = useState<EAuthModes>(EAuthModes.SIGN_UP); const [authMode, setAuthMode] = useState<EAuthModes>(EAuthModes.SIGN_UP);
const [authStep, setAuthStep] = useState<EAuthSteps>(EAuthSteps.EMAIL); const [authStep, setAuthStep] = useState<EAuthSteps>(EAuthSteps.EMAIL);
@@ -43,6 +52,7 @@ export const AuthRoot: FC = observer(() => {
const [errorInfo, setErrorInfo] = useState<TAuthErrorInfo | undefined>(undefined); const [errorInfo, setErrorInfo] = useState<TAuthErrorInfo | undefined>(undefined);
const [isPasswordAutoset, setIsPasswordAutoset] = useState(true); const [isPasswordAutoset, setIsPasswordAutoset] = useState(true);
// hooks // hooks
const { resolvedTheme } = useTheme();
const { config } = useInstance(); const { config } = useInstance();
useEffect(() => { useEffect(() => {
@@ -146,12 +156,54 @@ export const AuthRoot: FC = observer(() => {
}); });
}; };
const content = authMode === EAuthModes.SIGN_UP ? "Sign up" : "Sign in";
const OAuthConfig = [
{
id: "google",
text: `${content} with Google`,
icon: <Image src={GoogleLogo} height={18} width={18} alt="Google Logo" />,
onClick: () => {
window.location.assign(`${API_BASE_URL}/auth/google/${next_path ? `?next_path=${next_path}` : ``}`);
},
enabled: config?.is_google_enabled,
},
{
id: "github",
text: `${content} with GitHub`,
icon: (
<Image
src={resolvedTheme === "dark" ? GithubDarkLogo : GithubLightLogo}
height={18}
width={18}
alt="GitHub Logo"
/>
),
onClick: () => {
window.location.assign(`${API_BASE_URL}/auth/github/${next_path ? `?next_path=${next_path}` : ``}`);
},
enabled: config?.is_github_enabled,
},
{
id: "gitlab",
text: `${content} with GitLab`,
icon: <Image src={GitlabLogo} height={18} width={18} alt="GitLab Logo" />,
onClick: () => {
window.location.assign(`${API_BASE_URL}/auth/gitlab/${next_path ? `?next_path=${next_path}` : ``}`);
},
enabled: config?.is_gitlab_enabled,
},
];
return ( return (
<div className="relative flex flex-col space-y-6"> <div className="flex flex-col justify-center items-center flex-grow w-full py-6 mt-10">
<AuthHeader authMode={authMode}> <div className="relative flex flex-col gap-6 max-w-[22.5rem] w-full">
{errorInfo && errorInfo?.type === EErrorAlertType.BANNER_ALERT && ( {errorInfo && errorInfo?.type === EErrorAlertType.BANNER_ALERT && (
<AuthBanner bannerData={errorInfo} handleBannerData={(value) => setErrorInfo(value)} /> <AuthBanner bannerData={errorInfo} handleBannerData={(value) => setErrorInfo(value)} />
)} )}
<AuthHeader authMode={authMode} />
{isOAuthEnabled && <OAuthOptions options={OAuthConfig} compact={authStep === EAuthSteps.PASSWORD} />}
{authStep === EAuthSteps.EMAIL && <AuthEmailForm defaultEmail={email} onSubmit={handleEmailVerification} />} {authStep === EAuthSteps.EMAIL && <AuthEmailForm defaultEmail={email} onSubmit={handleEmailVerification} />}
{authStep === EAuthSteps.UNIQUE_CODE && ( {authStep === EAuthSteps.UNIQUE_CODE && (
<AuthUniqueCodeForm <AuthUniqueCodeForm
@@ -182,9 +234,8 @@ export const AuthRoot: FC = observer(() => {
}} }}
/> />
)} )}
{isOAuthEnabled && <OAuthOptions />}
<TermsAndConditions isSignUp={authMode === EAuthModes.SIGN_UP ? true : false} /> <TermsAndConditions isSignUp={authMode === EAuthModes.SIGN_UP ? true : false} />
</AuthHeader> </div>
</div> </div>
); );
}); });

View File

@@ -46,13 +46,13 @@ export const AuthEmailForm: FC<TAuthEmailForm> = observer((props) => {
return ( return (
<form onSubmit={handleFormSubmit} className="mt-5 space-y-4"> <form onSubmit={handleFormSubmit} className="mt-5 space-y-4">
<div className="space-y-1"> <div className="space-y-1">
<label className="text-sm text-onboarding-text-300 font-medium" htmlFor="email"> <label className="text-sm text-custom-text-300 font-medium" htmlFor="email">
Email Email
</label> </label>
<div <div
className={cn( className={cn(
`relative flex items-center rounded-md bg-onboarding-background-200 border`, `relative flex items-center rounded-md bg-custom-background-100 border`,
!isFocused && Boolean(emailError?.email) ? `border-red-500` : `border-onboarding-border-100` !isFocused && Boolean(emailError?.email) ? `border-red-500` : `border-custom-border-100`
)} )}
onFocus={() => { onFocus={() => {
setIsFocused(true); setIsFocused(true);
@@ -68,7 +68,7 @@ export const AuthEmailForm: FC<TAuthEmailForm> = observer((props) => {
value={email} value={email}
onChange={(e) => setEmail(e.target.value)} onChange={(e) => setEmail(e.target.value)}
placeholder="name@company.com" placeholder="name@company.com"
className={`disable-autofill-style h-[46px] w-full placeholder:text-onboarding-text-400 autofill:bg-red-500 border-0 focus:bg-none active:bg-transparent`} className={`disable-autofill-style h-10 w-full placeholder:text-custom-text-400 autofill:bg-red-500 border-0 focus:bg-none active:bg-transparent`}
autoComplete="on" autoComplete="on"
autoFocus autoFocus
ref={inputRef} ref={inputRef}
@@ -83,7 +83,7 @@ export const AuthEmailForm: FC<TAuthEmailForm> = observer((props) => {
}} }}
tabIndex={-1} tabIndex={-1}
> >
<XCircle className="h-[46px] w-11 px-3 stroke-custom-text-400 hover:cursor-pointer text-xs" /> <XCircle className="h-10 w-11 px-3 stroke-custom-text-400 hover:cursor-pointer text-xs" />
</button> </button>
)} )}
</div> </div>

View File

@@ -117,11 +117,11 @@ export const AuthPasswordForm: React.FC<Props> = observer((props: Props) => {
<input type="hidden" value={passwordFormData.email} name="email" /> <input type="hidden" value={passwordFormData.email} name="email" />
<input type="hidden" value={nextPath} name="next_path" /> <input type="hidden" value={nextPath} name="next_path" />
<div className="space-y-1"> <div className="space-y-1">
<label className="text-sm font-medium text-onboarding-text-300" htmlFor="email"> <label className="text-sm font-medium text-custom-text-300" htmlFor="email">
Email Email
</label> </label>
<div <div
className={`relative flex items-center rounded-md bg-onboarding-background-200 border border-onboarding-border-100`} className={`relative flex items-center rounded-md bg-custom-background-100 border border-custom-border-100`}
> >
<Input <Input
id="email" id="email"
@@ -130,7 +130,7 @@ export const AuthPasswordForm: React.FC<Props> = observer((props: Props) => {
value={passwordFormData.email} value={passwordFormData.email}
onChange={(e) => handleFormChange("email", e.target.value)} onChange={(e) => handleFormChange("email", e.target.value)}
placeholder="name@company.com" placeholder="name@company.com"
className={`disable-autofill-style h-[46px] w-full placeholder:text-onboarding-text-400 border-0`} className={`disable-autofill-style h-10 w-full placeholder:text-custom-text-400 border-0`}
disabled disabled
/> />
{passwordFormData.email.length > 0 && ( {passwordFormData.email.length > 0 && (
@@ -143,17 +143,17 @@ export const AuthPasswordForm: React.FC<Props> = observer((props: Props) => {
</div> </div>
<div className="space-y-1"> <div className="space-y-1">
<label className="text-sm text-onboarding-text-300 font-medium" htmlFor="password"> <label className="text-sm text-custom-text-300 font-medium" htmlFor="password">
{mode === EAuthModes.SIGN_IN ? "Password" : "Set a password"} {mode === EAuthModes.SIGN_IN ? "Password" : "Set a password"}
</label> </label>
<div className="relative flex items-center rounded-md bg-onboarding-background-200"> <div className="relative flex items-center rounded-md bg-custom-background-100">
<Input <Input
type={showPassword?.password ? "text" : "password"} type={showPassword?.password ? "text" : "password"}
name="password" name="password"
value={passwordFormData.password} value={passwordFormData.password}
onChange={(e) => handleFormChange("password", e.target.value)} onChange={(e) => handleFormChange("password", e.target.value)}
placeholder="Enter password" placeholder="Enter password"
className="disable-autofill-style h-[46px] w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 placeholder:text-onboarding-text-400" className="disable-autofill-style h-10 w-full border border-custom-border-100 !bg-custom-background-100 pr-12 placeholder:text-custom-text-400"
onFocus={() => setIsPasswordInputFocused(true)} onFocus={() => setIsPasswordInputFocused(true)}
onBlur={() => setIsPasswordInputFocused(false)} onBlur={() => setIsPasswordInputFocused(false)}
autoComplete="on" autoComplete="on"
@@ -176,17 +176,17 @@ export const AuthPasswordForm: React.FC<Props> = observer((props: Props) => {
{mode === EAuthModes.SIGN_UP && ( {mode === EAuthModes.SIGN_UP && (
<div className="space-y-1"> <div className="space-y-1">
<label className="text-sm text-onboarding-text-300 font-medium" htmlFor="confirm_password"> <label className="text-sm text-custom-text-300 font-medium" htmlFor="confirm_password">
Confirm password Confirm password
</label> </label>
<div className="relative flex items-center rounded-md bg-onboarding-background-200"> <div className="relative flex items-center rounded-md bg-custom-background-100">
<Input <Input
type={showPassword?.retypePassword ? "text" : "password"} type={showPassword?.retypePassword ? "text" : "password"}
name="confirm_password" name="confirm_password"
value={passwordFormData.confirm_password} value={passwordFormData.confirm_password}
onChange={(e) => handleFormChange("confirm_password", e.target.value)} onChange={(e) => handleFormChange("confirm_password", e.target.value)}
placeholder="Confirm password" placeholder="Confirm password"
className="disable-autofill-style h-[46px] w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 placeholder:text-onboarding-text-400" className="disable-autofill-style h-10 w-full border border-custom-border-100 !bg-custom-background-100 pr-12 placeholder:text-custom-text-400"
onFocus={() => setIsRetryPasswordInputFocused(true)} onFocus={() => setIsRetryPasswordInputFocused(true)}
onBlur={() => setIsRetryPasswordInputFocused(false)} onBlur={() => setIsRetryPasswordInputFocused(false)}
/> />

View File

@@ -81,11 +81,11 @@ export const AuthUniqueCodeForm: React.FC<TAuthUniqueCodeForm> = (props) => {
<input type="hidden" value={uniqueCodeFormData.email} name="email" /> <input type="hidden" value={uniqueCodeFormData.email} name="email" />
<input type="hidden" value={nextPath} name="next_path" /> <input type="hidden" value={nextPath} name="next_path" />
<div className="space-y-1"> <div className="space-y-1">
<label className="text-sm font-medium text-onboarding-text-300" htmlFor="email"> <label className="text-sm font-medium text-custom-text-300" htmlFor="email">
Email Email
</label> </label>
<div <div
className={`relative flex items-center rounded-md bg-onboarding-background-200 border border-onboarding-border-100`} className={`relative flex items-center rounded-md bg-custom-background-100 border border-custom-border-100`}
> >
<Input <Input
id="email" id="email"
@@ -94,7 +94,7 @@ export const AuthUniqueCodeForm: React.FC<TAuthUniqueCodeForm> = (props) => {
value={uniqueCodeFormData.email} value={uniqueCodeFormData.email}
onChange={(e) => handleFormChange("email", e.target.value)} onChange={(e) => handleFormChange("email", e.target.value)}
placeholder="name@company.com" placeholder="name@company.com"
className={`disable-autofill-style h-[46px] w-full placeholder:text-onboarding-text-400 border-0`} className={`disable-autofill-style h-10 w-full placeholder:text-custom-text-400 border-0`}
disabled disabled
/> />
{uniqueCodeFormData.email.length > 0 && ( {uniqueCodeFormData.email.length > 0 && (
@@ -107,7 +107,7 @@ export const AuthUniqueCodeForm: React.FC<TAuthUniqueCodeForm> = (props) => {
</div> </div>
<div className="space-y-1"> <div className="space-y-1">
<label className="text-sm font-medium text-onboarding-text-300" htmlFor="code"> <label className="text-sm font-medium text-custom-text-300" htmlFor="code">
Unique code Unique code
</label> </label>
<Input <Input
@@ -115,7 +115,7 @@ export const AuthUniqueCodeForm: React.FC<TAuthUniqueCodeForm> = (props) => {
value={uniqueCodeFormData.code} value={uniqueCodeFormData.code}
onChange={(e) => handleFormChange("code", e.target.value)} onChange={(e) => handleFormChange("code", e.target.value)}
placeholder="gets-sets-flys" placeholder="gets-sets-flys"
className="disable-autofill-style h-[46px] w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 placeholder:text-onboarding-text-400" className="disable-autofill-style h-10 w-full border border-custom-border-100 !bg-custom-background-100 pr-12 placeholder:text-custom-text-400"
autoFocus autoFocus
/> />
<div className="flex w-full items-center justify-between px-1 text-xs pt-1"> <div className="flex w-full items-center justify-between px-1 text-xs pt-1">
@@ -128,7 +128,7 @@ export const AuthUniqueCodeForm: React.FC<TAuthUniqueCodeForm> = (props) => {
onClick={() => generateNewCode(uniqueCodeFormData.email)} onClick={() => generateNewCode(uniqueCodeFormData.email)}
className={`${ className={`${
isRequestNewCodeDisabled isRequestNewCodeDisabled
? "text-onboarding-text-400" ? "text-custom-text-400"
: "font-medium text-custom-primary-300 hover:text-custom-primary-200" : "font-medium text-custom-primary-300 hover:text-custom-primary-200"
}`} }`}
disabled={isRequestNewCodeDisabled} disabled={isRequestNewCodeDisabled}

View File

@@ -1,4 +1,3 @@
export * from "./auth-forms"; export * from "./auth-forms";
export * from "./oauth";
export * from "./terms-and-conditions"; export * from "./terms-and-conditions";
export * from "./user-logged-in"; export * from "./user-logged-in";

View File

@@ -1,41 +0,0 @@
import { FC } from "react";
import Image from "next/image";
import { useSearchParams } from "next/navigation";
import { useTheme } from "next-themes";
import { API_BASE_URL } from "@plane/constants";
// images
import githubLightModeImage from "/public/logos/github-black.png";
import githubDarkModeImage from "/public/logos/github-dark.svg";
export type GithubOAuthButtonProps = {
text: string;
};
export const GithubOAuthButton: FC<GithubOAuthButtonProps> = (props) => {
const searchParams = useSearchParams();
const nextPath = searchParams.get("next_path") || undefined;
const { text } = props;
// hooks
const { resolvedTheme } = useTheme();
const handleSignIn = () => {
window.location.assign(`${API_BASE_URL}/auth/spaces/github/${nextPath ? `?next_path=${nextPath}` : ``}`);
};
return (
<button
className={`flex h-[42px] w-full items-center justify-center gap-2 rounded border px-2 text-sm font-medium text-custom-text-100 duration-300 hover:bg-onboarding-background-300 ${
resolvedTheme === "dark" ? "border-[#43484F] bg-[#2F3135]" : "border-[#D9E4FF]"
}`}
onClick={handleSignIn}
>
<Image
src={resolvedTheme === "dark" ? githubDarkModeImage : githubLightModeImage}
height={20}
width={20}
alt="GitHub Logo"
/>
{text}
</button>
);
};

View File

@@ -1,35 +0,0 @@
import { FC } from "react";
import Image from "next/image";
import { useSearchParams } from "next/navigation";
import { useTheme } from "next-themes";
import { API_BASE_URL } from "@plane/constants";
// images
import GitlabLogo from "/public/logos/gitlab-logo.svg";
export type GitlabOAuthButtonProps = {
text: string;
};
export const GitlabOAuthButton: FC<GitlabOAuthButtonProps> = (props) => {
const searchParams = useSearchParams();
const nextPath = searchParams.get("next_path") || undefined;
const { text } = props;
// hooks
const { resolvedTheme } = useTheme();
const handleSignIn = () => {
window.location.assign(`${API_BASE_URL}/auth/spaces/gitlab/${nextPath ? `?next_path=${nextPath}` : ``}`);
};
return (
<button
className={`flex h-[42px] w-full items-center justify-center gap-2 rounded border px-2 text-sm font-medium text-custom-text-100 duration-300 hover:bg-onboarding-background-300 ${
resolvedTheme === "dark" ? "border-[#43484F] bg-[#2F3135]" : "border-[#D9E4FF]"
}`}
onClick={handleSignIn}
>
<Image src={GitlabLogo} height={20} width={20} alt="GitLab Logo" />
{text}
</button>
);
};

View File

@@ -1,35 +0,0 @@
import { FC } from "react";
import Image from "next/image";
import { useSearchParams } from "next/navigation";
import { useTheme } from "next-themes";
import { API_BASE_URL } from "@plane/constants";
// images
import GoogleLogo from "/public/logos/google-logo.svg";
export type GoogleOAuthButtonProps = {
text: string;
};
export const GoogleOAuthButton: FC<GoogleOAuthButtonProps> = (props) => {
const searchParams = useSearchParams();
const nextPath = searchParams.get("next_path") || undefined;
const { text } = props;
// hooks
const { resolvedTheme } = useTheme();
const handleSignIn = () => {
window.location.assign(`${API_BASE_URL}/auth/spaces/google/${nextPath ? `?next_path=${nextPath}` : ``}`);
};
return (
<button
className={`flex h-[42px] w-full items-center justify-center gap-2 rounded border px-2 text-sm font-medium text-custom-text-100 duration-300 hover:bg-onboarding-background-300 ${
resolvedTheme === "dark" ? "border-[#43484F] bg-[#2F3135]" : "border-[#D9E4FF]"
}`}
onClick={handleSignIn}
>
<Image src={GoogleLogo} height={18} width={18} alt="Google Logo" />
{text}
</button>
);
};

View File

@@ -1,4 +0,0 @@
export * from "./oauth-options";
export * from "./google-button";
export * from "./github-button";
export * from "./gitlab-button";

View File

@@ -1,29 +0,0 @@
import { observer } from "mobx-react";
// components
import { GithubOAuthButton, GitlabOAuthButton, GoogleOAuthButton } from "@/components/account";
// hooks
import { useInstance } from "@/hooks/store";
export const OAuthOptions: React.FC = observer(() => {
// hooks
const { config } = useInstance();
return (
<>
<div className="mt-4 flex items-center">
<hr className="w-full border-onboarding-border-100" />
<p className="mx-3 flex-shrink-0 text-center text-sm text-onboarding-text-400">or</p>
<hr className="w-full border-onboarding-border-100" />
</div>
<div className={`mt-7 grid gap-4 overflow-hidden`}>
{config?.is_google_enabled && (
<div className="flex h-[42px] items-center !overflow-hidden">
<GoogleOAuthButton text="Sign in with Google" />
</div>
)}
{config?.is_github_enabled && <GithubOAuthButton text="Sign in with GitHub" />}
{config?.is_gitlab_enabled && <GitlabOAuthButton text="Sign in with GitLab" />}
</div>
</>
);
});

View File

@@ -11,7 +11,7 @@ export const TermsAndConditions: FC<Props> = (props) => {
const { isSignUp = false } = props; const { isSignUp = false } = props;
return ( return (
<span className="flex items-center justify-center py-6"> <span className="flex items-center justify-center py-6">
<p className="text-center text-sm text-onboarding-text-200 whitespace-pre-line"> <p className="text-center text-sm text-custom-text-200 whitespace-pre-line">
{isSignUp ? "By creating an account" : "By signing in"}, you agree to our{" \n"} {isSignUp ? "By creating an account" : "By signing in"}, you agree to our{" \n"}
<Link href="https://plane.so/legals/terms-and-conditions" target="_blank" rel="noopener noreferrer"> <Link href="https://plane.so/legals/terms-and-conditions" target="_blank" rel="noopener noreferrer">
<span className="text-sm font-medium underline hover:cursor-pointer">Terms of Service</span> <span className="text-sm font-medium underline hover:cursor-pointer">Terms of Service</span>

View File

@@ -2,33 +2,25 @@
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import Image from "next/image"; import Image from "next/image";
import { useTheme } from "next-themes"; import { PlaneLockup } from "@plane/ui";
// components // components
import { PoweredBy } from "@/components/common"; import { PoweredBy } from "@/components/common";
import { UserAvatar } from "@/components/issues"; import { UserAvatar } from "@/components/issues";
// hooks // hooks
import { useUser } from "@/hooks/store"; import { useUser } from "@/hooks/store";
// assets // assets
import PlaneBlackLogo from "@/public/plane-logos/black-horizontal-with-blue-logo.png";
import PlaneWhiteLogo from "@/public/plane-logos/white-horizontal-with-blue-logo.png";
import UserLoggedInImage from "@/public/user-logged-in.svg"; import UserLoggedInImage from "@/public/user-logged-in.svg";
export const UserLoggedIn = observer(() => { export const UserLoggedIn = observer(() => {
// store hooks // store hooks
const { data: user } = useUser(); const { data: user } = useUser();
// next-themes
const { resolvedTheme } = useTheme();
const logo = resolvedTheme === "dark" ? PlaneWhiteLogo : PlaneBlackLogo;
if (!user) return null; if (!user) return null;
return ( return (
<div className="flex flex-col h-screen w-screen"> <div className="flex flex-col h-screen w-screen">
<div className="relative flex w-full items-center justify-between gap-4 border-b border-custom-border-200 px-6 py-5"> <div className="relative flex w-full items-center justify-between gap-4 border-b border-custom-border-200 px-6 py-5">
<div className="h-[30px] w-[133px]"> <PlaneLockup className="h-6 w-auto text-custom-text-100" />
<Image src={logo} alt="Plane logo" />
</div>
<UserAvatar /> <UserAvatar />
</div> </div>

View File

@@ -11,10 +11,8 @@ export const LogoSpinner = () => {
const logoSrc = resolvedTheme === "dark" ? LogoSpinnerDark : LogoSpinnerLight; const logoSrc = resolvedTheme === "dark" ? LogoSpinnerDark : LogoSpinnerLight;
return ( return (
<div className="h-screen w-full flex min-h-[600px] justify-center items-center"> <div className="flex items-center justify-center">
<div className="flex items-center justify-center"> <Image src={logoSrc} alt="logo" className="h-6 w-auto sm:h-11" />
<Image src={logoSrc} alt="logo" className="w-[82px] h-[82px] mr-2" />
</div>
</div> </div>
); );
}; };

View File

@@ -1,10 +1,9 @@
"use client"; "use client";
import { FC } from "react"; import { FC } from "react";
import Image from "next/image";
import { WEBSITE_URL } from "@plane/constants"; import { WEBSITE_URL } from "@plane/constants";
// assets // assets
import planeLogo from "@/public/plane-logo.svg"; import { PlaneLogo } from "@plane/ui";
type TPoweredBy = { type TPoweredBy = {
disabled?: boolean; disabled?: boolean;
@@ -23,9 +22,7 @@ export const PoweredBy: FC<TPoweredBy> = (props) => {
target="_blank" target="_blank"
rel="noreferrer noopener" rel="noreferrer noopener"
> >
<div className="relative grid h-6 w-6 place-items-center"> <PlaneLogo className="h-3 w-auto text-custom-text-100" />
<Image src={planeLogo} alt="Plane logo" className="h-6 w-6" height="24" width="24" />
</div>
<div className="text-xs"> <div className="text-xs">
Powered by <span className="font-semibold">Plane Publish</span> Powered by <span className="font-semibold">Plane Publish</span>
</div> </div>

View File

@@ -1,47 +1,14 @@
"use client"; "use client";
import { observer } from "mobx-react";
import Image from "next/image";
import Link from "next/link";
import { useTheme } from "next-themes";
import { SPACE_BASE_PATH } from "@plane/constants";
// components // components
import { AuthRoot } from "@/components/account"; import { AuthRoot } from "@/components/account";
import { PoweredBy } from "@/components/common"; import { PoweredBy } from "@/components/common";
// images import { AuthHeader } from "./header";
import PlaneBackgroundPatternDark from "@/public/auth/background-pattern-dark.svg";
import PlaneBackgroundPattern from "@/public/auth/background-pattern.svg";
import BlackHorizontalLogo from "@/public/plane-logos/black-horizontal-with-blue-logo.png";
import WhiteHorizontalLogo from "@/public/plane-logos/white-horizontal-with-blue-logo.png";
export const AuthView = observer(() => { export const AuthView = () => (
// hooks <div className="relative z-10 flex flex-col items-center w-screen h-screen overflow-hidden overflow-y-auto pt-6 pb-10 px-8">
const { resolvedTheme } = useTheme(); <AuthHeader />
<AuthRoot />
const logo = resolvedTheme === "light" ? BlackHorizontalLogo : WhiteHorizontalLogo; <PoweredBy />
</div>
return ( );
<div className="relative w-screen h-screen overflow-hidden">
<div className="absolute inset-0 z-0">
<Image
src={resolvedTheme === "dark" ? PlaneBackgroundPatternDark : PlaneBackgroundPattern}
className="w-full h-full object-cover"
alt="Plane background pattern"
/>
</div>
<div className="relative z-10 w-screen h-screen overflow-hidden overflow-y-auto flex flex-col">
<div className="container min-w-full px-10 lg:px-20 xl:px-36 flex-shrink-0 relative flex items-center justify-between pb-4 transition-all">
<div className="flex items-center gap-x-2 py-10">
<Link href={`${SPACE_BASE_PATH}/`} className="h-[30px] w-[133px]">
<Image src={logo} alt="Plane logo" />
</Link>
</div>
</div>
<div className="flex flex-col justify-center flex-grow container h-[100vh-60px] mx-auto max-w-lg px-10 lg:max-w-md lg:px-5 transition-all">
<AuthRoot />
</div>
</div>
<PoweredBy />
</div>
);
});

View File

@@ -0,0 +1,13 @@
"use client";
import React from "react";
import Link from "next/link";
import { PlaneLockup } from "@plane/ui";
export const AuthHeader = () => (
<div className="flex items-center justify-between gap-6 w-full flex-shrink-0 sticky top-0">
<Link href="/">
<PlaneLockup height={20} width={95} className="text-custom-text-100" />
</Link>
</div>
);

View File

@@ -38,7 +38,7 @@ export const InstanceProvider = observer(({ children }: { children: ReactNode })
if (!instance && !error) if (!instance && !error)
return ( return (
<div className="flex h-screen min-h-[500px] w-full justify-center items-center"> <div className="flex items-center justify-center h-screen w-full">
<LogoSpinner /> <LogoSpinner />
</div> </div>
); );

View File

@@ -51,6 +51,7 @@ export class ProfileStore implements IProfileStore {
billing_address_country: undefined, billing_address_country: undefined,
billing_address: undefined, billing_address: undefined,
has_billing_address: false, has_billing_address: false,
has_marketing_email_consent: false,
created_at: "", created_at: "",
updated_at: "", updated_at: "",
language: "", language: "",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 466 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 761 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 919 B

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -1 +1,11 @@
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} {
"name": "",
"short_name": "",
"icons": [
{ "src": "/favicon/android-chrome-192x192.png", "sizes": "192x192", "type": "image/png" },
{ "src": "/favicon/android-chrome-512x512.png", "sizes": "512x512", "type": "image/png" }
],
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 954 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 418 KiB

View File

@@ -12,24 +12,24 @@
:root { :root {
color-scheme: light !important; color-scheme: light !important;
--color-primary-10: 236, 241, 255; --color-primary-10: 229, 243, 250;
--color-primary-20: 217, 228, 255; --color-primary-20: 216, 237, 248;
--color-primary-30: 197, 214, 255; --color-primary-30: 199, 229, 244;
--color-primary-40: 178, 200, 255; --color-primary-40: 169, 214, 239;
--color-primary-50: 159, 187, 255; --color-primary-50: 144, 202, 234;
--color-primary-60: 140, 173, 255; --color-primary-60: 109, 186, 227;
--color-primary-70: 121, 159, 255; --color-primary-70: 75, 170, 221;
--color-primary-80: 101, 145, 255; --color-primary-80: 41, 154, 214;
--color-primary-90: 82, 132, 255; --color-primary-90: 34, 129, 180;
--color-primary-100: 63, 118, 255; --color-primary-100: 0, 99, 153;
--color-primary-200: 57, 106, 230; --color-primary-200: 0, 92, 143;
--color-primary-300: 50, 94, 204; --color-primary-300: 0, 86, 133;
--color-primary-400: 44, 83, 179; --color-primary-400: 0, 77, 117;
--color-primary-500: 38, 71, 153; --color-primary-500: 0, 66, 102;
--color-primary-600: 32, 59, 128; --color-primary-600: 0, 53, 82;
--color-primary-700: 25, 47, 102; --color-primary-700: 0, 43, 66;
--color-primary-800: 19, 35, 76; --color-primary-800: 0, 33, 51;
--color-primary-900: 13, 24, 51; --color-primary-900: 0, 23, 36;
--color-background-100: 255, 255, 255; /* primary bg */ --color-background-100: 255, 255, 255; /* primary bg */
--color-background-90: 247, 247, 247; /* secondary bg */ --color-background-90: 247, 247, 247; /* secondary bg */
@@ -185,6 +185,25 @@
[data-theme="dark-contrast"] { [data-theme="dark-contrast"] {
color-scheme: dark !important; color-scheme: dark !important;
--color-primary-10: 8, 31, 43;
--color-primary-20: 10, 37, 51;
--color-primary-30: 13, 49, 69;
--color-primary-40: 16, 58, 81;
--color-primary-50: 18, 68, 94;
--color-primary-60: 23, 86, 120;
--color-primary-70: 28, 104, 146;
--color-primary-80: 31, 116, 163;
--color-primary-90: 34, 129, 180;
--color-primary-100: 40, 146, 204;
--color-primary-200: 41, 154, 214;
--color-primary-300: 75, 170, 221;
--color-primary-400: 109, 186, 227;
--color-primary-500: 144, 202, 234;
--color-primary-600: 169, 214, 239;
--color-primary-700: 199, 229, 244;
--color-primary-800: 216, 237, 248;
--color-primary-900: 229, 243, 250;
--color-background-100: 25, 25, 25; /* primary bg */ --color-background-100: 25, 25, 25; /* primary bg */
--color-background-90: 32, 32, 32; /* secondary bg */ --color-background-90: 32, 32, 32; /* secondary bg */
--color-background-80: 44, 44, 44; /* tertiary bg */ --color-background-80: 44, 44, 44; /* tertiary bg */
@@ -274,25 +293,6 @@
[data-theme="dark"], [data-theme="dark"],
[data-theme="light-contrast"], [data-theme="light-contrast"],
[data-theme="dark-contrast"] { [data-theme="dark-contrast"] {
--color-primary-10: 236, 241, 255;
--color-primary-20: 217, 228, 255;
--color-primary-30: 197, 214, 255;
--color-primary-40: 178, 200, 255;
--color-primary-50: 159, 187, 255;
--color-primary-60: 140, 173, 255;
--color-primary-70: 121, 159, 255;
--color-primary-80: 101, 145, 255;
--color-primary-90: 82, 132, 255;
--color-primary-100: 63, 118, 255;
--color-primary-200: 57, 106, 230;
--color-primary-300: 50, 94, 204;
--color-primary-400: 44, 83, 179;
--color-primary-500: 38, 71, 153;
--color-primary-600: 32, 59, 128;
--color-primary-700: 25, 47, 102;
--color-primary-800: 19, 35, 76;
--color-primary-900: 13, 24, 51;
--color-sidebar-background-100: var(--color-background-100); /* primary sidebar bg */ --color-sidebar-background-100: var(--color-background-100); /* primary sidebar bg */
--color-sidebar-background-90: var(--color-background-90); /* secondary sidebar bg */ --color-sidebar-background-90: var(--color-background-90); /* secondary sidebar bg */
--color-sidebar-background-80: var(--color-background-80); /* tertiary sidebar bg */ --color-sidebar-background-80: var(--color-background-80); /* tertiary sidebar bg */

View File

@@ -1,203 +1,24 @@
"use client"; "use client";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import Image from "next/image"; // components
import Link from "next/link"; import { ForgotPasswordForm } from "@/components/account/auth-forms/forgot-password";
import { useSearchParams } from "next/navigation"; import { AuthHeader } from "@/components/auth-screens/header";
import { useTheme } from "next-themes";
import { Controller, useForm } from "react-hook-form";
// icons
import { CircleCheck } from "lucide-react";
// plane imports
import { AUTH_TRACKER_ELEMENTS, AUTH_TRACKER_EVENTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { Button, Input, TOAST_TYPE, getButtonStyling, setToast } from "@plane/ui";
import { cn, checkEmailValidity } from "@plane/utils";
// helpers // helpers
import { EPageTypes } from "@/helpers/authentication.helper"; import { EAuthModes, EPageTypes } from "@/helpers/authentication.helper";
// hooks // layouts
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper"; import DefaultLayout from "@/layouts/default-layout";
import { useInstance } from "@/hooks/store";
import useTimer from "@/hooks/use-timer";
// wrappers
import { AuthenticationWrapper } from "@/lib/wrappers"; import { AuthenticationWrapper } from "@/lib/wrappers";
// images
import PlaneBackgroundPatternDark from "@/public/auth/background-pattern-dark.svg";
import PlaneBackgroundPattern from "@/public/auth/background-pattern.svg";
import BlackHorizontalLogo from "@/public/plane-logos/black-horizontal-with-blue-logo.png";
import WhiteHorizontalLogo from "@/public/plane-logos/white-horizontal-with-blue-logo.png";
// services
import { AuthService } from "@/services/auth.service";
type TForgotPasswordFormValues = { const ForgotPasswordPage = observer(() => (
email: string; <DefaultLayout>
};
const defaultValues: TForgotPasswordFormValues = {
email: "",
};
// services
const authService = new AuthService();
const ForgotPasswordPage = observer(() => {
// search params
const searchParams = useSearchParams();
const email = searchParams.get("email");
// plane hooks
const { t } = useTranslation();
const { config } = useInstance();
// hooks
const { resolvedTheme } = useTheme();
// timer
const { timer: resendTimerCode, setTimer: setResendCodeTimer } = useTimer(0);
// form info
const {
control,
formState: { errors, isSubmitting, isValid },
handleSubmit,
} = useForm<TForgotPasswordFormValues>({
defaultValues: {
...defaultValues,
email: email?.toString() ?? "",
},
});
const handleForgotPassword = async (formData: TForgotPasswordFormValues) => {
await authService
.sendResetPasswordLink({
email: formData.email,
})
.then(() => {
captureSuccess({
eventName: AUTH_TRACKER_EVENTS.forgot_password,
payload: {
email: formData.email,
},
});
setToast({
type: TOAST_TYPE.SUCCESS,
title: t("auth.forgot_password.toast.success.title"),
message: t("auth.forgot_password.toast.success.message"),
});
setResendCodeTimer(30);
})
.catch((err) => {
captureError({
eventName: AUTH_TRACKER_EVENTS.forgot_password,
payload: {
email: formData.email,
},
});
setToast({
type: TOAST_TYPE.ERROR,
title: t("auth.forgot_password.toast.error.title"),
message: err?.error ?? t("auth.forgot_password.toast.error.message"),
});
});
};
// derived values
const enableSignUpConfig = config?.enable_signup ?? false;
const logo = resolvedTheme === "light" ? BlackHorizontalLogo : WhiteHorizontalLogo;
return (
<AuthenticationWrapper pageType={EPageTypes.NON_AUTHENTICATED}> <AuthenticationWrapper pageType={EPageTypes.NON_AUTHENTICATED}>
<div className="relative w-screen h-screen overflow-hidden"> <div className="relative z-10 flex flex-col items-center w-screen h-screen overflow-hidden overflow-y-auto pt-6 pb-10 px-8">
<div className="absolute inset-0 z-0"> <AuthHeader type={EAuthModes.SIGN_IN} />
<Image <ForgotPasswordForm />
src={resolvedTheme === "dark" ? PlaneBackgroundPatternDark : PlaneBackgroundPattern}
className="object-cover w-full h-full"
alt="Plane background pattern"
/>
</div>
<div className="relative z-10 flex flex-col w-screen h-screen overflow-hidden overflow-y-auto">
<div className="container relative flex items-center justify-between flex-shrink-0 min-w-full px-10 pb-4 transition-all lg:px-20 xl:px-36">
<div className="flex items-center py-10 gap-x-2">
<Link href={`/`} className="h-[30px] w-[133px]">
<Image src={logo} alt="Plane logo" />
</Link>
</div>
{enableSignUpConfig && (
<div className="flex flex-col items-end text-sm font-medium text-center sm:items-center sm:gap-2 sm:flex-row text-onboarding-text-300">
{t("auth.common.new_to_plane")}
<Link
href="/"
data-ph-element={AUTH_TRACKER_ELEMENTS.SIGNUP_FROM_FORGOT_PASSWORD}
className="font-semibold text-custom-primary-100 hover:underline"
>
{t("auth.common.create_account")}
</Link>
</div>
)}
</div>
<div className="container flex-grow max-w-lg px-10 py-10 mx-auto transition-all lg:max-w-md lg:px-5 lg:pt-28">
<div className="relative flex flex-col space-y-6">
<div className="py-4 space-y-1 text-center">
<h3 className="flex justify-center gap-4 text-3xl font-bold text-onboarding-text-100">
{t("auth.forgot_password.title")}
</h3>
<p className="font-medium text-onboarding-text-400">{t("auth.forgot_password.description")}</p>
</div>
<form onSubmit={handleSubmit(handleForgotPassword)} className="mt-5 space-y-4">
<div className="space-y-1">
<label className="text-sm font-medium text-onboarding-text-300" htmlFor="email">
{t("auth.common.email.label")}
</label>
<Controller
control={control}
name="email"
rules={{
required: t("auth.common.email.errors.required"),
validate: (value) => checkEmailValidity(value) || t("auth.common.email.errors.invalid"),
}}
render={({ field: { value, onChange, ref } }) => (
<Input
id="email"
name="email"
type="email"
value={value}
onChange={onChange}
ref={ref}
hasError={Boolean(errors.email)}
placeholder={t("auth.common.email.placeholder")}
className="h-[46px] w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 placeholder:text-onboarding-text-400"
autoComplete="on"
disabled={resendTimerCode > 0}
/>
)}
/>
{resendTimerCode > 0 && (
<p className="flex items-start w-full gap-1 px-1 text-xs font-medium text-green-700">
<CircleCheck height={12} width={12} className="mt-0.5" />
{t("auth.forgot_password.email_sent")}
</p>
)}
</div>
<Button
type="submit"
variant="primary"
className="w-full"
size="lg"
disabled={!isValid}
loading={isSubmitting || resendTimerCode > 0}
>
{resendTimerCode > 0
? t("auth.common.resend_in", { seconds: resendTimerCode })
: t("auth.forgot_password.send_reset_link")}
</Button>
<Link href="/" className={cn("w-full", getButtonStyling("link-neutral", "lg"))}>
{t("auth.common.back_to_sign_in")}
</Link>
</form>
</div>
</div>
</div>
</div> </div>
</AuthenticationWrapper> </AuthenticationWrapper>
); </DefaultLayout>
}); ));
export default ForgotPasswordPage; export default ForgotPasswordPage;

View File

@@ -1,242 +1,25 @@
"use client"; "use client";
import { useEffect, useMemo, useState } from "react"; // plane imports
import { observer } from "mobx-react"; import { EAuthModes } from "@plane/constants";
import Image from "next/image";
import Link from "next/link";
import { useSearchParams } from "next/navigation";
// icons
import { useTheme } from "next-themes";
import { Eye, EyeOff } from "lucide-react";
// ui
import { API_BASE_URL, E_PASSWORD_STRENGTH } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { Button, Input, PasswordStrengthIndicator } from "@plane/ui";
// components // components
import { getPasswordStrength } from "@plane/utils"; import { ResetPasswordForm } from "@/components/account";
import { AuthBanner } from "@/components/account"; import { AuthHeader } from "@/components/auth-screens/header";
// helpers // helpers
import { import { EPageTypes } from "@/helpers/authentication.helper";
EAuthenticationErrorCodes, // layouts
EErrorAlertType, import DefaultLayout from "@/layouts/default-layout";
EPageTypes,
TAuthErrorInfo,
authErrorHandler,
} from "@/helpers/authentication.helper";
// wrappers
import { AuthenticationWrapper } from "@/lib/wrappers"; import { AuthenticationWrapper } from "@/lib/wrappers";
// services
// images
import PlaneBackgroundPatternDark from "@/public/auth/background-pattern-dark.svg";
import PlaneBackgroundPattern from "@/public/auth/background-pattern.svg";
import BlackHorizontalLogo from "@/public/plane-logos/black-horizontal-with-blue-logo.png";
import WhiteHorizontalLogo from "@/public/plane-logos/white-horizontal-with-blue-logo.png";
import { AuthService } from "@/services/auth.service";
type TResetPasswordFormValues = { const ResetPasswordPage = () => (
email: string; <DefaultLayout>
password: string;
confirm_password?: string;
};
const defaultValues: TResetPasswordFormValues = {
email: "",
password: "",
};
// services
const authService = new AuthService();
const ResetPasswordPage = observer(() => {
// search params
const searchParams = useSearchParams();
const uidb64 = searchParams.get("uidb64");
const token = searchParams.get("token");
const email = searchParams.get("email");
const error_code = searchParams.get("error_code");
// states
const [showPassword, setShowPassword] = useState({
password: false,
retypePassword: false,
});
const [resetFormData, setResetFormData] = useState<TResetPasswordFormValues>({
...defaultValues,
email: email ? email.toString() : "",
});
const [csrfToken, setCsrfToken] = useState<string | undefined>(undefined);
const [isPasswordInputFocused, setIsPasswordInputFocused] = useState(false);
const [isRetryPasswordInputFocused, setIsRetryPasswordInputFocused] = useState(false);
const [errorInfo, setErrorInfo] = useState<TAuthErrorInfo | undefined>(undefined);
// plane hooks
const { t } = useTranslation();
// hooks
const { resolvedTheme } = useTheme();
const handleShowPassword = (key: keyof typeof showPassword) =>
setShowPassword((prev) => ({ ...prev, [key]: !prev[key] }));
const handleFormChange = (key: keyof TResetPasswordFormValues, value: string) =>
setResetFormData((prev) => ({ ...prev, [key]: value }));
useEffect(() => {
if (csrfToken === undefined)
authService.requestCSRFToken().then((data) => data?.csrf_token && setCsrfToken(data.csrf_token));
}, [csrfToken]);
const isButtonDisabled = useMemo(
() =>
!!resetFormData.password &&
getPasswordStrength(resetFormData.password) === E_PASSWORD_STRENGTH.STRENGTH_VALID &&
resetFormData.password === resetFormData.confirm_password
? false
: true,
[resetFormData]
);
useEffect(() => {
if (error_code) {
const errorhandler = authErrorHandler(error_code?.toString() as EAuthenticationErrorCodes);
if (errorhandler) {
setErrorInfo(errorhandler);
}
}
}, [error_code]);
const password = resetFormData?.password ?? "";
const confirmPassword = resetFormData?.confirm_password ?? "";
const renderPasswordMatchError = !isRetryPasswordInputFocused || confirmPassword.length >= password.length;
const logo = resolvedTheme === "light" ? BlackHorizontalLogo : WhiteHorizontalLogo;
return (
<AuthenticationWrapper pageType={EPageTypes.NON_AUTHENTICATED}> <AuthenticationWrapper pageType={EPageTypes.NON_AUTHENTICATED}>
<div className="relative w-screen h-screen overflow-hidden"> <div className="relative z-10 flex flex-col items-center w-screen h-screen overflow-hidden overflow-y-auto pt-6 pb-10 px-8">
<div className="absolute inset-0 z-0"> <AuthHeader type={EAuthModes.SIGN_IN} />
<Image <ResetPasswordForm />
src={resolvedTheme === "dark" ? PlaneBackgroundPatternDark : PlaneBackgroundPattern}
className="w-full h-full object-cover"
alt="Plane background pattern"
/>
</div>
<div className="relative z-10 w-screen h-screen overflow-hidden overflow-y-auto flex flex-col">
<div className="container min-w-full px-10 lg:px-20 xl:px-36 flex-shrink-0 relative flex items-center justify-between pb-4 transition-all">
<div className="flex items-center gap-x-2 py-10">
<Link href={`/`} className="h-[30px] w-[133px]">
<Image src={logo} alt="Plane logo" />
</Link>
</div>
</div>
<div className="flex-grow container mx-auto max-w-lg px-10 lg:max-w-md lg:px-5 py-10 lg:pt-28 transition-all">
<div className="relative flex flex-col space-y-6">
<div className="text-center space-y-1 py-4">
<h3 className="flex gap-4 justify-center text-3xl font-bold text-onboarding-text-100">
{t("auth.reset_password.title")}
</h3>
<p className="font-medium text-onboarding-text-400">{t("auth.reset_password.description")}</p>
</div>
{errorInfo && errorInfo?.type === EErrorAlertType.BANNER_ALERT && (
<AuthBanner bannerData={errorInfo} handleBannerData={(value) => setErrorInfo(value)} />
)}
<form
className="mt-5 space-y-4"
method="POST"
action={`${API_BASE_URL}/auth/reset-password/${uidb64?.toString()}/${token?.toString()}/`}
>
<input type="hidden" name="csrfmiddlewaretoken" value={csrfToken} />
<div className="space-y-1">
<label className="text-sm text-onboarding-text-300 font-medium" htmlFor="email">
{t("auth.common.email.label")}
</label>
<div className="relative flex items-center rounded-md bg-onboarding-background-200">
<Input
id="email"
name="email"
type="email"
value={resetFormData.email}
//hasError={Boolean(errors.email)}
placeholder={t("auth.common.email.placeholder")}
className="h-[46px] w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 text-onboarding-text-400 cursor-not-allowed"
autoComplete="on"
disabled
/>
</div>
</div>
<div className="space-y-1">
<label className="text-sm text-onboarding-text-300 font-medium" htmlFor="password">
{t("auth.common.password.label")}
</label>
<div className="relative flex items-center rounded-md bg-onboarding-background-200">
<Input
type={showPassword.password ? "text" : "password"}
name="password"
value={resetFormData.password}
onChange={(e) => handleFormChange("password", e.target.value)}
//hasError={Boolean(errors.password)}
placeholder={t("auth.common.password.placeholder")}
className="h-[46px] w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 placeholder:text-onboarding-text-400"
minLength={8}
onFocus={() => setIsPasswordInputFocused(true)}
onBlur={() => setIsPasswordInputFocused(false)}
autoComplete="on"
autoFocus
/>
{showPassword.password ? (
<EyeOff
className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer"
onClick={() => handleShowPassword("password")}
/>
) : (
<Eye
className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer"
onClick={() => handleShowPassword("password")}
/>
)}
</div>
<PasswordStrengthIndicator password={resetFormData.password} isFocused={isPasswordInputFocused} />
</div>
<div className="space-y-1">
<label className="text-sm text-onboarding-text-300 font-medium" htmlFor="confirm_password">
{t("auth.common.password.confirm_password.label")}
</label>
<div className="relative flex items-center rounded-md bg-onboarding-background-200">
<Input
type={showPassword.retypePassword ? "text" : "password"}
name="confirm_password"
value={resetFormData.confirm_password}
onChange={(e) => handleFormChange("confirm_password", e.target.value)}
placeholder={t("auth.common.password.confirm_password.placeholder")}
className="h-[46px] w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 placeholder:text-onboarding-text-400"
onFocus={() => setIsRetryPasswordInputFocused(true)}
onBlur={() => setIsRetryPasswordInputFocused(false)}
/>
{showPassword.retypePassword ? (
<EyeOff
className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer"
onClick={() => handleShowPassword("retypePassword")}
/>
) : (
<Eye
className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer"
onClick={() => handleShowPassword("retypePassword")}
/>
)}
</div>
{!!resetFormData.confirm_password &&
resetFormData.password !== resetFormData.confirm_password &&
renderPasswordMatchError && (
<span className="text-sm text-red-500">{t("auth.common.password.errors.match")}</span>
)}
</div>
<Button type="submit" variant="primary" className="w-full" size="lg" disabled={isButtonDisabled}>
{t("auth.common.password.submit")}
</Button>
</form>
</div>
</div>
</div>
</div> </div>
</AuthenticationWrapper> </AuthenticationWrapper>
); </DefaultLayout>
}); );
export default ResetPasswordPage; export default ResetPasswordPage;

View File

@@ -1,253 +1,25 @@
"use client"; "use client";
import { FormEvent, useEffect, useMemo, useState } from "react";
import { observer } from "mobx-react";
import Image from "next/image";
import Link from "next/link";
import { useSearchParams } from "next/navigation";
// icons
import { useTheme } from "next-themes";
import { Eye, EyeOff } from "lucide-react";
// plane imports // plane imports
import { AUTH_TRACKER_ELEMENTS, AUTH_TRACKER_EVENTS, E_PASSWORD_STRENGTH } from "@plane/constants"; import { EAuthModes } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { Button, Input, PasswordStrengthIndicator, TOAST_TYPE, setToast } from "@plane/ui";
// components // components
import { getPasswordStrength } from "@plane/utils"; import { ResetPasswordForm } from "@/components/account";
import { AuthHeader } from "@/components/auth-screens/header";
// helpers // helpers
import { EPageTypes } from "@/helpers/authentication.helper"; import { EPageTypes } from "@/helpers/authentication.helper";
// hooks // layouts
import { captureError, captureSuccess, captureView } from "@/helpers/event-tracker.helper"; import DefaultLayout from "@/layouts/default-layout";
import { useUser } from "@/hooks/store";
import { useAppRouter } from "@/hooks/use-app-router";
// wrappers
import { AuthenticationWrapper } from "@/lib/wrappers"; import { AuthenticationWrapper } from "@/lib/wrappers";
// services
// images
import PlaneBackgroundPatternDark from "@/public/auth/background-pattern-dark.svg";
import PlaneBackgroundPattern from "@/public/auth/background-pattern.svg";
import BlackHorizontalLogo from "@/public/plane-logos/black-horizontal-with-blue-logo.png";
import WhiteHorizontalLogo from "@/public/plane-logos/white-horizontal-with-blue-logo.png";
import { AuthService } from "@/services/auth.service";
type TResetPasswordFormValues = { const SetPasswordPage = () => (
email: string; <DefaultLayout>
password: string;
confirm_password?: string;
};
const defaultValues: TResetPasswordFormValues = {
email: "",
password: "",
};
// services
const authService = new AuthService();
const SetPasswordPage = observer(() => {
// router
const router = useAppRouter();
// search params
const searchParams = useSearchParams();
const email = searchParams.get("email");
// states
const [showPassword, setShowPassword] = useState({
password: false,
retypePassword: false,
});
const [passwordFormData, setPasswordFormData] = useState<TResetPasswordFormValues>({
...defaultValues,
email: email ? email.toString() : "",
});
const [csrfToken, setCsrfToken] = useState<string | undefined>(undefined);
const [isPasswordInputFocused, setIsPasswordInputFocused] = useState(false);
const [isRetryPasswordInputFocused, setIsRetryPasswordInputFocused] = useState(false);
// plane hooks
const { t } = useTranslation();
// hooks
const { resolvedTheme } = useTheme();
const { data: user, handleSetPassword } = useUser();
useEffect(() => {
captureView({
elementName: AUTH_TRACKER_ELEMENTS.SET_PASSWORD_FORM,
});
}, []);
useEffect(() => {
if (csrfToken === undefined)
authService.requestCSRFToken().then((data) => data?.csrf_token && setCsrfToken(data.csrf_token));
}, [csrfToken]);
const handleShowPassword = (key: keyof typeof showPassword) =>
setShowPassword((prev) => ({ ...prev, [key]: !prev[key] }));
const handleFormChange = (key: keyof TResetPasswordFormValues, value: string) =>
setPasswordFormData((prev) => ({ ...prev, [key]: value }));
const isButtonDisabled = useMemo(
() =>
!!passwordFormData.password &&
getPasswordStrength(passwordFormData.password) === E_PASSWORD_STRENGTH.STRENGTH_VALID &&
passwordFormData.password === passwordFormData.confirm_password
? false
: true,
[passwordFormData]
);
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
try {
e.preventDefault();
if (!csrfToken) throw new Error("csrf token not found");
await handleSetPassword(csrfToken, { password: passwordFormData.password });
captureSuccess({
eventName: AUTH_TRACKER_EVENTS.password_created,
});
router.push("/");
} catch (error: unknown) {
let message = undefined;
if (error instanceof Error) {
const err = error as Error & { error?: string };
message = err.error;
}
captureError({
eventName: AUTH_TRACKER_EVENTS.password_created,
});
setToast({
type: TOAST_TYPE.ERROR,
title: t("common.errors.default.title"),
message: message ?? t("common.errors.default.message"),
});
}
};
const password = passwordFormData?.password ?? "";
const confirmPassword = passwordFormData?.confirm_password ?? "";
const renderPasswordMatchError = !isRetryPasswordInputFocused || confirmPassword.length >= password.length;
const logo = resolvedTheme === "light" ? BlackHorizontalLogo : WhiteHorizontalLogo;
return (
<AuthenticationWrapper pageType={EPageTypes.SET_PASSWORD}> <AuthenticationWrapper pageType={EPageTypes.SET_PASSWORD}>
<div className="relative w-screen h-screen overflow-hidden"> <div className="relative z-10 flex flex-col items-center w-screen h-screen overflow-hidden overflow-y-auto pt-6 pb-10 px-8">
<div className="absolute inset-0 z-0"> <AuthHeader type={EAuthModes.SIGN_IN} />
<Image <ResetPasswordForm />
src={resolvedTheme === "dark" ? PlaneBackgroundPatternDark : PlaneBackgroundPattern}
className="w-full h-full object-cover"
alt="Plane background pattern"
/>
</div>
<div className="relative z-10 w-screen h-screen overflow-hidden overflow-y-auto flex flex-col">
<div className="container min-w-full px-10 lg:px-20 xl:px-36 flex-shrink-0 relative flex items-center justify-between pb-4 transition-all">
<div className="flex items-center gap-x-2 py-10">
<Link href={`/`} className="h-[30px] w-[133px]">
<Image src={logo} alt="Plane logo" />
</Link>
</div>
</div>
<div className="flex-grow container mx-auto max-w-lg px-10 lg:max-w-md lg:px-5 py-10 lg:pt-28 transition-all">
<div className="relative flex flex-col space-y-6">
<div className="text-center space-y-1 py-4">
<h3 className="flex gap-4 justify-center text-3xl font-bold text-onboarding-text-100">
{t("auth.set_password.title")}
</h3>
<p className="font-medium text-onboarding-text-400">{t("auth.set_password.description")}</p>
</div>
<form className="mt-5 space-y-4" onSubmit={(e) => handleSubmit(e)}>
<div className="space-y-1">
<label className="text-sm text-onboarding-text-300 font-medium" htmlFor="email">
{t("auth.common.email.label")}
</label>
<div className="relative flex items-center rounded-md bg-onboarding-background-200">
<Input
id="email"
name="email"
type="email"
value={user?.email}
//hasError={Boolean(errors.email)}
placeholder={t("auth.common.email.placeholder")}
className="h-[46px] w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 text-onboarding-text-400 cursor-not-allowed"
autoComplete="on"
disabled
/>
</div>
</div>
<div className="space-y-1">
<label className="text-sm text-onboarding-text-300 font-medium" htmlFor="password">
{t("auth.common.password.label")}
</label>
<div className="relative flex items-center rounded-md bg-onboarding-background-200">
<Input
type={showPassword.password ? "text" : "password"}
name="password"
value={passwordFormData.password}
onChange={(e) => handleFormChange("password", e.target.value)}
//hasError={Boolean(errors.password)}
placeholder={t("auth.common.password.placeholder")}
className="h-[46px] w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 placeholder:text-onboarding-text-400"
minLength={8}
onFocus={() => setIsPasswordInputFocused(true)}
onBlur={() => setIsPasswordInputFocused(false)}
autoComplete="on"
autoFocus
/>
{showPassword.password ? (
<EyeOff
className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer"
onClick={() => handleShowPassword("password")}
/>
) : (
<Eye
className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer"
onClick={() => handleShowPassword("password")}
/>
)}
</div>
<PasswordStrengthIndicator password={passwordFormData.password} isFocused={isPasswordInputFocused} />
</div>
<div className="space-y-1">
<label className="text-sm text-onboarding-text-300 font-medium" htmlFor="confirm_password">
{t("auth.common.password.confirm_password.label")}
</label>
<div className="relative flex items-center rounded-md bg-onboarding-background-200">
<Input
type={showPassword.retypePassword ? "text" : "password"}
name="confirm_password"
value={passwordFormData.confirm_password}
onChange={(e) => handleFormChange("confirm_password", e.target.value)}
placeholder={t("auth.common.password.confirm_password.placeholder")}
className="h-[46px] w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 placeholder:text-onboarding-text-400"
onFocus={() => setIsRetryPasswordInputFocused(true)}
onBlur={() => setIsRetryPasswordInputFocused(false)}
/>
{showPassword.retypePassword ? (
<EyeOff
className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer"
onClick={() => handleShowPassword("retypePassword")}
/>
) : (
<Eye
className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer"
onClick={() => handleShowPassword("retypePassword")}
/>
)}
</div>
{!!passwordFormData.confirm_password &&
passwordFormData.password !== passwordFormData.confirm_password &&
renderPasswordMatchError && (
<span className="text-sm text-red-500">{t("auth.common.password.errors.match")}</span>
)}
</div>
<Button type="submit" variant="primary" className="w-full" size="lg" disabled={isButtonDisabled}>
{t("common.continue")}
</Button>
</form>
</div>
</div>
</div>
</div> </div>
</AuthenticationWrapper> </AuthenticationWrapper>
); </DefaultLayout>
}); );
export default SetPasswordPage; export default SetPasswordPage;

View File

@@ -4,11 +4,10 @@ import { useState } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import Image from "next/image"; import Image from "next/image";
import Link from "next/link"; import Link from "next/link";
import { useTheme } from "next-themes";
import { useTranslation } from "@plane/i18n"; import { useTranslation } from "@plane/i18n";
import { IWorkspace } from "@plane/types"; import { IWorkspace } from "@plane/types";
// components // components
import { Button, getButtonStyling } from "@plane/ui"; import { Button, getButtonStyling, PlaneLogo } from "@plane/ui";
import { CreateWorkspaceForm } from "@/components/workspace"; import { CreateWorkspaceForm } from "@/components/workspace";
// hooks // hooks
import { useUser, useUserProfile } from "@/hooks/store"; import { useUser, useUserProfile } from "@/hooks/store";
@@ -18,8 +17,6 @@ import { AuthenticationWrapper } from "@/lib/wrappers";
// plane web helpers // plane web helpers
import { getIsWorkspaceCreationDisabled } from "@/plane-web/helpers/instance.helper"; import { getIsWorkspaceCreationDisabled } from "@/plane-web/helpers/instance.helper";
// images // images
import BlackHorizontalLogo from "@/public/plane-logos/black-horizontal-with-blue-logo.png";
import WhiteHorizontalLogo from "@/public/plane-logos/white-horizontal-with-blue-logo.png";
import WorkspaceCreationDisabled from "@/public/workspace/workspace-creation-disabled.png"; import WorkspaceCreationDisabled from "@/public/workspace/workspace-creation-disabled.png";
const CreateWorkspacePage = observer(() => { const CreateWorkspacePage = observer(() => {
@@ -35,8 +32,6 @@ const CreateWorkspacePage = observer(() => {
slug: "", slug: "",
organization_size: "", organization_size: "",
}); });
// hooks
const { resolvedTheme } = useTheme();
// derived values // derived values
const isWorkspaceCreationDisabled = getIsWorkspaceCreationDisabled(); const isWorkspaceCreationDisabled = getIsWorkspaceCreationDisabled();
@@ -56,8 +51,6 @@ const CreateWorkspacePage = observer(() => {
await updateUserProfile({ last_workspace_id: workspace.id }).then(() => router.push(`/${workspace.slug}`)); await updateUserProfile({ last_workspace_id: workspace.id }).then(() => router.push(`/${workspace.slug}`));
}; };
const logo = resolvedTheme === "light" ? BlackHorizontalLogo : WhiteHorizontalLogo;
return ( return (
<AuthenticationWrapper> <AuthenticationWrapper>
<div className="flex h-full flex-col gap-y-2 overflow-hidden sm:flex-row sm:gap-y-0"> <div className="flex h-full flex-col gap-y-2 overflow-hidden sm:flex-row sm:gap-y-0">
@@ -67,9 +60,7 @@ const CreateWorkspacePage = observer(() => {
className="absolute left-5 top-1/2 grid -translate-y-1/2 place-items-center bg-custom-background-100 px-3 sm:left-1/2 sm:top-12 sm:-translate-x-[15px] sm:translate-y-0 sm:px-0 sm:py-5 md:left-1/3" className="absolute left-5 top-1/2 grid -translate-y-1/2 place-items-center bg-custom-background-100 px-3 sm:left-1/2 sm:top-12 sm:-translate-x-[15px] sm:translate-y-0 sm:px-0 sm:py-5 md:left-1/3"
href="/" href="/"
> >
<div className="h-[30px] w-[133px]"> <PlaneLogo className="h-9 w-auto text-custom-text-100" />
<Image src={logo} alt="Plane logo" />
</div>
</Link> </Link>
<div className="absolute right-4 top-1/4 -translate-y-1/2 text-sm text-custom-text-100 sm:fixed sm:right-16 sm:top-12 sm:translate-y-0 sm:py-5"> <div className="absolute right-4 top-1/4 -translate-y-1/2 text-sm text-custom-text-100 sm:fixed sm:right-16 sm:top-12 sm:translate-y-0 sm:py-5">
{currentUser?.email} {currentUser?.email}

View File

@@ -2,10 +2,8 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import Image from "next/image";
import Link from "next/link"; import Link from "next/link";
import { useTheme } from "next-themes";
import useSWR, { mutate } from "swr"; import useSWR, { mutate } from "swr";
import { CheckCircle2 } from "lucide-react"; import { CheckCircle2 } from "lucide-react";
// plane imports // plane imports
@@ -14,7 +12,7 @@ import { useTranslation } from "@plane/i18n";
// types // types
import type { IWorkspaceMemberInvitation } from "@plane/types"; import type { IWorkspaceMemberInvitation } from "@plane/types";
// ui // ui
import { Button, TOAST_TYPE, setToast } from "@plane/ui"; import { Button, TOAST_TYPE, setToast, PlaneLogo } from "@plane/ui";
import { truncateText } from "@plane/utils"; import { truncateText } from "@plane/utils";
// components // components
import { EmptyState } from "@/components/common"; import { EmptyState } from "@/components/common";
@@ -31,8 +29,6 @@ import { AuthenticationWrapper } from "@/lib/wrappers";
import { WorkspaceService } from "@/plane-web/services"; import { WorkspaceService } from "@/plane-web/services";
// images // images
import emptyInvitation from "@/public/empty-state/invitation.svg"; import emptyInvitation from "@/public/empty-state/invitation.svg";
import BlackHorizontalLogo from "@/public/plane-logos/black-horizontal-with-blue-logo.png";
import WhiteHorizontalLogo from "@/public/plane-logos/white-horizontal-with-blue-logo.png";
const workspaceService = new WorkspaceService(); const workspaceService = new WorkspaceService();
@@ -48,8 +44,6 @@ const UserInvitationsPage = observer(() => {
const { updateUserProfile } = useUserProfile(); const { updateUserProfile } = useUserProfile();
const { fetchWorkspaces } = useWorkspace(); const { fetchWorkspaces } = useWorkspace();
// next-themes
const { resolvedTheme } = useTheme();
const { data: invitations } = useSWR("USER_WORKSPACE_INVITATIONS", () => workspaceService.userWorkspaceInvitations()); const { data: invitations } = useSWR("USER_WORKSPACE_INVITATIONS", () => workspaceService.userWorkspaceInvitations());
@@ -130,8 +124,6 @@ const UserInvitationsPage = observer(() => {
}); });
}; };
const logo = resolvedTheme === "light" ? BlackHorizontalLogo : WhiteHorizontalLogo;
return ( return (
<AuthenticationWrapper> <AuthenticationWrapper>
<div className="flex h-full flex-col gap-y-2 overflow-hidden sm:flex-row sm:gap-y-0"> <div className="flex h-full flex-col gap-y-2 overflow-hidden sm:flex-row sm:gap-y-0">
@@ -141,9 +133,7 @@ const UserInvitationsPage = observer(() => {
href="/" href="/"
className="absolute left-5 top-1/2 grid -translate-y-1/2 place-items-center bg-custom-background-100 px-3 sm:left-1/2 sm:top-12 sm:-translate-x-[15px] sm:translate-y-0 sm:px-0 sm:py-5 md:left-1/3 z-10" className="absolute left-5 top-1/2 grid -translate-y-1/2 place-items-center bg-custom-background-100 px-3 sm:left-1/2 sm:top-12 sm:-translate-x-[15px] sm:translate-y-0 sm:px-0 sm:py-5 md:left-1/3 z-10"
> >
<div className="h-[30px] w-[133px]"> <PlaneLogo className="h-9 w-auto text-custom-text-100" />
<Image src={logo} alt="Plane logo" />
</div>
</Link> </Link>
<div className="absolute right-4 top-1/4 -translate-y-1/2 text-sm text-custom-text-100 sm:fixed sm:right-16 sm:top-12 sm:translate-y-0 sm:py-5"> <div className="absolute right-4 top-1/4 -translate-y-1/2 text-sm text-custom-text-100 sm:fixed sm:right-16 sm:top-12 sm:translate-y-0 sm:py-5">
{currentUser?.email} {currentUser?.email}

View File

@@ -1,50 +1,32 @@
"use client"; "use client";
import { useEffect, useState } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import useSWR from "swr"; import useSWR from "swr";
// types
import { USER_TRACKER_EVENTS } from "@plane/constants";
import { TOnboardingSteps, TUserProfile } from "@plane/types";
// ui
import { TOAST_TYPE, setToast } from "@plane/ui";
// components // components
import { LogoSpinner } from "@/components/common"; import { LogoSpinner } from "@/components/common";
import { InviteMembers, CreateOrJoinWorkspaces, ProfileSetup } from "@/components/onboarding"; import { OnboardingRoot } from "@/components/onboarding";
// constants // constants
import { USER_WORKSPACES_LIST } from "@/constants/fetch-keys"; import { USER_WORKSPACES_LIST } from "@/constants/fetch-keys";
// helpers // helpers
import { EPageTypes } from "@/helpers/authentication.helper"; import { EPageTypes } from "@/helpers/authentication.helper";
// hooks // hooks
import { captureSuccess } from "@/helpers/event-tracker.helper"; import { useUser, useWorkspace } from "@/hooks/store";
import { useUser, useWorkspace, useUserProfile } from "@/hooks/store";
// wrappers // wrappers
import { AuthenticationWrapper } from "@/lib/wrappers"; import { AuthenticationWrapper } from "@/lib/wrappers";
import { WorkspaceService } from "@/plane-web/services"; import { WorkspaceContentWrapper } from "@/plane-web/components/workspace";
// services // services
import { WorkspaceService } from "@/plane-web/services";
enum EOnboardingSteps {
PROFILE_SETUP = "PROFILE_SETUP",
WORKSPACE_CREATE_OR_JOIN = "WORKSPACE_CREATE_OR_JOIN",
INVITE_MEMBERS = "INVITE_MEMBERS",
}
const workspaceService = new WorkspaceService(); const workspaceService = new WorkspaceService();
const OnboardingPage = observer(() => { const OnboardingPage = observer(() => {
// states
const [step, setStep] = useState<EOnboardingSteps | null>(null);
const [totalSteps, setTotalSteps] = useState<number | null>(null);
// store hooks // store hooks
const { isLoading: userLoader, data: user, updateCurrentUser } = useUser(); const { data: user } = useUser();
const { data: profile, updateUserProfile, finishUserOnboarding } = useUserProfile(); const { fetchWorkspaces } = useWorkspace();
const { workspaces, fetchWorkspaces } = useWorkspace();
// computed values
const workspacesList = Object.values(workspaces ?? {});
// fetching workspaces list // fetching workspaces list
const { isLoading: workspaceListLoader } = useSWR(USER_WORKSPACES_LIST, () => { useSWR(USER_WORKSPACES_LIST, () => {
if (user?.id) { if (user?.id) {
fetchWorkspaces(); fetchWorkspaces();
} }
@@ -57,133 +39,22 @@ const OnboardingPage = observer(() => {
if (user?.id) return workspaceService.userWorkspaceInvitations(); if (user?.id) return workspaceService.userWorkspaceInvitations();
} }
); );
// handle step change
const stepChange = async (steps: Partial<TOnboardingSteps>) => {
if (!user) return;
const payload: Partial<TUserProfile> = {
onboarding_step: {
...profile.onboarding_step,
...steps,
},
};
await updateUserProfile(payload);
};
// complete onboarding
const finishOnboarding = async () => {
if (!user) return;
await finishUserOnboarding()
.then(() => {
captureSuccess({
eventName: USER_TRACKER_EVENTS.onboarding_complete,
payload: {
email: user.email,
user_id: user.id,
},
});
})
.catch(() => {
setToast({
type: TOAST_TYPE.ERROR,
title: "Failed",
message: "Failed to finish onboarding, Please try again later.",
});
});
};
useEffect(() => {
// Never update the total steps if it's already set.
if (!totalSteps && userLoader === false && workspaceListLoader === false) {
// If user is already invited to a workspace, only show profile setup steps.
if (workspacesList && workspacesList?.length > 0) {
// If password is auto set then show two different steps for profile setup, else merge them.
if (user?.is_password_autoset) setTotalSteps(2);
else setTotalSteps(1);
} else {
// If password is auto set then total steps will increase to 4 due to extra step at profile setup stage.
if (user?.is_password_autoset) setTotalSteps(4);
else setTotalSteps(3);
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [userLoader, workspaceListLoader]);
// If the user completes the profile setup and has workspaces (through invitations), then finish the onboarding.
useEffect(() => {
if (userLoader === false && profile && workspaceListLoader === false) {
const onboardingStep = profile.onboarding_step;
if (onboardingStep.profile_complete && !onboardingStep.workspace_create && workspacesList.length > 0)
finishOnboarding();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [userLoader, profile, workspaceListLoader]);
useEffect(() => {
const handleStepChange = async () => {
if (!user) return;
const onboardingStep = profile.onboarding_step;
if (!onboardingStep.profile_complete) setStep(EOnboardingSteps.PROFILE_SETUP);
if (
onboardingStep.profile_complete &&
!(onboardingStep.workspace_join || onboardingStep.workspace_create || workspacesList?.length > 0)
) {
setStep(EOnboardingSteps.WORKSPACE_CREATE_OR_JOIN);
}
if (
onboardingStep.profile_complete &&
(onboardingStep.workspace_join || onboardingStep.workspace_create) &&
!onboardingStep.workspace_invite
)
setStep(EOnboardingSteps.INVITE_MEMBERS);
};
handleStepChange();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [user, step, profile.onboarding_step, updateCurrentUser, workspacesList]);
return ( return (
<AuthenticationWrapper pageType={EPageTypes.ONBOARDING}> <AuthenticationWrapper pageType={EPageTypes.ONBOARDING}>
{user && totalSteps && step !== null && !invitationsLoader ? ( <div className="flex relative size-full overflow-hidden bg-custom-background-90 rounded-lg transition-all ease-in-out duration-300">
<div className={`flex h-full w-full flex-col`}> <div className="size-full p-2 flex-grow transition-all ease-in-out duration-300 overflow-hidden">
{step === EOnboardingSteps.PROFILE_SETUP ? ( <div className="relative flex flex-col h-full w-full overflow-hidden rounded-lg bg-custom-background-100 shadow-md border border-custom-border-200">
<ProfileSetup {user && !invitationsLoader ? (
user={user} <OnboardingRoot invitations={invitations ?? []} />
totalSteps={totalSteps} ) : (
stepChange={stepChange} <div className="grid h-full w-full place-items-center">
finishOnboarding={finishOnboarding} <LogoSpinner />
/> </div>
) : step === EOnboardingSteps.WORKSPACE_CREATE_OR_JOIN ? ( )}
<CreateOrJoinWorkspaces </div>
invitations={invitations ?? []}
totalSteps={totalSteps}
stepChange={stepChange}
finishOnboarding={finishOnboarding}
/>
) : step === EOnboardingSteps.INVITE_MEMBERS ? (
<InviteMembers
finishOnboarding={finishOnboarding}
totalSteps={totalSteps}
user={user}
workspace={workspacesList?.[0]}
/>
) : (
<div className="flex h-full w-full items-center justify-center">
Something Went wrong. Please try again.
</div>
)}
</div> </div>
) : ( </div>
<div className="grid h-screen w-full place-items-center">
<LogoSpinner />
</div>
)}
</AuthenticationWrapper> </AuthenticationWrapper>
); );
}); });

View File

@@ -1,69 +1,19 @@
"use client"; "use client";
import { observer } from "mobx-react";
import Image from "next/image";
import Link from "next/link";
// ui
import { useTheme } from "next-themes";
// components // components
import { AUTH_TRACKER_ELEMENTS } from "@plane/constants"; import { AuthBase } from "@/components/auth-screens";
import { useTranslation } from "@plane/i18n";
import { AuthRoot } from "@/components/account";
// constants
// helpers // helpers
import { EAuthModes, EPageTypes } from "@/helpers/authentication.helper"; import { EAuthModes, EPageTypes } from "@/helpers/authentication.helper";
// assets // assets
import DefaultLayout from "@/layouts/default-layout";
import { AuthenticationWrapper } from "@/lib/wrappers"; import { AuthenticationWrapper } from "@/lib/wrappers";
import PlaneBackgroundPatternDark from "@/public/auth/background-pattern-dark.svg";
import PlaneBackgroundPattern from "@/public/auth/background-pattern.svg";
import BlackHorizontalLogo from "@/public/plane-logos/black-horizontal-with-blue-logo.png";
import WhiteHorizontalLogo from "@/public/plane-logos/white-horizontal-with-blue-logo.png";
export type AuthType = "sign-in" | "sign-up"; const SignUpPage = () => (
<DefaultLayout>
const SignInPage = observer(() => {
// plane hooks
const { t } = useTranslation();
// hooks
const { resolvedTheme } = useTheme();
const logo = resolvedTheme === "light" ? BlackHorizontalLogo : WhiteHorizontalLogo;
return (
<AuthenticationWrapper pageType={EPageTypes.NON_AUTHENTICATED}> <AuthenticationWrapper pageType={EPageTypes.NON_AUTHENTICATED}>
<div className="relative w-screen h-screen overflow-hidden"> <AuthBase authType={EAuthModes.SIGN_UP} />
<div className="absolute inset-0 z-0">
<Image
src={resolvedTheme === "dark" ? PlaneBackgroundPatternDark : PlaneBackgroundPattern}
className="w-full h-full object-cover"
alt="Plane background pattern"
/>
</div>
<div className="relative z-10 w-screen h-screen overflow-hidden overflow-y-auto flex flex-col">
<div className="container min-w-full px-10 lg:px-20 xl:px-36 flex-shrink-0 relative flex items-center justify-between pb-4 transition-all">
<div className="flex items-center gap-x-2 py-10">
<Link href={`/`} className="h-[30px] w-[133px]">
<Image src={logo} alt="Plane logo" />
</Link>
</div>
<div className="flex flex-col items-end sm:items-center sm:gap-2 sm:flex-row text-center text-sm font-medium text-onboarding-text-300">
{t("auth.common.already_have_an_account")}
<Link
href="/"
data-ph-element={AUTH_TRACKER_ELEMENTS.SIGN_IN_FROM_SIGNUP}
className="font-semibold text-custom-primary-100 hover:underline"
>
{t("auth.common.login")}
</Link>
</div>
</div>
<div className="flex flex-col justify-center flex-grow container h-[100vh-60px] mx-auto max-w-lg px-10 lg:max-w-md lg:px-5 transition-all">
<AuthRoot authMode={EAuthModes.SIGN_UP} />
</div>
</div>
</div>
</AuthenticationWrapper> </AuthenticationWrapper>
); </DefaultLayout>
}); );
export default SignInPage; export default SignUpPage;

View File

@@ -1,83 +1,23 @@
"use client"; "use client";
import React from "react"; import React from "react";
import { observer } from "mobx-react";
import Image from "next/image";
import Link from "next/link";
// ui
import { useTheme } from "next-themes";
// components // components
import { AUTH_TRACKER_ELEMENTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { AuthRoot } from "@/components/account";
import { PageHead } from "@/components/core";
// constants // constants
// helpers // helpers
import { AuthBase } from "@/components/auth-screens";
import { EAuthModes, EPageTypes } from "@/helpers/authentication.helper"; import { EAuthModes, EPageTypes } from "@/helpers/authentication.helper";
// hooks // hooks
import { useInstance } from "@/hooks/store";
// layouts // layouts
import DefaultLayout from "@/layouts/default-layout"; import DefaultLayout from "@/layouts/default-layout";
// wrappers // wrappers
import { AuthenticationWrapper } from "@/lib/wrappers"; import { AuthenticationWrapper } from "@/lib/wrappers";
// assets // assets
import PlaneBackgroundPatternDark from "@/public/auth/background-pattern-dark.svg";
import PlaneBackgroundPattern from "@/public/auth/background-pattern.svg";
import BlackHorizontalLogo from "@/public/plane-logos/black-horizontal-with-blue-logo.png";
import WhiteHorizontalLogo from "@/public/plane-logos/white-horizontal-with-blue-logo.png";
const HomePage = observer(() => { const HomePage = () => (
const { resolvedTheme } = useTheme(); <DefaultLayout>
// plane hooks <AuthenticationWrapper pageType={EPageTypes.NON_AUTHENTICATED}>
const { t } = useTranslation(); <AuthBase authType={EAuthModes.SIGN_IN} />
// store </AuthenticationWrapper>
const { config } = useInstance(); </DefaultLayout>
// derived values );
const enableSignUpConfig = config?.enable_signup ?? false;
const logo = resolvedTheme === "light" ? BlackHorizontalLogo : WhiteHorizontalLogo;
return (
<DefaultLayout>
<AuthenticationWrapper pageType={EPageTypes.NON_AUTHENTICATED}>
<>
<div className="relative w-screen h-screen overflow-hidden">
<PageHead title={t("auth.common.login") + " - Plane"} />
<div className="absolute inset-0 z-0">
<Image
src={resolvedTheme === "dark" ? PlaneBackgroundPatternDark : PlaneBackgroundPattern}
className="object-cover w-full h-full"
alt="Plane background pattern"
/>
</div>
<div className="relative z-10 flex flex-col w-screen h-screen overflow-hidden overflow-y-auto">
<div className="container relative flex items-center justify-between flex-shrink-0 min-w-full px-10 pb-4 transition-all lg:px-20 xl:px-36">
<div className="flex items-center py-10 gap-x-2">
<Link href={`/`} className="h-[30px] w-[133px]">
<Image src={logo} alt="Plane logo" />
</Link>
</div>
{enableSignUpConfig && (
<div className="flex flex-col items-end text-sm font-medium text-center sm:items-center sm:gap-2 sm:flex-row text-onboarding-text-300">
{t("auth.common.new_to_plane")}
<Link
href="/sign-up"
data-ph-element={AUTH_TRACKER_ELEMENTS.NAVIGATE_TO_SIGN_UP}
className="font-semibold text-custom-primary-100 hover:underline"
>
{t("auth.common.create_account")}
</Link>
</div>
)}
</div>
<div className="flex flex-col justify-center flex-grow container h-[100vh-60px] mx-auto max-w-lg px-10 lg:max-w-md lg:px-5 transition-all">
<AuthRoot authMode={EAuthModes.SIGN_IN} />
</div>
</div>
</div>
</>
</AuthenticationWrapper>
</DefaultLayout>
);
});
export default HomePage; export default HomePage;

View File

@@ -37,7 +37,7 @@ export const AppProvider: FC<IAppProvider> = (props) => {
// themes // themes
return ( return (
<> <>
<AppProgressBar height="4px" color="#3F76FF" options={{ showSpinner: false }} shallowRouting /> <AppProgressBar height="4px" options={{ showSpinner: false }} shallowRouting />
<StoreProvider> <StoreProvider>
<ThemeProvider themes={["light", "dark", "light-contrast", "dark-contrast", "custom"]} defaultTheme="system"> <ThemeProvider themes={["light", "dark", "light-contrast", "dark-contrast", "custom"]} defaultTheme="system">
<ToastWithTheme /> <ToastWithTheme />

View File

@@ -1,10 +1,8 @@
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import Image from "next/image";
import { useTranslation } from "@plane/i18n"; import { useTranslation } from "@plane/i18n";
import { PlaneLogo } from "@plane/ui";
// helpers // helpers
import { cn } from "@plane/utils"; import { cn } from "@plane/utils";
// assets
import PlaneLogo from "@/public/plane-logos/blue-without-text.png";
// package.json // package.json
import packageJson from "package.json"; import packageJson from "package.json";
@@ -23,7 +21,7 @@ export const ProductUpdatesHeader = observer(() => {
</div> </div>
</div> </div>
<div className="flex flex-shrink-0 items-center gap-8"> <div className="flex flex-shrink-0 items-center gap-8">
<Image src={PlaneLogo} alt="Plane" width={24} height={24} /> <PlaneLogo className="h-6 w-auto text-custom-text-100" />
</div> </div>
</div> </div>
); );

View File

@@ -1,4 +1,4 @@
import { FC, ReactNode } from "react"; import { FC } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import useSWR from "swr"; import useSWR from "swr";
import { useTranslation } from "@plane/i18n"; import { useTranslation } from "@plane/i18n";
@@ -17,36 +17,35 @@ type TAuthHeader = {
invitationEmail: string | undefined; invitationEmail: string | undefined;
authMode: EAuthModes; authMode: EAuthModes;
currentAuthStep: EAuthSteps; currentAuthStep: EAuthSteps;
children: ReactNode;
}; };
const Titles = { const Titles = {
[EAuthModes.SIGN_IN]: { [EAuthModes.SIGN_IN]: {
[EAuthSteps.EMAIL]: { [EAuthSteps.EMAIL]: {
header: "auth.sign_in.header.step.email.header", header: "Work in all dimensions.",
subHeader: "", subHeader: "Welcome back to Plane.",
}, },
[EAuthSteps.PASSWORD]: { [EAuthSteps.PASSWORD]: {
header: "auth.sign_in.header.step.password.header", header: "Work in all dimensions.",
subHeader: "auth.sign_in.header.step.password.sub_header", subHeader: "Welcome back to Plane.",
}, },
[EAuthSteps.UNIQUE_CODE]: { [EAuthSteps.UNIQUE_CODE]: {
header: "auth.sign_in.header.step.unique_code.header", header: "Work in all dimensions.",
subHeader: "auth.sign_in.header.step.unique_code.sub_header", subHeader: "Welcome back to Plane.",
}, },
}, },
[EAuthModes.SIGN_UP]: { [EAuthModes.SIGN_UP]: {
[EAuthSteps.EMAIL]: { [EAuthSteps.EMAIL]: {
header: "auth.sign_up.header.step.email.header", header: "Work in all dimensions.",
subHeader: "", subHeader: "Create your Plane account.",
}, },
[EAuthSteps.PASSWORD]: { [EAuthSteps.PASSWORD]: {
header: "auth.sign_up.header.step.password.header", header: "Work in all dimensions.",
subHeader: "auth.sign_up.header.step.password.sub_header", subHeader: "Create your Plane account.",
}, },
[EAuthSteps.UNIQUE_CODE]: { [EAuthSteps.UNIQUE_CODE]: {
header: "auth.sign_up.header.step.unique_code.header", header: "Work in all dimensions.",
subHeader: "auth.sign_up.header.step.unique_code.sub_header", subHeader: "Create your Plane account.",
}, },
}, },
}; };
@@ -54,7 +53,7 @@ const Titles = {
const workSpaceService = new WorkspaceService(); const workSpaceService = new WorkspaceService();
export const AuthHeader: FC<TAuthHeader> = observer((props) => { export const AuthHeader: FC<TAuthHeader> = observer((props) => {
const { workspaceSlug, invitationId, invitationEmail, authMode, currentAuthStep, children } = props; const { workspaceSlug, invitationId, invitationEmail, authMode, currentAuthStep } = props;
// plane imports // plane imports
const { t } = useTranslation(); const { t } = useTranslation();
@@ -83,7 +82,10 @@ export const AuthHeader: FC<TAuthHeader> = observer((props) => {
{workspace.name} {workspace.name}
</div> </div>
), ),
subHeader: mode == EAuthModes.SIGN_UP ? "auth.sign_up.header.label" : "auth.sign_in.header.label", subHeader:
mode == EAuthModes.SIGN_UP
? "Create an account to start managing work with your team."
: "Log in to start managing work with your team.",
}; };
} }
@@ -100,14 +102,11 @@ export const AuthHeader: FC<TAuthHeader> = observer((props) => {
); );
return ( return (
<> <div className="flex flex-col gap-1">
<div className="space-y-1 text-center"> <span className="text-2xl font-semibold text-custom-text-100 leading-7">
<h1 className="text-3xl font-bold text-onboarding-text-100"> {typeof header === "string" ? t(header) : header}
{typeof header === "string" ? t(header) : header} </span>
</h1> <span className="text-2xl font-semibold text-custom-text-400 leading-7">{subHeader}</span>
<p className="font-medium text-onboarding-text-400">{t(subHeader)}</p> </div>
</div>
{children}
</>
); );
}); });

View File

@@ -1,18 +1,18 @@
import React, { FC, useEffect, useState } from "react"; import React, { FC, useEffect, useState } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import Image from "next/image";
import { useSearchParams } from "next/navigation"; import { useSearchParams } from "next/navigation";
import { useTranslation } from "@plane/i18n"; import { useTheme } from "next-themes";
import { IEmailCheckData } from "@plane/types"; // plane imports
import { API_BASE_URL } from "@plane/constants";
import { OAuthOptions } from "@plane/ui";
// assets
import GithubLightLogo from "/public/logos/github-black.png";
import GithubDarkLogo from "/public/logos/github-dark.svg";
import GitlabLogo from "/public/logos/gitlab-logo.svg";
import GoogleLogo from "/public/logos/google-logo.svg";
// components // components
import { import { AuthHeader, AuthBanner, TermsAndConditions } from "@/components/account";
AuthHeader,
AuthBanner,
AuthEmailForm,
AuthPasswordForm,
OAuthOptions,
TermsAndConditions,
AuthUniqueCodeForm,
} from "@/components/account";
// helpers // helpers
import { import {
EAuthModes, EAuthModes,
@@ -24,11 +24,8 @@ import {
} from "@/helpers/authentication.helper"; } from "@/helpers/authentication.helper";
// hooks // hooks
import { useInstance } from "@/hooks/store"; import { useInstance } from "@/hooks/store";
import { useAppRouter } from "@/hooks/use-app-router";
// services // services
import { AuthService } from "@/services/auth.service"; import { AuthFormRoot } from "./form-root";
const authService = new AuthService();
type TAuthRoot = { type TAuthRoot = {
authMode: EAuthModes; authMode: EAuthModes;
@@ -36,14 +33,14 @@ type TAuthRoot = {
export const AuthRoot: FC<TAuthRoot> = observer((props) => { export const AuthRoot: FC<TAuthRoot> = observer((props) => {
//router //router
const router = useAppRouter();
const searchParams = useSearchParams(); const searchParams = useSearchParams();
// query params // query params
const emailParam = searchParams.get("email"); const emailParam = searchParams.get("email");
const invitation_id = searchParams.get("invitation_id"); const invitation_id = searchParams.get("invitation_id");
const workspaceSlug = searchParams.get("slug"); const workspaceSlug = searchParams.get("slug");
const error_code = searchParams.get("error_code"); const error_code = searchParams.get("error_code");
const nextPath = searchParams.get("next_path"); const next_path = searchParams.get("next_path");
const { resolvedTheme } = useTheme();
// props // props
const { authMode: currentAuthMode } = props; const { authMode: currentAuthMode } = props;
// states // states
@@ -51,12 +48,14 @@ export const AuthRoot: FC<TAuthRoot> = observer((props) => {
const [authStep, setAuthStep] = useState<EAuthSteps>(EAuthSteps.EMAIL); const [authStep, setAuthStep] = useState<EAuthSteps>(EAuthSteps.EMAIL);
const [email, setEmail] = useState(emailParam ? emailParam.toString() : ""); const [email, setEmail] = useState(emailParam ? emailParam.toString() : "");
const [errorInfo, setErrorInfo] = useState<TAuthErrorInfo | undefined>(undefined); const [errorInfo, setErrorInfo] = useState<TAuthErrorInfo | undefined>(undefined);
const [isExistingEmail, setIsExistingEmail] = useState(false);
// plane hooks
const { t } = useTranslation();
// hooks // hooks
const { config } = useInstance(); const { config } = useInstance();
// derived values
const isOAuthEnabled =
(config && (config?.is_google_enabled || config?.is_github_enabled || config?.is_gitlab_enabled)) || false;
useEffect(() => { useEffect(() => {
if (!authMode && currentAuthMode) setAuthMode(currentAuthMode); if (!authMode && currentAuthMode) setAuthMode(currentAuthMode);
}, [currentAuthMode, authMode]); }, [currentAuthMode, authMode]);
@@ -103,102 +102,75 @@ export const AuthRoot: FC<TAuthRoot> = observer((props) => {
} }
}, [error_code, authMode]); }, [error_code, authMode]);
const isSMTPConfigured = config?.is_smtp_configured || false;
// submit handler- email verification
const handleEmailVerification = async (data: IEmailCheckData) => {
setEmail(data.email);
setErrorInfo(undefined);
await authService
.emailCheck(data)
.then(async (response) => {
if (response.existing) {
if (currentAuthMode === EAuthModes.SIGN_UP) setAuthMode(EAuthModes.SIGN_IN);
if (response.status === "MAGIC_CODE") {
setAuthStep(EAuthSteps.UNIQUE_CODE);
generateEmailUniqueCode(data.email);
} else if (response.status === "CREDENTIAL") {
setAuthStep(EAuthSteps.PASSWORD);
}
} else {
if (currentAuthMode === EAuthModes.SIGN_IN) setAuthMode(EAuthModes.SIGN_UP);
if (response.status === "MAGIC_CODE") {
setAuthStep(EAuthSteps.UNIQUE_CODE);
generateEmailUniqueCode(data.email);
} else if (response.status === "CREDENTIAL") {
setAuthStep(EAuthSteps.PASSWORD);
}
}
setIsExistingEmail(response.existing);
})
.catch((error) => {
const errorhandler = authErrorHandler(error?.error_code?.toString(), data?.email || undefined);
if (errorhandler?.type) setErrorInfo(errorhandler);
});
};
const handleEmailClear = () => {
setAuthMode(currentAuthMode);
setErrorInfo(undefined);
setEmail("");
setAuthStep(EAuthSteps.EMAIL);
router.push(currentAuthMode === EAuthModes.SIGN_IN ? `/` : "/sign-up");
};
// generating the unique code
const generateEmailUniqueCode = async (email: string): Promise<{ code: string } | undefined> => {
if (!isSMTPConfigured) return;
const payload = { email: email };
return await authService
.generateUniqueCode(payload)
.then(() => ({ code: "" }))
.catch((error) => {
const errorhandler = authErrorHandler(error?.error_code.toString());
if (errorhandler?.type) setErrorInfo(errorhandler);
throw error;
});
};
if (!authMode) return <></>; if (!authMode) return <></>;
const OauthButtonContent = authMode === EAuthModes.SIGN_UP ? "Sign up" : "Sign in";
const OAuthConfig = [
{
id: "google",
text: `${OauthButtonContent} with Google`,
icon: <Image src={GoogleLogo} height={18} width={18} alt="Google Logo" />,
onClick: () => {
window.location.assign(`${API_BASE_URL}/auth/google/${next_path ? `?next_path=${next_path}` : ``}`);
},
enabled: config?.is_google_enabled,
},
{
id: "github",
text: `${OauthButtonContent} with GitHub`,
icon: (
<Image
src={resolvedTheme === "dark" ? GithubLightLogo : GithubDarkLogo}
height={18}
width={18}
alt="GitHub Logo"
/>
),
onClick: () => {
window.location.assign(`${API_BASE_URL}/auth/github/${next_path ? `?next_path=${next_path}` : ``}`);
},
enabled: config?.is_github_enabled,
},
{
id: "gitlab",
text: `${OauthButtonContent} with GitLab`,
icon: <Image src={GitlabLogo} height={18} width={18} alt="GitLab Logo" />,
onClick: () => {
window.location.assign(`${API_BASE_URL}/auth/gitlab/${next_path ? `?next_path=${next_path}` : ``}`);
},
enabled: config?.is_gitlab_enabled,
},
];
return ( return (
<div className="relative flex flex-col space-y-6"> <div className="flex flex-col justify-center items-center flex-grow w-full py-6 mt-10">
<AuthHeader <div className="relative flex flex-col gap-6 max-w-[22.5rem] w-full">
workspaceSlug={workspaceSlug?.toString() || undefined}
invitationId={invitation_id?.toString() || undefined}
invitationEmail={email || undefined}
authMode={authMode}
currentAuthStep={authStep}
>
{errorInfo && errorInfo?.type === EErrorAlertType.BANNER_ALERT && ( {errorInfo && errorInfo?.type === EErrorAlertType.BANNER_ALERT && (
<AuthBanner bannerData={errorInfo} handleBannerData={(value) => setErrorInfo(value)} /> <AuthBanner bannerData={errorInfo} handleBannerData={(value) => setErrorInfo(value)} />
)} )}
{authStep === EAuthSteps.EMAIL && <AuthEmailForm defaultEmail={email} onSubmit={handleEmailVerification} />} <AuthHeader
{authStep === EAuthSteps.UNIQUE_CODE && ( workspaceSlug={workspaceSlug?.toString() || undefined}
<AuthUniqueCodeForm invitationId={invitation_id?.toString() || undefined}
mode={authMode} invitationEmail={email || undefined}
email={email} authMode={authMode}
isExistingEmail={isExistingEmail} currentAuthStep={authStep}
handleEmailClear={handleEmailClear} />
generateEmailUniqueCode={generateEmailUniqueCode}
nextPath={nextPath || undefined} {isOAuthEnabled && <OAuthOptions options={OAuthConfig} compact={authStep === EAuthSteps.PASSWORD} />}
/>
)} <AuthFormRoot
{authStep === EAuthSteps.PASSWORD && ( authStep={authStep}
<AuthPasswordForm authMode={authMode}
mode={authMode} email={email}
isSMTPConfigured={isSMTPConfigured} setEmail={(email) => setEmail(email)}
email={email} setAuthMode={(authMode) => setAuthMode(authMode)}
handleEmailClear={handleEmailClear} setAuthStep={(authStep) => setAuthStep(authStep)}
handleAuthStep={(step: EAuthSteps) => { setErrorInfo={(errorInfo) => setErrorInfo(errorInfo)}
if (step === EAuthSteps.UNIQUE_CODE) generateEmailUniqueCode(email); currentAuthMode={currentAuthMode}
setAuthStep(step); />
}} <TermsAndConditions authType={authMode} />
nextPath={nextPath || undefined} </div>
/>
)}
<OAuthOptions isSignUp={authMode === EAuthModes.SIGN_UP} />
<TermsAndConditions isSignUp={authMode === EAuthModes.SIGN_UP} />
</AuthHeader>
</div> </div>
); );
}); });

View File

@@ -0,0 +1,7 @@
"use client";
export const FormContainer = ({ children }: { children: React.ReactNode }) => (
<div className="flex flex-col justify-center items-center flex-grow w-full py-6 mt-10">
<div className="relative flex flex-col gap-6 max-w-[22.5rem] w-full">{children}</div>
</div>
);

View File

@@ -0,0 +1,8 @@
"use client";
export const AuthFormHeader = ({ title, description }: { title: string; description: string }) => (
<div className="flex flex-col gap-1">
<span className="text-2xl font-semibold text-custom-text-100">{title}</span>
<span className="text-2xl font-semibold text-custom-text-400">{description}</span>
</div>
);

View File

@@ -0,0 +1,2 @@
export * from "./container";
export * from "./header";

View File

@@ -43,15 +43,15 @@ export const AuthEmailForm: FC<TAuthEmailForm> = observer((props) => {
const inputRef = useRef<HTMLInputElement>(null); const inputRef = useRef<HTMLInputElement>(null);
return ( return (
<form onSubmit={handleFormSubmit} className="mt-5 space-y-4"> <form onSubmit={handleFormSubmit} className="space-y-4">
<div className="space-y-1"> <div className="space-y-1">
<label htmlFor="email" className="text-sm text-onboarding-text-300 font-medium"> <label htmlFor="email" className="text-sm text-custom-text-300 font-medium">
{t("auth.common.email.label")} {t("auth.common.email.label")}
</label> </label>
<div <div
className={cn( className={cn(
`relative flex items-center rounded-md bg-onboarding-background-200 border`, `relative flex items-center rounded-md bg-custom-background-100 border`,
!isFocused && Boolean(emailError?.email) ? `border-red-500` : `border-onboarding-border-100` !isFocused && Boolean(emailError?.email) ? `border-red-500` : `border-custom-border-300`
)} )}
onFocus={() => { onFocus={() => {
setIsFocused(true); setIsFocused(true);
@@ -67,7 +67,7 @@ export const AuthEmailForm: FC<TAuthEmailForm> = observer((props) => {
value={email} value={email}
onChange={(e) => setEmail(e.target.value)} onChange={(e) => setEmail(e.target.value)}
placeholder={t("auth.common.email.placeholder")} placeholder={t("auth.common.email.placeholder")}
className={`disable-autofill-style h-[46px] w-full placeholder:text-onboarding-text-400 autofill:bg-red-500 border-0 focus:bg-none active:bg-transparent`} className={`disable-autofill-style h-10 w-full placeholder:text-custom-text-400 autofill:bg-red-500 border-0 focus:bg-none active:bg-transparent`}
autoComplete="on" autoComplete="on"
autoFocus autoFocus
ref={inputRef} ref={inputRef}

View File

@@ -38,7 +38,7 @@ export const ForgotPasswordPopover = () => {
<Popover.Panel className="fixed z-10"> <Popover.Panel className="fixed z-10">
{({ close }) => ( {({ close }) => (
<div <div
className="border border-onboarding-border-300 bg-onboarding-background-100 rounded z-10 py-1 px-2 w-64 break-words flex items-start gap-3 text-left ml-3" className="border border-custom-border-300 bg-custom-background-100 rounded z-10 py-1 px-2 w-64 break-words flex items-start gap-3 text-left ml-3"
ref={setPopperElement} ref={setPopperElement}
style={styles.popper} style={styles.popper}
{...attributes.popper} {...attributes.popper}
@@ -51,7 +51,7 @@ export const ForgotPasswordPopover = () => {
onClick={() => close()} onClick={() => close()}
aria-label={t("aria_labels.auth_forms.close_popover")} aria-label={t("aria_labels.auth_forms.close_popover")}
> >
<X className="size-3 text-onboarding-text-200" /> <X className="size-3 text-custom-text-200" />
</button> </button>
</div> </div>
)} )}

View File

@@ -0,0 +1,145 @@
"use client";
import { observer } from "mobx-react";
import Link from "next/link";
import { useSearchParams } from "next/navigation";
import { Controller, useForm } from "react-hook-form";
// icons
import { CircleCheck } from "lucide-react";
// plane imports
import { AUTH_TRACKER_EVENTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { Button, Input, TOAST_TYPE, getButtonStyling, setToast } from "@plane/ui";
import { cn, checkEmailValidity } from "@plane/utils";
// helpers
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
// hooks
import useTimer from "@/hooks/use-timer";
// services
import { AuthService } from "@/services/auth.service";
// local components
import { FormContainer, AuthFormHeader } from "./common";
type TForgotPasswordFormValues = {
email: string;
};
const defaultValues: TForgotPasswordFormValues = {
email: "",
};
// services
const authService = new AuthService();
export const ForgotPasswordForm = observer(() => {
// search params
const searchParams = useSearchParams();
const email = searchParams.get("email");
// plane hooks
const { t } = useTranslation();
// timer
const { timer: resendTimerCode, setTimer: setResendCodeTimer } = useTimer(0);
// form info
const {
control,
formState: { errors, isSubmitting, isValid },
handleSubmit,
} = useForm<TForgotPasswordFormValues>({
defaultValues: {
...defaultValues,
email: email?.toString() ?? "",
},
});
const handleForgotPassword = async (formData: TForgotPasswordFormValues) => {
await authService
.sendResetPasswordLink({
email: formData.email,
})
.then(() => {
captureSuccess({
eventName: AUTH_TRACKER_EVENTS.forgot_password,
payload: {
email: formData.email,
},
});
setToast({
type: TOAST_TYPE.SUCCESS,
title: t("auth.forgot_password.toast.success.title"),
message: t("auth.forgot_password.toast.success.message"),
});
setResendCodeTimer(30);
})
.catch((err) => {
captureError({
eventName: AUTH_TRACKER_EVENTS.forgot_password,
payload: {
email: formData.email,
},
});
setToast({
type: TOAST_TYPE.ERROR,
title: t("auth.forgot_password.toast.error.title"),
message: err?.error ?? t("auth.forgot_password.toast.error.message"),
});
});
};
return (
<FormContainer>
<AuthFormHeader title="Reset password" description="Regain access to your account." />
<form onSubmit={handleSubmit(handleForgotPassword)} className="space-y-4">
<div className="space-y-1">
<label className="text-sm font-medium text-custom-text-300" htmlFor="email">
{t("auth.common.email.label")}
</label>
<Controller
control={control}
name="email"
rules={{
required: t("auth.common.email.errors.required"),
validate: (value) => checkEmailValidity(value) || t("auth.common.email.errors.invalid"),
}}
render={({ field: { value, onChange, ref } }) => (
<Input
id="email"
name="email"
type="email"
value={value}
onChange={onChange}
ref={ref}
hasError={Boolean(errors.email)}
placeholder={t("auth.common.email.placeholder")}
className="h-10 w-full border border-custom-border-300 !bg-custom-background-100 pr-12 placeholder:text-custom-text-400"
autoComplete="on"
disabled={resendTimerCode > 0}
/>
)}
/>
{resendTimerCode > 0 && (
<p className="flex items-start w-full gap-1 px-1 text-xs font-medium text-green-700">
<CircleCheck height={12} width={12} className="mt-0.5" />
{t("auth.forgot_password.email_sent")}
</p>
)}
</div>
<Button
type="submit"
variant="primary"
className="w-full"
size="lg"
disabled={!isValid}
loading={isSubmitting || resendTimerCode > 0}
>
{resendTimerCode > 0
? t("auth.common.resend_in", { seconds: resendTimerCode })
: t("auth.forgot_password.send_reset_link")}
</Button>
<Link href="/" className={cn("w-full", getButtonStyling("link-neutral", "lg"))}>
{t("auth.common.back_to_sign_in")}
</Link>
</form>
</FormContainer>
);
});

View File

@@ -0,0 +1,133 @@
"use client";
import React, { useState } from "react";
import { observer } from "mobx-react";
import { useSearchParams } from "next/navigation";
import { EAuthModes, EAuthSteps } from "@plane/constants";
import { IEmailCheckData } from "@plane/types";
// helpers
import { authErrorHandler, TAuthErrorInfo } from "@/helpers/authentication.helper";
// hooks
import { useInstance } from "@/hooks/store/use-instance";
import { useAppRouter } from "@/hooks/use-app-router";
// services
import { AuthService } from "@/services/auth.service";
// local components
import { AuthEmailForm } from "./email";
import { AuthPasswordForm } from "./password";
import { AuthUniqueCodeForm } from "./unique-code";
type TAuthFormRoot = {
authStep: EAuthSteps;
authMode: EAuthModes;
email: string;
setEmail: (email: string) => void;
setAuthMode: (authMode: EAuthModes) => void;
setAuthStep: (authStep: EAuthSteps) => void;
setErrorInfo: (errorInfo: TAuthErrorInfo | undefined) => void;
currentAuthMode: EAuthModes;
};
const authService = new AuthService();
export const AuthFormRoot = observer((props: TAuthFormRoot) => {
const { authStep, authMode, email, setEmail, setAuthMode, setAuthStep, setErrorInfo, currentAuthMode } = props;
// router
const router = useAppRouter();
// query params
const searchParams = useSearchParams();
const nextPath = searchParams.get("next_path");
// states
const [isExistingEmail, setIsExistingEmail] = useState(false);
// hooks
const { config } = useInstance();
const isSMTPConfigured = config?.is_smtp_configured || false;
// submit handler- email verification
const handleEmailVerification = async (data: IEmailCheckData) => {
setEmail(data.email);
setErrorInfo(undefined);
await authService
.emailCheck(data)
.then(async (response) => {
if (response.existing) {
if (currentAuthMode === EAuthModes.SIGN_UP) setAuthMode(EAuthModes.SIGN_IN);
if (response.status === "MAGIC_CODE") {
setAuthStep(EAuthSteps.UNIQUE_CODE);
generateEmailUniqueCode(data.email);
} else if (response.status === "CREDENTIAL") {
setAuthStep(EAuthSteps.PASSWORD);
}
} else {
if (currentAuthMode === EAuthModes.SIGN_IN) setAuthMode(EAuthModes.SIGN_UP);
if (response.status === "MAGIC_CODE") {
setAuthStep(EAuthSteps.UNIQUE_CODE);
generateEmailUniqueCode(data.email);
} else if (response.status === "CREDENTIAL") {
setAuthStep(EAuthSteps.PASSWORD);
}
}
setIsExistingEmail(response.existing);
})
.catch((error) => {
const errorhandler = authErrorHandler(error?.error_code?.toString(), data?.email || undefined);
if (errorhandler?.type) setErrorInfo(errorhandler);
});
};
const handleEmailClear = () => {
setAuthMode(currentAuthMode);
setErrorInfo(undefined);
setEmail("");
setAuthStep(EAuthSteps.EMAIL);
router.push(currentAuthMode === EAuthModes.SIGN_IN ? `/` : "/sign-up");
};
// generating the unique code
const generateEmailUniqueCode = async (email: string): Promise<{ code: string } | undefined> => {
if (!isSMTPConfigured) return;
const payload = { email: email };
return await authService
.generateUniqueCode(payload)
.then(() => ({ code: "" }))
.catch((error) => {
const errorhandler = authErrorHandler(error?.error_code.toString());
if (errorhandler?.type) setErrorInfo(errorhandler);
throw error;
});
};
if (authStep === EAuthSteps.EMAIL) {
return <AuthEmailForm defaultEmail={email} onSubmit={handleEmailVerification} />;
}
if (authStep === EAuthSteps.UNIQUE_CODE) {
return (
<AuthUniqueCodeForm
mode={authMode}
email={email}
isExistingEmail={isExistingEmail}
handleEmailClear={handleEmailClear}
generateEmailUniqueCode={generateEmailUniqueCode}
nextPath={nextPath || undefined}
/>
);
}
if (authStep === EAuthSteps.PASSWORD) {
return (
<AuthPasswordForm
mode={authMode}
isSMTPConfigured={isSMTPConfigured}
email={email}
handleEmailClear={handleEmailClear}
handleAuthStep={(step: EAuthSteps) => {
if (step === EAuthSteps.UNIQUE_CODE) generateEmailUniqueCode(email);
setAuthStep(step);
}}
nextPath={nextPath || undefined}
/>
);
}
return <></>;
});

View File

@@ -7,3 +7,8 @@ export * from "./email";
export * from "./forgot-password-popover"; export * from "./forgot-password-popover";
export * from "./password"; export * from "./password";
export * from "./unique-code"; export * from "./unique-code";
export * from "./common";
export * from "./forgot-password";
export * from "./reset-password";
export * from "./set-password";

View File

@@ -139,7 +139,7 @@ export const AuthPasswordForm: React.FC<Props> = observer((props: Props) => {
)} )}
<form <form
ref={formRef} ref={formRef}
className="mt-5 space-y-4" className="space-y-4"
method="POST" method="POST"
action={`${API_BASE_URL}/auth/${mode === EAuthModes.SIGN_IN ? "sign-in" : "sign-up"}/`} action={`${API_BASE_URL}/auth/${mode === EAuthModes.SIGN_IN ? "sign-in" : "sign-up"}/`}
onSubmit={async (event) => { onSubmit={async (event) => {
@@ -182,11 +182,11 @@ export const AuthPasswordForm: React.FC<Props> = observer((props: Props) => {
<input type="hidden" value={passwordFormData.email} name="email" /> <input type="hidden" value={passwordFormData.email} name="email" />
{nextPath && <input type="hidden" value={nextPath} name="next_path" />} {nextPath && <input type="hidden" value={nextPath} name="next_path" />}
<div className="space-y-1"> <div className="space-y-1">
<label htmlFor="email" className="text-sm font-medium text-onboarding-text-300"> <label htmlFor="email" className="text-sm font-medium text-custom-text-300">
{t("auth.common.email.label")} {t("auth.common.email.label")}
</label> </label>
<div <div
className={`relative flex items-center rounded-md bg-onboarding-background-200 border border-onboarding-border-100`} className={`relative flex items-center rounded-md bg-custom-background-100 border border-custom-border-300`}
> >
<Input <Input
id="email" id="email"
@@ -195,7 +195,7 @@ export const AuthPasswordForm: React.FC<Props> = observer((props: Props) => {
value={passwordFormData.email} value={passwordFormData.email}
onChange={(e) => handleFormChange("email", e.target.value)} onChange={(e) => handleFormChange("email", e.target.value)}
placeholder={t("auth.common.email.placeholder")} placeholder={t("auth.common.email.placeholder")}
className={`disable-autofill-style h-[46px] w-full placeholder:text-onboarding-text-400 border-0`} className={`disable-autofill-style h-10 w-full placeholder:text-custom-text-400 border-0`}
disabled disabled
/> />
{passwordFormData.email.length > 0 && ( {passwordFormData.email.length > 0 && (
@@ -212,10 +212,10 @@ export const AuthPasswordForm: React.FC<Props> = observer((props: Props) => {
</div> </div>
<div className="space-y-1"> <div className="space-y-1">
<label htmlFor="password" className="text-sm text-onboarding-text-300 font-medium"> <label htmlFor="password" className="text-sm text-custom-text-300 font-medium">
{mode === EAuthModes.SIGN_IN ? t("auth.common.password.label") : t("auth.common.password.set_password")} {mode === EAuthModes.SIGN_IN ? t("auth.common.password.label") : t("auth.common.password.set_password")}
</label> </label>
<div className="relative flex items-center rounded-md bg-onboarding-background-200"> <div className="relative flex items-center rounded-md bg-custom-background-100">
<Input <Input
type={showPassword?.password ? "text" : "password"} type={showPassword?.password ? "text" : "password"}
id="password" id="password"
@@ -223,7 +223,7 @@ export const AuthPasswordForm: React.FC<Props> = observer((props: Props) => {
value={passwordFormData.password} value={passwordFormData.password}
onChange={(e) => handleFormChange("password", e.target.value)} onChange={(e) => handleFormChange("password", e.target.value)}
placeholder={t("auth.common.password.placeholder")} placeholder={t("auth.common.password.placeholder")}
className="disable-autofill-style h-[46px] w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 placeholder:text-onboarding-text-400" className="disable-autofill-style h-10 w-full border border-custom-border-300 !bg-custom-background-100 pr-12 placeholder:text-custom-text-400"
onFocus={() => setIsPasswordInputFocused(true)} onFocus={() => setIsPasswordInputFocused(true)}
onBlur={() => setIsPasswordInputFocused(false)} onBlur={() => setIsPasswordInputFocused(false)}
autoComplete="on" autoComplete="on"
@@ -249,10 +249,10 @@ export const AuthPasswordForm: React.FC<Props> = observer((props: Props) => {
{mode === EAuthModes.SIGN_UP && ( {mode === EAuthModes.SIGN_UP && (
<div className="space-y-1"> <div className="space-y-1">
<label htmlFor="confirm-password" className="text-sm text-onboarding-text-300 font-medium"> <label htmlFor="confirm-password" className="text-sm text-custom-text-300 font-medium">
{t("auth.common.password.confirm_password.label")} {t("auth.common.password.confirm_password.label")}
</label> </label>
<div className="relative flex items-center rounded-md bg-onboarding-background-200"> <div className="relative flex items-center rounded-md bg-custom-background-100">
<Input <Input
type={showPassword?.retypePassword ? "text" : "password"} type={showPassword?.retypePassword ? "text" : "password"}
id="confirm-password" id="confirm-password"
@@ -260,7 +260,7 @@ export const AuthPasswordForm: React.FC<Props> = observer((props: Props) => {
value={passwordFormData.confirm_password} value={passwordFormData.confirm_password}
onChange={(e) => handleFormChange("confirm_password", e.target.value)} onChange={(e) => handleFormChange("confirm_password", e.target.value)}
placeholder={t("auth.common.password.confirm_password.placeholder")} placeholder={t("auth.common.password.confirm_password.placeholder")}
className="disable-autofill-style h-[46px] w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 placeholder:text-onboarding-text-400" className="disable-autofill-style h-10 w-full border border-custom-border-300 !bg-custom-background-100 pr-12 placeholder:text-custom-text-400"
onFocus={() => setIsRetryPasswordInputFocused(true)} onFocus={() => setIsRetryPasswordInputFocused(true)}
onBlur={() => setIsRetryPasswordInputFocused(false)} onBlur={() => setIsRetryPasswordInputFocused(false)}
/> />

View File

@@ -0,0 +1,198 @@
"use client";
import { useEffect, useMemo, useState } from "react";
import { observer } from "mobx-react";
import { useSearchParams } from "next/navigation";
// icons
import { Eye, EyeOff } from "lucide-react";
// ui
import { API_BASE_URL, E_PASSWORD_STRENGTH } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { Button, Input, PasswordStrengthIndicator } from "@plane/ui";
// components
import { getPasswordStrength } from "@plane/utils";
import { AuthBanner, FormContainer, AuthFormHeader } from "@/components/account";
// helpers
import {
EAuthenticationErrorCodes,
EErrorAlertType,
TAuthErrorInfo,
authErrorHandler,
} from "@/helpers/authentication.helper";
import { AuthService } from "@/services/auth.service";
type TResetPasswordFormValues = {
email: string;
password: string;
confirm_password?: string;
};
const defaultValues: TResetPasswordFormValues = {
email: "",
password: "",
};
// services
const authService = new AuthService();
export const ResetPasswordForm = observer(() => {
// search params
const searchParams = useSearchParams();
const uidb64 = searchParams.get("uidb64");
const token = searchParams.get("token");
const email = searchParams.get("email");
const error_code = searchParams.get("error_code");
// states
const [showPassword, setShowPassword] = useState({
password: false,
retypePassword: false,
});
const [resetFormData, setResetFormData] = useState<TResetPasswordFormValues>({
...defaultValues,
email: email ? email.toString() : "",
});
const [csrfToken, setCsrfToken] = useState<string | undefined>(undefined);
const [isPasswordInputFocused, setIsPasswordInputFocused] = useState(false);
const [isRetryPasswordInputFocused, setIsRetryPasswordInputFocused] = useState(false);
const [errorInfo, setErrorInfo] = useState<TAuthErrorInfo | undefined>(undefined);
// plane hooks
const { t } = useTranslation();
const handleShowPassword = (key: keyof typeof showPassword) =>
setShowPassword((prev) => ({ ...prev, [key]: !prev[key] }));
const handleFormChange = (key: keyof TResetPasswordFormValues, value: string) =>
setResetFormData((prev) => ({ ...prev, [key]: value }));
useEffect(() => {
if (csrfToken === undefined)
authService.requestCSRFToken().then((data) => data?.csrf_token && setCsrfToken(data.csrf_token));
}, [csrfToken]);
const isButtonDisabled = useMemo(
() =>
!!resetFormData.password &&
getPasswordStrength(resetFormData.password) === E_PASSWORD_STRENGTH.STRENGTH_VALID &&
resetFormData.password === resetFormData.confirm_password
? false
: true,
[resetFormData]
);
useEffect(() => {
if (error_code) {
const errorhandler = authErrorHandler(error_code?.toString() as EAuthenticationErrorCodes);
if (errorhandler) {
setErrorInfo(errorhandler);
}
}
}, [error_code]);
const password = resetFormData?.password ?? "";
const confirmPassword = resetFormData?.confirm_password ?? "";
const renderPasswordMatchError = !isRetryPasswordInputFocused || confirmPassword.length >= password.length;
return (
<FormContainer>
<AuthFormHeader title="Reset password" description="Create a new password." />
{errorInfo && errorInfo?.type === EErrorAlertType.BANNER_ALERT && (
<AuthBanner bannerData={errorInfo} handleBannerData={(value) => setErrorInfo(value)} />
)}
<form
className="space-y-4"
method="POST"
action={`${API_BASE_URL}/auth/reset-password/${uidb64?.toString()}/${token?.toString()}/`}
>
<input type="hidden" name="csrfmiddlewaretoken" value={csrfToken} />
<div className="space-y-1">
<label className="text-sm text-custom-text-300 font-medium" htmlFor="email">
{t("auth.common.email.label")}
</label>
<div className="relative flex items-center rounded-md bg-custom-background-100">
<Input
id="email"
name="email"
type="email"
value={resetFormData.email}
//hasError={Boolean(errors.email)}
placeholder={t("auth.common.email.placeholder")}
className="h-10 w-full border border-custom-border-300 !bg-custom-background-100 pr-12 text-custom-text-400 cursor-not-allowed"
autoComplete="on"
disabled
/>
</div>
</div>
<div className="space-y-1">
<label className="text-sm text-custom-text-300 font-medium" htmlFor="password">
{t("auth.common.password.label")}
</label>
<div className="relative flex items-center rounded-md bg-custom-background-100">
<Input
type={showPassword.password ? "text" : "password"}
name="password"
value={resetFormData.password}
onChange={(e) => handleFormChange("password", e.target.value)}
//hasError={Boolean(errors.password)}
placeholder={t("auth.common.password.placeholder")}
className="h-10 w-full border border-custom-border-300 !bg-custom-background-100 pr-12 placeholder:text-custom-text-400"
minLength={8}
onFocus={() => setIsPasswordInputFocused(true)}
onBlur={() => setIsPasswordInputFocused(false)}
autoComplete="on"
autoFocus
/>
{showPassword.password ? (
<EyeOff
className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer"
onClick={() => handleShowPassword("password")}
/>
) : (
<Eye
className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer"
onClick={() => handleShowPassword("password")}
/>
)}
</div>
<PasswordStrengthIndicator password={resetFormData.password} isFocused={isPasswordInputFocused} />
</div>
<div className="space-y-1">
<label className="text-sm text-custom-text-300 font-medium" htmlFor="confirm_password">
{t("auth.common.password.confirm_password.label")}
</label>
<div className="relative flex items-center rounded-md bg-custom-background-100">
<Input
type={showPassword.retypePassword ? "text" : "password"}
name="confirm_password"
value={resetFormData.confirm_password}
onChange={(e) => handleFormChange("confirm_password", e.target.value)}
placeholder={t("auth.common.password.confirm_password.placeholder")}
className="h-10 w-full border border-custom-border-300 !bg-custom-background-100 pr-12 placeholder:text-custom-text-400"
onFocus={() => setIsRetryPasswordInputFocused(true)}
onBlur={() => setIsRetryPasswordInputFocused(false)}
/>
{showPassword.retypePassword ? (
<EyeOff
className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer"
onClick={() => handleShowPassword("retypePassword")}
/>
) : (
<Eye
className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer"
onClick={() => handleShowPassword("retypePassword")}
/>
)}
</div>
{!!resetFormData.confirm_password &&
resetFormData.password !== resetFormData.confirm_password &&
renderPasswordMatchError && (
<span className="text-sm text-red-500">{t("auth.common.password.errors.match")}</span>
)}
</div>
<Button type="submit" variant="primary" className="w-full" size="lg" disabled={isButtonDisabled}>
{t("auth.common.password.submit")}
</Button>
</form>
</FormContainer>
);
});

View File

@@ -0,0 +1,212 @@
"use client";
import { FormEvent, useEffect, useMemo, useState } from "react";
import { observer } from "mobx-react";
import { useSearchParams } from "next/navigation";
// icons
import { Eye, EyeOff } from "lucide-react";
// plane imports
import { AUTH_TRACKER_ELEMENTS, AUTH_TRACKER_EVENTS, E_PASSWORD_STRENGTH } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { Button, Input, PasswordStrengthIndicator, TOAST_TYPE, setToast } from "@plane/ui";
// components
import { getPasswordStrength } from "@plane/utils";
// helpers
import { captureError, captureSuccess, captureView } from "@/helpers/event-tracker.helper";
// hooks
import { useUser } from "@/hooks/store";
import { useAppRouter } from "@/hooks/use-app-router";
// services
import { AuthService } from "@/services/auth.service";
// local components
import { AuthFormHeader, FormContainer } from "..";
type TResetPasswordFormValues = {
email: string;
password: string;
confirm_password?: string;
};
const defaultValues: TResetPasswordFormValues = {
email: "",
password: "",
};
// services
const authService = new AuthService();
export const SetPasswordForm = observer(() => {
// router
const router = useAppRouter();
// search params
const searchParams = useSearchParams();
const email = searchParams.get("email");
// states
const [showPassword, setShowPassword] = useState({
password: false,
retypePassword: false,
});
const [passwordFormData, setPasswordFormData] = useState<TResetPasswordFormValues>({
...defaultValues,
email: email ? email.toString() : "",
});
const [csrfToken, setCsrfToken] = useState<string | undefined>(undefined);
const [isPasswordInputFocused, setIsPasswordInputFocused] = useState(false);
const [isRetryPasswordInputFocused, setIsRetryPasswordInputFocused] = useState(false);
// plane hooks
const { t } = useTranslation();
// hooks
const { data: user, handleSetPassword } = useUser();
useEffect(() => {
captureView({
elementName: AUTH_TRACKER_ELEMENTS.SET_PASSWORD_FORM,
});
}, []);
useEffect(() => {
if (csrfToken === undefined)
authService.requestCSRFToken().then((data) => data?.csrf_token && setCsrfToken(data.csrf_token));
}, [csrfToken]);
const handleShowPassword = (key: keyof typeof showPassword) =>
setShowPassword((prev) => ({ ...prev, [key]: !prev[key] }));
const handleFormChange = (key: keyof TResetPasswordFormValues, value: string) =>
setPasswordFormData((prev) => ({ ...prev, [key]: value }));
const isButtonDisabled = useMemo(
() =>
!!passwordFormData.password &&
getPasswordStrength(passwordFormData.password) === E_PASSWORD_STRENGTH.STRENGTH_VALID &&
passwordFormData.password === passwordFormData.confirm_password
? false
: true,
[passwordFormData]
);
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
try {
e.preventDefault();
if (!csrfToken) throw new Error("csrf token not found");
await handleSetPassword(csrfToken, { password: passwordFormData.password });
captureSuccess({
eventName: AUTH_TRACKER_EVENTS.password_created,
});
router.push("/");
} catch (error: unknown) {
let message = undefined;
if (error instanceof Error) {
const err = error as Error & { error?: string };
message = err.error;
}
captureError({
eventName: AUTH_TRACKER_EVENTS.password_created,
});
setToast({
type: TOAST_TYPE.ERROR,
title: t("common.errors.default.title"),
message: message ?? t("common.errors.default.message"),
});
}
};
const password = passwordFormData?.password ?? "";
const confirmPassword = passwordFormData?.confirm_password ?? "";
const renderPasswordMatchError = !isRetryPasswordInputFocused || confirmPassword.length >= password.length;
return (
<FormContainer>
<AuthFormHeader title="Set password" description="Create a new password." />
<form className="space-y-4" onSubmit={(e) => handleSubmit(e)}>
<div className="space-y-1">
<label className="text-sm text-custom-text-300 font-medium" htmlFor="email">
{t("auth.common.email.label")}
</label>
<div className="relative flex items-center rounded-md bg-custom-background-100">
<Input
id="email"
name="email"
type="email"
value={user?.email}
//hasError={Boolean(errors.email)}
placeholder={t("auth.common.email.placeholder")}
className="h-10 w-full border border-custom-border-300 !bg-custom-background-100 pr-12 text-custom-text-400 cursor-not-allowed"
autoComplete="on"
disabled
/>
</div>
</div>
<div className="space-y-1">
<label className="text-sm text-custom-text-300 font-medium" htmlFor="password">
{t("auth.common.password.label")}
</label>
<div className="relative flex items-center rounded-md bg-custom-background-100">
<Input
type={showPassword.password ? "text" : "password"}
name="password"
value={passwordFormData.password}
onChange={(e) => handleFormChange("password", e.target.value)}
//hasError={Boolean(errors.password)}
placeholder={t("auth.common.password.placeholder")}
className="h-10 w-full border border-custom-border-300 !bg-custom-background-100 pr-12 placeholder:text-custom-text-400"
minLength={8}
onFocus={() => setIsPasswordInputFocused(true)}
onBlur={() => setIsPasswordInputFocused(false)}
autoComplete="on"
autoFocus
/>
{showPassword.password ? (
<EyeOff
className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer"
onClick={() => handleShowPassword("password")}
/>
) : (
<Eye
className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer"
onClick={() => handleShowPassword("password")}
/>
)}
</div>
<PasswordStrengthIndicator password={passwordFormData.password} isFocused={isPasswordInputFocused} />
</div>
<div className="space-y-1">
<label className="text-sm text-custom-text-300 font-medium" htmlFor="confirm_password">
{t("auth.common.password.confirm_password.label")}
</label>
<div className="relative flex items-center rounded-md bg-custom-background-100">
<Input
type={showPassword.retypePassword ? "text" : "password"}
name="confirm_password"
value={passwordFormData.confirm_password}
onChange={(e) => handleFormChange("confirm_password", e.target.value)}
placeholder={t("auth.common.password.confirm_password.placeholder")}
className="h-10 w-full border border-custom-border-300 !bg-custom-background-100 pr-12 placeholder:text-custom-text-400"
onFocus={() => setIsRetryPasswordInputFocused(true)}
onBlur={() => setIsRetryPasswordInputFocused(false)}
/>
{showPassword.retypePassword ? (
<EyeOff
className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer"
onClick={() => handleShowPassword("retypePassword")}
/>
) : (
<Eye
className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer"
onClick={() => handleShowPassword("retypePassword")}
/>
)}
</div>
{!!passwordFormData.confirm_password &&
passwordFormData.password !== passwordFormData.confirm_password &&
renderPasswordMatchError && (
<span className="text-sm text-red-500">{t("auth.common.password.errors.match")}</span>
)}
</div>
<Button type="submit" variant="primary" className="w-full" size="lg" disabled={isButtonDisabled}>
{t("common.continue")}
</Button>
</form>
</FormContainer>
);
});

View File

@@ -89,7 +89,7 @@ export const AuthUniqueCodeForm: React.FC<TAuthUniqueCodeForm> = (props) => {
return ( return (
<form <form
className="mt-5 space-y-4" className="space-y-4"
method="POST" method="POST"
action={`${API_BASE_URL}/auth/${mode === EAuthModes.SIGN_IN ? "magic-sign-in" : "magic-sign-up"}/`} action={`${API_BASE_URL}/auth/${mode === EAuthModes.SIGN_IN ? "magic-sign-in" : "magic-sign-up"}/`}
onSubmit={() => { onSubmit={() => {
@@ -116,11 +116,11 @@ export const AuthUniqueCodeForm: React.FC<TAuthUniqueCodeForm> = (props) => {
<input type="hidden" value={uniqueCodeFormData.email} name="email" /> <input type="hidden" value={uniqueCodeFormData.email} name="email" />
{nextPath && <input type="hidden" value={nextPath} name="next_path" />} {nextPath && <input type="hidden" value={nextPath} name="next_path" />}
<div className="space-y-1"> <div className="space-y-1">
<label htmlFor="email" className="text-sm font-medium text-onboarding-text-300"> <label htmlFor="email" className="text-sm font-medium text-custom-text-300">
{t("auth.common.email.label")} {t("auth.common.email.label")}
</label> </label>
<div <div
className={`relative flex items-center rounded-md bg-onboarding-background-200 border border-onboarding-border-100`} className={`relative flex items-center rounded-md bg-custom-background-100 border border-custom-border-300`}
> >
<Input <Input
id="email" id="email"
@@ -129,7 +129,7 @@ export const AuthUniqueCodeForm: React.FC<TAuthUniqueCodeForm> = (props) => {
value={uniqueCodeFormData.email} value={uniqueCodeFormData.email}
onChange={(e) => handleFormChange("email", e.target.value)} onChange={(e) => handleFormChange("email", e.target.value)}
placeholder={t("auth.common.email.placeholder")} placeholder={t("auth.common.email.placeholder")}
className="disable-autofill-style h-[46px] w-full placeholder:text-onboarding-text-400 border-0" className="disable-autofill-style h-10 w-full placeholder:text-custom-text-400 border-0"
autoComplete="on" autoComplete="on"
disabled disabled
/> />
@@ -147,7 +147,7 @@ export const AuthUniqueCodeForm: React.FC<TAuthUniqueCodeForm> = (props) => {
</div> </div>
<div className="space-y-1"> <div className="space-y-1">
<label htmlFor="unique-code" className="text-sm font-medium text-onboarding-text-300"> <label htmlFor="unique-code" className="text-sm font-medium text-custom-text-300">
{t("auth.common.unique_code.label")} {t("auth.common.unique_code.label")}
</label> </label>
<Input <Input
@@ -156,7 +156,7 @@ export const AuthUniqueCodeForm: React.FC<TAuthUniqueCodeForm> = (props) => {
value={uniqueCodeFormData.code} value={uniqueCodeFormData.code}
onChange={(e) => handleFormChange("code", e.target.value)} onChange={(e) => handleFormChange("code", e.target.value)}
placeholder={t("auth.common.unique_code.placeholder")} placeholder={t("auth.common.unique_code.placeholder")}
className="disable-autofill-style h-[46px] w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 placeholder:text-onboarding-text-400" className="disable-autofill-style h-10 w-full border border-custom-border-300 !bg-custom-background-100 pr-12 placeholder:text-custom-text-400"
autoFocus autoFocus
/> />
<div className="flex w-full items-center justify-between px-1 text-xs pt-1"> <div className="flex w-full items-center justify-between px-1 text-xs pt-1">
@@ -170,7 +170,7 @@ export const AuthUniqueCodeForm: React.FC<TAuthUniqueCodeForm> = (props) => {
onClick={() => generateNewCode(uniqueCodeFormData.email)} onClick={() => generateNewCode(uniqueCodeFormData.email)}
className={ className={
isRequestNewCodeDisabled isRequestNewCodeDisabled
? "text-onboarding-text-400" ? "text-custom-text-400"
: "font-medium text-custom-primary-300 hover:text-custom-primary-200" : "font-medium text-custom-primary-300 hover:text-custom-primary-200"
} }
disabled={isRequestNewCodeDisabled} disabled={isRequestNewCodeDisabled}

View File

@@ -1,4 +1,3 @@
export * from "./oauth";
export * from "./auth-forms"; export * from "./auth-forms";
export * from "./deactivate-account-modal"; export * from "./deactivate-account-modal";
export * from "./terms-and-conditions"; export * from "./terms-and-conditions";

View File

@@ -1,42 +0,0 @@
import { FC } from "react";
import { useSearchParams } from "next/navigation";
import Image from "next/image";
import { useTheme } from "next-themes";
// helpers
import { API_BASE_URL } from "@plane/constants";
// images
import githubLightModeImage from "/public/logos/github-black.png";
import githubDarkModeImage from "/public/logos/github-dark.svg";
export type GithubOAuthButtonProps = {
text: string;
};
export const GithubOAuthButton: FC<GithubOAuthButtonProps> = (props) => {
const searchParams = useSearchParams();
const next_path = searchParams.get("next_path");
const { text } = props;
// hooks
const { resolvedTheme } = useTheme();
const handleSignIn = () => {
window.location.assign(`${API_BASE_URL}/auth/github/${next_path ? `?next_path=${next_path}` : ``}`);
};
return (
<button
className={`flex h-[42px] w-full items-center justify-center gap-2 rounded border px-2 text-sm font-medium text-custom-text-100 duration-300 bg-onboarding-background-200 hover:bg-onboarding-background-300 ${
resolvedTheme === "dark" ? "border-[#43484F]" : "border-[#D9E4FF]"
}`}
onClick={handleSignIn}
>
<Image
src={resolvedTheme === "dark" ? githubDarkModeImage : githubLightModeImage}
height={20}
width={20}
alt="GitHub Logo"
/>
{text}
</button>
);
};

View File

@@ -1,36 +0,0 @@
import { FC } from "react";
import Image from "next/image";
import { useSearchParams } from "next/navigation";
import { useTheme } from "next-themes";
// helpers
import { API_BASE_URL } from "@plane/constants";
// images
import GitlabLogo from "/public/logos/gitlab-logo.svg";
export type GitlabOAuthButtonProps = {
text: string;
};
export const GitlabOAuthButton: FC<GitlabOAuthButtonProps> = (props) => {
const searchParams = useSearchParams();
const nextPath = searchParams.get("next_path") || undefined;
const { text } = props;
// hooks
const { resolvedTheme } = useTheme();
const handleSignIn = () => {
window.location.assign(`${API_BASE_URL}/auth/gitlab/${nextPath ? `?next_path=${nextPath}` : ``}`);
};
return (
<button
className={`flex h-[42px] w-full items-center justify-center gap-2 rounded border px-2 text-sm font-medium text-custom-text-100 duration-300 bg-onboarding-background-200 hover:bg-onboarding-background-300 ${
resolvedTheme === "dark" ? "border-[#43484F]" : "border-[#D9E4FF]"
}`}
onClick={handleSignIn}
>
<Image src={GitlabLogo} height={20} width={20} alt="GitLab Logo" />
{text}
</button>
);
};

View File

@@ -1,36 +0,0 @@
import { FC } from "react";
import { useSearchParams } from "next/navigation";
import Image from "next/image";
import { useTheme } from "next-themes";
// helpers
import { API_BASE_URL } from "@plane/constants";
// images
import GoogleLogo from "/public/logos/google-logo.svg";
export type GoogleOAuthButtonProps = {
text: string;
};
export const GoogleOAuthButton: FC<GoogleOAuthButtonProps> = (props) => {
const searchParams = useSearchParams();
const next_path = searchParams.get("next_path");
const { text } = props;
// hooks
const { resolvedTheme } = useTheme();
const handleSignIn = () => {
window.location.assign(`${API_BASE_URL}/auth/google/${next_path ? `?next_path=${next_path}` : ``}`);
};
return (
<button
className={`flex h-[42px] w-full items-center justify-center gap-2 rounded border px-2 text-sm font-medium text-custom-text-100 duration-300 bg-onboarding-background-200 hover:bg-onboarding-background-300 ${
resolvedTheme === "dark" ? "border-[#43484F]" : "border-[#D9E4FF]"
}`}
onClick={handleSignIn}
>
<Image src={GoogleLogo} height={18} width={18} alt="Google Logo" />
{text}
</button>
);
};

View File

@@ -1,4 +0,0 @@
export * from "./oauth-options";
export * from "./google-button";
export * from "./github-button";
export * from "./gitlab-button";

View File

@@ -1,38 +0,0 @@
import { observer } from "mobx-react";
// components
import { GithubOAuthButton, GitlabOAuthButton, GoogleOAuthButton } from "@/components/account";
// hooks
import { useInstance } from "@/hooks/store";
type TOAuthOptionProps = {
isSignUp?: boolean;
};
export const OAuthOptions: React.FC<TOAuthOptionProps> = observer(() => {
// hooks
const { config } = useInstance();
const isOAuthEnabled =
(config && (config?.is_google_enabled || config?.is_github_enabled || config?.is_gitlab_enabled)) || false;
if (!isOAuthEnabled) return null;
return (
<>
<div className="mt-4 flex items-center">
<hr className="w-full border-onboarding-border-100" />
<p className="mx-3 flex-shrink-0 text-center text-sm text-onboarding-text-400">or</p>
<hr className="w-full border-onboarding-border-100" />
</div>
<div className={`mt-7 grid gap-4 overflow-hidden`}>
{config?.is_google_enabled && (
<div className="flex h-[42px] items-center !overflow-hidden">
<GoogleOAuthButton text="Continue with Google" />
</div>
)}
{config?.is_github_enabled && <GithubOAuthButton text="Continue with GitHub" />}
{config?.is_gitlab_enabled && <GitlabOAuthButton text="Continue with GitLab" />}
</div>
</>
);
});

View File

@@ -1,25 +1,35 @@
import React, { FC } from "react"; import React from "react";
import Link from "next/link"; import Link from "next/link";
import { EAuthModes } from "@plane/constants";
type Props = { interface TermsAndConditionsProps {
isSignUp?: boolean; authType?: EAuthModes;
}; }
export const TermsAndConditions: FC<Props> = (props) => { // Constants for better maintainability
const { isSignUp = false } = props; const LEGAL_LINKS = {
return ( termsOfService: "https://plane.so/legals/terms-and-conditions",
<div className="flex items-center justify-center py-6"> privacyPolicy: "https://plane.so/legals/privacy-policy",
<p className="text-center text-sm text-onboarding-text-200 whitespace-pre-line"> } as const;
{isSignUp ? "By creating an account" : "By signing in"}, you agree to our{" \n"}
<Link href="https://plane.so/legals/terms-and-conditions" target="_blank" rel="noopener noreferrer"> const MESSAGES = {
<span className="text-sm font-medium underline hover:cursor-pointer">Terms of Service</span> [EAuthModes.SIGN_UP]: "By creating an account",
</Link>{" "} [EAuthModes.SIGN_IN]: "By signing in",
and{" "} } as const;
<Link href="https://plane.so/legals/privacy-policy" target="_blank" rel="noopener noreferrer">
<span className="text-sm font-medium underline hover:cursor-pointer">Privacy Policy</span> // Reusable link component to reduce duplication
</Link> const LegalLink: React.FC<{ href: string; children: React.ReactNode }> = ({ href, children }) => (
{"."} <Link href={href} className="text-custom-text-200" target="_blank" rel="noopener noreferrer">
</p> <span className="text-sm font-medium underline hover:cursor-pointer">{children}</span>
</div> </Link>
); );
};
export const TermsAndConditions: React.FC<TermsAndConditionsProps> = ({ authType = EAuthModes.SIGN_IN }) => (
<div className="flex items-center justify-center">
<p className="text-center text-sm text-custom-text-300 whitespace-pre-line">
{`${MESSAGES[authType]}, you understand and agree to \n our `}
<LegalLink href={LEGAL_LINKS.termsOfService}>Terms of Service</LegalLink> and{" "}
<LegalLink href={LEGAL_LINKS.privacyPolicy}>Privacy Policy</LegalLink>.
</p>
</div>
);

View File

@@ -0,0 +1,18 @@
"use client";
import React from "react";
import { AuthRoot } from "@/components/account";
import { EAuthModes } from "@/helpers/authentication.helper";
import { AuthFooter } from "./footer";
import { AuthHeader } from "./header";
type AuthBaseProps = {
authType: EAuthModes;
};
export const AuthBase = ({ authType }: AuthBaseProps) => (
<div className="relative z-10 flex flex-col items-center w-screen h-screen overflow-hidden overflow-y-auto pt-6 pb-10 px-8">
<AuthHeader type={authType} />
<AuthRoot authMode={authType} />
<AuthFooter />
</div>
);

View File

@@ -0,0 +1,38 @@
"use client";
import React from "react";
import { AccentureLogo, DolbyLogo, SonyLogo, ZerodhaLogo } from "@plane/ui";
const BRAND_LOGOS: {
id: string;
icon: React.ReactNode;
}[] = [
{
id: "zerodha",
icon: <ZerodhaLogo className="h-7 w-24 text-[#387ED1]" />,
},
{
id: "sony",
icon: <SonyLogo className="h-7 w-16 dark:text-white" />,
},
{
id: "dolby",
icon: <DolbyLogo className="h-7 w-16 dark:text-white" />,
},
{
id: "accenture",
icon: <AccentureLogo className="h-7 w-24 dark:text-white" />,
},
];
export const AuthFooter = () => (
<div className="flex flex-col items-center gap-6">
<span className="text-sm text-custom-text-300 whitespace-nowrap">Join 10,000+ teams building with Plane</span>
<div className="flex items-center justify-center gap-x-10 gap-y-4 w-full flex-wrap">
{BRAND_LOGOS.map((brand) => (
<div className="flex items-center justify-center h-7 flex-1" key={brand.id}>
{brand.icon}
</div>
))}
</div>
</div>
);

View File

@@ -0,0 +1,60 @@
"use client";
import React from "react";
import { observer } from "mobx-react";
import Link from "next/link";
import { AUTH_TRACKER_ELEMENTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { PlaneLockup } from "@plane/ui";
import { PageHead } from "@/components/core/page-title";
import { EAuthModes } from "@/helpers/authentication.helper";
import { useInstance } from "@/hooks/store";
const authContentMap = {
[EAuthModes.SIGN_IN]: {
pageTitle: "Sign up",
text: "auth.common.new_to_plane",
linkText: "Sign up",
linkHref: "/sign-up",
},
[EAuthModes.SIGN_UP]: {
pageTitle: "Sign in",
text: "auth.common.already_have_an_account",
linkText: "Sign in",
linkHref: "/sign-in",
},
};
type AuthHeaderProps = {
type: EAuthModes;
};
export const AuthHeader = observer(({ type }: AuthHeaderProps) => {
const { t } = useTranslation();
// store
const { config } = useInstance();
// derived values
const enableSignUpConfig = config?.enable_signup ?? false;
return (
<>
<PageHead title={t(authContentMap[type].pageTitle) + " - Plane"} />
<div className="flex items-center justify-between gap-6 w-full flex-shrink-0 sticky top-0">
<Link href="/">
<PlaneLockup height={20} width={95} className="text-custom-text-100" />
</Link>
{enableSignUpConfig && (
<div className="flex flex-col items-end text-sm font-medium text-center sm:items-center sm:gap-2 sm:flex-row text-custom-text-300">
{t(authContentMap[type].text)}
<Link
data-ph-element={AUTH_TRACKER_ELEMENTS.NAVIGATE_TO_SIGN_UP}
href={authContentMap[type].linkHref}
className="font-semibold text-custom-primary-100 hover:underline"
>
{t(authContentMap[type].linkText)}
</Link>
</div>
)}
</div>
</>
);
});

View File

@@ -1,3 +1,5 @@
export * from "./project"; export * from "./project";
export * from "./workspace"; export * from "./workspace";
export * from "./not-authorized-view"; export * from "./not-authorized-view";
export * from "./header";
export * from "./auth-base";

View File

@@ -11,9 +11,9 @@ export const LatestFeatureBlock = () => {
return ( return (
<> <>
<div className="mx-auto mt-16 flex rounded-[3.5px] border border-onboarding-border-200 bg-onboarding-background-100 py-2 sm:w-96"> <div className="mx-auto mt-16 flex rounded-[3.5px] border border-custom-border-200 bg-custom-background-100 py-2 sm:w-96">
<Lightbulb className="mx-3 mr-2 h-7 w-7" /> <Lightbulb className="mx-3 mr-2 h-7 w-7" />
<p className="text-left text-sm text-onboarding-text-100"> <p className="text-left text-sm text-custom-text-100">
Pages gets a facelift! Write anything and use Galileo to help you start.{" "} Pages gets a facelift! Write anything and use Galileo to help you start.{" "}
<Link href="https://plane.so/changelog" target="_blank" rel="noopener noreferrer"> <Link href="https://plane.so/changelog" target="_blank" rel="noopener noreferrer">
<span className="text-sm font-medium underline hover:cursor-pointer">Learn more</span> <span className="text-sm font-medium underline hover:cursor-pointer">Learn more</span>
@@ -21,8 +21,8 @@ export const LatestFeatureBlock = () => {
</p> </p>
</div> </div>
<div <div
className={`mx-auto mt-8 overflow-hidden rounded-md border border-onboarding-border-200 object-cover sm:h-52 sm:w-96 ${ className={`mx-auto mt-8 overflow-hidden rounded-md border border-custom-border-200 object-cover sm:h-52 sm:w-96 ${
resolvedTheme === "dark" ? "bg-onboarding-background-100" : "bg-custom-primary-70" resolvedTheme === "dark" ? "bg-custom-background-100" : "bg-custom-primary-70"
}`} }`}
> >
<div className="h-[90%]"> <div className="h-[90%]">
@@ -30,7 +30,7 @@ export const LatestFeatureBlock = () => {
src={latestFeatures} src={latestFeatures}
alt="Plane Work items" alt="Plane Work items"
className={`-mt-2 ml-10 h-full rounded-md ${ className={`-mt-2 ml-10 h-full rounded-md ${
resolvedTheme === "dark" ? "bg-onboarding-background-100" : "bg-custom-primary-70" resolvedTheme === "dark" ? "bg-custom-background-100" : "bg-custom-primary-70"
}`} }`}
/> />
</div> </div>

View File

@@ -11,7 +11,7 @@ export const LogoSpinner = () => {
return ( return (
<div className="flex items-center justify-center"> <div className="flex items-center justify-center">
<Image src={logoSrc} alt="logo" className="size-16 sm:size-20 mr-2" /> <Image src={logoSrc} alt="logo" className="h-6 w-auto sm:h-11" />
</div> </div>
); );
}; };

View File

@@ -1,12 +1,9 @@
import Image from "next/image";
import { USER_TRACKER_ELEMENTS } from "@plane/constants"; import { USER_TRACKER_ELEMENTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n"; import { useTranslation } from "@plane/i18n";
// ui // ui
import { getButtonStyling } from "@plane/ui"; import { getButtonStyling, PlaneLogo } from "@plane/ui";
// helpers // helpers
import { cn } from "@plane/utils"; import { cn } from "@plane/utils";
// assets
import PlaneLogo from "@/public/plane-logos/blue-without-text.png";
export const ProductUpdatesFooter = () => { export const ProductUpdatesFooter = () => {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -60,7 +57,7 @@ export const ProductUpdatesFooter = () => {
"flex gap-1.5 items-center text-center font-medium hover:underline underline-offset-2 outline-none" "flex gap-1.5 items-center text-center font-medium hover:underline underline-offset-2 outline-none"
)} )}
> >
<Image src={PlaneLogo} alt="Plane" width={12} height={12} /> <PlaneLogo className="h-4 w-auto text-custom-text-100" />
{t("powered_by_plane_pages")} {t("powered_by_plane_pages")}
</a> </a>
</div> </div>

View File

@@ -42,7 +42,7 @@ export const InstanceNotReady: FC = () => {
<div className="relative flex flex-col justify-center items-center space-y-4"> <div className="relative flex flex-col justify-center items-center space-y-4">
<h1 className="text-3xl font-bold pb-3">Welcome aboard Plane!</h1> <h1 className="text-3xl font-bold pb-3">Welcome aboard Plane!</h1>
<Image src={PlaneTakeOffImage} alt="Plane Logo" /> <Image src={PlaneTakeOffImage} alt="Plane Logo" />
<p className="font-medium text-base text-onboarding-text-400"> <p className="font-medium text-base text-custom-text-400">
Get started by setting up your instance and workspace Get started by setting up your instance and workspace
</p> </p>
</div> </div>

View File

@@ -41,7 +41,7 @@ export const WorkspaceDraftIssuesRoot: FC<TWorkspaceDraftIssuesRoot> = observer(
[EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER], [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER],
EUserPermissionsLevel.WORKSPACE EUserPermissionsLevel.WORKSPACE
); );
const noProjectResolvedPath = useResolvedAssetPath({ basePath: "/empty-state/onboarding/projects" }); const noProjectResolvedPath = useResolvedAssetPath({ basePath: "/empty-state/draft/draft-issues-empty" });
//swr hook for fetching issue properties //swr hook for fetching issue properties
useWorkspaceIssueProperties(workspaceSlug); useWorkspaceIssueProperties(workspaceSlug);

View File

@@ -1,20 +1,16 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import Image from "next/image";
// icons // icons
import { useTheme } from "next-themes";
// types // types
import { OctagonAlert } from "lucide-react"; import { OctagonAlert } from "lucide-react";
import { IWorkspaceMemberInvitation, TOnboardingSteps } from "@plane/types"; import { IWorkspaceMemberInvitation, TOnboardingSteps } from "@plane/types";
// components // components
import { Invitations, OnboardingHeader, SwitchAccountDropdown, CreateWorkspace } from "@/components/onboarding"; import { Invitations, SwitchAccountDropdown, CreateWorkspace } from "@/components/onboarding";
// hooks // hooks
import { useUser } from "@/hooks/store"; import { useUser } from "@/hooks/store";
// plane web helpers // plane web helpers
import { getIsWorkspaceCreationDisabled } from "@/plane-web/helpers/instance.helper"; import { getIsWorkspaceCreationDisabled } from "@/plane-web/helpers/instance.helper";
// assets // local components
import CreateJoinWorkspaceDark from "@/public/onboarding/create-join-workspace-dark.webp";
import CreateJoinWorkspace from "@/public/onboarding/create-join-workspace-light.webp";
import { LogoSpinner } from "../common"; import { LogoSpinner } from "../common";
export enum ECreateOrJoinWorkspaceViews { export enum ECreateOrJoinWorkspaceViews {
@@ -35,8 +31,6 @@ export const CreateOrJoinWorkspaces: React.FC<Props> = observer((props) => {
const [currentView, setCurrentView] = useState<ECreateOrJoinWorkspaceViews | null>(null); const [currentView, setCurrentView] = useState<ECreateOrJoinWorkspaceViews | null>(null);
// store hooks // store hooks
const { data: user } = useUser(); const { data: user } = useUser();
// hooks
const { resolvedTheme } = useTheme();
// derived values // derived values
const isWorkspaceCreationEnabled = getIsWorkspaceCreationDisabled() === false; const isWorkspaceCreationEnabled = getIsWorkspaceCreationDisabled() === false;
@@ -57,12 +51,6 @@ export const CreateOrJoinWorkspaces: React.FC<Props> = observer((props) => {
return ( return (
<div className="flex h-full w-full"> <div className="flex h-full w-full">
<div className="w-full h-full overflow-auto px-6 py-10 sm:px-7 sm:py-14 md:px-14 lg:px-28"> <div className="w-full h-full overflow-auto px-6 py-10 sm:px-7 sm:py-14 md:px-14 lg:px-28">
<div className="flex items-center justify-between">
<OnboardingHeader currentStep={totalSteps - 1} totalSteps={totalSteps} />
<div className="shrink-0 lg:hidden">
<SwitchAccountDropdown />
</div>
</div>
<div className="flex flex-col w-full items-center justify-center p-8 mt-6"> <div className="flex flex-col w-full items-center justify-center p-8 mt-6">
{currentView === ECreateOrJoinWorkspaceViews.WORKSPACE_JOIN ? ( {currentView === ECreateOrJoinWorkspaceViews.WORKSPACE_JOIN ? (
<Invitations <Invitations
@@ -97,16 +85,7 @@ export const CreateOrJoinWorkspaces: React.FC<Props> = observer((props) => {
)} )}
</div> </div>
</div> </div>
<div className="hidden lg:block relative w-2/5 h-screen overflow-hidden px-6 py-10 sm:px-7 sm:py-14 md:px-14 lg:px-28"> <SwitchAccountDropdown />
<SwitchAccountDropdown />
<div className="absolute inset-0 z-0">
<Image
src={resolvedTheme === "dark" ? CreateJoinWorkspaceDark : CreateJoinWorkspace}
className="h-screen w-auto float-end object-cover"
alt="Profile setup"
/>
</div>
</div>
</div> </div>
); );
}); });

View File

@@ -135,20 +135,20 @@ export const CreateWorkspace: React.FC<Props> = observer((props) => {
</span> </span>
</Button> </Button>
<div className="mx-auto mt-4 flex items-center sm:w-96"> <div className="mx-auto mt-4 flex items-center sm:w-96">
<hr className="w-full border-onboarding-border-100" /> <hr className="w-full border-custom-border-300" />
<p className="mx-3 flex-shrink-0 text-center text-sm text-onboarding-text-400">or</p> <p className="mx-3 flex-shrink-0 text-center text-sm text-custom-text-400">or</p>
<hr className="w-full border-onboarding-border-100" /> <hr className="w-full border-custom-border-300" />
</div> </div>
</> </>
)} )}
<div className="text-center space-y-1 py-4 mx-auto"> <div className="text-center space-y-1 py-4 mx-auto">
<h3 className="text-3xl font-bold text-onboarding-text-100">{t("workspace_creation.heading")}</h3> <h3 className="text-3xl font-bold text-custom-text-100">{t("workspace_creation.heading")}</h3>
<p className="font-medium text-onboarding-text-400">{t("workspace_creation.subheading")}</p> <p className="font-medium text-custom-text-400">{t("workspace_creation.subheading")}</p>
</div> </div>
<form className="w-full mx-auto mt-2 space-y-4" onSubmit={handleSubmit(handleCreateWorkspace)}> <form className="w-full mx-auto mt-2 space-y-4" onSubmit={handleSubmit(handleCreateWorkspace)}>
<div className="space-y-1"> <div className="space-y-1">
<label <label
className="text-sm text-onboarding-text-300 font-medium after:content-['*'] after:ml-0.5 after:text-red-500" className="text-sm text-custom-text-300 font-medium after:content-['*'] after:ml-0.5 after:text-red-500"
htmlFor="name" htmlFor="name"
> >
{t("workspace_creation.form.name.label")} {t("workspace_creation.form.name.label")}
@@ -182,7 +182,7 @@ export const CreateWorkspace: React.FC<Props> = observer((props) => {
placeholder={t("workspace_creation.form.name.placeholder")} placeholder={t("workspace_creation.form.name.placeholder")}
ref={ref} ref={ref}
hasError={Boolean(errors.name)} hasError={Boolean(errors.name)}
className="w-full border-onboarding-border-100 placeholder:text-custom-text-400" className="w-full border-custom-border-300 placeholder:text-custom-text-400"
autoFocus autoFocus
/> />
</div> </div>
@@ -192,7 +192,7 @@ export const CreateWorkspace: React.FC<Props> = observer((props) => {
</div> </div>
<div className="space-y-1"> <div className="space-y-1">
<label <label
className="text-sm text-onboarding-text-300 font-medium after:content-['*'] after:ml-0.5 after:text-red-500" className="text-sm text-custom-text-300 font-medium after:content-['*'] after:ml-0.5 after:text-red-500"
htmlFor="slug" htmlFor="slug"
> >
{t("workspace_creation.form.url.label")} {t("workspace_creation.form.url.label")}
@@ -210,7 +210,7 @@ export const CreateWorkspace: React.FC<Props> = observer((props) => {
render={({ field: { value, ref, onChange } }) => ( render={({ field: { value, ref, onChange } }) => (
<div <div
className={`relative flex items-center rounded-md border-[0.5px] px-3 ${ className={`relative flex items-center rounded-md border-[0.5px] px-3 ${
invalidSlug ? "border-red-500" : "border-onboarding-border-100" invalidSlug ? "border-red-500" : "border-custom-border-300"
}`} }`}
> >
<span className="whitespace-nowrap text-sm">{window && window.location.host}/</span> <span className="whitespace-nowrap text-sm">{window && window.location.host}/</span>
@@ -232,7 +232,7 @@ export const CreateWorkspace: React.FC<Props> = observer((props) => {
</div> </div>
)} )}
/> />
<p className="text-sm text-onboarding-text-300">{t("workspace_creation.form.url.edit_slug")}</p> <p className="text-sm text-custom-text-300">{t("workspace_creation.form.url.edit_slug")}</p>
{slugError && ( {slugError && (
<p className="-mt-3 text-sm text-red-500">{t("workspace_creation.errors.validation.url_already_taken")}</p> <p className="-mt-3 text-sm text-red-500">{t("workspace_creation.errors.validation.url_already_taken")}</p>
)} )}
@@ -241,10 +241,10 @@ export const CreateWorkspace: React.FC<Props> = observer((props) => {
)} )}
{errors.slug && <span className="text-sm text-red-500">{errors.slug.message}</span>} {errors.slug && <span className="text-sm text-red-500">{errors.slug.message}</span>}
</div> </div>
<hr className="w-full border-onboarding-border-100" /> <hr className="w-full border-custom-border-300" />
<div className="space-y-1"> <div className="space-y-1">
<label <label
className="text-sm text-onboarding-text-300 font-medium after:content-['*'] after:ml-0.5 after:text-red-500" className="text-sm text-custom-text-300 font-medium after:content-['*'] after:ml-0.5 after:text-red-500"
htmlFor="organization_size" htmlFor="organization_size"
> >
{t("workspace_creation.form.organization_size.label")} {t("workspace_creation.form.organization_size.label")}
@@ -265,7 +265,7 @@ export const CreateWorkspace: React.FC<Props> = observer((props) => {
</span> </span>
) )
} }
buttonClassName="!border-[0.5px] !border-onboarding-border-100 !shadow-none !rounded-md" buttonClassName="!border-[0.5px] !border-custom-border-300 !shadow-none !rounded-md"
input input
optionsClassName="w-full" optionsClassName="w-full"
> >

View File

@@ -1,24 +1,85 @@
import { FC } from "react"; "use client";
import Image from "next/image";
// images
import BluePlaneLogoWithoutText from "@/public/plane-logos/blue-without-text.png";
// components
import { OnboardingStepIndicator } from "./step-indicator";
export type OnboardingHeaderProps = { import { FC } from "react";
currentStep: number; import { observer } from "mobx-react";
totalSteps: number; import { ChevronLeft } from "lucide-react";
// plane imports
import { EOnboardingSteps, TOnboardingStep } from "@plane/types";
import { PlaneLockup, Tooltip } from "@plane/ui";
import { cn } from "@plane/utils";
// components
import { SwitchAccountDropdown } from "@/components/onboarding";
// hooks
import { useUser } from "@/hooks/store";
type OnboardingHeaderProps = {
currentStep: EOnboardingSteps;
updateCurrentStep: (step: EOnboardingSteps) => void;
hasInvitations: boolean;
}; };
export const OnboardingHeader: FC<OnboardingHeaderProps> = (props) => { export const OnboardingHeader: FC<OnboardingHeaderProps> = observer((props) => {
const { currentStep, totalSteps } = props; const { currentStep, updateCurrentStep, hasInvitations } = props;
// store hooks
const { data: user } = useUser();
// handle step back
const handleStepBack = () => {
switch (currentStep) {
case EOnboardingSteps.ROLE_SETUP:
updateCurrentStep(EOnboardingSteps.PROFILE_SETUP);
break;
case EOnboardingSteps.USE_CASE_SETUP:
updateCurrentStep(EOnboardingSteps.ROLE_SETUP);
break;
case EOnboardingSteps.WORKSPACE_CREATE_OR_JOIN:
updateCurrentStep(EOnboardingSteps.USE_CASE_SETUP);
break;
}
};
// can go back
const canGoBack = ![EOnboardingSteps.PROFILE_SETUP, EOnboardingSteps.INVITE_MEMBERS].includes(currentStep);
// Get current step number for progress tracking
const getCurrentStepNumber = (): number => {
const stepOrder: TOnboardingStep[] = [
EOnboardingSteps.PROFILE_SETUP,
EOnboardingSteps.ROLE_SETUP,
EOnboardingSteps.USE_CASE_SETUP,
...(hasInvitations
? [EOnboardingSteps.WORKSPACE_CREATE_OR_JOIN]
: [EOnboardingSteps.WORKSPACE_CREATE_OR_JOIN, EOnboardingSteps.INVITE_MEMBERS]),
];
return stepOrder.indexOf(currentStep) + 1;
};
// derived values
const currentStepNumber = getCurrentStepNumber();
const totalSteps = hasInvitations ? 4 : 5; // 4 if invites available, 5 if not
const userName = user?.display_name ?? `${user?.first_name} ${user?.last_name}` ?? user?.email;
return ( return (
<div className="flex w-full items-center justify-between font-semibold "> <div className="flex flex-col gap-4 sticky top-0 z-10">
<div className="flex items-center gap-x-2"> <div className="h-1.5 rounded-t-lg w-full bg-custom-background-100 overflow-hidden cursor-pointer">
<Image src={BluePlaneLogoWithoutText} height={30} width={30} alt="Plane Logo" className="mr-3" /> <Tooltip tooltipContent={`${currentStepNumber}/${totalSteps}`} position="bottom-right">
<OnboardingStepIndicator currentStep={currentStep} totalSteps={totalSteps} /> <div
className="h-full bg-custom-primary-100 transition-all duration-700 ease-out"
style={{ width: `${(currentStepNumber / totalSteps) * 100}%` }}
/>
</Tooltip>
</div>
<div className={cn("flex items-center justify-between gap-6 w-full px-6", canGoBack && "pl-4 pr-6")}>
<div className="flex items-center gap-2.5">
{canGoBack && (
<button onClick={handleStepBack} className="cursor-pointer" type="button" disabled={!canGoBack}>
<ChevronLeft className="size-6 text-custom-text-400" />
</button>
)}
<PlaneLockup height={20} width={95} className="text-custom-text-100" />
</div>
<SwitchAccountDropdown fullName={userName} />
</div> </div>
</div> </div>
); );
}; });

View File

@@ -8,3 +8,4 @@ export * from "./step-indicator";
export * from "./switch-account-dropdown"; export * from "./switch-account-dropdown";
export * from "./switch-account-modal"; export * from "./switch-account-modal";
export * from "./header"; export * from "./header";
export * from "./root";

View File

@@ -75,8 +75,8 @@ export const Invitations: React.FC<Props> = (props) => {
return invitations && invitations.length > 0 ? ( return invitations && invitations.length > 0 ? (
<div className="space-y-4"> <div className="space-y-4">
<div className="text-center space-y-1 py-4 mx-auto"> <div className="text-center space-y-1 py-4 mx-auto">
<h3 className="text-3xl font-bold text-onboarding-text-100">You are invited!</h3> <h3 className="text-3xl font-bold text-custom-text-100">You are invited!</h3>
<p className="font-medium text-onboarding-text-400">Accept the invites to collaborate with your team.</p> <p className="font-medium text-custom-text-400">Accept the invites to collaborate with your team.</p>
</div> </div>
<div> <div>
{invitations && {invitations &&
@@ -87,7 +87,7 @@ export const Invitations: React.FC<Props> = (props) => {
return ( return (
<div <div
key={invitation.id} key={invitation.id}
className={`flex cursor-pointer items-center gap-2 rounded border p-3.5 border-custom-border-200 hover:bg-onboarding-background-300/30`} className={`flex cursor-pointer items-center gap-2 rounded border p-3.5 border-custom-border-200 hover:bg-custom-background-90`}
onClick={() => handleInvitation(invitation, isSelected ? "withdraw" : "accepted")} onClick={() => handleInvitation(invitation, isSelected ? "withdraw" : "accepted")}
> >
<div className="flex-shrink-0"> <div className="flex-shrink-0">
@@ -119,9 +119,9 @@ export const Invitations: React.FC<Props> = (props) => {
{isJoiningWorkspaces ? <Spinner height="20px" width="20px" /> : "Continue to workspace"} {isJoiningWorkspaces ? <Spinner height="20px" width="20px" /> : "Continue to workspace"}
</Button> </Button>
<div className="mx-auto mt-4 flex items-center sm:w-96"> <div className="mx-auto mt-4 flex items-center sm:w-96">
<hr className="w-full border-onboarding-border-100" /> <hr className="w-full border-custom-border-300" />
<p className="mx-3 flex-shrink-0 text-center text-sm text-onboarding-text-400">or</p> <p className="mx-3 flex-shrink-0 text-center text-sm text-custom-text-400">or</p>
<hr className="w-full border-onboarding-border-100" /> <hr className="w-full border-custom-border-300" />
</div> </div>
<Button <Button
variant="link-neutral" variant="link-neutral"

View File

@@ -2,8 +2,6 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import Image from "next/image";
import { useTheme } from "next-themes";
import { import {
Control, Control,
Controller, Controller,
@@ -32,11 +30,7 @@ import { Button, Input, Spinner, TOAST_TYPE, setToast } from "@plane/ui";
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper"; import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
// services // services
import { WorkspaceService } from "@/plane-web/services"; import { WorkspaceService } from "@/plane-web/services";
// assets
import InviteMembersDark from "@/public/onboarding/invite-members-dark.webp";
import InviteMembersLight from "@/public/onboarding/invite-members-light.webp";
// components // components
import { OnboardingHeader } from "./header";
import { SwitchAccountDropdown } from "./switch-account-dropdown"; import { SwitchAccountDropdown } from "./switch-account-dropdown";
type Props = { type Props = {
@@ -169,7 +163,7 @@ const InviteMemberInput: React.FC<InviteMemberFormProps> = observer((props) => {
ref={ref} ref={ref}
hasError={Boolean(errors.emails?.[index]?.email)} hasError={Boolean(errors.emails?.[index]?.email)}
placeholder={placeholderEmails[index % placeholderEmails.length]} placeholder={placeholderEmails[index % placeholderEmails.length]}
className="w-full border-onboarding-border-100 text-xs placeholder:text-onboarding-text-400 sm:text-sm" className="w-full border-custom-border-300 text-xs placeholder:text-custom-text-400 sm:text-sm"
autoComplete="off" autoComplete="off"
/> />
)} )}
@@ -193,13 +187,11 @@ const InviteMemberInput: React.FC<InviteMemberFormProps> = observer((props) => {
<Listbox.Button <Listbox.Button
type="button" type="button"
ref={setReferenceElement} ref={setReferenceElement}
className="flex w-full items-center justify-between gap-1 rounded-md px-2.5 py-2 text-sm border-[0.5px] border-onboarding-border-100" className="flex w-full items-center justify-between gap-1 rounded-md px-2.5 py-2 text-sm border-[0.5px] border-custom-border-300"
> >
<span <span
className={`text-sm ${ className={`text-sm ${
!getValues(`emails.${index}.role_active`) !getValues(`emails.${index}.role_active`) ? "text-custom-text-400" : "text-custom-text-100"
? "text-onboarding-text-400"
: "text-onboarding-text-100"
} sm:text-sm`} } sm:text-sm`}
> >
{ROLE[value]} {ROLE[value]}
@@ -216,7 +208,7 @@ const InviteMemberInput: React.FC<InviteMemberFormProps> = observer((props) => {
<Listbox.Options as="div"> <Listbox.Options as="div">
<div <div
className="p-2 absolute space-y-1 z-10 mt-1 h-fit w-48 sm:w-60 rounded-md border border-onboarding-border-100 bg-onboarding-background-200 shadow-sm focus:outline-none" className="p-2 absolute space-y-1 z-10 mt-1 h-fit w-48 sm:w-60 rounded-md border border-custom-border-300 bg-custom-background-100 shadow-sm focus:outline-none"
ref={setPopperElement} ref={setPopperElement}
style={styles.popper} style={styles.popper}
{...attributes.popper} {...attributes.popper}
@@ -229,7 +221,7 @@ const InviteMemberInput: React.FC<InviteMemberFormProps> = observer((props) => {
className={({ active, selected }) => className={({ active, selected }) =>
`cursor-pointer select-none truncate rounded px-1 py-1.5 ${ `cursor-pointer select-none truncate rounded px-1 py-1.5 ${
active || selected ? "bg-onboarding-background-400/40" : "" active || selected ? "bg-onboarding-background-400/40" : ""
} ${selected ? "text-onboarding-text-100" : "text-custom-text-200"}` } ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
} }
> >
{({ selected }) => ( {({ selected }) => (
@@ -274,8 +266,6 @@ export const InviteMembers: React.FC<Props> = (props) => {
const [isInvitationDisabled, setIsInvitationDisabled] = useState(true); const [isInvitationDisabled, setIsInvitationDisabled] = useState(true);
const { resolvedTheme } = useTheme();
const { const {
control, control,
watch, watch,
@@ -360,17 +350,10 @@ export const InviteMembers: React.FC<Props> = (props) => {
return ( return (
<div className="flex w-full h-full"> <div className="flex w-full h-full">
<div className="w-full h-full overflow-auto px-6 py-10 sm:px-7 sm:py-14 md:px-14 lg:px-28"> <div className="w-full h-full overflow-auto px-6 py-10 sm:px-7 sm:py-14 md:px-14 lg:px-28">
<div className="flex items-center justify-between">
{/* Since this will always be the last step */}
<OnboardingHeader currentStep={totalSteps} totalSteps={totalSteps} />
<div className="shrink-0 lg:hidden">
<SwitchAccountDropdown />
</div>
</div>
<div className="flex flex-col w-full items-center justify-center p-8 mt-6 md:w-4/5 mx-auto"> <div className="flex flex-col w-full items-center justify-center p-8 mt-6 md:w-4/5 mx-auto">
<div className="text-center space-y-1 py-4 mx-auto w-4/5"> <div className="text-center space-y-1 py-4 mx-auto w-4/5">
<h3 className="text-3xl font-bold text-onboarding-text-100">Invite your teammates</h3> <h3 className="text-3xl font-bold text-custom-text-100">Invite your teammates</h3>
<p className="font-medium text-onboarding-text-400"> <p className="font-medium text-custom-text-400">
Work in plane happens best with your team. Invite them now to use Plane to its potential. Work in plane happens best with your team. Invite them now to use Plane to its potential.
</p> </p>
</div> </div>
@@ -383,8 +366,8 @@ export const InviteMembers: React.FC<Props> = (props) => {
> >
<div className="w-full text-sm py-4"> <div className="w-full text-sm py-4">
<div className="group relative grid grid-cols-10 gap-4 mx-8 py-2"> <div className="group relative grid grid-cols-10 gap-4 mx-8 py-2">
<div className="col-span-6 px-1 text-sm text-onboarding-text-200 font-medium">Email</div> <div className="col-span-6 px-1 text-sm text-custom-text-200 font-medium">Email</div>
<div className="col-span-4 px-1 text-sm text-onboarding-text-200 font-medium">Role</div> <div className="col-span-4 px-1 text-sm text-custom-text-200 font-medium">Role</div>
</div> </div>
<div className="mb-3 space-y-3 sm:space-y-4"> <div className="mb-3 space-y-3 sm:space-y-4">
{fields.map((field, index) => ( {fields.map((field, index) => (
@@ -431,16 +414,7 @@ export const InviteMembers: React.FC<Props> = (props) => {
</form> </form>
</div> </div>
</div> </div>
<div className="hidden lg:block relative w-2/5 h-screen overflow-hidden px-6 py-10 sm:px-7 sm:py-14 md:px-14 lg:px-28"> <SwitchAccountDropdown />
<SwitchAccountDropdown />
<div className="absolute inset-0 z-0">
<Image
src={resolvedTheme === "dark" ? InviteMembersDark : InviteMembersLight}
className="h-screen w-auto float-end object-cover"
alt="Profile setup"
/>
</div>
</div>
</div> </div>
); );
}; };

View File

@@ -2,8 +2,6 @@
import React, { useMemo, useState } from "react"; import React, { useMemo, useState } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import Image from "next/image";
import { useTheme } from "next-themes";
import { Controller, useForm } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
import { Eye, EyeOff } from "lucide-react"; import { Eye, EyeOff } from "lucide-react";
import { import {
@@ -20,17 +18,11 @@ import { Button, Input, PasswordStrengthIndicator, Spinner, TOAST_TYPE, setToast
// components // components
import { getFileURL, getPasswordStrength } from "@plane/utils"; import { getFileURL, getPasswordStrength } from "@plane/utils";
import { UserImageUploadModal } from "@/components/core"; import { UserImageUploadModal } from "@/components/core";
import { OnboardingHeader, SwitchAccountDropdown } from "@/components/onboarding";
// constants // constants
// helpers // helpers
// hooks // hooks
import { captureError, captureSuccess, captureView } from "@/helpers/event-tracker.helper"; import { captureError, captureSuccess, captureView } from "@/helpers/event-tracker.helper";
import { useUser, useUserProfile } from "@/hooks/store"; import { useUser, useUserProfile } from "@/hooks/store";
// assets
import ProfileSetupDark from "@/public/onboarding/profile-setup-dark.webp";
import ProfileSetupLight from "@/public/onboarding/profile-setup-light.webp";
import UserPersonalizationDark from "@/public/onboarding/user-personalization-dark.webp";
import UserPersonalizationLight from "@/public/onboarding/user-personalization-light.webp";
// services // services
import { AuthService } from "@/services/auth.service"; import { AuthService } from "@/services/auth.service";
@@ -98,8 +90,6 @@ export const ProfileSetup: React.FC<Props> = observer((props) => {
}); });
// plane hooks // plane hooks
const { t } = useTranslation(); const { t } = useTranslation();
// hooks
const { resolvedTheme } = useTheme();
// store hooks // store hooks
const { updateCurrentUser } = useUser(); const { updateCurrentUser } = useUser();
const { updateUserProfile } = useUserProfile(); const { updateUserProfile } = useUserProfile();
@@ -298,330 +288,287 @@ export const ProfileSetup: React.FC<Props> = observer((props) => {
const isButtonDisabled = const isButtonDisabled =
!isSubmitting && isValid ? (isPasswordAlreadySetup ? false : isValidPassword ? false : true) : true; !isSubmitting && isValid ? (isPasswordAlreadySetup ? false : isValidPassword ? false : true) : true;
const isCurrentStepUserPersonalization = profileSetupStep === EProfileSetupSteps.USER_PERSONALIZATION;
return ( return (
<div className="flex h-full w-full"> <div className="flex h-full w-full">
<div className="w-full h-full overflow-auto px-6 py-10 sm:px-7 sm:py-14 md:px-14 lg:px-28"> <div className="flex flex-col w-full items-center justify-center p-8 mt-6">
<div className="flex items-center justify-between"> <form onSubmit={handleSubmit(onSubmit)} className="w-full mx-auto mt-2 space-y-4 sm:w-96">
<OnboardingHeader currentStep={isCurrentStepUserPersonalization ? 2 : 1} totalSteps={totalSteps} /> {profileSetupStep !== EProfileSetupSteps.USER_PERSONALIZATION && (
<div className="shrink-0 lg:hidden"> <>
<SwitchAccountDropdown fullName={`${watch("first_name")} ${watch("last_name")}`} /> <Controller
</div> control={control}
</div> name="avatar_url"
<div className="flex flex-col w-full items-center justify-center p-8 mt-6"> render={({ field: { onChange, value } }) => (
<div className="text-center space-y-1 py-4 mx-auto"> <UserImageUploadModal
<h3 className="text-3xl font-bold text-onboarding-text-100"> isOpen={isImageUploadModalOpen}
{isCurrentStepUserPersonalization onClose={() => setIsImageUploadModalOpen(false)}
? `Looking good${user?.first_name && `, ${user.first_name}`}!` handleRemove={async () => handleDelete(getValues("avatar_url"))}
: "Welcome to Plane!"} onSuccess={(url) => {
</h3> onChange(url);
<p className="font-medium text-onboarding-text-400"> setIsImageUploadModalOpen(false);
{isCurrentStepUserPersonalization }}
? "Lets personalize Plane for you." value={value && value.trim() !== "" ? value : null}
: "Lets setup your profile, tell us a bit about yourself."} />
</p> )}
</div> />
<form onSubmit={handleSubmit(onSubmit)} className="w-full mx-auto mt-2 space-y-4 sm:w-96"> <div className="space-y-1 flex items-center justify-center">
{profileSetupStep !== EProfileSetupSteps.USER_PERSONALIZATION && ( <button type="button" onClick={() => setIsImageUploadModalOpen(true)}>
<> {!userAvatar || userAvatar === "" ? (
<div className="flex flex-col items-center justify-between">
<div className="relative h-14 w-14 overflow-hidden">
<div className="absolute left-0 top-0 flex items-center justify-center h-full w-full rounded-full text-white text-3xl font-medium bg-[#9747FF] uppercase">
{watch("first_name")[0] ?? "R"}
</div>
</div>
<div className="pt-1 text-sm font-medium text-custom-primary-300 hover:text-custom-primary-400">
Choose image
</div>
</div>
) : (
<div className="relative mr-3 h-16 w-16 overflow-hidden">
<img
src={getFileURL(userAvatar ?? "")}
className="absolute left-0 top-0 h-full w-full rounded-full object-cover"
onClick={() => setIsImageUploadModalOpen(true)}
alt={user?.display_name}
/>
</div>
)}
</button>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div className="space-y-1">
<label
className="text-sm text-custom-text-300 font-medium after:content-['*'] after:ml-0.5 after:text-red-500"
htmlFor="first_name"
>
First name
</label>
<Controller
control={control}
name="first_name"
rules={{
required: "First name is required",
maxLength: {
value: 24,
message: "First name must be within 24 characters.",
},
}}
render={({ field: { value, onChange, ref } }) => (
<Input
id="first_name"
name="first_name"
type="text"
value={value}
autoFocus
onChange={onChange}
ref={ref}
hasError={Boolean(errors.first_name)}
placeholder="Wilbur"
className="w-full border-custom-border-300"
autoComplete="on"
/>
)}
/>
{errors.first_name && <span className="text-sm text-red-500">{errors.first_name.message}</span>}
</div>
<div className="space-y-1">
<label
className="text-sm text-custom-text-300 font-medium after:content-['*'] after:ml-0.5 after:text-red-500"
htmlFor="last_name"
>
Last name
</label>
<Controller
control={control}
name="last_name"
rules={{
required: "Last name is required",
maxLength: {
value: 24,
message: "Last name must be within 24 characters.",
},
}}
render={({ field: { value, onChange, ref } }) => (
<Input
id="last_name"
name="last_name"
type="text"
value={value}
onChange={onChange}
ref={ref}
hasError={Boolean(errors.last_name)}
placeholder="Wright"
className="w-full border-custom-border-300"
autoComplete="on"
/>
)}
/>
{errors.last_name && <span className="text-sm text-red-500">{errors.last_name.message}</span>}
</div>
</div>
{/* setting up password for the first time */}
{!isPasswordAlreadySetup && (
<>
<div className="space-y-1">
<label className="text-sm text-custom-text-300 font-medium" htmlFor="password">
Set a password ({t("common.optional")})
</label>
<Controller
control={control}
name="password"
rules={{
required: false,
}}
render={({ field: { value, onChange, ref } }) => (
<div className="relative flex items-center rounded-md">
<Input
type={showPassword.password ? "text" : "password"}
name="password"
value={value}
onChange={onChange}
ref={ref}
hasError={Boolean(errors.password)}
placeholder="New password..."
className="w-full border-[0.5px] border-custom-border-300 pr-12 placeholder:text-custom-text-400"
onFocus={() => setIsPasswordInputFocused(true)}
onBlur={() => setIsPasswordInputFocused(false)}
autoComplete="on"
/>
{showPassword.password ? (
<EyeOff
className="absolute right-3 h-4 w-4 stroke-custom-text-400 hover:cursor-pointer"
onClick={() => handleShowPassword("password")}
/>
) : (
<Eye
className="absolute right-3 h-4 w-4 stroke-custom-text-400 hover:cursor-pointer"
onClick={() => handleShowPassword("password")}
/>
)}
</div>
)}
/>
<PasswordStrengthIndicator password={watch("password") ?? ""} isFocused={isPasswordInputFocused} />
</div>
<div className="space-y-1">
<label className="text-sm text-custom-text-300 font-medium" htmlFor="confirm_password">
{t("auth.common.password.confirm_password.label")} ({t("common.optional")})
</label>
<Controller
control={control}
name="confirm_password"
rules={{
required: watch("password") ? true : false,
validate: (value) =>
watch("password") ? (value === watch("password") ? true : "Passwords don't match") : true,
}}
render={({ field: { value, onChange, ref } }) => (
<div className="relative flex items-center rounded-md">
<Input
type={showPassword.retypePassword ? "text" : "password"}
name="confirm_password"
value={value}
onChange={onChange}
ref={ref}
hasError={Boolean(errors.confirm_password)}
placeholder={t("auth.common.password.confirm_password.placeholder")}
className="w-full border-custom-border-300 pr-12 placeholder:text-custom-text-400"
/>
{showPassword.retypePassword ? (
<EyeOff
className="absolute right-3 h-4 w-4 stroke-custom-text-400 hover:cursor-pointer"
onClick={() => handleShowPassword("retypePassword")}
/>
) : (
<Eye
className="absolute right-3 h-4 w-4 stroke-custom-text-400 hover:cursor-pointer"
onClick={() => handleShowPassword("retypePassword")}
/>
)}
</div>
)}
/>
{errors.confirm_password && (
<span className="text-sm text-red-500">{errors.confirm_password.message}</span>
)}
</div>
</>
)}
</>
)}
{/* user role once the password is set */}
{profileSetupStep !== EProfileSetupSteps.USER_DETAILS && (
<>
<div className="space-y-1">
<label
className="text-sm text-custom-text-300 font-medium after:content-['*'] after:ml-0.5 after:text-red-500"
htmlFor="role"
>
What role are you working on? Choose one.
</label>
<Controller <Controller
control={control} control={control}
name="avatar_url" name="role"
render={({ field: { onChange, value } }) => ( rules={{
<UserImageUploadModal required: "This field is required",
isOpen={isImageUploadModalOpen} }}
onClose={() => setIsImageUploadModalOpen(false)} render={({ field: { value, onChange } }) => (
handleRemove={async () => handleDelete(getValues("avatar_url"))} <div className="flex flex-wrap gap-2 py-2 overflow-auto break-all">
onSuccess={(url) => { {USER_ROLE.map((userRole) => (
onChange(url); <div
setIsImageUploadModalOpen(false); key={userRole}
}} className={`flex-shrink-0 border-[0.5px] hover:cursor-pointer hover:bg-custom-background-90 ${
value={value && value.trim() !== "" ? value : null} value === userRole ? "border-custom-primary-100" : "border-custom-border-300"
/> } rounded px-3 py-1.5 text-sm font-medium`}
onClick={() => onChange(userRole)}
>
{userRole}
</div>
))}
</div>
)} )}
/> />
<div className="space-y-1 flex items-center justify-center"> {errors.role && <span className="text-sm text-red-500">{errors.role.message}</span>}
<button type="button" onClick={() => setIsImageUploadModalOpen(true)}> </div>
{!userAvatar || userAvatar === "" ? ( <div className="space-y-1">
<div className="flex flex-col items-center justify-between"> <label
<div className="relative h-14 w-14 overflow-hidden"> className="text-sm text-custom-text-300 font-medium after:content-['*'] after:ml-0.5 after:text-red-500"
<div className="absolute left-0 top-0 flex items-center justify-center h-full w-full rounded-full text-white text-3xl font-medium bg-[#9747FF] uppercase"> htmlFor="use_case"
{watch("first_name")[0] ?? "R"} >
</div> What is your domain expertise? Choose one.
</label>
<Controller
control={control}
name="use_case"
rules={{
required: "This field is required",
}}
render={({ field: { value, onChange } }) => (
<div className="flex flex-wrap gap-2 py-2 overflow-auto break-all">
{USER_DOMAIN.map((userDomain) => (
<div
key={userDomain}
className={`flex-shrink-0 border-[0.5px] hover:cursor-pointer hover:bg-custom-background-90 ${
value === userDomain ? "border-custom-primary-100" : "border-custom-border-300"
} rounded px-3 py-1.5 text-sm font-medium`}
onClick={() => onChange(userDomain)}
>
{userDomain}
</div> </div>
<div className="pt-1 text-sm font-medium text-custom-primary-300 hover:text-custom-primary-400"> ))}
Choose image
</div>
</div>
) : (
<div className="relative mr-3 h-16 w-16 overflow-hidden">
<img
src={getFileURL(userAvatar ?? "")}
className="absolute left-0 top-0 h-full w-full rounded-full object-cover"
onClick={() => setIsImageUploadModalOpen(true)}
alt={user?.display_name}
/>
</div>
)}
</button>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div className="space-y-1">
<label
className="text-sm text-onboarding-text-300 font-medium after:content-['*'] after:ml-0.5 after:text-red-500"
htmlFor="first_name"
>
First name
</label>
<Controller
control={control}
name="first_name"
rules={{
required: "First name is required",
maxLength: {
value: 24,
message: "First name must be within 24 characters.",
},
}}
render={({ field: { value, onChange, ref } }) => (
<Input
id="first_name"
name="first_name"
type="text"
value={value}
autoFocus
onChange={onChange}
ref={ref}
hasError={Boolean(errors.first_name)}
placeholder="Wilbur"
className="w-full border-onboarding-border-100"
autoComplete="on"
/>
)}
/>
{errors.first_name && <span className="text-sm text-red-500">{errors.first_name.message}</span>}
</div>
<div className="space-y-1">
<label
className="text-sm text-onboarding-text-300 font-medium after:content-['*'] after:ml-0.5 after:text-red-500"
htmlFor="last_name"
>
Last name
</label>
<Controller
control={control}
name="last_name"
rules={{
required: "Last name is required",
maxLength: {
value: 24,
message: "Last name must be within 24 characters.",
},
}}
render={({ field: { value, onChange, ref } }) => (
<Input
id="last_name"
name="last_name"
type="text"
value={value}
onChange={onChange}
ref={ref}
hasError={Boolean(errors.last_name)}
placeholder="Wright"
className="w-full border-onboarding-border-100"
autoComplete="on"
/>
)}
/>
{errors.last_name && <span className="text-sm text-red-500">{errors.last_name.message}</span>}
</div>
</div>
{/* setting up password for the first time */}
{!isPasswordAlreadySetup && (
<>
<div className="space-y-1">
<label className="text-sm text-onboarding-text-300 font-medium" htmlFor="password">
Set a password ({t("common.optional")})
</label>
<Controller
control={control}
name="password"
rules={{
required: false,
}}
render={({ field: { value, onChange, ref } }) => (
<div className="relative flex items-center rounded-md">
<Input
type={showPassword.password ? "text" : "password"}
name="password"
value={value}
onChange={onChange}
ref={ref}
hasError={Boolean(errors.password)}
placeholder="New password..."
className="w-full border-[0.5px] border-onboarding-border-100 pr-12 placeholder:text-onboarding-text-400"
onFocus={() => setIsPasswordInputFocused(true)}
onBlur={() => setIsPasswordInputFocused(false)}
autoComplete="on"
/>
{showPassword.password ? (
<EyeOff
className="absolute right-3 h-4 w-4 stroke-custom-text-400 hover:cursor-pointer"
onClick={() => handleShowPassword("password")}
/>
) : (
<Eye
className="absolute right-3 h-4 w-4 stroke-custom-text-400 hover:cursor-pointer"
onClick={() => handleShowPassword("password")}
/>
)}
</div>
)}
/>
<PasswordStrengthIndicator
password={watch("password") ?? ""}
isFocused={isPasswordInputFocused}
/>
</div> </div>
<div className="space-y-1"> )}
<label className="text-sm text-onboarding-text-300 font-medium" htmlFor="confirm_password"> />
{t("auth.common.password.confirm_password.label")} ({t("common.optional")}) {errors.use_case && <span className="text-sm text-red-500">{errors.use_case.message}</span>}
</label> </div>
<Controller </>
control={control}
name="confirm_password"
rules={{
required: watch("password") ? true : false,
validate: (value) =>
watch("password") ? (value === watch("password") ? true : "Passwords don't match") : true,
}}
render={({ field: { value, onChange, ref } }) => (
<div className="relative flex items-center rounded-md">
<Input
type={showPassword.retypePassword ? "text" : "password"}
name="confirm_password"
value={value}
onChange={onChange}
ref={ref}
hasError={Boolean(errors.confirm_password)}
placeholder={t("auth.common.password.confirm_password.placeholder")}
className="w-full border-onboarding-border-100 pr-12 placeholder:text-onboarding-text-400"
/>
{showPassword.retypePassword ? (
<EyeOff
className="absolute right-3 h-4 w-4 stroke-custom-text-400 hover:cursor-pointer"
onClick={() => handleShowPassword("retypePassword")}
/>
) : (
<Eye
className="absolute right-3 h-4 w-4 stroke-custom-text-400 hover:cursor-pointer"
onClick={() => handleShowPassword("retypePassword")}
/>
)}
</div>
)}
/>
{errors.confirm_password && (
<span className="text-sm text-red-500">{errors.confirm_password.message}</span>
)}
</div>
</>
)}
</>
)}
{/* user role once the password is set */}
{profileSetupStep !== EProfileSetupSteps.USER_DETAILS && (
<>
<div className="space-y-1">
<label
className="text-sm text-onboarding-text-300 font-medium after:content-['*'] after:ml-0.5 after:text-red-500"
htmlFor="role"
>
What role are you working on? Choose one.
</label>
<Controller
control={control}
name="role"
rules={{
required: "This field is required",
}}
render={({ field: { value, onChange } }) => (
<div className="flex flex-wrap gap-2 py-2 overflow-auto break-all">
{USER_ROLE.map((userRole) => (
<div
key={userRole}
className={`flex-shrink-0 border-[0.5px] hover:cursor-pointer hover:bg-onboarding-background-300/30 ${
value === userRole ? "border-custom-primary-100" : "border-onboarding-border-100"
} rounded px-3 py-1.5 text-sm font-medium`}
onClick={() => onChange(userRole)}
>
{userRole}
</div>
))}
</div>
)}
/>
{errors.role && <span className="text-sm text-red-500">{errors.role.message}</span>}
</div>
<div className="space-y-1">
<label
className="text-sm text-onboarding-text-300 font-medium after:content-['*'] after:ml-0.5 after:text-red-500"
htmlFor="use_case"
>
What is your domain expertise? Choose one.
</label>
<Controller
control={control}
name="use_case"
rules={{
required: "This field is required",
}}
render={({ field: { value, onChange } }) => (
<div className="flex flex-wrap gap-2 py-2 overflow-auto break-all">
{USER_DOMAIN.map((userDomain) => (
<div
key={userDomain}
className={`flex-shrink-0 border-[0.5px] hover:cursor-pointer hover:bg-onboarding-background-300/30 ${
value === userDomain ? "border-custom-primary-100" : "border-onboarding-border-100"
} rounded px-3 py-1.5 text-sm font-medium`}
onClick={() => onChange(userDomain)}
>
{userDomain}
</div>
))}
</div>
)}
/>
{errors.use_case && <span className="text-sm text-red-500">{errors.use_case.message}</span>}
</div>
</>
)}
<Button variant="primary" type="submit" size="lg" className="w-full" disabled={isButtonDisabled}>
{isSubmitting ? <Spinner height="20px" width="20px" /> : "Continue"}
</Button>
</form>
</div>
</div>
<div className="hidden lg:block relative w-2/5 h-screen overflow-hidden px-6 py-10 sm:px-7 sm:py-14 md:px-14 lg:px-28">
<SwitchAccountDropdown fullName={`${watch("first_name")} ${watch("last_name")}`} />
<div className="absolute inset-0 z-0">
{profileSetupStep === EProfileSetupSteps.USER_PERSONALIZATION ? (
<Image
src={resolvedTheme === "dark" ? UserPersonalizationDark : UserPersonalizationLight}
className="h-screen w-auto float-end object-cover"
alt="User Personalization"
/>
) : (
<Image
src={resolvedTheme === "dark" ? ProfileSetupDark : ProfileSetupLight}
className="h-screen w-auto float-end object-cover"
alt="Profile setup"
/>
)} )}
</div> <Button variant="primary" type="submit" size="lg" className="w-full" disabled={isButtonDisabled}>
{isSubmitting ? <Spinner height="20px" width="20px" /> : "Continue"}
</Button>
</form>
</div> </div>
</div> </div>
); );

View File

@@ -0,0 +1,146 @@
"use client";
import { FC, useCallback, useEffect, useState } from "react";
import { observer } from "mobx-react";
// plane imports
import { USER_TRACKER_EVENTS } from "@plane/constants";
import {
EOnboardingSteps,
IWorkspaceMemberInvitation,
TOnboardingStep,
TOnboardingSteps,
TUserProfile,
} from "@plane/types";
import { setToast, TOAST_TYPE } from "@plane/ui";
// helpers
import { captureSuccess } from "@/helpers/event-tracker.helper";
// hooks
import { useUser, useUserProfile, useWorkspace } from "@/hooks/store";
// local components
import { OnboardingHeader } from "./header";
import { OnboardingStepRoot } from "./steps";
type Props = {
invitations?: IWorkspaceMemberInvitation[];
};
export const OnboardingRoot: FC<Props> = observer(({ invitations = [] }) => {
const [currentStep, setCurrentStep] = useState<TOnboardingStep>(EOnboardingSteps.PROFILE_SETUP);
// store hooks
const { data: user } = useUser();
const { data: userProfile, updateUserProfile, finishUserOnboarding } = useUserProfile();
const { workspaces } = useWorkspace();
const workspacesList = Object.values(workspaces ?? {});
// Calculate total steps based on whether invitations are available
const hasInvitations = invitations.length > 0;
// complete onboarding
const finishOnboarding = useCallback(async () => {
if (!user) return;
await finishUserOnboarding()
.then(() => {
captureSuccess({
eventName: USER_TRACKER_EVENTS.onboarding_complete,
payload: {
email: user.email,
user_id: user.id,
},
});
})
.catch(() => {
setToast({
type: TOAST_TYPE.ERROR,
title: "Failed",
message: "Failed to finish onboarding, Please try again later.",
});
});
}, [user, finishUserOnboarding]);
// handle step change
const stepChange = useCallback(
async (steps: Partial<TOnboardingSteps>) => {
if (!user) return;
const payload: Partial<TUserProfile> = {
onboarding_step: {
...userProfile.onboarding_step,
...steps,
},
};
await updateUserProfile(payload);
},
[user, userProfile, updateUserProfile]
);
const handleStepChange = useCallback(
(step: EOnboardingSteps, skipInvites?: boolean) => {
switch (step) {
case EOnboardingSteps.PROFILE_SETUP:
setCurrentStep(EOnboardingSteps.ROLE_SETUP);
break;
case EOnboardingSteps.ROLE_SETUP:
setCurrentStep(EOnboardingSteps.USE_CASE_SETUP);
break;
case EOnboardingSteps.USE_CASE_SETUP:
stepChange({ profile_complete: true });
if (workspacesList.length > 0) finishOnboarding();
else setCurrentStep(EOnboardingSteps.WORKSPACE_CREATE_OR_JOIN);
break;
case EOnboardingSteps.WORKSPACE_CREATE_OR_JOIN:
if (skipInvites) finishOnboarding();
else {
setCurrentStep(EOnboardingSteps.INVITE_MEMBERS);
stepChange({ workspace_create: true });
}
break;
case EOnboardingSteps.INVITE_MEMBERS:
stepChange({ workspace_invite: true });
finishOnboarding();
break;
}
},
[stepChange, finishOnboarding, workspacesList]
);
const updateCurrentStep = (step: EOnboardingSteps) => setCurrentStep(step);
useEffect(() => {
const handleInitialStep = () => {
if (
userProfile?.onboarding_step?.profile_complete &&
!userProfile?.onboarding_step?.workspace_create &&
!userProfile?.onboarding_step?.workspace_join
) {
setCurrentStep(EOnboardingSteps.WORKSPACE_CREATE_OR_JOIN);
}
if (
userProfile?.onboarding_step?.profile_complete &&
userProfile?.onboarding_step?.workspace_create &&
!userProfile?.onboarding_step?.workspace_invite
) {
setCurrentStep(EOnboardingSteps.INVITE_MEMBERS);
}
};
handleInitialStep();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<>
{/* Header with progress */}
<OnboardingHeader
currentStep={currentStep}
updateCurrentStep={updateCurrentStep}
hasInvitations={hasInvitations}
/>
{/* Main content area */}
<OnboardingStepRoot currentStep={currentStep} invitations={invitations} handleStepChange={handleStepChange} />
</>
);
});

View File

@@ -22,7 +22,7 @@ export const OnboardingStepIndicator: React.FC<OnboardingStepIndicatorProps> = (
key={`line-${i}`} key={`line-${i}`}
className={cn("h-1.5 -ml-0.5 w-full", { className={cn("h-1.5 -ml-0.5 w-full", {
"bg-green-700": isCompleted, "bg-green-700": isCompleted,
"bg-onboarding-background-100": !isCompleted, "bg-custom-background-100": !isCompleted,
"rounded-l-full": isFirstStep, "rounded-l-full": isFirstStep,
"rounded-r-full": isLastStep || isActive, "rounded-r-full": isLastStep || isActive,
"z-10": isActive, "z-10": isActive,
@@ -36,7 +36,7 @@ export const OnboardingStepIndicator: React.FC<OnboardingStepIndicatorProps> = (
return ( return (
<div className="flex flex-col justify-center"> <div className="flex flex-col justify-center">
<div className="text-sm text-onboarding-text-300 font-medium"> <div className="text-sm text-custom-text-300 font-medium">
{currentStep} of {totalSteps} steps {currentStep} of {totalSteps} steps
</div> </div>
<div className="flex items-center justify-center my-0.5 mx-1 w-40 lg:w-52">{renderIndicators()}</div> <div className="flex items-center justify-center my-0.5 mx-1 w-40 lg:w-52">{renderIndicators()}</div>

View File

@@ -0,0 +1,15 @@
"use client";
import { FC } from "react";
type Props = {
title: string;
description: string;
};
export const CommonOnboardingHeader: FC<Props> = ({ title, description }) => (
<div className="text-left space-y-2">
<h1 className="text-2xl font-semibold text-custom-text-200">{title}</h1>
<p className="text-base text-custom-text-300">{description}</p>
</div>
);

Some files were not shown because too many files have changed in this diff Show More