From 3eafd746445f18065d72513072b34e01f0fe4501 Mon Sep 17 00:00:00 2001 From: Carlos Date: Fri, 8 Nov 2024 20:43:09 +0100 Subject: [PATCH 1/2] waitlist page hosting --- pcweb/components/docpage/navbar/navbar.py | 12 +- pcweb/components/docpage/navbar/search.py | 2 +- pcweb/components/hosting_banner.py | 34 ++ pcweb/pages/__init__.py | 1 + pcweb/pages/hosting_countdown/animated_box.py | 300 ++++++++++++++++++ .../hosting_countdown/hosting_countdown.py | 37 +++ .../hosting_countdown/semicircle_timer.py | 120 +++++++ pcweb/pages/hosting_countdown/timer.py | 15 + pcweb/pages/hosting_countdown/waitlist.py | 60 ++++ pcweb/styles/tailwind_config.py | 74 ++++- 10 files changed, 649 insertions(+), 6 deletions(-) create mode 100644 pcweb/components/hosting_banner.py create mode 100644 pcweb/pages/hosting_countdown/animated_box.py create mode 100644 pcweb/pages/hosting_countdown/hosting_countdown.py create mode 100644 pcweb/pages/hosting_countdown/semicircle_timer.py create mode 100644 pcweb/pages/hosting_countdown/timer.py create mode 100644 pcweb/pages/hosting_countdown/waitlist.py diff --git a/pcweb/components/docpage/navbar/navbar.py b/pcweb/components/docpage/navbar/navbar.py index f834342555..1e0d331dbd 100644 --- a/pcweb/components/docpage/navbar/navbar.py +++ b/pcweb/components/docpage/navbar/navbar.py @@ -27,7 +27,7 @@ from pcweb.pages.blog import blogs from pcweb.pages.changelog import changelog from pcweb.pages.gallery import gallery - +from pcweb.components.hosting_banner import hosting_banner from pcweb.pages.blog.paths import blog_data from pcweb.components.docpage.navbar.navmenu.navmenu import nav_menu @@ -368,7 +368,11 @@ def new_component_section() -> rx.Component: def navbar() -> rx.Component: - return rx.el.header( - new_component_section(), - class_name="top-0 z-[9999] fixed flex flex-row items-center gap-12 bg-slate-1 shadow-[inset_0_-1px_0_0_var(--c-slate-3)] px-4 lg:px-6 w-screen h-[48px] lg:h-[65px]", + return rx.box( + hosting_banner(), + rx.el.header( + new_component_section(), + class_name="flex flex-row items-center gap-12 bg-slate-1 shadow-[inset_0_-1px_0_0_var(--c-slate-3)] px-4 lg:px-6 w-screen h-[48px] lg:h-[65px]", + ), + class_name="flex flex-col w-full top-0 z-[9999] fixed", ) diff --git a/pcweb/components/docpage/navbar/search.py b/pcweb/components/docpage/navbar/search.py index 18db7cfa74..dee3460cb9 100644 --- a/pcweb/components/docpage/navbar/search.py +++ b/pcweb/components/docpage/navbar/search.py @@ -3,6 +3,6 @@ import reflex as rx from .inkeep import inkeep - +@rx.memo def search_bar() -> rx.Component: return rx.box(inkeep(), class_name="w-full h-full min-w-0 max-w-[256px] max-h-[32px]") \ No newline at end of file diff --git a/pcweb/components/hosting_banner.py b/pcweb/components/hosting_banner.py new file mode 100644 index 0000000000..81c3d1199a --- /dev/null +++ b/pcweb/components/hosting_banner.py @@ -0,0 +1,34 @@ +import reflex as rx + + +def glow() -> rx.Component: + return rx.box( + class_name="absolute w-[120rem] h-[23.75rem] flex-shrink-0 rounded-[120rem] left-1/2 -translate-x-1/2 z-[0] top-[-16rem]", + background_image=rx.color_mode_cond( + "radial-gradient(50% 50% at 50% 50%, rgba(235, 228, 255, 0.80) 0%, rgba(252, 252, 253, 0.00) 100%)", + "radial-gradient(50% 50% at 50% 50%, rgba(38, 25, 88, 1) 0%, rgba(21, 22, 24, 0.00) 100%)", + ), + ) + + +def hosting_banner() -> rx.Component: + return rx.box( + rx.text( + "Meet Hosting. ", + rx.el.span( + "Deploy your app with a single command. Performant, secure, and scalable.", + class_name="text-slate-12 font-medium text-sm", + ), + class_name="text-slate-12 font-semibold text-sm z-[1]", + ), + rx.link( + rx.el.button( + "Learn More", + class_name="text-slate-12 h-[1.75rem] rounded-md bg-slate-4 px-1.5 text-sm font-semibold", + ), + href="/hosting", + class_name="z-[1] shrink-0", + ), + glow(), + class_name="px-4 lg:px-6 w-screen h-auto lg:h-[65px] shadow-[inset_0_-1px_0_0_var(--c-slate-3)] flex items-center justify-center bg-slate-1 flex-row gap-4 overflow-hidden relative lg:py-0 py-2 max-w-full", + ) diff --git a/pcweb/pages/__init__.py b/pcweb/pages/__init__.py index 781bcf2a44..13b2ba0a58 100644 --- a/pcweb/pages/__init__.py +++ b/pcweb/pages/__init__.py @@ -12,6 +12,7 @@ from .customers.landing import customers from .customers.data.customers import customers_routes from .gallery.apps import gallery_apps_routes +from .hosting_countdown.hosting_countdown import hosting_countdown routes = [ *[r for r in locals().values() if isinstance(r, Route) and r.add_as_page], diff --git a/pcweb/pages/hosting_countdown/animated_box.py b/pcweb/pages/hosting_countdown/animated_box.py new file mode 100644 index 0000000000..daceb75683 --- /dev/null +++ b/pcweb/pages/hosting_countdown/animated_box.py @@ -0,0 +1,300 @@ +import reflex as rx + + +def typing_text_script(): + return """ + var txt = 'reflex deploy'; + var speed = 50; + var isAnimating = false; // Add flag to track animation state + + function typeWriter(element, i) { + if (isAnimating) return; // Don't start if animation is running + + if (i < txt.length) { + element.innerHTML += txt.charAt(i); + i++; + setTimeout(function() { typeWriter(element, i); }, speed); + } else { + startDeployAnimation(); + } + } + + function startDeployAnimation() { + try { + if (isAnimating) return; // Don't start if animation is running + isAnimating = true; // Set flag when animation starts + + // Hide typing cursor and show "Deploying..." + document.querySelector('.typing-square').classList.replace('opacity-100', 'opacity-0'); + document.querySelector('.terminal-box').classList.add('expanded-height'); + document.querySelector('.deploying-text').classList.replace('hidden', 'flex'); + + // After 800ms, collapse terminal and show deploy box + setTimeout(function() { + try { + document.querySelector('.terminal-box').classList.add('collapsed'); + document.querySelector('.deploy-box').classList.add('expanded'); + + // After 2.5s, switch badge from loading to ready + setTimeout(function() { + try { + document.querySelector('.loading-badge').classList.add('hidden'); + document.querySelector('.ready-badge').classList.remove('hidden', 'opacity-0'); + + // After 3s with "Ready" showing, start unexpanding deploy box and reset terminal simultaneously + setTimeout(function() { + try { + document.querySelectorAll('.reflex-deploy').forEach(function(element) { + element.innerHTML = ''; + }); + document.querySelector('.deploy-box').classList.remove('expanded'); + document.querySelector('.terminal-box').classList.remove('expanded-height', 'collapsed'); + document.querySelector('.deploying-text').classList.replace('flex', 'hidden'); + document.querySelector('.typing-square').classList.replace('opacity-0', 'opacity-100'); + + // Reset badge after deploy box transition + setTimeout(function() { + try { + resetBadge(); + } catch (error) { + isAnimating = false; // Reset flag on error + } + }, 550); + + // Wait for transitions to complete + extra delay before starting new typing + setTimeout(function() { + try { + isAnimating = false; // Reset flag when animation completes + document.querySelectorAll('.reflex-deploy').forEach(function(element) { + typeWriter(element, 0); + }); + } catch (error) { + isAnimating = false; // Reset flag on error + } + }, 1500); + } catch (error) { + isAnimating = false; // Reset flag on error + } + }, 3000); + } catch (error) { + isAnimating = false; // Reset flag on error + } + }, 2500); + } catch (error) { + isAnimating = false; // Reset flag on error + } + }, 800); + } catch (error) { + isAnimating = false; // Reset flag on error + } + } + + function resetBadge() { + document.querySelector('.ready-badge').classList.add('hidden', 'opacity-0'); + document.querySelector('.loading-badge').classList.remove('hidden'); + } + + // Check if required elements exist before starting animation + function checkElementsExist() { + return document.querySelector('.typing-square') && + document.querySelector('.terminal-box') && + document.querySelector('.deploying-text') && + document.querySelector('.deploy-box') && + document.querySelector('.loading-badge') && + document.querySelector('.ready-badge'); + } + + // Only start initial animation if elements exist and animation is not running + function startInitialAnimation() { + if (!checkElementsExist()) { + setTimeout(startInitialAnimation, 100); // Check again in 100ms + return; + } + + if (!isAnimating) { + document.querySelectorAll('.reflex-deploy').forEach(function(element) { + typeWriter(element, 0); + }); + } + } + + // Start checking for elements after a 1 second delay + setTimeout(startInitialAnimation, 1000); + """ + + +def status_badge(icon: str, text: str, class_name: str = ""): + return rx.box( + # rx.icon(tag=icon, size=10, class_name="!text-[var(--amber-8)] animate-spin"), + ( + rx.html( + """ + + + + + + + + + + + + """, + class_name="animate-spin w-4 h-4 p-[0.1875rem] flex-shrink-0", + ) + if icon == "loader-circle" + else rx.html( + """ + + + + """, + class_name="h-4 w-4 shrink-0", + ) + ), + rx.text(text, class_name="text-xs font-medium text-slate-9"), + class_name=f"flex h-5 p-[0rem_0.375rem_0rem_0.125rem] items-center gap-0.5 rounded-[0.375rem] border border-slate-5 bg-slate-1 {class_name}", + ) + + +def purple_gradient_circle(): + return rx.box( + class_name="w-80 h-80 flex-shrink-0 rounded-[20rem] bg-[radial-gradient(50%_50%_at_50%_50%,_var(--c-violet-3)_0%,_rgba(26,27,29,0.00)_100%)] absolute bottom-[-15.5rem] left-1/2 transform -translate-x-1/2 z-[-1]", + ) + + +def terminal_box(): + return rx.box( + rx.el.style( + """ + .terminal-box.collapsed { + transform: translateY(-16px) scale(0.75); + } + + .terminal-box.expanded-height { + height: 6rem; + } + .deploy-box.expanded { + transform: scale(1); + bottom: 0px; + } + .deploy-box { + transform: scale(0.75); + } + .deploy-box { + bottom: -200px; + } + .typing-square.collapsed { + opacity: 0; + } + """ + ), + rx.box( + # Ellipsis + rx.box( + rx.box(class_name="w-2 h-2 rounded-full bg-[var(--red-9)]"), + rx.box(class_name="w-2 h-2 rounded-full bg-[var(--amber-9)]"), + rx.box(class_name="w-2 h-2 rounded-full bg-[var(--green-9)]"), + class_name="flex flex-row gap-1 items-center", + ), + # Terminal text + rx.text( + "terminal", + class_name="text-slate-9 text-xs font-medium text-center leading-4", + ), + class_name="flex p-[0.25rem_8.5rem_0.25rem_0.5rem] items-center gap-[6.0625rem] self-stretch", + ), + # Typing + rx.box( + rx.text( + "$", + class_name="text-slate-9 text-xs font-medium text-center leading-4 font-['JetBrains_Mono'] mr-4 self-baseline", + ), + rx.box( + rx.box( + rx.text( + "", + class_name="text-slate-12 text-xs font-medium text-center leading-4 font-['JetBrains_Mono'] reflex-deploy mr-0.5", + ), + # Square + rx.box( + class_name="w-2 h-4 border-2 border-slate-9 typing-square opacity-100 transition-all duration-200 ease-out" + ), + class_name="flex flex-row", + ), + rx.text( + "Deploying...", + class_name="text-slate-9 text-xs font-medium text-center leading-4 font-['JetBrains_Mono'] deploying-text self-start hidden", + ), + class_name="flex flex-col", + ), + class_name="flex items-center p-5", + ), + purple_gradient_circle(), + class_name="flex w-[20rem] h-[5rem] flex-col flex-shrink-0 rounded-xl rounded-b-none border-t-[1px] border-r-[1px] border-l-[1px] border-slate-4 bg-[light-dark(rgba(249, 249, 251, 0.48), rgba(26,27,29,0.48))] backdrop-filter backdrop-blur-[10px] terminal-box transition-all duration-[550ms] ease-in-out z-[5] origin-bottom translate-y-0 absolute bottom-0 overflow-hidden", + ) + + +def deploy_box(): + return rx.box( + rx.box( + # Gradient circle + rx.box( + class_name="rounded-full h-8 w-8 mr-4 shrink-0", + style={ + "background": "linear-gradient(180deg, #6056CF 0%, #48ACE5 100%)" + }, + ), + rx.box( + rx.text("My App", class_name="text-sm font-medium text-slate-12"), + rx.text( + "my-app.reflex.run", + class_name="text-sm font-medium text-slate-9", + ), + class_name="flex flex-col", + ), + rx.box(class_name="flex-1"), + # Purple gradient circle + status_badge( + icon="loader-circle", + text="Deploying", + class_name="loading-badge transition-all duration-200 ease-out", + ), + status_badge( + icon="ready", + text="Ready", + class_name="ready-badge hidden opacity-0 transition-all duration-200 ease-out", + ), + class_name="flex flex-row relative items-center w-full", + ), + purple_gradient_circle(), + class_name="flex w-[20rem] h-[4.5rem] flex-row flex-shrink-0 rounded-xl rounded-b-none border-t-[1px] border-r-[1px] border-l-[1px] border-slate-4 bg-[light-dark(rgba(249, 249, 251, 0.48), rgba(26,27,29,0.48))] backdrop-filter backdrop-blur-[10px] deploy-box transition-all duration-[550ms] ease-in-out z-[6] origin-bottom absolute items-center p-[1rem_1.625rem_1rem_1.25rem] overflow-hidden", + ) + + +def animated_box() -> rx.Component: + return rx.box( + rx.box( + terminal_box(), + deploy_box(), + # Glow + rx.html( + """ + + + + + + + + + + """, + class_name="absolute bottom-[-6.5rem] left-1/2 transform -translate-x-1/2 w-[26.625rem] h-[17rem] flex-shrink-0 pointer-events-none", + ), + class_name="justify-center flex flex-col items-center max-w-[34.5rem] max-h-[17.875rem] shrink-0 relative w-full h-full overflow-hidden", + ), + on_mount=rx.call_script(typing_text_script()), # On dev it will run twice + class_name="flex items-center justify-center w-[34.5rem] h-[17.875rem] top-[13rem] absolute", + ) diff --git a/pcweb/pages/hosting_countdown/hosting_countdown.py b/pcweb/pages/hosting_countdown/hosting_countdown.py new file mode 100644 index 0000000000..d5e1dcfb4e --- /dev/null +++ b/pcweb/pages/hosting_countdown/hosting_countdown.py @@ -0,0 +1,37 @@ +import reflex as rx +from pcweb.components.docpage.navbar import navbar +from pcweb.components.webpage.badge import badge +from pcweb.pages.index.index_colors import index_colors +from pcweb.pages.index.views.footer_index import footer_index +from pcweb.components.icons import get_icon +from pcweb.pages.hosting_countdown.semicircle_timer import semicircle_timer, ellipsis +from pcweb.pages.hosting_countdown.timer import timer +from pcweb.pages.hosting_countdown.animated_box import animated_box +from pcweb.pages.hosting_countdown.waitlist import waitlist + + +@rx.page(route="/hosting", title="Reflex ยท Hosting") +def hosting_countdown() -> rx.Component: + """Get the Hosting landing page.""" + return rx.box( + index_colors(), + navbar(), + rx.el.main( + rx.box( + semicircle_timer(), + ellipsis(), + timer(), + animated_box(), + class_name="flex flex-col relative justify-center items-center pt-[13rem]", + ), + waitlist(), + get_icon( + "bottom_logo", + class_name="absolute left-1/2 bottom-0 transform -translate-x-1/2 z-[-1] md:w-auto w-[21.9375rem] md:h-auto h-[4.125rem]", + ), + class_name="flex flex-col w-full relative h-full max-w-[64.19rem] border-b border-slate-4 lg:border-x", + ), + footer_index(), + badge(), + class_name="flex flex-col w-full max-w-[94.5rem] justify-center items-center mx-auto px-4 lg:px-5 relative overflow-hidden", + ) diff --git a/pcweb/pages/hosting_countdown/semicircle_timer.py b/pcweb/pages/hosting_countdown/semicircle_timer.py new file mode 100644 index 0000000000..e63876b4d0 --- /dev/null +++ b/pcweb/pages/hosting_countdown/semicircle_timer.py @@ -0,0 +1,120 @@ +import reflex as rx + +TimeSecondsClientState = rx._x.client_state("timer", default=0) + + +def semicircle_timer(): + segments = [ + rx.box( + class_name=rx.cond( + TimeSecondsClientState.value < i, + "timer-segment", + "timer-segment active", + ), + style={"--segment-index": i}, + ) + for i in range(61) + ] + + return rx.box( + TimeSecondsClientState, + rx.el.style( + """ + .timer-segment { + width: 0.25rem; + height: 1.5rem; + position: absolute; + left: 50%; + bottom: 0; + transform-origin: bottom center; + border-radius: 0.4375rem; + transform: rotate(calc((var(--segment-index) * 3deg) - 90deg)) translateY(-15.25rem); + background-color: var(--slate-3, #222326); + transition: all 0.2s ease-out; + } + + .timer-segment.active { + background-color: var(--violet-9, #6E56CF); + box-shadow: 0px 0px 8px 0px var(--violet-9, #6E56CF); + } + """ + ), + rx.box( + *segments, + ), + # Use moment with interval to update every second + rx.moment( + interval=1000, + format="ss", + on_change=TimeSecondsClientState.set_value, + class_name="hidden pointer-events-none", + ), + class_name="w-[33.5rem] h-[16.625rem] relative mb-[0.75rem] z-[1] pointer-events-none", + ) + + +def ellipsis(): + return rx.html( + rx.color_mode_cond( + dark=""" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + """, + light=""" + + + + + + + + + + + + + + + + + + + + + + + + + + + +""", + ), + class_name="absolute flex justify-center items-center mb-[0.75rem] top-[10.35rem] pointer-events-none", + ) diff --git a/pcweb/pages/hosting_countdown/timer.py b/pcweb/pages/hosting_countdown/timer.py new file mode 100644 index 0000000000..083e51fafb --- /dev/null +++ b/pcweb/pages/hosting_countdown/timer.py @@ -0,0 +1,15 @@ +import reflex as rx + + +def timer(): + return rx.box( + rx.text("Launch in", class_name="font-small font-medium text-slate-9"), + rx.moment( + date="2024-12-01T09:00:00", + duration_from_now=True, + format="DD[d] HH[h] mm[m] ss[s]", + class_name="text-3xl font-semibold text-slate-12 countdown text-nowrap", + interval=1000, + ), + class_name="absolute top-[18.25rem] left-1/2 -translate-x-1/2 flex flex-col gap-1 text-center", + ) diff --git a/pcweb/pages/hosting_countdown/waitlist.py b/pcweb/pages/hosting_countdown/waitlist.py new file mode 100644 index 0000000000..f3377c30ad --- /dev/null +++ b/pcweb/pages/hosting_countdown/waitlist.py @@ -0,0 +1,60 @@ +import httpx +import reflex as rx +from httpx import Response + +from pcweb.constants import REFLEX_DEV_WEB_PRICING_FORM_PRO_PLAN_WAITLIST_WEBHOOK_URL + + +class WaitlistState(rx.State): + + @rx.event + async def submit_pro_waitlist(self, form_data: dict): + try: + with httpx.Client() as client: + response: Response = client.post( + REFLEX_DEV_WEB_PRICING_FORM_PRO_PLAN_WAITLIST_WEBHOOK_URL, + json=form_data, + ) + response.raise_for_status() + + yield rx.toast.success("Thank you for joining the waitlist!") + + except httpx.HTTPError: + yield rx.toast.error("Failed to submit request. Please try again later.") + + +def waitlist(): + return rx.box( + rx.box( + rx.el.h2( + "Meet Reflex Hosting", + class_name="lg:text-5xl text-3xl gradient-heading font-semibold leading-[3.5rem] mb-4", + ), + rx.text( + """Deploy your app with a single command. + Performant, secure, and scalable.""", + class_name="text-slate-9 lg:text-lg text-base mb-8 whitespace-pre-line font-medium text-center", + ), + rx.form( + rx.el.input( + placeholder="Email", + name="email", + type="email", + required=True, + class_name="box-border flex flex-row gap-2 border-slate-5 bg-slate-1 focus:shadow-[0px_0px_0px_2px_var(--c-violet-4)] px-3.5 border rounded-[0.875rem] h-[3rem] font-medium text-slate-12 placeholder:text-slate-9 outline-none focus:outline-none caret-slate-12 max-w-[17.4rem] text-base", + ), + rx.el.button( + "Join Waitlist", + background="linear-gradient(180deg, #6E56CF 0%, #654DC4 100%)", + _hover={ + "background": "linear-gradient(180deg, #6E56CF 0%, #6E56CF 100%)" + }, + class_name="w-fit h-[3rem] px-3.5 rounded-[0.875rem] cursor-pointer text-[#FCFCFD] font-medium text-base transition-bg", + ), + on_submit=WaitlistState.submit_pro_waitlist, + class_name="flex lg:flex-row flex-col gap-2 justify-center items-center", + ), + class_name="flex flex-col items-center max-w-[40rem]bg-slate-1 self-center w-full", + ), + class_name="flex flex-col items-center h-[20rem] bg-slate-1 self-center mt-2 z-10 py-20 lg:mb-[20rem] mb-[10rem] border-t border-slate-4 w-full", + ) diff --git a/pcweb/styles/tailwind_config.py b/pcweb/styles/tailwind_config.py index 7ae9240299..566e5a9527 100644 --- a/pcweb/styles/tailwind_config.py +++ b/pcweb/styles/tailwind_config.py @@ -7,6 +7,78 @@ "fontFamily": { "code": ["JetBrains Mono", "monospace"], }, + "fontSize": { + "xs": [ + "0.75rem", + { + "lineHeight": "1rem", + "letterSpacing": "-0.00375rem", + }, + ], + "sm": [ + "0.875rem", + { + "lineHeight": "1.25rem", + "letterSpacing": "-0.01rem", + }, + ], + "base": [ + "1rem", + { + "lineHeight": "1.5rem", + "letterSpacing": "-0.015rem", + }, + ], + "lg": [ + "1.125rem", + { + "lineHeight": "1.625rem", + "letterSpacing": "-0.01625rem", + }, + ], + "xl": [ + "1.25rem", + { + "lineHeight": "1.75rem", + "letterSpacing": "-0.028125rem", + }, + ], + "2xl": [ + "1.5rem", + { + "lineHeight": "2rem", + "letterSpacing": "-0.0375rem", + }, + ], + "3xl": [ + "2rem", + { + "lineHeight": "2.5rem", + "letterSpacing": "-0.07rem", + }, + ], + "4xl": [ + "2.5rem", + { + "lineHeight": "3rem", + "letterSpacing": "-0.125rem", + }, + ], + "5xl": [ + "3rem", + { + "lineHeight": "3.5rem", + "letterSpacing": "-0.1575rem", + }, + ], + "6xl": [ + "3.5rem", + { + "lineHeight": "4rem", + "letterSpacing": "-0.1925rem", + }, + ], + }, "colors": { **radix_colors_dict, **custom_colors_dict, @@ -14,7 +86,7 @@ }, "boxShadow": { "none": "none", - "small": "0px 2px 4px 0px light-dark(rgba(28, 32, 36, 0.05), rgba(0, 0, 0, 0.00))", + "small": "0px 2px 5px 0px light-dark(rgba(28, 32, 36, 0.03), rgba(0, 0, 0, 0.00))", "medium": "0px 4px 8px 0px light-dark(rgba(28, 32, 36, 0.04), rgba(0, 0, 0, 0.00))", "large": "0px 24px 12px 0px light-dark(rgba(28, 32, 36, 0.02), rgba(0, 0, 0, 0.00)), 0px 8px 8px 0px light-dark(rgba(28, 32, 36, 0.02), rgba(0, 0, 0, 0.00)), 0px 2px 6px 0px light-dark(rgba(28, 32, 36, 0.02), rgba(0, 0, 0, 0.00))", }, From 588328deba02040f972a15c233c876c6b4c4b80e Mon Sep 17 00:00:00 2001 From: Alek Petuskey Date: Fri, 8 Nov 2024 17:17:55 -0800 Subject: [PATCH 2/2] More updates --- .gitignore | 1 + pcweb/components/hosting_banner.py | 54 ++++++++++++++--------- pcweb/pages/hosting_countdown/timer.py | 8 +++- pcweb/pages/hosting_countdown/waitlist.py | 54 ++++++++++++++++------- 4 files changed, 80 insertions(+), 37 deletions(-) diff --git a/.gitignore b/.gitignore index a46e11bcc0..b54ce02c31 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,4 @@ pcweb/whitelist.py pynecone.db reflex.db venv/ +.env diff --git a/pcweb/components/hosting_banner.py b/pcweb/components/hosting_banner.py index 81c3d1199a..11b2a486ff 100644 --- a/pcweb/components/hosting_banner.py +++ b/pcweb/components/hosting_banner.py @@ -5,30 +5,44 @@ def glow() -> rx.Component: return rx.box( class_name="absolute w-[120rem] h-[23.75rem] flex-shrink-0 rounded-[120rem] left-1/2 -translate-x-1/2 z-[0] top-[-16rem]", background_image=rx.color_mode_cond( - "radial-gradient(50% 50% at 50% 50%, rgba(235, 228, 255, 0.80) 0%, rgba(252, 252, 253, 0.00) 100%)", - "radial-gradient(50% 50% at 50% 50%, rgba(38, 25, 88, 1) 0%, rgba(21, 22, 24, 0.00) 100%)", + "radial-gradient(50% 50% at 50% 50%, rgba(235, 228, 255, 0.95) 0%, rgba(252, 252, 253, 0.00) 100%)", + "radial-gradient(50% 50% at 50% 50%, rgba(58, 45, 118, 1) 0%, rgba(21, 22, 24, 0.00) 100%)", ), ) +class HostingBannerState(rx.State): + show_banner: bool = True + + def hide_banner(self): + print("hide_banner") + self.show_banner = False + def hosting_banner() -> rx.Component: - return rx.box( - rx.text( - "Meet Hosting. ", - rx.el.span( - "Deploy your app with a single command. Performant, secure, and scalable.", - class_name="text-slate-12 font-medium text-sm", + return rx.cond( + HostingBannerState.show_banner, + rx.desktop_only(rx.box( + rx.link( + rx.box( + rx.text(""), + rx.text( + "Reflex Cloud - ", + rx.el.span( + "Fast, secure & scalable hosting. One command to deploy.", + class_name="text-slate-12 font-medium text-sm", + ), + class_name="text-slate-12 font-semibold text-sm z-[1]", + ), + rx.el.button( + "Launching Soon", + class_name="text-slate-11 h-[1.5rem] rounded-md bg-slate-4 px-1.5 text-sm font-semibold z-[1]", + ), + class_name="flex items-center gap-4", + ), + href="/hosting", ), - class_name="text-slate-12 font-semibold text-sm z-[1]", - ), - rx.link( - rx.el.button( - "Learn More", - class_name="text-slate-12 h-[1.75rem] rounded-md bg-slate-4 px-1.5 text-sm font-semibold", - ), - href="/hosting", - class_name="z-[1] shrink-0", - ), - glow(), - class_name="px-4 lg:px-6 w-screen h-auto lg:h-[65px] shadow-[inset_0_-1px_0_0_var(--c-slate-3)] flex items-center justify-center bg-slate-1 flex-row gap-4 overflow-hidden relative lg:py-0 py-2 max-w-full", + rx.icon("x", on_click=HostingBannerState.hide_banner, size=16, color=rx.color("slate", 9), class_name="absolute right-4 z-[1]"), + glow(), + class_name="px-4 lg:px-6 w-screen h-auto lg:h-[40px] shadow-[inset_0_-1px_0_0_var(--c-slate-3)] flex items-center justify-center bg-slate-1 flex-row gap-4 overflow-hidden relative lg:py-0 py-2 max-w-full", + )) ) diff --git a/pcweb/pages/hosting_countdown/timer.py b/pcweb/pages/hosting_countdown/timer.py index 083e51fafb..0e228109c6 100644 --- a/pcweb/pages/hosting_countdown/timer.py +++ b/pcweb/pages/hosting_countdown/timer.py @@ -2,6 +2,11 @@ def timer(): + remove_negative_sign = rx.vars.function.ArgsFunctionOperation.create( + args_names=("t",), + return_expr=rx.vars.sequence.string_replace_operation(rx.Var("t"), "-", ""), + ) + return rx.box( rx.text("Launch in", class_name="font-small font-medium text-slate-9"), rx.moment( @@ -9,7 +14,8 @@ def timer(): duration_from_now=True, format="DD[d] HH[h] mm[m] ss[s]", class_name="text-3xl font-semibold text-slate-12 countdown text-nowrap", + custom_attrs={"filter": remove_negative_sign}, interval=1000, ), class_name="absolute top-[18.25rem] left-1/2 -translate-x-1/2 flex flex-col gap-1 text-center", - ) + ) \ No newline at end of file diff --git a/pcweb/pages/hosting_countdown/waitlist.py b/pcweb/pages/hosting_countdown/waitlist.py index f3377c30ad..4ed354eee7 100644 --- a/pcweb/pages/hosting_countdown/waitlist.py +++ b/pcweb/pages/hosting_countdown/waitlist.py @@ -7,8 +7,14 @@ class WaitlistState(rx.State): + loading = False + success = False + @rx.event async def submit_pro_waitlist(self, form_data: dict): + self.loading = True + yield + try: with httpx.Client() as client: response: Response = client.post( @@ -17,17 +23,20 @@ async def submit_pro_waitlist(self, form_data: dict): ) response.raise_for_status() + self.success = True yield rx.toast.success("Thank you for joining the waitlist!") except httpx.HTTPError: yield rx.toast.error("Failed to submit request. Please try again later.") + self.loading = False + def waitlist(): return rx.box( rx.box( rx.el.h2( - "Meet Reflex Hosting", + "Claim your spot!", class_name="lg:text-5xl text-3xl gradient-heading font-semibold leading-[3.5rem] mb-4", ), rx.text( @@ -36,21 +45,34 @@ def waitlist(): class_name="text-slate-9 lg:text-lg text-base mb-8 whitespace-pre-line font-medium text-center", ), rx.form( - rx.el.input( - placeholder="Email", - name="email", - type="email", - required=True, - class_name="box-border flex flex-row gap-2 border-slate-5 bg-slate-1 focus:shadow-[0px_0px_0px_2px_var(--c-violet-4)] px-3.5 border rounded-[0.875rem] h-[3rem] font-medium text-slate-12 placeholder:text-slate-9 outline-none focus:outline-none caret-slate-12 max-w-[17.4rem] text-base", - ), - rx.el.button( - "Join Waitlist", - background="linear-gradient(180deg, #6E56CF 0%, #654DC4 100%)", - _hover={ - "background": "linear-gradient(180deg, #6E56CF 0%, #6E56CF 100%)" - }, - class_name="w-fit h-[3rem] px-3.5 rounded-[0.875rem] cursor-pointer text-[#FCFCFD] font-medium text-base transition-bg", - ), + rx.cond( + WaitlistState.success, + rx.badge("Thank you for joining the waitlist!", color_scheme="green", size="5"), + rx.vstack( + rx.hstack( + rx.el.input( + placeholder="Email", + name="email", + type="email", + required=True, + class_name="box-border flex flex-row gap-2 border-slate-5 bg-slate-1 focus:shadow-[0px_0px_0px_2px_var(--c-violet-4)] px-3.5 border rounded-[8px] h-[2rem] font-medium text-slate-12 placeholder:text-slate-9 outline-none focus:outline-none caret-slate-12 max-w-[17.4rem] text-base", + ), + rx.button( + "Join Waitlist", + background="linear-gradient(180deg, #6E56CF 0%, #654DC4 100%)", + _hover={ + "background": "linear-gradient(180deg, #6E56CF 0%, #6E56CF 100%)" + }, + class_name="w-fit h-[2rem] px-3.5 rounded-[8px] cursor-pointer text-[#FCFCFD] font-medium text-base transition-bg", + ), + is_loading=WaitlistState.loading, + ), + rx.text( + "Join the waitlist to get access.", + class_name="text-slate-8 lg:text-md text-base mb-8 whitespace-pre-line font-medium text-center", + ), + align_items="center", + )), on_submit=WaitlistState.submit_pro_waitlist, class_name="flex lg:flex-row flex-col gap-2 justify-center items-center", ),