Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/components/shared/header/menu-feature-cards/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import MenuFeatureCards from './menu-feature-cards';

export default MenuFeatureCards;
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
import PropTypes from 'prop-types';

import Link from 'components/shared/link';
import ArrowTopRightIcon from 'icons/arrow-right.inline.svg';
import { cn } from 'utils/cn';

const DatabaseIcon = ({ className }) => (
<svg
className={className}
width="28"
height="28"
viewBox="0 0 28 28"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-hidden
>
<ellipse cx="14" cy="7" rx="8" ry="3.5" stroke="currentColor" strokeWidth="1.4" />
<path
d="M6 7v14c0 1.93 3.58 3.5 8 3.5s8-1.57 8-3.5V7"
stroke="currentColor"
strokeWidth="1.4"
/>
<path d="M6 14c0 1.93 3.58 3.5 8 3.5s8-1.57 8-3.5" stroke="currentColor" strokeWidth="1.4" />
</svg>
);

DatabaseIcon.propTypes = {
className: PropTypes.string,
};

const NeonMark = ({ className }) => (
<svg
className={className}
width="28"
height="28"
viewBox="0 0 28 28"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-hidden
>
<rect width="28" height="28" rx="6" className="fill-green-45" />
<path
d="M22.5 4.5v19.5L15.1 17.4v6.6H4.5V4.5h18Zm-16.7 17h7V11.7l7.5 6.7V6.9h-14.5v14.6Z"
className="fill-black-pure"
/>
</svg>
);

NeonMark.propTypes = {
className: PropTypes.string,
};

const AgentsGraphic = () => (
<div className="pointer-events-none absolute top-1/2 right-3 flex -translate-y-1/2 items-center gap-x-2 sm:right-2">
<NeonMark className="shrink-0" />
<svg
width="28"
height="52"
viewBox="0 0 28 52"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-hidden
className="text-gray-new-80 dark:text-gray-new-20"
>
<path d="M0 26h10" stroke="currentColor" strokeWidth="1.4" />
<path d="M10 26v-18h10" stroke="currentColor" strokeWidth="1.4" />
<path d="M10 26h10" stroke="currentColor" strokeWidth="1.4" />
<path d="M10 26v18h10" stroke="currentColor" strokeWidth="1.4" />
</svg>
<div className="flex flex-col gap-y-1.5 text-gray-new-50 dark:text-gray-new-60">
<DatabaseIcon className="text-green-45" />
<DatabaseIcon />
<DatabaseIcon />
</div>
</div>
);

const PLATFORM_TILES = [false, true, false, true, false, false, false, false, true];

const PlatformsGraphic = () => (
<div className="pointer-events-none absolute top-1/2 right-4 grid -translate-y-1/2 grid-cols-3 gap-1.5 sm:right-2">
{PLATFORM_TILES.map((isHighlighted, index) => (
<DatabaseIcon
key={index}
className={cn(
'size-7',
isHighlighted ? 'text-green-45' : 'text-gray-new-80 dark:text-gray-new-30'
)}
/>
))}
</div>
);

const GRAPHICS = {
agents: AgentsGraphic,
platforms: PlatformsGraphic,
};

const MenuFeatureCards = ({
items,
linkProps: { className, ...linkProps } = {},
className: wrapperClassName,
}) => (
<ul
className={cn(
'flex h-full w-[340px] flex-1 flex-col gap-y-3 lg:w-auto md:w-[320px]',
wrapperClassName
)}
>
{items.map(({ title, description, to, isExternal, graphic }) => {
const Graphic = GRAPHICS[graphic];

return (
<li key={title} className="min-h-0 flex-1" role="none">
<Link
className={cn(
'group relative flex h-full min-h-[104px] items-end overflow-hidden rounded border border-gray-new-90 bg-gray-new-98 p-5 pr-32 transition-colors duration-200',
'hover:border-gray-new-80 dark:border-gray-new-15 dark:bg-gray-new-8 dark:hover:border-gray-new-30',
'lg:pr-24 md:pr-20',
className
)}
to={to}
isExternal={isExternal}
tagName="Navigation"
tagText={title}
role="menuitem"
{...linkProps}
>
{Graphic && <Graphic />}
<div className="relative z-10 flex max-w-[200px] flex-col gap-y-2 lg:max-w-[170px]">
<p className="flex items-baseline gap-x-2 text-lg leading-none font-medium tracking-extra-tight text-black-pure dark:text-white">
{title}
<ArrowTopRightIcon className="-translate-x-2 scale-75 text-black-pure opacity-0 transition-[translate,opacity] duration-200 group-hover:translate-x-0 group-hover:scale-100 group-hover:opacity-100 dark:text-white" />
</p>
<p className="text-[13px] leading-tight tracking-snug text-gray-new-40 dark:text-gray-new-60">
{description}
</p>
</div>
</Link>
</li>
);
})}
</ul>
);

MenuFeatureCards.propTypes = {
items: PropTypes.arrayOf(
PropTypes.shape({
title: PropTypes.string.isRequired,
description: PropTypes.string,
to: PropTypes.string.isRequired,
isExternal: PropTypes.bool,
graphic: PropTypes.oneOf(['agents', 'platforms']),
})
).isRequired,
linkProps: Link.propTypes,
className: PropTypes.string,
};

export default MenuFeatureCards;
43 changes: 24 additions & 19 deletions src/components/shared/header/mobile-menu/mobile-menu.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { cn } from 'utils/cn';

import Burger from '../burger';
import MenuBanner from '../menu-banner';
import MenuFeatureCards from '../menu-feature-cards';

const ANIMATION_DURATION = 0.2;

Expand Down Expand Up @@ -60,31 +61,35 @@ const MobileMenuItem = ({ text, to, sections, ...otherProps }) => {
className="flex gap-x-3.5 md:flex-col md:gap-y-9"
>
<ul className="grid grid-cols-[224px,224px] gap-x-5 gap-y-9 pt-3 sm:grid-cols-2 xs:grid-cols-1">
{sections.map(({ title, items }, index) => (
<li key={index}>
{sections.map(({ title, items, variant }, index) => (
<li key={index} className={cn(variant === 'cards' && 'flex flex-col')}>
{title && (
<h3 className="mb-5 text-[10px] leading-none tracking-snug text-gray-new-50 uppercase">
{title}
</h3>
)}
<ul className="flex flex-col gap-5">
{items.map(({ title, description, to, isExternal }) => (
<li key={title}>
<Link
className="grid gap-y-2 text-[13px] leading-tight tracking-snug text-gray-new-40 dark:text-gray-new-60"
to={to}
isExternal={isExternal}
tagName="MobileMenu"
>
<span className="text-lg leading-none font-medium tracking-extra-tight text-black-pure dark:text-white sm:text-base">
{title}
</span>
{variant === 'cards' ? (
<MenuFeatureCards items={items} className="w-full" />
) : (
<ul className="flex flex-col gap-5">
{items.map(({ title, description, to, isExternal }) => (
<li key={title}>
<Link
className="grid gap-y-2 text-[13px] leading-tight tracking-snug text-gray-new-40 dark:text-gray-new-60"
to={to}
isExternal={isExternal}
tagName="MobileMenu"
>
<span className="text-lg leading-none font-medium tracking-extra-tight text-black-pure dark:text-white sm:text-base">
{title}
</span>

{description}
</Link>
</li>
))}
</ul>
{description}
</Link>
</li>
))}
</ul>
)}
</li>
))}
</ul>
Expand Down
74 changes: 45 additions & 29 deletions src/components/shared/header/submenu/submenu.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import MENUS from 'constants/menus.js';
import { cn } from 'utils/cn';

import MenuBanner from '../menu-banner';
import MenuFeatureCards from '../menu-feature-cards';

const Submenu = ({
activeMenuIndex,
Expand Down Expand Up @@ -56,8 +57,12 @@ const Submenu = ({
size="1920"
>
<ul className="flex gap-x-[128px] pl-[195px] xl:gap-x-5 xl:pl-[143px]" role="menu">
{sections.map(({ title, items }, sectionIndex) => (
<li key={sectionIndex} role="none">
{sections.map(({ title, items, variant }, sectionIndex) => (
<li
key={sectionIndex}
className={cn(variant === 'cards' && 'flex flex-col')}
role="none"
>
{title && (
<span
className="mb-6 block text-[10px] leading-none font-medium tracking-snug text-gray-new-50 uppercase"
Expand All @@ -66,33 +71,44 @@ const Submenu = ({
{title}
</span>
)}
<ul
className="flex flex-col gap-y-6"
role="group"
aria-labelledby={
title ? `submenu-${index}-section-${sectionIndex}` : undefined
}
>
{items?.map(({ title, description, to, isExternal }) => (
<li key={title} role="none">
<Link
className={`group ${submenuLinkClassName} -mx-1 -my-3 grid min-w-[224px] gap-y-2 rounded px-1 py-3 text-[13px] leading-tight tracking-snug text-gray-new-40 dark:text-gray-new-60`}
to={to}
isExternal={isExternal}
tagName="Navigation"
tagText={title}
role="menuitem"
tabIndex={isActive ? 0 : -1}
onKeyDown={handleSubmenuNavigation(index)}
>
<span className="text-lg leading-none font-medium text-black-pure transition-colors duration-200 group-hover:text-gray-new-20 dark:text-white dark:group-hover:text-gray-new-80">
{title}
</span>
{description}
</Link>
</li>
))}
</ul>
{variant === 'cards' ? (
<MenuFeatureCards
items={items}
linkProps={{
className: submenuLinkClassName,
tabIndex: isActive ? 0 : -1,
onKeyDown: handleSubmenuNavigation(index),
}}
/>
) : (
<ul
className="flex flex-col gap-y-6"
role="group"
aria-labelledby={
title ? `submenu-${index}-section-${sectionIndex}` : undefined
}
>
{items?.map(({ title, description, to, isExternal }) => (
<li key={title} role="none">
<Link
className={`group ${submenuLinkClassName} -mx-1 -my-3 grid min-w-[224px] gap-y-2 rounded px-1 py-3 text-[13px] leading-tight tracking-snug text-gray-new-40 dark:text-gray-new-60`}
to={to}
isExternal={isExternal}
tagName="Navigation"
tagText={title}
role="menuitem"
tabIndex={isActive ? 0 : -1}
onKeyDown={handleSubmenuNavigation(index)}
>
<span className="text-lg leading-none font-medium text-black-pure transition-colors duration-200 group-hover:text-gray-new-20 dark:text-white dark:group-hover:text-gray-new-80">
{title}
</span>
{description}
</Link>
</li>
))}
</ul>
)}
</li>
))}
</ul>
Expand Down
45 changes: 24 additions & 21 deletions src/constants/menus.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,39 +74,42 @@ export default {
title: 'Use cases',
items: [
{
title: 'Serverless App',
to: `${LINKS.useCases}/serverless-apps`,
description: 'Autoscale with traffic',
title: 'Full-stack apps',
to: `${LINKS.useCases}/full-stack-apps`,
description: 'Deploy backends via your agent',
},
{
title: 'Multi-TB',
to: `${LINKS.useCases}/multi-tb`,
description: 'Scale and restore instantly',
title: 'Branching workflows',
to: `${LINKS.useCases}/branching-workflows`,
description: 'Simplify DB ops to ship faster & safer',
},
{
title: 'Database per tenant',
to: `${LINKS.useCases}/database-per-tenant`,
description: 'Data isolation without overhead',
title: 'Bursty workloads',
to: `${LINKS.useCases}/bursty-workloads`,
description: 'Avoid overprovisioning & optimize performance',
},
{
title: 'Large databases',
to: `${LINKS.useCases}/large-databases`,
description: 'Restore & replicate your DB in seconds',
},
],
},
{
title: 'Build & operate',
title: 'Deploy at scale',
variant: 'cards',
items: [
{
title: 'Platforms',
to: LINKS.platforms,
description: 'Offer Postgres for your users',
},
{
title: 'Dev/Tests',
to: `${LINKS.useCases}/dev-test`,
description: 'Production-like environment',
},
{
title: 'Agents',
to: `${LINKS.useCases}/ai-agents`,
description: 'Build full-stack AI agents',
description: 'Infra for app generation agents - used by Replit, v0, and others',
graphic: 'agents',
},
{
title: 'Platforms',
to: LINKS.platforms,
description: 'Deploy isolated backends for your end users',
graphic: 'platforms',
},
],
},
Expand Down