[WIP] feat: homepage and use case pages redesign (#11873)

* feat: connect homepage and use case pages

* fix: internalLink usage

* fix: query name

* chore: add homepage patterns

* chore: remove offerings

* chore: add intro features

* chore: bump subnav

* chore: updating patterns

* chore: add use case to the subnav

* chore: cleanup unused import

* chore: remove subnav border
This commit is contained in:
Alex Carpenter
2022-05-03 09:06:00 -04:00
committed by GitHub
parent d3f26a5536
commit e0ca2f4fd4
75 changed files with 7217 additions and 3259 deletions

View File

@@ -0,0 +1,82 @@
import * as React from 'react'
import classNames from 'classnames'
import Button from '@hashicorp/react-button'
import IoCard, { IoCardProps } from 'components/io-card'
import s from './style.module.css'
interface IoCardContaianerProps {
theme?: 'light' | 'dark'
heading?: string
description?: string
label?: string
cta?: {
url: string
text: string
}
cardsPerRow: 3 | 4
cards: Array<IoCardProps>
}
export default function IoCardContaianer({
theme = 'light',
heading,
description,
label,
cta,
cardsPerRow = 3,
cards,
}: IoCardContaianerProps): React.ReactElement {
return (
<div className={classNames(s.cardContainer, s[theme])}>
{heading || description ? (
<header className={s.header}>
{heading ? <h2 className={s.heading}>{heading}</h2> : null}
{description ? <p className={s.description}>{description}</p> : null}
</header>
) : null}
{cards.length ? (
<>
{label || cta ? (
<header className={s.subHeader}>
{label ? <h3 className={s.label}>{label}</h3> : null}
{cta ? (
<Button
title={cta.text}
url={cta.url}
linkType="inbound"
theme={{
brand: 'neutral',
variant: 'tertiary',
background: theme,
}}
/>
) : null}
</header>
) : null}
<ul
className={classNames(
s.cardList,
cardsPerRow === 3 && s.threeUp,
cardsPerRow === 4 && s.fourUp
)}
style={
{
'--length': cards.length,
} as React.CSSProperties
}
>
{cards.map((card, index) => {
return (
// Index is stable
// eslint-disable-next-line react/no-array-index-key
<li key={index}>
<IoCard variant={theme} {...card} />
</li>
)
})}
</ul>
</>
) : null}
</div>
)
}

View File

@@ -0,0 +1,114 @@
.cardContainer {
position: relative;
& + .cardContainer {
margin-top: 64px;
@media (--medium-up) {
margin-top: 132px;
}
}
}
.header {
margin: 0 auto 64px;
text-align: center;
max-width: 600px;
}
.heading {
margin: 0;
composes: g-type-display-2 from global;
@nest .dark & {
color: var(--white);
}
}
.description {
margin: 8px 0 0;
composes: g-type-body-large from global;
@nest .dark & {
color: var(--gray-5);
}
}
.subHeader {
margin: 0 0 32px;
display: flex;
align-items: center;
justify-content: space-between;
@nest .dark & {
color: var(--gray-5);
}
}
.label {
margin: 0;
composes: g-type-display-4 from global;
}
.cardList {
list-style: none;
--minCol: 250px;
--columns: var(--length);
position: relative;
gap: 32px;
padding: 0;
@media (--small) {
display: flex;
overflow-x: auto;
-ms-overflow-style: none;
scrollbar-width: none;
margin: 0;
padding: 6px 24px;
left: 50%;
margin-left: -50vw;
width: 100vw;
/* This is to ensure there is overflow padding right on mobile. */
&::after {
content: '';
display: block;
width: 1px;
flex-shrink: 0;
}
}
@media (--medium-up) {
display: grid;
grid-template-columns: repeat(var(--columns), minmax(var(--minCol), 1fr));
}
&.threeUp {
@media (--medium-up) {
--columns: 3;
--minCol: 0;
}
}
&.fourUp {
@media (--medium-up) {
--columns: 3;
--minCol: 0;
}
@media (--large) {
--columns: 4;
}
}
& > li {
display: flex;
@media (--small) {
flex-shrink: 0;
width: 250px;
}
}
}

View File

@@ -0,0 +1,124 @@
import * as React from 'react'
import Link from 'next/link'
import InlineSvg from '@hashicorp/react-inline-svg'
import classNames from 'classnames'
import { IconArrowRight24 } from '@hashicorp/flight-icons/svg-react/arrow-right-24'
import { IconExternalLink24 } from '@hashicorp/flight-icons/svg-react/external-link-24'
import { productLogos } from './product-logos'
import s from './style.module.css'
export interface IoCardProps {
variant?: 'light' | 'gray' | 'dark'
products?: Array<{
name: keyof typeof productLogos
}>
link: {
url: string
type: 'inbound' | 'outbound'
}
inset?: 'none' | 'sm' | 'md'
eyebrow?: string
heading?: string
description?: string
children?: React.ReactNode
}
function IoCard({
variant = 'light',
products,
link,
inset = 'md',
eyebrow,
heading,
description,
children,
}: IoCardProps): React.ReactElement {
const LinkWrapper = ({ className, children }) =>
link.type === 'inbound' ? (
<Link href={link.url}>
<a className={className}>{children}</a>
</Link>
) : (
<a
className={className}
href={link.url}
target="_blank"
rel="noopener noreferrer"
>
{children}
</a>
)
return (
<article className={classNames(s.card)}>
<LinkWrapper className={classNames(s[variant], s[inset])}>
{children ? (
children
) : (
<>
{eyebrow ? <Eyebrow>{eyebrow}</Eyebrow> : null}
{heading ? <Heading>{heading}</Heading> : null}
{description ? <Description>{description}</Description> : null}
</>
)}
<footer className={s.footer}>
{products && (
<ul className={s.products}>
{products.map(({ name }, index) => {
const key = name.toLowerCase()
const version = variant === 'dark' ? 'neutral' : 'color'
return (
// eslint-disable-next-line react/no-array-index-key
<li key={index}>
<InlineSvg
className={s.logo}
src={productLogos[key][version]}
/>
</li>
)
})}
</ul>
)}
<span className={s.linkType}>
{link.type === 'inbound' ? (
<IconArrowRight24 />
) : (
<IconExternalLink24 />
)}
</span>
</footer>
</LinkWrapper>
</article>
)
}
interface EyebrowProps {
children: string
}
function Eyebrow({ children }: EyebrowProps) {
return <p className={s.eyebrow}>{children}</p>
}
interface HeadingProps {
as?: 'h2' | 'h3' | 'h4'
children: React.ReactNode
}
function Heading({ as: Component = 'h2', children }: HeadingProps) {
return <Component className={s.heading}>{children}</Component>
}
interface DescriptionProps {
children: string
}
function Description({ children }: DescriptionProps) {
return <p className={s.description}>{children}</p>
}
IoCard.Eyebrow = Eyebrow
IoCard.Heading = Heading
IoCard.Description = Description
export default IoCard

View File

@@ -0,0 +1,34 @@
export const productLogos = {
boundary: {
color: require('@hashicorp/mktg-logos/product/boundary/logomark/color.svg?include'),
neutral: require('@hashicorp/mktg-logos/product/boundary/logomark/white.svg?include'),
},
consul: {
color: require('@hashicorp/mktg-logos/product/consul/logomark/color.svg?include'),
neutral: require('@hashicorp/mktg-logos/product/consul/logomark/white.svg?include'),
},
nomad: {
color: require('@hashicorp/mktg-logos/product/nomad/logomark/color.svg?include'),
neutral: require('@hashicorp/mktg-logos/product/nomad/logomark/white.svg?include'),
},
packer: {
color: require('@hashicorp/mktg-logos/product/packer/logomark/color.svg?include'),
neutral: require('@hashicorp/mktg-logos/product/packer/logomark/white.svg?include'),
},
terraform: {
color: require('@hashicorp/mktg-logos/product/terraform/logomark/color.svg?include'),
neutral: require('@hashicorp/mktg-logos/product/terraform/logomark/white.svg?include'),
},
vagrant: {
color: require('@hashicorp/mktg-logos/product/vagrant/logomark/color.svg?include'),
neutral: require('@hashicorp/mktg-logos/product/vagrant/logomark/white.svg?include'),
},
vault: {
color: require('@hashicorp/mktg-logos/product/vault/logomark/color.svg?include'),
neutral: require('@hashicorp/mktg-logos/product/vault/logomark/white.svg?include'),
},
waypoint: {
color: require('@hashicorp/mktg-logos/product/waypoint/logomark/color.svg?include'),
neutral: require('@hashicorp/mktg-logos/product/waypoint/logomark/white.svg?include'),
},
}

View File

@@ -0,0 +1,148 @@
.card {
/* Radii */
--token-radius: 6px;
/* Spacing */
--token-spacing-03: 8px;
--token-spacing-04: 16px;
--token-spacing-05: 24px;
--token-spacing-06: 32px;
/* Elevations */
--token-elevation-mid: 0 2px 3px rgba(101, 106, 118, 0.1),
0 8px 16px -10px rgba(101, 106, 118, 0.2);
--token-elevation-high: 0 2px 3px rgba(101, 106, 118, 0.15),
0 16px 16px -10px rgba(101, 106, 118, 0.2);
/* Transition */
--token-transition: ease-in-out 0.2s;
display: flex;
flex-direction: column;
flex-grow: 1;
min-height: 300px;
& a {
display: flex;
flex-direction: column;
flex-grow: 1;
border-radius: var(--token-radius);
box-shadow: 0 0 0 1px rgba(38, 53, 61, 0.1), var(--token-elevation-mid);
transition: var(--token-transition);
transition-property: background-color, box-shadow;
&:hover {
box-shadow: 0 0 0 2px rgba(38, 53, 61, 0.15), var(--token-elevation-high);
cursor: pointer;
}
/* Variants */
&.dark {
background-color: var(--gray-1);
&:hover {
background-color: var(--gray-2);
}
}
&.gray {
background-color: #f9f9fa;
}
&.light {
background-color: var(--white);
}
/* Spacing */
&.none {
padding: 0;
}
&.sm {
padding: var(--token-spacing-05);
}
&.md {
padding: var(--token-spacing-06);
}
}
}
.eyebrow {
margin: 0;
composes: g-type-label-small from global;
color: var(--gray-3);
@nest .dark & {
color: var(--gray-5);
}
}
.heading {
margin: 0;
composes: g-type-display-5 from global;
color: var(--black);
@nest * + & {
margin-top: var(--token-spacing-05);
}
@nest .dark & {
color: var(--white);
}
}
.description {
margin: 0;
composes: g-type-body-small from global;
color: var(--gray-3);
@nest * + & {
margin-top: var(--token-spacing-03);
}
@nest .dark & {
color: var(--gray-5);
}
}
.footer {
margin-top: auto;
display: flex;
justify-content: space-between;
align-items: flex-end;
padding-top: 32px;
}
.products {
display: flex;
gap: 8px;
margin: 0;
padding: 0;
& > li {
width: 32px;
height: 32px;
display: grid;
place-items: center;
}
& .logo {
display: flex;
& svg {
width: 32px;
height: 32px;
}
}
}
.linkType {
margin-left: auto;
display: flex;
color: var(--black);
@nest .dark & {
color: var(--white);
}
}

View File

@@ -0,0 +1,47 @@
import * as React from 'react'
import { DialogOverlay, DialogContent, DialogOverlayProps } from '@reach/dialog'
import { AnimatePresence, motion } from 'framer-motion'
import s from './style.module.css'
export interface IoDialogProps extends DialogOverlayProps {
label: string
}
export default function IoDialog({
isOpen,
onDismiss,
children,
label,
}: IoDialogProps): React.ReactElement {
const AnimatedDialogOverlay = motion(DialogOverlay)
return (
<AnimatePresence>
{isOpen && (
<AnimatedDialogOverlay
className={s.dialogOverlay}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onDismiss={onDismiss}
>
<div className={s.dialogWrapper}>
<motion.div
initial={{ y: 50 }}
animate={{ y: 0 }}
exit={{ y: 50 }}
transition={{ min: 0, max: 100, bounceDamping: 8 }}
style={{ width: '100%', maxWidth: 800 }}
>
<DialogContent className={s.dialogContent} aria-label={label}>
<button onClick={onDismiss} className={s.dialogClose}>
Close
</button>
{children}
</DialogContent>
</motion.div>
</div>
</AnimatedDialogOverlay>
)}
</AnimatePresence>
)
}

View File

@@ -0,0 +1,62 @@
.dialogOverlay {
background-color: rgba(0, 0, 0, 0.75);
height: 100%;
left: 0;
overflow-y: auto;
position: fixed;
top: 0;
width: 100%;
z-index: 666666667 /* higher than global nav */;
}
.dialogWrapper {
display: grid;
min-height: 100vh;
padding: 24px;
place-items: center;
}
.dialogContent {
background-color: var(--gray-1);
color: var(--white);
max-width: 800px;
outline: none;
overflow-y: auto;
padding: 24px;
position: relative;
width: 100%;
@media (min-width: 768px) {
padding: 48px;
}
}
.dialogClose {
appearance: none;
background-color: transparent;
border: 0;
composes: g-type-display-5 from global;
cursor: pointer;
margin: 0;
padding: 0;
position: absolute;
color: var(--white);
right: 24px;
top: 24px;
z-index: 1;
@media (min-width: 768px) {
right: 48px;
top: 48px;
}
@nest html[dir='rtl'] & {
left: 24px;
right: auto;
@media (min-width: 768px) {
left: 48px;
right: auto;
}
}
}

View File

@@ -0,0 +1,39 @@
import ReactCallToAction from '@hashicorp/react-call-to-action'
import { Products } from '@hashicorp/platform-product-meta'
import s from './style.module.css'
interface IoHomeCallToActionProps {
brand: Products
heading: string
content: string
links: Array<{
text: string
url: string
}>
}
export default function IoHomeCallToAction({
brand,
heading,
content,
links,
}: IoHomeCallToActionProps) {
return (
<div className={s.callToAction}>
<ReactCallToAction
variant="compact"
heading={heading}
content={content}
product={brand}
theme="dark"
links={links.map(({ text, url }, index) => {
return {
text,
url,
type: index === 1 ? 'inbound' : null,
}
})}
/>
</div>
)
}

View File

@@ -0,0 +1,12 @@
.callToAction {
margin: 60px auto;
background-image: linear-gradient(52.3deg, #2c2d2f 39.83%, #626264 96.92%);
@media (--medium-up) {
margin: 120px auto;
}
& > * {
background-color: transparent;
}
}

View File

@@ -0,0 +1,80 @@
import * as React from 'react'
import Image from 'next/image'
import { isInternalLink } from 'lib/utils'
import { IconExternalLink16 } from '@hashicorp/flight-icons/svg-react/external-link-16'
import { IconArrowRight16 } from '@hashicorp/flight-icons/svg-react/arrow-right-16'
import s from './style.module.css'
interface IoHomeCaseStudiesProps {
heading: string
description: string
primary: Array<{
thumbnail: {
url: string
alt: string
}
link: string
heading: string
}>
secondary: Array<{
link: string
heading: string
}>
}
export default function IoHomeCaseStudies({
heading,
description,
primary,
secondary,
}: IoHomeCaseStudiesProps): React.ReactElement {
return (
<section className={s.root}>
<div className={s.container}>
<header className={s.header}>
<h2 className={s.heading}>{heading}</h2>
<p className={s.description}>{description}</p>
</header>
<div className={s.caseStudies}>
<ul className={s.primary}>
{primary.map((item, index) => {
return (
<li key={index} className={s.primaryItem}>
<a className={s.card} href={item.link}>
<h3 className={s.cardHeading}>{item.heading}</h3>
<Image
className={s.cardThumbnail}
src={item.thumbnail.url}
layout="fill"
objectFit="cover"
alt={item.thumbnail.alt}
/>
</a>
</li>
)
})}
</ul>
<ul className={s.secondary}>
{secondary.map((item, index) => {
return (
<li key={index} className={s.secondaryItem}>
<a className={s.link} href={item.link}>
<span className={s.linkInner}>
<h3 className={s.linkHeading}>{item.heading}</h3>
{isInternalLink(item.link) ? (
<IconArrowRight16 />
) : (
<IconExternalLink16 />
)}
</span>
</a>
</li>
)
})}
</ul>
</div>
</div>
</section>
)
}

View File

@@ -0,0 +1,171 @@
.root {
position: relative;
margin: 0 auto;
margin: 60px auto;
max-width: 1600px;
@media (--medium-up) {
margin: 120px auto;
}
}
.container {
composes: g-grid-container from global;
}
.header {
margin-bottom: 32px;
@media (--medium-up) {
max-width: calc(100% * 5 / 12);
}
}
.heading {
margin: 0;
composes: g-type-display-3 from global;
}
.description {
margin: 8px 0 0;
composes: g-type-body from global;
color: var(--gray-3);
}
.caseStudies {
--columns: 1;
display: grid;
grid-template-columns: repeat(var(--columns), minmax(0, 1fr));
gap: 32px;
@media (--medium-up) {
--columns: 12;
}
}
.primary {
--columns: 1;
grid-column: 1 / -1;
list-style: none;
margin: 0;
padding: 0;
display: grid;
grid-template-columns: repeat(var(--columns), minmax(0, 1fr));
gap: 32px;
@media (--medium-up) {
--columns: 2;
}
@media (--large) {
grid-column: 1 / 9;
}
}
.primaryItem {
display: flex;
}
.card {
position: relative;
overflow: hidden;
display: flex;
flex-direction: column;
flex-grow: 1;
justify-content: flex-end;
padding: 32px;
box-shadow: 0 8px 16px -10px rgba(101, 106, 118, 0.2);
background-color: #000;
border-radius: 6px;
color: var(--white);
transition: ease-in-out 0.2s;
transition-property: box-shadow;
min-height: 300px;
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 10;
border-radius: 6px;
background-image: linear-gradient(
to bottom,
rgba(0, 0, 0, 0),
rgba(0, 0, 0, 0.45)
);
transition: opacity ease-in-out 0.2s;
}
&:hover {
box-shadow: 0 2px 3px rgba(101, 106, 118, 0.15),
0 16px 16px -10px rgba(101, 106, 118, 0.2);
&::before {
opacity: 0.75;
}
}
}
.cardThumbnail {
transition: transform 0.4s;
@nest .card:hover & {
transform: scale(1.04);
}
}
.cardHeading {
margin: 0;
composes: g-type-display-4 from global;
z-index: 10;
}
.secondary {
grid-column: 1 / -1;
list-style: none;
margin: 0;
padding: 0;
@media (--large) {
margin-top: -32px;
grid-column: 9 / -1;
}
}
.secondaryItem {
border-bottom: 1px solid var(--gray-5);
}
.link {
display: flex;
width: 100%;
color: var(--black);
}
.linkInner {
display: flex;
width: 100%;
justify-content: space-between;
padding-top: 32px;
padding-bottom: 32px;
transition: transform ease-in-out 0.2s;
@nest .link:hover & {
transform: translateX(4px);
}
& svg {
margin-top: 6px;
flex-shrink: 0;
}
}
.linkHeading {
margin: 0 32px 0 0;
composes: g-type-display-6 from global;
}

View File

@@ -0,0 +1,71 @@
import * as React from 'react'
import Image from 'next/image'
import Link from 'next/link'
import { isInternalLink } from 'lib/utils'
import { IconArrowRight16 } from '@hashicorp/flight-icons/svg-react/arrow-right-16'
import s from './style.module.css'
export interface IoHomeFeatureProps {
isInternalLink: (link: string) => boolean
link?: string
image: {
url: string
alt: string
}
heading: string
description: string
}
export default function IoHomeFeature({
isInternalLink,
link,
image,
heading,
description,
}: IoHomeFeatureProps): React.ReactElement {
return (
<IoHomeFeatureWrap isInternalLink={isInternalLink} href={link}>
<div className={s.featureMedia}>
<Image
src={image.url}
width={400}
height={200}
layout="responsive"
alt={image.alt}
/>
</div>
<div className={s.featureContent}>
<h3 className={s.featureHeading}>{heading}</h3>
<p className={s.featureDescription}>{description}</p>
{link ? (
<span className={s.featureCta} aria-hidden={true}>
Learn more{' '}
<span>
<IconArrowRight16 />
</span>
</span>
) : null}
</div>
</IoHomeFeatureWrap>
)
}
function IoHomeFeatureWrap({ isInternalLink, href, children }) {
if (!href) {
return <div className={s.feature}>{children}</div>
}
if (isInternalLink(href)) {
return (
<Link href={href}>
<a className={s.feature}>{children}</a>
</Link>
)
}
return (
<a className={s.feature} href={href}>
{children}
</a>
)
}

View File

@@ -0,0 +1,79 @@
.feature {
display: flex;
align-items: center;
flex-direction: column;
padding: 32px;
gap: 24px 64px;
border-radius: 6px;
background-color: #f9f9fa;
color: var(--black);
box-shadow: 0 2px 3px rgba(101, 106, 118, 0.1),
0 8px 16px -10px rgba(101, 106, 118, 0.2);
@media (--medium-up) {
flex-direction: row;
}
}
.featureLink {
transition: box-shadow ease-in-out 0.2s;
&:hover {
box-shadow: 0 2px 3px rgba(101, 106, 118, 0.15),
0 16px 16px -10px rgba(101, 106, 118, 0.2);
}
}
.featureMedia {
flex-shrink: 0;
display: flex;
width: 100%;
border-radius: 6px;
overflow: hidden;
border: 1px solid var(--gray-5);
@media (--medium-up) {
width: 300px;
}
@media (--large) {
width: 400px;
}
& > * {
width: 100%;
}
}
.featureContent {
max-width: 520px;
}
.featureHeading {
margin: 0;
composes: g-type-display-4 from global;
}
.featureDescription {
margin: 8px 0 24px;
composes: g-type-body-small from global;
color: var(--gray-3);
}
.featureCta {
display: inline-flex;
align-items: center;
& > span {
display: flex;
margin-left: 12px;
& > svg {
transition: transform 0.2s;
}
}
@nest .feature:hover & span svg {
transform: translateX(2px);
}
}

View File

@@ -0,0 +1,135 @@
import * as React from 'react'
import { Products } from '@hashicorp/platform-product-meta'
import Button from '@hashicorp/react-button'
import classNames from 'classnames'
import s from './style.module.css'
interface IoHomeHeroProps {
pattern: string
brand: Products | 'neutral'
heading: string
description: string
ctas: Array<{
title: string
link: string
}>
cards: Array<IoHomeHeroCardProps>
}
export default function IoHomeHero({
pattern,
brand,
heading,
description,
ctas,
cards,
}: IoHomeHeroProps) {
const [loaded, setLoaded] = React.useState(false)
React.useEffect(() => {
setTimeout(() => {
setLoaded(true)
}, 250)
}, [])
return (
<header
className={classNames(s.hero, loaded && s.loaded)}
style={
{
'--pattern': `url(${pattern})`,
} as React.CSSProperties
}
>
<span className={s.pattern} />
<div className={s.container}>
<div className={s.content}>
<h1 className={s.heading}>{heading}</h1>
<p className={s.description}>{description}</p>
{ctas && (
<div className={s.ctas}>
{ctas.map((cta, index) => {
return (
<Button
key={index}
title={cta.title}
url={cta.link}
linkType="inbound"
theme={{
brand: 'neutral',
variant: 'tertiary',
background: 'light',
}}
/>
)
})}
</div>
)}
</div>
{cards && (
<div className={s.cards}>
{cards.map((card, index) => {
return (
<IoHomeHeroCard
key={index}
index={index}
heading={card.heading}
description={card.description}
cta={{
brand: index === 0 ? 'neutral' : brand,
title: card.cta.title,
link: card.cta.link,
}}
subText={card.subText}
/>
)
})}
</div>
)}
</div>
</header>
)
}
interface IoHomeHeroCardProps {
index?: number
heading: string
description: string
cta: {
title: string
link: string
brand?: 'neutral' | Products
}
subText: string
}
function IoHomeHeroCard({
index,
heading,
description,
cta,
subText,
}: IoHomeHeroCardProps): React.ReactElement {
return (
<article
className={s.card}
style={
{
'--index': index,
} as React.CSSProperties
}
>
<h2 className={s.cardHeading}>{heading}</h2>
<p className={s.cardDescription}>{description}</p>
<Button
title={cta.title}
url={cta.link}
theme={{
variant: 'primary',
brand: cta.brand,
}}
/>
<p className={s.cardSubText}>{subText}</p>
</article>
)
}

View File

@@ -0,0 +1,148 @@
.hero {
position: relative;
padding-top: 64px;
padding-bottom: 64px;
background: linear-gradient(180deg, #f9f9fa 0%, #fff 28.22%, #fff 100%);
@media (--medium-up) {
padding-top: 128px;
padding-bottom: 128px;
}
}
.pattern {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
max-width: 1600px;
width: 100%;
margin: auto;
@media (--medium-up) {
background-image: var(--pattern);
background-repeat: no-repeat;
background-position: top right;
}
}
.container {
--columns: 1;
composes: g-grid-container from global;
display: grid;
grid-template-columns: repeat(var(--columns), minmax(0, 1fr));
gap: 48px 32px;
@media (--medium-up) {
--columns: 12;
}
}
.content {
grid-column: 1 / -1;
@media (--medium-up) {
grid-column: 1 / 6;
}
& > * {
max-width: 415px;
}
}
.heading {
margin: 0;
composes: g-type-display-1 from global;
}
.description {
margin: 8px 0 0;
composes: g-type-body-small from global;
color: var(--gray-3);
}
.ctas {
margin-top: 24px;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 24px;
}
.cards {
--columns: 1;
grid-column: 1 / -1;
align-self: start;
display: grid;
grid-template-columns: repeat(var(--columns), minmax(0, 1fr));
gap: 32px;
@media (min-width: 600px) {
--columns: 2;
}
@media (--medium-up) {
--columns: 1;
grid-column: 7 / -1;
}
@media (--large) {
--columns: 2;
grid-column: 6 / -1;
}
}
.card {
--token-radius: 6px;
--token-elevation-mid: 0 2px 3px rgba(101, 106, 118, 0.1),
0 8px 16px -10px rgba(101, 106, 118, 0.2);
opacity: 0;
padding: 40px 32px;
display: flex;
align-items: flex-start;
flex-direction: column;
flex-grow: 1;
background-color: var(--white);
border-radius: var(--token-radius);
box-shadow: 0 0 0 1px rgba(38, 53, 61, 0.1), var(--token-elevation-mid);
@nest .loaded & {
animation-name: slideIn;
animation-duration: 0.5s;
animation-delay: calc(var(--index) * 0.1s);
animation-fill-mode: forwards;
}
}
.cardHeading {
margin: 0;
composes: g-type-display-4 from global;
}
.cardDescription {
margin: 8px 0 16px;
composes: g-type-display-6 from global;
}
.cardSubText {
margin: 32px 0 0;
composes: g-type-body-small from global;
color: var(--gray-3);
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(50px);
}
to {
opacity: 1;
transform: translateY(0);
}
}

View File

@@ -0,0 +1,86 @@
import * as React from 'react'
import Image from 'next/image'
import Button from '@hashicorp/react-button'
import { Products } from '@hashicorp/platform-product-meta'
import { IoCardProps } from 'components/io-card'
import IoCardContainer from 'components/io-card-container'
import s from './style.module.css'
interface IoHomeInPracticeProps {
brand: Products
pattern: string
heading: string
description: string
cards: Array<IoCardProps>
cta: {
heading: string
description: string
link: string
image: {
url: string
alt: string
width: number
height: number
}
}
}
export default function IoHomeInPractice({
brand,
pattern,
heading,
description,
cards,
cta,
}: IoHomeInPracticeProps) {
return (
<section
className={s.inPractice}
style={
{
'--pattern': `url(${pattern})`,
} as React.CSSProperties
}
>
<div className={s.container}>
<IoCardContainer
theme="dark"
heading={heading}
description={description}
cardsPerRow={3}
cards={cards}
/>
{cta.heading ? (
<div className={s.inPracticeCta}>
<div className={s.inPracticeCtaContent}>
<h3 className={s.inPracticeCtaHeading}>{cta.heading}</h3>
{cta.description ? (
<p className={s.inPracticeCtaDescription}>{cta.description}</p>
) : null}
{cta.link ? (
<Button
title="Learn more"
url={cta.link}
theme={{
brand: brand,
}}
/>
) : null}
</div>
{cta.image?.url ? (
<div className={s.inPracticeCtaMedia}>
<Image
src={cta.image.url}
width={cta.image.width}
height={cta.image.height}
alt={cta.image.alt}
/>
</div>
) : null}
</div>
) : null}
</div>
</section>
)
}

View File

@@ -0,0 +1,98 @@
.inPractice {
position: relative;
margin: 60px auto;
padding: 64px 0;
max-width: 1600px;
@media (--medium-up) {
padding: 80px 0;
margin: 120px auto;
}
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: var(--black);
background-image: url('/img/practice-pattern.svg');
background-repeat: no-repeat;
background-size: 50%;
background-position: top 200px left;
@media (--large) {
border-radius: 6px;
left: 24px;
right: 24px;
background-size: 35%;
background-position: top 64px left;
}
}
}
.container {
composes: g-grid-container from global;
}
.inPracticeCta {
--columns: 1;
position: relative;
margin-top: 64px;
display: grid;
grid-template-columns: repeat(var(--columns), minmax(0, 1fr));
gap: 64px 32px;
@media (--medium-up) {
--columns: 12;
}
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
bottom: -64px;
background-image: radial-gradient(
42.33% 42.33% at 50% 100%,
#363638 0%,
#000 100%
);
@media (--medium-up) {
bottom: -80px;
}
}
}
.inPracticeCtaContent {
position: relative;
grid-column: 1 / -1;
@media (--medium-up) {
grid-column: 1 / 5;
}
}
.inPracticeCtaMedia {
grid-column: 1 / -1;
@media (--medium-up) {
grid-column: 6 / -1;
}
}
.inPracticeCtaHeading {
margin: 0;
color: var(--white);
composes: g-type-display-3 from global;
}
.inPracticeCtaDescription {
margin: 8px 0 32px;
color: var(--gray-5);
composes: g-type-body from global;
}

View File

@@ -0,0 +1,151 @@
import * as React from 'react'
import Image from 'next/image'
import classNames from 'classnames'
import { Products } from '@hashicorp/platform-product-meta'
import Button from '@hashicorp/react-button'
import IoVideoCallout, {
IoHomeVideoCalloutProps,
} from 'components/io-video-callout'
import IoHomeFeature, { IoHomeFeatureProps } from 'components/io-home-feature'
import s from './style.module.css'
interface IoHomeIntroProps {
isInternalLink: (link: string) => boolean
brand: Products
heading: string
description: string
features?: Array<IoHomeFeatureProps>
offerings?: {
image: {
src: string
width: number
height: number
alt: string
}
list: Array<{
heading: string
description: string
}>
cta?: {
title: string
link: string
}
}
video?: IoHomeVideoCalloutProps
}
export default function IoHomeIntro({
isInternalLink,
brand,
heading,
description,
features,
offerings,
video,
}: IoHomeIntroProps) {
return (
<section
className={classNames(
s.root,
s[brand],
features && s.withFeatures,
offerings && s.withOfferings
)}
style={
{
'--brand': `var(--${brand})`,
} as React.CSSProperties
}
>
<header className={s.header}>
<div className={s.container}>
<div className={s.headerInner}>
<h2 className={s.heading}>{heading}</h2>
<p className={s.description}>{description}</p>
</div>
</div>
</header>
{features ? (
<ul className={s.features}>
{features.map((feature, index) => {
return (
// Index is stable
// eslint-disable-next-line react/no-array-index-key
<li key={index}>
<div className={s.container}>
<IoHomeFeature
isInternalLink={isInternalLink}
image={{
url: feature.image.url,
alt: feature.image.alt,
}}
heading={feature.heading}
description={feature.description}
link={feature.link}
/>
</div>
</li>
)
})}
</ul>
) : null}
{offerings ? (
<div className={s.offerings}>
{offerings.image ? (
<div className={s.offeringsMedia}>
<Image
src={offerings.image.src}
width={offerings.image.width}
height={offerings.image.height}
alt={offerings.image.alt}
/>
</div>
) : null}
<div className={s.offeringsContent}>
<ul className={s.offeringsList}>
{offerings.list.map((offering, index) => {
return (
// Index is stable
// eslint-disable-next-line react/no-array-index-key
<li key={index}>
<h3 className={s.offeringsListHeading}>
{offering.heading}
</h3>
<p className={s.offeringsListDescription}>
{offering.description}
</p>
</li>
)
})}
</ul>
{offerings.cta ? (
<div className={s.offeringsCta}>
<Button
title={offerings.cta.title}
url={offerings.cta.link}
theme={{
brand: 'neutral',
}}
/>
</div>
) : null}
</div>
</div>
) : null}
{video.youtubeId && video.thumbnail ? (
<div className={s.video}>
<IoVideoCallout
youtubeId={video.youtubeId}
thumbnail={video.thumbnail}
heading={video.heading}
description={video.description}
person={video.person}
/>
</div>
) : null}
</section>
)
}

View File

@@ -0,0 +1,169 @@
.root {
position: relative;
margin-bottom: 60px;
@media (--medium-up) {
margin-bottom: 120px;
}
&.withOfferings:not(.withFeatures)::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-image: radial-gradient(
93.55% 93.55% at 50% 0%,
var(--gray-6) 0%,
rgba(242, 242, 243, 0) 100%
);
@media (--large) {
border-radius: 6px;
left: 24px;
right: 24px;
}
}
}
.container {
composes: g-grid-container from global;
}
.header {
padding-top: 64px;
padding-bottom: 64px;
text-align: center;
@nest .withFeatures & {
background-color: var(--brand);
}
@nest .withFeatures.consul & {
color: var(--white);
}
}
.headerInner {
margin: auto;
@media (--medium-up) {
max-width: calc(100% * 7 / 12);
}
}
.heading {
margin: 0;
composes: g-type-display-2 from global;
}
.description {
margin: 24px 0 0;
composes: g-type-body-large from global;
@nest .withOfferings:not(.withFeatures) & {
color: var(--gray-3);
}
}
/*
* Features
*/
.features {
list-style: none;
margin: 0;
padding: 0;
display: grid;
gap: 32px;
& li:first-of-type {
background-image: linear-gradient(
to bottom,
var(--brand) 50%,
var(--white) 50%
);
}
}
/*
* Offerings
*/
.offerings {
--columns: 1;
composes: g-grid-container from global;
display: grid;
grid-template-columns: repeat(var(--columns), minmax(0, 1fr));
gap: 64px 32px;
@media (--medium-up) {
--columns: 12;
}
@nest .features + & {
margin-top: 60px;
@media (--medium-up) {
margin-top: 120px;
}
}
}
.offeringsMedia {
grid-column: 1 / -1;
@media (--medium-up) {
grid-column: 1 / 6;
}
}
.offeringsContent {
grid-column: 1 / -1;
@media (--medium-up) {
grid-column: 7 / -1;
}
}
.offeringsList {
list-style: none;
margin: 0;
padding: 0;
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 32px;
@media (--small) {
grid-template-columns: repeat(1, 1fr);
}
}
.offeringsListHeading {
margin: 0;
composes: g-type-display-4 from global;
}
.offeringsListDescription {
margin: 16px 0 0;
composes: g-type-body-small from global;
}
.offeringsCta {
margin-top: 48px;
}
/*
* Video
*/
.video {
margin-top: 60px;
composes: g-grid-container from global;
@media (--medium-up) {
margin-top: 120px;
}
}

View File

@@ -0,0 +1,79 @@
import * as React from 'react'
import classNames from 'classnames'
import { Products } from '@hashicorp/platform-product-meta'
import { IconArrowRight16 } from '@hashicorp/flight-icons/svg-react/arrow-right-16'
import s from './style.module.css'
interface IoHomePreFooterProps {
brand: Products
heading: string
description: string
ctas: [IoHomePreFooterCard, IoHomePreFooterCard, IoHomePreFooterCard]
}
export default function IoHomePreFooter({
brand,
heading,
description,
ctas,
}: IoHomePreFooterProps) {
return (
<div className={classNames(s.preFooter, s[brand])}>
<div className={s.container}>
<div className={s.content}>
<h2 className={s.heading}>{heading}</h2>
<p className={s.description}>{description}</p>
</div>
<div className={s.cards}>
{ctas.map((cta, index) => {
return (
<IoHomePreFooterCard
key={index}
brand={brand}
link={cta.link}
heading={cta.heading}
description={cta.description}
cta={cta.cta}
/>
)
})}
</div>
</div>
</div>
)
}
interface IoHomePreFooterCard {
brand?: string
link: string
heading: string
description: string
cta: string
}
function IoHomePreFooterCard({
brand,
link,
heading,
description,
cta,
}: IoHomePreFooterCard): React.ReactElement {
return (
<a
href={link}
className={s.card}
style={
{
'--primary': `var(--${brand})`,
'--secondary': `var(--${brand}-secondary)`,
} as React.CSSProperties
}
>
<h3 className={s.cardHeading}>{heading}</h3>
<p className={s.cardDescription}>{description}</p>
<span className={s.cardCta}>
{cta} <IconArrowRight16 />
</span>
</a>
)
}

View File

@@ -0,0 +1,119 @@
.preFooter {
margin: 60px auto;
}
.container {
--columns: 1;
composes: g-grid-container from global;
display: grid;
grid-template-columns: repeat(var(--columns), minmax(0, 1fr));
gap: 32px;
@media (--medium-up) {
--columns: 12;
}
}
.content {
grid-column: 1 / -1;
@media (--medium-up) {
grid-column: 1 / 6;
}
@media (--large) {
grid-column: 1 / 4;
}
}
.heading {
margin: 0;
composes: g-type-display-1 from global;
}
.description {
margin: 24px 0 0;
composes: g-type-body from global;
color: var(--gray-3);
}
.cards {
grid-column: 1 / -1;
--columns: 1;
display: grid;
grid-template-columns: repeat(var(--columns), minmax(0, 1fr));
gap: 32px;
@media (--medium-up) {
--columns: 3;
grid-column: 1 / -1;
}
@media (--large) {
grid-column: 5 / -1;
}
}
.card {
display: flex;
flex-direction: column;
flex-grow: 1;
padding: 32px 24px;
background-color: var(--primary);
color: var(--black);
border-radius: 6px;
box-shadow: 0 2px 3px rgba(101, 106, 118, 0.1),
0 8px 16px -10px rgba(101, 106, 118, 0.2);
transition: ease-in-out 0.2s;
transition-property: box-shadow;
&:hover {
box-shadow: 0 2px 3px rgba(101, 106, 118, 0.15),
0 16px 16px -10px rgba(101, 106, 118, 0.2);
}
&:nth-of-type(1) {
@nest .consul & {
color: var(--white);
}
}
&:nth-of-type(2) {
background-color: var(--secondary);
}
&:nth-of-type(3) {
background-color: var(--gray-6);
}
}
.cardHeading {
margin: 0;
composes: g-type-display-4 from global;
}
.cardDescription {
margin: 8px 0 0;
padding-bottom: 48px;
composes: g-type-display-6 from global;
}
.cardCta {
margin-top: auto;
display: inline-flex;
align-items: center;
composes: g-type-buttons-and-standalone-links from global;
& svg {
margin-left: 12px;
transition: transform 0.2s;
}
@nest .card:hover & svg {
transform: translate(2px);
}
}

View File

@@ -0,0 +1,71 @@
import Image from 'next/image'
import * as React from 'react'
import { Products } from '@hashicorp/platform-product-meta'
import classNames from 'classnames'
import Button from '@hashicorp/react-button'
import s from './style.module.css'
interface IoUsecaseCallToActionProps {
brand: Products
theme?: 'light' | 'dark'
heading: string
description: string
links: Array<{
text: string
url: string
}>
// TODO document intended usage
pattern: string
}
export default function IoUsecaseCallToAction({
brand,
theme,
heading,
description,
links,
pattern,
}: IoUsecaseCallToActionProps): React.ReactElement {
return (
<div
className={classNames(s.callToAction, s[theme])}
style={
{
'--background-color': `var(--${brand})`,
} as React.CSSProperties
}
>
<h2 className={s.heading}>{heading}</h2>
<div className={s.content}>
<p className={s.description}>{description}</p>
<div className={s.links}>
{links.map((link, index) => {
return (
<Button
// Index is stable
// eslint-disable-next-line react/no-array-index-key
key={index}
title={link.text}
url={link.url}
theme={{
brand: 'neutral',
variant: index === 0 ? 'primary' : 'secondary',
background: theme,
}}
/>
)
})}
</div>
</div>
<div className={s.pattern}>
<Image
src={pattern}
layout="fill"
objectFit="cover"
objectPosition="center left"
alt=""
/>
</div>
</div>
)
}

View File

@@ -0,0 +1,66 @@
.callToAction {
--columns: 1;
position: relative;
display: grid;
grid-template-columns: repeat(var(--columns), minmax(0, 1fr));
gap: 0 32px;
padding: 32px;
background-color: var(--background-color);
border-radius: 6px;
&.light {
color: var(--black);
}
&.dark {
color: var(--white);
}
@media (--medium-up) {
--columns: 12;
padding: 0;
}
}
.heading {
grid-column: 1 / -1;
margin: 0 0 16px;
composes: g-type-display-3 from global;
@media (--medium-up) {
grid-column: 1 / 6;
padding: 88px 32px 88px 64px;
}
}
.content {
grid-column: 1 / -1;
@media (--medium-up) {
grid-column: 6 / 11;
padding: 88px 0;
}
}
.description {
margin: 0 0 32px;
composes: g-type-body-large from global;
}
.links {
display: flex;
flex-wrap: wrap;
gap: 16px 32px;
}
.pattern {
position: relative;
display: none;
@media (--medium-up) {
grid-column: 11 / -1;
display: flex;
}
}

View File

@@ -0,0 +1,86 @@
import * as React from 'react'
import Image from 'next/image'
import Button from '@hashicorp/react-button'
import s from './style.module.css'
interface IoUsecaseCustomerProps {
media: {
src: string
width: string
height: string
alt: string
}
logo: {
src: string
width: string
height: string
alt: string
}
heading: string
description: string
stats?: Array<{
value: string
key: string
}>
link: string
}
export default function IoUsecaseCustomer({
media,
logo,
heading,
description,
stats,
link,
}: IoUsecaseCustomerProps): React.ReactElement {
return (
<section className={s.customer}>
<div className={s.container}>
<div className={s.columns}>
<div className={s.media}>
{/* eslint-disable-next-line jsx-a11y/alt-text */}
<Image {...media} layout="responsive" />
</div>
<div className={s.content}>
<div className={s.eyebrow}>
<div className={s.eyebrowLogo}>
{/* eslint-disable-next-line jsx-a11y/alt-text */}
<Image {...logo} />
</div>
<span className={s.eyebrowLabel}>Customer case study</span>
</div>
<h2 className={s.heading}>{heading}</h2>
<p className={s.description}>{description}</p>
{link ? (
<div className={s.cta}>
<Button
title="Read more"
url={link}
theme={{
brand: 'neutral',
variant: 'secondary',
background: 'dark',
}}
/>
</div>
) : null}
</div>
</div>
{stats.length > 0 ? (
<ul className={s.stats}>
{stats.map(({ key, value }, index) => {
return (
// Index is stable
// eslint-disable-next-line react/no-array-index-key
<li key={index}>
<p className={s.value}>{value}</p>
<p className={s.key}>{key}</p>
</li>
)
})}
</ul>
) : null}
</div>
</section>
)
}

View File

@@ -0,0 +1,118 @@
.customer {
position: relative;
background-color: var(--black);
color: var(--white);
padding-bottom: 64px;
@media (--medium-up) {
padding-bottom: 132px;
}
}
.container {
composes: g-grid-container from global;
}
.columns {
--columns: 1;
display: grid;
grid-template-columns: repeat(var(--columns), minmax(0, 1fr));
gap: 64px 32px;
@media (--medium-up) {
--columns: 12;
}
}
.media {
margin-top: -64px;
grid-column: 1 / -1;
@media (--medium-up) {
grid-column: 1 / 7;
}
}
.content {
grid-column: 1 / -1;
@media (--medium-up) {
padding-top: 64px;
grid-column: 8 / -1;
}
}
.eyebrow {
display: flex;
}
.eyebrowLogo {
display: flex;
max-width: 120px;
}
.eyebrowLabel {
padding-top: 8px;
padding-bottom: 8px;
padding-left: 12px;
margin-left: 12px;
border-left: 1px solid var(--gray-5);
align-self: center;
composes: g-type-label-small-strong from global;
}
.heading {
margin: 32px 0 24px;
composes: g-type-display-2 from global;
}
.description {
margin: 0;
composes: g-type-body from global;
}
.cta {
margin-top: 32px;
}
.stats {
--columns: 1;
list-style: none;
margin: 64px 0 0;
padding: 0;
display: grid;
grid-template-columns: repeat(var(--columns), minmax(0, 1fr));
gap: 32px;
@media (--medium-up) {
--columns: 12;
margin-top: 132px;
}
& > li {
border-top: 1px solid var(--gray-2);
grid-column: span 4;
}
}
.value {
margin: 0;
padding-top: 32px;
font-family: var(--font-display);
font-size: 50px;
font-weight: 700;
line-height: 1;
@media (--large) {
font-size: 80px;
}
}
.key {
margin: 12px 0 0;
composes: g-type-display-4 from global;
color: var(--gray-3);
}

View File

@@ -0,0 +1,41 @@
import * as React from 'react'
import Image from 'next/image'
import s from './style.module.css'
interface IoUsecaseHeroProps {
eyebrow: string
heading: string
description: string
pattern?: string
}
export default function IoUsecaseHero({
eyebrow,
heading,
description,
pattern,
}: IoUsecaseHeroProps): React.ReactElement {
return (
<header className={s.hero}>
<div className={s.container}>
<div className={s.pattern}>
{pattern ? (
<Image
src={pattern}
layout="responsive"
width={420}
height={500}
priority={true}
alt=""
/>
) : null}
</div>
<div className={s.content}>
<p className={s.eyebrow}>{eyebrow}</p>
<h1 className={s.heading}>{heading}</h1>
<p className={s.description}>{description}</p>
</div>
</div>
</header>
)
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.3 KiB

View File

@@ -0,0 +1,83 @@
.hero {
position: relative;
max-width: 1600px;
margin-right: auto;
margin-left: auto;
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-image: radial-gradient(
95.97% 95.97% at 50% 100%,
#f2f2f3 0%,
rgba(242, 242, 243, 0) 100%
);
@media (--medium-up) {
border-radius: 6px;
left: 24px;
right: 24px;
}
}
}
.container {
@media (--medium-up) {
display: grid;
grid-template-columns: 1fr max-content 1fr;
gap: 32px;
}
}
.pattern {
margin-left: 24px;
transform: translateY(24px);
position: relative;
display: flex;
flex-direction: column;
justify-content: flex-end;
@media (--small) {
display: none;
}
@media (--medium) {
& > * {
display: none !important;
}
}
}
.content {
position: relative;
max-width: 520px;
width: 100%;
margin-right: auto;
margin-left: auto;
padding: 64px 24px;
@media (--medium-up) {
padding-top: 132px;
padding-bottom: 132px;
}
}
.eyebrow {
margin: 0;
composes: g-type-label-strong from global;
}
.heading {
margin: 24px 0;
composes: g-type-display-1 from global;
}
.description {
margin: 0;
composes: g-type-body-large from global;
color: var(--gray-2);
}

View File

@@ -0,0 +1,81 @@
import * as React from 'react'
import { Products } from '@hashicorp/platform-product-meta'
import classNames from 'classnames'
import Image from 'next/image'
import Button from '@hashicorp/react-button'
import s from './style.module.css'
interface IoUsecaseSectionProps {
brand?: Products | 'neutral'
bottomIsFlush?: boolean
eyebrow: string
heading: string
description: string
media?: {
src: string
width: string
height: string
alt: string
}
cta?: {
text: string
link: string
}
}
export default function IoUsecaseSection({
brand = 'neutral',
bottomIsFlush = false,
eyebrow,
heading,
description,
media,
cta,
}: IoUsecaseSectionProps): React.ReactElement {
return (
<section
className={classNames(s.section, s[brand], bottomIsFlush && s.isFlush)}
>
<div className={s.container}>
<p className={s.eyebrow}>{eyebrow}</p>
<div className={s.columns}>
<div className={s.column}>
<h2 className={s.heading}>{heading}</h2>
{media?.src ? (
<div
className={s.description}
dangerouslySetInnerHTML={{
__html: description,
}}
/>
) : null}
{cta?.link && cta?.text ? (
<div className={s.cta}>
<Button
title={cta.text}
url={cta.link}
theme={{
brand: brand,
}}
/>
</div>
) : null}
</div>
<div className={s.column}>
{media?.src ? (
// eslint-disable-next-line jsx-a11y/alt-text
<Image {...media} />
) : (
<div
className={s.description}
dangerouslySetInnerHTML={{
__html: description,
}}
/>
)}
</div>
</div>
</div>
</section>
)
}

View File

@@ -0,0 +1,106 @@
.section {
position: relative;
max-width: 1600px;
margin-right: auto;
margin-left: auto;
padding-top: 64px;
padding-bottom: 64px;
@media (--medium-up) {
padding-top: 132px;
padding-bottom: 132px;
}
& + .section {
padding-bottom: 132px;
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: var(--gray-6);
opacity: 0.4;
@media (--medium-up) {
border-radius: 6px;
left: 24px;
right: 24px;
}
}
}
&.isFlush {
padding-bottom: 96px;
@media (--medium-up) {
padding-bottom: 164px;
}
&::before {
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
}
}
.container {
composes: g-grid-container from global;
}
.columns {
--columns: 1;
display: grid;
grid-template-columns: repeat(var(--columns), minmax(0, 1fr));
gap: 32px;
@media (--medium-up) {
--columns: 12;
}
}
.column {
&:nth-child(1) {
@media (--medium-up) {
grid-column: 1 / 7;
}
}
&:nth-child(2) {
@media (--medium-up) {
grid-column: 8 / -1;
padding-top: 16px;
}
}
}
.eyebrow {
margin: 0;
composes: g-type-display-5 from global;
}
.heading {
margin: 16px 0 32px;
padding-bottom: 32px;
composes: g-type-display-3 from global;
border-bottom: 1px solid var(--black);
}
.description {
composes: g-type-body from global;
& > p {
margin: 0;
& + p {
margin-top: 16px;
}
}
}
.cta {
margin-top: 32px;
}

View File

@@ -0,0 +1,84 @@
import * as React from 'react'
import Image from 'next/image'
import ReactPlayer from 'react-player'
import VisuallyHidden from '@reach/visually-hidden'
import IoDialog from 'components/io-dialog'
import PlayIcon from './play-icon'
import s from './style.module.css'
export interface IoHomeVideoCalloutProps {
youtubeId: string
thumbnail: string
heading: string
description: string
person?: {
avatar: string
name: string
description: string
}
}
export default function IoVideoCallout({
youtubeId,
thumbnail,
heading,
description,
person,
}: IoHomeVideoCalloutProps): React.ReactElement {
const [showDialog, setShowDialog] = React.useState(false)
const showVideo = () => setShowDialog(true)
const hideVideo = () => setShowDialog(false)
return (
<>
<figure className={s.videoCallout}>
<button className={s.thumbnail} onClick={showVideo}>
<VisuallyHidden>Play video</VisuallyHidden>
<PlayIcon />
<Image src={thumbnail} layout="fill" objectFit="cover" alt="" />
</button>
<figcaption className={s.content}>
<h3 className={s.heading}>{heading}</h3>
<p className={s.description}>{description}</p>
{person && (
<div className={s.person}>
{person.avatar ? (
<div className={s.personThumbnail}>
<Image
src={person.avatar}
width={52}
height={52}
alt={`${person.name} avatar`}
/>
</div>
) : null}
<div>
{person.name ? (
<p className={s.personName}>{person.name}</p>
) : null}
{person.description ? (
<p className={s.personDescription}>{person.description}</p>
) : null}
</div>
</div>
)}
</figcaption>
</figure>
<IoDialog
isOpen={showDialog}
onDismiss={hideVideo}
label={`${heading} video}`}
>
<h2 className={s.videoHeading}>{heading}</h2>
<div className={s.video}>
<ReactPlayer
url={`https://www.youtube.com/watch?v=${youtubeId}`}
width="100%"
height="100%"
playing={true}
controls={true}
/>
</div>
</IoDialog>
</>
)
}

View File

@@ -0,0 +1,23 @@
import * as React from 'react'
export default function PlayIcon(): React.ReactElement {
return (
<svg
width="96"
height="96"
viewBox="0 0 96 96"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<circle cx="48" cy="48" r="48" fill="#fff" />
<path
fillRule="evenodd"
clipRule="evenodd"
d="m63.254 46.653-22.75-14.4a1.647 1.647 0 0 0-1.657-.057c-.522.28-.847.82-.847 1.405V62.4c0 .584.325 1.123.847 1.403a1.639 1.639 0 0 0 1.657-.057l22.75-14.4c.465-.294.746-.802.746-1.346 0-.545-.281-1.052-.746-1.347Z"
fill="#fff"
stroke="#000"
strokeWidth="2"
/>
</svg>
)
}

View File

@@ -0,0 +1,128 @@
.videoCallout {
--columns: 1;
margin: 0;
display: grid;
grid-template-columns: repeat(var(--columns), minmax(0, 1fr));
gap: 32px;
background-color: var(--black);
border-radius: 6px;
overflow: hidden;
@media (--medium-up) {
--columns: 12;
}
}
.thumbnail {
position: relative;
display: grid;
place-items: center;
grid-column: 1 / -1;
background-color: transparent;
border: 0;
cursor: pointer;
padding: 96px 32px;
min-height: 300px;
@media (--medium-up) {
grid-column: 1 / 7;
}
@media (--large) {
grid-column: 1 / 9;
}
& > svg {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 1;
@media (--small) {
width: 52px;
height: 52px;
}
}
&::after {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: #000;
opacity: 0.45;
transition: opacity ease-in-out 0.2s;
}
&:hover::after {
opacity: 0.2;
}
}
.content {
padding: 32px;
grid-column: 1 / -1;
@media (--medium-up) {
padding: 80px 32px;
grid-column: 7 / -1;
}
@media (--large) {
grid-column: 9 / -1;
}
}
.heading {
margin: 0;
composes: g-type-display-4 from global;
color: var(--white);
}
.description {
margin: 8px 0 0;
composes: g-type-body-small from global;
color: var(--white);
}
.person {
margin-top: 64px;
display: flex;
align-items: center;
gap: 16px;
}
.personThumbnail {
display: flex;
border-radius: 9999px;
overflow: hidden;
}
.personName {
margin: 0;
composes: g-type-body-strong from global;
color: var(--white);
}
.personDescription {
margin: 4px 0 0;
composes: g-type-label-strong from global;
color: var(--gray-3);
}
.videoHeading {
margin-top: 0;
margin-bottom: 32px;
padding-right: 100px;
composes: g-type-display-4 from global;
}
.video {
position: relative;
background-color: var(--gray-2);
aspect-ratio: 16 / 9;
}

View File

@@ -1,22 +1,30 @@
import Subnav from '@hashicorp/react-subnav'
import subnavItems from '../../data/subnav'
import { useRouter } from 'next/router'
import s from './style.module.css'
export default function NomadSubnav() {
export default function NomadSubnav({ menuItems }) {
const router = useRouter()
return (
<Subnav
className={s.subnav}
hideGithubStars={true}
titleLink={{
text: 'nomad',
text: 'HashiCorp Nomad',
url: '/',
}}
ctaLinks={[
{ text: 'GitHub', url: 'https://www.github.com/hashicorp/nomad' },
{ text: 'Download', url: '/downloads' },
{
text: 'Download',
url: '/downloads',
theme: {
brand: 'nomad',
},
},
]}
currentPath={router.asPath}
menuItemsAlign="right"
menuItems={subnavItems}
menuItems={menuItems}
constrainWidth
matchOnBasePath
/>

View File

@@ -0,0 +1,3 @@
.subnav {
border-top: 1px solid transparent;
}

View File

@@ -0,0 +1,78 @@
import query from './query.graphql'
import ProductSubnav from 'components/subnav'
import Footer from 'components/footer'
import { open } from '@hashicorp/react-consent-manager'
export default function StandardLayout(props: Props): React.ReactElement {
const { useCaseNavItems } = props.data
return (
<>
<ProductSubnav
menuItems={[
{ text: 'Overview', url: '/', type: 'inbound' },
{
text: 'Use Cases',
submenu: [
...useCaseNavItems.map((item) => {
return {
text: item.text,
url: `/use-cases/${item.url}`,
}
}),
].sort((a, b) => a.text.localeCompare(b.text)),
},
{
text: 'Enterprise',
url: 'https://www.hashicorp.com/products/nomad/',
type: 'outbound',
},
'divider',
{
text: 'Tutorials',
url: 'https://learn.hashicorp.com/nomad',
type: 'outbound',
},
{
text: 'Docs',
url: '/docs',
type: 'inbound',
},
{
text: 'API',
url: '/api-docs',
type: 'inbound',
},
{
text: 'Plugins',
url: '/plugins',
type: 'inbound',
},
{
text: 'Tools',
url: '/tools',
type: 'inbound',
},
{
text: 'Community',
url: '/community',
type: 'inbound',
},
]}
/>
{props.children}
<Footer openConsentManager={open} />
</>
)
}
StandardLayout.rivetParams = {
query,
dependencies: [],
}
interface Props {
children: React.ReactChildren
data: {
useCaseNavItems: Array<{ url: string; text: string }>
}
}

View File

@@ -0,0 +1,6 @@
query UseCasesQuery {
useCaseNavItems: allNomadUseCases {
url: slug
text: heroHeading
}
}

11
website/lib/utils.ts Normal file
View File

@@ -0,0 +1,11 @@
export const isInternalLink = (link: string): boolean => {
if (
link.startsWith('/') ||
link.startsWith('#') ||
link.startsWith('https://nomadproject.io') ||
link.startsWith('https://www.nomadproject.io')
) {
return true
}
return false
}

View File

@@ -2,8 +2,13 @@ const withHashicorp = require('@hashicorp/platform-nextjs-plugin')
const redirects = require('./redirects')
module.exports = withHashicorp({
dato: {
// This token is safe to be in this public repository, it only has access to content that is publicly viewable on the website
token: '88b4984480dad56295a8aadae6caad',
},
defaultLayout: true,
nextOptimizedImages: true,
transpileModules: ['@hashicorp/flight-icons'],
})({
redirects() {
return redirects
@@ -16,9 +21,14 @@ module.exports = withHashicorp({
],
},
env: {
HASHI_ENV: process.env.HASHI_ENV || 'development',
SEGMENT_WRITE_KEY: 'qW11yxgipKMsKFKQUCpTVgQUYftYsJj0',
BUGSNAG_CLIENT_KEY: '4fa712dfcabddd05da29fd1f5ea5a4c0',
BUGSNAG_SERVER_KEY: '61141296f1ba00a95a8788b7871e1184',
ENABLE_VERSIONED_DOCS: process.env.ENABLE_VERSIONED_DOCS || false,
},
images: {
domains: ['www.datocms-assets.com'],
disableStaticImages: true,
},
})

5690
website/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -8,8 +8,10 @@
"npm": ">=7.0.0"
},
"dependencies": {
"@hashicorp/flight-icons": "^2.0.2",
"@hashicorp/mktg-global-styles": "^4.0.0",
"@hashicorp/mktg-logos": "^1.2.0",
"@hashicorp/nextjs-scripts": "^19.0.3",
"@hashicorp/platform-analytics": "^0.2.0",
"@hashicorp/platform-code-highlighting": "^0.1.2",
"@hashicorp/platform-runtime-error-monitoring": "^0.1.0",
@@ -32,7 +34,7 @@
"@hashicorp/react-product-downloads-page": "^2.5.3",
"@hashicorp/react-search": "^6.1.1",
"@hashicorp/react-section-header": "^5.0.4",
"@hashicorp/react-subnav": "^9.3.2",
"@hashicorp/react-subnav": "^9.3.4",
"@hashicorp/react-tabs": "^7.0.1",
"@hashicorp/react-text-split": "^4.0.0",
"@hashicorp/react-text-split-with-code": "^3.3.8",
@@ -40,14 +42,18 @@
"@hashicorp/react-text-split-with-logo-grid": "^5.1.5",
"@hashicorp/react-use-cases": "^5.0.0",
"@hashicorp/react-vertical-text-block-list": "^7.0.0",
"@reach/dialog": "^0.16.2",
"framer-motion": "^5.6.0",
"marked": "0.7.0",
"next": "^11.1.2",
"next-mdx-remote": "3.0.1",
"next-remote-watch": "^1.0.0",
"nuka-carousel": "4.7.7",
"react": "^17.0.2",
"react-datocms": "^2.0.1",
"react-device-detect": "1.17.0",
"react-dom": "^17.0.2"
"react-dom": "^17.0.2",
"react-player": "^2.9.0"
},
"devDependencies": {
"@hashicorp/platform-cli": "^1.2.0",

View File

@@ -3,6 +3,7 @@ import '@hashicorp/platform-util/nprogress/style.css'
import Router from 'next/router'
import Head from 'next/head'
import rivetQuery from '@hashicorp/nextjs-scripts/dato/client'
import NProgress from '@hashicorp/platform-util/nprogress'
import { ErrorBoundary } from '@hashicorp/platform-runtime-error-monitoring'
import createConsentManager from '@hashicorp/react-consent-manager/loader'
@@ -12,21 +13,23 @@ import useAnchorLinkAnalytics from '@hashicorp/platform-util/anchor-link-analyti
import HashiStackMenu from '@hashicorp/react-hashi-stack-menu'
import AlertBanner from '@hashicorp/react-alert-banner'
import HashiHead from '@hashicorp/react-head'
import Footer from 'components/footer'
import ProductSubnav from 'components/subnav'
import Error from './_error'
import alertBannerData, { ALERT_BANNER_ACTIVE } from 'data/alert-banner'
import StandardLayout from 'layouts/standard'
NProgress({ Router })
const { ConsentManager, openConsentManager } = createConsentManager({
const { ConsentManager } = createConsentManager({
preset: 'oss',
otherServices: [...localConsentManagerServices],
})
export default function App({ Component, pageProps }) {
export default function App({ Component, pageProps, layoutData }) {
useFathomAnalytics()
useAnchorLinkAnalytics()
const Layout = Component.layout ?? StandardLayout
return (
<ErrorBoundary FallbackComponent={Error}>
<HashiHead
@@ -40,13 +43,27 @@ export default function App({ Component, pageProps }) {
{ALERT_BANNER_ACTIVE && (
<AlertBanner {...alertBannerData} product="nomad" hideOnMobile />
)}
<HashiStackMenu />
<ProductSubnav />
<div className={`content${ALERT_BANNER_ACTIVE ? ' banner' : ''}`}>
<Component {...pageProps} />
</div>
<Footer openConsentManager={openConsentManager} />
<Layout {...(layoutData && { data: layoutData })}>
<div className={`content${ALERT_BANNER_ACTIVE ? ' banner' : ''}`}>
<Component {...pageProps} />
</div>
</Layout>
<ConsentManager />
</ErrorBoundary>
)
}
App.getInitialProps = async ({ Component, ctx }) => {
const layoutQuery = Component.layout
? Component.layout?.rivetParams ?? null
: StandardLayout.rivetParams
const layoutData = layoutQuery ? await rivetQuery(layoutQuery) : null
let pageProps = {}
if (Component.getInitialProps) {
pageProps = await Component.getInitialProps(ctx)
}
return { pageProps, layoutData }
}

View File

@@ -1,465 +0,0 @@
import Link from 'next/link'
import UseCases from '@hashicorp/react-use-cases'
import CallToAction from '@hashicorp/react-call-to-action'
import ComparisonCallouts from 'components/comparison-callouts'
import TextSplitWithLogoGrid from '@hashicorp/react-text-split-with-logo-grid'
import LearnCallout from '@hashicorp/react-learn-callout'
import FeaturesList from 'components/features-list'
import HomepageHero from 'components/homepage-hero'
import CaseStudyCarousel from 'components/case-study-carousel'
import MiniCTA from 'components/mini-cta'
export default function Homepage() {
// Test comment to see if Vercel picks up this commit
return (
<div id="p-home">
<HomepageHero
title="Workload Orchestration Made Easy"
description="A simple and flexible scheduler and workload orchestrator to deploy and manage containers and non-containerized applications across on-prem and clouds at scale."
links={[
{
text: 'Download',
url: '/downloads',
type: 'download',
},
{
text: 'Get Started',
url: 'https://learn.hashicorp.com/nomad',
type: 'outbound',
},
]}
/>
<FeaturesList
title="Why Nomad?"
items={[
{
title: 'Simple and Lightweight',
content:
'Single binary that integrates into existing infrastructure. Easy to operate on-prem or in the cloud with minimal overhead.',
icon: require('./img/why-nomad/simple-and-lightweight.svg'),
},
{
title: 'Flexible Workload Support',
content:
'Orchestrate applications of any type - not just containers. First class support for Docker, Windows, Java, VMs, and more.',
icon: require('./img/why-nomad/flexible-workload-support.svg'),
},
{
title: 'Modernize Legacy Applications without Rewrite',
content:
'Bring orchestration benefits to existing services. Achieve zero downtime deployments, improved resilience, higher resource utilization, and more without containerization.',
icon: require('./img/why-nomad/modernize-legacy-applications.svg'),
},
{
title: 'Easy Federation at Scale',
content:
'Single command for multi-region, multi-cloud federation. Deploy applications globally to any region using Nomad as a single unified control plane.',
icon: require('./img/why-nomad/federation.svg'),
},
{
title: 'Deploy and Scale with Ease',
content:
'Deploy to bare metal with the same ease as in cloud environments. Scale globally without complexity. Read <a href="https://www.hashicorp.com/c2m">the 2 Million Container Challenge</a>.',
icon: require('./img/why-nomad/servers.svg'),
},
{
title: 'Native Integrations with Terraform, Consul, and Vault',
content:
'Nomad integrates seamlessly with Terraform, Consul and Vault for provisioning, service networking, and secrets management.',
icon: require('./img/why-nomad/native-integration.svg'),
},
]}
/>
<ComparisonCallouts
heading="Nomad vs. Kubernetes"
details={
<p>
Choose an orchestrator based on how it fits into your project. Find
out{' '}
<Link href="/docs/nomad-vs-kubernetes">
<a>Nomads unique strengths relative to Kubernetes.</a>
</Link>
</p>
}
items={[
{
title: 'Alternative to Kubernetes',
description: 'Deploy and scale containers without complexity',
imageUrl: require('./img/nomad-vs-kubernetes/alternative.svg?url'),
link: {
url: '/docs/nomad-vs-kubernetes/alternative',
text: 'Learn more',
type: 'inbound',
},
},
{
title: 'Supplement to Kubernetes',
description: 'Implement a multi-orchestrator pattern',
imageUrl: require('./img/nomad-vs-kubernetes/supplement.svg?url'),
link: {
url: '/docs/nomad-vs-kubernetes/supplement',
text: 'Learn more',
type: 'inbound',
},
},
]}
/>
<CaseStudyCarousel
title="Trusted by startups and the worlds largest organizations"
caseStudies={[
{
quote:
'After migrating to Nomad, our deployment is at least twice as fast as Kubernetes.',
caseStudyURL:
'https://www.hashicorp.com/resources/gitlab-nomad-gitops-internet-archive-migrated-from-kubernetes-nomad-consul',
person: {
firstName: 'Tracey',
lastName: 'Jaquith',
photo:
'https://www.datocms-assets.com/2885/1616436433-internetarhive.jpeg',
title: 'Software Architect',
},
company: {
name: 'Internet Archive',
logo:
'https://www.datocms-assets.com/2885/1616436427-artboard.png',
},
},
{
quote:
'We deployed a dynamic task scheduling system with Nomad. It helped us improve the availability of distributed services across more than 200 edge cities worldwide.',
caseStudyURL:
'https://blog.cloudflare.com/how-we-use-hashicorp-nomad/',
person: {
firstName: 'Thomas',
lastName: 'Lefebvre',
photo:
'https://www.datocms-assets.com/2885/1591836195-tlefebvrephoto.jpg',
title: 'Tech Lead, SRE',
},
company: {
name: 'Cloudflare',
logo:
'https://www.datocms-assets.com/2885/1522194205-cf-logo-h-rgb.png',
},
},
{
quote:
'Weve really streamlined our data operations with Nomad and freed up our time to work on more high-impact tasks. Once we launch new microservices, they just work.',
caseStudyURL:
'https://www.hashicorp.com/blog/nomad-community-story-navi-capital',
person: {
firstName: 'Carlos',
lastName: 'Domingues',
photo:
'https://www.datocms-assets.com/2885/1590508642-carlos.png',
title: 'IT Infrastructure Lead',
},
company: {
name: 'Navi Capital',
logo:
'https://www.datocms-assets.com/2885/1590509560-navi-logo.png',
},
},
{
quote:
'Kubernetes is the 800-pound gorilla of container orchestration, coming with a price tag. So we looked into alternatives - and fell in love with Nomad.',
caseStudyURL:
'https://endler.dev/2019/maybe-you-dont-need-kubernetes/',
person: {
firstName: 'Matthias',
lastName: 'Endler',
photo:
'https://www.datocms-assets.com/2885/1582163422-matthias-endler.png',
title: 'Backend Engineer',
},
company: {
name: 'Trivago',
logo:
'https://www.datocms-assets.com/2885/1582162145-trivago.svg',
},
},
{
quote:
'Our customers jobs are changing constantly. Its challenging to dynamically predict demand, what types of jobs, and the resource requirements. We found that Nomad excelled in this area.',
caseStudyURL:
'https://www.hashicorp.com/resources/nomad-vault-circleci-security-scheduling',
person: {
firstName: 'Rob',
lastName: 'Zuber',
photo:
'https://www.datocms-assets.com/2885/1582180618-rob-zuber.jpeg',
title: 'CTO',
},
company: {
name: 'CircleCI',
logo:
'https://www.datocms-assets.com/2885/1582180745-circleci-logo.svg',
},
},
{
quote:
"I know many teams doing incredible work with Kubernetes but I also have heard horror stories about what happens when it doesn't go well. We attribute our systems' stability to the simplicity and elegance of Nomad.",
caseStudyURL:
'https://www.hashicorp.com/resources/betterhelp-s-hashicorp-nomad-use-case/',
person: {
firstName: 'Michael',
lastName: 'Aldridge',
photo:
'https://www.datocms-assets.com/2885/1592925323-1587510032-michael-alridge.jpeg',
title: 'Staff Systems Engineer',
},
company: {
name: 'BetterHelp',
logo:
'https://www.datocms-assets.com/2885/1592925329-betterhelp-logo.png',
},
},
{
quote:
"Nomad gives us a unified control plane, enabling hardware and driver rollouts using vendor's drivers - be it a centrifuge, incubator, or mass spectrometer.",
caseStudyURL:
'https://thenewstack.io/applying-workload-orchestration-to-experimental-biology/',
person: {
firstName: 'Dhasharath',
lastName: 'Shrivathsa',
photo:
'https://www.datocms-assets.com/2885/1623450501-dhasharath-shrivathsa.jpg',
title: 'CEO',
},
company: {
name: 'Radix',
logo:
'https://www.datocms-assets.com/2885/1594233325-radix-logo-1.svg',
},
},
{
quote:
'Nomad has proven itself to be highly scalable, and were excited to scale our business alongside it.',
caseStudyURL:
'https://www.hashicorp.com/blog/how-nomad-powers-a-google-backed-indoor-farming-startup-to-disrupt-agtech/',
person: {
firstName: 'John',
lastName: 'Spencer',
photo:
'https://www.datocms-assets.com/2885/1594236857-johnspencer.jpeg',
title: 'Senior Site Reliability Engineer',
},
company: {
name: 'Bowery',
logo:
'https://www.datocms-assets.com/2885/1594242826-bowery-logo-2.png',
},
},
]}
featuredLogos={[
{
companyName: 'Trivago',
url:
'https://www.datocms-assets.com/2885/1582162317-trivago-monochromatic.svg',
},
{
companyName: 'CircleCI',
url:
'https://www.datocms-assets.com/2885/1582180745-circleci-logo.svg',
},
{
companyName: 'SAP Ariba',
url:
'https://www.datocms-assets.com/2885/1580419436-logosap-ariba.svg',
},
{
companyName: 'Pandora',
url:
'https://www.datocms-assets.com/2885/1523044075-pandora-black.svg',
},
{
companyName: 'Deluxe',
url:
'https://www.datocms-assets.com/2885/1582323254-deluxe-logo.svg',
},
{
companyName: 'Radix',
url:
'https://www.datocms-assets.com/2885/1594233325-radix-logo-1.svg',
},
]}
/>
<MiniCTA
title="Are you using Nomad in production?"
link={{
text: 'Share your success story and receive special Nomad swag.',
url: 'https://forms.gle/rdaLSuMGpvbomgYk9',
type: 'outbound',
}}
/>
<div className="use-cases g-grid-container">
<h2 className="g-type-display-2">Use Cases</h2>
<UseCases
product="nomad"
items={[
{
title: 'Simple Container Orchestration',
description:
'Deploy, manage, and scale enterprise containers in production with ease.',
image: {
alt: null,
format: 'png',
url: require('./img/use-cases/simple_container_orchestration_icon.svg?url'),
},
link: {
external: false,
title: 'Learn more',
url: '/use-cases/simple-container-orchestration',
},
},
{
title: 'Non Containerized Application Orchestration',
description:
'Modernize non-containerized applications without rewrite.',
image: {
alt: null,
format: 'png',
url: require('./img/use-cases/non-containerized_app_orch_icon.svg?url'),
},
link: {
external: false,
title: 'Learn more',
url: '/use-cases/non-containerized-application-orchestration',
},
},
{
title: 'Automated Service Networking with Consul',
description:
'Service discovery and service mesh with HashiCorp Consul to ensure secure service-to-service communication.',
image: {
alt: null,
format: 'png',
url: require('./img/use-cases/automated_service_networking_icon.svg?url'),
},
link: {
external: false,
title: 'Learn more',
url: '/use-cases/automated-service-networking-with-consul',
},
},
]}
/>
</div>
<LearnCallout
headline="Learn the latest Nomad skills"
product="nomad"
items={[
{
title: 'Getting Started',
category: 'Step-by-Step Guides',
time: '24 mins',
link: 'https://learn.hashicorp.com/collections/nomad/get-started',
image: require('./img/learn-nomad/cap.svg'),
},
{
title: 'Deploy and Manage Nomad Jobs',
category: 'Step-by-Step Guides',
time: '36 mins',
link: 'https://learn.hashicorp.com/collections/nomad/manage-jobs',
image: require('./img/learn-nomad/cubes.svg'),
},
]}
/>
<TextSplitWithLogoGrid
textSplit={{
product: 'nomad',
heading: 'Nomad Ecosystem',
content:
'Enable end-to-end automation for your application deployment.',
linkStyle: 'links',
links: [
{
text: 'Explore the Nomad Ecosystem',
url: '/docs/ecosystem',
type: 'inbound',
},
],
}}
logoGrid={[
{
url: require('@hashicorp/mktg-logos/product/consul/logomark/color.svg?url'),
alt: 'Consul',
linkUrl: '/docs/integrations/consul-integration',
},
{
url: require('@hashicorp/mktg-logos/product/vault/logomark/color.svg?url'),
alt: 'Vault',
linkUrl: '/docs/integrations/vault-integration',
},
{
url: require('./img/partner-logos/gitlab-logo-gray-stacked-rgb.svg?url'),
alt: 'Gitlab',
linkUrl:
'https://www.hashicorp.com/resources/nomad-ci-cd-developer-workflows-and-integrations',
},
{
url: require('./img/partner-logos/csi.svg?url'),
alt: 'Container Storage interface',
linkUrl: '/docs/internals/plugins/csi',
},
{
url: require('./img/partner-logos/cni.svg?url'),
alt: 'Container Network interface',
linkUrl: '/docs/integrations/consul-connect#cni-plugins',
},
{
url: require('./img/partner-logos/nvidia.svg?url'),
alt: 'NVIDIA',
linkUrl:
'https://www.hashicorp.com/resources/running-gpu-accelerated-applications-on-nomad',
},
{
url: require('./img/partner-logos/datadog.svg?url'),
alt: 'Datadog',
linkUrl: 'https://docs.datadoghq.com/integrations/nomad/',
},
{
url: require('./img/partner-logos/jfrog.svg?url'),
alt: 'JFrog Artifactory',
linkUrl:
'https://jfrog.com/blog/cluster-management-made-simple-with-jfrog-artifactory-and-hashicorp-nomad/',
},
{
url: require('./img/partner-logos/prometheus.svg?url'),
alt: 'Prometheus',
linkUrl:
'https://learn.hashicorp.com/tutorials/nomad/dynamic-application-sizing?in=nomad/nomad-1-0#start-prometheus',
},
]}
/>
<CallToAction
variant="compact"
heading="Ready to get started?"
content="Nomad Open Source addresses the technical complexity of managing a mixed type of workloads in production at scale by providing a simple and flexible workload orchestrator across distributed infrastructure and clouds."
product="nomad"
links={[
{
text: 'Explore HashiCorp Learn',
type: 'outbound',
url: 'https://learn.hashicorp.com/nomad',
},
{
text: 'Explore Documentation',
type: 'inbound',
url: '/docs',
},
]}
/>
</div>
)
}

View File

@@ -0,0 +1,180 @@
import * as React from 'react'
import rivetQuery from '@hashicorp/nextjs-scripts/dato/client'
import homepageQuery from './query.graphql'
import { isInternalLink } from 'lib/utils'
import ReactHead from '@hashicorp/react-head'
import IoHomeHero from 'components/io-home-hero'
import IoHomeIntro from 'components/io-home-intro'
import IoHomeInPractice from 'components/io-home-in-practice'
import IoCardContainer from 'components/io-card-container'
import IoHomeCaseStudies from 'components/io-home-case-studies'
import IoHomeCallToAction from 'components/io-home-call-to-action'
import IoHomePreFooter from 'components/io-home-pre-footer'
import s from './style.module.css'
export default function Homepage({ data }): React.ReactElement {
const {
seo,
heroHeading,
heroDescription,
heroCtas,
heroCards,
introHeading,
introDescription,
introFeatures,
introVideo,
inPracticeHeading,
inPracticeDescription,
inPracticeCards,
inPracticeCtaHeading,
inPracticeCtaDescription,
inPracticeCtaLink,
inPracticeCtaImage,
useCasesHeading,
useCasesDescription,
useCasesCards,
caseStudiesHeading,
caseStudiesDescription,
caseStudiesFeatured,
caseStudiesLinks,
callToActionHeading,
callToActionDescription,
callToActionCtas,
preFooterHeading,
preFooterDescription,
preFooterCtas,
} = data
const _introVideo = introVideo[0]
return (
<>
<ReactHead
title={seo.title}
description={seo.description}
image={seo.image?.url}
twitterCard="summary_large_image"
pageName={seo.title}
>
<meta name="twitter:title" content={seo.title} />
<meta name="twitter:description" content={seo.description} />
</ReactHead>
<IoHomeHero
pattern="/img/home-hero-pattern.svg"
brand="nomad"
heading={heroHeading}
description={heroDescription}
ctas={heroCtas}
cards={heroCards.map((card) => {
return {
...card,
cta: card.cta[0],
}
})}
/>
<IoHomeIntro
isInternalLink={isInternalLink}
brand="nomad"
heading={introHeading}
description={introDescription}
features={introFeatures}
video={{
youtubeId: _introVideo?.youtubeId,
thumbnail: _introVideo?.thumbnail?.url,
heading: _introVideo?.heading,
description: _introVideo?.description,
person: {
name: _introVideo?.personName,
description: _introVideo?.personDescription,
avatar: _introVideo?.personAvatar?.url,
},
}}
/>
<section className={s.useCases}>
<div className={s.container}>
<IoCardContainer
heading={useCasesHeading}
description={useCasesDescription}
cardsPerRow={4}
cards={useCasesCards.map((card) => {
return {
eyebrow: card.eyebrow,
link: {
url: card.link,
type: 'inbound',
},
heading: card.heading,
description: card.description,
products: card.products,
}
})}
/>
</div>
</section>
<IoHomeInPractice
brand="nomad"
pattern="/img/practice-pattern.svg"
heading={inPracticeHeading}
description={inPracticeDescription}
cards={inPracticeCards.map((card) => {
return {
eyebrow: card.eyebrow,
link: {
url: card.link,
type: 'inbound',
},
heading: card.heading,
description: card.description,
products: card.products,
}
})}
cta={{
heading: inPracticeCtaHeading,
description: inPracticeCtaDescription,
link: inPracticeCtaLink,
image: inPracticeCtaImage,
}}
/>
<IoHomeCaseStudies
heading={caseStudiesHeading}
description={caseStudiesDescription}
primary={caseStudiesFeatured}
secondary={caseStudiesLinks}
/>
<IoHomeCallToAction
brand="nomad"
heading={callToActionHeading}
content={callToActionDescription}
links={callToActionCtas}
/>
<IoHomePreFooter
brand="nomad"
heading={preFooterHeading}
description={preFooterDescription}
ctas={preFooterCtas}
/>
</>
)
}
export async function getStaticProps() {
const { nomadHomepage } = await rivetQuery({
query: homepageQuery,
})
return {
props: {
data: nomadHomepage,
},
revalidate:
process.env.HASHI_ENV === 'production'
? process.env.GLOBAL_REVALIDATE
: 10,
}
}

View File

@@ -0,0 +1,109 @@
query homepageQuery {
nomadHomepage {
seo: seoMeta {
title
description
image {
url
}
}
heroHeading
heroDescription
heroCtas {
title
link
}
heroCards {
heading
description
cta {
title
link
}
subText: subtext
}
introHeading
introDescription
introFeatures {
heading
description
link
image {
url
alt
}
}
introVideo {
youtubeId
heading
description
thumbnail {
url
}
personName
personDescription
personAvatar {
url
}
}
inPracticeHeading
inPracticeDescription
inPracticeCards {
eyebrow
heading
description
link
products {
name
}
}
inPracticeCtaHeading
inPracticeCtaDescription
inPracticeCtaLink
inPracticeCtaImage {
url
alt
width
height
}
useCasesHeading
useCasesDescription
useCasesCards {
eyebrow
heading
description
link
products {
name
}
}
caseStudiesHeading
caseStudiesDescription
caseStudiesFeatured {
thumbnail {
url
alt
}
heading
link
}
caseStudiesLinks {
heading
link
}
callToActionHeading
callToActionDescription
callToActionCtas {
text: title
url: link
}
preFooterHeading
preFooterDescription
preFooterCtas {
link
heading
description
cta
}
}
}

View File

@@ -1,33 +0,0 @@
#p-home {
& > section {
padding-top: 100px;
padding-bottom: 100px;
}
& .use-cases {
padding-top: 128px;
padding-bottom: 64px;
@media (max-width: 800px) {
padding-top: 88px;
}
& h2 {
margin: 0;
text-align: center;
margin-bottom: 64px;
@media (max-width: 800px) {
margin-bottom: 48px;
}
}
}
/* HACK: Override variant-tertiary-neutral's color rules */
& .g-text-split .g-btn.variant-tertiary-neutral {
color: var(--nomad-link);
& svg path {
stroke: var(--nomad-link);
}
}
}

View File

@@ -0,0 +1,15 @@
.container {
composes: g-grid-container from global;
}
/*
* Use cases
*/
.useCases {
margin: 60px auto;
@media (--medium-up) {
margin: 120px auto;
}
}

View File

@@ -1,3 +1,3 @@
import Homepage from './home'
import Homepage, { getStaticProps } from './home'
export { getStaticProps }
export default Homepage

View File

@@ -17,7 +17,7 @@
@import '../components/use-case-page/style.css';
/* Local Pages */
@import './home/style.css';
/* @import './home/style.css'; */
/* Print Styles */
@import './print.css';

View File

@@ -0,0 +1,244 @@
import * as React from 'react'
import rivetQuery from '@hashicorp/nextjs-scripts/dato/client'
import useCasesQuery from './query.graphql'
import ReactHead from '@hashicorp/react-head'
import IoUsecaseHero from 'components/io-usecase-hero'
import IoUsecaseSection from 'components/io-usecase-section'
import IoUsecaseCustomer from 'components/io-usecase-customer'
import IoCardContainer from 'components/io-card-container'
import IoVideoCallout from 'components/io-video-callout'
import IoUsecaseCallToAction from 'components/io-usecase-call-to-action'
import s from './style.module.css'
export default function UseCasePage({ data }) {
const {
seo,
heroHeading,
heroDescription,
challengeHeading,
challengeDescription,
challengeImage,
challengeLink,
solutionHeading,
solutionDescription,
solutionImage,
solutionLink,
customerCaseStudy,
cardsHeading,
cardsDescription,
tutorialsLink,
tutorialCards,
documentationLink,
documentationCards,
callToActionHeading,
callToActionDescription,
callToActionLinks,
videoCallout,
} = data
const _customerCaseStudy = customerCaseStudy[0]
const _videoCallout = videoCallout[0]
return (
<>
<ReactHead
title={seo.title}
description={seo.description}
image={seo.image?.url}
twitterCard="summary_large_image"
pageName={seo.title}
>
<meta name="twitter:title" content={seo.title} />
<meta name="twitter:description" content={seo.description} />
</ReactHead>
<IoUsecaseHero
eyebrow="Use case"
heading={heroHeading}
description={heroDescription}
pattern="/img/usecase-hero-pattern.svg"
/>
<IoUsecaseSection
brand="nomad"
eyebrow="Challenge"
heading={challengeHeading}
description={challengeDescription}
media={{
src: challengeImage?.url,
width: challengeImage?.width,
height: challengeImage?.height,
alt: challengeImage?.alt,
}}
cta={{
text: 'Learn more',
link: challengeLink,
}}
/>
<IoUsecaseSection
brand="nomad"
bottomIsFlush={_customerCaseStudy}
eyebrow="Solution"
heading={solutionHeading}
description={solutionDescription}
media={{
src: solutionImage?.url,
width: solutionImage?.width,
height: solutionImage?.height,
alt: solutionImage?.alt,
}}
cta={{
text: 'Learn more',
link: solutionLink,
}}
/>
{_customerCaseStudy ? (
<IoUsecaseCustomer
link={_customerCaseStudy.link}
media={{
src: _customerCaseStudy.image.url,
width: _customerCaseStudy.image.width,
height: _customerCaseStudy.image.height,
alt: _customerCaseStudy.image.alt,
}}
logo={{
src: _customerCaseStudy.logo.url,
width: _customerCaseStudy.logo.width,
height: _customerCaseStudy.logo.height,
alt: _customerCaseStudy.logo.alt,
}}
heading={_customerCaseStudy.heading}
description={_customerCaseStudy.description}
stats={_customerCaseStudy.stats.map((stat) => {
return {
value: stat.value,
key: stat.label,
}
})}
/>
) : null}
<div className={s.cards}>
<IoCardContainer
heading={cardsHeading}
description={cardsDescription}
label="Docs"
cta={{
url: documentationLink ? documentationLink : '/docs',
text: 'Explore all',
}}
cardsPerRow={3}
cards={documentationCards.map((card) => {
return {
eyebrow: card.eyebrow,
link: {
url: card.link,
type: 'inbound',
},
heading: card.heading,
description: card.description,
products: card.products,
}
})}
/>
<IoCardContainer
label="Tutorials"
cta={{
url: tutorialsLink
? tutorialsLink
: 'https://learn.hashicorp.com/vault',
text: 'Explore all',
}}
cardsPerRow={3}
cards={tutorialCards.map((card) => {
return {
eyebrow: card.eyebrow,
link: {
url: card.link,
type: 'inbound',
},
heading: card.heading,
description: card.description,
products: card.products,
}
})}
/>
</div>
<div className={s.callToAction}>
<IoUsecaseCallToAction
theme="light"
brand="nomad"
heading={callToActionHeading}
description={callToActionDescription}
links={callToActionLinks.map((link) => {
return {
text: link.title,
url: link.link,
}
})}
pattern="/img/usecase-callout-pattern.svg"
/>
</div>
{_videoCallout ? (
<div className={s.videoCallout}>
<IoVideoCallout
youtubeId={_videoCallout.youtubeId}
thumbnail={_videoCallout.thumbnail.url}
heading={_videoCallout.heading}
description={_videoCallout.description}
person={{
avatar: _videoCallout.personAvatar?.url,
name: _videoCallout.personName,
description: _videoCallout.personDescription,
}}
/>
</div>
) : null}
</>
)
}
export async function getStaticPaths() {
const { allNomadUseCases } = await rivetQuery({
query: useCasesQuery,
})
return {
paths: allNomadUseCases.map((page) => {
return {
params: {
slug: page.slug,
},
}
}),
fallback: 'blocking',
}
}
export async function getStaticProps({ params }) {
const { slug } = params
const { allNomadUseCases } = await rivetQuery({
query: useCasesQuery,
})
const page = allNomadUseCases.find((page) => page.slug === slug)
if (!page) {
return { notFound: true }
}
return {
props: {
data: page,
},
revalidate:
process.env.HASHI_ENV === 'production'
? process.env.GLOBAL_REVALIDATE
: 10,
}
}

View File

@@ -1,120 +0,0 @@
import UseCasesLayout from 'components/use-case-page'
import TextSplitWithImage from '@hashicorp/react-text-split-with-image'
import FeaturedSlider from '@hashicorp/react-featured-slider'
export default function AutomatedServiceNetworkingWithConsulPage() {
return (
<UseCasesLayout
title="Automated Service Networking with Consul"
description="Nomad natively integrates with Consul to provide automated clustering, built-in service discovery, and service mesh for secure service-to-service communications."
>
<TextSplitWithImage
textSplit={{
heading: 'Automatic Clustering',
content:
'Automatically bootstrap Nomad clusters using existing Consul agents on the same hosts.',
textSide: 'right',
links: [
{
text: 'Read More',
url:
'https://learn.hashicorp.com/tutorials/nomad/clustering#use-consul-to-automatically-cluster-nodes',
type: 'outbound',
},
],
}}
image={{
url: require('./img/automatic-clustering.png'),
alt: '',
}}
/>
<TextSplitWithImage
textSplit={{
heading: 'Automated Service Discovery',
content:
'Built-in service discovery, registration, and health check monitoring for all applications deployed under Nomad.',
textSide: 'left',
links: [
{
text: 'Read More',
url: '/docs/integrations/consul-integration#service-discovery',
type: 'inbound',
},
],
}}
image={{
url: require('./img/consul-interface.png'),
alt: '',
}}
/>
<TextSplitWithImage
textSplit={{
heading: 'Secure Service-to-Service Communication',
content:
'Enable seamless deployments of sidecar proxies and segmented microservices through Consul Connect.',
textSide: 'right',
links: [
{
text: 'Learn More',
url: '/docs/integrations/consul-connect',
type: 'inbound',
},
],
}}
image={{
url: require('./img/service-to-service.png'),
alt: '',
}}
/>
<FeaturedSlider
heading="Case Studies"
theme="dark"
features={[
{
logo: {
url:
'https://www.datocms-assets.com/2885/1582161366-deluxe-logo.svg',
alt: 'Deluxe',
},
image: {
url: require('./img/deluxe.png'),
alt: 'Deluxe Case Study',
},
heading: 'Deluxe',
content:
'Disrupt the traditional media supply chain with a digital platform powered by Nomad, Consul and Vault.',
link: {
text: 'Learn More',
url:
'https://www.hashicorp.com/resources/deluxe-hashistack-video-production',
type: 'outbound',
},
},
{
logo: {
url:
'https://www.datocms-assets.com/2885/1582161581-seatgeek.svg',
alt: 'SeatGeek',
},
image: {
url: require('./img/seatgeek.png'),
alt: 'Seat Geek Case Study',
},
heading: 'SeatGeek',
content:
'A team of 5 engineers built a infrastructure platform with Nomad, Consul, and Vault to provide tickets to millions of customers.',
link: {
text: 'Learn More',
url:
'https://www.hashicorp.com/resources/seatgeek-and-the-hashistack-a-tooling-and-automation-love-story',
type: 'outbound',
},
},
]}
/>
</UseCasesLayout>
)
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 186 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 127 KiB

View File

@@ -1,127 +0,0 @@
import UseCasesLayout from 'components/use-case-page'
import TextSplitWithImage from '@hashicorp/react-text-split-with-image'
import FeaturedSlider from '@hashicorp/react-featured-slider'
export default function NonContainerizedApplicationOrchestrationPage() {
return (
<UseCasesLayout
title="Non-Containerized Application Orchestration"
description="Nomad's flexible workload support enables an organization to run containerized, non containerized, and batch applications through a single workflow. Nomad brings core orchestration benefits to legacy applications without needing to containerize via pluggable task drivers."
>
<TextSplitWithImage
textSplit={{
heading: 'Non-Containerized Orchestration',
content:
'Deploy, manage, and scale your non-containerized applications using the Java, QEMU, or exec drivers.',
textSide: 'right',
links: [
{
text: 'Watch the Webinar',
url:
'https://www.hashicorp.com/resources/move-your-vmware-workloads-nomad',
type: 'outbound',
},
],
}}
image={{
url: require('./img/non-containerized-orch.png'),
alt: 'Non-Containerized Orchestration',
}}
/>
<TextSplitWithImage
textSplit={{
heading: 'Improve Resource Utilization with Bin Packing',
content:
'Improve resource utilization and reduce costs for non-containerized applications through Nomads bin-packing placements.',
textSide: 'left',
}}
image={{
url: require('./img/resource-utilization.png'),
alt: 'Bin Packing',
}}
/>
<TextSplitWithImage
textSplit={{
heading: 'Zero Downtime Deployments',
content:
'Apply modern upgrade strategies for legacy applications through rolling updates, blue/green, or canary deployment strategies.',
textSide: 'right',
links: [
{
text: 'Read more',
url: 'https://learn.hashicorp.com/collections/nomad/job-updates',
type: 'outbound',
},
],
}}
image={{
url: require('./img/zero-downtime.png'),
alt: '',
}}
/>
<TextSplitWithImage
textSplit={{
heading: 'Run On-Premise with Ease',
textSide: 'left',
content:
'Install and run Nomad easily on bare metal as a single binary and with the same ease as on cloud.',
}}
image={{
url: require('./img/run-on-prem-with-ease.png'),
alt: '',
}}
/>
<TextSplitWithImage
textSplit={{
heading: 'High Performance Batch Workloads',
content:
'Run batch jobs with proven scalability of thousands of deployments per second via the batch scheduler.',
textSide: 'right',
links: [
{
text: 'Watch GrayMeta tech presentation',
url:
'https://www.hashicorp.com/resources/backend-batch-processing-nomad',
type: 'outbound',
},
],
}}
image={{
url: require('./img/batch-workloads@3x.png'),
alt: '',
}}
/>
<FeaturedSlider
heading="Case Study"
theme="dark"
features={[
{
logo: {
url:
'https://www.datocms-assets.com/2885/1582149907-graymetalogo.svg',
alt: 'GrayMeta',
},
image: {
url: require('./img/grey_meta.png'),
alt: 'GrayMeta Presentation',
},
heading: 'GrayMeta',
content:
'Move an application from a traditional model of processing jobs out of a queue to scheduling them as container jobs in Nomad.',
link: {
text: 'Watch Presentation',
url:
'https://www.hashicorp.com/resources/backend-batch-processing-nomad',
type: 'outbound',
},
},
]}
/>
</UseCasesLayout>
)
}

View File

@@ -0,0 +1,94 @@
query UseCasesQuery {
allNomadUseCases {
seo: seoMeta {
title
description
image {
url
}
}
slug
heroHeading
heroDescription
challengeHeading
challengeDescription
challengeImage {
url
alt
width
height
}
challengeLink
solutionHeading
solutionDescription
solutionImage {
url
alt
width
height
}
solutionLink
customerCaseStudy {
image {
url
alt
width
height
}
logo {
url
alt
width
height
}
heading
description
link
stats {
value
label
}
}
cardsHeading
cardsDescription
tutorialsLink
tutorialCards {
eyebrow
heading
description
link
products {
name
}
}
documentationLink
documentationCards {
eyebrow
heading
description
link
products {
name
}
}
callToActionHeading
callToActionDescription
callToActionLinks {
link
title
}
videoCallout {
youtubeId
heading
description
thumbnail {
url
}
personName
personDescription
personAvatar {
url
}
}
}
}

View File

@@ -1,247 +0,0 @@
import UseCasesLayout from 'components/use-case-page'
import TextSplitWithCode from '@hashicorp/react-text-split-with-code'
import TextSplitWithImage from '@hashicorp/react-text-split-with-image'
import FeaturedSlider from '@hashicorp/react-featured-slider'
// Imports below are used in getStaticProps only
import highlightData from '@hashicorp/platform-code-highlighting/highlight-data'
export async function getStaticProps() {
const codeBlocksRaw = {
containerOrchestration: {
code:
'task "webservice" {\n driver = "docker"\n\n config {\n image = "redis:3.2"\n labels {\n group = "webservice-cache"\n }\n }\n}',
language: 'hcl',
},
windowsSupport: {
code:
'sc.exe start "Nomad"\n\nSERVICE_NAME: Nomad\n TYPE : 10 WIN32_OWN_PROCESS\n STATE : 4 RUNNING\n (STOPPABLE, NOT_PAUSABLE, ACCEPTS_SHUTDOWN)\n WIN32_EXIT_CODE : 0 (0x0)\n SERVICE_EXIT_CODE : 0 (0x0)\n CHECKPOINT : 0x0\n WAIT_HINT : 0x0\n PID : 8008\n FLAGS :',
},
multiRegionFederation: {
code: 'nomad server join 1.2.3.4:4648',
},
}
const codeBlocks = await highlightData(codeBlocksRaw)
return { props: { codeBlocks } }
}
export default function SimpleContainerOrchestrationPage({ codeBlocks }) {
return (
<UseCasesLayout
title="Simple Container Orchestration"
description="Nomad runs as a single binary with a small resource footprint. Developers use a declarative job specification to define how an application should be deployed. Nomad handles deployment and automatically recovers applications from failures."
>
<TextSplitWithCode
textSplit={{
heading: 'Container Orchestration',
textSide: 'right',
content:
'Deploy, manage, and scale your containers with the Docker, Podman, or Singularity task driver.',
links: [
{
text: 'Read More',
url: 'https://learn.hashicorp.com/collections/nomad/manage-jobs',
type: 'outbound',
},
],
}}
codeBlock={codeBlocks.containerOrchestration}
/>
<TextSplitWithImage
textSplit={{
heading: 'Run On-Premise with Ease',
textSide: 'left',
content:
'Install and run Nomad easily on bare metal as a single binary and with the same ease as on cloud.',
}}
image={{
url: require('./img/run-on-prem-with-ease.png'),
alt: '',
}}
/>
<TextSplitWithCode
textSplit={{
heading: 'Windows Support',
textSide: 'right',
content:
'Deploy Windows containers and processes or run Nomad as a native Windows service with Service Control Manager and NSSM.',
links: [
{
text: 'Watch Jet.com use case',
url:
'https://www.hashicorp.com/resources/running-windows-microservices-on-nomad-at-jet-com',
type: 'outbound',
},
],
}}
codeBlock={codeBlocks.windowsSupport}
/>
<TextSplitWithCode
textSplit={{
heading: 'Multi-Region Federation',
content:
'Federate Nomad clusters across regions with a single CLI command to deploy applications globally.',
textSide: 'left',
links: [
{
text: 'Read more',
url: 'https://learn.hashicorp.com/tutorials/nomad/federation',
type: 'outbound',
},
],
}}
codeBlock={codeBlocks.multiRegionFederation}
/>
<TextSplitWithImage
textSplit={{
heading: 'Edge Deployment with Simple Topology',
content:
'Deploy Nomad with a simple cluster topology on hybrid infrastructure to place workloads to the cloud or at the edge.',
textSide: 'right',
}}
image={{
url: require('./img/edge.png'),
alt: '',
}}
/>
<TextSplitWithImage
textSplit={{
heading: 'Zero Downtime Deployments',
content:
'Achieve zero downtime deployments for applications through rolling updates, blue/green, or canary deployment strategies.',
textSide: 'left',
links: [
{
text: 'Read more',
url: 'https://learn.hashicorp.com/collections/nomad/job-updates',
type: 'outbound',
},
],
}}
image={{
url: require('./img/zero-downtime.png'),
alt: 'Zero Downtime Deployments',
}}
/>
<TextSplitWithImage
textSplit={{
heading: 'High Performance Batch Workloads',
content:
'Run batch jobs with proven scalability of thousands of deployments per second via the batch scheduler.',
textSide: 'right',
links: [
{
text: 'Watch tech presentation from Citadel',
url:
'https://www.hashicorp.com/resources/end-to-end-production-nomad-citadel',
type: 'outbound',
},
],
}}
image={{
url: require('./img/batch-workloads@3x.png'),
alt: '',
}}
/>
<TextSplitWithImage
textSplit={{
heading: 'Run Specialized Hardware with Device Plugins',
content:
'Run GPU and other specialized workloads using Nomads device plugins.',
textSide: 'left',
links: [
{
text: 'Read more',
url: '/docs/devices',
type: 'inbound',
},
],
}}
image={{
url: require('./img/specialized-hardware.png'),
alt: 'Specialized Hardware',
}}
/>
<TextSplitWithImage
textSplit={{
heading: 'Run Stateful Workloads',
content:
'Natively connect and run stateful services with storage volumes from third-party providers via the Container Storage Interface plugin system.',
textSide: 'right',
}}
image={{
url: require('./img/stateful-workloads.png'),
alt: 'Stateful Workloads',
}}
/>
<TextSplitWithImage
textSplit={{
heading: 'Flexible Networking Capabilities',
content:
'Deploy containerized applications with customized network configurations from third-party vendors via Container Network Interface plugin system',
}}
image={{
url: require('./img/networking-capabilities.png'),
alt: 'Flexible Networking Capabilities',
}}
/>
<FeaturedSlider
heading="Case Studies"
theme="dark"
features={[
{
logo: {
url:
'https://www.datocms-assets.com/2885/1529339316-logocitadelwhite-knockout.svg',
alt: 'Citadel',
},
image: {
url:
'https://www.datocms-assets.com/2885/1509052483-hashiconf2017-end-to-end-production-nomad-at-citadel.jpg',
alt: 'Citadel Presentation',
},
heading: 'Citadel',
content:
'Optimize the cost efficiency of batch processing at scale with a hybrid, multi-cloud deployment with Nomad',
link: {
text: 'Learn More',
url:
'https://www.hashicorp.com/resources/end-to-end-production-nomad-citadel',
type: 'outbound',
},
},
{
logo: {
url:
'https://www.datocms-assets.com/2885/1594247944-better-help-white.png',
alt: 'BetterHelp',
},
image: {
url:
'https://www.datocms-assets.com/2885/1594247996-betterhelp-case-study-screen.png',
alt: 'BetterHelp Presentation',
},
heading: 'BetterHelp',
content:
'From 6 dedicated servers in a colocation facility to a cloud-based deployment workflow with Nomad',
link: {
text: 'Learn More',
url:
'https://www.hashicorp.com/resources/betterhelp-s-hashicorp-nomad-use-case/',
type: 'outbound',
},
},
]}
/>
</UseCasesLayout>
)
}

View File

@@ -0,0 +1,34 @@
.container {
composes: g-grid-container from global;
}
.callToAction {
composes: container;
margin-top: 64px;
margin-bottom: 64px;
@media (--medium-up) {
margin-top: 132px;
}
}
.videoCallout {
composes: container;
margin-top: 64px;
margin-bottom: 64px;
@media (--medium-up) {
margin-top: 132px;
}
}
.cards {
margin-top: 64px;
margin-bottom: 64px;
composes: container;
@media (--medium-up) {
margin-top: 132px;
margin-bottom: 132px;
}
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 120 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.7 KiB

View File

@@ -0,0 +1 @@
<svg width="175" height="350" viewBox="0 0 175 350" fill="none" xmlns="http://www.w3.org/2000/svg"><mask id="mask0_3_224" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="175" height="350"><path fill="#C4C4C4" d="M0 0h175v350H0z"/></mask><g mask="url(#mask0_3_224)"><g clip-path="url(#clip0_3_224)" fill="#000"><path d="M93.76 358.081 175 294.277v-1.78l-.058.046L93.5 356.492 12 292.491v1.786l81.21 63.772a.47.47 0 0 0 .269.111.45.45 0 0 0 .275-.079h.006Zm.025-21.353L175 272.95v-1.781l-.058.046L93.5 335.158 12 271.152v1.787l81.21 63.776a.444.444 0 0 0 .227.099.426.426 0 0 0 .348-.086Zm-.025 10.684L175 283.614v-1.781l-.058.046-81.438 63.948L12 281.822v1.786l81.21 63.772a.443.443 0 0 0 .545.032h.005Zm.027 31.981L175 315.611v-1.781l-.058.046-81.438 63.954L12 313.825v1.786l81.25 63.804a.461.461 0 0 0 .252.078.461.461 0 0 0 .252-.078h.002l.03-.022Zm0-53.337L175 262.28v-1.78l-.058.045L93.5 324.489 12 260.488v1.787l81.21 63.771a.47.47 0 0 0 .288.111.46.46 0 0 0 .29-.101Zm-.027 42.689L175 304.947v-1.78l-.058.046L93.5 367.161 12 303.155v1.792l81.25 63.798a.462.462 0 0 0 .252.078.462.462 0 0 0 .252-.078h.006ZM60.747 192 12 230.278v-1.786L58.47 192h2.277Zm-27.168 0L12 208.945v-1.787L31.305 192h2.274Zm-13.582 0L12 198.28v-1.785L17.724 192h2.273Zm54.156.138L12 240.942v-1.787L72.051 192h2.278l-.176.138ZM47.161 192 12 219.608v-1.786L44.884 192h2.277Zm40.582.138L12 251.616v-1.786L85.646 192h2.273l-.176.138Z"/></g><g clip-path="url(#clip1_3_224)"><path d="M126.253 0 175 38.278v-1.786L128.531 0h-2.278Zm-13.582 0 62.271 48.896.058.046v-1.787L114.949 0h-2.278Zm-13.59 0 75.861 59.57.058.046v-1.785L101.354 0h-2.273Zm40.758 0L175 27.608v-1.786L142.117 0h-2.278Zm27.164 0L175 6.28V4.495L169.276 0h-2.273Zm-13.582 0L175 16.945v-1.787L155.694 0h-2.273ZM93.707 68.544l-.007.01a.445.445 0 0 0-.2-.048.258.258 0 0 0-.055.008l-.037.007a.296.296 0 0 0-.055.013.489.489 0 0 0-.132.077L12 132.39v1.786L93.504 70.17l81.437 63.954.059.046v-1.78L93.786 68.607l-.08-.063Zm-.203 53.3a.227.227 0 0 0-.045.007.51.51 0 0 0-.254.107L12 185.725v1.787l81.499-64.001 81.442 63.943.059.047v-1.781l-81.213-63.777a.445.445 0 0 0-.283-.099Zm.144-42.645a.402.402 0 0 0-.222-.023.455.455 0 0 0-.209.1L12 143.053v1.792l81.499-64.006 81.442 63.949.059.046v-1.78L93.79 79.279a.435.435 0 0 0-.142-.08Zm.063 10.682-.007.008a.333.333 0 0 0-.24-.041.507.507 0 0 0-.258.107L12 153.722v1.786l81.499-64 81.442 63.948.059.046v-1.78L93.786 89.94l-.075-.06Zm-.252 21.301a.494.494 0 0 0-.254.106L12 175.061v1.786l81.499-64.005 81.442 63.943.059.046v-1.78l-81.213-63.777a.346.346 0 0 0-.328-.092Zm.252-10.632-.007.008a.345.345 0 0 0-.24-.041.517.517 0 0 0-.258.107L12 164.392v1.786l81.499-64 81.442 63.943.059.046v-1.78L93.786 100.61l-.075-.06Z" fill="#000"/><path d="m51.845 90.8 1.139-.894L12 57.72v1.792l39.787 31.242.058.047Zm6.794-5.332 1.134-.894L12 47.056v1.786l46.58 36.58.059.046Zm27.167-21.334 1.135-.893L12.058 4.435 12 4.39v1.786l73.748 57.913.058.046ZM72.22 74.798l1.139-.894-61.3-48.135-.059-.046v1.786l60.162 47.243.058.046Zm6.793-5.332 1.139-.893-68.094-53.474-.058-.046v1.791L78.955 69.42l.058.046Zm-13.586 10.67 1.139-.894L12 36.386v1.792l53.369 41.911.058.046ZM85.628 0 175 70.182v-1.793L87.902 0h-2.274ZM31.297 0l34.752 27.291.084.057 108.808 85.454.059.046v-1.786L33.57 0h-2.273Zm13.578 0 27.963 21.96.084.056 102.019 80.116.059.046v-1.786L47.156 0h-2.281ZM17.71 0l157.23 123.466.059.046v-1.786L19.992 0h-2.281Zm40.754 0L174.94 91.468l.059.046v-1.786L60.738 0h-2.273Zm13.582 0L174.94 80.799l.059.046V79.06L74.324 0h-2.277Z" fill="#60DEA9"/></g></g><defs><clipPath id="clip0_3_224"><path fill="#fff" transform="translate(12 192)" d="M0 0h163v192H0z"/></clipPath><clipPath id="clip1_3_224"><path fill="#fff" transform="translate(12)" d="M0 0h163v192H0z"/></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 3.7 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.3 KiB