Design refresh for marketing pages#980
Conversation
|
@jshwrnr is attempting to deploy a commit to the Inbox Zero OSS Program Team on Vercel. A member of the Team first needs to authorize it. |
|
Note Other AI code review bot(s) detectedCodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review. WalkthroughLarge UI rearrangement: landing/new-landing components were added, removed, or made prop-driven; Brand data moved to a utility; Banner and layout updates introduced; Pricing lazy-load wrappers changed; VSCode format-on-save override removed. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant B as Browser
participant LP as LandingPage (apps/web/app/(landing)/page.tsx)
participant Hero as Hero (home/Hero.tsx)
participant Sections as Section components
participant BrandU as BRANDS_LIST (apps/web/utils/brands.ts)
participant BS as BrandScroller
participant Footer as Footer
B->>LP: GET / (render)
LP->>Hero: render Hero(title, subtitle, badge, children)
Hero->>BS: render BrandScroller(brandList?)
BS->>BrandU: request brandList (default/realtor)
BrandU-->>BS: return brands array
Hero->>Sections: render OrganizedInbox / PreWrittenDrafts / StartedInMinutes with props
LP->>Footer: render Footer(variant="default")
Footer->>B: return final markup
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes
Possibly related PRs
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
📜 Recent review detailsConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro 📒 Files selected for processing (3)
🚧 Files skipped from review as they are similar to previous changes (1)
🧰 Additional context used🧠 Learnings (1)📚 Learning: 2025-06-05T09:49:12.168ZApplied to files:
🧬 Code graph analysis (2)apps/web/app/(landing)/welcome-upgrade/page.tsx (2)
apps/web/app/(app)/premium/OldPricingLazy.tsx (2)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
🔇 Additional comments (3)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (5)
apps/web/components/common/Banner.tsx (1)
13-15: Consider making the heading level configurable.The title is always rendered as an
h2, which may not fit all contexts where this component is used. Consider accepting a heading level prop to maintain proper semantic hierarchy.For example:
interface BannerProps { title: React.ReactNode; children: React.ReactNode; + as?: "h1" | "h2" | "h3" | "h4" | "h5" | "h6"; } -export function Banner({ title, children }: BannerProps) { +export function Banner({ title, children, as: Component = "h2" }: BannerProps) { return ( <div className="relative border border-[#E7E7E7A3] rounded-3xl my-10 px-6 py-24 sm:py-32 lg:px-8 overflow-hidden"> <UnicornScene className="opacity-10" /> <div className="mx-auto max-w-2xl text-center"> - <h2 className="font-title text-3xl text-gray-900 sm:text-4xl"> + <Component className="font-title text-3xl text-gray-900 sm:text-4xl"> {title} - </h2> + </Component>apps/web/components/new-landing/sections/Footer.tsx (1)
132-182: Footer variant handling is solid; consider looseningclassNameThe new
variantprop and conditional rendering ofUnicornScene/FooterLineLogoare straightforward and keep the default look unchanged.You might make
classNameoptional for ergonomics, since it’s already safely passed throughcn:-interface FooterProps { - className: string; - variant?: "default" | "simple"; -} +interface FooterProps { + className?: string; + variant?: "default" | "simple"; +}apps/web/app/(landing)/home/FAQs.tsx (1)
1-97: FAQ card layout and content look good; optional typing tweakThe refactor to a Card-based FAQ grid with
Section/Paragraphcomposition reads well and keeps semantics viadt/dd. The new entries (feature requests, refunds viaNEXT_PUBLIC_SUPPORT_EMAIL, free trial) are wired correctly.If you want slightly clearer types for future edits, you could make the FAQ shape explicit:
-import { Anchor } from "@/components/new-landing/common/Anchor"; +import { Anchor } from "@/components/new-landing/common/Anchor"; +import type { ReactNode } from "react"; @@ -const faqs = [ +const faqs: { question: string; answer: ReactNode }[] = [Not required, but it makes the mixed string/JSX
answerfield more self-documenting.apps/web/utils/brands.ts (1)
1-79: Type‑safe brand registry looks solidNice use of
as constwithsatisfiesto keep the brand data and lists type-safe (BrandKey/BrandListKeyare handy).If
heightis intended to hold arbitrary Tailwind classes (e.g. translations, responsive heights), you might consider a more general name likeclassNameorlogoClassNamebefore this spreads across the codebase, but that’s strictly a naming nit.apps/web/components/new-landing/common/Typography.tsx (1)
77-77: Consider the semantic meaning of "dl" for the Paragraph component.The
asprop now allows rendering as a definition list (dl), which is semantically a container element rather than a paragraph-like element. While this provides flexibility, rendering a "Paragraph" component as adlmay be semantically confusing sincedltypically containsdtandddchildren rather than text content.If you need definition list styling, consider whether a separate component or wrapper might be more semantically appropriate.
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (22)
.vscode/settings.json(0 hunks)apps/web/app/(app)/premium/PricingLazy.tsx(1 hunks)apps/web/app/(landing)/home/CTA.tsx(0 hunks)apps/web/app/(landing)/home/FAQs.tsx(1 hunks)apps/web/app/(landing)/home/Hero.tsx(1 hunks)apps/web/app/(landing)/old-landing/page.tsx(2 hunks)apps/web/app/(landing)/oss-friends/page.tsx(2 hunks)apps/web/app/(landing)/page.tsx(1 hunks)apps/web/app/(marketing)(1 hunks)apps/web/components/common/Banner.tsx(1 hunks)apps/web/components/layouts/BasicLayout.tsx(1 hunks)apps/web/components/layouts/BlogLayout.tsx(1 hunks)apps/web/components/new-landing/BrandScroller.tsx(1 hunks)apps/web/components/new-landing/common/BasicLayout.tsx(0 hunks)apps/web/components/new-landing/common/Typography.tsx(1 hunks)apps/web/components/new-landing/sections/FAQs.tsx(0 hunks)apps/web/components/new-landing/sections/Footer.tsx(2 hunks)apps/web/components/new-landing/sections/Hero.tsx(0 hunks)apps/web/components/new-landing/sections/OrganizedInbox.tsx(1 hunks)apps/web/components/new-landing/sections/PreWrittenDrafts.tsx(1 hunks)apps/web/components/new-landing/sections/StartedInMinutes.tsx(1 hunks)apps/web/utils/brands.ts(1 hunks)
💤 Files with no reviewable changes (5)
- apps/web/components/new-landing/common/BasicLayout.tsx
- apps/web/components/new-landing/sections/Hero.tsx
- apps/web/components/new-landing/sections/FAQs.tsx
- .vscode/settings.json
- apps/web/app/(landing)/home/CTA.tsx
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-06-05T09:49:12.168Z
Learnt from: elie222
Repo: elie222/inbox-zero PR: 485
File: apps/web/app/(landing)/login/page.tsx:41-43
Timestamp: 2025-06-05T09:49:12.168Z
Learning: In Next.js App Router, components that use the `useSearchParams` hook require a Suspense boundary to handle the asynchronous nature of search parameter access. The Suspense wrapper is necessary and should not be removed when a component uses useSearchParams.
Applied to files:
apps/web/app/(app)/premium/PricingLazy.tsx
🧬 Code graph analysis (13)
apps/web/components/new-landing/sections/StartedInMinutes.tsx (2)
apps/web/components/new-landing/common/Section.tsx (1)
Section(9-15)apps/web/components/new-landing/common/Typography.tsx (2)
SectionHeading(34-40)SectionSubtitle(46-52)
apps/web/components/new-landing/sections/OrganizedInbox.tsx (2)
apps/web/components/new-landing/common/Section.tsx (1)
Section(9-15)apps/web/components/new-landing/common/Typography.tsx (2)
SectionHeading(34-40)SectionSubtitle(46-52)
apps/web/app/(app)/premium/PricingLazy.tsx (1)
apps/web/components/Loading.tsx (1)
Loading(3-9)
apps/web/app/(landing)/old-landing/page.tsx (1)
apps/web/app/(landing)/home/FinalCTA.tsx (1)
FinalCTA(11-36)
apps/web/components/new-landing/sections/PreWrittenDrafts.tsx (2)
apps/web/components/new-landing/common/Section.tsx (1)
Section(9-15)apps/web/components/new-landing/common/Typography.tsx (2)
SectionHeading(34-40)SectionSubtitle(46-52)
apps/web/components/new-landing/sections/Footer.tsx (2)
apps/web/components/new-landing/UnicornScene.tsx (1)
UnicornScene(26-55)apps/web/components/new-landing/FooterLineLogo.tsx (1)
FooterLineLogo(5-22)
apps/web/app/(landing)/oss-friends/page.tsx (1)
apps/web/app/(landing)/home/FinalCTA.tsx (1)
FinalCTA(11-36)
apps/web/app/(landing)/home/Hero.tsx (11)
apps/web/components/new-landing/common/Badge.tsx (2)
BadgeVariant(3-15)Badge(24-99)apps/web/components/new-landing/common/Section.tsx (2)
Section(9-15)SectionContent(23-33)apps/web/components/new-landing/common/Typography.tsx (2)
PageHeading(25-27)Paragraph(80-113)apps/web/components/new-landing/common/BlurFade.tsx (1)
BlurFade(31-72)apps/web/components/new-landing/CallToAction.tsx (1)
CallToAction(16-45)apps/web/components/new-landing/icons/Outlook.tsx (1)
Outlook(6-262)apps/web/components/new-landing/icons/Gmail.tsx (1)
Gmail(6-78)apps/web/hooks/useAnalytics.ts (1)
landingPageAnalytics(23-42)apps/web/components/new-landing/LiquidGlassButton.tsx (1)
LiquidGlassButton(9-96)apps/web/components/new-landing/icons/Play.tsx (1)
Play(5-77)apps/web/components/new-landing/UnicornScene.tsx (1)
UnicornScene(26-55)
apps/web/app/(landing)/home/FAQs.tsx (6)
apps/web/components/new-landing/common/Anchor.tsx (1)
Anchor(11-21)apps/web/env.ts (1)
env(16-244)apps/web/components/new-landing/common/Section.tsx (2)
Section(9-15)SectionContent(23-33)apps/web/components/new-landing/common/Typography.tsx (2)
SectionHeading(34-40)Paragraph(80-113)apps/web/components/new-landing/common/CardWrapper.tsx (1)
CardWrapper(11-51)apps/web/components/new-landing/common/Card.tsx (2)
Card(66-104)CardContent(9-11)
apps/web/components/common/Banner.tsx (1)
apps/web/components/new-landing/UnicornScene.tsx (1)
UnicornScene(26-55)
apps/web/app/(landing)/page.tsx (7)
apps/web/components/layouts/BasicLayout.tsx (1)
BasicLayout(7-15)apps/web/app/(landing)/home/Hero.tsx (2)
Hero(40-79)HeroVideoPlayer(81-124)apps/web/components/new-landing/common/WordReveal.tsx (1)
WordReveal(12-38)apps/web/components/new-landing/BrandScroller.tsx (1)
BrandScroller(14-46)apps/web/components/new-landing/sections/OrganizedInbox.tsx (1)
OrganizedInbox(16-39)apps/web/components/new-landing/sections/PreWrittenDrafts.tsx (1)
PreWrittenDrafts(16-39)apps/web/components/new-landing/sections/StartedInMinutes.tsx (1)
StartedInMinutes(34-139)
apps/web/components/new-landing/BrandScroller.tsx (3)
apps/web/utils/brands.ts (1)
Brand(1-5)apps/web/components/new-landing/common/Typography.tsx (1)
Paragraph(80-113)apps/web/utils/config.ts (1)
userCount(8-8)
apps/web/components/layouts/BlogLayout.tsx (1)
apps/web/components/new-landing/sections/Footer.tsx (1)
Footer(137-185)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
- GitHub Check: cubic · AI code reviewer
- GitHub Check: Jit Security
- GitHub Check: test
🔇 Additional comments (16)
apps/web/app/(marketing) (1)
1-1: The original review comment is based on an incorrect assumption.Verification reveals that
apps/web/app/(marketing)is not a submodule:
- No
.gitmodulesfile exists in the repository- No submodules are registered
- The directory is a regular directory, not a git repository
- The directory is not staged as a submodule
The review comment incorrectly assumed this was a submodule pointer change. If this is a regular directory with code changes, the review should focus on the actual file modifications within that directory instead.
Likely an incorrect or invalid review comment.
apps/web/components/layouts/BlogLayout.tsx (3)
4-12: LGTM!The BlogHeader component is well-structured with appropriate sticky positioning, z-index layering, and backdrop blur styling. The semi-transparent background (
bg-white/75) correctly supports the backdrop blur effect.
19-24: Verify Footer padding on small screens.The Footer is styled with
px-0which removes horizontal padding on small screens. This differs from the Header'spx-6on small screens and may cause edge-to-edge content rendering on mobile devices.Ensure this is intentional for the blog layout. If content should have padding on mobile, consider changing
px-0topx-6:<Footer variant="simple" - className="mx-auto w-full max-w-screen-xl px-0 lg:px-20" + className="mx-auto w-full max-w-screen-xl px-6 lg:px-20" />
1-1: Code is correct—Header component properly accepts and applies the className prop.Verification confirms that the Header component at line 15 of
apps/web/components/new-landing/sections/Header.tsxacceptsclassNameas part ofHeaderPropsand correctly merges it into the<header>element using thecn()utility function (line 22), alongside the default styles. The className prop is properly consumed.apps/web/components/common/Banner.tsx (2)
1-2: LGTM!Import path is correct and the UnicornScene component is properly referenced.
3-6: LGTM!The interface is well-defined with appropriate types for flexible content rendering.
apps/web/components/new-landing/sections/StartedInMinutes.tsx (1)
29-38: Prop‑driven section title/subtitle look goodSwitching
StartedInMinutesto accepttitleandsubtitleprops is clean and keeps the existing layout intact while making the section reusable. No issues from a type or runtime perspective.apps/web/app/(landing)/oss-friends/page.tsx (1)
8-77: UsingFinalCTAhere keeps CTAs consistent across landing pagesImporting and rendering
FinalCTAbefore the Footer cleanly aligns this page with the rest of the landing experience without touching existing OSS Friends logic. Looks good as-is.apps/web/app/(landing)/old-landing/page.tsx (1)
9-9: LGTM! Clean component consolidation.The switch from
CTAtoFinalCTAaligns with the broader refactoring effort across the landing pages.Also applies to: 22-22
apps/web/app/(landing)/page.tsx (1)
22-67: Excellent compositional refactor!The new Hero composition with WordReveal, HeroVideoPlayer, and BrandScroller as children is well-structured. The prop-driven section components (OrganizedInbox, PreWrittenDrafts, StartedInMinutes) follow a consistent pattern that improves maintainability and reusability.
The use of
key="actually"on the<em>element in the WordReveal words array is good practice for React reconciliation.apps/web/components/new-landing/sections/PreWrittenDrafts.tsx (1)
11-20: LGTM! Clean prop-driven refactor.Converting this component to accept
titleandsubtitleprops improves reusability and maintains consistency with the other refactored section components.apps/web/components/new-landing/BrandScroller.tsx (1)
9-46: LGTM! Well-structured prop-driven component.The addition of
brandListandanimateprops with sensible defaults makes this component flexible and reusable. The user count display viaParagraphis a nice touch.apps/web/components/layouts/BasicLayout.tsx (1)
9-13: Verify background color handling.The root
divno longer hasbg-whiteapplied. While this is likely intentional for the design refresh, please ensure the background color is properly handled either by a parent component or global styles to avoid unexpected visual issues.apps/web/components/new-landing/sections/OrganizedInbox.tsx (1)
11-20: LGTM! Consistent prop-driven pattern.This refactor follows the same clean pattern as PreWrittenDrafts and other section components, making the component reusable and content-agnostic.
apps/web/app/(landing)/home/Hero.tsx (2)
1-1: Excellent Hero component refactor!The "use client" directive is correctly placed since
HeroVideoPlayeruses theusePostHoghook. The new prop-driven API with optionalbadge,title,subtitle, andchildrenprovides excellent flexibility for composition while maintaining clean structure.The staggered BlurFade animations create a nice progressive reveal effect.
Also applies to: 32-79
81-124: Well-structured video player component.The HeroVideoPlayer implementation is clean with proper:
- Dialog/DialogTrigger composition for modal behavior
- Analytics tracking via
landingPageAnalytics.videoClicked- Accessibility with sr-only DialogTitle
- YouTube embed with autoplay and proper iframe attributes
- UnicornScene background effect
There was a problem hiding this comment.
1 issue found across 23 files
Prompt for AI agents (all 1 issues)
Understand the root cause of the following 1 issues and fix them.
<file name="apps/web/app/(landing)/home/FAQs.tsx">
<violation number="1" location="apps/web/app/(landing)/home/FAQs.tsx:25">
Links opened in a new tab need `rel="noopener noreferrer"`; these new FAQ GitHub links now target `_blank` without that protection, exposing users to tabnabbing. Please ensure the Anchor component (or these usages) add the proper rel attributes.</violation>
</file>
Reply to cubic to teach it or ask questions. Re-run a review with @cubic-dev-ai review this PR
| answer: ( | ||
| <span> | ||
| Email us or post an issue on{" "} | ||
| <Anchor href="/github" newTab> |
There was a problem hiding this comment.
Links opened in a new tab need rel="noopener noreferrer"; these new FAQ GitHub links now target _blank without that protection, exposing users to tabnabbing. Please ensure the Anchor component (or these usages) add the proper rel attributes.
Prompt for AI agents
Address the following comment on apps/web/app/(landing)/home/FAQs.tsx at line 25:
<comment>Links opened in a new tab need `rel="noopener noreferrer"`; these new FAQ GitHub links now target `_blank` without that protection, exposing users to tabnabbing. Please ensure the Anchor component (or these usages) add the proper rel attributes.</comment>
<file context>
@@ -1,100 +1,98 @@
+ answer: (
+ <span>
+ Email us or post an issue on{" "}
+ <Anchor href="/github" newTab>
+ GitHub
+ </Anchor>
</file context>
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
apps/web/app/(landing)/home/Hero.tsx (1)
32-80: Hero composition and CTA flow look solid; consider tightening prop requirementsThe new
Herosurface (badge → heading → subtitle → CTA → integrations → children) is clean and matches the new Section/BlurFade patterns.Two small API/UX tweaks you might consider:
titleis central to this section; making it required inHeroPropswould prevent a “blank” hero from being rendered.subtitleis rendered unconditionally; conditionally rendering the<Paragraph>only whensubtitleis provided would avoid empty spacing on variants that don’t need copy.
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
apps/web/app/(landing)/home/Hero.tsx(1 hunks)apps/web/app/(marketing)(1 hunks)apps/web/components/new-landing/BrandScroller.tsx(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- apps/web/app/(marketing)
🧰 Additional context used
🧬 Code graph analysis (2)
apps/web/components/new-landing/BrandScroller.tsx (4)
apps/web/utils/brands.ts (1)
Brand(1-5)apps/web/components/new-landing/common/BlurFade.tsx (1)
BlurFade(31-72)apps/web/components/new-landing/common/Typography.tsx (1)
Paragraph(80-113)apps/web/utils/config.ts (1)
userCount(8-8)
apps/web/app/(landing)/home/Hero.tsx (11)
apps/web/components/new-landing/common/Badge.tsx (2)
BadgeVariant(3-15)Badge(24-99)apps/web/components/new-landing/common/Section.tsx (2)
Section(9-15)SectionContent(23-33)apps/web/components/new-landing/common/BlurFade.tsx (1)
BlurFade(31-72)apps/web/components/new-landing/common/Typography.tsx (2)
PageHeading(25-27)Paragraph(80-113)apps/web/components/new-landing/CallToAction.tsx (1)
CallToAction(16-45)apps/web/components/new-landing/icons/Outlook.tsx (1)
Outlook(6-262)apps/web/components/new-landing/icons/Gmail.tsx (1)
Gmail(6-78)apps/web/hooks/useAnalytics.ts (1)
landingPageAnalytics(23-42)apps/web/components/new-landing/LiquidGlassButton.tsx (1)
LiquidGlassButton(9-96)apps/web/components/new-landing/icons/Play.tsx (1)
Play(5-77)apps/web/components/new-landing/UnicornScene.tsx (1)
UnicornScene(26-55)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
- GitHub Check: cubic · AI code reviewer
- GitHub Check: Jit Security
- GitHub Check: test
🔇 Additional comments (1)
apps/web/components/new-landing/BrandScroller.tsx (1)
10-18: Prop-driven BrandScroller API looks goodThe
brandList+animateprops with sensible defaults (BRANDS_LIST.default,true) make the component reusable without breaking existing behavior.
| export function HeroVideoPlayer() { | ||
| const posthog = usePostHog(); | ||
|
|
||
| return ( | ||
| <div className="flex flex-col items-center justify-center gap-2 sm:flex-row"> | ||
| <Link | ||
| // href="https://www.producthunt.com/posts/inbox-zero-2?utm_source=badge-top-post-badge&utm_medium=badge&utm_souce=badge-inbox-zero-2" | ||
| href="https://www.producthunt.com/products/inbox-zero-tabs" | ||
| target="_blank" | ||
| rel="noreferrer" | ||
| > | ||
| <Image | ||
| src="/images/home/product-hunt-badge.svg" | ||
| alt="Inbox Zero | Product Hunt" | ||
| className="h-[54px] w-[250px]" | ||
| width="250" | ||
| height="54" | ||
| /> | ||
| </Link> | ||
| </div> | ||
| <BlurFade delay={0.125 * 9}> | ||
| <div className="relative w-full"> | ||
| <div className="relative border border-[#EFEFEF] rounded-3xl md:rounded-[43px] overflow-hidden block"> | ||
| <Dialog> | ||
| <DialogTrigger | ||
| asChild | ||
| onClick={() => landingPageAnalytics.videoClicked(posthog)} | ||
| > | ||
| <LiquidGlassButton className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2"> | ||
| <div> | ||
| <Play className="translate-x-[2px]" /> | ||
| </div> | ||
| </LiquidGlassButton> | ||
| </DialogTrigger> | ||
| <DialogContent className="max-w-7xl border-0 bg-transparent p-0"> | ||
| <DialogTitle className="sr-only">Video player</DialogTitle> | ||
| <div className="relative aspect-video w-full"> | ||
| <iframe | ||
| src="https://www.youtube.com/embed/hfvKvTHBjG0?autoplay=1&rel=0" | ||
| className="size-full rounded-lg" | ||
| title="Video content" | ||
| allowFullScreen | ||
| allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" | ||
| /> | ||
| </div> | ||
| </DialogContent> | ||
| </Dialog> | ||
| <Image | ||
| src="/images/new-landing/video-thumbnail.png" | ||
| alt="an organized inbox" | ||
| width={2000} | ||
| height={1000} | ||
| className="w-full" | ||
| /> | ||
| <UnicornScene className="h-[calc(100%+5px)] opacity-30" /> | ||
| </div> | ||
| </div> | ||
| </BlurFade> | ||
| ); | ||
| } |
There was a problem hiding this comment.
Add an accessible label to the video play button and refine video titles
The video CTA is an icon-only LiquidGlassButton wrapped in DialogTrigger. As-is, the button’s accessible name comes only from the SVG, which isn’t a helpful label for screen readers.
Consider giving the button an explicit label:
- <LiquidGlassButton className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2">
+ <LiquidGlassButton
+ className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2"
+ aria-label="Play inbox zero demo video"
+ >
<div>
<Play className="translate-x-[2px]" />
</div>
</LiquidGlassButton>Optionally, you could:
- Make
DialogTitleand the iframetitlemore descriptive (e.g., “Inbox Zero product demo video”) to improve screen reader context. - If desired, ensure the iframe is only mounted when the dialog is open (depending on the underlying
Dialogimplementation) to avoid any unnecessary background loading, though the current setup is likely acceptable.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export function HeroVideoPlayer() { | |
| const posthog = usePostHog(); | |
| return ( | |
| <div className="flex flex-col items-center justify-center gap-2 sm:flex-row"> | |
| <Link | |
| // href="https://www.producthunt.com/posts/inbox-zero-2?utm_source=badge-top-post-badge&utm_medium=badge&utm_souce=badge-inbox-zero-2" | |
| href="https://www.producthunt.com/products/inbox-zero-tabs" | |
| target="_blank" | |
| rel="noreferrer" | |
| > | |
| <Image | |
| src="/images/home/product-hunt-badge.svg" | |
| alt="Inbox Zero | Product Hunt" | |
| className="h-[54px] w-[250px]" | |
| width="250" | |
| height="54" | |
| /> | |
| </Link> | |
| </div> | |
| <BlurFade delay={0.125 * 9}> | |
| <div className="relative w-full"> | |
| <div className="relative border border-[#EFEFEF] rounded-3xl md:rounded-[43px] overflow-hidden block"> | |
| <Dialog> | |
| <DialogTrigger | |
| asChild | |
| onClick={() => landingPageAnalytics.videoClicked(posthog)} | |
| > | |
| <LiquidGlassButton className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2"> | |
| <div> | |
| <Play className="translate-x-[2px]" /> | |
| </div> | |
| </LiquidGlassButton> | |
| </DialogTrigger> | |
| <DialogContent className="max-w-7xl border-0 bg-transparent p-0"> | |
| <DialogTitle className="sr-only">Video player</DialogTitle> | |
| <div className="relative aspect-video w-full"> | |
| <iframe | |
| src="https://www.youtube.com/embed/hfvKvTHBjG0?autoplay=1&rel=0" | |
| className="size-full rounded-lg" | |
| title="Video content" | |
| allowFullScreen | |
| allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" | |
| /> | |
| </div> | |
| </DialogContent> | |
| </Dialog> | |
| <Image | |
| src="/images/new-landing/video-thumbnail.png" | |
| alt="an organized inbox" | |
| width={2000} | |
| height={1000} | |
| className="w-full" | |
| /> | |
| <UnicornScene className="h-[calc(100%+5px)] opacity-30" /> | |
| </div> | |
| </div> | |
| </BlurFade> | |
| ); | |
| } | |
| export function HeroVideoPlayer() { | |
| const posthog = usePostHog(); | |
| return ( | |
| <BlurFade delay={0.125 * 9}> | |
| <div className="relative w-full"> | |
| <div className="relative border border-[#EFEFEF] rounded-3xl md:rounded-[43px] overflow-hidden block"> | |
| <Dialog> | |
| <DialogTrigger | |
| asChild | |
| onClick={() => landingPageAnalytics.videoClicked(posthog)} | |
| > | |
| <LiquidGlassButton | |
| className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2" | |
| aria-label="Play inbox zero demo video" | |
| > | |
| <div> | |
| <Play className="translate-x-[2px]" /> | |
| </div> | |
| </LiquidGlassButton> | |
| </DialogTrigger> | |
| <DialogContent className="max-w-7xl border-0 bg-transparent p-0"> | |
| <DialogTitle className="sr-only">Video player</DialogTitle> | |
| <div className="relative aspect-video w-full"> | |
| <iframe | |
| src="https://www.youtube.com/embed/hfvKvTHBjG0?autoplay=1&rel=0" | |
| className="size-full rounded-lg" | |
| title="Video content" | |
| allowFullScreen | |
| allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" | |
| /> | |
| </div> | |
| </DialogContent> | |
| </Dialog> | |
| <Image | |
| src="/images/new-landing/video-thumbnail.png" | |
| alt="an organized inbox" | |
| width={2000} | |
| height={1000} | |
| className="w-full" | |
| /> | |
| <UnicornScene className="h-[calc(100%+5px)] opacity-30" /> | |
| </div> | |
| </div> | |
| </BlurFade> | |
| ); | |
| } |
🤖 Prompt for AI Agents
In apps/web/app/(landing)/home/Hero.tsx around lines 83 to 126 the play CTA is
an icon-only LiquidGlassButton inside DialogTrigger and lacks an explicit
accessible name; add an accessible label to the button (e.g., aria-label="Play
Inbox Zero product demo" or include visually hidden text inside the trigger) so
screen readers get a meaningful name, and update DialogTitle and the iframe
title to a more descriptive string like "Inbox Zero product demo video";
optionally, if you want to avoid background loading, render the iframe only when
the Dialog is open (use Dialog state or conditional rendering provided by your
Dialog implementation).
| <BlurFade duration={0.4} delay={0.125 * 10}> | ||
| <div className="mt-12"> | ||
| <Paragraph>Join {userCount} users saving hours daily</Paragraph> | ||
| <div className="group flex overflow-x-hidden py-10 [--gap:2rem] md:[--gap:3rem] [gap:var(--gap))] flex-row max-w-full [mask-image:linear-gradient(to_right,_rgba(0,_0,_0,_0),rgba(0,_0,_0,_1)_10%,rgba(0,_0,_0,_1)_90%,rgba(0,_0,_0,_0))]"> | ||
| {new Array(4).fill(0).map((_, i) => ( | ||
| <div | ||
| className={cx( | ||
| "flex shrink-0 justify-around [margin-right:var(--gap)] [gap:var(--gap)] flex-row [--duration:100s] opacity-90", | ||
| animate ? "animate-marquee" : "", | ||
| )} | ||
| key={i} | ||
| > | ||
| {brandList.map(({ alt, src, height }) => ( | ||
| <div className="flex items-start" key={alt}> | ||
| <Image | ||
| src={src} | ||
| alt={alt} | ||
| width={100} | ||
| height={100} | ||
| className={cx("w-auto", height || "h-5 sm:h-6 md:h-8")} | ||
| /> | ||
| </div> | ||
| ))} | ||
| </div> | ||
| ))} | ||
| </div> |
There was a problem hiding this comment.
Fix malformed Tailwind arbitrary gap class and consider logo accessibility
There’s a typo in the container classes:
"[gap:var(--gap))]"has an extra)and will not generate the intendedgaprule.
This likely breaks the intended spacing between the marquee items.
You can fix the spacing class with:
- <div className="group flex overflow-x-hidden py-10 [--gap:2rem] md:[--gap:3rem] [gap:var(--gap))] flex-row max-w-full [mask-image:linear-gradient(to_right,_rgba(0,_0,_0,_0),rgba(0,_0,_0,_1)_10%,rgba(0,_0,_0,_1)_90%,rgba(0,_0,_0,_0))]">
+ <div className="group flex overflow-x-hidden py-10 [--gap:2rem] md:[--gap:3rem] [gap:var(--gap)] flex-row max-w-full [mask-image:linear-gradient(to_right,_rgba(0,_0,_0,_0),rgba(0,_0,_0,_1)_10%,rgba(0,_0,_0,_1)_90%,rgba(0,_0,_0,_0))]">Optional: since each logo row is repeated 4 times for the marquee, consider marking the scroller or duplicate rows as decorative (e.g., aria-hidden="true") so screen readers don’t announce the same brand names multiple times.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <BlurFade duration={0.4} delay={0.125 * 10}> | |
| <div className="mt-12"> | |
| <Paragraph>Join {userCount} users saving hours daily</Paragraph> | |
| <div className="group flex overflow-x-hidden py-10 [--gap:2rem] md:[--gap:3rem] [gap:var(--gap))] flex-row max-w-full [mask-image:linear-gradient(to_right,_rgba(0,_0,_0,_0),rgba(0,_0,_0,_1)_10%,rgba(0,_0,_0,_1)_90%,rgba(0,_0,_0,_0))]"> | |
| {new Array(4).fill(0).map((_, i) => ( | |
| <div | |
| className={cx( | |
| "flex shrink-0 justify-around [margin-right:var(--gap)] [gap:var(--gap)] flex-row [--duration:100s] opacity-90", | |
| animate ? "animate-marquee" : "", | |
| )} | |
| key={i} | |
| > | |
| {brandList.map(({ alt, src, height }) => ( | |
| <div className="flex items-start" key={alt}> | |
| <Image | |
| src={src} | |
| alt={alt} | |
| width={100} | |
| height={100} | |
| className={cx("w-auto", height || "h-5 sm:h-6 md:h-8")} | |
| /> | |
| </div> | |
| ))} | |
| </div> | |
| ))} | |
| </div> | |
| <BlurFade duration={0.4} delay={0.125 * 10}> | |
| <div className="mt-12"> | |
| <Paragraph>Join {userCount} users saving hours daily</Paragraph> | |
| <div className="group flex overflow-x-hidden py-10 [--gap:2rem] md:[--gap:3rem] [gap:var(--gap)] flex-row max-w-full [mask-image:linear-gradient(to_right,_rgba(0,_0,_0,_0),rgba(0,_0,_0,_1)_10%,rgba(0,_0,_0,_1)_90%,rgba(0,_0,_0,_0))]"> | |
| {new Array(4).fill(0).map((_, i) => ( | |
| <div | |
| className={cx( | |
| "flex shrink-0 justify-around [margin-right:var(--gap)] [gap:var(--gap)] flex-row [--duration:100s] opacity-90", | |
| animate ? "animate-marquee" : "", | |
| )} | |
| key={i} | |
| > | |
| {brandList.map(({ alt, src, height }) => ( | |
| <div className="flex items-start" key={alt}> | |
| <Image | |
| src={src} | |
| alt={alt} | |
| width={100} | |
| height={100} | |
| className={cx("w-auto", height || "h-5 sm:h-6 md:h-8")} | |
| /> | |
| </div> | |
| ))} | |
| </div> | |
| ))} | |
| </div> |
🤖 Prompt for AI Agents
In apps/web/components/new-landing/BrandScroller.tsx around lines 20-45, the
Tailwind arbitrary gap class is malformed ("[gap:var(--gap))]" with an extra
")") which prevents the intended spacing; replace it with the correct
"[gap:var(--gap)]" and ensure the surrounding class string remains valid;
additionally, mark duplicated marquee rows or the scroller wrapper as decorative
(e.g., add aria-hidden="true" to the repeated rows or to the outer marquee
container) so screen readers do not announce the same brand names multiple
times.
There was a problem hiding this comment.
1 issue found across 9 files (reviewed changes from recent commits).
Prompt for AI agents (all 1 issues)
Understand the root cause of the following 1 issues and fix them.
<file name="apps/web/components/new-landing/BrandScroller.tsx">
<violation number="1" location="apps/web/components/new-landing/BrandScroller.tsx:23">
Malformed Tailwind arbitrary property in the class string: "[gap:var(--gap))]" has an extra ")", so the gap rule won’t apply. Replace with "[gap:var(--gap)]" to fix spacing between marquee items.</violation>
</file>
Reply to cubic to teach it or ask questions. Re-run a review with @cubic-dev-ai review this PR
| <BlurFade duration={0.4} delay={0.125 * 10}> | ||
| <div className="mt-12"> | ||
| <Paragraph>Join {userCount} users saving hours daily</Paragraph> | ||
| <div className="group flex overflow-x-hidden py-10 [--gap:2rem] md:[--gap:3rem] [gap:var(--gap))] flex-row max-w-full [mask-image:linear-gradient(to_right,_rgba(0,_0,_0,_0),rgba(0,_0,_0,_1)_10%,rgba(0,_0,_0,_1)_90%,rgba(0,_0,_0,_0))]"> |
There was a problem hiding this comment.
Malformed Tailwind arbitrary property in the class string: "[gap:var(--gap))]" has an extra ")", so the gap rule won’t apply. Replace with "[gap:var(--gap)]" to fix spacing between marquee items.
Prompt for AI agents
Address the following comment on apps/web/components/new-landing/BrandScroller.tsx at line 23:
<comment>Malformed Tailwind arbitrary property in the class string: "[gap:var(--gap))]" has an extra ")", so the gap rule won’t apply. Replace with "[gap:var(--gap)]" to fix spacing between marquee items.</comment>
<file context>
@@ -16,31 +17,33 @@ export const BrandScroller = ({
+ <BlurFade duration={0.4} delay={0.125 * 10}>
+ <div className="mt-12">
+ <Paragraph>Join {userCount} users saving hours daily</Paragraph>
+ <div className="group flex overflow-x-hidden py-10 [--gap:2rem] md:[--gap:3rem] [gap:var(--gap))] flex-row max-w-full [mask-image:linear-gradient(to_right,_rgba(0,_0,_0,_0),rgba(0,_0,_0,_1)_10%,rgba(0,_0,_0,_1)_90%,rgba(0,_0,_0,_0))]">
+ {new Array(4).fill(0).map((_, i) => (
+ <div
</file context>
| <div className="group flex overflow-x-hidden py-10 [--gap:2rem] md:[--gap:3rem] [gap:var(--gap))] flex-row max-w-full [mask-image:linear-gradient(to_right,_rgba(0,_0,_0,_0),rgba(0,_0,_0,_1)_10%,rgba(0,_0,_0,_1)_90%,rgba(0,_0,_0,_0))]"> | |
| + <div className="group flex overflow-x-hidden py-10 [--gap:2rem] md:[--gap:3rem] [gap:var(--gap)] flex-row max-w-full [mask-image:linear-gradient(to_right,_rgba(0,_0,_0,_0),rgba(0,_0,_0,_1)_10%,rgba(0,_0,_0,_1)_90%,rgba(0,_0,_0,_0))]"> |
There was a problem hiding this comment.
1 issue found across 3 files (reviewed changes from recent commits).
Prompt for AI agents (all 1 issues)
Understand the root cause of the following 1 issues and fix them.
<file name="apps/web/app/(app)/premium/OldPricingLazy.tsx">
<violation number="1" location="apps/web/app/(app)/premium/OldPricingLazy.tsx:6">
Enable Suspense on the dynamic Pricing component so the surrounding <Suspense> boundary functions instead of throwing and ignoring the fallback.</violation>
</file>
Reply to cubic to teach it or ask questions. Re-run a review with @cubic-dev-ai review this PR
| import { Suspense } from "react"; | ||
| import type { PricingProps } from "./Pricing"; | ||
|
|
||
| const PricingComponent = dynamic(() => import("./Pricing")); |
There was a problem hiding this comment.
Enable Suspense on the dynamic Pricing component so the surrounding boundary functions instead of throwing and ignoring the fallback.
Prompt for AI agents
Address the following comment on apps/web/app/(app)/premium/OldPricingLazy.tsx at line 6:
<comment>Enable Suspense on the dynamic Pricing component so the surrounding <Suspense> boundary functions instead of throwing and ignoring the fallback.</comment>
<file context>
@@ -0,0 +1,12 @@
+import { Suspense } from "react";
+import type { PricingProps } from "./Pricing";
+
+const PricingComponent = dynamic(() => import("./Pricing"));
+
+export const OldPricingLazy = (props: PricingProps) => (
</file context>
| const PricingComponent = dynamic(() => import("./Pricing")); | |
| const PricingComponent = dynamic(() => import("./Pricing"), { suspense: true }); |
Summary by CodeRabbit
New Features
Style
Refactor
Removed
Note
Redesigns marketing/landing pages with a new customizable Hero and FAQs, adds Banner and footer variants, refactors BrandScroller and layouts, and updates Pricing lazy import.
Herowith badge support andHeroVideoPlayer; integrated on/withWordRevealandBrandScroller.dt/dd.CTAwithFinalCTAacross pages.PricingLazynow loadscomponents/new-landing/sections/Pricingand no longer forwards props.components/common/Banner.BasicLayoutandBlogLayoutnow use newHeader/Footer;Footersupportsvariant="simple".brandList,animate), displaysuserCount, and uses centralized brands.Paragraphsupportsdt/dltags.OrganizedInbox,PreWrittenDrafts,StartedInMinutesaccepttitle/subtitleprops.utils/brandsexportingBRANDS_LISTand types.Written by Cursor Bugbot for commit 571b2da. Configure here.