[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
82
website/components/io-card-container/index.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
114
website/components/io-card-container/style.module.css
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
124
website/components/io-card/index.tsx
Normal 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
|
||||
34
website/components/io-card/product-logos.ts
Normal 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'),
|
||||
},
|
||||
}
|
||||
148
website/components/io-card/style.module.css
Normal 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);
|
||||
}
|
||||
}
|
||||
47
website/components/io-dialog/index.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
62
website/components/io-dialog/style.module.css
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
39
website/components/io-home-call-to-action/index.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
12
website/components/io-home-call-to-action/style.module.css
Normal 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;
|
||||
}
|
||||
}
|
||||
80
website/components/io-home-case-studies/index.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
171
website/components/io-home-case-studies/style.module.css
Normal 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;
|
||||
}
|
||||
71
website/components/io-home-feature/index.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
79
website/components/io-home-feature/style.module.css
Normal 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);
|
||||
}
|
||||
}
|
||||
135
website/components/io-home-hero/index.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
148
website/components/io-home-hero/style.module.css
Normal 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);
|
||||
}
|
||||
}
|
||||
86
website/components/io-home-in-practice/index.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
98
website/components/io-home-in-practice/style.module.css
Normal 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;
|
||||
}
|
||||
151
website/components/io-home-intro/index.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
169
website/components/io-home-intro/style.module.css
Normal 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;
|
||||
}
|
||||
}
|
||||
79
website/components/io-home-pre-footer/index.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
119
website/components/io-home-pre-footer/style.module.css
Normal 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);
|
||||
}
|
||||
}
|
||||
71
website/components/io-usecase-call-to-action/index.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
86
website/components/io-usecase-customer/index.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
118
website/components/io-usecase-customer/style.module.css
Normal 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);
|
||||
}
|
||||
41
website/components/io-usecase-hero/index.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
1
website/components/io-usecase-hero/pattern.svg
Normal file
|
After Width: | Height: | Size: 6.3 KiB |
83
website/components/io-usecase-hero/style.module.css
Normal 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);
|
||||
}
|
||||
81
website/components/io-usecase-section/index.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
106
website/components/io-usecase-section/style.module.css
Normal 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;
|
||||
}
|
||||
84
website/components/io-video-callout/index.tsx
Normal 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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
23
website/components/io-video-callout/play-icon.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
128
website/components/io-video-callout/style.module.css
Normal 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;
|
||||
}
|
||||
@@ -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
|
||||
/>
|
||||
|
||||
3
website/components/subnav/style.module.css
Normal file
@@ -0,0 +1,3 @@
|
||||
.subnav {
|
||||
border-top: 1px solid transparent;
|
||||
}
|
||||
78
website/layouts/standard/index.tsx
Normal 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 }>
|
||||
}
|
||||
}
|
||||
6
website/layouts/standard/query.graphql
Normal file
@@ -0,0 +1,6 @@
|
||||
query UseCasesQuery {
|
||||
useCaseNavItems: allNomadUseCases {
|
||||
url: slug
|
||||
text: heroHeading
|
||||
}
|
||||
}
|
||||
11
website/lib/utils.ts
Normal 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
|
||||
}
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
|
||||
@@ -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>Nomad’s 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 world’s 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:
|
||||
'We’ve 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. It’s 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 we’re 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>
|
||||
)
|
||||
}
|
||||
180
website/pages/home/index.tsx
Normal 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,
|
||||
}
|
||||
}
|
||||
109
website/pages/home/query.graphql
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
15
website/pages/home/style.module.css
Normal file
@@ -0,0 +1,15 @@
|
||||
.container {
|
||||
composes: g-grid-container from global;
|
||||
}
|
||||
|
||||
/*
|
||||
* Use cases
|
||||
*/
|
||||
|
||||
.useCases {
|
||||
margin: 60px auto;
|
||||
|
||||
@media (--medium-up) {
|
||||
margin: 120px auto;
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,3 @@
|
||||
import Homepage from './home'
|
||||
|
||||
import Homepage, { getStaticProps } from './home'
|
||||
export { getStaticProps }
|
||||
export default Homepage
|
||||
|
||||
@@ -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';
|
||||
|
||||
244
website/pages/use-cases/[slug].tsx
Normal 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,
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
Before Width: | Height: | Size: 8.0 KiB |
|
Before Width: | Height: | Size: 88 KiB |
|
Before Width: | Height: | Size: 124 KiB |
|
Before Width: | Height: | Size: 2.1 MiB |
|
Before Width: | Height: | Size: 186 KiB |
|
Before Width: | Height: | Size: 2.4 MiB |
|
Before Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 2.4 MiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 127 KiB |
@@ -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 Nomad’s 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>
|
||||
)
|
||||
}
|
||||
94
website/pages/use-cases/query.graphql
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 Nomad’s 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>
|
||||
)
|
||||
}
|
||||
34
website/pages/use-cases/style.module.css
Normal 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;
|
||||
}
|
||||
}
|
||||
1
website/public/img/home-hero-pattern.svg
Normal file
|
After Width: | Height: | Size: 120 KiB |
1
website/public/img/practice-pattern.svg
Normal file
|
After Width: | Height: | Size: 5.7 KiB |
1
website/public/img/usecase-callout-pattern.svg
Normal 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 |
1
website/public/img/usecase-hero-pattern.svg
Normal file
|
After Width: | Height: | Size: 6.3 KiB |