diff --git a/assets/common/dark/squares_vertical_blog.svg b/assets/common/dark/squares_vertical_blog.svg new file mode 100644 index 000000000..360dec19a --- /dev/null +++ b/assets/common/dark/squares_vertical_blog.svg @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/common/light/squares_vertical_blog.svg b/assets/common/light/squares_vertical_blog.svg new file mode 100644 index 000000000..0519c1bdf --- /dev/null +++ b/assets/common/light/squares_vertical_blog.svg @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pcweb/components/icons/icons.py b/pcweb/components/icons/icons.py index c7889244b..588542978 100644 --- a/pcweb/components/icons/icons.py +++ b/pcweb/components/icons/icons.py @@ -517,6 +517,27 @@ """ +twitter_blog = """ + +""" + +linkedin_blog = """ + +""" + +link_blog = """ + + +""" + +reddit_blog = """ + + + + + +""" + markdown = """""" ICONS = { @@ -598,6 +619,10 @@ "python-01": python_01, "shield-key": shield_key, "chart-up": chart_up, + "twitter_blog": twitter_blog, + "linkedin_blog": linkedin_blog, + "link_blog": link_blog, + "reddit_blog": reddit_blog, "markdown": markdown, } diff --git a/pcweb/pages/blog/blog.py b/pcweb/pages/blog/blog.py index a2589cce9..f40d32e82 100644 --- a/pcweb/pages/blog/blog.py +++ b/pcweb/pages/blog/blog.py @@ -3,6 +3,7 @@ from pcweb.components.icons.icons import get_icon from pcweb.components.webpage.comps import h1_title from pcweb.meta.meta import create_meta_tags +from pcweb.templates.marketing_page import marketing_page from pcweb.templates.webpage import webpage from .page import page @@ -160,7 +161,7 @@ def blogs(): # Get the docpage component. route = f"/blog/{path}" title = rx.utils.format.to_snake_case(path.rsplit("/", 1)[1].replace(".md", "")) - comp = webpage( + comp = marketing_page( path=route, title=document.metadata["title"] + " · Reflex Blog", description=document.metadata["description"], diff --git a/pcweb/pages/blog/page.py b/pcweb/pages/blog/page.py index dfe7b228b..404ad909a 100644 --- a/pcweb/pages/blog/page.py +++ b/pcweb/pages/blog/page.py @@ -1,12 +1,136 @@ import reflex as rx +import reflex_ui as ui +from reflex.experimental.client_state import ClientStateVar -from pcweb.components.icons import get_icon -from pcweb.components.webpage.comps import h1_title +from pcweb.components.hosting_banner import HostingBannerState +from pcweb.components.icons.icons import get_icon +from pcweb.components.marketing_button import button +from pcweb.constants import REFLEX_URL from pcweb.flexdown import xd2 as xd +from pcweb.templates.docpage import get_toc, right_sidebar_item_highlight from .paths import blog_data +def share_post_button(icon: str, href: str, aria_label: str) -> rx.Component: + return rx.el.a( + button( + get_icon(icon), + native_button=False, + variant="outline", + size="icon-sm", + aria_label=aria_label, + ), + target="_blank", + to=href, + ) + + +@rx.memo +def copy_link_button(url: str): + copied = ClientStateVar.create("is_copied", default=False, global_ref=False) + return button( + rx.cond( + copied.value, + ui.icon( + "Tick02Icon", + ), + get_icon("link_blog"), + ), + variant="outline", + aria_label="Copy link to post", + size="icon-sm", + on_click=[ + rx.call_function(copied.set_value(True)), + rx.set_clipboard(url), + ], + on_mouse_down=rx.call_function(copied.set_value(False)).debounce(1500), + ) + + +def table_of_contents(toc: list, path: str, page_url: str) -> rx.Component: + """Render the table of contents sidebar.""" + if len(toc) < 2: + return rx.fragment() + + return rx.el.nav( + rx.box( + rx.el.p( + "On This Page", + class_name="text-sm h-8 flex items-center justify-start font-[525] dark:text-m-slate-3 text-m-slate-12", + ), + rx.el.ul( + *[ + ( + rx.el.li( + rx.el.a( + text, + class_name="text-sm font-[525] text-m-slate-7 dark:text-m-slate-6 pl-4 py-1 block hover:text-m-slate-9 dark:hover:text-m-slate-5 transition-colors truncate", + href=path + "#" + text.lower().replace(" ", "-"), + ), + ) + if level == 1 + else ( + rx.el.li( + rx.el.a( + text, + class_name="text-sm font-[525] text-m-slate-7 dark:text-m-slate-6 pl-4 py-1 block hover:text-m-slate-9 dark:hover:text-m-slate-5 transition-colors truncate", + href=path + "#" + text.lower().replace(" ", "-"), + ), + ) + if level == 2 + else rx.el.li( + rx.el.a( + text, + class_name="text-sm font-[525] text-m-slate-7 dark:text-m-slate-6 pl-8 py-1 block hover:text-m-slate-9 dark:hover:text-m-slate-5 transition-colors truncate", + href=path + "#" + text.lower().replace(" ", "-"), + ), + ) + ) + ) + for level, text in toc + ], + id="toc-navigation", + class_name="flex flex-col gap-y-1 list-none shadow-[1.5px_0_0_0_var(--m-slate-4)_inset] dark:shadow-[1.5px_0_0_0_var(--m-slate-9)_inset]", + ), + rx.el.div( + rx.el.span( + "Share Post", + class_name="text-m-slate-12 dark:text-m-slate-3 font-[525] text-sm", + ), + rx.el.div( + share_post_button( + "twitter_blog", + f"https://twitter.com/intent/tweet?text={page_url}", + "Share on Twitter", + ), + share_post_button( + "linkedin_blog", + f"https://www.linkedin.com/feed/?shareActive=true&text={page_url}", + "Share on LinkedIn", + ), + share_post_button( + "reddit_blog", + f"https://www.reddit.com/submit?url={page_url}", + "Share on Reddit", + ), + copy_link_button(url=page_url), + class_name="flex flex-row gap-2", + ), + class_name="flex flex-col gap-5 mt-6", + ), + class_name="flex flex-col justify-start gap-y-4 overflow-y-auto", + ), + on_mount=rx.call_script(right_sidebar_item_highlight()), + class_name=ui.cn( + "sticky w-[14rem] shrink-0 hidden xl:block self-start max-lg:hidden", + rx.cond( + HostingBannerState.is_banner_visible, "top-[8.5rem]", "top-[6.5rem]" + ), + ), + ) + + def more_posts(current_post: dict) -> rx.Component: from .blog import card_content @@ -56,51 +180,89 @@ def more_posts(current_post: dict) -> rx.Component: def page(document, route) -> rx.Component: """Create a page.""" meta = document.metadata + toc, _ = get_toc(document, route) + page_url = f"{REFLEX_URL.strip('/')}{route}" return rx.el.section( rx.el.article( - rx.link( - rx.box( - get_icon("arrow_right", class_name="rotate-180"), - "Back to Blog", - class_name="box-border flex justify-center items-center gap-2 border-slate-5 bg-slate-1 hover:bg-slate-3 -mb-4 px-3 py-0.5 border rounded-full font-small text-secondary-11 transition-bg cursor-pointer", - ), - underline="none", - href="/blog", - ), - rx.el.header( - h1_title(title=meta["title"]), - rx.el.h2( - str(meta["description"]), - class_name="font-md text-balance text-slate-10", - ), - rx.box( - rx.text( - meta["author"], - ), - rx.text( - "·", + rx.el.div( + rx.el.div( + rx.el.a( + "Blog", + href="/blog", + class_name="text-m-slate-12 dark:text-m-slate-3 font-[575] hover:text-primary-10 dark:hover:text-primary-9 text-xs", ), + rx.el.div(class_name="w-4 h-px bg-m-slate-5 dark:bg-m-slate-10"), rx.moment( str(meta["date"]), format="MMM DD, YYYY", + class_name="font-[475] text-m-slate-7 dark:text-m-slate-6 text-xs", ), - class_name="flex items-center gap-2 !font-normal font-small text-nowrap text-secondary-11", + class_name="flex flex-row items-center gap-3 mb-6", + ), + rx.image( + src=f"/common/{rx.color_mode_cond('light', 'dark')}/squares_vertical_blog.svg", + alt="Squares Vertical Docs", + loading="lazy", + class_name="pointer-events-none w-auto h-[calc(100%-2rem)] absolute inset-y-4 left-2 max-lg:hidden", + ), + rx.image( + src=f"/common/{rx.color_mode_cond('light', 'dark')}/squares_vertical_blog.svg", + alt="Squares Vertical Docs", + loading="lazy", + class_name="pointer-events-none w-auto h-[calc(100%-2rem)] absolute inset-y-4 right-2 scale-x-[-1] max-lg:hidden", ), - class_name="section-header", + rx.el.header( + rx.el.h1( + meta["title"], + class_name="lg:text-5xl text-3xl text-m-slate-12 dark:text-m-slate-3 font-[575] mb-6 text-center text-balance", + ), + rx.el.h2( + str(meta["description"]), + class_name="lg:text-base text-sm text-m-slate-7 dark:text-m-slate-6 font-[475] mb-8 text-center", + ), + rx.el.span( + meta["author"], + class_name="text-m-slate-12 dark:text-m-slate-3 text-sm font-[525]", + ), + class_name="flex flex-col justify-center items-center max-w-[45rem] mx-auto w-full", + ), + class_name="flex flex-col justify-center items-center max-w-[69rem] lg:border-x border-m-slate-4 dark:border-m-slate-9 lg:py-16 pb-8 w-full mx-auto relative", ), - rx.image( - src=f"{meta['image']}", - alt=f"Image for blog post: {meta['title']}", - loading="eager", - custom_attrs={"fetchPriority": "high"}, - class_name="rounded-[1.125rem] w-auto object-contain max-w-full max-h-[40rem]", + rx.el.hr( + class_name="h-[1px] w-full bg-m-slate-4 dark:bg-m-slate-10", ), - rx.box( - xd.render(document, document.filename), - class_name="flex flex-col gap-4 w-full max-w-2xl", + rx.el.div( + rx.el.div( + rx.el.div( + rx.image( + src=f"{meta['image']}", + alt=f"Image for blog post: {meta['title']}", + loading="eager", + custom_attrs={"fetchPriority": "high"}, + class_name="rounded-xl object-contain w-full h-auto mb-4", + ), + rx.el.div( + xd.render(document, document.filename), + class_name="flex flex-col gap-4 w-full max-w-2xl", + ), + class_name="flex flex-col gap-12 flex-1", + ), + table_of_contents(toc, route, page_url), + class_name="flex flex-row gap-24 max-w-[69rem] mx-auto w-full lg:py-24 py-12", + ), + rx.el.div( + more_posts(meta), + class_name="max-w-[69rem] mx-auto w-full", + ), + class_name="bg-gradient-to-b from-white-1 to-m-slate-1 dark:from-m-slate-11 dark:to-m-slate-12 w-full flex flex-col gap-12", + ), + ), + class_name=ui.cn( + "flex flex-col mx-auto max-lg:px-6 w-full relative", + rx.cond( + HostingBannerState.is_banner_visible, + "lg:pt-[7rem] pt-[11rem]", + "lg:pt-[4rem] pt-[8rem]", ), - more_posts(meta), - class_name="flex flex-col justify-center items-center gap-12 max-w-full", ), - class_name="section-content", ) diff --git a/pcweb/templates/docpage/docpage.py b/pcweb/templates/docpage/docpage.py index 0fd567c3c..9d6ba0c6c 100644 --- a/pcweb/templates/docpage/docpage.py +++ b/pcweb/templates/docpage/docpage.py @@ -729,41 +729,36 @@ def wrapper(*args, **kwargs) -> rx.Component: rx.el.li( rx.el.a( text, - class_name="text-sm font-[525] text-m-slate-7 dark:text-m-slate-6 break-words pl-4 min-h-7 flex items-center", + class_name="text-sm font-[525] text-m-slate-7 dark:text-m-slate-6 pl-4 py-1 block hover:text-m-slate-9 dark:hover:text-m-slate-5 transition-colors truncate", href=path + "#" + text.lower().replace(" ", "-"), ), - class_name="min-h-8 flex items-center", ) if level == 1 else ( rx.el.li( rx.el.a( text, - class_name="text-sm font-[525] text-m-slate-7 dark:text-m-slate-6 break-words min-h-7 pl-4 flex items-center", - underline="none", + class_name="text-sm font-[525] text-m-slate-7 dark:text-m-slate-6 pl-4 py-1 block hover:text-m-slate-9 dark:hover:text-m-slate-5 transition-colors truncate", href=path + "#" + text.lower().replace( " ", "-" ), ), - class_name="min-h-8 flex items-center", ) if level == 2 else rx.el.li( rx.el.a( text, - underline="none", - class_name="text-sm font-[525] text-m-slate-7 dark:text-m-slate-6 break-words min-h-7 pl-8 flex items-center", + class_name="text-sm font-[525] text-m-slate-7 dark:text-m-slate-6 pl-8 py-1 block hover:text-m-slate-9 dark:hover:text-m-slate-5 transition-colors truncate", href=path + "#" + text.lower().replace( " ", "-" ), ), - class_name="min-h-8 flex items-center", ) ) ) diff --git a/pcweb/templates/marketing_page.py b/pcweb/templates/marketing_page.py new file mode 100644 index 000000000..3df41a651 --- /dev/null +++ b/pcweb/templates/marketing_page.py @@ -0,0 +1,90 @@ +import functools +from typing import Callable + +import reflex as rx + +from pcweb.route import Route + +DEFAULT_TITLE = "The platform to build and scale enterprise apps" +DEFAULT_DESCRIPTION = "Connect to all your company data and systems to build secure internal apps with AI. Deployed on prem with built-in governance and production-grade reliability, so technical and nontechnical teams can ship together." + + +def marketing_page( + path: str, + title: str = DEFAULT_TITLE, + description: str | None = DEFAULT_DESCRIPTION, + image: str | None = None, + meta: list[dict[str, str]] | None = None, + props=None, + add_as_page=True, +) -> Callable: + """A template that most pages on the reflex.dev site should use. + + This template wraps the webpage with the navbar and footer. + + Args: + path: The path of the page. + title: The title of the page. + description: The description of the page. + image: The image to use for social media. + meta: Additional meta tags to add to the page. + props: Props to apply to the template. + add_as_page: whether to add the route to the app pages. + + Returns: + A wrapper function that returns the full webpage. + """ + props = props or {} + + def marketing_page(contents: Callable[[], Route]) -> Route: + """Wrapper to create a templated route. + + Args: + contents: The function to create the page route. + + Returns: + The templated route. + """ + + @functools.wraps(contents) + def wrapper(*children, **props) -> rx.Component: + """The template component. + + Args: + children: The children components. + props: The props to apply to the component. + + Returns: + The component with the template applied. + """ + # Import here to avoid circular imports. + from pcweb.views.bottom_section.bottom_section import bottom_section + from pcweb.views.footer import footer + from pcweb.views.marketing_navbar import marketing_navbar + + # Wrap the component in the template. + return rx.el.div( + marketing_navbar(), + rx.el.main( + rx.el.div( + contents(*children, **props), + bottom_section(), + footer(), + class_name="flex flex-col relative justify-center items-center w-full", + ), + class_name="flex flex-col w-full relative h-full justify-center items-center", + ), + class_name="flex flex-col w-full justify-center items-center relative dark:bg-m-slate-12 bg-m-slate-1", + ) + + return Route( + path=path, + title=title, + description=description, + image=image, + meta=meta, + component=wrapper, + add_as_page=add_as_page, + ) + + return marketing_page diff --git a/pcweb/views/bottom_section/bottom_section.py b/pcweb/views/bottom_section/bottom_section.py index 27f98ecdb..175afcf40 100644 --- a/pcweb/views/bottom_section/bottom_section.py +++ b/pcweb/views/bottom_section/bottom_section.py @@ -9,5 +9,5 @@ def bottom_section() -> rx.Component: return rx.box( newsletter(), get_started(), - class_name="flex flex-col items-center gap-20 lg:gap-32 pt-8 lg:pt-[6.5rem] w-[22rem] lg:w-[25rem]", + class_name="flex flex-col items-center gap-20 lg:gap-32 pt-8 lg:pt-[6.5rem] max-lg:max-w-[22rem] w-full lg:w-[25rem] overflow-hidden", )